#![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::*;
const SOURCE_LOOP_UNTIL: &str = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
function loopUntil(uint256 target) external pure returns (uint256) {
for (uint256 i = 0; i < 100000; i++) {
if (i >= target) return i;
}
return 0xfffffff;
}
}
"#;
const SOURCE_BUMP_N: &str = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
uint256 public counter;
function bumpN(uint256 n) external {
for (uint256 i = 0; i < n; i++) {
counter += 1;
}
}
function getCounter() external view returns (uint256) {
return counter;
}
}
"#;
const SOURCE_SPIN: &str = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
function spin(uint256 n) external pure returns (uint256) {
uint256 acc = 0;
for (uint256 i = 0; i < n; i++) {
acc += i;
}
return acc;
}
}
"#;
const SOURCE_PER_CALL_KEY: &str = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
mapping(uint256 => uint256) public m;
function writeKey(uint256 idx) external {
m[idx] = idx + 1;
}
}
"#;
const SOURCE_FIB: &str = r#"// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract C {
function fib(uint256 n) external view returns (uint256) {
if (n < 2) return n;
return this.fib(n - 1) + this.fib(n - 2);
}
}
"#;
fn compile_one(src: &str, label: &str) -> neo_devpack_solidity::cli::CompilationArtifacts {
let arts = compile_contracts(src, false, 2)
.unwrap_or_else(|e| panic!("convergence_props {} compile: {:?}", label, e));
assert!(
!arts.is_empty(),
"convergence_props {} produced no artifacts",
label
);
arts.into_iter().next().unwrap()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn loop_convergence_with_break(target in 0u64..=1000) {
let art = compile_one(SOURCE_LOOP_UNTIL, "loopUntil");
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("loopUntil runtime");
let r = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest,
"loopUntil",
&[StackItem::Integer(target as i64)],
).expect("loopUntil host-level");
prop_assert!(
r.success,
"loopUntil({}) faulted: {:?}",
target,
r.exception.as_ref().map(|e| e.message.clone()),
);
let v = decode_uint_le(&r.return_data);
prop_assert_eq!(
v.clone(),
num_bigint::BigUint::from(target),
"loopUntil({}) must return {} (early-break); got {} (rd_hex={})",
target, target, v, hex::encode(&r.return_data),
);
}
#[test]
fn empty_loop_terminates(n in 0u64..=200) {
let art = compile_one(SOURCE_SPIN, "spin");
let mut rt = NeoRuntime::new(RuntimeConfig::default())
.expect("spin runtime");
let r = rt.call_method(
&art.bytecode, &art.tokens, &art.manifest,
"spin",
&[StackItem::Integer(n as i64)],
).expect("spin host-level");
prop_assert!(
r.success,
"spin({}) faulted: {:?}",
n,
r.exception.as_ref().map(|e| e.message.clone()),
);
let observed = decode_uint_le(&r.return_data);
let expected = num_bigint::BigUint::from(n * n.saturating_sub(1) / 2);
prop_assert_eq!(
observed.clone(), expected.clone(),
"spin({}) must return n*(n-1)/2 = {}; got {} (rd_hex={})",
n, expected, observed, hex::encode(&r.return_data),
);
}
}
#[test]
fn repeated_storage_writes_stable_gas() {
let art = compile_one(SOURCE_BUMP_N, "bumpN");
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("bumpN runtime");
let mut gas_per_call: [u64; 3] = [0; 3];
for (idx, slot) in gas_per_call.iter_mut().enumerate() {
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"bumpN",
&[StackItem::Integer(50)],
)
.expect("bumpN host-level");
assert!(
r.success,
"bumpN call #{} faulted: {:?}. If only call #2/#3 fault, the \
storage overlay is leaking writes from the previous call \
(e.g. drain_dirty_storage_overlay isn't running on commit), \
or the gas tracker isn't being reset between calls.",
idx + 1,
r.exception.as_ref().map(|e| &e.message),
);
*slot = r.gas_used;
}
let r_get = rt
.call_method(&art.bytecode, &art.tokens, &art.manifest, "getCounter", &[])
.expect("getCounter host-level");
assert!(
r_get.success,
"getCounter faulted: {:?}",
r_get.exception.as_ref().map(|e| &e.message),
);
let counter_val = decode_uint_le(&r_get.return_data);
assert_eq!(
counter_val,
num_bigint::BigUint::from(150u64),
"counter must equal 150 after 3 × bumpN(50); got {} (rd_hex={}). \
If lower than 150, storage writes did not persist across calls. \
If higher, the overlay is being committed multiple times.",
counter_val,
hex::encode(&r_get.return_data),
);
let g_min = *gas_per_call.iter().min().unwrap();
let g_max = *gas_per_call.iter().max().unwrap();
let delta = g_max.saturating_sub(g_min);
let g_min_for_pct = g_min.max(1);
let pct_drift = (delta as f64) / (g_min_for_pct as f64);
assert!(
pct_drift <= 0.20,
"bumpN(50) gas-per-call drifted >20% across 3 sequential calls \
on the SAME NeoRuntime: gas={:?}, min={}, max={}, drift={:.2}%. \
A drift >20% indicates per-call linear-growth fixed costs — most \
commonly: storage_overlay accumulating across calls, or the gas \
tracker not being fully reset between invocations.",
gas_per_call,
g_min,
g_max,
pct_drift * 100.0,
);
}
#[test]
fn memory_overlay_drains_on_call_end() {
let art = compile_one(SOURCE_PER_CALL_KEY, "writeKey");
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("writeKey runtime");
const N: usize = 50;
let mut gas_per_call: Vec<u64> = Vec::with_capacity(N);
for idx in 0..N {
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"writeKey",
&[StackItem::Integer(idx as i64)],
)
.expect("writeKey host-level");
assert!(
r.success,
"writeKey({}) faulted on call #{}: {:?}. If calls succeed up \
to some N then start failing, the per-call accumulated state \
(storage_overlay, evaluation stack, or memory pool) is not \
being drained on call termination.",
idx,
idx + 1,
r.exception.as_ref().map(|e| &e.message),
);
gas_per_call.push(r.gas_used);
}
let g_first = gas_per_call.first().copied().unwrap_or(0);
let g_last = gas_per_call.last().copied().unwrap_or(0);
let g_first_for_ratio = g_first.max(1);
let ratio = (g_last as f64) / (g_first_for_ratio as f64);
assert!(
ratio <= 5.0,
"writeKey gas-used grew >5x across 50 sequential calls on the \
same NeoRuntime: first={}, last={}, ratio={:.2}x. A linear-in-\
call-count growth signals the storage_overlay (or another \
per-call cache) is not being drained at the end of each call. \
Full series: {:?}",
g_first,
g_last,
ratio,
gas_per_call,
);
let g_min = *gas_per_call.iter().min().unwrap();
let g_max = *gas_per_call.iter().max().unwrap();
let g_min_for_ratio = g_min.max(1);
let max_ratio = (g_max as f64) / (g_min_for_ratio as f64);
assert!(
max_ratio <= 5.0,
"writeKey gas-used max/min ratio across 50 calls = {:.2}x \
(min={}, max={}). A spread >5x even if the *last* call is \
cheap suggests an accumulator that grows then shrinks — also a \
leak shape worth surfacing. Full series: {:?}",
max_ratio,
g_min,
g_max,
gas_per_call,
);
}
#[test]
fn recursive_returns_stable() {
let art = compile_one(SOURCE_FIB, "fib");
let mut rt = NeoRuntime::new(RuntimeConfig::default()).expect("fib rt");
let r = rt
.call_method(
&art.bytecode,
&art.tokens,
&art.manifest,
"fib",
&[StackItem::Integer(10)],
)
.expect("fib host-level");
assert!(
r.success,
"this.fib(10) faulted: {:?}. If exception mentions 'stack \
overflow' or 'gas exceeded', a per-frame state leak is \
compounding across the 177 self-external calls.",
r.exception.as_ref().map(|e| &e.message),
);
let v = decode_uint_le(&r.return_data);
assert_eq!(
v,
num_bigint::BigUint::from(55u64),
"this.fib(10) must return 55; got {} (rd_hex={}). A non-55 \
result indicates frame setup/teardown is corrupting the return \
path on at least one of the 177 self-external invocations.",
v,
hex::encode(&r.return_data),
);
}