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, L2_TO_L1_MESSAGE_PASSER_ADDRESS};
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(L2_TO_L1_MESSAGE_PASSER_ADDRESS);
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 validate_block_post_execution_with_hashed_state(
125 &self,
126 state_updates: &HashedPostState,
127 block: &RecoveredBlock<Self::Block>,
128 ) -> Result<(), ConsensusError> {
129 if self.chain_spec().is_isthmus_active_at_timestamp(block.timestamp()) {
130 let Ok(state) = self.provider.state_by_block_hash(block.parent_hash()) else {
131 return Ok(())
135 };
136 let predeploy_storage_updates = state_updates
137 .storages
138 .get(&self.hashed_addr_l2tol1_msg_passer)
139 .cloned()
140 .unwrap_or_default();
141 isthmus::verify_withdrawals_root_prehashed(
142 predeploy_storage_updates,
143 state,
144 block.header(),
145 )
146 .map_err(|err| {
147 ConsensusError::Other(format!("failed to verify block post-execution: {err}"))
148 })?
149 }
150
151 Ok(())
152 }
153
154 fn convert_payload_to_block(
155 &self,
156 payload: OpExecutionData,
157 ) -> Result<SealedBlock<Self::Block>, NewPayloadError> {
158 self.inner.ensure_well_formed_payload(payload).map_err(NewPayloadError::other)
159 }
160}
161
162impl<Types, P, Tx, ChainSpec> EngineApiValidator<Types> for OpEngineValidator<P, Tx, ChainSpec>
163where
164 Types: PayloadTypes<
165 PayloadAttributes = OpPayloadAttributes,
166 ExecutionData = OpExecutionData,
167 BuiltPayload: BuiltPayload<Primitives: NodePrimitives<SignedTx = Tx>>,
168 >,
169 P: StateProviderFactory + Unpin + 'static,
170 Tx: SignedTransaction + Unpin + 'static,
171 ChainSpec: OpHardforks + Send + Sync + 'static,
172{
173 fn validate_version_specific_fields(
174 &self,
175 version: EngineApiMessageVersion,
176 payload_or_attrs: PayloadOrAttributes<
177 '_,
178 Types::ExecutionData,
179 <Types as PayloadTypes>::PayloadAttributes,
180 >,
181 ) -> Result<(), EngineObjectValidationError> {
182 validate_withdrawals_presence(
183 self.chain_spec(),
184 version,
185 payload_or_attrs.message_validation_kind(),
186 payload_or_attrs.timestamp(),
187 payload_or_attrs.withdrawals().is_some(),
188 )?;
189 validate_parent_beacon_block_root_presence(
190 self.chain_spec(),
191 version,
192 payload_or_attrs.message_validation_kind(),
193 payload_or_attrs.timestamp(),
194 payload_or_attrs.parent_beacon_block_root().is_some(),
195 )
196 }
197
198 fn ensure_well_formed_attributes(
199 &self,
200 version: EngineApiMessageVersion,
201 attributes: &<Types as PayloadTypes>::PayloadAttributes,
202 ) -> Result<(), EngineObjectValidationError> {
203 validate_version_specific_fields(
204 self.chain_spec(),
205 version,
206 PayloadOrAttributes::<OpExecutionData, OpPayloadAttributes>::PayloadAttributes(
207 attributes,
208 ),
209 )?;
210
211 if attributes.gas_limit.is_none() {
212 return Err(EngineObjectValidationError::InvalidParams(
213 "MissingGasLimitInPayloadAttributes".to_string().into(),
214 ));
215 }
216
217 if self
218 .chain_spec()
219 .is_holocene_active_at_timestamp(attributes.payload_attributes.timestamp)
220 {
221 let (elasticity, denominator) =
222 attributes.decode_eip_1559_params().ok_or_else(|| {
223 EngineObjectValidationError::InvalidParams(
224 "MissingEip1559ParamsInPayloadAttributes".to_string().into(),
225 )
226 })?;
227
228 if elasticity != 0 && denominator == 0 {
229 return Err(EngineObjectValidationError::InvalidParams(
230 "Eip1559ParamsDenominatorZero".to_string().into(),
231 ));
232 }
233 }
234
235 if self.chain_spec().is_jovian_active_at_timestamp(attributes.payload_attributes.timestamp)
236 {
237 if attributes.min_base_fee.is_none() {
238 return Err(EngineObjectValidationError::InvalidParams(
239 "MissingMinBaseFeeInPayloadAttributes".to_string().into(),
240 ));
241 }
242 } else if attributes.min_base_fee.is_some() {
243 return Err(EngineObjectValidationError::InvalidParams(
244 "MinBaseFeeNotAllowedBeforeJovian".to_string().into(),
245 ));
246 }
247
248 Ok(())
249 }
250}
251
252pub fn validate_withdrawals_presence(
260 chain_spec: impl OpHardforks,
261 version: EngineApiMessageVersion,
262 message_validation_kind: MessageValidationKind,
263 timestamp: u64,
264 has_withdrawals: bool,
265) -> Result<(), EngineObjectValidationError> {
266 let is_shanghai = chain_spec.is_canyon_active_at_timestamp(timestamp);
267
268 match version {
269 EngineApiMessageVersion::V1 => {
270 if has_withdrawals {
271 return Err(message_validation_kind
272 .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1));
273 }
274 if is_shanghai {
275 return Err(message_validation_kind
276 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai));
277 }
278 }
279 EngineApiMessageVersion::V2 |
280 EngineApiMessageVersion::V3 |
281 EngineApiMessageVersion::V4 |
282 EngineApiMessageVersion::V5 => {
283 if is_shanghai && !has_withdrawals {
284 return Err(message_validation_kind
285 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai));
286 }
287 if !is_shanghai && has_withdrawals {
288 return Err(message_validation_kind
289 .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai));
290 }
291 }
292 };
293
294 Ok(())
295}
296
297#[cfg(test)]
298mod test {
299 use super::*;
300
301 use crate::engine;
302 use alloy_primitives::{b64, Address, B256, B64};
303 use alloy_rpc_types_engine::PayloadAttributes;
304 use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
305 use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
306 use reth_optimism_forks::OpHardfork;
307 use reth_provider::noop::NoopProvider;
308 use reth_trie_common::KeccakKeyHasher;
309
310 const JOVIAN_TIMESTAMP: u64 = 1744909000;
311
312 fn get_chainspec() -> Arc<OpChainSpec> {
313 let mut base_sepolia_spec = BASE_SEPOLIA.inner.clone();
314
315 base_sepolia_spec
317 .hardforks
318 .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP));
319
320 Arc::new(OpChainSpec {
321 inner: ChainSpec {
322 chain: base_sepolia_spec.chain,
323 genesis: base_sepolia_spec.genesis,
324 genesis_header: base_sepolia_spec.genesis_header,
325 paris_block_and_final_difficulty: base_sepolia_spec
326 .paris_block_and_final_difficulty,
327 hardforks: base_sepolia_spec.hardforks,
328 base_fee_params: base_sepolia_spec.base_fee_params,
329 prune_delete_limit: 10000,
330 ..Default::default()
331 },
332 })
333 }
334
335 const fn get_attributes(
336 eip_1559_params: Option<B64>,
337 min_base_fee: Option<u64>,
338 timestamp: u64,
339 ) -> OpPayloadAttributes {
340 OpPayloadAttributes {
341 gas_limit: Some(1000),
342 eip_1559_params,
343 min_base_fee,
344 transactions: None,
345 no_tx_pool: None,
346 payload_attributes: PayloadAttributes {
347 timestamp,
348 prev_randao: B256::ZERO,
349 suggested_fee_recipient: Address::ZERO,
350 withdrawals: Some(vec![]),
351 parent_beacon_block_root: Some(B256::ZERO),
352 },
353 }
354 }
355
356 #[test]
357 fn test_well_formed_attributes_pre_holocene() {
358 let validator =
359 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
360 let attributes = get_attributes(None, None, 1732633199);
361
362 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
363 OpEngineTypes,
364 >>::ensure_well_formed_attributes(
365 &validator, EngineApiMessageVersion::V3, &attributes,
366 );
367 assert!(result.is_ok());
368 }
369
370 #[test]
371 fn test_well_formed_attributes_holocene_no_eip1559_params() {
372 let validator =
373 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
374 let attributes = get_attributes(None, None, 1732633200);
375
376 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
377 OpEngineTypes,
378 >>::ensure_well_formed_attributes(
379 &validator, EngineApiMessageVersion::V3, &attributes,
380 );
381 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
382 }
383
384 #[test]
385 fn test_well_formed_attributes_holocene_eip1559_params_zero_denominator() {
386 let validator =
387 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
388 let attributes = get_attributes(Some(b64!("0000000000000008")), None, 1732633200);
389
390 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
391 OpEngineTypes,
392 >>::ensure_well_formed_attributes(
393 &validator, EngineApiMessageVersion::V3, &attributes,
394 );
395 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
396 }
397
398 #[test]
399 fn test_well_formed_attributes_holocene_valid() {
400 let validator =
401 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
402 let attributes = get_attributes(Some(b64!("0000000800000008")), None, 1732633200);
403
404 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
405 OpEngineTypes,
406 >>::ensure_well_formed_attributes(
407 &validator, EngineApiMessageVersion::V3, &attributes,
408 );
409 assert!(result.is_ok());
410 }
411
412 #[test]
413 fn test_well_formed_attributes_holocene_valid_all_zero() {
414 let validator =
415 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
416 let attributes = get_attributes(Some(b64!("0000000000000000")), None, 1732633200);
417
418 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
419 OpEngineTypes,
420 >>::ensure_well_formed_attributes(
421 &validator, EngineApiMessageVersion::V3, &attributes,
422 );
423 assert!(result.is_ok());
424 }
425
426 #[test]
427 fn test_well_formed_attributes_jovian_valid() {
428 let validator =
429 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
430 let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), JOVIAN_TIMESTAMP);
431
432 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
433 OpEngineTypes,
434 >>::ensure_well_formed_attributes(
435 &validator, EngineApiMessageVersion::V3, &attributes,
436 );
437 assert!(result.is_ok());
438 }
439
440 #[test]
442 fn test_malformed_attributes_jovian_with_eip_1559_params_none() {
443 let validator =
444 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
445 let attributes = get_attributes(None, Some(1), JOVIAN_TIMESTAMP);
446
447 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
448 OpEngineTypes,
449 >>::ensure_well_formed_attributes(
450 &validator, EngineApiMessageVersion::V3, &attributes,
451 );
452 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
453 }
454
455 #[test]
457 fn test_malformed_attributes_pre_jovian_with_min_base_fee() {
458 let validator =
459 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
460 let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), 1732633200);
461
462 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
463 OpEngineTypes,
464 >>::ensure_well_formed_attributes(
465 &validator, EngineApiMessageVersion::V3, &attributes,
466 );
467 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
468 }
469
470 #[test]
472 fn test_malformed_attributes_post_jovian_with_min_base_fee_none() {
473 let validator =
474 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
475 let attributes = get_attributes(Some(b64!("0000000000000000")), None, JOVIAN_TIMESTAMP);
476
477 let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
478 OpEngineTypes,
479 >>::ensure_well_formed_attributes(
480 &validator, EngineApiMessageVersion::V3, &attributes,
481 );
482 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
483 }
484}