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