use super::helpers::*;
use super::super::*;
use super::super::svm_snapshot::FINGERPRINT_BITS;
use crate::FastHashSet;
use anchor_lang::prelude::Clock;
use anchor_lang::prelude::sysvar::SysvarId;
use litesvm::LiteSVM;
use solana_account::Account;
use solana_pubkey::Pubkey;
use std::collections::HashSet;
fn fresh_bitmap() -> Vec<u8> {
vec![0u8; FIELD_NOVELTY_BITMAP_SIZE]
}
fn setup_stake_scenario() -> (LiteSVM, Pubkey, SvmSnapshot) {
let mut svm = LiteSVM::new();
let stake_pk = Pubkey::new_unique();
let mut initial_data = vec![0u8; 200];
initial_data[0..4].copy_from_slice(&1u32.to_le_bytes());
initial_data[4..36].copy_from_slice(&[0xAA; 32]); initial_data[36..68].copy_from_slice(&[0xBB; 32]);
svm.set_account(stake_pk, Account {
lamports: 10_000_000_000, data: initial_data,
owner: Pubkey::from_str_const("Stake11111111111111111111111111111111111111"),
executable: false,
rent_epoch: 0,
}).unwrap();
let mut tracked = HashSet::new();
tracked.insert(stake_pk);
let initial = SvmSnapshot::take(&svm, &tracked);
(svm, stake_pk, initial)
}
#[test]
fn advance_slots_novelty_collapse() {
assert_eq!(slot_diff_bucket(0), 0);
assert_eq!(slot_diff_bucket(5), 5);
assert_eq!(slot_diff_bucket(50), 9 + 5); assert_eq!(slot_diff_bucket(500), 18 + 5); assert_eq!(slot_diff_bucket(1500), 27 + 1); assert_eq!(slot_diff_bucket(2000), 27 + 2);
assert_eq!(slot_diff_bucket(2001), 27 + 2); assert_eq!(slot_diff_bucket(5000), 27 + 5); assert_eq!(slot_diff_bucket(9999), 27 + 9); assert_eq!(slot_diff_bucket(10000), 36 + 1); assert_eq!(slot_diff_bucket(54520), 36 + 5); assert_eq!(slot_diff_bucket(99999), 36 + 9); assert_eq!(slot_diff_bucket(100_000), 45 + 1); assert_eq!(slot_diff_bucket(999_999), 45 + 9); assert_eq!(slot_diff_bucket(1_000_000), 55);
assert_ne!(slot_diff_bucket(5000), slot_diff_bucket(54520));
let (mut svm, _stake_pk, initial) = setup_stake_scenario();
let mut bitmap = fresh_bitmap();
let tracker = DirtyTracker::new();
svm.warp_to_slot(100);
let novel_100 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
assert!(novel_100 >= 1, "advance to slot 100 should be novel, got {}", novel_100);
svm.warp_to_slot(1000);
let novel_1000 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
assert!(novel_1000 >= 1, "advance to slot 1000 should be novel, got {}", novel_1000);
svm.warp_to_slot(3000);
let novel_3000 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
assert!(novel_3000 >= 1, "advance to slot 3000 should be novel, got {}", novel_3000);
svm.warp_to_slot(54520);
let novel_54520 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
assert!(novel_54520 >= 1,
"advance to slot 54520 should now be novel (bucket {} vs {}), got {}",
slot_diff_bucket(54520), slot_diff_bucket(3000), novel_54520
);
}
#[test]
fn deactivate_after_advance_not_novel() {
let (mut svm, stake_pk, initial) = setup_stake_scenario();
let mut bitmap = fresh_bitmap();
let mut deactivated_data_a = svm.get_account(&stake_pk).unwrap().data.clone();
deactivated_data_a[0..4].copy_from_slice(&2u32.to_le_bytes());
deactivated_data_a[128..136].copy_from_slice(&1u64.to_le_bytes());
deactivated_data_a[136..144].copy_from_slice(&0u64.to_le_bytes());
svm.set_account(stake_pk, Account {
lamports: 10_000_000_000,
data: deactivated_data_a,
owner: Pubkey::from_str_const("Stake11111111111111111111111111111111111111"),
executable: false,
rent_epoch: 0,
}).unwrap();
let mut tracker_a = DirtyTracker::new();
tracker_a.mark_account_dirty(&stake_pk);
let novel_a = unsafe {
check_field_novelty(&svm, &tracker_a, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
assert!(novel_a >= 1, "first deactivation should be novel, got {}", novel_a);
eprintln!("[diag] scenario A (deactivate from initial): {} novel bits", novel_a);
let initial_account = initial.accounts().get(&stake_pk).unwrap();
svm.set_account(stake_pk, (**initial_account).clone()).unwrap();
svm.warp_to_slot(54520);
let mut deactivated_data_b = (**initial_account).data.clone();
deactivated_data_b[0..4].copy_from_slice(&2u32.to_le_bytes());
deactivated_data_b[128..136].copy_from_slice(&50u64.to_le_bytes());
deactivated_data_b[136..144].copy_from_slice(&0u64.to_le_bytes());
svm.set_account(stake_pk, Account {
lamports: 10_000_000_000,
data: deactivated_data_b,
owner: Pubkey::from_str_const("Stake11111111111111111111111111111111111111"),
executable: false,
rent_epoch: 0,
}).unwrap();
let mut tracker_b = DirtyTracker::new();
tracker_b.mark_account_dirty(&stake_pk);
let novel_b = unsafe {
check_field_novelty(&svm, &tracker_b, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
eprintln!("[diag] scenario B (deactivate after advance 54520): {} novel bits", novel_b);
eprintln!(
"[diag] DIAGNOSIS: scenario B novelty = {}. If 0, confirms hypothesis \
(deactivate novelty eaten by prior states). If >0, issue is advance_slots \
eviction or action selection.",
novel_b
);
}
#[test]
fn epoch_distinguishes_clock_novelty() {
let (mut svm, _stake_pk, initial) = setup_stake_scenario();
let mut bitmap = fresh_bitmap();
let tracker = DirtyTracker::new();
let clock_e1 = Clock { slot: 500, epoch: 1, epoch_start_timestamp: 0, leader_schedule_epoch: 0, unix_timestamp: 500 };
svm.set_sysvar(&clock_e1);
let novel_e1 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
assert!(novel_e1 >= 1, "epoch 1 should be novel");
eprintln!("[diag] epoch 1: {} novel bits", novel_e1);
let clock_e5 = Clock { slot: 2500, epoch: 5, epoch_start_timestamp: 0, leader_schedule_epoch: 0, unix_timestamp: 2500 };
svm.set_sysvar(&clock_e5);
let novel_e5 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
eprintln!("[diag] epoch 5: {} novel bits (should be >0 if epoch buckets differ)", novel_e5);
let clock_e50 = Clock { slot: 25000, epoch: 50, epoch_start_timestamp: 0, leader_schedule_epoch: 0, unix_timestamp: 25000 };
svm.set_sysvar(&clock_e50);
let novel_e50 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
eprintln!("[diag] epoch 50: {} novel bits", novel_e50);
let b1 = value_bucket(1);
let b5 = value_bucket(5);
let b50 = value_bucket(50);
eprintln!("[diag] value_bucket: epoch 1 → {}, epoch 5 → {}, epoch 50 → {}", b1, b5, b50);
assert_ne!(b1, b5, "epochs 1 and 5 should be in different value buckets");
assert_ne!(b5, b50, "epochs 5 and 50 should be in different value buckets");
let s500 = slot_diff_bucket(500);
let s2500 = slot_diff_bucket(2500);
let s25000 = slot_diff_bucket(25000);
eprintln!("[diag] slot_diff_bucket: slot 500 → {}, slot 2500 → {}, slot 25000 → {}", s500, s2500, s25000);
}
#[test]
fn clock_x_account_novelty_distinguishes_epochs() {
let (mut svm, stake_pk, initial) = setup_stake_scenario();
let mut bitmap = fresh_bitmap();
let initial_account = initial.accounts().get(&stake_pk).unwrap();
let mut data_a = (**initial_account).data.clone();
data_a[0..4].copy_from_slice(&2u32.to_le_bytes()); data_a[128..136].copy_from_slice(&1u64.to_le_bytes());
svm.set_account(stake_pk, Account {
lamports: 10_000_000_000,
data: data_a,
owner: (**initial_account).owner,
executable: false,
rent_epoch: 0,
}).unwrap();
let mut tracker_a = DirtyTracker::new();
tracker_a.mark_account_dirty(&stake_pk);
let novel_a = unsafe {
check_field_novelty(&svm, &tracker_a, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
eprintln!("[diag] state A (deactivate at epoch 0): {} novel bits", novel_a);
assert!(novel_a >= 1);
svm.set_account(stake_pk, (**initial_account).clone()).unwrap();
let clock_e50 = Clock { slot: 54520, epoch: 50, epoch_start_timestamp: 0, leader_schedule_epoch: 0, unix_timestamp: 54520 };
svm.set_sysvar(&clock_e50);
let mut data_b = (**initial_account).data.clone();
data_b[0..4].copy_from_slice(&2u32.to_le_bytes()); data_b[128..136].copy_from_slice(&50u64.to_le_bytes());
svm.set_account(stake_pk, Account {
lamports: 10_000_000_000,
data: data_b,
owner: (**initial_account).owner,
executable: false,
rent_epoch: 0,
}).unwrap();
let mut tracker_b = DirtyTracker::new();
tracker_b.mark_account_dirty(&stake_pk);
let novel_b = unsafe {
check_field_novelty(&svm, &tracker_b, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
eprintln!("[diag] state B (deactivate at epoch 50): {} novel bits", novel_b);
eprintln!(
"[diag] CONCLUSION: novel_b = {}. If >0, the deactivate-after-advance IS \
distinguishable — the real bottleneck is the advance_slots intermediate being \
evicted from the pool before the fuzzer tries deactivate from it.",
novel_b
);
}
#[test]
fn advance_slots_intermediate_novelty_budget() {
let (mut svm, _stake_pk, initial) = setup_stake_scenario();
let mut bitmap = fresh_bitmap();
let tracker = DirtyTracker::new();
svm.warp_to_slot(54520);
let novel = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
eprintln!("[diag] advance_slots(54520) from initial:");
eprintln!("[diag] novel bits = {}", novel);
eprintln!("[diag] slot_diff_bucket = {}", slot_diff_bucket(54520));
let clock: Clock = svm.get_sysvar();
eprintln!("[diag] epoch = {}", clock.epoch);
eprintln!("[diag] value_bucket(epoch) = {}", value_bucket(clock.epoch));
svm.warp_to_slot(100_000); let novel2 = unsafe {
check_field_novelty(&svm, &tracker, &initial, bitmap.as_mut_ptr(), bitmap.len())
};
let clock2: Clock = svm.get_sysvar();
eprintln!("[diag] advance_slots(100000) after advance_slots(54520):");
eprintln!("[diag] novel bits = {}", novel2);
eprintln!("[diag] epoch = {}, value_bucket = {}", clock2.epoch, value_bucket(clock2.epoch));
eprintln!(
"[diag] DIAGNOSIS: advance_slots produces {} novelty bits and 0 edge coverage. \
This makes it a low-weight state that gets evicted early, preventing the fuzzer \
from ever trying deactivate from delegate_stake → advance_slots.",
novel
);
}
#[test]
fn advance_slots_fingerprint_discrimination() {
let (mut svm, stake_pk, initial) = setup_stake_scenario();
let mut tracker = DirtyTracker::new();
tracker.mark_clock_dirty(100);
svm.warp_to_slot(100);
let fp_100 = compute_state_fingerprint_from_snapshot(&svm, &tracker, &initial);
initial.restore_full(&mut svm);
svm.warp_to_slot(1000);
let fp_1000 = compute_state_fingerprint_from_snapshot(&svm, &tracker, &initial);
initial.restore_full(&mut svm);
svm.warp_to_slot(3000);
let fp_3000 = compute_state_fingerprint_from_snapshot(&svm, &tracker, &initial);
initial.restore_full(&mut svm);
svm.warp_to_slot(54520);
let fp_54520 = compute_state_fingerprint_from_snapshot(&svm, &tracker, &initial);
eprintln!("[diag] fingerprints:");
eprintln!("[diag] advance(100): {:#018x}", fp_100);
eprintln!("[diag] advance(1000): {:#018x}", fp_1000);
eprintln!("[diag] advance(3000): {:#018x}", fp_3000);
eprintln!("[diag] advance(54520): {:#018x}", fp_54520);
assert_ne!(fp_100, fp_1000, "different slot_diff buckets should differ");
assert_ne!(fp_1000, fp_3000, "slot 1000 (bucket 28) vs 3000 (bucket 30) should differ");
assert_ne!(fp_3000, fp_54520,
"advance(3000) and advance(54520) should now have different fingerprints \
(slot_diff_bucket {} vs {})", slot_diff_bucket(3000), slot_diff_bucket(54520));
}