From fabf3e84d401df6839d076762fbb07fbc96a26d1 Mon Sep 17 00:00:00 2001 From: theo <80177219+theochap@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:47:01 +0200 Subject: [PATCH] feat(op/jovian): implement min base fee in op-reth. bump alloy, alloy-evm deps. (#18407) Co-authored-by: Emilia Hane Co-authored-by: Arsenii Kulikov --- Cargo.lock | 28 +-- Cargo.toml | 14 +- crates/engine/local/src/payload.rs | 1 + crates/engine/tree/src/tree/metrics.rs | 32 ++- crates/ethereum/evm/src/test_utils.rs | 33 +++- crates/optimism/chainspec/src/basefee.rs | 53 ++++- crates/optimism/chainspec/src/lib.rs | 4 +- .../optimism/consensus/src/validation/mod.rs | 185 ++++++++++++++++++ crates/optimism/node/src/engine.rs | 115 +++++++++-- crates/optimism/node/src/utils.rs | 1 + .../node/tests/e2e-testsuite/testsuite.rs | 1 + crates/optimism/payload/src/payload.rs | 76 ++++++- .../custom-beacon-withdrawals/src/main.rs | 20 +- examples/custom-node/src/evm/executor.rs | 29 ++- 14 files changed, 516 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9438e4757c..604b2b84a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "alloy-evm" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbe7c66c859b658d879b22e8aaa19546dab726b0639f4649a424ada3d99349e" +checksum = "a2b2845e4c4844e53dd08bf24d3af7b163ca7d1e3c68eb587e38c4e976659089" dependencies = [ "alloy-consensus", "alloy-eips", @@ -375,9 +375,9 @@ dependencies = [ [[package]] name = "alloy-op-evm" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9b726869a13d5d958f2f78fbef7ce522689c4d40d613c16239f5e286fbeb1a" +checksum = "57d69ffa57dbcabea651fbe2fd721e0deb8dc6f1a334d5853817cc7addd006fb" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6057,9 +6057,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "op-alloy-consensus" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ade20c592484ba1ea538006e0454284174447a3adf9bb59fa99ed512f95493" +checksum = "3a501241474c3118833d6195312ae7eb7cc90bbb0d5f524cbb0b06619e49ff67" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6083,9 +6083,9 @@ checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" [[package]] name = "op-alloy-network" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84741a798124ceb43979d70db654039937a00979b1341fa8bfdc48473bbd52bf" +checksum = "f80108e3b36901200a4c5df1db1ee9ef6ce685b59ea79d7be1713c845e3765da" dependencies = [ "alloy-consensus", "alloy-network", @@ -6099,9 +6099,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-jsonrpsee" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa85f170bf8f914a7619e1447918781a8c5bd1041bb6629940b851e865487156" +checksum = "e8eb878fc5ea95adb5abe55fb97475b3eb0dcc77dfcd6f61bd626a68ae0bdba1" dependencies = [ "alloy-primitives", "jsonrpsee", @@ -6109,9 +6109,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9076d4fcb8e260cec8ad01cd155200c0dbb562e62adb553af245914f30854e29" +checksum = "753d6f6b03beca1ba9cbd344c05fee075a2ce715ee9d61981c10b9c764a824a2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -6129,9 +6129,9 @@ dependencies = [ [[package]] name = "op-alloy-rpc-types-engine" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4256b1eda5766a9fa7de5874e54515994500bef632afda41e940aed015f9455" +checksum = "14e50c94013a1d036a529df259151991dbbd6cf8dc215e3b68b784f95eec60e6" dependencies = [ "alloy-consensus", "alloy-eips", diff --git a/Cargo.toml b/Cargo.toml index a4e39d05e9..369506adec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -477,7 +477,7 @@ revm-inspectors = "0.29.0" alloy-chains = { version = "0.2.5", default-features = false } alloy-dyn-abi = "1.3.1" alloy-eip2124 = { version = "0.2.0", default-features = false } -alloy-evm = { version = "0.20.1", default-features = false } +alloy-evm = { version = "0.21.0", default-features = false } alloy-primitives = { version = "1.3.1", default-features = false, features = ["map-foldhash"] } alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] } alloy-sol-macro = "1.3.1" @@ -515,13 +515,13 @@ alloy-transport-ipc = { version = "1.0.30", default-features = false } alloy-transport-ws = { version = "1.0.30", default-features = false } # op -alloy-op-evm = { version = "0.20.1", default-features = false } +alloy-op-evm = { version = "0.21.0", default-features = false } alloy-op-hardforks = "0.3.1" -op-alloy-rpc-types = { version = "0.19.0", default-features = false } -op-alloy-rpc-types-engine = { version = "0.19.0", default-features = false } -op-alloy-network = { version = "0.19.0", default-features = false } -op-alloy-consensus = { version = "0.19.0", default-features = false } -op-alloy-rpc-jsonrpsee = { version = "0.19.0", default-features = false } +op-alloy-rpc-types = { version = "0.20.0", default-features = false } +op-alloy-rpc-types-engine = { version = "0.20.0", default-features = false } +op-alloy-network = { version = "0.20.0", default-features = false } +op-alloy-consensus = { version = "0.20.0", default-features = false } +op-alloy-rpc-jsonrpsee = { version = "0.20.0", default-features = false } op-alloy-flz = { version = "0.13.1", default-features = false } # misc diff --git a/crates/engine/local/src/payload.rs b/crates/engine/local/src/payload.rs index 408ea2b8d0..34deaf3e10 100644 --- a/crates/engine/local/src/payload.rs +++ b/crates/engine/local/src/payload.rs @@ -61,6 +61,7 @@ where no_tx_pool: None, gas_limit: None, eip_1559_params: None, + min_base_fee: None, } } } diff --git a/crates/engine/tree/src/tree/metrics.rs b/crates/engine/tree/src/tree/metrics.rs index 3a64a259b8..b0f4c360a4 100644 --- a/crates/engine/tree/src/tree/metrics.rs +++ b/crates/engine/tree/src/tree/metrics.rs @@ -210,7 +210,7 @@ pub(crate) struct BlockBufferMetrics { mod tests { use super::*; use alloy_eips::eip7685::Requests; - use alloy_evm::block::{CommitChanges, StateChangeSource}; + use alloy_evm::block::StateChangeSource; use alloy_primitives::{B256, U256}; use metrics_util::debugging::{DebuggingRecorder, Snapshotter}; use reth_ethereum_primitives::{Receipt, TransactionSigned}; @@ -218,13 +218,14 @@ mod tests { use reth_execution_types::BlockExecutionResult; use reth_primitives_traits::RecoveredBlock; use revm::{ - context::result::ExecutionResult, + context::result::{ExecutionResult, Output, ResultAndState, SuccessReason}, database::State, database_interface::EmptyDB, inspector::NoOpInspector, state::{Account, AccountInfo, AccountStatus, EvmState, EvmStorage, EvmStorageSlot}, Context, MainBuilder, MainContext, }; + use revm_primitives::Bytes; use std::sync::mpsc; /// A simple mock executor for testing that doesn't require complex EVM setup @@ -251,16 +252,33 @@ mod tests { Ok(()) } - fn execute_transaction_with_commit_condition( + fn execute_transaction_without_commit( &mut self, - _tx: impl alloy_evm::block::ExecutableTx, - _f: impl FnOnce(&ExecutionResult<::HaltReason>) -> CommitChanges, - ) -> Result, BlockExecutionError> { + _tx: impl ExecutableTx, + ) -> Result::HaltReason>, BlockExecutionError> { // Call hook with our mock state for each transaction if let Some(hook) = self.hook.as_mut() { hook.on_state(StateChangeSource::Transaction(0), &self.state); } - Ok(Some(1000)) // Mock gas used + + Ok(ResultAndState::new( + ExecutionResult::Success { + reason: SuccessReason::Return, + gas_used: 1000, // Mock gas used + gas_refunded: 0, + logs: vec![], + output: Output::Call(Bytes::from(vec![])), + }, + Default::default(), + )) + } + + fn commit_transaction( + &mut self, + _output: ResultAndState<::HaltReason>, + _tx: impl ExecutableTx, + ) -> Result { + Ok(1000) } fn finish( diff --git a/crates/ethereum/evm/src/test_utils.rs b/crates/ethereum/evm/src/test_utils.rs index a4b3090aa8..92290e0a8c 100644 --- a/crates/ethereum/evm/src/test_utils.rs +++ b/crates/ethereum/evm/src/test_utils.rs @@ -1,14 +1,15 @@ use crate::EthEvmConfig; -use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, sync::Arc, vec, vec::Vec}; use alloy_consensus::Header; use alloy_eips::eip7685::Requests; use alloy_evm::precompiles::PrecompilesMap; +use alloy_primitives::Bytes; use alloy_rpc_types_engine::ExecutionData; use parking_lot::Mutex; use reth_ethereum_primitives::{Receipt, TransactionSigned}; use reth_evm::{ block::{ - BlockExecutionError, BlockExecutor, BlockExecutorFactory, BlockExecutorFor, CommitChanges, + BlockExecutionError, BlockExecutor, BlockExecutorFactory, BlockExecutorFor, ExecutableTx, }, eth::{EthBlockExecutionCtx, EthEvmContext}, ConfigureEngineEvm, ConfigureEvm, Database, EthEvm, EthEvmFactory, Evm, EvmEnvFor, EvmFactory, @@ -17,7 +18,7 @@ use reth_evm::{ use reth_execution_types::{BlockExecutionResult, ExecutionOutcome}; use reth_primitives_traits::{BlockTy, SealedBlock, SealedHeader}; use revm::{ - context::result::{ExecutionResult, HaltReason}, + context::result::{ExecutionResult, Output, ResultAndState, SuccessReason}, database::State, Inspector, }; @@ -88,12 +89,28 @@ impl<'a, DB: Database, I: Inspector>>> BlockExec Ok(()) } - fn execute_transaction_with_commit_condition( + fn execute_transaction_without_commit( &mut self, - _tx: impl alloy_evm::block::ExecutableTx, - _f: impl FnOnce(&ExecutionResult) -> CommitChanges, - ) -> Result, BlockExecutionError> { - Ok(Some(0)) + _tx: impl ExecutableTx, + ) -> Result::HaltReason>, BlockExecutionError> { + Ok(ResultAndState::new( + ExecutionResult::Success { + reason: SuccessReason::Return, + gas_used: 0, + gas_refunded: 0, + logs: vec![], + output: Output::Call(Bytes::from(vec![])), + }, + Default::default(), + )) + } + + fn commit_transaction( + &mut self, + _output: ResultAndState<::HaltReason>, + _tx: impl ExecutableTx, + ) -> Result { + Ok(0) } fn finish( diff --git a/crates/optimism/chainspec/src/basefee.rs b/crates/optimism/chainspec/src/basefee.rs index b28c0c478d..0ef712dc04 100644 --- a/crates/optimism/chainspec/src/basefee.rs +++ b/crates/optimism/chainspec/src/basefee.rs @@ -1,10 +1,26 @@ //! Base fee related utilities for Optimism chains. use alloy_consensus::BlockHeader; -use op_alloy_consensus::{decode_holocene_extra_data, EIP1559ParamError}; +use op_alloy_consensus::{decode_holocene_extra_data, decode_jovian_extra_data, EIP1559ParamError}; use reth_chainspec::{BaseFeeParams, EthChainSpec}; use reth_optimism_forks::OpHardforks; +fn next_base_fee_params( + chain_spec: impl EthChainSpec + OpHardforks, + parent: &H, + timestamp: u64, + denominator: u32, + elasticity: u32, +) -> u64 { + 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) + }; + + parent.next_block_base_fee(base_fee_params).unwrap_or_default() +} + /// 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. @@ -19,11 +35,34 @@ where H: BlockHeader, { 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()) + Ok(next_base_fee_params(chain_spec, parent, timestamp, denominator, elasticity)) +} + +/// Extracts the Jovian 1599 parameters from the encoded extra data from the parent header. +/// Additionally to [`decode_holocene_base_fee`], checks if the next block base fee is less than the +/// minimum base fee, then the minimum base fee is returned. +/// +/// Caution: Caller must ensure that jovian is active in the parent header. +/// +/// See also [Base fee computation](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/exec-engine.md#base-fee-computation) +/// and [Minimum base fee in block header](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/jovian/exec-engine.md#minimum-base-fee-in-block-header) +pub fn compute_jovian_base_fee( + chain_spec: impl EthChainSpec + OpHardforks, + parent: &H, + timestamp: u64, +) -> Result +where + H: BlockHeader, +{ + let (elasticity, denominator, min_base_fee) = decode_jovian_extra_data(parent.extra_data())?; + + let next_base_fee = + next_base_fee_params(chain_spec, parent, timestamp, denominator, elasticity); + + if next_base_fee < min_base_fee { + return Ok(min_base_fee); + } + + Ok(next_base_fee) } diff --git a/crates/optimism/chainspec/src/lib.rs b/crates/optimism/chainspec/src/lib.rs index 720c8b960e..c04d1df4b8 100644 --- a/crates/optimism/chainspec/src/lib.rs +++ b/crates/optimism/chainspec/src/lib.rs @@ -293,7 +293,9 @@ impl EthChainSpec for OpChainSpec { } fn next_block_base_fee(&self, parent: &Header, target_timestamp: u64) -> Option { - if self.is_holocene_active_at_timestamp(parent.timestamp()) { + if self.is_jovian_active_at_timestamp(parent.timestamp()) { + compute_jovian_base_fee(self, parent, target_timestamp).ok() + } else if self.is_holocene_active_at_timestamp(parent.timestamp()) { decode_holocene_base_fee(self, parent, target_timestamp).ok() } else { self.inner.next_block_base_fee(parent, target_timestamp) diff --git a/crates/optimism/consensus/src/validation/mod.rs b/crates/optimism/consensus/src/validation/mod.rs index 0846572a3d..416179d765 100644 --- a/crates/optimism/consensus/src/validation/mod.rs +++ b/crates/optimism/consensus/src/validation/mod.rs @@ -183,6 +183,9 @@ mod tests { use reth_optimism_forks::{OpHardfork, BASE_SEPOLIA_HARDFORKS}; use std::sync::Arc; + const JOVIAN_TIMESTAMP: u64 = 1900000000; + const BLOCK_TIME_SECONDS: u64 = 2; + fn holocene_chainspec() -> Arc { let mut hardforks = BASE_SEPOLIA_HARDFORKS.clone(); hardforks.insert(OpHardfork::Holocene.boxed(), ForkCondition::Timestamp(1800000000)); @@ -209,6 +212,15 @@ mod tests { chainspec } + fn jovian_chainspec() -> OpChainSpec { + let mut chainspec = BASE_SEPOLIA.as_ref().clone(); + chainspec + .inner + .hardforks + .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(1900000000)); + chainspec + } + #[test] fn test_get_base_fee_pre_holocene() { let op_chain_spec = BASE_SEPOLIA.clone(); @@ -293,6 +305,179 @@ mod tests { assert_eq!(base_fee, 507); } + #[test] + fn test_get_base_fee_holocene_extra_data_set_and_min_base_fee_set() { + const MIN_BASE_FEE: u64 = 10; + + let mut extra_data = Vec::new(); + // eip1559 params + extra_data.append(&mut hex!("00000000fa0000000a").to_vec()); + // min base fee + extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec()); + let extra_data = Bytes::from(extra_data); + + let parent = Header { + base_fee_per_gas: Some(507), + gas_used: 4847634, + gas_limit: 60000000, + extra_data, + timestamp: 1735315544, + ..Default::default() + }; + + let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee( + &*BASE_SEPOLIA, + &parent, + 1735315546, + ); + assert_eq!(base_fee, None); + } + + /// The version byte for Jovian is 1. + const JOVIAN_EXTRA_DATA_VERSION_BYTE: u8 = 1; + + #[test] + fn test_get_base_fee_jovian_extra_data_and_min_base_fee_not_set() { + let op_chain_spec = jovian_chainspec(); + + let mut extra_data = Vec::new(); + extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE); + // eip1559 params + extra_data.append(&mut [0_u8; 8].to_vec()); + let extra_data = Bytes::from(extra_data); + + let parent = Header { + base_fee_per_gas: Some(1), + gas_used: 15763614, + gas_limit: 144000000, + timestamp: JOVIAN_TIMESTAMP, + extra_data, + ..Default::default() + }; + let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee( + &op_chain_spec, + &parent, + JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS, + ); + assert_eq!(base_fee, None); + } + + /// After Jovian, the next block base fee cannot be less than the minimum base fee. + #[test] + fn test_get_base_fee_jovian_default_extra_data_and_min_base_fee() { + const CURR_BASE_FEE: u64 = 1; + const MIN_BASE_FEE: u64 = 10; + + let mut extra_data = Vec::new(); + extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE); + // eip1559 params + extra_data.append(&mut [0_u8; 8].to_vec()); + // min base fee + extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec()); + let extra_data = Bytes::from(extra_data); + + let op_chain_spec = jovian_chainspec(); + let parent = Header { + base_fee_per_gas: Some(CURR_BASE_FEE), + gas_used: 15763614, + gas_limit: 144000000, + timestamp: JOVIAN_TIMESTAMP, + extra_data, + ..Default::default() + }; + let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee( + &op_chain_spec, + &parent, + JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS, + ); + assert_eq!(base_fee, Some(MIN_BASE_FEE)); + } + + /// After Jovian, the next block base fee cannot be less than the minimum base fee. + #[test] + fn test_jovian_min_base_fee_cannot_decrease() { + const MIN_BASE_FEE: u64 = 10; + + let mut extra_data = Vec::new(); + extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE); + // eip1559 params + extra_data.append(&mut [0_u8; 8].to_vec()); + // min base fee + extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec()); + let extra_data = Bytes::from(extra_data); + + let op_chain_spec = jovian_chainspec(); + + // If we're currently at the minimum base fee, the next block base fee cannot decrease. + let parent = Header { + base_fee_per_gas: Some(MIN_BASE_FEE), + gas_used: 10, + gas_limit: 144000000, + timestamp: JOVIAN_TIMESTAMP, + extra_data: extra_data.clone(), + ..Default::default() + }; + let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee( + &op_chain_spec, + &parent, + JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS, + ); + assert_eq!(base_fee, Some(MIN_BASE_FEE)); + + // The next block can increase the base fee + let parent = Header { + base_fee_per_gas: Some(MIN_BASE_FEE), + gas_used: 144000000, + gas_limit: 144000000, + timestamp: JOVIAN_TIMESTAMP, + extra_data, + ..Default::default() + }; + let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee( + &op_chain_spec, + &parent, + JOVIAN_TIMESTAMP + 2 * BLOCK_TIME_SECONDS, + ); + assert_eq!(base_fee, Some(MIN_BASE_FEE + 1)); + } + + #[test] + fn test_jovian_base_fee_can_decrease_if_above_min_base_fee() { + const MIN_BASE_FEE: u64 = 10; + + let mut extra_data = Vec::new(); + extra_data.push(JOVIAN_EXTRA_DATA_VERSION_BYTE); + // eip1559 params + extra_data.append(&mut [0_u8; 8].to_vec()); + // min base fee + extra_data.append(&mut MIN_BASE_FEE.to_be_bytes().to_vec()); + let extra_data = Bytes::from(extra_data); + + let op_chain_spec = jovian_chainspec(); + + let parent = Header { + base_fee_per_gas: Some(100 * MIN_BASE_FEE), + gas_used: 10, + gas_limit: 144000000, + timestamp: JOVIAN_TIMESTAMP, + extra_data, + ..Default::default() + }; + let base_fee = reth_optimism_chainspec::OpChainSpec::next_block_base_fee( + &op_chain_spec, + &parent, + JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS, + ) + .unwrap(); + assert_eq!( + base_fee, + op_chain_spec + .inner + .next_block_base_fee(&parent, JOVIAN_TIMESTAMP + BLOCK_TIME_SECONDS) + .unwrap() + ); + } + #[test] fn body_against_header_isthmus() { let chainspec = isthmus_chainspec(); diff --git a/crates/optimism/node/src/engine.rs b/crates/optimism/node/src/engine.rs index 39bad86259..af018d6f27 100644 --- a/crates/optimism/node/src/engine.rs +++ b/crates/optimism/node/src/engine.rs @@ -226,6 +226,7 @@ where "MissingEip1559ParamsInPayloadAttributes".to_string().into(), ) })?; + if elasticity != 0 && denominator == 0 { return Err(EngineObjectValidationError::InvalidParams( "Eip1559ParamsDenominatorZero".to_string().into(), @@ -233,6 +234,19 @@ where } } + if self.chain_spec().is_jovian_active_at_timestamp(attributes.payload_attributes.timestamp) + { + if attributes.min_base_fee.is_none() { + return Err(EngineObjectValidationError::InvalidParams( + "MissingMinBaseFeeInPayloadAttributes".to_string().into(), + )); + } + } else if attributes.min_base_fee.is_some() { + return Err(EngineObjectValidationError::InvalidParams( + "MinBaseFeeNotAllowedBeforeJovian".to_string().into(), + )); + } + Ok(()) } } @@ -289,32 +303,46 @@ mod test { use crate::engine; use alloy_primitives::{b64, Address, B256, B64}; use alloy_rpc_types_engine::PayloadAttributes; - use reth_chainspec::ChainSpec; + use reth_chainspec::{ChainSpec, ForkCondition, Hardfork}; use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA}; + use reth_optimism_forks::OpHardfork; use reth_provider::noop::NoopProvider; use reth_trie_common::KeccakKeyHasher; + const JOVIAN_TIMESTAMP: u64 = 1744909000; + fn get_chainspec() -> Arc { + let mut base_sepolia_spec = BASE_SEPOLIA.inner.clone(); + + // TODO: Remove this once we know the Jovian timestamp + base_sepolia_spec + .hardforks + .insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP)); + 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 + chain: base_sepolia_spec.chain, + genesis: base_sepolia_spec.genesis, + genesis_header: base_sepolia_spec.genesis_header, + paris_block_and_final_difficulty: base_sepolia_spec .paris_block_and_final_difficulty, - hardforks: BASE_SEPOLIA.inner.hardforks.clone(), - base_fee_params: BASE_SEPOLIA.inner.base_fee_params.clone(), + hardforks: base_sepolia_spec.hardforks, + base_fee_params: base_sepolia_spec.base_fee_params, prune_delete_limit: 10000, ..Default::default() }, }) } - const fn get_attributes(eip_1559_params: Option, timestamp: u64) -> OpPayloadAttributes { + const fn get_attributes( + eip_1559_params: Option, + min_base_fee: Option, + timestamp: u64, + ) -> OpPayloadAttributes { OpPayloadAttributes { gas_limit: Some(1000), eip_1559_params, + min_base_fee, transactions: None, no_tx_pool: None, payload_attributes: PayloadAttributes { @@ -331,7 +359,7 @@ mod test { fn test_well_formed_attributes_pre_holocene() { let validator = OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); - let attributes = get_attributes(None, 1732633199); + let attributes = get_attributes(None, None, 1732633199); let result = as EngineApiValidator< OpEngineTypes, @@ -345,7 +373,7 @@ mod 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 attributes = get_attributes(None, None, 1732633200); let result = as EngineApiValidator< OpEngineTypes, @@ -359,7 +387,7 @@ mod 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 attributes = get_attributes(Some(b64!("0000000000000008")), None, 1732633200); let result = as EngineApiValidator< OpEngineTypes, @@ -373,7 +401,7 @@ mod 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 attributes = get_attributes(Some(b64!("0000000800000008")), None, 1732633200); let result = as EngineApiValidator< OpEngineTypes, @@ -387,7 +415,7 @@ mod 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 attributes = get_attributes(Some(b64!("0000000000000000")), None, 1732633200); let result = as EngineApiValidator< OpEngineTypes, @@ -396,4 +424,63 @@ mod test { ); assert!(result.is_ok()); } + + #[test] + fn test_well_formed_attributes_jovian_valid() { + let validator = + OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); + let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), JOVIAN_TIMESTAMP); + + let result = as EngineApiValidator< + OpEngineTypes, + >>::ensure_well_formed_attributes( + &validator, EngineApiMessageVersion::V3, &attributes, + ); + assert!(result.is_ok()); + } + + /// After Jovian (and holocene), eip1559 params must be Some + #[test] + fn test_malformed_attributes_jovian_with_eip_1559_params_none() { + let validator = + OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); + let attributes = get_attributes(None, Some(1), JOVIAN_TIMESTAMP); + + let result = as EngineApiValidator< + OpEngineTypes, + >>::ensure_well_formed_attributes( + &validator, EngineApiMessageVersion::V3, &attributes, + ); + assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_)))); + } + + /// Before Jovian, min base fee must be None + #[test] + fn test_malformed_attributes_pre_jovian_with_min_base_fee() { + let validator = + OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); + let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), 1732633200); + + let result = as EngineApiValidator< + OpEngineTypes, + >>::ensure_well_formed_attributes( + &validator, EngineApiMessageVersion::V3, &attributes, + ); + assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_)))); + } + + /// After Jovian, min base fee must be Some + #[test] + fn test_malformed_attributes_post_jovian_with_min_base_fee_none() { + let validator = + OpEngineValidator::new::(get_chainspec(), NoopProvider::default()); + let attributes = get_attributes(Some(b64!("0000000000000000")), None, JOVIAN_TIMESTAMP); + + let result = as EngineApiValidator< + OpEngineTypes, + >>::ensure_well_formed_attributes( + &validator, EngineApiMessageVersion::V3, &attributes, + ); + assert!(matches!(result, Err(EngineObjectValidationError::InvalidParams(_)))); + } } diff --git a/crates/optimism/node/src/utils.rs b/crates/optimism/node/src/utils.rs index 9e2f7b5b3b..42104c9df7 100644 --- a/crates/optimism/node/src/utils.rs +++ b/crates/optimism/node/src/utils.rs @@ -69,5 +69,6 @@ pub fn optimism_payload_attributes(timestamp: u64) -> OpPayloadBuilderAttribu no_tx_pool: false, gas_limit: Some(30_000_000), eip_1559_params: None, + min_base_fee: None, } } diff --git a/crates/optimism/node/tests/e2e-testsuite/testsuite.rs b/crates/optimism/node/tests/e2e-testsuite/testsuite.rs index b67c6e9770..75dff49c14 100644 --- a/crates/optimism/node/tests/e2e-testsuite/testsuite.rs +++ b/crates/optimism/node/tests/e2e-testsuite/testsuite.rs @@ -44,6 +44,7 @@ async fn test_testsuite_op_assert_mine_block() -> Result<()> { transactions: None, no_tx_pool: None, eip_1559_params: None, + min_base_fee: None, gas_limit: Some(30_000_000), }, )); diff --git a/crates/optimism/payload/src/payload.rs b/crates/optimism/payload/src/payload.rs index 388c950e0b..de1705faa8 100644 --- a/crates/optimism/payload/src/payload.rs +++ b/crates/optimism/payload/src/payload.rs @@ -12,7 +12,7 @@ use alloy_rpc_types_engine::{ BlobsBundleV1, ExecutionPayloadEnvelopeV2, ExecutionPayloadFieldV2, ExecutionPayloadV1, ExecutionPayloadV3, PayloadId, }; -use op_alloy_consensus::{encode_holocene_extra_data, EIP1559ParamError}; +use op_alloy_consensus::{encode_holocene_extra_data, encode_jovian_extra_data, EIP1559ParamError}; use op_alloy_rpc_types_engine::{ OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, }; @@ -44,6 +44,8 @@ pub struct OpPayloadBuilderAttributes { pub gas_limit: Option, /// EIP-1559 parameters for the generated payload pub eip_1559_params: Option, + /// Min base fee for the generated payload (only available post-Jovian) + pub min_base_fee: Option, } impl Default for OpPayloadBuilderAttributes { @@ -54,12 +56,14 @@ impl Default for OpPayloadBuilderAttributes { gas_limit: Default::default(), eip_1559_params: Default::default(), transactions: Default::default(), + min_base_fee: Default::default(), } } } impl OpPayloadBuilderAttributes { - /// Extracts the `eip1559` parameters for the payload. + /// Extracts the extra data parameters post-Holocene hardfork. + /// In Holocene, those parameters are the EIP-1559 base fee parameters. pub fn get_holocene_extra_data( &self, default_base_fee_params: BaseFeeParams, @@ -68,6 +72,18 @@ impl OpPayloadBuilderAttributes { .map(|params| encode_holocene_extra_data(params, default_base_fee_params)) .ok_or(EIP1559ParamError::NoEIP1559Params)? } + + /// Extracts the extra data parameters post-Jovian hardfork. + /// Those parameters are the EIP-1559 parameters from Holocene and the minimum base fee. + pub fn get_jovian_extra_data( + &self, + default_base_fee_params: BaseFeeParams, + ) -> Result { + let min_base_fee = self.min_base_fee.ok_or(EIP1559ParamError::MinBaseFeeNotSet)?; + self.eip_1559_params + .map(|params| encode_jovian_extra_data(params, default_base_fee_params, min_base_fee)) + .ok_or(EIP1559ParamError::NoEIP1559Params)? + } } impl PayloadBuilderAttributes @@ -111,6 +127,7 @@ impl PayloadBuilderAtt transactions, gas_limit: attributes.gas_limit, eip_1559_params: attributes.eip_1559_params, + min_base_fee: attributes.min_base_fee, }) } @@ -387,7 +404,13 @@ where parent: &SealedHeader, chain_spec: &ChainSpec, ) -> Result { - let extra_data = if chain_spec.is_holocene_active_at_timestamp(attributes.timestamp()) { + let extra_data = if chain_spec.is_jovian_active_at_timestamp(attributes.timestamp()) { + attributes + .get_jovian_extra_data( + chain_spec.base_fee_params_at_timestamp(attributes.timestamp()), + ) + .map_err(PayloadBuilderError::other)? + } else if chain_spec.is_holocene_active_at_timestamp(attributes.timestamp()) { attributes .get_holocene_extra_data( chain_spec.base_fee_params_at_timestamp(attributes.timestamp()), @@ -436,6 +459,7 @@ mod tests { no_tx_pool: None, gas_limit: Some(30000000), eip_1559_params: None, + min_base_fee: None, }; // Reth's `PayloadId` should match op-geth's `PayloadId`. This fails @@ -467,4 +491,50 @@ mod tests { let extra_data = attributes.get_holocene_extra_data(BaseFeeParams::new(80, 60)); assert_eq!(extra_data.unwrap(), Bytes::copy_from_slice(&[0, 0, 0, 0, 80, 0, 0, 0, 60])); } + + #[test] + fn test_get_extra_data_post_jovian() { + let attributes: OpPayloadBuilderAttributes = + OpPayloadBuilderAttributes { + eip_1559_params: Some(B64::from_str("0x0000000800000008").unwrap()), + min_base_fee: Some(10), + ..Default::default() + }; + let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60)); + assert_eq!( + extra_data.unwrap(), + // Version byte is 1 for Jovian, then holocene payload followed by 8 bytes for the + // minimum base fee + Bytes::copy_from_slice(&[1, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 10]) + ); + } + + #[test] + fn test_get_extra_data_post_jovian_default() { + let attributes: OpPayloadBuilderAttributes = + OpPayloadBuilderAttributes { + eip_1559_params: Some(B64::ZERO), + min_base_fee: Some(10), + ..Default::default() + }; + let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60)); + assert_eq!( + extra_data.unwrap(), + // Version byte is 1 for Jovian, then holocene payload followed by 8 bytes for the + // minimum base fee + Bytes::copy_from_slice(&[1, 0, 0, 0, 80, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 10]) + ); + } + + #[test] + fn test_get_extra_data_post_jovian_no_base_fee() { + let attributes: OpPayloadBuilderAttributes = + OpPayloadBuilderAttributes { + eip_1559_params: Some(B64::ZERO), + min_base_fee: None, + ..Default::default() + }; + let extra_data = attributes.get_jovian_extra_data(BaseFeeParams::new(80, 60)); + assert_eq!(extra_data.unwrap_err(), EIP1559ParamError::MinBaseFeeNotSet); + } } diff --git a/examples/custom-beacon-withdrawals/src/main.rs b/examples/custom-beacon-withdrawals/src/main.rs index 3aeaaa7176..713b02cc83 100644 --- a/examples/custom-beacon-withdrawals/src/main.rs +++ b/examples/custom-beacon-withdrawals/src/main.rs @@ -5,9 +5,10 @@ use alloy_eips::eip4895::Withdrawal; use alloy_evm::{ - block::{BlockExecutorFactory, BlockExecutorFor, CommitChanges, ExecutableTx}, + block::{BlockExecutorFactory, BlockExecutorFor, ExecutableTx}, eth::{EthBlockExecutionCtx, EthBlockExecutor}, precompiles::PrecompilesMap, + revm::context::result::ResultAndState, EthEvm, EthEvmFactory, }; use alloy_sol_macro::sol; @@ -22,7 +23,7 @@ use reth_ethereum::{ NextBlockEnvAttributes, OnStateHook, }, revm::{ - context::{result::ExecutionResult, TxEnv}, + context::TxEnv, db::State, primitives::{address, hardfork::SpecId, Address}, DatabaseCommit, @@ -191,12 +192,19 @@ where self.inner.apply_pre_execution_changes() } - fn execute_transaction_with_commit_condition( + fn execute_transaction_without_commit( &mut self, tx: impl ExecutableTx, - f: impl FnOnce(&ExecutionResult<::HaltReason>) -> CommitChanges, - ) -> Result, BlockExecutionError> { - self.inner.execute_transaction_with_commit_condition(tx, f) + ) -> Result::HaltReason>, BlockExecutionError> { + self.inner.execute_transaction_without_commit(tx) + } + + fn commit_transaction( + &mut self, + output: ResultAndState<::HaltReason>, + tx: impl ExecutableTx, + ) -> Result { + self.inner.commit_transaction(output, tx) } fn finish(mut self) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { diff --git a/examples/custom-node/src/evm/executor.rs b/examples/custom-node/src/evm/executor.rs index 61514813c2..5288e1d67a 100644 --- a/examples/custom-node/src/evm/executor.rs +++ b/examples/custom-node/src/evm/executor.rs @@ -9,7 +9,7 @@ use alloy_consensus::transaction::Recovered; use alloy_evm::{ block::{ BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory, - BlockExecutorFor, CommitChanges, ExecutableTx, OnStateHook, + BlockExecutorFor, ExecutableTx, OnStateHook, }, precompiles::PrecompilesMap, Database, Evm, @@ -17,7 +17,7 @@ use alloy_evm::{ use alloy_op_evm::{OpBlockExecutionCtx, OpBlockExecutor}; use reth_ethereum::evm::primitives::InspectorFor; use reth_op::{chainspec::OpChainSpec, node::OpRethReceiptBuilder, OpReceipt}; -use revm::{context::result::ExecutionResult, database::State}; +use revm::{context::result::ResultAndState, database::State}; use std::sync::Arc; pub struct CustomBlockExecutor { @@ -37,16 +37,27 @@ where self.inner.apply_pre_execution_changes() } - fn execute_transaction_with_commit_condition( + fn execute_transaction_without_commit( &mut self, tx: impl ExecutableTx, - f: impl FnOnce(&ExecutionResult<::HaltReason>) -> CommitChanges, - ) -> Result, BlockExecutionError> { + ) -> Result::HaltReason>, BlockExecutionError> { match tx.tx() { - CustomTransaction::Op(op_tx) => self.inner.execute_transaction_with_commit_condition( - Recovered::new_unchecked(op_tx, *tx.signer()), - f, - ), + CustomTransaction::Op(op_tx) => self + .inner + .execute_transaction_without_commit(Recovered::new_unchecked(op_tx, *tx.signer())), + CustomTransaction::Payment(..) => todo!(), + } + } + + fn commit_transaction( + &mut self, + output: ResultAndState<::HaltReason>, + tx: impl ExecutableTx, + ) -> Result { + match tx.tx() { + CustomTransaction::Op(op_tx) => { + self.inner.commit_transaction(output, Recovered::new_unchecked(op_tx, *tx.signer())) + } CustomTransaction::Payment(..) => todo!(), } }