use alloy_consensus::BlockHeader; use alloy_primitives::B256; use alloy_rpc_types_engine::{ExecutionPayloadEnvelopeV2, ExecutionPayloadV1}; use op_alloy_rpc_types_engine::{ OpExecutionData, OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpPayloadAttributes, }; use reth_chainspec::ChainSpec; use reth_consensus::ConsensusError; use reth_node_api::{ payload::{ validate_parent_beacon_block_root_presence, EngineApiMessageVersion, EngineObjectValidationError, MessageValidationKind, NewPayloadError, PayloadOrAttributes, PayloadTypes, VersionSpecificValidationError, }, validate_version_specific_fields, BuiltPayload, EngineTypes, EngineValidator, NodePrimitives, PayloadValidator, }; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_consensus::isthmus; use reth_optimism_forks::{OpHardfork, OpHardforks}; use reth_optimism_payload_builder::{OpExecutionPayloadValidator, OpPayloadTypes}; use reth_optimism_primitives::{OpBlock, ADDRESS_L2_TO_L1_MESSAGE_PASSER}; use reth_primitives_traits::{RecoveredBlock, SealedBlock}; use reth_provider::StateProviderFactory; use reth_trie_common::{HashedPostState, KeyHasher}; use std::sync::Arc; /// The types used in the optimism beacon consensus engine. #[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)] #[non_exhaustive] pub struct OpEngineTypes { _marker: std::marker::PhantomData, } impl< T: PayloadTypes< ExecutionData = OpExecutionData, BuiltPayload: BuiltPayload>, >, > PayloadTypes for OpEngineTypes { type ExecutionData = T::ExecutionData; type BuiltPayload = T::BuiltPayload; type PayloadAttributes = T::PayloadAttributes; type PayloadBuilderAttributes = T::PayloadBuilderAttributes; fn block_to_payload( block: SealedBlock< <::Primitives as NodePrimitives>::Block, >, ) -> ::ExecutionData { OpExecutionData::from_block_unchecked(block.hash(), &block.into_block()) } } impl> EngineTypes for OpEngineTypes where T::BuiltPayload: BuiltPayload> + TryInto + TryInto + TryInto + TryInto, { type ExecutionPayloadEnvelopeV1 = ExecutionPayloadV1; type ExecutionPayloadEnvelopeV2 = ExecutionPayloadEnvelopeV2; type ExecutionPayloadEnvelopeV3 = OpExecutionPayloadEnvelopeV3; type ExecutionPayloadEnvelopeV4 = OpExecutionPayloadEnvelopeV4; type ExecutionPayloadEnvelopeV5 = OpExecutionPayloadEnvelopeV4; } /// Validator for Optimism engine API. #[derive(Debug, Clone)] pub struct OpEngineValidator

{ inner: OpExecutionPayloadValidator, provider: P, hashed_addr_l2tol1_msg_passer: B256, } impl

OpEngineValidator

{ /// Instantiates a new validator. pub fn new(chain_spec: Arc, provider: P) -> Self { let hashed_addr_l2tol1_msg_passer = KH::hash_key(ADDRESS_L2_TO_L1_MESSAGE_PASSER); Self { inner: OpExecutionPayloadValidator::new(chain_spec), provider, hashed_addr_l2tol1_msg_passer, } } /// Returns the chain spec used by the validator. #[inline] fn chain_spec(&self) -> &OpChainSpec { self.inner.chain_spec() } } impl

PayloadValidator for OpEngineValidator

where P: StateProviderFactory + Unpin + 'static, { type Block = OpBlock; type ExecutionData = OpExecutionData; fn ensure_well_formed_payload( &self, payload: Self::ExecutionData, ) -> Result, NewPayloadError> { let sealed_block = self.inner.ensure_well_formed_payload(payload).map_err(NewPayloadError::other)?; sealed_block.try_recover().map_err(|e| NewPayloadError::Other(e.into())) } fn validate_block_post_execution_with_hashed_state( &self, state_updates: &HashedPostState, block: &RecoveredBlock, ) -> Result<(), ConsensusError> { if self.chain_spec().is_isthmus_active_at_timestamp(block.timestamp()) { let Ok(state) = self.provider.state_by_block_hash(block.parent_hash()) else { // FIXME: we don't necessarily have access to the parent block here because the // parent block isn't necessarily part of the canonical chain yet. Instead this // function should receive the list of in memory blocks as input return Ok(()) }; let predeploy_storage_updates = state_updates .storages .get(&self.hashed_addr_l2tol1_msg_passer) .cloned() .unwrap_or_default(); isthmus::verify_withdrawals_root_prehashed( predeploy_storage_updates, state, block.header(), ) .map_err(|err| { ConsensusError::Other(format!("failed to verify block post-execution: {err}")) })? } Ok(()) } } impl EngineValidator for OpEngineValidator

where Types: PayloadTypes, P: StateProviderFactory + Unpin + 'static, { fn validate_version_specific_fields( &self, version: EngineApiMessageVersion, payload_or_attrs: PayloadOrAttributes<'_, Self::ExecutionData, OpPayloadAttributes>, ) -> Result<(), EngineObjectValidationError> { validate_withdrawals_presence( self.chain_spec(), version, payload_or_attrs.message_validation_kind(), payload_or_attrs.timestamp(), payload_or_attrs.withdrawals().is_some(), )?; validate_parent_beacon_block_root_presence( self.chain_spec(), version, payload_or_attrs.message_validation_kind(), payload_or_attrs.timestamp(), payload_or_attrs.parent_beacon_block_root().is_some(), ) } fn ensure_well_formed_attributes( &self, version: EngineApiMessageVersion, attributes: &OpPayloadAttributes, ) -> Result<(), EngineObjectValidationError> { validate_version_specific_fields( self.chain_spec(), version, PayloadOrAttributes::::PayloadAttributes( attributes, ), )?; if attributes.gas_limit.is_none() { return Err(EngineObjectValidationError::InvalidParams( "MissingGasLimitInPayloadAttributes".to_string().into(), )) } if self .chain_spec() .is_holocene_active_at_timestamp(attributes.payload_attributes.timestamp) { let (elasticity, denominator) = attributes.decode_eip_1559_params().ok_or_else(|| { EngineObjectValidationError::InvalidParams( "MissingEip1559ParamsInPayloadAttributes".to_string().into(), ) })?; if elasticity != 0 && denominator == 0 { return Err(EngineObjectValidationError::InvalidParams( "Eip1559ParamsDenominatorZero".to_string().into(), )) } } Ok(()) } } /// Validates the presence of the `withdrawals` field according to the payload timestamp. /// /// After Canyon, withdrawals field must be [Some]. /// Before Canyon, withdrawals field must be [None]; /// /// Canyon activates the Shanghai EIPs, see the Canyon specs for more details: /// pub fn validate_withdrawals_presence( chain_spec: &ChainSpec, version: EngineApiMessageVersion, message_validation_kind: MessageValidationKind, timestamp: u64, has_withdrawals: bool, ) -> Result<(), EngineObjectValidationError> { let is_shanghai = chain_spec.fork(OpHardfork::Canyon).active_at_timestamp(timestamp); match version { EngineApiMessageVersion::V1 => { if has_withdrawals { return Err(message_validation_kind .to_error(VersionSpecificValidationError::WithdrawalsNotSupportedInV1)) } if is_shanghai { return Err(message_validation_kind .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai)) } } EngineApiMessageVersion::V2 | EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => { if is_shanghai && !has_withdrawals { return Err(message_validation_kind .to_error(VersionSpecificValidationError::NoWithdrawalsPostShanghai)) } if !is_shanghai && has_withdrawals { return Err(message_validation_kind .to_error(VersionSpecificValidationError::HasWithdrawalsPreShanghai)) } } }; Ok(()) } #[cfg(test)] mod test { use super::*; use crate::engine; use alloy_primitives::{b64, Address, B256, B64}; use alloy_rpc_types_engine::PayloadAttributes; use reth_node_builder::EngineValidator; use reth_optimism_chainspec::BASE_SEPOLIA; use reth_provider::noop::NoopProvider; use reth_trie_common::KeccakKeyHasher; fn get_chainspec() -> Arc { Arc::new(OpChainSpec { inner: ChainSpec { chain: BASE_SEPOLIA.inner.chain, genesis: BASE_SEPOLIA.inner.genesis.clone(), genesis_header: BASE_SEPOLIA.inner.genesis_header.clone(), paris_block_and_final_difficulty: BASE_SEPOLIA .inner .paris_block_and_final_difficulty, hardforks: BASE_SEPOLIA.inner.hardforks.clone(), base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(), prune_delete_limit: 10000, ..Default::default() }, }) } const fn get_attributes(eip_1559_params: Option, timestamp: u64) -> OpPayloadAttributes { OpPayloadAttributes { gas_limit: Some(1000), eip_1559_params, transactions: None, no_tx_pool: None, payload_attributes: PayloadAttributes { timestamp, prev_randao: B256::ZERO, suggested_fee_recipient: Address::ZERO, withdrawals: Some(vec![]), parent_beacon_block_root: Some(B256::ZERO), }, } } #[test] fn test_well_formed_attributes_pre_holocene() { let validator = OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); let attributes = get_attributes(None, 1732633199); let result = as EngineValidator< OpEngineTypes, >>::ensure_well_formed_attributes( &validator, EngineApiMessageVersion::V3, &attributes ); assert!(result.is_ok()); } #[test] fn test_well_formed_attributes_holocene_no_eip1559_params() { let validator = OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); let attributes = get_attributes(None, 1732633200); let result = as EngineValidator< OpEngineTypes, >>::ensure_well_formed_attributes( &validator, EngineApiMessageVersion::V3, &attributes ); assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_)))); } #[test] fn test_well_formed_attributes_holocene_eip1559_params_zero_denominator() { let validator = OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); let attributes = get_attributes(Some(b64!("0000000000000008")), 1732633200); let result = as EngineValidator< OpEngineTypes, >>::ensure_well_formed_attributes( &validator, EngineApiMessageVersion::V3, &attributes ); assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_)))); } #[test] fn test_well_formed_attributes_holocene_valid() { let validator = OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); let attributes = get_attributes(Some(b64!("0000000800000008")), 1732633200); let result = as EngineValidator< OpEngineTypes, >>::ensure_well_formed_attributes( &validator, EngineApiMessageVersion::V3, &attributes ); assert!(result.is_ok()); } #[test] fn test_well_formed_attributes_holocene_valid_all_zero() { let validator = OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); let attributes = get_attributes(Some(b64!("0000000000000000")), 1732633200); let result = as EngineValidator< OpEngineTypes, >>::ensure_well_formed_attributes( &validator, EngineApiMessageVersion::V3, &attributes ); assert!(result.is_ok()); } }