Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion crates/blockchain/src/block_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,10 +668,13 @@ fn compact_attestations(
let pubkeys = proof
.participant_indices()
.map(|vid| {
let not_in_state = StoreError::ValidatorNotInState {
validator_index: vid,
};
let validator = head_state
.validators
.get(vid as usize)
.ok_or(StoreError::InvalidValidatorIndex)?;
.ok_or(not_in_state)?;
ValidatorPublicKey::from_bytes(&validator.attestation_pubkey)
.map_err(|_| StoreError::PubkeyDecodingFailed(vid))
})
Expand Down
148 changes: 117 additions & 31 deletions crates/blockchain/src/spec_test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,104 @@
//! functions so fixture replay cannot drift between the two entry points.

use ethlambda_storage::Store;
use ethlambda_test_fixtures::fork_choice::ForkChoiceStep;
use ethlambda_test_fixtures::{RejectionReason, fork_choice::ForkChoiceStep};
use ethlambda_types::{
attestation::{
AggregationBits, HashedAttestationData, SignedAggregatedAttestation, SignedAttestation,
},
block::{ByteList512KiB, SingleMessageAggregate},
};

use crate::{MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, store};
use crate::{
MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT,
store::{self, StoreError},
};

/// Prefix emitted by leanSpec's mocked aggregation prover.
const MOCK_PROOF_PREFIX: &[u8] = b"\x00MOCKED-AGGREGATION-PROOF\x00";

/// Why a fork-choice fixture step failed.
///
/// Distinguishes a client rejection, which negative fixtures assert against
/// their `rejectionReason`, from a harness failure, which means the fixture
/// asked for something this runner cannot replay.
#[derive(Debug, thiserror::Error)]
pub enum StepError {
/// The store rejected the step.
#[error(transparent)]
Store(#[from] StoreError),

/// The step is malformed or names something the runner cannot replay. Never
/// a client rejection, so it never satisfies an expected `rejectionReason`.
#[error("{0}")]
Harness(String),
}

impl StepError {
/// The leanSpec rejection reason this failure corresponds to, if any.
pub fn rejection_reason(&self) -> Option<RejectionReason> {
match self {
Self::Store(err) => rejection_reason(err),
Self::Harness(_) => None,
}
}
}

/// Classify a store rejection into the reason leanSpec would report for it,
/// mirroring the spec's `classify_rejection`.
///
/// `None` means the variant has no spec counterpart, which the spec-test runners
/// report as an unclassified rejection rather than accepting silently. The match
/// is exhaustive so a new [`StoreError`] variant forces that decision here.
///
/// `StateTransitionFailed` is the one context-dependent variant: it defers to
/// the state-transition classification, which the STF runner asserts directly.
pub fn rejection_reason(err: &StoreError) -> Option<RejectionReason> {
let reason = match err {
StoreError::MissingParentState { .. } => RejectionReason::UnknownParentBlock,
StoreError::ValidatorNotInState { .. } => RejectionReason::ValidatorNotInState,
StoreError::AttesterIndexOutOfRange { .. } => RejectionReason::ValidatorIndexOutOfRange,
StoreError::ProposerIndexOutOfRange { .. } => RejectionReason::ProposerIndexOutOfRange,
StoreError::SignatureDecodingFailed | StoreError::SignatureVerificationFailed => {
RejectionReason::InvalidSignature
}
StoreError::StateTransitionFailed(err) => err.into(),
StoreError::UnknownSourceBlock(_) => RejectionReason::UnknownSourceBlock,
StoreError::UnknownTargetBlock(_) => RejectionReason::UnknownTargetBlock,
StoreError::UnknownHeadBlock(_) => RejectionReason::UnknownHeadBlock,
StoreError::SourceExceedsTarget => RejectionReason::SourceAfterTarget,
StoreError::HeadOlderThanTarget { .. } => RejectionReason::HeadOlderThanTarget,
StoreError::SourceSlotMismatch { .. } => RejectionReason::SourceSlotMismatch,
StoreError::TargetSlotMismatch { .. } => RejectionReason::TargetSlotMismatch,
StoreError::HeadSlotMismatch { .. } => RejectionReason::HeadSlotMismatch,
StoreError::SourceNotAncestorOfTarget => RejectionReason::SourceNotAncestorOfTarget,
StoreError::TargetNotAncestorOfHead => RejectionReason::TargetNotAncestorOfHead,
StoreError::HeadNotDescendantOfFinalized { .. } => {
RejectionReason::HeadNotDescendantOfFinalized
}
StoreError::AttestationSlotBeforeHead { .. } => RejectionReason::AttestationSlotBeforeHead,
StoreError::AttestationTooFarInFuture { .. } => RejectionReason::AttestationTooFarInFuture,
StoreError::AggregateVerificationFailed(_) => RejectionReason::InvalidSignature,
StoreError::BlockProofVerificationFailed(_) => RejectionReason::InvalidBlockProof,
StoreError::EmptyAggregationBits => RejectionReason::EmptyAggregationBits,
StoreError::NotProposer { .. } => RejectionReason::WrongProposer,
StoreError::DuplicateAttestationData { .. } => RejectionReason::DuplicateAttestationData,
StoreError::TooManyAttestationData { .. } => RejectionReason::TooManyAttestationData,
StoreError::BlockSlotGapTooLarge { .. } => RejectionReason::BlockSlotGapTooLarge,
StoreError::BlockTooFarInFuture { .. } => RejectionReason::BlockTooFarInFuture,

// Internal failures with no spec counterpart: the spec has no undecodable
// registry pubkey, no aggregation step inside validation, no state that
// can go missing behind a known block, and no slot width limit (its
// slots are unbounded where ours narrow to the XMSS epoch's u32).
StoreError::PubkeyDecodingFailed(_)
| StoreError::SignatureAggregationFailed(_)
| StoreError::MissingTargetState(_)
| StoreError::SlotOutOfRange(_) => return None,
};
Some(reason)
}

/// Apply one fork-choice fixture step.
///
/// `proofs_are_mocked` is supplied by complete offline vectors through their
Expand All @@ -26,7 +111,7 @@ pub fn apply_fork_choice_step(
store: &mut Store,
step: &ForkChoiceStep,
proofs_are_mocked: Option<bool>,
) -> Result<(), String> {
) -> Result<(), StepError> {
match step.step_type.as_str() {
"tick" => {
let genesis_time = store.config().expect("config exists").genesis_time;
Expand All @@ -35,7 +120,11 @@ pub fn apply_fork_choice_step(
(None, Some(interval)) => {
genesis_time * 1000 + interval * MILLISECONDS_PER_INTERVAL
}
(None, None) => return Err("tick step missing time and interval".to_string()),
(None, None) => {
return Err(StepError::Harness(
"tick step missing time and interval".to_string(),
));
}
};
store::on_tick(store, timestamp_ms, step.has_proposal.unwrap_or(false));
Ok(())
Expand All @@ -44,14 +133,14 @@ pub fn apply_fork_choice_step(
let block_data = step
.block
.as_ref()
.ok_or_else(|| "block step missing block data".to_string())?;
.ok_or_else(|| StepError::Harness("block step missing block data".to_string()))?;
let signed_block = block_data.to_blank_signed_block();
if step.tick_to_slot {
let block_time_ms = store.config().expect("config exists").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())?;
store::on_block_without_verification(store, signed_block)?;

let block = block_data.to_block();
let entries = block.body.attestations.iter().map(|att| {
Expand All @@ -68,48 +157,45 @@ pub fn apply_fork_choice_step(
let att = step
.attestation
.as_ref()
.ok_or_else(|| "attestation step missing data".to_string())?;
.ok_or_else(|| StepError::Harness("attestation step missing data".to_string()))?;
let signed = SignedAttestation {
validator_id: att
.validator_id
.ok_or_else(|| "attestation step missing validatorId".to_string())?,
validator_id: att.validator_id.ok_or_else(|| {
StepError::Harness("attestation step missing validatorId".to_string())
})?,
data: att.data.clone().into(),
signature: att
.signature
.clone()
.ok_or_else(|| "attestation step missing signature".to_string())?,
signature: att.signature.clone().ok_or_else(|| {
StepError::Harness("attestation step missing signature".to_string())
})?,
};
store::on_gossip_attestation(store, &signed, step.is_aggregator.unwrap_or(false))
.map_err(|e| e.to_string())
store::on_gossip_attestation(store, &signed, step.is_aggregator.unwrap_or(false))?;
Ok(())
}
"gossipAggregatedAttestation" => {
let att = step
.attestation
.as_ref()
.ok_or_else(|| "gossipAggregatedAttestation step missing data".to_string())?;
let proof = att
.proof
.as_ref()
.ok_or_else(|| "gossipAggregatedAttestation step missing proof".to_string())?;
let att = step.attestation.as_ref().ok_or_else(|| {
StepError::Harness("gossipAggregatedAttestation step missing data".to_string())
})?;
let proof = att.proof.as_ref().ok_or_else(|| {
StepError::Harness("gossipAggregatedAttestation step missing proof".to_string())
})?;
let participants: AggregationBits = proof.participants.clone().into();
let proof_bytes: Vec<u8> = proof.proof.clone().into();
let is_mocked =
proofs_are_mocked.unwrap_or_else(|| proof_bytes.starts_with(MOCK_PROOF_PREFIX));
let proof_data = ByteList512KiB::try_from(proof_bytes)
.map_err(|err| format!("aggregated proof data too large: {err:?}"))?;
let proof_data = ByteList512KiB::try_from(proof_bytes).map_err(|err| {
StepError::Harness(format!("aggregated proof data too large: {err:?}"))
})?;
let aggregated = SignedAggregatedAttestation {
proof: SingleMessageAggregate::new(participants, proof_data),
data: att.data.clone().into(),
};
if is_mocked {
store::on_gossip_aggregated_attestation_without_verification(store, aggregated)
.map_err(|e| e.to_string())
store::on_gossip_aggregated_attestation_without_verification(store, aggregated)?;
} else {
store::on_gossip_aggregated_attestation(store, aggregated)
.map_err(|e| e.to_string())
store::on_gossip_aggregated_attestation(store, aggregated)?;
}
Ok(())
}
"checks" => Ok(()),
other => Err(format!("unknown step type: {other}")),
other => Err(StepError::Harness(format!("unknown step type: {other}"))),
}
}
Loading