use std::num::NonZeroUsize;
use lru::LruCache;
use solana_signature::Signature;
use crate::types::TransactionResult;
#[derive(Clone, Debug)]
pub(crate) struct TransactionHistory {
entries: LruCache<Signature, TransactionResult>,
max_entries: usize,
}
impl TransactionHistory {
pub(crate) fn new() -> Self {
Self::with_capacity(32)
}
fn with_capacity(capacity: usize) -> Self {
Self {
entries: LruCache::new(NonZeroUsize::new(capacity.max(1)).expect("max(1) is non-zero")),
max_entries: capacity,
}
}
pub(crate) fn set_capacity(&mut self, new_cap: usize) {
if new_cap == 0 {
self.entries.clear();
self.max_entries = 0;
return;
}
let new_size = NonZeroUsize::new(new_cap).expect("new_cap is non-zero here");
self.entries.resize(new_size);
self.max_entries = new_cap;
}
pub(crate) fn get_transaction(&self, signature: &Signature) -> Option<&TransactionResult> {
if self.max_entries == 0 {
return None;
}
self.entries.peek(signature)
}
pub(crate) fn add_new_transaction(&mut self, signature: Signature, result: TransactionResult) {
if self.max_entries == 0 {
return;
}
self.entries.put(signature, result);
}
#[inline]
pub(crate) fn check_transaction(&self, signature: &Signature) -> bool {
if self.max_entries == 0 {
return false;
}
self.entries.peek(signature).is_some()
}
#[inline]
pub(crate) const fn is_enabled(&self) -> bool {
self.max_entries != 0
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use solana_transaction_error::TransactionError;
use super::*;
use crate::types::{FailedTransactionMetadata, TransactionMetadata};
fn signature(seed: u8) -> Signature {
Signature::from([seed; 64])
}
fn ok_result() -> TransactionResult {
Ok(TransactionMetadata::default())
}
fn err_result(err: TransactionError) -> TransactionResult {
Err(FailedTransactionMetadata { err, meta: TransactionMetadata::default() })
}
#[test]
fn set_capacity_limits_history_by_entry_count() {
let mut history = TransactionHistory::new();
history.set_capacity(1);
let first = Signature::from([1; 64]);
let second = Signature::from([2; 64]);
let result = Ok(TransactionMetadata::default());
history.add_new_transaction(first, result.clone());
history.add_new_transaction(second, result);
assert!(!history.check_transaction(&first));
assert!(history.check_transaction(&second));
}
#[test]
fn zero_capacity_disables_history_storage() {
let mut history = TransactionHistory::new();
history.set_capacity(0);
let signature = Signature::from([3; 64]);
history.add_new_transaction(
signature,
Err(FailedTransactionMetadata {
err: TransactionError::AlreadyProcessed,
meta: TransactionMetadata::default(),
}),
);
assert!(!history.check_transaction(&signature));
assert!(history.get_transaction(&signature).is_none());
}
#[test]
fn new_history_is_enabled_with_the_default_capacity() {
let history = TransactionHistory::new();
assert!(history.is_enabled());
assert_eq!(history.max_entries, 32);
}
#[test]
fn get_transaction_returns_the_stored_failure_variant() {
let mut history = TransactionHistory::new();
let sig = signature(9);
history.add_new_transaction(sig, err_result(TransactionError::AlreadyProcessed));
match history.get_transaction(&sig) {
Some(Err(meta)) => assert!(matches!(meta.err, TransactionError::AlreadyProcessed)),
other => panic!("expected a stored Err, got {other:?}"),
}
assert!(history.check_transaction(&sig));
}
#[test]
fn get_transaction_of_an_unknown_signature_is_none() {
let history = TransactionHistory::new();
assert!(history.get_transaction(&signature(1)).is_none());
assert!(!history.check_transaction(&signature(1)));
}
#[test]
fn raising_the_capacity_preserves_existing_entries() {
let mut history = TransactionHistory::new();
history.set_capacity(1);
history.add_new_transaction(signature(1), ok_result());
history.set_capacity(8);
assert!(history.is_enabled());
assert_eq!(history.max_entries, 8);
assert!(history.check_transaction(&signature(1)));
assert!(history.get_transaction(&signature(1)).is_some());
}
#[test]
fn shrinking_the_capacity_evicts_least_recently_used_entries() {
let mut history = TransactionHistory::new();
history.set_capacity(4);
for seed in 0..4 {
history.add_new_transaction(signature(seed), ok_result());
}
history.set_capacity(2);
assert!(!history.check_transaction(&signature(0)));
assert!(!history.check_transaction(&signature(1)));
assert!(history.check_transaction(&signature(2)));
assert!(history.check_transaction(&signature(3)));
}
#[test]
fn re_enabling_after_zero_capacity_stores_again() {
let mut history = TransactionHistory::new();
history.set_capacity(0);
assert!(!history.is_enabled());
history.set_capacity(4);
assert!(history.is_enabled());
assert_eq!(history.max_entries, 4);
history.add_new_transaction(signature(5), ok_result());
assert!(history.check_transaction(&signature(5)));
}
#[test]
fn disabling_history_twice_is_idempotent() {
let mut history = TransactionHistory::new();
history.set_capacity(0);
history.set_capacity(0);
assert!(!history.is_enabled());
history.add_new_transaction(signature(1), ok_result());
assert!(!history.check_transaction(&signature(1)));
}
#[test]
fn capacity_one_keeps_only_the_most_recent_entry() {
let mut history = TransactionHistory::with_capacity(1);
history.add_new_transaction(signature(1), ok_result());
history.add_new_transaction(signature(2), ok_result());
history.add_new_transaction(signature(3), ok_result());
assert!(!history.check_transaction(&signature(1)));
assert!(!history.check_transaction(&signature(2)));
assert!(history.check_transaction(&signature(3)));
}
#[test]
fn peeking_does_not_change_lru_ordering() {
let mut history = TransactionHistory::with_capacity(2);
history.add_new_transaction(signature(1), ok_result());
history.add_new_transaction(signature(2), ok_result());
assert!(history.get_transaction(&signature(1)).is_some());
history.add_new_transaction(signature(3), ok_result());
assert!(!history.check_transaction(&signature(1)));
assert!(history.check_transaction(&signature(2)));
assert!(history.check_transaction(&signature(3)));
}
proptest! {
#[test]
fn history_retains_exactly_the_last_capacity_entries(
capacity in 1usize..8,
total in 0usize..24,
) {
let mut history = TransactionHistory::with_capacity(capacity);
for index in 0..total {
history.add_new_transaction(signature(index as u8), ok_result());
}
let first_kept = total.saturating_sub(capacity);
for index in 0..total {
if index < first_kept {
prop_assert!(
!history.check_transaction(&signature(index as u8)),
"seed {index}"
);
} else {
prop_assert!(
history.check_transaction(&signature(index as u8)),
"seed {index}"
);
}
}
}
}
proptest! {
#[test]
fn disabled_history_never_records(
capacity in 1usize..6,
writes in 0usize..10,
) {
let mut history = TransactionHistory::with_capacity(capacity);
history.set_capacity(0);
for index in 0..writes {
history.add_new_transaction(signature(index as u8), ok_result());
}
prop_assert!(!history.is_enabled());
prop_assert_eq!(history.max_entries, 0);
for index in 0..writes {
prop_assert!(!history.check_transaction(&signature(index as u8)));
}
}
}
proptest! {
#[test]
fn capacity_assignment_is_idempotent(
capacity in 1usize..8,
repeats in 1usize..5,
) {
let mut history = TransactionHistory::new();
for _ in 0..repeats {
history.set_capacity(capacity);
}
prop_assert!(history.is_enabled());
prop_assert_eq!(history.max_entries, capacity);
history.set_capacity(0);
for _ in 0..repeats {
history.set_capacity(0);
}
prop_assert!(!history.is_enabled());
prop_assert_eq!(history.max_entries, 0);
}
}
}