reth_optimism_node/
engine.rs

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/// The types used in the optimism beacon consensus engine.
28#[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/// Validator for Optimism engine API.
68#[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    /// Instantiates a new validator.
78    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    /// Returns the chain spec used by the validator.
109    #[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                // FIXME: we don't necessarily have access to the parent block here because the
141                // parent block isn't necessarily part of the canonical chain yet. Instead this
142                // function should receive the list of in memory blocks as input
143                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
254/// Validates the presence of the `withdrawals` field according to the payload timestamp.
255///
256/// After Canyon, withdrawals field must be [Some].
257/// Before Canyon, withdrawals field must be [None];
258///
259/// Canyon activates the Shanghai EIPs, see the Canyon specs for more details:
260/// <https://github.com/ethereum-optimism/optimism/blob/ab926c5fd1e55b5c864341c44842d6d1ca679d99/specs/superchain-upgrades.md#canyon>
261pub 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        // TODO: Remove this once we know the Jovian timestamp
318        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    /// After Jovian (and holocene), eip1559 params must be Some
443    #[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    /// Before Jovian, min base fee must be None
458    #[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    /// After Jovian, min base fee must be Some
473    #[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}