From fd5abcfa5c68908cb113a99c33f6bb6bcc263bd0 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 12 May 2026 10:42:58 -0500 Subject: [PATCH 1/4] refactor(cli): replace --messaging JSON with --messaging.* and --settlement.* Re-apply CLI surface for messaging configuration on top of current main (which merged the messaging crate refactor in #564). The CLI now builds `Config.messaging: Option` from `--messaging.enabled` plus chain-spec settlement plus optional `--settlement.*` override flags, instead of consuming an opaque JSON file via `--messaging`. Changes: - `crates/cli/src/options.rs`: add `MessagingOptions`, `ChainSpec`, `SettlementOptions`, `SettlementChainKind`; remove `chain_id` from `EnvironmentOptions` (now lives on `ChainSpec`). - `crates/cli/src/args.rs`: flatten `chain: ChainSpecArgs` and `messaging: MessagingOptions` onto `SequencerNodeArgs`; drop the top-level `--chain` and `--messaging` JSON fields; add `build_messaging_config` and the `apply_chain_overrides` / `build_settlement_from_overrides` / `apply_partial_overrides` helpers that fold `--settlement.*` overrides into the chain spec. - `crates/cli/src/file.rs`: swap `messaging: Option` for `messaging: Option` and add `chain: Option` to the TOML config file shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cli/src/args.rs | 267 +++++++++++++++++++++++++++++++++----- crates/cli/src/file.rs | 9 +- crates/cli/src/options.rs | 174 +++++++++++++++++++++++-- 3 files changed, 402 insertions(+), 48 deletions(-) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 821b73871..957679f53 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -8,14 +8,12 @@ use alloy_primitives::U256; use anyhow::bail; use anyhow::{Context, Result}; pub use clap::Parser; -use katana_chain_spec::rollup::ChainConfigDir; use katana_chain_spec::ChainSpec; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; use katana_genesis::allocation::DevAllocationsGenerator; use katana_genesis::constant::{ DEFAULT_FROZEN_DEV_ACCOUNT_ADDRESS_CLASS_HASH, DEFAULT_PREFUNDED_ACCOUNT_BALANCE, }; -use katana_messaging::MessagingConfig; use katana_sequencer_node::config::db::DbConfig; use katana_sequencer_node::config::dev::{DevConfig, FixedL1GasPriceConfig}; use katana_sequencer_node::config::execution::ExecutionConfig; @@ -37,8 +35,8 @@ use tracing::info; use url::Url; use crate::file::NodeArgsConfig; -use crate::options::*; -use crate::utils::{self, parse_chain_config_dir, parse_seed, prompt_db_migration}; +use crate::options::{ChainSpec as ChainSpecArgs, *}; +use crate::utils::{self, parse_seed, prompt_db_migration}; pub(crate) const LOG_TARGET: &str = "katana::cli"; @@ -49,11 +47,6 @@ pub struct SequencerNodeArgs { #[arg(long)] pub silent: bool, - /// Path to the chain configuration file. - #[arg(long, hide = true)] - #[arg(value_parser = parse_chain_config_dir)] - pub chain: Option, - /// Disable auto and interval mining, and mine on demand instead via an endpoint. #[arg(long)] #[arg(conflicts_with = "block_time")] @@ -80,16 +73,6 @@ pub struct SequencerNodeArgs { #[arg(long)] pub config: Option, - /// Configure the messaging with an other chain. - /// - /// Configure the messaging to allow Katana listening/sending messages on a - /// settlement chain that can be Ethereum or an other Starknet sequencer. - #[arg(long)] - #[arg(value_name = "PATH")] - #[arg(value_parser = katana_messaging::MessagingConfig::parse)] - #[arg(conflicts_with = "chain")] - pub messaging: Option, - #[arg(long = "l1.provider", value_name = "URL", alias = "l1-provider")] #[arg(help = "The Ethereum RPC provider to sample the gas prices from to enable the gas \ price oracle.")] @@ -141,6 +124,12 @@ pub struct SequencerNodeArgs { #[command(flatten)] pub tee: TeeOptions, + #[command(flatten)] + pub chain: ChainSpecArgs, + + #[command(flatten)] + pub messaging: MessagingOptions, + #[cfg(all(feature = "server", feature = "grpc"))] #[command(flatten)] pub grpc: GrpcOptions, @@ -359,7 +348,7 @@ impl SequencerNodeArgs { let db = self.db_config()?; let rpc = self.rpc_config()?; let dev = self.dev_config(); - let (chain, cs_messaging) = self.chain_spec()?; + let chain = self.chain_spec()?; let metrics = self.metrics_config(); let gateway = self.gateway_config(); #[cfg(all(feature = "server", feature = "grpc"))] @@ -370,10 +359,7 @@ impl SequencerNodeArgs { let paymaster = self.paymaster_config(&chain)?; - // the `katana init` will automatically generate a messaging config. so if katana is run - // with `--chain` then the `--messaging` flag is not required. this is temporary and - // the messagign config will eventually be removed slowly. - let messaging = if cs_messaging.is_some() { cs_messaging } else { self.messaging.clone() }; + let messaging = self.build_messaging_config(&chain)?; Ok(Config { db, @@ -465,18 +451,17 @@ impl SequencerNodeArgs { } } - fn chain_spec(&self) -> Result<(Arc, Option)> { - if let Some(path) = &self.chain { + fn chain_spec(&self) -> Result> { + let mut chain_spec = if let Some(path) = &self.chain.path { let mut cs = katana_chain_spec::rollup::read(path)?; cs.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; - let messaging_config = MessagingConfig::from_chain_spec(&cs); - Ok((Arc::new(ChainSpec::Rollup(cs)), Some(messaging_config))) + ChainSpec::Rollup(cs) } // exclusively for development mode else { let mut chain_spec = katana_chain_spec::dev::DEV_UNALLOCATED.clone(); - if let Some(id) = self.starknet.environment.chain_id { + if let Some(id) = self.chain.chain_id { chain_spec.id = id; } @@ -501,8 +486,59 @@ impl SequencerNodeArgs { katana_slot_controller::add_vrf_provider_class(&mut chain_spec.genesis); } - Ok((Arc::new(ChainSpec::Dev(chain_spec)), None)) + ChainSpec::Dev(chain_spec) + }; + + apply_chain_overrides(&mut chain_spec, &self.chain)?; + + Ok(Arc::new(chain_spec)) + } + + /// Build the messaging config from chain spec settlement + CLI options. + /// Returns None when `--messaging.enabled` is false. Errors when enabled but the + /// chain spec has no settlement layer. + fn build_messaging_config( + &self, + chain: &ChainSpec, + ) -> Result> { + if !self.messaging.enabled { + return Ok(None); } + let settlement = chain.settlement().ok_or_else(|| { + anyhow::anyhow!( + "--messaging.enabled requires a settlement layer. Provide one via --chain with a \ + chain spec that defines settlement, or --settlement.* override flags." + ) + })?; + let (settlement_cfg, deployment_block) = match settlement { + katana_chain_spec::SettlementLayer::Ethereum { + rpc_url, core_contract, block, .. + } => ( + katana_messaging::SettlementChainConfig::Ethereum { + rpc_url: rpc_url.clone(), + contract_address: *core_contract, + }, + *block, + ), + katana_chain_spec::SettlementLayer::Starknet { + rpc_url, core_contract, block, .. + } => ( + katana_messaging::SettlementChainConfig::Starknet { + rpc_url: rpc_url.clone(), + contract_address: *core_contract, + }, + *block, + ), + katana_chain_spec::SettlementLayer::Sovereign { .. } => { + anyhow::bail!("sovereign settlement does not support messaging"); + } + }; + Ok(Some(katana_messaging::MessagingConfig { + settlement: settlement_cfg, + interval: self.messaging.interval, + from_block: self.messaging.from_block.unwrap_or(deployment_block), + confirmation_depth: 0, + })) } fn dev_config(&self) -> DevConfig { @@ -758,9 +794,8 @@ impl SequencerNodeArgs { } } - if self.messaging.is_none() { - self.messaging = config.messaging; - } + self.messaging.merge(config.messaging.as_ref()); + self.chain.merge(config.chain.as_ref()); #[cfg(feature = "server")] { @@ -818,6 +853,172 @@ impl SequencerNodeArgs { } } +/// Apply CLI / config-file settlement overrides on top of the loaded chain spec. +/// +/// Behaviour: +/// - If no overrides are set, this is a no-op. +/// - When the chain spec already has a settlement layer (Rollup, or Dev/FullNode with `Some(...)`), +/// supplied fields overwrite the corresponding fields. Unspecified fields are preserved from the +/// existing layer. +/// - When the chain spec has no settlement layer (Dev/FullNode with `None`), all three override +/// fields (`settlement-chain`, `settlement-rpc-url`, `settlement-contract`) must be supplied to +/// construct a fresh layer. +/// - If overrides change the chain *type* (e.g. existing `Ethereum` plus `--settlement.chain +/// starknet`), all three fields must be supplied — we cannot meaningfully reuse the previous +/// fields across chain types. +fn apply_chain_overrides(chain: &mut ChainSpec, overrides: &ChainSpecArgs) -> Result<()> { + if !overrides.settlement.any_set() { + return Ok(()); + } + + match chain { + ChainSpec::Dev(spec) => { + let settlement = match spec.settlement.take() { + Some(existing) => apply_partial_overrides(existing, overrides)?, + None => build_settlement_from_overrides(overrides)?, + }; + spec.settlement = Some(settlement); + } + ChainSpec::FullNode(spec) => { + let settlement = match spec.settlement.take() { + Some(existing) => apply_partial_overrides(existing, overrides)?, + None => build_settlement_from_overrides(overrides)?, + }; + spec.settlement = Some(settlement); + } + ChainSpec::Rollup(spec) => { + spec.settlement = apply_partial_overrides(spec.settlement.clone(), overrides)?; + } + } + Ok(()) +} + +/// Build a fresh [`katana_chain_spec::SettlementLayer`] from a fully-populated set of +/// overrides. Errors if any of `settlement_chain`, `settlement_rpc_url`, +/// `settlement_core_contract` is missing. +/// +/// Note: when constructing fresh, certain fields are not configurable from the CLI: +/// - Ethereum `id`: hardcoded to `1` (mainnet). Production rollups should use `--chain` with a +/// chain spec that fills this in correctly. +/// - Ethereum `account`: defaulted; not consumed by the messaging path. +/// - Starknet `id`: defaulted to [`katana_primitives::chain::ChainId`] default (id = 0). +/// - Starknet `proof_kind`: defaulted to `ValidityProof`. +fn build_settlement_from_overrides( + overrides: &ChainSpecArgs, +) -> Result { + use katana_chain_spec::{SettlementLayer, SettlementProofKind}; + use katana_primitives::chain::ChainId; + use katana_primitives::ContractAddress; + + let chain = overrides.settlement.chain.ok_or_else(|| { + anyhow::anyhow!( + "missing --settlement.chain; required to construct a settlement layer from overrides" + ) + })?; + let rpc_url = overrides.settlement.rpc_url.clone().ok_or_else(|| { + anyhow::anyhow!( + "missing --settlement.rpc-url; required to construct a settlement layer from overrides" + ) + })?; + let contract = overrides.settlement.core_contract.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "missing --settlement.core-contract; required to construct a settlement layer from \ + overrides" + ) + })?; + + match chain { + SettlementChainKind::Ethereum => { + let core_contract = contract.parse::().context( + "parse --settlement.core-contract as an Ethereum address (0x-prefixed hex)", + )?; + + Ok(SettlementLayer::Ethereum { + id: 1, // Default to mainnet; non-default ids must come from a chain spec file. + rpc_url, + account: alloy_primitives::Address::default(), + core_contract, + block: 0, + }) + } + SettlementChainKind::Starknet => { + let felt = katana_primitives::Felt::from_hex(contract).context( + "parse --settlement.core-contract as a Starknet contract address (0x-prefixed hex)", + )?; + Ok(SettlementLayer::Starknet { + id: ChainId::default(), + rpc_url, + core_contract: ContractAddress::new(felt), + block: 0, + proof_kind: SettlementProofKind::default(), + }) + } + } +} + +/// Apply per-field overrides on top of an existing [`SettlementLayer`]. +/// +/// If the override changes the chain type (Ethereum <-> Starknet), this defers to +/// [`build_settlement_from_overrides`] which requires all three fields. +fn apply_partial_overrides( + existing: katana_chain_spec::SettlementLayer, + overrides: &ChainSpecArgs, +) -> Result { + use katana_chain_spec::SettlementLayer; + use katana_primitives::ContractAddress; + + let target_chain = overrides.settlement.chain; + + match (&existing, target_chain) { + // No chain-type change requested, or matches existing → patch fields in place. + (SettlementLayer::Ethereum { .. }, None | Some(SettlementChainKind::Ethereum)) => { + let SettlementLayer::Ethereum { id, rpc_url, account, core_contract, block } = existing + else { + unreachable!("matched on Ethereum variant above"); + }; + + let rpc_url = overrides.settlement.rpc_url.clone().unwrap_or(rpc_url); + let core_contract = if let Some(addr) = &overrides.settlement.core_contract { + addr.parse::().context( + "parse --settlement.core-contract as an Ethereum address (0x-prefixed hex)", + )? + } else { + core_contract + }; + + Ok(SettlementLayer::Ethereum { id, rpc_url, account, core_contract, block }) + } + + (SettlementLayer::Starknet { .. }, None | Some(SettlementChainKind::Starknet)) => { + let SettlementLayer::Starknet { id, rpc_url, core_contract, block, proof_kind } = + existing + else { + unreachable!("matched on Starknet variant above"); + }; + + let rpc_url = overrides.settlement.rpc_url.clone().unwrap_or(rpc_url); + let core_contract = if let Some(addr) = &overrides.settlement.core_contract { + let felt = katana_primitives::Felt::from_hex(addr).context( + "parse --settlement.core-contract as a Starknet contract address (0x-prefixed \ + hex)", + )?; + ContractAddress::new(felt) + } else { + core_contract + }; + + Ok(SettlementLayer::Starknet { id, rpc_url, core_contract, block, proof_kind }) + } + + // Sovereign existing settlement: any override has to construct a non-Sovereign layer + // from scratch — all three fields required. + (SettlementLayer::Sovereign { .. }, _) => build_settlement_from_overrides(overrides), + + // Chain type swap (Ethereum <-> Starknet): treat as constructing fresh. + _ => build_settlement_from_overrides(overrides), + } +} + #[cfg(test)] mod test { use std::str::FromStr; diff --git a/crates/cli/src/file.rs b/crates/cli/src/file.rs index 07d9b496a..36f0fe1e8 100644 --- a/crates/cli/src/file.rs +++ b/crates/cli/src/file.rs @@ -1,10 +1,9 @@ use std::path::Path; use anyhow::Result; -use katana_messaging::MessagingConfig; use serde::{Deserialize, Serialize}; -use crate::options::*; +use crate::options::{ChainSpec, *}; use crate::SequencerNodeArgs; /// Node arguments configuration file. @@ -16,7 +15,8 @@ pub struct NodeArgsConfig { pub no_state_trie: Option, #[serde(flatten)] pub db: Option, - pub messaging: Option, + pub messaging: Option, + pub chain: Option, pub logging: Option, pub starknet: Option, pub gpo: Option, @@ -55,7 +55,8 @@ impl TryFrom for NodeArgsConfig { block_cairo_steps_limit: args.block_cairo_steps_limit, no_state_trie: if args.no_state_trie { Some(true) } else { None }, db: (!args.db.is_default()).then_some(args.db), - messaging: args.messaging, + messaging: (args.messaging != MessagingOptions::default()).then_some(args.messaging), + chain: (args.chain != ChainSpec::default()).then_some(args.chain), ..Default::default() }; diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index b42042da8..bac084d46 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -322,16 +322,6 @@ impl StarknetOptions { #[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq)] #[command(next_help_heading = "Environment options")] pub struct EnvironmentOptions { - /// The chain ID. - /// - /// The chain ID. If a raw hex string (`0x` prefix) is provided, then it'd - /// used as the actual chain ID. Otherwise, it's represented as the raw - /// ASCII values. It must be a valid Cairo short string. - #[arg(long, conflicts_with = "chain")] - #[arg(value_parser = ChainId::parse)] - #[serde(default)] - pub chain_id: Option, - /// The maximum number of steps available for the account validation logic. #[arg(long)] #[arg(default_value_t = DEFAULT_VALIDATION_MAX_STEPS)] @@ -356,7 +346,6 @@ impl Default for EnvironmentOptions { EnvironmentOptions { validate_max_steps: DEFAULT_VALIDATION_MAX_STEPS, invoke_max_steps: DEFAULT_INVOCATION_MAX_STEPS, - chain_id: None, #[cfg(feature = "native")] compile_native: false, } @@ -1188,3 +1177,166 @@ fn default_grpc_addr() -> IpAddr { fn default_grpc_port() -> u16 { katana_sequencer_node::config::grpc::DEFAULT_GRPC_PORT } + +const DEFAULT_MESSAGING_INTERVAL: u64 = 2; + +fn default_messaging_interval() -> u64 { + DEFAULT_MESSAGING_INTERVAL +} + +/// Messaging service options. +#[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[command(next_help_heading = "Messaging options")] +#[serde(default)] +pub struct MessagingOptions { + /// Enable the messaging service. + /// + /// When enabled, Katana consumes L1 -> L2 messages from the settlement chain configured + /// on the chain spec. Requires the chain spec (loaded via `--chain` or assembled from + /// `--settlement.*` overrides) to define a settlement layer. + #[arg(long = "messaging.enabled", id = "messaging_enabled")] + #[serde(default)] + pub enabled: bool, + + /// Polling interval for new settlement blocks, in seconds. + #[arg(long = "messaging.interval", value_name = "SECS")] + #[arg(default_value_t = DEFAULT_MESSAGING_INTERVAL)] + #[serde(default = "default_messaging_interval")] + pub interval: u64, + + /// Override the starting block on the settlement chain. + /// + /// Useful for replay / debugging. When set, takes precedence over the persisted + /// checkpoint and the chain spec's deployment block. + #[arg(long = "messaging.from-block", value_name = "N")] + #[serde(default)] + pub from_block: Option, +} + +impl Default for MessagingOptions { + fn default() -> Self { + Self { enabled: false, interval: DEFAULT_MESSAGING_INTERVAL, from_block: None } + } +} + +impl MessagingOptions { + pub fn merge(&mut self, other: Option<&Self>) { + if let Some(other) = other { + if !self.enabled { + self.enabled = other.enabled; + } + if self.interval == DEFAULT_MESSAGING_INTERVAL { + self.interval = other.interval; + } + if self.from_block.is_none() { + self.from_block = other.from_block; + } + } + } +} + +/// Chain specification: where the chain spec file lives, plus per-field overrides for the +/// settlement layer. +/// +/// `path` points at a chain spec file (loaded via `--chain`). The `settlement_*` fields let +/// users drop in an Ethereum (e.g. anvil) or Starknet endpoint without authoring a full chain +/// spec file. When the active chain spec already has a settlement layer, only the supplied +/// fields are overwritten — the rest of the layer is preserved. When the chain spec has no +/// settlement, all three values must be supplied to construct a fresh one. +#[derive(Debug, Args, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +#[command(next_help_heading = "Chain spec")] +#[serde(default)] +pub struct ChainSpec { + /// Path to the chain configuration file. + #[arg(long = "chain", id = "chain", hide = true)] + #[arg(value_parser = crate::utils::parse_chain_config_dir)] + #[serde(skip)] + pub path: Option, + + /// The chain ID. + /// + /// If a raw hex string (`0x` prefix) is provided, it is used as the actual chain id. + /// Otherwise, it is interpreted as the raw ASCII values and must be a valid Cairo short + /// string. Mutually exclusive with `--chain` (a chain spec file already encodes its id). + #[arg(long = "chain-id", alias = "chain.id", conflicts_with = "chain")] + #[arg(value_parser = ChainId::parse)] + #[serde(default)] + pub chain_id: Option, + + /// Settlement layer overrides. + #[command(flatten)] + #[serde(default)] + pub settlement: SettlementOptions, +} + +impl ChainSpec { + pub fn merge(&mut self, other: Option<&Self>) { + if let Some(other) = other { + if self.chain_id.is_none() { + self.chain_id = other.chain_id; + } + self.settlement.merge(Some(&other.settlement)); + // `path` is intentionally not merged from TOML config — it's CLI-only. + } + } +} + +/// The settlement chains Katana supports for messaging. +/// +/// This is the type-level whitelist for the `--settlement.chain` CLI flag and the +/// `[chain.settlement].chain` TOML key. Settlement chains outside this enum (e.g., the +/// `Sovereign` variant of [`katana_chain_spec::SettlementLayer`]) are not supported by the +/// messaging service. +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Serialize, Deserialize)] +#[clap(rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum SettlementChainKind { + Ethereum, + Starknet, +} + +/// Per-field overrides for the chain spec's settlement layer. +/// +/// Set via `--settlement.*` CLI flags or under the `[chain.settlement]` section of the +/// TOML config. When the active chain spec already has a settlement layer, only the supplied +/// fields are overwritten — the rest of the layer is preserved. When the chain spec has no +/// settlement, all three values must be supplied to construct a fresh one. +#[derive(Debug, Args, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +#[serde(default)] +pub struct SettlementOptions { + /// Override the settlement chain type. + #[arg(long = "settlement.chain", id = "settlement_chain", value_name = "CHAIN")] + #[serde(default)] + pub chain: Option, + + /// Override the settlement chain RPC URL. + #[arg(long = "settlement.rpc-url", id = "settlement_rpc_url", value_name = "URL")] + #[serde(default)] + pub rpc_url: Option, + + /// Override the settlement core contract address. + #[arg(long = "settlement.core-contract", id = "settlement_core_contract", value_name = "ADDR")] + #[serde(default)] + pub core_contract: Option, +} + +impl SettlementOptions { + /// True if any settlement override field is set. + pub fn any_set(&self) -> bool { + self.chain.is_some() || self.rpc_url.is_some() || self.core_contract.is_some() + } + + pub fn merge(&mut self, other: Option<&Self>) { + if let Some(other) = other { + if self.chain.is_none() { + self.chain = other.chain; + } + if self.rpc_url.is_none() { + self.rpc_url = other.rpc_url.clone(); + } + if self.core_contract.is_none() { + self.core_contract = other.core_contract.clone(); + } + } + } +} From 2007e16c5b6149ecda637dcd5f69242a145397e8 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 12 May 2026 15:24:42 -0500 Subject: [PATCH 2/4] fix(cli): reject messaging.interval = 0 before it reaches the messenger A zero polling interval panics deep inside `tokio::time::interval` construction in the messaging trigger. Reject it at both entry points: - CLI: `value_parser` range on `--messaging.interval` so clap fails with a standard out-of-range message. - TOML: explicit guard in `build_messaging_config` (the CLI parser doesn't run on values that came from a config file). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cli/src/args.rs | 9 +++++++++ crates/cli/src/options.rs | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 957679f53..2e4f4832a 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -504,6 +504,15 @@ impl SequencerNodeArgs { if !self.messaging.enabled { return Ok(None); } + // The CLI value_parser rejects interval=0, but TOML config can still reach here with one. + // Catch it before we hand the value to the messenger (which would panic in + // `tokio::time::interval` construction). + if self.messaging.interval == 0 { + anyhow::bail!( + "messaging.interval must be at least 1 second (got 0); set a non-zero value via \ + --messaging.interval or the [messaging].interval TOML key" + ); + } let settlement = chain.settlement().ok_or_else(|| { anyhow::anyhow!( "--messaging.enabled requires a settlement layer. Provide one via --chain with a \ diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index bac084d46..05aa354e3 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -1198,9 +1198,10 @@ pub struct MessagingOptions { #[serde(default)] pub enabled: bool, - /// Polling interval for new settlement blocks, in seconds. + /// Polling interval for new settlement blocks, in seconds. Must be at least 1. #[arg(long = "messaging.interval", value_name = "SECS")] #[arg(default_value_t = DEFAULT_MESSAGING_INTERVAL)] + #[arg(value_parser = clap::value_parser!(u64).range(1..))] #[serde(default = "default_messaging_interval")] pub interval: u64, From b50b30da90bf8e49f683f389efee1db8d76aaa9b Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 12 May 2026 15:34:41 -0500 Subject: [PATCH 3/4] test(cli): cover `apply_chain_overrides` settlement-layer behaviors Adds unit tests for the settlement override resolver: short-circuit on no inputs, fresh construction (Ethereum + Starknet) with partial-input rejection, per-field patching on existing layers (Ethereum + Starknet), chain-kind swaps (and their all-three requirement), Sovereign replacement, and core-contract parse failures on both the fresh and partial paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/cli/src/args.rs | 330 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 2e4f4832a..ab50bb98f 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -1471,4 +1471,334 @@ explorer = true assert!(!config.chain.genesis().classes.contains_key(&ControllerV109::HASH)); assert!(!config.chain.genesis().classes.contains_key(&ControllerLatest::HASH)); } + + mod chain_overrides { + use katana_chain_spec::{dev, SettlementLayer, SettlementProofKind}; + use katana_primitives::ContractAddress; + + use super::*; + + fn dev_with(settlement: Option) -> ChainSpec { + let mut spec = dev::DEV_UNALLOCATED.clone(); + spec.settlement = settlement; + ChainSpec::Dev(spec) + } + + fn ethereum_layer() -> SettlementLayer { + SettlementLayer::Ethereum { + id: 1, + rpc_url: "http://eth.example:8545".parse().unwrap(), + account: alloy_primitives::Address::ZERO, + core_contract: "0x0000000000000000000000000000000000000001" + .parse::() + .unwrap(), + block: 100, + } + } + + fn starknet_layer() -> SettlementLayer { + SettlementLayer::Starknet { + id: ChainId::SEPOLIA, + rpc_url: "http://sn.example:5050".parse().unwrap(), + core_contract: ContractAddress::new(Felt::from(0x1234u32)), + block: 42, + proof_kind: SettlementProofKind::ValidityProof, + } + } + + fn overrides( + chain: Option, + rpc_url: Option<&str>, + core_contract: Option<&str>, + ) -> ChainSpecArgs { + ChainSpecArgs { + path: None, + chain_id: None, + settlement: SettlementOptions { + chain, + rpc_url: rpc_url.map(|s| s.parse().unwrap()), + core_contract: core_contract.map(String::from), + }, + } + } + + fn dev_settlement(spec: ChainSpec) -> Option { + match spec { + ChainSpec::Dev(s) => s.settlement, + other => panic!("expected Dev, got {other:?}"), + } + } + + #[test] + fn noop_when_no_overrides_set() { + // No settlement on the spec stays no-settlement. + let mut spec = dev_with(None); + apply_chain_overrides(&mut spec, &overrides(None, None, None)).unwrap(); + assert!(dev_settlement(spec).is_none()); + + // Existing settlement stays exactly equal (every field preserved). + let mut spec = dev_with(Some(ethereum_layer())); + apply_chain_overrides(&mut spec, &overrides(None, None, None)).unwrap(); + assert_eq!(dev_settlement(spec), Some(ethereum_layer())); + } + + #[test] + fn dev_with_no_settlement_and_all_three_builds_fresh_ethereum() { + let mut spec = dev_with(None); + let o = overrides( + Some(SettlementChainKind::Ethereum), + Some("http://eth.example:8545"), + Some("0x0000000000000000000000000000000000000123"), + ); + apply_chain_overrides(&mut spec, &o).unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { + id, rpc_url, account, core_contract, block, + } => { + // Defaulted fields when constructing from overrides. + assert_eq!(id, 1); + assert_eq!(account, alloy_primitives::Address::default()); + assert_eq!(block, 0); + // Supplied fields. + assert_eq!(rpc_url.as_str(), "http://eth.example:8545/"); + assert_eq!( + core_contract, + "0x0000000000000000000000000000000000000123" + .parse::() + .unwrap(), + ); + }); + } + + #[test] + fn dev_with_no_settlement_and_all_three_builds_fresh_starknet() { + let mut spec = dev_with(None); + let o = overrides( + Some(SettlementChainKind::Starknet), + Some("http://sn.example:5050"), + Some("0x1234"), + ); + apply_chain_overrides(&mut spec, &o).unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { + id, rpc_url, core_contract, block, proof_kind, + } => { + assert_eq!(id, ChainId::default()); + assert_eq!(block, 0); + assert_eq!(proof_kind, SettlementProofKind::ValidityProof); + assert_eq!(rpc_url.as_str(), "http://sn.example:5050/"); + assert_eq!(core_contract, ContractAddress::new(Felt::from(0x1234u32))); + }); + } + + #[test] + fn dev_with_no_settlement_errors_on_partial_overrides() { + // Only chain set. + let mut spec = dev_with(None); + let err = apply_chain_overrides( + &mut spec, + &overrides(Some(SettlementChainKind::Ethereum), None, None), + ) + .unwrap_err(); + assert!(err.to_string().contains("missing --settlement.rpc-url")); + + // Only rpc-url set. + let mut spec = dev_with(None); + let err = apply_chain_overrides( + &mut spec, + &overrides(None, Some("http://eth.example:8545"), None), + ) + .unwrap_err(); + assert!(err.to_string().contains("missing --settlement.chain")); + + // Only core-contract set. + let mut spec = dev_with(None); + let err = + apply_chain_overrides(&mut spec, &overrides(None, None, Some("0x1"))).unwrap_err(); + assert!(err.to_string().contains("missing --settlement.chain")); + } + + #[test] + fn dev_existing_ethereum_patches_rpc_url_only() { + let mut spec = dev_with(Some(ethereum_layer())); + apply_chain_overrides(&mut spec, &overrides(None, Some("http://other:8545"), None)) + .unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { + id, rpc_url, account, core_contract, block, + } => { + // Override applied. + assert_eq!(rpc_url.as_str(), "http://other:8545/"); + // Original fields preserved. + assert_eq!(id, 1); + assert_eq!(account, alloy_primitives::Address::ZERO); + assert_eq!( + core_contract, + "0x0000000000000000000000000000000000000001" + .parse::() + .unwrap(), + ); + assert_eq!(block, 100); + }); + } + + #[test] + fn dev_existing_ethereum_patches_core_contract_only() { + let mut spec = dev_with(Some(ethereum_layer())); + apply_chain_overrides( + &mut spec, + &overrides(None, None, Some("0x000000000000000000000000000000000000beef")), + ) + .unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { + rpc_url, core_contract, block, .. + } => { + assert_eq!( + core_contract, + "0x000000000000000000000000000000000000beef" + .parse::() + .unwrap(), + ); + // Other fields preserved. + assert_eq!(rpc_url.as_str(), "http://eth.example:8545/"); + assert_eq!(block, 100); + }); + } + + #[test] + fn dev_existing_ethereum_with_matching_chain_kind_still_patches() { + // Setting --settlement.chain to the current kind is allowed and behaves + // like a partial override (no fresh-construction requirement). + let mut spec = dev_with(Some(ethereum_layer())); + apply_chain_overrides( + &mut spec, + &overrides(Some(SettlementChainKind::Ethereum), Some("http://other:8545"), None), + ) + .unwrap(); + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { + rpc_url, block, .. + } => { + assert_eq!(rpc_url.as_str(), "http://other:8545/"); + assert_eq!(block, 100); + }); + } + + #[test] + fn dev_existing_starknet_patches_rpc_url_only() { + let mut spec = dev_with(Some(starknet_layer())); + apply_chain_overrides(&mut spec, &overrides(None, Some("http://other:5050"), None)) + .unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { + id, rpc_url, core_contract, block, proof_kind, + } => { + assert_eq!(rpc_url.as_str(), "http://other:5050/"); + // Original fields preserved. + assert_eq!(id, ChainId::SEPOLIA); + assert_eq!(core_contract, ContractAddress::new(Felt::from(0x1234u32))); + assert_eq!(block, 42); + assert_eq!(proof_kind, SettlementProofKind::ValidityProof); + }); + } + + #[test] + fn chain_swap_with_all_three_builds_fresh_layer() { + // Existing Ethereum + Starknet override with all three fields → fresh Starknet. + let mut spec = dev_with(Some(ethereum_layer())); + apply_chain_overrides( + &mut spec, + &overrides( + Some(SettlementChainKind::Starknet), + Some("http://sn.example:5050"), + Some("0xabcd"), + ), + ) + .unwrap(); + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { + rpc_url, core_contract, block, .. + } => { + assert_eq!(rpc_url.as_str(), "http://sn.example:5050/"); + assert_eq!(core_contract, ContractAddress::new(Felt::from(0xabcdu32))); + // Block is reset because we built a fresh layer. + assert_eq!(block, 0); + }); + } + + #[test] + fn chain_swap_with_partial_overrides_errors() { + // Chain-type swap demands all three fields (cannot reuse fields across types). + let mut spec = dev_with(Some(ethereum_layer())); + let err = apply_chain_overrides( + &mut spec, + &overrides(Some(SettlementChainKind::Starknet), None, None), + ) + .unwrap_err(); + assert!(err.to_string().contains("missing --settlement.rpc-url")); + } + + #[test] + fn sovereign_settlement_requires_all_three_fields() { + let mut spec = dev_with(Some(SettlementLayer::Sovereign {})); + let err = apply_chain_overrides( + &mut spec, + &overrides(Some(SettlementChainKind::Ethereum), None, None), + ) + .unwrap_err(); + assert!(err.to_string().contains("missing --settlement.rpc-url")); + } + + #[test] + fn sovereign_settlement_replaced_with_fresh_layer() { + let mut spec = dev_with(Some(SettlementLayer::Sovereign {})); + apply_chain_overrides( + &mut spec, + &overrides( + Some(SettlementChainKind::Ethereum), + Some("http://eth.example:8545"), + Some("0x0000000000000000000000000000000000000123"), + ), + ) + .unwrap(); + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { .. }); + } + + #[test] + fn invalid_ethereum_contract_address_errors() { + // Fresh-construction path. + let mut spec = dev_with(None); + let err = apply_chain_overrides( + &mut spec, + &overrides( + Some(SettlementChainKind::Ethereum), + Some("http://eth.example:8545"), + Some("not-an-address"), + ), + ) + .unwrap_err(); + assert!(err + .chain() + .any(|e| e.to_string().contains("parse --settlement.core-contract"))); + + // Partial-patch path on an existing Ethereum layer. + let mut spec = dev_with(Some(ethereum_layer())); + let err = + apply_chain_overrides(&mut spec, &overrides(None, None, Some("not-an-address"))) + .unwrap_err(); + assert!(err + .chain() + .any(|e| e.to_string().contains("parse --settlement.core-contract"))); + } + + #[test] + fn invalid_starknet_contract_address_errors() { + // Partial-patch path on an existing Starknet layer. + let mut spec = dev_with(Some(starknet_layer())); + let err = apply_chain_overrides(&mut spec, &overrides(None, None, Some("not-a-felt"))) + .unwrap_err(); + assert!(err + .chain() + .any(|e| e.to_string().contains("parse --settlement.core-contract"))); + } + } } From 8aed8aab2c7dee7a6f306cfd33c17fcdd3bd267e Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 12 May 2026 16:10:48 -0500 Subject: [PATCH 4/4] fmt --- crates/cli/src/args.rs | 236 ++++++++++++++++++----------------------- 1 file changed, 106 insertions(+), 130 deletions(-) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index ab50bb98f..bb377e258 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -1474,7 +1474,7 @@ explorer = true mod chain_overrides { use katana_chain_spec::{dev, SettlementLayer, SettlementProofKind}; - use katana_primitives::ContractAddress; + use katana_primitives::{eth_address, ContractAddress}; use super::*; @@ -1489,9 +1489,7 @@ explorer = true id: 1, rpc_url: "http://eth.example:8545".parse().unwrap(), account: alloy_primitives::Address::ZERO, - core_contract: "0x0000000000000000000000000000000000000001" - .parse::() - .unwrap(), + core_contract: eth_address!("0x0000000000000000000000000000000000000001"), block: 100, } } @@ -1545,28 +1543,21 @@ explorer = true #[test] fn dev_with_no_settlement_and_all_three_builds_fresh_ethereum() { let mut spec = dev_with(None); - let o = overrides( + let ovs = overrides( Some(SettlementChainKind::Ethereum), Some("http://eth.example:8545"), Some("0x0000000000000000000000000000000000000123"), ); - apply_chain_overrides(&mut spec, &o).unwrap(); + apply_chain_overrides(&mut spec, &ovs).unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { - id, rpc_url, account, core_contract, block, - } => { + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { id, rpc_url, account, core_contract, block } => { // Defaulted fields when constructing from overrides. assert_eq!(id, 1); assert_eq!(account, alloy_primitives::Address::default()); assert_eq!(block, 0); // Supplied fields. assert_eq!(rpc_url.as_str(), "http://eth.example:8545/"); - assert_eq!( - core_contract, - "0x0000000000000000000000000000000000000123" - .parse::() - .unwrap(), - ); + assert_eq!(core_contract, eth_address!("0x0000000000000000000000000000000000000123")); }); } @@ -1593,73 +1584,55 @@ explorer = true #[test] fn dev_with_no_settlement_errors_on_partial_overrides() { - // Only chain set. + // Case 1: Overriding settlement chain kind i.e., --settlement.chain only. + let mut spec = dev_with(None); - let err = apply_chain_overrides( - &mut spec, - &overrides(Some(SettlementChainKind::Ethereum), None, None), - ) - .unwrap_err(); + let ovs = overrides(Some(SettlementChainKind::Ethereum), None, None); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); assert!(err.to_string().contains("missing --settlement.rpc-url")); - // Only rpc-url set. + // Case 2: Overriding settlement rpc url i.e., --settlement.rpc-url only. + let mut spec = dev_with(None); - let err = apply_chain_overrides( - &mut spec, - &overrides(None, Some("http://eth.example:8545"), None), - ) - .unwrap_err(); + let ovs = overrides(None, Some("http://eth.example:8545"), None); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); assert!(err.to_string().contains("missing --settlement.chain")); - // Only core-contract set. + // Case 3: Overriding settlement core contract i.e., --settlement.core-contract only. + let mut spec = dev_with(None); - let err = - apply_chain_overrides(&mut spec, &overrides(None, None, Some("0x1"))).unwrap_err(); + let ovs = overrides(None, None, Some("0x1")); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); assert!(err.to_string().contains("missing --settlement.chain")); } #[test] - fn dev_existing_ethereum_patches_rpc_url_only() { + fn dev_with_ethereum_settlement_partial_overrides() { + // Case 1: Overriding settlement rpc url i.e., --settlement.rpc-url only. + let mut spec = dev_with(Some(ethereum_layer())); - apply_chain_overrides(&mut spec, &overrides(None, Some("http://other:8545"), None)) - .unwrap(); + let ovs = overrides(None, Some("http://other:8545"), None); + apply_chain_overrides(&mut spec, &ovs).unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { - id, rpc_url, account, core_contract, block, - } => { + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { id, rpc_url, account, core_contract, block } => { // Override applied. assert_eq!(rpc_url.as_str(), "http://other:8545/"); // Original fields preserved. assert_eq!(id, 1); assert_eq!(account, alloy_primitives::Address::ZERO); - assert_eq!( - core_contract, - "0x0000000000000000000000000000000000000001" - .parse::() - .unwrap(), - ); + assert_eq!(core_contract, eth_address!("0x0000000000000000000000000000000000000001")); assert_eq!(block, 100); }); - } - #[test] - fn dev_existing_ethereum_patches_core_contract_only() { + // Case 2: Overriding settlement core contract i.e., --settlement.core-contract only. + let mut spec = dev_with(Some(ethereum_layer())); - apply_chain_overrides( - &mut spec, - &overrides(None, None, Some("0x000000000000000000000000000000000000beef")), - ) - .unwrap(); + let ovs = overrides(None, None, Some("0x000000000000000000000000000000000000beef")); + apply_chain_overrides(&mut spec, &ovs).unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { - rpc_url, core_contract, block, .. - } => { - assert_eq!( - core_contract, - "0x000000000000000000000000000000000000beef" - .parse::() - .unwrap(), - ); + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { rpc_url, core_contract, block, .. } => { + // Override applied. + assert_eq!(core_contract, eth_address!("0x000000000000000000000000000000000000beef")); // Other fields preserved. assert_eq!(rpc_url.as_str(), "http://eth.example:8545/"); assert_eq!(block, 100); @@ -1667,59 +1640,66 @@ explorer = true } #[test] - fn dev_existing_ethereum_with_matching_chain_kind_still_patches() { - // Setting --settlement.chain to the current kind is allowed and behaves - // like a partial override (no fresh-construction requirement). - let mut spec = dev_with(Some(ethereum_layer())); - apply_chain_overrides( - &mut spec, - &overrides(Some(SettlementChainKind::Ethereum), Some("http://other:8545"), None), - ) - .unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { - rpc_url, block, .. - } => { - assert_eq!(rpc_url.as_str(), "http://other:8545/"); - assert_eq!(block, 100); - }); - } + fn dev_with_starknet_settlement_partial_overrides() { + // Case 1: Overriding settlement rpc url i.e., --settlement.rpc-url only. - #[test] - fn dev_existing_starknet_patches_rpc_url_only() { let mut spec = dev_with(Some(starknet_layer())); - apply_chain_overrides(&mut spec, &overrides(None, Some("http://other:5050"), None)) - .unwrap(); + let ovs = overrides(None, Some("http://other:5050"), None); + apply_chain_overrides(&mut spec, &ovs).unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { - id, rpc_url, core_contract, block, proof_kind, - } => { + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { id, rpc_url, core_contract, block, proof_kind } => { assert_eq!(rpc_url.as_str(), "http://other:5050/"); // Original fields preserved. assert_eq!(id, ChainId::SEPOLIA); - assert_eq!(core_contract, ContractAddress::new(Felt::from(0x1234u32))); + assert_eq!(core_contract, address!("0x1234")); assert_eq!(block, 42); assert_eq!(proof_kind, SettlementProofKind::ValidityProof); }); + + // Case 2: Overriding settlement core contract i.e., --settlement.core-contract only. + + let mut spec = dev_with(Some(ethereum_layer())); + let ovs = overrides(None, None, Some("0x000000000000000000000000000000000000beef")); + apply_chain_overrides(&mut spec, &ovs).unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { rpc_url, core_contract, block, .. } => { + // Override applied. + assert_eq!(core_contract, eth_address!("0x000000000000000000000000000000000000beef")); + // Other fields preserved. + assert_eq!(rpc_url.as_str(), "http://eth.example:8545/"); + assert_eq!(block, 100); + }); + } + + #[test] + fn dev_existing_ethereum_with_matching_chain_kind_still_patches() { + // Setting --settlement.chain to the current kind is allowed and behaves + // like a partial override (no fresh-construction requirement). + let mut spec = dev_with(Some(ethereum_layer())); + let ovs = + overrides(Some(SettlementChainKind::Ethereum), Some("http://other:8545"), None); + apply_chain_overrides(&mut spec, &ovs).unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { rpc_url, block, .. } => { + assert_eq!(rpc_url.as_str(), "http://other:8545/"); + assert_eq!(block, 100); + }); } #[test] fn chain_swap_with_all_three_builds_fresh_layer() { // Existing Ethereum + Starknet override with all three fields → fresh Starknet. let mut spec = dev_with(Some(ethereum_layer())); - apply_chain_overrides( - &mut spec, - &overrides( - Some(SettlementChainKind::Starknet), - Some("http://sn.example:5050"), - Some("0xabcd"), - ), - ) - .unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { - rpc_url, core_contract, block, .. - } => { + let ovs = overrides( + Some(SettlementChainKind::Starknet), + Some("http://sn.example:5050"), + Some("0xabcd"), + ); + apply_chain_overrides(&mut spec, &ovs).unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Starknet { rpc_url, core_contract, block, .. } => { assert_eq!(rpc_url.as_str(), "http://sn.example:5050/"); - assert_eq!(core_contract, ContractAddress::new(Felt::from(0xabcdu32))); + assert_eq!(core_contract, address!("0xabcd")); // Block is reset because we built a fresh layer. assert_eq!(block, 0); }); @@ -1729,62 +1709,57 @@ explorer = true fn chain_swap_with_partial_overrides_errors() { // Chain-type swap demands all three fields (cannot reuse fields across types). let mut spec = dev_with(Some(ethereum_layer())); - let err = apply_chain_overrides( - &mut spec, - &overrides(Some(SettlementChainKind::Starknet), None, None), - ) - .unwrap_err(); + let ovs = overrides(Some(SettlementChainKind::Starknet), None, None); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); assert!(err.to_string().contains("missing --settlement.rpc-url")); } #[test] fn sovereign_settlement_requires_all_three_fields() { let mut spec = dev_with(Some(SettlementLayer::Sovereign {})); - let err = apply_chain_overrides( - &mut spec, - &overrides(Some(SettlementChainKind::Ethereum), None, None), - ) - .unwrap_err(); + let ovs = overrides(Some(SettlementChainKind::Ethereum), None, None); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); assert!(err.to_string().contains("missing --settlement.rpc-url")); } #[test] fn sovereign_settlement_replaced_with_fresh_layer() { let mut spec = dev_with(Some(SettlementLayer::Sovereign {})); - apply_chain_overrides( - &mut spec, - &overrides( - Some(SettlementChainKind::Ethereum), - Some("http://eth.example:8545"), - Some("0x0000000000000000000000000000000000000123"), - ), - ) - .unwrap(); - assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { .. }); + let ovs = overrides( + Some(SettlementChainKind::Ethereum), + Some("http://eth.example:8545"), + Some("0x0000000000000000000000000000000000000123"), + ); + apply_chain_overrides(&mut spec, &ovs).unwrap(); + + assert_matches!(dev_settlement(spec).unwrap(), SettlementLayer::Ethereum { id, block, rpc_url, core_contract, .. } => { + assert_eq!(id, 1); + assert_eq!(block, 0); + assert_eq!(rpc_url.as_str(), "http://eth.example:8545/"); + assert_eq!(core_contract, eth_address!("0x0000000000000000000000000000000000000123")); + }); } #[test] fn invalid_ethereum_contract_address_errors() { // Fresh-construction path. let mut spec = dev_with(None); - let err = apply_chain_overrides( - &mut spec, - &overrides( - Some(SettlementChainKind::Ethereum), - Some("http://eth.example:8545"), - Some("not-an-address"), - ), - ) - .unwrap_err(); + let ovs = overrides( + Some(SettlementChainKind::Ethereum), + Some("http://eth.example:8545"), + Some("not-an-address"), + ); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); + assert!(err .chain() .any(|e| e.to_string().contains("parse --settlement.core-contract"))); // Partial-patch path on an existing Ethereum layer. let mut spec = dev_with(Some(ethereum_layer())); - let err = - apply_chain_overrides(&mut spec, &overrides(None, None, Some("not-an-address"))) - .unwrap_err(); + let ovs = overrides(None, None, Some("not-an-address")); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); + assert!(err .chain() .any(|e| e.to_string().contains("parse --settlement.core-contract"))); @@ -1794,8 +1769,9 @@ explorer = true fn invalid_starknet_contract_address_errors() { // Partial-patch path on an existing Starknet layer. let mut spec = dev_with(Some(starknet_layer())); - let err = apply_chain_overrides(&mut spec, &overrides(None, None, Some("not-a-felt"))) - .unwrap_err(); + let ovs = overrides(None, None, Some("not-a-felt")); + let err = apply_chain_overrides(&mut spec, &ovs).unwrap_err(); + assert!(err .chain() .any(|e| e.to_string().contains("parse --settlement.core-contract")));