Skip to content

Commit

Permalink
Add RVRT instruction implementation (#27)
Browse files Browse the repository at this point in the history
`RVRT` halts the execution of the VM and revert the storage changes.
  • Loading branch information
vlopes11 authored Oct 25, 2021
1 parent 2397abc commit bf6881f
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 5 deletions.
2 changes: 2 additions & 0 deletions src/interpreter/executors/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ where

ProgramState::ReturnData(d) => Ok(ProgramState::ReturnData(d)),

ProgramState::Revert(w) => Ok(ProgramState::Revert(w)),

ProgramState::RunProgram(_) => self.run_program(),

ProgramState::VerifyPredicate(_) => unimplemented!(),
Expand Down
5 changes: 4 additions & 1 deletion src/interpreter/executors/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ where
result = Ok(ExecuteState::ReturnData(digest));
}

Opcode::RVRT(ra) if self.gas_charge(&op).is_ok() && self.revert(self.registers[ra]).is_ok() => {
result = Ok(ExecuteState::Revert(self.registers[ra]))
}

Opcode::ALOC(ra) if self.gas_charge(&op).is_ok() && self.malloc(self.registers[ra]).is_ok() => {}

Opcode::CFEI(imm)
Expand Down Expand Up @@ -467,7 +471,6 @@ where

Opcode::LDC(_ra, _rb, _rc) => result = Err(InterpreterError::OpcodeUnimplemented(op)),
Opcode::SLDC(_ra, _rb, _rc) => result = Err(InterpreterError::OpcodeUnimplemented(op)),
Opcode::RVRT(_ra) => result = Err(InterpreterError::OpcodeUnimplemented(op)),
Opcode::TR(_ra, _rb, _rc) => result = Err(InterpreterError::OpcodeUnimplemented(op)),
Opcode::TRO(_ra, _rb, _rc, _rd) => result = Err(InterpreterError::OpcodeUnimplemented(op)),
Opcode::Undefined => result = Err(InterpreterError::OpcodeFailure(op)),
Expand Down
8 changes: 6 additions & 2 deletions src/interpreter/executors/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,16 @@ where
return Ok(ProgramState::ReturnData(d));
}

ExecuteState::Revert(r) => {
return Ok(ProgramState::Revert(r));
}

ExecuteState::Proceed => (),

#[cfg(feature = "debug")]
ExecuteState::DebugEvent(d) => {
return Ok(ProgramState::RunProgram(d));
}

_ => (),
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/interpreter/executors/predicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ where
// A predicate is not expected to return data
ExecuteState::ReturnData(_) => return Err(InterpreterError::PredicateFailure),

ExecuteState::Revert(r) => return Ok(ProgramState::Revert(r)),

ExecuteState::Proceed => (),

#[cfg(feature = "debug")]
ExecuteState::DebugEvent(d) => {
return Ok(ProgramState::VerifyPredicate(d));
}

_ => (),
}

if self.registers[REG_PC] < pc || self.registers[REG_PC] >= end {
Expand Down
23 changes: 23 additions & 0 deletions src/interpreter/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ where
self.registers[REG_RET] = a;
self.registers[REG_RETL] = 0;

// TODO if ret instruction is in memory boundary, inc_pc shouldn't fail
self.return_from_context(receipt)
}

Expand All @@ -181,4 +182,26 @@ where

self.return_from_context(receipt)
}

pub(crate) fn revert(&mut self, a: Word) -> Result<(), InterpreterError> {
let receipt = Receipt::revert(
self.internal_contract_or_default(),
a,
self.registers[REG_PC],
self.registers[REG_IS],
);

self.receipts.push(receipt);

// TODO
// All OutputContract outputs will have the same amount and stateRoot as on
// initialization.
//
// All OutputVariable outputs will have to and amount of zero.
//
// All OutputContractConditional outputs will have contractID, amount, and
// stateRoot of zero.

Ok(())
}
}
2 changes: 2 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum ExecuteState {
Proceed,
Return(Word),
ReturnData(Bytes32),
Revert(Word),

#[cfg(feature = "debug")]
DebugEvent(DebugEval),
Expand All @@ -44,6 +45,7 @@ impl From<DebugEval> for ExecuteState {
pub enum ProgramState {
Return(Word),
ReturnData(Bytes32),
Revert(Word),

#[cfg(feature = "debug")]
RunProgram(DebugEval),
Expand Down
185 changes: 185 additions & 0 deletions tests/flow.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use fuel_tx::crypto::Hasher;
use fuel_types::bytes;
use fuel_vm::consts::*;
use fuel_vm::prelude::*;
use rand::rngs::StdRng;
Expand Down Expand Up @@ -285,3 +287,186 @@ fn call_frame_code_offset() {
assert_eq!(ssp, fp + stack);
assert_eq!(ssp, sp_p);
}

#[test]
fn revert() {
let rng = &mut StdRng::seed_from_u64(2322u64);

let mut client = MemoryClient::default();

let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = 0;

let salt: Salt = rng.gen();

#[rustfmt::skip]
let call_arguments_parser: Vec<Opcode> = vec![
Opcode::ADDI(0x10, REG_FP, CallFrame::a_offset() as Immediate12),
Opcode::LW(0x10, 0x10, 0),
Opcode::ADDI(0x11, REG_FP, CallFrame::b_offset() as Immediate12),
Opcode::LW(0x11, 0x11, 0),
];

#[rustfmt::skip]
let routine_add_word_to_state: Vec<Opcode> = vec![
Opcode::JNEI(0x10, 0x30, 13), // (0, b) Add word to state
Opcode::LW(0x20, 0x11, 4), // r[0x20] := m[b+32, 8]
Opcode::SRW(0x21, 0x11), // r[0x21] := s[m[b, 32], 8]
Opcode::ADD(0x20, 0x20, 0x21), // r[0x20] += r[0x21]
Opcode::SWW(0x11, 0x20), // s[m[b,32]] := r[0x20]
Opcode::LOG(0x20, 0x21, 0x00, 0x00),
Opcode::RET(REG_ONE),
];

let program: Witness = call_arguments_parser
.into_iter()
.chain(routine_add_word_to_state.into_iter())
.collect::<Vec<u8>>()
.into();

let contract = Contract::from(program.as_ref());
let contract_root = contract.root();
let contract = contract.id(&salt, &contract_root);

let output = Output::contract_created(contract);

let bytecode_witness = 0;
let tx = Transaction::create(
gas_price,
gas_limit,
maturity,
bytecode_witness,
salt,
vec![],
vec![],
vec![output],
vec![program],
);

// Deploy the contract into the blockchain
client.transition(vec![tx]).expect("Failed to transact");

let input = Input::contract(rng.gen(), rng.gen(), rng.gen(), contract);
let output = Output::contract(0, rng.gen(), rng.gen());

// The script needs to locate the data offset at runtime. Hence, we need to know
// upfront the serialized size of the script so we can set the registers
// accordingly.
//
// This variable is created to assert we have correct script size in the
// instructions.
let script_len = 16;

// Based on the defined script length, we set the appropriate data offset
let script_data_offset = VM_TX_MEMORY + Transaction::script_offset() + script_len;
let script_data_offset = script_data_offset as Immediate12;

let script = vec![
Opcode::ADDI(0x10, REG_ZERO, script_data_offset),
Opcode::ADDI(0x11, REG_ZERO, gas_limit as Immediate12),
Opcode::CALL(0x10, REG_ZERO, 0x10, 0x11),
Opcode::RET(REG_ONE),
]
.iter()
.copied()
.collect::<Vec<u8>>();

// Assert the offsets are set correctnly
let offset = VM_TX_MEMORY + Transaction::script_offset() + bytes::padded_len(script.as_slice());
assert_eq!(script_data_offset, offset as Immediate12);

let mut script_data = vec![];

// Routine to be called: Add word to state
let routine: Word = 0;

// Offset of the script data relative to the call data
let call_data_offset = script_data_offset as usize + ContractId::LEN + 2 * WORD_SIZE;
let call_data_offset = call_data_offset as Word;

// Key and value to be added
let key = Hasher::hash(b"some key");
let val: Word = 150;

// Script data containing the call arguments (contract, a, b) and (key, value)
script_data.extend(contract.as_ref());
script_data.extend(&routine.to_be_bytes());
script_data.extend(&call_data_offset.to_be_bytes());
script_data.extend(key.as_ref());
script_data.extend(&val.to_be_bytes());

let tx = Transaction::script(
gas_price,
gas_limit,
maturity,
script.clone(),
script_data,
vec![input.clone()],
vec![output],
vec![],
);

// Assert the initial state of `key` is empty
let state = client.as_ref().contract_state(&contract, &key);
assert_eq!(Bytes32::default(), state.into_owned());

let receipts = client.transition(vec![tx]).expect("Failed to transact");
let state = client.as_ref().contract_state(&contract, &key);

// Assert the state of `key` is mutated to `val`
assert_eq!(&val.to_be_bytes()[..], &state.as_ref()[..WORD_SIZE]);

// Expect the correct receipt
assert_eq!(receipts[1].ra().expect("Register value expected"), val);
assert_eq!(receipts[1].rb().expect("Register value expected"), 0);

// Create a script with revert instruction
let script = vec![
Opcode::ADDI(0x10, REG_ZERO, script_data_offset),
Opcode::ADDI(0x11, REG_ZERO, gas_limit as Immediate12),
Opcode::CALL(0x10, REG_ZERO, 0x10, 0x11),
Opcode::RVRT(REG_ONE),
]
.iter()
.copied()
.collect::<Vec<u8>>();

let mut script_data = vec![];

// Value to be added
let rev: Word = 250;

// Script data containing the call arguments (contract, a, b) and (key, value)
script_data.extend(contract.as_ref());
script_data.extend(&routine.to_be_bytes());
script_data.extend(&call_data_offset.to_be_bytes());
script_data.extend(key.as_ref());
script_data.extend(&rev.to_be_bytes());

let tx = Transaction::script(
gas_price,
gas_limit,
maturity,
script.clone(),
script_data,
vec![input.clone()],
vec![output],
vec![],
);

let receipts = client.transition(vec![tx]).expect("Failed to transact");
let state = client.as_ref().contract_state(&contract, &key);

// Assert the state of `key` is reverted to `val`
assert_eq!(&val.to_be_bytes()[..], &state.as_ref()[..WORD_SIZE]);

// Expect the correct receipt
assert_eq!(receipts[1].ra().expect("Register value expected"), val + rev);
assert_eq!(receipts[1].rb().expect("Register value expected"), val);

match receipts[3] {
Receipt::Revert { ra, .. } if ra == 1 => (),
_ => panic!("Expected revert receipt: {:?}", receipts[3]),
}
}

0 comments on commit bf6881f

Please sign in to comment.