diff --git a/crates/eips/src/eip6110.rs b/crates/eips/src/eip6110.rs
index 7dee0fa09c4..256409d4699 100644
--- a/crates/eips/src/eip6110.rs
+++ b/crates/eips/src/eip6110.rs
@@ -1,8 +1,8 @@
-//! Contains Deposit request constants, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md).
+//! Contains Deposit request types, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md).
//!
//! See also [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110): Supply validator deposits on chain
-use alloy_primitives::{address, Address};
+use alloy_primitives::{address, Address, FixedBytes, B256};
/// Mainnet deposit contract address.
pub const MAINNET_DEPOSIT_CONTRACT_ADDRESS: Address =
@@ -10,3 +10,65 @@ pub const MAINNET_DEPOSIT_CONTRACT_ADDRESS: Address =
/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for deposit requests.
pub const DEPOSIT_REQUEST_TYPE: u8 = 0x00;
+
+/// The [EIP-6110 Consensus Specs](https://github.com/ethereum/consensus-specs/blob/2660af05390aa61f06142e1c6311a3a3c633f720/specs/_features/eip6110/beacon-chain.md#constants) defined maximum payload size.
+pub const MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: usize = 8192;
+
+/// This structure maps onto the deposit object from [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110).
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
+#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
+pub struct DepositRequest {
+ /// Validator public key
+ pub pubkey: FixedBytes<48>,
+ /// Withdrawal credentials
+ pub withdrawal_credentials: B256,
+ /// Amount of ether deposited in gwei
+ #[cfg_attr(feature = "serde", serde(with = "alloy_serde::displayfromstr"))]
+ pub amount: u64,
+ /// Deposit signature
+ pub signature: FixedBytes<96>,
+ /// Deposit index
+ #[cfg_attr(feature = "serde", serde(with = "alloy_serde::displayfromstr"))]
+ pub index: u64,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloy_primitives::hex;
+
+ #[test]
+ #[cfg(feature = "serde")]
+ fn test_serde_deposit_request() {
+ // Sample JSON input representing a deposit request
+ let json_data = r#"{"pubkey":"0x8e01a8f21bdc38991ada53ca86d6c78d874675a450a38431cc6aa0f12d5661e344784c56c8a211f7025224d1303ee801","withdrawal_credentials":"0x010000000000000000000000af6df504f08ddf582d604d2f0a593bc153c25dbd","amount":"18112749083033600","signature":"0xb65f3db79405544528d6d92040282f29171f4ff6e5abb2d59f9ee1f1254aced2a7000f87bc2684f543e913a7cc1007ea0e97289b349c553eecdf253cd3ef5814088ba3d4ac286f2634dac3d026d9a01e4c166dc75e249d626a0f1c180dab75ce","index":"13343631333247680512"}"#;
+
+ // Deserialize the JSON into a DepositRequest struct
+ let deposit_request: DepositRequest =
+ serde_json::from_str(json_data).expect("Failed to deserialize");
+
+ // Verify the deserialized content
+ assert_eq!(
+ deposit_request.pubkey,
+ FixedBytes::<48>::from(hex!("8E01A8F21BDC38991ADA53CA86D6C78D874675A450A38431CC6AA0F12D5661E344784C56C8A211F7025224D1303EE801"))
+ );
+ assert_eq!(
+ deposit_request.withdrawal_credentials,
+ B256::from(hex!("010000000000000000000000AF6DF504F08DDF582D604D2F0A593BC153C25DBD"))
+ );
+ assert_eq!(deposit_request.amount, 0x0040597307000000u64);
+ assert_eq!(
+ deposit_request.signature,
+ FixedBytes::<96>::from(hex!("B65F3DB79405544528D6D92040282F29171F4FF6E5ABB2D59F9EE1F1254ACED2A7000F87BC2684F543E913A7CC1007EA0E97289B349C553EECDF253CD3EF5814088BA3D4AC286F2634DAC3D026D9A01E4C166DC75E249D626A0F1C180DAB75CE"))
+ );
+ assert_eq!(deposit_request.index, 0xB92E1A0000000000u64);
+
+ // Serialize the struct back into JSON
+ let serialized_json = serde_json::to_string(&deposit_request).expect("Failed to serialize");
+
+ // Check if the serialized JSON matches the expected JSON structure
+ assert_eq!(serialized_json, json_data);
+ }
+}
diff --git a/crates/eips/src/eip7002.rs b/crates/eips/src/eip7002.rs
index b8dfef97e53..4307866cc81 100644
--- a/crates/eips/src/eip7002.rs
+++ b/crates/eips/src/eip7002.rs
@@ -1,8 +1,8 @@
-//! Contains the system contract, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md).
+//! Contains the system contract and [WithdrawalRequest] types, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md).
//!
//! See also [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002): Execution layer triggerable withdrawals
-use alloy_primitives::{address, bytes, Address, Bytes};
+use alloy_primitives::{address, bytes, Address, Bytes, FixedBytes};
/// The caller to be used when calling the EIP-7002 withdrawal requests contract at the end of the
/// block.
@@ -17,3 +17,63 @@ pub static WITHDRAWAL_REQUEST_PREDEPLOY_CODE: Bytes = bytes!(" 3373fffffffffff
/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for withdrawal requests.
pub const WITHDRAWAL_REQUEST_TYPE: u8 = 0x01;
+
+/// The [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) defined maximum withdrawal requests per block.
+pub const MAX_WITHDRAWAL_REQUESTS_PER_BLOCK: usize = 16;
+
+/// Represents an execution layer triggerable withdrawal request.
+///
+/// See [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002).
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
+#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
+pub struct WithdrawalRequest {
+ /// Address of the source of the exit.
+ pub source_address: Address,
+ /// Validator public key.
+ pub validator_pubkey: FixedBytes<48>,
+ /// Amount of withdrawn ether in gwei.
+ #[cfg_attr(feature = "serde", serde(with = "alloy_serde::displayfromstr"))]
+ pub amount: u64,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloy_primitives::hex;
+
+ #[test]
+ #[cfg(feature = "serde")]
+ fn test_serde_withdrawal_request() {
+ // Sample JSON input representing a withdrawal request
+ let json_data = r#"{
+ "source_address":"0xAE0E8770147AaA6828a0D6f642504663F10F7d1E",
+ "validator_pubkey":"0x8e8d8749f6bc79b78be7cc6e49ff640e608454840c360b344c3a4d9b7428e280e7f40d2271bad65d8cbbfdd43cb8793b",
+ "amount":"1"
+ }"#;
+
+ // Deserialize the JSON into a WithdrawalRequest struct
+ let withdrawal_request: WithdrawalRequest =
+ serde_json::from_str(json_data).expect("Failed to deserialize");
+
+ // Verify the deserialized content
+ assert_eq!(
+ withdrawal_request.source_address,
+ address!("AE0E8770147AaA6828a0D6f642504663F10F7d1E")
+ );
+ assert_eq!(
+ withdrawal_request.validator_pubkey,
+ FixedBytes::<48>::from(hex!("8e8d8749f6bc79b78be7cc6e49ff640e608454840c360b344c3a4d9b7428e280e7f40d2271bad65d8cbbfdd43cb8793b"))
+ );
+ assert_eq!(withdrawal_request.amount, 1);
+
+ // Serialize the struct back into JSON
+ let serialized_json =
+ serde_json::to_string(&withdrawal_request).expect("Failed to serialize");
+
+ // Check if the serialized JSON matches the expected JSON structure
+ let expected_json = r#"{"source_address":"0xae0e8770147aaa6828a0d6f642504663f10f7d1e","validator_pubkey":"0x8e8d8749f6bc79b78be7cc6e49ff640e608454840c360b344c3a4d9b7428e280e7f40d2271bad65d8cbbfdd43cb8793b","amount":"1"}"#;
+ assert_eq!(serialized_json, expected_json);
+ }
+}
diff --git a/crates/eips/src/eip7251.rs b/crates/eips/src/eip7251.rs
index ead24920a23..8a4e701b242 100644
--- a/crates/eips/src/eip7251.rs
+++ b/crates/eips/src/eip7251.rs
@@ -1,8 +1,8 @@
-//! Contains consolidation code, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md).
+//! Contains consolidation types, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md).
//!
//! See also [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Increase the MAX_EFFECTIVE_BALANCE
-use alloy_primitives::{address, bytes, Address, Bytes};
+use alloy_primitives::{address, bytes, Address, Bytes, FixedBytes};
/// The address for the EIP-7251 consolidation requests contract:
/// `0x00b42dbF2194e931E80326D950320f7d9Dbeac02`
@@ -14,3 +14,63 @@ pub static CONSOLIDATION_REQUEST_PREDEPLOY_CODE: Bytes = bytes!("3373fffffffffff
/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for consolidation requests.
pub const CONSOLIDATION_REQUEST_TYPE: u8 = 0x02;
+
+/// The [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) defined maximum number of consolidation requests per block.
+pub const MAX_CONSOLIDATION_REQUESTS_PER_BLOCK: usize = 2;
+
+/// This structure maps onto the consolidation request object from [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251).
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
+#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
+pub struct ConsolidationRequest {
+ /// Source address
+ pub source_address: Address,
+ /// Source public key
+ pub source_pubkey: FixedBytes<48>,
+ /// Target public key
+ pub target_pubkey: FixedBytes<48>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use alloy_primitives::hex;
+ use core::str::FromStr;
+
+ #[test]
+ fn test_serde_consolidation_request() {
+ // Sample JSON input representing a consolidation request
+ let json_data = r#"{
+ "source_address":"0x007eABCA654E67103dF02f49EbdC5f6Cd9387a07",
+ "source_pubkey":"0xb13ff174911d0137e5f2b739fbf172b22cba35a037ef1edb03683b75c9abf5b271f8d48ad279cc89c7fae91db631c1e7",
+ "target_pubkey":"0xd0e5be6b709f2dc02a49f6e37e0d03b7d832b79b0db1c8bbfd5b81b8e57b79a1282fb99a671b4629a0e0bfffa7cf6d4f"
+ }"#;
+
+ // Deserialize the JSON into a ConsolidationRequest struct
+ let consolidation_request: ConsolidationRequest =
+ serde_json::from_str(json_data).expect("Failed to deserialize");
+
+ // Verify the deserialized content
+ assert_eq!(
+ consolidation_request.source_address,
+ Address::from_str("0x007eABCA654E67103dF02f49EbdC5f6Cd9387a07").unwrap()
+ );
+ assert_eq!(
+ consolidation_request.source_pubkey,
+ FixedBytes::<48>::from(hex!("b13ff174911d0137e5f2b739fbf172b22cba35a037ef1edb03683b75c9abf5b271f8d48ad279cc89c7fae91db631c1e7"))
+ );
+ assert_eq!(
+ consolidation_request.target_pubkey,
+ FixedBytes::<48>::from(hex!("d0e5be6b709f2dc02a49f6e37e0d03b7d832b79b0db1c8bbfd5b81b8e57b79a1282fb99a671b4629a0e0bfffa7cf6d4f"))
+ );
+
+ // Serialize the struct back into JSON
+ let serialized_json =
+ serde_json::to_string(&consolidation_request).expect("Failed to serialize");
+
+ // Check if the serialized JSON matches the expected JSON structure
+ let expected_json = r#"{"source_address":"0x007eabca654e67103df02f49ebdc5f6cd9387a07","source_pubkey":"0xb13ff174911d0137e5f2b739fbf172b22cba35a037ef1edb03683b75c9abf5b271f8d48ad279cc89c7fae91db631c1e7","target_pubkey":"0xd0e5be6b709f2dc02a49f6e37e0d03b7d832b79b0db1c8bbfd5b81b8e57b79a1282fb99a671b4629a0e0bfffa7cf6d4f"}"#;
+ assert_eq!(serialized_json, expected_json);
+ }
+}
diff --git a/crates/rpc-types-beacon/Cargo.toml b/crates/rpc-types-beacon/Cargo.toml
index 85eec6b6f02..9e9c60549f6 100644
--- a/crates/rpc-types-beacon/Cargo.toml
+++ b/crates/rpc-types-beacon/Cargo.toml
@@ -22,7 +22,6 @@ workspace = true
alloy-eips = { workspace = true, features = ["serde"] }
alloy-rpc-types-engine = { workspace = true, features = ["serde"] }
alloy-primitives.workspace = true
-alloy-serde.workspace = true
# ssz
ethereum_ssz_derive = { workspace = true, optional = true }
diff --git a/crates/rpc-types-beacon/src/examples/relay_builder_block_validation_request_v4.json b/crates/rpc-types-beacon/src/examples/relay_builder_block_validation_request_v4.json
new file mode 100644
index 00000000000..e61d8277280
--- /dev/null
+++ b/crates/rpc-types-beacon/src/examples/relay_builder_block_validation_request_v4.json
@@ -0,0 +1,164 @@
+{
+ "message": {
+ "slot": "6",
+ "parent_hash": "0x139b559e427d726a8cf7eda9c11a2586ed827a7619ffaeed3033845dc06b88e8",
+ "block_hash": "0x2982946c9fb44951fd9f65f73446a49d4fd6e0140b3f3ea857ff50eac7be69d7",
+ "builder_pubkey": "0xa1885d66bef164889a2e35845c3b626545d7b0e513efe335e97c3a45e534013fa3bc38c3b7e6143695aecc4872ac52c4",
+ "proposer_pubkey": "0x903e2989e7442ee0a8958d020507a8bd985d3974f5e8273093be00db3935f0500e141b252bd09e3728892c7a8443863c",
+ "proposer_fee_recipient": "0x690b9a9e9aa1c9db991c7721a92d351db4fac990",
+ "gas_limit": "30000000",
+ "gas_used": "42000",
+ "value": "999990575298322000"
+ },
+ "execution_payload": {
+ "parent_hash": "0x139b559e427d726a8cf7eda9c11a2586ed827a7619ffaeed3033845dc06b88e8",
+ "fee_recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
+ "state_root": "0x0f1109a777d62875f91b9457eb41122e27cf0d5eb478e475c46e57c6ee53ebcf",
+ "receipts_root": "0x75308898d571eafb5cd8cde8278bf5b3d13c5f6ec074926de3bb895b519264e1",
+ "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "prev_randao": "0x790a18ca85bf4a2e1f14679abda509fcdda1e322b6a2adbcabee1727793960d7",
+ "block_number": "6",
+ "gas_limit": "30000000",
+ "gas_used": "42000",
+ "timestamp": "1733949274",
+ "extra_data": "0xe29aa1f09fa496",
+ "base_fee_per_gas": "448795319",
+ "block_hash": "0x2982946c9fb44951fd9f65f73446a49d4fd6e0140b3f3ea857ff50eac7be69d7",
+ "transactions": [
+ "0x02f87082053901018445e0d81382520894f39fd6e51aad88f6f4ce6ab8827279cfffb92266880de0b6b3a764000080c001a0650262f2686338f44fc10d7da58e1aea564840d0c98ff11c44e0032143a52738a03d5bf4978d9d3429da0be7a5f0f82d1408a1a4c97ae2c72b7625c778d08190bd",
+ "0x02f8708205390180841ac012b782520894690b9a9e9aa1c9db991c7721a92d351db4fac990880de0ae214b651e5080c001a03ef471558fe8f14fbfc1e094e0e37da8f6fe16272e388fd00c47e438e4f7e68ea011a49e0f3fe3833c7629f809612c22dbba3361902721885bb0e2ce15e3efc16b"
+ ],
+ "withdrawals": [
+ {
+ "index": "33",
+ "validator_index": "84",
+ "address": "0x8794388915e86e4988363cdd4289ad19182209c8",
+ "amount": "4578"
+ },
+ {
+ "index": "34",
+ "validator_index": "85",
+ "address": "0xa3862121db5914d7272b0b705e6e3c5336b79e31",
+ "amount": "5886"
+ },
+ {
+ "index": "35",
+ "validator_index": "86",
+ "address": "0x96ef954b331a534199f4f113d993a50ec7a781fc",
+ "amount": "5232"
+ },
+ {
+ "index": "36",
+ "validator_index": "87",
+ "address": "0x96c8d3dd08724624017f178393d176b425dab9df",
+ "amount": "4578"
+ },
+ {
+ "index": "37",
+ "validator_index": "88",
+ "address": "0x92bd81b8e9099b9ca87a2033fdd84475752dc34a",
+ "amount": "2616"
+ },
+ {
+ "index": "38",
+ "validator_index": "89",
+ "address": "0x83802cd575a3cea7e3e38fc1a73d94a9e4fdb999",
+ "amount": "654"
+ },
+ {
+ "index": "39",
+ "validator_index": "90",
+ "address": "0xb451eb0ff4990917aba6e3d80c34aee91ea1ce49",
+ "amount": "5886"
+ },
+ {
+ "index": "40",
+ "validator_index": "91",
+ "address": "0xa7f711233af57440e9ea700113fc4dbaef97e7da",
+ "amount": "1962"
+ },
+ {
+ "index": "41",
+ "validator_index": "92",
+ "address": "0xaca5e4979f281b5ab0ea0f549d6dcc34989607c3",
+ "amount": "4578"
+ },
+ {
+ "index": "42",
+ "validator_index": "93",
+ "address": "0x984620db3658a19769475080998db9e7f5bcd425",
+ "amount": "5886"
+ },
+ {
+ "index": "43",
+ "validator_index": "94",
+ "address": "0x8f1ef3639aea57fef705847e251b785bb608a848",
+ "amount": "2616"
+ },
+ {
+ "index": "44",
+ "validator_index": "95",
+ "address": "0x8967da3c8071ba2bf632cd40ae08fbbf0a203c47",
+ "amount": "7194"
+ },
+ {
+ "index": "45",
+ "validator_index": "96",
+ "address": "0x8d58f7e2e58471b46d20a66a61f4cde3c78ab6c0",
+ "amount": "7194"
+ },
+ {
+ "index": "46",
+ "validator_index": "97",
+ "address": "0x8db9f236d3483af79703244c7034b5267a0546c3",
+ "amount": "3270"
+ },
+ {
+ "index": "47",
+ "validator_index": "98",
+ "address": "0xb7721412ae5a793f34ac8866698b221c67ef8272",
+ "amount": "3270"
+ },
+ {
+ "index": "48",
+ "validator_index": "99",
+ "address": "0x99f6e5b80dc52407f0436d3474bd5da5ff23a19c",
+ "amount": "2616"
+ }
+ ],
+ "blob_gas_used": "0",
+ "excess_blob_gas": "0"
+ },
+ "blobs_bundle": {
+ "commitments": [],
+ "proofs": [],
+ "blobs": []
+ },
+ "execution_requests": {
+ "deposits": [
+ {
+ "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+ "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
+ "amount": "1",
+ "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
+ "index": "1"
+ }
+ ],
+ "withdrawals": [
+ {
+ "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
+ "validator_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+ "amount": "1"
+ }
+ ],
+ "consolidations": [
+ {
+ "source_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
+ "source_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+ "target_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
+ }
+ ]
+ },
+ "target_blobs_per_block": "6",
+ "signature": "0xa37afc405ef69e4f331e79f9de77e0df870609898c0e10a1cfcd8162e8771c4e4fefa7258059d83f72fb599b12f1bb73068476ebfaedc65e5a068425693ba272f277d83e11334e87a7d1425a2fbd369ed9351f0eb14fdc8bd93115543f6a4c67"
+}
\ No newline at end of file
diff --git a/crates/rpc-types-beacon/src/lib.rs b/crates/rpc-types-beacon/src/lib.rs
index 9097056cc37..897cea89a2b 100644
--- a/crates/rpc-types-beacon/src/lib.rs
+++ b/crates/rpc-types-beacon/src/lib.rs
@@ -24,6 +24,9 @@ pub mod payload;
/// Types and functions related to the relay mechanism.
pub mod relay;
+/// Types and functions related to execution requests.
+pub mod requests;
+
/// Types and functions related to the sidecar.
pub mod sidecar;
diff --git a/crates/rpc-types-beacon/src/relay.rs b/crates/rpc-types-beacon/src/relay.rs
index 2940256c47e..2a4fe0e55dc 100644
--- a/crates/rpc-types-beacon/src/relay.rs
+++ b/crates/rpc-types-beacon/src/relay.rs
@@ -2,8 +2,8 @@
//!
//! See also
-use crate::{BlsPublicKey, BlsSignature};
-use alloy_primitives::{Address, Bytes, B256, U256};
+use crate::{requests::ExecutionRequestsV4, BlsPublicKey, BlsSignature};
+use alloy_primitives::{Address, B256, U256};
use alloy_rpc_types_engine::{
BlobsBundleV1, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3,
};
@@ -146,6 +146,7 @@ pub struct SignedBidSubmissionV3 {
}
/// Submission for the `/relay/v1/builder/blocks` endpoint (Electra).
+#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "ssz", derive(ssz_derive::Decode, ssz_derive::Encode))]
@@ -158,9 +159,9 @@ pub struct SignedBidSubmissionV4 {
/// The Electra block bundle for this bid.
pub blobs_bundle: BlobsBundleV1,
/// The Pectra execution requests for this bid.
- pub execution_requests: Vec,
+ pub execution_requests: ExecutionRequestsV4,
/// The EIP-7742 blobs per block for this bid.
- #[serde(with = "alloy_serde::quantity")]
+ #[serde_as(as = "DisplayFromStr")]
pub target_blobs_per_block: u64,
/// The signature associated with the submission.
pub signature: BlsSignature,
@@ -479,6 +480,15 @@ mod tests {
assert_eq!(json, serde_json::to_value(bid).unwrap());
}
+ #[test]
+ fn electra_bid_submission() {
+ let s = include_str!("examples/relay_builder_block_validation_request_v4.json");
+
+ let bid = serde_json::from_str::(s).unwrap();
+ let json: serde_json::Value = serde_json::from_str(s).unwrap();
+ assert_eq!(json, serde_json::to_value(bid).unwrap());
+ }
+
#[cfg(feature = "ssz")]
#[test]
fn capella_bid_submission_ssz() {
diff --git a/crates/rpc-types-beacon/src/requests.rs b/crates/rpc-types-beacon/src/requests.rs
new file mode 100644
index 00000000000..6e93bf2a36b
--- /dev/null
+++ b/crates/rpc-types-beacon/src/requests.rs
@@ -0,0 +1,184 @@
+#[cfg(feature = "ssz")]
+use alloy_eips::eip7685::Requests;
+use alloy_eips::{
+ eip6110::DepositRequest, eip7002::WithdrawalRequest, eip7251::ConsolidationRequest,
+};
+use serde::{Deserialize, Serialize};
+
+/// An Electra-compatible execution requests payload.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+#[serde(rename_all = "snake_case")]
+#[cfg_attr(feature = "ssz", derive(ssz_derive::Decode, ssz_derive::Encode))]
+pub struct ExecutionRequestsV4 {
+ /// The requested deposits.
+ pub deposits: Vec,
+ /// The requested withdrawals.
+ pub withdrawals: Vec,
+ /// The requested consolidations.
+ pub consolidations: Vec,
+}
+
+impl ExecutionRequestsV4 {
+ /// Convert the [ExecutionRequestsV4] into a [Requests].
+ #[cfg(feature = "ssz")]
+ pub fn to_requests(&self) -> Requests {
+ self.into()
+ }
+}
+
+#[cfg(feature = "ssz")]
+pub use ssz_requests_conversions::TryFromRequestsError;
+
+#[cfg(feature = "ssz")]
+mod ssz_requests_conversions {
+ use super::*;
+ use crate::requests::TryFromRequestsError::SszDecodeError;
+ use alloy_eips::{
+ eip6110::{DepositRequest, DEPOSIT_REQUEST_TYPE, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD},
+ eip7002::{WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_BLOCK, WITHDRAWAL_REQUEST_TYPE},
+ eip7251::{
+ ConsolidationRequest, CONSOLIDATION_REQUEST_TYPE, MAX_CONSOLIDATION_REQUESTS_PER_BLOCK,
+ },
+ eip7685::Requests,
+ };
+ use ssz::{Decode, DecodeError, Encode};
+
+ impl TryFrom<&Requests> for ExecutionRequestsV4 {
+ type Error = TryFromRequestsError;
+
+ fn try_from(value: &Requests) -> Result {
+ #[derive(Default)]
+ struct RequestAccumulator {
+ deposits: Vec,
+ withdrawals: Vec,
+ consolidations: Vec,
+ }
+
+ impl RequestAccumulator {
+ fn parse_request_payload(
+ payload: &[u8],
+ max_size: usize,
+ request_type: u8,
+ ) -> Result, TryFromRequestsError>
+ where
+ Vec: Decode + Encode,
+ {
+ let list: Vec = Vec::from_ssz_bytes(payload)
+ .map_err(|e| SszDecodeError(request_type, e))?;
+
+ if list.len() > max_size {
+ return Err(TryFromRequestsError::RequestPayloadSizeExceeded(
+ request_type,
+ list.len(),
+ ));
+ }
+
+ Ok(list)
+ }
+
+ fn accumulate(mut self, request: &[u8]) -> Result {
+ if request.is_empty() {
+ return Err(TryFromRequestsError::EmptyRequest);
+ }
+
+ let (request_type, payload) =
+ request.split_first().expect("already checked for empty");
+
+ match *request_type {
+ DEPOSIT_REQUEST_TYPE => {
+ self.deposits = Self::parse_request_payload(
+ payload,
+ MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD,
+ DEPOSIT_REQUEST_TYPE,
+ )?;
+ }
+ WITHDRAWAL_REQUEST_TYPE => {
+ self.withdrawals = Self::parse_request_payload(
+ payload,
+ MAX_WITHDRAWAL_REQUESTS_PER_BLOCK,
+ WITHDRAWAL_REQUEST_TYPE,
+ )?;
+ }
+ CONSOLIDATION_REQUEST_TYPE => {
+ self.consolidations = Self::parse_request_payload(
+ payload,
+ MAX_CONSOLIDATION_REQUESTS_PER_BLOCK,
+ CONSOLIDATION_REQUEST_TYPE,
+ )?;
+ }
+ unknown => return Err(TryFromRequestsError::UnknownRequestType(unknown)),
+ }
+
+ Ok(self)
+ }
+ }
+
+ let accumulator = value
+ .iter()
+ .try_fold(RequestAccumulator::default(), |acc, request| acc.accumulate(request))?;
+
+ Ok(Self {
+ deposits: accumulator.deposits,
+ withdrawals: accumulator.withdrawals,
+ consolidations: accumulator.consolidations,
+ })
+ }
+ }
+
+ /// Errors possible converting a [Requests] to [ExecutionRequestsV4]
+ #[derive(Debug, thiserror::Error)]
+ pub enum TryFromRequestsError {
+ /// One of the Bytes is empty.
+ #[error("empty bytes in requests body")]
+ EmptyRequest,
+ /// Bytes prefix is not a known EIP-7685 request_type in Electra.
+ #[error("unknown request_type prefix: {0}")]
+ UnknownRequestType(u8),
+ /// Remaining bytes could not be decoded as SSZ requests_data.
+ #[error("ssz error decoding requests_type: {0}")]
+ SszDecodeError(u8, DecodeError),
+ /// Requests of request_type exceeds Electra size limits
+ #[error("requests_data payload for request_type {0} exceeds Electra size limit {1}")]
+ RequestPayloadSizeExceeded(u8, usize),
+ }
+
+ impl From<&ExecutionRequestsV4> for Requests {
+ fn from(val: &ExecutionRequestsV4) -> Self {
+ let deposit_bytes = val.deposits.as_ssz_bytes();
+ let withdrawals_bytes = val.withdrawals.as_ssz_bytes();
+ let consolidations_bytes = val.consolidations.as_ssz_bytes();
+
+ let mut requests = Self::with_capacity(3);
+ requests.push_request_with_type(DEPOSIT_REQUEST_TYPE, deposit_bytes);
+ requests.push_request_with_type(WITHDRAWAL_REQUEST_TYPE, withdrawals_bytes);
+ requests.push_request_with_type(CONSOLIDATION_REQUEST_TYPE, consolidations_bytes);
+ requests
+ }
+ }
+
+ #[cfg(test)]
+ mod tests {
+ use super::*;
+ use alloy_primitives::Bytes;
+ use std::str::FromStr;
+ #[test]
+ fn test_from_requests() -> Result<(), TryFromRequestsError> {
+ let original = Requests::new(vec![
+ // Taken from: https://github.com/ensi321/execution-apis/blob/88c08d6104e9e8ae1d369c2b26c393a0df599e9a/src/engine/openrpc/methods/payload.yaml#L554-L556
+ Bytes::from_str("0x0096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20100000000000000b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9f000000000000000a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca01000000000000009561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1f100000000000000").unwrap(),
+ Bytes::from_str("0x01a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d0000000000000000000000000000000000000000000000000000010f698daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a75530100000000000000").unwrap(),
+ Bytes::from_str("0x02a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d098daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a7553").unwrap(),
+ ]);
+
+ let requests = ExecutionRequestsV4::try_from(&original)?;
+ assert_eq!(requests.deposits.len(), 2);
+ assert_eq!(requests.withdrawals.len(), 2);
+ assert_eq!(requests.consolidations.len(), 1);
+
+ let round_trip: Requests = (&requests).into();
+ assert_eq!(original, round_trip);
+ Ok(())
+ }
+ }
+}
diff --git a/crates/serde/src/displayfromstr.rs b/crates/serde/src/displayfromstr.rs
new file mode 100644
index 00000000000..6c7dd3b7dee
--- /dev/null
+++ b/crates/serde/src/displayfromstr.rs
@@ -0,0 +1,46 @@
+//! Serde functions for (de)serializing using FromStr and Display
+//!
+//! Useful for example in encoding SSZ `uintN` primitives using the "canonical JSON mapping"
+//! described in the consensus-specs here:
+//!
+//! # Example
+//! ```
+//! use alloy_serde;
+//! use serde::{Deserialize, Serialize};
+//!
+//! #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
+//! pub struct Container {
+//! #[serde(with = "alloy_serde::displayfromstr")]
+//! value: u64,
+//! }
+//!
+//! let val = Container { value: 18112749083033600 };
+//! let s = serde_json::to_string(&val).unwrap();
+//! assert_eq!(s, "{\"value\":\"18112749083033600\"}");
+//!
+//! let deserialized: Container = serde_json::from_str(&s).unwrap();
+//! assert_eq!(val, deserialized);
+//! ```
+
+use crate::alloc::string::{String, ToString};
+use core::{fmt, str::FromStr};
+use serde::{Deserialize, Deserializer, Serializer};
+
+/// Serialize a type `T` that implements [fmt::Display] as a quoted string.
+pub fn serialize(value: &T, serializer: S) -> Result
+where
+ T: fmt::Display,
+ S: Serializer,
+{
+ serializer.collect_str(&value.to_string())
+}
+
+/// Deserialize a quoted string to a type `T` using [FromStr].
+pub fn deserialize<'de, T, D>(deserializer: D) -> Result
+where
+ D: Deserializer<'de>,
+ T: FromStr,
+ T::Err: fmt::Display,
+{
+ String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
+}
diff --git a/crates/serde/src/lib.rs b/crates/serde/src/lib.rs
index 603300801fa..3ca005adc11 100644
--- a/crates/serde/src/lib.rs
+++ b/crates/serde/src/lib.rs
@@ -18,6 +18,8 @@ use serde::Serializer;
mod bool;
pub use self::bool::*;
+pub mod displayfromstr;
+
mod optional;
pub use self::optional::*;
@@ -31,6 +33,7 @@ pub mod ttd;
pub use ttd::*;
mod other;
+
pub use other::{OtherFields, WithOtherFields};
/// Serialize a byte vec as a hex string _without_ the "0x" prefix.