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            target_gas_limit: None,
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            target_gas_limit: None,
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            target_gas_limit: None,
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    fn from(value: PayloadAttributesAmsterdam) -> Self {
580        Self {
581            timestamp: value.timestamp,
582            prev_randao: value.prev_randao,
583            suggested_fee_recipient: value.suggested_fee_recipient,
584            withdrawals: Some(value.withdrawals),
585            parent_beacon_block_root: Some(value.parent_beacon_block_root),
586            slot_number: Some(value.slot_number),
587            target_gas_limit: Some(value.target_gas_limit),
588        }
589    }
590}
591
592impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesAmsterdam {
593    type Error = PayloadAttributesConversionError;
594
595    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
596        Ok(Self {
597            timestamp: value.timestamp,
598            prev_randao: value.prev_randao,
599            suggested_fee_recipient: value.suggested_fee_recipient,
600            withdrawals: require(value.withdrawals, "withdrawals")?,
601            parent_beacon_block_root: require(
602                value.parent_beacon_block_root,
603                "parent_beacon_block_root",
604            )?,
605            slot_number: require(value.slot_number, "slot_number")?,
606            target_gas_limit: require(value.target_gas_limit, "target_gas_limit")?,
607        })
608    }
609}
610
611/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Paris.
612///
613/// Unlike the legacy `engine_getPayloadV1` response, this includes the expected block value.
614#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
615pub struct BuiltPayloadParis {
616    /// Execution payload V1.
617    pub payload: ExecutionPayloadParis,
618    /// The expected value to be received by the fee recipient in wei.
619    pub block_value: U256,
620}
621
622/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Shanghai.
623///
624/// This follows the legacy `engine_getPayloadV2` payload-build response shape: execution payload
625/// plus block value only. `should_override_builder` starts at Cancun.
626/// The concrete V2 payload prevents the legacy V1/V2 untagged field from accepting a Paris payload.
627#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
628pub struct BuiltPayloadShanghai {
629    /// Execution payload V2.
630    pub payload: ExecutionPayloadShanghai,
631    /// The expected value to be received by the fee recipient in wei.
632    pub block_value: U256,
633}
634
635/// Engine API v2 REST-SSZ payload-build response for Cancun.
636///
637/// This is wire-compatible with the legacy `engine_getPayloadV3` response envelope.
638pub type BuiltPayloadCancun = alloy_rpc_types_engine::ExecutionPayloadEnvelopeV3;
639
640/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Prague.
641///
642/// Unlike the legacy [`alloy_rpc_types_engine::ExecutionPayloadEnvelopeV4`],
643/// `execution_requests` precedes `should_override_builder` in the normative SSZ field order.
644#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
645pub struct BuiltPayloadPrague {
646    /// Execution payload V3.
647    pub payload: ExecutionPayloadPrague,
648    /// The expected value to be received by the fee recipient in wei.
649    pub block_value: U256,
650    /// The blobs, commitments, and proofs associated with the executed payload.
651    pub blobs_bundle: BlobsBundleV1,
652    /// A list of opaque EIP-7685 requests.
653    pub execution_requests: Requests,
654    /// A suggestion from the execution layer whether this payload should be used instead of an
655    /// externally provided one.
656    pub should_override_builder: bool,
657}
658
659/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Osaka.
660///
661/// It is separate from legacy V5 because REST-SSZ places `execution_requests` before the builder
662/// override flag.
663#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
664pub struct BuiltPayloadOsaka {
665    /// Execution payload V3.
666    pub payload: ExecutionPayloadOsaka,
667    /// The expected value to be received by the fee recipient in wei.
668    pub block_value: U256,
669    /// The blobs, commitments, and EIP-7594 cell proofs associated with the executed payload.
670    pub blobs_bundle: BlobsBundleV2,
671    /// A list of opaque EIP-7685 requests.
672    pub execution_requests: Requests,
673    /// A suggestion from the execution layer whether this payload should be used instead of an
674    /// externally provided one.
675    pub should_override_builder: bool,
676}
677
678/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Amsterdam.
679///
680/// It is separate from legacy V6 because REST-SSZ places `execution_requests` before the builder
681/// override flag.
682#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
683pub struct BuiltPayloadAmsterdam {
684    /// Execution payload V4.
685    pub payload: ExecutionPayloadAmsterdam,
686    /// The expected value to be received by the fee recipient in wei.
687    pub block_value: U256,
688    /// The blobs, commitments, and EIP-7594 cell proofs associated with the executed payload.
689    pub blobs_bundle: BlobsBundleV2,
690    /// A list of opaque EIP-7685 requests.
691    pub execution_requests: Requests,
692    /// A suggestion from the execution layer whether this payload should be used instead of an
693    /// externally provided one.
694    pub should_override_builder: bool,
695}
696
697/// Error converting legacy payload-build envelopes into fork-specific REST-SSZ containers.
698#[derive(Clone, Copy, Debug, PartialEq, Eq)]
699pub enum BuiltPayloadConversionError {
700    /// The legacy envelope carried an execution payload from a different fork.
701    UnexpectedPayloadFork(&'static str),
702}
703
704impl core::fmt::Display for BuiltPayloadConversionError {
705    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
706        match self {
707            Self::UnexpectedPayloadFork(fork) => {
708                write!(f, "unexpected execution payload fork: {fork}")
709            }
710        }
711    }
712}
713
714impl core::error::Error for BuiltPayloadConversionError {}
715
716impl From<BuiltPayloadShanghai> for LegacyBuiltPayloadShanghai {
717    fn from(value: BuiltPayloadShanghai) -> Self {
718        Self {
719            execution_payload: ExecutionPayloadFieldV2::V2(value.payload),
720            block_value: value.block_value,
721        }
722    }
723}
724
725impl TryFrom<LegacyBuiltPayloadShanghai> for BuiltPayloadShanghai {
726    type Error = BuiltPayloadConversionError;
727
728    fn try_from(value: LegacyBuiltPayloadShanghai) -> Result<Self, Self::Error> {
729        match value.execution_payload {
730            ExecutionPayloadFieldV2::V2(payload) => {
731                Ok(Self { payload, block_value: value.block_value })
732            }
733            ExecutionPayloadFieldV2::V1(_) => {
734                Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
735            }
736        }
737    }
738}
739
740impl From<LegacyBuiltPayloadPrague> for BuiltPayloadPrague {
741    fn from(value: LegacyBuiltPayloadPrague) -> Self {
742        Self {
743            payload: value.envelope_inner.execution_payload,
744            block_value: value.envelope_inner.block_value,
745            blobs_bundle: value.envelope_inner.blobs_bundle,
746            execution_requests: value.execution_requests,
747            should_override_builder: value.envelope_inner.should_override_builder,
748        }
749    }
750}
751
752impl From<BuiltPayloadPrague> for LegacyBuiltPayloadPrague {
753    fn from(value: BuiltPayloadPrague) -> Self {
754        Self {
755            envelope_inner: alloy_rpc_types_engine::ExecutionPayloadEnvelopeV3 {
756                execution_payload: value.payload,
757                block_value: value.block_value,
758                blobs_bundle: value.blobs_bundle,
759                should_override_builder: value.should_override_builder,
760            },
761            execution_requests: value.execution_requests,
762        }
763    }
764}
765
766impl From<LegacyBuiltPayloadOsaka> for BuiltPayloadOsaka {
767    fn from(value: LegacyBuiltPayloadOsaka) -> Self {
768        Self {
769            payload: value.execution_payload,
770            block_value: value.block_value,
771            blobs_bundle: value.blobs_bundle,
772            execution_requests: value.execution_requests,
773            should_override_builder: value.should_override_builder,
774        }
775    }
776}
777
778impl From<BuiltPayloadOsaka> for LegacyBuiltPayloadOsaka {
779    fn from(value: BuiltPayloadOsaka) -> Self {
780        Self {
781            execution_payload: value.payload,
782            block_value: value.block_value,
783            blobs_bundle: value.blobs_bundle,
784            should_override_builder: value.should_override_builder,
785            execution_requests: value.execution_requests,
786        }
787    }
788}
789
790impl From<LegacyBuiltPayloadAmsterdam> for BuiltPayloadAmsterdam {
791    fn from(value: LegacyBuiltPayloadAmsterdam) -> Self {
792        Self {
793            payload: value.execution_payload,
794            block_value: value.block_value,
795            blobs_bundle: value.blobs_bundle,
796            execution_requests: value.execution_requests,
797            should_override_builder: value.should_override_builder,
798        }
799    }
800}
801
802impl From<BuiltPayloadAmsterdam> for LegacyBuiltPayloadAmsterdam {
803    fn from(value: BuiltPayloadAmsterdam) -> Self {
804        Self {
805            execution_payload: value.payload,
806            block_value: value.block_value,
807            blobs_bundle: value.blobs_bundle,
808            should_override_builder: value.should_override_builder,
809            execution_requests: value.execution_requests,
810        }
811    }
812}
813
814/// REST-SSZ payload-submission request containers.
815///
816/// These are distinct from the legacy Engine JSON-RPC get-payload envelopes: submission requests
817/// are fork-specific request bodies, while the legacy envelope types mostly model get-payload
818/// responses and sometimes carry response-only fields such as block value, blob bundles, builder
819/// override hints, or a different field order.
820///
821/// Paris payload-submission request.
822///
823/// The single-field container is required by REST-SSZ; the legacy endpoint submitted a bare
824/// payload.
825#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
826pub struct ExecutionPayloadEnvelopeParis {
827    /// Submitted execution payload.
828    pub payload: ExecutionPayloadParis,
829}
830
831/// Shanghai payload-submission request.
832///
833/// The single-field container is required by REST-SSZ and fixes the payload fork at decode time.
834#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
835pub struct ExecutionPayloadEnvelopeShanghai {
836    /// Submitted execution payload.
837    pub payload: ExecutionPayloadShanghai,
838}
839
840/// Cancun payload-submission request.
841///
842/// Cancun adds the parent beacon block root to the REST request envelope.
843#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
844pub struct ExecutionPayloadEnvelopeCancun {
845    /// Submitted execution payload.
846    pub payload: ExecutionPayloadCancun,
847    /// Root of the parent beacon block.
848    pub parent_beacon_block_root: B256,
849}
850
851/// Prague payload-submission request.
852///
853/// Prague adds execution requests to the REST request envelope.
854#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
855pub struct ExecutionPayloadEnvelopePrague {
856    /// Submitted execution payload.
857    pub payload: ExecutionPayloadPrague,
858    /// Root of the parent beacon block.
859    pub parent_beacon_block_root: B256,
860    /// EIP-7685 execution requests.
861    pub execution_requests: Requests,
862}
863
864/// Osaka payload-submission request.
865///
866/// Osaka keeps the REST envelope shape while selecting the Osaka payload schema.
867#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
868pub struct ExecutionPayloadEnvelopeOsaka {
869    /// Submitted execution payload.
870    pub payload: ExecutionPayloadOsaka,
871    /// Root of the parent beacon block.
872    pub parent_beacon_block_root: B256,
873    /// EIP-7685 execution requests.
874    pub execution_requests: Requests,
875}
876
877/// Amsterdam payload-submission request.
878///
879/// Amsterdam selects the V4 payload while retaining the Cancun and Prague envelope fields.
880#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
881pub struct ExecutionPayloadEnvelopeAmsterdam {
882    /// Submitted execution payload.
883    pub payload: ExecutionPayloadAmsterdam,
884    /// Root of the parent beacon block.
885    pub parent_beacon_block_root: B256,
886    /// EIP-7685 execution requests.
887    pub execution_requests: Requests,
888}
889
890impl From<ExecutionPayloadParis> for ExecutionPayloadEnvelopeParis {
891    fn from(payload: ExecutionPayloadParis) -> Self {
892        Self { payload }
893    }
894}
895
896impl From<ExecutionPayloadShanghai> for ExecutionPayloadEnvelopeShanghai {
897    fn from(payload: ExecutionPayloadShanghai) -> Self {
898        Self { payload }
899    }
900}
901
902impl From<(ExecutionPayloadCancun, B256)> for ExecutionPayloadEnvelopeCancun {
903    fn from((payload, parent_beacon_block_root): (ExecutionPayloadCancun, B256)) -> Self {
904        Self { payload, parent_beacon_block_root }
905    }
906}
907
908impl From<(ExecutionPayloadPrague, B256, Requests)> for ExecutionPayloadEnvelopePrague {
909    fn from(
910        (payload, parent_beacon_block_root, execution_requests): (
911            ExecutionPayloadPrague,
912            B256,
913            Requests,
914        ),
915    ) -> Self {
916        Self { payload, parent_beacon_block_root, execution_requests }
917    }
918}
919
920impl From<(ExecutionPayloadOsaka, B256, Requests)> for ExecutionPayloadEnvelopeOsaka {
921    fn from(
922        (payload, parent_beacon_block_root, execution_requests): (
923            ExecutionPayloadOsaka,
924            B256,
925            Requests,
926        ),
927    ) -> Self {
928        Self { payload, parent_beacon_block_root, execution_requests }
929    }
930}
931
932impl From<(ExecutionPayloadAmsterdam, B256, Requests)> for ExecutionPayloadEnvelopeAmsterdam {
933    fn from(
934        (payload, parent_beacon_block_root, execution_requests): (
935            ExecutionPayloadAmsterdam,
936            B256,
937            Requests,
938        ),
939    ) -> Self {
940        Self { payload, parent_beacon_block_root, execution_requests }
941    }
942}
943
944/// Paris forkchoice-update request.
945///
946/// REST-SSZ uses an `Optional` field inside one container; legacy FCU used separate RPC
947/// parameters and a legacy `Option` encoding.
948#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
949pub struct ForkchoiceUpdateParis {
950    /// Current forkchoice state.
951    pub forkchoice_state: ForkchoiceState,
952    /// Optional Paris payload attributes.
953    pub payload_attributes: Optional<PayloadAttributesParis>,
954}
955
956/// Shanghai forkchoice-update request.
957#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
958pub struct ForkchoiceUpdateShanghai {
959    /// Current forkchoice state.
960    pub forkchoice_state: ForkchoiceState,
961    /// Optional Shanghai payload attributes.
962    pub payload_attributes: Optional<PayloadAttributesShanghai>,
963}
964
965/// Cancun forkchoice-update request.
966#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
967pub struct ForkchoiceUpdateCancun {
968    /// Current forkchoice state.
969    pub forkchoice_state: ForkchoiceState,
970    /// Optional Cancun payload attributes.
971    pub payload_attributes: Optional<PayloadAttributesCancun>,
972}
973
974/// Prague forkchoice-update request.
975#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
976pub struct ForkchoiceUpdatePrague {
977    /// Current forkchoice state.
978    pub forkchoice_state: ForkchoiceState,
979    /// Optional Prague payload attributes.
980    pub payload_attributes: Optional<PayloadAttributesPrague>,
981}
982
983/// Osaka forkchoice-update request.
984#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
985pub struct ForkchoiceUpdateOsaka {
986    /// Current forkchoice state.
987    pub forkchoice_state: ForkchoiceState,
988    /// Optional Osaka payload attributes.
989    pub payload_attributes: Optional<PayloadAttributesOsaka>,
990}
991
992/// Amsterdam forkchoice-update request.
993#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
994pub struct ForkchoiceUpdateAmsterdam {
995    /// Current forkchoice state.
996    pub forkchoice_state: ForkchoiceState,
997    /// Optional Amsterdam payload attributes.
998    pub payload_attributes: Optional<PayloadAttributesAmsterdam>,
999    /// Optional `Bitvector[128]` custody-column selection.
1000    pub custody_columns: Optional<B128>,
1001}
1002
1003/// Fork-specific execution payload body for Paris.
1004///
1005/// Paris omits withdrawals entirely; the legacy body keeps them as an optional union field.
1006#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1007pub struct ExecutionPayloadBodyParis {
1008    /// Enveloped encoded transactions.
1009    pub transactions: Vec<Bytes>,
1010}
1011
1012/// Fork-specific execution payload body for Shanghai.
1013///
1014/// Shanghai makes withdrawals a direct field rather than the legacy optional union.
1015#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1016pub struct ExecutionPayloadBodyShanghai {
1017    /// Enveloped encoded transactions.
1018    pub transactions: Vec<Bytes>,
1019    /// Withdrawals included in the block.
1020    pub withdrawals: Vec<Withdrawal>,
1021}
1022
1023/// Cancun uses the Shanghai execution-payload-body schema.
1024pub type ExecutionPayloadBodyCancun = ExecutionPayloadBodyShanghai;
1025
1026/// Prague uses the Shanghai execution-payload-body schema.
1027pub type ExecutionPayloadBodyPrague = ExecutionPayloadBodyShanghai;
1028
1029/// Osaka uses the Shanghai execution-payload-body schema.
1030pub type ExecutionPayloadBodyOsaka = ExecutionPayloadBodyShanghai;
1031
1032/// Fork-specific execution payload body for Amsterdam.
1033///
1034/// Amsterdam adds the block access list as a direct field rather than a legacy optional field.
1035#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1036pub struct ExecutionPayloadBodyAmsterdam {
1037    /// Enveloped encoded transactions.
1038    pub transactions: Vec<Bytes>,
1039    /// Withdrawals included in the block.
1040    pub withdrawals: Vec<Withdrawal>,
1041    /// RLP-encoded EIP-7928 block access list.
1042    pub block_access_list: Bytes,
1043}
1044
1045/// Error converting legacy cross-fork execution payload bodies into fork-specific containers.
1046#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1047pub enum ExecutionPayloadBodyConversionError {
1048    /// A field required by the selected fork is absent.
1049    MissingField(&'static str),
1050    /// A field from a later fork is populated and would be lost.
1051    UnexpectedField(&'static str),
1052}
1053
1054impl core::fmt::Display for ExecutionPayloadBodyConversionError {
1055    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1056        match self {
1057            Self::MissingField(field) => {
1058                write!(f, "missing required execution payload body field: {field}")
1059            }
1060            Self::UnexpectedField(field) => {
1061                write!(f, "unexpected later-fork execution payload body field: {field}")
1062            }
1063        }
1064    }
1065}
1066
1067impl core::error::Error for ExecutionPayloadBodyConversionError {}
1068
1069impl From<ExecutionPayloadBodyParis> for LegacyExecutionPayloadBodyV1 {
1070    fn from(value: ExecutionPayloadBodyParis) -> Self {
1071        Self { transactions: value.transactions, withdrawals: None }
1072    }
1073}
1074
1075impl TryFrom<LegacyExecutionPayloadBodyV1> for ExecutionPayloadBodyParis {
1076    type Error = ExecutionPayloadBodyConversionError;
1077
1078    fn try_from(value: LegacyExecutionPayloadBodyV1) -> Result<Self, Self::Error> {
1079        if value.withdrawals.is_some() {
1080            return Err(ExecutionPayloadBodyConversionError::UnexpectedField("withdrawals"))
1081        }
1082        Ok(Self { transactions: value.transactions })
1083    }
1084}
1085
1086impl From<ExecutionPayloadBodyShanghai> for LegacyExecutionPayloadBodyV1 {
1087    fn from(value: ExecutionPayloadBodyShanghai) -> Self {
1088        Self { transactions: value.transactions, withdrawals: Some(value.withdrawals) }
1089    }
1090}
1091
1092impl TryFrom<LegacyExecutionPayloadBodyV1> for ExecutionPayloadBodyShanghai {
1093    type Error = ExecutionPayloadBodyConversionError;
1094
1095    fn try_from(value: LegacyExecutionPayloadBodyV1) -> Result<Self, Self::Error> {
1096        Ok(Self {
1097            transactions: value.transactions,
1098            withdrawals: value
1099                .withdrawals
1100                .ok_or(ExecutionPayloadBodyConversionError::MissingField("withdrawals"))?,
1101        })
1102    }
1103}
1104
1105impl From<ExecutionPayloadBodyAmsterdam> for LegacyExecutionPayloadBodyV2 {
1106    fn from(value: ExecutionPayloadBodyAmsterdam) -> Self {
1107        Self {
1108            transactions: value.transactions,
1109            withdrawals: Some(value.withdrawals),
1110            block_access_list: Some(value.block_access_list),
1111        }
1112    }
1113}
1114
1115impl TryFrom<LegacyExecutionPayloadBodyV2> for ExecutionPayloadBodyAmsterdam {
1116    type Error = ExecutionPayloadBodyConversionError;
1117
1118    fn try_from(value: LegacyExecutionPayloadBodyV2) -> Result<Self, Self::Error> {
1119        Ok(Self {
1120            transactions: value.transactions,
1121            withdrawals: value
1122                .withdrawals
1123                .ok_or(ExecutionPayloadBodyConversionError::MissingField("withdrawals"))?,
1124            block_access_list: value
1125                .block_access_list
1126                .ok_or(ExecutionPayloadBodyConversionError::MissingField("block_access_list"))?,
1127        })
1128    }
1129}
1130
1131/// REST-SSZ historical bodies-by-hash request.
1132///
1133/// This is a single-field container, not a bare SSZ list.
1134#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1135pub struct BodiesByHashRequest {
1136    /// Requested block hashes.
1137    pub block_hashes: Vec<B256>,
1138}
1139
1140/// Historical body response entry with explicit availability.
1141///
1142/// REST-SSZ uses a boolean availability bit instead of the legacy `Option<body>` union.
1143#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1144pub struct BodyEntry<T: ssz::Encode + ssz::Decode> {
1145    /// Whether the body is available and belongs to the requested fork.
1146    pub available: bool,
1147    /// Fork-specific body, ignored when `available` is false.
1148    pub body: T,
1149}
1150
1151impl<T: ssz::Encode + ssz::Decode> BodyEntry<T> {
1152    /// Creates an available body entry.
1153    pub const fn available(body: T) -> Self {
1154        Self { available: true, body }
1155    }
1156}
1157
1158impl<T: ssz::Encode + ssz::Decode + Default> BodyEntry<T> {
1159    /// Creates an unavailable body entry.
1160    pub fn unavailable() -> Self {
1161        Self { available: false, body: T::default() }
1162    }
1163}
1164
1165/// REST-SSZ historical bodies response.
1166///
1167/// The response is a one-field SSZ container around the entries list, not a bare list.
1168#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1169pub struct BodiesResponse<T: ssz::Encode + ssz::Decode> {
1170    /// Body entries in request or range order.
1171    pub entries: Vec<BodyEntry<T>>,
1172}
1173
1174impl<T: ssz::Encode + ssz::Decode + Default> BodiesResponse<T> {
1175    /// Creates a response from optional legacy bodies.
1176    ///
1177    /// Missing bodies, or bodies that do not convert to the requested fork container, are encoded
1178    /// as unavailable entries.
1179    pub fn from_optional_bodies<LegacyBody>(
1180        bodies: Vec<Option<LegacyBody>>,
1181        convert: impl Fn(LegacyBody) -> Option<T>,
1182    ) -> Self {
1183        let entries = bodies
1184            .into_iter()
1185            .map(|body| match body.and_then(&convert) {
1186                Some(body) => BodyEntry::available(body),
1187                None => BodyEntry::unavailable(),
1188            })
1189            .collect();
1190
1191        Self { entries }
1192    }
1193}
1194
1195/// Paris historical bodies response.
1196pub type BodiesResponseParis = BodiesResponse<ExecutionPayloadBodyParis>;
1197
1198/// Shanghai historical bodies response.
1199pub type BodiesResponseShanghai = BodiesResponse<ExecutionPayloadBodyShanghai>;
1200
1201/// Cancun historical bodies response.
1202pub type BodiesResponseCancun = BodiesResponse<ExecutionPayloadBodyCancun>;
1203
1204/// Prague historical bodies response.
1205pub type BodiesResponsePrague = BodiesResponse<ExecutionPayloadBodyPrague>;
1206
1207/// Osaka historical bodies response.
1208pub type BodiesResponseOsaka = BodiesResponse<ExecutionPayloadBodyOsaka>;
1209
1210/// Amsterdam historical bodies response.
1211pub type BodiesResponseAmsterdam = BodiesResponse<ExecutionPayloadBodyAmsterdam>;
1212
1213/// V1-V3 blob request container.
1214///
1215/// This single-field container starts with a four-byte SSZ offset and is not wire-equivalent to a
1216/// top-level list.
1217#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1218pub struct BlobsV1Request {
1219    /// Requested versioned blob hashes.
1220    pub versioned_hashes: Vec<B256>,
1221}
1222
1223/// V2 uses the V1 request schema.
1224pub type BlobsV2Request = BlobsV1Request;
1225
1226/// V3 uses the V1 request schema.
1227pub type BlobsV3Request = BlobsV1Request;
1228
1229/// V4 blob request container with a packed 128-bit index bitvector.
1230#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1231pub struct BlobsV4Request {
1232    /// Requested versioned blob hashes.
1233    pub versioned_hashes: Vec<B256>,
1234    /// Requested cell indices, SSZ `Bitvector[128]`.
1235    pub indices_bitarray: B128,
1236}
1237
1238/// Blob response entry with explicit outer availability.
1239///
1240/// REST-SSZ keeps availability separate from the blob contents instead of using a legacy option.
1241#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1242pub struct BlobEntry<T: ssz::Encode + ssz::Decode> {
1243    /// Whether the complete blob contents are available.
1244    pub available: bool,
1245    /// Complete contents, or valid zero-valued contents when unavailable.
1246    pub contents: T,
1247}
1248
1249/// Bounded blob response container.
1250///
1251/// The outer container and entry availability match the REST-SSZ blob endpoint contract.
1252#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1253pub struct BlobsResponse<T: ssz::Encode + ssz::Decode> {
1254    /// One response entry per requested hash.
1255    pub entries: Vec<BlobEntry<T>>,
1256}
1257
1258/// V1 whole-blob response.
1259pub type BlobsV1Response = BlobsResponse<BlobAndProofV1>;
1260
1261/// V2 all-or-nothing cell-proof response.
1262pub type BlobsV2Response = BlobsResponse<BlobAndProofV2>;
1263
1264/// V3 partial cell-proof response.
1265pub type BlobsV3Response = BlobsResponse<BlobAndProofV2>;
1266
1267/// V4 partial cell-range response.
1268pub type BlobsV4Response = BlobsResponse<BlobCellsAndProofs>;
1269
1270/// Blob cells and proofs with REST-SSZ optional cell positions.
1271///
1272/// This uses [`Optional`] (`List[T, 1]`) for per-cell nullability, not Rust [`Option`]'s SSZ
1273/// union encoding.
1274#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1275pub struct BlobCellsAndProofs {
1276    /// Requested blob cells.
1277    pub blob_cells: Vec<Optional<Cell>>,
1278    /// KZG proofs for the requested blob cells.
1279    pub proofs: Vec<Optional<Bytes48>>,
1280}
1281
1282fn zero_blob_v1() -> BlobAndProofV1 {
1283    BlobAndProofV1 { blob: Box::new(Blob::ZERO), proof: Bytes48::ZERO }
1284}
1285
1286fn zero_blob_v2() -> BlobAndProofV2 {
1287    BlobAndProofV2 { blob: Box::new(Blob::ZERO), proofs: Vec::new() }
1288}
1289
1290impl TryFrom<Vec<Option<BlobAndProofV1>>> for BlobsV1Response {
1291    type Error = ConversionError;
1292
1293    fn try_from(value: Vec<Option<BlobAndProofV1>>) -> Result<Self, Self::Error> {
1294        if value.len() > MAX_BLOBS_REQUEST {
1295            return Err(ConversionError::TooManyItems {
1296                field: "blobs",
1297                max: MAX_BLOBS_REQUEST,
1298                actual: value.len(),
1299            })
1300        }
1301
1302        let entries = value
1303            .into_iter()
1304            .map(|value| match value {
1305                Some(contents) => BlobEntry { available: true, contents },
1306                None => BlobEntry { available: false, contents: zero_blob_v1() },
1307            })
1308            .collect();
1309        Ok(Self { entries })
1310    }
1311}
1312
1313impl TryFrom<Vec<BlobAndProofV2>> for BlobsV2Response {
1314    type Error = ConversionError;
1315
1316    fn try_from(value: Vec<BlobAndProofV2>) -> Result<Self, Self::Error> {
1317        if value.len() > MAX_BLOBS_REQUEST {
1318            return Err(ConversionError::TooManyItems {
1319                field: "blobs",
1320                max: MAX_BLOBS_REQUEST,
1321                actual: value.len(),
1322            })
1323        }
1324
1325        let entries =
1326            value.into_iter().map(|contents| BlobEntry { available: true, contents }).collect();
1327        Ok(Self { entries })
1328    }
1329}
1330
1331impl TryFrom<Vec<Option<BlobAndProofV2>>> for BlobsV3Response {
1332    type Error = ConversionError;
1333
1334    fn try_from(value: Vec<Option<BlobAndProofV2>>) -> Result<Self, Self::Error> {
1335        if value.len() > MAX_BLOBS_REQUEST {
1336            return Err(ConversionError::TooManyItems {
1337                field: "blobs",
1338                max: MAX_BLOBS_REQUEST,
1339                actual: value.len(),
1340            })
1341        }
1342
1343        let entries = value
1344            .into_iter()
1345            .map(|value| match value {
1346                Some(contents) => BlobEntry { available: true, contents },
1347                None => BlobEntry { available: false, contents: zero_blob_v2() },
1348            })
1349            .collect();
1350        Ok(Self { entries })
1351    }
1352}
1353
1354impl TryFrom<Vec<Option<BlobCellsAndProofsV1>>> for BlobsV4Response {
1355    type Error = ConversionError;
1356
1357    fn try_from(value: Vec<Option<BlobCellsAndProofsV1>>) -> Result<Self, Self::Error> {
1358        if value.len() > MAX_BLOBS_REQUEST {
1359            return Err(ConversionError::TooManyItems {
1360                field: "blobs",
1361                max: MAX_BLOBS_REQUEST,
1362                actual: value.len(),
1363            })
1364        }
1365
1366        let entries = value
1367            .into_iter()
1368            .map(|value| match value {
1369                Some(contents) => BlobEntry {
1370                    available: true,
1371                    contents: BlobCellsAndProofs {
1372                        blob_cells: contents.blob_cells.into_iter().map(Optional::from).collect(),
1373                        proofs: contents.proofs.into_iter().map(Optional::from).collect(),
1374                    },
1375                },
1376                None => BlobEntry { available: false, contents: BlobCellsAndProofs::default() },
1377            })
1378            .collect();
1379        Ok(Self { entries })
1380    }
1381}
1382
1383/// A trie-node byte list in an [`ExecutionWitnessV1`].
1384pub type WitnessNodeV1 = Vec<u8>;
1385
1386/// A contract-code byte list in an [`ExecutionWitnessV1`].
1387pub type WitnessCodeV1 = Vec<u8>;
1388
1389/// An RLP-encoded header byte list in an [`ExecutionWitnessV1`].
1390pub type WitnessHeaderV1 = Vec<u8>;
1391
1392/// Canonical execution witness for `POST /payloads/witness`.
1393///
1394/// `state` and `codes` are produced in lexicographic ascending byte order. `headers` are
1395/// RLP-encoded and ordered by ascending block number; consecutive headers must be parent-linked.
1396/// These ordering rules are producer-side requirements from the execution-specs witness builder.
1397///
1398/// This is a REST-SSZ wire container, not the JSON-RPC debug witness shape.
1399#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
1400pub struct ExecutionWitnessV1 {
1401    /// Hashed trie-node preimages required during execution and state-root recomputation.
1402    pub state: Vec<WitnessNodeV1>,
1403    /// Contract bytecode preimages created or accessed during execution.
1404    pub codes: Vec<WitnessCodeV1>,
1405    /// RLP-encoded ancestor headers used for pre-state and `BLOCKHASH` correctness proofs.
1406    pub headers: Vec<WitnessHeaderV1>,
1407}
1408
1409/// Canonical execution witness for `POST /payloads/witness`.
1410pub type ExecutionWitness = ExecutionWitnessV1;
1411
1412/// REST-SSZ response for `POST /payloads/witness`.
1413///
1414/// The witness uses the Engine REST-SSZ `Optional[T]` encoding from execution-apis and is present
1415/// only when the payload status is `VALID`.
1416#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode)]
1417pub struct PayloadStatusWithWitness {
1418    /// Result of processing the submitted payload.
1419    pub payload_status: PayloadStatus,
1420    /// Execution witness produced for a valid payload.
1421    pub witness: Optional<ExecutionWitnessV1>,
1422}
1423
1424impl PayloadStatusWithWitness {
1425    /// Creates a response, converting the witness into the REST-SSZ `Optional[T]` representation.
1426    pub fn new(payload_status: PayloadStatus, witness: Option<ExecutionWitnessV1>) -> Self {
1427        let witness = match &payload_status.status {
1428            PayloadStatusEnum::Valid => witness.into(),
1429            _ => Optional::none(),
1430        };
1431        Self { payload_status, witness }
1432    }
1433}
1434
1435/// Backwards-compatible alias for the experimental witness response name.
1436pub type NewPayloadWithWitnessResponseV1 = PayloadStatusWithWitness;
1437
1438impl ssz::Decode for PayloadStatusWithWitness {
1439    fn is_ssz_fixed_len() -> bool {
1440        false
1441    }
1442
1443    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
1444        let mut builder = ssz::SszDecoderBuilder::new(bytes);
1445        builder.register_type::<PayloadStatus>()?;
1446        builder.register_type::<Optional<ExecutionWitnessV1>>()?;
1447        let mut decoder = builder.build()?;
1448        let response =
1449            Self { payload_status: decoder.decode_next()?, witness: decoder.decode_next()? };
1450        if response.witness.is_some() &&
1451            !matches!(response.payload_status.status, PayloadStatusEnum::Valid)
1452        {
1453            return Err(ssz::DecodeError::BytesInvalid(
1454                "execution witness is only valid for VALID payload status".into(),
1455            ))
1456        }
1457        Ok(response)
1458    }
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463    use super::*;
1464    use alloy_eips::eip4895::Withdrawal;
1465    use alloy_primitives::{Address, Bloom, Bytes};
1466    use ssz::{Decode, Encode};
1467
1468    fn payload_v1() -> ExecutionPayloadV1 {
1469        ExecutionPayloadV1 {
1470            parent_hash: B256::repeat_byte(1),
1471            fee_recipient: Address::repeat_byte(2),
1472            state_root: B256::repeat_byte(3),
1473            receipts_root: B256::repeat_byte(4),
1474            logs_bloom: Bloom::repeat_byte(5),
1475            prev_randao: B256::repeat_byte(6),
1476            block_number: 7,
1477            gas_limit: 8,
1478            gas_used: 9,
1479            timestamp: 10,
1480            extra_data: Bytes::from_static(&[11, 12]),
1481            base_fee_per_gas: U256::from(13),
1482            block_hash: B256::repeat_byte(14),
1483            transactions: vec![Bytes::from_static(&[15, 16])],
1484        }
1485    }
1486
1487    fn payload_v2() -> ExecutionPayloadV2 {
1488        ExecutionPayloadV2 { payload_inner: payload_v1(), withdrawals: vec![Withdrawal::default()] }
1489    }
1490
1491    fn payload_v3() -> ExecutionPayloadV3 {
1492        ExecutionPayloadV3 { payload_inner: payload_v2(), blob_gas_used: 17, excess_blob_gas: 18 }
1493    }
1494
1495    fn payload_v4() -> ExecutionPayloadV4 {
1496        ExecutionPayloadV4 {
1497            payload_inner: payload_v3(),
1498            block_access_list: Bytes::from_static(&[19, 20]),
1499            slot_number: 21,
1500        }
1501    }
1502
1503    fn attributes_cancun() -> PayloadAttributesCancun {
1504        PayloadAttributesCancun {
1505            timestamp: 1,
1506            prev_randao: B256::repeat_byte(2),
1507            suggested_fee_recipient: Address::repeat_byte(3),
1508            withdrawals: vec![Withdrawal::default()],
1509            parent_beacon_block_root: B256::repeat_byte(4),
1510        }
1511    }
1512
1513    fn state() -> ForkchoiceState {
1514        ForkchoiceState {
1515            head_block_hash: B256::repeat_byte(1),
1516            safe_block_hash: B256::repeat_byte(2),
1517            finalized_block_hash: B256::repeat_byte(3),
1518        }
1519    }
1520
1521    fn assert_roundtrip<T>(value: &T)
1522    where
1523        T: Encode + Decode + PartialEq + core::fmt::Debug,
1524    {
1525        assert_eq!(T::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), *value);
1526    }
1527
1528    #[test]
1529    fn execution_payload_envelopes_roundtrip() {
1530        assert_roundtrip(&ExecutionPayloadEnvelopeParis { payload: payload_v1() });
1531        assert_roundtrip(&ExecutionPayloadEnvelopeShanghai { payload: payload_v2() });
1532        assert_roundtrip(&ExecutionPayloadEnvelopeCancun {
1533            payload: payload_v3(),
1534            parent_beacon_block_root: B256::repeat_byte(1),
1535        });
1536        assert_roundtrip(&ExecutionPayloadEnvelopePrague {
1537            payload: payload_v3(),
1538            parent_beacon_block_root: B256::repeat_byte(1),
1539            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1540        });
1541        assert_roundtrip(&ExecutionPayloadEnvelopeOsaka {
1542            payload: payload_v3(),
1543            parent_beacon_block_root: B256::repeat_byte(1),
1544            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1545        });
1546        assert_roundtrip(&ExecutionPayloadEnvelopeAmsterdam {
1547            payload: payload_v4(),
1548            parent_beacon_block_root: B256::repeat_byte(1),
1549            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1550        });
1551    }
1552
1553    #[test]
1554    fn paris_submission_is_a_single_field_container() {
1555        let payload = payload_v1();
1556        let payload_bytes = payload.as_ssz_bytes();
1557        let envelope = ExecutionPayloadEnvelopeParis { payload };
1558        let encoded = envelope.as_ssz_bytes();
1559        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1560        assert_eq!(&encoded[4..], payload_bytes);
1561    }
1562
1563    #[test]
1564    fn built_payloads_roundtrip() {
1565        assert_roundtrip(&BuiltPayloadParis { payload: payload_v1(), block_value: U256::from(1) });
1566        assert_roundtrip(&BuiltPayloadShanghai {
1567            payload: payload_v2(),
1568            block_value: U256::from(1),
1569        });
1570        assert_roundtrip(&BuiltPayloadCancun {
1571            execution_payload: payload_v3(),
1572            block_value: U256::from(1),
1573            blobs_bundle: BlobsBundleV1::empty(),
1574            should_override_builder: true,
1575        });
1576        assert_roundtrip(&BuiltPayloadPrague {
1577            payload: payload_v3(),
1578            block_value: U256::from(1),
1579            blobs_bundle: BlobsBundleV1::empty(),
1580            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1581            should_override_builder: true,
1582        });
1583        assert_roundtrip(&BuiltPayloadOsaka {
1584            payload: payload_v3(),
1585            block_value: U256::from(1),
1586            blobs_bundle: BlobsBundleV2::empty(),
1587            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1588            should_override_builder: true,
1589        });
1590        assert_roundtrip(&BuiltPayloadAmsterdam {
1591            payload: payload_v4(),
1592            block_value: U256::from(1),
1593            blobs_bundle: BlobsBundleV2::empty(),
1594            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1595            should_override_builder: true,
1596        });
1597    }
1598
1599    #[test]
1600    fn shanghai_built_payload_has_no_builder_override() {
1601        let payload = payload_v2();
1602        let payload_len = payload.ssz_bytes_len();
1603        let value = BuiltPayloadShanghai { payload, block_value: U256::from(1) };
1604        let encoded = value.as_ssz_bytes();
1605
1606        assert_eq!(&encoded[..4], &36u32.to_le_bytes());
1607        assert_eq!(encoded.len(), 36 + payload_len);
1608    }
1609
1610    #[test]
1611    fn legacy_built_payload_conversions_preserve_fields() {
1612        let shanghai = BuiltPayloadShanghai { payload: payload_v2(), block_value: U256::from(1) };
1613        let legacy = LegacyBuiltPayloadShanghai::from(shanghai.clone());
1614        assert_eq!(BuiltPayloadShanghai::try_from(legacy).unwrap(), shanghai);
1615
1616        let prague = BuiltPayloadPrague {
1617            payload: payload_v3(),
1618            block_value: U256::from(2),
1619            blobs_bundle: BlobsBundleV1::empty(),
1620            execution_requests: Requests::from_requests([Bytes::from_static(&[3, 4])]),
1621            should_override_builder: true,
1622        };
1623        let legacy = LegacyBuiltPayloadPrague::from(prague.clone());
1624        assert_eq!(BuiltPayloadPrague::from(legacy), prague);
1625
1626        let osaka = BuiltPayloadOsaka {
1627            payload: payload_v3(),
1628            block_value: U256::from(5),
1629            blobs_bundle: BlobsBundleV2::empty(),
1630            execution_requests: Requests::from_requests([Bytes::from_static(&[6, 7])]),
1631            should_override_builder: true,
1632        };
1633        let legacy = LegacyBuiltPayloadOsaka::from(osaka.clone());
1634        assert_eq!(BuiltPayloadOsaka::from(legacy), osaka);
1635
1636        let amsterdam = BuiltPayloadAmsterdam {
1637            payload: payload_v4(),
1638            block_value: U256::from(8),
1639            blobs_bundle: BlobsBundleV2::empty(),
1640            execution_requests: Requests::from_requests([Bytes::from_static(&[9, 10])]),
1641            should_override_builder: true,
1642        };
1643        let legacy = LegacyBuiltPayloadAmsterdam::from(amsterdam.clone());
1644        assert_eq!(BuiltPayloadAmsterdam::from(legacy), amsterdam);
1645    }
1646
1647    #[test]
1648    fn legacy_shanghai_built_payload_rejects_paris_payload() {
1649        let legacy = LegacyBuiltPayloadShanghai {
1650            execution_payload: ExecutionPayloadFieldV2::V1(payload_v1()),
1651            block_value: U256::from(1),
1652        };
1653        assert_eq!(
1654            BuiltPayloadShanghai::try_from(legacy),
1655            Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
1656        );
1657    }
1658
1659    #[test]
1660    fn prague_requests_precede_should_override_builder() {
1661        let value = BuiltPayloadPrague {
1662            payload: payload_v3(),
1663            block_value: U256::from(1),
1664            blobs_bundle: BlobsBundleV1::empty(),
1665            execution_requests: Requests::from_requests([Bytes::from_static(&[0xaa, 0xbb])]),
1666            should_override_builder: true,
1667        };
1668        let encoded = value.as_ssz_bytes();
1669        assert_eq!(encoded[48], 1);
1670        assert_eq!(&encoded[encoded.len() - 2..], &[0xaa, 0xbb]);
1671    }
1672
1673    #[test]
1674    fn forkchoice_updates_roundtrip() {
1675        let paris = PayloadAttributesParis {
1676            timestamp: 1,
1677            prev_randao: B256::repeat_byte(2),
1678            suggested_fee_recipient: Address::repeat_byte(3),
1679        };
1680        let shanghai = PayloadAttributesShanghai {
1681            timestamp: 1,
1682            prev_randao: B256::repeat_byte(2),
1683            suggested_fee_recipient: Address::repeat_byte(3),
1684            withdrawals: vec![Withdrawal::default()],
1685        };
1686        let amsterdam = PayloadAttributesAmsterdam {
1687            timestamp: 1,
1688            prev_randao: B256::repeat_byte(2),
1689            suggested_fee_recipient: Address::repeat_byte(3),
1690            withdrawals: vec![Withdrawal::default()],
1691            parent_beacon_block_root: B256::repeat_byte(4),
1692            slot_number: 5,
1693            target_gas_limit: 6,
1694        };
1695        assert_roundtrip(&ForkchoiceUpdateParis {
1696            forkchoice_state: state(),
1697            payload_attributes: Optional::some(paris),
1698        });
1699        assert_roundtrip(&ForkchoiceUpdateShanghai {
1700            forkchoice_state: state(),
1701            payload_attributes: Optional::some(shanghai),
1702        });
1703        assert_roundtrip(&ForkchoiceUpdateCancun {
1704            forkchoice_state: state(),
1705            payload_attributes: Optional::some(attributes_cancun()),
1706        });
1707        assert_roundtrip(&ForkchoiceUpdatePrague {
1708            forkchoice_state: state(),
1709            payload_attributes: Optional::some(attributes_cancun()),
1710        });
1711        assert_roundtrip(&ForkchoiceUpdateOsaka {
1712            forkchoice_state: state(),
1713            payload_attributes: Optional::some(attributes_cancun()),
1714        });
1715        assert_roundtrip(&ForkchoiceUpdateAmsterdam {
1716            forkchoice_state: state(),
1717            payload_attributes: Optional::some(amsterdam),
1718            custody_columns: Optional::some(B128::repeat_byte(0xa5)),
1719        });
1720    }
1721
1722    #[test]
1723    fn payload_attributes_legacy_conversions_preserve_fork_shape() {
1724        let cancun = attributes_cancun();
1725        let legacy = LegacyPayloadAttributes::from(cancun.clone());
1726        assert_eq!(PayloadAttributesCancun::try_from(legacy).unwrap(), cancun);
1727
1728        let amsterdam = PayloadAttributesAmsterdam {
1729            timestamp: 1,
1730            prev_randao: B256::repeat_byte(2),
1731            suggested_fee_recipient: Address::repeat_byte(3),
1732            withdrawals: vec![Withdrawal::default()],
1733            parent_beacon_block_root: B256::repeat_byte(4),
1734            slot_number: 5,
1735            target_gas_limit: 6,
1736        };
1737        let legacy = LegacyPayloadAttributes::from(amsterdam.clone());
1738        assert_eq!(PayloadAttributesAmsterdam::try_from(legacy).unwrap(), amsterdam);
1739    }
1740
1741    #[test]
1742    fn payload_attributes_legacy_conversions_reject_loss() {
1743        let mut legacy = LegacyPayloadAttributes::default();
1744        assert_eq!(
1745            PayloadAttributesShanghai::try_from(legacy.clone()),
1746            Err(PayloadAttributesConversionError::MissingField("withdrawals"))
1747        );
1748
1749        legacy.withdrawals = Some(vec![]);
1750        legacy.parent_beacon_block_root = Some(B256::ZERO);
1751        assert_eq!(
1752            PayloadAttributesShanghai::try_from(legacy),
1753            Err(PayloadAttributesConversionError::UnexpectedField("parent_beacon_block_root"))
1754        );
1755    }
1756
1757    #[test]
1758    fn every_payload_status_roundtrips() {
1759        for status in [
1760            PayloadStatusEnum::Valid,
1761            PayloadStatusEnum::Invalid { validation_error: "invalid".into() },
1762            PayloadStatusEnum::Syncing,
1763            PayloadStatusEnum::Accepted,
1764        ] {
1765            let validation_error = legacy_validation_error(&status).unwrap();
1766            let value = PayloadStatus {
1767                status,
1768                latest_valid_hash: Optional::some(B256::ZERO),
1769                validation_error,
1770            };
1771            assert_eq!(PayloadStatus::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), value);
1772        }
1773    }
1774
1775    #[test]
1776    fn payload_status_preserves_absent_invalid_validation_error() {
1777        let mut bytes = Vec::new();
1778        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1779        encoder.append(&1u8);
1780        encoder.append(&Optional::<B256>::none());
1781        encoder.append(&Optional::<ErrorBytes>::none());
1782        encoder.finalize();
1783
1784        let decoded = PayloadStatus::from_ssz_bytes(&bytes).unwrap();
1785        assert!(decoded.validation_error.is_none());
1786        assert_eq!(decoded.as_ssz_bytes(), bytes);
1787    }
1788
1789    #[test]
1790    fn payload_status_rejects_non_invalid_validation_error() {
1791        let mut bytes = Vec::new();
1792        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1793        encoder.append(&0u8);
1794        encoder.append(&Optional::<B256>::none());
1795        encoder.append(&Optional::some(Vec::<u8>::new()));
1796        encoder.finalize();
1797        assert!(PayloadStatus::from_ssz_bytes(&bytes).is_err());
1798    }
1799
1800    #[test]
1801    fn payload_status_legacy_conversion_rejects_oversized_error() {
1802        assert!(PayloadStatus::try_from(LegacyPayloadStatus {
1803            status: PayloadStatusEnum::Invalid { validation_error: "x".repeat(1025) },
1804            latest_valid_hash: None,
1805        })
1806        .is_err());
1807    }
1808
1809    #[test]
1810    fn forkchoice_response_distinguishes_absent_and_zero_payload_id() {
1811        let status = PayloadStatus {
1812            status: PayloadStatusEnum::Valid,
1813            latest_valid_hash: Optional::none(),
1814            validation_error: Optional::none(),
1815        };
1816        let none = ForkchoiceUpdateResponse {
1817            payload_status: status.clone(),
1818            payload_id: Optional::none(),
1819        };
1820        let zero = ForkchoiceUpdateResponse {
1821            payload_status: status,
1822            payload_id: Optional::some(PayloadId::default()),
1823        };
1824
1825        assert_ne!(none.as_ssz_bytes(), zero.as_ssz_bytes());
1826        assert_roundtrip(&none);
1827        assert_roundtrip(&zero);
1828    }
1829
1830    #[test]
1831    fn forkchoice_conversion_rejects_accepted() {
1832        let legacy = LegacyForkchoice::from_status(PayloadStatusEnum::Accepted);
1833        assert_eq!(
1834            ForkchoiceUpdateResponse::try_from(legacy),
1835            Err(ConversionError::AcceptedForkchoice)
1836        );
1837    }
1838
1839    fn blob_v2(byte: u8) -> BlobAndProofV2 {
1840        BlobAndProofV2 {
1841            blob: Box::new(Blob::repeat_byte(byte)),
1842            proofs: vec![Bytes48::repeat_byte(byte)],
1843        }
1844    }
1845
1846    #[test]
1847    fn blob_requests_are_single_field_containers() {
1848        let request = BlobsV1Request { versioned_hashes: vec![B256::repeat_byte(0x42)] };
1849        let encoded = request.as_ssz_bytes();
1850
1851        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1852        assert_eq!(&encoded[4..], B256::repeat_byte(0x42).as_slice());
1853        assert_eq!(BlobsV1Request::from_ssz_bytes(&encoded).unwrap(), request);
1854
1855        let _: BlobsV2Request = BlobsV2Request::from_ssz_bytes(&encoded).unwrap();
1856        let _: BlobsV3Request = BlobsV3Request::from_ssz_bytes(&encoded).unwrap();
1857    }
1858
1859    #[test]
1860    fn blob_v4_request_roundtrips_bitvector() {
1861        let request = BlobsV4Request {
1862            versioned_hashes: vec![B256::repeat_byte(0x11)],
1863            indices_bitarray: B128::repeat_byte(0xa5),
1864        };
1865
1866        assert_roundtrip(&request);
1867    }
1868
1869    #[test]
1870    fn blob_response_conversions_preserve_availability_and_order() {
1871        let v1 = BlobsV1Response::try_from(vec![None]).unwrap();
1872        assert!(!v1.entries[0].available);
1873        assert_eq!(v1.entries[0].contents, zero_blob_v1());
1874
1875        let v2 = BlobsV2Response::try_from(vec![blob_v2(1), blob_v2(2)]).unwrap();
1876        assert!(v2.entries.iter().all(|entry| entry.available));
1877
1878        let v3 = BlobsV3Response::try_from(vec![Some(blob_v2(1)), None, Some(blob_v2(3))]).unwrap();
1879        assert_eq!(
1880            v3.entries.iter().map(|entry| entry.available).collect::<Vec<_>>(),
1881            [true, false, true]
1882        );
1883        assert_eq!(v3.entries[2].contents.blob.as_slice(), Blob::repeat_byte(3).as_slice());
1884
1885        let legacy_partial = BlobCellsAndProofsV1 {
1886            blob_cells: vec![Some(Cell::repeat_byte(1)), None],
1887            proofs: vec![Some(Bytes48::repeat_byte(2)), None],
1888        };
1889        let v4 = BlobsV4Response::try_from(vec![None, Some(legacy_partial)]).unwrap();
1890        assert!(!v4.entries[0].available);
1891        assert!(v4.entries[1].available);
1892        assert!(v4.entries[1].contents.blob_cells[0].is_some());
1893        assert!(v4.entries[1].contents.proofs[1].is_none());
1894    }
1895
1896    #[test]
1897    fn blob_cells_and_proofs_uses_rest_optional() {
1898        let value = BlobCellsAndProofs {
1899            blob_cells: vec![Optional::some(Cell::repeat_byte(1))],
1900            proofs: vec![Optional::some(Bytes48::repeat_byte(2))],
1901        };
1902        let encoded = value.as_ssz_bytes();
1903
1904        assert_eq!(BlobCellsAndProofs::from_ssz_bytes(&encoded).unwrap(), value);
1905        assert!(!encoded[8..].starts_with(&[1, 0, 0, 0]));
1906    }
1907
1908    #[test]
1909    fn payload_body_requests_are_single_field_containers() {
1910        let request = BodiesByHashRequest { block_hashes: vec![B256::repeat_byte(0x33)] };
1911        let encoded = request.as_ssz_bytes();
1912
1913        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1914        assert_eq!(&encoded[4..], B256::repeat_byte(0x33).as_slice());
1915        assert_eq!(BodiesByHashRequest::from_ssz_bytes(&encoded).unwrap(), request);
1916    }
1917
1918    #[test]
1919    fn payload_body_responses_preserve_availability() {
1920        let legacy = LegacyExecutionPayloadBodyV1 {
1921            transactions: vec![Bytes::from_static(&[1, 2, 3])],
1922            withdrawals: Some(vec![Withdrawal::default()]),
1923        };
1924        let response =
1925            BodiesResponseShanghai::from_optional_bodies(vec![Some(legacy), None], |body| {
1926                ExecutionPayloadBodyShanghai::try_from(body).ok()
1927            });
1928
1929        assert!(response.entries[0].available);
1930        assert!(!response.entries[1].available);
1931        assert_roundtrip(&response);
1932    }
1933
1934    #[test]
1935    fn witness_response_roundtrips_when_status_is_valid() {
1936        let payload_status = PayloadStatus {
1937            status: PayloadStatusEnum::Valid,
1938            latest_valid_hash: Optional::none(),
1939            validation_error: Optional::none(),
1940        };
1941        let witness = ExecutionWitnessV1 {
1942            state: vec![vec![1, 2, 3]],
1943            codes: vec![vec![4, 5]],
1944            headers: vec![vec![6]],
1945        };
1946        let response = PayloadStatusWithWitness::new(payload_status, Some(witness));
1947
1948        assert_roundtrip(&response);
1949    }
1950
1951    #[test]
1952    fn witness_response_omits_witness_for_non_valid_status() {
1953        let payload_status = PayloadStatus {
1954            status: PayloadStatusEnum::Syncing,
1955            latest_valid_hash: Optional::none(),
1956            validation_error: Optional::none(),
1957        };
1958        let response =
1959            PayloadStatusWithWitness::new(payload_status, Some(ExecutionWitnessV1::default()));
1960
1961        assert!(response.witness.is_none());
1962        assert_roundtrip(&response);
1963    }
1964
1965    #[test]
1966    fn optional_rejects_more_than_one_value() {
1967        assert!(Optional::<B256>::from_ssz_bytes(&[0; 64]).is_err());
1968    }
1969}