From e576c007e3d8f7e462ffb65781ade8be382a016b Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Tue, 29 Aug 2023 19:28:58 -0700 Subject: [PATCH] chore: introduce versioned `ExecutionPayload` (#4400) --- crates/consensus/beacon/src/engine/mod.rs | 34 +- .../consensus/beacon/src/engine/test_utils.rs | 11 +- crates/payload/builder/src/payload.rs | 34 +- crates/rpc/rpc-api/src/engine.rs | 17 +- crates/rpc/rpc-engine-api/src/engine_api.rs | 63 ++- crates/rpc/rpc-engine-api/src/payload.rs | 8 +- crates/rpc/rpc-engine-api/tests/it/payload.rs | 15 +- .../rpc/rpc-types/src/eth/engine/payload.rs | 504 ++++++++++++++---- 8 files changed, 515 insertions(+), 171 deletions(-) diff --git a/crates/consensus/beacon/src/engine/mod.rs b/crates/consensus/beacon/src/engine/mod.rs index 7665d575ce..c6f7cadfe5 100644 --- a/crates/consensus/beacon/src/engine/mod.rs +++ b/crates/consensus/beacon/src/engine/mod.rs @@ -1049,7 +1049,7 @@ where /// /// This returns a [`PayloadStatus`] that represents the outcome of a processed new payload and /// returns an error if an internal error occurred. - #[instrument(level = "trace", skip(self, payload, parent_beacon_block_root), fields(block_hash= ?payload.block_hash, block_number = %payload.block_number.as_u64(), is_pipeline_idle = %self.sync.is_pipeline_idle()), target = "consensus::engine")] + #[instrument(level = "trace", skip(self, payload, parent_beacon_block_root), fields(block_hash= ?payload.block_hash(), block_number = %payload.block_number(), is_pipeline_idle = %self.sync.is_pipeline_idle()), target = "consensus::engine")] fn on_new_payload( &mut self, payload: ExecutionPayload, @@ -1121,7 +1121,7 @@ where payload: ExecutionPayload, parent_beacon_block_root: Option, ) -> Result { - let parent_hash = payload.parent_hash; + let parent_hash = payload.parent_hash(); let block = match payload.try_into_sealed_block(parent_beacon_block_root) { Ok(block) => block, Err(error) => { @@ -1807,7 +1807,9 @@ mod tests { use assert_matches::assert_matches; use reth_primitives::{stage::StageCheckpoint, ChainSpec, ChainSpecBuilder, H256, MAINNET}; use reth_provider::{BlockWriter, ProviderFactory}; - use reth_rpc_types::engine::{ForkchoiceState, ForkchoiceUpdated, PayloadStatus}; + use reth_rpc_types::engine::{ + ExecutionPayloadV1, ForkchoiceState, ForkchoiceUpdated, PayloadStatus, + }; use reth_stages::{ExecOutput, PipelineError, StageError}; use std::{collections::VecDeque, sync::Arc, time::Duration}; use tokio::sync::oneshot::error::TryRecvError; @@ -1867,7 +1869,7 @@ mod tests { assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); // consensus engine is still idle because no FCUs were received - let _ = env.send_new_payload(SealedBlock::default().into(), None).await; + let _ = env.send_new_payload(ExecutionPayloadV1::from(SealedBlock::default()), None).await; assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); // consensus engine is still idle because pruning is running @@ -2282,14 +2284,20 @@ mod tests { // Send new payload let res = env - .send_new_payload(random_block(&mut rng, 0, None, None, Some(0)).into(), None) + .send_new_payload( + ExecutionPayloadV1::from(random_block(&mut rng, 0, None, None, Some(0))), + None, + ) .await; // Invalid, because this is a genesis block assert_matches!(res, Ok(result) => assert_matches!(result.status, PayloadStatusEnum::Invalid { .. })); // Send new payload let res = env - .send_new_payload(random_block(&mut rng, 1, None, None, Some(0)).into(), None) + .send_new_payload( + ExecutionPayloadV1::from(random_block(&mut rng, 1, None, None, Some(0))), + None, + ) .await; let expected_result = PayloadStatus::from_status(PayloadStatusEnum::Syncing); assert_matches!(res, Ok(result) => assert_eq!(result, expected_result)); @@ -2339,8 +2347,10 @@ mod tests { assert_matches!(res, Ok(ForkchoiceUpdated { payload_status, .. }) => assert_eq!(payload_status, expected_result)); // Send new payload - let result = - env.send_new_payload_retry_on_syncing(block2.clone().into(), None).await.unwrap(); + let result = env + .send_new_payload_retry_on_syncing(ExecutionPayloadV1::from(block2.clone()), None) + .await + .unwrap(); let expected_result = PayloadStatus::from_status(PayloadStatusEnum::Valid) .with_latest_valid_hash(block2.hash); assert_eq!(result, expected_result); @@ -2438,7 +2448,7 @@ mod tests { // Send new payload let block = random_block(&mut rng, 2, Some(H256::random()), None, Some(0)); - let res = env.send_new_payload(block.into(), None).await; + let res = env.send_new_payload(ExecutionPayloadV1::from(block), None).await; let expected_result = PayloadStatus::from_status(PayloadStatusEnum::Syncing); assert_matches!(res, Ok(result) => assert_eq!(result, expected_result)); @@ -2500,8 +2510,10 @@ mod tests { assert_matches!(res, Ok(ForkchoiceUpdated { payload_status, .. }) => assert_eq!(payload_status, expected_result)); // Send new payload - let result = - env.send_new_payload_retry_on_syncing(block2.clone().into(), None).await.unwrap(); + let result = env + .send_new_payload_retry_on_syncing(ExecutionPayloadV1::from(block2.clone()), None) + .await + .unwrap(); let expected_result = PayloadStatus::from_status(PayloadStatusEnum::Invalid { validation_error: BlockValidationError::BlockPreMerge { hash: block2.hash } diff --git a/crates/consensus/beacon/src/engine/test_utils.rs b/crates/consensus/beacon/src/engine/test_utils.rs index 287cd0f41c..c9c76a786f 100644 --- a/crates/consensus/beacon/src/engine/test_utils.rs +++ b/crates/consensus/beacon/src/engine/test_utils.rs @@ -66,21 +66,22 @@ impl TestEnv { Self { db, tip_rx, engine_handle } } - pub async fn send_new_payload( + pub async fn send_new_payload>( &self, - payload: ExecutionPayload, + payload: T, parent_beacon_block_root: Option, ) -> Result { - self.engine_handle.new_payload(payload, parent_beacon_block_root).await + self.engine_handle.new_payload(payload.into(), parent_beacon_block_root).await } /// Sends the `ExecutionPayload` message to the consensus engine and retries if the engine /// is syncing. - pub async fn send_new_payload_retry_on_syncing( + pub async fn send_new_payload_retry_on_syncing>( &self, - payload: ExecutionPayload, + payload: T, parent_beacon_block_root: Option, ) -> Result { + let payload: ExecutionPayload = payload.into(); loop { let result = self.send_new_payload(payload.clone(), parent_beacon_block_root).await?; if !result.is_syncing() { diff --git a/crates/payload/builder/src/payload.rs b/crates/payload/builder/src/payload.rs index cb17dc9b8f..c5e199ef71 100644 --- a/crates/payload/builder/src/payload.rs +++ b/crates/payload/builder/src/payload.rs @@ -6,7 +6,8 @@ use reth_primitives::{ use reth_revm_primitives::config::revm_spec_by_timestamp_after_merge; use reth_rlp::Encodable; use reth_rpc_types::engine::{ - ExecutionPayload, ExecutionPayloadEnvelope, PayloadAttributes, PayloadId, + ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadV1, PayloadAttributes, + PayloadId, }; use revm_primitives::{BlockEnv, CfgEnv}; @@ -57,26 +58,23 @@ impl BuiltPayload { } /// Converts the type into the response expected by `engine_getPayloadV1` - pub fn into_v1_payload(self) -> ExecutionPayload { + pub fn into_v1_payload(self) -> ExecutionPayloadV1 { self.into() } /// Converts the type into the response expected by `engine_getPayloadV2` - pub fn into_v2_payload(self) -> ExecutionPayloadEnvelope { - let mut envelope: ExecutionPayloadEnvelope = self.into(); - envelope.blobs_bundle = None; - envelope.should_override_builder = None; - envelope + pub fn into_v2_payload(self) -> ExecutionPayloadEnvelopeV2 { + self.into() } /// Converts the type into the response expected by `engine_getPayloadV2` - pub fn into_v3_payload(self) -> ExecutionPayloadEnvelope { + pub fn into_v3_payload(self) -> ExecutionPayloadEnvelopeV3 { self.into() } } // V1 engine_getPayloadV1 response -impl From for ExecutionPayload { +impl From for ExecutionPayloadV1 { fn from(value: BuiltPayload) -> Self { value.block.into() } @@ -87,13 +85,21 @@ impl From for ExecutionPayload { // have explicitly versioned return types for getPayload. Then BuiltPayload could essentially be a // builder for those types, and it would not be possible to e.g. return cancun fields for a // pre-cancun endpoint. -impl From for ExecutionPayloadEnvelope { +impl From for ExecutionPayloadEnvelopeV2 { + fn from(value: BuiltPayload) -> Self { + let BuiltPayload { block, fees, .. } = value; + + ExecutionPayloadEnvelopeV2 { block_value: fees, execution_payload: block.into() } + } +} + +impl From for ExecutionPayloadEnvelopeV3 { fn from(value: BuiltPayload) -> Self { let BuiltPayload { block, fees, sidecars, .. } = value; - ExecutionPayloadEnvelope { + ExecutionPayloadEnvelopeV3 { + payload_inner: block.into(), block_value: fees, - payload: block.into(), // From the engine API spec: // // > Client software **MAY** use any heuristics to decide whether to set @@ -102,8 +108,8 @@ impl From for ExecutionPayloadEnvelope { // // Spec: // - should_override_builder: Some(false), - blobs_bundle: Some(sidecars.into()), + should_override_builder: false, + blobs_bundle: sidecars.into(), } } } diff --git a/crates/rpc/rpc-api/src/engine.rs b/crates/rpc/rpc-api/src/engine.rs index 9905957e91..0fc169e626 100644 --- a/crates/rpc/rpc-api/src/engine.rs +++ b/crates/rpc/rpc-api/src/engine.rs @@ -2,8 +2,9 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use reth_primitives::{Address, BlockHash, BlockId, BlockNumberOrTag, Bytes, H256, U256, U64}; use reth_rpc_types::{ engine::{ - ExecutionPayload, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelope, ForkchoiceState, - ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, TransitionConfiguration, + ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, + ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, + PayloadAttributes, PayloadId, PayloadStatus, TransitionConfiguration, }, state::StateOverride, BlockOverrides, CallRequest, Filter, Log, RichBlock, SyncStatus, @@ -15,11 +16,11 @@ pub trait EngineApi { /// See also /// Caution: This should not accept the `withdrawals` field #[method(name = "newPayloadV1")] - async fn new_payload_v1(&self, payload: ExecutionPayload) -> RpcResult; + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> RpcResult; /// See also #[method(name = "newPayloadV2")] - async fn new_payload_v2(&self, payload: ExecutionPayload) -> RpcResult; + async fn new_payload_v2(&self, payload: ExecutionPayloadV1) -> RpcResult; /// Post Cancun payload handler /// @@ -27,7 +28,7 @@ pub trait EngineApi { #[method(name = "newPayloadV3")] async fn new_payload_v3( &self, - payload: ExecutionPayload, + payload: ExecutionPayloadV3, versioned_hashes: Vec, parent_beacon_block_root: H256, ) -> RpcResult; @@ -70,7 +71,7 @@ pub trait EngineApi { /// Note: /// > Provider software MAY stop the corresponding build process after serving this call. #[method(name = "getPayloadV1")] - async fn get_payload_v1(&self, payload_id: PayloadId) -> RpcResult; + async fn get_payload_v1(&self, payload_id: PayloadId) -> RpcResult; /// See also /// @@ -78,7 +79,7 @@ pub trait EngineApi { /// payload build process at the time of receiving this call. Note: /// > Provider software MAY stop the corresponding build process after serving this call. #[method(name = "getPayloadV2")] - async fn get_payload_v2(&self, payload_id: PayloadId) -> RpcResult; + async fn get_payload_v2(&self, payload_id: PayloadId) -> RpcResult; /// Post Cancun payload handler which also returns a blobs bundle. /// @@ -88,7 +89,7 @@ pub trait EngineApi { /// payload build process at the time of receiving this call. Note: /// > Provider software MAY stop the corresponding build process after serving this call. #[method(name = "getPayloadV3")] - async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult; + async fn get_payload_v3(&self, payload_id: PayloadId) -> RpcResult; /// See also #[method(name = "getPayloadBodiesByHashV1")] diff --git a/crates/rpc/rpc-engine-api/src/engine_api.rs b/crates/rpc/rpc-engine-api/src/engine_api.rs index 6a9afef436..cea1003ed0 100644 --- a/crates/rpc/rpc-engine-api/src/engine_api.rs +++ b/crates/rpc/rpc-engine-api/src/engine_api.rs @@ -10,7 +10,8 @@ use reth_primitives::{BlockHash, BlockHashOrNumber, BlockNumber, ChainSpec, Hard use reth_provider::{BlockReader, EvmEnvProvider, HeaderProvider, StateProviderFactory}; use reth_rpc_api::EngineApiServer; use reth_rpc_types::engine::{ - ExecutionPayload, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelope, ForkchoiceUpdated, + ExecutionPayload, ExecutionPayloadBodiesV1, ExecutionPayloadEnvelopeV2, + ExecutionPayloadEnvelopeV3, ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, TransitionConfiguration, CAPABILITIES, }; use reth_tasks::TaskSpawner; @@ -69,38 +70,36 @@ where /// Caution: This should not accept the `withdrawals` field pub async fn new_payload_v1( &self, - payload: ExecutionPayload, + payload: ExecutionPayloadV1, ) -> EngineApiResult { - self.validate_version_specific_fields( - EngineApiMessageVersion::V1, - PayloadOrAttributes::from_execution_payload(&payload, None), - )?; + let payload = ExecutionPayload::from(payload); + let payload_or_attrs = PayloadOrAttributes::from_execution_payload(&payload, None); + self.validate_version_specific_fields(EngineApiMessageVersion::V1, &payload_or_attrs)?; Ok(self.inner.beacon_consensus.new_payload(payload, None).await?) } /// See also pub async fn new_payload_v2( &self, - payload: ExecutionPayload, + payload: ExecutionPayloadV1, ) -> EngineApiResult { - self.validate_version_specific_fields( - EngineApiMessageVersion::V2, - PayloadOrAttributes::from_execution_payload(&payload, None), - )?; + let payload = ExecutionPayload::from(payload); + let payload_or_attrs = PayloadOrAttributes::from_execution_payload(&payload, None); + self.validate_version_specific_fields(EngineApiMessageVersion::V2, &payload_or_attrs)?; Ok(self.inner.beacon_consensus.new_payload(payload, None).await?) } /// See also pub async fn new_payload_v3( &self, - payload: ExecutionPayload, + payload: ExecutionPayloadV1, _versioned_hashes: Vec, parent_beacon_block_root: H256, ) -> EngineApiResult { - self.validate_version_specific_fields( - EngineApiMessageVersion::V3, - PayloadOrAttributes::from_execution_payload(&payload, Some(parent_beacon_block_root)), - )?; + let payload = ExecutionPayload::from(payload); + let payload_or_attrs = + PayloadOrAttributes::from_execution_payload(&payload, Some(parent_beacon_block_root)); + self.validate_version_specific_fields(EngineApiMessageVersion::V3, &payload_or_attrs)?; // TODO: validate versioned hashes and figure out what to do with parent_beacon_block_root Ok(self.inner.beacon_consensus.new_payload(payload, Some(parent_beacon_block_root)).await?) @@ -118,7 +117,7 @@ where payload_attrs: Option, ) -> EngineApiResult { if let Some(ref attrs) = payload_attrs { - self.validate_version_specific_fields(EngineApiMessageVersion::V1, attrs.into())?; + self.validate_version_specific_fields(EngineApiMessageVersion::V1, &attrs.into())?; } Ok(self.inner.beacon_consensus.fork_choice_updated(state, payload_attrs).await?) } @@ -133,7 +132,7 @@ where payload_attrs: Option, ) -> EngineApiResult { if let Some(ref attrs) = payload_attrs { - self.validate_version_specific_fields(EngineApiMessageVersion::V2, attrs.into())?; + self.validate_version_specific_fields(EngineApiMessageVersion::V2, &attrs.into())?; } Ok(self.inner.beacon_consensus.fork_choice_updated(state, payload_attrs).await?) } @@ -148,7 +147,7 @@ where payload_attrs: Option, ) -> EngineApiResult { if let Some(ref attrs) = payload_attrs { - self.validate_version_specific_fields(EngineApiMessageVersion::V3, attrs.into())?; + self.validate_version_specific_fields(EngineApiMessageVersion::V3, &attrs.into())?; } Ok(self.inner.beacon_consensus.fork_choice_updated(state, payload_attrs).await?) @@ -163,7 +162,10 @@ where /// /// Note: /// > Provider software MAY stop the corresponding build process after serving this call. - pub async fn get_payload_v1(&self, payload_id: PayloadId) -> EngineApiResult { + pub async fn get_payload_v1( + &self, + payload_id: PayloadId, + ) -> EngineApiResult { Ok(self .inner .payload_store @@ -183,7 +185,7 @@ where pub async fn get_payload_v2( &self, payload_id: PayloadId, - ) -> EngineApiResult { + ) -> EngineApiResult { Ok(self .inner .payload_store @@ -203,7 +205,7 @@ where pub async fn get_payload_v3( &self, payload_id: PayloadId, - ) -> EngineApiResult { + ) -> EngineApiResult { Ok(self .inner .payload_store @@ -428,7 +430,7 @@ where fn validate_version_specific_fields( &self, version: EngineApiMessageVersion, - payload_or_attrs: PayloadOrAttributes<'_>, + payload_or_attrs: &PayloadOrAttributes<'_>, ) -> EngineApiResult<()> { self.validate_withdrawals_presence( version, @@ -451,14 +453,14 @@ where /// Handler for `engine_newPayloadV1` /// See also /// Caution: This should not accept the `withdrawals` field - async fn new_payload_v1(&self, payload: ExecutionPayload) -> RpcResult { + async fn new_payload_v1(&self, payload: ExecutionPayloadV1) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_newPayloadV1"); Ok(EngineApi::new_payload_v1(self, payload).await?) } /// Handler for `engine_newPayloadV2` /// See also - async fn new_payload_v2(&self, payload: ExecutionPayload) -> RpcResult { + async fn new_payload_v2(&self, payload: ExecutionPayloadV1) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_newPayloadV2"); Ok(EngineApi::new_payload_v2(self, payload).await?) } @@ -467,7 +469,7 @@ where /// See also async fn new_payload_v3( &self, - _payload: ExecutionPayload, + _payload: ExecutionPayloadV3, _versioned_hashes: Vec, _parent_beacon_block_root: H256, ) -> RpcResult { @@ -520,7 +522,7 @@ where /// /// Note: /// > Provider software MAY stop the corresponding build process after serving this call. - async fn get_payload_v1(&self, payload_id: PayloadId) -> RpcResult { + async fn get_payload_v1(&self, payload_id: PayloadId) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_getPayloadV1"); Ok(EngineApi::get_payload_v1(self, payload_id).await?) } @@ -534,7 +536,7 @@ where /// /// Note: /// > Provider software MAY stop the corresponding build process after serving this call. - async fn get_payload_v2(&self, payload_id: PayloadId) -> RpcResult { + async fn get_payload_v2(&self, payload_id: PayloadId) -> RpcResult { trace!(target: "rpc::engine", "Serving engine_getPayloadV2"); Ok(EngineApi::get_payload_v2(self, payload_id).await?) } @@ -548,7 +550,10 @@ where /// /// Note: /// > Provider software MAY stop the corresponding build process after serving this call. - async fn get_payload_v3(&self, _payload_id: PayloadId) -> RpcResult { + async fn get_payload_v3( + &self, + _payload_id: PayloadId, + ) -> RpcResult { Err(jsonrpsee_types::error::ErrorCode::MethodNotFound.into()) } diff --git a/crates/rpc/rpc-engine-api/src/payload.rs b/crates/rpc/rpc-engine-api/src/payload.rs index 95db05a3f7..f738f6ef26 100644 --- a/crates/rpc/rpc-engine-api/src/payload.rs +++ b/crates/rpc/rpc-engine-api/src/payload.rs @@ -25,17 +25,17 @@ impl<'a> PayloadOrAttributes<'a> { } /// Return the withdrawals for the payload or attributes. - pub(crate) fn withdrawals(&self) -> &Option> { + pub(crate) fn withdrawals(&self) -> Option<&Vec> { match self { - Self::ExecutionPayload { payload, .. } => &payload.withdrawals, - Self::PayloadAttributes(attributes) => &attributes.withdrawals, + Self::ExecutionPayload { payload, .. } => payload.withdrawals(), + Self::PayloadAttributes(attributes) => attributes.withdrawals.as_ref(), } } /// Return the timestamp for the payload or attributes. pub(crate) fn timestamp(&self) -> u64 { match self { - Self::ExecutionPayload { payload, .. } => payload.timestamp.as_u64(), + Self::ExecutionPayload { payload, .. } => payload.timestamp(), Self::PayloadAttributes(attributes) => attributes.timestamp.as_u64(), } } diff --git a/crates/rpc/rpc-engine-api/tests/it/payload.rs b/crates/rpc/rpc-engine-api/tests/it/payload.rs index a39b59eea5..1dcac07025 100644 --- a/crates/rpc/rpc-engine-api/tests/it/payload.rs +++ b/crates/rpc/rpc-engine-api/tests/it/payload.rs @@ -10,7 +10,9 @@ use reth_primitives::{ Block, SealedBlock, TransactionSigned, H256, U256, }; use reth_rlp::{Decodable, DecodeError}; -use reth_rpc_types::engine::{ExecutionPayload, ExecutionPayloadBodyV1, PayloadError}; +use reth_rpc_types::engine::{ + ExecutionPayload, ExecutionPayloadBodyV1, ExecutionPayloadV1, PayloadError, +}; fn transform_block Block>(src: SealedBlock, f: F) -> ExecutionPayload { let unsealed = src.unseal(); @@ -81,12 +83,13 @@ fn payload_validation() { ); // Invalid encoded transactions - let mut payload_with_invalid_txs: ExecutionPayload = block.clone().into(); + let mut payload_with_invalid_txs: ExecutionPayloadV1 = block.clone().into(); payload_with_invalid_txs.transactions.iter_mut().for_each(|tx| { *tx = Bytes::new().into(); }); + let payload_with_invalid_txs = Block::try_from(payload_with_invalid_txs); assert_matches!( - payload_with_invalid_txs.try_into_sealed_block(None), + payload_with_invalid_txs, Err(PayloadError::Decode(DecodeError::InputTooShort)) ); @@ -98,7 +101,7 @@ fn payload_validation() { assert_matches!( block_with_ommers.clone().try_into_sealed_block(None), Err(PayloadError::BlockHash { consensus, .. }) - if consensus == block_with_ommers.block_hash + if consensus == block_with_ommers.block_hash() ); // None zero difficulty @@ -108,7 +111,7 @@ fn payload_validation() { }); assert_matches!( block_with_difficulty.clone().try_into_sealed_block(None), - Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_difficulty.block_hash + Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_difficulty.block_hash() ); // None zero nonce @@ -118,7 +121,7 @@ fn payload_validation() { }); assert_matches!( block_with_nonce.clone().try_into_sealed_block(None), - Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_nonce.block_hash + Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_nonce.block_hash() ); // Valid block diff --git a/crates/rpc/rpc-types/src/eth/engine/payload.rs b/crates/rpc/rpc-types/src/eth/engine/payload.rs index fa8cae1842..7749ce41b4 100644 --- a/crates/rpc/rpc-types/src/eth/engine/payload.rs +++ b/crates/rpc/rpc-types/src/eth/engine/payload.rs @@ -30,14 +30,55 @@ impl std::fmt::Display for PayloadId { } } +/// This represents the `executionPayload` field in the return value of `engine_getPayloadV2`, +/// specified as: +/// +/// - `executionPayload`: `ExecutionPayloadV1` | `ExecutionPayloadV2` where: +/// - `ExecutionPayloadV1` **MUST** be returned if the payload `timestamp` is lower than the +/// Shanghai timestamp +/// - `ExecutionPayloadV2` **MUST** be returned if the payload `timestamp` is greater or equal +/// to the Shanghai timestamp +/// +/// See: +/// +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ExecutionPayloadFieldV2 { + /// V1 payload + V1(ExecutionPayloadV1), + /// V2 payload + V2(ExecutionPayloadV2), +} + +impl ExecutionPayloadFieldV2 { + /// Returns the inner [ExecutionPayloadV1] + pub fn into_v1_payload(self) -> ExecutionPayloadV1 { + match self { + ExecutionPayloadFieldV2::V1(payload) => payload, + ExecutionPayloadFieldV2::V2(payload) => payload.payload_inner, + } + } +} + +impl From for ExecutionPayloadFieldV2 { + fn from(value: SealedBlock) -> Self { + // if there are withdrawals, return V2 + if value.withdrawals.is_some() { + ExecutionPayloadFieldV2::V2(value.into()) + } else { + ExecutionPayloadFieldV2::V1(value.into()) + } + } +} + /// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for -/// both V2 and V3. +/// V2. /// /// See also: /// -/// #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ExecutionPayloadEnvelope { +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayloadEnvelopeV2 { /// Execution payload, which could be either V1 or V2 /// /// V1 (_NO_ withdrawals) MUST be returned if the payload timestamp is lower than the Shanghai @@ -45,35 +86,44 @@ pub struct ExecutionPayloadEnvelope { /// /// V2 (_WITH_ withdrawals) MUST be returned if the payload timestamp is greater or equal to /// the Shanghai timestamp - #[serde(rename = "executionPayload")] - pub payload: ExecutionPayload, + pub execution_payload: ExecutionPayloadFieldV2, /// The expected value to be received by the feeRecipient in wei - #[serde(rename = "blockValue")] pub block_value: U256, - /// The blobs, commitments, and proofs associated with the executed payload. - #[serde(rename = "blobsBundle", skip_serializing_if = "Option::is_none")] - pub blobs_bundle: Option, - /// Introduced in V3, this represents a suggestion from the execution layer if the payload - /// should be used instead of an externally provided one. - #[serde(rename = "shouldOverrideBuilder", skip_serializing_if = "Option::is_none")] - pub should_override_builder: Option, } -impl ExecutionPayloadEnvelope { +impl ExecutionPayloadEnvelopeV2 { /// Returns the [ExecutionPayload] for the `engine_getPayloadV1` endpoint - pub fn into_v1_payload(mut self) -> ExecutionPayload { - // ensure withdrawals are removed - self.payload.withdrawals.take(); - self.payload + pub fn into_v1_payload(self) -> ExecutionPayloadV1 { + self.execution_payload.into_v1_payload() } } +/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for +/// V3. +/// +/// See also: +/// +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayloadEnvelopeV3 { + /// Execution payload V3 + #[serde(flatten)] + pub payload_inner: ExecutionPayloadV3, + /// The expected value to be received by the feeRecipient in wei + pub block_value: U256, + /// The blobs, commitments, and proofs associated with the executed payload. + pub blobs_bundle: BlobsBundleV1, + /// Introduced in V3, this represents a suggestion from the execution layer if the payload + /// should be used instead of an externally provided one. + pub should_override_builder: bool, +} + /// This structure maps on the ExecutionPayload structure of the beacon chain spec. /// /// See also: #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ExecutionPayload { +pub struct ExecutionPayloadV1 { pub parent_hash: H256, pub fee_recipient: Address, pub state_root: H256, @@ -88,21 +138,9 @@ pub struct ExecutionPayload { pub base_fee_per_gas: U256, pub block_hash: H256, pub transactions: Vec, - /// Array of [`Withdrawal`] enabled with V2 - /// See - #[serde(default, skip_serializing_if = "Option::is_none")] - pub withdrawals: Option>, - /// Array of [`U64`] representing blob gas used, enabled with V3 - /// See - #[serde(default, skip_serializing_if = "Option::is_none")] - pub blob_gas_used: Option, - /// Array of [`U64`] representing excess blob gas, enabled with V3 - /// See - #[serde(default, skip_serializing_if = "Option::is_none")] - pub excess_blob_gas: Option, } -impl From for ExecutionPayload { +impl From for ExecutionPayloadV1 { fn from(value: SealedBlock) -> Self { let transactions = value .body @@ -113,7 +151,7 @@ impl From for ExecutionPayload { encoded.into() }) .collect(); - ExecutionPayload { + ExecutionPayloadV1 { parent_hash: value.parent_hash, fee_recipient: value.beneficiary, state_root: value.state_root, @@ -126,88 +164,226 @@ impl From for ExecutionPayload { timestamp: value.timestamp.into(), extra_data: value.extra_data.clone(), base_fee_per_gas: U256::from(value.base_fee_per_gas.unwrap_or_default()), - blob_gas_used: value.blob_gas_used.map(U64::from), - excess_blob_gas: value.excess_blob_gas.map(U64::from), block_hash: value.hash(), transactions, - withdrawals: value.withdrawals, } } } -impl ExecutionPayload { - /// Tries to create a new block from the given payload and optional parent beacon block root. - /// Perform additional validation of `extra_data` and `base_fee_per_gas` fields. - /// - /// NOTE: The log bloom is assumed to be validated during serialization. - /// NOTE: Empty ommers, nonce and difficulty values are validated upon computing block hash and - /// comparing the value with `payload.block_hash`. - /// - /// See - pub fn try_into_sealed_block( - self, - parent_beacon_block_root: Option, - ) -> Result { - if self.extra_data.len() > MAXIMUM_EXTRA_DATA_SIZE { - return Err(PayloadError::ExtraData(self.extra_data)) +/// Try to construct a block from given payload. Perform addition validation of `extra_data` and +/// `base_fee_per_gas` fields. +/// +/// NOTE: The log bloom is assumed to be validated during serialization. +/// NOTE: Empty ommers, nonce and difficulty values are validated upon computing block hash and +/// comparing the value with `payload.block_hash`. +/// +/// See +impl TryFrom for Block { + type Error = PayloadError; + + fn try_from(payload: ExecutionPayloadV1) -> Result { + if payload.extra_data.len() > MAXIMUM_EXTRA_DATA_SIZE { + return Err(PayloadError::ExtraData(payload.extra_data)) } - if self.base_fee_per_gas < MIN_PROTOCOL_BASE_FEE_U256 { - return Err(PayloadError::BaseFee(self.base_fee_per_gas)) + if payload.base_fee_per_gas < MIN_PROTOCOL_BASE_FEE_U256 { + return Err(PayloadError::BaseFee(payload.base_fee_per_gas)) } - let transactions = self + let transactions = payload .transactions .iter() .map(|tx| TransactionSigned::decode(&mut tx.as_ref())) .collect::, _>>()?; let transactions_root = proofs::calculate_transaction_root(&transactions); - let withdrawals_root = - self.withdrawals.as_ref().map(|w| proofs::calculate_withdrawals_root(w)); - let header = Header { - parent_hash: self.parent_hash, - beneficiary: self.fee_recipient, - state_root: self.state_root, + parent_hash: payload.parent_hash, + beneficiary: payload.fee_recipient, + state_root: payload.state_root, transactions_root, - receipts_root: self.receipts_root, - withdrawals_root, - parent_beacon_block_root, - logs_bloom: self.logs_bloom, - number: self.block_number.as_u64(), - gas_limit: self.gas_limit.as_u64(), - gas_used: self.gas_used.as_u64(), - timestamp: self.timestamp.as_u64(), - mix_hash: self.prev_randao, + receipts_root: payload.receipts_root, + withdrawals_root: None, + logs_bloom: payload.logs_bloom, + number: payload.block_number.as_u64(), + gas_limit: payload.gas_limit.as_u64(), + gas_used: payload.gas_used.as_u64(), + timestamp: payload.timestamp.as_u64(), + mix_hash: payload.prev_randao, base_fee_per_gas: Some( - self.base_fee_per_gas + payload + .base_fee_per_gas .uint_try_to() - .map_err(|_| PayloadError::BaseFee(self.base_fee_per_gas))?, + .map_err(|_| PayloadError::BaseFee(payload.base_fee_per_gas))?, ), - blob_gas_used: self.blob_gas_used.map(|blob_gas_used| blob_gas_used.as_u64()), - excess_blob_gas: self.excess_blob_gas.map(|excess_blob_gas| excess_blob_gas.as_u64()), - extra_data: self.extra_data, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + extra_data: payload.extra_data, // Defaults ommers_hash: EMPTY_LIST_HASH, difficulty: Default::default(), nonce: Default::default(), - } - .seal_slow(); + }; - if self.block_hash != header.hash() { - return Err(PayloadError::BlockHash { - execution: header.hash(), - consensus: self.block_hash, + Ok(Block { header, body: transactions, withdrawals: None, ommers: Default::default() }) + } +} + +/// This structure maps on the ExecutionPayloadV2 structure of the beacon chain spec. +/// +/// See also: +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayloadV2 { + /// Inner V1 payload + #[serde(flatten)] + pub payload_inner: ExecutionPayloadV1, + + /// Array of [`Withdrawal`] enabled with V2 + /// See + pub withdrawals: Vec, +} + +impl ExecutionPayloadV2 { + /// Returns the timestamp for the execution payload. + pub fn timestamp(&self) -> u64 { + self.payload_inner.timestamp.as_u64() + } +} + +impl From for ExecutionPayloadV2 { + fn from(value: SealedBlock) -> Self { + let transactions = value + .body + .iter() + .map(|tx| { + let mut encoded = Vec::new(); + tx.encode_enveloped(&mut encoded); + encoded.into() }) - } + .collect(); - Ok(SealedBlock { - header, - body: transactions, - withdrawals: self.withdrawals, - ommers: Default::default(), - }) + ExecutionPayloadV2 { + payload_inner: ExecutionPayloadV1 { + parent_hash: value.parent_hash, + fee_recipient: value.beneficiary, + state_root: value.state_root, + receipts_root: value.receipts_root, + logs_bloom: value.logs_bloom, + prev_randao: value.mix_hash, + block_number: value.number.into(), + gas_limit: value.gas_limit.into(), + gas_used: value.gas_used.into(), + timestamp: value.timestamp.into(), + extra_data: value.extra_data.clone(), + base_fee_per_gas: U256::from(value.base_fee_per_gas.unwrap_or_default()), + block_hash: value.hash(), + transactions, + }, + withdrawals: value.withdrawals.unwrap_or_default(), + } + } +} + +impl TryFrom for Block { + type Error = PayloadError; + + fn try_from(payload: ExecutionPayloadV2) -> Result { + // this performs the same conversion as the underlying V1 payload, but calculates the + // withdrawals root and adds withdrawals + let mut base_sealed_block = Block::try_from(payload.payload_inner)?; + + let withdrawals_root = proofs::calculate_withdrawals_root(&payload.withdrawals); + base_sealed_block.withdrawals = Some(payload.withdrawals); + base_sealed_block.header.withdrawals_root = Some(withdrawals_root); + Ok(base_sealed_block) + } +} + +/// This structure maps on the ExecutionPayloadV3 structure of the beacon chain spec. +/// +/// See also: +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayloadV3 { + /// Inner V2 payload + #[serde(flatten)] + pub payload_inner: ExecutionPayloadV2, + + /// Array of [`U64`] representing blob gas used, enabled with V3 + /// See + pub blob_gas_used: U64, + /// Array of [`U64`] representing excess blob gas, enabled with V3 + /// See + pub excess_blob_gas: U64, +} + +impl ExecutionPayloadV3 { + /// Returns the withdrawals for the payload. + pub fn withdrawals(&self) -> &Vec { + &self.payload_inner.withdrawals + } + + /// Returns the timestamp for the payload. + pub fn timestamp(&self) -> u64 { + self.payload_inner.payload_inner.timestamp.as_u64() + } +} + +impl From for ExecutionPayloadV3 { + fn from(mut value: SealedBlock) -> Self { + let transactions = value + .body + .iter() + .map(|tx| { + let mut encoded = Vec::new(); + tx.encode_enveloped(&mut encoded); + encoded.into() + }) + .collect(); + + let withdrawals = value.withdrawals.take().unwrap_or_default(); + + ExecutionPayloadV3 { + payload_inner: ExecutionPayloadV2 { + payload_inner: ExecutionPayloadV1 { + parent_hash: value.parent_hash, + fee_recipient: value.beneficiary, + state_root: value.state_root, + receipts_root: value.receipts_root, + logs_bloom: value.logs_bloom, + prev_randao: value.mix_hash, + block_number: value.number.into(), + gas_limit: value.gas_limit.into(), + gas_used: value.gas_used.into(), + timestamp: value.timestamp.into(), + extra_data: value.extra_data.clone(), + base_fee_per_gas: U256::from(value.base_fee_per_gas.unwrap_or_default()), + block_hash: value.hash(), + transactions, + }, + withdrawals, + }, + + blob_gas_used: value.blob_gas_used.unwrap_or_default().into(), + excess_blob_gas: value.excess_blob_gas.unwrap_or_default().into(), + } + } +} + +impl TryFrom for Block { + type Error = PayloadError; + + fn try_from(payload: ExecutionPayloadV3) -> Result { + // this performs the same conversion as the underlying V2 payload, but inserts the blob gas + // used and excess blob gas + let mut base_block = Block::try_from(payload.payload_inner)?; + + base_block.header.blob_gas_used = Some(payload.blob_gas_used.as_u64()); + base_block.header.excess_blob_gas = Some(payload.excess_blob_gas.as_u64()); + + Ok(base_block) } } @@ -234,6 +410,130 @@ impl From> for BlobsBundleV1 { } } +/// An execution payload, which can be either [ExecutionPayloadV1], [ExecutionPayloadV2], or +/// [ExecutionPayloadV3]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ExecutionPayload { + /// V1 payload + V1(ExecutionPayloadV1), + /// V2 payload + V2(ExecutionPayloadV2), + /// V3 payload + V3(ExecutionPayloadV3), +} + +impl ExecutionPayload { + /// Returns the withdrawals for the payload. + pub fn withdrawals(&self) -> Option<&Vec> { + match self { + ExecutionPayload::V1(_) => None, + ExecutionPayload::V2(payload) => Some(&payload.withdrawals), + ExecutionPayload::V3(payload) => Some(payload.withdrawals()), + } + } + + /// Returns the timestamp for the payload. + pub fn timestamp(&self) -> u64 { + match self { + ExecutionPayload::V1(payload) => payload.timestamp.as_u64(), + ExecutionPayload::V2(payload) => payload.timestamp(), + ExecutionPayload::V3(payload) => payload.timestamp(), + } + } + + /// Returns the parent hash for the payload. + pub fn parent_hash(&self) -> H256 { + match self { + ExecutionPayload::V1(payload) => payload.parent_hash, + ExecutionPayload::V2(payload) => payload.payload_inner.parent_hash, + ExecutionPayload::V3(payload) => payload.payload_inner.payload_inner.parent_hash, + } + } + + /// Returns the block hash for the payload. + pub fn block_hash(&self) -> H256 { + match self { + ExecutionPayload::V1(payload) => payload.block_hash, + ExecutionPayload::V2(payload) => payload.payload_inner.block_hash, + ExecutionPayload::V3(payload) => payload.payload_inner.payload_inner.block_hash, + } + } + + /// Returns the block number for this payload. + pub fn block_number(&self) -> u64 { + match self { + ExecutionPayload::V1(payload) => payload.block_number.as_u64(), + ExecutionPayload::V2(payload) => payload.payload_inner.block_number.as_u64(), + ExecutionPayload::V3(payload) => { + payload.payload_inner.payload_inner.block_number.as_u64() + } + } + } + + /// Tries to create a new block from the given payload and optional parent beacon block root. + /// Perform additional validation of `extra_data` and `base_fee_per_gas` fields. + /// + /// NOTE: The log bloom is assumed to be validated during serialization. + /// NOTE: Empty ommers, nonce and difficulty values are validated upon computing block hash and + /// comparing the value with `payload.block_hash`. + /// + /// See + pub fn try_into_sealed_block( + self, + parent_beacon_block_root: Option, + ) -> Result { + let block_hash = self.block_hash(); + let mut base_payload = match self { + ExecutionPayload::V1(payload) => Block::try_from(payload)?, + ExecutionPayload::V2(payload) => Block::try_from(payload)?, + ExecutionPayload::V3(payload) => Block::try_from(payload)?, + }; + + base_payload.header.parent_beacon_block_root = parent_beacon_block_root; + + let payload = base_payload.seal_slow(); + + if block_hash != payload.hash() { + return Err(PayloadError::BlockHash { execution: payload.hash(), consensus: block_hash }) + } + + Ok(payload) + } +} + +impl From for ExecutionPayload { + fn from(payload: ExecutionPayloadV1) -> Self { + Self::V1(payload) + } +} + +impl From for ExecutionPayload { + fn from(payload: ExecutionPayloadV2) -> Self { + Self::V2(payload) + } +} + +impl From for ExecutionPayload { + fn from(payload: ExecutionPayloadV3) -> Self { + Self::V3(payload) + } +} + +impl From for ExecutionPayload { + fn from(block: SealedBlock) -> Self { + if block.header.parent_beacon_block_root.is_some() { + // block with parent beacon block root: V3 + Self::V3(block.into()) + } else if block.withdrawals.is_some() { + // block with withdrawals: V2 + Self::V2(block.into()) + } else { + // otherwise V1 + Self::V1(block.into()) + } + } +} + /// Error that can occur when handling payloads. #[derive(thiserror::Error, Debug)] pub enum PayloadError { @@ -555,18 +855,34 @@ mod tests { } #[test] - fn serde_roundtrip_legacy_txs_payload() { - // pulled from hive tests - modified with 4844 fields - let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x44bb4b98c59dbb726f96ffceb5ee028dcbe35b9bba4f9ffd56aeebf8d1e4db62","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0x5655011482546f16b2312ef18e9fad03d6a52b1be95401aea884b222477f9e64","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"],"blobGasUsed":"0xb10b","excessBlobGas":"0xb10b"}"#; - let payload: ExecutionPayload = serde_json::from_str(s).unwrap(); + fn serde_roundtrip_legacy_txs_payload_v1() { + // pulled from hive tests + let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x44bb4b98c59dbb726f96ffceb5ee028dcbe35b9bba4f9ffd56aeebf8d1e4db62","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0x5655011482546f16b2312ef18e9fad03d6a52b1be95401aea884b222477f9e64","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"]}"#; + let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap(); assert_eq!(serde_json::to_string(&payload).unwrap(), s); } #[test] - fn serde_roundtrip_enveloped_txs_payload() { + fn serde_roundtrip_legacy_txs_payload_v3() { // pulled from hive tests - modified with 4844 fields - let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x76a03cbcb7adce07fd284c61e4fa31e5e786175cefac54a29e46ec8efa28ea41","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x028111cb7d25918386a69656b3d17b2febe95fd0f11572c1a55c14f99fdfe3df","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0xa6f40ed042e61e88e76125dede8fff8026751ea14454b68fb534cea99f2b2a77","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"],"blobGasUsed":"0xb10b","excessBlobGas":"0xb10b"}"#; - let payload: ExecutionPayload = serde_json::from_str(s).unwrap(); + let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x44bb4b98c59dbb726f96ffceb5ee028dcbe35b9bba4f9ffd56aeebf8d1e4db62","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0x5655011482546f16b2312ef18e9fad03d6a52b1be95401aea884b222477f9e64","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"],"withdrawals":[],"blobGasUsed":"0xb10b","excessBlobGas":"0xb10b"}"#; + let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap(); + assert_eq!(serde_json::to_string(&payload).unwrap(), s); + } + + #[test] + fn serde_roundtrip_enveloped_txs_payload_v1() { + // pulled from hive tests + let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x76a03cbcb7adce07fd284c61e4fa31e5e786175cefac54a29e46ec8efa28ea41","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x028111cb7d25918386a69656b3d17b2febe95fd0f11572c1a55c14f99fdfe3df","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0xa6f40ed042e61e88e76125dede8fff8026751ea14454b68fb534cea99f2b2a77","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"]}"#; + let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap(); + assert_eq!(serde_json::to_string(&payload).unwrap(), s); + } + + #[test] + fn serde_roundtrip_enveloped_txs_payload_v3() { + // pulled from hive tests - modified with 4844 fields + let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x76a03cbcb7adce07fd284c61e4fa31e5e786175cefac54a29e46ec8efa28ea41","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x028111cb7d25918386a69656b3d17b2febe95fd0f11572c1a55c14f99fdfe3df","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0xa6f40ed042e61e88e76125dede8fff8026751ea14454b68fb534cea99f2b2a77","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"],"withdrawals":[],"blobGasUsed":"0xb10b","excessBlobGas":"0xb10b"}"#; + let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap(); assert_eq!(serde_json::to_string(&payload).unwrap(), s); } }