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