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