#![allow(clippy::uninlined_format_args)]
use neo_devpack_solidity::runtime::state::{StateBatch, StateManager};
use neo_devpack_solidity::runtime::types::{StateChange, StateChangeType};
use neo_devpack_solidity::runtime::RuntimeConfig;
use proptest::prelude::*;
fn addr(seed: u8) -> String {
let mut hex = String::from("0x");
for i in 0..20u8 {
hex.push_str(&format!("{:02x}", i.wrapping_add(seed)));
}
hex
}
fn valid_balance_change(account: String, new_balance: u64) -> StateChange {
StateChange {
change_type: StateChangeType::BalanceChange,
account,
key: None,
old_value: None,
new_value: new_balance.to_le_bytes().to_vec(),
}
}
fn invalid_balance_change(account: String) -> StateChange {
StateChange {
change_type: StateChangeType::BalanceChange,
account,
key: None,
old_value: None,
new_value: vec![1, 2, 3, 4, 5, 6, 7],
}
}
fn observe(state: &StateManager, addresses: &[String]) -> Vec<(String, u64, u64)> {
addresses
.iter()
.map(|a| {
let bal = state.get_balance(a).unwrap_or(0);
let nonce = state.get_nonce(a).unwrap_or(0);
(a.clone(), bal, nonce)
})
.collect()
}
fn valid_change_strategy() -> impl Strategy<Value = StateChange> {
(0u8..8u8, 0u64..1_000_000u64).prop_map(|(seed, bal)| valid_balance_change(addr(seed), bal))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn state_batch_atomic_rollback_on_error(
valid in proptest::collection::vec(valid_change_strategy(), 1..=10),
insert_at in 0usize..=10usize,
bad_seed in 0u8..8u8,
) {
let cfg = RuntimeConfig::default();
let mut state = StateManager::new(&cfg).expect("StateManager::new");
for s in 0u8..8u8 {
state.create_account(&addr(s), 100).expect("seed create_account");
}
let touched: Vec<String> = (0u8..8u8).map(addr).collect();
let pre = observe(&state, &touched);
let mut changes = valid.clone();
let pos = insert_at.min(changes.len());
changes.insert(pos, invalid_balance_change(addr(bad_seed)));
let batch = StateBatch { changes, atomic: true };
let result = state.execute_batch(batch);
prop_assert!(result.is_err(), "atomic batch with an invalid change must Err");
let post = observe(&state, &touched);
prop_assert_eq!(
&pre,
&post,
"atomic rollback must restore pre-batch observable state EXACTLY"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn state_batch_non_atomic_continues_past_invalid(
valid in proptest::collection::vec(valid_change_strategy(), 1..=10),
insert_at in 0usize..=10usize,
bad_seed in 0u8..8u8,
) {
let cfg = RuntimeConfig::default();
let mut state = StateManager::new(&cfg).expect("StateManager::new");
for s in 0u8..8u8 {
state.create_account(&addr(s), 100).expect("seed create_account");
}
let touched: Vec<String> = (0u8..8u8).map(addr).collect();
let mut changes = valid.clone();
let pos = insert_at.min(changes.len());
changes.insert(pos, invalid_balance_change(addr(bad_seed)));
let batch = StateBatch { changes: changes.clone(), atomic: false };
let mut expected = StateManager::new(&cfg).expect("expected");
for s in 0u8..8u8 {
expected.create_account(&addr(s), 100).expect("seed expected");
}
for (idx, c) in changes.iter().enumerate() {
if idx == pos { continue; } if let StateChangeType::BalanceChange = c.change_type {
let bytes: [u8; 8] = c.new_value.as_slice().try_into()
.expect("valid bytes by construction");
expected.set_balance(&c.account, u64::from_le_bytes(bytes))
.expect("set_balance");
}
}
let expected_post = observe(&expected, &touched);
let result = state.execute_batch(batch);
prop_assert!(result.is_ok(), "non-atomic batch must not return Err");
let post = observe(&state, &touched);
prop_assert_eq!(
&expected_post,
&post,
"non-atomic batch must apply every well-formed change exactly once"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn state_batch_non_atomic_returns_ok_and_skips_only_invalid(
valid in proptest::collection::vec(valid_change_strategy(), 1..=10),
insert_at in 0usize..=10usize,
bad_seed in 0u8..8u8,
) {
let cfg = RuntimeConfig::default();
let mut state = StateManager::new(&cfg).expect("StateManager::new");
for s in 0u8..8u8 {
state.create_account(&addr(s), 100).expect("seed create_account");
}
let touched: Vec<String> = (0u8..8u8).map(addr).collect();
let mut changes = valid.clone();
let pos = insert_at.min(changes.len());
let bad_addr = addr(bad_seed);
changes.insert(pos, invalid_balance_change(bad_addr.clone()));
let batch = StateBatch { changes: changes.clone(), atomic: false };
let valid_targets: std::collections::HashSet<&String> = valid
.iter()
.map(|c| &c.account)
.collect();
let result = state.execute_batch(batch);
prop_assert!(result.is_ok(), "non-atomic batch must return Ok(())");
let post = observe(&state, &touched);
if !valid_targets.contains(&bad_addr) {
let bad_post = post.iter().find(|(a, _, _)| a == &bad_addr).unwrap();
prop_assert_eq!(
bad_post.1, 100u64,
"malformed change must produce no side effect at addr {}", bad_addr
);
}
for target in &valid_targets {
let last_for_target = valid.iter().rev().find(|c| &c.account == *target);
if let Some(last) = last_for_target {
let bytes: [u8; 8] = last.new_value.as_slice().try_into()
.expect("valid bytes by construction");
let expected_bal = u64::from_le_bytes(bytes);
let actual = post.iter().find(|(a, _, _)| a == *target).unwrap();
prop_assert_eq!(
actual.1, expected_bal,
"addr {} must reflect last valid write (last-write-wins)",
target
);
}
}
}
}