mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat(l2-withdrawals): consensus rules (#14308)
This commit is contained in:
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -2460,15 +2460,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding-macro"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b"
|
||||
checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"data-encoding-macro-internal",
|
||||
@ -2476,9 +2476,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding-macro-internal"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b"
|
||||
checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 2.0.98",
|
||||
@ -8457,6 +8457,7 @@ dependencies = [
|
||||
name = "reth-optimism-consensus"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"alloy-chains",
|
||||
"alloy-consensus",
|
||||
"alloy-eips",
|
||||
"alloy-primitives",
|
||||
@ -8465,12 +8466,23 @@ dependencies = [
|
||||
"reth-chainspec",
|
||||
"reth-consensus",
|
||||
"reth-consensus-common",
|
||||
"reth-db-common",
|
||||
"reth-execution-types",
|
||||
"reth-optimism-chainspec",
|
||||
"reth-optimism-forks",
|
||||
"reth-optimism-node",
|
||||
"reth-optimism-primitives",
|
||||
"reth-primitives",
|
||||
"reth-primitives-traits",
|
||||
"reth-provider",
|
||||
"reth-revm",
|
||||
"reth-storage-api",
|
||||
"reth-storage-errors",
|
||||
"reth-trie",
|
||||
"reth-trie-common",
|
||||
"reth-trie-db",
|
||||
"revm",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::{fmt::Debug, sync::Arc, vec::Vec};
|
||||
use alloc::{fmt::Debug, string::String, sync::Arc, vec::Vec};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256, U256};
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
@ -433,6 +433,9 @@ pub enum ConsensusError {
|
||||
/// The block's timestamp.
|
||||
timestamp: u64,
|
||||
},
|
||||
/// Other, likely an injected L2 error.
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl ConsensusError {
|
||||
|
||||
@ -19,6 +19,9 @@ reth-consensus-common.workspace = true
|
||||
reth-consensus.workspace = true
|
||||
reth-primitives.workspace = true
|
||||
reth-primitives-traits.workspace = true
|
||||
reth-storage-api.workspace = true
|
||||
reth-storage-errors.workspace = true
|
||||
reth-trie-common.workspace = true
|
||||
|
||||
# op-reth
|
||||
reth-optimism-forks.workspace = true
|
||||
@ -31,13 +34,24 @@ alloy-eips.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-trie.workspace = true
|
||||
revm.workspace = true
|
||||
op-alloy-consensus.workspace = true
|
||||
|
||||
# misc
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
reth-provider = { workspace = true, features = ["test-utils"] }
|
||||
reth-trie-db.workspace = true
|
||||
reth-db-common.workspace = true
|
||||
reth-optimism-node.workspace = true
|
||||
reth-revm.workspace = true
|
||||
op-alloy-consensus.workspace = true
|
||||
alloy-chains.workspace = true
|
||||
alloy-primitives.workspace = true
|
||||
reth-optimism-chainspec.workspace = true
|
||||
reth-trie.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
@ -50,14 +64,24 @@ std = [
|
||||
"reth-optimism-forks/std",
|
||||
"reth-optimism-chainspec/std",
|
||||
"reth-optimism-primitives/std",
|
||||
"reth-storage-api/std",
|
||||
"reth-storage-errors/std",
|
||||
"reth-trie-common/std",
|
||||
"alloy-chains/std",
|
||||
"alloy-eips/std",
|
||||
"alloy-primitives/std",
|
||||
"alloy-consensus/std",
|
||||
"alloy-trie/std",
|
||||
"op-alloy-consensus/std",
|
||||
"reth-revm/std",
|
||||
"revm/std",
|
||||
"tracing/std",
|
||||
"thiserror/std",
|
||||
"reth-execution-types/std",
|
||||
]
|
||||
optimism = [
|
||||
"reth-optimism-primitives/optimism",
|
||||
"revm/optimism",
|
||||
"reth-execution-types/optimism",
|
||||
"reth-optimism-node/optimism",
|
||||
]
|
||||
|
||||
30
crates/optimism/consensus/src/error.rs
Normal file
30
crates/optimism/consensus/src/error.rs
Normal file
@ -0,0 +1,30 @@
|
||||
//! Optimism consensus errors
|
||||
|
||||
use alloy_primitives::B256;
|
||||
use reth_consensus::ConsensusError;
|
||||
use reth_storage_errors::provider::ProviderError;
|
||||
|
||||
/// Optimism consensus error.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum OpConsensusError {
|
||||
/// Block body has non-empty withdrawals list (l1 withdrawals).
|
||||
#[error("non-empty block body withdrawals list")]
|
||||
WithdrawalsNonEmpty,
|
||||
/// Failed to compute L2 withdrawals storage root.
|
||||
#[error("compute L2 withdrawals root failed: {_0}")]
|
||||
L2WithdrawalsRootCalculationFail(#[from] ProviderError),
|
||||
/// L2 withdrawals root missing in block header.
|
||||
#[error("L2 withdrawals root missing from block header")]
|
||||
L2WithdrawalsRootMissing,
|
||||
/// L2 withdrawals root in block header, doesn't match local storage root of predeploy.
|
||||
#[error("L2 withdrawals root mismatch, header: {header}, exec_res: {exec_res}")]
|
||||
L2WithdrawalsRootMismatch {
|
||||
/// Storage root of pre-deploy in block.
|
||||
header: B256,
|
||||
/// Storage root of pre-deploy loaded from local state.
|
||||
exec_res: B256,
|
||||
},
|
||||
/// L1 [`ConsensusError`], that also occurs on L2.
|
||||
#[error(transparent)]
|
||||
Eth(#[from] ConsensusError),
|
||||
}
|
||||
@ -12,33 +12,36 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use core::fmt::Debug;
|
||||
|
||||
use alloc::sync::Arc;
|
||||
use alloc::{format, sync::Arc};
|
||||
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
|
||||
use alloy_primitives::{B64, U256};
|
||||
use core::fmt::Debug;
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator};
|
||||
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_extra_data, validate_header_gas, validate_shanghai_withdrawals,
|
||||
validate_header_extra_data, validate_header_gas,
|
||||
};
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_optimism_forks::OpHardforks;
|
||||
use reth_optimism_primitives::DepositReceipt;
|
||||
use reth_primitives::{GotExpected, NodePrimitives, RecoveredBlock, SealedHeader};
|
||||
use reth_primitives_traits::{Block, BlockBody, BlockHeader, SealedBlock};
|
||||
|
||||
mod proof;
|
||||
pub use proof::calculate_receipt_root_no_memo_optimism;
|
||||
use reth_primitives_traits::{Block, BlockBody, BlockHeader, SealedBlock};
|
||||
|
||||
mod validation;
|
||||
pub mod validation;
|
||||
pub use validation::{
|
||||
decode_holocene_base_fee, next_block_base_fee, validate_block_post_execution,
|
||||
canyon, decode_holocene_base_fee, isthmus, next_block_base_fee, shanghai,
|
||||
validate_block_post_execution,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
pub use error::OpConsensusError;
|
||||
|
||||
/// Optimism consensus implementation.
|
||||
///
|
||||
/// Provides basic checks as outlined in the execution specs.
|
||||
@ -98,13 +101,30 @@ impl<ChainSpec: EthChainSpec + OpHardforks, B: Block> Consensus<B>
|
||||
return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
|
||||
}
|
||||
|
||||
// EIP-4895: Beacon chain push withdrawals as operations
|
||||
// Check empty shanghai-withdrawals
|
||||
if self.chain_spec.is_shanghai_active_at_timestamp(block.timestamp()) {
|
||||
validate_shanghai_withdrawals(block)?;
|
||||
shanghai::ensure_empty_shanghai_withdrawals(block.body()).map_err(|err| {
|
||||
ConsensusError::Other(format!("failed to verify block {}: {err}", block.number()))
|
||||
})?
|
||||
} else {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
if self.chain_spec.is_cancun_active_at_timestamp(block.timestamp()) {
|
||||
validate_cancun_gas(block)?;
|
||||
} else {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// Check withdrawals root field in header
|
||||
if self.chain_spec.is_isthmus_active_at_timestamp(block.timestamp()) {
|
||||
// storage root of withdrawals pre-deploy is verified post-execution
|
||||
isthmus::ensure_withdrawals_storage_root_is_some(block.header()).map_err(|err| {
|
||||
ConsensusError::Other(format!("failed to verify block {}: {err}", block.number()))
|
||||
})?
|
||||
} else {
|
||||
// canyon is active, else would have returned already
|
||||
canyon::ensure_empty_withdrawals_root(block.header())?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
24
crates/optimism/consensus/src/validation/canyon.rs
Normal file
24
crates/optimism/consensus/src/validation/canyon.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//! Canyon consensus rule checks.
|
||||
|
||||
use alloy_consensus::BlockHeader;
|
||||
use alloy_trie::EMPTY_ROOT_HASH;
|
||||
use reth_consensus::ConsensusError;
|
||||
use reth_primitives::GotExpected;
|
||||
|
||||
/// Verifies that withdrawals root in block header (Shanghai) is always [`EMPTY_ROOT_HASH`] in
|
||||
/// Canyon.
|
||||
#[inline]
|
||||
pub fn ensure_empty_withdrawals_root<H: BlockHeader>(header: &H) -> Result<(), ConsensusError> {
|
||||
// Shanghai rule
|
||||
let header_withdrawals_root =
|
||||
&header.withdrawals_root().ok_or(ConsensusError::WithdrawalsRootMissing)?;
|
||||
|
||||
// Canyon rules
|
||||
if *header_withdrawals_root != EMPTY_ROOT_HASH {
|
||||
return Err(ConsensusError::BodyWithdrawalsRootDiff(
|
||||
GotExpected { got: *header_withdrawals_root, expected: EMPTY_ROOT_HASH }.into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
127
crates/optimism/consensus/src/validation/isthmus.rs
Normal file
127
crates/optimism/consensus/src/validation/isthmus.rs
Normal file
@ -0,0 +1,127 @@
|
||||
//! Block verification w.r.t. consensus rules new in Isthmus hardfork.
|
||||
|
||||
use crate::OpConsensusError;
|
||||
use alloy_consensus::BlockHeader;
|
||||
use core::fmt;
|
||||
use reth_optimism_primitives::predeploys::ADDRESS_L2_TO_L1_MESSAGE_PASSER;
|
||||
use reth_storage_api::StorageRootProvider;
|
||||
use reth_trie_common::HashedStorage;
|
||||
use revm::db::BundleAccount;
|
||||
|
||||
/// Verifies that `withdrawals_root` (i.e. `l2tol1-msg-passer` storage root since Isthmus) field is
|
||||
/// set in block header.
|
||||
pub fn ensure_withdrawals_storage_root_is_some<H: BlockHeader>(
|
||||
header: H,
|
||||
) -> Result<(), OpConsensusError> {
|
||||
header.withdrawals_root().ok_or(OpConsensusError::L2WithdrawalsRootMissing)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies block header field `withdrawals_root` against storage root of
|
||||
/// `L2ToL1MessagePasser.sol` predeploy post block execution.
|
||||
///
|
||||
/// See <https://specs.optimism.io/protocol/isthmus/exec-engine.html#l2tol1messagepasser-storage-root-in-header>.
|
||||
pub fn verify_withdrawals_storage_root<DB, H>(
|
||||
predeploy_account_updates: Option<&BundleAccount>,
|
||||
state: DB,
|
||||
header: H,
|
||||
) -> Result<(), OpConsensusError>
|
||||
where
|
||||
DB: StorageRootProvider,
|
||||
H: BlockHeader + fmt::Debug,
|
||||
{
|
||||
let header_storage_root =
|
||||
header.withdrawals_root().ok_or(OpConsensusError::L2WithdrawalsRootMissing)?;
|
||||
|
||||
// if block contained l2 withdrawals transactions, use predeploy storage updates from
|
||||
// execution
|
||||
let hashed_storage_updates = predeploy_account_updates.map(|acc| {
|
||||
HashedStorage::from_plain_storage(
|
||||
acc.status,
|
||||
acc.storage.iter().map(|(slot, value)| (slot, &value.present_value)),
|
||||
)
|
||||
});
|
||||
|
||||
let storage_root = state
|
||||
.storage_root(ADDRESS_L2_TO_L1_MESSAGE_PASSER, hashed_storage_updates.unwrap_or_default())
|
||||
.map_err(OpConsensusError::L2WithdrawalsRootCalculationFail)?;
|
||||
|
||||
if header_storage_root != storage_root {
|
||||
return Err(OpConsensusError::L2WithdrawalsRootMismatch {
|
||||
header: header_storage_root,
|
||||
exec_res: storage_root,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use alloc::sync::Arc;
|
||||
use alloy_chains::Chain;
|
||||
use alloy_consensus::Header;
|
||||
use alloy_primitives::{keccak256, B256, U256};
|
||||
use core::str::FromStr;
|
||||
use reth_db_common::init::init_genesis;
|
||||
use reth_optimism_chainspec::OpChainSpecBuilder;
|
||||
use reth_optimism_node::OpNode;
|
||||
use reth_provider::{
|
||||
providers::BlockchainProvider, test_utils::create_test_provider_factory_with_node_types,
|
||||
StateWriter,
|
||||
};
|
||||
use reth_storage_api::StateProviderFactory;
|
||||
use reth_trie::test_utils::storage_root_prehashed;
|
||||
use reth_trie_common::HashedPostState;
|
||||
|
||||
#[test]
|
||||
fn l2tol1_message_passer_no_withdrawals() {
|
||||
let hashed_address = keccak256(ADDRESS_L2_TO_L1_MESSAGE_PASSER);
|
||||
|
||||
// create account storage
|
||||
let init_storage = HashedStorage::from_iter(
|
||||
false,
|
||||
[
|
||||
"50000000000000000000000000000004253371b55351a08cb3267d4d265530b6",
|
||||
"512428ed685fff57294d1a9cbb147b18ae5db9cf6ae4b312fa1946ba0561882e",
|
||||
"51e6784c736ef8548f856909870b38e49ef7a4e3e77e5e945e0d5e6fcaa3037f",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|str| (B256::from_str(str).unwrap(), U256::from(1))),
|
||||
);
|
||||
let mut state = HashedPostState::default();
|
||||
state.storages.insert(hashed_address, init_storage.clone());
|
||||
|
||||
// init test db
|
||||
// note: must be empty (default) chain spec to ensure storage is empty after init genesis,
|
||||
// otherwise can't use `storage_root_prehashed` to determine storage root later
|
||||
let provider_factory = create_test_provider_factory_with_node_types::<OpNode>(Arc::new(
|
||||
OpChainSpecBuilder::default().chain(Chain::dev()).genesis(Default::default()).build(),
|
||||
));
|
||||
let _ = init_genesis(&provider_factory).unwrap();
|
||||
|
||||
// write account storage to database
|
||||
let provider_rw = provider_factory.provider_rw().unwrap();
|
||||
provider_rw.write_hashed_state(&state.clone().into_sorted()).unwrap();
|
||||
provider_rw.commit().unwrap();
|
||||
|
||||
// create block header with withdrawals root set to storage root of l2tol1-msg-passer
|
||||
let header = Header {
|
||||
withdrawals_root: Some(storage_root_prehashed(init_storage.storage)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// create state provider factory
|
||||
let state_provider_factory = BlockchainProvider::new(provider_factory).unwrap();
|
||||
|
||||
// validate block against existing state by passing empty state updates
|
||||
verify_withdrawals_storage_root(
|
||||
None,
|
||||
state_provider_factory.latest().expect("load state"),
|
||||
&header,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,9 @@
|
||||
//! Verification of blocks w.r.t. Optimism hardforks.
|
||||
|
||||
pub mod canyon;
|
||||
pub mod isthmus;
|
||||
pub mod shanghai;
|
||||
|
||||
use crate::proof::calculate_receipt_root_optimism;
|
||||
use alloc::vec::Vec;
|
||||
use alloy_consensus::{BlockHeader, TxReceipt};
|
||||
20
crates/optimism/consensus/src/validation/shanghai.rs
Normal file
20
crates/optimism/consensus/src/validation/shanghai.rs
Normal file
@ -0,0 +1,20 @@
|
||||
//! L2 Shanghai consensus rule checks.
|
||||
|
||||
use crate::OpConsensusError;
|
||||
use reth_consensus::ConsensusError;
|
||||
use reth_primitives_traits::BlockBody;
|
||||
|
||||
/// Verifies that withdrawals in block body (Shanghai) is always empty in Canyon.
|
||||
/// <https://specs.optimism.io/protocol/rollup-node-p2p.html#block-validation>
|
||||
#[inline]
|
||||
pub fn ensure_empty_shanghai_withdrawals<T: BlockBody>(body: &T) -> Result<(), OpConsensusError> {
|
||||
// Shanghai rule
|
||||
let withdrawals = body.withdrawals().ok_or(ConsensusError::BodyWithdrawalsMissing)?;
|
||||
|
||||
// Canyon rule
|
||||
if !withdrawals.as_ref().is_empty() {
|
||||
return Err(OpConsensusError::WithdrawalsNonEmpty)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -27,10 +27,13 @@ pub mod primitives {
|
||||
pub mod consensus {
|
||||
#[doc(inline)]
|
||||
pub use reth_consensus::*;
|
||||
#[doc(inline)]
|
||||
pub use reth_consensus_common::*;
|
||||
#[doc(inline)]
|
||||
pub use reth_optimism_consensus::*;
|
||||
/// Consensus rule checks.
|
||||
pub mod validation {
|
||||
#[doc(inline)]
|
||||
pub use reth_consensus_common::validation::*;
|
||||
#[doc(inline)]
|
||||
pub use reth_optimism_consensus::validation::*;
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-exported from `reth_chainspec`
|
||||
|
||||
Reference in New Issue
Block a user