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