diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ea0954c5..8b85dbca5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added + +- [881](https://github.com/FuelLabs/fuel-vm/pull/881): Support for capturing execution traces. + ### Fixed - [879](https://github.com/FuelLabs/fuel-vm/pull/879): Debugger state wasn't propagated in contract contexts. - [878](https://github.com/FuelLabs/fuel-vm/pull/878): Fix the transaction de/serialization that wasn't backward compatible with the addition of the new policy. diff --git a/fuel-vm/examples/external.rs b/fuel-vm/examples/external.rs index 5cd56ae74f..c37ea2a50f 100644 --- a/fuel-vm/examples/external.rs +++ b/fuel-vm/examples/external.rs @@ -49,8 +49,8 @@ use fuel_vm::{ pub struct FileReadEcal; impl EcalHandler for FileReadEcal { - fn ecal( - vm: &mut Interpreter, + fn ecal( + vm: &mut Interpreter, a: RegId, b: RegId, c: RegId, @@ -130,8 +130,8 @@ pub struct CounterEcal { } impl EcalHandler for CounterEcal { - fn ecal( - vm: &mut Interpreter, + fn ecal( + vm: &mut Interpreter, a: RegId, _b: RegId, _c: RegId, @@ -193,8 +193,8 @@ pub struct SharedCounterEcal { } impl EcalHandler for SharedCounterEcal { - fn ecal( - vm: &mut Interpreter, + fn ecal( + vm: &mut Interpreter, a: RegId, _b: RegId, _c: RegId, diff --git a/fuel-vm/src/backtrace.rs b/fuel-vm/src/backtrace.rs index e2923a2999..e14b0a39d5 100644 --- a/fuel-vm/src/backtrace.rs +++ b/fuel-vm/src/backtrace.rs @@ -43,8 +43,8 @@ impl Backtrace { /// Create a backtrace from a vm instance and instruction result. /// /// This isn't copy-free and shouldn't be provided by default. - pub fn from_vm_error( - vm: &Interpreter, + pub fn from_vm_error( + vm: &Interpreter, result: ScriptExecutionResult, ) -> Self where diff --git a/fuel-vm/src/interpreter.rs b/fuel-vm/src/interpreter.rs index a2974a4238..7aa3a61357 100644 --- a/fuel-vm/src/interpreter.rs +++ b/fuel-vm/src/interpreter.rs @@ -67,6 +67,7 @@ mod memory; mod metadata; mod post_execution; mod receipts; +pub mod trace; mod debug; mod ecal; @@ -86,6 +87,7 @@ pub use memory::{ Memory, MemoryInstance, MemoryRange, + MemorySliceChange, }; use crate::checked_transaction::{ @@ -109,6 +111,10 @@ use self::receipts::ReceiptsCtx; #[derive(Debug, Copy, Clone, Default)] pub struct NotSupportedEcal; +/// Execution tracer is disabled. +#[derive(Debug, Copy, Clone, Default)] +pub struct NoTrace; + /// VM interpreter. /// /// The internal state of the VM isn't expose because the intended usage is to @@ -118,7 +124,7 @@ pub struct NotSupportedEcal; /// These can be obtained with the help of a [`crate::transactor::Transactor`] /// or a client implementation. #[derive(Debug, Clone)] -pub struct Interpreter { +pub struct Interpreter { registers: [Word; VM_REGISTER_COUNT], memory: M, frames: Vec, @@ -132,6 +138,7 @@ pub struct Interpreter { context: Context, balances: RuntimeBalances, profiler: Profiler, + trace: Trace, interpreter_params: InterpreterParams, /// `PanicContext` after the latest execution. It is consumed by /// `append_panic_receipt` and is `PanicContext::None` after consumption. @@ -210,21 +217,21 @@ pub(crate) enum PanicContext { ContractId(ContractId), } -impl Interpreter { +impl Interpreter { /// Returns the current state of the VM memory pub fn memory(&self) -> &MemoryInstance { self.memory.as_ref() } } -impl, S, Tx, Ecal> Interpreter { +impl, S, Tx, Ecal, Trace> Interpreter { /// Returns mutable access to the vm memory pub fn memory_mut(&mut self) -> &mut MemoryInstance { self.memory.as_mut() } } -impl Interpreter { +impl Interpreter { /// Returns the current state of the registers pub const fn registers(&self) -> &[Word] { &self.registers @@ -356,13 +363,13 @@ fn current_location( InstructionLocation::new(current_contract, offset) } -impl AsRef for Interpreter { +impl AsRef for Interpreter { fn as_ref(&self) -> &S { &self.storage } } -impl AsMut for Interpreter { +impl AsMut for Interpreter { fn as_mut(&mut self) -> &mut S { &mut self.storage } diff --git a/fuel-vm/src/interpreter/alu.rs b/fuel-vm/src/interpreter/alu.rs index b279699435..07113273bb 100644 --- a/fuel-vm/src/interpreter/alu.rs +++ b/fuel-vm/src/interpreter/alu.rs @@ -19,7 +19,7 @@ use fuel_types::{ mod muldiv; mod wideint; -impl Interpreter +impl Interpreter where Tx: ExecutableTransaction, { diff --git a/fuel-vm/src/interpreter/alu/muldiv.rs b/fuel-vm/src/interpreter/alu/muldiv.rs index 812d3753ce..422800730e 100644 --- a/fuel-vm/src/interpreter/alu/muldiv.rs +++ b/fuel-vm/src/interpreter/alu/muldiv.rs @@ -15,7 +15,7 @@ use fuel_types::{ Word, }; -impl Interpreter +impl Interpreter where Tx: ExecutableTransaction, { diff --git a/fuel-vm/src/interpreter/alu/wideint.rs b/fuel-vm/src/interpreter/alu/wideint.rs index 0fdd651a38..45b811574c 100644 --- a/fuel-vm/src/interpreter/alu/wideint.rs +++ b/fuel-vm/src/interpreter/alu/wideint.rs @@ -63,7 +63,7 @@ macro_rules! wideint_ops { $t::from_le_bytes(truncated) } - impl Interpreter + impl Interpreter where M: Memory, Tx: ExecutableTransaction, diff --git a/fuel-vm/src/interpreter/balances.rs b/fuel-vm/src/interpreter/balances.rs index ec50554f44..830fe205f6 100644 --- a/fuel-vm/src/interpreter/balances.rs +++ b/fuel-vm/src/interpreter/balances.rs @@ -166,7 +166,7 @@ impl RuntimeBalances { /// Write all assets into the start of VM stack, i.e. at $ssp. /// Panics if the assets cannot fit. - pub fn to_vm(self, vm: &mut Interpreter) + pub fn to_vm(self, vm: &mut Interpreter) where M: Memory, Tx: ExecutableTransaction, diff --git a/fuel-vm/src/interpreter/blob.rs b/fuel-vm/src/interpreter/blob.rs index 7e815407be..8571f7f7c3 100644 --- a/fuel-vm/src/interpreter/blob.rs +++ b/fuel-vm/src/interpreter/blob.rs @@ -27,7 +27,7 @@ use super::{ WriteRegKey, }; -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, diff --git a/fuel-vm/src/interpreter/blockchain.rs b/fuel-vm/src/interpreter/blockchain.rs index 28e276aead..6fcd2ea9e8 100644 --- a/fuel-vm/src/interpreter/blockchain.rs +++ b/fuel-vm/src/interpreter/blockchain.rs @@ -87,7 +87,7 @@ mod smo_tests; #[cfg(test)] mod test; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, diff --git a/fuel-vm/src/interpreter/constructors.rs b/fuel-vm/src/interpreter/constructors.rs index 9dd5c06c88..1d2c9049f0 100644 --- a/fuel-vm/src/interpreter/constructors.rs +++ b/fuel-vm/src/interpreter/constructors.rs @@ -3,6 +3,7 @@ #[cfg(any(test, feature = "test-helpers"))] use super::{ + trace::ExecutionTraceHooks, ExecutableTransaction, MemoryInstance, }; @@ -33,10 +34,11 @@ use crate::{ storage::MemoryStorage, }; -impl Interpreter +impl Interpreter where Tx: Default, Ecal: Default, + Trace: Default, { /// Create a new interpreter instance out of a storage implementation. /// @@ -52,9 +54,10 @@ where } } -impl Interpreter +impl Interpreter where Tx: Default, + Trace: Default, { /// Create a new interpreter instance out of a storage implementation. /// @@ -81,6 +84,7 @@ where context: Context::default(), balances: RuntimeBalances::default(), profiler: Profiler::default(), + trace: Trace::default(), interpreter_params, panic_context: PanicContext::None, ecal_state, @@ -88,7 +92,7 @@ where } } -impl Interpreter { +impl Interpreter { /// Sets a profiler for the VM #[cfg(feature = "profile-any")] pub fn with_profiler

(&mut self, receiver: P) -> &mut Self @@ -101,14 +105,15 @@ impl Interpreter { } #[cfg(any(test, feature = "test-helpers"))] -impl Default for Interpreter +impl Default for Interpreter where S: Default, Tx: ExecutableTransaction, Ecal: EcalHandler + Default, + Trace: Default, { fn default() -> Self { - Interpreter::<_, S, Tx, Ecal>::with_storage( + Interpreter::<_, S, Tx, Ecal, Trace>::with_storage( MemoryInstance::new(), Default::default(), InterpreterParams::default(), @@ -117,10 +122,11 @@ where } #[cfg(any(test, feature = "test-helpers"))] -impl Interpreter +impl Interpreter where Tx: ExecutableTransaction, Ecal: EcalHandler + Default, + Trace: ExecutionTraceHooks + Default, { /// Create a new interpreter without a storage backend. /// @@ -131,10 +137,11 @@ where } #[cfg(feature = "test-helpers")] -impl Interpreter +impl Interpreter where Tx: ExecutableTransaction, Ecal: EcalHandler + Default, + Trace: ExecutionTraceHooks + Default, { /// Create a new storage with a provided in-memory storage. /// @@ -145,16 +152,17 @@ where } #[cfg(feature = "test-helpers")] -impl Interpreter +impl Interpreter where Tx: ExecutableTransaction, Ecal: EcalHandler, + Trace: ExecutionTraceHooks + Default, { /// Create a new storage with a provided in-memory storage. /// /// It will have full capabilities. pub fn with_memory_storage_and_ecal(ecal: Ecal) -> Self { - Interpreter::<_, MemoryStorage, Tx, Ecal>::with_storage_and_ecal( + Interpreter::<_, MemoryStorage, Tx, Ecal, Trace>::with_storage_and_ecal( MemoryInstance::new(), Default::default(), InterpreterParams::default(), diff --git a/fuel-vm/src/interpreter/contract.rs b/fuel-vm/src/interpreter/contract.rs index f57b44cc03..a97c278052 100644 --- a/fuel-vm/src/interpreter/contract.rs +++ b/fuel-vm/src/interpreter/contract.rs @@ -56,7 +56,7 @@ use fuel_types::{ ContractId, }; -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, diff --git a/fuel-vm/src/interpreter/crypto.rs b/fuel-vm/src/interpreter/crypto.rs index 3732e92ce8..a73a63c1f5 100644 --- a/fuel-vm/src/interpreter/crypto.rs +++ b/fuel-vm/src/interpreter/crypto.rs @@ -43,7 +43,7 @@ use fuel_types::{ #[cfg(test)] mod tests; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, diff --git a/fuel-vm/src/interpreter/debug.rs b/fuel-vm/src/interpreter/debug.rs index b39b2152c9..e996a780f6 100644 --- a/fuel-vm/src/interpreter/debug.rs +++ b/fuel-vm/src/interpreter/debug.rs @@ -2,7 +2,7 @@ use super::Interpreter; use crate::prelude::*; use fuel_asm::RegId; -impl Interpreter +impl Interpreter where Tx: ExecutableTransaction, { diff --git a/fuel-vm/src/interpreter/diff.rs b/fuel-vm/src/interpreter/diff.rs index ffdb0005b1..1119da023e 100644 --- a/fuel-vm/src/interpreter/diff.rs +++ b/fuel-vm/src/interpreter/diff.rs @@ -52,7 +52,7 @@ use super::{ use crate::interpreter::memory::MemoryRollbackData; use storage::*; -mod storage; +pub(crate) mod storage; #[cfg(test)] mod tests; @@ -293,7 +293,7 @@ where .map(|((index, a), b)| (index, a.cloned(), b.cloned())) } -impl Interpreter +impl Interpreter where M: Memory, { @@ -363,7 +363,7 @@ where } } -impl Interpreter +impl Interpreter where M: Memory, { @@ -444,7 +444,7 @@ fn invert_receipts_ctx(ctx: &mut ReceiptsCtx, value: &VecState>) invert_vec(ctx_mut.receipts_mut(), value); } -impl PartialEq for Interpreter +impl PartialEq for Interpreter where M: Memory, Tx: PartialEq, diff --git a/fuel-vm/src/interpreter/diff/storage.rs b/fuel-vm/src/interpreter/diff/storage.rs index f885da0087..f22835945c 100644 --- a/fuel-vm/src/interpreter/diff/storage.rs +++ b/fuel-vm/src/interpreter/diff/storage.rs @@ -84,7 +84,7 @@ pub struct Record(pub(super) S, pub(super) Vec) where S: InterpreterStorage; -impl Interpreter, Tx, Ecal> +impl Interpreter, Tx, Ecal, Trace> where S: InterpreterStorage, Tx: ExecutableTransaction, @@ -92,7 +92,7 @@ where /// Remove the [`Recording`] wrapper from the storage. /// Recording storage changes has an overhead so it's /// useful to be able to remove it once the diff is generated. - pub fn remove_recording(self) -> Interpreter { + pub fn remove_recording(self) -> Interpreter { Interpreter { registers: self.registers, memory: self.memory, @@ -109,6 +109,7 @@ where balances: self.balances, panic_context: self.panic_context, profiler: self.profiler, + trace: self.trace, interpreter_params: self.interpreter_params, ecal_state: self.ecal_state, } @@ -172,7 +173,7 @@ where } } -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, @@ -182,7 +183,7 @@ where /// record any changes this VM makes to it's storage. /// Recording storage changes has an overhead so should /// be used in production. - pub fn add_recording(self) -> Interpreter, Tx, Ecal> { + pub fn add_recording(self) -> Interpreter, Tx, Ecal, Trace> { Interpreter { registers: self.registers, memory: self.memory, @@ -199,6 +200,7 @@ where balances: self.balances, panic_context: self.panic_context, profiler: self.profiler, + trace: self.trace, interpreter_params: self.interpreter_params, ecal_state: self.ecal_state, } diff --git a/fuel-vm/src/interpreter/ecal.rs b/fuel-vm/src/interpreter/ecal.rs index 4767dafcaa..d086f11e2b 100644 --- a/fuel-vm/src/interpreter/ecal.rs +++ b/fuel-vm/src/interpreter/ecal.rs @@ -30,8 +30,8 @@ where const INC_PC: bool = true; /// ECAL opcode handler - fn ecal( - vm: &mut Interpreter, + fn ecal( + vm: &mut Interpreter, a: RegId, b: RegId, c: RegId, @@ -43,8 +43,8 @@ where /// Default ECAL opcode handler function, which just errors immediately. impl EcalHandler for NotSupportedEcal { - fn ecal( - _: &mut Interpreter, + fn ecal( + _: &mut Interpreter, _: RegId, _: RegId, _: RegId, @@ -60,8 +60,8 @@ pub struct PredicateErrorEcal; /// ECAL is not allowed in predicates impl EcalHandler for PredicateErrorEcal { - fn ecal( - _vm: &mut Interpreter, + fn ecal( + _vm: &mut Interpreter, _: RegId, _: RegId, _: RegId, @@ -71,7 +71,7 @@ impl EcalHandler for PredicateErrorEcal { } } -impl Interpreter +impl Interpreter where M: Memory, Ecal: EcalHandler, @@ -94,7 +94,7 @@ where } } -impl Interpreter +impl Interpreter where Ecal: EcalHandler, { diff --git a/fuel-vm/src/interpreter/executors/debug.rs b/fuel-vm/src/interpreter/executors/debug.rs index 1b27bc994d..797d4882da 100644 --- a/fuel-vm/src/interpreter/executors/debug.rs +++ b/fuel-vm/src/interpreter/executors/debug.rs @@ -1,6 +1,7 @@ use crate::{ error::InterpreterError, interpreter::{ + trace::ExecutionTraceHooks, EcalHandler, ExecutableTransaction, Interpreter, @@ -10,12 +11,13 @@ use crate::{ storage::InterpreterStorage, }; -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, Tx: ExecutableTransaction, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { /// Continue the execution from a previously interrupted program flow. pub fn resume(&mut self) -> Result> { diff --git a/fuel-vm/src/interpreter/executors/instruction.rs b/fuel-vm/src/interpreter/executors/instruction.rs index 64737bccc3..da90165db6 100644 --- a/fuel-vm/src/interpreter/executors/instruction.rs +++ b/fuel-vm/src/interpreter/executors/instruction.rs @@ -11,6 +11,7 @@ use crate::{ JumpArgs, JumpMode, }, + trace::ExecutionTraceHooks, EcalHandler, ExecutableTransaction, Interpreter, @@ -32,12 +33,13 @@ use fuel_types::Word; use core::ops::Div; -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, Tx: ExecutableTransaction, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { /// Execute the current instruction located in `$m[$pc]`. pub fn execute(&mut self) -> Result> { @@ -80,8 +82,24 @@ where } } - self.instruction_inner(raw.into()) - .map_err(|e| InterpreterError::from_runtime(e, raw.into())) + Trace::before_instruction::(self); + + let result = self + .instruction_inner(raw.into()) + .map_err(|e| InterpreterError::from_runtime(e, raw.into())); + + if match result { + Ok(ExecuteState::DebugEvent(_)) => false, + Ok(_) => true, + Err(InterpreterError::PanicInstruction(_) | InterpreterError::Panic(_)) => { + true + } + Err(_) => false, + } { + Trace::after_instruction::(self); + } + + result } fn instruction_inner( diff --git a/fuel-vm/src/interpreter/executors/main.rs b/fuel-vm/src/interpreter/executors/main.rs index fe9fe16378..d5452c9dd8 100644 --- a/fuel-vm/src/interpreter/executors/main.rs +++ b/fuel-vm/src/interpreter/executors/main.rs @@ -14,6 +14,7 @@ use crate::{ PredicateVerificationFailed, }, interpreter::{ + trace::ExecutionTraceHooks, CheckedMetadata, EcalHandler, ExecutableTransaction, @@ -496,7 +497,7 @@ pub mod predicates { } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -563,7 +564,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -666,7 +667,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -775,7 +776,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -830,12 +831,13 @@ where } } -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, Tx: ExecutableTransaction, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { fn update_transaction_outputs( &mut self, @@ -1062,13 +1064,14 @@ where } } -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, Tx: ExecutableTransaction, ::Metadata: CheckedMetadata, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { /// Initialize a pre-allocated instance of [`Interpreter`] with the provided /// transaction and execute it. The result will be bound to the lifetime @@ -1101,7 +1104,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -1133,7 +1136,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -1165,7 +1168,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -1197,7 +1200,7 @@ where } } -impl Interpreter +impl Interpreter where S: InterpreterStorage, { @@ -1229,7 +1232,7 @@ where } } -impl Interpreter { +impl Interpreter { fn verify_ready_tx( &self, tx: &Ready, diff --git a/fuel-vm/src/interpreter/executors/predicate.rs b/fuel-vm/src/interpreter/executors/predicate.rs index 5d91788f59..87c3abc55c 100644 --- a/fuel-vm/src/interpreter/executors/predicate.rs +++ b/fuel-vm/src/interpreter/executors/predicate.rs @@ -1,6 +1,7 @@ use crate::{ error::PredicateVerificationFailed, interpreter::{ + trace::ExecutionTraceHooks, EcalHandler, Memory, }, @@ -18,12 +19,13 @@ use crate::{ use crate::storage::predicate::PredicateStorageRequirements; use fuel_asm::PanicReason; -impl Interpreter, Tx, Ecal> +impl Interpreter, Tx, Ecal, Trace> where M: Memory, + S: PredicateStorageRequirements, Tx: ExecutableTransaction, Ecal: EcalHandler, - S: PredicateStorageRequirements, + Trace: ExecutionTraceHooks, { /// Verify a predicate that has been initialized already pub(crate) fn verify_predicate( diff --git a/fuel-vm/src/interpreter/flow.rs b/fuel-vm/src/interpreter/flow.rs index 2b75aebe3b..33fc4966a5 100644 --- a/fuel-vm/src/interpreter/flow.rs +++ b/fuel-vm/src/interpreter/flow.rs @@ -82,7 +82,7 @@ mod ret_tests; #[cfg(test)] mod tests; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, @@ -331,7 +331,7 @@ impl JumpArgs { } } -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, diff --git a/fuel-vm/src/interpreter/gas.rs b/fuel-vm/src/interpreter/gas.rs index 62ef1dcb6f..456f7ceb6d 100644 --- a/fuel-vm/src/interpreter/gas.rs +++ b/fuel-vm/src/interpreter/gas.rs @@ -22,7 +22,7 @@ use fuel_types::{ #[cfg(test)] mod tests; -impl Interpreter { +impl Interpreter { /// Global remaining gas amount pub fn remaining_gas(&self) -> Word { self.registers[RegId::GGAS] diff --git a/fuel-vm/src/interpreter/initialization.rs b/fuel-vm/src/interpreter/initialization.rs index 8c327fe05e..877a7c3910 100644 --- a/fuel-vm/src/interpreter/initialization.rs +++ b/fuel-vm/src/interpreter/initialization.rs @@ -29,7 +29,7 @@ use fuel_types::Word; use crate::interpreter::CheckedMetadata; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, @@ -125,7 +125,7 @@ where } } -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, @@ -157,7 +157,7 @@ where } } -impl Interpreter +impl Interpreter where M: Memory, S: InterpreterStorage, diff --git a/fuel-vm/src/interpreter/internal.rs b/fuel-vm/src/interpreter/internal.rs index 3bf30b35c2..3fac210694 100644 --- a/fuel-vm/src/interpreter/internal.rs +++ b/fuel-vm/src/interpreter/internal.rs @@ -36,7 +36,7 @@ mod message_tests; #[cfg(test)] mod tests; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, @@ -98,7 +98,7 @@ pub(crate) fn update_memory_output( Ok(()) } -impl Interpreter +impl Interpreter where M: Memory, { diff --git a/fuel-vm/src/interpreter/log.rs b/fuel-vm/src/interpreter/log.rs index b38dc33540..edab9ae70c 100644 --- a/fuel-vm/src/interpreter/log.rs +++ b/fuel-vm/src/interpreter/log.rs @@ -21,7 +21,7 @@ use fuel_types::Word; #[cfg(test)] mod tests; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, diff --git a/fuel-vm/src/interpreter/memory.rs b/fuel-vm/src/interpreter/memory.rs index cfb4f78338..40bd3abe6f 100644 --- a/fuel-vm/src/interpreter/memory.rs +++ b/fuel-vm/src/interpreter/memory.rs @@ -130,6 +130,15 @@ impl MemoryInstance { self.hp = MEM_SIZE; } + /// Make memory equal to another instance, keeping the original allocations. + pub fn make_equal(&mut self, other: &Self) { + self.stack.truncate(0); + self.stack.extend_from_slice(&other.stack); + self.hp = other.hp; + self.heap.truncate(0); + self.heap.extend_from_slice(&other.heap); + } + /// Offset of the heap section fn heap_offset(&self) -> usize { MEM_SIZE.saturating_sub(self.heap.len()) @@ -499,14 +508,19 @@ fn get_changes( changes } -#[derive(Debug, Clone)] -struct MemorySliceChange { - global_start: usize, - data: Vec, +/// Memory change at a specific location. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MemorySliceChange { + /// Start address of the change. Global address. + pub global_start: usize, + /// Data that was changed. + pub data: Vec, } /// The container for the data used to rollback memory changes. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MemoryRollbackData { /// Desired stack pointer. sp: usize, @@ -650,7 +664,7 @@ impl MemoryRange { } } -impl Interpreter +impl Interpreter where M: Memory, { @@ -1046,7 +1060,9 @@ pub struct OwnershipRegisters { } impl OwnershipRegisters { - pub(crate) fn new(vm: &Interpreter) -> Self { + pub(crate) fn new( + vm: &Interpreter, + ) -> Self { let prev_hp = vm .frames .last() diff --git a/fuel-vm/src/interpreter/metadata.rs b/fuel-vm/src/interpreter/metadata.rs index bc2062f297..b9257fed34 100644 --- a/fuel-vm/src/interpreter/metadata.rs +++ b/fuel-vm/src/interpreter/metadata.rs @@ -46,7 +46,7 @@ use fuel_types::{ #[cfg(test)] mod tests; -impl Interpreter +impl Interpreter where M: Memory, Tx: ExecutableTransaction, diff --git a/fuel-vm/src/interpreter/post_execution.rs b/fuel-vm/src/interpreter/post_execution.rs index d1a8ffa83e..5e76b31fac 100644 --- a/fuel-vm/src/interpreter/post_execution.rs +++ b/fuel-vm/src/interpreter/post_execution.rs @@ -20,7 +20,7 @@ use fuel_types::{ Word, }; -impl Interpreter +impl Interpreter where S: InterpreterStorage, { diff --git a/fuel-vm/src/interpreter/trace.rs b/fuel-vm/src/interpreter/trace.rs new file mode 100644 index 0000000000..60957bb3d2 --- /dev/null +++ b/fuel-vm/src/interpreter/trace.rs @@ -0,0 +1,69 @@ +//! Execution traces + +use super::{ + Interpreter, + Memory, + NoTrace, +}; + +/// Hooks called at specific points during the execution. +/// Can be used to inspect the state of the VM. +/// Mutable access to the vm is provided so that the state of the tracer itself can be +/// modified. +pub trait ExecutionTraceHooks: Clone +where + Self: Sized, +{ + /// Runs after each instruction, unless that instruction enters a debugger pause + /// state, or causes a non-well-formed panic. + fn before_instruction( + vm: &mut Interpreter, + ) where + M: Memory {} + /// Runs before each instruction + fn after_instruction( + vm: &mut Interpreter, + ) where + M: Memory {} +} + +impl ExecutionTraceHooks for NoTrace {} + +impl Interpreter { + /// Replace trace hook type and state with a new one, discarding the old one. + pub fn with_trace_hooks( + self, + trace: NewTrace, + ) -> Interpreter { + Interpreter { + registers: self.registers, + memory: self.memory, + frames: self.frames, + receipts: self.receipts, + tx: self.tx, + initial_balances: self.initial_balances, + input_contracts: self.input_contracts, + input_contracts_index_to_output_index: self + .input_contracts_index_to_output_index, + storage: self.storage, + debugger: self.debugger, + context: self.context, + balances: self.balances, + profiler: self.profiler, + interpreter_params: self.interpreter_params, + panic_context: self.panic_context, + ecal_state: self.ecal_state, + trace, + } + } + + /// Read access to the trace state + pub fn trace(&self) -> &Trace { + &self.trace + } + + /// Write access to the trace state + pub fn trace_mut(&mut self) -> &mut Trace { + &mut self.trace + } +} diff --git a/fuel-vm/src/memory_client.rs b/fuel-vm/src/memory_client.rs index 8fc8a57ad0..f0709a4751 100644 --- a/fuel-vm/src/memory_client.rs +++ b/fuel-vm/src/memory_client.rs @@ -5,9 +5,11 @@ use crate::{ checked_transaction::Checked, error::InterpreterError, interpreter::{ + trace::ExecutionTraceHooks, EcalHandler, InterpreterParams, Memory, + NoTrace, NotSupportedEcal, }, state::StateTransitionRef, @@ -33,8 +35,8 @@ use crate::interpreter::MemoryInstance; #[derive(Debug)] /// Client implementation with in-memory storage backend. -pub struct MemoryClient { - transactor: Transactor, +pub struct MemoryClient { + transactor: Transactor, } #[cfg(any(test, feature = "test-helpers"))] @@ -48,19 +50,19 @@ impl Default for MemoryClient { } } -impl AsRef for MemoryClient { +impl AsRef for MemoryClient { fn as_ref(&self) -> &MemoryStorage { self.transactor.as_ref() } } -impl AsMut for MemoryClient { +impl AsMut for MemoryClient { fn as_mut(&mut self) -> &mut MemoryStorage { self.transactor.as_mut() } } -impl MemoryClient { +impl MemoryClient { /// Create a new instance of the memory client out of a provided storage. pub fn new( memory: M, @@ -73,16 +75,19 @@ impl MemoryClient { } } -impl MemoryClient { +impl MemoryClient { /// Create a new instance of the memory client out of a provided storage. - pub fn from_txtor(transactor: Transactor) -> Self { + pub fn from_txtor( + transactor: Transactor, + ) -> Self { Self { transactor } } } -impl MemoryClient +impl MemoryClient where M: Memory, + Trace: ExecutionTraceHooks, { /// If a transaction was executed and produced a VM panic, returns the /// backtrace; return `None` otherwise. @@ -179,10 +184,10 @@ where } } -impl From> - for Transactor +impl From> + for Transactor { - fn from(client: MemoryClient) -> Self { + fn from(client: MemoryClient) -> Self { client.transactor } } diff --git a/fuel-vm/src/tests/external.rs b/fuel-vm/src/tests/external.rs index 5c22d21fde..b4320ed94d 100644 --- a/fuel-vm/src/tests/external.rs +++ b/fuel-vm/src/tests/external.rs @@ -40,8 +40,8 @@ fn attempt_ecal_without_handler() { pub struct NoopEcal; impl ::fuel_vm::interpreter::EcalHandler for NoopEcal { - fn ecal( - vm: &mut ::fuel_vm::prelude::Interpreter, + fn ecal( + vm: &mut ::fuel_vm::prelude::Interpreter, _: RegId, _: RegId, _: RegId, @@ -88,8 +88,8 @@ pub struct SumProdEcal; impl ::fuel_vm::interpreter::EcalHandler for SumProdEcal { /// This ecal fn computes saturating sum and product of inputs (a,b,c,d), /// and stores them in a and b respectively. It charges only a single gas. - fn ecal( - vm: &mut ::fuel_vm::prelude::Interpreter, + fn ecal( + vm: &mut ::fuel_vm::prelude::Interpreter, a: RegId, b: RegId, c: RegId, @@ -169,8 +169,8 @@ impl ::fuel_vm::interpreter::EcalHandler for ComplexEcal { const INC_PC: bool = false; /// Ecal meant for testing cornercase behavior of the handler. - fn ecal( - vm: &mut ::fuel_vm::prelude::Interpreter, + fn ecal( + vm: &mut ::fuel_vm::prelude::Interpreter, a: RegId, _b: RegId, _c: RegId, diff --git a/fuel-vm/src/tests/mod.rs b/fuel-vm/src/tests/mod.rs index 59a667ee92..f5dba7d2f4 100644 --- a/fuel-vm/src/tests/mod.rs +++ b/fuel-vm/src/tests/mod.rs @@ -33,6 +33,7 @@ mod profile_gas; mod receipts; mod serde_profile; mod spec; +mod trace; mod upgrade; mod upload; mod validation; diff --git a/fuel-vm/src/tests/trace.rs b/fuel-vm/src/tests/trace.rs new file mode 100644 index 0000000000..d569e3f0ac --- /dev/null +++ b/fuel-vm/src/tests/trace.rs @@ -0,0 +1,213 @@ +use fuel_asm::{ + op, + RegId, +}; +use fuel_tx::{ + ConsensusParameters, + Finalizable, + Script, + TransactionBuilder, +}; +use fuel_types::canonical::Deserialize; + +use crate::{ + consts::{ + VM_REGISTER_COUNT, + WORD_SIZE, + }, + interpreter::{ + trace::ExecutionTraceHooks, + InterpreterParams, + NotSupportedEcal, + }, + prelude::*, + tests::test_helpers::assert_success, +}; + +#[derive(Debug, Clone)] +pub struct Record { + pub registers: [Word; VM_REGISTER_COUNT], + pub call_frame: Option, + /// Call frame params (a, b) interpreted as (ptr, len) slice, if available. + pub call_frame_params_slice: Option>, + pub receipt_count: usize, +} +impl Record { + fn capture(vm: &Interpreter) -> Self + where + M: Memory, + { + let mut registers = [0; VM_REGISTER_COUNT]; + registers.copy_from_slice(vm.registers()); + let receipt_count = vm.receipts().len(); + + let (call_frame, call_frame_params_slice) = if vm.context().is_internal() { + let size = CallFrame::serialized_size(); + let call_frame_data = vm + .memory() + .read(vm.registers()[RegId::FP], size) + .expect("Invalid fp value"); + let frame = + CallFrame::from_bytes(call_frame_data).expect("Invalid call frame"); + let params_slice = vm + .memory() + .read(frame.a(), frame.b() as usize) + .ok() + .map(|slice| slice.to_vec()); + (Some(frame), params_slice) + } else { + (None, None) + }; + + Record { + registers, + call_frame, + call_frame_params_slice, + receipt_count, + } + } +} + +/// Trace that's captured every time an new receipt is produced. +#[derive(Debug, Clone, Default)] +pub struct TestTrace { + receipts_before: usize, + frames: Vec, +} + +impl ExecutionTraceHooks for TestTrace { + fn before_instruction( + vm: &mut Interpreter, + ) where + M: Memory, + { + vm.trace_mut().receipts_before = vm.receipts().len(); + } + + fn after_instruction( + vm: &mut Interpreter, + ) where + M: Memory, + { + if vm.receipts().len() > vm.trace().receipts_before { + let record = Record::capture(vm); + vm.trace_mut().frames.push(record); + } + } +} + +#[test] +fn can_trace_simple_loop() { + let test_loop_rounds: usize = 5; + + let script_data: Vec = file!().bytes().collect(); + let script = vec![ + op::movi(0x20, test_loop_rounds as _), + op::log(0x20, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::subi(0x20, 0x20, 1), + op::jnzb(0x20, RegId::ZERO, 1), + op::ret(RegId::ONE), + ] + .into_iter() + .collect(); + + let consensus_params = ConsensusParameters::standard(); + let tx = TransactionBuilder::script(script, script_data) + .script_gas_limit(1_000_000) + .maturity(Default::default()) + .add_fee_input() + .finalize() + .into_checked(Default::default(), &consensus_params) + .expect("failed to generate a checked tx") + .test_into_ready(); + + let mut vm: Interpreter<_, _, Script, NotSupportedEcal, TestTrace> = + Interpreter::with_memory_storage(); + let result = vm.transact(tx).expect("Failed to transact"); + let receipts = result.receipts().to_vec(); + + assert_success(&receipts); + let trace = vm.trace(); + assert_eq!(trace.frames.len(), test_loop_rounds + 1); // + 1 for return + for i in 0..test_loop_rounds { + let frame = &trace.frames[i]; + assert_eq!(frame.receipt_count, i + 1); + assert_eq!(frame.registers[0x20], (test_loop_rounds - i) as Word); + } +} + +#[test] +fn can_trace_call_input_struct() { + let mut test_context = TestBuilder::new(2322u64); + let gas_limit = 1_000_000; + + // For this contract, param1 is pointer and param2 is length. + // Contract the logs this input data. + let contract_code = vec![ + // Fetch paramters from the call frame + op::addi(0x10, RegId::FP, CallFrame::a_offset() as _), + op::lw(0x10, 0x10, 0), + op::addi(0x11, RegId::FP, CallFrame::b_offset() as _), + op::lw(0x11, 0x11, 0), + // Log the input data + op::logd(RegId::ZERO, RegId::ZERO, 0x10, 0x11), + // Return + op::ret(RegId::ZERO), + ]; + + let contract_id = test_context + .setup_contract(contract_code, None, None) + .contract_id; + + // This script calls the input contract with params pointing to + // the the bytes of the first four instructions of this script. + // This is just used as some arbitrary data for testing. + let instructions_to_point = 4; + let script = vec![ + op::movi(0x10, (ContractId::LEN + WORD_SIZE * 2) as _), + op::aloc(0x10), + op::gtf_args(0x10, RegId::ZERO, GTFArgs::ScriptData), + op::mcpi(RegId::HP, 0x10, 32), + // a/param1: pointer to the input data + op::addi(0x11, RegId::HP, ContractId::LEN as _), + op::sw(0x11, RegId::IS, 0), + op::addi(0x11, 0x11, WORD_SIZE as _), + // b/param2: size of the input data + op::movi(0x12, (Instruction::SIZE * instructions_to_point) as _), + op::sw(0x11, 0x12, 0), + op::call(RegId::HP, RegId::ZERO, 0x10, 0x10), + op::ret(RegId::ONE), + ]; + let pointed_data: Vec = script.clone().into_iter().take(instructions_to_point).collect(); + + let tx = test_context + .start_script(script, contract_id.to_vec()) + .script_gas_limit(gas_limit) + .contract_input(contract_id) + .fee_input() + .contract_output(&contract_id) + .build() + .test_into_ready(); + + let mut vm: Interpreter<_, _, _, NotSupportedEcal, TestTrace> = + Interpreter::with_storage( + MemoryInstance::new(), + test_context.storage.clone(), + InterpreterParams::default(), + ); + + let result = vm.transact(tx).expect("Failed to transact"); + let receipts = result.receipts(); + dbg!(&receipts); + assert_success(&receipts); + + let call_frame = vm.trace().frames[0] + .call_frame + .as_ref() + .expect("Missing call frame"); + assert_eq!(*call_frame.to(), contract_id); + assert_eq!( + vm.trace().frames[0].call_frame_params_slice, + Some(pointed_data) + ); +} diff --git a/fuel-vm/src/transactor.rs b/fuel-vm/src/transactor.rs index 2f52aa5ee4..dd25ed516b 100644 --- a/fuel-vm/src/transactor.rs +++ b/fuel-vm/src/transactor.rs @@ -9,12 +9,14 @@ use crate::{ }, error::InterpreterError, interpreter::{ + trace::ExecutionTraceHooks, CheckedMetadata, EcalHandler, ExecutableTransaction, Interpreter, InterpreterParams, Memory, + NoTrace, NotSupportedEcal, }, state::{ @@ -46,25 +48,26 @@ use crate::interpreter::MemoryInstance; /// builder`. /// /// Based on -pub struct Transactor +pub struct Transactor where S: InterpreterStorage, { - interpreter: Interpreter, + interpreter: Interpreter, program_state: Option, error: Option>, } -impl Transactor +impl Transactor where S: InterpreterStorage, Tx: ExecutableTransaction, Ecal: EcalHandler + Default, + Trace: Default, { /// Transactor constructor pub fn new(memory: M, storage: S, interpreter_params: InterpreterParams) -> Self { Self { - interpreter: Interpreter::::with_storage( + interpreter: Interpreter::::with_storage( memory, storage, interpreter_params, @@ -74,11 +77,12 @@ where } } } -impl Transactor +impl Transactor where S: InterpreterStorage, Tx: ExecutableTransaction, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { /// State transition representation after the execution of a transaction. /// @@ -149,7 +153,7 @@ where } /// Gets the interpreter. - pub fn interpreter(&self) -> &Interpreter { + pub fn interpreter(&self) -> &Interpreter { &self.interpreter } @@ -175,7 +179,7 @@ where } } -impl Transactor +impl Transactor where M: Memory, S: InterpreterStorage, @@ -200,7 +204,7 @@ where } } -impl Transactor +impl Transactor where S: InterpreterStorage, { @@ -301,13 +305,14 @@ where } } -impl Transactor +impl Transactor where M: Memory, S: InterpreterStorage, Tx: ExecutableTransaction, ::Metadata: CheckedMetadata, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { /// Execute a transaction, and return the new state of the transactor pub fn transact(&mut self, tx: Checked) -> &mut Self { @@ -345,12 +350,13 @@ where } } -impl From> for Transactor +impl From> + for Transactor where Tx: ExecutableTransaction, S: InterpreterStorage, { - fn from(interpreter: Interpreter) -> Self { + fn from(interpreter: Interpreter) -> Self { let program_state = None; let error = None; @@ -362,28 +368,31 @@ where } } -impl From> for Interpreter +impl From> + for Interpreter where Tx: ExecutableTransaction, S: InterpreterStorage, { - fn from(transactor: Transactor) -> Self { + fn from(transactor: Transactor) -> Self { transactor.interpreter } } -impl AsRef> for Transactor +impl AsRef> + for Transactor where Tx: ExecutableTransaction, S: InterpreterStorage, Ecal: EcalHandler, + Trace: ExecutionTraceHooks, { - fn as_ref(&self) -> &Interpreter { + fn as_ref(&self) -> &Interpreter { &self.interpreter } } -impl AsRef for Transactor +impl AsRef for Transactor where Tx: ExecutableTransaction, S: InterpreterStorage, @@ -393,7 +402,7 @@ where } } -impl AsMut for Transactor +impl AsMut for Transactor where Tx: ExecutableTransaction, S: InterpreterStorage, @@ -404,11 +413,12 @@ where } #[cfg(feature = "test-helpers")] -impl Default for Transactor +impl Default for Transactor where S: InterpreterStorage + Default, Tx: ExecutableTransaction, Ecal: EcalHandler + Default, + Trace: Default, { fn default() -> Self { Self::new( diff --git a/fuel-vm/src/util.rs b/fuel-vm/src/util.rs index a56f1dff08..bc92e62d6a 100644 --- a/fuel-vm/src/util.rs +++ b/fuel-vm/src/util.rs @@ -187,7 +187,7 @@ pub mod test_helpers { max_fee_limit: Word, script_gas_limit: Word, builder: TransactionBuilder