Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Honor the configured transaction history capacity independently of its backing allocation, including after resizing, re-enabling, and snapshot restoration.

## [0.18.1] - 2026-10-19

### Changed
Expand Down
39 changes: 22 additions & 17 deletions crates/litesvm/src/history.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,62 @@
use {crate::types::TransactionResult, indexmap::IndexMap, solana_signature::Signature};

#[derive(Clone)]
pub struct TransactionHistory(IndexMap<Signature, TransactionResult>);
pub struct TransactionHistory {
entries: IndexMap<Signature, TransactionResult>,
capacity: usize,
}

impl TransactionHistory {
pub fn new() -> Self {
TransactionHistory(IndexMap::with_capacity(32))
Self {
entries: IndexMap::with_capacity(32),
capacity: 32,
}
}

pub fn set_capacity(&mut self, new_cap: usize) {
if new_cap <= self.0.capacity() {
self.0.truncate(new_cap);
self.0.shrink_to_fit();
} else {
self.0.reserve(new_cap - self.0.capacity())
if new_cap <= self.capacity {
self.entries.truncate(new_cap);
self.entries.shrink_to_fit();
}
self.capacity = new_cap;
}

pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionResult> {
self.0.get(signature)
self.entries.get(signature)
}

pub fn is_enabled(&self) -> bool {
self.0.capacity() != 0
self.capacity != 0
}

pub fn add_new_transaction(&mut self, signature: Signature, result: TransactionResult) {
let capacity = self.0.capacity();
let capacity = self.capacity;
if capacity != 0 {
if self.0.len() == capacity {
self.0.shift_remove_index(0);
if self.entries.len() == capacity {
self.entries.shift_remove_index(0);
}
self.0.insert(signature, result);
self.entries.insert(signature, result);
}
}

pub fn check_transaction(&self, signature: &Signature) -> bool {
self.0.contains_key(signature)
self.entries.contains_key(signature)
}

#[cfg(feature = "persistence-internal")]
pub fn entries(&self) -> &IndexMap<Signature, TransactionResult> {
&self.0
&self.entries
}

#[cfg(feature = "persistence-internal")]
pub fn capacity(&self) -> usize {
self.0.capacity()
self.capacity
}

#[cfg(feature = "persistence-internal")]
pub fn from_entries(entries: IndexMap<Signature, TransactionResult>, capacity: usize) -> Self {
let mut history = TransactionHistory(entries);
let mut history = Self { entries, capacity };
history.set_capacity(capacity);
history
}
Expand Down
116 changes: 116 additions & 0 deletions crates/litesvm/tests/history_capacity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use {
litesvm::LiteSVM, solana_address::Address, solana_keypair::Keypair, solana_signer::Signer,
solana_system_interface::instruction::transfer, solana_transaction::Transaction,
solana_transaction_error::TransactionError,
};

fn fixture(capacity: usize) -> (LiteSVM, Keypair, Address) {
let mut svm = LiteSVM::new().with_transaction_history(capacity);
let payer = Keypair::new();
let recipient = Address::new_unique();
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
svm.airdrop(&recipient, 1_000_000).unwrap();
(svm, payer, recipient)
}

fn transaction(svm: &LiteSVM, payer: &Keypair, recipient: &Address, amount: u64) -> Transaction {
Transaction::new_signed_with_payer(
&[transfer(&payer.pubkey(), recipient, amount)],
Some(&payer.pubkey()),
&[payer],
svm.latest_blockhash(),
)
}

#[test]
fn configured_capacity_retains_exactly_the_latest_transactions() {
for capacity in [1, 2, 4, 32, 33] {
let (mut svm, payer, recipient) = fixture(capacity);
let mut signatures = Vec::new();
for amount in 1..=capacity as u64 + 2 {
let tx = transaction(&svm, &payer, &recipient, amount);
let meta = svm.send_transaction(tx).unwrap();
signatures.push(meta.signature);
}
for (index, signature) in signatures.iter().enumerate() {
assert_eq!(
svm.get_transaction(signature).is_some(),
index >= 2,
"capacity {capacity}, transaction {index}"
);
}
let tx = transaction(&svm, &payer, &recipient, capacity as u64 + 2);
let balance = svm.get_balance(&payer.pubkey());
assert_eq!(
svm.send_transaction(tx).unwrap_err().err,
TransactionError::AlreadyProcessed
);
assert_eq!(svm.get_balance(&payer.pubkey()), balance);
}
}

#[test]
fn increasing_capacity_with_few_entries_uses_the_requested_limit() {
let (mut svm, payer, recipient) = fixture(32);
svm = svm.with_transaction_history(33);
let mut signatures = Vec::new();
for amount in 1..=34 {
let tx = transaction(&svm, &payer, &recipient, amount);
signatures.push(svm.send_transaction(tx).unwrap().signature);
}
assert!(svm.get_transaction(&signatures[0]).is_none());
for signature in &signatures[1..] {
assert!(svm.get_transaction(signature).is_some());
}
}

#[test]
fn setting_capacity_after_partial_use_keeps_the_configured_limit() {
let (mut svm, payer, recipient) = fixture(32);
svm = svm.with_transaction_history(8);
svm = svm.with_transaction_history(8);
let mut signatures = Vec::new();
for amount in 1..=9 {
let tx = transaction(&svm, &payer, &recipient, amount);
signatures.push(svm.send_transaction(tx).unwrap().signature);
}
assert!(svm.get_transaction(&signatures[0]).is_none());
for signature in &signatures[1..] {
assert!(svm.get_transaction(signature).is_some());
}
}

#[test]
fn history_can_be_disabled_and_reenabled_with_capacity_one() {
let (mut svm, payer, recipient) = fixture(32);
let tx = transaction(&svm, &payer, &recipient, 1);
let signature = tx.signatures[0];
svm.send_transaction(tx.clone()).unwrap();
svm = svm.with_transaction_history(0);
assert!(svm.get_transaction(&signature).is_none());
svm.send_transaction(tx.clone()).unwrap();
assert!(svm.get_transaction(&signature).is_none());
svm = svm.with_transaction_history(1);
svm.send_transaction(tx).unwrap();
assert!(svm.get_transaction(&signature).is_some());
let next = transaction(&svm, &payer, &recipient, 2);
let next_signature = svm.send_transaction(next).unwrap().signature;
assert!(svm.get_transaction(&signature).is_none());
assert!(svm.get_transaction(&next_signature).is_some());
}

#[test]
fn failed_transactions_use_the_same_retention_limit() {
let (mut svm, payer, recipient) = fixture(1);
let tx = transaction(&svm, &payer, &recipient, u64::MAX);
let signature = tx.signatures[0];
let failed = svm.send_transaction(tx.clone()).unwrap_err();
assert_eq!(svm.get_transaction(&signature), Some(&Err(failed)));
assert_eq!(
svm.send_transaction(tx).unwrap_err().err,
TransactionError::AlreadyProcessed
);
let next = transaction(&svm, &payer, &recipient, 1);
svm.send_transaction(next).unwrap();
assert!(svm.get_transaction(&signature).is_none());
}
44 changes: 44 additions & 0 deletions crates/persistence/tests/history_capacity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use {
litesvm::LiteSVM,
litesvm_persistence::{from_bytes, to_bytes},
solana_address::Address,
solana_keypair::Keypair,
solana_signer::Signer,
solana_system_interface::instruction::transfer,
solana_transaction::Transaction,
};

#[test]
fn configured_capacity_survives_empty_and_populated_snapshots() {
for capacity in [1, 4, 33] {
let payer = Keypair::new();
let recipient = Address::new_unique();
let mut svm = LiteSVM::new().with_transaction_history(0);
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
svm.airdrop(&recipient, 1_000_000).unwrap();
svm = svm.with_transaction_history(capacity);
for populated in [false, true] {
let mut restored = from_bytes(&to_bytes(&svm).unwrap()).unwrap();
assert_eq!(restored.transaction_history_capacity(), capacity);
let mut signatures = Vec::new();
for amount in 1..=capacity as u64 + 1 {
let tx = Transaction::new_signed_with_payer(
&[transfer(&payer.pubkey(), &recipient, amount)],
Some(&payer.pubkey()),
&[&payer],
restored.latest_blockhash(),
);
signatures.push(restored.send_transaction(tx).unwrap().signature);
}
assert!(restored.get_transaction(&signatures[0]).is_none());
for signature in &signatures[1..] {
assert!(restored.get_transaction(signature).is_some());
}
assert_eq!(restored.transaction_history_capacity(), capacity);
if !populated {
svm = restored;
svm.expire_blockhash();
}
}
}
}