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 = Bytes;
1387
1388/// A contract-code byte list in an [`ExecutionWitnessV1`].
1389pub type WitnessCodeV1 = Bytes;
1390
1391/// An RLP-encoded header byte list in an [`ExecutionWitnessV1`].
1392pub type WitnessHeaderV1 = Bytes;
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 required from the pre-state.
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`. A `VALID` status without a witness means the parent
1418/// state was not yet available through the provider (the parent is only known to the engine
1419/// tree); resubmitting the payload once forkchoice has made the parent canonical yields it.
1420#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode)]
1421pub struct PayloadStatusWithWitness {
1422    /// Result of processing the submitted payload.
1423    pub payload_status: PayloadStatus,
1424    /// Execution witness produced for a valid payload.
1425    pub witness: Optional<ExecutionWitnessV1>,
1426}
1427
1428impl PayloadStatusWithWitness {
1429    /// Creates a response, converting the witness into the REST-SSZ `Optional[T]` representation.
1430    pub fn new(payload_status: PayloadStatus, witness: Option<ExecutionWitnessV1>) -> Self {
1431        let witness = match &payload_status.status {
1432            PayloadStatusEnum::Valid => witness.into(),
1433            _ => Optional::none(),
1434        };
1435        Self { payload_status, witness }
1436    }
1437}
1438
1439/// Backwards-compatible alias for the experimental witness response name.
1440pub type NewPayloadWithWitnessResponseV1 = PayloadStatusWithWitness;
1441
1442impl ssz::Decode for PayloadStatusWithWitness {
1443    fn is_ssz_fixed_len() -> bool {
1444        false
1445    }
1446
1447    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1448        let mut builder = ssz::SszDecoderBuilder::new(bytes);
1449        builder.register_type::<PayloadStatus>()?;
1450        builder.register_type::<Optional<ExecutionWitnessV1>>()?;
1451        let mut decoder = builder.build()?;
1452        let response =
1453            Self { payload_status: decoder.decode_next()?, witness: decoder.decode_next()? };
1454        if response.witness.is_some() &&
1455            !matches!(response.payload_status.status, PayloadStatusEnum::Valid)
1456        {
1457            return Err(ssz::DecodeError::BytesInvalid(
1458                "execution witness is only valid for VALID payload status".into(),
1459            ))
1460        }
1461        Ok(response)
1462    }
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467    use super::*;
1468    use alloy_eips::eip4895::Withdrawal;
1469    use alloy_primitives::{Address, Bloom, Bytes};
1470    use ssz::{Decode, Encode};
1471
1472    fn payload_v1() -> ExecutionPayloadV1 {
1473        ExecutionPayloadV1 {
1474            parent_hash: B256::repeat_byte(1),
1475            fee_recipient: Address::repeat_byte(2),
1476            state_root: B256::repeat_byte(3),
1477            receipts_root: B256::repeat_byte(4),
1478            logs_bloom: Bloom::repeat_byte(5),
1479            prev_randao: B256::repeat_byte(6),
1480            block_number: 7,
1481            gas_limit: 8,
1482            gas_used: 9,
1483            timestamp: 10,
1484            extra_data: Bytes::from_static(&[11, 12]),
1485            base_fee_per_gas: U256::from(13),
1486            block_hash: B256::repeat_byte(14),
1487            transactions: vec![Bytes::from_static(&[15, 16])],
1488        }
1489    }
1490
1491    fn payload_v2() -> ExecutionPayloadV2 {
1492        ExecutionPayloadV2 { payload_inner: payload_v1(), withdrawals: vec![Withdrawal::default()] }
1493    }
1494
1495    fn payload_v3() -> ExecutionPayloadV3 {
1496        ExecutionPayloadV3 { payload_inner: payload_v2(), blob_gas_used: 17, excess_blob_gas: 18 }
1497    }
1498
1499    fn payload_v4() -> ExecutionPayloadV4 {
1500        ExecutionPayloadV4 {
1501            payload_inner: payload_v3(),
1502            block_access_list: Bytes::from_static(&[19, 20]),
1503            slot_number: 21,
1504        }
1505    }
1506
1507    fn attributes_cancun() -> PayloadAttributesCancun {
1508        PayloadAttributesCancun {
1509            timestamp: 1,
1510            prev_randao: B256::repeat_byte(2),
1511            suggested_fee_recipient: Address::repeat_byte(3),
1512            withdrawals: vec![Withdrawal::default()],
1513            parent_beacon_block_root: B256::repeat_byte(4),
1514        }
1515    }
1516
1517    fn state() -> ForkchoiceState {
1518        ForkchoiceState {
1519            head_block_hash: B256::repeat_byte(1),
1520            safe_block_hash: B256::repeat_byte(2),
1521            finalized_block_hash: B256::repeat_byte(3),
1522        }
1523    }
1524
1525    fn assert_roundtrip<T>(value: &T)
1526    where
1527        T: Encode + Decode + PartialEq + core::fmt::Debug,
1528    {
1529        assert_eq!(T::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), *value);
1530    }
1531
1532    #[test]
1533    fn execution_payload_envelopes_roundtrip() {
1534        assert_roundtrip(&ExecutionPayloadEnvelopeParis { payload: payload_v1() });
1535        assert_roundtrip(&ExecutionPayloadEnvelopeShanghai { payload: payload_v2() });
1536        assert_roundtrip(&ExecutionPayloadEnvelopeCancun {
1537            payload: payload_v3(),
1538            parent_beacon_block_root: B256::repeat_byte(1),
1539        });
1540        assert_roundtrip(&ExecutionPayloadEnvelopePrague {
1541            payload: payload_v3(),
1542            parent_beacon_block_root: B256::repeat_byte(1),
1543            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1544        });
1545        assert_roundtrip(&ExecutionPayloadEnvelopeOsaka {
1546            payload: payload_v3(),
1547            parent_beacon_block_root: B256::repeat_byte(1),
1548            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1549        });
1550        assert_roundtrip(&ExecutionPayloadEnvelopeAmsterdam {
1551            payload: payload_v4(),
1552            parent_beacon_block_root: B256::repeat_byte(1),
1553            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1554        });
1555    }
1556
1557    #[test]
1558    fn paris_submission_is_a_single_field_container() {
1559        let payload = payload_v1();
1560        let payload_bytes = payload.as_ssz_bytes();
1561        let envelope = ExecutionPayloadEnvelopeParis { payload };
1562        let encoded = envelope.as_ssz_bytes();
1563        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1564        assert_eq!(&encoded[4..], payload_bytes);
1565    }
1566
1567    #[test]
1568    fn built_payloads_roundtrip() {
1569        assert_roundtrip(&BuiltPayloadParis { payload: payload_v1(), block_value: U256::from(1) });
1570        assert_roundtrip(&BuiltPayloadShanghai {
1571            payload: payload_v2(),
1572            block_value: U256::from(1),
1573        });
1574        assert_roundtrip(&BuiltPayloadCancun {
1575            execution_payload: payload_v3(),
1576            block_value: U256::from(1),
1577            blobs_bundle: BlobsBundleV1::empty(),
1578            should_override_builder: true,
1579        });
1580        assert_roundtrip(&BuiltPayloadPrague {
1581            payload: payload_v3(),
1582            block_value: U256::from(1),
1583            blobs_bundle: BlobsBundleV1::empty(),
1584            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1585            should_override_builder: true,
1586        });
1587        assert_roundtrip(&BuiltPayloadOsaka {
1588            payload: payload_v3(),
1589            block_value: U256::from(1),
1590            blobs_bundle: BlobsBundleV2::empty(),
1591            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1592            should_override_builder: true,
1593        });
1594        assert_roundtrip(&BuiltPayloadAmsterdam {
1595            payload: payload_v4(),
1596            block_value: U256::from(1),
1597            blobs_bundle: BlobsBundleV2::empty(),
1598            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1599            should_override_builder: true,
1600        });
1601    }
1602
1603    #[test]
1604    fn shanghai_built_payload_has_no_builder_override() {
1605        let payload = payload_v2();
1606        let payload_len = payload.ssz_bytes_len();
1607        let value = BuiltPayloadShanghai { payload, block_value: U256::from(1) };
1608        let encoded = value.as_ssz_bytes();
1609
1610        assert_eq!(&encoded[..4], &36u32.to_le_bytes());
1611        assert_eq!(encoded.len(), 36 + payload_len);
1612    }
1613
1614    #[test]
1615    fn legacy_built_payload_conversions_preserve_fields() {
1616        let shanghai = BuiltPayloadShanghai { payload: payload_v2(), block_value: U256::from(1) };
1617        let legacy = LegacyBuiltPayloadShanghai::from(shanghai.clone());
1618        assert_eq!(BuiltPayloadShanghai::try_from(legacy).unwrap(), shanghai);
1619
1620        let prague = BuiltPayloadPrague {
1621            payload: payload_v3(),
1622            block_value: U256::from(2),
1623            blobs_bundle: BlobsBundleV1::empty(),
1624            execution_requests: Requests::from_requests([Bytes::from_static(&[3, 4])]),
1625            should_override_builder: true,
1626        };
1627        let legacy = LegacyBuiltPayloadPrague::from(prague.clone());
1628        assert_eq!(BuiltPayloadPrague::from(legacy), prague);
1629
1630        let osaka = BuiltPayloadOsaka {
1631            payload: payload_v3(),
1632            block_value: U256::from(5),
1633            blobs_bundle: BlobsBundleV2::empty(),
1634            execution_requests: Requests::from_requests([Bytes::from_static(&[6, 7])]),
1635            should_override_builder: true,
1636        };
1637        let legacy = LegacyBuiltPayloadOsaka::from(osaka.clone());
1638        assert_eq!(BuiltPayloadOsaka::from(legacy), osaka);
1639
1640        let amsterdam = BuiltPayloadAmsterdam {
1641            payload: payload_v4(),
1642            block_value: U256::from(8),
1643            blobs_bundle: BlobsBundleV2::empty(),
1644            execution_requests: Requests::from_requests([Bytes::from_static(&[9, 10])]),
1645            should_override_builder: true,
1646        };
1647        let legacy = LegacyBuiltPayloadAmsterdam::from(amsterdam.clone());
1648        assert_eq!(BuiltPayloadAmsterdam::from(legacy), amsterdam);
1649    }
1650
1651    #[test]
1652    fn legacy_shanghai_built_payload_rejects_paris_payload() {
1653        let legacy = LegacyBuiltPayloadShanghai {
1654            execution_payload: ExecutionPayloadFieldV2::V1(payload_v1()),
1655            block_value: U256::from(1),
1656        };
1657        assert_eq!(
1658            BuiltPayloadShanghai::try_from(legacy),
1659            Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
1660        );
1661    }
1662
1663    #[test]
1664    fn prague_requests_precede_should_override_builder() {
1665        let value = BuiltPayloadPrague {
1666            payload: payload_v3(),
1667            block_value: U256::from(1),
1668            blobs_bundle: BlobsBundleV1::empty(),
1669            execution_requests: Requests::from_requests([Bytes::from_static(&[0xaa, 0xbb])]),
1670            should_override_builder: true,
1671        };
1672        let encoded = value.as_ssz_bytes();
1673        assert_eq!(encoded[48], 1);
1674        assert_eq!(&encoded[encoded.len() - 2..], &[0xaa, 0xbb]);
1675    }
1676
1677    #[test]
1678    fn forkchoice_updates_roundtrip() {
1679        let paris = PayloadAttributesParis {
1680            timestamp: 1,
1681            prev_randao: B256::repeat_byte(2),
1682            suggested_fee_recipient: Address::repeat_byte(3),
1683        };
1684        let shanghai = PayloadAttributesShanghai {
1685            timestamp: 1,
1686            prev_randao: B256::repeat_byte(2),
1687            suggested_fee_recipient: Address::repeat_byte(3),
1688            withdrawals: vec![Withdrawal::default()],
1689        };
1690        let amsterdam = PayloadAttributesAmsterdam {
1691            timestamp: 1,
1692            prev_randao: B256::repeat_byte(2),
1693            suggested_fee_recipient: Address::repeat_byte(3),
1694            withdrawals: vec![Withdrawal::default()],
1695            parent_beacon_block_root: B256::repeat_byte(4),
1696            slot_number: 5,
1697            target_gas_limit: 6,
1698        };
1699        assert_roundtrip(&ForkchoiceUpdateParis {
1700            forkchoice_state: state(),
1701            payload_attributes: Optional::some(paris),
1702        });
1703        assert_roundtrip(&ForkchoiceUpdateShanghai {
1704            forkchoice_state: state(),
1705            payload_attributes: Optional::some(shanghai),
1706        });
1707        assert_roundtrip(&ForkchoiceUpdateCancun {
1708            forkchoice_state: state(),
1709            payload_attributes: Optional::some(attributes_cancun()),
1710        });
1711        assert_roundtrip(&ForkchoiceUpdatePrague {
1712            forkchoice_state: state(),
1713            payload_attributes: Optional::some(attributes_cancun()),
1714        });
1715        assert_roundtrip(&ForkchoiceUpdateOsaka {
1716            forkchoice_state: state(),
1717            payload_attributes: Optional::some(attributes_cancun()),
1718        });
1719        assert_roundtrip(&ForkchoiceUpdateAmsterdam {
1720            forkchoice_state: state(),
1721            payload_attributes: Optional::some(amsterdam),
1722            custody_columns: Optional::some(B128::repeat_byte(0xa5)),
1723        });
1724    }
1725
1726    #[test]
1727    fn payload_attributes_legacy_conversions_preserve_fork_shape() {
1728        let cancun = attributes_cancun();
1729        let legacy = LegacyPayloadAttributes::from(cancun.clone());
1730        assert_eq!(PayloadAttributesCancun::try_from(legacy).unwrap(), cancun);
1731
1732        let amsterdam = PayloadAttributesAmsterdam {
1733            timestamp: 1,
1734            prev_randao: B256::repeat_byte(2),
1735            suggested_fee_recipient: Address::repeat_byte(3),
1736            withdrawals: vec![Withdrawal::default()],
1737            parent_beacon_block_root: B256::repeat_byte(4),
1738            slot_number: 5,
1739            target_gas_limit: 6,
1740        };
1741        let legacy = LegacyPayloadAttributes::from(amsterdam.clone());
1742        assert_eq!(PayloadAttributesAmsterdam::try_from(legacy).unwrap(), amsterdam);
1743    }
1744
1745    #[test]
1746    fn payload_attributes_legacy_conversions_reject_loss() {
1747        let mut legacy = LegacyPayloadAttributes::default();
1748        assert_eq!(
1749            PayloadAttributesShanghai::try_from(legacy.clone()),
1750            Err(PayloadAttributesConversionError::MissingField("withdrawals"))
1751        );
1752
1753        legacy.withdrawals = Some(vec![]);
1754        legacy.parent_beacon_block_root = Some(B256::ZERO);
1755        assert_eq!(
1756            PayloadAttributesShanghai::try_from(legacy),
1757            Err(PayloadAttributesConversionError::UnexpectedField("parent_beacon_block_root"))
1758        );
1759    }
1760
1761    #[test]
1762    fn every_payload_status_roundtrips() {
1763        for status in [
1764            PayloadStatusEnum::Valid,
1765            PayloadStatusEnum::Invalid { validation_error: "invalid".into() },
1766            PayloadStatusEnum::Syncing,
1767            PayloadStatusEnum::Accepted,
1768        ] {
1769            let validation_error = legacy_validation_error(&status).unwrap();
1770            let value = PayloadStatus {
1771                status,
1772                latest_valid_hash: Optional::some(B256::ZERO),
1773                validation_error,
1774            };
1775            assert_eq!(PayloadStatus::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), value);
1776        }
1777    }
1778
1779    #[test]
1780    fn payload_status_preserves_absent_invalid_validation_error() {
1781        let mut bytes = Vec::new();
1782        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1783        encoder.append(&1u8);
1784        encoder.append(&Optional::<B256>::none());
1785        encoder.append(&Optional::<ErrorBytes>::none());
1786        encoder.finalize();
1787
1788        let decoded = PayloadStatus::from_ssz_bytes(&bytes).unwrap();
1789        assert!(decoded.validation_error.is_none());
1790        assert_eq!(decoded.as_ssz_bytes(), bytes);
1791    }
1792
1793    #[test]
1794    fn payload_status_rejects_non_invalid_validation_error() {
1795        let mut bytes = Vec::new();
1796        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1797        encoder.append(&0u8);
1798        encoder.append(&Optional::<B256>::none());
1799        encoder.append(&Optional::some(Vec::<u8>::new()));
1800        encoder.finalize();
1801        assert!(PayloadStatus::from_ssz_bytes(&bytes).is_err());
1802    }
1803
1804    #[test]
1805    fn payload_status_legacy_conversion_rejects_oversized_error() {
1806        assert!(PayloadStatus::try_from(LegacyPayloadStatus {
1807            status: PayloadStatusEnum::Invalid { validation_error: "x".repeat(1025) },
1808            latest_valid_hash: None,
1809        })
1810        .is_err());
1811    }
1812
1813    #[test]
1814    fn forkchoice_response_distinguishes_absent_and_zero_payload_id() {
1815        let status = PayloadStatus {
1816            status: PayloadStatusEnum::Valid,
1817            latest_valid_hash: Optional::none(),
1818            validation_error: Optional::none(),
1819        };
1820        let none = ForkchoiceUpdateResponse {
1821            payload_status: status.clone(),
1822            payload_id: Optional::none(),
1823        };
1824        let zero = ForkchoiceUpdateResponse {
1825            payload_status: status,
1826            payload_id: Optional::some(PayloadId::default()),
1827        };
1828
1829        assert_ne!(none.as_ssz_bytes(), zero.as_ssz_bytes());
1830        assert_roundtrip(&none);
1831        assert_roundtrip(&zero);
1832    }
1833
1834    #[test]
1835    fn forkchoice_conversion_rejects_accepted() {
1836        let legacy = LegacyForkchoice::from_status(PayloadStatusEnum::Accepted);
1837        assert_eq!(
1838            ForkchoiceUpdateResponse::try_from(legacy),
1839            Err(ConversionError::AcceptedForkchoice)
1840        );
1841    }
1842
1843    fn blob_v2(byte: u8) -> BlobAndProofV2 {
1844        BlobAndProofV2 {
1845            blob: Box::new(Blob::repeat_byte(byte)),
1846            proofs: vec![Bytes48::repeat_byte(byte)],
1847        }
1848    }
1849
1850    #[test]
1851    fn blob_requests_are_single_field_containers() {
1852        let request = BlobsV1Request { versioned_hashes: vec![B256::repeat_byte(0x42)] };
1853        let encoded = request.as_ssz_bytes();
1854
1855        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1856        assert_eq!(&encoded[4..], B256::repeat_byte(0x42).as_slice());
1857        assert_eq!(BlobsV1Request::from_ssz_bytes(&encoded).unwrap(), request);
1858
1859        let _: BlobsV2Request = BlobsV2Request::from_ssz_bytes(&encoded).unwrap();
1860        let _: BlobsV3Request = BlobsV3Request::from_ssz_bytes(&encoded).unwrap();
1861    }
1862
1863    #[test]
1864    fn blob_v4_request_roundtrips_bitvector() {
1865        let request = BlobsV4Request {
1866            versioned_hashes: vec![B256::repeat_byte(0x11)],
1867            indices_bitarray: B128::repeat_byte(0xa5),
1868        };
1869
1870        assert_roundtrip(&request);
1871    }
1872
1873    #[test]
1874    fn blob_response_conversions_preserve_availability_and_order() {
1875        let v1 = BlobsV1Response::try_from(vec![None]).unwrap();
1876        assert!(!v1.entries[0].available);
1877        assert_eq!(v1.entries[0].contents, zero_blob_v1());
1878
1879        let v2 = BlobsV2Response::try_from(vec![blob_v2(1), blob_v2(2)]).unwrap();
1880        assert!(v2.entries.iter().all(|entry| entry.available));
1881
1882        let v3 = BlobsV3Response::try_from(vec![Some(blob_v2(1)), None, Some(blob_v2(3))]).unwrap();
1883        assert_eq!(
1884            v3.entries.iter().map(|entry| entry.available).collect::<Vec<_>>(),
1885            [true, false, true]
1886        );
1887        assert_eq!(v3.entries[2].contents.blob.as_slice(), Blob::repeat_byte(3).as_slice());
1888
1889        let legacy_partial = BlobCellsAndProofsV1 {
1890            blob_cells: vec![Some(Cell::repeat_byte(1)), None],
1891            proofs: vec![Some(Bytes48::repeat_byte(2)), None],
1892        };
1893        let v4 = BlobsV4Response::try_from(vec![None, Some(legacy_partial)]).unwrap();
1894        assert!(!v4.entries[0].available);
1895        assert!(v4.entries[1].available);
1896        assert!(v4.entries[1].contents.blob_cells[0].is_some());
1897        assert!(v4.entries[1].contents.proofs[1].is_none());
1898    }
1899
1900    #[test]
1901    fn blob_cells_and_proofs_uses_rest_optional() {
1902        let value = BlobCellsAndProofs {
1903            blob_cells: vec![Optional::some(Cell::repeat_byte(1))],
1904            proofs: vec![Optional::some(Bytes48::repeat_byte(2))],
1905        };
1906        let encoded = value.as_ssz_bytes();
1907
1908        assert_eq!(BlobCellsAndProofs::from_ssz_bytes(&encoded).unwrap(), value);
1909        assert!(!encoded[8..].starts_with(&[1, 0, 0, 0]));
1910    }
1911
1912    #[test]
1913    fn payload_body_requests_are_single_field_containers() {
1914        let request = BodiesByHashRequest { block_hashes: vec![B256::repeat_byte(0x33)] };
1915        let encoded = request.as_ssz_bytes();
1916
1917        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1918        assert_eq!(&encoded[4..], B256::repeat_byte(0x33).as_slice());
1919        assert_eq!(BodiesByHashRequest::from_ssz_bytes(&encoded).unwrap(), request);
1920    }
1921
1922    #[test]
1923    fn payload_body_responses_preserve_availability() {
1924        let legacy = LegacyExecutionPayloadBodyV1 {
1925            transactions: vec![Bytes::from_static(&[1, 2, 3])],
1926            withdrawals: Some(vec![Withdrawal::default()]),
1927        };
1928        let response =
1929            BodiesResponseShanghai::from_optional_bodies(vec![Some(legacy), None], |body| {
1930                ExecutionPayloadBodyShanghai::try_from(body).ok()
1931            });
1932
1933        assert!(response.entries[0].available);
1934        assert!(!response.entries[1].available);
1935        assert_roundtrip(&response);
1936    }
1937
1938    #[test]
1939    fn witness_response_roundtrips_when_status_is_valid() {
1940        let payload_status = PayloadStatus {
1941            status: PayloadStatusEnum::Valid,
1942            latest_valid_hash: Optional::none(),
1943            validation_error: Optional::none(),
1944        };
1945        let witness = ExecutionWitnessV1 {
1946            state: vec![vec![1, 2, 3].into()],
1947            codes: vec![vec![4, 5].into()],
1948            headers: vec![vec![6].into()],
1949        };
1950        let response = PayloadStatusWithWitness::new(payload_status, Some(witness));
1951
1952        assert_roundtrip(&response);
1953    }
1954
1955    #[test]
1956    fn witness_response_omits_witness_for_non_valid_status() {
1957        let payload_status = PayloadStatus {
1958            status: PayloadStatusEnum::Syncing,
1959            latest_valid_hash: Optional::none(),
1960            validation_error: Optional::none(),
1961        };
1962        let response =
1963            PayloadStatusWithWitness::new(payload_status, Some(ExecutionWitnessV1::default()));
1964
1965        assert!(response.witness.is_none());
1966        assert_roundtrip(&response);
1967    }
1968
1969    #[test]
1970    fn optional_rejects_more_than_one_value() {
1971        assert!(Optional::<B256>::from_ssz_bytes(&[0; 64]).is_err());
1972    }
1973}