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.
65/// * If V5, this ensures that the payload timestamp is within the Osaka timestamp, and within the
66///   Bogota timestamp for `engine_forkchoiceUpdatedV5` payload attributes.
67/// * If V6, this ensures that the payload timestamp is within the Amsterdam timestamp, and within
68///   the Bogota timestamp for `engine_newPayloadV6` payloads.
69///
70/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload and that
71/// staggered endpoint upgrades reject the next fork once a newer method version is required.
72///
73/// Otherwise, this will return [`EngineObjectValidationError::UnsupportedFork`].
74pub fn validate_payload_timestamp(
75    chain_spec: impl EthereumHardforks,
76    version: EngineApiMessageVersion,
77    timestamp: u64,
78    kind: MessageValidationKind,
79) -> Result<(), EngineObjectValidationError> {
80    let is_cancun = chain_spec.is_cancun_active_at_timestamp(timestamp);
81    if version.is_v2() && is_cancun {
82        // From the Engine API spec:
83        //
84        // ### Update the methods of previous forks
85        //
86        // This document defines how Cancun payload should be handled by the [`Shanghai
87        // API`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md).
88        //
89        // For the following methods:
90        //
91        // - [`engine_forkchoiceUpdatedV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_forkchoiceupdatedv2)
92        // - [`engine_newPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_newpayloadV2)
93        // - [`engine_getPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_getpayloadv2)
94        //
95        // a validation **MUST** be added:
96        //
97        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
98        //    payload or payloadAttributes is greater or equal to the Cancun activation timestamp.
99        return Err(EngineObjectValidationError::UnsupportedFork)
100    }
101
102    if version.is_v3() && !is_cancun {
103        // From the Engine API spec:
104        // <https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/cancun.md#specification-2>
105        //
106        // For `engine_getPayloadV3`:
107        //
108        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
109        //    the built payload does not fall within the time frame of the Cancun fork.
110        //
111        // For `engine_forkchoiceUpdatedV3`:
112        //
113        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
114        //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
115        //    the time frame of the Cancun fork.
116        //
117        // For `engine_newPayloadV3`:
118        //
119        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
120        //    the payload does not fall within the time frame of the Cancun fork.
121        return Err(EngineObjectValidationError::UnsupportedFork)
122    }
123
124    let is_prague = chain_spec.is_prague_active_at_timestamp(timestamp);
125    if version.is_v4() && !is_prague {
126        // From the Engine API spec:
127        // <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#specification-1>
128        //
129        // For `engine_getPayloadV4`:
130        //
131        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
132        //    the built payload does not fall within the time frame of the Prague fork.
133        //
134        // For `engine_forkchoiceUpdatedV4`:
135        //
136        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
137        //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
138        //    the time frame of the Prague fork.
139        //
140        // For `engine_newPayloadV4`:
141        //
142        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
143        //    the payload does not fall within the time frame of the Prague fork.
144        return Err(EngineObjectValidationError::UnsupportedFork)
145    }
146
147    let is_osaka = chain_spec.is_osaka_active_at_timestamp(timestamp);
148    if version.is_v5() && !is_osaka {
149        // From the Engine API spec:
150        // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
151        //
152        // For `engine_getPayloadV5`
153        //
154        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
155        //    built payload does not fall within the time frame of the Osaka fork.
156        return Err(EngineObjectValidationError::UnsupportedFork)
157    }
158
159    let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
160
161    // Staggered endpoint upgrades must reject Amsterdam payloads until the Amsterdam-specific
162    // method version is used.
163    if is_amsterdam &&
164        matches!(
165            (version, kind),
166            (EngineApiMessageVersion::V3, MessageValidationKind::PayloadAttributes) |
167                (EngineApiMessageVersion::V4, MessageValidationKind::Payload) |
168                (EngineApiMessageVersion::V5, MessageValidationKind::GetPayload)
169        )
170    {
171        return Err(EngineObjectValidationError::UnsupportedFork)
172    }
173
174    // `engine_getPayloadV4` MUST reject payloads with a timestamp >= Osaka.
175    if version.is_v4() && kind == MessageValidationKind::GetPayload && is_osaka {
176        return Err(EngineObjectValidationError::UnsupportedFork)
177    }
178
179    if version.is_v6() && !is_amsterdam {
180        // From the Engine API spec:
181        // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
182        //
183        // For `engine_getPayloadV6`
184        //
185        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
186        //    built payload does not fall within the time frame of the Amsterdam fork.
187
188        return Err(EngineObjectValidationError::UnsupportedFork)
189    }
190
191    let is_bogota = chain_spec.is_bogota_active_at_timestamp(timestamp);
192
193    // Staggered endpoint upgrades must reject Bogota payloads until the Bogota-specific method
194    // version is used.
195    //
196    // From the Engine API spec:
197    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#update-the-methods-of-previous-forks>
198    //
199    // For `engine_newPayloadV5` and `engine_forkchoiceUpdatedV4`:
200    //
201    // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of payload is
202    //    greater than or equal to the Bogota activation timestamp.
203    if is_bogota &&
204        matches!(
205            (version, kind),
206            (EngineApiMessageVersion::V4, MessageValidationKind::PayloadAttributes) |
207                (EngineApiMessageVersion::V5, MessageValidationKind::Payload)
208        )
209    {
210        return Err(EngineObjectValidationError::UnsupportedFork)
211    }
212
213    if !is_bogota &&
214        matches!(
215            (version, kind),
216            (EngineApiMessageVersion::V5, MessageValidationKind::PayloadAttributes) |
217                (EngineApiMessageVersion::V6, MessageValidationKind::Payload)
218        )
219    {
220        // From the Engine API spec:
221        // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md>
222        //
223        // For `engine_newPayloadV6`:
224        //
225        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
226        //    payload does not fall within the time frame of the Bogota fork.
227        //
228        // For `engine_forkchoiceUpdatedV5`:
229        //
230        // 2. Client software MUST return -38005: Unsupported fork error if the payloadAttributes is
231        //    set and the payloadAttributes.timestamp does not fall within the time frame of the
232        //    Bogota fork.
233        return Err(EngineObjectValidationError::UnsupportedFork)
234    }
235
236    Ok(())
237}
238
239/// Validates the presence of the `block access lists` field according to the payload timestamp.
240/// After Amsterdam, block access list field must be [Some].
241/// Before Amsterdam, block access list field must be [None];
242pub fn validate_block_access_list_presence<T: EthereumHardforks>(
243    chain_spec: &T,
244    version: EngineApiMessageVersion,
245    message_validation_kind: MessageValidationKind,
246    timestamp: u64,
247    has_block_access_list: bool,
248) -> Result<(), EngineObjectValidationError> {
249    let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
250    match version {
251        EngineApiMessageVersion::V1 |
252        EngineApiMessageVersion::V2 |
253        EngineApiMessageVersion::V3 |
254        EngineApiMessageVersion::V4 => {
255            if has_block_access_list {
256                return Err(message_validation_kind
257                    .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
258            }
259        }
260
261        EngineApiMessageVersion::V5 => {
262            if message_validation_kind == MessageValidationKind::Payload {
263                if is_amsterdam_active && !has_block_access_list {
264                    return Err(message_validation_kind
265                        .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
266                }
267                if !is_amsterdam_active && has_block_access_list {
268                    return Err(message_validation_kind
269                        .to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
270                }
271            } else if has_block_access_list {
272                return Err(message_validation_kind
273                    .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
274            }
275        }
276
277        EngineApiMessageVersion::V6 => {
278            if is_amsterdam_active && !has_block_access_list {
279                return Err(message_validation_kind
280                    .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
281            }
282            if !is_amsterdam_active && has_block_access_list {
283                return Err(message_validation_kind
284                    .to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
285            }
286        }
287    };
288
289    Ok(())
290}
291
292/// Validates the presence of the `slot number` field according to the payload timestamp.
293/// After Amsterdam, slot number field must be [Some].
294/// Before Amsterdam, slot number field must be [None];
295pub fn validate_slot_number_presence<T: EthereumHardforks>(
296    chain_spec: &T,
297    version: EngineApiMessageVersion,
298    message_validation_kind: MessageValidationKind,
299    timestamp: u64,
300    has_slot_number: bool,
301) -> Result<(), EngineObjectValidationError> {
302    let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
303
304    match version {
305        EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => {
306            if has_slot_number {
307                return Err(message_validation_kind
308                    .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
309            }
310        }
311
312        EngineApiMessageVersion::V4 => {
313            if message_validation_kind == MessageValidationKind::PayloadAttributes {
314                if is_amsterdam_active && !has_slot_number {
315                    return Err(message_validation_kind
316                        .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
317                }
318                if !is_amsterdam_active && has_slot_number {
319                    return Err(message_validation_kind
320                        .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
321                }
322            } else if has_slot_number {
323                return Err(message_validation_kind
324                    .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
325            }
326        }
327
328        EngineApiMessageVersion::V5 => {
329            if matches!(
330                message_validation_kind,
331                MessageValidationKind::Payload | MessageValidationKind::PayloadAttributes
332            ) {
333                if is_amsterdam_active && !has_slot_number {
334                    return Err(message_validation_kind
335                        .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
336                }
337                if !is_amsterdam_active && has_slot_number {
338                    return Err(message_validation_kind
339                        .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
340                }
341            } else if has_slot_number {
342                return Err(message_validation_kind
343                    .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
344            }
345        }
346
347        EngineApiMessageVersion::V6 => {
348            if is_amsterdam_active && !has_slot_number {
349                return Err(message_validation_kind
350                    .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
351            }
352            if !is_amsterdam_active && has_slot_number {
353                return Err(message_validation_kind
354                    .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
355            }
356        }
357    };
358
359    Ok(())
360}
361
362/// Validates the presence of the `withdrawals` field according to the payload timestamp.
363/// After Shanghai, withdrawals field must be [Some].
364/// Before Shanghai, withdrawals field must be [None];
365pub fn validate_withdrawals_presence<T: EthereumHardforks>(
366    chain_spec: &T,
367    version: EngineApiMessageVersion,
368    message_validation_kind: MessageValidationKind,
369    timestamp: u64,
370    has_withdrawals: bool,
371) -> Result<(), EngineObjectValidationError> {
372    let is_shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
373
374    match version {
375        EngineApiMessageVersion::V1 => {
376            if has_withdrawals {
377                return Err(message_validation_kind
378                    .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1))
379            }
380        }
381        EngineApiMessageVersion::V2 |
382        EngineApiMessageVersion::V3 |
383        EngineApiMessageVersion::V4 |
384        EngineApiMessageVersion::V5 |
385        EngineApiMessageVersion::V6 => {
386            if is_shanghai_active && !has_withdrawals {
387                return Err(message_validation_kind
388                    .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
389            }
390            if !is_shanghai_active && has_withdrawals {
391                return Err(message_validation_kind
392                    .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai))
393            }
394        }
395    };
396
397    Ok(())
398}
399
400/// Validate the presence of the `parentBeaconBlockRoot` field according to the given timestamp.
401/// This method is meant to be used with either a `payloadAttributes` field or a full payload, with
402/// the `engine_forkchoiceUpdated` and `engine_newPayload` methods respectively.
403///
404/// After Cancun, the `parentBeaconBlockRoot` field must be [Some].
405/// Before Cancun, the `parentBeaconBlockRoot` field must be [None].
406///
407/// If the engine API message version is V1 or V2, and the timestamp is post-Cancun, then this will
408/// return [`EngineObjectValidationError::UnsupportedFork`].
409///
410/// If the timestamp is before the Cancun fork and the engine API message version is V3, then this
411/// will return [`EngineObjectValidationError::UnsupportedFork`].
412///
413/// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then
414/// this will return [`VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun`].
415///
416/// This implements the following Engine API spec rules:
417///
418/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
419///    matches the expected one and return `-32602: Invalid params` error if this check fails. Any
420///    field having `null` value **MUST** be considered as not provided.
421///
422/// For `engine_forkchoiceUpdatedV3`:
423///
424/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
425///    matches the expected one and return `-32602: Invalid params` error if this check fails. Any
426///    field having `null` value **MUST** be considered as not provided.
427///
428/// 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the following
429///    sequence of checks that **MUST** be run over `payloadAttributes`:
430///     1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003: Invalid
431///        payload attributes` on failure.
432///     2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
433///        `-38005: Unsupported fork` on failure.
434///     3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
435///        `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on failure.
436///     4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
437///        back.
438///
439/// For `engine_newPayloadV3`:
440///
441/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
442///    payload does not fall within the time frame of the Cancun fork.
443///
444/// For `engine_newPayloadV4`:
445///
446/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
447///    payload does not fall within the time frame of the Prague fork.
448///
449/// Returning the right error code (ie, if the client should return `-38003: Invalid payload
450/// attributes` is handled by the `message_validation_kind` parameter. If the parameter is
451/// `MessageValidationKind::Payload`, then the error code will be `-32602: Invalid params`. If the
452/// parameter is `MessageValidationKind::PayloadAttributes`, then the error code will be `-38003:
453/// Invalid payload attributes`.
454pub fn validate_parent_beacon_block_root_presence<T: EthereumHardforks>(
455    chain_spec: &T,
456    version: EngineApiMessageVersion,
457    validation_kind: MessageValidationKind,
458    timestamp: u64,
459    has_parent_beacon_block_root: bool,
460) -> Result<(), EngineObjectValidationError> {
461    // 1. Client software **MUST** check that provided set of parameters and their fields strictly
462    //    matches the expected one and return `-32602: Invalid params` error if this check fails.
463    //    Any field having `null` value **MUST** be considered as not provided.
464    //
465    // For `engine_forkchoiceUpdatedV3`:
466    //
467    // 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the
468    //    following sequence of checks that **MUST** be run over `payloadAttributes`:
469    //     1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003:
470    //        Invalid payload attributes` on failure.
471    //     2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
472    //        `-38005: Unsupported fork` on failure.
473    //     3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
474    //        `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on
475    //        failure.
476    //     4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
477    //        back.
478    match version {
479        EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => {
480            if has_parent_beacon_block_root {
481                return Err(validation_kind.to_error(
482                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
483                ))
484            }
485        }
486        EngineApiMessageVersion::V3 |
487        EngineApiMessageVersion::V4 |
488        EngineApiMessageVersion::V5 |
489        EngineApiMessageVersion::V6 => {
490            if !has_parent_beacon_block_root {
491                return Err(validation_kind
492                    .to_error(VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun))
493            }
494        }
495    };
496
497    // For `engine_forkchoiceUpdatedV3`:
498    //
499    // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
500    //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the
501    //    time frame of the Cancun fork.
502    //
503    // For `engine_newPayloadV3`:
504    //
505    // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
506    //    payload does not fall within the time frame of the Cancun fork.
507    validate_payload_timestamp(chain_spec, version, timestamp, validation_kind)?;
508
509    Ok(())
510}
511
512/// A type that represents whether or not we are validating a payload or payload attributes.
513///
514/// This is used to ensure that the correct error code is returned when validating the payload or
515/// payload attributes.
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517pub enum MessageValidationKind {
518    /// We are validating fields of a payload attributes.
519    /// This corresponds to `engine_forkchoiceUpdated`.
520    PayloadAttributes,
521    /// We are validating fields of a payload.
522    /// This corresponds to `engine_newPayload`.
523    Payload,
524    /// We are validating a built payload.
525    /// This corresponds to `engine_getPayload`.
526    GetPayload,
527}
528
529impl MessageValidationKind {
530    /// Returns an `EngineObjectValidationError` based on the given
531    /// `VersionSpecificValidationError` and the current validation kind.
532    pub const fn to_error(
533        self,
534        error: VersionSpecificValidationError,
535    ) -> EngineObjectValidationError {
536        match self {
537            // Both NewPayload and GetPayload errors are treated as generic Payload validation
538            // errors
539            Self::Payload | Self::GetPayload => EngineObjectValidationError::Payload(error),
540            Self::PayloadAttributes => EngineObjectValidationError::PayloadAttributes(error),
541        }
542    }
543}
544
545/// Validates the presence or exclusion of fork-specific fields based on the ethereum execution
546/// payload, or payload attributes, and the message version.
547///
548/// The object being validated is provided by the [`PayloadOrAttributes`] argument, which can be
549/// either an execution payload, or payload attributes.
550///
551/// The version is provided by the [`EngineApiMessageVersion`] argument.
552pub fn validate_version_specific_fields<Payload, Type, T>(
553    chain_spec: &T,
554    version: EngineApiMessageVersion,
555    payload_or_attrs: PayloadOrAttributes<'_, Payload, Type>,
556) -> Result<(), EngineObjectValidationError>
557where
558    Payload: ExecutionPayload,
559    Type: PayloadAttributes,
560    T: EthereumHardforks,
561{
562    // BAL only exists in ExecutionPayload, not PayloadAttributes (EIP-7928)
563    if let PayloadOrAttributes::ExecutionPayload(_) = payload_or_attrs {
564        validate_block_access_list_presence(
565            chain_spec,
566            version,
567            payload_or_attrs.message_validation_kind(),
568            payload_or_attrs.timestamp(),
569            payload_or_attrs.block_access_list().is_some(),
570        )?;
571    }
572
573    validate_slot_number_presence(
574        chain_spec,
575        version,
576        payload_or_attrs.message_validation_kind(),
577        payload_or_attrs.timestamp(),
578        payload_or_attrs.slot_number().is_some(),
579    )?;
580
581    validate_withdrawals_presence(
582        chain_spec,
583        version,
584        payload_or_attrs.message_validation_kind(),
585        payload_or_attrs.timestamp(),
586        payload_or_attrs.withdrawals().is_some(),
587    )?;
588    validate_parent_beacon_block_root_presence(
589        chain_spec,
590        version,
591        payload_or_attrs.message_validation_kind(),
592        payload_or_attrs.timestamp(),
593        payload_or_attrs.parent_beacon_block_root().is_some(),
594    )
595}
596
597/// The version of Engine API message.
598#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
599pub enum EngineApiMessageVersion {
600    /// Version 1
601    V1 = 1,
602    /// Version 2
603    ///
604    /// Added in the Shanghai hardfork.
605    V2 = 2,
606    /// Version 3
607    ///
608    /// Added in the Cancun hardfork.
609    V3 = 3,
610    /// Version 4
611    ///
612    /// Added in the Prague hardfork.
613    #[default]
614    V4 = 4,
615    /// Version 5
616    ///
617    /// Added in the Osaka hardfork.
618    V5 = 5,
619    /// Version 6
620    ///
621    /// Added in the Amsterdam hardfork.
622    V6 = 6,
623}
624
625impl EngineApiMessageVersion {
626    /// Returns true if the version is V1.
627    pub const fn is_v1(&self) -> bool {
628        matches!(self, Self::V1)
629    }
630
631    /// Returns true if the version is V2.
632    pub const fn is_v2(&self) -> bool {
633        matches!(self, Self::V2)
634    }
635
636    /// Returns true if the version is V3.
637    pub const fn is_v3(&self) -> bool {
638        matches!(self, Self::V3)
639    }
640
641    /// Returns true if the version is V4.
642    pub const fn is_v4(&self) -> bool {
643        matches!(self, Self::V4)
644    }
645
646    /// Returns true if the version is V5.
647    pub const fn is_v5(&self) -> bool {
648        matches!(self, Self::V5)
649    }
650
651    /// Returns true if the version is V6.
652    pub const fn is_v6(&self) -> bool {
653        matches!(self, Self::V6)
654    }
655
656    /// Returns the method name for the given version.
657    pub const fn method_name(&self) -> &'static str {
658        match self {
659            Self::V1 => "engine_newPayloadV1",
660            Self::V2 => "engine_newPayloadV2",
661            Self::V3 => "engine_newPayloadV3",
662            Self::V4 => "engine_newPayloadV4",
663            Self::V5 | Self::V6 => "engine_newPayloadV5",
664        }
665    }
666}
667
668/// Determines how we should choose the payload to return.
669#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
670pub enum PayloadKind {
671    /// Returns the next best available payload (the earliest available payload).
672    /// This does not wait for a real for pending job to finish if there's no best payload yet and
673    /// is allowed to race various payload jobs (empty, pending best) against each other and
674    /// returns whichever job finishes faster.
675    ///
676    /// This should be used when it's more important to return a valid payload as fast as possible.
677    /// For example, the engine API timeout for `engine_getPayload` is 1s and clients should rather
678    /// return an empty payload than indefinitely waiting for the pending payload job to finish and
679    /// risk missing the deadline.
680    #[default]
681    Earliest,
682    /// Only returns once we have at least one built payload.
683    ///
684    /// Compared to [`PayloadKind::Earliest`] this does not race an empty payload job against the
685    /// already in progress one, and returns the best available built payload or awaits the job in
686    /// progress.
687    WaitForPending,
688}
689
690/// Validates that execution requests are valid according to Engine API specification.
691///
692/// `executionRequests`: `Array of DATA` - List of execution layer triggered requests. Each list
693/// element is a `requests` byte array as defined by [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685).
694/// The first byte of each element is the `request_type` and the remaining bytes are the
695/// `request_data`. Elements of the list **MUST** be ordered by `request_type` in ascending order.
696/// Elements with empty `request_data` **MUST** be excluded from the list. If any element is out of
697/// order, has a length of 1-byte or shorter, or more than one element has the same type byte,
698/// client software **MUST** return `-32602: Invalid params` error.
699pub fn validate_execution_requests(requests: &[Bytes]) -> Result<(), EngineObjectValidationError> {
700    let mut last_request_type = None;
701    for request in requests {
702        if request.len() <= 1 {
703            return Err(EngineObjectValidationError::InvalidParams("EmptyExecutionRequest".into()))
704        }
705
706        let request_type = request[0];
707        if Some(request_type) < last_request_type {
708            return Err(EngineObjectValidationError::InvalidParams(
709                "OutOfOrderExecutionRequest".into(),
710            ))
711        }
712
713        if Some(request_type) == last_request_type {
714            return Err(EngineObjectValidationError::InvalidParams(
715                "DuplicatedExecutionRequestType".into(),
716            ))
717        }
718
719        last_request_type = Some(request_type);
720    }
721    Ok(())
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727    use assert_matches::assert_matches;
728    use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition};
729
730    #[test]
731    fn version_ord() {
732        assert!(EngineApiMessageVersion::V4 > EngineApiMessageVersion::V3);
733    }
734
735    #[test]
736    fn validate_osaka_get_payload_restrictions() {
737        // Osaka activates at timestamp 1000
738        let osaka_activation = 1000;
739        let chain_spec = ChainSpecBuilder::mainnet()
740            .with_fork(EthereumHardfork::Prague, ForkCondition::Timestamp(0))
741            .with_fork(EthereumHardfork::Osaka, ForkCondition::Timestamp(osaka_activation))
742            .build();
743
744        // Osaka is Active + V4 + GetPayload
745        let res = validate_payload_timestamp(
746            &chain_spec,
747            EngineApiMessageVersion::V4,
748            osaka_activation,
749            MessageValidationKind::GetPayload,
750        );
751        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
752
753        // Osaka is Active + V4 + Payload (NewPayload)
754        let res = validate_payload_timestamp(
755            &chain_spec,
756            EngineApiMessageVersion::V4,
757            osaka_activation,
758            MessageValidationKind::Payload,
759        );
760        assert_matches!(res, Ok(()));
761    }
762
763    #[test]
764    fn validate_amsterdam_staggered_version_restrictions() {
765        let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
766
767        let res = validate_payload_timestamp(
768            &chain_spec,
769            EngineApiMessageVersion::V3,
770            0,
771            MessageValidationKind::PayloadAttributes,
772        );
773        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
774
775        let res = validate_payload_timestamp(
776            &chain_spec,
777            EngineApiMessageVersion::V4,
778            0,
779            MessageValidationKind::Payload,
780        );
781        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
782
783        let res = validate_payload_timestamp(
784            &chain_spec,
785            EngineApiMessageVersion::V5,
786            0,
787            MessageValidationKind::GetPayload,
788        );
789        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
790
791        let res = validate_payload_timestamp(
792            &chain_spec,
793            EngineApiMessageVersion::V6,
794            0,
795            MessageValidationKind::GetPayload,
796        );
797        assert_matches!(res, Ok(()));
798    }
799
800    #[test]
801    fn validate_bogota_fork_timeframe() {
802        // Amsterdam active, Bogota not scheduled: the Bogota-specific methods must reject
803        let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
804
805        // `engine_newPayloadV6` requires a Bogota timestamp
806        let res = validate_payload_timestamp(
807            &chain_spec,
808            EngineApiMessageVersion::V6,
809            0,
810            MessageValidationKind::Payload,
811        );
812        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
813
814        // `engine_forkchoiceUpdatedV5` requires Bogota payload attributes
815        let res = validate_payload_timestamp(
816            &chain_spec,
817            EngineApiMessageVersion::V5,
818            0,
819            MessageValidationKind::PayloadAttributes,
820        );
821        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
822
823        // the Amsterdam methods remain valid pre-Bogota
824        let res = validate_payload_timestamp(
825            &chain_spec,
826            EngineApiMessageVersion::V5,
827            0,
828            MessageValidationKind::Payload,
829        );
830        assert_matches!(res, Ok(()));
831        let res = validate_payload_timestamp(
832            &chain_spec,
833            EngineApiMessageVersion::V4,
834            0,
835            MessageValidationKind::PayloadAttributes,
836        );
837        assert_matches!(res, Ok(()));
838    }
839
840    #[test]
841    fn validate_bogota_staggered_version_restrictions() {
842        let chain_spec = ChainSpecBuilder::mainnet().bogota_activated().build();
843
844        // `engine_newPayloadV5` must reject Bogota payloads
845        let res = validate_payload_timestamp(
846            &chain_spec,
847            EngineApiMessageVersion::V5,
848            0,
849            MessageValidationKind::Payload,
850        );
851        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
852
853        // `engine_forkchoiceUpdatedV4` must reject Bogota payload attributes
854        let res = validate_payload_timestamp(
855            &chain_spec,
856            EngineApiMessageVersion::V4,
857            0,
858            MessageValidationKind::PayloadAttributes,
859        );
860        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
861
862        // the Bogota-specific methods are accepted
863        let res = validate_payload_timestamp(
864            &chain_spec,
865            EngineApiMessageVersion::V6,
866            0,
867            MessageValidationKind::Payload,
868        );
869        assert_matches!(res, Ok(()));
870        let res = validate_payload_timestamp(
871            &chain_spec,
872            EngineApiMessageVersion::V5,
873            0,
874            MessageValidationKind::PayloadAttributes,
875        );
876        assert_matches!(res, Ok(()));
877
878        // Bogota defines no new getPayload version, `engine_getPayloadV6` remains valid
879        let res = validate_payload_timestamp(
880            &chain_spec,
881            EngineApiMessageVersion::V6,
882            0,
883            MessageValidationKind::GetPayload,
884        );
885        assert_matches!(res, Ok(()));
886    }
887
888    #[test]
889    fn validate_amsterdam_slot_and_bal_presence() {
890        let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
891
892        let res = validate_slot_number_presence(
893            &chain_spec,
894            EngineApiMessageVersion::V4,
895            MessageValidationKind::PayloadAttributes,
896            0,
897            true,
898        );
899        assert_matches!(res, Ok(()));
900
901        let res = validate_slot_number_presence(
902            &chain_spec,
903            EngineApiMessageVersion::V5,
904            MessageValidationKind::Payload,
905            0,
906            true,
907        );
908        assert_matches!(res, Ok(()));
909
910        let res = validate_slot_number_presence(
911            &chain_spec,
912            EngineApiMessageVersion::V5,
913            MessageValidationKind::PayloadAttributes,
914            0,
915            true,
916        );
917        assert_matches!(res, Ok(()));
918
919        let res = validate_block_access_list_presence(
920            &chain_spec,
921            EngineApiMessageVersion::V5,
922            MessageValidationKind::Payload,
923            0,
924            true,
925        );
926        assert_matches!(res, Ok(()));
927    }
928
929    #[test]
930    fn execution_requests_validation() {
931        assert_matches!(validate_execution_requests(&[]), Ok(()));
932
933        let valid_requests = [
934            Bytes::from_iter([1, 2]),
935            Bytes::from_iter([2, 3]),
936            Bytes::from_iter([3, 4]),
937            Bytes::from_iter([4, 5]),
938        ];
939        assert_matches!(validate_execution_requests(&valid_requests), Ok(()));
940
941        let requests_with_empty = [
942            Bytes::from_iter([1, 2]),
943            Bytes::from_iter([2, 3]),
944            Bytes::new(),
945            Bytes::from_iter([3, 4]),
946        ];
947        assert_matches!(
948            validate_execution_requests(&requests_with_empty),
949            Err(EngineObjectValidationError::InvalidParams(_))
950        );
951
952        let mut requests_valid_reversed = valid_requests;
953        requests_valid_reversed.reverse();
954        assert_matches!(
955            validate_execution_requests(&requests_valid_reversed),
956            Err(EngineObjectValidationError::InvalidParams(_))
957        );
958
959        let requests_out_of_order = [
960            Bytes::from_iter([1, 2]),
961            Bytes::from_iter([2, 3]),
962            Bytes::from_iter([4, 5]),
963            Bytes::from_iter([3, 4]),
964        ];
965        assert_matches!(
966            validate_execution_requests(&requests_out_of_order),
967            Err(EngineObjectValidationError::InvalidParams(_))
968        );
969
970        let duplicate_request_types = [
971            Bytes::from_iter([1, 2]),
972            Bytes::from_iter([3, 3]),
973            Bytes::from_iter([4, 5]),
974            Bytes::from_iter([4, 4]),
975        ];
976        assert_matches!(
977            validate_execution_requests(&duplicate_request_types),
978            Err(EngineObjectValidationError::InvalidParams(_))
979        );
980    }
981}