use super::super::*;
use super::helpers::*;
use crate::{FastHashMap, FastHashSet};
use anchor_lang::prelude::Clock;
use litesvm::LiteSVM;
use solana_account::Account;
use solana_pubkey::Pubkey;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
#[test]
fn test_stateless_restore_vs_stateful_restore_selective_empty_delta() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let pk4 = Pubkey::new_unique();
let pk5 = Pubkey::new_unique();
let pks = [pk1, pk2, pk3, pk4, pk5];
let accts: Vec<Account> = (1..=5)
.map(|i| make_account(i * 100, &[i as u8; 8]))
.collect();
let mut svm_a = LiteSVM::new();
for (pk, acct) in pks.iter().zip(accts.iter()) {
svm_a.set_account(*pk, acct.clone()).unwrap();
}
let tracked: HashSet<Pubkey> = pks.iter().copied().collect();
let snap = SvmSnapshot::take(&svm_a, &tracked);
let mut svm_b = LiteSVM::new();
for (pk, acct) in pks.iter().zip(accts.iter()) {
svm_b.set_account(*pk, acct.clone()).unwrap();
}
let initial = SvmSnapshot::take_all(&svm_b);
let dirty_pks = [pk1, pk3, pk5];
for pk in &dirty_pks {
let modified = make_account(9999, &[0xFF; 8]);
svm_a.set_account(*pk, modified.clone()).unwrap();
svm_b.set_account(*pk, modified).unwrap();
}
let mut dirty = DirtyTracker::new();
for pk in &dirty_pks {
dirty.mark_account_dirty(pk);
}
snap.restore(&mut svm_a, &dirty);
let divergent: FastHashSet<Pubkey> = dirty_pks.iter().copied().collect();
let empty_delta = SvmSnapshot::empty(svm_b.get_sysvar::<Clock>());
initial.restore_selective(&mut svm_b, &divergent, &empty_delta);
for (i, pk) in pks.iter().enumerate() {
let a = svm_a.get_account(pk).expect("SVM A missing account");
let b = svm_b.get_account(pk).expect("SVM B missing account");
assert_eq!(
a.lamports, b.lamports,
"pk[{}] lamports mismatch: A={} B={}",
i, a.lamports, b.lamports
);
assert_eq!(a.data, b.data, "pk[{}] data mismatch", i);
}
}
#[test]
fn test_restore_selective_from_equivalent_to_restore_selective() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let pk4 = Pubkey::new_unique();
let mut svm_setup = LiteSVM::new();
let initial_accts: Vec<(Pubkey, Account)> = vec![
(pk1, make_account(100, &[1; 16])),
(pk2, make_account(200, &[2; 16])),
(pk3, make_account(300, &[3; 16])),
(pk4, make_account(400, &[4; 16])),
];
for (pk, acct) in &initial_accts {
svm_setup.set_account(*pk, acct.clone()).unwrap();
}
let initial = SvmSnapshot::take_all(&svm_setup);
let delta_a = {
let mut map = FastHashMap::default();
map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 16])));
map.insert(pk2, Arc::new(make_account(2000, &[0xA2; 16])));
SvmSnapshot {
accounts: map,
sysvars: make_test_sysvars(10),
}
};
let delta_b = {
let mut map = FastHashMap::default();
map.insert(pk2, delta_a.accounts.get(&pk2).unwrap().clone()); map.insert(pk3, Arc::new(make_account(3000, &[0xB3; 16])));
SvmSnapshot {
accounts: map,
sysvars: make_test_sysvars(20),
}
};
let mut svm1 = LiteSVM::new();
for (pk, acct) in &initial_accts {
svm1.set_account(*pk, acct.clone()).unwrap();
}
let divergent_a: FastHashSet<Pubkey> = delta_a.accounts.keys().copied().collect();
initial.restore_selective(&mut svm1, &FastHashSet::default(), &delta_a);
let mut divergent_for_b: FastHashSet<Pubkey> = divergent_a.clone();
initial.restore_selective(&mut svm1, &divergent_for_b, &delta_b);
let mut svm2 = LiteSVM::new();
for (pk, acct) in &initial_accts {
svm2.set_account(*pk, acct.clone()).unwrap();
}
initial.restore_selective(&mut svm2, &FastHashSet::default(), &delta_a);
divergent_for_b = delta_a.accounts.keys().copied().collect();
let empty_exec_dirty = FastHashSet::default();
initial.restore_selective_from(
&mut svm2,
&divergent_for_b,
&delta_a,
&delta_b,
&empty_exec_dirty,
);
for pk in &[pk1, pk2, pk3, pk4] {
let a1 = svm1.get_account(pk);
let a2 = svm2.get_account(pk);
match (a1, a2) {
(Some(a), Some(b)) => {
assert_eq!(a.lamports, b.lamports, "{:?} lamports differ", pk);
assert_eq!(a.data, b.data, "{:?} data differs", pk);
}
(None, None) => {}
_ => panic!("{:?}: one SVM has account, other doesn't", pk),
}
}
}
#[test]
fn test_both_modes_produce_same_state_after_same_modifications() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let init_accts = vec![
(pk1, make_account(100, &[1; 8])),
(pk2, make_account(200, &[2; 8])),
(pk3, make_account(300, &[3; 8])),
];
let mut svm_stateless = LiteSVM::new();
for (pk, acct) in &init_accts {
svm_stateless.set_account(*pk, acct.clone()).unwrap();
}
let mut svm_stateful = LiteSVM::new();
for (pk, acct) in &init_accts {
svm_stateful.set_account(*pk, acct.clone()).unwrap();
}
let mod1 = make_account(999, &[0xAA; 8]);
let mod2 = make_account(888, &[0xBB; 8]);
svm_stateless.set_account(pk1, mod1.clone()).unwrap();
svm_stateless.set_account(pk2, mod2.clone()).unwrap();
svm_stateful.set_account(pk1, mod1).unwrap();
svm_stateful.set_account(pk2, mod2).unwrap();
for pk in &[pk1, pk2, pk3] {
let sl = svm_stateless.get_account(pk).unwrap();
let sf = svm_stateful.get_account(pk).unwrap();
assert_eq!(sl.lamports, sf.lamports, "{:?} lamports", pk);
assert_eq!(sl.data, sf.data, "{:?} data", pk);
}
let tracked: HashSet<Pubkey> = [pk1, pk2, pk3].into_iter().collect();
let snap = SvmSnapshot::take(
&{
let mut tmp = LiteSVM::new();
for (pk, acct) in &init_accts {
tmp.set_account(*pk, acct.clone()).unwrap();
}
tmp
},
&tracked,
);
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
dirty.mark_account_dirty(&pk2);
snap.restore(&mut svm_stateless, &dirty);
let initial_sf = SvmSnapshot::take_all(&{
let mut tmp = LiteSVM::new();
for (pk, acct) in &init_accts {
tmp.set_account(*pk, acct.clone()).unwrap();
}
tmp
});
let delta_root = SvmSnapshot::empty(svm_stateful.get_sysvar::<Clock>());
let mut dirty_sf = DirtyTracker::new();
dirty_sf.mark_account_dirty(&pk1);
dirty_sf.mark_account_dirty(&pk2);
let delta = SvmSnapshot::take_delta(&svm_stateful, &delta_root, &dirty_sf);
let divergent: FastHashSet<Pubkey> = [pk1, pk2].iter().copied().collect();
initial_sf.restore_selective(&mut svm_stateful, &divergent, &delta);
assert_eq!(svm_stateful.get_account(&pk1).unwrap().lamports, 999);
assert_eq!(svm_stateful.get_account(&pk2).unwrap().lamports, 888);
assert_eq!(svm_stateful.get_account(&pk3).unwrap().lamports, 300);
assert_eq!(svm_stateless.get_account(&pk1).unwrap().lamports, 100);
assert_eq!(svm_stateless.get_account(&pk2).unwrap().lamports, 200);
assert_eq!(svm_stateless.get_account(&pk3).unwrap().lamports, 300);
}
#[test]
fn test_stateless_3_iteration_cycle_no_leakage() {
let pk_a = Pubkey::new_unique();
let pk_b = Pubkey::new_unique();
let pk_c = Pubkey::new_unique();
let pk_d = Pubkey::new_unique();
let init = vec![
(pk_a, make_account(100, &[1; 4])),
(pk_b, make_account(200, &[2; 4])),
(pk_c, make_account(300, &[3; 4])),
(pk_d, make_account(400, &[4; 4])),
];
let mut svm = LiteSVM::new();
for (pk, acct) in &init {
svm.set_account(*pk, acct.clone()).unwrap();
}
let tracked: HashSet<Pubkey> = init.iter().map(|(pk, _)| *pk).collect();
let snap = SvmSnapshot::take(&svm, &tracked);
let mut dirty = DirtyTracker::new();
let verify_initial = |svm: &LiteSVM, label: &str| {
for (pk, acct) in &init {
let got = svm
.get_account(pk)
.unwrap_or_else(|| panic!("{}: missing {:?}", label, pk));
assert_eq!(got.lamports, acct.lamports, "{}: {:?} lamports", label, pk);
assert_eq!(got.data, acct.data, "{}: {:?} data", label, pk);
}
};
dirty.clear();
svm.set_account(pk_a, make_account(9999, &[0xFF; 4]))
.unwrap();
svm.set_account(pk_b, make_account(8888, &[0xFE; 4]))
.unwrap();
dirty.mark_account_dirty(&pk_a);
dirty.mark_account_dirty(&pk_b);
snap.restore(&mut svm, &dirty);
verify_initial(&svm, "iter1");
dirty.clear();
let pk_new = Pubkey::new_unique();
svm.set_account(pk_c, make_account(7777, &[0xFD; 4]))
.unwrap();
svm.set_account(pk_d, make_account(6666, &[0xFC; 4]))
.unwrap();
svm.set_account(pk_new, make_account(5555, &[0xFB; 4]))
.unwrap();
dirty.mark_account_dirty(&pk_c);
dirty.mark_account_dirty(&pk_d);
dirty.mark_account_dirty(&pk_new);
snap.restore(&mut svm, &dirty);
verify_initial(&svm, "iter2");
let new_acct = svm.get_account(&pk_new);
assert!(
new_acct.is_none() || new_acct.unwrap().lamports == 0,
"iter2: created account should be gone"
);
dirty.clear();
svm.set_account(pk_a, make_account(4444, &[0xFA; 4]))
.unwrap();
svm.set_account(pk_c, make_account(3333, &[0xF9; 4]))
.unwrap();
dirty.mark_account_dirty(&pk_a);
dirty.mark_account_dirty(&pk_c);
snap.restore(&mut svm, &dirty);
verify_initial(&svm, "iter3");
}
#[test]
fn test_stateful_3_iteration_cycle_state_switching() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let init_accts = vec![
(pk1, make_account(100, &[1; 8])),
(pk2, make_account(200, &[2; 8])),
(pk3, make_account(300, &[3; 8])),
];
let mut svm = LiteSVM::new();
for (pk, acct) in &init_accts {
svm.set_account(*pk, acct.clone()).unwrap();
}
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_a = {
let mut map = FastHashMap::default();
map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let delta_b = {
let mut map = FastHashMap::default();
map.insert(pk2, Arc::new(make_account(2000, &[0xB2; 8])));
map.insert(pk3, Arc::new(make_account(3000, &[0xB3; 8])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let mut divergent_keys = FastHashSet::default();
let mut prev_delta: Option<&SvmSnapshot> = None;
let prev_exec_dirty = FastHashSet::default();
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_a,
prev_delta,
&prev_exec_dirty,
);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
1000,
"iter1: pk1 should be delta_A"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
200,
"iter1: pk2 should be initial"
);
assert_eq!(
svm.get_account(&pk3).unwrap().lamports,
300,
"iter1: pk3 should be initial"
);
divergent_keys.clear();
divergent_keys.extend(delta_a.accounts.keys().copied());
prev_delta = Some(&delta_a);
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_b,
prev_delta,
&prev_exec_dirty,
);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
100,
"iter2: pk1 should be initial"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
2000,
"iter2: pk2 should be delta_B"
);
assert_eq!(
svm.get_account(&pk3).unwrap().lamports,
3000,
"iter2: pk3 should be delta_B"
);
divergent_keys.clear();
divergent_keys.extend(delta_b.accounts.keys().copied());
prev_delta = Some(&delta_b);
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_a,
prev_delta,
&prev_exec_dirty,
);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
1000,
"iter3: pk1 should be delta_A"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
200,
"iter3: pk2 should be initial"
);
assert_eq!(
svm.get_account(&pk3).unwrap().lamports,
300,
"iter3: pk3 should be initial"
);
}
#[test]
fn test_stateful_iteration_with_failed_action_clears_prev_delta() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_a = {
let mut map = FastHashMap::default();
map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let delta_b = {
let mut map = FastHashMap::default();
map.insert(pk2, Arc::new(make_account(2000, &[0xB2; 8])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let mut divergent_keys = FastHashSet::default();
let mut prev_delta_opt: Option<SvmSnapshot> = None;
let mut prev_exec_dirty = FastHashSet::default();
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_a,
prev_delta_opt.as_ref(),
&prev_exec_dirty,
);
svm.set_account(pk1, make_account(1111, &[0xEE; 8]))
.unwrap();
divergent_keys.clear();
divergent_keys.extend(delta_a.accounts.keys());
divergent_keys.insert(pk1); prev_exec_dirty.clear();
prev_exec_dirty.insert(pk1);
prev_delta_opt = Some(delta_a.clone());
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_a,
prev_delta_opt.as_ref(),
&prev_exec_dirty,
);
svm.set_account(pk2, make_account(2222, &[0xDD; 8]))
.unwrap();
divergent_keys.clear();
divergent_keys.extend(delta_a.accounts.keys());
divergent_keys.insert(pk2);
prev_exec_dirty.clear();
prev_exec_dirty.insert(pk2);
prev_delta_opt = None;
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_b,
prev_delta_opt.as_ref(),
&prev_exec_dirty,
);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
100,
"pk1 should be initial"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
2000,
"pk2 should be delta_B"
);
}
#[test]
fn test_stateful_failed_action_dirty_accounts_in_divergent_keys() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_empty = SvmSnapshot::empty(clock.clone());
let delta_target = {
let mut map = FastHashMap::default();
map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let mut divergent_keys = FastHashSet::default();
initial.restore_selective(&mut svm, &divergent_keys, &delta_empty);
svm.set_account(pk2, make_account(9999, &[0xFF; 8]))
.unwrap();
svm.set_account(pk3, make_account(8888, &[0xFE; 8]))
.unwrap();
let mut exec_dirty = FastHashSet::default();
exec_dirty.insert(pk2);
exec_dirty.insert(pk3);
divergent_keys.extend(exec_dirty.iter());
initial.restore_selective(&mut svm, &divergent_keys, &delta_target);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
1000,
"pk1 should be delta_target"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
200,
"pk2 should be restored to initial"
);
assert_eq!(
svm.get_account(&pk3).unwrap().lamports,
300,
"pk3 should be restored to initial"
);
let mut svm_bad = LiteSVM::new();
svm_bad
.set_account(pk1, make_account(100, &[1; 8]))
.unwrap();
svm_bad
.set_account(pk2, make_account(200, &[2; 8]))
.unwrap();
svm_bad
.set_account(pk3, make_account(300, &[3; 8]))
.unwrap();
initial.restore_selective(&mut svm_bad, &FastHashSet::default(), &delta_empty);
svm_bad
.set_account(pk2, make_account(9999, &[0xFF; 8]))
.unwrap();
svm_bad
.set_account(pk3, make_account(8888, &[0xFE; 8]))
.unwrap();
let no_dirty_divergent: FastHashSet<Pubkey> = FastHashSet::default();
initial.restore_selective(&mut svm_bad, &no_dirty_divergent, &delta_target);
assert_eq!(
svm_bad.get_account(&pk2).unwrap().lamports,
9999,
"pk2 stale without divergent tracking"
);
assert_eq!(
svm_bad.get_account(&pk3).unwrap().lamports,
8888,
"pk3 stale without divergent tracking"
);
}
#[test]
fn test_stateless_dirty_clear_makes_second_restore_noop() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 4])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 4])).unwrap();
let tracked: HashSet<Pubkey> = [pk1, pk2].into_iter().collect();
let snap = SvmSnapshot::take(&svm, &tracked);
svm.set_account(pk1, make_account(9999, &[0xFF; 4]))
.unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
let count1 = snap.restore(&mut svm, &dirty);
assert_eq!(count1, 1);
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 100);
dirty.clear();
let count2 = snap.restore(&mut svm, &dirty);
assert_eq!(count2, 0, "second restore with empty dirty should be noop");
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 100);
assert_eq!(svm.get_account(&pk2).unwrap().lamports, 200);
}
#[test]
fn test_stateful_dirty_not_cleared_feeds_delta_and_divergent() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let delta_root = SvmSnapshot::empty(svm.get_sysvar::<Clock>());
svm.set_account(pk1, make_account(1000, &[0xA1; 8]))
.unwrap();
svm.set_account(pk2, make_account(2000, &[0xA2; 8]))
.unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
dirty.mark_account_dirty(&pk2);
let dirty_set_a: Vec<Pubkey> = dirty.dirty_accounts().iter().copied().collect();
assert_eq!(dirty_set_a.len(), 2);
let delta = SvmSnapshot::take_delta(&svm, &delta_root, &dirty);
assert_eq!(
delta.account_count(),
2,
"delta should have exactly the dirty accounts"
);
assert!(delta.accounts().contains_key(&pk1));
assert!(delta.accounts().contains_key(&pk2));
assert!(!delta.accounts().contains_key(&pk3));
let mut divergent_keys = FastHashSet::default();
divergent_keys.extend(dirty.dirty_accounts().iter().copied());
assert!(divergent_keys.contains(&pk1));
assert!(divergent_keys.contains(&pk2));
assert!(!divergent_keys.contains(&pk3));
assert_eq!(dirty.dirty_count(), divergent_keys.len());
assert_eq!(dirty.dirty_count(), delta.account_count());
}
#[test]
fn test_dirty_tracker_used_for_restore_vs_delta_capture() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let init_accts = vec![
(pk1, make_account(100, &[1; 8])),
(pk2, make_account(200, &[2; 8])),
(pk3, make_account(300, &[3; 8])),
];
let mut svm_sl = LiteSVM::new();
for (pk, acct) in &init_accts {
svm_sl.set_account(*pk, acct.clone()).unwrap();
}
let tracked: HashSet<Pubkey> = init_accts.iter().map(|(pk, _)| *pk).collect();
let snap_sl = SvmSnapshot::take(&svm_sl, &tracked);
let mut svm_sf = LiteSVM::new();
for (pk, acct) in &init_accts {
svm_sf.set_account(*pk, acct.clone()).unwrap();
}
let delta_root = SvmSnapshot::empty(svm_sf.get_sysvar::<Clock>());
let mod1 = make_account(999, &[0xAA; 8]);
let mod2 = make_account(888, &[0xBB; 8]);
svm_sl.set_account(pk1, mod1.clone()).unwrap();
svm_sl.set_account(pk2, mod2.clone()).unwrap();
svm_sf.set_account(pk1, mod1).unwrap();
svm_sf.set_account(pk2, mod2).unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
dirty.mark_account_dirty(&pk2);
snap_sl.restore(&mut svm_sl, &dirty);
assert_eq!(
svm_sl.get_account(&pk1).unwrap().lamports,
100,
"stateless: pk1 restored"
);
assert_eq!(
svm_sl.get_account(&pk2).unwrap().lamports,
200,
"stateless: pk2 restored"
);
assert_eq!(
svm_sl.get_account(&pk3).unwrap().lamports,
300,
"stateless: pk3 unchanged"
);
let delta = SvmSnapshot::take_delta(&svm_sf, &delta_root, &dirty);
assert_eq!(
delta.accounts().get(&pk1).unwrap().lamports,
999,
"stateful: delta captured pk1"
);
assert_eq!(
delta.accounts().get(&pk2).unwrap().lamports,
888,
"stateful: delta captured pk2"
);
assert!(
!delta.accounts().contains_key(&pk3),
"stateful: pk3 not in delta"
);
}
#[test]
fn test_take_delta_only_includes_accounts_in_dirty_set() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let pk4 = Pubkey::new_unique();
let pk5 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
for (pk, lamports) in [(pk1, 100), (pk2, 200), (pk3, 300), (pk4, 400), (pk5, 500)] {
svm.set_account(pk, make_account(lamports, &[lamports as u8]))
.unwrap();
}
let delta_root = SvmSnapshot::empty(svm.get_sysvar::<Clock>());
svm.set_account(pk1, make_account(1000, &[0xA1])).unwrap();
svm.set_account(pk3, make_account(3000, &[0xA3])).unwrap();
svm.set_account(pk5, make_account(5000, &[0xA5])).unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
dirty.mark_account_dirty(&pk3);
dirty.mark_account_dirty(&pk5);
let delta = SvmSnapshot::take_delta(&svm, &delta_root, &dirty);
assert_eq!(delta.account_count(), 3);
assert!(delta.accounts().contains_key(&pk1));
assert!(
!delta.accounts().contains_key(&pk2),
"pk2 should not be in delta"
);
assert!(delta.accounts().contains_key(&pk3));
assert!(
!delta.accounts().contains_key(&pk4),
"pk4 should not be in delta"
);
assert!(delta.accounts().contains_key(&pk5));
assert_eq!(delta.accounts().get(&pk1).unwrap().lamports, 1000);
assert_eq!(delta.accounts().get(&pk3).unwrap().lamports, 3000);
assert_eq!(delta.accounts().get(&pk5).unwrap().lamports, 5000);
}
#[test]
fn test_take_all_superset_of_take_tracked() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1])).unwrap();
svm.set_account(pk2, make_account(200, &[2])).unwrap();
svm.set_account(pk3, make_account(300, &[3])).unwrap();
let tracked: HashSet<Pubkey> = [pk1, pk2].into_iter().collect();
let snap_tracked = SvmSnapshot::take(&svm, &tracked);
let snap_all = SvmSnapshot::take_all(&svm);
for pk in snap_tracked.accounts().keys() {
assert!(
snap_all.accounts().contains_key(pk),
"take_all should contain all tracked accounts"
);
let tracked_acct = snap_tracked.accounts().get(pk).unwrap();
let all_acct = snap_all.accounts().get(pk).unwrap();
assert_eq!(tracked_acct.lamports, all_acct.lamports);
assert_eq!(tracked_acct.data, all_acct.data);
}
assert!(snap_all.accounts().contains_key(&pk3));
assert!(!snap_tracked.accounts().contains_key(&pk3));
assert!(snap_all.account_count() >= snap_tracked.account_count());
}
#[test]
fn test_restore_full_vs_restore_selective_with_all_accounts_as_delta() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let init_accts = vec![
(pk1, make_account(100, &[1; 8])),
(pk2, make_account(200, &[2; 8])),
(pk3, make_account(300, &[3; 8])),
];
let mut delta_map = FastHashMap::default();
delta_map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
delta_map.insert(pk2, Arc::new(make_account(2000, &[0xA2; 8])));
delta_map.insert(pk3, Arc::new(make_account(3000, &[0xA3; 8])));
let full_delta = SvmSnapshot {
accounts: delta_map,
sysvars: make_test_sysvars(42),
};
let mut svm1 = LiteSVM::new();
for (pk, acct) in &init_accts {
svm1.set_account(*pk, acct.clone()).unwrap();
}
full_delta.restore_full(&mut svm1);
let mut svm2 = LiteSVM::new();
for (pk, acct) in &init_accts {
svm2.set_account(*pk, acct.clone()).unwrap();
}
let initial2 = SvmSnapshot::take_all(&svm2);
let all_divergent: FastHashSet<Pubkey> = [pk1, pk2, pk3].into_iter().collect();
initial2.restore_selective(&mut svm2, &all_divergent, &full_delta);
for pk in &[pk1, pk2, pk3] {
let a1 = svm1.get_account(pk).unwrap();
let a2 = svm2.get_account(pk).unwrap();
assert_eq!(a1.lamports, a2.lamports, "{:?} lamports differ", pk);
assert_eq!(a1.data, a2.data, "{:?} data differs", pk);
}
}
#[test]
fn test_dual_svm_traced_and_fast_same_state_after_restore() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let init_accts = vec![
(pk1, make_account(100, &[1; 16])),
(pk2, make_account(200, &[2; 16])),
(pk3, make_account(300, &[3; 16])),
];
let mut svm_traced = LiteSVM::new();
let mut svm_fast = LiteSVM::new();
for (pk, acct) in &init_accts {
svm_traced.set_account(*pk, acct.clone()).unwrap();
svm_fast.set_account(*pk, acct.clone()).unwrap();
}
let initial_traced = SvmSnapshot::take_all(&svm_traced);
let initial_fast = SvmSnapshot::take_all(&svm_fast);
let clock = svm_traced.get_sysvar::<Clock>();
let delta_prev = {
let mut map = FastHashMap::default();
map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 16])));
map.insert(pk2, Arc::new(make_account(2000, &[0xA2; 16])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let delta_next = {
let mut map = FastHashMap::default();
map.insert(pk1, delta_prev.accounts.get(&pk1).unwrap().clone());
map.insert(pk3, Arc::new(make_account(3000, &[0xB3; 16])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
let mut traced_divergent: FastHashSet<Pubkey> = FastHashSet::default();
initial_traced.restore_selective(&mut svm_traced, &traced_divergent, &delta_prev);
traced_divergent.extend(delta_prev.accounts.keys());
let mut fast_divergent: FastHashSet<Pubkey> = FastHashSet::default();
initial_fast.restore_selective(&mut svm_fast, &fast_divergent, &delta_prev);
fast_divergent.extend(delta_prev.accounts.keys());
initial_traced.restore_selective(&mut svm_traced, &traced_divergent, &delta_next);
let empty_exec_dirty = FastHashSet::default();
initial_fast.restore_selective_from(
&mut svm_fast,
&fast_divergent,
&delta_prev,
&delta_next,
&empty_exec_dirty,
);
for pk in &[pk1, pk2, pk3] {
let traced = svm_traced.get_account(pk);
let fast = svm_fast.get_account(pk);
match (traced, fast) {
(Some(t), Some(f)) => {
assert_eq!(t.lamports, f.lamports, "{:?} lamports", pk);
assert_eq!(t.data, f.data, "{:?} data", pk);
}
(None, None) => {}
_ => panic!(
"{:?}: traced has={}, fast has={}",
pk,
svm_traced.get_account(pk).is_some(),
svm_fast.get_account(pk).is_some()
),
}
}
}
#[test]
fn test_dual_svm_separate_divergent_keys() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let pk4 = Pubkey::new_unique();
let init_accts = vec![
(pk1, make_account(100, &[1; 8])),
(pk2, make_account(200, &[2; 8])),
(pk3, make_account(300, &[3; 8])),
(pk4, make_account(400, &[4; 8])),
];
let mut svm_traced = LiteSVM::new();
let mut svm_fast = LiteSVM::new();
for (pk, acct) in &init_accts {
svm_traced.set_account(*pk, acct.clone()).unwrap();
svm_fast.set_account(*pk, acct.clone()).unwrap();
}
let initial_traced = SvmSnapshot::take_all(&svm_traced);
let initial_fast = SvmSnapshot::take_all(&svm_fast);
let clock = svm_traced.get_sysvar::<Clock>();
let target_delta = {
let mut map = FastHashMap::default();
map.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
SvmSnapshot {
accounts: map,
sysvars: clock_to_sysvars(&clock),
}
};
svm_traced
.set_account(pk2, make_account(9999, &[0xFF; 8]))
.unwrap();
svm_traced
.set_account(pk3, make_account(8888, &[0xFE; 8]))
.unwrap();
let traced_divergent: FastHashSet<Pubkey> = [pk2, pk3].into_iter().collect();
svm_fast
.set_account(pk3, make_account(7777, &[0xFD; 8]))
.unwrap();
svm_fast
.set_account(pk4, make_account(6666, &[0xFC; 8]))
.unwrap();
let fast_divergent: FastHashSet<Pubkey> = [pk3, pk4].into_iter().collect();
initial_traced.restore_selective(&mut svm_traced, &traced_divergent, &target_delta);
initial_fast.restore_selective(&mut svm_fast, &fast_divergent, &target_delta);
for pk in &[pk1, pk2, pk3, pk4] {
let t = svm_traced.get_account(pk).unwrap();
let f = svm_fast.get_account(pk).unwrap();
assert_eq!(t.lamports, f.lamports, "{:?} lamports", pk);
assert_eq!(t.data, f.data, "{:?} data", pk);
}
assert_eq!(
svm_traced.get_account(&pk1).unwrap().lamports,
1000,
"pk1 from delta"
);
assert_eq!(
svm_traced.get_account(&pk2).unwrap().lamports,
200,
"pk2 restored to initial"
);
assert_eq!(
svm_traced.get_account(&pk3).unwrap().lamports,
300,
"pk3 restored to initial"
);
assert_eq!(
svm_traced.get_account(&pk4).unwrap().lamports,
400,
"pk4 initial (fast restored)"
);
}
#[test]
fn test_svm_swap_roundtrip_preserves_accounts() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
let mut holder = LiteSVM::new();
std::mem::swap(&mut svm, &mut holder);
assert_eq!(holder.get_account(&pk1).unwrap().lamports, 100);
assert_eq!(holder.get_account(&pk2).unwrap().lamports, 200);
assert!(
svm.get_account(&pk1).is_none(),
"swapped-out SVM should not have pk1"
);
assert!(
svm.get_account(&pk2).is_none(),
"swapped-out SVM should not have pk2"
);
std::mem::swap(&mut svm, &mut holder);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
100,
"pk1 restored after swap back"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
200,
"pk2 restored after swap back"
);
}
#[test]
fn test_empty_svm_after_swap_means_cheap_clone() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 32])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 32])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 32])).unwrap();
let mut empty = LiteSVM::new();
std::mem::swap(&mut svm, &mut empty);
assert!(svm.get_account(&pk1).is_none());
assert!(svm.get_account(&pk2).is_none());
assert!(svm.get_account(&pk3).is_none());
let db = svm.accounts_db();
assert!(!db.inner.contains_key(&pk1));
assert!(!db.inner.contains_key(&pk2));
assert!(!db.inner.contains_key(&pk3));
}
#[test]
fn test_stateless_crash_dedup_by_input_bytes() {
use std::collections::hash_map::DefaultHasher;
let hash_bytes = |bytes: &[u8]| -> u64 {
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
hasher.finish()
};
let input_a = vec![0x01, 0x02, 0x03, 0x04];
let input_b = vec![0x01, 0x02, 0x03, 0x05]; let input_c = vec![0x01, 0x02, 0x03, 0x04];
let hash_a = hash_bytes(&input_a);
let hash_b = hash_bytes(&input_b);
let hash_c = hash_bytes(&input_c);
assert_ne!(
hash_a, hash_b,
"different inputs should produce different hashes"
);
assert_eq!(hash_a, hash_c, "same inputs should produce same hash");
let input_short = vec![0x01, 0x02];
let input_long = vec![0x01, 0x02, 0x00, 0x00, 0x00];
assert_ne!(
hash_bytes(&input_short),
hash_bytes(&input_long),
"different lengths should produce different hashes"
);
}
#[test]
fn test_stateful_crash_dedup_by_variant_sequence() {
use std::collections::hash_map::DefaultHasher;
let hash_variant_seq = |variants: &[u16]| -> u64 {
let bytes: Vec<u8> = variants.iter().flat_map(|v| v.to_le_bytes()).collect();
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
hasher.finish()
};
let seq_a = vec![0u16, 1, 2];
let seq_b = vec![0u16, 1, 2];
let seq_c = vec![1u16, 0, 2];
let seq_d = vec![0u16, 1];
assert_eq!(
hash_variant_seq(&seq_a),
hash_variant_seq(&seq_b),
"same variant order = same dedup hash"
);
assert_ne!(
hash_variant_seq(&seq_a),
hash_variant_seq(&seq_c),
"different order = different dedup hash"
);
assert_ne!(
hash_variant_seq(&seq_a),
hash_variant_seq(&seq_d),
"different length = different dedup hash"
);
let mut pool = StatePool::new(100, 10);
add_test_state(&mut pool, 1, 0, None, "root", None);
add_test_state(&mut pool, 2, 1, Some(0), "deposit", Some(0));
add_test_state(&mut pool, 3, 2, Some(1), "borrow", Some(1));
add_test_state(&mut pool, 4, 3, Some(2), "withdraw", Some(2));
let variants = pool.reconstruct_variant_sequence(3);
assert_eq!(
variants,
vec![0, 1, 2],
"variant sequence should be oldest-first"
);
add_test_state(&mut pool, 10, 1, Some(0), "borrow", Some(1));
add_test_state(&mut pool, 11, 2, Some(4), "deposit", Some(0));
add_test_state(&mut pool, 12, 3, Some(5), "withdraw", Some(2));
let variants2 = pool.reconstruct_variant_sequence(6);
assert_eq!(
variants2,
vec![1, 0, 2],
"reversed chain should give different sequence"
);
assert_ne!(
hash_variant_seq(&variants),
hash_variant_seq(&variants2),
"different chains should produce different dedup hashes"
);
}
#[test]
fn test_arc_pointer_equality_skips_redundant_set_account() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 16])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 16])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 16])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let shared_pk1_arc = Arc::new(make_account(1000, &[0xA1; 16]));
let shared_pk2_arc = Arc::new(make_account(2000, &[0xA2; 16]));
let delta_parent = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, shared_pk1_arc.clone()); m.insert(pk2, shared_pk2_arc.clone()); m
},
sysvars: make_test_sysvars(10),
};
let delta_child = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, shared_pk1_arc.clone()); m.insert(pk2, Arc::new(make_account(2500, &[0xB2; 16]))); m.insert(pk3, Arc::new(make_account(3000, &[0xB3; 16]))); m
},
sysvars: make_test_sysvars(20),
};
let divergent: FastHashSet<Pubkey> = FastHashSet::default();
initial.restore_selective(&mut svm, &divergent, &delta_parent);
let divergent_after_parent: FastHashSet<Pubkey> =
delta_parent.accounts.keys().copied().collect();
let empty_exec_dirty = FastHashSet::default();
let count_from = initial.restore_selective_from(
&mut svm,
&divergent_after_parent,
&delta_parent,
&delta_child,
&empty_exec_dirty,
);
let mut svm2 = LiteSVM::new();
svm2.set_account(pk1, make_account(100, &[1; 16])).unwrap();
svm2.set_account(pk2, make_account(200, &[2; 16])).unwrap();
svm2.set_account(pk3, make_account(300, &[3; 16])).unwrap();
let initial2 = SvmSnapshot::take_all(&svm2);
initial2.restore_selective(&mut svm2, &FastHashSet::default(), &delta_parent);
let divergent2: FastHashSet<Pubkey> = delta_parent.accounts.keys().copied().collect();
let count_selective = initial2.restore_selective(&mut svm2, &divergent2, &delta_child);
for pk in &[pk1, pk2, pk3] {
let a = svm.get_account(pk).unwrap();
let b = svm2.get_account(pk).unwrap();
assert_eq!(a.lamports, b.lamports, "{:?} lamports", pk);
assert_eq!(a.data, b.data, "{:?} data", pk);
}
assert!(
count_from < count_selective,
"restore_selective_from ({}) should do fewer writes than restore_selective ({})",
count_from,
count_selective
);
}
#[test]
fn test_arc_skip_overridden_by_exec_dirty() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let shared_arc = Arc::new(make_account(1000, &[0xA1; 8]));
let delta = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, shared_arc.clone());
m
},
sysvars: make_test_sysvars(10),
};
initial.restore_selective(&mut svm, &FastHashSet::default(), &delta);
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 1000);
svm.set_account(pk1, make_account(9999, &[0xFF; 8]))
.unwrap();
let _divergent: FastHashSet<Pubkey> = [pk1].into_iter().collect();
let mut svm_no_dirty = LiteSVM::new();
svm_no_dirty
.set_account(pk1, make_account(100, &[1; 8]))
.unwrap();
svm_no_dirty
.set_account(pk2, make_account(200, &[2; 8]))
.unwrap();
let initial_nd = SvmSnapshot::take_all(&svm_no_dirty);
initial_nd.restore_selective(&mut svm_no_dirty, &FastHashSet::default(), &delta);
svm_no_dirty
.set_account(pk1, make_account(9999, &[0xFF; 8]))
.unwrap();
let divergent_nd: FastHashSet<Pubkey> = [pk1].into_iter().collect();
initial_nd.restore_selective_from(
&mut svm_no_dirty,
&divergent_nd,
&delta,
&delta,
&FastHashSet::default(), );
assert_eq!(
svm_no_dirty.get_account(&pk1).unwrap().lamports,
9999,
"without exec_dirty, Arc-equal account is skipped (stale)"
);
let exec_dirty: FastHashSet<Pubkey> = [pk1].into_iter().collect();
let divergent2: FastHashSet<Pubkey> = [pk1].into_iter().collect();
initial.restore_selective_from(&mut svm, &divergent2, &delta, &delta, &exec_dirty);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
1000,
"with exec_dirty, Arc-equal account is forced to correct value"
);
}
#[test]
fn test_sibling_deltas_share_parent_arcs() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let parent_pk1_arc = Arc::new(make_account(1000, &[0xA1; 16]));
let sibling_a = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, parent_pk1_arc.clone());
m.insert(pk2, Arc::new(make_account(2000, &[0xAA; 16])));
m
},
sysvars: make_test_sysvars(10),
};
let sibling_b = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, parent_pk1_arc.clone());
m.insert(pk3, Arc::new(make_account(3000, &[0xBB; 16])));
m
},
sysvars: make_test_sysvars(20),
};
assert!(Arc::ptr_eq(
sibling_a.accounts.get(&pk1).unwrap(),
sibling_b.accounts.get(&pk1).unwrap()
));
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 16])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 16])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 16])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
initial.restore_selective(&mut svm, &FastHashSet::default(), &sibling_a);
let divergent_a: FastHashSet<Pubkey> = sibling_a.accounts.keys().copied().collect();
let count = initial.restore_selective_from(
&mut svm,
&divergent_a,
&sibling_a,
&sibling_b,
&FastHashSet::default(),
);
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 1000);
assert_eq!(svm.get_account(&pk2).unwrap().lamports, 200);
assert_eq!(svm.get_account(&pk3).unwrap().lamports, 3000);
assert_eq!(count, 2, "pk1 should be skipped due to shared Arc");
}
#[test]
fn test_stateless_divergent_cleared_every_iteration() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 4])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 4])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 4])).unwrap();
let tracked: HashSet<Pubkey> = [pk1, pk2, pk3].into_iter().collect();
let snap = SvmSnapshot::take(&svm, &tracked);
let mut dirty = DirtyTracker::new();
dirty.clear();
svm.set_account(pk1, make_account(999, &[0xFF; 4])).unwrap();
dirty.mark_account_dirty(&pk1);
assert_eq!(dirty.dirty_count(), 1);
snap.restore(&mut svm, &dirty);
dirty.clear();
assert_eq!(
dirty.dirty_count(),
0,
"dirty tracker cleared after iteration"
);
svm.set_account(pk2, make_account(888, &[0xFE; 4])).unwrap();
dirty.mark_account_dirty(&pk2);
assert_eq!(
dirty.dirty_count(),
1,
"only pk2 dirty, no carryover from iter1"
);
let count = snap.restore(&mut svm, &dirty);
assert_eq!(count, 1, "only one account restored");
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 100);
assert_eq!(svm.get_account(&pk2).unwrap().lamports, 200);
}
#[test]
fn test_stateful_divergent_accumulates_across_iterations() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_a = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
m
},
sysvars: clock_to_sysvars(&clock),
};
let delta_b = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk2, Arc::new(make_account(2000, &[0xB2; 8])));
m
},
sysvars: clock_to_sysvars(&clock),
};
let mut divergent_keys = FastHashSet::default();
initial.restore_selective(&mut svm, &divergent_keys, &delta_a);
divergent_keys.clear();
divergent_keys.extend(delta_a.accounts.keys());
svm.set_account(pk3, make_account(9999, &[0xFF; 8]))
.unwrap();
divergent_keys.insert(pk3);
assert_eq!(
divergent_keys.len(),
2,
"divergent = delta keys + exec dirty"
);
initial.restore_selective(&mut svm, &divergent_keys, &delta_b);
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 100);
assert_eq!(svm.get_account(&pk2).unwrap().lamports, 2000);
assert_eq!(svm.get_account(&pk3).unwrap().lamports, 300);
}
#[test]
fn test_divergent_keys_cleared_then_rebuilt_each_iteration() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let pk4 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
for (pk, l) in [(pk1, 100), (pk2, 200), (pk3, 300), (pk4, 400)] {
svm.set_account(pk, make_account(l, &[l as u8; 8])).unwrap();
}
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_1 = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
m.insert(pk2, Arc::new(make_account(2000, &[0xA2; 8])));
m
},
sysvars: clock_to_sysvars(&clock),
};
let delta_2 = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk3, Arc::new(make_account(3000, &[0xB3; 8])));
m
},
sysvars: clock_to_sysvars(&clock),
};
let mut divergent_keys = FastHashSet::default();
let mut prev_delta: Option<SvmSnapshot> = None;
let mut prev_exec_dirty = FastHashSet::default();
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_1,
prev_delta.as_ref(),
&prev_exec_dirty,
);
divergent_keys.clear();
divergent_keys.extend(delta_1.accounts.keys());
svm.set_account(pk4, make_account(9999, &[0xFF; 8]))
.unwrap();
prev_exec_dirty.clear();
prev_exec_dirty.insert(pk4);
divergent_keys.extend(prev_exec_dirty.iter()); prev_delta = Some(delta_1.clone());
assert_eq!(divergent_keys.len(), 3);
assert!(divergent_keys.contains(&pk1));
assert!(divergent_keys.contains(&pk2));
assert!(divergent_keys.contains(&pk4));
simulate_restore(
&initial,
&mut svm,
&divergent_keys,
&delta_2,
prev_delta.as_ref(),
&prev_exec_dirty,
);
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 100);
assert_eq!(svm.get_account(&pk2).unwrap().lamports, 200);
assert_eq!(svm.get_account(&pk3).unwrap().lamports, 3000);
assert_eq!(svm.get_account(&pk4).unwrap().lamports, 400);
}
#[test]
fn test_take_full_captures_all_accounts() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let tracked: HashSet<Pubkey> = [pk1, pk2, pk3].into_iter().collect();
let base = SvmSnapshot::take(&svm, &tracked);
svm.set_account(pk1, make_account(1000, &[0xA1; 8]))
.unwrap();
svm.set_account(pk2, make_account(2000, &[0xA2; 8]))
.unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
dirty.mark_account_dirty(&pk2);
let full = SvmSnapshot::take_full(&svm, &base, &dirty);
assert_eq!(full.account_count(), 3);
assert_eq!(full.accounts().get(&pk1).unwrap().lamports, 1000);
assert_eq!(full.accounts().get(&pk2).unwrap().lamports, 2000);
assert_eq!(full.accounts().get(&pk3).unwrap().lamports, 300); }
#[test]
fn test_take_delta_only_captures_dirty() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let tracked: HashSet<Pubkey> = [pk1, pk2, pk3].into_iter().collect();
let base = SvmSnapshot::take(&svm, &tracked);
let delta_root = SvmSnapshot::empty(svm.get_sysvar::<Clock>());
svm.set_account(pk1, make_account(1000, &[0xA1; 8]))
.unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
let full = SvmSnapshot::take_full(&svm, &base, &dirty);
let delta = SvmSnapshot::take_delta(&svm, &delta_root, &dirty);
assert_eq!(full.account_count(), 3);
assert!(full.accounts().contains_key(&pk2));
assert!(full.accounts().contains_key(&pk3));
assert_eq!(delta.account_count(), 1);
assert!(delta.accounts().contains_key(&pk1));
assert!(!delta.accounts().contains_key(&pk2));
assert!(!delta.accounts().contains_key(&pk3));
assert_eq!(
full.accounts().get(&pk1).unwrap().lamports,
delta.accounts().get(&pk1).unwrap().lamports
);
}
#[test]
fn test_take_full_then_restore_full_roundtrip() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
let tracked: HashSet<Pubkey> = [pk1, pk2].into_iter().collect();
let base = SvmSnapshot::take(&svm, &tracked);
svm.set_account(pk1, make_account(1000, &[0xA1; 8]))
.unwrap();
let mut dirty = DirtyTracker::new();
dirty.mark_account_dirty(&pk1);
let full_snap = SvmSnapshot::take_full(&svm, &base, &dirty);
svm.set_account(pk1, make_account(1, &[0])).unwrap();
svm.set_account(pk2, make_account(1, &[0])).unwrap();
full_snap.restore_full(&mut svm);
assert_eq!(svm.get_account(&pk1).unwrap().lamports, 1000);
assert_eq!(svm.get_account(&pk2).unwrap().lamports, 200);
}
#[test]
fn test_delta_chain_inherits_parent_arcs() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
let delta_root = SvmSnapshot::empty(svm.get_sysvar::<Clock>());
svm.set_account(pk1, make_account(1000, &[0xA1; 8]))
.unwrap();
let mut dirty_a = DirtyTracker::new();
dirty_a.mark_account_dirty(&pk1);
let delta_a = SvmSnapshot::take_delta(&svm, &delta_root, &dirty_a);
svm.set_account(pk2, make_account(2000, &[0xB2; 8]))
.unwrap();
let mut dirty_b = DirtyTracker::new();
dirty_b.mark_account_dirty(&pk2);
let delta_b = SvmSnapshot::take_delta(&svm, &delta_a, &dirty_b);
assert_eq!(delta_b.account_count(), 2);
assert!(
Arc::ptr_eq(
delta_a.accounts.get(&pk1).unwrap(),
delta_b.accounts.get(&pk1).unwrap()
),
"inherited account should share Arc pointer"
);
assert!(!Arc::ptr_eq(
&Arc::new(make_account(200, &[2; 8])), delta_b.accounts.get(&pk2).unwrap()
));
assert_eq!(delta_b.accounts.get(&pk2).unwrap().lamports, 2000);
}
#[test]
fn test_delta_chain_3_levels_correct_values() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let delta_root = SvmSnapshot::empty(svm.get_sysvar::<Clock>());
svm.set_account(pk1, make_account(1000, &[0xA1; 8]))
.unwrap();
let mut dirty1 = DirtyTracker::new();
dirty1.mark_account_dirty(&pk1);
let delta_1 = SvmSnapshot::take_delta(&svm, &delta_root, &dirty1);
svm.set_account(pk2, make_account(2000, &[0xB2; 8]))
.unwrap();
let mut dirty2 = DirtyTracker::new();
dirty2.mark_account_dirty(&pk2);
let delta_2 = SvmSnapshot::take_delta(&svm, &delta_1, &dirty2);
svm.set_account(pk3, make_account(3000, &[0xC3; 8]))
.unwrap();
let mut dirty3 = DirtyTracker::new();
dirty3.mark_account_dirty(&pk3);
let delta_3 = SvmSnapshot::take_delta(&svm, &delta_2, &dirty3);
assert_eq!(delta_1.account_count(), 1);
assert_eq!(delta_2.account_count(), 2);
assert_eq!(delta_3.account_count(), 3);
for (label, delta, expected) in [
("level1", &delta_1, [1000u64, 200, 300]),
("level2", &delta_2, [1000, 2000, 300]),
("level3", &delta_3, [1000, 2000, 3000]),
] {
let mut svm_test = LiteSVM::new();
svm_test
.set_account(pk1, make_account(100, &[1; 8]))
.unwrap();
svm_test
.set_account(pk2, make_account(200, &[2; 8]))
.unwrap();
svm_test
.set_account(pk3, make_account(300, &[3; 8]))
.unwrap();
let init_test = SvmSnapshot::take_all(&svm_test);
let all_divergent: FastHashSet<Pubkey> = [pk1, pk2, pk3].into_iter().collect();
init_test.restore_selective(&mut svm_test, &all_divergent, delta);
for (pk, exp) in [pk1, pk2, pk3].iter().zip(expected.iter()) {
assert_eq!(
svm_test.get_account(pk).unwrap().lamports,
*exp,
"{}: {:?} expected {}",
label,
pk,
exp
);
}
}
}
#[test]
fn test_action_stats_laplace_smoothing_never_zero() {
let mut stats = ActionStats::new(5);
for _ in 0..100 {
stats.record(0, false);
}
for _ in 0..100 {
stats.record(1, true);
}
for _ in 0..50 {
stats.record(3, true);
stats.record(3, false);
}
let weights = stats.weights();
for (i, &w) in weights.iter().enumerate() {
assert!(
w > 0.0,
"variant {} weight should be positive, got {}",
i,
w
);
}
assert!(
weights[2] > weights[0],
"untried variant should have higher weight than always-failing"
);
assert!(
weights[2] > weights[1],
"untried variant should have higher weight than always-succeeding (exploration bonus)"
);
}
#[test]
fn test_action_stats_map_per_state_class() {
let mut stats_map = ActionStatsMap::new(3);
let class_a: u16 = 42;
let class_b: u16 = 99;
for _ in 0..10 {
stats_map.record(class_a, 0, true);
}
for _ in 0..10 {
stats_map.record(class_b, 0, false);
}
let pick_a = stats_map.pick_variant(class_a, u64::MAX / 2, 50); let pick_b = stats_map.pick_variant(class_b, u64::MAX / 2, 50);
assert!(
pick_a.is_some() || pick_b.is_some(),
"at least one pick should be non-epsilon"
);
}
#[test]
fn test_state_class_from_fingerprint() {
let fp_a = 0x1234_0000_0000_0000u64;
let fp_b = 0x1234_FFFF_FFFF_FFFFu64;
let fp_c = 0x5678_0000_0000_0000u64;
assert_eq!(
state_class_from_fingerprint(fp_a),
state_class_from_fingerprint(fp_b),
"same top 16 bits → same class"
);
assert_ne!(
state_class_from_fingerprint(fp_a),
state_class_from_fingerprint(fp_c),
"different top 16 bits → different class"
);
assert_eq!(state_class_from_fingerprint(fp_a), 0x1234);
assert_eq!(state_class_from_fingerprint(fp_c), 0x5678);
}
#[test]
fn test_pool_pick_weighted_favors_underexplored() {
let mut pool = StatePool::new(100, 10);
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
add_pool_entry(&mut pool, 1, make_pool_snapshot(vec![(pk1, 1000)]), 1, None);
add_pool_entry(&mut pool, 2, make_pool_snapshot(vec![(pk2, 2000)]), 1, None);
add_pool_entry(&mut pool, 3, make_pool_snapshot(vec![(pk3, 3000)]), 1, None);
for _ in 0..50 {
pool.pick_weighted(0); }
let mut counts = [0u32; 3];
for i in 0..3000u64 {
if let Some(idx) = pool.pick_weighted(i.wrapping_mul(6364136223846793005).wrapping_add(1)) {
if idx < 3 {
counts[idx] += 1;
}
}
}
assert!(
counts[0] < counts[1] + counts[2],
"heavily picked state ({}) should be less than others ({} + {})",
counts[0],
counts[1],
counts[2]
);
}
#[test]
fn test_pool_coverage_novel_3x_boost() {
let mut pool = StatePool::new(100, 10);
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
add_pool_entry(&mut pool, 1, make_pool_snapshot(vec![(pk1, 1000)]), 1, None);
pool.try_add(
2,
snapshot_to_compact_delta(make_pool_snapshot(vec![(pk2, 2000)])),
1,
None,
vec![0u8; 8],
"coverage_action".to_string(),
Some(0),
vec![],
None,
10,
10,
true,
None,
);
let mut counts = [0u32; 2];
for i in 0..3000u64 {
if let Some(idx) = pool.pick_weighted(i.wrapping_mul(2862933555777941757).wrapping_add(1)) {
if idx < 2 {
counts[idx] += 1;
}
}
}
assert!(
counts[1] > counts[0],
"novelty_bits state ({}) should be picked more than non-novel ({})",
counts[1],
counts[0]
);
}
#[test]
fn test_pool_violation_penalty_reduces_weight() {
let mut pool = StatePool::new(100, 10);
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
add_pool_entry(&mut pool, 1, make_pool_snapshot(vec![(pk1, 1000)]), 1, None);
add_pool_entry(&mut pool, 2, make_pool_snapshot(vec![(pk2, 2000)]), 1, None);
for _ in 0..20 {
pool.record_violation(0);
}
let mut counts = [0u32; 2];
for i in 0..3000u64 {
if let Some(idx) = pool.pick_weighted(i.wrapping_mul(6364136223846793005).wrapping_add(1)) {
if idx < 2 {
counts[idx] += 1;
}
}
}
assert!(
counts[0] < counts[1],
"high-violation state ({}) should be picked less than clean state ({})",
counts[0],
counts[1]
);
}
#[test]
fn test_fingerprint_truncation_causes_collisions() {
let mut pool = StatePool::new(100, 10);
let fp1 = 0xAAAA_BBBB_0000_1234u64;
let fp2 = 0xCCCC_DDDD_0000_1234u64;
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let added1 = add_pool_entry(
&mut pool,
fp1,
make_pool_snapshot(vec![(pk1, 1000)]),
1,
None,
);
let added2 = add_pool_entry(
&mut pool,
fp2,
make_pool_snapshot(vec![(pk2, 2000)]),
1,
None,
);
assert!(added1, "first fingerprint should be added");
assert!(
!added2,
"second fingerprint should collide (same lower 17 bits)"
);
assert_eq!(pool.len(), 1, "only one state in pool due to collision");
}
#[test]
fn test_fingerprint_zero_never_saved() {
let mut pool = StatePool::new(100, 10);
let pk1 = Pubkey::new_unique();
let added = add_pool_entry(&mut pool, 0, make_pool_snapshot(vec![(pk1, 1000)]), 0, None);
assert!(added, "fingerprint 0 CAN be added if not previously seen");
let added2 = add_pool_entry(&mut pool, 0, make_pool_snapshot(vec![(pk1, 2000)]), 0, None);
assert!(!added2, "duplicate fingerprint 0 should be rejected");
}
#[test]
fn test_pool_depth_limit_rejects_deep_states() {
let mut pool = StatePool::new(100, 5);
let pk1 = Pubkey::new_unique();
let added_5 = add_pool_entry(&mut pool, 1, make_pool_snapshot(vec![(pk1, 100)]), 5, None);
assert!(added_5, "depth=5 should be accepted (max_depth=5)");
let added_6 = add_pool_entry(&mut pool, 2, make_pool_snapshot(vec![(pk1, 200)]), 6, None);
assert!(!added_6, "depth=6 should be rejected (max_depth=5)");
}
#[test]
fn test_pool_capacity_limit_evicts_when_full() {
let mut pool = StatePool::new(3, 10);
let added1 = add_pool_entry(&mut pool, 1, make_pool_snapshot(vec![]), 0, None);
let added2 = add_pool_entry(&mut pool, 2, make_pool_snapshot(vec![]), 1, None);
let added3 = add_pool_entry(&mut pool, 3, make_pool_snapshot(vec![]), 2, None);
let added4 = add_pool_entry(&mut pool, 4, make_pool_snapshot(vec![]), 3, None);
assert!(added1 && added2 && added3, "first 3 should be added");
assert!(added4, "4th should evict weakest and succeed");
assert_eq!(pool.active_count(), 3); }
#[test]
fn test_crashed_state_removed_from_active_but_kept_for_reconstruction() {
let mut pool = StatePool::new(100, 10);
add_test_state(&mut pool, 1, 0, None, "root", None);
add_test_state(&mut pool, 2, 1, Some(0), "deposit", Some(0));
add_test_state(&mut pool, 3, 2, Some(1), "borrow", Some(1));
assert_eq!(pool.active_count(), 3);
assert_eq!(pool.len(), 3);
pool.mark_crashed(1);
assert_eq!(pool.active_count(), 2, "one state removed from active");
assert_eq!(pool.len(), 3, "all states still in pool for reconstruction");
let variants = pool.reconstruct_variant_sequence(2);
assert_eq!(
variants,
vec![0, 1],
"chain through crashed state still works"
);
let descs = pool.reconstruct_action_descriptions(2);
assert_eq!(descs.len(), 3);
assert_eq!(descs[0], "root");
assert_eq!(descs[1], "deposit");
assert_eq!(descs[2], "borrow");
}
#[test]
fn test_is_novel_crash_dedup() {
let mut pool = StatePool::new(100, 10);
let hash_a = 0x1234567890ABCDEFu64;
let hash_b = 0xFEDCBA0987654321u64;
assert!(
pool.is_novel_crash(hash_a),
"first occurrence should be novel"
);
assert!(
!pool.is_novel_crash(hash_a),
"duplicate should not be novel"
);
assert!(
pool.is_novel_crash(hash_b),
"different hash should be novel"
);
assert_eq!(pool.unique_crash_count(), 2);
}
#[test]
fn test_simulate_fuzzer_iteration_5_rounds_no_stale_state() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let pk3 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
svm.set_account(pk3, make_account(300, &[3; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_a = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
m
},
sysvars: clock_to_sysvars(&clock),
};
let delta_b = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk2, Arc::new(make_account(2000, &[0xB2; 8])));
m
},
sysvars: clock_to_sysvars(&clock),
};
let empty_delta = SvmSnapshot::empty(clock.clone());
let mut divergent_keys = FastHashSet::default();
let mut prev_delta_arc: Option<SvmSnapshot> = None;
let mut prev_exec_dirty = FastHashSet::default();
let schedule = [
(
&delta_a,
vec![(pk3, Some(make_account(999, &[0xFF; 8])))],
true,
),
(
&delta_b,
vec![(pk1, Some(make_account(888, &[0xFE; 8])))],
true,
),
(&delta_a, vec![], true), (
&empty_delta,
vec![(pk2, Some(make_account(777, &[0xFD; 8])))],
false,
), (&delta_b, vec![], true),
];
for (i, (delta, mods, success)) in schedule.iter().enumerate() {
simulate_fuzzer_iteration(
&initial,
&mut svm,
&mut divergent_keys,
&mut prev_delta_arc,
&mut prev_exec_dirty,
delta,
mods,
*success,
);
match i {
0 => {
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
1000,
"iter0: pk1 from delta_a"
);
assert_eq!(
svm.get_account(&pk3).unwrap().lamports,
999,
"iter0: pk3 exec modified"
);
}
1 => {
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
2000,
"iter1: pk2 from delta_b"
);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
888,
"iter1: pk1 exec modified"
);
}
2 => {
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
1000,
"iter2: pk1 from delta_a"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
200,
"iter2: pk2 initial"
);
assert_eq!(
svm.get_account(&pk3).unwrap().lamports,
300,
"iter2: pk3 initial"
);
}
3 => {
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
100,
"iter3: pk1 initial"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
777,
"iter3: pk2 exec modified"
);
}
4 => {
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
2000,
"iter4: pk2 from delta_b"
);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
100,
"iter4: pk1 initial"
);
}
_ => unreachable!(),
}
}
}
#[test]
fn test_simulate_fuzzer_iteration_cpi_account_cleanup() {
let pk1 = Pubkey::new_unique();
let pk_cpi = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let clock = svm.get_sysvar::<Clock>();
let delta_empty = SvmSnapshot::empty(clock.clone());
let mut divergent_keys = FastHashSet::default();
let mut prev_delta_arc: Option<SvmSnapshot> = None;
let mut prev_exec_dirty = FastHashSet::default();
let cpi_created = simulate_fuzzer_iteration(
&initial,
&mut svm,
&mut divergent_keys,
&mut prev_delta_arc,
&mut prev_exec_dirty,
&delta_empty,
&[(pk_cpi, Some(make_account(5000, &[0xCC; 8])))],
true,
);
assert_eq!(cpi_created, vec![pk_cpi]);
assert!(
svm.get_account(&pk_cpi).is_some(),
"CPI account exists after iter1"
);
simulate_fuzzer_iteration(
&initial,
&mut svm,
&mut divergent_keys,
&mut prev_delta_arc,
&mut prev_exec_dirty,
&delta_empty,
&[],
true,
);
assert!(
svm.get_account(&pk_cpi).is_none(),
"CPI account should be cleaned up after iteration 2 restore"
);
}
#[test]
fn test_periodic_svm_reset_restores_pristine_state() {
let pk1 = Pubkey::new_unique();
let pk2 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
svm.set_account(pk2, make_account(200, &[2; 8])).unwrap();
let pristine = SvmSnapshot::take_all(&svm);
let tracked: HashSet<Pubkey> = [pk1, pk2].into_iter().collect();
let snap = SvmSnapshot::take(&svm, &tracked);
let mut dirty = DirtyTracker::new();
for i in 0..10u64 {
dirty.clear();
let pk_ephemeral = Pubkey::new_unique();
svm.set_account(pk_ephemeral, make_account(i * 100 + 1, &[i as u8; 4]))
.unwrap();
dirty.mark_account_dirty(&pk_ephemeral);
svm.set_account(pk1, make_account(i * 1000, &[i as u8; 8]))
.unwrap();
dirty.mark_account_dirty(&pk1);
snap.restore(&mut svm, &dirty);
}
pristine.restore_full(&mut svm);
assert_eq!(
svm.get_account(&pk1).unwrap().lamports,
100,
"pk1 at pristine value"
);
assert_eq!(
svm.get_account(&pk2).unwrap().lamports,
200,
"pk2 at pristine value"
);
}
#[test]
fn test_stateless_restore_resets_clock() {
let mut svm = LiteSVM::new();
let pk1 = Pubkey::new_unique();
svm.set_account(pk1, make_account(100, &[1; 4])).unwrap();
let tracked: HashSet<Pubkey> = [pk1].into_iter().collect();
let snap = SvmSnapshot::take(&svm, &tracked);
let original_clock = svm.get_sysvar::<Clock>();
let mut new_clock = original_clock.clone();
new_clock.slot = 9999;
new_clock.unix_timestamp = 999999;
svm.set_sysvar(&new_clock);
assert_eq!(svm.get_sysvar::<Clock>().slot, 9999);
let mut dirty = DirtyTracker::new();
dirty.mark_clock_dirty(100);
snap.restore(&mut svm, &dirty);
assert_eq!(
svm.get_sysvar::<Clock>().slot,
original_clock.slot,
"clock should be restored to snapshot value"
);
}
#[test]
fn test_stateful_restore_selective_uses_delta_clock() {
let pk1 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let delta = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, Arc::new(make_account(1000, &[0xA1; 8])));
m
},
sysvars: make_test_sysvars(42),
};
initial.restore_selective(&mut svm, &FastHashSet::default(), &delta);
let clock = svm.get_sysvar::<Clock>();
assert_eq!(clock.slot, 42, "clock should come from delta, not initial");
}
#[test]
fn test_restore_selective_from_uses_next_delta_clock() {
let pk1 = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.set_account(pk1, make_account(100, &[1; 8])).unwrap();
let initial = SvmSnapshot::take_all(&svm);
let delta_prev = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, Arc::new(make_account(500, &[0x55; 8])));
m
},
sysvars: make_test_sysvars(10),
};
let delta_next = SvmSnapshot {
accounts: {
let mut m = FastHashMap::default();
m.insert(pk1, Arc::new(make_account(1000, &[0xAA; 8])));
m
},
sysvars: make_test_sysvars(99),
};
initial.restore_selective(&mut svm, &FastHashSet::default(), &delta_prev);
assert_eq!(svm.get_sysvar::<Clock>().slot, 10);
let divergent: FastHashSet<Pubkey> = [pk1].into_iter().collect();
initial.restore_selective_from(
&mut svm,
&divergent,
&delta_prev,
&delta_next,
&FastHashSet::default(),
);
assert_eq!(
svm.get_sysvar::<Clock>().slot,
99,
"clock should come from next_delta"
);
}