use super::*; use crate::bls_setting::BlsSetting; use crate::case_result::compare_beacon_state_results_without_caches; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ process_consolidation_requests, process_deposit_requests, process_withdrawal_requests, }; use state_processing::{ per_block_processing::{ errors::BlockProcessingError, process_block_header, process_execution_payload, process_operations::{ altair_deneb, base, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, process_proposer_slashings, }, process_sync_aggregate, process_withdrawals, VerifyBlockRoot, VerifySignatures, }, ConsensusContext, }; use std::fmt::Debug; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] struct Metadata { description: Option, bls_setting: Option, } #[derive(Debug, Clone, Deserialize)] struct ExecutionMetadata { execution_valid: bool, } /// Newtype for testing withdrawals. #[derive(Debug, Clone, Deserialize)] pub struct WithdrawalsPayload { payload: FullPayload, } #[derive(Debug, Clone)] pub struct Operations> { metadata: Metadata, execution_metadata: Option, pub pre: BeaconState, pub operation: Option, pub post: Option>, } pub trait Operation: Debug + Sync + Sized { fn handler_name() -> String; fn filename() -> String { format!("{}.ssz_snappy", Self::handler_name()) } fn is_enabled_for_fork(_fork_name: ForkName) -> bool { true } fn decode(path: &Path, fork_name: ForkName, spec: &ChainSpec) -> Result; fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError>; } impl Operation for Attestation { fn handler_name() -> String { "attestation".into() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { if fork_name < ForkName::Electra { Ok(Self::Base(ssz_decode_file(path)?)) } else { Ok(Self::Electra(ssz_decode_file(path)?)) } } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { initialize_epoch_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); if state.fork_name_unchecked().altair_enabled() { initialize_progressive_balances_cache(state, spec)?; altair_deneb::process_attestation( state, self.to_ref(), 0, &mut ctxt, VerifySignatures::True, spec, ) } else { base::process_attestations( state, [self.clone().to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, spec, ) } } } impl Operation for AttesterSlashing { fn handler_name() -> String { "attester_slashing".into() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { if fork_name.electra_enabled() { Ok(Self::Electra(ssz_decode_file(path)?)) } else { Ok(Self::Base(ssz_decode_file(path)?)) } } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); initialize_progressive_balances_cache(state, spec)?; process_attester_slashings( state, [self.clone().to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, spec, ) } } impl Operation for Deposit { fn handler_name() -> String { "deposit".into() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn is_enabled_for_fork(_: ForkName) -> bool { // Some deposit tests require signature verification but are not marked as such. cfg!(not(feature = "fake_crypto")) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { process_deposits(state, std::slice::from_ref(self), spec) } } impl Operation for ProposerSlashing { fn handler_name() -> String { "proposer_slashing".into() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); initialize_progressive_balances_cache(state, spec)?; process_proposer_slashings( state, std::slice::from_ref(self), VerifySignatures::True, &mut ctxt, spec, ) } } impl Operation for SignedVoluntaryExit { fn handler_name() -> String { "voluntary_exit".into() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { process_exits( state, std::slice::from_ref(self), VerifySignatures::True, spec, ) } } impl Operation for BeaconBlock { fn handler_name() -> String { "block_header".into() } fn filename() -> String { "block.ssz_snappy".into() } fn decode(path: &Path, _fork_name: ForkName, spec: &ChainSpec) -> Result { ssz_decode_file_with(path, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { let mut ctxt = ConsensusContext::new(state.slot()); process_block_header( state, self.to_ref().temporary_block_header(), VerifyBlockRoot::True, &mut ctxt, spec, )?; Ok(()) } } impl Operation for SyncAggregate { fn handler_name() -> String { "sync_aggregate".into() } fn filename() -> String { "sync_aggregate.ssz_snappy".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.altair_enabled() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { let proposer_index = state.get_beacon_proposer_index(state.slot(), spec)? as u64; process_sync_aggregate(state, self, proposer_index, VerifySignatures::True, spec) } } impl Operation for BeaconBlockBody> { fn handler_name() -> String { "execution_payload".into() } fn filename() -> String { "body.ssz_snappy".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.bellatrix_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file_with(path, |bytes| { Ok(match fork_name { ForkName::Bellatrix => BeaconBlockBody::Bellatrix(<_>::from_ssz_bytes(bytes)?), ForkName::Capella => BeaconBlockBody::Capella(<_>::from_ssz_bytes(bytes)?), ForkName::Deneb => BeaconBlockBody::Deneb(<_>::from_ssz_bytes(bytes)?), ForkName::Electra => BeaconBlockBody::Electra(<_>::from_ssz_bytes(bytes)?), ForkName::Fulu => BeaconBlockBody::Fulu(<_>::from_ssz_bytes(bytes)?), _ => panic!(), }) }) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, extra: &Operations, ) -> Result<(), BlockProcessingError> { let valid = extra .execution_metadata .as_ref() .is_some_and(|e| e.execution_valid); if valid { process_execution_payload::>(state, self.to_ref(), spec) } else { Err(BlockProcessingError::ExecutionInvalid) } } } impl Operation for BeaconBlockBody> { fn handler_name() -> String { "execution_payload".into() } fn filename() -> String { "body.ssz_snappy".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.bellatrix_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file_with(path, |bytes| { Ok(match fork_name { ForkName::Bellatrix => { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Bellatrix(inner.clone_as_blinded()) } ForkName::Capella => { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Capella(inner.clone_as_blinded()) } ForkName::Deneb => { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Deneb(inner.clone_as_blinded()) } ForkName::Electra => { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Electra(inner.clone_as_blinded()) } ForkName::Fulu => { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Fulu(inner.clone_as_blinded()) } _ => panic!(), }) }) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, extra: &Operations, ) -> Result<(), BlockProcessingError> { let valid = extra .execution_metadata .as_ref() .is_some_and(|e| e.execution_valid); if valid { process_execution_payload::>(state, self.to_ref(), spec) } else { Err(BlockProcessingError::ExecutionInvalid) } } } impl Operation for WithdrawalsPayload { fn handler_name() -> String { "withdrawals".into() } fn filename() -> String { "execution_payload.ssz_snappy".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.capella_enabled() } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file_with(path, |bytes| { ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) }) .map(|payload| WithdrawalsPayload { payload: payload.into(), }) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _: &Operations, ) -> Result<(), BlockProcessingError> { process_withdrawals::<_, FullPayload<_>>(state, self.payload.to_ref(), spec) } } impl Operation for SignedBlsToExecutionChange { fn handler_name() -> String { "bls_to_execution_change".into() } fn filename() -> String { "address_change.ssz_snappy".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.capella_enabled() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { process_bls_to_execution_changes( state, std::slice::from_ref(self), VerifySignatures::True, spec, ) } } impl Operation for WithdrawalRequest { fn handler_name() -> String { "withdrawal_request".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.electra_enabled() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { state.update_pubkey_cache()?; process_withdrawal_requests(state, std::slice::from_ref(self), spec) } } impl Operation for DepositRequest { fn handler_name() -> String { "deposit_request".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.electra_enabled() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { process_deposit_requests(state, std::slice::from_ref(self), spec) } } impl Operation for ConsolidationRequest { fn handler_name() -> String { "consolidation_request".into() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { fork_name.electra_enabled() } fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file(path) } fn apply_to( &self, state: &mut BeaconState, spec: &ChainSpec, _extra: &Operations, ) -> Result<(), BlockProcessingError> { state.update_pubkey_cache()?; process_consolidation_requests(state, std::slice::from_ref(self), spec) } } impl> LoadCase for Operations { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { let spec = &testing_spec::(fork_name); let metadata_path = path.join("meta.yaml"); let metadata: Metadata = if metadata_path.is_file() { yaml_decode_file(&metadata_path)? } else { Metadata::default() }; // For execution payloads only. let execution_yaml_path = path.join("execution.yaml"); let execution_metadata = if execution_yaml_path.is_file() { Some(yaml_decode_file(&execution_yaml_path)?) } else { None }; let pre = ssz_decode_state(&path.join("pre.ssz_snappy"), spec)?; // Check BLS setting here before SSZ deserialization, as most types require signatures // to be valid. let (operation, bls_error) = if metadata.bls_setting.unwrap_or_default().check().is_ok() { match O::decode(&path.join(O::filename()), fork_name, spec) { Ok(op) => (Some(op), None), Err(Error::InvalidBLSInput(error)) => (None, Some(error)), Err(e) => return Err(e), } } else { (None, None) }; let post_filename = path.join("post.ssz_snappy"); let post = if post_filename.is_file() { if let Some(bls_error) = bls_error { panic!("input is unexpectedly invalid: {}", bls_error); } Some(ssz_decode_state(&post_filename, spec)?) } else { None }; Ok(Self { metadata, execution_metadata, pre, operation, post, }) } } impl> Case for Operations { fn description(&self) -> String { self.metadata.description.clone().unwrap_or_default() } fn is_enabled_for_fork(fork_name: ForkName) -> bool { O::is_enabled_for_fork(fork_name) } fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let spec = &testing_spec::(fork_name); let mut pre_state = self.pre.clone(); // Processing requires the committee caches. // NOTE: some of the withdrawals tests have 0 active validators, do not try // to build the commitee cache in this case. if O::handler_name() != "withdrawals" { pre_state.build_all_committee_caches(spec).unwrap(); } let mut state = pre_state.clone(); let mut expected = self.post.clone(); if O::handler_name() != "withdrawals" { if let Some(post_state) = expected.as_mut() { post_state.build_all_committee_caches(spec).unwrap(); } } let mut result = self .operation .as_ref() .ok_or(Error::SkippedBls)? .apply_to(&mut state, spec, self) .map(|()| state); compare_beacon_state_results_without_caches(&mut result, &mut expected) } }