From 801294252ed1ba70d671e723fa771a4635aae3fd Mon Sep 17 00:00:00 2001 From: "Supernovahs.eth" <91280922+supernovahs@users.noreply.github.com> Date: Tue, 19 Sep 2023 22:27:32 +0530 Subject: [PATCH] feat: Duplicate Withdrawal and move try from impls to rpc-compat (#4186) --- Cargo.lock | 4 + crates/consensus/beacon/Cargo.toml | 2 +- crates/consensus/beacon/src/engine/mod.rs | 26 +- crates/payload/builder/Cargo.toml | 1 + crates/payload/builder/src/payload.rs | 26 +- crates/rpc/rpc-builder/Cargo.toml | 1 + crates/rpc/rpc-builder/tests/it/auth.rs | 8 +- crates/rpc/rpc-engine-api/Cargo.toml | 2 +- crates/rpc/rpc-engine-api/src/engine_api.rs | 20 +- crates/rpc/rpc-engine-api/src/payload.rs | 6 +- crates/rpc/rpc-engine-api/tests/it/payload.rs | 43 ++- crates/rpc/rpc-types-compat/Cargo.toml | 1 + crates/rpc/rpc-types-compat/src/engine/mod.rs | 6 + .../rpc-types-compat/src/engine/payload.rs | 328 ++++++++++++++++++ crates/rpc/rpc-types-compat/src/lib.rs | 2 + crates/rpc/rpc-types/Cargo.toml | 3 + crates/rpc/rpc-types/src/eth/engine/mod.rs | 3 +- .../rpc/rpc-types/src/eth/engine/payload.rs | 272 +-------------- crates/rpc/rpc-types/src/eth/mod.rs | 3 + crates/rpc/rpc-types/src/eth/withdrawal.rs | 40 +++ 20 files changed, 480 insertions(+), 317 deletions(-) create mode 100644 crates/rpc/rpc-types-compat/src/engine/mod.rs create mode 100644 crates/rpc/rpc-types-compat/src/engine/payload.rs create mode 100644 crates/rpc/rpc-types/src/eth/withdrawal.rs diff --git a/Cargo.lock b/Cargo.lock index 18ccf93cf..ee5ae3e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5339,6 +5339,7 @@ dependencies = [ "reth-prune", "reth-revm", "reth-rpc-types", + "reth-rpc-types-compat", "reth-stages", "reth-tasks", "reth-tracing", @@ -5795,6 +5796,7 @@ dependencies = [ "reth-revm-primitives", "reth-rlp", "reth-rpc-types", + "reth-rpc-types-compat", "reth-transaction-pool", "revm-primitives", "sha2", @@ -6056,6 +6058,7 @@ dependencies = [ "reth-rpc-api", "reth-rpc-engine-api", "reth-rpc-types", + "reth-rpc-types-compat", "reth-tasks", "reth-tracing", "reth-transaction-pool", @@ -6085,6 +6088,7 @@ dependencies = [ "reth-rlp", "reth-rpc-api", "reth-rpc-types", + "reth-rpc-types-compat", "reth-tasks", "thiserror", "tokio", diff --git a/crates/consensus/beacon/Cargo.toml b/crates/consensus/beacon/Cargo.toml index f44de6c55..d18018747 100644 --- a/crates/consensus/beacon/Cargo.toml +++ b/crates/consensus/beacon/Cargo.toml @@ -19,7 +19,7 @@ reth-rpc-types.workspace = true reth-tasks.workspace = true reth-payload-builder.workspace = true reth-prune = { path = "../../prune" } - +reth-rpc-types-compat.workspace = true # async tokio = { workspace = true, features = ["sync"] } tokio-stream.workspace = true diff --git a/crates/consensus/beacon/src/engine/mod.rs b/crates/consensus/beacon/src/engine/mod.rs index 2d6d12714..f47da4827 100644 --- a/crates/consensus/beacon/src/engine/mod.rs +++ b/crates/consensus/beacon/src/engine/mod.rs @@ -33,6 +33,7 @@ use reth_rpc_types::engine::{ CancunPayloadFields, ExecutionPayload, PayloadAttributes, PayloadError, PayloadStatus, PayloadStatusEnum, PayloadValidationError, }; +use reth_rpc_types_compat::engine::payload::try_into_sealed_block; use reth_stages::{ControlFlow, Pipeline, PipelineError}; use reth_tasks::TaskSpawner; use std::{ @@ -1137,7 +1138,8 @@ where cancun_fields: Option, ) -> Result { let parent_hash = payload.parent_hash(); - let block = match payload.try_into_sealed_block( + let block = match try_into_sealed_block( + payload, cancun_fields.as_ref().map(|fields| fields.parent_beacon_block_root), ) { Ok(block) => block, @@ -1833,9 +1835,8 @@ 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::{ - ExecutionPayloadV1, ForkchoiceState, ForkchoiceUpdated, PayloadStatus, - }; + use reth_rpc_types::engine::{ForkchoiceState, ForkchoiceUpdated, PayloadStatus}; + use reth_rpc_types_compat::engine::payload::try_block_to_payload_v1; use reth_stages::{ExecOutput, PipelineError, StageError}; use std::{collections::VecDeque, sync::Arc, time::Duration}; use tokio::sync::oneshot::error::TryRecvError; @@ -1895,7 +1896,8 @@ 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(ExecutionPayloadV1::from(SealedBlock::default()), None).await; + let _ = env.send_new_payload(try_block_to_payload_v1(SealedBlock::default()), None).await; + assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); // consensus engine is still idle because pruning is running @@ -2017,7 +2019,6 @@ mod tests { use reth_db::{tables, transaction::DbTxMut}; use reth_interfaces::test_utils::{generators, generators::random_block}; use reth_rpc_types::engine::ForkchoiceUpdateError; - #[tokio::test] async fn empty_head() { let chain_spec = Arc::new( @@ -2311,20 +2312,22 @@ mod tests { // Send new payload let res = env .send_new_payload( - ExecutionPayloadV1::from(random_block(&mut rng, 0, None, None, Some(0))), + try_block_to_payload_v1(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( - ExecutionPayloadV1::from(random_block(&mut rng, 1, None, None, Some(0))), + try_block_to_payload_v1(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)); @@ -2374,9 +2377,10 @@ mod tests { // Send new payload let result = env - .send_new_payload_retry_on_syncing(ExecutionPayloadV1::from(block2.clone()), None) + .send_new_payload_retry_on_syncing(try_block_to_payload_v1(block2.clone()), None) .await .unwrap(); + let expected_result = PayloadStatus::from_status(PayloadStatusEnum::Valid) .with_latest_valid_hash(block2.hash); assert_eq!(result, expected_result); @@ -2474,7 +2478,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(ExecutionPayloadV1::from(block), None).await; + let res = env.send_new_payload(try_block_to_payload_v1(block), None).await; let expected_result = PayloadStatus::from_status(PayloadStatusEnum::Syncing); assert_matches!(res, Ok(result) => assert_eq!(result, expected_result)); @@ -2537,7 +2541,7 @@ mod tests { // Send new payload let result = env - .send_new_payload_retry_on_syncing(ExecutionPayloadV1::from(block2.clone()), None) + .send_new_payload_retry_on_syncing(try_block_to_payload_v1(block2.clone()), None) .await .unwrap(); diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index 48a8a3ec6..b4dd6b1d7 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -16,6 +16,7 @@ reth-rlp.workspace = true reth-transaction-pool.workspace = true reth-interfaces.workspace = true reth-revm-primitives = { path = "../../revm/revm-primitives" } +reth-rpc-types-compat.workspace = true ## ethereum revm-primitives.workspace = true diff --git a/crates/payload/builder/src/payload.rs b/crates/payload/builder/src/payload.rs index cbc30a0d2..d63d694e5 100644 --- a/crates/payload/builder/src/payload.rs +++ b/crates/payload/builder/src/payload.rs @@ -9,8 +9,11 @@ use reth_rpc_types::engine::{ ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, ExecutionPayloadV1, PayloadAttributes, PayloadId, }; +use reth_rpc_types_compat::engine::payload::{ + convert_block_to_payload_field_v2, convert_standalonewithdraw_to_withdrawal, + try_block_to_payload_v1, try_block_to_payload_v3, +}; use revm_primitives::{BlockEnv, CfgEnv}; - /// Contains the built payload. /// /// According to the [engine API specification](https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md) the execution layer should build the initial version of the payload with an empty transaction set and then keep update it in order to maximize the revenue. @@ -76,7 +79,7 @@ impl BuiltPayload { // V1 engine_getPayloadV1 response impl From for ExecutionPayloadV1 { fn from(value: BuiltPayload) -> Self { - value.block.into() + try_block_to_payload_v1(value.block) } } @@ -85,7 +88,10 @@ impl From for ExecutionPayloadEnvelopeV2 { fn from(value: BuiltPayload) -> Self { let BuiltPayload { block, fees, .. } = value; - ExecutionPayloadEnvelopeV2 { block_value: fees, execution_payload: block.into() } + ExecutionPayloadEnvelopeV2 { + block_value: fees, + execution_payload: convert_block_to_payload_field_v2(block), + } } } @@ -94,7 +100,7 @@ impl From for ExecutionPayloadEnvelopeV3 { let BuiltPayload { block, fees, sidecars, .. } = value; ExecutionPayloadEnvelopeV3 { - execution_payload: block.into(), + execution_payload: try_block_to_payload_v3(block), block_value: fees, // From the engine API spec: // @@ -137,13 +143,23 @@ impl PayloadBuilderAttributes { /// Derives the unique [PayloadId] for the given parent and attributes pub fn new(parent: H256, attributes: PayloadAttributes) -> Self { let id = payload_id(&parent, &attributes); + + let withdraw = attributes.withdrawals.map( + |withdrawals: Vec| { + withdrawals + .into_iter() + .map(convert_standalonewithdraw_to_withdrawal) // Removed the parentheses here + .collect::>() + }, + ); + Self { id, parent, timestamp: attributes.timestamp.as_u64(), suggested_fee_recipient: attributes.suggested_fee_recipient, prev_randao: attributes.prev_randao, - withdrawals: attributes.withdrawals.unwrap_or_default(), + withdrawals: withdraw.unwrap_or_default(), parent_beacon_block_root: attributes.parent_beacon_block_root, } } diff --git a/crates/rpc/rpc-builder/Cargo.toml b/crates/rpc/rpc-builder/Cargo.toml index c9246376a..477717c50 100644 --- a/crates/rpc/rpc-builder/Cargo.toml +++ b/crates/rpc/rpc-builder/Cargo.toml @@ -21,6 +21,7 @@ reth-rpc-engine-api = { path = "../rpc-engine-api" } reth-rpc-types.workspace = true reth-tasks.workspace = true reth-transaction-pool.workspace = true +reth-rpc-types-compat.workspace = true # rpc/net jsonrpsee = { workspace = true, features = ["server"] } diff --git a/crates/rpc/rpc-builder/tests/it/auth.rs b/crates/rpc/rpc-builder/tests/it/auth.rs index 3e8063d73..fefa25024 100644 --- a/crates/rpc/rpc-builder/tests/it/auth.rs +++ b/crates/rpc/rpc-builder/tests/it/auth.rs @@ -6,15 +6,17 @@ use reth_primitives::Block; use reth_rpc::JwtSecret; use reth_rpc_api::clients::EngineApiClient; use reth_rpc_types::engine::{ForkchoiceState, PayloadId, TransitionConfiguration}; - +use reth_rpc_types_compat::engine::payload::{ + convert_block_to_payload_input_v2, try_block_to_payload_v1, +}; #[allow(unused_must_use)] async fn test_basic_engine_calls(client: &C) where C: ClientT + SubscriptionClientT + Sync, { let block = Block::default().seal_slow(); - EngineApiClient::new_payload_v1(client, block.clone().into()).await; - EngineApiClient::new_payload_v2(client, block.into()).await; + EngineApiClient::new_payload_v1(client, try_block_to_payload_v1(block.clone())).await; + EngineApiClient::new_payload_v2(client, convert_block_to_payload_input_v2(block)).await; EngineApiClient::fork_choice_updated_v1(client, ForkchoiceState::default(), None).await; EngineApiClient::get_payload_v1(client, PayloadId::new([0, 0, 0, 0, 0, 0, 0, 0])).await; EngineApiClient::get_payload_v2(client, PayloadId::new([0, 0, 0, 0, 0, 0, 0, 0])).await; diff --git a/crates/rpc/rpc-engine-api/Cargo.toml b/crates/rpc/rpc-engine-api/Cargo.toml index d08e7c8fa..7d8161c5d 100644 --- a/crates/rpc/rpc-engine-api/Cargo.toml +++ b/crates/rpc/rpc-engine-api/Cargo.toml @@ -18,7 +18,7 @@ reth-rpc-api = { path = "../rpc-api" } reth-beacon-consensus = { path = "../../consensus/beacon" } reth-payload-builder.workspace = true reth-tasks.workspace = true - +reth-rpc-types-compat.workspace = true # async tokio = { workspace = true, features = ["sync"] } diff --git a/crates/rpc/rpc-engine-api/src/engine_api.rs b/crates/rpc/rpc-engine-api/src/engine_api.rs index 8f05f5d4a..632676af7 100644 --- a/crates/rpc/rpc-engine-api/src/engine_api.rs +++ b/crates/rpc/rpc-engine-api/src/engine_api.rs @@ -15,6 +15,9 @@ use reth_rpc_types::engine::{ ForkchoiceUpdated, PayloadAttributes, PayloadId, PayloadStatus, TransitionConfiguration, CAPABILITIES, }; +use reth_rpc_types_compat::engine::payload::{ + convert_payload_input_v2_to_payload, convert_to_payload_body_v1, +}; use reth_tasks::TaskSpawner; use std::sync::Arc; use tokio::sync::oneshot; @@ -84,7 +87,7 @@ where &self, payload: ExecutionPayloadInputV2, ) -> EngineApiResult { - let payload = ExecutionPayload::from(payload); + let payload = convert_payload_input_v2_to_payload(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?) @@ -280,7 +283,7 @@ where let block_result = inner.provider.block(BlockHashOrNumber::Number(num)); match block_result { Ok(block) => { - result.push(block.map(Into::into)); + result.push(block.map(convert_to_payload_body_v1)); } Err(err) => { tx.send(Err(EngineApiError::Internal(Box::new(err)))).ok(); @@ -311,7 +314,7 @@ where .provider .block(BlockHashOrNumber::Hash(hash)) .map_err(|err| EngineApiError::Internal(Box::new(err)))?; - result.push(block.map(Into::into)); + result.push(block.map(convert_to_payload_body_v1)); } Ok(result) @@ -836,8 +839,11 @@ mod tests { random_block_range(&mut rng, start..=start + count - 1, H256::default(), 0..2); handle.provider.extend_blocks(blocks.iter().cloned().map(|b| (b.hash(), b.unseal()))); - let expected = - blocks.iter().cloned().map(|b| Some(b.unseal().into())).collect::>(); + let expected = blocks + .iter() + .cloned() + .map(|b| Some(convert_to_payload_body_v1(b.unseal()))) + .collect::>(); let res = api.get_payload_bodies_by_range(start, count).await.unwrap(); assert_eq!(res, expected); @@ -875,7 +881,7 @@ mod tests { if first_missing_range.contains(&b.number) { None } else { - Some(b.unseal().into()) + Some(convert_to_payload_body_v1(b.unseal())) } }) .collect::>(); @@ -894,7 +900,7 @@ mod tests { { None } else { - Some(b.unseal().into()) + Some(convert_to_payload_body_v1(b.unseal())) } }) .collect::>(); diff --git a/crates/rpc/rpc-engine-api/src/payload.rs b/crates/rpc/rpc-engine-api/src/payload.rs index f738f6ef2..0c9b1429a 100644 --- a/crates/rpc/rpc-engine-api/src/payload.rs +++ b/crates/rpc/rpc-engine-api/src/payload.rs @@ -1,6 +1,6 @@ -use reth_primitives::{Withdrawal, H256}; -use reth_rpc_types::engine::{ExecutionPayload, PayloadAttributes}; +use reth_primitives::H256; +use reth_rpc_types::engine::{ExecutionPayload, PayloadAttributes}; /// Either an [ExecutionPayload] or a [PayloadAttributes]. pub(crate) enum PayloadOrAttributes<'a> { /// An [ExecutionPayload] and optional parent beacon block root. @@ -25,7 +25,7 @@ impl<'a> PayloadOrAttributes<'a> { } /// Return the withdrawals for the payload or attributes. - pub(crate) fn withdrawals(&self) -> Option<&Vec> { + pub(crate) fn withdrawals(&self) -> Option<&Vec> { match self { Self::ExecutionPayload { payload, .. } => payload.withdrawals(), Self::PayloadAttributes(attributes) => attributes.withdrawals.as_ref(), diff --git a/crates/rpc/rpc-engine-api/tests/it/payload.rs b/crates/rpc/rpc-engine-api/tests/it/payload.rs index 1dcac0702..fdc19854b 100644 --- a/crates/rpc/rpc-engine-api/tests/it/payload.rs +++ b/crates/rpc/rpc-engine-api/tests/it/payload.rs @@ -13,6 +13,10 @@ use reth_rlp::{Decodable, DecodeError}; use reth_rpc_types::engine::{ ExecutionPayload, ExecutionPayloadBodyV1, ExecutionPayloadV1, PayloadError, }; +use reth_rpc_types_compat::engine::payload::{ + convert_standalonewithdraw_to_withdrawal, convert_to_payload_body_v1, try_block_to_payload, + try_block_to_payload_v1, try_into_sealed_block, try_payload_v1_to_block, +}; fn transform_block Block>(src: SealedBlock, f: F) -> ExecutionPayload { let unsealed = src.unseal(); @@ -20,13 +24,12 @@ fn transform_block Block>(src: SealedBlock, f: F) -> Executi // Recalculate roots transformed.header.transactions_root = proofs::calculate_transaction_root(&transformed.body); transformed.header.ommers_hash = proofs::calculate_ommers_root(&transformed.ommers); - SealedBlock { + try_block_to_payload(SealedBlock { header: transformed.header.seal_slow(), body: transformed.body, ommers: transformed.ommers, withdrawals: transformed.withdrawals, - } - .into() + }) } #[test] @@ -34,7 +37,7 @@ fn payload_body_roundtrip() { let mut rng = generators::rng(); for block in random_block_range(&mut rng, 0..=99, H256::default(), 0..2) { let unsealed = block.clone().unseal(); - let payload_body: ExecutionPayloadBodyV1 = unsealed.into(); + let payload_body: ExecutionPayloadBodyV1 = convert_to_payload_body_v1(unsealed); assert_eq!( Ok(block.body), @@ -44,8 +47,13 @@ fn payload_body_roundtrip() { .map(|x| TransactionSigned::decode(&mut &x[..])) .collect::, _>>(), ); - - assert_eq!(block.withdrawals, payload_body.withdrawals); + let withdraw = payload_body.withdrawals.map(|withdrawals| { + withdrawals + .into_iter() + .map(convert_standalonewithdraw_to_withdrawal) + .collect::>() + }); + assert_eq!(block.withdrawals, withdraw); } } @@ -59,7 +67,8 @@ fn payload_validation() { b.header.extra_data = BytesMut::zeroed(32).freeze().into(); b }); - assert_matches!(block_with_valid_extra_data.try_into_sealed_block(None), Ok(_)); + + assert_matches!(try_into_sealed_block(block_with_valid_extra_data, None), Ok(_)); // Invalid extra data let block_with_invalid_extra_data: Bytes = BytesMut::zeroed(33).freeze(); @@ -68,7 +77,8 @@ fn payload_validation() { b }); assert_matches!( - invalid_extra_data_block.try_into_sealed_block(None), + + try_into_sealed_block(invalid_extra_data_block,None), Err(PayloadError::ExtraData(data)) if data == block_with_invalid_extra_data ); @@ -78,16 +88,18 @@ fn payload_validation() { b }); assert_matches!( - block_with_zero_base_fee.try_into_sealed_block(None), + + try_into_sealed_block(block_with_zero_base_fee,None), Err(PayloadError::BaseFee(val)) if val == U256::ZERO ); // Invalid encoded transactions - let mut payload_with_invalid_txs: ExecutionPayloadV1 = block.clone().into(); + let mut payload_with_invalid_txs: ExecutionPayloadV1 = try_block_to_payload_v1(block.clone()); + 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); + let payload_with_invalid_txs = try_payload_v1_to_block(payload_with_invalid_txs); assert_matches!( payload_with_invalid_txs, Err(PayloadError::Decode(DecodeError::InputTooShort)) @@ -99,7 +111,8 @@ fn payload_validation() { b }); assert_matches!( - block_with_ommers.clone().try_into_sealed_block(None), + try_into_sealed_block(block_with_ommers.clone(),None), + Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_ommers.block_hash() ); @@ -110,8 +123,9 @@ fn payload_validation() { b }); assert_matches!( - block_with_difficulty.clone().try_into_sealed_block(None), + try_into_sealed_block(block_with_difficulty.clone(),None), Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_difficulty.block_hash() + ); // None zero nonce @@ -120,8 +134,9 @@ fn payload_validation() { b }); assert_matches!( - block_with_nonce.clone().try_into_sealed_block(None), + try_into_sealed_block(block_with_nonce.clone(),None), Err(PayloadError::BlockHash { consensus, .. }) if consensus == block_with_nonce.block_hash() + ); // Valid block diff --git a/crates/rpc/rpc-types-compat/Cargo.toml b/crates/rpc/rpc-types-compat/Cargo.toml index a7997e341..b6f203ef4 100644 --- a/crates/rpc/rpc-types-compat/Cargo.toml +++ b/crates/rpc/rpc-types-compat/Cargo.toml @@ -14,3 +14,4 @@ Compatibility layer for reth-primitives and ethereum RPC types reth-primitives.workspace = true reth-rpc-types.workspace = true reth-rlp.workspace = true + diff --git a/crates/rpc/rpc-types-compat/src/engine/mod.rs b/crates/rpc/rpc-types-compat/src/engine/mod.rs new file mode 100644 index 000000000..10e60327b --- /dev/null +++ b/crates/rpc/rpc-types-compat/src/engine/mod.rs @@ -0,0 +1,6 @@ +//! Standalone functions for engine specific rpc type conversions +pub mod payload; +pub use payload::{ + convert_standalonewithdraw_to_withdrawal, convert_withdrawal_to_standalonewithdraw, + try_block_to_payload_v1, try_into_sealed_block, try_payload_v1_to_block, +}; diff --git a/crates/rpc/rpc-types-compat/src/engine/payload.rs b/crates/rpc/rpc-types-compat/src/engine/payload.rs new file mode 100644 index 000000000..4158e9990 --- /dev/null +++ b/crates/rpc/rpc-types-compat/src/engine/payload.rs @@ -0,0 +1,328 @@ +//! Standalone Conversion Functions for Handling Different Versions of Execution Payloads in +//! Ethereum's Engine +use reth_primitives::{ + constants::{MAXIMUM_EXTRA_DATA_SIZE, MIN_PROTOCOL_BASE_FEE_U256}, + proofs::{self, EMPTY_LIST_HASH}, + Block, Header, SealedBlock, TransactionSigned, UintTryTo, Withdrawal, H256, U256, +}; +use reth_rlp::Decodable; +use reth_rpc_types::engine::{ + payload::{ExecutionPayloadBodyV1, ExecutionPayloadFieldV2, ExecutionPayloadInputV2}, + ExecutionPayload, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, PayloadError, +}; + +/// Converts [ExecutionPayloadV1] to [Block] +pub fn try_payload_v1_to_block(payload: ExecutionPayloadV1) -> Result { + if payload.extra_data.len() > MAXIMUM_EXTRA_DATA_SIZE { + return Err(PayloadError::ExtraData(payload.extra_data)) + } + + if payload.base_fee_per_gas < MIN_PROTOCOL_BASE_FEE_U256 { + return Err(PayloadError::BaseFee(payload.base_fee_per_gas)) + } + + let transactions = payload + .transactions + .iter() + .map(|tx| TransactionSigned::decode(&mut tx.as_ref())) + .collect::, _>>()?; + let transactions_root = proofs::calculate_transaction_root(&transactions); + + let header = Header { + parent_hash: payload.parent_hash, + beneficiary: payload.fee_recipient, + state_root: payload.state_root, + transactions_root, + 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( + payload + .base_fee_per_gas + .uint_try_to() + .map_err(|_| PayloadError::BaseFee(payload.base_fee_per_gas))?, + ), + 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(), + }; + + Ok(Block { header, body: transactions, withdrawals: None, ommers: Default::default() }) +} + +/// Converts [ExecutionPayloadV2] to [Block] +pub fn try_payload_v2_to_block(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 = try_payload_v1_to_block(payload.payload_inner)?; + let withdrawals: Vec<_> = payload + .withdrawals + .iter() + .map(|w| convert_standalonewithdraw_to_withdrawal(w.clone())) + .collect(); + let withdrawals_root = proofs::calculate_withdrawals_root(&withdrawals); + base_sealed_block.withdrawals = Some(withdrawals); + base_sealed_block.header.withdrawals_root = Some(withdrawals_root); + Ok(base_sealed_block) +} + +/// Converts [ExecutionPayloadV3] to [Block] +pub fn try_payload_v3_to_block(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 = try_payload_v2_to_block(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) +} + +/// Converts [SealedBlock] to [ExecutionPayload] +pub fn try_block_to_payload(value: SealedBlock) -> ExecutionPayload { + if value.header.parent_beacon_block_root.is_some() { + // block with parent beacon block root: V3 + ExecutionPayload::V3(try_block_to_payload_v3(value)) + } else if value.withdrawals.is_some() { + // block with withdrawals: V2 + ExecutionPayload::V2(try_block_to_payload_v2(value)) + } else { + // otherwise V1 + ExecutionPayload::V1(try_block_to_payload_v1(value)) + } +} + +/// Converts [SealedBlock] to [ExecutionPayloadV1] +pub fn try_block_to_payload_v1(value: SealedBlock) -> ExecutionPayloadV1 { + let transactions = value + .body + .iter() + .map(|tx| { + let mut encoded = Vec::new(); + tx.encode_enveloped(&mut encoded); + encoded.into() + }) + .collect(); + 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, + } +} + +/// Converts [SealedBlock] to [ExecutionPayloadV2] +pub fn try_block_to_payload_v2(value: SealedBlock) -> ExecutionPayloadV2 { + let transactions = value + .body + .iter() + .map(|tx| { + let mut encoded = Vec::new(); + tx.encode_enveloped(&mut encoded); + encoded.into() + }) + .collect(); + let standalone_withdrawals: Vec = value + .withdrawals + .clone() + .unwrap_or_default() + .into_iter() + .map(convert_withdrawal_to_standalonewithdraw) + .collect(); + + 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: standalone_withdrawals, + } +} + +/// Converts [SealedBlock] to [ExecutionPayloadV3] +pub fn try_block_to_payload_v3(value: SealedBlock) -> ExecutionPayloadV3 { + let transactions = value + .body + .iter() + .map(|tx| { + let mut encoded = Vec::new(); + tx.encode_enveloped(&mut encoded); + encoded.into() + }) + .collect(); + + let withdrawals: Vec = value + .withdrawals + .clone() + .unwrap_or_default() + .into_iter() + .map(convert_withdrawal_to_standalonewithdraw) + .collect(); + + 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(), + } +} + +/// Converts [SealedBlock] to [ExecutionPayloadFieldV2] +pub fn convert_block_to_payload_field_v2(value: SealedBlock) -> ExecutionPayloadFieldV2 { + // if there are withdrawals, return V2 + if value.withdrawals.is_some() { + ExecutionPayloadFieldV2::V2(try_block_to_payload_v2(value)) + } else { + ExecutionPayloadFieldV2::V1(try_block_to_payload_v1(value)) + } +} + +/// Converts [ExecutionPayloadFieldV2] to [ExecutionPayload] +pub fn convert_payload_field_v2_to_payload(value: ExecutionPayloadFieldV2) -> ExecutionPayload { + match value { + ExecutionPayloadFieldV2::V1(payload) => ExecutionPayload::V1(payload), + ExecutionPayloadFieldV2::V2(payload) => ExecutionPayload::V2(payload), + } +} + +/// Converts [ExecutionPayloadInputV2] to [ExecutionPayload] +pub fn convert_payload_input_v2_to_payload(value: ExecutionPayloadInputV2) -> ExecutionPayload { + match value.withdrawals { + Some(withdrawals) => ExecutionPayload::V2(ExecutionPayloadV2 { + payload_inner: value.execution_payload, + withdrawals, + }), + None => ExecutionPayload::V1(value.execution_payload), + } +} + +/// Converts [SealedBlock] to [ExecutionPayloadInputV2] +pub fn convert_block_to_payload_input_v2(value: SealedBlock) -> ExecutionPayloadInputV2 { + let withdraw = value.withdrawals.clone().map(|withdrawals| { + withdrawals.into_iter().map(convert_withdrawal_to_standalonewithdraw).collect::>() + }); + ExecutionPayloadInputV2 { + withdrawals: withdraw, + execution_payload: try_block_to_payload_v1(value), + } +} + +/// 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( + value: ExecutionPayload, + parent_beacon_block_root: Option, +) -> Result { + let block_hash = value.block_hash(); + let mut base_payload = match value { + ExecutionPayload::V1(payload) => try_payload_v1_to_block(payload)?, + ExecutionPayload::V2(payload) => try_payload_v2_to_block(payload)?, + ExecutionPayload::V3(payload) => try_payload_v3_to_block(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) +} + +/// Converts [Withdrawal] to [reth_rpc_types::engine::payload::Withdrawal] +pub fn convert_withdrawal_to_standalonewithdraw( + withdrawal: Withdrawal, +) -> reth_rpc_types::engine::payload::Withdrawal { + reth_rpc_types::engine::payload::Withdrawal { + index: withdrawal.index, + validator_index: withdrawal.validator_index, + address: withdrawal.address, + amount: withdrawal.amount, + } +} + +/// Converts [reth_rpc_types::engine::payload::Withdrawal] to [Withdrawal] +pub fn convert_standalonewithdraw_to_withdrawal( + standalone: reth_rpc_types::engine::payload::Withdrawal, +) -> Withdrawal { + Withdrawal { + index: standalone.index, + validator_index: standalone.validator_index, + address: standalone.address, + amount: standalone.amount, + } +} + +/// Converts [Block] to [ExecutionPayloadBodyV1] +pub fn convert_to_payload_body_v1(value: Block) -> ExecutionPayloadBodyV1 { + let transactions = value.body.into_iter().map(|tx| { + let mut out = Vec::new(); + tx.encode_enveloped(&mut out); + out.into() + }); + let withdraw: Option> = + value.withdrawals.map(|withdrawals| { + withdrawals + .into_iter() + .map(convert_withdrawal_to_standalonewithdraw) + .collect::>() + }); + ExecutionPayloadBodyV1 { transactions: transactions.collect(), withdrawals: withdraw } +} diff --git a/crates/rpc/rpc-types-compat/src/lib.rs b/crates/rpc/rpc-types-compat/src/lib.rs index b4628cfcb..3774a0837 100644 --- a/crates/rpc/rpc-types-compat/src/lib.rs +++ b/crates/rpc/rpc-types-compat/src/lib.rs @@ -19,3 +19,5 @@ pub mod block; pub use block::*; pub mod transaction; pub use transaction::*; +pub mod engine; +pub use engine::*; diff --git a/crates/rpc/rpc-types/Cargo.toml b/crates/rpc/rpc-types/Cargo.toml index 3cce6c1a8..3dbe236b1 100644 --- a/crates/rpc/rpc-types/Cargo.toml +++ b/crates/rpc/rpc-types/Cargo.toml @@ -23,6 +23,7 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true jsonrpsee-types = { workspace = true, optional = true } + [features] default = ["jsonrpsee-types"] @@ -30,3 +31,5 @@ default = ["jsonrpsee-types"] # misc rand.workspace = true similar-asserts = "1.4" + + diff --git a/crates/rpc/rpc-types/src/eth/engine/mod.rs b/crates/rpc/rpc-types/src/eth/engine/mod.rs index ad76be1e3..86df72fb5 100644 --- a/crates/rpc/rpc-types/src/eth/engine/mod.rs +++ b/crates/rpc/rpc-types/src/eth/engine/mod.rs @@ -4,9 +4,8 @@ mod cancun; mod forkchoice; -mod payload; +pub mod payload; mod transition; - pub use self::{cancun::*, forkchoice::*, payload::*, transition::*}; /// The list of all supported Engine capabilities available over the engine endpoint. diff --git a/crates/rpc/rpc-types/src/eth/engine/payload.rs b/crates/rpc/rpc-types/src/eth/engine/payload.rs index 9b6dcea07..b805b2945 100644 --- a/crates/rpc/rpc-types/src/eth/engine/payload.rs +++ b/crates/rpc/rpc-types/src/eth/engine/payload.rs @@ -1,11 +1,8 @@ +pub use crate::Withdrawal; use reth_primitives::{ - constants::{MAXIMUM_EXTRA_DATA_SIZE, MIN_PROTOCOL_BASE_FEE_U256}, kzg::{Blob, Bytes48}, - proofs::{self, EMPTY_LIST_HASH}, - Address, BlobTransactionSidecar, Block, Bloom, Bytes, Header, SealedBlock, TransactionSigned, - UintTryTo, Withdrawal, H256, H64, U256, U64, + Address, BlobTransactionSidecar, Bloom, Bytes, SealedBlock, H256, H64, U256, U64, }; -use reth_rlp::Decodable; use serde::{ser::SerializeMap, Deserialize, Serialize, Serializer}; /// The execution payload body response that allows for `null` values. @@ -60,26 +57,6 @@ impl ExecutionPayloadFieldV2 { } } -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()) - } - } -} - -impl From for ExecutionPayload { - fn from(value: ExecutionPayloadFieldV2) -> Self { - match value { - ExecutionPayloadFieldV2::V1(payload) => ExecutionPayload::V1(payload), - ExecutionPayloadFieldV2::V2(payload) => ExecutionPayload::V2(payload), - } - } -} - /// This is the input to `engine_newPayloadV2`, which may or may not have a withdrawals field. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -92,27 +69,6 @@ pub struct ExecutionPayloadInputV2 { pub withdrawals: Option>, } -impl From for ExecutionPayload { - fn from(value: ExecutionPayloadInputV2) -> Self { - match value.withdrawals { - Some(withdrawals) => ExecutionPayload::V2(ExecutionPayloadV2 { - payload_inner: value.execution_payload, - withdrawals, - }), - None => ExecutionPayload::V1(value.execution_payload), - } - } -} - -impl From for ExecutionPayloadInputV2 { - fn from(value: SealedBlock) -> Self { - ExecutionPayloadInputV2 { - withdrawals: value.withdrawals.clone(), - execution_payload: value.into(), - } - } -} - /// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for /// V2. /// @@ -211,66 +167,6 @@ impl From for ExecutionPayloadV1 { } } -/// 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 payload.base_fee_per_gas < MIN_PROTOCOL_BASE_FEE_U256 { - return Err(PayloadError::BaseFee(payload.base_fee_per_gas)) - } - - let transactions = payload - .transactions - .iter() - .map(|tx| TransactionSigned::decode(&mut tx.as_ref())) - .collect::, _>>()?; - let transactions_root = proofs::calculate_transaction_root(&transactions); - - let header = Header { - parent_hash: payload.parent_hash, - beneficiary: payload.fee_recipient, - state_root: payload.state_root, - transactions_root, - 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( - payload - .base_fee_per_gas - .uint_try_to() - .map_err(|_| PayloadError::BaseFee(payload.base_fee_per_gas))?, - ), - 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(), - }; - - Ok(Block { header, body: transactions, withdrawals: None, ommers: Default::default() }) - } -} - /// This structure maps on the ExecutionPayloadV2 structure of the beacon chain spec. /// /// See also: @@ -293,55 +189,6 @@ impl ExecutionPayloadV2 { } } -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(); - - 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: @@ -372,62 +219,6 @@ impl ExecutionPayloadV3 { } } -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) - } -} - /// This includes all bundled blob related data of an executed payload. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BlobsBundleV1 { @@ -510,36 +301,6 @@ 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 { - 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 { @@ -560,21 +321,6 @@ impl From for ExecutionPayload { } } -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 { @@ -626,20 +372,6 @@ pub struct ExecutionPayloadBodyV1 { pub withdrawals: Option>, } -impl From for ExecutionPayloadBodyV1 { - fn from(value: Block) -> Self { - let transactions = value.body.into_iter().map(|tx| { - let mut out = Vec::new(); - tx.encode_enveloped(&mut out); - out.into() - }); - ExecutionPayloadBodyV1 { - transactions: transactions.collect(), - withdrawals: value.withdrawals, - } - } -} - /// This structure contains the attributes required to initiate a payload build process in the /// context of an `engine_forkchoiceUpdated` call. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/rpc/rpc-types/src/eth/mod.rs b/crates/rpc/rpc-types/src/eth/mod.rs index ae249e197..81d19d07d 100644 --- a/crates/rpc/rpc-types/src/eth/mod.rs +++ b/crates/rpc/rpc-types/src/eth/mod.rs @@ -15,15 +15,18 @@ mod syncing; pub mod trace; mod transaction; pub mod txpool; +mod withdrawal; mod work; pub use account::*; pub use block::*; pub use call::{Bundle, CallInput, CallInputError, CallRequest, EthCallResponse, StateContext}; +pub use engine::{ExecutionPayload, PayloadError}; pub use fee::{FeeHistory, TxGasAndReward}; pub use filter::*; pub use index::Index; pub use log::Log; pub use syncing::*; pub use transaction::*; +pub use withdrawal::Withdrawal; pub use work::Work; diff --git a/crates/rpc/rpc-types/src/eth/withdrawal.rs b/crates/rpc/rpc-types/src/eth/withdrawal.rs new file mode 100644 index 000000000..41314ebb5 --- /dev/null +++ b/crates/rpc/rpc-types/src/eth/withdrawal.rs @@ -0,0 +1,40 @@ +use reth_primitives::{constants::GWEI_TO_WEI, serde_helper::u64_hex, Address, U256}; +use reth_rlp::RlpEncodable; +use serde::{Deserialize, Serialize}; +/// Withdrawal represents a validator withdrawal from the consensus layer. +#[derive(Debug, Clone, PartialEq, Eq, Default, Hash, RlpEncodable, Serialize, Deserialize)] +pub struct Withdrawal { + /// Monotonically increasing identifier issued by consensus layer. + #[serde(with = "u64_hex")] + pub index: u64, + /// Index of validator associated with withdrawal. + #[serde(with = "u64_hex", rename = "validatorIndex")] + pub validator_index: u64, + /// Target address for withdrawn ether. + pub address: Address, + /// Value of the withdrawal in gwei. + #[serde(with = "u64_hex")] + pub amount: u64, +} + +impl Withdrawal { + /// Return the withdrawal amount in wei. + pub fn amount_wei(&self) -> U256 { + U256::from(self.amount) * U256::from(GWEI_TO_WEI) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // + #[test] + fn test_withdrawal_serde_roundtrip() { + let input = r#"[{"index":"0x0","validatorIndex":"0x0","address":"0x0000000000000000000000000000000000001000","amount":"0x1"},{"index":"0x1","validatorIndex":"0x1","address":"0x0000000000000000000000000000000000001001","amount":"0x1"},{"index":"0x2","validatorIndex":"0x2","address":"0x0000000000000000000000000000000000001002","amount":"0x1"},{"index":"0x3","validatorIndex":"0x3","address":"0x0000000000000000000000000000000000001003","amount":"0x1"},{"index":"0x4","validatorIndex":"0x4","address":"0x0000000000000000000000000000000000001004","amount":"0x1"},{"index":"0x5","validatorIndex":"0x5","address":"0x0000000000000000000000000000000000001005","amount":"0x1"},{"index":"0x6","validatorIndex":"0x6","address":"0x0000000000000000000000000000000000001006","amount":"0x1"},{"index":"0x7","validatorIndex":"0x7","address":"0x0000000000000000000000000000000000001007","amount":"0x1"},{"index":"0x8","validatorIndex":"0x8","address":"0x0000000000000000000000000000000000001008","amount":"0x1"},{"index":"0x9","validatorIndex":"0x9","address":"0x0000000000000000000000000000000000001009","amount":"0x1"},{"index":"0xa","validatorIndex":"0xa","address":"0x000000000000000000000000000000000000100a","amount":"0x1"},{"index":"0xb","validatorIndex":"0xb","address":"0x000000000000000000000000000000000000100b","amount":"0x1"},{"index":"0xc","validatorIndex":"0xc","address":"0x000000000000000000000000000000000000100c","amount":"0x1"},{"index":"0xd","validatorIndex":"0xd","address":"0x000000000000000000000000000000000000100d","amount":"0x1"},{"index":"0xe","validatorIndex":"0xe","address":"0x000000000000000000000000000000000000100e","amount":"0x1"},{"index":"0xf","validatorIndex":"0xf","address":"0x000000000000000000000000000000000000100f","amount":"0x1"}]"#; + + let withdrawals: Vec = serde_json::from_str(input).unwrap(); + let s = serde_json::to_string(&withdrawals).unwrap(); + assert_eq!(input, s); + } +}