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