Skip to main content

reth_node_ethereum/
engine_ssz_containers.rs

1//! Experimental Engine API v2 REST-SSZ wire types.
2//!
3//! These types intentionally live apart from the legacy JSON-RPC Engine API types because their
4//! SSZ encodings are not always wire-compatible. This module contains the shared endpoint
5//! containers, fork-specific payload containers, blob containers, payload-body containers, and the
6//! experimental payload-with-witness response type from the same REST-SSZ model.
7
8use alloy_eips::{
9    eip4844::{Blob, BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofsV1, Bytes48},
10    eip4895::Withdrawal,
11    eip7594::Cell,
12    eip7685::Requests,
13};
14use alloy_primitives::{Address, Bytes, B128, B256, U256};
15use alloy_rpc_types_engine::{
16    BlobsBundleV1, BlobsBundleV2, ExecutionPayloadBodyV1 as LegacyExecutionPayloadBodyV1,
17    ExecutionPayloadBodyV2 as LegacyExecutionPayloadBodyV2,
18    ExecutionPayloadEnvelopeV2 as LegacyBuiltPayloadShanghai,
19    ExecutionPayloadEnvelopeV4 as LegacyBuiltPayloadPrague,
20    ExecutionPayloadEnvelopeV5 as LegacyBuiltPayloadOsaka,
21    ExecutionPayloadEnvelopeV6 as LegacyBuiltPayloadAmsterdam, ExecutionPayloadFieldV2,
22    ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ExecutionPayloadV4,
23    ForkchoiceState, ForkchoiceUpdated as LegacyForkchoice,
24    PayloadAttributes as LegacyPayloadAttributes, PayloadId, PayloadStatus as LegacyPayloadStatus,
25    PayloadStatusEnum,
26};
27
28type ErrorBytes = Vec<u8>;
29
30/// Maximum number of blobs in a REST-SSZ blob request or response.
31pub const MAX_BLOBS_REQUEST: usize = 128;
32
33/// Maximum number of payload bodies in a REST-SSZ request or response.
34pub const MAX_BODIES_REQUEST: usize = 32;
35
36/// An Engine API v2 SSZ optional encoded as `List[T, 1]`.
37///
38/// This differs from [`Option<T>`]'s `ethereum_ssz` encoding, which uses an SSZ union.
39#[derive(Clone, Debug, Default, PartialEq, Eq)]
40pub struct Optional<T>(Vec<T>);
41
42impl<T> Optional<T> {
43    /// Creates an absent optional.
44    pub const fn none() -> Self {
45        Self(Vec::new())
46    }
47
48    /// Creates a present optional.
49    pub fn some(value: T) -> Self {
50        Self(vec![value])
51    }
52
53    /// Returns the contained value, if present.
54    pub fn as_ref(&self) -> Option<&T> {
55        self.0.first()
56    }
57
58    /// Returns true if no value is present.
59    pub const fn is_none(&self) -> bool {
60        self.0.is_empty()
61    }
62
63    /// Returns true if a value is present.
64    pub const fn is_some(&self) -> bool {
65        !self.is_none()
66    }
67
68    /// Converts into a Rust optional.
69    pub fn into_option(mut self) -> Option<T> {
70        self.0.pop()
71    }
72}
73
74impl<T> From<Option<T>> for Optional<T> {
75    fn from(value: Option<T>) -> Self {
76        value.map_or_else(Self::none, Self::some)
77    }
78}
79
80impl<T> From<Optional<T>> for Option<T> {
81    fn from(value: Optional<T>) -> Self {
82        value.into_option()
83    }
84}
85
86impl<T: ssz::Encode> ssz::Encode for Optional<T> {
87    fn is_ssz_fixed_len() -> bool {
88        false
89    }
90
91    fn ssz_bytes_len(&self) -> usize {
92        self.0.ssz_bytes_len()
93    }
94
95    fn ssz_append(&self, buf: &mut Vec<u8>) {
96        self.0.ssz_append(buf);
97    }
98}
99
100impl<T: ssz::Decode> ssz::Decode for Optional<T> {
101    fn is_ssz_fixed_len() -> bool {
102        false
103    }
104
105    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
106        let values = Vec::<T>::from_ssz_bytes(bytes)?;
107        if values.len() > 1 {
108            return Err(ssz::DecodeError::BytesInvalid("optional has more than one value".into()))
109        }
110        Ok(Self(values))
111    }
112}
113
114/// Engine API v2 REST-SSZ payload status.
115///
116/// This is separate from the legacy status because REST-SSZ uses `Optional` fields instead of
117/// zero-value sentinels and legacy byte lists.
118#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct PayloadStatus {
120    /// Payload validation status.
121    pub status: PayloadStatusEnum,
122    /// Most recent valid block hash.
123    pub latest_valid_hash: Optional<B256>,
124    /// Optional payload validation error bytes.
125    pub validation_error: Optional<ErrorBytes>,
126}
127
128const MAX_ERROR_BYTES: usize = 1024;
129
130const fn status_code(status: &PayloadStatusEnum) -> u8 {
131    match status {
132        PayloadStatusEnum::Valid => 0,
133        PayloadStatusEnum::Invalid { .. } => 1,
134        PayloadStatusEnum::Syncing => 2,
135        PayloadStatusEnum::Accepted => 3,
136    }
137}
138
139fn status_from_code(code: u8) -> Result<PayloadStatusEnum, ssz::DecodeError> {
140    match code {
141        0 => Ok(PayloadStatusEnum::Valid),
142        1 => Ok(PayloadStatusEnum::Invalid { validation_error: String::new() }),
143        2 => Ok(PayloadStatusEnum::Syncing),
144        3 => Ok(PayloadStatusEnum::Accepted),
145        _ => Err(ssz::DecodeError::BytesInvalid("unknown payload status code".into())),
146    }
147}
148
149fn legacy_validation_error(
150    status: &PayloadStatusEnum,
151) -> Result<Optional<ErrorBytes>, ConversionError> {
152    match status {
153        PayloadStatusEnum::Invalid { validation_error } => {
154            let bytes = validation_error.as_bytes().to_vec();
155            if bytes.len() > MAX_ERROR_BYTES {
156                return Err(ConversionError::ErrorBytesTooLong)
157            }
158            Ok(Optional::some(bytes))
159        }
160        _ => Ok(Optional::none()),
161    }
162}
163
164impl ssz::Encode for PayloadStatus {
165    fn is_ssz_fixed_len() -> bool {
166        false
167    }
168
169    fn ssz_bytes_len(&self) -> usize {
170        1 + ssz::BYTES_PER_LENGTH_OFFSET * 2 +
171            self.latest_valid_hash.ssz_bytes_len() +
172            self.validation_error.ssz_bytes_len()
173    }
174
175    fn ssz_append(&self, buf: &mut Vec<u8>) {
176        let mut encoder = ssz::SszEncoder::container(buf, 1 + ssz::BYTES_PER_LENGTH_OFFSET * 2);
177        encoder.append(&status_code(&self.status));
178        encoder.append(&self.latest_valid_hash);
179        encoder.append(&self.validation_error);
180        encoder.finalize();
181    }
182}
183
184impl ssz::Decode for PayloadStatus {
185    fn is_ssz_fixed_len() -> bool {
186        false
187    }
188
189    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
190        let mut builder = ssz::SszDecoderBuilder::new(bytes);
191        builder.register_type::<u8>()?;
192        builder.register_type::<Optional<B256>>()?;
193        builder.register_type::<Optional<ErrorBytes>>()?;
194        let mut decoder = builder.build()?;
195        let mut status = status_from_code(decoder.decode_next()?)?;
196        let latest_valid_hash = decoder.decode_next()?;
197        let validation_error: Optional<ErrorBytes> = decoder.decode_next()?;
198        if let PayloadStatusEnum::Invalid { validation_error: error } = &mut status {
199            *error = match validation_error.as_ref() {
200                Some(error) => String::from_utf8(error.clone())
201                    .map_err(|err| ssz::DecodeError::BytesInvalid(err.to_string()))?,
202                None => String::new(),
203            };
204            if validation_error.as_ref().is_some_and(|error| error.len() > MAX_ERROR_BYTES) {
205                return Err(ssz::DecodeError::BytesInvalid(
206                    "payload validation error is too long".into(),
207                ))
208            }
209        } else if validation_error.is_some() {
210            return Err(ssz::DecodeError::BytesInvalid(
211                "validation error is only valid for INVALID status".into(),
212            ));
213        }
214        Ok(Self { status, latest_valid_hash, validation_error })
215    }
216}
217
218/// Error converting legacy Engine API values into v2 REST-SSZ values.
219#[derive(Clone, Debug, PartialEq, Eq)]
220pub enum ConversionError {
221    /// Payload validation error exceeded the REST-SSZ byte bound.
222    ErrorBytesTooLong,
223    /// `ACCEPTED` is not permitted in a forkchoice response.
224    AcceptedForkchoice,
225    /// A bounded REST-SSZ list exceeded its maximum length.
226    TooManyItems {
227        /// Name of the field that exceeded its bound.
228        field: &'static str,
229        /// Maximum permitted item count.
230        max: usize,
231        /// Actual item count.
232        actual: usize,
233    },
234}
235
236impl core::fmt::Display for ConversionError {
237    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
238        match self {
239            Self::ErrorBytesTooLong => f.write_str("payload validation error is too long"),
240            Self::AcceptedForkchoice => {
241                f.write_str("ACCEPTED is not valid in a forkchoice response")
242            }
243            Self::TooManyItems { field, max, actual } => {
244                write!(f, "too many {field}: expected at most {max}, got {actual}")
245            }
246        }
247    }
248}
249
250impl core::error::Error for ConversionError {}
251
252impl TryFrom<LegacyPayloadStatus> for PayloadStatus {
253    type Error = ConversionError;
254
255    fn try_from(value: LegacyPayloadStatus) -> Result<Self, Self::Error> {
256        let validation_error = legacy_validation_error(&value.status)?;
257        Ok(Self {
258            status: value.status,
259            latest_valid_hash: value.latest_valid_hash.into(),
260            validation_error,
261        })
262    }
263}
264
265impl From<PayloadStatus> for LegacyPayloadStatus {
266    fn from(value: PayloadStatus) -> Self {
267        let status = match value.status {
268            PayloadStatusEnum::Invalid { .. } => PayloadStatusEnum::Invalid {
269                validation_error: value
270                    .validation_error
271                    .as_ref()
272                    .and_then(|error| String::from_utf8(error.clone()).ok())
273                    .unwrap_or_default(),
274            },
275            status => status,
276        };
277        Self { status, latest_valid_hash: value.latest_valid_hash.into() }
278    }
279}
280
281/// Engine API v2 REST-SSZ forkchoice update response.
282///
283/// The REST response is a container of two variable fields, unlike the legacy fixed payload ID.
284#[derive(Clone, Debug, PartialEq, Eq)]
285pub struct ForkchoiceUpdateResponse {
286    /// Restricted payload status; `ACCEPTED` is invalid here.
287    pub payload_status: PayloadStatus,
288    /// Opaque server-assigned payload identifier.
289    pub payload_id: Optional<PayloadId>,
290}
291
292impl ssz::Encode for ForkchoiceUpdateResponse {
293    fn is_ssz_fixed_len() -> bool {
294        false
295    }
296
297    fn ssz_bytes_len(&self) -> usize {
298        ssz::BYTES_PER_LENGTH_OFFSET * 2 +
299            self.payload_status.ssz_bytes_len() +
300            self.payload_id.ssz_bytes_len()
301    }
302
303    fn ssz_append(&self, buf: &mut Vec<u8>) {
304        let mut encoder = ssz::SszEncoder::container(buf, ssz::BYTES_PER_LENGTH_OFFSET * 2);
305        encoder.append(&self.payload_status);
306        encoder.append(&self.payload_id);
307        encoder.finalize();
308    }
309}
310
311impl ssz::Decode for ForkchoiceUpdateResponse {
312    fn is_ssz_fixed_len() -> bool {
313        false
314    }
315
316    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
317        let mut builder = ssz::SszDecoderBuilder::new(bytes);
318        builder.register_type::<PayloadStatus>()?;
319        builder.register_type::<Optional<PayloadId>>()?;
320        let mut decoder = builder.build()?;
321        let response =
322            Self { payload_status: decoder.decode_next()?, payload_id: decoder.decode_next()? };
323        if matches!(response.payload_status.status, PayloadStatusEnum::Accepted) {
324            return Err(ssz::DecodeError::BytesInvalid(
325                "ACCEPTED is not valid in a forkchoice response".into(),
326            ));
327        }
328        Ok(response)
329    }
330}
331
332impl TryFrom<LegacyForkchoice> for ForkchoiceUpdateResponse {
333    type Error = ConversionError;
334
335    fn try_from(value: LegacyForkchoice) -> Result<Self, Self::Error> {
336        let payload_status = PayloadStatus::try_from(value.payload_status)?;
337        if matches!(payload_status.status, PayloadStatusEnum::Accepted) {
338            return Err(ConversionError::AcceptedForkchoice)
339        }
340        Ok(Self { payload_status, payload_id: value.payload_id.into() })
341    }
342}
343
344impl From<ForkchoiceUpdateResponse> for LegacyForkchoice {
345    fn from(value: ForkchoiceUpdateResponse) -> Self {
346        Self { payload_status: value.payload_status.into(), payload_id: value.payload_id.into() }
347    }
348}
349
350/// Paris execution payload.
351pub type ExecutionPayloadParis = ExecutionPayloadV1;
352
353/// Shanghai execution payload.
354pub type ExecutionPayloadShanghai = ExecutionPayloadV2;
355
356/// Cancun execution payload.
357pub type ExecutionPayloadCancun = ExecutionPayloadV3;
358
359/// Prague execution payload.
360pub type ExecutionPayloadPrague = ExecutionPayloadV3;
361
362/// Osaka execution payload.
363pub type ExecutionPayloadOsaka = ExecutionPayloadV3;
364
365/// Amsterdam execution payload.
366pub type ExecutionPayloadAmsterdam = ExecutionPayloadV4;
367
368/// Paris payload attributes.
369///
370/// Fork-specific attributes keep later-fork fields out of the SSZ body; the legacy type is a
371/// permissive superset.
372#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
373pub struct PayloadAttributesParis {
374    /// Payload timestamp.
375    pub timestamp: u64,
376    /// Previous RANDAO value.
377    pub prev_randao: B256,
378    /// Suggested fee recipient.
379    pub suggested_fee_recipient: Address,
380}
381
382/// Shanghai payload attributes.
383///
384/// Fork-specific attributes keep later-fork fields out of the SSZ body; the legacy type is a
385/// permissive superset.
386#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
387pub struct PayloadAttributesShanghai {
388    /// Payload timestamp.
389    pub timestamp: u64,
390    /// Previous RANDAO value.
391    pub prev_randao: B256,
392    /// Suggested fee recipient.
393    pub suggested_fee_recipient: Address,
394    /// Withdrawals to include in the payload.
395    pub withdrawals: Vec<Withdrawal>,
396}
397
398/// Cancun payload attributes.
399///
400/// Fork-specific attributes keep later-fork fields out of the SSZ body; the legacy type is a
401/// permissive superset.
402#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
403pub struct PayloadAttributesCancun {
404    /// Payload timestamp.
405    pub timestamp: u64,
406    /// Previous RANDAO value.
407    pub prev_randao: B256,
408    /// Suggested fee recipient.
409    pub suggested_fee_recipient: Address,
410    /// Withdrawals to include in the payload.
411    pub withdrawals: Vec<Withdrawal>,
412    /// Root of the parent beacon block.
413    pub parent_beacon_block_root: B256,
414}
415
416/// Prague uses the Cancun payload-attributes schema.
417pub type PayloadAttributesPrague = PayloadAttributesCancun;
418
419/// Osaka uses the Cancun payload-attributes schema.
420pub type PayloadAttributesOsaka = PayloadAttributesCancun;
421
422/// Amsterdam payload attributes.
423///
424/// Fork-specific attributes keep the Amsterdam-only fields in their defined SSZ position.
425#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
426pub struct PayloadAttributesAmsterdam {
427    /// Payload timestamp.
428    pub timestamp: u64,
429    /// Previous RANDAO value.
430    pub prev_randao: B256,
431    /// Suggested fee recipient.
432    pub suggested_fee_recipient: Address,
433    /// Withdrawals to include in the payload.
434    pub withdrawals: Vec<Withdrawal>,
435    /// Root of the parent beacon block.
436    pub parent_beacon_block_root: B256,
437    /// Consensus-layer slot number.
438    pub slot_number: u64,
439    /// Target gas limit.
440    pub target_gas_limit: u64,
441}
442
443/// Error converting legacy cross-fork payload attributes into a fork-specific SSZ container.
444#[derive(Clone, Copy, Debug, PartialEq, Eq)]
445pub enum PayloadAttributesConversionError {
446    /// A field required by the selected fork is absent.
447    MissingField(&'static str),
448    /// A field from a later fork is populated and would be lost.
449    UnexpectedField(&'static str),
450}
451
452impl core::fmt::Display for PayloadAttributesConversionError {
453    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
454        match self {
455            Self::MissingField(field) => {
456                write!(f, "missing required payload attributes field: {field}")
457            }
458            Self::UnexpectedField(field) => {
459                write!(f, "unexpected later-fork payload attributes field: {field}")
460            }
461        }
462    }
463}
464
465impl core::error::Error for PayloadAttributesConversionError {}
466
467const fn ensure_absent<T>(
468    value: &Option<T>,
469    field: &'static str,
470) -> Result<(), PayloadAttributesConversionError> {
471    if value.is_some() {
472        Err(PayloadAttributesConversionError::UnexpectedField(field))
473    } else {
474        Ok(())
475    }
476}
477
478fn require<T>(
479    value: Option<T>,
480    field: &'static str,
481) -> Result<T, PayloadAttributesConversionError> {
482    value.ok_or(PayloadAttributesConversionError::MissingField(field))
483}
484
485impl From<PayloadAttributesParis> for LegacyPayloadAttributes {
486    fn from(value: PayloadAttributesParis) -> Self {
487        Self {
488            timestamp: value.timestamp,
489            prev_randao: value.prev_randao,
490            suggested_fee_recipient: value.suggested_fee_recipient,
491            withdrawals: None,
492            parent_beacon_block_root: None,
493            slot_number: None,
494            ..Default::default()
495        }
496    }
497}
498
499impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesParis {
500    type Error = PayloadAttributesConversionError;
501
502    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
503        ensure_absent(&value.withdrawals, "withdrawals")?;
504        ensure_absent(&value.parent_beacon_block_root, "parent_beacon_block_root")?;
505        ensure_absent(&value.slot_number, "slot_number")?;
506        ensure_absent(&value.target_gas_limit, "target_gas_limit")?;
507        Ok(Self {
508            timestamp: value.timestamp,
509            prev_randao: value.prev_randao,
510            suggested_fee_recipient: value.suggested_fee_recipient,
511        })
512    }
513}
514
515impl From<PayloadAttributesShanghai> for LegacyPayloadAttributes {
516    fn from(value: PayloadAttributesShanghai) -> Self {
517        Self {
518            timestamp: value.timestamp,
519            prev_randao: value.prev_randao,
520            suggested_fee_recipient: value.suggested_fee_recipient,
521            withdrawals: Some(value.withdrawals),
522            parent_beacon_block_root: None,
523            slot_number: None,
524            ..Default::default()
525        }
526    }
527}
528
529impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesShanghai {
530    type Error = PayloadAttributesConversionError;
531
532    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
533        ensure_absent(&value.parent_beacon_block_root, "parent_beacon_block_root")?;
534        ensure_absent(&value.slot_number, "slot_number")?;
535        ensure_absent(&value.target_gas_limit, "target_gas_limit")?;
536        Ok(Self {
537            timestamp: value.timestamp,
538            prev_randao: value.prev_randao,
539            suggested_fee_recipient: value.suggested_fee_recipient,
540            withdrawals: require(value.withdrawals, "withdrawals")?,
541        })
542    }
543}
544
545impl From<PayloadAttributesCancun> for LegacyPayloadAttributes {
546    fn from(value: PayloadAttributesCancun) -> Self {
547        Self {
548            timestamp: value.timestamp,
549            prev_randao: value.prev_randao,
550            suggested_fee_recipient: value.suggested_fee_recipient,
551            withdrawals: Some(value.withdrawals),
552            parent_beacon_block_root: Some(value.parent_beacon_block_root),
553            slot_number: None,
554            ..Default::default()
555        }
556    }
557}
558
559impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesCancun {
560    type Error = PayloadAttributesConversionError;
561
562    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
563        ensure_absent(&value.slot_number, "slot_number")?;
564        ensure_absent(&value.target_gas_limit, "target_gas_limit")?;
565        Ok(Self {
566            timestamp: value.timestamp,
567            prev_randao: value.prev_randao,
568            suggested_fee_recipient: value.suggested_fee_recipient,
569            withdrawals: require(value.withdrawals, "withdrawals")?,
570            parent_beacon_block_root: require(
571                value.parent_beacon_block_root,
572                "parent_beacon_block_root",
573            )?,
574        })
575    }
576}
577
578impl From<PayloadAttributesAmsterdam> for LegacyPayloadAttributes {
579    #[allow(clippy::needless_update)]
580    fn from(value: PayloadAttributesAmsterdam) -> Self {
581        Self {
582            timestamp: value.timestamp,
583            prev_randao: value.prev_randao,
584            suggested_fee_recipient: value.suggested_fee_recipient,
585            withdrawals: Some(value.withdrawals),
586            parent_beacon_block_root: Some(value.parent_beacon_block_root),
587            slot_number: Some(value.slot_number),
588            target_gas_limit: Some(value.target_gas_limit),
589            ..Default::default()
590        }
591    }
592}
593
594impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesAmsterdam {
595    type Error = PayloadAttributesConversionError;
596
597    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
598        Ok(Self {
599            timestamp: value.timestamp,
600            prev_randao: value.prev_randao,
601            suggested_fee_recipient: value.suggested_fee_recipient,
602            withdrawals: require(value.withdrawals, "withdrawals")?,
603            parent_beacon_block_root: require(
604                value.parent_beacon_block_root,
605                "parent_beacon_block_root",
606            )?,
607            slot_number: require(value.slot_number, "slot_number")?,
608            target_gas_limit: require(value.target_gas_limit, "target_gas_limit")?,
609        })
610    }
611}
612
613/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Paris.
614///
615/// Unlike the legacy `engine_getPayloadV1` response, this includes the expected block value.
616#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
617pub struct BuiltPayloadParis {
618    /// Execution payload V1.
619    pub payload: ExecutionPayloadParis,
620    /// The expected value to be received by the fee recipient in wei.
621    pub block_value: U256,
622}
623
624/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Shanghai.
625///
626/// This follows the legacy `engine_getPayloadV2` payload-build response shape: execution payload
627/// plus block value only. `should_override_builder` starts at Cancun.
628/// The concrete V2 payload prevents the legacy V1/V2 untagged field from accepting a Paris payload.
629#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
630pub struct BuiltPayloadShanghai {
631    /// Execution payload V2.
632    pub payload: ExecutionPayloadShanghai,
633    /// The expected value to be received by the fee recipient in wei.
634    pub block_value: U256,
635}
636
637/// Engine API v2 REST-SSZ payload-build response for Cancun.
638///
639/// This is wire-compatible with the legacy `engine_getPayloadV3` response envelope.
640pub type BuiltPayloadCancun = alloy_rpc_types_engine::ExecutionPayloadEnvelopeV3;
641
642/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Prague.
643///
644/// Unlike the legacy [`alloy_rpc_types_engine::ExecutionPayloadEnvelopeV4`],
645/// `execution_requests` precedes `should_override_builder` in the normative SSZ field order.
646#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
647pub struct BuiltPayloadPrague {
648    /// Execution payload V3.
649    pub payload: ExecutionPayloadPrague,
650    /// The expected value to be received by the fee recipient in wei.
651    pub block_value: U256,
652    /// The blobs, commitments, and proofs associated with the executed payload.
653    pub blobs_bundle: BlobsBundleV1,
654    /// A list of opaque EIP-7685 requests.
655    pub execution_requests: Requests,
656    /// A suggestion from the execution layer whether this payload should be used instead of an
657    /// externally provided one.
658    pub should_override_builder: bool,
659}
660
661/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Osaka.
662///
663/// It is separate from legacy V5 because REST-SSZ places `execution_requests` before the builder
664/// override flag.
665#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
666pub struct BuiltPayloadOsaka {
667    /// Execution payload V3.
668    pub payload: ExecutionPayloadOsaka,
669    /// The expected value to be received by the fee recipient in wei.
670    pub block_value: U256,
671    /// The blobs, commitments, and EIP-7594 cell proofs associated with the executed payload.
672    pub blobs_bundle: BlobsBundleV2,
673    /// A list of opaque EIP-7685 requests.
674    pub execution_requests: Requests,
675    /// A suggestion from the execution layer whether this payload should be used instead of an
676    /// externally provided one.
677    pub should_override_builder: bool,
678}
679
680/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Amsterdam.
681///
682/// It is separate from legacy V6 because REST-SSZ places `execution_requests` before the builder
683/// override flag.
684#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
685pub struct BuiltPayloadAmsterdam {
686    /// Execution payload V4.
687    pub payload: ExecutionPayloadAmsterdam,
688    /// The expected value to be received by the fee recipient in wei.
689    pub block_value: U256,
690    /// The blobs, commitments, and EIP-7594 cell proofs associated with the executed payload.
691    pub blobs_bundle: BlobsBundleV2,
692    /// A list of opaque EIP-7685 requests.
693    pub execution_requests: Requests,
694    /// A suggestion from the execution layer whether this payload should be used instead of an
695    /// externally provided one.
696    pub should_override_builder: bool,
697}
698
699/// Error converting legacy payload-build envelopes into fork-specific REST-SSZ containers.
700#[derive(Clone, Copy, Debug, PartialEq, Eq)]
701pub enum BuiltPayloadConversionError {
702    /// The legacy envelope carried an execution payload from a different fork.
703    UnexpectedPayloadFork(&'static str),
704}
705
706impl core::fmt::Display for BuiltPayloadConversionError {
707    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
708        match self {
709            Self::UnexpectedPayloadFork(fork) => {
710                write!(f, "unexpected execution payload fork: {fork}")
711            }
712        }
713    }
714}
715
716impl core::error::Error for BuiltPayloadConversionError {}
717
718impl From<BuiltPayloadShanghai> for LegacyBuiltPayloadShanghai {
719    fn from(value: BuiltPayloadShanghai) -> Self {
720        Self {
721            execution_payload: ExecutionPayloadFieldV2::V2(value.payload),
722            block_value: value.block_value,
723        }
724    }
725}
726
727impl TryFrom<LegacyBuiltPayloadShanghai> for BuiltPayloadShanghai {
728    type Error = BuiltPayloadConversionError;
729
730    fn try_from(value: LegacyBuiltPayloadShanghai) -> Result<Self, Self::Error> {
731        match value.execution_payload {
732            ExecutionPayloadFieldV2::V2(payload) => {
733                Ok(Self { payload, block_value: value.block_value })
734            }
735            ExecutionPayloadFieldV2::V1(_) => {
736                Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
737            }
738        }
739    }
740}
741
742impl From<LegacyBuiltPayloadPrague> for BuiltPayloadPrague {
743    fn from(value: LegacyBuiltPayloadPrague) -> Self {
744        Self {
745            payload: value.envelope_inner.execution_payload,
746            block_value: value.envelope_inner.block_value,
747            blobs_bundle: value.envelope_inner.blobs_bundle,
748            execution_requests: value.execution_requests,
749            should_override_builder: value.envelope_inner.should_override_builder,
750        }
751    }
752}
753
754impl From<BuiltPayloadPrague> for LegacyBuiltPayloadPrague {
755    fn from(value: BuiltPayloadPrague) -> Self {
756        Self {
757            envelope_inner: alloy_rpc_types_engine::ExecutionPayloadEnvelopeV3 {
758                execution_payload: value.payload,
759                block_value: value.block_value,
760                blobs_bundle: value.blobs_bundle,
761                should_override_builder: value.should_override_builder,
762            },
763            execution_requests: value.execution_requests,
764        }
765    }
766}
767
768impl From<LegacyBuiltPayloadOsaka> for BuiltPayloadOsaka {
769    fn from(value: LegacyBuiltPayloadOsaka) -> Self {
770        Self {
771            payload: value.execution_payload,
772            block_value: value.block_value,
773            blobs_bundle: value.blobs_bundle,
774            execution_requests: value.execution_requests,
775            should_override_builder: value.should_override_builder,
776        }
777    }
778}
779
780impl From<BuiltPayloadOsaka> for LegacyBuiltPayloadOsaka {
781    fn from(value: BuiltPayloadOsaka) -> Self {
782        Self {
783            execution_payload: value.payload,
784            block_value: value.block_value,
785            blobs_bundle: value.blobs_bundle,
786            should_override_builder: value.should_override_builder,
787            execution_requests: value.execution_requests,
788        }
789    }
790}
791
792impl From<LegacyBuiltPayloadAmsterdam> for BuiltPayloadAmsterdam {
793    fn from(value: LegacyBuiltPayloadAmsterdam) -> Self {
794        Self {
795            payload: value.execution_payload,
796            block_value: value.block_value,
797            blobs_bundle: value.blobs_bundle,
798            execution_requests: value.execution_requests,
799            should_override_builder: value.should_override_builder,
800        }
801    }
802}
803
804impl From<BuiltPayloadAmsterdam> for LegacyBuiltPayloadAmsterdam {
805    fn from(value: BuiltPayloadAmsterdam) -> Self {
806        Self {
807            execution_payload: value.payload,
808            block_value: value.block_value,
809            blobs_bundle: value.blobs_bundle,
810            should_override_builder: value.should_override_builder,
811            execution_requests: value.execution_requests,
812        }
813    }
814}
815
816/// REST-SSZ payload-submission request containers.
817///
818/// These are distinct from the legacy Engine JSON-RPC get-payload envelopes: submission requests
819/// are fork-specific request bodies, while the legacy envelope types mostly model get-payload
820/// responses and sometimes carry response-only fields such as block value, blob bundles, builder
821/// override hints, or a different field order.
822///
823/// Paris payload-submission request.
824///
825/// The single-field container is required by REST-SSZ; the legacy endpoint submitted a bare
826/// payload.
827#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
828pub struct ExecutionPayloadEnvelopeParis {
829    /// Submitted execution payload.
830    pub payload: ExecutionPayloadParis,
831}
832
833/// Shanghai payload-submission request.
834///
835/// The single-field container is required by REST-SSZ and fixes the payload fork at decode time.
836#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
837pub struct ExecutionPayloadEnvelopeShanghai {
838    /// Submitted execution payload.
839    pub payload: ExecutionPayloadShanghai,
840}
841
842/// Cancun payload-submission request.
843///
844/// Cancun adds the parent beacon block root to the REST request envelope.
845#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
846pub struct ExecutionPayloadEnvelopeCancun {
847    /// Submitted execution payload.
848    pub payload: ExecutionPayloadCancun,
849    /// Root of the parent beacon block.
850    pub parent_beacon_block_root: B256,
851}
852
853/// Prague payload-submission request.
854///
855/// Prague adds execution requests to the REST request envelope.
856#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
857pub struct ExecutionPayloadEnvelopePrague {
858    /// Submitted execution payload.
859    pub payload: ExecutionPayloadPrague,
860    /// Root of the parent beacon block.
861    pub parent_beacon_block_root: B256,
862    /// EIP-7685 execution requests.
863    pub execution_requests: Requests,
864}
865
866/// Osaka payload-submission request.
867///
868/// Osaka keeps the REST envelope shape while selecting the Osaka payload schema.
869#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
870pub struct ExecutionPayloadEnvelopeOsaka {
871    /// Submitted execution payload.
872    pub payload: ExecutionPayloadOsaka,
873    /// Root of the parent beacon block.
874    pub parent_beacon_block_root: B256,
875    /// EIP-7685 execution requests.
876    pub execution_requests: Requests,
877}
878
879/// Amsterdam payload-submission request.
880///
881/// Amsterdam selects the V4 payload while retaining the Cancun and Prague envelope fields.
882#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
883pub struct ExecutionPayloadEnvelopeAmsterdam {
884    /// Submitted execution payload.
885    pub payload: ExecutionPayloadAmsterdam,
886    /// Root of the parent beacon block.
887    pub parent_beacon_block_root: B256,
888    /// EIP-7685 execution requests.
889    pub execution_requests: Requests,
890}
891
892impl From<ExecutionPayloadParis> for ExecutionPayloadEnvelopeParis {
893    fn from(payload: ExecutionPayloadParis) -> Self {
894        Self { payload }
895    }
896}
897
898impl From<ExecutionPayloadShanghai> for ExecutionPayloadEnvelopeShanghai {
899    fn from(payload: ExecutionPayloadShanghai) -> Self {
900        Self { payload }
901    }
902}
903
904impl From<(ExecutionPayloadCancun, B256)> for ExecutionPayloadEnvelopeCancun {
905    fn from((payload, parent_beacon_block_root): (ExecutionPayloadCancun, B256)) -> Self {
906        Self { payload, parent_beacon_block_root }
907    }
908}
909
910impl From<(ExecutionPayloadPrague, B256, Requests)> for ExecutionPayloadEnvelopePrague {
911    fn from(
912        (payload, parent_beacon_block_root, execution_requests): (
913            ExecutionPayloadPrague,
914            B256,
915            Requests,
916        ),
917    ) -> Self {
918        Self { payload, parent_beacon_block_root, execution_requests }
919    }
920}
921
922impl From<(ExecutionPayloadOsaka, B256, Requests)> for ExecutionPayloadEnvelopeOsaka {
923    fn from(
924        (payload, parent_beacon_block_root, execution_requests): (
925            ExecutionPayloadOsaka,
926            B256,
927            Requests,
928        ),
929    ) -> Self {
930        Self { payload, parent_beacon_block_root, execution_requests }
931    }
932}
933
934impl From<(ExecutionPayloadAmsterdam, B256, Requests)> for ExecutionPayloadEnvelopeAmsterdam {
935    fn from(
936        (payload, parent_beacon_block_root, execution_requests): (
937            ExecutionPayloadAmsterdam,
938            B256,
939            Requests,
940        ),
941    ) -> Self {
942        Self { payload, parent_beacon_block_root, execution_requests }
943    }
944}
945
946/// Paris forkchoice-update request.
947///
948/// REST-SSZ uses an `Optional` field inside one container; legacy FCU used separate RPC
949/// parameters and a legacy `Option` encoding.
950#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
951pub struct ForkchoiceUpdateParis {
952    /// Current forkchoice state.
953    pub forkchoice_state: ForkchoiceState,
954    /// Optional Paris payload attributes.
955    pub payload_attributes: Optional<PayloadAttributesParis>,
956}
957
958/// Shanghai forkchoice-update request.
959#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
960pub struct ForkchoiceUpdateShanghai {
961    /// Current forkchoice state.
962    pub forkchoice_state: ForkchoiceState,
963    /// Optional Shanghai payload attributes.
964    pub payload_attributes: Optional<PayloadAttributesShanghai>,
965}
966
967/// Cancun forkchoice-update request.
968#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
969pub struct ForkchoiceUpdateCancun {
970    /// Current forkchoice state.
971    pub forkchoice_state: ForkchoiceState,
972    /// Optional Cancun payload attributes.
973    pub payload_attributes: Optional<PayloadAttributesCancun>,
974}
975
976/// Prague forkchoice-update request.
977#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
978pub struct ForkchoiceUpdatePrague {
979    /// Current forkchoice state.
980    pub forkchoice_state: ForkchoiceState,
981    /// Optional Prague payload attributes.
982    pub payload_attributes: Optional<PayloadAttributesPrague>,
983}
984
985/// Osaka forkchoice-update request.
986#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
987pub struct ForkchoiceUpdateOsaka {
988    /// Current forkchoice state.
989    pub forkchoice_state: ForkchoiceState,
990    /// Optional Osaka payload attributes.
991    pub payload_attributes: Optional<PayloadAttributesOsaka>,
992}
993
994/// Amsterdam forkchoice-update request.
995#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
996pub struct ForkchoiceUpdateAmsterdam {
997    /// Current forkchoice state.
998    pub forkchoice_state: ForkchoiceState,
999    /// Optional Amsterdam payload attributes.
1000    pub payload_attributes: Optional<PayloadAttributesAmsterdam>,
1001    /// Optional `Bitvector[128]` custody-column selection.
1002    pub custody_columns: Optional<B128>,
1003}
1004
1005/// Fork-specific execution payload body for Paris.
1006///
1007/// Paris omits withdrawals entirely; the legacy body keeps them as an optional union field.
1008#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1009pub struct ExecutionPayloadBodyParis {
1010    /// Enveloped encoded transactions.
1011    pub transactions: Vec<Bytes>,
1012}
1013
1014/// Fork-specific execution payload body for Shanghai.
1015///
1016/// Shanghai makes withdrawals a direct field rather than the legacy optional union.
1017#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1018pub struct ExecutionPayloadBodyShanghai {
1019    /// Enveloped encoded transactions.
1020    pub transactions: Vec<Bytes>,
1021    /// Withdrawals included in the block.
1022    pub withdrawals: Vec<Withdrawal>,
1023}
1024
1025/// Cancun uses the Shanghai execution-payload-body schema.
1026pub type ExecutionPayloadBodyCancun = ExecutionPayloadBodyShanghai;
1027
1028/// Prague uses the Shanghai execution-payload-body schema.
1029pub type ExecutionPayloadBodyPrague = ExecutionPayloadBodyShanghai;
1030
1031/// Osaka uses the Shanghai execution-payload-body schema.
1032pub type ExecutionPayloadBodyOsaka = ExecutionPayloadBodyShanghai;
1033
1034/// Fork-specific execution payload body for Amsterdam.
1035///
1036/// Amsterdam adds the block access list as a direct field rather than a legacy optional field.
1037#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1038pub struct ExecutionPayloadBodyAmsterdam {
1039    /// Enveloped encoded transactions.
1040    pub transactions: Vec<Bytes>,
1041    /// Withdrawals included in the block.
1042    pub withdrawals: Vec<Withdrawal>,
1043    /// RLP-encoded EIP-7928 block access list.
1044    pub block_access_list: Bytes,
1045}
1046
1047/// Error converting legacy cross-fork execution payload bodies into fork-specific containers.
1048#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1049pub enum ExecutionPayloadBodyConversionError {
1050    /// A field required by the selected fork is absent.
1051    MissingField(&'static str),
1052    /// A field from a later fork is populated and would be lost.
1053    UnexpectedField(&'static str),
1054}
1055
1056impl core::fmt::Display for ExecutionPayloadBodyConversionError {
1057    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1058        match self {
1059            Self::MissingField(field) => {
1060                write!(f, "missing required execution payload body field: {field}")
1061            }
1062            Self::UnexpectedField(field) => {
1063                write!(f, "unexpected later-fork execution payload body field: {field}")
1064            }
1065        }
1066    }
1067}
1068
1069impl core::error::Error for ExecutionPayloadBodyConversionError {}
1070
1071impl From<ExecutionPayloadBodyParis> for LegacyExecutionPayloadBodyV1 {
1072    fn from(value: ExecutionPayloadBodyParis) -> Self {
1073        Self { transactions: value.transactions, withdrawals: None }
1074    }
1075}
1076
1077impl TryFrom<LegacyExecutionPayloadBodyV1> for ExecutionPayloadBodyParis {
1078    type Error = ExecutionPayloadBodyConversionError;
1079
1080    fn try_from(value: LegacyExecutionPayloadBodyV1) -> Result<Self, Self::Error> {
1081        if value.withdrawals.is_some() {
1082            return Err(ExecutionPayloadBodyConversionError::UnexpectedField("withdrawals"))
1083        }
1084        Ok(Self { transactions: value.transactions })
1085    }
1086}
1087
1088impl From<ExecutionPayloadBodyShanghai> for LegacyExecutionPayloadBodyV1 {
1089    fn from(value: ExecutionPayloadBodyShanghai) -> Self {
1090        Self { transactions: value.transactions, withdrawals: Some(value.withdrawals) }
1091    }
1092}
1093
1094impl TryFrom<LegacyExecutionPayloadBodyV1> for ExecutionPayloadBodyShanghai {
1095    type Error = ExecutionPayloadBodyConversionError;
1096
1097    fn try_from(value: LegacyExecutionPayloadBodyV1) -> Result<Self, Self::Error> {
1098        Ok(Self {
1099            transactions: value.transactions,
1100            withdrawals: value
1101                .withdrawals
1102                .ok_or(ExecutionPayloadBodyConversionError::MissingField("withdrawals"))?,
1103        })
1104    }
1105}
1106
1107impl From<ExecutionPayloadBodyAmsterdam> for LegacyExecutionPayloadBodyV2 {
1108    fn from(value: ExecutionPayloadBodyAmsterdam) -> Self {
1109        Self {
1110            transactions: value.transactions,
1111            withdrawals: Some(value.withdrawals),
1112            block_access_list: Some(value.block_access_list),
1113        }
1114    }
1115}
1116
1117impl TryFrom<LegacyExecutionPayloadBodyV2> for ExecutionPayloadBodyAmsterdam {
1118    type Error = ExecutionPayloadBodyConversionError;
1119
1120    fn try_from(value: LegacyExecutionPayloadBodyV2) -> Result<Self, Self::Error> {
1121        Ok(Self {
1122            transactions: value.transactions,
1123            withdrawals: value
1124                .withdrawals
1125                .ok_or(ExecutionPayloadBodyConversionError::MissingField("withdrawals"))?,
1126            block_access_list: value
1127                .block_access_list
1128                .ok_or(ExecutionPayloadBodyConversionError::MissingField("block_access_list"))?,
1129        })
1130    }
1131}
1132
1133/// REST-SSZ historical bodies-by-hash request.
1134///
1135/// This is a single-field container, not a bare SSZ list.
1136#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1137pub struct BodiesByHashRequest {
1138    /// Requested block hashes.
1139    pub block_hashes: Vec<B256>,
1140}
1141
1142/// Historical body response entry with explicit availability.
1143///
1144/// REST-SSZ uses a boolean availability bit instead of the legacy `Option<body>` union.
1145#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1146pub struct BodyEntry<T: ssz::Encode + ssz::Decode> {
1147    /// Whether the body is available and belongs to the requested fork.
1148    pub available: bool,
1149    /// Fork-specific body, ignored when `available` is false.
1150    pub body: T,
1151}
1152
1153impl<T: ssz::Encode + ssz::Decode> BodyEntry<T> {
1154    /// Creates an available body entry.
1155    pub const fn available(body: T) -> Self {
1156        Self { available: true, body }
1157    }
1158}
1159
1160impl<T: ssz::Encode + ssz::Decode + Default> BodyEntry<T> {
1161    /// Creates an unavailable body entry.
1162    pub fn unavailable() -> Self {
1163        Self { available: false, body: T::default() }
1164    }
1165}
1166
1167/// REST-SSZ historical bodies response.
1168///
1169/// The response is a one-field SSZ container around the entries list, not a bare list.
1170#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1171pub struct BodiesResponse<T: ssz::Encode + ssz::Decode> {
1172    /// Body entries in request or range order.
1173    pub entries: Vec<BodyEntry<T>>,
1174}
1175
1176impl<T: ssz::Encode + ssz::Decode + Default> BodiesResponse<T> {
1177    /// Creates a response from optional legacy bodies.
1178    ///
1179    /// Missing bodies, or bodies that do not convert to the requested fork container, are encoded
1180    /// as unavailable entries.
1181    pub fn from_optional_bodies<LegacyBody>(
1182        bodies: Vec<Option<LegacyBody>>,
1183        convert: impl Fn(LegacyBody) -> Option<T>,
1184    ) -> Self {
1185        let entries = bodies
1186            .into_iter()
1187            .map(|body| match body.and_then(&convert) {
1188                Some(body) => BodyEntry::available(body),
1189                None => BodyEntry::unavailable(),
1190            })
1191            .collect();
1192
1193        Self { entries }
1194    }
1195}
1196
1197/// Paris historical bodies response.
1198pub type BodiesResponseParis = BodiesResponse<ExecutionPayloadBodyParis>;
1199
1200/// Shanghai historical bodies response.
1201pub type BodiesResponseShanghai = BodiesResponse<ExecutionPayloadBodyShanghai>;
1202
1203/// Cancun historical bodies response.
1204pub type BodiesResponseCancun = BodiesResponse<ExecutionPayloadBodyCancun>;
1205
1206/// Prague historical bodies response.
1207pub type BodiesResponsePrague = BodiesResponse<ExecutionPayloadBodyPrague>;
1208
1209/// Osaka historical bodies response.
1210pub type BodiesResponseOsaka = BodiesResponse<ExecutionPayloadBodyOsaka>;
1211
1212/// Amsterdam historical bodies response.
1213pub type BodiesResponseAmsterdam = BodiesResponse<ExecutionPayloadBodyAmsterdam>;
1214
1215/// V1-V3 blob request container.
1216///
1217/// This single-field container starts with a four-byte SSZ offset and is not wire-equivalent to a
1218/// top-level list.
1219#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1220pub struct BlobsV1Request {
1221    /// Requested versioned blob hashes.
1222    pub versioned_hashes: Vec<B256>,
1223}
1224
1225/// V2 uses the V1 request schema.
1226pub type BlobsV2Request = BlobsV1Request;
1227
1228/// V3 uses the V1 request schema.
1229pub type BlobsV3Request = BlobsV1Request;
1230
1231/// V4 blob request container with a packed 128-bit index bitvector.
1232#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1233pub struct BlobsV4Request {
1234    /// Requested versioned blob hashes.
1235    pub versioned_hashes: Vec<B256>,
1236    /// Requested cell indices, SSZ `Bitvector[128]`.
1237    pub indices_bitarray: B128,
1238}
1239
1240/// Blob response entry with explicit outer availability.
1241///
1242/// REST-SSZ keeps availability separate from the blob contents instead of using a legacy option.
1243#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1244pub struct BlobEntry<T: ssz::Encode + ssz::Decode> {
1245    /// Whether the complete blob contents are available.
1246    pub available: bool,
1247    /// Complete contents, or valid zero-valued contents when unavailable.
1248    pub contents: T,
1249}
1250
1251/// Bounded blob response container.
1252///
1253/// The outer container and entry availability match the REST-SSZ blob endpoint contract.
1254#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1255pub struct BlobsResponse<T: ssz::Encode + ssz::Decode> {
1256    /// One response entry per requested hash.
1257    pub entries: Vec<BlobEntry<T>>,
1258}
1259
1260/// V1 whole-blob response.
1261pub type BlobsV1Response = BlobsResponse<BlobAndProofV1>;
1262
1263/// V2 all-or-nothing cell-proof response.
1264pub type BlobsV2Response = BlobsResponse<BlobAndProofV2>;
1265
1266/// V3 partial cell-proof response.
1267pub type BlobsV3Response = BlobsResponse<BlobAndProofV2>;
1268
1269/// V4 partial cell-range response.
1270pub type BlobsV4Response = BlobsResponse<BlobCellsAndProofs>;
1271
1272/// Blob cells and proofs with REST-SSZ optional cell positions.
1273///
1274/// This uses [`Optional`] (`List[T, 1]`) for per-cell nullability, not Rust [`Option`]'s SSZ
1275/// union encoding.
1276#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1277pub struct BlobCellsAndProofs {
1278    /// Requested blob cells.
1279    pub blob_cells: Vec<Optional<Cell>>,
1280    /// KZG proofs for the requested blob cells.
1281    pub proofs: Vec<Optional<Bytes48>>,
1282}
1283
1284fn zero_blob_v1() -> BlobAndProofV1 {
1285    BlobAndProofV1 { blob: Box::new(Blob::ZERO), proof: Bytes48::ZERO }
1286}
1287
1288fn zero_blob_v2() -> BlobAndProofV2 {
1289    BlobAndProofV2 { blob: Box::new(Blob::ZERO), proofs: Vec::new() }
1290}
1291
1292impl TryFrom<Vec<Option<BlobAndProofV1>>> for BlobsV1Response {
1293    type Error = ConversionError;
1294
1295    fn try_from(value: Vec<Option<BlobAndProofV1>>) -> Result<Self, Self::Error> {
1296        if value.len() > MAX_BLOBS_REQUEST {
1297            return Err(ConversionError::TooManyItems {
1298                field: "blobs",
1299                max: MAX_BLOBS_REQUEST,
1300                actual: value.len(),
1301            })
1302        }
1303
1304        let entries = value
1305            .into_iter()
1306            .map(|value| match value {
1307                Some(contents) => BlobEntry { available: true, contents },
1308                None => BlobEntry { available: false, contents: zero_blob_v1() },
1309            })
1310            .collect();
1311        Ok(Self { entries })
1312    }
1313}
1314
1315impl TryFrom<Vec<BlobAndProofV2>> for BlobsV2Response {
1316    type Error = ConversionError;
1317
1318    fn try_from(value: Vec<BlobAndProofV2>) -> Result<Self, Self::Error> {
1319        if value.len() > MAX_BLOBS_REQUEST {
1320            return Err(ConversionError::TooManyItems {
1321                field: "blobs",
1322                max: MAX_BLOBS_REQUEST,
1323                actual: value.len(),
1324            })
1325        }
1326
1327        let entries =
1328            value.into_iter().map(|contents| BlobEntry { available: true, contents }).collect();
1329        Ok(Self { entries })
1330    }
1331}
1332
1333impl TryFrom<Vec<Option<BlobAndProofV2>>> for BlobsV3Response {
1334    type Error = ConversionError;
1335
1336    fn try_from(value: Vec<Option<BlobAndProofV2>>) -> Result<Self, Self::Error> {
1337        if value.len() > MAX_BLOBS_REQUEST {
1338            return Err(ConversionError::TooManyItems {
1339                field: "blobs",
1340                max: MAX_BLOBS_REQUEST,
1341                actual: value.len(),
1342            })
1343        }
1344
1345        let entries = value
1346            .into_iter()
1347            .map(|value| match value {
1348                Some(contents) => BlobEntry { available: true, contents },
1349                None => BlobEntry { available: false, contents: zero_blob_v2() },
1350            })
1351            .collect();
1352        Ok(Self { entries })
1353    }
1354}
1355
1356impl TryFrom<Vec<Option<BlobCellsAndProofsV1>>> for BlobsV4Response {
1357    type Error = ConversionError;
1358
1359    fn try_from(value: Vec<Option<BlobCellsAndProofsV1>>) -> Result<Self, Self::Error> {
1360        if value.len() > MAX_BLOBS_REQUEST {
1361            return Err(ConversionError::TooManyItems {
1362                field: "blobs",
1363                max: MAX_BLOBS_REQUEST,
1364                actual: value.len(),
1365            })
1366        }
1367
1368        let entries = value
1369            .into_iter()
1370            .map(|value| match value {
1371                Some(contents) => BlobEntry {
1372                    available: true,
1373                    contents: BlobCellsAndProofs {
1374                        blob_cells: contents.blob_cells.into_iter().map(Optional::from).collect(),
1375                        proofs: contents.proofs.into_iter().map(Optional::from).collect(),
1376                    },
1377                },
1378                None => BlobEntry { available: false, contents: BlobCellsAndProofs::default() },
1379            })
1380            .collect();
1381        Ok(Self { entries })
1382    }
1383}
1384
1385/// A trie-node byte list in an [`ExecutionWitnessV1`].
1386pub type WitnessNodeV1 = Vec<u8>;
1387
1388/// A contract-code byte list in an [`ExecutionWitnessV1`].
1389pub type WitnessCodeV1 = Vec<u8>;
1390
1391/// An RLP-encoded header byte list in an [`ExecutionWitnessV1`].
1392pub type WitnessHeaderV1 = Vec<u8>;
1393
1394/// Canonical execution witness for `POST /payloads/witness`.
1395///
1396/// `state` and `codes` are produced in lexicographic ascending byte order. `headers` are
1397/// RLP-encoded and ordered by ascending block number; consecutive headers must be parent-linked.
1398/// These ordering rules are producer-side requirements from the execution-specs witness builder.
1399///
1400/// This is a REST-SSZ wire container, not the JSON-RPC debug witness shape.
1401#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1402pub struct ExecutionWitnessV1 {
1403    /// Hashed trie-node preimages required during execution and state-root recomputation.
1404    pub state: Vec<WitnessNodeV1>,
1405    /// Contract bytecode preimages created or accessed during execution.
1406    pub codes: Vec<WitnessCodeV1>,
1407    /// RLP-encoded ancestor headers used for pre-state and `BLOCKHASH` correctness proofs.
1408    pub headers: Vec<WitnessHeaderV1>,
1409}
1410
1411/// Canonical execution witness for `POST /payloads/witness`.
1412pub type ExecutionWitness = ExecutionWitnessV1;
1413
1414/// REST-SSZ response for `POST /payloads/witness`.
1415///
1416/// The witness uses the Engine REST-SSZ `Optional[T]` encoding from execution-apis and is present
1417/// only when the payload status is `VALID`.
1418#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode)]
1419pub struct PayloadStatusWithWitness {
1420    /// Result of processing the submitted payload.
1421    pub payload_status: PayloadStatus,
1422    /// Execution witness produced for a valid payload.
1423    pub witness: Optional<ExecutionWitnessV1>,
1424}
1425
1426impl PayloadStatusWithWitness {
1427    /// Creates a response, converting the witness into the REST-SSZ `Optional[T]` representation.
1428    pub fn new(payload_status: PayloadStatus, witness: Option<ExecutionWitnessV1>) -> Self {
1429        let witness = match &payload_status.status {
1430            PayloadStatusEnum::Valid => witness.into(),
1431            _ => Optional::none(),
1432        };
1433        Self { payload_status, witness }
1434    }
1435}
1436
1437/// Backwards-compatible alias for the experimental witness response name.
1438pub type NewPayloadWithWitnessResponseV1 = PayloadStatusWithWitness;
1439
1440impl ssz::Decode for PayloadStatusWithWitness {
1441    fn is_ssz_fixed_len() -> bool {
1442        false
1443    }
1444
1445    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1446        let mut builder = ssz::SszDecoderBuilder::new(bytes);
1447        builder.register_type::<PayloadStatus>()?;
1448        builder.register_type::<Optional<ExecutionWitnessV1>>()?;
1449        let mut decoder = builder.build()?;
1450        let response =
1451            Self { payload_status: decoder.decode_next()?, witness: decoder.decode_next()? };
1452        if response.witness.is_some() &&
1453            !matches!(response.payload_status.status, PayloadStatusEnum::Valid)
1454        {
1455            return Err(ssz::DecodeError::BytesInvalid(
1456                "execution witness is only valid for VALID payload status".into(),
1457            ))
1458        }
1459        Ok(response)
1460    }
1461}
1462
1463#[cfg(test)]
1464mod tests {
1465    use super::*;
1466    use alloy_eips::eip4895::Withdrawal;
1467    use alloy_primitives::{Address, Bloom, Bytes};
1468    use ssz::{Decode, Encode};
1469
1470    fn payload_v1() -> ExecutionPayloadV1 {
1471        ExecutionPayloadV1 {
1472            parent_hash: B256::repeat_byte(1),
1473            fee_recipient: Address::repeat_byte(2),
1474            state_root: B256::repeat_byte(3),
1475            receipts_root: B256::repeat_byte(4),
1476            logs_bloom: Bloom::repeat_byte(5),
1477            prev_randao: B256::repeat_byte(6),
1478            block_number: 7,
1479            gas_limit: 8,
1480            gas_used: 9,
1481            timestamp: 10,
1482            extra_data: Bytes::from_static(&[11, 12]),
1483            base_fee_per_gas: U256::from(13),
1484            block_hash: B256::repeat_byte(14),
1485            transactions: vec![Bytes::from_static(&[15, 16])],
1486        }
1487    }
1488
1489    fn payload_v2() -> ExecutionPayloadV2 {
1490        ExecutionPayloadV2 { payload_inner: payload_v1(), withdrawals: vec![Withdrawal::default()] }
1491    }
1492
1493    fn payload_v3() -> ExecutionPayloadV3 {
1494        ExecutionPayloadV3 { payload_inner: payload_v2(), blob_gas_used: 17, excess_blob_gas: 18 }
1495    }
1496
1497    fn payload_v4() -> ExecutionPayloadV4 {
1498        ExecutionPayloadV4 {
1499            payload_inner: payload_v3(),
1500            block_access_list: Bytes::from_static(&[19, 20]),
1501            slot_number: 21,
1502        }
1503    }
1504
1505    fn attributes_cancun() -> PayloadAttributesCancun {
1506        PayloadAttributesCancun {
1507            timestamp: 1,
1508            prev_randao: B256::repeat_byte(2),
1509            suggested_fee_recipient: Address::repeat_byte(3),
1510            withdrawals: vec![Withdrawal::default()],
1511            parent_beacon_block_root: B256::repeat_byte(4),
1512        }
1513    }
1514
1515    fn state() -> ForkchoiceState {
1516        ForkchoiceState {
1517            head_block_hash: B256::repeat_byte(1),
1518            safe_block_hash: B256::repeat_byte(2),
1519            finalized_block_hash: B256::repeat_byte(3),
1520        }
1521    }
1522
1523    fn assert_roundtrip<T>(value: &T)
1524    where
1525        T: Encode + Decode + PartialEq + core::fmt::Debug,
1526    {
1527        assert_eq!(T::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), *value);
1528    }
1529
1530    #[test]
1531    fn execution_payload_envelopes_roundtrip() {
1532        assert_roundtrip(&ExecutionPayloadEnvelopeParis { payload: payload_v1() });
1533        assert_roundtrip(&ExecutionPayloadEnvelopeShanghai { payload: payload_v2() });
1534        assert_roundtrip(&ExecutionPayloadEnvelopeCancun {
1535            payload: payload_v3(),
1536            parent_beacon_block_root: B256::repeat_byte(1),
1537        });
1538        assert_roundtrip(&ExecutionPayloadEnvelopePrague {
1539            payload: payload_v3(),
1540            parent_beacon_block_root: B256::repeat_byte(1),
1541            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1542        });
1543        assert_roundtrip(&ExecutionPayloadEnvelopeOsaka {
1544            payload: payload_v3(),
1545            parent_beacon_block_root: B256::repeat_byte(1),
1546            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1547        });
1548        assert_roundtrip(&ExecutionPayloadEnvelopeAmsterdam {
1549            payload: payload_v4(),
1550            parent_beacon_block_root: B256::repeat_byte(1),
1551            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1552        });
1553    }
1554
1555    #[test]
1556    fn paris_submission_is_a_single_field_container() {
1557        let payload = payload_v1();
1558        let payload_bytes = payload.as_ssz_bytes();
1559        let envelope = ExecutionPayloadEnvelopeParis { payload };
1560        let encoded = envelope.as_ssz_bytes();
1561        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1562        assert_eq!(&encoded[4..], payload_bytes);
1563    }
1564
1565    #[test]
1566    fn built_payloads_roundtrip() {
1567        assert_roundtrip(&BuiltPayloadParis { payload: payload_v1(), block_value: U256::from(1) });
1568        assert_roundtrip(&BuiltPayloadShanghai {
1569            payload: payload_v2(),
1570            block_value: U256::from(1),
1571        });
1572        assert_roundtrip(&BuiltPayloadCancun {
1573            execution_payload: payload_v3(),
1574            block_value: U256::from(1),
1575            blobs_bundle: BlobsBundleV1::empty(),
1576            should_override_builder: true,
1577        });
1578        assert_roundtrip(&BuiltPayloadPrague {
1579            payload: payload_v3(),
1580            block_value: U256::from(1),
1581            blobs_bundle: BlobsBundleV1::empty(),
1582            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1583            should_override_builder: true,
1584        });
1585        assert_roundtrip(&BuiltPayloadOsaka {
1586            payload: payload_v3(),
1587            block_value: U256::from(1),
1588            blobs_bundle: BlobsBundleV2::empty(),
1589            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1590            should_override_builder: true,
1591        });
1592        assert_roundtrip(&BuiltPayloadAmsterdam {
1593            payload: payload_v4(),
1594            block_value: U256::from(1),
1595            blobs_bundle: BlobsBundleV2::empty(),
1596            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1597            should_override_builder: true,
1598        });
1599    }
1600
1601    #[test]
1602    fn shanghai_built_payload_has_no_builder_override() {
1603        let payload = payload_v2();
1604        let payload_len = payload.ssz_bytes_len();
1605        let value = BuiltPayloadShanghai { payload, block_value: U256::from(1) };
1606        let encoded = value.as_ssz_bytes();
1607
1608        assert_eq!(&encoded[..4], &36u32.to_le_bytes());
1609        assert_eq!(encoded.len(), 36 + payload_len);
1610    }
1611
1612    #[test]
1613    fn legacy_built_payload_conversions_preserve_fields() {
1614        let shanghai = BuiltPayloadShanghai { payload: payload_v2(), block_value: U256::from(1) };
1615        let legacy = LegacyBuiltPayloadShanghai::from(shanghai.clone());
1616        assert_eq!(BuiltPayloadShanghai::try_from(legacy).unwrap(), shanghai);
1617
1618        let prague = BuiltPayloadPrague {
1619            payload: payload_v3(),
1620            block_value: U256::from(2),
1621            blobs_bundle: BlobsBundleV1::empty(),
1622            execution_requests: Requests::from_requests([Bytes::from_static(&[3, 4])]),
1623            should_override_builder: true,
1624        };
1625        let legacy = LegacyBuiltPayloadPrague::from(prague.clone());
1626        assert_eq!(BuiltPayloadPrague::from(legacy), prague);
1627
1628        let osaka = BuiltPayloadOsaka {
1629            payload: payload_v3(),
1630            block_value: U256::from(5),
1631            blobs_bundle: BlobsBundleV2::empty(),
1632            execution_requests: Requests::from_requests([Bytes::from_static(&[6, 7])]),
1633            should_override_builder: true,
1634        };
1635        let legacy = LegacyBuiltPayloadOsaka::from(osaka.clone());
1636        assert_eq!(BuiltPayloadOsaka::from(legacy), osaka);
1637
1638        let amsterdam = BuiltPayloadAmsterdam {
1639            payload: payload_v4(),
1640            block_value: U256::from(8),
1641            blobs_bundle: BlobsBundleV2::empty(),
1642            execution_requests: Requests::from_requests([Bytes::from_static(&[9, 10])]),
1643            should_override_builder: true,
1644        };
1645        let legacy = LegacyBuiltPayloadAmsterdam::from(amsterdam.clone());
1646        assert_eq!(BuiltPayloadAmsterdam::from(legacy), amsterdam);
1647    }
1648
1649    #[test]
1650    fn legacy_shanghai_built_payload_rejects_paris_payload() {
1651        let legacy = LegacyBuiltPayloadShanghai {
1652            execution_payload: ExecutionPayloadFieldV2::V1(payload_v1()),
1653            block_value: U256::from(1),
1654        };
1655        assert_eq!(
1656            BuiltPayloadShanghai::try_from(legacy),
1657            Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
1658        );
1659    }
1660
1661    #[test]
1662    fn prague_requests_precede_should_override_builder() {
1663        let value = BuiltPayloadPrague {
1664            payload: payload_v3(),
1665            block_value: U256::from(1),
1666            blobs_bundle: BlobsBundleV1::empty(),
1667            execution_requests: Requests::from_requests([Bytes::from_static(&[0xaa, 0xbb])]),
1668            should_override_builder: true,
1669        };
1670        let encoded = value.as_ssz_bytes();
1671        assert_eq!(encoded[48], 1);
1672        assert_eq!(&encoded[encoded.len() - 2..], &[0xaa, 0xbb]);
1673    }
1674
1675    #[test]
1676    fn forkchoice_updates_roundtrip() {
1677        let paris = PayloadAttributesParis {
1678            timestamp: 1,
1679            prev_randao: B256::repeat_byte(2),
1680            suggested_fee_recipient: Address::repeat_byte(3),
1681        };
1682        let shanghai = PayloadAttributesShanghai {
1683            timestamp: 1,
1684            prev_randao: B256::repeat_byte(2),
1685            suggested_fee_recipient: Address::repeat_byte(3),
1686            withdrawals: vec![Withdrawal::default()],
1687        };
1688        let amsterdam = PayloadAttributesAmsterdam {
1689            timestamp: 1,
1690            prev_randao: B256::repeat_byte(2),
1691            suggested_fee_recipient: Address::repeat_byte(3),
1692            withdrawals: vec![Withdrawal::default()],
1693            parent_beacon_block_root: B256::repeat_byte(4),
1694            slot_number: 5,
1695            target_gas_limit: 6,
1696        };
1697        assert_roundtrip(&ForkchoiceUpdateParis {
1698            forkchoice_state: state(),
1699            payload_attributes: Optional::some(paris),
1700        });
1701        assert_roundtrip(&ForkchoiceUpdateShanghai {
1702            forkchoice_state: state(),
1703            payload_attributes: Optional::some(shanghai),
1704        });
1705        assert_roundtrip(&ForkchoiceUpdateCancun {
1706            forkchoice_state: state(),
1707            payload_attributes: Optional::some(attributes_cancun()),
1708        });
1709        assert_roundtrip(&ForkchoiceUpdatePrague {
1710            forkchoice_state: state(),
1711            payload_attributes: Optional::some(attributes_cancun()),
1712        });
1713        assert_roundtrip(&ForkchoiceUpdateOsaka {
1714            forkchoice_state: state(),
1715            payload_attributes: Optional::some(attributes_cancun()),
1716        });
1717        assert_roundtrip(&ForkchoiceUpdateAmsterdam {
1718            forkchoice_state: state(),
1719            payload_attributes: Optional::some(amsterdam),
1720            custody_columns: Optional::some(B128::repeat_byte(0xa5)),
1721        });
1722    }
1723
1724    #[test]
1725    fn payload_attributes_legacy_conversions_preserve_fork_shape() {
1726        let cancun = attributes_cancun();
1727        let legacy = LegacyPayloadAttributes::from(cancun.clone());
1728        assert_eq!(PayloadAttributesCancun::try_from(legacy).unwrap(), cancun);
1729
1730        let amsterdam = PayloadAttributesAmsterdam {
1731            timestamp: 1,
1732            prev_randao: B256::repeat_byte(2),
1733            suggested_fee_recipient: Address::repeat_byte(3),
1734            withdrawals: vec![Withdrawal::default()],
1735            parent_beacon_block_root: B256::repeat_byte(4),
1736            slot_number: 5,
1737            target_gas_limit: 6,
1738        };
1739        let legacy = LegacyPayloadAttributes::from(amsterdam.clone());
1740        assert_eq!(PayloadAttributesAmsterdam::try_from(legacy).unwrap(), amsterdam);
1741    }
1742
1743    #[test]
1744    fn payload_attributes_legacy_conversions_reject_loss() {
1745        let mut legacy = LegacyPayloadAttributes::default();
1746        assert_eq!(
1747            PayloadAttributesShanghai::try_from(legacy.clone()),
1748            Err(PayloadAttributesConversionError::MissingField("withdrawals"))
1749        );
1750
1751        legacy.withdrawals = Some(vec![]);
1752        legacy.parent_beacon_block_root = Some(B256::ZERO);
1753        assert_eq!(
1754            PayloadAttributesShanghai::try_from(legacy),
1755            Err(PayloadAttributesConversionError::UnexpectedField("parent_beacon_block_root"))
1756        );
1757    }
1758
1759    #[test]
1760    fn every_payload_status_roundtrips() {
1761        for status in [
1762            PayloadStatusEnum::Valid,
1763            PayloadStatusEnum::Invalid { validation_error: "invalid".into() },
1764            PayloadStatusEnum::Syncing,
1765            PayloadStatusEnum::Accepted,
1766        ] {
1767            let validation_error = legacy_validation_error(&status).unwrap();
1768            let value = PayloadStatus {
1769                status,
1770                latest_valid_hash: Optional::some(B256::ZERO),
1771                validation_error,
1772            };
1773            assert_eq!(PayloadStatus::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), value);
1774        }
1775    }
1776
1777    #[test]
1778    fn payload_status_preserves_absent_invalid_validation_error() {
1779        let mut bytes = Vec::new();
1780        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1781        encoder.append(&1u8);
1782        encoder.append(&Optional::<B256>::none());
1783        encoder.append(&Optional::<ErrorBytes>::none());
1784        encoder.finalize();
1785
1786        let decoded = PayloadStatus::from_ssz_bytes(&bytes).unwrap();
1787        assert!(decoded.validation_error.is_none());
1788        assert_eq!(decoded.as_ssz_bytes(), bytes);
1789    }
1790
1791    #[test]
1792    fn payload_status_rejects_non_invalid_validation_error() {
1793        let mut bytes = Vec::new();
1794        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1795        encoder.append(&0u8);
1796        encoder.append(&Optional::<B256>::none());
1797        encoder.append(&Optional::some(Vec::<u8>::new()));
1798        encoder.finalize();
1799        assert!(PayloadStatus::from_ssz_bytes(&bytes).is_err());
1800    }
1801
1802    #[test]
1803    fn payload_status_legacy_conversion_rejects_oversized_error() {
1804        assert!(PayloadStatus::try_from(LegacyPayloadStatus {
1805            status: PayloadStatusEnum::Invalid { validation_error: "x".repeat(1025) },
1806            latest_valid_hash: None,
1807        })
1808        .is_err());
1809    }
1810
1811    #[test]
1812    fn forkchoice_response_distinguishes_absent_and_zero_payload_id() {
1813        let status = PayloadStatus {
1814            status: PayloadStatusEnum::Valid,
1815            latest_valid_hash: Optional::none(),
1816            validation_error: Optional::none(),
1817        };
1818        let none = ForkchoiceUpdateResponse {
1819            payload_status: status.clone(),
1820            payload_id: Optional::none(),
1821        };
1822        let zero = ForkchoiceUpdateResponse {
1823            payload_status: status,
1824            payload_id: Optional::some(PayloadId::default()),
1825        };
1826
1827        assert_ne!(none.as_ssz_bytes(), zero.as_ssz_bytes());
1828        assert_roundtrip(&none);
1829        assert_roundtrip(&zero);
1830    }
1831
1832    #[test]
1833    fn forkchoice_conversion_rejects_accepted() {
1834        let legacy = LegacyForkchoice::from_status(PayloadStatusEnum::Accepted);
1835        assert_eq!(
1836            ForkchoiceUpdateResponse::try_from(legacy),
1837            Err(ConversionError::AcceptedForkchoice)
1838        );
1839    }
1840
1841    fn blob_v2(byte: u8) -> BlobAndProofV2 {
1842        BlobAndProofV2 {
1843            blob: Box::new(Blob::repeat_byte(byte)),
1844            proofs: vec![Bytes48::repeat_byte(byte)],
1845        }
1846    }
1847
1848    #[test]
1849    fn blob_requests_are_single_field_containers() {
1850        let request = BlobsV1Request { versioned_hashes: vec![B256::repeat_byte(0x42)] };
1851        let encoded = request.as_ssz_bytes();
1852
1853        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1854        assert_eq!(&encoded[4..], B256::repeat_byte(0x42).as_slice());
1855        assert_eq!(BlobsV1Request::from_ssz_bytes(&encoded).unwrap(), request);
1856
1857        let _: BlobsV2Request = BlobsV2Request::from_ssz_bytes(&encoded).unwrap();
1858        let _: BlobsV3Request = BlobsV3Request::from_ssz_bytes(&encoded).unwrap();
1859    }
1860
1861    #[test]
1862    fn blob_v4_request_roundtrips_bitvector() {
1863        let request = BlobsV4Request {
1864            versioned_hashes: vec![B256::repeat_byte(0x11)],
1865            indices_bitarray: B128::repeat_byte(0xa5),
1866        };
1867
1868        assert_roundtrip(&request);
1869    }
1870
1871    #[test]
1872    fn blob_response_conversions_preserve_availability_and_order() {
1873        let v1 = BlobsV1Response::try_from(vec![None]).unwrap();
1874        assert!(!v1.entries[0].available);
1875        assert_eq!(v1.entries[0].contents, zero_blob_v1());
1876
1877        let v2 = BlobsV2Response::try_from(vec![blob_v2(1), blob_v2(2)]).unwrap();
1878        assert!(v2.entries.iter().all(|entry| entry.available));
1879
1880        let v3 = BlobsV3Response::try_from(vec![Some(blob_v2(1)), None, Some(blob_v2(3))]).unwrap();
1881        assert_eq!(
1882            v3.entries.iter().map(|entry| entry.available).collect::<Vec<_>>(),
1883            [true, false, true]
1884        );
1885        assert_eq!(v3.entries[2].contents.blob.as_slice(), Blob::repeat_byte(3).as_slice());
1886
1887        let legacy_partial = BlobCellsAndProofsV1 {
1888            blob_cells: vec![Some(Cell::repeat_byte(1)), None],
1889            proofs: vec![Some(Bytes48::repeat_byte(2)), None],
1890        };
1891        let v4 = BlobsV4Response::try_from(vec![None, Some(legacy_partial)]).unwrap();
1892        assert!(!v4.entries[0].available);
1893        assert!(v4.entries[1].available);
1894        assert!(v4.entries[1].contents.blob_cells[0].is_some());
1895        assert!(v4.entries[1].contents.proofs[1].is_none());
1896    }
1897
1898    #[test]
1899    fn blob_cells_and_proofs_uses_rest_optional() {
1900        let value = BlobCellsAndProofs {
1901            blob_cells: vec![Optional::some(Cell::repeat_byte(1))],
1902            proofs: vec![Optional::some(Bytes48::repeat_byte(2))],
1903        };
1904        let encoded = value.as_ssz_bytes();
1905
1906        assert_eq!(BlobCellsAndProofs::from_ssz_bytes(&encoded).unwrap(), value);
1907        assert!(!encoded[8..].starts_with(&[1, 0, 0, 0]));
1908    }
1909
1910    #[test]
1911    fn payload_body_requests_are_single_field_containers() {
1912        let request = BodiesByHashRequest { block_hashes: vec![B256::repeat_byte(0x33)] };
1913        let encoded = request.as_ssz_bytes();
1914
1915        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1916        assert_eq!(&encoded[4..], B256::repeat_byte(0x33).as_slice());
1917        assert_eq!(BodiesByHashRequest::from_ssz_bytes(&encoded).unwrap(), request);
1918    }
1919
1920    #[test]
1921    fn payload_body_responses_preserve_availability() {
1922        let legacy = LegacyExecutionPayloadBodyV1 {
1923            transactions: vec![Bytes::from_static(&[1, 2, 3])],
1924            withdrawals: Some(vec![Withdrawal::default()]),
1925        };
1926        let response =
1927            BodiesResponseShanghai::from_optional_bodies(vec![Some(legacy), None], |body| {
1928                ExecutionPayloadBodyShanghai::try_from(body).ok()
1929            });
1930
1931        assert!(response.entries[0].available);
1932        assert!(!response.entries[1].available);
1933        assert_roundtrip(&response);
1934    }
1935
1936    #[test]
1937    fn witness_response_roundtrips_when_status_is_valid() {
1938        let payload_status = PayloadStatus {
1939            status: PayloadStatusEnum::Valid,
1940            latest_valid_hash: Optional::none(),
1941            validation_error: Optional::none(),
1942        };
1943        let witness = ExecutionWitnessV1 {
1944            state: vec![vec![1, 2, 3]],
1945            codes: vec![vec![4, 5]],
1946            headers: vec![vec![6]],
1947        };
1948        let response = PayloadStatusWithWitness::new(payload_status, Some(witness));
1949
1950        assert_roundtrip(&response);
1951    }
1952
1953    #[test]
1954    fn witness_response_omits_witness_for_non_valid_status() {
1955        let payload_status = PayloadStatus {
1956            status: PayloadStatusEnum::Syncing,
1957            latest_valid_hash: Optional::none(),
1958            validation_error: Optional::none(),
1959        };
1960        let response =
1961            PayloadStatusWithWitness::new(payload_status, Some(ExecutionWitnessV1::default()));
1962
1963        assert!(response.witness.is_none());
1964        assert_roundtrip(&response);
1965    }
1966
1967    #[test]
1968    fn optional_rejects_more_than_one_value() {
1969        assert!(Optional::<B256>::from_ssz_bytes(&[0; 64]).is_err());
1970    }
1971}