Skip to main content

espresso_types/v0/impls/
instance_state.rs

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