espresso_types/v0/impls/
instance_state.rs

1use std::{collections::BTreeMap, sync::Arc};
2
3use alloy::primitives::Address;
4#[cfg(any(test, feature = "testing"))]
5use async_lock::RwLock;
6use async_trait::async_trait;
7use hotshot::types::BLSPubKey;
8use hotshot_types::{
9    data::EpochNumber, epoch_membership::EpochMembershipCoordinator, traits::states::InstanceState,
10    HotShotConfig,
11};
12use indexmap::IndexMap;
13use sequencer_utils::ser::FromStringOrInteger;
14use time::OffsetDateTime;
15#[cfg(any(test, feature = "testing"))]
16use vbs::version::StaticVersionType;
17use vbs::version::Version;
18
19use super::{
20    state::ValidatedState,
21    traits::MembershipPersistence,
22    v0_1::NoStorage,
23    v0_3::{EventKey, IndexedStake, StakeTableEvent, Validator},
24    SeqTypes, TimeBasedUpgrade, UpgradeType, ViewBasedUpgrade,
25};
26use crate::v0::{
27    traits::StateCatchup, v0_99::ChainConfig, GenesisHeader, L1BlockInfo, L1Client, Timestamp,
28    Upgrade, UpgradeMode,
29};
30#[cfg(any(test, feature = "testing"))]
31use crate::EpochCommittees;
32
33/// Represents the immutable state of a node.
34///
35/// For mutable state, use `ValidatedState`.
36#[derive(derive_more::Debug, Clone)]
37pub struct NodeState {
38    pub node_id: u64,
39    pub chain_config: crate::v0_99::ChainConfig,
40    pub l1_client: L1Client,
41    #[debug("{}", state_catchup.name())]
42    pub state_catchup: Arc<dyn StateCatchup>,
43    pub genesis_header: GenesisHeader,
44    pub genesis_state: ValidatedState,
45    pub l1_genesis: Option<L1BlockInfo>,
46    #[debug(skip)]
47    pub coordinator: EpochMembershipCoordinator<SeqTypes>,
48    pub epoch_height: Option<u64>,
49
50    /// Map containing all planned and executed upgrades.
51    ///
52    /// Currently, only one upgrade can be executed at a time.
53    /// For multiple upgrades, the node needs to be restarted after each upgrade.
54    ///
55    /// This field serves as a record for planned and past upgrades,
56    /// listed in the genesis TOML file. It will be very useful if multiple upgrades
57    /// are supported in the future.
58    pub upgrades: BTreeMap<Version, Upgrade>,
59    /// Current version of the sequencer.
60    ///
61    /// This version is checked to determine if an upgrade is planned,
62    /// and which version variant for versioned types  
63    /// to use in functions such as genesis.
64    /// (example: genesis returns V2 Header if version is 0.2)
65    pub current_version: Version,
66}
67
68#[async_trait]
69impl MembershipPersistence for NoStorage {
70    async fn load_stake(
71        &self,
72        _epoch: EpochNumber,
73    ) -> anyhow::Result<Option<IndexMap<alloy::primitives::Address, Validator<BLSPubKey>>>> {
74        Ok(None)
75    }
76
77    async fn load_latest_stake(&self, _limit: u64) -> anyhow::Result<Option<Vec<IndexedStake>>> {
78        Ok(None)
79    }
80
81    async fn store_stake(
82        &self,
83        _epoch: EpochNumber,
84        _stake: IndexMap<alloy::primitives::Address, Validator<BLSPubKey>>,
85    ) -> anyhow::Result<()> {
86        Ok(())
87    }
88
89    async fn store_events(
90        &self,
91        _l1_block: u64,
92        _events: Vec<(EventKey, StakeTableEvent)>,
93    ) -> anyhow::Result<()> {
94        Ok(())
95    }
96    async fn load_events(&self) -> anyhow::Result<Option<(u64, Vec<(EventKey, StakeTableEvent)>)>> {
97        Ok(None)
98    }
99}
100
101impl NodeState {
102    pub fn new(
103        node_id: u64,
104        chain_config: ChainConfig,
105        l1_client: L1Client,
106        catchup: impl StateCatchup + 'static,
107        current_version: Version,
108        coordinator: EpochMembershipCoordinator<SeqTypes>,
109    ) -> Self {
110        Self {
111            node_id,
112            chain_config,
113            l1_client,
114            state_catchup: Arc::new(catchup),
115            genesis_header: Default::default(),
116            genesis_state: ValidatedState {
117                chain_config: chain_config.into(),
118                ..Default::default()
119            },
120            l1_genesis: None,
121            upgrades: Default::default(),
122            current_version,
123            epoch_height: None,
124            coordinator,
125        }
126    }
127
128    #[cfg(any(test, feature = "testing"))]
129    pub fn mock() -> Self {
130        use hotshot_example_types::storage_types::TestStorage;
131        use vbs::version::StaticVersion;
132
133        use crate::v0_3::StakeTableFetcher;
134
135        let chain_config = ChainConfig::default();
136        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
137            .expect("Failed to create L1 client");
138
139        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
140            vec![],
141            vec![],
142            StakeTableFetcher::mock(),
143        )));
144
145        let storage = TestStorage::default();
146        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
147        Self::new(
148            0,
149            chain_config,
150            l1,
151            Arc::new(mock::MockStateCatchup::default()),
152            StaticVersion::<0, 1>::version(),
153            coordinator,
154        )
155    }
156
157    #[cfg(any(test, feature = "testing"))]
158    pub fn mock_v2() -> Self {
159        use hotshot_example_types::storage_types::TestStorage;
160        use vbs::version::StaticVersion;
161
162        use crate::v0_3::StakeTableFetcher;
163
164        let chain_config = ChainConfig::default();
165        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
166            .expect("Failed to create L1 client");
167
168        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
169            vec![],
170            vec![],
171            StakeTableFetcher::mock(),
172        )));
173        let storage = TestStorage::default();
174        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
175
176        Self::new(
177            0,
178            chain_config,
179            l1,
180            Arc::new(mock::MockStateCatchup::default()),
181            StaticVersion::<0, 2>::version(),
182            coordinator,
183        )
184    }
185
186    #[cfg(any(test, feature = "testing"))]
187    pub fn mock_v3() -> Self {
188        use hotshot_example_types::storage_types::TestStorage;
189        use vbs::version::StaticVersion;
190
191        use crate::v0_3::StakeTableFetcher;
192        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
193            .expect("Failed to create L1 client");
194
195        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
196            vec![],
197            vec![],
198            StakeTableFetcher::mock(),
199        )));
200
201        let storage = TestStorage::default();
202        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
203        Self::new(
204            0,
205            ChainConfig::default(),
206            l1,
207            mock::MockStateCatchup::default(),
208            StaticVersion::<0, 3>::version(),
209            coordinator,
210        )
211    }
212
213    #[cfg(any(test, feature = "testing"))]
214    pub fn mock_v99() -> Self {
215        use hotshot_example_types::storage_types::TestStorage;
216        use vbs::version::StaticVersion;
217
218        use crate::v0_3::StakeTableFetcher;
219
220        let chain_config = ChainConfig::default();
221        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
222            .expect("Failed to create L1 client");
223
224        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
225            vec![],
226            vec![],
227            StakeTableFetcher::mock(),
228        )));
229        let storage = TestStorage::default();
230        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
231
232        Self::new(
233            0,
234            chain_config,
235            l1,
236            Arc::new(mock::MockStateCatchup::default()),
237            StaticVersion::<0, 99>::version(),
238            coordinator,
239        )
240    }
241
242    pub fn with_l1(mut self, l1_client: L1Client) -> Self {
243        self.l1_client = l1_client;
244        self
245    }
246
247    pub fn with_genesis(mut self, state: ValidatedState) -> Self {
248        self.genesis_state = state;
249        self
250    }
251
252    pub fn with_chain_config(mut self, cfg: ChainConfig) -> Self {
253        self.chain_config = cfg;
254        self
255    }
256
257    pub fn with_upgrades(mut self, upgrades: BTreeMap<Version, Upgrade>) -> Self {
258        self.upgrades = upgrades;
259        self
260    }
261
262    pub fn with_current_version(mut self, version: Version) -> Self {
263        self.current_version = version;
264        self
265    }
266
267    pub fn with_epoch_height(mut self, epoch_height: u64) -> Self {
268        self.epoch_height = Some(epoch_height);
269        self
270    }
271}
272
273/// NewType to hold upgrades and some convenience behavior.
274pub struct UpgradeMap(pub BTreeMap<Version, Upgrade>);
275impl UpgradeMap {
276    pub fn chain_config(&self, version: Version) -> ChainConfig {
277        self.0
278            .get(&version)
279            .unwrap()
280            .upgrade_type
281            .chain_config()
282            .unwrap()
283    }
284}
285
286impl From<BTreeMap<Version, Upgrade>> for UpgradeMap {
287    fn from(inner: BTreeMap<Version, Upgrade>) -> Self {
288        Self(inner)
289    }
290}
291
292// This allows us to turn on `Default` on InstanceState trait
293// which is used in `HotShot` by `TestBuilderImplementation`.
294#[cfg(any(test, feature = "testing"))]
295impl Default for NodeState {
296    fn default() -> Self {
297        use hotshot_example_types::storage_types::TestStorage;
298        use vbs::version::StaticVersion;
299
300        use crate::v0_3::StakeTableFetcher;
301
302        let chain_config = ChainConfig::default();
303        let l1 = L1Client::new(vec!["http://localhost:3331".parse().unwrap()])
304            .expect("Failed to create L1 client");
305
306        let membership = Arc::new(RwLock::new(EpochCommittees::new_stake(
307            vec![],
308            vec![],
309            StakeTableFetcher::mock(),
310        )));
311        let storage = TestStorage::default();
312        let coordinator = EpochMembershipCoordinator::new(membership, 100, &storage);
313
314        Self::new(
315            1u64,
316            chain_config,
317            l1,
318            Arc::new(mock::MockStateCatchup::default()),
319            StaticVersion::<0, 1>::version(),
320            coordinator,
321        )
322    }
323}
324
325impl InstanceState for NodeState {}
326
327impl Upgrade {
328    pub fn set_hotshot_config_parameters(&self, config: &mut HotShotConfig<SeqTypes>) {
329        match &self.mode {
330            UpgradeMode::View(v) => {
331                config.start_proposing_view = v.start_proposing_view;
332                config.stop_proposing_view = v.stop_proposing_view;
333                config.start_voting_view = v.start_voting_view.unwrap_or(0);
334                config.stop_voting_view = v.stop_voting_view.unwrap_or(u64::MAX);
335                config.start_proposing_time = 0;
336                config.stop_proposing_time = u64::MAX;
337                config.start_voting_time = 0;
338                config.stop_voting_time = u64::MAX;
339            },
340            UpgradeMode::Time(t) => {
341                config.start_proposing_time = t.start_proposing_time.unix_timestamp();
342                config.stop_proposing_time = t.stop_proposing_time.unix_timestamp();
343                config.start_voting_time = t.start_voting_time.unwrap_or_default().unix_timestamp();
344                config.stop_voting_time = t
345                    .stop_voting_time
346                    .unwrap_or(Timestamp::max())
347                    .unix_timestamp();
348                config.start_proposing_view = 0;
349                config.stop_proposing_view = u64::MAX;
350                config.start_voting_view = 0;
351                config.stop_voting_view = u64::MAX;
352            },
353        }
354    }
355    pub fn pos_view_based(address: Address) -> Upgrade {
356        let chain_config = ChainConfig {
357            base_fee: 0.into(),
358            stake_table_contract: Some(address),
359            ..Default::default()
360        };
361
362        let mode = UpgradeMode::View(ViewBasedUpgrade {
363            start_voting_view: None,
364            stop_voting_view: None,
365            start_proposing_view: 200,
366            stop_proposing_view: 1000,
367        });
368
369        let upgrade_type = UpgradeType::Epoch { chain_config };
370        Upgrade { mode, upgrade_type }
371    }
372
373    pub fn marketplace_time_based() -> Upgrade {
374        let now = OffsetDateTime::now_utc().unix_timestamp() as u64;
375        let mode = UpgradeMode::Time(TimeBasedUpgrade {
376            start_proposing_time: Timestamp::from_integer(now).unwrap(),
377            stop_proposing_time: Timestamp::from_integer(now + 500).unwrap(),
378            start_voting_time: None,
379            stop_voting_time: None,
380        });
381
382        let upgrade_type = UpgradeType::Marketplace {
383            chain_config: ChainConfig {
384                max_block_size: 400.into(),
385                base_fee: 2.into(),
386                bid_recipient: Some(Default::default()),
387                ..Default::default()
388            },
389        };
390        Upgrade { mode, upgrade_type }
391    }
392}
393
394#[cfg(any(test, feature = "testing"))]
395pub mod mock {
396    use std::collections::HashMap;
397
398    use alloy::primitives::U256;
399    use anyhow::Context;
400    use async_trait::async_trait;
401    use committable::Commitment;
402    use hotshot_types::{data::ViewNumber, stake_table::HSStakeTable};
403    use jf_merkle_tree::{ForgetableMerkleTreeScheme, MerkleTreeScheme};
404
405    use super::*;
406    use crate::{
407        retain_accounts,
408        v0_1::{RewardAccount, RewardAccountProof, RewardMerkleCommitment},
409        BackoffParams, BlockMerkleTree, FeeAccount, FeeAccountProof, FeeMerkleCommitment, Leaf2,
410    };
411
412    #[derive(Debug, Clone, Default)]
413    pub struct MockStateCatchup {
414        backoff: BackoffParams,
415        state: HashMap<ViewNumber, Arc<ValidatedState>>,
416    }
417
418    impl FromIterator<(ViewNumber, Arc<ValidatedState>)> for MockStateCatchup {
419        fn from_iter<I: IntoIterator<Item = (ViewNumber, Arc<ValidatedState>)>>(iter: I) -> Self {
420            Self {
421                backoff: Default::default(),
422                state: iter.into_iter().collect(),
423            }
424        }
425    }
426
427    #[async_trait]
428    impl StateCatchup for MockStateCatchup {
429        async fn try_fetch_leaf(
430            &self,
431            _retry: usize,
432            _height: u64,
433            _stake_table: HSStakeTable<SeqTypes>,
434            _success_threshold: U256,
435        ) -> anyhow::Result<Leaf2> {
436            Err(anyhow::anyhow!("todo"))
437        }
438
439        async fn try_fetch_accounts(
440            &self,
441            _retry: usize,
442            _instance: &NodeState,
443            _height: u64,
444            view: ViewNumber,
445            fee_merkle_tree_root: FeeMerkleCommitment,
446            accounts: &[FeeAccount],
447        ) -> anyhow::Result<Vec<FeeAccountProof>> {
448            let src = &self.state[&view].fee_merkle_tree;
449            assert_eq!(src.commitment(), fee_merkle_tree_root);
450
451            tracing::info!("catchup: fetching accounts {accounts:?} for view {view:?}");
452            let tree = retain_accounts(src, accounts.iter().copied())
453                .with_context(|| "failed to retain accounts")?;
454
455            // Verify the proofs
456            let mut proofs = Vec::new();
457            for account in accounts {
458                let (proof, _) = FeeAccountProof::prove(&tree, (*account).into())
459                    .context(format!("response missing fee account {account}"))?;
460                proof
461                    .verify(&fee_merkle_tree_root)
462                    .context(format!("invalid proof for fee account {account}"))?;
463                proofs.push(proof);
464            }
465
466            Ok(proofs)
467        }
468
469        async fn try_remember_blocks_merkle_tree(
470            &self,
471            _retry: usize,
472            _instance: &NodeState,
473            _height: u64,
474            view: ViewNumber,
475            mt: &mut BlockMerkleTree,
476        ) -> anyhow::Result<()> {
477            tracing::info!("catchup: fetching frontier for view {view:?}");
478            let src = &self.state[&view].block_merkle_tree;
479
480            assert_eq!(src.commitment(), mt.commitment());
481            assert!(
482                src.num_leaves() > 0,
483                "catchup should not be triggered when blocks tree is empty"
484            );
485
486            let index = src.num_leaves() - 1;
487            let (elem, proof) = src.lookup(index).expect_ok().unwrap();
488            mt.remember(index, elem, proof.clone())
489                .expect("Proof verifies");
490
491            Ok(())
492        }
493
494        async fn try_fetch_chain_config(
495            &self,
496            _retry: usize,
497            _commitment: Commitment<ChainConfig>,
498        ) -> anyhow::Result<ChainConfig> {
499            Ok(ChainConfig::default())
500        }
501
502        async fn try_fetch_reward_accounts(
503            &self,
504            _retry: usize,
505            _instance: &NodeState,
506            _height: u64,
507            _view: ViewNumber,
508            _reward_merkle_tree_root: RewardMerkleCommitment,
509            _accounts: &[RewardAccount],
510        ) -> anyhow::Result<Vec<RewardAccountProof>> {
511            anyhow::bail!("unimplemented")
512        }
513
514        fn backoff(&self) -> &BackoffParams {
515            &self.backoff
516        }
517
518        fn name(&self) -> String {
519            "MockStateCatchup".into()
520        }
521
522        fn is_local(&self) -> bool {
523            true
524        }
525    }
526}