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