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