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