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