use fuel_asm::RegId;
use fuel_crypto::{
Hasher,
SecretKey,
};
use fuel_tx::{
field::Outputs,
Finalizable,
Input,
Output,
Receipt,
TransactionBuilder,
};
use fuel_types::{
AssetId,
BlockHeight,
};
use itertools::Itertools;
use rand::{
rngs::StdRng,
Rng,
SeedableRng,
};
use crate::{
consts::*,
prelude::*,
};
use crate::script_with_data_offset;
use fuel_asm::{
op,
Instruction,
PanicReason::{
ArithmeticOverflow,
ContractNotInInputs,
ErrorFlag,
ExpectedUnallocatedStack,
MemoryOverflow,
},
};
use fuel_tx::field::Script as ScriptField;
use fuel_vm::util::test_helpers::check_expected_reason_for_instructions;
const SET_STATUS_REG: u8 = 0x39;
const MAX_MEM_SHL: Immediate12 = 26 as Immediate12;
#[test]
fn state_read_write() {
let mut test_context = TestBuilder::new(2322u64);
let gas_limit = 1_000_000;
#[rustfmt::skip]
let function_selector = vec![
op::move_(0x30, RegId::ZERO),
op::move_(0x31, RegId::ONE),
];
#[rustfmt::skip]
let call_arguments_parser = vec![
op::addi(0x10, RegId::FP, CallFrame::a_offset() as Immediate12),
op::lw(0x10, 0x10, 0),
op::addi(0x11, RegId::FP, CallFrame::b_offset() as Immediate12),
op::lw(0x11, 0x11, 0),
];
#[rustfmt::skip]
let routine_add_word_to_state = vec![
op::jnei(0x10, 0x30, 13), op::lw(0x20, 0x11, 4), op::srw(0x21, SET_STATUS_REG, 0x11), op::add(0x20, 0x20, 0x21), op::sww(0x11, SET_STATUS_REG, 0x20), op::log(0x20, 0x21, 0x00, 0x00),
op::ret(RegId::ONE),
];
#[rustfmt::skip]
let routine_unpack_and_xor_limbs_into_state = vec![
op::jnei(0x10, 0x31, 45), op::movi(0x20, 32), op::aloc(0x20), op::move_(0x20, RegId::HP), op::srwq(0x20, SET_STATUS_REG, 0x11, RegId::ONE), op::lw(0x21, 0x11, 4), op::log(0x21, 0x00, 0x00, 0x00),
op::srli(0x22, 0x21, 48), op::srli(0x23, 0x21, 32), op::andi(0x23, 0x23, 0xff), op::srli(0x24, 0x21, 16), op::andi(0x24, 0x24, 0xff), op::andi(0x25, 0x21, 0xff), op::log(0x22, 0x23, 0x24, 0x25),
op::lw(0x26, 0x20, 0), op::xor(0x26, 0x26, 0x22), op::log(0x26, 0x00, 0x00, 0x00),
op::sw(0x20, 0x26, 0), op::lw(0x26, 0x20, 1), op::xor(0x26, 0x26, 0x22), op::log(0x26, 0x00, 0x00, 0x00),
op::sw(0x20, 0x26, 1), op::lw(0x26, 0x20, 2), op::xor(0x26, 0x26, 0x22), op::log(0x26, 0x00, 0x00, 0x00),
op::sw(0x20, 0x26, 2), op::lw(0x26, 0x20, 3), op::xor(0x26, 0x26, 0x22), op::log(0x26, 0x00, 0x00, 0x00),
op::sw(0x20, 0x26, 3), op::swwq(0x11, SET_STATUS_REG, 0x20, RegId::ONE), op::ret(RegId::ONE),
];
#[rustfmt::skip]
let invalid_call = vec![
op::ret(RegId::ZERO),
];
let program = function_selector
.into_iter()
.chain(call_arguments_parser.into_iter())
.chain(routine_add_word_to_state.into_iter())
.chain(routine_unpack_and_xor_limbs_into_state.into_iter())
.chain(invalid_call.into_iter())
.collect_vec();
let contract_id = test_context.setup_contract(program, None, None).contract_id;
let (script, script_data_offset) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset as Immediate18),
op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS),
op::ret(RegId::ONE),
],
test_context.tx_offset()
);
let mut script_data = vec![];
let routine: Word = 0;
let call_data_offset = script_data_offset as usize + ContractId::LEN + 2 * WORD_SIZE;
let call_data_offset = call_data_offset as Word;
let key = Hasher::hash(b"some key");
let val: Word = 150;
script_data.extend(contract_id.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 state = test_context
.get_storage()
.contract_state(&contract_id, &key);
assert_eq!(Bytes32::default(), state.into_owned());
let result = test_context
.start_script(script.clone(), script_data)
.gas_limit(gas_limit)
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.execute();
let receipts = result.receipts();
let state = test_context
.get_storage()
.contract_state(&contract_id, &key);
assert_eq!(&val.to_be_bytes()[..], &state.as_ref()[..WORD_SIZE]);
assert_eq!(receipts[1].ra().expect("Register value expected"), val);
assert_eq!(receipts[1].rb().expect("Register value expected"), 0);
let mut script_data = vec![];
let routine: Word = 1;
let a = 0x25;
let b = 0xc1;
let c = 0xd3;
let d = 0xaa;
let val: Word = (a << 48) | (b << 32) | (c << 16) | d;
script_data.extend(contract_id.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 result = test_context
.start_script(script, script_data)
.gas_limit(gas_limit)
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.execute();
let receipts = result.receipts();
assert_eq!(receipts[1].ra().expect("Register value expected"), val);
assert_eq!(receipts[2].ra().expect("Register value expected"), a);
assert_eq!(receipts[2].rb().expect("Register value expected"), b);
assert_eq!(receipts[2].rc().expect("Register value expected"), c);
assert_eq!(receipts[2].rd().expect("Register value expected"), d);
let m = a ^ 0x96;
let n = a;
let o = a;
let p = a;
assert_eq!(receipts[3].ra().expect("Register value expected"), m);
assert_eq!(receipts[4].ra().expect("Register value expected"), n);
assert_eq!(receipts[5].ra().expect("Register value expected"), o);
assert_eq!(receipts[6].ra().expect("Register value expected"), p);
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&m.to_be_bytes());
bytes[8..16].copy_from_slice(&n.to_be_bytes());
bytes[16..24].copy_from_slice(&o.to_be_bytes());
bytes[24..].copy_from_slice(&p.to_be_bytes());
let bytes = Bytes32::from(bytes);
let state = test_context
.get_storage()
.contract_state(&contract_id, &key);
assert_eq!(bytes, state.into_owned());
}
#[test]
fn load_external_contract_code() {
let rng = &mut StdRng::seed_from_u64(2322u64);
let salt: Salt = rng.gen();
let mut client = MemoryClient::default();
let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = Default::default();
let height = Default::default();
let params = ConsensusParameters::default();
let contract_code = vec![
op::log(RegId::ONE, RegId::ONE, RegId::ONE, RegId::ONE),
op::ret(RegId::ONE),
op::ret(RegId::ZERO), ];
let program: Witness = contract_code.into_iter().collect::<Vec<u8>>().into();
let contract = Contract::from(program.as_ref());
let contract_root = contract.root();
let state_root = Contract::default_state_root();
let contract_id = contract.id(&salt, &contract_root, &state_root);
let input0 = Input::contract(rng.gen(), rng.gen(), rng.gen(), rng.gen(), contract_id);
let output0 = Output::contract_created(contract_id, state_root);
let output1 = Output::contract(0, rng.gen(), rng.gen());
let tx_create_target = TransactionBuilder::create(program.clone(), salt, vec![])
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.add_output(output0)
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
client.deploy(tx_create_target);
let reg_a = 0x20;
let reg_b = 0x21;
let count = ContractId::LEN as Immediate12;
let mut load_contract = vec![
op::xor(reg_a, reg_a, reg_a), op::ori(reg_a, reg_a, count), op::aloc(reg_a), ];
for (i, byte) in contract_id.as_ref().iter().enumerate() {
let index = i as Immediate12;
let value = *byte as Immediate12;
load_contract.extend([
op::xor(reg_a, reg_a, reg_a), op::ori(reg_a, reg_a, value), op::sb(RegId::HP, reg_a, index), ]);
}
load_contract.extend([
op::move_(reg_a, RegId::HP), op::xor(reg_b, reg_b, reg_b), op::ori(reg_b, reg_b, 12),
op::ldc(reg_a, RegId::ZERO, reg_b), op::move_(reg_a, RegId::SSP), op::subi(reg_a, reg_a, 8 * 2), op::xor(reg_b, reg_b, reg_b), op::addi(reg_b, reg_b, 16), op::logd(RegId::ZERO, RegId::ZERO, reg_a, reg_b), op::noop(), ]);
let tx_deploy_loader = TransactionBuilder::script(
#[allow(clippy::iter_cloned_collect)]
load_contract.iter().copied().collect(),
vec![],
)
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_input(input0.clone())
.add_random_fee_input()
.add_output(output1)
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
let transaction_end_addr =
tx_deploy_loader.transaction().serialized_size() - Script::script_offset_static();
*load_contract.last_mut().unwrap() =
op::ji((transaction_end_addr / 4) as Immediate24);
let tx_deploy_loader =
TransactionBuilder::script(load_contract.into_iter().collect(), vec![])
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_input(input0)
.add_random_fee_input()
.add_output(output1)
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
let receipts = client.transact(tx_deploy_loader);
if let Receipt::LogData { digest, .. } = receipts.get(0).expect("No receipt") {
let mut code = program.into_inner();
code.extend([0; 4]);
assert_eq!(digest, &Hasher::hash(&code), "Loaded code digest incorrect");
} else {
panic!("Script did not return a value");
}
if let Receipt::Log { ra, .. } = receipts.get(1).expect("No receipt") {
assert_eq!(*ra, 1, "Invalid log from loaded code");
} else {
panic!("Script did not return a value");
}
}
fn ldc_reason_helper(
cmd: Vec<Instruction>,
expected_reason: PanicReason,
should_patch_jump: bool,
) {
let rng = &mut StdRng::seed_from_u64(2322u64);
let salt: Salt = rng.gen();
let mut client = MemoryClient::new(
MemoryStorage::default(),
Default::default(),
GasCosts::free(),
);
let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = Default::default();
let height = Default::default();
let params = ConsensusParameters::default();
let contract_code = vec![
op::log(RegId::ONE, RegId::ONE, RegId::ONE, RegId::ONE),
op::ret(RegId::ONE),
op::ret(RegId::ZERO), ];
let program: Witness = contract_code.into_iter().collect::<Vec<u8>>().into();
let contract = Contract::from(program.as_ref());
let contract_root = contract.root();
let state_root = Contract::default_state_root();
let contract_id = contract.id(&salt, &contract_root, &state_root);
let input0 = Input::contract(rng.gen(), rng.gen(), rng.gen(), rng.gen(), contract_id);
let output0 = Output::contract_created(contract_id, state_root);
let output1 = Output::contract(0, rng.gen(), rng.gen());
let tx_create_target = TransactionBuilder::create(program, salt, vec![])
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.add_output(output0)
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
client.deploy(tx_create_target);
let mut load_contract: Vec<Instruction>;
let mut tx_deploy_loader;
if !should_patch_jump {
load_contract = cmd;
tx_deploy_loader =
TransactionBuilder::script(load_contract.into_iter().collect(), vec![])
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
} else {
let reg_a = 0x20;
let count = ContractId::LEN as Immediate12;
load_contract = vec![
op::xor(reg_a, reg_a, reg_a), op::ori(reg_a, reg_a, count), op::aloc(reg_a), ];
for (i, byte) in contract_id.as_ref().iter().enumerate() {
let index = i as Immediate12;
let value = *byte as Immediate12;
load_contract.extend([
op::xor(reg_a, reg_a, reg_a), op::ori(reg_a, reg_a, value), op::sb(RegId::HP, reg_a, index), ]);
}
load_contract.extend(cmd);
tx_deploy_loader = TransactionBuilder::script(
load_contract.clone().into_iter().collect(),
vec![],
)
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_input(input0.clone())
.add_random_fee_input()
.add_output(output1)
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
let transaction_end_addr = tx_deploy_loader.transaction().serialized_size()
- Script::script_offset_static();
*load_contract.last_mut().unwrap() =
op::ji((transaction_end_addr / 4) as Immediate24);
tx_deploy_loader =
TransactionBuilder::script(load_contract.into_iter().collect(), vec![])
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_input(input0)
.add_random_fee_input()
.add_output(output1)
.finalize()
.into_checked(height, ¶ms, client.gas_costs())
.expect("failed to check tx");
}
let receipts = client.transact(tx_deploy_loader);
if let Receipt::Panic {
id: _,
reason,
contract_id: actual_contract_id,
..
} = receipts.get(0).expect("No receipt")
{
assert_eq!(
&expected_reason,
reason.reason(),
"Expected {}, found {}",
expected_reason,
reason.reason()
);
if expected_reason == PanicReason::ContractNotInInputs {
assert!(actual_contract_id.is_some());
assert_ne!(actual_contract_id, &Some(contract_id));
};
} else {
panic!("Script should have panicked");
}
}
#[test]
fn ldc_ssp_not_sp() {
let load_contract = vec![
op::cfei(0x1), op::ldc(RegId::ZERO, RegId::ZERO, RegId::ZERO),
];
ldc_reason_helper(load_contract, ExpectedUnallocatedStack, false);
}
#[test]
fn ldc_mem_offset_above_reg_hp() {
let reg_a = 0x20;
let load_contract = vec![
op::move_(reg_a, RegId::HP), op::ldc(RegId::ZERO, RegId::ZERO, reg_a),
];
ldc_reason_helper(load_contract, MemoryOverflow, false);
}
#[test]
fn ldc_contract_id_end_beyond_max_ram() {
let reg_a = 0x20;
let reg_b = 0x21;
let load_contract = vec![
op::move_(reg_a, RegId::HP), op::xor(reg_b, reg_b, reg_b), op::ori(reg_b, reg_b, 12), op::ldc(reg_a, RegId::ZERO, reg_b), ];
ldc_reason_helper(load_contract, MemoryOverflow, false);
}
#[test]
fn ldc_contract_not_in_inputs() {
let reg_a = 0x20;
let reg_b = 0x21;
let load_contract = vec![
op::xor(reg_a, reg_a, reg_a), op::addi(reg_a, reg_a, 1), op::xor(reg_b, reg_b, reg_b), op::ori(reg_b, reg_b, 12), op::ldc(reg_a, RegId::ZERO, reg_b), ];
ldc_reason_helper(load_contract, ContractNotInInputs, false);
}
#[test]
fn ldc_contract_offset_over_length() {
let reg_a = 0x20;
let reg_b = 0x21;
let load_contract = vec![
op::move_(reg_a, RegId::HP), op::xor(reg_b, reg_b, reg_b), op::ori(reg_b, reg_b, 12),
op::ldc(reg_a, reg_a, reg_b), op::move_(reg_a, RegId::SSP), op::subi(reg_a, reg_a, 8 * 2), op::xor(reg_b, reg_b, reg_b), op::ori(reg_b, reg_b, 16), op::logd(RegId::ZERO, RegId::ZERO, reg_a, reg_b), op::noop(), ];
ldc_reason_helper(load_contract, MemoryOverflow, true);
}
#[test]
fn code_copy_a_gt_vmmax_sub_d() {
let reg_a = 0x20;
let code_copy = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::addi(reg_a, reg_a, 1),
op::ccp(reg_a, RegId::ZERO, RegId::ZERO, RegId::ZERO),
];
check_expected_reason_for_instructions(code_copy, MemoryOverflow);
}
#[test]
fn code_copy_b_plus_32_overflow() {
let reg_a = 0x20;
let code_copy = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::ccp(RegId::ZERO, reg_a, RegId::ZERO, RegId::ZERO),
];
check_expected_reason_for_instructions(code_copy, MemoryOverflow);
}
#[test]
fn code_copy_b_gt_vm_max_ram() {
let reg_a = 0x20;
let code_copy = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31),
op::ccp(RegId::ZERO, reg_a, RegId::ZERO, RegId::ZERO),
];
check_expected_reason_for_instructions(code_copy, MemoryOverflow);
}
#[test]
fn code_copy_c_gt_vm_max_ram() {
let reg_a = 0x20;
let code_copy = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::addi(reg_a, reg_a, 1),
op::ccp(RegId::ZERO, RegId::ZERO, reg_a, RegId::ZERO),
];
check_expected_reason_for_instructions(code_copy, MemoryOverflow);
}
#[test]
fn code_root_a_plus_32_overflow() {
let reg_a = 0x20;
let code_root = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::croo(reg_a, RegId::ZERO),
];
check_expected_reason_for_instructions(code_root, ArithmeticOverflow);
}
#[test]
fn code_root_b_plus_32_overflow() {
let reg_a = 0x20;
let code_root = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::croo(RegId::ZERO, reg_a),
];
check_expected_reason_for_instructions(code_root, MemoryOverflow);
}
#[test]
fn code_root_a_over_max_ram() {
let reg_a = 0x20;
let code_root = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::croo(reg_a, RegId::ZERO),
];
check_expected_reason_for_instructions(code_root, MemoryOverflow);
}
#[test]
fn code_root_b_over_max_ram() {
let reg_a = 0x20;
let code_root = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::croo(RegId::ZERO, reg_a),
];
check_expected_reason_for_instructions(code_root, MemoryOverflow);
}
#[test]
fn code_size_b_plus_32_overflow() {
let reg_a = 0x20;
let code_root = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::csiz(reg_a, reg_a),
];
check_expected_reason_for_instructions(code_root, MemoryOverflow);
}
#[test]
fn code_size_b_over_max_ram() {
let reg_a = 0x20;
let code_root = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::csiz(reg_a, reg_a),
];
check_expected_reason_for_instructions(code_root, MemoryOverflow);
}
#[test]
fn sww_sets_status() {
#[rustfmt::skip]
let program = vec![
op::sww(0x30, SET_STATUS_REG, RegId::ZERO),
op::srw(0x31, SET_STATUS_REG + 1, RegId::ZERO),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, 0x00, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 1, 0, 0]);
}
#[test]
fn scwq_clears_status() {
#[rustfmt::skip]
let program = vec![
op::sww(0x30, SET_STATUS_REG, RegId::ZERO),
op::scwq(0x30, SET_STATUS_REG + 1, RegId::ONE),
op::srw(0x30, SET_STATUS_REG + 2, RegId::ZERO),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, SET_STATUS_REG + 2, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 1, 0, 0]);
}
#[test]
fn scwq_clears_status_for_range() {
#[rustfmt::skip]
let program = vec![
op::movi(0x11, 100),
op::aloc(0x11),
op::addi(0x31, RegId::HP, 0x4),
op::addi(0x32, RegId::ONE, 2),
op::scwq(0x31, SET_STATUS_REG, 0x32),
op::addi(0x31, RegId::HP, 0x4),
op::swwq(0x31, SET_STATUS_REG + 1, 0x31, 0x32),
op::addi(0x31, RegId::HP, 0x4),
op::scwq(0x31, SET_STATUS_REG + 2, 0x32),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, SET_STATUS_REG + 2, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 0, 1, 0]);
}
#[test]
fn srw_reads_status() {
#[rustfmt::skip]
let program = vec![
op::sww(0x30, SET_STATUS_REG, RegId::ZERO),
op::srw(0x30, SET_STATUS_REG + 1, RegId::ZERO),
op::srw(0x30, SET_STATUS_REG + 2, RegId::ZERO),
op::srw(0x30, SET_STATUS_REG + 3, RegId::ONE),
op::log(SET_STATUS_REG,
SET_STATUS_REG + 1,
SET_STATUS_REG + 2,
SET_STATUS_REG + 3),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 1, 1, 0]);
}
#[test]
fn srwq_reads_status() {
#[rustfmt::skip]
let program = vec![
op::aloc(0x10),
op::addi(0x31, RegId::HP, 0x5),
op::sww(0x31, SET_STATUS_REG, RegId::ZERO),
op::srwq(0x31, SET_STATUS_REG + 1, 0x31, RegId::ONE),
op::srw(0x31, SET_STATUS_REG + 2, 0x31),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, SET_STATUS_REG + 2, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 1, 1, 0]);
}
#[test]
fn srwq_reads_status_with_range() {
#[rustfmt::skip]
let program = vec![
op::movi(0x11, 100),
op::aloc(0x11),
op::addi(0x31, RegId::HP, 0x5),
op::movi(0x32, 0x2),
op::srwq(0x31, SET_STATUS_REG, 0x31, 0x32),
op::movi(0x32, 0x2),
op::swwq(0x31, SET_STATUS_REG + 1, 0x31, 0x32),
op::movi(0x32, 0x2),
op::srwq(0x31, SET_STATUS_REG + 2, 0x31, 0x32),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, SET_STATUS_REG + 2, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 0, 1, 0]);
}
#[test]
fn swwq_sets_status() {
#[rustfmt::skip]
let program = vec![
op::aloc(0x10),
op::addi(0x31, RegId::HP, 0x5),
op::srw(0x31, SET_STATUS_REG, 0x31),
op::swwq(0x31, SET_STATUS_REG + 1, 0x31, RegId::ONE),
op::srw(0x31, SET_STATUS_REG + 2, 0x31),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, SET_STATUS_REG + 2, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 0, 1, 0]);
}
#[test]
fn swwq_sets_status_with_range() {
#[rustfmt::skip]
let program = vec![
op::movi(0x11, 100),
op::aloc(0x11),
op::movi(0x32, 0x2),
op::swwq(RegId::HP, SET_STATUS_REG, 0x31, 0x32),
op::swwq(RegId::HP, SET_STATUS_REG + 1, 0x31, 0x32),
op::log(SET_STATUS_REG, SET_STATUS_REG + 1, 0x00, 0x00),
op::ret(RegId::ONE),
];
check_receipts_for_program_call(program, vec![0, 1, 0, 0]);
}
fn check_receipts_for_program_call(
program: Vec<Instruction>,
expected_values: Vec<Word>,
) -> bool {
let mut test_context = TestBuilder::new(2322u64);
let gas_limit = 1_000_000;
let contract_id = test_context.setup_contract(program, None, None).contract_id;
let (script, script_data_offset) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset as Immediate18),
op::call(0x10, RegId::ZERO, RegId::ZERO, RegId::CGAS),
op::ret(RegId::ONE),
],
test_context.tx_offset()
);
let mut script_data = vec![];
let routine: Word = 0;
let call_data_offset = script_data_offset as usize + ContractId::LEN + 2 * WORD_SIZE;
let call_data_offset = call_data_offset as Word;
let key = Hasher::hash(b"some key");
let val: Word = 150;
script_data.extend(contract_id.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 state = test_context
.get_storage()
.contract_state(&contract_id, &key);
assert_eq!(Bytes32::default(), state.into_owned());
let result = test_context
.start_script(script, script_data)
.gas_limit(gas_limit)
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.execute();
let receipts = result.receipts();
assert_eq!(
receipts[1].ra().expect("Register value expected"),
expected_values[0]
);
assert_eq!(
receipts[1].rb().expect("Register value expected"),
expected_values[1]
);
assert_eq!(
receipts[1].rc().expect("Register value expected"),
expected_values[2]
);
assert_eq!(
receipts[1].rd().expect("Register value expected"),
expected_values[3]
);
true
}
#[test]
fn state_r_word_b_plus_32_over() {
let reg_a = 0x20;
let state_read_word = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::srw(reg_a, SET_STATUS_REG, reg_a),
];
check_expected_reason_for_instructions(state_read_word, MemoryOverflow);
}
#[test]
fn state_r_word_b_over_max_ram() {
let reg_a = 0x20;
let state_read_word = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::srw(reg_a, SET_STATUS_REG, reg_a),
];
check_expected_reason_for_instructions(state_read_word, MemoryOverflow);
}
#[test]
fn state_r_qword_a_plus_32_over() {
let reg_a = 0x20;
let state_read_qword = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::srwq(reg_a, SET_STATUS_REG, RegId::ZERO, RegId::ONE),
];
check_expected_reason_for_instructions(state_read_qword, MemoryOverflow);
}
#[test]
fn state_r_qword_c_plus_32_over() {
let reg_a = 0x20;
let state_read_qword = vec![
op::movi(0x11, 100),
op::aloc(0x11),
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::srwq(RegId::HP, SET_STATUS_REG, reg_a, RegId::ONE),
];
check_expected_reason_for_instructions(state_read_qword, MemoryOverflow);
}
#[test]
fn state_r_qword_a_over_max_ram() {
let reg_a = 0x20;
let state_read_qword = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::srwq(reg_a, SET_STATUS_REG, RegId::ZERO, RegId::ONE),
];
check_expected_reason_for_instructions(state_read_qword, MemoryOverflow);
}
#[test]
fn state_r_qword_c_over_max_ram() {
let reg_a = 0x20;
let state_read_qword = vec![
op::movi(0x11, 100),
op::aloc(0x11),
op::move_(0x31, RegId::HP),
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::srwq(0x31, SET_STATUS_REG, reg_a, RegId::ONE),
];
check_expected_reason_for_instructions(state_read_qword, MemoryOverflow);
}
#[test]
fn state_w_word_a_plus_32_over() {
let reg_a = 0x20;
let state_write_word = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::sww(reg_a, SET_STATUS_REG, RegId::ZERO),
];
check_expected_reason_for_instructions(state_write_word, MemoryOverflow);
}
#[test]
fn state_w_word_a_over_max_ram() {
let reg_a = 0x20;
let state_write_word = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::sww(reg_a, SET_STATUS_REG, RegId::ZERO),
];
check_expected_reason_for_instructions(state_write_word, MemoryOverflow);
}
#[test]
fn state_w_qword_a_plus_32_over() {
let reg_a = 0x20;
let state_write_qword = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::swwq(reg_a, SET_STATUS_REG, RegId::ZERO, RegId::ONE),
];
check_expected_reason_for_instructions(state_write_qword, MemoryOverflow);
}
#[test]
fn state_w_qword_b_plus_32_over() {
let reg_a = 0x20;
let state_write_qword = vec![
op::xor(reg_a, reg_a, reg_a),
op::not(reg_a, reg_a),
op::subi(reg_a, reg_a, 31 as Immediate12),
op::swwq(RegId::ZERO, SET_STATUS_REG, reg_a, RegId::ONE),
];
check_expected_reason_for_instructions(state_write_qword, ArithmeticOverflow);
}
#[test]
fn state_w_qword_a_over_max_ram() {
let reg_a = 0x20;
let state_write_qword = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31),
op::swwq(reg_a, SET_STATUS_REG, RegId::ZERO, RegId::ONE),
];
check_expected_reason_for_instructions(state_write_qword, MemoryOverflow);
}
#[test]
fn state_w_qword_b_over_max_ram() {
let reg_a = 0x20;
let state_write_qword = vec![
op::xor(reg_a, reg_a, reg_a),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::subi(reg_a, reg_a, 31),
op::swwq(RegId::ZERO, SET_STATUS_REG, reg_a, RegId::ONE),
];
check_expected_reason_for_instructions(state_write_qword, MemoryOverflow);
}
#[test]
fn message_output_b_gt_msg_len() {
let reg_a = 0x20;
let message_output = vec![
op::xor(reg_a, reg_a, reg_a), op::ori(reg_a, reg_a, 1), op::slli(reg_a, reg_a, 20), op::addi(reg_a, reg_a, 1), op::smo(RegId::ZERO, reg_a, RegId::ZERO, RegId::ZERO),
];
check_expected_reason_for_instructions(message_output, ErrorFlag);
}
#[test]
fn message_output_a_b_over() {
let reg_a = 0x20;
let reg_b = 0x21;
let message_output = vec![
op::xor(reg_a, reg_a, reg_a), op::xor(reg_b, reg_b, reg_b), op::not(reg_a, reg_a), op::addi(reg_b, reg_b, 1), op::smo(reg_a, reg_b, RegId::ZERO, RegId::ZERO),
];
check_expected_reason_for_instructions(message_output, MemoryOverflow);
}
#[test]
fn message_output_a_b_gt_max_mem() {
let reg_a = 0x20;
let reg_b = 0x21;
let message_output = vec![
op::xor(reg_a, reg_a, reg_a),
op::xor(reg_b, reg_b, reg_b),
op::ori(reg_a, reg_a, 1),
op::slli(reg_a, reg_a, MAX_MEM_SHL),
op::addi(reg_b, reg_b, 1),
op::smo(reg_a, reg_b, RegId::ZERO, RegId::ZERO),
];
check_expected_reason_for_instructions(message_output, MemoryOverflow);
}
#[test]
fn smo_instruction_works() {
fn execute_test<R>(
rng: &mut R,
inputs: Vec<(u64, Vec<u8>)>,
message_output_amount: Word,
gas_price: Word,
) -> bool
where
R: Rng,
{
let mut client = MemoryClient::default();
let gas_limit = 1_000_000;
let maturity = Default::default();
let block_height = Default::default();
let params = client.params();
let secret = SecretKey::random(rng);
let sender = rng.gen();
let msg_data = [rng.gen::<u8>(), rng.gen::<u8>()];
#[rustfmt::skip]
let script = vec![
op::movi(0x10, 2), op::aloc(0x10), op::movi(0x10, msg_data[0].into()), op::sb(RegId::HP, 0x10, 0), op::movi(0x10, msg_data[1].into()), op::sb(RegId::HP, 0x10, 1), op::movi(0x10, 0), op::movi(0x12, 2), op::movi(0x13, message_output_amount as Immediate24), op::smo(0x10,RegId::HP,0x12,0x13),
op::ret(RegId::ONE)
];
let script = script.into_iter().collect();
let script_data = vec![];
let mut tx = TransactionBuilder::script(script, script_data);
tx.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity);
for (amount, data) in inputs {
tx.add_unsigned_message_input(secret, sender, rng.gen(), amount, data);
}
let tx = tx
.add_output(Output::Change {
to: Default::default(),
amount: 0,
asset_id: Default::default(),
})
.add_random_fee_input()
.with_params(*params)
.finalize_checked(block_height, client.gas_costs());
let non_retryable_free_balance =
tx.metadata().non_retryable_balances[&AssetId::BASE];
let retryable_balance: u64 = tx.metadata().retryable_balance.into();
let txid = tx.transaction().id(¶ms.chain_id);
let receipts = client.transact(tx);
let success = receipts.iter().any(|r| {
matches!(
r,
Receipt::ScriptResult {
result: ScriptExecutionResult::Success,
..
}
)
});
let state = client.state_transition().expect("tx was executed");
let message_receipt = state.receipts().iter().find_map(|o| match o {
Receipt::MessageOut {
recipient,
amount,
data,
..
} => Some((*recipient, *amount, data.clone())),
_ => None,
});
assert_eq!(message_receipt.is_some(), success);
if let Some((recipient, transferred, data)) = message_receipt {
assert_eq!(txid.as_ref(), recipient.as_ref());
assert_eq!(message_output_amount, transferred);
assert_eq!(data.unwrap(), msg_data);
}
let gas_used = if let Receipt::ScriptResult { gas_used, .. } =
state.receipts().last().unwrap()
{
gas_used
} else {
panic!("expected script result")
};
let refund_amount =
TransactionFee::gas_refund_value(client.params(), *gas_used, gas_price)
.unwrap();
if !success {
assert!(
matches!(state.tx().outputs()[0], Output::Change { amount, ..} if amount == non_retryable_free_balance + refund_amount)
);
} else {
assert!(
matches!(state.tx().outputs()[0], Output::Change { amount, ..} if amount == non_retryable_free_balance + refund_amount + retryable_balance - message_output_amount)
);
}
success
}
let rng = &mut StdRng::seed_from_u64(2322u64);
assert!(execute_test(rng, vec![(10, vec![0xfa; 15])], 10, 0));
assert!(execute_test(rng, vec![(0, vec![0xfa; 15])], 0, 0));
assert!(!execute_test(rng, vec![(10, vec![0xfa; 15])], 11, 0));
assert!(!execute_test(
rng,
vec![(10, vec![0xfa; 15]), (1000, vec![])],
1011,
1
));
assert!(execute_test(
rng,
vec![(10, vec![0xfa; 15]), (1000, vec![])],
50,
1
));
}
#[test]
fn timestamp_works() {
let mut client = MemoryClient::default();
let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = Default::default();
let block_height = Default::default();
let params = *client.params();
let cases = vec![
(0, 0),
(0, 1),
(5, 0),
(5, 5),
(5, 6),
(10, 0),
(10, 5),
(10, 10),
(10, 1_000),
];
for (height, input) in cases {
client.as_mut().set_block_height(height.into());
let expected = client
.as_ref()
.timestamp(input.into())
.expect("failed to calculate timestamp");
#[rustfmt::skip]
let script = vec![
op::movi(0x11, input), op::time(0x10, 0x11), op::log(0x10, 0x00, 0x00, 0x00), op::ret(RegId::ONE)
];
let script = script.into_iter().collect();
let script_data = vec![];
let tx = TransactionBuilder::script(script, script_data)
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.with_params(params)
.finalize_checked(block_height, client.gas_costs());
let receipts = client.transact(tx);
let result = receipts.iter().any(|r| {
matches!(
r,
Receipt::ScriptResult {
result: ScriptExecutionResult::Success,
..
}
)
});
assert_eq!(result, input <= height);
if result {
let ra = receipts
.iter()
.find_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.expect("failed to fetch log");
assert_eq!(ra, expected);
}
}
}
#[rstest::rstest]
fn block_height_works(#[values(0, 1, 2, 10, 100)] current_height: u32) {
let current_height: BlockHeight = current_height.into();
let mut client = MemoryClient::default();
let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = Default::default();
let params = *client.params();
client.as_mut().set_block_height(current_height);
#[rustfmt::skip]
let script = vec![
op::bhei(0x20), op::log(0x20, 0, 0, 0), op::ret(RegId::ONE)
];
let script = script.into_iter().collect();
let script_data = vec![];
let tx = TransactionBuilder::script(script, script_data)
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.with_params(params)
.finalize_checked(current_height, client.gas_costs());
let receipts = client.transact(tx);
let Some(Receipt::Log { ra, .. }) = receipts.first() else {
panic!("expected log receipt");
};
let r: u32 = (*ra).try_into().unwrap();
let result: BlockHeight = r.into();
assert_eq!(result, current_height);
}
#[rstest::rstest]
fn block_hash_works(
#[values(0, 1, 2, 10, 100)] current_height: u32,
#[values(0, 1, 2, 10, 100)] test_height: u32,
) {
let current_height: BlockHeight = current_height.into();
let test_height: BlockHeight = test_height.into();
let mut client = MemoryClient::default();
let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = Default::default();
let params = *client.params();
client.as_mut().set_block_height(current_height);
let expected = client
.as_ref()
.block_hash(test_height)
.expect("failed to calculate block hash");
#[rustfmt::skip]
let script = vec![
op::movi(0x10, 32), op::aloc(0x10), op::movi(0x11, test_height.into()), op::bhsh(RegId::HP, 0x11), op::logd(0, 0, RegId::HP, 0x10), op::ret(RegId::ONE)
];
let script = script.into_iter().collect();
let script_data = vec![];
let tx = TransactionBuilder::script(script, script_data)
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.with_params(params)
.finalize_checked(current_height, client.gas_costs());
let receipts = client.transact(tx);
let Some(Receipt::LogData { data, .. }) = receipts.first() else {
panic!("expected log receipt");
};
assert_eq!(data.as_ref().unwrap(), &*expected);
}
#[rstest::rstest]
fn coinbase_works() {
let mut client = MemoryClient::default();
let gas_price = 0;
let gas_limit = 1_000_000;
let maturity = Default::default();
let params = *client.params();
let expected = client
.as_ref()
.coinbase()
.expect("failed to calculate block hash");
#[rustfmt::skip]
let script = vec![
op::movi(0x10, 32), op::aloc(0x10), op::cb(RegId::HP), op::logd(0, 0, RegId::HP, 0x10), op::ret(RegId::ONE)
];
let script = script.into_iter().collect();
let script_data = vec![];
let tx = TransactionBuilder::script(script, script_data)
.gas_price(gas_price)
.gas_limit(gas_limit)
.maturity(maturity)
.add_random_fee_input()
.with_params(params)
.finalize_checked(10.into(), client.gas_costs());
let receipts = client.transact(tx);
let Some(Receipt::LogData { data, .. }) = receipts.first() else {
panic!("expected log receipt");
};
assert_eq!(data.as_ref().unwrap(), &*expected);
}