diff --git a/src/interpreter/executors/debug.rs b/src/interpreter/executors/debug.rs index 2aa4070160..a2bfcdad0a 100644 --- a/src/interpreter/executors/debug.rs +++ b/src/interpreter/executors/debug.rs @@ -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!(), diff --git a/src/interpreter/executors/instruction.rs b/src/interpreter/executors/instruction.rs index f2c8805708..85eaa0c99b 100644 --- a/src/interpreter/executors/instruction.rs +++ b/src/interpreter/executors/instruction.rs @@ -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) @@ -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)), diff --git a/src/interpreter/executors/main.rs b/src/interpreter/executors/main.rs index 97a5e101e4..a486433381 100644 --- a/src/interpreter/executors/main.rs +++ b/src/interpreter/executors/main.rs @@ -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)); } - - _ => (), } } } diff --git a/src/interpreter/executors/predicate.rs b/src/interpreter/executors/predicate.rs index 6354940e32..87e0c6ccf0 100644 --- a/src/interpreter/executors/predicate.rs +++ b/src/interpreter/executors/predicate.rs @@ -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 { diff --git a/src/interpreter/flow.rs b/src/interpreter/flow.rs index 4eac637288..034e793640 100644 --- a/src/interpreter/flow.rs +++ b/src/interpreter/flow.rs @@ -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) } @@ -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(()) + } } diff --git a/src/state.rs b/src/state.rs index 5875a39c8c..2c7a6d37a5 100644 --- a/src/state.rs +++ b/src/state.rs @@ -21,6 +21,7 @@ pub enum ExecuteState { Proceed, Return(Word), ReturnData(Bytes32), + Revert(Word), #[cfg(feature = "debug")] DebugEvent(DebugEval), @@ -44,6 +45,7 @@ impl From for ExecuteState { pub enum ProgramState { Return(Word), ReturnData(Bytes32), + Revert(Word), #[cfg(feature = "debug")] RunProgram(DebugEval), diff --git a/tests/flow.rs b/tests/flow.rs index 7c6744643b..b6be71dfa8 100644 --- a/tests/flow.rs +++ b/tests/flow.rs @@ -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; @@ -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 = 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 = 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::>() + .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::>(); + + // 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::>(); + + 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]), + } +}