Skip to content
Draft
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
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ docker-build: ## 🐳 Build the Docker image
-t ghcr.io/lambdaclass/ethlambda:$(DOCKER_TAG) .
@echo

LEAN_SPEC_COMMIT_HASH:=ad9a3226f55e1ba143e0991010ff1f6c2de62941
LEAN_SPEC_COMMIT_HASH:=9c30436bf4c073d1a994f37a3241e83ef5a3ce6f

leanSpec:
git clone https://github.com/leanEthereum/leanSpec.git --single-branch
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 @@ -34,7 +34,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 @@ -144,7 +144,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 @@ -230,22 +232,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
61 changes: 39 additions & 22 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 @@ -237,13 +238,16 @@ fn read_bootnodes(bootnodes_path: impl AsRef<Path>) -> Vec<Bootnode> {
#[derive(Debug, Deserialize)]
struct AnnotatedValidator {
index: u64,
#[serde(rename = "pubkey_hex")]
#[serde(rename = "attestation_pubkey_hex")]
#[serde(deserialize_with = "deser_pubkey_hex")]
_pubkey: ValidatorPubkeyBytes,
privkey_file: PathBuf,
_attestation_pubkey: ValidatorPubkeyBytes,
#[serde(rename = "proposal_pubkey_hex")]
#[serde(deserialize_with = "deser_pubkey_hex")]
_proposal_pubkey: ValidatorPubkeyBytes,
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,12 +266,11 @@ fn read_validator_keys(
validators_path: impl AsRef<Path>,
validator_keys_dir: impl AsRef<Path>,
node_id: &str,
) -> HashMap<u64, ValidatorSecretKey> {
) -> HashMap<u64, ValidatorKeyPair> {
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 validator_infos: BTreeMap<String, Vec<AnnotatedValidator>> =
serde_yaml_ng::from_str(&validators_yaml).expect("Failed to parse validators file");

Expand All @@ -280,32 +283,46 @@ fn read_validator_keys(
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);

info!(node_id=%node_id, index=validator_index, attestation_key=?att_key_path, proposal_key=?prop_key_path, "Loading validator key pair");

// 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");
let load_key = |path: &Path, purpose: &str| -> ValidatorSecretKey {
let bytes = std::fs::read(path).unwrap_or_else(|err| {
error!(node_id=%node_id, index=validator_index, file=?path, %err, "Failed to read {purpose} key file");
std::process::exit(1);
});
ValidatorSecretKey::from_bytes(&bytes).unwrap_or_else(|err| {
error!(node_id=%node_id, index=validator_index, file=?path, ?err, "Failed to parse {purpose} key");
std::process::exit(1);
})
};

// 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 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,
count = validator_keys.len(),
"Loaded validator secret keys"
"Loaded validator key pairs"
);

validator_keys
Expand Down
129 changes: 71 additions & 58 deletions crates/blockchain/src/key_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,103 +19,103 @@ pub enum KeyManagerError {
SignatureConversionError(String),
}

/// Manages validator secret keys for signing attestations.
/// A validator's dual XMSS key pair for attestation and block proposal signing.
///
/// The KeyManager stores a mapping of validator IDs to their secret keys
/// and provides methods to sign attestations on behalf of validators.
/// Each key is independent and advances its OTS preparation separately,
/// allowing the validator to sign both an attestation and a block proposal
/// within the same slot.
pub struct ValidatorKeyPair {
pub attestation_key: ValidatorSecretKey,
pub proposal_key: ValidatorSecretKey,
}

/// Manages validator secret keys for signing attestations and block proposals.
///
/// Each validator has two independent XMSS keys: one for attestation signing
/// and one for block proposal signing.
pub struct KeyManager {
keys: HashMap<u64, ValidatorSecretKey>,
keys: HashMap<u64, ValidatorKeyPair>,
}

impl KeyManager {
/// Creates a new KeyManager with the given mapping of validator IDs to secret keys.
///
/// # Arguments
///
/// * `keys` - A HashMap mapping validator IDs (u64) to their secret keys
///
/// # Example
///
/// ```ignore
/// let mut keys = HashMap::new();
/// keys.insert(0, ValidatorSecretKey::from_bytes(&key_bytes)?);
/// let key_manager = KeyManager::new(keys);
/// ```
pub fn new(keys: HashMap<u64, ValidatorSecretKey>) -> Self {
pub fn new(keys: HashMap<u64, ValidatorKeyPair>) -> Self {
Self { keys }
}

/// Returns a list of all registered validator IDs.
///
/// The returned vector contains all validator IDs that have keys registered
/// in this KeyManager instance.
pub fn validator_ids(&self) -> Vec<u64> {
self.keys.keys().copied().collect()
}

/// Signs an attestation for the specified validator.
///
/// This method computes the message hash from the attestation data and signs it
/// using the validator's secret key.
///
/// # Arguments
///
/// * `validator_id` - The ID of the validator whose key should be used for signing
/// * `attestation_data` - The attestation data to sign
///
/// # Returns
///
/// Returns an `XmssSignature` (3112 bytes) on success, or a `KeyManagerError` if:
/// - The validator ID is not found in the KeyManager
/// - The signing operation fails
/// Signs an attestation using the validator's attestation key.
pub fn sign_attestation(
&mut self,
validator_id: u64,
attestation_data: &AttestationData,
) -> Result<XmssSignature, KeyManagerError> {
let message_hash = attestation_data.tree_hash_root();
let slot = attestation_data.slot as u32;
self.sign_message(validator_id, slot, &message_hash)
self.sign_with_attestation_key(validator_id, slot, &message_hash)
}

/// Signs a block root using the validator's proposal key.
pub fn sign_block_root(
&mut self,
validator_id: u64,
slot: u32,
block_root: &H256,
) -> Result<XmssSignature, KeyManagerError> {
self.sign_with_proposal_key(validator_id, slot, block_root)
}

/// Signs a message hash for the specified validator.
///
/// # Arguments
///
/// * `validator_id` - The ID of the validator whose key should be used for signing
/// * `slot` - The slot number used in the XMSS signature scheme
/// * `message` - The message hash to sign
///
/// # Returns
///
/// Returns an `XmssSignature` (3112 bytes) on success, or a `KeyManagerError` if:
/// - The validator ID is not found in the KeyManager
/// - The signing operation fails
fn sign_message(
fn sign_with_attestation_key(
&mut self,
validator_id: u64,
slot: u32,
message: &H256,
) -> Result<XmssSignature, KeyManagerError> {
let secret_key = self
let key_pair = self
.keys
.get_mut(&validator_id)
.ok_or(KeyManagerError::ValidatorKeyNotFound(validator_id))?;

let signature: ValidatorSignature = {
let _timing = metrics::time_pq_sig_attestation_signing();
secret_key
key_pair
.attestation_key
.sign(slot, message)
.map_err(|e| KeyManagerError::SigningError(e.to_string()))
}?;
metrics::inc_pq_sig_attestation_signatures();

// Convert ValidatorSignature to XmssSignature (FixedVector<u8, SignatureSize>)
let sig_bytes = signature.to_bytes();
let xmss_sig = XmssSignature::try_from(sig_bytes)
.map_err(|e| KeyManagerError::SignatureConversionError(e.to_string()))?;
XmssSignature::try_from(sig_bytes)
.map_err(|e| KeyManagerError::SignatureConversionError(e.to_string()))
}

fn sign_with_proposal_key(
&mut self,
validator_id: u64,
slot: u32,
message: &H256,
) -> Result<XmssSignature, KeyManagerError> {
let key_pair = self
.keys
.get_mut(&validator_id)
.ok_or(KeyManagerError::ValidatorKeyNotFound(validator_id))?;

Ok(xmss_sig)
let signature: ValidatorSignature = {
let _timing = metrics::time_pq_sig_proposal_signing();
key_pair
.proposal_key
.sign(slot, message)
.map_err(|e| KeyManagerError::SigningError(e.to_string()))
}?;
metrics::inc_pq_sig_proposal_signatures();

let sig_bytes = signature.to_bytes();
XmssSignature::try_from(sig_bytes)
.map_err(|e| KeyManagerError::SignatureConversionError(e.to_string()))
}
}

Expand All @@ -136,7 +136,20 @@ mod tests {
let mut key_manager = KeyManager::new(keys);
let message = H256::default();

let result = key_manager.sign_message(123, 0, &message);
let result = key_manager.sign_with_attestation_key(123, 0, &message);
assert!(matches!(
result,
Err(KeyManagerError::ValidatorKeyNotFound(123))
));
}

#[test]
fn test_sign_block_root_validator_not_found() {
let keys = HashMap::new();
let mut key_manager = KeyManager::new(keys);
let message = H256::default();

let result = key_manager.sign_block_root(123, 0, &message);
assert!(matches!(
result,
Err(KeyManagerError::ValidatorKeyNotFound(123))
Expand Down
Loading
Loading