Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 1 addition & 2 deletions bin/ethlambda/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 15 additions & 36 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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,
}

Expand All @@ -183,33 +189,6 @@ where
Ok(pubkey)
}

fn read_validators(validators_path: impl AsRef<Path>) -> Vec<Validator> {
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<String, Vec<AnnotatedValidator>> =
serde_yaml_ng::from_str(&validators_yaml).expect("Failed to parse validators file");

let mut validators: Vec<Validator> = 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<Path>,
validator_keys_dir: impl AsRef<Path>,
Expand Down
3 changes: 3 additions & 0 deletions crates/common/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
171 changes: 155 additions & 16 deletions crates/common/types/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -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<H256>,
pub justified_slots: Vec<bool>,
// TODO: uncomment
pub justifications_roots: Vec<String>,
// 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<ValidatorPubkeyBytes>,
}

impl GenesisConfig {
pub fn validators(&self) -> Vec<Validator> {
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<Vec<ValidatorPubkeyBytes>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;

let hex_strings: Vec<String> = 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<u8>| {
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<Validator> = 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"
);
}
}
13 changes: 7 additions & 6 deletions crates/common/types/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use ssz_types::typenum::{U4096, U262144};

use crate::{
block::{BlockBody, BlockHeader},
genesis::Genesis,
primitives::{
H256,
ssz::{Decode, DecodeError, Encode, TreeHash},
Expand Down Expand Up @@ -87,7 +86,7 @@ impl Validator {
pub type ValidatorPubkeyBytes = [u8; 52];

impl State {
pub fn from_genesis(genesis: &Genesis, validators: Vec<Validator>) -> Self {
pub fn from_genesis(genesis_time: u64, validators: Vec<Validator>) -> Self {
let genesis_header = BlockHeader {
slot: 0,
proposer_index: 0,
Expand All @@ -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,
Expand All @@ -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,
Expand Down