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;
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    ) -> Self::ExecutionData;
57}
58
59/// Validates the timestamp depending on the version called:
60///
61/// * If V2, this ensures that the payload timestamp is pre-Cancun.
62/// * If V3, this ensures that the payload timestamp is within the Cancun timestamp.
63/// * If V4, this ensures that the payload timestamp is within the Prague timestamp.
64/// * If V5, this ensures that the payload timestamp is within the Osaka timestamp.
65///
66/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload.
67///
68/// Otherwise, this will return [`EngineObjectValidationError::UnsupportedFork`].
69pub fn validate_payload_timestamp(
70    chain_spec: impl EthereumHardforks,
71    version: EngineApiMessageVersion,
72    timestamp: u64,
73    kind: MessageValidationKind,
74) -> Result<(), EngineObjectValidationError> {
75    let is_cancun = chain_spec.is_cancun_active_at_timestamp(timestamp);
76    if version.is_v2() && is_cancun {
77        // From the Engine API spec:
78        //
79        // ### Update the methods of previous forks
80        //
81        // This document defines how Cancun payload should be handled by the [`Shanghai
82        // API`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md).
83        //
84        // For the following methods:
85        //
86        // - [`engine_forkchoiceUpdatedV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_forkchoiceupdatedv2)
87        // - [`engine_newPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_newpayloadV2)
88        // - [`engine_getPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_getpayloadv2)
89        //
90        // a validation **MUST** be added:
91        //
92        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
93        //    payload or payloadAttributes is greater or equal to the Cancun activation timestamp.
94        return Err(EngineObjectValidationError::UnsupportedFork)
95    }
96
97    if version.is_v3() && !is_cancun {
98        // From the Engine API spec:
99        // <https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/cancun.md#specification-2>
100        //
101        // For `engine_getPayloadV3`:
102        //
103        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
104        //    the built payload does not fall within the time frame of the Cancun fork.
105        //
106        // For `engine_forkchoiceUpdatedV3`:
107        //
108        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
109        //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
110        //    the time frame of the Cancun fork.
111        //
112        // For `engine_newPayloadV3`:
113        //
114        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
115        //    the payload does not fall within the time frame of the Cancun fork.
116        return Err(EngineObjectValidationError::UnsupportedFork)
117    }
118
119    let is_prague = chain_spec.is_prague_active_at_timestamp(timestamp);
120    if version.is_v4() && !is_prague {
121        // From the Engine API spec:
122        // <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#specification-1>
123        //
124        // For `engine_getPayloadV4`:
125        //
126        // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
127        //    the built payload does not fall within the time frame of the Prague fork.
128        //
129        // For `engine_forkchoiceUpdatedV4`:
130        //
131        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
132        //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
133        //    the time frame of the Prague fork.
134        //
135        // For `engine_newPayloadV4`:
136        //
137        // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
138        //    the payload does not fall within the time frame of the Prague fork.
139        return Err(EngineObjectValidationError::UnsupportedFork)
140    }
141
142    let is_osaka = chain_spec.is_osaka_active_at_timestamp(timestamp);
143    if version.is_v5() && !is_osaka {
144        // From the Engine API spec:
145        // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
146        //
147        // For `engine_getPayloadV5`
148        //
149        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
150        //    built payload does not fall within the time frame of the Osaka fork.
151        return Err(EngineObjectValidationError::UnsupportedFork)
152    }
153
154    // `engine_getPayloadV4` MUST reject payloads with a timestamp >= Osaka.
155    if version.is_v4() && kind == MessageValidationKind::GetPayload && is_osaka {
156        return Err(EngineObjectValidationError::UnsupportedFork)
157    }
158
159    let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
160    if version.is_v6() && !is_amsterdam {
161        // From the Engine API spec:
162        // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
163        //
164        // For `engine_getPayloadV6`
165        //
166        // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
167        //    built payload does not fall within the time frame of the Amsterdam fork.
168
169        return Err(EngineObjectValidationError::UnsupportedFork)
170    }
171
172    Ok(())
173}
174
175/// Validates the presence of the `block access lists` field according to the payload timestamp.
176/// After Amsterdam, block access list field must be [Some].
177/// Before Amsterdam, block access list field must be [None];
178pub fn validate_block_access_list_presence<T: EthereumHardforks>(
179    chain_spec: &T,
180    version: EngineApiMessageVersion,
181    message_validation_kind: MessageValidationKind,
182    timestamp: u64,
183    has_block_access_list: bool,
184) -> Result<(), EngineObjectValidationError> {
185    let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
186
187    match version {
188        EngineApiMessageVersion::V1 |
189        EngineApiMessageVersion::V2 |
190        EngineApiMessageVersion::V3 |
191        EngineApiMessageVersion::V4 |
192        EngineApiMessageVersion::V5 => {
193            if has_block_access_list {
194                return Err(message_validation_kind
195                    .to_error(VersionSpecificValidationError::BlockAccessListNotSupportedBeforeV6))
196            }
197        }
198
199        EngineApiMessageVersion::V6 => {
200            if is_amsterdam_active && !has_block_access_list {
201                return Err(message_validation_kind
202                    .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
203            }
204            if !is_amsterdam_active && has_block_access_list {
205                return Err(message_validation_kind
206                    .to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
207            }
208        }
209    };
210
211    Ok(())
212}
213
214/// Validates the presence of the `slot number` field according to the payload timestamp.
215/// After Amsterdam, slot number field must be [Some].
216/// Before Amsterdam, slot number field must be [None];
217pub fn validate_slot_number_presence<T: EthereumHardforks>(
218    chain_spec: &T,
219    version: EngineApiMessageVersion,
220    message_validation_kind: MessageValidationKind,
221    timestamp: u64,
222    has_slot_number: bool,
223) -> Result<(), EngineObjectValidationError> {
224    let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
225
226    match version {
227        EngineApiMessageVersion::V1 |
228        EngineApiMessageVersion::V2 |
229        EngineApiMessageVersion::V3 |
230        EngineApiMessageVersion::V4 |
231        EngineApiMessageVersion::V5 => {
232            if has_slot_number {
233                return Err(message_validation_kind
234                    .to_error(VersionSpecificValidationError::SlotNumberNotSupportedBeforeV6))
235            }
236        }
237
238        EngineApiMessageVersion::V6 => {
239            if is_amsterdam_active && !has_slot_number {
240                return Err(message_validation_kind
241                    .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
242            }
243            if !is_amsterdam_active && has_slot_number {
244                return Err(message_validation_kind
245                    .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
246            }
247        }
248    };
249
250    Ok(())
251}
252
253/// Validates the presence of the `withdrawals` field according to the payload timestamp.
254/// After Shanghai, withdrawals field must be [Some].
255/// Before Shanghai, withdrawals field must be [None];
256pub fn validate_withdrawals_presence<T: EthereumHardforks>(
257    chain_spec: &T,
258    version: EngineApiMessageVersion,
259    message_validation_kind: MessageValidationKind,
260    timestamp: u64,
261    has_withdrawals: bool,
262) -> Result<(), EngineObjectValidationError> {
263    let is_shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
264
265    match version {
266        EngineApiMessageVersion::V1 => {
267            if has_withdrawals {
268                return Err(message_validation_kind
269                    .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1))
270            }
271        }
272        EngineApiMessageVersion::V2 |
273        EngineApiMessageVersion::V3 |
274        EngineApiMessageVersion::V4 |
275        EngineApiMessageVersion::V5 |
276        EngineApiMessageVersion::V6 => {
277            if is_shanghai_active && !has_withdrawals {
278                return Err(message_validation_kind
279                    .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
280            }
281            if !is_shanghai_active && has_withdrawals {
282                return Err(message_validation_kind
283                    .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai))
284            }
285        }
286    };
287
288    Ok(())
289}
290
291/// Validate the presence of the `parentBeaconBlockRoot` field according to the given timestamp.
292/// This method is meant to be used with either a `payloadAttributes` field or a full payload, with
293/// the `engine_forkchoiceUpdated` and `engine_newPayload` methods respectively.
294///
295/// After Cancun, the `parentBeaconBlockRoot` field must be [Some].
296/// Before Cancun, the `parentBeaconBlockRoot` field must be [None].
297///
298/// If the engine API message version is V1 or V2, and the timestamp is post-Cancun, then this will
299/// return [`EngineObjectValidationError::UnsupportedFork`].
300///
301/// If the timestamp is before the Cancun fork and the engine API message version is V3, then this
302/// will return [`EngineObjectValidationError::UnsupportedFork`].
303///
304/// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then
305/// this will return [`VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun`].
306///
307/// This implements the following Engine API spec rules:
308///
309/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
310///    matches the expected one and return `-32602: Invalid params` error if this check fails. Any
311///    field having `null` value **MUST** be considered as not provided.
312///
313/// For `engine_forkchoiceUpdatedV3`:
314///
315/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
316///    matches the expected one and return `-32602: Invalid params` error if this check fails. Any
317///    field having `null` value **MUST** be considered as not provided.
318///
319/// 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the following
320///    sequence of checks that **MUST** be run over `payloadAttributes`:
321///     1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003: Invalid
322///        payload attributes` on failure.
323///     2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
324///        `-38005: Unsupported fork` on failure.
325///     3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
326///        `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on failure.
327///     4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
328///        back.
329///
330/// For `engine_newPayloadV3`:
331///
332/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
333///    payload does not fall within the time frame of the Cancun fork.
334///
335/// For `engine_newPayloadV4`:
336///
337/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
338///    payload does not fall within the time frame of the Prague fork.
339///
340/// Returning the right error code (ie, if the client should return `-38003: Invalid payload
341/// attributes` is handled by the `message_validation_kind` parameter. If the parameter is
342/// `MessageValidationKind::Payload`, then the error code will be `-32602: Invalid params`. If the
343/// parameter is `MessageValidationKind::PayloadAttributes`, then the error code will be `-38003:
344/// Invalid payload attributes`.
345pub fn validate_parent_beacon_block_root_presence<T: EthereumHardforks>(
346    chain_spec: &T,
347    version: EngineApiMessageVersion,
348    validation_kind: MessageValidationKind,
349    timestamp: u64,
350    has_parent_beacon_block_root: bool,
351) -> Result<(), EngineObjectValidationError> {
352    // 1. Client software **MUST** check that provided set of parameters and their fields strictly
353    //    matches the expected one and return `-32602: Invalid params` error if this check fails.
354    //    Any field having `null` value **MUST** be considered as not provided.
355    //
356    // For `engine_forkchoiceUpdatedV3`:
357    //
358    // 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the
359    //    following sequence of checks that **MUST** be run over `payloadAttributes`:
360    //     1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003:
361    //        Invalid payload attributes` on failure.
362    //     2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
363    //        `-38005: Unsupported fork` on failure.
364    //     3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
365    //        `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on
366    //        failure.
367    //     4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
368    //        back.
369    match version {
370        EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => {
371            if has_parent_beacon_block_root {
372                return Err(validation_kind.to_error(
373                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
374                ))
375            }
376        }
377        EngineApiMessageVersion::V3 |
378        EngineApiMessageVersion::V4 |
379        EngineApiMessageVersion::V5 |
380        EngineApiMessageVersion::V6 => {
381            if !has_parent_beacon_block_root {
382                return Err(validation_kind
383                    .to_error(VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun))
384            }
385        }
386    };
387
388    // For `engine_forkchoiceUpdatedV3`:
389    //
390    // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
391    //    `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the
392    //    time frame of the Cancun fork.
393    //
394    // For `engine_newPayloadV3`:
395    //
396    // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
397    //    payload does not fall within the time frame of the Cancun fork.
398    validate_payload_timestamp(chain_spec, version, timestamp, validation_kind)?;
399
400    Ok(())
401}
402
403/// A type that represents whether or not we are validating a payload or payload attributes.
404///
405/// This is used to ensure that the correct error code is returned when validating the payload or
406/// payload attributes.
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408pub enum MessageValidationKind {
409    /// We are validating fields of a payload attributes.
410    /// This corresponds to `engine_forkchoiceUpdated`.
411    PayloadAttributes,
412    /// We are validating fields of a payload.
413    /// This corresponds to `engine_newPayload`.
414    Payload,
415    /// We are validating a built payload.
416    /// This corresponds to `engine_getPayload`.
417    GetPayload,
418}
419
420impl MessageValidationKind {
421    /// Returns an `EngineObjectValidationError` based on the given
422    /// `VersionSpecificValidationError` and the current validation kind.
423    pub const fn to_error(
424        self,
425        error: VersionSpecificValidationError,
426    ) -> EngineObjectValidationError {
427        match self {
428            // Both NewPayload and GetPayload errors are treated as generic Payload validation
429            // errors
430            Self::Payload | Self::GetPayload => EngineObjectValidationError::Payload(error),
431            Self::PayloadAttributes => EngineObjectValidationError::PayloadAttributes(error),
432        }
433    }
434}
435
436/// Validates the presence or exclusion of fork-specific fields based on the ethereum execution
437/// payload, or payload attributes, and the message version.
438///
439/// The object being validated is provided by the [`PayloadOrAttributes`] argument, which can be
440/// either an execution payload, or payload attributes.
441///
442/// The version is provided by the [`EngineApiMessageVersion`] argument.
443pub fn validate_version_specific_fields<Payload, Type, T>(
444    chain_spec: &T,
445    version: EngineApiMessageVersion,
446    payload_or_attrs: PayloadOrAttributes<'_, Payload, Type>,
447) -> Result<(), EngineObjectValidationError>
448where
449    Payload: ExecutionPayload,
450    Type: PayloadAttributes,
451    T: EthereumHardforks,
452{
453    // BAL only exists in ExecutionPayload, not PayloadAttributes (EIP-7928)
454    if let PayloadOrAttributes::ExecutionPayload(_) = payload_or_attrs {
455        validate_block_access_list_presence(
456            chain_spec,
457            version,
458            payload_or_attrs.message_validation_kind(),
459            payload_or_attrs.timestamp(),
460            payload_or_attrs.block_access_list().is_some(),
461        )?;
462    }
463
464    validate_slot_number_presence(
465        chain_spec,
466        version,
467        payload_or_attrs.message_validation_kind(),
468        payload_or_attrs.timestamp(),
469        payload_or_attrs.slot_number().is_some(),
470    )?;
471
472    validate_withdrawals_presence(
473        chain_spec,
474        version,
475        payload_or_attrs.message_validation_kind(),
476        payload_or_attrs.timestamp(),
477        payload_or_attrs.withdrawals().is_some(),
478    )?;
479    validate_parent_beacon_block_root_presence(
480        chain_spec,
481        version,
482        payload_or_attrs.message_validation_kind(),
483        payload_or_attrs.timestamp(),
484        payload_or_attrs.parent_beacon_block_root().is_some(),
485    )
486}
487
488/// The version of Engine API message.
489#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
490pub enum EngineApiMessageVersion {
491    /// Version 1
492    V1 = 1,
493    /// Version 2
494    ///
495    /// Added in the Shanghai hardfork.
496    V2 = 2,
497    /// Version 3
498    ///
499    /// Added in the Cancun hardfork.
500    V3 = 3,
501    /// Version 4
502    ///
503    /// Added in the Prague hardfork.
504    #[default]
505    V4 = 4,
506    /// Version 5
507    ///
508    /// Added in the Osaka hardfork.
509    V5 = 5,
510    /// Version 6
511    ///
512    /// Added in the Amsterdam hardfork.
513    V6 = 6,
514}
515
516impl EngineApiMessageVersion {
517    /// Returns true if the version is V1.
518    pub const fn is_v1(&self) -> bool {
519        matches!(self, Self::V1)
520    }
521
522    /// Returns true if the version is V2.
523    pub const fn is_v2(&self) -> bool {
524        matches!(self, Self::V2)
525    }
526
527    /// Returns true if the version is V3.
528    pub const fn is_v3(&self) -> bool {
529        matches!(self, Self::V3)
530    }
531
532    /// Returns true if the version is V4.
533    pub const fn is_v4(&self) -> bool {
534        matches!(self, Self::V4)
535    }
536
537    /// Returns true if the version is V5.
538    pub const fn is_v5(&self) -> bool {
539        matches!(self, Self::V5)
540    }
541
542    /// Returns true if the version is V6.
543    pub const fn is_v6(&self) -> bool {
544        matches!(self, Self::V6)
545    }
546
547    /// Returns the method name for the given version.
548    pub const fn method_name(&self) -> &'static str {
549        match self {
550            Self::V1 => "engine_newPayloadV1",
551            Self::V2 => "engine_newPayloadV2",
552            Self::V3 => "engine_newPayloadV3",
553            Self::V4 => "engine_newPayloadV4",
554            Self::V5 | Self::V6 => "engine_newPayloadV5",
555        }
556    }
557}
558
559/// Determines how we should choose the payload to return.
560#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
561pub enum PayloadKind {
562    /// Returns the next best available payload (the earliest available payload).
563    /// This does not wait for a real for pending job to finish if there's no best payload yet and
564    /// is allowed to race various payload jobs (empty, pending best) against each other and
565    /// returns whichever job finishes faster.
566    ///
567    /// This should be used when it's more important to return a valid payload as fast as possible.
568    /// For example, the engine API timeout for `engine_getPayload` is 1s and clients should rather
569    /// return an empty payload than indefinitely waiting for the pending payload job to finish and
570    /// risk missing the deadline.
571    #[default]
572    Earliest,
573    /// Only returns once we have at least one built payload.
574    ///
575    /// Compared to [`PayloadKind::Earliest`] this does not race an empty payload job against the
576    /// already in progress one, and returns the best available built payload or awaits the job in
577    /// progress.
578    WaitForPending,
579}
580
581/// Validates that execution requests are valid according to Engine API specification.
582///
583/// `executionRequests`: `Array of DATA` - List of execution layer triggered requests. Each list
584/// element is a `requests` byte array as defined by [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685).
585/// The first byte of each element is the `request_type` and the remaining bytes are the
586/// `request_data`. Elements of the list **MUST** be ordered by `request_type` in ascending order.
587/// Elements with empty `request_data` **MUST** be excluded from the list. If any element is out of
588/// order, has a length of 1-byte or shorter, or more than one element has the same type byte,
589/// client software **MUST** return `-32602: Invalid params` error.
590pub fn validate_execution_requests(requests: &[Bytes]) -> Result<(), EngineObjectValidationError> {
591    let mut last_request_type = None;
592    for request in requests {
593        if request.len() <= 1 {
594            return Err(EngineObjectValidationError::InvalidParams("EmptyExecutionRequest".into()))
595        }
596
597        let request_type = request[0];
598        if Some(request_type) < last_request_type {
599            return Err(EngineObjectValidationError::InvalidParams(
600                "OutOfOrderExecutionRequest".into(),
601            ))
602        }
603
604        if Some(request_type) == last_request_type {
605            return Err(EngineObjectValidationError::InvalidParams(
606                "DuplicatedExecutionRequestType".into(),
607            ))
608        }
609
610        last_request_type = Some(request_type);
611    }
612    Ok(())
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use assert_matches::assert_matches;
619    use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition};
620
621    #[test]
622    fn version_ord() {
623        assert!(EngineApiMessageVersion::V4 > EngineApiMessageVersion::V3);
624    }
625
626    #[test]
627    fn validate_osaka_get_payload_restrictions() {
628        // Osaka activates at timestamp 1000
629        let osaka_activation = 1000;
630        let chain_spec = ChainSpecBuilder::mainnet()
631            .with_fork(EthereumHardfork::Prague, ForkCondition::Timestamp(0))
632            .with_fork(EthereumHardfork::Osaka, ForkCondition::Timestamp(osaka_activation))
633            .build();
634
635        // Osaka is Active + V4 + GetPayload
636        let res = validate_payload_timestamp(
637            &chain_spec,
638            EngineApiMessageVersion::V4,
639            osaka_activation,
640            MessageValidationKind::GetPayload,
641        );
642        assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
643
644        // Osaka is Active + V4 + Payload (NewPayload)
645        let res = validate_payload_timestamp(
646            &chain_spec,
647            EngineApiMessageVersion::V4,
648            osaka_activation,
649            MessageValidationKind::Payload,
650        );
651        assert_matches!(res, Ok(()));
652    }
653
654    #[test]
655    fn execution_requests_validation() {
656        assert_matches!(validate_execution_requests(&[]), Ok(()));
657
658        let valid_requests = [
659            Bytes::from_iter([1, 2]),
660            Bytes::from_iter([2, 3]),
661            Bytes::from_iter([3, 4]),
662            Bytes::from_iter([4, 5]),
663        ];
664        assert_matches!(validate_execution_requests(&valid_requests), Ok(()));
665
666        let requests_with_empty = [
667            Bytes::from_iter([1, 2]),
668            Bytes::from_iter([2, 3]),
669            Bytes::new(),
670            Bytes::from_iter([3, 4]),
671        ];
672        assert_matches!(
673            validate_execution_requests(&requests_with_empty),
674            Err(EngineObjectValidationError::InvalidParams(_))
675        );
676
677        let mut requests_valid_reversed = valid_requests;
678        requests_valid_reversed.reverse();
679        assert_matches!(
680            validate_execution_requests(&requests_valid_reversed),
681            Err(EngineObjectValidationError::InvalidParams(_))
682        );
683
684        let requests_out_of_order = [
685            Bytes::from_iter([1, 2]),
686            Bytes::from_iter([2, 3]),
687            Bytes::from_iter([4, 5]),
688            Bytes::from_iter([3, 4]),
689        ];
690        assert_matches!(
691            validate_execution_requests(&requests_out_of_order),
692            Err(EngineObjectValidationError::InvalidParams(_))
693        );
694
695        let duplicate_request_types = [
696            Bytes::from_iter([1, 2]),
697            Bytes::from_iter([3, 3]),
698            Bytes::from_iter([4, 5]),
699            Bytes::from_iter([4, 4]),
700        ];
701        assert_matches!(
702            validate_execution_requests(&duplicate_request_types),
703            Err(EngineObjectValidationError::InvalidParams(_))
704        );
705    }
706}