Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1de4efd
Devnet4 types: dual-key Validator, SignedBlock, genesis config format
pablodeymo Mar 16, 2026
8228ecf
Devnet4 key manager and block proposal: dual keys, sign block root
pablodeymo Mar 16, 2026
909be88
Devnet4 store and verification: dual-key verification, remove propose…
pablodeymo Mar 16, 2026
1fceccf
Devnet4 network layer and tests: type cascade, test harness updates
pablodeymo Mar 16, 2026
45e5654
Update ignored SSZ test message to reflect devnet4 layout change
pablodeymo Mar 20, 2026
04ead4e
Remove unused local_validator_ids parameter from on_block
pablodeymo Mar 31, 2026
e1c0740
Add missing AggregatedSignatureProof import in storage crate
pablodeymo Mar 31, 2026
9f8a586
Fix formatting in storage import
pablodeymo Mar 31, 2026
deda8a1
Add proposal signing metrics, pin genesis root hashes, fix stale comment
pablodeymo Apr 1, 2026
2d1bf53
Update stale references from devnet4 type rename across docs and tests
pablodeymo Apr 1, 2026
c70a6f1
Bump leanSpec to 488518c to fix dual-key test fixture generation
pablodeymo Apr 1, 2026
c8ea8d9
Bump leanSpec to HEAD (9c30436) and adapt test harness to new fixture…
pablodeymo Apr 1, 2026
d966d0d
Merge devnet4 into devnet4-phase4-network
pablodeymo Apr 7, 2026
c7a8f91
erge branch 'devnet4' into devnet4-phase4-network
pablodeymo Apr 7, 2026
5c002aa
Merge devnet4 into devnet4-phase4-network and fix conflicts
pablodeymo Apr 8, 2026
3067dcf
Fix test code to use phase4 types (SignedBlock, dual-key Validator)
pablodeymo Apr 8, 2026
93785e5
merge devnet4
pablodeymo Apr 9, 2026
826c075
Merge devnet4 into devnet4-phase4-network, add attestation step suppo…
pablodeymo Apr 9, 2026
e720507
Merge branch 'main' into devnet4-phase4-network
pablodeymo Apr 10, 2026
02f792b
Fix test harness to pair block attestations with signature proofs
pablodeymo Apr 10, 2026
8aafdbd
Address PR review: remove unused pubkey fields, return errors instead…
pablodeymo Apr 10, 2026
2e8b627
Merge branch 'devnet4' into devnet4-phase4-network
MegaRedHand Apr 13, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ ethlambda_p2p: Published block to gossipsub slot=X proposer=Y
```
ethlambda_blockchain: Published attestation slot=X validator_id=Y
ethlambda_p2p::gossipsub::handler: Received new attestation from gossipsub, sending for processing slot=X validator=Y
ethlambda_blockchain: Skipping attestation for proposer slot=X (expected: proposers don't attest to their own slot)
```

### Block Processing
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ crates/
### Tick-Based Validator Duties (4-second slots, 5 intervals per slot)
```
Interval 0: Block proposal → accept attestations if proposal exists
Interval 1: Vote propagation (no action)
Interval 1: Attestation production (all validators, including proposer)
Interval 2: Aggregation (aggregators create proofs from gossip signatures)
Interval 3: Safe target update (fork choice)
Interval 4: Accept accumulated attestations
Expand Down Expand Up @@ -106,7 +106,7 @@ let byte: u8 = code.into();
### Ownership for Large Structures
```rust
// Prefer taking ownership to avoid cloning large data (signatures ~3KB)
pub fn insert_signed_block(&mut self, root: H256, signed_block: SignedBlockWithAttestation) { ... }
pub fn insert_signed_block(&mut self, root: H256, signed_block: SignedBlock) { ... }

// Add .clone() at call site if needed - makes cost explicit
store.insert_signed_block(block_root, signed_block.clone());
Expand Down Expand Up @@ -310,8 +310,8 @@ Both servers are spawned as independent `tokio::spawn` tasks from `main.rs`. Bin
```yaml
GENESIS_TIME: 1770407233
GENESIS_VALIDATORS:
- "cd323f232b34ab26d6db7402c886e74ca81cfd3a..." # 52-byte XMSS pubkeys (hex)
- "b7b0f72e24801b02bda64073cb4de6699a416b37..."
- attestation_pubkey: "cd323f232b34ab26d6db7402c886e74ca81cfd3a..." # 52-byte XMSS pubkeys (hex)
proposal_pubkey: "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
Expand Down Expand Up @@ -363,7 +363,7 @@ cargo test -p ethlambda-blockchain --test forkchoice_spectests -- --test-threads
|-------|-------------|---------|
| `BlockHeaders` | H256 → BlockHeader | Block headers by root |
| `BlockBodies` | H256 → BlockBody | Block bodies (empty for genesis) |
| `BlockSignatures` | H256 → BlockSignaturesWithAttestation | Signatures (absent for genesis) |
| `BlockSignatures` | H256 → BlockSignatures | Signatures (absent for genesis) |
| `States` | H256 → State | Beacon states by root |
| `LatestKnownAttestations` | u64 → AttestationData | Fork-choice-active attestations |
| `LatestNewAttestations` | u64 → AttestationData | Pending (pre-promotion) attestations |
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ docker-build: ## 🐳 Build the Docker image
-t ghcr.io/lambdaclass/ethlambda:$(DOCKER_TAG) .
@echo

LEAN_SPEC_COMMIT_HASH:=d39d10195414921e979e2fdd43723d89cee13c8b
LEAN_SPEC_COMMIT_HASH:=9c30436bf4c073d1a994f37a3241e83ef5a3ce6f

leanSpec:
git clone https://github.com/leanEthereum/leanSpec.git --single-branch
cd leanSpec && git checkout $(LEAN_SPEC_COMMIT_HASH)
cd leanSpec && git apply ../patches/fix-lexicographic-tiebreaker-test.patch

leanSpec/fixtures: leanSpec
cd leanSpec && uv run fill --fork devnet --scheme=prod -o fixtures
Expand Down
15 changes: 10 additions & 5 deletions bin/ethlambda/src/checkpoint_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub enum CheckpointSyncError {
expected: u64,
got: u64,
},
#[error("validator {index} pubkey mismatch")]
#[error("validator {index} pubkey mismatch (attestation or proposal key)")]
ValidatorPubkeyMismatch { index: usize },
#[error("finalized slot cannot exceed state slot")]
FinalizedExceedsStateSlot,
Expand Down Expand Up @@ -145,7 +145,9 @@ fn verify_checkpoint_state(
.zip(expected_validators.iter())
.enumerate()
{
if state_val.pubkey != expected_val.pubkey {
if state_val.attestation_pubkey != expected_val.attestation_pubkey
|| state_val.proposal_pubkey != expected_val.proposal_pubkey
{
return Err(CheckpointSyncError::ValidatorPubkeyMismatch { index: i });
}
}
Expand Down Expand Up @@ -229,22 +231,25 @@ mod tests {

fn create_test_validator() -> Validator {
Validator {
pubkey: [1u8; 52],
attestation_pubkey: [1u8; 52],
proposal_pubkey: [11u8; 52],
index: 0,
}
}

fn create_different_validator() -> Validator {
Validator {
pubkey: [2u8; 52],
attestation_pubkey: [2u8; 52],
proposal_pubkey: [22u8; 52],
index: 0,
}
}

fn create_validators_with_indices(count: usize) -> Vec<Validator> {
(0..count)
.map(|i| Validator {
pubkey: [i as u8 + 1; 52],
attestation_pubkey: [i as u8 + 1; 52],
proposal_pubkey: [i as u8 + 101; 52],
index: i as u64,
})
.collect()
Expand Down
73 changes: 43 additions & 30 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::{
};

use clap::Parser;
use ethlambda_blockchain::key_manager::ValidatorKeyPair;
use ethlambda_network_api::{InitBlockChain, InitP2P, ToBlockChainToP2PRef, ToP2PToBlockChainRef};
use ethlambda_p2p::{Bootnode, P2P, SwarmConfig, build_swarm, parse_enrs};
use ethlambda_types::primitives::H256;
Expand Down Expand Up @@ -135,7 +136,8 @@ async fn main() -> eyre::Result<()> {
let bootnodes = read_bootnodes(&bootnodes_path);

let validator_keys =
read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id);
read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id)
.expect("Failed to load validator keys");

let data_dir =
std::path::absolute(&options.data_dir).unwrap_or_else(|_| options.data_dir.clone());
Expand Down Expand Up @@ -237,13 +239,10 @@ fn read_bootnodes(bootnodes_path: impl AsRef<Path>) -> Vec<Bootnode> {
#[derive(Debug, Deserialize)]
struct AnnotatedValidator {
index: u64,
#[serde(rename = "pubkey_hex")]
#[serde(deserialize_with = "deser_pubkey_hex")]
_pubkey: ValidatorPubkeyBytes,
privkey_file: PathBuf,
attestation_privkey_file: PathBuf,
proposal_privkey_file: PathBuf,
}

// Taken from ethrex-common
pub fn deser_pubkey_hex<'de, D>(d: D) -> Result<ValidatorPubkeyBytes, D::Error>
where
D: serde::Deserializer<'de>,
Expand All @@ -262,53 +261,67 @@ fn read_validator_keys(
validators_path: impl AsRef<Path>,
validator_keys_dir: impl AsRef<Path>,
node_id: &str,
) -> HashMap<u64, ValidatorSecretKey> {
) -> Result<HashMap<u64, ValidatorKeyPair>, String> {
let validators_path = validators_path.as_ref();
let validator_keys_dir = validator_keys_dir.as_ref();
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 validators_yaml = std::fs::read_to_string(validators_path)
.map_err(|err| format!("Failed to read validators file: {err}"))?;
let validator_infos: BTreeMap<String, Vec<AnnotatedValidator>> =
serde_yaml_ng::from_str(&validators_yaml).expect("Failed to parse validators file");
serde_yaml_ng::from_str(&validators_yaml)
.map_err(|err| format!("Failed to parse validators file: {err}"))?;

let validator_vec = validator_infos
.get(node_id)
.unwrap_or_else(|| panic!("Node ID '{}' not found in validators config", node_id));
.ok_or_else(|| format!("Node ID '{node_id}' not found in validators config"))?;

let mut validator_keys = HashMap::new();

for validator in validator_vec {
let validator_index = validator.index;

// Resolve the secret key file path relative to the validators config directory
let secret_key_path = if validator.privkey_file.is_absolute() {
validator.privkey_file.clone()
} else {
validator_keys_dir.join(&validator.privkey_file)
let resolve_path = |file: &PathBuf| -> PathBuf {
if file.is_absolute() {
file.clone()
} else {
validator_keys_dir.join(file)
}
};

info!(node_id=%node_id, index=validator_index, secret_key_file=?secret_key_path, "Loading validator secret key");
let att_key_path = resolve_path(&validator.attestation_privkey_file);
let prop_key_path = resolve_path(&validator.proposal_privkey_file);

// Read the hex-encoded secret key file
let secret_key_bytes =
std::fs::read(&secret_key_path).expect("Failed to read validator secret key file");
info!(%node_id, index=validator_index, attestation_key=?att_key_path, proposal_key=?prop_key_path, "Loading validator key pair");

// Parse the secret key
let secret_key = ValidatorSecretKey::from_bytes(&secret_key_bytes).unwrap_or_else(|err| {
error!(node_id=%node_id, index=validator_index, secret_key_file=?secret_key_path, ?err, "Failed to parse validator secret key");
std::process::exit(1);
});
let load_key = |path: &Path, purpose: &str| -> Result<ValidatorSecretKey, String> {
let bytes = std::fs::read(path).map_err(|err| {
format!(
"Failed to read {purpose} key file {}: {err}",
path.display()
)
})?;
ValidatorSecretKey::from_bytes(&bytes)
.map_err(|err| format!("Failed to parse {purpose} key {}: {err:?}", path.display()))
};

let attestation_key = load_key(&att_key_path, "attestation")?;
let proposal_key = load_key(&prop_key_path, "proposal")?;

validator_keys.insert(validator_index, secret_key);
validator_keys.insert(
validator_index,
ValidatorKeyPair {
attestation_key,
proposal_key,
},
);
}

info!(
node_id = %node_id,
%node_id,
count = validator_keys.len(),
"Loaded validator secret keys"
"Loaded validator key pairs"
);

validator_keys
Ok(validator_keys)
}

fn read_hex_file_bytes(path: impl AsRef<Path>) -> Vec<u8> {
Expand Down
Loading
Loading