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