Skip to main content

alloy_rpc_types_engine/
payload.rs

1//! Payload types.
2
3use crate::{CancunPayloadFields, ExecutionPayloadSidecar, PayloadError, PraguePayloadFields};
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8use alloy_consensus::{
9    constants::MAXIMUM_EXTRA_DATA_SIZE, Blob, Block, BlockBody, BlockHeader, Bytes48, Header,
10    HeaderInfo, Transaction, EMPTY_OMMER_ROOT_HASH,
11};
12#[cfg(feature = "kzg")]
13use alloy_eips::eip4844::{AsAlloy, AsCkzg};
14use alloy_eips::{
15    calc_next_block_base_fee,
16    eip1559::BaseFeeParams,
17    eip2718::{Decodable2718, Eip2718Result, Encodable2718, WithEncoded},
18    eip4844::BlobTransactionSidecar,
19    eip4895::{Withdrawal, Withdrawals},
20    eip7594::{BlobTransactionSidecarEip7594, CELLS_PER_EXT_BLOB},
21    eip7685::Requests,
22    eip7840::BlobParams,
23    eip7928::EMPTY_BLOCK_ACCESS_LIST_HASH,
24    BlockNumHash,
25};
26use alloy_primitives::{keccak256, Address, Bloom, Bytes, Sealable, Sealed, B256, B64, U256};
27use core::iter::{FromIterator, IntoIterator};
28
29/// The execution payload body response that allows for `null` values.
30pub type ExecutionPayloadBodiesV1 = Vec<Option<ExecutionPayloadBodyV1>>;
31
32/// The execution payload body V2 response that allows for `null` values.
33///
34/// See also: <https://eips.ethereum.org/EIPS/eip-7928>
35pub type ExecutionPayloadBodiesV2 = Vec<Option<ExecutionPayloadBodyV2>>;
36
37/// And 8-byte identifier for an execution payload.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
41pub struct PayloadId(pub B64);
42
43#[cfg(feature = "ssz")]
44impl ssz::Encode for PayloadId {
45    fn is_ssz_fixed_len() -> bool {
46        <B64 as ssz::Encode>::is_ssz_fixed_len()
47    }
48
49    fn ssz_fixed_len() -> usize {
50        <B64 as ssz::Encode>::ssz_fixed_len()
51    }
52
53    fn ssz_bytes_len(&self) -> usize {
54        ssz::Encode::ssz_bytes_len(&self.0)
55    }
56
57    fn ssz_append(&self, buf: &mut Vec<u8>) {
58        ssz::Encode::ssz_append(&self.0, buf);
59    }
60}
61
62#[cfg(feature = "ssz")]
63impl ssz::Decode for PayloadId {
64    fn is_ssz_fixed_len() -> bool {
65        <B64 as ssz::Decode>::is_ssz_fixed_len()
66    }
67
68    fn ssz_fixed_len() -> usize {
69        <B64 as ssz::Decode>::ssz_fixed_len()
70    }
71
72    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
73        <B64 as ssz::Decode>::from_ssz_bytes(bytes).map(Self)
74    }
75}
76
77// === impl PayloadId ===
78
79impl PayloadId {
80    /// Creates a new payload id from the given identifier.
81    pub fn new(id: [u8; 8]) -> Self {
82        Self(B64::from(id))
83    }
84}
85
86impl core::fmt::Display for PayloadId {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        self.0.fmt(f)
89    }
90}
91
92impl core::str::FromStr for PayloadId {
93    type Err = <B64 as core::str::FromStr>::Err;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        s.parse().map(Self)
97    }
98}
99
100impl From<B64> for PayloadId {
101    fn from(value: B64) -> Self {
102        Self(value)
103    }
104}
105
106/// Extra fields for payload construction.
107#[derive(Clone, Debug, Default, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
110#[non_exhaustive]
111pub struct PayloadExtras {
112    /// The block access list bytes.
113    pub bal: Option<Bytes>,
114}
115
116impl From<Option<Bytes>> for PayloadExtras {
117    fn from(bal: Option<Bytes>) -> Self {
118        Self { bal }
119    }
120}
121
122impl From<Bytes> for PayloadExtras {
123    fn from(bal: Bytes) -> Self {
124        Self { bal: Some(bal) }
125    }
126}
127
128/// This represents the `executionPayload` field in the return value of `engine_getPayloadV2`,
129/// specified as:
130///
131/// - `executionPayload`: `ExecutionPayloadV1` | `ExecutionPayloadV2` where:
132///   - `ExecutionPayloadV1` **MUST** be returned if the payload `timestamp` is lower than the
133///     Shanghai timestamp
134///   - `ExecutionPayloadV2` **MUST** be returned if the payload `timestamp` is greater or equal to
135///     the Shanghai timestamp
136///
137/// See:
138/// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/shanghai.md#response>
139#[derive(Clone, Debug, PartialEq, Eq)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize))]
141#[cfg_attr(feature = "serde", serde(untagged))]
142#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
143pub enum ExecutionPayloadFieldV2 {
144    /// V1 payload
145    V1(ExecutionPayloadV1),
146    /// V2 payload
147    V2(ExecutionPayloadV2),
148}
149
150// Deserializes untagged ExecutionPayloadFieldV2 as V2 if withdrawals are present, V1 otherwise.
151// A derived untagged impl would try V1 first, which also matches V2 input and drops withdrawals.
152#[cfg(feature = "serde")]
153impl<'de> serde::Deserialize<'de> for ExecutionPayloadFieldV2 {
154    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
155    where
156        D: serde::Deserializer<'de>,
157    {
158        #[derive(serde::Deserialize)]
159        struct Helper {
160            #[serde(flatten)]
161            payload_inner: ExecutionPayloadV1,
162            withdrawals: Option<Vec<Withdrawal>>,
163        }
164
165        let helper = Helper::deserialize(deserializer)?;
166        Ok(match helper.withdrawals {
167            Some(withdrawals) => {
168                Self::V2(ExecutionPayloadV2 { payload_inner: helper.payload_inner, withdrawals })
169            }
170            None => Self::V1(helper.payload_inner),
171        })
172    }
173}
174
175impl ExecutionPayloadFieldV2 {
176    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadFieldV2`].
177    ///
178    /// See also:
179    ///  - [`ExecutionPayloadV1::from_block_unchecked`].
180    ///  - [`ExecutionPayloadV2::from_block_unchecked`].
181    ///
182    /// If the block body contains withdrawals this returns [`ExecutionPayloadFieldV2::V2`].
183    ///
184    /// Note: This re-calculates the block hash.
185    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
186    where
187        T: Encodable2718,
188        H: BlockHeader + Sealable,
189    {
190        Self::from_block_unchecked(block.hash_slow(), block)
191    }
192
193    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadFieldV2`] using the given block
194    /// hash.
195    ///
196    /// See also:
197    ///  - [`ExecutionPayloadV1::from_block_unchecked`].
198    ///  - [`ExecutionPayloadV2::from_block_unchecked`].
199    ///
200    /// If the block body contains withdrawals this returns [`ExecutionPayloadFieldV2::V2`].
201    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
202    where
203        T: Encodable2718,
204        H: BlockHeader,
205    {
206        if block.body.withdrawals.is_some() {
207            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
208        } else {
209            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
210        }
211    }
212
213    /// Returns the inner [ExecutionPayloadV1]
214    pub fn into_v1_payload(self) -> ExecutionPayloadV1 {
215        match self {
216            Self::V1(payload) => payload,
217            Self::V2(payload) => payload.payload_inner,
218        }
219    }
220
221    /// Converts this payload variant into the corresponding [ExecutionPayload]
222    pub fn into_payload(self) -> ExecutionPayload {
223        match self {
224            Self::V1(payload) => ExecutionPayload::V1(payload),
225            Self::V2(payload) => ExecutionPayload::V2(payload),
226        }
227    }
228}
229
230#[cfg(feature = "ssz")]
231impl ssz::Encode for ExecutionPayloadFieldV2 {
232    fn is_ssz_fixed_len() -> bool {
233        false
234    }
235
236    fn ssz_append(&self, buf: &mut Vec<u8>) {
237        match self {
238            Self::V1(payload) => payload.ssz_append(buf),
239            Self::V2(payload) => payload.ssz_append(buf),
240        }
241    }
242
243    fn ssz_bytes_len(&self) -> usize {
244        match self {
245            Self::V1(payload) => payload.ssz_bytes_len(),
246            Self::V2(payload) => payload.ssz_bytes_len(),
247        }
248    }
249}
250
251#[cfg(feature = "ssz")]
252impl ssz::Decode for ExecutionPayloadFieldV2 {
253    fn is_ssz_fixed_len() -> bool {
254        false
255    }
256
257    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
258        match <ExecutionPayloadV2 as ssz::Decode>::from_ssz_bytes(bytes) {
259            Ok(payload) => Ok(Self::V2(payload)),
260            Err(_) => <ExecutionPayloadV1 as ssz::Decode>::from_ssz_bytes(bytes).map(Self::V1),
261        }
262    }
263}
264
265/// This is the input to `engine_newPayloadV2`, which may or may not have a withdrawals field.
266#[derive(Clone, Debug, PartialEq, Eq)]
267#[cfg_attr(feature = "serde", derive(serde::Serialize))]
268#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
269#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode))]
270#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
271pub struct ExecutionPayloadInputV2 {
272    /// The V1 execution payload
273    #[cfg_attr(feature = "serde", serde(flatten))]
274    pub execution_payload: ExecutionPayloadV1,
275    /// The payload withdrawals
276    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
277    pub withdrawals: Option<Vec<Withdrawal>>,
278}
279
280#[cfg(feature = "serde")]
281impl<'de> serde::Deserialize<'de> for ExecutionPayloadInputV2 {
282    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283    where
284        D: serde::Deserializer<'de>,
285    {
286        #[derive(serde::Deserialize)]
287        #[serde(rename_all = "camelCase", deny_unknown_fields)]
288        struct Helper {
289            parent_hash: B256,
290            fee_recipient: Address,
291            state_root: B256,
292            receipts_root: B256,
293            logs_bloom: Bloom,
294            prev_randao: B256,
295            #[serde(with = "alloy_serde::quantity")]
296            block_number: u64,
297            #[serde(with = "alloy_serde::quantity")]
298            gas_limit: u64,
299            #[serde(with = "alloy_serde::quantity")]
300            gas_used: u64,
301            #[serde(with = "alloy_serde::quantity")]
302            timestamp: u64,
303            extra_data: Bytes,
304            base_fee_per_gas: U256,
305            block_hash: B256,
306            transactions: Vec<Bytes>,
307            withdrawals: Option<Vec<Withdrawal>>,
308        }
309
310        let helper = Helper::deserialize(deserializer)?;
311        Ok(Self {
312            execution_payload: ExecutionPayloadV1 {
313                parent_hash: helper.parent_hash,
314                fee_recipient: helper.fee_recipient,
315                state_root: helper.state_root,
316                receipts_root: helper.receipts_root,
317                logs_bloom: helper.logs_bloom,
318                prev_randao: helper.prev_randao,
319                block_number: helper.block_number,
320                gas_limit: helper.gas_limit,
321                gas_used: helper.gas_used,
322                timestamp: helper.timestamp,
323                extra_data: helper.extra_data,
324                base_fee_per_gas: helper.base_fee_per_gas,
325                block_hash: helper.block_hash,
326                transactions: helper.transactions,
327            },
328            withdrawals: helper.withdrawals,
329        })
330    }
331}
332
333impl ExecutionPayloadInputV2 {
334    /// Converts [`ExecutionPayloadInputV2`] to [`ExecutionPayload`]
335    pub fn into_payload(self) -> ExecutionPayload {
336        match self.withdrawals {
337            Some(withdrawals) => ExecutionPayload::V2(ExecutionPayloadV2 {
338                payload_inner: self.execution_payload,
339                withdrawals,
340            }),
341            None => ExecutionPayload::V1(self.execution_payload),
342        }
343    }
344}
345
346impl From<ExecutionPayloadInputV2> for ExecutionPayload {
347    fn from(input: ExecutionPayloadInputV2) -> Self {
348        input.into_payload()
349    }
350}
351
352/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
353/// V2.
354///
355/// See also:
356/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md#engine_getpayloadv2>
357#[derive(Clone, Debug, PartialEq, Eq)]
358#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
359#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
360#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
361#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
362pub struct ExecutionPayloadEnvelopeV2 {
363    /// Execution payload, which could be either V1 or V2
364    ///
365    /// V1 (_NO_ withdrawals) MUST be returned if the payload timestamp is lower than the Shanghai
366    /// timestamp
367    ///
368    /// V2 (_WITH_ withdrawals) MUST be returned if the payload timestamp is greater or equal to
369    /// the Shanghai timestamp
370    pub execution_payload: ExecutionPayloadFieldV2,
371    /// The expected value to be received by the feeRecipient in wei
372    pub block_value: U256,
373}
374
375impl ExecutionPayloadEnvelopeV2 {
376    /// Returns the [ExecutionPayload] for the `engine_getPayloadV1` endpoint
377    pub fn into_v1_payload(self) -> ExecutionPayloadV1 {
378        self.execution_payload.into_v1_payload()
379    }
380}
381
382/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
383/// V3.
384///
385/// See also:
386/// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#response-2>
387#[derive(Clone, Debug, PartialEq, Eq)]
388#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
389#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
390#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
391#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
392pub struct ExecutionPayloadEnvelopeV3 {
393    /// Execution payload V3
394    pub execution_payload: ExecutionPayloadV3,
395    /// The expected value to be received by the feeRecipient in wei
396    pub block_value: U256,
397    /// The blobs, commitments, and proofs associated with the executed payload.
398    pub blobs_bundle: BlobsBundleV1,
399    /// Introduced in V3, this represents a suggestion from the execution layer if the payload
400    /// should be used instead of an externally provided one.
401    pub should_override_builder: bool,
402}
403
404/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
405/// V4.
406///
407/// See also:
408/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md#engine_getpayloadv4>
409#[derive(Clone, Debug, PartialEq, Eq, derive_more::Deref, derive_more::DerefMut)]
410#[cfg_attr(feature = "serde", derive(serde::Serialize))]
411#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
412#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
413pub struct ExecutionPayloadEnvelopeV4 {
414    /// Inner [`ExecutionPayloadEnvelopeV3`].
415    #[deref]
416    #[deref_mut]
417    #[cfg_attr(feature = "serde", serde(flatten))]
418    pub envelope_inner: ExecutionPayloadEnvelopeV3,
419
420    /// A list of opaque [EIP-7685][eip7685] requests.
421    ///
422    /// [eip7685]: https://eips.ethereum.org/EIPS/eip-7685
423    pub execution_requests: Requests,
424}
425
426#[cfg(feature = "ssz")]
427impl ssz::Encode for ExecutionPayloadEnvelopeV4 {
428    fn is_ssz_fixed_len() -> bool {
429        false
430    }
431
432    fn ssz_append(&self, buf: &mut Vec<u8>) {
433        let offset = <ExecutionPayloadV3 as ssz::Encode>::ssz_fixed_len()
434            + <U256 as ssz::Encode>::ssz_fixed_len()
435            + <BlobsBundleV1 as ssz::Encode>::ssz_fixed_len()
436            + <bool as ssz::Encode>::ssz_fixed_len()
437            + <Requests as ssz::Encode>::ssz_fixed_len();
438        let mut encoder = ssz::SszEncoder::container(buf, offset);
439
440        encoder.append(&self.envelope_inner.execution_payload);
441        encoder.append(&self.envelope_inner.block_value);
442        encoder.append(&self.envelope_inner.blobs_bundle);
443        encoder.append(&self.envelope_inner.should_override_builder);
444        encoder.append(&self.execution_requests);
445
446        encoder.finalize();
447    }
448
449    fn ssz_bytes_len(&self) -> usize {
450        let fixed_section_len = <ExecutionPayloadV3 as ssz::Encode>::ssz_fixed_len()
451            + <U256 as ssz::Encode>::ssz_fixed_len()
452            + <BlobsBundleV1 as ssz::Encode>::ssz_fixed_len()
453            + <bool as ssz::Encode>::ssz_fixed_len()
454            + <Requests as ssz::Encode>::ssz_fixed_len();
455
456        fixed_section_len
457            + self.envelope_inner.execution_payload.ssz_bytes_len()
458            + self.envelope_inner.blobs_bundle.ssz_bytes_len()
459            + self.execution_requests.ssz_bytes_len()
460    }
461}
462
463#[cfg(feature = "ssz")]
464impl ssz::Decode for ExecutionPayloadEnvelopeV4 {
465    fn is_ssz_fixed_len() -> bool {
466        false
467    }
468
469    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
470        let mut builder = ssz::SszDecoderBuilder::new(bytes);
471
472        builder.register_type::<ExecutionPayloadV3>()?;
473        builder.register_type::<U256>()?;
474        builder.register_type::<BlobsBundleV1>()?;
475        builder.register_type::<bool>()?;
476        builder.register_type::<Requests>()?;
477
478        let mut decoder = builder.build()?;
479        Ok(Self {
480            envelope_inner: ExecutionPayloadEnvelopeV3 {
481                execution_payload: decoder.decode_next()?,
482                block_value: decoder.decode_next()?,
483                blobs_bundle: decoder.decode_next()?,
484                should_override_builder: decoder.decode_next()?,
485            },
486            execution_requests: decoder.decode_next()?,
487        })
488    }
489}
490
491impl ExecutionPayloadEnvelopeV4 {
492    /// Converts this V4 envelope into an [`ExecutionPayload`] and [`ExecutionPayloadSidecar`].
493    ///
494    /// The `parent_beacon_block_root` is required because it is not part of the envelope
495    /// but is needed for the sidecar's [`CancunPayloadFields`].
496    ///
497    /// The versioned hashes are computed from the blobs bundle commitments.
498    pub fn into_payload_and_sidecar(
499        self,
500        parent_beacon_block_root: B256,
501    ) -> (ExecutionPayload, ExecutionPayloadSidecar) {
502        let versioned_hashes = self.blobs_bundle.versioned_hashes();
503
504        let cancun_fields = CancunPayloadFields { parent_beacon_block_root, versioned_hashes };
505        let prague_fields = PraguePayloadFields::new(self.execution_requests);
506
507        (
508            ExecutionPayload::V3(self.envelope_inner.execution_payload),
509            ExecutionPayloadSidecar::v4(cancun_fields, prague_fields),
510        )
511    }
512
513    /// Converts this V4 envelope into a [`ExecutionPayloadEnvelopeV5`] by computing EIP-7594
514    /// cell proofs for the blobs bundle.
515    ///
516    /// This uses the default KZG settings. See [`Self::try_into_v5_with_settings`] for custom
517    /// settings.
518    ///
519    /// # Errors
520    ///
521    /// Returns an error if KZG proof computation fails.
522    #[cfg(feature = "kzg")]
523    pub fn try_into_v5(
524        self,
525    ) -> Result<ExecutionPayloadEnvelopeV5, alloy_eips::eip4844::c_kzg::Error> {
526        self.try_into_v5_with_settings(
527            alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get(),
528        )
529    }
530
531    /// Converts this V4 envelope into a [`ExecutionPayloadEnvelopeV5`] by computing EIP-7594
532    /// cell proofs for the blobs bundle using the provided KZG settings.
533    ///
534    /// # Errors
535    ///
536    /// Returns an error if KZG proof computation fails.
537    #[cfg(feature = "kzg")]
538    pub fn try_into_v5_with_settings(
539        self,
540        settings: &alloy_eips::eip4844::c_kzg::KzgSettings,
541    ) -> Result<ExecutionPayloadEnvelopeV5, alloy_eips::eip4844::c_kzg::Error> {
542        let blobs_bundle = self.envelope_inner.blobs_bundle.try_into_v2_with_settings(settings)?;
543        Ok(ExecutionPayloadEnvelopeV5 {
544            execution_payload: self.envelope_inner.execution_payload,
545            block_value: self.envelope_inner.block_value,
546            blobs_bundle,
547            should_override_builder: self.envelope_inner.should_override_builder,
548            execution_requests: self.execution_requests,
549        })
550    }
551}
552
553#[cfg(feature = "serde")]
554impl<'de> serde::Deserialize<'de> for ExecutionPayloadEnvelopeV4 {
555    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
556    where
557        D: serde::Deserializer<'de>,
558    {
559        #[derive(serde::Deserialize)]
560        #[serde(rename_all = "camelCase")]
561        struct Helper {
562            execution_payload: ExecutionPayloadV3,
563            block_value: U256,
564            blobs_bundle: BlobsBundleV1,
565            should_override_builder: bool,
566            execution_requests: Requests,
567        }
568
569        let helper = Helper::deserialize(deserializer)?;
570        Ok(Self {
571            envelope_inner: ExecutionPayloadEnvelopeV3 {
572                execution_payload: helper.execution_payload,
573                block_value: helper.block_value,
574                blobs_bundle: helper.blobs_bundle,
575                should_override_builder: helper.should_override_builder,
576            },
577            execution_requests: helper.execution_requests,
578        })
579    }
580}
581
582/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
583/// V5.
584///
585/// See also:
586/// <https://github.com/ethereum/execution-apis/blob/a091e7c3b6a5748a8843a1a9130d5fbfc3191a2c/src/engine/osaka.md#engine_getpayloadv5>
587#[derive(Clone, Debug, PartialEq, Eq)]
588#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
589#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
590#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
591#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
592pub struct ExecutionPayloadEnvelopeV5 {
593    /// Execution payload V3
594    pub execution_payload: ExecutionPayloadV3,
595    /// The expected value to be received by the feeRecipient in wei
596    pub block_value: U256,
597    /// The blobs, commitments, and EIP-7594 style cell proofs associated with the executed
598    /// payload. See also: <https://github.com/ethereum/execution-apis/blob/a091e7c3b6a5748a8843a1a9130d5fbfc3191a2c/src/engine/osaka.md#BlobsBundleV2>.
599    pub blobs_bundle: BlobsBundleV2,
600    /// Introduced in V3, this represents a suggestion from the execution layer if the payload
601    /// should be used instead of an externally provided one.
602    pub should_override_builder: bool,
603    /// A list of opaque [EIP-7685][eip7685] requests.
604    ///
605    /// [eip7685]: https://eips.ethereum.org/EIPS/eip-7685
606    pub execution_requests: Requests,
607}
608
609#[cfg(feature = "kzg")]
610impl TryFrom<ExecutionPayloadEnvelopeV4> for ExecutionPayloadEnvelopeV5 {
611    type Error = alloy_eips::eip4844::c_kzg::Error;
612
613    fn try_from(value: ExecutionPayloadEnvelopeV4) -> Result<Self, Self::Error> {
614        value.try_into_v5()
615    }
616}
617
618impl ExecutionPayloadEnvelopeV5 {
619    /// Converts this V5 envelope into an [`ExecutionPayload`] and [`ExecutionPayloadSidecar`].
620    ///
621    /// The `parent_beacon_block_root` is required because it is not part of the envelope
622    /// but is needed for the sidecar's [`CancunPayloadFields`].
623    ///
624    /// The versioned hashes are computed from the blobs bundle commitments.
625    pub fn into_payload_and_sidecar(
626        self,
627        parent_beacon_block_root: B256,
628    ) -> (ExecutionPayload, ExecutionPayloadSidecar) {
629        let versioned_hashes = self.blobs_bundle.versioned_hashes();
630
631        let cancun_fields = CancunPayloadFields { parent_beacon_block_root, versioned_hashes };
632        let prague_fields = PraguePayloadFields::new(self.execution_requests);
633
634        (
635            ExecutionPayload::V3(self.execution_payload),
636            ExecutionPayloadSidecar::v4(cancun_fields, prague_fields),
637        )
638    }
639
640    /// Converts this V5 envelope into a [`ExecutionPayloadEnvelopeV4`] by computing EIP-4844
641    /// blob proofs for the blobs bundle.
642    ///
643    /// This uses the default KZG settings. See [`Self::try_into_v4_with_settings`] for custom
644    /// settings.
645    ///
646    /// # Errors
647    ///
648    /// Returns an error if KZG proof computation fails.
649    #[cfg(feature = "kzg")]
650    pub fn try_into_v4(
651        self,
652    ) -> Result<ExecutionPayloadEnvelopeV4, alloy_eips::eip4844::c_kzg::Error> {
653        self.try_into_v4_with_settings(
654            alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get(),
655        )
656    }
657
658    /// Converts this V5 envelope into a [`ExecutionPayloadEnvelopeV4`] by computing EIP-4844
659    /// blob proofs for the blobs bundle using the provided KZG settings.
660    ///
661    /// # Errors
662    ///
663    /// Returns an error if KZG proof computation fails.
664    #[cfg(feature = "kzg")]
665    pub fn try_into_v4_with_settings(
666        self,
667        settings: &alloy_eips::eip4844::c_kzg::KzgSettings,
668    ) -> Result<ExecutionPayloadEnvelopeV4, alloy_eips::eip4844::c_kzg::Error> {
669        let blobs_bundle = self.blobs_bundle.try_into_v1_with_settings(settings)?;
670        Ok(ExecutionPayloadEnvelopeV4 {
671            envelope_inner: ExecutionPayloadEnvelopeV3 {
672                execution_payload: self.execution_payload,
673                block_value: self.block_value,
674                blobs_bundle,
675                should_override_builder: self.should_override_builder,
676            },
677            execution_requests: self.execution_requests,
678        })
679    }
680}
681
682#[cfg(feature = "kzg")]
683impl TryFrom<ExecutionPayloadEnvelopeV5> for ExecutionPayloadEnvelopeV4 {
684    type Error = alloy_eips::eip4844::c_kzg::Error;
685
686    fn try_from(value: ExecutionPayloadEnvelopeV5) -> Result<Self, Self::Error> {
687        value.try_into_v4()
688    }
689}
690
691/// This structure maps for the return value of `engine_getPayloadV6` of the beacon chain spec.
692///
693/// See also:
694/// <https://github.com/ethereum/execution-apis/blob/7b4d9f62a3fe62b9b8dcb355f1c5a38b5ff084f6/src/engine/amsterdam.md#engine_getpayloadv6>
695#[derive(Clone, Debug, PartialEq, Eq)]
696#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
697#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
698#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
699#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
700pub struct ExecutionPayloadEnvelopeV6 {
701    /// Execution payload V4
702    pub execution_payload: ExecutionPayloadV4,
703    /// The expected value to be received by the feeRecipient in wei
704    pub block_value: U256,
705    /// The blobs, commitments, and EIP-7594 style cell proofs associated with the executed
706    /// payload.
707    ///
708    /// See also: <https://github.com/ethereum/execution-apis/blob/a091e7c3b6a5748a8843a1a9130d5fbfc3191a2c/src/engine/osaka.md#BlobsBundleV2>.
709    pub blobs_bundle: BlobsBundleV2,
710    /// Introduced in V3, this represents a suggestion from the execution layer if the payload
711    /// should be used instead of an externally provided one.
712    pub should_override_builder: bool,
713    /// A list of opaque [EIP-7685][eip7685] requests.
714    ///
715    /// [eip7685]: https://eips.ethereum.org/EIPS/eip-7685
716    pub execution_requests: Requests,
717}
718
719/// This structure maps on the ExecutionPayload structure of the beacon chain spec.
720///
721/// See also: <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/paris.md#executionpayloadv1>
722#[derive(Clone, Debug, PartialEq, Eq)]
723#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
724#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
725#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
726#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
727pub struct ExecutionPayloadV1 {
728    /// The parent hash of the block.
729    pub parent_hash: B256,
730    /// The fee recipient of the block.
731    pub fee_recipient: Address,
732    /// The state root of the block.
733    pub state_root: B256,
734    /// The receipts root of the block.
735    pub receipts_root: B256,
736    /// The logs bloom of the block.
737    pub logs_bloom: Bloom,
738    /// The previous randao of the block.
739    pub prev_randao: B256,
740    /// The block number.
741    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
742    pub block_number: u64,
743    /// The gas limit of the block.
744    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
745    pub gas_limit: u64,
746    /// The gas used of the block.
747    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
748    pub gas_used: u64,
749    /// The timestamp of the block.
750    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
751    pub timestamp: u64,
752    /// The extra data of the block.
753    pub extra_data: Bytes,
754    /// The base fee per gas of the block.
755    pub base_fee_per_gas: U256,
756    /// The block hash of the block.
757    pub block_hash: B256,
758    /// The transactions of the block.
759    pub transactions: Vec<Bytes>,
760}
761
762impl ExecutionPayloadV1 {
763    /// Returns the block number and hash as a [`BlockNumHash`].
764    pub const fn block_num_hash(&self) -> BlockNumHash {
765        BlockNumHash::new(self.block_number, self.block_hash)
766    }
767
768    /// Converts [`ExecutionPayloadV1`] to an unsealed [`Block`].
769    ///
770    /// This does not recompute or compare the payload's advertised [`Self::block_hash`]. Callers
771    /// performing Engine API validation must hash the returned block and compare it separately.
772    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
773        self.try_into_block_with(|tx| {
774            T::decode_2718_exact(tx.as_ref())
775                .map_err(alloy_rlp::Error::from)
776                .map_err(PayloadError::from)
777        })
778    }
779
780    /// Converts [`ExecutionPayloadV1`] to [`Block`] with the given closure.
781    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
782    where
783        F: FnMut(Bytes) -> Result<T, E>,
784        E: Into<PayloadError>,
785    {
786        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
787    }
788
789    /// Converts [`ExecutionPayloadV1`] to [`Block`] with raw [`Bytes`] transactions.
790    ///
791    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
792    /// without any conversion.
793    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
794        self.into_block_raw_with_transactions_root_opt(None)
795    }
796
797    /// Converts [`ExecutionPayloadV1`] to [`Block`] with raw [`Bytes`] transactions using the
798    /// given `transactions_root`.
799    ///
800    /// This is the same as [`Self::into_block_raw`] but allows the caller to provide a
801    /// pre-computed transactions root instead of computing it from the transactions.
802    pub fn into_block_raw_with_transactions_root(
803        self,
804        transactions_root: B256,
805    ) -> Result<Block<Bytes>, PayloadError> {
806        self.into_block_raw_with_transactions_root_opt(Some(transactions_root))
807    }
808
809    /// Converts [`ExecutionPayloadV1`] to [`Block`] with raw [`Bytes`] transactions, optionally
810    /// using the given `transactions_root`.
811    ///
812    /// If `transactions_root` is `None`, it will be computed from the transactions.
813    pub fn into_block_raw_with_transactions_root_opt(
814        self,
815        transactions_root: Option<B256>,
816    ) -> Result<Block<Bytes>, PayloadError> {
817        if self.extra_data.len() > MAXIMUM_EXTRA_DATA_SIZE {
818            return Err(PayloadError::ExtraData(self.extra_data));
819        }
820
821        let transactions_root = transactions_root.unwrap_or_else(|| {
822            alloy_consensus::proofs::ordered_trie_root_encoded(&self.transactions)
823        });
824
825        let header = Header {
826            parent_hash: self.parent_hash,
827            beneficiary: self.fee_recipient,
828            state_root: self.state_root,
829            transactions_root,
830            receipts_root: self.receipts_root,
831            withdrawals_root: None,
832            logs_bloom: self.logs_bloom,
833            number: self.block_number,
834            gas_limit: self.gas_limit,
835            gas_used: self.gas_used,
836            timestamp: self.timestamp,
837            mix_hash: self.prev_randao,
838            // WARNING: It's allowed for a base fee in EIP1559 to increase unbounded. We assume that
839            // it will fit in an u64. This is not always necessarily true, although it is extremely
840            // unlikely not to be the case, a u64 maximum would have 2^64 which equates to 18 ETH
841            // per gas.
842            base_fee_per_gas: Some(
843                self.base_fee_per_gas
844                    .try_into()
845                    .map_err(|_| PayloadError::BaseFee(self.base_fee_per_gas))?,
846            ),
847            blob_gas_used: None,
848            excess_blob_gas: None,
849            parent_beacon_block_root: None,
850            requests_hash: None,
851            block_access_list_hash: None,
852            slot_number: None,
853            extra_data: self.extra_data,
854            // Defaults
855            ommers_hash: EMPTY_OMMER_ROOT_HASH,
856            difficulty: Default::default(),
857            nonce: Default::default(),
858        };
859
860        Ok(Block {
861            header,
862            body: BlockBody { transactions: self.transactions, ommers: vec![], withdrawals: None },
863        })
864    }
865
866    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV1`].
867    ///
868    /// Note: This re-calculates the block hash.
869    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
870    where
871        T: Encodable2718,
872        H: BlockHeader + Sealable,
873    {
874        Self::from_block_unchecked(block.header.hash_slow(), block)
875    }
876
877    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV1`] using the given block hash.
878    ///
879    /// The supplied hash is stored verbatim without checking it against the block.
880    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
881    where
882        T: Encodable2718,
883        H: BlockHeader,
884    {
885        let transactions =
886            block.body.transactions().map(|tx| tx.encoded_2718().into()).collect::<Vec<_>>();
887        Self {
888            parent_hash: block.parent_hash(),
889            fee_recipient: block.beneficiary(),
890            state_root: block.state_root(),
891            receipts_root: block.receipts_root(),
892            logs_bloom: block.logs_bloom(),
893            prev_randao: block.mix_hash().unwrap_or_default(),
894            block_number: block.number(),
895            gas_limit: block.gas_limit(),
896            gas_used: block.gas_used(),
897            timestamp: block.timestamp(),
898            base_fee_per_gas: U256::from(block.base_fee_per_gas().unwrap_or_default()),
899            extra_data: block.header.extra_data().clone(),
900            block_hash,
901            transactions,
902        }
903    }
904
905    /// Calculate base fee for next block according to the EIP-1559 spec.
906    ///
907    /// Returns a `None` if no base fee is set, no EIP-1559 support
908    pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option<u64> {
909        Some(calc_next_block_base_fee(
910            self.gas_used,
911            self.gas_limit,
912            self.base_fee_per_gas.try_into().ok()?,
913            base_fee_params,
914        ))
915    }
916}
917
918impl<T: Decodable2718> TryFrom<ExecutionPayloadV1> for Block<T> {
919    type Error = PayloadError;
920
921    fn try_from(value: ExecutionPayloadV1) -> Result<Self, Self::Error> {
922        value.try_into_block()
923    }
924}
925
926/// This structure maps on the ExecutionPayloadV2 structure of the beacon chain spec.
927///
928/// See also: <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/shanghai.md#executionpayloadv2>
929#[derive(Clone, Debug, PartialEq, Eq)]
930#[cfg_attr(feature = "serde", derive(serde::Serialize))]
931#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
932#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
933pub struct ExecutionPayloadV2 {
934    /// Inner V1 payload
935    #[cfg_attr(feature = "serde", serde(flatten))]
936    pub payload_inner: ExecutionPayloadV1,
937
938    /// Array of [`Withdrawal`] enabled with V2
939    /// See <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/shanghai.md#executionpayloadv2>
940    pub withdrawals: Vec<Withdrawal>,
941}
942
943#[cfg(feature = "serde")]
944impl<'de> serde::Deserialize<'de> for ExecutionPayloadV2 {
945    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
946    where
947        D: serde::Deserializer<'de>,
948    {
949        #[derive(serde::Deserialize)]
950        #[serde(rename_all = "camelCase")]
951        struct Helper {
952            parent_hash: B256,
953            fee_recipient: Address,
954            state_root: B256,
955            receipts_root: B256,
956            logs_bloom: Bloom,
957            prev_randao: B256,
958            #[serde(with = "alloy_serde::quantity")]
959            block_number: u64,
960            #[serde(with = "alloy_serde::quantity")]
961            gas_limit: u64,
962            #[serde(with = "alloy_serde::quantity")]
963            gas_used: u64,
964            #[serde(with = "alloy_serde::quantity")]
965            timestamp: u64,
966            extra_data: Bytes,
967            base_fee_per_gas: U256,
968            block_hash: B256,
969            transactions: Vec<Bytes>,
970            withdrawals: Vec<Withdrawal>,
971        }
972
973        let helper = Helper::deserialize(deserializer)?;
974        Ok(Self {
975            payload_inner: ExecutionPayloadV1 {
976                parent_hash: helper.parent_hash,
977                fee_recipient: helper.fee_recipient,
978                state_root: helper.state_root,
979                receipts_root: helper.receipts_root,
980                logs_bloom: helper.logs_bloom,
981                prev_randao: helper.prev_randao,
982                block_number: helper.block_number,
983                gas_limit: helper.gas_limit,
984                gas_used: helper.gas_used,
985                timestamp: helper.timestamp,
986                extra_data: helper.extra_data,
987                base_fee_per_gas: helper.base_fee_per_gas,
988                block_hash: helper.block_hash,
989                transactions: helper.transactions,
990            },
991            withdrawals: helper.withdrawals,
992        })
993    }
994}
995
996impl ExecutionPayloadV2 {
997    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV2`].
998    ///
999    /// See also [`ExecutionPayloadV1::from_block_unchecked`].
1000    ///
1001    /// If the block does not have any withdrawals, an empty vector is used.
1002    ///
1003    /// Note: This re-calculates the block hash.
1004    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
1005    where
1006        T: Encodable2718,
1007        H: BlockHeader + Sealable,
1008    {
1009        Self::from_block_unchecked(block.header.hash_slow(), block)
1010    }
1011
1012    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV2`] using the given block hash.
1013    ///
1014    /// See also [`ExecutionPayloadV1::from_block_unchecked`].
1015    ///
1016    /// If the block does not have any withdrawals, an empty vector is used.
1017    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
1018    where
1019        T: Encodable2718,
1020        H: BlockHeader,
1021    {
1022        Self {
1023            withdrawals: block
1024                .body
1025                .withdrawals
1026                .clone()
1027                .map(Withdrawals::into_inner)
1028                .unwrap_or_default(),
1029            payload_inner: ExecutionPayloadV1::from_block_unchecked(block_hash, block),
1030        }
1031    }
1032
1033    /// Returns the timestamp for the execution payload.
1034    pub const fn timestamp(&self) -> u64 {
1035        self.payload_inner.timestamp
1036    }
1037
1038    /// Converts [`ExecutionPayloadV2`] to [`ExecutionPayloadInputV2`].
1039    ///
1040    /// An [`ExecutionPayloadInputV2`] should have a [`Some`] withdrawals field if shanghai is
1041    /// active, otherwise the withdrawals field should be [`None`], so the `is_shanghai_active`
1042    /// argument is provided which will either:
1043    /// - include the withdrawals field as [`Some`] if true
1044    /// - set the withdrawals field to [`None`] if false
1045    pub fn into_payload_input_v2(self, is_shanghai_active: bool) -> ExecutionPayloadInputV2 {
1046        ExecutionPayloadInputV2 {
1047            execution_payload: self.payload_inner,
1048            withdrawals: is_shanghai_active.then_some(self.withdrawals),
1049        }
1050    }
1051
1052    /// Converts [`ExecutionPayloadV2`] to [`Block`].
1053    ///
1054    /// This performs the same conversion as the underlying V1 payload, but calculates the
1055    /// withdrawals root and adds withdrawals.
1056    ///
1057    /// See also [`ExecutionPayloadV1::try_into_block`].
1058    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
1059        self.try_into_block_with(|tx| {
1060            T::decode_2718_exact(tx.as_ref())
1061                .map_err(alloy_rlp::Error::from)
1062                .map_err(PayloadError::from)
1063        })
1064    }
1065
1066    /// Converts [`ExecutionPayloadV2`] to [`Block`] with a custom transaction mapper.
1067    ///
1068    /// See also [`ExecutionPayloadV1::try_into_block_with`].
1069    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
1070    where
1071        F: FnMut(Bytes) -> Result<T, E>,
1072        E: Into<PayloadError>,
1073    {
1074        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
1075    }
1076
1077    /// Converts [`ExecutionPayloadV2`] to [`Block`] with raw [`Bytes`] transactions.
1078    ///
1079    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
1080    /// without any conversion.
1081    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
1082        self.into_block_raw_with_transactions_root_opt(None)
1083    }
1084
1085    /// Converts [`ExecutionPayloadV2`] to [`Block`] with raw [`Bytes`] transactions using the
1086    /// given `transactions_root`.
1087    ///
1088    /// See also [`ExecutionPayloadV1::into_block_raw_with_transactions_root`].
1089    pub fn into_block_raw_with_transactions_root(
1090        self,
1091        transactions_root: B256,
1092    ) -> Result<Block<Bytes>, PayloadError> {
1093        self.into_block_raw_with_transactions_root_opt(Some(transactions_root))
1094    }
1095
1096    /// Converts [`ExecutionPayloadV2`] to [`Block`] with raw [`Bytes`] transactions, optionally
1097    /// using the given `transactions_root`.
1098    ///
1099    /// If `transactions_root` is `None`, it will be computed from the transactions.
1100    pub fn into_block_raw_with_transactions_root_opt(
1101        self,
1102        transactions_root: Option<B256>,
1103    ) -> Result<Block<Bytes>, PayloadError> {
1104        let mut base_sealed_block =
1105            self.payload_inner.into_block_raw_with_transactions_root_opt(transactions_root)?;
1106        let withdrawals_root =
1107            alloy_consensus::proofs::calculate_withdrawals_root(&self.withdrawals);
1108        base_sealed_block.body.withdrawals = Some(self.withdrawals.into());
1109        base_sealed_block.header.withdrawals_root = Some(withdrawals_root);
1110        Ok(base_sealed_block)
1111    }
1112}
1113
1114impl<T: Decodable2718> TryFrom<ExecutionPayloadV2> for Block<T> {
1115    type Error = PayloadError;
1116
1117    fn try_from(value: ExecutionPayloadV2) -> Result<Self, Self::Error> {
1118        value.try_into_block()
1119    }
1120}
1121
1122#[cfg(feature = "ssz")]
1123impl ssz::Decode for ExecutionPayloadV2 {
1124    fn is_ssz_fixed_len() -> bool {
1125        false
1126    }
1127
1128    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1129        let mut builder = ssz::SszDecoderBuilder::new(bytes);
1130
1131        builder.register_type::<B256>()?;
1132        builder.register_type::<Address>()?;
1133        builder.register_type::<B256>()?;
1134        builder.register_type::<B256>()?;
1135        builder.register_type::<Bloom>()?;
1136        builder.register_type::<B256>()?;
1137        builder.register_type::<u64>()?;
1138        builder.register_type::<u64>()?;
1139        builder.register_type::<u64>()?;
1140        builder.register_type::<u64>()?;
1141        builder.register_type::<Bytes>()?;
1142        builder.register_type::<U256>()?;
1143        builder.register_type::<B256>()?;
1144        builder.register_type::<Vec<Bytes>>()?;
1145        builder.register_type::<Vec<Withdrawal>>()?;
1146
1147        let mut decoder = builder.build()?;
1148
1149        Ok(Self {
1150            payload_inner: ExecutionPayloadV1 {
1151                parent_hash: decoder.decode_next()?,
1152                fee_recipient: decoder.decode_next()?,
1153                state_root: decoder.decode_next()?,
1154                receipts_root: decoder.decode_next()?,
1155                logs_bloom: decoder.decode_next()?,
1156                prev_randao: decoder.decode_next()?,
1157                block_number: decoder.decode_next()?,
1158                gas_limit: decoder.decode_next()?,
1159                gas_used: decoder.decode_next()?,
1160                timestamp: decoder.decode_next()?,
1161                extra_data: decoder.decode_next()?,
1162                base_fee_per_gas: decoder.decode_next()?,
1163                block_hash: decoder.decode_next()?,
1164                transactions: decoder.decode_next()?,
1165            },
1166            withdrawals: decoder.decode_next()?,
1167        })
1168    }
1169}
1170
1171#[cfg(feature = "ssz")]
1172impl ssz::Encode for ExecutionPayloadV2 {
1173    fn is_ssz_fixed_len() -> bool {
1174        false
1175    }
1176
1177    fn ssz_append(&self, buf: &mut Vec<u8>) {
1178        let offset = <B256 as ssz::Encode>::ssz_fixed_len() * 5
1179            + <Address as ssz::Encode>::ssz_fixed_len()
1180            + <Bloom as ssz::Encode>::ssz_fixed_len()
1181            + <u64 as ssz::Encode>::ssz_fixed_len() * 4
1182            + <U256 as ssz::Encode>::ssz_fixed_len()
1183            + ssz::BYTES_PER_LENGTH_OFFSET * 3;
1184
1185        let mut encoder = ssz::SszEncoder::container(buf, offset);
1186
1187        encoder.append(&self.payload_inner.parent_hash);
1188        encoder.append(&self.payload_inner.fee_recipient);
1189        encoder.append(&self.payload_inner.state_root);
1190        encoder.append(&self.payload_inner.receipts_root);
1191        encoder.append(&self.payload_inner.logs_bloom);
1192        encoder.append(&self.payload_inner.prev_randao);
1193        encoder.append(&self.payload_inner.block_number);
1194        encoder.append(&self.payload_inner.gas_limit);
1195        encoder.append(&self.payload_inner.gas_used);
1196        encoder.append(&self.payload_inner.timestamp);
1197        encoder.append(&self.payload_inner.extra_data);
1198        encoder.append(&self.payload_inner.base_fee_per_gas);
1199        encoder.append(&self.payload_inner.block_hash);
1200        encoder.append(&self.payload_inner.transactions);
1201        encoder.append(&self.withdrawals);
1202
1203        encoder.finalize();
1204    }
1205
1206    fn ssz_bytes_len(&self) -> usize {
1207        <ExecutionPayloadV1 as ssz::Encode>::ssz_bytes_len(&self.payload_inner)
1208            + ssz::BYTES_PER_LENGTH_OFFSET
1209            + self.withdrawals.ssz_bytes_len()
1210    }
1211}
1212
1213/// This structure maps on the ExecutionPayloadV3 structure of the beacon chain spec.
1214///
1215/// See also: <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#executionpayloadv3>
1216#[derive(Clone, Debug, PartialEq, Eq)]
1217#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1218#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
1219#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1220pub struct ExecutionPayloadV3 {
1221    /// Inner V2 payload
1222    #[cfg_attr(feature = "serde", serde(flatten))]
1223    pub payload_inner: ExecutionPayloadV2,
1224
1225    /// Array of hex [`u64`] representing blob gas used, enabled with V3
1226    /// See <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#ExecutionPayloadV3>
1227    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
1228    pub blob_gas_used: u64,
1229    /// Array of hex [`u64`] representing excess blob gas, enabled with V3
1230    /// See <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#ExecutionPayloadV3>
1231    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
1232    pub excess_blob_gas: u64,
1233}
1234
1235#[cfg(feature = "serde")]
1236impl<'de> serde::Deserialize<'de> for ExecutionPayloadV3 {
1237    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1238    where
1239        D: serde::Deserializer<'de>,
1240    {
1241        #[derive(serde::Deserialize)]
1242        #[serde(rename_all = "camelCase")]
1243        struct Helper {
1244            parent_hash: B256,
1245            fee_recipient: Address,
1246            state_root: B256,
1247            receipts_root: B256,
1248            logs_bloom: Bloom,
1249            prev_randao: B256,
1250            #[serde(with = "alloy_serde::quantity")]
1251            block_number: u64,
1252            #[serde(with = "alloy_serde::quantity")]
1253            gas_limit: u64,
1254            #[serde(with = "alloy_serde::quantity")]
1255            gas_used: u64,
1256            #[serde(with = "alloy_serde::quantity")]
1257            timestamp: u64,
1258            extra_data: Bytes,
1259            base_fee_per_gas: U256,
1260            block_hash: B256,
1261            transactions: Vec<Bytes>,
1262            withdrawals: Vec<Withdrawal>,
1263            #[serde(with = "alloy_serde::quantity")]
1264            blob_gas_used: u64,
1265            #[serde(with = "alloy_serde::quantity")]
1266            excess_blob_gas: u64,
1267        }
1268
1269        let helper = Helper::deserialize(deserializer)?;
1270        Ok(Self {
1271            payload_inner: ExecutionPayloadV2 {
1272                payload_inner: ExecutionPayloadV1 {
1273                    parent_hash: helper.parent_hash,
1274                    fee_recipient: helper.fee_recipient,
1275                    state_root: helper.state_root,
1276                    receipts_root: helper.receipts_root,
1277                    logs_bloom: helper.logs_bloom,
1278                    prev_randao: helper.prev_randao,
1279                    block_number: helper.block_number,
1280                    gas_limit: helper.gas_limit,
1281                    gas_used: helper.gas_used,
1282                    timestamp: helper.timestamp,
1283                    extra_data: helper.extra_data,
1284                    base_fee_per_gas: helper.base_fee_per_gas,
1285                    block_hash: helper.block_hash,
1286                    transactions: helper.transactions,
1287                },
1288                withdrawals: helper.withdrawals,
1289            },
1290            blob_gas_used: helper.blob_gas_used,
1291            excess_blob_gas: helper.excess_blob_gas,
1292        })
1293    }
1294}
1295
1296impl ExecutionPayloadV3 {
1297    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV3`].
1298    ///
1299    /// See also [`ExecutionPayloadV2::from_block_unchecked`].
1300    ///
1301    /// Note: This re-calculates the block hash.
1302    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
1303    where
1304        T: Encodable2718,
1305        H: BlockHeader + Sealable,
1306    {
1307        Self::from_block_unchecked(block.hash_slow(), block)
1308    }
1309
1310    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV3`] using the given block hash.
1311    ///
1312    /// See also [`ExecutionPayloadV2::from_block_unchecked`].
1313    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
1314    where
1315        T: Encodable2718,
1316        H: BlockHeader,
1317    {
1318        Self {
1319            blob_gas_used: block.blob_gas_used().unwrap_or_default(),
1320            excess_blob_gas: block.excess_blob_gas().unwrap_or_default(),
1321            payload_inner: ExecutionPayloadV2::from_block_unchecked(block_hash, block),
1322        }
1323    }
1324
1325    /// Returns the withdrawals for the payload.
1326    pub const fn withdrawals(&self) -> &Vec<Withdrawal> {
1327        &self.payload_inner.withdrawals
1328    }
1329
1330    /// Returns the timestamp for the payload.
1331    pub const fn timestamp(&self) -> u64 {
1332        self.payload_inner.payload_inner.timestamp
1333    }
1334
1335    /// Converts [`ExecutionPayloadV3`] to [`Block`].
1336    ///
1337    /// This performs the same conversion as the underlying V2 payload, but inserts the blob gas
1338    /// used and excess blob gas.
1339    ///
1340    /// See also [`ExecutionPayloadV2::try_into_block`].
1341    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
1342        self.try_into_block_with(|tx| {
1343            T::decode_2718_exact(tx.as_ref())
1344                .map_err(alloy_rlp::Error::from)
1345                .map_err(PayloadError::from)
1346        })
1347    }
1348
1349    /// Converts [`ExecutionPayloadV3`] to [`Block`] with a custom transaction mapper.
1350    ///
1351    /// See also [`ExecutionPayloadV2::try_into_block_with`].
1352    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
1353    where
1354        F: FnMut(Bytes) -> Result<T, E>,
1355        E: Into<PayloadError>,
1356    {
1357        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
1358    }
1359
1360    /// Converts [`ExecutionPayloadV3`] to [`Block`] with raw [`Bytes`] transactions.
1361    ///
1362    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
1363    /// without any conversion.
1364    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
1365        self.into_block_raw_with_transactions_root_opt(None)
1366    }
1367
1368    /// Converts [`ExecutionPayloadV3`] to [`Block`] with raw [`Bytes`] transactions using the
1369    /// given `transactions_root`.
1370    ///
1371    /// See also [`ExecutionPayloadV1::into_block_raw_with_transactions_root`].
1372    pub fn into_block_raw_with_transactions_root(
1373        self,
1374        transactions_root: B256,
1375    ) -> Result<Block<Bytes>, PayloadError> {
1376        self.into_block_raw_with_transactions_root_opt(Some(transactions_root))
1377    }
1378
1379    /// Converts [`ExecutionPayloadV3`] to [`Block`] with raw [`Bytes`] transactions, optionally
1380    /// using the given `transactions_root`.
1381    ///
1382    /// If `transactions_root` is `None`, it will be computed from the transactions.
1383    pub fn into_block_raw_with_transactions_root_opt(
1384        self,
1385        transactions_root: Option<B256>,
1386    ) -> Result<Block<Bytes>, PayloadError> {
1387        let mut base_block =
1388            self.payload_inner.into_block_raw_with_transactions_root_opt(transactions_root)?;
1389
1390        base_block.header.blob_gas_used = Some(self.blob_gas_used);
1391        base_block.header.excess_blob_gas = Some(self.excess_blob_gas);
1392
1393        Ok(base_block)
1394    }
1395}
1396
1397impl<T: Decodable2718> TryFrom<ExecutionPayloadV3> for Block<T> {
1398    type Error = PayloadError;
1399
1400    fn try_from(value: ExecutionPayloadV3) -> Result<Self, Self::Error> {
1401        value.try_into_block()
1402    }
1403}
1404
1405#[cfg(feature = "ssz")]
1406impl ssz::Decode for ExecutionPayloadV3 {
1407    fn is_ssz_fixed_len() -> bool {
1408        false
1409    }
1410
1411    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1412        let mut builder = ssz::SszDecoderBuilder::new(bytes);
1413
1414        builder.register_type::<B256>()?;
1415        builder.register_type::<Address>()?;
1416        builder.register_type::<B256>()?;
1417        builder.register_type::<B256>()?;
1418        builder.register_type::<Bloom>()?;
1419        builder.register_type::<B256>()?;
1420        builder.register_type::<u64>()?;
1421        builder.register_type::<u64>()?;
1422        builder.register_type::<u64>()?;
1423        builder.register_type::<u64>()?;
1424        builder.register_type::<Bytes>()?;
1425        builder.register_type::<U256>()?;
1426        builder.register_type::<B256>()?;
1427        builder.register_type::<Vec<Bytes>>()?;
1428        builder.register_type::<Vec<Withdrawal>>()?;
1429        builder.register_type::<u64>()?;
1430        builder.register_type::<u64>()?;
1431
1432        let mut decoder = builder.build()?;
1433
1434        Ok(Self {
1435            payload_inner: ExecutionPayloadV2 {
1436                payload_inner: ExecutionPayloadV1 {
1437                    parent_hash: decoder.decode_next()?,
1438                    fee_recipient: decoder.decode_next()?,
1439                    state_root: decoder.decode_next()?,
1440                    receipts_root: decoder.decode_next()?,
1441                    logs_bloom: decoder.decode_next()?,
1442                    prev_randao: decoder.decode_next()?,
1443                    block_number: decoder.decode_next()?,
1444                    gas_limit: decoder.decode_next()?,
1445                    gas_used: decoder.decode_next()?,
1446                    timestamp: decoder.decode_next()?,
1447                    extra_data: decoder.decode_next()?,
1448                    base_fee_per_gas: decoder.decode_next()?,
1449                    block_hash: decoder.decode_next()?,
1450                    transactions: decoder.decode_next()?,
1451                },
1452                withdrawals: decoder.decode_next()?,
1453            },
1454            blob_gas_used: decoder.decode_next()?,
1455            excess_blob_gas: decoder.decode_next()?,
1456        })
1457    }
1458}
1459
1460#[cfg(feature = "ssz")]
1461impl ssz::Encode for ExecutionPayloadV3 {
1462    fn is_ssz_fixed_len() -> bool {
1463        false
1464    }
1465
1466    fn ssz_append(&self, buf: &mut Vec<u8>) {
1467        let offset = <B256 as ssz::Encode>::ssz_fixed_len() * 5
1468            + <Address as ssz::Encode>::ssz_fixed_len()
1469            + <Bloom as ssz::Encode>::ssz_fixed_len()
1470            + <u64 as ssz::Encode>::ssz_fixed_len() * 6
1471            + <U256 as ssz::Encode>::ssz_fixed_len()
1472            + ssz::BYTES_PER_LENGTH_OFFSET * 3;
1473
1474        let mut encoder = ssz::SszEncoder::container(buf, offset);
1475
1476        encoder.append(&self.payload_inner.payload_inner.parent_hash);
1477        encoder.append(&self.payload_inner.payload_inner.fee_recipient);
1478        encoder.append(&self.payload_inner.payload_inner.state_root);
1479        encoder.append(&self.payload_inner.payload_inner.receipts_root);
1480        encoder.append(&self.payload_inner.payload_inner.logs_bloom);
1481        encoder.append(&self.payload_inner.payload_inner.prev_randao);
1482        encoder.append(&self.payload_inner.payload_inner.block_number);
1483        encoder.append(&self.payload_inner.payload_inner.gas_limit);
1484        encoder.append(&self.payload_inner.payload_inner.gas_used);
1485        encoder.append(&self.payload_inner.payload_inner.timestamp);
1486        encoder.append(&self.payload_inner.payload_inner.extra_data);
1487        encoder.append(&self.payload_inner.payload_inner.base_fee_per_gas);
1488        encoder.append(&self.payload_inner.payload_inner.block_hash);
1489        encoder.append(&self.payload_inner.payload_inner.transactions);
1490        encoder.append(&self.payload_inner.withdrawals);
1491        encoder.append(&self.blob_gas_used);
1492        encoder.append(&self.excess_blob_gas);
1493
1494        encoder.finalize();
1495    }
1496
1497    fn ssz_bytes_len(&self) -> usize {
1498        <ExecutionPayloadV2 as ssz::Encode>::ssz_bytes_len(&self.payload_inner)
1499            + <u64 as ssz::Encode>::ssz_fixed_len() * 2
1500    }
1501}
1502
1503/// Execution payload V4 as defined in the Amsterdam fork.
1504///
1505/// This extends [`ExecutionPayloadV3`] with the `block_access_list` field for [EIP-7928] and the
1506/// `slot_number` field for [EIP-7843].
1507///
1508/// See also:
1509/// <https://github.com/ethereum/execution-apis/blob/7b4d9f62a3fe62b9b8dcb355f1c5a38b5ff084f6/src/engine/amsterdam.md#executionpayloadv4>
1510///
1511/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
1512/// [EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843
1513#[derive(Clone, Debug, PartialEq, Eq)]
1514#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1515#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
1516#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1517pub struct ExecutionPayloadV4 {
1518    /// Inner V3 payload
1519    #[cfg_attr(feature = "serde", serde(flatten))]
1520    pub payload_inner: ExecutionPayloadV3,
1521    /// RLP-encoded block access list as defined in [EIP-7928].
1522    ///
1523    /// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
1524    pub block_access_list: Bytes,
1525    /// The slot number corresponding to this block, calculated in the consensus layer.
1526    ///
1527    /// [EIP-7843]: https://eips.ethereum.org/EIPS/eip-7843
1528    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
1529    pub slot_number: u64,
1530}
1531
1532#[cfg(feature = "serde")]
1533impl<'de> serde::Deserialize<'de> for ExecutionPayloadV4 {
1534    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1535    where
1536        D: serde::Deserializer<'de>,
1537    {
1538        #[derive(serde::Deserialize)]
1539        #[serde(rename_all = "camelCase")]
1540        struct Helper {
1541            parent_hash: B256,
1542            fee_recipient: Address,
1543            state_root: B256,
1544            receipts_root: B256,
1545            logs_bloom: Bloom,
1546            prev_randao: B256,
1547            #[serde(with = "alloy_serde::quantity")]
1548            block_number: u64,
1549            #[serde(with = "alloy_serde::quantity")]
1550            gas_limit: u64,
1551            #[serde(with = "alloy_serde::quantity")]
1552            gas_used: u64,
1553            #[serde(with = "alloy_serde::quantity")]
1554            timestamp: u64,
1555            extra_data: Bytes,
1556            base_fee_per_gas: U256,
1557            block_hash: B256,
1558            transactions: Vec<Bytes>,
1559            withdrawals: Vec<Withdrawal>,
1560            #[serde(with = "alloy_serde::quantity")]
1561            blob_gas_used: u64,
1562            #[serde(with = "alloy_serde::quantity")]
1563            excess_blob_gas: u64,
1564            block_access_list: Bytes,
1565            #[serde(with = "alloy_serde::quantity")]
1566            slot_number: u64,
1567        }
1568
1569        let helper = Helper::deserialize(deserializer)?;
1570        Ok(Self {
1571            payload_inner: ExecutionPayloadV3 {
1572                payload_inner: ExecutionPayloadV2 {
1573                    payload_inner: ExecutionPayloadV1 {
1574                        parent_hash: helper.parent_hash,
1575                        fee_recipient: helper.fee_recipient,
1576                        state_root: helper.state_root,
1577                        receipts_root: helper.receipts_root,
1578                        logs_bloom: helper.logs_bloom,
1579                        prev_randao: helper.prev_randao,
1580                        block_number: helper.block_number,
1581                        gas_limit: helper.gas_limit,
1582                        gas_used: helper.gas_used,
1583                        timestamp: helper.timestamp,
1584                        extra_data: helper.extra_data,
1585                        base_fee_per_gas: helper.base_fee_per_gas,
1586                        block_hash: helper.block_hash,
1587                        transactions: helper.transactions,
1588                    },
1589                    withdrawals: helper.withdrawals,
1590                },
1591                blob_gas_used: helper.blob_gas_used,
1592                excess_blob_gas: helper.excess_blob_gas,
1593            },
1594            block_access_list: helper.block_access_list,
1595            slot_number: helper.slot_number,
1596        })
1597    }
1598}
1599
1600#[cfg(feature = "ssz")]
1601impl ssz::Decode for ExecutionPayloadV4 {
1602    fn is_ssz_fixed_len() -> bool {
1603        false
1604    }
1605
1606    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1607        let mut builder = ssz::SszDecoderBuilder::new(bytes);
1608
1609        builder.register_type::<B256>()?;
1610        builder.register_type::<Address>()?;
1611        builder.register_type::<B256>()?;
1612        builder.register_type::<B256>()?;
1613        builder.register_type::<Bloom>()?;
1614        builder.register_type::<B256>()?;
1615        builder.register_type::<u64>()?;
1616        builder.register_type::<u64>()?;
1617        builder.register_type::<u64>()?;
1618        builder.register_type::<u64>()?;
1619        builder.register_type::<Bytes>()?;
1620        builder.register_type::<U256>()?;
1621        builder.register_type::<B256>()?;
1622        builder.register_type::<Vec<Bytes>>()?;
1623        builder.register_type::<Vec<Withdrawal>>()?;
1624        builder.register_type::<u64>()?;
1625        builder.register_type::<u64>()?;
1626        builder.register_type::<Bytes>()?;
1627        builder.register_type::<u64>()?;
1628
1629        let mut decoder = builder.build()?;
1630
1631        Ok(Self {
1632            payload_inner: ExecutionPayloadV3 {
1633                payload_inner: ExecutionPayloadV2 {
1634                    payload_inner: ExecutionPayloadV1 {
1635                        parent_hash: decoder.decode_next()?,
1636                        fee_recipient: decoder.decode_next()?,
1637                        state_root: decoder.decode_next()?,
1638                        receipts_root: decoder.decode_next()?,
1639                        logs_bloom: decoder.decode_next()?,
1640                        prev_randao: decoder.decode_next()?,
1641                        block_number: decoder.decode_next()?,
1642                        gas_limit: decoder.decode_next()?,
1643                        gas_used: decoder.decode_next()?,
1644                        timestamp: decoder.decode_next()?,
1645                        extra_data: decoder.decode_next()?,
1646                        base_fee_per_gas: decoder.decode_next()?,
1647                        block_hash: decoder.decode_next()?,
1648                        transactions: decoder.decode_next()?,
1649                    },
1650                    withdrawals: decoder.decode_next()?,
1651                },
1652                blob_gas_used: decoder.decode_next()?,
1653                excess_blob_gas: decoder.decode_next()?,
1654            },
1655            block_access_list: decoder.decode_next()?,
1656            slot_number: decoder.decode_next()?,
1657        })
1658    }
1659}
1660
1661#[cfg(feature = "ssz")]
1662impl ssz::Encode for ExecutionPayloadV4 {
1663    fn is_ssz_fixed_len() -> bool {
1664        false
1665    }
1666
1667    fn ssz_append(&self, buf: &mut Vec<u8>) {
1668        let offset = <B256 as ssz::Encode>::ssz_fixed_len() * 5
1669            + <Address as ssz::Encode>::ssz_fixed_len()
1670            + <Bloom as ssz::Encode>::ssz_fixed_len()
1671            + <u64 as ssz::Encode>::ssz_fixed_len() * 7 // includes slot_number
1672            + <U256 as ssz::Encode>::ssz_fixed_len()
1673            + ssz::BYTES_PER_LENGTH_OFFSET * 4;
1674
1675        let mut encoder = ssz::SszEncoder::container(buf, offset);
1676
1677        encoder.append(&self.payload_inner.payload_inner.payload_inner.parent_hash);
1678        encoder.append(&self.payload_inner.payload_inner.payload_inner.fee_recipient);
1679        encoder.append(&self.payload_inner.payload_inner.payload_inner.state_root);
1680        encoder.append(&self.payload_inner.payload_inner.payload_inner.receipts_root);
1681        encoder.append(&self.payload_inner.payload_inner.payload_inner.logs_bloom);
1682        encoder.append(&self.payload_inner.payload_inner.payload_inner.prev_randao);
1683        encoder.append(&self.payload_inner.payload_inner.payload_inner.block_number);
1684        encoder.append(&self.payload_inner.payload_inner.payload_inner.gas_limit);
1685        encoder.append(&self.payload_inner.payload_inner.payload_inner.gas_used);
1686        encoder.append(&self.payload_inner.payload_inner.payload_inner.timestamp);
1687        encoder.append(&self.payload_inner.payload_inner.payload_inner.extra_data);
1688        encoder.append(&self.payload_inner.payload_inner.payload_inner.base_fee_per_gas);
1689        encoder.append(&self.payload_inner.payload_inner.payload_inner.block_hash);
1690        encoder.append(&self.payload_inner.payload_inner.payload_inner.transactions);
1691        encoder.append(&self.payload_inner.payload_inner.withdrawals);
1692        encoder.append(&self.payload_inner.blob_gas_used);
1693        encoder.append(&self.payload_inner.excess_blob_gas);
1694        encoder.append(&self.block_access_list);
1695        encoder.append(&self.slot_number);
1696
1697        encoder.finalize();
1698    }
1699
1700    fn ssz_bytes_len(&self) -> usize {
1701        <ExecutionPayloadV3 as ssz::Encode>::ssz_bytes_len(&self.payload_inner)
1702            + ssz::BYTES_PER_LENGTH_OFFSET
1703            + self.block_access_list.len()
1704            + <u64 as ssz::Encode>::ssz_fixed_len()
1705    }
1706}
1707
1708impl ExecutionPayloadV4 {
1709    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV4`].
1710    ///
1711    /// This uses the header's `block_access_list_hash` bytes as the `block_access_list` fallback
1712    /// when the full RLP-encoded block access list is not available on the block value. If the
1713    /// block header does not carry a BAL hash, this falls back to the canonical empty BAL hash
1714    /// bytes.
1715    /// Use [`Self::from_block_unchecked_with_bal`] when the full block access list bytes are
1716    /// available and should be preserved.
1717    ///
1718    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
1719    ///
1720    /// Note: This re-calculates the block hash.
1721    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
1722    where
1723        T: Encodable2718,
1724        H: BlockHeader + Sealable,
1725    {
1726        Self::from_block_unchecked(block.header.hash_slow(), block)
1727    }
1728
1729    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV4`] using the given block hash.
1730    ///
1731    /// This uses the header's `block_access_list_hash` bytes as the `block_access_list` fallback
1732    /// because the full RLP-encoded block access list is not available on the block value. If the
1733    /// block header does not carry a BAL hash, this falls back to the canonical empty BAL hash
1734    /// bytes.
1735    /// Use [`Self::from_block_unchecked_with_bal`] when the full block access list bytes are
1736    /// available and should be preserved.
1737    ///
1738    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
1739    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
1740    where
1741        T: Encodable2718,
1742        H: BlockHeader,
1743    {
1744        Self {
1745            payload_inner: ExecutionPayloadV3::from_block_unchecked(block_hash, block),
1746            block_access_list: block.header.block_access_list_hash().map_or_else(
1747                || Bytes::copy_from_slice(EMPTY_BLOCK_ACCESS_LIST_HASH.as_slice()),
1748                |hash| Bytes::copy_from_slice(hash.as_slice()),
1749            ),
1750            slot_number: block.header.slot_number().unwrap_or_default(),
1751        }
1752    }
1753
1754    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV4`] using the given block hash
1755    /// and block access list.
1756    ///
1757    /// Unlike [`Self::from_block_unchecked`], this preserves the full RLP-encoded block access
1758    /// list instead of falling back to the header hash bytes.
1759    ///
1760    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
1761    pub fn from_block_unchecked_with_bal<T, H>(
1762        block_hash: B256,
1763        block: &Block<T, H>,
1764        block_access_list: Bytes,
1765    ) -> Self
1766    where
1767        T: Encodable2718,
1768        H: BlockHeader,
1769    {
1770        Self {
1771            payload_inner: ExecutionPayloadV3::from_block_unchecked(block_hash, block),
1772            block_access_list,
1773            slot_number: block.header.slot_number().unwrap_or_default(),
1774        }
1775    }
1776
1777    /// Returns the timestamp for the execution payload.
1778    pub const fn timestamp(&self) -> u64 {
1779        self.payload_inner.timestamp()
1780    }
1781
1782    /// Converts [`ExecutionPayloadV4`] to [`Block`].
1783    ///
1784    /// This performs the same conversion as the underlying V3 payload, but calculates the
1785    /// block access list hash and sets the slot number.
1786    ///
1787    /// See also [`ExecutionPayloadV3::try_into_block`].
1788    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
1789        self.try_into_block_with(|tx| {
1790            T::decode_2718_exact(tx.as_ref())
1791                .map_err(alloy_rlp::Error::from)
1792                .map_err(PayloadError::from)
1793        })
1794    }
1795
1796    /// Converts [`ExecutionPayloadV4`] to [`Block`] with a custom transaction mapper.
1797    ///
1798    /// See also [`ExecutionPayloadV3::try_into_block_with`].
1799    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
1800    where
1801        F: FnMut(Bytes) -> Result<T, E>,
1802        E: Into<PayloadError>,
1803    {
1804        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
1805    }
1806
1807    /// Converts [`ExecutionPayloadV4`] to [`Block`] with raw [`Bytes`] transactions.
1808    ///
1809    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
1810    /// without any conversion.
1811    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
1812        let mut base_block = self.payload_inner.into_block_raw()?;
1813
1814        let block_access_list_hash = alloy_primitives::keccak256(&self.block_access_list);
1815        base_block.header.block_access_list_hash = Some(block_access_list_hash);
1816        base_block.header.slot_number = Some(self.slot_number);
1817
1818        Ok(base_block)
1819    }
1820
1821    /// Converts [`ExecutionPayloadV4`] to [`Block`] with raw [`Bytes`] transactions using the
1822    /// given `transactions_root`.
1823    ///
1824    /// See also [`ExecutionPayloadV1::into_block_raw_with_transactions_root`].
1825    pub fn into_block_raw_with_transactions_root(
1826        self,
1827        transactions_root: B256,
1828    ) -> Result<Block<Bytes>, PayloadError> {
1829        self.into_block_raw_with_transactions_root_opt(Some(transactions_root))
1830    }
1831
1832    /// Converts [`ExecutionPayloadV4`] to [`Block`] with raw [`Bytes`] transactions, optionally
1833    /// using the given `transactions_root`.
1834    ///
1835    /// If `transactions_root` is `None`, it will be computed from the transactions.
1836    pub fn into_block_raw_with_transactions_root_opt(
1837        self,
1838        transactions_root: Option<B256>,
1839    ) -> Result<Block<Bytes>, PayloadError> {
1840        let mut base_block =
1841            self.payload_inner.into_block_raw_with_transactions_root_opt(transactions_root)?;
1842
1843        base_block.header.block_access_list_hash = Some(keccak256(self.block_access_list));
1844        base_block.header.slot_number = Some(self.slot_number);
1845
1846        Ok(base_block)
1847    }
1848}
1849
1850impl<T: Decodable2718> TryFrom<ExecutionPayloadV4> for Block<T> {
1851    type Error = PayloadError;
1852
1853    fn try_from(value: ExecutionPayloadV4) -> Result<Self, Self::Error> {
1854        value.try_into_block()
1855    }
1856}
1857
1858/// This includes all bundled blob related data of an executed payload.
1859#[derive(Clone, Debug, Default, PartialEq, Eq)]
1860#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1861#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
1862#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1863pub struct BlobsBundleV1 {
1864    /// All commitments in the bundle.
1865    pub commitments: Vec<alloy_consensus::Bytes48>,
1866    /// All proofs in the bundle.
1867    pub proofs: Vec<alloy_consensus::Bytes48>,
1868    /// All blobs in the bundle.
1869    pub blobs: Vec<alloy_consensus::Blob>,
1870}
1871
1872#[cfg(feature = "serde")]
1873impl<'de> serde::Deserialize<'de> for BlobsBundleV1 {
1874    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1875    where
1876        D: serde::Deserializer<'de>,
1877    {
1878        #[derive(serde::Deserialize)]
1879        struct BlobsBundleRaw {
1880            commitments: Vec<alloy_consensus::Bytes48>,
1881            proofs: Vec<alloy_consensus::Bytes48>,
1882            blobs: Vec<alloy_consensus::Blob>,
1883        }
1884        let raw = BlobsBundleRaw::deserialize(deserializer)?;
1885
1886        if raw.proofs.len() == raw.commitments.len() && raw.proofs.len() == raw.blobs.len() {
1887            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
1888        } else {
1889            Err(serde::de::Error::invalid_length(
1890                raw.proofs.len(),
1891                &format!("{}", raw.commitments.len()).as_str(),
1892            ))
1893        }
1894    }
1895}
1896
1897impl BlobsBundleV1 {
1898    /// Creates a new blob bundle from the given sidecars.
1899    ///
1900    /// This folds the sidecar fields into single commit, proof, and blob vectors.
1901    pub fn new(sidecars: impl IntoIterator<Item = BlobTransactionSidecar>) -> Self {
1902        let (commitments, proofs, blobs) = sidecars.into_iter().fold(
1903            (Vec::new(), Vec::new(), Vec::new()),
1904            |(mut commitments, mut proofs, mut blobs), sidecar| {
1905                commitments.extend(sidecar.commitments);
1906                proofs.extend(sidecar.proofs);
1907                blobs.extend(sidecar.blobs);
1908                (commitments, proofs, blobs)
1909            },
1910        );
1911        Self { commitments, proofs, blobs }
1912    }
1913
1914    /// Returns a new empty blobs bundle.
1915    ///
1916    /// This is useful for the opstack engine API that expects an empty bundle as part of the
1917    /// payload for API compatibility reasons.
1918    pub fn empty() -> Self {
1919        Self::default()
1920    }
1921
1922    /// Computes the versioned hashes from the KZG commitments.
1923    pub fn versioned_hashes(&self) -> Vec<B256> {
1924        self.commitments
1925            .iter()
1926            .map(|c| alloy_eips::eip4844::kzg_to_versioned_hash(c.as_slice()))
1927            .collect()
1928    }
1929
1930    /// Take `len` blob data from the bundle.
1931    ///
1932    /// # Panics
1933    ///
1934    /// If len is more than the blobs bundle len.
1935    pub fn take(&mut self, len: usize) -> (Vec<Bytes48>, Vec<Bytes48>, Vec<Blob>) {
1936        (
1937            self.commitments.drain(0..len).collect(),
1938            self.proofs.drain(0..len).collect(),
1939            self.blobs.drain(0..len).collect(),
1940        )
1941    }
1942
1943    /// Returns the sidecar from the bundle
1944    ///
1945    /// # Panics
1946    ///
1947    /// If len is more than the blobs bundle len.
1948    pub fn pop_sidecar(&mut self, len: usize) -> BlobTransactionSidecar {
1949        let (commitments, proofs, blobs) = self.take(len);
1950        BlobTransactionSidecar { commitments, proofs, blobs }
1951    }
1952
1953    /// Converts this bundle into a single [`BlobTransactionSidecar`].
1954    ///
1955    /// Returns an error if the bundle doesn't contain the same number of commitments as blobs and
1956    /// proofs.
1957    ///
1958    /// Returns an empty [`BlobTransactionSidecar`] if the bundle is empty.
1959    #[cfg(feature = "kzg")]
1960    pub fn try_into_sidecar(
1961        self,
1962    ) -> Result<BlobTransactionSidecar, alloy_consensus::error::ValueError<Self>> {
1963        if self.commitments.len() != self.proofs.len() || self.commitments.len() != self.blobs.len()
1964        {
1965            return Err(alloy_consensus::error::ValueError::new(self, "length mismatch"));
1966        }
1967
1968        let Self { commitments, proofs, blobs } = self;
1969        Ok(BlobTransactionSidecar { blobs, commitments, proofs })
1970    }
1971
1972    /// Converts this V1 bundle into a [`BlobsBundleV2`] by computing EIP-7594 cell proofs.
1973    ///
1974    /// This uses the default KZG settings. See [`Self::try_into_v2_with_settings`] for custom
1975    /// settings.
1976    ///
1977    /// # Errors
1978    ///
1979    /// Returns an error if the bundle has mismatched lengths or if KZG proof computation fails.
1980    #[cfg(feature = "kzg")]
1981    pub fn try_into_v2(self) -> Result<BlobsBundleV2, alloy_eips::eip4844::c_kzg::Error> {
1982        self.try_into_v2_with_settings(
1983            alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get(),
1984        )
1985    }
1986
1987    /// Converts this V1 bundle into a [`BlobsBundleV2`] by computing EIP-7594 cell proofs
1988    /// using the provided KZG settings.
1989    ///
1990    /// # Errors
1991    ///
1992    /// Returns an error if the bundle has mismatched lengths or if KZG proof computation fails.
1993    #[cfg(feature = "kzg")]
1994    pub fn try_into_v2_with_settings(
1995        self,
1996        settings: &alloy_eips::eip4844::c_kzg::KzgSettings,
1997    ) -> Result<BlobsBundleV2, alloy_eips::eip4844::c_kzg::Error> {
1998        use alloy_eips::eip7594::CELLS_PER_EXT_BLOB;
1999
2000        if let [blob] = self.blobs.as_slice() {
2001            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
2002            let cell_proofs =
2003                alloy_eips::eip4844::c_kzg::KzgProof::boxed_slice_as_alloy(kzg_proofs).into();
2004            return Ok(BlobsBundleV2 {
2005                commitments: self.commitments,
2006                proofs: cell_proofs,
2007                blobs: self.blobs,
2008            });
2009        }
2010
2011        let mut cell_proofs = Vec::with_capacity(self.blobs.len() * CELLS_PER_EXT_BLOB);
2012
2013        for blob in self.blobs.iter() {
2014            // Compute cells and their KZG proofs for this blob
2015            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
2016            cell_proofs.extend_from_slice(alloy_eips::eip4844::c_kzg::KzgProof::slice_as_alloy(
2017                kzg_proofs.as_ref(),
2018            ));
2019        }
2020
2021        Ok(BlobsBundleV2 { commitments: self.commitments, proofs: cell_proofs, blobs: self.blobs })
2022    }
2023}
2024
2025impl From<Vec<BlobTransactionSidecar>> for BlobsBundleV1 {
2026    fn from(sidecars: Vec<BlobTransactionSidecar>) -> Self {
2027        Self::new(sidecars)
2028    }
2029}
2030
2031impl FromIterator<BlobTransactionSidecar> for BlobsBundleV1 {
2032    fn from_iter<T: IntoIterator<Item = BlobTransactionSidecar>>(iter: T) -> Self {
2033        Self::new(iter)
2034    }
2035}
2036
2037#[cfg(feature = "kzg")]
2038impl TryFrom<BlobsBundleV1> for BlobTransactionSidecar {
2039    type Error = alloy_consensus::error::ValueError<BlobsBundleV1>;
2040
2041    fn try_from(value: BlobsBundleV1) -> Result<Self, Self::Error> {
2042        value.try_into_sidecar()
2043    }
2044}
2045
2046#[cfg(feature = "kzg")]
2047impl TryFrom<BlobsBundleV1> for BlobsBundleV2 {
2048    type Error = alloy_eips::eip4844::c_kzg::Error;
2049
2050    fn try_from(value: BlobsBundleV1) -> Result<Self, Self::Error> {
2051        value.try_into_v2()
2052    }
2053}
2054
2055/// This includes all bundled blob related data of an executed payload.
2056#[derive(Clone, Debug, Default, PartialEq, Eq)]
2057#[cfg_attr(feature = "serde", derive(serde::Serialize))]
2058#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode))]
2059#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
2060pub struct BlobsBundleV2 {
2061    /// All commitments in the bundle.
2062    pub commitments: Vec<alloy_consensus::Bytes48>,
2063    /// All cell proofs in the bundle.
2064    pub proofs: Vec<alloy_consensus::Bytes48>,
2065    /// All blobs in the bundle.
2066    pub blobs: Vec<alloy_consensus::Blob>,
2067}
2068
2069#[cfg(feature = "serde")]
2070impl<'de> serde::Deserialize<'de> for BlobsBundleV2 {
2071    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2072    where
2073        D: serde::Deserializer<'de>,
2074    {
2075        #[derive(serde::Deserialize)]
2076        struct BlobsBundleRaw {
2077            commitments: Vec<alloy_consensus::Bytes48>,
2078            proofs: Vec<alloy_consensus::Bytes48>,
2079            blobs: Vec<alloy_consensus::Blob>,
2080        }
2081        let raw = BlobsBundleRaw::deserialize(deserializer)?;
2082
2083        if raw.proofs.len() == raw.blobs.len() * CELLS_PER_EXT_BLOB
2084            && raw.commitments.len() == raw.blobs.len()
2085        {
2086            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
2087        } else {
2088            Err(serde::de::Error::invalid_length(
2089                raw.proofs.len(),
2090                &format!("{}", raw.commitments.len() * CELLS_PER_EXT_BLOB).as_str(),
2091            ))
2092        }
2093    }
2094}
2095
2096#[cfg(feature = "ssz")]
2097impl ssz::Decode for BlobsBundleV2 {
2098    fn is_ssz_fixed_len() -> bool {
2099        false
2100    }
2101
2102    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
2103        #[derive(ssz_derive::Decode)]
2104        struct BlobsBundleRaw {
2105            commitments: Vec<alloy_consensus::Bytes48>,
2106            proofs: Vec<alloy_consensus::Bytes48>,
2107            blobs: Vec<alloy_consensus::Blob>,
2108        }
2109
2110        let raw = BlobsBundleRaw::from_ssz_bytes(bytes)?;
2111
2112        if raw.proofs.len() == raw.blobs.len() * CELLS_PER_EXT_BLOB
2113            && raw.commitments.len() == raw.blobs.len()
2114        {
2115            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
2116        } else {
2117            Err(ssz::DecodeError::BytesInvalid(
2118                format!(
2119                    "Invalid BlobsBundleV2: expected {} proofs and {} commitments for {} blobs, got {} proofs and {} commitments",
2120                    raw.blobs.len() * CELLS_PER_EXT_BLOB,
2121                    raw.blobs.len(),
2122                    raw.blobs.len(),
2123                    raw.proofs.len(),
2124                    raw.commitments.len()
2125                )
2126            ))
2127        }
2128    }
2129}
2130
2131impl BlobsBundleV2 {
2132    /// Creates a new blob bundle from the given sidecars.
2133    ///
2134    /// This folds the sidecar fields into single commit, proof, and blob vectors.
2135    pub fn new(sidecars: impl IntoIterator<Item = BlobTransactionSidecarEip7594>) -> Self {
2136        let (commitments, proofs, blobs) = sidecars.into_iter().fold(
2137            (Vec::new(), Vec::new(), Vec::new()),
2138            |(mut commitments, mut proofs, mut blobs), sidecar| {
2139                commitments.extend(sidecar.commitments);
2140                proofs.extend(sidecar.cell_proofs);
2141                blobs.extend(sidecar.blobs);
2142                (commitments, proofs, blobs)
2143            },
2144        );
2145        Self { commitments, proofs, blobs }
2146    }
2147
2148    /// Returns a new empty blobs bundle.
2149    ///
2150    /// This is useful for the opstack engine API that expects an empty bundle as part of the
2151    /// payload for API compatibility reasons.
2152    pub fn empty() -> Self {
2153        Self::default()
2154    }
2155
2156    /// Computes the versioned hashes from the KZG commitments.
2157    pub fn versioned_hashes(&self) -> Vec<B256> {
2158        self.commitments
2159            .iter()
2160            .map(|c| alloy_eips::eip4844::kzg_to_versioned_hash(c.as_slice()))
2161            .collect()
2162    }
2163
2164    /// Take `len` blob data from the bundle.
2165    ///
2166    /// Note this will take `len * CELLS_PER_EXT_BLOB` proofs.
2167    ///
2168    /// # Panics
2169    ///
2170    /// If len is more than the blobs bundle len.
2171    pub fn take(&mut self, len: usize) -> (Vec<Bytes48>, Vec<Bytes48>, Vec<Blob>) {
2172        (
2173            self.commitments.drain(0..len).collect(),
2174            self.proofs.drain(0..len * CELLS_PER_EXT_BLOB).collect(),
2175            self.blobs.drain(0..len).collect(),
2176        )
2177    }
2178
2179    /// Returns the sidecar from the bundle
2180    ///
2181    /// # Panics
2182    ///
2183    /// If len is more than the blobs bundle len.
2184    pub fn pop_sidecar(&mut self, len: usize) -> BlobTransactionSidecarEip7594 {
2185        let (commitments, cell_proofs, blobs) = self.take(len);
2186        BlobTransactionSidecarEip7594 { commitments, cell_proofs, blobs }
2187    }
2188
2189    /// Converts this bundle into a single [`BlobTransactionSidecarEip7594`].
2190    ///
2191    /// Returns an error if the bundle doesn't contain the correct number of cell proofs
2192    /// (expected blobs.len() * CELLS_PER_EXT_BLOB) or if the commitments length doesn't
2193    /// match the blobs length.
2194    ///
2195    /// Returns an empty [`BlobTransactionSidecarEip7594`] if the bundle is empty.
2196    #[cfg(feature = "kzg")]
2197    pub fn try_into_sidecar(
2198        self,
2199    ) -> Result<BlobTransactionSidecarEip7594, alloy_consensus::error::ValueError<Self>> {
2200        let expected_cell_proofs_len = self.blobs.len() * CELLS_PER_EXT_BLOB;
2201        if self.proofs.len() != expected_cell_proofs_len {
2202            let msg = format!(
2203                "cell proofs length mismatch, expected {expected_cell_proofs_len}, has {}",
2204                self.proofs.len()
2205            );
2206            return Err(alloy_consensus::error::ValueError::new(self, msg));
2207        }
2208
2209        if self.commitments.len() != self.blobs.len() {
2210            let msg = format!(
2211                "commitments length ({}) mismatch, expected blob length ({})",
2212                self.commitments.len(),
2213                self.blobs.len()
2214            );
2215            return Err(alloy_consensus::error::ValueError::new(self, msg));
2216        }
2217
2218        let Self { commitments, proofs, blobs } = self;
2219        Ok(BlobTransactionSidecarEip7594 { blobs, commitments, cell_proofs: proofs })
2220    }
2221
2222    /// Converts this V2 bundle into a [`BlobsBundleV1`] by computing EIP-4844 blob proofs.
2223    ///
2224    /// This uses the default KZG settings. See [`Self::try_into_v1_with_settings`] for custom
2225    /// settings.
2226    ///
2227    /// # Errors
2228    ///
2229    /// Returns an error if KZG proof computation fails.
2230    #[cfg(feature = "kzg")]
2231    pub fn try_into_v1(self) -> Result<BlobsBundleV1, alloy_eips::eip4844::c_kzg::Error> {
2232        self.try_into_v1_with_settings(
2233            alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get(),
2234        )
2235    }
2236
2237    /// Converts this V2 bundle into a [`BlobsBundleV1`] by computing EIP-4844 blob proofs
2238    /// using the provided KZG settings.
2239    ///
2240    /// This recomputes the blob proofs from the blobs and commitments. The cell proofs from
2241    /// V2 are discarded as they are not used in V1.
2242    ///
2243    /// # Errors
2244    ///
2245    /// Returns an error if KZG proof computation fails.
2246    #[cfg(feature = "kzg")]
2247    pub fn try_into_v1_with_settings(
2248        self,
2249        settings: &alloy_eips::eip4844::c_kzg::KzgSettings,
2250    ) -> Result<BlobsBundleV1, alloy_eips::eip4844::c_kzg::Error> {
2251        let mut proofs = Vec::with_capacity(self.blobs.len());
2252
2253        for (blob, commitment) in self.blobs.iter().zip(self.commitments.iter()) {
2254            // Compute the blob proof
2255            let proof = settings.compute_blob_kzg_proof(blob.as_ckzg(), commitment.as_ckzg())?;
2256
2257            proofs.push(Bytes48::from_ckzg(proof.to_bytes()));
2258        }
2259
2260        Ok(BlobsBundleV1 { commitments: self.commitments, proofs, blobs: self.blobs })
2261    }
2262}
2263
2264impl From<Vec<BlobTransactionSidecarEip7594>> for BlobsBundleV2 {
2265    fn from(sidecars: Vec<BlobTransactionSidecarEip7594>) -> Self {
2266        Self::new(sidecars)
2267    }
2268}
2269
2270impl FromIterator<BlobTransactionSidecarEip7594> for BlobsBundleV2 {
2271    fn from_iter<T: IntoIterator<Item = BlobTransactionSidecarEip7594>>(iter: T) -> Self {
2272        Self::new(iter)
2273    }
2274}
2275
2276#[cfg(feature = "kzg")]
2277impl TryFrom<BlobsBundleV2> for BlobTransactionSidecarEip7594 {
2278    type Error = alloy_consensus::error::ValueError<BlobsBundleV2>;
2279
2280    fn try_from(value: BlobsBundleV2) -> Result<Self, Self::Error> {
2281        value.try_into_sidecar()
2282    }
2283}
2284
2285#[cfg(feature = "kzg")]
2286impl TryFrom<BlobsBundleV2> for BlobsBundleV1 {
2287    type Error = alloy_eips::eip4844::c_kzg::Error;
2288
2289    fn try_from(value: BlobsBundleV2) -> Result<Self, Self::Error> {
2290        value.try_into_v1()
2291    }
2292}
2293
2294/// An execution payload, which can be either [ExecutionPayloadV1], [ExecutionPayloadV2],
2295/// [ExecutionPayloadV3], or [ExecutionPayloadV4].
2296///
2297/// Payload-to-block conversions return an unsealed block and do not recompute or compare the
2298/// advertised `block_hash`. Callers performing Engine API validation must hash the returned block
2299/// and compare it separately.
2300#[derive(Clone, Debug, PartialEq, Eq)]
2301#[cfg_attr(feature = "serde", derive(serde::Serialize))]
2302#[cfg_attr(feature = "serde", serde(untagged))]
2303#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
2304pub enum ExecutionPayload {
2305    /// V1 payload
2306    V1(ExecutionPayloadV1),
2307    /// V2 payload
2308    V2(ExecutionPayloadV2),
2309    /// V3 payload
2310    V3(ExecutionPayloadV3),
2311    /// V4 payload (Amsterdam)
2312    V4(ExecutionPayloadV4),
2313}
2314
2315impl ExecutionPayload {
2316    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2317    /// [`ExecutionPayloadSidecar`] extracted from the block.
2318    ///
2319    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2320    /// See also [`ExecutionPayloadSidecar::from_block`].
2321    ///
2322    /// Note: This re-calculates the block hash.
2323    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> (Self, ExecutionPayloadSidecar)
2324    where
2325        T: Encodable2718 + Transaction,
2326        H: BlockHeader + Sealable,
2327    {
2328        Self::from_block_unchecked(block.hash_slow(), block)
2329    }
2330
2331    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2332    /// [`ExecutionPayloadSidecar`] extracted from the block along with block access list.
2333    ///
2334    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads.
2335    ///
2336    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2337    /// See also [`ExecutionPayloadSidecar::from_block`].
2338    ///
2339    /// Note: This re-calculates the block hash.
2340    pub fn from_block_slow_with_bal<T, H>(
2341        block: &Block<T, H>,
2342        block_access_list: Bytes,
2343    ) -> (Self, ExecutionPayloadSidecar)
2344    where
2345        T: Encodable2718 + Transaction,
2346        H: BlockHeader + Sealable,
2347    {
2348        Self::from_block_slow_with_extras(block, block_access_list)
2349    }
2350
2351    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2352    /// [`ExecutionPayloadSidecar`] extracted from the block along with payload extras.
2353    ///
2354    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads.
2355    ///
2356    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2357    /// See also [`ExecutionPayloadSidecar::from_block`].
2358    ///
2359    /// Note: This re-calculates the block hash.
2360    pub fn from_block_slow_with_extras<T, H>(
2361        block: &Block<T, H>,
2362        extras: impl Into<PayloadExtras>,
2363    ) -> (Self, ExecutionPayloadSidecar)
2364    where
2365        T: Encodable2718 + Transaction,
2366        H: BlockHeader + Sealable,
2367    {
2368        let extras = extras.into();
2369        if let Some(block_access_list) = extras.bal {
2370            Self::from_block_unchecked_with_bal(block.hash_slow(), block, block_access_list)
2371        } else {
2372            Self::from_block_unchecked(block.hash_slow(), block)
2373        }
2374    }
2375
2376    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2377    /// [`ExecutionPayloadSidecar`] extracted from the block.
2378    ///
2379    /// For Amsterdam/V4 payloads this uses the header's `block_access_list_hash` bytes as the
2380    /// `block_access_list` fallback, because the full RLP-encoded block access list is not part of
2381    /// the block value. If the block header does not carry a BAL hash, this falls back to the
2382    /// canonical empty BAL hash bytes. Use [`Self::from_block_unchecked_with_bal`] when the full
2383    /// block access list bytes are available and should be preserved.
2384    ///
2385    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2386    /// See also [`ExecutionPayloadSidecar::from_block`].
2387    ///
2388    /// The supplied hash is stored verbatim without checking it against the block. The payload
2389    /// version is inferred from the block access-list hash, parent beacon block root, and
2390    /// withdrawals.
2391    pub fn from_block_unchecked<T, H>(
2392        block_hash: B256,
2393        block: &Block<T, H>,
2394    ) -> (Self, ExecutionPayloadSidecar)
2395    where
2396        T: Encodable2718 + Transaction,
2397        H: BlockHeader,
2398    {
2399        let sidecar = ExecutionPayloadSidecar::from_block(block);
2400
2401        let execution_payload = if block.header.block_access_list_hash().is_some() {
2402            // block with block access list hash: V4 (Amsterdam)
2403            Self::V4(ExecutionPayloadV4::from_block_unchecked(block_hash, block))
2404        } else if block.header.parent_beacon_block_root().is_some() {
2405            // block with parent beacon block root: V3
2406            Self::V3(ExecutionPayloadV3::from_block_unchecked(block_hash, block))
2407        } else if block.body.withdrawals.is_some() {
2408            // block with withdrawals: V2
2409            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
2410        } else {
2411            // otherwise V1
2412            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
2413        };
2414
2415        (execution_payload, sidecar)
2416    }
2417
2418    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2419    /// [`ExecutionPayloadSidecar`] extracted from the block along with block access list.
2420    ///
2421    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads.
2422    ///
2423    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2424    /// See also [`ExecutionPayloadSidecar::from_block`].
2425    pub fn from_block_unchecked_with_bal<T, H>(
2426        block_hash: B256,
2427        block: &Block<T, H>,
2428        block_access_list: Bytes,
2429    ) -> (Self, ExecutionPayloadSidecar)
2430    where
2431        T: Encodable2718 + Transaction,
2432        H: BlockHeader,
2433    {
2434        let sidecar = ExecutionPayloadSidecar::from_block(block);
2435
2436        let execution_payload = if block.header.block_access_list_hash().is_some() {
2437            // block with block access list hash: V4 (Amsterdam)
2438            Self::V4(ExecutionPayloadV4::from_block_unchecked_with_bal(
2439                block_hash,
2440                block,
2441                block_access_list,
2442            ))
2443        } else if block.header.parent_beacon_block_root().is_some() {
2444            // block with parent beacon block root: V3
2445            Self::V3(ExecutionPayloadV3::from_block_unchecked(block_hash, block))
2446        } else if block.body.withdrawals.is_some() {
2447            // block with withdrawals: V2
2448            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
2449        } else {
2450            // otherwise V1
2451            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
2452        };
2453
2454        (execution_payload, sidecar)
2455    }
2456
2457    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2458    /// [`ExecutionPayloadSidecar`] extracted from the block along with optional extras.
2459    ///
2460    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads if provided.
2461    ///
2462    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2463    /// See also [`ExecutionPayloadSidecar::from_block`].
2464    pub fn from_block_unchecked_with_extras<T, H>(
2465        block_hash: B256,
2466        block: &Block<T, H>,
2467        extras: impl Into<PayloadExtras>,
2468    ) -> (Self, ExecutionPayloadSidecar)
2469    where
2470        T: Encodable2718 + Transaction,
2471        H: BlockHeader,
2472    {
2473        let extras = extras.into();
2474        if let Some(block_access_list) = extras.bal {
2475            Self::from_block_unchecked_with_bal(block_hash, block, block_access_list)
2476        } else {
2477            Self::from_block_unchecked(block_hash, block)
2478        }
2479    }
2480
2481    /// Tries to create a new unsealed block from the given payload and payload sidecar.
2482    ///
2483    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
2484    /// The payload's advertised `block_hash` is not recomputed or compared.
2485    ///
2486    /// # Note
2487    ///
2488    /// The log bloom is assumed to be validated during serialization.
2489    ///
2490    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
2491    pub fn try_into_block_with_sidecar<T: Decodable2718>(
2492        self,
2493        sidecar: &ExecutionPayloadSidecar,
2494    ) -> Result<Block<T>, PayloadError> {
2495        self.try_into_block_with_sidecar_with(sidecar, |tx| {
2496            T::decode_2718_exact(tx.as_ref())
2497                .map_err(alloy_rlp::Error::from)
2498                .map_err(PayloadError::from)
2499        })
2500    }
2501
2502    /// Converts [`ExecutionPayload`] to [`Block`] with sidecar and a custom transaction mapper.
2503    ///
2504    /// The log bloom is assumed to be validated during serialization.
2505    ///
2506    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
2507    pub fn try_into_block_with_sidecar_with<T, F, E>(
2508        self,
2509        sidecar: &ExecutionPayloadSidecar,
2510        f: F,
2511    ) -> Result<Block<T>, PayloadError>
2512    where
2513        F: FnMut(Bytes) -> Result<T, E>,
2514        E: Into<PayloadError>,
2515    {
2516        self.into_block_with_sidecar_raw(sidecar)?.try_map_transactions(f).map_err(Into::into)
2517    }
2518
2519    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions and sidecar.
2520    ///
2521    /// This is similar to [`Self::try_into_block_with_sidecar_with`] but returns the transactions
2522    /// as raw bytes without any conversion.
2523    pub fn into_block_with_sidecar_raw(
2524        self,
2525        sidecar: &ExecutionPayloadSidecar,
2526    ) -> Result<Block<Bytes>, PayloadError> {
2527        let mut base_block = self.into_block_raw()?;
2528        base_block.header.parent_beacon_block_root = sidecar.parent_beacon_block_root();
2529        base_block.header.requests_hash = sidecar.requests_hash();
2530        Ok(base_block)
2531    }
2532
2533    /// Converts [`ExecutionPayload`] to [`Block`].
2534    ///
2535    /// The returned block is unsealed, and the payload's advertised `block_hash` is not recomputed
2536    /// or compared.
2537    ///
2538    /// Caution: This does not set fields that are not part of the payload and only part of the
2539    /// [`ExecutionPayloadSidecar`]:
2540    /// - parent_beacon_block_root
2541    /// - requests_hash
2542    ///
2543    /// See also: [`ExecutionPayload::try_into_block_with_sidecar`]
2544    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
2545        self.try_into_block_with(|tx| {
2546            T::decode_2718_exact(tx.as_ref())
2547                .map_err(alloy_rlp::Error::from)
2548                .map_err(PayloadError::from)
2549        })
2550    }
2551
2552    /// Converts [`ExecutionPayload`] to [`Block`] with a custom transaction mapper.
2553    ///
2554    /// Caution: This does not set fields that are not part of the payload and only part of the
2555    /// [`ExecutionPayloadSidecar`]:
2556    /// - parent_beacon_block_root
2557    /// - requests_hash
2558    ///
2559    /// See also: [`ExecutionPayload::try_into_block_with_sidecar`]
2560    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
2561    where
2562        F: FnMut(Bytes) -> Result<T, E>,
2563        E: Into<PayloadError>,
2564    {
2565        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
2566    }
2567
2568    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions.
2569    ///
2570    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
2571    /// without any conversion.
2572    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
2573        self.into_block_raw_with_transactions_root_opt(None)
2574    }
2575
2576    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions using the
2577    /// given `transactions_root`.
2578    ///
2579    /// See also [`ExecutionPayloadV1::into_block_raw_with_transactions_root`].
2580    pub fn into_block_raw_with_transactions_root(
2581        self,
2582        transactions_root: B256,
2583    ) -> Result<Block<Bytes>, PayloadError> {
2584        self.into_block_raw_with_transactions_root_opt(Some(transactions_root))
2585    }
2586
2587    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions, optionally
2588    /// using the given `transactions_root`.
2589    ///
2590    /// If `transactions_root` is `None`, it will be computed from the transactions.
2591    pub fn into_block_raw_with_transactions_root_opt(
2592        self,
2593        transactions_root: Option<B256>,
2594    ) -> Result<Block<Bytes>, PayloadError> {
2595        match self {
2596            Self::V1(payload) => {
2597                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2598            }
2599            Self::V2(payload) => {
2600                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2601            }
2602            Self::V3(payload) => {
2603                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2604            }
2605            Self::V4(payload) => {
2606                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2607            }
2608        }
2609    }
2610
2611    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions and sidecar
2612    /// using the given `transactions_root`.
2613    ///
2614    /// See also [`Self::into_block_with_sidecar_raw`].
2615    pub fn into_block_with_sidecar_raw_with_transactions_root(
2616        self,
2617        sidecar: &ExecutionPayloadSidecar,
2618        transactions_root: B256,
2619    ) -> Result<Block<Bytes>, PayloadError> {
2620        let mut base_block = self.into_block_raw_with_transactions_root(transactions_root)?;
2621        base_block.header.parent_beacon_block_root = sidecar.parent_beacon_block_root();
2622        base_block.header.requests_hash = sidecar.requests_hash();
2623        Ok(base_block)
2624    }
2625
2626    /// Returns a reference to the V1 payload.
2627    pub const fn as_v1(&self) -> &ExecutionPayloadV1 {
2628        match self {
2629            Self::V1(payload) => payload,
2630            Self::V2(payload) => &payload.payload_inner,
2631            Self::V3(payload) => &payload.payload_inner.payload_inner,
2632            Self::V4(payload) => &payload.payload_inner.payload_inner.payload_inner,
2633        }
2634    }
2635
2636    /// Returns a mutable reference to the V1 payload.
2637    pub const fn as_v1_mut(&mut self) -> &mut ExecutionPayloadV1 {
2638        match self {
2639            Self::V1(payload) => payload,
2640            Self::V2(payload) => &mut payload.payload_inner,
2641            Self::V3(payload) => &mut payload.payload_inner.payload_inner,
2642            Self::V4(payload) => &mut payload.payload_inner.payload_inner.payload_inner,
2643        }
2644    }
2645
2646    /// Consumes the payload and returns the V1 payload.
2647    pub fn into_v1(self) -> ExecutionPayloadV1 {
2648        match self {
2649            Self::V1(payload) => payload,
2650            Self::V2(payload) => payload.payload_inner,
2651            Self::V3(payload) => payload.payload_inner.payload_inner,
2652            Self::V4(payload) => payload.payload_inner.payload_inner.payload_inner,
2653        }
2654    }
2655
2656    /// Returns a reference to the V2 payload, if any.
2657    pub const fn as_v2(&self) -> Option<&ExecutionPayloadV2> {
2658        match self {
2659            Self::V1(_) => None,
2660            Self::V2(payload) => Some(payload),
2661            Self::V3(payload) => Some(&payload.payload_inner),
2662            Self::V4(payload) => Some(&payload.payload_inner.payload_inner),
2663        }
2664    }
2665
2666    /// Returns a mutable reference to the V2 payload, if any.
2667    pub const fn as_v2_mut(&mut self) -> Option<&mut ExecutionPayloadV2> {
2668        match self {
2669            Self::V1(_) => None,
2670            Self::V2(payload) => Some(payload),
2671            Self::V3(payload) => Some(&mut payload.payload_inner),
2672            Self::V4(payload) => Some(&mut payload.payload_inner.payload_inner),
2673        }
2674    }
2675
2676    /// Returns a reference to the V3 payload, if any.
2677    pub const fn as_v3(&self) -> Option<&ExecutionPayloadV3> {
2678        match self {
2679            Self::V1(_) | Self::V2(_) => None,
2680            Self::V3(payload) => Some(payload),
2681            Self::V4(payload) => Some(&payload.payload_inner),
2682        }
2683    }
2684
2685    /// Returns a mutable reference to the V3 payload, if any.
2686    pub const fn as_v3_mut(&mut self) -> Option<&mut ExecutionPayloadV3> {
2687        match self {
2688            Self::V1(_) | Self::V2(_) => None,
2689            Self::V3(payload) => Some(payload),
2690            Self::V4(payload) => Some(&mut payload.payload_inner),
2691        }
2692    }
2693
2694    /// Returns a reference to the V4 payload, if any.
2695    pub const fn as_v4(&self) -> Option<&ExecutionPayloadV4> {
2696        match self {
2697            Self::V1(_) | Self::V2(_) | Self::V3(_) => None,
2698            Self::V4(payload) => Some(payload),
2699        }
2700    }
2701
2702    /// Returns a mutable reference to the V4 payload, if any.
2703    pub const fn as_v4_mut(&mut self) -> Option<&mut ExecutionPayloadV4> {
2704        match self {
2705            Self::V1(_) | Self::V2(_) | Self::V3(_) => None,
2706            Self::V4(payload) => Some(payload),
2707        }
2708    }
2709
2710    /// Returns the withdrawals for the payload.
2711    pub const fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
2712        match self.as_v2() {
2713            Some(payload) => Some(&payload.withdrawals),
2714            None => None,
2715        }
2716    }
2717
2718    /// Returns the transactions for the payload.
2719    pub const fn transactions(&self) -> &Vec<Bytes> {
2720        &self.as_v1().transactions
2721    }
2722
2723    /// Returns a mutable reference to the transactions for the payload.
2724    pub const fn transactions_mut(&mut self) -> &mut Vec<Bytes> {
2725        &mut self.as_v1_mut().transactions
2726    }
2727
2728    /// Extracts essential information into one container type.
2729    pub fn header_info(&self) -> HeaderInfo {
2730        HeaderInfo {
2731            number: self.block_number(),
2732            beneficiary: self.fee_recipient(),
2733            timestamp: self.timestamp(),
2734            gas_limit: self.gas_limit(),
2735            base_fee_per_gas: Some(self.saturated_base_fee_per_gas()),
2736            excess_blob_gas: self.excess_blob_gas(),
2737            blob_gas_used: self.blob_gas_used(),
2738            difficulty: U256::ZERO,
2739            mix_hash: Some(self.prev_randao()),
2740            slot_number: self.as_v4().map(|payload| payload.slot_number),
2741        }
2742    }
2743
2744    /// Returns the gas limit for the payload.
2745    ///
2746    /// Note: this returns the u64 saturated base fee, but it is specified as [`U256`].
2747    pub fn saturated_base_fee_per_gas(&self) -> u64 {
2748        self.as_v1().base_fee_per_gas.saturating_to()
2749    }
2750
2751    /// Returns the blob gas used for the payload.
2752    pub fn blob_gas_used(&self) -> Option<u64> {
2753        self.as_v3().map(|payload| payload.blob_gas_used)
2754    }
2755
2756    /// Returns the excess blob gas for the payload.
2757    pub fn excess_blob_gas(&self) -> Option<u64> {
2758        self.as_v3().map(|payload| payload.excess_blob_gas)
2759    }
2760
2761    /// Returns the block access list for the payload (EIP-7928).
2762    ///
2763    /// Returns `None` for pre-Amsterdam payloads (V1, V2, V3).
2764    pub fn block_access_list(&self) -> Option<&Bytes> {
2765        self.as_v4().map(|payload| &payload.block_access_list)
2766    }
2767
2768    /// Returns the block access list hash for the payload (EIP-7928).
2769    ///
2770    /// Returns `None` for pre-Amsterdam payloads (V1, V2, V3).
2771    pub fn bal_hash(&self) -> Option<B256> {
2772        self.as_v4().map(|payload| keccak256(&payload.block_access_list))
2773    }
2774
2775    /// Returns the slot number for the payload (EIP-7843).
2776    ///
2777    /// Returns `None` for pre-Amsterdam payloads (V1, V2, V3).
2778    pub fn slot_number(&self) -> Option<u64> {
2779        self.as_v4().map(|payload| payload.slot_number)
2780    }
2781
2782    /// Returns the gas limit for the payload.
2783    pub const fn gas_limit(&self) -> u64 {
2784        self.as_v1().gas_limit
2785    }
2786
2787    /// Returns the fee recipient.
2788    pub const fn fee_recipient(&self) -> Address {
2789        self.as_v1().fee_recipient
2790    }
2791
2792    /// Returns the timestamp for the payload.
2793    pub const fn timestamp(&self) -> u64 {
2794        self.as_v1().timestamp
2795    }
2796
2797    /// Returns the parent hash for the payload.
2798    pub const fn parent_hash(&self) -> B256 {
2799        self.as_v1().parent_hash
2800    }
2801
2802    /// Returns the block hash for the payload.
2803    pub const fn block_hash(&self) -> B256 {
2804        self.as_v1().block_hash
2805    }
2806
2807    /// Returns the block number for this payload.
2808    pub const fn block_number(&self) -> u64 {
2809        self.as_v1().block_number
2810    }
2811
2812    /// Returns the block number for this payload.
2813    pub const fn block_num_hash(&self) -> BlockNumHash {
2814        self.as_v1().block_num_hash()
2815    }
2816
2817    /// Returns the prev randao for this payload.
2818    pub const fn prev_randao(&self) -> B256 {
2819        self.as_v1().prev_randao
2820    }
2821
2822    /// Returns the blob fee for _this_ block according to the EIP-4844 spec.
2823    ///
2824    /// Returns `None` if `excess_blob_gas` is None
2825    pub fn blob_fee(&self, blob_params: BlobParams) -> Option<u128> {
2826        Some(blob_params.calc_blob_fee(self.excess_blob_gas()?))
2827    }
2828
2829    /// Returns the blob fee for the next block according to the EIP-4844 spec.
2830    ///
2831    /// Returns `None` if `excess_blob_gas` is None.
2832    ///
2833    /// See also [Self::next_block_excess_blob_gas]
2834    pub fn next_block_blob_fee(&self, blob_params: BlobParams) -> Option<u128> {
2835        Some(blob_params.calc_blob_fee(self.next_block_excess_blob_gas(blob_params)?))
2836    }
2837
2838    /// Calculate base fee for next block according to the EIP-1559 spec.
2839    ///
2840    /// Returns a `None` if no base fee is set, no EIP-1559 support
2841    pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option<u64> {
2842        self.as_v1().next_block_base_fee(base_fee_params)
2843    }
2844
2845    /// Calculate excess blob gas for the next block according to the EIP-4844
2846    /// spec.
2847    ///
2848    /// Returns a `None` if no excess blob gas is set, no EIP-4844 support
2849    pub fn next_block_excess_blob_gas(&self, blob_params: BlobParams) -> Option<u64> {
2850        Some(blob_params.next_block_excess_blob_gas_osaka(
2851            self.excess_blob_gas()?,
2852            self.blob_gas_used()?,
2853            self.as_v1().base_fee_per_gas.to(),
2854        ))
2855    }
2856
2857    /// Convenience function for [`Self::next_block_excess_blob_gas`] with an optional
2858    /// [`BlobParams`] argument.
2859    ///
2860    /// Returns `None` if the `blob_params` are `None`.
2861    pub fn maybe_next_block_excess_blob_gas(&self, blob_params: Option<BlobParams>) -> Option<u64> {
2862        self.next_block_excess_blob_gas(blob_params?)
2863    }
2864
2865    /// Returns an iterator over the decoded transactions in this payload.
2866    ///
2867    /// This iterator will decode transactions on the fly.
2868    pub fn decoded_transactions<T: Decodable2718>(
2869        &self,
2870    ) -> impl Iterator<Item = Eip2718Result<T>> + '_ {
2871        self.transactions().iter().map(|tx_bytes| T::decode_2718_exact(tx_bytes.as_ref()))
2872    }
2873
2874    /// Returns iterator over decoded transactions with their original encoded bytes.
2875    ///
2876    /// This iterator will decode transactions on the fly and return them with their bytes.
2877    pub fn decoded_transactions_with_encoded<T: Decodable2718>(
2878        &self,
2879    ) -> impl Iterator<Item = Eip2718Result<WithEncoded<T>>> + '_ {
2880        self.transactions().iter().map(|tx_bytes| {
2881            T::decode_2718_exact(tx_bytes.as_ref()).map(|tx| WithEncoded::new(tx_bytes.clone(), tx))
2882        })
2883    }
2884
2885    /// Returns an iterator over the recovered transactions in this payload.
2886    ///
2887    /// This iterator will decode and recover signer addresses for transactions on the fly.
2888    pub fn recovered_transactions<T>(
2889        &self,
2890    ) -> impl Iterator<
2891        Item = Result<
2892            alloy_consensus::transaction::Recovered<T>,
2893            alloy_consensus::crypto::RecoveryError,
2894        >,
2895    > + '_
2896    where
2897        T: Decodable2718 + alloy_consensus::transaction::SignerRecoverable,
2898    {
2899        self.decoded_transactions::<T>().map(|res| {
2900            res.map_err(alloy_consensus::crypto::RecoveryError::from_source)
2901                .and_then(|tx| tx.try_into_recovered())
2902        })
2903    }
2904
2905    /// Returns an iterator over the recovered transactions in this payload with their
2906    /// original encoded bytes.
2907    ///
2908    /// This iterator will decode and recover signer addresses for transactions on the fly
2909    /// and return them with their bytes.
2910    pub fn recovered_transactions_with_encoded<T>(
2911        &self,
2912    ) -> impl Iterator<
2913        Item = Result<
2914            WithEncoded<alloy_consensus::transaction::Recovered<T>>,
2915            alloy_consensus::crypto::RecoveryError,
2916        >,
2917    > + '_
2918    where
2919        T: Decodable2718 + alloy_consensus::transaction::SignerRecoverable,
2920    {
2921        self.transactions().iter().map(|tx_bytes| {
2922            T::decode_2718_exact(tx_bytes.as_ref())
2923                .map_err(alloy_consensus::crypto::RecoveryError::from_source)
2924                .and_then(|tx| {
2925                    tx.try_into_recovered()
2926                        .map(|recovered| WithEncoded::new(tx_bytes.clone(), recovered))
2927                })
2928        })
2929    }
2930
2931    /// Sets the parent hash for the payload.
2932    #[doc(hidden)]
2933    pub const fn set_parent_hash(&mut self, parent_hash: B256) {
2934        self.as_v1_mut().parent_hash = parent_hash;
2935    }
2936
2937    /// Sets the fee recipient for the payload.
2938    #[doc(hidden)]
2939    pub const fn set_fee_recipient(&mut self, fee_recipient: Address) {
2940        self.as_v1_mut().fee_recipient = fee_recipient;
2941    }
2942
2943    /// Sets the state root for the payload.
2944    #[doc(hidden)]
2945    pub const fn set_state_root(&mut self, state_root: B256) {
2946        self.as_v1_mut().state_root = state_root;
2947    }
2948
2949    /// Sets the receipts root for the payload.
2950    #[doc(hidden)]
2951    pub const fn set_receipts_root(&mut self, receipts_root: B256) {
2952        self.as_v1_mut().receipts_root = receipts_root;
2953    }
2954
2955    /// Sets the logs bloom for the payload.
2956    #[doc(hidden)]
2957    pub const fn set_logs_bloom(&mut self, logs_bloom: Bloom) {
2958        self.as_v1_mut().logs_bloom = logs_bloom;
2959    }
2960
2961    /// Sets the prev randao for the payload.
2962    #[doc(hidden)]
2963    pub const fn set_prev_randao(&mut self, prev_randao: B256) {
2964        self.as_v1_mut().prev_randao = prev_randao;
2965    }
2966
2967    /// Sets the block number for the payload.
2968    #[doc(hidden)]
2969    pub const fn set_block_number(&mut self, block_number: u64) {
2970        self.as_v1_mut().block_number = block_number;
2971    }
2972
2973    /// Sets the gas limit for the payload.
2974    #[doc(hidden)]
2975    pub const fn set_gas_limit(&mut self, gas_limit: u64) {
2976        self.as_v1_mut().gas_limit = gas_limit;
2977    }
2978
2979    /// Sets the gas used for the payload.
2980    #[doc(hidden)]
2981    pub const fn set_gas_used(&mut self, gas_used: u64) {
2982        self.as_v1_mut().gas_used = gas_used;
2983    }
2984
2985    /// Sets the timestamp for the payload.
2986    #[doc(hidden)]
2987    pub const fn set_timestamp(&mut self, timestamp: u64) {
2988        self.as_v1_mut().timestamp = timestamp;
2989    }
2990
2991    /// Sets the extra data for the payload.
2992    #[doc(hidden)]
2993    pub fn set_extra_data(&mut self, extra_data: Bytes) {
2994        self.as_v1_mut().extra_data = extra_data;
2995    }
2996
2997    /// Sets the base fee per gas for the payload.
2998    #[doc(hidden)]
2999    pub const fn set_base_fee_per_gas(&mut self, base_fee_per_gas: U256) {
3000        self.as_v1_mut().base_fee_per_gas = base_fee_per_gas;
3001    }
3002
3003    /// Sets the block hash for the payload.
3004    #[doc(hidden)]
3005    pub const fn set_block_hash(&mut self, block_hash: B256) {
3006        self.as_v1_mut().block_hash = block_hash;
3007    }
3008
3009    /// Sets the withdrawals for the payload.
3010    ///
3011    /// Returns `true` if the payload is V2 or higher and the withdrawals were set.
3012    #[doc(hidden)]
3013    pub fn set_withdrawals(&mut self, withdrawals: Vec<Withdrawal>) -> bool {
3014        match self.as_v2_mut() {
3015            Some(payload) => {
3016                payload.withdrawals = withdrawals;
3017                true
3018            }
3019            None => false,
3020        }
3021    }
3022
3023    /// Sets the blob gas used for the payload.
3024    ///
3025    /// Returns `true` if the payload is V3 or higher and the value was set.
3026    #[doc(hidden)]
3027    pub const fn set_blob_gas_used(&mut self, blob_gas_used: u64) -> bool {
3028        match self.as_v3_mut() {
3029            Some(payload) => {
3030                payload.blob_gas_used = blob_gas_used;
3031                true
3032            }
3033            None => false,
3034        }
3035    }
3036
3037    /// Sets the excess blob gas for the payload.
3038    ///
3039    /// Returns `true` if the payload is V3 or higher and the value was set.
3040    #[doc(hidden)]
3041    pub const fn set_excess_blob_gas(&mut self, excess_blob_gas: u64) -> bool {
3042        match self.as_v3_mut() {
3043            Some(payload) => {
3044                payload.excess_blob_gas = excess_blob_gas;
3045                true
3046            }
3047            None => false,
3048        }
3049    }
3050}
3051
3052impl From<ExecutionPayloadV1> for ExecutionPayload {
3053    fn from(payload: ExecutionPayloadV1) -> Self {
3054        Self::V1(payload)
3055    }
3056}
3057
3058impl From<ExecutionPayloadV2> for ExecutionPayload {
3059    fn from(payload: ExecutionPayloadV2) -> Self {
3060        Self::V2(payload)
3061    }
3062}
3063
3064impl From<ExecutionPayloadFieldV2> for ExecutionPayload {
3065    fn from(payload: ExecutionPayloadFieldV2) -> Self {
3066        payload.into_payload()
3067    }
3068}
3069
3070impl From<ExecutionPayloadV3> for ExecutionPayload {
3071    fn from(payload: ExecutionPayloadV3) -> Self {
3072        Self::V3(payload)
3073    }
3074}
3075
3076impl From<ExecutionPayloadV4> for ExecutionPayload {
3077    fn from(payload: ExecutionPayloadV4) -> Self {
3078        Self::V4(payload)
3079    }
3080}
3081
3082impl<T: Decodable2718> TryFrom<ExecutionPayload> for Block<T> {
3083    type Error = PayloadError;
3084
3085    fn try_from(value: ExecutionPayload) -> Result<Self, Self::Error> {
3086        value.try_into_block()
3087    }
3088}
3089
3090// Deserializes untagged ExecutionPayload depending on the available fields
3091#[cfg(feature = "serde")]
3092impl<'de> serde::Deserialize<'de> for ExecutionPayload {
3093    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3094    where
3095        D: serde::Deserializer<'de>,
3096    {
3097        use alloy_primitives::U64;
3098
3099        struct ExecutionPayloadVisitor;
3100
3101        impl<'de> serde::de::Visitor<'de> for ExecutionPayloadVisitor {
3102            type Value = ExecutionPayload;
3103
3104            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3105                formatter.write_str("a valid ExecutionPayload object")
3106            }
3107
3108            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
3109            where
3110                A: serde::de::MapAccess<'de>,
3111            {
3112                // this currently rejects unknown fields
3113                #[cfg_attr(feature = "serde", derive(serde::Deserialize))]
3114                #[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "camelCase"))]
3115                enum Fields {
3116                    ParentHash,
3117                    FeeRecipient,
3118                    StateRoot,
3119                    ReceiptsRoot,
3120                    LogsBloom,
3121                    PrevRandao,
3122                    BlockNumber,
3123                    GasLimit,
3124                    GasUsed,
3125                    Timestamp,
3126                    ExtraData,
3127                    BaseFeePerGas,
3128                    BlockHash,
3129                    Transactions,
3130                    // V2
3131                    Withdrawals,
3132                    // V3
3133                    BlobGasUsed,
3134                    ExcessBlobGas,
3135                    // V4
3136                    BlockAccessList,
3137                    SlotNumber,
3138                }
3139
3140                let mut parent_hash = None;
3141                let mut fee_recipient = None;
3142                let mut state_root = None;
3143                let mut receipts_root = None;
3144                let mut logs_bloom = None;
3145                let mut prev_randao = None;
3146                let mut block_number = None;
3147                let mut gas_limit = None;
3148                let mut gas_used = None;
3149                let mut timestamp = None;
3150                let mut extra_data = None;
3151                let mut base_fee_per_gas = None;
3152                let mut block_hash = None;
3153                let mut transactions = None;
3154                let mut withdrawals = None;
3155                let mut blob_gas_used = None;
3156                let mut excess_blob_gas = None;
3157                let mut block_access_list = None;
3158                let mut slot_number = None;
3159
3160                while let Some(key) = map.next_key()? {
3161                    match key {
3162                        Fields::ParentHash => parent_hash = Some(map.next_value()?),
3163                        Fields::FeeRecipient => fee_recipient = Some(map.next_value()?),
3164                        Fields::StateRoot => state_root = Some(map.next_value()?),
3165                        Fields::ReceiptsRoot => receipts_root = Some(map.next_value()?),
3166                        Fields::LogsBloom => logs_bloom = Some(map.next_value()?),
3167                        Fields::PrevRandao => prev_randao = Some(map.next_value()?),
3168                        Fields::BlockNumber => {
3169                            let raw = map.next_value::<U64>()?;
3170                            block_number = Some(raw.to());
3171                        }
3172                        Fields::GasLimit => {
3173                            let raw = map.next_value::<U64>()?;
3174                            gas_limit = Some(raw.to());
3175                        }
3176                        Fields::GasUsed => {
3177                            let raw = map.next_value::<U64>()?;
3178                            gas_used = Some(raw.to());
3179                        }
3180                        Fields::Timestamp => {
3181                            let raw = map.next_value::<U64>()?;
3182                            timestamp = Some(raw.to());
3183                        }
3184                        Fields::ExtraData => extra_data = Some(map.next_value()?),
3185                        Fields::BaseFeePerGas => base_fee_per_gas = Some(map.next_value()?),
3186                        Fields::BlockHash => block_hash = Some(map.next_value()?),
3187                        Fields::Transactions => transactions = Some(map.next_value()?),
3188                        Fields::Withdrawals => withdrawals = Some(map.next_value()?),
3189                        Fields::BlobGasUsed => {
3190                            let raw = map.next_value::<U64>()?;
3191                            blob_gas_used = Some(raw.to());
3192                        }
3193                        Fields::ExcessBlobGas => {
3194                            let raw = map.next_value::<U64>()?;
3195                            excess_blob_gas = Some(raw.to());
3196                        }
3197                        Fields::BlockAccessList => {
3198                            block_access_list = Some(map.next_value()?);
3199                        }
3200                        Fields::SlotNumber => {
3201                            let raw = map.next_value::<U64>()?;
3202                            slot_number = Some(raw.to());
3203                        }
3204                    }
3205                }
3206
3207                let parent_hash =
3208                    parent_hash.ok_or_else(|| serde::de::Error::missing_field("parentHash"))?;
3209                let fee_recipient =
3210                    fee_recipient.ok_or_else(|| serde::de::Error::missing_field("feeRecipient"))?;
3211                let state_root =
3212                    state_root.ok_or_else(|| serde::de::Error::missing_field("stateRoot"))?;
3213                let receipts_root =
3214                    receipts_root.ok_or_else(|| serde::de::Error::missing_field("receiptsRoot"))?;
3215                let logs_bloom =
3216                    logs_bloom.ok_or_else(|| serde::de::Error::missing_field("logsBloom"))?;
3217                let prev_randao =
3218                    prev_randao.ok_or_else(|| serde::de::Error::missing_field("prevRandao"))?;
3219                let block_number =
3220                    block_number.ok_or_else(|| serde::de::Error::missing_field("blockNumber"))?;
3221                let gas_limit =
3222                    gas_limit.ok_or_else(|| serde::de::Error::missing_field("gasLimit"))?;
3223                let gas_used =
3224                    gas_used.ok_or_else(|| serde::de::Error::missing_field("gasUsed"))?;
3225                let timestamp =
3226                    timestamp.ok_or_else(|| serde::de::Error::missing_field("timestamp"))?;
3227                let extra_data =
3228                    extra_data.ok_or_else(|| serde::de::Error::missing_field("extraData"))?;
3229                let base_fee_per_gas = base_fee_per_gas
3230                    .ok_or_else(|| serde::de::Error::missing_field("baseFeePerGas"))?;
3231                let block_hash =
3232                    block_hash.ok_or_else(|| serde::de::Error::missing_field("blockHash"))?;
3233                let transactions =
3234                    transactions.ok_or_else(|| serde::de::Error::missing_field("transactions"))?;
3235
3236                let v1 = ExecutionPayloadV1 {
3237                    parent_hash,
3238                    fee_recipient,
3239                    state_root,
3240                    receipts_root,
3241                    logs_bloom,
3242                    prev_randao,
3243                    block_number,
3244                    gas_limit,
3245                    gas_used,
3246                    timestamp,
3247                    extra_data,
3248                    base_fee_per_gas,
3249                    block_hash,
3250                    transactions,
3251                };
3252
3253                let Some(withdrawals) = withdrawals else {
3254                    return if blob_gas_used.is_none() && excess_blob_gas.is_none() {
3255                        Ok(ExecutionPayload::V1(v1))
3256                    } else {
3257                        Err(serde::de::Error::custom("invalid enum variant"))
3258                    };
3259                };
3260
3261                if let (Some(blob_gas_used), Some(excess_blob_gas)) =
3262                    (blob_gas_used, excess_blob_gas)
3263                {
3264                    let v3 = ExecutionPayloadV3 {
3265                        payload_inner: ExecutionPayloadV2 { payload_inner: v1, withdrawals },
3266                        blob_gas_used,
3267                        excess_blob_gas,
3268                    };
3269
3270                    // Check for V4 fields (block_access_list and slot_number)
3271                    return match (block_access_list, slot_number) {
3272                        (Some(block_access_list), Some(slot_number)) => {
3273                            Ok(ExecutionPayload::V4(ExecutionPayloadV4 {
3274                                payload_inner: v3,
3275                                block_access_list,
3276                                slot_number,
3277                            }))
3278                        }
3279                        // reject incomplete V4 payloads
3280                        (None, None) => Ok(ExecutionPayload::V3(v3)),
3281                        _ => Err(serde::de::Error::custom("invalid enum variant")),
3282                    };
3283                }
3284
3285                // reject incomplete V3 payloads even if they could construct a valid V2
3286                if blob_gas_used.is_some() || excess_blob_gas.is_some() {
3287                    return Err(serde::de::Error::custom("invalid enum variant"));
3288                }
3289
3290                // reject V4 fields without V3 fields
3291                if block_access_list.is_some() || slot_number.is_some() {
3292                    return Err(serde::de::Error::custom("invalid enum variant"));
3293                }
3294
3295                Ok(ExecutionPayload::V2(ExecutionPayloadV2 { payload_inner: v1, withdrawals }))
3296            }
3297        }
3298
3299        const FIELDS: &[&str] = &[
3300            "parentHash",
3301            "feeRecipient",
3302            "stateRoot",
3303            "receiptsRoot",
3304            "logsBloom",
3305            "prevRandao",
3306            "blockNumber",
3307            "gasLimit",
3308            "gasUsed",
3309            "timestamp",
3310            "extraData",
3311            "baseFeePerGas",
3312            "blockHash",
3313            "transactions",
3314            "withdrawals",
3315            "blobGasUsed",
3316            "excessBlobGas",
3317            "blockAccessList",
3318            "slotNumber",
3319        ];
3320        deserializer.deserialize_struct("ExecutionPayload", FIELDS, ExecutionPayloadVisitor)
3321    }
3322}
3323
3324/// This structure contains a body of an execution payload.
3325///
3326/// See also: <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#executionpayloadbodyv1>
3327#[derive(Clone, Debug, PartialEq, Eq)]
3328#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3329#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
3330#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3331pub struct ExecutionPayloadBodyV1 {
3332    /// Enveloped encoded transactions.
3333    pub transactions: Vec<Bytes>,
3334    /// All withdrawals in the block.
3335    ///
3336    /// Will always be `None` if pre shanghai.
3337    pub withdrawals: Option<Vec<Withdrawal>>,
3338}
3339
3340impl ExecutionPayloadBodyV1 {
3341    /// Creates an [`ExecutionPayloadBodyV1`] from the given withdrawals and transactions
3342    pub fn new<'a, T>(
3343        withdrawals: Option<Withdrawals>,
3344        transactions: impl IntoIterator<Item = &'a T>,
3345    ) -> Self
3346    where
3347        T: Encodable2718 + 'a,
3348    {
3349        Self {
3350            transactions: transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(),
3351            withdrawals: withdrawals.map(Withdrawals::into_inner),
3352        }
3353    }
3354
3355    /// Converts a [`alloy_consensus::Block`] into an execution payload body.
3356    pub fn from_block<T: Encodable2718, H>(block: Block<T, H>) -> Self {
3357        let BlockBody { withdrawals, transactions, .. } = block.into_body();
3358        Self::new(withdrawals, transactions.iter())
3359    }
3360}
3361
3362impl<T: Encodable2718, H> From<Block<T, H>> for ExecutionPayloadBodyV1 {
3363    fn from(value: Block<T, H>) -> Self {
3364        Self::from_block(value)
3365    }
3366}
3367
3368/// This structure contains a body of an execution payload (V2).
3369///
3370/// V2 extends V1 with the `blockAccessList` field introduced in EIP-7928.
3371///
3372/// See also: <https://eips.ethereum.org/EIPS/eip-7928>
3373#[derive(Clone, Debug, PartialEq, Eq)]
3374#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3375#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3376#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
3377#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3378pub struct ExecutionPayloadBodyV2 {
3379    /// Enveloped encoded transactions.
3380    pub transactions: Vec<Bytes>,
3381    /// All withdrawals in the block.
3382    ///
3383    /// Will always be `None` if pre shanghai.
3384    pub withdrawals: Option<Vec<Withdrawal>>,
3385    /// The RLP-encoded block access list.
3386    ///
3387    /// Will be `None` for pre-Amsterdam blocks or when data has been pruned.
3388    pub block_access_list: Option<Bytes>,
3389}
3390
3391impl ExecutionPayloadBodyV2 {
3392    /// Creates an [`ExecutionPayloadBodyV2`] from the given withdrawals, transactions, and block
3393    /// access list.
3394    pub fn new<'a, T>(
3395        withdrawals: Option<Withdrawals>,
3396        transactions: impl IntoIterator<Item = &'a T>,
3397        block_access_list: Option<Bytes>,
3398    ) -> Self
3399    where
3400        T: Encodable2718 + 'a,
3401    {
3402        Self {
3403            transactions: transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(),
3404            withdrawals: withdrawals.map(Withdrawals::into_inner),
3405            block_access_list,
3406        }
3407    }
3408
3409    /// Converts a [`alloy_consensus::Block`] into an execution payload body, with an optional
3410    /// block access list.
3411    pub fn from_block<T: Encodable2718, H>(
3412        block: Block<T, H>,
3413        block_access_list: Option<Bytes>,
3414    ) -> Self {
3415        let BlockBody { withdrawals, transactions, .. } = block.into_body();
3416        Self::new(withdrawals, transactions.iter(), block_access_list)
3417    }
3418}
3419
3420impl From<ExecutionPayloadBodyV1> for ExecutionPayloadBodyV2 {
3421    fn from(v1: ExecutionPayloadBodyV1) -> Self {
3422        Self { transactions: v1.transactions, withdrawals: v1.withdrawals, block_access_list: None }
3423    }
3424}
3425
3426impl From<ExecutionPayloadBodyV2> for ExecutionPayloadBodyV1 {
3427    fn from(v2: ExecutionPayloadBodyV2) -> Self {
3428        Self { transactions: v2.transactions, withdrawals: v2.withdrawals }
3429    }
3430}
3431
3432/// This structure contains the attributes required to initiate a payload build process in the
3433/// context of an `engine_forkchoiceUpdated` call.
3434#[derive(Clone, Debug, Default, PartialEq, Eq)]
3435#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3436#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3437#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3438pub struct PayloadAttributes {
3439    /// Value for the `timestamp` field of the new payload
3440    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
3441    pub timestamp: u64,
3442    /// Value for the `prevRandao` field of the new payload
3443    pub prev_randao: B256,
3444    /// Suggested value for the `feeRecipient` field of the new payload
3445    pub suggested_fee_recipient: Address,
3446    /// Array of [`Withdrawal`] enabled with V2
3447    /// See <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#payloadattributesv2>
3448    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3449    pub withdrawals: Option<Vec<Withdrawal>>,
3450    /// Root of the parent beacon block enabled with V3.
3451    ///
3452    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#payloadattributesv3>
3453    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3454    pub parent_beacon_block_root: Option<B256>,
3455    /// Slot of the current block enabled with Amsterdam fork.
3456    ///
3457    /// See <https://github.com/ethereum/execution-apis/pull/731>
3458    #[cfg_attr(
3459        feature = "serde",
3460        serde(
3461            default,
3462            skip_serializing_if = "Option::is_none",
3463            with = "alloy_serde::quantity::opt"
3464        )
3465    )]
3466    pub slot_number: Option<u64>,
3467    /// Gas limit of the current block enabled with Amsterdam fork.
3468    ///
3469    /// See <https://github.com/ethereum/execution-apis/pull/796>
3470    #[cfg_attr(
3471        feature = "serde",
3472        serde(
3473            default,
3474            skip_serializing_if = "Option::is_none",
3475            with = "alloy_serde::quantity::opt"
3476        )
3477    )]
3478    pub target_gas_limit: Option<u64>,
3479}
3480
3481impl PayloadAttributes {
3482    /// Sets the timestamp for the payload attributes.
3483    pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
3484        self.timestamp = timestamp;
3485        self
3486    }
3487
3488    /// Sets the withdrawals for the payload attributes.
3489    pub fn with_withdrawals(mut self, withdrawals: Vec<Withdrawal>) -> Self {
3490        self.withdrawals = Some(withdrawals);
3491        self
3492    }
3493
3494    /// Sets the parent beacon block root for the payload attributes.
3495    pub const fn with_parent_beacon_block_root(mut self, parent_beacon_block_root: B256) -> Self {
3496        self.parent_beacon_block_root = Some(parent_beacon_block_root);
3497        self
3498    }
3499
3500    /// Sets the slot number for the payload attributes.
3501    pub const fn with_slot_number(mut self, slot_number: u64) -> Self {
3502        self.slot_number = Some(slot_number);
3503        self
3504    }
3505}
3506
3507#[cfg(feature = "ssz")]
3508impl PayloadAttributes {
3509    fn ssz_v1_fixed_len() -> usize {
3510        <u64 as ssz::Encode>::ssz_fixed_len()
3511            + <B256 as ssz::Encode>::ssz_fixed_len()
3512            + <Address as ssz::Encode>::ssz_fixed_len()
3513    }
3514
3515    fn ssz_v2_fixed_len() -> usize {
3516        Self::ssz_v1_fixed_len() + <Vec<Withdrawal> as ssz::Encode>::ssz_fixed_len()
3517    }
3518
3519    fn ssz_v3_fixed_len() -> usize {
3520        Self::ssz_v2_fixed_len() + <B256 as ssz::Encode>::ssz_fixed_len()
3521    }
3522
3523    fn ssz_v4_slot_fixed_len() -> usize {
3524        Self::ssz_v3_fixed_len() + <u64 as ssz::Encode>::ssz_fixed_len()
3525    }
3526
3527    fn ssz_v4_target_fixed_len() -> usize {
3528        Self::ssz_v4_slot_fixed_len() + <u64 as ssz::Encode>::ssz_fixed_len()
3529    }
3530
3531    fn ssz_fixed_section_len(&self) -> usize {
3532        if self.target_gas_limit.is_some() {
3533            Self::ssz_v4_target_fixed_len()
3534        } else if self.slot_number.is_some() {
3535            Self::ssz_v4_slot_fixed_len()
3536        } else if self.parent_beacon_block_root.is_some() {
3537            Self::ssz_v3_fixed_len()
3538        } else if self.withdrawals.is_some() {
3539            Self::ssz_v2_fixed_len()
3540        } else {
3541            Self::ssz_v1_fixed_len()
3542        }
3543    }
3544}
3545
3546#[cfg(feature = "ssz")]
3547impl ssz::Encode for PayloadAttributes {
3548    fn is_ssz_fixed_len() -> bool {
3549        false
3550    }
3551
3552    fn ssz_append(&self, buf: &mut Vec<u8>) {
3553        let fixed_section_len = self.ssz_fixed_section_len();
3554        let mut encoder = ssz::SszEncoder::container(buf, fixed_section_len);
3555
3556        encoder.append(&self.timestamp);
3557        encoder.append(&self.prev_randao);
3558        encoder.append(&self.suggested_fee_recipient);
3559
3560        if fixed_section_len >= Self::ssz_v2_fixed_len() {
3561            let empty_withdrawals = Vec::new();
3562            let withdrawals = self.withdrawals.as_ref().unwrap_or(&empty_withdrawals);
3563            encoder.append(withdrawals);
3564        }
3565
3566        if fixed_section_len >= Self::ssz_v3_fixed_len() {
3567            encoder.append(&self.parent_beacon_block_root.unwrap_or_default());
3568        }
3569
3570        if fixed_section_len >= Self::ssz_v4_slot_fixed_len() {
3571            encoder.append(&self.slot_number.unwrap_or_default());
3572        }
3573
3574        if fixed_section_len == Self::ssz_v4_target_fixed_len() {
3575            encoder.append(&self.target_gas_limit.unwrap_or_default());
3576        }
3577
3578        encoder.finalize();
3579    }
3580
3581    fn ssz_bytes_len(&self) -> usize {
3582        let fixed_section_len = self.ssz_fixed_section_len();
3583        let withdrawals_len = if fixed_section_len >= Self::ssz_v2_fixed_len() {
3584            self.withdrawals.as_ref().map(ssz::Encode::ssz_bytes_len).unwrap_or_default()
3585        } else {
3586            0
3587        };
3588
3589        fixed_section_len + withdrawals_len
3590    }
3591}
3592
3593#[cfg(feature = "ssz")]
3594impl ssz::Decode for PayloadAttributes {
3595    fn is_ssz_fixed_len() -> bool {
3596        false
3597    }
3598
3599    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
3600        if bytes.len() == Self::ssz_v1_fixed_len() {
3601            let mut builder = ssz::SszDecoderBuilder::new(bytes);
3602
3603            builder.register_type::<u64>()?;
3604            builder.register_type::<B256>()?;
3605            builder.register_type::<Address>()?;
3606
3607            let mut decoder = builder.build()?;
3608
3609            return Ok(Self {
3610                timestamp: decoder.decode_next()?,
3611                prev_randao: decoder.decode_next()?,
3612                suggested_fee_recipient: decoder.decode_next()?,
3613                withdrawals: None,
3614                parent_beacon_block_root: None,
3615                slot_number: None,
3616                target_gas_limit: None,
3617            });
3618        }
3619
3620        if bytes.len() < Self::ssz_v2_fixed_len() {
3621            return Err(ssz::DecodeError::InvalidByteLength {
3622                len: bytes.len(),
3623                expected: Self::ssz_v2_fixed_len(),
3624            });
3625        }
3626
3627        let offset = u32::from_le_bytes([
3628            bytes[Self::ssz_v1_fixed_len()],
3629            bytes[Self::ssz_v1_fixed_len() + 1],
3630            bytes[Self::ssz_v1_fixed_len() + 2],
3631            bytes[Self::ssz_v1_fixed_len() + 3],
3632        ]) as usize;
3633
3634        let mut builder = ssz::SszDecoderBuilder::new(bytes);
3635
3636        builder.register_type::<u64>()?;
3637        builder.register_type::<B256>()?;
3638        builder.register_type::<Address>()?;
3639        builder.register_type::<Vec<Withdrawal>>()?;
3640
3641        match offset {
3642            offset if offset == Self::ssz_v2_fixed_len() => {
3643                let mut decoder = builder.build()?;
3644
3645                Ok(Self {
3646                    timestamp: decoder.decode_next()?,
3647                    prev_randao: decoder.decode_next()?,
3648                    suggested_fee_recipient: decoder.decode_next()?,
3649                    withdrawals: Some(decoder.decode_next()?),
3650                    parent_beacon_block_root: None,
3651                    slot_number: None,
3652                    target_gas_limit: None,
3653                })
3654            }
3655            offset if offset == Self::ssz_v3_fixed_len() => {
3656                builder.register_type::<B256>()?;
3657                let mut decoder = builder.build()?;
3658
3659                Ok(Self {
3660                    timestamp: decoder.decode_next()?,
3661                    prev_randao: decoder.decode_next()?,
3662                    suggested_fee_recipient: decoder.decode_next()?,
3663                    withdrawals: Some(decoder.decode_next()?),
3664                    parent_beacon_block_root: Some(decoder.decode_next()?),
3665                    slot_number: None,
3666                    target_gas_limit: None,
3667                })
3668            }
3669            offset if offset == Self::ssz_v4_slot_fixed_len() => {
3670                builder.register_type::<B256>()?;
3671                builder.register_type::<u64>()?;
3672                let mut decoder = builder.build()?;
3673
3674                Ok(Self {
3675                    timestamp: decoder.decode_next()?,
3676                    prev_randao: decoder.decode_next()?,
3677                    suggested_fee_recipient: decoder.decode_next()?,
3678                    withdrawals: Some(decoder.decode_next()?),
3679                    parent_beacon_block_root: Some(decoder.decode_next()?),
3680                    slot_number: Some(decoder.decode_next()?),
3681                    target_gas_limit: None,
3682                })
3683            }
3684            offset if offset == Self::ssz_v4_target_fixed_len() => {
3685                builder.register_type::<B256>()?;
3686                builder.register_type::<u64>()?;
3687                builder.register_type::<u64>()?;
3688                let mut decoder = builder.build()?;
3689
3690                Ok(Self {
3691                    timestamp: decoder.decode_next()?,
3692                    prev_randao: decoder.decode_next()?,
3693                    suggested_fee_recipient: decoder.decode_next()?,
3694                    withdrawals: Some(decoder.decode_next()?),
3695                    parent_beacon_block_root: Some(decoder.decode_next()?),
3696                    slot_number: Some(decoder.decode_next()?),
3697                    target_gas_limit: Some(decoder.decode_next()?),
3698                })
3699            }
3700            offset => Err(ssz::DecodeError::BytesInvalid(format!(
3701                "invalid PayloadAttributes SSZ fixed section offset: {offset}"
3702            ))),
3703        }
3704    }
3705}
3706
3707/// This structure contains the result of processing a payload or fork choice update.
3708#[derive(Clone, Debug, PartialEq, Eq)]
3709#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
3710#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3711#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3712pub struct PayloadStatus {
3713    /// The status of the payload.
3714    #[cfg_attr(feature = "serde", serde(flatten))]
3715    pub status: PayloadStatusEnum,
3716    /// Hash of the most recent valid block in the branch defined by payload and its ancestors
3717    pub latest_valid_hash: Option<B256>,
3718}
3719
3720impl PayloadStatus {
3721    /// Initializes a new payload status.
3722    pub const fn new(status: PayloadStatusEnum, latest_valid_hash: Option<B256>) -> Self {
3723        Self { status, latest_valid_hash }
3724    }
3725
3726    /// Creates a new payload status from the given status.
3727    pub const fn from_status(status: PayloadStatusEnum) -> Self {
3728        Self { status, latest_valid_hash: None }
3729    }
3730
3731    /// Sets the latest valid hash.
3732    pub const fn with_latest_valid_hash(mut self, latest_valid_hash: B256) -> Self {
3733        self.latest_valid_hash = Some(latest_valid_hash);
3734        self
3735    }
3736
3737    /// Sets the latest valid hash if it's not None.
3738    pub const fn maybe_latest_valid_hash(mut self, latest_valid_hash: Option<B256>) -> Self {
3739        self.latest_valid_hash = latest_valid_hash;
3740        self
3741    }
3742
3743    /// Returns true if the payload status is syncing.
3744    pub const fn is_syncing(&self) -> bool {
3745        self.status.is_syncing()
3746    }
3747
3748    /// Returns true if the payload status is valid.
3749    pub const fn is_valid(&self) -> bool {
3750        self.status.is_valid()
3751    }
3752
3753    /// Returns true if the payload status is invalid.
3754    pub const fn is_invalid(&self) -> bool {
3755        self.status.is_invalid()
3756    }
3757}
3758
3759#[cfg(feature = "ssz")]
3760impl ssz::Encode for PayloadStatus {
3761    fn is_ssz_fixed_len() -> bool {
3762        false
3763    }
3764
3765    fn ssz_append(&self, buf: &mut Vec<u8>) {
3766        let validation_error =
3767            self.status.validation_error().map(str::as_bytes).unwrap_or_default().to_vec();
3768        let latest_valid_hash = self.latest_valid_hash.unwrap_or_default();
3769        let offset = <u8 as ssz::Encode>::ssz_fixed_len()
3770            + <B256 as ssz::Encode>::ssz_fixed_len()
3771            + <Vec<u8> as ssz::Encode>::ssz_fixed_len();
3772        let mut encoder = ssz::SszEncoder::container(buf, offset);
3773
3774        encoder.append(&self.status.ssz_code());
3775        encoder.append(&latest_valid_hash);
3776        encoder.append(&validation_error);
3777
3778        encoder.finalize();
3779    }
3780
3781    fn ssz_bytes_len(&self) -> usize {
3782        let validation_error_len = self.status.validation_error().map(str::len).unwrap_or_default();
3783        <u8 as ssz::Encode>::ssz_fixed_len()
3784            + <B256 as ssz::Encode>::ssz_fixed_len()
3785            + <Vec<u8> as ssz::Encode>::ssz_fixed_len()
3786            + validation_error_len
3787    }
3788}
3789
3790#[cfg(feature = "ssz")]
3791impl ssz::Decode for PayloadStatus {
3792    fn is_ssz_fixed_len() -> bool {
3793        false
3794    }
3795
3796    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
3797        let mut builder = ssz::SszDecoderBuilder::new(bytes);
3798
3799        builder.register_type::<u8>()?;
3800        builder.register_type::<B256>()?;
3801        builder.register_type::<Vec<u8>>()?;
3802
3803        let mut decoder = builder.build()?;
3804        let status_code: u8 = decoder.decode_next()?;
3805        let latest_valid_hash: B256 = decoder.decode_next()?;
3806        let validation_error: Vec<u8> = decoder.decode_next()?;
3807
3808        let status = PayloadStatusEnum::from_ssz_code(status_code, validation_error)?;
3809        let latest_valid_hash = (!latest_valid_hash.is_zero()).then_some(latest_valid_hash);
3810
3811        Ok(Self { status, latest_valid_hash })
3812    }
3813}
3814
3815impl core::fmt::Display for PayloadStatus {
3816    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3817        write!(
3818            f,
3819            "PayloadStatus {{ status: {}, latestValidHash: {:?} }}",
3820            self.status, self.latest_valid_hash
3821        )
3822    }
3823}
3824
3825#[cfg(feature = "serde")]
3826impl serde::Serialize for PayloadStatus {
3827    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3828    where
3829        S: serde::Serializer,
3830    {
3831        use serde::ser::SerializeMap;
3832        let mut map = serializer.serialize_map(Some(3))?;
3833        map.serialize_entry("status", self.status.as_str())?;
3834        map.serialize_entry("latestValidHash", &self.latest_valid_hash)?;
3835        map.serialize_entry("validationError", &self.status.validation_error())?;
3836        map.end()
3837    }
3838}
3839
3840impl From<PayloadError> for PayloadStatusEnum {
3841    fn from(error: PayloadError) -> Self {
3842        Self::Invalid { validation_error: error.to_string() }
3843    }
3844}
3845
3846/// Represents the status response of a payload.
3847#[derive(Clone, Debug, PartialEq, Eq)]
3848#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3849#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "SCREAMING_SNAKE_CASE"))]
3850#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3851pub enum PayloadStatusEnum {
3852    /// VALID is returned by the engine API in the following calls:
3853    ///   - newPayload:       if the payload was already known or was just validated and executed
3854    ///   - forkchoiceUpdate: if the chain accepted the reorg (might ignore if it's stale)
3855    Valid,
3856
3857    /// INVALID is returned by the engine API in the following calls:
3858    ///   - newPayload:       if the payload failed to execute on top of the local chain
3859    ///   - forkchoiceUpdate: if the new head is unknown, pre-merge, or reorg to it fails
3860    Invalid {
3861        /// The error message for the invalid payload.
3862        #[cfg_attr(feature = "serde", serde(rename = "validationError"))]
3863        validation_error: String,
3864    },
3865
3866    /// SYNCING is returned by the engine API in the following calls:
3867    ///   - newPayload:       if the payload was accepted on top of an active sync
3868    ///   - forkchoiceUpdate: if the new head was seen before, but not part of the chain
3869    Syncing,
3870
3871    /// ACCEPTED is returned by the engine API in the following calls:
3872    ///   - newPayload: if the payload was accepted, but not processed (side chain)
3873    Accepted,
3874}
3875
3876impl PayloadStatusEnum {
3877    /// Returns the string representation of the payload status.
3878    pub const fn as_str(&self) -> &'static str {
3879        match self {
3880            Self::Valid => "VALID",
3881            Self::Invalid { .. } => "INVALID",
3882            Self::Syncing => "SYNCING",
3883            Self::Accepted => "ACCEPTED",
3884        }
3885    }
3886
3887    /// Returns the validation error if the payload status is invalid.
3888    pub fn validation_error(&self) -> Option<&str> {
3889        match self {
3890            Self::Invalid { validation_error } => Some(validation_error),
3891            _ => None,
3892        }
3893    }
3894
3895    /// Returns true if the payload status is syncing.
3896    pub const fn is_syncing(&self) -> bool {
3897        matches!(self, Self::Syncing)
3898    }
3899
3900    /// Returns true if the payload status is valid.
3901    pub const fn is_valid(&self) -> bool {
3902        matches!(self, Self::Valid)
3903    }
3904
3905    /// Returns true if the payload status is invalid.
3906    pub const fn is_invalid(&self) -> bool {
3907        matches!(self, Self::Invalid { .. })
3908    }
3909
3910    #[cfg(feature = "ssz")]
3911    const fn ssz_code(&self) -> u8 {
3912        match self {
3913            Self::Valid => 0,
3914            Self::Invalid { .. } => 1,
3915            Self::Syncing => 2,
3916            Self::Accepted => 3,
3917        }
3918    }
3919
3920    #[cfg(feature = "ssz")]
3921    fn from_ssz_code(status_code: u8, validation_error: Vec<u8>) -> Result<Self, ssz::DecodeError> {
3922        match status_code {
3923            0 => {
3924                if !validation_error.is_empty() {
3925                    return Err(ssz::DecodeError::BytesInvalid(
3926                        "unexpected validation error for VALID status".to_string(),
3927                    ));
3928                }
3929                Ok(Self::Valid)
3930            }
3931            1 => String::from_utf8(validation_error)
3932                .map(|validation_error| Self::Invalid { validation_error })
3933                .map_err(|err| ssz::DecodeError::BytesInvalid(err.to_string())),
3934            2 => {
3935                if !validation_error.is_empty() {
3936                    return Err(ssz::DecodeError::BytesInvalid(
3937                        "unexpected validation error for SYNCING status".to_string(),
3938                    ));
3939                }
3940                Ok(Self::Syncing)
3941            }
3942            3 => {
3943                if !validation_error.is_empty() {
3944                    return Err(ssz::DecodeError::BytesInvalid(
3945                        "unexpected validation error for ACCEPTED status".to_string(),
3946                    ));
3947                }
3948                Ok(Self::Accepted)
3949            }
3950            _ => Err(ssz::DecodeError::BytesInvalid("unknown payload status code".to_string())),
3951        }
3952    }
3953}
3954
3955impl core::fmt::Display for PayloadStatusEnum {
3956    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3957        match self {
3958            Self::Invalid { validation_error } => {
3959                f.write_str(self.as_str())?;
3960                f.write_str(": ")?;
3961                f.write_str(validation_error.as_str())
3962            }
3963            _ => f.write_str(self.as_str()),
3964        }
3965    }
3966}
3967
3968/// This structure contains the result of processing a payload in the Bogota Engine API.
3969///
3970/// It extends [`PayloadStatus`] with the EIP-7805 inclusion-list validation result.
3971///
3972/// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#payloadstatusv2>
3973#[derive(Clone, Debug, PartialEq, Eq)]
3974#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
3975#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3976#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3977pub struct PayloadStatusV2 {
3978    /// The common payload status fields.
3979    #[cfg_attr(feature = "serde", serde(flatten))]
3980    pub payload_inner: PayloadStatus,
3981    /// Whether the payload satisfied the inclusion-list constraints if it was deemed valid.
3982    #[cfg_attr(feature = "serde", serde(default))]
3983    pub inclusion_list_satisfied: Option<bool>,
3984}
3985
3986impl PayloadStatusV2 {
3987    /// Creates a new payload status.
3988    pub const fn new(
3989        payload_status: PayloadStatus,
3990        inclusion_list_satisfied: Option<bool>,
3991    ) -> Self {
3992        Self { payload_inner: payload_status, inclusion_list_satisfied }
3993    }
3994
3995    /// Sets whether the payload satisfied the inclusion-list constraints.
3996    pub const fn with_inclusion_list_satisfied(mut self, satisfied: bool) -> Self {
3997        self.inclusion_list_satisfied = Some(satisfied);
3998        self
3999    }
4000
4001    /// Returns true if the payload status is syncing.
4002    pub const fn is_syncing(&self) -> bool {
4003        self.payload_inner.is_syncing()
4004    }
4005
4006    /// Returns true if the payload status is valid.
4007    pub const fn is_valid(&self) -> bool {
4008        self.payload_inner.is_valid()
4009    }
4010
4011    /// Returns true if the payload status is invalid.
4012    pub const fn is_invalid(&self) -> bool {
4013        self.payload_inner.is_invalid()
4014    }
4015}
4016
4017impl From<PayloadStatus> for PayloadStatusV2 {
4018    fn from(payload_status: PayloadStatus) -> Self {
4019        Self::new(payload_status, None)
4020    }
4021}
4022
4023/// Downgrades a V2 payload status, discarding its inclusion-list validation result.
4024impl From<PayloadStatusV2> for PayloadStatus {
4025    fn from(payload_status: PayloadStatusV2) -> Self {
4026        payload_status.payload_inner
4027    }
4028}
4029
4030#[cfg(feature = "serde")]
4031impl serde::Serialize for PayloadStatusV2 {
4032    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4033    where
4034        S: serde::Serializer,
4035    {
4036        use serde::ser::SerializeMap;
4037
4038        let mut map = serializer.serialize_map(Some(4))?;
4039        map.serialize_entry("status", self.payload_inner.status.as_str())?;
4040        map.serialize_entry("latestValidHash", &self.payload_inner.latest_valid_hash)?;
4041        map.serialize_entry("validationError", &self.payload_inner.status.validation_error())?;
4042        map.serialize_entry("inclusionListSatisfied", &self.inclusion_list_satisfied)?;
4043        map.end()
4044    }
4045}
4046
4047/// Struct aggregating [`ExecutionPayload`] and [`ExecutionPayloadSidecar`] and encapsulating
4048/// complete payload supplied for execution.
4049#[derive(Debug, Clone)]
4050#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4051#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
4052pub struct ExecutionData {
4053    /// Execution payload.
4054    pub payload: ExecutionPayload,
4055    /// Additional fork-specific fields.
4056    pub sidecar: ExecutionPayloadSidecar,
4057}
4058
4059impl ExecutionData {
4060    /// Creates new instance of [`ExecutionData`].
4061    pub const fn new(payload: ExecutionPayload, sidecar: ExecutionPayloadSidecar) -> Self {
4062        Self { payload, sidecar }
4063    }
4064
4065    /// Conversion from [`alloy_consensus::Block`]. Also returns the [`ExecutionPayloadSidecar`]
4066    /// extracted from the block.
4067    ///
4068    /// For the [`ExecutionPayloadSidecar`] this is expected to use just the requests hash, because
4069    /// the [`Requests`] are not part of the block/header. See also
4070    /// [`RequestsOrHash`](alloy_eips::eip7685::RequestsOrHash).
4071    /// Likewise, Amsterdam/V4 payload conversion falls back to the header's
4072    /// `block_access_list_hash` bytes when the full RLP-encoded block access list is not
4073    /// available on the block value, or to the canonical empty BAL hash bytes if the header does
4074    /// not carry a BAL hash.
4075    ///
4076    /// See also [`ExecutionPayload::from_block_unchecked`].
4077    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
4078    where
4079        T: Encodable2718 + Transaction,
4080        H: BlockHeader,
4081    {
4082        let (payload, sidecar) = ExecutionPayload::from_block_unchecked(block_hash, block);
4083        Self::new(payload, sidecar)
4084    }
4085
4086    /// Returns the parent hash of the block.
4087    pub const fn parent_hash(&self) -> B256 {
4088        self.payload.parent_hash()
4089    }
4090
4091    /// Returns the hash of the block.
4092    pub const fn block_hash(&self) -> B256 {
4093        self.payload.block_hash()
4094    }
4095
4096    /// Returns the number of the block.
4097    pub const fn block_number(&self) -> u64 {
4098        self.payload.block_number()
4099    }
4100
4101    /// Returns the parent beacon block root, if any.
4102    pub fn parent_beacon_block_root(&self) -> Option<B256> {
4103        self.sidecar.parent_beacon_block_root()
4104    }
4105
4106    /// Return the withdrawals for the payload or attributes.
4107    pub const fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
4108        self.payload.withdrawals()
4109    }
4110
4111    /// Returns the number of transactions in the payload.
4112    pub const fn transaction_count(&self) -> usize {
4113        self.payload.transactions().len()
4114    }
4115
4116    /// Tries to create a new unsealed block from the given payload and payload sidecar.
4117    ///
4118    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
4119    /// The payload's advertised `block_hash` is not recomputed or compared.
4120    ///
4121    /// # Note
4122    ///
4123    /// The log bloom is assumed to be validated during serialization.
4124    ///
4125    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
4126    pub fn try_into_block<T: Decodable2718>(
4127        self,
4128    ) -> Result<alloy_consensus::Block<T>, PayloadError> {
4129        self.try_into_block_with(|tx| {
4130            T::decode_2718_exact(tx.as_ref())
4131                .map_err(alloy_rlp::Error::from)
4132                .map_err(PayloadError::from)
4133        })
4134    }
4135
4136    /// Tries to create a new unsealed block from the given payload and payload sidecar with a
4137    /// custom transaction mapper.
4138    ///
4139    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
4140    ///
4141    /// # Note
4142    ///
4143    /// The log bloom is assumed to be validated during serialization.
4144    ///
4145    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
4146    pub fn try_into_block_with<T, F, E>(
4147        self,
4148        f: F,
4149    ) -> Result<alloy_consensus::Block<T>, PayloadError>
4150    where
4151        F: FnMut(Bytes) -> Result<T, E>,
4152        E: Into<PayloadError>,
4153    {
4154        self.payload.try_into_block_with_sidecar_with(&self.sidecar, f)
4155    }
4156
4157    /// Converts [`ExecutionData`] to [`Block`] with raw [`Bytes`] transactions.
4158    ///
4159    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
4160    /// without any conversion.
4161    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
4162        let mut base_block = self.payload.into_block_raw()?;
4163        base_block.header.parent_beacon_block_root = self.sidecar.parent_beacon_block_root();
4164        base_block.header.requests_hash = self.sidecar.requests_hash();
4165        Ok(base_block)
4166    }
4167}
4168
4169impl<T, H> From<Sealed<Block<T, H>>> for ExecutionData
4170where
4171    T: Encodable2718 + Transaction,
4172    H: BlockHeader,
4173{
4174    fn from(sealed: Sealed<Block<T, H>>) -> Self {
4175        let (block, block_hash) = sealed.into_parts();
4176        Self::from_block_unchecked(block_hash, &block)
4177    }
4178}
4179
4180impl<T, H> From<Sealed<&Block<T, H>>> for ExecutionData
4181where
4182    T: Encodable2718 + Transaction,
4183    H: BlockHeader,
4184{
4185    fn from(sealed: Sealed<&Block<T, H>>) -> Self {
4186        let (block, block_hash) = sealed.into_parts();
4187        Self::from_block_unchecked(block_hash, block)
4188    }
4189}
4190
4191impl<T, H> From<(Sealed<Block<T, H>>, PayloadExtras)> for ExecutionData
4192where
4193    T: Encodable2718 + Transaction,
4194    H: BlockHeader,
4195{
4196    fn from((sealed, extras): (Sealed<Block<T, H>>, PayloadExtras)) -> Self {
4197        let (block, block_hash) = sealed.into_parts();
4198        let (payload, sidecar) =
4199            ExecutionPayload::from_block_unchecked_with_extras(block_hash, &block, extras);
4200        Self::new(payload, sidecar)
4201    }
4202}
4203
4204impl<T, H> From<(Sealed<&Block<T, H>>, PayloadExtras)> for ExecutionData
4205where
4206    T: Encodable2718 + Transaction,
4207    H: BlockHeader,
4208{
4209    fn from((sealed, extras): (Sealed<&Block<T, H>>, PayloadExtras)) -> Self {
4210        let (block, block_hash) = sealed.into_parts();
4211        let (payload, sidecar) =
4212            ExecutionPayload::from_block_unchecked_with_extras(block_hash, block, extras);
4213        Self::new(payload, sidecar)
4214    }
4215}
4216
4217#[cfg(test)]
4218mod tests {
4219    use super::*;
4220    use crate::{CancunPayloadFields, PayloadValidationError};
4221    use alloc::vec;
4222    use alloy_consensus::TxEnvelope;
4223    use alloy_primitives::{b256, hex};
4224    use similar_asserts::assert_eq;
4225
4226    #[test]
4227    #[cfg(feature = "kzg")]
4228    fn convert_empty_bundle() {
4229        let bundle = BlobsBundleV1::default();
4230        let _sidecar = bundle.try_into_sidecar().unwrap();
4231    }
4232
4233    #[test]
4234    #[cfg(feature = "serde")]
4235    fn serde_blobsbundlev1_empty() {
4236        let blobs_bundle_v1 = BlobsBundleV1::empty();
4237
4238        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
4239        let deserialized: BlobsBundleV1 = serde_json::from_str(&serialized).unwrap();
4240        assert_eq!(deserialized, blobs_bundle_v1);
4241    }
4242
4243    #[test]
4244    #[cfg(feature = "serde")]
4245    #[cfg(not(debug_assertions))]
4246    fn serde_blobsbundlev1_not_empty_pass() {
4247        let blobs_bundle_v1 = BlobsBundleV1 {
4248            proofs: vec![Bytes48::default()],
4249            commitments: vec![Bytes48::default()],
4250            blobs: vec![Blob::default()],
4251        };
4252
4253        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
4254        let deserialized: BlobsBundleV1 = serde_json::from_str(&serialized).unwrap();
4255        assert_eq!(deserialized, blobs_bundle_v1);
4256    }
4257
4258    #[test]
4259    #[cfg(feature = "serde")]
4260    #[cfg(not(debug_assertions))]
4261    fn serde_blobsbundlev1_not_empty_fail() {
4262        let blobs_bundle_v1 = BlobsBundleV1 {
4263            proofs: vec![Bytes48::default(), Bytes48::default()],
4264            commitments: vec![Bytes48::default()],
4265            blobs: vec![Blob::default()],
4266        };
4267
4268        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
4269        let deserialized: Result<BlobsBundleV1, serde_json::Error> =
4270            serde_json::from_str(&serialized);
4271        assert!(deserialized.is_err(), "invalid length 2, expected commitments.len()");
4272    }
4273
4274    #[test]
4275    #[cfg(feature = "serde")]
4276    #[cfg(not(debug_assertions))]
4277    fn serde_blobsbundlev2_not_empty_pass() {
4278        let commitments = vec![Bytes48::default()];
4279
4280        let blobs_bundle_v2 = BlobsBundleV2 {
4281            proofs: vec![Bytes48::default(); commitments.len() * CELLS_PER_EXT_BLOB],
4282            commitments,
4283            blobs: vec![Blob::default()],
4284        };
4285
4286        let serialized = serde_json::to_string(&blobs_bundle_v2).unwrap();
4287        let deserialized: BlobsBundleV2 = serde_json::from_str(&serialized).unwrap();
4288        assert_eq!(deserialized, blobs_bundle_v2);
4289    }
4290
4291    #[test]
4292    #[cfg(feature = "serde")]
4293    #[cfg(not(debug_assertions))]
4294    fn serde_blobsbundlev2_not_empty_fail() {
4295        let blobs_bundle_v2 = BlobsBundleV2 {
4296            proofs: vec![Bytes48::default()],
4297            commitments: vec![Bytes48::default()],
4298            blobs: vec![],
4299        };
4300
4301        let serialized = serde_json::to_string(&blobs_bundle_v2).unwrap();
4302        let deserialized: Result<BlobsBundleV2, serde_json::Error> =
4303            serde_json::from_str(&serialized);
4304        assert!(deserialized.is_err());
4305    }
4306
4307    #[test]
4308    #[cfg(feature = "ssz")]
4309    #[cfg(not(debug_assertions))]
4310    fn ssz_blobsbundlev2_roundtrip() {
4311        let commitments = vec![Bytes48::default(), Bytes48::default()];
4312        let num_blobs = commitments.len();
4313
4314        let blobs_bundle_v2 = BlobsBundleV2 {
4315            commitments,
4316            proofs: vec![Bytes48::default(); num_blobs * CELLS_PER_EXT_BLOB],
4317            blobs: vec![Blob::default(); num_blobs],
4318        };
4319
4320        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4321        let decoded: BlobsBundleV2 = ssz::Decode::from_ssz_bytes(&encoded).unwrap();
4322
4323        assert_eq!(decoded, blobs_bundle_v2);
4324    }
4325
4326    #[test]
4327    #[cfg(feature = "ssz")]
4328    #[cfg(not(debug_assertions))]
4329    fn ssz_blobsbundlev2_invalid_proofs_length() {
4330        let commitments = vec![Bytes48::default()];
4331
4332        let blobs_bundle_v2 = BlobsBundleV2 {
4333            commitments,
4334            proofs: vec![Bytes48::default(); 2],
4335            blobs: vec![Blob::default()],
4336        };
4337
4338        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4339
4340        // Attempt to decode - should fail due to mismatched proofs length
4341        let result: Result<BlobsBundleV2, _> = ssz::Decode::from_ssz_bytes(&encoded);
4342        assert!(result.is_err());
4343    }
4344
4345    #[test]
4346    #[cfg(feature = "ssz")]
4347    #[cfg(not(debug_assertions))]
4348    fn ssz_blobsbundlev2_mismatched_commitments_blobs() {
4349        let blobs_bundle_v2 = BlobsBundleV2 {
4350            commitments: vec![Bytes48::default(), Bytes48::default()],
4351            proofs: vec![Bytes48::default(); CELLS_PER_EXT_BLOB],
4352            blobs: vec![Blob::default()],
4353        };
4354
4355        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4356
4357        // Attempt to decode - should fail due to wrong number of commitments
4358        let result: Result<BlobsBundleV2, _> = ssz::Decode::from_ssz_bytes(&encoded);
4359        assert!(result.is_err());
4360    }
4361
4362    #[test]
4363    #[cfg(feature = "ssz")]
4364    fn ssz_blobsbundlev2_empty() {
4365        let blobs_bundle_v2 = BlobsBundleV2 { commitments: vec![], proofs: vec![], blobs: vec![] };
4366
4367        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4368
4369        // Decode from SSZ - empty bundle should be valid
4370        let decoded: BlobsBundleV2 = ssz::Decode::from_ssz_bytes(&encoded).unwrap();
4371        assert_eq!(decoded, blobs_bundle_v2);
4372    }
4373
4374    #[cfg(feature = "ssz")]
4375    fn ssz_payload_v1() -> ExecutionPayloadV1 {
4376        ExecutionPayloadV1 {
4377            parent_hash: B256::with_last_byte(1),
4378            fee_recipient: Address::with_last_byte(2),
4379            state_root: B256::with_last_byte(3),
4380            receipts_root: B256::with_last_byte(4),
4381            logs_bloom: Bloom::default(),
4382            prev_randao: B256::with_last_byte(5),
4383            block_number: 6,
4384            gas_limit: 7,
4385            gas_used: 8,
4386            timestamp: 9,
4387            extra_data: Bytes::from(vec![10, 11]),
4388            base_fee_per_gas: U256::from(12),
4389            block_hash: B256::with_last_byte(13),
4390            transactions: vec![Bytes::from(vec![14, 15])],
4391        }
4392    }
4393
4394    #[cfg(feature = "ssz")]
4395    fn ssz_payload_v2() -> ExecutionPayloadV2 {
4396        ExecutionPayloadV2 {
4397            payload_inner: ssz_payload_v1(),
4398            withdrawals: vec![Withdrawal {
4399                index: 1,
4400                validator_index: 2,
4401                address: Address::with_last_byte(3),
4402                amount: 4,
4403            }],
4404        }
4405    }
4406
4407    #[cfg(feature = "ssz")]
4408    fn ssz_payload_v3() -> ExecutionPayloadV3 {
4409        ExecutionPayloadV3 {
4410            payload_inner: ssz_payload_v2(),
4411            blob_gas_used: 16,
4412            excess_blob_gas: 17,
4413        }
4414    }
4415
4416    #[cfg(feature = "ssz")]
4417    fn ssz_payload_v4() -> ExecutionPayloadV4 {
4418        ExecutionPayloadV4 {
4419            payload_inner: ssz_payload_v3(),
4420            block_access_list: Bytes::from(vec![18, 19]),
4421            slot_number: 20,
4422        }
4423    }
4424
4425    #[test]
4426    #[cfg(feature = "ssz")]
4427    fn ssz_execution_payload_envelope_v1_response_roundtrip() {
4428        use ssz::{Decode, Encode};
4429
4430        let payload = ssz_payload_v1();
4431        let decoded = ExecutionPayloadV1::from_ssz_bytes(&payload.as_ssz_bytes()).unwrap();
4432
4433        assert_eq!(decoded, payload);
4434    }
4435
4436    #[test]
4437    #[cfg(feature = "ssz")]
4438    fn ssz_execution_payload_envelope_v2_roundtrip() {
4439        use ssz::{Decode, Encode};
4440
4441        let envelope = ExecutionPayloadEnvelopeV2 {
4442            execution_payload: ExecutionPayloadFieldV2::V2(ssz_payload_v2()),
4443            block_value: U256::from(21),
4444        };
4445
4446        let decoded = ExecutionPayloadEnvelopeV2::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4447        assert_eq!(decoded, envelope);
4448
4449        let envelope = ExecutionPayloadEnvelopeV2 {
4450            execution_payload: ExecutionPayloadFieldV2::V1(ssz_payload_v1()),
4451            block_value: U256::from(22),
4452        };
4453
4454        let decoded = ExecutionPayloadEnvelopeV2::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4455        assert_eq!(decoded, envelope);
4456    }
4457
4458    #[test]
4459    #[cfg(feature = "ssz")]
4460    fn ssz_execution_payload_envelope_v3_roundtrip() {
4461        use ssz::{Decode, Encode};
4462
4463        let envelope = ExecutionPayloadEnvelopeV3 {
4464            execution_payload: ssz_payload_v3(),
4465            block_value: U256::from(23),
4466            blobs_bundle: BlobsBundleV1::empty(),
4467            should_override_builder: true,
4468        };
4469
4470        let decoded = ExecutionPayloadEnvelopeV3::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4471        assert_eq!(decoded, envelope);
4472    }
4473
4474    #[test]
4475    #[cfg(feature = "ssz")]
4476    fn ssz_execution_payload_envelope_v4_roundtrip() {
4477        use ssz::{Decode, Encode};
4478
4479        let envelope = ExecutionPayloadEnvelopeV4 {
4480            envelope_inner: ExecutionPayloadEnvelopeV3 {
4481                execution_payload: ssz_payload_v3(),
4482                block_value: U256::from(24),
4483                blobs_bundle: BlobsBundleV1::empty(),
4484                should_override_builder: false,
4485            },
4486            execution_requests: Requests::from_requests([Bytes::from(vec![1, 2, 3])]),
4487        };
4488
4489        let decoded = ExecutionPayloadEnvelopeV4::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4490        assert_eq!(decoded, envelope);
4491    }
4492
4493    #[test]
4494    #[cfg(feature = "ssz")]
4495    fn ssz_execution_payload_envelope_v5_roundtrip() {
4496        use ssz::{Decode, Encode};
4497
4498        let envelope = ExecutionPayloadEnvelopeV5 {
4499            execution_payload: ssz_payload_v3(),
4500            block_value: U256::from(25),
4501            blobs_bundle: BlobsBundleV2::empty(),
4502            should_override_builder: true,
4503            execution_requests: Requests::from_requests([Bytes::from(vec![4, 5, 6])]),
4504        };
4505
4506        let decoded = ExecutionPayloadEnvelopeV5::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4507        assert_eq!(decoded, envelope);
4508    }
4509
4510    #[test]
4511    #[cfg(feature = "ssz")]
4512    fn ssz_execution_payload_envelope_v6_roundtrip() {
4513        use ssz::{Decode, Encode};
4514
4515        let envelope = ExecutionPayloadEnvelopeV6 {
4516            execution_payload: ssz_payload_v4(),
4517            block_value: U256::from(26),
4518            blobs_bundle: BlobsBundleV2::empty(),
4519            should_override_builder: false,
4520            execution_requests: Requests::from_requests([Bytes::from(vec![7, 8, 9])]),
4521        };
4522
4523        let decoded = ExecutionPayloadEnvelopeV6::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4524        assert_eq!(decoded, envelope);
4525    }
4526
4527    #[test]
4528    #[cfg(feature = "serde")]
4529    fn serde_payload_status() {
4530        let s = r#"{"status":"SYNCING","latestValidHash":null,"validationError":null}"#;
4531        let status: PayloadStatus = serde_json::from_str(s).unwrap();
4532        assert_eq!(status.status, PayloadStatusEnum::Syncing);
4533        assert!(status.latest_valid_hash.is_none());
4534        assert!(status.status.validation_error().is_none());
4535        assert_eq!(serde_json::to_string(&status).unwrap(), s);
4536
4537        let full = s;
4538        let s = r#"{"status":"SYNCING","latestValidHash":null}"#;
4539        let status: PayloadStatus = serde_json::from_str(s).unwrap();
4540        assert_eq!(status.status, PayloadStatusEnum::Syncing);
4541        assert!(status.latest_valid_hash.is_none());
4542        assert!(status.status.validation_error().is_none());
4543        assert_eq!(serde_json::to_string(&status).unwrap(), full);
4544    }
4545
4546    #[test]
4547    #[cfg(feature = "serde")]
4548    fn serde_payload_status_v2() {
4549        let json = r#"{"status":"VALID","latestValidHash":null,"validationError":null,"inclusionListSatisfied":true}"#;
4550        let status: PayloadStatusV2 = serde_json::from_str(json).unwrap();
4551        assert!(status.is_valid());
4552        assert!(status.payload_inner.latest_valid_hash.is_none());
4553        assert_eq!(status.inclusion_list_satisfied, Some(true));
4554        assert_eq!(serde_json::to_string(&status).unwrap(), json);
4555
4556        let json = r#"{"status":"SYNCING","latestValidHash":null,"validationError":null,"inclusionListSatisfied":null}"#;
4557        let status: PayloadStatusV2 = serde_json::from_str(json).unwrap();
4558        assert!(status.is_syncing());
4559        assert_eq!(status.inclusion_list_satisfied, None);
4560        assert_eq!(serde_json::to_string(&status).unwrap(), json);
4561    }
4562
4563    #[test]
4564    fn payload_status_v2_conversions() {
4565        let v1 = PayloadStatus::from_status(PayloadStatusEnum::Valid)
4566            .with_latest_valid_hash(B256::with_last_byte(1));
4567
4568        let v2: PayloadStatusV2 = v1.clone().into();
4569        assert_eq!(v2.payload_inner, v1);
4570        assert_eq!(v2.inclusion_list_satisfied, None);
4571
4572        let downgraded: PayloadStatus = v2.with_inclusion_list_satisfied(true).into();
4573        assert_eq!(downgraded, v1);
4574    }
4575
4576    #[test]
4577    fn payload_attributes_builder_setters() {
4578        let withdrawal = Withdrawal {
4579            index: 1,
4580            validator_index: 2,
4581            address: Address::with_last_byte(3),
4582            amount: 4,
4583        };
4584        let parent_beacon_block_root = B256::with_last_byte(5);
4585
4586        let attributes = PayloadAttributes::default()
4587            .with_timestamp(10)
4588            .with_withdrawals(vec![withdrawal])
4589            .with_parent_beacon_block_root(parent_beacon_block_root)
4590            .with_slot_number(6);
4591
4592        assert_eq!(attributes.timestamp, 10);
4593        assert_eq!(attributes.withdrawals, Some(vec![withdrawal]));
4594        assert_eq!(attributes.parent_beacon_block_root, Some(parent_beacon_block_root));
4595        assert_eq!(attributes.slot_number, Some(6));
4596    }
4597
4598    #[test]
4599    #[cfg(feature = "ssz")]
4600    fn ssz_payload_attributes_roundtrip_all_versions() {
4601        use ssz::{Decode, Encode};
4602
4603        let withdrawal = Withdrawal {
4604            index: 1,
4605            validator_index: 2,
4606            address: Address::with_last_byte(3),
4607            amount: 4,
4608        };
4609
4610        let v1 = PayloadAttributes {
4611            timestamp: 10,
4612            prev_randao: B256::with_last_byte(11),
4613            suggested_fee_recipient: Address::with_last_byte(12),
4614            withdrawals: None,
4615            parent_beacon_block_root: None,
4616            slot_number: None,
4617            target_gas_limit: None,
4618        };
4619        let decoded_v1 = PayloadAttributes::from_ssz_bytes(&v1.as_ssz_bytes()).unwrap();
4620        assert_eq!(decoded_v1, v1);
4621
4622        let v2 = PayloadAttributes {
4623            timestamp: 20,
4624            prev_randao: B256::with_last_byte(21),
4625            suggested_fee_recipient: Address::with_last_byte(22),
4626            withdrawals: Some(vec![withdrawal]),
4627            parent_beacon_block_root: None,
4628            slot_number: None,
4629            target_gas_limit: None,
4630        };
4631        let decoded_v2 = PayloadAttributes::from_ssz_bytes(&v2.as_ssz_bytes()).unwrap();
4632        assert_eq!(decoded_v2, v2);
4633
4634        let v3 = PayloadAttributes {
4635            timestamp: 30,
4636            prev_randao: B256::with_last_byte(31),
4637            suggested_fee_recipient: Address::with_last_byte(32),
4638            withdrawals: Some(vec![withdrawal]),
4639            parent_beacon_block_root: Some(B256::with_last_byte(33)),
4640            slot_number: None,
4641            target_gas_limit: None,
4642        };
4643        let decoded_v3 = PayloadAttributes::from_ssz_bytes(&v3.as_ssz_bytes()).unwrap();
4644        assert_eq!(decoded_v3, v3);
4645
4646        let v4 = PayloadAttributes {
4647            timestamp: 40,
4648            prev_randao: B256::with_last_byte(41),
4649            suggested_fee_recipient: Address::with_last_byte(42),
4650            withdrawals: Some(vec![withdrawal]),
4651            parent_beacon_block_root: Some(B256::with_last_byte(43)),
4652            slot_number: Some(44),
4653            target_gas_limit: Some(45),
4654        };
4655        let decoded_v4 = PayloadAttributes::from_ssz_bytes(&v4.as_ssz_bytes()).unwrap();
4656        assert_eq!(decoded_v4, v4);
4657    }
4658
4659    #[test]
4660    #[cfg(feature = "ssz")]
4661    fn ssz_payload_attributes_match_spec_container_offsets() {
4662        use ssz::Encode;
4663
4664        let withdrawal = Withdrawal {
4665            index: 1,
4666            validator_index: 2,
4667            address: Address::with_last_byte(3),
4668            amount: 4,
4669        };
4670
4671        let v1 = PayloadAttributes {
4672            timestamp: 10,
4673            prev_randao: B256::with_last_byte(11),
4674            suggested_fee_recipient: Address::with_last_byte(12),
4675            withdrawals: None,
4676            parent_beacon_block_root: None,
4677            slot_number: None,
4678            target_gas_limit: None,
4679        };
4680        assert_eq!(v1.as_ssz_bytes().len(), 60);
4681
4682        let v2 = PayloadAttributes {
4683            timestamp: 20,
4684            prev_randao: B256::with_last_byte(21),
4685            suggested_fee_recipient: Address::with_last_byte(22),
4686            withdrawals: Some(vec![withdrawal]),
4687            parent_beacon_block_root: None,
4688            slot_number: None,
4689            target_gas_limit: None,
4690        };
4691        let bytes = v2.as_ssz_bytes();
4692        assert_eq!(u32::from_le_bytes(bytes[60..64].try_into().unwrap()), 64);
4693
4694        let v3 = PayloadAttributes {
4695            timestamp: 30,
4696            prev_randao: B256::with_last_byte(31),
4697            suggested_fee_recipient: Address::with_last_byte(32),
4698            withdrawals: Some(vec![withdrawal]),
4699            parent_beacon_block_root: Some(B256::with_last_byte(33)),
4700            slot_number: None,
4701            target_gas_limit: None,
4702        };
4703        let bytes = v3.as_ssz_bytes();
4704        assert_eq!(u32::from_le_bytes(bytes[60..64].try_into().unwrap()), 96);
4705
4706        let v4 = PayloadAttributes {
4707            timestamp: 40,
4708            prev_randao: B256::with_last_byte(41),
4709            suggested_fee_recipient: Address::with_last_byte(42),
4710            withdrawals: Some(vec![withdrawal]),
4711            parent_beacon_block_root: Some(B256::with_last_byte(43)),
4712            slot_number: Some(44),
4713            target_gas_limit: Some(45),
4714        };
4715        let bytes = v4.as_ssz_bytes();
4716        assert_eq!(u32::from_le_bytes(bytes[60..64].try_into().unwrap()), 112);
4717    }
4718
4719    #[test]
4720    #[cfg(feature = "ssz")]
4721    fn ssz_payload_status_roundtrip() {
4722        use ssz::{Decode, Encode};
4723
4724        let statuses = [
4725            PayloadStatus {
4726                status: PayloadStatusEnum::Valid,
4727                latest_valid_hash: Some(B256::with_last_byte(1)),
4728            },
4729            PayloadStatus {
4730                status: PayloadStatusEnum::Invalid { validation_error: "bad payload".to_string() },
4731                latest_valid_hash: Some(B256::with_last_byte(2)),
4732            },
4733            PayloadStatus { status: PayloadStatusEnum::Syncing, latest_valid_hash: None },
4734            PayloadStatus { status: PayloadStatusEnum::Accepted, latest_valid_hash: None },
4735        ];
4736
4737        for status in statuses {
4738            let decoded = PayloadStatus::from_ssz_bytes(&status.as_ssz_bytes()).unwrap();
4739            assert_eq!(decoded, status);
4740        }
4741    }
4742
4743    #[test]
4744    #[cfg(feature = "ssz")]
4745    fn ssz_payload_status_matches_eip8178_container() {
4746        use ssz::{Decode, Encode};
4747
4748        let status = PayloadStatus {
4749            status: PayloadStatusEnum::Invalid { validation_error: "bad payload".to_string() },
4750            latest_valid_hash: None,
4751        };
4752        let spec = (1u8, B256::ZERO, b"bad payload".to_vec());
4753
4754        assert_eq!(status.as_ssz_bytes(), spec.as_ssz_bytes());
4755        assert_eq!(PayloadStatus::from_ssz_bytes(&spec.as_ssz_bytes()).unwrap(), status);
4756    }
4757
4758    #[test]
4759    #[cfg(feature = "ssz")]
4760    fn ssz_payload_id_roundtrip() {
4761        use ssz::{Decode, Encode};
4762
4763        let payload_id = PayloadId(B64::with_last_byte(42));
4764        let decoded = PayloadId::from_ssz_bytes(&payload_id.as_ssz_bytes()).unwrap();
4765        assert_eq!(decoded, payload_id);
4766    }
4767
4768    #[test]
4769    fn payload_id_from_str() {
4770        let expected = PayloadId(B64::with_last_byte(42));
4771
4772        assert_eq!("0x000000000000002a".parse::<PayloadId>().unwrap(), expected);
4773        assert_eq!("000000000000002a".parse::<PayloadId>().unwrap(), expected);
4774    }
4775
4776    #[test]
4777    fn payload_id_from_str_rejects_invalid_hex() {
4778        assert!("0x2a".parse::<PayloadId>().is_err());
4779        assert!("0x00000000000000zz".parse::<PayloadId>().is_err());
4780    }
4781
4782    #[test]
4783    #[cfg(feature = "serde")]
4784    fn serde_payload_status_error_deserialize() {
4785        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"Failed to decode block"}"#;
4786        let q = PayloadStatus {
4787            latest_valid_hash: None,
4788            status: PayloadStatusEnum::Invalid {
4789                validation_error: "Failed to decode block".to_string(),
4790            },
4791        };
4792        assert_eq!(q, serde_json::from_str(s).unwrap());
4793
4794        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"links to previously rejected block"}"#;
4795        let q = PayloadStatus {
4796            latest_valid_hash: None,
4797            status: PayloadStatusEnum::Invalid {
4798                validation_error: PayloadValidationError::LinksToRejectedPayload.to_string(),
4799            },
4800        };
4801        assert_eq!(q, serde_json::from_str(s).unwrap());
4802
4803        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"invalid block number"}"#;
4804        let q = PayloadStatus {
4805            latest_valid_hash: None,
4806            status: PayloadStatusEnum::Invalid {
4807                validation_error: PayloadValidationError::InvalidBlockNumber.to_string(),
4808            },
4809        };
4810        assert_eq!(q, serde_json::from_str(s).unwrap());
4811
4812        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":
4813        "invalid merkle root: (remote: 0x3f77fb29ce67436532fee970e1add8f5cc80e8878c79b967af53b1fd92a0cab7 local: 0x603b9628dabdaadb442a3bb3d7e0360efc110e1948472909230909f1690fed17)"}"#;
4814        let q = PayloadStatus {
4815            latest_valid_hash: None,
4816            status: PayloadStatusEnum::Invalid {
4817                validation_error: PayloadValidationError::InvalidStateRoot {
4818                    remote: "0x3f77fb29ce67436532fee970e1add8f5cc80e8878c79b967af53b1fd92a0cab7"
4819                        .parse()
4820                        .unwrap(),
4821                    local: "0x603b9628dabdaadb442a3bb3d7e0360efc110e1948472909230909f1690fed17"
4822                        .parse()
4823                        .unwrap(),
4824                }
4825                .to_string(),
4826            },
4827        };
4828        assert_eq!(q, serde_json::from_str(s).unwrap());
4829    }
4830
4831    #[test]
4832    #[cfg(feature = "serde")]
4833    fn serde_roundtrip_legacy_txs_payload_v1() {
4834        // pulled from hive tests
4835        let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x44bb4b98c59dbb726f96ffceb5ee028dcbe35b9bba4f9ffd56aeebf8d1e4db62","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0x5655011482546f16b2312ef18e9fad03d6a52b1be95401aea884b222477f9e64","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"]}"#;
4836        let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap();
4837        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4838
4839        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4840        assert_eq!(any_payload, payload.into());
4841    }
4842
4843    #[test]
4844    #[cfg(feature = "serde")]
4845    fn serde_roundtrip_legacy_txs_payload_v3() {
4846        // pulled from hive tests - modified with 4844 fields
4847        let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x0000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x44bb4b98c59dbb726f96ffceb5ee028dcbe35b9bba4f9ffd56aeebf8d1e4db62","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0x5655011482546f16b2312ef18e9fad03d6a52b1be95401aea884b222477f9e64","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"],"withdrawals":[],"blobGasUsed":"0xb10b","excessBlobGas":"0xb10b"}"#;
4848        let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap();
4849        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4850
4851        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4852        assert_eq!(any_payload, payload.into());
4853    }
4854
4855    #[test]
4856    #[cfg(feature = "serde")]
4857    fn serde_roundtrip_enveloped_txs_payload_v1() {
4858        // pulled from hive tests
4859        let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x76a03cbcb7adce07fd284c61e4fa31e5e786175cefac54a29e46ec8efa28ea41","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x028111cb7d25918386a69656b3d17b2febe95fd0f11572c1a55c14f99fdfe3df","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0xa6f40ed042e61e88e76125dede8fff8026751ea14454b68fb534cea99f2b2a77","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"]}"#;
4860        let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap();
4861        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4862
4863        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4864        assert_eq!(any_payload, payload.into());
4865    }
4866
4867    #[test]
4868    #[cfg(feature = "serde")]
4869    fn serde_roundtrip_enveloped_txs_payload_v3() {
4870        // pulled from hive tests - modified with 4844 fields
4871        let s = r#"{"parentHash":"0x67ead97eb79b47a1638659942384143f36ed44275d4182799875ab5a87324055","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x76a03cbcb7adce07fd284c61e4fa31e5e786175cefac54a29e46ec8efa28ea41","receiptsRoot":"0x4e3c608a9f2e129fccb91a1dae7472e78013b8e654bccc8d224ce3d63ae17006","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0x028111cb7d25918386a69656b3d17b2febe95fd0f11572c1a55c14f99fdfe3df","blockNumber":"0x1","gasLimit":"0x2fefd8","gasUsed":"0xa860","timestamp":"0x1235","extraData":"0x8b726574682f76302e312e30","baseFeePerGas":"0x342770c0","blockHash":"0xa6f40ed042e61e88e76125dede8fff8026751ea14454b68fb534cea99f2b2a77","transactions":["0xf865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"],"withdrawals":[],"blobGasUsed":"0xb10b","excessBlobGas":"0xb10b"}"#;
4872        let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap();
4873        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4874
4875        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4876        assert_eq!(any_payload, payload.into());
4877    }
4878
4879    #[test]
4880    #[cfg(feature = "serde")]
4881    fn serde_roundtrip_execution_payload_envelope_v3() {
4882        // pulled from a geth response getPayloadV3 in hive tests
4883        let response = r#"{"executionPayload":{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[],"blobGasUsed":"0x0","excessBlobGas":"0x0"},"blockValue":"0x0","blobsBundle":{"commitments":[],"proofs":[],"blobs":[]},"shouldOverrideBuilder":false}"#;
4884        let envelope: ExecutionPayloadEnvelopeV3 = serde_json::from_str(response).unwrap();
4885        assert_eq!(serde_json::to_string(&envelope).unwrap(), response);
4886    }
4887
4888    #[test]
4889    #[cfg(feature = "serde")]
4890    fn serde_roundtrip_execution_payload_field_v2() {
4891        // withdrawals must select the V2 variant instead of collapsing into V1
4892        let s = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[{"index":"0x0","validatorIndex":"0x1","address":"0x00000000000000000000000000000000000010f0","amount":"0x64"}]}"#;
4893        let field: ExecutionPayloadFieldV2 = serde_json::from_str(s).unwrap();
4894        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(s).unwrap();
4895        assert_eq!(field, ExecutionPayloadFieldV2::V2(payload_v2));
4896        assert_eq!(serde_json::to_string(&field).unwrap(), s);
4897
4898        // empty withdrawals still mean V2
4899        let s_empty = s.replace(
4900            r#"[{"index":"0x0","validatorIndex":"0x1","address":"0x00000000000000000000000000000000000010f0","amount":"0x64"}]"#,
4901            "[]",
4902        );
4903        let field: ExecutionPayloadFieldV2 = serde_json::from_str(&s_empty).unwrap();
4904        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(&s_empty).unwrap();
4905        assert_eq!(field, ExecutionPayloadFieldV2::V2(payload_v2));
4906        assert_eq!(serde_json::to_string(&field).unwrap(), s_empty);
4907
4908        // no withdrawals field means V1
4909        let s_v1 = s_empty.replace(r#","withdrawals":[]"#, "");
4910        let field: ExecutionPayloadFieldV2 = serde_json::from_str(&s_v1).unwrap();
4911        let payload_v1: ExecutionPayloadV1 = serde_json::from_str(&s_v1).unwrap();
4912        assert_eq!(field, ExecutionPayloadFieldV2::V1(payload_v1));
4913        assert_eq!(serde_json::to_string(&field).unwrap(), s_v1);
4914    }
4915
4916    #[test]
4917    #[cfg(feature = "serde")]
4918    fn serde_roundtrip_execution_payload_envelope_v2() {
4919        // a getPayloadV2 response with withdrawals in the payload
4920        let response = r#"{"executionPayload":{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[{"index":"0x0","validatorIndex":"0x1","address":"0x00000000000000000000000000000000000010f0","amount":"0x64"}]},"blockValue":"0x123"}"#;
4921        let envelope: ExecutionPayloadEnvelopeV2 = serde_json::from_str(response).unwrap();
4922        assert!(matches!(envelope.execution_payload, ExecutionPayloadFieldV2::V2(_)));
4923        assert_eq!(serde_json::to_string(&envelope).unwrap(), response);
4924    }
4925
4926    #[test]
4927    #[cfg(feature = "serde")]
4928    fn serde_payload_input_enum_v3() {
4929        let response_v3 = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[],"blobGasUsed":"0x0","excessBlobGas":"0x0"}"#;
4930
4931        let payload: ExecutionPayload = serde_json::from_str(response_v3).unwrap();
4932        assert!(payload.as_v3().is_some());
4933        assert_eq!(serde_json::to_string(&payload).unwrap(), response_v3);
4934
4935        let payload_v3: ExecutionPayloadV3 = serde_json::from_str(response_v3).unwrap();
4936        assert_eq!(payload.as_v3().unwrap(), &payload_v3);
4937    }
4938
4939    #[test]
4940    #[cfg(feature = "serde")]
4941    fn serde_payload_input_enum_v2() {
4942        let response_v2 = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[]}"#;
4943
4944        let payload: ExecutionPayload = serde_json::from_str(response_v2).unwrap();
4945        assert!(payload.as_v3().is_none());
4946        assert!(payload.as_v2().is_some());
4947        assert_eq!(serde_json::to_string(&payload).unwrap(), response_v2);
4948
4949        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(response_v2).unwrap();
4950        assert_eq!(payload.as_v2().unwrap(), &payload_v2);
4951    }
4952
4953    #[test]
4954    #[cfg(feature = "serde")]
4955    fn serde_payload_input_enum_faulty_v2() {
4956        // incomplete V3 payload should be rejected even if it has all V2 fields
4957        let response_faulty = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[], "blobGasUsed": "0x0"}"#;
4958
4959        let payload: Result<ExecutionPayload, serde_json::Error> =
4960            serde_json::from_str(response_faulty);
4961        assert!(payload.is_err());
4962    }
4963
4964    #[test]
4965    #[cfg(feature = "serde")]
4966    fn serde_payload_input_enum_faulty_v1() {
4967        // incomplete V3 payload should be rejected even if it has all V1 fields
4968        let response_faulty = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"blobGasUsed": "0x0"}"#;
4969
4970        let payload: Result<ExecutionPayload, serde_json::Error> =
4971            serde_json::from_str(response_faulty);
4972        assert!(payload.is_err());
4973    }
4974
4975    #[test]
4976    #[cfg(feature = "serde")]
4977    fn serde_faulty_roundtrip_payload_input_v3() {
4978        // The deserialization behavior of ExecutionPayload structs is faulty.
4979        // They should not be implicitly deserializable to an earlier version,
4980        // as this breaks round-trip behavior
4981        let response_v3 = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[],"blobGasUsed":"0x0","excessBlobGas":"0x0"}"#;
4982
4983        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(response_v3).unwrap();
4984        assert_ne!(response_v3, serde_json::to_string(&payload_v2).unwrap());
4985
4986        let payload_v1: ExecutionPayloadV1 = serde_json::from_str(response_v3).unwrap();
4987        assert_ne!(response_v3, serde_json::to_string(&payload_v1).unwrap());
4988    }
4989
4990    #[test]
4991    #[cfg(feature = "serde")]
4992    fn serde_faulty_roundtrip_payload_input_v2() {
4993        // The deserialization behavior of ExecutionPayload structs is faulty.
4994        // They should not be implicitly deserializable to an earlier version,
4995        // as this breaks round-trip behavior
4996        let response_v2 = r#"{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[]}"#;
4997
4998        let payload: ExecutionPayloadV1 = serde_json::from_str(response_v2).unwrap();
4999        assert_ne!(response_v2, serde_json::to_string(&payload).unwrap());
5000    }
5001
5002    #[test]
5003    #[cfg(feature = "serde")]
5004    fn serde_deserialize_execution_payload_input_v2() {
5005        let response = r#"
5006{
5007  "baseFeePerGas": "0x173b30b3",
5008  "blockHash": "0x99d486755fd046ad0bbb60457bac93d4856aa42fa00629cc7e4a28b65b5f8164",
5009  "blockNumber": "0xb",
5010  "extraData": "0xd883010d01846765746888676f312e32302e33856c696e7578",
5011  "feeRecipient": "0x0000000000000000000000000000000000000000",
5012  "gasLimit": "0x405829",
5013  "gasUsed": "0x3f0ca0",
5014  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5015  "parentHash": "0xfe34aaa2b869c66a727783ee5ad3e3983b6ef22baf24a1e502add94e7bcac67a",
5016  "prevRandao": "0x74132c32fe3ab9a470a8352544514d21b6969e7749f97742b53c18a1b22b396c",
5017  "receiptsRoot": "0x6a5c41dc55a1bd3e74e7f6accc799efb08b00c36c15265058433fcea6323e95f",
5018  "stateRoot": "0xde3b357f5f099e4c33d0343c9e9d204d663d7bd9c65020a38e5d0b2a9ace78a2",
5019  "timestamp": "0x6507d6b4",
5020  "transactions": [
5021    "0xf86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53",
5022    "0xf86d0b8458b20efd82520894687704db07e902e9a8b3754031d168d46e3d586e8806f3e8d87878800080820a95a0e3108f710902be662d5c978af16109961ffaf2ac4f88522407d40949a9574276a0205719ed21889b42ab5c1026d40b759a507c12d92db0d100fa69e1ac79137caa",
5023    "0xf86d0c8458b20efd8252089415e6a5a2e131dd5467fa1ff3acd104f45ee5940b8806f3e8d87878800080820a96a0af556ba9cda1d686239e08c24e169dece7afa7b85e0948eaa8d457c0561277fca029da03d3af0978322e54ac7e8e654da23934e0dd839804cb0430f8aaafd732dc",
5024    "0xf8521784565adcb7830186a0808080820a96a0ec782872a673a9fe4eff028a5bdb30d6b8b7711f58a187bf55d3aec9757cb18ea001796d373da76f2b0aeda72183cce0ad070a4f03aa3e6fee4c757a9444245206",
5025    "0xf8521284565adcb7830186a0808080820a95a08a0ea89028eff02596b385a10e0bd6ae098f3b281be2c95a9feb1685065d7384a06239d48a72e4be767bd12f317dd54202f5623a33e71e25a87cb25dd781aa2fc8",
5026    "0xf8521384565adcb7830186a0808080820a95a0784dbd311a82f822184a46f1677a428cbe3a2b88a798fb8ad1370cdbc06429e8a07a7f6a0efd428e3d822d1de9a050b8a883938b632185c254944dd3e40180eb79"
5027  ],
5028  "withdrawals": []
5029}
5030        "#;
5031        let payload: ExecutionPayloadInputV2 = serde_json::from_str(response).unwrap();
5032        assert_eq!(payload.withdrawals, Some(vec![]));
5033
5034        let response = r#"
5035{
5036  "baseFeePerGas": "0x173b30b3",
5037  "blockHash": "0x99d486755fd046ad0bbb60457bac93d4856aa42fa00629cc7e4a28b65b5f8164",
5038  "blockNumber": "0xb",
5039  "extraData": "0xd883010d01846765746888676f312e32302e33856c696e7578",
5040  "feeRecipient": "0x0000000000000000000000000000000000000000",
5041  "gasLimit": "0x405829",
5042  "gasUsed": "0x3f0ca0",
5043  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5044  "parentHash": "0xfe34aaa2b869c66a727783ee5ad3e3983b6ef22baf24a1e502add94e7bcac67a",
5045  "prevRandao": "0x74132c32fe3ab9a470a8352544514d21b6969e7749f97742b53c18a1b22b396c",
5046  "receiptsRoot": "0x6a5c41dc55a1bd3e74e7f6accc799efb08b00c36c15265058433fcea6323e95f",
5047  "stateRoot": "0xde3b357f5f099e4c33d0343c9e9d204d663d7bd9c65020a38e5d0b2a9ace78a2",
5048  "timestamp": "0x6507d6b4",
5049  "transactions": [
5050    "0xf86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53",
5051    "0xf86d0b8458b20efd82520894687704db07e902e9a8b3754031d168d46e3d586e8806f3e8d87878800080820a95a0e3108f710902be662d5c978af16109961ffaf2ac4f88522407d40949a9574276a0205719ed21889b42ab5c1026d40b759a507c12d92db0d100fa69e1ac79137caa",
5052    "0xf86d0c8458b20efd8252089415e6a5a2e131dd5467fa1ff3acd104f45ee5940b8806f3e8d87878800080820a96a0af556ba9cda1d686239e08c24e169dece7afa7b85e0948eaa8d457c0561277fca029da03d3af0978322e54ac7e8e654da23934e0dd839804cb0430f8aaafd732dc",
5053    "0xf8521784565adcb7830186a0808080820a96a0ec782872a673a9fe4eff028a5bdb30d6b8b7711f58a187bf55d3aec9757cb18ea001796d373da76f2b0aeda72183cce0ad070a4f03aa3e6fee4c757a9444245206",
5054    "0xf8521284565adcb7830186a0808080820a95a08a0ea89028eff02596b385a10e0bd6ae098f3b281be2c95a9feb1685065d7384a06239d48a72e4be767bd12f317dd54202f5623a33e71e25a87cb25dd781aa2fc8",
5055    "0xf8521384565adcb7830186a0808080820a95a0784dbd311a82f822184a46f1677a428cbe3a2b88a798fb8ad1370cdbc06429e8a07a7f6a0efd428e3d822d1de9a050b8a883938b632185c254944dd3e40180eb79"
5056  ]
5057}
5058        "#;
5059        let payload: ExecutionPayloadInputV2 = serde_json::from_str(response).unwrap();
5060        assert_eq!(payload.withdrawals, None);
5061    }
5062
5063    #[test]
5064    #[cfg(feature = "serde")]
5065    fn serde_deserialize_v2_input_with_blob_fields() {
5066        let input = r#"
5067{
5068    "parentHash": "0xaaa4c5b574f37e1537c78931d1bca24a4d17d4f29f1ee97e1cd48b704909de1f",
5069    "feeRecipient": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
5070    "stateRoot": "0x308ee9c5c6fab5e3d08763a3b5fe0be8ada891fa5010a49a3390e018dd436810",
5071    "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
5072    "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5073    "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
5074    "blockNumber": "0xf",
5075    "gasLimit": "0x16345785d8a0000",
5076    "gasUsed": "0x0",
5077    "timestamp": "0x3a97",
5078    "extraData": "0x",
5079    "baseFeePerGas": "0x7",
5080    "blockHash": "0x38bb6ba645c7e6bd970f9c7d492fafe1e04d85349054cb48d16c9d2c3e3cd0bf",
5081    "transactions": [],
5082    "withdrawals": [],
5083    "excessBlobGas": "0x0",
5084    "blobGasUsed": "0x0"
5085}
5086        "#;
5087
5088        // ensure that deserializing this (it includes blob fields) fails
5089        let payload_res: Result<ExecutionPayloadInputV2, serde_json::Error> =
5090            serde_json::from_str(input);
5091        assert!(payload_res.is_err());
5092    }
5093
5094    // <https://github.com/paradigmxyz/reth/issues/6036>
5095    #[test]
5096    #[cfg(feature = "serde")]
5097    fn deserialize_op_base_payload() {
5098        let payload = r#"{"parentHash":"0x24e8df372a61cdcdb1a163b52aaa1785e0c869d28c3b742ac09e826bbb524723","feeRecipient":"0x4200000000000000000000000000000000000011","stateRoot":"0x9a5db45897f1ff1e620a6c14b0a6f1b3bcdbed59f2adc516a34c9a9d6baafa71","receiptsRoot":"0x8af6f74835d47835deb5628ca941d00e0c9fd75585f26dabdcb280ec7122e6af","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xf37b24eeff594848072a05f74c8600001706c83e489a9132e55bf43a236e42ec","blockNumber":"0xe3d5d8","gasLimit":"0x17d7840","gasUsed":"0xb705","timestamp":"0x65a118c0","extraData":"0x","baseFeePerGas":"0x7a0ff32","blockHash":"0xf5c147b2d60a519b72434f0a8e082e18599021294dd9085d7597b0ffa638f1c0","withdrawals":[],"transactions":["0x7ef90159a05ba0034ffdcb246703298224564720b66964a6a69d0d7e9ffd970c546f7c048094deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b90104015d8eb900000000000000000000000000000000000000000000000000000000009e1c4a0000000000000000000000000000000000000000000000000000000065a11748000000000000000000000000000000000000000000000000000000000000000a4b479e5fa8d52dd20a8a66e468b56e993bdbffcccf729223aabff06299ab36db000000000000000000000000000000000000000000000000000000000000000400000000000000000000000073b4168cc87f35cc239200a20eb841cded23493b000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240"]}"#;
5099        let _payload = serde_json::from_str::<ExecutionPayloadInputV2>(payload).unwrap();
5100    }
5101
5102    #[test]
5103    fn roundtrip_payload_to_block() {
5104        let first_transaction_raw = Bytes::from_static(&hex!("02f9017a8501a1f0ff438211cc85012a05f2008512a05f2000830249f094d5409474fd5a725eab2ac9a8b26ca6fb51af37ef80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000001bdd2ed4b616c800000000000000000000000000001e9ee781dd4b97bdef92e5d1785f73a1f931daa20000000000000000000000007a40026a3b9a41754a95eec8c92c6b99886f440c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ae80eb647dd09968488fa1d7e412bf8558a0b7a0000000000000000000000000f9815537d361cb02befd9918c95c97d4d8a4a2bc001a0ba8f1928bb0efc3fcd01524a2039a9a2588fa567cd9a7cc18217e05c615e9d69a0544bfd11425ac7748e76b3795b57a5563e2b0eff47b5428744c62ff19ccfc305")[..]);
5105        let second_transaction_raw = Bytes::from_static(&hex!("03f901388501a1f0ff430c843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c005f8c6a00195f6dff17753fc89b60eac6477026a805116962c9e412de8015c0484e661c1a001aae314061d4f5bbf158f15d9417a238f9589783f58762cd39d05966b3ba2fba0013f5be9b12e7da06f0dd11a7bdc4e0db8ef33832acc23b183bd0a2c1408a757a0019d9ac55ea1a615d92965e04d960cb3be7bff121a381424f1f22865bd582e09a001def04412e76df26fefe7b0ed5e10580918ae4f355b074c0cfe5d0259157869a0011c11a415db57e43db07aef0de9280b591d65ca0cce36c7002507f8191e5d4a80a0c89b59970b119187d97ad70539f1624bbede92648e2dc007890f9658a88756c5a06fb2e3d4ce2c438c0856c2de34948b7032b1aadc4642a9666228ea8cdc7786b7")[..]);
5106
5107        let new_payload = ExecutionPayloadV3 {
5108            payload_inner: ExecutionPayloadV2 {
5109                payload_inner: ExecutionPayloadV1 {
5110                    base_fee_per_gas:  U256::from(7u64),
5111                    block_number: 0xa946u64,
5112                    block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(),
5113                    logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(),
5114                    extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(),
5115                    gas_limit: 0x1c9c380,
5116                    gas_used: 0x1f4a9,
5117                    timestamp: 0x651f35b8,
5118                    fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
5119                    parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(),
5120                    prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(),
5121                    receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(),
5122                    state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(),
5123                    transactions: vec![first_transaction_raw, second_transaction_raw],
5124                },
5125                withdrawals: vec![],
5126            },
5127            blob_gas_used: 0xc0000,
5128            excess_blob_gas: 0x580000,
5129        };
5130
5131        let mut block: Block<TxEnvelope> = new_payload.clone().try_into_block().unwrap();
5132
5133        // this newPayload came with a parent beacon block root, we need to manually insert it
5134        // before hashing
5135        let parent_beacon_block_root =
5136            b256!("531cd53b8e68deef0ea65edfa3cda927a846c307b0907657af34bc3f313b5871");
5137        block.header.parent_beacon_block_root = Some(parent_beacon_block_root);
5138
5139        let converted_payload = ExecutionPayloadV3::from_block_unchecked(block.hash_slow(), &block);
5140
5141        // ensure the payloads are the same
5142        assert_eq!(new_payload, converted_payload);
5143    }
5144
5145    #[test]
5146    fn payload_to_block_rejects_network_encoded_tx() {
5147        let first_transaction_raw = Bytes::from_static(&hex!("b9017e02f9017a8501a1f0ff438211cc85012a05f2008512a05f2000830249f094d5409474fd5a725eab2ac9a8b26ca6fb51af37ef80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000001bdd2ed4b616c800000000000000000000000000001e9ee781dd4b97bdef92e5d1785f73a1f931daa20000000000000000000000007a40026a3b9a41754a95eec8c92c6b99886f440c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ae80eb647dd09968488fa1d7e412bf8558a0b7a0000000000000000000000000f9815537d361cb02befd9918c95c97d4d8a4a2bc001a0ba8f1928bb0efc3fcd01524a2039a9a2588fa567cd9a7cc18217e05c615e9d69a0544bfd11425ac7748e76b3795b57a5563e2b0eff47b5428744c62ff19ccfc305")[..]);
5148        let second_transaction_raw = Bytes::from_static(&hex!("b9013c03f901388501a1f0ff430c843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c005f8c6a00195f6dff17753fc89b60eac6477026a805116962c9e412de8015c0484e661c1a001aae314061d4f5bbf158f15d9417a238f9589783f58762cd39d05966b3ba2fba0013f5be9b12e7da06f0dd11a7bdc4e0db8ef33832acc23b183bd0a2c1408a757a0019d9ac55ea1a615d92965e04d960cb3be7bff121a381424f1f22865bd582e09a001def04412e76df26fefe7b0ed5e10580918ae4f355b074c0cfe5d0259157869a0011c11a415db57e43db07aef0de9280b591d65ca0cce36c7002507f8191e5d4a80a0c89b59970b119187d97ad70539f1624bbede92648e2dc007890f9658a88756c5a06fb2e3d4ce2c438c0856c2de34948b7032b1aadc4642a9666228ea8cdc7786b7")[..]);
5149
5150        let new_payload = ExecutionPayloadV3 {
5151            payload_inner: ExecutionPayloadV2 {
5152                payload_inner: ExecutionPayloadV1 {
5153                    base_fee_per_gas:  U256::from(7u64),
5154                    block_number: 0xa946u64,
5155                    block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(),
5156                    logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(),
5157                    extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(),
5158                    gas_limit: 0x1c9c380,
5159                    gas_used: 0x1f4a9,
5160                    timestamp: 0x651f35b8,
5161                    fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
5162                    parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(),
5163                    prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(),
5164                    receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(),
5165                    state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(),
5166                    transactions: vec![first_transaction_raw, second_transaction_raw],
5167                },
5168                withdrawals: vec![],
5169            },
5170            blob_gas_used: 0xc0000,
5171            excess_blob_gas: 0x580000,
5172        };
5173
5174        let _block = new_payload
5175            .try_into_block::<TxEnvelope>()
5176            .expect_err("execution payload conversion requires typed txs without a rlp header");
5177    }
5178
5179    #[test]
5180    fn devnet_invalid_block_hash_repro() {
5181        let deser_block = r#"
5182        {
5183            "parentHash": "0xae8315ee86002e6269a17dd1e9516a6cf13223e9d4544d0c32daff826fb31acc",
5184            "feeRecipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134",
5185            "stateRoot": "0x03787f1579efbaa4a8234e72465eb4e29ef7e62f61242d6454661932e1a282a1",
5186            "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
5187            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5188            "prevRandao": "0x918e86b497dc15de7d606457c36ca583e24d9b0a110a814de46e33d5bb824a66",
5189            "blockNumber": "0x6a784",
5190            "gasLimit": "0x1c9c380",
5191            "gasUsed": "0x0",
5192            "timestamp": "0x65bc1d60",
5193            "extraData": "0x9a726574682f76302e312e302d616c7068612e31362f6c696e7578",
5194            "baseFeePerGas": "0x8",
5195            "blobGasUsed": "0x0",
5196            "excessBlobGas": "0x0",
5197            "blockHash": "0x340c157eca9fd206b87c17f0ecbe8d411219de7188a0a240b635c88a96fe91c5",
5198            "transactions": [],
5199            "withdrawals": [
5200                {
5201                    "index": "0x5ab202",
5202                    "validatorIndex": "0xb1b",
5203                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5204                    "amount": "0x19b3d"
5205                },
5206                {
5207                    "index": "0x5ab203",
5208                    "validatorIndex": "0xb1c",
5209                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5210                    "amount": "0x15892"
5211                },
5212                {
5213                    "index": "0x5ab204",
5214                    "validatorIndex": "0xb1d",
5215                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5216                    "amount": "0x19b3d"
5217                },
5218                {
5219                    "index": "0x5ab205",
5220                    "validatorIndex": "0xb1e",
5221                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5222                    "amount": "0x19b3d"
5223                },
5224                {
5225                    "index": "0x5ab206",
5226                    "validatorIndex": "0xb1f",
5227                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5228                    "amount": "0x19b3d"
5229                },
5230                {
5231                    "index": "0x5ab207",
5232                    "validatorIndex": "0xb20",
5233                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5234                    "amount": "0x19b3d"
5235                },
5236                {
5237                    "index": "0x5ab208",
5238                    "validatorIndex": "0xb21",
5239                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5240                    "amount": "0x15892"
5241                },
5242                {
5243                    "index": "0x5ab209",
5244                    "validatorIndex": "0xb22",
5245                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5246                    "amount": "0x19b3d"
5247                },
5248                {
5249                    "index": "0x5ab20a",
5250                    "validatorIndex": "0xb23",
5251                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5252                    "amount": "0x19b3d"
5253                },
5254                {
5255                    "index": "0x5ab20b",
5256                    "validatorIndex": "0xb24",
5257                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5258                    "amount": "0x17db2"
5259                },
5260                {
5261                    "index": "0x5ab20c",
5262                    "validatorIndex": "0xb25",
5263                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5264                    "amount": "0x19b3d"
5265                },
5266                {
5267                    "index": "0x5ab20d",
5268                    "validatorIndex": "0xb26",
5269                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5270                    "amount": "0x19b3d"
5271                },
5272                {
5273                    "index": "0x5ab20e",
5274                    "validatorIndex": "0xa91",
5275                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5276                    "amount": "0x15892"
5277                },
5278                {
5279                    "index": "0x5ab20f",
5280                    "validatorIndex": "0xa92",
5281                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5282                    "amount": "0x1c05d"
5283                },
5284                {
5285                    "index": "0x5ab210",
5286                    "validatorIndex": "0xa93",
5287                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5288                    "amount": "0x15892"
5289                },
5290                {
5291                    "index": "0x5ab211",
5292                    "validatorIndex": "0xa94",
5293                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5294                    "amount": "0x19b3d"
5295                }
5296            ]
5297        }
5298        "#;
5299
5300        // deserialize payload
5301        let payload: ExecutionPayload =
5302            serde_json::from_str::<ExecutionPayloadV3>(deser_block).unwrap().into();
5303
5304        // NOTE: the actual block hash here is incorrect, it is a result of a bug, this was the
5305        // fix:
5306        // <https://github.com/paradigmxyz/reth/pull/6328>
5307        let block_hash_with_blob_fee_fields =
5308            b256!("a7cdd5f9e54147b53a15833a8c45dffccbaed534d7fdc23458f45102a4bf71b0");
5309
5310        let versioned_hashes = vec![];
5311        let parent_beacon_block_root =
5312            b256!("1162de8a0f4d20d86b9ad6e0a2575ab60f00a433dc70d9318c8abc9041fddf54");
5313
5314        // set up cancun payload fields
5315        let cancun_fields = CancunPayloadFields { parent_beacon_block_root, versioned_hashes };
5316
5317        // convert into block
5318        let block = payload
5319            .try_into_block_with_sidecar::<TxEnvelope>(&ExecutionPayloadSidecar::v3(cancun_fields))
5320            .unwrap();
5321
5322        // Ensure the actual hash is calculated if we set the fields to what they should be
5323        assert_eq!(block_hash_with_blob_fee_fields, block.header.hash_slow());
5324    }
5325
5326    #[test]
5327    fn test_payload_to_block_with_sidecar_raw() {
5328        use std::path::PathBuf;
5329
5330        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/payload");
5331        let dir = std::fs::read_dir(path).expect("Unable to read payload folder");
5332
5333        for entry in dir {
5334            let entry = entry.expect("Unable to read entry");
5335            let path = entry.path();
5336
5337            if path.extension().and_then(|s| s.to_str()) != Some("json") {
5338                continue;
5339            }
5340
5341            let contents = std::fs::read_to_string(&path).expect("Unable to read file");
5342            let value: serde_json::Value = serde_json::from_str(&contents)
5343                .unwrap_or_else(|e| panic!("Failed to parse JSON from {path:?}: {e}"));
5344
5345            // Extract the newPayload object
5346            let new_payload = &value["newPayload"];
5347            let payload_value = &new_payload["payload"];
5348            let sidecar_value = &new_payload["sidecar"];
5349
5350            let payload: ExecutionPayload = serde_json::from_value(payload_value.clone())
5351                .unwrap_or_else(|e| panic!("Failed to deserialize payload from {path:?}: {e}"));
5352
5353            // Deserialize the sidecar
5354            let sidecar: ExecutionPayloadSidecar = serde_json::from_value(sidecar_value.clone())
5355                .unwrap_or_else(|e| panic!("Failed to deserialize sidecar from {path:?}: {e}"));
5356
5357            // Convert to block with raw transactions
5358            let block = payload.clone().into_block_with_sidecar_raw(&sidecar).unwrap_or_else(|e| {
5359                panic!("Failed to convert payload to block from {path:?}: {e}")
5360            });
5361
5362            // Verify the block has raw transactions (Bytes) if there are any
5363            if let Some(tx_count) = payload_value["transactions"].as_array().map(|a| a.len()) {
5364                assert_eq!(
5365                    block.body.transactions.len(),
5366                    tx_count,
5367                    "Transaction count mismatch in {:?}",
5368                    path
5369                );
5370            }
5371
5372            // Verify sidecar fields are applied
5373            assert_eq!(
5374                block.header.parent_beacon_block_root,
5375                sidecar.parent_beacon_block_root(),
5376                "Parent beacon block root mismatch in {:?}",
5377                path
5378            );
5379            assert_eq!(
5380                block.header.requests_hash,
5381                sidecar.requests_hash(),
5382                "Requests hash mismatch in {:?}",
5383                path
5384            );
5385
5386            // Verify the block hash matches the one in the payload
5387            let expected_hash = payload_value["blockHash"]
5388                .as_str()
5389                .unwrap()
5390                .parse::<B256>()
5391                .unwrap_or_else(|e| panic!("Failed to parse block hash from {path:?}: {e}"));
5392            let actual_hash = block.header.hash_slow();
5393            assert_eq!(
5394                actual_hash, expected_hash,
5395                "Block hash mismatch in {:?}: expected {}, got {}",
5396                path, expected_hash, actual_hash
5397            );
5398
5399            let block =
5400                payload.try_into_block_with_sidecar::<TxEnvelope>(&sidecar).unwrap_or_else(|e| {
5401                    panic!("Failed to convert payload to block from {path:?}: {e}")
5402                });
5403            let actual_hash = block.header.hash_slow();
5404            assert_eq!(
5405                actual_hash, expected_hash,
5406                "Block hash mismatch in {:?}: expected {}, got {}",
5407                path, expected_hash, actual_hash
5408            );
5409        }
5410    }
5411
5412    #[test]
5413    #[cfg(feature = "serde")]
5414    fn test_into_block_raw_with_transactions_root() {
5415        use std::path::PathBuf;
5416
5417        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/payload");
5418        let dir = std::fs::read_dir(path).expect("Unable to read payload folder");
5419
5420        for entry in dir {
5421            let entry = entry.expect("Unable to read entry");
5422            let path = entry.path();
5423
5424            if path.extension().and_then(|s| s.to_str()) != Some("json") {
5425                continue;
5426            }
5427
5428            let contents = std::fs::read_to_string(&path).expect("Unable to read file");
5429            let value: serde_json::Value = serde_json::from_str(&contents)
5430                .unwrap_or_else(|e| panic!("Failed to parse JSON from {path:?}: {e}"));
5431
5432            let new_payload = &value["newPayload"];
5433            let payload_value = &new_payload["payload"];
5434            let sidecar_value = &new_payload["sidecar"];
5435
5436            let payload: ExecutionPayload = serde_json::from_value(payload_value.clone())
5437                .unwrap_or_else(|e| panic!("Failed to deserialize payload from {path:?}: {e}"));
5438
5439            let sidecar: ExecutionPayloadSidecar = serde_json::from_value(sidecar_value.clone())
5440                .unwrap_or_else(|e| panic!("Failed to deserialize sidecar from {path:?}: {e}"));
5441
5442            let expected_hash = payload_value["blockHash"]
5443                .as_str()
5444                .unwrap()
5445                .parse::<B256>()
5446                .unwrap_or_else(|e| panic!("Failed to parse block hash from {path:?}: {e}"));
5447
5448            // Build the block normally to get the computed transactions root
5449            let block_normal =
5450                payload.clone().into_block_with_sidecar_raw(&sidecar).unwrap_or_else(|e| {
5451                    panic!("Failed to convert payload to block from {path:?}: {e}")
5452                });
5453            let tx_root = block_normal.header.transactions_root;
5454
5455            // Build using pre-computed transactions root
5456            let block_with_root =
5457                payload.clone().into_block_raw_with_transactions_root(tx_root).unwrap();
5458            assert_eq!(
5459                block_with_root.header.transactions_root, tx_root,
5460                "transactions_root mismatch in {path:?}"
5461            );
5462
5463            // Build using the opt variant with Some
5464            let block_opt_some =
5465                payload.clone().into_block_raw_with_transactions_root_opt(Some(tx_root)).unwrap();
5466            assert_eq!(
5467                block_opt_some.header.transactions_root, tx_root,
5468                "opt(Some) transactions_root mismatch in {path:?}"
5469            );
5470
5471            // Build using the opt variant with None (should compute same root)
5472            let block_opt_none =
5473                payload.clone().into_block_raw_with_transactions_root_opt(None).unwrap();
5474            assert_eq!(
5475                block_opt_none.header.transactions_root, tx_root,
5476                "opt(None) transactions_root mismatch in {path:?}"
5477            );
5478
5479            // Build with sidecar + pre-computed root and verify block hash
5480            let block_sidecar_root = payload
5481                .into_block_with_sidecar_raw_with_transactions_root(&sidecar, tx_root)
5482                .unwrap();
5483            let actual_hash = block_sidecar_root.header.hash_slow();
5484            assert_eq!(
5485                actual_hash, expected_hash,
5486                "Block hash mismatch with pre-computed tx root in {path:?}"
5487            );
5488        }
5489    }
5490
5491    #[test]
5492    fn test_v1_with_transactions_root_override() {
5493        let transaction = Bytes::from_static(&hex!("f86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53"));
5494
5495        let payload = ExecutionPayloadV1 {
5496            parent_hash: B256::default(),
5497            fee_recipient: Address::default(),
5498            state_root: B256::default(),
5499            receipts_root: B256::default(),
5500            logs_bloom: Bloom::default(),
5501            prev_randao: B256::default(),
5502            block_number: 0,
5503            gas_limit: 0,
5504            gas_used: 0,
5505            timestamp: 0,
5506            extra_data: Bytes::default(),
5507            base_fee_per_gas: U256::from(1),
5508            block_hash: B256::default(),
5509            transactions: vec![transaction],
5510        };
5511
5512        let computed_root = payload.clone().into_block_raw().unwrap().header.transactions_root;
5513
5514        let fake_root = b256!("1111111111111111111111111111111111111111111111111111111111111111");
5515        assert_ne!(computed_root, fake_root);
5516
5517        let block = payload.clone().into_block_raw_with_transactions_root(fake_root).unwrap();
5518        assert_eq!(block.header.transactions_root, fake_root);
5519
5520        let block_opt = payload.into_block_raw_with_transactions_root_opt(Some(fake_root)).unwrap();
5521        assert_eq!(block_opt.header.transactions_root, fake_root);
5522    }
5523
5524    #[test]
5525    fn test_with_transactions_root_extra_data_validation() {
5526        let payload = ExecutionPayloadV1 {
5527            parent_hash: B256::default(),
5528            fee_recipient: Address::default(),
5529            state_root: B256::default(),
5530            receipts_root: B256::default(),
5531            logs_bloom: Bloom::default(),
5532            prev_randao: B256::default(),
5533            block_number: 0,
5534            gas_limit: 0,
5535            gas_used: 0,
5536            timestamp: 0,
5537            extra_data: Bytes::from(vec![0u8; MAXIMUM_EXTRA_DATA_SIZE + 1]),
5538            base_fee_per_gas: U256::from(1),
5539            block_hash: B256::default(),
5540            transactions: vec![],
5541        };
5542
5543        let fake_root = b256!("1111111111111111111111111111111111111111111111111111111111111111");
5544
5545        assert!(payload.clone().into_block_raw().is_err());
5546        assert!(payload.clone().into_block_raw_with_transactions_root(fake_root).is_err());
5547        assert!(payload.into_block_raw_with_transactions_root_opt(Some(fake_root)).is_err());
5548    }
5549
5550    #[test]
5551    fn test_decoded_transactions() {
5552        let transaction = Bytes::from_static(&hex!("f86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53"));
5553
5554        let payload = ExecutionPayload::V1(ExecutionPayloadV1 {
5555            parent_hash: B256::default(),
5556            fee_recipient: Address::default(),
5557            state_root: B256::default(),
5558            receipts_root: B256::default(),
5559            logs_bloom: Bloom::default(),
5560            prev_randao: B256::default(),
5561            block_number: 0,
5562            gas_limit: 0,
5563            gas_used: 0,
5564            timestamp: 0,
5565            extra_data: Bytes::default(),
5566            base_fee_per_gas: U256::default(),
5567            block_hash: B256::default(),
5568            transactions: vec![transaction.clone()],
5569        });
5570
5571        // Test decoded_transactions
5572        let decoded: Vec<_> = payload.decoded_transactions::<TxEnvelope>().collect();
5573        assert_eq!(decoded.len(), 1);
5574        assert!(decoded[0].is_ok(), "Failed to decode transaction: {:?}", decoded[0]);
5575
5576        // Test decoded_transactions_with_encoded
5577        let decoded_with_encoded: Vec<_> =
5578            payload.decoded_transactions_with_encoded::<TxEnvelope>().collect();
5579        assert_eq!(decoded_with_encoded.len(), 1);
5580        assert!(decoded_with_encoded[0].is_ok());
5581        if let Ok(with_encoded) = &decoded_with_encoded[0] {
5582            assert_eq!(with_encoded.encoded_bytes(), &transaction);
5583        }
5584    }
5585
5586    #[test]
5587    #[cfg(feature = "serde")]
5588    fn serde_payload_attributes_without_slot_number() {
5589        let json = r#"{
5590            "timestamp": "0x1234",
5591            "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
5592            "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000"
5593        }"#;
5594
5595        let attrs: PayloadAttributes = serde_json::from_str(json).unwrap();
5596        assert_eq!(attrs.timestamp, 0x1234);
5597        assert!(attrs.slot_number.is_none());
5598        assert!(attrs.target_gas_limit.is_none());
5599    }
5600
5601    #[test]
5602    #[cfg(feature = "serde")]
5603    fn serde_payload_attributes_with_hex_amsterdam_fields() {
5604        let json = r#"{
5605            "timestamp": "0x2",
5606            "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
5607            "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000",
5608            "withdrawals": [],
5609            "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
5610            "slotNumber": "0x0",
5611            "targetGasLimit": "0x1c9c380"
5612        }"#;
5613
5614        let attrs: PayloadAttributes = serde_json::from_str(json).unwrap();
5615        assert_eq!(attrs.timestamp, 0x2);
5616        assert_eq!(attrs.slot_number, Some(0));
5617        assert_eq!(attrs.target_gas_limit, Some(30_000_000));
5618    }
5619
5620    #[test]
5621    #[cfg(feature = "serde")]
5622    fn serde_execution_payload_body_v2() {
5623        let body = ExecutionPayloadBodyV2 {
5624            transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])],
5625            withdrawals: Some(vec![Withdrawal {
5626                index: 1,
5627                validator_index: 2,
5628                address: Address::default(),
5629                amount: 100,
5630            }]),
5631            block_access_list: Some(Bytes::from(vec![0xaa, 0xbb, 0xcc])),
5632        };
5633
5634        let serialized = serde_json::to_string(&body).unwrap();
5635        let deserialized: ExecutionPayloadBodyV2 = serde_json::from_str(&serialized).unwrap();
5636        assert_eq!(deserialized, body);
5637    }
5638
5639    #[test]
5640    #[cfg(feature = "serde")]
5641    fn serde_execution_payload_body_v2_null_fields() {
5642        let body = ExecutionPayloadBodyV2 {
5643            transactions: vec![],
5644            withdrawals: None,
5645            block_access_list: None,
5646        };
5647
5648        let serialized = serde_json::to_string(&body).unwrap();
5649        let deserialized: ExecutionPayloadBodyV2 = serde_json::from_str(&serialized).unwrap();
5650        assert_eq!(deserialized, body);
5651    }
5652
5653    #[test]
5654    #[cfg(feature = "ssz")]
5655    fn ssz_execution_payload_body_v1_roundtrip() {
5656        use ssz::{Decode, Encode};
5657
5658        let body = ExecutionPayloadBodyV1 {
5659            transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])],
5660            withdrawals: Some(vec![Withdrawal {
5661                index: 1,
5662                validator_index: 2,
5663                address: Address::with_last_byte(3),
5664                amount: 4,
5665            }]),
5666        };
5667
5668        let decoded = ExecutionPayloadBodyV1::from_ssz_bytes(&body.as_ssz_bytes()).unwrap();
5669        assert_eq!(decoded, body);
5670
5671        let bodies: ExecutionPayloadBodiesV1 = vec![Some(body), None];
5672        let decoded = ExecutionPayloadBodiesV1::from_ssz_bytes(&bodies.as_ssz_bytes()).unwrap();
5673        assert_eq!(decoded, bodies);
5674    }
5675
5676    #[test]
5677    #[cfg(feature = "ssz")]
5678    fn ssz_execution_payload_body_v2_roundtrip() {
5679        use ssz::{Decode, Encode};
5680
5681        let body = ExecutionPayloadBodyV2 {
5682            transactions: vec![Bytes::from(vec![0x04, 0x05, 0x06])],
5683            withdrawals: None,
5684            block_access_list: Some(Bytes::from(vec![0xaa, 0xbb, 0xcc])),
5685        };
5686
5687        let decoded = ExecutionPayloadBodyV2::from_ssz_bytes(&body.as_ssz_bytes()).unwrap();
5688        assert_eq!(decoded, body);
5689
5690        let bodies: ExecutionPayloadBodiesV2 = vec![Some(body), None];
5691        let decoded = ExecutionPayloadBodiesV2::from_ssz_bytes(&bodies.as_ssz_bytes()).unwrap();
5692        assert_eq!(decoded, bodies);
5693    }
5694
5695    #[test]
5696    fn execution_payload_body_v1_to_v2_conversion() {
5697        let v1 = ExecutionPayloadBodyV1 {
5698            transactions: vec![Bytes::from(vec![0x01, 0x02])],
5699            withdrawals: Some(vec![Withdrawal {
5700                index: 1,
5701                validator_index: 2,
5702                address: Address::default(),
5703                amount: 100,
5704            }]),
5705        };
5706
5707        let v2: ExecutionPayloadBodyV2 = v1.clone().into();
5708        assert_eq!(v2.transactions, v1.transactions);
5709        assert_eq!(v2.withdrawals, v1.withdrawals);
5710        assert_eq!(v2.block_access_list, None);
5711    }
5712
5713    #[test]
5714    fn execution_payload_body_v2_to_v1_conversion() {
5715        let v2 = ExecutionPayloadBodyV2 {
5716            transactions: vec![Bytes::from(vec![0x01, 0x02])],
5717            withdrawals: Some(vec![Withdrawal {
5718                index: 1,
5719                validator_index: 2,
5720                address: Address::default(),
5721                amount: 100,
5722            }]),
5723            block_access_list: Some(Bytes::from(vec![0xaa, 0xbb])),
5724        };
5725
5726        let v1: ExecutionPayloadBodyV1 = v2.clone().into();
5727        assert_eq!(v1.transactions, v2.transactions);
5728        assert_eq!(v1.withdrawals, v2.withdrawals);
5729    }
5730
5731    #[test]
5732    #[cfg(feature = "serde")]
5733    fn serde_roundtrip_payload_v2() {
5734        let payload = ExecutionPayloadV2 {
5735            payload_inner: ExecutionPayloadV1 {
5736                parent_hash: B256::default(),
5737                fee_recipient: Address::default(),
5738                state_root: B256::default(),
5739                receipts_root: B256::default(),
5740                logs_bloom: Bloom::default(),
5741                prev_randao: B256::default(),
5742                block_number: 1,
5743                gas_limit: 30_000_000,
5744                gas_used: 21000,
5745                timestamp: 1234,
5746                extra_data: Bytes::default(),
5747                base_fee_per_gas: U256::from(7u64),
5748                block_hash: B256::default(),
5749                transactions: vec![],
5750            },
5751            withdrawals: vec![Withdrawal {
5752                index: 1,
5753                validator_index: 2,
5754                address: Address::default(),
5755                amount: 100,
5756            }],
5757        };
5758
5759        let serialized = serde_json::to_string(&payload).unwrap();
5760        let deserialized: ExecutionPayloadV2 = serde_json::from_str(&serialized).unwrap();
5761        assert_eq!(payload, deserialized);
5762    }
5763
5764    #[test]
5765    #[cfg(feature = "serde")]
5766    fn serde_roundtrip_payload_v4() {
5767        let payload = ExecutionPayloadV4 {
5768            payload_inner: ExecutionPayloadV3 {
5769                payload_inner: ExecutionPayloadV2 {
5770                    payload_inner: ExecutionPayloadV1 {
5771                        parent_hash: B256::default(),
5772                        fee_recipient: Address::default(),
5773                        state_root: B256::default(),
5774                        receipts_root: B256::default(),
5775                        logs_bloom: Bloom::default(),
5776                        prev_randao: B256::default(),
5777                        block_number: 1,
5778                        gas_limit: 30_000_000,
5779                        gas_used: 21000,
5780                        timestamp: 1234,
5781                        extra_data: Bytes::default(),
5782                        base_fee_per_gas: U256::from(7u64),
5783                        block_hash: B256::default(),
5784                        transactions: vec![],
5785                    },
5786                    withdrawals: vec![],
5787                },
5788                blob_gas_used: 0,
5789                excess_blob_gas: 0,
5790            },
5791            block_access_list: Bytes::from(vec![0xaa, 0xbb]),
5792            slot_number: 0,
5793        };
5794
5795        let serialized = serde_json::to_string(&payload).unwrap();
5796        let deserialized: ExecutionPayloadV4 = serde_json::from_str(&serialized).unwrap();
5797        assert_eq!(payload, deserialized);
5798    }
5799
5800    #[test]
5801    fn payload_v4_from_block_falls_back_to_bal_hash_bytes() {
5802        let bal_hash = b256!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
5803        let header = Header {
5804            block_access_list_hash: Some(bal_hash),
5805            slot_number: Some(7),
5806            ..Default::default()
5807        };
5808
5809        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5810        let (payload, _) = ExecutionPayload::from_block_unchecked(B256::with_last_byte(1), &block);
5811
5812        let payload = payload.as_v4().expect("expected V4 payload");
5813        assert_eq!(payload.block_access_list, Bytes::copy_from_slice(bal_hash.as_slice()));
5814        assert_eq!(payload.slot_number, 7);
5815    }
5816
5817    #[test]
5818    fn payload_v4_from_block_without_bal_hash_uses_empty_bal_hash_bytes() {
5819        let header = Header { slot_number: Some(3), ..Default::default() };
5820
5821        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5822        let payload = ExecutionPayloadV4::from_block_unchecked(B256::with_last_byte(2), &block);
5823
5824        assert_eq!(
5825            payload.block_access_list,
5826            Bytes::copy_from_slice(EMPTY_BLOCK_ACCESS_LIST_HASH.as_slice())
5827        );
5828        assert_eq!(payload.slot_number, 3);
5829    }
5830
5831    #[test]
5832    fn execution_data_from_sealed_block_uses_sealed_hash() {
5833        let block: Block<TxEnvelope> = Block::new(Header::default(), BlockBody::default());
5834        let block_hash = B256::with_last_byte(3);
5835
5836        let execution_data = ExecutionData::from(Sealed::new_unchecked(block, block_hash));
5837
5838        assert_eq!(execution_data.block_hash(), block_hash);
5839    }
5840
5841    #[test]
5842    fn execution_data_from_sealed_block_ref_uses_sealed_hash() {
5843        let block: Block<TxEnvelope> = Block::new(Header::default(), BlockBody::default());
5844        let block_hash = B256::with_last_byte(4);
5845
5846        let execution_data = ExecutionData::from(Sealed::new_unchecked(&block, block_hash));
5847
5848        assert_eq!(execution_data.block_hash(), block_hash);
5849    }
5850
5851    #[test]
5852    fn execution_data_from_sealed_block_with_extras_preserves_bal() {
5853        let block_access_list = Bytes::from(vec![0xaa, 0xbb, 0xcc]);
5854        let header = Header {
5855            block_access_list_hash: Some(keccak256(&block_access_list)),
5856            slot_number: Some(7),
5857            ..Default::default()
5858        };
5859
5860        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5861        let block_hash = B256::with_last_byte(5);
5862        let execution_data = ExecutionData::from((
5863            Sealed::new_unchecked(block, block_hash),
5864            PayloadExtras::from(block_access_list.clone()),
5865        ));
5866
5867        assert_eq!(execution_data.block_hash(), block_hash);
5868        assert_eq!(execution_data.payload.block_access_list(), Some(&block_access_list));
5869        assert_eq!(execution_data.payload.slot_number(), Some(7));
5870    }
5871
5872    #[test]
5873    fn execution_data_from_sealed_block_ref_with_extras_preserves_bal() {
5874        let block_access_list = Bytes::from(vec![0xaa, 0xbb, 0xcc]);
5875        let header = Header {
5876            block_access_list_hash: Some(keccak256(&block_access_list)),
5877            slot_number: Some(7),
5878            ..Default::default()
5879        };
5880
5881        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5882        let block_hash = B256::with_last_byte(6);
5883        let execution_data = ExecutionData::from((
5884            Sealed::new_unchecked(&block, block_hash),
5885            PayloadExtras::from(block_access_list.clone()),
5886        ));
5887
5888        assert_eq!(execution_data.block_hash(), block_hash);
5889        assert_eq!(execution_data.payload.block_access_list(), Some(&block_access_list));
5890        assert_eq!(execution_data.payload.slot_number(), Some(7));
5891    }
5892
5893    #[test]
5894    fn execution_payload_gets_bal_hash_and_slot_number_from_v4() {
5895        let block_access_list = Bytes::from(vec![0xaa, 0xbb, 0xcc]);
5896        let payload = ExecutionPayload::from(ExecutionPayloadV4 {
5897            payload_inner: ExecutionPayloadV3 {
5898                payload_inner: ExecutionPayloadV2 {
5899                    payload_inner: ExecutionPayloadV1 {
5900                        parent_hash: B256::default(),
5901                        fee_recipient: Address::default(),
5902                        state_root: B256::default(),
5903                        receipts_root: B256::default(),
5904                        logs_bloom: Bloom::default(),
5905                        prev_randao: B256::default(),
5906                        block_number: 1,
5907                        gas_limit: 30_000_000,
5908                        gas_used: 21_000,
5909                        timestamp: 1_234,
5910                        extra_data: Bytes::default(),
5911                        base_fee_per_gas: U256::ZERO,
5912                        block_hash: B256::default(),
5913                        transactions: vec![],
5914                    },
5915                    withdrawals: vec![],
5916                },
5917                blob_gas_used: 0,
5918                excess_blob_gas: 0,
5919            },
5920            block_access_list: block_access_list.clone(),
5921            slot_number: 7,
5922        });
5923
5924        assert_eq!(payload.slot_number(), Some(7));
5925        assert_eq!(payload.bal_hash(), Some(keccak256(&block_access_list)));
5926    }
5927
5928    #[test]
5929    #[cfg(feature = "serde")]
5930    fn serde_roundtrip_payload_input_v2_with_withdrawals() {
5931        let payload = ExecutionPayloadInputV2 {
5932            execution_payload: ExecutionPayloadV1 {
5933                parent_hash: B256::default(),
5934                fee_recipient: Address::default(),
5935                state_root: B256::default(),
5936                receipts_root: B256::default(),
5937                logs_bloom: Bloom::default(),
5938                prev_randao: B256::default(),
5939                block_number: 1,
5940                gas_limit: 30_000_000,
5941                gas_used: 21000,
5942                timestamp: 1234,
5943                extra_data: Bytes::default(),
5944                base_fee_per_gas: U256::from(7u64),
5945                block_hash: B256::default(),
5946                transactions: vec![],
5947            },
5948            withdrawals: Some(vec![]),
5949        };
5950
5951        let serialized = serde_json::to_string(&payload).unwrap();
5952        let deserialized: ExecutionPayloadInputV2 = serde_json::from_str(&serialized).unwrap();
5953        assert_eq!(payload, deserialized);
5954    }
5955
5956    #[test]
5957    #[cfg(feature = "serde")]
5958    fn serde_roundtrip_payload_input_v2_without_withdrawals() {
5959        let payload = ExecutionPayloadInputV2 {
5960            execution_payload: ExecutionPayloadV1 {
5961                parent_hash: B256::default(),
5962                fee_recipient: Address::default(),
5963                state_root: B256::default(),
5964                receipts_root: B256::default(),
5965                logs_bloom: Bloom::default(),
5966                prev_randao: B256::default(),
5967                block_number: 1,
5968                gas_limit: 30_000_000,
5969                gas_used: 21000,
5970                timestamp: 1234,
5971                extra_data: Bytes::default(),
5972                base_fee_per_gas: U256::from(7u64),
5973                block_hash: B256::default(),
5974                transactions: vec![],
5975            },
5976            withdrawals: None,
5977        };
5978
5979        let serialized = serde_json::to_string(&payload).unwrap();
5980        let deserialized: ExecutionPayloadInputV2 = serde_json::from_str(&serialized).unwrap();
5981        assert_eq!(payload, deserialized);
5982    }
5983
5984    #[test]
5985    #[cfg(feature = "serde")]
5986    fn serde_roundtrip_envelope_v4() {
5987        let envelope = ExecutionPayloadEnvelopeV4 {
5988            envelope_inner: ExecutionPayloadEnvelopeV3 {
5989                execution_payload: ExecutionPayloadV3 {
5990                    payload_inner: ExecutionPayloadV2 {
5991                        payload_inner: ExecutionPayloadV1 {
5992                            parent_hash: B256::default(),
5993                            fee_recipient: Address::default(),
5994                            state_root: B256::default(),
5995                            receipts_root: B256::default(),
5996                            logs_bloom: Bloom::default(),
5997                            prev_randao: B256::default(),
5998                            block_number: 1,
5999                            gas_limit: 30_000_000,
6000                            gas_used: 21000,
6001                            timestamp: 1234,
6002                            extra_data: Bytes::default(),
6003                            base_fee_per_gas: U256::from(7u64),
6004                            block_hash: B256::default(),
6005                            transactions: vec![],
6006                        },
6007                        withdrawals: vec![],
6008                    },
6009                    blob_gas_used: 0,
6010                    excess_blob_gas: 0,
6011                },
6012                block_value: U256::from(1u64),
6013                blobs_bundle: BlobsBundleV1::empty(),
6014                should_override_builder: false,
6015            },
6016            execution_requests: Default::default(),
6017        };
6018
6019        let serialized = serde_json::to_string(&envelope).unwrap();
6020        let deserialized: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&serialized).unwrap();
6021        assert_eq!(envelope, deserialized);
6022    }
6023
6024    #[test]
6025    #[cfg(feature = "serde")]
6026    fn serde_v3_with_many_transactions() {
6027        let tx = Bytes::from_static(&hex!("f865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"));
6028        let transactions: Vec<Bytes> = (0..100).map(|_| tx.clone()).collect();
6029
6030        let payload = ExecutionPayloadV3 {
6031            payload_inner: ExecutionPayloadV2 {
6032                payload_inner: ExecutionPayloadV1 {
6033                    parent_hash: B256::default(),
6034                    fee_recipient: Address::default(),
6035                    state_root: B256::default(),
6036                    receipts_root: B256::default(),
6037                    logs_bloom: Bloom::default(),
6038                    prev_randao: B256::default(),
6039                    block_number: 1,
6040                    gas_limit: 30_000_000,
6041                    gas_used: 2_100_000,
6042                    timestamp: 1234,
6043                    extra_data: Bytes::default(),
6044                    base_fee_per_gas: U256::from(7u64),
6045                    block_hash: B256::default(),
6046                    transactions,
6047                },
6048                withdrawals: vec![],
6049            },
6050            blob_gas_used: 0,
6051            excess_blob_gas: 0,
6052        };
6053
6054        let serialized = serde_json::to_string(&payload).unwrap();
6055        let deserialized: ExecutionPayloadV3 = serde_json::from_str(&serialized).unwrap();
6056        assert_eq!(payload, deserialized);
6057    }
6058
6059    #[test]
6060    #[cfg(feature = "serde")]
6061    fn serde_input_v2_rejects_unknown_fields() {
6062        let input = r#"{
6063            "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
6064            "feeRecipient": "0x0000000000000000000000000000000000000000",
6065            "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
6066            "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
6067            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
6068            "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
6069            "blockNumber": "0x1",
6070            "gasLimit": "0x1c9c380",
6071            "gasUsed": "0x0",
6072            "timestamp": "0x1235",
6073            "extraData": "0x",
6074            "baseFeePerGas": "0x7",
6075            "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
6076            "transactions": [],
6077            "unknownField": "should fail"
6078        }"#;
6079
6080        let result: Result<ExecutionPayloadInputV2, _> = serde_json::from_str(input);
6081        assert!(result.is_err());
6082    }
6083}