use crate::{
consts::VM_MAX_RAM,
prelude::*,
tests::test_helpers::{
assert_panics,
assert_success,
set_full_word,
},
};
use alloc::{
vec,
vec::Vec,
};
use fuel_asm::{
RegId,
op,
};
use super::storage::call_contract_once;
const STATUS: RegId = RegId::new(0x20);
const KEY: RegId = RegId::new(0x21);
const BUF: RegId = RegId::new(0x22);
enum MemOobCase {
OffByOne,
Outside,
Overflow,
}
fn setup_oob_ptr(case: &MemOobCase) -> Vec<Instruction> {
let mut ops = set_full_word(0x11, VM_MAX_RAM);
ops.push(match case {
MemOobCase::OffByOne => op::addi(0x10, 0x11, 0), MemOobCase::Outside => op::addi(0x10, 0x11, 1), MemOobCase::Overflow => op::not(0x10, RegId::ZERO), });
ops
}
#[test]
fn sww_zeroes_remaining_bytes_on_overwrite() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::not(0x10, RegId::ZERO),
op::sw(BUF, 0x10, 0),
op::sw(BUF, 0x10, 1),
op::sw(BUF, 0x10, 2),
op::sw(BUF, 0x10, 3),
op::swwq(KEY, STATUS, BUF, RegId::ONE),
op::movi(0x10, 42),
op::sww(KEY, STATUS, 0x10),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::srwq(BUF, STATUS, KEY, RegId::ONE),
op::movi(0x15, 32),
op::logd(RegId::ZERO, RegId::ZERO, BUF, 0x15),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let mut expected = [0u8; 32];
expected[7] = 42;
for r in receipts {
let Receipt::LogData { data, .. } = r else {
continue;
};
assert_eq!(&**data.as_ref().unwrap(), &expected);
return;
}
panic!("Missing LogData receipt");
}
#[test]
fn sww_status_new_vs_existing() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x10, 99),
op::sww(KEY, STATUS, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::movi(0x10, 100),
op::sww(KEY, STATUS, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let statuses: Vec<u64> = receipts
.iter()
.filter_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.collect();
assert_eq!(
statuses,
vec![1, 0],
"SWW status: 1 for new, 0 for overwrite"
);
}
#[test]
fn srw_reads_word_written_by_sww() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x10, 0xABCD),
op::sww(KEY, STATUS, 0x10),
op::srw(0x11, STATUS, KEY, 0),
op::log(0x11, STATUS, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
for r in &receipts {
let Receipt::Log { ra, rb, .. } = r else {
continue;
};
assert_eq!(*ra, 0xABCD, "SRW should read back written value");
assert_eq!(*rb, 1, "Status should be 1 (slot exists)");
return;
}
panic!("Missing Log receipt");
}
#[test]
fn srw_returns_zero_for_nonexistent_slot() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::srw(0x11, STATUS, KEY, 0),
op::log(0x11, STATUS, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
for r in &receipts {
let Receipt::Log { ra, rb, .. } = r else {
continue;
};
assert_eq!(*ra, 0, "SRW should return 0 for missing slot");
assert_eq!(*rb, 0, "Status should be 0 (slot not set)");
return;
}
panic!("Missing Log receipt");
}
#[test]
fn srw_reads_at_word_offsets() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 1),
op::sw(BUF, 0x10, 0),
op::movi(0x10, 2),
op::sw(BUF, 0x10, 1),
op::movi(0x10, 3),
op::sw(BUF, 0x10, 2),
op::movi(0x10, 4),
op::sw(BUF, 0x10, 3),
op::swwq(KEY, STATUS, BUF, RegId::ONE),
op::srw(0x10, STATUS, KEY, 0),
op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::srw(0x10, STATUS, KEY, 1),
op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::srw(0x10, STATUS, KEY, 2),
op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::srw(0x10, STATUS, KEY, 3),
op::log(0x10, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let values: Vec<u64> = receipts
.iter()
.filter_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.collect();
assert_eq!(
values,
vec![1, 2, 3, 4],
"SRW should read correct word at each offset"
);
}
#[test]
fn srw_panics_when_slot_too_small_for_offset() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 8),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::swri(KEY, BUF, 8),
op::srw(0x11, STATUS, KEY, 1),
op::ret(RegId::ONE),
]);
assert_panics(&receipts, PanicReason::StorageOutOfBounds);
}
#[rstest::rstest]
fn srw_key_out_of_memory_panics(
#[values(MemOobCase::OffByOne, MemOobCase::Outside, MemOobCase::Overflow)]
case: MemOobCase,
) {
let mut program = setup_oob_ptr(&case);
program.extend([op::srw(0x12, STATUS, 0x10, 0), op::ret(RegId::ONE)]);
let receipts = call_contract_once(program);
assert_panics(&receipts, PanicReason::MemoryOverflow);
}
#[test]
fn swwq_writes_correct_data_to_multiple_slots() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 64),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 1),
op::sw(BUF, 0x10, 0),
op::movi(0x10, 2),
op::sw(BUF, 0x10, 1),
op::movi(0x10, 3),
op::sw(BUF, 0x10, 2),
op::movi(0x10, 4),
op::sw(BUF, 0x10, 3),
op::movi(0x10, 5),
op::sw(BUF, 0x10, 4),
op::movi(0x10, 6),
op::sw(BUF, 0x10, 5),
op::movi(0x10, 7),
op::sw(BUF, 0x10, 6),
op::movi(0x10, 8),
op::sw(BUF, 0x10, 7),
op::movi(0x10, 2),
op::swwq(KEY, STATUS, BUF, 0x10),
op::movi(0x15, 64),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 2),
op::srwq(BUF, STATUS, KEY, 0x10),
op::movi(0x15, 64),
op::logd(RegId::ZERO, RegId::ZERO, BUF, 0x15),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let mut expected = [0u8; 64];
for (i, v) in (1u64..=8).enumerate() {
expected[i * 8 + 7] = v as u8;
}
for r in receipts {
let Receipt::LogData { data, .. } = r else {
continue;
};
assert_eq!(&**data.as_ref().unwrap(), &expected);
return;
}
panic!("Missing LogData receipt");
}
#[test]
fn swwq_status_counts_new_slots() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 96),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 3),
op::swwq(KEY, STATUS, BUF, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::movi(0x10, 3),
op::swwq(KEY, STATUS, BUF, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let statuses: Vec<u64> = receipts
.iter()
.filter_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.collect();
assert_eq!(statuses, vec![3, 0], "SWWQ status should count new slots");
}
#[rstest::rstest]
fn swwq_key_out_of_memory_panics(
#[values(MemOobCase::OffByOne, MemOobCase::Outside, MemOobCase::Overflow)]
case: MemOobCase,
) {
let mut program = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
];
program.extend(setup_oob_ptr(&case));
program.extend([
op::swwq(0x10, STATUS, BUF, RegId::ONE),
op::ret(RegId::ONE),
]);
let receipts = call_contract_once(program);
assert_panics(&receipts, PanicReason::MemoryOverflow);
}
#[rstest::rstest]
fn swwq_src_out_of_memory_panics(
#[values(MemOobCase::OffByOne, MemOobCase::Outside, MemOobCase::Overflow)]
case: MemOobCase,
) {
let mut program = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
];
program.extend(setup_oob_ptr(&case));
program.extend([
op::swwq(KEY, STATUS, 0x10, RegId::ONE),
op::ret(RegId::ONE),
]);
let receipts = call_contract_once(program);
assert_panics(&receipts, PanicReason::MemoryOverflow);
}
#[test]
fn srwq_reads_correct_data_from_swwq_slots() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 64),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 0xAA),
op::sb(BUF, 0x10, 0),
op::sb(BUF, 0x10, 7),
op::movi(0x10, 0xBB),
op::sb(BUF, 0x10, 32),
op::sb(BUF, 0x10, 39),
op::movi(0x10, 2),
op::swwq(KEY, STATUS, BUF, 0x10),
op::movi(0x15, 64),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 2),
op::srwq(BUF, STATUS, KEY, 0x10),
op::movi(0x15, 64),
op::logd(RegId::ZERO, RegId::ZERO, BUF, 0x15),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
for r in receipts {
let Receipt::LogData { data, .. } = r else {
continue;
};
let data = data.as_ref().unwrap();
assert_eq!(data[0], 0xAA, "slot 0 byte 0");
assert_eq!(data[7], 0xAA, "slot 0 byte 7");
assert_eq!(data[32], 0xBB, "slot 1 byte 0");
assert_eq!(data[39], 0xBB, "slot 1 byte 7");
assert!(data[1..7].iter().all(|&b| b == 0));
assert!(data[8..32].iter().all(|&b| b == 0));
assert!(data[33..39].iter().all(|&b| b == 0));
assert!(data[40..64].iter().all(|&b| b == 0));
return;
}
panic!("Missing LogData receipt");
}
#[test]
fn srwq_unset_slots_read_as_zeroes_with_false_status() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::not(0x10, RegId::ZERO),
op::sw(BUF, 0x10, 0),
op::sw(BUF, 0x10, 1),
op::sw(BUF, 0x10, 2),
op::sw(BUF, 0x10, 3),
op::srwq(BUF, STATUS, KEY, RegId::ONE),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::movi(0x15, 32),
op::logd(RegId::ZERO, RegId::ZERO, BUF, 0x15),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let mut found_status = false;
let mut found_data = false;
for r in &receipts {
match r {
Receipt::Log { ra, .. } => {
assert_eq!(*ra, 0, "SRWQ status should be 0 when any slot is unset");
found_status = true;
}
Receipt::LogData { data, .. } => {
assert_eq!(
&**data.as_ref().unwrap(),
&[0u8; 32],
"Unset slot should read as zeroes"
);
found_data = true;
}
_ => {}
}
}
assert!(found_status && found_data);
}
#[test]
fn srwq_status_is_true_for_single_set_slot() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::swwq(KEY, STATUS, BUF, RegId::ONE),
op::srwq(BUF, STATUS, KEY, RegId::ONE),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let Some(Receipt::Log { ra, .. }) =
receipts.iter().find(|r| matches!(r, Receipt::Log { .. }))
else {
panic!("Missing Log receipt");
};
assert_eq!(
*ra, 1,
"SRWQ status should be 1 when the single slot is set"
);
}
#[test]
fn srwq_status_true_only_when_all_slots_set() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 64),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::swwq(KEY, STATUS, BUF, RegId::ONE),
op::movi(0x10, 2),
op::srwq(BUF, STATUS, KEY, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::movi(0x10, 2),
op::swwq(KEY, STATUS, BUF, 0x10),
op::movi(0x10, 2),
op::srwq(BUF, STATUS, KEY, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let statuses: Vec<u64> = receipts
.iter()
.filter_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.collect();
assert_eq!(
statuses,
vec![0, 1],
"SRWQ status: 0 if any unset, 1 if all set"
);
}
#[rstest::rstest]
fn srwq_dst_out_of_memory_panics(
#[values(MemOobCase::OffByOne, MemOobCase::Outside, MemOobCase::Overflow)]
case: MemOobCase,
) {
let mut program = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
];
program.extend(setup_oob_ptr(&case));
program.extend([
op::srwq(0x10, STATUS, KEY, RegId::ONE),
op::ret(RegId::ONE),
]);
let receipts = call_contract_once(program);
assert_panics(&receipts, PanicReason::MemoryOverflow);
}
#[rstest::rstest]
fn srwq_key_out_of_memory_panics(
#[values(MemOobCase::OffByOne, MemOobCase::Outside, MemOobCase::Overflow)]
case: MemOobCase,
) {
let mut program = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
];
program.extend(setup_oob_ptr(&case));
program.extend([
op::srwq(BUF, STATUS, 0x10, RegId::ONE),
op::ret(RegId::ONE),
]);
let receipts = call_contract_once(program);
assert_panics(&receipts, PanicReason::MemoryOverflow);
}
#[test]
fn scwq_cleared_slot_is_unset() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x10, 0xFF),
op::sww(KEY, STATUS, 0x10),
op::scwq(KEY, STATUS, RegId::ONE),
op::srw(0x11, STATUS, KEY, 0),
op::log(0x11, STATUS, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
for r in &receipts {
let Receipt::Log { ra, rb, .. } = r else {
continue;
};
assert_eq!(*ra, 0, "Cleared slot should read as 0");
assert_eq!(*rb, 0, "Status should be 0 (slot no longer set)");
return;
}
panic!("Missing Log receipt");
}
#[test]
fn scwq_clears_exact_range_of_slots() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 160),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::movi(0x10, 5),
op::swwq(KEY, STATUS, BUF, 0x10),
op::movi(0x10, 1),
op::sb(KEY, 0x10, 31),
op::movi(0x10, 3),
op::scwq(KEY, STATUS, 0x10),
op::mcli(KEY, 32),
op::srw(0x11, STATUS, KEY, 0),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO), op::movi(0x10, 4),
op::sb(KEY, 0x10, 31),
op::srw(0x11, STATUS, KEY, 0),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO), op::movi(0x10, 2),
op::sb(KEY, 0x10, 31),
op::srw(0x11, STATUS, KEY, 0),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO), op::ret(RegId::ONE),
]);
assert_success(&receipts);
let statuses: Vec<u64> = receipts
.iter()
.filter_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.collect();
assert_eq!(
statuses,
vec![1, 1, 0],
"Slots 0 and 4 should remain; slot 2 should be cleared"
);
}
#[test]
fn scwq_status_all_set_vs_any_unset() {
let receipts = call_contract_once(vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY, RegId::HP),
op::movi(0x15, 64),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::swwq(KEY, STATUS, BUF, RegId::ONE),
op::movi(0x10, 2),
op::scwq(KEY, STATUS, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::movi(0x10, 2),
op::swwq(KEY, STATUS, BUF, 0x10),
op::movi(0x10, 2),
op::scwq(KEY, STATUS, 0x10),
op::log(STATUS, RegId::ZERO, RegId::ZERO, RegId::ZERO),
op::ret(RegId::ONE),
]);
assert_success(&receipts);
let statuses: Vec<u64> = receipts
.iter()
.filter_map(|r| match r {
Receipt::Log { ra, .. } => Some(*ra),
_ => None,
})
.collect();
assert_eq!(
statuses,
vec![0, 1],
"SCWQ status: 0 if any was unset, 1 if all were set"
);
}
#[rstest::rstest]
fn scwq_key_out_of_memory_panics(
#[values(MemOobCase::OffByOne, MemOobCase::Outside, MemOobCase::Overflow)]
case: MemOobCase,
) {
let mut program = setup_oob_ptr(&case);
program.extend([op::scwq(0x10, STATUS, RegId::ONE), op::ret(RegId::ONE)]);
let receipts = call_contract_once(program);
assert_panics(&receipts, PanicReason::MemoryOverflow);
}