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
77 changes: 77 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ indexmap = "2.12"
itertools = "0.14"
libsecp256k1 = "0.6.0"
litesvm = { path = "crates/litesvm", version = "0.11" }
litesvm-persistence = { path = "crates/persistence", version = "0.11" }
log = "0.4"
napi = { version = "3.8.3", default-features = false }
napi-build = "2.3.1"
Expand Down Expand Up @@ -90,6 +91,7 @@ solana-vote-interface = "5.0.0"
spl-associated-token-account-interface = "2.0.0"
spl-token-2022-interface = "2.0.0"
spl-token-interface = "2.0.0"
tempfile = "3"
test-log = "0.2"
thiserror = "2.0"

Expand Down
1 change: 1 addition & 0 deletions crates/litesvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ invocation-inspect-callback = []
nodejs-internal = ["dep:qualifier_attr"]
hashbrown = ["dep:hashbrown"]
serde = []
persistence-internal = ["serde"]
precompiles = ["dep:agave-precompiles"]
register-tracing = ["invocation-inspect-callback", "dep:hex", "dep:sha2"]

Expand Down
39 changes: 38 additions & 1 deletion crates/litesvm/src/accounts_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,39 @@ impl AccountsDb {
self.inner.insert(address, data);
}

/// Rebuilds the sysvar cache from account data already present in `self.inner`.
#[cfg(feature = "persistence-internal")]
pub(crate) fn rebuild_sysvar_cache(&mut self) {
self.sysvar_cache.reset();
let accounts = &self.inner;
self.sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| {
if let Some(acc) = accounts.get(pubkey) {
set_sysvar(acc.data())
}
});
if let Ok(clock) = self.sysvar_cache.get_clock() {
self.programs_cache.set_slot_for_tests(clock.slot);
}
}

/// Scans all accounts for executable BPF programs and loads them into the program cache.
#[cfg(feature = "persistence-internal")]
pub(crate) fn load_all_existing_programs(&mut self) -> Result<(), LiteSVMError> {
let executable_keys: Vec<Address> = self
.inner
.iter()
.filter(|(_, acc)| acc.executable() && acc.owner() != &native_loader::ID)
.map(|(k, _)| *k)
.collect();

for key in executable_keys {
let account = self.inner.get(&key).unwrap().clone();
let loaded = self.load_program(&account)?;
self.programs_cache.replenish(key, Arc::new(loaded));
}
Ok(())
}

pub(crate) fn sync_accounts(
&mut self,
mut accounts: Vec<(Address, AccountSharedData)>,
Expand All @@ -236,7 +269,11 @@ impl AccountsDb {

let owner = program_account.owner();
let program_runtime_v1 = self.environments.program_runtime_v1.clone();
let slot = self.sysvar_cache.get_clock().unwrap().slot;
let slot = self
.sysvar_cache
.get_clock()
.map(|c| c.slot)
.unwrap_or(0);

if bpf_loader::check_id(owner) || bpf_loader_deprecated::check_id(owner) {
ProgramCacheEntry::new(
Expand Down
17 changes: 17 additions & 0 deletions crates/litesvm/src/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,21 @@ impl TransactionHistory {
pub fn check_transaction(&self, signature: &Signature) -> bool {
self.0.contains_key(signature)
}

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

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

#[cfg(feature = "persistence-internal")]
pub fn from_entries(entries: IndexMap<Signature, TransactionResult>, capacity: usize) -> Self {
let mut history = TransactionHistory(entries);
history.set_capacity(capacity);
history
}
}
87 changes: 87 additions & 0 deletions crates/litesvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ use {
},
};

#[cfg(feature = "persistence-internal")]
use indexmap::IndexMap;

pub mod error;
pub mod types;

Expand Down Expand Up @@ -1678,6 +1681,90 @@ impl LiteSVM {

self
}

// ── persistence-internal: getters ──────────────────────────────────

#[cfg(feature = "persistence-internal")]
pub fn airdrop_keypair_bytes(&self) -> &[u8; 64] {
&self.airdrop_kp
}

#[cfg(feature = "persistence-internal")]
pub fn get_blockhash_check(&self) -> bool {
self.blockhash_check
}

#[cfg(feature = "persistence-internal")]
pub fn get_fee_structure(&self) -> &FeeStructure {
&self.fee_structure
}

#[cfg(feature = "persistence-internal")]
pub fn get_log_bytes_limit(&self) -> Option<usize> {
self.log_bytes_limit
}

#[cfg(feature = "persistence-internal")]
pub fn get_feature_set_ref(&self) -> &FeatureSet {
&self.feature_set
}

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

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

// ── persistence-internal: setters ──────────────────────────────────

#[cfg(feature = "persistence-internal")]
pub fn set_latest_blockhash(&mut self, hash: Hash) {
self.latest_blockhash = hash;
}

#[cfg(feature = "persistence-internal")]
pub fn set_airdrop_keypair(&mut self, kp: [u8; 64]) {
self.airdrop_kp = kp;
}

#[cfg(feature = "persistence-internal")]
pub fn set_account_no_checks(&mut self, pubkey: Address, account: AccountSharedData) {
self.accounts.add_account_no_checks(pubkey, account);
}

#[cfg(feature = "persistence-internal")]
pub fn restore_transaction_history(
&mut self,
entries: IndexMap<Signature, TransactionResult>,
capacity: usize,
) {
self.history = TransactionHistory::from_entries(entries, capacity);
}

#[cfg(feature = "persistence-internal")]
pub fn set_fee_structure(&mut self, fee_structure: FeeStructure) {
self.fee_structure = fee_structure;
}

// ── persistence-internal: cache rebuild ────────────────────────────

/// Rebuilds all derived caches after bulk account insertion.
///
/// Must be called after restoring accounts via `set_account_no_checks`.
/// Order matters: environments first, then sysvars, then BPF programs.
#[cfg(feature = "persistence-internal")]
pub fn rebuild_caches(&mut self) -> Result<(), LiteSVMError> {
self.reserved_account_keys =
Self::reserved_account_keys_for_feature_set(&self.feature_set);
self.set_builtins();
self.accounts.rebuild_sysvar_cache();
self.accounts.load_all_existing_programs()?;
Ok(())
}
}

struct CheckAndProcessTransactionSuccessCore<'ix_data> {
Expand Down
36 changes: 36 additions & 0 deletions crates/persistence/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "litesvm-persistence"
description = "Save and load LiteSVM state snapshots"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
litesvm = { workspace = true, features = ["persistence-internal"] }
bincode.workspace = true
serde = { workspace = true, features = ["derive"] }
indexmap = { workspace = true, features = ["serde"] }
solana-account.workspace = true
solana-address.workspace = true
solana-compute-budget.workspace = true
solana-fee-structure.workspace = true
solana-hash.workspace = true
solana-signature.workspace = true
agave-feature-set = { workspace = true, features = ["agave-unstable-api"] }
thiserror.workspace = true

[dev-dependencies]
solana-instruction.workspace = true
solana-keypair.workspace = true
solana-signer.workspace = true
solana-system-interface = { workspace = true, features = ["bincode"] }
solana-native-token.workspace = true
solana-message.workspace = true
solana-transaction = { workspace = true, features = ["verify"] }
solana-clock.workspace = true
tempfile.workspace = true

[lints]
workspace = true
15 changes: 15 additions & 0 deletions crates/persistence/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use thiserror::Error;

#[derive(Error, Debug)]
pub enum PersistenceError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("serialization error: {0}")]
Serialize(#[from] bincode::Error),
#[error("unsupported snapshot version: {0}")]
UnsupportedVersion(u8),
#[error("failed to rebuild caches: {0}")]
CacheRebuild(#[from] litesvm::error::LiteSVMError),
#[error("serialization thread panicked")]
ThreadPanic,
}
Loading