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, 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/// 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(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    /// 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 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                // FIXME: we don't necessarily have access to the parent block here because the
132                // parent block isn't necessarily part of the canonical chain yet. Instead this
133                // function should receive the list of in memory blocks as input
134                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
252/// Validates the presence of the `withdrawals` field according to the payload timestamp.
253///
254/// After Canyon, withdrawals field must be [Some].
255/// Before Canyon, withdrawals field must be [None];
256///
257/// Canyon activates the Shanghai EIPs, see the Canyon specs for more details:
258/// <https://github.com/ethereum-optimism/optimism/blob/ab926c5fd1e55b5c864341c44842d6d1ca679d99/specs/superchain-upgrades.md#canyon>
259pub 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        // TODO: Remove this once we know the Jovian timestamp
316        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    /// After Jovian (and holocene), eip1559 params must be Some
441    #[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    /// Before Jovian, min base fee must be None
456    #[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    /// After Jovian, min base fee must be Some
471    #[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}