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