Skip to content
Open
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ alloy-evm = { version = "0.39.0", path = "crates/evm", default-features = false
# alloy
alloy-eip2124 = { version = "0.2", default-features = false }
alloy-chains = { version = "0.2.0", default-features = false }
alloy-eips = { git = "https://github.com/Rimeeeeee/alloy", branch = "feat/eip8141-transaction", default-features = false }
alloy-consensus = { git = "https://github.com/Rimeeeeee/alloy", branch = "feat/eip8141-transaction", default-features = false }
alloy-eips = { git = "https://github.com/Rimeeeeee/alloy", rev = "7ed87e72a06060a5b43ba6d5dd700b73c29b537a", default-features = false }
alloy-consensus = { git = "https://github.com/Rimeeeeee/alloy", rev = "7ed87e72a06060a5b43ba6d5dd700b73c29b537a", default-features = false }
alloy-primitives = { version = "1.0.0", default-features = false }
alloy-sol-types = { version = "1.0.0", default-features = false }
alloy-hardforks = "0.4.8"
alloy-rpc-types-eth = { git = "https://github.com/Rimeeeeee/alloy", branch = "feat/eip8141-transaction", default-features = false }
alloy-rpc-types-engine = { git = "https://github.com/Rimeeeeee/alloy", branch = "feat/eip8141-transaction", default-features = false }
alloy-rpc-types-eth = { git = "https://github.com/Rimeeeeee/alloy", rev = "7ed87e72a06060a5b43ba6d5dd700b73c29b537a", default-features = false }
alloy-rpc-types-engine = { git = "https://github.com/Rimeeeeee/alloy", rev = "7ed87e72a06060a5b43ba6d5dd700b73c29b537a", default-features = false }

# revm
revm = { git = "https://github.com/Soubhik-10/revm", branch = "feat/eip8141-frame-transactions", default-features = false }
revm = { git = "https://github.com/Rimeeeeee/revm", rev = "096a2b9322a1894aa5c3a35c49e6e518dbbf7e66", default-features = false }

# tracing
tracing = { version = "0.1", default-features = false }
Expand Down
30 changes: 29 additions & 1 deletion crates/evm/src/block/system_calls/eip8141.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use crate::{
block::{BlockExecutionError, StateDB},
Evm,
};
use alloy_eips::eip8141::{EXPIRY_VERIFIER, EXPIRY_VERIFIER_RUNTIME};
use alloy_eips::eip8141::{
EXPIRY_VERIFIER, EXPIRY_VERIFIER_RUNTIME, NONCE_MANAGER, NONCE_MANAGER_CODE,
};
use alloy_primitives::{keccak256, Bytes};
use revm::{
state::{Account, Bytecode, TransactionId},
Expand Down Expand Up @@ -35,3 +37,29 @@ pub(crate) fn install_expiry_verifier(
db.commit([(EXPIRY_VERIFIER, account)].into_iter().collect());
Ok(())
}

/// Installs the EIP-8250 protocol nonce manager without changing its balance.
///
/// Fork configuration must ensure this address has no code or storage before activation. The
/// canonical runtime cannot delete itself, so an already initialized account is left untouched
/// on later blocks, preserving consumed nonce slots.
pub(crate) fn install_nonce_manager(
evm: &mut impl Evm<DB: StateDB>,
) -> Result<(), BlockExecutionError> {
let db = evm.db_mut();
let runtime_hash = keccak256(NONCE_MANAGER_CODE);
let current = db.basic(NONCE_MANAGER).map_err(BlockExecutionError::other)?;
if current.as_ref().is_some_and(|account| account.code_hash == runtime_hash) {
return Ok(());
}

let mut account = current
.map(Account::from)
.unwrap_or_else(|| Account::new_not_existing(TransactionId::ZERO));
account.info.nonce = account.info.nonce.max(1);
account.info.code_hash = runtime_hash;
account.info.code = Some(Bytecode::new_legacy(Bytes::copy_from_slice(&NONCE_MANAGER_CODE)));
account.mark_touch();
db.commit([(NONCE_MANAGER, account)].into_iter().collect());
Ok(())
}
110 changes: 110 additions & 0 deletions crates/evm/src/block/system_calls/eip8250.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! EIP-8250 nonce manager activation.

use crate::{
block::{BlockExecutionError, StateDB},
Evm,
};
use alloy_eips::eip8141::{NONCE_MANAGER, NONCE_MANAGER_CODE};
use alloy_primitives::{keccak256, Bytes, KECCAK256_EMPTY};
use revm::{
state::{Account, Bytecode, TransactionId},
Database, DatabaseCommit,
};

/// Installs the nonce manager without replacing existing code or resetting its state.
pub(crate) fn install_nonce_manager(
evm: &mut impl Evm<DB: StateDB>,
) -> Result<(), BlockExecutionError> {
let db = evm.db_mut();
let code_hash = keccak256(NONCE_MANAGER_CODE);
let current = db.basic(NONCE_MANAGER).map_err(BlockExecutionError::other)?;
// The reverting runtime cannot be replaced or destroyed; its presence marks completed
// activation.
if current.as_ref().is_some_and(|account| account.code_hash == code_hash) {
return Ok(());
}
if current.as_ref().is_some_and(|account| account.code_hash != KECCAK256_EMPTY) {
return Err(BlockExecutionError::msg("EIP-8250 nonce manager address contains code"));
}
let mut account = current
.map(Account::from)
.unwrap_or_else(|| Account::new_not_existing(TransactionId::ZERO));
account.info.nonce = account.info.nonce.max(1);
account.info.code_hash = code_hash;
account.info.code = Some(Bytecode::new_legacy(Bytes::copy_from_slice(&NONCE_MANAGER_CODE)));
account.mark_touch();
db.commit([(NONCE_MANAGER, account)].into_iter().collect());
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{eth::EthEvmFactory, EvmEnv, EvmFactory};
use alloy_primitives::U256;
use revm::{
database::{CacheDB, EmptyDB},
state::AccountInfo,
};

#[test]
fn activation_follows_eip8141() {
use alloy_hardforks::{EthereumHardfork, EthereumHardforks, ForkCondition};
struct Spec;
impl EthereumHardforks for Spec {
fn ethereum_fork_activation(&self, _: EthereumHardfork) -> ForkCondition {
ForkCondition::Timestamp(10)
}
}
let mut caller = super::super::SystemCaller::new(Spec);
let mut db = CacheDB::<EmptyDB>::default();
for timestamp in [9, 10, 11] {
let mut evm = EthEvmFactory
.create_evm(db, EvmEnv::default().with_timestamp(U256::from(timestamp)));
caller.apply_eip8141_fork_transition(&mut evm).unwrap();
let account = evm.db_mut().basic(NONCE_MANAGER).unwrap();
assert_eq!(account.is_some(), timestamp >= 10);
let verifier = evm.db_mut().basic(alloy_eips::eip8141::EXPIRY_VERIFIER).unwrap();
assert_eq!(verifier.is_some(), timestamp >= 10);
db = evm.into_db();
}
}

#[test]
fn activation_preserves_balance_nonce_and_subsequent_storage() {
for existing_nonce in [0, 7] {
let mut db = CacheDB::<EmptyDB>::default();
db.insert_account_info(
NONCE_MANAGER,
AccountInfo {
nonce: existing_nonce,
balance: U256::from(19),
..Default::default()
},
);
let mut evm = EthEvmFactory.create_evm(db, EvmEnv::default());
install_nonce_manager(&mut evm).unwrap();
let info = evm.db_mut().basic(NONCE_MANAGER).unwrap().unwrap();
assert_eq!(info.nonce, existing_nonce.max(1));
assert_eq!(info.balance, U256::from(19));
assert_eq!(info.code_hash, keccak256(NONCE_MANAGER_CODE));
evm.db_mut()
.insert_account_storage(NONCE_MANAGER, U256::from(42), U256::from(3))
.unwrap();
install_nonce_manager(&mut evm).unwrap();
assert_eq!(evm.db_mut().basic(NONCE_MANAGER).unwrap().unwrap(), info);
assert_eq!(evm.db_mut().storage(NONCE_MANAGER, U256::from(42)).unwrap(), U256::from(3));
}
}

#[test]
fn activation_rejects_existing_code() {
let mut db = CacheDB::<EmptyDB>::default();
let info =
AccountInfo::default().with_code(Bytecode::new_legacy(Bytes::from_static(&[0x00])));
db.insert_account_info(NONCE_MANAGER, info.clone());
let mut evm = EthEvmFactory.create_evm(db, EvmEnv::default());
assert!(install_nonce_manager(&mut evm).is_err());
assert_eq!(evm.db_mut().basic(NONCE_MANAGER).unwrap().unwrap(), info);
}
}
4 changes: 3 additions & 1 deletion crates/evm/src/block/system_calls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod eip4788;
mod eip7002;
mod eip7251;
mod eip8141;
mod eip8250;
mod eip8282;

pub use eip8282::{
Expand Down Expand Up @@ -56,13 +57,14 @@ where
/// Applies the EIP-8141 fork-state transition.
///
/// Unlike the other operations in this module, this does not execute a system call. The
/// hardfork installs the verifier runtime directly into state.
/// hardfork installs the verifier and nonce-manager runtimes directly into state.
pub fn apply_eip8141_fork_transition(
&mut self,
evm: &mut impl Evm<DB: DatabaseCommit>,
) -> Result<(), BlockExecutionError> {
if self.spec.is_bogota_active_at_timestamp(evm.block().timestamp().saturating_to()) {
eip8141::install_expiry_verifier(evm)?;
eip8250::install_nonce_manager(evm)?;
}

Ok(())
Expand Down
21 changes: 21 additions & 0 deletions crates/evm/src/eth/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ impl EvmEnv<SpecId> {
let spec =
crate::spec_by_timestamp_and_block_number(&chain_spec, input.timestamp, input.number);
let mut cfg_env = CfgEnv::new_with_spec(spec).with_chain_id(chain_id);
cfg_env.enable_eip8250 = chain_spec.is_bogota_active_at_timestamp(input.timestamp);

if let Some(blob_params) = &blob_params {
cfg_env.set_max_blobs_per_tx(blob_params.max_blobs_per_tx);
Expand Down Expand Up @@ -218,6 +219,26 @@ mod tests {
use alloy_hardforks::ethereum::MAINNET_PARIS_BLOCK;
use alloy_primitives::B256;

#[test]
fn keyed_nonce_activation_follows_frames() {
use alloy_hardforks::{EthereumHardfork, ForkCondition};
struct Spec;
impl EthereumHardforks for Spec {
fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
match fork {
EthereumHardfork::Bogota => ForkCondition::Timestamp(10),
_ => ForkCondition::Never,
}
}
}
for timestamp in [9, 10, 11] {
let header = Header { timestamp, ..Default::default() };
let env = EvmEnv::for_eth_block(header, Spec, 1, None);
assert_eq!(env.cfg_env.enable_eip8250, timestamp >= 10);
assert_eq!(env.cfg_env.spec == SpecId::BOGOTA, timestamp >= 10);
}
}

#[test_case::test_case(
Header::default(),
EvmEnv {
Expand Down
3 changes: 2 additions & 1 deletion crates/evm/src/eth/spec_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub fn spec_by_timestamp_and_block_number<C>(
where
C: EthereumHardforks,
{
// The frames devnet activates the EIP-8141 envelope and EIP-8250 keyed nonce rules together.
if chain_spec.is_bogota_active_at_timestamp(timestamp) {
SpecId::BOGOTA
} else if chain_spec.is_amsterdam_active_at_timestamp(timestamp) {
Expand Down Expand Up @@ -156,7 +157,7 @@ mod tests {

#[test_case::test_case(FakeHardfork::osaka(), SpecId::OSAKA; "Osaka")]
#[test_case::test_case(FakeHardfork::amsterdam(), SpecId::AMSTERDAM; "Amsterdam")]
#[test_case::test_case(FakeHardfork::bogota(), SpecId::BOGOTA; "Bogota with EIP-8141")]
#[test_case::test_case(FakeHardfork::bogota(), SpecId::BOGOTA; "Bogota with EIP-8141 and EIP-8250")]
#[test_case::test_case(FakeHardfork::prague(), SpecId::PRAGUE; "Prague")]
#[test_case::test_case(FakeHardfork::cancun(), SpecId::CANCUN; "Cancun")]
#[test_case::test_case(FakeHardfork::shanghai(), SpecId::SHANGHAI; "Shanghai")]
Expand Down
2 changes: 2 additions & 0 deletions crates/evm/src/rpc/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl<Spec, Block: BlockEnvironment> TryIntoTxEnv<TxEnv, Spec, Block> for Transac
value,
input,
nonce,
nonce_keys,
access_list,
chain_id,
blob_versioned_hashes,
Expand Down Expand Up @@ -150,6 +151,7 @@ impl<Spec, Block: BlockEnvironment> TryIntoTxEnv<TxEnv, Spec, Block> for Transac
let tx = TxEip8141 {
chain_id,
nonce,
nonce_keys,
sender: caller,
frames: frames
.unwrap_or_default()
Expand Down
9 changes: 9 additions & 0 deletions crates/evm/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,12 @@ pub fn tx_env_from_eip8141(tx: TxEip8141, gas_params: &GasParams) -> TxEnv {
// EIP-8141 has no outer ECDSA signature. Its consensus sender is an explicit field, so the
// synthetic signer carried by `Recovered` must not be allowed to override it.
let frame_transaction = FrameTransaction {
nonce_calldata: tx
.nonce_keys
.as_deref()
.map(|keys| alloy_eips::eip8141::nonce_calldata(keys, tx.nonce))
.unwrap_or_default(),
nonce_keys: tx.nonce_keys,
frames: tx.frames,
signatures: tx.signatures,
signature_hash,
Expand Down Expand Up @@ -742,6 +748,7 @@ mod tests {
let tx = TxEip8141 {
chain_id: 1,
nonce: 7,
nonce_keys: Some(vec![U256::from(1)]),
sender,
frames: vec![Frame {
mode: FrameMode::Sender,
Expand Down Expand Up @@ -770,6 +777,8 @@ mod tests {
assert_eq!(env.caller, sender);
assert_eq!(env.kind, TxKind::Call(sender));
assert_eq!(env.nonce, tx.nonce);
assert_eq!(frame_transaction.nonce_keys, tx.nonce_keys);
assert_eq!(frame_transaction.nonce_calldata, vec![0xc1, 0x01, 0x07]);
assert_eq!(env.chain_id, Some(tx.chain_id));
assert_eq!(env.gas_price, tx.fees.max_fee_per_gas.saturating_to());
assert_eq!(env.gas_priority_fee, Some(tx.fees.max_priority_fee_per_gas.saturating_to()));
Expand Down
Loading