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 + From<Self::BuiltPayload>;
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 bal: Option<Bytes>,
57 ) -> Self::ExecutionData;
58}
59
60/// Validates the timestamp depending on the version called:
61///
62/// * If V2, this ensures that the payload timestamp is pre-Cancun.
63/// * If V3, this ensures that the payload timestamp is within the Cancun timestamp.
64/// * If V4, this ensures that the payload timestamp is within the Prague timestamp, and within the
65/// Amsterdam timestamp for `engine_forkchoiceUpdatedV4` payload attributes.
66/// * If V5, this ensures that the payload timestamp is within the Osaka timestamp, within the
67/// Amsterdam timestamp for `engine_newPayloadV5` payloads, and within the Bogota timestamp for
68/// `engine_forkchoiceUpdatedV5` payload attributes.
69/// * If V6, this ensures that the payload timestamp is within the Amsterdam timestamp, and within
70/// the Bogota timestamp for `engine_newPayloadV6` payloads.
71///
72/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload and that
73/// staggered endpoint upgrades reject the next fork once a newer method version is required.
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 kind: MessageValidationKind,
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 let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
162
163 if !is_amsterdam &&
164 matches!(
165 (version, kind),
166 (EngineApiMessageVersion::V4, MessageValidationKind::PayloadAttributes) |
167 (EngineApiMessageVersion::V5, MessageValidationKind::Payload)
168 )
169 {
170 // From the Engine API spec:
171 // <https://github.com/ethereum/execution-apis/blob/main/src/engine/amsterdam.md>
172 //
173 // For `engine_newPayloadV5`:
174 //
175 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
176 // payload does not fall within the time frame of the Amsterdam fork.
177 //
178 // For `engine_forkchoiceUpdatedV4`:
179 //
180 // 2. payloadAttributes.timestamp does not fall within the time frame of the Amsterdam fork,
181 // return -38005: Unsupported fork on failure.
182 //
183 // `engine_getPayloadV5` is the Osaka method and is not subject to this rule.
184 return Err(EngineObjectValidationError::UnsupportedFork)
185 }
186
187 // Staggered endpoint upgrades must reject Amsterdam payloads until the Amsterdam-specific
188 // method version is used.
189 if is_amsterdam &&
190 matches!(
191 (version, kind),
192 (EngineApiMessageVersion::V3, MessageValidationKind::PayloadAttributes) |
193 (EngineApiMessageVersion::V4, MessageValidationKind::Payload) |
194 (EngineApiMessageVersion::V5, MessageValidationKind::GetPayload)
195 )
196 {
197 return Err(EngineObjectValidationError::UnsupportedFork)
198 }
199
200 // `engine_getPayloadV4` MUST reject payloads with a timestamp >= Osaka.
201 if version.is_v4() && kind == MessageValidationKind::GetPayload && is_osaka {
202 return Err(EngineObjectValidationError::UnsupportedFork)
203 }
204
205 if version.is_v6() && !is_amsterdam {
206 // From the Engine API spec:
207 // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
208 //
209 // For `engine_getPayloadV6`
210 //
211 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
212 // built payload does not fall within the time frame of the Amsterdam fork.
213
214 return Err(EngineObjectValidationError::UnsupportedFork)
215 }
216
217 let is_bogota = chain_spec.is_bogota_active_at_timestamp(timestamp);
218
219 // Staggered endpoint upgrades must reject Bogota payloads until the Bogota-specific method
220 // version is used.
221 //
222 // From the Engine API spec:
223 // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#update-the-methods-of-previous-forks>
224 //
225 // For `engine_newPayloadV5` and `engine_forkchoiceUpdatedV4`:
226 //
227 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of payload is
228 // greater than or equal to the Bogota activation timestamp.
229 if is_bogota &&
230 matches!(
231 (version, kind),
232 (EngineApiMessageVersion::V4, MessageValidationKind::PayloadAttributes) |
233 (EngineApiMessageVersion::V5, MessageValidationKind::Payload)
234 )
235 {
236 return Err(EngineObjectValidationError::UnsupportedFork)
237 }
238
239 if !is_bogota &&
240 matches!(
241 (version, kind),
242 (EngineApiMessageVersion::V5, MessageValidationKind::PayloadAttributes) |
243 (EngineApiMessageVersion::V6, MessageValidationKind::Payload)
244 )
245 {
246 // From the Engine API spec:
247 // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md>
248 //
249 // For `engine_newPayloadV6`:
250 //
251 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
252 // payload does not fall within the time frame of the Bogota fork.
253 //
254 // For `engine_forkchoiceUpdatedV5`:
255 //
256 // 2. Client software MUST return -38005: Unsupported fork error if the payloadAttributes is
257 // set and the payloadAttributes.timestamp does not fall within the time frame of the
258 // Bogota fork.
259 return Err(EngineObjectValidationError::UnsupportedFork)
260 }
261
262 Ok(())
263}
264
265/// Validates the presence of the `block access list` field for the engine method version.
266///
267/// `engine_newPayloadV5` and `engine_newPayloadV6` take an `ExecutionPayloadV4`, so the field must
268/// be [Some]; every other method must leave it [None]. Presence does not depend on the payload
269/// timestamp, so a well-formed payload outside the method's fork reaches the fork check in
270/// [`validate_payload_timestamp`] and fails with `-38005: Unsupported fork`.
271pub fn validate_block_access_list_presence(
272 version: EngineApiMessageVersion,
273 message_validation_kind: MessageValidationKind,
274 has_block_access_list: bool,
275) -> Result<(), EngineObjectValidationError> {
276 match version {
277 EngineApiMessageVersion::V1 |
278 EngineApiMessageVersion::V2 |
279 EngineApiMessageVersion::V3 |
280 EngineApiMessageVersion::V4 => {
281 if has_block_access_list {
282 return Err(message_validation_kind
283 .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
284 }
285 }
286
287 EngineApiMessageVersion::V5 => {
288 if message_validation_kind == MessageValidationKind::Payload {
289 if !has_block_access_list {
290 return Err(message_validation_kind
291 .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
292 }
293 } else if has_block_access_list {
294 return Err(message_validation_kind
295 .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
296 }
297 }
298
299 EngineApiMessageVersion::V6 => {
300 if !has_block_access_list {
301 return Err(message_validation_kind
302 .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
303 }
304 }
305 };
306
307 Ok(())
308}
309
310/// Validates the presence of the `slot number` field for the engine method version.
311///
312/// `engine_newPayloadV5`, `engine_newPayloadV6`, `engine_forkchoiceUpdatedV4` and
313/// `engine_forkchoiceUpdatedV5` carry the field, so it must be [Some] for them; every other method
314/// must leave it [None]. Presence does not depend on the payload timestamp, so a well-formed
315/// request outside the method's fork reaches the fork check in [`validate_payload_timestamp`] and
316/// fails with `-38005: Unsupported fork`.
317pub fn validate_slot_number_presence(
318 version: EngineApiMessageVersion,
319 message_validation_kind: MessageValidationKind,
320 has_slot_number: bool,
321) -> Result<(), EngineObjectValidationError> {
322 match version {
323 EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => {
324 if has_slot_number {
325 return Err(message_validation_kind
326 .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
327 }
328 }
329
330 EngineApiMessageVersion::V4 => {
331 if message_validation_kind == MessageValidationKind::PayloadAttributes {
332 if !has_slot_number {
333 return Err(message_validation_kind
334 .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
335 }
336 } else if has_slot_number {
337 return Err(message_validation_kind
338 .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
339 }
340 }
341
342 EngineApiMessageVersion::V5 => {
343 if matches!(
344 message_validation_kind,
345 MessageValidationKind::Payload | MessageValidationKind::PayloadAttributes
346 ) {
347 if !has_slot_number {
348 return Err(message_validation_kind
349 .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
350 }
351 } else if has_slot_number {
352 return Err(message_validation_kind
353 .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
354 }
355 }
356
357 EngineApiMessageVersion::V6 => {
358 if !has_slot_number {
359 return Err(message_validation_kind
360 .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
361 }
362 }
363 };
364
365 Ok(())
366}
367
368/// Validates the presence of `withdrawals` for the engine method version.
369///
370/// V1 forbids withdrawals. V2 supports both Paris and Shanghai payloads, so withdrawals must be
371/// present exactly when Shanghai is active. V3 and later require withdrawals regardless of the
372/// timestamp, allowing complete requests outside the method's fork to reach the timeframe check.
373pub fn validate_withdrawals_presence<T: EthereumHardforks>(
374 chain_spec: &T,
375 version: EngineApiMessageVersion,
376 message_validation_kind: MessageValidationKind,
377 timestamp: u64,
378 has_withdrawals: bool,
379) -> Result<(), EngineObjectValidationError> {
380 match version {
381 EngineApiMessageVersion::V1 => {
382 if has_withdrawals {
383 return Err(message_validation_kind
384 .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1))
385 }
386 }
387 EngineApiMessageVersion::V2 => {
388 let is_shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
389 if is_shanghai_active && !has_withdrawals {
390 return Err(message_validation_kind
391 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
392 }
393 if !is_shanghai_active && has_withdrawals {
394 return Err(message_validation_kind
395 .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai))
396 }
397 }
398 EngineApiMessageVersion::V3 |
399 EngineApiMessageVersion::V4 |
400 EngineApiMessageVersion::V5 |
401 EngineApiMessageVersion::V6 => {
402 if !has_withdrawals {
403 return Err(message_validation_kind
404 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
405 }
406 }
407 };
408
409 Ok(())
410}
411
412/// Validate the presence of the `parentBeaconBlockRoot` field according to the given timestamp.
413/// This method is meant to be used with either a `payloadAttributes` field or a full payload, with
414/// the `engine_forkchoiceUpdated` and `engine_newPayload` methods respectively.
415///
416/// After Cancun, the `parentBeaconBlockRoot` field must be [Some].
417/// Before Cancun, the `parentBeaconBlockRoot` field must be [None].
418///
419/// If the engine API message version is V1 or V2, and the timestamp is post-Cancun, then this will
420/// return [`EngineObjectValidationError::UnsupportedFork`].
421///
422/// If the timestamp is before the Cancun fork and the engine API message version is V3, then this
423/// will return [`EngineObjectValidationError::UnsupportedFork`].
424///
425/// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then
426/// this will return [`VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun`].
427///
428/// This implements the following Engine API spec rules:
429///
430/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
431/// matches the expected one and return `-32602: Invalid params` error if this check fails. Any
432/// field having `null` value **MUST** be considered as not provided.
433///
434/// For `engine_forkchoiceUpdatedV3`:
435///
436/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
437/// matches the expected one and return `-32602: Invalid params` error if this check fails. Any
438/// field having `null` value **MUST** be considered as not provided.
439///
440/// 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the following
441/// sequence of checks that **MUST** be run over `payloadAttributes`:
442/// 1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003: Invalid
443/// payload attributes` on failure.
444/// 2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
445/// `-38005: Unsupported fork` on failure.
446/// 3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
447/// `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on failure.
448/// 4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
449/// back.
450///
451/// For `engine_newPayloadV3`:
452///
453/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
454/// payload does not fall within the time frame of the Cancun fork.
455///
456/// For `engine_newPayloadV4`:
457///
458/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
459/// payload does not fall within the time frame of the Prague fork.
460///
461/// Returning the right error code (ie, if the client should return `-38003: Invalid payload
462/// attributes` is handled by the `message_validation_kind` parameter. If the parameter is
463/// `MessageValidationKind::Payload`, then the error code will be `-32602: Invalid params`. If the
464/// parameter is `MessageValidationKind::PayloadAttributes`, then the error code will be `-38003:
465/// Invalid payload attributes`.
466pub fn validate_parent_beacon_block_root_presence<T: EthereumHardforks>(
467 chain_spec: &T,
468 version: EngineApiMessageVersion,
469 validation_kind: MessageValidationKind,
470 timestamp: u64,
471 has_parent_beacon_block_root: bool,
472) -> Result<(), EngineObjectValidationError> {
473 // 1. Client software **MUST** check that provided set of parameters and their fields strictly
474 // matches the expected one and return `-32602: Invalid params` error if this check fails.
475 // Any field having `null` value **MUST** be considered as not provided.
476 //
477 // For `engine_forkchoiceUpdatedV3`:
478 //
479 // 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the
480 // following sequence of checks that **MUST** be run over `payloadAttributes`:
481 // 1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003:
482 // Invalid payload attributes` on failure.
483 // 2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
484 // `-38005: Unsupported fork` on failure.
485 // 3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
486 // `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on
487 // failure.
488 // 4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
489 // back.
490 match version {
491 EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => {
492 if has_parent_beacon_block_root {
493 return Err(validation_kind.to_error(
494 VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
495 ))
496 }
497 }
498 EngineApiMessageVersion::V3 |
499 EngineApiMessageVersion::V4 |
500 EngineApiMessageVersion::V5 |
501 EngineApiMessageVersion::V6 => {
502 if !has_parent_beacon_block_root {
503 return Err(validation_kind
504 .to_error(VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun))
505 }
506 }
507 };
508
509 // For `engine_forkchoiceUpdatedV3`:
510 //
511 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
512 // `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the
513 // time frame of the Cancun fork.
514 //
515 // For `engine_newPayloadV3`:
516 //
517 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
518 // payload does not fall within the time frame of the Cancun fork.
519 validate_payload_timestamp(chain_spec, version, timestamp, validation_kind)?;
520
521 Ok(())
522}
523
524/// A type that represents whether or not we are validating a payload or payload attributes.
525///
526/// This is used to ensure that the correct error code is returned when validating the payload or
527/// payload attributes.
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum MessageValidationKind {
530 /// We are validating fields of a payload attributes.
531 /// This corresponds to `engine_forkchoiceUpdated`.
532 PayloadAttributes,
533 /// We are validating fields of a payload.
534 /// This corresponds to `engine_newPayload`.
535 Payload,
536 /// We are validating a built payload.
537 /// This corresponds to `engine_getPayload`.
538 GetPayload,
539}
540
541impl MessageValidationKind {
542 /// Returns an `EngineObjectValidationError` based on the given
543 /// `VersionSpecificValidationError` and the current validation kind.
544 pub const fn to_error(
545 self,
546 error: VersionSpecificValidationError,
547 ) -> EngineObjectValidationError {
548 match self {
549 // Both NewPayload and GetPayload errors are treated as generic Payload validation
550 // errors
551 Self::Payload | Self::GetPayload => EngineObjectValidationError::Payload(error),
552 Self::PayloadAttributes => EngineObjectValidationError::PayloadAttributes(error),
553 }
554 }
555}
556
557/// Validates the presence or exclusion of fork-specific fields based on the ethereum execution
558/// payload, or payload attributes, and the message version.
559///
560/// The object being validated is provided by the [`PayloadOrAttributes`] argument, which can be
561/// either an execution payload, or payload attributes.
562///
563/// The version is provided by the [`EngineApiMessageVersion`] argument.
564pub fn validate_version_specific_fields<Payload, Type, T>(
565 chain_spec: &T,
566 version: EngineApiMessageVersion,
567 payload_or_attrs: PayloadOrAttributes<'_, Payload, Type>,
568) -> Result<(), EngineObjectValidationError>
569where
570 Payload: ExecutionPayload,
571 Type: PayloadAttributes,
572 T: EthereumHardforks,
573{
574 // BAL only exists in ExecutionPayload, not PayloadAttributes (EIP-7928)
575 if let PayloadOrAttributes::ExecutionPayload(_) = payload_or_attrs {
576 validate_block_access_list_presence(
577 version,
578 payload_or_attrs.message_validation_kind(),
579 payload_or_attrs.block_access_list().is_some(),
580 )?;
581 }
582
583 validate_slot_number_presence(
584 version,
585 payload_or_attrs.message_validation_kind(),
586 payload_or_attrs.slot_number().is_some(),
587 )?;
588
589 validate_withdrawals_presence(
590 chain_spec,
591 version,
592 payload_or_attrs.message_validation_kind(),
593 payload_or_attrs.timestamp(),
594 payload_or_attrs.withdrawals().is_some(),
595 )?;
596 validate_parent_beacon_block_root_presence(
597 chain_spec,
598 version,
599 payload_or_attrs.message_validation_kind(),
600 payload_or_attrs.timestamp(),
601 payload_or_attrs.parent_beacon_block_root().is_some(),
602 )
603}
604
605/// The version of Engine API message.
606#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
607pub enum EngineApiMessageVersion {
608 /// Version 1
609 V1 = 1,
610 /// Version 2
611 ///
612 /// Added in the Shanghai hardfork.
613 V2 = 2,
614 /// Version 3
615 ///
616 /// Added in the Cancun hardfork.
617 V3 = 3,
618 /// Version 4
619 ///
620 /// Added in the Prague hardfork.
621 #[default]
622 V4 = 4,
623 /// Version 5
624 ///
625 /// Added in the Osaka hardfork.
626 V5 = 5,
627 /// Version 6
628 ///
629 /// Added in the Amsterdam hardfork.
630 V6 = 6,
631}
632
633impl EngineApiMessageVersion {
634 /// Returns true if the version is V1.
635 pub const fn is_v1(&self) -> bool {
636 matches!(self, Self::V1)
637 }
638
639 /// Returns true if the version is V2.
640 pub const fn is_v2(&self) -> bool {
641 matches!(self, Self::V2)
642 }
643
644 /// Returns true if the version is V3.
645 pub const fn is_v3(&self) -> bool {
646 matches!(self, Self::V3)
647 }
648
649 /// Returns true if the version is V4.
650 pub const fn is_v4(&self) -> bool {
651 matches!(self, Self::V4)
652 }
653
654 /// Returns true if the version is V5.
655 pub const fn is_v5(&self) -> bool {
656 matches!(self, Self::V5)
657 }
658
659 /// Returns true if the version is V6.
660 pub const fn is_v6(&self) -> bool {
661 matches!(self, Self::V6)
662 }
663
664 /// Returns the method name for the given version.
665 pub const fn method_name(&self) -> &'static str {
666 match self {
667 Self::V1 => "engine_newPayloadV1",
668 Self::V2 => "engine_newPayloadV2",
669 Self::V3 => "engine_newPayloadV3",
670 Self::V4 => "engine_newPayloadV4",
671 Self::V5 | Self::V6 => "engine_newPayloadV5",
672 }
673 }
674}
675
676/// Determines how we should choose the payload to return.
677#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
678pub enum PayloadKind {
679 /// Returns the next best available payload (the earliest available payload).
680 /// This does not wait for a real for pending job to finish if there's no best payload yet and
681 /// is allowed to race various payload jobs (empty, pending best) against each other and
682 /// returns whichever job finishes faster.
683 ///
684 /// This should be used when it's more important to return a valid payload as fast as possible.
685 /// For example, the engine API timeout for `engine_getPayload` is 1s and clients should rather
686 /// return an empty payload than indefinitely waiting for the pending payload job to finish and
687 /// risk missing the deadline.
688 #[default]
689 Earliest,
690 /// Only returns once we have at least one built payload.
691 ///
692 /// Compared to [`PayloadKind::Earliest`] this does not race an empty payload job against the
693 /// already in progress one, and returns the best available built payload or awaits the job in
694 /// progress.
695 WaitForPending,
696}
697
698/// Validates that execution requests are valid according to Engine API specification.
699///
700/// `executionRequests`: `Array of DATA` - List of execution layer triggered requests. Each list
701/// element is a `requests` byte array as defined by [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685).
702/// The first byte of each element is the `request_type` and the remaining bytes are the
703/// `request_data`. Elements of the list **MUST** be ordered by `request_type` in ascending order.
704/// Elements with empty `request_data` **MUST** be excluded from the list. If any element is out of
705/// order, has a length of 1-byte or shorter, or more than one element has the same type byte,
706/// client software **MUST** return `-32602: Invalid params` error.
707pub fn validate_execution_requests(requests: &[Bytes]) -> Result<(), EngineObjectValidationError> {
708 let mut last_request_type = None;
709 for request in requests {
710 if request.len() <= 1 {
711 return Err(EngineObjectValidationError::InvalidParams("EmptyExecutionRequest".into()))
712 }
713
714 let request_type = request[0];
715 if Some(request_type) < last_request_type {
716 return Err(EngineObjectValidationError::InvalidParams(
717 "OutOfOrderExecutionRequest".into(),
718 ))
719 }
720
721 if Some(request_type) == last_request_type {
722 return Err(EngineObjectValidationError::InvalidParams(
723 "DuplicatedExecutionRequestType".into(),
724 ))
725 }
726
727 last_request_type = Some(request_type);
728 }
729 Ok(())
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use assert_matches::assert_matches;
736 use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition};
737
738 #[test]
739 fn version_ord() {
740 assert!(EngineApiMessageVersion::V4 > EngineApiMessageVersion::V3);
741 }
742
743 #[test]
744 fn validate_osaka_get_payload_restrictions() {
745 // Osaka activates at timestamp 1000
746 let osaka_activation = 1000;
747 let chain_spec = ChainSpecBuilder::mainnet()
748 .with_fork(EthereumHardfork::Prague, ForkCondition::Timestamp(0))
749 .with_fork(EthereumHardfork::Osaka, ForkCondition::Timestamp(osaka_activation))
750 .build();
751
752 // Osaka is Active + V4 + GetPayload
753 let res = validate_payload_timestamp(
754 &chain_spec,
755 EngineApiMessageVersion::V4,
756 osaka_activation,
757 MessageValidationKind::GetPayload,
758 );
759 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
760
761 // Osaka is Active + V4 + Payload (NewPayload)
762 let res = validate_payload_timestamp(
763 &chain_spec,
764 EngineApiMessageVersion::V4,
765 osaka_activation,
766 MessageValidationKind::Payload,
767 );
768 assert_matches!(res, Ok(()));
769 }
770
771 #[test]
772 fn validate_amsterdam_staggered_version_restrictions() {
773 let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
774
775 let res = validate_payload_timestamp(
776 &chain_spec,
777 EngineApiMessageVersion::V3,
778 0,
779 MessageValidationKind::PayloadAttributes,
780 );
781 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
782
783 let res = validate_payload_timestamp(
784 &chain_spec,
785 EngineApiMessageVersion::V4,
786 0,
787 MessageValidationKind::Payload,
788 );
789 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
790
791 let res = validate_payload_timestamp(
792 &chain_spec,
793 EngineApiMessageVersion::V5,
794 0,
795 MessageValidationKind::GetPayload,
796 );
797 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
798
799 let res = validate_payload_timestamp(
800 &chain_spec,
801 EngineApiMessageVersion::V6,
802 0,
803 MessageValidationKind::GetPayload,
804 );
805 assert_matches!(res, Ok(()));
806 }
807
808 #[test]
809 fn validate_bogota_fork_timeframe() {
810 // Amsterdam active, Bogota not scheduled: the Bogota-specific methods must reject
811 let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
812
813 // `engine_newPayloadV6` requires a Bogota timestamp
814 let res = validate_payload_timestamp(
815 &chain_spec,
816 EngineApiMessageVersion::V6,
817 0,
818 MessageValidationKind::Payload,
819 );
820 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
821
822 // `engine_forkchoiceUpdatedV5` requires Bogota payload attributes
823 let res = validate_payload_timestamp(
824 &chain_spec,
825 EngineApiMessageVersion::V5,
826 0,
827 MessageValidationKind::PayloadAttributes,
828 );
829 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
830
831 // the Amsterdam methods remain valid pre-Bogota
832 let res = validate_payload_timestamp(
833 &chain_spec,
834 EngineApiMessageVersion::V5,
835 0,
836 MessageValidationKind::Payload,
837 );
838 assert_matches!(res, Ok(()));
839 let res = validate_payload_timestamp(
840 &chain_spec,
841 EngineApiMessageVersion::V4,
842 0,
843 MessageValidationKind::PayloadAttributes,
844 );
845 assert_matches!(res, Ok(()));
846 }
847
848 #[test]
849 fn validate_bogota_staggered_version_restrictions() {
850 let chain_spec = ChainSpecBuilder::mainnet().bogota_activated().build();
851
852 // `engine_newPayloadV5` must reject Bogota payloads
853 let res = validate_payload_timestamp(
854 &chain_spec,
855 EngineApiMessageVersion::V5,
856 0,
857 MessageValidationKind::Payload,
858 );
859 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
860
861 // `engine_forkchoiceUpdatedV4` must reject Bogota payload attributes
862 let res = validate_payload_timestamp(
863 &chain_spec,
864 EngineApiMessageVersion::V4,
865 0,
866 MessageValidationKind::PayloadAttributes,
867 );
868 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
869
870 // the Bogota-specific methods are accepted
871 let res = validate_payload_timestamp(
872 &chain_spec,
873 EngineApiMessageVersion::V6,
874 0,
875 MessageValidationKind::Payload,
876 );
877 assert_matches!(res, Ok(()));
878 let res = validate_payload_timestamp(
879 &chain_spec,
880 EngineApiMessageVersion::V5,
881 0,
882 MessageValidationKind::PayloadAttributes,
883 );
884 assert_matches!(res, Ok(()));
885
886 // Bogota defines no new getPayload version, `engine_getPayloadV6` remains valid
887 let res = validate_payload_timestamp(
888 &chain_spec,
889 EngineApiMessageVersion::V6,
890 0,
891 MessageValidationKind::GetPayload,
892 );
893 assert_matches!(res, Ok(()));
894 }
895
896 #[test]
897 fn validate_amsterdam_slot_and_bal_presence() {
898 let res = validate_slot_number_presence(
899 EngineApiMessageVersion::V4,
900 MessageValidationKind::PayloadAttributes,
901 true,
902 );
903 assert_matches!(res, Ok(()));
904
905 let res = validate_slot_number_presence(
906 EngineApiMessageVersion::V5,
907 MessageValidationKind::Payload,
908 true,
909 );
910 assert_matches!(res, Ok(()));
911
912 let res = validate_slot_number_presence(
913 EngineApiMessageVersion::V5,
914 MessageValidationKind::PayloadAttributes,
915 true,
916 );
917 assert_matches!(res, Ok(()));
918
919 let res = validate_block_access_list_presence(
920 EngineApiMessageVersion::V5,
921 MessageValidationKind::Payload,
922 true,
923 );
924 assert_matches!(res, Ok(()));
925 }
926
927 #[test]
928 fn validate_amsterdam_fork_timeframe() {
929 // Osaka active, Amsterdam not scheduled: the Amsterdam methods must reject.
930 let chain_spec = ChainSpecBuilder::mainnet().osaka_activated().build();
931 assert!(!chain_spec.is_amsterdam_active_at_timestamp(0));
932
933 // `engine_newPayloadV5` requires an Amsterdam timestamp.
934 let res = validate_payload_timestamp(
935 &chain_spec,
936 EngineApiMessageVersion::V5,
937 0,
938 MessageValidationKind::Payload,
939 );
940 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
941
942 // `engine_forkchoiceUpdatedV4` requires Amsterdam payload attributes.
943 let res = validate_payload_timestamp(
944 &chain_spec,
945 EngineApiMessageVersion::V4,
946 0,
947 MessageValidationKind::PayloadAttributes,
948 );
949 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
950
951 // `engine_getPayloadV5` is the Osaka method and remains valid pre-Amsterdam.
952 let res = validate_payload_timestamp(
953 &chain_spec,
954 EngineApiMessageVersion::V5,
955 0,
956 MessageValidationKind::GetPayload,
957 );
958 assert_matches!(res, Ok(()));
959
960 // `engine_newPayloadV4` remains valid pre-Amsterdam.
961 let res = validate_payload_timestamp(
962 &chain_spec,
963 EngineApiMessageVersion::V4,
964 0,
965 MessageValidationKind::Payload,
966 );
967 assert_matches!(res, Ok(()));
968 }
969
970 #[test]
971 fn validate_slot_and_bal_presence_is_structural() {
972 // The presence checks take no timestamp, a well-formed V5 payload passes them and the
973 // fork timeframe check rejects it afterwards.
974 let res = validate_block_access_list_presence(
975 EngineApiMessageVersion::V5,
976 MessageValidationKind::Payload,
977 true,
978 );
979 assert_matches!(res, Ok(()));
980 let res = validate_slot_number_presence(
981 EngineApiMessageVersion::V5,
982 MessageValidationKind::Payload,
983 true,
984 );
985 assert_matches!(res, Ok(()));
986 let res = validate_slot_number_presence(
987 EngineApiMessageVersion::V4,
988 MessageValidationKind::PayloadAttributes,
989 true,
990 );
991 assert_matches!(res, Ok(()));
992
993 // Missing fields are structural errors.
994 let res = validate_block_access_list_presence(
995 EngineApiMessageVersion::V5,
996 MessageValidationKind::Payload,
997 false,
998 );
999 assert_matches!(
1000 res,
1001 Err(EngineObjectValidationError::Payload(
1002 VersionSpecificValidationError::NoBlockAccessListPostAmsterdam
1003 ))
1004 );
1005 let res = validate_block_access_list_presence(
1006 EngineApiMessageVersion::V6,
1007 MessageValidationKind::Payload,
1008 false,
1009 );
1010 assert_matches!(
1011 res,
1012 Err(EngineObjectValidationError::Payload(
1013 VersionSpecificValidationError::NoBlockAccessListPostAmsterdam
1014 ))
1015 );
1016 let res = validate_slot_number_presence(
1017 EngineApiMessageVersion::V5,
1018 MessageValidationKind::Payload,
1019 false,
1020 );
1021 assert_matches!(
1022 res,
1023 Err(EngineObjectValidationError::Payload(
1024 VersionSpecificValidationError::NoSlotNumberPostAmsterdam
1025 ))
1026 );
1027 let res = validate_slot_number_presence(
1028 EngineApiMessageVersion::V4,
1029 MessageValidationKind::PayloadAttributes,
1030 false,
1031 );
1032 assert_matches!(
1033 res,
1034 Err(EngineObjectValidationError::PayloadAttributes(
1035 VersionSpecificValidationError::NoSlotNumberPostAmsterdam
1036 ))
1037 );
1038
1039 // Methods that predate Amsterdam still reject the fields.
1040 let res = validate_block_access_list_presence(
1041 EngineApiMessageVersion::V4,
1042 MessageValidationKind::Payload,
1043 true,
1044 );
1045 assert_matches!(
1046 res,
1047 Err(EngineObjectValidationError::Payload(
1048 VersionSpecificValidationError::BlockAccessListNotSupported
1049 ))
1050 );
1051 let res = validate_slot_number_presence(
1052 EngineApiMessageVersion::V4,
1053 MessageValidationKind::Payload,
1054 true,
1055 );
1056 assert_matches!(
1057 res,
1058 Err(EngineObjectValidationError::Payload(
1059 VersionSpecificValidationError::SlotNumberNotSupported
1060 ))
1061 );
1062 }
1063
1064 #[test]
1065 fn execution_requests_validation() {
1066 assert_matches!(validate_execution_requests(&[]), Ok(()));
1067
1068 let valid_requests = [
1069 Bytes::from_iter([1, 2]),
1070 Bytes::from_iter([2, 3]),
1071 Bytes::from_iter([3, 4]),
1072 Bytes::from_iter([4, 5]),
1073 ];
1074 assert_matches!(validate_execution_requests(&valid_requests), Ok(()));
1075
1076 let requests_with_empty = [
1077 Bytes::from_iter([1, 2]),
1078 Bytes::from_iter([2, 3]),
1079 Bytes::new(),
1080 Bytes::from_iter([3, 4]),
1081 ];
1082 assert_matches!(
1083 validate_execution_requests(&requests_with_empty),
1084 Err(EngineObjectValidationError::InvalidParams(_))
1085 );
1086
1087 let mut requests_valid_reversed = valid_requests;
1088 requests_valid_reversed.reverse();
1089 assert_matches!(
1090 validate_execution_requests(&requests_valid_reversed),
1091 Err(EngineObjectValidationError::InvalidParams(_))
1092 );
1093
1094 let requests_out_of_order = [
1095 Bytes::from_iter([1, 2]),
1096 Bytes::from_iter([2, 3]),
1097 Bytes::from_iter([4, 5]),
1098 Bytes::from_iter([3, 4]),
1099 ];
1100 assert_matches!(
1101 validate_execution_requests(&requests_out_of_order),
1102 Err(EngineObjectValidationError::InvalidParams(_))
1103 );
1104
1105 let duplicate_request_types = [
1106 Bytes::from_iter([1, 2]),
1107 Bytes::from_iter([3, 3]),
1108 Bytes::from_iter([4, 5]),
1109 Bytes::from_iter([4, 4]),
1110 ];
1111 assert_matches!(
1112 validate_execution_requests(&duplicate_request_types),
1113 Err(EngineObjectValidationError::InvalidParams(_))
1114 );
1115 }
1116
1117 #[test]
1118 fn validate_v2_withdrawals_follow_timestamp() {
1119 let chain_spec = ChainSpecBuilder::mainnet()
1120 .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(100))
1121 .build();
1122 for (timestamp, has_withdrawals) in [(99, false), (100, true)] {
1123 assert_matches!(
1124 validate_withdrawals_presence(
1125 &chain_spec,
1126 EngineApiMessageVersion::V2,
1127 MessageValidationKind::Payload,
1128 timestamp,
1129 has_withdrawals,
1130 ),
1131 Ok(())
1132 );
1133 }
1134 assert_matches!(
1135 validate_withdrawals_presence(
1136 &chain_spec,
1137 EngineApiMessageVersion::V2,
1138 MessageValidationKind::Payload,
1139 99,
1140 true,
1141 ),
1142 Err(EngineObjectValidationError::Payload(
1143 VersionSpecificValidationError::HasWithdrawalsPreShanghai
1144 ))
1145 );
1146 assert_matches!(
1147 validate_withdrawals_presence(
1148 &chain_spec,
1149 EngineApiMessageVersion::V2,
1150 MessageValidationKind::Payload,
1151 100,
1152 false,
1153 ),
1154 Err(EngineObjectValidationError::Payload(
1155 VersionSpecificValidationError::NoWithdrawalsPostShanghai
1156 ))
1157 );
1158 }
1159
1160 #[test]
1161 fn validate_v3_and_later_withdrawals_are_structural() {
1162 let chain_spec = ChainSpecBuilder::mainnet()
1163 .with_fork(EthereumHardfork::Shanghai, ForkCondition::Timestamp(100))
1164 .build();
1165 for version in [
1166 EngineApiMessageVersion::V3,
1167 EngineApiMessageVersion::V4,
1168 EngineApiMessageVersion::V5,
1169 EngineApiMessageVersion::V6,
1170 ] {
1171 for timestamp in [99, 100] {
1172 assert_matches!(
1173 validate_withdrawals_presence(
1174 &chain_spec,
1175 version,
1176 MessageValidationKind::Payload,
1177 timestamp,
1178 true,
1179 ),
1180 Ok(())
1181 );
1182 assert_matches!(
1183 validate_withdrawals_presence(
1184 &chain_spec,
1185 version,
1186 MessageValidationKind::Payload,
1187 timestamp,
1188 false,
1189 ),
1190 Err(EngineObjectValidationError::Payload(
1191 VersionSpecificValidationError::NoWithdrawalsPostShanghai
1192 ))
1193 );
1194 }
1195 }
1196 }
1197}