From 7ad4b859d39f520e11c426f9d3f353cbd942be7b Mon Sep 17 00:00:00 2001 From: Pierrick Couderc Date: Tue, 14 May 2024 17:42:47 +0200 Subject: [PATCH 01/11] EVM/Kernel: tracer input type and encoding --- etherlink/kernel_evm/evm_execution/src/lib.rs | 1 + .../kernel_evm/evm_execution/src/trace.rs | 45 +++++++++++++++++++ etherlink/kernel_evm/kernel/src/storage.rs | 18 ++++++++ 3 files changed, 64 insertions(+) create mode 100644 etherlink/kernel_evm/evm_execution/src/trace.rs diff --git a/etherlink/kernel_evm/evm_execution/src/lib.rs b/etherlink/kernel_evm/evm_execution/src/lib.rs index b0f4d70d4b51..45d40bb80923 100644 --- a/etherlink/kernel_evm/evm_execution/src/lib.rs +++ b/etherlink/kernel_evm/evm_execution/src/lib.rs @@ -27,6 +27,7 @@ pub mod handler; pub mod precompiles; pub mod storage; pub mod tick_model_opcodes; +pub mod trace; pub mod transaction; pub mod transaction_layer_data; pub mod utilities; diff --git a/etherlink/kernel_evm/evm_execution/src/trace.rs b/etherlink/kernel_evm/evm_execution/src/trace.rs new file mode 100644 index 000000000000..03e4a6a57686 --- /dev/null +++ b/etherlink/kernel_evm/evm_execution/src/trace.rs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Functori +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use primitive_types::H256; +use rlp::{Decodable, DecoderError, Rlp}; +use tezos_ethereum::rlp_helpers::{check_list, decode_field, next}; + +#[derive(Debug, Clone, Copy)] +pub struct TracerConfig { + pub enable_memory: bool, + pub enable_return_data: bool, + pub disable_stack: bool, + pub disable_storage: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct TracerInput { + pub transaction_hash: H256, + pub config: TracerConfig, +} + +impl Decodable for TracerInput { + fn decode(decoder: &Rlp) -> Result { + check_list(decoder, 5)?; + + let mut it = decoder.iter(); + let transaction_hash = decode_field(&next(&mut it)?, "transaction_hash")?; + let enable_memory = decode_field(&next(&mut it)?, "enable_memory")?; + let enable_return_data = decode_field(&next(&mut it)?, "enable_return_data")?; + let disable_stack = decode_field(&next(&mut it)?, "disable_stack")?; + let disable_storage = decode_field(&next(&mut it)?, "disable_storage")?; + + Ok(TracerInput { + transaction_hash, + config: TracerConfig { + enable_return_data, + enable_memory, + disable_stack, + disable_storage, + }, + }) + } +} diff --git a/etherlink/kernel_evm/kernel/src/storage.rs b/etherlink/kernel_evm/kernel/src/storage.rs index 590c7f648d44..7bb69e418945 100644 --- a/etherlink/kernel_evm/kernel/src/storage.rs +++ b/etherlink/kernel_evm/kernel/src/storage.rs @@ -12,6 +12,7 @@ use crate::simulation::SimulationResult; use anyhow::Context; use evm_execution::account_storage::EthereumAccount; use evm_execution::storage::blocks::add_new_block_hash; +use evm_execution::trace::TracerInput; use tezos_crypto_rs::hash::{ContractKt1Hash, HashTrait}; use tezos_evm_logging::{log, Level::*}; use tezos_smart_rollup_core::MAX_FILE_CHUNK_SIZE; @@ -138,6 +139,9 @@ pub const WORD_SIZE: usize = 32usize; // at this path, the kernel is in proxy mode. pub const SEQUENCER: RefPath = RefPath::assert_from(b"/evm/sequencer"); +// Path where the input for the tracer is stored by the sequencer. +const TRACER_INPUT: RefPath = RefPath::assert_from(b"/evm/trace/input"); + pub fn store_read_slice( host: &Host, path: &T, @@ -1065,6 +1069,20 @@ pub fn delayed_inbox_min_levels(host: &Host) -> anyhow::Result( + host: &mut Host, +) -> anyhow::Result> { + if let Some(ValueType::Value) = host.store_has(&TRACER_INPUT).map_err(Error::from)? { + let bytes = host + .store_read_all(&TRACER_INPUT) + .context("Cannot read tracer input")?; + + Ok(Some(FromRlpBytes::from_rlp_bytes(&bytes)?)) + } else { + Ok(None) + } +} + #[cfg(test)] mod internal_for_tests { use super::*; -- GitLab From 8345b56b551f0b348c5d57b0f92866c77403518e Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Thu, 23 May 2024 15:01:14 +0200 Subject: [PATCH 02/11] EVM/Exec: add struct logs tracer items and their encodings --- .../kernel_evm/evm_execution/src/trace.rs | 66 ++++++++++++++++++- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/etherlink/kernel_evm/evm_execution/src/trace.rs b/etherlink/kernel_evm/evm_execution/src/trace.rs index 03e4a6a57686..990f9e30786d 100644 --- a/etherlink/kernel_evm/evm_execution/src/trace.rs +++ b/etherlink/kernel_evm/evm_execution/src/trace.rs @@ -3,9 +3,11 @@ // // SPDX-License-Identifier: MIT -use primitive_types::H256; -use rlp::{Decodable, DecoderError, Rlp}; -use tezos_ethereum::rlp_helpers::{check_list, decode_field, next}; +use primitive_types::{H160, H256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; +use tezos_ethereum::rlp_helpers::{ + append_u16_le, append_u64_le, check_list, decode_field, next, +}; #[derive(Debug, Clone, Copy)] pub struct TracerConfig { @@ -43,3 +45,61 @@ impl Decodable for TracerInput { }) } } + +#[derive(Clone, PartialEq, Debug)] +pub struct StorageMapItem { + pub address: H160, + pub index: H256, + pub value: H256, +} + +impl Encodable for StorageMapItem { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(3); + stream.append(&self.address); + stream.append(&self.index); + stream.append(&self.value); + } +} + +#[derive(PartialEq, Debug)] +pub struct StructLog { + pub pc: u64, + pub opcode: u8, + pub gas: u64, + pub gas_cost: u64, + pub depth: u16, + pub error: Vec, + pub stack: Option>, + pub return_data: Option>, + pub memory: Option>, + pub storage: Option>, +} + +impl Encodable for StructLog { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(10); + append_u64_le(stream, &self.pc); + stream.append(&self.opcode); + append_u64_le(stream, &self.gas); + append_u64_le(stream, &self.gas_cost); + append_u16_le(stream, &self.depth); + stream.append(&self.error); + match &self.stack { + Some(stack) => stream.append_list(stack), + None => stream.append_empty_data(), + }; + match &self.return_data { + Some(return_data) => stream.append(return_data), + None => stream.append_empty_data(), + }; + match &self.memory { + Some(memory) => stream.append(memory), + None => stream.append_empty_data(), + }; + match &self.storage { + Some(storage) => stream.append_list(storage), + None => stream.append_empty_data(), + }; + } +} -- GitLab From fda18586b26b9d26dd15bc211fb6d501f70fb758 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Thu, 23 May 2024 15:04:57 +0200 Subject: [PATCH 03/11] EVM/Exec: unit tests to decode tracing structures --- .../kernel_evm/evm_execution/src/trace.rs | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/etherlink/kernel_evm/evm_execution/src/trace.rs b/etherlink/kernel_evm/evm_execution/src/trace.rs index 990f9e30786d..0aa463534c84 100644 --- a/etherlink/kernel_evm/evm_execution/src/trace.rs +++ b/etherlink/kernel_evm/evm_execution/src/trace.rs @@ -8,6 +8,11 @@ use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; use tezos_ethereum::rlp_helpers::{ append_u16_le, append_u64_le, check_list, decode_field, next, }; +#[cfg(test)] +use tezos_ethereum::rlp_helpers::{ + decode_field_u16_le, decode_field_u64_le, decode_list, decode_option, + decode_option_explicit, +}; #[derive(Debug, Clone, Copy)] pub struct TracerConfig { @@ -62,6 +67,29 @@ impl Encodable for StorageMapItem { } } +#[cfg(test)] +impl Decodable for StorageMapItem { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(3) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let address: H160 = decode_field(&next(&mut it)?, "address")?; + let index: H256 = decode_field(&next(&mut it)?, "index")?; + let value: H256 = decode_field(&next(&mut it)?, "value")?; + + Ok(Self { + address, + index, + value, + }) + } +} + #[derive(PartialEq, Debug)] pub struct StructLog { pub pc: u64, @@ -103,3 +131,92 @@ impl Encodable for StructLog { }; } } + +#[cfg(test)] +impl Decodable for StructLog { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(10) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let pc: u64 = decode_field_u64_le(&next(&mut it)?, "pc")?; + let opcode: u8 = decode_field(&next(&mut it)?, "opcode")?; + let gas: u64 = decode_field_u64_le(&next(&mut it)?, "gas")?; + let gas_cost: u64 = decode_field_u64_le(&next(&mut it)?, "gas")?; + let depth: u16 = decode_field_u16_le(&next(&mut it)?, "depth")?; + let error: Vec = decode_field(&next(&mut it)?, "error")?; + let stack: Option> = + decode_option_explicit(&next(&mut it)?, "stack", decode_list)?; + let return_data: Option> = decode_option(&next(&mut it)?, "return_data")?; + let memory: Option> = decode_option(&next(&mut it)?, "memory")?; + let storage: Option> = + decode_option_explicit(&next(&mut it)?, "storage", decode_list)?; + + Ok(Self { + pc, + opcode, + gas, + gas_cost, + depth, + error, + stack, + return_data, + memory, + storage, + }) + } +} + +#[cfg(test)] +pub mod tests { + use primitive_types::{H160, H256}; + + use super::{StorageMapItem, StructLog}; + + #[test] + fn rlp_encode_decode_storage_map_item() { + let storage_map_item = StorageMapItem { + address: H160::from([25; 20]), + index: H256::from([11; 32]), + value: H256::from([97; 32]), + }; + + let encoded = rlp::encode(&storage_map_item); + let decoded: StorageMapItem = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + assert_eq!(storage_map_item, decoded) + } + + #[test] + fn rlp_encode_decode_struct_log() { + let storage_map_item = StorageMapItem { + address: H160::from([25; 20]), + index: H256::from([11; 32]), + value: H256::from([97; 32]), + }; + + let struct_log = StructLog { + pc: 25, + opcode: 11, + gas: 97, + gas_cost: 100, + depth: 3, + error: vec![25, 11, 97], + stack: Some(vec![H256::from([33; 32]), H256::from([35; 32])]), + return_data: Some(vec![25, 11, 97]), + memory: Some(vec![25, 11, 97]), + storage: Some(vec![storage_map_item]), + }; + + let encoded = rlp::encode(&struct_log); + let decoded: StructLog = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + assert_eq!(struct_log, decoded) + } +} -- GitLab From 07f818360ce07cda51d28633d6748c92c2f684f3 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Thu, 2 May 2024 16:24:16 +0200 Subject: [PATCH 04/11] EVM/Kernel: enable the tracer configuration to run transactions when available --- .../kernel_evm/evm_evaluation/src/runner.rs | 1 + etherlink/kernel_evm/evm_execution/src/lib.rs | 48 +++++++++++++++++++ .../evm_execution/src/precompiles/mod.rs | 1 + etherlink/kernel_evm/kernel/src/apply.rs | 27 +++++++++++ etherlink/kernel_evm/kernel/src/block.rs | 30 ++++++++++++ etherlink/kernel_evm/kernel/src/lib.rs | 9 +++- etherlink/kernel_evm/kernel/src/simulation.rs | 3 ++ 7 files changed, 117 insertions(+), 2 deletions(-) diff --git a/etherlink/kernel_evm/evm_evaluation/src/runner.rs b/etherlink/kernel_evm/evm_evaluation/src/runner.rs index ef2c0cb41c06..ab797052b096 100644 --- a/etherlink/kernel_evm/evm_evaluation/src/runner.rs +++ b/etherlink/kernel_evm/evm_evaluation/src/runner.rs @@ -241,6 +241,7 @@ fn execute_transaction( u64::MAX, // don't account for ticks during the test false, true, + None, ) } diff --git a/etherlink/kernel_evm/evm_execution/src/lib.rs b/etherlink/kernel_evm/evm_execution/src/lib.rs index 45d40bb80923..1d6408b95e0e 100644 --- a/etherlink/kernel_evm/evm_execution/src/lib.rs +++ b/etherlink/kernel_evm/evm_execution/src/lib.rs @@ -40,6 +40,7 @@ extern crate tezos_smart_rollup_debug as debug; extern crate tezos_smart_rollup_host as host; use precompiles::PrecompileSet; +use trace::TracerConfig; use crate::handler::ExtendedExitReason; @@ -154,6 +155,7 @@ pub fn run_transaction<'a, Host>( allocated_ticks: u64, retriable: bool, enable_warm_cold_access: bool, + tracer: Option, ) -> Result, EthereumError> where Host: Runtime, @@ -421,6 +423,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_result = Ok(Some(ExecutionOutcome { @@ -488,6 +491,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_result = Ok(Some(ExecutionOutcome { @@ -549,6 +553,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_result = Ok(Some(ExecutionOutcome { @@ -607,6 +612,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let new_address = @@ -643,6 +649,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); assert!(result2.is_ok(), "execution should have succeeded"); let result = result2.unwrap(); @@ -673,6 +680,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); assert!(result3.is_ok(), "execution should have succeeded"); let result = result3.unwrap(); @@ -702,6 +710,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); assert!(result2.is_ok(), "execution should have succeeded"); let result = result2.unwrap(); @@ -758,6 +767,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); assert!(result.is_ok()); @@ -810,6 +820,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_result = Ok(Some(ExecutionOutcome { @@ -865,6 +876,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000; // base cost @@ -1013,6 +1025,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -1078,6 +1091,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -1142,6 +1156,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // Assert @@ -1201,6 +1216,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_result = Ok(Some(ExecutionOutcome { @@ -1261,6 +1277,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -1323,6 +1340,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // Assert @@ -1437,6 +1455,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // Assert @@ -1517,6 +1536,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -1612,6 +1632,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -1729,6 +1750,7 @@ mod test { 1_000_000_000, false, false, + None, ); let log_record1 = Log { @@ -1843,6 +1865,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let log_record1 = Log { @@ -1946,6 +1969,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost + 5124; // execution gas cost (taken at face value from tests) @@ -2066,11 +2090,13 @@ mod test { 10_000_000_000, false, false, + None, ); let expected_result = Ok(Some(ExecutionOutcome { gas_used: all_the_gas, is_success: false, + reason: ExitReason::Error(ExitError::InvalidCode(Opcode::INVALID)).into(), new_address: None, logs: vec![], @@ -2161,6 +2187,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -2240,6 +2267,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_gas = 21000 // base cost @@ -2319,6 +2347,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let expected_result = Err(EthereumError::EthereumAccountError( @@ -2370,6 +2399,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let result = unwrap_outcome!(result); @@ -2427,6 +2457,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let result = unwrap_outcome!(&result, false); @@ -2494,6 +2525,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // Assert @@ -2558,6 +2590,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // Assert @@ -2616,6 +2649,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ) } @@ -2722,6 +2756,7 @@ mod test { 10_000_000_000, false, false, + None, ); let result = unwrap_outcome!(&result, false); @@ -2804,6 +2839,7 @@ mod test { 10_000_000_000, false, false, + None, ); let result_init = unwrap_outcome!(&result_init, true); @@ -2913,6 +2949,7 @@ mod test { 10_000_000_000, false, false, + None, ); let result_init = unwrap_outcome!(&result_init, true); @@ -2993,6 +3030,7 @@ mod test { 10_000_000_000, false, false, + None, ); let result = unwrap_outcome!(&result, false); @@ -3040,6 +3078,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let result = unwrap_outcome!(result); @@ -3096,6 +3135,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); let path = account_path(&caller).unwrap(); @@ -3185,6 +3225,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // Get info on contract that should not be created @@ -3283,6 +3324,7 @@ mod test { 10_000, retriable, false, + None, ); (host, result, account, initial_balance, initial_caller_nonce) @@ -3413,6 +3455,7 @@ mod test { u64::MAX, false, false, + None, ); unwrap_outcome!(result, true); @@ -3498,6 +3541,7 @@ mod test { u64::MAX, false, false, + None, ); unwrap_outcome!(result, false); @@ -3536,6 +3580,7 @@ mod test { u64::MAX, false, false, + None, ); let internal_address_nonce = @@ -3616,6 +3661,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 100, false, false, + None, ) .unwrap() .unwrap(); @@ -3706,6 +3752,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ) .unwrap() .unwrap(); @@ -3766,6 +3813,7 @@ mod test { DUMMY_ALLOCATED_TICKS, false, false, + None, ); // The origin address is empty but when you start a transaction the nonce is bump diff --git a/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs b/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs index 79e44ee5016c..511cac875438 100644 --- a/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs +++ b/etherlink/kernel_evm/evm_execution/src/precompiles/mod.rs @@ -316,6 +316,7 @@ mod test_helpers { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let is_static = true; diff --git a/etherlink/kernel_evm/kernel/src/apply.rs b/etherlink/kernel_evm/kernel/src/apply.rs index 05babbca3918..0ed40f8fc731 100644 --- a/etherlink/kernel_evm/kernel/src/apply.rs +++ b/etherlink/kernel_evm/kernel/src/apply.rs @@ -13,6 +13,7 @@ use evm_execution::account_storage::{ use evm_execution::handler::{ExecutionOutcome, ExtendedExitReason}; use evm_execution::precompiles::PrecompileBTreeMap; use evm_execution::run_transaction; +use evm_execution::trace::{TracerConfig, TracerInput}; use primitive_types::{H160, U256}; use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_ethereum::block::BlockConstants; @@ -337,6 +338,7 @@ fn apply_ethereum_transaction_common( retriable: bool, is_delayed: bool, limits: &Limits, + tracer_config: Option, ) -> Result, anyhow::Error> { let effective_gas_price = block_constants.base_fee_per_gas(); let (caller, gas_limit) = match is_valid_ethereum_transaction_common( @@ -375,6 +377,7 @@ fn apply_ethereum_transaction_common( allocated_ticks, retriable, false, + tracer_config, ) { Ok(outcome) => outcome, Err(err) => { @@ -611,6 +614,25 @@ pub fn handle_transaction_result( }) } +fn get_tracer_config( + current_transaction_hash: TransactionHash, + trace_input: &Option, +) -> Option { + if let Some(TracerInput { + transaction_hash, + config, + }) = trace_input + { + if transaction_hash.0 == current_transaction_hash { + Some(*config) + } else { + None + } + } else { + None + } +} + #[allow(clippy::too_many_arguments)] pub fn apply_transaction( host: &mut Host, @@ -626,7 +648,10 @@ pub fn apply_transaction( ticketer: &Option, sequencer_pool_address: Option, limits: &Limits, + trace_input: &Option, ) -> Result, anyhow::Error> { + let current_transaction_hash = transaction.tx_hash; + let tracer_config = get_tracer_config(current_transaction_hash, trace_input); let apply_result = match &transaction.content { TransactionContent::Ethereum(tx) => apply_ethereum_transaction_common( host, @@ -638,6 +663,7 @@ pub fn apply_transaction( retriable, false, limits, + tracer_config, )?, TransactionContent::EthereumDelayed(tx) => apply_ethereum_transaction_common( host, @@ -649,6 +675,7 @@ pub fn apply_transaction( retriable, true, limits, + tracer_config, )?, TransactionContent::Deposit(deposit) => { log!(host, Benchmarking, "Transaction type: DEPOSIT"); diff --git a/etherlink/kernel_evm/kernel/src/block.rs b/etherlink/kernel_evm/kernel/src/block.rs index 7c68dce35a29..2a1ef576e838 100644 --- a/etherlink/kernel_evm/kernel/src/block.rs +++ b/etherlink/kernel_evm/kernel/src/block.rs @@ -27,6 +27,7 @@ use block_in_progress::BlockInProgress; use evm_execution::account_storage::{init_account_storage, EthereumAccountStorage}; use evm_execution::precompiles; use evm_execution::precompiles::PrecompileBTreeMap; +use evm_execution::trace::TracerInput; use primitive_types::{H160, H256, U256}; use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_ethereum::block::BlockFees; @@ -76,6 +77,7 @@ fn compute( ticketer: &Option, sequencer_pool_address: Option, limits: &Limits, + trace_input: &Option, ) -> Result { log!( host, @@ -134,6 +136,7 @@ fn compute( ticketer, sequencer_pool_address, limits, + trace_input, )? { ExecutionResult::Valid(ExecutionInfo { receipt_info, @@ -269,6 +272,7 @@ fn compute_bip( ticketer: &Option, sequencer_pool_address: Option, limits: &Limits, + trace_input: &Option, ) -> anyhow::Result { let result = compute( host, @@ -282,6 +286,7 @@ fn compute_bip( ticketer, sequencer_pool_address, limits, + trace_input, )?; match result { ComputationResult::RebootNeeded => { @@ -370,6 +375,7 @@ pub fn produce( block_fees: BlockFees, config: &mut Configuration, sequencer_pool_address: Option, + trace_input: Option, ) -> Result { let kernel_upgrade = upgrade::read_kernel_upgrade(host)?; @@ -427,6 +433,7 @@ pub fn produce( &config.tezos_contracts.ticketer, sequencer_pool_address, &config.limits, + &trace_input, ) { Ok(ComputationResult::Finished) => promote_block( &mut safe_host, @@ -509,6 +516,7 @@ pub fn produce( &config.tezos_contracts.ticketer, sequencer_pool_address, &config.limits, + &trace_input, ) { Ok(ComputationResult::Finished) => { promote_block(&mut safe_host, &outbox_queue, false, processed_blueprint)? @@ -779,6 +787,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); } @@ -826,6 +835,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -870,6 +880,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -917,6 +928,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); let receipt = read_transaction_receipt(&mut host, &tx_hash) @@ -997,6 +1009,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1055,6 +1068,7 @@ mod tests { dummy_block_fees, &mut Configuration::default(), None, + None, ) .expect("The block production failed."); let receipt0 = read_transaction_receipt(&mut host, &tx_hash_0) @@ -1119,6 +1133,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1179,6 +1194,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1214,6 +1230,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1226,6 +1243,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1278,6 +1296,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1371,6 +1390,7 @@ mod tests { &None, None, &limits, + &None, ) .expect("Should safely ask for a reboot"); @@ -1445,6 +1465,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); assert!( @@ -1491,6 +1512,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("Empty block should have been produced"); check_current_block_number(&mut host, 0); @@ -1504,6 +1526,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("Empty block should have been produced"); check_current_block_number(&mut host, 1); @@ -1517,6 +1540,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("Empty block should have been produced"); check_current_block_number(&mut host, 2); @@ -1659,6 +1683,7 @@ mod tests { dummy_block_fees(), &mut configuration, None, + None, ) .expect("Should have produced"); @@ -1743,6 +1768,7 @@ mod tests { dummy_block_fees(), &mut configuration, None, + None, ) .expect("Should have produced"); @@ -1837,6 +1863,7 @@ mod tests { block_fees, &mut Configuration::default(), None, + None, ) .expect("The block production failed."); @@ -1912,6 +1939,7 @@ mod tests { dummy_block_fees(), &mut configuration, None, + None, ) .expect("Should have produced"); @@ -1932,6 +1960,7 @@ mod tests { dummy_block_fees(), &mut configuration, None, + None, ) .expect("Should have produced"); @@ -2018,6 +2047,7 @@ mod tests { dummy_block_fees(), &mut Configuration::default(), None, + None, ) .expect("The block production failed."); diff --git a/etherlink/kernel_evm/kernel/src/lib.rs b/etherlink/kernel_evm/kernel/src/lib.rs index 353b2ab66d67..3ac68e3c62cb 100644 --- a/etherlink/kernel_evm/kernel/src/lib.rs +++ b/etherlink/kernel_evm/kernel/src/lib.rs @@ -23,8 +23,9 @@ use safe_storage::WORLD_STATE_PATH; use storage::{ read_base_fee_per_gas, read_chain_id, read_da_fee, read_kernel_version, read_last_info_per_level_timestamp, read_last_info_per_level_timestamp_stats, - read_minimum_base_fee_per_gas, store_base_fee_per_gas, store_chain_id, store_da_fee, - store_kernel_version, store_storage_version, STORAGE_VERSION, STORAGE_VERSION_PATH, + read_minimum_base_fee_per_gas, read_tracer_input, store_base_fee_per_gas, + store_chain_id, store_da_fee, store_kernel_version, store_storage_version, + STORAGE_VERSION, STORAGE_VERSION_PATH, }; use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_ethereum::block::BlockFees; @@ -264,6 +265,8 @@ pub fn main(host: &mut Host) -> Result<(), anyhow::Error> { }; let block_fees = retrieve_block_fees(host)?; + let trace_input = read_tracer_input(host)?; + // Start processing blueprints #[cfg(not(feature = "benchmark-bypass-stage2"))] { @@ -274,6 +277,7 @@ pub fn main(host: &mut Host) -> Result<(), anyhow::Error> { block_fees, &mut configuration, sequencer_pool_address, + trace_input, ) .context("Failed during stage 2")? { @@ -546,6 +550,7 @@ mod tests { block_fees, &mut configuration, None, + None, ) .expect("Should have produced"); diff --git a/etherlink/kernel_evm/kernel/src/simulation.rs b/etherlink/kernel_evm/kernel/src/simulation.rs index c2bacbbc1d3e..921c7ffabdba 100644 --- a/etherlink/kernel_evm/kernel/src/simulation.rs +++ b/etherlink/kernel_evm/kernel/src/simulation.rs @@ -289,6 +289,7 @@ impl Evaluation { allocated_ticks, false, false, + None, ) { Ok(Some(outcome)) => { let outcome = @@ -414,6 +415,7 @@ impl TxValidation { allocated_ticks, false, false, + None, ) { Ok(Some(ExecutionOutcome { reason: ExtendedExitReason::OutOfTicks, @@ -761,6 +763,7 @@ mod tests { DUMMY_ALLOCATED_TICKS, false, false, + None, ); assert!(outcome.is_ok(), "contract should have been created"); let outcome = outcome.unwrap(); -- GitLab From 608a04b30f5d8eca0ad8bac0113d59571c523383 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Thu, 2 May 2024 16:35:47 +0200 Subject: [PATCH 05/11] EVM/Kernel: enable tracer from Sputnik's handler --- .../kernel_evm/evm_execution/src/handler.rs | 39 ++++++++++++++++++- etherlink/kernel_evm/evm_execution/src/lib.rs | 1 + 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index 809020f8e9e2..f56d6e75fd24 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -14,12 +14,12 @@ use crate::account_storage::{ CODE_HASH_DEFAULT, }; use crate::storage::blocks::get_block_hash; -use crate::tick_model_opcodes; use crate::transaction::TransactionContext; use crate::transaction_layer_data::TransactionLayerData; use crate::utilities::create_address_legacy; use crate::EthereumError; use crate::PrecompileSet; +use crate::{tick_model_opcodes, TracerConfig}; use alloc::borrow::Cow; use alloc::rc::Rc; use core::convert::Infallible; @@ -266,6 +266,8 @@ pub struct EvmHandler<'a, Host: Runtime> { /// Whether warm/cold storage and address access is enabled /// If not, all access are considered warm pub enable_warm_cold_access: bool, + /// Tracer configuration for debugging. + tracer: Option, } impl<'a, Host: Runtime> EvmHandler<'a, Host> { @@ -281,6 +283,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { ticks_allocated: u64, effective_gas_price: U256, enable_warm_cold_access: bool, + tracer: Option, ) -> Self { Self { host, @@ -294,6 +297,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { estimated_ticks_used: 0, effective_gas_price, enable_warm_cold_access, + tracer, } } @@ -2383,6 +2387,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let result = handler @@ -2417,6 +2422,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let code_hash: H256 = CODE_HASH_DEFAULT; @@ -2459,6 +2465,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let code_hash: H256 = CODE_HASH_DEFAULT; @@ -2505,6 +2512,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(213_u64); @@ -2566,6 +2574,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(213_u64); @@ -2659,6 +2668,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let input_value = U256::from(2026_u32); @@ -2753,6 +2763,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let input_value = U256::from(1025_u32); // transaction depth for contract below is callarg - 1 @@ -2845,6 +2856,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(312); @@ -2916,6 +2928,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let value = U256::zero(); @@ -2972,6 +2985,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let value = U256::zero(); @@ -3037,6 +3051,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(117); @@ -3099,6 +3114,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(210_u64); @@ -3160,6 +3176,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(210_u64); @@ -3230,6 +3247,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let hash_of_unavailable_block = handler.block_hash(U256::zero()); @@ -3257,6 +3275,7 @@ mod test { 10_000, gas_price, false, + None, ); let address = H160::from_low_u64_be(210_u64); @@ -3317,6 +3336,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(210_u64); @@ -3368,6 +3388,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address = H160::from_low_u64_be(210_u64); @@ -3454,6 +3475,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); // { (SELFDESTRUCT 0x10) } @@ -3502,6 +3524,7 @@ mod test { DUMMY_ALLOCATED_TICKS, U256::one(), false, + None, ); set_balance(&mut handler, &caller, U256::from(10000)); @@ -3545,6 +3568,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let address_1 = H160::from_low_u64_be(210_u64); @@ -3620,6 +3644,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let hash = handler.code_hash(H160::from_low_u64_le(1)); @@ -3649,6 +3674,7 @@ mod test { DUMMY_ALLOCATED_TICKS, U256::one(), false, + None, ); set_balance(&mut handler, &caller, U256::from(1000000000)); @@ -3691,6 +3717,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 10000, gas_price, false, + None, ); let address_1 = H160::from_low_u64_be(210_u64); @@ -3773,6 +3800,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 10000, gas_price, false, + None, ); let address_1 = H160::from_low_u64_be(210_u64); @@ -3873,6 +3901,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 10000, gas_price, false, + None, ); let target_destruct = @@ -3938,6 +3967,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 10000, gas_price, true, + None, ); let contrac_addr = @@ -4008,6 +4038,7 @@ mod test { DUMMY_ALLOCATED_TICKS, U256::from(21000), false, + None, ); handler @@ -4068,6 +4099,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 1000, gas_price, false, + None, ); let address = H160::from_low_u64_be(210_u64); @@ -4155,6 +4187,7 @@ mod test { DUMMY_ALLOCATED_TICKS, gas_price, false, + None, ); let initial_code = [1; 49153]; // MAX_INIT_CODE_SIZE + 1 @@ -4193,6 +4226,7 @@ mod test { DUMMY_ALLOCATED_TICKS, U256::one(), false, + None, ); set_balance(&mut handler, &caller, U256::from(1000000000)); @@ -4233,6 +4267,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 1000, U256::from(21000), false, + None, ); let address1 = H160::from_low_u64_be(210_u64); @@ -4312,6 +4347,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 10000, gas_price, false, + None, ); // SUICIDE would charge 25,000 gas when the destination is non-existent, @@ -4383,6 +4419,7 @@ mod test { DUMMY_ALLOCATED_TICKS * 10000, gas_price, false, + None, ); // CALL would charge 25,000 gas when the destination is non-existent, diff --git a/etherlink/kernel_evm/evm_execution/src/lib.rs b/etherlink/kernel_evm/evm_execution/src/lib.rs index 1d6408b95e0e..2317128c71a4 100644 --- a/etherlink/kernel_evm/evm_execution/src/lib.rs +++ b/etherlink/kernel_evm/evm_execution/src/lib.rs @@ -180,6 +180,7 @@ where allocated_ticks, effective_gas_price, enable_warm_cold_access, + tracer, ); if (!pay_for_gas) -- GitLab From 4927c83009b9781df15f14a696ff3d8d788f27de Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Wed, 15 May 2024 14:25:10 +0200 Subject: [PATCH 06/11] EVM/Kernel: extract IndexableStorage into its own library --- etherlink/kernel_evm/Cargo.lock | 12 ++++++ etherlink/kernel_evm/Cargo.toml | 15 +++++-- .../kernel_evm/indexable_storage/Cargo.toml | 16 ++++++++ .../src/lib.rs} | 39 +++++++++++++------ etherlink/kernel_evm/kernel/Cargo.toml | 1 + etherlink/kernel_evm/kernel/src/apply.rs | 2 +- etherlink/kernel_evm/kernel/src/block.rs | 2 +- etherlink/kernel_evm/kernel/src/error.rs | 14 +++++++ etherlink/kernel_evm/kernel/src/lib.rs | 1 - etherlink/kernel_evm/kernel/src/storage.rs | 8 ++-- 10 files changed, 88 insertions(+), 22 deletions(-) create mode 100644 etherlink/kernel_evm/indexable_storage/Cargo.toml rename etherlink/kernel_evm/{kernel/src/indexable_storage.rs => indexable_storage/src/lib.rs} (84%) diff --git a/etherlink/kernel_evm/Cargo.lock b/etherlink/kernel_evm/Cargo.lock index 56c8e13526e8..f65b518c1a4c 100644 --- a/etherlink/kernel_evm/Cargo.lock +++ b/etherlink/kernel_evm/Cargo.lock @@ -615,6 +615,7 @@ dependencies = [ "sha3", "softfloat", "tezos-evm-logging", + "tezos-indexable-storage", "tezos-smart-rollup", "tezos-smart-rollup-core", "tezos-smart-rollup-debug", @@ -1944,6 +1945,17 @@ dependencies = [ "tezos-smart-rollup-debug", ] +[[package]] +name = "tezos-indexable-storage" +version = "0.1.0" +dependencies = [ + "tezos-evm-logging", + "tezos-smart-rollup-host", + "tezos-smart-rollup-mock", + "tezos-smart-rollup-storage", + "thiserror", +] + [[package]] name = "tezos-smart-rollup" version = "0.2.2" diff --git a/etherlink/kernel_evm/Cargo.toml b/etherlink/kernel_evm/Cargo.toml index 027cde4d9596..0e676b8dd758 100644 --- a/etherlink/kernel_evm/Cargo.toml +++ b/etherlink/kernel_evm/Cargo.toml @@ -8,7 +8,15 @@ [workspace] -members = ["ethereum", "fa_bridge", "kernel", "evm_execution", "evm_evaluation", "logging"] +members = [ + "ethereum", + "fa_bridge", + "kernel", + "evm_execution", + "evm_evaluation", + "indexable_storage", + "logging", +] [workspace.dependencies] @@ -51,11 +59,12 @@ fa-bridge = { path = "./fa_bridge" } tezos_ethereum = { path = "./ethereum" } evm-execution = { path = "./evm_execution" } tezos-evm-logging = { path = "./logging" } +tezos-indexable-storage = { path = "./indexable_storage" } # SDK tezos-smart-rollup = { path = "../../src/kernel_sdk/sdk" } -tezos-smart-rollup-core = { path = "../../src/kernel_sdk/core"} -tezos-smart-rollup-host = { path = "../../src/kernel_sdk/host"} +tezos-smart-rollup-core = { path = "../../src/kernel_sdk/core" } +tezos-smart-rollup-host = { path = "../../src/kernel_sdk/host" } tezos-smart-rollup-panic-hook = { path = "../../src/kernel_sdk/panic-hook" } tezos-smart-rollup-entrypoint = { path = "../../src/kernel_sdk/entrypoint" } tezos-smart-rollup-debug = { path = "../../src/kernel_sdk/debug" } diff --git a/etherlink/kernel_evm/indexable_storage/Cargo.toml b/etherlink/kernel_evm/indexable_storage/Cargo.toml new file mode 100644 index 000000000000..3d3f27fc81fa --- /dev/null +++ b/etherlink/kernel_evm/indexable_storage/Cargo.toml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2024 Functori +# +# SPDX-License-Identifier: MIT + +[package] +name = "tezos-indexable-storage" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +thiserror.workspace = true +tezos-evm-logging.workspace = true +tezos-smart-rollup-host.workspace = true +tezos-smart-rollup-mock.workspace = true +tezos-smart-rollup-storage.workspace = true diff --git a/etherlink/kernel_evm/kernel/src/indexable_storage.rs b/etherlink/kernel_evm/indexable_storage/src/lib.rs similarity index 84% rename from etherlink/kernel_evm/kernel/src/indexable_storage.rs rename to etherlink/kernel_evm/indexable_storage/src/lib.rs index 9080124d3138..93a4e8875dda 100644 --- a/etherlink/kernel_evm/kernel/src/indexable_storage.rs +++ b/etherlink/kernel_evm/indexable_storage/src/lib.rs @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Functori // // SPDX-License-Identifier: MIT -use crate::error::StorageError; use tezos_evm_logging::log; use tezos_evm_logging::Level::Error; -use tezos_smart_rollup_host::path::{concat, OwnedPath, RefPath}; +use tezos_smart_rollup_host::path::{concat, OwnedPath, PathError, RefPath}; use tezos_smart_rollup_host::runtime::{Runtime, RuntimeError}; +use tezos_smart_rollup_storage::StorageError; +use thiserror::Error; const LENGTH: RefPath = RefPath::assert_from(b"/length"); @@ -40,17 +42,29 @@ pub struct IndexableStorage { pub path: OwnedPath, } +#[derive(Error, Debug, Eq, PartialEq)] +pub enum IndexableStorageError { + #[error(transparent)] + Path(#[from] PathError), + #[error(transparent)] + Runtime(#[from] RuntimeError), + #[error(transparent)] + Storage(#[from] StorageError), + #[error("Storage error: index out of bound")] + IndexOutOfBounds, +} + impl IndexableStorage { pub fn new(path: &RefPath<'_>) -> Result { Ok(Self { path: path.into() }) } - fn value_path(&self, index: u64) -> Result { + fn value_path(&self, index: u64) -> Result { let index_as_path: Vec = format!("/{}", index).into(); // The key being an integer value, it will always be valid as a path, // `assert_from` cannot fail. let index_subkey = RefPath::assert_from(&index_as_path); - concat(&self.path, &index_subkey).map_err(StorageError::from) + concat(&self.path, &index_subkey) } fn store_index( @@ -58,16 +72,16 @@ impl IndexableStorage { host: &mut Host, index: u64, value_repr: &[u8], - ) -> Result<(), StorageError> { + ) -> Result<(), IndexableStorageError> { let key_path = self.value_path(index)?; host.store_write_all(&key_path, value_repr) - .map_err(StorageError::from) + .map_err(IndexableStorageError::from) } fn get_length_and_increment( &self, host: &mut Host, - ) -> Result { + ) -> Result { let path = concat(&self.path, &LENGTH)?; let length = read_u64(host, &path).unwrap_or(0); store_u64(host, &path, length + 1)?; @@ -109,17 +123,18 @@ impl IndexableStorage { /// Returns the value a the given index. Fails if the index is greater or /// equal to the length. - #[cfg(test)] + #[cfg(debug_assertions)] pub fn get_value( &self, host: &Host, index: u64, - ) -> Result, StorageError> { + ) -> Result, IndexableStorageError> { let length = self.length(host)?; if index >= length { - return Err(StorageError::IndexOutOfBounds); + return Err(IndexableStorageError::IndexOutOfBounds); }; self.unsafe_get_value(host, index) + .map_err(IndexableStorageError::from) } /// Push a value at index `length`, and increments the length. @@ -127,7 +142,7 @@ impl IndexableStorage { &self, host: &mut Host, value: &[u8], - ) -> Result<(), StorageError> { + ) -> Result<(), IndexableStorageError> { let new_index = self.get_length_and_increment(host)?; self.store_index(host, new_index, value) } @@ -181,7 +196,7 @@ mod tests { assert_eq!( storage.get_value(&host, 1), - Err(StorageError::IndexOutOfBounds) + Err(IndexableStorageError::IndexOutOfBounds) ) } } diff --git a/etherlink/kernel_evm/kernel/Cargo.toml b/etherlink/kernel_evm/kernel/Cargo.toml index 5deb68b2b235..071856d7cd4f 100644 --- a/etherlink/kernel_evm/kernel/Cargo.toml +++ b/etherlink/kernel_evm/kernel/Cargo.toml @@ -37,6 +37,7 @@ evm.workspace = true evm-execution.workspace = true tezos_ethereum.workspace = true tezos-evm-logging.workspace = true +tezos-indexable-storage.workspace = true tezos-smart-rollup.workspace = true tezos-smart-rollup-core.workspace = true diff --git a/etherlink/kernel_evm/kernel/src/apply.rs b/etherlink/kernel_evm/kernel/src/apply.rs index 0ed40f8fc731..6dacddc63602 100644 --- a/etherlink/kernel_evm/kernel/src/apply.rs +++ b/etherlink/kernel_evm/kernel/src/apply.rs @@ -22,6 +22,7 @@ use tezos_ethereum::tx_common::EthereumTransactionCommon; use tezos_ethereum::tx_signature::TxSignature; use tezos_ethereum::withdrawal::Withdrawal; use tezos_evm_logging::{log, Level::*}; +use tezos_indexable_storage::IndexableStorage; use tezos_smart_rollup::outbox::OutboxQueue; use tezos_smart_rollup_encoding::contract::Contract; use tezos_smart_rollup_encoding::entrypoint::Entrypoint; @@ -38,7 +39,6 @@ use crate::configuration::Limits; use crate::error::Error; use crate::fees::{tx_execution_gas_limit, FeeUpdates}; use crate::inbox::{Deposit, Transaction, TransactionContent}; -use crate::indexable_storage::IndexableStorage; use crate::storage::index_account; use crate::{tick_model, CONFIG}; diff --git a/etherlink/kernel_evm/kernel/src/block.rs b/etherlink/kernel_evm/kernel/src/block.rs index 2a1ef576e838..a833363fa170 100644 --- a/etherlink/kernel_evm/kernel/src/block.rs +++ b/etherlink/kernel_evm/kernel/src/block.rs @@ -12,7 +12,6 @@ use crate::blueprint_storage::{drop_blueprint, read_next_blueprint}; use crate::configuration::Limits; use crate::error::Error; use crate::event::Event; -use crate::indexable_storage::IndexableStorage; use crate::internal_storage::InternalRuntime; use crate::safe_storage::{KernelRuntime, SafeStorage}; use crate::storage; @@ -32,6 +31,7 @@ use primitive_types::{H160, H256, U256}; use tezos_crypto_rs::hash::ContractKt1Hash; use tezos_ethereum::block::BlockFees; use tezos_evm_logging::{log, Level::*}; +use tezos_indexable_storage::IndexableStorage; use tezos_smart_rollup::outbox::OutboxQueue; use tezos_smart_rollup_host::path::Path; use tezos_smart_rollup_host::runtime::Runtime; diff --git a/etherlink/kernel_evm/kernel/src/error.rs b/etherlink/kernel_evm/kernel/src/error.rs index e303c8c233b9..7dcf1f1b72fa 100644 --- a/etherlink/kernel_evm/kernel/src/error.rs +++ b/etherlink/kernel_evm/kernel/src/error.rs @@ -11,6 +11,7 @@ use primitive_types::U256; use rlp::DecoderError; use tezos_data_encoding::enc::BinError; use tezos_ethereum::tx_common::SigError; +use tezos_indexable_storage::IndexableStorageError; use tezos_smart_rollup_encoding::entrypoint::EntrypointError; use tezos_smart_rollup_encoding::michelson::ticket::TicketError; use tezos_smart_rollup_host::path::PathError; @@ -189,3 +190,16 @@ impl From for Error { Self::Encoding(EncodingError::Bin(e)) } } + +impl From for Error { + fn from(e: IndexableStorageError) -> Self { + match e { + IndexableStorageError::Path(e) => Error::Storage(StorageError::Path(e)), + IndexableStorageError::Runtime(e) => Error::Storage(StorageError::Runtime(e)), + IndexableStorageError::Storage(e) => Error::Storage(StorageError::Storage(e)), + IndexableStorageError::IndexOutOfBounds => { + Error::Storage(StorageError::IndexOutOfBounds) + } + } + } +} diff --git a/etherlink/kernel_evm/kernel/src/lib.rs b/etherlink/kernel_evm/kernel/src/lib.rs index 3ac68e3c62cb..4aa1f03bcb64 100644 --- a/etherlink/kernel_evm/kernel/src/lib.rs +++ b/etherlink/kernel_evm/kernel/src/lib.rs @@ -52,7 +52,6 @@ mod fallback_upgrade; mod fees; mod gas_price; mod inbox; -mod indexable_storage; mod internal_storage; mod linked_list; mod migration; diff --git a/etherlink/kernel_evm/kernel/src/storage.rs b/etherlink/kernel_evm/kernel/src/storage.rs index 7bb69e418945..0304e9c0b84a 100644 --- a/etherlink/kernel_evm/kernel/src/storage.rs +++ b/etherlink/kernel_evm/kernel/src/storage.rs @@ -7,7 +7,6 @@ use crate::block_in_progress::BlockInProgress; use crate::event::Event; -use crate::indexable_storage::IndexableStorage; use crate::simulation::SimulationResult; use anyhow::Context; use evm_execution::account_storage::EthereumAccount; @@ -15,6 +14,7 @@ use evm_execution::storage::blocks::add_new_block_hash; use evm_execution::trace::TracerInput; use tezos_crypto_rs::hash::{ContractKt1Hash, HashTrait}; use tezos_evm_logging::{log, Level::*}; +use tezos_indexable_storage::IndexableStorage; use tezos_smart_rollup_core::MAX_FILE_CHUNK_SIZE; use tezos_smart_rollup_encoding::public_key::PublicKey; use tezos_smart_rollup_encoding::timestamp::Timestamp; @@ -777,19 +777,19 @@ pub fn read_last_info_per_level_timestamp( /// Get the index of accounts. pub fn init_account_index() -> Result { let path = concat(&EVM_INDEXES, &ACCOUNTS_INDEX)?; - IndexableStorage::new(&RefPath::from(&path)) + IndexableStorage::new(&RefPath::from(&path)).map_err(StorageError::Storage) } /// Get the index of blocks. pub fn init_blocks_index() -> Result { let path = concat(&EVM_INDEXES, &BLOCKS_INDEX)?; - IndexableStorage::new(&RefPath::from(&path)) + IndexableStorage::new(&RefPath::from(&path)).map_err(StorageError::Storage) } /// Get the index of transactions pub fn init_transaction_hashes_index() -> Result { let path = concat(&EVM_INDEXES, &TRANSACTIONS_INDEX)?; - IndexableStorage::new(&RefPath::from(&path)) + IndexableStorage::new(&RefPath::from(&path)).map_err(StorageError::Storage) } pub fn index_account( -- GitLab From a0765d79bc2ce2169a37b2cdf45df6d4d352cbd0 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Wed, 15 May 2024 14:13:16 +0200 Subject: [PATCH 07/11] EVM/Exec: tracing infos storage utilities --- etherlink/kernel_evm/Cargo.lock | 1 + etherlink/kernel_evm/evm_execution/Cargo.toml | 1 + .../kernel_evm/evm_execution/src/storage.rs | 63 +++++++++++++++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/etherlink/kernel_evm/Cargo.lock b/etherlink/kernel_evm/Cargo.lock index f65b518c1a4c..8957bbe54d3b 100644 --- a/etherlink/kernel_evm/Cargo.lock +++ b/etherlink/kernel_evm/Cargo.lock @@ -566,6 +566,7 @@ dependencies = [ "sha3", "substrate-bn", "tezos-evm-logging", + "tezos-indexable-storage", "tezos-smart-rollup-core", "tezos-smart-rollup-debug", "tezos-smart-rollup-encoding", diff --git a/etherlink/kernel_evm/evm_execution/Cargo.toml b/etherlink/kernel_evm/evm_execution/Cargo.toml index 02d3ce9c9ac2..791cf643abd3 100644 --- a/etherlink/kernel_evm/evm_execution/Cargo.toml +++ b/etherlink/kernel_evm/evm_execution/Cargo.toml @@ -38,6 +38,7 @@ bn.workspace = true tezos_ethereum.workspace = true tezos-evm-logging.workspace = true +tezos-indexable-storage.workspace = true tezos-smart-rollup-core.workspace = true tezos-smart-rollup-host.workspace = true diff --git a/etherlink/kernel_evm/evm_execution/src/storage.rs b/etherlink/kernel_evm/evm_execution/src/storage.rs index d48e1d5b8b08..656e688ee5ec 100644 --- a/etherlink/kernel_evm/evm_execution/src/storage.rs +++ b/etherlink/kernel_evm/evm_execution/src/storage.rs @@ -3,12 +3,63 @@ // // SPDX-License-Identifier: MIT -//! Storage interface for EVM kernel -//! -//! This interfaces to the part of Ethereum world state we keep in durable -//! storage. It does not contain any part of storage related to the tickets. -//! This module (and transactions.rs module) should be refactored completely -//! as part of milestone . +pub mod tracer { + use host::{ + path::RefPath, + runtime::{Runtime, RuntimeError}, + }; + + use tezos_indexable_storage::IndexableStorage; + use thiserror::Error; + + use crate::trace::StructLog; + + const TRACE_GAS: RefPath = RefPath::assert_from(b"/evm/trace/gas"); + const TRACE_FAILED: RefPath = RefPath::assert_from(b"/evm/trace/failed"); + const TRACE_RETURN_VALUE: RefPath = RefPath::assert_from(b"/evm/trace/return_value"); + const TRACE_STRUCT_LOGS: RefPath = RefPath::assert_from(b"/evm/trace/struct_logs"); + + #[derive(Error, Debug, PartialEq)] + pub enum Error { + #[error("Error while tracing.")] + TracerError, + } + + pub fn store_trace_gas( + host: &mut Host, + gas: u64, + ) -> Result<(), RuntimeError> { + host.store_write_all(&TRACE_GAS, gas.to_le_bytes().as_slice()) + } + + pub fn store_trace_failed( + host: &mut Host, + status: u8, + ) -> Result<(), RuntimeError> { + host.store_write_all(&TRACE_FAILED, &[status]) + } + + pub fn store_return_value( + host: &mut Host, + value: &[u8], + ) -> Result<(), RuntimeError> { + host.store_write_all(&TRACE_RETURN_VALUE, value) + } + + pub fn store_struct_log( + host: &mut Host, + struct_log: StructLog, + ) -> Result<(), Error> { + let logs = rlp::encode(&struct_log); + + let struct_logs_storage = + IndexableStorage::new(&TRACE_STRUCT_LOGS).map_err(|_| Error::TracerError)?; + + struct_logs_storage + .push_value(host, &logs) + .map_err(|_| Error::TracerError) + } +} /// API to interact with blocks storage pub mod blocks { -- GitLab From 8b4595ee2485107a5785dfe22e0c26b64847ee6e Mon Sep 17 00:00:00 2001 From: Pierrick Couderc Date: Wed, 22 May 2024 15:16:08 +0200 Subject: [PATCH 08/11] EVM/Kernel: allow promoting the trace folder of the storage --- etherlink/kernel_evm/kernel/src/block.rs | 1 + etherlink/kernel_evm/kernel/src/safe_storage.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/etherlink/kernel_evm/kernel/src/block.rs b/etherlink/kernel_evm/kernel/src/block.rs index a833363fa170..f7516b31a51e 100644 --- a/etherlink/kernel_evm/kernel/src/block.rs +++ b/etherlink/kernel_evm/kernel/src/block.rs @@ -349,6 +349,7 @@ fn promote_block( storage::delete_block_in_progress(safe_host)?; } safe_host.promote()?; + safe_host.promote_trace()?; drop_blueprint(safe_host.host, number)?; let number = storage::read_current_block_number(safe_host.host)?; diff --git a/etherlink/kernel_evm/kernel/src/safe_storage.rs b/etherlink/kernel_evm/kernel/src/safe_storage.rs index bbe49399d346..2df7b5dcbfd9 100644 --- a/etherlink/kernel_evm/kernel/src/safe_storage.rs +++ b/etherlink/kernel_evm/kernel/src/safe_storage.rs @@ -19,6 +19,8 @@ use tezos_smart_rollup_host::{ pub const TMP_PATH: RefPath = RefPath::assert_from(b"/tmp"); pub const WORLD_STATE_PATH: RefPath = RefPath::assert_from(b"/evm/world_state"); pub const TMP_WORLD_STATE_PATH: RefPath = RefPath::assert_from(b"/tmp/evm/world_state"); +pub const TRACE_PATH: RefPath = RefPath::assert_from(b"/evm/trace"); +pub const TMP_TRACE_PATH: RefPath = RefPath::assert_from(b"/tmp/evm/trace"); // The kernel runtime requires both the standard Runtime and the // new Extended one. @@ -232,6 +234,15 @@ impl SafeStorage<&mut Host, &mut Internal> { .store_move(&TMP_WORLD_STATE_PATH, &WORLD_STATE_PATH) } + // Only used in tracing mode, so that the trace doesn't polute the world + // state but is still promoted at the end and accessible from the node. + pub fn promote_trace(&mut self) -> Result<(), RuntimeError> { + if let Ok(Some(_)) = self.host.store_has(&TMP_TRACE_PATH) { + self.host.store_move(&TMP_TRACE_PATH, &TRACE_PATH)? + } + Ok(()) + } + pub fn revert(&mut self) -> Result<(), RuntimeError> { self.host.store_delete(&TMP_PATH) } -- GitLab From 3719600601b4834c2b9208fc2a5ff2ad7eb2ac79 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Wed, 15 May 2024 15:57:02 +0200 Subject: [PATCH 09/11] Sputnik: expose a function to retrieve a dereferenced (cloned) program counter It's more convenient for the tracing part and avoid some cumbersome code. --- etherlink/sputnikvm/core/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/etherlink/sputnikvm/core/src/lib.rs b/etherlink/sputnikvm/core/src/lib.rs index 0dd187c546ca..b19d61bd153a 100644 --- a/etherlink/sputnikvm/core/src/lib.rs +++ b/etherlink/sputnikvm/core/src/lib.rs @@ -66,6 +66,12 @@ impl Machine { &self.position } + /// ONLY USED FOR TRACING + /// Return a cloned value of the program counter. + pub fn trace_position(&self) -> Result { + self.position.clone() + } + /// Create a new machine with given code and data. pub fn new( code: Rc>, -- GitLab From 0a600c80027073331bfe0cd9c6176b54f26a0a0b Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Fri, 3 May 2024 11:30:11 +0200 Subject: [PATCH 10/11] EVM/Exec: trace virtual machine from runtime The tracing was implemented in a way where the code can easily be hidden behind a feature flag. --- .../kernel_evm/evm_execution/src/handler.rs | 126 ++++++++++++++++-- etherlink/kernel_evm/evm_execution/src/lib.rs | 31 ++++- 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/etherlink/kernel_evm/evm_execution/src/handler.rs b/etherlink/kernel_evm/evm_execution/src/handler.rs index f56d6e75fd24..ab719116002d 100644 --- a/etherlink/kernel_evm/evm_execution/src/handler.rs +++ b/etherlink/kernel_evm/evm_execution/src/handler.rs @@ -14,6 +14,8 @@ use crate::account_storage::{ CODE_HASH_DEFAULT, }; use crate::storage::blocks::get_block_hash; +use crate::storage::tracer; +use crate::trace::{StorageMapItem, StructLog}; use crate::transaction::TransactionContext; use crate::transaction_layer_data::TransactionLayerData; use crate::utilities::create_address_legacy; @@ -242,7 +244,7 @@ mod benchmarks { /// The implementation of the SputnikVM [Handler] trait pub struct EvmHandler<'a, Host: Runtime> { /// The host - host: &'a mut Host, + pub host: &'a mut Host, /// The ethereum accounts storage evm_account_storage: &'a mut EthereumAccountStorage, /// The original caller initiating the toplevel transaction @@ -268,6 +270,64 @@ pub struct EvmHandler<'a, Host: Runtime> { pub enable_warm_cold_access: bool, /// Tracer configuration for debugging. tracer: Option, + /// State of the storage during a given execution. + /// NB: For now, only used for tracing. + /// + /// TODO: https://gitlab.com/tezos/tezos/-/issues/6879 + /// With a better representation (map) this could easily be + /// used for caching and optimise the VM's execution in terms + /// of speed. + storage_state: Vec, +} + +fn trace_map_error(result: Result<(), E>) -> Result<(), EthereumError> { + result.map_err(|_| { + EthereumError::WrappedError(std::borrow::Cow::Borrowed( + "Something went wrong while storing tracing informations.", + )) + }) +} + +#[allow(clippy::too_many_arguments)] +fn trace( + host: &mut Host, + tracer: &Option, + return_data: Vec, + pc: Result, + opcode: Option, + gas: u64, + gas_cost: u64, + depth: usize, + error: Vec, + stack: Vec, + memory: Vec, + storage: Vec, +) -> Result<(), EthereumError> { + if let (Some(tracer), Some(opcode), Ok(pc)) = (tracer, opcode, pc) { + let opcode = opcode.as_u8(); + let pc: u64 = pc.try_into().unwrap_or_default(); + let depth: u16 = depth.try_into().unwrap_or_default(); + let stack = tracer.disable_stack.then_some(stack); + let return_data = tracer.enable_return_data.then_some(return_data); + let memory = tracer.enable_memory.then_some(memory); + let storage = tracer.disable_storage.then_some(storage); + + let struct_log = StructLog { + pc, + opcode, + gas, + gas_cost, + depth, + error, + stack, + return_data, + memory, + storage, + }; + + trace_map_error(tracer::store_struct_log(host, struct_log))?; + } + Ok(()) } impl<'a, Host: Runtime> EvmHandler<'a, Host> { @@ -298,6 +358,7 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { effective_gas_price, enable_warm_cold_access, tracer, + storage_state: vec![], } } @@ -609,6 +670,13 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { // level. At the end of each step if the kernel takes more than the // allocated ticks the transaction is marked as failed. let opcode = runtime.machine().inspect().map(|p| p.0); + let program_counter = runtime.machine().trace_position(); + let gas_remaining = self.gas_remaining(); + let return_value = runtime.machine().return_value(); + let depth = self.transaction_data.len(); + let stack = runtime.machine().stack().data().to_owned(); + let memory = runtime.machine().memory().data()[..].to_vec(); + let storage = self.storage_state.clone(); #[cfg(feature = "benchmark-opcodes")] if let Some(opcode) = opcode { @@ -626,17 +694,47 @@ impl<'a, Host: Runtime> EvmHandler<'a, Host> { #[cfg_attr(not(feature = "benchmark-opcodes"), allow(unused_variables))] let gas_after = self.gas_used(); + let gas_cost = gas_after - gas_before; + if let Some(opcode) = opcode { - let gas = gas_after - gas_before; - self.account_for_ticks(&opcode, gas)?; + self.account_for_ticks(&opcode, gas_cost)?; #[cfg(feature = "benchmark-opcodes")] - benchmarks::end_opcode_section(self.host, gas, &step_result); + benchmarks::end_opcode_section(self.host, gas_cost, &step_result); + }; + + let error = if let Err(Capture::Exit(reason)) = &step_result { + match &reason { + ExitReason::Error(exit) => format!("{:?}", exit).as_bytes().to_vec(), + ExitReason::Fatal(exit) => format!("{:?}", exit).as_bytes().to_vec(), + _ => vec![], + } + } else { + vec![] }; + trace( + self.host, + &self.tracer, + return_value, + program_counter, + opcode, + gas_remaining, + gas_cost, + depth, + error, + stack, + memory, + storage, + )?; + match step_result { Ok(()) => (), - Err(Capture::Exit(reason)) => return Ok(reason), - Err(Capture::Trap(_)) => return Err(EthereumError::InternalTrapError), + Err(Capture::Exit(reason)) => { + return Ok(reason); + } + Err(Capture::Trap(_)) => { + return Err(EthereumError::InternalTrapError); + } } } } @@ -2000,9 +2098,21 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { let mut account = self.get_or_create_account(address).map_err(|_| { ExitError::Other(Cow::from("Could not get account for set_storage")) })?; - account + + let storage_result = account .set_storage(self.host, &index, &value) - .map_err(|_| ExitError::Other(Cow::from("Could not set_storage in handler"))) + .map_err(|_| ExitError::Other(Cow::from("Could not set_storage in handler"))); + + // Tracing + if self.tracer.is_some() { + self.storage_state.push(StorageMapItem { + address, + index, + value, + }); + } + + storage_result } fn log( diff --git a/etherlink/kernel_evm/evm_execution/src/lib.rs b/etherlink/kernel_evm/evm_execution/src/lib.rs index 2317128c71a4..bf82a3fe7b74 100644 --- a/etherlink/kernel_evm/evm_execution/src/lib.rs +++ b/etherlink/kernel_evm/evm_execution/src/lib.rs @@ -12,6 +12,7 @@ use alloc::borrow::Cow; use alloc::collections::TryReserveError; use evm::executor::stack::PrecompileFailure; use evm::ExitReason; +use handler::EvmHandler; use host::runtime::Runtime; use primitive_types::{H160, U256}; use tezos_ethereum::block::BlockConstants; @@ -42,7 +43,7 @@ extern crate tezos_smart_rollup_host as host; use precompiles::PrecompileSet; use trace::TracerConfig; -use crate::handler::ExtendedExitReason; +use crate::{handler::ExtendedExitReason, storage::tracer}; #[derive(Error, Clone, Copy, Debug, Eq, PartialEq)] pub enum DurableStorageError { @@ -124,6 +125,25 @@ pub enum EthereumError { GasToFeesUnderflow, } +fn trace_outcome( + handler: EvmHandler, + tracing: bool, + failure: u8, + result: &Option>, +) -> Result<(), StorageError> { + if tracing { + tracer::store_trace_failed(handler.host, failure) + .map_err(StorageError::RuntimeError)?; + tracer::store_trace_gas(handler.host, handler.gas_used()) + .map_err(StorageError::RuntimeError)?; + if let Some(return_value) = result { + tracer::store_return_value(handler.host, return_value) + .map_err(StorageError::RuntimeError)?; + } + } + Ok(()) +} + /// Execute an Ethereum Transaction /// /// The function returns `Err` only if something is wrong with the kernel and/or the @@ -170,6 +190,8 @@ where log!(host, Info, "Going to run an Ethereum transaction\n - from address: {}\n - to address: {:?}", caller, address); + let tracing = tracer.is_some(); + let mut handler = handler::EvmHandler::<'_, Host>::new( host, evm_account_storage, @@ -221,9 +243,14 @@ where } } + trace_outcome(handler, tracing, 0, &result.result)?; + Ok(Some(result)) } - Err(e) => Err(e), + Err(e) => { + trace_outcome(handler, tracing, 1, &None)?; + Err(e) + } } } else { // caller was unable to pay for the gas limit -- GitLab From 63fc767b308f3e5f9c313cfce8cd550e2d504751 Mon Sep 17 00:00:00 2001 From: Rodi-Can Bozman Date: Thu, 23 May 2024 10:17:19 +0200 Subject: [PATCH 11/11] EVM/Logs: add an entry to kernel's features --- etherlink/CHANGES_KERNEL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/etherlink/CHANGES_KERNEL.md b/etherlink/CHANGES_KERNEL.md index c7b7408055ee..1cb2699d9668 100644 --- a/etherlink/CHANGES_KERNEL.md +++ b/etherlink/CHANGES_KERNEL.md @@ -4,6 +4,9 @@ ### Features +- The kernel has now the ability to trace a transaction (via + the EVM runtime). (!13185) + ### Bug fixes ### Breaking changes -- GitLab