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#[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 pub light_client_contract_address: Cache<(), Address>,
64 pub token_contract_address: Cache<(), Address>,
65 pub finalized_hotshot_height: Cache<(), u64>,
66
67 pub upgrades: BTreeMap<Version, Upgrade>,
76 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 #[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 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 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
481pub 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#[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 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}