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::{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<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/// A default payload type for [`OpEngineTypes`]
68#[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/// Validator for Optimism engine API.
79#[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    /// Instantiates a new validator.
88    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    /// Returns the chain spec used by the validator.
98    #[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
214/// Validates the presence of the `withdrawals` field according to the payload timestamp.
215///
216/// After Canyon, withdrawals field must be [Some].
217/// Before Canyon, withdrawals field must be [None];
218///
219/// Canyon activates the Shanghai EIPs, see the Canyon specs for more details:
220/// <https://github.com/ethereum-optimism/optimism/blob/ab926c5fd1e55b5c864341c44842d6d1ca679d99/specs/superchain-upgrades.md#canyon>
221pub 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}