use crate::{
prelude::*,
script_with_data_offset,
tests::test_helpers::assert_success,
util::test_helpers::TestBuilder,
};
use alloc::{
vec,
vec::Vec,
};
use consensus_parameters::gas::{
GasCostsValues,
GasCostsValuesV7,
};
use fuel_asm::{
RegId,
op,
};
use fuel_types::canonical::Serialize;
const KEY1: RegId = RegId::new(0x21);
const KEY2: RegId = RegId::new(0x22);
const BUF: RegId = RegId::new(0x23);
const DST: RegId = RegId::new(0x24);
const COLD_BASE: u64 = 100;
const HOT_BASE: u64 = 1;
const SLOT_LEN: u8 = 32;
fn hot_cold_gas_costs() -> GasCosts {
let v7 = GasCostsValuesV7 {
storage_read_cold: DependentCost::LightOperation {
base: COLD_BASE,
units_per_gas: u64::MAX,
},
storage_read_hot: DependentCost::LightOperation {
base: HOT_BASE,
units_per_gas: u64::MAX,
},
..GasCostsValuesV7::free()
};
GasCosts::new(GasCostsValues::V7(v7))
}
fn run_contract(program: Vec<Instruction>, gas_costs: GasCosts) -> Vec<Receipt> {
let mut test_context = TestBuilder::new(2322u64);
test_context.with_gas_costs(gas_costs);
let contract_id = test_context.setup_contract(program, None, None).contract_id;
let (script, _) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset as Immediate18),
op::call(0x10, RegId::ZERO, 0x10, RegId::CGAS),
op::ret(RegId::ONE),
],
test_context.get_tx_params().tx_offset()
);
let script_data = Call::new(contract_id, 0, 0).to_bytes();
let result = test_context
.start_script(script, script_data)
.script_gas_limit(10_000_000)
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.execute();
result.receipts().to_vec()
}
fn gas_used(receipts: &[Receipt]) -> u64 {
let Some(Receipt::ScriptResult { gas_used, .. }) = receipts.last() else {
panic!("Last receipt must be ScriptResult");
};
*gas_used
}
#[test]
fn read_after_write_is_hot() {
let common: Vec<Instruction> = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY1, RegId::HP),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY2, RegId::HP),
op::movi(0x10, 1),
op::sb(KEY2, 0x10, 31),
op::movi(0x15, SLOT_LEN as _),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::swri(KEY1, BUF, SLOT_LEN as _),
op::movi(0x15, SLOT_LEN as _),
op::aloc(0x15),
op::move_(DST, RegId::HP),
];
let hot_program: Vec<Instruction> = common
.iter()
.cloned()
.chain([
op::srdi(DST, KEY1, RegId::ZERO, SLOT_LEN),
op::ret(RegId::ONE),
])
.collect();
let cold_program: Vec<Instruction> = common
.into_iter()
.chain([
op::srdi(DST, KEY2, RegId::ZERO, SLOT_LEN),
op::ret(RegId::ONE),
])
.collect();
let gas_costs = hot_cold_gas_costs();
let receipts_hot = run_contract(hot_program, gas_costs.clone());
let receipts_cold = run_contract(cold_program, gas_costs);
assert_success(&receipts_hot);
assert_success(&receipts_cold);
let hot = gas_used(&receipts_hot);
let cold = gas_used(&receipts_cold);
assert!(
cold > hot,
"Cold read ({cold}) should cost more gas than hot read ({hot})"
);
assert_eq!(
cold - hot,
COLD_BASE - HOT_BASE,
"Gas difference should be exactly COLD_BASE - HOT_BASE"
);
}
#[test]
fn cache_not_shared_across_contracts() {
let read_program: Vec<Instruction> = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY1, RegId::HP),
op::movi(0x15, SLOT_LEN as _),
op::aloc(0x15),
op::move_(DST, RegId::HP),
op::srdi(DST, KEY1, RegId::ZERO, SLOT_LEN),
op::ret(RegId::ONE),
];
let gas_costs = hot_cold_gas_costs();
let mut test_context = TestBuilder::new(2322u64);
test_context.with_gas_costs(gas_costs);
let contract_a = test_context
.setup_contract(read_program.clone(), None, None)
.contract_id;
let contract_b = test_context
.setup_contract(read_program, None, None)
.contract_id;
let call_size = Call::new(contract_a, 0, 0).to_bytes().len() as u32;
let (script, _) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset as Immediate18),
op::call(0x10, RegId::ZERO, 0x10, RegId::CGAS), op::movi(0x10, (data_offset + call_size) as Immediate18),
op::call(0x10, RegId::ZERO, 0x10, RegId::CGAS), op::ret(RegId::ONE),
],
test_context.get_tx_params().tx_offset()
);
let mut script_data = Call::new(contract_a, 0, 0).to_bytes();
script_data.extend(Call::new(contract_b, 0, 0).to_bytes());
let receipts = test_context
.start_script(script, script_data)
.script_gas_limit(10_000_000)
.contract_input(contract_a)
.contract_input(contract_b)
.fee_input()
.contract_output(&contract_a)
.contract_output(&contract_b)
.execute()
.receipts()
.to_vec();
assert_success(&receipts);
assert_eq!(
gas_used(&receipts),
2 * COLD_BASE,
"Two different contracts reading the same key should both pay cold gas"
);
}
#[test]
fn scwq_clears_populate_cache() {
const STATUS: RegId = RegId::new(0x25);
let common: Vec<Instruction> = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY1, RegId::HP),
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY2, RegId::HP),
op::movi(0x10, 1),
op::sb(KEY2, 0x10, 31),
op::movi(0x15, SLOT_LEN as _),
op::aloc(0x15),
op::move_(BUF, RegId::HP),
op::swwq(KEY1, STATUS, BUF, RegId::ONE),
op::scwq(KEY1, STATUS, RegId::ONE),
op::movi(0x15, SLOT_LEN as _),
op::aloc(0x15),
op::move_(DST, RegId::HP),
];
let hot_program: Vec<Instruction> = common
.iter()
.cloned()
.chain([
op::srdi(DST, KEY1, RegId::ZERO, SLOT_LEN),
op::ret(RegId::ONE),
])
.collect();
let cold_program: Vec<Instruction> = common
.into_iter()
.chain([
op::srdi(DST, KEY2, RegId::ZERO, SLOT_LEN),
op::ret(RegId::ONE),
])
.collect();
let gas_costs = hot_cold_gas_costs();
let receipts_hot = run_contract(hot_program, gas_costs.clone());
let receipts_cold = run_contract(cold_program, gas_costs);
assert_success(&receipts_hot);
assert_success(&receipts_cold);
let hot = gas_used(&receipts_hot);
let cold = gas_used(&receipts_cold);
assert_eq!(
cold - hot,
COLD_BASE - HOT_BASE,
"After SCWQ, reading a cleared slot should be a hot cache hit"
);
}
#[test]
fn cache_persists_across_calls() {
let read_program: Vec<Instruction> = vec![
op::movi(0x15, 32),
op::aloc(0x15),
op::move_(KEY1, RegId::HP),
op::movi(0x15, SLOT_LEN as _),
op::aloc(0x15),
op::move_(DST, RegId::HP),
op::srdi(DST, KEY1, RegId::ZERO, SLOT_LEN),
op::ret(RegId::ONE),
];
let gas_costs = hot_cold_gas_costs();
let mut test_context = TestBuilder::new(2322u64);
test_context.with_gas_costs(gas_costs);
let contract_id = test_context
.setup_contract(read_program, None, None)
.contract_id;
let (script, _) = script_with_data_offset!(
data_offset,
vec![
op::movi(0x10, data_offset as Immediate18),
op::call(0x10, RegId::ZERO, 0x10, RegId::CGAS), op::movi(0x10, data_offset as Immediate18),
op::call(0x10, RegId::ZERO, 0x10, RegId::CGAS), op::ret(RegId::ONE),
],
test_context.get_tx_params().tx_offset()
);
let script_data = Call::new(contract_id, 0, 0).to_bytes();
let receipts = test_context
.start_script(script, script_data)
.script_gas_limit(10_000_000)
.contract_input(contract_id)
.fee_input()
.contract_output(&contract_id)
.execute()
.receipts()
.to_vec();
assert_success(&receipts);
assert_eq!(
gas_used(&receipts),
COLD_BASE + HOT_BASE,
"Cache should persist across the call-return boundary"
);
}