From f6ba24677aca6fe29509d4c6a2edbc8ae06a6db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:37:42 -0300 Subject: [PATCH 1/6] fix: move to using config.yaml --- CLAUDE.md | 14 ++- Cargo.lock | 2 +- Cargo.toml | 1 + bin/ethlambda/Cargo.toml | 3 +- bin/ethlambda/src/main.rs | 50 +++------- crates/common/types/Cargo.toml | 3 + crates/common/types/src/genesis.rs | 153 ++++++++++++++++++++++++++--- crates/common/types/src/state.rs | 13 +-- 8 files changed, 176 insertions(+), 63 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 98285e1..86150e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -245,11 +245,17 @@ actual_slot = finalized_slot + 1 + relative_index ## Configuration Files -**Genesis:** `genesis.json` (JSON format, cross-client compatible) -- `GENESIS_TIME`: Unix timestamp for slot 0 -- `GENESIS_VALIDATORS`: Array of 52-byte XMSS pubkeys (hex) +**Genesis:** `config.yaml` (YAML format, cross-client compatible) +```yaml +GENESIS_TIME: 1770407233 +GENESIS_VALIDATORS: + - "cd323f232b34ab26d6db7402c886e74ca81cfd3a..." # 52-byte XMSS pubkeys (hex) + - "b7b0f72e24801b02bda64073cb4de6699a416b37..." +``` +- Validator indices are assigned sequentially (0, 1, 2, ...) based on array order +- All genesis state fields (checkpoints, justified_slots, etc.) initialize to zero/empty defaults +- Matches Ream/Zeam format — no extra state fields in the config file -**Validators:** JSON array of `{"pubkey": "...", "index": 0}` **Bootnodes:** ENR records (Base64-encoded, RLP decoded for QUIC port + secp256k1 pubkey) ## Testing diff --git a/Cargo.lock b/Cargo.lock index 88a3df4..7558bbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1947,7 +1947,6 @@ dependencies = [ "ethlambda-types", "hex", "serde", - "serde_json", "serde_yaml_ng", "spawned-concurrency", "spawned-rt", @@ -2084,6 +2083,7 @@ dependencies = [ "hex", "leansig", "serde", + "serde_yaml_ng", "ssz_types", "thiserror 2.0.17", "tree_hash", diff --git a/Cargo.toml b/Cargo.toml index e925252..07efe5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ tracing = "0.1" thiserror = "2.0.9" serde = { version = "1", features = ["derive"] } serde_json = "1.0.117" +serde_yaml_ng = "0.10" hex = "0.4" spawned-concurrency = "0.4" diff --git a/bin/ethlambda/Cargo.toml b/bin/ethlambda/Cargo.toml index 16eb517..7300f93 100644 --- a/bin/ethlambda/Cargo.toml +++ b/bin/ethlambda/Cargo.toml @@ -20,8 +20,7 @@ tracing.workspace = true tracing-subscriber = "0.3" serde.workspace = true -serde_json.workspace = true -serde_yaml_ng = "0.10" +serde_yaml_ng.workspace = true hex.workspace = true clap.workspace = true diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index aa438dc..afcffd1 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -11,7 +11,7 @@ use clap::Parser; use ethlambda_p2p::{Bootnode, parse_enrs, start_p2p}; use ethlambda_types::primitives::H256; use ethlambda_types::{ - genesis::Genesis, + genesis::GenesisConfig, signature::ValidatorSecretKey, state::{State, Validator, ValidatorPubkeyBytes}, }; @@ -72,7 +72,7 @@ async fn main() { info!(node_key=?options.node_key, "got node key"); - let genesis_path = options.custom_network_config_dir.join("genesis.json"); + let config_path = options.custom_network_config_dir.join("config.yaml"); let bootnodes_path = options.custom_network_config_dir.join("nodes.yaml"); let validators_path = options .custom_network_config_dir @@ -82,18 +82,26 @@ async fn main() { .join("validator-config.yaml"); let validator_keys_dir = options.custom_network_config_dir.join("hash-sig-keys"); - let genesis_json = std::fs::read_to_string(&genesis_path).expect("Failed to read genesis.json"); - let genesis: Genesis = - serde_json::from_str(&genesis_json).expect("Failed to parse genesis.json"); + let config_yaml = std::fs::read_to_string(&config_path).expect("Failed to read config.yaml"); + let genesis_config: GenesisConfig = + serde_yaml_ng::from_str(&config_yaml).expect("Failed to parse config.yaml"); populate_name_registry(&validator_config); let bootnodes = read_bootnodes(&bootnodes_path); - let validators = read_validators(&validators_path); + let validators: Vec = genesis_config + .genesis_validators + .into_iter() + .enumerate() + .map(|(i, pubkey)| Validator { + pubkey, + index: i as u64, + }) + .collect(); let validator_keys = read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id); - let genesis_state = State::from_genesis(&genesis, validators); + let genesis_state = State::from_genesis(genesis_config.genesis_time, validators); let backend = Arc::new(RocksDBBackend::open("./data").expect("Failed to open RocksDB")); let store = Store::from_genesis(backend, genesis_state); @@ -162,6 +170,7 @@ fn read_bootnodes(bootnodes_path: impl AsRef) -> Vec { #[derive(Debug, Deserialize)] struct AnnotatedValidator { index: u64, + #[allow(dead_code)] // Present in YAML, needed for deserialization but not read in code #[serde(rename = "pubkey_hex")] #[serde(deserialize_with = "deser_pubkey_hex")] pubkey: ValidatorPubkeyBytes, @@ -183,33 +192,6 @@ where Ok(pubkey) } -fn read_validators(validators_path: impl AsRef) -> Vec { - let validators_yaml = - std::fs::read_to_string(validators_path).expect("Failed to read validators file"); - // File is a map from validator name to its annotated info (the info is inside a vec for some reason) - let validator_infos: BTreeMap> = - serde_yaml_ng::from_str(&validators_yaml).expect("Failed to parse validators file"); - - let mut validators: Vec = validator_infos - .into_values() - .map(|v| Validator { - pubkey: v[0].pubkey, - index: v[0].index, - }) - .collect(); - - validators.sort_by_key(|v| v.index); - let num_validators = validators.len(); - - validators.dedup_by_key(|v| v.index); - - if validators.len() != num_validators { - panic!("Duplicate validator indices found in config"); - } - - validators -} - fn read_validator_keys( validators_path: impl AsRef, validator_keys_dir: impl AsRef, diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index c841756..25f9f56 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -22,3 +22,6 @@ ethereum_ssz = "0.10.0" ssz_types = "0.14.0" tree_hash = "0.12.0" tree_hash_derive = "0.12.0" + +[dev-dependencies] +serde_yaml_ng.workspace = true diff --git a/crates/common/types/src/genesis.rs b/crates/common/types/src/genesis.rs index f4fdfbd..6a303df 100644 --- a/crates/common/types/src/genesis.rs +++ b/crates/common/types/src/genesis.rs @@ -1,17 +1,138 @@ -use ethereum_types::H256; -use serde::{Deserialize, Serialize}; - -use crate::state::{ChainConfig, Checkpoint}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Genesis { - pub config: ChainConfig, - pub latest_justified: Checkpoint, - pub latest_finalized: Checkpoint, - pub historical_block_hashes: Vec, - pub justified_slots: Vec, - // TODO: uncomment - pub justifications_roots: Vec, - // TODO: this is an SSZ bitlist - pub justifications_validators: String, +use serde::Deserialize; + +use crate::state::ValidatorPubkeyBytes; + +#[derive(Debug, Clone, Deserialize)] +pub struct GenesisConfig { + #[serde(rename = "GENESIS_TIME")] + pub genesis_time: u64, + #[serde(rename = "GENESIS_VALIDATORS")] + #[serde(deserialize_with = "deser_hex_pubkeys")] + pub genesis_validators: Vec, +} + +fn deser_hex_pubkeys<'de, D>(d: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::de::Error; + + let hex_strings: Vec = Vec::deserialize(d)?; + hex_strings + .into_iter() + .map(|s| { + let s = s.strip_prefix("0x").unwrap_or(&s); + let bytes = hex::decode(s) + .map_err(|_| D::Error::custom("GENESIS_VALIDATORS value is not valid hex"))?; + bytes + .try_into() + .map_err(|_| D::Error::custom("GENESIS_VALIDATORS pubkey length != 52")) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + primitives::ssz::TreeHash, + state::{State, Validator}, + }; + + const PUBKEY_A: &str = "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d2d25ca7b19604f5a180037046cf2a02e1da4a800"; + const PUBKEY_B: &str = "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333"; + const PUBKEY_C: &str = "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410"; + + const TEST_CONFIG_JSON: &str = r#"# Genesis Settings +GENESIS_TIME: 1770407233 + +# Key Settings +ACTIVE_EPOCH: 18 + +# Validator Settings +VALIDATOR_COUNT: 3 + +# Genesis Validator Pubkeys +GENESIS_VALIDATORS: + - "cd323f232b34ab26d6db7402c886e74ca81cfd3a0c659d2fe022356f25592f7d2d25ca7b19604f5a180037046cf2a02e1da4a800" + - "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333" + - "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410" +"#; + + #[test] + fn deserialize_genesis_config() { + let config: GenesisConfig = serde_yaml_ng::from_str(TEST_CONFIG_JSON) + .expect("Failed to deserialize genesis config"); + + assert_eq!(config.genesis_time, 1770407233); + assert_eq!(config.genesis_validators.len(), 3); + assert_eq!( + config.genesis_validators[0], + hex::decode(PUBKEY_A).unwrap().as_slice() + ); + assert_eq!( + config.genesis_validators[1], + hex::decode(PUBKEY_B).unwrap().as_slice() + ); + assert_eq!( + config.genesis_validators[2], + hex::decode(PUBKEY_C).unwrap().as_slice() + ); + } + + #[test] + fn state_from_genesis_uses_defaults() { + let validators = vec![Validator { + pubkey: hex::decode(PUBKEY_A).unwrap().try_into().unwrap(), + index: 0, + }]; + + let state = State::from_genesis(1770407233, validators); + + assert_eq!(state.config.genesis_time, 1770407233); + assert_eq!(state.slot, 0); + assert!(state.latest_justified.root.is_zero()); + assert_eq!(state.latest_justified.slot, 0); + assert!(state.latest_finalized.root.is_zero()); + assert_eq!(state.latest_finalized.slot, 0); + assert!(state.historical_block_hashes.is_empty()); + assert!(state.justified_slots.is_empty()); + assert!(state.justifications_roots.is_empty()); + assert!(state.justifications_validators.is_empty()); + } + + #[test] + fn state_from_genesis_root() { + let config: GenesisConfig = serde_yaml_ng::from_str(TEST_CONFIG_JSON).unwrap(); + + let validators: Vec = config + .genesis_validators + .into_iter() + .enumerate() + .map(|(i, pubkey)| Validator { + pubkey, + index: i as u64, + }) + .collect(); + let state = State::from_genesis(config.genesis_time, validators); + let root = state.tree_hash_root(); + + // Pin the state root so changes are caught immediately. + let expected = + hex::decode("362db4ffe968f1d100934797f6d3c7985b4aee9d96b328ad2e47243b8292e434") + .unwrap(); + assert_eq!(root.as_slice(), &expected[..], "state root mismatch"); + + let expected_block_root = + hex::decode("8b04a5a7c03abda086237c329392953a0308888e4a22481a39ce06a95f38b8c4") + .unwrap(); + let mut block = state.latest_block_header; + block.state_root = root; + let block_root = block.tree_hash_root(); + assert_eq!( + block_root.as_slice(), + &expected_block_root[..], + "justified root mismatch" + ); + } } diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index ec3e25b..893f1a3 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -3,7 +3,6 @@ use ssz_types::typenum::{U4096, U262144}; use crate::{ block::{BlockBody, BlockHeader}, - genesis::Genesis, primitives::{ H256, ssz::{Decode, DecodeError, Encode, TreeHash}, @@ -87,7 +86,7 @@ impl Validator { pub type ValidatorPubkeyBytes = [u8; 52]; impl State { - pub fn from_genesis(genesis: &Genesis, validators: Vec) -> Self { + pub fn from_genesis(genesis_time: u64, validators: Vec) -> Self { let genesis_header = BlockHeader { slot: 0, proposer_index: 0, @@ -102,11 +101,11 @@ impl State { JustificationValidators::with_capacity(0).expect("failed to initialize empty list"); Self { - config: genesis.config.clone(), + config: ChainConfig { genesis_time }, slot: 0, latest_block_header: genesis_header, - latest_justified: genesis.latest_justified, - latest_finalized: genesis.latest_finalized, + latest_justified: Checkpoint::default(), + latest_finalized: Checkpoint::default(), historical_block_hashes: Default::default(), justified_slots, validators, @@ -117,7 +116,9 @@ impl State { } /// Represents a checkpoint in the chain's history. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, TreeHash)] +#[derive( + Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, TreeHash, +)] pub struct Checkpoint { /// The root hash of the checkpoint's block. pub root: H256, From 5458d9a91ee6647d5f2d0b54a017ad157d40369d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:20:53 -0300 Subject: [PATCH 2/6] feat: add log for initial genesis config --- bin/ethlambda/src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index afcffd1..fb0a3b2 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -86,6 +86,12 @@ async fn main() { let genesis_config: GenesisConfig = serde_yaml_ng::from_str(&config_yaml).expect("Failed to parse config.yaml"); + info!( + genesis_time = genesis_config.genesis_time, + validator_count = genesis_config.genesis_validators.len(), + "Loaded genesis configuration" + ); + populate_name_registry(&validator_config); let bootnodes = read_bootnodes(&bootnodes_path); From 180a0907746a373d876cb2049a79c505c4fbb778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:49:56 -0300 Subject: [PATCH 3/6] refactor: move validator transform to method --- bin/ethlambda/src/main.rs | 12 ++---------- crates/common/types/src/genesis.rs | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index fb0a3b2..55fbacd 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -13,7 +13,7 @@ use ethlambda_types::primitives::H256; use ethlambda_types::{ genesis::GenesisConfig, signature::ValidatorSecretKey, - state::{State, Validator, ValidatorPubkeyBytes}, + state::{State, ValidatorPubkeyBytes}, }; use serde::Deserialize; use tracing::{error, info}; @@ -95,15 +95,7 @@ async fn main() { populate_name_registry(&validator_config); let bootnodes = read_bootnodes(&bootnodes_path); - let validators: Vec = genesis_config - .genesis_validators - .into_iter() - .enumerate() - .map(|(i, pubkey)| Validator { - pubkey, - index: i as u64, - }) - .collect(); + let validators = genesis_config.validators(); let validator_keys = read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id); diff --git a/crates/common/types/src/genesis.rs b/crates/common/types/src/genesis.rs index 6a303df..1c7db0a 100644 --- a/crates/common/types/src/genesis.rs +++ b/crates/common/types/src/genesis.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use crate::state::ValidatorPubkeyBytes; +use crate::state::{Validator, ValidatorPubkeyBytes}; #[derive(Debug, Clone, Deserialize)] pub struct GenesisConfig { @@ -11,6 +11,19 @@ pub struct GenesisConfig { pub genesis_validators: Vec, } +impl GenesisConfig { + pub fn validators(&self) -> Vec { + self.genesis_validators + .iter() + .enumerate() + .map(|(i, pubkey)| Validator { + pubkey: *pubkey, + index: i as u64, + }) + .collect() + } +} + fn deser_hex_pubkeys<'de, D>(d: D) -> Result, D::Error> where D: serde::Deserializer<'de>, From 6f8cff3cd13a1cab716cdc209613f76ac1f8cba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:56:26 -0300 Subject: [PATCH 4/6] test: use correct hash --- crates/common/types/src/genesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/common/types/src/genesis.rs b/crates/common/types/src/genesis.rs index 1c7db0a..ac6e3b7 100644 --- a/crates/common/types/src/genesis.rs +++ b/crates/common/types/src/genesis.rs @@ -132,7 +132,7 @@ GENESIS_VALIDATORS: // Pin the state root so changes are caught immediately. let expected = - hex::decode("362db4ffe968f1d100934797f6d3c7985b4aee9d96b328ad2e47243b8292e434") + hex::decode("118054414cf28edb0835fd566785c46c0de82ac717ee83a809786bc0c5bb7ef2") .unwrap(); assert_eq!(root.as_slice(), &expected[..], "state root mismatch"); From 8d24722f4231fccd9c608cf3b607e050d7047d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:01:46 -0300 Subject: [PATCH 5/6] chore: apply review comments --- crates/common/types/src/genesis.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/common/types/src/genesis.rs b/crates/common/types/src/genesis.rs index ac6e3b7..5c0039d 100644 --- a/crates/common/types/src/genesis.rs +++ b/crates/common/types/src/genesis.rs @@ -33,13 +33,18 @@ where let hex_strings: Vec = Vec::deserialize(d)?; hex_strings .into_iter() - .map(|s| { + .enumerate() + .map(|(idx, s)| { let s = s.strip_prefix("0x").unwrap_or(&s); - let bytes = hex::decode(s) - .map_err(|_| D::Error::custom("GENESIS_VALIDATORS value is not valid hex"))?; - bytes - .try_into() - .map_err(|_| D::Error::custom("GENESIS_VALIDATORS pubkey length != 52")) + let bytes = hex::decode(s).map_err(|_| { + D::Error::custom(format!("GENESIS_VALIDATORS[{idx}] is not valid hex: {s}")) + })?; + bytes.try_into().map_err(|v: Vec| { + D::Error::custom(format!( + "GENESIS_VALIDATORS[{idx}] has length {} (expected 52)", + v.len() + )) + }) }) .collect() } @@ -56,7 +61,7 @@ mod tests { const PUBKEY_B: &str = "b7b0f72e24801b02bda64073cb4de6699a416b37dfead227d7ca3922647c940fa03e4c012e8a0e656b731934aeac124a5337e333"; const PUBKEY_C: &str = "8d9cbc508b20ef43e165f8559c1bdd18aaeda805ef565a4f9ffd6e4fbed01c05e143e305017847445859650d6dd06e6efb3f8410"; - const TEST_CONFIG_JSON: &str = r#"# Genesis Settings + const TEST_CONFIG_YAML: &str = r#"# Genesis Settings GENESIS_TIME: 1770407233 # Key Settings @@ -74,7 +79,7 @@ GENESIS_VALIDATORS: #[test] fn deserialize_genesis_config() { - let config: GenesisConfig = serde_yaml_ng::from_str(TEST_CONFIG_JSON) + let config: GenesisConfig = serde_yaml_ng::from_str(TEST_CONFIG_YAML) .expect("Failed to deserialize genesis config"); assert_eq!(config.genesis_time, 1770407233); @@ -116,7 +121,7 @@ GENESIS_VALIDATORS: #[test] fn state_from_genesis_root() { - let config: GenesisConfig = serde_yaml_ng::from_str(TEST_CONFIG_JSON).unwrap(); + let config: GenesisConfig = serde_yaml_ng::from_str(TEST_CONFIG_YAML).unwrap(); let validators: Vec = config .genesis_validators From 236732db20a4935e781e12125f57ee0835ed4819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:59:16 -0300 Subject: [PATCH 6/6] chore: remove dead_code allow and rename unused field --- bin/ethlambda/src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 55fbacd..6bb9bad 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -168,10 +168,9 @@ fn read_bootnodes(bootnodes_path: impl AsRef) -> Vec { #[derive(Debug, Deserialize)] struct AnnotatedValidator { index: u64, - #[allow(dead_code)] // Present in YAML, needed for deserialization but not read in code #[serde(rename = "pubkey_hex")] #[serde(deserialize_with = "deser_pubkey_hex")] - pubkey: ValidatorPubkeyBytes, + _pubkey: ValidatorPubkeyBytes, privkey_file: PathBuf, }