alloy_rpc_types_engine/
payload.rs

1//! Payload types.
2
3use crate::{ExecutionPayloadSidecar, PayloadError};
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};
12use alloy_eips::{
13    calc_next_block_base_fee,
14    eip1559::BaseFeeParams,
15    eip2718::{Decodable2718, Eip2718Result, Encodable2718, WithEncoded},
16    eip4844::BlobTransactionSidecar,
17    eip4895::{Withdrawal, Withdrawals},
18    eip7594::{BlobTransactionSidecarEip7594, CELLS_PER_EXT_BLOB},
19    eip7685::Requests,
20    eip7840::BlobParams,
21    BlockNumHash,
22};
23use alloy_primitives::{Address, Bloom, Bytes, Sealable, B256, B64, U256};
24use core::iter::{FromIterator, IntoIterator};
25
26/// The execution payload body response that allows for `null` values.
27pub type ExecutionPayloadBodiesV1 = Vec<Option<ExecutionPayloadBodyV1>>;
28
29/// And 8-byte identifier for an execution payload.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
33pub struct PayloadId(pub B64);
34
35// === impl PayloadId ===
36
37impl PayloadId {
38    /// Creates a new payload id from the given identifier.
39    pub fn new(id: [u8; 8]) -> Self {
40        Self(B64::from(id))
41    }
42}
43
44impl core::fmt::Display for PayloadId {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        self.0.fmt(f)
47    }
48}
49
50impl From<B64> for PayloadId {
51    fn from(value: B64) -> Self {
52        Self(value)
53    }
54}
55
56/// This represents the `executionPayload` field in the return value of `engine_getPayloadV2`,
57/// specified as:
58///
59/// - `executionPayload`: `ExecutionPayloadV1` | `ExecutionPayloadV2` where:
60///   - `ExecutionPayloadV1` **MUST** be returned if the payload `timestamp` is lower than the
61///     Shanghai timestamp
62///   - `ExecutionPayloadV2` **MUST** be returned if the payload `timestamp` is greater or equal to
63///     the Shanghai timestamp
64///
65/// See:
66/// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/shanghai.md#response>
67#[derive(Clone, Debug, PartialEq, Eq)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69#[cfg_attr(feature = "serde", serde(untagged))]
70#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
71pub enum ExecutionPayloadFieldV2 {
72    /// V1 payload
73    V1(ExecutionPayloadV1),
74    /// V2 payload
75    V2(ExecutionPayloadV2),
76}
77
78impl ExecutionPayloadFieldV2 {
79    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadFieldV2`].
80    ///
81    /// See also:
82    ///  - [`ExecutionPayloadV1::from_block_unchecked`].
83    ///  - [`ExecutionPayloadV2::from_block_unchecked`].
84    ///
85    /// If the block body contains withdrawals this returns [`ExecutionPayloadFieldV2::V2`].
86    ///
87    /// Note: This re-calculates the block hash.
88    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
89    where
90        T: Encodable2718,
91        H: BlockHeader + Sealable,
92    {
93        Self::from_block_unchecked(block.hash_slow(), block)
94    }
95
96    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadFieldV2`] using the given block
97    /// hash.
98    ///
99    /// See also:
100    ///  - [`ExecutionPayloadV1::from_block_unchecked`].
101    ///  - [`ExecutionPayloadV2::from_block_unchecked`].
102    ///
103    /// If the block body contains withdrawals this returns [`ExecutionPayloadFieldV2::V2`].
104    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
105    where
106        T: Encodable2718,
107        H: BlockHeader,
108    {
109        if block.body.withdrawals.is_some() {
110            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
111        } else {
112            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
113        }
114    }
115
116    /// Returns the inner [ExecutionPayloadV1]
117    pub fn into_v1_payload(self) -> ExecutionPayloadV1 {
118        match self {
119            Self::V1(payload) => payload,
120            Self::V2(payload) => payload.payload_inner,
121        }
122    }
123
124    /// Converts this payload variant into the corresponding [ExecutionPayload]
125    pub fn into_payload(self) -> ExecutionPayload {
126        match self {
127            Self::V1(payload) => ExecutionPayload::V1(payload),
128            Self::V2(payload) => ExecutionPayload::V2(payload),
129        }
130    }
131}
132
133/// This is the input to `engine_newPayloadV2`, which may or may not have a withdrawals field.
134#[derive(Clone, Debug, PartialEq, Eq)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136#[cfg_attr(feature = "serde", serde(rename_all = "camelCase", deny_unknown_fields))]
137#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
138pub struct ExecutionPayloadInputV2 {
139    /// The V1 execution payload
140    #[cfg_attr(feature = "serde", serde(flatten))]
141    pub execution_payload: ExecutionPayloadV1,
142    /// The payload withdrawals
143    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
144    pub withdrawals: Option<Vec<Withdrawal>>,
145}
146
147impl ExecutionPayloadInputV2 {
148    /// Converts [`ExecutionPayloadInputV2`] to [`ExecutionPayload`]
149    pub fn into_payload(self) -> ExecutionPayload {
150        match self.withdrawals {
151            Some(withdrawals) => ExecutionPayload::V2(ExecutionPayloadV2 {
152                payload_inner: self.execution_payload,
153                withdrawals,
154            }),
155            None => ExecutionPayload::V1(self.execution_payload),
156        }
157    }
158}
159
160impl From<ExecutionPayloadInputV2> for ExecutionPayload {
161    fn from(input: ExecutionPayloadInputV2) -> Self {
162        input.into_payload()
163    }
164}
165
166/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
167/// V2.
168///
169/// See also:
170/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md#engine_getpayloadv2>
171#[derive(Clone, Debug, PartialEq, Eq)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
174#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
175pub struct ExecutionPayloadEnvelopeV2 {
176    /// Execution payload, which could be either V1 or V2
177    ///
178    /// V1 (_NO_ withdrawals) MUST be returned if the payload timestamp is lower than the Shanghai
179    /// timestamp
180    ///
181    /// V2 (_WITH_ withdrawals) MUST be returned if the payload timestamp is greater or equal to
182    /// the Shanghai timestamp
183    pub execution_payload: ExecutionPayloadFieldV2,
184    /// The expected value to be received by the feeRecipient in wei
185    pub block_value: U256,
186}
187
188impl ExecutionPayloadEnvelopeV2 {
189    /// Returns the [ExecutionPayload] for the `engine_getPayloadV1` endpoint
190    pub fn into_v1_payload(self) -> ExecutionPayloadV1 {
191        self.execution_payload.into_v1_payload()
192    }
193}
194
195/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
196/// V3.
197///
198/// See also:
199/// <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#response-2>
200#[derive(Clone, Debug, PartialEq, Eq)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
202#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
203#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
204pub struct ExecutionPayloadEnvelopeV3 {
205    /// Execution payload V3
206    pub execution_payload: ExecutionPayloadV3,
207    /// The expected value to be received by the feeRecipient in wei
208    pub block_value: U256,
209    /// The blobs, commitments, and proofs associated with the executed payload.
210    pub blobs_bundle: BlobsBundleV1,
211    /// Introduced in V3, this represents a suggestion from the execution layer if the payload
212    /// should be used instead of an externally provided one.
213    pub should_override_builder: bool,
214}
215
216/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
217/// V4.
218///
219/// See also:
220/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md#engine_getpayloadv4>
221#[derive(Clone, Debug, PartialEq, Eq, derive_more::Deref, derive_more::DerefMut)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
223#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
224#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
225pub struct ExecutionPayloadEnvelopeV4 {
226    /// Inner [`ExecutionPayloadEnvelopeV3`].
227    #[deref]
228    #[deref_mut]
229    #[cfg_attr(feature = "serde", serde(flatten))]
230    pub envelope_inner: ExecutionPayloadEnvelopeV3,
231
232    /// A list of opaque [EIP-7685][eip7685] requests.
233    ///
234    /// [eip7685]: https://eips.ethereum.org/EIPS/eip-7685
235    pub execution_requests: Requests,
236}
237
238/// This structure maps for the return value of `engine_getPayload` of the beacon chain spec, for
239/// V5.
240///
241/// See also:
242/// <https://github.com/ethereum/execution-apis/blob/a091e7c3b6a5748a8843a1a9130d5fbfc3191a2c/src/engine/osaka.md#engine_getpayloadv5>
243#[derive(Clone, Debug, PartialEq, Eq)]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
246#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
247pub struct ExecutionPayloadEnvelopeV5 {
248    /// Execution payload V3
249    pub execution_payload: ExecutionPayloadV3,
250    /// The expected value to be received by the feeRecipient in wei
251    pub block_value: U256,
252    /// The blobs, commitments, and EIP-7594 style cell proofs associated with the executed
253    /// payload. See also: <https://github.com/ethereum/execution-apis/blob/a091e7c3b6a5748a8843a1a9130d5fbfc3191a2c/src/engine/osaka.md#BlobsBundleV2>.
254    pub blobs_bundle: BlobsBundleV2,
255    /// Introduced in V3, this represents a suggestion from the execution layer if the payload
256    /// should be used instead of an externally provided one.
257    pub should_override_builder: bool,
258    /// A list of opaque [EIP-7685][eip7685] requests.
259    ///
260    /// [eip7685]: https://eips.ethereum.org/EIPS/eip-7685
261    pub execution_requests: Requests,
262}
263
264/// This structure maps on the ExecutionPayload structure of the beacon chain spec.
265///
266/// See also: <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/paris.md#executionpayloadv1>
267#[derive(Clone, Debug, PartialEq, Eq)]
268#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
269#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
270#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
271#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
272pub struct ExecutionPayloadV1 {
273    /// The parent hash of the block.
274    pub parent_hash: B256,
275    /// The fee recipient of the block.
276    pub fee_recipient: Address,
277    /// The state root of the block.
278    pub state_root: B256,
279    /// The receipts root of the block.
280    pub receipts_root: B256,
281    /// The logs bloom of the block.
282    pub logs_bloom: Bloom,
283    /// The previous randao of the block.
284    pub prev_randao: B256,
285    /// The block number.
286    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
287    pub block_number: u64,
288    /// The gas limit of the block.
289    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
290    pub gas_limit: u64,
291    /// The gas used of the block.
292    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
293    pub gas_used: u64,
294    /// The timestamp of the block.
295    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
296    pub timestamp: u64,
297    /// The extra data of the block.
298    pub extra_data: Bytes,
299    /// The base fee per gas of the block.
300    pub base_fee_per_gas: U256,
301    /// The block hash of the block.
302    pub block_hash: B256,
303    /// The transactions of the block.
304    pub transactions: Vec<Bytes>,
305}
306
307impl ExecutionPayloadV1 {
308    /// Returns the block number and hash as a [`BlockNumHash`].
309    pub const fn block_num_hash(&self) -> BlockNumHash {
310        BlockNumHash::new(self.block_number, self.block_hash)
311    }
312
313    /// Converts [`ExecutionPayloadV1`] to [`Block`]
314    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
315        self.try_into_block_with(|tx| {
316            T::decode_2718_exact(tx.as_ref())
317                .map_err(alloy_rlp::Error::from)
318                .map_err(PayloadError::from)
319        })
320    }
321
322    /// Converts [`ExecutionPayloadV1`] to [`Block`] with the given closure.
323    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
324    where
325        F: FnMut(Bytes) -> Result<T, E>,
326        E: Into<PayloadError>,
327    {
328        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
329    }
330
331    /// Converts [`ExecutionPayloadV1`] to [`Block`] with raw [`Bytes`] transactions.
332    ///
333    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
334    /// without any conversion.
335    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
336        if self.extra_data.len() > MAXIMUM_EXTRA_DATA_SIZE {
337            return Err(PayloadError::ExtraData(self.extra_data));
338        }
339
340        // Calculate the transactions root using encoded bytes
341        let transactions_root =
342            alloy_consensus::proofs::ordered_trie_root_encoded(&self.transactions);
343
344        let header = Header {
345            parent_hash: self.parent_hash,
346            beneficiary: self.fee_recipient,
347            state_root: self.state_root,
348            transactions_root,
349            receipts_root: self.receipts_root,
350            withdrawals_root: None,
351            logs_bloom: self.logs_bloom,
352            number: self.block_number,
353            gas_limit: self.gas_limit,
354            gas_used: self.gas_used,
355            timestamp: self.timestamp,
356            mix_hash: self.prev_randao,
357            // WARNING: It's allowed for a base fee in EIP1559 to increase unbounded. We assume that
358            // it will fit in an u64. This is not always necessarily true, although it is extremely
359            // unlikely not to be the case, a u64 maximum would have 2^64 which equates to 18 ETH
360            // per gas.
361            base_fee_per_gas: Some(
362                self.base_fee_per_gas
363                    .try_into()
364                    .map_err(|_| PayloadError::BaseFee(self.base_fee_per_gas))?,
365            ),
366            blob_gas_used: None,
367            excess_blob_gas: None,
368            parent_beacon_block_root: None,
369            requests_hash: None,
370            extra_data: self.extra_data,
371            // Defaults
372            ommers_hash: EMPTY_OMMER_ROOT_HASH,
373            difficulty: Default::default(),
374            nonce: Default::default(),
375        };
376
377        Ok(Block {
378            header,
379            body: BlockBody { transactions: self.transactions, ommers: vec![], withdrawals: None },
380        })
381    }
382
383    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV1`].
384    ///
385    /// Note: This re-calculates the block hash.
386    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
387    where
388        T: Encodable2718,
389        H: BlockHeader + Sealable,
390    {
391        Self::from_block_unchecked(block.header.hash_slow(), block)
392    }
393
394    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV1`] using the given block hash.
395    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
396    where
397        T: Encodable2718,
398        H: BlockHeader,
399    {
400        let transactions =
401            block.body.transactions().map(|tx| tx.encoded_2718().into()).collect::<Vec<_>>();
402        Self {
403            parent_hash: block.parent_hash(),
404            fee_recipient: block.beneficiary(),
405            state_root: block.state_root(),
406            receipts_root: block.receipts_root(),
407            logs_bloom: block.logs_bloom(),
408            prev_randao: block.mix_hash().unwrap_or_default(),
409            block_number: block.number(),
410            gas_limit: block.gas_limit(),
411            gas_used: block.gas_used(),
412            timestamp: block.timestamp(),
413            base_fee_per_gas: U256::from(block.base_fee_per_gas().unwrap_or_default()),
414            extra_data: block.header.extra_data().clone(),
415            block_hash,
416            transactions,
417        }
418    }
419
420    /// Calculate base fee for next block according to the EIP-1559 spec.
421    ///
422    /// Returns a `None` if no base fee is set, no EIP-1559 support
423    pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option<u64> {
424        Some(calc_next_block_base_fee(
425            self.gas_used,
426            self.gas_limit,
427            self.base_fee_per_gas.try_into().ok()?,
428            base_fee_params,
429        ))
430    }
431}
432
433impl<T: Decodable2718> TryFrom<ExecutionPayloadV1> for Block<T> {
434    type Error = PayloadError;
435
436    fn try_from(value: ExecutionPayloadV1) -> Result<Self, Self::Error> {
437        value.try_into_block()
438    }
439}
440
441/// This structure maps on the ExecutionPayloadV2 structure of the beacon chain spec.
442///
443/// See also: <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/shanghai.md#executionpayloadv2>
444#[derive(Clone, Debug, PartialEq, Eq)]
445#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
446#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
447#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
448pub struct ExecutionPayloadV2 {
449    /// Inner V1 payload
450    #[cfg_attr(feature = "serde", serde(flatten))]
451    pub payload_inner: ExecutionPayloadV1,
452
453    /// Array of [`Withdrawal`] enabled with V2
454    /// See <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/shanghai.md#executionpayloadv2>
455    pub withdrawals: Vec<Withdrawal>,
456}
457
458impl ExecutionPayloadV2 {
459    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV2`].
460    ///
461    /// See also [`ExecutionPayloadV1::from_block_unchecked`].
462    ///
463    /// If the block does not have any withdrawals, an empty vector is used.
464    ///
465    /// Note: This re-calculates the block hash.
466    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
467    where
468        T: Encodable2718,
469        H: BlockHeader + Sealable,
470    {
471        Self::from_block_unchecked(block.header.hash_slow(), block)
472    }
473
474    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV2`] using the given block hash.
475    ///
476    /// See also [`ExecutionPayloadV1::from_block_unchecked`].
477    ///
478    /// If the block does not have any withdrawals, an empty vector is used.
479    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
480    where
481        T: Encodable2718,
482        H: BlockHeader,
483    {
484        Self {
485            withdrawals: block
486                .body
487                .withdrawals
488                .clone()
489                .map(Withdrawals::into_inner)
490                .unwrap_or_default(),
491            payload_inner: ExecutionPayloadV1::from_block_unchecked(block_hash, block),
492        }
493    }
494
495    /// Returns the timestamp for the execution payload.
496    pub const fn timestamp(&self) -> u64 {
497        self.payload_inner.timestamp
498    }
499
500    /// Converts [`ExecutionPayloadV2`] to [`ExecutionPayloadInputV2`].
501    ///
502    /// An [`ExecutionPayloadInputV2`] should have a [`Some`] withdrawals field if shanghai is
503    /// active, otherwise the withdrawals field should be [`None`], so the `is_shanghai_active`
504    /// argument is provided which will either:
505    /// - include the withdrawals field as [`Some`] if true
506    /// - set the withdrawals field to [`None`] if false
507    pub fn into_payload_input_v2(self, is_shanghai_active: bool) -> ExecutionPayloadInputV2 {
508        ExecutionPayloadInputV2 {
509            execution_payload: self.payload_inner,
510            withdrawals: is_shanghai_active.then_some(self.withdrawals),
511        }
512    }
513
514    /// Converts [`ExecutionPayloadV2`] to [`Block`].
515    ///
516    /// This performs the same conversion as the underlying V1 payload, but calculates the
517    /// withdrawals root and adds withdrawals.
518    ///
519    /// See also [`ExecutionPayloadV1::try_into_block`].
520    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
521        self.try_into_block_with(|tx| {
522            T::decode_2718_exact(tx.as_ref())
523                .map_err(alloy_rlp::Error::from)
524                .map_err(PayloadError::from)
525        })
526    }
527
528    /// Converts [`ExecutionPayloadV2`] to [`Block`] with a custom transaction mapper.
529    ///
530    /// See also [`ExecutionPayloadV1::try_into_block_with`].
531    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
532    where
533        F: FnMut(Bytes) -> Result<T, E>,
534        E: Into<PayloadError>,
535    {
536        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
537    }
538
539    /// Converts [`ExecutionPayloadV2`] to [`Block`] with raw [`Bytes`] transactions.
540    ///
541    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
542    /// without any conversion.
543    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
544        let mut base_sealed_block = self.payload_inner.into_block_raw()?;
545        let withdrawals_root =
546            alloy_consensus::proofs::calculate_withdrawals_root(&self.withdrawals);
547        base_sealed_block.body.withdrawals = Some(self.withdrawals.into());
548        base_sealed_block.header.withdrawals_root = Some(withdrawals_root);
549        Ok(base_sealed_block)
550    }
551}
552
553impl<T: Decodable2718> TryFrom<ExecutionPayloadV2> for Block<T> {
554    type Error = PayloadError;
555
556    fn try_from(value: ExecutionPayloadV2) -> Result<Self, Self::Error> {
557        value.try_into_block()
558    }
559}
560
561#[cfg(feature = "ssz")]
562impl ssz::Decode for ExecutionPayloadV2 {
563    fn is_ssz_fixed_len() -> bool {
564        false
565    }
566
567    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
568        let mut builder = ssz::SszDecoderBuilder::new(bytes);
569
570        builder.register_type::<B256>()?;
571        builder.register_type::<Address>()?;
572        builder.register_type::<B256>()?;
573        builder.register_type::<B256>()?;
574        builder.register_type::<Bloom>()?;
575        builder.register_type::<B256>()?;
576        builder.register_type::<u64>()?;
577        builder.register_type::<u64>()?;
578        builder.register_type::<u64>()?;
579        builder.register_type::<u64>()?;
580        builder.register_type::<Bytes>()?;
581        builder.register_type::<U256>()?;
582        builder.register_type::<B256>()?;
583        builder.register_type::<Vec<Bytes>>()?;
584        builder.register_type::<Vec<Withdrawal>>()?;
585
586        let mut decoder = builder.build()?;
587
588        Ok(Self {
589            payload_inner: ExecutionPayloadV1 {
590                parent_hash: decoder.decode_next()?,
591                fee_recipient: decoder.decode_next()?,
592                state_root: decoder.decode_next()?,
593                receipts_root: decoder.decode_next()?,
594                logs_bloom: decoder.decode_next()?,
595                prev_randao: decoder.decode_next()?,
596                block_number: decoder.decode_next()?,
597                gas_limit: decoder.decode_next()?,
598                gas_used: decoder.decode_next()?,
599                timestamp: decoder.decode_next()?,
600                extra_data: decoder.decode_next()?,
601                base_fee_per_gas: decoder.decode_next()?,
602                block_hash: decoder.decode_next()?,
603                transactions: decoder.decode_next()?,
604            },
605            withdrawals: decoder.decode_next()?,
606        })
607    }
608}
609
610#[cfg(feature = "ssz")]
611impl ssz::Encode for ExecutionPayloadV2 {
612    fn is_ssz_fixed_len() -> bool {
613        false
614    }
615
616    fn ssz_append(&self, buf: &mut Vec<u8>) {
617        let offset = <B256 as ssz::Encode>::ssz_fixed_len() * 5
618            + <Address as ssz::Encode>::ssz_fixed_len()
619            + <Bloom as ssz::Encode>::ssz_fixed_len()
620            + <u64 as ssz::Encode>::ssz_fixed_len() * 4
621            + <U256 as ssz::Encode>::ssz_fixed_len()
622            + ssz::BYTES_PER_LENGTH_OFFSET * 3;
623
624        let mut encoder = ssz::SszEncoder::container(buf, offset);
625
626        encoder.append(&self.payload_inner.parent_hash);
627        encoder.append(&self.payload_inner.fee_recipient);
628        encoder.append(&self.payload_inner.state_root);
629        encoder.append(&self.payload_inner.receipts_root);
630        encoder.append(&self.payload_inner.logs_bloom);
631        encoder.append(&self.payload_inner.prev_randao);
632        encoder.append(&self.payload_inner.block_number);
633        encoder.append(&self.payload_inner.gas_limit);
634        encoder.append(&self.payload_inner.gas_used);
635        encoder.append(&self.payload_inner.timestamp);
636        encoder.append(&self.payload_inner.extra_data);
637        encoder.append(&self.payload_inner.base_fee_per_gas);
638        encoder.append(&self.payload_inner.block_hash);
639        encoder.append(&self.payload_inner.transactions);
640        encoder.append(&self.withdrawals);
641
642        encoder.finalize();
643    }
644
645    fn ssz_bytes_len(&self) -> usize {
646        <ExecutionPayloadV1 as ssz::Encode>::ssz_bytes_len(&self.payload_inner)
647            + ssz::BYTES_PER_LENGTH_OFFSET
648            + self.withdrawals.ssz_bytes_len()
649    }
650}
651
652/// This structure maps on the ExecutionPayloadV3 structure of the beacon chain spec.
653///
654/// See also: <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#executionpayloadv3>
655#[derive(Clone, Debug, PartialEq, Eq)]
656#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
657#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
658#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
659pub struct ExecutionPayloadV3 {
660    /// Inner V2 payload
661    #[cfg_attr(feature = "serde", serde(flatten))]
662    pub payload_inner: ExecutionPayloadV2,
663
664    /// Array of hex [`u64`] representing blob gas used, enabled with V3
665    /// See <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#ExecutionPayloadV3>
666    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
667    pub blob_gas_used: u64,
668    /// Array of hex [`u64`] representing excess blob gas, enabled with V3
669    /// See <https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md#ExecutionPayloadV3>
670    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
671    pub excess_blob_gas: u64,
672}
673
674impl ExecutionPayloadV3 {
675    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV3`].
676    ///
677    /// See also [`ExecutionPayloadV2::from_block_unchecked`].
678    ///
679    /// Note: This re-calculates the block hash.
680    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> Self
681    where
682        T: Encodable2718,
683        H: BlockHeader + Sealable,
684    {
685        Self::from_block_unchecked(block.hash_slow(), block)
686    }
687
688    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayloadV3`] using the given block hash.
689    ///
690    /// See also [`ExecutionPayloadV2::from_block_unchecked`].
691    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
692    where
693        T: Encodable2718,
694        H: BlockHeader,
695    {
696        Self {
697            blob_gas_used: block.blob_gas_used().unwrap_or_default(),
698            excess_blob_gas: block.excess_blob_gas().unwrap_or_default(),
699            payload_inner: ExecutionPayloadV2::from_block_unchecked(block_hash, block),
700        }
701    }
702
703    /// Returns the withdrawals for the payload.
704    pub const fn withdrawals(&self) -> &Vec<Withdrawal> {
705        &self.payload_inner.withdrawals
706    }
707
708    /// Returns the timestamp for the payload.
709    pub const fn timestamp(&self) -> u64 {
710        self.payload_inner.payload_inner.timestamp
711    }
712
713    /// Converts [`ExecutionPayloadV3`] to [`Block`].
714    ///
715    /// This performs the same conversion as the underlying V2 payload, but inserts the blob gas
716    /// used and excess blob gas.
717    ///
718    /// See also [`ExecutionPayloadV2::try_into_block`].
719    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
720        self.try_into_block_with(|tx| {
721            T::decode_2718_exact(tx.as_ref())
722                .map_err(alloy_rlp::Error::from)
723                .map_err(PayloadError::from)
724        })
725    }
726
727    /// Converts [`ExecutionPayloadV3`] to [`Block`] with a custom transaction mapper.
728    ///
729    /// See also [`ExecutionPayloadV2::try_into_block_with`].
730    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
731    where
732        F: FnMut(Bytes) -> Result<T, E>,
733        E: Into<PayloadError>,
734    {
735        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
736    }
737
738    /// Converts [`ExecutionPayloadV3`] to [`Block`] with raw [`Bytes`] transactions.
739    ///
740    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
741    /// without any conversion.
742    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
743        let mut base_block = self.payload_inner.into_block_raw()?;
744
745        base_block.header.blob_gas_used = Some(self.blob_gas_used);
746        base_block.header.excess_blob_gas = Some(self.excess_blob_gas);
747
748        Ok(base_block)
749    }
750}
751
752impl<T: Decodable2718> TryFrom<ExecutionPayloadV3> for Block<T> {
753    type Error = PayloadError;
754
755    fn try_from(value: ExecutionPayloadV3) -> Result<Self, Self::Error> {
756        value.try_into_block()
757    }
758}
759
760#[cfg(feature = "ssz")]
761impl ssz::Decode for ExecutionPayloadV3 {
762    fn is_ssz_fixed_len() -> bool {
763        false
764    }
765
766    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
767        let mut builder = ssz::SszDecoderBuilder::new(bytes);
768
769        builder.register_type::<B256>()?;
770        builder.register_type::<Address>()?;
771        builder.register_type::<B256>()?;
772        builder.register_type::<B256>()?;
773        builder.register_type::<Bloom>()?;
774        builder.register_type::<B256>()?;
775        builder.register_type::<u64>()?;
776        builder.register_type::<u64>()?;
777        builder.register_type::<u64>()?;
778        builder.register_type::<u64>()?;
779        builder.register_type::<Bytes>()?;
780        builder.register_type::<U256>()?;
781        builder.register_type::<B256>()?;
782        builder.register_type::<Vec<Bytes>>()?;
783        builder.register_type::<Vec<Withdrawal>>()?;
784        builder.register_type::<u64>()?;
785        builder.register_type::<u64>()?;
786
787        let mut decoder = builder.build()?;
788
789        Ok(Self {
790            payload_inner: ExecutionPayloadV2 {
791                payload_inner: ExecutionPayloadV1 {
792                    parent_hash: decoder.decode_next()?,
793                    fee_recipient: decoder.decode_next()?,
794                    state_root: decoder.decode_next()?,
795                    receipts_root: decoder.decode_next()?,
796                    logs_bloom: decoder.decode_next()?,
797                    prev_randao: decoder.decode_next()?,
798                    block_number: decoder.decode_next()?,
799                    gas_limit: decoder.decode_next()?,
800                    gas_used: decoder.decode_next()?,
801                    timestamp: decoder.decode_next()?,
802                    extra_data: decoder.decode_next()?,
803                    base_fee_per_gas: decoder.decode_next()?,
804                    block_hash: decoder.decode_next()?,
805                    transactions: decoder.decode_next()?,
806                },
807                withdrawals: decoder.decode_next()?,
808            },
809            blob_gas_used: decoder.decode_next()?,
810            excess_blob_gas: decoder.decode_next()?,
811        })
812    }
813}
814
815#[cfg(feature = "ssz")]
816impl ssz::Encode for ExecutionPayloadV3 {
817    fn is_ssz_fixed_len() -> bool {
818        false
819    }
820
821    fn ssz_append(&self, buf: &mut Vec<u8>) {
822        let offset = <B256 as ssz::Encode>::ssz_fixed_len() * 5
823            + <Address as ssz::Encode>::ssz_fixed_len()
824            + <Bloom as ssz::Encode>::ssz_fixed_len()
825            + <u64 as ssz::Encode>::ssz_fixed_len() * 6
826            + <U256 as ssz::Encode>::ssz_fixed_len()
827            + ssz::BYTES_PER_LENGTH_OFFSET * 3;
828
829        let mut encoder = ssz::SszEncoder::container(buf, offset);
830
831        encoder.append(&self.payload_inner.payload_inner.parent_hash);
832        encoder.append(&self.payload_inner.payload_inner.fee_recipient);
833        encoder.append(&self.payload_inner.payload_inner.state_root);
834        encoder.append(&self.payload_inner.payload_inner.receipts_root);
835        encoder.append(&self.payload_inner.payload_inner.logs_bloom);
836        encoder.append(&self.payload_inner.payload_inner.prev_randao);
837        encoder.append(&self.payload_inner.payload_inner.block_number);
838        encoder.append(&self.payload_inner.payload_inner.gas_limit);
839        encoder.append(&self.payload_inner.payload_inner.gas_used);
840        encoder.append(&self.payload_inner.payload_inner.timestamp);
841        encoder.append(&self.payload_inner.payload_inner.extra_data);
842        encoder.append(&self.payload_inner.payload_inner.base_fee_per_gas);
843        encoder.append(&self.payload_inner.payload_inner.block_hash);
844        encoder.append(&self.payload_inner.payload_inner.transactions);
845        encoder.append(&self.payload_inner.withdrawals);
846        encoder.append(&self.blob_gas_used);
847        encoder.append(&self.excess_blob_gas);
848
849        encoder.finalize();
850    }
851
852    fn ssz_bytes_len(&self) -> usize {
853        <ExecutionPayloadV2 as ssz::Encode>::ssz_bytes_len(&self.payload_inner)
854            + <u64 as ssz::Encode>::ssz_fixed_len() * 2
855    }
856}
857
858/// This includes all bundled blob related data of an executed payload.
859#[derive(Clone, Debug, Default, PartialEq, Eq)]
860#[cfg_attr(feature = "serde", derive(serde::Serialize))]
861#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode, ssz_derive::Decode))]
862#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
863pub struct BlobsBundleV1 {
864    /// All commitments in the bundle.
865    pub commitments: Vec<alloy_consensus::Bytes48>,
866    /// All proofs in the bundle.
867    pub proofs: Vec<alloy_consensus::Bytes48>,
868    /// All blobs in the bundle.
869    pub blobs: Vec<alloy_consensus::Blob>,
870}
871
872#[cfg(feature = "serde")]
873impl<'de> serde::Deserialize<'de> for BlobsBundleV1 {
874    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
875    where
876        D: serde::Deserializer<'de>,
877    {
878        #[derive(serde::Deserialize)]
879        struct BlobsBundleRaw {
880            commitments: Vec<alloy_consensus::Bytes48>,
881            proofs: Vec<alloy_consensus::Bytes48>,
882            blobs: Vec<alloy_consensus::Blob>,
883        }
884        let raw = BlobsBundleRaw::deserialize(deserializer)?;
885
886        if raw.proofs.len() == raw.commitments.len() && raw.proofs.len() == raw.blobs.len() {
887            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
888        } else {
889            Err(serde::de::Error::invalid_length(
890                raw.proofs.len(),
891                &format!("{}", raw.commitments.len()).as_str(),
892            ))
893        }
894    }
895}
896
897impl BlobsBundleV1 {
898    /// Creates a new blob bundle from the given sidecars.
899    ///
900    /// This folds the sidecar fields into single commit, proof, and blob vectors.
901    pub fn new(sidecars: impl IntoIterator<Item = BlobTransactionSidecar>) -> Self {
902        let (commitments, proofs, blobs) = sidecars.into_iter().fold(
903            (Vec::new(), Vec::new(), Vec::new()),
904            |(mut commitments, mut proofs, mut blobs), sidecar| {
905                commitments.extend(sidecar.commitments);
906                proofs.extend(sidecar.proofs);
907                blobs.extend(sidecar.blobs);
908                (commitments, proofs, blobs)
909            },
910        );
911        Self { commitments, proofs, blobs }
912    }
913
914    /// Returns a new empty blobs bundle.
915    ///
916    /// This is useful for the opstack engine API that expects an empty bundle as part of the
917    /// payload for API compatibility reasons.
918    pub fn empty() -> Self {
919        Self::default()
920    }
921
922    /// Take `len` blob data from the bundle.
923    ///
924    /// # Panics
925    ///
926    /// If len is more than the blobs bundle len.
927    pub fn take(&mut self, len: usize) -> (Vec<Bytes48>, Vec<Bytes48>, Vec<Blob>) {
928        (
929            self.commitments.drain(0..len).collect(),
930            self.proofs.drain(0..len).collect(),
931            self.blobs.drain(0..len).collect(),
932        )
933    }
934
935    /// Returns the sidecar from the bundle
936    ///
937    /// # Panics
938    ///
939    /// If len is more than the blobs bundle len.
940    pub fn pop_sidecar(&mut self, len: usize) -> BlobTransactionSidecar {
941        let (commitments, proofs, blobs) = self.take(len);
942        BlobTransactionSidecar { commitments, proofs, blobs }
943    }
944
945    /// Converts this bundle into a single [`BlobTransactionSidecar`].
946    ///
947    /// Returns an error if the bundle doesn't contain the same number of commitments as blobs and
948    /// proofs.
949    ///
950    /// Returns an empty [`BlobTransactionSidecar`] if the bundle is empty.
951    #[cfg(feature = "kzg")]
952    pub fn try_into_sidecar(
953        self,
954    ) -> Result<BlobTransactionSidecar, alloy_consensus::error::ValueError<Self>> {
955        if self.commitments.len() != self.proofs.len() || self.commitments.len() != self.blobs.len()
956        {
957            return Err(alloy_consensus::error::ValueError::new(self, "length mismatch"));
958        }
959
960        let Self { commitments, proofs, blobs } = self;
961        Ok(BlobTransactionSidecar { blobs, commitments, proofs })
962    }
963}
964
965impl From<Vec<BlobTransactionSidecar>> for BlobsBundleV1 {
966    fn from(sidecars: Vec<BlobTransactionSidecar>) -> Self {
967        Self::new(sidecars)
968    }
969}
970
971impl FromIterator<BlobTransactionSidecar> for BlobsBundleV1 {
972    fn from_iter<T: IntoIterator<Item = BlobTransactionSidecar>>(iter: T) -> Self {
973        Self::new(iter)
974    }
975}
976
977#[cfg(feature = "kzg")]
978impl TryFrom<BlobsBundleV1> for BlobTransactionSidecar {
979    type Error = alloy_consensus::error::ValueError<BlobsBundleV1>;
980
981    fn try_from(value: BlobsBundleV1) -> Result<Self, Self::Error> {
982        value.try_into_sidecar()
983    }
984}
985
986/// This includes all bundled blob related data of an executed payload.
987#[derive(Clone, Debug, Default, PartialEq, Eq)]
988#[cfg_attr(feature = "serde", derive(serde::Serialize))]
989#[cfg_attr(feature = "ssz", derive(ssz_derive::Encode))]
990#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
991pub struct BlobsBundleV2 {
992    /// All commitments in the bundle.
993    pub commitments: Vec<alloy_consensus::Bytes48>,
994    /// All cell proofs in the bundle.
995    pub proofs: Vec<alloy_consensus::Bytes48>,
996    /// All blobs in the bundle.
997    pub blobs: Vec<alloy_consensus::Blob>,
998}
999
1000#[cfg(feature = "serde")]
1001impl<'de> serde::Deserialize<'de> for BlobsBundleV2 {
1002    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1003    where
1004        D: serde::Deserializer<'de>,
1005    {
1006        #[derive(serde::Deserialize)]
1007        struct BlobsBundleRaw {
1008            commitments: Vec<alloy_consensus::Bytes48>,
1009            proofs: Vec<alloy_consensus::Bytes48>,
1010            blobs: Vec<alloy_consensus::Blob>,
1011        }
1012        let raw = BlobsBundleRaw::deserialize(deserializer)?;
1013
1014        if raw.proofs.len() == raw.blobs.len() * CELLS_PER_EXT_BLOB
1015            && raw.commitments.len() == raw.blobs.len()
1016        {
1017            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
1018        } else {
1019            Err(serde::de::Error::invalid_length(
1020                raw.proofs.len(),
1021                &format!("{}", raw.commitments.len() * CELLS_PER_EXT_BLOB).as_str(),
1022            ))
1023        }
1024    }
1025}
1026
1027#[cfg(feature = "ssz")]
1028impl ssz::Decode for BlobsBundleV2 {
1029    fn is_ssz_fixed_len() -> bool {
1030        false
1031    }
1032
1033    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1034        #[derive(ssz_derive::Decode)]
1035        struct BlobsBundleRaw {
1036            commitments: Vec<alloy_consensus::Bytes48>,
1037            proofs: Vec<alloy_consensus::Bytes48>,
1038            blobs: Vec<alloy_consensus::Blob>,
1039        }
1040
1041        let raw = BlobsBundleRaw::from_ssz_bytes(bytes)?;
1042
1043        if raw.proofs.len() == raw.blobs.len() * CELLS_PER_EXT_BLOB
1044            && raw.commitments.len() == raw.blobs.len()
1045        {
1046            Ok(Self { commitments: raw.commitments, proofs: raw.proofs, blobs: raw.blobs })
1047        } else {
1048            Err(ssz::DecodeError::BytesInvalid(
1049                format!(
1050                    "Invalid BlobsBundleV2: expected {} proofs and {} commitments for {} blobs, got {} proofs and {} commitments",
1051                    raw.blobs.len() * CELLS_PER_EXT_BLOB,
1052                    raw.blobs.len(),
1053                    raw.blobs.len(),
1054                    raw.proofs.len(),
1055                    raw.commitments.len()
1056                )
1057            ))
1058        }
1059    }
1060}
1061
1062impl BlobsBundleV2 {
1063    /// Creates a new blob bundle from the given sidecars.
1064    ///
1065    /// This folds the sidecar fields into single commit, proof, and blob vectors.
1066    pub fn new(sidecars: impl IntoIterator<Item = BlobTransactionSidecarEip7594>) -> Self {
1067        let (commitments, proofs, blobs) = sidecars.into_iter().fold(
1068            (Vec::new(), Vec::new(), Vec::new()),
1069            |(mut commitments, mut proofs, mut blobs), sidecar| {
1070                commitments.extend(sidecar.commitments);
1071                proofs.extend(sidecar.cell_proofs);
1072                blobs.extend(sidecar.blobs);
1073                (commitments, proofs, blobs)
1074            },
1075        );
1076        Self { commitments, proofs, blobs }
1077    }
1078
1079    /// Returns a new empty blobs bundle.
1080    ///
1081    /// This is useful for the opstack engine API that expects an empty bundle as part of the
1082    /// payload for API compatibility reasons.
1083    pub fn empty() -> Self {
1084        Self::default()
1085    }
1086
1087    /// Take `len` blob data from the bundle.
1088    ///
1089    /// Note this will take `len * CELLS_PER_EXT_BLOB` proofs.
1090    ///
1091    /// # Panics
1092    ///
1093    /// If len is more than the blobs bundle len.
1094    pub fn take(&mut self, len: usize) -> (Vec<Bytes48>, Vec<Bytes48>, Vec<Blob>) {
1095        (
1096            self.commitments.drain(0..len).collect(),
1097            self.proofs.drain(0..len * CELLS_PER_EXT_BLOB).collect(),
1098            self.blobs.drain(0..len).collect(),
1099        )
1100    }
1101
1102    /// Returns the sidecar from the bundle
1103    ///
1104    /// # Panics
1105    ///
1106    /// If len is more than the blobs bundle len.
1107    pub fn pop_sidecar(&mut self, len: usize) -> BlobTransactionSidecarEip7594 {
1108        let (commitments, cell_proofs, blobs) = self.take(len);
1109        BlobTransactionSidecarEip7594 { commitments, cell_proofs, blobs }
1110    }
1111
1112    /// Converts this bundle into a single [`BlobTransactionSidecarEip7594`].
1113    ///
1114    /// Returns an error if the bundle doesn't contain the correct number of cell proofs
1115    /// (expected blobs.len() * CELLS_PER_EXT_BLOB) or if the commitments length doesn't
1116    /// match the blobs length.
1117    ///
1118    /// Returns an empty [`BlobTransactionSidecarEip7594`] if the bundle is empty.
1119    #[cfg(feature = "kzg")]
1120    pub fn try_into_sidecar(
1121        self,
1122    ) -> Result<BlobTransactionSidecarEip7594, alloy_consensus::error::ValueError<Self>> {
1123        let expected_cell_proofs_len = self.blobs.len() * CELLS_PER_EXT_BLOB;
1124        if self.proofs.len() != expected_cell_proofs_len {
1125            let msg = format!(
1126                "cell proofs length mismatch, expected {expected_cell_proofs_len}, has {}",
1127                self.proofs.len()
1128            );
1129            return Err(alloy_consensus::error::ValueError::new(self, msg));
1130        }
1131
1132        if self.commitments.len() != self.blobs.len() {
1133            let msg = format!(
1134                "commitments length ({}) mismatch, expected blob length ({})",
1135                self.commitments.len(),
1136                self.blobs.len()
1137            );
1138            return Err(alloy_consensus::error::ValueError::new(self, msg));
1139        }
1140
1141        let Self { commitments, proofs, blobs } = self;
1142        Ok(BlobTransactionSidecarEip7594 { blobs, commitments, cell_proofs: proofs })
1143    }
1144}
1145
1146impl From<Vec<BlobTransactionSidecarEip7594>> for BlobsBundleV2 {
1147    fn from(sidecars: Vec<BlobTransactionSidecarEip7594>) -> Self {
1148        Self::new(sidecars)
1149    }
1150}
1151
1152impl FromIterator<BlobTransactionSidecarEip7594> for BlobsBundleV2 {
1153    fn from_iter<T: IntoIterator<Item = BlobTransactionSidecarEip7594>>(iter: T) -> Self {
1154        Self::new(iter)
1155    }
1156}
1157
1158#[cfg(feature = "kzg")]
1159impl TryFrom<BlobsBundleV2> for BlobTransactionSidecarEip7594 {
1160    type Error = alloy_consensus::error::ValueError<BlobsBundleV2>;
1161
1162    fn try_from(value: BlobsBundleV2) -> Result<Self, Self::Error> {
1163        value.try_into_sidecar()
1164    }
1165}
1166
1167/// An execution payload, which can be either [ExecutionPayloadV1], [ExecutionPayloadV2], or
1168/// [ExecutionPayloadV3].
1169#[derive(Clone, Debug, PartialEq, Eq)]
1170#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1171#[cfg_attr(feature = "serde", serde(untagged))]
1172#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1173pub enum ExecutionPayload {
1174    /// V1 payload
1175    V1(ExecutionPayloadV1),
1176    /// V2 payload
1177    V2(ExecutionPayloadV2),
1178    /// V3 payload
1179    V3(ExecutionPayloadV3),
1180}
1181
1182impl ExecutionPayload {
1183    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
1184    /// [`ExecutionPayloadSidecar`] extracted from the block.
1185    ///
1186    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
1187    /// See also [`ExecutionPayloadSidecar::from_block`].
1188    ///
1189    /// Note: This re-calculates the block hash.
1190    pub fn from_block_slow<T, H>(block: &Block<T, H>) -> (Self, ExecutionPayloadSidecar)
1191    where
1192        T: Encodable2718 + Transaction,
1193        H: BlockHeader + Sealable,
1194    {
1195        Self::from_block_unchecked(block.hash_slow(), block)
1196    }
1197
1198    /// Converts [`alloy_consensus::Block`] to [`ExecutionPayload`] and also returns the
1199    /// [`ExecutionPayloadSidecar`] extracted from the block.
1200    ///
1201    /// See also [`ExecutionPayloadV3::from_block_unchecked`].
1202    /// See also [`ExecutionPayloadSidecar::from_block`].
1203    pub fn from_block_unchecked<T, H>(
1204        block_hash: B256,
1205        block: &Block<T, H>,
1206    ) -> (Self, ExecutionPayloadSidecar)
1207    where
1208        T: Encodable2718 + Transaction,
1209        H: BlockHeader,
1210    {
1211        let sidecar = ExecutionPayloadSidecar::from_block(block);
1212
1213        let execution_payload = if block.header.parent_beacon_block_root().is_some() {
1214            // block with parent beacon block root: V3
1215            Self::V3(ExecutionPayloadV3::from_block_unchecked(block_hash, block))
1216        } else if block.body.withdrawals.is_some() {
1217            // block with withdrawals: V2
1218            Self::V2(ExecutionPayloadV2::from_block_unchecked(block_hash, block))
1219        } else {
1220            // otherwise V1
1221            Self::V1(ExecutionPayloadV1::from_block_unchecked(block_hash, block))
1222        };
1223
1224        (execution_payload, sidecar)
1225    }
1226
1227    /// Tries to create a new unsealed block from the given payload and payload sidecar.
1228    ///
1229    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
1230    ///
1231    /// # Note
1232    ///
1233    /// The log bloom is assumed to be validated during serialization.
1234    ///
1235    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
1236    pub fn try_into_block_with_sidecar<T: Decodable2718>(
1237        self,
1238        sidecar: &ExecutionPayloadSidecar,
1239    ) -> Result<Block<T>, PayloadError> {
1240        self.try_into_block_with_sidecar_with(sidecar, |tx| {
1241            T::decode_2718_exact(tx.as_ref())
1242                .map_err(alloy_rlp::Error::from)
1243                .map_err(PayloadError::from)
1244        })
1245    }
1246
1247    /// Converts [`ExecutionPayload`] to [`Block`] with sidecar and a custom transaction mapper.
1248    ///
1249    /// The log bloom is assumed to be validated during serialization.
1250    ///
1251    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
1252    pub fn try_into_block_with_sidecar_with<T, F, E>(
1253        self,
1254        sidecar: &ExecutionPayloadSidecar,
1255        f: F,
1256    ) -> Result<Block<T>, PayloadError>
1257    where
1258        F: FnMut(Bytes) -> Result<T, E>,
1259        E: Into<PayloadError>,
1260    {
1261        self.into_block_with_sidecar_raw(sidecar)?.try_map_transactions(f).map_err(Into::into)
1262    }
1263
1264    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions and sidecar.
1265    ///
1266    /// This is similar to [`Self::try_into_block_with_sidecar_with`] but returns the transactions
1267    /// as raw bytes without any conversion.
1268    pub fn into_block_with_sidecar_raw(
1269        self,
1270        sidecar: &ExecutionPayloadSidecar,
1271    ) -> Result<Block<Bytes>, PayloadError> {
1272        let mut base_block = self.into_block_raw()?;
1273        base_block.header.parent_beacon_block_root = sidecar.parent_beacon_block_root();
1274        base_block.header.requests_hash = sidecar.requests_hash();
1275        Ok(base_block)
1276    }
1277
1278    /// Converts [`ExecutionPayload`] to [`Block`].
1279    ///
1280    /// Caution: This does not set fields that are not part of the payload and only part of the
1281    /// [`ExecutionPayloadSidecar`]:
1282    /// - parent_beacon_block_root
1283    /// - requests_hash
1284    ///
1285    /// See also: [`ExecutionPayload::try_into_block_with_sidecar`]
1286    pub fn try_into_block<T: Decodable2718>(self) -> Result<Block<T>, PayloadError> {
1287        self.try_into_block_with(|tx| {
1288            T::decode_2718_exact(tx.as_ref())
1289                .map_err(alloy_rlp::Error::from)
1290                .map_err(PayloadError::from)
1291        })
1292    }
1293
1294    /// Converts [`ExecutionPayload`] to [`Block`] with a custom transaction mapper.
1295    ///
1296    /// Caution: This does not set fields that are not part of the payload and only part of the
1297    /// [`ExecutionPayloadSidecar`]:
1298    /// - parent_beacon_block_root
1299    /// - requests_hash
1300    ///
1301    /// See also: [`ExecutionPayload::try_into_block_with_sidecar`]
1302    pub fn try_into_block_with<T, F, E>(self, f: F) -> Result<Block<T>, PayloadError>
1303    where
1304        F: FnMut(Bytes) -> Result<T, E>,
1305        E: Into<PayloadError>,
1306    {
1307        self.into_block_raw()?.try_map_transactions(f).map_err(Into::into)
1308    }
1309
1310    /// Converts [`ExecutionPayload`] to [`Block`] with raw [`Bytes`] transactions.
1311    ///
1312    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
1313    /// without any conversion.
1314    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
1315        match self {
1316            Self::V1(payload) => payload.into_block_raw(),
1317            Self::V2(payload) => payload.into_block_raw(),
1318            Self::V3(payload) => payload.into_block_raw(),
1319        }
1320    }
1321
1322    /// Returns a reference to the V1 payload.
1323    pub const fn as_v1(&self) -> &ExecutionPayloadV1 {
1324        match self {
1325            Self::V1(payload) => payload,
1326            Self::V2(payload) => &payload.payload_inner,
1327            Self::V3(payload) => &payload.payload_inner.payload_inner,
1328        }
1329    }
1330
1331    /// Returns a mutable reference to the V1 payload.
1332    pub const fn as_v1_mut(&mut self) -> &mut ExecutionPayloadV1 {
1333        match self {
1334            Self::V1(payload) => payload,
1335            Self::V2(payload) => &mut payload.payload_inner,
1336            Self::V3(payload) => &mut payload.payload_inner.payload_inner,
1337        }
1338    }
1339
1340    /// Consumes the payload and returns the V1 payload.
1341    pub fn into_v1(self) -> ExecutionPayloadV1 {
1342        match self {
1343            Self::V1(payload) => payload,
1344            Self::V2(payload) => payload.payload_inner,
1345            Self::V3(payload) => payload.payload_inner.payload_inner,
1346        }
1347    }
1348
1349    /// Returns a reference to the V2 payload, if any.
1350    pub const fn as_v2(&self) -> Option<&ExecutionPayloadV2> {
1351        match self {
1352            Self::V1(_) => None,
1353            Self::V2(payload) => Some(payload),
1354            Self::V3(payload) => Some(&payload.payload_inner),
1355        }
1356    }
1357
1358    /// Returns a mutable reference to the V2 payload, if any.
1359    pub const fn as_v2_mut(&mut self) -> Option<&mut ExecutionPayloadV2> {
1360        match self {
1361            Self::V1(_) => None,
1362            Self::V2(payload) => Some(payload),
1363            Self::V3(payload) => Some(&mut payload.payload_inner),
1364        }
1365    }
1366
1367    /// Returns a reference to the V3 payload, if any.
1368    pub const fn as_v3(&self) -> Option<&ExecutionPayloadV3> {
1369        match self {
1370            Self::V1(_) | Self::V2(_) => None,
1371            Self::V3(payload) => Some(payload),
1372        }
1373    }
1374
1375    /// Returns a mutable reference to the V3 payload, if any.
1376    pub const fn as_v3_mut(&mut self) -> Option<&mut ExecutionPayloadV3> {
1377        match self {
1378            Self::V1(_) | Self::V2(_) => None,
1379            Self::V3(payload) => Some(payload),
1380        }
1381    }
1382
1383    /// Returns the withdrawals for the payload.
1384    pub const fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
1385        match self.as_v2() {
1386            Some(payload) => Some(&payload.withdrawals),
1387            None => None,
1388        }
1389    }
1390
1391    /// Returns the transactions for the payload.
1392    pub const fn transactions(&self) -> &Vec<Bytes> {
1393        &self.as_v1().transactions
1394    }
1395
1396    /// Returns a mutable reference to the transactions for the payload.
1397    pub const fn transactions_mut(&mut self) -> &mut Vec<Bytes> {
1398        &mut self.as_v1_mut().transactions
1399    }
1400
1401    /// Extracts essential information into one container type.
1402    pub fn header_info(&self) -> HeaderInfo {
1403        HeaderInfo {
1404            number: self.block_number(),
1405            beneficiary: self.fee_recipient(),
1406            timestamp: self.timestamp(),
1407            gas_limit: self.gas_limit(),
1408            base_fee_per_gas: Some(self.saturated_base_fee_per_gas()),
1409            excess_blob_gas: self.excess_blob_gas(),
1410            blob_gas_used: self.blob_gas_used(),
1411            difficulty: U256::ZERO,
1412            mix_hash: Some(self.prev_randao()),
1413        }
1414    }
1415
1416    /// Returns the gas limit for the payload.
1417    ///
1418    /// Note: this returns the u64 saturated base fee, but it is specified as [`U256`].
1419    pub fn saturated_base_fee_per_gas(&self) -> u64 {
1420        self.as_v1().base_fee_per_gas.saturating_to()
1421    }
1422
1423    /// Returns the blob gas used for the payload.
1424    pub fn blob_gas_used(&self) -> Option<u64> {
1425        self.as_v3().map(|payload| payload.blob_gas_used)
1426    }
1427
1428    /// Returns the excess blob gas for the payload.
1429    pub fn excess_blob_gas(&self) -> Option<u64> {
1430        self.as_v3().map(|payload| payload.excess_blob_gas)
1431    }
1432
1433    /// Returns the gas limit for the payload.
1434    pub const fn gas_limit(&self) -> u64 {
1435        self.as_v1().gas_limit
1436    }
1437
1438    /// Returns the fee recipient.
1439    pub const fn fee_recipient(&self) -> Address {
1440        self.as_v1().fee_recipient
1441    }
1442
1443    /// Returns the timestamp for the payload.
1444    pub const fn timestamp(&self) -> u64 {
1445        self.as_v1().timestamp
1446    }
1447
1448    /// Returns the parent hash for the payload.
1449    pub const fn parent_hash(&self) -> B256 {
1450        self.as_v1().parent_hash
1451    }
1452
1453    /// Returns the block hash for the payload.
1454    pub const fn block_hash(&self) -> B256 {
1455        self.as_v1().block_hash
1456    }
1457
1458    /// Returns the block number for this payload.
1459    pub const fn block_number(&self) -> u64 {
1460        self.as_v1().block_number
1461    }
1462
1463    /// Returns the block number for this payload.
1464    pub const fn block_num_hash(&self) -> BlockNumHash {
1465        self.as_v1().block_num_hash()
1466    }
1467
1468    /// Returns the prev randao for this payload.
1469    pub const fn prev_randao(&self) -> B256 {
1470        self.as_v1().prev_randao
1471    }
1472
1473    /// Returns the blob fee for _this_ block according to the EIP-4844 spec.
1474    ///
1475    /// Returns `None` if `excess_blob_gas` is None
1476    pub fn blob_fee(&self, blob_params: BlobParams) -> Option<u128> {
1477        Some(blob_params.calc_blob_fee(self.excess_blob_gas()?))
1478    }
1479
1480    /// Returns the blob fee for the next block according to the EIP-4844 spec.
1481    ///
1482    /// Returns `None` if `excess_blob_gas` is None.
1483    ///
1484    /// See also [Self::next_block_excess_blob_gas]
1485    pub fn next_block_blob_fee(&self, blob_params: BlobParams) -> Option<u128> {
1486        Some(blob_params.calc_blob_fee(self.next_block_excess_blob_gas(blob_params)?))
1487    }
1488
1489    /// Calculate base fee for next block according to the EIP-1559 spec.
1490    ///
1491    /// Returns a `None` if no base fee is set, no EIP-1559 support
1492    pub fn next_block_base_fee(&self, base_fee_params: BaseFeeParams) -> Option<u64> {
1493        self.as_v1().next_block_base_fee(base_fee_params)
1494    }
1495
1496    /// Calculate excess blob gas for the next block according to the EIP-4844
1497    /// spec.
1498    ///
1499    /// Returns a `None` if no excess blob gas is set, no EIP-4844 support
1500    pub fn next_block_excess_blob_gas(&self, blob_params: BlobParams) -> Option<u64> {
1501        Some(blob_params.next_block_excess_blob_gas_osaka(
1502            self.excess_blob_gas()?,
1503            self.blob_gas_used()?,
1504            self.as_v1().base_fee_per_gas.to(),
1505        ))
1506    }
1507
1508    /// Convenience function for [`Self::next_block_excess_blob_gas`] with an optional
1509    /// [`BlobParams`] argument.
1510    ///
1511    /// Returns `None` if the `blob_params` are `None`.
1512    pub fn maybe_next_block_excess_blob_gas(&self, blob_params: Option<BlobParams>) -> Option<u64> {
1513        self.next_block_excess_blob_gas(blob_params?)
1514    }
1515
1516    /// Returns an iterator over the decoded transactions in this payload.
1517    ///
1518    /// This iterator will decode transactions on the fly.
1519    pub fn decoded_transactions<T: Decodable2718>(
1520        &self,
1521    ) -> impl Iterator<Item = Eip2718Result<T>> + '_ {
1522        self.transactions().iter().map(|tx_bytes| T::decode_2718_exact(tx_bytes.as_ref()))
1523    }
1524
1525    /// Returns iterator over decoded transactions with their original encoded bytes.
1526    ///
1527    /// This iterator will decode transactions on the fly and return them with their bytes.
1528    pub fn decoded_transactions_with_encoded<T: Decodable2718>(
1529        &self,
1530    ) -> impl Iterator<Item = Eip2718Result<WithEncoded<T>>> + '_ {
1531        self.transactions().iter().map(|tx_bytes| {
1532            T::decode_2718_exact(tx_bytes.as_ref()).map(|tx| WithEncoded::new(tx_bytes.clone(), tx))
1533        })
1534    }
1535
1536    /// Returns an iterator over the recovered transactions in this payload.
1537    ///
1538    /// This iterator will decode and recover signer addresses for transactions on the fly.
1539    pub fn recovered_transactions<T>(
1540        &self,
1541    ) -> impl Iterator<
1542        Item = Result<
1543            alloy_consensus::transaction::Recovered<T>,
1544            alloy_consensus::crypto::RecoveryError,
1545        >,
1546    > + '_
1547    where
1548        T: Decodable2718 + alloy_consensus::transaction::SignerRecoverable,
1549    {
1550        self.decoded_transactions::<T>().map(|res| {
1551            res.map_err(alloy_consensus::crypto::RecoveryError::from_source)
1552                .and_then(|tx| tx.try_into_recovered())
1553        })
1554    }
1555
1556    /// Returns an iterator over the recovered transactions in this payload with their
1557    /// original encoded bytes.
1558    ///
1559    /// This iterator will decode and recover signer addresses for transactions on the fly
1560    /// and return them with their bytes.
1561    pub fn recovered_transactions_with_encoded<T>(
1562        &self,
1563    ) -> impl Iterator<
1564        Item = Result<
1565            WithEncoded<alloy_consensus::transaction::Recovered<T>>,
1566            alloy_consensus::crypto::RecoveryError,
1567        >,
1568    > + '_
1569    where
1570        T: Decodable2718 + alloy_consensus::transaction::SignerRecoverable,
1571    {
1572        self.transactions().iter().map(|tx_bytes| {
1573            T::decode_2718_exact(tx_bytes.as_ref())
1574                .map_err(alloy_consensus::crypto::RecoveryError::from_source)
1575                .and_then(|tx| {
1576                    tx.try_into_recovered()
1577                        .map(|recovered| WithEncoded::new(tx_bytes.clone(), recovered))
1578                })
1579        })
1580    }
1581}
1582
1583impl From<ExecutionPayloadV1> for ExecutionPayload {
1584    fn from(payload: ExecutionPayloadV1) -> Self {
1585        Self::V1(payload)
1586    }
1587}
1588
1589impl From<ExecutionPayloadV2> for ExecutionPayload {
1590    fn from(payload: ExecutionPayloadV2) -> Self {
1591        Self::V2(payload)
1592    }
1593}
1594
1595impl From<ExecutionPayloadFieldV2> for ExecutionPayload {
1596    fn from(payload: ExecutionPayloadFieldV2) -> Self {
1597        payload.into_payload()
1598    }
1599}
1600
1601impl From<ExecutionPayloadV3> for ExecutionPayload {
1602    fn from(payload: ExecutionPayloadV3) -> Self {
1603        Self::V3(payload)
1604    }
1605}
1606
1607impl<T: Decodable2718> TryFrom<ExecutionPayload> for Block<T> {
1608    type Error = PayloadError;
1609
1610    fn try_from(value: ExecutionPayload) -> Result<Self, Self::Error> {
1611        value.try_into_block()
1612    }
1613}
1614
1615// Deserializes untagged ExecutionPayload depending on the available fields
1616#[cfg(feature = "serde")]
1617impl<'de> serde::Deserialize<'de> for ExecutionPayload {
1618    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1619    where
1620        D: serde::Deserializer<'de>,
1621    {
1622        use alloy_primitives::U64;
1623
1624        struct ExecutionPayloadVisitor;
1625
1626        impl<'de> serde::de::Visitor<'de> for ExecutionPayloadVisitor {
1627            type Value = ExecutionPayload;
1628
1629            fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1630                formatter.write_str("a valid ExecutionPayload object")
1631            }
1632
1633            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
1634            where
1635                A: serde::de::MapAccess<'de>,
1636            {
1637                // this currently rejects unknown fields
1638                #[cfg_attr(feature = "serde", derive(serde::Deserialize))]
1639                #[cfg_attr(feature = "serde", serde(field_identifier, rename_all = "camelCase"))]
1640                enum Fields {
1641                    ParentHash,
1642                    FeeRecipient,
1643                    StateRoot,
1644                    ReceiptsRoot,
1645                    LogsBloom,
1646                    PrevRandao,
1647                    BlockNumber,
1648                    GasLimit,
1649                    GasUsed,
1650                    Timestamp,
1651                    ExtraData,
1652                    BaseFeePerGas,
1653                    BlockHash,
1654                    Transactions,
1655                    // V2
1656                    Withdrawals,
1657                    // V3
1658                    BlobGasUsed,
1659                    ExcessBlobGas,
1660                }
1661
1662                let mut parent_hash = None;
1663                let mut fee_recipient = None;
1664                let mut state_root = None;
1665                let mut receipts_root = None;
1666                let mut logs_bloom = None;
1667                let mut prev_randao = None;
1668                let mut block_number = None;
1669                let mut gas_limit = None;
1670                let mut gas_used = None;
1671                let mut timestamp = None;
1672                let mut extra_data = None;
1673                let mut base_fee_per_gas = None;
1674                let mut block_hash = None;
1675                let mut transactions = None;
1676                let mut withdrawals = None;
1677                let mut blob_gas_used = None;
1678                let mut excess_blob_gas = None;
1679
1680                while let Some(key) = map.next_key()? {
1681                    match key {
1682                        Fields::ParentHash => parent_hash = Some(map.next_value()?),
1683                        Fields::FeeRecipient => fee_recipient = Some(map.next_value()?),
1684                        Fields::StateRoot => state_root = Some(map.next_value()?),
1685                        Fields::ReceiptsRoot => receipts_root = Some(map.next_value()?),
1686                        Fields::LogsBloom => logs_bloom = Some(map.next_value()?),
1687                        Fields::PrevRandao => prev_randao = Some(map.next_value()?),
1688                        Fields::BlockNumber => {
1689                            let raw = map.next_value::<U64>()?;
1690                            block_number = Some(raw.to());
1691                        }
1692                        Fields::GasLimit => {
1693                            let raw = map.next_value::<U64>()?;
1694                            gas_limit = Some(raw.to());
1695                        }
1696                        Fields::GasUsed => {
1697                            let raw = map.next_value::<U64>()?;
1698                            gas_used = Some(raw.to());
1699                        }
1700                        Fields::Timestamp => {
1701                            let raw = map.next_value::<U64>()?;
1702                            timestamp = Some(raw.to());
1703                        }
1704                        Fields::ExtraData => extra_data = Some(map.next_value()?),
1705                        Fields::BaseFeePerGas => base_fee_per_gas = Some(map.next_value()?),
1706                        Fields::BlockHash => block_hash = Some(map.next_value()?),
1707                        Fields::Transactions => transactions = Some(map.next_value()?),
1708                        Fields::Withdrawals => withdrawals = Some(map.next_value()?),
1709                        Fields::BlobGasUsed => {
1710                            let raw = map.next_value::<U64>()?;
1711                            blob_gas_used = Some(raw.to());
1712                        }
1713                        Fields::ExcessBlobGas => {
1714                            let raw = map.next_value::<U64>()?;
1715                            excess_blob_gas = Some(raw.to());
1716                        }
1717                    }
1718                }
1719
1720                let parent_hash =
1721                    parent_hash.ok_or_else(|| serde::de::Error::missing_field("parentHash"))?;
1722                let fee_recipient =
1723                    fee_recipient.ok_or_else(|| serde::de::Error::missing_field("feeRecipient"))?;
1724                let state_root =
1725                    state_root.ok_or_else(|| serde::de::Error::missing_field("stateRoot"))?;
1726                let receipts_root =
1727                    receipts_root.ok_or_else(|| serde::de::Error::missing_field("receiptsRoot"))?;
1728                let logs_bloom =
1729                    logs_bloom.ok_or_else(|| serde::de::Error::missing_field("logsBloom"))?;
1730                let prev_randao =
1731                    prev_randao.ok_or_else(|| serde::de::Error::missing_field("prevRandao"))?;
1732                let block_number =
1733                    block_number.ok_or_else(|| serde::de::Error::missing_field("blockNumber"))?;
1734                let gas_limit =
1735                    gas_limit.ok_or_else(|| serde::de::Error::missing_field("gasLimit"))?;
1736                let gas_used =
1737                    gas_used.ok_or_else(|| serde::de::Error::missing_field("gasUsed"))?;
1738                let timestamp =
1739                    timestamp.ok_or_else(|| serde::de::Error::missing_field("timestamp"))?;
1740                let extra_data =
1741                    extra_data.ok_or_else(|| serde::de::Error::missing_field("extraData"))?;
1742                let base_fee_per_gas = base_fee_per_gas
1743                    .ok_or_else(|| serde::de::Error::missing_field("baseFeePerGas"))?;
1744                let block_hash =
1745                    block_hash.ok_or_else(|| serde::de::Error::missing_field("blockHash"))?;
1746                let transactions =
1747                    transactions.ok_or_else(|| serde::de::Error::missing_field("transactions"))?;
1748
1749                let v1 = ExecutionPayloadV1 {
1750                    parent_hash,
1751                    fee_recipient,
1752                    state_root,
1753                    receipts_root,
1754                    logs_bloom,
1755                    prev_randao,
1756                    block_number,
1757                    gas_limit,
1758                    gas_used,
1759                    timestamp,
1760                    extra_data,
1761                    base_fee_per_gas,
1762                    block_hash,
1763                    transactions,
1764                };
1765
1766                let Some(withdrawals) = withdrawals else {
1767                    return if blob_gas_used.is_none() && excess_blob_gas.is_none() {
1768                        Ok(ExecutionPayload::V1(v1))
1769                    } else {
1770                        Err(serde::de::Error::custom("invalid enum variant"))
1771                    };
1772                };
1773
1774                if let (Some(blob_gas_used), Some(excess_blob_gas)) =
1775                    (blob_gas_used, excess_blob_gas)
1776                {
1777                    return Ok(ExecutionPayload::V3(ExecutionPayloadV3 {
1778                        payload_inner: ExecutionPayloadV2 { payload_inner: v1, withdrawals },
1779                        blob_gas_used,
1780                        excess_blob_gas,
1781                    }));
1782                }
1783
1784                // reject incomplete V3 payloads even if they could construct a valid V2
1785                if blob_gas_used.is_some() || excess_blob_gas.is_some() {
1786                    return Err(serde::de::Error::custom("invalid enum variant"));
1787                }
1788
1789                Ok(ExecutionPayload::V2(ExecutionPayloadV2 { payload_inner: v1, withdrawals }))
1790            }
1791        }
1792
1793        const FIELDS: &[&str] = &[
1794            "parentHash",
1795            "feeRecipient",
1796            "stateRoot",
1797            "receiptsRoot",
1798            "logsBloom",
1799            "prevRandao",
1800            "blockNumber",
1801            "gasLimit",
1802            "gasUsed",
1803            "timestamp",
1804            "extraData",
1805            "baseFeePerGas",
1806            "blockHash",
1807            "transactions",
1808            "withdrawals",
1809            "blobGasUsed",
1810            "excessBlobGas",
1811        ];
1812        deserializer.deserialize_struct("ExecutionPayload", FIELDS, ExecutionPayloadVisitor)
1813    }
1814}
1815
1816/// This structure contains a body of an execution payload.
1817///
1818/// See also: <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#executionpayloadbodyv1>
1819#[derive(Clone, Debug, PartialEq, Eq)]
1820#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1821#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1822pub struct ExecutionPayloadBodyV1 {
1823    /// Enveloped encoded transactions.
1824    pub transactions: Vec<Bytes>,
1825    /// All withdrawals in the block.
1826    ///
1827    /// Will always be `None` if pre shanghai.
1828    pub withdrawals: Option<Vec<Withdrawal>>,
1829}
1830
1831impl ExecutionPayloadBodyV1 {
1832    /// Creates an [`ExecutionPayloadBodyV1`] from the given withdrawals and transactions
1833    pub fn new<'a, T>(
1834        withdrawals: Option<Withdrawals>,
1835        transactions: impl IntoIterator<Item = &'a T>,
1836    ) -> Self
1837    where
1838        T: Encodable2718 + 'a,
1839    {
1840        Self {
1841            transactions: transactions.into_iter().map(|tx| tx.encoded_2718().into()).collect(),
1842            withdrawals: withdrawals.map(Withdrawals::into_inner),
1843        }
1844    }
1845
1846    /// Converts a [`alloy_consensus::Block`] into an execution payload body.
1847    pub fn from_block<T: Encodable2718, H>(block: Block<T, H>) -> Self {
1848        Self::new(block.body.withdrawals.clone(), block.body.transactions())
1849    }
1850}
1851
1852impl<T: Encodable2718, H> From<Block<T, H>> for ExecutionPayloadBodyV1 {
1853    fn from(value: Block<T, H>) -> Self {
1854        Self::from_block(value)
1855    }
1856}
1857
1858/// This structure contains the attributes required to initiate a payload build process in the
1859/// context of an `engine_forkchoiceUpdated` call.
1860#[derive(Clone, Debug, Default, PartialEq, Eq)]
1861#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1862#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
1863#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1864pub struct PayloadAttributes {
1865    /// Value for the `timestamp` field of the new payload
1866    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
1867    pub timestamp: u64,
1868    /// Value for the `prevRandao` field of the new payload
1869    pub prev_randao: B256,
1870    /// Suggested value for the `feeRecipient` field of the new payload
1871    pub suggested_fee_recipient: Address,
1872    /// Array of [`Withdrawal`] enabled with V2
1873    /// See <https://github.com/ethereum/execution-apis/blob/6452a6b194d7db269bf1dbd087a267251d3cc7f8/src/engine/shanghai.md#payloadattributesv2>
1874    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1875    pub withdrawals: Option<Vec<Withdrawal>>,
1876    /// Root of the parent beacon block enabled with V3.
1877    ///
1878    /// See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#payloadattributesv3>
1879    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
1880    pub parent_beacon_block_root: Option<B256>,
1881}
1882
1883/// This structure contains the result of processing a payload or fork choice update.
1884#[derive(Clone, Debug, PartialEq, Eq)]
1885#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
1886#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
1887#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1888pub struct PayloadStatus {
1889    /// The status of the payload.
1890    #[cfg_attr(feature = "serde", serde(flatten))]
1891    pub status: PayloadStatusEnum,
1892    /// Hash of the most recent valid block in the branch defined by payload and its ancestors
1893    pub latest_valid_hash: Option<B256>,
1894}
1895
1896impl PayloadStatus {
1897    /// Initializes a new payload status.
1898    pub const fn new(status: PayloadStatusEnum, latest_valid_hash: Option<B256>) -> Self {
1899        Self { status, latest_valid_hash }
1900    }
1901
1902    /// Creates a new payload status from the given status.
1903    pub const fn from_status(status: PayloadStatusEnum) -> Self {
1904        Self { status, latest_valid_hash: None }
1905    }
1906
1907    /// Sets the latest valid hash.
1908    pub const fn with_latest_valid_hash(mut self, latest_valid_hash: B256) -> Self {
1909        self.latest_valid_hash = Some(latest_valid_hash);
1910        self
1911    }
1912
1913    /// Sets the latest valid hash if it's not None.
1914    pub const fn maybe_latest_valid_hash(mut self, latest_valid_hash: Option<B256>) -> Self {
1915        self.latest_valid_hash = latest_valid_hash;
1916        self
1917    }
1918
1919    /// Returns true if the payload status is syncing.
1920    pub const fn is_syncing(&self) -> bool {
1921        self.status.is_syncing()
1922    }
1923
1924    /// Returns true if the payload status is valid.
1925    pub const fn is_valid(&self) -> bool {
1926        self.status.is_valid()
1927    }
1928
1929    /// Returns true if the payload status is invalid.
1930    pub const fn is_invalid(&self) -> bool {
1931        self.status.is_invalid()
1932    }
1933}
1934
1935impl core::fmt::Display for PayloadStatus {
1936    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1937        write!(
1938            f,
1939            "PayloadStatus {{ status: {}, latestValidHash: {:?} }}",
1940            self.status, self.latest_valid_hash
1941        )
1942    }
1943}
1944
1945#[cfg(feature = "serde")]
1946impl serde::Serialize for PayloadStatus {
1947    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1948    where
1949        S: serde::Serializer,
1950    {
1951        use serde::ser::SerializeMap;
1952        let mut map = serializer.serialize_map(Some(3))?;
1953        map.serialize_entry("status", self.status.as_str())?;
1954        map.serialize_entry("latestValidHash", &self.latest_valid_hash)?;
1955        map.serialize_entry("validationError", &self.status.validation_error())?;
1956        map.end()
1957    }
1958}
1959
1960impl From<PayloadError> for PayloadStatusEnum {
1961    fn from(error: PayloadError) -> Self {
1962        Self::Invalid { validation_error: error.to_string() }
1963    }
1964}
1965
1966/// Represents the status response of a payload.
1967#[derive(Clone, Debug, PartialEq, Eq)]
1968#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1969#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "SCREAMING_SNAKE_CASE"))]
1970#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
1971pub enum PayloadStatusEnum {
1972    /// VALID is returned by the engine API in the following calls:
1973    ///   - newPayload:       if the payload was already known or was just validated and executed
1974    ///   - forkchoiceUpdate: if the chain accepted the reorg (might ignore if it's stale)
1975    Valid,
1976
1977    /// INVALID is returned by the engine API in the following calls:
1978    ///   - newPayload:       if the payload failed to execute on top of the local chain
1979    ///   - forkchoiceUpdate: if the new head is unknown, pre-merge, or reorg to it fails
1980    Invalid {
1981        /// The error message for the invalid payload.
1982        #[cfg_attr(feature = "serde", serde(rename = "validationError"))]
1983        validation_error: String,
1984    },
1985
1986    /// SYNCING is returned by the engine API in the following calls:
1987    ///   - newPayload:       if the payload was accepted on top of an active sync
1988    ///   - forkchoiceUpdate: if the new head was seen before, but not part of the chain
1989    Syncing,
1990
1991    /// ACCEPTED is returned by the engine API in the following calls:
1992    ///   - newPayload: if the payload was accepted, but not processed (side chain)
1993    Accepted,
1994}
1995
1996impl PayloadStatusEnum {
1997    /// Returns the string representation of the payload status.
1998    pub const fn as_str(&self) -> &'static str {
1999        match self {
2000            Self::Valid => "VALID",
2001            Self::Invalid { .. } => "INVALID",
2002            Self::Syncing => "SYNCING",
2003            Self::Accepted => "ACCEPTED",
2004        }
2005    }
2006
2007    /// Returns the validation error if the payload status is invalid.
2008    pub fn validation_error(&self) -> Option<&str> {
2009        match self {
2010            Self::Invalid { validation_error } => Some(validation_error),
2011            _ => None,
2012        }
2013    }
2014
2015    /// Returns true if the payload status is syncing.
2016    pub const fn is_syncing(&self) -> bool {
2017        matches!(self, Self::Syncing)
2018    }
2019
2020    /// Returns true if the payload status is valid.
2021    pub const fn is_valid(&self) -> bool {
2022        matches!(self, Self::Valid)
2023    }
2024
2025    /// Returns true if the payload status is invalid.
2026    pub const fn is_invalid(&self) -> bool {
2027        matches!(self, Self::Invalid { .. })
2028    }
2029}
2030
2031impl core::fmt::Display for PayloadStatusEnum {
2032    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2033        match self {
2034            Self::Invalid { validation_error } => {
2035                f.write_str(self.as_str())?;
2036                f.write_str(": ")?;
2037                f.write_str(validation_error.as_str())
2038            }
2039            _ => f.write_str(self.as_str()),
2040        }
2041    }
2042}
2043
2044/// Struct aggregating [`ExecutionPayload`] and [`ExecutionPayloadSidecar`] and encapsulating
2045/// complete payload supplied for execution.
2046#[derive(Debug, Clone)]
2047#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2048#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
2049pub struct ExecutionData {
2050    /// Execution payload.
2051    pub payload: ExecutionPayload,
2052    /// Additional fork-specific fields.
2053    pub sidecar: ExecutionPayloadSidecar,
2054}
2055
2056impl ExecutionData {
2057    /// Creates new instance of [`ExecutionData`].
2058    pub const fn new(payload: ExecutionPayload, sidecar: ExecutionPayloadSidecar) -> Self {
2059        Self { payload, sidecar }
2060    }
2061
2062    /// Conversion from [`alloy_consensus::Block`]. Also returns the [`ExecutionPayloadSidecar`]
2063    /// extracted from the block.
2064    ///
2065    /// For the [`ExecutionPayloadSidecar`] this is expected to use just the requests hash, because
2066    /// the [`Requests`] are not part of the block/header. See also
2067    /// [`RequestsOrHash`](alloy_eips::eip7685::RequestsOrHash).
2068    ///
2069    /// See also [`ExecutionPayload::from_block_unchecked`].
2070    pub fn from_block_unchecked<T, H>(block_hash: B256, block: &Block<T, H>) -> Self
2071    where
2072        T: Encodable2718 + Transaction,
2073        H: BlockHeader,
2074    {
2075        let (payload, sidecar) = ExecutionPayload::from_block_unchecked(block_hash, block);
2076        Self::new(payload, sidecar)
2077    }
2078
2079    /// Returns the parent hash of the block.
2080    pub const fn parent_hash(&self) -> B256 {
2081        self.payload.parent_hash()
2082    }
2083
2084    /// Returns the hash of the block.
2085    pub const fn block_hash(&self) -> B256 {
2086        self.payload.block_hash()
2087    }
2088
2089    /// Returns the number of the block.
2090    pub const fn block_number(&self) -> u64 {
2091        self.payload.block_number()
2092    }
2093
2094    /// Returns the parent beacon block root, if any.
2095    pub fn parent_beacon_block_root(&self) -> Option<B256> {
2096        self.sidecar.parent_beacon_block_root()
2097    }
2098
2099    /// Return the withdrawals for the payload or attributes.
2100    pub const fn withdrawals(&self) -> Option<&Vec<Withdrawal>> {
2101        self.payload.withdrawals()
2102    }
2103
2104    /// Tries to create a new unsealed block from the given payload and payload sidecar.
2105    ///
2106    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
2107    ///
2108    /// # Note
2109    ///
2110    /// The log bloom is assumed to be validated during serialization.
2111    ///
2112    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
2113    pub fn try_into_block<T: Decodable2718>(
2114        self,
2115    ) -> Result<alloy_consensus::Block<T>, PayloadError> {
2116        self.try_into_block_with(|tx| {
2117            T::decode_2718_exact(tx.as_ref())
2118                .map_err(alloy_rlp::Error::from)
2119                .map_err(PayloadError::from)
2120        })
2121    }
2122
2123    /// Tries to create a new unsealed block from the given payload and payload sidecar with a
2124    /// custom transaction mapper.
2125    ///
2126    /// Performs additional validation of `extra_data` and `base_fee_per_gas` fields.
2127    ///
2128    /// # Note
2129    ///
2130    /// The log bloom is assumed to be validated during serialization.
2131    ///
2132    /// See <https://github.com/ethereum/go-ethereum/blob/79a478bb6176425c2400e949890e668a3d9a3d05/core/beacon/types.go#L145>
2133    pub fn try_into_block_with<T, F, E>(
2134        self,
2135        f: F,
2136    ) -> Result<alloy_consensus::Block<T>, PayloadError>
2137    where
2138        F: FnMut(Bytes) -> Result<T, E>,
2139        E: Into<PayloadError>,
2140    {
2141        self.payload.try_into_block_with_sidecar_with(&self.sidecar, f)
2142    }
2143
2144    /// Converts [`ExecutionData`] to [`Block`] with raw [`Bytes`] transactions.
2145    ///
2146    /// This is similar to [`Self::try_into_block_with`] but returns the transactions as raw bytes
2147    /// without any conversion.
2148    pub fn into_block_raw(self) -> Result<Block<Bytes>, PayloadError> {
2149        let mut base_block = self.payload.into_block_raw()?;
2150        base_block.header.parent_beacon_block_root = self.sidecar.parent_beacon_block_root();
2151        base_block.header.requests_hash = self.sidecar.requests_hash();
2152        Ok(base_block)
2153    }
2154}
2155
2156#[cfg(test)]
2157mod tests {
2158    use super::*;
2159    use crate::{CancunPayloadFields, PayloadValidationError};
2160    use alloc::vec;
2161    use alloy_consensus::TxEnvelope;
2162    use alloy_primitives::{b256, hex};
2163    use similar_asserts::assert_eq;
2164
2165    #[test]
2166    #[cfg(feature = "kzg")]
2167    fn convert_empty_bundle() {
2168        let bundle = BlobsBundleV1::default();
2169        let _sidecar = bundle.try_into_sidecar().unwrap();
2170    }
2171
2172    #[test]
2173    #[cfg(feature = "serde")]
2174    fn serde_blobsbundlev1_empty() {
2175        let blobs_bundle_v1 = BlobsBundleV1::empty();
2176
2177        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
2178        let deserialized: BlobsBundleV1 = serde_json::from_str(&serialized).unwrap();
2179        assert_eq!(deserialized, blobs_bundle_v1);
2180    }
2181
2182    #[test]
2183    #[cfg(feature = "serde")]
2184    #[cfg(not(debug_assertions))]
2185    fn serde_blobsbundlev1_not_empty_pass() {
2186        let blobs_bundle_v1 = BlobsBundleV1 {
2187            proofs: vec![Bytes48::default()],
2188            commitments: vec![Bytes48::default()],
2189            blobs: vec![Blob::default()],
2190        };
2191
2192        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
2193        let deserialized: BlobsBundleV1 = serde_json::from_str(&serialized).unwrap();
2194        assert_eq!(deserialized, blobs_bundle_v1);
2195    }
2196
2197    #[test]
2198    #[cfg(feature = "serde")]
2199    #[cfg(not(debug_assertions))]
2200    fn serde_blobsbundlev1_not_empty_fail() {
2201        let blobs_bundle_v1 = BlobsBundleV1 {
2202            proofs: vec![Bytes48::default(), Bytes48::default()],
2203            commitments: vec![Bytes48::default()],
2204            blobs: vec![Blob::default()],
2205        };
2206
2207        let serialized = serde_json::to_string(&blobs_bundle_v1).unwrap();
2208        let deserialized: Result<BlobsBundleV1, serde_json::Error> =
2209            serde_json::from_str(&serialized);
2210        assert!(deserialized.is_err(), "invalid length 2, expected commitments.len()");
2211    }
2212
2213    #[test]
2214    #[cfg(feature = "serde")]
2215    #[cfg(not(debug_assertions))]
2216    fn serde_blobsbundlev2_not_empty_pass() {
2217        let commitments = vec![Bytes48::default()];
2218
2219        let blobs_bundle_v2 = BlobsBundleV2 {
2220            proofs: vec![Bytes48::default(); commitments.len() * CELLS_PER_EXT_BLOB],
2221            commitments,
2222            blobs: vec![Blob::default()],
2223        };
2224
2225        let serialized = serde_json::to_string(&blobs_bundle_v2).unwrap();
2226        let deserialized: BlobsBundleV2 = serde_json::from_str(&serialized).unwrap();
2227        assert_eq!(deserialized, blobs_bundle_v2);
2228    }
2229
2230    #[test]
2231    #[cfg(feature = "serde")]
2232    #[cfg(not(debug_assertions))]
2233    fn serde_blobsbundlev2_not_empty_fail() {
2234        let blobs_bundle_v2 = BlobsBundleV2 {
2235            proofs: vec![Bytes48::default()],
2236            commitments: vec![Bytes48::default()],
2237            blobs: vec![],
2238        };
2239
2240        let serialized = serde_json::to_string(&blobs_bundle_v2).unwrap();
2241        let deserialized: Result<BlobsBundleV2, serde_json::Error> =
2242            serde_json::from_str(&serialized);
2243        assert!(deserialized.is_err());
2244    }
2245
2246    #[test]
2247    #[cfg(feature = "ssz")]
2248    #[cfg(not(debug_assertions))]
2249    fn ssz_blobsbundlev2_roundtrip() {
2250        let commitments = vec![Bytes48::default(), Bytes48::default()];
2251        let num_blobs = commitments.len();
2252
2253        let blobs_bundle_v2 = BlobsBundleV2 {
2254            commitments,
2255            proofs: vec![Bytes48::default(); num_blobs * CELLS_PER_EXT_BLOB],
2256            blobs: vec![Blob::default(); num_blobs],
2257        };
2258
2259        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
2260        let decoded: BlobsBundleV2 = ssz::Decode::from_ssz_bytes(&encoded).unwrap();
2261
2262        assert_eq!(decoded, blobs_bundle_v2);
2263    }
2264
2265    #[test]
2266    #[cfg(feature = "ssz")]
2267    #[cfg(not(debug_assertions))]
2268    fn ssz_blobsbundlev2_invalid_proofs_length() {
2269        let commitments = vec![Bytes48::default()];
2270
2271        let blobs_bundle_v2 = BlobsBundleV2 {
2272            commitments,
2273            proofs: vec![Bytes48::default(); 2],
2274            blobs: vec![Blob::default()],
2275        };
2276
2277        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
2278
2279        // Attempt to decode - should fail due to mismatched proofs length
2280        let result: Result<BlobsBundleV2, _> = ssz::Decode::from_ssz_bytes(&encoded);
2281        assert!(result.is_err());
2282    }
2283
2284    #[test]
2285    #[cfg(feature = "ssz")]
2286    #[cfg(not(debug_assertions))]
2287    fn ssz_blobsbundlev2_mismatched_commitments_blobs() {
2288        let blobs_bundle_v2 = BlobsBundleV2 {
2289            commitments: vec![Bytes48::default(), Bytes48::default()],
2290            proofs: vec![Bytes48::default(); CELLS_PER_EXT_BLOB],
2291            blobs: vec![Blob::default()],
2292        };
2293
2294        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
2295
2296        // Attempt to decode - should fail due to wrong number of commitments
2297        let result: Result<BlobsBundleV2, _> = ssz::Decode::from_ssz_bytes(&encoded);
2298        assert!(result.is_err());
2299    }
2300
2301    #[test]
2302    #[cfg(feature = "ssz")]
2303    fn ssz_blobsbundlev2_empty() {
2304        let blobs_bundle_v2 = BlobsBundleV2 { commitments: vec![], proofs: vec![], blobs: vec![] };
2305
2306        let encoded = ssz::Encode::as_ssz_bytes(&blobs_bundle_v2);
2307
2308        // Decode from SSZ - empty bundle should be valid
2309        let decoded: BlobsBundleV2 = ssz::Decode::from_ssz_bytes(&encoded).unwrap();
2310        assert_eq!(decoded, blobs_bundle_v2);
2311    }
2312
2313    #[test]
2314    #[cfg(feature = "serde")]
2315    fn serde_payload_status() {
2316        let s = r#"{"status":"SYNCING","latestValidHash":null,"validationError":null}"#;
2317        let status: PayloadStatus = serde_json::from_str(s).unwrap();
2318        assert_eq!(status.status, PayloadStatusEnum::Syncing);
2319        assert!(status.latest_valid_hash.is_none());
2320        assert!(status.status.validation_error().is_none());
2321        assert_eq!(serde_json::to_string(&status).unwrap(), s);
2322
2323        let full = s;
2324        let s = r#"{"status":"SYNCING","latestValidHash":null}"#;
2325        let status: PayloadStatus = serde_json::from_str(s).unwrap();
2326        assert_eq!(status.status, PayloadStatusEnum::Syncing);
2327        assert!(status.latest_valid_hash.is_none());
2328        assert!(status.status.validation_error().is_none());
2329        assert_eq!(serde_json::to_string(&status).unwrap(), full);
2330    }
2331
2332    #[test]
2333    #[cfg(feature = "serde")]
2334    fn serde_payload_status_error_deserialize() {
2335        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"Failed to decode block"}"#;
2336        let q = PayloadStatus {
2337            latest_valid_hash: None,
2338            status: PayloadStatusEnum::Invalid {
2339                validation_error: "Failed to decode block".to_string(),
2340            },
2341        };
2342        assert_eq!(q, serde_json::from_str(s).unwrap());
2343
2344        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"links to previously rejected block"}"#;
2345        let q = PayloadStatus {
2346            latest_valid_hash: None,
2347            status: PayloadStatusEnum::Invalid {
2348                validation_error: PayloadValidationError::LinksToRejectedPayload.to_string(),
2349            },
2350        };
2351        assert_eq!(q, serde_json::from_str(s).unwrap());
2352
2353        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":"invalid block number"}"#;
2354        let q = PayloadStatus {
2355            latest_valid_hash: None,
2356            status: PayloadStatusEnum::Invalid {
2357                validation_error: PayloadValidationError::InvalidBlockNumber.to_string(),
2358            },
2359        };
2360        assert_eq!(q, serde_json::from_str(s).unwrap());
2361
2362        let s = r#"{"status":"INVALID","latestValidHash":null,"validationError":
2363        "invalid merkle root: (remote: 0x3f77fb29ce67436532fee970e1add8f5cc80e8878c79b967af53b1fd92a0cab7 local: 0x603b9628dabdaadb442a3bb3d7e0360efc110e1948472909230909f1690fed17)"}"#;
2364        let q = PayloadStatus {
2365            latest_valid_hash: None,
2366            status: PayloadStatusEnum::Invalid {
2367                validation_error: PayloadValidationError::InvalidStateRoot {
2368                    remote: "0x3f77fb29ce67436532fee970e1add8f5cc80e8878c79b967af53b1fd92a0cab7"
2369                        .parse()
2370                        .unwrap(),
2371                    local: "0x603b9628dabdaadb442a3bb3d7e0360efc110e1948472909230909f1690fed17"
2372                        .parse()
2373                        .unwrap(),
2374                }
2375                .to_string(),
2376            },
2377        };
2378        assert_eq!(q, serde_json::from_str(s).unwrap());
2379    }
2380
2381    #[test]
2382    #[cfg(feature = "serde")]
2383    fn serde_roundtrip_legacy_txs_payload_v1() {
2384        // pulled from hive tests
2385        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"]}"#;
2386        let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap();
2387        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
2388
2389        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
2390        assert_eq!(any_payload, payload.into());
2391    }
2392
2393    #[test]
2394    #[cfg(feature = "serde")]
2395    fn serde_roundtrip_legacy_txs_payload_v3() {
2396        // pulled from hive tests - modified with 4844 fields
2397        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"}"#;
2398        let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap();
2399        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
2400
2401        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
2402        assert_eq!(any_payload, payload.into());
2403    }
2404
2405    #[test]
2406    #[cfg(feature = "serde")]
2407    fn serde_roundtrip_enveloped_txs_payload_v1() {
2408        // pulled from hive tests
2409        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"]}"#;
2410        let payload: ExecutionPayloadV1 = serde_json::from_str(s).unwrap();
2411        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
2412
2413        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
2414        assert_eq!(any_payload, payload.into());
2415    }
2416
2417    #[test]
2418    #[cfg(feature = "serde")]
2419    fn serde_roundtrip_enveloped_txs_payload_v3() {
2420        // pulled from hive tests - modified with 4844 fields
2421        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"}"#;
2422        let payload: ExecutionPayloadV3 = serde_json::from_str(s).unwrap();
2423        assert_eq!(serde_json::to_string(&payload).unwrap(), s);
2424
2425        let any_payload: ExecutionPayload = serde_json::from_str(s).unwrap();
2426        assert_eq!(any_payload, payload.into());
2427    }
2428
2429    #[test]
2430    #[cfg(feature = "serde")]
2431    fn serde_roundtrip_execution_payload_envelope_v3() {
2432        // pulled from a geth response getPayloadV3 in hive tests
2433        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}"#;
2434        let envelope: ExecutionPayloadEnvelopeV3 = serde_json::from_str(response).unwrap();
2435        assert_eq!(serde_json::to_string(&envelope).unwrap(), response);
2436    }
2437
2438    #[test]
2439    #[cfg(feature = "serde")]
2440    fn serde_payload_input_enum_v3() {
2441        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"}"#;
2442
2443        let payload: ExecutionPayload = serde_json::from_str(response_v3).unwrap();
2444        assert!(payload.as_v3().is_some());
2445        assert_eq!(serde_json::to_string(&payload).unwrap(), response_v3);
2446
2447        let payload_v3: ExecutionPayloadV3 = serde_json::from_str(response_v3).unwrap();
2448        assert_eq!(payload.as_v3().unwrap(), &payload_v3);
2449    }
2450
2451    #[test]
2452    #[cfg(feature = "serde")]
2453    fn serde_payload_input_enum_v2() {
2454        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":[]}"#;
2455
2456        let payload: ExecutionPayload = serde_json::from_str(response_v2).unwrap();
2457        assert!(payload.as_v3().is_none());
2458        assert!(payload.as_v2().is_some());
2459        assert_eq!(serde_json::to_string(&payload).unwrap(), response_v2);
2460
2461        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(response_v2).unwrap();
2462        assert_eq!(payload.as_v2().unwrap(), &payload_v2);
2463    }
2464
2465    #[test]
2466    #[cfg(feature = "serde")]
2467    fn serde_payload_input_enum_faulty_v2() {
2468        // incomplete V3 payload should be rejected even if it has all V2 fields
2469        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"}"#;
2470
2471        let payload: Result<ExecutionPayload, serde_json::Error> =
2472            serde_json::from_str(response_faulty);
2473        assert!(payload.is_err());
2474    }
2475
2476    #[test]
2477    #[cfg(feature = "serde")]
2478    fn serde_payload_input_enum_faulty_v1() {
2479        // incomplete V3 payload should be rejected even if it has all V1 fields
2480        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"}"#;
2481
2482        let payload: Result<ExecutionPayload, serde_json::Error> =
2483            serde_json::from_str(response_faulty);
2484        assert!(payload.is_err());
2485    }
2486
2487    #[test]
2488    #[cfg(feature = "serde")]
2489    fn serde_faulty_roundtrip_payload_input_v3() {
2490        // The deserialization behavior of ExecutionPayload structs is faulty.
2491        // They should not be implicitly deserializable to an earlier version,
2492        // as this breaks round-trip behavior
2493        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"}"#;
2494
2495        let payload_v2: ExecutionPayloadV2 = serde_json::from_str(response_v3).unwrap();
2496        assert_ne!(response_v3, serde_json::to_string(&payload_v2).unwrap());
2497
2498        let payload_v1: ExecutionPayloadV1 = serde_json::from_str(response_v3).unwrap();
2499        assert_ne!(response_v3, serde_json::to_string(&payload_v1).unwrap());
2500    }
2501
2502    #[test]
2503    #[cfg(feature = "serde")]
2504    fn serde_faulty_roundtrip_payload_input_v2() {
2505        // The deserialization behavior of ExecutionPayload structs is faulty.
2506        // They should not be implicitly deserializable to an earlier version,
2507        // as this breaks round-trip behavior
2508        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":[]}"#;
2509
2510        let payload: ExecutionPayloadV1 = serde_json::from_str(response_v2).unwrap();
2511        assert_ne!(response_v2, serde_json::to_string(&payload).unwrap());
2512    }
2513
2514    #[test]
2515    #[cfg(feature = "serde")]
2516    fn serde_deserialize_execution_payload_input_v2() {
2517        let response = r#"
2518{
2519  "baseFeePerGas": "0x173b30b3",
2520  "blockHash": "0x99d486755fd046ad0bbb60457bac93d4856aa42fa00629cc7e4a28b65b5f8164",
2521  "blockNumber": "0xb",
2522  "extraData": "0xd883010d01846765746888676f312e32302e33856c696e7578",
2523  "feeRecipient": "0x0000000000000000000000000000000000000000",
2524  "gasLimit": "0x405829",
2525  "gasUsed": "0x3f0ca0",
2526  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2527  "parentHash": "0xfe34aaa2b869c66a727783ee5ad3e3983b6ef22baf24a1e502add94e7bcac67a",
2528  "prevRandao": "0x74132c32fe3ab9a470a8352544514d21b6969e7749f97742b53c18a1b22b396c",
2529  "receiptsRoot": "0x6a5c41dc55a1bd3e74e7f6accc799efb08b00c36c15265058433fcea6323e95f",
2530  "stateRoot": "0xde3b357f5f099e4c33d0343c9e9d204d663d7bd9c65020a38e5d0b2a9ace78a2",
2531  "timestamp": "0x6507d6b4",
2532  "transactions": [
2533    "0xf86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53",
2534    "0xf86d0b8458b20efd82520894687704db07e902e9a8b3754031d168d46e3d586e8806f3e8d87878800080820a95a0e3108f710902be662d5c978af16109961ffaf2ac4f88522407d40949a9574276a0205719ed21889b42ab5c1026d40b759a507c12d92db0d100fa69e1ac79137caa",
2535    "0xf86d0c8458b20efd8252089415e6a5a2e131dd5467fa1ff3acd104f45ee5940b8806f3e8d87878800080820a96a0af556ba9cda1d686239e08c24e169dece7afa7b85e0948eaa8d457c0561277fca029da03d3af0978322e54ac7e8e654da23934e0dd839804cb0430f8aaafd732dc",
2536    "0xf8521784565adcb7830186a0808080820a96a0ec782872a673a9fe4eff028a5bdb30d6b8b7711f58a187bf55d3aec9757cb18ea001796d373da76f2b0aeda72183cce0ad070a4f03aa3e6fee4c757a9444245206",
2537    "0xf8521284565adcb7830186a0808080820a95a08a0ea89028eff02596b385a10e0bd6ae098f3b281be2c95a9feb1685065d7384a06239d48a72e4be767bd12f317dd54202f5623a33e71e25a87cb25dd781aa2fc8",
2538    "0xf8521384565adcb7830186a0808080820a95a0784dbd311a82f822184a46f1677a428cbe3a2b88a798fb8ad1370cdbc06429e8a07a7f6a0efd428e3d822d1de9a050b8a883938b632185c254944dd3e40180eb79"
2539  ],
2540  "withdrawals": []
2541}
2542        "#;
2543        let payload: ExecutionPayloadInputV2 = serde_json::from_str(response).unwrap();
2544        assert_eq!(payload.withdrawals, Some(vec![]));
2545
2546        let response = r#"
2547{
2548  "baseFeePerGas": "0x173b30b3",
2549  "blockHash": "0x99d486755fd046ad0bbb60457bac93d4856aa42fa00629cc7e4a28b65b5f8164",
2550  "blockNumber": "0xb",
2551  "extraData": "0xd883010d01846765746888676f312e32302e33856c696e7578",
2552  "feeRecipient": "0x0000000000000000000000000000000000000000",
2553  "gasLimit": "0x405829",
2554  "gasUsed": "0x3f0ca0",
2555  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2556  "parentHash": "0xfe34aaa2b869c66a727783ee5ad3e3983b6ef22baf24a1e502add94e7bcac67a",
2557  "prevRandao": "0x74132c32fe3ab9a470a8352544514d21b6969e7749f97742b53c18a1b22b396c",
2558  "receiptsRoot": "0x6a5c41dc55a1bd3e74e7f6accc799efb08b00c36c15265058433fcea6323e95f",
2559  "stateRoot": "0xde3b357f5f099e4c33d0343c9e9d204d663d7bd9c65020a38e5d0b2a9ace78a2",
2560  "timestamp": "0x6507d6b4",
2561  "transactions": [
2562    "0xf86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53",
2563    "0xf86d0b8458b20efd82520894687704db07e902e9a8b3754031d168d46e3d586e8806f3e8d87878800080820a95a0e3108f710902be662d5c978af16109961ffaf2ac4f88522407d40949a9574276a0205719ed21889b42ab5c1026d40b759a507c12d92db0d100fa69e1ac79137caa",
2564    "0xf86d0c8458b20efd8252089415e6a5a2e131dd5467fa1ff3acd104f45ee5940b8806f3e8d87878800080820a96a0af556ba9cda1d686239e08c24e169dece7afa7b85e0948eaa8d457c0561277fca029da03d3af0978322e54ac7e8e654da23934e0dd839804cb0430f8aaafd732dc",
2565    "0xf8521784565adcb7830186a0808080820a96a0ec782872a673a9fe4eff028a5bdb30d6b8b7711f58a187bf55d3aec9757cb18ea001796d373da76f2b0aeda72183cce0ad070a4f03aa3e6fee4c757a9444245206",
2566    "0xf8521284565adcb7830186a0808080820a95a08a0ea89028eff02596b385a10e0bd6ae098f3b281be2c95a9feb1685065d7384a06239d48a72e4be767bd12f317dd54202f5623a33e71e25a87cb25dd781aa2fc8",
2567    "0xf8521384565adcb7830186a0808080820a95a0784dbd311a82f822184a46f1677a428cbe3a2b88a798fb8ad1370cdbc06429e8a07a7f6a0efd428e3d822d1de9a050b8a883938b632185c254944dd3e40180eb79"
2568  ]
2569}
2570        "#;
2571        let payload: ExecutionPayloadInputV2 = serde_json::from_str(response).unwrap();
2572        assert_eq!(payload.withdrawals, None);
2573    }
2574
2575    #[test]
2576    #[cfg(feature = "serde")]
2577    fn serde_deserialize_v2_input_with_blob_fields() {
2578        let input = r#"
2579{
2580    "parentHash": "0xaaa4c5b574f37e1537c78931d1bca24a4d17d4f29f1ee97e1cd48b704909de1f",
2581    "feeRecipient": "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
2582    "stateRoot": "0x308ee9c5c6fab5e3d08763a3b5fe0be8ada891fa5010a49a3390e018dd436810",
2583    "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
2584    "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2585    "prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
2586    "blockNumber": "0xf",
2587    "gasLimit": "0x16345785d8a0000",
2588    "gasUsed": "0x0",
2589    "timestamp": "0x3a97",
2590    "extraData": "0x",
2591    "baseFeePerGas": "0x7",
2592    "blockHash": "0x38bb6ba645c7e6bd970f9c7d492fafe1e04d85349054cb48d16c9d2c3e3cd0bf",
2593    "transactions": [],
2594    "withdrawals": [],
2595    "excessBlobGas": "0x0",
2596    "blobGasUsed": "0x0"
2597}
2598        "#;
2599
2600        // ensure that deserializing this (it includes blob fields) fails
2601        let payload_res: Result<ExecutionPayloadInputV2, serde_json::Error> =
2602            serde_json::from_str(input);
2603        assert!(payload_res.is_err());
2604    }
2605
2606    // <https://github.com/paradigmxyz/reth/issues/6036>
2607    #[test]
2608    #[cfg(feature = "serde")]
2609    fn deserialize_op_base_payload() {
2610        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"]}"#;
2611        let _payload = serde_json::from_str::<ExecutionPayloadInputV2>(payload).unwrap();
2612    }
2613
2614    #[test]
2615    fn roundtrip_payload_to_block() {
2616        let first_transaction_raw = Bytes::from_static(&hex!("02f9017a8501a1f0ff438211cc85012a05f2008512a05f2000830249f094d5409474fd5a725eab2ac9a8b26ca6fb51af37ef80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000001bdd2ed4b616c800000000000000000000000000001e9ee781dd4b97bdef92e5d1785f73a1f931daa20000000000000000000000007a40026a3b9a41754a95eec8c92c6b99886f440c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ae80eb647dd09968488fa1d7e412bf8558a0b7a0000000000000000000000000f9815537d361cb02befd9918c95c97d4d8a4a2bc001a0ba8f1928bb0efc3fcd01524a2039a9a2588fa567cd9a7cc18217e05c615e9d69a0544bfd11425ac7748e76b3795b57a5563e2b0eff47b5428744c62ff19ccfc305")[..]);
2617        let second_transaction_raw = Bytes::from_static(&hex!("03f901388501a1f0ff430c843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c005f8c6a00195f6dff17753fc89b60eac6477026a805116962c9e412de8015c0484e661c1a001aae314061d4f5bbf158f15d9417a238f9589783f58762cd39d05966b3ba2fba0013f5be9b12e7da06f0dd11a7bdc4e0db8ef33832acc23b183bd0a2c1408a757a0019d9ac55ea1a615d92965e04d960cb3be7bff121a381424f1f22865bd582e09a001def04412e76df26fefe7b0ed5e10580918ae4f355b074c0cfe5d0259157869a0011c11a415db57e43db07aef0de9280b591d65ca0cce36c7002507f8191e5d4a80a0c89b59970b119187d97ad70539f1624bbede92648e2dc007890f9658a88756c5a06fb2e3d4ce2c438c0856c2de34948b7032b1aadc4642a9666228ea8cdc7786b7")[..]);
2618
2619        let new_payload = ExecutionPayloadV3 {
2620            payload_inner: ExecutionPayloadV2 {
2621                payload_inner: ExecutionPayloadV1 {
2622                    base_fee_per_gas:  U256::from(7u64),
2623                    block_number: 0xa946u64,
2624                    block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(),
2625                    logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(),
2626                    extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(),
2627                    gas_limit: 0x1c9c380,
2628                    gas_used: 0x1f4a9,
2629                    timestamp: 0x651f35b8,
2630                    fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
2631                    parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(),
2632                    prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(),
2633                    receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(),
2634                    state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(),
2635                    transactions: vec![first_transaction_raw, second_transaction_raw],
2636                },
2637                withdrawals: vec![],
2638            },
2639            blob_gas_used: 0xc0000,
2640            excess_blob_gas: 0x580000,
2641        };
2642
2643        let mut block: Block<TxEnvelope> = new_payload.clone().try_into_block().unwrap();
2644
2645        // this newPayload came with a parent beacon block root, we need to manually insert it
2646        // before hashing
2647        let parent_beacon_block_root =
2648            b256!("531cd53b8e68deef0ea65edfa3cda927a846c307b0907657af34bc3f313b5871");
2649        block.header.parent_beacon_block_root = Some(parent_beacon_block_root);
2650
2651        let converted_payload = ExecutionPayloadV3::from_block_unchecked(block.hash_slow(), &block);
2652
2653        // ensure the payloads are the same
2654        assert_eq!(new_payload, converted_payload);
2655    }
2656
2657    #[test]
2658    fn payload_to_block_rejects_network_encoded_tx() {
2659        let first_transaction_raw = Bytes::from_static(&hex!("b9017e02f9017a8501a1f0ff438211cc85012a05f2008512a05f2000830249f094d5409474fd5a725eab2ac9a8b26ca6fb51af37ef80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000001bdd2ed4b616c800000000000000000000000000001e9ee781dd4b97bdef92e5d1785f73a1f931daa20000000000000000000000007a40026a3b9a41754a95eec8c92c6b99886f440c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009ae80eb647dd09968488fa1d7e412bf8558a0b7a0000000000000000000000000f9815537d361cb02befd9918c95c97d4d8a4a2bc001a0ba8f1928bb0efc3fcd01524a2039a9a2588fa567cd9a7cc18217e05c615e9d69a0544bfd11425ac7748e76b3795b57a5563e2b0eff47b5428744c62ff19ccfc305")[..]);
2660        let second_transaction_raw = Bytes::from_static(&hex!("b9013c03f901388501a1f0ff430c843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c005f8c6a00195f6dff17753fc89b60eac6477026a805116962c9e412de8015c0484e661c1a001aae314061d4f5bbf158f15d9417a238f9589783f58762cd39d05966b3ba2fba0013f5be9b12e7da06f0dd11a7bdc4e0db8ef33832acc23b183bd0a2c1408a757a0019d9ac55ea1a615d92965e04d960cb3be7bff121a381424f1f22865bd582e09a001def04412e76df26fefe7b0ed5e10580918ae4f355b074c0cfe5d0259157869a0011c11a415db57e43db07aef0de9280b591d65ca0cce36c7002507f8191e5d4a80a0c89b59970b119187d97ad70539f1624bbede92648e2dc007890f9658a88756c5a06fb2e3d4ce2c438c0856c2de34948b7032b1aadc4642a9666228ea8cdc7786b7")[..]);
2661
2662        let new_payload = ExecutionPayloadV3 {
2663            payload_inner: ExecutionPayloadV2 {
2664                payload_inner: ExecutionPayloadV1 {
2665                    base_fee_per_gas:  U256::from(7u64),
2666                    block_number: 0xa946u64,
2667                    block_hash: hex!("a5ddd3f286f429458a39cafc13ffe89295a7efa8eb363cf89a1a4887dbcf272b").into(),
2668                    logs_bloom: hex!("00200004000000000000000080000000000200000000000000000000000000000000200000000000000000000000000000000000800000000200000000000000000000000000000000000008000000200000000000000000000001000000000000000000000000000000800000000000000000000100000000000030000000000000000040000000000000000000000000000000000800080080404000000000000008000000000008200000000000200000000000000000000000000000000000000002000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000100000000000000000000").into(),
2669                    extra_data: hex!("d883010d03846765746888676f312e32312e31856c696e7578").into(),
2670                    gas_limit: 0x1c9c380,
2671                    gas_used: 0x1f4a9,
2672                    timestamp: 0x651f35b8,
2673                    fee_recipient: hex!("f97e180c050e5ab072211ad2c213eb5aee4df134").into(),
2674                    parent_hash: hex!("d829192799c73ef28a7332313b3c03af1f2d5da2c36f8ecfafe7a83a3bfb8d1e").into(),
2675                    prev_randao: hex!("753888cc4adfbeb9e24e01c84233f9d204f4a9e1273f0e29b43c4c148b2b8b7e").into(),
2676                    receipts_root: hex!("4cbc48e87389399a0ea0b382b1c46962c4b8e398014bf0cc610f9c672bee3155").into(),
2677                    state_root: hex!("017d7fa2b5adb480f5e05b2c95cb4186e12062eed893fc8822798eed134329d1").into(),
2678                    transactions: vec![first_transaction_raw, second_transaction_raw],
2679                },
2680                withdrawals: vec![],
2681            },
2682            blob_gas_used: 0xc0000,
2683            excess_blob_gas: 0x580000,
2684        };
2685
2686        let _block = new_payload
2687            .try_into_block::<TxEnvelope>()
2688            .expect_err("execution payload conversion requires typed txs without a rlp header");
2689    }
2690
2691    #[test]
2692    fn devnet_invalid_block_hash_repro() {
2693        let deser_block = r#"
2694        {
2695            "parentHash": "0xae8315ee86002e6269a17dd1e9516a6cf13223e9d4544d0c32daff826fb31acc",
2696            "feeRecipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134",
2697            "stateRoot": "0x03787f1579efbaa4a8234e72465eb4e29ef7e62f61242d6454661932e1a282a1",
2698            "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
2699            "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
2700            "prevRandao": "0x918e86b497dc15de7d606457c36ca583e24d9b0a110a814de46e33d5bb824a66",
2701            "blockNumber": "0x6a784",
2702            "gasLimit": "0x1c9c380",
2703            "gasUsed": "0x0",
2704            "timestamp": "0x65bc1d60",
2705            "extraData": "0x9a726574682f76302e312e302d616c7068612e31362f6c696e7578",
2706            "baseFeePerGas": "0x8",
2707            "blobGasUsed": "0x0",
2708            "excessBlobGas": "0x0",
2709            "blockHash": "0x340c157eca9fd206b87c17f0ecbe8d411219de7188a0a240b635c88a96fe91c5",
2710            "transactions": [],
2711            "withdrawals": [
2712                {
2713                    "index": "0x5ab202",
2714                    "validatorIndex": "0xb1b",
2715                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2716                    "amount": "0x19b3d"
2717                },
2718                {
2719                    "index": "0x5ab203",
2720                    "validatorIndex": "0xb1c",
2721                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2722                    "amount": "0x15892"
2723                },
2724                {
2725                    "index": "0x5ab204",
2726                    "validatorIndex": "0xb1d",
2727                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2728                    "amount": "0x19b3d"
2729                },
2730                {
2731                    "index": "0x5ab205",
2732                    "validatorIndex": "0xb1e",
2733                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2734                    "amount": "0x19b3d"
2735                },
2736                {
2737                    "index": "0x5ab206",
2738                    "validatorIndex": "0xb1f",
2739                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2740                    "amount": "0x19b3d"
2741                },
2742                {
2743                    "index": "0x5ab207",
2744                    "validatorIndex": "0xb20",
2745                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2746                    "amount": "0x19b3d"
2747                },
2748                {
2749                    "index": "0x5ab208",
2750                    "validatorIndex": "0xb21",
2751                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2752                    "amount": "0x15892"
2753                },
2754                {
2755                    "index": "0x5ab209",
2756                    "validatorIndex": "0xb22",
2757                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2758                    "amount": "0x19b3d"
2759                },
2760                {
2761                    "index": "0x5ab20a",
2762                    "validatorIndex": "0xb23",
2763                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2764                    "amount": "0x19b3d"
2765                },
2766                {
2767                    "index": "0x5ab20b",
2768                    "validatorIndex": "0xb24",
2769                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2770                    "amount": "0x17db2"
2771                },
2772                {
2773                    "index": "0x5ab20c",
2774                    "validatorIndex": "0xb25",
2775                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2776                    "amount": "0x19b3d"
2777                },
2778                {
2779                    "index": "0x5ab20d",
2780                    "validatorIndex": "0xb26",
2781                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2782                    "amount": "0x19b3d"
2783                },
2784                {
2785                    "index": "0x5ab20e",
2786                    "validatorIndex": "0xa91",
2787                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2788                    "amount": "0x15892"
2789                },
2790                {
2791                    "index": "0x5ab20f",
2792                    "validatorIndex": "0xa92",
2793                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2794                    "amount": "0x1c05d"
2795                },
2796                {
2797                    "index": "0x5ab210",
2798                    "validatorIndex": "0xa93",
2799                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2800                    "amount": "0x15892"
2801                },
2802                {
2803                    "index": "0x5ab211",
2804                    "validatorIndex": "0xa94",
2805                    "address": "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5",
2806                    "amount": "0x19b3d"
2807                }
2808            ]
2809        }
2810        "#;
2811
2812        // deserialize payload
2813        let payload: ExecutionPayload =
2814            serde_json::from_str::<ExecutionPayloadV3>(deser_block).unwrap().into();
2815
2816        // NOTE: the actual block hash here is incorrect, it is a result of a bug, this was the
2817        // fix:
2818        // <https://github.com/paradigmxyz/reth/pull/6328>
2819        let block_hash_with_blob_fee_fields =
2820            b256!("a7cdd5f9e54147b53a15833a8c45dffccbaed534d7fdc23458f45102a4bf71b0");
2821
2822        let versioned_hashes = vec![];
2823        let parent_beacon_block_root =
2824            b256!("1162de8a0f4d20d86b9ad6e0a2575ab60f00a433dc70d9318c8abc9041fddf54");
2825
2826        // set up cancun payload fields
2827        let cancun_fields = CancunPayloadFields { parent_beacon_block_root, versioned_hashes };
2828
2829        // convert into block
2830        let block = payload
2831            .try_into_block_with_sidecar::<TxEnvelope>(&ExecutionPayloadSidecar::v3(cancun_fields))
2832            .unwrap();
2833
2834        // Ensure the actual hash is calculated if we set the fields to what they should be
2835        assert_eq!(block_hash_with_blob_fee_fields, block.header.hash_slow());
2836    }
2837
2838    #[test]
2839    fn test_payload_to_block_with_sidecar_raw() {
2840        use std::path::PathBuf;
2841
2842        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata/payload");
2843        let dir = std::fs::read_dir(path).expect("Unable to read payload folder");
2844
2845        for entry in dir {
2846            let entry = entry.expect("Unable to read entry");
2847            let path = entry.path();
2848
2849            if path.extension().and_then(|s| s.to_str()) != Some("json") {
2850                continue;
2851            }
2852
2853            let contents = std::fs::read_to_string(&path).expect("Unable to read file");
2854            let value: serde_json::Value = serde_json::from_str(&contents)
2855                .unwrap_or_else(|e| panic!("Failed to parse JSON from {path:?}: {e}"));
2856
2857            // Extract the newPayload object
2858            let new_payload = &value["newPayload"];
2859            let payload_value = &new_payload["payload"];
2860            let sidecar_value = &new_payload["sidecar"];
2861
2862            let payload: ExecutionPayload = serde_json::from_value(payload_value.clone())
2863                .unwrap_or_else(|e| panic!("Failed to deserialize payload from {path:?}: {e}"));
2864
2865            // Deserialize the sidecar
2866            let sidecar: ExecutionPayloadSidecar = serde_json::from_value(sidecar_value.clone())
2867                .unwrap_or_else(|e| panic!("Failed to deserialize sidecar from {path:?}: {e}"));
2868
2869            // Convert to block with raw transactions
2870            let block = payload.clone().into_block_with_sidecar_raw(&sidecar).unwrap_or_else(|e| {
2871                panic!("Failed to convert payload to block from {path:?}: {e}")
2872            });
2873
2874            // Verify the block has raw transactions (Bytes) if there are any
2875            if let Some(tx_count) = payload_value["transactions"].as_array().map(|a| a.len()) {
2876                assert_eq!(
2877                    block.body.transactions.len(),
2878                    tx_count,
2879                    "Transaction count mismatch in {:?}",
2880                    path
2881                );
2882            }
2883
2884            // Verify sidecar fields are applied
2885            assert_eq!(
2886                block.header.parent_beacon_block_root,
2887                sidecar.parent_beacon_block_root(),
2888                "Parent beacon block root mismatch in {:?}",
2889                path
2890            );
2891            assert_eq!(
2892                block.header.requests_hash,
2893                sidecar.requests_hash(),
2894                "Requests hash mismatch in {:?}",
2895                path
2896            );
2897
2898            // Verify the block hash matches the one in the payload
2899            let expected_hash = payload_value["blockHash"]
2900                .as_str()
2901                .unwrap()
2902                .parse::<B256>()
2903                .unwrap_or_else(|e| panic!("Failed to parse block hash from {path:?}: {e}"));
2904            let actual_hash = block.header.hash_slow();
2905            assert_eq!(
2906                actual_hash, expected_hash,
2907                "Block hash mismatch in {:?}: expected {}, got {}",
2908                path, expected_hash, actual_hash
2909            );
2910
2911            let block =
2912                payload.try_into_block_with_sidecar::<TxEnvelope>(&sidecar).unwrap_or_else(|e| {
2913                    panic!("Failed to convert payload to block from {path:?}: {e}")
2914                });
2915            let actual_hash = block.header.hash_slow();
2916            assert_eq!(
2917                actual_hash, expected_hash,
2918                "Block hash mismatch in {:?}: expected {}, got {}",
2919                path, expected_hash, actual_hash
2920            );
2921        }
2922    }
2923
2924    #[test]
2925    fn test_decoded_transactions() {
2926        let transaction = Bytes::from_static(&hex!("f86d0a8458b20efd825208946177843db3138ae69679a54b95cf345ed759450d8806f3e8d87878800080820a95a0f8bddb1dcc4558b532ff747760a6f547dd275afdbe7bdecc90680e71de105757a014f34ba38c180913c0543b0ac2eccfb77cc3f801a535008dc50e533fbe435f53"));
2927
2928        let payload = ExecutionPayload::V1(ExecutionPayloadV1 {
2929            parent_hash: B256::default(),
2930            fee_recipient: Address::default(),
2931            state_root: B256::default(),
2932            receipts_root: B256::default(),
2933            logs_bloom: Bloom::default(),
2934            prev_randao: B256::default(),
2935            block_number: 0,
2936            gas_limit: 0,
2937            gas_used: 0,
2938            timestamp: 0,
2939            extra_data: Bytes::default(),
2940            base_fee_per_gas: U256::default(),
2941            block_hash: B256::default(),
2942            transactions: vec![transaction.clone()],
2943        });
2944
2945        // Test decoded_transactions
2946        let decoded: Vec<_> = payload.decoded_transactions::<TxEnvelope>().collect();
2947        assert_eq!(decoded.len(), 1);
2948        assert!(decoded[0].is_ok(), "Failed to decode transaction: {:?}", decoded[0]);
2949
2950        // Test decoded_transactions_with_encoded
2951        let decoded_with_encoded: Vec<_> =
2952            payload.decoded_transactions_with_encoded::<TxEnvelope>().collect();
2953        assert_eq!(decoded_with_encoded.len(), 1);
2954        assert!(decoded_with_encoded[0].is_ok());
2955        if let Ok(with_encoded) = &decoded_with_encoded[0] {
2956            assert_eq!(with_encoded.encoded_bytes(), &transaction);
2957        }
2958    }
2959}