1use alloy_consensus::BlockHeader;
2use alloy_primitives::B256;
3use alloy_rpc_types_engine::{ExecutionPayloadEnvelopeV2, ExecutionPayloadV1};
4use op_alloy_rpc_types_engine::{
5 OpExecutionData, OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4,
6 OpPayloadAttributes,
7};
8use reth_consensus::ConsensusError;
9use reth_node_api::{
10 payload::{
11 validate_parent_beacon_block_root_presence, EngineApiMessageVersion,
12 EngineObjectValidationError, MessageValidationKind, NewPayloadError, PayloadOrAttributes,
13 PayloadTypes, VersionSpecificValidationError,
14 },
15 validate_version_specific_fields, BuiltPayload, EngineApiValidator, EngineTypes,
16 NodePrimitives, PayloadValidator,
17};
18use reth_optimism_consensus::isthmus;
19use reth_optimism_forks::OpHardforks;
20use reth_optimism_payload_builder::{OpExecutionPayloadValidator, OpPayloadTypes};
21use reth_optimism_primitives::{OpBlock, ADDRESS_L2_TO_L1_MESSAGE_PASSER};
22use reth_primitives_traits::{Block, RecoveredBlock, SealedBlock, SignedTransaction};
23use reth_provider::StateProviderFactory;
24use reth_trie_common::{HashedPostState, KeyHasher};
25use std::{marker::PhantomData, sync::Arc};
26
27#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)]
29#[non_exhaustive]
30pub struct OpEngineTypes<T: PayloadTypes = OpPayloadTypes> {
31 _marker: PhantomData<T>,
32}
33
34impl<T: PayloadTypes<ExecutionData = OpExecutionData>> PayloadTypes for OpEngineTypes<T> {
35 type ExecutionData = T::ExecutionData;
36 type BuiltPayload = T::BuiltPayload;
37 type PayloadAttributes = T::PayloadAttributes;
38 type PayloadBuilderAttributes = T::PayloadBuilderAttributes;
39
40 fn block_to_payload(
41 block: SealedBlock<
42 <<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
43 >,
44 ) -> <T as PayloadTypes>::ExecutionData {
45 OpExecutionData::from_block_unchecked(
46 block.hash(),
47 &block.into_block().into_ethereum_block(),
48 )
49 }
50}
51
52impl<T: PayloadTypes<ExecutionData = OpExecutionData>> EngineTypes for OpEngineTypes<T>
53where
54 T::BuiltPayload: BuiltPayload<Primitives: NodePrimitives<Block = OpBlock>>
55 + TryInto<ExecutionPayloadV1>
56 + TryInto<ExecutionPayloadEnvelopeV2>
57 + TryInto<OpExecutionPayloadEnvelopeV3>
58 + TryInto<OpExecutionPayloadEnvelopeV4>,
59{
60 type ExecutionPayloadEnvelopeV1 = ExecutionPayloadV1;
61 type ExecutionPayloadEnvelopeV2 = ExecutionPayloadEnvelopeV2;
62 type ExecutionPayloadEnvelopeV3 = OpExecutionPayloadEnvelopeV3;
63 type ExecutionPayloadEnvelopeV4 = OpExecutionPayloadEnvelopeV4;
64 type ExecutionPayloadEnvelopeV5 = OpExecutionPayloadEnvelopeV4;
65}
66
67#[derive(Debug)]
69pub struct OpEngineValidator<P, Tx, ChainSpec> {
70 inner: OpExecutionPayloadValidator<ChainSpec>,
71 provider: P,
72 hashed_addr_l2tol1_msg_passer: B256,
73 phantom: PhantomData<Tx>,
74}
75
76impl<P, Tx, ChainSpec> OpEngineValidator<P, Tx, ChainSpec> {
77 pub fn new<KH: KeyHasher>(chain_spec: Arc<ChainSpec>, provider: P) -> Self {
79 let hashed_addr_l2tol1_msg_passer = KH::hash_key(ADDRESS_L2_TO_L1_MESSAGE_PASSER);
80 Self {
81 inner: OpExecutionPayloadValidator::new(chain_spec),
82 provider,
83 hashed_addr_l2tol1_msg_passer,
84 phantom: PhantomData,
85 }
86 }
87}
88
89impl<P, Tx, ChainSpec> Clone for OpEngineValidator<P, Tx, ChainSpec>
90where
91 P: Clone,
92 ChainSpec: OpHardforks,
93{
94 fn clone(&self) -> Self {
95 Self {
96 inner: OpExecutionPayloadValidator::new(self.inner.clone()),
97 provider: self.provider.clone(),
98 hashed_addr_l2tol1_msg_passer: self.hashed_addr_l2tol1_msg_passer,
99 phantom: Default::default(),
100 }
101 }
102}
103
104impl<P, Tx, ChainSpec> OpEngineValidator<P, Tx, ChainSpec>
105where
106 ChainSpec: OpHardforks,
107{
108 #[inline]
110 pub fn chain_spec(&self) -> &ChainSpec {
111 self.inner.chain_spec()
112 }
113}
114
115impl<P, Tx, ChainSpec, Types> PayloadValidator<Types> for OpEngineValidator<P, Tx, ChainSpec>
116where
117 P: StateProviderFactory + Unpin + 'static,
118 Tx: SignedTransaction + Unpin + 'static,
119 ChainSpec: OpHardforks + Send + Sync + 'static,
120 Types: PayloadTypes<ExecutionData = OpExecutionData>,
121{
122 type Block = alloy_consensus::Block<Tx>;
123
124 fn ensure_well_formed_payload(
125 &self,
126 payload: OpExecutionData,
127 ) -> Result<RecoveredBlock<Self::Block>, NewPayloadError> {
128 let sealed_block =
129 self.inner.ensure_well_formed_payload(payload).map_err(NewPayloadError::other)?;
130 sealed_block.try_recover().map_err(|e| NewPayloadError::Other(e.into()))
131 }
132
133 fn validate_block_post_execution_with_hashed_state(
134 &self,
135 state_updates: &HashedPostState,
136 block: &RecoveredBlock<Self::Block>,
137 ) -> Result<(), ConsensusError> {
138 if self.chain_spec().is_isthmus_active_at_timestamp(block.timestamp()) {
139 let Ok(state) = self.provider.state_by_block_hash(block.parent_hash()) else {
140 return Ok(())
144 };
145 let predeploy_storage_updates = state_updates
146 .storages
147 .get(&self.hashed_addr_l2tol1_msg_passer)
148 .cloned()
149 .unwrap_or_default();
150 isthmus::verify_withdrawals_root_prehashed(
151 predeploy_storage_updates,
152 state,
153 block.header(),
154 )
155 .map_err(|err| {
156 ConsensusError::Other(format!("failed to verify block post-execution: {err}"))
157 })?
158 }
159
160 Ok(())
161 }
162}
163
164impl<Types, P, Tx, ChainSpec> EngineApiValidator<Types> for OpEngineValidator<P, Tx, ChainSpec>
165where
166 Types: PayloadTypes<
167 PayloadAttributes = OpPayloadAttributes,
168 ExecutionData = OpExecutionData,
169 BuiltPayload: BuiltPayload<Primitives: NodePrimitives<SignedTx = Tx>>,
170 >,
171 P: StateProviderFactory + Unpin + 'static,
172 Tx: SignedTransaction + Unpin + 'static,
173 ChainSpec: OpHardforks + Send + Sync + 'static,
174{
175 fn validate_version_specific_fields(
176 &self,
177 version: EngineApiMessageVersion,
178 payload_or_attrs: PayloadOrAttributes<
179 '_,
180 Types::ExecutionData,
181 <Types as PayloadTypes>::PayloadAttributes,
182 >,
183 ) -> Result<(), EngineObjectValidationError> {
184 validate_withdrawals_presence(
185 self.chain_spec(),
186 version,
187 payload_or_attrs.message_validation_kind(),
188 payload_or_attrs.timestamp(),
189 payload_or_attrs.withdrawals().is_some(),
190 )?;
191 validate_parent_beacon_block_root_presence(
192 self.chain_spec(),
193 version,
194 payload_or_attrs.message_validation_kind(),
195 payload_or_attrs.timestamp(),
196 payload_or_attrs.parent_beacon_block_root().is_some(),
197 )
198 }
199
200 fn ensure_well_formed_attributes(
201 &self,
202 version: EngineApiMessageVersion,
203 attributes: &<Types as PayloadTypes>::PayloadAttributes,
204 ) -> Result<(), EngineObjectValidationError> {
205 validate_version_specific_fields(
206 self.chain_spec(),
207 version,
208 PayloadOrAttributes::<OpExecutionData, OpPayloadAttributes>::PayloadAttributes(
209 attributes,
210 ),
211 )?;
212
213 if attributes.gas_limit.is_none() {
214 return Err(EngineObjectValidationError::InvalidParams(
215 "MissingGasLimitInPayloadAttributes".to_string().into(),
216 ));
217 }
218
219 if self
220 .chain_spec()
221 .is_holocene_active_at_timestamp(attributes.payload_attributes.timestamp)
222 {
223 let (elasticity, denominator) =
224 attributes.decode_eip_1559_params().ok_or_else(|| {
225 EngineObjectValidationError::InvalidParams(
226 "MissingEip1559ParamsInPayloadAttributes".to_string().into(),
227 )
228 })?;
229
230 if elasticity != 0 && denominator == 0 {
231 return Err(EngineObjectValidationError::InvalidParams(
232 "Eip1559ParamsDenominatorZero".to_string().into(),
233 ));
234 }
235 }
236
237 if self.chain_spec().is_jovian_active_at_timestamp(attributes.payload_attributes.timestamp)
238 {
239 if attributes.min_base_fee.is_none() {
240 return Err(EngineObjectValidationError::InvalidParams(
241 "MissingMinBaseFeeInPayloadAttributes".to_string().into(),
242 ));
243 }
244 } else if attributes.min_base_fee.is_some() {
245 return Err(EngineObjectValidationError::InvalidParams(
246 "MinBaseFeeNotAllowedBeforeJovian".to_string().into(),
247 ));
248 }
249
250 Ok(())
251 }
252}
253
254pub fn validate_withdrawals_presence(
262 chain_spec: impl OpHardforks,
263 version: EngineApiMessageVersion,
264 message_validation_kind: MessageValidationKind,
265 timestamp: u64,
266 has_withdrawals: bool,
267) -> Result<(), EngineObjectValidationError> {
268 let is_shanghai = chain_spec.is_canyon_active_at_timestamp(timestamp);
269
270 match version {
271 EngineApiMessageVersion::V1 => {
272 if has_withdrawals {
273 return Err(message_validation_kind
274 .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1));
275 }
276 if is_shanghai {
277 return Err(message_validation_kind
278 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai));
279 }
280 }
281 EngineApiMessageVersion::V2 |
282 EngineApiMessageVersion::V3 |
283 EngineApiMessageVersion::V4 |
284 EngineApiMessageVersion::V5 => {
285 if is_shanghai && !has_withdrawals {
286 return Err(message_validation_kind
287 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai));
288 }
289 if !is_shanghai && has_withdrawals {
290 return Err(message_validation_kind
291 .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai));
292 }
293 }
294 };
295
296 Ok(())
297}
298
299#[cfg(test)]
300mod test {
301 use super::*;
302
303 use crate::engine;
304 use alloy_primitives::{b64, Address, B256, B64};
305 use alloy_rpc_types_engine::PayloadAttributes;
306 use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
307 use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
308 use reth_optimism_forks::OpHardfork;
309 use reth_provider::noop::NoopProvider;
310 use reth_trie_common::KeccakKeyHasher;
311
312 const JOVIAN_TIMESTAMP: u64 = 1744909000;
313
314 fn get_chainspec() -> Arc<OpChainSpec> {
315 let mut base_sepolia_spec = BASE_SEPOLIA.inner.clone();
316
317 base_sepolia_spec
319 .hardforks
320 .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP));
321
322 Arc::new(OpChainSpec {
323 inner: ChainSpec {
324 chain: base_sepolia_spec.chain,
325 genesis: base_sepolia_spec.genesis,
326 genesis_header: base_sepolia_spec.genesis_header,
327 paris_block_and_final_difficulty: base_sepolia_spec
328 .paris_block_and_final_difficulty,
329 hardforks: base_sepolia_spec.hardforks,
330 base_fee_params: base_sepolia_spec.base_fee_params,
331 prune_delete_limit: 10000,
332 ..Default::default()
333 },
334 })
335 }
336
337 const fn get_attributes(
338 eip_1559_params: Option<B64>,
339 min_base_fee: Option<u64>,
340 timestamp: u64,
341 ) -> OpPayloadAttributes {
342 OpPayloadAttributes {
343 gas_limit: Some(1000),
344 eip_1559_params,
345 min_base_fee,
346 transactions: None,
347 no_tx_pool: None,
348 payload_attributes: PayloadAttributes {
349 timestamp,
350 prev_randao: B256::ZERO,
351 suggested_fee_recipient: Address::ZERO,
352 withdrawals: Some(vec![]),
353 parent_beacon_block_root: Some(B256::ZERO),
354 },
355 }
356 }
357
358 #[test]
359 fn test_well_formed_attributes_pre_holocene() {
360 let validator =
361 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
362 let attributes = get_attributes(None, None, 1732633199);
363
364 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
365 OpEngineTypes,
366 >>::ensure_well_formed_attributes(
367 &validator, EngineApiMessageVersion::V3, &attributes,
368 );
369 assert!(result.is_ok());
370 }
371
372 #[test]
373 fn test_well_formed_attributes_holocene_no_eip1559_params() {
374 let validator =
375 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
376 let attributes = get_attributes(None, None, 1732633200);
377
378 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
379 OpEngineTypes,
380 >>::ensure_well_formed_attributes(
381 &validator, EngineApiMessageVersion::V3, &attributes,
382 );
383 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
384 }
385
386 #[test]
387 fn test_well_formed_attributes_holocene_eip1559_params_zero_denominator() {
388 let validator =
389 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
390 let attributes = get_attributes(Some(b64!("0000000000000008")), None, 1732633200);
391
392 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
393 OpEngineTypes,
394 >>::ensure_well_formed_attributes(
395 &validator, EngineApiMessageVersion::V3, &attributes,
396 );
397 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
398 }
399
400 #[test]
401 fn test_well_formed_attributes_holocene_valid() {
402 let validator =
403 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
404 let attributes = get_attributes(Some(b64!("0000000800000008")), None, 1732633200);
405
406 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
407 OpEngineTypes,
408 >>::ensure_well_formed_attributes(
409 &validator, EngineApiMessageVersion::V3, &attributes,
410 );
411 assert!(result.is_ok());
412 }
413
414 #[test]
415 fn test_well_formed_attributes_holocene_valid_all_zero() {
416 let validator =
417 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
418 let attributes = get_attributes(Some(b64!("0000000000000000")), None, 1732633200);
419
420 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
421 OpEngineTypes,
422 >>::ensure_well_formed_attributes(
423 &validator, EngineApiMessageVersion::V3, &attributes,
424 );
425 assert!(result.is_ok());
426 }
427
428 #[test]
429 fn test_well_formed_attributes_jovian_valid() {
430 let validator =
431 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
432 let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), JOVIAN_TIMESTAMP);
433
434 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
435 OpEngineTypes,
436 >>::ensure_well_formed_attributes(
437 &validator, EngineApiMessageVersion::V3, &attributes,
438 );
439 assert!(result.is_ok());
440 }
441
442 #[test]
444 fn test_malformed_attributes_jovian_with_eip_1559_params_none() {
445 let validator =
446 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
447 let attributes = get_attributes(None, Some(1), JOVIAN_TIMESTAMP);
448
449 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
450 OpEngineTypes,
451 >>::ensure_well_formed_attributes(
452 &validator, EngineApiMessageVersion::V3, &attributes,
453 );
454 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
455 }
456
457 #[test]
459 fn test_malformed_attributes_pre_jovian_with_min_base_fee() {
460 let validator =
461 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
462 let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), 1732633200);
463
464 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
465 OpEngineTypes,
466 >>::ensure_well_formed_attributes(
467 &validator, EngineApiMessageVersion::V3, &attributes,
468 );
469 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
470 }
471
472 #[test]
474 fn test_malformed_attributes_post_jovian_with_min_base_fee_none() {
475 let validator =
476 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
477 let attributes = get_attributes(Some(b64!("0000000000000000")), None, JOVIAN_TIMESTAMP);
478
479 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
480 OpEngineTypes,
481 >>::ensure_well_formed_attributes(
482 &validator, EngineApiMessageVersion::V3, &attributes,
483 );
484 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
485 }
486}