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#[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 pub light_client_contract_address: Cache<(), Address>,
58 pub token_contract_address: Cache<(), Address>,
59 pub finalized_hotshot_height: Cache<(), u64>,
60
61 pub upgrades: BTreeMap<Version, Upgrade>,
70 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
409pub 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#[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 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}