diff --git a/Cargo.toml b/Cargo.toml index f201c4bc26..7be67d6847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,24 +15,27 @@ dyn-clone = { version = "1.0", optional = true } fuel-asm = "0.1" fuel-merkle = "0.1" fuel-storage = "0.1" -fuel-tx = "0.2" +fuel-tx = "0.3" fuel-types = "0.1" itertools = "0.10" secp256k1 = { version = "0.20", features = ["recovery"] } serde = { version = "1.0", features = ["derive"], optional = true } sha3 = "0.9" tracing = "0.1" +rand = { version = "0.8", optional = true } [dev-dependencies] -rand = "0.8" +fuel-tx = { version = "0.3", features = ["random"] } +fuel-vm = { path = ".", default-features = false, features = ["test-helpers"]} [features] debug = [] profile-gas = ["profile-any"] profile-coverage = ["profile-any"] profile-any = ["dyn-clone"] # All profiling features should depend on this -random = ["fuel-types/random", "fuel-tx/random"] +random = ["fuel-types/random", "fuel-tx/random", "rand"] serde-types = ["fuel-asm/serde-types", "fuel-types/serde-types", "fuel-tx/serde-types", "serde"] +test-helpers = ["random"] [[test]] name = "test-backtrace" diff --git a/src/error.rs b/src/error.rs index 9495a16c5f..d8f6869aa8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -120,6 +120,15 @@ impl From for InterpreterError { } } +impl From for InterpreterError { + fn from(error: RuntimeError) -> Self { + match error { + RuntimeError::Recoverable(e) => Self::Panic(e), + RuntimeError::Halt(e) => Self::Io(e), + } + } +} + #[derive(Debug)] #[cfg_attr(feature = "serde-types-minimal", derive(serde::Serialize, serde::Deserialize))] /// Runtime error description that should either be specified in the protocol or diff --git a/src/interpreter.rs b/src/interpreter.rs index 240c15f843..9d02783804 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -4,9 +4,10 @@ use crate::call::CallFrame; use crate::consts::*; use crate::context::Context; use crate::state::Debugger; +use std::collections::HashMap; use fuel_tx::{Receipt, Transaction}; -use fuel_types::Word; +use fuel_types::{Color, Word}; mod alu; mod blockchain; @@ -22,6 +23,7 @@ mod internal; mod log; mod memory; mod metadata; +mod post_execution; mod transaction; #[cfg(feature = "debug")] @@ -54,6 +56,8 @@ pub struct Interpreter { block_height: u32, #[cfg(feature = "profile-any")] profiler: Profiler, + // track the offset for each unused balance in memory + unused_balance_index: HashMap, } impl Interpreter { diff --git a/src/interpreter/constructors.rs b/src/interpreter/constructors.rs index d9758601e4..28fb0e1aa9 100644 --- a/src/interpreter/constructors.rs +++ b/src/interpreter/constructors.rs @@ -30,6 +30,7 @@ impl Interpreter { block_height: 0, #[cfg(feature = "profile-any")] profiler: Profiler::default(), + unused_balance_index: Default::default(), } } diff --git a/src/interpreter/contract.rs b/src/interpreter/contract.rs index 3b5c13cc7b..312e1ce4a3 100644 --- a/src/interpreter/contract.rs +++ b/src/interpreter/contract.rs @@ -5,7 +5,8 @@ use crate::error::RuntimeError; use crate::storage::InterpreterStorage; use fuel_asm::{PanicReason, RegisterId, Word}; -use fuel_types::{Color, ContractId}; +use fuel_tx::Receipt; +use fuel_types::{Address, Color, ContractId}; use std::borrow::Cow; @@ -44,6 +45,98 @@ where self.inc_pc() } + pub(crate) fn transfer(&mut self, a: Word, b: Word, c: Word) -> Result<(), RuntimeError> { + let (ax, overflow) = a.overflowing_add(ContractId::LEN as Word); + let (cx, of) = c.overflowing_add(Color::LEN as Word); + let overflow = overflow || of; + + if overflow || ax > VM_MAX_RAM || cx > VM_MAX_RAM { + return Err(PanicReason::MemoryOverflow.into()); + } + + let amount = b; + let destination = + ContractId::try_from(&self.memory[a as usize..ax as usize]).expect("Unreachable! Checked memory range"); + let asset_id = + Color::try_from(&self.memory[c as usize..cx as usize]).expect("Unreachable! Checked memory range"); + + if !self.tx.input_contracts().any(|contract| &destination == contract) { + return Err(PanicReason::ContractNotInInputs.into()); + } + + if amount == 0 { + return Err(PanicReason::NotEnoughBalance.into()); + } + + let internal_context = match self.internal_contract() { + // optimistically attempt to load the internal contract id + Ok(source_contract) => Some(*source_contract), + // revert to external context if no internal contract is set + Err(RuntimeError::Recoverable(PanicReason::ExpectedInternalContext)) => None, + // bubble up any other kind of errors + Err(e) => return Err(e), + }; + + if let Some(source_contract) = internal_context { + // debit funding source (source contract balance) + self.balance_decrease(&source_contract, &asset_id, amount)?; + } else { + // debit external funding source (i.e. free balance) + self.external_color_balance_sub(&asset_id, amount)?; + } + // credit destination contract + self.balance_increase(&destination, &asset_id, amount)?; + + self.receipts.push(Receipt::transfer( + internal_context.unwrap_or_default(), + destination, + amount, + asset_id, + self.registers[REG_PC], + self.registers[REG_IS], + )); + + self.inc_pc() + } + + pub(crate) fn transfer_output(&mut self, a: Word, b: Word, c: Word, d: Word) -> Result<(), RuntimeError> { + let (ax, overflow) = a.overflowing_add(ContractId::LEN as Word); + let (dx, of) = d.overflowing_add(Color::LEN as Word); + let overflow = overflow || of; + let out_idx = b as usize; + + if overflow || ax > VM_MAX_RAM || dx > VM_MAX_RAM { + return Err(PanicReason::MemoryOverflow.into()); + } + + let to = Address::try_from(&self.memory[a as usize..ax as usize]).expect("Unreachable! Checked memory range"); + let asset_id = + Color::try_from(&self.memory[d as usize..dx as usize]).expect("Unreachable! Checked memory range"); + let amount = c; + + let internal_context = match self.internal_contract() { + // optimistically attempt to load the internal contract id + Ok(source_contract) => Some(*source_contract), + // revert to external context if no internal contract is set + Err(RuntimeError::Recoverable(PanicReason::ExpectedInternalContext)) => None, + // bubble up any other kind of errors + Err(e) => return Err(e), + }; + + if let Some(source_contract) = internal_context { + // debit funding source (source contract balance) + self.balance_decrease(&source_contract, &asset_id, amount)?; + } else { + // debit external funding source (i.e. UTXOs) + self.external_color_balance_sub(&asset_id, amount)?; + } + + // credit variable output + self.set_variable_output(out_idx, asset_id, amount, to)?; + + self.inc_pc() + } + pub(crate) fn check_contract_exists(&self, contract: &ContractId) -> Result { self.storage .storage_contract_exists(contract) @@ -57,4 +150,34 @@ where .map_err(RuntimeError::from_io)? .unwrap_or_default()) } + + /// Increase the asset balance for a contract + pub(crate) fn balance_increase( + &mut self, + contract: &ContractId, + asset_id: &Color, + amount: Word, + ) -> Result { + let balance = self.balance(&contract, &asset_id)?; + let balance = balance.checked_add(amount).ok_or(PanicReason::ArithmeticOverflow)?; + self.storage + .merkle_contract_color_balance_insert(&contract, &asset_id, balance) + .map_err(RuntimeError::from_io)?; + Ok(balance) + } + + /// Decrease the asset balance for a contract + pub(crate) fn balance_decrease( + &mut self, + contract: &ContractId, + asset_id: &Color, + amount: Word, + ) -> Result { + let balance = self.balance(&contract, &asset_id)?; + let balance = balance.checked_sub(amount).ok_or(PanicReason::NotEnoughBalance)?; + self.storage + .merkle_contract_color_balance_insert(&contract, &asset_id, balance) + .map_err(RuntimeError::from_io)?; + Ok(balance) + } } diff --git a/src/interpreter/debug.rs b/src/interpreter/debug.rs index e93048d898..59541a9a0b 100644 --- a/src/interpreter/debug.rs +++ b/src/interpreter/debug.rs @@ -41,6 +41,7 @@ fn breakpoint_script() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let script = vec![ @@ -55,7 +56,17 @@ fn breakpoint_script() { .copied() .collect(); - let tx = Transaction::script(gas_price, gas_limit, maturity, script, vec![], vec![], vec![], vec![]); + let tx = Transaction::script( + gas_price, + gas_limit, + byte_price, + maturity, + script, + vec![], + vec![], + vec![], + vec![], + ); let suite = vec![ ( diff --git a/src/interpreter/executors/instruction.rs b/src/interpreter/executors/instruction.rs index 316b5f7e6b..c7a64dedf7 100644 --- a/src/interpreter/executors/instruction.rs +++ b/src/interpreter/executors/instruction.rs @@ -469,8 +469,18 @@ where self.metadata(ra, imm as Immediate18)?; } + OpcodeRepr::TR => { + self.gas_charge(GAS_TR)?; + self.transfer(a, b, c)?; + } + + OpcodeRepr::TRO => { + self.gas_charge(GAS_TRO)?; + self.transfer_output(a, b, c, d)?; + } + // list of currently unimplemented opcodes - OpcodeRepr::SLDC | OpcodeRepr::TR | OpcodeRepr::TRO | _ => { + OpcodeRepr::SLDC | _ => { return Err(PanicReason::ErrorFlag.into()); } } diff --git a/src/interpreter/executors/main.rs b/src/interpreter/executors/main.rs index 65c458bd89..e1472c3835 100644 --- a/src/interpreter/executors/main.rs +++ b/src/interpreter/executors/main.rs @@ -28,7 +28,7 @@ where .iter() .any(|id| !self.check_contract_exists(id).unwrap_or(false)) { - Err(InterpreterError::Panic(PanicReason::ContractNotFound))? + return Err(InterpreterError::Panic(PanicReason::ContractNotFound)); } let contract = Contract::try_from(&self.tx)?; @@ -41,7 +41,7 @@ where .iter() .any(|output| matches!(output, Output::ContractCreated { contract_id } if contract_id == &id)) { - Err(InterpreterError::Panic(PanicReason::ContractNotInInputs))?; + return Err(InterpreterError::Panic(PanicReason::ContractNotInInputs)); } self.storage @@ -84,13 +84,21 @@ where } } - Transaction::Script { .. } => { + Transaction::Script { inputs, .. } => { + if inputs.iter().any(|input| { + if let Input::Contract { contract_id, .. } = input { + !self.check_contract_exists(contract_id).unwrap_or(false) + } else { + false + } + }) { + return Err(InterpreterError::Panic(PanicReason::ContractNotFound)); + } + let offset = (VM_TX_MEMORY + Transaction::script_offset()) as Word; self.registers[REG_PC] = offset; self.registers[REG_IS] = offset; - self.registers[REG_GGAS] = self.tx.gas_limit(); - self.registers[REG_CGAS] = self.tx.gas_limit(); // TODO set tree balance @@ -141,6 +149,11 @@ where self.tx.set_receipts_root(receipts_root); } + // refund remaining global gas + let gas_refund = self.registers[REG_GGAS] * self.tx.gas_price(); + let revert = matches!(state, ProgramState::Revert(_)); + self.update_change_amounts(gas_refund, revert)?; + Ok(state) } diff --git a/src/interpreter/gas.rs b/src/interpreter/gas.rs index 664bffa78e..ff2daed98f 100644 --- a/src/interpreter/gas.rs +++ b/src/interpreter/gas.rs @@ -29,9 +29,7 @@ impl Interpreter { .join(GasUnit::Branching(1)) .join(GasUnit::RegisterWrite(1)), - // TODO Compile-time panic didn't land in stable yet - // https://github.com/rust-lang/rust/issues/51999 - _ => loop {}, //panic!("Opcode is not gas constant"), + _ => panic!("Opcode is not gas constant"), } .cost() } @@ -48,9 +46,7 @@ impl Interpreter { MCP | MCPI => GasUnit::Arithmetic(2).join(GasUnit::MemoryOwnership(1)), - // TODO Compile-time panic didn't land in stable yet - // https://github.com/rust-lang/rust/issues/51999 - _ => loop {}, //panic!("Opcode is not variable gas"), + _ => panic!("Opcode is not variable gas"), } .cost() } @@ -85,6 +81,7 @@ impl Interpreter { Err(PanicReason::OutOfGas.into()) } else { self.registers[REG_CGAS] -= gas; + self.registers[REG_GGAS] -= gas; Ok(()) } diff --git a/src/interpreter/gas/consts.rs b/src/interpreter/gas/consts.rs index 37ffe16e39..23cfafae28 100644 --- a/src/interpreter/gas/consts.rs +++ b/src/interpreter/gas/consts.rs @@ -79,6 +79,8 @@ pub const GAS_XWL: Word = Interpreter::<()>::gas_cost_const(OpcodeRepr::ADD); pub const GAS_XWS: Word = Interpreter::<()>::gas_cost_const(OpcodeRepr::ADD); pub const GAS_FLAG: Word = Interpreter::<()>::gas_cost_const(OpcodeRepr::ADD); pub const GAS_GM: Word = Interpreter::<()>::gas_cost_const(OpcodeRepr::ADD); +pub const GAS_TR: Word = Interpreter::<()>::gas_cost_const(OpcodeRepr::ADD); +pub const GAS_TRO: Word = Interpreter::<()>::gas_cost_const(OpcodeRepr::ADD); // Variable gas cost const GAS_OP_MEMORY_WRITE: Word = GasUnit::MemoryWrite(0).unit_price(); diff --git a/src/interpreter/initialization.rs b/src/interpreter/initialization.rs index 107f5182cf..c86277235b 100644 --- a/src/interpreter/initialization.rs +++ b/src/interpreter/initialization.rs @@ -4,15 +4,14 @@ use crate::context::Context; use crate::error::InterpreterError; use crate::storage::InterpreterStorage; -use fuel_tx::consts::*; -use fuel_tx::{Input, Transaction}; +use fuel_asm::PanicReason; +use fuel_tx::consts::MAX_INPUTS; +use fuel_tx::{Input, Output, Transaction}; use fuel_types::bytes::{SerializableVec, SizedBytes}; use fuel_types::{Color, Word}; use itertools::Itertools; - -use std::{io, mem}; - -const WORD_SIZE: usize = mem::size_of::(); +use std::collections::HashMap; +use std::io; impl Interpreter where @@ -40,41 +39,32 @@ where self.push_stack(tx.id().as_ref()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - let zeroes = &[0; MAX_INPUTS as usize * (Color::LEN + WORD_SIZE)]; - let ssp = self.registers[REG_SSP] as usize; - - self.push_stack(zeroes) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - if tx.is_script() { - tx.inputs() - .iter() - .filter_map(|input| match input { - Input::Coin { color, amount, .. } => Some((color, amount)), - _ => None, - }) - .sorted_by_key(|i| i.0) - .take(MAX_INPUTS as usize) - .fold(ssp, |mut ssp, (color, amount)| { - self.memory[ssp..ssp + Color::LEN].copy_from_slice(color.as_ref()); - ssp += Color::LEN; - - self.memory[ssp..ssp + WORD_SIZE].copy_from_slice(&amount.to_be_bytes()); - ssp += WORD_SIZE; - - ssp - }); + // Set initial unused balances + let free_balances = Self::initial_free_balances(&tx)?; + for (color, amount) in free_balances.iter().sorted_by_key(|i| i.0) { + // push color + self.push_stack(color.as_ref()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + // stack position + let color_offset = self.registers[REG_SSP] as usize; + self.unused_balance_index.insert(*color, color_offset); + // push spendable amount + self.push_stack(&amount.to_be_bytes()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + } + // zero out remaining unused balance types + for _i in free_balances.len()..(MAX_INPUTS as usize) { + self.push_stack(&[0; Color::LEN + WORD_SIZE]) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; } let tx_size = tx.serialized_size() as Word; - - if tx.is_script() { - self.registers[REG_GGAS] = tx.gas_limit(); - self.registers[REG_CGAS] = tx.gas_limit(); - } + self.registers[REG_GGAS] = tx.gas_limit(); + self.registers[REG_CGAS] = tx.gas_limit(); self.push_stack(&tx_size.to_be_bytes()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + self.push_stack(tx.to_bytes().as_slice()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; @@ -84,4 +74,45 @@ where Ok(()) } + + // compute the initial free balances for each asset type + pub(crate) fn initial_free_balances(tx: &Transaction) -> Result, InterpreterError> { + let mut balances = HashMap::::new(); + + // Add up all the inputs for each color + for (color, amount) in tx.inputs().iter().filter_map(|input| match input { + Input::Coin { color, amount, .. } => Some((color, amount)), + _ => None, + }) { + *balances.entry(*color).or_default() += amount; + } + + // Reduce by unavailable balances + let base_asset = Color::default(); + if let Some(base_asset_balance) = balances.get_mut(&base_asset) { + // remove byte costs from base asset spendable balance + let byte_balance = (tx.metered_bytes_size() as Word) * tx.byte_price(); + *base_asset_balance = base_asset_balance + .checked_sub(byte_balance) + .ok_or(InterpreterError::Panic(PanicReason::NotEnoughBalance))?; + // remove gas costs from base asset spendable balance + *base_asset_balance = base_asset_balance + .checked_sub(tx.gas_limit() * tx.gas_price()) + .ok_or(InterpreterError::Panic(PanicReason::NotEnoughBalance))?; + } + + // reduce free balances by coin and withdrawal outputs + for (color, amount) in tx.outputs().iter().filter_map(|output| match output { + Output::Coin { color, amount, .. } => Some((color, amount)), + Output::Withdrawal { color, amount, .. } => Some((color, amount)), + _ => None, + }) { + let balance = balances.get_mut(color).unwrap(); + *balance = balance + .checked_sub(*amount) + .ok_or(InterpreterError::Panic(PanicReason::NotEnoughBalance))?; + } + + Ok(balances) + } } diff --git a/src/interpreter/internal.rs b/src/interpreter/internal.rs index 4478a79aa2..d3ba6800c8 100644 --- a/src/interpreter/internal.rs +++ b/src/interpreter/internal.rs @@ -4,13 +4,9 @@ use crate::context::Context; use crate::error::RuntimeError; use fuel_asm::{Instruction, PanicReason}; -use fuel_tx::consts::*; -use fuel_tx::Transaction; -use fuel_types::{Bytes32, Color, ContractId, RegisterId, Word}; - -use std::mem; - -const WORD_SIZE: usize = mem::size_of::(); +use fuel_tx::{Output, Transaction}; +use fuel_types::{Address, Color, ContractId, RegisterId, Word}; +use std::io::Read; impl Interpreter { pub(crate) fn push_stack(&mut self, data: &[u8]) -> Result<(), RuntimeError> { @@ -110,18 +106,26 @@ impl Interpreter { .ok_or(PanicReason::ExpectedInternalContext.into()) } + /// Retrieve the unspent balance for a given color + pub(crate) fn external_color_balance(&self, color: &Color) -> Result { + let offset = *self.unused_balance_index.get(color).ok_or(PanicReason::ColorNotFound)?; + let balance_memory = &self.memory[offset..offset + WORD_SIZE]; + + let balance = <[u8; WORD_SIZE]>::try_from(&*balance_memory).expect("Expected slice to be word length!"); + let balance = Word::from_be_bytes(balance); + + Ok(balance) + } + + /// Reduces the unspent balance of a given color pub(crate) fn external_color_balance_sub(&mut self, color: &Color, value: Word) -> Result<(), RuntimeError> { if value == 0 { return Ok(()); } - const LEN: usize = Color::LEN + WORD_SIZE; + let offset = *self.unused_balance_index.get(color).ok_or(PanicReason::ColorNotFound)?; - let balance_memory = self.memory[Bytes32::LEN..Bytes32::LEN + MAX_INPUTS as usize * LEN] - .chunks_mut(LEN) - .find(|chunk| &chunk[..Color::LEN] == color.as_ref()) - .map(|chunk| &mut chunk[Color::LEN..]) - .ok_or(PanicReason::ColorNotFound)?; + let balance_memory = &mut self.memory[offset..offset + WORD_SIZE]; let balance = <[u8; WORD_SIZE]>::try_from(&*balance_memory).expect("Sized chunk expected to fit!"); let balance = Word::from_be_bytes(balance); @@ -132,6 +136,46 @@ impl Interpreter { Ok(()) } + + /// Increase the variable output with a given color. Modifies both the referenced tx and the + /// serialized tx in vm memory. + pub(crate) fn set_variable_output( + &mut self, + out_idx: usize, + color_to_update: Color, + amount_to_set: Word, + owner_to_set: Address, + ) -> Result<(), RuntimeError> { + let outputs = self.tx.outputs(); + + if out_idx >= outputs.len() { + return Err(PanicReason::OutputNotFound.into()); + } + let output = outputs[out_idx]; + match output { + Output::Variable { amount, .. } if amount == 0 => Ok(()), + // if variable output was already set, panic with memory write overlap error. + Output::Variable { amount, .. } if amount != 0 => Err(PanicReason::MemoryWriteOverlap), + _ => Err(PanicReason::ExpectedOutputVariable), + }?; + + // update the local copy of the output + let mut output = Output::variable(owner_to_set, amount_to_set, color_to_update); + + // update serialized memory state + let offset = self.tx.output_offset(out_idx).ok_or(PanicReason::OutputNotFound)?; + let bytes = &mut self.memory[offset..]; + let _ = output.read(bytes)?; + + let outputs = match &mut self.tx { + Transaction::Script { outputs, .. } => outputs, + Transaction::Create { outputs, .. } => outputs, + }; + // update referenced tx + outputs[out_idx] = output; + + Ok(()) + } } #[cfg(all(test, feature = "random"))] @@ -139,6 +183,7 @@ mod tests { use crate::prelude::*; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; + use std::io::Write; #[test] fn external_balance() { @@ -149,6 +194,7 @@ mod tests { let gas_price = 0; let gas_limit = 1_000_000; let maturity = 0; + let byte_price = 0; let script = vec![Opcode::RET(0x01)].iter().copied().collect(); let balances = vec![(rng.gen(), 100), (rng.gen(), 500)]; @@ -161,6 +207,7 @@ mod tests { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, vec![], @@ -179,4 +226,57 @@ mod tests { assert!(vm.external_color_balance_sub(&color, 1).is_err()); } } + + #[test] + fn variable_output_updates_in_memory() { + let mut rng = StdRng::seed_from_u64(2322u64); + + let mut vm = Interpreter::with_memory_storage(); + + let gas_price = 0; + let gas_limit = 1_000_000; + let maturity = 0; + let byte_price = 0; + let color_to_update: Color = rng.gen(); + let amount_to_set: Word = 100; + let owner: Address = rng.gen(); + + let variable_output = Output::Variable { + to: rng.gen(), + amount: 0, + color: rng.gen(), + }; + + let tx = Transaction::script( + gas_price, + gas_limit, + byte_price, + maturity, + vec![], + vec![], + vec![], + vec![variable_output], + vec![Witness::default()], + ); + + vm.init(tx).expect("Failed to init VM!"); + + // increase variable output + vm.set_variable_output(0, color_to_update, amount_to_set, owner) + .unwrap(); + + // verify the referenced tx output is updated properly + assert!(matches!( + vm.tx.outputs()[0], + Output::Variable {amount, color, to} if amount == amount_to_set + && color == color_to_update + && to == owner + )); + + // verify the vm memory is updated properly + let position = vm.tx.output_offset(0).unwrap(); + let mut mem_output = Output::variable(Default::default(), Default::default(), Default::default()); + let _ = mem_output.write(&vm.memory()[position..]).unwrap(); + assert_eq!(vm.tx.outputs()[0], mem_output); + } } diff --git a/src/interpreter/post_execution.rs b/src/interpreter/post_execution.rs new file mode 100644 index 0000000000..a6b875e963 --- /dev/null +++ b/src/interpreter/post_execution.rs @@ -0,0 +1,49 @@ +use crate::error::InterpreterError; +use crate::prelude::{Interpreter, InterpreterStorage}; +use fuel_tx::{Output, Transaction}; +use fuel_types::{Color, Word}; + +impl Interpreter +where + S: InterpreterStorage, +{ + /// Set the appropriate change output values after execution has concluded. + pub(crate) fn update_change_amounts( + &mut self, + unused_gas_cost: Word, + revert: bool, + ) -> Result<(), InterpreterError> { + let init_balances = Self::initial_free_balances(&self.tx)?; + + let mut update_outputs = match &self.tx { + Transaction::Script { outputs, .. } => outputs.clone(), + Transaction::Create { outputs, .. } => outputs.clone(), + }; + // Update each output based on free balance + for output in update_outputs.iter_mut() { + if let Output::Change { color, amount, .. } = output { + let refund = if *color == Color::default() { unused_gas_cost } else { 0 }; + + if revert { + *amount = init_balances[color] + refund; + } else { + let balance = self.external_color_balance(color)?; + *amount = balance + refund; + } + } + if let Output::Variable { amount, .. } = output { + if revert { + // reset amounts to zero on revert + *amount = 0; + } + } + } + // set outputs on tx + match &mut self.tx { + Transaction::Script { outputs, .. } => *outputs = update_outputs, + Transaction::Create { outputs, .. } => *outputs = update_outputs, + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 42a376c9f6..101937cd1d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ pub mod memory_client; pub mod state; pub mod storage; pub mod transactor; +pub mod util; #[cfg(feature = "profile-any")] pub mod profiler; diff --git a/src/memory_client/mod.rs b/src/memory_client/mod.rs index 54ac29f5c1..052178b11c 100644 --- a/src/memory_client/mod.rs +++ b/src/memory_client/mod.rs @@ -90,3 +90,9 @@ impl<'a> From for MemoryClient<'a> { Self::new(s) } } + +impl<'a> From> for Transactor<'a, MemoryStorage> { + fn from(client: MemoryClient<'a>) -> Self { + client.transactor + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000000..7e4451614e --- /dev/null +++ b/src/util.rs @@ -0,0 +1,320 @@ +//! FuelVM utilities + +/// A utility macro for writing scripts with the data offset included. Since the script data offset +/// depends on the length of the script, this macro will evaluate the length and then rewrite the +/// resultant script output with the correct offset (using the offset parameter). +/// +/// # Example +/// +/// ``` +/// use itertools::Itertools; +/// use fuel_types::Word; +/// use fuel_vm::consts::{REG_ONE, REG_ZERO}; +/// use fuel_vm::prelude::{Opcode, Call, SerializableVec, ContractId, Immediate12}; +/// use fuel_vm::script_with_data_offset; +/// +/// // Example of making a contract call script using script_data for the call info and asset id. +/// let contract_id = ContractId::from([0x11; 32]); +/// let call = Call::new(contract_id, 0, 0).to_bytes(); +/// let asset_id = [0x00; 32]; +/// let transfer_amount: Word = 100; +/// let gas_to_forward = 1_000_000; +/// let script_data = [call.as_ref(), asset_id.as_ref()] +/// .into_iter() +/// .flatten() +/// .copied() +/// .collect_vec(); +/// +/// // Use the macro since we don't know the exact offset for script_data. +/// let (script, data_offset) = script_with_data_offset!(data_offset, vec![ +/// // use data_offset to reference the location of the call bytes inside script_data +/// Opcode::ADDI(0x10, REG_ZERO, data_offset), +/// Opcode::ADDI(0x11, REG_ZERO, transfer_amount as Immediate12), +/// // use data_offset again to reference the location of the asset id inside of script data +/// Opcode::ADDI(0x12, REG_ZERO, data_offset + call.len() as Immediate12), +/// Opcode::ADDI(0x13, REG_ZERO, gas_to_forward as Immediate12), +/// Opcode::CALL(0x10, 0x11, 0x12, 0x13), +/// Opcode::RET(REG_ONE), +/// ]); +/// ``` +#[macro_export] +macro_rules! script_with_data_offset { + ($offset:ident, $script:expr) => {{ + use fuel_types::{bytes, Immediate12}; + use $crate::consts::VM_TX_MEMORY; + use $crate::prelude::Transaction; + let $offset = 0 as Immediate12; + let script_bytes: Vec = { $script }.into_iter().collect(); + let data_offset = VM_TX_MEMORY + Transaction::script_offset() + bytes::padded_len(script_bytes.as_slice()); + let $offset = data_offset as Immediate12; + ($script, $offset) + }}; +} + +#[allow(missing_docs)] +#[cfg(any(test, feature = "test-helpers"))] +/// Testing utilities +pub mod test_helpers { + use crate::consts::{REG_ONE, REG_ZERO}; + use crate::prelude::{InterpreterStorage, MemoryClient, MemoryStorage, Transactor}; + use crate::state::StateTransition; + use fuel_asm::Opcode; + use fuel_tx::{Input, Output, Transaction, Witness}; + use fuel_types::{Color, ContractId, Salt, Word}; + use itertools::Itertools; + use rand::prelude::StdRng; + use rand::{Rng, SeedableRng}; + + pub struct CreatedContract { + pub tx: Transaction, + pub contract_id: ContractId, + pub salt: Salt, + } + + pub struct TestBuilder { + rng: StdRng, + gas_price: Word, + gas_limit: Word, + byte_price: Word, + inputs: Vec, + outputs: Vec, + script: Vec, + script_data: Vec, + witness: Vec, + storage: MemoryStorage, + } + + impl TestBuilder { + pub fn new(seed: u64) -> Self { + TestBuilder { + rng: StdRng::seed_from_u64(seed), + gas_price: 0, + gas_limit: 100, + byte_price: 0, + inputs: Default::default(), + outputs: Default::default(), + script: vec![Opcode::RET(REG_ONE)], + script_data: vec![], + witness: vec![Witness::default()], + storage: MemoryStorage::default(), + } + } + + pub fn gas_price(&mut self, price: Word) -> &mut TestBuilder { + self.gas_price = price; + self + } + + pub fn gas_limit(&mut self, limit: Word) -> &mut TestBuilder { + self.gas_limit = limit; + self + } + + pub fn byte_price(&mut self, price: Word) -> &mut TestBuilder { + self.byte_price = price; + self + } + + pub fn change_output(&mut self, color: Color) -> &mut TestBuilder { + self.outputs.push(Output::change(self.rng.gen(), 0, color)); + self + } + + pub fn coin_output(&mut self, color: Color, amount: Word) -> &mut TestBuilder { + self.outputs.push(Output::coin(self.rng.gen(), amount, color)); + self + } + + pub fn withdrawal_output(&mut self, color: Color, amount: Word) -> &mut TestBuilder { + self.outputs.push(Output::withdrawal(self.rng.gen(), amount, color)); + self + } + + pub fn variable_output(&mut self, color: Color) -> &mut TestBuilder { + self.outputs.push(Output::variable(self.rng.gen(), 0, color)); + self + } + + pub fn contract_output(&mut self, id: &ContractId) -> &mut TestBuilder { + let input_idx = self + .inputs + .iter() + .find_position(|input| matches!(input, Input::Contract {contract_id, ..} if contract_id == id)) + .expect("expected contract input with matching contract id"); + self.outputs + .push(Output::contract(input_idx.0 as u8, self.rng.gen(), self.rng.gen())); + self + } + + pub fn coin_input(&mut self, color: Color, amount: Word) -> &mut TestBuilder { + self.inputs.push(Input::coin( + self.rng.gen(), + self.rng.gen(), + amount, + color, + 0, + 0, + vec![], + vec![], + )); + self + } + + pub fn contract_input(&mut self, contract_id: ContractId) -> &mut TestBuilder { + self.inputs.push(Input::contract( + self.rng.gen(), + self.rng.gen(), + self.rng.gen(), + contract_id, + )); + self + } + + pub fn script(&mut self, script: Vec) -> &mut TestBuilder { + self.script = script; + self + } + + pub fn script_data(&mut self, script_data: Vec) -> &mut TestBuilder { + self.script_data = script_data; + self + } + + pub fn witness(&mut self, witness: Vec) -> &mut TestBuilder { + self.witness = witness; + self + } + + pub fn storage(&mut self, storage: MemoryStorage) -> &mut TestBuilder { + self.storage = storage; + self + } + + pub fn build(&mut self) -> Transaction { + Transaction::script( + self.gas_price, + self.gas_limit, + self.byte_price, + 0, + self.script.iter().copied().collect(), + self.script_data.clone(), + self.inputs.clone(), + self.outputs.clone(), + self.witness.clone(), + ) + } + + pub fn build_get_balance_tx(contract_id: &ContractId, asset_id: &Color) -> Transaction { + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + Opcode::ADDI(0x11, REG_ZERO, data_offset), + Opcode::ADDI(0x12, 0x11, Color::LEN as Immediate12), + Opcode::BAL(0x10, 0x11, 0x12), + Opcode::LOG(0x10, REG_ZERO, REG_ZERO, REG_ZERO), + Opcode::RET(REG_ONE) + ] + ); + + let script_data: Vec = [asset_id.as_ref(), contract_id.as_ref()] + .into_iter() + .flatten() + .copied() + .collect(); + + TestBuilder::new(2322u64) + .gas_price(0) + .byte_price(0) + .gas_limit(1_000_000) + .script(script) + .script_data(script_data) + .contract_input(*contract_id) + .contract_output(contract_id) + .build() + } + + pub fn setup_contract( + &mut self, + contract: Vec, + initial_balance: Option<(Color, Word)>, + ) -> CreatedContract { + let salt: Salt = self.rng.gen(); + let program: Witness = contract.iter().copied().collect::>().into(); + let contract = crate::contract::Contract::from(program.as_ref()); + let contract_root = contract.root(); + let contract_id = contract.id(&salt, &contract_root); + + let tx = Transaction::create( + self.gas_price, + self.gas_limit, + self.byte_price, + 0, + 0, + salt, + vec![], + vec![], + vec![Output::contract_created(contract_id)], + vec![program], + ); + + // setup a contract in current test state + let state = self.execute_tx(tx); + + // set initial contract balance + if let Some((asset_id, amount)) = initial_balance { + self.storage + .merkle_contract_color_balance_insert(&contract_id, &asset_id, amount) + .unwrap(); + } + + CreatedContract { + tx: state.tx().clone(), + contract_id, + salt, + } + } + + fn execute_tx(&mut self, tx: Transaction) -> StateTransition { + let mut client = MemoryClient::new(self.storage.clone()); + client.transact(tx); + let storage = client.as_ref().clone(); + let txtor: Transactor<_> = client.into(); + let state = txtor.state_transition().unwrap().into_owned(); + self.storage = storage; + state + } + + /// Build test tx and execute it + pub fn execute(&mut self) -> StateTransition { + let tx = self.build(); + self.execute_tx(tx) + } + + pub fn execute_get_outputs(&mut self) -> Vec { + self.execute().tx().outputs().to_vec() + } + + pub fn execute_get_change(&mut self, find_color: Color) -> Word { + let outputs = self.execute_get_outputs(); + let change = outputs.into_iter().find_map(|output| { + if let Output::Change { amount, color, .. } = output { + if &color == &find_color { + Some(amount) + } else { + None + } + } else { + None + } + }); + change.expect(format!("no change matching color {:x} was found", &find_color).as_str()) + } + + pub fn get_contract_balance(&mut self, contract_id: &ContractId, asset_id: &Color) -> Word { + let tx = TestBuilder::build_get_balance_tx(contract_id, asset_id); + let state = self.execute_tx(tx); + let receipts = state.receipts(); + receipts[0].ra().expect("Balance expected") + } + } +} diff --git a/tests/alu.rs b/tests/alu.rs index 1058505c1c..42e7916295 100644 --- a/tests/alu.rs +++ b/tests/alu.rs @@ -6,6 +6,7 @@ fn alu(registers_init: &[(RegisterId, Immediate12)], op: Opcode, reg: RegisterId let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let script = registers_init @@ -14,7 +15,17 @@ fn alu(registers_init: &[(RegisterId, Immediate12)], op: Opcode, reg: RegisterId .chain([op, Opcode::LOG(reg, 0, 0, 0), Opcode::RET(REG_ONE)].iter().copied()) .collect(); - let tx = Transaction::script(gas_price, gas_limit, maturity, script, vec![], vec![], vec![], vec![]); + let tx = Transaction::script( + gas_price, + gas_limit, + byte_price, + maturity, + script, + vec![], + vec![], + vec![], + vec![], + ); let receipts = Transactor::new(storage) .transact(tx) .receipts() @@ -32,6 +43,7 @@ fn alu_err(registers_init: &[(RegisterId, Immediate12)], op: Opcode) { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let script = registers_init @@ -40,7 +52,17 @@ fn alu_err(registers_init: &[(RegisterId, Immediate12)], op: Opcode) { .chain([op, Opcode::RET(REG_ONE)].iter().copied()) .collect(); - let tx = Transaction::script(gas_price, gas_limit, maturity, script, vec![], vec![], vec![], vec![]); + let tx = Transaction::script( + gas_price, + gas_limit, + byte_price, + maturity, + script, + vec![], + vec![], + vec![], + vec![], + ); let receipts = Transactor::new(storage) .transact(tx) .receipts() diff --git a/tests/backtrace.rs b/tests/backtrace.rs index d350dc7748..f6c128b8d7 100644 --- a/tests/backtrace.rs +++ b/tests/backtrace.rs @@ -15,6 +15,7 @@ fn backtrace() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; #[rustfmt::skip] @@ -35,6 +36,7 @@ fn backtrace() { let tx_deploy = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -75,6 +77,7 @@ fn backtrace() { let tx_deploy = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -111,6 +114,7 @@ fn backtrace() { let tx_script = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.into_iter().collect(), vec![], diff --git a/tests/blockchain.rs b/tests/blockchain.rs index 9397e10bbb..7e93009400 100644 --- a/tests/blockchain.rs +++ b/tests/blockchain.rs @@ -17,6 +17,7 @@ fn state_read_write() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let salt: Salt = rng.gen(); @@ -113,6 +114,7 @@ fn state_read_write() { let tx_deploy = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -174,6 +176,7 @@ fn state_read_write() { let tx_add_word = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.clone(), script_data, @@ -222,6 +225,7 @@ fn state_read_write() { let tx_unpack_xor = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -276,6 +280,7 @@ fn load_external_contract_code() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; // Start by creating and deploying a new example contract @@ -298,6 +303,7 @@ fn load_external_contract_code() { let tx_create_target = Transaction::create( gas_price, gas_limit, + byte_price, maturity, 0, salt, @@ -349,6 +355,7 @@ fn load_external_contract_code() { let tx_deploy_loader = Transaction::script( gas_price, gas_limit, + byte_price, maturity, load_contract.clone().into_iter().collect(), vec![], @@ -364,6 +371,7 @@ fn load_external_contract_code() { let tx_deploy_loader = Transaction::script( gas_price, gas_limit, + byte_price, maturity, load_contract.into_iter().collect(), vec![], diff --git a/tests/code_coverage.rs b/tests/code_coverage.rs index 171ab8c8a6..bdc842ddf5 100644 --- a/tests/code_coverage.rs +++ b/tests/code_coverage.rs @@ -2,26 +2,21 @@ use std::sync::{Arc, Mutex}; use fuel_vm::consts::*; use fuel_vm::prelude::*; -use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; - use fuel_vm::profiler::{InstructionLocation, ProfileReceiver, ProfilingData}; const HALF_WORD_SIZE: u64 = 4; #[test] fn code_coverage() { - let rng = &mut StdRng::seed_from_u64(2322u64); - let salt: Salt = rng.gen(); - let gas_price = 1; let gas_limit = 1_000; + let byte_price = 0; let maturity = 0; // Deploy contract with loops let reg_a = 0x20; - let contract_code: Vec = vec![ + let script_code: Vec = vec![ Opcode::JNEI(REG_ZERO, REG_ONE, 2), // Skip next Opcode::XOR(reg_a, reg_a, reg_a), // Skipped Opcode::JNEI(REG_ZERO, REG_ZERO, 2), // Do not skip @@ -29,22 +24,15 @@ fn code_coverage() { Opcode::RET(REG_ONE), ]; - let program: Witness = contract_code.clone().into_iter().collect::>().into(); - let contract = Contract::from(program.as_ref()); - let contract_root = contract.root(); - let contract_id = contract.id(&salt, &contract_root); - - let input = Input::contract(rng.gen(), rng.gen(), rng.gen(), contract_id); - let output = Output::contract(0, rng.gen(), rng.gen()); - - let tx_deploy = Transaction::script( + let tx_script = Transaction::script( gas_price, gas_limit, + byte_price, maturity, - contract_code.clone().into_iter().collect(), + script_code.into_iter().collect(), + vec![], + vec![], vec![], - vec![input], - vec![output], vec![], ); @@ -68,7 +56,7 @@ fn code_coverage() { .into(), ); - let receipts = client.transact(tx_deploy); + let receipts = client.transact(tx_script); if let Some(Receipt::ScriptResult { result, .. }) = receipts.last() { assert!(result.is_success()); diff --git a/tests/contract.rs b/tests/contract.rs index 61e7fc1ee3..31d2b23cab 100644 --- a/tests/contract.rs +++ b/tests/contract.rs @@ -1,5 +1,7 @@ use fuel_vm::consts::*; use fuel_vm::prelude::*; +use fuel_vm::script_with_data_offset; +use fuel_vm::util::test_helpers::TestBuilder; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; @@ -13,6 +15,7 @@ fn mint_burn() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let salt: Salt = rng.gen(); @@ -43,6 +46,7 @@ fn mint_burn() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -68,6 +72,7 @@ fn mint_burn() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, vec![], @@ -84,6 +89,7 @@ fn mint_burn() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -105,6 +111,7 @@ fn mint_burn() { let tx_check_balance = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script_check_balance.iter().copied().collect(), vec![], @@ -119,6 +126,7 @@ fn mint_burn() { let tx_check_balance = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script_check_balance.into_iter().collect(), script_data_check_balance, @@ -145,6 +153,7 @@ fn mint_burn() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -174,6 +183,7 @@ fn mint_burn() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -196,6 +206,7 @@ fn mint_burn() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -211,3 +222,166 @@ fn mint_burn() { .expect("Balance expected"); assert_eq!(0, storage_balance); } + +#[test] +fn internal_transfer_reduces_source_contract_balance_and_increases_destination_contract_balance() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + let gas_limit = 1_000_000; + let asset_id: Color = rng.gen(); + let transfer_amount = 500; + let initial_internal_balance = 1_000_000; + + let mut test_context = TestBuilder::new(2322u64); + let dest_contract_id = test_context.setup_contract(vec![], None).contract_id; + + let program = vec![ + // load amount of tokens + Opcode::ADDI(0x10, REG_FP, CallFrame::a_offset() as Immediate12), + Opcode::LW(0x10, 0x10, 0), + // load color + Opcode::ADDI(0x11, REG_FP, CallFrame::b_offset() as Immediate12), + Opcode::LW(0x11, 0x11, 0), + // load contract id + Opcode::ADDI(0x12, 0x11, 32 as Immediate12), + Opcode::TR(0x12, 0x10, 0x11), + Opcode::RET(REG_ONE), + ]; + let sender_contract_id = test_context + .setup_contract(program, Some((asset_id, initial_internal_balance))) + .contract_id; + + let (script_ops, offset) = script_with_data_offset!( + data_offset, + vec![ + // load call data to 0x10 + Opcode::ADDI(0x10, REG_ZERO, data_offset + 64), + // load gas forward to 0x11 + Opcode::ADDI(0x11, REG_ZERO, gas_limit as Immediate12), + // call the transfer contract + Opcode::CALL(0x10, REG_ZERO, REG_ZERO, 0x11), + Opcode::RET(REG_ONE), + ] + ); + let script_data: Vec = [ + asset_id.as_ref(), + dest_contract_id.as_ref(), + Call::new(sender_contract_id, transfer_amount, offset as Word) + .to_bytes() + .as_slice(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + // assert initial balance state + let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); + assert_eq!(dest_balance, 0); + let source_balance = test_context.get_contract_balance(&sender_contract_id, &asset_id); + assert_eq!(source_balance, initial_internal_balance); + + // initiate the transfer between contracts + let transfer_tx = test_context + .gas_limit(gas_limit) + .gas_price(0) + .byte_price(0) + .contract_input(sender_contract_id) + .contract_input(dest_contract_id) + .contract_output(&sender_contract_id) + .contract_output(&dest_contract_id) + .script(script_ops) + .script_data(script_data) + .execute(); + + // Ensure transfer tx processed correctly + assert!(!transfer_tx.should_revert()); + + // verify balance transfer occurred + let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); + assert_eq!(dest_balance, transfer_amount); + let source_balance = test_context.get_contract_balance(&sender_contract_id, &asset_id); + assert_eq!(source_balance, initial_internal_balance - transfer_amount); +} + +#[test] +fn internal_transfer_cant_exceed_more_than_source_contract_balance() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + let gas_limit = 1_000_000; + let asset_id: Color = rng.gen(); + let transfer_amount = 500; + // set initial internal balance to < transfer amount + let initial_internal_balance = 100; + + let mut test_context = TestBuilder::new(2322u64); + let dest_contract_id = test_context.setup_contract(vec![], None).contract_id; + + let program = vec![ + // load amount of tokens + Opcode::ADDI(0x10, REG_FP, CallFrame::a_offset() as Immediate12), + Opcode::LW(0x10, 0x10, 0), + // load color + Opcode::ADDI(0x11, REG_FP, CallFrame::b_offset() as Immediate12), + Opcode::LW(0x11, 0x11, 0), + // load contract id + Opcode::ADDI(0x12, 0x11, 32 as Immediate12), + Opcode::TR(0x12, 0x10, 0x11), + Opcode::RET(REG_ONE), + ]; + + let sender_contract_id = test_context + .setup_contract(program, Some((asset_id, initial_internal_balance))) + .contract_id; + + let (script_ops, offset) = script_with_data_offset!( + data_offset, + vec![ + // load call data to 0x10 + Opcode::ADDI(0x10, REG_ZERO, data_offset + 64), + // load gas forward to 0x11 + Opcode::ADDI(0x11, REG_ZERO, gas_limit as Immediate12), + // call the transfer contract + Opcode::CALL(0x10, REG_ZERO, REG_ZERO, 0x11), + Opcode::RET(REG_ONE), + ] + ); + let script_data: Vec = [ + asset_id.as_ref(), + dest_contract_id.as_ref(), + Call::new(sender_contract_id, transfer_amount, offset as Word) + .to_bytes() + .as_slice(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + // assert initial balance state + let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); + assert_eq!(dest_balance, 0); + let source_balance = test_context.get_contract_balance(&sender_contract_id, &asset_id); + assert_eq!(source_balance, initial_internal_balance); + + let transfer_tx = test_context + .gas_limit(gas_limit) + .gas_price(0) + .byte_price(0) + .contract_input(sender_contract_id) + .contract_input(dest_contract_id) + .contract_output(&sender_contract_id) + .contract_output(&dest_contract_id) + .script(script_ops) + .script_data(script_data) + .execute(); + + // Ensure transfer tx reverts since transfer amount is too large + assert!(transfer_tx.should_revert()); + + // verify balance transfer did not occur + let dest_balance = test_context.get_contract_balance(&dest_contract_id, &asset_id); + assert_eq!(dest_balance, 0); + let source_balance = test_context.get_contract_balance(&sender_contract_id, &asset_id); + assert_eq!(source_balance, initial_internal_balance); +} diff --git a/tests/crypto.rs b/tests/crypto.rs index 7a11d9f52f..098ad22c11 100644 --- a/tests/crypto.rs +++ b/tests/crypto.rs @@ -13,6 +13,7 @@ fn ecrecover() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let secp = Secp256k1::new(); @@ -78,6 +79,7 @@ fn ecrecover() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.into_iter().collect(), vec![], @@ -102,6 +104,7 @@ fn sha256() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let message = b"I say let the world go to hell, but I should always have my tea."; @@ -159,6 +162,7 @@ fn sha256() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.into_iter().collect(), vec![], @@ -185,6 +189,7 @@ fn keccak256() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let message = b"...and, moreover, I consider it my duty to warn you that the cat is an ancient, inviolable animal."; @@ -245,6 +250,7 @@ fn keccak256() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.into_iter().collect(), vec![], diff --git a/tests/encoding.rs b/tests/encoding.rs index 7a738ab1f4..9063fbb8dd 100644 --- a/tests/encoding.rs +++ b/tests/encoding.rs @@ -172,6 +172,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![0xfa], vec![0xfb, 0xfc], vec![i.clone()], @@ -182,6 +183,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![], vec![0xfb, 0xfc], vec![i.clone()], @@ -192,6 +194,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![0xfa], vec![], vec![i.clone()], @@ -202,6 +205,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![], vec![], vec![i.clone()], @@ -212,6 +216,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![], vec![], vec![], @@ -222,6 +227,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![], vec![], vec![], @@ -232,6 +238,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, vec![], vec![], vec![], @@ -242,6 +249,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, 0xba, [0xdd; 32].into(), vec![[0xce; 32].into()], @@ -253,6 +261,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, 0xba, [0xdd; 32].into(), vec![], @@ -264,6 +273,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, 0xba, [0xdd; 32].into(), vec![], @@ -275,6 +285,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, 0xba, [0xdd; 32].into(), vec![], @@ -286,6 +297,7 @@ fn transaction() { Word::MAX >> 1, Word::MAX >> 2, Word::MAX >> 3, + Word::MAX >> 4, 0xba, [0xdd; 32].into(), vec![], diff --git a/tests/flow.rs b/tests/flow.rs index 30ca2e2b4d..0c17fb0095 100644 --- a/tests/flow.rs +++ b/tests/flow.rs @@ -17,6 +17,7 @@ fn code_copy() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let salt: Salt = rng.gen(); @@ -44,6 +45,7 @@ fn code_copy() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, 0, salt, @@ -77,6 +79,7 @@ fn code_copy() { let mut tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -108,6 +111,7 @@ fn call() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let salt: Salt = rng.gen(); @@ -133,6 +137,7 @@ fn call() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, 0, salt, @@ -160,6 +165,7 @@ fn call() { let mut tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -196,6 +202,7 @@ fn call_frame_code_offset() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let salt: Salt = rng.gen(); @@ -222,6 +229,7 @@ fn call_frame_code_offset() { let deploy = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness_index, salt, @@ -262,6 +270,7 @@ fn call_frame_code_offset() { let script = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script, script_data, @@ -300,6 +309,7 @@ fn revert() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; let salt: Salt = rng.gen(); @@ -339,6 +349,7 @@ fn revert() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -403,6 +414,7 @@ fn revert() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.clone(), script_data, @@ -453,6 +465,7 @@ fn revert() { let tx = Transaction::script( gas_price, gas_limit, + byte_price, maturity, script.clone(), script_data, diff --git a/tests/metadata.rs b/tests/metadata.rs index 15dafe064d..41c85a3277 100644 --- a/tests/metadata.rs +++ b/tests/metadata.rs @@ -12,6 +12,7 @@ fn metadata() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let maturity = 0; #[rustfmt::skip] @@ -40,6 +41,7 @@ fn metadata() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -82,6 +84,7 @@ fn metadata() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness, salt, @@ -120,7 +123,17 @@ fn metadata() { let script = script.iter().copied().collect::>(); - let tx = Transaction::script(gas_price, gas_limit, maturity, script, vec![], inputs, outputs, vec![]); + let tx = Transaction::script( + gas_price, + gas_limit, + byte_price, + maturity, + script, + vec![], + inputs, + outputs, + vec![], + ); let receipts = Transactor::new(&mut storage) .transact(tx) diff --git a/tests/outputs.rs b/tests/outputs.rs new file mode 100644 index 0000000000..33e7d4c0e4 --- /dev/null +++ b/tests/outputs.rs @@ -0,0 +1,543 @@ +use fuel_vm::{ + consts::{REG_FP, REG_ONE, REG_ZERO}, + prelude::*, + script_with_data_offset, + util::test_helpers::TestBuilder, +}; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; + +/// Testing of post-execution output handling + +#[test] +fn full_change_with_no_fees() { + let input_amount = 1000; + let gas_price = 0; + let byte_price = 0; + + let change = TestBuilder::new(2322u64) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(Color::default(), input_amount) + .change_output(Color::default()) + .execute_get_change(Color::default()); + + assert_eq!(change, input_amount); +} + +#[test] +fn byte_fees_are_deducted_from_base_asset_change() { + let input_amount = 1000; + let gas_price = 0; + let byte_price = 1; + + let change = TestBuilder::new(2322u64) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(Color::default(), input_amount) + .change_output(Color::default()) + .execute_get_change(Color::default()); + + assert!(change < input_amount); +} + +#[test] +fn used_gas_is_deducted_from_base_asset_change() { + let input_amount = 1000; + let gas_price = 1; + let byte_price = 0; + + let change = TestBuilder::new(2322u64) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(Color::default(), input_amount) + .change_output(Color::default()) + .execute_get_change(Color::default()); + + assert!(change < input_amount); +} + +#[test] +fn used_gas_is_deducted_from_base_asset_change_on_revert() { + let input_amount = 1000; + let gas_price = 1; + let byte_price = 0; + + let change = TestBuilder::new(2322u64) + .script(vec![ + // Log some dummy data to burn extra gas + Opcode::LOG(REG_ONE, REG_ONE, REG_ONE, REG_ONE), + // Revert transaction + Opcode::RVRT(REG_ONE), + ]) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(Color::default(), input_amount) + .change_output(Color::default()) + .execute_get_change(Color::default()); + + assert!(change < input_amount); +} + +#[test] +fn correct_change_is_provided_for_coin_outputs() { + let input_amount = 1000; + let gas_price = 0; + let byte_price = 0; + let spend_amount = 600; + let color = Color::default(); + + let change = TestBuilder::new(2322u64) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(color, input_amount) + .change_output(color) + .coin_output(color, spend_amount) + .execute_get_change(color); + + assert_eq!(change, input_amount - spend_amount); +} + +#[test] +fn correct_change_is_provided_for_withdrawal_outputs() { + let input_amount = 1000; + let gas_price = 0; + let byte_price = 0; + let spend_amount = 650; + let color = Color::default(); + + let change = TestBuilder::new(2322u64) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(color, input_amount) + .change_output(color) + .withdrawal_output(color, spend_amount) + .execute_get_change(color); + + assert_eq!(change, input_amount - spend_amount); +} + +#[test] +#[should_panic(expected = "ValidationError(TransactionOutputChangeColorDuplicated)")] +fn change_is_not_duplicated_for_each_base_asset_change_output() { + // create multiple change outputs for the base asset and ensure the total change is correct + let input_amount = 1000; + let gas_price = 0; + let byte_price = 0; + let color = Color::default(); + + let outputs = TestBuilder::new(2322u64) + .gas_price(gas_price) + .byte_price(byte_price) + .coin_input(color, input_amount) + .change_output(color) + .change_output(color) + .execute_get_outputs(); + + let mut total_change = 0; + for output in outputs { + if let Output::Change { amount, .. } = output { + total_change += amount; + } + } + // verify total change matches the input amount + assert_eq!(total_change, input_amount); +} + +#[test] +fn change_is_reduced_by_external_transfer() { + let input_amount = 1000; + let transfer_amount: Word = 400; + let gas_price = 0; + let gas_limit = 1_000_000; + let byte_price = 0; + let asset_id = Color::default(); + + // simple dummy contract for transferring value to + let contract_code = vec![Opcode::RET(REG_ONE)]; + + let mut test_context = TestBuilder::new(2322u64); + let contract_id = test_context.setup_contract(contract_code, None).contract_id; + + // setup script for transfer + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // set reg 0x10 to contract id + Opcode::ADDI(0x10, REG_ZERO, data_offset as Immediate12), + // set reg 0x11 to transfer amount + Opcode::ADDI(0x11, REG_ZERO, transfer_amount as Immediate12), + // set reg 0x12 to color + Opcode::ADDI(0x12, REG_ZERO, (data_offset + 32) as Immediate12), + // transfer to contract ID at 0x10, the amount of coins at 0x11, of the color at 0x12 + Opcode::TR(0x10, 0x11, 0x12), + Opcode::RET(REG_ONE), + ] + ); + + let script_data = [contract_id.as_ref(), asset_id.as_ref()] + .into_iter() + .flatten() + .copied() + .collect(); + + // execute and get change + let change = test_context + .gas_price(gas_price) + .gas_limit(gas_limit) + .byte_price(byte_price) + .coin_input(asset_id, input_amount) + .contract_input(contract_id) + .change_output(asset_id) + .contract_output(&contract_id) + .script(script) + .script_data(script_data) + .execute_get_change(asset_id); + + assert_eq!(change, input_amount - transfer_amount); +} + +#[test] +fn change_is_not_reduced_by_external_transfer_on_revert() { + let input_amount = 1000; + // attempt overspend to cause a revert + let transfer_amount: Word = input_amount + 100; + let gas_price = 0; + let gas_limit = 1_000_000; + let byte_price = 0; + let asset_id = Color::default(); + + // setup state for test + // simple dummy contract for transferring value to + let contract_code = vec![Opcode::RET(REG_ONE)]; + + let mut test_context = TestBuilder::new(2322u64); + let contract_id = test_context.setup_contract(contract_code, None).contract_id; + + // setup script for transfer + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // set reg 0x10 to contract id + Opcode::ADDI(0x10, REG_ZERO, data_offset as Immediate12), + // set reg 0x11 to transfer amount + Opcode::ADDI(0x11, REG_ZERO, transfer_amount as Immediate12), + // set reg 0x12 to color + Opcode::ADDI(0x12, REG_ZERO, (data_offset + 32) as Immediate12), + // transfer to contract ID at 0x10, the amount of coins at 0x11, of the color at 0x12 + Opcode::TR(0x10, 0x11, 0x12), + Opcode::RET(REG_ONE), + ] + ); + + let script_data = [contract_id.as_ref(), asset_id.as_ref()] + .into_iter() + .flatten() + .copied() + .collect(); + + // execute and get change + let change = test_context + .gas_price(gas_price) + .gas_limit(gas_limit) + .byte_price(byte_price) + .coin_input(asset_id, input_amount) + .contract_input(contract_id) + .change_output(asset_id) + .contract_output(&contract_id) + .script(script) + .script_data(script_data) + .execute_get_change(asset_id); + + assert_eq!(change, input_amount); +} + +#[test] +fn variable_output_set_by_external_transfer_out() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + // the initial external (coin) balance + let external_balance = 1_000_000; + // the amount to transfer out from external balance + let transfer_amount: Word = 600; + let gas_price = 0; + let gas_limit = 1_000_000; + let byte_price = 0; + let asset_id = Color::default(); + let owner: Address = rng.gen(); + + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // load amount of coins to 0x10 + Opcode::ADDI(0x10, REG_ZERO, data_offset), + Opcode::LW(0x10, 0x10, 0), + // load color to 0x11 + Opcode::ADDI(0x11, REG_ZERO, data_offset + 8), + // load address to 0x12 + Opcode::ADDI(0x12, REG_ZERO, data_offset + 40), + // load output index (0) to 0x13 + Opcode::ADDI(0x13, REG_ZERO, 0), + // call contract without any tokens to transfer in + Opcode::TRO(0x12, 0x13, 0x10, 0x11), + Opcode::RET(REG_ONE), + ] + ); + + let script_data: Vec = [ + transfer_amount.to_be_bytes().as_ref(), + asset_id.as_ref(), + owner.as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + // create and run the tx + let outputs = TestBuilder::new(2322u64) + .gas_price(gas_price) + .gas_limit(gas_limit) + .byte_price(byte_price) + .coin_input(asset_id, external_balance) + .variable_output(asset_id) + .change_output(asset_id) + .script(script) + .script_data(script_data) + .execute_get_outputs(); + + assert!(matches!( + outputs[0], Output::Variable { amount, to, color } + if amount == transfer_amount + && to == owner + && color == asset_id + )); + + assert!(matches!( + outputs[1], Output::Change {amount, color, .. } + if amount == external_balance - transfer_amount + && color == asset_id + )); +} + +#[test] +fn variable_output_not_set_by_external_transfer_out_on_revert() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + // the initial external (coin) balance (set to less than transfer amount to cause a revert) + let external_balance = 100; + // the amount to transfer out from external balance + let transfer_amount: Word = 600; + let gas_price = 0; + let gas_limit = 1_000_000; + let byte_price = 0; + let asset_id = Color::default(); + let owner: Address = rng.gen(); + + let (script, _) = script_with_data_offset!( + data_offset, + vec![ + // load amount of coins to 0x10 + Opcode::ADDI(0x10, REG_ZERO, data_offset), + Opcode::LW(0x10, 0x10, 0), + // load color to 0x11 + Opcode::ADDI(0x11, REG_ZERO, data_offset + 8), + // load address to 0x12 + Opcode::ADDI(0x12, REG_ZERO, data_offset + 40), + // load output index (0) to 0x13 + Opcode::ADDI(0x13, REG_ZERO, 0), + // call contract without any tokens to transfer in + Opcode::TRO(0x12, 0x13, 0x10, 0x11), + Opcode::RET(REG_ONE), + ] + ); + + let script_data: Vec = [ + transfer_amount.to_be_bytes().as_ref(), + asset_id.as_ref(), + owner.as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + // create and run the tx + let outputs = TestBuilder::new(2322u64) + .gas_price(gas_price) + .gas_limit(gas_limit) + .byte_price(byte_price) + .coin_input(asset_id, external_balance) + .variable_output(asset_id) + .change_output(asset_id) + .script(script) + .script_data(script_data) + .execute_get_outputs(); + + assert!(matches!( + outputs[0], Output::Variable { amount, .. } if amount == 0 + )); + + // full input amount is converted into change + assert!(matches!( + outputs[1], Output::Change {amount, color, .. } + if amount == external_balance + && color == asset_id + )); +} + +#[test] +fn variable_output_set_by_internal_contract_transfer_out() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + // the initial contract balance + let internal_balance = 1_000_000; + // the amount to transfer out of a contract + let transfer_amount: Word = 600; + let gas_price = 0; + let gas_limit = 1_000_000; + let byte_price = 0; + let asset_id = Color::default(); + let owner: Address = rng.gen(); + + // setup state for test + let contract_code = vec![ + // load amount of coins to 0x10 + Opcode::ADDI(0x10, REG_FP, CallFrame::a_offset() as Immediate12), + Opcode::LW(0x10, 0x10, 0), + // load color to 0x11 + Opcode::ADDI(0x11, REG_FP, CallFrame::b_offset() as Immediate12), + Opcode::LW(0x11, 0x11, 0), + // load address to 0x12 + Opcode::ADDI(0x12, 0x11, 32 as Immediate12), + // load output index (0) to 0x13 + Opcode::ADDI(0x13, REG_ZERO, 0 as Immediate12), + Opcode::TRO(0x12, 0x13, 0x10, 0x11), + Opcode::RET(REG_ONE), + ]; + let mut test_context = TestBuilder::new(2322u64); + let contract_id = test_context + .setup_contract(contract_code, Some((asset_id, internal_balance))) + .contract_id; + + let (script, data_offset) = script_with_data_offset!( + data_offset, + vec![ + // set reg 0x10 to call data + Opcode::ADDI(0x10, REG_ZERO, (data_offset + 64) as Immediate12), + // set reg 0x11 to transfer amount + Opcode::ADDI(0x11, REG_ZERO, gas_limit as Immediate12), + // call contract without any tokens to transfer in (3rd arg arbitrary when 2nd is zero) + Opcode::CALL(0x10, REG_ZERO, REG_ZERO, 0x11), + Opcode::RET(REG_ONE), + ] + ); + + let script_data: Vec = [ + asset_id.as_ref(), + owner.as_ref(), + Call::new(contract_id, transfer_amount, data_offset as Word) + .to_bytes() + .as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + // create and run the tx + let outputs = test_context + .gas_price(gas_price) + .gas_limit(gas_limit) + .byte_price(byte_price) + .contract_input(contract_id) + .variable_output(asset_id) + .contract_output(&contract_id) + .script(script) + .script_data(script_data) + .execute_get_outputs(); + + assert!(matches!( + outputs[0], Output::Variable { amount, to, color } + if amount == transfer_amount + && to == owner + && color == asset_id + )); +} + +#[test] +fn variable_output_not_increased_by_contract_transfer_out_on_revert() { + let rng = &mut StdRng::seed_from_u64(2322u64); + + // the initial contract balance (set to zero so TRO will intentionally fail) + let internal_balance = 0; + // the amount to transfer out of a contract + let transfer_amount: Word = 600; + let gas_price = 0; + let gas_limit = 1_000_000; + let byte_price = 0; + let asset_id = Color::default(); + let owner: Address = rng.gen(); + + // setup state for test + let contract_code = vec![ + // load amount of coins to 0x10 + Opcode::ADDI(0x10, REG_FP, CallFrame::a_offset() as Immediate12), + Opcode::LW(0x10, 0x10, 0), + // load color to 0x11 + Opcode::ADDI(0x11, REG_FP, CallFrame::b_offset() as Immediate12), + Opcode::LW(0x11, 0x11, 0), + // load to address to 0x12 + Opcode::ADDI(0x12, 0x11, 32 as Immediate12), + // load output index (0) to 0x13 + Opcode::ADDI(0x13, REG_ZERO, 0 as Immediate12), + Opcode::TRO(0x12, 0x13, 0x10, 0x11), + Opcode::RET(REG_ONE), + ]; + + let mut test_context = TestBuilder::new(2322u64); + let contract_id = test_context + .setup_contract(contract_code, Some((asset_id, internal_balance))) + .contract_id; + + let (script, data_offset) = script_with_data_offset!( + data_offset, + vec![ + // set reg 0x10 to call data + Opcode::ADDI(0x10, REG_ZERO, (data_offset + 64) as Immediate12), + // set reg 0x11 to gas forward amount + Opcode::ADDI(0x11, REG_ZERO, gas_limit as Immediate12), + // call contract without any tokens to transfer in + Opcode::CALL(0x10, REG_ZERO, 0x10, 0x11), + Opcode::RET(REG_ONE), + ] + ); + + let script_data: Vec = [ + asset_id.as_ref(), + owner.as_ref(), + Call::new(contract_id, transfer_amount, data_offset as Word) + .to_bytes() + .as_ref(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + // create and run the tx + let outputs = test_context + .gas_price(gas_price) + .gas_limit(gas_limit) + .byte_price(byte_price) + .contract_input(contract_id) + .variable_output(asset_id) + .contract_output(&contract_id) + .script(script) + .script_data(script_data) + .execute_get_outputs(); + + assert!(matches!( + outputs[0], Output::Variable { amount, .. } if amount == 0 + )); +} diff --git a/tests/predicate.rs b/tests/predicate.rs index a7c517541d..433194b9de 100644 --- a/tests/predicate.rs +++ b/tests/predicate.rs @@ -58,6 +58,7 @@ fn predicate() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let bytecode_witness_index = 0; let static_contracts = vec![]; let inputs = vec![input]; @@ -67,6 +68,7 @@ fn predicate() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness_index, salt, @@ -133,6 +135,7 @@ fn predicate_false() { let gas_price = 0; let gas_limit = 1_000_000; + let byte_price = 0; let bytecode_witness_index = 0; let static_contracts = vec![]; let inputs = vec![input]; @@ -142,6 +145,7 @@ fn predicate_false() { let tx = Transaction::create( gas_price, gas_limit, + byte_price, maturity, bytecode_witness_index, salt, diff --git a/tests/profile_gas.rs b/tests/profile_gas.rs index 47c06d933d..f0dfbae343 100644 --- a/tests/profile_gas.rs +++ b/tests/profile_gas.rs @@ -2,18 +2,13 @@ use std::sync::{Arc, Mutex}; use fuel_vm::consts::*; use fuel_vm::prelude::*; -use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; - use fuel_vm::profiler::{ProfileReceiver, ProfilingData}; #[test] fn profile_gas() { - let rng = &mut StdRng::seed_from_u64(2322u64); - let salt: Salt = rng.gen(); - let gas_price = 1; let gas_limit = 1_000; + let byte_price = 0; let maturity = 0; // Deploy contract with loops @@ -21,7 +16,7 @@ fn profile_gas() { let case_out_of_gas = 1_000; let mut rounds = [2, 12, 22, case_out_of_gas].into_iter().map(|count| { - let contract_code: Vec = vec![ + let script_code: Vec = vec![ Opcode::XOR(reg_a, reg_a, reg_a), // r[a] := 0 Opcode::ORI(reg_a, reg_a, count), // r[a] := count Opcode::SUBI(reg_a, reg_a, 1), // r[a] -= count <-| @@ -29,22 +24,15 @@ fn profile_gas() { Opcode::RET(REG_ONE), ]; - let program: Witness = contract_code.clone().into_iter().collect::>().into(); - let contract = Contract::from(program.as_ref()); - let contract_root = contract.root(); - let contract_id = contract.id(&salt, &contract_root); - - let input = Input::contract(rng.gen(), rng.gen(), rng.gen(), contract_id); - let output = Output::contract(0, rng.gen(), rng.gen()); - let tx_deploy = Transaction::script( gas_price, gas_limit, + byte_price, maturity, - contract_code.clone().into_iter().collect(), + script_code.into_iter().collect(), + vec![], + vec![], vec![], - vec![input], - vec![output], vec![], );