diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 554f4b9d..0cdd0167 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -84,12 +84,21 @@ jobs: submodules: recursive fetch-depth: 1 + - name: Download Ethereum spec tests fixtures + run: | + wget https://github.com/ethereum/execution-spec-tests/releases/download/pectra-devnet-3%40v1.5.0/fixtures_pectra-devnet-3.tar.gz + mkdir ethereum-spec-tests + tar -xzf fixtures_pectra-devnet-3.tar.gz -C ethereum-spec-tests + tree ethereum-spec-tests/fixtures/state_tests/prague/eip7702_set_code_tx + - name: Run Ethereum state tests run: | cargo run -r -p evm-jsontests -F enable-slow-tests -- state -f \ ethtests/GeneralStateTests/ \ ethtests/LegacyTests/Cancun/GeneralStateTests/ \ - ethtests/EIPTests/StateTests/ + ethereum-spec-tests/fixtures/state_tests/prague/eip7702_set_code_tx/ + # Temporally disable as EOFv1 not implemented + # ethtests/EIPTests/StateTests/ - name: Run Ethereum vm tests run: | @@ -135,8 +144,9 @@ jobs: export PATH="$PATH:$HOME/.cargo/bin" cargo run -r -p evm-jsontests -F enable-slow-tests -- state -f \ ethtests/GeneralStateTests/ \ - ethtests/LegacyTests/Cancun/GeneralStateTests/ \ - ethtests/EIPTests/StateTests/ + ethtests/LegacyTests/Cancun/GeneralStateTests/ + # Temporally disable as EOFv1 not implemented + # ethtests/EIPTests/StateTests/ cargo run -r -p evm-jsontests -F enable-slow-tests -- vm -f \ ethtests/LegacyTests/Constantinople/VMTests/vmArithmeticTest \ ethtests/LegacyTests/Constantinople/VMTests/vmBitwiseLogicOperation \ diff --git a/benches/loop.rs b/benches/loop.rs index d586fd14..4171474c 100644 --- a/benches/loop.rs +++ b/benches/loop.rs @@ -60,6 +60,7 @@ fn run_loop_contract() { // hex::decode("0f14a4060000000000000000000000000000000000000000000000000000000000002ee0").unwrap(), u64::MAX, Vec::new(), + Vec::new(), ); } diff --git a/core/src/memory.rs b/core/src/memory.rs index 54516888..3c6e5e60 100644 --- a/core/src/memory.rs +++ b/core/src/memory.rs @@ -5,12 +5,15 @@ use core::cmp::min; use core::ops::{BitAnd, Not}; use primitive_types::{H256, U256}; -/// A sequencial memory. It uses Rust's `Vec` for internal +/// A sequential memory. It uses Rust's `Vec` for internal /// representation. #[derive(Clone, Debug)] pub struct Memory { + /// Memory data data: Vec, + /// Memory effective length, that changed after resize operations. effective_len: usize, + /// Memory limit limit: usize, } @@ -59,7 +62,7 @@ impl Memory { /// with 32 bytes as the step. If the length is zero, this function does nothing. /// /// # Errors - /// Return `ExitError` + /// Return `ExitError::InvalidRange` if `offset + len` is overflow. pub fn resize_offset(&mut self, offset: usize, len: usize) -> Result<(), ExitError> { if len == 0 { return Ok(()); @@ -73,7 +76,7 @@ impl Memory { /// Resize the memory, making it cover to `end`, with 32 bytes as the step. /// /// # Errors - /// Return `ExitError` + /// Return `ExitError::InvalidRange` if `end` value is overflow in `next_multiple_of_32` call. pub fn resize_end(&mut self, end: usize) -> Result<(), ExitError> { if end > self.effective_len { let new_end = next_multiple_of_32(end).ok_or(ExitError::InvalidRange)?; @@ -105,7 +108,7 @@ impl Memory { ret } - /// Get `H256` from a specific offset in memory. + /// Get `H256` value from a specific offset in memory. #[must_use] pub fn get_h256(&self, offset: usize) -> H256 { let mut ret = [0; 32]; @@ -127,7 +130,7 @@ impl Memory { /// untrusted. /// /// # Errors - /// Return `ExitFatal` + /// Return `ExitFatal::NotSupported` if `offset + target_size` is out of memory limit or overflow. pub fn set( &mut self, offset: usize, @@ -166,7 +169,9 @@ impl Memory { /// `copy_within` uses `memmove` to avoid `DoS` attacks. /// /// # Errors - /// Return `ExitFatal` + /// Return `ExitFatal::Other`: + /// - `OverflowOnCopy` if `offset + length` is overflow + /// - `OutOfGasOnCopy` if `offst_length` out of memory limit pub fn copy( &mut self, src_offset: usize, @@ -200,7 +205,7 @@ impl Memory { /// Copy `data` into the memory, of given `len`. /// /// # Errors - /// Return `ExitFatal` + /// Return `ExitFatal::NotSupported` if `set()` call return out of memory limit. pub fn copy_large( &mut self, memory_offset: usize, diff --git a/core/src/stack.rs b/core/src/stack.rs index fc73f84d..04e88396 100644 --- a/core/src/stack.rs +++ b/core/src/stack.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::prelude::Vec; use crate::utils::USIZE_MAX; use crate::ExitError; use primitive_types::{H256, U256}; diff --git a/evm-tests/ethjson/src/spec/spec.rs b/evm-tests/ethjson/src/spec/spec.rs index 92666934..1a823416 100644 --- a/evm-tests/ethjson/src/spec/spec.rs +++ b/evm-tests/ethjson/src/spec/spec.rs @@ -64,9 +64,9 @@ pub enum ForkSpec { Paris, /// Shanghai (#17,034,870, 2023-04-12) Shanghai, - /// Cancun (2024-03-13) + /// Cancun (28,750,000, 2024-03-13) Cancun, - /// Prague-Electra—aka Pectra + /// Prague (future) Prague, } @@ -118,6 +118,7 @@ impl TryFrom for ForkSpec { "paris" => Self::Paris, "shanghai" => Self::Shanghai, "cancun" => Self::Cancun, + "prague" => Self::Prague, other => return Err(format!("Unknown hard fork spec {other}")), }; Ok(res) diff --git a/evm-tests/ethjson/src/test_helpers/state.rs b/evm-tests/ethjson/src/test_helpers/state.rs index ec7034d5..97c9cfc1 100644 --- a/evm-tests/ethjson/src/test_helpers/state.rs +++ b/evm-tests/ethjson/src/test_helpers/state.rs @@ -79,6 +79,10 @@ pub struct MultiTransaction { pub blob_versioned_hashes: Vec, /// EIP-4844 pub max_fee_per_blob_gas: Option, + + /// EIP-7702 + #[serde(default)] + pub authorization_list: AuthorizationList, } impl MultiTransaction { @@ -109,6 +113,7 @@ impl MultiTransaction { v: Default::default(), secret: self.secret, access_list, + authorization_list: self.authorization_list.clone(), } } } @@ -127,6 +132,28 @@ pub struct AccessListTuple { pub storage_keys: Vec, } +/// EIP-7702 Authorization List +pub type AuthorizationList = Vec; +/// EIP-7702 Authorization item +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizationItem { + /// Chain ID + pub chain_id: Uint, + /// Address to access + pub address: Address, + /// Keys (slots) to access at that address + pub nonce: Uint, + /// r signature + pub r: Uint, + /// s signature + pub s: Uint, + /// Parity + pub v: Uint, + /// Signer address + pub signer: Option
, +} + /// State test indexes deserialization. #[derive(Debug, PartialEq, Eq, Deserialize)] pub struct PostStateIndexes { diff --git a/evm-tests/ethjson/src/transaction.rs b/evm-tests/ethjson/src/transaction.rs index 47e5508b..0c0ef2aa 100644 --- a/evm-tests/ethjson/src/transaction.rs +++ b/evm-tests/ethjson/src/transaction.rs @@ -16,6 +16,7 @@ //! Transaction deserialization. +use crate::test_helpers::state::AuthorizationList; use crate::{ bytes::Bytes, hash::{Address, H256}, @@ -33,6 +34,9 @@ pub struct Transaction { /// Transaction access list (see EIP-2930). #[serde(default)] pub access_list: Vec<(Address, Vec)>, + /// Transaction authorization list (see EIP-7702`s). + #[serde(default)] + pub authorization_list: AuthorizationList, /// Gas limit. pub gas_limit: Uint, /// To. diff --git a/evm-tests/jsontests/Cargo.toml b/evm-tests/jsontests/Cargo.toml index 77327993..c60c36e5 100644 --- a/evm-tests/jsontests/Cargo.toml +++ b/evm-tests/jsontests/Cargo.toml @@ -23,7 +23,7 @@ rlp = "0.5" sha3 = "0.10" parity-bytes = "0.1" env_logger = "0.11" -lazy_static = "1.4.0" +hex-literal = "0.4" [features] enable-slow-tests = [] diff --git a/evm-tests/jsontests/src/main.rs b/evm-tests/jsontests/src/main.rs index 92f9e900..9aed9c30 100644 --- a/evm-tests/jsontests/src/main.rs +++ b/evm-tests/jsontests/src/main.rs @@ -235,7 +235,6 @@ fn run_test_for_file( ); } let file = File::open(file_name).expect("Open file failed"); - let reader = BufReader::new(file); let test_suite = serde_json::from_reader::<_, HashMap>(reader) .expect("Parse test cases failed"); diff --git a/evm-tests/jsontests/src/state.rs b/evm-tests/jsontests/src/state.rs index 7fa8c436..b4b86050 100644 --- a/evm-tests/jsontests/src/state.rs +++ b/evm-tests/jsontests/src/state.rs @@ -7,18 +7,18 @@ use ethjson::test_helpers::state::PostStateResult; use ethjson::uint::Uint; use evm::backend::{ApplyBackend, MemoryAccount, MemoryBackend, MemoryVicinity}; use evm::executor::stack::{ - MemoryStackState, PrecompileFailure, PrecompileFn, PrecompileOutput, StackExecutor, - StackSubstateMetadata, + Authorization, MemoryStackState, PrecompileFailure, PrecompileFn, PrecompileOutput, + StackExecutor, StackSubstateMetadata, }; use evm::utils::U64_MAX; use evm::{Config, Context, ExitError, ExitReason, ExitSucceed}; -use lazy_static::lazy_static; use libsecp256k1::SecretKey; use primitive_types::{H160, H256, U256}; use serde::Deserialize; use sha3::{Digest, Keccak256}; use std::collections::BTreeMap; use std::str::FromStr; +use std::sync::LazyLock; #[derive(Default, Debug, Clone)] pub struct VerboseOutput { @@ -152,17 +152,11 @@ impl Test { } } -lazy_static! { - static ref ISTANBUL_BUILTINS: BTreeMap = istanbul_builtins(); -} - -lazy_static! { - static ref BERLIN_BUILTINS: BTreeMap = berlin_builtins(); -} - -lazy_static! { - static ref CANCUN_BUILTINS: BTreeMap = cancun_builtins(); -} +type LazyPrecompiles = LazyLock>; +static ISTANBUL_BUILTINS: LazyPrecompiles = LazyLock::new(istanbul_builtins); +static BERLIN_BUILTINS: LazyPrecompiles = LazyLock::new(berlin_builtins); +static CANCUN_BUILTINS: LazyPrecompiles = LazyLock::new(cancun_builtins); +static PRAGUE_BUILTINS: LazyPrecompiles = LazyLock::new(prague_builtins); macro_rules! precompile_entry { ($map:expr, $builtins:expr, $index:expr) => { @@ -228,6 +222,29 @@ impl JsonPrecompile { precompile_entry!(map, CANCUN_BUILTINS, 0xA); Some(map) } + ForkSpec::Prague => { + let mut map = BTreeMap::new(); + precompile_entry!(map, PRAGUE_BUILTINS, 1); + precompile_entry!(map, PRAGUE_BUILTINS, 2); + precompile_entry!(map, PRAGUE_BUILTINS, 3); + precompile_entry!(map, PRAGUE_BUILTINS, 4); + precompile_entry!(map, PRAGUE_BUILTINS, 5); + precompile_entry!(map, PRAGUE_BUILTINS, 6); + precompile_entry!(map, PRAGUE_BUILTINS, 7); + precompile_entry!(map, PRAGUE_BUILTINS, 8); + precompile_entry!(map, PRAGUE_BUILTINS, 9); + precompile_entry!(map, PRAGUE_BUILTINS, 0x0A); + precompile_entry!(map, PRAGUE_BUILTINS, 0x0B); + precompile_entry!(map, PRAGUE_BUILTINS, 0x0C); + precompile_entry!(map, PRAGUE_BUILTINS, 0x0D); + precompile_entry!(map, PRAGUE_BUILTINS, 0x0E); + precompile_entry!(map, PRAGUE_BUILTINS, 0x0F); + precompile_entry!(map, PRAGUE_BUILTINS, 0x10); + precompile_entry!(map, PRAGUE_BUILTINS, 0x11); + precompile_entry!(map, PRAGUE_BUILTINS, 0x12); + precompile_entry!(map, PRAGUE_BUILTINS, 0x13); + Some(map) + } _ => None, } } @@ -508,136 +525,148 @@ fn berlin_builtins() -> BTreeMap { } fn cancun_builtins() -> BTreeMap { - use ethjson::spec::builtin::{BuiltinCompat, Linear, Modexp, PricingCompat}; + use ethjson::spec::builtin::{BuiltinCompat, Linear, PricingCompat}; - let builtins: BTreeMap = BTreeMap::from([ - ( - Address(H160::from_low_u64_be(1)), - BuiltinCompat { - name: "ecrecover".to_string(), - pricing: PricingCompat::Single(Pricing::Linear(Linear { - base: 3000, - word: 0, - })), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(2)), - BuiltinCompat { - name: "sha256".to_string(), - pricing: PricingCompat::Single(Pricing::Linear(Linear { base: 60, word: 12 })), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(3)), - BuiltinCompat { - name: "ripemd160".to_string(), - pricing: PricingCompat::Single(Pricing::Linear(Linear { - base: 600, - word: 120, - })), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(4)), - BuiltinCompat { - name: "identity".to_string(), - pricing: PricingCompat::Single(Pricing::Linear(Linear { base: 15, word: 3 })), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(5)), - BuiltinCompat { - name: "modexp".to_string(), - pricing: PricingCompat::Single(Pricing::Modexp(Modexp { - divisor: 3, - is_eip_2565: true, - })), - activate_at: Some(Uint(U256::zero())), - }, - ), - ( - Address(H160::from_low_u64_be(6)), - BuiltinCompat { - name: "alt_bn128_add".to_string(), - pricing: PricingCompat::Multi(BTreeMap::from([( - Uint(U256::zero()), - PricingAt { - info: Some("EIP 1108 transition".to_string()), - price: Pricing::AltBn128ConstOperations(AltBn128ConstOperations { - price: 150, - }), - }, - )])), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(7)), - BuiltinCompat { - name: "alt_bn128_mul".to_string(), - pricing: PricingCompat::Multi(BTreeMap::from([( - Uint(U256::zero()), - PricingAt { - info: Some("EIP 1108 transition".to_string()), - price: Pricing::AltBn128ConstOperations(AltBn128ConstOperations { - price: 6000, - }), - }, - )])), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(8)), - BuiltinCompat { - name: "alt_bn128_pairing".to_string(), - pricing: PricingCompat::Multi(BTreeMap::from([( - Uint(U256::zero()), - PricingAt { - info: Some("EIP 1108 transition".to_string()), - price: Pricing::AltBn128Pairing(AltBn128Pairing { - base: 45000, - pair: 34000, - }), - }, - )])), - activate_at: None, - }, - ), - ( - Address(H160::from_low_u64_be(9)), - BuiltinCompat { - name: "blake2_f".to_string(), - pricing: PricingCompat::Single(Pricing::Blake2F { gas_per_round: 1 }), - activate_at: Some(Uint(U256::zero())), - }, - ), - ( - Address(H160::from_low_u64_be(0xA)), - BuiltinCompat { - name: "kzg".to_string(), - pricing: PricingCompat::Single(Pricing::Linear(Linear { - base: 50_000, - word: 0, - })), - activate_at: None, - }, - ), - ]); + let mut builtins = berlin_builtins(); + builtins.insert( + Address(H160::from_low_u64_be(0xA)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "kzg".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); builtins - .into_iter() - .map(|(address, builtin)| { - ( - address.into(), - ethjson::spec::Builtin::from(builtin).try_into().unwrap(), - ) +} + +fn prague_builtins() -> BTreeMap { + use ethjson::spec::builtin::{BuiltinCompat, Linear, PricingCompat}; + + let mut builtins = berlin_builtins(); + builtins.insert( + Address(H160::from_low_u64_be(0xB)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_g1_add".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, }) - .collect() + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0xC)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_g1_mul".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0xD)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_g1_multiexp".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0xE)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_g2_add".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0xF)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_g2_mul".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0x10)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_g2_multiexp".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0x11)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_pairing".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0x12)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_fp_to_g1".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + builtins.insert( + Address(H160::from_low_u64_be(0x13)).into(), + ethjson::spec::Builtin::from(BuiltinCompat { + name: "bls12_381_fp2_to_g2".to_string(), + pricing: PricingCompat::Single(Pricing::Linear(Linear { + base: 50_000, + word: 0, + })), + activate_at: None, + }) + .try_into() + .unwrap(), + ); + + builtins } pub fn test( @@ -709,6 +738,14 @@ fn check_create_exit_reason( ); return true; } + ExitError::OutOfGas => { + let check_result = + exception == "TransactionException.INTRINSIC_GAS_TOO_LOW"; + assert!(check_result, + "unexpected exception {exception:?} for OutOfGas error for test: {name}" + ); + return true; + } _ => { panic!("unexpected error: {err:?} for exception: {exception}") } @@ -875,6 +912,23 @@ fn assert_vicinity_validation( } _ => panic!("Unexpected validation reason: {reason:?} [{spec:?}] {name}"), }, + ForkSpec::Prague => match reason { + InvalidTxReason::GasPriceLessThenBlockBaseFee => { + for (i, state) in states.iter().enumerate() { + let expected = state + .expect_exception + .as_deref() + .expect("expected error message for test: {reason:?} [{spec}] {name}:{i}"); + let is_checked = expected == "TR_FeeCapLessThanBlocks" + || expected == "TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS"; + assert!( + is_checked, + "unexpected error message {expected:?} for: {reason:?} [{spec:?}] {name}:{i}", + ); + } + } + _ => panic!("Unexpected validation reason: {reason:?} [{spec:?}] {name}"), + }, _ => panic!("Unexpected validation reason: {reason:?} [{spec:?}] {name}"), } } @@ -979,6 +1033,27 @@ fn check_validate_exit_reason( check_result, "unexpected exception {exception:?} for BlobVersionedHashesNotSupported for test: [{spec:?}] {name}" ); + }, + InvalidTxReason::InvalidAuthorizationSignature => { + let check_result = exception == "TransactionException.TYPE_4_INVALID_AUTHORITY_SIGNATURE"; + assert!( + check_result, + "unexpected exception {exception:?} for InvalidAuthorizationSignature for test: [{spec:?}] {name}" + ); + } + InvalidTxReason::AuthorizationListNotExist => { + let check_result = exception == "TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST"; + assert!( + check_result, + "unexpected exception {exception:?} for AuthorizationListNotExist for test: [{spec:?}] {name}" + ); + } + InvalidTxReason::CreateTransaction => { + let check_result = exception == "TransactionException.TYPE_4_TX_CONTRACT_CREATION"; + assert!( + check_result, + "unexpected exception {exception:?} for CreateTransaction for test: [{spec:?}] {name}" + ); } _ => { panic!( @@ -1007,6 +1082,7 @@ fn test_run( continue; } } + let (gasometer_config, delete_empty) = match spec { ForkSpec::Istanbul => (Config::istanbul(), true), ForkSpec::Berlin => (Config::berlin(), true), @@ -1015,6 +1091,7 @@ fn test_run( ForkSpec::Paris => (Config::merge(), true), ForkSpec::Shanghai => (Config::shanghai(), true), ForkSpec::Cancun => (Config::cancun(), true), + ForkSpec::Prague => (Config::prague(), true), _ => { continue; } @@ -1094,6 +1171,11 @@ fn test_run( let caller_code = original_state .get(&caller) .map_or_else(Vec::new, |acc| acc.code.clone()); + // EIP-7702 - check if it's delegated designation. If it's delegation designation then + // even if `caller_code` is non-empty transaction should be executed. + let is_delegated = original_state + .get(&caller) + .map_or(false, |c| Authorization::is_delegated(&c.code)); for (i, state) in states.iter().enumerate() { let transaction = test_tx.select(&state.indexes); @@ -1121,7 +1203,6 @@ fn test_run( let gas_limit: u64 = transaction.gas_limit.into(); let data: Vec = transaction.data.clone().into(); - let valid_tx = crate::utils::transaction::validate( &transaction, test.0.env.gas_limit.0, @@ -1132,12 +1213,15 @@ fn test_run( blob_gas_price, data_max_fee, spec, + state, ); + // Only execute valid transactions if let Err(err) = &valid_tx { if check_validate_exit_reason(err, &state.expect_exception, name, spec) { continue; } } + let authorization_list = valid_tx.unwrap(); // We do not check overflow after TX validation let total_fee = if let Some(data_fee) = data_fee { @@ -1146,123 +1230,118 @@ fn test_run( vicinity.effective_gas_price * gas_limit }; - // Only execute valid transactions - if valid_tx.is_ok() { - let metadata = - StackSubstateMetadata::new(transaction.gas_limit.into(), &gasometer_config); - let executor_state = MemoryStackState::new(metadata, &backend); - let precompile = JsonPrecompile::precompile(spec).unwrap(); - let mut executor = StackExecutor::new_with_precompiles( - executor_state, - &gasometer_config, - &precompile, - ); - executor.state_mut().withdraw(caller, total_fee).unwrap(); + let metadata = + StackSubstateMetadata::new(transaction.gas_limit.into(), &gasometer_config); + let executor_state = MemoryStackState::new(metadata, &backend); + let precompile = JsonPrecompile::precompile(spec).unwrap(); + let mut executor = + StackExecutor::new_with_precompiles(executor_state, &gasometer_config, &precompile); + executor.state_mut().withdraw(caller, total_fee).unwrap(); - let access_list = transaction - .access_list - .into_iter() - .map(|(address, keys)| (address.0, keys.into_iter().map(|k| k.0).collect())) - .collect(); + let access_list = transaction + .access_list + .into_iter() + .map(|(address, keys)| (address.0, keys.into_iter().map(|k| k.0).collect())) + .collect(); - // EIP-3607: Reject transactions from senders with deployed code - if caller_code.is_empty() { - match transaction.to { - ethjson::maybe::MaybeEmpty::Some(to) => { - let value = transaction.value.into(); + // EIP-3607: Reject transactions from senders with deployed code + // EIP-7702: Accept transaction even if caller has code. + if caller_code.is_empty() || is_delegated { + match transaction.to { + ethjson::maybe::MaybeEmpty::Some(to) => { + let value = transaction.value.into(); - // Exit reason for Call do not analyzed as it mostly do not expect exceptions - let _reason = executor.transact_call( - caller, - to.into(), - value, - data, - gas_limit, - access_list, - ); - assert_call_exit_exception(&state.expect_exception); - } - ethjson::maybe::MaybeEmpty::None => { - let code = data; - let value = transaction.value.into(); + // Exit reason for Call do not analyzed as it mostly do not expect exceptions + let _reason = executor.transact_call( + caller, + to.into(), + value, + data, + gas_limit, + access_list, + authorization_list, + ); + assert_call_exit_exception(&state.expect_exception); + } + ethjson::maybe::MaybeEmpty::None => { + let code = data; + let value = transaction.value.into(); - let reason = executor.transact_create( - caller, - value, - code, - gas_limit, - access_list, - ); - if check_create_exit_reason( - &reason.0, - &state.expect_exception, - &format!("{spec:?}-{name}-{i}"), - ) { - continue; - } + let reason = + executor.transact_create(caller, value, code, gas_limit, access_list); + if check_create_exit_reason( + &reason.0, + &state.expect_exception, + &format!("{spec:?}-{name}-{i}"), + ) { + continue; } } - } else { + } + } else { + // According to EIP7702 - https://eips.ethereum.org/EIPS/eip-7702#transaction-origination: + // allow EOAs whose code is a valid delegation designation, i.e. `0xef0100 || address`, + // to continue to originate transactions. + #[allow(clippy::collapsible_if)] + if !(*spec >= ForkSpec::Prague + && TxType::from_txbytes(&state.txbytes) == TxType::EOAAccountCode) + { assert_empty_create_caller(&state.expect_exception, name); } + } - if verbose_output.print_state { - println!( - "gas_limit: {gas_limit}\nused_gas: {:?}", - executor.used_gas() - ); - } + if verbose_output.print_state { + println!( + "gas_limit: {gas_limit}\nused_gas: {:?}", + executor.used_gas() + ); + } - let actual_fee = executor.fee(vicinity.effective_gas_price); - // Forks after London burn miner rewards and thus have different gas fee - // calculation (see EIP-1559) - let miner_reward = if spec.is_eth2() { - let coinbase_gas_price = vicinity - .effective_gas_price - .saturating_sub(vicinity.block_base_fee_per_gas); - executor.fee(coinbase_gas_price) - } else { - actual_fee - }; + let actual_fee = executor.fee(vicinity.effective_gas_price); + // Forks after London burn miner rewards and thus have different gas fee + // calculation (see EIP-1559) + let miner_reward = if spec.is_eth2() { + let coinbase_gas_price = vicinity + .effective_gas_price + .saturating_sub(vicinity.block_base_fee_per_gas); + executor.fee(coinbase_gas_price) + } else { + actual_fee + }; - executor - .state_mut() - .deposit(vicinity.block_coinbase, miner_reward); + executor + .state_mut() + .deposit(vicinity.block_coinbase, miner_reward); - let amount_to_return_for_caller = data_fee.map_or_else( - || total_fee - actual_fee, - |data_fee| total_fee - actual_fee - data_fee, - ); - executor - .state_mut() - .deposit(caller, amount_to_return_for_caller); + let amount_to_return_for_caller = data_fee.map_or_else( + || total_fee - actual_fee, + |data_fee| total_fee - actual_fee - data_fee, + ); + executor + .state_mut() + .deposit(caller, amount_to_return_for_caller); - let (values, logs) = executor.into_state().deconstruct(); + let (values, logs) = executor.into_state().deconstruct(); - backend.apply(values, logs, delete_empty); - // It's special case for hard forks: London or before London - // According to EIP-160 empty account should be removed. But in that particular test - original test state - // contains account 0x03 (it's precompile), and when precompile 0x03 was called it exit with - // OutOfGas result. And after exit of substate account not marked as touched, as exit reason - // is not success. And it mean, that it don't appeared in Apply::Modify, then as untouched it - // can't be removed by backend.apply event. In that particular case we should manage it manually. - // NOTE: it's not realistic situation for real life flow. - if *spec <= ForkSpec::London && delete_empty && name == "failed_tx_xcf416c53" { - let state = backend.state_mut(); - state.retain(|addr, account| { - // Check is account empty for precompile 0x03 - !(addr == &H160::from_low_u64_be(3) - && account.balance == U256::zero() - && account.nonce == U256::zero() - && account.code.is_empty()) - }); - } - } else { - if let Some(e) = state.expect_exception.as_ref() { - panic!("unexpected exception: {e} for test {name}-{i}"); - } - panic!("unexpected validation for test {name}-{i}") + backend.apply(values, logs, delete_empty); + // It's special case for hard forks: London or before London + // According to EIP-160 empty account should be removed. But in that particular test - original test state + // contains account 0x03 (it's precompile), and when precompile 0x03 was called it exit with + // OutOfGas result. And after exit of substate account not marked as touched, as exit reason + // is not success. And it mean, that it don't appeared in Apply::Modify, then as untouched it + // can't be removed by backend.apply event. In that particular case we should manage it manually. + // NOTE: it's not realistic situation for real life flow. + if *spec <= ForkSpec::London && delete_empty && name == "failed_tx_xcf416c53" { + let state = backend.state_mut(); + state.retain(|addr, account| { + // Check is account empty for precompile 0x03 + !(addr == &H160::from_low_u64_be(3) + && account.balance == U256::zero() + && account.nonce == U256::zero() + && account.code.is_empty()) + }); } + let (is_valid_hash, actual_hash) = crate::utils::check_valid_hash(&state.hash.0, backend.state()); if !is_valid_hash { @@ -1312,8 +1391,8 @@ fn test_run( } /// Denotes the type of transaction. -#[derive(Debug, PartialEq)] -enum TxType { +#[derive(Debug, PartialEq, Eq)] +pub enum TxType { /// All transactions before EIP-2718 are legacy. Legacy, /// https://eips.ethereum.org/EIPS/eip-2718 @@ -1322,18 +1401,21 @@ enum TxType { DynamicFee, /// https://eips.ethereum.org/EIPS/eip-4844 ShardBlob, + /// https://eips.ethereum.org/EIPS/eip-7702 + EOAAccountCode, } impl TxType { /// Whether this is a legacy, access list, dynamic fee, etc transaction // Taken from geth's core/types/transaction.go/UnmarshalBinary, but we only detect the transaction // type rather than unmarshal the entire payload. - const fn from_txbytes(txbytes: &[u8]) -> Self { + pub const fn from_txbytes(txbytes: &[u8]) -> Self { match txbytes[0] { b if b > 0x7f => Self::Legacy, 1 => Self::AccessList, 2 => Self::DynamicFee, 3 => Self::ShardBlob, + 4 => Self::EOAAccountCode, _ => panic!( "Unknown tx type. \ You may need to update the TxType enum if Ethereum introduced new enveloped transaction types." diff --git a/evm-tests/jsontests/src/utils.rs b/evm-tests/jsontests/src/utils.rs index d2539cda..491b50a8 100644 --- a/evm-tests/jsontests/src/utils.rs +++ b/evm-tests/jsontests/src/utils.rs @@ -1,6 +1,8 @@ use evm::backend::MemoryAccount; +use evm::ExitError; use primitive_types::{H160, H256, U256}; use sha3::{Digest, Keccak256}; +use std::borrow::Cow; use std::collections::BTreeMap; pub fn u256_to_h256(u: U256) -> H256 { @@ -121,7 +123,6 @@ pub fn check_valid_hash(h: &H256, b: &BTreeMap) -> (bool, H code_hash, code_version: U256::zero(), }; - (address, rlp::encode(&account)) }) .collect::>(); @@ -137,6 +138,93 @@ pub fn flush() { io::stdout().flush().expect("Could not flush stdout"); } +/// EIP-7702 +pub mod eip7702 { + use super::{Digest, Keccak256, H160, H256, U256}; + use evm::ExitError; + use rlp::RlpStream; + + pub const MAGIC: u8 = 0x5; + /// The order of the secp256k1 curve, divided by two. Signatures that should be checked according + /// to EIP-2 should have an S value less than or equal to this. + /// + /// `57896044618658097711785492504343953926418782139537452191302581570759080747168` + pub const SECP256K1N_HALF: U256 = U256([ + 0xDFE92F46681B20A0, + 0x5D576E7357A4501D, + 0xFFFFFFFFFFFFFFFF, + 0x7FFFFFFFFFFFFFFF, + ]); + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Authorization { + pub chain_id: U256, + pub address: H160, + pub nonce: u64, + } + + impl Authorization { + #[must_use] + pub const fn new(chain_id: U256, address: H160, nonce: u64) -> Self { + Self { + chain_id, + address, + nonce, + } + } + + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(3); + s.append(&self.chain_id); + s.append(&self.address); + s.append(&self.nonce); + } + + pub fn signature_hash(&self) -> H256 { + let mut rlp_stream = RlpStream::new(); + rlp_stream.append(&MAGIC); + self.rlp_append(&mut rlp_stream); + H256::from_slice(Keccak256::digest(rlp_stream.as_raw()).as_slice()) + } + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct SignedAuthorization { + chain_id: U256, + address: H160, + nonce: u64, + v: bool, + r: U256, + s: U256, + } + + impl SignedAuthorization { + #[must_use] + pub const fn new( + chain_id: U256, + address: H160, + nonce: u64, + r: U256, + s: U256, + v: bool, + ) -> Self { + Self { + chain_id, + address, + nonce, + s, + r, + v, + } + } + + pub fn recover_address(&self) -> Result { + let auth = Authorization::new(self.chain_id, self.address, self.nonce).signature_hash(); + super::ecrecover(auth, &super::vrs_to_arr(self.v, self.r, self.s)) + } + } +} + /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 pub mod eip_4844 { use super::U256; @@ -241,13 +329,16 @@ pub mod eip_4844 { } pub mod transaction { + use crate::state::TxType; + use crate::utils::eip7702; use ethjson::hash::Address; use ethjson::maybe::MaybeEmpty; use ethjson::spec::ForkSpec; - use ethjson::test_helpers::state::MultiTransaction; + use ethjson::test_helpers::state::{MultiTransaction, PostStateResult}; use ethjson::transaction::Transaction; use ethjson::uint::Uint; use evm::backend::MemoryVicinity; + use evm::executor::stack::Authorization; use evm::gasometer::{self, Gasometer}; use primitive_types::{H160, H256, U256}; @@ -263,7 +354,9 @@ pub mod transaction { blob_gas_price: Option, data_fee: Option, spec: &ForkSpec, - ) -> Result<(), InvalidTxReason> { + tx_state: &PostStateResult, + ) -> Result, InvalidTxReason> { + let mut authorization_list: Vec = vec![]; match intrinsic_gas(tx, config) { None => return Err(InvalidTxReason::IntrinsicGas), Some(required_gas) => { @@ -345,7 +438,71 @@ pub mod transaction { } } - Ok(()) + if *spec >= ForkSpec::Prague { + // EIP-7702 - if transaction type is EOAAccountCode then + // `authorization_list` must be present + if TxType::from_txbytes(&tx_state.txbytes) == TxType::EOAAccountCode + && test_tx.authorization_list.is_empty() + { + return Err(InvalidTxReason::AuthorizationListNotExist); + } + + // The field `to` deviates slightly from the semantics with the exception + // that it MUST NOT be nil and therefore must always represent + // a 20-byte address. This means that blob transactions cannot + // have the form of a create transaction. + let to_address: Option
= test_tx.to.clone().into(); + if to_address.is_none() { + return Err(InvalidTxReason::CreateTransaction); + } + + // Check EIP-7702 Spec validation steps: 1 and 2 + // Other validation step inside EVM transact logic. + for auth in test_tx.authorization_list.iter() { + // 1. Verify the chain id is either 0 or the chain’s current ID. + let mut is_valid = + auth.chain_id.0 == U256::from(0) || auth.chain_id.0 == vicinity.chain_id; + // 2. `authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]` + + // Validate the signature, as in tests it is possible to have invalid signatures values. + let v = auth.v.0 .0; + if !(v[0] < u64::from(u8::MAX) && v[1..4].iter().all(|&elem| elem == 0)) { + return Err(InvalidTxReason::InvalidAuthorizationSignature); + } + // Value `v` shouldn't be greater then 1 + if v[0] > 1 { + return Err(InvalidTxReason::InvalidAuthorizationSignature); + } + // EIP-2 validation + if auth.s.0 > eip7702::SECP256K1N_HALF { + return Err(InvalidTxReason::InvalidAuthorizationSignature); + } + + let auth_address = eip7702::SignedAuthorization::new( + auth.chain_id.0, + auth.address.0, + auth.nonce.0.as_u64(), + auth.r.0, + auth.s.0, + auth.v.0.as_u32() > 0, + ) + .recover_address(); + let auth_address = auth_address.unwrap_or_else(|_| { + is_valid = false; + H160::zero() + }); + + authorization_list.push(Authorization { + authority: auth_address, + address: auth.address.0, + nonce: auth.nonce.0.as_u64(), + is_valid, + }); + } + } else if !test_tx.authorization_list.is_empty() { + return Err(InvalidTxReason::AuthorizationListNotSupported); + } + Ok(authorization_list) } fn intrinsic_gas(tx: &Transaction, config: &evm::Config) -> Option { @@ -360,10 +517,13 @@ pub mod transaction { .map(|(a, s)| (a.0, s.iter().map(|h| h.0).collect())) .collect(); + // EIP-7702 + let authorization_list_len = tx.authorization_list.len(); + let cost = if is_contract_creation { gasometer::create_transaction_cost(data, &access_list) } else { - gasometer::call_transaction_cost(data, &access_list) + gasometer::call_transaction_cost(data, &access_list, authorization_list_len) }; let mut g = Gasometer::new(u64::MAX, config); @@ -387,5 +547,87 @@ pub mod transaction { BlobVersionedHashesNotSupported, MaxFeePerBlobGasNotSupported, GasPriseEip1559, + AuthorizationListNotExist, + AuthorizationListNotSupported, + InvalidAuthorizationSignature, + CreateTransaction, + } +} + +fn ecrecover(hash: H256, signature: &[u8]) -> Result { + let hash = libsecp256k1::Message::parse_slice(hash.as_bytes()) + .map_err(|e| ExitError::Other(Cow::from(e.to_string())))?; + let v = signature[64]; + let signature = libsecp256k1::Signature::parse_standard_slice(&signature[0..64]) + .map_err(|e| ExitError::Other(Cow::from(e.to_string())))?; + let bit = match v { + 0..=26 => v, + _ => v - 27, + }; + + if let Ok(recovery_id) = libsecp256k1::RecoveryId::parse(bit) { + if let Ok(public_key) = libsecp256k1::recover(&hash, &signature, &recovery_id) { + // recover returns a 65-byte key, but addresses come from the raw 64-byte key + let r = sha3::Keccak256::digest(&public_key.serialize()[1..]); + return Ok(H160::from_slice(&r[12..])); + } + } + + Err(ExitError::Other(Cow::from("ECRecoverErr unknown error"))) +} + +/// v, r, s signature values to array +fn vrs_to_arr(v: bool, r: U256, s: U256) -> [u8; 65] { + let mut result = [0u8; 65]; // (r, s, v), typed (uint256, uint256, uint8) + r.to_big_endian(&mut result[0..32]); + s.to_big_endian(&mut result[32..64]); + result[64] = u8::from(v); + result +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use primitive_types::H160; + + #[test] + fn test_ecrecover_success() { + let hash = H256::from_slice(&hex!( + "47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" + )); + let signature = hex!("650acf9d3f5f0a2c799776a1254355d5f4061762a237396a99a0e0e3fc2bcd6729514a0dacb2e623ac4abd157cb18163ff942280db4d5caad66ddf941ba12e031b"); + let expected_address = H160::from_slice(&hex!("c08b5542d177ac6686946920409741463a15dddb")); + + let result = ecrecover(hash, &signature).expect("ecrecover should succeed"); + assert_eq!(result, expected_address); + } + + #[test] + fn test_ecrecover_invalid_signature() { + let hash = H256::from_slice(&hex!( + "47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" + )); + let signature = hex!("00650acf9d3f5f0a2c799776a1254355d5f4061762a237396a99a0e0e3fc2bcd6729514a0dacb2e623ac4abd157cb18163ff942280db4d5caad66ddf941ba12e031c"); + + let result = ecrecover(hash, &signature); + assert_eq!( + result, + Err(ExitError::Other(Cow::from("ECRecoverErr unknown error"))) + ); + } + + #[test] + fn test_ecrecover_invalid_recovery_id() { + let hash = H256::from_slice(&hex!( + "47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" + )); + let signature = hex!("650acf9d3f5f0a2c799776a1254355d5f4061762a237396a99a0e0e3fc2bcd6729514a0dacb2e623ac4abd157cb18163ff942280db4d5caad66ddf941ba12e0327"); + + let result = ecrecover(hash, &signature); + assert_eq!( + result, + Err(ExitError::Other(Cow::from("ECRecoverErr unknown error"))) + ); } } diff --git a/gasometer/src/costs.rs b/gasometer/src/costs.rs index 57be9142..16b0b891 100644 --- a/gasometer/src/costs.rs +++ b/gasometer/src/costs.rs @@ -120,16 +120,26 @@ pub fn verylowcopy_cost(len: U256) -> Result { Ok(gas.as_u64()) } -pub fn extcodecopy_cost(len: U256, is_cold: bool, config: &Config) -> Result { +pub fn extcodecopy_cost( + len: U256, + is_cold: bool, + delegated_designator_is_cold: Option, + config: &Config, +) -> Result { let wordd = len / U256::from(32); let is_wordr = (len % U256::from(32)) == U256::zero(); - let gas = U256::from(address_access_cost(is_cold, config.gas_ext_code, config)) - .checked_add( - U256::from(consts::G_COPY) - .checked_mul(if is_wordr { wordd } else { wordd + U256::one() }) - .ok_or(ExitError::OutOfGas)?, - ) - .ok_or(ExitError::OutOfGas)?; + let gas = U256::from(address_access_cost( + is_cold, + delegated_designator_is_cold, + config.gas_ext_code, + config, + )) + .checked_add( + U256::from(consts::G_COPY) + .checked_mul(if is_wordr { wordd } else { wordd + U256::one() }) + .ok_or(ExitError::OutOfGas)?, + ) + .ok_or(ExitError::OutOfGas)?; if gas > U64_MAX { return Err(ExitError::OutOfGas); @@ -264,24 +274,44 @@ pub fn suicide_cost(value: U256, is_cold: bool, target_exists: bool, config: &Co pub fn call_cost( value: U256, is_cold: bool, + delegated_designator_is_cold: Option, is_call_or_callcode: bool, is_call_or_staticcall: bool, new_account: bool, config: &Config, ) -> u64 { let transfers_value = value != U256::default(); - address_access_cost(is_cold, config.gas_call, config) - + xfer_cost(is_call_or_callcode, transfers_value) + address_access_cost( + is_cold, + delegated_designator_is_cold, + config.gas_call, + config, + ) + xfer_cost(is_call_or_callcode, transfers_value) + new_cost(is_call_or_staticcall, new_account, transfers_value, config) } -pub const fn address_access_cost(is_cold: bool, regular_value: u64, config: &Config) -> u64 { +pub const fn address_access_cost( + is_cold: bool, + delegated_designator_is_cold: Option, + regular_value: u64, + config: &Config, +) -> u64 { if config.increase_state_access_gas { - if is_cold { + let mut gas = if is_cold { config.gas_account_access_cold } else { config.gas_storage_read_warm + }; + if config.has_authorization_list { + if let Some(target_is_cold) = delegated_designator_is_cold { + if target_is_cold { + gas += config.gas_account_access_cold; + } else { + gas += config.gas_storage_read_warm; + } + } } + gas } else { regular_value } diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index b9509c27..222c8938 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -220,9 +220,21 @@ impl<'config> Gasometer<'config> { Ok(()) } - /// Record `CREATE` code deposit. - /// + /// Record refund for `authority` - EIP-7702 + /// `refunded_accounts` represent count of valid `authority` accounts. /// + /// ## Errors + /// Return `ExitError` if `record_refund` operation fails. + pub fn record_authority_refund(&mut self, refunded_accounts: u64) -> Result<(), ExitError> { + let refund = i64::try_from( + refunded_accounts + * (self.config.gas_per_empty_account_cost - self.config.gas_per_auth_base_cost), + ) + .unwrap_or(i64::MAX); + self.record_refund(refund) + } + + /// Record `CREATE` code deposit. /// /// # Errors /// Return `ExitError` @@ -284,7 +296,10 @@ impl<'config> Gasometer<'config> { inner_mut.refunded_gas += gas_refund; // NOTE Extended meesage: "Record dynamic cost {gas_cost} - memory_gas {} - gas_refund {}", - log_gas!(self, "record_dynamic_cost: {}", gas_cost,); + log_gas!( + self, + "record_dynamic_cost: {gas_cost} - {memory_gas} - {gas_refund}" + ); Ok(()) } @@ -311,35 +326,38 @@ impl<'config> Gasometer<'config> { /// Return `ExitError` pub fn record_transaction(&mut self, cost: TransactionCost) -> Result<(), ExitError> { let gas_cost = match cost { - // NOTE: in that context usize->u64 `as_conversions` is save + // NOTE: in that context usize->u64 `as_conversions` is safe #[allow(clippy::as_conversions)] TransactionCost::Call { zero_data_len, non_zero_data_len, access_list_address_len, access_list_storage_len, + authorization_list_len, } => { #[deny(clippy::let_and_return)] let cost = self.config.gas_transaction_call + zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + access_list_address_len as u64 * self.config.gas_access_list_address - + access_list_storage_len as u64 * self.config.gas_access_list_storage_key; + + access_list_storage_len as u64 * self.config.gas_access_list_storage_key + + authorization_list_len as u64 * self.config.gas_per_empty_account_cost; log_gas!( self, - "Record Call {} [gas_transaction_call: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}]", + "Record Call {} [gas_transaction_call: {}, zero_data_len: {}, non_zero_data_len: {}, access_list_address_len: {}, access_list_storage_len: {}, authorization_list_len: {}]", cost, self.config.gas_transaction_call, zero_data_len, non_zero_data_len, access_list_address_len, - access_list_storage_len + access_list_storage_len, + authorization_list_len ); cost } - // NOTE: in that context usize->u64 `as_conversions` is save + // NOTE: in that context usize->u64 `as_conversions` is safe #[allow(clippy::as_conversions)] TransactionCost::Create { zero_data_len, @@ -399,7 +417,11 @@ impl<'config> Gasometer<'config> { /// Calculate the call transaction cost. #[allow(clippy::naive_bytecount)] #[must_use] -pub fn call_transaction_cost(data: &[u8], access_list: &[(H160, Vec)]) -> TransactionCost { +pub fn call_transaction_cost( + data: &[u8], + access_list: &[(H160, Vec)], + authorization_list_len: usize, +) -> TransactionCost { let zero_data_len = data.iter().filter(|v| **v == 0).count(); let non_zero_data_len = data.len() - zero_data_len; let (access_list_address_len, access_list_storage_len) = count_access_list(access_list); @@ -409,6 +431,7 @@ pub fn call_transaction_cost(data: &[u8], access_list: &[(H160, Vec)]) -> non_zero_data_len, access_list_address_len, access_list_storage_len, + authorization_list_len, } } @@ -574,6 +597,26 @@ pub fn static_opcode_cost(opcode: Opcode) -> Option { TABLE[opcode.as_usize()] } +/// Get and set warm address if it's not warmed. +fn get_and_set_warm(handler: &mut H, target: H160) -> (bool, Option) { + let delegated_designator_is_cold = + handler + .get_authority_target(target) + .map(|authority_target| { + if handler.is_cold(authority_target, None) { + handler.warm_target((authority_target, None)); + true + } else { + false + } + }); + let target_is_cold = handler.is_cold(target, None); + if target_is_cold { + handler.warm_target((target, None)); + } + (target_is_cold, delegated_designator_is_cold) +} + /// Calculate the opcode cost. /// /// # Errors @@ -591,8 +634,7 @@ pub fn dynamic_opcode_cost( is_static: bool, config: &Config, handler: &mut H, -) -> Result<(GasCost, StorageTarget, Option), ExitError> { - let mut storage_target = StorageTarget::None; +) -> Result<(GasCost, Option), ExitError> { let gas_cost = match opcode { Opcode::RETURN => GasCost::Zero, @@ -632,36 +674,40 @@ pub fn dynamic_opcode_cost( Opcode::EXTCODESIZE => { let target = stack.peek_h256(0)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::ExtCodeSize { - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, } } Opcode::BALANCE => { let target = stack.peek_h256(0)?.into(); - storage_target = StorageTarget::Address(target); - GasCost::Balance { - target_is_cold: handler.is_cold(target, None), + let target_is_cold = handler.is_cold(target, None); + if target_is_cold { + handler.warm_target((target, None)); } + GasCost::Balance { target_is_cold } } Opcode::BLOCKHASH => GasCost::BlockHash, Opcode::EXTCODEHASH if config.has_ext_code_hash => { let target = stack.peek_h256(0)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::ExtCodeHash { - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, } } Opcode::EXTCODEHASH => GasCost::Invalid(opcode), Opcode::CALLCODE => { let target = stack.peek_h256(1)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::CallCode { value: stack.peek(2)?, gas: stack.peek(0)?, - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, target_exists: { handler.record_external_operation(evm_core::ExternalOperation::IsEmpty)?; handler.exists(target) @@ -670,10 +716,11 @@ pub fn dynamic_opcode_cost( } Opcode::STATICCALL => { let target = stack.peek_h256(1)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::StaticCall { gas: stack.peek(0)?, - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, target_exists: { handler.record_external_operation(evm_core::ExternalOperation::IsEmpty)?; handler.exists(target) @@ -685,9 +732,10 @@ pub fn dynamic_opcode_cost( }, Opcode::EXTCODECOPY => { let target = stack.peek_h256(0)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::ExtCodeCopy { - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, len: stack.peek(3)?, } } @@ -699,18 +747,20 @@ pub fn dynamic_opcode_cost( }, Opcode::SLOAD => { let index = stack.peek_h256(0)?; - storage_target = StorageTarget::Slot(address, index); - GasCost::SLoad { - target_is_cold: handler.is_cold(address, Some(index)), + let target_is_cold = handler.is_cold(address, Some(index)); + if target_is_cold { + handler.warm_target((address, Some(index))); } + GasCost::SLoad { target_is_cold } } Opcode::DELEGATECALL if config.has_delegate_call => { let target = stack.peek_h256(1)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::DelegateCall { gas: stack.peek(0)?, - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, target_exists: { handler.record_external_operation(evm_core::ExternalOperation::IsEmpty)?; handler.exists(target) @@ -728,13 +778,15 @@ pub fn dynamic_opcode_cost( Opcode::SSTORE if !is_static => { let index = stack.peek_h256(0)?; let value = stack.peek_h256(1)?; - storage_target = StorageTarget::Slot(address, index); - + let target_is_cold = handler.is_cold(address, Some(index)); + if target_is_cold { + handler.warm_target((address, Some(index))); + } GasCost::SStore { original: handler.original_storage(address, index), current: handler.storage(address, index), new: value, - target_is_cold: handler.is_cold(address, Some(index)), + target_is_cold, } } Opcode::LOG0 if !is_static => GasCost::Log { @@ -763,10 +815,13 @@ pub fn dynamic_opcode_cost( }, Opcode::SELFDESTRUCT if !is_static => { let target = stack.peek_h256(0)?.into(); - storage_target = StorageTarget::Address(target); + let target_is_cold = handler.is_cold(target, None); + if target_is_cold { + handler.warm_target((target, None)); + } GasCost::Suicide { value: handler.balance(address), - target_is_cold: handler.is_cold(target, None), + target_is_cold, target_exists: { handler.record_external_operation(evm_core::ExternalOperation::IsEmpty)?; handler.exists(target) @@ -776,11 +831,12 @@ pub fn dynamic_opcode_cost( } Opcode::CALL if !is_static || (is_static && stack.peek(2)? == U256::zero()) => { let target = stack.peek_h256(1)?.into(); - storage_target = StorageTarget::Address(target); + let (target_is_cold, delegated_designator_is_cold) = get_and_set_warm(handler, target); GasCost::Call { value: stack.peek(2)?, gas: stack.peek(0)?, - target_is_cold: handler.is_cold(target, None), + target_is_cold, + delegated_designator_is_cold, target_exists: { handler.record_external_operation(evm_core::ExternalOperation::IsEmpty)?; handler.exists(target) @@ -848,7 +904,7 @@ pub fn dynamic_opcode_cost( _ => None, }; - Ok((gas_cost, storage_target, memory_cost)) + Ok((gas_cost, memory_cost)) } fn peek_memory_cost( @@ -906,16 +962,19 @@ impl<'config> Inner<'config> { } /// Returns the gas cost numerical value. + #[allow(clippy::too_many_lines)] fn gas_cost(&self, cost: GasCost, gas: u64) -> Result { Ok(match cost { GasCost::Call { value, target_is_cold, + delegated_designator_is_cold, target_exists, .. } => costs::call_cost( value, target_is_cold, + delegated_designator_is_cold, true, true, !target_exists, @@ -924,11 +983,13 @@ impl<'config> Inner<'config> { GasCost::CallCode { value, target_is_cold, + delegated_designator_is_cold, target_exists, .. } => costs::call_cost( value, target_is_cold, + delegated_designator_is_cold, true, false, !target_exists, @@ -936,11 +997,13 @@ impl<'config> Inner<'config> { ), GasCost::DelegateCall { target_is_cold, + delegated_designator_is_cold, target_exists, .. } => costs::call_cost( U256::zero(), target_is_cold, + delegated_designator_is_cold, false, false, !target_exists, @@ -948,11 +1011,13 @@ impl<'config> Inner<'config> { ), GasCost::StaticCall { target_is_cold, + delegated_designator_is_cold, target_exists, .. } => costs::call_cost( U256::zero(), target_is_cold, + delegated_designator_is_cold, false, true, !target_exists, @@ -986,19 +1051,38 @@ impl<'config> Inner<'config> { GasCost::Low => u64::from(consts::G_LOW), GasCost::Invalid(opcode) => return Err(ExitError::InvalidCode(opcode)), - GasCost::ExtCodeSize { target_is_cold } => { - costs::address_access_cost(target_is_cold, self.config.gas_ext_code, self.config) - } + GasCost::ExtCodeSize { + target_is_cold, + delegated_designator_is_cold, + } => costs::address_access_cost( + target_is_cold, + delegated_designator_is_cold, + self.config.gas_ext_code, + self.config, + ), GasCost::ExtCodeCopy { target_is_cold, + delegated_designator_is_cold, len, - } => costs::extcodecopy_cost(len, target_is_cold, self.config)?, - GasCost::Balance { target_is_cold } => { - costs::address_access_cost(target_is_cold, self.config.gas_balance, self.config) - } + } => costs::extcodecopy_cost( + len, + target_is_cold, + delegated_designator_is_cold, + self.config, + )?, + GasCost::Balance { target_is_cold } => costs::address_access_cost( + target_is_cold, + None, + self.config.gas_balance, + self.config, + ), GasCost::BlockHash => u64::from(consts::G_BLOCKHASH), - GasCost::ExtCodeHash { target_is_cold } => costs::address_access_cost( + GasCost::ExtCodeHash { + target_is_cold, + delegated_designator_is_cold, + } => costs::address_access_cost( target_is_cold, + delegated_designator_is_cold, self.config.gas_ext_code_hash, self.config, ), @@ -1042,6 +1126,8 @@ pub enum GasCost { ExtCodeSize { /// True if address has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, }, /// Gas cost for `BALANCE`. Balance { @@ -1054,6 +1140,8 @@ pub enum GasCost { ExtCodeHash { /// True if address has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, }, /// Gas cost for `CALL`. @@ -1064,6 +1152,8 @@ pub enum GasCost { gas: U256, /// True if target has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, /// Whether the target exists. target_exists: bool, }, @@ -1075,6 +1165,8 @@ pub enum GasCost { gas: U256, /// True if target has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, /// Whether the target exists. target_exists: bool, }, @@ -1084,6 +1176,8 @@ pub enum GasCost { gas: U256, /// True if target has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, /// Whether the target exists. target_exists: bool, }, @@ -1093,6 +1187,8 @@ pub enum GasCost { gas: U256, /// True if target has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, /// Whether the target exists. target_exists: bool, }, @@ -1134,6 +1230,8 @@ pub enum GasCost { ExtCodeCopy { /// True if target has not been previously accessed in this transaction target_is_cold: bool, + /// True if delegated designator of authority has not been previously accessed in this transaction (EIP-7702) + delegated_designator_is_cold: Option, /// Length. len: U256, }, @@ -1195,6 +1293,8 @@ pub enum TransactionCost { access_list_address_len: usize, /// Total number of storage keys in transaction access list (see EIP-2930) access_list_storage_len: usize, + /// Number of authorities in transaction authorization list (see EIP-7702) + authorization_list_len: usize, }, /// Create transaction cost. Create { diff --git a/runtime/src/eval/system.rs b/runtime/src/eval/system.rs index 7d942391..e8be40d6 100644 --- a/runtime/src/eval/system.rs +++ b/runtime/src/eval/system.rs @@ -128,21 +128,21 @@ pub fn blob_hash(runtime: &mut Runtime, handler: &H) -> Control { Control::Continue } -pub fn extcodesize(runtime: &mut Runtime, handler: &H) -> Control { +pub fn extcodesize(runtime: &mut Runtime, handler: &mut H) -> Control { pop_h256!(runtime, address); push_u256!(runtime, handler.code_size(address.into())); Control::Continue } -pub fn extcodehash(runtime: &mut Runtime, handler: &H) -> Control { +pub fn extcodehash(runtime: &mut Runtime, handler: &mut H) -> Control { pop_h256!(runtime, address); push_h256!(runtime, handler.code_hash(address.into())); Control::Continue } -pub fn extcodecopy(runtime: &mut Runtime, handler: &H) -> Control { +pub fn extcodecopy(runtime: &mut Runtime, handler: &mut H) -> Control { pop_h256!(runtime, address); pop_u256!(runtime, memory_offset, code_offset, len); @@ -162,7 +162,7 @@ pub fn extcodecopy(runtime: &mut Runtime, handler: &H) -> Control memory_offset, code_offset, len, - &handler.code(address.into()), + &handler.authority_code(address.into()), ) { Ok(()) => (), Err(e) => return Control::Exit(e.into()), diff --git a/runtime/src/handler.rs b/runtime/src/handler.rs index 309b80db..afda015b 100644 --- a/runtime/src/handler.rs +++ b/runtime/src/handler.rs @@ -28,9 +28,9 @@ pub trait Handler { /// Get balance of address. fn balance(&self, address: H160) -> U256; /// Get code size of address. - fn code_size(&self, address: H160) -> U256; + fn code_size(&mut self, address: H160) -> U256; /// Get code hash of address. - fn code_hash(&self, address: H160) -> H256; + fn code_hash(&mut self, address: H160) -> H256; /// Get code of address. fn code(&self, address: H160) -> Vec; /// Get storage value of address at index. @@ -172,4 +172,14 @@ pub trait Handler { /// # Errors /// Return `ExitError` fn tload(&mut self, address: H160, index: H256) -> Result; + + /// Return the target address of the authority delegation designation (EIP-7702). + fn get_authority_target(&mut self, address: H160) -> Option; + + /// Get delegation designator code for the authority code. + /// EIP-7702 + fn authority_code(&mut self, authority: H160) -> Vec; + + /// Warm target according to EIP-2929 + fn warm_target(&mut self, target: (H160, Option)); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index c40af8e6..7f66c80a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -288,6 +288,12 @@ pub struct Config { pub has_mcopy: bool, /// SELFDESTRUCT restriction: EIP-6780 pub has_restricted_selfdestruct: bool, + /// EIP-7702 + pub has_authorization_list: bool, + /// EIP-7702 + pub gas_per_empty_account_cost: u64, + /// EIP-7702 + pub gas_per_auth_base_cost: u64, } impl Config { @@ -348,6 +354,9 @@ impl Config { has_transient_storage: false, has_mcopy: false, has_restricted_selfdestruct: false, + has_authorization_list: false, + gas_per_empty_account_cost: 0, + gas_per_auth_base_cost: 0, } } @@ -408,6 +417,9 @@ impl Config { has_transient_storage: false, has_mcopy: false, has_restricted_selfdestruct: false, + has_authorization_list: false, + gas_per_auth_base_cost: 0, + gas_per_empty_account_cost: 0, } } @@ -441,6 +453,12 @@ impl Config { Self::config_with_derived_values(DerivedConfigInputs::cancun()) } + /// Prague hard fork configuration. + #[must_use] + pub const fn prague() -> Self { + Self::config_with_derived_values(DerivedConfigInputs::prague()) + } + const fn config_with_derived_values(inputs: DerivedConfigInputs) -> Self { let DerivedConfigInputs { gas_storage_read_warm, @@ -457,6 +475,9 @@ impl Config { has_transient_storage, has_mcopy, has_restricted_selfdestruct, + has_authorization_list, + gas_per_empty_account_cost, + gas_per_auth_base_cost, } = inputs; // See https://eips.ethereum.org/EIPS/eip-2929 @@ -527,6 +548,9 @@ impl Config { has_transient_storage, has_mcopy, has_restricted_selfdestruct, + has_authorization_list, + gas_per_empty_account_cost, + gas_per_auth_base_cost, } } } @@ -550,6 +574,9 @@ struct DerivedConfigInputs { has_transient_storage: bool, has_mcopy: bool, has_restricted_selfdestruct: bool, + has_authorization_list: bool, + gas_per_empty_account_cost: u64, + gas_per_auth_base_cost: u64, } impl DerivedConfigInputs { @@ -569,6 +596,9 @@ impl DerivedConfigInputs { has_transient_storage: false, has_mcopy: false, has_restricted_selfdestruct: false, + has_authorization_list: false, + gas_per_auth_base_cost: 0, + gas_per_empty_account_cost: 0, } } @@ -588,6 +618,9 @@ impl DerivedConfigInputs { has_transient_storage: false, has_mcopy: false, has_restricted_selfdestruct: false, + has_authorization_list: false, + gas_per_auth_base_cost: 0, + gas_per_empty_account_cost: 0, } } @@ -607,6 +640,9 @@ impl DerivedConfigInputs { has_transient_storage: false, has_mcopy: false, has_restricted_selfdestruct: false, + has_authorization_list: false, + gas_per_auth_base_cost: 0, + gas_per_empty_account_cost: 0, } } @@ -627,26 +663,27 @@ impl DerivedConfigInputs { has_transient_storage: false, has_mcopy: false, has_restricted_selfdestruct: false, + has_authorization_list: false, + gas_per_auth_base_cost: 0, + gas_per_empty_account_cost: 0, } } const fn cancun() -> Self { - Self { - gas_storage_read_warm: 100, - gas_sload_cold: 2100, - gas_access_list_storage_key: 1900, - decrease_clears_refund: true, - has_base_fee: true, - has_push0: true, - disallow_executable_format: true, - warm_coinbase_address: true, - // 2 * 24576 as per EIP-3860 - max_initcode_size: Some(0xC000), - has_blob_base_fee: true, - has_shard_blob_transactions: true, - has_transient_storage: true, - has_mcopy: true, - has_restricted_selfdestruct: true, - } + let mut config = Self::shanghai(); + config.has_blob_base_fee = true; + config.has_shard_blob_transactions = true; + config.has_transient_storage = true; + config.has_mcopy = true; + config.has_restricted_selfdestruct = true; + config + } + + const fn prague() -> Self { + let mut config = Self::cancun(); + config.has_authorization_list = true; + config.gas_per_empty_account_cost = 25000; + config.gas_per_auth_base_cost = 2500; + config } } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 935d0564..8a4dde51 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -9,6 +9,7 @@ pub use self::memory::{MemoryAccount, MemoryBackend, MemoryVicinity}; mod memory; /// Basic account information. +/// #[derive(Clone, Eq, PartialEq, Debug, Default)] #[cfg_attr( feature = "with-codec", diff --git a/src/executor/stack/executor.rs b/src/executor/stack/executor.rs index 8d884886..4aa89b48 100644 --- a/src/executor/stack/executor.rs +++ b/src/executor/stack/executor.rs @@ -58,10 +58,67 @@ pub enum StackExitKind { Failed, } +/// `Authorization` contains already prepared data for EIP-7702. +/// - `authority`is `ecrecovered` authority address. +/// - `address` is delegation destination address. +/// - `nonce` is the `nonce` value which `authority.nonce` should be equal. +/// - `is_valid` is the flag that indicates the validity of the authorization. It is used to +/// charge gas for each authorization item, but if it's invalid exclude from EVM `authority_list` flow. +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct Authorization { + pub authority: H160, + pub address: H160, + pub nonce: u64, + pub is_valid: bool, +} + +impl Authorization { + /// Create a new `Authorization` with given `authority`, `address`, and `nonce`. + #[must_use] + pub const fn new(authority: H160, address: H160, nonce: u64, is_valid: bool) -> Self { + Self { + authority, + address, + nonce, + is_valid, + } + } + + /// Returns `true` if `authority` is delegated to `address`. + /// `0xef0100 ++ address`, and it is always 23 bytes. + #[must_use] + pub fn is_delegated(code: &[u8]) -> bool { + code.len() == 23 && code.starts_with(&[0xEF, 0x01, 0x00]) + } + + /// Get `authority` delegated `address`. + /// It checks, is it delegation designation (EIP-7702). + #[must_use] + pub fn get_delegated_address(code: &[u8]) -> Option { + if Self::is_delegated(code) { + // `code` size is always 23 bytes. + Some(H160::from_slice(&code[3..])) + } else { + None + } + } + + /// Returns the delegation code as composing: `0xef0100 ++ address`. + /// Result code is always 23 bytes. + #[must_use] + pub fn delegation_code(&self) -> Vec { + let mut code = Vec::with_capacity(23); + code.extend(&[0xEF, 0x01, 0x00]); + code.extend(self.address.as_bytes()); + code + } +} + #[derive(Default, Clone, Debug)] pub struct Accessed { pub accessed_addresses: BTreeSet, pub accessed_storage: BTreeSet<(H160, H256)>, + pub authority: BTreeMap, } impl Accessed { @@ -84,6 +141,23 @@ impl Accessed { self.accessed_storage.insert((storage.0, storage.1)); } } + + /// Add authority to the accessed authority list (EIP-7702). + pub fn add_authority(&mut self, authority: H160, address: H160) { + self.authority.insert(authority, address); + } + + /// Get authority from the accessed authority list (EIP-7702). + #[must_use] + pub fn get_authority_target(&self, authority: H160) -> Option { + self.authority.get(&authority).copied() + } + + /// Check if authority is in the accessed authority list (EIP-7702). + #[must_use] + pub fn is_authority(&self, authority: H160) -> bool { + self.authority.contains_key(&authority) + } } #[derive(Clone, Debug)] @@ -132,6 +206,9 @@ impl<'config> StackSubstateMetadata<'config> { self_accessed .accessed_storage .append(&mut other_accessed.accessed_storage); + self_accessed + .authority + .append(&mut other_accessed.authority); } Ok(()) @@ -215,6 +292,13 @@ impl<'config> StackSubstateMetadata<'config> { pub const fn accessed(&self) -> &Option { &self.accessed } + + /// Add authority to accessed list (related to EIP-7702) + pub fn add_authority(&mut self, authority: H160, address: H160) { + if let Some(accessed) = &mut self.accessed { + accessed.add_authority(authority, address); + } + } } #[auto_impl::auto_impl(& mut, Box)] @@ -254,22 +338,6 @@ pub trait StackState<'config>: Backend { fn reset_balance(&mut self, address: H160); fn touch(&mut self, address: H160); - /// Fetch the code size of an address. - /// Provide a default implementation by fetching the code, but - /// can be customized to use a more performant approach that don't need to - /// fetch the code. - fn code_size(&self, address: H160) -> U256 { - U256::from(self.code(address).len()) - } - - /// Fetch the code hash of an address. - /// Provide a default implementation by fetching the code, but - /// can be customized to use a more performant approach that don't need to - /// fetch the code. - fn code_hash(&self, address: H160) -> H256 { - H256::from_slice(Keccak256::digest(self.code(address)).as_slice()) - } - /// # Errors /// Return `ExitError` fn record_external_operation( @@ -320,6 +388,12 @@ pub trait StackState<'config>: Backend { /// # Errors /// Return `ExitError` fn tload(&mut self, address: H160, index: H256) -> Result; + + /// EIP-7702 - check is authority cold. + fn is_authority_cold(&mut self, address: H160) -> Option; + + /// EIP-7702 - get authority target address. + fn get_authority_target(&mut self, address: H160) -> Option; } /// Stack-based executor. @@ -519,12 +593,18 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> gas_limit: u64, access_list: Vec<(H160, Vec)>, // See EIP-2930 ) -> (ExitReason, Vec) { + if self.nonce(caller) >= U64_MAX { + return (ExitError::MaxNonce.into(), Vec::new()); + } + + let address = self.create_address(CreateScheme::Legacy { caller }); + event!(TransactCreate { caller, value, init_code: &init_code, gas_limit, - address: self.create_address(CreateScheme::Legacy { caller }), + address, }); if let Some(limit) = self.config.max_initcode_size { @@ -538,8 +618,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> return emit_exit!(e.into(), Vec::new()); } - // Initialize initial addresses for EIP-2929 - self.initialize_addresses(&[caller], access_list); + self.warm_addresses_and_storage(caller, address, access_list); match self.create_inner( caller, @@ -570,20 +649,21 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> gas_limit: u64, access_list: Vec<(H160, Vec)>, // See EIP-2930 ) -> (ExitReason, Vec) { + let address = self.create_address(CreateScheme::Fixed(address)); + event!(TransactCreate { caller, value, init_code: &init_code, gas_limit, - address: self.create_address(CreateScheme::Fixed(address)), + address }); if let Err(e) = self.record_create_transaction_cost(&init_code, &access_list) { return emit_exit!(e.into(), Vec::new()); } - // Initialize initial addresses for EIP-2929 - self.initialize_addresses(&[caller], access_list); + self.warm_addresses_and_storage(caller, address, access_list); match self.create_inner( caller, @@ -604,6 +684,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> } /// Execute a `CREATE2` transaction. + #[allow(clippy::too_many_arguments)] pub fn transact_create2( &mut self, caller: H160, @@ -613,33 +694,33 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> gas_limit: u64, access_list: Vec<(H160, Vec)>, // See EIP-2930 ) -> (ExitReason, Vec) { + if let Some(limit) = self.config.max_initcode_size { + if init_code.len() > limit { + self.state.metadata_mut().gasometer.fail(); + return emit_exit!(ExitError::CreateContractLimit.into(), Vec::new()); + } + } + let code_hash = H256::from_slice(Keccak256::digest(&init_code).as_slice()); + let address = self.create_address(CreateScheme::Create2 { + caller, + code_hash, + salt, + }); event!(TransactCreate2 { caller, value, init_code: &init_code, salt, gas_limit, - address: self.create_address(CreateScheme::Create2 { - caller, - code_hash, - salt, - }), + address, }); - if let Some(limit) = self.config.max_initcode_size { - if init_code.len() > limit { - self.state.metadata_mut().gasometer.fail(); - return emit_exit!(ExitError::CreateContractLimit.into(), Vec::new()); - } - } - if let Err(e) = self.record_create_transaction_cost(&init_code, &access_list) { return emit_exit!(e.into(), Vec::new()); } - // Initialize initial addresses for EIP-2929 - self.initialize_addresses(&[caller], access_list); + self.warm_addresses_and_storage(caller, address, access_list); match self.create_inner( caller, @@ -663,12 +744,12 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> } } - /// Execute a `CALL` transaction with a given caller, address, value and - /// gas limit and data. + /// Execute a `CALL` transaction with a given parameters /// - /// Takes in an additional `access_list` parameter for EIP-2930 which was - /// introduced in the Ethereum Berlin hard fork. If you do not wish to use - /// this functionality, just pass in an empty vector. + /// ## Notes + /// - `access_list` associated to [EIP-2930: Optional access lists](https://eips.ethereum.org/EIPS/eip-2930) + /// - `authorization_list` associated to [EIP-7702: Authorized accounts](https://eips.ethereum.org/EIPS/eip-7702) + #[allow(clippy::too_many_arguments)] pub fn transact_call( &mut self, caller: H160, @@ -677,6 +758,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> data: Vec, gas_limit: u64, access_list: Vec<(H160, Vec)>, + authorization_list: Vec, ) -> (ExitReason, Vec) { event!(TransactCall { caller, @@ -690,20 +772,25 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> return (ExitError::MaxNonce.into(), Vec::new()); } - let transaction_cost = gasometer::call_transaction_cost(&data, &access_list); + let transaction_cost = + gasometer::call_transaction_cost(&data, &access_list, authorization_list.len()); let gasometer = &mut self.state.metadata_mut().gasometer; match gasometer.record_transaction(transaction_cost) { Ok(()) => (), Err(e) => return emit_exit!(e.into(), Vec::new()), } - // Initialize initial addresses for EIP-2929 - self.initialize_addresses(&[caller, address], access_list); - if let Err(e) = self.state.inc_nonce(caller) { return (e.into(), Vec::new()); } + self.warm_addresses_and_storage(caller, address, access_list); + // EIP-7702. authorized accounts + // NOTE: it must be after `inc_nonce` + if let Err(e) = self.authorized_accounts(authorization_list) { + return (e.into(), Vec::new()); + } + let context = Context { caller, address, @@ -761,7 +848,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> /// Check if the existing account is "create collision". /// [EIP-7610](https://eips.ethereum.org/EIPS/eip-7610) pub fn is_create_collision(&self, address: H160) -> bool { - self.code_size(address) != U256::zero() + !self.code(address).is_empty() || self.nonce(address) > U256::zero() || !self.state.is_empty_storage(address) } @@ -792,7 +879,11 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> } } - pub fn initialize_with_access_list(&mut self, access_list: Vec<(H160, Vec)>) { + /// According to `EIP-2930` - `access_list` should be warmed. + /// This function warms addresses and storage keys. + /// + /// [EIP-2930: Optional access lists](https://eips.ethereum.org/EIPS/eip-2930) + pub fn warm_access_list(&mut self, access_list: Vec<(H160, Vec)>) { let addresses = access_list.iter().map(|a| a.0); self.state.metadata_mut().access_addresses(addresses); @@ -802,22 +893,110 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> self.state.metadata_mut().access_storages(storage_keys); } - fn initialize_addresses(&mut self, addresses: &[H160], access_list: Vec<(H160, Vec)>) { + /// Warm addresses and storage keys. + /// - According to `EIP-2929` the addresses should be warmed: + /// 1. caller (tx.sender) + /// 2. address (tx.to or the address being created if it is a contract creation transaction) + /// - Warm coinbase according to `EIP-3651` + /// - Warm `access_list` according to `EIP-2931` + /// + /// ## References + /// - [EIP-2929: Gas cost increases for state access opcodes](https://eips.ethereum.org/EIPS/eip-2929) + /// - [EIP-2930: Optional access lists](https://eips.ethereum.org/EIPS/eip-2930) + /// - [EIP-3651: Warm COINBASE](https://eips.ethereum.org/EIPS/eip-3651) + fn warm_addresses_and_storage( + &mut self, + caller: H160, + address: H160, + access_list: Vec<(H160, Vec)>, + ) { if self.config.increase_state_access_gas { if self.config.warm_coinbase_address { // Warm coinbase address for EIP-3651 let coinbase = self.block_coinbase(); self.state .metadata_mut() - .access_addresses(addresses.iter().copied().chain(Some(coinbase))); + .access_addresses([caller, address, coinbase].iter().copied()); } else { self.state .metadata_mut() - .access_addresses(addresses.iter().copied()); + .access_addresses([caller, address].iter().copied()); }; - self.initialize_with_access_list(access_list); + self.warm_access_list(access_list); + } + } + + /// Authorized accounts behavior. + /// + /// According to `EIP-7702` behavior section should be several steps of verifications. + /// Current function includes steps 3-8 from the spec: + /// 3. Add `authority` to `accessed_addresses` + /// 4. Verify the code of `authority` is either empty or already delegated. + /// 5. Verify the `nonce` of `authority` is equal to `nonce` (of address). + /// 7. Set the code of `authority` to be `0xef0100 || address`. This is a delegation designation. + /// 8. Increase the `nonce` of `authority` by one. + /// + /// It means, that steps 1-2 of spec must be passed before calling this function: + /// 1 Verify the chain id is either 0 or the chain’s current ID. + /// 2. `authority = ecrecover(...)` + /// + /// See: [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702#behavior) + /// + /// ## Errors + /// Return error if nonce increment return error. + fn authorized_accounts( + &mut self, + authorization_list: Vec, + ) -> Result<(), ExitError> { + if !self.config.has_authorization_list { + return Ok(()); + } + let mut refunded_accounts = 0; + + let state = self.state_mut(); + let mut warm_authority: Vec = Vec::with_capacity(authorization_list.len()); + for authority in authorization_list { + // If EIP-7703 Spec validation steps 1 or 2 return false. + if !authority.is_valid { + continue; + } + // 3. Add authority to accessed_addresses (as defined in EIP-2929) + warm_authority.push(authority.authority); + // 4. Verify the code of authority is either empty or already delegated. + let authority_code = state.code(authority.authority); + if !authority_code.is_empty() && !Authorization::is_delegated(&authority_code) { + continue; + } + + // 5. Verify the nonce of authority is equal to nonce. + if state.basic(authority.authority).nonce != U256::from(authority.nonce) { + continue; + } + + // 6. Add PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST gas to the global refund counter if authority exists in the trie. + if !state.is_empty(authority.authority) { + refunded_accounts += 1; + } + // 7. Set the code of authority to be `0xef0100 || address`. This is a delegation designation. + state.set_code(authority.authority, authority.delegation_code()); + // 8. Increase the nonce of authority by one. + state.inc_nonce(authority.authority)?; + + // Add to authority access list cache + state + .metadata_mut() + .add_authority(authority.authority, authority.address); } + // Warm addresses for [Step 3]. + self.state + .metadata_mut() + .access_addresses(warm_authority.into_iter()); + + self.state + .metadata_mut() + .gasometer + .record_authority_refund(refunded_accounts) } /// Calculate gas limit and record it in the gasometer. @@ -971,7 +1150,13 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> } } - let code = self.code(code_address); + // EIP-7702 - get delegated designation address code + // Detect loop for Delegated designation + let code = self.authority_code(code_address); + // Warm Delegated address after access + if let Some(target_address) = self.get_authority_target(code_address) { + self.warm_target((target_address, None)); + } self.enter_substate(gas_limit, is_static); self.state.touch(context.address); @@ -1177,8 +1362,8 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Interprete #[cfg(feature = "tracing")] { use evm_runtime::tracing::Event::Step; - #[allow(clippy::used_underscore_binding)] evm_runtime::tracing::with(|listener| { + #[allow(clippy::used_underscore_binding)] listener.event(Step { address: *address, opcode, @@ -1198,7 +1383,7 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Interprete .record_cost(u64::from(cost))?; } else { let is_static = self.state.metadata().is_static; - let (gas_cost, target, memory_cost) = gasometer::dynamic_opcode_cost( + let (gas_cost, memory_cost) = gasometer::dynamic_opcode_cost( *address, opcode, machine.stack(), @@ -1211,15 +1396,6 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Interprete .metadata_mut() .gasometer .record_dynamic_cost(gas_cost, memory_cost)?; - match target { - StorageTarget::Address(address) => { - self.state.metadata_mut().access_address(address); - } - StorageTarget::Slot(address, key) => { - self.state.metadata_mut().access_storage(address, key); - } - StorageTarget::None => (), - } } Ok(()) } @@ -1233,8 +1409,8 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Interprete #[cfg(feature = "tracing")] { use evm_runtime::tracing::Event::StepResult; - #[allow(clippy::used_underscore_binding)] evm_runtime::tracing::with(|listener| { + #[allow(clippy::used_underscore_binding)] listener.event(StepResult { result: _result, return_value: _machine.return_value().as_slice(), @@ -1262,18 +1438,32 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Handler self.state.basic(address).balance } - /// Get account code size - fn code_size(&self, address: H160) -> U256 { - self.state.code_size(address) + /// Fetch the code size of an address. + /// Provide a default implementation by fetching the code. + /// + /// According to EIP-7702, the code size of an address is the size of the + /// delegated address code size. + fn code_size(&mut self, address: H160) -> U256 { + let target_code = self.authority_code(address); + U256::from(target_code.len()) } - /// Get account code hash - fn code_hash(&self, address: H160) -> H256 { + /// Fetch the code hash of an address. + /// Provide a default implementation by fetching the code. + /// + /// According to EIP-7702, the code hash of an address is the hash of the + /// delegated address code hash. + fn code_hash(&mut self, address: H160) -> H256 { if !self.exists(address) { return H256::default(); } - - self.state.code_hash(address) + if let Some(target) = self.get_authority_target(address) { + if !self.exists(target) { + return H256::default(); + } + } + let code = self.authority_code(address); + H256::from_slice(Keccak256::digest(code).as_slice()) } /// Get account code @@ -1532,6 +1722,45 @@ impl<'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Handler Err(ExitError::InvalidCode(Opcode::TLOAD)) } } + + /// Return the target address of the authority delegation designation (EIP-7702). + fn get_authority_target(&mut self, address: H160) -> Option { + if self.config.has_authorization_list { + self.state.get_authority_target(address) + } else { + None + } + } + + /// Get delegation designator code for the authority code. + /// If the code of address is delegation designator, then retrieve code + /// from the designation address for the `authority`. + /// Detect delegated designation loop and return basic byte code for loop. + /// + /// It's related to [EIP-7702 Delegation Designation](https://eips.ethereum.org/EIPS/eip-7702#delegation-designation) + /// When authority code is found, it should set delegated address to `authority_access` array for + /// calculating additional gas cost. Gas must be charged for the authority address and + /// for delegated address, for detection is address warm or cold. + fn authority_code(&mut self, authority: H160) -> Vec { + if !self.config.has_authorization_list { + return self.code(authority); + } + // Check if it is a loop for Delegated designation + self.get_authority_target(authority).map_or_else( + || self.code(authority), + |target_address| self.code(target_address), + ) + } + + // Warm target according to EIP-2929 + // It warm up the target address or storage value by key. If in the target tuple + // the storage is `None` then it's warming up the address. + fn warm_target(&mut self, target: (H160, Option)) { + match target { + (address, None) => self.state.metadata_mut().access_address(address), + (address, Some(key)) => self.state.metadata_mut().access_storage(address, key), + } + } } struct StackExecutorHandle<'inner, 'config, 'precompiles, S, P> { @@ -1561,10 +1790,17 @@ impl<'inner, 'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Pr // Since we don't go through opcodes we need manually record the call // cost. Not doing so will make the code panic as recording the call stipend // will do an underflow. + let target_is_cold = self.executor.is_cold(code_address, None); + let delegated_designator_is_cold = self + .executor + .get_authority_target(code_address) + .map(|target| self.executor.is_cold(target, None)); + let gas_cost = gasometer::GasCost::Call { value: transfer.clone().map_or_else(U256::zero, |x| x.value), gas: U256::from(gas_limit.unwrap_or(u64::MAX)), - target_is_cold: self.executor.is_cold(code_address, None), + target_is_cold, + delegated_designator_is_cold, target_exists: self.executor.exists(code_address), }; @@ -1647,7 +1883,7 @@ impl<'inner, 'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Pr .refund_external_cost(ref_time, proof_size); } - /// Retreive the remaining gas. + /// Retrieve the remaining gas. fn remaining_gas(&self) -> u64 { self.executor.state.metadata().gasometer.gas() } @@ -1657,17 +1893,17 @@ impl<'inner, 'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Pr Handler::log(self.executor, address, topics, data) } - /// Retreive the code address (what is the address of the precompile being called). + /// Retrieve the code address (what is the address of the precompile being called). fn code_address(&self) -> H160 { self.code_address } - /// Retreive the input data the precompile is called with. + /// Retrieve the input data the precompile is called with. fn input(&self) -> &[u8] { self.input } - /// Retreive the context in which the precompile is executed. + /// Retrieve the context in which the precompile is executed. fn context(&self) -> &Context { self.context } @@ -1677,7 +1913,7 @@ impl<'inner, 'config, 'precompiles, S: StackState<'config>, P: PrecompileSet> Pr self.is_static } - /// Retreive the gas limit of this call. + /// Retrieve the gas limit of this call. fn gas_limit(&self) -> Option { self.gas_limit } diff --git a/src/executor/stack/memory.rs b/src/executor/stack/memory.rs index 367e25e4..4606d400 100644 --- a/src/executor/stack/memory.rs +++ b/src/executor/stack/memory.rs @@ -1,5 +1,7 @@ use crate::backend::{Apply, Backend, Basic, Log}; -use crate::executor::stack::executor::{Accessed, StackState, StackSubstateMetadata}; +use crate::executor::stack::executor::{ + Accessed, Authorization, StackState, StackSubstateMetadata, +}; use crate::prelude::*; use crate::{ExitError, Transfer}; use core::mem; @@ -491,6 +493,22 @@ impl<'config> MemoryStackSubstate<'config> { pub fn set_tstorage(&mut self, address: H160, key: H256, value: U256) { self.tstorages.insert((address, key), value); } + + /// Get authority target from the current state. If it's `None` just take a look + /// recursively in the parent state. + fn get_authority_target_recursive(&self, authority: H160) -> Option { + if let Some(target) = self + .metadata + .accessed() + .as_ref() + .and_then(|accessed| accessed.get_authority_target(authority)) + { + return Some(target); + } + self.parent + .as_ref() + .and_then(|p| p.get_authority_target_recursive(authority)) + } } #[derive(Clone, Debug)] @@ -608,7 +626,7 @@ impl<'backend, 'config, B: Backend> StackState<'config> for MemoryStackState<'ba self.backend.basic(address).balance == U256::zero() && self.backend.basic(address).nonce == U256::zero() - && self.backend.code(address).len() == 0 + && self.backend.code(address).is_empty() } fn deleted(&self, address: H160) -> bool { @@ -675,6 +693,33 @@ impl<'backend, 'config, B: Backend> StackState<'config> for MemoryStackState<'ba self.substate.set_tstorage(address, index, value); Ok(()) } + + /// EIP-7702 - check is authority cold. + fn is_authority_cold(&mut self, address: H160) -> Option { + self.get_authority_target(address) + .map(|target| self.is_cold(target)) + } + + /// Get authority target (EIP-7702) - delegated address. + /// First we're trying to get authority target from the cache recursively with parent state, + /// if it's not found we get code for the authority address and check if it's delegation + /// designator. If it's true, we add result to cache and return delegated target address. + fn get_authority_target(&mut self, authority: H160) -> Option { + // Read from cache + if let Some(target_address) = self.substate.get_authority_target_recursive(authority) { + Some(target_address) + } else { + // If not found in the cache + // Get code for delegated address + let authority_code = self.code(authority); + if let Some(target) = Authorization::get_delegated_address(&authority_code) { + // Add to cache + self.metadata_mut().add_authority(authority, target); + return Some(target); + } + None + } + } } impl<'backend, 'config, B: Backend> MemoryStackState<'backend, 'config, B> { diff --git a/src/executor/stack/mod.rs b/src/executor/stack/mod.rs index 9908f24e..f99d22b2 100644 --- a/src/executor/stack/mod.rs +++ b/src/executor/stack/mod.rs @@ -8,7 +8,7 @@ mod precompile; mod tagged_runtime; pub use self::executor::{ - Accessed, StackExecutor, StackExitKind, StackState, StackSubstateMetadata, + Accessed, Authorization, StackExecutor, StackExitKind, StackState, StackSubstateMetadata, }; pub use self::memory::{MemoryStackAccount, MemoryStackState, MemoryStackSubstate}; pub use self::precompile::{