diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index 8d54d0417b01e1bbc645ae981afe308fbce782c8..204e9aae6bb765ba9d1662f491131a293f9eface 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -28,6 +28,8 @@ - [`KT1NnH9DCAoY1pfPNvb9cw9XPKQnHAFYFHXa` for the sequencer governance][sqgov] - Computes the execution gas assuming a minimum base fee per gas instead of current gas price. The paid DA fees remain unchanged. (!17954) +- The EVM now supports optional access lists. + See [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930). (!17766) ### Bug fixes diff --git a/etherlink/kernel_latest/Cargo.lock b/etherlink/kernel_latest/Cargo.lock index d2ff15fb7805d6957d19ece7cc2f9b6a8df6982b..c7ed4deaa0ccb8a2800373d36fa20651b5d15046 100644 --- a/etherlink/kernel_latest/Cargo.lock +++ b/etherlink/kernel_latest/Cargo.lock @@ -2313,6 +2313,7 @@ dependencies = [ "libsecp256k1", "primitive-types", "rlp", + "serde", "sha3", "tezos-smart-rollup-encoding", "tezos_crypto_rs", diff --git a/etherlink/kernel_latest/Cargo.toml b/etherlink/kernel_latest/Cargo.toml index e61e3a3e89375cc2df29d4445982d3a96d9054d3..06cfe6b38e04e73c8df57a7dd223972b7e2070bd 100644 --- a/etherlink/kernel_latest/Cargo.toml +++ b/etherlink/kernel_latest/Cargo.toml @@ -44,6 +44,7 @@ tezos_data_encoding = { version = "0.6", path = "../../sdk/rust/encoding" } const-decoder = { version = "0.3.0" } rlp = "0.5.2" nom = { version = "7.1", default-features = false } +serde = { version = "1.0", features = ["derive", "rc"] } # ethereum VM evm = { path = "../sputnikvm", default-features = false } diff --git a/etherlink/kernel_latest/ethereum/Cargo.toml b/etherlink/kernel_latest/ethereum/Cargo.toml index b20e5f4845c828a82002d5d2ea0d4d4e094b45fb..ee2bdc90fa8f0fdeddd44eb34adc44a91c0f7aac 100644 --- a/etherlink/kernel_latest/ethereum/Cargo.toml +++ b/etherlink/kernel_latest/ethereum/Cargo.toml @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2023 Nomadic Labs # SPDX-FileCopyrightText: 2023 Marigold +# SPDX-FileCopyrightText: 2025 Functori # # SPDX-License-Identifier: MIT @@ -29,6 +30,11 @@ libsecp256k1.workspace = true tezos-smart-rollup-encoding.workspace = true +# Useful to keep the same types between the execution and evaluation +serde = { workspace = true, optional = true } + [features] default = [] benchmark = [] +serde = ["dep:serde"] +evaluation = ["serde"] diff --git a/etherlink/kernel_latest/ethereum/src/access_list.rs b/etherlink/kernel_latest/ethereum/src/access_list.rs index 79c72e28edbe02b6b90a3a7e9bdeb9221bc29fda..012605ab3057d22f7ea3feed5941aacb2f7f7fe8 100644 --- a/etherlink/kernel_latest/ethereum/src/access_list.rs +++ b/etherlink/kernel_latest/ethereum/src/access_list.rs @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2025 Functori // // SPDX-License-Identifier: MIT @@ -11,6 +12,8 @@ use crate::rlp_helpers::{decode_field, decode_list, next}; /// which are being accessed during a contract invocation. /// For more information see ``. #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "evaluation", derive(serde::Deserialize))] +#[cfg_attr(feature = "evaluation", serde(rename_all = "camelCase"))] pub struct AccessListItem { /// Address of the contract invoked during execution pub address: H160, @@ -46,3 +49,7 @@ impl Decodable for AccessListItem { } pub type AccessList = Vec; + +pub fn empty_access_list() -> AccessList { + vec![] +} diff --git a/etherlink/kernel_latest/evm_evaluation/Cargo.toml b/etherlink/kernel_latest/evm_evaluation/Cargo.toml index 0b066f78c0a14dcee7a17f5749ad4ea79d1e4abe..3c21cfc9f53b6ddd28dafec12d6d28b3db17b0bb 100644 --- a/etherlink/kernel_latest/evm_evaluation/Cargo.toml +++ b/etherlink/kernel_latest/evm_evaluation/Cargo.toml @@ -12,19 +12,19 @@ license = "MIT" thiserror.workspace = true evm-execution = { workspace = true, features = ["debug"] } -tezos_ethereum.workspace = true +tezos_ethereum = { workspace = true, features = ["evaluation"] } tezos-evm-logging.workspace = true tezos-evm-runtime.workspace = true tezos-smart-rollup-mock.workspace = true tezos-smart-rollup-host.workspace = true tezos-smart-rollup-core.workspace = true +serde.workspace = true hex = { version = "0.4.3", features = ["serde"] } hex-literal.workspace = true bytes = "1.5" walkdir = "2.4" -serde = { version = "1.0", features = ["derive", "rc"] } serde_json = { version = "1.0", features = ["preserve_order"] } serde_yaml = "0.9.25" structopt = "0.3.26" diff --git a/etherlink/kernel_latest/evm_evaluation/src/main.rs b/etherlink/kernel_latest/evm_evaluation/src/main.rs index 72ecff856c68bd89031be414f79b19edf893d38b..ee7cdea6771cdff78404ba33db23963949cc47b0 100644 --- a/etherlink/kernel_latest/evm_evaluation/src/main.rs +++ b/etherlink/kernel_latest/evm_evaluation/src/main.rs @@ -396,15 +396,12 @@ pub fn check_skip(test_file_path: &Path) -> bool { | "Create2OnDepth1024.json" | "CallRecursiveBombLog2.json" - // Reason: EIP-2930 (https://eips.ethereum.org/EIPS/eip-2930) concerns optional - // access lists and we don't intend to implement them for now - | "addressOpcodes.json" + // Reason: Coinbase related tests that expects an absurd amount of WEI + // after the execution. + // NB: The expected storage slots for 0x000000000000000000000000000000000000C0DE + // which contains the gas cost with hot/cold access are accurate. | "coinbaseT01.json" | "coinbaseT2.json" - | "manualCreate.json" - | "storageCosts.json" - | "transactionCosts.json" - | "variedContext.json" // Reason: relying on precompile contract kzg_point_evaluation from // EIP-4844 which we don't support. diff --git a/etherlink/kernel_latest/evm_evaluation/src/models/mod.rs b/etherlink/kernel_latest/evm_evaluation/src/models/mod.rs index e19df831d032efcb185bd48d67aa51a887ccf85d..bce856616f92541adb458488578f39c26cdda551 100644 --- a/etherlink/kernel_latest/evm_evaluation/src/models/mod.rs +++ b/etherlink/kernel_latest/evm_evaluation/src/models/mod.rs @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023-2024 Functori +// SPDX-FileCopyrightText: 2023-2025 Functori // SPDX-FileCopyrightText: 2021-2023 draganrakita // // SPDX-License-Identifier: MIT @@ -13,6 +13,7 @@ use std::{ collections::{BTreeMap, HashMap}, fmt, }; +use tezos_ethereum::access_list::AccessList; use crate::models::deserializer::*; @@ -173,6 +174,7 @@ pub struct UnitEnv { #[derive(Debug, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionParts { + pub access_lists: Option>>, #[serde(deserialize_with = "deserialize_vec_as_vec_bytes")] pub data: Vec, pub gas_limit: Vec, diff --git a/etherlink/kernel_latest/evm_evaluation/src/runner.rs b/etherlink/kernel_latest/evm_evaluation/src/runner.rs index 3f9a37a7b462d845131edf9f5a4893902f58b1d0..91e64e2978acc504819f6046c73e4668b9491b72 100644 --- a/etherlink/kernel_latest/evm_evaluation/src/runner.rs +++ b/etherlink/kernel_latest/evm_evaluation/src/runner.rs @@ -12,6 +12,7 @@ use evm_execution::handler::ExecutionOutcome; use evm_execution::precompiles::{precompile_set, PrecompileBTreeMap}; use evm_execution::{run_transaction, Config, EthereumError}; +use tezos_ethereum::access_list::AccessList; use tezos_ethereum::block::{BlockConstants, BlockFees}; use hex_literal::hex; @@ -197,6 +198,7 @@ fn execute_transaction( env: &mut Env, test: &Test, data: Bytes, + access_list: AccessList, ) -> Result, EthereumError> { let gas_limit = *unit.transaction.gas_limit.get(test.indexes.gas).unwrap(); let gas_limit = u64::try_from(gas_limit).unwrap_or(u64::MAX); @@ -228,10 +230,12 @@ fn execute_transaction( "Executing transaction with:\n\ \t- data: {}\n\ \t- gas: {} gas\n\ - \t- value: {} wei", + \t- value: {} wei\n\ + \t- access list: {:?}", string_of_hexa(&env.tx.data), gas_limit, - env.tx.value + env.tx.value, + access_list ); run_transaction( host, @@ -247,6 +251,7 @@ fn execute_transaction( transaction_value, pay_for_gas, None, + access_list, ) } @@ -373,12 +378,17 @@ pub fn run_test( } } - let data = unit - .transaction - .data - .get(test_execution.indexes.data) - .unwrap() - .clone(); + let data_index = test_execution.indexes.data; + + let data = unit.transaction.data.get(data_index).unwrap().clone(); + + let access_list = match unit.transaction.access_lists { + Some(ref access_list) => match access_list.get(data_index).unwrap() { + Some(access_list) => access_list.to_vec(), + None => vec![], + }, + None => vec![], + }; if data_to_skip(&name, &data, skip_data) { continue; @@ -393,6 +403,7 @@ pub fn run_test( &mut env, test_execution, data, + access_list, ); let labels = LabelIndexes { diff --git a/etherlink/kernel_latest/evm_execution/src/fa_bridge/mod.rs b/etherlink/kernel_latest/evm_execution/src/fa_bridge/mod.rs index 8edc3d92c40279f4e8605138520469ddd0ce1e99..44bf2a554ede61b9d423bf4da8fa9f9c9377ee6d 100644 --- a/etherlink/kernel_latest/evm_execution/src/fa_bridge/mod.rs +++ b/etherlink/kernel_latest/evm_execution/src/fa_bridge/mod.rs @@ -53,6 +53,7 @@ use primitive_types::H256; use primitive_types::{H160, U256}; use rlp::Decodable; use rlp::Rlp; +use tezos_ethereum::access_list::empty_access_list; use tezos_ethereum::block::BlockConstants; use tezos_ethereum::Log; use tezos_evm_logging::{ @@ -221,6 +222,7 @@ pub fn queue_fa_deposit<'a, Host: Runtime>( precompiles, block.base_fee_per_gas(), tracer_input, + empty_access_list(), ); handler.begin_initial_transaction( diff --git a/etherlink/kernel_latest/evm_execution/src/fa_bridge/test_utils.rs b/etherlink/kernel_latest/evm_execution/src/fa_bridge/test_utils.rs index 9222cf70fdea264b780f44dccecfdda0468c6778..063823eb2a006940a04e6cf0ca006d0e0fe25269 100644 --- a/etherlink/kernel_latest/evm_execution/src/fa_bridge/test_utils.rs +++ b/etherlink/kernel_latest/evm_execution/src/fa_bridge/test_utils.rs @@ -10,6 +10,7 @@ use num_bigint::BigInt; use primitive_types::{H160, H256, U256}; use tezos_data_encoding::enc::BinWriter; use tezos_ethereum::{ + access_list::empty_access_list, block::{BlockConstants, BlockFees}, Log, }; @@ -99,6 +100,7 @@ pub fn deploy_mock_wrapper( U256::zero(), false, None, + empty_access_list(), ) .expect("Failed to deploy") .unwrap() @@ -143,6 +145,7 @@ pub fn deploy_reentrancy_tester( U256::zero(), false, None, + empty_access_list(), ) .expect("Failed to deploy") .unwrap() @@ -189,6 +192,7 @@ pub fn run_fa_deposit( &precompiles, U256::from(21000), None, + empty_access_list(), ); handler @@ -439,6 +443,7 @@ pub fn fa_bridge_precompile_call_withdraw( &precompiles, U256::from(21000), None, + empty_access_list(), ); handler diff --git a/etherlink/kernel_latest/evm_execution/src/handler.rs b/etherlink/kernel_latest/evm_execution/src/handler.rs index b90dd2b2491d6e6c1d66cb7815891b153b961c31..9a176aa9405f4d5fe00e31f05949b75b2bc42942 100644 --- a/etherlink/kernel_latest/evm_execution/src/handler.rs +++ b/etherlink/kernel_latest/evm_execution/src/handler.rs @@ -33,7 +33,7 @@ use alloc::borrow::Cow; use alloc::rc::Rc; use core::convert::Infallible; use evm::executor::stack::Log; -use evm::gasometer::{GasCost, Gasometer, MemoryCost}; +use evm::gasometer::{GasCost, Gasometer, MemoryCost, TransactionCost}; use evm::{ CallScheme, Capture, Config, Context, CreateScheme, ExitError, ExitFatal, ExitReason, ExitRevert, ExitSucceed, Handler, Opcode, Resolve, Stack, Transfer, @@ -44,6 +44,7 @@ use std::cmp::min; use std::collections::{BTreeSet, HashMap}; use std::fmt::Debug; use tezos_data_encoding::enc::{BinResult, BinWriter}; +use tezos_ethereum::access_list::{AccessList, AccessListItem}; use tezos_ethereum::block::BlockConstants; use tezos_evm_logging::{log, Level::*}; use tezos_evm_runtime::runtime::Runtime; @@ -445,6 +446,8 @@ pub struct EvmHandler<'a, Host: Runtime> { pub created_contracts: BTreeSet, /// Reentrancy guard prevents circular calls to impure precompiles reentrancy_guard: ReentrancyGuard, + /// Access list as specified by EIP-2930. + access_list: AccessList, } impl<'a, Host: Runtime> EvmHandler<'a, Host> { @@ -459,6 +462,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { precompiles: &'a dyn PrecompileSet, effective_gas_price: U256, tracer: Option, + access_list: AccessList, ) -> Self { Self { host, @@ -480,9 +484,51 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { WITHDRAWAL_ADDRESS, FA_BRIDGE_PRECOMPILE_ADDRESS, ]), + access_list, } } + pub fn preheat_with_accesslist(&mut self) -> Result<(), EthereumError> { + // EIP-3651 + if self.config.warm_coinbase_address { + self.get_contract(self.block_coinbase()); + if self.mark_address_as_hot(self.block_coinbase()).is_err() { + return Err(EthereumError::InconsistentState(Cow::from(format!( + "Failed to mark coinbase address {} as hot", + self.block_coinbase(), + )))); + } + } + + for AccessListItem { + address, + storage_keys, + } in self.access_list.clone() + { + // Pre-heat contract code + self.get_contract(address); + if self.mark_address_as_hot(address).is_err() { + return Err(EthereumError::InconsistentState(Cow::from(format!( + "Failed to mark access list address {} as hot", + address + )))); + } + + for index in storage_keys { + // Pre-heat storage slots + self.storage(address, index); + if self.mark_storage_as_hot(address, index).is_err() { + return Err(EthereumError::InconsistentState(Cow::from(format!( + "Failed to mark access list storage slot at address {} and index {} as hot", + address, index + )))); + } + } + } + + Ok(()) + } + /// Get the total amount of gas used for the duration of the current /// transaction. pub fn gas_used(&self) -> u64 { @@ -563,6 +609,24 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { .unwrap_or(Ok(())) } + /// Record the intrinsic gas cost of a transaction + pub fn record_transaction( + &mut self, + transaction_cost: TransactionCost, + ) -> Result<(), ExitError> { + let Some(layer) = self.transaction_data.last_mut() else { + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); + }; + + layer + .gasometer + .as_mut() + .map(|gasometer| gasometer.record_transaction(transaction_cost)) + .unwrap_or(Ok(())) + } + /// Record code deposit. Pay per byte for a CREATE operation pub fn record_deposit(&mut self, len: usize) -> Result<(), ExitError> { let Some(layer) = self.transaction_data.last_mut() else { @@ -649,12 +713,15 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { (init_code.len() as u64).div_ceil(32) } - fn record_init_code_cost(&mut self, init_code: &[u8]) -> Result<(), ExitError> { + fn init_code_cost(&self, init_code: &[u8]) -> u64 { // As per EIP-3860: // > We define init_code_cost to equal INITCODE_WORD_COST * get_word_size(init_code). // where INITCODE_WORD_COST is 2. - let init_code_cost = 2 * self.get_word_size(init_code); - self.record_cost(init_code_cost) + 2 * self.get_word_size(init_code) + } + + fn record_init_code_cost(&mut self, init_code: &[u8]) -> Result<(), ExitError> { + self.record_cost(self.init_code_cost(init_code)) } /// Mark a location in durable storage as _hot_ for the purpose of calculating @@ -742,6 +809,17 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { self.created_contracts.contains(address) } + fn access_list_address_len(&self) -> usize { + self.access_list.len() + } + + fn access_list_storage_len(&self) -> usize { + self.access_list + .iter() + .map(|AccessListItem { storage_keys, .. }| storage_keys.len()) + .sum() + } + /// Record the base fee part of the transaction cost. We need the SputnikVM /// error code in case this goes wrong, so that's what we return. fn record_base_gas_cost( @@ -749,24 +827,38 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { is_create: bool, data: &[u8], ) -> Result<(), ExitError> { - let base_cost = if is_create { - self.config.gas_transaction_create + let mut zero_data_len = 0; + let mut non_zero_data_len = 0; + let access_list_address_len = self.access_list_address_len(); + let access_list_storage_len = self.access_list_storage_len(); + + data.iter().for_each(|datum| { + if *datum == 0_u8 { + zero_data_len += 1; + } else { + non_zero_data_len += 1; + } + }); + + let transaction_cost = if is_create { + let initcode_cost = self.init_code_cost(data); + TransactionCost::Create { + zero_data_len, + non_zero_data_len, + access_list_address_len, + access_list_storage_len, + initcode_cost, + } } else { - self.config.gas_transaction_call + TransactionCost::Call { + zero_data_len, + non_zero_data_len, + access_list_address_len, + access_list_storage_len, + } }; - let data_cost: u64 = data - .iter() - .map(|datum| { - if *datum == 0_u8 { - self.config.gas_transaction_zero_data - } else { - self.config.gas_transaction_non_zero_data - } - }) - .sum(); - - self.record_cost(base_cost + data_cost) + self.record_transaction(transaction_cost) } /// Add withdrawals to the current transaction layer @@ -1468,20 +1560,6 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { ))); } - if let Err(err) = self.record_base_gas_cost(true, &input) { - return self.end_initial_transaction(Ok(( - ExitReason::Error(err), - None, - vec![], - ))); - } - - if self.mark_address_as_hot(address).is_err() { - return Err(EthereumError::InconsistentState(Cow::from( - "Failed to mark callee address as hot", - ))); - } - // We check that the maximum allowed init code size as specified by EIP-3860 // can not be reached. if let Some(max_initcode_size) = self.config.max_initcode_size { @@ -1494,16 +1572,20 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { } } - if let Err(err) = self.record_init_code_cost(&input) { - log!(self.host, Debug, "{:?}: Cannot record init code cost.", err); - + if let Err(err) = self.record_base_gas_cost(true, &input) { return self.end_initial_transaction(Ok(( - ExitReason::Error(ExitError::OutOfGas), + ExitReason::Error(err), None, vec![], ))); } + if self.mark_address_as_hot(address).is_err() { + return Err(EthereumError::InconsistentState(Cow::from( + "Failed to mark callee address as hot", + ))); + } + let result = self.execute_create(caller, value.unwrap_or_default(), input, address); @@ -1663,6 +1745,8 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { AccessRecord::default(), )); + self.preheat_with_accesslist()?; + self.evm_account_storage .begin_transaction(self.host) .map_err(EthereumError::from) @@ -2693,11 +2777,6 @@ impl Handler for EvmHandler<'_, Host> { } fn is_cold(&mut self, address: H160, index: Option) -> Result { - // EIP-3651 - if self.config.warm_coinbase_address && address == self.block_coinbase() { - return Ok(false); - } - match index { Some(index) => { let is_cold = self.is_storage_hot(address, index).map(|x| !x); @@ -3128,6 +3207,7 @@ mod test { use std::cmp::Ordering; use std::str::FromStr; use std::vec; + use tezos_ethereum::access_list::empty_access_list; use tezos_ethereum::block::BlockFees; use tezos_evm_runtime::runtime::MockKernelHost; @@ -3233,6 +3313,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let result = handler @@ -3267,6 +3348,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let code_hash: H256 = CODE_HASH_DEFAULT; @@ -3308,6 +3390,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let code_hash: H256 = CODE_HASH_DEFAULT; @@ -3354,6 +3437,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(213_u64); @@ -3423,6 +3507,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(213_u64); @@ -3524,6 +3609,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let input_value = U256::from(1025_u32); // transaction depth for contract below is callarg - 1 @@ -3624,6 +3710,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(312); @@ -3703,6 +3790,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let value = U256::zero(); @@ -3767,6 +3855,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let value = U256::zero(); @@ -3840,6 +3929,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(117); @@ -3910,6 +4000,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(210_u64); @@ -3979,6 +4070,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(210_u64); @@ -4057,6 +4149,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let hash_of_unavailable_block = handler.block_hash(U256::zero()); @@ -4084,6 +4177,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(210_u64); @@ -4143,6 +4237,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(210_u64); @@ -4236,6 +4331,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); // { (SELFDESTRUCT 0x10) } @@ -4291,6 +4387,7 @@ mod test { &precompiles, U256::one(), None, + empty_access_list(), ); set_balance(&mut handler, &caller, U256::from(10000)); @@ -4340,6 +4437,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address_1 = H160::from_low_u64_be(210_u64); @@ -4423,6 +4521,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let hash = handler.code_hash(H160::from_low_u64_le(1)); @@ -4451,6 +4550,7 @@ mod test { &precompiles, U256::one(), None, + empty_access_list(), ); set_balance(&mut handler, &caller, U256::from(1000000000)); @@ -4493,6 +4593,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address_1 = H160::from_low_u64_be(210_u64); @@ -4581,6 +4682,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address_1 = H160::from_low_u64_be(210_u64); @@ -4687,6 +4789,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let target_destruct = @@ -4751,6 +4854,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let contrac_addr = @@ -4827,6 +4931,7 @@ mod test { &precompiles, U256::from(21000), None, + empty_access_list(), ); handler @@ -4899,6 +5004,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let address = H160::from_low_u64_be(210_u64); @@ -4980,6 +5086,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); let initial_code = [1; 49153]; // MAX_INIT_CODE_SIZE + 1 @@ -5035,6 +5142,7 @@ mod test { &precompiles, U256::one(), None, + empty_access_list(), ); let _ = handler.begin_initial_transaction( @@ -5083,6 +5191,7 @@ mod test { &precompiles, U256::from(21000), None, + empty_access_list(), ); let address1 = H160::from_low_u64_be(210_u64); @@ -5163,6 +5272,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); // SUICIDE would charge 25,000 gas when the destination is non-existent, @@ -5234,6 +5344,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); // CALL would charge 25,000 gas when the destination is non-existent, @@ -5316,6 +5427,7 @@ mod test { &precompiles, gas_price, None, + empty_access_list(), ); set_balance(&mut handler, &caller, U256::from(100_000_u32)); diff --git a/etherlink/kernel_latest/evm_execution/src/lib.rs b/etherlink/kernel_latest/evm_execution/src/lib.rs index 585da7a96ae4e0973ef20cccbb15cce1cf1f61a6..75624b0d26ce848a18a0e402330e0882be047bd4 100755 --- a/etherlink/kernel_latest/evm_execution/src/lib.rs +++ b/etherlink/kernel_latest/evm_execution/src/lib.rs @@ -17,7 +17,7 @@ use evm::executor::stack::PrecompileFailure; use handler::{EvmHandler, ExecutionOutcome, ExecutionResult}; use host::{path::RefPath, runtime::RuntimeError}; use primitive_types::{H160, H256, U256}; -use tezos_ethereum::block::BlockConstants; +use tezos_ethereum::{access_list::AccessList, block::BlockConstants}; use tezos_evm_logging::{log, Level::*}; use tezos_evm_runtime::runtime::Runtime; use tezos_smart_rollup_storage::StorageError; @@ -278,6 +278,7 @@ pub fn run_transaction<'a, Host>( value: U256, pay_for_gas: bool, tracer: Option, + access_list: AccessList, ) -> Result, EthereumError> where Host: Runtime, @@ -300,6 +301,7 @@ where precompiles, effective_gas_price, tracer, + access_list, ); let call_data_for_tracing = if tracer.is_some() { @@ -478,6 +480,7 @@ mod test { use primitive_types::{H160, H256}; use std::str::FromStr; use std::vec; + use tezos_ethereum::access_list::empty_access_list; use tezos_ethereum::block::BlockFees; use tezos_ethereum::tx_common::EthereumTransactionCommon; use tezos_evm_runtime::runtime::MockKernelHost; @@ -653,6 +656,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let expected_result = Ok(Some(ExecutionOutcome { @@ -715,6 +719,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let expected_result = Ok(Some(ExecutionOutcome { @@ -771,6 +776,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let expected_result = Ok(Some(ExecutionOutcome { @@ -824,6 +830,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let new_address = @@ -859,6 +866,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); assert!(result2.is_ok(), "execution should have succeeded"); let result = result2.unwrap(); @@ -890,6 +898,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); assert!(result3.is_ok(), "execution should have succeeded"); let result = result3.unwrap(); @@ -920,6 +929,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); assert!(result2.is_ok(), "execution should have succeeded"); let result = result2.unwrap(); @@ -977,6 +987,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); assert!(result.is_ok()); @@ -1027,6 +1038,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let expected_result = Ok(Some(ExecutionOutcome { @@ -1077,6 +1089,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000; // base cost @@ -1220,6 +1233,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -1280,6 +1294,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -1339,6 +1354,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); // Assert @@ -1393,6 +1409,7 @@ mod test { U256::from(100), true, None, + empty_access_list(), ); let expected_result = Ok(Some(ExecutionOutcome { @@ -1450,6 +1467,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -1507,6 +1525,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); // Assert @@ -1619,6 +1638,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); // Assert @@ -1697,6 +1717,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -1787,6 +1808,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -1899,6 +1921,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let log_record1 = Log { @@ -2008,6 +2031,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let log_record1 = Log { @@ -2107,6 +2131,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost + 7624; // execution gas cost (taken at face value from tests) @@ -2221,6 +2246,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_result = Ok(Some(ExecutionOutcome { @@ -2316,6 +2342,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -2398,6 +2425,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let expected_gas = 21000 // base cost @@ -2476,6 +2504,7 @@ mod test { U256::from(100), true, None, + empty_access_list(), ); let expected_result = Err(EthereumError::EthereumAccountError( @@ -2525,6 +2554,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let result = unwrap_outcome!(result); @@ -2583,6 +2613,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let result = unwrap_outcome!(&result, false); @@ -2655,6 +2686,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); // Assert @@ -2724,6 +2756,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); // Assert @@ -2786,6 +2819,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ) } @@ -2888,6 +2922,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let result = unwrap_outcome!(&result, false); @@ -2968,6 +3003,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let result_init = unwrap_outcome!(&result_init, true); @@ -3075,6 +3111,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let result_init = unwrap_outcome!(&result_init, true); @@ -3153,6 +3190,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let result = unwrap_outcome!(&result, false); @@ -3198,6 +3236,7 @@ mod test { transaction_value, true, None, + empty_access_list(), ); let result = unwrap_outcome!(result); @@ -3251,6 +3290,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); let path = account_path(&caller).unwrap(); @@ -3338,6 +3378,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ); // Get info on contract that should not be created @@ -3460,6 +3501,7 @@ mod test { U256::zero(), false, None, + empty_access_list(), ); unwrap_outcome!(result, true); @@ -3543,6 +3585,7 @@ mod test { U256::zero(), false, None, + empty_access_list(), ); unwrap_outcome!(result, false); @@ -3580,6 +3623,7 @@ mod test { U256::zero(), false, None, + empty_access_list(), ); let internal_address_nonce = @@ -3658,6 +3702,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ) .unwrap() .unwrap(); @@ -3746,6 +3791,7 @@ mod test { U256::zero(), true, None, + empty_access_list(), ) .unwrap() .unwrap(); @@ -3804,6 +3850,7 @@ mod test { U256::zero(), false, None, + empty_access_list(), ); // The origin address is empty but when you start a transaction the nonce is bump diff --git a/etherlink/kernel_latest/evm_execution/src/precompiles/fa_bridge.rs b/etherlink/kernel_latest/evm_execution/src/precompiles/fa_bridge.rs index 5c9e1960fd17c63dddd37b0c396311354782c961..f69efaa2d535f61348904285c202ae836e278bbe 100644 --- a/etherlink/kernel_latest/evm_execution/src/precompiles/fa_bridge.rs +++ b/etherlink/kernel_latest/evm_execution/src/precompiles/fa_bridge.rs @@ -236,6 +236,7 @@ mod tests { use evm::ExitError; use primitive_types::{H160, U256}; use tezos_data_encoding::enc::BinWriter; + use tezos_ethereum::access_list::empty_access_list; use tezos_evm_runtime::runtime::MockKernelHost; use crate::{ @@ -282,6 +283,7 @@ mod tests { &precompiles, U256::from(21000), None, + empty_access_list(), ); if disable_reentrancy_guard { @@ -408,6 +410,7 @@ mod tests { &precompiles, U256::from(21000), None, + empty_access_list(), ); handler diff --git a/etherlink/kernel_latest/evm_execution/src/precompiles/mod.rs b/etherlink/kernel_latest/evm_execution/src/precompiles/mod.rs index 9a985fcecb51419b8a3a11a792f1757902f7dc58..669ad1bc16cfea01d75740b29b69e1d82e027fbf 100644 --- a/etherlink/kernel_latest/evm_execution/src/precompiles/mod.rs +++ b/etherlink/kernel_latest/evm_execution/src/precompiles/mod.rs @@ -299,6 +299,7 @@ mod test_helpers { use evm::Transfer; use host::runtime::Runtime; use primitive_types::{H160, U256}; + use tezos_ethereum::access_list::empty_access_list; use tezos_ethereum::block::BlockConstants; use tezos_ethereum::block::BlockFees; use tezos_evm_runtime::runtime::MockKernelHost; @@ -377,6 +378,7 @@ mod test_helpers { &precompiles, gas_price, None, + empty_access_list(), ); let value = transfer.map(|t| t.value); diff --git a/etherlink/kernel_latest/kernel/src/apply.rs b/etherlink/kernel_latest/kernel/src/apply.rs index cf8285bdb2394120fffb493ad96f5ac46bab827c..a91cf6c6520c5335968675c2b0e7a78b3ae5e1c7 100644 --- a/etherlink/kernel_latest/kernel/src/apply.rs +++ b/etherlink/kernel_latest/kernel/src/apply.rs @@ -345,6 +345,7 @@ fn apply_ethereum_transaction_common( value, true, tracer_input, + transaction.access_list.clone(), ) { Ok(outcome) => outcome, Err(err) => { diff --git a/etherlink/kernel_latest/kernel/src/simulation.rs b/etherlink/kernel_latest/kernel/src/simulation.rs index 064eead08e8134464e3f28c71d0159ad47e00e4d..0bfea78cb65172abdae295bc4f627c4854beed48 100644 --- a/etherlink/kernel_latest/kernel/src/simulation.rs +++ b/etherlink/kernel_latest/kernel/src/simulation.rs @@ -33,6 +33,7 @@ use evm_execution::{ use evm_execution::{run_transaction, EthereumError}; use primitive_types::{H160, U256}; use rlp::{Decodable, DecoderError, Encodable, Rlp}; +use tezos_ethereum::access_list::empty_access_list; use tezos_ethereum::block::{BlockConstants, BlockFees}; use tezos_ethereum::rlp_helpers::{ append_option_u64_le, check_list, decode_field, decode_option, decode_option_u64_le, @@ -485,6 +486,8 @@ impl Evaluation { self.value.unwrap_or_default(), false, tracer_input, + // TODO: Replace this by the decoded access lists if any. + empty_access_list(), ) { Ok(Some(outcome)) if !self.with_da_fees => { let result: SimulationResult = @@ -782,6 +785,7 @@ mod tests { transaction_value, false, None, + empty_access_list(), ); assert!(outcome.is_ok(), "contract should have been created"); let outcome = outcome.unwrap(); diff --git a/etherlink/kernel_latest/solidity_examples/eip2930_storage_access.sol b/etherlink/kernel_latest/solidity_examples/eip2930_storage_access.sol new file mode 100644 index 0000000000000000000000000000000000000000..a106577994f6e37a3be5fbdbfa8d54ff09a08a4a --- /dev/null +++ b/etherlink/kernel_latest/solidity_examples/eip2930_storage_access.sol @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Functori +// +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +contract StorageAccess { + uint256 public value = 1; + + function setValue(uint256 newValue) public { + value = newValue; + } +} diff --git a/etherlink/tezt/lib/solidity_contracts.ml b/etherlink/tezt/lib/solidity_contracts.ml index 4a0a6ebfea9b29974bccf8759843da2158260787..c34cbeaeb570bc4d2f34af560c882f1993ed6d8d 100644 --- a/etherlink/tezt/lib/solidity_contracts.ml +++ b/etherlink/tezt/lib/solidity_contracts.ml @@ -432,6 +432,12 @@ let incrementor_proxy = ~label:"proxy" ~contract:"Proxy" +let eip2930_storage_access = + compile_contract + ~source:(solidity_contracts_path ^ "/eip2930_storage_access.sol") + ~label:"storageaccess" + ~contract:"StorageAccess" + module Precompile = struct let withdrawal = "0xff00000000000000000000000000000000000001" diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index d3e5fd426812218a27e55402d04b3d377bd66ea1..bf0d01c7a4bc77e536530fb6e07bb5d9372dc7e9 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -13474,6 +13474,92 @@ let test_fa_deposit_can_be_claimed = unit +let test_eip2930_storage_access = + register_all + ~kernels:[Latest] + ~tags:["evm"; "eip2930"] + ~title:"Check EIP-2930's semantic correctness" + ~da_fee:Wei.zero + ~time_between_blocks:Nothing + @@ fun {sequencer; evm_version; _} _protocol -> + let whale = Eth_account.bootstrap_accounts.(0) in + let* eip2930_storage_access = + Solidity_contracts.eip2930_storage_access evm_version + in + let* eip2930_contract, _ = + send_transaction_to_sequencer + (Eth_cli.deploy + ~source_private_key:whale.private_key + ~endpoint:(Evm_node.endpoint sequencer) + ~abi:eip2930_storage_access.abi + ~bin:eip2930_storage_access.bin) + sequencer + in + let* gas_price = Rpc.get_gas_price sequencer in + let gas_price = Int32.to_int gas_price in + let base_tx ~nonce ~access_list ~arg = + Cast.craft_tx + ~signature:"setValue(uint256)" + ~source_private_key:whale.private_key + ~chain_id:1337 + ~nonce + ~gas:100_000 + ~gas_price + ~value:Wei.zero + ~access_list + ~address:eip2930_contract + ~arguments:[arg] + ~legacy:false + () + in + let* raw_tx_with_storage_slot_access_list = + base_tx + ~nonce:1 + ~access_list: + [ + ( eip2930_contract, + (* slot 0 = [uint256 public value] in [eip2930_storage_access.sol] *) + [ + "0x0000000000000000000000000000000000000000000000000000000000000000"; + ] ); + ] + ~arg:"42" + in + let* raw_tx_without_storage_slot_access_list = + base_tx ~nonce:2 ~access_list:[(eip2930_contract, [])] ~arg:"43" + in + let*@ tx_with_storage_slot_access_list_hash = + Rpc.send_raw_transaction + ~raw_tx:raw_tx_with_storage_slot_access_list + sequencer + in + let* _ = produce_block sequencer in + let*@ tx_without_storage_slot_access_list_hash = + Rpc.send_raw_transaction + ~raw_tx:raw_tx_without_storage_slot_access_list + sequencer + in + let* _ = produce_block sequencer in + let*@! Transaction.{gasUsed = gas_with_storage_slot_access_list; _} = + Rpc.get_transaction_receipt + ~tx_hash:tx_with_storage_slot_access_list_hash + sequencer + in + let*@! Transaction.{gasUsed = gas_without_storage_slot_access_list; _} = + Rpc.get_transaction_receipt + ~tx_hash:tx_without_storage_slot_access_list_hash + sequencer + in + Check.( + Int64.( + sub gas_without_storage_slot_access_list gas_with_storage_slot_access_list + = of_int 200) + int64) + ~error_msg: + "The gas consumption with the preheated slot should have saved exactly \ + %R but got %L" ; + unit + let protocols = Protocol.all let () = @@ -13659,4 +13745,5 @@ let () = test_tezlink_hash_rpc [Alpha] ; test_tezlink_chain_id [Alpha] ; test_tezlink_bootstrapped [Alpha] ; - test_fa_deposit_can_be_claimed [Alpha] + test_fa_deposit_can_be_claimed [Alpha] ; + test_eip2930_storage_access [Alpha]