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}