diff --git a/Cargo.lock b/Cargo.lock index e191b69ebf53..2a083de8ad89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8426,6 +8426,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-trie", + "derive_more", "reth-chainspec", "reth-consensus", "reth-consensus-common", @@ -8433,6 +8434,8 @@ dependencies = [ "reth-optimism-forks", "reth-optimism-primitives", "reth-primitives", + "reth-storage-api", + "reth-storage-errors", "reth-trie-common", "tracing", ] @@ -8553,6 +8556,7 @@ dependencies = [ "reth-optimism-consensus", "reth-optimism-evm", "reth-optimism-forks", + "reth-optimism-primitives", "reth-payload-builder", "reth-payload-builder-primitives", "reth-payload-primitives", diff --git a/crates/consensus/consensus/src/lib.rs b/crates/consensus/consensus/src/lib.rs index ba1b1321e776..8c7b9ff3aa00 100644 --- a/crates/consensus/consensus/src/lib.rs +++ b/crates/consensus/consensus/src/lib.rs @@ -451,6 +451,11 @@ pub enum ConsensusError { /// The block's timestamp. timestamp: u64, }, + /// Custom error + // todo: remove in favour of AT Consensus::Error, so OpConsensusError can wrap ConsensusError + // in a variant instead + #[display("custom l2 error (search for it in debug logs)")] + Other, } impl ConsensusError { diff --git a/crates/optimism/chainspec/src/lib.rs b/crates/optimism/chainspec/src/lib.rs index 907599fe2a29..7a1dcd19b3f4 100644 --- a/crates/optimism/chainspec/src/lib.rs +++ b/crates/optimism/chainspec/src/lib.rs @@ -1038,7 +1038,7 @@ mod tests { OpHardfork::Fjord.boxed(), OpHardfork::Granite.boxed(), OpHardfork::Holocene.boxed(), - // OpHardfork::Isthmus.boxed(), + //OpHardfork::Isthmus.boxed(), ]; assert!(expected_hardforks diff --git a/crates/optimism/consensus/Cargo.toml b/crates/optimism/consensus/Cargo.toml index 4f4868a454dc..57dfc7bcd680 100644 --- a/crates/optimism/consensus/Cargo.toml +++ b/crates/optimism/consensus/Cargo.toml @@ -18,6 +18,8 @@ reth-consensus-common.workspace = true reth-consensus.workspace = true reth-primitives.workspace = true reth-trie-common.workspace = true +reth-storage-api.workspace = true +reth-storage-errors.workspace = true # op-reth reth-optimism-forks.workspace = true @@ -31,6 +33,8 @@ alloy-primitives.workspace = true alloy-consensus.workspace = true alloy-trie.workspace = true +# misc +derive_more = { workspace = true, features = ["display", "error"] } tracing.workspace = true [dev-dependencies] diff --git a/crates/optimism/consensus/src/error.rs b/crates/optimism/consensus/src/error.rs new file mode 100644 index 000000000000..180b9fdc2962 --- /dev/null +++ b/crates/optimism/consensus/src/error.rs @@ -0,0 +1,33 @@ +//! Optimism consensus errors + +use alloy_primitives::B256; +use derive_more::{Display, Error, From}; +use reth_storage_errors::ProviderError; + +/// Optimism consensus error. +#[derive(Debug, PartialEq, Eq, Clone, Display, Error, From)] +pub enum OpConsensusError { + /// Block body has non-empty withdrawals list. + #[display("non-empty withdrawals list")] + WithdrawalsNonEmpty, + /// Failed to load storage root of + /// [`L2toL1MessagePasser`](reth_optimism_primitives::ADDRESS_L2_TO_L1_MESSAGE_PASSER). + #[display("failed to load storage root of L2toL1MessagePasser pre-deploy: {_0}")] + #[from] + LoadStorageRootFailed(ProviderError), + /// Storage root of + /// [`L2toL1MessagePasser`](reth_optimism_primitives::ADDRESS_L2_TO_L1_MESSAGE_PASSER) missing + /// in block (withdrawals root field). + #[display("storage root of L2toL1MessagePasser missing (withdrawals root field empty)")] + StorageRootMissing, + /// Storage root of + /// [`L2toL1MessagePasser`](reth_optimism_primitives::ADDRESS_L2_TO_L1_MESSAGE_PASSER) + /// in block (withdrawals field), doesn't match local storage root. + #[display("L2toL1MessagePasser storage root mismatch, got: {}, expected {expected}", got.map(|hash| hash.to_string()).unwrap_or_else(|| "null".to_string()))] + StorageRootMismatch { + /// Storage root of pre-deploy in block. + got: Option, + /// Storage root of pre-deploy loaded from local state. + expected: B256, + }, +} diff --git a/crates/optimism/consensus/src/lib.rs b/crates/optimism/consensus/src/lib.rs index d05ff9c9bd76..2743c45e0680 100644 --- a/crates/optimism/consensus/src/lib.rs +++ b/crates/optimism/consensus/src/lib.rs @@ -9,6 +9,8 @@ // The `optimism` feature must be enabled to use this crate. #![cfg(feature = "optimism")] +use core::fmt; + use alloy_consensus::{BlockHeader, Header, EMPTY_OMMER_ROOT_HASH}; use alloy_primitives::{B64, U256}; use reth_chainspec::EthereumHardforks; @@ -19,13 +21,18 @@ use reth_consensus_common::validation::{ validate_against_parent_4844, validate_against_parent_eip1559_base_fee, validate_against_parent_hash_number, validate_against_parent_timestamp, validate_body_against_header, validate_cancun_gas, validate_header_base_fee, - validate_header_extradata, validate_header_gas, validate_shanghai_withdrawals, + validate_header_extradata, validate_header_gas, }; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_forks::OpHardforks; -use reth_optimism_primitives::OpPrimitives; +use reth_optimism_primitives::{predeploys::ADDRESS_L2_TO_L1_MESSAGE_PASSER, OpPrimitives}; use reth_primitives::{BlockBody, BlockWithSenders, GotExpected, SealedBlock, SealedHeader}; +use reth_storage_api::{StateProviderFactory, StorageRootProvider}; use std::{sync::Arc, time::SystemTime}; +use tracing::debug; + +pub mod error; +pub use error::OpConsensusError; mod proof; pub use proof::calculate_receipt_root_no_memo_optimism; @@ -37,19 +44,23 @@ pub use validation::validate_block_post_execution; /// /// Provides basic checks as outlined in the execution specs. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct OpBeaconConsensus { +pub struct OpBeaconConsensus

{ /// Configuration chain_spec: Arc, + provider: P, } -impl OpBeaconConsensus { +impl

OpBeaconConsensus

{ /// Create a new instance of [`OpBeaconConsensus`] - pub const fn new(chain_spec: Arc) -> Self { - Self { chain_spec } + pub const fn new(chain_spec: Arc, provider: P) -> Self { + Self { chain_spec, provider } } } -impl FullConsensus for OpBeaconConsensus { +impl

FullConsensus for OpBeaconConsensus

+where + P: StateProviderFactory + fmt::Debug, +{ fn validate_block_post_execution( &self, block: &BlockWithSenders, @@ -59,7 +70,10 @@ impl FullConsensus for OpBeaconConsensus { } } -impl Consensus for OpBeaconConsensus { +impl

Consensus for OpBeaconConsensus

+where + P: StateProviderFactory + fmt::Debug, +{ fn validate_body_against_header( &self, body: &BlockBody, @@ -83,19 +97,67 @@ impl Consensus for OpBeaconConsensus { } // EIP-4895: Beacon chain push withdrawals as operations - if self.chain_spec.is_shanghai_active_at_timestamp(block.timestamp) { - validate_shanghai_withdrawals(block)?; + if self.chain_spec.is_shanghai_active_at_timestamp(block.timestamp) && + block.body.withdrawals.as_ref().is_some_and(|withdrawals| !withdrawals.is_empty()) + { + debug!(target: "op::consensus", + block_number=block.number, + err=%OpConsensusError::WithdrawalsNonEmpty, + "block failed validation", + ); + return Err(ConsensusError::Other) } if self.chain_spec.is_cancun_active_at_timestamp(block.timestamp) { validate_cancun_gas(block)?; } + if self.chain_spec.is_isthmus_active_at_timestamp(block.timestamp) { + let storage_root_msg_passer = self + .provider + .latest() + .map_err(|err| { + debug!(target: "op::consensus", + block_number=block.number, + err=%OpConsensusError::LoadStorageRootFailed(err), + "failed to load latest state", + ); + + ConsensusError::Other + })? + .storage_root(ADDRESS_L2_TO_L1_MESSAGE_PASSER, Default::default()) + .map_err(|err| { + debug!(target: "op::consensus", + block_number=block.number, + err=%OpConsensusError::LoadStorageRootFailed(err), + "failed to load storage root for L2toL1MessagePasser pre-deploy", + ); + + ConsensusError::Other + })?; + + if block.withdrawals_root.is_none_or(|root| root != storage_root_msg_passer) { + debug!(target: "op::consensus", + block_number=block.number, + err=%OpConsensusError::StorageRootMismatch { + got: block.withdrawals_root, + expected: storage_root_msg_passer + }, + "block failed validation", + ); + + return Err(ConsensusError::Other) + } + } + Ok(()) } } -impl HeaderValidator for OpBeaconConsensus { +impl

HeaderValidator for OpBeaconConsensus

+where + P: Send + Sync + fmt::Debug, +{ fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { validate_header_gas(header.header())?; validate_header_base_fee(header.header(), &self.chain_spec) diff --git a/crates/optimism/node/src/node.rs b/crates/optimism/node/src/node.rs index 43585b3762fb..00228e81251b 100644 --- a/crates/optimism/node/src/node.rs +++ b/crates/optimism/node/src/node.rs @@ -7,6 +7,7 @@ use crate::{ OpEngineTypes, }; use alloy_consensus::Header; +use core::fmt; use reth_basic_payload_builder::{BasicPayloadJobGenerator, BasicPayloadJobGeneratorConfig}; use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; use reth_db::transaction::{DbTx, DbTxMut}; @@ -152,6 +153,7 @@ impl OpNode { ChainSpec = OpChainSpec, Primitives = OpPrimitives, >, + Provider: fmt::Debug, >, { let RollupArgs { disable_txpool_gossip, compute_pending_block, discovery_v4, .. } = args; @@ -177,6 +179,7 @@ where Primitives = OpPrimitives, Storage = OpStorage, >, + Provider: fmt::Debug, >, { type ComponentsBuilder = ComponentsBuilder< @@ -679,12 +682,15 @@ pub struct OpConsensusBuilder; impl ConsensusBuilder for OpConsensusBuilder where - Node: FullNodeTypes>, + Node: FullNodeTypes< + Types: NodeTypes, + Provider: fmt::Debug, + >, { - type Consensus = Arc; + type Consensus = Arc>; async fn build_consensus(self, ctx: &BuilderContext) -> eyre::Result { - Ok(Arc::new(OpBeaconConsensus::new(ctx.chain_spec()))) + Ok(Arc::new(OpBeaconConsensus::new(ctx.chain_spec(), ctx.provider().clone()))) } } diff --git a/crates/optimism/node/tests/it/priority.rs b/crates/optimism/node/tests/it/priority.rs index 1b49ed684bfc..44ed46d7816a 100644 --- a/crates/optimism/node/tests/it/priority.rs +++ b/crates/optimism/node/tests/it/priority.rs @@ -1,5 +1,7 @@ //! Node builder test that customizes priority of transactions in the block. +use core::fmt; + use alloy_consensus::TxEip1559; use alloy_genesis::Genesis; use alloy_network::TxSignerSync; @@ -100,6 +102,7 @@ where ChainSpec = OpChainSpec, Primitives = OpPrimitives, >, + Provider: fmt::Debug, >, { let RollupArgs { disable_txpool_gossip, compute_pending_block, discovery_v4, .. } = diff --git a/crates/optimism/payload/Cargo.toml b/crates/optimism/payload/Cargo.toml index 1c4f855b6aa2..1551e715221f 100644 --- a/crates/optimism/payload/Cargo.toml +++ b/crates/optimism/payload/Cargo.toml @@ -33,6 +33,7 @@ reth-optimism-chainspec.workspace = true reth-optimism-consensus.workspace = true reth-optimism-evm.workspace = true reth-optimism-forks.workspace = true +reth-optimism-primitives.workspace = true # ethereum revm.workspace = true @@ -57,5 +58,6 @@ optimism = [ "reth-optimism-evm/optimism", "revm/optimism", "reth-execution-types/optimism", - "reth-optimism-consensus/optimism" + "reth-optimism-consensus/optimism", + "reth-optimism-primitives/optimism", ] \ No newline at end of file diff --git a/crates/optimism/payload/src/builder.rs b/crates/optimism/payload/src/builder.rs index 27778da8f429..150988b6a051 100644 --- a/crates/optimism/payload/src/builder.rs +++ b/crates/optimism/payload/src/builder.rs @@ -5,7 +5,7 @@ use crate::{ error::OpPayloadBuilderError, payload::{OpBuiltPayload, OpPayloadBuilderAttributes}, }; -use alloy_consensus::{Header, Transaction, EMPTY_OMMER_ROOT_HASH}; +use alloy_consensus::{constants::EMPTY_WITHDRAWALS, Header, Transaction, EMPTY_OMMER_ROOT_HASH}; use alloy_eips::{eip4895::Withdrawals, merge::BEACON_NONCE}; use alloy_primitives::{Address, Bytes, B256, U256}; use alloy_rpc_types_debug::ExecutionWitness; @@ -20,6 +20,7 @@ use reth_execution_types::ExecutionOutcome; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_consensus::calculate_receipt_root_no_memo_optimism; use reth_optimism_forks::OpHardforks; +use reth_optimism_primitives::ADDRESS_L2_TO_L1_MESSAGE_PASSER; use reth_payload_builder_primitives::PayloadBuilderError; use reth_payload_primitives::PayloadBuilderAttributes; use reth_payload_util::PayloadTransactions; @@ -29,7 +30,7 @@ use reth_primitives::{ }; use reth_provider::{ HashedPostStateProvider, ProviderError, StateProofProvider, StateProviderFactory, - StateRootProvider, + StateRootProvider, StorageRootProvider, }; use reth_revm::{database::StateProviderDatabase, witness::ExecutionWitnessRecord}; use reth_transaction_pool::{ @@ -295,14 +296,15 @@ where Txs: OpPayloadTransactions, { /// Executes the payload and returns the outcome. - pub fn execute( + pub fn execute( self, state: &mut State, ctx: &OpPayloadBuilderCtx, ) -> Result, PayloadBuilderError> where EvmConfig: ConfigureEvm

, - DB: Database, + DB: Database + AsRef

, + P: StorageRootProvider, { let Self { pool, best } = self; debug!(target: "payload_builder", id=%ctx.payload_id(), parent_header = ?ctx.parent().hash(), parent_number = ctx.parent().number, "building new payload"); @@ -330,13 +332,26 @@ where } } - let withdrawals_root = ctx.commit_withdrawals(state)?; + debug_assert!(ctx.attributes().payload_attributes.withdrawals.is_empty()); // merge all transitions into bundle state, this would apply the withdrawal balance changes // and 4788 contract call state.merge_transitions(BundleRetention::Reverts); - Ok(BuildOutcomeKind::Better { payload: ExecutedPayload { info, withdrawals_root } }) + // withdrawals root field in block header is used for storage root of L2 predeploy + // `l2tol1-message-passer` + let withdrawals_root = Some(if ctx.is_isthmus_active() { + state + .database + .as_ref() + .storage_root(ADDRESS_L2_TO_L1_MESSAGE_PASSER, Default::default())? + } else { + EMPTY_WITHDRAWALS + }); + + let payload = ExecutedPayload { info, withdrawals_root }; + + Ok(BuildOutcomeKind::Better { payload }) } /// Builds the payload on top of the state. @@ -348,7 +363,7 @@ where where EvmConfig: ConfigureEvm

, DB: Database + AsRef

, - P: StateRootProvider + HashedPostStateProvider, + P: StateRootProvider + HashedPostStateProvider + StorageRootProvider, { let ExecutedPayload { info, withdrawals_root } = match self.execute(&mut state, &ctx)? { BuildOutcomeKind::Better { payload } | BuildOutcomeKind::Freeze(payload) => payload, @@ -474,7 +489,7 @@ where where EvmConfig: ConfigureEvm

, DB: Database + AsRef

, - P: StateProofProvider, + P: StateProofProvider + StorageRootProvider, { let _ = self.execute(state, ctx)?; let ExecutionWitnessRecord { hashed_state, codes, keys } = @@ -666,6 +681,11 @@ impl OpPayloadBuilderCtx { self.chain_spec.is_holocene_active_at_timestamp(self.attributes().timestamp()) } + /// Returns true if isthmus is active for the payload. + pub fn is_isthmus_active(&self) -> bool { + self.chain_spec.is_isthmus_active_at_timestamp(self.attributes().timestamp()) + } + /// Returns true if the fees are higher than the previous payload. pub fn is_better_payload(&self, total_fees: U256) -> bool { is_better_payload(self.best_payload.as_ref(), total_fees) diff --git a/crates/optimism/primitives/src/lib.rs b/crates/optimism/primitives/src/lib.rs index b1f029d20bc2..14aacdb2a857 100644 --- a/crates/optimism/primitives/src/lib.rs +++ b/crates/optimism/primitives/src/lib.rs @@ -14,8 +14,10 @@ extern crate alloc; pub mod bedrock; +pub mod predeploys; pub mod transaction; +pub use predeploys::ADDRESS_L2_TO_L1_MESSAGE_PASSER; pub use transaction::{signed::OpTransactionSigned, tx_type::OpTxType, OpTransaction}; /// Optimism primitive types. diff --git a/crates/optimism/primitives/src/predeploys.rs b/crates/optimism/primitives/src/predeploys.rs new file mode 100644 index 000000000000..1a306e86967c --- /dev/null +++ b/crates/optimism/primitives/src/predeploys.rs @@ -0,0 +1,8 @@ +//! Addresses of OP pre-deploys. +// todo: move to alloy + +use alloy_primitives::{address, Address}; + +/// The L2 contract `L2ToL1MessagePasser`, stores commitments to withdrawal transactions. +pub const ADDRESS_L2_TO_L1_MESSAGE_PASSER: Address = + address!("4200000000000000000000000000000000000016"); diff --git a/crates/storage/errors/src/lib.rs b/crates/storage/errors/src/lib.rs index 6abb0cd9b425..4beca464b12b 100644 --- a/crates/storage/errors/src/lib.rs +++ b/crates/storage/errors/src/lib.rs @@ -22,3 +22,5 @@ pub mod provider; /// Writer error pub mod writer; + +pub use provider::ProviderError; diff --git a/crates/storage/provider/src/providers/mod.rs b/crates/storage/provider/src/providers/mod.rs index d02c59278666..9e9e1d16fe0e 100644 --- a/crates/storage/provider/src/providers/mod.rs +++ b/crates/storage/provider/src/providers/mod.rs @@ -1,3 +1,5 @@ +use core::fmt; + use crate::{ AccountReader, BlockHashReader, BlockIdReader, BlockNumReader, BlockReader, BlockReaderIdExt, BlockSource, BlockchainTreePendingStateProvider, CanonStateNotifications, @@ -124,7 +126,6 @@ impl TreeNodeTypes for T where T: ProviderNodeTypes + NodeTypesForTree {} /// This type serves as the main entry point for interacting with the blockchain and provides data /// from database storage and from the blockchain tree (pending state etc.) It is a simple wrapper /// type that holds an instance of the database and the blockchain tree. -#[allow(missing_debug_implementations)] pub struct BlockchainProvider { /// Provider type used to access the database. database: ProviderFactory, @@ -964,3 +965,9 @@ impl AccountReader for BlockchainProvider { self.database.provider()?.basic_account(address) } } + +impl fmt::Debug for BlockchainProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlockchainProvider").finish_non_exhaustive() + } +}