Skip to main content

alloy_rpc_types_beacon/
payload.rs

1//! Payload support for the beacon API.
2//!
3//! Internal helper module to deserialize/serialize the payload attributes for the beacon API, which
4//! uses snake case and quoted decimals.
5//!
6//! This is necessary because we don't want to allow a mixture of both formats, hence `serde`
7//! aliases are not an option.
8//!
9//! See also <https://github.com/ethereum/consensus-specs/blob/master/specs/deneb/beacon-chain.md#executionpayload>
10
11use crate::{withdrawals::BeaconWithdrawal, BlsPublicKey};
12use alloy_eips::eip4895::Withdrawal;
13use alloy_primitives::{Address, Bloom, Bytes, B256, U256};
14use alloy_rpc_types_engine::{
15    ExecutionPayload, ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3,
16    ExecutionPayloadV4,
17};
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19use serde_with::{serde_as, DeserializeAs, DisplayFromStr, SerializeAs};
20use std::borrow::Cow;
21
22/// Response object of GET `/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}`
23///
24/// See also <https://ethereum.github.io/builder-specs/#/Builder/getHeader>
25#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct GetExecutionPayloadHeaderResponse {
27    /// The version of the response.
28    pub version: String,
29    /// The data associated with the execution payload header.
30    pub data: ExecutionPayloadHeaderData,
31}
32
33/// Data structure representing the header data of an execution payload.
34///
35/// This structure is used to hold the core elements of an execution payload header,
36/// including the message and signature components.
37#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct ExecutionPayloadHeaderData {
39    /// The message of the execution payload header.
40    pub message: ExecutionPayloadHeaderMessage,
41    /// The signature of the execution payload header.
42    pub signature: Bytes,
43}
44
45/// Message structure within the header of an execution payload.
46///
47/// This structure contains detailed information about the execution payload,
48/// including the header, value, and public key associated with the payload.
49#[serde_as]
50#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct ExecutionPayloadHeaderMessage {
52    /// The header of the execution payload.
53    pub header: ExecutionPayloadHeader,
54    /// The value of the execution payload, represented as a `U256`.
55    #[serde_as(as = "DisplayFromStr")]
56    pub value: U256,
57    /// The public key associated with the execution payload.
58    pub pubkey: BlsPublicKey,
59}
60
61/// Data structure representing the signed blinded block submitted to the builder, binding the
62/// proposer to the block. with its signature.
63///
64/// See <https://ethereum.github.io/builder-specs/#/Builder/submitBlindedBlockV2>.
65#[serde_as]
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct BeaconBlockData {
68    /// The message of the signed beacon block
69    pub message: BeaconBlockMessage,
70    /// The signature of the beacon block
71    pub signature: Bytes,
72}
73
74/// Block Body Message
75#[serde_as]
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct BeaconBlockMessage {
78    /// Slot number
79    pub slot: String,
80    /// Proposer index  
81    pub proposer_index: String,
82    /// Parent root
83    pub parent_root: String,
84    /// State root
85    pub state_root: String,
86    /// Block body
87    pub body: BeaconBlockBody,
88}
89
90/// Execution payload body
91#[serde_as]
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct BeaconBlockBody {
94    /// Execution payload
95    #[serde(
96        serialize_with = "beacon_payload::serialize",
97        deserialize_with = "beacon_payload::deserialize"
98    )]
99    pub execution_payload: ExecutionPayload,
100}
101
102impl BeaconBlockData {
103    /// Get the execution payload
104    pub const fn execution_payload(&self) -> &ExecutionPayload {
105        &self.message.body.execution_payload
106    }
107}
108
109/// The header of the execution payload.
110#[serde_as]
111#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct ExecutionPayloadHeader {
113    /// The parent hash of the execution payload.
114    pub parent_hash: B256,
115    /// The fee recipient address of the execution payload.
116    pub fee_recipient: Address,
117    /// The state root of the execution payload.
118    pub state_root: B256,
119    /// The receipts root of the execution payload.
120    pub receipts_root: B256,
121    /// The logs bloom filter of the execution payload.
122    pub logs_bloom: Bloom,
123    /// The previous Randao value of the execution payload.
124    pub prev_randao: B256,
125    /// The block number of the execution payload, represented as a `u64`.
126    #[serde_as(as = "DisplayFromStr")]
127    pub block_number: u64,
128    /// The gas limit of the execution payload, represented as a `u64`.
129    #[serde_as(as = "DisplayFromStr")]
130    pub gas_limit: u64,
131    /// The gas used by the execution payload, represented as a `u64`.
132    #[serde_as(as = "DisplayFromStr")]
133    pub gas_used: u64,
134    /// The timestamp of the execution payload, represented as a `u64`.
135    #[serde_as(as = "DisplayFromStr")]
136    pub timestamp: u64,
137    /// The extra data of the execution payload.
138    pub extra_data: Bytes,
139    /// The base fee per gas of the execution payload, represented as a `U256`.
140    #[serde_as(as = "DisplayFromStr")]
141    pub base_fee_per_gas: U256,
142    /// The block hash of the execution payload.
143    pub block_hash: B256,
144    /// The transactions root of the execution payload.
145    pub transactions_root: B256,
146}
147
148#[serde_as]
149#[derive(Serialize, Deserialize)]
150struct BeaconPayloadAttributes {
151    #[serde_as(as = "DisplayFromStr")]
152    timestamp: u64,
153    prev_randao: B256,
154    suggested_fee_recipient: Address,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    #[serde_as(as = "Option<Vec<BeaconWithdrawal>>")]
157    withdrawals: Option<Vec<Withdrawal>>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    parent_beacon_block_root: Option<B256>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub slot_number: Option<u64>,
162}
163
164/// A helper module for serializing and deserializing the payload attributes for the beacon API.
165///
166/// The beacon API encoded object has equivalent fields to the
167/// [PayloadAttributes](alloy_rpc_types_engine::PayloadAttributes) with two differences:
168/// 1) `snake_case` identifiers must be used rather than `camelCase`;
169/// 2) integers must be encoded as quoted decimals rather than big-endian hex.
170pub mod beacon_api_payload_attributes {
171    use super::*;
172    use alloy_rpc_types_engine::PayloadAttributes;
173
174    /// Serialize the payload attributes for the beacon API.
175    pub fn serialize<S>(
176        payload_attributes: &PayloadAttributes,
177        serializer: S,
178    ) -> Result<S::Ok, S::Error>
179    where
180        S: Serializer,
181    {
182        let beacon_api_payload_attributes = BeaconPayloadAttributes {
183            timestamp: payload_attributes.timestamp,
184            prev_randao: payload_attributes.prev_randao,
185            suggested_fee_recipient: payload_attributes.suggested_fee_recipient,
186            withdrawals: payload_attributes.withdrawals.clone(),
187            parent_beacon_block_root: payload_attributes.parent_beacon_block_root,
188            slot_number: payload_attributes.slot_number,
189        };
190        beacon_api_payload_attributes.serialize(serializer)
191    }
192
193    /// Deserialize the payload attributes for the beacon API.
194    pub fn deserialize<'de, D>(deserializer: D) -> Result<PayloadAttributes, D::Error>
195    where
196        D: Deserializer<'de>,
197    {
198        let beacon_api_payload_attributes = BeaconPayloadAttributes::deserialize(deserializer)?;
199        Ok(PayloadAttributes {
200            timestamp: beacon_api_payload_attributes.timestamp,
201            prev_randao: beacon_api_payload_attributes.prev_randao,
202            suggested_fee_recipient: beacon_api_payload_attributes.suggested_fee_recipient,
203            withdrawals: beacon_api_payload_attributes.withdrawals,
204            parent_beacon_block_root: beacon_api_payload_attributes.parent_beacon_block_root,
205            slot_number: beacon_api_payload_attributes.slot_number,
206        })
207    }
208}
209
210#[serde_as]
211#[derive(Debug, Serialize, Deserialize)]
212struct BeaconExecutionPayloadV1<'a> {
213    parent_hash: Cow<'a, B256>,
214    fee_recipient: Cow<'a, Address>,
215    state_root: Cow<'a, B256>,
216    receipts_root: Cow<'a, B256>,
217    logs_bloom: Cow<'a, Bloom>,
218    prev_randao: Cow<'a, B256>,
219    #[serde_as(as = "DisplayFromStr")]
220    block_number: u64,
221    #[serde_as(as = "DisplayFromStr")]
222    gas_limit: u64,
223    #[serde_as(as = "DisplayFromStr")]
224    gas_used: u64,
225    #[serde_as(as = "DisplayFromStr")]
226    timestamp: u64,
227    extra_data: Cow<'a, Bytes>,
228    #[serde_as(as = "DisplayFromStr")]
229    base_fee_per_gas: U256,
230    block_hash: Cow<'a, B256>,
231    transactions: Cow<'a, [Bytes]>,
232}
233
234impl<'a> From<BeaconExecutionPayloadV1<'a>> for ExecutionPayloadV1 {
235    fn from(payload: BeaconExecutionPayloadV1<'a>) -> Self {
236        let BeaconExecutionPayloadV1 {
237            parent_hash,
238            fee_recipient,
239            state_root,
240            receipts_root,
241            logs_bloom,
242            prev_randao,
243            block_number,
244            gas_limit,
245            gas_used,
246            timestamp,
247            extra_data,
248            base_fee_per_gas,
249            block_hash,
250            transactions,
251        } = payload;
252        Self {
253            parent_hash: parent_hash.into_owned(),
254            fee_recipient: fee_recipient.into_owned(),
255            state_root: state_root.into_owned(),
256            receipts_root: receipts_root.into_owned(),
257            logs_bloom: logs_bloom.into_owned(),
258            prev_randao: prev_randao.into_owned(),
259            block_number,
260            gas_limit,
261            gas_used,
262            timestamp,
263            extra_data: extra_data.into_owned(),
264            base_fee_per_gas,
265            block_hash: block_hash.into_owned(),
266            transactions: transactions.into_owned(),
267        }
268    }
269}
270
271impl<'a> From<&'a ExecutionPayloadV1> for BeaconExecutionPayloadV1<'a> {
272    fn from(value: &'a ExecutionPayloadV1) -> Self {
273        let ExecutionPayloadV1 {
274            parent_hash,
275            fee_recipient,
276            state_root,
277            receipts_root,
278            logs_bloom,
279            prev_randao,
280            block_number,
281            gas_limit,
282            gas_used,
283            timestamp,
284            extra_data,
285            base_fee_per_gas,
286            block_hash,
287            transactions,
288        } = value;
289
290        BeaconExecutionPayloadV1 {
291            parent_hash: Cow::Borrowed(parent_hash),
292            fee_recipient: Cow::Borrowed(fee_recipient),
293            state_root: Cow::Borrowed(state_root),
294            receipts_root: Cow::Borrowed(receipts_root),
295            logs_bloom: Cow::Borrowed(logs_bloom),
296            prev_randao: Cow::Borrowed(prev_randao),
297            block_number: *block_number,
298            gas_limit: *gas_limit,
299            gas_used: *gas_used,
300            timestamp: *timestamp,
301            extra_data: Cow::Borrowed(extra_data),
302            base_fee_per_gas: *base_fee_per_gas,
303            block_hash: Cow::Borrowed(block_hash),
304            transactions: Cow::Borrowed(transactions),
305        }
306    }
307}
308
309/// A helper serde module to convert from/to the Beacon API which uses quoted decimals rather than
310/// big-endian hex.
311pub mod beacon_payload_v1 {
312    use super::*;
313
314    /// Serialize the payload attributes for the beacon API.
315    pub fn serialize<S>(
316        payload_attributes: &ExecutionPayloadV1,
317        serializer: S,
318    ) -> Result<S::Ok, S::Error>
319    where
320        S: Serializer,
321    {
322        BeaconExecutionPayloadV1::from(payload_attributes).serialize(serializer)
323    }
324
325    /// Deserialize the payload attributes for the beacon API.
326    pub fn deserialize<'de, D>(deserializer: D) -> Result<ExecutionPayloadV1, D::Error>
327    where
328        D: Deserializer<'de>,
329    {
330        BeaconExecutionPayloadV1::deserialize(deserializer).map(Into::into)
331    }
332}
333
334#[serde_as]
335#[derive(Debug, Serialize, Deserialize)]
336struct BeaconExecutionPayloadV2<'a> {
337    /// Inner V1 payload
338    #[serde(flatten)]
339    payload_inner: BeaconExecutionPayloadV1<'a>,
340    /// Array of [`Withdrawal`] enabled with V2
341    /// See <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/shanghai.md#executionpayloadv2>
342    #[serde_as(as = "Vec<BeaconWithdrawal>")]
343    withdrawals: Vec<Withdrawal>,
344}
345
346impl<'a> From<BeaconExecutionPayloadV2<'a>> for ExecutionPayloadV2 {
347    fn from(payload: BeaconExecutionPayloadV2<'a>) -> Self {
348        let BeaconExecutionPayloadV2 { payload_inner, withdrawals } = payload;
349        Self { payload_inner: payload_inner.into(), withdrawals }
350    }
351}
352
353impl<'a> From<&'a ExecutionPayloadV2> for BeaconExecutionPayloadV2<'a> {
354    fn from(value: &'a ExecutionPayloadV2) -> Self {
355        let ExecutionPayloadV2 { payload_inner, withdrawals } = value;
356        BeaconExecutionPayloadV2 {
357            payload_inner: payload_inner.into(),
358            withdrawals: withdrawals.clone(),
359        }
360    }
361}
362
363/// A helper serde module to convert from/to the Beacon API which uses quoted decimals rather than
364/// big-endian hex.
365pub mod beacon_payload_v2 {
366    use super::*;
367
368    /// Serialize the payload attributes for the beacon API.
369    pub fn serialize<S>(
370        payload_attributes: &ExecutionPayloadV2,
371        serializer: S,
372    ) -> Result<S::Ok, S::Error>
373    where
374        S: Serializer,
375    {
376        BeaconExecutionPayloadV2::from(payload_attributes).serialize(serializer)
377    }
378
379    /// Deserialize the payload attributes for the beacon API.
380    pub fn deserialize<'de, D>(deserializer: D) -> Result<ExecutionPayloadV2, D::Error>
381    where
382        D: Deserializer<'de>,
383    {
384        BeaconExecutionPayloadV2::deserialize(deserializer).map(Into::into)
385    }
386}
387
388#[serde_as]
389#[derive(Debug, Serialize, Deserialize)]
390struct BeaconExecutionPayloadV3<'a> {
391    /// Inner V2 payload
392    #[serde(flatten)]
393    payload_inner: BeaconExecutionPayloadV2<'a>,
394    #[serde_as(as = "DisplayFromStr")]
395    blob_gas_used: u64,
396    #[serde_as(as = "DisplayFromStr")]
397    excess_blob_gas: u64,
398}
399
400impl<'a> From<BeaconExecutionPayloadV3<'a>> for ExecutionPayloadV3 {
401    fn from(payload: BeaconExecutionPayloadV3<'a>) -> Self {
402        let BeaconExecutionPayloadV3 { payload_inner, blob_gas_used, excess_blob_gas } = payload;
403        Self { payload_inner: payload_inner.into(), blob_gas_used, excess_blob_gas }
404    }
405}
406
407impl<'a> From<&'a ExecutionPayloadV3> for BeaconExecutionPayloadV3<'a> {
408    fn from(value: &'a ExecutionPayloadV3) -> Self {
409        let ExecutionPayloadV3 { payload_inner, blob_gas_used, excess_blob_gas } = value;
410        BeaconExecutionPayloadV3 {
411            payload_inner: payload_inner.into(),
412            blob_gas_used: *blob_gas_used,
413            excess_blob_gas: *excess_blob_gas,
414        }
415    }
416}
417
418/// A helper serde module to convert from/to the Beacon API which uses quoted decimals rather than
419/// big-endian hex.
420pub mod beacon_payload_v3 {
421    use super::*;
422
423    /// Serialize the payload attributes for the beacon API.
424    pub fn serialize<S>(
425        payload_attributes: &ExecutionPayloadV3,
426        serializer: S,
427    ) -> Result<S::Ok, S::Error>
428    where
429        S: Serializer,
430    {
431        BeaconExecutionPayloadV3::from(payload_attributes).serialize(serializer)
432    }
433
434    /// Deserialize the payload attributes for the beacon API.
435    pub fn deserialize<'de, D>(deserializer: D) -> Result<ExecutionPayloadV3, D::Error>
436    where
437        D: Deserializer<'de>,
438    {
439        BeaconExecutionPayloadV3::deserialize(deserializer).map(Into::into)
440    }
441}
442
443/// Beacon API representation of [`ExecutionPayloadV4`].
444///
445/// See also <https://github.com/alloy-rs/alloy/pull/3330>
446#[serde_as]
447#[derive(Debug, Serialize, Deserialize)]
448struct BeaconExecutionPayloadV4<'a> {
449    /// Inner V3 payload
450    #[serde(flatten)]
451    payload_inner: BeaconExecutionPayloadV3<'a>,
452    /// RLP-encoded block access list as defined in EIP-7928.
453    block_access_list: Cow<'a, Bytes>,
454    /// The slot number corresponding to this block, calculated in the consensus layer.
455    #[serde_as(as = "DisplayFromStr")]
456    slot_number: u64,
457}
458
459impl<'a> From<BeaconExecutionPayloadV4<'a>> for ExecutionPayloadV4 {
460    fn from(payload: BeaconExecutionPayloadV4<'a>) -> Self {
461        let BeaconExecutionPayloadV4 { payload_inner, block_access_list, slot_number } = payload;
462        Self {
463            payload_inner: payload_inner.into(),
464            block_access_list: block_access_list.into_owned(),
465            slot_number,
466        }
467    }
468}
469
470impl<'a> From<&'a ExecutionPayloadV4> for BeaconExecutionPayloadV4<'a> {
471    fn from(value: &'a ExecutionPayloadV4) -> Self {
472        let ExecutionPayloadV4 { payload_inner, block_access_list, slot_number } = value;
473        BeaconExecutionPayloadV4 {
474            payload_inner: payload_inner.into(),
475            block_access_list: Cow::Borrowed(block_access_list),
476            slot_number: *slot_number,
477        }
478    }
479}
480
481/// A helper serde module to convert from/to the Beacon API which uses quoted decimals rather than
482/// big-endian hex.
483pub mod beacon_payload_v4 {
484    use super::*;
485
486    /// Serialize the payload attributes for the beacon API.
487    pub fn serialize<S>(
488        payload_attributes: &ExecutionPayloadV4,
489        serializer: S,
490    ) -> Result<S::Ok, S::Error>
491    where
492        S: Serializer,
493    {
494        BeaconExecutionPayloadV4::from(payload_attributes).serialize(serializer)
495    }
496
497    /// Deserialize the payload attributes for the beacon API.
498    pub fn deserialize<'de, D>(deserializer: D) -> Result<ExecutionPayloadV4, D::Error>
499    where
500        D: Deserializer<'de>,
501    {
502        BeaconExecutionPayloadV4::deserialize(deserializer).map(Into::into)
503    }
504}
505
506/// Represents all possible payload versions.
507#[derive(Debug, Serialize)]
508#[serde(untagged)]
509enum BeaconExecutionPayload<'a> {
510    /// V1 payload
511    V1(BeaconExecutionPayloadV1<'a>),
512    /// V2 payload
513    V2(BeaconExecutionPayloadV2<'a>),
514    /// V3 payload
515    V3(BeaconExecutionPayloadV3<'a>),
516    /// V4 payload (Amsterdam)
517    V4(BeaconExecutionPayloadV4<'a>),
518}
519
520// Deserializes untagged ExecutionPayload by trying each variant in falling order
521impl<'de> Deserialize<'de> for BeaconExecutionPayload<'de> {
522    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
523    where
524        D: Deserializer<'de>,
525    {
526        #[serde_as]
527        #[derive(Deserialize)]
528        #[serde(untagged)]
529        enum BeaconExecutionPayloadDesc<'a> {
530            V4(BeaconExecutionPayloadV4<'a>),
531            V3(BeaconExecutionPayloadV3<'a>),
532            V2(BeaconExecutionPayloadV2<'a>),
533            V1(BeaconExecutionPayloadV1<'a>),
534        }
535        match BeaconExecutionPayloadDesc::deserialize(deserializer)? {
536            BeaconExecutionPayloadDesc::V4(payload) => Ok(Self::V4(payload)),
537            BeaconExecutionPayloadDesc::V3(payload) => Ok(Self::V3(payload)),
538            BeaconExecutionPayloadDesc::V2(payload) => Ok(Self::V2(payload)),
539            BeaconExecutionPayloadDesc::V1(payload) => Ok(Self::V1(payload)),
540        }
541    }
542}
543
544impl<'a> From<BeaconExecutionPayload<'a>> for ExecutionPayload {
545    fn from(payload: BeaconExecutionPayload<'a>) -> Self {
546        match payload {
547            BeaconExecutionPayload::V1(payload) => Self::V1(ExecutionPayloadV1::from(payload)),
548            BeaconExecutionPayload::V2(payload) => Self::V2(ExecutionPayloadV2::from(payload)),
549            BeaconExecutionPayload::V3(payload) => Self::V3(ExecutionPayloadV3::from(payload)),
550            BeaconExecutionPayload::V4(payload) => Self::V4(ExecutionPayloadV4::from(payload)),
551        }
552    }
553}
554
555impl<'a> From<&'a ExecutionPayload> for BeaconExecutionPayload<'a> {
556    fn from(value: &'a ExecutionPayload) -> Self {
557        match value {
558            ExecutionPayload::V1(payload) => {
559                BeaconExecutionPayload::V1(BeaconExecutionPayloadV1::from(payload))
560            }
561            ExecutionPayload::V2(payload) => {
562                BeaconExecutionPayload::V2(BeaconExecutionPayloadV2::from(payload))
563            }
564            ExecutionPayload::V3(payload) => {
565                BeaconExecutionPayload::V3(BeaconExecutionPayloadV3::from(payload))
566            }
567            ExecutionPayload::V4(payload) => {
568                BeaconExecutionPayload::V4(BeaconExecutionPayloadV4::from(payload))
569            }
570        }
571    }
572}
573
574impl SerializeAs<ExecutionPayload> for BeaconExecutionPayload<'_> {
575    fn serialize_as<S>(source: &ExecutionPayload, serializer: S) -> Result<S::Ok, S::Error>
576    where
577        S: Serializer,
578    {
579        beacon_payload::serialize(source, serializer)
580    }
581}
582
583impl<'de> DeserializeAs<'de, ExecutionPayload> for BeaconExecutionPayload<'de> {
584    fn deserialize_as<D>(deserializer: D) -> Result<ExecutionPayload, D::Error>
585    where
586        D: Deserializer<'de>,
587    {
588        beacon_payload::deserialize(deserializer)
589    }
590}
591
592/// Module providing serialization and deserialization support for the beacon API payload
593/// attributes.
594pub mod beacon_payload {
595    use super::*;
596
597    /// Serialize the payload attributes for the beacon API.
598    pub fn serialize<S>(
599        payload_attributes: &ExecutionPayload,
600        serializer: S,
601    ) -> Result<S::Ok, S::Error>
602    where
603        S: Serializer,
604    {
605        BeaconExecutionPayload::from(payload_attributes).serialize(serializer)
606    }
607
608    /// Deserialize the payload attributes for the beacon API.
609    pub fn deserialize<'de, D>(deserializer: D) -> Result<ExecutionPayload, D::Error>
610    where
611        D: Deserializer<'de>,
612    {
613        BeaconExecutionPayload::deserialize(deserializer).map(Into::into)
614    }
615}
616
617/// Helper for deserializing an execution layer [`ExecutionPayload`] payload from the consensus
618/// layer format (snake_case)
619pub fn execution_payload_from_beacon_str(val: &str) -> Result<ExecutionPayload, serde_json::Error> {
620    #[derive(Deserialize)]
621    #[serde(transparent)]
622    struct E {
623        #[serde(deserialize_with = "beacon_payload::deserialize")]
624        payload: ExecutionPayload,
625    }
626    serde_json::from_str::<E>(val).map(|val| val.payload)
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632    use similar_asserts::assert_eq;
633
634    #[test]
635    fn serde_get_payload_header_response() {
636        let s = r#"{"version":"bellatrix","data":{"message":{"header":{"parent_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","fee_recipient":"0xabcf8e0d4e9587369b2301d0790347320302cc09","state_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","receipts_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prev_randao":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","block_number":"1","gas_limit":"1","gas_used":"1","timestamp":"1","extra_data":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","base_fee_per_gas":"1","block_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","transactions_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},"value":"1","pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"},"signature":"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"}}"#;
637        let resp: GetExecutionPayloadHeaderResponse = serde_json::from_str(s).unwrap();
638        let json: serde_json::Value = serde_json::from_str(s).unwrap();
639        assert_eq!(json, serde_json::to_value(resp).unwrap());
640    }
641
642    #[test]
643    fn serde_payload_header() {
644        let s = r#"{"parent_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","fee_recipient":"0xabcf8e0d4e9587369b2301d0790347320302cc09","state_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","receipts_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","logs_bloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prev_randao":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","block_number":"1","gas_limit":"1","gas_used":"1","timestamp":"1","extra_data":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","base_fee_per_gas":"1","block_hash":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2","transactions_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}"#;
645        let header: ExecutionPayloadHeader = serde_json::from_str(s).unwrap();
646        let json: serde_json::Value = serde_json::from_str(s).unwrap();
647        assert_eq!(json, serde_json::to_value(header).unwrap());
648    }
649
650    #[test]
651    fn test_execution_payload_from_beacon_str() {
652        // Test V1 payload
653        let v1_payload_str = r#"{
654            "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
655            "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
656            "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
657            "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
658            "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
659            "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
660            "block_number": "1",
661            "gas_limit": "1000",
662            "gas_used": "500",
663            "timestamp": "1234567890",
664            "extra_data": "0x",
665            "base_fee_per_gas": "1000000000",
666            "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
667            "transactions": ["0x02f878831469668303f51d843b9ac9f9843b9aca0082520894c93269b73096998db66be0441e836d873535cb9c8894a19041886f000080c001a031cc29234036afbf9a1fb9476b463367cb1f957ac0b919b69bbc798436e604aaa018c4e9c3914eb27aadd0b91e10b18655739fcf8c1fc398763a9f1beecb8ddc86"]
668        }"#;
669
670        let payload = execution_payload_from_beacon_str(v1_payload_str).unwrap();
671        match payload {
672            ExecutionPayload::V1(v1) => {
673                assert_eq!(v1.block_number, 1);
674                assert_eq!(v1.gas_limit, 1000);
675                assert_eq!(v1.gas_used, 500);
676                assert_eq!(v1.timestamp, 1234567890);
677                assert_eq!(v1.base_fee_per_gas, U256::from(1000000000u64));
678                assert_eq!(v1.transactions.len(), 1);
679            }
680            _ => panic!("Expected V1 payload"),
681        }
682
683        // Test V2 payload with withdrawals
684        let v2_payload_str = r#"{
685            "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
686            "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
687            "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
688            "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
689            "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
690            "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
691            "block_number": "2",
692            "gas_limit": "2000",
693            "gas_used": "1000",
694            "timestamp": "1234567891",
695            "extra_data": "0x",
696            "base_fee_per_gas": "2000000000",
697            "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
698            "transactions": [],
699            "withdrawals": [
700                {
701                    "index": "0",
702                    "validator_index": "0",
703                    "address": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
704                    "amount": "32000000000"
705                }
706            ]
707        }"#;
708
709        let payload = execution_payload_from_beacon_str(v2_payload_str).unwrap();
710        match payload {
711            ExecutionPayload::V2(v2) => {
712                assert_eq!(v2.payload_inner.block_number, 2);
713                assert_eq!(v2.payload_inner.gas_limit, 2000);
714                assert_eq!(v2.payload_inner.gas_used, 1000);
715                assert_eq!(v2.withdrawals.len(), 1);
716                assert_eq!(v2.withdrawals[0].index, 0);
717                assert_eq!(v2.withdrawals[0].amount, 32000000000);
718            }
719            _ => panic!("Expected V2 payload"),
720        }
721
722        // Test V3 payload with blob gas fields
723        let v3_payload_str = r#"{
724            "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
725            "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
726            "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
727            "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
728            "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
729            "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
730            "block_number": "3",
731            "gas_limit": "3000",
732            "gas_used": "1500",
733            "timestamp": "1234567892",
734            "extra_data": "0x",
735            "base_fee_per_gas": "3000000000",
736            "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
737            "transactions": [],
738            "withdrawals": [],
739            "blob_gas_used": "131072",
740            "excess_blob_gas": "262144"
741        }"#;
742
743        let payload = execution_payload_from_beacon_str(v3_payload_str).unwrap();
744        match payload {
745            ExecutionPayload::V3(v3) => {
746                assert_eq!(v3.payload_inner.payload_inner.block_number, 3);
747                assert_eq!(v3.payload_inner.payload_inner.gas_limit, 3000);
748                assert_eq!(v3.payload_inner.payload_inner.gas_used, 1500);
749                assert_eq!(v3.blob_gas_used, 131072);
750                assert_eq!(v3.excess_blob_gas, 262144);
751            }
752            _ => panic!("Expected V3 payload"),
753        }
754
755        // Test invalid JSON should return error
756        let invalid_json = r#"{ invalid json }"#;
757        assert!(execution_payload_from_beacon_str(invalid_json).is_err());
758    }
759
760    #[test]
761    fn test_extract_payload_from_beacon_block() {
762        // Extracted from https://light-mainnet.beaconcha.in/slot/0x6ceadbf2a6adbbd64cbec33fdebbc582f25171cd30ac43f641cbe76ac7313ddf with only 2 transactions
763        let beacon_block_json = r#"{
764        "message": {
765            "slot": "12225729",
766            "proposer_index": "496520",
767            "parent_root": "0x462f4abf9b6881724e6489085b3bb3931312e31ffb43f7cec3d0ee624dc2b58e",
768            "state_root": "0x2c6e3ff0b0f7bc33b30a020e75e69c2bba26fb42a7e234e8275e655170925a71",
769            "body": {
770                "randao_reveal": "0x825dc181628713b55f40ed3f489be0c60f0513f88eecb25c7aa512ad24b912b3929bdf1930b50af4c18fb8b5f490352218a1c25adc01f7c3aaa50f982d762f589b4f5b6806e1d37e3f70af7afe990d1b1e8e337ac67b53bb7896f2052ecfccc1",
771                "eth1_data": {
772                    "deposit_root": "0x2ebc563cabdbbacbc56f0de1d2d1c2d5315a4b071fcd8566aabbf0a45161c64e",
773                    "deposit_count": "2045305",
774                    "block_hash": "0x0958d83550263ff0d9f9a0bc5ea3cd2a136e0933b6f43cbb17f36e4da8d809b1"
775                },
776                "graffiti": "0x52502d4e502076312e31372e3000000000000000000000000000000000000000",
777                "proposer_slashings": [],
778                "attester_slashings": [],
779                "attestations": [],
780                "deposits": [],
781                "voluntary_exits": [],
782                "sync_aggregate": {
783                    "sync_committee_bits": "0x71b7f7596e64ef7f7ef4f938e9f68abfbfe95bff09393315bb93bbec7f7ef27effa4c7f25ba7cbdb87efbbf73fdaebb9efefeb3ef7fff8effafdd7aff5677bfc",
784                    "sync_committee_signature": "0xb45afdccf46b3518c295407594d82fcfd7fbff767f1b7bb2e7c9bdc8a0229232d201247b449d4bddf01fc974ce0b57601987fb401bb346062e53981cfb81dd6f9c519d645248a46ceba695c2d9630cfc68b26efc35f6ca14c49af9170581ad90"
785                },
786                "execution_payload": {
787                    "parent_hash": "0x3a798cf01d2c58af71b4d00f6b343c1faa88a4e8350d763d181928205ece05fa",
788                    "fee_recipient": "0xdadB0d80178819F2319190D340ce9A924f783711",
789                    "state_root": "0xf258006fe790a654326ceb30933e4216cd8cc2087b16f5189c8ac316d22b918f",
790                    "receipts_root": "0xf8e75ccce80b590f6ac30b859f308edab28c9d87ed8ad50d257902df4ba05ca5",
791                    "logs_bloom": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
792                    "prev_randao": "0x6a4900e9b3958061c0d7fff943a7cd75d9c13f1ba16bd4f9b7dd31b72a82cc11",
793                    "block_number": "23003311",
794                    "gas_limit": "45043901",
795                    "gas_used": "41421505",
796                    "timestamp": "1753532771",
797                    "extra_data": "0x4275696c6465724e6574202842656176657229",
798                    "base_fee_per_gas": "236192093",
799                    "block_hash": "0xa46feca5c8c498c9bf9741f3716d935b25a1a7ff2961d5d1e692f1e97f93a2ca",
800                    "transactions": [
801                        "0x02f901540182e0948505dec6f0ec8505dec6f0ec8307a12094360e051a25ca6decd2f0e91ea4c179a96c0e565e80b8e4ccf22927000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000bc396689893d065f41bc2c6ecbee5e008523344700000000000000000000000000000000000000000000000012651a94c4e78f2200000000000000000000000000000000000000000000034519cd0a9daa3a356e000000000000000000000000cd83055557536eff25fd0eafbc56e74a1b4260b30000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000000c001a0c45c9362e16382b20cc8f04599743f8cdb52031868251c5e4baa8add569019f1a02558d71f50ed265f7d63dde46e8ae477d58d833c36dfe9b4e7eb8783732cbf63",
802                        "0x02f90405018265c38084151e020b8303ca8894a69babef1ca67a37ffaf7a485dfff3382056e78c83db9700b9014478e111f600000000000000000000000039807fc9a64a376b99b1cebde2e79e3826d39aa1000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c42f1c6b50000000000000000000000000000000000000000000000000abd98bd97ba9e63c00000000000000000000000000000000000000000000000033f578d0b9b5b4000000000000000000000000000000000000000000000402d44ba9f99ee186b7d50000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006884c963ff8000000000000000000000000000000000000000000000000000000001227b00000000000000000000000000000000000000000000000000000000f90251d69439807fc9a64a376b99b1cebde2e79e3826d39aa1c0f85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a00cb865ff1951c90111975d77bc75fa8312f25b08bb19b908f6b9c43691ac0cafa075245230289a9f0bf73a6c59aef6651b98b3833a62a3c0bd9ab6b0dec8ed4d8ff8dd9411b815efb8f581194ae79006d24e0d814b7697f6f8c6a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000004a0000000000000000000000000000000000000000000000000000000000000001ba0000000000000000000000000000000000000000000000000000000000000001ca02f2606b2c0d121a5cc1b59088ba7234e9d1c805f41724c938a2661d69532e0e9f8fe94dac17f958d2ee523a2206206994597c13d831ec7f8e7a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000000004a0000000000000000000000000000000000000000000000000000000000000000aa0169228ca33ea854d54aa1e506e59ec687f618a41074f5f5de937a0e9c6343e5aa035d7fb7665514f774d2c2df607e197eb8674b6e63d2638472758647a2e67406aa03ad2db55fe5657fe773e3b7111e43f4b662a181a20e875b3b8be52dd9f0e233380a00cc4b1ab039d330bff37a79b961104004056d1fdf24ecf370036b453fdd4f2c7a0197e3c66c1f3fbaf9cb07902e38ebb451fd9f98e5a5a283d21716cfe3dc761fa"
803                        ]
804                    }
805                }
806            },
807            "signature": "0x8a9cfe747dbb5d6ee1538638b2adfc304c8bcbeb03f489756ca7dc7a12081df892f38b924d19c9f5530c746b86a34beb019070bb7707de5a8efc8bdab8ca5668d7bb0e31c5ffd24913d23c80a6f6f70ba89e280dd46d19d6128ac7f42ffee93e"
808
809        }"#;
810
811        let beacon_block: BeaconBlockData =
812            serde_json::from_str(beacon_block_json).expect("Failed to deserialize beacon block");
813
814        let execution_payload = beacon_block.execution_payload();
815
816        match execution_payload {
817            ExecutionPayload::V1(v1) => {
818                assert_eq!(v1.block_number, 23003311);
819                assert_eq!(v1.gas_limit, 45043901);
820                assert_eq!(v1.gas_used, 41421505);
821                assert_eq!(v1.timestamp, 1753532771);
822                assert_eq!(
823                    v1.parent_hash.to_string(),
824                    "0x3a798cf01d2c58af71b4d00f6b343c1faa88a4e8350d763d181928205ece05fa"
825                );
826                assert_eq!(
827                    v1.fee_recipient.to_string().to_lowercase(),
828                    "0xdadb0d80178819f2319190d340ce9a924f783711"
829                );
830                assert_eq!(
831                    v1.block_hash.to_string(),
832                    "0xa46feca5c8c498c9bf9741f3716d935b25a1a7ff2961d5d1e692f1e97f93a2ca"
833                );
834
835                // Verify 2 transaction were included
836                assert_eq!(v1.transactions.len(), 2);
837                assert!(v1.transactions[0].to_string().starts_with("0x02f901540182e094"));
838            }
839            ExecutionPayload::V2(_) => panic!("Expected V1 payload, got V2"),
840            ExecutionPayload::V3(_) => panic!("Expected V1 payload, got V3"),
841            ExecutionPayload::V4(_) => panic!("Expected V1 payload, got V4"),
842        }
843    }
844
845    #[test]
846    fn serde_beacon_payload_attributes_without_slot_number() {
847        use alloy_rpc_types_engine::PayloadAttributes;
848
849        let json = r#"{
850            "timestamp": "1234",
851            "prev_randao": "0x0000000000000000000000000000000000000000000000000000000000000000",
852            "suggested_fee_recipient": "0x0000000000000000000000000000000000000000"
853        }"#;
854
855        let attrs: BeaconPayloadAttributes = serde_json::from_str(json).unwrap();
856        assert_eq!(attrs.timestamp, 1234);
857        assert!(attrs.slot_number.is_none());
858
859        let engine_attrs = PayloadAttributes {
860            timestamp: attrs.timestamp,
861            prev_randao: attrs.prev_randao,
862            suggested_fee_recipient: attrs.suggested_fee_recipient,
863            withdrawals: attrs.withdrawals,
864            parent_beacon_block_root: attrs.parent_beacon_block_root,
865            slot_number: attrs.slot_number,
866        };
867        assert!(engine_attrs.slot_number.is_none());
868    }
869}