#![allow(unused_imports)]
use super::common::*;
use neo_devpack_solidity::cli::compile_contracts;
use neo_devpack_solidity::runtime::types::StackItem;
use neo_devpack_solidity::runtime::{NeoRuntime, RuntimeConfig};
use proptest::prelude::*;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
enum Op {
Find(Vec<u8>),
Next,
Value,
}
const OP_FIND: u8 = 0x01;
const OP_NEXT: u8 = 0x02;
const OP_VALUE: u8 = 0x03;
const TR_FIND: u8 = 0xF1;
const TR_NEXT: u8 = 0xF2;
const TR_VALUE: u8 = 0xF3;
fn storage_strategy() -> impl Strategy<Value = BTreeMap<Vec<u8>, Vec<u8>>> {
prop::collection::vec(
(
prop::collection::vec(any::<u8>(), 1..=8),
prop::collection::vec(any::<u8>(), 1..=32),
),
3..=50,
)
.prop_map(|pairs| {
let mut m = BTreeMap::new();
for (k, v) in pairs {
m.insert(k, v);
}
m
})
.prop_filter("must produce >= 3 unique keys after dedup", |m| {
m.len() >= 3
})
}
fn ops_strategy(storage: BTreeMap<Vec<u8>, Vec<u8>>) -> impl Strategy<Value = Vec<Op>> {
let keys: Vec<Vec<u8>> = storage.keys().cloned().collect();
let keys_for_strategy = keys.clone();
let prefix_strategy = prop_oneof![
Just(Vec::<u8>::new()),
(0usize..keys_for_strategy.len(), 1usize..=8usize).prop_map(move |(idx, take)| {
let k = &keys_for_strategy[idx];
let n = take.min(k.len());
k[..n].to_vec()
}),
prop::collection::vec(any::<u8>(), 1..=4),
];
let op_strategy = prop_oneof![
4 => prefix_strategy.prop_map(Op::Find),
6 => Just(Op::Next),
4 => Just(Op::Value),
];
prop::collection::vec(op_strategy, 5..=30).prop_map(|ops| {
let needs_prefix_find = matches!(ops.first(), Some(Op::Next | Op::Value));
if needs_prefix_find {
let mut out = Vec::with_capacity(ops.len() + 1);
out.push(Op::Find(Vec::new()));
out.extend(ops);
out
} else {
ops
}
})
}
fn encode_kv(map: &BTreeMap<Vec<u8>, Vec<u8>>) -> Vec<u8> {
let mut out = Vec::new();
for (k, v) in map {
debug_assert!(k.len() <= u8::MAX as usize);
debug_assert!(v.len() <= u8::MAX as usize);
out.push(k.len() as u8);
out.extend_from_slice(k);
out.push(v.len() as u8);
out.extend_from_slice(v);
}
out
}
fn encode_ops(ops: &[Op]) -> Vec<u8> {
let mut out = Vec::new();
for op in ops {
match op {
Op::Find(p) => {
out.push(OP_FIND);
debug_assert!(p.len() <= u8::MAX as usize);
out.push(p.len() as u8);
out.extend_from_slice(p);
}
Op::Next => out.push(OP_NEXT),
Op::Value => out.push(OP_VALUE),
}
}
out
}
fn model_trace(map: &BTreeMap<Vec<u8>, Vec<u8>>, ops: &[Op]) -> Vec<u8> {
let mut trace = Vec::new();
let mut entries: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
let mut cursor: usize = 0;
let mut live = false;
let mut past_end = false;
let mut last_ok = false;
for op in ops {
match op {
Op::Find(prefix) => {
trace.push(TR_FIND);
entries = map
.iter()
.filter(|(k, _)| k.starts_with(prefix))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
cursor = 0;
live = true;
past_end = false;
last_ok = false;
}
Op::Next => {
trace.push(TR_NEXT);
if live && !past_end {
if cursor < entries.len() {
cursor += 1;
last_ok = true;
trace.push(0x01);
} else {
past_end = true;
last_ok = false;
trace.push(0x00);
}
} else {
last_ok = false;
trace.push(0x00);
}
}
Op::Value => {
trace.push(TR_VALUE);
if live && last_ok && cursor >= 1 && cursor <= entries.len() {
let (k, v) = &entries[cursor - 1];
let kl = k.len() as u16;
let vl = v.len() as u16;
trace.extend_from_slice(&kl.to_le_bytes());
trace.extend_from_slice(k);
trace.extend_from_slice(&vl.to_le_bytes());
trace.extend_from_slice(v);
} else {
trace.extend_from_slice(&[0, 0, 0, 0]);
}
}
}
}
trace
}
const SOURCE: &str = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
function runStateMachine(bytes memory kvBlob, bytes memory opTape)
external
returns (bytes memory)
{
// ---- Phase 1: populate storage from kvBlob. ----
uint256 p = 0;
while (p < kvBlob.length) {
uint8 klen = uint8(kvBlob[p]); p += 1;
bytes memory k = new bytes(klen);
for (uint256 i = 0; i < klen; i++) { k[i] = kvBlob[p + i]; }
p += klen;
uint8 vlen = uint8(kvBlob[p]); p += 1;
bytes memory v = new bytes(vlen);
for (uint256 i = 0; i < vlen; i++) { v[i] = kvBlob[p + i]; }
p += vlen;
Storage.put(k, v);
}
// ---- Phase 2: walk opTape and emit trace. ----
// Pre-allocate a max-size buffer (re-trim at the end). Each op
// emits at most 1 (Find) + 2 (Next) + (4 + 8 + 32 = 44) (Value)
// bytes. 64 bytes/op is a comfortable upper bound; opTape itself
// is <=30 ops + Find prefixes (each <=9 bytes), so 64*opTape.length
// covers the worst case with significant headroom.
bytes memory trace = new bytes(64 * (opTape.length + 8) + 64);
uint256 traceLen = 0;
bytes memory it;
bool live = false;
bool pastNext = false;
bool lastOk = false;
uint256 q = 0;
while (q < opTape.length) {
uint8 tag = uint8(opTape[q]); q += 1;
if (tag == 0x01) {
// Find.
uint8 plen = uint8(opTape[q]); q += 1;
bytes memory prefix = new bytes(plen);
for (uint256 i = 0; i < plen; i++) {
prefix[i] = opTape[q + i];
}
q += plen;
it = Storage.find(prefix);
live = true;
pastNext = false;
lastOk = false;
trace[traceLen] = 0xF1; traceLen += 1;
} else if (tag == 0x02) {
// Next.
trace[traceLen] = 0xF2; traceLen += 1;
if (live && !pastNext) {
bool ok = it.next();
lastOk = ok;
if (ok) {
trace[traceLen] = 0x01;
} else {
pastNext = true;
trace[traceLen] = 0x00;
}
} else {
// No iterator OR already past end. Do NOT call
// it.next() again past end — the test asserts no
// double-read.
lastOk = false;
trace[traceLen] = 0x00;
}
traceLen += 1;
} else if (tag == 0x03) {
// Value.
trace[traceLen] = 0xF3; traceLen += 1;
if (live && lastOk) {
bytes memory ck = it.currentKey;
bytes memory cv = it.value();
uint256 klen2 = ck.length;
uint256 vlen2 = cv.length;
trace[traceLen] = bytes1(uint8(klen2 & 0xFF)); traceLen += 1;
trace[traceLen] = bytes1(uint8((klen2 >> 8) & 0xFF)); traceLen += 1;
for (uint256 i = 0; i < klen2; i++) {
trace[traceLen] = ck[i]; traceLen += 1;
}
trace[traceLen] = bytes1(uint8(vlen2 & 0xFF)); traceLen += 1;
trace[traceLen] = bytes1(uint8((vlen2 >> 8) & 0xFF)); traceLen += 1;
for (uint256 i = 0; i < vlen2; i++) {
trace[traceLen] = cv[i]; traceLen += 1;
}
} else {
// No live cursor: emit 0,0,0,0 (klen=0, vlen=0).
trace[traceLen] = 0x00; traceLen += 1;
trace[traceLen] = 0x00; traceLen += 1;
trace[traceLen] = 0x00; traceLen += 1;
trace[traceLen] = 0x00; traceLen += 1;
}
} else {
// Unknown tag.
trace[traceLen] = 0xEE; traceLen += 1;
break;
}
}
// ---- Phase 3: trim trace to actually-emitted length. ----
bytes memory out = new bytes(traceLen);
for (uint256 i = 0; i < traceLen; i++) { out[i] = trace[i]; }
return out;
}
}
"#;
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn storage_iterator_state_machine_matches_model(
(storage, ops) in storage_strategy()
.prop_flat_map(|s| (Just(s.clone()), ops_strategy(s)))
) {
for (k, v) in &storage {
prop_assume!(k.len() <= u8::MAX as usize);
prop_assume!(v.len() <= u8::MAX as usize);
}
let kv_blob = encode_kv(&storage);
let op_tape = encode_ops(&ops);
let expected_trace = model_trace(&storage, &ops);
let arts = compile_contracts(SOURCE, false, 2)
.unwrap_or_else(|e| panic!("storage_iterator_stress compile: {:?}", e));
prop_assert!(!arts.is_empty(), "storage_iterator_stress: no artifacts");
let art = &arts[0];
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("storage_iterator_stress runtime");
let r = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest,
"runStateMachine",
&[
StackItem::byte_array(kv_blob.clone()),
StackItem::byte_array(op_tape.clone()),
],
).expect("runStateMachine host-level");
prop_assert!(
r.success,
"runStateMachine faulted (storage entries={}, ops={}): {:?}\n\
kv_blob_hex={}\nop_tape_hex={}",
storage.len(),
ops.len(),
r.exception.as_ref().map(|e| e.message.clone()),
hex::encode(&kv_blob),
hex::encode(&op_tape),
);
let observed = r.return_data.clone();
prop_assert_eq!(
&observed, &expected_trace,
"iterator state machine trace divergence:\n\
storage entries={}, ops={}\n\
kv_blob_hex={}\n\
op_tape_hex={}\n\
expected_trace_hex={}\n\
observed_trace_hex={}\n\
ops={:?}",
storage.len(),
ops.len(),
hex::encode(&kv_blob),
hex::encode(&op_tape),
hex::encode(&expected_trace),
hex::encode(&observed),
ops,
);
}
#[test]
fn storage_iterator_find_count_matches_btreemap(
storage in storage_strategy(),
prefix_choice in 0u8..3,
prefix_seed in any::<u32>(),
) {
let keys: Vec<Vec<u8>> = storage.keys().cloned().collect();
let prefix: Vec<u8> = match prefix_choice {
0 => Vec::new(),
1 => {
let k = &keys[(prefix_seed as usize) % keys.len()];
let take = ((prefix_seed >> 8) as usize % k.len()).max(1);
k[..take].to_vec()
}
_ => {
let n = ((prefix_seed >> 16) as usize % 4) + 1;
let mut p = Vec::with_capacity(n);
let mut s = prefix_seed;
for _ in 0..n {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
p.push((s & 0xFF) as u8);
}
p
}
};
let expected_count: u64 = storage
.keys()
.filter(|k| k.starts_with(&prefix))
.count() as u64;
let mut ops: Vec<Op> = vec![Op::Find(prefix.clone())];
for _ in 0..(expected_count + 2) {
ops.push(Op::Next);
}
let kv_blob = encode_kv(&storage);
let op_tape = encode_ops(&ops);
let expected_trace = model_trace(&storage, &ops);
let arts = compile_contracts(SOURCE, false, 2)
.unwrap_or_else(|e| panic!("storage_iterator_stress compile: {:?}", e));
let art = &arts[0];
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("storage_iterator_stress runtime");
let r = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest,
"runStateMachine",
&[
StackItem::byte_array(kv_blob.clone()),
StackItem::byte_array(op_tape.clone()),
],
).expect("runStateMachine host-level");
prop_assert!(
r.success,
"runStateMachine (find-count case) faulted: {:?}",
r.exception.as_ref().map(|e| e.message.clone()),
);
prop_assert_eq!(
&r.return_data, &expected_trace,
"find-count trace divergence (prefix_hex={}, expected_count={}):\n\
expected_trace_hex={}\n\
observed_trace_hex={}",
hex::encode(&prefix),
expected_count,
hex::encode(&expected_trace),
hex::encode(&r.return_data),
);
}
}