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 genesis;
12pub use espresso_keyset as keyset;
13pub mod network;
14pub mod options;
15pub mod persistence;
16pub mod run;
17pub mod state;
18pub mod state_cert;
19pub mod state_signature;
20pub mod util;
21
22use std::{fmt::Debug, marker::PhantomData, sync::Arc, time::Duration};
23
24use alloy::primitives::U256;
25use anyhow::Context;
26use async_lock::Mutex;
27use catchup::{ParallelStateCatchup, StatePeers};
28use context::SequencerContext;
29use derivative::Derivative;
30use dyn_clone::clone_box;
31use espresso_types::{
32 BackoffParams, EpochCommittees, EpochRewardsCalculator, L1ClientOptions, NodeState, PubKey,
33 SeqTypes, ValidatedState,
34 traits::{EventConsumer, MembershipPersistence},
35 v0::traits::SequencerPersistence,
36 v0_1::{ChainId, DECAF_CHAIN_ID, MAINNET_CHAIN_ID},
37 v0_3::Fetcher,
38};
39
40pub(crate) const MAINNET_TELEMETRY_ENDPOINT: &str = "https://telemetry.main.net.espresso.network";
41pub(crate) const DECAF_TELEMETRY_ENDPOINT: &str =
42 "https://telemetry.decaf.testnet.espresso.network";
43
44pub(crate) fn default_telemetry_endpoint(chain_id: ChainId) -> Option<&'static str> {
47 if chain_id == MAINNET_CHAIN_ID {
48 Some(MAINNET_TELEMETRY_ENDPOINT)
49 } else if chain_id == DECAF_CHAIN_ID {
50 Some(DECAF_TELEMETRY_ENDPOINT)
51 } else {
52 None
53 }
54}
55
56pub use espresso_types::RECENT_STAKE_TABLES_LIMIT;
57use genesis::L1Finalized;
58pub use genesis::{Genesis, GenesisSource};
59use hotshot::{
60 HotShotInitializer,
61 traits::implementations::{
62 CdnMetricsValue, CdnTopic, CombinedNetworks, GossipConfig, KeyPair, Libp2pNetwork,
63 MemoryNetwork, PushCdnNetwork, RequestResponseConfig, WrappedSignatureKey,
64 derive_libp2p_multiaddr, derive_libp2p_peer_id,
65 },
66 types::SignatureKey,
67};
68use hotshot_libp2p_networking::network::behaviours::dht::store::persistent::DhtPersistentStorage;
69use hotshot_new_protocol::network::Cliquenet;
70use hotshot_orchestrator::client::{OrchestratorClient, get_complete_config};
71use hotshot_types::{
72 ValidatorConfig,
73 addr::NetAddr,
74 data::ViewNumber,
75 epoch_membership::EpochMembershipCoordinator,
76 light_client::{StateKeyPair, StateSignKey},
77 signature_key::{BLSPrivKey, BLSPubKey},
78 traits::{
79 metrics::{Metrics, NoMetrics},
80 network::ConnectedNetwork,
81 node_implementation::{NodeImplementation, NodeType},
82 storage::Storage,
83 },
84 utils::BuilderCommitment,
85 x25519,
86};
87use libp2p::Multiaddr;
88use moka::future::Cache;
89use network::libp2p::split_off_peer_id;
90use options::Identity;
91pub use options::Options;
92use proposal_fetcher::ProposalFetcherConfig;
93pub use run::main;
94use serde::{Deserialize, Serialize};
95use tokio::select;
96use tracing::info;
97use url::Url;
98use vbs::version::StaticVersion;
99
100use crate::request_response::data_source::Storage as RequestResponseStorage;
101
102#[derive(Derivative, Serialize, Deserialize)]
104#[derivative(
105 Copy(bound = ""),
106 Debug(bound = ""),
107 Default(bound = ""),
108 PartialEq(bound = ""),
109 Eq(bound = ""),
110 Hash(bound = "")
111)]
112pub struct Node<N: ConnectedNetwork<PubKey>, P: SequencerPersistence>(PhantomData<fn(&N, &P)>);
113
114impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> Clone for Node<N, P> {
117 fn clone(&self) -> Self {
118 *self
119 }
120}
121
122pub type SequencerApiVersion = StaticVersion<0, 1>;
123
124impl<N: ConnectedNetwork<PubKey>, P: SequencerPersistence> NodeImplementation<SeqTypes>
125 for Node<N, P>
126{
127 type Network = N;
128 type Storage = Arc<P>;
129}
130
131#[derive(Clone, Debug)]
132pub struct NetworkParams {
133 pub cdn_endpoint: String,
135 pub orchestrator_url: Url,
136 pub state_relay_server_url: Url,
137
138 pub builder_urls: Vec<Url>,
140
141 pub private_staking_key: BLSPrivKey,
142 pub private_state_key: StateSignKey,
143 pub state_peers: Vec<Url>,
144 pub config_peers: Option<Vec<Url>>,
145 pub catchup_backoff: BackoffParams,
146 pub catchup_base_timeout: Duration,
148 pub local_catchup_timeout: Duration,
150 pub bootstrap_epoch_catchup_timeout: Duration,
153 pub public_api_url: Option<Url>,
155 pub cliquenet_bind_addr: NetAddr,
157 pub cliquenet_advertise_addr: Option<NetAddr>,
159 pub x25519_secret_key: x25519::SecretKey,
161 pub libp2p_advertise_address: Option<String>,
165 pub libp2p_bind_address: String,
167 pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
170
171 pub libp2p_heartbeat_interval: Duration,
173
174 pub libp2p_history_gossip: usize,
176 pub libp2p_history_length: usize,
178
179 pub libp2p_mesh_n: usize,
181 pub libp2p_mesh_n_high: usize,
183 pub libp2p_mesh_n_low: usize,
185 pub libp2p_mesh_outbound_min: usize,
187
188 pub libp2p_max_gossip_transmit_size: usize,
190
191 pub libp2p_max_direct_transmit_size: u64,
193
194 pub libp2p_max_ihave_length: usize,
196
197 pub libp2p_max_ihave_messages: usize,
199
200 pub libp2p_published_message_ids_cache_time: Duration,
202
203 pub libp2p_iwant_followup_time: Duration,
205
206 pub libp2p_max_messages_per_rpc: Option<usize>,
208
209 pub libp2p_gossip_retransmission: u32,
211
212 pub libp2p_flood_publish: bool,
214
215 pub libp2p_duplicate_cache_time: Duration,
217
218 pub libp2p_fanout_ttl: Duration,
220
221 pub libp2p_heartbeat_initial_delay: Duration,
223
224 pub libp2p_gossip_factor: f64,
226
227 pub libp2p_gossip_lazy: usize,
229
230 pub libp2p_dht_put_quorum: Option<std::num::NonZeroUsize>,
231}
232
233pub struct L1Params {
234 pub urls: Vec<Url>,
235 pub options: L1ClientOptions,
236}
237
238#[allow(clippy::too_many_arguments)]
239pub async fn init_node<P>(
240 genesis: Genesis,
241 network_params: NetworkParams,
242 metrics: Box<dyn Metrics>,
243 mut persistence: P,
244 l1_params: L1Params,
245 storage: Option<RequestResponseStorage>,
246 event_consumer: impl EventConsumer + 'static,
247 is_da: bool,
248 identity: Identity,
249 proposal_fetcher_config: ProposalFetcherConfig,
250) -> anyhow::Result<SequencerContext<network::Production, P>>
251where
252 P: SequencerPersistence + MembershipPersistence + DhtPersistentStorage,
253 Arc<P>: Storage<SeqTypes>,
254{
255 metrics
257 .text_family(
258 "genesis".into(),
259 vec![
260 "base_version".into(),
261 "upgrade_version".into(),
262 "genesis_version".into(),
263 ],
264 )
265 .create(vec![
266 genesis.base_version.to_string(),
267 genesis.upgrade_version.to_string(),
268 genesis.genesis_version.to_string(),
269 ]);
270 let upgrades_family = metrics.text_family("genesis_upgrade".into(), vec!["version".into()]);
271 for version in genesis.upgrades.keys() {
272 upgrades_family.create(vec![version.to_string()]);
273 }
274
275 let info = espresso_utils::build_info!();
277 metrics
278 .text_family(
279 "version".into(),
280 vec!["rev".into(), "desc".into(), "timestamp".into()],
281 )
282 .create(vec![
283 info.git_sha.into(),
284 info.git_describe.into(),
285 info.git_commit_timestamp.into(),
286 ]);
287
288 metrics
289 .text_family(
290 "build_info".into(),
291 vec![
292 "modified".into(),
293 "branch".into(),
294 "debug".into(),
295 "features".into(),
296 ],
297 )
298 .create(vec![
299 info.git_dirty.into(),
300 info.git_branch.into(),
301 info.is_debug.to_string(),
302 env!("VERGEN_CARGO_FEATURES").into(),
303 ]);
304
305 metrics
307 .text_family(
308 "node_identity_general".into(),
309 vec![
310 "name".into(),
311 "description".into(),
312 "company_name".into(),
313 "company_website".into(),
314 "operating_system".into(),
315 "node_type".into(),
316 "network_type".into(),
317 ],
318 )
319 .create(vec![
320 identity.node_name.unwrap_or_default(),
321 identity.node_description.unwrap_or_default(),
322 identity.company_name.unwrap_or_default(),
323 identity
324 .company_website
325 .map(|u| u.into())
326 .unwrap_or_default(),
327 identity.operating_system.unwrap_or_default(),
328 identity.node_type.unwrap_or_default(),
329 identity.network_type.unwrap_or_default(),
330 ]);
331
332 metrics
334 .text_family(
335 "node_identity_location".into(),
336 vec!["country".into(), "latitude".into(), "longitude".into()],
337 )
338 .create(vec![
339 identity.country_code.unwrap_or_default(),
340 identity.latitude.map(|l| l.to_string()).unwrap_or_default(),
341 identity
342 .longitude
343 .map(|l| l.to_string())
344 .unwrap_or_default(),
345 ]);
346
347 metrics
349 .text_family(
350 "node_identity_icon".into(),
351 vec![
352 "small_1x".into(),
353 "small_2x".into(),
354 "small_3x".into(),
355 "large_1x".into(),
356 "large_2x".into(),
357 "large_3x".into(),
358 ],
359 )
360 .create(vec![
361 identity
362 .icon_14x14_1x
363 .map(|u| u.to_string())
364 .unwrap_or_default(),
365 identity
366 .icon_14x14_2x
367 .map(|u| u.to_string())
368 .unwrap_or_default(),
369 identity
370 .icon_14x14_3x
371 .map(|u| u.to_string())
372 .unwrap_or_default(),
373 identity
374 .icon_24x24_1x
375 .map(|u| u.to_string())
376 .unwrap_or_default(),
377 identity
378 .icon_24x24_2x
379 .map(|u| u.to_string())
380 .unwrap_or_default(),
381 identity
382 .icon_24x24_3x
383 .map(|u| u.to_string())
384 .unwrap_or_default(),
385 ]);
386
387 let pub_key = BLSPubKey::from_private(&network_params.private_staking_key);
389 metrics
390 .text_family("node".into(), vec!["key".into()])
391 .create(vec![pub_key.to_string()]);
392
393 let libp2p_bind_address = derive_libp2p_multiaddr(&network_params.libp2p_bind_address)
395 .with_context(|| {
396 format!(
397 "Failed to derive Libp2p bind address of {}",
398 network_params.libp2p_bind_address
399 )
400 })?;
401 let advertise_multiaddr = network_params
402 .libp2p_advertise_address
403 .as_ref()
404 .map(|addr| {
405 derive_libp2p_multiaddr(addr)
406 .with_context(|| format!("Failed to derive Libp2p advertise address of {addr}"))
407 })
408 .transpose()?;
409 let advertise_is_global = match network_params
410 .libp2p_advertise_address
411 .as_deref()
412 .and_then(|s| s.parse::<NetAddr>().ok())
413 {
414 Some(parsed) if !parsed.is_probably_global() => {
415 tracing::error!(
416 "Libp2p advertise address {parsed} is probably not publicly routable. This is \
417 fine for local testing (demo-native, docker-compose) but is wrong for any real \
418 deployment: remote peers will fail to dial us."
419 );
420 false
421 },
422 _ => true,
423 };
424
425 let libp2p_announce_addresses: Vec<Multiaddr> = advertise_multiaddr.iter().cloned().collect();
428
429 let libp2p_external_addresses: Vec<Multiaddr> = if advertise_is_global {
434 advertise_multiaddr.iter().cloned().collect()
435 } else {
436 Vec::new()
437 };
438
439 info!("Libp2p bind address: {}", libp2p_bind_address);
440 info!("Libp2p announce addresses: {:?}", libp2p_announce_addresses);
441 info!("Libp2p external addresses: {:?}", libp2p_external_addresses);
442
443 let orchestrator_client = OrchestratorClient::new(network_params.orchestrator_url);
445 let state_key_pair = StateKeyPair::from_sign_key(network_params.private_state_key);
446
447 let mut validator_config = ValidatorConfig {
450 public_key: pub_key,
451 private_key: network_params.private_staking_key,
452 stake_value: U256::ONE,
453 state_public_key: state_key_pair.ver_key(),
454 state_private_key: state_key_pair.sign_key(),
455 is_da,
456 x25519_keypair: None,
457 p2p_addr: None,
458 };
459
460 let libp2p_public_key = derive_libp2p_peer_id::<<SeqTypes as NodeType>::SignatureKey>(
462 &validator_config.private_key,
463 )
464 .with_context(|| "Failed to derive Libp2p peer ID")?;
465
466 info!("Starting Libp2p with PeerID: {libp2p_public_key}");
468
469 let loaded_network_config_from_persistence = persistence.load_config().await?;
470 let (mut network_config, wait_for_orchestrator, persist_config) = match (
471 loaded_network_config_from_persistence,
472 network_params.config_peers,
473 ) {
474 (Some(config), _) => {
475 tracing::warn!("loaded network config from storage, rejoining existing network");
476 (config, false, false)
477 },
478 (None, Some(peers)) => {
480 tracing::warn!(?peers, "loading network config from peers");
481 let peers = StatePeers::<SequencerApiVersion>::from_urls(
482 peers,
483 network_params.catchup_backoff,
484 network_params.catchup_base_timeout,
485 &NoMetrics,
486 );
487 let config = peers.fetch_config(validator_config.clone()).await?;
488
489 tracing::warn!(
490 node_id = config.node_index,
491 stake_table = ?config.config.known_nodes_with_stake,
492 "loaded config",
493 );
494 (config, false, true)
495 },
496 (None, None) => {
498 tracing::warn!("loading network config from orchestrator");
499 tracing::warn!(
500 "waiting for other nodes to connect, DO NOT RESTART until fully connected"
501 );
502
503 if genesis.base_version >= versions::NEW_PROTOCOL_VERSION {
508 let advertise_addr = network_params.cliquenet_advertise_addr.clone().context(
509 "ESPRESSO_NODE_CLIQUENET_ADVERTISE_ADDRESS must be set when bootstrapping a \
510 Cliquenet network from the orchestrator",
511 )?;
512 validator_config.x25519_keypair =
513 Some(x25519::Keypair::from(&network_params.x25519_secret_key));
514 validator_config.p2p_addr = Some(advertise_addr);
515 }
516
517 let bootstrap_advertise_addr = libp2p_announce_addresses.first().cloned().context(
518 "ESPRESSO_NODE_LIBP2P_ADVERTISE_ADDRESS must be set when bootstrapping a libp2p \
519 network from the orchestrator",
520 )?;
521
522 let config = get_complete_config(
523 &orchestrator_client,
524 validator_config.clone(),
525 Some(bootstrap_advertise_addr),
528 Some(libp2p_public_key),
529 )
530 .await?
531 .0;
532
533 tracing::warn!(
534 node_id = config.node_index,
535 stake_table = ?config.config.known_nodes_with_stake,
536 "loaded config",
537 );
538 tracing::warn!("all nodes connected");
539 (config, true, true)
540 },
541 };
542
543 if let Some(upgrade) = genesis.upgrades.get(&genesis.upgrade_version) {
544 upgrade.set_hotshot_config_parameters(&mut network_config.config);
545 }
546
547 if !network_params.builder_urls.is_empty() {
550 network_config.config.builder_urls = network_params.builder_urls.try_into().unwrap();
551 }
552
553 let epoch_height = genesis.epoch_height.unwrap_or_default();
554 let drb_difficulty = genesis.drb_difficulty.unwrap_or_default();
555 let drb_upgrade_difficulty = genesis.drb_upgrade_difficulty.unwrap_or_default();
556 let epoch_start_block = genesis.epoch_start_block.unwrap_or_default();
557 let stake_table_capacity = genesis
558 .stake_table_capacity
559 .unwrap_or(hotshot_types::light_client::DEFAULT_STAKE_TABLE_CAPACITY);
560
561 let version_upgrade = versions::Upgrade::new(genesis.base_version, genesis.upgrade_version);
562
563 tracing::warn!("setting epoch_height={epoch_height:?}");
564 tracing::warn!("setting drb_difficulty={drb_difficulty:?}");
565 tracing::warn!("setting drb_upgrade_difficulty={drb_upgrade_difficulty:?}");
566 tracing::warn!("setting epoch_start_block={epoch_start_block:?}");
567 tracing::warn!("setting stake_table_capacity={stake_table_capacity:?}");
568 tracing::warn!("setting version_upgrade={version_upgrade}");
569 network_config.config.epoch_height = epoch_height;
570 network_config.config.drb_difficulty = drb_difficulty;
571 network_config.config.drb_upgrade_difficulty = drb_upgrade_difficulty;
572 network_config.config.epoch_start_block = epoch_start_block;
573 network_config.config.stake_table_capacity = stake_table_capacity;
574
575 if let Some(da_committees) = &genesis.da_committees {
576 tracing::warn!("setting da_committees from genesis: {da_committees:?}");
577 network_config.config.da_committees = da_committees.clone();
578 }
579
580 if persist_config {
584 persistence.save_config(&network_config).await?;
585 }
586
587 if let Some(bootstrap_nodes) = network_params.libp2p_bootstrap_nodes {
590 if let Some(libp2p_config) = network_config.libp2p_config.as_mut() {
591 libp2p_config.bootstrap_nodes = bootstrap_nodes
595 .into_iter()
596 .map(split_off_peer_id)
597 .collect::<Result<Vec<_>, _>>()
598 .with_context(|| "Failed to parse peer ID from bootstrap node")?;
599 } else {
600 tracing::warn!("No libp2p configuration found, ignoring supplied bootstrap nodes");
603 }
604 }
605
606 let node_index = network_config.node_index;
607
608 let topics = {
610 let mut topics = vec![CdnTopic::Global];
611 if is_da {
612 topics.push(CdnTopic::Da);
613 }
614 topics
615 };
616
617 let cdn_network = PushCdnNetwork::new(
619 network_params.cdn_endpoint,
620 topics,
621 KeyPair {
622 public_key: WrappedSignatureKey(validator_config.public_key),
623 private_key: validator_config.private_key.clone(),
624 },
625 CdnMetricsValue::new(&*metrics),
626 )
627 .with_context(|| format!("Failed to create CDN network {node_index}"))?;
628
629 let gossip_config = GossipConfig {
631 heartbeat_interval: network_params.libp2p_heartbeat_interval,
632 history_gossip: network_params.libp2p_history_gossip,
633 history_length: network_params.libp2p_history_length,
634 mesh_n: network_params.libp2p_mesh_n,
635 mesh_n_high: network_params.libp2p_mesh_n_high,
636 mesh_n_low: network_params.libp2p_mesh_n_low,
637 mesh_outbound_min: network_params.libp2p_mesh_outbound_min,
638 max_ihave_messages: network_params.libp2p_max_ihave_messages,
639 max_transmit_size: network_params.libp2p_max_gossip_transmit_size,
640 max_ihave_length: network_params.libp2p_max_ihave_length,
641 published_message_ids_cache_time: network_params.libp2p_published_message_ids_cache_time,
642 iwant_followup_time: network_params.libp2p_iwant_followup_time,
643 max_messages_per_rpc: network_params.libp2p_max_messages_per_rpc,
644 gossip_retransmission: network_params.libp2p_gossip_retransmission,
645 flood_publish: network_params.libp2p_flood_publish,
646 duplicate_cache_time: network_params.libp2p_duplicate_cache_time,
647 fanout_ttl: network_params.libp2p_fanout_ttl,
648 heartbeat_initial_delay: network_params.libp2p_heartbeat_initial_delay,
649 gossip_factor: network_params.libp2p_gossip_factor,
650 gossip_lazy: network_params.libp2p_gossip_lazy,
651 };
652
653 let request_response_config = RequestResponseConfig {
655 request_size_maximum: network_params.libp2p_max_direct_transmit_size,
656 response_size_maximum: network_params.libp2p_max_direct_transmit_size,
657 };
658
659 let l1_client = l1_params
660 .options
661 .with_metrics(&*metrics)
662 .connect(l1_params.urls)
663 .with_context(|| "failed to create L1 client")?;
664
665 info!("Validating fee contract");
666
667 genesis.validate_fee_contract(&l1_client).await?;
668
669 info!("Fee contract validated. Spawning L1 tasks");
670
671 l1_client.spawn_tasks().await;
672
673 info!(
674 "L1 tasks spawned. Waiting for L1 genesis: {:?}",
675 genesis.l1_finalized
676 );
677
678 let l1_genesis = match genesis.l1_finalized {
679 L1Finalized::Block(b) => b,
680 L1Finalized::Number { number } => l1_client.wait_for_finalized_block(number).await,
681 L1Finalized::Timestamp { timestamp } => {
682 l1_client
683 .wait_for_finalized_block_with_timestamp(U256::from(timestamp.unix_timestamp()))
684 .await
685 },
686 };
687
688 info!("L1 genesis found: {:?}", l1_genesis);
689
690 let genesis_chain_config = genesis.header.chain_config;
691 let mut genesis_state = ValidatedState {
692 chain_config: genesis_chain_config.into(),
693 ..Default::default()
694 };
695 for (address, amount) in genesis.accounts {
696 tracing::warn!(%address, %amount, "Prefunding account for demo");
697 genesis_state.prefund_account(address, amount);
698 }
699
700 let state_catchup_providers =
702 ParallelStateCatchup::new(&[], network_params.local_catchup_timeout);
703
704 let state_peers = StatePeers::<SequencerApiVersion>::from_urls(
706 network_params.state_peers,
707 network_params.catchup_backoff,
708 network_params.catchup_base_timeout,
709 &*metrics,
710 );
711 state_catchup_providers.add_provider(Arc::new(state_peers));
712
713 match persistence
715 .clone()
716 .into_catchup_provider(network_params.catchup_backoff)
717 {
718 Ok(catchup) => {
719 state_catchup_providers.add_provider(Arc::new(catchup));
720 },
721 Err(e) => {
722 tracing::warn!(
723 "Failed to create local catchup provider: {e:#}. Only using remote catchup."
724 );
725 },
726 };
727
728 persistence.enable_metrics(&*metrics);
729
730 let fetcher = Fetcher::new(
731 Arc::new(state_catchup_providers.clone()),
732 Arc::new(Mutex::new(persistence.clone())),
733 l1_client.clone(),
734 genesis.chain_config,
735 );
736
737 info!("Spawning update loop");
738
739 fetcher.spawn_update_loop().await;
740 info!("Update loop spawned. Fetching block reward");
741
742 let block_reward = fetcher.fetch_fixed_block_reward().await.ok();
743 info!("Block reward fetched: {:?}", block_reward);
744 let mut membership = EpochCommittees::new_stake(
746 network_config.config.known_nodes_with_stake.clone(),
747 network_config.config.known_da_nodes.clone(),
748 block_reward,
749 fetcher,
750 epoch_height,
751 );
752 info!("Membership created. Reloading stake");
753 membership.reload_stake(RECENT_STAKE_TABLES_LIMIT).await;
754 info!("Stake reloaded");
755
756 check_cliquenet_info_registered(
757 &membership,
758 &validator_config.public_key,
759 genesis.base_version,
760 genesis.chain_config.stake_table_contract,
761 &l1_client,
762 )
763 .await;
764
765 let persistence = Arc::new(persistence);
766 let coordinator = EpochMembershipCoordinator::new(
767 membership,
768 network_config.config.epoch_height,
769 &persistence,
770 );
771
772 let epoch_rewards_calculator = Arc::new(Mutex::new(EpochRewardsCalculator::new()));
773
774 let instance_state = NodeState {
775 chain_config: genesis.chain_config,
776 genesis_chain_config,
777 l1_client,
778 genesis_header: genesis.header,
779 genesis_state,
780 l1_genesis: Some(l1_genesis),
781 node_id: node_index,
782 upgrades: genesis.upgrades,
783 current_version: genesis.base_version,
784 epoch_height: Some(epoch_height),
785 state_catchup: Arc::new(state_catchup_providers.clone()),
786 coordinator: coordinator.clone(),
787 genesis_version: genesis.genesis_version,
788 epoch_start_block: genesis.epoch_start_block.unwrap_or_default(),
789 epoch_rewards_calculator,
790 light_client_contract_address: Cache::builder().max_capacity(1).build(),
791 token_contract_address: Cache::builder().max_capacity(1).build(),
792 finalized_hotshot_height: Cache::builder()
793 .max_capacity(1)
794 .time_to_live(Duration::from_secs(30))
795 .build(),
796 };
797
798 let (initializer, anchor_view) = persistence
802 .load_consensus_state(instance_state, version_upgrade)
803 .await?;
804
805 let combined_network = {
806 info!("Initializing Libp2p network");
807 let chain_id = genesis.chain_config.chain_id;
809 let network_discriminator = (chain_id != MAINNET_CHAIN_ID).then_some(chain_id.0);
810 let p2p_network = Libp2pNetwork::from_config(
811 network_config.clone(),
812 persistence.clone(),
813 gossip_config,
814 request_response_config,
815 libp2p_bind_address,
816 libp2p_external_addresses,
817 &validator_config.public_key,
818 &validator_config.private_key,
821 hotshot::traits::implementations::Libp2pMetricsValue::new(&*metrics),
822 network_discriminator,
823 network_params.libp2p_dht_put_quorum,
824 )
825 .await
826 .with_context(|| {
827 format!(
828 "Failed to create libp2p network on node {node_index}; binding to {:?}",
829 network_params.libp2p_bind_address
830 )
831 })?;
832
833 info!("Libp2p network initialized");
834
835 if genesis.base_version < versions::NEW_PROTOCOL_VERSION
843 && !new_protocol_cutover_complete(&initializer)
844 {
845 tracing::warn!("Waiting for at least one connection to be initialized");
846 select! {
847 _ = cdn_network.wait_for_ready() => {
848 tracing::warn!("CDN connection initialized");
849 },
850 _ = p2p_network.wait_for_ready() => {
851 tracing::warn!("P2P connection initialized");
852 },
853 };
854 }
855
856 CombinedNetworks::new(cdn_network, p2p_network, Some(Duration::from_secs(1)))
858 };
859
860 let cliquenet = {
861 let metrics = clone_box(&*metrics);
862 let secret_key = network_params.x25519_secret_key.into();
863 let bind_addr = network_params.cliquenet_bind_addr.clone();
864 let name = format!("espresso-{}", genesis.chain_config.chain_id);
865 move |upgrade| Cliquenet::create(name, pub_key, secret_key, bind_addr, [], upgrade, metrics)
866 };
867
868 let network = Arc::new(combined_network);
869
870 let mut ctx = SequencerContext::init(
871 network_config,
872 version_upgrade,
873 validator_config,
874 coordinator,
875 initializer,
876 anchor_view,
877 storage,
878 state_catchup_providers,
879 persistence,
880 network.clone(),
881 cliquenet,
882 Some(network_params.state_relay_server_url),
883 &*metrics,
884 genesis.stake_table.capacity,
885 event_consumer,
886 proposal_fetcher_config,
887 network_params.bootstrap_epoch_catchup_timeout,
888 )
889 .await?;
890
891 if wait_for_orchestrator {
892 ctx = ctx.wait_for_orchestrator(orchestrator_client);
893 }
894
895 Ok(ctx)
896}
897
898pub fn empty_builder_commitment() -> BuilderCommitment {
899 BuilderCommitment::from_bytes([])
900}
901
902async fn check_cliquenet_info_registered(
907 membership: &EpochCommittees,
908 pub_key: &BLSPubKey,
909 current_version: vbs::version::Version,
910 stake_table_contract: Option<alloy::primitives::Address>,
911 l1_client: &espresso_types::v0::L1Client,
912) {
913 if current_version != versions::EPOCH_REWARD_VERSION {
914 return;
915 }
916 let Some(addr) = stake_table_contract else {
917 return;
918 };
919 let stake_table =
920 hotshot_contract_adapter::sol_types::StakeTableV3::new(addr, l1_client.provider.clone());
921 let mut major = None;
922 let mut last_err = None;
923 for attempt in 1..=3 {
924 match stake_table.getVersion().call().await {
925 Ok(v) => {
926 major = Some(v.majorVersion);
927 break;
928 },
929 Err(e) => {
930 tracing::warn!(attempt, %e, "failed to read StakeTable getVersion(), retrying");
931 last_err = Some(e);
932 if attempt < 3 {
933 tokio::time::sleep(Duration::from_secs(1)).await;
934 }
935 },
936 }
937 }
938 let Some(major) = major else {
939 tracing::warn!(
940 err = ?last_err,
941 "could not read StakeTable getVersion() after 3 attempts; skipping check"
942 );
943 return;
944 };
945 if major < 3 {
946 tracing::info!(
947 major,
948 "StakeTableV3 not deployed; skipping network-info registration check"
949 );
950 return;
951 }
952 let Some(cfg) = membership.latest_peer_config(pub_key) else {
953 return;
954 };
955 if cfg.connect_info.is_some() {
956 return;
957 }
958 tracing::error!(
959 bls_key = %pub_key,
960 "Validator has no x25519 key or p2p address registered on-chain. After the CLIQUENET \
961 upgrade activates, the validator will be excluded from the active set and stop earning \
962 rewards. To fix: (1) generate an x25519 keypair if needed (`keygen --scheme x25519 \
963 --out keys.env`), then (2) register it on-chain (`staking-cli update-network-config \
964 --x25519-key <ESPRESSO_NODE_PUBLIC_X25519_KEY> --p2p-addr <host:port>`)."
965 );
966}
967
968fn new_protocol_cutover_complete(initializer: &HotShotInitializer<SeqTypes>) -> bool {
973 let Some(cert) = initializer.decided_upgrade_certificate() else {
974 return false;
975 };
976 let complete = cert.data.new_version >= versions::NEW_PROTOCOL_VERSION
977 && initializer.start_view() >= cert.data.new_version_first_view;
978 if complete {
979 tracing::info!(
980 start_view = %initializer.start_view(),
981 cutover_view = %cert.data.new_version_first_view,
982 "network already upgraded to the new protocol, not waiting for the legacy network"
983 );
984 }
985 complete
986}
987
988#[cfg(any(test, feature = "testing"))]
989pub mod testing {
990 use std::{
991 cmp::max,
992 collections::{BTreeMap, HashMap, HashSet},
993 net::Ipv4Addr,
994 time::Duration,
995 };
996
997 use alloy::{
998 network::EthereumWallet,
999 node_bindings::{Anvil, AnvilInstance},
1000 primitives::{Address, U256},
1001 providers::{
1002 Provider, ProviderBuilder, RootProvider, WalletProvider,
1003 fillers::{
1004 BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
1005 },
1006 layers::AnvilProvider,
1007 },
1008 signers::{k256::ecdsa::SigningKey, local::LocalSigner},
1009 };
1010 use catchup::NullStateCatchup;
1011 use committable::Committable;
1012 use espresso_contract_deployer::{
1013 Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
1014 builder::{DeployerArgs, DeployerArgsBuilder},
1015 network_config::light_client_genesis_from_stake_table,
1016 };
1017 use espresso_types::{
1018 EpochVersion, Event, FeeAccount, L1Client, NetworkConfig, PubKey, SeqTypes, Transaction,
1019 Upgrade, UpgradeMap, UpgradeMode,
1020 eth_signature_key::EthKeyPair,
1021 v0::traits::{EventConsumer, NullEventConsumer, PersistenceOptions, StateCatchup},
1022 };
1023 use futures::{
1024 future::join_all,
1025 stream::{Stream, StreamExt},
1026 };
1027 use hotshot::{
1028 traits::{
1029 BlockPayload,
1030 implementations::{MasterMap, MemoryNetwork},
1031 },
1032 types::EventType,
1033 };
1034 use hotshot_builder_refactored::service::{
1035 BuilderConfig as LegacyBuilderConfig, GlobalState as LegacyGlobalState,
1036 };
1037 use hotshot_contract_adapter::stake_table::StakeTableContractVersion;
1038 use hotshot_testing::block_builder::{
1039 BuilderTask, SimpleBuilderImplementation, TestBuilderImplementation,
1040 };
1041 use hotshot_types::{
1042 HotShotConfig, PeerConfig, PeerConnectInfo,
1043 data::EpochNumber,
1044 event::LeafInfo,
1045 light_client::StateKeyPair,
1046 new_protocol::CoordinatorEvent,
1047 traits::{
1048 EncodeBytes, block_contents::BlockHeader, metrics::NoMetrics, network::Topic,
1049 signature_key::BuilderSignatureKey,
1050 },
1051 };
1052 use rand::SeedableRng as _;
1053 use rand_chacha::ChaCha20Rng;
1054 use staking_cli::demo::{DelegationConfig, StakingKeySet, StakingTransactions};
1055 use test_utils::reserve_tcp_port;
1056 use tokio::{spawn, time::timeout};
1057 use vbs::version::{StaticVersionType, Version};
1058 use versions::EPOCH_VERSION;
1059
1060 use super::*;
1061 use crate::{
1062 catchup::ParallelStateCatchup,
1063 persistence::no_storage::{self, NoStorage},
1064 };
1065
1066 const STAKE_TABLE_CAPACITY_FOR_TEST: usize = 10;
1067 const BUILDER_CHANNEL_CAPACITY_FOR_TEST: usize = 128;
1068 type AnvilFillProvider = AnvilProvider<
1069 FillProvider<
1070 JoinFill<
1071 alloy::providers::Identity,
1072 JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
1073 >,
1074 RootProvider,
1075 >,
1076 >;
1077 struct LegacyBuilderImplementation {
1078 global_state: Arc<LegacyGlobalState<SeqTypes>>,
1079 }
1080
1081 impl BuilderTask<SeqTypes> for LegacyBuilderImplementation {
1082 fn start(
1083 self: Box<Self>,
1084 stream: Box<
1085 dyn futures::prelude::Stream<Item = hotshot::types::Event<SeqTypes>>
1086 + std::marker::Unpin
1087 + Send
1088 + 'static,
1089 >,
1090 ) {
1091 spawn(async move {
1092 let res = self.global_state.start_event_loop(stream).await;
1093 tracing::error!(?res, "testing legacy builder service exited");
1094 });
1095 }
1096 }
1097
1098 pub async fn run_legacy_builder<const NUM_NODES: usize>(
1099 port: Option<u16>,
1100 max_block_size: Option<u64>,
1101 ) -> (Box<dyn BuilderTask<SeqTypes>>, Url) {
1102 let builder_key_pair = TestConfig::<0>::builder_key();
1103 let port = match port {
1104 Some(p) => p,
1105 None => reserve_tcp_port().expect("OS should have ephemeral ports available"),
1106 };
1107
1108 let url: Url = format!("http://localhost:{port}")
1110 .parse()
1111 .expect("Failed to parse builder URL");
1112
1113 let global_state = LegacyGlobalState::new(
1115 LegacyBuilderConfig {
1116 builder_keys: (builder_key_pair.fee_account(), builder_key_pair),
1117 max_api_waiting_time: Duration::from_secs(1),
1118 max_block_size_increment_period: Duration::from_secs(60),
1119 maximize_txn_capture_timeout: Duration::from_millis(100),
1120 txn_garbage_collect_duration: Duration::from_secs(60),
1121 txn_channel_capacity: BUILDER_CHANNEL_CAPACITY_FOR_TEST,
1122 tx_status_cache_capacity: 81920,
1123 base_fee: 10,
1124 },
1125 NodeState::default(),
1126 max_block_size.unwrap_or(300),
1127 NUM_NODES,
1128 );
1129
1130 let app = Arc::clone(&global_state)
1132 .into_app()
1133 .expect("Failed to create builder tide-disco app");
1134
1135 spawn(async move {
1136 app.serve(
1137 format!("http://0.0.0.0:{port}")
1138 .parse::<Url>()
1139 .expect("Failed to parse builder listener"),
1140 EpochVersion::instance(),
1141 )
1142 .await
1143 });
1144
1145 (Box::new(LegacyBuilderImplementation { global_state }), url)
1147 }
1148
1149 pub async fn run_test_builder<const NUM_NODES: usize>(
1150 port: Option<u16>,
1151 ) -> (Box<dyn BuilderTask<SeqTypes>>, Url) {
1152 let port = match port {
1153 Some(p) => p,
1154 None => reserve_tcp_port().expect("OS should have ephemeral ports available"),
1155 };
1156
1157 let url: Url = format!("http://localhost:{port}")
1159 .parse()
1160 .expect("Failed to parse builder URL");
1161 tracing::info!("Starting test builder on {url}");
1162
1163 let task = <SimpleBuilderImplementation as TestBuilderImplementation<SeqTypes>>::start(
1164 NUM_NODES,
1165 format!("http://0.0.0.0:{port}")
1166 .parse()
1167 .expect("Failed to parse builder listener"),
1168 (),
1169 HashMap::new(),
1170 )
1171 .await;
1172
1173 (task, url)
1174 }
1175
1176 pub struct TestConfigBuilder<const NUM_NODES: usize> {
1177 config: HotShotConfig<SeqTypes>,
1178 priv_keys: Vec<BLSPrivKey>,
1179 state_key_pairs: Vec<StateKeyPair>,
1180 master_map: Arc<MasterMap<PubKey>>,
1181 l1_url: Url,
1182 l1_opt: L1ClientOptions,
1183 anvil_provider: Option<AnvilFillProvider>,
1184 signer: LocalSigner<SigningKey>,
1185 state_relay_url: Option<Url>,
1186 builder_port: Option<u16>,
1187 upgrades: BTreeMap<Version, Upgrade>,
1188 coordinator_addrs: Vec<NetAddr>,
1189 contracts: Option<Contracts>,
1190 }
1191
1192 fn select_staking_key_sets(all: Vec<StakingKeySet>, indices: &[usize]) -> Vec<StakingKeySet> {
1196 let mut by_index: Vec<Option<StakingKeySet>> = all.into_iter().map(Some).collect();
1197 indices
1198 .iter()
1199 .map(|&i| {
1200 by_index
1201 .get_mut(i)
1202 .unwrap_or_else(|| panic!("validator index {i} out of range"))
1203 .take()
1204 .unwrap_or_else(|| panic!("duplicate validator index {i}"))
1205 })
1206 .collect()
1207 }
1208
1209 pub async fn deploy_stake_table(
1212 args: &DeployerArgs<impl Provider + WalletProvider>,
1213 version: StakeTableContractVersion,
1214 contracts: &mut Contracts,
1215 ) -> anyhow::Result<()> {
1216 match version {
1217 StakeTableContractVersion::V1 => args.deploy_to_stake_table_v1(contracts).await,
1218 StakeTableContractVersion::V2 => args.deploy_to_stake_table_v2(contracts).await,
1219 StakeTableContractVersion::V3 => args.deploy_to_stake_table_v3(contracts).await,
1220 }
1221 }
1222
1223 pub fn staking_priv_keys(
1224 priv_keys: &[BLSPrivKey],
1225 state_key_pairs: &[StateKeyPair],
1226 coordinator_addrs: &[NetAddr],
1227 num_nodes: usize,
1228 ) -> Vec<StakingKeySet> {
1229 let seed = [42u8; 32];
1230 let mut rng = ChaCha20Rng::from_seed(seed); let eth_key_pairs = (0..num_nodes).map(|_| SigningKey::random(&mut rng).into());
1232 eth_key_pairs
1233 .zip(priv_keys.iter())
1234 .zip(state_key_pairs.iter())
1235 .enumerate()
1236 .map(|(i, ((eth, bls), state))| StakingKeySet {
1237 signer: eth,
1238 bls: bls.clone().into(),
1239 state: state.clone(),
1240 x25519: x25519::Keypair::derive_from::<PubKey>(bls)
1243 .expect("x25519 keypair derivation should succeed"),
1244 p2p_addr: coordinator_addrs
1245 .get(i)
1246 .cloned()
1247 .unwrap_or_else(|| "127.0.0.1:8080".parse().unwrap()),
1248 metadata_uri: None,
1249 })
1250 .collect()
1251 }
1252
1253 impl<const NUM_NODES: usize> TestConfigBuilder<NUM_NODES> {
1254 pub fn builder_port(mut self, builder_port: Option<u16>) -> Self {
1255 self.builder_port = builder_port;
1256 self
1257 }
1258
1259 pub fn state_relay_url(mut self, url: Url) -> Self {
1260 self.state_relay_url = Some(url);
1261 self
1262 }
1263
1264 pub fn anvil_provider(mut self, anvil: AnvilInstance) -> Self {
1269 self.l1_url = anvil.endpoint().parse().unwrap();
1270 let l1_client = L1Client::anvil(&anvil).expect("create l1 client");
1271 let anvil_provider = AnvilProvider::new(l1_client.provider, Arc::new(anvil));
1272 self.anvil_provider = Some(anvil_provider);
1273 self
1274 }
1275
1276 pub fn l1_url(mut self, l1_url: Url) -> Self {
1279 self.anvil_provider = None;
1280 self.l1_url = l1_url;
1281 self
1282 }
1283
1284 pub fn l1_opt(mut self, opt: L1ClientOptions) -> Self {
1285 self.l1_opt = opt;
1286 self
1287 }
1288
1289 pub fn signer(mut self, signer: LocalSigner<SigningKey>) -> Self {
1290 self.signer = signer;
1291 self
1292 }
1293
1294 pub fn upgrades(mut self, v: Version, upgrades: BTreeMap<Version, Upgrade>) -> Self {
1295 let upgrade = upgrades.get(&v).unwrap();
1296 upgrade.set_hotshot_config_parameters(&mut self.config);
1297 self.upgrades = upgrades;
1298 self
1299 }
1300
1301 pub async fn set_upgrades(self, version: Version) -> Self {
1304 let registered: Vec<usize> = (0..NUM_NODES).collect();
1305 self.set_upgrades_with(version, StakeTableContractVersion::V3, ®istered)
1306 .await
1307 }
1308
1309 pub async fn set_upgrades_with(
1314 mut self,
1315 version: Version,
1316 stake_table_version: StakeTableContractVersion,
1317 registered: &[usize],
1318 ) -> Self {
1319 let upgrade = match version {
1320 version if version >= EPOCH_VERSION => {
1321 tracing::debug!(?version, "upgrade version");
1322 let blocks_per_epoch = self.config.epoch_height;
1323 let epoch_start_block = self.config.epoch_start_block;
1324
1325 let (genesis_state, genesis_stake) = light_client_genesis_from_stake_table(
1326 &self.config.hotshot_stake_table(),
1327 STAKE_TABLE_CAPACITY_FOR_TEST,
1328 )
1329 .unwrap();
1330
1331 let validators = select_staking_key_sets(
1332 staking_priv_keys(
1333 &self.priv_keys,
1334 &self.state_key_pairs,
1335 &self.coordinator_addrs,
1336 NUM_NODES,
1337 ),
1338 registered,
1339 );
1340
1341 let deployer = ProviderBuilder::new()
1342 .wallet(EthereumWallet::from(self.signer.clone()))
1343 .connect_http(self.l1_url.clone());
1344
1345 let mut contracts = Contracts::new();
1346 let args = DeployerArgsBuilder::default()
1347 .deployer(deployer.clone())
1348 .rpc_url(self.l1_url.clone())
1349 .mock_light_client(true)
1350 .genesis_lc_state(genesis_state)
1351 .genesis_st_state(genesis_stake)
1352 .blocks_per_epoch(blocks_per_epoch)
1353 .epoch_start_block(epoch_start_block)
1354 .exit_escrow_period(U256::from(max(
1355 blocks_per_epoch * 15 + 100,
1356 DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
1357 )))
1358 .multisig_pauser(self.signer.address())
1359 .token_name("Espresso".to_string())
1360 .token_symbol("ESP".to_string())
1361 .initial_token_supply(U256::from(3590000000u64))
1362 .ops_timelock_delay(U256::from(0))
1363 .ops_timelock_admin(self.signer.address())
1364 .ops_timelock_proposers(vec![self.signer.address()])
1365 .ops_timelock_executors(vec![self.signer.address()])
1366 .safe_exit_timelock_delay(U256::from(10))
1367 .safe_exit_timelock_admin(self.signer.address())
1368 .safe_exit_timelock_proposers(vec![self.signer.address()])
1369 .safe_exit_timelock_executors(vec![self.signer.address()])
1370 .build()
1371 .unwrap();
1372 deploy_stake_table(&args, stake_table_version, &mut contracts)
1373 .await
1374 .expect("failed to deploy all contracts");
1375
1376 let st_addr = contracts
1377 .address(Contract::StakeTableProxy)
1378 .expect("StakeTableProxy address not found");
1379 StakingTransactions::create(
1380 self.l1_url.clone(),
1381 &deployer,
1382 st_addr,
1383 validators,
1384 None,
1385 DelegationConfig::default(),
1386 )
1387 .await
1388 .expect("stake table setup failed")
1389 .apply_all()
1390 .await
1391 .expect("send all txns failed");
1392
1393 self.contracts = Some(contracts);
1394
1395 Upgrade::pos_view_based(st_addr)
1396 },
1397 _ => panic!("Upgrade not configured for version {version:?}"),
1398 };
1399
1400 let mut upgrades = std::collections::BTreeMap::new();
1401 upgrade.set_hotshot_config_parameters(&mut self.config);
1402 upgrades.insert(version, upgrade);
1403
1404 self.upgrades = upgrades;
1405 self
1406 }
1407
1408 pub fn epoch_height(mut self, epoch_height: u64) -> Self {
1409 self.config.epoch_height = epoch_height;
1410 self
1411 }
1412
1413 pub fn epoch_start_block(mut self, start_block: u64) -> Self {
1414 self.config.epoch_start_block = start_block;
1415 self
1416 }
1417
1418 pub fn builder_timeout(mut self, timeout: Duration) -> Self {
1419 self.config.builder_timeout = timeout;
1420 self
1421 }
1422
1423 pub fn next_view_timeout(mut self, timeout: Duration) -> Self {
1426 self.config.next_view_timeout = timeout.as_millis() as u64;
1427 self
1428 }
1429
1430 pub fn upgrade_proposing_views(mut self, start: u64, stop: u64) -> Self {
1432 for upgrade in self.upgrades.values_mut() {
1433 if let UpgradeMode::View(v) = &mut upgrade.mode {
1434 v.start_proposing_view = start;
1435 v.stop_proposing_view = stop;
1436 }
1437 }
1438 self.config.start_proposing_view = start;
1439 self.config.stop_proposing_view = stop;
1440 self
1441 }
1442
1443 pub fn build(self) -> TestConfig<NUM_NODES> {
1444 TestConfig {
1445 config: self.config,
1446 priv_keys: self.priv_keys,
1447 state_key_pairs: self.state_key_pairs,
1448 master_map: self.master_map,
1449 l1_url: self.l1_url,
1450 l1_opt: self.l1_opt,
1451 signer: self.signer,
1452 state_relay_url: self.state_relay_url,
1453 builder_port: self.builder_port,
1454 upgrades: self.upgrades,
1455 anvil_provider: self.anvil_provider,
1456 coordinator_addrs: self.coordinator_addrs,
1457 contracts: self.contracts,
1458 }
1459 }
1460
1461 pub fn stake_table_capacity(mut self, stake_table_capacity: usize) -> Self {
1462 self.config.stake_table_capacity = stake_table_capacity;
1463 self
1464 }
1465 }
1466
1467 impl<const NUM_NODES: usize> Default for TestConfigBuilder<NUM_NODES> {
1468 fn default() -> Self {
1469 let num_nodes = NUM_NODES;
1470
1471 let seed = [0; 32];
1473 let (pub_keys, priv_keys): (Vec<_>, Vec<_>) = (0..num_nodes)
1474 .map(|i| <PubKey as SignatureKey>::generated_from_seed_indexed(seed, i as u64))
1475 .unzip();
1476 let state_key_pairs = (0..num_nodes)
1477 .map(|i| StateKeyPair::generate_from_seed_indexed(seed, i as u64))
1478 .collect::<Vec<_>>();
1479
1480 let coordinator_addrs: Vec<NetAddr> = (0..num_nodes)
1482 .map(|_| {
1483 let port =
1484 reserve_tcp_port().expect("OS should have ephemeral ports available");
1485 NetAddr::Inet(Ipv4Addr::LOCALHOST.into(), port)
1486 })
1487 .collect();
1488
1489 let known_nodes_with_stake = pub_keys
1490 .iter()
1491 .zip(&priv_keys)
1492 .zip(&state_key_pairs)
1493 .zip(&coordinator_addrs)
1494 .map(
1495 |(((pub_key, priv_key), state_key_pair), addr)| PeerConfig::<SeqTypes> {
1496 stake_table_entry: pub_key.stake_table_entry(U256::from(1)),
1497 state_ver_key: state_key_pair.ver_key(),
1498 connect_info: Some(PeerConnectInfo {
1499 x25519_key: x25519::Keypair::derive_from::<PubKey>(priv_key)
1500 .expect("x25519 keypair derivation should succeed")
1501 .public_key(),
1502 p2p_addr: addr.clone(),
1503 }),
1504 },
1505 )
1506 .collect::<Vec<_>>();
1507
1508 let master_map = MasterMap::new();
1509
1510 let builder_port = reserve_tcp_port().unwrap();
1511
1512 let config: HotShotConfig<SeqTypes> = HotShotConfig {
1513 fixed_leader_for_gpuvid: 0,
1514 num_nodes_with_stake: num_nodes.try_into().unwrap(),
1515 known_da_nodes: known_nodes_with_stake.clone(),
1516 da_committees: Default::default(),
1517 known_nodes_with_stake: known_nodes_with_stake.clone(),
1518 next_view_timeout: Duration::from_secs(5).as_millis() as u64,
1519 num_bootstrap: 1usize,
1520 da_staked_committee_size: num_nodes,
1521 view_sync_timeout: Duration::from_secs(1),
1522 data_request_delay: Duration::from_secs(1),
1523 builder_urls: vec1::vec1![
1524 Url::parse(&format!("http://127.0.0.1:{builder_port}")).unwrap()
1525 ],
1526 builder_timeout: Duration::from_secs(1),
1527 start_threshold: (
1528 known_nodes_with_stake.clone().len() as u64,
1529 known_nodes_with_stake.clone().len() as u64,
1530 ),
1531 start_proposing_view: 0,
1532 stop_proposing_view: 0,
1533 start_voting_view: 0,
1534 stop_voting_view: 0,
1535 start_proposing_time: 0,
1536 start_voting_time: 0,
1537 stop_proposing_time: 0,
1538 stop_voting_time: 0,
1539 epoch_height: 30,
1540 epoch_start_block: 1,
1541 stake_table_capacity: hotshot_types::light_client::DEFAULT_STAKE_TABLE_CAPACITY,
1542 drb_difficulty: 10,
1543 drb_upgrade_difficulty: 20,
1544 };
1545
1546 let anvil = Anvil::new()
1547 .args(["--slots-in-an-epoch", "0", "--balance", "1000000"])
1548 .spawn();
1549
1550 let l1_client = L1Client::anvil(&anvil).expect("failed to create l1 client");
1551 let anvil_provider = AnvilProvider::new(l1_client.provider, Arc::new(anvil));
1552
1553 let l1_signer_key = anvil_provider.anvil().keys()[0].clone();
1554 let signer = LocalSigner::from(l1_signer_key);
1555
1556 Self {
1557 config,
1558 priv_keys,
1559 state_key_pairs,
1560 master_map,
1561 l1_url: anvil_provider.anvil().endpoint().parse().unwrap(),
1562 l1_opt: L1ClientOptions {
1563 stake_table_update_interval: Duration::from_secs(5),
1564 l1_events_max_block_range: 1000,
1565 l1_polling_interval: Duration::from_secs(1),
1566 subscription_timeout: Duration::from_secs(5),
1567 ..Default::default()
1568 },
1569 anvil_provider: Some(anvil_provider),
1570 signer,
1571 state_relay_url: None,
1572 builder_port: None,
1573 upgrades: Default::default(),
1574 coordinator_addrs,
1575 contracts: None,
1576 }
1577 }
1578 }
1579
1580 #[derive(Clone)]
1581 pub struct TestConfig<const NUM_NODES: usize> {
1582 config: HotShotConfig<SeqTypes>,
1583 priv_keys: Vec<BLSPrivKey>,
1584 state_key_pairs: Vec<StateKeyPair>,
1585 master_map: Arc<MasterMap<PubKey>>,
1586 l1_url: Url,
1587 l1_opt: L1ClientOptions,
1588 anvil_provider: Option<AnvilFillProvider>,
1589 signer: LocalSigner<SigningKey>,
1590 state_relay_url: Option<Url>,
1591 builder_port: Option<u16>,
1592 upgrades: BTreeMap<Version, Upgrade>,
1593 coordinator_addrs: Vec<NetAddr>,
1595 contracts: Option<Contracts>,
1597 }
1598
1599 impl<const NUM_NODES: usize> TestConfig<NUM_NODES> {
1600 pub fn num_nodes(&self) -> usize {
1601 self.priv_keys.len()
1602 }
1603
1604 pub fn hotshot_config(&self) -> &HotShotConfig<SeqTypes> {
1605 &self.config
1606 }
1607
1608 pub fn set_builder_urls(&mut self, builder_urls: vec1::Vec1<Url>) {
1609 self.config.builder_urls = builder_urls;
1610 }
1611
1612 pub fn builder_port(&self) -> Option<u16> {
1613 self.builder_port
1614 }
1615
1616 pub fn signer(&self) -> LocalSigner<SigningKey> {
1617 self.signer.clone()
1618 }
1619
1620 pub fn l1_url(&self) -> Url {
1621 self.l1_url.clone()
1622 }
1623
1624 pub fn anvil(&self) -> Option<&AnvilFillProvider> {
1625 self.anvil_provider.as_ref()
1626 }
1627
1628 pub fn get_upgrade_map(&self) -> UpgradeMap {
1629 self.upgrades.clone().into()
1630 }
1631
1632 pub fn upgrades(&self) -> BTreeMap<Version, Upgrade> {
1633 self.upgrades.clone()
1634 }
1635
1636 pub fn staking_priv_keys(&self) -> Vec<StakingKeySet> {
1637 staking_priv_keys(
1638 &self.priv_keys,
1639 &self.state_key_pairs,
1640 &self.coordinator_addrs,
1641 self.num_nodes(),
1642 )
1643 }
1644
1645 pub fn staking_key_sets(&self, indices: &[usize]) -> Vec<StakingKeySet> {
1650 select_staking_key_sets(self.staking_priv_keys(), indices)
1651 }
1652
1653 pub fn coordinator_addr(&self, i: usize) -> NetAddr {
1655 self.coordinator_addrs[i].clone()
1656 }
1657
1658 pub fn set_coordinator_addr(&mut self, i: usize, addr: NetAddr) {
1663 self.coordinator_addrs[i] = addr;
1664 }
1665
1666 pub fn set_consensus_keys(&mut self, i: usize, bls: BLSPrivKey, state: StateKeyPair) {
1681 self.priv_keys[i] = bls;
1682 self.state_key_pairs[i] = state;
1683 }
1684
1685 pub fn contracts(&self) -> Option<Contracts> {
1688 self.contracts.clone()
1689 }
1690
1691 pub fn validator_providers(
1692 &self,
1693 ) -> Vec<(Address, impl Provider + Clone + use<NUM_NODES>)> {
1694 self.staking_priv_keys()
1695 .into_iter()
1696 .map(|key_set| {
1697 (
1698 key_set.signer.address(),
1699 ProviderBuilder::new()
1700 .wallet(EthereumWallet::from(key_set.signer))
1701 .connect_http(self.l1_url.clone()),
1702 )
1703 })
1704 .collect()
1705 }
1706
1707 pub async fn init_nodes(
1708 &self,
1709 upgrade: versions::Upgrade,
1710 ) -> Vec<SequencerContext<network::Memory, NoStorage>> {
1711 join_all((0..self.num_nodes()).map(|i| async move {
1712 self.init_node(
1713 i,
1714 ValidatedState::default(),
1715 no_storage::Options,
1716 Some(NullStateCatchup::default()),
1717 None,
1718 &NoMetrics,
1719 STAKE_TABLE_CAPACITY_FOR_TEST,
1720 NullEventConsumer,
1721 upgrade,
1722 Default::default(),
1723 )
1724 .await
1725 }))
1726 .await
1727 }
1728
1729 pub fn known_nodes_with_stake(&self) -> &[PeerConfig<SeqTypes>] {
1730 &self.config.known_nodes_with_stake
1731 }
1732
1733 #[allow(clippy::too_many_arguments)]
1734 pub async fn init_node<P: PersistenceOptions>(
1735 &self,
1736 i: usize,
1737 mut state: ValidatedState,
1738 mut persistence_opt: P,
1739 state_peers: Option<impl StateCatchup + 'static>,
1740 storage: Option<RequestResponseStorage>,
1741 metrics: &dyn Metrics,
1742 stake_table_capacity: usize,
1743 event_consumer: impl EventConsumer + 'static,
1744 upgrade: versions::Upgrade,
1745 upgrades: BTreeMap<Version, Upgrade>,
1746 ) -> SequencerContext<network::Memory, P::Persistence> {
1747 let config = self.config.clone();
1748 let my_peer_config = &config.known_nodes_with_stake[i];
1749 let is_da = config.known_da_nodes.contains(my_peer_config);
1750
1751 let x25519_keypair = x25519::Keypair::derive_from::<PubKey>(&self.priv_keys[i])
1757 .expect("x25519 keypair derivation should succeed");
1758 let coordinator_addr = self.coordinator_addrs[i].clone();
1759
1760 let pub_key = PubKey::from_private(&self.priv_keys[i]);
1761
1762 let validator_config = ValidatorConfig {
1763 public_key: pub_key,
1764 private_key: self.priv_keys[i].clone(),
1765 stake_value: my_peer_config.stake_table_entry.stake_amount,
1766 state_public_key: self.state_key_pairs[i].ver_key(),
1767 state_private_key: self.state_key_pairs[i].sign_key(),
1768 is_da,
1769 x25519_keypair: Some(x25519_keypair.clone()),
1770 p2p_addr: Some(coordinator_addr.clone()),
1771 };
1772
1773 let topics = if is_da {
1774 vec![Topic::Global, Topic::Da]
1775 } else {
1776 vec![Topic::Global]
1777 };
1778
1779 let network = Arc::new(MemoryNetwork::new(
1780 &pub_key,
1781 &self.master_map,
1782 &topics,
1783 None,
1784 ));
1785
1786 let builder_account = Self::builder_key().fee_account();
1788 tracing::info!(%builder_account, "prefunding builder account");
1789 state.prefund_account(builder_account, U256::MAX.into());
1790
1791 let persistence = persistence_opt.create().await.unwrap();
1792
1793 let chain_config = state.chain_config.resolve().unwrap_or_default();
1794
1795 let catchup_providers = ParallelStateCatchup::new(&[], Duration::from_secs(5));
1797
1798 if let Some(state_peers) = state_peers {
1800 catchup_providers.add_provider(Arc::new(state_peers));
1801 }
1802
1803 match persistence
1805 .clone()
1806 .into_catchup_provider(BackoffParams::default())
1807 {
1808 Ok(local_catchup) => {
1809 catchup_providers.add_provider(local_catchup);
1810 },
1811 Err(e) => {
1812 tracing::warn!(
1813 "Failed to create local catchup provider: {e:#}. Only using remote \
1814 catchup."
1815 );
1816 },
1817 };
1818
1819 let l1_client = self
1820 .l1_opt
1821 .clone()
1822 .connect(vec![self.l1_url.clone()])
1823 .expect("failed to create L1 client");
1824 l1_client.spawn_tasks().await;
1825
1826 let fetcher = Fetcher::new(
1827 Arc::new(catchup_providers.clone()),
1828 Arc::new(Mutex::new(persistence.clone())),
1829 l1_client.clone(),
1830 chain_config,
1831 );
1832 fetcher.spawn_update_loop().await;
1833
1834 let block_reward = fetcher.fetch_fixed_block_reward().await.ok();
1835 let mut membership = EpochCommittees::new_stake(
1836 config.known_nodes_with_stake.clone(),
1837 config.known_da_nodes.clone(),
1838 block_reward,
1839 fetcher,
1840 config.epoch_height,
1841 );
1842 membership.reload_stake(50).await;
1843
1844 let membership = Arc::new(membership);
1845 let persistence = Arc::new(persistence);
1846
1847 let coordinator = EpochMembershipCoordinator::new(
1848 membership,
1849 config.epoch_height,
1850 &persistence.clone(),
1851 );
1852
1853 let node_state = NodeState::new(
1854 i as u64,
1855 chain_config,
1856 l1_client,
1857 Arc::new(catchup_providers.clone()),
1858 upgrade.base,
1859 coordinator.clone(),
1860 upgrade.base,
1861 )
1862 .with_current_version(upgrade.base)
1863 .with_genesis(state)
1864 .with_epoch_height(config.epoch_height)
1865 .with_upgrades(upgrades)
1866 .with_epoch_start_block(config.epoch_start_block);
1867
1868 tracing::info!(
1869 i,
1870 key = %pub_key,
1871 state_key = %self.state_key_pairs[i].ver_key(),
1872 "starting node",
1873 );
1874
1875 let coordinator_network = move |upgrade| {
1876 Cliquenet::create(
1877 "test-coordinator",
1878 pub_key,
1879 x25519_keypair,
1880 coordinator_addr,
1881 [],
1882 upgrade,
1883 Box::new(NoMetrics),
1884 )
1885 };
1886
1887 let (initializer, anchor_view) = persistence
1888 .load_consensus_state(node_state, upgrade)
1889 .await
1890 .unwrap();
1891
1892 SequencerContext::init(
1893 NetworkConfig {
1894 config,
1895 ..Default::default()
1898 },
1899 upgrade,
1900 validator_config,
1901 coordinator,
1902 initializer,
1903 anchor_view,
1904 storage,
1905 catchup_providers,
1906 persistence,
1907 network,
1908 coordinator_network,
1909 self.state_relay_url.clone(),
1910 metrics,
1911 stake_table_capacity,
1912 event_consumer,
1913 Default::default(),
1914 Duration::from_secs(2),
1915 )
1916 .await
1917 .unwrap()
1918 }
1919
1920 pub fn builder_key() -> EthKeyPair {
1921 FeeAccount::generated_from_seed_indexed([1; 32], 0).1
1922 }
1923 }
1924
1925 pub async fn wait_for_decide_on_handle(
1928 events: &mut (impl Stream<Item = CoordinatorEvent<SeqTypes>> + Unpin),
1929 submitted_txn: &Transaction,
1930 ) -> (u64, usize) {
1931 let commitment = submitted_txn.commit();
1932
1933 let mut reconstructed = HashMap::new();
1939 let mut decided_without_payload = HashSet::new();
1940
1941 let (height, size) = timeout(Duration::from_secs(120), async {
1942 loop {
1943 let event = events.next().await.unwrap();
1944 tracing::info!("Received event from handle: {event:?}");
1945
1946 if let CoordinatorEvent::BlockPayloadReconstructed {
1947 view,
1948 header,
1949 payload,
1950 } = &event
1951 {
1952 if payload
1953 .transaction_commitments(header.metadata())
1954 .contains(&commitment)
1955 {
1956 let found = (header.block_number(), payload.encode().len());
1957 if decided_without_payload.contains(view) {
1958 return found;
1959 }
1960 reconstructed.insert(*view, found);
1961 }
1962 continue;
1963 }
1964
1965 let leaf_chain: &[LeafInfo<SeqTypes>] = match &event {
1968 CoordinatorEvent::LegacyEvent(Event {
1969 event: EventType::Decide { leaf_chain, .. },
1970 ..
1971 }) => leaf_chain,
1972 CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos,
1973 _ => continue,
1974 };
1975 for LeafInfo { leaf, .. } in leaf_chain {
1976 let Some(payload) = leaf.block_payload() else {
1977 let view = leaf.view_number();
1978 if let Some(found) = reconstructed.remove(&view) {
1979 return found;
1980 }
1981 decided_without_payload.insert(view);
1982 continue;
1983 };
1984 if payload
1985 .transaction_commitments(leaf.block_header().metadata())
1986 .contains(&commitment)
1987 {
1988 return (leaf.block_header().block_number(), payload.encode().len());
1989 }
1990 }
1991 }
1992 })
1993 .await
1994 .unwrap_or_else(|_| {
1995 panic!("transaction {commitment} was not sequenced within the timeout")
1996 });
1997 tracing::info!(height, "transaction {commitment} sequenced");
1998 (height, size)
1999 }
2000
2001 pub async fn wait_for_epochs(
2005 events: &mut (impl futures::Stream<Item = CoordinatorEvent<SeqTypes>> + std::marker::Unpin),
2006 epoch_height: u64,
2007 target_epoch: u64,
2008 ) {
2009 tracing::info!(target_epoch, "waiting for epoch");
2010 let mut last_seen = None;
2011 while let Some(event) = events.next().await {
2012 let leaf = match event {
2015 CoordinatorEvent::LegacyEvent(Event {
2016 event: EventType::Decide { leaf_chain, .. },
2017 ..
2018 }) => leaf_chain[0].leaf.clone(),
2019 CoordinatorEvent::NewDecide { leaf_infos, .. } => leaf_infos[0].leaf.clone(),
2020 _ => continue,
2021 };
2022 let epoch = leaf.epoch(epoch_height);
2023 tracing::debug!(
2024 "Node decided at height: {}, epoch: {epoch:?}",
2025 leaf.height(),
2026 );
2027
2028 if epoch > Some(EpochNumber::new(target_epoch)) {
2029 tracing::info!(target_epoch, "epoch started");
2030 return;
2031 }
2032 last_seen = Some((leaf.height(), epoch));
2033 }
2034 panic!("event stream ended before target epoch {target_epoch}, last decide: {last_seen:?}");
2035 }
2036}
2037
2038#[cfg(test)]
2039mod test {
2040 use alloy::node_bindings::Anvil;
2041 use espresso_types::{Header, MOCK_SEQUENCER_VERSIONS, NamespaceId, Payload, Transaction};
2042 use futures::StreamExt;
2043 use hotshot::types::{Event, EventType};
2044 use hotshot_example_types::node_types::TEST_VERSIONS;
2045 use hotshot_types::{
2046 event::LeafInfo,
2047 new_protocol::CoordinatorEvent,
2048 traits::block_contents::{BlockHeader, BlockPayload},
2049 };
2050 use testing::{TestConfigBuilder, wait_for_decide_on_handle};
2051
2052 #[test]
2053 fn telemetry_endpoint_defaults_by_chain() {
2054 use super::{
2055 ChainId, DECAF_TELEMETRY_ENDPOINT, MAINNET_CHAIN_ID, MAINNET_TELEMETRY_ENDPOINT, U256,
2056 default_telemetry_endpoint,
2057 };
2058
2059 assert_eq!(
2060 default_telemetry_endpoint(MAINNET_CHAIN_ID),
2061 Some(MAINNET_TELEMETRY_ENDPOINT)
2062 );
2063 assert_eq!(
2064 default_telemetry_endpoint(DECAF_CHAIN_ID),
2065 Some(DECAF_TELEMETRY_ENDPOINT)
2066 );
2067 assert_eq!(
2069 default_telemetry_endpoint(ChainId(U256::from(999999999u64))),
2070 None
2071 );
2072 }
2073
2074 use self::testing::run_test_builder;
2075 use super::*;
2076
2077 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2078 async fn test_skeleton_instantiation() {
2079 let anvil = Anvil::new().spawn();
2081 let url = anvil.endpoint_url();
2082 const NUM_NODES: usize = 5;
2083 let mut config = TestConfigBuilder::<NUM_NODES>::default()
2084 .l1_url(url)
2085 .build();
2086
2087 let (builder_task, builder_url) = run_test_builder::<NUM_NODES>(None).await;
2088
2089 config.set_builder_urls(vec1::vec1![builder_url]);
2090
2091 let handles = config.init_nodes(MOCK_SEQUENCER_VERSIONS).await;
2092
2093 let handle_0 = &handles[0];
2094
2095 builder_task.start(Box::new(
2097 handle_0
2098 .consensus_handle()
2099 .legacy_consensus()
2100 .read()
2101 .await
2102 .event_stream(),
2103 ));
2104
2105 let mut events = handle_0.event_stream();
2106
2107 for handle in handles.iter() {
2108 handle.start_consensus().await;
2109 }
2110
2111 let txn = Transaction::new(NamespaceId::from(1_u32), vec![1, 2, 3]);
2113 handles[0]
2114 .submit_transaction(txn.clone())
2115 .await
2116 .expect("Failed to submit transaction");
2117 tracing::info!("Submitted transaction to handle: {txn:?}");
2118
2119 wait_for_decide_on_handle(&mut events, &txn).await;
2120 }
2121
2122 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2123 async fn test_header_invariants() {
2124 let success_height = 30;
2125 let anvil = Anvil::new().spawn();
2127 let url = anvil.endpoint_url();
2128 const NUM_NODES: usize = 5;
2129 let mut config = TestConfigBuilder::<NUM_NODES>::default()
2130 .l1_url(url)
2131 .build();
2132
2133 let (builder_task, builder_url) = run_test_builder::<NUM_NODES>(None).await;
2134
2135 config.set_builder_urls(vec1::vec1![builder_url]);
2136 let handles = config.init_nodes(MOCK_SEQUENCER_VERSIONS).await;
2137
2138 let handle_0 = &handles[0];
2139
2140 let mut events = handle_0.event_stream();
2141
2142 builder_task.start(Box::new(
2144 handle_0
2145 .consensus_handle()
2146 .legacy_consensus()
2147 .read()
2148 .await
2149 .event_stream(),
2150 ));
2151
2152 for handle in handles.iter() {
2153 handle.start_consensus().await;
2154 }
2155
2156 let mut parent = {
2157 let (genesis_payload, genesis_ns_table) =
2159 Payload::from_transactions([], &ValidatedState::default(), &NodeState::mock())
2160 .await
2161 .unwrap();
2162
2163 let genesis_state = NodeState::mock();
2164 Header::genesis(
2165 &genesis_state,
2166 genesis_payload,
2167 &genesis_ns_table,
2168 TEST_VERSIONS.test.base,
2169 )
2170 };
2171
2172 loop {
2173 let event = events.next().await.unwrap();
2174 tracing::info!("Received event from handle: {event:?}");
2175 let CoordinatorEvent::LegacyEvent(Event {
2176 event: EventType::Decide { leaf_chain, .. },
2177 ..
2178 }) = event
2179 else {
2180 continue;
2181 };
2182 tracing::info!("Got decide {leaf_chain:?}");
2183
2184 for LeafInfo { leaf, .. } in leaf_chain.iter().rev() {
2187 let header = leaf.block_header().clone();
2188 if header.height() == 0 {
2189 parent = header;
2190 continue;
2191 }
2192 assert_eq!(header.height(), parent.height() + 1);
2193 assert!(header.timestamp() >= parent.timestamp());
2194 assert!(header.l1_head() >= parent.l1_head());
2195 assert!(header.l1_finalized() >= parent.l1_finalized());
2196 parent = header;
2197 }
2198
2199 if parent.height() >= success_height {
2200 break;
2201 }
2202 }
2203 }
2204}