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