feat: abstract OpBeaconConsensus over primitives and chainspec (#14171)

This commit is contained in:
Arsenii Kulikov
2025-02-04 16:55:51 +04:00
committed by GitHub
parent 740bf04351
commit 07090b315c
9 changed files with 237 additions and 221 deletions

View File

@ -30,12 +30,12 @@ alloy-eips.workspace = true
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-trie.workspace = true
op-alloy-consensus.workspace = true
tracing.workspace = true
[dev-dependencies]
alloy-primitives.workspace = true
op-alloy-consensus.workspace = true
reth-optimism-chainspec.workspace = true
[features]

View File

@ -12,6 +12,8 @@
extern crate alloc;
use core::fmt::Debug;
use alloc::sync::Arc;
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
use alloy_primitives::{B64, U256};
@ -25,45 +27,50 @@ use reth_consensus_common::validation::{
validate_body_against_header, validate_cancun_gas, validate_header_base_fee,
validate_header_extra_data, validate_header_gas, validate_shanghai_withdrawals,
};
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_forks::OpHardforks;
use reth_optimism_primitives::{OpBlock, OpPrimitives, OpReceipt};
use reth_primitives::{GotExpected, RecoveredBlock, SealedHeader};
use reth_optimism_primitives::DepositReceipt;
use reth_primitives::{GotExpected, NodePrimitives, RecoveredBlock, SealedHeader};
mod proof;
pub use proof::calculate_receipt_root_no_memo_optimism;
use reth_primitives_traits::{Block, BlockBody, BlockHeader, SealedBlock};
mod validation;
pub use validation::validate_block_post_execution;
pub use validation::{
decode_holocene_base_fee, next_block_base_fee, validate_block_post_execution,
};
/// Optimism consensus implementation.
///
/// Provides basic checks as outlined in the execution specs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpBeaconConsensus {
pub struct OpBeaconConsensus<ChainSpec> {
/// Configuration
chain_spec: Arc<OpChainSpec>,
chain_spec: Arc<ChainSpec>,
}
impl OpBeaconConsensus {
impl<ChainSpec> OpBeaconConsensus<ChainSpec> {
/// Create a new instance of [`OpBeaconConsensus`]
pub const fn new(chain_spec: Arc<OpChainSpec>) -> Self {
pub const fn new(chain_spec: Arc<ChainSpec>) -> Self {
Self { chain_spec }
}
}
impl FullConsensus<OpPrimitives> for OpBeaconConsensus {
impl<ChainSpec: EthChainSpec + OpHardforks, N: NodePrimitives<Receipt: DepositReceipt>>
FullConsensus<N> for OpBeaconConsensus<ChainSpec>
{
fn validate_block_post_execution(
&self,
block: &RecoveredBlock<OpBlock>,
input: PostExecutionInput<'_, OpReceipt>,
block: &RecoveredBlock<N::Block>,
input: PostExecutionInput<'_, N::Receipt>,
) -> Result<(), ConsensusError> {
validate_block_post_execution(block.header(), &self.chain_spec, input.receipts)
}
}
impl<B: Block> Consensus<B> for OpBeaconConsensus {
impl<ChainSpec: EthChainSpec + OpHardforks, B: Block> Consensus<B>
for OpBeaconConsensus<ChainSpec>
{
type Error = ConsensusError;
fn validate_body_against_header(
@ -105,7 +112,9 @@ impl<B: Block> Consensus<B> for OpBeaconConsensus {
}
}
impl<H: BlockHeader> HeaderValidator<H> for OpBeaconConsensus {
impl<ChainSpec: EthChainSpec + OpHardforks, H: BlockHeader> HeaderValidator<H>
for OpBeaconConsensus<ChainSpec>
{
fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
validate_header_gas(header.header())?;
validate_header_base_fee(header.header(), &self.chain_spec)
@ -129,10 +138,9 @@ impl<H: BlockHeader> HeaderValidator<H> for OpBeaconConsensus {
if self.chain_spec.is_holocene_active_at_timestamp(parent.timestamp()) {
let header_base_fee =
header.base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)?;
let expected_base_fee = self
.chain_spec
.decode_holocene_base_fee(parent.header(), header.timestamp())
.map_err(|_| ConsensusError::BaseFeeMissing)?;
let expected_base_fee =
decode_holocene_base_fee(&self.chain_spec, parent.header(), header.timestamp())
.map_err(|_| ConsensusError::BaseFeeMissing)?;
if expected_base_fee != header_base_fee {
return Err(ConsensusError::BaseFeeDiff(GotExpected {
expected: expected_base_fee,

View File

@ -4,15 +4,14 @@ use alloc::vec::Vec;
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::B256;
use alloy_trie::root::ordered_trie_root_with_encoder;
use reth_chainspec::ChainSpec;
use reth_optimism_forks::{OpHardfork, OpHardforks};
use reth_optimism_forks::OpHardforks;
use reth_optimism_primitives::DepositReceipt;
use reth_primitives::ReceiptWithBloom;
/// Calculates the receipt root for a header.
pub(crate) fn calculate_receipt_root_optimism<R: DepositReceipt>(
receipts: &[ReceiptWithBloom<R>],
chain_spec: &ChainSpec,
chain_spec: impl OpHardforks,
timestamp: u64,
) -> B256 {
// There is a minor bug in op-geth and op-erigon where in the Regolith hardfork,
@ -20,8 +19,8 @@ pub(crate) fn calculate_receipt_root_optimism<R: DepositReceipt>(
// encoding. In the Regolith Hardfork, we must strip the deposit nonce from the
// receipts before calculating the receipt root. This was corrected in the Canyon
// hardfork.
if chain_spec.is_fork_active_at_timestamp(OpHardfork::Regolith, timestamp) &&
!chain_spec.is_fork_active_at_timestamp(OpHardfork::Canyon, timestamp)
if chain_spec.is_regolith_active_at_timestamp(timestamp) &&
!chain_spec.is_canyon_active_at_timestamp(timestamp)
{
let receipts = receipts
.iter()

View File

@ -2,8 +2,10 @@ use crate::proof::calculate_receipt_root_optimism;
use alloc::vec::Vec;
use alloy_consensus::{BlockHeader, TxReceipt};
use alloy_primitives::{Bloom, B256};
use reth_chainspec::{ChainSpec, EthereumHardforks};
use op_alloy_consensus::{decode_holocene_extra_data, EIP1559ParamError};
use reth_chainspec::{BaseFeeParams, EthChainSpec};
use reth_consensus::ConsensusError;
use reth_optimism_forks::OpHardforks;
use reth_optimism_primitives::DepositReceipt;
use reth_primitives::{gas_spent_by_transactions, GotExpected};
@ -13,7 +15,7 @@ use reth_primitives::{gas_spent_by_transactions, GotExpected};
/// - Compares the gas used in the block header to the actual gas usage after execution
pub fn validate_block_post_execution<R: DepositReceipt>(
header: impl BlockHeader,
chain_spec: &ChainSpec,
chain_spec: impl OpHardforks,
receipts: &[R],
) -> Result<(), ConsensusError> {
// Before Byzantium, receipts contained state root that would mean that expensive
@ -21,7 +23,7 @@ pub fn validate_block_post_execution<R: DepositReceipt>(
// transaction This was replaced with is_success flag.
// See more about EIP here: https://eips.ethereum.org/EIPS/eip-658
if chain_spec.is_byzantium_active_at_block(header.number()) {
if let Err(error) = verify_receipts(
if let Err(error) = verify_receipts_optimism(
header.receipts_root(),
header.logs_bloom(),
receipts,
@ -47,11 +49,11 @@ pub fn validate_block_post_execution<R: DepositReceipt>(
}
/// Verify the calculated receipts root against the expected receipts root.
fn verify_receipts<R: DepositReceipt>(
fn verify_receipts_optimism<R: DepositReceipt>(
expected_receipts_root: B256,
expected_logs_bloom: Bloom,
receipts: &[R],
chain_spec: &ChainSpec,
chain_spec: impl OpHardforks,
timestamp: u64,
) -> Result<(), ConsensusError> {
// Calculate receipts root.
@ -94,3 +96,145 @@ fn compare_receipts_root_and_logs_bloom(
Ok(())
}
/// Extracts the Holocene 1599 parameters from the encoded extra data from the parent header.
///
/// Caution: Caller must ensure that holocene is active in the parent header.
///
/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#base-fee-computation)
pub fn decode_holocene_base_fee(
chain_spec: impl EthChainSpec + OpHardforks,
parent: impl BlockHeader,
timestamp: u64,
) -> Result<u64, EIP1559ParamError> {
let (elasticity, denominator) = decode_holocene_extra_data(parent.extra_data())?;
let base_fee_params = if elasticity == 0 && denominator == 0 {
chain_spec.base_fee_params_at_timestamp(timestamp)
} else {
BaseFeeParams::new(denominator as u128, elasticity as u128)
};
Ok(parent.next_block_base_fee(base_fee_params).unwrap_or_default())
}
/// Read from parent to determine the base fee for the next block
///
/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#base-fee-computation)
pub fn next_block_base_fee(
chain_spec: impl EthChainSpec + OpHardforks,
parent: impl BlockHeader,
timestamp: u64,
) -> Result<u64, EIP1559ParamError> {
// If we are in the Holocene, we need to use the base fee params
// from the parent block's extra data.
// Else, use the base fee params (default values) from chainspec
if chain_spec.is_holocene_active_at_timestamp(parent.timestamp()) {
Ok(decode_holocene_base_fee(chain_spec, parent, timestamp)?)
} else {
Ok(parent
.next_block_base_fee(chain_spec.base_fee_params_at_timestamp(timestamp))
.unwrap_or_default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::Header;
use alloy_primitives::{hex, Bytes, U256};
use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
use reth_optimism_forks::OpHardfork;
use std::sync::Arc;
fn holocene_chainspec() -> Arc<OpChainSpec> {
let mut hardforks = OpHardfork::base_sepolia();
hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000));
Arc::new(OpChainSpec {
inner: ChainSpec {
chain: BASE_SEPOLIA.inner.chain,
genesis: BASE_SEPOLIA.inner.genesis.clone(),
genesis_hash: BASE_SEPOLIA.inner.genesis_hash.clone(),
paris_block_and_final_difficulty: Some((0, U256::from(0))),
hardforks,
base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(),
prune_delete_limit: 10000,
..Default::default()
},
})
}
#[test]
fn test_get_base_fee_pre_holocene() {
let op_chain_spec = BASE_SEPOLIA.clone();
let parent = Header {
base_fee_per_gas: Some(1),
gas_used: 15763614,
gas_limit: 144000000,
..Default::default()
};
let base_fee = next_block_base_fee(&op_chain_spec, &parent, 0);
assert_eq!(
base_fee.unwrap(),
parent
.next_block_base_fee(op_chain_spec.base_fee_params_at_timestamp(0))
.unwrap_or_default()
);
}
#[test]
fn test_get_base_fee_holocene_extra_data_not_set() {
let op_chain_spec = holocene_chainspec();
let parent = Header {
base_fee_per_gas: Some(1),
gas_used: 15763614,
gas_limit: 144000000,
timestamp: 1800000003,
extra_data: Bytes::from_static(&[0, 0, 0, 0, 0, 0, 0, 0, 0]),
..Default::default()
};
let base_fee = next_block_base_fee(&op_chain_spec, &parent, 1800000005);
assert_eq!(
base_fee.unwrap(),
parent
.next_block_base_fee(op_chain_spec.base_fee_params_at_timestamp(0))
.unwrap_or_default()
);
}
#[test]
fn test_get_base_fee_holocene_extra_data_set() {
let parent = Header {
base_fee_per_gas: Some(1),
gas_used: 15763614,
gas_limit: 144000000,
extra_data: Bytes::from_static(&[0, 0, 0, 0, 8, 0, 0, 0, 8]),
timestamp: 1800000003,
..Default::default()
};
let base_fee = next_block_base_fee(holocene_chainspec(), &parent, 1800000005);
assert_eq!(
base_fee.unwrap(),
parent
.next_block_base_fee(BaseFeeParams::new(0x00000008, 0x00000008))
.unwrap_or_default()
);
}
// <https://sepolia.basescan.org/block/19773628>
#[test]
fn test_get_base_fee_holocene_extra_data_set_base_sepolia() {
let parent = Header {
base_fee_per_gas: Some(507),
gas_used: 4847634,
gas_limit: 60000000,
extra_data: hex!("00000000fa0000000a").into(),
timestamp: 1735315544,
..Default::default()
};
let base_fee = next_block_base_fee(&*BASE_SEPOLIA, &parent, 1735315546).unwrap();
assert_eq!(base_fee, 507);
}
}