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.
65/// * If V5, this ensures that the payload timestamp is within the Osaka timestamp, and within the
66/// Bogota timestamp for `engine_forkchoiceUpdatedV5` payload attributes.
67/// * If V6, this ensures that the payload timestamp is within the Amsterdam timestamp, and within
68/// the Bogota timestamp for `engine_newPayloadV6` payloads.
69///
70/// Additionally, it ensures that `engine_getPayloadV4` is not used for an Osaka payload and that
71/// staggered endpoint upgrades reject the next fork once a newer method version is required.
72///
73/// Otherwise, this will return [`EngineObjectValidationError::UnsupportedFork`].
74pub fn validate_payload_timestamp(
75 chain_spec: impl EthereumHardforks,
76 version: EngineApiMessageVersion,
77 timestamp: u64,
78 kind: MessageValidationKind,
79) -> Result<(), EngineObjectValidationError> {
80 let is_cancun = chain_spec.is_cancun_active_at_timestamp(timestamp);
81 if version.is_v2() && is_cancun {
82 // From the Engine API spec:
83 //
84 // ### Update the methods of previous forks
85 //
86 // This document defines how Cancun payload should be handled by the [`Shanghai
87 // API`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md).
88 //
89 // For the following methods:
90 //
91 // - [`engine_forkchoiceUpdatedV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_forkchoiceupdatedv2)
92 // - [`engine_newPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_newpayloadV2)
93 // - [`engine_getPayloadV2`](https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/shanghai.md#engine_getpayloadv2)
94 //
95 // a validation **MUST** be added:
96 //
97 // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
98 // payload or payloadAttributes is greater or equal to the Cancun activation timestamp.
99 return Err(EngineObjectValidationError::UnsupportedFork)
100 }
101
102 if version.is_v3() && !is_cancun {
103 // From the Engine API spec:
104 // <https://github.com/ethereum/execution-apis/blob/ff43500e653abde45aec0f545564abfb648317af/src/engine/cancun.md#specification-2>
105 //
106 // For `engine_getPayloadV3`:
107 //
108 // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
109 // the built payload does not fall within the time frame of the Cancun fork.
110 //
111 // For `engine_forkchoiceUpdatedV3`:
112 //
113 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
114 // `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
115 // the time frame of the Cancun fork.
116 //
117 // For `engine_newPayloadV3`:
118 //
119 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
120 // the payload does not fall within the time frame of the Cancun fork.
121 return Err(EngineObjectValidationError::UnsupportedFork)
122 }
123
124 let is_prague = chain_spec.is_prague_active_at_timestamp(timestamp);
125 if version.is_v4() && !is_prague {
126 // From the Engine API spec:
127 // <https://github.com/ethereum/execution-apis/blob/7907424db935b93c2fe6a3c0faab943adebe8557/src/engine/prague.md#specification-1>
128 //
129 // For `engine_getPayloadV4`:
130 //
131 // 1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
132 // the built payload does not fall within the time frame of the Prague fork.
133 //
134 // For `engine_forkchoiceUpdatedV4`:
135 //
136 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
137 // `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within
138 // the time frame of the Prague fork.
139 //
140 // For `engine_newPayloadV4`:
141 //
142 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of
143 // the payload does not fall within the time frame of the Prague fork.
144 return Err(EngineObjectValidationError::UnsupportedFork)
145 }
146
147 let is_osaka = chain_spec.is_osaka_active_at_timestamp(timestamp);
148 if version.is_v5() && !is_osaka {
149 // From the Engine API spec:
150 // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
151 //
152 // For `engine_getPayloadV5`
153 //
154 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
155 // built payload does not fall within the time frame of the Osaka fork.
156 return Err(EngineObjectValidationError::UnsupportedFork)
157 }
158
159 let is_amsterdam = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
160
161 // Staggered endpoint upgrades must reject Amsterdam payloads until the Amsterdam-specific
162 // method version is used.
163 if is_amsterdam &&
164 matches!(
165 (version, kind),
166 (EngineApiMessageVersion::V3, MessageValidationKind::PayloadAttributes) |
167 (EngineApiMessageVersion::V4, MessageValidationKind::Payload) |
168 (EngineApiMessageVersion::V5, MessageValidationKind::GetPayload)
169 )
170 {
171 return Err(EngineObjectValidationError::UnsupportedFork)
172 }
173
174 // `engine_getPayloadV4` MUST reject payloads with a timestamp >= Osaka.
175 if version.is_v4() && kind == MessageValidationKind::GetPayload && is_osaka {
176 return Err(EngineObjectValidationError::UnsupportedFork)
177 }
178
179 if version.is_v6() && !is_amsterdam {
180 // From the Engine API spec:
181 // <https://github.com/ethereum/execution-apis/blob/15399c2e2f16a5f800bf3f285640357e2c245ad9/src/engine/osaka.md#specification>
182 //
183 // For `engine_getPayloadV6`
184 //
185 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
186 // built payload does not fall within the time frame of the Amsterdam fork.
187
188 return Err(EngineObjectValidationError::UnsupportedFork)
189 }
190
191 let is_bogota = chain_spec.is_bogota_active_at_timestamp(timestamp);
192
193 // Staggered endpoint upgrades must reject Bogota payloads until the Bogota-specific method
194 // version is used.
195 //
196 // From the Engine API spec:
197 // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md#update-the-methods-of-previous-forks>
198 //
199 // For `engine_newPayloadV5` and `engine_forkchoiceUpdatedV4`:
200 //
201 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of payload is
202 // greater than or equal to the Bogota activation timestamp.
203 if is_bogota &&
204 matches!(
205 (version, kind),
206 (EngineApiMessageVersion::V4, MessageValidationKind::PayloadAttributes) |
207 (EngineApiMessageVersion::V5, MessageValidationKind::Payload)
208 )
209 {
210 return Err(EngineObjectValidationError::UnsupportedFork)
211 }
212
213 if !is_bogota &&
214 matches!(
215 (version, kind),
216 (EngineApiMessageVersion::V5, MessageValidationKind::PayloadAttributes) |
217 (EngineApiMessageVersion::V6, MessageValidationKind::Payload)
218 )
219 {
220 // From the Engine API spec:
221 // <https://github.com/ethereum/execution-apis/blob/main/src/engine/bogota.md>
222 //
223 // For `engine_newPayloadV6`:
224 //
225 // 1. Client software MUST return -38005: Unsupported fork error if the timestamp of the
226 // payload does not fall within the time frame of the Bogota fork.
227 //
228 // For `engine_forkchoiceUpdatedV5`:
229 //
230 // 2. Client software MUST return -38005: Unsupported fork error if the payloadAttributes is
231 // set and the payloadAttributes.timestamp does not fall within the time frame of the
232 // Bogota fork.
233 return Err(EngineObjectValidationError::UnsupportedFork)
234 }
235
236 Ok(())
237}
238
239/// Validates the presence of the `block access lists` field according to the payload timestamp.
240/// After Amsterdam, block access list field must be [Some].
241/// Before Amsterdam, block access list field must be [None];
242pub fn validate_block_access_list_presence<T: EthereumHardforks>(
243 chain_spec: &T,
244 version: EngineApiMessageVersion,
245 message_validation_kind: MessageValidationKind,
246 timestamp: u64,
247 has_block_access_list: bool,
248) -> Result<(), EngineObjectValidationError> {
249 let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
250 match version {
251 EngineApiMessageVersion::V1 |
252 EngineApiMessageVersion::V2 |
253 EngineApiMessageVersion::V3 |
254 EngineApiMessageVersion::V4 => {
255 if has_block_access_list {
256 return Err(message_validation_kind
257 .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
258 }
259 }
260
261 EngineApiMessageVersion::V5 => {
262 if message_validation_kind == MessageValidationKind::Payload {
263 if is_amsterdam_active && !has_block_access_list {
264 return Err(message_validation_kind
265 .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
266 }
267 if !is_amsterdam_active && has_block_access_list {
268 return Err(message_validation_kind
269 .to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
270 }
271 } else if has_block_access_list {
272 return Err(message_validation_kind
273 .to_error(VersionSpecificValidationError::BlockAccessListNotSupported))
274 }
275 }
276
277 EngineApiMessageVersion::V6 => {
278 if is_amsterdam_active && !has_block_access_list {
279 return Err(message_validation_kind
280 .to_error(VersionSpecificValidationError::NoBlockAccessListPostAmsterdam))
281 }
282 if !is_amsterdam_active && has_block_access_list {
283 return Err(message_validation_kind
284 .to_error(VersionSpecificValidationError::HasBlockAccessListPreAmsterdam))
285 }
286 }
287 };
288
289 Ok(())
290}
291
292/// Validates the presence of the `slot number` field according to the payload timestamp.
293/// After Amsterdam, slot number field must be [Some].
294/// Before Amsterdam, slot number field must be [None];
295pub fn validate_slot_number_presence<T: EthereumHardforks>(
296 chain_spec: &T,
297 version: EngineApiMessageVersion,
298 message_validation_kind: MessageValidationKind,
299 timestamp: u64,
300 has_slot_number: bool,
301) -> Result<(), EngineObjectValidationError> {
302 let is_amsterdam_active = chain_spec.is_amsterdam_active_at_timestamp(timestamp);
303
304 match version {
305 EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 => {
306 if has_slot_number {
307 return Err(message_validation_kind
308 .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
309 }
310 }
311
312 EngineApiMessageVersion::V4 => {
313 if message_validation_kind == MessageValidationKind::PayloadAttributes {
314 if is_amsterdam_active && !has_slot_number {
315 return Err(message_validation_kind
316 .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
317 }
318 if !is_amsterdam_active && has_slot_number {
319 return Err(message_validation_kind
320 .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
321 }
322 } else if has_slot_number {
323 return Err(message_validation_kind
324 .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
325 }
326 }
327
328 EngineApiMessageVersion::V5 => {
329 if matches!(
330 message_validation_kind,
331 MessageValidationKind::Payload | MessageValidationKind::PayloadAttributes
332 ) {
333 if is_amsterdam_active && !has_slot_number {
334 return Err(message_validation_kind
335 .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
336 }
337 if !is_amsterdam_active && has_slot_number {
338 return Err(message_validation_kind
339 .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
340 }
341 } else if has_slot_number {
342 return Err(message_validation_kind
343 .to_error(VersionSpecificValidationError::SlotNumberNotSupported))
344 }
345 }
346
347 EngineApiMessageVersion::V6 => {
348 if is_amsterdam_active && !has_slot_number {
349 return Err(message_validation_kind
350 .to_error(VersionSpecificValidationError::NoSlotNumberPostAmsterdam))
351 }
352 if !is_amsterdam_active && has_slot_number {
353 return Err(message_validation_kind
354 .to_error(VersionSpecificValidationError::HasSlotNumberPreAmsterdam))
355 }
356 }
357 };
358
359 Ok(())
360}
361
362/// Validates the presence of the `withdrawals` field according to the payload timestamp.
363/// After Shanghai, withdrawals field must be [Some].
364/// Before Shanghai, withdrawals field must be [None];
365pub fn validate_withdrawals_presence<T: EthereumHardforks>(
366 chain_spec: &T,
367 version: EngineApiMessageVersion,
368 message_validation_kind: MessageValidationKind,
369 timestamp: u64,
370 has_withdrawals: bool,
371) -> Result<(), EngineObjectValidationError> {
372 let is_shanghai_active = chain_spec.is_shanghai_active_at_timestamp(timestamp);
373
374 match version {
375 EngineApiMessageVersion::V1 => {
376 if has_withdrawals {
377 return Err(message_validation_kind
378 .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1))
379 }
380 }
381 EngineApiMessageVersion::V2 |
382 EngineApiMessageVersion::V3 |
383 EngineApiMessageVersion::V4 |
384 EngineApiMessageVersion::V5 |
385 EngineApiMessageVersion::V6 => {
386 if is_shanghai_active && !has_withdrawals {
387 return Err(message_validation_kind
388 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
389 }
390 if !is_shanghai_active && has_withdrawals {
391 return Err(message_validation_kind
392 .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai))
393 }
394 }
395 };
396
397 Ok(())
398}
399
400/// Validate the presence of the `parentBeaconBlockRoot` field according to the given timestamp.
401/// This method is meant to be used with either a `payloadAttributes` field or a full payload, with
402/// the `engine_forkchoiceUpdated` and `engine_newPayload` methods respectively.
403///
404/// After Cancun, the `parentBeaconBlockRoot` field must be [Some].
405/// Before Cancun, the `parentBeaconBlockRoot` field must be [None].
406///
407/// If the engine API message version is V1 or V2, and the timestamp is post-Cancun, then this will
408/// return [`EngineObjectValidationError::UnsupportedFork`].
409///
410/// If the timestamp is before the Cancun fork and the engine API message version is V3, then this
411/// will return [`EngineObjectValidationError::UnsupportedFork`].
412///
413/// If the engine API message version is V3, but the `parentBeaconBlockRoot` is [None], then
414/// this will return [`VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun`].
415///
416/// This implements the following Engine API spec rules:
417///
418/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
419/// matches the expected one and return `-32602: Invalid params` error if this check fails. Any
420/// field having `null` value **MUST** be considered as not provided.
421///
422/// For `engine_forkchoiceUpdatedV3`:
423///
424/// 1. Client software **MUST** check that provided set of parameters and their fields strictly
425/// matches the expected one and return `-32602: Invalid params` error if this check fails. Any
426/// field having `null` value **MUST** be considered as not provided.
427///
428/// 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the following
429/// sequence of checks that **MUST** be run over `payloadAttributes`:
430/// 1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003: Invalid
431/// payload attributes` on failure.
432/// 2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
433/// `-38005: Unsupported fork` on failure.
434/// 3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
435/// `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on failure.
436/// 4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
437/// back.
438///
439/// For `engine_newPayloadV3`:
440///
441/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
442/// payload does not fall within the time frame of the Cancun fork.
443///
444/// For `engine_newPayloadV4`:
445///
446/// 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
447/// payload does not fall within the time frame of the Prague fork.
448///
449/// Returning the right error code (ie, if the client should return `-38003: Invalid payload
450/// attributes` is handled by the `message_validation_kind` parameter. If the parameter is
451/// `MessageValidationKind::Payload`, then the error code will be `-32602: Invalid params`. If the
452/// parameter is `MessageValidationKind::PayloadAttributes`, then the error code will be `-38003:
453/// Invalid payload attributes`.
454pub fn validate_parent_beacon_block_root_presence<T: EthereumHardforks>(
455 chain_spec: &T,
456 version: EngineApiMessageVersion,
457 validation_kind: MessageValidationKind,
458 timestamp: u64,
459 has_parent_beacon_block_root: bool,
460) -> Result<(), EngineObjectValidationError> {
461 // 1. Client software **MUST** check that provided set of parameters and their fields strictly
462 // matches the expected one and return `-32602: Invalid params` error if this check fails.
463 // Any field having `null` value **MUST** be considered as not provided.
464 //
465 // For `engine_forkchoiceUpdatedV3`:
466 //
467 // 2. Extend point (7) of the `engine_forkchoiceUpdatedV1` specification by defining the
468 // following sequence of checks that **MUST** be run over `payloadAttributes`:
469 // 1. `payloadAttributes` matches the `PayloadAttributesV3` structure, return `-38003:
470 // Invalid payload attributes` on failure.
471 // 2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return
472 // `-38005: Unsupported fork` on failure.
473 // 3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by
474 // `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on
475 // failure.
476 // 4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled
477 // back.
478 match version {
479 EngineApiMessageVersion::V1 | EngineApiMessageVersion::V2 => {
480 if has_parent_beacon_block_root {
481 return Err(validation_kind.to_error(
482 VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
483 ))
484 }
485 }
486 EngineApiMessageVersion::V3 |
487 EngineApiMessageVersion::V4 |
488 EngineApiMessageVersion::V5 |
489 EngineApiMessageVersion::V6 => {
490 if !has_parent_beacon_block_root {
491 return Err(validation_kind
492 .to_error(VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun))
493 }
494 }
495 };
496
497 // For `engine_forkchoiceUpdatedV3`:
498 //
499 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the
500 // `payloadAttributes` is set and the `payloadAttributes.timestamp` does not fall within the
501 // time frame of the Cancun fork.
502 //
503 // For `engine_newPayloadV3`:
504 //
505 // 2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the
506 // payload does not fall within the time frame of the Cancun fork.
507 validate_payload_timestamp(chain_spec, version, timestamp, validation_kind)?;
508
509 Ok(())
510}
511
512/// A type that represents whether or not we are validating a payload or payload attributes.
513///
514/// This is used to ensure that the correct error code is returned when validating the payload or
515/// payload attributes.
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517pub enum MessageValidationKind {
518 /// We are validating fields of a payload attributes.
519 /// This corresponds to `engine_forkchoiceUpdated`.
520 PayloadAttributes,
521 /// We are validating fields of a payload.
522 /// This corresponds to `engine_newPayload`.
523 Payload,
524 /// We are validating a built payload.
525 /// This corresponds to `engine_getPayload`.
526 GetPayload,
527}
528
529impl MessageValidationKind {
530 /// Returns an `EngineObjectValidationError` based on the given
531 /// `VersionSpecificValidationError` and the current validation kind.
532 pub const fn to_error(
533 self,
534 error: VersionSpecificValidationError,
535 ) -> EngineObjectValidationError {
536 match self {
537 // Both NewPayload and GetPayload errors are treated as generic Payload validation
538 // errors
539 Self::Payload | Self::GetPayload => EngineObjectValidationError::Payload(error),
540 Self::PayloadAttributes => EngineObjectValidationError::PayloadAttributes(error),
541 }
542 }
543}
544
545/// Validates the presence or exclusion of fork-specific fields based on the ethereum execution
546/// payload, or payload attributes, and the message version.
547///
548/// The object being validated is provided by the [`PayloadOrAttributes`] argument, which can be
549/// either an execution payload, or payload attributes.
550///
551/// The version is provided by the [`EngineApiMessageVersion`] argument.
552pub fn validate_version_specific_fields<Payload, Type, T>(
553 chain_spec: &T,
554 version: EngineApiMessageVersion,
555 payload_or_attrs: PayloadOrAttributes<'_, Payload, Type>,
556) -> Result<(), EngineObjectValidationError>
557where
558 Payload: ExecutionPayload,
559 Type: PayloadAttributes,
560 T: EthereumHardforks,
561{
562 // BAL only exists in ExecutionPayload, not PayloadAttributes (EIP-7928)
563 if let PayloadOrAttributes::ExecutionPayload(_) = payload_or_attrs {
564 validate_block_access_list_presence(
565 chain_spec,
566 version,
567 payload_or_attrs.message_validation_kind(),
568 payload_or_attrs.timestamp(),
569 payload_or_attrs.block_access_list().is_some(),
570 )?;
571 }
572
573 validate_slot_number_presence(
574 chain_spec,
575 version,
576 payload_or_attrs.message_validation_kind(),
577 payload_or_attrs.timestamp(),
578 payload_or_attrs.slot_number().is_some(),
579 )?;
580
581 validate_withdrawals_presence(
582 chain_spec,
583 version,
584 payload_or_attrs.message_validation_kind(),
585 payload_or_attrs.timestamp(),
586 payload_or_attrs.withdrawals().is_some(),
587 )?;
588 validate_parent_beacon_block_root_presence(
589 chain_spec,
590 version,
591 payload_or_attrs.message_validation_kind(),
592 payload_or_attrs.timestamp(),
593 payload_or_attrs.parent_beacon_block_root().is_some(),
594 )
595}
596
597/// The version of Engine API message.
598#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
599pub enum EngineApiMessageVersion {
600 /// Version 1
601 V1 = 1,
602 /// Version 2
603 ///
604 /// Added in the Shanghai hardfork.
605 V2 = 2,
606 /// Version 3
607 ///
608 /// Added in the Cancun hardfork.
609 V3 = 3,
610 /// Version 4
611 ///
612 /// Added in the Prague hardfork.
613 #[default]
614 V4 = 4,
615 /// Version 5
616 ///
617 /// Added in the Osaka hardfork.
618 V5 = 5,
619 /// Version 6
620 ///
621 /// Added in the Amsterdam hardfork.
622 V6 = 6,
623}
624
625impl EngineApiMessageVersion {
626 /// Returns true if the version is V1.
627 pub const fn is_v1(&self) -> bool {
628 matches!(self, Self::V1)
629 }
630
631 /// Returns true if the version is V2.
632 pub const fn is_v2(&self) -> bool {
633 matches!(self, Self::V2)
634 }
635
636 /// Returns true if the version is V3.
637 pub const fn is_v3(&self) -> bool {
638 matches!(self, Self::V3)
639 }
640
641 /// Returns true if the version is V4.
642 pub const fn is_v4(&self) -> bool {
643 matches!(self, Self::V4)
644 }
645
646 /// Returns true if the version is V5.
647 pub const fn is_v5(&self) -> bool {
648 matches!(self, Self::V5)
649 }
650
651 /// Returns true if the version is V6.
652 pub const fn is_v6(&self) -> bool {
653 matches!(self, Self::V6)
654 }
655
656 /// Returns the method name for the given version.
657 pub const fn method_name(&self) -> &'static str {
658 match self {
659 Self::V1 => "engine_newPayloadV1",
660 Self::V2 => "engine_newPayloadV2",
661 Self::V3 => "engine_newPayloadV3",
662 Self::V4 => "engine_newPayloadV4",
663 Self::V5 | Self::V6 => "engine_newPayloadV5",
664 }
665 }
666}
667
668/// Determines how we should choose the payload to return.
669#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
670pub enum PayloadKind {
671 /// Returns the next best available payload (the earliest available payload).
672 /// This does not wait for a real for pending job to finish if there's no best payload yet and
673 /// is allowed to race various payload jobs (empty, pending best) against each other and
674 /// returns whichever job finishes faster.
675 ///
676 /// This should be used when it's more important to return a valid payload as fast as possible.
677 /// For example, the engine API timeout for `engine_getPayload` is 1s and clients should rather
678 /// return an empty payload than indefinitely waiting for the pending payload job to finish and
679 /// risk missing the deadline.
680 #[default]
681 Earliest,
682 /// Only returns once we have at least one built payload.
683 ///
684 /// Compared to [`PayloadKind::Earliest`] this does not race an empty payload job against the
685 /// already in progress one, and returns the best available built payload or awaits the job in
686 /// progress.
687 WaitForPending,
688}
689
690/// Validates that execution requests are valid according to Engine API specification.
691///
692/// `executionRequests`: `Array of DATA` - List of execution layer triggered requests. Each list
693/// element is a `requests` byte array as defined by [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685).
694/// The first byte of each element is the `request_type` and the remaining bytes are the
695/// `request_data`. Elements of the list **MUST** be ordered by `request_type` in ascending order.
696/// Elements with empty `request_data` **MUST** be excluded from the list. If any element is out of
697/// order, has a length of 1-byte or shorter, or more than one element has the same type byte,
698/// client software **MUST** return `-32602: Invalid params` error.
699pub fn validate_execution_requests(requests: &[Bytes]) -> Result<(), EngineObjectValidationError> {
700 let mut last_request_type = None;
701 for request in requests {
702 if request.len() <= 1 {
703 return Err(EngineObjectValidationError::InvalidParams("EmptyExecutionRequest".into()))
704 }
705
706 let request_type = request[0];
707 if Some(request_type) < last_request_type {
708 return Err(EngineObjectValidationError::InvalidParams(
709 "OutOfOrderExecutionRequest".into(),
710 ))
711 }
712
713 if Some(request_type) == last_request_type {
714 return Err(EngineObjectValidationError::InvalidParams(
715 "DuplicatedExecutionRequestType".into(),
716 ))
717 }
718
719 last_request_type = Some(request_type);
720 }
721 Ok(())
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use assert_matches::assert_matches;
728 use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition};
729
730 #[test]
731 fn version_ord() {
732 assert!(EngineApiMessageVersion::V4 > EngineApiMessageVersion::V3);
733 }
734
735 #[test]
736 fn validate_osaka_get_payload_restrictions() {
737 // Osaka activates at timestamp 1000
738 let osaka_activation = 1000;
739 let chain_spec = ChainSpecBuilder::mainnet()
740 .with_fork(EthereumHardfork::Prague, ForkCondition::Timestamp(0))
741 .with_fork(EthereumHardfork::Osaka, ForkCondition::Timestamp(osaka_activation))
742 .build();
743
744 // Osaka is Active + V4 + GetPayload
745 let res = validate_payload_timestamp(
746 &chain_spec,
747 EngineApiMessageVersion::V4,
748 osaka_activation,
749 MessageValidationKind::GetPayload,
750 );
751 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
752
753 // Osaka is Active + V4 + Payload (NewPayload)
754 let res = validate_payload_timestamp(
755 &chain_spec,
756 EngineApiMessageVersion::V4,
757 osaka_activation,
758 MessageValidationKind::Payload,
759 );
760 assert_matches!(res, Ok(()));
761 }
762
763 #[test]
764 fn validate_amsterdam_staggered_version_restrictions() {
765 let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
766
767 let res = validate_payload_timestamp(
768 &chain_spec,
769 EngineApiMessageVersion::V3,
770 0,
771 MessageValidationKind::PayloadAttributes,
772 );
773 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
774
775 let res = validate_payload_timestamp(
776 &chain_spec,
777 EngineApiMessageVersion::V4,
778 0,
779 MessageValidationKind::Payload,
780 );
781 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
782
783 let res = validate_payload_timestamp(
784 &chain_spec,
785 EngineApiMessageVersion::V5,
786 0,
787 MessageValidationKind::GetPayload,
788 );
789 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
790
791 let res = validate_payload_timestamp(
792 &chain_spec,
793 EngineApiMessageVersion::V6,
794 0,
795 MessageValidationKind::GetPayload,
796 );
797 assert_matches!(res, Ok(()));
798 }
799
800 #[test]
801 fn validate_bogota_fork_timeframe() {
802 // Amsterdam active, Bogota not scheduled: the Bogota-specific methods must reject
803 let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
804
805 // `engine_newPayloadV6` requires a Bogota timestamp
806 let res = validate_payload_timestamp(
807 &chain_spec,
808 EngineApiMessageVersion::V6,
809 0,
810 MessageValidationKind::Payload,
811 );
812 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
813
814 // `engine_forkchoiceUpdatedV5` requires Bogota payload attributes
815 let res = validate_payload_timestamp(
816 &chain_spec,
817 EngineApiMessageVersion::V5,
818 0,
819 MessageValidationKind::PayloadAttributes,
820 );
821 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
822
823 // the Amsterdam methods remain valid pre-Bogota
824 let res = validate_payload_timestamp(
825 &chain_spec,
826 EngineApiMessageVersion::V5,
827 0,
828 MessageValidationKind::Payload,
829 );
830 assert_matches!(res, Ok(()));
831 let res = validate_payload_timestamp(
832 &chain_spec,
833 EngineApiMessageVersion::V4,
834 0,
835 MessageValidationKind::PayloadAttributes,
836 );
837 assert_matches!(res, Ok(()));
838 }
839
840 #[test]
841 fn validate_bogota_staggered_version_restrictions() {
842 let chain_spec = ChainSpecBuilder::mainnet().bogota_activated().build();
843
844 // `engine_newPayloadV5` must reject Bogota payloads
845 let res = validate_payload_timestamp(
846 &chain_spec,
847 EngineApiMessageVersion::V5,
848 0,
849 MessageValidationKind::Payload,
850 );
851 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
852
853 // `engine_forkchoiceUpdatedV4` must reject Bogota payload attributes
854 let res = validate_payload_timestamp(
855 &chain_spec,
856 EngineApiMessageVersion::V4,
857 0,
858 MessageValidationKind::PayloadAttributes,
859 );
860 assert_matches!(res, Err(EngineObjectValidationError::UnsupportedFork));
861
862 // the Bogota-specific methods are accepted
863 let res = validate_payload_timestamp(
864 &chain_spec,
865 EngineApiMessageVersion::V6,
866 0,
867 MessageValidationKind::Payload,
868 );
869 assert_matches!(res, Ok(()));
870 let res = validate_payload_timestamp(
871 &chain_spec,
872 EngineApiMessageVersion::V5,
873 0,
874 MessageValidationKind::PayloadAttributes,
875 );
876 assert_matches!(res, Ok(()));
877
878 // Bogota defines no new getPayload version, `engine_getPayloadV6` remains valid
879 let res = validate_payload_timestamp(
880 &chain_spec,
881 EngineApiMessageVersion::V6,
882 0,
883 MessageValidationKind::GetPayload,
884 );
885 assert_matches!(res, Ok(()));
886 }
887
888 #[test]
889 fn validate_amsterdam_slot_and_bal_presence() {
890 let chain_spec = ChainSpecBuilder::mainnet().amsterdam_activated().build();
891
892 let res = validate_slot_number_presence(
893 &chain_spec,
894 EngineApiMessageVersion::V4,
895 MessageValidationKind::PayloadAttributes,
896 0,
897 true,
898 );
899 assert_matches!(res, Ok(()));
900
901 let res = validate_slot_number_presence(
902 &chain_spec,
903 EngineApiMessageVersion::V5,
904 MessageValidationKind::Payload,
905 0,
906 true,
907 );
908 assert_matches!(res, Ok(()));
909
910 let res = validate_slot_number_presence(
911 &chain_spec,
912 EngineApiMessageVersion::V5,
913 MessageValidationKind::PayloadAttributes,
914 0,
915 true,
916 );
917 assert_matches!(res, Ok(()));
918
919 let res = validate_block_access_list_presence(
920 &chain_spec,
921 EngineApiMessageVersion::V5,
922 MessageValidationKind::Payload,
923 0,
924 true,
925 );
926 assert_matches!(res, Ok(()));
927 }
928
929 #[test]
930 fn execution_requests_validation() {
931 assert_matches!(validate_execution_requests(&[]), Ok(()));
932
933 let valid_requests = [
934 Bytes::from_iter([1, 2]),
935 Bytes::from_iter([2, 3]),
936 Bytes::from_iter([3, 4]),
937 Bytes::from_iter([4, 5]),
938 ];
939 assert_matches!(validate_execution_requests(&valid_requests), Ok(()));
940
941 let requests_with_empty = [
942 Bytes::from_iter([1, 2]),
943 Bytes::from_iter([2, 3]),
944 Bytes::new(),
945 Bytes::from_iter([3, 4]),
946 ];
947 assert_matches!(
948 validate_execution_requests(&requests_with_empty),
949 Err(EngineObjectValidationError::InvalidParams(_))
950 );
951
952 let mut requests_valid_reversed = valid_requests;
953 requests_valid_reversed.reverse();
954 assert_matches!(
955 validate_execution_requests(&requests_valid_reversed),
956 Err(EngineObjectValidationError::InvalidParams(_))
957 );
958
959 let requests_out_of_order = [
960 Bytes::from_iter([1, 2]),
961 Bytes::from_iter([2, 3]),
962 Bytes::from_iter([4, 5]),
963 Bytes::from_iter([3, 4]),
964 ];
965 assert_matches!(
966 validate_execution_requests(&requests_out_of_order),
967 Err(EngineObjectValidationError::InvalidParams(_))
968 );
969
970 let duplicate_request_types = [
971 Bytes::from_iter([1, 2]),
972 Bytes::from_iter([3, 3]),
973 Bytes::from_iter([4, 5]),
974 Bytes::from_iter([4, 4]),
975 ];
976 assert_matches!(
977 validate_execution_requests(&duplicate_request_types),
978 Err(EngineObjectValidationError::InvalidParams(_))
979 );
980 }
981}