Skip to content

Commit bc674ca

Browse files
authored
Merge branch 'devnet5-bump-leanmultisig' into devnet5-zk-alloc
2 parents cc61b83 + 9d377b1 commit bc674ca

10 files changed

Lines changed: 507 additions & 102 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ actual_slot = finalized_slot + 1 + relative_index
254254
- Post-quantum signature scheme
255255
- 52-byte public keys, 3112-byte signatures
256256
- Epoch-based to prevent reuse
257-
- Aggregation via leanVM for efficiency
257+
- Aggregation via leanVM (previously leanMultisig) for efficiency
258258

259259
**Signature Aggregation (Two-Phase):**
260260
1. **Gossip signatures**: Fresh XMSS from network → aggregate via leanVM

bin/ethlambda/src/main.rs

Lines changed: 117 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ use std::{
2222
net::{IpAddr, SocketAddr},
2323
path::{Path, PathBuf},
2424
sync::Arc,
25+
time::SystemTime,
2526
};
2627
use tokio_util::sync::CancellationToken;
2728

2829
use clap::Parser;
30+
use ethlambda_blockchain::MILLISECONDS_PER_SLOT;
2931
use ethlambda_blockchain::key_manager::ValidatorKeyPair;
3032
use ethlambda_network_api::{InitBlockChain, InitP2P, ToBlockChainToP2PRef, ToP2PToBlockChainRef};
3133
use ethlambda_p2p::{Bootnode, P2P, PeerId, SwarmConfig, build_swarm, parse_enrs};
@@ -36,13 +38,16 @@ use ethlambda_types::{
3638
signature::ValidatorSecretKey,
3739
state::{State, ValidatorPubkeyBytes},
3840
};
41+
use eyre::WrapErr;
3942
use serde::Deserialize;
4043
use tracing::{error, info, warn};
4144
use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt};
4245

4346
use ethlambda_blockchain::BlockChain;
4447
use ethlambda_rpc::RpcConfig;
45-
use ethlambda_storage::{StorageBackend, Store, backend::RocksDBBackend};
48+
use ethlambda_storage::{
49+
MAX_RESUMABLE_DB_STATE_AGE, StorageBackend, Store, backend::RocksDBBackend,
50+
};
4651

4752
const ASCII_ART: &str = r#"
4853
_ _ _ _ _
@@ -124,7 +129,8 @@ async fn main() -> eyre::Result<()> {
124129
.with_default_directive(tracing::Level::INFO.into())
125130
.from_env_lossy();
126131
let subscriber = Registry::default().with(tracing_subscriber::fmt::layer().with_filter(filter));
127-
tracing::subscriber::set_global_default(subscriber).unwrap();
132+
tracing::subscriber::set_global_default(subscriber)
133+
.wrap_err("failed to set global tracing subscriber")?;
128134

129135
let options = CliOptions::parse();
130136

@@ -155,7 +161,12 @@ async fn main() -> eyre::Result<()> {
155161
return run_test_driver(rpc_config).await;
156162
}
157163

158-
let node_p2p_key = read_hex_file_bytes(&options.node_key);
164+
let node_p2p_key = read_hex_file_bytes(&options.node_key).wrap_err_with(|| {
165+
format!(
166+
"failed to load node key from {}",
167+
options.node_key.display()
168+
)
169+
})?;
159170
let p2p_socket = SocketAddr::new(IpAddr::from([0, 0, 0, 0]), options.gossipsub_port);
160171

161172
#[cfg(all(not(target_env = "msvc"), not(feature = "zk-alloc")))]
@@ -182,17 +193,27 @@ async fn main() -> eyre::Result<()> {
182193
let validator_config = options.validator_config;
183194
let validator_keys_dir = options.hash_sig_keys_dir;
184195

185-
let config_yaml = std::fs::read_to_string(&config_path).expect("Failed to read config.yaml");
196+
let config_yaml = std::fs::read_to_string(&config_path).wrap_err_with(|| {
197+
format!(
198+
"failed to read genesis config from {}",
199+
config_path.display()
200+
)
201+
})?;
186202
let genesis_config: GenesisConfig =
187-
serde_yaml_ng::from_str(&config_yaml).expect("Failed to parse config.yaml");
203+
serde_yaml_ng::from_str(&config_yaml).wrap_err_with(|| {
204+
format!(
205+
"failed to parse genesis config from {}",
206+
config_path.display()
207+
)
208+
})?;
188209

189210
info!(
190211
genesis_time = genesis_config.genesis_time,
191212
validator_count = genesis_config.genesis_validators.len(),
192213
"Loaded genesis configuration"
193214
);
194215

195-
let validator_config_file = read_validator_config_file(&validator_config);
216+
let validator_config_file = read_validator_config_file(&validator_config)?;
196217
let node_names = load_node_names(&validator_config_file);
197218

198219
// Resolve attestation_committee_count: CLI flag > validator-config.yaml > 1.
@@ -212,17 +233,22 @@ async fn main() -> eyre::Result<()> {
212233
);
213234
ethlambda_blockchain::metrics::set_attestation_committee_count(attestation_committee_count);
214235

215-
let bootnodes = read_bootnodes(&bootnodes_path);
236+
let bootnodes = read_bootnodes(&bootnodes_path)?;
216237

217238
let validator_keys =
218239
read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id)
219-
.expect("Failed to load validator keys");
240+
.wrap_err("failed to load validator keys")?;
220241

221242
let data_dir =
222243
std::path::absolute(&options.data_dir).unwrap_or_else(|_| options.data_dir.clone());
223244
info!(data_dir = %data_dir.display(), "Initializing DB");
224-
std::fs::create_dir_all(&data_dir).expect("Failed to create data directory");
225-
let backend = Arc::new(RocksDBBackend::open(&data_dir).expect("Failed to open RocksDB"));
245+
std::fs::create_dir_all(&data_dir)
246+
.wrap_err_with(|| format!("failed to create data directory {}", data_dir.display()))?;
247+
let backend = Arc::new(
248+
RocksDBBackend::open(&data_dir)
249+
.map_err(|err| eyre::eyre!("{err}"))
250+
.wrap_err_with(|| format!("failed to open RocksDB at {}", data_dir.display()))?,
251+
);
226252

227253
let store = fetch_initial_state(
228254
options.checkpoint_sync_url.as_deref(),
@@ -260,7 +286,7 @@ async fn main() -> eyre::Result<()> {
260286
is_aggregator: options.is_aggregator,
261287
aggregate_subnet_ids: options.aggregate_subnet_ids,
262288
})
263-
.expect("failed to build swarm");
289+
.wrap_err("failed to build swarm")?;
264290

265291
let p2p = P2P::spawn(built, store.clone(), node_names);
266292

@@ -383,9 +409,20 @@ struct ValidatorConfigEntry {
383409
privkey: H256,
384410
}
385411

386-
fn read_validator_config_file(path: impl AsRef<Path>) -> ValidatorConfigFile {
387-
let yaml = std::fs::read_to_string(&path).expect("Failed to read validator config file");
388-
serde_yaml_ng::from_str(&yaml).expect("Failed to parse validator config file")
412+
fn read_validator_config_file(path: impl AsRef<Path>) -> eyre::Result<ValidatorConfigFile> {
413+
let path = path.as_ref();
414+
let yaml = std::fs::read_to_string(path).wrap_err_with(|| {
415+
format!(
416+
"failed to read validator config file from {}",
417+
path.display()
418+
)
419+
})?;
420+
serde_yaml_ng::from_str(&yaml).wrap_err_with(|| {
421+
format!(
422+
"failed to parse validator config file from {}",
423+
path.display()
424+
)
425+
})
389426
}
390427

391428
fn load_node_names(file: &ValidatorConfigFile) -> HashMap<PeerId, String> {
@@ -398,12 +435,21 @@ fn load_node_names(file: &ValidatorConfigFile) -> HashMap<PeerId, String> {
398435
ethlambda_p2p::derive_peer_ids(names_and_privkeys)
399436
}
400437

401-
fn read_bootnodes(bootnodes_path: impl AsRef<Path>) -> Vec<Bootnode> {
402-
let bootnodes_yaml =
403-
std::fs::read_to_string(bootnodes_path).expect("Failed to read bootnodes file");
404-
let enrs: Vec<String> =
405-
serde_yaml_ng::from_str(&bootnodes_yaml).expect("Failed to parse bootnodes file");
406-
parse_enrs(enrs)
438+
fn read_bootnodes(bootnodes_path: impl AsRef<Path>) -> eyre::Result<Vec<Bootnode>> {
439+
let bootnodes_path = bootnodes_path.as_ref();
440+
let bootnodes_yaml = std::fs::read_to_string(bootnodes_path).wrap_err_with(|| {
441+
format!(
442+
"failed to read bootnodes file from {}",
443+
bootnodes_path.display()
444+
)
445+
})?;
446+
let enrs: Vec<String> = serde_yaml_ng::from_str(&bootnodes_yaml).wrap_err_with(|| {
447+
format!(
448+
"failed to parse bootnodes file from {}",
449+
bootnodes_path.display()
450+
)
451+
})?;
452+
Ok(parse_enrs(enrs))
407453
}
408454

409455
/// One entry in `annotated_validators.yaml` as emitted by `lean-quickstart`'s
@@ -476,18 +522,26 @@ fn read_validator_keys(
476522
validators_path: impl AsRef<Path>,
477523
validator_keys_dir: impl AsRef<Path>,
478524
node_id: &str,
479-
) -> Result<HashMap<u64, ValidatorKeyPair>, String> {
525+
) -> eyre::Result<HashMap<u64, ValidatorKeyPair>> {
480526
let validators_path = validators_path.as_ref();
481527
let validator_keys_dir = validator_keys_dir.as_ref();
482-
let validators_yaml = std::fs::read_to_string(validators_path)
483-
.map_err(|err| format!("Failed to read validators file: {err}"))?;
528+
let validators_yaml = std::fs::read_to_string(validators_path).wrap_err_with(|| {
529+
format!(
530+
"failed to read validators file from {}",
531+
validators_path.display()
532+
)
533+
})?;
484534
let validator_infos: BTreeMap<String, Vec<AnnotatedValidator>> =
485-
serde_yaml_ng::from_str(&validators_yaml)
486-
.map_err(|err| format!("Failed to parse validators file: {err}"))?;
535+
serde_yaml_ng::from_str(&validators_yaml).wrap_err_with(|| {
536+
format!(
537+
"failed to parse validators file from {}",
538+
validators_path.display()
539+
)
540+
})?;
487541

488542
let validator_vec = validator_infos
489543
.get(node_id)
490-
.ok_or_else(|| format!("Node ID '{node_id}' not found in validators config"))?;
544+
.ok_or_else(|| eyre::eyre!("node ID '{node_id}' not found in validators config"))?;
491545

492546
let resolve_path = |file: &Path| -> PathBuf {
493547
if file.is_absolute() {
@@ -500,41 +554,34 @@ fn read_validator_keys(
500554
// Group entries per validator index, routing each to its role slot.
501555
let mut grouped: BTreeMap<u64, RoleSlots> = BTreeMap::new();
502556
for entry in validator_vec {
503-
let role = classify_role(&entry.privkey_file)?;
557+
let role = classify_role(&entry.privkey_file).map_err(eyre::Report::msg)?;
504558
let path = resolve_path(&entry.privkey_file);
505559
let slots = grouped.entry(entry.index).or_default();
506560
let target = match role {
507561
ValidatorKeyRole::Attestation => &mut slots.attestation,
508562
ValidatorKeyRole::Proposal => &mut slots.proposal,
509563
};
510564
if target.is_some() {
511-
return Err(format!(
512-
"validator {}: duplicate {role:?} entry",
513-
entry.index
514-
));
565+
eyre::bail!("validator {}: duplicate {role:?} entry", entry.index);
515566
}
516567
*target = Some(path);
517568
}
518569

519-
let load_key = |path: &Path, purpose: &str| -> Result<ValidatorSecretKey, String> {
520-
let bytes = std::fs::read(path).map_err(|err| {
521-
format!(
522-
"Failed to read {purpose} key file {}: {err}",
523-
path.display()
524-
)
525-
})?;
570+
let load_key = |path: &Path, purpose: &str| -> eyre::Result<ValidatorSecretKey> {
571+
let bytes = std::fs::read(path)
572+
.wrap_err_with(|| format!("failed to read {purpose} key file {}", path.display()))?;
526573
ValidatorSecretKey::from_bytes(&bytes)
527-
.map_err(|err| format!("Failed to parse {purpose} key {}: {err:?}", path.display()))
574+
.map_err(|err| eyre::eyre!("failed to parse {purpose} key {}: {err:?}", path.display()))
528575
};
529576

530577
let mut validator_keys = HashMap::new();
531578
for (idx, slots) in grouped {
532579
let att_path = slots
533580
.attestation
534-
.ok_or_else(|| format!("validator {idx}: missing attester entry"))?;
581+
.ok_or_else(|| eyre::eyre!("validator {idx}: missing attester entry"))?;
535582
let prop_path = slots
536583
.proposal
537-
.ok_or_else(|| format!("validator {idx}: missing proposer entry"))?;
584+
.ok_or_else(|| eyre::eyre!("validator {idx}: missing proposer entry"))?;
538585

539586
info!(
540587
%node_id,
@@ -565,20 +612,13 @@ fn read_validator_keys(
565612
Ok(validator_keys)
566613
}
567614

568-
fn read_hex_file_bytes(path: impl AsRef<Path>) -> Vec<u8> {
615+
fn read_hex_file_bytes(path: impl AsRef<Path>) -> eyre::Result<Vec<u8>> {
569616
let path = path.as_ref();
570-
let Ok(file_content) = std::fs::read_to_string(path)
571-
.inspect_err(|err| error!(file=%path.display(), %err, "Failed to read hex file"))
572-
else {
573-
std::process::exit(1);
574-
};
617+
let file_content = std::fs::read_to_string(path)
618+
.wrap_err_with(|| format!("failed to read hex file from {}", path.display()))?;
575619
let hex_string = file_content.trim().trim_start_matches("0x");
576-
let Ok(bytes) = hex::decode(hex_string)
577-
.inspect_err(|err| error!(file=%path.display(), %err, "Failed to decode hex file"))
578-
else {
579-
std::process::exit(1);
580-
};
581-
bytes
620+
hex::decode(hex_string)
621+
.wrap_err_with(|| format!("failed to decode hex file from {}", path.display()))
582622
}
583623

584624
/// Fetch the initial state for the node.
@@ -617,6 +657,30 @@ async fn fetch_initial_state(
617657
};
618658

619659
// Checkpoint sync path
660+
661+
// Prefer resuming from a fresh on-disk state to avoid re-downloading what we already have.
662+
if let Some(store) = Store::from_db_state(backend.clone(), genesis.genesis_time) {
663+
let now_ms = SystemTime::UNIX_EPOCH
664+
.elapsed()
665+
.expect("already past the unix epoch")
666+
.as_millis() as u64;
667+
let current_slot =
668+
now_ms.saturating_sub(genesis.genesis_time * 1000) / MILLISECONDS_PER_SLOT;
669+
let finalized_slot = store.latest_finalized().slot;
670+
let gap = current_slot.saturating_sub(finalized_slot);
671+
if gap <= MAX_RESUMABLE_DB_STATE_AGE {
672+
info!(
673+
finalized_slot,
674+
current_slot, gap, "Resuming from existing DB state"
675+
);
676+
return Ok(store);
677+
}
678+
warn!(
679+
finalized_slot,
680+
current_slot, gap, "Existing DB state is stale; falling through to checkpoint sync"
681+
);
682+
}
683+
620684
info!(%checkpoint_url, "Starting checkpoint sync");
621685

622686
// The state and block are fetched in parallel; if the peer advances

crates/blockchain/src/block_builder.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,14 @@ fn score_entry(
364364
let total = prior_count + new_voters.len();
365365
let crosses_2_3 = 3 * total >= 2 * validator_count;
366366

367-
// 3SF-mini finalization requires no slot strictly between source.slot
368-
// and target.slot to still be justifiable (so source and target are
369-
// consecutive justified checkpoints in the projected post-state).
367+
// 3SF-mini finalization requires the source to lie past the finalized
368+
// boundary (a source at or behind it is already final and must not
369+
// re-finalize) and no slot strictly between source.slot and target.slot to
370+
// still be justifiable (so source and target are consecutive justified
371+
// checkpoints in the projected post-state). Mirrors `try_finalize` in the
372+
// state transition.
370373
let finalizes = crosses_2_3
374+
&& att_data.source.slot > projected_finalized_slot
371375
&& (att_data.source.slot + 1..att_data.target.slot)
372376
.all(|s| !slot_is_justifiable_after(s, projected_finalized_slot));
373377

@@ -683,6 +687,52 @@ mod tests {
683687
bits
684688
}
685689

690+
/// Regression (leanSpec #802): a supermajority entry whose source sits at
691+
/// the finalized boundary must be scored `Justify`, not `Finalize`. Such a
692+
/// source is already final, so it advances nothing; the empty scan range
693+
/// `(source.slot + 1..target.slot)` would otherwise make `.all(...)`
694+
/// vacuously true and mis-tier the entry as a finalizer.
695+
#[test]
696+
fn score_entry_does_not_finalize_source_at_boundary() {
697+
const NUM_VALIDATORS: usize = 4;
698+
const FINALIZED_SLOT: u64 = 4;
699+
700+
// Source at the finalized boundary, target one slot ahead (empty scan).
701+
let att_data = AttestationData {
702+
slot: 7,
703+
head: Checkpoint {
704+
slot: 5,
705+
root: H256([5u8; 32]),
706+
},
707+
target: Checkpoint {
708+
slot: 5,
709+
root: H256([5u8; 32]),
710+
},
711+
source: Checkpoint {
712+
slot: FINALIZED_SLOT,
713+
root: H256([4u8; 32]),
714+
},
715+
};
716+
717+
// Supermajority (3 of 4) so the entry crosses 2/3.
718+
let proofs = vec![TypeOneMultiSignature::empty(make_bits(&[0, 1, 2]))];
719+
720+
let (score, _) = score_entry(
721+
&att_data,
722+
&proofs,
723+
&HashMap::new(),
724+
FINALIZED_SLOT,
725+
NUM_VALIDATORS,
726+
)
727+
.expect("entry contributes new voters");
728+
729+
assert_eq!(
730+
score.tier,
731+
Tier::Justify,
732+
"source at the finalized boundary must justify, not finalize"
733+
);
734+
}
735+
686736
/// Regression test for https://github.com/lambdaclass/ethlambda/issues/259
687737
///
688738
/// Simulates a stall scenario by populating the payload pool with 50

0 commit comments

Comments
 (0)