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 and fork-specific payload containers from
6//! [execution-apis PR #793](https://github.com/ethereum/execution-apis/pull/793), plus the
7//! experimental payload-with-witness response type that extends the same REST-SSZ model.
8
9use alloy_eips::{eip4895::Withdrawal, eip7685::Requests};
10use alloy_primitives::{Address, B128, B256, U256};
11use alloy_rpc_types_engine::{
12    BlobsBundleV1, BlobsBundleV2, ExecutionPayloadEnvelopeV2 as LegacyBuiltPayloadShanghai,
13    ExecutionPayloadEnvelopeV4 as LegacyBuiltPayloadPrague,
14    ExecutionPayloadEnvelopeV5 as LegacyBuiltPayloadOsaka,
15    ExecutionPayloadEnvelopeV6 as LegacyBuiltPayloadAmsterdam, ExecutionPayloadFieldV2,
16    ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, ExecutionPayloadV4,
17    ForkchoiceState, ForkchoiceUpdated as LegacyForkchoice,
18    PayloadAttributes as LegacyPayloadAttributes, PayloadId, PayloadStatus as LegacyPayloadStatus,
19    PayloadStatusEnum,
20};
21
22type ErrorBytes = Vec<u8>;
23
24/// An Engine API v2 SSZ optional encoded as `List[T, 1]`.
25///
26/// This differs from [`Option<T>`]'s `ethereum_ssz` encoding, which uses an SSZ union.
27#[derive(Clone, Debug, Default, PartialEq, Eq)]
28pub struct Optional<T>(Vec<T>);
29
30impl<T> Optional<T> {
31    /// Creates an absent optional.
32    pub const fn none() -> Self {
33        Self(Vec::new())
34    }
35
36    /// Creates a present optional.
37    pub fn some(value: T) -> Self {
38        Self(vec![value])
39    }
40
41    /// Returns the contained value, if present.
42    pub fn as_ref(&self) -> Option<&T> {
43        self.0.first()
44    }
45
46    /// Returns true if no value is present.
47    pub const fn is_none(&self) -> bool {
48        self.0.is_empty()
49    }
50
51    /// Returns true if a value is present.
52    pub const fn is_some(&self) -> bool {
53        !self.is_none()
54    }
55
56    /// Converts into a Rust optional.
57    pub fn into_option(mut self) -> Option<T> {
58        self.0.pop()
59    }
60}
61
62impl<T> From<Option<T>> for Optional<T> {
63    fn from(value: Option<T>) -> Self {
64        value.map_or_else(Self::none, Self::some)
65    }
66}
67
68impl<T> From<Optional<T>> for Option<T> {
69    fn from(value: Optional<T>) -> Self {
70        value.into_option()
71    }
72}
73
74impl<T: ssz::Encode> ssz::Encode for Optional<T> {
75    fn is_ssz_fixed_len() -> bool {
76        false
77    }
78
79    fn ssz_bytes_len(&self) -> usize {
80        self.0.ssz_bytes_len()
81    }
82
83    fn ssz_append(&self, buf: &mut Vec<u8>) {
84        self.0.ssz_append(buf);
85    }
86}
87
88impl<T: ssz::Decode> ssz::Decode for Optional<T> {
89    fn is_ssz_fixed_len() -> bool {
90        false
91    }
92
93    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
94        let values = Vec::<T>::from_ssz_bytes(bytes)?;
95        if values.len() > 1 {
96            return Err(ssz::DecodeError::BytesInvalid("optional has more than one value".into()))
97        }
98        Ok(Self(values))
99    }
100}
101
102/// Engine API v2 REST-SSZ payload status.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct PayloadStatus {
105    /// Payload validation status.
106    pub status: PayloadStatusEnum,
107    /// Most recent valid block hash.
108    pub latest_valid_hash: Optional<B256>,
109    /// Optional payload validation error bytes.
110    pub validation_error: Optional<ErrorBytes>,
111}
112
113const MAX_ERROR_BYTES: usize = 1024;
114
115const fn status_code(status: &PayloadStatusEnum) -> u8 {
116    match status {
117        PayloadStatusEnum::Valid => 0,
118        PayloadStatusEnum::Invalid { .. } => 1,
119        PayloadStatusEnum::Syncing => 2,
120        PayloadStatusEnum::Accepted => 3,
121    }
122}
123
124fn status_from_code(code: u8) -> Result<PayloadStatusEnum, ssz::DecodeError> {
125    match code {
126        0 => Ok(PayloadStatusEnum::Valid),
127        1 => Ok(PayloadStatusEnum::Invalid { validation_error: String::new() }),
128        2 => Ok(PayloadStatusEnum::Syncing),
129        3 => Ok(PayloadStatusEnum::Accepted),
130        _ => Err(ssz::DecodeError::BytesInvalid("unknown payload status code".into())),
131    }
132}
133
134fn legacy_validation_error(
135    status: &PayloadStatusEnum,
136) -> Result<Optional<ErrorBytes>, ConversionError> {
137    match status {
138        PayloadStatusEnum::Invalid { validation_error } => {
139            let bytes = validation_error.as_bytes().to_vec();
140            if bytes.len() > MAX_ERROR_BYTES {
141                return Err(ConversionError::ErrorBytesTooLong)
142            }
143            Ok(Optional::some(bytes))
144        }
145        _ => Ok(Optional::none()),
146    }
147}
148
149impl ssz::Encode for PayloadStatus {
150    fn is_ssz_fixed_len() -> bool {
151        false
152    }
153
154    fn ssz_bytes_len(&self) -> usize {
155        1 + ssz::BYTES_PER_LENGTH_OFFSET * 2 +
156            self.latest_valid_hash.ssz_bytes_len() +
157            self.validation_error.ssz_bytes_len()
158    }
159
160    fn ssz_append(&self, buf: &mut Vec<u8>) {
161        let mut encoder = ssz::SszEncoder::container(buf, 1 + ssz::BYTES_PER_LENGTH_OFFSET * 2);
162        encoder.append(&status_code(&self.status));
163        encoder.append(&self.latest_valid_hash);
164        encoder.append(&self.validation_error);
165        encoder.finalize();
166    }
167}
168
169impl ssz::Decode for PayloadStatus {
170    fn is_ssz_fixed_len() -> bool {
171        false
172    }
173
174    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
175        let mut builder = ssz::SszDecoderBuilder::new(bytes);
176        builder.register_type::<u8>()?;
177        builder.register_type::<Optional<B256>>()?;
178        builder.register_type::<Optional<ErrorBytes>>()?;
179        let mut decoder = builder.build()?;
180        let mut status = status_from_code(decoder.decode_next()?)?;
181        let latest_valid_hash = decoder.decode_next()?;
182        let validation_error: Optional<ErrorBytes> = decoder.decode_next()?;
183        if let PayloadStatusEnum::Invalid { validation_error: error } = &mut status {
184            *error = match validation_error.as_ref() {
185                Some(error) => String::from_utf8(error.clone())
186                    .map_err(|err| ssz::DecodeError::BytesInvalid(err.to_string()))?,
187                None => String::new(),
188            };
189            if validation_error.as_ref().is_some_and(|error| error.len() > MAX_ERROR_BYTES) {
190                return Err(ssz::DecodeError::BytesInvalid(
191                    "payload validation error is too long".into(),
192                ))
193            }
194        } else if validation_error.is_some() {
195            return Err(ssz::DecodeError::BytesInvalid(
196                "validation error is only valid for INVALID status".into(),
197            ));
198        }
199        Ok(Self { status, latest_valid_hash, validation_error })
200    }
201}
202
203/// Error converting legacy Engine API values into v2 REST-SSZ values.
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub enum ConversionError {
206    /// Payload validation error exceeded the REST-SSZ byte bound.
207    ErrorBytesTooLong,
208    /// `ACCEPTED` is not permitted in a forkchoice response.
209    AcceptedForkchoice,
210}
211
212impl core::fmt::Display for ConversionError {
213    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214        match self {
215            Self::ErrorBytesTooLong => f.write_str("payload validation error is too long"),
216            Self::AcceptedForkchoice => {
217                f.write_str("ACCEPTED is not valid in a forkchoice response")
218            }
219        }
220    }
221}
222
223impl core::error::Error for ConversionError {}
224
225impl TryFrom<LegacyPayloadStatus> for PayloadStatus {
226    type Error = ConversionError;
227
228    fn try_from(value: LegacyPayloadStatus) -> Result<Self, Self::Error> {
229        let validation_error = legacy_validation_error(&value.status)?;
230        Ok(Self {
231            status: value.status,
232            latest_valid_hash: value.latest_valid_hash.into(),
233            validation_error,
234        })
235    }
236}
237
238impl From<PayloadStatus> for LegacyPayloadStatus {
239    fn from(value: PayloadStatus) -> Self {
240        let status = match value.status {
241            PayloadStatusEnum::Invalid { .. } => PayloadStatusEnum::Invalid {
242                validation_error: value
243                    .validation_error
244                    .as_ref()
245                    .and_then(|error| String::from_utf8(error.clone()).ok())
246                    .unwrap_or_default(),
247            },
248            status => status,
249        };
250        Self { status, latest_valid_hash: value.latest_valid_hash.into() }
251    }
252}
253
254/// Engine API v2 REST-SSZ forkchoice update response.
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct ForkchoiceUpdateResponse {
257    /// Restricted payload status; `ACCEPTED` is invalid here.
258    pub payload_status: PayloadStatus,
259    /// Opaque server-assigned payload identifier.
260    pub payload_id: Optional<PayloadId>,
261}
262
263impl ssz::Encode for ForkchoiceUpdateResponse {
264    fn is_ssz_fixed_len() -> bool {
265        false
266    }
267
268    fn ssz_bytes_len(&self) -> usize {
269        ssz::BYTES_PER_LENGTH_OFFSET * 2 +
270            self.payload_status.ssz_bytes_len() +
271            self.payload_id.ssz_bytes_len()
272    }
273
274    fn ssz_append(&self, buf: &mut Vec<u8>) {
275        let mut encoder = ssz::SszEncoder::container(buf, ssz::BYTES_PER_LENGTH_OFFSET * 2);
276        encoder.append(&self.payload_status);
277        encoder.append(&self.payload_id);
278        encoder.finalize();
279    }
280}
281
282impl ssz::Decode for ForkchoiceUpdateResponse {
283    fn is_ssz_fixed_len() -> bool {
284        false
285    }
286
287    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
288        let mut builder = ssz::SszDecoderBuilder::new(bytes);
289        builder.register_type::<PayloadStatus>()?;
290        builder.register_type::<Optional<PayloadId>>()?;
291        let mut decoder = builder.build()?;
292        let response =
293            Self { payload_status: decoder.decode_next()?, payload_id: decoder.decode_next()? };
294        if matches!(response.payload_status.status, PayloadStatusEnum::Accepted) {
295            return Err(ssz::DecodeError::BytesInvalid(
296                "ACCEPTED is not valid in a forkchoice response".into(),
297            ));
298        }
299        Ok(response)
300    }
301}
302
303impl TryFrom<LegacyForkchoice> for ForkchoiceUpdateResponse {
304    type Error = ConversionError;
305
306    fn try_from(value: LegacyForkchoice) -> Result<Self, Self::Error> {
307        let payload_status = PayloadStatus::try_from(value.payload_status)?;
308        if matches!(payload_status.status, PayloadStatusEnum::Accepted) {
309            return Err(ConversionError::AcceptedForkchoice)
310        }
311        Ok(Self { payload_status, payload_id: value.payload_id.into() })
312    }
313}
314
315impl From<ForkchoiceUpdateResponse> for LegacyForkchoice {
316    fn from(value: ForkchoiceUpdateResponse) -> Self {
317        Self { payload_status: value.payload_status.into(), payload_id: value.payload_id.into() }
318    }
319}
320
321/// Paris execution payload.
322pub type ExecutionPayloadParis = ExecutionPayloadV1;
323
324/// Shanghai execution payload.
325pub type ExecutionPayloadShanghai = ExecutionPayloadV2;
326
327/// Cancun execution payload.
328pub type ExecutionPayloadCancun = ExecutionPayloadV3;
329
330/// Prague execution payload.
331pub type ExecutionPayloadPrague = ExecutionPayloadV3;
332
333/// Osaka execution payload.
334pub type ExecutionPayloadOsaka = ExecutionPayloadV3;
335
336/// Amsterdam execution payload.
337pub type ExecutionPayloadAmsterdam = ExecutionPayloadV4;
338
339/// Paris payload attributes.
340#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
341pub struct PayloadAttributesParis {
342    /// Payload timestamp.
343    pub timestamp: u64,
344    /// Previous RANDAO value.
345    pub prev_randao: B256,
346    /// Suggested fee recipient.
347    pub suggested_fee_recipient: Address,
348}
349
350/// Shanghai payload attributes.
351#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
352pub struct PayloadAttributesShanghai {
353    /// Payload timestamp.
354    pub timestamp: u64,
355    /// Previous RANDAO value.
356    pub prev_randao: B256,
357    /// Suggested fee recipient.
358    pub suggested_fee_recipient: Address,
359    /// Withdrawals to include in the payload.
360    pub withdrawals: Vec<Withdrawal>,
361}
362
363/// Cancun payload attributes.
364#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
365pub struct PayloadAttributesCancun {
366    /// Payload timestamp.
367    pub timestamp: u64,
368    /// Previous RANDAO value.
369    pub prev_randao: B256,
370    /// Suggested fee recipient.
371    pub suggested_fee_recipient: Address,
372    /// Withdrawals to include in the payload.
373    pub withdrawals: Vec<Withdrawal>,
374    /// Root of the parent beacon block.
375    pub parent_beacon_block_root: B256,
376}
377
378/// Prague uses the Cancun payload-attributes schema.
379pub type PayloadAttributesPrague = PayloadAttributesCancun;
380
381/// Osaka uses the Cancun payload-attributes schema.
382pub type PayloadAttributesOsaka = PayloadAttributesCancun;
383
384/// Amsterdam payload attributes.
385#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
386pub struct PayloadAttributesAmsterdam {
387    /// Payload timestamp.
388    pub timestamp: u64,
389    /// Previous RANDAO value.
390    pub prev_randao: B256,
391    /// Suggested fee recipient.
392    pub suggested_fee_recipient: Address,
393    /// Withdrawals to include in the payload.
394    pub withdrawals: Vec<Withdrawal>,
395    /// Root of the parent beacon block.
396    pub parent_beacon_block_root: B256,
397    /// Consensus-layer slot number.
398    pub slot_number: u64,
399    /// Target gas limit.
400    pub target_gas_limit: u64,
401}
402
403/// Error converting legacy cross-fork payload attributes into a fork-specific SSZ container.
404#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405pub enum PayloadAttributesConversionError {
406    /// A field required by the selected fork is absent.
407    MissingField(&'static str),
408    /// A field from a later fork is populated and would be lost.
409    UnexpectedField(&'static str),
410}
411
412impl core::fmt::Display for PayloadAttributesConversionError {
413    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
414        match self {
415            Self::MissingField(field) => {
416                write!(f, "missing required payload attributes field: {field}")
417            }
418            Self::UnexpectedField(field) => {
419                write!(f, "unexpected later-fork payload attributes field: {field}")
420            }
421        }
422    }
423}
424
425impl core::error::Error for PayloadAttributesConversionError {}
426
427const fn ensure_absent<T>(
428    value: &Option<T>,
429    field: &'static str,
430) -> Result<(), PayloadAttributesConversionError> {
431    if value.is_some() {
432        Err(PayloadAttributesConversionError::UnexpectedField(field))
433    } else {
434        Ok(())
435    }
436}
437
438fn require<T>(
439    value: Option<T>,
440    field: &'static str,
441) -> Result<T, PayloadAttributesConversionError> {
442    value.ok_or(PayloadAttributesConversionError::MissingField(field))
443}
444
445impl From<PayloadAttributesParis> for LegacyPayloadAttributes {
446    fn from(value: PayloadAttributesParis) -> Self {
447        Self {
448            timestamp: value.timestamp,
449            prev_randao: value.prev_randao,
450            suggested_fee_recipient: value.suggested_fee_recipient,
451            withdrawals: None,
452            parent_beacon_block_root: None,
453            slot_number: None,
454            target_gas_limit: None,
455        }
456    }
457}
458
459impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesParis {
460    type Error = PayloadAttributesConversionError;
461
462    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
463        ensure_absent(&value.withdrawals, "withdrawals")?;
464        ensure_absent(&value.parent_beacon_block_root, "parent_beacon_block_root")?;
465        ensure_absent(&value.slot_number, "slot_number")?;
466        ensure_absent(&value.target_gas_limit, "target_gas_limit")?;
467        Ok(Self {
468            timestamp: value.timestamp,
469            prev_randao: value.prev_randao,
470            suggested_fee_recipient: value.suggested_fee_recipient,
471        })
472    }
473}
474
475impl From<PayloadAttributesShanghai> for LegacyPayloadAttributes {
476    fn from(value: PayloadAttributesShanghai) -> Self {
477        Self {
478            timestamp: value.timestamp,
479            prev_randao: value.prev_randao,
480            suggested_fee_recipient: value.suggested_fee_recipient,
481            withdrawals: Some(value.withdrawals),
482            parent_beacon_block_root: None,
483            slot_number: None,
484            target_gas_limit: None,
485        }
486    }
487}
488
489impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesShanghai {
490    type Error = PayloadAttributesConversionError;
491
492    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
493        ensure_absent(&value.parent_beacon_block_root, "parent_beacon_block_root")?;
494        ensure_absent(&value.slot_number, "slot_number")?;
495        ensure_absent(&value.target_gas_limit, "target_gas_limit")?;
496        Ok(Self {
497            timestamp: value.timestamp,
498            prev_randao: value.prev_randao,
499            suggested_fee_recipient: value.suggested_fee_recipient,
500            withdrawals: require(value.withdrawals, "withdrawals")?,
501        })
502    }
503}
504
505impl From<PayloadAttributesCancun> for LegacyPayloadAttributes {
506    fn from(value: PayloadAttributesCancun) -> Self {
507        Self {
508            timestamp: value.timestamp,
509            prev_randao: value.prev_randao,
510            suggested_fee_recipient: value.suggested_fee_recipient,
511            withdrawals: Some(value.withdrawals),
512            parent_beacon_block_root: Some(value.parent_beacon_block_root),
513            slot_number: None,
514            target_gas_limit: None,
515        }
516    }
517}
518
519impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesCancun {
520    type Error = PayloadAttributesConversionError;
521
522    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
523        ensure_absent(&value.slot_number, "slot_number")?;
524        ensure_absent(&value.target_gas_limit, "target_gas_limit")?;
525        Ok(Self {
526            timestamp: value.timestamp,
527            prev_randao: value.prev_randao,
528            suggested_fee_recipient: value.suggested_fee_recipient,
529            withdrawals: require(value.withdrawals, "withdrawals")?,
530            parent_beacon_block_root: require(
531                value.parent_beacon_block_root,
532                "parent_beacon_block_root",
533            )?,
534        })
535    }
536}
537
538impl From<PayloadAttributesAmsterdam> for LegacyPayloadAttributes {
539    fn from(value: PayloadAttributesAmsterdam) -> Self {
540        Self {
541            timestamp: value.timestamp,
542            prev_randao: value.prev_randao,
543            suggested_fee_recipient: value.suggested_fee_recipient,
544            withdrawals: Some(value.withdrawals),
545            parent_beacon_block_root: Some(value.parent_beacon_block_root),
546            slot_number: Some(value.slot_number),
547            target_gas_limit: Some(value.target_gas_limit),
548        }
549    }
550}
551
552impl TryFrom<LegacyPayloadAttributes> for PayloadAttributesAmsterdam {
553    type Error = PayloadAttributesConversionError;
554
555    fn try_from(value: LegacyPayloadAttributes) -> Result<Self, Self::Error> {
556        Ok(Self {
557            timestamp: value.timestamp,
558            prev_randao: value.prev_randao,
559            suggested_fee_recipient: value.suggested_fee_recipient,
560            withdrawals: require(value.withdrawals, "withdrawals")?,
561            parent_beacon_block_root: require(
562                value.parent_beacon_block_root,
563                "parent_beacon_block_root",
564            )?,
565            slot_number: require(value.slot_number, "slot_number")?,
566            target_gas_limit: require(value.target_gas_limit, "target_gas_limit")?,
567        })
568    }
569}
570
571/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Paris.
572///
573/// Unlike the legacy `engine_getPayloadV1` response, this includes the expected block value.
574#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
575pub struct BuiltPayloadParis {
576    /// Execution payload V1.
577    pub payload: ExecutionPayloadParis,
578    /// The expected value to be received by the fee recipient in wei.
579    pub block_value: U256,
580}
581
582/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Shanghai.
583///
584/// This follows the legacy `engine_getPayloadV2` payload-build response shape: execution payload
585/// plus block value only. `should_override_builder` starts at Cancun.
586#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
587pub struct BuiltPayloadShanghai {
588    /// Execution payload V2.
589    pub payload: ExecutionPayloadShanghai,
590    /// The expected value to be received by the fee recipient in wei.
591    pub block_value: U256,
592}
593
594/// Engine API v2 REST-SSZ payload-build response for Cancun.
595///
596/// This is wire-compatible with the legacy `engine_getPayloadV3` response envelope.
597pub type BuiltPayloadCancun = alloy_rpc_types_engine::ExecutionPayloadEnvelopeV3;
598
599/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Prague.
600///
601/// Unlike the legacy [`alloy_rpc_types_engine::ExecutionPayloadEnvelopeV4`],
602/// `execution_requests` precedes `should_override_builder` in the normative SSZ field order.
603#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
604pub struct BuiltPayloadPrague {
605    /// Execution payload V3.
606    pub payload: ExecutionPayloadPrague,
607    /// The expected value to be received by the fee recipient in wei.
608    pub block_value: U256,
609    /// The blobs, commitments, and proofs associated with the executed payload.
610    pub blobs_bundle: BlobsBundleV1,
611    /// A list of opaque EIP-7685 requests.
612    pub execution_requests: Requests,
613    /// A suggestion from the execution layer whether this payload should be used instead of an
614    /// externally provided one.
615    pub should_override_builder: bool,
616}
617
618/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Osaka.
619#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
620pub struct BuiltPayloadOsaka {
621    /// Execution payload V3.
622    pub payload: ExecutionPayloadOsaka,
623    /// The expected value to be received by the fee recipient in wei.
624    pub block_value: U256,
625    /// The blobs, commitments, and EIP-7594 cell proofs associated with the executed payload.
626    pub blobs_bundle: BlobsBundleV2,
627    /// A list of opaque EIP-7685 requests.
628    pub execution_requests: Requests,
629    /// A suggestion from the execution layer whether this payload should be used instead of an
630    /// externally provided one.
631    pub should_override_builder: bool,
632}
633
634/// This structure maps to the Engine API v2 REST-SSZ payload-build response for Amsterdam.
635#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
636pub struct BuiltPayloadAmsterdam {
637    /// Execution payload V4.
638    pub payload: ExecutionPayloadAmsterdam,
639    /// The expected value to be received by the fee recipient in wei.
640    pub block_value: U256,
641    /// The blobs, commitments, and EIP-7594 cell proofs associated with the executed payload.
642    pub blobs_bundle: BlobsBundleV2,
643    /// A list of opaque EIP-7685 requests.
644    pub execution_requests: Requests,
645    /// A suggestion from the execution layer whether this payload should be used instead of an
646    /// externally provided one.
647    pub should_override_builder: bool,
648}
649
650/// Error converting legacy payload-build envelopes into fork-specific REST-SSZ containers.
651#[derive(Clone, Copy, Debug, PartialEq, Eq)]
652pub enum BuiltPayloadConversionError {
653    /// The legacy envelope carried an execution payload from a different fork.
654    UnexpectedPayloadFork(&'static str),
655}
656
657impl core::fmt::Display for BuiltPayloadConversionError {
658    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
659        match self {
660            Self::UnexpectedPayloadFork(fork) => {
661                write!(f, "unexpected execution payload fork: {fork}")
662            }
663        }
664    }
665}
666
667impl core::error::Error for BuiltPayloadConversionError {}
668
669impl From<BuiltPayloadShanghai> for LegacyBuiltPayloadShanghai {
670    fn from(value: BuiltPayloadShanghai) -> Self {
671        Self {
672            execution_payload: ExecutionPayloadFieldV2::V2(value.payload),
673            block_value: value.block_value,
674        }
675    }
676}
677
678impl TryFrom<LegacyBuiltPayloadShanghai> for BuiltPayloadShanghai {
679    type Error = BuiltPayloadConversionError;
680
681    fn try_from(value: LegacyBuiltPayloadShanghai) -> Result<Self, Self::Error> {
682        match value.execution_payload {
683            ExecutionPayloadFieldV2::V2(payload) => {
684                Ok(Self { payload, block_value: value.block_value })
685            }
686            ExecutionPayloadFieldV2::V1(_) => {
687                Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
688            }
689        }
690    }
691}
692
693impl From<LegacyBuiltPayloadPrague> for BuiltPayloadPrague {
694    fn from(value: LegacyBuiltPayloadPrague) -> Self {
695        Self {
696            payload: value.envelope_inner.execution_payload,
697            block_value: value.envelope_inner.block_value,
698            blobs_bundle: value.envelope_inner.blobs_bundle,
699            execution_requests: value.execution_requests,
700            should_override_builder: value.envelope_inner.should_override_builder,
701        }
702    }
703}
704
705impl From<BuiltPayloadPrague> for LegacyBuiltPayloadPrague {
706    fn from(value: BuiltPayloadPrague) -> Self {
707        Self {
708            envelope_inner: alloy_rpc_types_engine::ExecutionPayloadEnvelopeV3 {
709                execution_payload: value.payload,
710                block_value: value.block_value,
711                blobs_bundle: value.blobs_bundle,
712                should_override_builder: value.should_override_builder,
713            },
714            execution_requests: value.execution_requests,
715        }
716    }
717}
718
719impl From<LegacyBuiltPayloadOsaka> for BuiltPayloadOsaka {
720    fn from(value: LegacyBuiltPayloadOsaka) -> Self {
721        Self {
722            payload: value.execution_payload,
723            block_value: value.block_value,
724            blobs_bundle: value.blobs_bundle,
725            execution_requests: value.execution_requests,
726            should_override_builder: value.should_override_builder,
727        }
728    }
729}
730
731impl From<BuiltPayloadOsaka> for LegacyBuiltPayloadOsaka {
732    fn from(value: BuiltPayloadOsaka) -> Self {
733        Self {
734            execution_payload: value.payload,
735            block_value: value.block_value,
736            blobs_bundle: value.blobs_bundle,
737            should_override_builder: value.should_override_builder,
738            execution_requests: value.execution_requests,
739        }
740    }
741}
742
743impl From<LegacyBuiltPayloadAmsterdam> for BuiltPayloadAmsterdam {
744    fn from(value: LegacyBuiltPayloadAmsterdam) -> Self {
745        Self {
746            payload: value.execution_payload,
747            block_value: value.block_value,
748            blobs_bundle: value.blobs_bundle,
749            execution_requests: value.execution_requests,
750            should_override_builder: value.should_override_builder,
751        }
752    }
753}
754
755impl From<BuiltPayloadAmsterdam> for LegacyBuiltPayloadAmsterdam {
756    fn from(value: BuiltPayloadAmsterdam) -> Self {
757        Self {
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            execution_requests: value.execution_requests,
763        }
764    }
765}
766
767/// REST-SSZ payload-submission request containers.
768///
769/// These are distinct from the legacy Engine JSON-RPC get-payload envelopes: submission requests
770/// are fork-specific request bodies, while the legacy envelope types mostly model get-payload
771/// responses and sometimes carry response-only fields such as block value, blob bundles, builder
772/// override hints, or a different field order.
773///
774/// Paris payload-submission request.
775#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
776pub struct ExecutionPayloadEnvelopeParis {
777    /// Submitted execution payload.
778    pub payload: ExecutionPayloadParis,
779}
780
781/// Shanghai payload-submission request.
782#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
783pub struct ExecutionPayloadEnvelopeShanghai {
784    /// Submitted execution payload.
785    pub payload: ExecutionPayloadShanghai,
786}
787
788/// Cancun payload-submission request.
789#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
790pub struct ExecutionPayloadEnvelopeCancun {
791    /// Submitted execution payload.
792    pub payload: ExecutionPayloadCancun,
793    /// Root of the parent beacon block.
794    pub parent_beacon_block_root: B256,
795}
796
797/// Prague payload-submission request.
798#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
799pub struct ExecutionPayloadEnvelopePrague {
800    /// Submitted execution payload.
801    pub payload: ExecutionPayloadPrague,
802    /// Root of the parent beacon block.
803    pub parent_beacon_block_root: B256,
804    /// EIP-7685 execution requests.
805    pub execution_requests: Requests,
806}
807
808/// Osaka payload-submission request.
809#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
810pub struct ExecutionPayloadEnvelopeOsaka {
811    /// Submitted execution payload.
812    pub payload: ExecutionPayloadOsaka,
813    /// Root of the parent beacon block.
814    pub parent_beacon_block_root: B256,
815    /// EIP-7685 execution requests.
816    pub execution_requests: Requests,
817}
818
819/// Amsterdam payload-submission request.
820#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
821pub struct ExecutionPayloadEnvelopeAmsterdam {
822    /// Submitted execution payload.
823    pub payload: ExecutionPayloadAmsterdam,
824    /// Root of the parent beacon block.
825    pub parent_beacon_block_root: B256,
826    /// EIP-7685 execution requests.
827    pub execution_requests: Requests,
828}
829
830impl From<ExecutionPayloadParis> for ExecutionPayloadEnvelopeParis {
831    fn from(payload: ExecutionPayloadParis) -> Self {
832        Self { payload }
833    }
834}
835
836impl From<ExecutionPayloadShanghai> for ExecutionPayloadEnvelopeShanghai {
837    fn from(payload: ExecutionPayloadShanghai) -> Self {
838        Self { payload }
839    }
840}
841
842impl From<(ExecutionPayloadCancun, B256)> for ExecutionPayloadEnvelopeCancun {
843    fn from((payload, parent_beacon_block_root): (ExecutionPayloadCancun, B256)) -> Self {
844        Self { payload, parent_beacon_block_root }
845    }
846}
847
848impl From<(ExecutionPayloadPrague, B256, Requests)> for ExecutionPayloadEnvelopePrague {
849    fn from(
850        (payload, parent_beacon_block_root, execution_requests): (
851            ExecutionPayloadPrague,
852            B256,
853            Requests,
854        ),
855    ) -> Self {
856        Self { payload, parent_beacon_block_root, execution_requests }
857    }
858}
859
860impl From<(ExecutionPayloadOsaka, B256, Requests)> for ExecutionPayloadEnvelopeOsaka {
861    fn from(
862        (payload, parent_beacon_block_root, execution_requests): (
863            ExecutionPayloadOsaka,
864            B256,
865            Requests,
866        ),
867    ) -> Self {
868        Self { payload, parent_beacon_block_root, execution_requests }
869    }
870}
871
872impl From<(ExecutionPayloadAmsterdam, B256, Requests)> for ExecutionPayloadEnvelopeAmsterdam {
873    fn from(
874        (payload, parent_beacon_block_root, execution_requests): (
875            ExecutionPayloadAmsterdam,
876            B256,
877            Requests,
878        ),
879    ) -> Self {
880        Self { payload, parent_beacon_block_root, execution_requests }
881    }
882}
883
884/// Paris forkchoice-update request.
885#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
886pub struct ForkchoiceUpdateParis {
887    /// Current forkchoice state.
888    pub forkchoice_state: ForkchoiceState,
889    /// Optional Paris payload attributes.
890    pub payload_attributes: Optional<PayloadAttributesParis>,
891}
892
893/// Shanghai forkchoice-update request.
894#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
895pub struct ForkchoiceUpdateShanghai {
896    /// Current forkchoice state.
897    pub forkchoice_state: ForkchoiceState,
898    /// Optional Shanghai payload attributes.
899    pub payload_attributes: Optional<PayloadAttributesShanghai>,
900}
901
902/// Cancun forkchoice-update request.
903#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
904pub struct ForkchoiceUpdateCancun {
905    /// Current forkchoice state.
906    pub forkchoice_state: ForkchoiceState,
907    /// Optional Cancun payload attributes.
908    pub payload_attributes: Optional<PayloadAttributesCancun>,
909}
910
911/// Prague forkchoice-update request.
912#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
913pub struct ForkchoiceUpdatePrague {
914    /// Current forkchoice state.
915    pub forkchoice_state: ForkchoiceState,
916    /// Optional Prague payload attributes.
917    pub payload_attributes: Optional<PayloadAttributesPrague>,
918}
919
920/// Osaka forkchoice-update request.
921#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
922pub struct ForkchoiceUpdateOsaka {
923    /// Current forkchoice state.
924    pub forkchoice_state: ForkchoiceState,
925    /// Optional Osaka payload attributes.
926    pub payload_attributes: Optional<PayloadAttributesOsaka>,
927}
928
929/// Amsterdam forkchoice-update request.
930#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
931pub struct ForkchoiceUpdateAmsterdam {
932    /// Current forkchoice state.
933    pub forkchoice_state: ForkchoiceState,
934    /// Optional Amsterdam payload attributes.
935    pub payload_attributes: Optional<PayloadAttributesAmsterdam>,
936    /// Optional `Bitvector[128]` custody-column selection.
937    pub custody_columns: Optional<B128>,
938}
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943    use alloy_eips::eip4895::Withdrawal;
944    use alloy_primitives::{Address, Bloom, Bytes};
945    use ssz::{Decode, Encode};
946
947    fn payload_v1() -> ExecutionPayloadV1 {
948        ExecutionPayloadV1 {
949            parent_hash: B256::repeat_byte(1),
950            fee_recipient: Address::repeat_byte(2),
951            state_root: B256::repeat_byte(3),
952            receipts_root: B256::repeat_byte(4),
953            logs_bloom: Bloom::repeat_byte(5),
954            prev_randao: B256::repeat_byte(6),
955            block_number: 7,
956            gas_limit: 8,
957            gas_used: 9,
958            timestamp: 10,
959            extra_data: Bytes::from_static(&[11, 12]),
960            base_fee_per_gas: U256::from(13),
961            block_hash: B256::repeat_byte(14),
962            transactions: vec![Bytes::from_static(&[15, 16])],
963        }
964    }
965
966    fn payload_v2() -> ExecutionPayloadV2 {
967        ExecutionPayloadV2 { payload_inner: payload_v1(), withdrawals: vec![Withdrawal::default()] }
968    }
969
970    fn payload_v3() -> ExecutionPayloadV3 {
971        ExecutionPayloadV3 { payload_inner: payload_v2(), blob_gas_used: 17, excess_blob_gas: 18 }
972    }
973
974    fn payload_v4() -> ExecutionPayloadV4 {
975        ExecutionPayloadV4 {
976            payload_inner: payload_v3(),
977            block_access_list: Bytes::from_static(&[19, 20]),
978            slot_number: 21,
979        }
980    }
981
982    fn attributes_cancun() -> PayloadAttributesCancun {
983        PayloadAttributesCancun {
984            timestamp: 1,
985            prev_randao: B256::repeat_byte(2),
986            suggested_fee_recipient: Address::repeat_byte(3),
987            withdrawals: vec![Withdrawal::default()],
988            parent_beacon_block_root: B256::repeat_byte(4),
989        }
990    }
991
992    fn state() -> ForkchoiceState {
993        ForkchoiceState {
994            head_block_hash: B256::repeat_byte(1),
995            safe_block_hash: B256::repeat_byte(2),
996            finalized_block_hash: B256::repeat_byte(3),
997        }
998    }
999
1000    fn assert_roundtrip<T>(value: &T)
1001    where
1002        T: Encode + Decode + PartialEq + core::fmt::Debug,
1003    {
1004        assert_eq!(T::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), *value);
1005    }
1006
1007    #[test]
1008    fn execution_payload_envelopes_roundtrip() {
1009        assert_roundtrip(&ExecutionPayloadEnvelopeParis { payload: payload_v1() });
1010        assert_roundtrip(&ExecutionPayloadEnvelopeShanghai { payload: payload_v2() });
1011        assert_roundtrip(&ExecutionPayloadEnvelopeCancun {
1012            payload: payload_v3(),
1013            parent_beacon_block_root: B256::repeat_byte(1),
1014        });
1015        assert_roundtrip(&ExecutionPayloadEnvelopePrague {
1016            payload: payload_v3(),
1017            parent_beacon_block_root: B256::repeat_byte(1),
1018            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1019        });
1020        assert_roundtrip(&ExecutionPayloadEnvelopeOsaka {
1021            payload: payload_v3(),
1022            parent_beacon_block_root: B256::repeat_byte(1),
1023            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1024        });
1025        assert_roundtrip(&ExecutionPayloadEnvelopeAmsterdam {
1026            payload: payload_v4(),
1027            parent_beacon_block_root: B256::repeat_byte(1),
1028            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1029        });
1030    }
1031
1032    #[test]
1033    fn paris_submission_is_a_single_field_container() {
1034        let payload = payload_v1();
1035        let payload_bytes = payload.as_ssz_bytes();
1036        let envelope = ExecutionPayloadEnvelopeParis { payload };
1037        let encoded = envelope.as_ssz_bytes();
1038        assert_eq!(&encoded[..4], &4u32.to_le_bytes());
1039        assert_eq!(&encoded[4..], payload_bytes);
1040    }
1041
1042    #[test]
1043    fn built_payloads_roundtrip() {
1044        assert_roundtrip(&BuiltPayloadParis { payload: payload_v1(), block_value: U256::from(1) });
1045        assert_roundtrip(&BuiltPayloadShanghai {
1046            payload: payload_v2(),
1047            block_value: U256::from(1),
1048        });
1049        assert_roundtrip(&BuiltPayloadCancun {
1050            execution_payload: payload_v3(),
1051            block_value: U256::from(1),
1052            blobs_bundle: BlobsBundleV1::empty(),
1053            should_override_builder: true,
1054        });
1055        assert_roundtrip(&BuiltPayloadPrague {
1056            payload: payload_v3(),
1057            block_value: U256::from(1),
1058            blobs_bundle: BlobsBundleV1::empty(),
1059            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1060            should_override_builder: true,
1061        });
1062        assert_roundtrip(&BuiltPayloadOsaka {
1063            payload: payload_v3(),
1064            block_value: U256::from(1),
1065            blobs_bundle: BlobsBundleV2::empty(),
1066            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1067            should_override_builder: true,
1068        });
1069        assert_roundtrip(&BuiltPayloadAmsterdam {
1070            payload: payload_v4(),
1071            block_value: U256::from(1),
1072            blobs_bundle: BlobsBundleV2::empty(),
1073            execution_requests: Requests::from_requests([Bytes::from_static(&[2, 3])]),
1074            should_override_builder: true,
1075        });
1076    }
1077
1078    #[test]
1079    fn shanghai_built_payload_has_no_builder_override() {
1080        let payload = payload_v2();
1081        let payload_len = payload.ssz_bytes_len();
1082        let value = BuiltPayloadShanghai { payload, block_value: U256::from(1) };
1083        let encoded = value.as_ssz_bytes();
1084
1085        assert_eq!(&encoded[..4], &36u32.to_le_bytes());
1086        assert_eq!(encoded.len(), 36 + payload_len);
1087    }
1088
1089    #[test]
1090    fn legacy_built_payload_conversions_preserve_fields() {
1091        let shanghai = BuiltPayloadShanghai { payload: payload_v2(), block_value: U256::from(1) };
1092        let legacy = LegacyBuiltPayloadShanghai::from(shanghai.clone());
1093        assert_eq!(BuiltPayloadShanghai::try_from(legacy).unwrap(), shanghai);
1094
1095        let prague = BuiltPayloadPrague {
1096            payload: payload_v3(),
1097            block_value: U256::from(2),
1098            blobs_bundle: BlobsBundleV1::empty(),
1099            execution_requests: Requests::from_requests([Bytes::from_static(&[3, 4])]),
1100            should_override_builder: true,
1101        };
1102        let legacy = LegacyBuiltPayloadPrague::from(prague.clone());
1103        assert_eq!(BuiltPayloadPrague::from(legacy), prague);
1104
1105        let osaka = BuiltPayloadOsaka {
1106            payload: payload_v3(),
1107            block_value: U256::from(5),
1108            blobs_bundle: BlobsBundleV2::empty(),
1109            execution_requests: Requests::from_requests([Bytes::from_static(&[6, 7])]),
1110            should_override_builder: true,
1111        };
1112        let legacy = LegacyBuiltPayloadOsaka::from(osaka.clone());
1113        assert_eq!(BuiltPayloadOsaka::from(legacy), osaka);
1114
1115        let amsterdam = BuiltPayloadAmsterdam {
1116            payload: payload_v4(),
1117            block_value: U256::from(8),
1118            blobs_bundle: BlobsBundleV2::empty(),
1119            execution_requests: Requests::from_requests([Bytes::from_static(&[9, 10])]),
1120            should_override_builder: true,
1121        };
1122        let legacy = LegacyBuiltPayloadAmsterdam::from(amsterdam.clone());
1123        assert_eq!(BuiltPayloadAmsterdam::from(legacy), amsterdam);
1124    }
1125
1126    #[test]
1127    fn legacy_shanghai_built_payload_rejects_paris_payload() {
1128        let legacy = LegacyBuiltPayloadShanghai {
1129            execution_payload: ExecutionPayloadFieldV2::V1(payload_v1()),
1130            block_value: U256::from(1),
1131        };
1132        assert_eq!(
1133            BuiltPayloadShanghai::try_from(legacy),
1134            Err(BuiltPayloadConversionError::UnexpectedPayloadFork("Paris"))
1135        );
1136    }
1137
1138    #[test]
1139    fn prague_requests_precede_should_override_builder() {
1140        let value = BuiltPayloadPrague {
1141            payload: payload_v3(),
1142            block_value: U256::from(1),
1143            blobs_bundle: BlobsBundleV1::empty(),
1144            execution_requests: Requests::from_requests([Bytes::from_static(&[0xaa, 0xbb])]),
1145            should_override_builder: true,
1146        };
1147        let encoded = value.as_ssz_bytes();
1148        assert_eq!(encoded[48], 1);
1149        assert_eq!(&encoded[encoded.len() - 2..], &[0xaa, 0xbb]);
1150    }
1151
1152    #[test]
1153    fn forkchoice_updates_roundtrip() {
1154        let paris = PayloadAttributesParis {
1155            timestamp: 1,
1156            prev_randao: B256::repeat_byte(2),
1157            suggested_fee_recipient: Address::repeat_byte(3),
1158        };
1159        let shanghai = PayloadAttributesShanghai {
1160            timestamp: 1,
1161            prev_randao: B256::repeat_byte(2),
1162            suggested_fee_recipient: Address::repeat_byte(3),
1163            withdrawals: vec![Withdrawal::default()],
1164        };
1165        let amsterdam = PayloadAttributesAmsterdam {
1166            timestamp: 1,
1167            prev_randao: B256::repeat_byte(2),
1168            suggested_fee_recipient: Address::repeat_byte(3),
1169            withdrawals: vec![Withdrawal::default()],
1170            parent_beacon_block_root: B256::repeat_byte(4),
1171            slot_number: 5,
1172            target_gas_limit: 6,
1173        };
1174        assert_roundtrip(&ForkchoiceUpdateParis {
1175            forkchoice_state: state(),
1176            payload_attributes: Optional::some(paris),
1177        });
1178        assert_roundtrip(&ForkchoiceUpdateShanghai {
1179            forkchoice_state: state(),
1180            payload_attributes: Optional::some(shanghai),
1181        });
1182        assert_roundtrip(&ForkchoiceUpdateCancun {
1183            forkchoice_state: state(),
1184            payload_attributes: Optional::some(attributes_cancun()),
1185        });
1186        assert_roundtrip(&ForkchoiceUpdatePrague {
1187            forkchoice_state: state(),
1188            payload_attributes: Optional::some(attributes_cancun()),
1189        });
1190        assert_roundtrip(&ForkchoiceUpdateOsaka {
1191            forkchoice_state: state(),
1192            payload_attributes: Optional::some(attributes_cancun()),
1193        });
1194        assert_roundtrip(&ForkchoiceUpdateAmsterdam {
1195            forkchoice_state: state(),
1196            payload_attributes: Optional::some(amsterdam),
1197            custody_columns: Optional::some(B128::repeat_byte(0xa5)),
1198        });
1199    }
1200
1201    #[test]
1202    fn payload_attributes_legacy_conversions_preserve_fork_shape() {
1203        let cancun = attributes_cancun();
1204        let legacy = LegacyPayloadAttributes::from(cancun.clone());
1205        assert_eq!(PayloadAttributesCancun::try_from(legacy).unwrap(), cancun);
1206
1207        let amsterdam = PayloadAttributesAmsterdam {
1208            timestamp: 1,
1209            prev_randao: B256::repeat_byte(2),
1210            suggested_fee_recipient: Address::repeat_byte(3),
1211            withdrawals: vec![Withdrawal::default()],
1212            parent_beacon_block_root: B256::repeat_byte(4),
1213            slot_number: 5,
1214            target_gas_limit: 6,
1215        };
1216        let legacy = LegacyPayloadAttributes::from(amsterdam.clone());
1217        assert_eq!(PayloadAttributesAmsterdam::try_from(legacy).unwrap(), amsterdam);
1218    }
1219
1220    #[test]
1221    fn payload_attributes_legacy_conversions_reject_loss() {
1222        let mut legacy = LegacyPayloadAttributes::default();
1223        assert_eq!(
1224            PayloadAttributesShanghai::try_from(legacy.clone()),
1225            Err(PayloadAttributesConversionError::MissingField("withdrawals"))
1226        );
1227
1228        legacy.withdrawals = Some(vec![]);
1229        legacy.parent_beacon_block_root = Some(B256::ZERO);
1230        assert_eq!(
1231            PayloadAttributesShanghai::try_from(legacy),
1232            Err(PayloadAttributesConversionError::UnexpectedField("parent_beacon_block_root"))
1233        );
1234    }
1235
1236    #[test]
1237    fn every_payload_status_roundtrips() {
1238        for status in [
1239            PayloadStatusEnum::Valid,
1240            PayloadStatusEnum::Invalid { validation_error: "invalid".into() },
1241            PayloadStatusEnum::Syncing,
1242            PayloadStatusEnum::Accepted,
1243        ] {
1244            let validation_error = legacy_validation_error(&status).unwrap();
1245            let value = PayloadStatus {
1246                status,
1247                latest_valid_hash: Optional::some(B256::ZERO),
1248                validation_error,
1249            };
1250            assert_eq!(PayloadStatus::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), value);
1251        }
1252    }
1253
1254    #[test]
1255    fn payload_status_preserves_absent_invalid_validation_error() {
1256        let mut bytes = Vec::new();
1257        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1258        encoder.append(&1u8);
1259        encoder.append(&Optional::<B256>::none());
1260        encoder.append(&Optional::<ErrorBytes>::none());
1261        encoder.finalize();
1262
1263        let decoded = PayloadStatus::from_ssz_bytes(&bytes).unwrap();
1264        assert!(decoded.validation_error.is_none());
1265        assert_eq!(decoded.as_ssz_bytes(), bytes);
1266    }
1267
1268    #[test]
1269    fn payload_status_rejects_non_invalid_validation_error() {
1270        let mut bytes = Vec::new();
1271        let mut encoder = ssz::SszEncoder::container(&mut bytes, 9);
1272        encoder.append(&0u8);
1273        encoder.append(&Optional::<B256>::none());
1274        encoder.append(&Optional::some(Vec::<u8>::new()));
1275        encoder.finalize();
1276        assert!(PayloadStatus::from_ssz_bytes(&bytes).is_err());
1277    }
1278
1279    #[test]
1280    fn payload_status_legacy_conversion_rejects_oversized_error() {
1281        assert!(PayloadStatus::try_from(LegacyPayloadStatus {
1282            status: PayloadStatusEnum::Invalid { validation_error: "x".repeat(1025) },
1283            latest_valid_hash: None,
1284        })
1285        .is_err());
1286    }
1287
1288    #[test]
1289    fn forkchoice_response_distinguishes_absent_and_zero_payload_id() {
1290        let status = PayloadStatus {
1291            status: PayloadStatusEnum::Valid,
1292            latest_valid_hash: Optional::none(),
1293            validation_error: Optional::none(),
1294        };
1295        let none = ForkchoiceUpdateResponse {
1296            payload_status: status.clone(),
1297            payload_id: Optional::none(),
1298        };
1299        let zero = ForkchoiceUpdateResponse {
1300            payload_status: status,
1301            payload_id: Optional::some(PayloadId::default()),
1302        };
1303
1304        assert_ne!(none.as_ssz_bytes(), zero.as_ssz_bytes());
1305        assert_roundtrip(&none);
1306        assert_roundtrip(&zero);
1307    }
1308
1309    #[test]
1310    fn forkchoice_conversion_rejects_accepted() {
1311        let legacy = LegacyForkchoice::from_status(PayloadStatusEnum::Accepted);
1312        assert_eq!(
1313            ForkchoiceUpdateResponse::try_from(legacy),
1314            Err(ConversionError::AcceptedForkchoice)
1315        );
1316    }
1317
1318    #[test]
1319    fn optional_rejects_more_than_one_value() {
1320        assert!(Optional::<B256>::from_ssz_bytes(&[0; 64]).is_err());
1321    }
1322}