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