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