diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd4058eb7..a74e3dd969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +#### Changed + +- [#653](https://github.com/FuelLabs/fuel-vm/pull/653): `ECAL` opcode handler can now hold internal state. + ## [Version 0.43.2] ### Changed diff --git a/fuel-vm/examples/external.rs b/fuel-vm/examples/external.rs index 26a063c860..5d732cb8f5 100644 --- a/fuel-vm/examples/external.rs +++ b/fuel-vm/examples/external.rs @@ -12,6 +12,10 @@ use std::{ SeekFrom, }, path::PathBuf, + sync::{ + Arc, + Mutex, + }, }; use fuel_asm::{ @@ -78,7 +82,7 @@ impl EcalHandler for FileReadEcal { } } -fn main() { +fn example_file_read() { let vm: Interpreter = Interpreter::with_memory_storage(); @@ -116,3 +120,134 @@ fn main() { let expected_bytes = &fs::read(file!()).expect("Couldn't read")[4..12]; assert_eq!(read_bytes, expected_bytes); } + +#[derive(Debug, Clone, Default)] +pub struct CounterEcal { + counter: u64, +} + +impl EcalHandler for CounterEcal { + fn ecal( + vm: &mut Interpreter, + a: RegId, + _b: RegId, + _c: RegId, + _d: RegId, + ) -> SimpleResult<()> { + vm.registers_mut()[a] = vm.ecal_state().counter; + vm.ecal_state_mut().counter += 1; + vm.gas_charge(1)?; + Ok(()) + } +} + +fn example_counter() { + let mut vm: Interpreter = + Interpreter::with_memory_storage(); + + vm.ecal_state_mut().counter = 5; + + let script_data: Vec = file!().bytes().collect(); + let script = vec![ + op::ecal(0x20, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ecal(0x21, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ecal(0x22, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ecal(0x23, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::log(0x20, 0x21, 0x22, 0x23), + op::ret(RegId::ONE), + ] + .into_iter() + .collect(); + + let mut client = MemoryClient::from_txtor(vm.into()); + let consensus_params = ConsensusParameters::standard(); + let tx = TransactionBuilder::script(script, script_data) + .gas_price(0) + .script_gas_limit(1_000_000) + .maturity(Default::default()) + .add_random_fee_input() + .finalize() + .into_checked(Default::default(), &consensus_params) + .expect("failed to generate a checked tx"); + client.transact(tx); + let receipts = client.receipts().expect("Expected receipts"); + + let Receipt::Log { ra, rb, rc, rd, .. } = receipts.first().unwrap() else { + panic!("Expected a log receipt"); + }; + + assert_eq!(*ra, 5); + assert_eq!(*rb, 6); + assert_eq!(*rc, 7); + assert_eq!(*rd, 8); +} + +#[derive(Debug, Clone)] +pub struct SharedCounterEcal { + counter: Arc>, +} + +impl EcalHandler for SharedCounterEcal { + fn ecal( + vm: &mut Interpreter, + a: RegId, + _b: RegId, + _c: RegId, + _d: RegId, + ) -> SimpleResult<()> { + let mut counter = vm.ecal_state().counter.lock().expect("poisoned"); + let old_value = *counter; + *counter += 1; + drop(counter); + vm.registers_mut()[a] = old_value; + vm.gas_charge(1)?; + Ok(()) + } +} + +fn example_shared_counter() { + let vm: Interpreter = + Interpreter::with_memory_storage_and_ecal(SharedCounterEcal { + counter: Arc::new(Mutex::new(5)), + }); + + let script_data: Vec = file!().bytes().collect(); + let script = vec![ + op::ecal(0x20, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ecal(0x21, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ecal(0x22, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::ecal(0x23, RegId::ZERO, RegId::ZERO, RegId::ZERO), + op::log(0x20, 0x21, 0x22, 0x23), + op::ret(RegId::ONE), + ] + .into_iter() + .collect(); + + let mut client = MemoryClient::from_txtor(vm.into()); + let consensus_params = ConsensusParameters::standard(); + let tx = TransactionBuilder::script(script, script_data) + .gas_price(0) + .script_gas_limit(1_000_000) + .maturity(Default::default()) + .add_random_fee_input() + .finalize() + .into_checked(Default::default(), &consensus_params) + .expect("failed to generate a checked tx"); + client.transact(tx); + let receipts = client.receipts().expect("Expected receipts"); + + let Receipt::Log { ra, rb, rc, rd, .. } = receipts.first().unwrap() else { + panic!("Expected a log receipt"); + }; + + assert_eq!(*ra, 5); + assert_eq!(*rb, 6); + assert_eq!(*rc, 7); + assert_eq!(*rd, 8); +} + +fn main() { + example_file_read(); + example_counter(); + example_shared_counter(); +} diff --git a/fuel-vm/src/interpreter.rs b/fuel-vm/src/interpreter.rs index b9a26892b3..1a539ba18c 100644 --- a/fuel-vm/src/interpreter.rs +++ b/fuel-vm/src/interpreter.rs @@ -128,7 +128,7 @@ pub struct Interpreter { /// `PanicContext` after the latest execution. It is consumed by /// `append_panic_receipt` and is `PanicContext::None` after consumption. panic_context: PanicContext, - _ecal_handler: core::marker::PhantomData, + ecal_state: Ecal, } /// Interpreter parameters diff --git a/fuel-vm/src/interpreter/constructors.rs b/fuel-vm/src/interpreter/constructors.rs index aee079ae8c..fcc973ab99 100644 --- a/fuel-vm/src/interpreter/constructors.rs +++ b/fuel-vm/src/interpreter/constructors.rs @@ -28,6 +28,7 @@ use crate::profiler::Profiler; impl Interpreter where Tx: Default, + Ecal: Default, { /// Create a new interpreter instance out of a storage implementation. /// @@ -35,6 +36,24 @@ where /// [`crate::storage::InterpreterStorage`], the returned interpreter /// will provide full functionality. pub fn with_storage(storage: S, interpreter_params: InterpreterParams) -> Self { + Self::with_storage_and_ecal(storage, interpreter_params, Ecal::default()) + } +} + +impl Interpreter +where + Tx: Default, +{ + /// Create a new interpreter instance out of a storage implementation. + /// + /// If the provided storage implements + /// [`crate::storage::InterpreterStorage`], the returned interpreter + /// will provide full functionality. + pub fn with_storage_and_ecal( + storage: S, + interpreter_params: InterpreterParams, + ecal_state: Ecal, + ) -> Self { Self { registers: [0; VM_REGISTER_COUNT], memory: vec![0; MEM_SIZE] @@ -51,7 +70,7 @@ where profiler: Profiler::default(), interpreter_params, panic_context: PanicContext::None, - _ecal_handler: core::marker::PhantomData::, + ecal_state, } } } @@ -84,7 +103,7 @@ impl Default for Interpreter where S: Default, Tx: ExecutableTransaction, - Ecal: EcalHandler, + Ecal: EcalHandler + Default, { fn default() -> Self { Interpreter::::with_storage( @@ -98,7 +117,7 @@ where impl Interpreter<(), Tx, Ecal> where Tx: ExecutableTransaction, - Ecal: EcalHandler, + Ecal: EcalHandler + Default, { /// Create a new interpreter without a storage backend. /// @@ -111,7 +130,7 @@ where impl Interpreter where Tx: ExecutableTransaction, - Ecal: EcalHandler, + Ecal: EcalHandler + Default, { /// Create a new storage with a provided in-memory storage. /// @@ -120,3 +139,20 @@ where Self::default() } } + +impl Interpreter +where + Tx: ExecutableTransaction, + Ecal: EcalHandler, +{ + /// 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::::with_storage_and_ecal( + Default::default(), + InterpreterParams::default(), + ecal, + ) + } +} diff --git a/fuel-vm/src/interpreter/diff/storage.rs b/fuel-vm/src/interpreter/diff/storage.rs index b042b6656a..2a557e916f 100644 --- a/fuel-vm/src/interpreter/diff/storage.rs +++ b/fuel-vm/src/interpreter/diff/storage.rs @@ -97,7 +97,7 @@ where panic_context: self.panic_context, profiler: self.profiler, interpreter_params: self.interpreter_params, - _ecal_handler: core::marker::PhantomData::, + ecal_state: self.ecal_state, } } @@ -171,7 +171,7 @@ where panic_context: self.panic_context, profiler: self.profiler, interpreter_params: self.interpreter_params, - _ecal_handler: core::marker::PhantomData::, + ecal_state: self.ecal_state, } } diff --git a/fuel-vm/src/interpreter/ecal.rs b/fuel-vm/src/interpreter/ecal.rs index 0f81d7810b..36f8e9bd2a 100644 --- a/fuel-vm/src/interpreter/ecal.rs +++ b/fuel-vm/src/interpreter/ecal.rs @@ -20,7 +20,7 @@ use super::{ }; /// ECAL opcode handler -pub trait EcalHandler: Default + Clone + Copy +pub trait EcalHandler: Clone where Self: Sized, { @@ -88,4 +88,14 @@ where Ok(()) } } + + /// Read access to the ECAL state + pub fn ecal_state(&self) -> &Ecal { + &self.ecal_state + } + + /// Write access to the ECAL state + pub fn ecal_state_mut(&mut self) -> &mut Ecal { + &mut self.ecal_state + } } diff --git a/fuel-vm/src/interpreter/executors/main.rs b/fuel-vm/src/interpreter/executors/main.rs index 080599f1c1..6e99246bfe 100644 --- a/fuel-vm/src/interpreter/executors/main.rs +++ b/fuel-vm/src/interpreter/executors/main.rs @@ -626,7 +626,7 @@ where S: InterpreterStorage, Tx: ExecutableTransaction, ::Metadata: CheckedMetadata, - Ecal: EcalHandler, + Ecal: EcalHandler + Default, { /// Allocate internally a new instance of [`Interpreter`] with the provided /// storage, initialize it with the provided transaction and return the @@ -644,7 +644,15 @@ where StateTransition::new(state, interpreter.tx, interpreter.receipts.into()) }) } +} +impl Interpreter +where + S: InterpreterStorage, + Tx: ExecutableTransaction, + ::Metadata: CheckedMetadata, + Ecal: EcalHandler, +{ /// Initialize a pre-allocated instance of [`Interpreter`] with the provided /// transaction and execute it. The result will be bound to the lifetime /// of the interpreter and will avoid unnecessary copy with the data diff --git a/fuel-vm/src/memory_client.rs b/fuel-vm/src/memory_client.rs index dc2178bbea..7d646b53bc 100644 --- a/fuel-vm/src/memory_client.rs +++ b/fuel-vm/src/memory_client.rs @@ -45,14 +45,16 @@ impl AsMut for MemoryClient { } } -impl MemoryClient { +impl MemoryClient { /// Create a new instance of the memory client out of a provided storage. pub fn new(storage: MemoryStorage, interpreter_params: InterpreterParams) -> Self { Self { transactor: Transactor::new(storage, interpreter_params), } } +} +impl MemoryClient { /// Create a new instance of the memory client out of a provided storage. pub fn from_txtor(transactor: Transactor) -> Self { Self { transactor } @@ -121,7 +123,7 @@ impl MemoryClient { } } -impl From for MemoryClient { +impl From for MemoryClient { fn from(s: MemoryStorage) -> Self { Self::new(s, InterpreterParams::default()) } diff --git a/fuel-vm/src/transactor.rs b/fuel-vm/src/transactor.rs index fd8fa968f6..6dd48c50a4 100644 --- a/fuel-vm/src/transactor.rs +++ b/fuel-vm/src/transactor.rs @@ -49,11 +49,11 @@ where error: Option>, } -impl<'a, S, Tx, Ecal> Transactor +impl Transactor where S: InterpreterStorage, Tx: ExecutableTransaction, - Ecal: EcalHandler, + Ecal: EcalHandler + Default, { /// Transactor constructor pub fn new(storage: S, interpreter_params: InterpreterParams) -> Self { @@ -66,7 +66,13 @@ where error: None, } } - +} +impl<'a, S, Tx, Ecal> Transactor +where + S: InterpreterStorage, + Tx: ExecutableTransaction, + Ecal: EcalHandler, +{ /// State transition representation after the execution of a transaction. /// /// Will be `None` if the last transaction resulted in a VM panic, or if no @@ -243,6 +249,7 @@ impl AsRef> for Transactor where Tx: ExecutableTransaction, S: InterpreterStorage, + Ecal: EcalHandler, { fn as_ref(&self) -> &Interpreter { &self.interpreter @@ -273,7 +280,7 @@ impl Default for Transactor where S: InterpreterStorage + Default, Tx: ExecutableTransaction, - Ecal: EcalHandler, + Ecal: EcalHandler + Default, { fn default() -> Self { Self::new(S::default(), InterpreterParams::default())