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