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_chainspec::ChainSpec;
9use reth_consensus::ConsensusError;
10use reth_node_api::{
11 payload::{
12 validate_parent_beacon_block_root_presence, EngineApiMessageVersion,
13 EngineObjectValidationError, MessageValidationKind, NewPayloadError, PayloadOrAttributes,
14 PayloadTypes, VersionSpecificValidationError,
15 },
16 validate_version_specific_fields, BuiltPayload, EngineTypes, EngineValidator, NodePrimitives,
17 PayloadValidator,
18};
19use reth_optimism_chainspec::OpChainSpec;
20use reth_optimism_consensus::isthmus;
21use reth_optimism_forks::{OpHardfork, OpHardforks};
22use reth_optimism_payload_builder::{
23 OpBuiltPayload, OpExecutionPayloadValidator, OpPayloadBuilderAttributes,
24};
25use reth_optimism_primitives::{OpBlock, OpPrimitives, ADDRESS_L2_TO_L1_MESSAGE_PASSER};
26use reth_primitives_traits::{RecoveredBlock, SealedBlock};
27use reth_provider::StateProviderFactory;
28use reth_trie_common::{HashedPostState, KeyHasher};
29use std::sync::Arc;
30
31#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)]
33#[non_exhaustive]
34pub struct OpEngineTypes<T: PayloadTypes = OpPayloadTypes> {
35 _marker: std::marker::PhantomData<T>,
36}
37
38impl<
39 T: PayloadTypes<
40 ExecutionData = OpExecutionData,
41 BuiltPayload: BuiltPayload<Primitives: NodePrimitives<Block = OpBlock>>,
42 >,
43 > PayloadTypes for OpEngineTypes<T>
44{
45 type ExecutionData = T::ExecutionData;
46 type BuiltPayload = T::BuiltPayload;
47 type PayloadAttributes = T::PayloadAttributes;
48 type PayloadBuilderAttributes = T::PayloadBuilderAttributes;
49
50 fn block_to_payload(
51 block: SealedBlock<
52 <<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
53 >,
54 ) -> <T as PayloadTypes>::ExecutionData {
55 OpExecutionData::from_block_unchecked(block.hash(), &block.into_block())
56 }
57}
58
59impl<T: PayloadTypes<ExecutionData = OpExecutionData>> EngineTypes for OpEngineTypes<T>
60where
61 T::BuiltPayload: BuiltPayload<Primitives: NodePrimitives<Block = OpBlock>>
62 + TryInto<ExecutionPayloadV1>
63 + TryInto<ExecutionPayloadEnvelopeV2>
64 + TryInto<OpExecutionPayloadEnvelopeV3>
65 + TryInto<OpExecutionPayloadEnvelopeV4>,
66{
67 type ExecutionPayloadEnvelopeV1 = ExecutionPayloadV1;
68 type ExecutionPayloadEnvelopeV2 = ExecutionPayloadEnvelopeV2;
69 type ExecutionPayloadEnvelopeV3 = OpExecutionPayloadEnvelopeV3;
70 type ExecutionPayloadEnvelopeV4 = OpExecutionPayloadEnvelopeV4;
71}
72
73#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)]
75#[non_exhaustive]
76pub struct OpPayloadTypes<N: NodePrimitives = OpPrimitives>(core::marker::PhantomData<N>);
77
78impl<N: NodePrimitives> PayloadTypes for OpPayloadTypes<N>
79where
80 OpBuiltPayload<N>: BuiltPayload<Primitives: NodePrimitives<Block = OpBlock>>,
81{
82 type ExecutionData = OpExecutionData;
83 type BuiltPayload = OpBuiltPayload<N>;
84 type PayloadAttributes = OpPayloadAttributes;
85 type PayloadBuilderAttributes = OpPayloadBuilderAttributes<N::SignedTx>;
86
87 fn block_to_payload(
88 block: SealedBlock<
89 <<Self::BuiltPayload as BuiltPayload>::Primitives as NodePrimitives>::Block,
90 >,
91 ) -> Self::ExecutionData {
92 OpExecutionData::from_block_unchecked(block.hash(), &block.into_block())
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct OpEngineValidator<P> {
99 inner: OpExecutionPayloadValidator<OpChainSpec>,
100 provider: P,
101 hashed_addr_l2tol1_msg_passer: B256,
102}
103
104impl<P> OpEngineValidator<P> {
105 pub fn new<KH: KeyHasher>(chain_spec: Arc<OpChainSpec>, provider: P) -> Self {
107 let hashed_addr_l2tol1_msg_passer = KH::hash_key(ADDRESS_L2_TO_L1_MESSAGE_PASSER);
108 Self {
109 inner: OpExecutionPayloadValidator::new(chain_spec),
110 provider,
111 hashed_addr_l2tol1_msg_passer,
112 }
113 }
114
115 #[inline]
117 fn chain_spec(&self) -> &OpChainSpec {
118 self.inner.chain_spec()
119 }
120}
121
122impl<P> PayloadValidator for OpEngineValidator<P>
123where
124 P: StateProviderFactory + Unpin + 'static,
125{
126 type Block = OpBlock;
127 type ExecutionData = OpExecutionData;
128
129 fn ensure_well_formed_payload(
130 &self,
131 payload: Self::ExecutionData,
132 ) -> Result<RecoveredBlock<Self::Block>, NewPayloadError> {
133 let sealed_block =
134 self.inner.ensure_well_formed_payload(payload).map_err(NewPayloadError::other)?;
135 sealed_block.try_recover().map_err(|e| NewPayloadError::Other(e.into()))
136 }
137
138 fn validate_block_post_execution_with_hashed_state(
139 &self,
140 state_updates: &HashedPostState,
141 block: &RecoveredBlock<Self::Block>,
142 ) -> Result<(), ConsensusError> {
143 if self.chain_spec().is_isthmus_active_at_timestamp(block.timestamp()) {
144 let Ok(state) = self.provider.state_by_block_hash(block.parent_hash()) else {
145 return Ok(())
149 };
150 let predeploy_storage_updates = state_updates
151 .storages
152 .get(&self.hashed_addr_l2tol1_msg_passer)
153 .cloned()
154 .unwrap_or_default();
155 isthmus::verify_withdrawals_root_prehashed(
156 predeploy_storage_updates,
157 state,
158 block.header(),
159 )
160 .map_err(|err| {
161 ConsensusError::Other(format!("failed to verify block post-execution: {err}"))
162 })?
163 }
164
165 Ok(())
166 }
167}
168
169impl<Types, P> EngineValidator<Types> for OpEngineValidator<P>
170where
171 Types: PayloadTypes<PayloadAttributes = OpPayloadAttributes, ExecutionData = OpExecutionData>,
172 P: StateProviderFactory + Unpin + 'static,
173{
174 fn validate_version_specific_fields(
175 &self,
176 version: EngineApiMessageVersion,
177 payload_or_attrs: PayloadOrAttributes<'_, Self::ExecutionData, OpPayloadAttributes>,
178 ) -> Result<(), EngineObjectValidationError> {
179 validate_withdrawals_presence(
180 self.chain_spec(),
181 version,
182 payload_or_attrs.message_validation_kind(),
183 payload_or_attrs.timestamp(),
184 payload_or_attrs.withdrawals().is_some(),
185 )?;
186 validate_parent_beacon_block_root_presence(
187 self.chain_spec(),
188 version,
189 payload_or_attrs.message_validation_kind(),
190 payload_or_attrs.timestamp(),
191 payload_or_attrs.parent_beacon_block_root().is_some(),
192 )
193 }
194
195 fn ensure_well_formed_attributes(
196 &self,
197 version: EngineApiMessageVersion,
198 attributes: &OpPayloadAttributes,
199 ) -> Result<(), EngineObjectValidationError> {
200 validate_version_specific_fields(
201 self.chain_spec(),
202 version,
203 PayloadOrAttributes::<Self::ExecutionData, OpPayloadAttributes>::PayloadAttributes(
204 attributes,
205 ),
206 )?;
207
208 if attributes.gas_limit.is_none() {
209 return Err(EngineObjectValidationError::InvalidParams(
210 "MissingGasLimitInPayloadAttributes".to_string().into(),
211 ))
212 }
213
214 if self
215 .chain_spec()
216 .is_holocene_active_at_timestamp(attributes.payload_attributes.timestamp)
217 {
218 let (elasticity, denominator) =
219 attributes.decode_eip_1559_params().ok_or_else(|| {
220 EngineObjectValidationError::InvalidParams(
221 "MissingEip1559ParamsInPayloadAttributes".to_string().into(),
222 )
223 })?;
224 if elasticity != 0 && denominator == 0 {
225 return Err(EngineObjectValidationError::InvalidParams(
226 "Eip1559ParamsDenominatorZero".to_string().into(),
227 ))
228 }
229 }
230
231 Ok(())
232 }
233}
234
235pub fn validate_withdrawals_presence(
243 chain_spec: &ChainSpec,
244 version: EngineApiMessageVersion,
245 message_validation_kind: MessageValidationKind,
246 timestamp: u64,
247 has_withdrawals: bool,
248) -> Result<(), EngineObjectValidationError> {
249 let is_shanghai = chain_spec.fork(OpHardfork::Canyon).active_at_timestamp(timestamp);
250
251 match version {
252 EngineApiMessageVersion::V1 => {
253 if has_withdrawals {
254 return Err(message_validation_kind
255 .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1))
256 }
257 if is_shanghai {
258 return Err(message_validation_kind
259 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
260 }
261 }
262 EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 => {
263 if is_shanghai && !has_withdrawals {
264 return Err(message_validation_kind
265 .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai))
266 }
267 if !is_shanghai && has_withdrawals {
268 return Err(message_validation_kind
269 .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai))
270 }
271 }
272 };
273
274 Ok(())
275}
276
277#[cfg(test)]
278mod test {
279 use super::*;
280
281 use crate::engine;
282 use alloy_primitives::{b64, Address, B256, B64};
283 use alloy_rpc_types_engine::PayloadAttributes;
284 use reth_node_builder::EngineValidator;
285 use reth_optimism_chainspec::BASE_SEPOLIA;
286 use reth_provider::noop::NoopProvider;
287 use reth_trie_common::KeccakKeyHasher;
288
289 fn get_chainspec() -> Arc<OpChainSpec> {
290 Arc::new(OpChainSpec {
291 inner: ChainSpec {
292 chain: BASE_SEPOLIA.inner.chain,
293 genesis: BASE_SEPOLIA.inner.genesis.clone(),
294 genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(),
295 paris_block_and_final_difficulty: BASE_SEPOLIA
296 .inner
297 .paris_block_and_final_difficulty,
298 hardforks: BASE_SEPOLIA.inner.hardforks.clone(),
299 base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
300 prune_delete_limit: 10000,
301 ..Default::default()
302 },
303 })
304 }
305
306 const fn get_attributes(eip_1559_params: Option<B64>, timestamp: u64) -> OpPayloadAttributes {
307 OpPayloadAttributes {
308 gas_limit: Some(1000),
309 eip_1559_params,
310 transactions: None,
311 no_tx_pool: None,
312 payload_attributes: PayloadAttributes {
313 timestamp,
314 prev_randao: B256::ZERO,
315 suggested_fee_recipient: Address::ZERO,
316 withdrawals: Some(vec![]),
317 parent_beacon_block_root: Some(B256::ZERO),
318 },
319 }
320 }
321
322 #[test]
323 fn test_well_formed_attributes_pre_holocene() {
324 let validator =
325 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
326 let attributes = get_attributes(None, 1732633199);
327
328 let result = <engine::OpEngineValidator<_> as EngineValidator<
329 OpEngineTypes,
330 >>::ensure_well_formed_attributes(
331 &validator, EngineApiMessageVersion::V3, &attributes
332 );
333 assert!(result.is_ok());
334 }
335
336 #[test]
337 fn test_well_formed_attributes_holocene_no_eip1559_params() {
338 let validator =
339 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
340 let attributes = get_attributes(None, 1732633200);
341
342 let result = <engine::OpEngineValidator<_> as EngineValidator<
343 OpEngineTypes,
344 >>::ensure_well_formed_attributes(
345 &validator, EngineApiMessageVersion::V3, &attributes
346 );
347 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
348 }
349
350 #[test]
351 fn test_well_formed_attributes_holocene_eip1559_params_zero_denominator() {
352 let validator =
353 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
354 let attributes = get_attributes(Some(b64!("0000000000000008")), 1732633200);
355
356 let result = <engine::OpEngineValidator<_> as EngineValidator<
357 OpEngineTypes,
358 >>::ensure_well_formed_attributes(
359 &validator, EngineApiMessageVersion::V3, &attributes
360 );
361 assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_))));
362 }
363
364 #[test]
365 fn test_well_formed_attributes_holocene_valid() {
366 let validator =
367 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
368 let attributes = get_attributes(Some(b64!("0000000800000008")), 1732633200);
369
370 let result = <engine::OpEngineValidator<_> as EngineValidator<
371 OpEngineTypes,
372 >>::ensure_well_formed_attributes(
373 &validator, EngineApiMessageVersion::V3, &attributes
374 );
375 assert!(result.is_ok());
376 }
377
378 #[test]
379 fn test_well_formed_attributes_holocene_valid_all_zero() {
380 let validator =
381 OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
382 let attributes = get_attributes(Some(b64!("0000000000000000")), 1732633200);
383
384 let result = <engine::OpEngineValidator<_> as EngineValidator<
385 OpEngineTypes,
386 >>::ensure_well_formed_attributes(
387 &validator, EngineApiMessageVersion::V3, &attributes
388 );
389 assert!(result.is_ok());
390 }
391}