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