use neo_devpack_solidity::runtime::execution::ExecutionContext;
use neo_devpack_solidity::runtime::RuntimeConfig;
use sha2::{Digest, Sha256};
fn syscall_id(name: &str) -> [u8; 4] {
let mut h = Sha256::new();
h.update(name.as_bytes());
let d = h.finalize();
[d[0], d[1], d[2], d[3]]
}
fn push_data(script: &mut Vec<u8>, data: &[u8]) {
assert!(data.len() <= u8::MAX as usize, "push_data length overflow");
script.push(0x0C); script.push(data.len() as u8);
script.extend_from_slice(data);
}
#[test]
fn storage_get_missing_key_returns_empty() {
let get_id = syscall_id("System.Storage.Get");
let mut code = vec![];
let get_context_id = syscall_id("System.Storage.GetContext");
code.push(0x41); code.extend_from_slice(&get_context_id);
push_data(&mut code, b"missing_key");
code.push(0x41); code.extend_from_slice(&get_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert!(ctx.return_data().is_empty());
}
#[test]
fn storage_put_get_roundtrip() {
let put_id = syscall_id("System.Storage.Put");
let get_id = syscall_id("System.Storage.Get");
let get_context_id = syscall_id("System.Storage.GetContext");
let mut code = vec![];
code.extend_from_slice(&[0x57, 0x01, 0x00]);
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x70);
code.push(0x68);
push_data(&mut code, b"test_key");
push_data(&mut code, b"test_value");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68);
push_data(&mut code, b"test_key");
code.push(0x41);
code.extend_from_slice(&get_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
}
#[test]
fn storage_delete_removes_key() {
let put_id = syscall_id("System.Storage.Put");
let delete_id = syscall_id("System.Storage.Delete");
let get_id = syscall_id("System.Storage.Get");
let get_context_id = syscall_id("System.Storage.GetContext");
let mut code = vec![];
code.extend_from_slice(&[0x57, 0x01, 0x00]);
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x70);
code.push(0x68); push_data(&mut code, b"key");
push_data(&mut code, b"value");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"key");
code.push(0x41);
code.extend_from_slice(&delete_id);
code.push(0x68); push_data(&mut code, b"key");
code.push(0x41);
code.extend_from_slice(&get_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert!(
ctx.return_data().is_empty(),
"Deleted key should return empty"
);
}
#[test]
fn storage_find_returns_iterator_token() {
let find_id = syscall_id("System.Storage.Find");
let get_context_id = syscall_id("System.Storage.GetContext");
let mut code = vec![];
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x10);
push_data(&mut code, b"prefix");
code.push(0x41);
code.extend_from_slice(&find_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(
ctx.return_data().len(),
8,
"Iterator token should be 8 bytes"
);
}
#[test]
fn storage_find_with_prefix_matching() {
let put_id = syscall_id("System.Storage.Put");
let find_id = syscall_id("System.Storage.Find");
let get_context_id = syscall_id("System.Storage.GetContext");
let mut code = vec![];
code.extend_from_slice(&[0x57, 0x01, 0x00]);
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x70);
code.push(0x68); push_data(&mut code, b"prefix:aaa");
push_data(&mut code, b"value1");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"prefix:bbb");
push_data(&mut code, b"value2");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"other:ccc");
push_data(&mut code, b"value3");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"prefix"); code.push(0x10); code.push(0x41);
code.extend_from_slice(&find_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert_eq!(ctx.return_data().len(), 8, "Should return iterator token");
}
#[test]
fn storage_put_empty_value_deletes() {
let put_id = syscall_id("System.Storage.Put");
let get_id = syscall_id("System.Storage.Get");
let get_context_id = syscall_id("System.Storage.GetContext");
let mut code = vec![];
code.extend_from_slice(&[0x57, 0x01, 0x00]);
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x70);
code.push(0x68); push_data(&mut code, b"key");
push_data(&mut code, b"original_value");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"key");
push_data(&mut code, b""); code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"key");
code.push(0x41);
code.extend_from_slice(&get_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert!(
ctx.return_data().is_empty(),
"Empty value should delete the key"
);
}
#[test]
fn storage_operations_consume_gas() {
let config = RuntimeConfig {
gas_limit: 5_000_000,
..Default::default()
};
let put_id = syscall_id("System.Storage.Put");
let get_id = syscall_id("System.Storage.Get");
let get_context_id = syscall_id("System.Storage.GetContext");
let mut code = vec![];
code.extend_from_slice(&[0x57, 0x01, 0x00]);
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x70);
code.push(0x68); push_data(&mut code, b"key");
push_data(&mut code, b"value");
code.push(0x41);
code.extend_from_slice(&put_id);
code.push(0x68); push_data(&mut code, b"key");
code.push(0x41);
code.extend_from_slice(&get_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&config).expect("context init");
ctx.initialize(&code, &[]).expect("init");
let gas_before = ctx.gas_used();
while !ctx.step().expect("step").halted {}
let gas_after = ctx.gas_used();
assert!(
gas_after > gas_before,
"Storage operations should consume gas"
);
}
#[test]
fn multiple_storage_contexts() {
let get_context_id = syscall_id("System.Storage.GetContext");
let get_context_readonly_id = syscall_id("System.Storage.GetReadOnlyContext");
let as_readonly_id = syscall_id("System.Storage.AsReadOnly");
let mut code = vec![];
code.extend_from_slice(&[0x57, 0x02, 0x00]);
code.push(0x41);
code.extend_from_slice(&get_context_id);
code.push(0x70);
code.push(0x41);
code.extend_from_slice(&get_context_readonly_id);
code.push(0x71);
code.push(0x68); code.push(0x41);
code.extend_from_slice(&as_readonly_id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
while !ctx.step().expect("step").halted {}
assert!(ctx.return_data().is_empty() || ctx.return_data().len() <= 20);
}
#[test]
fn storage_local_syscalls_fault_as_unknown() {
for name in [
"System.Storage.Local.Get",
"System.Storage.Local.Put",
"System.Storage.Local.Delete",
"System.Storage.Local.Find",
] {
let id = syscall_id(name);
let mut code = vec![];
push_data(&mut code, b"local_value");
push_data(&mut code, b"local_key");
code.push(0x41); code.extend_from_slice(&id);
code.push(0x40);
let mut ctx = ExecutionContext::new(&RuntimeConfig::default()).expect("context init");
ctx.initialize(&code, &[]).expect("init");
let mut fault = None;
loop {
match ctx.step() {
Ok(state) => {
if state.halted {
break;
}
}
Err(err) => {
fault = Some(err);
break;
}
}
}
let err = fault.unwrap_or_else(|| panic!("{name} must FAULT as an unknown syscall"));
let message = err.to_string();
assert!(
message.contains("Unsupported syscall"),
"{name} should fault as an unknown syscall, got: {message}"
);
}
}