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            #[serde(deserialize_with = "alloy_eips::eip4844::deserialize_blobs")]
1883            blobs: Vec<alloy_consensus::Blob>,
1884        }
1885        let raw = BlobsBundleRaw::deserialize(deserializer)?;
1886
1887        if raw.proofs.len() == raw.commitments.len() && raw.proofs.len() == raw.blobs.len() {
1888            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
1889        } else {
1890            Err(serde::de::Error::invalid_length(
1891                raw.proofs.len(),
1892                &format!("{}", raw.commitments.len()).as_str(),
1893            ))
1894        }
1895    }
1896}
1897
1898impl BlobsBundleV1 {
1899    /// Creates a new blob bundle from the given sidecars.
1900    ///
1901    /// This folds the sidecar fields into single commit, proof, and blob vectors.
1902    pub fn new(sidecars: impl IntoIterator<Item = BlobTransactionSidecar>) -> Self {
1903        let (commitments, proofs, blobs) = sidecars.into_iter().fold(
1904            (Vec::new(), Vec::new(), Vec::new()),
1905            |(mut commitments, mut proofs, mut blobs), sidecar| {
1906                commitments.extend(sidecar.commitments);
1907                proofs.extend(sidecar.proofs);
1908                blobs.extend(sidecar.blobs);
1909                (commitments, proofs, blobs)
1910            },
1911        );
1912        Self { commitments, proofs, blobs }
1913    }
1914
1915    /// Returns a new empty blobs bundle.
1916    ///
1917    /// This is useful for the opstack engine API that expects an empty bundle as part of the
1918    /// payload for API compatibility reasons.
1919    pub fn empty() -> Self {
1920        Self::default()
1921    }
1922
1923    /// Computes the versioned hashes from the KZG commitments.
1924    pub fn versioned_hashes(&self) -> Vec<B256> {
1925        self.commitments
1926            .iter()
1927            .map(|c| alloy_eips::eip4844::kzg_to_versioned_hash(c.as_slice()))
1928            .collect()
1929    }
1930
1931    /// Take `len` blob data from the bundle.
1932    ///
1933    /// # Panics
1934    ///
1935    /// If len is more than the blobs bundle len.
1936    pub fn take(&mut self, len: usize) -> (Vec<Bytes48>, Vec<Bytes48>, Vec<Blob>) {
1937        (
1938            self.commitments.drain(0..len).collect(),
1939            self.proofs.drain(0..len).collect(),
1940            self.blobs.drain(0..len).collect(),
1941        )
1942    }
1943
1944    /// Returns the sidecar from the bundle
1945    ///
1946    /// # Panics
1947    ///
1948    /// If len is more than the blobs bundle len.
1949    pub fn pop_sidecar(&mut self, len: usize) -> BlobTransactionSidecar {
1950        let (commitments, proofs, blobs) = self.take(len);
1951        BlobTransactionSidecar { commitments, proofs, blobs }
1952    }
1953
1954    /// Converts this bundle into a single [`BlobTransactionSidecar`].
1955    ///
1956    /// Returns an error if the bundle doesn't contain the same number of commitments as blobs and
1957    /// proofs.
1958    ///
1959    /// Returns an empty [`BlobTransactionSidecar`] if the bundle is empty.
1960    #[cfg(feature = "kzg")]
1961    pub fn try_into_sidecar(
1962        self,
1963    ) -> Result<BlobTransactionSidecar, alloy_consensus::error::ValueError<Self>> {
1964        if self.commitments.len() != self.proofs.len() || self.commitments.len() != self.blobs.len()
1965        {
1966            return Err(alloy_consensus::error::ValueError::new(self, "length mismatch"));
1967        }
1968
1969        let Self { commitments, proofs, blobs } = self;
1970        Ok(BlobTransactionSidecar { blobs, commitments, proofs })
1971    }
1972
1973    /// Converts this V1 bundle into a [`BlobsBundleV2`] by computing EIP-7594 cell proofs.
1974    ///
1975    /// This uses the default KZG settings. See [`Self::try_into_v2_with_settings`] for custom
1976    /// settings.
1977    ///
1978    /// # Errors
1979    ///
1980    /// Returns an error if the bundle has mismatched lengths or if KZG proof computation fails.
1981    #[cfg(feature = "kzg")]
1982    pub fn try_into_v2(self) -> Result<BlobsBundleV2, alloy_eips::eip4844::c_kzg::Error> {
1983        self.try_into_v2_with_settings(
1984            alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get(),
1985        )
1986    }
1987
1988    /// Converts this V1 bundle into a [`BlobsBundleV2`] by computing EIP-7594 cell proofs
1989    /// using the provided KZG settings.
1990    ///
1991    /// # Errors
1992    ///
1993    /// Returns an error if the bundle has mismatched lengths or if KZG proof computation fails.
1994    #[cfg(feature = "kzg")]
1995    pub fn try_into_v2_with_settings(
1996        self,
1997        settings: &alloy_eips::eip4844::c_kzg::KzgSettings,
1998    ) -> Result<BlobsBundleV2, alloy_eips::eip4844::c_kzg::Error> {
1999        use alloy_eips::eip7594::CELLS_PER_EXT_BLOB;
2000
2001        if let [blob] = self.blobs.as_slice() {
2002            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
2003            let cell_proofs =
2004                alloy_eips::eip4844::c_kzg::KzgProof::boxed_slice_as_alloy(kzg_proofs).into();
2005            return Ok(BlobsBundleV2 {
2006                commitments: self.commitments,
2007                proofs: cell_proofs,
2008                blobs: self.blobs,
2009            });
2010        }
2011
2012        let mut cell_proofs = Vec::with_capacity(self.blobs.len() * CELLS_PER_EXT_BLOB);
2013
2014        for blob in self.blobs.iter() {
2015            // Compute cells and their KZG proofs for this blob
2016            let (_cells, kzg_proofs) = settings.compute_cells_and_kzg_proofs(blob.as_ckzg())?;
2017            cell_proofs.extend_from_slice(alloy_eips::eip4844::c_kzg::KzgProof::slice_as_alloy(
2018                kzg_proofs.as_ref(),
2019            ));
2020        }
2021
2022        Ok(BlobsBundleV2 { commitments: self.commitments, proofs: cell_proofs, blobs: self.blobs })
2023    }
2024}
2025
2026impl From<Vec<BlobTransactionSidecar>> for BlobsBundleV1 {
2027    fn from(sidecars: Vec<BlobTransactionSidecar>) -> Self {
2028        Self::new(sidecars)
2029    }
2030}
2031
2032impl FromIterator<BlobTransactionSidecar> for BlobsBundleV1 {
2033    fn from_iter<T: IntoIterator<Item = BlobTransactionSidecar>>(iter: T) -> Self {
2034        Self::new(iter)
2035    }
2036}
2037
2038#[cfg(feature = "kzg")]
2039impl TryFrom<BlobsBundleV1> for BlobTransactionSidecar {
2040    type Error = alloy_consensus::error::ValueError<BlobsBundleV1>;
2041
2042    fn try_from(value: BlobsBundleV1) -> Result<Self, Self::Error> {
2043        value.try_into_sidecar()
2044    }
2045}
2046
2047#[cfg(feature = "kzg")]
2048impl TryFrom<BlobsBundleV1> for BlobsBundleV2 {
2049    type Error = alloy_eips::eip4844::c_kzg::Error;
2050
2051    fn try_from(value: BlobsBundleV1) -> Result<Self, Self::Error> {
2052        value.try_into_v2()
2053    }
2054}
2055
2056/// This includes all bundled blob related data of an executed payload.
2057#[derive(Clone, Debug, Default, PartialEq, Eq)]
2058#[cfg_attr(feature = "serde", derive(serde::Serialize))]
2059#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode))]
2060#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
2061pub struct BlobsBundleV2 {
2062    /// All commitments in the bundle.
2063    pub commitments: Vec<alloy_consensus::Bytes48>,
2064    /// All cell proofs in the bundle.
2065    pub proofs: Vec<alloy_consensus::Bytes48>,
2066    /// All blobs in the bundle.
2067    pub blobs: Vec<alloy_consensus::Blob>,
2068}
2069
2070#[cfg(feature = "serde")]
2071impl<'de> serde::Deserialize<'de> for BlobsBundleV2 {
2072    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2073    where
2074        D: serde::Deserializer<'de>,
2075    {
2076        #[derive(serde::Deserialize)]
2077        struct BlobsBundleRaw {
2078            commitments: Vec<alloy_consensus::Bytes48>,
2079            proofs: Vec<alloy_consensus::Bytes48>,
2080            #[serde(deserialize_with = "alloy_eips::eip4844::deserialize_blobs")]
2081            blobs: Vec<alloy_consensus::Blob>,
2082        }
2083        let raw = BlobsBundleRaw::deserialize(deserializer)?;
2084
2085        if raw.proofs.len() == raw.blobs.len() * CELLS_PER_EXT_BLOB
2086            && raw.commitments.len() == raw.blobs.len()
2087        {
2088            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
2089        } else {
2090            Err(serde::de::Error::invalid_length(
2091                raw.proofs.len(),
2092                &format!("{}", raw.commitments.len() * CELLS_PER_EXT_BLOB).as_str(),
2093            ))
2094        }
2095    }
2096}
2097
2098#[cfg(feature = "ssz")]
2099impl ssz::Decode for BlobsBundleV2 {
2100    fn is_ssz_fixed_len() -> bool {
2101        false
2102    }
2103
2104    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
2105        #[derive(ssz_derive::Decode)]
2106        struct BlobsBundleRaw {
2107            commitments: Vec<alloy_consensus::Bytes48>,
2108            proofs: Vec<alloy_consensus::Bytes48>,
2109            blobs: Vec<alloy_consensus::Blob>,
2110        }
2111
2112        let raw = BlobsBundleRaw::from_ssz_bytes(bytes)?;
2113
2114        if raw.proofs.len() == raw.blobs.len() * CELLS_PER_EXT_BLOB
2115            && raw.commitments.len() == raw.blobs.len()
2116        {
2117            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
2118        } else {
2119            Err(ssz::DecodeError::BytesInvalid(
2120                format!(
2121                    "Invalid BlobsBundleV2: expected {} proofs and {} commitments for {} blobs, got {} proofs and {} commitments",
2122                    raw.blobs.len() * CELLS_PER_EXT_BLOB,
2123                    raw.blobs.len(),
2124                    raw.blobs.len(),
2125                    raw.proofs.len(),
2126                    raw.commitments.len()
2127                )
2128            ))
2129        }
2130    }
2131}
2132
2133impl BlobsBundleV2 {
2134    /// Creates a new blob bundle from the given sidecars.
2135    ///
2136    /// This folds the sidecar fields into single commit, proof, and blob vectors.
2137    pub fn new(sidecars: impl IntoIterator<Item = BlobTransactionSidecarEip7594>) -> Self {
2138        let (commitments, proofs, blobs) = sidecars.into_iter().fold(
2139            (Vec::new(), Vec::new(), Vec::new()),
2140            |(mut commitments, mut proofs, mut blobs), sidecar| {
2141                commitments.extend(sidecar.commitments);
2142                proofs.extend(sidecar.cell_proofs);
2143                blobs.extend(sidecar.blobs);
2144                (commitments, proofs, blobs)
2145            },
2146        );
2147        Self { commitments, proofs, blobs }
2148    }
2149
2150    /// Returns a new empty blobs bundle.
2151    ///
2152    /// This is useful for the opstack engine API that expects an empty bundle as part of the
2153    /// payload for API compatibility reasons.
2154    pub fn empty() -> Self {
2155        Self::default()
2156    }
2157
2158    /// Computes the versioned hashes from the KZG commitments.
2159    pub fn versioned_hashes(&self) -> Vec<B256> {
2160        self.commitments
2161            .iter()
2162            .map(|c| alloy_eips::eip4844::kzg_to_versioned_hash(c.as_slice()))
2163            .collect()
2164    }
2165
2166    /// Take `len` blob data from the bundle.
2167    ///
2168    /// Note this will take `len * CELLS_PER_EXT_BLOB` proofs.
2169    ///
2170    /// # Panics
2171    ///
2172    /// If len is more than the blobs bundle len.
2173    pub fn take(&mut self, len: usize) -> (Vec<Bytes48>, Vec<Bytes48>, Vec<Blob>) {
2174        (
2175            self.commitments.drain(0..len).collect(),
2176            self.proofs.drain(0..len * CELLS_PER_EXT_BLOB).collect(),
2177            self.blobs.drain(0..len).collect(),
2178        )
2179    }
2180
2181    /// Returns the sidecar from the bundle
2182    ///
2183    /// # Panics
2184    ///
2185    /// If len is more than the blobs bundle len.
2186    pub fn pop_sidecar(&mut self, len: usize) -> BlobTransactionSidecarEip7594 {
2187        let (commitments, cell_proofs, blobs) = self.take(len);
2188        BlobTransactionSidecarEip7594 { commitments, cell_proofs, blobs }
2189    }
2190
2191    /// Converts this bundle into a single [`BlobTransactionSidecarEip7594`].
2192    ///
2193    /// Returns an error if the bundle doesn't contain the correct number of cell proofs
2194    /// (expected blobs.len() * CELLS_PER_EXT_BLOB) or if the commitments length doesn't
2195    /// match the blobs length.
2196    ///
2197    /// Returns an empty [`BlobTransactionSidecarEip7594`] if the bundle is empty.
2198    #[cfg(feature = "kzg")]
2199    pub fn try_into_sidecar(
2200        self,
2201    ) -> Result<BlobTransactionSidecarEip7594, alloy_consensus::error::ValueError<Self>> {
2202        let expected_cell_proofs_len = self.blobs.len() * CELLS_PER_EXT_BLOB;
2203        if self.proofs.len() != expected_cell_proofs_len {
2204            let msg = format!(
2205                "cell proofs length mismatch, expected {expected_cell_proofs_len}, has {}",
2206                self.proofs.len()
2207            );
2208            return Err(alloy_consensus::error::ValueError::new(self, msg));
2209        }
2210
2211        if self.commitments.len() != self.blobs.len() {
2212            let msg = format!(
2213                "commitments length ({}) mismatch, expected blob length ({})",
2214                self.commitments.len(),
2215                self.blobs.len()
2216            );
2217            return Err(alloy_consensus::error::ValueError::new(self, msg));
2218        }
2219
2220        let Self { commitments, proofs, blobs } = self;
2221        Ok(BlobTransactionSidecarEip7594 { blobs, commitments, cell_proofs: proofs })
2222    }
2223
2224    /// Converts this V2 bundle into a [`BlobsBundleV1`] by computing EIP-4844 blob proofs.
2225    ///
2226    /// This uses the default KZG settings. See [`Self::try_into_v1_with_settings`] for custom
2227    /// settings.
2228    ///
2229    /// # Errors
2230    ///
2231    /// Returns an error if KZG proof computation fails.
2232    #[cfg(feature = "kzg")]
2233    pub fn try_into_v1(self) -> Result<BlobsBundleV1, alloy_eips::eip4844::c_kzg::Error> {
2234        self.try_into_v1_with_settings(
2235            alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get(),
2236        )
2237    }
2238
2239    /// Converts this V2 bundle into a [`BlobsBundleV1`] by computing EIP-4844 blob proofs
2240    /// using the provided KZG settings.
2241    ///
2242    /// This recomputes the blob proofs from the blobs and commitments. The cell proofs from
2243    /// V2 are discarded as they are not used in V1.
2244    ///
2245    /// # Errors
2246    ///
2247    /// Returns an error if KZG proof computation fails.
2248    #[cfg(feature = "kzg")]
2249    pub fn try_into_v1_with_settings(
2250        self,
2251        settings: &alloy_eips::eip4844::c_kzg::KzgSettings,
2252    ) -> Result<BlobsBundleV1, alloy_eips::eip4844::c_kzg::Error> {
2253        let mut proofs = Vec::with_capacity(self.blobs.len());
2254
2255        for (blob, commitment) in self.blobs.iter().zip(self.commitments.iter()) {
2256            // Compute the blob proof
2257            let proof = settings.compute_blob_kzg_proof(blob.as_ckzg(), commitment.as_ckzg())?;
2258
2259            proofs.push(Bytes48::from_ckzg(proof.to_bytes()));
2260        }
2261
2262        Ok(BlobsBundleV1 { commitments: self.commitments, proofs, blobs: self.blobs })
2263    }
2264}
2265
2266impl From<Vec<BlobTransactionSidecarEip7594>> for BlobsBundleV2 {
2267    fn from(sidecars: Vec<BlobTransactionSidecarEip7594>) -> Self {
2268        Self::new(sidecars)
2269    }
2270}
2271
2272impl FromIterator<BlobTransactionSidecarEip7594> for BlobsBundleV2 {
2273    fn from_iter<T: IntoIterator<Item = BlobTransactionSidecarEip7594>>(iter: T) -> Self {
2274        Self::new(iter)
2275    }
2276}
2277
2278#[cfg(feature = "kzg")]
2279impl TryFrom<BlobsBundleV2> for BlobTransactionSidecarEip7594 {
2280    type Error = alloy_consensus::error::ValueError<BlobsBundleV2>;
2281
2282    fn try_from(value: BlobsBundleV2) -> Result<Self, Self::Error> {
2283        value.try_into_sidecar()
2284    }
2285}
2286
2287#[cfg(feature = "kzg")]
2288impl TryFrom<BlobsBundleV2> for BlobsBundleV1 {
2289    type Error = alloy_eips::eip4844::c_kzg::Error;
2290
2291    fn try_from(value: BlobsBundleV2) -> Result<Self, Self::Error> {
2292        value.try_into_v1()
2293    }
2294}
2295
2296/// An execution payload, which can be either [ExecutionPayloadV1], [ExecutionPayloadV2],
2297/// [ExecutionPayloadV3], or [ExecutionPayloadV4].
2298///
2299/// Payload-to-block conversions return an unsealed block and do not recompute or compare the
2300/// advertised `block_hash`. Callers performing Engine API validation must hash the returned block
2301/// and compare it separately.
2302#[derive(Clone, Debug, PartialEq, Eq)]
2303#[cfg_attr(feature = "serde", derive(serde::Serialize))]
2304#[cfg_attr(feature = "serde", serde(untagged))]
2305#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
2306pub enum ExecutionPayload {
2307    /// V1 payload
2308    V1(ExecutionPayloadV1),
2309    /// V2 payload
2310    V2(ExecutionPayloadV2),
2311    /// V3 payload
2312    V3(ExecutionPayloadV3),
2313    /// V4 payload (Amsterdam)
2314    V4(ExecutionPayloadV4),
2315}
2316
2317impl ExecutionPayload {
2318    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2319    /// [`ExecutionPayloadSidecar`] extracted from the block.
2320    ///
2321    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2322    /// See also [`ExecutionPayloadSidecar::from_block`].
2323    ///
2324    /// Note: This re-calculates the block hash.
2325    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> (Self, ExecutionPayloadSidecar)
2326    where
2327        T: Encodable2718 + Transaction,
2328        H: BlockHeader + Sealable,
2329    {
2330        Self::from_block_unchecked(block.hash_slow(), block)
2331    }
2332
2333    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2334    /// [`ExecutionPayloadSidecar`] extracted from the block along with block access list.
2335    ///
2336    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads.
2337    ///
2338    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2339    /// See also [`ExecutionPayloadSidecar::from_block`].
2340    ///
2341    /// Note: This re-calculates the block hash.
2342    pub fn from_block_slow_with_bal<T, H>(
2343        block: &Block<T, H>,
2344        block_access_list: Bytes,
2345    ) -> (Self, ExecutionPayloadSidecar)
2346    where
2347        T: Encodable2718 + Transaction,
2348        H: BlockHeader + Sealable,
2349    {
2350        Self::from_block_slow_with_extras(block, block_access_list)
2351    }
2352
2353    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2354    /// [`ExecutionPayloadSidecar`] extracted from the block along with payload extras.
2355    ///
2356    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads.
2357    ///
2358    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2359    /// See also [`ExecutionPayloadSidecar::from_block`].
2360    ///
2361    /// Note: This re-calculates the block hash.
2362    pub fn from_block_slow_with_extras<T, H>(
2363        block: &Block<T, H>,
2364        extras: impl Into<PayloadExtras>,
2365    ) -> (Self, ExecutionPayloadSidecar)
2366    where
2367        T: Encodable2718 + Transaction,
2368        H: BlockHeader + Sealable,
2369    {
2370        let extras = extras.into();
2371        if let Some(block_access_list) = extras.bal {
2372            Self::from_block_unchecked_with_bal(block.hash_slow(), block, block_access_list)
2373        } else {
2374            Self::from_block_unchecked(block.hash_slow(), block)
2375        }
2376    }
2377
2378    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2379    /// [`ExecutionPayloadSidecar`] extracted from the block.
2380    ///
2381    /// For Amsterdam/V4 payloads this uses the header's `block_access_list_hash` bytes as the
2382    /// `block_access_list` fallback, because the full RLP-encoded block access list is not part of
2383    /// the block value. If the block header does not carry a BAL hash, this falls back to the
2384    /// canonical empty BAL hash bytes. Use [`Self::from_block_unchecked_with_bal`] when the full
2385    /// block access list bytes are available and should be preserved.
2386    ///
2387    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2388    /// See also [`ExecutionPayloadSidecar::from_block`].
2389    ///
2390    /// The supplied hash is stored verbatim without checking it against the block. The payload
2391    /// version is inferred from the block access-list hash, parent beacon block root, and
2392    /// withdrawals.
2393    pub fn from_block_unchecked<T, H>(
2394        block_hash: B256,
2395        block: &Block<T, H>,
2396    ) -> (Self, ExecutionPayloadSidecar)
2397    where
2398        T: Encodable2718 + Transaction,
2399        H: BlockHeader,
2400    {
2401        let sidecar = ExecutionPayloadSidecar::from_block(block);
2402
2403        let execution_payload = if block.header.block_access_list_hash().is_some() {
2404            // block with block access list hash: V4 (Amsterdam)
2405            Self::V4(ExecutionPayloadV4::from_block_unchecked(block_hash, block))
2406        } else if block.header.parent_beacon_block_root().is_some() {
2407            // block with parent beacon block root: V3
2408            Self::V3(ExecutionPayloadV3::from_block_unchecked(block_hash, block))
2409        } else if block.body.withdrawals.is_some() {
2410            // block with withdrawals: V2
2411            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
2412        } else {
2413            // otherwise V1
2414            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
2415        };
2416
2417        (execution_payload, sidecar)
2418    }
2419
2420    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2421    /// [`ExecutionPayloadSidecar`] extracted from the block along with block access list.
2422    ///
2423    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads.
2424    ///
2425    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2426    /// See also [`ExecutionPayloadSidecar::from_block`].
2427    pub fn from_block_unchecked_with_bal<T, H>(
2428        block_hash: B256,
2429        block: &Block<T, H>,
2430        block_access_list: Bytes,
2431    ) -> (Self, ExecutionPayloadSidecar)
2432    where
2433        T: Encodable2718 + Transaction,
2434        H: BlockHeader,
2435    {
2436        let sidecar = ExecutionPayloadSidecar::from_block(block);
2437
2438        let execution_payload = if block.header.block_access_list_hash().is_some() {
2439            // block with block access list hash: V4 (Amsterdam)
2440            Self::V4(ExecutionPayloadV4::from_block_unchecked_with_bal(
2441                block_hash,
2442                block,
2443                block_access_list,
2444            ))
2445        } else if block.header.parent_beacon_block_root().is_some() {
2446            // block with parent beacon block root: V3
2447            Self::V3(ExecutionPayloadV3::from_block_unchecked(block_hash, block))
2448        } else if block.body.withdrawals.is_some() {
2449            // block with withdrawals: V2
2450            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
2451        } else {
2452            // otherwise V1
2453            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
2454        };
2455
2456        (execution_payload, sidecar)
2457    }
2458
2459    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
2460    /// [`ExecutionPayloadSidecar`] extracted from the block along with optional extras.
2461    ///
2462    /// This preserves the full RLP-encoded block access list for Amsterdam/V4 payloads if provided.
2463    ///
2464    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
2465    /// See also [`ExecutionPayloadSidecar::from_block`].
2466    pub fn from_block_unchecked_with_extras<T, H>(
2467        block_hash: B256,
2468        block: &Block<T, H>,
2469        extras: impl Into<PayloadExtras>,
2470    ) -> (Self, ExecutionPayloadSidecar)
2471    where
2472        T: Encodable2718 + Transaction,
2473        H: BlockHeader,
2474    {
2475        let extras = extras.into();
2476        if let Some(block_access_list) = extras.bal {
2477            Self::from_block_unchecked_with_bal(block_hash, block, block_access_list)
2478        } else {
2479            Self::from_block_unchecked(block_hash, block)
2480        }
2481    }
2482
2483    /// Tries to create a new unsealed block from the given payload and payload sidecar.
2484    ///
2485    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
2486    /// The payload's advertised `block_hash` is not recomputed or compared.
2487    ///
2488    /// # Note
2489    ///
2490    /// The log bloom is assumed to be validated during serialization.
2491    ///
2492    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
2493    pub fn try_into_block_with_sidecar<T: Decodable2718>(
2494        self,
2495        sidecar: &ExecutionPayloadSidecar,
2496    ) -> Result<Block<T>, PayloadError> {
2497        self.try_into_block_with_sidecar_with(sidecar, |tx| {
2498            T::decode_2718_exact(tx.as_ref())
2499                .map_err(alloy_rlp::Error::from)
2500                .map_err(PayloadError::from)
2501        })
2502    }
2503
2504    /// Converts [`ExecutionPayload`] to [`Block`] with sidecar and a custom transaction mapper.
2505    ///
2506    /// The log bloom is assumed to be validated during serialization.
2507    ///
2508    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
2509    pub fn try_into_block_with_sidecar_with<T, F, E>(
2510        self,
2511        sidecar: &ExecutionPayloadSidecar,
2512        f: F,
2513    ) -> Result<Block<T>, PayloadError>
2514    where
2515        F: FnMut(Bytes) -> Result<T, E>,
2516        E: Into<PayloadError>,
2517    {
2518        self.into_block_with_sidecar_raw(sidecar)?.try_map_transactions(f).map_err(Into::into)
2519    }
2520
2521    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions and sidecar.
2522    ///
2523    /// This is similar to [`Self::try_into_block_with_sidecar_with`] but returns the transactions
2524    /// as raw bytes without any conversion.
2525    pub fn into_block_with_sidecar_raw(
2526        self,
2527        sidecar: &ExecutionPayloadSidecar,
2528    ) -> Result<Block<Bytes>, PayloadError> {
2529        let mut base_block = self.into_block_raw()?;
2530        base_block.header.parent_beacon_block_root = sidecar.parent_beacon_block_root();
2531        base_block.header.requests_hash = sidecar.requests_hash();
2532        Ok(base_block)
2533    }
2534
2535    /// Converts [`ExecutionPayload`] to [`Block`].
2536    ///
2537    /// The returned block is unsealed, and the payload's advertised `block_hash` is not recomputed
2538    /// or compared.
2539    ///
2540    /// Caution: This does not set fields that are not part of the payload and only part of the
2541    /// [`ExecutionPayloadSidecar`]:
2542    /// - parent_beacon_block_root
2543    /// - requests_hash
2544    ///
2545    /// See also: [`ExecutionPayload::try_into_block_with_sidecar`]
2546    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
2547        self.try_into_block_with(|tx| {
2548            T::decode_2718_exact(tx.as_ref())
2549                .map_err(alloy_rlp::Error::from)
2550                .map_err(PayloadError::from)
2551        })
2552    }
2553
2554    /// Converts [`ExecutionPayload`] to [`Block`] with a custom transaction mapper.
2555    ///
2556    /// Caution: This does not set fields that are not part of the payload and only part of the
2557    /// [`ExecutionPayloadSidecar`]:
2558    /// - parent_beacon_block_root
2559    /// - requests_hash
2560    ///
2561    /// See also: [`ExecutionPayload::try_into_block_with_sidecar`]
2562    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
2563    where
2564        F: FnMut(Bytes) -> Result<T, E>,
2565        E: Into<PayloadError>,
2566    {
2567        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
2568    }
2569
2570    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions.
2571    ///
2572    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
2573    /// without any conversion.
2574    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
2575        self.into_block_raw_with_transactions_root_opt(None)
2576    }
2577
2578    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions using the
2579    /// given `transactions_root`.
2580    ///
2581    /// See also [`ExecutionPayloadV1::into_block_raw_with_transactions_root`].
2582    pub fn into_block_raw_with_transactions_root(
2583        self,
2584        transactions_root: B256,
2585    ) -> Result<Block<Bytes>, PayloadError> {
2586        self.into_block_raw_with_transactions_root_opt(Some(transactions_root))
2587    }
2588
2589    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions, optionally
2590    /// using the given `transactions_root`.
2591    ///
2592    /// If `transactions_root` is `None`, it will be computed from the transactions.
2593    pub fn into_block_raw_with_transactions_root_opt(
2594        self,
2595        transactions_root: Option<B256>,
2596    ) -> Result<Block<Bytes>, PayloadError> {
2597        match self {
2598            Self::V1(payload) => {
2599                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2600            }
2601            Self::V2(payload) => {
2602                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2603            }
2604            Self::V3(payload) => {
2605                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2606            }
2607            Self::V4(payload) => {
2608                payload.into_block_raw_with_transactions_root_opt(transactions_root)
2609            }
2610        }
2611    }
2612
2613    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions and sidecar
2614    /// using the given `transactions_root`.
2615    ///
2616    /// See also [`Self::into_block_with_sidecar_raw`].
2617    pub fn into_block_with_sidecar_raw_with_transactions_root(
2618        self,
2619        sidecar: &ExecutionPayloadSidecar,
2620        transactions_root: B256,
2621    ) -> Result<Block<Bytes>, PayloadError> {
2622        let mut base_block = self.into_block_raw_with_transactions_root(transactions_root)?;
2623        base_block.header.parent_beacon_block_root = sidecar.parent_beacon_block_root();
2624        base_block.header.requests_hash = sidecar.requests_hash();
2625        Ok(base_block)
2626    }
2627
2628    /// Returns a reference to the V1 payload.
2629    pub const fn as_v1(&self) -> &ExecutionPayloadV1 {
2630        match self {
2631            Self::V1(payload) => payload,
2632            Self::V2(payload) => &payload.payload_inner,
2633            Self::V3(payload) => &payload.payload_inner.payload_inner,
2634            Self::V4(payload) => &payload.payload_inner.payload_inner.payload_inner,
2635        }
2636    }
2637
2638    /// Returns a mutable reference to the V1 payload.
2639    pub const fn as_v1_mut(&mut self) -> &mut ExecutionPayloadV1 {
2640        match self {
2641            Self::V1(payload) => payload,
2642            Self::V2(payload) => &mut payload.payload_inner,
2643            Self::V3(payload) => &mut payload.payload_inner.payload_inner,
2644            Self::V4(payload) => &mut payload.payload_inner.payload_inner.payload_inner,
2645        }
2646    }
2647
2648    /// Consumes the payload and returns the V1 payload.
2649    pub fn into_v1(self) -> ExecutionPayloadV1 {
2650        match self {
2651            Self::V1(payload) => payload,
2652            Self::V2(payload) => payload.payload_inner,
2653            Self::V3(payload) => payload.payload_inner.payload_inner,
2654            Self::V4(payload) => payload.payload_inner.payload_inner.payload_inner,
2655        }
2656    }
2657
2658    /// Returns a reference to the V2 payload, if any.
2659    pub const fn as_v2(&self) -> Option<&ExecutionPayloadV2> {
2660        match self {
2661            Self::V1(_) => None,
2662            Self::V2(payload) => Some(payload),
2663            Self::V3(payload) => Some(&payload.payload_inner),
2664            Self::V4(payload) => Some(&payload.payload_inner.payload_inner),
2665        }
2666    }
2667
2668    /// Returns a mutable reference to the V2 payload, if any.
2669    pub const fn as_v2_mut(&mut self) -> Option<&mut ExecutionPayloadV2> {
2670        match self {
2671            Self::V1(_) => None,
2672            Self::V2(payload) => Some(payload),
2673            Self::V3(payload) => Some(&mut payload.payload_inner),
2674            Self::V4(payload) => Some(&mut payload.payload_inner.payload_inner),
2675        }
2676    }
2677
2678    /// Returns a reference to the V3 payload, if any.
2679    pub const fn as_v3(&self) -> Option<&ExecutionPayloadV3> {
2680        match self {
2681            Self::V1(_) | Self::V2(_) => None,
2682            Self::V3(payload) => Some(payload),
2683            Self::V4(payload) => Some(&payload.payload_inner),
2684        }
2685    }
2686
2687    /// Returns a mutable reference to the V3 payload, if any.
2688    pub const fn as_v3_mut(&mut self) -> Option<&mut ExecutionPayloadV3> {
2689        match self {
2690            Self::V1(_) | Self::V2(_) => None,
2691            Self::V3(payload) => Some(payload),
2692            Self::V4(payload) => Some(&mut payload.payload_inner),
2693        }
2694    }
2695
2696    /// Returns a reference to the V4 payload, if any.
2697    pub const fn as_v4(&self) -> Option<&ExecutionPayloadV4> {
2698        match self {
2699            Self::V1(_) | Self::V2(_) | Self::V3(_) => None,
2700            Self::V4(payload) => Some(payload),
2701        }
2702    }
2703
2704    /// Returns a mutable reference to the V4 payload, if any.
2705    pub const fn as_v4_mut(&mut self) -> Option<&mut ExecutionPayloadV4> {
2706        match self {
2707            Self::V1(_) | Self::V2(_) | Self::V3(_) => None,
2708            Self::V4(payload) => Some(payload),
2709        }
2710    }
2711
2712    /// Returns the withdrawals for the payload.
2713    pub const fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
2714        match self.as_v2() {
2715            Some(payload) => Some(&payload.withdrawals),
2716            None => None,
2717        }
2718    }
2719
2720    /// Returns the transactions for the payload.
2721    pub const fn transactions(&self) -> &Vec<Bytes> {
2722        &self.as_v1().transactions
2723    }
2724
2725    /// Returns a mutable reference to the transactions for the payload.
2726    pub const fn transactions_mut(&mut self) -> &mut Vec<Bytes> {
2727        &mut self.as_v1_mut().transactions
2728    }
2729
2730    /// Extracts essential information into one container type.
2731    pub fn header_info(&self) -> HeaderInfo {
2732        HeaderInfo {
2733            number: self.block_number(),
2734            beneficiary: self.fee_recipient(),
2735            timestamp: self.timestamp(),
2736            gas_limit: self.gas_limit(),
2737            base_fee_per_gas: Some(self.saturated_base_fee_per_gas()),
2738            excess_blob_gas: self.excess_blob_gas(),
2739            blob_gas_used: self.blob_gas_used(),
2740            difficulty: U256::ZERO,
2741            mix_hash: Some(self.prev_randao()),
2742            slot_number: self.as_v4().map(|payload| payload.slot_number),
2743        }
2744    }
2745
2746    /// Returns the gas limit for the payload.
2747    ///
2748    /// Note: this returns the u64 saturated base fee, but it is specified as [`U256`].
2749    pub fn saturated_base_fee_per_gas(&self) -> u64 {
2750        self.as_v1().base_fee_per_gas.saturating_to()
2751    }
2752
2753    /// Returns the blob gas used for the payload.
2754    pub fn blob_gas_used(&self) -> Option<u64> {
2755        self.as_v3().map(|payload| payload.blob_gas_used)
2756    }
2757
2758    /// Returns the excess blob gas for the payload.
2759    pub fn excess_blob_gas(&self) -> Option<u64> {
2760        self.as_v3().map(|payload| payload.excess_blob_gas)
2761    }
2762
2763    /// Returns the block access list for the payload (EIP-7928).
2764    ///
2765    /// Returns `None` for pre-Amsterdam payloads (V1, V2, V3).
2766    pub fn block_access_list(&self) -> Option<&Bytes> {
2767        self.as_v4().map(|payload| &payload.block_access_list)
2768    }
2769
2770    /// Returns the block access list hash for the payload (EIP-7928).
2771    ///
2772    /// Returns `None` for pre-Amsterdam payloads (V1, V2, V3).
2773    pub fn bal_hash(&self) -> Option<B256> {
2774        self.as_v4().map(|payload| keccak256(&payload.block_access_list))
2775    }
2776
2777    /// Returns the slot number for the payload (EIP-7843).
2778    ///
2779    /// Returns `None` for pre-Amsterdam payloads (V1, V2, V3).
2780    pub fn slot_number(&self) -> Option<u64> {
2781        self.as_v4().map(|payload| payload.slot_number)
2782    }
2783
2784    /// Returns the gas limit for the payload.
2785    pub const fn gas_limit(&self) -> u64 {
2786        self.as_v1().gas_limit
2787    }
2788
2789    /// Returns the fee recipient.
2790    pub const fn fee_recipient(&self) -> Address {
2791        self.as_v1().fee_recipient
2792    }
2793
2794    /// Returns the timestamp for the payload.
2795    pub const fn timestamp(&self) -> u64 {
2796        self.as_v1().timestamp
2797    }
2798
2799    /// Returns the parent hash for the payload.
2800    pub const fn parent_hash(&self) -> B256 {
2801        self.as_v1().parent_hash
2802    }
2803
2804    /// Returns the block hash for the payload.
2805    pub const fn block_hash(&self) -> B256 {
2806        self.as_v1().block_hash
2807    }
2808
2809    /// Returns the block number for this payload.
2810    pub const fn block_number(&self) -> u64 {
2811        self.as_v1().block_number
2812    }
2813
2814    /// Returns the block number for this payload.
2815    pub const fn block_num_hash(&self) -> BlockNumHash {
2816        self.as_v1().block_num_hash()
2817    }
2818
2819    /// Returns the prev randao for this payload.
2820    pub const fn prev_randao(&self) -> B256 {
2821        self.as_v1().prev_randao
2822    }
2823
2824    /// Returns the blob fee for _this_ block according to the EIP-4844 spec.
2825    ///
2826    /// Returns `None` if `excess_blob_gas` is None
2827    pub fn blob_fee(&self, blob_params: BlobParams) -> Option<u128> {
2828        Some(blob_params.calc_blob_fee(self.excess_blob_gas()?))
2829    }
2830
2831    /// Returns the blob fee for the next block according to the EIP-4844 spec.
2832    ///
2833    /// Returns `None` if `excess_blob_gas` is None.
2834    ///
2835    /// See also [Self::next_block_excess_blob_gas]
2836    pub fn next_block_blob_fee(&self, blob_params: BlobParams) -> Option<u128> {
2837        Some(blob_params.calc_blob_fee(self.next_block_excess_blob_gas(blob_params)?))
2838    }
2839
2840    /// Calculate base fee for next block according to the EIP-1559 spec.
2841    ///
2842    /// Returns a `None` if no base fee is set, no EIP-1559 support
2843    pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option<u64> {
2844        self.as_v1().next_block_base_fee(base_fee_params)
2845    }
2846
2847    /// Calculate excess blob gas for the next block according to the EIP-4844
2848    /// spec.
2849    ///
2850    /// Returns a `None` if no excess blob gas is set, no EIP-4844 support
2851    pub fn next_block_excess_blob_gas(&self, blob_params: BlobParams) -> Option<u64> {
2852        Some(blob_params.next_block_excess_blob_gas_osaka(
2853            self.excess_blob_gas()?,
2854            self.blob_gas_used()?,
2855            self.as_v1().base_fee_per_gas.to(),
2856        ))
2857    }
2858
2859    /// Convenience function for [`Self::next_block_excess_blob_gas`] with an optional
2860    /// [`BlobParams`] argument.
2861    ///
2862    /// Returns `None` if the `blob_params` are `None`.
2863    pub fn maybe_next_block_excess_blob_gas(&self, blob_params: Option<BlobParams>) -> Option<u64> {
2864        self.next_block_excess_blob_gas(blob_params?)
2865    }
2866
2867    /// Returns an iterator over the decoded transactions in this payload.
2868    ///
2869    /// This iterator will decode transactions on the fly.
2870    pub fn decoded_transactions<T: Decodable2718>(
2871        &self,
2872    ) -> impl Iterator<Item = Eip2718Result<T>> + '_ {
2873        self.transactions().iter().map(|tx_bytes| T::decode_2718_exact(tx_bytes.as_ref()))
2874    }
2875
2876    /// Returns iterator over decoded transactions with their original encoded bytes.
2877    ///
2878    /// This iterator will decode transactions on the fly and return them with their bytes.
2879    pub fn decoded_transactions_with_encoded<T: Decodable2718>(
2880        &self,
2881    ) -> impl Iterator<Item = Eip2718Result<WithEncoded<T>>> + '_ {
2882        self.transactions().iter().map(|tx_bytes| {
2883            T::decode_2718_exact(tx_bytes.as_ref()).map(|tx| WithEncoded::new(tx_bytes.clone(), tx))
2884        })
2885    }
2886
2887    /// Returns an iterator over the recovered transactions in this payload.
2888    ///
2889    /// This iterator will decode and recover signer addresses for transactions on the fly.
2890    pub fn recovered_transactions<T>(
2891        &self,
2892    ) -> impl Iterator<
2893        Item = Result<
2894            alloy_consensus::transaction::Recovered<T>,
2895            alloy_consensus::crypto::RecoveryError,
2896        >,
2897    > + '_
2898    where
2899        T: Decodable2718 + alloy_consensus::transaction::SignerRecoverable,
2900    {
2901        self.decoded_transactions::<T>().map(|res| {
2902            res.map_err(alloy_consensus::crypto::RecoveryError::from_source)
2903                .and_then(|tx| tx.try_into_recovered())
2904        })
2905    }
2906
2907    /// Returns an iterator over the recovered transactions in this payload with their
2908    /// original encoded bytes.
2909    ///
2910    /// This iterator will decode and recover signer addresses for transactions on the fly
2911    /// and return them with their bytes.
2912    pub fn recovered_transactions_with_encoded<T>(
2913        &self,
2914    ) -> impl Iterator<
2915        Item = Result<
2916            WithEncoded<alloy_consensus::transaction::Recovered<T>>,
2917            alloy_consensus::crypto::RecoveryError,
2918        >,
2919    > + '_
2920    where
2921        T: Decodable2718 + alloy_consensus::transaction::SignerRecoverable,
2922    {
2923        self.transactions().iter().map(|tx_bytes| {
2924            T::decode_2718_exact(tx_bytes.as_ref())
2925                .map_err(alloy_consensus::crypto::RecoveryError::from_source)
2926                .and_then(|tx| {
2927                    tx.try_into_recovered()
2928                        .map(|recovered| WithEncoded::new(tx_bytes.clone(), recovered))
2929                })
2930        })
2931    }
2932
2933    /// Sets the parent hash for the payload.
2934    #[doc(hidden)]
2935    pub const fn set_parent_hash(&mut self, parent_hash: B256) {
2936        self.as_v1_mut().parent_hash = parent_hash;
2937    }
2938
2939    /// Sets the fee recipient for the payload.
2940    #[doc(hidden)]
2941    pub const fn set_fee_recipient(&mut self, fee_recipient: Address) {
2942        self.as_v1_mut().fee_recipient = fee_recipient;
2943    }
2944
2945    /// Sets the state root for the payload.
2946    #[doc(hidden)]
2947    pub const fn set_state_root(&mut self, state_root: B256) {
2948        self.as_v1_mut().state_root = state_root;
2949    }
2950
2951    /// Sets the receipts root for the payload.
2952    #[doc(hidden)]
2953    pub const fn set_receipts_root(&mut self, receipts_root: B256) {
2954        self.as_v1_mut().receipts_root = receipts_root;
2955    }
2956
2957    /// Sets the logs bloom for the payload.
2958    #[doc(hidden)]
2959    pub const fn set_logs_bloom(&mut self, logs_bloom: Bloom) {
2960        self.as_v1_mut().logs_bloom = logs_bloom;
2961    }
2962
2963    /// Sets the prev randao for the payload.
2964    #[doc(hidden)]
2965    pub const fn set_prev_randao(&mut self, prev_randao: B256) {
2966        self.as_v1_mut().prev_randao = prev_randao;
2967    }
2968
2969    /// Sets the block number for the payload.
2970    #[doc(hidden)]
2971    pub const fn set_block_number(&mut self, block_number: u64) {
2972        self.as_v1_mut().block_number = block_number;
2973    }
2974
2975    /// Sets the gas limit for the payload.
2976    #[doc(hidden)]
2977    pub const fn set_gas_limit(&mut self, gas_limit: u64) {
2978        self.as_v1_mut().gas_limit = gas_limit;
2979    }
2980
2981    /// Sets the gas used for the payload.
2982    #[doc(hidden)]
2983    pub const fn set_gas_used(&mut self, gas_used: u64) {
2984        self.as_v1_mut().gas_used = gas_used;
2985    }
2986
2987    /// Sets the timestamp for the payload.
2988    #[doc(hidden)]
2989    pub const fn set_timestamp(&mut self, timestamp: u64) {
2990        self.as_v1_mut().timestamp = timestamp;
2991    }
2992
2993    /// Sets the extra data for the payload.
2994    #[doc(hidden)]
2995    pub fn set_extra_data(&mut self, extra_data: Bytes) {
2996        self.as_v1_mut().extra_data = extra_data;
2997    }
2998
2999    /// Sets the base fee per gas for the payload.
3000    #[doc(hidden)]
3001    pub const fn set_base_fee_per_gas(&mut self, base_fee_per_gas: U256) {
3002        self.as_v1_mut().base_fee_per_gas = base_fee_per_gas;
3003    }
3004
3005    /// Sets the block hash for the payload.
3006    #[doc(hidden)]
3007    pub const fn set_block_hash(&mut self, block_hash: B256) {
3008        self.as_v1_mut().block_hash = block_hash;
3009    }
3010
3011    /// Sets the withdrawals for the payload.
3012    ///
3013    /// Returns `true` if the payload is V2 or higher and the withdrawals were set.
3014    #[doc(hidden)]
3015    pub fn set_withdrawals(&mut self, withdrawals: Vec<Withdrawal>) -> bool {
3016        match self.as_v2_mut() {
3017            Some(payload) => {
3018                payload.withdrawals = withdrawals;
3019                true
3020            }
3021            None => false,
3022        }
3023    }
3024
3025    /// Sets the blob gas used for the payload.
3026    ///
3027    /// Returns `true` if the payload is V3 or higher and the value was set.
3028    #[doc(hidden)]
3029    pub const fn set_blob_gas_used(&mut self, blob_gas_used: u64) -> bool {
3030        match self.as_v3_mut() {
3031            Some(payload) => {
3032                payload.blob_gas_used = blob_gas_used;
3033                true
3034            }
3035            None => false,
3036        }
3037    }
3038
3039    /// Sets the excess blob gas for the payload.
3040    ///
3041    /// Returns `true` if the payload is V3 or higher and the value was set.
3042    #[doc(hidden)]
3043    pub const fn set_excess_blob_gas(&mut self, excess_blob_gas: u64) -> bool {
3044        match self.as_v3_mut() {
3045            Some(payload) => {
3046                payload.excess_blob_gas = excess_blob_gas;
3047                true
3048            }
3049            None => false,
3050        }
3051    }
3052}
3053
3054impl From<ExecutionPayloadV1> for ExecutionPayload {
3055    fn from(payload: ExecutionPayloadV1) -> Self {
3056        Self::V1(payload)
3057    }
3058}
3059
3060impl From<ExecutionPayloadV2> for ExecutionPayload {
3061    fn from(payload: ExecutionPayloadV2) -> Self {
3062        Self::V2(payload)
3063    }
3064}
3065
3066impl From<ExecutionPayloadFieldV2> for ExecutionPayload {
3067    fn from(payload: ExecutionPayloadFieldV2) -> Self {
3068        payload.into_payload()
3069    }
3070}
3071
3072impl From<ExecutionPayloadV3> for ExecutionPayload {
3073    fn from(payload: ExecutionPayloadV3) -> Self {
3074        Self::V3(payload)
3075    }
3076}
3077
3078impl From<ExecutionPayloadV4> for ExecutionPayload {
3079    fn from(payload: ExecutionPayloadV4) -> Self {
3080        Self::V4(payload)
3081    }
3082}
3083
3084impl<T: Decodable2718> TryFrom<ExecutionPayload> for Block<T> {
3085    type Error = PayloadError;
3086
3087    fn try_from(value: ExecutionPayload) -> Result<Self, Self::Error> {
3088        value.try_into_block()
3089    }
3090}
3091
3092// Deserializes untagged ExecutionPayload depending on the available fields
3093#[cfg(feature = "serde")]
3094impl<'de> serde::Deserialize<'de> for ExecutionPayload {
3095    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3096    where
3097        D: serde::Deserializer<'de>,
3098    {
3099        use alloy_primitives::U64;
3100
3101        struct ExecutionPayloadVisitor;
3102
3103        impl<'de> serde::de::Visitor<'de> for ExecutionPayloadVisitor {
3104            type Value = ExecutionPayload;
3105
3106            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3107                formatter.write_str("a valid ExecutionPayload object")
3108            }
3109
3110            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
3111            where
3112                A: serde::de::MapAccess<'de>,
3113            {
3114                // this currently rejects unknown fields
3115                #[cfg_attr(feature = "serde", derive(serde::Deserialize))]
3116                #[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "camelCase"))]
3117                enum Fields {
3118                    ParentHash,
3119                    FeeRecipient,
3120                    StateRoot,
3121                    ReceiptsRoot,
3122                    LogsBloom,
3123                    PrevRandao,
3124                    BlockNumber,
3125                    GasLimit,
3126                    GasUsed,
3127                    Timestamp,
3128                    ExtraData,
3129                    BaseFeePerGas,
3130                    BlockHash,
3131                    Transactions,
3132                    // V2
3133                    Withdrawals,
3134                    // V3
3135                    BlobGasUsed,
3136                    ExcessBlobGas,
3137                    // V4
3138                    BlockAccessList,
3139                    SlotNumber,
3140                }
3141
3142                let mut parent_hash = None;
3143                let mut fee_recipient = None;
3144                let mut state_root = None;
3145                let mut receipts_root = None;
3146                let mut logs_bloom = None;
3147                let mut prev_randao = None;
3148                let mut block_number = None;
3149                let mut gas_limit = None;
3150                let mut gas_used = None;
3151                let mut timestamp = None;
3152                let mut extra_data = None;
3153                let mut base_fee_per_gas = None;
3154                let mut block_hash = None;
3155                let mut transactions = None;
3156                let mut withdrawals = None;
3157                let mut blob_gas_used = None;
3158                let mut excess_blob_gas = None;
3159                let mut block_access_list = None;
3160                let mut slot_number = None;
3161
3162                while let Some(key) = map.next_key()? {
3163                    match key {
3164                        Fields::ParentHash => parent_hash = Some(map.next_value()?),
3165                        Fields::FeeRecipient => fee_recipient = Some(map.next_value()?),
3166                        Fields::StateRoot => state_root = Some(map.next_value()?),
3167                        Fields::ReceiptsRoot => receipts_root = Some(map.next_value()?),
3168                        Fields::LogsBloom => logs_bloom = Some(map.next_value()?),
3169                        Fields::PrevRandao => prev_randao = Some(map.next_value()?),
3170                        Fields::BlockNumber => {
3171                            let raw = map.next_value::<U64>()?;
3172                            block_number = Some(raw.to());
3173                        }
3174                        Fields::GasLimit => {
3175                            let raw = map.next_value::<U64>()?;
3176                            gas_limit = Some(raw.to());
3177                        }
3178                        Fields::GasUsed => {
3179                            let raw = map.next_value::<U64>()?;
3180                            gas_used = Some(raw.to());
3181                        }
3182                        Fields::Timestamp => {
3183                            let raw = map.next_value::<U64>()?;
3184                            timestamp = Some(raw.to());
3185                        }
3186                        Fields::ExtraData => extra_data = Some(map.next_value()?),
3187                        Fields::BaseFeePerGas => base_fee_per_gas = Some(map.next_value()?),
3188                        Fields::BlockHash => block_hash = Some(map.next_value()?),
3189                        Fields::Transactions => transactions = Some(map.next_value()?),
3190                        Fields::Withdrawals => withdrawals = Some(map.next_value()?),
3191                        Fields::BlobGasUsed => {
3192                            let raw = map.next_value::<U64>()?;
3193                            blob_gas_used = Some(raw.to());
3194                        }
3195                        Fields::ExcessBlobGas => {
3196                            let raw = map.next_value::<U64>()?;
3197                            excess_blob_gas = Some(raw.to());
3198                        }
3199                        Fields::BlockAccessList => {
3200                            block_access_list = Some(map.next_value()?);
3201                        }
3202                        Fields::SlotNumber => {
3203                            let raw = map.next_value::<U64>()?;
3204                            slot_number = Some(raw.to());
3205                        }
3206                    }
3207                }
3208
3209                let parent_hash =
3210                    parent_hash.ok_or_else(|| serde::de::Error::missing_field("parentHash"))?;
3211                let fee_recipient =
3212                    fee_recipient.ok_or_else(|| serde::de::Error::missing_field("feeRecipient"))?;
3213                let state_root =
3214                    state_root.ok_or_else(|| serde::de::Error::missing_field("stateRoot"))?;
3215                let receipts_root =
3216                    receipts_root.ok_or_else(|| serde::de::Error::missing_field("receiptsRoot"))?;
3217                let logs_bloom =
3218                    logs_bloom.ok_or_else(|| serde::de::Error::missing_field("logsBloom"))?;
3219                let prev_randao =
3220                    prev_randao.ok_or_else(|| serde::de::Error::missing_field("prevRandao"))?;
3221                let block_number =
3222                    block_number.ok_or_else(|| serde::de::Error::missing_field("blockNumber"))?;
3223                let gas_limit =
3224                    gas_limit.ok_or_else(|| serde::de::Error::missing_field("gasLimit"))?;
3225                let gas_used =
3226                    gas_used.ok_or_else(|| serde::de::Error::missing_field("gasUsed"))?;
3227                let timestamp =
3228                    timestamp.ok_or_else(|| serde::de::Error::missing_field("timestamp"))?;
3229                let extra_data =
3230                    extra_data.ok_or_else(|| serde::de::Error::missing_field("extraData"))?;
3231                let base_fee_per_gas = base_fee_per_gas
3232                    .ok_or_else(|| serde::de::Error::missing_field("baseFeePerGas"))?;
3233                let block_hash =
3234                    block_hash.ok_or_else(|| serde::de::Error::missing_field("blockHash"))?;
3235                let transactions =
3236                    transactions.ok_or_else(|| serde::de::Error::missing_field("transactions"))?;
3237
3238                let v1 = ExecutionPayloadV1 {
3239                    parent_hash,
3240                    fee_recipient,
3241                    state_root,
3242                    receipts_root,
3243                    logs_bloom,
3244                    prev_randao,
3245                    block_number,
3246                    gas_limit,
3247                    gas_used,
3248                    timestamp,
3249                    extra_data,
3250                    base_fee_per_gas,
3251                    block_hash,
3252                    transactions,
3253                };
3254
3255                let Some(withdrawals) = withdrawals else {
3256                    return if blob_gas_used.is_none() && excess_blob_gas.is_none() {
3257                        Ok(ExecutionPayload::V1(v1))
3258                    } else {
3259                        Err(serde::de::Error::custom("invalid enum variant"))
3260                    };
3261                };
3262
3263                if let (Some(blob_gas_used), Some(excess_blob_gas)) =
3264                    (blob_gas_used, excess_blob_gas)
3265                {
3266                    let v3 = ExecutionPayloadV3 {
3267                        payload_inner: ExecutionPayloadV2 { payload_inner: v1, withdrawals },
3268                        blob_gas_used,
3269                        excess_blob_gas,
3270                    };
3271
3272                    // Check for V4 fields (block_access_list and slot_number)
3273                    return match (block_access_list, slot_number) {
3274                        (Some(block_access_list), Some(slot_number)) => {
3275                            Ok(ExecutionPayload::V4(ExecutionPayloadV4 {
3276                                payload_inner: v3,
3277                                block_access_list,
3278                                slot_number,
3279                            }))
3280                        }
3281                        // reject incomplete V4 payloads
3282                        (None, None) => Ok(ExecutionPayload::V3(v3)),
3283                        _ => Err(serde::de::Error::custom("invalid enum variant")),
3284                    };
3285                }
3286
3287                // reject incomplete V3 payloads even if they could construct a valid V2
3288                if blob_gas_used.is_some() || excess_blob_gas.is_some() {
3289                    return Err(serde::de::Error::custom("invalid enum variant"));
3290                }
3291
3292                // reject V4 fields without V3 fields
3293                if block_access_list.is_some() || slot_number.is_some() {
3294                    return Err(serde::de::Error::custom("invalid enum variant"));
3295                }
3296
3297                Ok(ExecutionPayload::V2(ExecutionPayloadV2 { payload_inner: v1, withdrawals }))
3298            }
3299        }
3300
3301        const FIELDS: &[&str] = &[
3302            "parentHash",
3303            "feeRecipient",
3304            "stateRoot",
3305            "receiptsRoot",
3306            "logsBloom",
3307            "prevRandao",
3308            "blockNumber",
3309            "gasLimit",
3310            "gasUsed",
3311            "timestamp",
3312            "extraData",
3313            "baseFeePerGas",
3314            "blockHash",
3315            "transactions",
3316            "withdrawals",
3317            "blobGasUsed",
3318            "excessBlobGas",
3319            "blockAccessList",
3320            "slotNumber",
3321        ];
3322        deserializer.deserialize_struct("ExecutionPayload", FIELDS, ExecutionPayloadVisitor)
3323    }
3324}
3325
3326/// This structure contains a body of an execution payload.
3327///
3328/// See also: <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#executionpayloadbodyv1>
3329#[derive(Clone, Debug, PartialEq, Eq)]
3330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3331#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
3332#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3333pub struct ExecutionPayloadBodyV1 {
3334    /// Enveloped encoded transactions.
3335    pub transactions: Vec<Bytes>,
3336    /// All withdrawals in the block.
3337    ///
3338    /// Will always be `None` if pre shanghai.
3339    pub withdrawals: Option<Vec<Withdrawal>>,
3340}
3341
3342impl ExecutionPayloadBodyV1 {
3343    /// Creates an [`ExecutionPayloadBodyV1`] from the given withdrawals and transactions
3344    pub fn new<'a, T>(
3345        withdrawals: Option<Withdrawals>,
3346        transactions: impl IntoIterator<Item = &'a T>,
3347    ) -> Self
3348    where
3349        T: Encodable2718 + 'a,
3350    {
3351        Self {
3352            transactions: transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(),
3353            withdrawals: withdrawals.map(Withdrawals::into_inner),
3354        }
3355    }
3356
3357    /// Converts a [`alloy_consensus::Block`] into an execution payload body.
3358    pub fn from_block<T: Encodable2718, H>(block: Block<T, H>) -> Self {
3359        let BlockBody { withdrawals, transactions, .. } = block.into_body();
3360        Self::new(withdrawals, transactions.iter())
3361    }
3362}
3363
3364impl<T: Encodable2718, H> From<Block<T, H>> for ExecutionPayloadBodyV1 {
3365    fn from(value: Block<T, H>) -> Self {
3366        Self::from_block(value)
3367    }
3368}
3369
3370/// This structure contains a body of an execution payload (V2).
3371///
3372/// V2 extends V1 with the `blockAccessList` field introduced in EIP-7928.
3373///
3374/// See also: <https://eips.ethereum.org/EIPS/eip-7928>
3375#[derive(Clone, Debug, PartialEq, Eq)]
3376#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3377#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3378#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
3379#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3380pub struct ExecutionPayloadBodyV2 {
3381    /// Enveloped encoded transactions.
3382    pub transactions: Vec<Bytes>,
3383    /// All withdrawals in the block.
3384    ///
3385    /// Will always be `None` if pre shanghai.
3386    pub withdrawals: Option<Vec<Withdrawal>>,
3387    /// The RLP-encoded block access list.
3388    ///
3389    /// Will be `None` for pre-Amsterdam blocks or when data has been pruned.
3390    pub block_access_list: Option<Bytes>,
3391}
3392
3393impl ExecutionPayloadBodyV2 {
3394    /// Creates an [`ExecutionPayloadBodyV2`] from the given withdrawals, transactions, and block
3395    /// access list.
3396    pub fn new<'a, T>(
3397        withdrawals: Option<Withdrawals>,
3398        transactions: impl IntoIterator<Item = &'a T>,
3399        block_access_list: Option<Bytes>,
3400    ) -> Self
3401    where
3402        T: Encodable2718 + 'a,
3403    {
3404        Self {
3405            transactions: transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(),
3406            withdrawals: withdrawals.map(Withdrawals::into_inner),
3407            block_access_list,
3408        }
3409    }
3410
3411    /// Converts a [`alloy_consensus::Block`] into an execution payload body, with an optional
3412    /// block access list.
3413    pub fn from_block<T: Encodable2718, H>(
3414        block: Block<T, H>,
3415        block_access_list: Option<Bytes>,
3416    ) -> Self {
3417        let BlockBody { withdrawals, transactions, .. } = block.into_body();
3418        Self::new(withdrawals, transactions.iter(), block_access_list)
3419    }
3420}
3421
3422impl From<ExecutionPayloadBodyV1> for ExecutionPayloadBodyV2 {
3423    fn from(v1: ExecutionPayloadBodyV1) -> Self {
3424        Self { transactions: v1.transactions, withdrawals: v1.withdrawals, block_access_list: None }
3425    }
3426}
3427
3428impl From<ExecutionPayloadBodyV2> for ExecutionPayloadBodyV1 {
3429    fn from(v2: ExecutionPayloadBodyV2) -> Self {
3430        Self { transactions: v2.transactions, withdrawals: v2.withdrawals }
3431    }
3432}
3433
3434/// This structure contains the attributes required to initiate a payload build process in the
3435/// context of an `engine_forkchoiceUpdated` call.
3436#[derive(Clone, Debug, Default, PartialEq, Eq)]
3437#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3438#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3439#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3440pub struct PayloadAttributes {
3441    /// Value for the `timestamp` field of the new payload
3442    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
3443    pub timestamp: u64,
3444    /// Value for the `prevRandao` field of the new payload
3445    pub prev_randao: B256,
3446    /// Suggested value for the `feeRecipient` field of the new payload
3447    pub suggested_fee_recipient: Address,
3448    /// Array of [`Withdrawal`] enabled with V2
3449    /// See <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#payloadattributesv2>
3450    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3451    pub withdrawals: Option<Vec<Withdrawal>>,
3452    /// Root of the parent beacon block enabled with V3.
3453    ///
3454    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#payloadattributesv3>
3455    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3456    pub parent_beacon_block_root: Option<B256>,
3457    /// Slot of the current block enabled with Amsterdam fork.
3458    ///
3459    /// See <https://github.com/ethereum/execution-apis/pull/731>
3460    #[cfg_attr(
3461        feature = "serde",
3462        serde(
3463            default,
3464            skip_serializing_if = "Option::is_none",
3465            with = "alloy_serde::quantity::opt"
3466        )
3467    )]
3468    pub slot_number: Option<u64>,
3469    /// Gas limit of the current block enabled with Amsterdam fork.
3470    ///
3471    /// See <https://github.com/ethereum/execution-apis/pull/796>
3472    #[cfg_attr(
3473        feature = "serde",
3474        serde(
3475            default,
3476            skip_serializing_if = "Option::is_none",
3477            with = "alloy_serde::quantity::opt"
3478        )
3479    )]
3480    pub target_gas_limit: Option<u64>,
3481}
3482
3483impl PayloadAttributes {
3484    /// Sets the timestamp for the payload attributes.
3485    pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
3486        self.timestamp = timestamp;
3487        self
3488    }
3489
3490    /// Sets the withdrawals for the payload attributes.
3491    pub fn with_withdrawals(mut self, withdrawals: Vec<Withdrawal>) -> Self {
3492        self.withdrawals = Some(withdrawals);
3493        self
3494    }
3495
3496    /// Sets the parent beacon block root for the payload attributes.
3497    pub const fn with_parent_beacon_block_root(mut self, parent_beacon_block_root: B256) -> Self {
3498        self.parent_beacon_block_root = Some(parent_beacon_block_root);
3499        self
3500    }
3501
3502    /// Sets the slot number for the payload attributes.
3503    pub const fn with_slot_number(mut self, slot_number: u64) -> Self {
3504        self.slot_number = Some(slot_number);
3505        self
3506    }
3507}
3508
3509#[cfg(feature = "ssz")]
3510impl PayloadAttributes {
3511    fn ssz_v1_fixed_len() -> usize {
3512        <u64 as ssz::Encode>::ssz_fixed_len()
3513            + <B256 as ssz::Encode>::ssz_fixed_len()
3514            + <Address as ssz::Encode>::ssz_fixed_len()
3515    }
3516
3517    fn ssz_v2_fixed_len() -> usize {
3518        Self::ssz_v1_fixed_len() + <Vec<Withdrawal> as ssz::Encode>::ssz_fixed_len()
3519    }
3520
3521    fn ssz_v3_fixed_len() -> usize {
3522        Self::ssz_v2_fixed_len() + <B256 as ssz::Encode>::ssz_fixed_len()
3523    }
3524
3525    fn ssz_v4_slot_fixed_len() -> usize {
3526        Self::ssz_v3_fixed_len() + <u64 as ssz::Encode>::ssz_fixed_len()
3527    }
3528
3529    fn ssz_v4_target_fixed_len() -> usize {
3530        Self::ssz_v4_slot_fixed_len() + <u64 as ssz::Encode>::ssz_fixed_len()
3531    }
3532
3533    fn ssz_fixed_section_len(&self) -> usize {
3534        if self.target_gas_limit.is_some() {
3535            Self::ssz_v4_target_fixed_len()
3536        } else if self.slot_number.is_some() {
3537            Self::ssz_v4_slot_fixed_len()
3538        } else if self.parent_beacon_block_root.is_some() {
3539            Self::ssz_v3_fixed_len()
3540        } else if self.withdrawals.is_some() {
3541            Self::ssz_v2_fixed_len()
3542        } else {
3543            Self::ssz_v1_fixed_len()
3544        }
3545    }
3546}
3547
3548#[cfg(feature = "ssz")]
3549impl ssz::Encode for PayloadAttributes {
3550    fn is_ssz_fixed_len() -> bool {
3551        false
3552    }
3553
3554    fn ssz_append(&self, buf: &mut Vec<u8>) {
3555        let fixed_section_len = self.ssz_fixed_section_len();
3556        let mut encoder = ssz::SszEncoder::container(buf, fixed_section_len);
3557
3558        encoder.append(&self.timestamp);
3559        encoder.append(&self.prev_randao);
3560        encoder.append(&self.suggested_fee_recipient);
3561
3562        if fixed_section_len >= Self::ssz_v2_fixed_len() {
3563            let empty_withdrawals = Vec::new();
3564            let withdrawals = self.withdrawals.as_ref().unwrap_or(&empty_withdrawals);
3565            encoder.append(withdrawals);
3566        }
3567
3568        if fixed_section_len >= Self::ssz_v3_fixed_len() {
3569            encoder.append(&self.parent_beacon_block_root.unwrap_or_default());
3570        }
3571
3572        if fixed_section_len >= Self::ssz_v4_slot_fixed_len() {
3573            encoder.append(&self.slot_number.unwrap_or_default());
3574        }
3575
3576        if fixed_section_len == Self::ssz_v4_target_fixed_len() {
3577            encoder.append(&self.target_gas_limit.unwrap_or_default());
3578        }
3579
3580        encoder.finalize();
3581    }
3582
3583    fn ssz_bytes_len(&self) -> usize {
3584        let fixed_section_len = self.ssz_fixed_section_len();
3585        let withdrawals_len = if fixed_section_len >= Self::ssz_v2_fixed_len() {
3586            self.withdrawals.as_ref().map(ssz::Encode::ssz_bytes_len).unwrap_or_default()
3587        } else {
3588            0
3589        };
3590
3591        fixed_section_len + withdrawals_len
3592    }
3593}
3594
3595#[cfg(feature = "ssz")]
3596impl ssz::Decode for PayloadAttributes {
3597    fn is_ssz_fixed_len() -> bool {
3598        false
3599    }
3600
3601    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
3602        if bytes.len() == Self::ssz_v1_fixed_len() {
3603            let mut builder = ssz::SszDecoderBuilder::new(bytes);
3604
3605            builder.register_type::<u64>()?;
3606            builder.register_type::<B256>()?;
3607            builder.register_type::<Address>()?;
3608
3609            let mut decoder = builder.build()?;
3610
3611            return Ok(Self {
3612                timestamp: decoder.decode_next()?,
3613                prev_randao: decoder.decode_next()?,
3614                suggested_fee_recipient: decoder.decode_next()?,
3615                withdrawals: None,
3616                parent_beacon_block_root: None,
3617                slot_number: None,
3618                target_gas_limit: None,
3619            });
3620        }
3621
3622        if bytes.len() < Self::ssz_v2_fixed_len() {
3623            return Err(ssz::DecodeError::InvalidByteLength {
3624                len: bytes.len(),
3625                expected: Self::ssz_v2_fixed_len(),
3626            });
3627        }
3628
3629        let offset = u32::from_le_bytes([
3630            bytes[Self::ssz_v1_fixed_len()],
3631            bytes[Self::ssz_v1_fixed_len() + 1],
3632            bytes[Self::ssz_v1_fixed_len() + 2],
3633            bytes[Self::ssz_v1_fixed_len() + 3],
3634        ]) as usize;
3635
3636        let mut builder = ssz::SszDecoderBuilder::new(bytes);
3637
3638        builder.register_type::<u64>()?;
3639        builder.register_type::<B256>()?;
3640        builder.register_type::<Address>()?;
3641        builder.register_type::<Vec<Withdrawal>>()?;
3642
3643        match offset {
3644            offset if offset == Self::ssz_v2_fixed_len() => {
3645                let mut decoder = builder.build()?;
3646
3647                Ok(Self {
3648                    timestamp: decoder.decode_next()?,
3649                    prev_randao: decoder.decode_next()?,
3650                    suggested_fee_recipient: decoder.decode_next()?,
3651                    withdrawals: Some(decoder.decode_next()?),
3652                    parent_beacon_block_root: None,
3653                    slot_number: None,
3654                    target_gas_limit: None,
3655                })
3656            }
3657            offset if offset == Self::ssz_v3_fixed_len() => {
3658                builder.register_type::<B256>()?;
3659                let mut decoder = builder.build()?;
3660
3661                Ok(Self {
3662                    timestamp: decoder.decode_next()?,
3663                    prev_randao: decoder.decode_next()?,
3664                    suggested_fee_recipient: decoder.decode_next()?,
3665                    withdrawals: Some(decoder.decode_next()?),
3666                    parent_beacon_block_root: Some(decoder.decode_next()?),
3667                    slot_number: None,
3668                    target_gas_limit: None,
3669                })
3670            }
3671            offset if offset == Self::ssz_v4_slot_fixed_len() => {
3672                builder.register_type::<B256>()?;
3673                builder.register_type::<u64>()?;
3674                let mut decoder = builder.build()?;
3675
3676                Ok(Self {
3677                    timestamp: decoder.decode_next()?,
3678                    prev_randao: decoder.decode_next()?,
3679                    suggested_fee_recipient: decoder.decode_next()?,
3680                    withdrawals: Some(decoder.decode_next()?),
3681                    parent_beacon_block_root: Some(decoder.decode_next()?),
3682                    slot_number: Some(decoder.decode_next()?),
3683                    target_gas_limit: None,
3684                })
3685            }
3686            offset if offset == Self::ssz_v4_target_fixed_len() => {
3687                builder.register_type::<B256>()?;
3688                builder.register_type::<u64>()?;
3689                builder.register_type::<u64>()?;
3690                let mut decoder = builder.build()?;
3691
3692                Ok(Self {
3693                    timestamp: decoder.decode_next()?,
3694                    prev_randao: decoder.decode_next()?,
3695                    suggested_fee_recipient: decoder.decode_next()?,
3696                    withdrawals: Some(decoder.decode_next()?),
3697                    parent_beacon_block_root: Some(decoder.decode_next()?),
3698                    slot_number: Some(decoder.decode_next()?),
3699                    target_gas_limit: Some(decoder.decode_next()?),
3700                })
3701            }
3702            offset => Err(ssz::DecodeError::BytesInvalid(format!(
3703                "invalid PayloadAttributes SSZ fixed section offset: {offset}"
3704            ))),
3705        }
3706    }
3707}
3708
3709/// This structure contains the result of processing a payload or fork choice update.
3710#[derive(Clone, Debug, PartialEq, Eq)]
3711#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
3712#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3713#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3714pub struct PayloadStatus {
3715    /// The status of the payload.
3716    #[cfg_attr(feature = "serde", serde(flatten))]
3717    pub status: PayloadStatusEnum,
3718    /// Hash of the most recent valid block in the branch defined by payload and its ancestors
3719    pub latest_valid_hash: Option<B256>,
3720}
3721
3722impl PayloadStatus {
3723    /// Initializes a new payload status.
3724    pub const fn new(status: PayloadStatusEnum, latest_valid_hash: Option<B256>) -> Self {
3725        Self { status, latest_valid_hash }
3726    }
3727
3728    /// Creates a new payload status from the given status.
3729    pub const fn from_status(status: PayloadStatusEnum) -> Self {
3730        Self { status, latest_valid_hash: None }
3731    }
3732
3733    /// Sets the latest valid hash.
3734    pub const fn with_latest_valid_hash(mut self, latest_valid_hash: B256) -> Self {
3735        self.latest_valid_hash = Some(latest_valid_hash);
3736        self
3737    }
3738
3739    /// Sets the latest valid hash if it's not None.
3740    pub const fn maybe_latest_valid_hash(mut self, latest_valid_hash: Option<B256>) -> Self {
3741        self.latest_valid_hash = latest_valid_hash;
3742        self
3743    }
3744
3745    /// Returns true if the payload status is syncing.
3746    pub const fn is_syncing(&self) -> bool {
3747        self.status.is_syncing()
3748    }
3749
3750    /// Returns true if the payload status is valid.
3751    pub const fn is_valid(&self) -> bool {
3752        self.status.is_valid()
3753    }
3754
3755    /// Returns true if the payload status is invalid.
3756    pub const fn is_invalid(&self) -> bool {
3757        self.status.is_invalid()
3758    }
3759}
3760
3761#[cfg(feature = "ssz")]
3762impl ssz::Encode for PayloadStatus {
3763    fn is_ssz_fixed_len() -> bool {
3764        false
3765    }
3766
3767    fn ssz_append(&self, buf: &mut Vec<u8>) {
3768        let validation_error =
3769            self.status.validation_error().map(str::as_bytes).unwrap_or_default().to_vec();
3770        let latest_valid_hash = self.latest_valid_hash.unwrap_or_default();
3771        let offset = <u8 as ssz::Encode>::ssz_fixed_len()
3772            + <B256 as ssz::Encode>::ssz_fixed_len()
3773            + <Vec<u8> as ssz::Encode>::ssz_fixed_len();
3774        let mut encoder = ssz::SszEncoder::container(buf, offset);
3775
3776        encoder.append(&self.status.ssz_code());
3777        encoder.append(&latest_valid_hash);
3778        encoder.append(&validation_error);
3779
3780        encoder.finalize();
3781    }
3782
3783    fn ssz_bytes_len(&self) -> usize {
3784        let validation_error_len = self.status.validation_error().map(str::len).unwrap_or_default();
3785        <u8 as ssz::Encode>::ssz_fixed_len()
3786            + <B256 as ssz::Encode>::ssz_fixed_len()
3787            + <Vec<u8> as ssz::Encode>::ssz_fixed_len()
3788            + validation_error_len
3789    }
3790}
3791
3792#[cfg(feature = "ssz")]
3793impl ssz::Decode for PayloadStatus {
3794    fn is_ssz_fixed_len() -> bool {
3795        false
3796    }
3797
3798    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
3799        let mut builder = ssz::SszDecoderBuilder::new(bytes);
3800
3801        builder.register_type::<u8>()?;
3802        builder.register_type::<B256>()?;
3803        builder.register_type::<Vec<u8>>()?;
3804
3805        let mut decoder = builder.build()?;
3806        let status_code: u8 = decoder.decode_next()?;
3807        let latest_valid_hash: B256 = decoder.decode_next()?;
3808        let validation_error: Vec<u8> = decoder.decode_next()?;
3809
3810        let status = PayloadStatusEnum::from_ssz_code(status_code, validation_error)?;
3811        let latest_valid_hash = (!latest_valid_hash.is_zero()).then_some(latest_valid_hash);
3812
3813        Ok(Self { status, latest_valid_hash })
3814    }
3815}
3816
3817impl core::fmt::Display for PayloadStatus {
3818    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3819        write!(
3820            f,
3821            "PayloadStatus {{ status: {}, latestValidHash: {:?} }}",
3822            self.status, self.latest_valid_hash
3823        )
3824    }
3825}
3826
3827#[cfg(feature = "serde")]
3828impl serde::Serialize for PayloadStatus {
3829    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3830    where
3831        S: serde::Serializer,
3832    {
3833        use serde::ser::SerializeMap;
3834        let mut map = serializer.serialize_map(Some(3))?;
3835        map.serialize_entry("status", self.status.as_str())?;
3836        map.serialize_entry("latestValidHash", &self.latest_valid_hash)?;
3837        map.serialize_entry("validationError", &self.status.validation_error())?;
3838        map.end()
3839    }
3840}
3841
3842impl From<PayloadError> for PayloadStatusEnum {
3843    fn from(error: PayloadError) -> Self {
3844        Self::Invalid { validation_error: error.to_string() }
3845    }
3846}
3847
3848/// Represents the status response of a payload.
3849#[derive(Clone, Debug, PartialEq, Eq)]
3850#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3851#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "SCREAMING_SNAKE_CASE"))]
3852#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3853pub enum PayloadStatusEnum {
3854    /// VALID is returned by the engine API in the following calls:
3855    ///   - newPayload:       if the payload was already known or was just validated and executed
3856    ///   - forkchoiceUpdate: if the chain accepted the reorg (might ignore if it's stale)
3857    Valid,
3858
3859    /// INVALID is returned by the engine API in the following calls:
3860    ///   - newPayload:       if the payload failed to execute on top of the local chain
3861    ///   - forkchoiceUpdate: if the new head is unknown, pre-merge, or reorg to it fails
3862    Invalid {
3863        /// The error message for the invalid payload.
3864        #[cfg_attr(feature = "serde", serde(rename = "validationError"))]
3865        validation_error: String,
3866    },
3867
3868    /// SYNCING is returned by the engine API in the following calls:
3869    ///   - newPayload:       if the payload was accepted on top of an active sync
3870    ///   - forkchoiceUpdate: if the new head was seen before, but not part of the chain
3871    Syncing,
3872
3873    /// ACCEPTED is returned by the engine API in the following calls:
3874    ///   - newPayload: if the payload was accepted, but not processed (side chain)
3875    Accepted,
3876}
3877
3878impl PayloadStatusEnum {
3879    /// Returns the string representation of the payload status.
3880    pub const fn as_str(&self) -> &'static str {
3881        match self {
3882            Self::Valid => "VALID",
3883            Self::Invalid { .. } => "INVALID",
3884            Self::Syncing => "SYNCING",
3885            Self::Accepted => "ACCEPTED",
3886        }
3887    }
3888
3889    /// Returns the validation error if the payload status is invalid.
3890    pub fn validation_error(&self) -> Option<&str> {
3891        match self {
3892            Self::Invalid { validation_error } => Some(validation_error),
3893            _ => None,
3894        }
3895    }
3896
3897    /// Returns true if the payload status is syncing.
3898    pub const fn is_syncing(&self) -> bool {
3899        matches!(self, Self::Syncing)
3900    }
3901
3902    /// Returns true if the payload status is valid.
3903    pub const fn is_valid(&self) -> bool {
3904        matches!(self, Self::Valid)
3905    }
3906
3907    /// Returns true if the payload status is invalid.
3908    pub const fn is_invalid(&self) -> bool {
3909        matches!(self, Self::Invalid { .. })
3910    }
3911
3912    #[cfg(feature = "ssz")]
3913    const fn ssz_code(&self) -> u8 {
3914        match self {
3915            Self::Valid => 0,
3916            Self::Invalid { .. } => 1,
3917            Self::Syncing => 2,
3918            Self::Accepted => 3,
3919        }
3920    }
3921
3922    #[cfg(feature = "ssz")]
3923    fn from_ssz_code(status_code: u8, validation_error: Vec<u8>) -> Result<Self, ssz::DecodeError> {
3924        match status_code {
3925            0 => {
3926                if !validation_error.is_empty() {
3927                    return Err(ssz::DecodeError::BytesInvalid(
3928                        "unexpected validation error for VALID status".to_string(),
3929                    ));
3930                }
3931                Ok(Self::Valid)
3932            }
3933            1 => String::from_utf8(validation_error)
3934                .map(|validation_error| Self::Invalid { validation_error })
3935                .map_err(|err| ssz::DecodeError::BytesInvalid(err.to_string())),
3936            2 => {
3937                if !validation_error.is_empty() {
3938                    return Err(ssz::DecodeError::BytesInvalid(
3939                        "unexpected validation error for SYNCING status".to_string(),
3940                    ));
3941                }
3942                Ok(Self::Syncing)
3943            }
3944            3 => {
3945                if !validation_error.is_empty() {
3946                    return Err(ssz::DecodeError::BytesInvalid(
3947                        "unexpected validation error for ACCEPTED status".to_string(),
3948                    ));
3949                }
3950                Ok(Self::Accepted)
3951            }
3952            _ => Err(ssz::DecodeError::BytesInvalid("unknown payload status code".to_string())),
3953        }
3954    }
3955}
3956
3957impl core::fmt::Display for PayloadStatusEnum {
3958    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3959        match self {
3960            Self::Invalid { validation_error } => {
3961                f.write_str(self.as_str())?;
3962                f.write_str(": ")?;
3963                f.write_str(validation_error.as_str())
3964            }
3965            _ => f.write_str(self.as_str()),
3966        }
3967    }
3968}
3969
3970/// This structure contains the result of processing a payload in the Bogota Engine API.
3971///
3972/// It extends [`PayloadStatus`] with the EIP-7805 inclusion-list validation result.
3973///
3974/// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#payloadstatusv2>
3975#[derive(Clone, Debug, PartialEq, Eq)]
3976#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
3977#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
3978#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
3979pub struct PayloadStatusV2 {
3980    /// The common payload status fields.
3981    #[cfg_attr(feature = "serde", serde(flatten))]
3982    pub payload_inner: PayloadStatus,
3983    /// Whether the payload satisfied the inclusion-list constraints if it was deemed valid.
3984    #[cfg_attr(feature = "serde", serde(default))]
3985    pub inclusion_list_satisfied: Option<bool>,
3986}
3987
3988impl PayloadStatusV2 {
3989    /// Creates a new payload status.
3990    pub const fn new(
3991        payload_status: PayloadStatus,
3992        inclusion_list_satisfied: Option<bool>,
3993    ) -> Self {
3994        Self { payload_inner: payload_status, inclusion_list_satisfied }
3995    }
3996
3997    /// Sets whether the payload satisfied the inclusion-list constraints.
3998    pub const fn with_inclusion_list_satisfied(mut self, satisfied: bool) -> Self {
3999        self.inclusion_list_satisfied = Some(satisfied);
4000        self
4001    }
4002
4003    /// Returns true if the payload status is syncing.
4004    pub const fn is_syncing(&self) -> bool {
4005        self.payload_inner.is_syncing()
4006    }
4007
4008    /// Returns true if the payload status is valid.
4009    pub const fn is_valid(&self) -> bool {
4010        self.payload_inner.is_valid()
4011    }
4012
4013    /// Returns true if the payload status is invalid.
4014    pub const fn is_invalid(&self) -> bool {
4015        self.payload_inner.is_invalid()
4016    }
4017}
4018
4019impl From<PayloadStatus> for PayloadStatusV2 {
4020    fn from(payload_status: PayloadStatus) -> Self {
4021        Self::new(payload_status, None)
4022    }
4023}
4024
4025/// Downgrades a V2 payload status, discarding its inclusion-list validation result.
4026impl From<PayloadStatusV2> for PayloadStatus {
4027    fn from(payload_status: PayloadStatusV2) -> Self {
4028        payload_status.payload_inner
4029    }
4030}
4031
4032#[cfg(feature = "serde")]
4033impl serde::Serialize for PayloadStatusV2 {
4034    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4035    where
4036        S: serde::Serializer,
4037    {
4038        use serde::ser::SerializeMap;
4039
4040        let mut map = serializer.serialize_map(Some(4))?;
4041        map.serialize_entry("status", self.payload_inner.status.as_str())?;
4042        map.serialize_entry("latestValidHash", &self.payload_inner.latest_valid_hash)?;
4043        map.serialize_entry("validationError", &self.payload_inner.status.validation_error())?;
4044        map.serialize_entry("inclusionListSatisfied", &self.inclusion_list_satisfied)?;
4045        map.end()
4046    }
4047}
4048
4049/// Struct aggregating [`ExecutionPayload`] and [`ExecutionPayloadSidecar`] and encapsulating
4050/// complete payload supplied for execution.
4051#[derive(Debug, Clone)]
4052#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
4053#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
4054pub struct ExecutionData {
4055    /// Execution payload.
4056    pub payload: ExecutionPayload,
4057    /// Additional fork-specific fields.
4058    pub sidecar: ExecutionPayloadSidecar,
4059}
4060
4061impl ExecutionData {
4062    /// Creates new instance of [`ExecutionData`].
4063    pub const fn new(payload: ExecutionPayload, sidecar: ExecutionPayloadSidecar) -> Self {
4064        Self { payload, sidecar }
4065    }
4066
4067    /// Conversion from [`alloy_consensus::Block`]. Also returns the [`ExecutionPayloadSidecar`]
4068    /// extracted from the block.
4069    ///
4070    /// For the [`ExecutionPayloadSidecar`] this is expected to use just the requests hash, because
4071    /// the [`Requests`] are not part of the block/header. See also
4072    /// [`RequestsOrHash`](alloy_eips::eip7685::RequestsOrHash).
4073    /// Likewise, Amsterdam/V4 payload conversion falls back to the header's
4074    /// `block_access_list_hash` bytes when the full RLP-encoded block access list is not
4075    /// available on the block value, or to the canonical empty BAL hash bytes if the header does
4076    /// not carry a BAL hash.
4077    ///
4078    /// See also [`ExecutionPayload::from_block_unchecked`].
4079    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
4080    where
4081        T: Encodable2718 + Transaction,
4082        H: BlockHeader,
4083    {
4084        let (payload, sidecar) = ExecutionPayload::from_block_unchecked(block_hash, block);
4085        Self::new(payload, sidecar)
4086    }
4087
4088    /// Returns the parent hash of the block.
4089    pub const fn parent_hash(&self) -> B256 {
4090        self.payload.parent_hash()
4091    }
4092
4093    /// Returns the hash of the block.
4094    pub const fn block_hash(&self) -> B256 {
4095        self.payload.block_hash()
4096    }
4097
4098    /// Returns the number of the block.
4099    pub const fn block_number(&self) -> u64 {
4100        self.payload.block_number()
4101    }
4102
4103    /// Returns the parent beacon block root, if any.
4104    pub fn parent_beacon_block_root(&self) -> Option<B256> {
4105        self.sidecar.parent_beacon_block_root()
4106    }
4107
4108    /// Return the withdrawals for the payload or attributes.
4109    pub const fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
4110        self.payload.withdrawals()
4111    }
4112
4113    /// Returns the number of transactions in the payload.
4114    pub const fn transaction_count(&self) -> usize {
4115        self.payload.transactions().len()
4116    }
4117
4118    /// Tries to create a new unsealed block from the given payload and payload sidecar.
4119    ///
4120    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
4121    /// The payload's advertised `block_hash` is not recomputed or compared.
4122    ///
4123    /// # Note
4124    ///
4125    /// The log bloom is assumed to be validated during serialization.
4126    ///
4127    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
4128    pub fn try_into_block<T: Decodable2718>(
4129        self,
4130    ) -> Result<alloy_consensus::Block<T>, PayloadError> {
4131        self.try_into_block_with(|tx| {
4132            T::decode_2718_exact(tx.as_ref())
4133                .map_err(alloy_rlp::Error::from)
4134                .map_err(PayloadError::from)
4135        })
4136    }
4137
4138    /// Tries to create a new unsealed block from the given payload and payload sidecar with a
4139    /// custom transaction mapper.
4140    ///
4141    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
4142    ///
4143    /// # Note
4144    ///
4145    /// The log bloom is assumed to be validated during serialization.
4146    ///
4147    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
4148    pub fn try_into_block_with<T, F, E>(
4149        self,
4150        f: F,
4151    ) -> Result<alloy_consensus::Block<T>, PayloadError>
4152    where
4153        F: FnMut(Bytes) -> Result<T, E>,
4154        E: Into<PayloadError>,
4155    {
4156        self.payload.try_into_block_with_sidecar_with(&self.sidecar, f)
4157    }
4158
4159    /// Converts [`ExecutionData`] to [`Block`] with raw [`Bytes`] transactions.
4160    ///
4161    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
4162    /// without any conversion.
4163    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
4164        let mut base_block = self.payload.into_block_raw()?;
4165        base_block.header.parent_beacon_block_root = self.sidecar.parent_beacon_block_root();
4166        base_block.header.requests_hash = self.sidecar.requests_hash();
4167        Ok(base_block)
4168    }
4169}
4170
4171impl<T, H> From<Sealed<Block<T, H>>> for ExecutionData
4172where
4173    T: Encodable2718 + Transaction,
4174    H: BlockHeader,
4175{
4176    fn from(sealed: Sealed<Block<T, H>>) -> Self {
4177        let (block, block_hash) = sealed.into_parts();
4178        Self::from_block_unchecked(block_hash, &block)
4179    }
4180}
4181
4182impl<T, H> From<Sealed<&Block<T, H>>> for ExecutionData
4183where
4184    T: Encodable2718 + Transaction,
4185    H: BlockHeader,
4186{
4187    fn from(sealed: Sealed<&Block<T, H>>) -> Self {
4188        let (block, block_hash) = sealed.into_parts();
4189        Self::from_block_unchecked(block_hash, block)
4190    }
4191}
4192
4193impl<T, H> From<(Sealed<Block<T, H>>, PayloadExtras)> for ExecutionData
4194where
4195    T: Encodable2718 + Transaction,
4196    H: BlockHeader,
4197{
4198    fn from((sealed, extras): (Sealed<Block<T, H>>, PayloadExtras)) -> Self {
4199        let (block, block_hash) = sealed.into_parts();
4200        let (payload, sidecar) =
4201            ExecutionPayload::from_block_unchecked_with_extras(block_hash, &block, extras);
4202        Self::new(payload, sidecar)
4203    }
4204}
4205
4206impl<T, H> From<(Sealed<&Block<T, H>>, PayloadExtras)> for ExecutionData
4207where
4208    T: Encodable2718 + Transaction,
4209    H: BlockHeader,
4210{
4211    fn from((sealed, extras): (Sealed<&Block<T, H>>, PayloadExtras)) -> Self {
4212        let (block, block_hash) = sealed.into_parts();
4213        let (payload, sidecar) =
4214            ExecutionPayload::from_block_unchecked_with_extras(block_hash, block, extras);
4215        Self::new(payload, sidecar)
4216    }
4217}
4218
4219#[cfg(test)]
4220mod tests {
4221    use super::*;
4222    use crate::{CancunPayloadFields, PayloadValidationError};
4223    use alloc::vec;
4224    use alloy_consensus::TxEnvelope;
4225    use alloy_primitives::{b256, hex};
4226    use similar_asserts::assert_eq;
4227
4228    #[test]
4229    #[cfg(feature = "kzg")]
4230    fn convert_empty_bundle() {
4231        let bundle = BlobsBundleV1::default();
4232        let _sidecar = bundle.try_into_sidecar().unwrap();
4233    }
4234
4235    #[test]
4236    #[cfg(feature = "serde")]
4237    fn serde_blobsbundlev1_empty() {
4238        let blobs_bundle_v1 = BlobsBundleV1::empty();
4239
4240        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
4241        let deserialized: BlobsBundleV1 = serde_json::from_str(&serialized).unwrap();
4242        assert_eq!(deserialized, blobs_bundle_v1);
4243    }
4244
4245    #[test]
4246    #[cfg(feature = "serde")]
4247    fn serde_blobsbundlev1_not_empty_pass() {
4248        let blobs_bundle_v1 = BlobsBundleV1 {
4249            proofs: vec![Bytes48::default()],
4250            commitments: vec![Bytes48::default()],
4251            blobs: vec![Blob::default()],
4252        };
4253
4254        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
4255        // Limit the stack to catch large fixed-array temporaries during blob deserialization.
4256        let deserialized = std::thread::Builder::new()
4257            .stack_size(1024 * 1024)
4258            .spawn(move || serde_json::from_str::<BlobsBundleV1>(&serialized).unwrap())
4259            .unwrap()
4260            .join()
4261            .unwrap();
4262        assert_eq!(deserialized, blobs_bundle_v1);
4263    }
4264
4265    #[test]
4266    #[cfg(feature = "serde")]
4267    fn serde_blobsbundlev1_not_empty_fail() {
4268        let blobs_bundle_v1 = BlobsBundleV1 {
4269            proofs: vec![Bytes48::default(), Bytes48::default()],
4270            commitments: vec![Bytes48::default()],
4271            blobs: vec![Blob::default()],
4272        };
4273
4274        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
4275        let deserialized: Result<BlobsBundleV1, serde_json::Error> =
4276            serde_json::from_str(&serialized);
4277        assert!(deserialized.is_err(), "invalid length 2, expected commitments.len()");
4278    }
4279
4280    #[test]
4281    #[cfg(feature = "serde")]
4282    fn serde_blobsbundlev2_not_empty_pass() {
4283        let commitments = vec![Bytes48::default()];
4284
4285        let blobs_bundle_v2 = BlobsBundleV2 {
4286            proofs: vec![Bytes48::default(); commitments.len() * CELLS_PER_EXT_BLOB],
4287            commitments,
4288            blobs: vec![Blob::default()],
4289        };
4290
4291        let serialized = serde_json::to_string(&blobs_bundle_v2).unwrap();
4292        // Limit the stack to catch large fixed-array temporaries during blob deserialization.
4293        let deserialized = std::thread::Builder::new()
4294            .stack_size(1024 * 1024)
4295            .spawn(move || serde_json::from_str::<BlobsBundleV2>(&serialized).unwrap())
4296            .unwrap()
4297            .join()
4298            .unwrap();
4299        assert_eq!(deserialized, blobs_bundle_v2);
4300    }
4301
4302    #[test]
4303    #[cfg(feature = "serde")]
4304    fn serde_blobsbundlev2_not_empty_fail() {
4305        let blobs_bundle_v2 = BlobsBundleV2 {
4306            proofs: vec![Bytes48::default()],
4307            commitments: vec![Bytes48::default()],
4308            blobs: vec![],
4309        };
4310
4311        let serialized = serde_json::to_string(&blobs_bundle_v2).unwrap();
4312        let deserialized: Result<BlobsBundleV2, serde_json::Error> =
4313            serde_json::from_str(&serialized);
4314        assert!(deserialized.is_err());
4315    }
4316
4317    #[test]
4318    #[cfg(feature = "ssz")]
4319    #[cfg(not(debug_assertions))]
4320    fn ssz_blobsbundlev2_roundtrip() {
4321        let commitments = vec![Bytes48::default(), Bytes48::default()];
4322        let num_blobs = commitments.len();
4323
4324        let blobs_bundle_v2 = BlobsBundleV2 {
4325            commitments,
4326            proofs: vec![Bytes48::default(); num_blobs * CELLS_PER_EXT_BLOB],
4327            blobs: vec![Blob::default(); num_blobs],
4328        };
4329
4330        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4331        let decoded: BlobsBundleV2 = ssz::Decode::from_ssz_bytes(&encoded).unwrap();
4332
4333        assert_eq!(decoded, blobs_bundle_v2);
4334    }
4335
4336    #[test]
4337    #[cfg(feature = "ssz")]
4338    #[cfg(not(debug_assertions))]
4339    fn ssz_blobsbundlev2_invalid_proofs_length() {
4340        let commitments = vec![Bytes48::default()];
4341
4342        let blobs_bundle_v2 = BlobsBundleV2 {
4343            commitments,
4344            proofs: vec![Bytes48::default(); 2],
4345            blobs: vec![Blob::default()],
4346        };
4347
4348        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4349
4350        // Attempt to decode - should fail due to mismatched proofs length
4351        let result: Result<BlobsBundleV2, _> = ssz::Decode::from_ssz_bytes(&encoded);
4352        assert!(result.is_err());
4353    }
4354
4355    #[test]
4356    #[cfg(feature = "ssz")]
4357    #[cfg(not(debug_assertions))]
4358    fn ssz_blobsbundlev2_mismatched_commitments_blobs() {
4359        let blobs_bundle_v2 = BlobsBundleV2 {
4360            commitments: vec![Bytes48::default(), Bytes48::default()],
4361            proofs: vec![Bytes48::default(); CELLS_PER_EXT_BLOB],
4362            blobs: vec![Blob::default()],
4363        };
4364
4365        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4366
4367        // Attempt to decode - should fail due to wrong number of commitments
4368        let result: Result<BlobsBundleV2, _> = ssz::Decode::from_ssz_bytes(&encoded);
4369        assert!(result.is_err());
4370    }
4371
4372    #[test]
4373    #[cfg(feature = "ssz")]
4374    fn ssz_blobsbundlev2_empty() {
4375        let blobs_bundle_v2 = BlobsBundleV2 { commitments: vec![], proofs: vec![], blobs: vec![] };
4376
4377        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
4378
4379        // Decode from SSZ - empty bundle should be valid
4380        let decoded: BlobsBundleV2 = ssz::Decode::from_ssz_bytes(&encoded).unwrap();
4381        assert_eq!(decoded, blobs_bundle_v2);
4382    }
4383
4384    #[cfg(feature = "ssz")]
4385    fn ssz_payload_v1() -> ExecutionPayloadV1 {
4386        ExecutionPayloadV1 {
4387            parent_hash: B256::with_last_byte(1),
4388            fee_recipient: Address::with_last_byte(2),
4389            state_root: B256::with_last_byte(3),
4390            receipts_root: B256::with_last_byte(4),
4391            logs_bloom: Bloom::default(),
4392            prev_randao: B256::with_last_byte(5),
4393            block_number: 6,
4394            gas_limit: 7,
4395            gas_used: 8,
4396            timestamp: 9,
4397            extra_data: Bytes::from(vec![10, 11]),
4398            base_fee_per_gas: U256::from(12),
4399            block_hash: B256::with_last_byte(13),
4400            transactions: vec![Bytes::from(vec![14, 15])],
4401        }
4402    }
4403
4404    #[cfg(feature = "ssz")]
4405    fn ssz_payload_v2() -> ExecutionPayloadV2 {
4406        ExecutionPayloadV2 {
4407            payload_inner: ssz_payload_v1(),
4408            withdrawals: vec![Withdrawal {
4409                index: 1,
4410                validator_index: 2,
4411                address: Address::with_last_byte(3),
4412                amount: 4,
4413            }],
4414        }
4415    }
4416
4417    #[cfg(feature = "ssz")]
4418    fn ssz_payload_v3() -> ExecutionPayloadV3 {
4419        ExecutionPayloadV3 {
4420            payload_inner: ssz_payload_v2(),
4421            blob_gas_used: 16,
4422            excess_blob_gas: 17,
4423        }
4424    }
4425
4426    #[cfg(feature = "ssz")]
4427    fn ssz_payload_v4() -> ExecutionPayloadV4 {
4428        ExecutionPayloadV4 {
4429            payload_inner: ssz_payload_v3(),
4430            block_access_list: Bytes::from(vec![18, 19]),
4431            slot_number: 20,
4432        }
4433    }
4434
4435    #[test]
4436    #[cfg(feature = "ssz")]
4437    fn ssz_execution_payload_envelope_v1_response_roundtrip() {
4438        use ssz::{Decode, Encode};
4439
4440        let payload = ssz_payload_v1();
4441        let decoded = ExecutionPayloadV1::from_ssz_bytes(&payload.as_ssz_bytes()).unwrap();
4442
4443        assert_eq!(decoded, payload);
4444    }
4445
4446    #[test]
4447    #[cfg(feature = "ssz")]
4448    fn ssz_execution_payload_envelope_v2_roundtrip() {
4449        use ssz::{Decode, Encode};
4450
4451        let envelope = ExecutionPayloadEnvelopeV2 {
4452            execution_payload: ExecutionPayloadFieldV2::V2(ssz_payload_v2()),
4453            block_value: U256::from(21),
4454        };
4455
4456        let decoded = ExecutionPayloadEnvelopeV2::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4457        assert_eq!(decoded, envelope);
4458
4459        let envelope = ExecutionPayloadEnvelopeV2 {
4460            execution_payload: ExecutionPayloadFieldV2::V1(ssz_payload_v1()),
4461            block_value: U256::from(22),
4462        };
4463
4464        let decoded = ExecutionPayloadEnvelopeV2::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4465        assert_eq!(decoded, envelope);
4466    }
4467
4468    #[test]
4469    #[cfg(feature = "ssz")]
4470    fn ssz_execution_payload_envelope_v3_roundtrip() {
4471        use ssz::{Decode, Encode};
4472
4473        let envelope = ExecutionPayloadEnvelopeV3 {
4474            execution_payload: ssz_payload_v3(),
4475            block_value: U256::from(23),
4476            blobs_bundle: BlobsBundleV1::empty(),
4477            should_override_builder: true,
4478        };
4479
4480        let decoded = ExecutionPayloadEnvelopeV3::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4481        assert_eq!(decoded, envelope);
4482    }
4483
4484    #[test]
4485    #[cfg(feature = "ssz")]
4486    fn ssz_execution_payload_envelope_v4_roundtrip() {
4487        use ssz::{Decode, Encode};
4488
4489        let envelope = ExecutionPayloadEnvelopeV4 {
4490            envelope_inner: ExecutionPayloadEnvelopeV3 {
4491                execution_payload: ssz_payload_v3(),
4492                block_value: U256::from(24),
4493                blobs_bundle: BlobsBundleV1::empty(),
4494                should_override_builder: false,
4495            },
4496            execution_requests: Requests::from_requests([Bytes::from(vec![1, 2, 3])]),
4497        };
4498
4499        let decoded = ExecutionPayloadEnvelopeV4::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4500        assert_eq!(decoded, envelope);
4501    }
4502
4503    #[test]
4504    #[cfg(feature = "ssz")]
4505    fn ssz_execution_payload_envelope_v5_roundtrip() {
4506        use ssz::{Decode, Encode};
4507
4508        let envelope = ExecutionPayloadEnvelopeV5 {
4509            execution_payload: ssz_payload_v3(),
4510            block_value: U256::from(25),
4511            blobs_bundle: BlobsBundleV2::empty(),
4512            should_override_builder: true,
4513            execution_requests: Requests::from_requests([Bytes::from(vec![4, 5, 6])]),
4514        };
4515
4516        let decoded = ExecutionPayloadEnvelopeV5::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4517        assert_eq!(decoded, envelope);
4518    }
4519
4520    #[test]
4521    #[cfg(feature = "ssz")]
4522    fn ssz_execution_payload_envelope_v6_roundtrip() {
4523        use ssz::{Decode, Encode};
4524
4525        let envelope = ExecutionPayloadEnvelopeV6 {
4526            execution_payload: ssz_payload_v4(),
4527            block_value: U256::from(26),
4528            blobs_bundle: BlobsBundleV2::empty(),
4529            should_override_builder: false,
4530            execution_requests: Requests::from_requests([Bytes::from(vec![7, 8, 9])]),
4531        };
4532
4533        let decoded = ExecutionPayloadEnvelopeV6::from_ssz_bytes(&envelope.as_ssz_bytes()).unwrap();
4534        assert_eq!(decoded, envelope);
4535    }
4536
4537    #[test]
4538    #[cfg(feature = "serde")]
4539    fn serde_payload_status() {
4540        let s = r#"{"status":"SYNCING","latestValidHash":null,"validationError":null}"#;
4541        let status: PayloadStatus = serde_json::from_str(s).unwrap();
4542        assert_eq!(status.status, PayloadStatusEnum::Syncing);
4543        assert!(status.latest_valid_hash.is_none());
4544        assert!(status.status.validation_error().is_none());
4545        assert_eq!(serde_json::to_string(&status).unwrap(), s);
4546
4547        let full = s;
4548        let s = r#"{"status":"SYNCING","latestValidHash":null}"#;
4549        let status: PayloadStatus = serde_json::from_str(s).unwrap();
4550        assert_eq!(status.status, PayloadStatusEnum::Syncing);
4551        assert!(status.latest_valid_hash.is_none());
4552        assert!(status.status.validation_error().is_none());
4553        assert_eq!(serde_json::to_string(&status).unwrap(), full);
4554    }
4555
4556    #[test]
4557    #[cfg(feature = "serde")]
4558    fn serde_payload_status_v2() {
4559        let json = r#"{"status":"VALID","latestValidHash":null,"validationError":null,"inclusionListSatisfied":true}"#;
4560        let status: PayloadStatusV2 = serde_json::from_str(json).unwrap();
4561        assert!(status.is_valid());
4562        assert!(status.payload_inner.latest_valid_hash.is_none());
4563        assert_eq!(status.inclusion_list_satisfied, Some(true));
4564        assert_eq!(serde_json::to_string(&status).unwrap(), json);
4565
4566        let json = r#"{"status":"SYNCING","latestValidHash":null,"validationError":null,"inclusionListSatisfied":null}"#;
4567        let status: PayloadStatusV2 = serde_json::from_str(json).unwrap();
4568        assert!(status.is_syncing());
4569        assert_eq!(status.inclusion_list_satisfied, None);
4570        assert_eq!(serde_json::to_string(&status).unwrap(), json);
4571    }
4572
4573    #[test]
4574    fn payload_status_v2_conversions() {
4575        let v1 = PayloadStatus::from_status(PayloadStatusEnum::Valid)
4576            .with_latest_valid_hash(B256::with_last_byte(1));
4577
4578        let v2: PayloadStatusV2 = v1.clone().into();
4579        assert_eq!(v2.payload_inner, v1);
4580        assert_eq!(v2.inclusion_list_satisfied, None);
4581
4582        let downgraded: PayloadStatus = v2.with_inclusion_list_satisfied(true).into();
4583        assert_eq!(downgraded, v1);
4584    }
4585
4586    #[test]
4587    fn payload_attributes_builder_setters() {
4588        let withdrawal = Withdrawal {
4589            index: 1,
4590            validator_index: 2,
4591            address: Address::with_last_byte(3),
4592            amount: 4,
4593        };
4594        let parent_beacon_block_root = B256::with_last_byte(5);
4595
4596        let attributes = PayloadAttributes::default()
4597            .with_timestamp(10)
4598            .with_withdrawals(vec![withdrawal])
4599            .with_parent_beacon_block_root(parent_beacon_block_root)
4600            .with_slot_number(6);
4601
4602        assert_eq!(attributes.timestamp, 10);
4603        assert_eq!(attributes.withdrawals, Some(vec![withdrawal]));
4604        assert_eq!(attributes.parent_beacon_block_root, Some(parent_beacon_block_root));
4605        assert_eq!(attributes.slot_number, Some(6));
4606    }
4607
4608    #[test]
4609    #[cfg(feature = "ssz")]
4610    fn ssz_payload_attributes_roundtrip_all_versions() {
4611        use ssz::{Decode, Encode};
4612
4613        let withdrawal = Withdrawal {
4614            index: 1,
4615            validator_index: 2,
4616            address: Address::with_last_byte(3),
4617            amount: 4,
4618        };
4619
4620        let v1 = PayloadAttributes {
4621            timestamp: 10,
4622            prev_randao: B256::with_last_byte(11),
4623            suggested_fee_recipient: Address::with_last_byte(12),
4624            withdrawals: None,
4625            parent_beacon_block_root: None,
4626            slot_number: None,
4627            target_gas_limit: None,
4628        };
4629        let decoded_v1 = PayloadAttributes::from_ssz_bytes(&v1.as_ssz_bytes()).unwrap();
4630        assert_eq!(decoded_v1, v1);
4631
4632        let v2 = PayloadAttributes {
4633            timestamp: 20,
4634            prev_randao: B256::with_last_byte(21),
4635            suggested_fee_recipient: Address::with_last_byte(22),
4636            withdrawals: Some(vec![withdrawal]),
4637            parent_beacon_block_root: None,
4638            slot_number: None,
4639            target_gas_limit: None,
4640        };
4641        let decoded_v2 = PayloadAttributes::from_ssz_bytes(&v2.as_ssz_bytes()).unwrap();
4642        assert_eq!(decoded_v2, v2);
4643
4644        let v3 = PayloadAttributes {
4645            timestamp: 30,
4646            prev_randao: B256::with_last_byte(31),
4647            suggested_fee_recipient: Address::with_last_byte(32),
4648            withdrawals: Some(vec![withdrawal]),
4649            parent_beacon_block_root: Some(B256::with_last_byte(33)),
4650            slot_number: None,
4651            target_gas_limit: None,
4652        };
4653        let decoded_v3 = PayloadAttributes::from_ssz_bytes(&v3.as_ssz_bytes()).unwrap();
4654        assert_eq!(decoded_v3, v3);
4655
4656        let v4 = PayloadAttributes {
4657            timestamp: 40,
4658            prev_randao: B256::with_last_byte(41),
4659            suggested_fee_recipient: Address::with_last_byte(42),
4660            withdrawals: Some(vec![withdrawal]),
4661            parent_beacon_block_root: Some(B256::with_last_byte(43)),
4662            slot_number: Some(44),
4663            target_gas_limit: Some(45),
4664        };
4665        let decoded_v4 = PayloadAttributes::from_ssz_bytes(&v4.as_ssz_bytes()).unwrap();
4666        assert_eq!(decoded_v4, v4);
4667    }
4668
4669    #[test]
4670    #[cfg(feature = "ssz")]
4671    fn ssz_payload_attributes_match_spec_container_offsets() {
4672        use ssz::Encode;
4673
4674        let withdrawal = Withdrawal {
4675            index: 1,
4676            validator_index: 2,
4677            address: Address::with_last_byte(3),
4678            amount: 4,
4679        };
4680
4681        let v1 = PayloadAttributes {
4682            timestamp: 10,
4683            prev_randao: B256::with_last_byte(11),
4684            suggested_fee_recipient: Address::with_last_byte(12),
4685            withdrawals: None,
4686            parent_beacon_block_root: None,
4687            slot_number: None,
4688            target_gas_limit: None,
4689        };
4690        assert_eq!(v1.as_ssz_bytes().len(), 60);
4691
4692        let v2 = PayloadAttributes {
4693            timestamp: 20,
4694            prev_randao: B256::with_last_byte(21),
4695            suggested_fee_recipient: Address::with_last_byte(22),
4696            withdrawals: Some(vec![withdrawal]),
4697            parent_beacon_block_root: None,
4698            slot_number: None,
4699            target_gas_limit: None,
4700        };
4701        let bytes = v2.as_ssz_bytes();
4702        assert_eq!(u32::from_le_bytes(bytes[60..64].try_into().unwrap()), 64);
4703
4704        let v3 = PayloadAttributes {
4705            timestamp: 30,
4706            prev_randao: B256::with_last_byte(31),
4707            suggested_fee_recipient: Address::with_last_byte(32),
4708            withdrawals: Some(vec![withdrawal]),
4709            parent_beacon_block_root: Some(B256::with_last_byte(33)),
4710            slot_number: None,
4711            target_gas_limit: None,
4712        };
4713        let bytes = v3.as_ssz_bytes();
4714        assert_eq!(u32::from_le_bytes(bytes[60..64].try_into().unwrap()), 96);
4715
4716        let v4 = PayloadAttributes {
4717            timestamp: 40,
4718            prev_randao: B256::with_last_byte(41),
4719            suggested_fee_recipient: Address::with_last_byte(42),
4720            withdrawals: Some(vec![withdrawal]),
4721            parent_beacon_block_root: Some(B256::with_last_byte(43)),
4722            slot_number: Some(44),
4723            target_gas_limit: Some(45),
4724        };
4725        let bytes = v4.as_ssz_bytes();
4726        assert_eq!(u32::from_le_bytes(bytes[60..64].try_into().unwrap()), 112);
4727    }
4728
4729    #[test]
4730    #[cfg(feature = "ssz")]
4731    fn ssz_payload_status_roundtrip() {
4732        use ssz::{Decode, Encode};
4733
4734        let statuses = [
4735            PayloadStatus {
4736                status: PayloadStatusEnum::Valid,
4737                latest_valid_hash: Some(B256::with_last_byte(1)),
4738            },
4739            PayloadStatus {
4740                status: PayloadStatusEnum::Invalid { validation_error: "bad payload".to_string() },
4741                latest_valid_hash: Some(B256::with_last_byte(2)),
4742            },
4743            PayloadStatus { status: PayloadStatusEnum::Syncing, latest_valid_hash: None },
4744            PayloadStatus { status: PayloadStatusEnum::Accepted, latest_valid_hash: None },
4745        ];
4746
4747        for status in statuses {
4748            let decoded = PayloadStatus::from_ssz_bytes(&status.as_ssz_bytes()).unwrap();
4749            assert_eq!(decoded, status);
4750        }
4751    }
4752
4753    #[test]
4754    #[cfg(feature = "ssz")]
4755    fn ssz_payload_status_matches_eip8178_container() {
4756        use ssz::{Decode, Encode};
4757
4758        let status = PayloadStatus {
4759            status: PayloadStatusEnum::Invalid { validation_error: "bad payload".to_string() },
4760            latest_valid_hash: None,
4761        };
4762        let spec = (1u8, B256::ZERO, b"bad payload".to_vec());
4763
4764        assert_eq!(status.as_ssz_bytes(), spec.as_ssz_bytes());
4765        assert_eq!(PayloadStatus::from_ssz_bytes(&spec.as_ssz_bytes()).unwrap(), status);
4766    }
4767
4768    #[test]
4769    #[cfg(feature = "ssz")]
4770    fn ssz_payload_id_roundtrip() {
4771        use ssz::{Decode, Encode};
4772
4773        let payload_id = PayloadId(B64::with_last_byte(42));
4774        let decoded = PayloadId::from_ssz_bytes(&payload_id.as_ssz_bytes()).unwrap();
4775        assert_eq!(decoded, payload_id);
4776    }
4777
4778    #[test]
4779    fn payload_id_from_str() {
4780        let expected = PayloadId(B64::with_last_byte(42));
4781
4782        assert_eq!("0x000000000000002a".parse::<PayloadId>().unwrap(), expected);
4783        assert_eq!("000000000000002a".parse::<PayloadId>().unwrap(), expected);
4784    }
4785
4786    #[test]
4787    fn payload_id_from_str_rejects_invalid_hex() {
4788        assert!("0x2a".parse::<PayloadId>().is_err());
4789        assert!("0x00000000000000zz".parse::<PayloadId>().is_err());
4790    }
4791
4792    #[test]
4793    #[cfg(feature = "serde")]
4794    fn serde_payload_status_error_deserialize() {
4795        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"Failed to decode block"}"#;
4796        let q = PayloadStatus {
4797            latest_valid_hash: None,
4798            status: PayloadStatusEnum::Invalid {
4799                validation_error: "Failed to decode block".to_string(),
4800            },
4801        };
4802        assert_eq!(q, serde_json::from_str(s).unwrap());
4803
4804        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"links to previously rejected block"}"#;
4805        let q = PayloadStatus {
4806            latest_valid_hash: None,
4807            status: PayloadStatusEnum::Invalid {
4808                validation_error: PayloadValidationError::LinksToRejectedPayload.to_string(),
4809            },
4810        };
4811        assert_eq!(q, serde_json::from_str(s).unwrap());
4812
4813        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"invalid block number"}"#;
4814        let q = PayloadStatus {
4815            latest_valid_hash: None,
4816            status: PayloadStatusEnum::Invalid {
4817                validation_error: PayloadValidationError::InvalidBlockNumber.to_string(),
4818            },
4819        };
4820        assert_eq!(q, serde_json::from_str(s).unwrap());
4821
4822        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":
4823        "invalid merkle root: (remote: 0x3f77fb29ce67436532fee970e1add8f5cc80e8878c79b967af53b1fd92a0cab7 local: 0x603b9628dabdaadb442a3bb3d7e0360efc110e1948472909230909f1690fed17)"}"#;
4824        let q = PayloadStatus {
4825            latest_valid_hash: None,
4826            status: PayloadStatusEnum::Invalid {
4827                validation_error: PayloadValidationError::InvalidStateRoot {
4828                    remote: "0x3f77fb29ce67436532fee970e1add8f5cc80e8878c79b967af53b1fd92a0cab7"
4829                        .parse()
4830                        .unwrap(),
4831                    local: "0x603b9628dabdaadb442a3bb3d7e0360efc110e1948472909230909f1690fed17"
4832                        .parse()
4833                        .unwrap(),
4834                }
4835                .to_string(),
4836            },
4837        };
4838        assert_eq!(q, serde_json::from_str(s).unwrap());
4839    }
4840
4841    #[test]
4842    #[cfg(feature = "serde")]
4843    fn serde_roundtrip_legacy_txs_payload_v1() {
4844        // pulled from hive tests
4845        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"]}"#;
4846        let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap();
4847        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4848
4849        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4850        assert_eq!(any_payload, payload.into());
4851    }
4852
4853    #[test]
4854    #[cfg(feature = "serde")]
4855    fn serde_roundtrip_legacy_txs_payload_v3() {
4856        // pulled from hive tests - modified with 4844 fields
4857        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"}"#;
4858        let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap();
4859        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4860
4861        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4862        assert_eq!(any_payload, payload.into());
4863    }
4864
4865    #[test]
4866    #[cfg(feature = "serde")]
4867    fn serde_roundtrip_enveloped_txs_payload_v1() {
4868        // pulled from hive tests
4869        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"]}"#;
4870        let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap();
4871        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4872
4873        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4874        assert_eq!(any_payload, payload.into());
4875    }
4876
4877    #[test]
4878    #[cfg(feature = "serde")]
4879    fn serde_roundtrip_enveloped_txs_payload_v3() {
4880        // pulled from hive tests - modified with 4844 fields
4881        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"}"#;
4882        let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap();
4883        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
4884
4885        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
4886        assert_eq!(any_payload, payload.into());
4887    }
4888
4889    #[test]
4890    #[cfg(feature = "serde")]
4891    fn serde_roundtrip_execution_payload_envelope_v3() {
4892        // pulled from a geth response getPayloadV3 in hive tests
4893        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}"#;
4894        let envelope: ExecutionPayloadEnvelopeV3 = serde_json::from_str(response).unwrap();
4895        assert_eq!(serde_json::to_string(&envelope).unwrap(), response);
4896    }
4897
4898    #[test]
4899    #[cfg(feature = "serde")]
4900    fn serde_roundtrip_execution_payload_field_v2() {
4901        // withdrawals must select the V2 variant instead of collapsing into V1
4902        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"}]}"#;
4903        let field: ExecutionPayloadFieldV2 = serde_json::from_str(s).unwrap();
4904        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(s).unwrap();
4905        assert_eq!(field, ExecutionPayloadFieldV2::V2(payload_v2));
4906        assert_eq!(serde_json::to_string(&field).unwrap(), s);
4907
4908        // empty withdrawals still mean V2
4909        let s_empty = s.replace(
4910            r#"[{"index":"0x0","validatorIndex":"0x1","address":"0x00000000000000000000000000000000000010f0","amount":"0x64"}]"#,
4911            "[]",
4912        );
4913        let field: ExecutionPayloadFieldV2 = serde_json::from_str(&s_empty).unwrap();
4914        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(&s_empty).unwrap();
4915        assert_eq!(field, ExecutionPayloadFieldV2::V2(payload_v2));
4916        assert_eq!(serde_json::to_string(&field).unwrap(), s_empty);
4917
4918        // no withdrawals field means V1
4919        let s_v1 = s_empty.replace(r#","withdrawals":[]"#, "");
4920        let field: ExecutionPayloadFieldV2 = serde_json::from_str(&s_v1).unwrap();
4921        let payload_v1: ExecutionPayloadV1 = serde_json::from_str(&s_v1).unwrap();
4922        assert_eq!(field, ExecutionPayloadFieldV2::V1(payload_v1));
4923        assert_eq!(serde_json::to_string(&field).unwrap(), s_v1);
4924    }
4925
4926    #[test]
4927    #[cfg(feature = "serde")]
4928    fn serde_roundtrip_execution_payload_envelope_v2() {
4929        // a getPayloadV2 response with withdrawals in the payload
4930        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"}"#;
4931        let envelope: ExecutionPayloadEnvelopeV2 = serde_json::from_str(response).unwrap();
4932        assert!(matches!(envelope.execution_payload, ExecutionPayloadFieldV2::V2(_)));
4933        assert_eq!(serde_json::to_string(&envelope).unwrap(), response);
4934    }
4935
4936    #[test]
4937    #[cfg(feature = "serde")]
4938    fn serde_payload_input_enum_v3() {
4939        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"}"#;
4940
4941        let payload: ExecutionPayload = serde_json::from_str(response_v3).unwrap();
4942        assert!(payload.as_v3().is_some());
4943        assert_eq!(serde_json::to_string(&payload).unwrap(), response_v3);
4944
4945        let payload_v3: ExecutionPayloadV3 = serde_json::from_str(response_v3).unwrap();
4946        assert_eq!(payload.as_v3().unwrap(), &payload_v3);
4947    }
4948
4949    #[test]
4950    #[cfg(feature = "serde")]
4951    fn serde_payload_input_enum_v2() {
4952        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":[]}"#;
4953
4954        let payload: ExecutionPayload = serde_json::from_str(response_v2).unwrap();
4955        assert!(payload.as_v3().is_none());
4956        assert!(payload.as_v2().is_some());
4957        assert_eq!(serde_json::to_string(&payload).unwrap(), response_v2);
4958
4959        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(response_v2).unwrap();
4960        assert_eq!(payload.as_v2().unwrap(), &payload_v2);
4961    }
4962
4963    #[test]
4964    #[cfg(feature = "serde")]
4965    fn serde_payload_input_enum_faulty_v2() {
4966        // incomplete V3 payload should be rejected even if it has all V2 fields
4967        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"}"#;
4968
4969        let payload: Result<ExecutionPayload, serde_json::Error> =
4970            serde_json::from_str(response_faulty);
4971        assert!(payload.is_err());
4972    }
4973
4974    #[test]
4975    #[cfg(feature = "serde")]
4976    fn serde_payload_input_enum_faulty_v1() {
4977        // incomplete V3 payload should be rejected even if it has all V1 fields
4978        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"}"#;
4979
4980        let payload: Result<ExecutionPayload, serde_json::Error> =
4981            serde_json::from_str(response_faulty);
4982        assert!(payload.is_err());
4983    }
4984
4985    #[test]
4986    #[cfg(feature = "serde")]
4987    fn serde_faulty_roundtrip_payload_input_v3() {
4988        // The deserialization behavior of ExecutionPayload structs is faulty.
4989        // They should not be implicitly deserializable to an earlier version,
4990        // as this breaks round-trip behavior
4991        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"}"#;
4992
4993        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(response_v3).unwrap();
4994        assert_ne!(response_v3, serde_json::to_string(&payload_v2).unwrap());
4995
4996        let payload_v1: ExecutionPayloadV1 = serde_json::from_str(response_v3).unwrap();
4997        assert_ne!(response_v3, serde_json::to_string(&payload_v1).unwrap());
4998    }
4999
5000    #[test]
5001    #[cfg(feature = "serde")]
5002    fn serde_faulty_roundtrip_payload_input_v2() {
5003        // The deserialization behavior of ExecutionPayload structs is faulty.
5004        // They should not be implicitly deserializable to an earlier version,
5005        // as this breaks round-trip behavior
5006        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":[]}"#;
5007
5008        let payload: ExecutionPayloadV1 = serde_json::from_str(response_v2).unwrap();
5009        assert_ne!(response_v2, serde_json::to_string(&payload).unwrap());
5010    }
5011
5012    #[test]
5013    #[cfg(feature = "serde")]
5014    fn serde_deserialize_execution_payload_input_v2() {
5015        let response = r#"
5016{
5017  "baseFeePerGas": "0x173b30b3",
5018  "blockHash": "0x99d486755fd046ad0bbb60457bac93d4856aa42fa00629cc7e4a28b65b5f8164",
5019  "blockNumber": "0xb",
5020  "extraData": "0xd883010d01846765746888676f312e32302e33856c696e7578",
5021  "feeRecipient": "0x0000000000000000000000000000000000000000",
5022  "gasLimit": "0x405829",
5023  "gasUsed": "0x3f0ca0",
5024  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5025  "parentHash": "0xfe34aaa2b869c66a727783ee5ad3e3983b6ef22baf24a1e502add94e7bcac67a",
5026  "prevRandao": "0x74132c32fe3ab9a470a8352544514d21b6969e7749f97742b53c18a1b22b396c",
5027  "receiptsRoot": "0x6a5c41dc55a1bd3e74e7f6accc799efb08b00c36c15265058433fcea6323e95f",
5028  "stateRoot": "0xde3b357f5f099e4c33d0343c9e9d204d663d7bd9c65020a38e5d0b2a9ace78a2",
5029  "timestamp": "0x6507d6b4",
5030  "transactions": [
5031    "0xf86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53",
5032    "0xf86d0b8458b20efd82520894687704db07e902e9a8b3754031d168d46e3d586e8806f3e8d87878800080820a95a0e3108f710902be662d5c978af16109961ffaf2ac4f88522407d40949a9574276a0205719ed21889b42ab5c1026d40b759a507c12d92db0d100fa69e1ac79137caa",
5033    "0xf86d0c8458b20efd8252089415e6a5a2e131dd5467fa1ff3acd104f45ee5940b8806f3e8d87878800080820a96a0af556ba9cda1d686239e08c24e169dece7afa7b85e0948eaa8d457c0561277fca029da03d3af0978322e54ac7e8e654da23934e0dd839804cb0430f8aaafd732dc",
5034    "0xf8521784565adcb7830186a0808080820a96a0ec782872a673a9fe4eff028a5bdb30d6b8b7711f58a187bf55d3aec9757cb18ea001796d373da76f2b0aeda72183cce0ad070a4f03aa3e6fee4c757a9444245206",
5035    "0xf8521284565adcb7830186a0808080820a95a08a0ea89028eff02596b385a10e0bd6ae098f3b281be2c95a9feb1685065d7384a06239d48a72e4be767bd12f317dd54202f5623a33e71e25a87cb25dd781aa2fc8",
5036    "0xf8521384565adcb7830186a0808080820a95a0784dbd311a82f822184a46f1677a428cbe3a2b88a798fb8ad1370cdbc06429e8a07a7f6a0efd428e3d822d1de9a050b8a883938b632185c254944dd3e40180eb79"
5037  ],
5038  "withdrawals": []
5039}
5040        "#;
5041        let payload: ExecutionPayloadInputV2 = serde_json::from_str(response).unwrap();
5042        assert_eq!(payload.withdrawals, Some(vec![]));
5043
5044        let response = r#"
5045{
5046  "baseFeePerGas": "0x173b30b3",
5047  "blockHash": "0x99d486755fd046ad0bbb60457bac93d4856aa42fa00629cc7e4a28b65b5f8164",
5048  "blockNumber": "0xb",
5049  "extraData": "0xd883010d01846765746888676f312e32302e33856c696e7578",
5050  "feeRecipient": "0x0000000000000000000000000000000000000000",
5051  "gasLimit": "0x405829",
5052  "gasUsed": "0x3f0ca0",
5053  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5054  "parentHash": "0xfe34aaa2b869c66a727783ee5ad3e3983b6ef22baf24a1e502add94e7bcac67a",
5055  "prevRandao": "0x74132c32fe3ab9a470a8352544514d21b6969e7749f97742b53c18a1b22b396c",
5056  "receiptsRoot": "0x6a5c41dc55a1bd3e74e7f6accc799efb08b00c36c15265058433fcea6323e95f",
5057  "stateRoot": "0xde3b357f5f099e4c33d0343c9e9d204d663d7bd9c65020a38e5d0b2a9ace78a2",
5058  "timestamp": "0x6507d6b4",
5059  "transactions": [
5060    "0xf86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53",
5061    "0xf86d0b8458b20efd82520894687704db07e902e9a8b3754031d168d46e3d586e8806f3e8d87878800080820a95a0e3108f710902be662d5c978af16109961ffaf2ac4f88522407d40949a9574276a0205719ed21889b42ab5c1026d40b759a507c12d92db0d100fa69e1ac79137caa",
5062    "0xf86d0c8458b20efd8252089415e6a5a2e131dd5467fa1ff3acd104f45ee5940b8806f3e8d87878800080820a96a0af556ba9cda1d686239e08c24e169dece7afa7b85e0948eaa8d457c0561277fca029da03d3af0978322e54ac7e8e654da23934e0dd839804cb0430f8aaafd732dc",
5063    "0xf8521784565adcb7830186a0808080820a96a0ec782872a673a9fe4eff028a5bdb30d6b8b7711f58a187bf55d3aec9757cb18ea001796d373da76f2b0aeda72183cce0ad070a4f03aa3e6fee4c757a9444245206",
5064    "0xf8521284565adcb7830186a0808080820a95a08a0ea89028eff02596b385a10e0bd6ae098f3b281be2c95a9feb1685065d7384a06239d48a72e4be767bd12f317dd54202f5623a33e71e25a87cb25dd781aa2fc8",
5065    "0xf8521384565adcb7830186a0808080820a95a0784dbd311a82f822184a46f1677a428cbe3a2b88a798fb8ad1370cdbc06429e8a07a7f6a0efd428e3d822d1de9a050b8a883938b632185c254944dd3e40180eb79"
5066  ]
5067}
5068        "#;
5069        let payload: ExecutionPayloadInputV2 = serde_json::from_str(response).unwrap();
5070        assert_eq!(payload.withdrawals, None);
5071    }
5072
5073    #[test]
5074    #[cfg(feature = "serde")]
5075    fn serde_deserialize_v2_input_with_blob_fields() {
5076        let input = r#"
5077{
5078    "parentHash": "0xaaa4c5b574f37e1537c78931d1bca24a4d17d4f29f1ee97e1cd48b704909de1f",
5079    "feeRecipient": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
5080    "stateRoot": "0x308ee9c5c6fab5e3d08763a3b5fe0be8ada891fa5010a49a3390e018dd436810",
5081    "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
5082    "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5083    "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
5084    "blockNumber": "0xf",
5085    "gasLimit": "0x16345785d8a0000",
5086    "gasUsed": "0x0",
5087    "timestamp": "0x3a97",
5088    "extraData": "0x",
5089    "baseFeePerGas": "0x7",
5090    "blockHash": "0x38bb6ba645c7e6bd970f9c7d492fafe1e04d85349054cb48d16c9d2c3e3cd0bf",
5091    "transactions": [],
5092    "withdrawals": [],
5093    "excessBlobGas": "0x0",
5094    "blobGasUsed": "0x0"
5095}
5096        "#;
5097
5098        // ensure that deserializing this (it includes blob fields) fails
5099        let payload_res: Result<ExecutionPayloadInputV2, serde_json::Error> =
5100            serde_json::from_str(input);
5101        assert!(payload_res.is_err());
5102    }
5103
5104    // <https://github.com/paradigmxyz/reth/issues/6036>
5105    #[test]
5106    #[cfg(feature = "serde")]
5107    fn deserialize_op_base_payload() {
5108        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"]}"#;
5109        let _payload = serde_json::from_str::<ExecutionPayloadInputV2>(payload).unwrap();
5110    }
5111
5112    #[test]
5113    fn roundtrip_payload_to_block() {
5114        let first_transaction_raw = Bytes::from_static(&hex!("02f9017a8501a1f0ff438211cc85012a05f2008512a05f2000830249f094d5409474fd5a725eab2ac9a8b26ca6fb51af37ef80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000001bdd2ed4b616c800000000000000000000000000001e9ee781dd4b97bdef92e5d1785f73a1f931daa20000000000000000000000007a40026a3b9a41754a95eec8c92c6b99886f440c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ae80eb647dd09968488fa1d7e412bf8558a0b7a0000000000000000000000000f9815537d361cb02befd9918c95c97d4d8a4a2bc001a0ba8f1928bb0efc3fcd01524a2039a9a2588fa567cd9a7cc18217e05c615e9d69a0544bfd11425ac7748e76b3795b57a5563e2b0eff47b5428744c62ff19ccfc305")[..]);
5115        let second_transaction_raw = Bytes::from_static(&hex!("03f901388501a1f0ff430c843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c005f8c6a00195f6dff17753fc89b60eac6477026a805116962c9e412de8015c0484e661c1a001aae314061d4f5bbf158f15d9417a238f9589783f58762cd39d05966b3ba2fba0013f5be9b12e7da06f0dd11a7bdc4e0db8ef33832acc23b183bd0a2c1408a757a0019d9ac55ea1a615d92965e04d960cb3be7bff121a381424f1f22865bd582e09a001def04412e76df26fefe7b0ed5e10580918ae4f355b074c0cfe5d0259157869a0011c11a415db57e43db07aef0de9280b591d65ca0cce36c7002507f8191e5d4a80a0c89b59970b119187d97ad70539f1624bbede92648e2dc007890f9658a88756c5a06fb2e3d4ce2c438c0856c2de34948b7032b1aadc4642a9666228ea8cdc7786b7")[..]);
5116
5117        let new_payload = ExecutionPayloadV3 {
5118            payload_inner: ExecutionPayloadV2 {
5119                payload_inner: ExecutionPayloadV1 {
5120                    base_fee_per_gas:  U256::from(7u64),
5121                    block_number: 0xa946u64,
5122                    block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(),
5123                    logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(),
5124                    extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(),
5125                    gas_limit: 0x1c9c380,
5126                    gas_used: 0x1f4a9,
5127                    timestamp: 0x651f35b8,
5128                    fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
5129                    parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(),
5130                    prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(),
5131                    receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(),
5132                    state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(),
5133                    transactions: vec![first_transaction_raw, second_transaction_raw],
5134                },
5135                withdrawals: vec![],
5136            },
5137            blob_gas_used: 0xc0000,
5138            excess_blob_gas: 0x580000,
5139        };
5140
5141        let mut block: Block<TxEnvelope> = new_payload.clone().try_into_block().unwrap();
5142
5143        // this newPayload came with a parent beacon block root, we need to manually insert it
5144        // before hashing
5145        let parent_beacon_block_root =
5146            b256!("531cd53b8e68deef0ea65edfa3cda927a846c307b0907657af34bc3f313b5871");
5147        block.header.parent_beacon_block_root = Some(parent_beacon_block_root);
5148
5149        let converted_payload = ExecutionPayloadV3::from_block_unchecked(block.hash_slow(), &block);
5150
5151        // ensure the payloads are the same
5152        assert_eq!(new_payload, converted_payload);
5153    }
5154
5155    #[test]
5156    fn payload_to_block_rejects_network_encoded_tx() {
5157        let first_transaction_raw = Bytes::from_static(&hex!("b9017e02f9017a8501a1f0ff438211cc85012a05f2008512a05f2000830249f094d5409474fd5a725eab2ac9a8b26ca6fb51af37ef80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000001bdd2ed4b616c800000000000000000000000000001e9ee781dd4b97bdef92e5d1785f73a1f931daa20000000000000000000000007a40026a3b9a41754a95eec8c92c6b99886f440c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ae80eb647dd09968488fa1d7e412bf8558a0b7a0000000000000000000000000f9815537d361cb02befd9918c95c97d4d8a4a2bc001a0ba8f1928bb0efc3fcd01524a2039a9a2588fa567cd9a7cc18217e05c615e9d69a0544bfd11425ac7748e76b3795b57a5563e2b0eff47b5428744c62ff19ccfc305")[..]);
5158        let second_transaction_raw = Bytes::from_static(&hex!("b9013c03f901388501a1f0ff430c843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c005f8c6a00195f6dff17753fc89b60eac6477026a805116962c9e412de8015c0484e661c1a001aae314061d4f5bbf158f15d9417a238f9589783f58762cd39d05966b3ba2fba0013f5be9b12e7da06f0dd11a7bdc4e0db8ef33832acc23b183bd0a2c1408a757a0019d9ac55ea1a615d92965e04d960cb3be7bff121a381424f1f22865bd582e09a001def04412e76df26fefe7b0ed5e10580918ae4f355b074c0cfe5d0259157869a0011c11a415db57e43db07aef0de9280b591d65ca0cce36c7002507f8191e5d4a80a0c89b59970b119187d97ad70539f1624bbede92648e2dc007890f9658a88756c5a06fb2e3d4ce2c438c0856c2de34948b7032b1aadc4642a9666228ea8cdc7786b7")[..]);
5159
5160        let new_payload = ExecutionPayloadV3 {
5161            payload_inner: ExecutionPayloadV2 {
5162                payload_inner: ExecutionPayloadV1 {
5163                    base_fee_per_gas:  U256::from(7u64),
5164                    block_number: 0xa946u64,
5165                    block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(),
5166                    logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(),
5167                    extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(),
5168                    gas_limit: 0x1c9c380,
5169                    gas_used: 0x1f4a9,
5170                    timestamp: 0x651f35b8,
5171                    fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
5172                    parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(),
5173                    prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(),
5174                    receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(),
5175                    state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(),
5176                    transactions: vec![first_transaction_raw, second_transaction_raw],
5177                },
5178                withdrawals: vec![],
5179            },
5180            blob_gas_used: 0xc0000,
5181            excess_blob_gas: 0x580000,
5182        };
5183
5184        let _block = new_payload
5185            .try_into_block::<TxEnvelope>()
5186            .expect_err("execution payload conversion requires typed txs without a rlp header");
5187    }
5188
5189    #[test]
5190    fn devnet_invalid_block_hash_repro() {
5191        let deser_block = r#"
5192        {
5193            "parentHash": "0xae8315ee86002e6269a17dd1e9516a6cf13223e9d4544d0c32daff826fb31acc",
5194            "feeRecipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134",
5195            "stateRoot": "0x03787f1579efbaa4a8234e72465eb4e29ef7e62f61242d6454661932e1a282a1",
5196            "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
5197            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
5198            "prevRandao": "0x918e86b497dc15de7d606457c36ca583e24d9b0a110a814de46e33d5bb824a66",
5199            "blockNumber": "0x6a784",
5200            "gasLimit": "0x1c9c380",
5201            "gasUsed": "0x0",
5202            "timestamp": "0x65bc1d60",
5203            "extraData": "0x9a726574682f76302e312e302d616c7068612e31362f6c696e7578",
5204            "baseFeePerGas": "0x8",
5205            "blobGasUsed": "0x0",
5206            "excessBlobGas": "0x0",
5207            "blockHash": "0x340c157eca9fd206b87c17f0ecbe8d411219de7188a0a240b635c88a96fe91c5",
5208            "transactions": [],
5209            "withdrawals": [
5210                {
5211                    "index": "0x5ab202",
5212                    "validatorIndex": "0xb1b",
5213                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5214                    "amount": "0x19b3d"
5215                },
5216                {
5217                    "index": "0x5ab203",
5218                    "validatorIndex": "0xb1c",
5219                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5220                    "amount": "0x15892"
5221                },
5222                {
5223                    "index": "0x5ab204",
5224                    "validatorIndex": "0xb1d",
5225                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5226                    "amount": "0x19b3d"
5227                },
5228                {
5229                    "index": "0x5ab205",
5230                    "validatorIndex": "0xb1e",
5231                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5232                    "amount": "0x19b3d"
5233                },
5234                {
5235                    "index": "0x5ab206",
5236                    "validatorIndex": "0xb1f",
5237                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5238                    "amount": "0x19b3d"
5239                },
5240                {
5241                    "index": "0x5ab207",
5242                    "validatorIndex": "0xb20",
5243                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5244                    "amount": "0x19b3d"
5245                },
5246                {
5247                    "index": "0x5ab208",
5248                    "validatorIndex": "0xb21",
5249                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5250                    "amount": "0x15892"
5251                },
5252                {
5253                    "index": "0x5ab209",
5254                    "validatorIndex": "0xb22",
5255                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5256                    "amount": "0x19b3d"
5257                },
5258                {
5259                    "index": "0x5ab20a",
5260                    "validatorIndex": "0xb23",
5261                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5262                    "amount": "0x19b3d"
5263                },
5264                {
5265                    "index": "0x5ab20b",
5266                    "validatorIndex": "0xb24",
5267                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5268                    "amount": "0x17db2"
5269                },
5270                {
5271                    "index": "0x5ab20c",
5272                    "validatorIndex": "0xb25",
5273                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5274                    "amount": "0x19b3d"
5275                },
5276                {
5277                    "index": "0x5ab20d",
5278                    "validatorIndex": "0xb26",
5279                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5280                    "amount": "0x19b3d"
5281                },
5282                {
5283                    "index": "0x5ab20e",
5284                    "validatorIndex": "0xa91",
5285                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5286                    "amount": "0x15892"
5287                },
5288                {
5289                    "index": "0x5ab20f",
5290                    "validatorIndex": "0xa92",
5291                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5292                    "amount": "0x1c05d"
5293                },
5294                {
5295                    "index": "0x5ab210",
5296                    "validatorIndex": "0xa93",
5297                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5298                    "amount": "0x15892"
5299                },
5300                {
5301                    "index": "0x5ab211",
5302                    "validatorIndex": "0xa94",
5303                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
5304                    "amount": "0x19b3d"
5305                }
5306            ]
5307        }
5308        "#;
5309
5310        // deserialize payload
5311        let payload: ExecutionPayload =
5312            serde_json::from_str::<ExecutionPayloadV3>(deser_block).unwrap().into();
5313
5314        // NOTE: the actual block hash here is incorrect, it is a result of a bug, this was the
5315        // fix:
5316        // <https://github.com/paradigmxyz/reth/pull/6328>
5317        let block_hash_with_blob_fee_fields =
5318            b256!("a7cdd5f9e54147b53a15833a8c45dffccbaed534d7fdc23458f45102a4bf71b0");
5319
5320        let versioned_hashes = vec![];
5321        let parent_beacon_block_root =
5322            b256!("1162de8a0f4d20d86b9ad6e0a2575ab60f00a433dc70d9318c8abc9041fddf54");
5323
5324        // set up cancun payload fields
5325        let cancun_fields = CancunPayloadFields { parent_beacon_block_root, versioned_hashes };
5326
5327        // convert into block
5328        let block = payload
5329            .try_into_block_with_sidecar::<TxEnvelope>(&ExecutionPayloadSidecar::v3(cancun_fields))
5330            .unwrap();
5331
5332        // Ensure the actual hash is calculated if we set the fields to what they should be
5333        assert_eq!(block_hash_with_blob_fee_fields, block.header.hash_slow());
5334    }
5335
5336    #[test]
5337    fn test_payload_to_block_with_sidecar_raw() {
5338        use std::path::PathBuf;
5339
5340        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/payload");
5341        let dir = std::fs::read_dir(path).expect("Unable to read payload folder");
5342
5343        for entry in dir {
5344            let entry = entry.expect("Unable to read entry");
5345            let path = entry.path();
5346
5347            if path.extension().and_then(|s| s.to_str()) != Some("json") {
5348                continue;
5349            }
5350
5351            let contents = std::fs::read_to_string(&path).expect("Unable to read file");
5352            let value: serde_json::Value = serde_json::from_str(&contents)
5353                .unwrap_or_else(|e| panic!("Failed to parse JSON from {path:?}: {e}"));
5354
5355            // Extract the newPayload object
5356            let new_payload = &value["newPayload"];
5357            let payload_value = &new_payload["payload"];
5358            let sidecar_value = &new_payload["sidecar"];
5359
5360            let payload: ExecutionPayload = serde_json::from_value(payload_value.clone())
5361                .unwrap_or_else(|e| panic!("Failed to deserialize payload from {path:?}: {e}"));
5362
5363            // Deserialize the sidecar
5364            let sidecar: ExecutionPayloadSidecar = serde_json::from_value(sidecar_value.clone())
5365                .unwrap_or_else(|e| panic!("Failed to deserialize sidecar from {path:?}: {e}"));
5366
5367            // Convert to block with raw transactions
5368            let block = payload.clone().into_block_with_sidecar_raw(&sidecar).unwrap_or_else(|e| {
5369                panic!("Failed to convert payload to block from {path:?}: {e}")
5370            });
5371
5372            // Verify the block has raw transactions (Bytes) if there are any
5373            if let Some(tx_count) = payload_value["transactions"].as_array().map(|a| a.len()) {
5374                assert_eq!(
5375                    block.body.transactions.len(),
5376                    tx_count,
5377                    "Transaction count mismatch in {:?}",
5378                    path
5379                );
5380            }
5381
5382            // Verify sidecar fields are applied
5383            assert_eq!(
5384                block.header.parent_beacon_block_root,
5385                sidecar.parent_beacon_block_root(),
5386                "Parent beacon block root mismatch in {:?}",
5387                path
5388            );
5389            assert_eq!(
5390                block.header.requests_hash,
5391                sidecar.requests_hash(),
5392                "Requests hash mismatch in {:?}",
5393                path
5394            );
5395
5396            // Verify the block hash matches the one in the payload
5397            let expected_hash = payload_value["blockHash"]
5398                .as_str()
5399                .unwrap()
5400                .parse::<B256>()
5401                .unwrap_or_else(|e| panic!("Failed to parse block hash from {path:?}: {e}"));
5402            let actual_hash = block.header.hash_slow();
5403            assert_eq!(
5404                actual_hash, expected_hash,
5405                "Block hash mismatch in {:?}: expected {}, got {}",
5406                path, expected_hash, actual_hash
5407            );
5408
5409            let block =
5410                payload.try_into_block_with_sidecar::<TxEnvelope>(&sidecar).unwrap_or_else(|e| {
5411                    panic!("Failed to convert payload to block from {path:?}: {e}")
5412                });
5413            let actual_hash = block.header.hash_slow();
5414            assert_eq!(
5415                actual_hash, expected_hash,
5416                "Block hash mismatch in {:?}: expected {}, got {}",
5417                path, expected_hash, actual_hash
5418            );
5419        }
5420    }
5421
5422    #[test]
5423    #[cfg(feature = "serde")]
5424    fn test_into_block_raw_with_transactions_root() {
5425        use std::path::PathBuf;
5426
5427        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/payload");
5428        let dir = std::fs::read_dir(path).expect("Unable to read payload folder");
5429
5430        for entry in dir {
5431            let entry = entry.expect("Unable to read entry");
5432            let path = entry.path();
5433
5434            if path.extension().and_then(|s| s.to_str()) != Some("json") {
5435                continue;
5436            }
5437
5438            let contents = std::fs::read_to_string(&path).expect("Unable to read file");
5439            let value: serde_json::Value = serde_json::from_str(&contents)
5440                .unwrap_or_else(|e| panic!("Failed to parse JSON from {path:?}: {e}"));
5441
5442            let new_payload = &value["newPayload"];
5443            let payload_value = &new_payload["payload"];
5444            let sidecar_value = &new_payload["sidecar"];
5445
5446            let payload: ExecutionPayload = serde_json::from_value(payload_value.clone())
5447                .unwrap_or_else(|e| panic!("Failed to deserialize payload from {path:?}: {e}"));
5448
5449            let sidecar: ExecutionPayloadSidecar = serde_json::from_value(sidecar_value.clone())
5450                .unwrap_or_else(|e| panic!("Failed to deserialize sidecar from {path:?}: {e}"));
5451
5452            let expected_hash = payload_value["blockHash"]
5453                .as_str()
5454                .unwrap()
5455                .parse::<B256>()
5456                .unwrap_or_else(|e| panic!("Failed to parse block hash from {path:?}: {e}"));
5457
5458            // Build the block normally to get the computed transactions root
5459            let block_normal =
5460                payload.clone().into_block_with_sidecar_raw(&sidecar).unwrap_or_else(|e| {
5461                    panic!("Failed to convert payload to block from {path:?}: {e}")
5462                });
5463            let tx_root = block_normal.header.transactions_root;
5464
5465            // Build using pre-computed transactions root
5466            let block_with_root =
5467                payload.clone().into_block_raw_with_transactions_root(tx_root).unwrap();
5468            assert_eq!(
5469                block_with_root.header.transactions_root, tx_root,
5470                "transactions_root mismatch in {path:?}"
5471            );
5472
5473            // Build using the opt variant with Some
5474            let block_opt_some =
5475                payload.clone().into_block_raw_with_transactions_root_opt(Some(tx_root)).unwrap();
5476            assert_eq!(
5477                block_opt_some.header.transactions_root, tx_root,
5478                "opt(Some) transactions_root mismatch in {path:?}"
5479            );
5480
5481            // Build using the opt variant with None (should compute same root)
5482            let block_opt_none =
5483                payload.clone().into_block_raw_with_transactions_root_opt(None).unwrap();
5484            assert_eq!(
5485                block_opt_none.header.transactions_root, tx_root,
5486                "opt(None) transactions_root mismatch in {path:?}"
5487            );
5488
5489            // Build with sidecar + pre-computed root and verify block hash
5490            let block_sidecar_root = payload
5491                .into_block_with_sidecar_raw_with_transactions_root(&sidecar, tx_root)
5492                .unwrap();
5493            let actual_hash = block_sidecar_root.header.hash_slow();
5494            assert_eq!(
5495                actual_hash, expected_hash,
5496                "Block hash mismatch with pre-computed tx root in {path:?}"
5497            );
5498        }
5499    }
5500
5501    #[test]
5502    fn test_v1_with_transactions_root_override() {
5503        let transaction = Bytes::from_static(&hex!("f86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53"));
5504
5505        let payload = ExecutionPayloadV1 {
5506            parent_hash: B256::default(),
5507            fee_recipient: Address::default(),
5508            state_root: B256::default(),
5509            receipts_root: B256::default(),
5510            logs_bloom: Bloom::default(),
5511            prev_randao: B256::default(),
5512            block_number: 0,
5513            gas_limit: 0,
5514            gas_used: 0,
5515            timestamp: 0,
5516            extra_data: Bytes::default(),
5517            base_fee_per_gas: U256::from(1),
5518            block_hash: B256::default(),
5519            transactions: vec![transaction],
5520        };
5521
5522        let computed_root = payload.clone().into_block_raw().unwrap().header.transactions_root;
5523
5524        let fake_root = b256!("1111111111111111111111111111111111111111111111111111111111111111");
5525        assert_ne!(computed_root, fake_root);
5526
5527        let block = payload.clone().into_block_raw_with_transactions_root(fake_root).unwrap();
5528        assert_eq!(block.header.transactions_root, fake_root);
5529
5530        let block_opt = payload.into_block_raw_with_transactions_root_opt(Some(fake_root)).unwrap();
5531        assert_eq!(block_opt.header.transactions_root, fake_root);
5532    }
5533
5534    #[test]
5535    fn test_with_transactions_root_extra_data_validation() {
5536        let payload = ExecutionPayloadV1 {
5537            parent_hash: B256::default(),
5538            fee_recipient: Address::default(),
5539            state_root: B256::default(),
5540            receipts_root: B256::default(),
5541            logs_bloom: Bloom::default(),
5542            prev_randao: B256::default(),
5543            block_number: 0,
5544            gas_limit: 0,
5545            gas_used: 0,
5546            timestamp: 0,
5547            extra_data: Bytes::from(vec![0u8; MAXIMUM_EXTRA_DATA_SIZE + 1]),
5548            base_fee_per_gas: U256::from(1),
5549            block_hash: B256::default(),
5550            transactions: vec![],
5551        };
5552
5553        let fake_root = b256!("1111111111111111111111111111111111111111111111111111111111111111");
5554
5555        assert!(payload.clone().into_block_raw().is_err());
5556        assert!(payload.clone().into_block_raw_with_transactions_root(fake_root).is_err());
5557        assert!(payload.into_block_raw_with_transactions_root_opt(Some(fake_root)).is_err());
5558    }
5559
5560    #[test]
5561    fn test_decoded_transactions() {
5562        let transaction = Bytes::from_static(&hex!("f86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53"));
5563
5564        let payload = ExecutionPayload::V1(ExecutionPayloadV1 {
5565            parent_hash: B256::default(),
5566            fee_recipient: Address::default(),
5567            state_root: B256::default(),
5568            receipts_root: B256::default(),
5569            logs_bloom: Bloom::default(),
5570            prev_randao: B256::default(),
5571            block_number: 0,
5572            gas_limit: 0,
5573            gas_used: 0,
5574            timestamp: 0,
5575            extra_data: Bytes::default(),
5576            base_fee_per_gas: U256::default(),
5577            block_hash: B256::default(),
5578            transactions: vec![transaction.clone()],
5579        });
5580
5581        // Test decoded_transactions
5582        let decoded: Vec<_> = payload.decoded_transactions::<TxEnvelope>().collect();
5583        assert_eq!(decoded.len(), 1);
5584        assert!(decoded[0].is_ok(), "Failed to decode transaction: {:?}", decoded[0]);
5585
5586        // Test decoded_transactions_with_encoded
5587        let decoded_with_encoded: Vec<_> =
5588            payload.decoded_transactions_with_encoded::<TxEnvelope>().collect();
5589        assert_eq!(decoded_with_encoded.len(), 1);
5590        assert!(decoded_with_encoded[0].is_ok());
5591        if let Ok(with_encoded) = &decoded_with_encoded[0] {
5592            assert_eq!(with_encoded.encoded_bytes(), &transaction);
5593        }
5594    }
5595
5596    #[test]
5597    #[cfg(feature = "serde")]
5598    fn serde_payload_attributes_without_slot_number() {
5599        let json = r#"{
5600            "timestamp": "0x1234",
5601            "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
5602            "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000"
5603        }"#;
5604
5605        let attrs: PayloadAttributes = serde_json::from_str(json).unwrap();
5606        assert_eq!(attrs.timestamp, 0x1234);
5607        assert!(attrs.slot_number.is_none());
5608        assert!(attrs.target_gas_limit.is_none());
5609    }
5610
5611    #[test]
5612    #[cfg(feature = "serde")]
5613    fn serde_payload_attributes_with_hex_amsterdam_fields() {
5614        let json = r#"{
5615            "timestamp": "0x2",
5616            "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
5617            "suggestedFeeRecipient": "0x0000000000000000000000000000000000000000",
5618            "withdrawals": [],
5619            "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
5620            "slotNumber": "0x0",
5621            "targetGasLimit": "0x1c9c380"
5622        }"#;
5623
5624        let attrs: PayloadAttributes = serde_json::from_str(json).unwrap();
5625        assert_eq!(attrs.timestamp, 0x2);
5626        assert_eq!(attrs.slot_number, Some(0));
5627        assert_eq!(attrs.target_gas_limit, Some(30_000_000));
5628    }
5629
5630    #[test]
5631    #[cfg(feature = "serde")]
5632    fn serde_execution_payload_body_v2() {
5633        let body = ExecutionPayloadBodyV2 {
5634            transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])],
5635            withdrawals: Some(vec![Withdrawal {
5636                index: 1,
5637                validator_index: 2,
5638                address: Address::default(),
5639                amount: 100,
5640            }]),
5641            block_access_list: Some(Bytes::from(vec![0xaa, 0xbb, 0xcc])),
5642        };
5643
5644        let serialized = serde_json::to_string(&body).unwrap();
5645        let deserialized: ExecutionPayloadBodyV2 = serde_json::from_str(&serialized).unwrap();
5646        assert_eq!(deserialized, body);
5647    }
5648
5649    #[test]
5650    #[cfg(feature = "serde")]
5651    fn serde_execution_payload_body_v2_null_fields() {
5652        let body = ExecutionPayloadBodyV2 {
5653            transactions: vec![],
5654            withdrawals: None,
5655            block_access_list: None,
5656        };
5657
5658        let serialized = serde_json::to_string(&body).unwrap();
5659        let deserialized: ExecutionPayloadBodyV2 = serde_json::from_str(&serialized).unwrap();
5660        assert_eq!(deserialized, body);
5661    }
5662
5663    #[test]
5664    #[cfg(feature = "ssz")]
5665    fn ssz_execution_payload_body_v1_roundtrip() {
5666        use ssz::{Decode, Encode};
5667
5668        let body = ExecutionPayloadBodyV1 {
5669            transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])],
5670            withdrawals: Some(vec![Withdrawal {
5671                index: 1,
5672                validator_index: 2,
5673                address: Address::with_last_byte(3),
5674                amount: 4,
5675            }]),
5676        };
5677
5678        let decoded = ExecutionPayloadBodyV1::from_ssz_bytes(&body.as_ssz_bytes()).unwrap();
5679        assert_eq!(decoded, body);
5680
5681        let bodies: ExecutionPayloadBodiesV1 = vec![Some(body), None];
5682        let decoded = ExecutionPayloadBodiesV1::from_ssz_bytes(&bodies.as_ssz_bytes()).unwrap();
5683        assert_eq!(decoded, bodies);
5684    }
5685
5686    #[test]
5687    #[cfg(feature = "ssz")]
5688    fn ssz_execution_payload_body_v2_roundtrip() {
5689        use ssz::{Decode, Encode};
5690
5691        let body = ExecutionPayloadBodyV2 {
5692            transactions: vec![Bytes::from(vec![0x04, 0x05, 0x06])],
5693            withdrawals: None,
5694            block_access_list: Some(Bytes::from(vec![0xaa, 0xbb, 0xcc])),
5695        };
5696
5697        let decoded = ExecutionPayloadBodyV2::from_ssz_bytes(&body.as_ssz_bytes()).unwrap();
5698        assert_eq!(decoded, body);
5699
5700        let bodies: ExecutionPayloadBodiesV2 = vec![Some(body), None];
5701        let decoded = ExecutionPayloadBodiesV2::from_ssz_bytes(&bodies.as_ssz_bytes()).unwrap();
5702        assert_eq!(decoded, bodies);
5703    }
5704
5705    #[test]
5706    fn execution_payload_body_v1_to_v2_conversion() {
5707        let v1 = ExecutionPayloadBodyV1 {
5708            transactions: vec![Bytes::from(vec![0x01, 0x02])],
5709            withdrawals: Some(vec![Withdrawal {
5710                index: 1,
5711                validator_index: 2,
5712                address: Address::default(),
5713                amount: 100,
5714            }]),
5715        };
5716
5717        let v2: ExecutionPayloadBodyV2 = v1.clone().into();
5718        assert_eq!(v2.transactions, v1.transactions);
5719        assert_eq!(v2.withdrawals, v1.withdrawals);
5720        assert_eq!(v2.block_access_list, None);
5721    }
5722
5723    #[test]
5724    fn execution_payload_body_v2_to_v1_conversion() {
5725        let v2 = ExecutionPayloadBodyV2 {
5726            transactions: vec![Bytes::from(vec![0x01, 0x02])],
5727            withdrawals: Some(vec![Withdrawal {
5728                index: 1,
5729                validator_index: 2,
5730                address: Address::default(),
5731                amount: 100,
5732            }]),
5733            block_access_list: Some(Bytes::from(vec![0xaa, 0xbb])),
5734        };
5735
5736        let v1: ExecutionPayloadBodyV1 = v2.clone().into();
5737        assert_eq!(v1.transactions, v2.transactions);
5738        assert_eq!(v1.withdrawals, v2.withdrawals);
5739    }
5740
5741    #[test]
5742    #[cfg(feature = "serde")]
5743    fn serde_roundtrip_payload_v2() {
5744        let payload = ExecutionPayloadV2 {
5745            payload_inner: ExecutionPayloadV1 {
5746                parent_hash: B256::default(),
5747                fee_recipient: Address::default(),
5748                state_root: B256::default(),
5749                receipts_root: B256::default(),
5750                logs_bloom: Bloom::default(),
5751                prev_randao: B256::default(),
5752                block_number: 1,
5753                gas_limit: 30_000_000,
5754                gas_used: 21000,
5755                timestamp: 1234,
5756                extra_data: Bytes::default(),
5757                base_fee_per_gas: U256::from(7u64),
5758                block_hash: B256::default(),
5759                transactions: vec![],
5760            },
5761            withdrawals: vec![Withdrawal {
5762                index: 1,
5763                validator_index: 2,
5764                address: Address::default(),
5765                amount: 100,
5766            }],
5767        };
5768
5769        let serialized = serde_json::to_string(&payload).unwrap();
5770        let deserialized: ExecutionPayloadV2 = serde_json::from_str(&serialized).unwrap();
5771        assert_eq!(payload, deserialized);
5772    }
5773
5774    #[test]
5775    #[cfg(feature = "serde")]
5776    fn serde_roundtrip_payload_v4() {
5777        let payload = ExecutionPayloadV4 {
5778            payload_inner: ExecutionPayloadV3 {
5779                payload_inner: ExecutionPayloadV2 {
5780                    payload_inner: ExecutionPayloadV1 {
5781                        parent_hash: B256::default(),
5782                        fee_recipient: Address::default(),
5783                        state_root: B256::default(),
5784                        receipts_root: B256::default(),
5785                        logs_bloom: Bloom::default(),
5786                        prev_randao: B256::default(),
5787                        block_number: 1,
5788                        gas_limit: 30_000_000,
5789                        gas_used: 21000,
5790                        timestamp: 1234,
5791                        extra_data: Bytes::default(),
5792                        base_fee_per_gas: U256::from(7u64),
5793                        block_hash: B256::default(),
5794                        transactions: vec![],
5795                    },
5796                    withdrawals: vec![],
5797                },
5798                blob_gas_used: 0,
5799                excess_blob_gas: 0,
5800            },
5801            block_access_list: Bytes::from(vec![0xaa, 0xbb]),
5802            slot_number: 0,
5803        };
5804
5805        let serialized = serde_json::to_string(&payload).unwrap();
5806        let deserialized: ExecutionPayloadV4 = serde_json::from_str(&serialized).unwrap();
5807        assert_eq!(payload, deserialized);
5808    }
5809
5810    #[test]
5811    fn payload_v4_from_block_falls_back_to_bal_hash_bytes() {
5812        let bal_hash = b256!("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
5813        let header = Header {
5814            block_access_list_hash: Some(bal_hash),
5815            slot_number: Some(7),
5816            ..Default::default()
5817        };
5818
5819        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5820        let (payload, _) = ExecutionPayload::from_block_unchecked(B256::with_last_byte(1), &block);
5821
5822        let payload = payload.as_v4().expect("expected V4 payload");
5823        assert_eq!(payload.block_access_list, Bytes::copy_from_slice(bal_hash.as_slice()));
5824        assert_eq!(payload.slot_number, 7);
5825    }
5826
5827    #[test]
5828    fn payload_v4_from_block_without_bal_hash_uses_empty_bal_hash_bytes() {
5829        let header = Header { slot_number: Some(3), ..Default::default() };
5830
5831        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5832        let payload = ExecutionPayloadV4::from_block_unchecked(B256::with_last_byte(2), &block);
5833
5834        assert_eq!(
5835            payload.block_access_list,
5836            Bytes::copy_from_slice(EMPTY_BLOCK_ACCESS_LIST_HASH.as_slice())
5837        );
5838        assert_eq!(payload.slot_number, 3);
5839    }
5840
5841    #[test]
5842    fn execution_data_from_sealed_block_uses_sealed_hash() {
5843        let block: Block<TxEnvelope> = Block::new(Header::default(), BlockBody::default());
5844        let block_hash = B256::with_last_byte(3);
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_ref_uses_sealed_hash() {
5853        let block: Block<TxEnvelope> = Block::new(Header::default(), BlockBody::default());
5854        let block_hash = B256::with_last_byte(4);
5855
5856        let execution_data = ExecutionData::from(Sealed::new_unchecked(&block, block_hash));
5857
5858        assert_eq!(execution_data.block_hash(), block_hash);
5859    }
5860
5861    #[test]
5862    fn execution_data_from_sealed_block_with_extras_preserves_bal() {
5863        let block_access_list = Bytes::from(vec![0xaa, 0xbb, 0xcc]);
5864        let header = Header {
5865            block_access_list_hash: Some(keccak256(&block_access_list)),
5866            slot_number: Some(7),
5867            ..Default::default()
5868        };
5869
5870        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5871        let block_hash = B256::with_last_byte(5);
5872        let execution_data = ExecutionData::from((
5873            Sealed::new_unchecked(block, block_hash),
5874            PayloadExtras::from(block_access_list.clone()),
5875        ));
5876
5877        assert_eq!(execution_data.block_hash(), block_hash);
5878        assert_eq!(execution_data.payload.block_access_list(), Some(&block_access_list));
5879        assert_eq!(execution_data.payload.slot_number(), Some(7));
5880    }
5881
5882    #[test]
5883    fn execution_data_from_sealed_block_ref_with_extras_preserves_bal() {
5884        let block_access_list = Bytes::from(vec![0xaa, 0xbb, 0xcc]);
5885        let header = Header {
5886            block_access_list_hash: Some(keccak256(&block_access_list)),
5887            slot_number: Some(7),
5888            ..Default::default()
5889        };
5890
5891        let block: Block<TxEnvelope> = Block::new(header, BlockBody::default());
5892        let block_hash = B256::with_last_byte(6);
5893        let execution_data = ExecutionData::from((
5894            Sealed::new_unchecked(&block, block_hash),
5895            PayloadExtras::from(block_access_list.clone()),
5896        ));
5897
5898        assert_eq!(execution_data.block_hash(), block_hash);
5899        assert_eq!(execution_data.payload.block_access_list(), Some(&block_access_list));
5900        assert_eq!(execution_data.payload.slot_number(), Some(7));
5901    }
5902
5903    #[test]
5904    fn execution_payload_gets_bal_hash_and_slot_number_from_v4() {
5905        let block_access_list = Bytes::from(vec![0xaa, 0xbb, 0xcc]);
5906        let payload = ExecutionPayload::from(ExecutionPayloadV4 {
5907            payload_inner: ExecutionPayloadV3 {
5908                payload_inner: ExecutionPayloadV2 {
5909                    payload_inner: ExecutionPayloadV1 {
5910                        parent_hash: B256::default(),
5911                        fee_recipient: Address::default(),
5912                        state_root: B256::default(),
5913                        receipts_root: B256::default(),
5914                        logs_bloom: Bloom::default(),
5915                        prev_randao: B256::default(),
5916                        block_number: 1,
5917                        gas_limit: 30_000_000,
5918                        gas_used: 21_000,
5919                        timestamp: 1_234,
5920                        extra_data: Bytes::default(),
5921                        base_fee_per_gas: U256::ZERO,
5922                        block_hash: B256::default(),
5923                        transactions: vec![],
5924                    },
5925                    withdrawals: vec![],
5926                },
5927                blob_gas_used: 0,
5928                excess_blob_gas: 0,
5929            },
5930            block_access_list: block_access_list.clone(),
5931            slot_number: 7,
5932        });
5933
5934        assert_eq!(payload.slot_number(), Some(7));
5935        assert_eq!(payload.bal_hash(), Some(keccak256(&block_access_list)));
5936    }
5937
5938    #[test]
5939    #[cfg(feature = "serde")]
5940    fn serde_roundtrip_payload_input_v2_with_withdrawals() {
5941        let payload = ExecutionPayloadInputV2 {
5942            execution_payload: ExecutionPayloadV1 {
5943                parent_hash: B256::default(),
5944                fee_recipient: Address::default(),
5945                state_root: B256::default(),
5946                receipts_root: B256::default(),
5947                logs_bloom: Bloom::default(),
5948                prev_randao: B256::default(),
5949                block_number: 1,
5950                gas_limit: 30_000_000,
5951                gas_used: 21000,
5952                timestamp: 1234,
5953                extra_data: Bytes::default(),
5954                base_fee_per_gas: U256::from(7u64),
5955                block_hash: B256::default(),
5956                transactions: vec![],
5957            },
5958            withdrawals: Some(vec![]),
5959        };
5960
5961        let serialized = serde_json::to_string(&payload).unwrap();
5962        let deserialized: ExecutionPayloadInputV2 = serde_json::from_str(&serialized).unwrap();
5963        assert_eq!(payload, deserialized);
5964    }
5965
5966    #[test]
5967    #[cfg(feature = "serde")]
5968    fn serde_roundtrip_payload_input_v2_without_withdrawals() {
5969        let payload = ExecutionPayloadInputV2 {
5970            execution_payload: ExecutionPayloadV1 {
5971                parent_hash: B256::default(),
5972                fee_recipient: Address::default(),
5973                state_root: B256::default(),
5974                receipts_root: B256::default(),
5975                logs_bloom: Bloom::default(),
5976                prev_randao: B256::default(),
5977                block_number: 1,
5978                gas_limit: 30_000_000,
5979                gas_used: 21000,
5980                timestamp: 1234,
5981                extra_data: Bytes::default(),
5982                base_fee_per_gas: U256::from(7u64),
5983                block_hash: B256::default(),
5984                transactions: vec![],
5985            },
5986            withdrawals: None,
5987        };
5988
5989        let serialized = serde_json::to_string(&payload).unwrap();
5990        let deserialized: ExecutionPayloadInputV2 = serde_json::from_str(&serialized).unwrap();
5991        assert_eq!(payload, deserialized);
5992    }
5993
5994    #[test]
5995    #[cfg(feature = "serde")]
5996    fn serde_roundtrip_envelope_v4() {
5997        let envelope = ExecutionPayloadEnvelopeV4 {
5998            envelope_inner: ExecutionPayloadEnvelopeV3 {
5999                execution_payload: ExecutionPayloadV3 {
6000                    payload_inner: ExecutionPayloadV2 {
6001                        payload_inner: ExecutionPayloadV1 {
6002                            parent_hash: B256::default(),
6003                            fee_recipient: Address::default(),
6004                            state_root: B256::default(),
6005                            receipts_root: B256::default(),
6006                            logs_bloom: Bloom::default(),
6007                            prev_randao: B256::default(),
6008                            block_number: 1,
6009                            gas_limit: 30_000_000,
6010                            gas_used: 21000,
6011                            timestamp: 1234,
6012                            extra_data: Bytes::default(),
6013                            base_fee_per_gas: U256::from(7u64),
6014                            block_hash: B256::default(),
6015                            transactions: vec![],
6016                        },
6017                        withdrawals: vec![],
6018                    },
6019                    blob_gas_used: 0,
6020                    excess_blob_gas: 0,
6021                },
6022                block_value: U256::from(1u64),
6023                blobs_bundle: BlobsBundleV1::empty(),
6024                should_override_builder: false,
6025            },
6026            execution_requests: Default::default(),
6027        };
6028
6029        let serialized = serde_json::to_string(&envelope).unwrap();
6030        let deserialized: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&serialized).unwrap();
6031        assert_eq!(envelope, deserialized);
6032    }
6033
6034    #[test]
6035    #[cfg(feature = "serde")]
6036    fn serde_v3_with_many_transactions() {
6037        let tx = Bytes::from_static(&hex!("f865808506fc23ac00830124f8940000000000000000000000000000000000000316018032a044b25a8b9b247d01586b3d59c71728ff49c9b84928d9e7fa3377ead3b5570b5da03ceac696601ff7ee6f5fe8864e2998db9babdf5eeba1a0cd5b4d44b3fcbd181b"));
6038        let transactions: Vec<Bytes> = (0..100).map(|_| tx.clone()).collect();
6039
6040        let payload = ExecutionPayloadV3 {
6041            payload_inner: ExecutionPayloadV2 {
6042                payload_inner: ExecutionPayloadV1 {
6043                    parent_hash: B256::default(),
6044                    fee_recipient: Address::default(),
6045                    state_root: B256::default(),
6046                    receipts_root: B256::default(),
6047                    logs_bloom: Bloom::default(),
6048                    prev_randao: B256::default(),
6049                    block_number: 1,
6050                    gas_limit: 30_000_000,
6051                    gas_used: 2_100_000,
6052                    timestamp: 1234,
6053                    extra_data: Bytes::default(),
6054                    base_fee_per_gas: U256::from(7u64),
6055                    block_hash: B256::default(),
6056                    transactions,
6057                },
6058                withdrawals: vec![],
6059            },
6060            blob_gas_used: 0,
6061            excess_blob_gas: 0,
6062        };
6063
6064        let serialized = serde_json::to_string(&payload).unwrap();
6065        let deserialized: ExecutionPayloadV3 = serde_json::from_str(&serialized).unwrap();
6066        assert_eq!(payload, deserialized);
6067    }
6068
6069    #[test]
6070    #[cfg(feature = "serde")]
6071    fn serde_input_v2_rejects_unknown_fields() {
6072        let input = r#"{
6073            "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
6074            "feeRecipient": "0x0000000000000000000000000000000000000000",
6075            "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
6076            "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
6077            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
6078            "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
6079            "blockNumber": "0x1",
6080            "gasLimit": "0x1c9c380",
6081            "gasUsed": "0x0",
6082            "timestamp": "0x1235",
6083            "extraData": "0x",
6084            "baseFeePerGas": "0x7",
6085            "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
6086            "transactions": [],
6087            "unknownField": "should fail"
6088        }"#;
6089
6090        let result: Result<ExecutionPayloadInputV2, _> = serde_json::from_str(input);
6091        assert!(result.is_err());
6092    }
6093}