From 5f25cf4de542ae86c603ac3575ae0f063ba60906 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Mon, 17 Nov 2025 14:53:46 +0100 Subject: [PATCH 1/4] upgrade process --- README.md | 11 +- crates/ev-precompiles/README.md | 6 +- crates/ev-revm/src/factory.rs | 253 ++++++++++++++++-- crates/ev-revm/src/lib.rs | 2 +- crates/node/src/builder.rs | 13 +- crates/node/src/config.rs | 98 +++++++ crates/node/src/executor.rs | 33 ++- crates/node/src/payload_service.rs | 23 +- crates/tests/src/common.rs | 13 +- docs/adr/ADR-0001-base-fee-redirect.md | 2 +- .../adr/ADR-0002-native-minting-precompile.md | 4 +- etc/ev-reth-genesis.json | 3 +- 12 files changed, 403 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 1c4e0c3..9c9bd54 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,8 @@ Add an `evolve` stanza to your chainspec under the `config` section: "config": { ..., "evolve": { - "baseFeeSink": "0xYourRecipientAddressHere" + "baseFeeSink": "0xYourRecipientAddressHere", + "baseFeeRedirectActivationHeight": 0 } } ``` @@ -233,13 +234,18 @@ Rebuild (or restart) the node with the updated chainspec so the payload builder You can see a working example in `etc/ev-reth-genesis.json`, which routes the base fee to `0x00000000000000000000000000000000000000fe` by default. +Set `baseFeeRedirectActivationHeight` to the block where the new behavior should begin. Leave it at +`0` for fresh chains that enable the redirect from genesis. + What it does: + - Intercepts the base fee during EVM execution (via the ev-revm handler) - Credits `base_fee_per_gas * gas_used` to the specified recipient for each transaction - The redirect happens at the EVM handler level, ensuring the state root reflects the credited balance - This effectively "unburns" the base fee on your network (Ethereum mainnet keeps burning the base fee by protocol design) Implementation details: + - Uses the `ev-revm` crate to wrap the EVM with a custom handler - The handler intercepts the `reward_beneficiary` hook to redirect base fees - No runtime environment variables are required; the chainspec carries the policy alongside other fork settings @@ -260,10 +266,12 @@ The txpool RPC extension can be configured with: - `max_txpool_gas`: Maximum cumulative gas for transactions to return (default: 30,000,000) Notes: + - Both limits apply together. Selection stops when either cap is reached. - Set a limit to `0` to disable that constraint. CLI/env overrides: + - None for txpool gas. The RPC follows the current block gas automatically. ### Gas Limits: Block vs Txpool @@ -273,6 +281,7 @@ CLI/env overrides: - Relationship: These limits are aligned by default. Overriding the txpool cap makes them independent again; exact packing still depends on real execution. Changing limits on a running chain: + - Per-block gas: Set `gasLimit` in Engine API payload attributes to change the block’s gas limit for that payload. Subsequent payloads will default to that new parent header gas limit unless overridden again. - Txpool gas cap: Follows the head block’s gas limit automatically. There is no fixed-cap override; change your block gas and the RPC alignment follows. diff --git a/crates/ev-precompiles/README.md b/crates/ev-precompiles/README.md index 43adbd9..89a304a 100644 --- a/crates/ev-precompiles/README.md +++ b/crates/ev-precompiles/README.md @@ -133,13 +133,17 @@ The mint admin is configured in the chain specification. See `crates/node/src/co "config": { "chainId": 1234, "evolve": { - "mintAdmin": "0x1234567890123456789012345678901234567890" + "mintAdmin": "0x1234567890123456789012345678901234567890", + "mintPrecompileActivationHeight": 0 } } } ``` If no mint admin is specified, the precompile is still available but will reject all calls. +Set `mintPrecompileActivationHeight` to the block where the precompile should become callable. For +new networks keep it at `0` so the admin is active from genesis; existing chains can use a higher +value to stage upgrades safely. ### Allowlist Management diff --git a/crates/ev-revm/src/factory.rs b/crates/ev-revm/src/factory.rs index 2919a8c..8ac5184 100644 --- a/crates/ev-revm/src/factory.rs +++ b/crates/ev-revm/src/factory.rs @@ -6,7 +6,7 @@ use alloy_evm::{ precompiles::{DynPrecompile, Precompile, PrecompilesMap}, Database, EvmEnv, EvmFactory, }; -use alloy_primitives::Address; +use alloy_primitives::{Address, U256}; use ev_precompiles::mint::{MintPrecompile, MINT_PRECOMPILE_ADDR}; use reth_evm_ethereum::EthEvmConfig; use reth_revm::{ @@ -23,32 +23,87 @@ use reth_revm::{ }; use std::sync::Arc; +/// Settings for enabling the base-fee redirect at a specific block height. +#[derive(Debug, Clone, Copy)] +pub struct BaseFeeRedirectSettings { + redirect: BaseFeeRedirect, + activation_height: u64, +} + +impl BaseFeeRedirectSettings { + /// Creates a new settings object. + pub const fn new(redirect: BaseFeeRedirect, activation_height: u64) -> Self { + Self { + redirect, + activation_height, + } + } + + const fn activation_height(&self) -> u64 { + self.activation_height + } + + const fn redirect(&self) -> BaseFeeRedirect { + self.redirect + } +} + +/// Settings for enabling the mint precompile at a specific block height. +#[derive(Debug, Clone, Copy)] +pub struct MintPrecompileSettings { + admin: Address, + activation_height: u64, +} + +impl MintPrecompileSettings { + /// Creates a new settings object. + pub const fn new(admin: Address, activation_height: u64) -> Self { + Self { + admin, + activation_height, + } + } + + const fn activation_height(&self) -> u64 { + self.activation_height + } + + const fn admin(&self) -> Address { + self.admin + } +} + /// Wrapper around an existing `EvmFactory` that produces [`EvEvm`] instances. #[derive(Debug, Clone)] pub struct EvEvmFactory { inner: F, - redirect: Option, - mint_admin: Option
, + redirect: Option, + mint_precompile: Option, } impl EvEvmFactory { /// Creates a new factory wrapper with the given redirect policy. pub const fn new( inner: F, - redirect: Option, - mint_admin: Option
, + redirect: Option, + mint_precompile: Option, ) -> Self { Self { inner, redirect, - mint_admin, + mint_precompile, } } - fn install_mint_precompile(&self, precompiles: &mut PrecompilesMap) { - let Some(admin) = self.mint_admin else { return }; + fn install_mint_precompile(&self, precompiles: &mut PrecompilesMap, block_number: U256) { + let Some(settings) = self.mint_precompile else { + return; + }; + if block_number < U256::from(settings.activation_height()) { + return; + } - let mint = Arc::new(MintPrecompile::new(admin)); + let mint = Arc::new(MintPrecompile::new(settings.admin())); let id = MintPrecompile::id().clone(); precompiles.apply_precompile(&MINT_PRECOMPILE_ADDR, move |_| { @@ -59,6 +114,16 @@ impl EvEvmFactory { })) }); } + + fn redirect_for_block(&self, block_number: U256) -> Option { + self.redirect.and_then(|settings| { + if block_number >= U256::from(settings.activation_height()) { + Some(settings.redirect()) + } else { + None + } + }) + } } impl EvmFactory for EvEvmFactory { @@ -78,11 +143,12 @@ impl EvmFactory for EvEvmFactory { db: DB, evm_env: EvmEnv, ) -> Self::Evm { + let block_number = evm_env.block_env.number; let inner = self.inner.create_evm(db, evm_env); - let mut evm = EvEvm::from_inner(inner, self.redirect, false); + let mut evm = EvEvm::from_inner(inner, self.redirect_for_block(block_number), false); { let inner = evm.inner_mut(); - self.install_mint_precompile(&mut inner.precompiles); + self.install_mint_precompile(&mut inner.precompiles, block_number); } evm } @@ -93,11 +159,12 @@ impl EvmFactory for EvEvmFactory { input: EvmEnv, inspector: I, ) -> Self::Evm { + let block_number = input.block_env.number; let inner = self.inner.create_evm_with_inspector(db, input, inspector); - let mut evm = EvEvm::from_inner(inner, self.redirect, true); + let mut evm = EvEvm::from_inner(inner, self.redirect_for_block(block_number), true); { let inner = evm.inner_mut(); - self.install_mint_precompile(&mut inner.precompiles); + self.install_mint_precompile(&mut inner.precompiles, block_number); } evm } @@ -106,14 +173,15 @@ impl EvmFactory for EvEvmFactory { /// Wraps an [`EthEvmConfig`] so that it produces [`EvEvm`] instances. pub fn with_ev_handler( config: EthEvmConfig, - redirect: Option, - mint_admin: Option
, + redirect: Option, + mint_precompile: Option, ) -> EthEvmConfig> { let EthEvmConfig { executor_factory, block_assembler, } = config; - let wrapped_factory = EvEvmFactory::new(*executor_factory.evm_factory(), redirect, mint_admin); + let wrapped_factory = + EvEvmFactory::new(*executor_factory.evm_factory(), redirect, mint_precompile); let new_executor_factory = EthBlockExecutorFactory::new( *executor_factory.receipt_builder(), executor_factory.spec().clone(), @@ -144,6 +212,23 @@ mod tests { State, }; + sol! { + contract MintAdminProxy { + function mint(address to, uint256 amount); + } + } + + const ADMIN_PROXY_RUNTIME: [u8; 42] = alloy_primitives::hex!( + "36600060003760006000366000600073000000000000000000000000000000000000f1005af1600080f3" + ); + + fn empty_state() -> State> { + State::builder() + .with_database(CacheDB::::default()) + .with_bundle_update() + .build() + } + #[test] fn factory_applies_base_fee_redirect() { let sink = address!("0x00000000000000000000000000000000000000fe"); @@ -178,7 +263,7 @@ mod tests { let redirect = BaseFeeRedirect::new(sink); let mut evm = EvEvmFactory::new( alloy_evm::eth::EthEvmFactory::default(), - Some(redirect), + Some(BaseFeeRedirectSettings::new(redirect, 0)), None, ) .create_evm(state, evm_env.clone()); @@ -227,16 +312,6 @@ mod tests { #[test] fn mint_precompile_via_proxy_runtime_mints() { - sol! { - contract MintAdminProxy { - function mint(address to, uint256 amount); - } - } - - const ADMIN_PROXY_RUNTIME: [u8; 42] = alloy_primitives::hex!( - "36600060003760006000366000600073000000000000000000000000000000000000f1005af1600080f3" - ); - let caller = address!("0x0000000000000000000000000000000000000aaa"); let contract = address!("0x0000000000000000000000000000000000000bbb"); let mintee = address!("0x0000000000000000000000000000000000000ccc"); @@ -279,7 +354,7 @@ mod tests { let mut evm = EvEvmFactory::new( alloy_evm::eth::EthEvmFactory::default(), None, - Some(contract), + Some(MintPrecompileSettings::new(contract, 0)), ) .create_evm(state, evm_env); @@ -312,4 +387,126 @@ mod tests { "mint proxy should credit the recipient" ); } + + #[test] + fn base_fee_redirect_respects_activation_height() { + let sink = address!("0x0000000000000000000000000000000000000123"); + let factory = EvEvmFactory::new( + alloy_evm::eth::EthEvmFactory::default(), + Some(BaseFeeRedirectSettings::new(BaseFeeRedirect::new(sink), 5)), + None, + ); + + let mut before_env: alloy_evm::EvmEnv = EvmEnv::default(); + before_env.cfg_env.chain_id = 1; + before_env.cfg_env.spec = SpecId::CANCUN; + before_env.block_env.number = U256::from(4); + before_env.block_env.gas_limit = 30_000_000; + + let mut after_env = before_env.clone(); + after_env.block_env.number = U256::from(5); + + let evm_before = factory.create_evm(empty_state(), before_env); + assert!( + evm_before.redirect().is_none(), + "redirect inactive before fork" + ); + + let evm_after = factory.create_evm(empty_state(), after_env); + assert!( + evm_after.redirect().is_some(), + "redirect active at fork height" + ); + } + + #[test] + fn mint_precompile_respects_activation_height() { + let caller = address!("0x0000000000000000000000000000000000000aaa"); + let contract = address!("0x0000000000000000000000000000000000000bbb"); + let mintee = address!("0x0000000000000000000000000000000000000ccc"); + let amount = U256::from(1_000_000u64); + + let build_state = || { + let mut state = State::builder() + .with_database(CacheDB::::default()) + .with_bundle_update() + .build(); + + state.insert_account( + caller, + AccountInfo { + balance: U256::from(10_000_000_000u64), + nonce: 0, + code_hash: KECCAK_EMPTY, + code: None, + }, + ); + + state.insert_account( + contract, + AccountInfo { + balance: U256::ZERO, + nonce: 1, + code_hash: keccak256(ADMIN_PROXY_RUNTIME.as_slice()), + code: Some(RevmBytecode::new_raw(Bytes::copy_from_slice( + ADMIN_PROXY_RUNTIME.as_slice(), + ))), + }, + ); + + state + }; + + let factory = EvEvmFactory::new( + alloy_evm::eth::EthEvmFactory::default(), + None, + Some(MintPrecompileSettings::new(contract, 3)), + ); + + let tx_env = || crate::factory::TxEnv { + caller, + kind: TxKind::Call(contract), + gas_limit: 500_000, + gas_price: 1, + value: U256::ZERO, + data: MintAdminProxy::mintCall { to: mintee, amount } + .abi_encode() + .into(), + ..Default::default() + }; + + let mut before_env: alloy_evm::EvmEnv = EvmEnv::default(); + before_env.cfg_env.chain_id = 1; + before_env.cfg_env.spec = SpecId::CANCUN; + before_env.block_env.number = U256::from(2); + before_env.block_env.basefee = 1; + before_env.block_env.gas_limit = 30_000_000; + + let mut evm_before = factory.create_evm(build_state(), before_env); + let result_before = evm_before + .transact_raw(tx_env()) + .expect("pre-activation call executes"); + let state: EvmState = result_before.state; + assert!( + state.get(&mintee).is_none(), + "precompile must not mint before activation height" + ); + + let mut after_env: alloy_evm::EvmEnv = EvmEnv::default(); + after_env.cfg_env.chain_id = 1; + after_env.cfg_env.spec = SpecId::CANCUN; + after_env.block_env.number = U256::from(3); + after_env.block_env.basefee = 1; + after_env.block_env.gas_limit = 30_000_000; + + let mut evm_after = factory.create_evm(build_state(), after_env); + let result_after = evm_after + .transact_raw(tx_env()) + .expect("post-activation call executes"); + let state: EvmState = result_after.state; + let mintee_account = state + .get(&mintee) + .expect("mint precompile should mint after activation"); + assert_eq!(mintee_account.info.balance, amount); + } } diff --git a/crates/ev-revm/src/lib.rs b/crates/ev-revm/src/lib.rs index 319969e..f784b5f 100644 --- a/crates/ev-revm/src/lib.rs +++ b/crates/ev-revm/src/lib.rs @@ -11,5 +11,5 @@ pub use api::EvBuilder; pub use base_fee::{BaseFeeRedirect, BaseFeeRedirectError}; pub use config::{BaseFeeConfig, ConfigError}; pub use evm::{DefaultEvEvm, EvEvm}; -pub use factory::{with_ev_handler, EvEvmFactory}; +pub use factory::{with_ev_handler, BaseFeeRedirectSettings, EvEvmFactory, MintPrecompileSettings}; pub use handler::EvHandler; diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs index c8b174f..23acc2c 100644 --- a/crates/node/src/builder.rs +++ b/crates/node/src/builder.rs @@ -46,8 +46,13 @@ where evm_config: EvolveEthEvmConfig, config: EvolvePayloadBuilderConfig, ) -> Self { - if let Some(sink) = config.base_fee_sink { - info!(target: "ev-reth", fee_sink = ?sink, "Base fee redirect enabled via chainspec"); + if let Some((sink, activation)) = config.base_fee_redirect_settings() { + info!( + target: "ev-reth", + fee_sink = ?sink, + activation_height = activation, + "Base fee redirect enabled via chainspec" + ); } Self { @@ -85,6 +90,7 @@ where .ok_or_else(|| { PayloadBuilderError::Internal(RethError::Other("Parent header not found".into())) })?; + let block_number = parent_header.number + 1; let sealed_parent = SealedHeader::new(parent_header, attributes.parent_hash); // Create next block environment attributes @@ -97,11 +103,12 @@ where // Set coinbase/beneficiary from attributes, defaulting to sink when unset. let mut suggested_fee_recipient = attributes.suggested_fee_recipient; if suggested_fee_recipient == Address::ZERO { - if let Some(sink) = self.config.base_fee_sink { + if let Some(sink) = self.config.base_fee_sink_for_block(block_number) { suggested_fee_recipient = sink; info!( target: "ev-reth", fee_sink = ?sink, + block_number, "Suggested fee recipient missing; defaulting to base-fee sink" ); } diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index dd30e6f..520405f 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -8,6 +8,10 @@ struct ChainspecEvolveConfig { pub base_fee_sink: Option
, #[serde(default, rename = "mintAdmin")] pub mint_admin: Option
, + #[serde(default, rename = "baseFeeRedirectActivationHeight")] + pub base_fee_redirect_activation_height: Option, + #[serde(default, rename = "mintPrecompileActivationHeight")] + pub mint_precompile_activation_height: Option, } /// Configuration for the Evolve payload builder @@ -19,6 +23,12 @@ pub struct EvolvePayloadBuilderConfig { /// Optional mint precompile admin address sourced from the chainspec. #[serde(default)] pub mint_admin: Option
, + /// Optional activation height for base-fee redirect; defaults to 0 when sink set. + #[serde(default)] + pub base_fee_redirect_activation_height: Option, + /// Optional activation height for mint precompile; defaults to 0 when admin set. + #[serde(default)] + pub mint_precompile_activation_height: Option, } impl EvolvePayloadBuilderConfig { @@ -27,6 +37,8 @@ impl EvolvePayloadBuilderConfig { Self { base_fee_sink: None, mint_admin: None, + base_fee_redirect_activation_height: None, + mint_precompile_activation_height: None, } } @@ -41,10 +53,22 @@ impl EvolvePayloadBuilderConfig { { let extras = extra.map_err(ConfigError::InvalidExtras)?; config.base_fee_sink = extras.base_fee_sink; + config.base_fee_redirect_activation_height = extras.base_fee_redirect_activation_height; config.mint_admin = extras .mint_admin .and_then(|addr| if addr.is_zero() { None } else { Some(addr) }); + config.mint_precompile_activation_height = extras.mint_precompile_activation_height; + + if config.base_fee_sink.is_some() + && config.base_fee_redirect_activation_height.is_none() + { + config.base_fee_redirect_activation_height = Some(0); + } + + if config.mint_admin.is_some() && config.mint_precompile_activation_height.is_none() { + config.mint_precompile_activation_height = Some(0); + } } Ok(config) } @@ -53,6 +77,28 @@ impl EvolvePayloadBuilderConfig { pub const fn validate(&self) -> Result<(), ConfigError> { Ok(()) } + + /// Returns the configured base-fee redirect sink and activation height (defaulting to 0). + pub fn base_fee_redirect_settings(&self) -> Option<(Address, u64)> { + self.base_fee_sink.map(|sink| { + let activation = self.base_fee_redirect_activation_height.unwrap_or(0); + (sink, activation) + }) + } + + /// Returns the mint precompile admin and activation height (defaulting to 0). + pub fn mint_precompile_settings(&self) -> Option<(Address, u64)> { + self.mint_admin.map(|admin| { + let activation = self.mint_precompile_activation_height.unwrap_or(0); + (admin, activation) + }) + } + + /// Returns the sink if the redirect is active for the provided block number. + pub fn base_fee_sink_for_block(&self, block_number: u64) -> Option
{ + self.base_fee_redirect_settings() + .and_then(|(sink, activation)| (block_number >= activation).then_some(sink)) + } } /// Errors that can occur during configuration validation @@ -103,6 +149,8 @@ mod tests { assert_eq!(config.base_fee_sink, Some(test_address)); assert_eq!(config.mint_admin, None); + assert_eq!(config.base_fee_redirect_activation_height, Some(0)); + assert_eq!(config.mint_precompile_activation_height, None); } #[test] @@ -117,6 +165,28 @@ mod tests { assert_eq!(config.base_fee_sink, None); assert_eq!(config.mint_admin, Some(mint_admin)); + assert_eq!(config.base_fee_redirect_activation_height, None); + assert_eq!(config.mint_precompile_activation_height, Some(0)); + } + + #[test] + fn test_activation_heights_override() { + let sink = address!("0000000000000000000000000000000000000002"); + let admin = address!("00000000000000000000000000000000000000bb"); + let extras = json!({ + "baseFeeSink": sink, + "baseFeeRedirectActivationHeight": 42, + "mintAdmin": admin, + "mintPrecompileActivationHeight": 64 + }); + + let chainspec = create_test_chainspec_with_extras(Some(extras)); + let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap(); + + assert_eq!(config.base_fee_sink, Some(sink)); + assert_eq!(config.base_fee_redirect_activation_height, Some(42)); + assert_eq!(config.mint_admin, Some(admin)); + assert_eq!(config.mint_precompile_activation_height, Some(64)); } #[test] @@ -129,6 +199,7 @@ mod tests { let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap(); assert_eq!(config.mint_admin, None); + assert_eq!(config.mint_precompile_activation_height, None); } #[test] @@ -140,6 +211,7 @@ mod tests { let config = EvolvePayloadBuilderConfig::from_chain_spec(&chainspec).unwrap(); assert_eq!(config.base_fee_sink, None); + assert_eq!(config.base_fee_redirect_activation_height, None); } #[test] @@ -150,6 +222,8 @@ mod tests { assert_eq!(config.base_fee_sink, None); assert_eq!(config.mint_admin, None); + assert_eq!(config.base_fee_redirect_activation_height, None); + assert_eq!(config.mint_precompile_activation_height, None); } #[test] @@ -186,6 +260,8 @@ mod tests { let config = EvolvePayloadBuilderConfig::default(); assert_eq!(config.base_fee_sink, None); assert_eq!(config.mint_admin, None); + assert_eq!(config.base_fee_redirect_activation_height, None); + assert_eq!(config.mint_precompile_activation_height, None); } #[test] @@ -194,6 +270,8 @@ mod tests { let config = EvolvePayloadBuilderConfig::new(); assert_eq!(config.base_fee_sink, None); assert_eq!(config.mint_admin, None); + assert_eq!(config.base_fee_redirect_activation_height, None); + assert_eq!(config.mint_precompile_activation_height, None); } #[test] @@ -205,10 +283,30 @@ mod tests { let config_with_sink = EvolvePayloadBuilderConfig { base_fee_sink: Some(address!("0000000000000000000000000000000000000001")), mint_admin: Some(address!("00000000000000000000000000000000000000aa")), + base_fee_redirect_activation_height: Some(0), + mint_precompile_activation_height: Some(0), }; assert!(config_with_sink.validate().is_ok()); } + #[test] + fn test_base_fee_sink_for_block() { + let sink = address!("0000000000000000000000000000000000000003"); + let mut config = EvolvePayloadBuilderConfig { + base_fee_sink: Some(sink), + mint_admin: None, + base_fee_redirect_activation_height: Some(5), + mint_precompile_activation_height: None, + }; + + assert_eq!(config.base_fee_sink_for_block(4), None); + assert_eq!(config.base_fee_sink_for_block(5), Some(sink)); + assert_eq!(config.base_fee_sink_for_block(10), Some(sink)); + + config.base_fee_redirect_activation_height = None; + assert_eq!(config.base_fee_sink_for_block(0), Some(sink)); + } + #[test] fn test_chainspec_evolve_config_deserialization() { // Test direct deserialization of ChainspecEvolveConfig diff --git a/crates/node/src/executor.rs b/crates/node/src/executor.rs index 6062a7c..1d570b3 100644 --- a/crates/node/src/executor.rs +++ b/crates/node/src/executor.rs @@ -1,7 +1,9 @@ //! Helpers to build the ev-reth executor with EV-specific hooks applied. use alloy_evm::eth::{spec::EthExecutorSpec, EthEvmFactory}; -use ev_revm::{with_ev_handler, BaseFeeRedirect, EvEvmFactory}; +use ev_revm::{ + with_ev_handler, BaseFeeRedirect, BaseFeeRedirectSettings, EvEvmFactory, MintPrecompileSettings, +}; use reth_chainspec::ChainSpec; use reth_ethereum::{ chainspec::EthereumHardforks, @@ -33,20 +35,23 @@ where let evolve_config = EvolvePayloadBuilderConfig::from_chain_spec(chain_spec.as_ref())?; evolve_config.validate()?; - let redirect = evolve_config.base_fee_sink.map(|sink| { - info!( - target = "ev-reth::executor", - fee_sink = ?sink, - "Base fee redirect enabled" - ); - BaseFeeRedirect::new(sink) - }); + let redirect = evolve_config + .base_fee_redirect_settings() + .map(|(sink, activation)| { + info!( + target = "ev-reth::executor", + fee_sink = ?sink, + activation_height = activation, + "Base fee redirect enabled" + ); + BaseFeeRedirectSettings::new(BaseFeeRedirect::new(sink), activation) + }); + + let mint_precompile = evolve_config + .mint_precompile_settings() + .map(|(admin, activation)| MintPrecompileSettings::new(admin, activation)); - Ok(with_ev_handler( - base_config, - redirect, - evolve_config.mint_admin, - )) + Ok(with_ev_handler(base_config, redirect, mint_precompile)) } /// Thin wrapper so we can plug the EV executor into the node components builder. diff --git a/crates/node/src/payload_service.rs b/crates/node/src/payload_service.rs index 26e9df4..d966776 100644 --- a/crates/node/src/payload_service.rs +++ b/crates/node/src/payload_service.rs @@ -90,6 +90,17 @@ where if self.config.base_fee_sink.is_some() { config.base_fee_sink = self.config.base_fee_sink; } + if self.config.base_fee_redirect_activation_height.is_some() { + config.base_fee_redirect_activation_height = + self.config.base_fee_redirect_activation_height; + } + if self.config.mint_admin.is_some() { + config.mint_admin = self.config.mint_admin; + } + if self.config.mint_precompile_activation_height.is_some() { + config.mint_precompile_activation_height = + self.config.mint_precompile_activation_height; + } config.validate()?; @@ -145,12 +156,14 @@ where // Publish effective gas limit for RPC alignment. set_current_block_gas_limit(effective_gas_limit); + let block_number = parent_header.number + 1; let mut fee_recipient = attributes.suggested_fee_recipient(); if fee_recipient == Address::ZERO { - if let Some(sink) = self.config.base_fee_sink { + if let Some(sink) = self.config.base_fee_sink_for_block(block_number) { info!( target: "ev-reth", fee_sink = ?sink, + block_number, "Suggested fee recipient missing; defaulting to base-fee sink" ); fee_recipient = sink; @@ -164,7 +177,7 @@ where attributes.prev_randao(), fee_recipient, attributes.parent(), - parent_header.number + 1, + block_number, ); // Build the payload using the evolve payload builder - use spawn_blocking for async work. @@ -212,12 +225,14 @@ where // Publish effective gas limit for RPC alignment. set_current_block_gas_limit(effective_gas_limit); + let block_number = parent_header.number + 1; let mut fee_recipient = attributes.suggested_fee_recipient(); if fee_recipient == Address::ZERO { - if let Some(sink) = self.config.base_fee_sink { + if let Some(sink) = self.config.base_fee_sink_for_block(block_number) { info!( target: "ev-reth", fee_sink = ?sink, + block_number, "Suggested fee recipient missing; defaulting to base-fee sink" ); fee_recipient = sink; @@ -231,7 +246,7 @@ where attributes.prev_randao(), fee_recipient, attributes.parent(), - parent_header.number + 1, + block_number, ); // Build empty payload - use spawn_blocking for async work. diff --git a/crates/tests/src/common.rs b/crates/tests/src/common.rs index 449588a..e4ef238 100644 --- a/crates/tests/src/common.rs +++ b/crates/tests/src/common.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use alloy_consensus::{transaction::SignerRecoverable, TxLegacy, TypedTransaction}; use alloy_genesis::Genesis; use alloy_primitives::{Address, Bytes, ChainId, Signature, TxKind, B256, U256}; -use ev_revm::{with_ev_handler, BaseFeeRedirect}; +use ev_revm::{with_ev_handler, BaseFeeRedirect, BaseFeeRedirectSettings, MintPrecompileSettings}; use eyre::Result; use reth_chainspec::{ChainSpec, ChainSpecBuilder}; use reth_ethereum_primitives::TransactionSigned; @@ -129,8 +129,15 @@ impl EvolveTestFixture { let config = EvolvePayloadBuilderConfig::from_chain_spec(test_chainspec.as_ref()).unwrap(); config.validate().unwrap(); - let base_fee_redirect = config.base_fee_sink.map(BaseFeeRedirect::new); - let wrapped_evm = with_ev_handler(evm_config, base_fee_redirect, config.mint_admin); + let base_fee_redirect = config + .base_fee_redirect_settings() + .map(|(sink, activation)| { + BaseFeeRedirectSettings::new(BaseFeeRedirect::new(sink), activation) + }); + let mint_precompile = config + .mint_precompile_settings() + .map(|(admin, activation)| MintPrecompileSettings::new(admin, activation)); + let wrapped_evm = with_ev_handler(evm_config, base_fee_redirect, mint_precompile); let builder = EvolvePayloadBuilder::new(Arc::new(provider.clone()), wrapped_evm, config); diff --git a/docs/adr/ADR-0001-base-fee-redirect.md b/docs/adr/ADR-0001-base-fee-redirect.md index 54ff266..ba06340 100644 --- a/docs/adr/ADR-0001-base-fee-redirect.md +++ b/docs/adr/ADR-0001-base-fee-redirect.md @@ -39,7 +39,7 @@ Integration happens in three small steps: 2. `with_ev_handler` swaps the factory inside `EthEvmConfig` before the payload builder ever requests an executor. 3. `EvBuilder` mirrors `MainBuilder`, allowing code that builds EVMs from contexts to opt into the wrapper with a single method change. -Configuration remains opt-in: operators add an `ev_reth` block under the chainspec’s `config` section (for example, `{ "config": { ..., "ev_reth": { "baseFeeSink": "0x…" }}}`). On startup the node deserializes that block into `EvolvePayloadBuilderConfig`, converts the optional sink address into a `BaseFeeRedirect`, and hands it to `with_ev_handler`. If the field is absent or fails validation, the wrapper records `None` and the handler leaves the base-fee path untouched. +Configuration remains opt-in: operators add an `ev_reth` block under the chainspec’s `config` section (for example, `{ "config": { ..., "ev_reth": { "baseFeeSink": "0x…", "baseFeeRedirectActivationHeight": 0 }}}`). On startup the node deserializes that block into `EvolvePayloadBuilderConfig`, converts the optional sink address plus activation height into `BaseFeeRedirectSettings`, and hands those settings to `with_ev_handler`. If either field is absent or fails validation, the wrapper records `None` and the handler leaves the base-fee path untouched, allowing networks to stage upgrades at non-zero heights while still letting fresh chains enable the redirect from genesis. ## Consequences diff --git a/docs/adr/ADR-0002-native-minting-precompile.md b/docs/adr/ADR-0002-native-minting-precompile.md index 56097ba..9dd0c6a 100644 --- a/docs/adr/ADR-0002-native-minting-precompile.md +++ b/docs/adr/ADR-0002-native-minting-precompile.md @@ -57,8 +57,10 @@ Authorization is managed through a combination of a genesis-configured `mintAdmi * The `mintAdmin` is the only account that can add or remove addresses from the `allowlist`. * Both the `mintAdmin` and the addresses on the `allowlist` can call `mint` and `burn`. +* An optional `mintPrecompileActivationHeight` field allows networks to delay activation until a + future block so archival nodes remain compatible with historical state transitions. -This design provides a flexible and secure way to manage the native token supply. The `mintAdmin` can be a simple EOA for testing or a complex smart contract (e.g., a multisig wallet) for production environments. +This design provides a flexible and secure way to manage the native token supply. The `mintAdmin` can be a simple EOA for testing or a complex smart contract (e.g., a multisig wallet) for production environments, and activation can be staged at any block height by setting the chainspec field described above. ### Implementation Details diff --git a/etc/ev-reth-genesis.json b/etc/ev-reth-genesis.json index 034d068..b351cf3 100644 --- a/etc/ev-reth-genesis.json +++ b/etc/ev-reth-genesis.json @@ -17,7 +17,8 @@ "terminalTotalDifficulty": 0, "terminalTotalDifficultyPassed": true, "evolve": { - "baseFeeSink": "0x00000000000000000000000000000000000000fe" + "baseFeeSink": "0x00000000000000000000000000000000000000fe", + "baseFeeRedirectActivationHeight": 0 } }, "difficulty": "0x1", From 0f345d6c800a0f394104cd7bd3117ca3819559de Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Mon, 17 Nov 2025 15:36:05 +0100 Subject: [PATCH 2/4] clippy --- crates/ev-revm/src/factory.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ev-revm/src/factory.rs b/crates/ev-revm/src/factory.rs index 8ac5184..17a27c8 100644 --- a/crates/ev-revm/src/factory.rs +++ b/crates/ev-revm/src/factory.rs @@ -488,7 +488,7 @@ mod tests { .expect("pre-activation call executes"); let state: EvmState = result_before.state; assert!( - state.get(&mintee).is_none(), + !state.contains_key(&mintee), "precompile must not mint before activation height" ); From 971a9beafb156293c861c8e51fca437d97c70330 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 18 Nov 2025 13:01:50 +0100 Subject: [PATCH 3/4] fix: reorder mint_admin field in ChainspecEvolveConfig and EvolvePayloadBuilderConfig for consistency --- crates/node/src/config.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index 520405f..d3a6737 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -6,10 +6,10 @@ use serde::{Deserialize, Serialize}; struct ChainspecEvolveConfig { #[serde(default, rename = "baseFeeSink")] pub base_fee_sink: Option
, - #[serde(default, rename = "mintAdmin")] - pub mint_admin: Option
, #[serde(default, rename = "baseFeeRedirectActivationHeight")] pub base_fee_redirect_activation_height: Option, + #[serde(default, rename = "mintAdmin")] + pub mint_admin: Option
, #[serde(default, rename = "mintPrecompileActivationHeight")] pub mint_precompile_activation_height: Option, } @@ -20,12 +20,12 @@ pub struct EvolvePayloadBuilderConfig { /// Optional chainspec-configured recipient for redirected base fees. #[serde(default)] pub base_fee_sink: Option
, - /// Optional mint precompile admin address sourced from the chainspec. - #[serde(default)] - pub mint_admin: Option
, /// Optional activation height for base-fee redirect; defaults to 0 when sink set. #[serde(default)] pub base_fee_redirect_activation_height: Option, + /// Optional mint precompile admin address sourced from the chainspec. + #[serde(default)] + pub mint_admin: Option
, /// Optional activation height for mint precompile; defaults to 0 when admin set. #[serde(default)] pub mint_precompile_activation_height: Option, From f29341da2c12923359d5600037954d0f521546d7 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Tue, 18 Nov 2025 14:34:20 +0100 Subject: [PATCH 4/4] feat: add mint precompile admin and activation height to genesis config --- etc/ev-reth-genesis.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/etc/ev-reth-genesis.json b/etc/ev-reth-genesis.json index b351cf3..3522fbe 100644 --- a/etc/ev-reth-genesis.json +++ b/etc/ev-reth-genesis.json @@ -18,7 +18,9 @@ "terminalTotalDifficultyPassed": true, "evolve": { "baseFeeSink": "0x00000000000000000000000000000000000000fe", - "baseFeeRedirectActivationHeight": 0 + "baseFeeRedirectActivationHeight": 0, + "mintPrecompileAdmin": "0x0000000000000000000000000000000000000000", + "mintPrecompileActivationHeight": 0 } }, "difficulty": "0x1",