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