Skip to main content

espresso_types/v0/impls/
instance_state.rs

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