From abc0b7586b6f89636944026c7d01a9bc1e6d1761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:11:37 -0300 Subject: [PATCH 1/4] feat(cli): default attestation_committee_count from validator-config.yaml Make `--attestation-committee-count` optional and fall back to the `config.attestation_committee_count` field in `validator-config.yaml`, then to `1`. Lets multi-subnet networks (e.g. lean-quickstart's 2-subnet ansible devnet) drop the per-client flag and rely on the shared genesis bundle. --- bin/ethlambda/src/main.rs | 90 ++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 3c3f816..408727a 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -76,9 +76,13 @@ struct CliOptions { /// use the admin endpoint to rotate duties (hot-standby model). #[arg(long, default_value = "false")] is_aggregator: bool, - /// Number of attestation committees (subnets) per slot - #[arg(long, default_value = "1", value_parser = clap::value_parser!(u64).range(1..))] - attestation_committee_count: u64, + /// Number of attestation committees (subnets) per slot. + /// + /// If unset, falls back to `config.attestation_committee_count` from + /// `validator-config.yaml` in the network config dir, or `1` if that + /// field is also absent. + #[arg(long, value_parser = clap::value_parser!(u64).range(1..))] + attestation_committee_count: Option, /// Subnet IDs this aggregator should subscribe to (comma-separated). /// Requires --is-aggregator. Defaults to the subnets of the node's validators. #[arg(long, value_delimiter = ',', requires = "is_aggregator")] @@ -102,9 +106,6 @@ async fn main() -> eyre::Result<()> { ethlambda_blockchain::metrics::init(); ethlambda_blockchain::metrics::set_node_info("ethlambda", version::CLIENT_VERSION); ethlambda_blockchain::metrics::set_node_start_time(); - ethlambda_blockchain::metrics::set_attestation_committee_count( - options.attestation_committee_count, - ); let api_socket = SocketAddr::new(options.http_address, options.api_port); let metrics_socket = SocketAddr::new(options.http_address, options.metrics_port); @@ -141,7 +142,31 @@ async fn main() -> eyre::Result<()> { "Loaded genesis configuration" ); - populate_name_registry(&validator_config); + let validator_config_file = read_validator_config_file(&validator_config); + populate_name_registry(&validator_config_file); + + // Resolve attestation_committee_count: CLI flag > validator-config.yaml > 1. + let attestation_committee_count = options + .attestation_committee_count + .or(validator_config_file.config.attestation_committee_count) + .unwrap_or(1); + info!( + attestation_committee_count, + source = if options.attestation_committee_count.is_some() { + "cli" + } else if validator_config_file + .config + .attestation_committee_count + .is_some() + { + "validator-config.yaml" + } else { + "default" + }, + "Resolved attestation committee count" + ); + ethlambda_blockchain::metrics::set_attestation_committee_count(attestation_committee_count); + let bootnodes = read_bootnodes(&bootnodes_path); let validator_keys = @@ -181,7 +206,7 @@ async fn main() -> eyre::Result<()> { bootnodes, listening_socket: p2p_socket, validator_ids, - attestation_committee_count: options.attestation_committee_count, + attestation_committee_count, is_aggregator: options.is_aggregator, aggregate_subnet_ids: options.aggregate_subnet_ids, }) @@ -223,25 +248,40 @@ async fn main() -> eyre::Result<()> { Ok(()) } -fn populate_name_registry(validator_config: impl AsRef) { - #[derive(Deserialize)] - struct Validator { - name: String, - privkey: H256, - } - #[derive(Deserialize)] - struct Config { - validators: Vec, - } - let config_yaml = - std::fs::read_to_string(&validator_config).expect("Failed to read validator config file"); - let config: Config = - serde_yaml_ng::from_str(&config_yaml).expect("Failed to parse validator config file"); +/// Subset of `validator-config.yaml` consumed by ethlambda. +/// +/// The `config` block is a network-wide settings bag shared across clients; +/// only fields ethlambda actually reads are deserialized. The `validators` +/// list feeds the metrics name registry. +#[derive(Debug, Deserialize)] +struct ValidatorConfigFile { + #[serde(default)] + config: ValidatorConfigBlock, + validators: Vec, +} + +#[derive(Debug, Default, Deserialize)] +struct ValidatorConfigBlock { + #[serde(default)] + attestation_committee_count: Option, +} + +#[derive(Debug, Deserialize)] +struct ValidatorConfigEntry { + name: String, + privkey: H256, +} + +fn read_validator_config_file(path: impl AsRef) -> ValidatorConfigFile { + let yaml = std::fs::read_to_string(&path).expect("Failed to read validator config file"); + serde_yaml_ng::from_str(&yaml).expect("Failed to parse validator config file") +} - let names_and_privkeys = config +fn populate_name_registry(file: &ValidatorConfigFile) { + let names_and_privkeys = file .validators - .into_iter() - .map(|v| (v.name, v.privkey)) + .iter() + .map(|v| (v.name.clone(), v.privkey)) .collect(); // Populates a dictionary used for labeling metrics with node names From 6490483e7308a5631ddc99c0f79e7ed29b0d86da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:34:15 -0300 Subject: [PATCH 2/4] test(cli): cover validator-config.yaml committee-count fallback Verify the CLI/file/default precedence and that the new fields parse against snippets from both the ansible-devnet (with the field set) and the local-devnet (with the field absent). --- bin/ethlambda/src/main.rs | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 408727a..16dd230 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -518,3 +518,96 @@ async fn fetch_initial_state( // Store the anchor state and header, without body Ok(Store::from_anchor_state(backend, state)) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Validator-config snippet matching `lean-quickstart`'s ansible-devnet + /// (devnet-4) where networks share a non-default committee count. + const VC_WITH_COMMITTEE_COUNT: &str = r#" +shuffle: roundrobin +deployment_mode: ansible +config: + activeEpoch: 18 + keyType: "hash-sig" + attestation_committee_count: 2 +validators: + - name: "ethlambda_0" + privkey: "299550529a79bc2dce003747c52fb0639465c893e00b0440ac66144d625e066a" + enrFields: + ip: "127.0.0.1" + quic: 9001 + metricsPort: 9095 + apiPort: 5055 + subnet: 0 + isAggregator: false + count: 1 +"#; + + /// Local-devnet snippet without the optional field — committee count is + /// expected to fall back to the binary default. + const VC_WITHOUT_COMMITTEE_COUNT: &str = r#" +shuffle: roundrobin +deployment_mode: local +config: + activeEpoch: 18 + keyType: "hash-sig" +validators: + - name: "ethlambda_0" + privkey: "299550529a79bc2dce003747c52fb0639465c893e00b0440ac66144d625e066a" + enrFields: + ip: "127.0.0.1" + quic: 9001 + metricsPort: 8087 + apiPort: 5055 + isAggregator: false + count: 1 +"#; + + #[test] + fn parses_committee_count_when_present() { + let file: ValidatorConfigFile = serde_yaml_ng::from_str(VC_WITH_COMMITTEE_COUNT).unwrap(); + assert_eq!(file.config.attestation_committee_count, Some(2)); + assert_eq!(file.validators.len(), 1); + assert_eq!(file.validators[0].name, "ethlambda_0"); + } + + #[test] + fn defaults_to_none_when_field_absent() { + let file: ValidatorConfigFile = + serde_yaml_ng::from_str(VC_WITHOUT_COMMITTEE_COUNT).unwrap(); + assert_eq!(file.config.attestation_committee_count, None); + } + + #[test] + fn cli_overrides_file_value() { + let file: ValidatorConfigFile = serde_yaml_ng::from_str(VC_WITH_COMMITTEE_COUNT).unwrap(); + let cli_override: Option = Some(5); + let resolved = cli_override + .or(file.config.attestation_committee_count) + .unwrap_or(1); + assert_eq!(resolved, 5); + } + + #[test] + fn falls_back_to_file_when_cli_absent() { + let file: ValidatorConfigFile = serde_yaml_ng::from_str(VC_WITH_COMMITTEE_COUNT).unwrap(); + let cli_override: Option = None; + let resolved = cli_override + .or(file.config.attestation_committee_count) + .unwrap_or(1); + assert_eq!(resolved, 2); + } + + #[test] + fn falls_back_to_default_when_neither_set() { + let file: ValidatorConfigFile = + serde_yaml_ng::from_str(VC_WITHOUT_COMMITTEE_COUNT).unwrap(); + let cli_override: Option = None; + let resolved = cli_override + .or(file.config.attestation_committee_count) + .unwrap_or(1); + assert_eq!(resolved, 1); + } +} From 8785a32edb6be740039ba8da7ccd95fbedce4f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:41:35 -0300 Subject: [PATCH 3/4] refactor: simplify code a bit --- bin/ethlambda/src/main.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 16dd230..297d61b 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -150,21 +150,7 @@ async fn main() -> eyre::Result<()> { .attestation_committee_count .or(validator_config_file.config.attestation_committee_count) .unwrap_or(1); - info!( - attestation_committee_count, - source = if options.attestation_committee_count.is_some() { - "cli" - } else if validator_config_file - .config - .attestation_committee_count - .is_some() - { - "validator-config.yaml" - } else { - "default" - }, - "Resolved attestation committee count" - ); + info!(attestation_committee_count, "Loaded attestation committee count"); ethlambda_blockchain::metrics::set_attestation_committee_count(attestation_committee_count); let bootnodes = read_bootnodes(&bootnodes_path); From 421cbba4d61926303af43b68748411216d28c778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:45:03 -0300 Subject: [PATCH 4/4] chore: fmt --- bin/ethlambda/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 297d61b..5309d19 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -150,7 +150,10 @@ async fn main() -> eyre::Result<()> { .attestation_committee_count .or(validator_config_file.config.attestation_committee_count) .unwrap_or(1); - info!(attestation_committee_count, "Loaded attestation committee count"); + info!( + attestation_committee_count, + "Loaded attestation committee count" + ); ethlambda_blockchain::metrics::set_attestation_committee_count(attestation_committee_count); let bootnodes = read_bootnodes(&bootnodes_path);