diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 07a7d4c6b6c..522b0a824a0 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -22,6 +22,8 @@ use beacon_chain::{ use execution_layer::{PayloadStatusV1, json_structures::JsonPayloadStatusV1Status}; use serde::Deserialize; use ssz_derive::Decode; +use state_processing::VerifySignatures; +use state_processing::envelope_processing::{self, VerifyStateRoot}; use state_processing::state_advance::complete_state_advance; use std::future::Future; use std::sync::Arc; @@ -29,8 +31,9 @@ use std::time::Duration; use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecar, - DataColumnSidecarList, DataColumnSubnetId, ExecutionBlockHash, Hash256, IndexedAttestation, - KzgProof, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, + DataColumnSidecarList, DataColumnSubnetId, ExecutionBlockHash, ExecutionPayloadEnvelope, + ExecutionPayloadGloas, ExecutionRequests, Hash256, IndexedAttestation, KzgProof, + ProposerPreparationData, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -72,6 +75,7 @@ pub struct Checks { proposer_boost_root: Option, get_proposer_head: Option, should_override_forkchoice_update: Option, + head_payload_status: Option, } #[derive(Debug, Clone, Deserialize)] @@ -94,7 +98,7 @@ impl From for PayloadStatusV1 { #[derive(Debug, Clone, Deserialize)] #[serde(untagged, deny_unknown_fields)] -pub enum Step { +pub enum Step { Tick { tick: u64, }, @@ -120,6 +124,10 @@ pub enum Step, }, @@ -151,6 +159,7 @@ pub struct ForkChoiceTest { Attestation, AttesterSlashing, PowBlock, + SignedExecutionPayloadEnvelope, >, >, } @@ -165,7 +174,7 @@ impl LoadCase for ForkChoiceTest { .expect("path must be valid OsStr") .to_string(); let spec = &testing_spec::(fork_name); - let steps: Vec, String, String, String>> = + let steps: Vec, String, String, String, String>> = yaml_decode_file(&path.join("steps.yaml"))?; // Resolve the object names in `steps.yaml` into actual decoded block/attestation objects. let steps = steps @@ -237,6 +246,17 @@ impl LoadCase for ForkChoiceTest { block_hash, payload_status, }), + Step::ExecutionPayloadEnvelope { + execution_payload, + valid, + } => { + let envelope = + ssz_decode_file(&path.join(format!("{execution_payload}.ssz_snappy")))?; + Ok(Step::ExecutionPayloadEnvelope { + execution_payload: envelope, + valid, + }) + } Step::Checks { checks } => Ok(Step::Checks { checks }), Step::MaybeValidBlockAndColumns { block, @@ -346,6 +366,12 @@ impl Case for ForkChoiceTest { el.server .set_payload_statuses(*block_hash, payload_status.clone().into()); } + Step::ExecutionPayloadEnvelope { + execution_payload, + valid, + } => { + tester.process_execution_payload_envelope(execution_payload.clone(), *valid)? + } Step::Checks { checks } => { let Checks { head, @@ -359,6 +385,7 @@ impl Case for ForkChoiceTest { proposer_boost_root, get_proposer_head, should_override_forkchoice_update: should_override_fcu, + head_payload_status, } = checks.as_ref(); if let Some(expected_head) = head { @@ -405,6 +432,10 @@ impl Case for ForkChoiceTest { if let Some(expected_proposer_head) = get_proposer_head { tester.check_expected_proposer_head(*expected_proposer_head)?; } + + if let Some(expected_payload_status) = head_payload_status { + tester.check_head_payload_status(*expected_payload_status)?; + } } Step::MaybeValidBlockAndColumns { @@ -466,6 +497,31 @@ impl Tester { )); } + // Store envelope for the anchor block so child blocks can resolve parent's full state root. + if case + .anchor_block + .body() + .signed_execution_payload_bid() + .is_ok() + { + let anchor_envelope = SignedExecutionPayloadEnvelope { + message: ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: harness.chain.genesis_block_root, + slot: case.anchor_state.slot(), + state_root: case.anchor_block.state_root(), + }, + signature: bls::Signature::empty(), + }; + harness + .chain + .store + .put_payload_envelope(&harness.chain.genesis_block_root, anchor_envelope) + .expect("should store genesis envelope"); + } + // Drop any blocks that might be loaded in the mock execution layer. Some of these tests // will provide their own blocks and we want to start from a clean state. harness @@ -784,6 +840,68 @@ impl Tester { ); } + pub fn process_execution_payload_envelope( + &self, + envelope: SignedExecutionPayloadEnvelope, + valid: bool, + ) -> Result<(), Error> { + let block_root = envelope.beacon_block_root(); + let envelope_state_root = envelope.message.state_root; + + let block = self + .harness + .chain + .get_blinded_block(&block_root) + .unwrap() + .unwrap(); + + let mut state = self + .harness + .chain + .get_state( + &block.state_root(), + Some(block.slot()), + CACHE_STATE_IN_TESTS, + ) + .unwrap() + .unwrap(); + + let result = envelope_processing::process_execution_payload_envelope( + &mut state, + Some(block.state_root()), + &envelope, + VerifySignatures::True, + VerifyStateRoot::True, + &self.spec, + ); + + if result.is_ok() != valid { + return Err(Error::DidntFail(format!( + "execution payload envelope for block root {} was valid={} whilst test expects valid={}. result: {:?}", + block_root, + result.is_ok(), + valid, + result + ))); + } + + if valid { + self.harness + .chain + .store + .put_payload_envelope(&block_root, envelope) + .expect("should store envelope"); + + self.harness + .chain + .store + .put_state(&envelope_state_root, &state) + .expect("should store post-envelope state"); + } + + Ok(()) + } + pub fn check_head(&self, expected_head: Head) -> Result<(), Error> { let head = self.find_head()?; let chain_head = Head { @@ -931,6 +1049,23 @@ impl Tester { check_equal("proposer_head", proposer_head, expected_proposer_head) } + pub fn check_head_payload_status(&self, expected_status: u8) -> Result<(), Error> { + let head = self.find_head()?; + let head_block_root = head.head_block_root(); + + let has_envelope = self + .harness + .chain + .store + .get_payload_envelope(&head_block_root) + .map_err(|e| Error::InternalError(format!("{:?}", e)))? + .is_some(); + + let actual_status: u8 = if has_envelope { 1 } else { 0 }; + + check_equal("head_payload_status", actual_status, expected_status) + } + pub fn check_should_override_fcu( &self, expected_should_override_fcu: ShouldOverrideFcu, diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index da3c5533b68..b5098916090 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -721,6 +721,11 @@ impl Handler for ForkChoiceHandler { return false; } + // Execution Payload tests exist only after Gloas. + if self.handler_name == "on_execution_payload" && !fork_name.gloas_enabled() { + return false; + } + // These tests check block validity (which may include signatures) and there is no need to // run them with fake crypto. cfg!(not(feature = "fake_crypto")) diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 3893df2ef74..cb4abed90ab 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1032,6 +1032,12 @@ fn fork_choice_deposit_with_reorg() { // There is no mainnet variant for this test. } +#[test] +fn fork_choice_on_execution_payload() { + ForkChoiceHandler::::new("on_execution_payload").run(); + ForkChoiceHandler::::new("on_execution_payload").run(); +} + #[test] fn optimistic_sync() { OptimisticSyncHandler::::default().run();