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