espresso_types/v0/impls/
instance_state.rs

1use std::{collections::BTreeMap, sync::Arc, time::Duration};
2
3use alloy::primitives::Address;
4use anyhow::{bail, Context};
5#[cfg(any(test, feature = "testing"))]
6use async_lock::RwLock;
7use async_trait::async_trait;
8use hotshot_contract_adapter::sol_types::{LightClientV3, StakeTableV2};
9use hotshot_types::{
10    data::EpochNumber, epoch_membership::EpochMembershipCoordinator, traits::states::InstanceState,
11    HotShotConfig,
12};
13use moka::future::Cache;
14use vbs::version::Version;
15
16use super::{
17    state::ValidatedState,
18    traits::{EventsPersistenceRead, MembershipPersistence, StakeTuple},
19    v0_1::NoStorage,
20    v0_3::{EventKey, IndexedStake, StakeTableEvent},
21    SeqTypes, UpgradeType, ViewBasedUpgrade,
22};
23use crate::{
24    v0::{
25        impls::StakeTableHash, traits::StateCatchup, v0_3::ChainConfig, GenesisHeader, L1BlockInfo,
26        L1Client, Timestamp, Upgrade, UpgradeMode,
27    },
28    v0_3::{RegisteredValidator, RewardAmount},
29    AuthenticatedValidatorMap, EpochCommittees, PubKey, RegisteredValidatorMap,
30};
31
32/// Represents the immutable state of a node.
33///
34/// For mutable state, use `ValidatedState`.
35#[derive(derive_more::Debug, Clone)]
36pub struct NodeState {
37    pub node_id: u64,
38    pub chain_config: ChainConfig,
39    pub l1_client: L1Client,
40    #[debug("{}", state_catchup.name())]
41    pub state_catchup: Arc<dyn StateCatchup>,
42    pub genesis_header: GenesisHeader,
43    pub genesis_state: ValidatedState,
44    pub genesis_chain_config: ChainConfig,
45    pub l1_genesis: Option<L1BlockInfo>,
46    #[debug(skip)]
47    pub coordinator: EpochMembershipCoordinator<SeqTypes>,
48    pub epoch_height: Option<u64>,
49    pub genesis_version: Version,
50    pub epoch_start_block: u64,
51
52    // some address are fetched from the stake table contract,
53    // but we can cache them for the duration of the program since we do not expect this to ever change
54    pub light_client_contract_address: Cache<(), Address>,
55    pub token_contract_address: Cache<(), Address>,
56    pub finalized_hotshot_height: Cache<(), u64>,
57
58    /// Map containing all planned and executed upgrades.
59    ///
60    /// Currently, only one upgrade can be executed at a time.
61    /// For multiple upgrades, the node needs to be restarted after each upgrade.
62    ///
63    /// This field serves as a record for planned and past upgrades,
64    /// listed in the genesis TOML file. It will be very useful if multiple upgrades
65    /// are supported in the future.
66    pub upgrades: BTreeMap<Version, Upgrade>,
67    /// Current version of the sequencer.
68    ///
69    /// This version is checked to determine if an upgrade is planned,
70    /// and which version variant for versioned types
71    /// to use in functions such as genesis.
72    /// (example: genesis returns V2 Header if version is 0.2)
73    pub current_version: Version,
74}
75
76impl NodeState {
77    pub async fn block_reward(&self, epoch: EpochNumber) -> anyhow::Result<RewardAmount> {
78        EpochCommittees::fetch_and_calculate_block_reward(epoch, self.coordinator.clone()).await
79    }
80
81    pub async fn fixed_block_reward(&self) -> anyhow::Result<RewardAmount> {
82        let coordinator = self.coordinator.clone();
83        let membership = coordinator.membership().read().await;
84        membership
85            .fixed_block_reward()
86            .context("fixed block reward not found")
87    }
88
89    pub async fn light_client_contract_address(&self) -> anyhow::Result<Address> {
90        match self.light_client_contract_address.get(&()).await {
91            Some(address) => Ok(address),
92            None => {
93                let stake_table_address = self
94                    .chain_config
95                    .stake_table_contract
96                    .context("No stake table contract in chain config")?;
97
98                let stake_table =
99                    StakeTableV2::new(stake_table_address, self.l1_client.provider.clone());
100                let light_client_contract_address = stake_table.lightClient().call().await?;
101
102                self.light_client_contract_address
103                    .insert((), light_client_contract_address)
104                    .await;
105
106                Ok(light_client_contract_address)
107            },
108        }
109    }
110
111    pub async fn token_contract_address(&self) -> anyhow::Result<Address> {
112        match self.token_contract_address.get(&()).await {
113            Some(address) => Ok(address),
114            None => {
115                let stake_table_address = self
116                    .chain_config
117                    .stake_table_contract
118                    .context("No stake table contract in chain config")?;
119
120                let stake_table =
121                    StakeTableV2::new(stake_table_address, self.l1_client.provider.clone());
122                let token_contract_address = stake_table.token().call().await?;
123
124                self.token_contract_address
125                    .insert((), token_contract_address)
126                    .await;
127
128                Ok(token_contract_address)
129            },
130        }
131    }
132
133    pub async fn finalized_hotshot_height(&self) -> anyhow::Result<u64> {
134        match self.finalized_hotshot_height.get(&()).await {
135            Some(block) => Ok(block),
136            None => {
137                let light_client_contract_address = self.light_client_contract_address().await?;
138
139                let light_client_contract = LightClientV3::new(
140                    light_client_contract_address,
141                    self.l1_client.provider.clone(),
142                );
143
144                let finalized_hotshot_height = light_client_contract
145                    .finalizedState()
146                    .call()
147                    .await?
148                    .blockHeight;
149
150                self.finalized_hotshot_height
151                    .insert((), finalized_hotshot_height)
152                    .await;
153
154                Ok(finalized_hotshot_height)
155            },
156        }
157    }
158}
159
160#[async_trait]
161impl MembershipPersistence for NoStorage {
162    async fn load_stake(&self, _epoch: EpochNumber) -> anyhow::Result<Option<StakeTuple>> {
163        Ok(None)
164    }
165
166    async fn load_latest_stake(&self, _limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
167        Ok(None)
168    }
169
170    async fn store_stake(
171        &self,
172        _epoch: EpochNumber,
173        _stake: AuthenticatedValidatorMap,
174        _block_reward: Option<RewardAmount>,
175        _stake_table_hash: Option<StakeTableHash>,
176    ) -> anyhow::Result<()> {
177        Ok(())
178    }
179
180    async fn store_events(
181        &self,
182        _l1_finalized: u64,
183        _events: Vec<(EventKey, StakeTableEvent)>,
184    ) -> anyhow::Result<()> {
185        Ok(())
186    }
187
188    async fn load_events(
189        &self,
190        _from_l1_block: u64,
191        _l1_block: u64,
192    ) -> anyhow::Result<(
193        Option<EventsPersistenceRead>,
194        Vec<(EventKey, StakeTableEvent)>,
195    )> {
196        bail!("unimplemented")
197    }
198
199    async fn delete_stake_tables(&self) -> anyhow::Result<()> {
200        Ok(())
201    }
202
203    async fn store_all_validators(
204        &self,
205        _epoch: EpochNumber,
206        _all_validators: RegisteredValidatorMap,
207    ) -> anyhow::Result<()> {
208        Ok(())
209    }
210
211    async fn load_all_validators(
212        &self,
213        _epoch: EpochNumber,
214        _offset: u64,
215        _limit: u64,
216    ) -> anyhow::Result<Vec<RegisteredValidator<PubKey>>> {
217        bail!("unimplemented")
218    }
219}
220
221impl NodeState {
222    pub fn new(
223        node_id: u64,
224        chain_config: ChainConfig,
225        l1_client: L1Client,
226        catchup: impl StateCatchup + 'static,
227        current_version: Version,
228        coordinator: EpochMembershipCoordinator<SeqTypes>,
229        genesis_version: Version,
230    ) -> Self {
231        Self {
232            node_id,
233            chain_config,
234            genesis_chain_config: chain_config,
235            l1_client,
236            state_catchup: Arc::new(catchup),
237            genesis_header: GenesisHeader {
238                timestamp: Default::default(),
239                chain_config,
240            },
241            genesis_state: ValidatedState {
242                chain_config: chain_config.into(),
243                ..Default::default()
244            },
245            l1_genesis: None,
246            upgrades: Default::default(),
247            current_version,
248            epoch_height: None,
249            coordinator,
250            genesis_version,
251            epoch_start_block: 0,
252            light_client_contract_address: Cache::builder().max_capacity(1).build(),
253            token_contract_address: Cache::builder().max_capacity(1).build(),
254            finalized_hotshot_height: if cfg!(any(test, feature = "testing")) {
255                Cache::builder()
256                    .max_capacity(1)
257                    .time_to_live(Duration::from_secs(1))
258                    .build()
259            } else {
260                Cache::builder()
261                    .max_capacity(1)
262                    .time_to_live(Duration::from_secs(30))
263                    .build()
264            },
265        }
266    }
267
268    #[cfg(any(test, feature = "testing"))]
269    pub fn mock() -> Self {
270        use hotshot_example_types::storage_types::TestStorage;
271        use versions::version;
272
273        use crate::v0_3::Fetcher;
274
275        let chain_config = ChainConfig::default();
276        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
277            .expect("Failed to create L1 client");
278
279        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
280            vec![],
281            Default::default(),
282            None,
283            Fetcher::mock(),
284            0,
285        )));
286
287        let storage = TestStorage::default();
288        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
289        Self::new(
290            0,
291            chain_config,
292            l1,
293            Arc::new(mock::MockStateCatchup::default()),
294            version(0, 1),
295            coordinator,
296            version(0, 1),
297        )
298    }
299
300    #[cfg(any(test, feature = "testing"))]
301    pub fn mock_v2() -> Self {
302        use hotshot_example_types::storage_types::TestStorage;
303        use versions::version;
304
305        use crate::v0_3::Fetcher;
306
307        let chain_config = ChainConfig::default();
308        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
309            .expect("Failed to create L1 client");
310
311        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
312            vec![],
313            Default::default(),
314            None,
315            Fetcher::mock(),
316            0,
317        )));
318        let storage = TestStorage::default();
319        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
320
321        Self::new(
322            0,
323            chain_config,
324            l1,
325            Arc::new(mock::MockStateCatchup::default()),
326            version(0, 2),
327            coordinator,
328            version(0, 2),
329        )
330    }
331
332    #[cfg(any(test, feature = "testing"))]
333    pub fn mock_v3() -> Self {
334        use hotshot_example_types::storage_types::TestStorage;
335        use versions::version;
336
337        use crate::v0_3::Fetcher;
338        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
339            .expect("Failed to create L1 client");
340
341        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
342            vec![],
343            Default::default(),
344            None,
345            Fetcher::mock(),
346            0,
347        )));
348
349        let storage = TestStorage::default();
350        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
351        Self::new(
352            0,
353            ChainConfig::default(),
354            l1,
355            mock::MockStateCatchup::default(),
356            version(0, 3),
357            coordinator,
358            version(0, 3),
359        )
360    }
361
362    pub fn with_l1(mut self, l1_client: L1Client) -> Self {
363        self.l1_client = l1_client;
364        self
365    }
366
367    pub fn with_genesis(mut self, state: ValidatedState) -> Self {
368        self.genesis_state = state;
369        self
370    }
371
372    pub fn with_chain_config(mut self, cfg: ChainConfig) -> Self {
373        self.chain_config = cfg;
374        self
375    }
376
377    pub fn with_upgrades(mut self, upgrades: BTreeMap<Version, Upgrade>) -> Self {
378        self.upgrades = upgrades;
379        self
380    }
381
382    pub fn with_current_version(mut self, version: Version) -> Self {
383        self.current_version = version;
384        self
385    }
386
387    pub fn with_genesis_version(mut self, version: Version) -> Self {
388        self.genesis_version = version;
389        self
390    }
391
392    pub fn with_epoch_height(mut self, epoch_height: u64) -> Self {
393        self.epoch_height = Some(epoch_height);
394        self
395    }
396
397    pub fn with_epoch_start_block(mut self, epoch_start_block: u64) -> Self {
398        self.epoch_start_block = epoch_start_block;
399        self
400    }
401}
402
403/// NewType to hold upgrades and some convenience behavior.
404pub struct UpgradeMap(pub BTreeMap<Version, Upgrade>);
405impl UpgradeMap {
406    pub fn chain_config(&self, version: Version) -> ChainConfig {
407        self.0
408            .get(&version)
409            .unwrap()
410            .upgrade_type
411            .chain_config()
412            .unwrap()
413    }
414}
415
416impl From<BTreeMap<Version, Upgrade>> for UpgradeMap {
417    fn from(inner: BTreeMap<Version, Upgrade>) -> Self {
418        Self(inner)
419    }
420}
421
422// This allows us to turn on `Default` on InstanceState trait
423// which is used in `HotShot` by `TestBuilderImplementation`.
424#[cfg(any(test, feature = "testing"))]
425impl Default for NodeState {
426    fn default() -> Self {
427        use hotshot_example_types::storage_types::TestStorage;
428        use versions::version;
429
430        use crate::v0_3::Fetcher;
431
432        let chain_config = ChainConfig::default();
433        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
434            .expect("Failed to create L1 client");
435
436        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
437            vec![],
438            Default::default(),
439            None,
440            Fetcher::mock(),
441            0,
442        )));
443        let storage = TestStorage::default();
444        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
445
446        Self::new(
447            1u64,
448            chain_config,
449            l1,
450            Arc::new(mock::MockStateCatchup::default()),
451            version(0, 1),
452            coordinator,
453            version(0, 1),
454        )
455    }
456}
457
458impl InstanceState for NodeState {}
459
460impl Upgrade {
461    pub fn set_hotshot_config_parameters(&self, config: &mut HotShotConfig<SeqTypes>) {
462        match &self.mode {
463            UpgradeMode::View(v) => {
464                config.start_proposing_view = v.start_proposing_view;
465                config.stop_proposing_view = v.stop_proposing_view;
466                config.start_voting_view = v.start_voting_view.unwrap_or(0);
467                config.stop_voting_view = v.stop_voting_view.unwrap_or(u64::MAX);
468                config.start_proposing_time = 0;
469                config.stop_proposing_time = u64::MAX;
470                config.start_voting_time = 0;
471                config.stop_voting_time = u64::MAX;
472            },
473            UpgradeMode::Time(t) => {
474                config.start_proposing_time = t.start_proposing_time.unix_timestamp();
475                config.stop_proposing_time = t.stop_proposing_time.unix_timestamp();
476                config.start_voting_time = t.start_voting_time.unwrap_or_default().unix_timestamp();
477                config.stop_voting_time = t
478                    .stop_voting_time
479                    .unwrap_or(Timestamp::max())
480                    .unix_timestamp();
481                config.start_proposing_view = 0;
482                config.stop_proposing_view = u64::MAX;
483                config.start_voting_view = 0;
484                config.stop_voting_view = u64::MAX;
485            },
486        }
487    }
488    pub fn pos_view_based(address: Address) -> Upgrade {
489        let chain_config = ChainConfig {
490            base_fee: 0.into(),
491            stake_table_contract: Some(address),
492            ..Default::default()
493        };
494
495        let mode = UpgradeMode::View(ViewBasedUpgrade {
496            start_voting_view: None,
497            stop_voting_view: None,
498            start_proposing_view: 200,
499            stop_proposing_view: 1000,
500        });
501
502        let upgrade_type = UpgradeType::Epoch { chain_config };
503        Upgrade { mode, upgrade_type }
504    }
505}
506
507#[cfg(any(test, feature = "testing"))]
508pub mod mock {
509    use std::collections::HashMap;
510
511    use alloy::primitives::U256;
512    use anyhow::Context;
513    use async_trait::async_trait;
514    use committable::Commitment;
515    use hotshot_types::{
516        data::ViewNumber, simple_certificate::LightClientStateUpdateCertificateV2,
517        stake_table::HSStakeTable,
518    };
519    use jf_merkle_tree_compat::{ForgetableMerkleTreeScheme, MerkleTreeScheme};
520
521    use super::*;
522    use crate::{
523        retain_accounts,
524        v0_3::{RewardAccountProofV1, RewardAccountV1, RewardMerkleCommitmentV1},
525        v0_4::{PermittedRewardMerkleTreeV2, RewardAccountV2, RewardMerkleCommitmentV2},
526        BackoffParams, BlockMerkleTree, FeeAccount, FeeAccountProof, FeeMerkleCommitment, Leaf2,
527    };
528
529    #[derive(Debug, Clone)]
530    pub struct MockStateCatchup {
531        backoff: BackoffParams,
532        state: HashMap<ViewNumber, Arc<ValidatedState>>,
533        delay: std::time::Duration,
534    }
535
536    impl Default for MockStateCatchup {
537        fn default() -> Self {
538            Self {
539                backoff: Default::default(),
540                state: Default::default(),
541                delay: std::time::Duration::ZERO,
542            }
543        }
544    }
545
546    impl FromIterator<(ViewNumber, Arc<ValidatedState>)> for MockStateCatchup {
547        fn from_iter<I: IntoIterator<Item = (ViewNumber, Arc<ValidatedState>)>>(iter: I) -> Self {
548            Self {
549                backoff: Default::default(),
550                state: iter.into_iter().collect(),
551                delay: std::time::Duration::ZERO,
552            }
553        }
554    }
555
556    impl MockStateCatchup {
557        pub fn with_delay(mut self, delay: std::time::Duration) -> Self {
558            self.delay = delay;
559            self
560        }
561    }
562
563    #[async_trait]
564    impl StateCatchup for MockStateCatchup {
565        async fn try_fetch_leaf(
566            &self,
567            _retry: usize,
568            _height: u64,
569            _stake_table: HSStakeTable<SeqTypes>,
570            _success_threshold: U256,
571        ) -> anyhow::Result<Leaf2> {
572            Err(anyhow::anyhow!("todo"))
573        }
574
575        async fn try_fetch_accounts(
576            &self,
577            _retry: usize,
578            _instance: &NodeState,
579            _height: u64,
580            view: ViewNumber,
581            fee_merkle_tree_root: FeeMerkleCommitment,
582            accounts: &[FeeAccount],
583        ) -> anyhow::Result<Vec<FeeAccountProof>> {
584            tokio::time::sleep(self.delay).await;
585
586            let src = &self.state[&view].fee_merkle_tree;
587            assert_eq!(src.commitment(), fee_merkle_tree_root);
588
589            tracing::info!("catchup: fetching accounts {accounts:?} for view {view}");
590            let tree = retain_accounts(src, accounts.iter().copied())
591                .with_context(|| "failed to retain accounts")?;
592
593            // Verify the proofs
594            let mut proofs = Vec::new();
595            for account in accounts {
596                let (proof, _) = FeeAccountProof::prove(&tree, (*account).into())
597                    .context(format!("response missing fee account {account}"))?;
598                proof.verify(&fee_merkle_tree_root).context(format!(
599                    "invalid proof for fee account {account}, root: {fee_merkle_tree_root}"
600                ))?;
601                proofs.push(proof);
602            }
603
604            Ok(proofs)
605        }
606
607        async fn try_remember_blocks_merkle_tree(
608            &self,
609            _retry: usize,
610            _instance: &NodeState,
611            _height: u64,
612            view: ViewNumber,
613            mt: &mut BlockMerkleTree,
614        ) -> anyhow::Result<()> {
615            tokio::time::sleep(self.delay).await;
616
617            tracing::info!("catchup: fetching frontier for view {view}");
618            let src = &self.state[&view].block_merkle_tree;
619
620            assert_eq!(src.commitment(), mt.commitment());
621            assert!(
622                src.num_leaves() > 0,
623                "catchup should not be triggered when blocks tree is empty"
624            );
625
626            let index = src.num_leaves() - 1;
627            let (elem, proof) = src.lookup(index).expect_ok().unwrap();
628            mt.remember(index, elem, proof.clone())
629                .expect("Proof verifies");
630
631            Ok(())
632        }
633
634        async fn try_fetch_chain_config(
635            &self,
636            _retry: usize,
637            _commitment: Commitment<ChainConfig>,
638        ) -> anyhow::Result<ChainConfig> {
639            tokio::time::sleep(self.delay).await;
640
641            Ok(ChainConfig::default())
642        }
643
644        async fn try_fetch_reward_merkle_tree_v2(
645            &self,
646            _retry: usize,
647            _height: u64,
648            _view: ViewNumber,
649            _reward_merkle_tree_root: RewardMerkleCommitmentV2,
650            _accounts: Arc<Vec<RewardAccountV2>>,
651        ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
652            anyhow::bail!("unimplemented")
653        }
654
655        async fn try_fetch_reward_accounts_v1(
656            &self,
657            _retry: usize,
658            _instance: &NodeState,
659            _height: u64,
660            _view: ViewNumber,
661            _reward_merkle_tree_root: RewardMerkleCommitmentV1,
662            _accounts: &[RewardAccountV1],
663        ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
664            anyhow::bail!("unimplemented")
665        }
666
667        async fn try_fetch_state_cert(
668            &self,
669            _retry: usize,
670            _epoch: u64,
671        ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
672            anyhow::bail!("unimplemented")
673        }
674
675        fn backoff(&self) -> &BackoffParams {
676            &self.backoff
677        }
678
679        fn name(&self) -> String {
680            "MockStateCatchup".into()
681        }
682
683        fn is_local(&self) -> bool {
684            true
685        }
686    }
687}