From bca71c5d5837b0f1c1cb5203f490e579bdb81ad4 Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Sun, 16 Aug 2026 21:39:21 +0530 Subject: [PATCH 1/3] Skip XMSS verify for mocked aggregate proofs --- lean_client/fork_choice/src/handlers.rs | 14 +++++++---- lean_client/http_api/src/test_driver.rs | 32 ++++++++++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 778c38d4..1f90c117 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -409,10 +409,14 @@ pub fn on_attestation( /// Verifies the aggregated XMSS proof against participant public keys and stores /// it in `latest_new_aggregated_payloads`. At interval 3, these are merged with /// `latest_known_aggregated_payloads` (from blocks) to compute safe target. +/// +/// `verify_proof` gates only the XMSS SNARK check (production: `true`; mocked +/// spec-test proofs: `false`). #[inline] pub fn on_aggregated_attestation( store: &mut Store, signed_aggregated_attestation: SignedAggregatedAttestation, + verify_proof: bool, ) -> Result<()> { // Structure: { data: AttestationData, proof: AggregatedSignatureProof } let attestation_data = signed_aggregated_attestation.data.clone(); @@ -472,9 +476,11 @@ pub fn on_aggregated_attestation( }) .collect::>>()?; - proof - .verify(public_keys, data_root, attestation_data.slot.0 as u32) - .context("aggregated attestation proof verification failed")?; + if verify_proof { + proof + .verify(public_keys, data_root, attestation_data.slot.0 as u32) + .context("aggregated attestation proof verification failed")?; + } let attestation_slot = attestation_data.slot; for vid in &validator_ids { @@ -796,7 +802,7 @@ pub fn apply_verified_block( .remove(&block_root) .unwrap_or_default(); for signed_agg in pending_agg { - if let Err(err) = on_aggregated_attestation(store, signed_agg) { + if let Err(err) = on_aggregated_attestation(store, signed_agg, true) { warn!(%err, "Pending aggregated attestation retry failed after block arrival"); } } diff --git a/lean_client/http_api/src/test_driver.rs b/lean_client/http_api/src/test_driver.rs index e2f17a87..cf894cd0 100644 --- a/lean_client/http_api/src/test_driver.rs +++ b/lean_client/http_api/src/test_driver.rs @@ -412,8 +412,8 @@ fn apply_step( // `Checks` steps still see a snapshot. return Ok(()); }; - let signed = build_signed_aggregated_attestation(step)?; - on_aggregated_attestation(store, signed).map_err(|err| err.to_string()) + let (signed, verify_proof) = build_signed_aggregated_attestation(step)?; + on_aggregated_attestation(store, signed, verify_proof).map_err(|err| err.to_string()) } ForkChoiceStep::Checks { .. } => { // Pure-assertion step. The simulator validates against the @@ -461,6 +461,10 @@ fn hex_root(root: &ssz::H256) -> String { format!("0x{}", hex::encode(root.as_bytes())) } +/// Sentinel emitted by the leanSpec fixture generator for `proofSetting: 0` +/// (mocked) aggregation proofs. Real proofs never contain it. +const MOCK_AGGREGATION_PROOF_MARKER: &[u8] = b"MOCKED-AGGREGATION-PROOF"; + /// Build a `SignedAggregatedAttestation` from the fixture-supplied /// `gossipAggregatedAttestation` step payload. /// @@ -468,20 +472,30 @@ fn hex_root(root: &ssz::H256) -> String { /// string (the harness cannot re-aggregate without the signers' private /// keys), so we decode it directly into an [`AggregatedSignature`] and /// wrap with the participants bitfield from the same payload. +/// +/// Returns the attestation and whether its proof should be XMSS-verified — +/// `false` when the proof is the mocked sentinel, since the simulator (in the +/// hive repo) does not forward the fixture's `proofSetting`. fn build_signed_aggregated_attestation( step: GossipAggregatedAttestationStep, -) -> Result { +) -> Result<(SignedAggregatedAttestation, bool), String> { let proof_hex = step.proof.proof.data.trim_start_matches("0x"); let proof_bytes = hex::decode(proof_hex) .map_err(|err| format!("invalid hex in aggregate proof_data: {err}"))?; + let verify_proof = !proof_bytes + .windows(MOCK_AGGREGATION_PROOF_MARKER.len()) + .any(|window| window == MOCK_AGGREGATION_PROOF_MARKER); let proof_data = AggregatedSignature::new(&proof_bytes) .map_err(|err| format!("failed to construct aggregated signature: {err}"))?; - Ok(SignedAggregatedAttestation { - data: step.data.into(), - proof: AggregatedSignatureProof { - participants: step.proof.participants.into(), - proof_data, + Ok(( + SignedAggregatedAttestation { + data: step.data.into(), + proof: AggregatedSignatureProof { + participants: step.proof.participants.into(), + proof_data, + }, }, - }) + verify_proof, + )) } From 1c62cad96360ba4998815ee20fbdf0fc361d244e Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Sun, 16 Aug 2026 21:51:19 +0530 Subject: [PATCH 2/3] minor comment fixes --- lean_client/fork_choice/src/handlers.rs | 3 +-- lean_client/http_api/src/test_driver.rs | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 1f90c117..59c0bdab 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -410,8 +410,7 @@ pub fn on_attestation( /// it in `latest_new_aggregated_payloads`. At interval 3, these are merged with /// `latest_known_aggregated_payloads` (from blocks) to compute safe target. /// -/// `verify_proof` gates only the XMSS SNARK check (production: `true`; mocked -/// spec-test proofs: `false`). +/// `verify_proof` gates only the XMSS SNARK check. #[inline] pub fn on_aggregated_attestation( store: &mut Store, diff --git a/lean_client/http_api/src/test_driver.rs b/lean_client/http_api/src/test_driver.rs index cf894cd0..ed79e1ed 100644 --- a/lean_client/http_api/src/test_driver.rs +++ b/lean_client/http_api/src/test_driver.rs @@ -34,6 +34,8 @@ use spec_test_fixtures::{ use ssz::SszHash; use xmss::{AggregatedSignature, Signature}; +const MOCK_AGGREGATION_PROOF_MARKER: &[u8] = b"MOCKED-AGGREGATION-PROOF"; + /// Shared state for test-driver routes. Carries a writable handle to the /// fork-choice store plus the `BlockCache` that `on_block` requires. /// @@ -461,10 +463,6 @@ fn hex_root(root: &ssz::H256) -> String { format!("0x{}", hex::encode(root.as_bytes())) } -/// Sentinel emitted by the leanSpec fixture generator for `proofSetting: 0` -/// (mocked) aggregation proofs. Real proofs never contain it. -const MOCK_AGGREGATION_PROOF_MARKER: &[u8] = b"MOCKED-AGGREGATION-PROOF"; - /// Build a `SignedAggregatedAttestation` from the fixture-supplied /// `gossipAggregatedAttestation` step payload. /// @@ -474,8 +472,7 @@ const MOCK_AGGREGATION_PROOF_MARKER: &[u8] = b"MOCKED-AGGREGATION-PROOF"; /// wrap with the participants bitfield from the same payload. /// /// Returns the attestation and whether its proof should be XMSS-verified — -/// `false` when the proof is the mocked sentinel, since the simulator (in the -/// hive repo) does not forward the fixture's `proofSetting`. +/// `false` when the proof is the mocked sentinel. fn build_signed_aggregated_attestation( step: GossipAggregatedAttestationStep, ) -> Result<(SignedAggregatedAttestation, bool), String> { From cc18d23e2353bc4efa21b2e9b1afabed4b98c5e6 Mon Sep 17 00:00:00 2001 From: SamAg19 Date: Wed, 19 Aug 2026 05:49:38 +0530 Subject: [PATCH 3/3] Fix fork-choice spec-test harness deserialization and validation gaps --- lean_client/fork_choice/src/handlers.rs | 33 +++++++++++++++++-- lean_client/http_api/src/test_driver.rs | 22 ++++++++----- .../spec_test_fixtures/src/fork_choice.rs | 6 ++-- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 59c0bdab..be03d43f 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use anyhow::{Context, Result, anyhow, bail, ensure}; use containers::{ - AttestationData, Checkpoint, SignatureKey, SignedAggregatedAttestation, SignedAttestation, - SignedBlock, State, + AggregatedAttestation, AttestationData, Checkpoint, SignatureKey, SignedAggregatedAttestation, + SignedAttestation, SignedBlock, State, }; use metrics::METRICS; use parking_lot::RwLock; @@ -74,6 +74,14 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<() data.target.slot.0 ); + // A head in a future slot relative to the attestation slot is invalid. + ensure!( + data.slot >= data.head.slot, + "Attestation slot {} precedes head slot {}", + data.slot.0, + data.head.slot.0 + ); + // Validate checkpoint slots match block slots. let source_block = &store.blocks[&data.source.root]; let target_block = &store.blocks[&data.target.root]; @@ -226,6 +234,20 @@ pub fn on_gossip_attestation( }); })?; + // Reject validators outside the registry (independent of signature checks). + let num_validators = store + .states + .get(&attestation_data.target.root) + .ok_or_else(|| anyhow!("no state for target block {}", attestation_data.target.root))? + .validators + .len_u64(); + ensure!( + validator_id < num_validators, + "validator {} out of range (max {})", + validator_id, + num_validators + ); + // Non-aggregators validate attestation data but do not store or verify individual // signatures. Per leanSpec: only aggregators import gossip attestations for aggregation. // Subnet filtering is already enforced at the p2p subscription layer. @@ -724,6 +746,13 @@ pub fn on_block( current_slot, ); + // Reject block bodies carrying duplicate AttestationData (the state + // transition only bounds the distinct count, it does not dedup). + ensure!( + !AggregatedAttestation::has_duplicate_data(&signed_block.block.body.attestations), + "block body has duplicate AttestationData" + ); + process_block_internal(store, signed_block, block_root, verify_signatures)?; process_pending_blocks(store, cache, vec![block_root], verify_signatures); diff --git a/lean_client/http_api/src/test_driver.rs b/lean_client/http_api/src/test_driver.rs index ed79e1ed..6cbbe3cb 100644 --- a/lean_client/http_api/src/test_driver.rs +++ b/lean_client/http_api/src/test_driver.rs @@ -377,20 +377,26 @@ fn apply_step( on_tick(store, target_time_millis, has_proposal.unwrap_or(false)); Ok(()) } - ForkChoiceStep::Block { block, .. } => { + ForkChoiceStep::Block { + block, + tick_to_slot, + .. + } => { let block: containers::Block = block.into(); let signed = SignedBlock { block, proof: MultiMessageAggregate::default(), }; - // Advance store time to the block's slot before applying it. - // Mirrors the local fork-choice test: attestations embedded in - // the block reference the slot, so the store needs to be at or - // past that interval. - let slot_time_millis = - (store.config.genesis_time + signed.block.slot.0 * SECONDS_PER_SLOT) * 1000; - on_tick(store, slot_time_millis, false); + // Advance store time to the block's slot before applying it, but + // only when the fixture requests it. Fixtures with + // `tickToSlot: false` deliberately keep the clock behind the block + // to exercise early-arrival and future-horizon handling. + if tick_to_slot { + let slot_time_millis = + (store.config.genesis_time + signed.block.slot.0 * SECONDS_PER_SLOT) * 1000; + on_tick(store, slot_time_millis, false); + } // Skip XMSS signature verification — fork_choice fixtures ship // unsigned step blocks, so we apply them with a placeholder diff --git a/lean_client/spec_test_fixtures/src/fork_choice.rs b/lean_client/spec_test_fixtures/src/fork_choice.rs index 74250afc..4df693d9 100644 --- a/lean_client/spec_test_fixtures/src/fork_choice.rs +++ b/lean_client/spec_test_fixtures/src/fork_choice.rs @@ -43,7 +43,7 @@ pub enum ForkChoiceStep { time: Option, #[serde(default)] interval: Option, - #[serde(default)] + #[serde(default, rename = "hasProposal")] has_proposal: Option, #[serde(default)] checks: Option, @@ -58,7 +58,7 @@ pub enum ForkChoiceStep { time: Option, #[serde(default)] interval: Option, - #[serde(default)] + #[serde(default, rename = "hasProposal")] has_proposal: Option, #[serde(default)] checks: Option, @@ -69,6 +69,8 @@ pub enum ForkChoiceStep { valid: bool, #[serde(default)] checks: Option, + #[serde(default, rename = "tickToSlot")] + tick_to_slot: bool, block: TestBlockWithAttestation, }, /// Apply a single-validator gossip attestation to the store.