Skip to content

Commit

Permalink
Add state to ECAL handler (#653)
Browse files Browse the repository at this point in the history
* Add state to ECAL handler

* Add changelog

* Remove Copy bound from EcalHandler

* Remove Default bound from EcalHandler

* Not a breaking change anymore, update changelog

* Fix tests

* Make separate constructors for ecal-enabled vm

* Simplify the call

---------

Co-authored-by: xgreenx <xgreenx9999@gmail.com>
  • Loading branch information
Dentosal and xgreenx authored Jan 9, 2024
1 parent 0d4fd45 commit f79b70f
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
137 changes: 136 additions & 1 deletion fuel-vm/examples/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ use std::{
SeekFrom,
},
path::PathBuf,
sync::{
Arc,
Mutex,
},
};

use fuel_asm::{
Expand Down Expand Up @@ -78,7 +82,7 @@ impl EcalHandler for FileReadEcal {
}
}

fn main() {
fn example_file_read() {
let vm: Interpreter<MemoryStorage, Script, FileReadEcal> =
Interpreter::with_memory_storage();

Expand Down Expand Up @@ -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<S, Tx>(
vm: &mut Interpreter<S, Tx, Self>,
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<MemoryStorage, Script, CounterEcal> =
Interpreter::with_memory_storage();

vm.ecal_state_mut().counter = 5;

let script_data: Vec<u8> = 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<Mutex<u64>>,
}

impl EcalHandler for SharedCounterEcal {
fn ecal<S, Tx>(
vm: &mut Interpreter<S, Tx, Self>,
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<MemoryStorage, Script, SharedCounterEcal> =
Interpreter::with_memory_storage_and_ecal(SharedCounterEcal {
counter: Arc::new(Mutex::new(5)),
});

let script_data: Vec<u8> = 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();
}
2 changes: 1 addition & 1 deletion fuel-vm/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub struct Interpreter<S, Tx = (), Ecal = NotSupportedEcal> {
/// `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>,
ecal_state: Ecal,
}

/// Interpreter parameters
Expand Down
44 changes: 40 additions & 4 deletions fuel-vm/src/interpreter/constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,32 @@ use crate::profiler::Profiler;
impl<S, Tx, Ecal> Interpreter<S, Tx, Ecal>
where
Tx: Default,
Ecal: 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(storage: S, interpreter_params: InterpreterParams) -> Self {
Self::with_storage_and_ecal(storage, interpreter_params, Ecal::default())
}
}

impl<S, Tx, Ecal> Interpreter<S, Tx, Ecal>
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]
Expand All @@ -51,7 +70,7 @@ where
profiler: Profiler::default(),
interpreter_params,
panic_context: PanicContext::None,
_ecal_handler: core::marker::PhantomData::<Ecal>,
ecal_state,
}
}
}
Expand Down Expand Up @@ -84,7 +103,7 @@ impl<S, Tx, Ecal> Default for Interpreter<S, Tx, Ecal>
where
S: Default,
Tx: ExecutableTransaction,
Ecal: EcalHandler,
Ecal: EcalHandler + Default,
{
fn default() -> Self {
Interpreter::<S, Tx, Ecal>::with_storage(
Expand All @@ -98,7 +117,7 @@ where
impl<Tx, Ecal> Interpreter<(), Tx, Ecal>
where
Tx: ExecutableTransaction,
Ecal: EcalHandler,
Ecal: EcalHandler + Default,
{
/// Create a new interpreter without a storage backend.
///
Expand All @@ -111,7 +130,7 @@ where
impl<Tx, Ecal> Interpreter<MemoryStorage, Tx, Ecal>
where
Tx: ExecutableTransaction,
Ecal: EcalHandler,
Ecal: EcalHandler + Default,
{
/// Create a new storage with a provided in-memory storage.
///
Expand All @@ -120,3 +139,20 @@ where
Self::default()
}
}

impl<Tx, Ecal> Interpreter<MemoryStorage, Tx, Ecal>
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::<MemoryStorage, Tx, Ecal>::with_storage_and_ecal(
Default::default(),
InterpreterParams::default(),
ecal,
)
}
}
4 changes: 2 additions & 2 deletions fuel-vm/src/interpreter/diff/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ where
panic_context: self.panic_context,
profiler: self.profiler,
interpreter_params: self.interpreter_params,
_ecal_handler: core::marker::PhantomData::<Ecal>,
ecal_state: self.ecal_state,
}
}

Expand Down Expand Up @@ -171,7 +171,7 @@ where
panic_context: self.panic_context,
profiler: self.profiler,
interpreter_params: self.interpreter_params,
_ecal_handler: core::marker::PhantomData::<Ecal>,
ecal_state: self.ecal_state,
}
}

Expand Down
12 changes: 11 additions & 1 deletion fuel-vm/src/interpreter/ecal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use super::{
};

/// ECAL opcode handler
pub trait EcalHandler: Default + Clone + Copy
pub trait EcalHandler: Clone
where
Self: Sized,
{
Expand Down Expand Up @@ -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
}
}
10 changes: 9 additions & 1 deletion fuel-vm/src/interpreter/executors/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ where
S: InterpreterStorage,
Tx: ExecutableTransaction,
<Tx as IntoChecked>::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
Expand All @@ -644,7 +644,15 @@ where
StateTransition::new(state, interpreter.tx, interpreter.receipts.into())
})
}
}

impl<S, Tx, Ecal> Interpreter<S, Tx, Ecal>
where
S: InterpreterStorage,
Tx: ExecutableTransaction,
<Tx as IntoChecked>::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
Expand Down
6 changes: 4 additions & 2 deletions fuel-vm/src/memory_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ impl<Ecal: EcalHandler> AsMut<MemoryStorage> for MemoryClient<Ecal> {
}
}

impl<Ecal: EcalHandler> MemoryClient<Ecal> {
impl<Ecal: EcalHandler + Default> MemoryClient<Ecal> {
/// 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<Ecal: EcalHandler> MemoryClient<Ecal> {
/// Create a new instance of the memory client out of a provided storage.
pub fn from_txtor(transactor: Transactor<MemoryStorage, Script, Ecal>) -> Self {
Self { transactor }
Expand Down Expand Up @@ -121,7 +123,7 @@ impl<Ecal: EcalHandler> MemoryClient<Ecal> {
}
}

impl<Ecal: EcalHandler> From<MemoryStorage> for MemoryClient<Ecal> {
impl<Ecal: EcalHandler + Default> From<MemoryStorage> for MemoryClient<Ecal> {
fn from(s: MemoryStorage) -> Self {
Self::new(s, InterpreterParams::default())
}
Expand Down
Loading

0 comments on commit f79b70f

Please sign in to comment.