Skip to content
Merged
1 change: 0 additions & 1 deletion Cargo.lock

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

42 changes: 16 additions & 26 deletions bin/ethlambda/src/benchmark/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,14 @@ use std::collections::HashMap;
use std::path::Path;
use std::time::Instant;

use ethlambda_blockchain::key_manager::{KeyManager, ValidatorKeyPair};
use ethlambda_blockchain::key_manager::{KeyManager, KeyRole, ValidatorKeyPair};
use ethlambda_crypto::signature::ValidatorSecretKey;
use ethlambda_types::state::ValidatorPubkeyBytes;
use eyre::WrapErr as _;
use rayon::prelude::*;

const PUBKEY_LEN: usize = size_of::<ValidatorPubkeyBytes>();

#[derive(Debug, Clone, Copy)]
#[repr(u64)]
enum Role {
Attestation = 0,
Proposal = 1,
}

impl Role {
fn tag(self) -> &'static str {
match self {
Role::Attestation => "attestation",
Role::Proposal => "proposal",
}
}
}

struct Key {
pubkey: ValidatorPubkeyBytes,
secret: ValidatorSecretKey,
Expand Down Expand Up @@ -75,8 +59,8 @@ impl KeySet {
// Every key is independent and deterministic in (seed, index, role), so
// they are produced in parallel; `collect` keeps the job order.
let start = Instant::now();
let jobs: Vec<(u64, Role)> = (0..num_validators)
.flat_map(|index| [(index, Role::Attestation), (index, Role::Proposal)])
let jobs: Vec<(u64, KeyRole)> = (0..num_validators)
.flat_map(|index| [(index, KeyRole::Attestation), (index, KeyRole::Proposal)])
.collect();
let keys: Vec<Key> = jobs
.into_par_iter()
Expand Down Expand Up @@ -118,15 +102,15 @@ impl KeySet {
fn load_or_generate(
seed: u64,
index: u64,
role: Role,
role: KeyRole,
num_active_epochs: usize,
cache: Option<&Path>,
) -> eyre::Result<Key> {
let file = cache.map(|dir| {
dir.join(format!(
"xmss-{}-seed{seed}-v{index}-{}-w{num_active_epochs}.bin",
env!("ETHLAMBDA_LEANVM_REV"),
role.tag()
role.name()
))
});
if let Some(file) = &file
Expand All @@ -135,7 +119,13 @@ fn load_or_generate(
return load_cached(file);
}

let key_seed = seed ^ (index << 1 | role as u64).rotate_left(32);
// Spelled out rather than `role as u64`, so reordering `KeyRole` cannot
// change the keys a seed derives.
let role_bit = match role {
KeyRole::Attestation => 0,
KeyRole::Proposal => 1,
};
let key_seed = seed ^ (index << 1 | role_bit).rotate_left(32);
// leanVM seeds a key with 32 bytes; the run's `u64` fills the low end and
// the rest stays zero, which keeps the derivation reproducible without
// pretending to more entropy than the seed carries.
Expand Down Expand Up @@ -198,13 +188,13 @@ mod tests {

#[test]
fn keys_are_deterministic_per_seed_and_distinct_per_role() {
let a = load_or_generate(7, 3, Role::Attestation, 2, None).unwrap();
let b = load_or_generate(7, 3, Role::Attestation, 2, None).unwrap();
let a = load_or_generate(7, 3, KeyRole::Attestation, 2, None).unwrap();
let b = load_or_generate(7, 3, KeyRole::Attestation, 2, None).unwrap();
assert_eq!(a.pubkey, b.pubkey);
assert_eq!(a.secret.to_bytes().unwrap(), b.secret.to_bytes().unwrap());
let proposal = load_or_generate(7, 3, Role::Proposal, 2, None).unwrap();
let proposal = load_or_generate(7, 3, KeyRole::Proposal, 2, None).unwrap();
assert_ne!(a.pubkey, proposal.pubkey);
let other_seed = load_or_generate(8, 3, Role::Attestation, 2, None).unwrap();
let other_seed = load_or_generate(8, 3, KeyRole::Attestation, 2, None).unwrap();
assert_ne!(a.pubkey, other_seed.pubkey);
}

Expand Down
18 changes: 6 additions & 12 deletions bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use cli::NodeOptions;
use command::Command;

use ethlambda_blockchain::block_builder::ProposerConfig;
use ethlambda_blockchain::key_manager::ValidatorKeyPair;
use ethlambda_blockchain::key_manager::{KeyRole, ValidatorKeyPair};
use ethlambda_crypto::signature::ValidatorSecretKey;
use ethlambda_network_api::{InitBlockChain, InitP2P, ToBlockChainToP2PRef, ToP2PToBlockChainRef};
use ethlambda_p2p::{
Expand Down Expand Up @@ -619,27 +619,21 @@ where
Ok(pubkey)
}

#[derive(Debug)]
enum ValidatorKeyRole {
Attestation,
Proposal,
}

/// Classify a privkey file as attestation or proposal based on the filename.
///
/// Matches zeam's (`pkgs/cli/src/node.zig:540`) and lantern's
/// (`client_keys.c:606`) routing, which lets all three clients share the
/// `lean-quickstart` generator output unchanged.
fn classify_role(file: &Path) -> Result<ValidatorKeyRole, String> {
fn classify_role(file: &Path) -> Result<KeyRole, String> {
let name = file
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| format!("non-utf8 filename '{}'", file.display()))?;
let is_attester = name.contains("attester");
let is_proposer = name.contains("proposer");
match (is_attester, is_proposer) {
(true, false) => Ok(ValidatorKeyRole::Attestation),
(false, true) => Ok(ValidatorKeyRole::Proposal),
(true, false) => Ok(KeyRole::Attestation),
(false, true) => Ok(KeyRole::Proposal),
(false, false) => Err(format!(
"filename '{name}' must contain 'attester' or 'proposer'"
)),
Expand Down Expand Up @@ -695,8 +689,8 @@ fn read_validator_keys(
let path = resolve_path(&entry.privkey_file);
let slots = grouped.entry(entry.index).or_default();
let target = match role {
ValidatorKeyRole::Attestation => &mut slots.attestation,
ValidatorKeyRole::Proposal => &mut slots.proposal,
KeyRole::Attestation => &mut slots.attestation,
KeyRole::Proposal => &mut slots.proposal,
};
if target.is_some() {
eyre::bail!("validator {}: duplicate {role:?} entry", entry.index);
Expand Down
1 change: 0 additions & 1 deletion crates/blockchain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ spawned-concurrency.workspace = true
tokio.workspace = true
tokio-util = { version = "0.7", default-features = false }

rayon.workspace = true
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true
Expand Down
Loading
Loading