reth_payload_primitives/
lib.rs

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