espresso_types/v0/impls/
instance_state.rs

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