diff --git a/Cargo.toml b/Cargo.toml
index 18be369a..bfd1735d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
[workspace]
members = [
+ "accountsdb",
"nucleus",
"programs/magic-root-interface",
"programs/v42-calculator-interface",
@@ -21,30 +22,37 @@ rust-version = "1.94.1"
version = "0.1.0"
[workspace.dependencies]
+accountsdb = { path = "accountsdb", package = "magicblock-accountsdb" }
magic-root-interface = { path = "programs/magic-root-interface" }
magic-root-program = { path = "programs/magic-root-program" }
nucleus = { path = "nucleus", package = "magicblock-engine-nucleus" }
-v42-calculator-interface = { path = "programs/v42-calculator-interface", default-features = false }
solana-account = { path = "solana/account" }
solana-program-runtime = { path = "solana/program-runtime" }
solana-svm = { path = "solana/svm" }
solana-transaction-context = { path = "solana/transaction-context" }
+v42-calculator-interface = { path = "programs/v42-calculator-interface", default-features = false }
ahash = "0.8.12"
arc-swap = "1.9.1"
assert_matches = "1.5.0"
base64 = "0.22.1"
bincode = "1.3.3"
+bitcode = "0.6.9"
bitflags = "2.11.1"
blake3 = "1.8.5"
+bytemuck = "1.25"
cfg-if = "1.0.4"
+clonetree = "0.0.2"
criterion = "0.8.2"
derive_more = "2.1.1"
env_logger = "0.11.8"
futures = { version = "0.3.32", default-features = false }
heed = { version = "0.22.1", default-features = false }
itertools = "0.13.0"
+memmap2 = "0.9.10"
+num_cpus = "1.17.0"
oneshot = "0.2.1"
+parking_lot = "0.12.5"
prometheus = { version = "0.14.0", default-features = false }
qualifier_attr = "0.2.2"
rand = "0.9.2"
@@ -60,7 +68,8 @@ thiserror = "2.0.17"
tokio = "1.52.1"
tokio-util = "0.7.18"
tracing = "0.1.44"
-tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] }
+tracing-subscriber = "0.3.23"
+twox-hash = { version = "2.1.2", default-features = false }
wincode = "0.5.1"
zstd = { version = "0.13.3", default-features = false }
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..01c073d3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,352 @@
+
MagicBlock Engine
+
+
+ Execution engine for ephemeral rollups — Solana transactions over durable, locally-owned state.
+
+
+
+
+
+
+
+
+
+
+
+---
+
+MagicBlock Engine executes Solana transactions for ephemeral rollups. It owns
+account state, records transaction and block history, and exposes asynchronous
+APIs for execution, simulation, reads, and subscriptions.
+
+## ✨ Highlights
+
+| | | |
+| :-- | :-- | :-- |
+| ⚙️ **Runs Solana programs** — a real SVM, without the overhead of a validator | 🗃️ **Storage that fits the account** — engine-owned state on disk, chain-mirrored state in memory | 📚 **Retained history** — transactions and blocks kept in segments you can retain or drop wholesale |
+| 🔁 **Replication** — mirror a live engine onto standby nodes over TCP | 🩹 **Recoverable startup** — restores snapshots and verifies replayed history after a crash | 📡 **Async APIs** — execute, simulate, read, and subscribe over live state |
+
+## 📖 Contents
+
+- [🚀 Starting the engine](#-starting-the-engine)
+- [🛑 Shutdown](#-shutdown)
+- [🔁 Replication](#-replication)
+- [📦 Account state](#-account-state)
+- [📨 Transactions](#-transactions)
+- [📡 Subscriptions](#-subscriptions)
+- [🩹 Startup and recovery](#-startup-and-recovery)
+- [🧩 Workspace layout](#-workspace-layout)
+
+---
+
+## 🚀 Starting the engine
+
+Bringing up an engine is mostly filling in one struct and awaiting one call —
+everything underneath (storage, ledger, scheduler, background tasks) is wired up
+for you.
+
+The embedding service must retain both the engine and its `ShutdownManager`.
+The manager coordinates every background service started by `Engine::new`.
+
+```rust
+use std::{num::NonZeroU64, path::PathBuf, time::Duration};
+
+use engine::Engine;
+use keeper::builder::KeeperBuilder;
+use nucleus::{
+ config::{AccountsDBParams, BlockstoreParams, LedgerParams},
+ shutdown::ShutdownManager,
+};
+use solana_keypair::Keypair;
+use solana_sysvar::rent::Rent;
+
+async fn open_engine(
+ home: PathBuf,
+) -> engine::Result<(Engine, ShutdownManager)> {
+ let mut shutdown = ShutdownManager::default();
+ let builder = KeeperBuilder {
+ authority: Keypair::new().into(),
+ accountsdb: AccountsDBParams {
+ directory: home.join("accountsdb"),
+ lru_capacity: 10_000,
+ },
+ ledger: LedgerParams {
+ directory: home.join("ledger"),
+ size_limit: 256 * 1024 * 1024 * 1024,
+ },
+ blockstore: BlockstoreParams {
+ blocktime: Duration::from_millis(400),
+ superblock: NonZeroU64::new(16).unwrap(),
+ },
+ builtins: Default::default(),
+ programs: Default::default(),
+ accounts: Default::default(),
+ rent: Rent::default(),
+ };
+
+ let engine = Engine::new(builder, None, &mut shutdown).await?;
+ Ok((engine, shutdown))
+}
+```
+
+The second argument chooses who advances blocks. `None` runs the built-in
+pacer, which produces blocks on its own clock — the standalone case. Passing a
+channel instead makes block boundaries caller-driven, as replication followers
+do when they step in time with a leader. External producers supply the slot and
+timestamp; the sequencer computes and overwrites the block hash and parent.
+
+The two modes also start differently: the built-in pacer wipes chain-mirrored
+volatile accounts at startup (internal system accounts stay available), so a
+standalone engine begins from clean external state. An external pacer keeps
+whatever volatile state was restored, which replication depends on.
+
+---
+
+## 🛑 Shutdown
+
+Shutdown isn't a hard stop — it unwinds in tiers, so in-flight work drains and
+durable state lands on disk before the process goes away.
+
+The host waits for an OS signal or premature service termination with
+`ShutdownManager::wait`. It should then stop external ingress and call
+`ShutdownManager::terminate` while retaining the engine handle.
+
+```rust
+let cause = shutdown.wait().await;
+
+// Stop accepting transactions and other external work here.
+shutdown.terminate().await;
+```
+
+`wait` returns whether shutdown was requested by an OS signal or by a managed
+service terminating early. Embedding processes can use the service reason to
+distinguish recoverable lifecycle events, such as a replication snapshot that
+requires reopening the engine, from fatal failures.
+
+Shutdown proceeds by service tier:
+
+1. A replication client stops consuming upstream state.
+2. The pacemaker stops producing boundaries and calls `Engine::shutdown`.
+3. The already-drained sequencer and terminally-synced ledger appender stop.
+4. Ledger readers, simulation, subscriptions, and other backing services stop.
+
+Internal pacing publishes a final block and flushes durable state. External
+pacing also writes volatile state to `CURRENT/volatile.db` after flushing the
+corresponding ledger cursor. The final sync explicitly closes ledger workers,
+so retained but inactive engine handles cannot hold shutdown open. Each tier
+has a bounded termination window.
+
+---
+
+## 🔁 Replication
+
+Point a follower at a leader and it keeps itself in sync — replaying the stream
+when it can, and pulling a fresh snapshot when it has fallen too far behind.
+
+Replication keeps a standby engine in step with a live one: a **leader** serves
+its history over TCP, and one or more **followers** replay that stream to stay
+current. On the leader machine, bind a dispatcher to a reachable address and
+serve the retained ledger:
+
+```rust
+use std::sync::Arc;
+
+use replicator::ReplicationDispatcher;
+
+let allowed = Arc::from([follower_identity]);
+ReplicationDispatcher::spawn(bind_addr, engine.clone(), allowed, &mut shutdown).await?;
+```
+
+On the follower machine, open its engine with an external pacer and connect the
+client to the leader's address:
+
+```rust
+use replicator::ReplicationClient;
+use tokio::sync::mpsc;
+
+let (block_tx, block_rx) = mpsc::channel(16);
+builder.authority.remote = Some(leader_identity);
+let engine = Engine::new(builder, Some(block_rx), &mut shutdown).await?;
+ReplicationClient::spawn(leader_addr, engine.clone(), block_tx, &mut shutdown)?;
+```
+
+Leader and follower local keypairs do not need to match. The server allowlist
+contains follower local identities and denies all access when empty. The
+follower's remote authority identifies its immediate upstream, whose signed
+responses must arrive within 30 seconds of the follower's clock.
+
+The external pacer keeps replicated blocks ordered with transactions, resets,
+and seals. If the leader's retained stream cannot satisfy the follower's cursor,
+it sends the newest snapshot. The client stages it, reports `RestartRequired`
+through the follower's shutdown manager, and the follower host reopens its
+engine from the same directories.
+
+---
+
+## 📦 Account state
+
+You never have to decide where an account lives — the engine watches what each
+account *is* and keeps it in the right place on its own.
+
+The engine holds two kinds of accounts and stores each where it makes sense:
+
+- Accounts the engine controls — delegated, ephemeral, and transient — are
+ authoritative here and **persisted to disk**.
+- Accounts that only mirror external chain or system state — read-only,
+ placeholders, and sysvars — are kept **in volatile memory**.
+
+An account's `AccountMode::authoritative()` classification decides which side it
+belongs to. When that changes, accountsdb moves the account and drops the stale
+copy from the other backend, so there is only ever one live copy. `Transient`
+accounts remain authoritative and persisted even though runtime code cannot
+mutate them.
+
+To change accounts directly, use `Engine::account(pubkey)`. `create`, `update`,
+`patch`, and `delete` each run as one signed, committed transaction and require
+the local signer to match the engine authority.
+
+```rust
+use solana_account::{AccountBuilder, AccountFieldPatch, AccountMode};
+use solana_pubkey::Pubkey;
+
+let key = Pubkey::new_unique();
+let owner = Pubkey::new_unique();
+let account = AccountBuilder::default()
+ .lamports(2_000_000)
+ .owner(owner)
+ .mode(AccountMode::Delegated)
+ .data(vec![1, 2, 3, 4])
+ .build();
+
+engine.account(key).create(account, None).await?;
+let current = engine.accounts().loader().load(&key)?;
+
+engine
+ .account(key)
+ .patch(vec![AccountFieldPatch::DataAt {
+ offset: 0,
+ data: vec![9; 4],
+ }])
+ .await?;
+
+let replacement = AccountBuilder::default()
+ .lamports(2_000_000)
+ .owner(owner)
+ .mode(AccountMode::Delegated)
+ .data(vec![5; 4])
+ .build();
+engine.account(key).update(replacement).await?;
+engine.account(key).delete().await?;
+```
+
+Each mutation is one committed transaction. `create` can also run optional
+post-finalize instructions in that transaction; if an instruction fails, the
+creation does not commit.
+
+Missing external accounts can be coordinated with `Engine::accounts().ensure`.
+The first caller receives `MissingAccount::Load`; concurrent callers receive a
+wait handle for the same pubkey. After storing the account, the loader calls
+`AccountLoad::complete(mode)` to publish success and update recency tracking for
+non-authoritative accounts. Dropping the load guard instead wakes waiters with a
+failed outcome.
+
+---
+
+## 📨 Transactions
+
+Hand it whatever you've already got — a few instructions, a `Message`, or raw
+encoded bytes — and pick how much you want to wait around for.
+
+`Engine::transaction` accepts an instruction slice, `Message`, sanitized
+`TransactionView`, or encoded transaction bytes. Instruction slices and messages
+use the effective authority as payer and the local signer with the latest
+blockhash, so local composition requires those identities to match.
+
+```rust
+use engine::Engine;
+use solana_instruction::Instruction;
+
+async fn submit(
+ engine: &Engine,
+ instructions: &[Instruction],
+) -> engine::Result<()> {
+ engine
+ .transaction(instructions)?
+ .execute()
+ .await?
+ .map_err(Into::into)
+}
+```
+
+- `execute` waits for the committed transaction result.
+- `schedule` queues execution without waiting for its result.
+- `simulate` executes against owned account copies without committing state.
+
+---
+
+## 📡 Subscriptions
+
+No polling loops — subscribe to what you care about and the engine pushes
+updates as they happen.
+
+Keeper accessors expose Tokio broadcast receivers for live state:
+
+```rust
+let mut account_updates = engine.accounts().subscribe(key).await;
+let mut blocks = engine.blocks().subscribe();
+
+let account = account_updates.recv().await?;
+let block = blocks.recv().await?;
+```
+
+Related accessors subscribe to program-owned accounts, cache evictions,
+snapshot completion, transaction status, logs, processed transactions, and
+service messages. Broadcast consumers must handle `Lagged` when they fall
+behind and `Closed` during shutdown; retained reads are available separately.
+
+---
+
+## 🩹 Startup and recovery
+
+After an interrupted write, the next start checks local state against retained
+history and restores a retained snapshot when necessary.
+
+Every startup reconciles the account store with the transaction history. A
+crash, corruption, and a staged replication snapshot enter the same recovery
+path, but recovery requires a valid retained snapshot when the current store
+cannot be used.
+
+Concretely: keeper validates the account store against the retained ledger. A
+corrupt store, or a valid one whose latest checkpoint trails the ledger, is
+replaced with the newest retained snapshot. If that restored state still trails
+the ledger tip, the engine replays the missing history to catch up, checking the
+rebuilt state against each recorded checkpoint and refusing to continue
+(`ReplayError::StateMismatch`) if they diverge. When the store is already
+current, nothing runs.
+
+---
+
+## 🧩 Workspace layout
+
+| Crate | Role |
+| :-- | :-- |
+| `nucleus` | Shared ledger, runtime, metrics, TLS, and shutdown types. |
+| `solana/*` | The runtime forks required by the engine account model. |
+| `accountsdb` | Owns persisted and volatile account storage and snapshots. |
+| `ledger` | Stores transactions, execution records, blocks, and superblocks. |
+| `keeper` | Opens both stores and provides caches, reads, and subscriptions. |
+| `processor` | Schedules transactions across SVM executors and commits results. |
+| `programs/*` | MagicRoot and the v42 test program and interfaces. |
+| `engine` | Wires the execution engine and exposes the public handle. |
+| `replicator` | Streams durable engine state between nodes. |
+
+Transactions are appended before execution, then paired with execution metadata.
+Successful dirty accounts are written through accountsdb and live notifications
+are published. Superblock boundaries quiesce execution while keeper snapshots
+accountsdb and archives it beside the next retained ledger segment.
+
+---
+
+
+ Built with 🦀 Rust · licensed under Apache-2.0 · © MagicBlock contributors
+
diff --git a/accountsdb/Cargo.toml b/accountsdb/Cargo.toml
new file mode 100644
index 00000000..d6cc7aeb
--- /dev/null
+++ b/accountsdb/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "magicblock-accountsdb"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+version.workspace = true
+
+[lib]
+name = "accountsdb"
+
+[features]
+testkit = []
+
+[dependencies]
+nucleus = { workspace = true, features = ["heed", "metrics"] }
+
+ahash = { workspace = true }
+bincode = { workspace = true }
+bytemuck = { workspace = true, features = ["derive", "extern_crate_std"] }
+clonetree = { workspace = true }
+derive_more = { workspace = true, features = ["from"] }
+heed = { workspace = true }
+memmap2 = { workspace = true }
+parking_lot = { workspace = true }
+scc = { workspace = true, features = ["serde"] }
+thiserror = { workspace = true }
+tracing = { workspace = true }
+twox-hash = { workspace = true, features = ["alloc", "xxhash3_64"] }
+
+solana-account = { workspace = true, features = ["serde"] }
+solana-pubkey = { workspace = true, features = ["bytemuck"] }
+
+[dev-dependencies]
+accountsdb = { workspace = true, features = ["testkit"] }
+assert_matches = { workspace = true }
+nucleus = { workspace = true, features = ["testkit"] }
+
+[lints]
+workspace = true
diff --git a/accountsdb/README.md b/accountsdb/README.md
new file mode 100644
index 00000000..2c25bfb5
--- /dev/null
+++ b/accountsdb/README.md
@@ -0,0 +1,66 @@
+# `magicblock-accountsdb`
+
+Accountsdb routes account state between two backends according to
+`AccountMode::authoritative()`:
+
+- `PersistedStore` is an mmap-backed account file with LMDB indexes. It holds
+ delegated, ephemeral, and transient accounts controlled by the engine.
+- `VolatileStore` is an in-memory map for externally owned state that can be
+ fetched again.
+
+Every store operation touches the backend required by both the account's current
+representation and authoritative classification. This commits borrowed images
+in persistent storage, inserts owned images there, updates owned volatile
+images, and removes stale copies after mode changes or closure. `Transient`
+remains authoritative and runtime-immutable until its lifecycle state resolves.
+
+`AccountsDB::commit` is the ledger-transaction boundary. It stores successful
+account transitions and then advances a persistent transaction counter; empty
+transitions from failed executions advance the counter as well. Direct `store`
+operations used for initialization, sysvars, and administrative writes do not.
+
+## Persisted layout
+
+`CURRENT/storage.db` contains a metadata header followed by account images in the
+borrowed `solana-account` layout. Each image includes its full pubkey so scans can
+recover keys without the index. Offsets are measured in 8-byte `StorageUnit`s.
+The transaction counter is metadata and is not part of the account checksum.
+
+The LMDB index under `CURRENT/index` contains:
+
+- `accounts`: account key tag to storage offset and owner tag.
+- `programs`: owner tag to account offsets.
+- `freelist`: image size to reusable offsets.
+
+`PersistedProgramIter` retains its read transaction for the persisted portion of
+iteration. The optional `testkit` feature uses smaller maps and growth blocks
+without changing the on-disk format.
+
+## Writes and compaction
+
+A persisted batch commits its LMDB transaction once. If applying or committing
+the batch fails, already committed borrowed images are rolled back so indexed
+state remains authoritative. Freed image spans enter the freelist.
+
+Defragmentation requires exclusive access. Snapshot export packs tail accounts
+into exact holes or the smallest fitting holes that leave a minimum useful
+remainder. It copies only between non-overlapping spans and publishes all
+relocations in one index transaction. Vacated source spans are deferred to the
+next pass, so some fragmented layouts may stall.
+
+After validation, keeper startup repeats committed packing passes to a fixed
+point before exposing the database to readers. Snapshot export runs one pass.
+Both paths synchronously flush successful changes.
+
+## Snapshots and volatile state
+
+`AccountsDB::snapshot` requires exclusive write access. It records the
+superblock id, runs one packing pass and flushes persisted state, clones the
+active tree, and serializes the current volatile map into the clone's
+`volatile.db`.
+
+`dump(None)` writes `CURRENT/volatile.db` for a clean externally paced shutdown.
+The next open restores that file into memory and removes it. `reset` instead
+removes chain-mirrored volatile accounts while preserving internal system
+accounts and rebuilding their owner indexes. Persisted engine-authoritative
+state is never reset.
diff --git a/accountsdb/src/lib.rs b/accountsdb/src/lib.rs
new file mode 100644
index 00000000..d1dc7ff0
--- /dev/null
+++ b/accountsdb/src/lib.rs
@@ -0,0 +1,349 @@
+#![doc = include_str!("../README.md")]
+
+use std::{
+ cell::RefCell,
+ collections::BTreeSet,
+ path::{Path, PathBuf},
+ sync::atomic::Ordering::*,
+};
+
+use derive_more::From;
+use nucleus::Slot;
+use nucleus::heed::RoTxnTls;
+use solana_account::{AccountSeqLock, AccountSharedData, CoWAccount};
+use solana_pubkey::Pubkey;
+use tracing::{info, warn};
+
+use crate::{
+ store::{DatabaseVersion, PersistedProgramIter, PersistedStore},
+ volatile::VolatileStore,
+};
+
+pub use snapshot::{BackupOp, SnapshotError, SnapshotResult};
+pub use store::mmap::STORAGE_FILE;
+
+mod metrics;
+mod snapshot;
+mod store;
+mod volatile;
+
+#[cfg(test)]
+mod tests;
+
+/// Active database subdirectory.
+const ACTIVE_DIR: &str = "CURRENT";
+
+/// Top-level account store backed by persisted and volatile backends.
+pub struct AccountsDB {
+ /// On-disk store for engine-authoritative account modes.
+ persisted: PersistedStore,
+ /// Rebuildable in-memory store for non-authoritative account modes.
+ volatile: VolatileStore,
+ /// Database root directory.
+ root: PathBuf,
+}
+
+impl AccountsDB {
+ /// Opens or creates the database at `root`.
+ pub fn new(root: impl AsRef) -> Result {
+ let root = root.as_ref().to_owned();
+ let path = Self::directory(&root);
+ let persisted = PersistedStore::new(&path)?;
+ let volatile = VolatileStore::new(&path)?;
+ info!(?path, "opened accountsdb");
+ let db = Self { persisted, volatile, root };
+ metrics::init(&db);
+ Ok(db)
+ }
+
+ /// Returns the active database directory under `root`.
+ pub fn directory(root: &Path) -> PathBuf {
+ root.join(ACTIVE_DIR)
+ }
+
+ /// Stores accounts in the backend that matches their current form.
+ ///
+ /// Persistent modes are kept in persisted storage. Other modes are kept in
+ /// volatile storage. Each batch also touches the opposite backend so stale
+ /// copies are removed after mode changes. Persisted failures roll back
+ /// borrowed images before the caller sees the error.
+ pub fn store<'a, AC>(&self, accounts: AC) -> Result<()>
+ where
+ AC: IntoIterator- + Clone,
+
::IntoIter: Clone,
+ {
+ let iter = accounts.clone().into_iter().filter(persisted);
+ self.persisted.upsert(iter)?;
+
+ let iter = accounts.into_iter().filter(volatile);
+ self.volatile.upsert(iter);
+
+ Ok(())
+ }
+
+ /// Commits one ledger transaction's account transitions.
+ ///
+ /// The transaction count advances only after every supplied transition is
+ /// stored successfully. Empty transitions count, including failed SVM
+ /// executions that reached the commit path without account writes.
+ pub fn commit<'a, AC>(&self, accounts: AC) -> Result<()>
+ where
+ AC: IntoIterator- + Clone,
+
::IntoIter: Clone,
+ {
+ self.store(accounts)?;
+ self.persisted.meta().transactions.fetch_add(1, Release);
+ Ok(())
+ }
+
+ /// Creates a loader that reuses a read transaction for persisted lookups.
+ pub fn loader(&self) -> AccountLoader<'_> {
+ AccountLoader::new(self)
+ }
+
+ /// Iterates program-owned accounts across both backends.
+ pub fn program(&self, owner: &Pubkey) -> Result> {
+ let persisted = self.persisted.program(*owner)?;
+ let volatile = self.volatile.program(owner);
+ Ok(ProgramIter { persisted, volatile, db: self })
+ }
+
+ /// Returns the latest slot persisted in the database metadata.
+ pub fn slot(&self) -> Slot {
+ self.persisted.meta().slot.load(Acquire)
+ }
+
+ /// Sets the database slot and flushes dirty pages asynchronously.
+ pub fn set_slot(&self, slot: Slot) -> Result<()> {
+ self.persisted.meta().slot.store(slot, Release);
+ self.flush(false)
+ }
+
+ /// Returns the id of the last sealed superblock recorded in the database metadata.
+ pub fn superblock(&self) -> Slot {
+ self.persisted.meta().superblock.load(Acquire)
+ }
+
+ /// Returns the number of successfully committed ledger transactions.
+ pub fn transactions(&self) -> u64 {
+ self.persisted.meta().transactions.load(Acquire)
+ }
+
+ /// Records the last sealed superblock id. Set on snapshot, and on replay
+ /// before recomputing the checksum to compare against a seal.
+ pub fn set_superblock(&self, superblock: u64) {
+ self.persisted.meta().superblock.store(superblock, Release);
+ }
+
+ /// Flushes persisted account storage, forcing synchronous durability when requested.
+ pub fn flush(&self, force: bool) -> Result<()> {
+ self.persisted.flush(force).map_err(Into::into)
+ }
+
+ /// Validates the persisted store checksum and on-disk format version.
+ pub fn validate(&self) -> Result<()> {
+ self.persisted.validate()
+ }
+
+ /// Compacts persisted storage to a non-overlapping packing fixed point.
+ ///
+ /// This must run only after validation and before loaders or iterators are
+ /// created. Vacated sources become eligible on the following pass, and all
+ /// successful passes are flushed synchronously before returning.
+ pub fn compact(&mut self) -> Result {
+ let mut reclaimed = 0;
+ let mut changed = false;
+ loop {
+ // SAFETY: `&mut self` excludes readers and writers through this handle;
+ // the store owns its LMDB environment and mapped storage.
+ let pass = unsafe { self.persisted.defragment() }?;
+ reclaimed += pass.reclaimed;
+ changed |= pass.changed();
+ if !pass.changed() {
+ break;
+ }
+ }
+ if changed {
+ self.flush(true)?;
+ }
+ Ok(reclaimed)
+ }
+
+ /// Returns the last checksum published on superblock boundary.
+ pub fn checksum(&self) -> u64 {
+ self.persisted.meta().checksum.load(Acquire)
+ }
+
+ /// Drops chain-mirrored volatile state while retaining system accounts;
+ /// persisted state is left untouched.
+ ///
+ /// Chain-owned accounts can be fetched again when synchronization resumes.
+ /// System accounts hold internal runtime state and survive the reset; their
+ /// volatile owner indexes are rebuilt. Persisted, engine-authoritative state
+ /// is never reset.
+ pub fn reset(&self) {
+ self.volatile.reset();
+ }
+}
+
+/// Loader that caches a read transaction for persisted account lookups.
+pub struct AccountLoader<'a> {
+ /// Cached read transaction for the persisted index.
+ txn: RefCell>>,
+ /// Database handle used for volatile and persisted lookups.
+ db: &'a AccountsDB,
+}
+
+impl<'a> AccountLoader<'a> {
+ /// Creates a new loader bound to `db`.
+ pub fn new(db: &'a AccountsDB) -> Self {
+ Self { txn: Default::default(), db }
+ }
+
+ /// Loads one account, reusing the persisted read transaction across calls.
+ ///
+ /// Reuse the loader for batch lookups to keep them on the same persisted
+ /// index snapshot. Persisted accounts take precedence over volatile ones.
+ pub fn load(&self, pubkey: &Pubkey) -> Result > {
+ let txn = &mut self.txn.borrow_mut();
+ if let Some(acc) = self.db.persisted.load(txn, pubkey)? {
+ metrics::load(StoreKind::Persisted);
+ return Ok(Some(acc.into()));
+ }
+ let account = self.db.volatile.load(pubkey).map(Into::into);
+ if account.is_some() {
+ metrics::load(StoreKind::Volatile);
+ } else {
+ metrics::load(StoreKind::Absent);
+ }
+ Ok(account)
+ }
+
+ /// Applies `reader` to an account image stable across a concurrent publish.
+ ///
+ /// Prefer this over [`Self::load`] when reading fields from persisted
+ /// accounts that may be updated concurrently. The reader may be called more
+ /// than once when the borrowed image changes, so it should have no side
+ /// effects.
+ pub fn read(&self, pubkey: &Pubkey, reader: F) -> Result>
+ where
+ F: Fn(&AccountSharedData) -> R,
+ {
+ let Some(account) = self.load(pubkey)? else {
+ return Ok(None);
+ };
+ Ok(Some(AccountSeqLock::new(account).read(reader)))
+ }
+
+ /// Returns whether an account exists in either backend.
+ pub fn contains(&self, pubkey: &Pubkey) -> Result {
+ let txn = &mut self.txn.borrow_mut();
+ if self.db.persisted.contains(txn, pubkey)? {
+ return Ok(true);
+ }
+ let contains = self.db.volatile.contains(pubkey);
+ Ok(contains)
+ }
+}
+
+/// Iterates program-owned accounts across both backends.
+pub struct ProgramIter<'a> {
+ /// Persisted program accounts.
+ persisted: Option>,
+ /// Volatile program pubkeys.
+ volatile: BTreeSet,
+ /// Database handle used to resolve volatile accounts.
+ db: &'a AccountsDB,
+}
+
+impl<'a> Iterator for ProgramIter<'a> {
+ type Item = AccountEntry;
+ /// Yields authoritative accounts first, then volatile ones.
+ fn next(&mut self) -> Option {
+ if let Some(persisted) = &mut self.persisted {
+ // Yield authoritative entries first.
+ if let Some(item) = persisted.next() {
+ return Some(item);
+ }
+ }
+ // Release the persisted read txn before draining volatile entries.
+ let _ = self.persisted.take();
+ // Then drain the in-memory set of non-authoritative accounts.
+ while let Some(pubkey) = self.volatile.pop_first() {
+ if let Some(account) = self.db.volatile.load(&pubkey) {
+ return Some((pubkey, account.into()));
+ }
+ warn!(%pubkey, "volatile program set references a missing account; skipping");
+ }
+ None
+ }
+}
+
+/// Errors returned by accountsdb.
+#[derive(Debug, thiserror::Error, From)]
+pub enum AccountsDBError {
+ /// LMDB key-value codec error.
+ #[error("LMDB key/value codec error: {0}")]
+ Codec(#[source] heed::BoxedError),
+ /// Filesystem error.
+ #[error("filesystem I/O error: {0}")]
+ IO(#[source] std::io::Error),
+ /// LMDB index access error.
+ #[error("LMDB index error: {0}")]
+ Index(#[source] heed::Error),
+ /// Storage allocation would exceed the maximum mapped size.
+ #[error("mapped storage exceeded the 32 GiB limit")]
+ Allocation,
+ /// Opened database version is not supported by current implementation.
+ #[error("unsupported database version: {0:?}")]
+ UnsupportedVersion(DatabaseVersion),
+ /// Database was corrupted during the shutdown/crash.
+ #[error("database integrity check failed")]
+ Corruption,
+ /// Volatile snapshot serialization error.
+ #[error("volatile snapshot serialization error: {0}")]
+ Serde(#[source] Box),
+}
+
+/// Result type used by the accountsdb crate.
+type Result = std::result::Result;
+/// Account key plus shared account payload.
+pub type AccountEntry = (Pubkey, AccountSharedData);
+
+/// Classification used by accountsdb metrics.
+#[derive(Clone, Copy)]
+pub(crate) enum StoreKind {
+ /// Mmap-backed persisted storage.
+ Persisted,
+ /// In-memory volatile storage.
+ Volatile,
+ /// Account was absent from both storage backends.
+ Absent,
+}
+
+impl StoreKind {
+ /// Returns the Prometheus label value for this classification.
+ pub(crate) fn label(self) -> &'static str {
+ match self {
+ StoreKind::Persisted => "persisted",
+ StoreKind::Volatile => "volatile",
+ StoreKind::Absent => "absent",
+ }
+ }
+}
+
+/// Returns `true` for entries that must touch persisted storage.
+fn persisted(entry: &&AccountEntry) -> bool {
+ match entry.1.cow() {
+ CoWAccount::Borrowed(_) => true,
+ CoWAccount::Owned(_) => entry.1.mode().authoritative(),
+ }
+}
+
+/// Returns `true` for entries that must touch volatile storage.
+fn volatile(entry: &&AccountEntry) -> bool {
+ match entry.1.cow() {
+ CoWAccount::Borrowed(_) => !entry.1.mode().authoritative(),
+ CoWAccount::Owned(_) => true,
+ }
+}
diff --git a/accountsdb/src/metrics.rs b/accountsdb/src/metrics.rs
new file mode 100644
index 00000000..a1263c96
--- /dev/null
+++ b/accountsdb/src/metrics.rs
@@ -0,0 +1,199 @@
+//! Prometheus metrics for accountsdb.
+
+use std::sync::{OnceLock, atomic::Ordering::*};
+
+use nucleus::metrics as metric;
+use nucleus::metrics::{IntCounter, IntGaugeVec, MetricOperation, MetricSpec, OperationCounters};
+
+use crate::{AccountsDB, StoreKind, store::Stats};
+
+/// Process-wide accountsdb metrics registered in the default Prometheus registry.
+static METRICS: OnceLock = OnceLock::new();
+
+/// Persisted account image load counter.
+const READS: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_reads",
+ help: "Persisted account image loads.",
+};
+/// Borrowed account commit counter.
+const COMMITS: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_commits",
+ help: "Borrowed account commits into persisted storage.",
+};
+/// Fresh mapped-storage allocation counter.
+const ALLOCS: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_allocs",
+ help: "Fresh allocations from the mapped persisted storage file.",
+};
+/// Persisted freelist reuse counter.
+const REALLOCS: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_reallocs",
+ help: "Allocations reused from the persisted freelist.",
+};
+/// Defragmentation relocation counter.
+const COMPACTIONS: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_compactions",
+ help: "Persisted account relocations during defragmentation.",
+};
+/// Persisted account removal counter.
+const REMOVALS: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_removals",
+ help: "Persisted account removals.",
+};
+
+/// Persisted storage resize counter.
+const RESIZES: MetricSpec = MetricSpec {
+ name: "accountsdb_persisted_resizes",
+ help: "Persisted storage file resizes.",
+};
+/// Account load counter grouped by source or absence.
+const LOADS: MetricSpec = MetricSpec {
+ name: "accountsdb_loads",
+ help: "Account loads by source or absence.",
+};
+/// Operation latency histogram recorded in microseconds.
+const OPERATION_TIME: MetricSpec = MetricSpec {
+ name: "accountsdb_operation_duration_micros",
+ help: "Accountsdb operation duration distribution in microseconds.",
+};
+/// Account count gauge grouped by backend store.
+const ACCOUNTS: MetricSpec = MetricSpec {
+ name: "accountsdb_accounts",
+ help: "Current accountsdb account count by backend store.",
+};
+
+/// Label used to separate persisted and volatile account counts.
+const STORE_LABEL: &str = "store";
+
+/// Accountsdb operation used as a low-cardinality operation label.
+#[derive(Clone, Copy)]
+pub(crate) enum Operation {
+ /// Persisted store flush path.
+ Flush,
+ /// Persisted checksum path.
+ Checksum,
+ /// Accountsdb snapshot path.
+ Snapshot,
+ /// Volatile-state dump path.
+ Dump,
+ /// Persisted store defragmentation path.
+ Defragmentation,
+}
+
+impl MetricOperation for Operation {
+ /// Returns the Prometheus label value for this operation.
+ fn label(self) -> &'static str {
+ match self {
+ Operation::Flush => "flush",
+ Operation::Checksum => "checksum",
+ Operation::Snapshot => "snapshot",
+ Operation::Dump => "dump",
+ Operation::Defragmentation => "defragmentation",
+ }
+ }
+}
+
+/// Registers accountsdb metrics once, seeding durable counters from persisted stats.
+pub(crate) fn init(db: &AccountsDB) {
+ METRICS.get_or_init(|| Metrics::new(db.persisted.storage.stats()));
+}
+
+/// Records one persisted account image load.
+pub(crate) fn read() {
+ metric::with_metrics(&METRICS, |m| m.reads.inc());
+}
+
+/// Records one borrowed account commit into persisted storage.
+pub(crate) fn commit() {
+ metric::with_metrics(&METRICS, |m| m.commits.inc());
+}
+
+/// Records one fresh allocation from the mapped persisted storage file.
+pub(crate) fn alloc() {
+ metric::with_metrics(&METRICS, |m| m.allocs.inc());
+}
+
+/// Records one allocation reuse from the persisted freelist.
+pub(crate) fn realloc() {
+ metric::with_metrics(&METRICS, |m| m.reallocs.inc());
+}
+
+/// Records persisted account relocations during defragmentation.
+pub(crate) fn compaction(count: u64) {
+ metric::with_metrics(&METRICS, |m| m.compactions.inc_by(count));
+}
+
+/// Records one persisted account removal.
+pub(crate) fn removal() {
+ metric::with_metrics(&METRICS, |m| m.removals.inc());
+}
+
+/// Records one persisted storage file resize.
+pub(crate) fn resize() {
+ metric::with_metrics(&METRICS, |m| m.resizes.inc());
+}
+
+/// Refreshes the current account count for `store`.
+pub(crate) fn accounts(store: StoreKind, count: u64) {
+ metric::with_metrics(&METRICS, |m| {
+ m.accounts.with_label_values(&[store.label()]).set(metric::gauge_value(count));
+ });
+}
+
+/// Starts an operation timer that records latency when the returned guard drops.
+pub(crate) fn time(op: Operation) -> metric::OperationTimer<'static> {
+ op.time(METRICS.get().map(|m| &m.operations))
+}
+
+/// Records one account load satisfied by `store`.
+pub(crate) fn load(store: StoreKind) {
+ metric::with_metrics(&METRICS, |m| m.loads[store as usize].inc());
+}
+
+/// Owns all Prometheus collectors registered by accountsdb.
+struct Metrics {
+ /// Durable persisted account image load counter.
+ reads: IntCounter,
+ /// Durable borrowed account commit counter.
+ commits: IntCounter,
+ /// Durable fresh allocation counter.
+ allocs: IntCounter,
+ /// Durable freelist reuse counter.
+ reallocs: IntCounter,
+ /// Durable defragmentation relocation counter.
+ compactions: IntCounter,
+ /// Durable persisted account removal counter.
+ removals: IntCounter,
+ /// Durable persisted storage resize counter.
+ resizes: IntCounter,
+ /// Per-`StoreKind` load counters pre-resolved from `loads_vec`.
+ loads: [IntCounter; 3],
+ /// Runtime operation duration and completion counters.
+ operations: OperationCounters,
+ /// Runtime account count gauge labeled by backend store.
+ accounts: IntGaugeVec,
+}
+
+impl Metrics {
+ /// Builds collectors and seeds durable counters from persisted mmap stats.
+ fn new(stats: &Stats) -> Self {
+ let loads_vec = metric::counter_vec(LOADS, &[STORE_LABEL]);
+ let loads = [
+ loads_vec.with_label_values(&[StoreKind::Persisted.label()]),
+ loads_vec.with_label_values(&[StoreKind::Volatile.label()]),
+ loads_vec.with_label_values(&[StoreKind::Absent.label()]),
+ ];
+ Self {
+ reads: metric::counter(READS, stats.reads.load(Relaxed)),
+ commits: metric::counter(COMMITS, stats.commits.load(Relaxed)),
+ allocs: metric::counter(ALLOCS, stats.allocs.load(Relaxed)),
+ reallocs: metric::counter(REALLOCS, stats.reallocs.load(Relaxed)),
+ compactions: metric::counter(COMPACTIONS, stats.compactions.load(Relaxed)),
+ removals: metric::counter(REMOVALS, stats.removals.load(Relaxed)),
+ resizes: metric::counter(RESIZES, stats.resizes.load(Relaxed)),
+ loads,
+ operations: OperationCounters::new(OPERATION_TIME),
+ accounts: metric::gauge_vec(ACCOUNTS, &[STORE_LABEL]),
+ }
+ }
+}
diff --git a/accountsdb/src/snapshot.rs b/accountsdb/src/snapshot.rs
new file mode 100644
index 00000000..d5190008
--- /dev/null
+++ b/accountsdb/src/snapshot.rs
@@ -0,0 +1,116 @@
+//! Snapshot export helpers.
+
+use std::{
+ fs::{self, File},
+ io::{self, BufWriter},
+ path::PathBuf,
+};
+
+use nucleus::MB;
+use tracing::info;
+
+use crate::{
+ ACTIVE_DIR, AccountsDB,
+ metrics::{self, Operation},
+};
+
+/// Snapshot directory prefix.
+const PREFIX: &str = "snapshot-";
+/// Snapshot payload filename for the volatile store.
+pub(crate) const VOLATILE_DB_FILE: &str = "volatile.db";
+
+/// Errors while writing a snapshot directory.
+#[derive(thiserror::Error, Debug)]
+pub enum SnapshotError {
+ /// I/O while writing the snapshot.
+ #[error("snapshot export I/O error")]
+ IO(#[from] io::Error),
+ /// Failed to flush the persisted store before copying the tree.
+ #[error("failed to flush persisted store")]
+ Flush(#[from] heed::Error),
+ /// Failed to serialize the volatile store into the snapshot.
+ #[error("failed to serialize volatile store")]
+ Serde(#[from] Box),
+ /// Failed to clone the active database tree into the snapshot slot.
+ #[error("failed to clone snapshot tree")]
+ FsClone(#[from] Box),
+ /// No archived snapshot could be restored.
+ #[error("no valid archived accountsdb snapshot found")]
+ Missing,
+}
+
+/// Result type used by snapshot export and restore helpers.
+pub type SnapshotResult = Result;
+
+/// Active database backup operation.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum BackupOp {
+ /// Move the active database tree to its backup path.
+ Save,
+ /// Move the saved backup tree back to the active database path.
+ Restore,
+}
+
+impl AccountsDB {
+ /// Writes a superblock snapshot under `root`.
+ ///
+ /// # Safety
+ /// The caller must ensure exclusive access while the snapshot is in
+ /// progress. The persisted backend runs one non-overlapping packing pass
+ /// and is flushed before the active tree is cloned and the volatile store
+ /// is rewritten in the clone. That ordering keeps the exported state
+ /// coherent only when no concurrent access can race with the export.
+ pub unsafe fn snapshot(&self, superblock: u64) -> SnapshotResult {
+ let _timer = metrics::time(Operation::Snapshot);
+ let src = self.root.join(ACTIVE_DIR);
+ let dst = self.root.join(format!("{PREFIX}{superblock:0>9}"));
+ self.set_superblock(superblock);
+ // SAFETY: snapshot owns exclusive access, so defrag cannot race with
+ // readers or writers while compacting the persisted store.
+ unsafe { self.persisted.defragment() }?;
+ // Persisted state must reach disk before we copy the active tree.
+ self.persisted.flush(true)?;
+ // Clone the whole active tree, then replace the volatile payload below.
+ clonetree::clone_tree(src, &dst, &Default::default()).map_err(Box::new)?;
+ self.dump(Some(&dst))?;
+
+ Ok(dst)
+ }
+
+ /// Serializes volatile accounts into `volatile.db` under `dst`.
+ ///
+ /// When `dst` is omitted, writes into the active database tree so the next
+ /// open restores the volatile store and consumes the file. Callers must
+ /// prevent concurrent account writes to obtain a coherent image.
+ pub fn dump(&self, dst: Option<&PathBuf>) -> SnapshotResult<()> {
+ let _timer = metrics::time(Operation::Dump);
+ let path = match dst {
+ Some(dst) => dst.join(VOLATILE_DB_FILE),
+ None => Self::directory(&self.root).join(VOLATILE_DB_FILE),
+ };
+ let db = File::options().create(true).truncate(true).write(true).open(path)?;
+ let mut buffered = BufWriter::with_capacity(4 * MB, db);
+ bincode::serialize_into(&mut buffered, &self.volatile.accounts)?;
+ let db = buffered.into_inner().map_err(|e| e.into_error())?;
+ db.sync_data().map_err(Into::into)
+ }
+
+ /// Saves or restores the active database tree and returns its destination.
+ ///
+ /// After restoring, callers must drop this instance and reopen the database:
+ /// its open handles still refer to the removed active tree.
+ pub fn backup(&self, op: BackupOp) -> SnapshotResult {
+ let active = self.root.join(ACTIVE_DIR);
+ let backup = self.root.join(format!("{ACTIVE_DIR}.bkp"));
+ let (from, to) = match op {
+ BackupOp::Save => (&active, &backup),
+ BackupOp::Restore => (&backup, &active),
+ };
+ if to.exists() {
+ fs::remove_dir_all(to)?;
+ }
+ info!(?op, "accountsdb backup");
+ fs::rename(from, to)?;
+ Ok(to.clone())
+ }
+}
diff --git a/accountsdb/src/store/defrag.rs b/accountsdb/src/store/defrag.rs
new file mode 100644
index 00000000..a4fecd81
--- /dev/null
+++ b/accountsdb/src/store/defrag.rs
@@ -0,0 +1,343 @@
+#![allow(unsafe_op_in_unsafe_fn)]
+
+use std::{collections::BTreeSet, ops::Range};
+
+use heed::Result;
+use solana_account::BorrowedAccount;
+use tracing::info;
+
+use crate::{
+ metrics::{self, Operation},
+ store::kv::{Offset, OwnerAndOffset},
+};
+
+use super::PersistedStore;
+
+/// Smallest useful destination remainder, in 8-byte storage units.
+pub(crate) const MIN_REMAINDER: u32 = 43;
+type Fit = (u32, Offset, usize);
+
+/// Result of one committed packing pass.
+pub(crate) struct Defragged {
+ pub(crate) moved: usize,
+ pub(crate) reclaimed: u32,
+}
+
+impl Defragged {
+ pub(crate) fn changed(&self) -> bool {
+ self.moved > 0 || self.reclaimed > 0
+ }
+}
+
+/// Free span in the persisted image file, measured in storage units.
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+struct Hole {
+ offset: Offset,
+ units: u32,
+}
+
+impl Hole {
+ fn new((units, offset): (u32, Offset)) -> Self {
+ Self { offset, units }
+ }
+
+ fn end(self) -> Offset {
+ self.offset + self.units
+ }
+}
+
+/// Adjacent entry-time holes treated as one packing destination.
+struct Run {
+ parts: Range,
+ free: Hole,
+}
+
+impl Run {
+ fn take(&mut self, units: u32) -> Offset {
+ debug_assert!(units <= self.free.units);
+ let dst = self.free.offset;
+ self.free.offset = self.free.offset + units;
+ self.free.units -= units;
+ dst
+ }
+}
+
+/// One account relocation planned against entry-time free space.
+#[derive(Clone, Copy)]
+struct Move {
+ src: Offset,
+ dst: Offset,
+ units: u32,
+}
+
+impl Move {
+ fn source(self) -> Hole {
+ Hole {
+ offset: self.src,
+ units: self.units,
+ }
+ }
+}
+
+/// Temporary state for one non-overlapping packing pass.
+struct Defrag<'a> {
+ store: &'a PersistedStore,
+ holes: Vec,
+ runs: Vec,
+ moves: Vec,
+ tail: Offset,
+}
+
+impl PersistedStore {
+ /// Packs tail accounts into holes that existed at the start of this pass.
+ ///
+ /// Adjacent freelist entries form logical runs. An account uses an exact
+ /// fit when available, otherwise the smallest run that leaves at least
+ /// [`MIN_REMAINDER`] units. Destination remainders may accept more accounts
+ /// in this pass; vacated source spans are deferred until a later pass. Some
+ /// fragmented layouts therefore cannot progress.
+ ///
+ /// This operation is not crash-safe: interruption after publishing moved
+ /// offsets can leave the active tree inconsistent and require a backup.
+ ///
+ /// # Safety
+ ///
+ /// No concurrent access may touch the persisted index or mapped storage
+ /// while offsets are rewritten and bytes are moved.
+ pub(crate) unsafe fn defragment(&self) -> Result {
+ let _timer = metrics::time(Operation::Defragmentation);
+ Defrag::new(self)?.execute()
+ }
+}
+
+impl<'a> Defrag<'a> {
+ /// Reads a consistent entry-time layout and plans tail-to-left moves.
+ ///
+ /// # Safety
+ ///
+ /// The store must be exclusively accessed, and indexed offsets must point
+ /// to valid serialized accounts in its mapped storage.
+ unsafe fn new(store: &'a PersistedStore) -> Result {
+ let (mut holes, mut accounts) = {
+ let txn = store.index.env.read_txn()?;
+ let holes = store
+ .index
+ .freelist
+ .iter(&txn)?
+ .map(|r| r.map(Hole::new))
+ .collect::>>()?;
+ let accounts = if holes.is_empty() {
+ Vec::new()
+ } else {
+ store
+ .index
+ .accounts
+ .iter(&txn)?
+ .map(|r| r.map(|(_, data)| data.offset))
+ .collect::>>()?
+ };
+ (holes, accounts)
+ };
+ holes.sort_unstable();
+ accounts.sort_unstable();
+
+ let runs = Self::runs(&holes);
+ let mut defrag = Self {
+ store,
+ holes,
+ runs,
+ moves: Vec::new(),
+ tail: Offset(store.storage.cursor()),
+ };
+ defrag.pack(accounts.into_iter().rev());
+ Ok(defrag)
+ }
+
+ /// Groups physically adjacent holes without changing their freelist shape.
+ fn runs(holes: &[Hole]) -> Vec {
+ let mut runs = Vec::new();
+ let mut i = 0;
+ while i < holes.len() {
+ let first = i;
+ let offset = holes[i].offset;
+ let mut end = holes[i].end();
+ i += 1;
+ while let Some(hole) = holes.get(i)
+ && hole.offset == end
+ {
+ end = hole.end();
+ i += 1;
+ }
+ runs.push(Run {
+ parts: first..i,
+ free: Hole { offset, units: end - offset },
+ });
+ }
+ runs
+ }
+
+ /// Selects the best exact fit or the best fit with a useful remainder.
+ fn fit(fit: &BTreeSet, units: u32) -> Option {
+ let &(largest, _, _) = fit.last()?;
+ if units > largest {
+ return None;
+ }
+
+ let low = (units, Offset(0), 0);
+ let high = (units, Offset(u32::MAX), usize::MAX);
+ if let Some(exact) = fit.range(low..=high).next() {
+ return Some(*exact);
+ }
+
+ let minimum = units.checked_add(MIN_REMAINDER)?;
+ if minimum > largest {
+ return None;
+ }
+ fit.range((minimum, Offset(0), 0)..).next().copied()
+ }
+
+ /// Packs accounts in descending source order into eligible runs.
+ ///
+ /// # Safety
+ ///
+ /// Every supplied offset must point to a valid serialized account, and no
+ /// concurrent access may modify the index, freelist, or mapped storage.
+ unsafe fn pack(&mut self, accounts: impl Iterator- ) {
+ // Best fit by remaining units, then by the lowest current offset.
+ let mut fit: BTreeSet
= self
+ .runs
+ .iter()
+ .enumerate()
+ .map(|(i, run)| (run.free.units, run.free.offset, i))
+ .collect();
+ let mut eligible = self.runs.len();
+
+ for src in accounts {
+ // Runs are already ordered by their physical end.
+ while eligible > 0 && self.runs[eligible - 1].free.end() > src {
+ let i = eligible - 1;
+ fit.remove(&(self.runs[i].free.units, self.runs[i].free.offset, i));
+ eligible -= 1;
+ }
+ if fit.is_empty() {
+ break;
+ }
+
+ let units = BorrowedAccount::span(self.store.storage.at(src));
+ let Some((remaining, start, i)) = Self::fit(&fit, units) else {
+ continue;
+ };
+ fit.remove(&(remaining, start, i));
+ let dst = self.runs[i].take(units);
+ self.moves.push(Move { src, dst, units });
+ let free = self.runs[i].free;
+ if free.units > 0 {
+ fit.insert((free.units, free.offset, i));
+ }
+ }
+ }
+
+ /// Returns the first unit in the final free suffix without re-sorting it.
+ fn compacted_tail(&self) -> Offset {
+ let mut run = self.runs.len();
+ let mut movement = 0;
+ let mut tail = self.tail;
+
+ loop {
+ while run > 0 && self.runs[run - 1].free.units == 0 {
+ run -= 1;
+ }
+ let free = (run > 0).then(|| self.runs[run - 1].free);
+ let source = self.moves.get(movement).copied().map(Move::source);
+ let (hole, from_run) = match (free, source) {
+ (Some(free), Some(source)) => (free.max(source), free.offset >= source.offset),
+ (Some(free), None) => (free, true),
+ (None, Some(source)) => (source, false),
+ (None, None) => break,
+ };
+ if hole.end() != tail {
+ break;
+ }
+ tail = hole.offset;
+ if from_run {
+ run -= 1;
+ } else {
+ movement += 1;
+ }
+ }
+ tail
+ }
+
+ /// Copies the plan and publishes all index and freelist changes.
+ ///
+ /// # Safety
+ ///
+ /// The entry-time layout must remain unchanged since planning, and no
+ /// concurrent access may observe or modify storage while moves publish.
+ unsafe fn execute(self) -> Result {
+ let tail = self.compacted_tail();
+ let outcome = Defragged {
+ moved: self.moves.len(),
+ reclaimed: self.tail - tail,
+ };
+ if !outcome.changed() {
+ info!("nothing to defragment");
+ return Ok(outcome);
+ }
+
+ // Entry-time destinations are disjoint, so every source remains intact
+ // until the complete plan has been copied.
+ for movement in &self.moves {
+ self.store.storage.at(movement.src).copy_to_nonoverlapping(
+ self.store.storage.at(movement.dst),
+ movement.units as usize,
+ );
+ }
+
+ let mut txn = self.store.index.env.write_txn()?;
+ for movement in &self.moves {
+ let ptr = self.store.storage.at(movement.src);
+ let pubkey = BorrowedAccount::pubkey(ptr);
+ let owner = BorrowedAccount::init(ptr).owner().into();
+ let data = OwnerAndOffset { owner, offset: movement.dst };
+ self.store.index.relocate(&pubkey, movement.src, data, &mut txn)?;
+ }
+ self.publish(tail, &mut txn)?;
+ txn.commit()?;
+
+ self.store.storage.stats().compact(outcome.moved);
+ if outcome.reclaimed > 0 {
+ self.store.storage.shrink(tail.0)?;
+ }
+ info!(
+ moved = outcome.moved,
+ reclaimed = outcome.reclaimed,
+ "defragmented persisted storage"
+ );
+ Ok(outcome)
+ }
+
+ /// Publishes final free spans while retaining untouched component sizes.
+ fn publish(&self, tail: Offset, txn: &mut heed::RwTxn<'_>) -> Result<()> {
+ for run in &self.runs {
+ for &hole in &self.holes[run.parts.clone()] {
+ let offset = hole.offset.max(run.free.offset);
+ let end = hole.end().min(tail);
+ if offset == hole.offset && end == hole.end() {
+ continue;
+ }
+ self.store.index.freelist.delete_one_duplicate(txn, &hole.units, &hole.offset)?;
+ if offset < end {
+ self.store.index.freelist.put(txn, &(end - offset), &offset)?;
+ }
+ }
+ }
+ for movement in &self.moves {
+ if movement.src < tail {
+ let end = movement.source().end().min(tail);
+ self.store.index.freelist.put(txn, &(end - movement.src), &movement.src)?;
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/accountsdb/src/store/index.rs b/accountsdb/src/store/index.rs
new file mode 100644
index 00000000..776be137
--- /dev/null
+++ b/accountsdb/src/store/index.rs
@@ -0,0 +1,207 @@
+//! LMDB index for persisted accounts.
+//!
+//! The index maps compact pubkey tags to storage offsets and owner tags,
+//! plus a freelist keyed by image size.
+
+use std::{fs, mem, path::Path};
+
+use heed::{
+ Database, DatabaseFlags, Env, EnvFlags, EnvOpenOptions, IntegerComparator, Result, RoIter,
+ RoTxn, RwTxn, iteration_method::MoveOnCurrentKeyDuplicates,
+};
+use nucleus::heed::{DatabaseIndex, RoTxnTls};
+use solana_pubkey::Pubkey;
+
+use crate::store::kv::{KeyTail, Offset, OwnerAndOffset, PubkeyBytes, U32LE};
+
+/// LMDB map size for the index database.
+#[cfg(any(test, feature = "testkit"))]
+const INDEX_MAP_SIZE: usize = nucleus::MB;
+#[cfg(not(any(test, feature = "testkit")))]
+const INDEX_MAP_SIZE: usize = nucleus::GB;
+/// Subdirectory used for the LMDB index.
+const INDEX_SUBDIR: &str = "index";
+/// Accounts table name.
+const ACCOUNTS_INDEX: &str = "accounts";
+/// Program ownership table name.
+const PROGRAMS_INDEX: &str = "programs";
+/// Freelist table name.
+const FREELIST_INDEX: &str = "freelist";
+
+/// Iterator over all persisted accounts in pubkey order.
+type RoAccountIter<'a> = RoIter<'a, PubkeyBytes, OwnerAndOffset>;
+/// Duplicate iterator over program-owned persisted accounts.
+type RoProgramIter<'a> = RoIter<'a, KeyTail, Offset, MoveOnCurrentKeyDuplicates>;
+/// Iterator over persisted accounts.
+pub(crate) struct AccountIter<'a> {
+ /// Iterator over `pubkey -> account` entries.
+ pub(super) inner: RoAccountIter<'a>,
+ /// Keeps the read transaction alive for the iterator lifetime.
+ pub(super) _txn: RoTxnTls<'a>,
+}
+/// Duplicate iterator over persisted accounts for one owner.
+pub(crate) struct OwnerIter<'a> {
+ /// Duplicates iterator over `owner -> account` entries.
+ pub(crate) inner: RoProgramIter<'a>,
+ /// Keeps the read transaction alive for the iterator lifetime.
+ pub(crate) _txn: RoTxnTls<'a>,
+}
+
+/// LMDB index over persisted account offsets and owners.
+pub(crate) struct Index {
+ /// LMDB environment for the on-disk index.
+ pub(super) env: Env,
+ /// Account pubkey -> offset + owner keytag.
+ pub(super) accounts: Database,
+ /// Owner keytag -> offset.
+ pub(super) programs: Database,
+ /// Image size -> offset.
+ pub(super) freelist: Database,
+}
+
+impl Index {
+ /// Opens or creates the index directory and databases.
+ pub(crate) fn new(path: &Path) -> crate::Result {
+ let path = path.join(INDEX_SUBDIR);
+ fs::create_dir_all(&path)?;
+ // SAFETY: this process owns the index directory for the lifetime of
+ // the database, so the backing files are not mutated behind LMDB's back.
+ let env = unsafe {
+ EnvOpenOptions::new()
+ .max_dbs(3)
+ .map_size(INDEX_MAP_SIZE)
+ .flags(EnvFlags::WRITE_MAP)
+ .flags(EnvFlags::NO_READ_AHEAD)
+ .flags(EnvFlags::NO_SYNC)
+ .open(path)?
+ };
+
+ let mut txn = env.write_txn()?;
+ let accounts = env.database_options().name(ACCOUNTS_INDEX).types().create(&mut txn)?;
+ let programs = env
+ .database_options()
+ .name(PROGRAMS_INDEX)
+ .flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
+ .types()
+ .create(&mut txn)?;
+ let freelist = env
+ .database_options()
+ .name(FREELIST_INDEX)
+ .flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
+ .key_comparator()
+ .types()
+ .create(&mut txn)?;
+ txn.commit()?;
+ Ok(Self {
+ env,
+ accounts,
+ programs,
+ freelist,
+ })
+ }
+
+ /// Returns the persisted offset for `pubkey`.
+ pub(crate) fn offset(&self, key: &Pubkey, txn: &RoTxn<'_>) -> Result> {
+ let entry = self.accounts.get(txn, key)?;
+ Ok(entry.map(|e| e.offset))
+ }
+
+ /// Takes a freed span from the freelist when one matches `units`.
+ pub(crate) fn allocate(&self, units: u32, txn: &mut RwTxn<'_>) -> Result > {
+ let offset = self.freelist.get(txn, &units)?;
+ if let Some(offset) = offset {
+ self.freelist.delete_one_duplicate(txn, &units, &offset)?;
+ Ok(Some(offset))
+ } else {
+ Ok(None)
+ }
+ }
+
+ /// Inserts an account and its owner mapping.
+ pub(crate) fn insert(
+ &self,
+ key: &Pubkey,
+ data: OwnerAndOffset,
+ txn: &mut RwTxn<'_>,
+ ) -> Result<()> {
+ self.accounts.put(txn, key, &data)?;
+ let OwnerAndOffset { owner, offset } = data;
+ self.programs.put(txn, &owner, &offset)
+ }
+
+ /// Removes an account and returns its persisted offset.
+ pub(crate) fn delete(&self, key: &Pubkey, txn: &mut RwTxn<'_>) -> Result > {
+ let Some(entry) = self.accounts.get(txn, key)? else {
+ return Ok(None);
+ };
+
+ let OwnerAndOffset { owner, offset } = entry;
+ self.accounts.delete(txn, key)?;
+
+ self.programs.delete_one_duplicate(txn, &owner, &offset)?;
+ Ok(Some(offset))
+ }
+
+ /// Returns the duplicate iterator for accounts owned by `owner`.
+ pub(crate) fn program<'a>(&'a self, owner: Pubkey) -> Result >> {
+ let owner = owner.into();
+ let txn = self.env.read_txn()?;
+ let Some(iter) = self.programs.get_duplicates(&txn, &owner)? else {
+ return Ok(None);
+ };
+ // The duplicate iterator borrows `txn`; storing it in the wrapper keeps
+ // the borrow alive for the iterator lifetime.
+ // SAFETY: the wrapper owns `txn`, so the duplicate iterator cannot outlive it.
+ let iter = unsafe { mem::transmute::, RoProgramIter<'a>>(iter) };
+ Ok(Some(OwnerIter { _txn: txn, inner: iter }))
+ }
+
+ /// Returns an iterator over all accounts in pubkey order.
+ pub(crate) fn accounts<'a>(&'a self) -> Result> {
+ let txn = self.env.read_txn()?;
+ let iter = self.accounts.iter(&txn)?;
+ // The iterator borrows `txn`; storing it in the wrapper keeps the
+ // transaction alive for the iterator lifetime.
+ // SAFETY: the wrapper owns `txn`, so the iterator cannot outlive it.
+ let iter = unsafe { mem::transmute::, RoAccountIter<'a>>(iter) };
+ Ok(AccountIter { _txn: txn, inner: iter })
+ }
+
+ /// Moves an account entry to a new owner while preserving its offset.
+ pub(crate) fn update_owner(
+ &self,
+ acc: &Pubkey,
+ new: KeyTail,
+ txn: &mut RwTxn<'_>,
+ ) -> Result<()> {
+ let Some(val) = self.accounts.get(txn, acc)? else {
+ return Ok(());
+ };
+ let OwnerAndOffset { owner: old, offset } = val;
+ self.programs.delete_one_duplicate(txn, &old, &offset)?;
+
+ let data = OwnerAndOffset { owner: new, offset };
+ self.accounts.put(txn, acc, &data)?;
+ self.programs.put(txn, &new, &offset)
+ }
+
+ /// Moves an account entry to a new offset while preserving its owner.
+ pub(crate) fn relocate(
+ &self,
+ key: &Pubkey,
+ old: Offset,
+ new: OwnerAndOffset,
+ txn: &mut RwTxn<'_>,
+ ) -> Result<()> {
+ self.accounts.put(txn, key, &new)?;
+ let OwnerAndOffset { owner, offset } = new;
+ self.programs.delete_one_duplicate(txn, &owner, &old)?;
+ self.programs.put(txn, &owner, &offset)
+ }
+}
+
+impl DatabaseIndex for Index {
+ fn env(&self) -> &Env {
+ &self.env
+ }
+}
diff --git a/accountsdb/src/store/kv.rs b/accountsdb/src/store/kv.rs
new file mode 100644
index 00000000..75b26a1e
--- /dev/null
+++ b/accountsdb/src/store/kv.rs
@@ -0,0 +1,123 @@
+use std::{array, borrow::Cow, ops};
+
+use bytemuck::{Pod, Zeroable};
+use heed::{BoxedError, BytesDecode, BytesEncode, byteorder::LittleEndian, types::U32};
+use solana_pubkey::Pubkey;
+
+/// Result type used by LMDB byte codecs.
+pub(crate) type CodecResult = Result;
+/// Little-endian `u32` value stored in the freelist.
+pub(super) type U32LE = U32;
+/// Offset into mapped storage, measured in storage units.
+#[derive(Clone, Copy, Zeroable, Pod, PartialEq, Eq, PartialOrd, Ord)]
+#[repr(C)]
+pub(crate) struct Offset(pub(super) u32);
+
+/// Compact 16-byte LMDB tag derived from the tail half of a pubkey.
+#[derive(Clone, Copy, Pod, Zeroable)]
+#[repr(C)]
+pub(crate) struct KeyTail([u8; 16]);
+
+impl From for KeyTail {
+ fn from(v: Pubkey) -> Self {
+ Self(array::from_fn(|i| v.as_array()[i + size_of::()]))
+ }
+}
+
+/// Full 32-byte pubkey codec for the accounts table.
+pub(super) struct PubkeyBytes;
+
+/// LMDB value for the accounts table.
+#[derive(Clone, Copy, Pod, Zeroable)]
+#[repr(C)]
+pub(crate) struct OwnerAndOffset {
+ /// Owner key tag for the stored account image.
+ pub(crate) owner: KeyTail,
+ /// Offset into mapped storage.
+ pub(crate) offset: Offset,
+}
+
+impl<'a> BytesEncode<'a> for KeyTail {
+ type EItem = Self;
+
+ fn bytes_encode(item: &'a Self::EItem) -> CodecResult> {
+ Ok(bytemuck::bytes_of(item).into())
+ }
+}
+
+impl<'a> BytesDecode<'a> for KeyTail {
+ type DItem = &'a Self;
+
+ fn bytes_decode(bytes: &'a [u8]) -> CodecResult {
+ bytemuck::try_from_bytes(bytes).map_err(Into::into)
+ }
+}
+
+impl<'a> BytesEncode<'a> for PubkeyBytes {
+ type EItem = Pubkey;
+
+ fn bytes_encode(item: &'a Self::EItem) -> CodecResult> {
+ Ok(item.as_array().into())
+ }
+}
+
+impl<'a> BytesDecode<'a> for PubkeyBytes {
+ type DItem = &'a Pubkey;
+
+ fn bytes_decode(bytes: &'a [u8]) -> CodecResult {
+ bytemuck::try_from_bytes(bytes).map_err(Into::into)
+ }
+}
+
+impl<'a> BytesEncode<'a> for OwnerAndOffset {
+ type EItem = Self;
+
+ fn bytes_encode(item: &'a Self::EItem) -> CodecResult> {
+ Ok(bytemuck::bytes_of(item).into())
+ }
+}
+
+impl<'a> BytesDecode<'a> for OwnerAndOffset {
+ type DItem = Self;
+
+ fn bytes_decode(bytes: &'a [u8]) -> CodecResult {
+ bytemuck::try_pod_read_unaligned(bytes).map_err(Into::into)
+ }
+}
+
+impl<'a> BytesEncode<'a> for Offset {
+ type EItem = Self;
+
+ fn bytes_encode(item: &'a Self::EItem) -> CodecResult> {
+ U32LE::bytes_encode(&item.0)
+ }
+}
+
+impl<'a> BytesDecode<'a> for Offset {
+ type DItem = Self;
+
+ fn bytes_decode(bytes: &'a [u8]) -> CodecResult {
+ U32LE::bytes_decode(bytes).map(Self)
+ }
+}
+
+impl ops::Add for Offset {
+ type Output = Self;
+ fn add(self, rhs: u32) -> Self::Output {
+ Self(self.0 + rhs)
+ }
+}
+
+impl ops::Sub for Offset {
+ type Output = Self;
+ fn sub(self, rhs: u32) -> Self::Output {
+ Self(self.0 - rhs)
+ }
+}
+
+impl ops::Sub for Offset {
+ type Output = u32;
+ fn sub(self, rhs: Self) -> Self::Output {
+ self.0 - rhs.0
+ }
+}
diff --git a/accountsdb/src/store/mmap.rs b/accountsdb/src/store/mmap.rs
new file mode 100644
index 00000000..e394016d
--- /dev/null
+++ b/accountsdb/src/store/mmap.rs
@@ -0,0 +1,306 @@
+//! Mapped storage for persisted account images.
+//!
+//! The file reserves a small meta header at the front, followed by the raw
+//! account images written in `solana-account`'s borrowed layout.
+
+use std::{
+ fs::File,
+ io::{self, Write},
+ ops::Range,
+ os::fd::AsRawFd,
+ path::Path,
+ ptr::NonNull,
+ sync::atomic::{AtomicU32, AtomicU64, Ordering::*},
+};
+
+use memmap2::{MmapMut, MmapOptions};
+use nucleus::MB;
+use parking_lot::Mutex;
+use solana_account::{STORAGE_UNIT, StorageUnit};
+use tracing::{debug, error};
+
+use crate::{
+ AccountsDBError, Result, metrics,
+ store::{DatabaseVersion, VERSION, kv::Offset},
+};
+
+/// Bytes reserved at the front of the mapped file for metadata.
+const DATABASE_META_RESERVATION: usize = 256;
+/// Filename used for the mapped storage file.
+pub const STORAGE_FILE: &str = "storage.db";
+/// Growth block for the mapped storage file.
+#[cfg(any(test, feature = "testkit"))]
+pub(crate) const STORAGE_BLOCK: u64 = 16 * MB as u64;
+#[cfg(not(any(test, feature = "testkit")))]
+pub(crate) const STORAGE_BLOCK: u64 = 256 * MB as u64;
+/// Initial file size: one storage block plus the metadata reservation.
+const INIT_STORAGE_SIZE: u64 = STORAGE_BLOCK + DATABASE_META_RESERVATION as u64;
+/// Maximum mapped storage size.
+#[cfg(any(test, feature = "testkit"))]
+const MMAP_SIZE: usize = 64 * MB;
+#[cfg(not(any(test, feature = "testkit")))]
+const MMAP_SIZE: usize = u32::MAX as usize * STORAGE_UNIT + DATABASE_META_RESERVATION;
+
+/// One allocation inside the mapped storage.
+pub(crate) struct Allocation {
+ /// Offset from the start of the storage area, in storage units.
+ pub(crate) offset: Offset,
+ /// Pointer to the start of the allocated image.
+ pub(crate) ptr: NonNull,
+}
+
+/// Mapped storage backing persisted account images.
+pub(crate) struct MappedStorage {
+ /// Pointer to the reserved metadata header.
+ meta: NonNull,
+ /// Full file mapping.
+ mmap: MmapMut,
+ /// Start of the account image region.
+ head: NonNull,
+ /// File handle used for resizing.
+ file: Mutex,
+}
+
+#[repr(C)]
+#[derive(Default)]
+/// Runtime counters for the persisted backend.
+pub(crate) struct Stats {
+ /// Persisted image loads.
+ pub(crate) reads: AtomicU64,
+ /// `BorrowedAccount::commit` calls.
+ pub(crate) commits: AtomicU64,
+ /// Fresh allocations on backing storage.
+ pub(crate) allocs: AtomicU64,
+ /// Freelist allocation reuse.
+ pub(crate) reallocs: AtomicU64,
+ /// Account relocations during defrag.
+ pub(crate) compactions: AtomicU64,
+ /// Persisted deletes.
+ pub(crate) removals: AtomicU64,
+ /// File resizes.
+ pub(crate) resizes: AtomicU64,
+}
+
+impl Stats {
+ /// Counts one persisted read.
+ pub(crate) fn read(&self) {
+ self.reads.fetch_add(1, Relaxed);
+ metrics::read();
+ }
+
+ /// Counts one borrowed account commit.
+ pub(crate) fn commit(&self) {
+ self.commits.fetch_add(1, Relaxed);
+ metrics::commit();
+ }
+
+ /// Counts one fresh allocation from the mapped file.
+ pub(crate) fn alloc(&self) {
+ self.allocs.fetch_add(1, Relaxed);
+ metrics::alloc();
+ }
+
+ /// Counts one freelist reuse.
+ pub(crate) fn realloc(&self) {
+ self.reallocs.fetch_add(1, Relaxed);
+ metrics::realloc();
+ }
+
+ /// Counts relocations during defragmentation.
+ pub(crate) fn compact(&self, count: usize) {
+ let count = count as u64;
+ self.compactions.fetch_add(count, Relaxed);
+ metrics::compaction(count);
+ }
+
+ /// Counts one persisted removal.
+ pub(crate) fn remove(&self) {
+ self.removals.fetch_add(1, Relaxed);
+ metrics::removal();
+ }
+
+ /// Counts one file resize.
+ pub(crate) fn resize(&self) {
+ self.resizes.fetch_add(1, Relaxed);
+ metrics::resize();
+ }
+}
+
+#[repr(C)]
+#[derive(Default)]
+/// Metadata header stored at the front of the mapped file.
+pub(crate) struct DatabaseMeta {
+ /// On-disk format version.
+ version: DatabaseVersion,
+ /// Last computed database checksum.
+ pub(crate) checksum: AtomicU64,
+ /// Current slot.
+ pub(crate) slot: AtomicU64,
+ /// Id of the last sealed superblock; folded into the checksum fingerprint.
+ pub(crate) superblock: AtomicU64,
+ /// Transactions whose account-state commit completed successfully.
+ pub(crate) transactions: AtomicU64,
+ /// Current backing file length in bytes.
+ len: AtomicU64,
+ /// Database statistics.
+ stats: Stats,
+ /// Next allocation cursor.
+ pub(super) cursor: AtomicU32,
+}
+
+impl MappedStorage {
+ /// Opens or creates the mapped storage file.
+ pub(crate) fn new(path: &Path) -> Result {
+ let path = path.join(STORAGE_FILE);
+ let mut file =
+ File::options().create(true).truncate(false).read(true).write(true).open(path)?;
+ let fd = file.as_raw_fd();
+ // SAFETY: the file is opened read/write and mapped for the full fixed size.
+ let mut mmap = unsafe { MmapOptions::new().len(MMAP_SIZE).map_mut(fd)? };
+ if file.metadata()?.len() == 0 {
+ file.set_len(INIT_STORAGE_SIZE)?;
+ file.flush()?;
+ let meta = DatabaseMeta {
+ version: VERSION,
+ len: INIT_STORAGE_SIZE.into(),
+ slot: 1.into(),
+ ..Default::default()
+ };
+ // SAFETY: the first bytes of the mapping are reserved for `DatabaseMeta`.
+ unsafe { mmap.as_mut_ptr().cast::().write(meta) };
+ mmap.flush()?;
+ }
+ // SAFETY: the mapping is at least `DATABASE_META_RESERVATION` bytes long,
+ // so the meta header and account head pointers stay within the map.
+ let (meta, head) = unsafe {
+ let head = mmap.as_mut_ptr().add(DATABASE_META_RESERVATION);
+ let head = NonNull::new_unchecked(head.cast());
+ let meta = NonNull::new_unchecked(mmap.as_mut_ptr().cast());
+ (meta, head)
+ };
+ let file = Mutex::new(file);
+ Ok(Self { meta, mmap, head, file })
+ }
+
+ /// Flushes dirty pages to durable storage.
+ pub(crate) fn flush(&self, sync: bool) -> io::Result<()> {
+ let range = self.active();
+ if sync {
+ self.mmap.flush_range(range.start, range.len())
+ } else {
+ self.mmap.flush_async_range(range.start, range.len())
+ }
+ }
+
+ /// Validates the opened storage format.
+ pub(crate) fn validate(&self) -> Result<()> {
+ let meta = self.meta();
+ if meta.version != VERSION {
+ Err(AccountsDBError::UnsupportedVersion(meta.version))
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Returns a pointer inside the account image region.
+ pub(crate) fn at(&self, offset: Offset) -> NonNull {
+ // SAFETY: private call sites pass offsets from the index, allocator, or
+ // defrag cursor and uphold the mapped-region bounds.
+ unsafe { self.head.add(offset.0 as usize) }
+ }
+
+ /// Returns the runtime counters.
+ pub(crate) fn stats(&self) -> &Stats {
+ &self.meta().stats
+ }
+
+ /// Allocates a fresh span of `units` storage units.
+ pub(crate) fn allocate(&self, units: u32) -> Result {
+ let meta = self.meta();
+ let mut offset = meta.cursor.load(Acquire);
+ loop {
+ let end = offset.checked_add(units).ok_or(AccountsDBError::Allocation)?;
+ let needed = Self::bytes(end as u64);
+ if needed > meta.len.load(Acquire) {
+ self.grow(needed)?;
+ }
+ if let Err(updated) = meta.cursor.compare_exchange(offset, end, AcqRel, Acquire) {
+ offset = updated;
+ } else {
+ break;
+ }
+ }
+ self.stats().alloc();
+ let offset = Offset(offset);
+ let ptr = self.at(offset);
+ Ok(Allocation { offset, ptr })
+ }
+
+ /// Returns a shared reference to the metadata header.
+ pub(crate) fn meta(&self) -> &DatabaseMeta {
+ // SAFETY: `meta` points to the reserved header at the front of the map.
+ unsafe { &*self.meta.as_ptr() }
+ }
+
+ /// Returns the current allocation cursor in storage units.
+ pub(crate) fn cursor(&self) -> u32 {
+ self.meta().cursor.load(Acquire)
+ }
+
+ /// Shrinks the file to the current cursor.
+ pub(super) fn shrink(&self, units: u32) -> io::Result<()> {
+ self.resize(Self::bytes(units as u64), u64::le)?;
+ self.meta().cursor.store(units, Release);
+ Ok(())
+ }
+
+ /// Returns the active byte range, including the metadata reservation.
+ fn active(&self) -> Range {
+ 0..Self::bytes(self.cursor() as u64) as usize
+ }
+
+ /// Converts storage units into file bytes, including the metadata reservation.
+ fn bytes(units: u64) -> u64 {
+ units * STORAGE_UNIT as u64 + DATABASE_META_RESERVATION as u64
+ }
+
+ /// Grows the file to at least `len` bytes.
+ /// Rounds up to a storage block before resizing.
+ fn grow(&self, mut len: u64) -> Result<()> {
+ len = len.div_ceil(STORAGE_BLOCK) * STORAGE_BLOCK;
+ if len > MMAP_SIZE as u64 {
+ error!(
+ requested = len,
+ limit = MMAP_SIZE,
+ "mapped storage limit exceeded"
+ );
+ return Err(AccountsDBError::Allocation);
+ }
+ self.resize(len, u64::ge).map_err(Into::into)
+ }
+
+ /// Resizes the file when the current size does not satisfy `cmp`.
+ ///
+ /// The file is updated before the new size is published into metadata so
+ /// readers never observe a larger size than the actual mapping.
+ fn resize(&self, len: u64, cmp: fn(&u64, &u64) -> bool) -> io::Result<()> {
+ let mut file = self.file.lock();
+ if cmp(&file.metadata()?.len(), &len) {
+ return Ok(());
+ }
+ // Resize the file first, then publish the new size into metadata.
+ file.set_len(len)?;
+ file.flush()?;
+ self.meta().len.store(len, Release);
+ self.stats().resize();
+ self.mmap.flush()?;
+ debug!(len, "resized storage file");
+ Ok(())
+ }
+}
+
+// SAFETY: the `NonNull` pointers point into the owned `mmap` and are never
+// reseated; concurrent access is synchronized through atomics in the metadata
+// header and the `Mutex`, so the storage is safe to send and share.
+unsafe impl Send for MappedStorage {}
+unsafe impl Sync for MappedStorage {}
diff --git a/accountsdb/src/store/mod.rs b/accountsdb/src/store/mod.rs
new file mode 100644
index 00000000..9214362c
--- /dev/null
+++ b/accountsdb/src/store/mod.rs
@@ -0,0 +1,303 @@
+//! Persisted account load and write path.
+//!
+//! This module coordinates the mmap, LMDB index, and borrowed account layout.
+
+use core::{hash::Hasher, slice};
+use std::sync::atomic::Ordering::{Acquire, Release};
+
+use solana_account::{
+ AccountMode, AccountSharedData, BorrowedAccount, CoWAccount::*, DirtyMarkers, OwnedAccount,
+};
+use solana_pubkey::Pubkey;
+use tracing::{error, warn};
+
+use nucleus::heed::{DatabaseIndex, OptRoTxn, OptRwTxn, read_txn, write_txn};
+use twox_hash::XxHash3_64;
+
+use crate::{
+ AccountEntry, AccountsDBError, Result, StoreKind,
+ metrics::{self, Operation},
+ store::{
+ index::{Index, OwnerIter},
+ kv::{Offset, OwnerAndOffset},
+ mmap::{DatabaseMeta, MappedStorage},
+ },
+};
+
+mod defrag;
+pub(crate) mod index;
+mod kv;
+pub(crate) mod mmap;
+
+#[cfg(test)]
+pub(crate) use defrag::MIN_REMAINDER;
+pub(crate) use mmap::Stats;
+
+/// Current on-disk storage format version.
+pub(crate) const VERSION: DatabaseVersion = 1;
+/// Version tag stored in the metadata header.
+pub(crate) type DatabaseVersion = u64;
+
+/// Persisted store backed by the mmap and LMDB index.
+pub(crate) struct PersistedStore {
+ /// Mapped account storage.
+ pub(crate) storage: MappedStorage,
+ /// LMDB index over persisted accounts.
+ pub(crate) index: Index,
+}
+
+/// Iterator over persisted program-owned accounts.
+pub(crate) struct PersistedProgramIter<'a> {
+ /// Keeps the read transaction alive while iterating.
+ iter: OwnerIter<'a>,
+ /// Mapped storage backing the returned borrowed accounts.
+ mmap: &'a MappedStorage,
+}
+
+impl PersistedStore {
+ /// Opens or creates the persisted store at `path`.
+ pub(crate) fn new(path: &std::path::Path) -> Result {
+ let index = Index::new(path)?;
+ let storage = MappedStorage::new(path)?;
+ Ok(Self { storage, index })
+ }
+
+ /// Loads the persisted image for `pubkey` from the mapped file.
+ pub(crate) fn load<'e>(
+ &'e self,
+ txn: OptRoTxn<'_, 'e>,
+ pubkey: &Pubkey,
+ ) -> Result> {
+ let txn = read_txn(self.index.env(), txn)?;
+ let offset = self.index.offset(pubkey, txn)?;
+ offset.is_some().then(|| self.storage.stats().read());
+ // SAFETY: offsets come from the persisted index and point into the map.
+ Ok(offset.map(|o| unsafe { BorrowedAccount::init(self.storage.at(o)) }))
+ }
+
+ /// Returns whether a persisted account image exists for `pubkey`.
+ pub(crate) fn contains<'e>(&'e self, txn: OptRoTxn<'_, 'e>, pubkey: &Pubkey) -> Result {
+ let txn = read_txn(self.index.env(), txn)?;
+ self.index.offset(pubkey, txn).map(|o| o.is_some()).map_err(Into::into)
+ }
+
+ /// Applies a batch of account updates to the persisted store.
+ ///
+ /// Borrowed accounts in authoritative modes are committed in place. Owned
+ /// accounts in those modes are serialized into the mmap. Other modes delete
+ /// stale persisted entries. If the LMDB commit fails or database runs out of
+ /// space, the borrowed images are rolled back so in-memory state stays
+ /// aligned with the durable index.
+ pub(crate) fn upsert<'a, AC>(&self, accounts: AC) -> Result<()>
+ where
+ AC: IntoIterator- + Clone,
+ {
+ let mut applied = 0;
+ let mut result = Ok(());
+ let mut txn = None;
+ for entry in accounts.clone() {
+ result = self.apply(entry, &mut txn);
+ if result.is_err() {
+ break;
+ }
+ applied += 1;
+ }
+ // Commit once after the batch so the index and mmap stay in sync.
+ if let Some(txn) = txn
+ && result.is_ok()
+ {
+ metrics::accounts(StoreKind::Persisted, self.index.accounts.len(&txn)?);
+ result = txn.commit().map_err(Into::into);
+ }
+ if let Err(error) = &result {
+ warn!(applied, ?error, "accounts persistence failed; rolling back");
+ // Only borrowed accounts need rollback here: owned inserts never
+ // mutate an existing borrowed image in place.
+ let processed = accounts.into_iter().take(applied).map(|(_, a)| a);
+ Self::rollback(processed);
+ }
+
+ result
+ }
+
+ /// Returns the persisted program iterator for `owner`.
+ pub(crate) fn program(&self, owner: Pubkey) -> Result
>> {
+ let i = self.index.program(owner)?;
+ Ok(i.map(|iter| PersistedProgramIter { iter, mmap: &self.storage }))
+ }
+
+ /// Flushes the mapped storage and LMDB index to durable storage.
+ pub(crate) fn flush(&self, sync: bool) -> heed::Result<()> {
+ let _timer = metrics::time(Operation::Flush);
+ self.index.flush()?;
+ if sync {
+ let checksum = self.checksum()?;
+ self.meta().checksum.store(checksum, Release);
+ }
+ self.storage.flush(sync)?;
+ Ok(())
+ }
+
+ /// Validates the persisted store checksum and on-disk format version.
+ pub(crate) fn validate(&self) -> Result<()> {
+ self.storage.validate()?;
+ if self.storage.cursor() == 0 {
+ return Ok(());
+ }
+ let expected = self.meta().checksum.load(Acquire);
+ let actual = self.checksum()?;
+ if expected != actual {
+ error!(expected, actual, "state checksum mismatch");
+ return Err(AccountsDBError::Corruption);
+ }
+
+ Ok(())
+ }
+
+ /// Applies one account state transition to the persisted backend.
+ fn apply<'e>(&'e self, acc: &AccountEntry, txn: OptRwTxn<'_, 'e>) -> Result<()> {
+ let (pubkey, account) = acc;
+ // An account that has moved to a non-authoritative mode, or has been
+ // closed, no longer belongs here, so drop any stale persisted entry.
+ if !account.mode().authoritative() || account.is(AccountMode::Closed) {
+ self.delete(pubkey, txn)?;
+ if let Borrowed(acc) = account.cow() {
+ acc.commit();
+ }
+ return Ok(());
+ }
+
+ let markers = account.markers();
+ match account.cow() {
+ Borrowed(acc) => self.update(pubkey, acc, markers, txn),
+ Owned(acc) => self.insert(pubkey, acc, txn),
+ }
+ }
+
+ /// Rolls back borrowed accounts that were already touched in the batch.
+ fn rollback<'a, AC>(accounts: AC)
+ where
+ AC: Iterator- ,
+ {
+ for acc in accounts {
+ if !acc.dirty() {
+ continue;
+ }
+ let Borrowed(acc) = acc.cow() else { continue };
+ // SAFETY: only borrowed accounts were updated before the failed commit.
+ unsafe { acc.rollback() };
+ }
+ }
+
+ /// Commits a borrowed image after updating its owner mapping if needed.
+ fn update<'e>(
+ &'e self,
+ pubkey: &Pubkey,
+ acc: &BorrowedAccount,
+ markers: &DirtyMarkers,
+ txn: OptRwTxn<'_, 'e>,
+ ) -> Result<()> {
+ if markers.contains(DirtyMarkers::OWNER) {
+ let txn = write_txn(self.index.env(), txn)?;
+ let owner = acc.owner().into();
+ self.index.update_owner(pubkey, owner, txn)?;
+ }
+ if !markers.intersects(DirtyMarkers::all()) {
+ return Ok(());
+ }
+ acc.commit();
+ self.storage.stats().commit();
+ Ok(())
+ }
+
+ /// Serializes an owned image into mapped storage and records its offset.
+ fn insert<'e>(
+ &'e self,
+ pubkey: &Pubkey,
+ acc: &OwnedAccount,
+ txn: OptRwTxn<'_, 'e>,
+ ) -> Result<()> {
+ let txn = write_txn(self.index.env(), txn)?;
+ let units = acc.units();
+ let owner = acc.owner().into();
+
+ let (ptr, offset) = if let Some(offset) = self.index.allocate(units, txn)? {
+ let ptr = self.storage.at(offset);
+ self.storage.stats().realloc();
+ (ptr, offset)
+ } else {
+ let alloc = self.storage.allocate(units)?;
+ (alloc.ptr, alloc.offset)
+ };
+ let data = OwnerAndOffset { owner, offset };
+ if let Some(offset) = self.index.delete(pubkey, txn)? {
+ self.free(offset, txn)?;
+ }
+ self.index.insert(pubkey, data, txn)?;
+ // SAFETY: `ptr` points at a fresh span inside the mapped storage and
+ // `units` is the exact serialized size of this owned account.
+ unsafe {
+ let buffer = slice::from_raw_parts_mut(ptr.as_ptr(), units as usize);
+ acc.serialize(buffer, pubkey);
+ };
+ Ok(())
+ }
+
+ /// Returns one persisted span to the freelist.
+ fn free(&self, offset: Offset, txn: &mut heed::RwTxn<'_>) -> Result<()> {
+ // SAFETY: `offset` was returned by the index and still points at a valid image.
+ let space = unsafe { BorrowedAccount::span(self.storage.at(offset)) };
+ self.index.freelist.put(txn, &space, &offset)?;
+ Ok(())
+ }
+
+ /// Removes a persisted image and returns its storage span to the freelist.
+ fn delete<'e>(&'e self, pubkey: &Pubkey, txn: OptRwTxn<'_, 'e>) -> Result<()> {
+ let txn = write_txn(self.index.env(), txn)?;
+ let Some(offset) = self.index.delete(pubkey, txn)? else {
+ return Ok(());
+ };
+ self.free(offset, txn)?;
+ self.storage.stats().remove();
+ Ok(())
+ }
+
+ /// Returns the persisted storage metadata header.
+ pub(crate) fn meta(&self) -> &DatabaseMeta {
+ self.storage.meta()
+ }
+
+ /// Computes a deterministic checksum over persisted accounts in pubkey order.
+ fn checksum(&self) -> heed::Result
{
+ let _timer = metrics::time(Operation::Checksum);
+ let mut hasher = XxHash3_64::new();
+ let mut iter = self.index.accounts()?;
+ hasher.write(&self.meta().slot.load(Acquire).to_le_bytes());
+ hasher.write(&self.meta().superblock.load(Acquire).to_le_bytes());
+ for entry in &mut iter.inner {
+ let (pubkey, data) = entry?;
+ hasher.write(pubkey.as_array());
+ // SAFETY: offsets come from the persisted accounts index and point
+ // into the mapped storage for this store.
+ let account = unsafe { BorrowedAccount::init(self.storage.at(data.offset)) };
+ hasher.write(account.storage());
+ }
+ Ok(hasher.finish())
+ }
+}
+
+impl<'a> Iterator for PersistedProgramIter<'a> {
+ type Item = AccountEntry;
+
+ fn next(&mut self) -> Option {
+ let (_, offset) = self.iter.inner.next()?.ok()?;
+ let ptr = self.mmap.at(offset);
+ // The image prefix stores the full pubkey, so iteration can recover it
+ // without consulting LMDB again.
+ // SAFETY: the iterator yields offsets stored in the same mapped database.
+ self.mmap.stats().read();
+ let pubkey = unsafe { BorrowedAccount::pubkey(ptr) };
+ let account = unsafe { BorrowedAccount::init(ptr).into() };
+ Some((pubkey, account))
+ }
+}
diff --git a/accountsdb/src/tests.rs b/accountsdb/src/tests.rs
new file mode 100644
index 00000000..ac2ff604
--- /dev/null
+++ b/accountsdb/src/tests.rs
@@ -0,0 +1,764 @@
+//! Integration-style unit tests for the two-backend account store.
+//!
+//! Each test drives a realistic multi-step flow through the public `AccountsDB`
+//! surface and reaches into `pub(crate)` internals only to assert *which*
+//! backend a given account landed in — the crate's central persisted/volatile
+//! invariant that no public method exposes directly.
+
+use std::sync::atomic::Ordering::{Relaxed, Release};
+
+use assert_matches::assert_matches;
+use nucleus::{
+ heed::{DatabaseIndex, read_txn},
+ testkit::{TempDir, init_tracing, tempdir},
+};
+use solana_account::{
+ AccountBuilder, AccountMode, AccountSharedData, ReadableAccount, WritableAccount,
+};
+use solana_pubkey::Pubkey;
+
+use super::*;
+use crate::{snapshot::VOLATILE_DB_FILE, store::MIN_REMAINDER};
+
+/// Fresh database on a throwaway directory; the `TempDir` must outlive the db.
+fn db() -> (TempDir, AccountsDB) {
+ init_tracing();
+ let dir = tempdir();
+ let db = AccountsDB::new(dir.path()).unwrap();
+ (dir, db)
+}
+
+/// Owned mutable (persisted) account carrying `data`; its size follows the data.
+fn mutable_data(lamports: u64, data: Vec, owner: &Pubkey) -> AccountSharedData {
+ let mut a = AccountSharedData::new(lamports, data.len(), owner);
+ a.set_data_from_slice(&data);
+ a.set_mode(AccountMode::Delegated).unwrap();
+ a
+}
+
+/// Empty mutable (persisted) account; `owner` defaults to the system program.
+fn delegated(lamports: u64) -> AccountSharedData {
+ AccountBuilder::default()
+ .lamports(lamports)
+ .mode(AccountMode::Delegated)
+ .build()
+}
+
+/// Stores one account, the shape every single-account write below takes.
+fn store(db: &AccountsDB, pubkey: Pubkey, account: AccountSharedData) {
+ db.store(&[(pubkey, account)]).unwrap();
+}
+
+/// Whether a persisted image exists for `pubkey`.
+fn in_persisted(db: &AccountsDB, pubkey: &Pubkey) -> bool {
+ let mut txn = None;
+ db.persisted.contains(&mut txn, pubkey).unwrap()
+}
+
+/// Whether a volatile entry exists for `pubkey`.
+fn in_volatile(db: &AccountsDB, pubkey: &Pubkey) -> bool {
+ db.volatile.contains(pubkey)
+}
+
+/// Pubkeys `owner` owns, in iteration order (persisted first, then volatile).
+fn program(db: &AccountsDB, owner: &Pubkey) -> Vec {
+ db.program(owner).unwrap().map(|(k, _)| k).collect()
+}
+
+/// Balance of the account currently loaded for `pubkey`.
+fn lamports(db: &AccountsDB, pubkey: &Pubkey) -> u64 {
+ db.loader().load(pubkey).unwrap().unwrap().lamports()
+}
+
+/// Loads the account currently stored for `pubkey`.
+///
+/// A persisted account comes back as a *borrowed* image and a volatile one as
+/// *owned*; storing the loaded value back is how the engine drives mode changes
+/// through the routing layer (a freshly built owned account with a
+/// non-authoritative mode is filtered out of the persisted backend entirely).
+fn reload(db: &AccountsDB, pubkey: &Pubkey) -> AccountSharedData {
+ db.loader().load(pubkey).unwrap().unwrap()
+}
+
+/// Closes `pubkey`, deleting it from whichever backend currently holds it.
+///
+/// Goes through the load→mutate→store path so the account is a *borrowed* image
+/// the routing layer will actually evict (see [`reload`]).
+fn close(db: &AccountsDB, pubkey: &Pubkey) {
+ let mut acc = reload(db, pubkey);
+ if acc.is(AccountMode::Delegated) {
+ acc.set_mode(AccountMode::Transient).unwrap();
+ }
+ if acc.is(AccountMode::Transient) {
+ acc.set_mode(AccountMode::ReadOnly).unwrap();
+ }
+ acc.set_mode(AccountMode::Closed).unwrap();
+ store(db, *pubkey, acc);
+}
+
+/// Allocation high-water mark of the persisted store, in storage units.
+fn cursor(db: &AccountsDB) -> u32 {
+ db.persisted.storage.cursor()
+}
+
+/// Current persisted offset for one live account.
+fn offset(db: &AccountsDB, pubkey: &Pubkey) -> impl Copy + PartialEq + use<> {
+ let mut txn = None;
+ let txn = read_txn(db.persisted.index.env(), &mut txn).unwrap();
+ db.persisted.index.offset(pubkey, txn).unwrap().unwrap()
+}
+
+/// Defragments until a pass makes no change and returns the reclaimed total.
+fn defrag_to_stable(db: &AccountsDB) -> u32 {
+ let mut total = 0;
+ loop {
+ // SAFETY: the test is the sole owner of the store during defrag.
+ let pass = unsafe { db.persisted.defragment() }.unwrap();
+ total += pass.reclaimed;
+ if !pass.changed() {
+ return total;
+ }
+ }
+}
+
+/// Builds a mutable account with an exact persisted span.
+fn mutable_units(lamports: u64, units: u32, owner: &Pubkey) -> AccountSharedData {
+ let account = mutable_at_least(lamports, units, owner);
+ assert_eq!(account.owned().units(), units);
+ account
+}
+
+/// Builds the smallest mutable account spanning at least `units` storage units.
+fn mutable_at_least(lamports: u64, units: u32, owner: &Pubkey) -> AccountSharedData {
+ (0..=units as usize * solana_account::STORAGE_UNIT)
+ .map(|len| mutable_data(lamports, vec![0; len], owner))
+ .find(|account| account.owned().units() >= units)
+ .unwrap()
+}
+
+// Routing, both eviction directions, owner remap and Closed/reset handling in
+// one flow — the persisted-vs-volatile invariant is what this whole crate
+// exists to enforce.
+#[test]
+fn test_routing_and_persistence_flips() {
+ let (_dir, db) = db();
+ let (p, q) = (Pubkey::new_unique(), Pubkey::new_unique());
+ let (a, b) = (Pubkey::new_unique(), Pubkey::new_unique());
+
+ let aacc = AccountBuilder::default().lamports(10).owner(p).mode(AccountMode::Delegated);
+ let bacc = AccountBuilder::default().lamports(20).owner(p);
+ // `a` is authoritative, `b` is non-authoritative; both are owned by `p`.
+ db.store(&[(a, aacc.build()), (b, bacc.build())]).unwrap();
+ assert!(in_persisted(&db, &a) && !in_volatile(&db, &a));
+ assert!(in_volatile(&db, &b) && !in_persisted(&db, &b));
+
+ // Loader reads across both backends; contains agrees.
+ let loader = db.loader();
+ assert_eq!(loader.load(&a).unwrap().unwrap().lamports(), 10);
+ assert_eq!(loader.load(&b).unwrap().unwrap().lamports(), 20);
+ assert!(loader.contains(&a).unwrap() && loader.contains(&b).unwrap());
+ assert!(!loader.contains(&Pubkey::new_unique()).unwrap());
+ drop(loader);
+
+ // Persisted account is yielded before the volatile one.
+ assert_eq!(program(&db, &p), vec![a, b]);
+
+ // Loading a persisted account returns a borrowed image; mutating its owner
+ // and re-storing must commit in place and remap the program index.
+ let mut borrowed = reload(&db, &a);
+ borrowed.set_owner(q);
+ store(&db, a, borrowed);
+ assert_eq!(program(&db, &p), vec![b]); // `a` left p's set
+ assert_eq!(program(&db, &q), vec![a]); // and joined q's
+
+ // Transient is immutable to programs but remains persistent while its
+ // lifecycle state is unresolved.
+ let mut flip = reload(&db, &a);
+ flip.set_mode(AccountMode::Transient).unwrap();
+ flip.set_lamports(30);
+ store(&db, a, flip);
+ let transient = reload(&db, &a);
+ assert!(!transient.mutable());
+ assert!(in_persisted(&db, &a) && !in_volatile(&db, &a));
+ assert_eq!(lamports(&db, &a), 30);
+
+ // Resolving to ReadOnly evicts the persisted copy into volatile.
+ let mut flip = reload(&db, &a);
+ flip.set_mode(AccountMode::ReadOnly).unwrap();
+ store(&db, a, flip);
+ assert!(!in_persisted(&db, &a) && in_volatile(&db, &a));
+ assert_eq!(lamports(&db, &a), 30);
+
+ // ReadOnly → Delegated evicts it back into persisted.
+ let mut flip = reload(&db, &a);
+ flip.set_mode(AccountMode::Delegated).unwrap();
+ store(&db, a, flip);
+ assert!(in_persisted(&db, &a) && !in_volatile(&db, &a));
+
+ // Closing removes it from both backends.
+ close(&db, &a);
+ assert!(!in_persisted(&db, &a) && !in_volatile(&db, &a));
+
+ // reset() drops volatile mirror only; persisted state is authoritative.
+ let c = Pubkey::new_unique();
+ store(&db, c, mutable_data(50, vec![], &p));
+ db.reset();
+ assert!(!in_volatile(&db, &b));
+ assert!(in_persisted(&db, &c));
+}
+
+// Both migration directions remove the source image and owner mapping, retain
+// the account contents across reopen, and recycle persisted storage.
+#[test]
+fn test_store_kind_migration_invariants() {
+ let dir = tempdir();
+ let (persisted_owner, volatile_owner, reuse_owner) = (
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ );
+ let (key, reuse) = (Pubkey::new_unique(), Pubkey::new_unique());
+ let data = vec![1, 2, 3, 4];
+ let assert_migrated = |db: &AccountsDB, persisted: bool, owner: Pubkey| {
+ assert_eq!(in_persisted(db, &key), persisted);
+ assert_eq!(in_volatile(db, &key), !persisted);
+ assert_eq!(program(db, &owner), vec![key]);
+ let account = reload(db, &key);
+ assert_eq!(account.owner(), &owner);
+ assert_eq!(account.lamports(), 20);
+ assert_eq!(account.data(), data);
+ };
+
+ {
+ let db = AccountsDB::new(dir.path()).unwrap();
+ store(&db, key, mutable_data(10, data.clone(), &persisted_owner));
+ let base = cursor(&db);
+
+ let mut account = reload(&db, &key);
+ account.set_mode(AccountMode::Transient).unwrap();
+ account.set_mode(AccountMode::ReadOnly).unwrap();
+ account.set_owner(volatile_owner);
+ account.set_lamports(20);
+ store(&db, key, account);
+
+ assert_migrated(&db, false, volatile_owner);
+ assert!(program(&db, &persisted_owner).is_empty());
+
+ // A same-sized persisted account must reuse the span released by the
+ // migration instead of extending the mmap.
+ store(&db, reuse, mutable_data(30, data.clone(), &reuse_owner));
+ assert_eq!(cursor(&db), base);
+
+ db.dump(None).unwrap();
+ }
+
+ {
+ let db = AccountsDB::new(dir.path()).unwrap();
+ assert_migrated(&db, false, volatile_owner);
+ assert!(program(&db, &persisted_owner).is_empty());
+
+ let mut account = reload(&db, &key);
+ account.set_mode(AccountMode::Delegated).unwrap();
+ account.set_owner(persisted_owner);
+ store(&db, key, account);
+
+ assert_migrated(&db, true, persisted_owner);
+ assert!(program(&db, &volatile_owner).is_empty());
+
+ db.flush(true).unwrap();
+ // Persist a stale volatile copy if cleanup regresses, so the final open
+ // can verify source-store cleanup rather than merely losing memory state.
+ db.dump(None).unwrap();
+ }
+
+ let db = AccountsDB::new(dir.path()).unwrap();
+ assert_migrated(&db, true, persisted_owner);
+ assert!(program(&db, &volatile_owner).is_empty());
+ assert_eq!(program(&db, &reuse_owner), vec![reuse]);
+}
+
+// Persisted state and metadata survive a close/reopen, and validate() accepts
+// the synced checksum.
+#[test]
+fn test_persistence_reopen_and_validate() {
+ let dir = tempdir();
+ let keys: Vec = (0..8).map(|_| Pubkey::new_unique()).collect();
+
+ let (checksum, before) = {
+ let db = AccountsDB::new(dir.path()).unwrap();
+ for (i, k) in keys.iter().enumerate() {
+ store(&db, *k, delegated(100 + i as u64));
+ }
+ let discarded = Pubkey::new_unique();
+ store(&db, discarded, delegated(0));
+ close(&db, &discarded);
+ db.set_slot(42).unwrap();
+ // Sync the checksum into the header so a reopen can validate against it.
+ db.persisted.flush(true).unwrap();
+ assert!(db.validate().is_ok());
+ (db.checksum(), cursor(&db))
+ };
+
+ let mut db = AccountsDB::new(dir.path()).unwrap();
+ assert!(db.validate().is_ok());
+ let reclaimed = db.compact().unwrap();
+ assert_eq!(reclaimed, before - cursor(&db));
+ assert!(reclaimed > 0);
+ for (i, k) in keys.iter().enumerate() {
+ assert_eq!(lamports(&db, k), 100 + i as u64);
+ }
+ assert_eq!(db.slot(), 42);
+ assert_eq!(db.checksum(), checksum);
+ assert!(db.validate().is_ok());
+}
+
+// A clean-shutdown dump lives in the active tree, is restored on the next open,
+// and is then removed so volatile state returns to its in-memory-only form.
+#[test]
+fn test_dump_restores_volatile_on_reopen() {
+ let dir = tempdir();
+ let key = Pubkey::new_unique();
+ let active = AccountsDB::directory(dir.path());
+ let dump = active.join(VOLATILE_DB_FILE);
+
+ {
+ let db = AccountsDB::new(dir.path()).unwrap();
+ let account = AccountBuilder::default().lamports(42).mode(AccountMode::ReadOnly).build();
+ store(&db, key, account);
+ db.dump(None).unwrap();
+ assert!(dump.exists(), "dump is written into the active tree");
+ }
+
+ let db = AccountsDB::new(dir.path()).unwrap();
+ assert_eq!(lamports(&db, &key), 42);
+ assert!(in_volatile(&db, &key));
+ assert!(!dump.exists(), "restored dump is consumed on open");
+}
+
+// A freed span is reused for a same-sized insert instead of growing the file;
+// genuinely new accounts still extend it.
+#[test]
+fn test_freelist_reuse_and_growth() {
+ let (_dir, db) = db();
+ let stats = || {
+ let s = db.persisted.storage.stats();
+ (s.allocs.load(Relaxed), s.reallocs.load(Relaxed))
+ };
+
+ let k1 = Pubkey::new_unique();
+ store(&db, k1, delegated(1));
+ let base = cursor(&db);
+ let (_, reallocs) = stats();
+
+ // Close k1 (returns its span to the freelist), then insert a same-sized
+ // account: it should land in the freed span without advancing the cursor.
+ close(&db, &k1);
+ store(&db, Pubkey::new_unique(), delegated(2));
+ assert_eq!(cursor(&db), base);
+ assert_eq!(stats().1, reallocs + 1);
+
+ // Fresh accounts have no reusable span, so the file grows.
+ let (allocs, _) = stats();
+ for _ in 0..8 {
+ store(&db, Pubkey::new_unique(), delegated(1));
+ }
+ assert!(cursor(&db) > base);
+ assert!(stats().0 > allocs);
+}
+
+// Defragmentation reclaims interior holes while preserving every live account's
+// content, ownership index, and checksum.
+#[test]
+fn test_defragment_preserves_live_accounts() {
+ let (_dir, db) = db();
+ let owner = Pubkey::new_unique();
+ let keys: Vec = (0..16).map(|_| Pubkey::new_unique()).collect();
+ for (i, k) in keys.iter().enumerate() {
+ store(&db, *k, mutable_data(100 + i as u64, vec![], &owner));
+ }
+
+ // Punch alternating holes; keep the survivors for later comparison.
+ let mut live = Vec::new();
+ for (i, k) in keys.iter().enumerate() {
+ if i % 2 == 0 {
+ close(&db, k);
+ } else {
+ live.push((*k, 100 + i as u64));
+ }
+ }
+ db.persisted.flush(true).unwrap();
+ let checksum = db.checksum();
+ let before = cursor(&db);
+
+ let reclaimed = defrag_to_stable(&db);
+ assert_eq!(reclaimed, before - cursor(&db));
+ assert!(cursor(&db) < before);
+
+ // Every survivor still loads unchanged and remains program-indexed.
+ for (k, lam) in &live {
+ let acc = db.loader().load(k).unwrap().unwrap();
+ assert_eq!(acc.lamports(), *lam);
+ assert_eq!(acc.owner(), &owner);
+ }
+ let mut owned = program(&db, &owner);
+ owned.sort();
+ let mut expected: Vec = live.iter().map(|(k, _)| *k).collect();
+ expected.sort();
+ assert_eq!(owned, expected);
+
+ // Relocating images must not change the content checksum.
+ db.persisted.flush(true).unwrap();
+ assert_eq!(db.checksum(), checksum);
+}
+
+/// Exact and thresholded best-fit packing updates component holes correctly,
+/// while deferred source holes become usable only by a later committed pass.
+#[test]
+fn test_defragment_best_fit_and_deferred_holes() {
+ let owner = Pubkey::new_unique();
+ let small = mutable_units(1, 21, &owner);
+ let small_units = small.owned().units();
+ let medium = mutable_at_least(2, MIN_REMAINDER, &owner);
+ let medium_units = medium.owned().units();
+
+ // Two adjacent component holes form one run. The small tail account leaves
+ // a useful remainder, which the following account consumes exactly.
+ {
+ let (_dir, db) = db();
+ let (small_hole, medium_hole, anchor, medium_key, small_key) = (
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ );
+ store(&db, small_hole, small.clone());
+ store(&db, medium_hole, medium.clone());
+ store(&db, anchor, small.clone());
+ store(&db, medium_key, medium.clone());
+ store(&db, small_key, small.clone());
+ let medium_dst = offset(&db, &medium_hole);
+ let small_dst = offset(&db, &small_hole);
+ close(&db, &small_hole);
+ close(&db, &medium_hole);
+
+ let pass = unsafe { db.persisted.defragment() }.unwrap();
+ assert_eq!(pass.moved, 2);
+ assert_eq!(pass.reclaimed, small_units + medium_units);
+ assert!(offset(&db, &medium_key) == medium_dst);
+ assert!(offset(&db, &small_key) == small_dst);
+ assert_eq!(lamports(&db, &medium_key), 2);
+ assert_eq!(lamports(&db, &small_key), 1);
+ assert_eq!(lamports(&db, &anchor), 1);
+
+ let before = cursor(&db);
+ store(&db, Pubkey::new_unique(), small.clone());
+ assert_eq!(cursor(&db), before + small_units);
+ }
+
+ // Exact fit wins first. The next account skips a hole whose remainder is
+ // just below the threshold. Two accounts instead use a wider hole and
+ // publish a useful suffix.
+ {
+ let (_dir, db) = db();
+ let short_remainder = (MIN_REMAINDER - 1) & !1;
+ let near_units = small_units + short_remainder;
+ let near = mutable_units(3, near_units, &owner);
+ let wide = mutable_units(4, 2 * small_units + medium_units, &owner);
+ let (
+ near_hole,
+ anchor_a,
+ wide_hole,
+ anchor_b,
+ exact_hole,
+ anchor_c,
+ wide_key_a,
+ wide_key_b,
+ exact_key,
+ ) = (
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ );
+ store(&db, near_hole, near);
+ store(&db, anchor_a, small.clone());
+ store(&db, wide_hole, wide);
+ store(&db, anchor_b, small.clone());
+ store(&db, exact_hole, small.clone());
+ store(&db, anchor_c, small.clone());
+ store(&db, wide_key_a, small.clone());
+ store(&db, wide_key_b, small.clone());
+ store(&db, exact_key, small.clone());
+ let near_dst = offset(&db, &near_hole);
+ let wide_dst = offset(&db, &wide_hole);
+ let exact_dst = offset(&db, &exact_hole);
+ close(&db, &near_hole);
+ close(&db, &wide_hole);
+ close(&db, &exact_hole);
+
+ let pass = unsafe { db.persisted.defragment() }.unwrap();
+ assert_eq!(pass.moved, 3);
+ assert_eq!(pass.reclaimed, 63);
+ assert!(offset(&db, &wide_key_b) == wide_dst);
+ assert!(offset(&db, &exact_key) == exact_dst);
+
+ let before = cursor(&db);
+ store(&db, Pubkey::new_unique(), medium.clone());
+ assert_eq!(cursor(&db), before);
+ let near_key = Pubkey::new_unique();
+ store(&db, near_key, mutable_units(5, near_units, &owner));
+ assert_eq!(cursor(&db), before);
+ assert!(offset(&db, &near_key) == near_dst);
+ }
+
+ // The first pass moves only the middle account. Its source joins the next
+ // hole after commit, and public startup compaction exhausts later passes.
+ {
+ let (_dir, mut db) = db();
+ let second = mutable_units(3, 2 * medium_units - small_units, &owner);
+ let (first_hole, middle, second_hole, tail) = (
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ );
+ store(&db, first_hole, small.clone());
+ store(&db, middle, small.clone());
+ store(&db, second_hole, second);
+ store(&db, tail, medium.clone());
+ let middle_dst = offset(&db, &first_hole);
+ let tail_dst = offset(&db, &middle);
+ close(&db, &first_hole);
+ close(&db, &second_hole);
+ let before = cursor(&db);
+
+ let pass = unsafe { db.persisted.defragment() }.unwrap();
+ assert_eq!((pass.moved, pass.reclaimed), (1, 0));
+ assert_eq!(cursor(&db), before);
+ assert!(offset(&db, &middle) == middle_dst);
+
+ assert_eq!(db.compact().unwrap(), 2 * medium_units);
+ assert!(offset(&db, &tail) == tail_dst);
+ assert_eq!(lamports(&db, &middle), 1);
+ assert_eq!(lamports(&db, &tail), 2);
+
+ let before = cursor(&db);
+ store(&db, Pubkey::new_unique(), small.clone());
+ assert_eq!(cursor(&db), before + small_units);
+ }
+}
+
+// A snapshot is a self-contained tree: reopening it restores persisted accounts
+// and bootstraps the volatile store from volatile.db, which is then consumed.
+#[test]
+fn test_snapshot_export_and_volatile_restore() {
+ let src = tempdir();
+ let (a, b) = (Pubkey::new_unique(), Pubkey::new_unique());
+
+ let snapshot = {
+ let db = AccountsDB::new(src.path()).unwrap();
+ let aacc = AccountBuilder::default().lamports(10).mode(AccountMode::Delegated);
+ let bacc = AccountBuilder::default().lamports(20).mode(AccountMode::ReadOnly);
+ db.store(&[(a, aacc.build()), (b, bacc.build())]).unwrap();
+ // SAFETY: the test holds exclusive access to the store.
+ unsafe { db.snapshot(1) }.unwrap()
+ };
+
+ // Adopt the snapshot as a new database's active tree.
+ let dst = tempdir();
+ let active = AccountsDB::directory(dst.path());
+ std::fs::rename(&snapshot, &active).unwrap();
+ let db = AccountsDB::new(dst.path()).unwrap();
+
+ assert_eq!(lamports(&db, &a), 10);
+ assert_eq!(lamports(&db, &b), 20);
+ assert!(in_persisted(&db, &a));
+ assert!(in_volatile(&db, &b));
+ // The volatile payload is single-sourced back into memory on open.
+ assert!(!active.join(VOLATILE_DB_FILE).exists());
+
+ // Backup renames the active tree out and back.
+ let saved = db.backup(BackupOp::Save).unwrap();
+ assert!(saved.exists() && !active.exists());
+ db.backup(BackupOp::Restore).unwrap();
+ assert!(active.exists());
+}
+
+// validate() flags a persisted checksum that no longer matches the images.
+#[test]
+fn test_corruption_detection() {
+ let (_dir, db) = db();
+ for _ in 0..4 {
+ store(&db, Pubkey::new_unique(), delegated(1));
+ }
+ db.persisted.flush(true).unwrap();
+ assert!(db.validate().is_ok());
+
+ // Corrupt the recorded checksum; recomputation must no longer agree.
+ db.persisted.meta().checksum.store(0xDEAD_BEEF, Release);
+ assert_matches!(db.validate(), Err(AccountsDBError::Corruption));
+}
+
+// Several freed spans of one size accumulate as duplicates under a single
+// freelist key and are all reissued before the file grows — the N>1 duplicate
+// case a broken DUP config silently loses.
+#[test]
+fn test_freelist_multi_duplicate_reuse() {
+ let (_dir, db) = db();
+ let reallocs = || db.persisted.storage.stats().reallocs.load(Relaxed);
+
+ const N: usize = 6;
+ let keys: Vec = (0..N).map(|_| Pubkey::new_unique()).collect();
+ for k in &keys {
+ store(&db, *k, delegated(1));
+ }
+ let base = cursor(&db);
+ // Close them all: N same-size spans return to the freelist as N duplicates.
+ for k in &keys {
+ close(&db, k);
+ }
+ let reused = reallocs();
+
+ // Each of N fresh same-size inserts must land in a freed span, so the cursor
+ // never advances and every insert is a reuse.
+ for _ in 0..N {
+ store(&db, Pubkey::new_unique(), delegated(2));
+ }
+ assert_eq!(cursor(&db), base);
+ assert_eq!(reallocs(), reused + N as u64);
+}
+
+// An immutable account changing owner is re-homed in the volatile program index
+// and the now-empty old owner set is pruned.
+#[test]
+fn test_volatile_owner_remap() {
+ let (_dir, db) = db();
+ let (x, y) = (Pubkey::new_unique(), Pubkey::new_unique());
+ let k = Pubkey::new_unique();
+
+ store(
+ &db,
+ k,
+ AccountBuilder::default().lamports(10).owner(x).build(),
+ );
+ assert_eq!(program(&db, &x), vec![k]);
+
+ // Re-store the volatile account under a new owner.
+ let mut moved = reload(&db, &k);
+ moved.set_owner(y);
+ store(&db, k, moved);
+
+ assert_eq!(program(&db, &x), Vec::::new()); // old set pruned
+ assert_eq!(program(&db, &y), vec![k]);
+ assert!(in_volatile(&db, &k));
+}
+
+// The freelist reuses a span only on an exact size match, and accounts of mixed
+// sizes survive defragmentation with their data intact.
+#[test]
+fn test_variable_sizes_and_exact_freelist() {
+ let (_dir, db) = db();
+ let owner = Pubkey::new_unique();
+ let reallocs = || db.persisted.storage.stats().reallocs.load(Relaxed);
+
+ // A freed large span cannot satisfy a smaller allocation: sizes differ, so
+ // the small insert allocates fresh rather than reusing the hole.
+ let big = Pubkey::new_unique();
+ store(&db, big, mutable_data(1, vec![0; 4096], &owner));
+ close(&db, &big);
+ let before = reallocs();
+ let small = Pubkey::new_unique();
+ store(&db, small, mutable_data(2, vec![0; 64], &owner));
+ assert_eq!(reallocs(), before); // size mismatch -> no reuse
+
+ // Store a spread of sizes with distinct data, punch an interior hole, then
+ // defragment and confirm every survivor keeps its exact bytes.
+ let sizes = [8usize, 512, 100, 4096, 1];
+ let mut live = Vec::new();
+ for (i, &space) in sizes.iter().enumerate() {
+ let k = Pubkey::new_unique();
+ let data: Vec = (0..space).map(|b| (b as u8).wrapping_add(i as u8)).collect();
+ store(&db, k, mutable_data(i as u64, data.clone(), &owner));
+ live.push((k, data));
+ }
+ close(&db, &small);
+
+ defrag_to_stable(&db);
+ for (k, data) in &live {
+ assert_eq!(
+ db.loader().load(k).unwrap().unwrap().data(),
+ data.as_slice()
+ );
+ }
+}
+
+// The checksum hashes accounts in pubkey order, so it depends only on content —
+// not on insertion order or the resulting on-disk offsets.
+#[test]
+fn test_checksum_order_independent() {
+ let keys: Vec = (0..8).map(|_| Pubkey::new_unique()).collect();
+
+ let checksum = |order: &[usize]| {
+ let (_dir, db) = db();
+ for &i in order {
+ store(&db, keys[i], delegated(100 + i as u64));
+ }
+ db.persisted.flush(true).unwrap();
+ db.checksum()
+ };
+
+ let forward: Vec = (0..keys.len()).collect();
+ let reversed: Vec = (0..keys.len()).rev().collect();
+ assert_eq!(checksum(&forward), checksum(&reversed));
+}
+
+// 2 MiB accounts overflow the initial storage block, forcing the file to grow;
+// removing half then defragmenting reclaims the large holes and shrinks the
+// cursor back — growth and compaction over multi-megabyte images.
+#[test]
+fn test_large_accounts_growth_and_defrag() {
+ const SIZE: usize = 2 << 20; // 2 MiB of data per account
+ const COUNT: usize = 12; // ~24 MiB total, past the 16 MiB test block
+
+ let (_dir, db) = db();
+ let owner = Pubkey::new_unique();
+ let resizes = || db.persisted.storage.stats().resizes.load(Relaxed);
+ let baseline = resizes();
+
+ // Distinct fill byte per account so content is verifiable without retaining
+ // the expected bytes.
+ let keys: Vec = (0..COUNT).map(|_| Pubkey::new_unique()).collect();
+ for (i, k) in keys.iter().enumerate() {
+ store(&db, *k, mutable_data(i as u64, vec![i as u8; SIZE], &owner));
+ }
+ // Crossing the initial block must have grown the file.
+ assert!(resizes() > baseline);
+
+ // Close every other account to punch large interior holes.
+ let mut live = Vec::new();
+ for (i, k) in keys.iter().enumerate() {
+ if i % 2 == 0 {
+ close(&db, k);
+ } else {
+ live.push((*k, i as u8));
+ }
+ }
+ let before = cursor(&db);
+
+ let reclaimed = defrag_to_stable(&db);
+ assert_eq!(reclaimed, before - cursor(&db));
+ assert!(cursor(&db) < before);
+
+ // Every survivor keeps its full 2 MiB image byte-for-byte.
+ for (k, fill) in &live {
+ let acc = db.loader().load(k).unwrap().unwrap();
+ assert_eq!(acc.data().len(), SIZE);
+ assert!(acc.data().iter().all(|&b| b == *fill));
+ }
+}
diff --git a/accountsdb/src/volatile.rs b/accountsdb/src/volatile.rs
new file mode 100644
index 00000000..8e296291
--- /dev/null
+++ b/accountsdb/src/volatile.rs
@@ -0,0 +1,129 @@
+//! In-memory account cache and program ownership sets.
+
+use std::{
+ collections::BTreeSet,
+ fs::{self, File},
+ io::BufReader,
+ path::Path,
+};
+
+use ahash::RandomState;
+use scc::HashMap;
+use solana_account::{AccountMode, AccountSharedData, OwnedAccount, ReadableAccount};
+use solana_pubkey::Pubkey;
+use tracing::info;
+
+use crate::{Result, StoreKind, metrics, snapshot::VOLATILE_DB_FILE};
+
+/// Owned accounts keyed by account pubkey.
+type AccountsMap = HashMap;
+/// Program ownership sets keyed by owner pubkey.
+type ProgramsMap = HashMap, RandomState>;
+
+/// Volatile account store backed by concurrent hash maps.
+pub(crate) struct VolatileStore {
+ /// Current owned accounts.
+ pub(crate) accounts: AccountsMap,
+ /// Program owner -> account pubkeys.
+ pub(crate) programs: ProgramsMap,
+}
+
+impl VolatileStore {
+ /// Opens the volatile store, optionally bootstrapping from a snapshot file.
+ ///
+ /// If `volatile.db` exists, it is loaded into memory and then removed from
+ /// the snapshot directory so the active tree stays single-sourced.
+ pub(crate) fn new(path: &Path) -> Result {
+ const CAP: usize = 2048;
+ let snapshot = path.join(VOLATILE_DB_FILE);
+ let accounts: AccountsMap = if snapshot.exists() {
+ let mut r = BufReader::new(File::open(&snapshot)?);
+ let accs: AccountsMap = bincode::deserialize_from(&mut r)?;
+ fs::remove_file(snapshot)?;
+ info!(
+ count = accs.len(),
+ "restored volatile accounts from snapshot"
+ );
+ accs
+ } else {
+ AccountsMap::with_capacity_and_hasher(CAP, Default::default())
+ };
+
+ let programs = ProgramsMap::with_capacity_and_hasher(CAP, Default::default());
+ accounts.iter_sync(|&pk, acc| {
+ BTreeSet::insert(&mut programs.entry_sync(acc.owner()).or_default(), pk)
+ });
+ Ok(Self { accounts, programs })
+ }
+
+ /// Stores volatile accounts and keeps the program ownership sets in sync.
+ pub(crate) fn upsert<'a, AC>(&self, accounts: AC)
+ where
+ AC: IntoIterator- ,
+ {
+ for (pubkey, account) in accounts {
+ // An account that has moved to an authoritative mode, or has been
+ // closed, drops any stale volatile copy.
+ if account.mode().authoritative() || account.is(AccountMode::Closed) {
+ self.delete(pubkey);
+ continue;
+ }
+ // Non-authoritative accounts stay volatile and update the program
+ // mapping.
+ let owner = *account.owner();
+ {
+ let mut set = self.programs.entry_sync(owner).or_default();
+ BTreeSet::insert(&mut set, *pubkey);
+ }
+ let Some(prev) = self.accounts.upsert_sync(*pubkey, account.owned()) else {
+ continue;
+ };
+ if prev.owner() == owner {
+ continue;
+ }
+ // Only the old owner set needs cleanup; the new owner was inserted above.
+ self.programs.remove_if_sync(&prev.owner(), |set| {
+ set.remove(pubkey);
+ set.is_empty()
+ });
+ }
+ metrics::accounts(StoreKind::Volatile, self.accounts.len() as u64);
+ }
+
+ /// Returns the owned account currently cached for `pubkey`.
+ pub(crate) fn load(&self, pubkey: &Pubkey) -> Option
{
+ let entry = self.accounts.get_sync(pubkey)?;
+ Some(entry.get().clone())
+ }
+
+ /// Returns whether a volatile account exists for `pubkey`.
+ pub(crate) fn contains(&self, pubkey: &Pubkey) -> bool {
+ self.accounts.contains_sync(pubkey)
+ }
+
+ /// Returns the owned accounts currently mapped to `owner`.
+ pub(crate) fn program(&self, owner: &Pubkey) -> BTreeSet {
+ self.programs.read_sync(owner, |_, s| s.clone()).unwrap_or_default()
+ }
+
+ /// Drops chain-mirrored accounts while retaining internal system accounts.
+ pub(crate) fn reset(&self) {
+ self.programs.clear_sync();
+ self.accounts.retain_sync(|k, a| {
+ if !a.is(AccountMode::System) {
+ return false;
+ }
+ let mut set = self.programs.entry_sync(a.owner()).or_default();
+ BTreeSet::insert(&mut set, *k)
+ });
+ }
+
+ /// Removes the cached account and drops its owner mapping.
+ fn delete(&self, pubkey: &Pubkey) {
+ let Some(e) = self.accounts.remove_sync(pubkey) else { return };
+ self.programs.remove_if_sync(&e.1.owner(), |set| {
+ set.remove(pubkey);
+ set.is_empty()
+ });
+ }
+}
diff --git a/solana/account/src/cow/tests.rs b/solana/account/src/cow/tests.rs
deleted file mode 100644
index 89ad2b65..00000000
--- a/solana/account/src/cow/tests.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-use super::borrowed::BorrowedAccount;
-use super::{StorageUnit, init, serialize_buf};
-use crate::AccountBuilder;
-use solana_pubkey::Pubkey;
-use std::sync::atomic::Ordering::Acquire;
-
-const BORROWED_LAMPORTS: u64 = 5;
-const ACTIVE_DATA: &[u8] = &[1, 2, 3];
-const COMMIT_DATA: &[u8] = &[4, 5, 6];
-const COMMITTED_DATA: &[u8] = &[9, 2, 3];
-const ACTIVE_WRITE: u8 = 9;
-const ROLLBACK_WRITE: u8 = 8;
-const INITIAL_SEQUENCE: u32 = 0;
-const COMMITTED_SEQUENCE: u32 = 1;
-
-// Serializes an owned account into a borrowed buffer image.
-fn make_buf(data: &[u8]) -> Vec {
- let owner = Pubkey::new_unique();
- let owned = AccountBuilder::default()
- .lamports(BORROWED_LAMPORTS)
- .data(data.to_vec())
- .owner(owner)
- .build();
- serialize_buf(&owned)
-}
-
-// Reads the active sequence counter.
-fn seq(acc: &BorrowedAccount) -> u32 {
- // SAFETY: test helpers only call this on a live borrowed buffer.
- unsafe { acc.header.as_ref().sequence.load(Acquire) }
-}
-
-// Returns the active image bytes for direct assertions.
-fn data(acc: &BorrowedAccount) -> &[u8] {
- &acc.data
-}
-
-#[test]
-// `init` should read the active image without changing the sequence.
-fn test_init_reads_active_image() {
- let mut buf = make_buf(ACTIVE_DATA);
- let borrowed = init(&mut buf);
-
- assert_eq!(data(&borrowed), ACTIVE_DATA);
- assert_eq!(seq(&borrowed), INITIAL_SEQUENCE);
-}
-
-#[test]
-// `translate` should copy the active image into the shadow view, and `commit` should publish it.
-fn test_translate_commit_publishes_shadow_image() {
- let mut buf = make_buf(ACTIVE_DATA);
- let mut borrowed = init(&mut buf);
-
- // SAFETY: `borrowed` still points at the live borrowed image selected by `init`.
- unsafe { borrowed.translate() };
- assert_eq!(seq(&borrowed), INITIAL_SEQUENCE);
-
- borrowed.data[0] = ACTIVE_WRITE;
- borrowed.commit();
- assert_eq!(seq(&borrowed), COMMITTED_SEQUENCE);
-
- let borrowed = init(&mut buf);
- assert_eq!(data(&borrowed), COMMITTED_DATA);
-}
-
-#[test]
-// `rollback` should discard shadow writes and restore the active view.
-fn test_translate_rollback_discards_shadow_writes() {
- let mut buf = make_buf(COMMIT_DATA);
- let mut borrowed = init(&mut buf);
-
- // SAFETY: `borrowed` still points at the live borrowed image selected by `init`.
- unsafe { borrowed.translate() };
- borrowed.data[0] = ROLLBACK_WRITE;
-
- // SAFETY: `reset` is paired with the preceding `translate`.
- unsafe { borrowed.reset() };
- assert_eq!(seq(&borrowed), INITIAL_SEQUENCE);
- assert_eq!(data(&borrowed), COMMIT_DATA);
-
- let borrowed = init(&mut buf);
- assert_eq!(data(&borrowed), COMMIT_DATA);
-}