1mod external_event_handler;
2mod message_compat_tests;
3mod proposal_fetcher;
4mod request_response;
5mod startup_catchup;
6
7pub mod api;
8pub mod catchup;
9pub mod consensus_handle;
10pub mod context;
11pub mod follower;
12pub mod genesis;
13#[cfg(any(test, feature = "testing"))]
14pub mod legacy_chain;
15pub use espresso_keyset as keyset;
16pub mod options;
17pub mod persistence;
18pub mod run;
19pub mod state;
20pub mod state_cert;
21pub mod state_signature;
22pub mod util;
23
24use std::{fmt::Debug, path::PathBuf, sync::Arc, time::Duration};
25
26use alloy::primitives::U256;
27use anyhow::Context;
28use async_lock::Mutex;
29use catchup::{ParallelStateCatchup, StatePeers};
30use context::SequencerContext;
31use dyn_clone::clone_box;
32use espresso_types::{
33 BackoffParams, EpochCommittees, EpochRewardsCalculator, L1ClientOptions, NodeState, SeqTypes,
34 ValidatedState,
35 traits::{EventConsumer, MembershipPersistence},
36 v0::traits::SequencerPersistence,
37 v0_1::{ChainId, DECAF_CHAIN_ID, MAINNET_CHAIN_ID},
38 v0_3::Fetcher,
39};
40
41pub(crate) const MAINNET_TELEMETRY_ENDPOINT: &str = "https://telemetry.main.net.espresso.network";
42pub(crate) const DECAF_TELEMETRY_ENDPOINT: &str =
43 "https://telemetry.decaf.testnet.espresso.network";
44
45pub(crate) fn default_telemetry_endpoint(chain_id: ChainId) -> Option<&'static str> {
48 if chain_id == MAINNET_CHAIN_ID {
49 Some(MAINNET_TELEMETRY_ENDPOINT)
50 } else if chain_id == DECAF_CHAIN_ID {
51 Some(DECAF_TELEMETRY_ENDPOINT)
52 } else {
53 None
54 }
55}
56
57pub use espresso_types::RECENT_STAKE_TABLES_LIMIT;
58use genesis::L1Finalized;
59pub use genesis::{Genesis, GenesisSource};
60use hotshot_new_protocol::network::{Cliquenet, message_limit};
61use hotshot_orchestrator::client::{OrchestratorClient, get_complete_config};
62use hotshot_types::{
63 ValidatorConfig,
64 addr::NetAddr,
65 data::ViewNumber,
66 epoch_membership::EpochMembershipCoordinator,
67 light_client::{StateKeyPair, StateSignKey},
68 network::NetworkConfig,
69 signature_key::{BLSPrivKey, BLSPubKey},
70 traits::{
71 metrics::{Metrics, NoMetrics},
72 node_implementation::NodeType,
73 signature_key::SignatureKey,
74 storage::Storage,
75 },
76 x25519,
77};
78use moka::future::Cache;
79use options::Identity;
80pub use options::Options;
81use proposal_fetcher::ProposalFetcherConfig;
82pub use run::main;
83use tracing::info;
84use url::Url;
85use vbs::version::StaticVersion;
86
87use crate::request_response::data_source::Storage as RequestResponseStorage;
88
89pub type SequencerApiVersion = StaticVersion<0, 1>;
90
91#[derive(Clone, Debug)]
92pub struct NetworkParams {
93 pub orchestrator_url: Url,
94 pub state_relay_server_url: Url,
95
96 pub builder_urls: Vec<Url>,
98
99 pub private_staking_key: BLSPrivKey,
100 pub private_state_key: StateSignKey,
101 pub config_peers: Option<Vec<Url>>,
102 pub catchup: CatchupParams,
103 pub bootstrap_epoch_catchup_timeout: Duration,
106 pub public_api_url: Option<Url>,
108 pub cliquenet_bind_addr: NetAddr,
110 pub cliquenet_advertise_addr: Option<NetAddr>,
112 pub x25519_secret_key: x25519::SecretKey,
114}
115
116pub struct L1Params {
117 pub urls: Vec<Url>,
118 pub options: L1ClientOptions,
119}
120
121#[derive(Clone, Debug)]
122pub struct CatchupParams {
123 pub state_peers: Vec<Url>,
124 pub backoff: BackoffParams,
125 pub base_timeout: Duration,
126 pub local_timeout: Duration,
127 pub http_delay: Duration,
128}
129
130pub(crate) enum LoadedNetworkConfig {
131 Persisted(NetworkConfig<SeqTypes>),
132 Fetched(NetworkConfig<SeqTypes>),
134}
135
136pub(crate) struct NodeStateParts<P> {
137 pub node_state: NodeState,
138 pub state_catchup: ParallelStateCatchup,
139 pub persistence: Arc<P>,
140}
141
142#[allow(clippy::too_many_arguments)]
143pub async fn init_node<P>(
144 genesis: Genesis,
145 network_params: NetworkParams,
146 metrics: Box<dyn Metrics>,
147 persistence: P,
148 l1_params: L1Params,
149 storage: Option<RequestResponseStorage>,
150 event_consumer: impl EventConsumer + 'static,
151 is_da: bool,
152 identity: Identity,
153 proposal_fetcher_config: ProposalFetcherConfig,
154 empty_block_delay: Duration,
155 leader_trace_dir: Option<PathBuf>,
156) -> anyhow::Result<SequencerContext<P>>
157where
158 P: SequencerPersistence + MembershipPersistence,
159 Arc<P>: Storage<SeqTypes>,
160{
161 metrics
163 .text_family(
164 "genesis".into(),
165 vec![
166 "base_version".into(),
167 "upgrade_version".into(),
168 "genesis_version".into(),
169 ],
170 )
171 .create(vec![
172 genesis.base_version.to_string(),
173 genesis.upgrade_version.to_string(),
174 genesis.genesis_version.to_string(),
175 ]);
176 let upgrades_family = metrics.text_family("genesis_upgrade".into(), vec!["version".into()]);
177 for version in genesis.upgrades.keys() {
178 upgrades_family.create(vec![version.to_string()]);
179 }
180
181 let info = espresso_utils::build_info!();
183 metrics
184 .text_family(
185 "version".into(),
186 vec!["rev".into(), "desc".into(), "timestamp".into()],
187 )
188 .create(vec![
189 info.git_sha.into(),
190 info.git_describe.into(),
191 info.git_commit_timestamp.into(),
192 ]);
193
194 metrics
195 .text_family(
196 "build_info".into(),
197 vec![
198 "modified".into(),
199 "branch".into(),
200 "debug".into(),
201 "features".into(),
202 ],
203 )
204 .create(vec![
205 info.git_dirty.into(),
206 info.git_branch.into(),
207 info.is_debug.to_string(),
208 env!("VERGEN_CARGO_FEATURES").into(),
209 ]);
210
211 metrics
213 .text_family(
214 "node_identity_general".into(),
215 vec![
216 "name".into(),
217 "description".into(),
218 "company_name".into(),
219 "company_website".into(),
220 "operating_system".into(),
221 "node_type".into(),
222 "network_type".into(),
223 ],
224 )
225 .create(vec![
226 identity.node_name.unwrap_or_default(),
227 identity.node_description.unwrap_or_default(),
228 identity.company_name.unwrap_or_default(),
229 identity
230 .company_website
231 .map(|u| u.into())
232 .unwrap_or_default(),
233 identity.operating_system.unwrap_or_default(),
234 identity.node_type.unwrap_or_default(),
235 identity.network_type.unwrap_or_default(),
236 ]);
237
238 metrics
240 .text_family(
241 "node_identity_location".into(),
242 vec!["country".into(), "latitude".into(), "longitude".into()],
243 )
244 .create(vec![
245 identity.country_code.unwrap_or_default(),
246 identity.latitude.map(|l| l.to_string()).unwrap_or_default(),
247 identity
248 .longitude
249 .map(|l| l.to_string())
250 .unwrap_or_default(),
251 ]);
252
253 metrics
255 .text_family(
256 "node_identity_icon".into(),
257 vec![
258 "small_1x".into(),
259 "small_2x".into(),
260 "small_3x".into(),
261 "large_1x".into(),
262 "large_2x".into(),
263 "large_3x".into(),
264 ],
265 )
266 .create(vec![
267 identity
268 .icon_14x14_1x
269 .map(|u| u.to_string())
270 .unwrap_or_default(),
271 identity
272 .icon_14x14_2x
273 .map(|u| u.to_string())
274 .unwrap_or_default(),
275 identity
276 .icon_14x14_3x
277 .map(|u| u.to_string())
278 .unwrap_or_default(),
279 identity
280 .icon_24x24_1x
281 .map(|u| u.to_string())
282 .unwrap_or_default(),
283 identity
284 .icon_24x24_2x
285 .map(|u| u.to_string())
286 .unwrap_or_default(),
287 identity
288 .icon_24x24_3x
289 .map(|u| u.to_string())
290 .unwrap_or_default(),
291 ]);
292
293 let pub_key = BLSPubKey::from_private(&network_params.private_staking_key);
295 metrics
296 .text_family("node".into(), vec!["key".into()])
297 .create(vec![pub_key.to_string()]);
298
299 let orchestrator_client = OrchestratorClient::new(network_params.orchestrator_url);
301
302 let validator_config = local_validator_config(
303 network_params.private_staking_key,
304 network_params.private_state_key,
305 &network_params.x25519_secret_key,
306 network_params.cliquenet_advertise_addr,
307 is_da,
308 );
309
310 let catchup_params = network_params.catchup;
311
312 let loaded_network_config = load_or_fetch_network_config(
313 &persistence,
314 network_params.config_peers,
315 &validator_config,
316 catchup_params.backoff,
317 catchup_params.base_timeout,
318 )
319 .await?;
320 let (mut network_config, orchestrator_peer_config, persist_config) = match loaded_network_config
321 {
322 Some(LoadedNetworkConfig::Persisted(config)) => (config, None, false),
323 Some(LoadedNetworkConfig::Fetched(config)) => (config, None, true),
324 None => {
326 tracing::warn!("loading network config from orchestrator");
327 tracing::warn!(
328 "waiting for other nodes to connect, DO NOT RESTART until fully connected"
329 );
330
331 let registration = orchestrator_registration(&validator_config)?;
332
333 let config = get_complete_config(&orchestrator_client, registration.clone())
334 .await?
335 .0;
336
337 tracing::warn!(
338 node_id = config.node_index,
339 stake_table = ?config.config.known_nodes_with_stake,
340 "loaded config",
341 );
342 tracing::warn!("all nodes connected");
343 (config, Some(registration.public_config()), true)
344 },
345 };
346
347 apply_genesis_overrides(&mut network_config, &genesis);
348
349 if !network_params.builder_urls.is_empty() {
352 network_config.config.builder_urls = network_params.builder_urls.try_into().unwrap();
353 }
354
355 let version_upgrade = versions::Upgrade::new(genesis.base_version, genesis.upgrade_version);
356 tracing::warn!("setting version_upgrade={version_upgrade}");
357
358 if persist_config {
362 persistence.save_config(&network_config).await?;
363 }
364
365 let NodeStateParts {
366 node_state: instance_state,
367 state_catchup: state_catchup_providers,
368 persistence,
369 } = init_node_state(
370 &genesis,
371 &network_config,
372 l1_params,
373 catchup_params,
374 persistence,
375 &*metrics,
376 )
377 .await?;
378 let coordinator = instance_state.coordinator.clone();
379
380 check_cliquenet_info_registered(
381 coordinator.membership(),
382 &validator_config.public_key,
383 genesis.base_version,
384 genesis.chain_config.stake_table_contract,
385 &instance_state.l1_client,
386 )
387 .await;
388
389 let (initializer, anchor_view) = persistence
393 .load_consensus_state(instance_state, version_upgrade)
394 .await?;
395
396 let block_sizes = genesis.block_sizes();
397 let cliquenet = {
398 let metrics = clone_box(&*metrics);
399 let secret_key = network_params.x25519_secret_key.into();
400 let bind_addr = network_params.cliquenet_bind_addr.clone();
401 let largest = *block_sizes
403 .values()
404 .max()
405 .expect("genesis sets a block size");
406 let max_message_size = Some(message_limit(largest));
407 let name = format!("espresso-{}", genesis.chain_config.chain_id);
408 move |upgrade| {
409 Cliquenet::create(
410 name,
411 pub_key,
412 secret_key,
413 bind_addr,
414 [],
415 max_message_size,
416 upgrade,
417 metrics,
418 )
419 }
420 };
421
422 let mut ctx = SequencerContext::init(
423 network_config,
424 version_upgrade,
425 validator_config,
426 coordinator,
427 initializer,
428 anchor_view,
429 storage,
430 state_catchup_providers,
431 persistence,
432 cliquenet,
433 Some(network_params.state_relay_server_url),
434 &*metrics,
435 genesis.stake_table.capacity,
436 event_consumer,
437 proposal_fetcher_config,
438 network_params.bootstrap_epoch_catchup_timeout,
439 empty_block_delay,
440 block_sizes,
441 leader_trace_dir,
442 )
443 .await?;
444
445 if let Some(peer_config) = orchestrator_peer_config {
446 ctx = ctx.wait_for_orchestrator(orchestrator_client, peer_config);
447 }
448
449 Ok(ctx)
450}
451
452fn local_validator_config(
466 staking_key: BLSPrivKey,
467 state_key: StateSignKey,
468 x25519_key: &x25519::SecretKey,
469 advertise_addr: Option<NetAddr>,
470 is_da: bool,
471) -> ValidatorConfig<SeqTypes> {
472 let state_key_pair = StateKeyPair::from_sign_key(state_key);
473 ValidatorConfig {
474 public_key: BLSPubKey::from_private(&staking_key),
475 private_key: staking_key,
476 stake_value: U256::ONE,
477 state_public_key: state_key_pair.ver_key(),
478 state_private_key: state_key_pair.sign_key(),
479 is_da,
480 x25519_keypair: Some(x25519::Keypair::from(x25519_key)),
481 p2p_addr: advertise_addr,
482 }
483}
484
485fn orchestrator_registration(
490 validator_config: &ValidatorConfig<SeqTypes>,
491) -> anyhow::Result<ValidatorConfig<SeqTypes>> {
492 validator_config.p2p_addr.as_ref().context(
493 "ESPRESSO_NODE_CLIQUENET_ADVERTISE_ADDRESS must be set when bootstrapping a Cliquenet \
494 network from the orchestrator",
495 )?;
496 Ok(validator_config.clone())
497}
498
499pub(crate) async fn load_or_fetch_network_config<P: SequencerPersistence>(
500 persistence: &P,
501 config_peers: Option<Vec<Url>>,
502 validator_config: &ValidatorConfig<SeqTypes>,
503 backoff: BackoffParams,
504 base_timeout: Duration,
505) -> anyhow::Result<Option<LoadedNetworkConfig>> {
506 if let Some(config) = persistence.load_config().await? {
507 tracing::warn!("loaded network config from storage, rejoining existing network");
508 return Ok(Some(LoadedNetworkConfig::Persisted(config)));
509 }
510
511 let Some(peers) = config_peers else {
512 return Ok(None);
513 };
514 tracing::warn!(?peers, "loading network config from peers");
515 let peers =
516 StatePeers::<SequencerApiVersion>::from_urls(peers, backoff, base_timeout, &NoMetrics);
517 let config = peers.fetch_config(validator_config.clone()).await?;
518
519 tracing::warn!(
520 node_id = config.node_index,
521 stake_table = ?config.config.known_nodes_with_stake,
522 "loaded config",
523 );
524 Ok(Some(LoadedNetworkConfig::Fetched(config)))
525}
526
527pub(crate) fn apply_genesis_overrides(
529 network_config: &mut NetworkConfig<SeqTypes>,
530 genesis: &Genesis,
531) {
532 if let Some(upgrade) = genesis.upgrades.get(&genesis.upgrade_version) {
533 upgrade.set_hotshot_config_parameters(&mut network_config.config);
534 }
535
536 let epoch_height = genesis.epoch_height.unwrap_or_default();
537 let drb_difficulty = genesis.drb_difficulty.unwrap_or_default();
538 let drb_upgrade_difficulty = genesis.drb_upgrade_difficulty.unwrap_or_default();
539 let epoch_start_block = genesis.epoch_start_block.unwrap_or_default();
540 let stake_table_capacity = genesis
541 .stake_table_capacity
542 .unwrap_or(hotshot_types::light_client::DEFAULT_STAKE_TABLE_CAPACITY);
543
544 tracing::warn!("setting epoch_height={epoch_height:?}");
545 tracing::warn!("setting drb_difficulty={drb_difficulty:?}");
546 tracing::warn!("setting drb_upgrade_difficulty={drb_upgrade_difficulty:?}");
547 tracing::warn!("setting epoch_start_block={epoch_start_block:?}");
548 tracing::warn!("setting stake_table_capacity={stake_table_capacity:?}");
549 network_config.config.epoch_height = epoch_height;
550 network_config.config.drb_difficulty = drb_difficulty;
551 network_config.config.drb_upgrade_difficulty = drb_upgrade_difficulty;
552 network_config.config.epoch_start_block = epoch_start_block;
553 network_config.config.stake_table_capacity = stake_table_capacity;
554
555 if let Some(da_committees) = &genesis.da_committees {
556 tracing::warn!("setting da_committees from genesis: {da_committees:?}");
557 network_config.config.da_committees = da_committees.clone();
558 }
559}
560
561pub(crate) async fn init_node_state<P>(
564 genesis: &Genesis,
565 network_config: &NetworkConfig<SeqTypes>,
566 l1_params: L1Params,
567 catchup: CatchupParams,
568 mut persistence: P,
569 metrics: &dyn Metrics,
570) -> anyhow::Result<NodeStateParts<P>>
571where
572 P: SequencerPersistence + MembershipPersistence,
573 Arc<P>: Storage<SeqTypes>,
574{
575 let first_epoch = genesis.first_epoch()?;
577
578 let l1_client = l1_params
579 .options
580 .with_metrics(metrics)
581 .connect(l1_params.urls)
582 .with_context(|| "failed to create L1 client")?;
583
584 info!("Validating fee contract");
585
586 genesis.validate_fee_contract(&l1_client).await?;
587
588 info!("Fee contract validated. Spawning L1 tasks");
589
590 l1_client.spawn_tasks().await;
591
592 info!(
593 "L1 tasks spawned. Waiting for L1 genesis: {:?}",
594 genesis.l1_finalized
595 );
596
597 let l1_genesis = match &genesis.l1_finalized {
598 L1Finalized::Block(b) => *b,
599 L1Finalized::Number { number } => l1_client.wait_for_finalized_block(*number).await,
600 L1Finalized::Timestamp { timestamp } => {
601 l1_client
602 .wait_for_finalized_block_with_timestamp(U256::from(timestamp.unix_timestamp()))
603 .await
604 },
605 };
606
607 info!("L1 genesis found: {:?}", l1_genesis);
608
609 let genesis_chain_config = genesis.header.chain_config;
610 let mut genesis_state = ValidatedState {
611 chain_config: genesis_chain_config.into(),
612 ..Default::default()
613 };
614 for (address, amount) in &genesis.accounts {
615 tracing::warn!(%address, %amount, "Prefunding account for demo");
616 genesis_state.prefund_account(*address, *amount);
617 }
618
619 let state_catchup_providers = ParallelStateCatchup::new(&[], catchup.local_timeout)
620 .with_fallback_delay(catchup.http_delay);
621 let state_peers = StatePeers::<SequencerApiVersion>::from_urls(
622 catchup.state_peers,
623 catchup.backoff,
624 catchup.base_timeout,
625 metrics,
626 );
627 state_catchup_providers.add_fallback_provider(Arc::new(state_peers));
628 match persistence
630 .clone()
631 .into_catchup_provider(BackoffParams::disabled())
632 {
633 Ok(provider) => {
634 state_catchup_providers.add_provider(Arc::new(provider));
635 },
636 Err(e) => {
637 tracing::warn!(
638 "Failed to create local catchup provider: {e:#}. Only using remote catchup."
639 );
640 },
641 };
642
643 persistence.enable_metrics(metrics);
644
645 let fetcher = Fetcher::new(
646 Arc::new(state_catchup_providers.clone()),
647 Arc::new(Mutex::new(persistence.clone())),
648 l1_client.clone(),
649 genesis.chain_config,
650 );
651
652 prefetch_stake_table_events(
654 &fetcher,
655 &l1_client,
656 &l1_genesis,
657 genesis.chain_config.stake_table_contract,
658 )
659 .await?;
660
661 info!("Spawning update loop");
662
663 fetcher.spawn_update_loop().await;
664 info!("Update loop spawned. Fetching block reward");
665
666 let block_reward = fetcher.fetch_fixed_block_reward().await.ok();
667 info!("Block reward fetched: {:?}", block_reward);
668 let epoch_height = genesis.epoch_height.unwrap_or_default();
669 let mut membership = EpochCommittees::new(
670 network_config.config.known_nodes_with_stake.clone(),
671 network_config.config.known_da_nodes.clone(),
672 block_reward,
673 fetcher,
674 epoch_height,
675 first_epoch,
676 );
677 info!("Membership created. Reloading stake");
678 membership.reload_stake(RECENT_STAKE_TABLES_LIMIT).await;
679 info!("Stake reloaded");
680
681 let persistence = Arc::new(persistence);
682 let coordinator = EpochMembershipCoordinator::new(membership, epoch_height, &persistence);
683
684 let epoch_rewards_calculator = Arc::new(Mutex::new(EpochRewardsCalculator::new()));
685
686 let node_state = NodeState {
687 chain_config: genesis.chain_config,
688 genesis_chain_config,
689 l1_client,
690 genesis_header: genesis.header.clone(),
691 genesis_state,
692 l1_genesis: Some(l1_genesis),
693 node_id: network_config.node_index,
694 upgrades: genesis.upgrades.clone(),
695 current_version: genesis.base_version,
696 epoch_height: Some(epoch_height),
697 state_catchup: Arc::new(state_catchup_providers.clone()),
698 coordinator,
699 genesis_version: genesis.genesis_version,
700 epoch_start_block: genesis.epoch_start_block.unwrap_or_default(),
701 epoch_rewards_calculator,
702 light_client_contract_address: Cache::builder().max_capacity(1).build(),
703 token_contract_address: Cache::builder().max_capacity(1).build(),
704 finalized_hotshot_height: Cache::builder()
705 .max_capacity(1)
706 .time_to_live(Duration::from_secs(30))
707 .build(),
708 };
709
710 Ok(NodeStateParts {
711 node_state,
712 state_catchup: state_catchup_providers,
713 persistence,
714 })
715}
716
717pub(crate) async fn prefetch_stake_table_events(
723 fetcher: &Fetcher,
724 l1_client: &espresso_types::v0::L1Client,
725 l1_genesis: &espresso_types::v0::L1BlockInfo,
726 stake_table_contract: Option<alloy::primitives::Address>,
727) -> anyhow::Result<()> {
728 let Some(addr) = stake_table_contract else {
729 return Ok(());
730 };
731
732 let finalized = l1_client
735 .wait_for_latest_finalized_at_least(l1_genesis.number)
736 .await;
737
738 tracing::info!(
739 target: "announce",
740 %addr,
741 to_block = finalized.number,
742 "prefetching stake table events",
743 );
744 let events = fetcher
745 .fetch_and_store_stake_table_events(addr, finalized.number)
746 .await
747 .context("prefetching stake table events")?;
748 tracing::info!(target: "announce", events = events.len(), "prefetched stake table events");
749 Ok(())
750}
751
752async fn check_cliquenet_info_registered(
757 membership: &EpochCommittees,
758 pub_key: &BLSPubKey,
759 current_version: vbs::version::Version,
760 stake_table_contract: Option<alloy::primitives::Address>,
761 l1_client: &espresso_types::v0::L1Client,
762) {
763 if current_version != versions::EPOCH_REWARD_VERSION {
764 return;
765 }
766 let Some(addr) = stake_table_contract else {
767 return;
768 };
769 let stake_table =
770 hotshot_contract_adapter::sol_types::StakeTableV3::new(addr, l1_client.provider.clone());
771 let mut major = None;
772 let mut last_err = None;
773 for attempt in 1..=3 {
774 match stake_table.getVersion().call().await {
775 Ok(v) => {
776 major = Some(v.majorVersion);
777 break;
778 },
779 Err(e) => {
780 tracing::warn!(attempt, %e, "failed to read StakeTable getVersion(), retrying");
781 last_err = Some(e);
782 if attempt < 3 {
783 tokio::time::sleep(Duration::from_secs(1)).await;
784 }
785 },
786 }
787 }
788 let Some(major) = major else {
789 tracing::warn!(
790 err = ?last_err,
791 "could not read StakeTable getVersion() after 3 attempts; skipping check"
792 );
793 return;
794 };
795 if major < 3 {
796 tracing::info!(
797 major,
798 "StakeTableV3 not deployed; skipping network-info registration check"
799 );
800 return;
801 }
802 let Some(cfg) = membership.latest_peer_config(pub_key) else {
803 return;
804 };
805 if cfg.connect_info.is_some() {
806 return;
807 }
808 tracing::error!(
809 bls_key = %pub_key,
810 "Validator has no x25519 key or p2p address registered on-chain. After the CLIQUENET \
811 upgrade activates, the validator will be excluded from the active set and stop earning \
812 rewards. To fix: (1) generate an x25519 keypair if needed (`keygen --scheme x25519 \
813 --out keys.env`), then (2) register it on-chain (`staking-cli update-network-config \
814 --x25519-key <ESPRESSO_NODE_PUBLIC_X25519_KEY> --p2p-addr <host:port>`)."
815 );
816}
817
818#[cfg(any(test, feature = "testing"))]
819pub mod testing {
820 use std::{
821 cmp::max,
822 collections::{BTreeMap, HashMap, HashSet},
823 net::Ipv4Addr,
824 time::Duration,
825 };
826
827 use alloy::{
828 network::EthereumWallet,
829 node_bindings::{Anvil, AnvilInstance},
830 primitives::{Address, U256},
831 providers::{
832 Provider, ProviderBuilder, RootProvider, WalletProvider,
833 fillers::{
834 BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
835 },
836 layers::AnvilProvider,
837 },
838 signers::{k256::ecdsa::SigningKey, local::LocalSigner},
839 };
840 use catchup::NullStateCatchup;
841 use committable::Committable;
842 use espresso_contract_deployer::{
843 Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
844 builder::{DeployerArgs, DeployerArgsBuilder},
845 network_config::light_client_genesis_from_stake_table,
846 };
847 use espresso_types::{
848 ChainConfig, FeeAccount, L1Client, NetworkConfig, PubKey, SeqTypes, Transaction, Upgrade,
849 UpgradeMap, UpgradeMode,
850 eth_signature_key::EthKeyPair,
851 v0::traits::{EventConsumer, NullEventConsumer, PersistenceOptions, StateCatchup},
852 };
853 use futures::{
854 future::join_all,
855 stream::{Stream, StreamExt},
856 };
857 use hotshot_contract_adapter::stake_table::StakeTableContractVersion;
858 use hotshot_types::{
859 HotShotConfig, PeerConfig, PeerConnectInfo,
860 data::EpochNumber,
861 event::LeafInfo,
862 light_client::StateKeyPair,
863 new_protocol::CoordinatorEvent,
864 traits::{
865 BlockPayload, EncodeBytes, block_contents::BlockHeader, metrics::NoMetrics,
866 signature_key::BuilderSignatureKey,
867 },
868 utils::epoch_from_block_number,
869 };
870 use rand::SeedableRng as _;
871 use rand_chacha::ChaCha20Rng;
872 use staking_cli::demo::{DelegationConfig, StakingKeySet, StakingTransactions};
873 use test_utils::reserve_tcp_port;
874 use tokio::time::timeout;
875 use vbs::version::Version;
876 use versions::{EPOCH_VERSION, LARGE_BLOCK_VERSION};
877
878 use super::*;
879 use crate::{
880 catchup::ParallelStateCatchup,
881 persistence::no_storage::{self, NoStorage},
882 };
883
884 const STAKE_TABLE_CAPACITY_FOR_TEST: usize = 10;
885 type AnvilFillProvider = AnvilProvider<
886 FillProvider<
887 JoinFill<
888 alloy::providers::Identity,
889 JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
890 >,
891 RootProvider,
892 >,
893 >;
894
895 pub struct TestConfigBuilder<const NUM_NODES: usize> {
896 config: HotShotConfig<SeqTypes>,
897 priv_keys: Vec<BLSPrivKey>,
898 state_key_pairs: Vec<StateKeyPair>,
899 l1_url: Url,
900 l1_opt: L1ClientOptions,
901 anvil_provider: Option<AnvilFillProvider>,
902 signer: LocalSigner<SigningKey>,
903 state_relay_url: Option<Url>,
904 upgrades: BTreeMap<Version, Upgrade>,
905 coordinator_addrs: Vec<NetAddr>,
906 contracts: Option<Contracts>,
907 }
908
909 fn select_staking_key_sets(all: Vec<StakingKeySet>, indices: &[usize]) -> Vec<StakingKeySet> {
913 let mut by_index: Vec<Option<StakingKeySet>> = all.into_iter().map(Some).collect();
914 indices
915 .iter()
916 .map(|&i| {
917 by_index
918 .get_mut(i)
919 .unwrap_or_else(|| panic!("validator index {i} out of range"))
920 .take()
921 .unwrap_or_else(|| panic!("duplicate validator index {i}"))
922 })
923 .collect()
924 }
925
926 pub async fn deploy_stake_table(
929 args: &DeployerArgs<impl Provider + WalletProvider>,
930 version: StakeTableContractVersion,
931 contracts: &mut Contracts,
932 ) -> anyhow::Result<()> {
933 match version {
934 StakeTableContractVersion::V1 => args.deploy_to_stake_table_v1(contracts).await,
935 StakeTableContractVersion::V2 => args.deploy_to_stake_table_v2(contracts).await,
936 StakeTableContractVersion::V3 => args.deploy_to_stake_table_v3(contracts).await,
937 }
938 }
939
940 pub fn staking_priv_keys(
941 priv_keys: &[BLSPrivKey],
942 state_key_pairs: &[StateKeyPair],
943 coordinator_addrs: &[NetAddr],
944 num_nodes: usize,
945 ) -> Vec<StakingKeySet> {
946 let seed = [42u8; 32];
947 let mut rng = ChaCha20Rng::from_seed(seed); let eth_key_pairs = (0..num_nodes).map(|_| SigningKey::random(&mut rng).into());
949 eth_key_pairs
950 .zip(priv_keys.iter())
951 .zip(state_key_pairs.iter())
952 .enumerate()
953 .map(|(i, ((eth, bls), state))| StakingKeySet {
954 signer: eth,
955 bls: bls.clone().into(),
956 state: state.clone(),
957 x25519: x25519::Keypair::derive_from::<PubKey>(bls)
960 .expect("x25519 keypair derivation should succeed"),
961 p2p_addr: coordinator_addrs
962 .get(i)
963 .cloned()
964 .unwrap_or_else(|| "127.0.0.1:8080".parse().unwrap()),
965 metadata_uri: None,
966 })
967 .collect()
968 }
969
970 impl<const NUM_NODES: usize> TestConfigBuilder<NUM_NODES> {
971 pub fn state_relay_url(mut self, url: Url) -> Self {
972 self.state_relay_url = Some(url);
973 self
974 }
975
976 pub fn anvil_provider(mut self, anvil: AnvilInstance) -> Self {
981 self.l1_url = anvil.endpoint().parse().unwrap();
982 let l1_client = L1Client::anvil(&anvil).expect("create l1 client");
983 let anvil_provider = AnvilProvider::new(l1_client.provider, Arc::new(anvil));
984 self.anvil_provider = Some(anvil_provider);
985 self
986 }
987
988 pub fn l1_url(mut self, l1_url: Url) -> Self {
991 self.anvil_provider = None;
992 self.l1_url = l1_url;
993 self
994 }
995
996 pub fn l1_opt(mut self, opt: L1ClientOptions) -> Self {
997 self.l1_opt = opt;
998 self
999 }
1000
1001 pub fn signer(mut self, signer: LocalSigner<SigningKey>) -> Self {
1002 self.signer = signer;
1003 self
1004 }
1005
1006 pub fn upgrades(mut self, v: Version, upgrades: BTreeMap<Version, Upgrade>) -> Self {
1007 let upgrade = upgrades.get(&v).unwrap();
1008 upgrade.set_hotshot_config_parameters(&mut self.config);
1009 self.upgrades = upgrades;
1010 self
1011 }
1012
1013 pub async fn set_upgrades(self, version: Version) -> Self {
1016 let registered: Vec<usize> = (0..NUM_NODES).collect();
1017 self.set_upgrades_with(version, StakeTableContractVersion::V3, ®istered)
1018 .await
1019 }
1020
1021 pub async fn set_upgrades_with(
1026 mut self,
1027 version: Version,
1028 stake_table_version: StakeTableContractVersion,
1029 registered: &[usize],
1030 ) -> Self {
1031 let upgrade = match version {
1032 version if version >= EPOCH_VERSION => {
1033 tracing::debug!(?version, "upgrade version");
1034 let blocks_per_epoch = self.config.epoch_height;
1035 let epoch_start_block = self.config.epoch_start_block;
1036
1037 let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table(
1038 &self.config.hotshot_stake_table(),
1039 STAKE_TABLE_CAPACITY_FOR_TEST,
1040 )
1041 .unwrap();
1042
1043 let validators = select_staking_key_sets(
1044 staking_priv_keys(
1045 &self.priv_keys,
1046 &self.state_key_pairs,
1047 &self.coordinator_addrs,
1048 NUM_NODES,
1049 ),
1050 registered,
1051 );
1052
1053 let deployer = ProviderBuilder::new()
1054 .wallet(EthereumWallet::from(self.signer.clone()))
1055 .connect_http(self.l1_url.clone());
1056
1057 let mut contracts = Contracts::new();
1058 let args = DeployerArgsBuilder::default()
1059 .deployer(deployer.clone())
1060 .rpc_url(self.l1_url.clone())
1061 .mock_light_client(true)
1062 .genesis_lc_state(genesis_state)
1063 .genesis_st_state(genesis_stake)
1064 .blocks_per_epoch(blocks_per_epoch)
1065 .epoch_start_block(epoch_start_block)
1066 .exit_escrow_period(U256::from(max(
1067 blocks_per_epoch * 15 + 100,
1068 DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
1069 )))
1070 .multisig_pauser(self.signer.address())
1071 .token_name("Espresso".to_string())
1072 .token_symbol("ESP".to_string())
1073 .initial_token_supply(U256::from(3590000000u64))
1074 .ops_timelock_delay(U256::from(0))
1075 .ops_timelock_admin(self.signer.address())
1076 .ops_timelock_proposers(vec![self.signer.address()])
1077 .ops_timelock_executors(vec![self.signer.address()])
1078 .safe_exit_timelock_delay(U256::from(10))
1079 .safe_exit_timelock_admin(self.signer.address())
1080 .safe_exit_timelock_proposers(vec![self.signer.address()])
1081 .safe_exit_timelock_executors(vec![self.signer.address()])
1082 .build()
1083 .unwrap();
1084 deploy_stake_table(&args, stake_table_version, &mut contracts)
1085 .await
1086 .expect("failed to deploy all contracts");
1087
1088 let st_addr = contracts
1089 .address(Contract::StakeTableProxy)
1090 .expect("StakeTableProxy address not found");
1091 StakingTransactions::create(
1092 self.l1_url.clone(),
1093 &deployer,
1094 st_addr,
1095 validators,
1096 None,
1097 DelegationConfig::default(),
1098 )
1099 .await
1100 .expect("stake table setup failed")
1101 .apply_all()
1102 .await
1103 .expect("send all txns failed");
1104
1105 self.contracts = Some(contracts);
1106
1107 if version >= LARGE_BLOCK_VERSION {
1108 Upgrade::large_block_view_based(st_addr)
1109 } else {
1110 Upgrade::pos_view_based(st_addr)
1111 }
1112 },
1113 _ => panic!("Upgrade not configured for version {version:?}"),
1114 };
1115
1116 let mut upgrades = std::collections::BTreeMap::new();
1117 upgrade.set_hotshot_config_parameters(&mut self.config);
1118 upgrades.insert(version, upgrade);
1119
1120 self.upgrades = upgrades;
1121 self
1122 }
1123
1124 pub fn epoch_height(mut self, epoch_height: u64) -> Self {
1125 self.config.epoch_height = epoch_height;
1126 self
1127 }
1128
1129 pub fn epoch_start_block(mut self, start_block: u64) -> Self {
1130 self.config.epoch_start_block = start_block;
1131 self
1132 }
1133
1134 pub fn builder_timeout(mut self, timeout: Duration) -> Self {
1135 self.config.builder_timeout = timeout;
1136 self
1137 }
1138
1139 pub fn next_view_timeout(mut self, timeout: Duration) -> Self {
1142 self.config.next_view_timeout = timeout.as_millis() as u64;
1143 self
1144 }
1145
1146 pub fn upgrade_proposing_views(mut self, start: u64, stop: u64) -> Self {
1148 for upgrade in self.upgrades.values_mut() {
1149 if let UpgradeMode::View(v) = &mut upgrade.mode {
1150 v.start_proposing_view = start;
1151 v.stop_proposing_view = stop;
1152 }
1153 }
1154 self.config.start_proposing_view = start;
1155 self.config.stop_proposing_view = stop;
1156 self
1157 }
1158
1159 pub fn build(self) -> TestConfig<NUM_NODES> {
1160 TestConfig {
1161 config: self.config,
1162 priv_keys: self.priv_keys,
1163 state_key_pairs: self.state_key_pairs,
1164 l1_url: self.l1_url,
1165 l1_opt: self.l1_opt,
1166 signer: self.signer,
1167 state_relay_url: self.state_relay_url,
1168 upgrades: self.upgrades,
1169 anvil_provider: self.anvil_provider,
1170 coordinator_addrs: self.coordinator_addrs,
1171 contracts: self.contracts,
1172 genesis_chain_config: None,
1173 }
1174 }
1175
1176 pub fn stake_table_capacity(mut self, stake_table_capacity: usize) -> Self {
1177 self.config.stake_table_capacity = stake_table_capacity;
1178 self
1179 }
1180 }
1181
1182 impl<const NUM_NODES: usize> Default for TestConfigBuilder<NUM_NODES> {
1183 fn default() -> Self {
1184 let num_nodes = NUM_NODES;
1185
1186 let seed = [0; 32];
1188 let (pub_keys, priv_keys): (Vec<_>, Vec<_>) = (0..num_nodes)
1189 .map(|i| <PubKey as SignatureKey>::generated_from_seed_indexed(seed, i as u64))
1190 .unzip();
1191 let state_key_pairs = (0..num_nodes)
1192 .map(|i| StateKeyPair::generate_from_seed_indexed(seed, i as u64))
1193 .collect::<Vec<_>>();
1194
1195 let coordinator_addrs: Vec<NetAddr> = (0..num_nodes)
1197 .map(|_| {
1198 let port =
1199 reserve_tcp_port().expect("OS should have ephemeral ports available");
1200 NetAddr::Inet(Ipv4Addr::LOCALHOST.into(), port)
1201 })
1202 .collect();
1203
1204 let known_nodes_with_stake = pub_keys
1205 .iter()
1206 .zip(&priv_keys)
1207 .zip(&state_key_pairs)
1208 .zip(&coordinator_addrs)
1209 .map(
1210 |(((pub_key, priv_key), state_key_pair), addr)| PeerConfig::<SeqTypes> {
1211 stake_table_entry: pub_key.stake_table_entry(U256::from(1)),
1212 state_ver_key: state_key_pair.ver_key(),
1213 connect_info: Some(PeerConnectInfo {
1214 x25519_key: x25519::Keypair::derive_from::<PubKey>(priv_key)
1215 .expect("x25519 keypair derivation should succeed")
1216 .public_key(),
1217 p2p_addr: addr.clone(),
1218 }),
1219 },
1220 )
1221 .collect::<Vec<_>>();
1222
1223 let builder_port = reserve_tcp_port().unwrap();
1224
1225 let config: HotShotConfig<SeqTypes> = HotShotConfig {
1226 fixed_leader_for_gpuvid: 0,
1227 num_nodes_with_stake: num_nodes.try_into().unwrap(),
1228 known_da_nodes: known_nodes_with_stake.clone(),
1229 da_committees: Default::default(),
1230 known_nodes_with_stake: known_nodes_with_stake.clone(),
1231 next_view_timeout: Duration::from_secs(5).as_millis() as u64,
1232 num_bootstrap: 1usize,
1233 da_staked_committee_size: num_nodes,
1234 view_sync_timeout: Duration::from_secs(1),
1235 data_request_delay: Duration::from_secs(1),
1236 builder_urls: vec1::vec1![
1237 Url::parse(&format!("http://localhost:{builder_port}")).unwrap()
1238 ],
1239 builder_timeout: Duration::from_secs(1),
1240 start_threshold: (
1241 known_nodes_with_stake.clone().len() as u64,
1242 known_nodes_with_stake.clone().len() as u64,
1243 ),
1244 start_proposing_view: 0,
1245 stop_proposing_view: 0,
1246 start_voting_view: 0,
1247 stop_voting_view: 0,
1248 start_proposing_time: 0,
1249 start_voting_time: 0,
1250 stop_proposing_time: 0,
1251 stop_voting_time: 0,
1252 epoch_height: 30,
1253 epoch_start_block: 1,
1254 stake_table_capacity: hotshot_types::light_client::DEFAULT_STAKE_TABLE_CAPACITY,
1255 drb_difficulty: 10,
1256 drb_upgrade_difficulty: 20,
1257 };
1258
1259 let anvil = Anvil::new()
1260 .args(["--slots-in-an-epoch", "0", "--balance", "1000000"])
1261 .spawn();
1262
1263 let l1_client = L1Client::anvil(&anvil).expect("failed to create l1 client");
1264 let anvil_provider = AnvilProvider::new(l1_client.provider, Arc::new(anvil));
1265
1266 let l1_signer_key = anvil_provider.anvil().keys()[0].clone();
1267 let signer = LocalSigner::from(l1_signer_key);
1268
1269 Self {
1270 config,
1271 priv_keys,
1272 state_key_pairs,
1273 l1_url: anvil_provider.anvil().endpoint().parse().unwrap(),
1274 l1_opt: L1ClientOptions {
1275 stake_table_update_interval: Duration::from_secs(5),
1276 l1_events_max_block_range: 1000,
1277 l1_polling_interval: Duration::from_secs(1),
1278 subscription_timeout: Duration::from_secs(5),
1279 ..Default::default()
1280 },
1281 anvil_provider: Some(anvil_provider),
1282 signer,
1283 state_relay_url: None,
1284 upgrades: Default::default(),
1285 coordinator_addrs,
1286 contracts: None,
1287 }
1288 }
1289 }
1290
1291 #[derive(Clone)]
1292 pub struct TestConfig<const NUM_NODES: usize> {
1293 config: HotShotConfig<SeqTypes>,
1294 priv_keys: Vec<BLSPrivKey>,
1295 state_key_pairs: Vec<StateKeyPair>,
1296 l1_url: Url,
1297 l1_opt: L1ClientOptions,
1298 anvil_provider: Option<AnvilFillProvider>,
1299 signer: LocalSigner<SigningKey>,
1300 state_relay_url: Option<Url>,
1301 upgrades: BTreeMap<Version, Upgrade>,
1302 coordinator_addrs: Vec<NetAddr>,
1304 contracts: Option<Contracts>,
1306 genesis_chain_config: Option<ChainConfig>,
1308 }
1309
1310 impl<const NUM_NODES: usize> TestConfig<NUM_NODES> {
1311 pub fn num_nodes(&self) -> usize {
1312 self.priv_keys.len()
1313 }
1314
1315 pub fn hotshot_config(&self) -> &HotShotConfig<SeqTypes> {
1316 &self.config
1317 }
1318
1319 pub fn signer(&self) -> LocalSigner<SigningKey> {
1320 self.signer.clone()
1321 }
1322
1323 pub fn l1_url(&self) -> Url {
1324 self.l1_url.clone()
1325 }
1326
1327 pub fn l1_opt(&self) -> L1ClientOptions {
1328 self.l1_opt.clone()
1329 }
1330
1331 pub fn anvil(&self) -> Option<&AnvilFillProvider> {
1332 self.anvil_provider.as_ref()
1333 }
1334
1335 pub fn get_upgrade_map(&self) -> UpgradeMap {
1336 self.upgrades.clone().into()
1337 }
1338
1339 pub fn upgrades(&self) -> BTreeMap<Version, Upgrade> {
1340 self.upgrades.clone()
1341 }
1342
1343 pub fn staking_priv_keys(&self) -> Vec<StakingKeySet> {
1344 staking_priv_keys(
1345 &self.priv_keys,
1346 &self.state_key_pairs,
1347 &self.coordinator_addrs,
1348 self.num_nodes(),
1349 )
1350 }
1351
1352 pub fn staking_key_sets(&self, indices: &[usize]) -> Vec<StakingKeySet> {
1357 select_staking_key_sets(self.staking_priv_keys(), indices)
1358 }
1359
1360 pub fn coordinator_addr(&self, i: usize) -> NetAddr {
1362 self.coordinator_addrs[i].clone()
1363 }
1364
1365 pub fn set_coordinator_addr(&mut self, i: usize, addr: NetAddr) {
1370 self.coordinator_addrs[i] = addr;
1371 }
1372
1373 pub fn set_consensus_keys(&mut self, i: usize, bls: BLSPrivKey, state: StateKeyPair) {
1388 self.priv_keys[i] = bls;
1389 self.state_key_pairs[i] = state;
1390 }
1391
1392 pub fn set_genesis_chain_config(&mut self, chain_config: ChainConfig) {
1398 self.genesis_chain_config = Some(chain_config);
1399 }
1400
1401 pub fn contracts(&self) -> Option<Contracts> {
1404 self.contracts.clone()
1405 }
1406
1407 pub fn validator_providers(
1408 &self,
1409 ) -> Vec<(Address, impl Provider + Clone + use<NUM_NODES>)> {
1410 self.staking_priv_keys()
1411 .into_iter()
1412 .map(|key_set| {
1413 (
1414 key_set.signer.address(),
1415 ProviderBuilder::new()
1416 .wallet(EthereumWallet::from(key_set.signer))
1417 .connect_http(self.l1_url.clone()),
1418 )
1419 })
1420 .collect()
1421 }
1422
1423 pub async fn init_nodes(
1424 &self,
1425 upgrade: versions::Upgrade,
1426 ) -> Vec<SequencerContext<NoStorage>> {
1427 join_all((0..self.num_nodes()).map(|i| async move {
1428 self.init_node(
1429 i,
1430 ValidatedState::default(),
1431 no_storage::Options,
1432 Some(NullStateCatchup::default()),
1433 None,
1434 &NoMetrics,
1435 STAKE_TABLE_CAPACITY_FOR_TEST,
1436 NullEventConsumer,
1437 upgrade,
1438 Default::default(),
1439 )
1440 .await
1441 }))
1442 .await
1443 }
1444
1445 pub fn known_nodes_with_stake(&self) -> &[PeerConfig<SeqTypes>] {
1446 &self.config.known_nodes_with_stake
1447 }
1448
1449 #[allow(clippy::too_many_arguments)]
1450 pub async fn init_node<P: PersistenceOptions>(
1451 &self,
1452 i: usize,
1453 state: ValidatedState,
1454 persistence_opt: P,
1455 state_peers: Option<impl StateCatchup + 'static>,
1456 storage: Option<RequestResponseStorage>,
1457 metrics: &dyn Metrics,
1458 stake_table_capacity: usize,
1459 event_consumer: impl EventConsumer + 'static,
1460 upgrade: versions::Upgrade,
1461 upgrades: BTreeMap<Version, Upgrade>,
1462 ) -> SequencerContext<P::Persistence> {
1463 self.try_init_node(
1464 i,
1465 state,
1466 persistence_opt,
1467 state_peers,
1468 storage,
1469 metrics,
1470 stake_table_capacity,
1471 event_consumer,
1472 upgrade,
1473 upgrades,
1474 )
1475 .await
1476 .expect("node initializes")
1477 }
1478
1479 #[allow(clippy::too_many_arguments)]
1481 pub async fn try_init_node<P: PersistenceOptions>(
1482 &self,
1483 i: usize,
1484 mut state: ValidatedState,
1485 mut persistence_opt: P,
1486 state_peers: Option<impl StateCatchup + 'static>,
1487 storage: Option<RequestResponseStorage>,
1488 metrics: &dyn Metrics,
1489 stake_table_capacity: usize,
1490 event_consumer: impl EventConsumer + 'static,
1491 upgrade: versions::Upgrade,
1492 upgrades: BTreeMap<Version, Upgrade>,
1493 ) -> anyhow::Result<SequencerContext<P::Persistence>> {
1494 let config = self.config.clone();
1495 let my_peer_config = &config.known_nodes_with_stake[i];
1496 let is_da = config.known_da_nodes.contains(my_peer_config);
1497
1498 let x25519_keypair = x25519::Keypair::derive_from::<PubKey>(&self.priv_keys[i])
1504 .expect("x25519 keypair derivation should succeed");
1505 let coordinator_addr = self.coordinator_addrs[i].clone();
1506
1507 let pub_key = PubKey::from_private(&self.priv_keys[i]);
1508
1509 let validator_config = ValidatorConfig {
1510 public_key: pub_key,
1511 private_key: self.priv_keys[i].clone(),
1512 stake_value: my_peer_config.stake_table_entry.stake_amount,
1513 state_public_key: self.state_key_pairs[i].ver_key(),
1514 state_private_key: self.state_key_pairs[i].sign_key(),
1515 is_da,
1516 x25519_keypair: Some(x25519_keypair.clone()),
1517 p2p_addr: Some(coordinator_addr.clone()),
1518 };
1519
1520 let builder_account = Self::builder_key().fee_account();
1522 tracing::info!(%builder_account, "prefunding builder account");
1523 state.prefund_account(builder_account, U256::MAX.into());
1524
1525 let persistence = persistence_opt.create().await.unwrap();
1526
1527 let chain_config = state.chain_config.resolve().unwrap_or_default();
1528
1529 let catchup_providers = ParallelStateCatchup::new(&[], Duration::from_secs(5));
1531
1532 if let Some(state_peers) = state_peers {
1534 catchup_providers.add_provider(Arc::new(state_peers));
1535 }
1536
1537 match persistence
1540 .clone()
1541 .into_catchup_provider(BackoffParams::disabled())
1542 {
1543 Ok(local_catchup) => {
1544 catchup_providers.add_provider(local_catchup);
1545 },
1546 Err(e) => {
1547 tracing::warn!(
1548 "Failed to create local catchup provider: {e:#}. Only using remote \
1549 catchup."
1550 );
1551 },
1552 };
1553
1554 let l1_client = self
1555 .l1_opt
1556 .clone()
1557 .connect(vec![self.l1_url.clone()])
1558 .expect("failed to create L1 client");
1559 l1_client.spawn_tasks().await;
1560
1561 let fetcher = Fetcher::new(
1562 Arc::new(catchup_providers.clone()),
1563 Arc::new(Mutex::new(persistence.clone())),
1564 l1_client.clone(),
1565 chain_config,
1566 );
1567 fetcher.spawn_update_loop().await;
1568
1569 let block_reward = fetcher.fetch_fixed_block_reward().await.ok();
1570 let mut membership = EpochCommittees::new(
1571 config.known_nodes_with_stake.clone(),
1572 config.known_da_nodes.clone(),
1573 block_reward,
1574 fetcher,
1575 config.epoch_height,
1576 EpochNumber::new(epoch_from_block_number(
1577 config.epoch_start_block,
1578 config.epoch_height,
1579 )),
1580 );
1581 membership.reload_stake(50).await;
1582
1583 let membership = Arc::new(membership);
1584 let persistence = Arc::new(persistence);
1585
1586 let coordinator = EpochMembershipCoordinator::new(
1587 membership,
1588 config.epoch_height,
1589 &persistence.clone(),
1590 );
1591
1592 let max_block_size = *chain_config.max_block_size;
1593 let mut node_state = NodeState::new(
1594 i as u64,
1595 chain_config,
1596 l1_client,
1597 Arc::new(catchup_providers.clone()),
1598 upgrade.base,
1599 coordinator.clone(),
1600 upgrade.base,
1601 )
1602 .with_current_version(upgrade.base)
1603 .with_genesis(state)
1604 .with_epoch_height(config.epoch_height)
1605 .with_upgrades(upgrades)
1606 .with_epoch_start_block(config.epoch_start_block);
1607 if let Some(genesis_chain_config) = self.genesis_chain_config {
1610 node_state.genesis_header.chain_config = genesis_chain_config;
1611 node_state.genesis_chain_config = genesis_chain_config;
1612 }
1613
1614 tracing::info!(
1615 i,
1616 key = %pub_key,
1617 state_key = %self.state_key_pairs[i].ver_key(),
1618 "starting node",
1619 );
1620
1621 let coordinator_network = move |upgrade| {
1622 Cliquenet::create(
1623 "test-coordinator",
1624 pub_key,
1625 x25519_keypair,
1626 coordinator_addr,
1627 [],
1628 Some(message_limit(max_block_size)),
1629 upgrade,
1630 Box::new(NoMetrics),
1631 )
1632 };
1633
1634 let (initializer, anchor_view) = persistence
1635 .load_consensus_state(node_state, upgrade)
1636 .await
1637 .unwrap();
1638
1639 SequencerContext::init(
1640 NetworkConfig {
1641 config,
1642 ..Default::default()
1645 },
1646 upgrade,
1647 validator_config,
1648 coordinator,
1649 initializer,
1650 anchor_view,
1651 storage,
1652 catchup_providers,
1653 persistence,
1654 coordinator_network,
1655 self.state_relay_url.clone(),
1656 metrics,
1657 stake_table_capacity,
1658 event_consumer,
1659 Default::default(),
1660 Duration::from_secs(2),
1661 Duration::from_millis(500),
1662 BTreeMap::from([(upgrade.base, max_block_size)]),
1663 None,
1664 )
1665 .await
1666 }
1667
1668 pub fn builder_key() -> EthKeyPair {
1669 FeeAccount::generated_from_seed_indexed([1; 32], 0).1
1670 }
1671 }
1672
1673 pub fn decided_leaves(event: &CoordinatorEvent<SeqTypes>) -> Option<&[LeafInfo<SeqTypes>]> {
1675 match event {
1676 CoordinatorEvent::NewDecide { leaf_infos, .. } => Some(leaf_infos),
1677 _ => None,
1678 }
1679 }
1680
1681 pub async fn wait_for_decide_on_handle(
1683 events: &mut (impl Stream<Item = CoordinatorEvent<SeqTypes>> + Unpin),
1684 submitted_txn: &Transaction,
1685 ) -> (u64, usize) {
1686 let commitment = submitted_txn.commit();
1687
1688 let mut reconstructed = HashMap::new();
1693 let mut decided_without_payload = HashSet::new();
1694
1695 let (height, size) = timeout(Duration::from_secs(120), async {
1696 loop {
1697 let event = events.next().await.unwrap();
1698 tracing::info!("Received event from handle: {event:?}");
1699
1700 if let CoordinatorEvent::BlockPayloadReconstructed {
1701 view,
1702 header,
1703 payload,
1704 } = &event
1705 {
1706 if payload
1707 .transaction_commitments(header.metadata())
1708 .contains(&commitment)
1709 {
1710 let found = (header.block_number(), payload.encode().len());
1711 if decided_without_payload.contains(view) {
1712 return found;
1713 }
1714 reconstructed.insert(*view, found);
1715 }
1716 continue;
1717 }
1718
1719 let Some(leaf_chain) = decided_leaves(&event) else {
1720 continue;
1721 };
1722 for LeafInfo { leaf, .. } in leaf_chain {
1723 let Some(payload) = leaf.block_payload() else {
1724 let view = leaf.view_number();
1725 if let Some(found) = reconstructed.remove(&view) {
1726 return found;
1727 }
1728 decided_without_payload.insert(view);
1729 continue;
1730 };
1731 if payload
1732 .transaction_commitments(leaf.block_header().metadata())
1733 .contains(&commitment)
1734 {
1735 return (leaf.block_header().block_number(), payload.encode().len());
1736 }
1737 }
1738 }
1739 })
1740 .await
1741 .unwrap_or_else(|_| {
1742 panic!("transaction {commitment} was not sequenced within the timeout")
1743 });
1744 tracing::info!(height, "transaction {commitment} sequenced");
1745 (height, size)
1746 }
1747
1748 pub async fn wait_for_epochs(
1752 events: &mut (impl futures::Stream<Item = CoordinatorEvent<SeqTypes>> + std::marker::Unpin),
1753 epoch_height: u64,
1754 target_epoch: u64,
1755 ) {
1756 tracing::info!(target_epoch, "waiting for epoch");
1757 let mut last_seen = None;
1758 while let Some(event) = events.next().await {
1759 let leaf = match event {
1761 CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos[0].leaf.clone(),
1762 _ => continue,
1763 };
1764 let epoch = leaf.epoch(epoch_height);
1765 tracing::debug!(
1766 "Node decided at height: {}, epoch: {epoch:?}",
1767 leaf.height(),
1768 );
1769
1770 if epoch > Some(EpochNumber::new(target_epoch)) {
1771 tracing::info!(target_epoch, "epoch started");
1772 return;
1773 }
1774 last_seen = Some((leaf.height(), epoch));
1775 }
1776 panic!("event stream ended before target epoch {target_epoch}, last decide: {last_seen:?}");
1777 }
1778}
1779
1780#[cfg(test)]
1781mod test {
1782 use alloy::signers::local::coins_bip39::{English, Mnemonic};
1783 use espresso_keyset::KeySet;
1784 use espresso_types::{Header, NamespaceId, TEST_UPGRADE, Transaction};
1785 use futures::StreamExt;
1786 use hotshot_types::{
1787 PeerConnectInfo, addr::NetAddr, event::LeafInfo, traits::block_contents::BlockHeader,
1788 x25519,
1789 };
1790 use test_utils::reserve_tcp_port;
1791 use testing::{TestConfigBuilder, decided_leaves, wait_for_decide_on_handle};
1792
1793 use super::{local_validator_config, orchestrator_registration};
1794 use crate::api::{
1795 Options,
1796 test_helpers::{TestNetwork, TestNetworkConfigBuilder},
1797 };
1798
1799 fn test_keys() -> KeySet {
1800 let mnemonic = Mnemonic::<English>::new_from_phrase(
1801 "test test test test test test test test test test test junk",
1802 )
1803 .unwrap();
1804 KeySet::from_mnemonic(mnemonic, Some(7)).unwrap()
1805 }
1806
1807 #[test]
1813 fn local_validator_config_records_cliquenet_identity() {
1814 let keys = test_keys();
1815 let advertise: NetAddr = "node.example.com:9977".parse().unwrap();
1816 let x25519_key = x25519::Keypair::from(&keys.x25519).public_key();
1817
1818 let with_addr = local_validator_config(
1819 keys.staking.clone(),
1820 keys.state.clone(),
1821 &keys.x25519,
1822 Some(advertise.clone()),
1823 false,
1824 );
1825 assert_eq!(
1826 with_addr.public_config().connect_info,
1827 Some(PeerConnectInfo {
1828 x25519_key,
1829 p2p_addr: advertise,
1830 })
1831 );
1832
1833 let without_addr =
1834 local_validator_config(keys.staking, keys.state, &keys.x25519, None, false);
1835 assert_eq!(
1836 without_addr
1837 .x25519_keypair
1838 .as_ref()
1839 .map(|kp| kp.public_key()),
1840 Some(x25519_key)
1841 );
1842 assert!(without_addr.public_config().connect_info.is_none());
1843 }
1844
1845 #[test]
1846 fn orchestrator_registration_publishes_connect_info() {
1847 let keys = test_keys();
1848 let advertise: NetAddr = "node.example.com:9977".parse().unwrap();
1849 let config = local_validator_config(
1850 keys.staking.clone(),
1851 keys.state.clone(),
1852 &keys.x25519,
1853 Some(advertise.clone()),
1854 false,
1855 );
1856
1857 let registration = orchestrator_registration(&config).unwrap();
1858 assert_eq!(
1859 registration
1860 .public_config()
1861 .connect_info
1862 .map(|i| i.p2p_addr),
1863 Some(advertise)
1864 );
1865
1866 let unconfigured =
1867 local_validator_config(keys.staking, keys.state, &keys.x25519, None, false);
1868 assert!(orchestrator_registration(&unconfigured).is_err());
1869 }
1870
1871 #[test]
1872 fn telemetry_endpoint_defaults_by_chain() {
1873 use super::{
1874 ChainId, DECAF_TELEMETRY_ENDPOINT, MAINNET_CHAIN_ID, MAINNET_TELEMETRY_ENDPOINT, U256,
1875 default_telemetry_endpoint,
1876 };
1877
1878 assert_eq!(
1879 default_telemetry_endpoint(MAINNET_CHAIN_ID),
1880 Some(MAINNET_TELEMETRY_ENDPOINT)
1881 );
1882 assert_eq!(
1883 default_telemetry_endpoint(DECAF_CHAIN_ID),
1884 Some(DECAF_TELEMETRY_ENDPOINT)
1885 );
1886 assert_eq!(
1888 default_telemetry_endpoint(ChainId(U256::from(999999999u64))),
1889 None
1890 );
1891 }
1892
1893 #[test]
1894 fn apply_genesis_overrides_writes_genesis_values() {
1895 use hotshot_types::light_client::DEFAULT_STAKE_TABLE_CAPACITY;
1896 use vbs::version::Version;
1897
1898 use crate::genesis::StakeTableConfig;
1899
1900 let version = Version { major: 0, minor: 1 };
1901 let mut genesis = Genesis {
1902 chain_config: Default::default(),
1903 stake_table: StakeTableConfig { capacity: 10 },
1904 accounts: Default::default(),
1905 l1_finalized: L1Finalized::Number { number: 0 },
1906 header: Default::default(),
1907 upgrades: Default::default(),
1908 base_version: version,
1909 upgrade_version: version,
1910 genesis_version: version,
1911 epoch_height: Some(30),
1912 drb_difficulty: Some(10),
1913 drb_upgrade_difficulty: Some(20),
1914 epoch_start_block: Some(100),
1915 stake_table_capacity: None,
1916 da_committees: None,
1917 };
1918 let mut config = NetworkConfig::<SeqTypes>::default();
1919
1920 apply_genesis_overrides(&mut config, &genesis);
1921 assert_eq!(config.config.epoch_height, 30);
1922 assert_eq!(config.config.drb_difficulty, 10);
1923 assert_eq!(config.config.drb_upgrade_difficulty, 20);
1924 assert_eq!(config.config.epoch_start_block, 100);
1925 assert_eq!(
1926 config.config.stake_table_capacity,
1927 DEFAULT_STAKE_TABLE_CAPACITY
1928 );
1929
1930 genesis.stake_table_capacity = Some(50);
1931 apply_genesis_overrides(&mut config, &genesis);
1932 assert_eq!(config.config.stake_table_capacity, 50);
1933 }
1934
1935 use super::*;
1936
1937 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1938 async fn test_skeleton_instantiation() {
1939 let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
1940 let config = TestNetworkConfigBuilder::default()
1941 .api_config(Options::with_port(port))
1942 .network_config(TestConfigBuilder::default().build())
1943 .build();
1944 let network = TestNetwork::new(config, TEST_UPGRADE).await;
1945 let mut events = network.server.event_stream();
1946
1947 let txn = Transaction::new(NamespaceId::from(1_u32), vec![1, 2, 3]);
1948 network
1949 .server
1950 .submit_transaction(txn.clone())
1951 .await
1952 .expect("Failed to submit transaction");
1953 tracing::info!("Submitted transaction to handle: {txn:?}");
1954
1955 wait_for_decide_on_handle(&mut events, &txn).await;
1956 }
1957
1958 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1959 async fn test_header_invariants() {
1960 let success_height = 30;
1961 let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
1962 let config = TestNetworkConfigBuilder::default()
1963 .api_config(Options::with_port(port))
1964 .network_config(TestConfigBuilder::default().build())
1965 .build();
1966 let network = TestNetwork::new(config, TEST_UPGRADE).await;
1967 let mut events = network.server.event_stream();
1968
1969 let mut parent: Option<Header> = None;
1972
1973 loop {
1974 let event = events.next().await.unwrap();
1975 tracing::info!("Received event from handle: {event:?}");
1976 let Some(leaf_chain) = decided_leaves(&event) else {
1977 continue;
1978 };
1979 tracing::info!("Got decide {leaf_chain:?}");
1980
1981 for LeafInfo { leaf, .. } in leaf_chain.iter().rev() {
1984 let header = leaf.block_header().clone();
1985 if let Some(parent) = &parent {
1986 assert_eq!(header.height(), parent.height() + 1);
1987 assert!(header.timestamp() >= parent.timestamp());
1988 assert!(header.l1_head() >= parent.l1_head());
1989 assert!(header.l1_finalized() >= parent.l1_finalized());
1990 }
1991 parent = Some(header);
1992 }
1993
1994 if parent
1995 .as_ref()
1996 .is_some_and(|p| p.height() >= success_height)
1997 {
1998 break;
1999 }
2000 }
2001 }
2002
2003 #[rstest::rstest]
2004 #[case::missing(None)]
2005 #[case::zero(Some(0))]
2006 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2007 async fn init_node_state_rejects_epochless_genesis_before_l1(
2008 #[case] epoch_height: Option<u64>,
2009 ) {
2010 use espresso_types::v0::traits::PersistenceOptions;
2011
2012 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2013 .join("../../../data/genesis/mainnet.toml");
2014 let mut genesis = Genesis::from_file(&path).unwrap();
2015 genesis.epoch_height = epoch_height;
2016 genesis.l1_finalized = L1Finalized::Number { number: u64::MAX };
2018 let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
2019 let l1_params = L1Params {
2020 urls: vec![format!("http://127.0.0.1:{port}").parse().unwrap()],
2021 options: L1ClientOptions::default(),
2022 };
2023 let catchup = CatchupParams {
2024 state_peers: vec![],
2025 backoff: Default::default(),
2026 base_timeout: Duration::from_secs(2),
2027 local_timeout: Duration::from_secs(5),
2028 http_delay: Duration::ZERO,
2029 };
2030 let tmp = tempfile::TempDir::new().unwrap();
2031 let persistence = persistence::fs::Options::new(tmp.path().into())
2032 .create()
2033 .await
2034 .unwrap();
2035
2036 let result = tokio::time::timeout(
2037 Duration::from_secs(10),
2038 init_node_state(
2039 &genesis,
2040 &NetworkConfig::<SeqTypes>::default(),
2041 l1_params,
2042 catchup,
2043 persistence,
2044 &NoMetrics,
2045 ),
2046 )
2047 .await;
2048
2049 let err = match result {
2050 Ok(Err(err)) => err,
2051 Ok(Ok(_)) => panic!("init_node_state accepted epoch_height={epoch_height:?}"),
2052 Err(_) => panic!("init_node_state did not return within 10s: it is doing L1 work"),
2053 };
2054 assert!(
2055 format!("{err:#}").contains("epoch_height"),
2056 "expected an epoch_height error, got: {err:#}"
2057 );
2058 }
2059}