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