diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 15291764..cf8e8f7c 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -392,6 +392,26 @@ pub fn on_gossip_attestation( pub fn on_gossip_aggregated_attestation( store: &mut Store, aggregated: SignedAggregatedAttestation, +) -> Result<(), StoreError> { + on_gossip_aggregated_attestation_core(store, aggregated, true) +} + +/// Process a gossiped aggregated attestation WITHOUT verifying its proof. +/// +/// Only for spec tests whose fixtures carry mocked (placeholder) proofs +/// (`proofSetting == 0`); production paths must use +/// [`on_gossip_aggregated_attestation`]. +pub fn on_gossip_aggregated_attestation_without_verification( + store: &mut Store, + aggregated: SignedAggregatedAttestation, +) -> Result<(), StoreError> { + on_gossip_aggregated_attestation_core(store, aggregated, false) +} + +fn on_gossip_aggregated_attestation_core( + store: &mut Store, + aggregated: SignedAggregatedAttestation, + verify: bool, ) -> Result<(), StoreError> { validate_attestation_data(store, &aggregated.data) .inspect_err(|_| metrics::inc_attestations_invalid())?; @@ -420,7 +440,7 @@ pub fn on_gossip_aggregated_attestation( let data_root = hashed.root(); let slot: u32 = aggregated.data.slot.try_into().expect("slot exceeds u32"); - { + if verify { let _timing = metrics::time_pq_sig_aggregated_signatures_verification(); ethlambda_crypto::verify_aggregated_signature( &aggregated.proof.proof, @@ -428,8 +448,8 @@ pub fn on_gossip_aggregated_attestation( &data_root, slot, ) + .map_err(StoreError::AggregateVerificationFailed)?; } - .map_err(StoreError::AggregateVerificationFailed)?; // Read stats before moving the proof into the store. let num_participants = aggregated.proof.participants.count_ones(); diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index 500c6bd2..1169dbd1 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -41,6 +41,10 @@ fn run(path: &Path) -> datatest_stable::Result<()> { } println!("Running test: {}", name); + // Mocked-proof vectors (`proofSetting == 0`) carry placeholder + // aggregation proofs that must not be cryptographically verified. + let proofs_are_mocked = test.proofs_are_mocked(); + // Initialize store from anchor state/block. // // Fixtures whose `steps` is empty are "anchor rejection" cases (e.g. @@ -93,11 +97,14 @@ fn run(path: &Path) -> datatest_stable::Result<()> { let signed_block = block_data.to_blank_signed_block(); - let block_time_ms = - genesis_time * 1000 + signed_block.message.slot * MILLISECONDS_PER_SLOT; - + // Advance time to the block's slot unless the test delivers + // the block ahead of the store clock. // NOTE: the has_proposal argument is set to true, following the spec - store::on_tick(&mut store, block_time_ms, true); + if step.tick_to_slot { + let block_time_ms = + genesis_time * 1000 + signed_block.message.slot * MILLISECONDS_PER_SLOT; + store::on_tick(&mut store, block_time_ms, true); + } let result = store::on_block_without_verification(&mut store, signed_block); let import_ok = result.is_ok(); assert_step_outcome(step_idx, step.valid, result)?; @@ -184,7 +191,13 @@ fn run(path: &Path) -> datatest_stable::Result<()> { TypeOneMultiSignature::new(proof_fixture.participants.into(), proof_data); let aggregated = SignedAggregatedAttestation { data, proof }; - let result = store::on_gossip_aggregated_attestation(&mut store, aggregated); + let result = if proofs_are_mocked { + store::on_gossip_aggregated_attestation_without_verification( + &mut store, aggregated, + ) + } else { + store::on_gossip_aggregated_attestation(&mut store, aggregated) + }; assert_step_outcome(step_idx, step.valid, result)?; } other => { diff --git a/crates/common/test-fixtures/src/fork_choice.rs b/crates/common/test-fixtures/src/fork_choice.rs index fe453f7f..1cda6280 100644 --- a/crates/common/test-fixtures/src/fork_choice.rs +++ b/crates/common/test-fixtures/src/fork_choice.rs @@ -44,6 +44,11 @@ pub struct ForkChoiceTest { #[serde(rename = "anchorBlock")] pub anchor_block: Block, pub steps: Vec, + /// Aggregation proof regime: 0 = mocked (placeholder bytes, must not be + /// verified), 1 = real and must verify, 2 = real and must fail + /// verification. Older fixtures lack the field and carry real proofs. + #[serde(rename = "proofSetting", default = "default_proof_setting")] + pub proof_setting: u8, #[serde(rename = "maxSlot")] #[allow(dead_code)] pub max_slot: u64, @@ -51,6 +56,18 @@ pub struct ForkChoiceTest { pub info: TestInfo, } +fn default_proof_setting() -> u8 { + 1 +} + +impl ForkChoiceTest { + /// Whether the vector's aggregation proofs are placeholders that must + /// not be cryptographically verified (`proofSetting == 0`). + pub fn proofs_are_mocked(&self) -> bool { + self.proof_setting == 0 + } +} + // ============================================================================ // Step Types // ============================================================================ @@ -76,6 +93,11 @@ pub struct ForkChoiceStep { pub has_proposal: Option, #[serde(rename = "isAggregator")] pub is_aggregator: Option, + /// Whether the harness must advance the store clock to the block's slot + /// before delivering a `block` step. Early-arrival tests set this to + /// `false` to deliver the block ahead of the store clock. + #[serde(rename = "tickToSlot", default = "default_true")] + pub tick_to_slot: bool, } fn default_true() -> bool { diff --git a/crates/common/test-fixtures/src/state_transition.rs b/crates/common/test-fixtures/src/state_transition.rs index 5bf159d2..890ff197 100644 --- a/crates/common/test-fixtures/src/state_transition.rs +++ b/crates/common/test-fixtures/src/state_transition.rs @@ -19,6 +19,6 @@ use serde::Deserialize; pub struct StateTransitionRunRequest { pub pre: TestState, pub blocks: Vec, - #[serde(default, rename = "expectException")] + #[serde(default, rename = "expectException", alias = "rejectionReason")] pub expect_exception: Option, } diff --git a/crates/common/test-fixtures/src/verify_signatures.rs b/crates/common/test-fixtures/src/verify_signatures.rs index 496a7c57..9793ee5c 100644 --- a/crates/common/test-fixtures/src/verify_signatures.rs +++ b/crates/common/test-fixtures/src/verify_signatures.rs @@ -44,7 +44,10 @@ pub struct VerifySignaturesTest { pub anchor_state: TestState, #[serde(rename = "signedBlock")] pub signed_block: TestSignedBlock, - #[serde(rename = "expectException")] + /// Expected rejection, when present. Newer fixtures name this field + /// `rejectionReason` (leanSpec replaced `expectException`); both + /// spellings are accepted. + #[serde(default, rename = "expectException", alias = "rejectionReason")] pub expect_exception: Option, #[serde(rename = "_info")] #[allow(dead_code)] diff --git a/crates/common/types/tests/ssz_spectests.rs b/crates/common/types/tests/ssz_spectests.rs index 8391d3ff..8cdb877c 100644 --- a/crates/common/types/tests/ssz_spectests.rs +++ b/crates/common/types/tests/ssz_spectests.rs @@ -5,7 +5,7 @@ use ethlambda_types::primitives::HashTreeRoot; mod ssz_types; use ssz_types::{SszTestCase, SszTestVector, decode_hex, decode_hex_h256}; -const SUPPORTED_FIXTURE_FORMAT: &str = "ssz"; +const SUPPORTED_FIXTURE_FORMAT: &str = "ssz_test"; fn run(path: &Path) -> datatest_stable::Result<()> { let tests = SszTestVector::from_file(path)?; diff --git a/crates/net/rpc/src/test_driver.rs b/crates/net/rpc/src/test_driver.rs index 1a19f4f4..c963d1ff 100644 --- a/crates/net/rpc/src/test_driver.rs +++ b/crates/net/rpc/src/test_driver.rs @@ -356,10 +356,13 @@ fn apply_step(store: &mut Store, step: ForkChoiceStep) -> Result<(), String> { .ok_or_else(|| "block step missing block data".to_string())?; let signed_block = block_data.to_blank_signed_block(); // Match the spec-test runner: advance time to the block's slot - // before importing so the future-slot guard doesn't reject it. - let block_time_ms = store.config().genesis_time * 1000 - + signed_block.message.slot * MILLISECONDS_PER_SLOT; - store::on_tick(store, block_time_ms, true); + // before importing, unless the step delivers the block ahead of + // the store clock. + if step.tick_to_slot { + let block_time_ms = store.config().genesis_time * 1000 + + signed_block.message.slot * MILLISECONDS_PER_SLOT; + store::on_tick(store, block_time_ms, true); + } store::on_block_without_verification(store, signed_block).map_err(|e| e.to_string()) } "attestation" => {