diff --git a/lean_client/fork_choice/src/handlers.rs b/lean_client/fork_choice/src/handlers.rs index 778c38d4..59c0bdab 100644 --- a/lean_client/fork_choice/src/handlers.rs +++ b/lean_client/fork_choice/src/handlers.rs @@ -409,10 +409,13 @@ 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. #[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 +475,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 +801,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..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. /// @@ -412,8 +414,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 @@ -468,20 +470,29 @@ 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. 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, + )) }