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..6bb9bad 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -11,9 +11,9 @@ 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}, + state::{State, ValidatorPubkeyBytes}, }; use serde::Deserialize; use tracing::{error, info}; @@ -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,24 @@ 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"); + + 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); - let validators = read_validators(&validators_path); + let validators = genesis_config.validators(); 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); @@ -164,7 +170,7 @@ struct AnnotatedValidator { index: u64, #[serde(rename = "pubkey_hex")] #[serde(deserialize_with = "deser_pubkey_hex")] - pubkey: ValidatorPubkeyBytes, + _pubkey: ValidatorPubkeyBytes, privkey_file: PathBuf, } @@ -183,33 +189,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..5c0039d 100644 --- a/crates/common/types/src/genesis.rs +++ b/crates/common/types/src/genesis.rs @@ -1,17 +1,156 @@ -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::{Validator, 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, +} + +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>, +{ + use serde::de::Error; + + let hex_strings: Vec = Vec::deserialize(d)?; + hex_strings + .into_iter() + .enumerate() + .map(|(idx, s)| { + let s = s.strip_prefix("0x").unwrap_or(&s); + 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() +} + +#[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_YAML: &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_YAML) + .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_YAML).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("118054414cf28edb0835fd566785c46c0de82ac717ee83a809786bc0c5bb7ef2") + .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,