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_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/// The types used in the optimism beacon consensus engine.
32#[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/// A default payload type for [`OpEngineTypes`]
74#[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/// Validator for Optimism engine API.
97#[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    /// Instantiates a new validator.
106    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    /// Returns the chain spec used by the validator.
116    #[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                // FIXME: we don't necessarily have access to the parent block here because the
146                // parent block isn't necessarily part of the canonical chain yet. Instead this
147                // function should receive the list of in memory blocks as input
148                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
235/// Validates the presence of the `withdrawals` field according to the payload timestamp.
236///
237/// After Canyon, withdrawals field must be [Some].
238/// Before Canyon, withdrawals field must be [None];
239///
240/// Canyon activates the Shanghai EIPs, see the Canyon specs for more details:
241/// <https://github.com/ethereum-optimism/optimism/blob/ab926c5fd1e55b5c864341c44842d6d1ca679d99/specs/superchain-upgrades.md#canyon>
242pub 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}