Skip to main content

espresso_node/
lib.rs

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
44/// Default telemetry endpoint for known networks. Unknown chains have no
45/// default; operators set `ESPRESSO_NODE_TELEMETRY_ENDPOINT` explicitly.
46pub(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/// The Sequencer node is generic over the hotshot CommChannel.
103#[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
114// Using derivative to derive Clone triggers the clippy lint
115// https://rust-lang.github.io/rust-clippy/master/index.html#/incorrect_clone_impl_on_copy_type
116impl<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    /// The address where a CDN marshal is located
134    pub cdn_endpoint: String,
135    pub orchestrator_url: Url,
136    pub state_relay_server_url: Url,
137
138    /// The URLs of the builders to use for submitting transactions
139    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    /// Base timeout for catchup requests to peers.
147    pub catchup_base_timeout: Duration,
148    /// Timeout for local catchup provider requests.
149    pub local_catchup_timeout: Duration,
150    /// Per-step timeout for the startup stake-table catchup walk
151    /// (`bootstrap_epoch_window`).
152    pub bootstrap_epoch_catchup_timeout: Duration,
153    /// The address to advertise as our public API's URL
154    pub public_api_url: Option<Url>,
155    /// Cliquenet network address.
156    pub cliquenet_bind_addr: NetAddr,
157    /// Cliquenet address to advertise to other nodes (registered in the stake table).
158    pub cliquenet_advertise_addr: Option<NetAddr>,
159    /// X25519 secret key.
160    pub x25519_secret_key: x25519::SecretKey,
161    /// The address to send to other Libp2p nodes to contact us. Required for orchestrator
162    /// bootstrap; optional otherwise. When set, it is added to the swarm as an external address
163    /// so peers can reach us behind NAT.
164    pub libp2p_advertise_address: Option<String>,
165    /// The address to bind to for Libp2p
166    pub libp2p_bind_address: String,
167    /// The (optional) bootstrap node addresses for Libp2p. If supplied, these will
168    /// override the bootstrap nodes specified in the config file.
169    pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
170
171    /// The heartbeat interval
172    pub libp2p_heartbeat_interval: Duration,
173
174    /// The number of past heartbeats to gossip about
175    pub libp2p_history_gossip: usize,
176    /// The number of past heartbeats to remember the full messages for
177    pub libp2p_history_length: usize,
178
179    /// The target number of peers in the mesh
180    pub libp2p_mesh_n: usize,
181    /// The maximum number of peers in the mesh
182    pub libp2p_mesh_n_high: usize,
183    /// The minimum number of peers in the mesh
184    pub libp2p_mesh_n_low: usize,
185    /// The minimum number of mesh peers that must be outbound
186    pub libp2p_mesh_outbound_min: usize,
187
188    /// The maximum gossip message size
189    pub libp2p_max_gossip_transmit_size: usize,
190
191    /// The maximum direct message size
192    pub libp2p_max_direct_transmit_size: u64,
193
194    /// The maximum number of IHAVE messages to accept from a Libp2p peer within a heartbeat
195    pub libp2p_max_ihave_length: usize,
196
197    /// The maximum number of IHAVE messages to accept from a Libp2p peer within a heartbeat
198    pub libp2p_max_ihave_messages: usize,
199
200    /// The time period that message hashes are stored in the cache
201    pub libp2p_published_message_ids_cache_time: Duration,
202
203    /// The time to wait for a Libp2p message requested through IWANT following an IHAVE advertisement
204    pub libp2p_iwant_followup_time: Duration,
205
206    /// The maximum number of Libp2p messages we will process in a given RPC
207    pub libp2p_max_messages_per_rpc: Option<usize>,
208
209    /// How many times we will allow a peer to request the same message id through IWANT gossip before we start ignoring them
210    pub libp2p_gossip_retransmission: u32,
211
212    /// If enabled newly created messages will always be sent to all peers that are subscribed to the topic and have a good enough score
213    pub libp2p_flood_publish: bool,
214
215    /// The time period that Libp2p message hashes are stored in the cache
216    pub libp2p_duplicate_cache_time: Duration,
217
218    /// Time to live for Libp2p fanout peers
219    pub libp2p_fanout_ttl: Duration,
220
221    /// Initial delay in each Libp2p heartbeat
222    pub libp2p_heartbeat_initial_delay: Duration,
223
224    /// How many Libp2p peers we will emit gossip to at each heartbeat
225    pub libp2p_gossip_factor: f64,
226
227    /// Minimum number of Libp2p peers to emit gossip to during a heartbeat
228    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    // Expose genesis version fields via the status API.
256    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    // Expose git information via status API.
276    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    // Expose Node Entity Information via the status/metrics API
306    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    // Expose Node Identity Location via the status/metrics API
333    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    // Expose icons for node dashboard via the status/metrics API
348    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    // Stick our public key in `metrics` so it is easily accessible via the status API.
388    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    // Parse the Libp2p bind and advertise addresses to multiaddresses
394    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    // Always pass the configured address to the orchestrator stake table; that path is
426    // testing-only and demo-native legitimately uses loopback.
427    let libp2p_announce_addresses: Vec<Multiaddr> = advertise_multiaddr.iter().cloned().collect();
428
429    // Only register the advertise address as a libp2p `external_address` when it looks
430    // publicly routable: announcing local/private values via Identify / Kademlia poisons peer
431    // routing tables in production. Local tests don't need it since peers find each other via
432    // `libp2p_bootstrap_nodes`.
433    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    // Orchestrator client
444    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    // Only the orchestrator bootstrap path publishes these into the stake table; overridden
448    // below when needed.
449    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    // Derive our Libp2p public key from our private key
461    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    // Print the libp2p public key
467    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        // If we were told to fetch the config from an already-started peer, do so.
479        (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        // Otherwise, this is a fresh network; load from the orchestrator.
497        (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            // Publish our cliquenet `connect_info` into the stake table from
504            // `NEW_PROTOCOL_VERSION` on, so peers can dial us. Modify `validator_config`
505            // in place so the same `connect_info` is sent later when posting to
506            // `/ready` (the orchestrator equality-checks against `known_nodes_with_stake`).
507            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                // Register in our Libp2p advertise address and public key so other nodes
526                // can contact us on startup
527                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    // Override the builder URLs in the network config with the ones from the command line
548    // if any were provided
549    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    // Save *after* the above updates. The orchestrator and peer fetched configs don't include
581    // epoch_height, drb_difficulty, etc. those come from genesis and are applied above.
582    // Saving before these updates would persist zeros for those values
583    if persist_config {
584        persistence.save_config(&network_config).await?;
585    }
586
587    // If the `Libp2p` bootstrap nodes were supplied via the command line, override those
588    // present in the config file.
589    if let Some(bootstrap_nodes) = network_params.libp2p_bootstrap_nodes {
590        if let Some(libp2p_config) = network_config.libp2p_config.as_mut() {
591            // If the libp2p configuration is present, we can override the bootstrap nodes.
592
593            // Split off the peer ID from the addresses
594            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            // If not, don't try launching with them. Eventually we may want to
601            // provide a default configuration here instead.
602            tracing::warn!("No libp2p configuration found, ignoring supplied bootstrap nodes");
603        }
604    }
605
606    let node_index = network_config.node_index;
607
608    // If we are a DA node, we need to subscribe to the DA topic
609    let topics = {
610        let mut topics = vec![CdnTopic::Global];
611        if is_da {
612            topics.push(CdnTopic::Da);
613        }
614        topics
615    };
616
617    // Initialize the push CDN network (and perform the initial connection)
618    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    // Configure gossipsub based on the command line options
630    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    // Configure request/response based on the command line options
654    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    // Create the list of parallel catchup providers
701    let state_catchup_providers =
702        ParallelStateCatchup::new(&[], network_params.local_catchup_timeout);
703
704    // Add the state peers to the list
705    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    // Add the local (persistence) catchup provider to the list (if we can)
714    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    // Create the HotShot membership
745    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    // Load saved consensus state from storage. It is loaded once here, both to
799    // seed consensus in `SequencerContext::init` below and to tell whether the
800    // network has already cut over to the new protocol.
801    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        // Mainnet keeps today's libp2p protocol strings byte-identical.
808        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            // We need the private key so we can derive our Libp2p keypair
819            // (using https://docs.rs/blake3/latest/blake3/fn.derive_key.html)
820            &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        // From `NEW_PROTOCOL_VERSION` on, all consensus traffic runs on
836        // cliquenet and the legacy stack is torn down at startup, so don't
837        // hold up boot waiting for legacy connectivity. The same applies when
838        // the configured base version predates the cutover but the network has
839        // already upgraded (a decided upgrade certificate is persisted): the
840        // legacy network may be gone entirely, so waiting could block boot
841        // forever.
842        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        // Combine the CDN and P2P networks
857        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
902/// On the version immediately preceding CLIQUENET, log an error if this
903/// validator has a stake-table entry without an x25519 key or p2p address.
904/// Skipped unless StakeTableV3 is deployed (otherwise the operator has no
905/// actionable path), detected via `getVersion()` on the proxy.
906async 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
968/// Whether the loaded consensus state shows the network has already upgraded
969/// to `NEW_PROTOCOL_VERSION`, even though the configured base version predates
970/// it: a decided upgrade certificate to the new protocol is stored and the
971/// view we restart from is past the cutover view.
972fn 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        // This should never fail.
1109        let url: Url = format!("http://localhost:{port}")
1110            .parse()
1111            .expect("Failed to parse builder URL");
1112
1113        // create the global state
1114        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        // Create and spawn the tide-disco app to serve the builder APIs
1131        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        // Pass on the builder task to be injected in the testing harness
1146        (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        // This should never fail.
1158        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    /// Picks the key sets at `indices` out of the full deterministic sequence,
1193    /// preserving the order of `indices`. Panics on duplicate or out-of-range
1194    /// indices.
1195    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    /// Deploys the contract suite up to and including the requested stake
1210    /// table version, recording addresses in `contracts`.
1211    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); // Create a deterministic RNG
1231        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 key derived from the BLS key, matching `init_node`'s
1241                // cliquenet bind.
1242                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        /// Sets the Anvil provider, constructed using the Anvil instance.
1265        /// Also sets the L1 URL based on the Anvil endpoint.
1266        /// The `AnvilProvider` can be used to configure the Anvil, for example,
1267        /// by enabling interval mining after the test network is initialized.
1268        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        /// Sets a custom L1 URL, overriding any previously set Anvil instance URL.
1277        /// This removes the anvil provider, as well as it is no longer needed
1278        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        /// Version specific upgrade setup. Extend to future upgrades
1302        /// by adding a branch to the `match` statement.
1303        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, &registered)
1306                .await
1307        }
1308
1309        /// Like [`Self::set_upgrades`], but deploys the requested stake table
1310        /// contract version and registers only the validators at the
1311        /// `registered` node indices. The deployed [`Contracts`] registry is
1312        /// retained and available via [`TestConfig::contracts`].
1313        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        /// Override the base next-view (failure) timeout. Raise it when using a large
1424        /// [`Self::builder_timeout`] so a slow-but-healthy view isn't mistaken for a failed one.
1425        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        /// Override the views during which the upgrade is proposed. Call after `set_upgrades`.
1431        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            // Generate keys for the nodes.
1472            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            // Reserve one cliquenet coordinator port per node.
1481            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        /// Per-node cliquenet coordinator bind addresses, indexed by node.
1594        coordinator_addrs: Vec<NetAddr>,
1595        /// Contracts deployed by [`TestConfigBuilder::set_upgrades_with`], if any.
1596        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        /// Key sets for the given node indices, aligned with
1646        /// [`Self::staking_priv_keys`]: the full deterministic sequence is
1647        /// generated first and then filtered, so a subset keeps the same keys
1648        /// per node index.
1649        pub fn staking_key_sets(&self, indices: &[usize]) -> Vec<StakingKeySet> {
1650            select_staking_key_sets(self.staking_priv_keys(), indices)
1651        }
1652
1653        /// The cliquenet coordinator address assigned to node `i`.
1654        pub fn coordinator_addr(&self, i: usize) -> NetAddr {
1655            self.coordinator_addrs[i].clone()
1656        }
1657
1658        /// Rebinds node `i`'s cliquenet coordinator to `addr`, taking effect
1659        /// the next time the node is initialized. Peers learn the address
1660        /// from the stake table contract, so the caller must also publish it
1661        /// there (`updateP2pAddr` or `updateNetworkConfig`).
1662        pub fn set_coordinator_addr(&mut self, i: usize, addr: NetAddr) {
1663            self.coordinator_addrs[i] = addr;
1664        }
1665
1666        /// Replaces node `i`'s consensus keys, taking effect the next time
1667        /// the node is initialized: it then signs with the new keys and
1668        /// derives its cliquenet x25519 identity from the new BLS key (see
1669        /// [`Self::init_node`]). The genesis stake table intentionally keeps
1670        /// the old keys — it must stay identical across nodes — so the new
1671        /// keys only become effective once the caller publishes the same
1672        /// rotation on-chain (`updateConsensusKeysV2`, plus
1673        /// `updateNetworkConfig` with the newly derived x25519 key) and the
1674        /// change reaches an epoch's stake table snapshot.
1675        ///
1676        /// Caveat: the light-client state signer checks membership against
1677        /// the genesis stake table, so a rotated node stops contributing
1678        /// state-relay signatures. No test combines rotation with a state
1679        /// relay yet.
1680        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        /// Contracts deployed by [`TestConfigBuilder::set_upgrades_with`], if
1686        /// that was used to set up this config.
1687        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            // The new-protocol (cliquenet) coordinator network identifies this
1752            // node by an x25519 key derived from its BLS key, reachable at its
1753            // pre-assigned coordinator address. These must match what is
1754            // registered on-chain for this validator (see `staking_priv_keys`),
1755            // so peers can resolve and dial each other from the stake table.
1756            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            // Make sure the builder account is funded.
1787            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            // Create an empty list of catchup providers
1796            let catchup_providers = ParallelStateCatchup::new(&[], Duration::from_secs(5));
1797
1798            // If we have the state peers, add them
1799            if let Some(state_peers) = state_peers {
1800                catchup_providers.add_provider(Arc::new(state_peers));
1801            }
1802
1803            // If we have a working local catchup provider, add it
1804            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                    // For testing, we use a fake network, so the rest of the network config beyond
1896                    // the base consensus config does not matter.
1897                    ..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    // Wait for the submitted transaction to be sequenced in a decided block. Return the block
1926    // number containing the transaction and the block payload size.
1927    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        // At 0.6 a decide carries the block payload only on the node that
1934        // built the block; every other node receives the payload through a
1935        // separate `BlockPayloadReconstructed` event, which can arrive before
1936        // or after the decide. Pair the two by view so the transaction is
1937        // only reported once its block is decided.
1938        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                // Decides arrive as `LegacyEvent` before the new protocol and
1966                // as `NewDecide` after.
1967                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    /// Waits until a node has reached the given target epoch (exclusive).
2002    /// The function returns once the first event indicates an epoch higher than `target_epoch`.
2003    /// Panics if the event stream ends before reaching `target_epoch`.
2004    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            // Decides arrive as `LegacyEvent` before the new protocol and as
2013            // `NewDecide` after; both carry the most recent leaf first.
2014            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        // Unknown chains (e.g. the demo chain) get no default.
2068        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        // Assign `config` so it isn't dropped early.
2080        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        // Hook the builder up to the event stream from the first node
2096        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        // Submit target transaction to handle
2112        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        // Assign `config` so it isn't dropped early.
2126        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        // Hook the builder up to the event stream from the first node
2143        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            // TODO refactor repeated code from other tests
2158            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            // Check that each successive header satisfies invariants relative to its parent: all
2185            // the fields which should be monotonic are.
2186            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}