Skip to main content

reth_payload_primitives/
lib.rs

1//! Abstractions for working with execution payloads.
2//!
3//! This crate provides types and traits for execution and building payloads.
4
5#![doc(
6    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
7    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
8    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
9)]
10#![cfg_attr(not(test), warn(unused_crate_dependencies))]
11#![cfg_attr(docsrs, feature(doc_cfg))]
12#![cfg_attr(not(feature = "std"), no_std)]
13
14extern crate alloc;
15
16use alloy_primitives::Bytes;
17use reth_chainspec::EthereumHardforks;
18use reth_primitives_traits::{NodePrimitives, SealedBlock};
19
20mod error;
21pub use error::{
22    EngineObjectValidationError, InvalidPayloadAttributesError, NewPayloadError,
23    PayloadBuilderError, VersionSpecificValidationError,
24};
25
26mod traits;
27pub use traits::{
28    payload_id, BuildNextEnv, BuiltPayload, BuiltPayloadExecutedBlock, PayloadAttributes,
29    PayloadAttributesBuilder,
30};
31
32mod payload;
33pub use payload::{ExecutionPayload, PayloadOrAttributes};
34
35/// Core trait that defines the associated types for working with execution payloads.
36pub trait PayloadTypes: Send + Sync + Unpin + core::fmt::Debug + Clone + 'static {
37    /// The format for execution payload data that can be processed and validated.
38    ///
39    /// This type represents the canonical format for block data that includes
40    /// all necessary information for execution and validation.
41    type ExecutionData: ExecutionPayload + From<Self::BuiltPayload>;
42    /// The type representing a successfully built payload/block.
43    type BuiltPayload: BuiltPayload + Clone + Unpin;
44
45    /// Attributes that specify how a payload should be constructed.
46    ///
47    /// These attributes typically come from external sources (e.g., consensus layer over RPC such
48    /// as the Engine API) and contain parameters like timestamp, fee recipient, and randomness.
49    type PayloadAttributes: PayloadAttributes + Unpin;
50
51    /// Converts a sealed block into the execution payload format.
52    fn block_to_payload(
53        block: SealedBlock<
54            <<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
55        >,
56        bal: Option<Bytes>,
57    ) -> Self::ExecutionData;
58}
59
60/// Validates the timestamp depending on the version called:
61///
62/// * If V2, this ensures that the payload timestamp is pre-Cancun.
63/// * If V3, this ensures that the payload timestamp is within the Cancun timestamp.
64/// * If V4, this ensures that the payload timestamp is within the Prague timestamp, and within the
65///   Amsterdam timestamp for `engine_forkchoiceUpdatedV4` payload attributes.
66/// * If V5, this ensures that the payload timestamp is within the Osaka timestamp, within the
67///   Amsterdam timestamp for `engine_newPayloadV5` payloads, and within the Bogota timestamp for
68///   `engine_forkchoiceUpdatedV5` payload attributes.
69/// * If V6, this ensures that the payload timestamp is within the Amsterdam timestamp, and within
70///   the Bogota timestamp for `engine_newPayloadV6` payloads.
71///
72/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload and that
73/// staggered endpoint upgrades reject the next fork once a newer method version is required.
74///
75/// Otherwise, this will return [`EngineObjectValidationError::UnsupportedFork`].
76pub fn validate_payload_timestamp(
77    chain_spec: impl EthereumHardforks,
78    version: EngineApiMessageVersion,
79    timestamp: u64,
80    kind: MessageValidationKind,
81) -> Result<(), EngineObjectValidationError> {
82    let is_cancun = chain_spec.is_cancun_active_at_timestamp(timestamp);
83    if version.is_v2() && is_cancun {
84        // From the Engine API spec:
85        //
86        // ### Update the methods of previous forks
87        //
88        // This document defines how Cancun payload should be handled by the [`Shanghai
89        // API`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md).
90        //
91        // For the following methods:
92        //
93        // - [`engine_forkchoiceUpdatedV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_forkchoiceupdatedv2)
94        // - [`engine_newPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_newpayloadV2)
95        // - [`engine_getPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_getpayloadv2)
96        //
97        // a validation **MUST** be added:
98        //
99        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
100        //    payload or payloadAttributes is greater or equal to the Cancun activation timestamp.
101        return Err(EngineObjectValidationError::UnsupportedFork)
102    }
103
104    if version.is_v3() && !is_cancun {
105        // From the Engine API spec:
106        // <https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/cancun.md#specification-2>
107        //
108        // For `engine_getPayloadV3`:
109        //
110        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
111        //    the built payload does not fall within the time frame of the Cancun fork.
112        //
113        // For `engine_forkchoiceUpdatedV3`:
114        //
115        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
116        //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
117        //    the time frame of the Cancun fork.
118        //
119        // For `engine_newPayloadV3`:
120        //
121        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
122        //    the payload does not fall within the time frame of the Cancun fork.
123        return Err(EngineObjectValidationError::UnsupportedFork)
124    }
125
126    let is_prague = chain_spec.is_prague_active_at_timestamp(timestamp);
127    if version.is_v4() && !is_prague {
128        // From the Engine API spec:
129        // <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#specification-1>
130        //
131        // For `engine_getPayloadV4`:
132        //
133        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
134        //    the built payload does not fall within the time frame of the Prague fork.
135        //
136        // For `engine_forkchoiceUpdatedV4`:
137        //
138        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
139        //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
140        //    the time frame of the Prague fork.
141        //
142        // For `engine_newPayloadV4`:
143        //
144        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
145        //    the payload does not fall within the time frame of the Prague fork.
146        return Err(EngineObjectValidationError::UnsupportedFork)
147    }
148
149    let is_osaka = chain_spec.is_osaka_active_at_timestamp(timestamp);
150    if version.is_v5() && !is_osaka {
151        // From the Engine API spec:
152        // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
153        //
154        // For `engine_getPayloadV5`
155        //
156        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
157        //    built payload does not fall within the time frame of the Osaka fork.
158        return Err(EngineObjectValidationError::UnsupportedFork)
159    }
160
161    let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
162
163    if !is_amsterdam &&
164        matches!(
165            (version, kind),
166            (EngineApiMessageVersion::V4, MessageValidationKind::PayloadAttributes) |
167                (EngineApiMessageVersion::V5, MessageValidationKind::Payload)
168        )
169    {
170        // From the Engine API spec:
171        // <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md>
172        //
173        // For `engine_newPayloadV5`:
174        //
175        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
176        //    payload does not fall within the time frame of the Amsterdam fork.
177        //
178        // For `engine_forkchoiceUpdatedV4`:
179        //
180        // 2. payloadAttributes.timestamp does not fall within the time frame of the Amsterdam fork,
181        //    return -38005: Unsupported fork on failure.
182        //
183        // `engine_getPayloadV5` is the Osaka method and is not subject to this rule.
184        return Err(EngineObjectValidationError::UnsupportedFork)
185    }
186
187    // Staggered endpoint upgrades must reject Amsterdam payloads until the Amsterdam-specific
188    // method version is used.
189    if is_amsterdam &&
190        matches!(
191            (version, kind),
192            (EngineApiMessageVersion::V3, MessageValidationKind::PayloadAttributes) |
193                (EngineApiMessageVersion::V4, MessageValidationKind::Payload) |
194                (EngineApiMessageVersion::V5, MessageValidationKind::GetPayload)
195        )
196    {
197        return Err(EngineObjectValidationError::UnsupportedFork)
198    }
199
200    // `engine_getPayloadV4` MUST reject payloads with a timestamp >= Osaka.
201    if version.is_v4() && kind == MessageValidationKind::GetPayload && is_osaka {
202        return Err(EngineObjectValidationError::UnsupportedFork)
203    }
204
205    if version.is_v6() && !is_amsterdam {
206        // From the Engine API spec:
207        // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
208        //
209        // For `engine_getPayloadV6`
210        //
211        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
212        //    built payload does not fall within the time frame of the Amsterdam fork.
213
214        return Err(EngineObjectValidationError::UnsupportedFork)
215    }
216
217    let is_bogota = chain_spec.is_bogota_active_at_timestamp(timestamp);
218
219    // Staggered endpoint upgrades must reject Bogota payloads until the Bogota-specific method
220    // version is used.
221    //
222    // From the Engine API spec:
223    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#update-the-methods-of-previous-forks>
224    //
225    // For `engine_newPayloadV5` and `engine_forkchoiceUpdatedV4`:
226    //
227    // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of payload is
228    //    greater than or equal to the Bogota activation timestamp.
229    if is_bogota &&
230        matches!(
231            (version, kind),
232            (EngineApiMessageVersion::V4, MessageValidationKind::PayloadAttributes) |
233                (EngineApiMessageVersion::V5, MessageValidationKind::Payload)
234        )
235    {
236        return Err(EngineObjectValidationError::UnsupportedFork)
237    }
238
239    if !is_bogota &&
240        matches!(
241            (version, kind),
242            (EngineApiMessageVersion::V5, MessageValidationKind::PayloadAttributes) |
243                (EngineApiMessageVersion::V6, MessageValidationKind::Payload)
244        )
245    {
246        // From the Engine API spec:
247        // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md>
248        //
249        // For `engine_newPayloadV6`:
250        //
251        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
252        //    payload does not fall within the time frame of the Bogota fork.
253        //
254        // For `engine_forkchoiceUpdatedV5`:
255        //
256        // 2. Client software MUST return -38005: Unsupported fork error if the payloadAttributes is
257        //    set and the payloadAttributes.timestamp does not fall within the time frame of the
258        //    Bogota fork.
259        return Err(EngineObjectValidationError::UnsupportedFork)
260    }
261
262    Ok(())
263}
264
265/// Validates the presence of the `block access list` field for the engine method version.
266///
267/// `engine_newPayloadV5` and `engine_newPayloadV6` take an `ExecutionPayloadV4`, so the field must
268/// be [Some]; every other method must leave it [None]. Presence does not depend on the payload
269/// timestamp, so a well-formed payload outside the method's fork reaches the fork check in
270/// [`validate_payload_timestamp`] and fails with `-38005: Unsupported fork`.
271pub fn validate_block_access_list_presence(
272    version: EngineApiMessageVersion,
273    message_validation_kind: MessageValidationKind,
274    has_block_access_list: bool,
275) -> Result<(), EngineObjectValidationError> {
276    match version {
277        EngineApiMessageVersion::V1 |
278        EngineApiMessageVersion::V2 |
279        EngineApiMessageVersion::V3 |
280        EngineApiMessageVersion::V4 => {
281            if has_block_access_list {
282                return Err(message_validation_kind
283                    .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
284            }
285        }
286
287        EngineApiMessageVersion::V5 => {
288            if message_validation_kind == MessageValidationKind::Payload {
289                if !has_block_access_list {
290                    return Err(message_validation_kind
291                        .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
292                }
293            } else if has_block_access_list {
294                return Err(message_validation_kind
295                    .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
296            }
297        }
298
299        EngineApiMessageVersion::V6 => {
300            if !has_block_access_list {
301                return Err(message_validation_kind
302                    .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
303            }
304        }
305    };
306
307    Ok(())
308}
309
310/// Validates the presence of the `slot number` field for the engine method version.
311///
312/// `engine_newPayloadV5`, `engine_newPayloadV6`, `engine_forkchoiceUpdatedV4` and
313/// `engine_forkchoiceUpdatedV5` carry the field, so it must be [Some] for them; every other method
314/// must leave it [None]. Presence does not depend on the payload timestamp, so a well-formed
315/// request outside the method's fork reaches the fork check in [`validate_payload_timestamp`] and
316/// fails with `-38005: Unsupported fork`.
317pub fn validate_slot_number_presence(
318    version: EngineApiMessageVersion,
319    message_validation_kind: MessageValidationKind,
320    has_slot_number: bool,
321) -> Result<(), EngineObjectValidationError> {
322    match version {
323        EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => {
324            if has_slot_number {
325                return Err(message_validation_kind
326                    .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
327            }
328        }
329
330        EngineApiMessageVersion::V4 => {
331            if message_validation_kind == MessageValidationKind::PayloadAttributes {
332                if !has_slot_number {
333                    return Err(message_validation_kind
334                        .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
335                }
336            } else if has_slot_number {
337                return Err(message_validation_kind
338                    .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
339            }
340        }
341
342        EngineApiMessageVersion::V5 => {
343            if matches!(
344                message_validation_kind,
345                MessageValidationKind::Payload | MessageValidationKind::PayloadAttributes
346            ) {
347                if !has_slot_number {
348                    return Err(message_validation_kind
349                        .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
350                }
351            } else if has_slot_number {
352                return Err(message_validation_kind
353                    .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
354            }
355        }
356
357        EngineApiMessageVersion::V6 => {
358            if !has_slot_number {
359                return Err(message_validation_kind
360                    .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
361            }
362        }
363    };
364
365    Ok(())
366}
367
368/// Validates the presence of `withdrawals` for the engine method version.
369///
370/// V1 forbids withdrawals. V2 supports both Paris and Shanghai payloads, so withdrawals must be
371/// present exactly when Shanghai is active. V3 and later require withdrawals regardless of the
372/// timestamp, allowing complete requests outside the method's fork to reach the timeframe check.
373pub fn validate_withdrawals_presence<T: EthereumHardforks>(
374    chain_spec: &T,
375    version: EngineApiMessageVersion,
376    message_validation_kind: MessageValidationKind,
377    timestamp: u64,
378    has_withdrawals: bool,
379) -> Result<(), EngineObjectValidationError> {
380    match version {
381        EngineApiMessageVersion::V1 => {
382            if has_withdrawals {
383                return Err(message_validation_kind
384                    .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1))
385            }
386        }
387        EngineApiMessageVersion::V2 => {
388            let is_shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
389            if is_shanghai_active && !has_withdrawals {
390                return Err(message_validation_kind
391                    .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
392            }
393            if !is_shanghai_active && has_withdrawals {
394                return Err(message_validation_kind
395                    .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai))
396            }
397        }
398        EngineApiMessageVersion::V3 |
399        EngineApiMessageVersion::V4 |
400        EngineApiMessageVersion::V5 |
401        EngineApiMessageVersion::V6 => {
402            if !has_withdrawals {
403                return Err(message_validation_kind
404                    .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
405            }
406        }
407    };
408
409    Ok(())
410}
411
412/// Validate the presence of the `parentBeaconBlockRoot` field according to the given timestamp.
413/// This method is meant to be used with either a `payloadAttributes` field or a full payload, with
414/// the `engine_forkchoiceUpdated` and `engine_newPayload` methods respectively.
415///
416/// After Cancun, the `parentBeaconBlockRoot` field must be [Some].
417/// Before Cancun, the `parentBeaconBlockRoot` field must be [None].
418///
419/// If the engine API message version is V1 or V2, and the timestamp is post-Cancun, then this will
420/// return [`EngineObjectValidationError::UnsupportedFork`].
421///
422/// If the timestamp is before the Cancun fork and the engine API message version is V3, then this
423/// will return [`EngineObjectValidationError::UnsupportedFork`].
424///
425/// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then
426/// this will return [`VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun`].
427///
428/// This implements the following Engine API spec rules:
429///
430/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
431///    matches the expected one and return `-32602: Invalid params` error if this check fails. Any
432///    field having `null` value **MUST** be considered as not provided.
433///
434/// For `engine_forkchoiceUpdatedV3`:
435///
436/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
437///    matches the expected one and return `-32602: Invalid params` error if this check fails. Any
438///    field having `null` value **MUST** be considered as not provided.
439///
440/// 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the following
441///    sequence of checks that **MUST** be run over `payloadAttributes`:
442///     1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003: Invalid
443///        payload attributes` on failure.
444///     2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
445///        `-38005: Unsupported fork` on failure.
446///     3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
447///        `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on failure.
448///     4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
449///        back.
450///
451/// For `engine_newPayloadV3`:
452///
453/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
454///    payload does not fall within the time frame of the Cancun fork.
455///
456/// For `engine_newPayloadV4`:
457///
458/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
459///    payload does not fall within the time frame of the Prague fork.
460///
461/// Returning the right error code (ie, if the client should return `-38003: Invalid payload
462/// attributes` is handled by the `message_validation_kind` parameter. If the parameter is
463/// `MessageValidationKind::Payload`, then the error code will be `-32602: Invalid params`. If the
464/// parameter is `MessageValidationKind::PayloadAttributes`, then the error code will be `-38003:
465/// Invalid payload attributes`.
466pub fn validate_parent_beacon_block_root_presence<T: EthereumHardforks>(
467    chain_spec: &T,
468    version: EngineApiMessageVersion,
469    validation_kind: MessageValidationKind,
470    timestamp: u64,
471    has_parent_beacon_block_root: bool,
472) -> Result<(), EngineObjectValidationError> {
473    // 1. Client software **MUST** check that provided set of parameters and their fields strictly
474    //    matches the expected one and return `-32602: Invalid params` error if this check fails.
475    //    Any field having `null` value **MUST** be considered as not provided.
476    //
477    // For `engine_forkchoiceUpdatedV3`:
478    //
479    // 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the
480    //    following sequence of checks that **MUST** be run over `payloadAttributes`:
481    //     1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003:
482    //        Invalid payload attributes` on failure.
483    //     2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
484    //        `-38005: Unsupported fork` on failure.
485    //     3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
486    //        `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on
487    //        failure.
488    //     4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
489    //        back.
490    match version {
491        EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => {
492            if has_parent_beacon_block_root {
493                return Err(validation_kind.to_error(
494                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
495                ))
496            }
497        }
498        EngineApiMessageVersion::V3 |
499        EngineApiMessageVersion::V4 |
500        EngineApiMessageVersion::V5 |
501        EngineApiMessageVersion::V6 => {
502            if !has_parent_beacon_block_root {
503                return Err(validation_kind
504                    .to_error(VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun))
505            }
506        }
507    };
508
509    // For `engine_forkchoiceUpdatedV3`:
510    //
511    // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
512    //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the
513    //    time frame of the Cancun fork.
514    //
515    // For `engine_newPayloadV3`:
516    //
517    // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
518    //    payload does not fall within the time frame of the Cancun fork.
519    validate_payload_timestamp(chain_spec, version, timestamp, validation_kind)?;
520
521    Ok(())
522}
523
524/// A type that represents whether or not we are validating a payload or payload attributes.
525///
526/// This is used to ensure that the correct error code is returned when validating the payload or
527/// payload attributes.
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum MessageValidationKind {
530    /// We are validating fields of a payload attributes.
531    /// This corresponds to `engine_forkchoiceUpdated`.
532    PayloadAttributes,
533    /// We are validating fields of a payload.
534    /// This corresponds to `engine_newPayload`.
535    Payload,
536    /// We are validating a built payload.
537    /// This corresponds to `engine_getPayload`.
538    GetPayload,
539}
540
541impl MessageValidationKind {
542    /// Returns an `EngineObjectValidationError` based on the given
543    /// `VersionSpecificValidationError` and the current validation kind.
544    pub const fn to_error(
545        self,
546        error: VersionSpecificValidationError,
547    ) -> EngineObjectValidationError {
548        match self {
549            // Both NewPayload and GetPayload errors are treated as generic Payload validation
550            // errors
551            Self::Payload | Self::GetPayload => EngineObjectValidationError::Payload(error),
552            Self::PayloadAttributes => EngineObjectValidationError::PayloadAttributes(error),
553        }
554    }
555}
556
557/// Validates the presence or exclusion of fork-specific fields based on the ethereum execution
558/// payload, or payload attributes, and the message version.
559///
560/// The object being validated is provided by the [`PayloadOrAttributes`] argument, which can be
561/// either an execution payload, or payload attributes.
562///
563/// The version is provided by the [`EngineApiMessageVersion`] argument.
564pub fn validate_version_specific_fields<Payload, Type, T>(
565    chain_spec: &T,
566    version: EngineApiMessageVersion,
567    payload_or_attrs: PayloadOrAttributes<'_, Payload, Type>,
568) -> Result<(), EngineObjectValidationError>
569where
570    Payload: ExecutionPayload,
571    Type: PayloadAttributes,
572    T: EthereumHardforks,
573{
574    // BAL only exists in ExecutionPayload, not PayloadAttributes (EIP-7928)
575    if let PayloadOrAttributes::ExecutionPayload(_) = payload_or_attrs {
576        validate_block_access_list_presence(
577            version,
578            payload_or_attrs.message_validation_kind(),
579            payload_or_attrs.block_access_list().is_some(),
580        )?;
581    }
582
583    validate_slot_number_presence(
584        version,
585        payload_or_attrs.message_validation_kind(),
586        payload_or_attrs.slot_number().is_some(),
587    )?;
588
589    validate_withdrawals_presence(
590        chain_spec,
591        version,
592        payload_or_attrs.message_validation_kind(),
593        payload_or_attrs.timestamp(),
594        payload_or_attrs.withdrawals().is_some(),
595    )?;
596    validate_parent_beacon_block_root_presence(
597        chain_spec,
598        version,
599        payload_or_attrs.message_validation_kind(),
600        payload_or_attrs.timestamp(),
601        payload_or_attrs.parent_beacon_block_root().is_some(),
602    )
603}
604
605/// The version of Engine API message.
606#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
607pub enum EngineApiMessageVersion {
608    /// Version 1
609    V1 = 1,
610    /// Version 2
611    ///
612    /// Added in the Shanghai hardfork.
613    V2 = 2,
614    /// Version 3
615    ///
616    /// Added in the Cancun hardfork.
617    V3 = 3,
618    /// Version 4
619    ///
620    /// Added in the Prague hardfork.
621    #[default]
622    V4 = 4,
623    /// Version 5
624    ///
625    /// Added in the Osaka hardfork.
626    V5 = 5,
627    /// Version 6
628    ///
629    /// Added in the Amsterdam hardfork.
630    V6 = 6,
631}
632
633impl EngineApiMessageVersion {
634    /// Returns true if the version is V1.
635    pub const fn is_v1(&self) -> bool {
636        matches!(self, Self::V1)
637    }
638
639    /// Returns true if the version is V2.
640    pub const fn is_v2(&self) -> bool {
641        matches!(self, Self::V2)
642    }
643
644    /// Returns true if the version is V3.
645    pub const fn is_v3(&self) -> bool {
646        matches!(self, Self::V3)
647    }
648
649    /// Returns true if the version is V4.
650    pub const fn is_v4(&self) -> bool {
651        matches!(self, Self::V4)
652    }
653
654    /// Returns true if the version is V5.
655    pub const fn is_v5(&self) -> bool {
656        matches!(self, Self::V5)
657    }
658
659    /// Returns true if the version is V6.
660    pub const fn is_v6(&self) -> bool {
661        matches!(self, Self::V6)
662    }
663
664    /// Returns the method name for the given version.
665    pub const fn method_name(&self) -> &'static str {
666        match self {
667            Self::V1 => "engine_newPayloadV1",
668            Self::V2 => "engine_newPayloadV2",
669            Self::V3 => "engine_newPayloadV3",
670            Self::V4 => "engine_newPayloadV4",
671            Self::V5 | Self::V6 => "engine_newPayloadV5",
672        }
673    }
674}
675
676/// Determines how we should choose the payload to return.
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
678pub enum PayloadKind {
679    /// Returns the next best available payload (the earliest available payload).
680    /// This does not wait for a real for pending job to finish if there's no best payload yet and
681    /// is allowed to race various payload jobs (empty, pending best) against each other and
682    /// returns whichever job finishes faster.
683    ///
684    /// This should be used when it's more important to return a valid payload as fast as possible.
685    /// For example, the engine API timeout for `engine_getPayload` is 1s and clients should rather
686    /// return an empty payload than indefinitely waiting for the pending payload job to finish and
687    /// risk missing the deadline.
688    #[default]
689    Earliest,
690    /// Only returns once we have at least one built payload.
691    ///
692    /// Compared to [`PayloadKind::Earliest`] this does not race an empty payload job against the
693    /// already in progress one, and returns the best available built payload or awaits the job in
694    /// progress.
695    WaitForPending,
696}
697
698/// Validates that execution requests are valid according to Engine API specification.
699///
700/// `executionRequests`: `Array of DATA` - List of execution layer triggered requests. Each list
701/// element is a `requests` byte array as defined by [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685).
702/// The first byte of each element is the `request_type` and the remaining bytes are the
703/// `request_data`. Elements of the list **MUST** be ordered by `request_type` in ascending order.
704/// Elements with empty `request_data` **MUST** be excluded from the list. If any element is out of
705/// order, has a length of 1-byte or shorter, or more than one element has the same type byte,
706/// client software **MUST** return `-32602: Invalid params` error.
707pub fn validate_execution_requests(requests: &[Bytes]) -> Result<(), EngineObjectValidationError> {
708    let mut last_request_type = None;
709    for request in requests {
710        if request.len() <= 1 {
711            return Err(EngineObjectValidationError::InvalidParams("EmptyExecutionRequest".into()))
712        }
713
714        let request_type = request[0];
715        if Some(request_type) < last_request_type {
716            return Err(EngineObjectValidationError::InvalidParams(
717                "OutOfOrderExecutionRequest".into(),
718            ))
719        }
720
721        if Some(request_type) == last_request_type {
722            return Err(EngineObjectValidationError::InvalidParams(
723                "DuplicatedExecutionRequestType".into(),
724            ))
725        }
726
727        last_request_type = Some(request_type);
728    }
729    Ok(())
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735    use assert_matches::assert_matches;
736    use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition};
737
738    #[test]
739    fn version_ord() {
740        assert!(EngineApiMessageVersion::V4 > EngineApiMessageVersion::V3);
741    }
742
743    #[test]
744    fn validate_osaka_get_payload_restrictions() {
745        // Osaka activates at timestamp 1000
746        let osaka_activation = 1000;
747        let chain_spec = ChainSpecBuilder::mainnet()
748            .with_fork(EthereumHardfork::Prague, ForkCondition::Timestamp(0))
749            .with_fork(EthereumHardfork::Osaka, ForkCondition::Timestamp(osaka_activation))
750            .build();
751
752        // Osaka is Active + V4 + GetPayload
753        let res = validate_payload_timestamp(
754            &chain_spec,
755            EngineApiMessageVersion::V4,
756            osaka_activation,
757            MessageValidationKind::GetPayload,
758        );
759        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
760
761        // Osaka is Active + V4 + Payload (NewPayload)
762        let res = validate_payload_timestamp(
763            &chain_spec,
764            EngineApiMessageVersion::V4,
765            osaka_activation,
766            MessageValidationKind::Payload,
767        );
768        assert_matches!(res, Ok(()));
769    }
770
771    #[test]
772    fn validate_amsterdam_staggered_version_restrictions() {
773        let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
774
775        let res = validate_payload_timestamp(
776            &chain_spec,
777            EngineApiMessageVersion::V3,
778            0,
779            MessageValidationKind::PayloadAttributes,
780        );
781        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
782
783        let res = validate_payload_timestamp(
784            &chain_spec,
785            EngineApiMessageVersion::V4,
786            0,
787            MessageValidationKind::Payload,
788        );
789        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
790
791        let res = validate_payload_timestamp(
792            &chain_spec,
793            EngineApiMessageVersion::V5,
794            0,
795            MessageValidationKind::GetPayload,
796        );
797        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
798
799        let res = validate_payload_timestamp(
800            &chain_spec,
801            EngineApiMessageVersion::V6,
802            0,
803            MessageValidationKind::GetPayload,
804        );
805        assert_matches!(res, Ok(()));
806    }
807
808    #[test]
809    fn validate_bogota_fork_timeframe() {
810        // Amsterdam active, Bogota not scheduled: the Bogota-specific methods must reject
811        let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
812
813        // `engine_newPayloadV6` requires a Bogota timestamp
814        let res = validate_payload_timestamp(
815            &chain_spec,
816            EngineApiMessageVersion::V6,
817            0,
818            MessageValidationKind::Payload,
819        );
820        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
821
822        // `engine_forkchoiceUpdatedV5` requires Bogota payload attributes
823        let res = validate_payload_timestamp(
824            &chain_spec,
825            EngineApiMessageVersion::V5,
826            0,
827            MessageValidationKind::PayloadAttributes,
828        );
829        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
830
831        // the Amsterdam methods remain valid pre-Bogota
832        let res = validate_payload_timestamp(
833            &chain_spec,
834            EngineApiMessageVersion::V5,
835            0,
836            MessageValidationKind::Payload,
837        );
838        assert_matches!(res, Ok(()));
839        let res = validate_payload_timestamp(
840            &chain_spec,
841            EngineApiMessageVersion::V4,
842            0,
843            MessageValidationKind::PayloadAttributes,
844        );
845        assert_matches!(res, Ok(()));
846    }
847
848    #[test]
849    fn validate_bogota_staggered_version_restrictions() {
850        let chain_spec = ChainSpecBuilder::mainnet().bogota_activated().build();
851
852        // `engine_newPayloadV5` must reject Bogota payloads
853        let res = validate_payload_timestamp(
854            &chain_spec,
855            EngineApiMessageVersion::V5,
856            0,
857            MessageValidationKind::Payload,
858        );
859        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
860
861        // `engine_forkchoiceUpdatedV4` must reject Bogota payload attributes
862        let res = validate_payload_timestamp(
863            &chain_spec,
864            EngineApiMessageVersion::V4,
865            0,
866            MessageValidationKind::PayloadAttributes,
867        );
868        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
869
870        // the Bogota-specific methods are accepted
871        let res = validate_payload_timestamp(
872            &chain_spec,
873            EngineApiMessageVersion::V6,
874            0,
875            MessageValidationKind::Payload,
876        );
877        assert_matches!(res, Ok(()));
878        let res = validate_payload_timestamp(
879            &chain_spec,
880            EngineApiMessageVersion::V5,
881            0,
882            MessageValidationKind::PayloadAttributes,
883        );
884        assert_matches!(res, Ok(()));
885
886        // Bogota defines no new getPayload version, `engine_getPayloadV6` remains valid
887        let res = validate_payload_timestamp(
888            &chain_spec,
889            EngineApiMessageVersion::V6,
890            0,
891            MessageValidationKind::GetPayload,
892        );
893        assert_matches!(res, Ok(()));
894    }
895
896    #[test]
897    fn validate_amsterdam_slot_and_bal_presence() {
898        let res = validate_slot_number_presence(
899            EngineApiMessageVersion::V4,
900            MessageValidationKind::PayloadAttributes,
901            true,
902        );
903        assert_matches!(res, Ok(()));
904
905        let res = validate_slot_number_presence(
906            EngineApiMessageVersion::V5,
907            MessageValidationKind::Payload,
908            true,
909        );
910        assert_matches!(res, Ok(()));
911
912        let res = validate_slot_number_presence(
913            EngineApiMessageVersion::V5,
914            MessageValidationKind::PayloadAttributes,
915            true,
916        );
917        assert_matches!(res, Ok(()));
918
919        let res = validate_block_access_list_presence(
920            EngineApiMessageVersion::V5,
921            MessageValidationKind::Payload,
922            true,
923        );
924        assert_matches!(res, Ok(()));
925    }
926
927    #[test]
928    fn validate_amsterdam_fork_timeframe() {
929        // Osaka active, Amsterdam not scheduled: the Amsterdam methods must reject.
930        let chain_spec = ChainSpecBuilder::mainnet().osaka_activated().build();
931        assert!(!chain_spec.is_amsterdam_active_at_timestamp(0));
932
933        // `engine_newPayloadV5` requires an Amsterdam timestamp.
934        let res = validate_payload_timestamp(
935            &chain_spec,
936            EngineApiMessageVersion::V5,
937            0,
938            MessageValidationKind::Payload,
939        );
940        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
941
942        // `engine_forkchoiceUpdatedV4` requires Amsterdam payload attributes.
943        let res = validate_payload_timestamp(
944            &chain_spec,
945            EngineApiMessageVersion::V4,
946            0,
947            MessageValidationKind::PayloadAttributes,
948        );
949        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
950
951        // `engine_getPayloadV5` is the Osaka method and remains valid pre-Amsterdam.
952        let res = validate_payload_timestamp(
953            &chain_spec,
954            EngineApiMessageVersion::V5,
955            0,
956            MessageValidationKind::GetPayload,
957        );
958        assert_matches!(res, Ok(()));
959
960        // `engine_newPayloadV4` remains valid pre-Amsterdam.
961        let res = validate_payload_timestamp(
962            &chain_spec,
963            EngineApiMessageVersion::V4,
964            0,
965            MessageValidationKind::Payload,
966        );
967        assert_matches!(res, Ok(()));
968    }
969
970    #[test]
971    fn validate_slot_and_bal_presence_is_structural() {
972        // The presence checks take no timestamp, a well-formed V5 payload passes them and the
973        // fork timeframe check rejects it afterwards.
974        let res = validate_block_access_list_presence(
975            EngineApiMessageVersion::V5,
976            MessageValidationKind::Payload,
977            true,
978        );
979        assert_matches!(res, Ok(()));
980        let res = validate_slot_number_presence(
981            EngineApiMessageVersion::V5,
982            MessageValidationKind::Payload,
983            true,
984        );
985        assert_matches!(res, Ok(()));
986        let res = validate_slot_number_presence(
987            EngineApiMessageVersion::V4,
988            MessageValidationKind::PayloadAttributes,
989            true,
990        );
991        assert_matches!(res, Ok(()));
992
993        // Missing fields are structural errors.
994        let res = validate_block_access_list_presence(
995            EngineApiMessageVersion::V5,
996            MessageValidationKind::Payload,
997            false,
998        );
999        assert_matches!(
1000            res,
1001            Err(EngineObjectValidationError::Payload(
1002                VersionSpecificValidationError::NoBlockAccessListPostAmsterdam
1003            ))
1004        );
1005        let res = validate_block_access_list_presence(
1006            EngineApiMessageVersion::V6,
1007            MessageValidationKind::Payload,
1008            false,
1009        );
1010        assert_matches!(
1011            res,
1012            Err(EngineObjectValidationError::Payload(
1013                VersionSpecificValidationError::NoBlockAccessListPostAmsterdam
1014            ))
1015        );
1016        let res = validate_slot_number_presence(
1017            EngineApiMessageVersion::V5,
1018            MessageValidationKind::Payload,
1019            false,
1020        );
1021        assert_matches!(
1022            res,
1023            Err(EngineObjectValidationError::Payload(
1024                VersionSpecificValidationError::NoSlotNumberPostAmsterdam
1025            ))
1026        );
1027        let res = validate_slot_number_presence(
1028            EngineApiMessageVersion::V4,
1029            MessageValidationKind::PayloadAttributes,
1030            false,
1031        );
1032        assert_matches!(
1033            res,
1034            Err(EngineObjectValidationError::PayloadAttributes(
1035                VersionSpecificValidationError::NoSlotNumberPostAmsterdam
1036            ))
1037        );
1038
1039        // Methods that predate Amsterdam still reject the fields.
1040        let res = validate_block_access_list_presence(
1041            EngineApiMessageVersion::V4,
1042            MessageValidationKind::Payload,
1043            true,
1044        );
1045        assert_matches!(
1046            res,
1047            Err(EngineObjectValidationError::Payload(
1048                VersionSpecificValidationError::BlockAccessListNotSupported
1049            ))
1050        );
1051        let res = validate_slot_number_presence(
1052            EngineApiMessageVersion::V4,
1053            MessageValidationKind::Payload,
1054            true,
1055        );
1056        assert_matches!(
1057            res,
1058            Err(EngineObjectValidationError::Payload(
1059                VersionSpecificValidationError::SlotNumberNotSupported
1060            ))
1061        );
1062    }
1063
1064    #[test]
1065    fn execution_requests_validation() {
1066        assert_matches!(validate_execution_requests(&[]), Ok(()));
1067
1068        let valid_requests = [
1069            Bytes::from_iter([1, 2]),
1070            Bytes::from_iter([2, 3]),
1071            Bytes::from_iter([3, 4]),
1072            Bytes::from_iter([4, 5]),
1073        ];
1074        assert_matches!(validate_execution_requests(&valid_requests), Ok(()));
1075
1076        let requests_with_empty = [
1077            Bytes::from_iter([1, 2]),
1078            Bytes::from_iter([2, 3]),
1079            Bytes::new(),
1080            Bytes::from_iter([3, 4]),
1081        ];
1082        assert_matches!(
1083            validate_execution_requests(&requests_with_empty),
1084            Err(EngineObjectValidationError::InvalidParams(_))
1085        );
1086
1087        let mut requests_valid_reversed = valid_requests;
1088        requests_valid_reversed.reverse();
1089        assert_matches!(
1090            validate_execution_requests(&requests_valid_reversed),
1091            Err(EngineObjectValidationError::InvalidParams(_))
1092        );
1093
1094        let requests_out_of_order = [
1095            Bytes::from_iter([1, 2]),
1096            Bytes::from_iter([2, 3]),
1097            Bytes::from_iter([4, 5]),
1098            Bytes::from_iter([3, 4]),
1099        ];
1100        assert_matches!(
1101            validate_execution_requests(&requests_out_of_order),
1102            Err(EngineObjectValidationError::InvalidParams(_))
1103        );
1104
1105        let duplicate_request_types = [
1106            Bytes::from_iter([1, 2]),
1107            Bytes::from_iter([3, 3]),
1108            Bytes::from_iter([4, 5]),
1109            Bytes::from_iter([4, 4]),
1110        ];
1111        assert_matches!(
1112            validate_execution_requests(&duplicate_request_types),
1113            Err(EngineObjectValidationError::InvalidParams(_))
1114        );
1115    }
1116
1117    #[test]
1118    fn validate_v2_withdrawals_follow_timestamp() {
1119        let chain_spec = ChainSpecBuilder::mainnet()
1120            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(100))
1121            .build();
1122        for (timestamp, has_withdrawals) in [(99, false), (100, true)] {
1123            assert_matches!(
1124                validate_withdrawals_presence(
1125                    &chain_spec,
1126                    EngineApiMessageVersion::V2,
1127                    MessageValidationKind::Payload,
1128                    timestamp,
1129                    has_withdrawals,
1130                ),
1131                Ok(())
1132            );
1133        }
1134        assert_matches!(
1135            validate_withdrawals_presence(
1136                &chain_spec,
1137                EngineApiMessageVersion::V2,
1138                MessageValidationKind::Payload,
1139                99,
1140                true,
1141            ),
1142            Err(EngineObjectValidationError::Payload(
1143                VersionSpecificValidationError::HasWithdrawalsPreShanghai
1144            ))
1145        );
1146        assert_matches!(
1147            validate_withdrawals_presence(
1148                &chain_spec,
1149                EngineApiMessageVersion::V2,
1150                MessageValidationKind::Payload,
1151                100,
1152                false,
1153            ),
1154            Err(EngineObjectValidationError::Payload(
1155                VersionSpecificValidationError::NoWithdrawalsPostShanghai
1156            ))
1157        );
1158    }
1159
1160    #[test]
1161    fn validate_v3_and_later_withdrawals_are_structural() {
1162        let chain_spec = ChainSpecBuilder::mainnet()
1163            .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(100))
1164            .build();
1165        for version in [
1166            EngineApiMessageVersion::V3,
1167            EngineApiMessageVersion::V4,
1168            EngineApiMessageVersion::V5,
1169            EngineApiMessageVersion::V6,
1170        ] {
1171            for timestamp in [99, 100] {
1172                assert_matches!(
1173                    validate_withdrawals_presence(
1174                        &chain_spec,
1175                        version,
1176                        MessageValidationKind::Payload,
1177                        timestamp,
1178                        true,
1179                    ),
1180                    Ok(())
1181                );
1182                assert_matches!(
1183                    validate_withdrawals_presence(
1184                        &chain_spec,
1185                        version,
1186                        MessageValidationKind::Payload,
1187                        timestamp,
1188                        false,
1189                    ),
1190                    Err(EngineObjectValidationError::Payload(
1191                        VersionSpecificValidationError::NoWithdrawalsPostShanghai
1192                    ))
1193                );
1194            }
1195        }
1196    }
1197}