diff --git a/.coderabbit.yaml b/.coderabbit.yaml
index 32c47d81..b755b0a3 100644
--- a/.coderabbit.yaml
+++ b/.coderabbit.yaml
@@ -8,11 +8,19 @@ reviews:
review_status: false
review_details: false
poem: false
- request_changes_workflow: false
+ request_changes_workflow: true
+
+
auto_review:
- enabled: true
- drafts: false
+ enabled: true # Disables automatic reviews on push
+ drafts: true
+ base_branches:
+ - ".*"
+ auto_incremental_review: true
+
+ # Never pause after repeated pushes.
+ auto_pause_after_reviewed_commits: 0
path_instructions:
- path: "**/*.rs"
@@ -26,11 +34,9 @@ reviews:
For Rust workspace code:
- Prioritize ownership, borrowing, lifetimes, unsafe usage, panic paths, concurrency, and API contract correctness.
- - Do not comment on minor style preferences, formatting, naming bikeshedding, speculative refactors, or micro-optimizations.
- Do not suggest changes unless the issue is concrete and actionable.
- path: "{**/*.md,**/*.rs}"
instructions: |
Check docs and rustdoc for factual consistency with the code.
Flag only real mismatches, broken examples, stale comments, or important omissions.
- Ignore wording nits unless they are genuine typos or change meaning.
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..8ec98ed8
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+*.rs linguist-language=Rust
+Cargo.toml linguist-language=Rust
+Cargo.lock linguist-language=Rust
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 7044ec5f..003cb7f0 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -6,6 +6,12 @@ type: Bug
labels: ["bug"]
---
+
+
## Problem
diff --git a/.github/ISSUE_TEMPLATE/change_request.md b/.github/ISSUE_TEMPLATE/change_request.md
index 50b40309..7b36f843 100644
--- a/.github/ISSUE_TEMPLATE/change_request.md
+++ b/.github/ISSUE_TEMPLATE/change_request.md
@@ -6,6 +6,12 @@ type: Feature
labels: ["enhancement"]
---
+
+
## Goal
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index b0a2c6ed..803d4b3d 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -2,9 +2,15 @@
name: Spike / question
about: Ask a design question or request an investigation
title: ""
-labels: question
+labels: ["question"]
---
+
+
## Question
diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md
index c7c3f712..f005f160 100644
--- a/.github/ISSUE_TEMPLATE/task.md
+++ b/.github/ISSUE_TEMPLATE/task.md
@@ -6,6 +6,12 @@ type: Task
labels: []
---
+
+
## Outcome
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index ffd1f751..3d946471 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,9 @@
+
+
## What changed
@@ -7,8 +13,5 @@ Closes #ISSUE
## Impact
-## Validation
-
-
## Reviewer notes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..da075d1b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,80 @@
+name: Run CI
+
+on:
+ push:
+ branches: [master, dev]
+ pull_request:
+ types: [opened, reopened, synchronize]
+
+permissions:
+ contents: read
+
+env:
+ AGAVE_VERSION: v4.2.0
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ check:
+ name: ${{ matrix.name }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: Format
+ command: cargo fmt --check
+ cache: false
+ sbf: false
+ nextest: false
+ - name: Clippy
+ command: cargo clippy --quiet --all-features --all-targets -- -D warnings
+ cache: true
+ sbf: true
+ nextest: false
+ - name: Tests
+ command: cargo nextest run --cargo-quiet --nff
+ cache: true
+ sbf: true
+ nextest: true
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Install Rust toolchain
+ run: rustup toolchain install
+
+ - name: Cache Rust build artifacts
+ if: matrix.cache
+ uses: Swatinem/rust-cache@v2
+ with:
+ key: ${{ matrix.name }}
+ cache-on-failure: true
+
+ - name: Cache Solana SBF tools
+ if: matrix.sbf
+ id: cache-solana
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.local/share/solana
+ ~/.cache/solana
+ key: solana-sbf-${{ runner.os }}-${{ runner.arch }}-${{ env.AGAVE_VERSION }}
+
+ - name: Install Solana CLI
+ if: matrix.sbf && steps.cache-solana.outputs.cache-hit != 'true'
+ run: sh -c "$(curl -sSfL https://release.anza.xyz/$AGAVE_VERSION/install)"
+
+ - name: Add Solana CLI to PATH
+ if: matrix.sbf
+ run: echo "$HOME/.local/share/solana/install/active_release/bin" >> "$GITHUB_PATH"
+
+ - name: Install cargo-nextest
+ if: matrix.nextest
+ uses: taiki-e/install-action@nextest
+
+ - name: Run ${{ matrix.name }}
+ run: ${{ matrix.command }}
diff --git a/.gitignore b/.gitignore
index 1798e8ba..8b202a78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
/target
-Cargo.lock
AGENTS.md
+CLAUDE.md
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 00000000..cf3a784b
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,5455 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "agave-feature-set"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402f8462d64e03c9b09a96b1898f7dbc8b9c5d6e9534fb777905df599ad5d787"
+dependencies = [
+ "ahash",
+ "solana-epoch-schedule",
+ "solana-hash",
+ "solana-keypair",
+ "solana-pubkey",
+ "solana-sha256-hasher",
+ "solana-svm-feature-set",
+]
+
+[[package]]
+name = "agave-precompiles"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a97e478437ac3d1cc1446600afb972e446736b301b02f0c1ba31d81bb0c91b6"
+dependencies = [
+ "agave-feature-set",
+ "bincode",
+ "ed25519-dalek 1.0.1",
+ "libsecp256k1",
+ "openssl",
+ "solana-ed25519-program",
+ "solana-keccak-hasher",
+ "solana-message",
+ "solana-precompile-error",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-secp256k1-program",
+ "solana-secp256r1-program",
+]
+
+[[package]]
+name = "agave-transaction-view"
+version = "4.1.1"
+dependencies = [
+ "agave-transaction-view",
+ "bincode",
+ "criterion",
+ "solana-hash",
+ "solana-instruction",
+ "solana-keypair",
+ "solana-message",
+ "solana-packet",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-short-vec",
+ "solana-signature",
+ "solana-signer",
+ "solana-svm-transaction",
+ "solana-system-interface",
+ "solana-transaction",
+ "solana-transaction-context",
+ "wincode",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloca"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "arc-swap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "ark-bn254"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f"
+dependencies = [
+ "ark-ec 0.4.2",
+ "ark-ff 0.4.2",
+ "ark-std 0.4.0",
+]
+
+[[package]]
+name = "ark-bn254"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc"
+dependencies = [
+ "ark-ec 0.5.0",
+ "ark-ff 0.5.0",
+ "ark-std 0.5.0",
+]
+
+[[package]]
+name = "ark-ec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba"
+dependencies = [
+ "ark-ff 0.4.2",
+ "ark-poly 0.4.2",
+ "ark-serialize 0.4.2",
+ "ark-std 0.4.0",
+ "derivative",
+ "hashbrown 0.13.2",
+ "itertools 0.10.5",
+ "num-traits",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ec"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce"
+dependencies = [
+ "ahash",
+ "ark-ff 0.5.0",
+ "ark-poly 0.5.0",
+ "ark-serialize 0.5.0",
+ "ark-std 0.5.0",
+ "educe",
+ "fnv",
+ "hashbrown 0.15.5",
+ "itertools 0.13.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba"
+dependencies = [
+ "ark-ff-asm 0.4.2",
+ "ark-ff-macros 0.4.2",
+ "ark-serialize 0.4.2",
+ "ark-std 0.4.0",
+ "derivative",
+ "digest 0.10.7",
+ "itertools 0.10.5",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "rustc_version",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70"
+dependencies = [
+ "ark-ff-asm 0.5.0",
+ "ark-ff-macros 0.5.0",
+ "ark-serialize 0.5.0",
+ "ark-std 0.5.0",
+ "arrayvec",
+ "digest 0.10.7",
+ "educe",
+ "itertools 0.13.0",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ark-poly"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf"
+dependencies = [
+ "ark-ff 0.4.2",
+ "ark-serialize 0.4.2",
+ "ark-std 0.4.0",
+ "derivative",
+ "hashbrown 0.13.2",
+]
+
+[[package]]
+name = "ark-poly"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27"
+dependencies = [
+ "ahash",
+ "ark-ff 0.5.0",
+ "ark-serialize 0.5.0",
+ "ark-std 0.5.0",
+ "educe",
+ "fnv",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
+dependencies = [
+ "ark-serialize-derive 0.4.2",
+ "ark-std 0.4.0",
+ "digest 0.10.7",
+ "num-bigint",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7"
+dependencies = [
+ "ark-serialize-derive 0.5.0",
+ "ark-std 0.5.0",
+ "arrayvec",
+ "digest 0.10.7",
+ "num-bigint",
+]
+
+[[package]]
+name = "ark-serialize-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-serialize-derive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
+dependencies = [
+ "num-traits",
+ "rand 0.8.7",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a"
+dependencies = [
+ "num-traits",
+ "rand 0.8.7",
+]
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "ascii"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
+
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
+[[package]]
+name = "assoc"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfdc70193dadb9d7287fa4b633f15f90c876915b31f6af17da307fc59c9859a8"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitcode"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d"
+dependencies = [
+ "arrayvec",
+ "bitcode_derive",
+ "bytemuck",
+ "glam",
+ "serde",
+]
+
+[[package]]
+name = "bitcode_derive"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "blake3"
+version = "1.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "blst"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe"
+dependencies = [
+ "cc",
+ "glob",
+ "threadpool",
+ "zeroize",
+]
+
+[[package]]
+name = "blstrs"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29"
+dependencies = [
+ "blst",
+ "byte-slice-cast",
+ "ff",
+ "group",
+ "pairing",
+ "rand_core 0.6.4",
+ "serde",
+ "subtle",
+]
+
+[[package]]
+name = "borsh"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f"
+dependencies = [
+ "borsh-derive",
+ "bytes",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bstr"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
+dependencies = [
+ "memchr",
+ "serde_core",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bv"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340"
+dependencies = [
+ "feature-probe",
+ "serde",
+]
+
+[[package]]
+name = "byte-slice-cast"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+dependencies = [
+ "bytemuck_derive",
+]
+
+[[package]]
+name = "bytemuck_derive"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstyle",
+ "clap_lex",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "clonetree"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bfe46018b627d1c990f2ac173e876b1d82b18dc3b6b05691dae2fe4a07ba5b5"
+dependencies = [
+ "ignore",
+ "reflink-copy",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "combine"
+version = "3.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
+dependencies = [
+ "ascii",
+ "byteorder",
+ "either",
+ "memchr",
+ "unreachable",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "criterion"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
+dependencies = [
+ "alloca",
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "itertools 0.13.0",
+ "num-traits",
+ "oorandom",
+ "page_size",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
+dependencies = [
+ "cast",
+ "itertools 0.13.0",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
+dependencies = [
+ "byteorder",
+ "digest 0.9.0",
+ "rand_core 0.5.1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "digest 0.10.7",
+ "fiat-crypto",
+ "rand_core 0.6.4",
+ "rustc_version",
+ "serde",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "const-oid",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "crypto-common 0.2.2",
+ "ctutils",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "doxygen-rs"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9"
+dependencies = [
+ "phf",
+]
+
+[[package]]
+name = "eager"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe71d579d1812060163dff96056261deb5bf6729b100fa2e36a68b9649ba3d3"
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest 0.10.7",
+ "elliptic-curve",
+ "rfc6979",
+ "signature 2.2.0",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+dependencies = [
+ "signature 1.6.4",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature 2.2.0",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
+dependencies = [
+ "curve25519-dalek 3.2.0",
+ "ed25519 1.5.3",
+ "rand 0.7.3",
+ "serde",
+ "sha2 0.9.9",
+ "zeroize",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek 4.1.3",
+ "ed25519 2.2.3",
+ "rand_core 0.6.4",
+ "serde",
+ "sha2 0.10.9",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "either"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest 0.10.7",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "enum-iterator"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016"
+dependencies = [
+ "enum-iterator-derive",
+]
+
+[[package]]
+name = "enum-iterator-derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "env_filter"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+dependencies = [
+ "getrandom 0.4.3",
+]
+
+[[package]]
+name = "feature-probe"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da"
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "bitvec",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
+
+[[package]]
+name = "five8"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772"
+dependencies = [
+ "five8_core",
+]
+
+[[package]]
+name = "five8_const"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5"
+dependencies = [
+ "five8_core",
+]
+
+[[package]]
+name = "five8_core"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406"
+
+[[package]]
+name = "flume"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
+
+[[package]]
+name = "futures-io"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
+
+[[package]]
+name = "futures-task"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
+
+[[package]]
+name = "futures-util"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdbstub"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e02bf1b1a624d96925c608f1b268d82a76cbc587ce9e59f7c755e9ea11c75c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cfg-if",
+ "log",
+ "managed",
+ "num-traits",
+ "paste",
+]
+
+[[package]]
+name = "generator"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "log",
+ "rustversion",
+ "windows-link",
+ "windows-result",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "glam"
+version = "0.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360bd2cd76e0cd9032d42cf2922155cecea2685b0cfa4630c3246df030bcfd6"
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "globset"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand 0.8.7",
+ "rand_core 0.6.4",
+ "rand_xorshift",
+ "subtle",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heed"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202"
+dependencies = [
+ "bitflags 2.13.1",
+ "byteorder",
+ "heed-traits",
+ "heed-types",
+ "libc",
+ "lmdb-master-sys",
+ "once_cell",
+ "page_size",
+ "synchronoise",
+ "url",
+]
+
+[[package]]
+name = "heed-traits"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff"
+
+[[package]]
+name = "heed-types"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d"
+dependencies = [
+ "byteorder",
+ "heed-traits",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
+dependencies = [
+ "crypto-mac",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "hmac-drbg"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1"
+dependencies = [
+ "digest 0.9.0",
+ "generic-array",
+ "hmac 0.8.1",
+]
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "k256"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
+dependencies = [
+ "cfg-if",
+ "ecdsa",
+ "elliptic-curve",
+ "once_cell",
+ "sha2 0.10.9",
+ "signature 2.2.0",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libsecp256k1"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139"
+dependencies = [
+ "arrayref",
+ "base64",
+ "digest 0.9.0",
+ "hmac-drbg",
+ "libsecp256k1-core",
+ "libsecp256k1-gen-ecmult",
+ "libsecp256k1-gen-genmult",
+ "rand 0.8.7",
+ "serde",
+ "sha2 0.9.9",
+ "typenum",
+]
+
+[[package]]
+name = "libsecp256k1-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451"
+dependencies = [
+ "crunchy",
+ "digest 0.9.0",
+ "subtle",
+]
+
+[[package]]
+name = "libsecp256k1-gen-ecmult"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809"
+dependencies = [
+ "libsecp256k1-core",
+]
+
+[[package]]
+name = "libsecp256k1-gen-genmult"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c"
+dependencies = [
+ "libsecp256k1-core",
+]
+
+[[package]]
+name = "light-poseidon"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee"
+dependencies = [
+ "ark-bn254 0.4.0",
+ "ark-ff 0.4.2",
+ "num-bigint",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "light-poseidon"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862"
+dependencies = [
+ "ark-bn254 0.5.0",
+ "ark-ff 0.5.0",
+ "num-bigint",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "lmdb-master-sys"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aaeb9bd22e73bd1babffff614994b341e9b2008de7bb73bf1f7e9154f1978f8b"
+dependencies = [
+ "cc",
+ "doxygen-rs",
+ "libc",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "magic-root-interface"
+version = "0.1.0"
+dependencies = [
+ "solana-account",
+ "solana-instruction",
+ "solana-pubkey",
+ "wincode",
+]
+
+[[package]]
+name = "magic-root-program"
+version = "0.1.0"
+dependencies = [
+ "magic-root-interface",
+ "magicblock-engine-nucleus",
+ "solana-account",
+ "solana-instruction",
+ "solana-instruction-error",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-svm-callback",
+ "solana-svm-feature-set",
+ "solana-svm-log-collector",
+ "solana-transaction-context",
+ "wincode",
+]
+
+[[package]]
+name = "magicblock-accountsdb"
+version = "0.1.0"
+dependencies = [
+ "ahash",
+ "assert_matches",
+ "bincode",
+ "bytemuck",
+ "clonetree",
+ "derive_more",
+ "heed",
+ "magicblock-accountsdb",
+ "magicblock-engine-nucleus",
+ "memmap2",
+ "parking_lot",
+ "scc",
+ "solana-account",
+ "solana-pubkey",
+ "thiserror 2.0.19",
+ "tracing",
+ "twox-hash",
+]
+
+[[package]]
+name = "magicblock-engine"
+version = "0.1.0"
+dependencies = [
+ "agave-transaction-view",
+ "derive_more",
+ "magic-root-interface",
+ "magic-root-program",
+ "magicblock-engine",
+ "magicblock-engine-nucleus",
+ "magicblock-keeper",
+ "magicblock-ledger",
+ "magicblock-processor",
+ "num_cpus",
+ "oneshot",
+ "solana-account",
+ "solana-compute-budget-program",
+ "solana-instruction",
+ "solana-instruction-error",
+ "solana-keypair",
+ "solana-message",
+ "solana-packet",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-signer",
+ "solana-system-interface",
+ "solana-system-program",
+ "solana-sysvar",
+ "solana-transaction",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "v42-calculator-interface",
+ "wincode",
+]
+
+[[package]]
+name = "magicblock-engine-nucleus"
+version = "0.1.0"
+dependencies = [
+ "agave-transaction-view",
+ "derive_more",
+ "futures",
+ "heed",
+ "oneshot",
+ "prometheus",
+ "serde",
+ "serde_with",
+ "solana-hash",
+ "solana-instruction",
+ "solana-instruction-error",
+ "solana-keypair",
+ "solana-message",
+ "solana-pubkey",
+ "solana-signature",
+ "solana-signer",
+ "solana-svm",
+ "solana-transaction",
+ "solana-transaction-error",
+ "tempfile",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+ "v42-calculator-interface",
+ "wincode",
+]
+
+[[package]]
+name = "magicblock-keeper"
+version = "0.1.0"
+dependencies = [
+ "agave-feature-set",
+ "ahash",
+ "arc-swap",
+ "derive_more",
+ "flume",
+ "magicblock-accountsdb",
+ "magicblock-engine-nucleus",
+ "magicblock-keeper",
+ "magicblock-ledger",
+ "oneshot",
+ "parking_lot",
+ "scc",
+ "serde",
+ "smallvec",
+ "solana-account",
+ "solana-feature-gate-interface",
+ "solana-hash",
+ "solana-instruction",
+ "solana-keypair",
+ "solana-message",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-signature",
+ "solana-signer",
+ "solana-svm",
+ "solana-sysvar",
+ "solana-transaction-error",
+ "tar",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "zstd",
+]
+
+[[package]]
+name = "magicblock-ledger"
+version = "0.1.0"
+dependencies = [
+ "agave-transaction-view",
+ "bitcode",
+ "bytemuck",
+ "derive_more",
+ "flume",
+ "heed",
+ "magicblock-engine-nucleus",
+ "magicblock-ledger",
+ "memmap2",
+ "num_cpus",
+ "oneshot",
+ "parking_lot",
+ "rustix",
+ "solana-pubkey",
+ "solana-signature",
+ "solana-transaction-error",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "wincode",
+ "zstd",
+]
+
+[[package]]
+name = "magicblock-processor"
+version = "0.1.0"
+dependencies = [
+ "agave-feature-set",
+ "agave-precompiles",
+ "agave-transaction-view",
+ "ahash",
+ "blake3",
+ "derive_more",
+ "magicblock-accountsdb",
+ "magicblock-engine-nucleus",
+ "magicblock-keeper",
+ "oneshot",
+ "solana-account",
+ "solana-compute-budget-instruction",
+ "solana-hash",
+ "solana-instruction",
+ "solana-keypair",
+ "solana-precompile-error",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-signature",
+ "solana-svm",
+ "solana-svm-transaction",
+ "solana-syscalls",
+ "solana-sysvar",
+ "solana-transaction-error",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "v42-calculator-interface",
+]
+
+[[package]]
+name = "magicblock-replicator"
+version = "0.1.0"
+dependencies = [
+ "derive_more",
+ "magicblock-engine",
+ "magicblock-engine-nucleus",
+ "magicblock-keeper",
+ "magicblock-ledger",
+ "scc",
+ "snedfile",
+ "solana-account",
+ "solana-keypair",
+ "solana-pubkey",
+ "solana-signature",
+ "solana-sysvar",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "v42-calculator-interface",
+ "wincode",
+]
+
+[[package]]
+name = "managed"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memmap2"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "oneshot"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl"
+version = "0.10.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
+dependencies = [
+ "bitflags 2.13.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "openssl-src"
+version = "300.6.1+3.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
+name = "page_size"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "pairing"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
+dependencies = [
+ "group",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pastey"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
+
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared",
+ "rand 0.8.7",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prometheus"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a"
+dependencies = [
+ "cfg-if",
+ "fnv",
+ "lazy_static",
+ "memchr",
+ "parking_lot",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "qualifier_attr"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.1",
+]
+
+[[package]]
+name = "reflink-copy"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9dd7ab4af0363d5ccfd2838d782a28196cf32a5cc2e4fe3c5dc83f2be588b8b"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rustix",
+ "windows",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac 0.12.1",
+ "subtle",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "saa"
+version = "5.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f5acb362a0e75c2a963532fa7fabf13dff81626dc494df16488d30befcbea0"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scc"
+version = "3.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8af0b99483d1c3e59471d4f0cb58b244169436a8979c889a91a3f697075ea01"
+dependencies = [
+ "saa",
+ "sdd",
+ "serde",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sdd"
+version = "4.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1836bad8bdc9c6d665b63202da3d9c6d60ed1e597cae63620e21ebf89a3595a9"
+dependencies = [
+ "saa",
+]
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-big-array"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_bytes"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2-const-stable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
+
+[[package]]
+name = "sha3"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
+dependencies = [
+ "digest 0.10.7",
+ "keccak",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "shuttle"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d9a8db61a44e2b663f169a08206a789bcbd22ba32011e14951562848e7b9c98"
+dependencies = [
+ "assoc",
+ "bitvec",
+ "generator",
+ "hex",
+ "owo-colors",
+ "rand 0.8.7",
+ "rand_core 0.6.4",
+ "rand_pcg",
+ "scoped-tls",
+ "smallvec",
+ "tracing",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest 0.10.7",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "snedfile"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b49d9659526e7275bf3cd23e43cff8d14e4864ff1eb1e3836a07decbc3689b4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "solana-account"
+version = "4.3.1"
+dependencies = [
+ "bincode",
+ "bitflags 2.13.1",
+ "serde",
+ "serde_bytes",
+ "solana-account",
+ "solana-account-info",
+ "solana-clock",
+ "solana-instruction-error",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-sysvar",
+ "thiserror 2.0.19",
+ "wincode",
+]
+
+[[package]]
+name = "solana-account-info"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95"
+dependencies = [
+ "solana-address",
+ "solana-program-error",
+ "solana-program-memory",
+]
+
+[[package]]
+name = "solana-address"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb"
+dependencies = [
+ "borsh",
+ "bytemuck",
+ "bytemuck_derive",
+ "curve25519-dalek 4.1.3",
+ "five8",
+ "five8_const",
+ "rand 0.9.5",
+ "serde",
+ "serde_derive",
+ "sha2-const-stable",
+ "solana-atomic-u64",
+ "solana-define-syscall 5.2.0",
+ "solana-program-error",
+ "solana-sanitize",
+ "solana-sha256-hasher",
+ "wincode",
+]
+
+[[package]]
+name = "solana-atomic-u64"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "085db4906d89324cef2a30840d59eaecf3d4231c560ec7c9f6614a93c652f501"
+dependencies = [
+ "parking_lot",
+]
+
+[[package]]
+name = "solana-big-mod-exp"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30c80fb6d791b3925d5ec4bf23a7c169ef5090c013059ec3ed7d0b2c04efa085"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "solana-define-syscall 3.0.0",
+]
+
+[[package]]
+name = "solana-bincode"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "278a1a5bad62cd9da89ac8d4b7ec444e83caa8ae96aa656dfc27684b28d49a5d"
+dependencies = [
+ "bincode",
+ "serde_core",
+ "solana-instruction-error",
+]
+
+[[package]]
+name = "solana-blake3-hasher"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176"
+dependencies = [
+ "blake3",
+ "solana-define-syscall 4.0.1",
+ "solana-hash",
+]
+
+[[package]]
+name = "solana-bls12-381-syscall"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94e8256fda0542709fd1320a96d81c3c66e10ac5f680cca9b991d7d3b0e3fe86"
+dependencies = [
+ "blst",
+ "blstrs",
+ "bytemuck",
+ "bytemuck_derive",
+ "pairing",
+]
+
+[[package]]
+name = "solana-bn254"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62ff13a8867fcc7b0f1114764e1bf6191b4551dcaf93729ddc676cd4ec6abc9f"
+dependencies = [
+ "ark-bn254 0.5.0",
+ "ark-ec 0.5.0",
+ "ark-ff 0.5.0",
+ "ark-serialize 0.5.0",
+ "bytemuck",
+ "solana-define-syscall 5.2.0",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "solana-borsh"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c04abbae16f57178a163125805637b8a076175bb5c0002fb04f4792bea901cf7"
+dependencies = [
+ "borsh",
+]
+
+[[package]]
+name = "solana-builtins-default-costs"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f40331a0b7a959114d0cf8ad6d1fdd74c1fb15acd9bdede706f63517e649c1"
+dependencies = [
+ "agave-feature-set",
+ "ahash",
+ "solana-pubkey",
+ "solana-sdk-ids",
+]
+
+[[package]]
+name = "solana-clock"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0acdace90d96e2c9e70d681465b4fe888b6bcf27c354ae9774e9f8a3b72923d"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-sdk-ids",
+ "solana-sdk-macro",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-compute-budget"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5029b1e141344bba1261d5c28de8067cb89927bb06fbbfa71df6ef84d25eaf1b"
+dependencies = [
+ "solana-fee-structure",
+ "solana-program-runtime",
+]
+
+[[package]]
+name = "solana-compute-budget-instruction"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921873119f821519389b1909697a5b6286c7eb3cba72a5ba3ef172489dde531c"
+dependencies = [
+ "agave-feature-set",
+ "solana-borsh",
+ "solana-builtins-default-costs",
+ "solana-compute-budget",
+ "solana-compute-budget-interface",
+ "solana-instruction",
+ "solana-packet",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-svm-transaction",
+ "solana-transaction-error",
+]
+
+[[package]]
+name = "solana-compute-budget-interface"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8292c436b269ad23cecc8b24f7da3ab07ca111661e25e00ce0e1d22771951ab9"
+dependencies = [
+ "borsh",
+ "solana-instruction",
+ "solana-sdk-ids",
+]
+
+[[package]]
+name = "solana-compute-budget-program"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c05c42c6d7a20d221ba188d0ab6914081910e8ff4b35c02e6536b94104f575c"
+dependencies = [
+ "solana-program-runtime",
+]
+
+[[package]]
+name = "solana-cpi"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c"
+dependencies = [
+ "solana-account-info",
+ "solana-define-syscall 4.0.1",
+ "solana-instruction",
+ "solana-program-error",
+ "solana-pubkey",
+ "solana-stable-layout",
+]
+
+[[package]]
+name = "solana-curve25519"
+version = "4.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af"
+dependencies = [
+ "bytemuck",
+ "bytemuck_derive",
+ "curve25519-dalek 4.1.3",
+ "solana-define-syscall 5.2.0",
+ "subtle",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "solana-define-syscall"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee"
+
+[[package]]
+name = "solana-define-syscall"
+version = "4.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738"
+
+[[package]]
+name = "solana-define-syscall"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf8209ece2bd9f1450e672858ffc0e5c8c786ff6916d2a862b126dd0128f380f"
+
+[[package]]
+name = "solana-ed25519-program"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1419197f1c06abf760043f6d64ba9d79a03ad5a43f18c7586471937122094da"
+dependencies = [
+ "bytemuck",
+ "bytemuck_derive",
+ "solana-instruction",
+ "solana-sdk-ids",
+]
+
+[[package]]
+name = "solana-epoch-rewards"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf7eb4986b0b1d6f562b21f75a836f1a6df6e00c275efcef50aab5c144dc59e"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-hash",
+ "solana-sdk-ids",
+ "solana-sdk-macro",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-epoch-schedule"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8116e6ffa6002237d5ab5edcbda17f9ba66b6742c45a89c9fb40a94dbacd4c1d"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-program-error",
+ "solana-sdk-ids",
+ "solana-sdk-macro",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-feature-gate-interface"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd7545e02f91da1d6996f32b18f7796aa01e0682f8f3a7434b82cd1a10448add"
+dependencies = [
+ "bincode",
+ "serde",
+ "serde_derive",
+ "solana-account",
+ "solana-account-info",
+ "solana-instruction",
+ "solana-program-error",
+ "solana-pubkey",
+ "solana-rent",
+ "solana-sdk-ids",
+ "solana-system-interface",
+]
+
+[[package]]
+name = "solana-fee-calculator"
+version = "3.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef67f01cc6a0c72e99a08d0d484683f995de4c80e9568728fa77d1537f9b7e09"
+dependencies = [
+ "log",
+ "serde",
+ "serde_derive",
+ "wincode",
+]
+
+[[package]]
+name = "solana-fee-structure"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2abdb1223eea8ec64136f39cb1ffcf257e00f915c957c35c0dd9e3f4e700b0"
+
+[[package]]
+name = "solana-get-sysvar"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4"
+dependencies = [
+ "solana-address",
+ "solana-define-syscall 5.2.0",
+ "solana-program-error",
+]
+
+[[package]]
+name = "solana-hash"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe51db00ac3aa9f950d1e6201a126acfa26e6d81bc4a183ba64ec02effcad883"
+dependencies = [
+ "bytemuck",
+ "bytemuck_derive",
+ "five8",
+ "serde",
+ "serde_derive",
+ "solana-atomic-u64",
+ "solana-sanitize",
+ "wincode",
+]
+
+[[package]]
+name = "solana-hash-512"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce934b02bab639341f2fd62c3e7e3c39dcceb47f0196b7630ed1f82ecb704bd"
+
+[[package]]
+name = "solana-instruction"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1"
+dependencies = [
+ "bincode",
+ "serde",
+ "solana-define-syscall 5.2.0",
+ "solana-instruction-error",
+ "solana-pubkey",
+ "wincode",
+]
+
+[[package]]
+name = "solana-instruction-error"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b7d34343838343a3755b7dfb1e438d94c6db2263b519cfe3c2257af932b6e93"
+dependencies = [
+ "num-traits",
+ "serde",
+ "serde_derive",
+ "solana-program-error",
+ "wincode",
+]
+
+[[package]]
+name = "solana-instructions-sysvar"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e38363a181313d607f7a118df2b401bb27b477a0104b09283cbf504f5f0f00cc"
+dependencies = [
+ "bitflags 2.13.1",
+ "solana-account-info",
+ "solana-instruction",
+ "solana-instruction-error",
+ "solana-program-error",
+ "solana-sanitize",
+ "solana-sdk-ids",
+ "solana-serialize-utils",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-keccak-hasher"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8"
+dependencies = [
+ "sha3",
+ "solana-define-syscall 4.0.1",
+ "solana-hash",
+]
+
+[[package]]
+name = "solana-keypair"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "263d614c12aa267a3278703175fd6440552ca61bc960b5a02a4482720c53438b"
+dependencies = [
+ "ed25519-dalek 2.2.0",
+ "five8",
+ "five8_core",
+ "rand 0.9.5",
+ "solana-address",
+ "solana-seed-phrase",
+ "solana-signature",
+ "solana-signer",
+]
+
+[[package]]
+name = "solana-last-restart-slot"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c22474b83d3c7c318e1c3a725784fc2d1d03b728e36369e58ce48769a61ed85e"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-sdk-ids",
+ "solana-sdk-macro",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-loader-v3-interface"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68029ab11d9c891d4ce23ada75745e40f983c5674af366f447b672569634231b"
+dependencies = [
+ "serde",
+ "serde_bytes",
+ "serde_derive",
+ "solana-instruction",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-system-interface",
+]
+
+[[package]]
+name = "solana-message"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a634d1db65a393d4e87ec49ef97c8dfb12201e2ba9e5faf87ff34e632f29e509"
+dependencies = [
+ "blake3",
+ "lazy_static",
+ "serde",
+ "serde_derive",
+ "solana-address",
+ "solana-hash",
+ "solana-instruction",
+ "solana-sanitize",
+ "solana-sdk-ids",
+ "solana-short-vec",
+ "solana-transaction-error",
+ "wincode",
+]
+
+[[package]]
+name = "solana-msg"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "726b7cbbc6be6f1c6f29146ac824343b9415133eee8cce156452ad1db93f8008"
+dependencies = [
+ "solana-define-syscall 5.2.0",
+]
+
+[[package]]
+name = "solana-native-token"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f"
+
+[[package]]
+name = "solana-nonce"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4172d5b33a0a38fcdb2af8f84406570dd51567267da96c28ad0f31fc11621c0"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-fee-calculator",
+ "solana-hash",
+ "solana-pubkey",
+ "solana-sha256-hasher",
+ "wincode",
+]
+
+[[package]]
+name = "solana-nonce-account"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b81bf7e2aa8c443724051507c1007d0b6d9c34b70925d3232dc78f5ca485209e"
+dependencies = [
+ "solana-account",
+ "solana-hash",
+ "solana-nonce",
+ "solana-sdk-ids",
+ "wincode",
+]
+
+[[package]]
+name = "solana-packet"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a2a582d7863548f047f49aa3745c076cfa9e1364baa080ef0c1210f0e4ebda"
+dependencies = [
+ "bitflags 2.13.1",
+ "solana-pubkey",
+]
+
+[[package]]
+name = "solana-poseidon"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "737b8ab25bf4cc8e618f80f1fe40709b2ace708bc764a36b8a4c81eea8c07034"
+dependencies = [
+ "ark-bn254 0.4.0",
+ "ark-bn254 0.5.0",
+ "light-poseidon 0.2.0",
+ "light-poseidon 0.4.0",
+ "solana-define-syscall 4.0.1",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "solana-precompile-error"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cafcd950de74c6c39d55dc8ca108bbb007799842ab370ef26cf45a34453c31e1"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "solana-program-entrypoint"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997"
+dependencies = [
+ "solana-account-info",
+ "solana-define-syscall 4.0.1",
+ "solana-program-error",
+ "solana-pubkey",
+]
+
+[[package]]
+name = "solana-program-error"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6"
+
+[[package]]
+name = "solana-program-memory"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4068648649653c2c50546e9a7fb761791b5ab0cda054c771bb5808d3a4b9eb52"
+dependencies = [
+ "solana-define-syscall 4.0.1",
+]
+
+[[package]]
+name = "solana-program-runtime"
+version = "4.1.1"
+dependencies = [
+ "assert_matches",
+ "base64",
+ "bincode",
+ "cfg-if",
+ "itertools 0.13.0",
+ "qualifier_attr",
+ "scc",
+ "serde",
+ "solana-account",
+ "solana-account-info",
+ "solana-clock",
+ "solana-epoch-rewards",
+ "solana-epoch-schedule",
+ "solana-fee-structure",
+ "solana-hash",
+ "solana-instruction",
+ "solana-keypair",
+ "solana-last-restart-slot",
+ "solana-loader-v3-interface",
+ "solana-program-entrypoint",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-rent",
+ "solana-sbpf",
+ "solana-sdk-ids",
+ "solana-signer",
+ "solana-slot-hashes",
+ "solana-stable-layout",
+ "solana-svm-callback",
+ "solana-svm-feature-set",
+ "solana-svm-log-collector",
+ "solana-svm-measure",
+ "solana-svm-timings",
+ "solana-svm-transaction",
+ "solana-svm-type-overrides",
+ "solana-system-interface",
+ "solana-sysvar",
+ "solana-sysvar-id",
+ "solana-transaction",
+ "solana-transaction-context",
+ "test-case",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "solana-pubkey"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7db719574990de7e8b0f55a8593ac92a5ccb42c8ce67b3e4bf05b139d5d9ee71"
+dependencies = [
+ "rand 0.9.5",
+ "solana-address",
+]
+
+[[package]]
+name = "solana-rent"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39f0d780bf8e8a1fe8b5b5fce1acad6b209485b86dec246e7523d5e4a8b7c7fc"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-sdk-ids",
+ "solana-sdk-macro",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-sanitize"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9"
+
+[[package]]
+name = "solana-sbpf"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f84c593fa3d4131045b606dec5acf9d8eac73791bc786ca9911057aec8f43ec"
+dependencies = [
+ "byteorder",
+ "combine",
+ "gdbstub",
+ "hash32",
+ "libc",
+ "log",
+ "rand 0.8.7",
+ "rustc-demangle",
+ "shuttle",
+ "thiserror 2.0.19",
+ "winapi",
+]
+
+[[package]]
+name = "solana-sdk-ids"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280"
+dependencies = [
+ "solana-address",
+]
+
+[[package]]
+name = "solana-sdk-macro"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f"
+dependencies = [
+ "bs58",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "solana-secp256k1-program"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad4cf8232f7aef9ff2dd95d701f63e3c11909dec2400def5c361be29d24291e7"
+dependencies = [
+ "digest 0.10.7",
+ "k256",
+ "serde",
+ "serde_derive",
+ "sha3",
+ "solana-signature",
+]
+
+[[package]]
+name = "solana-secp256k1-recover"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce0d2e8d53946f04e789476d6c407e4997b7fb412483df5fc10c075c65cb2edf"
+dependencies = [
+ "k256",
+ "solana-define-syscall 5.2.0",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "solana-secp256r1-program"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445d8e12592631d76fc4dc57858bae66c9fd7cc838c306c62a472547fc9d0ce6"
+dependencies = [
+ "bytemuck",
+ "openssl",
+ "solana-instruction",
+ "solana-sdk-ids",
+]
+
+[[package]]
+name = "solana-seed-phrase"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc905b200a95f2ea9146e43f2a7181e3aeb55de6bc12afb36462d00a3c7310de"
+dependencies = [
+ "hmac 0.12.1",
+ "pbkdf2",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "solana-serialize-utils"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "761357b0853c9623bf12c1d2314b3d6160a85b087b84c45224fb85766d22616b"
+dependencies = [
+ "solana-instruction-error",
+ "solana-pubkey",
+ "solana-sanitize",
+]
+
+[[package]]
+name = "solana-sha256-hasher"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f"
+dependencies = [
+ "sha2 0.10.9",
+ "solana-define-syscall 4.0.1",
+ "solana-hash",
+]
+
+[[package]]
+name = "solana-sha512-hasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11e10e103ab5bd52af341e7bc2f4123a2f4e66bb7ba4e6ca40f1e00cd6314e02"
+dependencies = [
+ "sha2 0.10.9",
+ "solana-define-syscall 5.2.0",
+ "solana-hash-512",
+]
+
+[[package]]
+name = "solana-short-vec"
+version = "3.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647"
+dependencies = [
+ "serde_core",
+ "wincode",
+]
+
+[[package]]
+name = "solana-signature"
+version = "3.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0364c7577c3c82a693ce28a1febc8d1b5d1b0a175fdc2114ae6186b69effe1e"
+dependencies = [
+ "ed25519-dalek 2.2.0",
+ "five8",
+ "rand 0.9.5",
+ "serde",
+ "serde-big-array",
+ "serde_derive",
+ "solana-sanitize",
+ "wincode",
+]
+
+[[package]]
+name = "solana-signer"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520bd6021163ee517f4bdc7ae03ded904f97e11320001ba0b3355f45eb14f558"
+dependencies = [
+ "solana-pubkey",
+ "solana-signature",
+ "solana-transaction-error",
+]
+
+[[package]]
+name = "solana-slot-hashes"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7ce2b4b8911bf2db3de7b6266e67bfc21a6a9f8c566fb096d9782ca2ad16ee"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-hash",
+ "solana-sdk-ids",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-slot-history"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40427c04d3e808493cb5e3d1a97cef84d7c15cb6f89b15c5684d0d4027105600"
+dependencies = [
+ "bv",
+ "serde",
+ "serde_derive",
+ "solana-get-sysvar",
+ "solana-sdk-ids",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-stable-layout"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9f6a291ba063a37780af29e7db14bdd3dc447584d8ba5b3fc4b88e2bbc982fa"
+dependencies = [
+ "solana-instruction",
+ "solana-pubkey",
+]
+
+[[package]]
+name = "solana-stake-history"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c736a6aa0e53b9d264d90b06589fc0996f49f882f3e71842ed754fc57ffc1a43"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-clock",
+ "solana-get-sysvar",
+ "solana-sdk-ids",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-stake-interface"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f49eb5c77484214c3484921e2cdda79d185373118b5458c1b2df0f1a04c3bc30"
+dependencies = [
+ "num-traits",
+ "solana-clock",
+ "solana-instruction",
+ "solana-program-error",
+ "solana-pubkey",
+ "solana-system-interface",
+]
+
+[[package]]
+name = "solana-svm"
+version = "4.1.1"
+dependencies = [
+ "ahash",
+ "bincode",
+ "env_logger",
+ "magic-root-interface",
+ "qualifier_attr",
+ "rand 0.9.5",
+ "serde",
+ "solana-account",
+ "solana-clock",
+ "solana-ed25519-program",
+ "solana-epoch-schedule",
+ "solana-fee-calculator",
+ "solana-fee-structure",
+ "solana-hash",
+ "solana-instruction",
+ "solana-instructions-sysvar",
+ "solana-keypair",
+ "solana-loader-v3-interface",
+ "solana-message",
+ "solana-native-token",
+ "solana-precompile-error",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-rent",
+ "solana-sbpf",
+ "solana-sdk-ids",
+ "solana-signature",
+ "solana-signer",
+ "solana-svm",
+ "solana-svm-callback",
+ "solana-svm-feature-set",
+ "solana-svm-log-collector",
+ "solana-svm-transaction",
+ "solana-svm-type-overrides",
+ "solana-system-interface",
+ "solana-sysvar",
+ "solana-transaction",
+ "solana-transaction-context",
+ "solana-transaction-error",
+]
+
+[[package]]
+name = "solana-svm-callback"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3170a3cfc032f3efca975154dee904ecefea5ae11fbd50787c062886196d32c1"
+dependencies = [
+ "solana-account",
+ "solana-clock",
+ "solana-precompile-error",
+ "solana-pubkey",
+]
+
+[[package]]
+name = "solana-svm-feature-set"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3d427bb7cd5182365e7e89d73fcfcb54565d15445ad2d228921811c3836099"
+
+[[package]]
+name = "solana-svm-log-collector"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afdf3074910afe016266bf73606724543b6829b5656221c8060ebcfcc844b39a"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "solana-svm-measure"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4ecaaadf4ddaabdba865c7f9aade0c51ac7c393c786f8b4e9590127f5ee9d62"
+
+[[package]]
+name = "solana-svm-timings"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b179027c8bf618df4a7f02f937ddef54770f8f5584195c94a03437ee6dc7c7f3"
+dependencies = [
+ "eager",
+ "enum-iterator",
+ "solana-pubkey",
+]
+
+[[package]]
+name = "solana-svm-transaction"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fa27dd1eb7ce4d339cf9dfc1dfb70866488e4e1436fb05728aea7af0185191a"
+dependencies = [
+ "solana-hash",
+ "solana-message",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-signature",
+ "solana-transaction",
+]
+
+[[package]]
+name = "solana-svm-type-overrides"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e0ca52c449480ab3a1ef614ae10576d6ccc33730a0f4d8b4a69f38e8decb622"
+dependencies = [
+ "rand 0.9.5",
+ "shuttle",
+]
+
+[[package]]
+name = "solana-syscalls"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d855514df361d211bd254929ab816321f3bd136f3b985605c68ba68a1b1d729"
+dependencies = [
+ "bincode",
+ "libsecp256k1",
+ "num-traits",
+ "solana-account",
+ "solana-account-info",
+ "solana-big-mod-exp",
+ "solana-blake3-hasher",
+ "solana-bls12-381-syscall",
+ "solana-bn254",
+ "solana-clock",
+ "solana-cpi",
+ "solana-curve25519",
+ "solana-hash",
+ "solana-hash-512",
+ "solana-instruction",
+ "solana-keccak-hasher",
+ "solana-poseidon",
+ "solana-program-entrypoint",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sbpf",
+ "solana-sdk-ids",
+ "solana-secp256k1-recover",
+ "solana-sha256-hasher",
+ "solana-sha512-hasher",
+ "solana-stable-layout",
+ "solana-stake-interface",
+ "solana-svm-feature-set",
+ "solana-svm-log-collector",
+ "solana-svm-type-overrides",
+ "solana-sysvar",
+ "solana-sysvar-id",
+ "solana-transaction-context",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "solana-system-interface"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55b54965bf0b76fa8e2b35376583efddd4d916618cfe595bf48c7d7b55a9e628"
+dependencies = [
+ "num-traits",
+ "serde",
+ "serde_derive",
+ "solana-address",
+ "solana-instruction",
+ "solana-msg",
+ "solana-program-error",
+ "wincode",
+]
+
+[[package]]
+name = "solana-system-program"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84eaae98e1576c1b490760d102ca15cb0cfc7ed2b52012dc0d06698a2529e093"
+dependencies = [
+ "bincode",
+ "log",
+ "solana-account",
+ "solana-bincode",
+ "solana-fee-calculator",
+ "solana-instruction",
+ "solana-nonce",
+ "solana-nonce-account",
+ "solana-packet",
+ "solana-program-runtime",
+ "solana-pubkey",
+ "solana-sdk-ids",
+ "solana-svm-log-collector",
+ "solana-system-interface",
+ "solana-sysvar",
+ "solana-transaction-context",
+]
+
+[[package]]
+name = "solana-sysvar"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada7045bbfdd802af08fbc9c7e2b56ddabb20599d22e4c3840fcef5e7afb8324"
+dependencies = [
+ "base64",
+ "bincode",
+ "bytemuck",
+ "bytemuck_derive",
+ "lazy_static",
+ "serde",
+ "serde_derive",
+ "solana-account-info",
+ "solana-clock",
+ "solana-define-syscall 5.2.0",
+ "solana-epoch-rewards",
+ "solana-epoch-schedule",
+ "solana-fee-calculator",
+ "solana-get-sysvar",
+ "solana-hash",
+ "solana-instruction",
+ "solana-last-restart-slot",
+ "solana-program-entrypoint",
+ "solana-program-error",
+ "solana-program-memory",
+ "solana-pubkey",
+ "solana-rent",
+ "solana-sdk-ids",
+ "solana-sdk-macro",
+ "solana-slot-hashes",
+ "solana-slot-history",
+ "solana-stake-history",
+ "solana-sysvar-id",
+]
+
+[[package]]
+name = "solana-sysvar-id"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de"
+dependencies = [
+ "solana-address",
+ "solana-sdk-ids",
+]
+
+[[package]]
+name = "solana-transaction"
+version = "4.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a98253cefd62dea714b67926d3d918c3b9f7d66993f1f2322ac7549ea991dc3e"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-address",
+ "solana-hash",
+ "solana-instruction",
+ "solana-instruction-error",
+ "solana-message",
+ "solana-sanitize",
+ "solana-sdk-ids",
+ "solana-short-vec",
+ "solana-signature",
+ "solana-signer",
+ "solana-transaction-error",
+ "wincode",
+]
+
+[[package]]
+name = "solana-transaction-context"
+version = "4.1.1"
+dependencies = [
+ "bincode",
+ "serde",
+ "solana-account",
+ "solana-account-info",
+ "solana-instruction",
+ "solana-instructions-sysvar",
+ "solana-program-entrypoint",
+ "solana-pubkey",
+ "solana-rent",
+ "solana-sbpf",
+ "solana-sdk-ids",
+ "solana-system-interface",
+ "solana-transaction-context",
+ "static_assertions",
+]
+
+[[package]]
+name = "solana-transaction-error"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a949797bc1ac31836d0070e791083028a8c2e0d493fa4cf51c0bed7a04c65c22"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "solana-instruction-error",
+ "solana-sanitize",
+ "wincode",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synchronoise"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2"
+dependencies = [
+ "crossbeam-queue",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "test-case"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
+dependencies = [
+ "test-case-macros",
+]
+
+[[package]]
+name = "test-case-core"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
+dependencies = [
+ "cfg-if",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "test-case-macros"
+version = "3.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "test-case-core",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+dependencies = [
+ "thiserror-impl 2.0.19",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.13+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "twox-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unreachable"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
+dependencies = [
+ "void",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "v42-calculator-interface"
+version = "0.1.0"
+dependencies = [
+ "solana-instruction",
+ "solana-pubkey",
+]
+
+[[package]]
+name = "v42-calculator-program"
+version = "0.1.0"
+dependencies = [
+ "solana-account-info",
+ "solana-cpi",
+ "solana-instruction",
+ "solana-msg",
+ "solana-program-entrypoint",
+ "solana-program-error",
+ "solana-pubkey",
+ "solana-sysvar",
+ "v42-calculator-interface",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "wincode"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a"
+dependencies = [
+ "pastey",
+ "proc-macro2",
+ "quote",
+ "thiserror 2.0.19",
+ "wincode-derive",
+]
+
+[[package]]
+name = "wincode-derive"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 85ad1f11..c0d6e21a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,22 @@
[workspace]
+members = [
+ "accountsdb",
+ "engine",
+ "keeper",
+ "ledger",
+ "nucleus",
+ "processor",
+ "programs/magic-root-interface",
+ "programs/magic-root-program",
+ "programs/v42-calculator-interface",
+ "programs/v42-calculator-program",
+ "replicator",
+ "solana/account",
+ "solana/program-runtime",
+ "solana/svm",
+ "solana/transaction-context",
+ "solana/transaction-view"
+]
resolver = "3"
[workspace.package]
@@ -10,11 +28,127 @@ repository = "https://github.com/magicblock-labs/engine"
rust-version = "1.94.1"
version = "0.1.0"
+[workspace.dependencies]
+accountsdb = { path = "accountsdb", package = "magicblock-accountsdb" }
+engine = { path = "engine", package = "magicblock-engine" }
+keeper = { path = "keeper", package = "magicblock-keeper" }
+ledger = { path = "ledger", package = "magicblock-ledger" }
+magic-root-interface = { path = "programs/magic-root-interface" }
+magic-root-program = { path = "programs/magic-root-program" }
+nucleus = { path = "nucleus", package = "magicblock-engine-nucleus" }
+processor = { path = "processor", package = "magicblock-processor" }
+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"
+flume = { version = "0.12" }
+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"
+rustix = { version = "1.1.4" }
+scc = "3.8.4"
+serde = "1.0.228"
+serde_bytes = "0.11.19"
+serde_with = { version = "3.21.0", default-features = false }
+smallvec = "1.15.2"
+snedfile = "0.1"
+tar = "0.4.45"
+tempfile = "3"
+thiserror = "2.0.17"
+tokio = "1.52.1"
+tokio-util = "0.7.18"
+tracing = "0.1.44"
+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 }
+
+agave-feature-set = "4.1.1"
+agave-precompiles = "4.1.1"
+agave-syscalls = { package = "solana-syscalls", version = "=4.1.1", default-features = false }
+agave-transaction-view = "4.1.1"
+solana-account-info = "3.1.1"
+solana-clock = "3.1.0"
+solana-compute-budget-instruction = "=4.1.1"
+solana-compute-budget-program = "=4.1.1"
+solana-cpi = "3.1.0"
+solana-ed25519-program = "3.0.0"
+solana-epoch-rewards = "3.0.1"
+solana-epoch-schedule = "3.1.0"
+solana-feature-gate-interface = "4.0.0"
+solana-fee-calculator = "=3.2.2"
+solana-fee-structure = "3.0.0"
+solana-hash = "4.3.0"
+solana-instruction = "=3.4.0"
+solana-instruction-error = "=2.4.0"
+solana-instructions-sysvar = "4.0.0"
+solana-keypair = "3.1.2"
+solana-last-restart-slot = "3.0.0"
+solana-loader-v3-interface = "7.0.0"
+solana-message = "4.1.1"
+solana-msg = "3.1.0"
+solana-native-token = "3.0.0"
+solana-packet = "=4.2.0"
+solana-precompile-error = "3.0.0"
+solana-program-entrypoint = "3.1.1"
+solana-program-error = "3.0.1"
+solana-pubkey = "=4.2.0"
+solana-rent = "4.2.0"
+solana-sbpf = "=0.21.0"
+solana-sdk-ids = "3.1.0"
+solana-short-vec = "=3.2.2"
+solana-signature = "=3.4.1"
+solana-signer = "3.0.1"
+solana-slot-hashes = "3.0.1"
+solana-stable-layout = "3.0.1"
+solana-svm-callback = "4.1.1"
+solana-svm-feature-set = "4.1.1"
+solana-svm-log-collector = "4.1.1"
+solana-svm-measure = "4.1.1"
+solana-svm-timings = "4.1.1"
+solana-svm-transaction = "4.1.1"
+solana-svm-type-overrides = "4.1.1"
+solana-system-interface = "=3.2.0"
+solana-system-program = "=4.1.1"
+solana-sysvar = "4.0.0"
+solana-sysvar-id = "3.1.0"
+solana-transaction = "4.1.1"
+solana-transaction-error = "=3.3.1"
+
+[patch.crates-io]
+agave-transaction-view = { path = "solana/transaction-view" }
+solana-account = { path = "solana/account" }
+solana-program-runtime = { path = "solana/program-runtime" }
+solana-svm = { path = "solana/svm" }
+solana-transaction-context = { path = "solana/transaction-context" }
+
[workspace.lints.rust]
missing_docs = "deny"
rust_2018_idioms = { level = "warn", priority = -1 }
unreachable_pub = "warn"
-unsafe_op_in_unsafe_fn = "allow"
unused_lifetimes = "warn"
unused_macro_rules = "warn"
unused_qualifications = "warn"
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..5f8eb4dd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,353 @@
+
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 replace accounts directly, use `Engine::account(pubkey)`. `create`, `update`,
+and `delete` each run as one signed, committed transaction and require the local
+signer to match the engine authority.
+
+```rust
+use solana_account::{AccountBuilder, 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::ReadOnly)
+ .slot(1)
+ .data(vec![1, 2, 3, 4])
+ .build();
+
+engine.account(key).create(account, None).await?;
+
+let replacement = AccountBuilder::default()
+ .lamports(2_000_000)
+ .owner(owner)
+ .mode(AccountMode::ReadOnly)
+ .slot(2)
+ .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. Complete-account patches cover non-flag fields, and
+finalization atomically installs the caller-supplied flags without changing
+lamports. Callers are responsible for supplying current state; later
+replacements remain subject to the account's slot and lifecycle rules. Internal
+create composition places post-finalize instructions immediately after
+finalization.
+
+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 dedicated Tokio channels 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.expect("account stream is open");
+let block = blocks.recv().await.expect("block stream is open");
+```
+
+Related accessors subscribe to program-owned accounts, cache evictions,
+snapshot completion, transaction status, logs, processed transactions, and
+service messages. Signatures use terminal oneshot channels; other multicast
+streams give each consumer a bounded queue and disconnect a consumer that falls
+behind. Processed transactions, service messages, and cache evictions each have
+one process-lifetime consumer and apply producer backpressure when its queue is
+full.
+
+---
+
+## 🩹 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..7b3c95c2
--- /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(feature = "testkit")]
+const INDEX_MAP_SIZE: usize = nucleus::MB;
+#[cfg(not(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..26ce556b
--- /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(feature = "testkit")]
+pub(crate) const STORAGE_BLOCK: u64 = 16 * MB as u64;
+#[cfg(not(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(feature = "testkit")]
+const MMAP_SIZE: usize = 64 * MB;
+#[cfg(not(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/clippy.toml b/clippy.toml
index db02f67e..19eebeb6 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -19,7 +19,6 @@ max-struct-bools = 2
check-private-items = true
disallowed-methods = [
- { path = "std::option::Option::unwrap", reason = "Use expect with context or handle the None case explicitly" },
- { path = "std::result::Result::unwrap", reason = "Use expect with context or propagate the error explicitly" },
- { path = "std::thread::sleep", reason = "Avoid timing-based flakiness in workspace code; isolate retries/backoff behind abstractions" }
+ { path = "std::thread::sleep", reason = "Avoid timing-based flakiness in workspace code; use event driven logic" },
+ { path = "tokio::time::sleep", reason = "Avoid timing-based flakiness in workspace code; use event driven logic" }
]
diff --git a/engine/Cargo.toml b/engine/Cargo.toml
new file mode 100644
index 00000000..00dc5f68
--- /dev/null
+++ b/engine/Cargo.toml
@@ -0,0 +1,61 @@
+[package]
+name = "magicblock-engine"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+version.workspace = true
+
+[lib]
+name = "engine"
+
+[features]
+testkit = ["keeper/testkit", "nucleus/testkit", "tokio/time"]
+
+[dependencies]
+keeper = { workspace = true }
+ledger = { workspace = true }
+magic-root-interface = { workspace = true }
+magic-root-program = { workspace = true }
+nucleus = { workspace = true, features = ["config", "shutdown"] }
+processor = { workspace = true }
+
+derive_more = { workspace = true }
+num_cpus = { workspace = true }
+oneshot = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["sync"] }
+tracing = { workspace = true }
+wincode = { workspace = true }
+
+agave-transaction-view = { workspace = true }
+solana-account = { workspace = true }
+solana-compute-budget-program = { workspace = true, features = ["agave-unstable-api"] }
+solana-instruction = { workspace = true }
+solana-keypair = { workspace = true }
+solana-message = { workspace = true }
+solana-program-runtime = { workspace = true }
+solana-pubkey = { workspace = true }
+solana-sdk-ids = { workspace = true }
+solana-signer = { workspace = true }
+solana-system-program = { workspace = true, features = ["agave-unstable-api"] }
+solana-transaction = { workspace = true, features = ["wincode"] }
+
+[dev-dependencies]
+keeper = { workspace = true, features = ["testkit"] }
+magicblock-engine = { path = ".", features = ["testkit"] }
+nucleus = { workspace = true, features = ["testkit"] }
+v42-calculator-interface = { workspace = true, features = ["builder"] }
+
+solana-instruction-error = { workspace = true }
+solana-packet = { workspace = true }
+solana-signer = { workspace = true }
+solana-system-interface = { workspace = true, features = ["bincode"] }
+solana-sysvar = { workspace = true }
+tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread", "sync", "time"] }
+
+[lints]
+workspace = true
diff --git a/engine/README.md b/engine/README.md
new file mode 100644
index 00000000..c68ac2e4
--- /dev/null
+++ b/engine/README.md
@@ -0,0 +1,62 @@
+# `magicblock-engine`
+
+This crate exposes `Engine`, the consumer-facing handle over keeper state,
+transaction sequencing, simulation, block pacing, recovery, and MagicRoot
+account operations. It registers MagicRoot and the System Program as native
+builtins before keeper opens startup state.
+
+`Engine::signer` is always the local keypair. `Engine::authority` returns the
+configured remote authority for a replica, or the local identity when no
+override is configured. Replication uses that distinction to sign locally while
+authenticating its immediate upstream.
+
+## Account replacement
+
+`AccountAccessor::{create, update}` composes complete-account MagicRoot patch
+transactions. Replacement slots are monotonic: a newer slot is accepted, an
+equal slot requires a genuine account-mode transition, and an older slot is
+rejected even when the mode changes. Failed replacements are transactionally
+rolled back. Complete-account patch sequences cover non-flag fields, while
+finalization atomically installs the caller-supplied complete flag value without
+changing lamports. Callers are responsible for supplying current state; later
+replacements remain subject to the account's slot and lifecycle rules. `create`
+appends any `PostFinalize` actions immediately after finalization in the same
+transaction. Magicblock construction rejects instruction, address, account-meta,
+and instruction-data lengths that cannot be represented by the V1 wire fields.
+
+## Startup and recovery
+
+Keeper restores an accountsdb snapshot when the active store is corrupt, its
+sealed superblock trails the retained ledger, or its committed transaction count
+trails the ledger's durable count. Accountsdb's count is a checkpoint high-water
+mark, so a count ahead of the locally retained ledger is current, including for
+snapshots staged by a replication follower. Superblock lag remains recoverable
+independently of the counters.
+
+If accountsdb then trails the ledger tip, `Engine::new` replays retained entries
+from the successor of its sealed snapshot through a temporary sequencer. Replay
+quiesces at superblock seals and compares the reconstructed checksum with the
+recorded seal. A mismatch returns `ReplayError::StateMismatch`. Current state
+opens without replay when its slot and transaction count are each at least the
+ledger values. After replay actually runs, the final transaction counts must be
+equal or startup returns `ReplayError::StateMismatch`.
+
+Internal pacing appends one reset marker at the current slot and clears
+chain-mirrored volatile accounts before the pacemaker task starts. Internal
+system accounts remain available. Replicas use external pacing and retain
+restored volatile state. External block producers supply the slot and timestamp;
+the sequencer overwrites hash-chain metadata with its locally computed hash and
+parent.
+
+## Shutdown
+
+Shutdown behavior follows the pacing source. Internal pacing publishes a final
+block and flushes durable state. External pacing flushes the durable cursor
+before writing `CURRENT/volatile.db`, allowing the next open and replication
+handshake to resume from matching state. The pacemaker holds the sequencer
+barrier while issuing a terminal ledger sync, which closes the appender and
+reader workers without waiting for every engine handle to be dropped.
+
+The embedding service retains the `ShutdownManager` passed to `Engine::new` and
+calls `terminate` after stopping external ingress. The manager stops the
+replication client, pacemaker, sequencer, and backing services in order.
diff --git a/engine/src/accessor.rs b/engine/src/accessor.rs
new file mode 100644
index 00000000..87116c34
--- /dev/null
+++ b/engine/src/accessor.rs
@@ -0,0 +1,106 @@
+//! Account- and transaction-scoped operation facades.
+
+use std::{sync::atomic::Ordering, time::Duration};
+
+use keeper::{ExecutionRecord, TransactionView};
+use magic_root_interface::MagicRootInstruction;
+use processor::{SequencerMessage, Simulation, SimulatorMessage};
+use solana_account::OwnedAccount;
+use solana_instruction::Instruction;
+use solana_pubkey::Pubkey;
+use solana_transaction::TransactionResult;
+use tokio::time;
+
+use crate::{Engine, error::EngineError, error::Result, transaction};
+
+/// Upper bound on awaiting a submitted transaction's committed result.
+const EXECUTION_TIMEOUT: Duration = Duration::from_secs(8);
+
+/// Account-scoped operations bound to a single `pubkey`.
+pub struct AccountAccessor<'a> {
+ pub(crate) pubkey: Pubkey,
+ pub(crate) engine: &'a Engine,
+}
+
+/// Transaction-submission operations bound to an engine instance.
+pub struct TransactionAccessor<'a> {
+ pub(crate) engine: &'a Engine,
+ pub(crate) transaction: TransactionView,
+}
+
+impl AccountAccessor<'_> {
+ /// Creates the account by patching in every field and finalizing it,
+ /// optionally running follow-up `actions` once it is finalized.
+ pub async fn create(
+ &self,
+ acc: impl Into,
+ actions: Option>,
+ ) -> Result<()> {
+ let mut instructions = MagicRootInstruction::compose_account(self.pubkey, acc.into())?;
+ if let Some(actions) = actions {
+ instructions.push(MagicRootInstruction::PostFinalize(actions).compose(self.pubkey)?);
+ }
+ self.execute(instructions).await
+ }
+
+ /// Updates the account by patching in every field of `account`
+ pub async fn update(&self, acc: impl Into) -> Result<()> {
+ let instructions = MagicRootInstruction::compose_account(self.pubkey, acc.into())?;
+ self.execute(instructions).await
+ }
+
+ /// Closes the account.
+ pub async fn delete(&self) -> Result<()> {
+ let instructions = vec![MagicRootInstruction::Delete.compose(self.pubkey)?];
+ self.execute(instructions).await
+ }
+
+ /// Composes the instructions into a signed engine transaction, executes it,
+ /// and flattens the committed transaction result into the engine error type.
+ async fn execute(&self, instructions: Vec) -> Result<()> {
+ let txn = transaction::magicblock(&instructions, self.engine)?;
+ self.engine.transaction(txn)?.execute().await?.map_err(Into::into)
+ }
+}
+
+impl TransactionAccessor<'_> {
+ /// Submits `transaction` for execution and awaits its committed result.
+ /// A timeout does not cancel the submitted transaction.
+ pub async fn execute(self) -> Result> {
+ if self.engine.terminating.load(Ordering::Acquire) {
+ return Err(EngineError::ShuttingDown);
+ }
+ let signature = self.transaction.signatures()[0];
+ let msg = SequencerMessage::Transaction(self.transaction);
+ let rx = self.engine.transactions().subscribe_signature(signature).await;
+ self.engine.sequencer.send(msg).await?;
+ let status = time::timeout(EXECUTION_TIMEOUT, rx)
+ .await
+ .map_err(|_| EngineError::TransactionTimeout)?
+ .map_err(|e| e.to_string())?;
+ Ok(status.result)
+ }
+
+ /// Submits `transaction` for execution without awaiting its result.
+ pub async fn schedule(self) -> Result<()> {
+ if self.engine.terminating.load(Ordering::Acquire) {
+ return Err(EngineError::ShuttingDown);
+ }
+ let msg = SequencerMessage::Transaction(self.transaction);
+ self.engine.sequencer.send(msg).await.map_err(Into::into)
+ }
+
+ /// Simulates `transaction` against current state without committing it.
+ pub async fn simulate(self) -> Result> {
+ if self.engine.terminating.load(Ordering::Acquire) {
+ return Err(EngineError::ShuttingDown);
+ }
+ let (response, rx) = oneshot::channel();
+ let msg = SimulatorMessage::Transaction(Simulation {
+ transaction: self.transaction,
+ response,
+ });
+ self.engine.sequencer.simulation.send(msg).await?;
+ rx.await.map_err(Into::into)
+ }
+}
diff --git a/engine/src/error.rs b/engine/src/error.rs
new file mode 100644
index 00000000..0e297bee
--- /dev/null
+++ b/engine/src/error.rs
@@ -0,0 +1,91 @@
+//! Engine error types.
+
+use agave_transaction_view::result::TransactionViewError;
+use derive_more::From;
+use keeper::error::KeeperError;
+use ledger::{LedgerError, LedgerRequestError};
+use nucleus::shutdown::Service;
+use processor::ProcessorError;
+use solana_message::CompileError;
+use solana_transaction::{InstructionError, SignerError, TransactionError};
+use tokio::sync::mpsc::error::SendError;
+
+/// Result type used by engine APIs.
+pub type Result = std::result::Result;
+
+/// Failures surfaced by the top-level engine.
+#[derive(From, thiserror::Error, Debug)]
+pub enum EngineError {
+ /// A durable-state (keeper) operation failed.
+ #[error("state error: {0}")]
+ State(#[source] KeeperError),
+ /// Scheduling or executing a transaction failed.
+ #[error("processor error: {0}")]
+ Processor(#[source] ProcessorError),
+ /// Replaying the ledger into volatile state on startup failed.
+ #[error("replay error: {0}")]
+ Replay(#[source] ReplayError),
+ /// A background service is no longer reachable.
+ #[error("service became unavailable: {0:?}")]
+ ServiceUnavailable(Service),
+ /// The engine has begun coordinated shutdown and rejects new work.
+ #[error("engine is shutting down")]
+ ShuttingDown,
+ /// Timed out waiting for a submitted transaction's committed result.
+ #[error("timed out waiting for transaction result")]
+ TransactionTimeout,
+ /// Signing a transaction with the engine authority failed.
+ #[error("signature error: {0}")]
+ Signature(#[source] SignerError),
+ /// Serializing or deserializing a transaction failed.
+ #[error("serialization error: {0}")]
+ Serde(#[source] wincode::Error),
+ /// Sanitizing a serialized transaction into a transaction view failed.
+ #[error("transaction sanitization: {0:?}")]
+ Sanitization(TransactionViewError),
+ /// Compiling instructions into a versioned transaction message failed.
+ #[error("transaction compilation failed: {0}")]
+ TransactionCompile(#[source] CompileError),
+ /// A submitted transaction carried an invalid signature.
+ #[error("transaction signature verification failed")]
+ SignatureVerification,
+ /// A submitted transaction was committed with an execution failure.
+ #[error("transaction execution failed: {0}")]
+ TransactionExecution(#[source] TransactionError),
+ /// An unexpected internal failure carrying a contextual message.
+ #[error("internal error: {0}")]
+ Internal(String),
+}
+
+impl From> for EngineError {
+ fn from(_: SendError) -> Self {
+ Self::ServiceUnavailable(Service::Sequencer)
+ }
+}
+impl From for EngineError {
+ fn from(error: InstructionError) -> Self {
+ Self::TransactionExecution(TransactionError::InstructionError(0, error))
+ }
+}
+impl From for EngineError {
+ fn from(_: oneshot::RecvError) -> Self {
+ Self::ServiceUnavailable(Service::Sequencer)
+ }
+}
+
+/// Failures raised while replaying retained ledger entries on startup.
+#[derive(From, thiserror::Error, Debug)]
+pub enum ReplayError {
+ /// A retained transaction could not be sanitized into a transaction view.
+ #[error("transaction sanitization: {0:?}")]
+ Sanitization(TransactionViewError),
+ /// The replayed account state checksum diverged from the sealed superblock.
+ #[error("replayed state checksum mismatch")]
+ StateMismatch,
+ /// Waiting for the ledger reader's replay response failed.
+ #[error("ledger replay request failed: {0}")]
+ Request(#[source] LedgerRequestError),
+ /// Reading or decoding retained ledger entries failed.
+ #[error("ledger replay failed: {0}")]
+ Ledger(#[source] LedgerError),
+}
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
new file mode 100644
index 00000000..954737d0
--- /dev/null
+++ b/engine/src/lib.rs
@@ -0,0 +1,220 @@
+#![doc = include_str!("../README.md")]
+
+use std::{
+ sync::{
+ Arc,
+ atomic::{AtomicBool, Ordering},
+ },
+ time::Instant,
+};
+
+use derive_more::Deref;
+use keeper::{Keeper, builder::KeeperBuilder, error::KeeperError};
+use ledger::schema::OwnedBlockstoreEntry;
+use magic_root_program::entrypoint::MagicRootEntrypoint;
+use nucleus::{
+ runtime::{self, BarrierHandle, SequencerHandle},
+ shutdown::{Service, ShutdownManager, ShutdownReason},
+};
+use processor::{SequencerMessage, sequencer::Sequencer};
+use solana_compute_budget_program::Entrypoint as ComputeBudgetEntrypoint;
+use solana_program_runtime::{
+ loaded_programs::{ProgramCache, ProgramCacheEntry},
+ solana_sbpf::program::BuiltinFunctionDefinition,
+};
+use solana_pubkey::Pubkey;
+use solana_system_program::system_processor::Entrypoint as SystemProgramEntrypoint;
+use tracing::{error, info};
+
+mod accessor;
+mod error;
+pub mod pacemaker;
+mod transaction;
+
+#[cfg(feature = "testkit")]
+pub mod testkit;
+
+pub use accessor::{AccountAccessor, TransactionAccessor};
+pub use error::{EngineError, ReplayError, Result};
+pub use transaction::IntoTransactionView;
+
+use crate::pacemaker::{ExternalPacer, PaceMaker};
+
+/// Top-level engine handle: owns the durable state and the sequencer submission
+/// channels.
+#[derive(Deref, Clone)]
+pub struct Engine {
+ /// Durable engine state (accountsdb + ledger), shared across components.
+ #[deref]
+ state: Arc,
+ /// Submission handle into the sequencer's execution and simulation channels.
+ sequencer: SequencerHandle,
+ /// Rejects new transactions once coordinated shutdown begins.
+ terminating: Arc,
+}
+
+impl Engine {
+ /// Builds and starts the engine.
+ ///
+ /// Opens durable state through the keeper builder (coming up on persisted
+ /// state), replays retained ledger entries to rebuild volatile state only when
+ /// recovering from a rewound accountsdb, starts the live sequencer, and spawns
+ /// the pacemaker using the builder's blockstore timing.
+ pub async fn new(
+ mut builder: KeeperBuilder,
+ pacer: Option,
+ shutdown: &mut ShutdownManager,
+ ) -> Result {
+ let cache = Arc::new(ProgramCache::default());
+ let cpus = (num_cpus::get().saturating_sub(2)).max(2);
+
+ builder.builtins.insert(
+ magic_root_interface::ID,
+ (MagicRootEntrypoint::vm, MagicRootEntrypoint::codegen),
+ );
+ builder.builtins.insert(
+ solana_system_program::id(),
+ (
+ SystemProgramEntrypoint::vm,
+ SystemProgramEntrypoint::codegen,
+ ),
+ );
+ builder.builtins.insert(
+ solana_sdk_ids::compute_budget::id(),
+ (
+ ComputeBudgetEntrypoint::vm,
+ ComputeBudgetEntrypoint::codegen,
+ ),
+ );
+
+ for (&id, builtin) in &builder.builtins {
+ let entry = ProgramCacheEntry::new_builtin(*builtin);
+ cache.assign_program(id, entry.into());
+ }
+ let blockstore = builder.blockstore;
+ let state = Arc::new(builder.build(shutdown).await?);
+ Self::try_replay(&state, &cache, cpus).await?;
+ let (service, sequencer) = Sequencer::new(cpus / 2, state.clone(), cache, shutdown, false)?;
+ service.spawn()?;
+ let terminating = Arc::new(AtomicBool::new(false));
+ let engine = Self { state, sequencer, terminating };
+ PaceMaker::spawn(engine.clone(), pacer, blockstore, shutdown)?;
+ info!(authority = %engine.authority(), cpus, "engine started");
+ Ok(engine)
+ }
+
+ /// Quiesces execution and closes durable state.
+ ///
+ /// `dump` serializes chain-mirrored state for an externally paced replica to
+ /// restore on its next open. Internally paced leaders clear that state once
+ /// during startup instead and only flush durable state here.
+ pub async fn shutdown(&self, dump: bool) -> Result<()> {
+ info!(dump, "shutting down the engine");
+ self.terminating.store(true, Ordering::Release);
+ let _guard = self.barrier().await?;
+ if dump {
+ self.accounts().dump(None).map_err(KeeperError::from)?;
+ }
+ self.sync(true).map_err(Into::into)
+ }
+
+ /// Returns an accessor for mutating the account at `pubkey`.
+ pub fn account(&self, pubkey: Pubkey) -> AccountAccessor<'_> {
+ AccountAccessor { engine: self, pubkey }
+ }
+
+ /// Returns an accessor for signing and submitting transactions.
+ pub fn transaction(&self, transaction: T) -> Result>
+ where
+ T: IntoTransactionView,
+ {
+ let transaction = transaction.compose(self)?;
+ Ok(TransactionAccessor { engine: self, transaction })
+ }
+
+ /// Drains in-flight execution and keeps the sequencer paused until the handle is dropped.
+ pub async fn barrier(&self) -> Result {
+ let (controller, guard) = runtime::barrier();
+ self.sequencer.send(SequencerMessage::Barrier(guard)).await?;
+ controller.acknowledged.await?;
+ Ok(controller.released)
+ }
+
+ /// Applies one retained ledger entry through the engine's ordered paths.
+ ///
+ /// Seal and reset entries quiesce execution before touching shared state;
+ /// a reconstructed seal whose checksum differs returns
+ /// [`ReplayError::StateMismatch`].
+ pub async fn replay(&self, entry: OwnedBlockstoreEntry) -> Result<()> {
+ match entry {
+ OwnedBlockstoreEntry::Transaction(txn) => self.transaction(txn)?.schedule().await?,
+ OwnedBlockstoreEntry::Block(block) => {
+ self.sequencer.send(SequencerMessage::Block(block)).await?;
+ }
+ OwnedBlockstoreEntry::Superblock(expected) => {
+ let _guard = self.barrier().await?;
+ let previous = self.superblocks().sealed().id;
+ self.accounts().set_superblock(expected.id);
+ self.sync(false)?;
+ let observed = self.superblocks().sealed();
+ if observed != expected {
+ error!(?observed, ?expected, "state mismatch; aborting replay");
+ self.accounts().set_superblock(previous);
+ self.sync(false)?;
+ Err(ReplayError::StateMismatch)?;
+ }
+ }
+ OwnedBlockstoreEntry::Reset(slot) => {
+ let _guard = self.barrier().await?;
+ self.reset(slot)?;
+ }
+ };
+ Ok(())
+ }
+
+ /// Rebuilds state through a temporary replay sequencer when accountsdb trails
+ /// the retained ledger, then stops every temporary service before returning.
+ async fn try_replay(state: &Arc, cache: &Arc, cpus: usize) -> Result<()> {
+ let timer = Instant::now();
+ let Some(mut replayer) = state.replay().await? else {
+ return Ok(());
+ };
+ let mut shutdown = ShutdownManager::default();
+ let mut sh = shutdown.handle(Service::LedgerReplayer);
+ let (service, sequencer) =
+ Sequencer::new(cpus, state.clone(), cache.clone(), &mut shutdown, true)?;
+ service.spawn()?;
+ let engine = Self {
+ state: state.clone(),
+ sequencer,
+ terminating: Default::default(),
+ };
+ while let Some(entry) = replayer.rx.recv().await {
+ engine.replay(entry).await?;
+ }
+ replayer
+ .response
+ .recv_timeout()
+ .await
+ .map_err(ReplayError::from)?
+ .map_err(ReplayError::from)?;
+
+ drop(engine.barrier().await?);
+ engine.sync(false)?;
+ let accountsdb = state.accounts().transactions();
+ let ledger = state.ledger().transactions();
+ if accountsdb != ledger {
+ error!(
+ accountsdb,
+ ledger, "transaction count mismatch; aborting replay"
+ );
+ Err(ReplayError::StateMismatch)?;
+ }
+
+ let slot = state.blocks().latest().slot;
+ info!(slot, duration = ?timer.elapsed(), "ledger replay complete");
+ sh.terminate(ShutdownReason::Signalled);
+ shutdown.terminate().await;
+ Ok(())
+ }
+}
diff --git a/engine/src/pacemaker.rs b/engine/src/pacemaker.rs
new file mode 100644
index 00000000..dbea204c
--- /dev/null
+++ b/engine/src/pacemaker.rs
@@ -0,0 +1,190 @@
+//! Block-boundary pacing.
+
+use std::{num::NonZeroU64, time::Duration};
+
+use derive_more::Deref;
+use ledger::schema::Block;
+use nucleus::{
+ Slot,
+ config::BlockstoreParams,
+ shutdown::{Service, ShutdownHandle, ShutdownManager, ShutdownReason},
+ unix_time,
+};
+use processor::{SequencerMessage, SimulatorMessage};
+use tokio::{
+ sync::mpsc::Receiver,
+ time::{self, Interval, MissedTickBehavior},
+};
+use tracing::error;
+
+use crate::{Engine, Result};
+
+/// Channel used by external block producers.
+pub type ExternalPacer = Receiver;
+
+/// Emits block boundaries into engine execution paths.
+#[derive(Deref)]
+pub struct PaceMaker {
+ /// Engine handle used to submit each boundary.
+ #[deref]
+ engine: Engine,
+ /// Source for the next block boundary.
+ pacer: Pacer,
+ /// Number of slots sealed into each superblock.
+ superblock: NonZeroU64,
+}
+
+/// Source of block boundaries.
+pub enum Pacer {
+ /// Interval-driven slot production.
+ Internal(BlockTicker),
+ /// Externally supplied block boundaries.
+ External(ExternalPacer),
+}
+
+/// State for interval-driven slot production.
+pub struct BlockTicker {
+ /// Next slot to emit.
+ slot: Slot,
+ /// Block production interval.
+ ticker: Interval,
+}
+
+impl BlockTicker {
+ /// Builds an interval ticker starting at the engine's current slot.
+ pub(crate) fn new(engine: &Engine, blocktime: Duration) -> Self {
+ let slot = engine.blocks().current_slot();
+ let mut ticker = time::interval(blocktime);
+ ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
+ ticker.reset();
+ BlockTicker { slot, ticker }
+ }
+
+ /// Returns the next block boundary and advances the slot cursor.
+ pub(crate) fn block(&mut self) -> Block {
+ let time = unix_time().as_secs() as i64;
+ let block = Block::new(self.slot, time);
+ self.slot += 1;
+ block
+ }
+}
+
+/// Block boundary submitted by an external producer.
+///
+/// The caller supplies its slot and timestamp. The sequencer overwrites the
+/// hash and parent with locally computed hash-chain metadata.
+pub struct ExternalBlock {
+ /// Boundary to enqueue.
+ pub block: Block,
+ /// Notified after the pacemaker handles the boundary locally.
+ ///
+ /// On ordinary slots this means the boundary was queued and the keeper slot
+ /// was advanced. On superblock slots it also includes the synchronous seal.
+ pub submitted: oneshot::Sender<()>,
+}
+
+impl ExternalBlock {
+ /// Pairs a boundary with the receiver signalled once the pacemaker has locally
+ /// handled it, letting the submitter await ordered application.
+ pub fn new(block: Block) -> (Self, oneshot::Receiver<()>) {
+ let (submitted, guard) = oneshot::channel();
+ let block = Self { block, submitted };
+ (block, guard)
+ }
+}
+
+impl PaceMaker {
+ /// Registers and starts the pacemaker task.
+ ///
+ /// Uses an external block source when supplied. Otherwise it records one
+ /// reset at the keeper's current slot, clears chain-mirrored volatile state,
+ /// and starts emitting slots on the configured block interval.
+ pub fn spawn(
+ engine: Engine,
+ pacer: Option,
+ blockstore: BlockstoreParams,
+ shutdown: &mut ShutdownManager,
+ ) -> Result<()> {
+ let pacer = match pacer {
+ Some(rx) => Pacer::External(rx),
+ None => {
+ let ticker = BlockTicker::new(&engine, blockstore.blocktime);
+ engine.reset(ticker.slot)?;
+ Pacer::Internal(ticker)
+ }
+ };
+ let shutdown = shutdown.handle(Service::PaceMaker);
+ let superblock = blockstore.superblock;
+ let pacemaker = Self { engine, pacer, superblock };
+ tokio::spawn(pacemaker.run(shutdown));
+ Ok(())
+ }
+
+ /// Paces block boundaries until shutdown or the block source is exhausted.
+ ///
+ /// Shutdown follows the pacing mode. Internal pacing publishes one last
+ /// block and flushes durable state. External pacing also checkpoints
+ /// volatile state alongside its durable cursor for the next upstream
+ /// handshake.
+ async fn run(mut self, mut shutdown: ShutdownHandle) {
+ let mut res = loop {
+ let next = tokio::select! {
+ biased;
+ _ = shutdown.signalled() => None,
+ next = self.next() => next,
+ };
+ let Some((block, submission)) = next else {
+ break Ok(());
+ };
+ if let Err(error) = self.handle(block).await {
+ break Err(error);
+ }
+ if let Some(submission) = submission {
+ let _ = submission.send(());
+ }
+ };
+ res = if let Pacer::Internal(ref mut t) = self.pacer {
+ // Await every shutdown step even after an earlier failure.
+ let b = t.block();
+ res.and(self.handle(b).await).and(self.shutdown(false).await)
+ } else {
+ res.and(self.shutdown(true).await)
+ };
+ // Release engine storage before the manager can reopen it.
+ drop(self);
+ if let Err(error) = res {
+ error!(?error, "pace maker terminated with critical failure");
+ shutdown.terminate(ShutdownReason::Error(error.into()));
+ } else {
+ shutdown.terminate(ShutdownReason::Signalled);
+ }
+ }
+
+ /// Waits for the next block boundary without applying it.
+ async fn next(&mut self) -> Option<(Block, Option>)> {
+ match &mut self.pacer {
+ Pacer::Internal(t) => {
+ t.ticker.tick().await;
+ Some((t.block(), None))
+ }
+ Pacer::External(rx) => rx.recv().await.map(|msg| (msg.block, Some(msg.submitted))),
+ }
+ }
+
+ /// Advances the execution and simulation environments to `block`, sealing a
+ /// superblock when the slot lands on the configured interval.
+ ///
+ /// The seal is taken behind a barrier and runs synchronously: it exports an
+ /// accountsdb snapshot, which is only coherent while no store operation can
+ /// race it. Holding the boundary here is what buys that exclusivity, at the
+ /// cost of stalling block production until the seal completes.
+ async fn handle(&self, block: Block) -> Result<()> {
+ self.sequencer.send(SequencerMessage::Block(block)).await?;
+ self.sequencer.simulation.send(SimulatorMessage::Block(block)).await?;
+ if block.slot.is_multiple_of(self.superblock.get()) {
+ let _guard = self.barrier().await?;
+ self.finalize_superblock()?;
+ }
+ Ok(())
+ }
+}
diff --git a/engine/src/testkit.rs b/engine/src/testkit.rs
new file mode 100644
index 00000000..7939834c
--- /dev/null
+++ b/engine/src/testkit.rs
@@ -0,0 +1,177 @@
+//! Shared black-box harness for engine-backed integration suites.
+//!
+//! Builds a real [`Engine`] over [`keeper::testkit`] directories with internal or
+//! externally controlled pacing. Compiled only under the `testkit` feature.
+#![allow(clippy::expect_used, clippy::unwrap_used)]
+
+use std::{path::PathBuf, sync::Arc, time::Duration};
+
+use derive_more::Deref;
+use keeper::{
+ ExecutionRecord,
+ builder::KeeperBuilder,
+ testkit::{Dirs, SUPERBLOCK, await_archive, block, keeper_builder},
+};
+use nucleus::{Slot, config::Authority, ledger::BlockstorePosition, shutdown::ShutdownManager};
+use solana_account::AccountSharedData;
+use solana_keypair::Keypair;
+use solana_pubkey::Pubkey;
+use solana_transaction::TransactionResult;
+use tokio::{sync::mpsc, time};
+
+use crate::{Engine, IntoTransactionView, pacemaker::ExternalBlock};
+
+const TIMEOUT: Duration = Duration::from_secs(4);
+
+/// Block pacing for a [`TestEngine`].
+pub enum Pacing {
+ /// The test supplies blocks through [`TestEngine::pacer`].
+ External,
+ /// The engine runs its own pacemaker.
+ Internal,
+}
+
+/// A running engine plus its deterministic pacing and lifecycle handles.
+#[derive(Deref)]
+pub struct TestEngine {
+ #[deref]
+ engine: Engine,
+ shutdown: ShutdownManager,
+ authority: Authority,
+ dirs: Dirs,
+ pacer: Option>,
+ slot: Slot,
+}
+
+impl TestEngine {
+ /// Starts the standard test engine on fresh directories.
+ pub async fn new() -> Self {
+ Self::with(Dirs::default(), Arc::new(Keypair::new())).await
+ }
+
+ /// Starts the standard test engine over `dirs` with `authority`.
+ pub async fn with(dirs: Dirs, authority: impl Into) -> Self {
+ Self::try_with(dirs, authority).await.unwrap()
+ }
+
+ /// Fallible [`Self::with`], used when startup failure is the assertion.
+ pub async fn try_with(dirs: Dirs, authority: impl Into) -> crate::Result {
+ let mut builder = keeper_builder(&dirs);
+ builder.authority = authority.into();
+ Self::try_from_builder(dirs, builder, Pacing::External).await
+ }
+
+ /// Starts an engine from a caller-configured keeper builder.
+ ///
+ /// `dirs` must own the directories referenced by `builder` and outlive the
+ /// resulting engine.
+ pub async fn from_builder(dirs: Dirs, builder: KeeperBuilder, pacing: Pacing) -> Self {
+ Self::try_from_builder(dirs, builder, pacing).await.unwrap()
+ }
+
+ /// Fallible [`Self::from_builder`].
+ pub async fn try_from_builder(
+ dirs: Dirs,
+ builder: KeeperBuilder,
+ pacing: Pacing,
+ ) -> crate::Result {
+ let authority = builder.authority.clone();
+ let (pacer, rx) = match pacing {
+ Pacing::External => {
+ let (tx, rx) = mpsc::channel(64);
+ (Some(tx), Some(rx))
+ }
+ Pacing::Internal => (None, None),
+ };
+ let mut shutdown = ShutdownManager::default();
+ let engine = Engine::new(builder, rx, &mut shutdown).await?;
+ let slot = engine.blocks().current_slot();
+ Ok(Self {
+ engine,
+ shutdown,
+ authority,
+ dirs,
+ pacer,
+ slot,
+ })
+ }
+
+ /// Cloneable external pacemaker sender for services under test.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the engine is internally paced.
+ pub fn pacer(&self) -> mpsc::Sender {
+ self.pacer.clone().expect("engine is externally paced")
+ }
+
+ /// Mutable lifecycle manager used to register or await test services.
+ pub fn shutdown(&mut self) -> &mut ShutdownManager {
+ &mut self.shutdown
+ }
+
+ /// Drains engine work, flushes queued ledger appends, and returns the durable cursor.
+ pub async fn sync(&self) -> BlockstorePosition {
+ drop(self.barrier().await.unwrap());
+ self.superblocks().sync(false).unwrap();
+ self.superblocks().position()
+ }
+
+ /// Full committed account, or `None` when absent/closed.
+ pub fn get_account(&self, key: Pubkey) -> Option {
+ self.engine.accounts().loader().load(&key).unwrap()
+ }
+
+ /// Executes instructions and returns the committed transaction result.
+ pub async fn execute(&self, txn: impl IntoTransactionView) -> TransactionResult<()> {
+ self.transaction(txn).unwrap().execute().await.unwrap()
+ }
+
+ /// Simulates instructions without committing them.
+ pub async fn simulate(
+ &self,
+ txn: impl IntoTransactionView,
+ ) -> TransactionResult {
+ self.transaction(txn).unwrap().simulate().await.unwrap()
+ }
+
+ /// Schedules instructions without awaiting commit.
+ pub async fn schedule(&self, txn: impl IntoTransactionView) {
+ self.transaction(txn).unwrap().schedule().await.unwrap();
+ }
+
+ /// Advances `n` block boundaries.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the engine is internally paced.
+ pub async fn advance(&mut self, n: u64) {
+ for _ in 0..n {
+ let (block, submitted) = ExternalBlock::new(block(self.slot));
+ self.pacer().send(block).await.unwrap();
+ time::timeout(TIMEOUT, submitted)
+ .await
+ .expect("pacemaker accepts the block in time")
+ .expect("pacemaker reports block submission");
+ self.slot += 1;
+ }
+ }
+
+ /// Seals the next superblock and waits for its snapshot archive.
+ pub async fn seal_and_archive(&mut self) -> PathBuf {
+ let boundary = self.slot.next_multiple_of(SUPERBLOCK.into());
+ while self.slot <= boundary {
+ self.advance(1).await;
+ }
+ await_archive(self).await
+ }
+
+ /// Stops every service and returns the directories and authority for reopen.
+ pub async fn close(self) -> (Dirs, Authority) {
+ let Self {
+ mut shutdown, dirs, authority, ..
+ } = self;
+ shutdown.terminate().await;
+ (dirs, authority)
+ }
+}
diff --git a/engine/src/transaction.rs b/engine/src/transaction.rs
new file mode 100644
index 00000000..d4be48c3
--- /dev/null
+++ b/engine/src/transaction.rs
@@ -0,0 +1,122 @@
+//! Composing values into sanitized transaction views.
+
+use agave_transaction_view::{
+ MAGICBLOCK_INSTRUCTION_TRACE_LENGTH, MAX_MAGICBLOCK_ACCOUNT_LOCKS,
+ transaction_version::{MAGICBLOCK_PREFIX, TransactionVersion},
+};
+use keeper::TransactionView;
+use solana_instruction::Instruction;
+use solana_message::{
+ VersionedMessage,
+ v1::{self, SIGNATURE_SIZE},
+};
+use solana_signer::Signer;
+use solana_transaction::{Message, Transaction, TransactionError, versioned::VersionedTransaction};
+
+use crate::{Engine, error::EngineError, error::Result};
+
+/// Conversion of anything composable into an executable
+/// transaction into a sanitized [`TransactionView`].
+pub trait IntoTransactionView {
+ /// Composes `self` into a sanitized [`TransactionView`], signing with
+ /// `engine`'s authority and latest blockhash where applicable.
+ fn compose(self, engine: &Engine) -> Result;
+}
+
+impl IntoTransactionView for Message {
+ fn compose(self, engine: &Engine) -> Result {
+ let mut transaction = Transaction::new_unsigned(self);
+ transaction.try_sign(&[engine.signer()], engine.blockhash())?;
+ transaction.compose(engine)
+ }
+}
+
+impl IntoTransactionView for Transaction {
+ fn compose(self, engine: &Engine) -> Result {
+ let data = wincode::serialize(&self).map_err(wincode::Error::from)?;
+ data.compose(engine)
+ }
+}
+
+impl IntoTransactionView for &[Instruction] {
+ fn compose(self, engine: &Engine) -> Result {
+ let msg = Message::new(self, Some(&engine.authority()));
+ msg.compose(engine)
+ }
+}
+
+impl IntoTransactionView for &[Instruction; N] {
+ fn compose(self, engine: &Engine) -> Result {
+ self.as_slice().compose(engine)
+ }
+}
+
+impl IntoTransactionView for Vec {
+ fn compose(self, engine: &Engine) -> Result {
+ TransactionView::try_new_sanitized(self.into(), true)?.compose(engine)
+ }
+}
+
+impl IntoTransactionView for TransactionView {
+ fn compose(self, engine: &Engine) -> Result {
+ if matches!(self.version(), TransactionVersion::Magicblock)
+ && self.static_account_keys()[0] != engine.authority()
+ {
+ return Err(EngineError::SignatureVerification);
+ }
+ sigverify(&self)?;
+ Ok(self)
+ }
+}
+
+/// The engine's sole signature-verification point.
+///
+/// Execution is trustless: every submission funnels through the
+/// [`TransactionView`] `compose` and is verified here, including replay and
+/// replication of already-committed transactions. No path reaches the
+/// sequencer unverified, so downstream code may assume the fee payer and every
+/// required signer actually signed.
+fn sigverify(view: &TransactionView) -> Result<()> {
+ // Sanitization guarantees one static key for every required signature.
+ let message = view.message_data();
+ for (signature, key) in view.signatures().iter().zip(view.static_account_keys()) {
+ if !signature.verify(key.as_ref(), message) {
+ return Err(EngineError::SignatureVerification);
+ }
+ }
+ Ok(())
+}
+
+/// Composes an Engine-private transaction and signs its final
+/// Magicblock wire representation with the Engine authority.
+pub(crate) fn magicblock(instructions: &[Instruction], engine: &Engine) -> Result> {
+ let message = v1::Message::try_compile(&engine.authority(), instructions, engine.blockhash())?;
+ let message = VersionedMessage::V1(message);
+ // These checks are merely future proof defenses, currently it should be
+ // impossible to construct a transaction which might violate any of them
+ if message.instructions().len() > MAGICBLOCK_INSTRUCTION_TRACE_LENGTH {
+ Err(TransactionError::SanitizeFailure)?;
+ } else if message.static_account_keys().len() > MAX_MAGICBLOCK_ACCOUNT_LOCKS {
+ Err(TransactionError::TooManyAccountLocks)?;
+ }
+ for ix in message.instructions() {
+ if ix.accounts.len() > MAX_MAGICBLOCK_ACCOUNT_LOCKS {
+ Err(TransactionError::TooManyAccountLocks)?;
+ }
+ }
+
+ // Reserve the trailing signature slot without signing the V1 prefix, which
+ // is replaced below before the only signing operation.
+ let transaction = VersionedTransaction {
+ signatures: vec![Default::default()],
+ message,
+ };
+ let mut data = wincode::serialize(&transaction).map_err(wincode::Error::from)?;
+ // Patch the transaction prefix to allow for larger tranaction limits
+ data[0] = MAGICBLOCK_PREFIX;
+
+ let signature_offset = data.len() - SIGNATURE_SIZE;
+ let signature = engine.signer().sign_message(&data[..signature_offset]);
+ data[signature_offset..].copy_from_slice(signature.as_ref());
+ Ok(data)
+}
diff --git a/engine/tests/accounts.rs b/engine/tests/accounts.rs
new file mode 100644
index 00000000..28e0e739
--- /dev/null
+++ b/engine/tests/accounts.rs
@@ -0,0 +1,460 @@
+//! Account CRUD through the MagicRoot builtin — the privileged mutation path
+//! exposed by `AccountAccessor`. This path is untested below the engine: it needs
+//! the always-on MagicRoot builtin plus the executor's per-thread authority
+//! (MagicRoot authorizes the transaction's fee payer against it). Asserts the
+//! create/update/delete round-trip and the sponsor-balance invariant, and
+//! that post-finalize actions actually run.
+#![cfg(test)]
+
+use engine::{Engine, EngineError, testkit::TestEngine};
+use keeper::testkit::{
+ V42_ID, load_v42_data, load_v42_lamports, patterned_bytes, store_v42, v42_builder,
+};
+use magic_root_interface::MagicRootInstruction;
+use solana_account::{AccountBuilder, AccountMode, OwnedAccount, ReadableAccount};
+use solana_instruction_error::InstructionError;
+use solana_pubkey::Pubkey;
+use solana_system_interface::MAX_PERMITTED_DATA_LENGTH;
+use solana_sysvar::rent::Rent;
+use solana_transaction::TransactionError;
+use v42_calculator_interface::builder::{Expr as E, transfer};
+
+/// Rent-exempt for the data sizes used below; the SVM rejects a created account
+/// that falls under the rent floor.
+const LAMPORTS: u64 = 2_000_000;
+const SLOT: u64 = 42;
+
+/// Account with explicit lifecycle state, funded at the shared rent-exempt balance.
+fn account(owner: Pubkey, data: Vec, mode: AccountMode, slot: u64) -> OwnedAccount {
+ AccountBuilder::default()
+ .lamports(LAMPORTS)
+ .owner(owner)
+ .mode(mode)
+ .slot(slot)
+ .data(data)
+ .build()
+}
+
+/// Delegated account with `data` at `slot`.
+fn delegated(owner: Pubkey, data: Vec, slot: u64) -> OwnedAccount {
+ account(owner, data, AccountMode::Delegated, slot)
+}
+
+/// Materializes `mode`, entering transient through its required delegated state.
+async fn create_with(engine: &Engine, key: Pubkey, owner: Pubkey, mode: AccountMode) {
+ let initial = if mode == AccountMode::Transient {
+ delegated(owner, vec![1], SLOT - 1)
+ } else {
+ account(owner, vec![1], mode, SLOT)
+ };
+ engine
+ .account(key)
+ .create(initial, None)
+ .await
+ .expect("initial account is created");
+ if mode == AccountMode::Transient {
+ engine
+ .account(key)
+ .update(account(owner, vec![1], mode, SLOT))
+ .await
+ .expect("delegated account enters transient");
+ }
+}
+
+/// Asserts MagicRoot rejected the slot patch in a complete-account sequence.
+fn assert_non_advancing_slot(error: EngineError) {
+ let errored = matches!(
+ error,
+ EngineError::TransactionExecution(TransactionError::InstructionError(
+ 2,
+ InstructionError::InvalidArgument
+ ))
+ );
+ assert!(errored, "unexpected replacement error: {error:?}");
+}
+
+/// Asserts MagicRoot rejected the mode patch in a complete-account sequence.
+fn assert_invalid_mode_transition(error: EngineError) {
+ let errored = matches!(
+ error,
+ EngineError::TransactionExecution(TransactionError::InstructionError(
+ 1,
+ InstructionError::InvalidArgument
+ ))
+ );
+ assert!(errored, "unexpected replacement error: {error:?}");
+}
+
+// The full lifecycle. `create` materializes a fresh account by patching every
+// non-flag field, balancing lamport patches against the authority, then
+// finalizing its flags; `update` overwrites an existing account or materializes
+// a fresh key; and
+// `delete` closes it. Mutations here keep the balance constant after creation.
+#[tokio::test(flavor = "multi_thread")]
+async fn account_crud_lifecycle() {
+ let te = TestEngine::new().await;
+ let key = Pubkey::new_unique();
+ let owner = Pubkey::new_unique();
+
+ let created = account(
+ owner,
+ vec![1, 2, 3, 4, 5, 6, 7, 8],
+ AccountMode::ReadOnly,
+ 10,
+ );
+ let authority_before = te.get_account(te.authority()).expect("sponsor exists").lamports();
+
+ te.account(key).create(created, None).await.unwrap();
+
+ let acc = te.get_account(key).expect("created account exists");
+ assert_eq!(acc.lamports(), LAMPORTS);
+ assert_eq!(acc.owner(), &owner);
+ assert_eq!(acc.data(), &[1, 2, 3, 4, 5, 6, 7, 8]);
+ assert!(acc.is(AccountMode::ReadOnly));
+
+ let authority_after = te.get_account(te.authority()).expect("sponsor exists").lamports();
+ assert_eq!(
+ authority_before - authority_after,
+ LAMPORTS,
+ "the lamport patch sponsors the created balance from the authority"
+ );
+
+ // update overwrites the existing account in place: same-length data (the
+ // patch sequence replaces the exact data length) and identical lamports.
+ // Read-only accounts remain replaceable after finalization.
+ te.account(key)
+ .update(account(owner, vec![5; 16], AccountMode::ReadOnly, 11))
+ .await
+ .unwrap();
+ let acc = te.get_account(key).expect("still exists");
+ assert_eq!(acc.data(), &[5; 16], "update replaced the data wholesale");
+ assert_eq!(acc.owner(), &owner, "update replaced the patched owner");
+ assert!(acc.is(AccountMode::ReadOnly));
+
+ // delete: the account is gone from storage.
+ te.account(key).delete().await.unwrap();
+ assert!(te.get_account(key).is_none(), "deleted account is removed");
+
+ // update also materializes a fresh account the same way create does, minus
+ // the post-finalize actions.
+ let key2 = Pubkey::new_unique();
+ te.account(key2).update(delegated(owner, vec![3; 8], 10)).await.unwrap();
+ assert_eq!(te.get_account(key2).expect("materialized").data(), &[3; 8]);
+
+ te.close().await;
+}
+
+// Account cloning reconstructs every field and data chunk in one atomic private
+// transaction. Growing the same clone through the 64 KiB boundary and beyond,
+// then shrinking it below the boundary and to empty, proves replacement keeps
+// the exact data length. The caller supplies each successive current state.
+#[tokio::test(flavor = "multi_thread")]
+async fn account_clone_create_and_update_accept_large_data() {
+ const MAX_DATA_LEN: usize = 128 * 1024 + 1;
+
+ let te = TestEngine::new().await;
+ let key = Pubkey::new_unique();
+ let owner = Pubkey::new_unique();
+ let lamports = Rent::default().minimum_balance(MAX_DATA_LEN);
+
+ for (index, (len, seed)) in [
+ (u16::MAX as usize, 1),
+ (64 * 1024, 2),
+ (MAX_DATA_LEN, 3),
+ (32 * 1024, 4),
+ (0, 5),
+ ]
+ .into_iter()
+ .enumerate()
+ {
+ let data = patterned_bytes(len, seed);
+ let account = AccountBuilder::default()
+ .lamports(lamports)
+ .owner(owner)
+ .mode(AccountMode::ReadOnly)
+ .slot(SLOT + index as u64)
+ .data(data.clone());
+
+ if index == 0 {
+ te.account(key).create(account, None).await.unwrap();
+ } else {
+ te.account(key).update(account).await.unwrap();
+ }
+
+ let stored = te.get_account(key).expect("large account exists");
+ assert_eq!(stored.lamports(), lamports);
+ assert_eq!(stored.owner(), &owner);
+ assert!(stored.is(AccountMode::ReadOnly));
+ assert_eq!(stored.slot(), SLOT + index as u64);
+ assert_eq!(stored.data(), data);
+ }
+
+ te.close().await;
+}
+
+/// Proves an exact maximum-sized Solana account can run a PostFinalize SBPF
+/// action above trace index 64, while 257 subsequent V42 self-CPIs hit the CPI
+/// trace limit and roll back both the account creation and an earlier action.
+#[tokio::test(flavor = "multi_thread")]
+async fn account_create_accepts_max_data_with_post_finalize() {
+ const CPI_CALLS: usize = 257;
+
+ let te = TestEngine::new().await;
+ let key = Pubkey::new_unique();
+ let source = store_v42(&te, 7, AccountMode::Delegated);
+ let output = store_v42(&te, 0, AccountMode::Ephemeral);
+ let data = patterned_bytes(MAX_PERMITTED_DATA_LENGTH as usize, 42);
+ let account = AccountBuilder::default()
+ .lamports(Rent::default().minimum_balance(data.len()))
+ .owner(Pubkey::new_unique())
+ .mode(AccountMode::Delegated)
+ .slot(SLOT)
+ .data(data.clone());
+ let action = transfer(source, output, 1);
+
+ te.account(key)
+ .create(account, Some(vec![action]))
+ .await
+ .expect("maximum-sized account and post-finalize action execute atomically");
+
+ let stored = te.get_account(key).expect("maximum-sized account exists");
+ assert_eq!(stored.data().len(), MAX_PERMITTED_DATA_LENGTH as usize);
+ assert!(stored.data() == data, "maximum-sized account data differs");
+ assert_eq!(load_v42_data(&te, source), Some(6));
+ assert_eq!(load_v42_data(&te, output), Some(1));
+
+ let failed_key = Pubkey::new_unique();
+ let failed_account = AccountBuilder::default()
+ .lamports(Rent::default().minimum_balance(data.len()))
+ .owner(Pubkey::new_unique())
+ .mode(AccountMode::Delegated)
+ .slot(SLOT)
+ .data(data);
+ let excessive_cpis = (1..CPI_CALLS)
+ .fold(E::lit(1).cpi(), |expr, _| expr + E::lit(1).cpi())
+ .compose(output, &[]);
+ let error = te
+ .account(failed_key)
+ .create(
+ failed_account,
+ Some(vec![transfer(source, output, 1), excessive_cpis]),
+ )
+ .await
+ .expect_err("257 V42 self-CPIs exceed the trace limit");
+
+ assert!(
+ matches!(
+ error,
+ EngineError::TransactionExecution(TransactionError::InstructionError(
+ _,
+ InstructionError::MaxInstructionTraceLengthExceeded
+ ))
+ ),
+ "unexpected CPI trace error: {error:?}"
+ );
+ assert!(
+ te.get_account(failed_key).is_none(),
+ "failed creation was rolled back"
+ );
+ assert_eq!(load_v42_data(&te, source), Some(6));
+ assert_eq!(load_v42_data(&te, output), Some(1));
+
+ te.close().await;
+}
+
+/// Proves program-cache entries follow the complete v42 account lifecycle:
+/// transaction-local deletion hides a loaded program immediately but rolls back
+/// on a later instruction failure, while committed deletion evicts the shared
+/// entry so invalid executable data restored at the same key cannot use stale code.
+#[tokio::test(flavor = "multi_thread")]
+async fn account_program_cache_tracks_v42_lifecycle() {
+ let te = TestEngine::new().await;
+ let seeded = te.get_account(V42_ID).expect("v42 program is seeded");
+ let program = Pubkey::new_unique();
+ let closeable = AccountBuilder::from(seeded.clone())
+ .mode(AccountMode::Ephemeral)
+ .slot(seeded.slot() + 1);
+ te.account(program).create(closeable, None).await.unwrap();
+
+ let output = Pubkey::new_unique();
+ te.accounts()
+ .store(&[(
+ output,
+ v42_builder(0, AccountMode::Ephemeral).owner(program).build(),
+ )])
+ .unwrap();
+ let invoke = |value| {
+ let mut instruction = E::lit(value).compose(output, &[]);
+ instruction.program_id = program;
+ instruction.accounts.last_mut().unwrap().pubkey = program;
+ instruction
+ };
+
+ te.execute(&[invoke(42)])
+ .await
+ .expect("fresh v42 program executes and primes the shared cache");
+ assert_eq!(load_v42_data(&te, output), Some(42));
+
+ let delete = MagicRootInstruction::Delete.compose(program).unwrap();
+ assert_eq!(
+ te.execute(&[delete, invoke(7)]).await,
+ Err(TransactionError::InstructionError(
+ 1,
+ InstructionError::UnsupportedProgramId
+ )),
+ "deletion hides the program from later instructions in the transaction"
+ );
+ assert!(
+ te.get_account(program).is_some(),
+ "failed transaction rolls back account deletion"
+ );
+
+ te.execute(&[invoke(7)])
+ .await
+ .expect("rolled-back deletion preserves the shared cache entry");
+ assert_eq!(load_v42_data(&te, output), Some(7));
+
+ te.account(program).delete().await.unwrap();
+ assert!(
+ te.get_account(program).is_none(),
+ "committed deletion removes the account"
+ );
+
+ let invalid = AccountBuilder::from(seeded).mode(AccountMode::Ephemeral).data(vec![0]);
+ te.accounts().store(&[(program, invalid.build())]).unwrap();
+ assert_eq!(
+ te.execute(&[invoke(9)]).await,
+ Err(TransactionError::InstructionError(
+ 0,
+ InstructionError::UnsupportedProgramId
+ )),
+ "invalid restored executable cannot run through a stale compiled entry"
+ );
+
+ te.close().await;
+}
+
+// Complete replacements are monotonic by slot. An equal-slot replacement is
+// meaningful only when it performs a real lifecycle transition; mode is patched
+// before slot, and no-op mode writes deliberately leave the mode marker clean.
+#[tokio::test(flavor = "multi_thread")]
+async fn account_replacement_slot_ordering() {
+ let te = TestEngine::new().await;
+ let owner = Pubkey::new_unique();
+
+ for (from, to) in [
+ (AccountMode::ReadOnly, AccountMode::Delegated),
+ (AccountMode::Placeholder, AccountMode::Ephemeral),
+ ] {
+ let key = Pubkey::new_unique();
+ create_with(&te, key, owner, from).await;
+ te.account(key)
+ .update(account(owner, vec![2], to, SLOT))
+ .await
+ .expect("equal-slot mode transition is accepted");
+
+ let updated = te.get_account(key).expect("transitioned account exists");
+ assert!(updated.is(to), "{from:?} transitions to {to:?}");
+ assert_eq!(updated.slot(), SLOT);
+ assert_eq!(updated.data(), &[2]);
+ }
+
+ for (from, to) in [
+ (AccountMode::Placeholder, AccountMode::Transient),
+ (AccountMode::Ephemeral, AccountMode::Delegated),
+ (AccountMode::System, AccountMode::ReadOnly),
+ ] {
+ let key = Pubkey::new_unique();
+ // Seed the source directly so only the mode-transition invariant is
+ // under test.
+ te.accounts()
+ .store(&[(key, account(owner, vec![1], from, SLOT).into())])
+ .unwrap();
+ let error = te
+ .account(key)
+ .update(account(owner, vec![2], to, SLOT))
+ .await
+ .expect_err("invalid mode transition is rejected");
+ assert_invalid_mode_transition(error);
+
+ let unchanged = te.get_account(key).expect("rejected transition preserves the account");
+ assert!(unchanged.is(from), "{from:?} does not transition to {to:?}");
+ assert_eq!(unchanged.slot(), SLOT);
+ assert_eq!(unchanged.data(), &[1]);
+ }
+
+ let key = Pubkey::new_unique();
+ te.account(key)
+ .create(account(owner, vec![3], AccountMode::ReadOnly, SLOT), None)
+ .await
+ .expect("baseline account is created");
+
+ let error = te
+ .account(key)
+ .update(account(owner, vec![4], AccountMode::ReadOnly, SLOT))
+ .await
+ .expect_err("equal-slot replacement without a mode change is rejected");
+ assert_non_advancing_slot(error);
+
+ let error = te
+ .account(key)
+ .update(account(owner, vec![5], AccountMode::Delegated, SLOT - 1))
+ .await
+ .expect_err("a mode change never authorizes an older slot");
+ assert_non_advancing_slot(error);
+
+ let unchanged = te.get_account(key).expect("rejected replacements preserve the account");
+ assert!(unchanged.is(AccountMode::ReadOnly));
+ assert_eq!(unchanged.slot(), SLOT);
+ assert_eq!(unchanged.data(), &[3]);
+
+ te.close().await;
+}
+
+// Post-finalize actions are invoked via CPI after the account is finalized, so a
+// failing action aborts the whole creation (nothing commits), while a benign one
+// lets it through. The contrast proves the actions actually execute rather than
+// being silently dropped.
+#[tokio::test(flavor = "multi_thread")]
+async fn create_runs_post_finalize_actions() {
+ let te = TestEngine::new().await;
+
+ // A successful v42 transfer proves the post-finalize action ran after the
+ // new account became writable and program-owned.
+ let source = store_v42(&te, 0, AccountMode::Delegated);
+ let source_before = load_v42_lamports(&te, source).expect("source exists");
+ let ok_key = Pubkey::new_unique();
+ let acc = v42_builder(0, AccountMode::Delegated);
+ let benign = transfer(source, ok_key, 1);
+ te.account(ok_key)
+ .create(acc, Some(vec![benign]))
+ .await
+ .expect("create with a succeeding post-finalize action");
+ assert_eq!(
+ load_v42_lamports(&te, source).expect("source remains"),
+ source_before - 1,
+ "post-finalize action debited its source"
+ );
+ assert_eq!(
+ load_v42_lamports(&te, ok_key).expect("created account exists"),
+ source_before + 1,
+ "post-finalize action credited the created account"
+ );
+
+ // An overflowing v42 action errors; the failure propagates and rolls back
+ // the account creation in the same transaction.
+ let bad_key = Pubkey::new_unique();
+ let failing = (E::lit(i64::MIN) - E::lit(1)).compose(bad_key, &[]);
+ let acc = v42_builder(0, AccountMode::Delegated);
+ let result = te.account(bad_key).create(acc, Some(vec![failing])).await;
+ assert!(
+ result.is_err(),
+ "failing post-finalize action surfaces an error"
+ );
+ assert!(
+ te.get_account(bad_key).is_none(),
+ "nothing commits when the action fails"
+ );
+
+ te.close().await;
+}
diff --git a/engine/tests/builtins.rs b/engine/tests/builtins.rs
new file mode 100644
index 00000000..595da1c4
--- /dev/null
+++ b/engine/tests/builtins.rs
@@ -0,0 +1,62 @@
+//! Full-engine coverage for native builtins registered during startup.
+#![cfg(test)]
+
+use engine::testkit::TestEngine;
+use solana_account::{AccountBuilder, AccountMode, AccountSharedData, ReadableAccount};
+use solana_keypair::Keypair;
+use solana_pubkey::Pubkey;
+use solana_signer::Signer;
+use solana_system_interface::{
+ instruction::{allocate, assign, transfer},
+ program,
+};
+use solana_sysvar::rent::Rent;
+use solana_transaction::Transaction;
+
+#[tokio::test(flavor = "multi_thread")]
+async fn system_program_executes_transfer_allocate_and_assign() {
+ const LAMPORTS: u64 = 42;
+ const SPACE: usize = 8;
+
+ let te = TestEngine::new().await;
+ let source = te.authority();
+
+ let source_before = te.get_account(source).expect("authority account remains");
+ assert_eq!(source_before.owner(), &program::ID);
+
+ let destination = Keypair::new();
+ let destination_before = Rent::default().minimum_balance(SPACE);
+ let account: AccountSharedData = AccountBuilder::default()
+ .lamports(destination_before)
+ .mode(AccountMode::Delegated)
+ .build();
+ assert_eq!(account.owner(), &program::ID);
+ te.accounts().store(&[(destination.pubkey(), account)]).unwrap();
+
+ let owner = Pubkey::new_unique();
+ let instructions = [
+ transfer(&source, &destination.pubkey(), LAMPORTS),
+ allocate(&destination.pubkey(), SPACE as u64),
+ assign(&destination.pubkey(), &owner),
+ ];
+ let transaction = Transaction::new_signed_with_payer(
+ &instructions,
+ Some(&source),
+ &[te.signer(), &destination],
+ te.blockhash(),
+ );
+ te.execute(transaction).await.expect("failed to execute system ixs");
+
+ let source_after = te.get_account(source).expect("authority account remains");
+ assert_eq!(source_after.lamports(), source_before.lamports() - LAMPORTS);
+ assert_eq!(source_after.data(), source_before.data());
+ assert_eq!(source_after.owner(), source_before.owner());
+
+ let destination_after =
+ te.get_account(destination.pubkey()).expect("destination account remains");
+ assert_eq!(destination_after.lamports(), destination_before + LAMPORTS);
+ assert_eq!(destination_after.data(), &[0; SPACE]);
+ assert_eq!(destination_after.owner(), &owner);
+
+ te.close().await;
+}
diff --git a/engine/tests/recovery.rs b/engine/tests/recovery.rs
new file mode 100644
index 00000000..9130b995
--- /dev/null
+++ b/engine/tests/recovery.rs
@@ -0,0 +1,146 @@
+//! Full-engine replay recovery — the engine's most distinctive orchestration.
+//! After an accountsdb inconsistency the keeper restores an older archived snapshot,
+//! leaving durable state behind the ledger tip; the engine then spins a temporary
+//! replay sequencer to re-execute the retained ledger entries and rebuild the
+//! missing state, checksum-verified at each sealed superblock. Nothing below the
+//! engine wires this end to end. Covered here: the healthy restart that must not
+//! recover, the replay that crosses a sealed checksum and succeeds, and the
+//! replay that diverges from one and must refuse to start.
+#![cfg(test)]
+
+use std::{path::PathBuf, time::Duration};
+
+use engine::{EngineError, ReplayError, testkit::TestEngine};
+use keeper::testkit::{corrupt, load_v42_data, store_v42};
+use nucleus::ledger::ACCOUNTSDB_SNAPSHOT_FILE;
+use solana_account::AccountMode;
+use solana_pubkey::Pubkey;
+use tokio::time;
+use v42_calculator_interface::builder::Expr as E;
+
+/// Commits `K = value` through a full transaction and seals the following
+/// superblock, returning its archived snapshot path.
+async fn commit_and_seal(te: &mut TestEngine, key: Pubkey, value: i64) -> PathBuf {
+ te.execute(&[E::lit(value).compose(key, &[])]).await.unwrap();
+ te.seal_and_archive().await
+}
+
+// Replay must rebuild everything between the restored snapshot and the ledger
+// tip: dropping superblock 2's archive forces the restore back onto snapshot 1,
+// so re-executing B crosses superblock 2's sealed checksum (the verification
+// arm's happy path) before C is rebuilt from the unsealed head.
+#[tokio::test(flavor = "multi_thread")]
+async fn replay_rebuilds_state_after_counter_lag() {
+ let mut te = TestEngine::new().await;
+ let key = store_v42(&te, 0, AccountMode::Delegated);
+
+ // A: K = 10 sealed into superblock 1, whose snapshot the restore lands on.
+ let s1 = commit_and_seal(&mut te, key, 10).await;
+ assert!(s1.exists(), "archived accountsdb snapshot exists on disk");
+ assert!(
+ s1.ends_with(ACCOUNTSDB_SNAPSHOT_FILE),
+ "archive is the compressed accountsdb tarball"
+ );
+ // B: K = 20 sealed into superblock 2; C: K = 30 lives only in the ledger's
+ // unsealed head, past every archived snapshot.
+ let s2 = commit_and_seal(&mut te, key, 20).await;
+ te.execute(&[E::lit(30).compose(key, &[])]).await.expect("C commits");
+ te.advance(2).await;
+ let (dirs, authority) = te.close().await;
+
+ // Lag only accountsdb's durable checkpoint in the closed store, preserving
+ // valid account content and its checksum.
+ corrupt(dirs.accounts.path(), 32, 2);
+
+ // Drop the newest archive so recovery falls back to snapshot 1 (K = 10) and
+ // replays both a sealed successor and the unsealed ledger head.
+ std::fs::remove_file(&s2).unwrap();
+
+ let te2 = TestEngine::with(dirs, authority).await;
+ assert_eq!(
+ load_v42_data(&te2, key),
+ Some(30),
+ "both post-snapshot mutations were rebuilt purely from ledger replay"
+ );
+ // The temporary replay sequencer must hand off to a working live one.
+ te2.execute(&[E::lit(1).compose(key, &[])])
+ .await
+ .expect("engine is live after replay");
+
+ te2.close().await;
+}
+
+// A mutation that bypasses the ledger is sealed into superblock 2's checksum but
+// can never be rebuilt by replay, so the reopen must refuse to come up with
+// `StateMismatch` rather than run on quietly diverged state.
+#[tokio::test(flavor = "multi_thread")]
+async fn replay_aborts_on_checksum_mismatch() {
+ let mut te = TestEngine::new().await;
+ let key = store_v42(&te, 0, AccountMode::Delegated);
+ commit_and_seal(&mut te, key, 10).await;
+ // Direct store: lands in persisted state (and superblock 2's checksum)
+ // without a ledger entry.
+ store_v42(&te, 7, AccountMode::Delegated);
+ let s2 = commit_and_seal(&mut te, key, 20).await;
+ let (dirs, authority) = te.close().await;
+
+ corrupt(dirs.accounts.path(), 8, 0xABAB_ABAB_ABAB_ABAB);
+ std::fs::remove_file(&s2).unwrap();
+
+ let result = time::timeout(
+ Duration::from_secs(4),
+ TestEngine::try_with(dirs, authority),
+ )
+ .await
+ .expect("replay aborts in time");
+ let error = result.err().expect("diverged checksum refuses startup");
+ assert!(
+ matches!(error, EngineError::Replay(ReplayError::StateMismatch)),
+ "unexpected startup error: {error:?}"
+ );
+}
+
+// A healthy restart opens persisted state as-is and restores the clean-shutdown
+// volatile dump. A failed execution still counts on both durable sides without
+// writing accounts. The direct-stored delegated account exists in neither
+// snapshots nor ledger, while the read-only account exists only in the volatile
+// dump; the post-seal transaction write pins the persisted tip alongside them.
+#[tokio::test(flavor = "multi_thread")]
+async fn clean_restart_reopens_persisted_and_volatile_state() {
+ let mut te = TestEngine::new().await;
+ let key = store_v42(&te, 0, AccountMode::Delegated);
+ commit_and_seal(&mut te, key, 10).await;
+ te.execute(&[E::lit(20).compose(key, &[])]).await.unwrap();
+ let failed = (E::lit(i64::MIN) - E::lit(1)).compose(key, &[]);
+ assert!(
+ te.execute(&[failed]).await.is_err(),
+ "overflow execution fails"
+ );
+ assert_eq!(
+ load_v42_data(&te, key),
+ Some(20),
+ "failed execution writes no state"
+ );
+ let direct = store_v42(&te, 7, AccountMode::Delegated);
+ let volatile = store_v42(&te, 8, AccountMode::ReadOnly);
+ let (dirs, authority) = te.close().await;
+
+ let te2 = TestEngine::with(dirs, authority).await;
+ assert_eq!(
+ load_v42_data(&te2, key),
+ Some(20),
+ "persisted tip state reopened as-is"
+ );
+ assert_eq!(
+ load_v42_data(&te2, direct),
+ Some(7),
+ "ledger-invisible account intact, so no snapshot was restored"
+ );
+ assert_eq!(
+ load_v42_data(&te2, volatile),
+ Some(8),
+ "clean shutdown restores volatile state"
+ );
+
+ te2.close().await;
+}
diff --git a/engine/tests/security.rs b/engine/tests/security.rs
new file mode 100644
index 00000000..d8247a11
--- /dev/null
+++ b/engine/tests/security.rs
@@ -0,0 +1,185 @@
+//! Account-mutability enforcement at the engine boundary.
+//!
+//! The SVM lets a program write any account it owns; the engine's post-execution
+//! guard (`validate_access`) is what rejects writes to accounts that are not in a
+//! mutable mode, unless the whole transaction is privileged (every instruction
+//! targets MagicRoot). These black-box tests drive the full engine and assert both
+//! that the rejection surfaces the right error and that the illegal write never
+//! commits. A second enforcement path — MagicRoot's own `post_finalize` check —
+//! is covered by `post_finalize_immutable_action_is_rejected`.
+#![cfg(test)]
+
+use engine::{EngineError, testkit::TestEngine};
+use keeper::testkit::{load_v42_data, load_v42_lamports, signed_view, store_v42, v42_builder};
+use magic_root_interface::MagicRootInstruction;
+use solana_account::{AccountFieldPatch, AccountMode};
+use solana_instruction::Instruction;
+use solana_instruction_error::InstructionError;
+use solana_keypair::Keypair;
+use solana_pubkey::Pubkey;
+use solana_signer::Signer;
+use solana_transaction::TransactionError;
+use v42_calculator_interface::builder::{Expr as E, transfer};
+
+/// Complete v42 account replacement at an explicit non-default slot.
+fn compose_v42_replacement(key: Pubkey, mode: AccountMode, slot: u64) -> Vec {
+ let account = v42_builder(0, mode).slot(slot).build();
+ MagicRootInstruction::compose_account(key, account).unwrap()
+}
+
+// The SVM permits the v42 program to write accounts it owns, but the guard
+// rejects the commit and discards the mutation whenever the account is immutable
+// and the transaction is not privileged. Two branches: a writable operand yields
+// InvalidWritableAccount, the fee payer itself yields InvalidAccountForFee. A
+// delegated (mutable) account is the positive control.
+#[tokio::test(flavor = "multi_thread")]
+async fn immutable_writes_are_rejected_and_not_committed() {
+ let te = TestEngine::new().await;
+
+ // A writable, non-payer immutable source: the transfer dirties both balance
+ // fields before the guard rejects the source account's engine mode.
+ let operand = store_v42(&te, 5, AccountMode::ReadOnly);
+ let recipient = store_v42(&te, 0, AccountMode::Delegated);
+ let operand_before = load_v42_lamports(&te, operand).expect("operand exists");
+ let recipient_before = load_v42_lamports(&te, recipient).expect("recipient exists");
+ assert_eq!(
+ te.execute(&[transfer(operand, recipient, 1)]).await,
+ Err(TransactionError::InvalidWritableAccount)
+ );
+ assert_eq!(
+ load_v42_lamports(&te, operand).expect("operand remains"),
+ operand_before,
+ "immutable source debit discarded"
+ );
+ assert_eq!(
+ load_v42_lamports(&te, recipient).expect("recipient remains"),
+ recipient_before,
+ "recipient credit rolled back with the transaction"
+ );
+
+ // The immutable account is the fee payer itself. The harness `execute` always
+ // pays with the engine authority, so this branch needs a hand-signed
+ // transaction: message compilation merges the signer and the writable output
+ // into account 0. Fees are zero and this SVM does no fee-payer validation, so
+ // a program-owned payer loads as-is.
+ let payer = Keypair::new();
+ let acc = v42_builder(5, AccountMode::ReadOnly).build();
+ te.accounts().store(&[(payer.pubkey(), acc)]).unwrap();
+ let (_sig, view) = signed_view(&te, Some(&payer), E::lit(9).compose(payer.pubkey(), &[]));
+ let result = te.transaction(view).unwrap().execute().await.unwrap();
+ assert_eq!(result, Err(TransactionError::InvalidAccountForFee));
+ assert_eq!(
+ load_v42_data(&te, payer.pubkey()),
+ Some(5),
+ "fee-payer write discarded"
+ );
+
+ // Positive control: a delegated (mutable) account commits normally.
+ let mutable = store_v42(&te, 0, AccountMode::Delegated);
+ assert!(te.execute(&[E::lit(9).compose(mutable, &[])]).await.is_ok());
+ assert_eq!(
+ load_v42_data(&te, mutable),
+ Some(9),
+ "mutable write commits"
+ );
+
+ te.close().await;
+}
+
+// Post-finalize actions are invoked via CPI after an account is created, and
+// MagicRoot's `post_finalize` refuses to run them against a writable account that
+// is not mutable. Creating a ReadOnly account with an attached v42 write is
+// therefore rejected, and the whole creation rolls back — a distinct enforcement
+// path from `validate_access` (this fires inside the program, not after).
+#[tokio::test(flavor = "multi_thread")]
+async fn post_finalize_immutable_action_is_rejected() {
+ let te = TestEngine::new().await;
+
+ let key = Pubkey::new_unique();
+ let mut ixs = compose_v42_replacement(key, AccountMode::ReadOnly, 1);
+ let post_finalize_idx = ixs.len();
+ let post_finalize = MagicRootInstruction::PostFinalize(vec![E::lit(9).compose(key, &[])]);
+ ixs.push(post_finalize.compose(key).unwrap());
+ assert_eq!(
+ te.execute(ixs.as_slice()).await,
+ Err(TransactionError::InstructionError(
+ post_finalize_idx as u8,
+ InstructionError::Immutable
+ )),
+ "MagicRoot's PostFinalize guard rejects the immutable writable account"
+ );
+ assert!(
+ te.get_account(key).is_none(),
+ "the rejected creation commits nothing"
+ );
+
+ te.close().await;
+}
+
+/// Proves PostFinalize rejects a recursive MagicRoot instruction of a delegated
+/// account owned by an unrelated program and rolls back its creation.
+#[tokio::test(flavor = "multi_thread")]
+async fn post_finalize_rejects_magic_root_ix() {
+ let te = TestEngine::new().await;
+
+ let key = Pubkey::new_unique();
+ let owner = Pubkey::new_unique();
+ let account = v42_builder(0, AccountMode::Delegated).owner(owner);
+ let patch = MagicRootInstruction::Patch(AccountFieldPatch::DataAt {
+ offset: 0,
+ data: 9_i64.to_le_bytes().to_vec(),
+ })
+ .compose(key)
+ .unwrap();
+
+ let error = te
+ .account(key)
+ .create(account, Some(vec![patch]))
+ .await
+ .expect_err("PostFinalize rejects a recursive MagicRoot patch");
+ assert!(
+ matches!(
+ error,
+ EngineError::TransactionExecution(TransactionError::InstructionError(
+ _,
+ InstructionError::CallDepth
+ ))
+ ),
+ "unexpected recursive invocation error: {error:?}"
+ );
+ assert!(
+ te.get_account(key).is_none(),
+ "the rejected recursive action rolls back account creation"
+ );
+
+ te.close().await;
+}
+
+// Privilege cannot be laundered through account creation: a single transaction
+// that mixes MagicRoot's create-a-ReadOnly-account instructions with a top-level
+// (foreign) v42 write is not privileged — `is_privileged` requires *every*
+// instruction to be MagicRoot — so the guard runs and the whole transaction,
+// creation included, reverts.
+#[tokio::test(flavor = "multi_thread")]
+async fn mixed_foreign_write_on_created_readonly_is_rejected() {
+ let te = TestEngine::new().await;
+
+ let key = Pubkey::new_unique();
+ // A missing account starts as ReadOnly at slot zero. Advance the replacement
+ // slot so this test reaches the access guard rather than MagicRoot's
+ // duplicate-replacement guard.
+ let mut ixs = compose_v42_replacement(key, AccountMode::ReadOnly, 1);
+ // The foreign instruction that makes the whole transaction non-privileged.
+ ixs.push(E::lit(9).compose(key, &[]));
+
+ assert_eq!(
+ te.execute(ixs.as_slice()).await,
+ Err(TransactionError::InvalidWritableAccount)
+ );
+ assert!(
+ te.get_account(key).is_none(),
+ "the mixed transaction reverts wholesale"
+ );
+
+ te.close().await;
+}
diff --git a/engine/tests/transactions.rs b/engine/tests/transactions.rs
new file mode 100644
index 00000000..e0c3ee30
--- /dev/null
+++ b/engine/tests/transactions.rs
@@ -0,0 +1,249 @@
+//! Transaction submission at the engine boundary: the `execute`, `simulate`, and
+//! `schedule` wrappers around the sequencer. The processor suite already proves
+//! the SVM commits/simulates correctly; these assert the `TransactionAccessor`
+//! ergonomics on top — subscribe-then-await commit, the separate simulation
+//! channel that never commits, and fire-and-forget scheduling.
+#![cfg(test)]
+
+use agave_transaction_view::MAX_STANDARD_TRANSACTION_SIZE;
+use engine::testkit::TestEngine;
+use keeper::testkit::{
+ WireVersion, decode_v42, load_v42_data, load_v42_lamports, sign_versioned_instructions,
+ signed_view, store_v42, v42_padded_value, v42_sum,
+};
+use nucleus::KB;
+use solana_account::{AccountMode, ReadableAccount};
+use solana_instruction_error::InstructionError;
+use solana_packet::PACKET_DATA_SIZE;
+use solana_transaction::TransactionError;
+use v42_calculator_interface::builder::{Expr as E, transfer};
+
+// The Engine accepts the same standard wire formats produced by Solana clients
+// on either side of the canonical packet boundary. The wide form reads every
+// supplied account, while the batched form independently exercises instruction
+// framing instead of relying on one large payload.
+#[tokio::test(flavor = "multi_thread")]
+async fn client_transaction_formats_execute_below_and_above_packet_limit() {
+ const WIDE_ACCOUNTS: usize = 32;
+ const BATCHED_INSTRUCTIONS: usize = 32;
+ const BATCHED_TERMS: usize = 16;
+ const FOUR_KIB: usize = 4 * KB;
+
+ let te = TestEngine::new().await;
+ let operands: Vec<_> =
+ (0..WIDE_ACCOUNTS).map(|_| store_v42(&te, 1, AccountMode::Delegated)).collect();
+
+ for version in [WireVersion::Legacy, WireVersion::V0, WireVersion::V1] {
+ let output = store_v42(&te, 0, AccountMode::Ephemeral);
+ let (_, small) = sign_versioned_instructions(
+ te.signer(),
+ version,
+ [E::lit(42).compose(output, &[])],
+ te.blockhash(),
+ );
+ assert!(small.len() < PACKET_DATA_SIZE,);
+ te.execute(small).await.expect("small v42 transaction succeeds");
+ assert_eq!(load_v42_data(&te, output), Some(42));
+
+ let output = store_v42(&te, 0, AccountMode::Ephemeral);
+ let (_, wide) = sign_versioned_instructions(
+ te.signer(),
+ version,
+ [v42_sum(output, &operands)],
+ te.blockhash(),
+ );
+ assert!(wide.len() > PACKET_DATA_SIZE);
+ assert!(wide.len() < MAX_STANDARD_TRANSACTION_SIZE);
+ te.execute(wide).await.expect("wide v42 transaction succeeds");
+ assert_eq!(load_v42_data(&te, output), Some(WIDE_ACCOUNTS as i64));
+
+ let output = store_v42(&te, 0, AccountMode::Ephemeral);
+ let instructions: Vec<_> = (0..BATCHED_INSTRUCTIONS)
+ .map(|value| v42_padded_value(output, value as i64, BATCHED_TERMS))
+ .collect();
+ let (_, batched) =
+ sign_versioned_instructions(te.signer(), version, &instructions, te.blockhash());
+ assert!(batched.len() > FOUR_KIB);
+ assert!(batched.len() < MAX_STANDARD_TRANSACTION_SIZE);
+ te.execute(batched).await.expect("batched v42 transaction succeeds");
+ assert_eq!(
+ load_v42_data(&te, output),
+ Some((BATCHED_INSTRUCTIONS - 1) as i64)
+ );
+ }
+
+ te.close().await;
+}
+
+// Simulation runs against live state through the dedicated simulation channel
+// but must not commit; execution of the same transfer does.
+#[tokio::test(flavor = "multi_thread")]
+async fn simulate_does_not_commit_execute_does() {
+ let te = TestEngine::new().await;
+ let source = store_v42(&te, 0, AccountMode::Delegated);
+ let recipient = store_v42(&te, 0, AccountMode::Ephemeral);
+ let source_before = load_v42_lamports(&te, source).expect("source exists");
+ let recipient_before = load_v42_lamports(&te, recipient).expect("recipient exists");
+ let ixs = [transfer(source, recipient, 42)];
+
+ // The record's post-execution account copy proves simulation actually ran
+ // the program, not merely that the channel round-tripped.
+ let record = te.simulate(&ixs).await.expect("simulation resolves");
+ let executed = record.result.expect("simulated transaction processes");
+ assert!(executed.was_successful(), "simulated execution succeeds");
+ let (_, simulated_source) = executed
+ .loaded_transaction
+ .accounts
+ .iter()
+ .find(|(key, _)| *key == source)
+ .expect("simulation loaded the source account");
+ assert_eq!(
+ simulated_source.lamports(),
+ source_before - 42,
+ "simulation debited its source copy"
+ );
+ let (_, simulated_recipient) = executed
+ .loaded_transaction
+ .accounts
+ .iter()
+ .find(|(key, _)| *key == recipient)
+ .expect("simulation loaded the recipient account");
+ assert_eq!(
+ simulated_recipient.lamports(),
+ recipient_before + 42,
+ "simulation credited its recipient copy"
+ );
+ assert_eq!(
+ load_v42_lamports(&te, source).expect("source remains"),
+ source_before,
+ "simulation leaves the live source untouched"
+ );
+ assert_eq!(
+ load_v42_lamports(&te, recipient).expect("recipient remains"),
+ recipient_before,
+ "simulation leaves the live recipient untouched"
+ );
+
+ assert!(te.execute(&ixs).await.is_ok(), "execution resolves");
+ assert_eq!(
+ load_v42_lamports(&te, source).expect("source remains"),
+ source_before - 42,
+ "execution commits the source debit"
+ );
+ assert_eq!(
+ load_v42_lamports(&te, recipient).expect("recipient remains"),
+ recipient_before + 42,
+ "execution commits the recipient credit"
+ );
+
+ te.close().await;
+}
+
+// A real processed transaction retains its execution artifacts through keeper's
+// projection and the ledger's compressed append→index→reader round-trip.
+#[tokio::test(flavor = "multi_thread")]
+async fn processed_transaction_details_roundtrip() {
+ let te = TestEngine::new().await;
+ let slot = te.blocks().current_slot();
+ let mut expected = Vec::new();
+
+ for value in [7, 42, 99] {
+ let output = store_v42(&te, 0, AccountMode::Ephemeral);
+ let ix = E::lit(value).cpi().compose(output, &[]);
+ let (signature, transaction) = signed_view(&te, None, ix.clone());
+ let bytes = transaction.inner_data().as_ref().clone();
+
+ te.execute(&[ix]).await.expect("processed transaction succeeds");
+ expected.push((signature, bytes, value));
+ }
+
+ te.sync().await;
+
+ for (signature, bytes, value) in expected {
+ let response = te
+ .transactions()
+ .get(signature)
+ .await
+ .expect("ledger read succeeds")
+ .expect("processed transaction is retained");
+ assert_eq!(response.transaction, bytes);
+ assert_eq!(response.execution.header.signature, signature);
+ assert_eq!(response.execution.header.slot, slot);
+ assert!(response.execution.header.result.is_ok());
+
+ let details = response.execution.details.expect("execution details retained");
+ assert_eq!(
+ details.fee, 0,
+ "the engine does not charge transaction fees"
+ );
+ assert!(
+ !details.balances.pre.is_empty(),
+ "native balances were recorded"
+ );
+ assert_eq!(
+ details.balances.pre, details.balances.post,
+ "the calculator changes account data, not lamports"
+ );
+ assert!(details.logs.iter().any(|line| line.contains("v42:")));
+ assert!(details.compute_units > 0);
+ assert!(
+ details
+ .cpi
+ .as_ref()
+ .is_some_and(|groups| groups.iter().any(|group| !group.0.is_empty())),
+ "the nested expression retains its CPI trace"
+ );
+ let returned = details.return_data.expect("CPI return data retained");
+ assert_eq!(returned.program, v42_calculator_interface::ID.to_bytes());
+ assert_eq!(returned.data.as_slice(), &value.to_le_bytes());
+ }
+
+ te.close().await;
+}
+
+// A transaction that runs but errors resolves as a committed error result and
+// leaves its output account untouched — the engine surfaces the failure through
+// the outer Ok / inner Err split rather than dropping it.
+#[tokio::test(flavor = "multi_thread")]
+async fn failed_execution_surfaces_error_result() {
+ let te = TestEngine::new().await;
+ let output = store_v42(&te, 5, AccountMode::Ephemeral);
+ // MIN - 1 overflows the program's checked_sub before any write.
+ let ixs = [(E::lit(i64::MIN) - E::lit(1)).compose(output, &[])];
+
+ let error = te.execute(&ixs).await.expect_err("overflow yields an error result");
+ // CalcError::Arithmetic = 6; its discriminants are stable for tests.
+ assert_eq!(
+ error,
+ TransactionError::InstructionError(0, InstructionError::Custom(6)),
+ "the program's own failure is surfaced, not a substitute"
+ );
+ assert_eq!(
+ load_v42_data(&te, output),
+ Some(5),
+ "failed execution commits no writes"
+ );
+
+ te.close().await;
+}
+
+// schedule returns before the transaction commits; the write still lands, and an
+// account subscription (not a poll loop) observes it.
+#[tokio::test(flavor = "multi_thread")]
+async fn schedule_is_fire_and_forget() {
+ let te = TestEngine::new().await;
+ let output = store_v42(&te, 0, AccountMode::Ephemeral);
+ let mut updates = te.accounts().subscribe(output).await;
+ let ixs = [E::lit(7).compose(output, &[])];
+
+ te.schedule(&ixs).await;
+
+ let account = updates.recv().await.expect("scheduled write reaches the subscriber");
+ assert_eq!(
+ decode_v42(&account),
+ 7,
+ "scheduled transaction commits the write"
+ );
+
+ te.close().await;
+}
diff --git a/keeper/Cargo.toml b/keeper/Cargo.toml
new file mode 100644
index 00000000..76a8a348
--- /dev/null
+++ b/keeper/Cargo.toml
@@ -0,0 +1,66 @@
+[package]
+name = "magicblock-keeper"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+version.workspace = true
+
+[lib]
+name = "keeper"
+
+[features]
+# Exposes `keeper::testkit`, the shared keeper-level test harness, as normal code
+# so downstream crates can import it via a dev-dependency (no new crate needed).
+testkit = ["accountsdb/testkit", "dep:solana-instruction", "ledger/testkit", "nucleus/testkit"]
+
+[dependencies]
+accountsdb = { workspace = true }
+ledger = { workspace = true }
+nucleus = { workspace = true, features = ["config", "notifier", "runtime"] }
+
+ahash = { workspace = true }
+arc-swap = { workspace = true }
+derive_more = { workspace = true, features = ["from"] }
+flume = { workspace = true }
+oneshot = { workspace = true }
+parking_lot = { workspace = true }
+scc = { workspace = true }
+serde = { workspace = true }
+smallvec = { workspace = true }
+tar = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["rt", "sync"] }
+tracing = { workspace = true }
+zstd = { workspace = true }
+
+agave-feature-set = { workspace = true, features = ["agave-unstable-api"] }
+solana-account = { workspace = true, features = ["bincode"] }
+solana-feature-gate-interface = { workspace = true, features = ["bincode"] }
+solana-hash = { workspace = true }
+solana-instruction = { workspace = true, optional = true }
+solana-keypair = { workspace = true }
+solana-message = { workspace = true }
+solana-program-runtime = { workspace = true }
+solana-pubkey = { workspace = true }
+solana-sdk-ids = { workspace = true }
+solana-signature = { workspace = true }
+solana-signer = { workspace = true }
+solana-svm = { workspace = true }
+solana-sysvar = { workspace = true }
+solana-transaction-error = { workspace = true }
+
+[dev-dependencies]
+keeper = { workspace = true, features = ["testkit"] }
+nucleus = { workspace = true, features = ["testkit"] }
+
+solana-instruction = { workspace = true }
+solana-keypair = { workspace = true }
+solana-signer = { workspace = true }
+tokio = { workspace = true, features = ["macros"] }
+
+[lints]
+workspace = true
diff --git a/keeper/README.md b/keeper/README.md
new file mode 100644
index 00000000..074a0957
--- /dev/null
+++ b/keeper/README.md
@@ -0,0 +1,79 @@
+# `magicblock-keeper`
+
+Keeper opens accountsdb and the ledger as one durable state boundary. It also
+owns startup account seeding, read-side caches, and live subscription fanout.
+Account routing remains in accountsdb and ledger retention remains in ledger.
+
+## Startup and recovery
+
+`KeeperBuilder::build` opens both stores and seeds active feature accounts,
+native builtins, configured loader-v4 programs, caller-provided accounts,
+authority funding, and sysvars.
+
+Accountsdb is restored from the newest retained snapshot when validation finds
+corruption, its sealed superblock trails the ledger head, or its committed
+transaction count trails the ledger's durable count. The accountsdb count is a
+checkpoint high-water mark and may exceed the locally retained ledger count,
+including after replication snapshot bootstrap. The original active tree is
+saved until the restored snapshot validates. Engine replay is responsible for
+advancing restored state from the successor of its sealed superblock through the
+ledger tip and must finish with matching transaction counts when replay runs.
+
+`nucleus::config::BlockstoreParams` supplies the expected block time and
+non-zero superblock interval used by pacing and cache TTL calculation. The
+shared accountsdb, blockstore, and ledger parameters are defined by nucleus;
+keeper consumes them when opening its durable stores and caches.
+
+## Authority
+
+`nucleus::config::Authority::local` is the keypair used for locally signed
+messages. When `Authority::remote` is set, `Keeper::authority` returns that
+immediate upstream identity instead of the local pubkey, while `Keeper::signer`
+continues to return the local signer. Replication followers retain both values
+across restart.
+
+The effective authority also identifies the engine's sponsor account. Keeper
+creates this engine-local account only for an empty ledger, persists its spent
+balance across restarts, and restores its initial balance on reset. Startup
+rejects a non-empty deployment whose configured authority account is absent.
+
+## Superblock finalization
+
+`Keeper::finalize_superblock` snapshots accountsdb at the current ledger head,
+computes the persisted-account checksum, appends the corresponding
+`SuperblockSeal`, and archives the snapshot in the successor superblock
+directory.
+
+Finalization requires exclusive account-store access. Engine obtains that
+exclusivity through the sequencer and simulator barriers before calling it.
+
+## Synchronization
+
+`Keeper::sync(false)` flushes queued appends and accountsdb while keeping ledger
+workers available, as required by replay and replication. `Keeper::sync(true)`
+is the irreversible shutdown fence: it closes every reader after earlier queued
+requests, flushes and closes the appender, then flushes accountsdb.
+
+## Caches and subscriptions
+
+Signature and recent-block caches use slot-based TTLs with lazy eviction on
+insertion. The account cache is an LRU that also coordinates concurrent loads of
+missing accounts. Only non-authoritative modes enter the eviction LRU;
+delegated, ephemeral, and unresolved transient state remains outside it.
+
+Dedicated channels publish account and program updates, signature results, logs,
+processed transactions, blocks, cache evictions, completed snapshots, and
+service messages. Signatures have terminal oneshot fanout; persistent multicast
+streams give each receiver a bounded queue and disconnect a receiver that falls
+behind. Processed transactions, service messages, and cache evictions each have
+one process-lifetime receiver and apply producer backpressure when full.
+Append rejection notifies only its newest signature waiter, preserving older
+waiters for an already accepted transaction; invalid-blockhash status is cached.
+
+## `testkit`
+
+The `testkit` feature exposes a keeper backed by throwaway directories plus v42
+account and transaction helpers, including persisted-metadata fault injection.
+When enabled, Keeper's build script builds the v42 SBF artifact consumed by the
+harness. Downstream tests enable the feature on their dev-dependency instead of
+duplicating the setup.
diff --git a/keeper/build.rs b/keeper/build.rs
new file mode 100644
index 00000000..d285f3d9
--- /dev/null
+++ b/keeper/build.rs
@@ -0,0 +1,67 @@
+//! Builds the v42 calculator SBF program for runtime tests.
+
+use std::{env, io, path::PathBuf, process::Command};
+
+const PROGRAM_DIR: &str = "programs/v42-calculator-program";
+const PROGRAM: &str = "programs/v42-calculator-program/Cargo.toml";
+const SO: &str = "v42_calculator_program.so";
+
+fn main() -> Result<(), Box> {
+ // The embedded program is only compiled by `keeper::testkit`.
+ if env::var_os("CARGO_FEATURE_TESTKIT").is_none() {
+ return Ok(());
+ }
+
+ let manifest_dir = PathBuf::from(
+ env::var_os("CARGO_MANIFEST_DIR")
+ .ok_or_else(|| io::Error::other("CARGO_MANIFEST_DIR is not set"))?,
+ );
+ let workspace = manifest_dir.parent().ok_or_else(|| {
+ io::Error::other(format!(
+ "CARGO_MANIFEST_DIR has no parent: {}",
+ manifest_dir.display()
+ ))
+ })?;
+ let manifest = workspace.join(PROGRAM);
+ let artifact = workspace.join("target/deploy").join(SO);
+ println!(
+ "cargo:rerun-if-changed={}",
+ workspace.join(PROGRAM_DIR).display()
+ );
+
+ let output = Command::new("cargo")
+ .arg("build-sbf")
+ .arg("--manifest-path")
+ .arg(&manifest)
+ .arg("--arch")
+ .arg("v3")
+ .current_dir(workspace)
+ // `cargo clippy` exports these wrappers pointing at `clippy-driver`; left in
+ // place they hijack the SBF toolchain's rustc, which can't resolve the
+ // `sbpf*-solana` target. Strip them so `build-sbf` uses its own toolchain.
+ .env_remove("RUSTC_WRAPPER")
+ .env_remove("RUSTC_WORKSPACE_WRAPPER")
+ .output()
+ .map_err(|e| io::Error::other(format!("failed to run `cargo build-sbf`: {e}")))?;
+
+ if !output.status.success() {
+ return Err(io::Error::other(format!(
+ "`cargo build-sbf` failed with {}\nstdout:\n{}\nstderr:\n{}",
+ output.status,
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ ))
+ .into());
+ }
+ if !artifact.is_file() {
+ Err(io::Error::other(format!(
+ "missing SBF artifact: {}",
+ artifact.display()
+ )))?;
+ }
+ println!(
+ "cargo:rustc-env=V42_CALCULATOR_PROGRAM_SO={}",
+ artifact.display()
+ );
+ Ok(())
+}
diff --git a/keeper/src/accessor.rs b/keeper/src/accessor.rs
new file mode 100644
index 00000000..5d729b96
--- /dev/null
+++ b/keeper/src/accessor.rs
@@ -0,0 +1,372 @@
+//! Namespaced keeper access APIs.
+
+use std::{ops::Deref, path::PathBuf, sync::Arc};
+
+use accountsdb::{AccountEntry, AccountLoader, AccountsDB, AccountsDBError};
+use ledger::{
+ LedgerRequestError,
+ request::*,
+ schema::{Event, SuperblockSeal, TransactionEntry},
+};
+use nucleus::{
+ Slot,
+ ledger::{Block, BlockstorePosition},
+ tls::{EncodedMessage, TlsManager},
+};
+use solana_account::{AccountSharedData, ReadableAccount};
+use solana_hash::Hash;
+use solana_pubkey::Pubkey;
+use solana_sdk_ids::sysvar;
+use solana_signature::Signature;
+use solana_svm::{
+ transaction_execution_result::ExecutedTransaction,
+ transaction_processing_result::TransactionProcessingResult,
+};
+use solana_sysvar::{
+ clock::Clock,
+ slot_hashes::{SlotHashes, SysvarId},
+};
+use solana_transaction_error::TransactionError;
+use tokio::sync::mpsc::Receiver;
+
+use crate::{
+ FullTransaction, Keeper, ResolvedTransaction,
+ cache::{AccountCache, MissingAccount},
+ error::Result,
+ subscriptions::TransactionLogs,
+ util::{execution_commit, request},
+};
+
+/// Account operations namespace.
+pub struct AccountsAccessor<'a> {
+ pub(crate) keeper: &'a Keeper,
+}
+
+/// Cursor over accounts missing from local storage.
+///
+/// Each item either gives the caller load ownership or waits for another
+/// caller that already owns the load.
+pub struct MissingAccounts<'a> {
+ index: usize,
+ accounts: &'a [Pubkey],
+ loader: AccountLoader<'a>,
+ cache: &'a Arc,
+}
+
+impl<'a> Iterator for MissingAccounts<'a> {
+ type Item = MissingAccount;
+ /// Returns the next account that still needs resolution.
+ fn next(&mut self) -> Option {
+ loop {
+ let pubkey = self.accounts.get(self.index)?;
+ self.index += 1;
+ if self.loader.contains(pubkey).unwrap_or_default() {
+ self.cache.promote(pubkey);
+ continue;
+ }
+ return Some(self.cache.reserve(*pubkey));
+ }
+ }
+}
+
+impl<'a> AccountsAccessor<'a> {
+ /// Coordinates loading of accounts missing from local storage.
+ pub fn ensure(&'a self, accounts: &'a [Pubkey]) -> MissingAccounts<'a> {
+ MissingAccounts {
+ index: 0,
+ accounts,
+ loader: self.keeper.accountsdb.loader(),
+ cache: &self.keeper.caches.accounts,
+ }
+ }
+
+ /// Returns recent transaction signatures that mention the account.
+ pub async fn signatures(
+ &self,
+ params: AccountSignaturesParams,
+ ) -> Result> {
+ Ok(request(self.keeper, params, ReadRequest::AccountSignatures).await??)
+ }
+
+ /// Subscribes to updates for one account pubkey.
+ pub async fn subscribe(&self, account: Pubkey) -> Receiver {
+ self.keeper.subscriptions.accounts.subscribe(account).await
+ }
+
+ /// Subscribes to account updates for accounts owned by `program`.
+ pub async fn subscribe_program(&self, program: Pubkey) -> Receiver {
+ self.keeper.subscriptions.programs.subscribe(program).await
+ }
+
+ /// Subscribes as the sole receiver of account pubkeys evicted from the recent-load cache.
+ ///
+ /// Returns an error if the process-lifetime eviction receiver was already registered.
+ pub fn subscribe_evictions(&self) -> Result> {
+ self.keeper.caches.accounts.evictions.subscribe()
+ }
+
+ /// Subscribes to completed accountsdb snapshot archives.
+ pub fn subscribe_snapshots(&self) -> Receiver {
+ self.keeper.subscriptions.snapshots.subscribe_sync(())
+ }
+
+ /// Updates durable `SlotHashes` and `Clock` sysvar accounts from `block`.
+ ///
+ /// If either sysvar account is absent, no account updates are stored.
+ pub fn update_sysvars(&self, block: Block) -> Result<()> {
+ let loader = self.loader();
+
+ let Some(mut hacc) = loader.load(&SlotHashes::id())? else {
+ return Ok(());
+ };
+ let mut hashes: SlotHashes = hacc.deserialize_data().map_err(AccountsDBError::from)?;
+ hashes.add(block.slot, block.hash);
+ hacc.serialize_data(&hashes).map_err(AccountsDBError::from)?;
+ let Some(mut cacc) = loader.load(&Clock::id())? else {
+ return Ok(());
+ };
+ let mut clock: Clock = cacc.deserialize_data().map_err(AccountsDBError::from)?;
+ clock.slot = block.slot;
+ clock.unix_timestamp = block.time;
+ cacc.serialize_data(&clock).map_err(AccountsDBError::from)?;
+ self.store(&[(SlotHashes::id(), hacc), (Clock::id(), cacc)]).map_err(Into::into)
+ }
+}
+
+impl Deref for AccountsAccessor<'_> {
+ type Target = AccountsDB;
+
+ fn deref(&self) -> &Self::Target {
+ &self.keeper.accountsdb
+ }
+}
+
+/// Transaction operations namespace.
+pub struct TransactionsAccessor<'a> {
+ pub(crate) keeper: &'a Keeper,
+}
+
+impl<'a> TransactionsAccessor<'a> {
+ /// Loads the full retained transaction for `signature`.
+ pub async fn get(&self, signature: Signature) -> Result> {
+ Ok(request(self.keeper, signature, ReadRequest::Transaction).await??)
+ }
+
+ /// Loads the retained execution status for `signature`.
+ pub async fn status(&self, signature: Signature) -> Result > {
+ if let Some(status) = self.keeper.caches.signatures.get(&signature) {
+ return Ok(status);
+ }
+ Ok(request(self.keeper, signature, ReadRequest::TransactionStatus).await??)
+ }
+
+ /// Subscribes as the sole receiver of all processed transactions, including failures.
+ ///
+ /// Returns an error if the process-lifetime transaction receiver was already registered.
+ pub fn subscribe_processed(&self) -> Result> {
+ self.keeper.subscriptions.transactions.subscribe()
+ }
+
+ /// Subscribes to status updates for one transaction signature.
+ pub async fn subscribe_signature(
+ &self,
+ signature: Signature,
+ ) -> oneshot::Receiver {
+ self.keeper.subscriptions.signatures.subscribe(signature).await
+ }
+
+ /// Subscribes to log batches mentioning `account`.
+ pub async fn subscribe_logs(&self, account: Pubkey) -> Receiver> {
+ self.keeper.subscriptions.logs.subscribe(account).await
+ }
+
+ /// Subscribes as the sole receiver of encoded service messages.
+ ///
+ /// Returns an error if the process-lifetime service receiver was already registered.
+ pub fn subscribe_service_messages(&self) -> Result> {
+ self.keeper.subscriptions.services.subscribe()
+ }
+
+ /// Appends transaction bytes to the ledger, deduplicating by signature.
+ ///
+ /// Returns `Ok(true)` when the transaction was appended. On `Ok(false)`,
+ /// the latest signature subscriber receives `AlreadyProcessed` or
+ /// `BlockhashNotFound`. Execution details are appended later by
+ /// `commit_execution`.
+ pub async fn append(&self, transaction: &ResolvedTransaction) -> Result {
+ let caches = &self.keeper.caches;
+ let slot = caches.blocks.latest.load().slot + 1;
+ let signature = transaction.signatures()[0];
+ let mut result = Ok(());
+ if !caches.signatures.push(signature, None, slot) {
+ result = Err(TransactionError::AlreadyProcessed);
+ } else if !self.keeper.blocks().is_valid(transaction.recent_blockhash()) {
+ result = Err(TransactionError::BlockhashNotFound);
+ let status = TransactionStatus { result: result.clone(), slot };
+ caches.signatures.update(&signature, Some(status));
+ }
+ if result.is_err() {
+ let status = TransactionStatus { result, slot };
+ self.keeper.subscriptions.signatures.send_last(&signature, &status);
+ return Ok(false);
+ }
+ let event = Event::Transaction(TransactionEntry {
+ signature,
+ payload: transaction.inner_data().clone(),
+ });
+ self.keeper.ledger.appender.send_async(event).await?;
+ Ok(true)
+ }
+
+ /// Commits execution metadata and publishes resulting account changes.
+ pub fn commit_execution(&self, mut txn: FullTransaction) -> Result<()> {
+ let subs = &self.keeper.subscriptions;
+ let commit = execution_commit(&mut txn);
+ self.keeper.ledger.appender.send(commit.event)?;
+
+ if let Some(execution) = self.commit_state_transitions(&txn.execution.result)? {
+ let accounts = &execution.loaded_transaction.accounts;
+ let mut logs = None;
+ for (pubkey, acc) in accounts {
+ if subs.logs.contains(pubkey) {
+ let logs = logs.get_or_insert_with(|| {
+ Arc::new(TransactionLogs {
+ signature: commit.signature,
+ result: commit.status.result.clone(),
+ logs: Arc::clone(&commit.logs),
+ })
+ });
+ subs.logs.send(pubkey, logs);
+ }
+ if !acc.dirty() {
+ continue;
+ }
+ subs.accounts.send(pubkey, acc);
+ if subs.programs.contains(acc.owner()) {
+ let account = &(*pubkey, acc.clone());
+ subs.programs.send(acc.owner(), account);
+ }
+ }
+ while let Some(msg) = TlsManager::dequeue() {
+ subs.services.blocking_send(msg);
+ }
+ }
+ subs.transactions.blocking_send(txn);
+ // Clear TLS unconditionally so unsent messages cannot leak into the next transaction.
+ TlsManager::clear();
+ subs.signatures.send(&commit.signature, &commit.status);
+ self.keeper.caches.signatures.update(&commit.signature, Some(commit.status));
+ Ok(())
+ }
+
+ /// Commits one accepted transaction to accountsdb, writing dirty accounts
+ /// only for successful execution and returning it for downstream fanout.
+ pub fn commit_state_transitions<'t>(
+ &self,
+ result: &'t TransactionProcessingResult,
+ ) -> Result> {
+ let execution = result.as_ref().ok().filter(|e| e.was_successful());
+ let accounts = execution
+ .into_iter()
+ .flat_map(|execution| execution.loaded_transaction.accounts.iter())
+ .filter(|(id, a)| a.dirty() && !sysvar::instructions::check_id(id));
+ self.keeper.accountsdb.commit(accounts)?;
+ Ok(execution.map(|execution| &**execution))
+ }
+}
+
+/// Block operations namespace.
+pub struct BlocksAccessor<'a> {
+ pub(crate) keeper: &'a Keeper,
+}
+
+impl<'a> BlocksAccessor<'a> {
+ /// Loads a retained block at the requested detail level.
+ pub async fn get(&self, params: BlockParams) -> Result > {
+ Ok(request(self.keeper, params, ReadRequest::Block).await??)
+ }
+
+ /// Returns the latest block boundary known to keeper.
+ pub fn latest(&self) -> Block {
+ **self.keeper.caches.blocks.latest.load()
+ }
+
+ /// Returns the slot currently being built (one past the latest block).
+ pub fn current_slot(&self) -> Slot {
+ self.keeper.caches.blocks.latest.load().slot + 1
+ }
+
+ /// Returns whether `hash` is in the recent block hash cache (still valid).
+ pub fn is_valid(&self, hash: &Hash) -> bool {
+ self.keeper.caches.blocks.history.contains(hash)
+ }
+
+ /// Subscribes to newly committed slots.
+ pub fn subscribe(&self) -> Receiver {
+ self.keeper.subscriptions.blocks.subscribe_sync(())
+ }
+
+ /// Publishes a completed block and advances block-derived account state.
+ ///
+ /// Replay skips the ledger append because the block is already stored.
+ pub fn append(&self, block: Block, replay: bool) -> Result<()> {
+ let event = Event::Block(block);
+ if !replay {
+ self.keeper.ledger.appender.send(event)?;
+ self.keeper.subscriptions.blocks.send(&(), &block);
+ }
+ self.keeper.caches.blocks.push(block);
+ self.keeper.accounts().update_sysvars(block)?;
+ self.keeper.accounts().set_slot(block.slot)?;
+ Ok(())
+ }
+}
+
+/// Superblock operations namespace
+pub struct SuperblockAccessor<'a> {
+ pub(crate) keeper: &'a Keeper,
+}
+
+impl SuperblockAccessor<'_> {
+ /// Id of the superblock accountsdb last sealed.
+ pub fn sealed(&self) -> SuperblockSeal {
+ SuperblockSeal {
+ id: self.keeper.accountsdb.superblock(),
+ checksum: self.keeper.accountsdb.checksum(),
+ transactions: self.keeper.accountsdb.transactions(),
+ }
+ }
+
+ /// Returns the ledger root containing retained superblock directories.
+ pub fn directory(&self) -> &PathBuf {
+ &self.keeper.ledger.directory
+ }
+
+ /// Follower's current durable blockstore position, reported to the leader at handshake.
+ pub fn position(&self) -> BlockstorePosition {
+ self.keeper.ledger.position()
+ }
+
+ /// Enqueues a seal onto the append stream, sealing the current superblock and
+ /// rotating to the next. Used by a follower applying a seal received from the leader.
+ pub fn append(&self, seal: SuperblockSeal) -> Result<()> {
+ let event = Event::Superblock(seal);
+ self.keeper.ledger.appender.send(event)?;
+ self.sync(false)
+ }
+
+ /// Installs a snapshot seal and adopts its cumulative transaction count.
+ pub fn bootstrap(&self, seal: SuperblockSeal) -> Result<()> {
+ let event = Event::Bootstrap(seal);
+ self.keeper.ledger.appender.send(event)?;
+ self.sync(false)
+ }
+
+ /// Blocks until every queued append event has been flushed and made durable.
+ pub fn sync(&self, is_final: bool) -> Result<()> {
+ let (response, ack) = oneshot::channel();
+ let event = Event::Sync { response, is_final };
+ self.keeper.ledger.appender.send(event)?;
+ ack.recv().map_err(LedgerRequestError::from)?.map_err(Into::into)
+ }
+}
diff --git a/keeper/src/builder.rs b/keeper/src/builder.rs
new file mode 100644
index 00000000..d50f48c8
--- /dev/null
+++ b/keeper/src/builder.rs
@@ -0,0 +1,325 @@
+//! Keeper construction, recovery, and startup account seeding.
+
+use std::{
+ collections::HashMap,
+ fs::{self, File},
+ sync::Arc,
+ time::Duration,
+};
+
+use accountsdb::{AccountEntry, AccountsDB, AccountsDBError, BackupOp, SnapshotError};
+use agave_feature_set::FeatureSet;
+use ledger::{
+ Ledger, LedgerHandle,
+ request::{BlockDetails, BlockParams, ReadRequest, RequestPayload},
+};
+use nucleus::{
+ Slot,
+ config::{AccountsDBParams, Authority, BlockstoreParams, LedgerParams},
+ ledger::{ACCOUNTSDB_SNAPSHOT_FILE, Block},
+ shutdown::ShutdownManager,
+};
+use serde::Serialize;
+use solana_account::{AccountBuilder, AccountMode, AccountSharedData, ReadableAccount};
+use solana_feature_gate_interface::Feature;
+use solana_program_runtime::invoke_context::BuiltinFunctionWithContext;
+use solana_pubkey::Pubkey;
+use solana_sdk_ids::sysvar;
+#[allow(deprecated)]
+use solana_sysvar::fees::Fees;
+use solana_sysvar::{
+ clock::Clock,
+ epoch_rewards::EpochRewards,
+ epoch_schedule::EpochSchedule,
+ last_restart_slot::LastRestartSlot,
+ rent::Rent,
+ slot_hashes::{SlotHashes, SysvarId},
+};
+use tracing::{error, info, warn};
+
+use crate::{
+ Keeper,
+ cache::{AccountCache, BlocksCache, Caches, ExpiringCache},
+ error::Result,
+ metrics,
+ subscriptions::Subscriptions,
+};
+
+/// Initial balance assigned to the authority account that sponsors account creation.
+pub(crate) const SPONSOR_INIT_BALANCE: u64 = u64::MAX / 2;
+/// Maximum number of recent hashes retained by the `SlotHashes` sysvar.
+const SLOTHASH_ENTRIES: usize = 512;
+/// Wall-clock retention window for recently processed signatures.
+const SIGNATURE_CACHE_WINDOW: Duration = Duration::from_secs(75);
+/// Wall-clock retention window for recently produced blocks.
+const BLOCK_CACHE_WINDOW: Duration = Duration::from_secs(60);
+
+/// Builder for keeper directories and cache timing.
+#[derive(Clone)]
+pub struct KeeperBuilder {
+ /// Local signer and optional remote authority represented by the engine.
+ pub authority: Authority,
+ /// Accounts database storage parameters.
+ pub accountsdb: AccountsDBParams,
+ /// Ledger storage parameters.
+ pub ledger: LedgerParams,
+ /// Block production timing and superblock sealing parameters.
+ pub blockstore: BlockstoreParams,
+ /// Native builtin program ids to seed as executable accounts.
+ pub builtins: HashMap,
+ /// Upgradeable program accounts to seed, paired as `(program id, ELF bytes)`.
+ pub programs: HashMap>,
+ /// Plain accounts to seed into storage before startup completes.
+ pub accounts: HashMap,
+ /// Rent parameters used to size seeded accounts and the Rent sysvar.
+ pub rent: Rent,
+}
+
+impl KeeperBuilder {
+ /// Open durable stores, recover accounts from the latest snapshot if needed, and wire caches.
+ pub async fn build(mut self, shutdown: &mut ShutdownManager) -> Result {
+ let ledger = Ledger::init(&self.ledger.directory, self.ledger.size_limit, shutdown)?;
+ let accountsdb = self.accountsdb(&ledger)?;
+ let (block, featureset) = self.prepopulate(&accountsdb, &ledger).await?;
+ let caches = self.caches(block);
+ metrics::init();
+ Ok(Keeper {
+ authority: self.authority,
+ featureset,
+ rent: self.rent,
+ accountsdb,
+ ledger,
+ caches,
+ subscriptions: Subscriptions::new(shutdown),
+ })
+ }
+
+ /// Seeds accounts needed before the engine starts serving reads.
+ async fn prepopulate(
+ &mut self,
+ accountsdb: &AccountsDB,
+ ledger: &LedgerHandle,
+ ) -> Result<(Block, FeatureSet)> {
+ let mut accounts = Vec::new();
+ let featureset = self.seed_featureset(&mut accounts)?;
+ self.seed_programs(&mut accounts)?;
+ let block = self.seed_sysvars(accountsdb, ledger, &mut accounts).await?;
+ let authority = self.authority.pubkey();
+ if accountsdb.loader().load(&authority)?.is_none() {
+ let sponsor = AccountBuilder::default()
+ .lamports(SPONSOR_INIT_BALANCE)
+ .mode(AccountMode::Ephemeral);
+ accounts.push((authority, sponsor.build()));
+ }
+ accounts.extend(self.accounts.drain());
+ accountsdb.store(&accounts)?;
+ Ok((block, featureset))
+ }
+
+ /// Builds read-side caches using blocktime-derived slot TTLs.
+ fn caches(&self, latest: Block) -> Caches {
+ let blocktime = self.blockstore.blocktime;
+ let ttl = |window: Duration| window.div_duration_f64(blocktime).ceil() as Slot;
+ let blocks = BlocksCache::new(latest, ttl(BLOCK_CACHE_WINDOW));
+ let signatures = ExpiringCache::new(ttl(SIGNATURE_CACHE_WINDOW));
+ let accounts = Arc::new(AccountCache::new(self.accountsdb.lru_capacity));
+
+ Caches { signatures, blocks, accounts }
+ }
+
+ /// Activates the engine's required feature gates at slot 0, seeds a feature
+ /// account for each, and returns the resulting [`FeatureSet`].
+ fn seed_featureset(&self, accounts: &mut Vec) -> Result {
+ let mut featureset = FeatureSet::default();
+ [
+ agave_feature_set::curve25519_syscall_enabled::ID,
+ agave_feature_set::curve25519_restrict_msm_length::ID,
+ agave_feature_set::enable_poseidon_syscall::ID,
+ agave_feature_set::enable_sbpf_v3_deployment_and_execution::ID,
+ agave_feature_set::virtual_address_space_adjustments::ID,
+ agave_feature_set::syscall_parameter_address_restrictions::ID,
+ agave_feature_set::get_sysvar_syscall_enabled::ID,
+ agave_feature_set::ed25519_program_enabled::ID,
+ agave_feature_set::secp256k1_program_enabled::ID,
+ agave_feature_set::enable_secp256r1_precompile::ID,
+ ]
+ .iter()
+ .for_each(|f| featureset.activate(f, 0));
+ for (&id, &slot) in featureset.active() {
+ let feature = &Feature { activated_at: Some(slot) };
+ let account = self.account(feature, &solana_feature_gate_interface::ID)?;
+ accounts.push((id, account.build()));
+ }
+ Ok(featureset)
+ }
+
+ /// Seeds builtin and upgradeable program accounts.
+ fn seed_programs(&self, accounts: &mut Vec) -> Result<()> {
+ for &builtin in self.builtins.keys() {
+ let account = self.account(&(), &solana_sdk_ids::native_loader::ID)?;
+ let account = account.executable(true).build();
+ accounts.push((builtin, account));
+ }
+
+ for (&program, elf) in &self.programs {
+ let lamports = self.rent.minimum_balance(elf.len());
+ let account = AccountBuilder::default()
+ .lamports(lamports)
+ .mode(AccountMode::System)
+ .owner(solana_sdk_ids::loader_v4::ID)
+ .executable(true)
+ .data(elf.clone());
+ accounts.push((program, account.build()));
+ }
+ Ok(())
+ }
+
+ /// Seeds sysvars derived from retained ledger state and keeper config.
+ ///
+ /// Returns the latest available block, resolved from accountsdb or ledger
+ async fn seed_sysvars(
+ &self,
+ accountsdb: &AccountsDB,
+ ledger: &LedgerHandle,
+ accounts: &mut Vec,
+ ) -> Result {
+ let slot = accountsdb.slot();
+ let loader = accountsdb.loader();
+ let mut last_block = None;
+ if let Some(hashes) = loader.load(&SlotHashes::id())? {
+ let hashes = hashes.deserialize_data::().map_err(AccountsDBError::from)?;
+ // `SlotHashes` is ordered newest-first, so the latest block is `first`
+ if let Some(&(slot, hash)) = hashes.first() {
+ let parent = &slot.saturating_sub(1);
+ let parent = hashes.get(parent).copied().unwrap_or_default();
+ let time = self.blocktime(ledger, slot).await?;
+ last_block.replace(Block { slot, hash, time, parent });
+ }
+ } else {
+ let range = slot.saturating_sub(SLOTHASH_ENTRIES as u64)..slot + 1;
+ let (payload, handle) = RequestPayload::new(range);
+ ledger.reader.send(ReadRequest::BlockRange(payload))?;
+
+ let mut hashes = SlotHashes::new(&[Default::default(); SLOTHASH_ENTRIES]);
+ for block in handle.recv_timeout().await?? {
+ hashes.add(block.slot, block.hash);
+ last_block.replace(block);
+ }
+ let acc = self.account(&hashes, &sysvar::ID)?;
+ accounts.push((SlotHashes::id(), acc.build()));
+ }
+
+ let block = last_block.unwrap_or_default();
+ // Set the clock slot one ahead from the last
+ let clock = Clock {
+ slot: block.slot + 1,
+ unix_timestamp: block.time,
+ ..Default::default()
+ };
+ accounts.push((Clock::id(), self.account(&clock, &sysvar::ID)?.build()));
+ accounts.push((Rent::id(), self.account(&self.rent, &sysvar::ID)?.build()));
+ #[allow(deprecated)]
+ accounts.push((
+ Fees::id(),
+ self.account(&Fees::default(), &sysvar::ID)?.build(),
+ ));
+ accounts.push((
+ sysvar::last_restart_slot::id(),
+ self.account(&LastRestartSlot::default(), &sysvar::ID)?.build(),
+ ));
+ accounts.push((
+ sysvar::instructions::id(),
+ self.account(&(), &sysvar::ID)?.build(),
+ ));
+ accounts.push((
+ EpochSchedule::id(),
+ self.account(&EpochSchedule::default(), &sysvar::ID)?.build(),
+ ));
+ accounts.push((
+ EpochRewards::id(),
+ self.account(&EpochRewards::default(), &sysvar::ID)?.build(),
+ ));
+ Ok(block)
+ }
+
+ /// Builds a rent-exempt system account containing a serialized sysvar-like state.
+ fn account(&self, state: &S, owner: &Pubkey) -> Result {
+ let account =
+ AccountSharedData::new_data(0, state, owner).map_err(AccountsDBError::from)?;
+ let lamports = self.rent.minimum_balance(account.data().len());
+ Ok(AccountBuilder::from(account).lamports(lamports).mode(AccountMode::System))
+ }
+
+ /// Returns the retained block time for the given slot.
+ async fn blocktime(&self, ledger: &LedgerHandle, slot: Slot) -> Result {
+ let (payload, handle) = RequestPayload::new(BlockParams {
+ slot,
+ details: BlockDetails::None,
+ });
+ ledger.reader.send(ReadRequest::Block(payload))?;
+ Ok(handle.recv_timeout().await??.map(|r| r.block().time).unwrap_or_default())
+ }
+ /// Opens accountsdb, restoring the newest archived snapshot after corruption.
+ ///
+ /// A restored store trails the ledger tip — snapshots are archived at sealed
+ /// superblocks, not at the tip — so the returned accountsdb is only
+ /// guaranteed to validate, not to be current. Catching it back up is the
+ /// caller's job.
+ fn accountsdb(&self, ledger: &LedgerHandle) -> Result {
+ let mut backup = None;
+ loop {
+ let mut accountsdb = AccountsDB::new(&self.accountsdb.directory)?;
+ // Seal N opens ledger head N+1, so accountsdb is current at head-1.
+ let expected = ledger.head().saturating_sub(1);
+ let restored = backup.is_some();
+ let lagging = accountsdb.superblock() < expected;
+ let count_lagging = accountsdb.transactions() < ledger.transactions();
+ match accountsdb.validate() {
+ Ok(()) if restored || (!lagging && !count_lagging) => {
+ let reclaimed = accountsdb.compact()?;
+ info!(
+ lagging,
+ count_lagging, reclaimed, "accountsdb validation succeeded"
+ );
+ backup.map(fs::remove_dir_all).transpose()?;
+ return Ok(accountsdb);
+ }
+ validation @ (Err(AccountsDBError::Corruption) | Ok(())) => {
+ if restored {
+ error!(?validation, "restored accountsdb is corrupt");
+ accountsdb.backup(BackupOp::Restore)?;
+ return Err(SnapshotError::Missing.into());
+ }
+ warn!(
+ ?validation,
+ lagging, count_lagging, "state inconsistency detected"
+ );
+ backup.replace(accountsdb.backup(BackupOp::Save)?);
+ if let Err(error) = self.unarchive(ledger) {
+ accountsdb.backup(BackupOp::Restore)?;
+ return Err(error);
+ }
+ }
+ Err(other) => return Err(other.into()),
+ }
+ }
+ }
+
+ /// Restores the first retained accountsdb snapshot found, from newest to oldest.
+ fn unarchive(&self, ledger: &LedgerHandle) -> Result<()> {
+ info!("restoring accountsdb from latest available snapshot");
+ for superblock in ledger.iter() {
+ let src = superblock.directory.join(ACCOUNTSDB_SNAPSHOT_FILE);
+ if !src.exists() {
+ continue;
+ }
+ let dst = AccountsDB::directory(&self.accountsdb.directory);
+ let file = File::open(src)?;
+ let mut tar = tar::Archive::new(zstd::Decoder::new(file)?);
+ tar.unpack(dst)?;
+ info!(directory = ?superblock.directory, "restored accountsdb snapshot");
+ return Ok(());
+ }
+ Err(SnapshotError::Missing.into())
+ }
+}
diff --git a/keeper/src/cache.rs b/keeper/src/cache.rs
new file mode 100644
index 00000000..13526cac
--- /dev/null
+++ b/keeper/src/cache.rs
@@ -0,0 +1,250 @@
+//! Read-side caches owned by keeper.
+
+use std::{collections::VecDeque, hash::Hash, sync::Arc};
+
+use ahash::RandomState;
+use arc_swap::ArcSwap;
+use ledger::request::TransactionStatus;
+use nucleus::{Slot, ledger::Block, notifier::EventNotifier};
+use parking_lot::Mutex;
+use scc::{HashCache, HashMap, hash_map::Entry};
+use solana_account::AccountMode;
+use solana_hash::Hash as SolanaHash;
+use solana_pubkey::Pubkey;
+use solana_signature::Signature;
+
+use crate::{
+ metrics,
+ subscriptions::{Subscription, Unicast},
+};
+
+pub(crate) struct Caches {
+ /// Recent signature statuses keyed by transaction signature.
+ pub(crate) signatures: ExpiringCache>,
+ /// Recent block hashes and latest block boundary.
+ pub(crate) blocks: BlocksCache,
+ /// Account recency and missing-load coordination.
+ pub(crate) accounts: Arc,
+}
+
+/// Account access cache along with missing-account load reservations.
+pub(crate) struct AccountCache {
+ /// Committed account loads, ordered by recent access.
+ pub(crate) lru: HashCache,
+ /// In-flight account loads keyed by account pubkey.
+ pub(crate) reservations: HashMap, RandomState>,
+ /// Pubkeys evicted when a committed load displaces a cold account.
+ pub(crate) evictions: Unicast,
+}
+
+impl AccountCache {
+ /// Creates missing-load coordination with the requested recent-load capacity.
+ pub(crate) fn new(capacity: usize) -> Self {
+ let lru = HashCache::with_capacity_and_hasher(256, capacity, Default::default());
+ Self {
+ lru,
+ reservations: Default::default(),
+ evictions: Unicast::new(32, Subscription::Evictions),
+ }
+ }
+}
+
+/// Load guard owned by the task responsible for one missing account.
+pub struct AccountLoad {
+ /// Account this guard is responsible for loading.
+ pub pubkey: Pubkey,
+ /// Cache reservation released on commit or drop.
+ cache: Option>,
+}
+
+/// Wait handle for an account currently being loaded by another task.
+pub struct AccountWait(Pubkey, Arc);
+
+/// Coordination state for one account missing from local storage.
+pub enum MissingAccount {
+ /// Caller owns the load; dropping the guard releases waiters.
+ Load(AccountLoad),
+ /// Another caller is already loading the account.
+ Wait(AccountWait),
+}
+
+impl AccountLoad {
+ /// Completes the load, wakes waiters, and tracks non-authoritative modes for
+ /// eviction.
+ pub async fn complete(mut self, mode: AccountMode) {
+ let Some((cache, notifier)) = self.release() else {
+ return;
+ };
+ notifier.notify(true);
+ if mode.authoritative() {
+ return;
+ }
+ if let Ok(Some(evicted)) = cache.lru.put_sync(self.pubkey, ()) {
+ metrics::account_cache_eviction();
+ cache.evictions.send(evicted.0).await;
+ } else {
+ metrics::account_cache_insert()
+ }
+ }
+
+ fn release(&mut self) -> Option<(Arc, Arc)> {
+ let cache = self.cache.take()?;
+ let (_, notifier) = cache.reservations.remove_sync(&self.pubkey)?;
+ Some((cache, notifier))
+ }
+}
+
+impl Drop for AccountLoad {
+ /// Cancels the load reservation and wakes waiters without caching.
+ fn drop(&mut self) {
+ let Some((_, notifier)) = self.release() else {
+ return;
+ };
+ notifier.notify(false);
+ }
+}
+
+impl AccountWait {
+ /// Waits for the active loader and returns the pubkey plus whether it committed.
+ pub async fn wait(self) -> (Pubkey, bool) {
+ let result = self.1.notified().await;
+ (self.0, result)
+ }
+}
+
+impl AccountCache {
+ /// Promotes `pubkey` only if it has already been committed to the cache.
+ pub(crate) fn promote(&self, pubkey: &Pubkey) {
+ self.lru.get_sync(pubkey);
+ }
+
+ /// Reserves a missing account load or returns a waiter for the active load.
+ pub(crate) fn reserve(self: &Arc, pubkey: Pubkey) -> MissingAccount {
+ match self.reservations.entry_sync(pubkey) {
+ Entry::Occupied(e) => {
+ metrics::account_resolution_race();
+ MissingAccount::Wait(AccountWait(pubkey, e.get().clone()))
+ }
+ Entry::Vacant(e) => {
+ let notifier = Arc::new(EventNotifier::default());
+ e.insert_entry(notifier);
+ MissingAccount::Load(AccountLoad {
+ pubkey,
+ cache: Some(self.clone()),
+ })
+ }
+ }
+ }
+}
+
+/// Block lookup cache with a lock-free latest-block pointer.
+pub(crate) struct BlocksCache {
+ /// Latest block boundary known at keeper startup or after updates.
+ pub(crate) latest: ArcSwap,
+ /// Recent block hash to slot lookups.
+ pub(crate) history: ExpiringCache,
+}
+
+impl BlocksCache {
+ /// Creates a block cache seeded with the current latest block.
+ pub(crate) fn new(block: Block, ttl: Slot) -> Self {
+ let cache = Self {
+ latest: ArcSwap::new(block.into()),
+ history: ExpiringCache::new(ttl),
+ };
+ cache.history.push(block.hash, block.slot, block.slot);
+ cache
+ }
+
+ /// Records `block` as the latest and adds its hash to the recent history.
+ pub(crate) fn push(&self, block: Block) {
+ self.latest.store(block.into());
+ self.history.push(block.hash, block.slot, block.slot);
+ metrics::block_hash_entries(self.history.len());
+ }
+}
+
+/// Concurrent cache with slot-based lazy eviction.
+///
+/// Entries are evicted only when another entry is pushed. Re-inserting an
+/// existing key leaves its value and expiry slot unchanged.
+pub(crate) struct ExpiringCache {
+ /// Cached values by key.
+ index: HashMap,
+ /// Expiry order used for lazy eviction.
+ queue: Mutex>>,
+ /// Number of slots each entry lives after insertion.
+ ttl: Slot,
+}
+
+struct ExpiringRecord {
+ key: K,
+ expires: Slot,
+}
+
+impl ExpiringCache {
+ /// Creates a cache whose entries live for `ttl` slots after insertion.
+ pub(crate) fn new(ttl: Slot) -> Self {
+ Self {
+ index: HashMap::default(),
+ queue: Default::default(),
+ ttl,
+ }
+ }
+
+ /// Insert a key and evict entries expired at `slot`.
+ ///
+ /// Returns `false` if `key` already exists. Existing values and expiry slots
+ /// are left unchanged.
+ pub(crate) fn push(&self, key: K, value: V, slot: Slot) -> bool {
+ let mut queue = self.queue.lock();
+ // Lazily evict expired entries from the front of the queue.
+ while let Some(expired) = queue.pop_front_if(|e| e.expired(slot)) {
+ self.index.remove_sync(&expired.key);
+ }
+
+ match self.index.entry_sync(key) {
+ Entry::Occupied(_) => false,
+ Entry::Vacant(v) => {
+ v.insert_entry(value);
+ queue.push_back(ExpiringRecord::new(key, slot + self.ttl));
+ true
+ }
+ }
+ }
+
+ /// Entry count, including expired entries no sweep has reached yet, so the
+ /// metrics gauges fed from this can read above the live entry count.
+ pub(crate) fn len(&self) -> usize {
+ self.index.len()
+ }
+
+ /// May yield an entry already past its expiry slot but not yet swept.
+ pub(crate) fn get(&self, key: &K) -> Option {
+ self.index.read_sync(key, |_, v| v.clone())
+ }
+
+ /// May report an entry already past its expiry slot but not yet swept.
+ pub(crate) fn contains(&self, key: &K) -> bool {
+ self.index.contains_sync(key)
+ }
+
+ /// Replaces the value for `key`, leaving its expiry slot untouched: an
+ /// update does not extend the entry's life. No-op if `key` is absent.
+ pub(crate) fn update(&self, key: &K, value: V) {
+ let Some(mut entry) = self.index.get_sync(key) else {
+ return;
+ };
+ entry.insert(value);
+ }
+}
+
+impl ExpiringRecord {
+ fn new(key: K, expires: Slot) -> Self {
+ Self { key, expires }
+ }
+
+ fn expired(&self, instant: Slot) -> bool {
+ instant >= self.expires
+ }
+}
diff --git a/keeper/src/error.rs b/keeper/src/error.rs
new file mode 100644
index 00000000..d8c67c59
--- /dev/null
+++ b/keeper/src/error.rs
@@ -0,0 +1,48 @@
+//! Keeper error types.
+
+use accountsdb::{AccountsDBError, SnapshotError};
+use derive_more::From;
+use flume::SendError;
+use ledger::{LedgerError, LedgerRequestError, request::ReadRequest, schema::Event};
+use nucleus::shutdown::Service;
+
+/// Errors produced while initializing, finalizing, or serving keeper state.
+#[derive(Debug, thiserror::Error, From)]
+pub enum KeeperError {
+ /// Filesystem or archive IO failed.
+ #[error("io: {0}")]
+ IO(#[source] std::io::Error),
+ /// Accounts database operation failed.
+ #[error("accountsdb: {0}")]
+ AccountsDB(#[source] AccountsDBError),
+ /// Snapshot creation, restore, or archive operation failed.
+ #[error("snapshot: {0}")]
+ Snapshot(#[source] SnapshotError),
+ /// Ledger initialization or append failed.
+ #[error("ledger: {0}")]
+ Ledger(#[source] LedgerError),
+ /// A background service is no longer reachable, so the request was dropped.
+ #[error("service became unavailable: {0:?}")]
+ ServiceUnavailable(Service),
+ /// Ledger read request failed before a response was received.
+ #[error("ledger read request: {0}")]
+ LedgerRequest(#[source] LedgerRequestError),
+ /// A process-lifetime unicast stream was already registered.
+ #[error("subscription already registered: {0}")]
+ SubscriptionRegistered(&'static str),
+}
+
+impl From> for KeeperError {
+ fn from(_: SendError) -> Self {
+ Self::ServiceUnavailable(Service::LedgerAppender)
+ }
+}
+
+impl From> for KeeperError {
+ fn from(_: SendError) -> Self {
+ Self::ServiceUnavailable(Service::LedgerReader)
+ }
+}
+
+/// Result type used by keeper APIs.
+pub type Result = std::result::Result;
diff --git a/keeper/src/lib.rs b/keeper/src/lib.rs
new file mode 100644
index 00000000..8e54b129
--- /dev/null
+++ b/keeper/src/lib.rs
@@ -0,0 +1,245 @@
+#![doc = include_str!("../README.md")]
+
+use std::{
+ fs::{self, File},
+ path::PathBuf,
+ sync::Arc,
+ thread,
+};
+
+use agave_feature_set::FeatureSet;
+use solana_account::AccountBuilder;
+use solana_hash::Hash;
+use solana_keypair::Keypair;
+use solana_pubkey::Pubkey;
+use tokio::sync::mpsc;
+use tracing::{error, info, warn};
+
+use accountsdb::{AccountsDB, SnapshotError};
+use ledger::{
+ LedgerHandle, Superblock,
+ request::{ReadRequest, ReplayHandle, ReplayParams, RequestPayload},
+ schema::Event,
+};
+use nucleus::{
+ Slot,
+ config::Authority,
+ ledger::{ACCOUNTSDB_SNAPSHOT_FILE, SuperblockSeal},
+};
+use solana_sysvar::rent::Rent;
+
+use crate::{
+ accessor::{AccountsAccessor, BlocksAccessor, SuperblockAccessor, TransactionsAccessor},
+ builder::SPONSOR_INIT_BALANCE,
+ cache::Caches,
+ error::Result,
+ metrics::Operation,
+ subscriptions::Subscriptions,
+};
+
+pub use cache::{AccountLoad, AccountWait, MissingAccount};
+/// Re-exported so callers can name what `Keeper::transactions().status()` returns.
+pub use ledger::request::TransactionStatus;
+pub use nucleus::runtime::{
+ ExecutionRecord, FullTransaction, ResolvedTransaction, TransactionView,
+};
+
+mod accessor;
+pub mod builder;
+mod cache;
+pub mod error;
+mod metrics;
+mod subscriptions;
+mod util;
+
+#[cfg(feature = "testkit")]
+pub mod testkit;
+
+#[cfg(test)]
+mod tests;
+
+/// Owns the durable state and live access helpers for the execution engine.
+pub struct Keeper {
+ /// Local signer and optional remote authority represented by this engine.
+ authority: Authority,
+ /// Active feature set governing runtime behavior.
+ featureset: FeatureSet,
+ /// Rent parameters applied during execution.
+ rent: Rent,
+ /// Account state store.
+ accountsdb: AccountsDB,
+ /// Ledger worker handles and append path.
+ ledger: LedgerHandle,
+ /// Read-side caches shared by accessors.
+ caches: Caches,
+ /// Subscription fanout maps for live updates.
+ subscriptions: Arc,
+}
+
+impl Keeper {
+ /// Returns the account operations namespace.
+ pub fn accounts(&self) -> AccountsAccessor<'_> {
+ AccountsAccessor { keeper: self }
+ }
+
+ /// Returns the transaction operations namespace.
+ pub fn transactions(&self) -> TransactionsAccessor<'_> {
+ TransactionsAccessor { keeper: self }
+ }
+
+ /// Returns the block operations namespace.
+ pub fn blocks(&self) -> BlocksAccessor<'_> {
+ BlocksAccessor { keeper: self }
+ }
+
+ /// Returns the superblock operations namespace.
+ pub fn superblocks(&self) -> SuperblockAccessor<'_> {
+ SuperblockAccessor { keeper: self }
+ }
+
+ /// Returns the configured remote authority, or the local identity when unset.
+ pub fn authority(&self) -> Pubkey {
+ self.authority.pubkey()
+ }
+
+ /// Returns the local signer, which may differ from [`Self::authority`].
+ pub fn signer(&self) -> &Keypair {
+ &self.authority.local
+ }
+
+ /// Returns the latest block hash
+ pub fn blockhash(&self) -> Hash {
+ self.blocks().latest().hash
+ }
+
+ /// Borrows the handle used for direct ledger reads, appends, and
+ /// durable-position subscriptions.
+ pub fn ledger(&self) -> &LedgerHandle {
+ &self.ledger
+ }
+
+ /// Streams retained ledger entries after accountsdb's sealed superblock up to
+ /// the ledger tip, used to rebuild state after snapshot restoration. Returns
+ /// `None` when accountsdb is already current by slot and transaction count.
+ pub async fn replay(&self) -> Result> {
+ let ledger_slot = self.ledger.tip().unwrap_or_default();
+ let accountsdb_slot = self.accountsdb.slot();
+ let ledger_txns = self.ledger.transactions();
+ let accountsdb_txns = self.accountsdb.transactions();
+ if accountsdb_slot >= ledger_slot && accountsdb_txns >= ledger_txns {
+ return Ok(None);
+ };
+ let (tx, rx) = mpsc::channel(16);
+ let params = ReplayParams {
+ tx,
+ superblock: self.accountsdb.superblock(),
+ };
+ let (payload, response) = RequestPayload::new(params);
+ let handle = ReplayHandle { rx, response };
+ self.ledger.reader.send_async(ReadRequest::Replay(payload)).await?;
+ warn!(
+ accountsdb_slot,
+ ledger_slot, accountsdb_txns, ledger_txns, "starting ledger replay"
+ );
+ Ok(Some(handle))
+ }
+
+ /// Seal the current superblock and archive the matching accounts snapshot.
+ ///
+ /// Must run only when no account store can race the snapshot export; the
+ /// in-body `SAFETY` note relies on this exclusivity.
+ pub fn finalize_superblock(&self) -> Result<()> {
+ let _timer = metrics::time(Operation::FinalizeSuperblock);
+ let head = self.ledger.head();
+ let next = head + 1;
+ // SAFETY: `snapshot` requires exclusive write access to accountsdb,
+ // i.e. no store operation may race the export. `finalize_superblock`
+ // is only run when there're no concurrent mutations taking place
+ let snapshot = unsafe { self.accountsdb.snapshot(head) }?;
+ let checksum = self.accountsdb.checksum();
+ let transactions = self.accountsdb.transactions();
+ let seal = SuperblockSeal { id: head, checksum, transactions };
+ self.superblocks().append(seal)?;
+ let dir = Superblock::init_dir(&self.ledger.directory, next)?;
+ self.archive(snapshot, dir)?;
+ info!(head, "finalized superblock");
+ Ok(())
+ }
+
+ /// Resolved once from the seeded feature accounts at startup and fixed for
+ /// the engine's lifetime — features never activate mid-run.
+ pub fn features(&self) -> &FeatureSet {
+ &self.featureset
+ }
+
+ /// Supplied by the builder at startup and fixed for the engine's lifetime;
+ /// the same parameters that sized the seeded accounts.
+ pub fn rent(&self) -> &Rent {
+ &self.rent
+ }
+
+ /// Waits for queued ledger work to become durable, then synchronously
+ /// flushes persisted account storage. Volatile accounts are not serialized.
+ ///
+ /// A final sync closes every ledger reader and the appender. It is
+ /// irreversible and must only be used during coordinated shutdown.
+ pub fn sync(&self, is_final: bool) -> Result<()> {
+ if is_final {
+ for _ in 0..self.ledger.reader.receiver_count() {
+ self.ledger.reader.send(ReadRequest::Shutdown)?;
+ }
+ }
+ self.superblocks().sync(is_final)?;
+ self.accountsdb.flush(true).map_err(Into::into)
+ }
+
+ /// Appends a reset marker before discarding chain-synchronized volatile
+ /// accounts and restoring the authority sponsor's initial balance.
+ ///
+ /// Internal system accounts and persisted engine-authoritative state remain
+ /// available.
+ pub fn reset(&self, slot: Slot) -> Result<()> {
+ self.ledger.appender.send(Event::Reset(slot))?;
+ self.accountsdb.reset();
+ let authority = self.authority();
+ let account = self.accounts().loader().load(&authority)?;
+ if let Some(account) = account {
+ let acc = AccountBuilder::from(account).lamports(SPONSOR_INIT_BALANCE);
+ self.accounts().store(&[(authority, acc.build())])?;
+ }
+ info!(slot, "reset volatile state");
+ Ok(())
+ }
+
+ /// Spawns a background thread that tars and zstd-compresses the accountsdb
+ /// snapshot at `snapshot` into `target`, removing the snapshot afterward.
+ fn archive(
+ &self,
+ snapshot: PathBuf,
+ target: PathBuf,
+ ) -> std::result::Result<(), SnapshotError> {
+ let path = target.join(ACCOUNTSDB_SNAPSHOT_FILE);
+ let tmp = target.join(format!("{ACCOUNTSDB_SNAPSHOT_FILE}.tmp"));
+ let dst = File::options().write(true).create(true).truncate(true).open(&tmp)?;
+ let subscriptions = self.subscriptions.clone();
+ thread::Builder::new().name("snapshot-archiver".into()).spawn(move || {
+ {
+ let _timer = metrics::time(Operation::ArchiveSnapshot);
+ let mut tar = tar::Builder::new(zstd::Encoder::new(dst, 0)?);
+ tar.append_dir_all(".", &snapshot)?;
+ {
+ let archive = tar.into_inner()?.finish()?;
+ metrics::snapshot_size(archive.metadata()?.len());
+ archive.sync_data()?;
+ }
+ // Rename only after sync so replication cannot serve a partial archive.
+ fs::rename(tmp, &path)?;
+ fs::remove_dir_all(snapshot)?;
+ subscriptions.snapshots.send(&(), &path);
+ Ok::<(), SnapshotError>(())
+ }
+ .inspect_err(|error| error!(?error, "snapshot archival failed"))
+ })?;
+ Ok(())
+ }
+}
diff --git a/keeper/src/metrics.rs b/keeper/src/metrics.rs
new file mode 100644
index 00000000..aa9654be
--- /dev/null
+++ b/keeper/src/metrics.rs
@@ -0,0 +1,152 @@
+//! Prometheus metrics for keeper.
+
+use std::sync::OnceLock;
+
+use nucleus::metrics::{self as metric, OperationTimer};
+use nucleus::metrics::{
+ IntCounter, IntCounterVec, IntGauge, MetricOperation, MetricSpec, OperationCounters,
+};
+
+use crate::subscriptions::Subscription;
+
+/// Process-wide keeper metrics registered in the default Prometheus registry.
+static METRICS: OnceLock = OnceLock::new();
+
+/// Operation latency histogram recorded in microseconds.
+const OPERATION_TIME: MetricSpec = MetricSpec {
+ name: "keeper_operation_duration_micros",
+ help: "Keeper operation duration distribution in microseconds.",
+};
+/// Account load cache entries.
+const ACCOUNT_CACHE_ENTRIES: MetricSpec = MetricSpec {
+ name: "keeper_account_cache_entries",
+ help: "Current account load cache entries.",
+};
+/// Block hash cache entries.
+const BLOCK_HASH_CACHE_ENTRIES: MetricSpec = MetricSpec {
+ name: "keeper_block_hash_cache_entries",
+ help: "Current block hash cache entries.",
+};
+/// Most recently completed snapshot archive size.
+const SNAPSHOT_SIZE: MetricSpec = MetricSpec {
+ name: "keeper_snapshot_size",
+ help: "Most recently completed snapshot archive size in bytes.",
+};
+/// Account cache eviction counter.
+const ACCOUNT_CACHE_EVICTIONS: MetricSpec = MetricSpec {
+ name: "keeper_account_cache_evictions",
+ help: "Account cache evictions.",
+};
+/// Account resolution conflict counter.
+const ACCOUNT_RESOLUTION_RACES: MetricSpec = MetricSpec {
+ name: "keeper_account_resolution_races",
+ help: "Account resolution race conditions.",
+};
+/// Slow multicast consumer disconnection counter.
+const SLOW_CONSUMER_DISCONNECTS: MetricSpec = MetricSpec {
+ name: "keeper_subscription_slow_consumer_disconnects",
+ help: "Multicast receivers disconnected because their queue was full.",
+};
+
+/// Keeper operation used as a low-cardinality operation label.
+#[derive(Clone, Copy)]
+pub(crate) enum Operation {
+ /// Superblock finalization path.
+ FinalizeSuperblock,
+ /// Idle subscription cleanup path.
+ Cleanup,
+ /// Snapshot tar/zstd archival path.
+ ArchiveSnapshot,
+}
+
+impl MetricOperation for Operation {
+ /// Returns the Prometheus label value for this operation.
+ fn label(self) -> &'static str {
+ match self {
+ Operation::FinalizeSuperblock => "finalize_superblock",
+ Operation::Cleanup => "cleanup",
+ Operation::ArchiveSnapshot => "archive_snapshot",
+ }
+ }
+}
+
+/// Registers keeper metrics once in the default Prometheus registry.
+pub(crate) fn init() {
+ METRICS.get_or_init(Default::default);
+}
+
+/// Starts an operation timer that records latency when the returned guard drops.
+pub(crate) fn time(op: Operation) -> OperationTimer<'static> {
+ op.time(METRICS.get().map(|m| &m.operations))
+}
+
+/// Records an account cache insertion that increases current occupancy.
+pub(crate) fn account_cache_insert() {
+ metric::with_metrics(&METRICS, |m| m.account_cache_entries.inc());
+}
+
+/// Refreshes block hash cache entry gauge.
+pub(crate) fn block_hash_entries(count: usize) {
+ metric::with_metrics(&METRICS, |m| {
+ m.block_hash_cache_entries.set(metric::gauge_value(count))
+ });
+}
+
+/// Records the most recently completed snapshot archive size in bytes.
+pub(crate) fn snapshot_size(bytes: u64) {
+ metric::with_metrics(&METRICS, |m| {
+ m.snapshot_size.set(metric::gauge_value(bytes))
+ });
+}
+
+/// Records one account cache eviction.
+pub(crate) fn account_cache_eviction() {
+ metric::with_metrics(&METRICS, |m| m.account_cache_evictions.inc());
+}
+
+/// Records one account resolution race condition.
+pub(crate) fn account_resolution_race() {
+ metric::with_metrics(&METRICS, |m| m.account_resolution_race.inc());
+}
+
+/// Records one receiver disconnected because its queue was full.
+pub(crate) fn slow_consumer_disconnect(subscription: Subscription) {
+ metric::with_metrics(&METRICS, |m| {
+ m.slow_consumer_disconnects.with_label_values(&[subscription.label()]).inc()
+ });
+}
+
+/// Owns all Prometheus collectors registered by keeper.
+struct Metrics {
+ /// Runtime operation duration and completion counters.
+ operations: OperationCounters,
+ /// Account cache entry gauge.
+ account_cache_entries: IntGauge,
+ /// Block hash cache entry gauge.
+ block_hash_cache_entries: IntGauge,
+ /// Most recently completed snapshot archive size in bytes.
+ snapshot_size: IntGauge,
+ /// Account cache eviction counter.
+ account_cache_evictions: IntCounter,
+ /// Account resolution race conditions counter.
+ account_resolution_race: IntCounter,
+ /// Slow consumer disconnections labeled by subscription stream.
+ slow_consumer_disconnects: IntCounterVec,
+}
+impl Default for Metrics {
+ /// Builds collectors and registers them in the default Prometheus registry.
+ fn default() -> Self {
+ Self {
+ operations: OperationCounters::new(OPERATION_TIME),
+ account_cache_entries: metric::gauge(ACCOUNT_CACHE_ENTRIES, 0),
+ block_hash_cache_entries: metric::gauge(BLOCK_HASH_CACHE_ENTRIES, 0),
+ snapshot_size: metric::gauge(SNAPSHOT_SIZE, 0),
+ account_cache_evictions: metric::counter(ACCOUNT_CACHE_EVICTIONS, 0),
+ account_resolution_race: metric::counter(ACCOUNT_RESOLUTION_RACES, 0),
+ slow_consumer_disconnects: metric::counter_vec(
+ SLOW_CONSUMER_DISCONNECTS,
+ &["subscription"],
+ ),
+ }
+ }
+}
diff --git a/keeper/src/subscriptions.rs b/keeper/src/subscriptions.rs
new file mode 100644
index 00000000..1fe11373
--- /dev/null
+++ b/keeper/src/subscriptions.rs
@@ -0,0 +1,317 @@
+//! Live read-side notification channels.
+
+use std::{
+ hash::Hash,
+ path::PathBuf,
+ sync::{Arc, OnceLock},
+ time::Duration,
+};
+
+use accountsdb::AccountEntry;
+use ahash::RandomState;
+use ledger::{request::TransactionStatus, schema::Block};
+use nucleus::{
+ shutdown::{Service, ShutdownHandle, ShutdownManager, ShutdownReason},
+ tls::EncodedMessage,
+};
+use scc::HashMap;
+use smallvec::SmallVec;
+use solana_account::AccountSharedData;
+use solana_pubkey::Pubkey;
+use solana_signature::Signature;
+use solana_transaction_error::TransactionResult;
+use tokio::{
+ sync::mpsc::{self, error::TrySendError},
+ time::{MissedTickBehavior, interval},
+};
+
+use crate::{
+ FullTransaction,
+ error::{KeeperError, Result},
+ metrics::{self, Operation},
+};
+
+type MpscSenders = SmallVec<[mpsc::Sender; 1]>;
+type OneshotSenders = SmallVec<[oneshot::Sender; 1]>;
+
+/// Stable metric identity for a subscription stream.
+#[derive(Clone, Copy)]
+pub(crate) enum Subscription {
+ Accounts,
+ Programs,
+ Logs,
+ Blocks,
+ Transactions,
+ Snapshots,
+ Services,
+ Evictions,
+}
+
+impl Subscription {
+ pub(crate) const fn label(self) -> &'static str {
+ match self {
+ Self::Accounts => "accounts",
+ Self::Programs => "programs",
+ Self::Logs => "logs",
+ Self::Blocks => "blocks",
+ Self::Transactions => "transactions",
+ Self::Snapshots => "snapshots",
+ Self::Services => "services",
+ Self::Evictions => "evictions",
+ }
+ }
+}
+
+/// Live notification channels owned by keeper.
+pub(crate) struct Subscriptions {
+ /// Account updates keyed by account pubkey.
+ pub(crate) accounts: Multicast,
+ /// Program account updates keyed by owner pubkey.
+ pub(crate) programs: Multicast,
+ /// Signature status updates keyed by transaction signature.
+ pub(crate) signatures: MulticastOneshot,
+ /// Log notifications keyed by mentioned program or account pubkey.
+ pub(crate) logs: Multicast>,
+ /// Newly committed blocks.
+ pub(crate) blocks: Multicast<(), Block>,
+ /// All committed transactions for the sole stream consumer.
+ pub(crate) transactions: Unicast,
+ /// Accountsdb snapshot archive completions.
+ pub(crate) snapshots: Multicast<(), PathBuf>,
+ /// Encoded service messages for the sole stream consumer.
+ pub(crate) services: Unicast,
+}
+
+impl Subscriptions {
+ /// Builds subscription channels and starts cleanup for idle keyed entries.
+ pub(crate) fn new(shutdown: &mut ShutdownManager) -> Arc {
+ let subscriptions = Arc::new(Self {
+ accounts: Multicast::new(8, Subscription::Accounts),
+ programs: Multicast::new(16, Subscription::Programs),
+ signatures: Default::default(),
+ logs: Multicast::new(8, Subscription::Logs),
+ blocks: Multicast::new(32, Subscription::Blocks),
+ transactions: Unicast::new(1024, Subscription::Transactions),
+ snapshots: Multicast::new(4, Subscription::Snapshots),
+ services: Unicast::new(64, Subscription::Services),
+ });
+ let shutdown = shutdown.handle(Service::SubscriptionsCleanup);
+ tokio::spawn(cleanup(subscriptions.clone(), shutdown));
+ subscriptions
+ }
+
+ async fn cleanup(&self) {
+ self.accounts.cleanup().await;
+ self.programs.cleanup().await;
+ self.signatures.cleanup().await;
+ self.logs.cleanup().await;
+ self.blocks.cleanup().await;
+ self.snapshots.cleanup().await;
+ }
+}
+
+/// Composite log notification sent to log subscribers.
+#[derive(Clone)]
+pub struct TransactionLogs {
+ /// First transaction signature.
+ pub signature: Signature,
+ /// Runtime transaction result (carries the error on failure).
+ pub result: TransactionResult<()>,
+ /// Log lines emitted during execution.
+ pub logs: Arc>,
+}
+
+/// One process-lifetime bounded receiver.
+pub(crate) struct Unicast {
+ sender: OnceLock>,
+ capacity: usize,
+ subscription: Subscription,
+}
+
+impl Unicast {
+ pub(crate) const fn new(capacity: usize, subscription: Subscription) -> Self {
+ Self {
+ sender: OnceLock::new(),
+ capacity,
+ subscription,
+ }
+ }
+
+ /// Creates the process-lifetime receiver, rejecting every later subscriber.
+ pub(crate) fn subscribe(&self) -> Result> {
+ let (tx, rx) = mpsc::channel(self.capacity);
+ self.sender
+ .set(tx)
+ .map_err(|_| KeeperError::SubscriptionRegistered(self.subscription.label()))?;
+ Ok(rx)
+ }
+
+ /// Sends asynchronously, waiting until the receiver has capacity.
+ pub(crate) async fn send(&self, value: V) {
+ let Some(sender) = self.sender.get() else {
+ return;
+ };
+ let _ = sender.send(value).await;
+ }
+
+ /// Sends from a synchronous worker, waiting until the receiver has capacity.
+ pub(crate) fn blocking_send(&self, value: V) {
+ let Some(sender) = self.sender.get() else {
+ return;
+ };
+ let _ = sender.blocking_send(value);
+ }
+}
+
+/// Persistent per-key fanout over one bounded queue per receiver.
+pub(crate) struct Multicast {
+ senders: HashMap, RandomState>,
+ capacity: usize,
+ subscription: Subscription,
+}
+
+impl Multicast
+where
+ K: Eq + Hash,
+{
+ pub(crate) fn new(capacity: usize, subscription: Subscription) -> Self {
+ Self {
+ senders: Default::default(),
+ capacity,
+ subscription,
+ }
+ }
+
+ /// Adds a receiver for `key` with its own bounded queue.
+ pub(crate) async fn subscribe(&self, key: K) -> mpsc::Receiver {
+ let (tx, rx) = mpsc::channel(self.capacity);
+ self.senders.entry_async(key).await.or_default().push(tx);
+ rx
+ }
+
+ /// Adds a receiver synchronously when the public accessor cannot await.
+ pub(crate) fn subscribe_sync(&self, key: K) -> mpsc::Receiver {
+ let (tx, rx) = mpsc::channel(self.capacity);
+ self.senders.entry_sync(key).or_default().push(tx);
+ rx
+ }
+
+ /// Returns whether `key` has any live receivers.
+ pub(crate) fn contains(&self, key: &K) -> bool {
+ let mut contains = false;
+ self.senders.remove_if_sync(key, |senders| {
+ senders.retain(|sender| !sender.is_closed());
+ contains = !senders.is_empty();
+ !contains
+ });
+ contains
+ }
+
+ /// Drops closed receivers and keys that no longer have receivers.
+ async fn cleanup(&self) {
+ self.senders
+ .retain_async(|_, senders| {
+ senders.retain(|sender| !sender.is_closed());
+ !senders.is_empty()
+ })
+ .await;
+ }
+}
+
+impl Multicast
+where
+ K: Eq + Hash,
+ V: Clone,
+{
+ /// Fans out without blocking, disconnecting receivers whose queues are full.
+ pub(crate) fn send(&self, key: &K, value: &V) {
+ self.senders.remove_if_sync(key, |senders| {
+ senders.retain(|sender| match sender.try_send(value.clone()) {
+ Ok(()) => true,
+ Err(TrySendError::Full(_)) => {
+ metrics::slow_consumer_disconnect(self.subscription);
+ false
+ }
+ Err(TrySendError::Closed(_)) => false,
+ });
+ senders.is_empty()
+ });
+ }
+}
+
+/// Terminal per-key fanout over one oneshot channel per receiver.
+pub(crate) struct MulticastOneshot(HashMap, RandomState>);
+
+impl Default for MulticastOneshot {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl MulticastOneshot
+where
+ K: Eq + Hash,
+{
+ /// Adds a receiver for the terminal value associated with `key`.
+ pub(crate) async fn subscribe(&self, key: K) -> oneshot::Receiver {
+ let (tx, rx) = oneshot::channel();
+ self.0.entry_async(key).await.or_default().push(tx);
+ rx
+ }
+
+ /// Drops closed receivers and keys that no longer have receivers.
+ async fn cleanup(&self) {
+ self.0
+ // Keep closed positions while any receiver is live so `send_last`
+ // cannot mistake an older subscription for the newest one.
+ .retain_async(|_, senders| senders.iter().any(|sender| !sender.is_closed()))
+ .await;
+ }
+}
+
+impl MulticastOneshot
+where
+ K: Eq + Hash,
+ V: Clone,
+{
+ /// Sends the terminal value only to the most recently added receiver.
+ pub(crate) fn send_last(&self, key: &K, value: &V) {
+ let Some(mut senders) = self.0.get_sync(key) else {
+ return;
+ };
+ let sender = senders.pop();
+ if senders.is_empty() {
+ let _ = senders.remove_entry();
+ }
+ if let Some(sender) = sender {
+ let _ = sender.send(value.clone());
+ }
+ }
+
+ /// Removes `key` and sends its terminal value to every current receiver.
+ pub(crate) fn send(&self, key: &K, value: &V) {
+ let Some((_, senders)) = self.0.remove_sync(key) else {
+ return;
+ };
+ for sender in senders {
+ let _ = sender.send(value.clone());
+ }
+ }
+}
+
+/// Drops abandoned multicast senders after their receivers are gone.
+async fn cleanup(subscriptions: Arc, mut shutdown: ShutdownHandle) {
+ let mut ticker = interval(Duration::from_secs(60));
+ ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
+ loop {
+ tokio::select! {
+ biased;
+ _ = shutdown.signalled() => break,
+ _ = ticker.tick() => {
+ let _timer = metrics::time(Operation::Cleanup);
+ subscriptions.cleanup().await;
+ }
+ }
+ }
+ shutdown.terminate(ShutdownReason::Signalled);
+}
diff --git a/keeper/src/testkit.rs b/keeper/src/testkit.rs
new file mode 100644
index 00000000..0595ed25
--- /dev/null
+++ b/keeper/src/testkit.rs
@@ -0,0 +1,273 @@
+//! Keeper-level test harness shared by keeper and processor test suites.
+//!
+//! Builds a real [`Keeper`] over throwaway directories with the canonical test
+//! parameters (retention disabled, 400 ms blocktime, superblock 16), and exposes
+//! the loadable v42 calculator program guaranteed by `build.rs`. The low-level,
+//! engine-agnostic builders (transactions, blocks, tempdirs) are re-exported from
+//! [`nucleus::testkit`]. Compiled only under the `testkit` feature (or a crate's
+//! own `cfg(test)`), so it never reaches release builds.
+#![allow(clippy::expect_used, clippy::unwrap_used)]
+
+use std::{
+ collections::HashMap,
+ fs::{self, File},
+ num::NonZeroU64,
+ path::{Path, PathBuf},
+ sync::Arc,
+ time::Duration,
+};
+
+use accountsdb::{AccountsDB, STORAGE_FILE};
+use derive_more::Deref;
+use nucleus::{
+ config::{AccountsDBParams, BlockstoreParams, LedgerParams},
+ ledger::ACCOUNTSDB_SNAPSHOT_FILE,
+ shutdown::ShutdownManager,
+ testkit::signed_view as compose_view,
+};
+use solana_account::{AccountBuilder, AccountMode, ReadableAccount};
+use solana_hash::Hash;
+use solana_instruction::{AccountMeta, Instruction};
+use solana_keypair::Keypair;
+use solana_pubkey::Pubkey;
+use solana_signature::Signature;
+use solana_signer::Signer;
+use solana_sysvar::rent::Rent;
+
+pub use nucleus::testkit::{
+ TempDir, V42_ID, WireVersion, block, init_tracing, patterned_bytes,
+ sign_versioned_instructions, tempdir, transaction, v42_padded_value, v42_sum,
+};
+use tokio::time;
+
+use crate::{Keeper, ResolvedTransaction, TransactionView, builder::KeeperBuilder};
+
+/// The v42 calculator ELF, built and located by `keeper/build.rs`.
+pub const V42_PROGRAM_ELF: &[u8] = include_bytes!(env!("V42_CALCULATOR_PROGRAM_SO"));
+/// Slots sealed into each superblock by the standard test engine.
+pub const SUPERBLOCK: NonZeroU64 = NonZeroU64::new(4).unwrap();
+
+/// Throwaway on-disk homes for the accountsdb and ledger stores.
+///
+/// The directories must outlive every keeper opened over them — the stores keep
+/// their files open/mmapped — which is why the recovery tests hold `Dirs` across
+/// a full close-and-reopen cycle.
+pub struct Dirs {
+ /// Accounts database directory.
+ pub accounts: TempDir,
+ /// Ledger directory.
+ pub ledger: TempDir,
+}
+
+impl Default for Dirs {
+ fn default() -> Self {
+ Self {
+ accounts: tempdir(),
+ ledger: tempdir(),
+ }
+ }
+}
+
+/// A keeper builder over `dirs` with retention disabled and a 100 ms blocktime.
+///
+/// `builtins` and `accounts` default to empty and `programs` holds only v42;
+/// individual tests fill the rest as needed. [`TestKeeper::new`] is the seeded
+/// path, adding a funded payer on top.
+pub fn keeper_builder(dirs: &Dirs) -> KeeperBuilder {
+ let mut programs = HashMap::new();
+ programs.insert(V42_ID, V42_PROGRAM_ELF.to_vec());
+ init_tracing();
+
+ KeeperBuilder {
+ authority: Keypair::new().into(),
+ accountsdb: AccountsDBParams {
+ directory: dirs.accounts.path().to_owned(),
+ lru_capacity: 256,
+ },
+ ledger: LedgerParams {
+ directory: dirs.ledger.path().to_owned(),
+ size_limit: u64::MAX,
+ },
+ blockstore: BlockstoreParams {
+ blocktime: Duration::from_millis(100),
+ superblock: SUPERBLOCK,
+ },
+ builtins: Default::default(),
+ programs,
+ accounts: Default::default(),
+ rent: Rent::default(),
+ }
+}
+
+/// A built keeper together with the directories and shutdown manager keeping its
+/// background services alive. Derefs to [`Keeper`] for accessor calls.
+///
+/// The keeper is seeded at construction with the v42 program and one funded
+/// payer, so keeper-backed suites can build and run transactions immediately
+/// instead of re-loading the ELF or re-storing a signer per test.
+#[derive(Deref)]
+pub struct TestKeeper {
+ /// Directories backing this keeper, returned by [`Self::close`] so a test can
+ /// reopen over the same on-disk state.
+ pub dirs: Dirs,
+ /// Lifecycle manager owning the keeper's background services; exposed so
+ /// tests can register their own services against the same shutdown.
+ pub shutdown: ShutdownManager,
+ #[deref]
+ keeper: Arc,
+}
+
+impl TestKeeper {
+ /// Builds a keeper on fresh directories seeded with v42 and a funded payer.
+ pub async fn new() -> Self {
+ Self::with(Dirs::default()).await
+ }
+
+ /// [`Self::new`] over `dirs`, which may already hold state from an earlier
+ /// keeper closed over the same directories.
+ pub async fn with(dirs: Dirs) -> Self {
+ let mut builder = keeper_builder(&dirs);
+ let payer = Keypair::new();
+ builder.accounts.insert(
+ payer.pubkey(),
+ AccountBuilder::default().lamports(1_000_000).build(),
+ );
+ Self::from_builder(dirs, builder).await
+ }
+
+ /// Builds a keeper from a caller-configured builder.
+ ///
+ /// `dirs` must own the directories referenced by `builder` and outlive the
+ /// resulting keeper. Unlike [`Self::with`], nothing is seeded beyond what the
+ /// builder already carries.
+ pub async fn from_builder(dirs: Dirs, builder: KeeperBuilder) -> Self {
+ let mut shutdown = ShutdownManager::default();
+ let keeper = Arc::new(builder.build(&mut shutdown).await.unwrap());
+ Self { dirs, shutdown, keeper }
+ }
+
+ /// Flushes durable state, stops every background service, and returns the
+ /// directories for reopen.
+ ///
+ /// The flush republishes a valid accountsdb checksum, so a test that wants a
+ /// corrupt store must call [`corrupt`] on the returned directories *after*
+ /// this, never before.
+ pub async fn close(self) -> Dirs {
+ let Self { mut shutdown, keeper, dirs } = self;
+ keeper.sync(true).unwrap();
+ shutdown.terminate().await;
+ dirs
+ }
+}
+
+/// A signed, resolved no-op transaction and its first signature, built the same
+/// way the sequencer resolves inbound transactions.
+pub fn signed_tx() -> (Signature, ResolvedTransaction) {
+ let (signature, bytes) = transaction(&[]);
+ let view = TransactionView::try_new_sanitized(bytes, true).unwrap();
+ let resolved =
+ ResolvedTransaction::try_new(view, Some(Default::default()), &Default::default()).unwrap();
+ (signature, resolved)
+}
+
+/// A resolved transaction whose account metadata matches `accounts`.
+///
+/// Each tuple is `(pubkey, writable)`. The transaction is fully sanitized and
+/// resolved so scheduling sees the same account flags the keeper resolution path
+/// would produce. A fresh random program id per call keeps the referenced account
+/// set disjoint from other transactions under test.
+pub fn resolved(accounts: &[(Pubkey, bool)]) -> ResolvedTransaction {
+ let payer = Keypair::new();
+ let program = Pubkey::new_unique();
+ let metas = accounts
+ .iter()
+ .map(|(key, writable)| {
+ if *writable {
+ AccountMeta::new(*key, false)
+ } else {
+ AccountMeta::new_readonly(*key, false)
+ }
+ })
+ .collect();
+ let ix = Instruction::new_with_bytes(program, &[], metas);
+ let (_signature, view) = compose_view(&payer, [ix], Hash::default());
+ ResolvedTransaction::try_new(view, Some(Default::default()), &Default::default()).unwrap()
+}
+
+/// Configures a v42 account carrying an 8-byte little-endian `i64` and twice
+/// its rent-exempt minimum, leaving one reserve available for transfer tests.
+pub fn v42_builder(value: i64, mode: AccountMode) -> AccountBuilder {
+ AccountBuilder::default()
+ .lamports(Rent::default().minimum_balance(8) * 2)
+ .owner(V42_ID)
+ .mode(mode)
+ .data(value.to_le_bytes().to_vec())
+}
+
+/// Stores a funded v42 `i64` account in `mode` and returns its pubkey.
+pub fn store_v42(keeper: &Keeper, value: i64, mode: AccountMode) -> Pubkey {
+ let key = Pubkey::new_unique();
+ keeper.accounts().store(&[(key, v42_builder(value, mode).build())]).unwrap();
+ key
+}
+
+/// Reads the little-endian `i64` payload of a v42 account, or `None` if absent.
+pub fn load_v42_data(keeper: &Keeper, key: Pubkey) -> Option {
+ keeper.accounts().loader().read(&key, decode_v42).unwrap()
+}
+
+/// Reads the lamport balance of a stored v42 account, or `None` if absent.
+pub fn load_v42_lamports(keeper: &Keeper, key: Pubkey) -> Option {
+ keeper.accounts().loader().read(&key, ReadableAccount::lamports).unwrap()
+}
+
+/// Signs `instruction` into the sanitized transaction view consumed by services.
+pub fn signed_view(
+ keeper: &Keeper,
+ payer: Option<&Keypair>,
+ instruction: Instruction,
+) -> (Signature, TransactionView) {
+ let payer = payer.unwrap_or(keeper.signer());
+ compose_view(payer, [instruction], keeper.blockhash())
+}
+
+/// Returns the archived accountsdb snapshot path under any retained superblock,
+/// or `None` when no superblock directory holds one yet.
+pub fn archived_snapshot(keeper: &Keeper) -> Option {
+ fs::read_dir(&keeper.ledger.directory)
+ .unwrap()
+ .filter_map(Result::ok)
+ .map(|e| e.path().join(ACCOUNTSDB_SNAPSHOT_FILE))
+ .find(|p| p.exists())
+}
+
+/// Waits until a subscribed detached snapshot archiver reports completion.
+pub async fn await_archive(keeper: &Keeper) -> PathBuf {
+ let mut rx = keeper.accounts().subscribe_snapshots();
+ time::timeout(Duration::from_secs(8), rx.recv())
+ .await
+ .expect("snapshot archives in time")
+ .unwrap()
+}
+
+/// Overwrites one `u64` metadata word in the closed persisted store.
+///
+/// `DatabaseMeta` starts with version at offset 0, checksum at 8, slot at 16,
+/// superblock at 24, and committed transaction count at 32.
+///
+/// This must be the last write to the store: `flush(true)` recomputes and
+/// republishes the checksum, so this must run *after* the keeper is closed, never
+/// against a live one.
+pub fn corrupt(root: &Path, offset: u64, value: u64) {
+ use std::io::{Seek, SeekFrom, Write};
+ let path = AccountsDB::directory(root).join(STORAGE_FILE);
+ let mut file = File::options().write(true).open(&path).unwrap();
+ file.seek(SeekFrom::Start(offset)).unwrap();
+ file.write_all(&value.to_ne_bytes()).unwrap();
+ file.flush().unwrap();
+}
+
+/// Decodes the little-endian `i64` payload stored in a v42 account.
+pub fn decode_v42(account: &impl ReadableAccount) -> i64 {
+ i64::from_le_bytes(account.data()[..8].try_into().expect("v42 account holds an i64"))
+}
diff --git a/keeper/src/tests/caches.rs b/keeper/src/tests/caches.rs
new file mode 100644
index 00000000..99fd3557
--- /dev/null
+++ b/keeper/src/tests/caches.rs
@@ -0,0 +1,120 @@
+//! Read-side cache primitives keeper owns: the slot-based `ExpiringCache` and the
+//! `AccountCache` missing-load coordination.
+
+use std::sync::Arc;
+
+use solana_account::{AccountBuilder, AccountMode};
+use solana_pubkey::Pubkey;
+
+use super::TestKeeper;
+use crate::cache::{AccountCache, AccountLoad, AccountWait, ExpiringCache, MissingAccount};
+
+// `ExpiringCache` evicts lazily on push, never on read; re-inserting an existing
+// key is a no-op; `update` replaces only present values.
+#[test]
+fn expiring_cache_lazy_eviction() {
+ // ttl = 2 slots: a key pushed at slot s expires at s + 2.
+ let cache: ExpiringCache = ExpiringCache::new(2);
+
+ assert!(cache.push(1, 10, 0)); // inserted, expires at slot 2
+ assert!(!cache.push(1, 99, 0)); // re-insert of an existing key is a no-op
+ assert_eq!(
+ cache.get(&1),
+ Some(10),
+ "value left unchanged by the re-insert"
+ );
+
+ // Eviction runs only on push: at slot 5 the entry is well past its expiry but
+ // stays readable until the next push sweeps the queue.
+ assert!(cache.contains(&1));
+ assert_eq!(cache.get(&1), Some(10));
+
+ // A push at slot 5 first evicts everything expired at 5 (key 1), then inserts.
+ assert!(cache.push(2, 20, 5));
+ assert!(!cache.contains(&1), "expired key swept on the next push");
+ assert_eq!(cache.get(&2), Some(20));
+
+ // `update` replaces a present value and no-ops for an absent key.
+ cache.update(&2, 21);
+ assert_eq!(cache.get(&2), Some(21));
+ cache.update(&404, 0);
+ assert!(!cache.contains(&404));
+
+ // A key re-admitted after expiry is a fresh insert again.
+ assert!(cache.push(1, 11, 5));
+ assert_eq!(cache.get(&1), Some(11));
+}
+
+// Two callers racing on the same missing account get exactly one loader and one
+// waiter; committing caches the account while dropping the load guard does not.
+#[tokio::test]
+async fn account_load_release_paths_wake_waiters() {
+ use AccountMode::*;
+ let modes = [ReadOnly, Placeholder, Delegated, Ephemeral, Transient, System];
+ for mode in modes {
+ for commit in [true, false] {
+ let cache = Arc::new(AccountCache::new(256));
+ let pk = Pubkey::new_unique();
+ let (load, wait) = reserve_load_and_wait(&cache, pk);
+
+ let waiter = tokio::spawn(async move { wait.wait().await });
+ if commit {
+ load.complete(mode).await;
+ } else {
+ drop(load)
+ }
+
+ assert_eq!(
+ waiter.await.unwrap(),
+ (pk, commit),
+ "waiter returns the load outcome"
+ );
+ let tracked = matches!(mode, ReadOnly | Placeholder | System);
+ assert_eq!(cache.lru.get_sync(&pk).is_some(), tracked && commit);
+ assert!(matches!(cache.reserve(pk), MissingAccount::Load(_)));
+ }
+ }
+}
+
+// `ensure` is the production seam over `AccountCache`: it skips accounts already
+// resident in storage (promoting them) and hands back a coordination item only
+// for the ones missing, with the first caller owning the load.
+#[tokio::test]
+async fn ensure_reserves_only_missing_accounts() {
+ let keeper = TestKeeper::new().await;
+ let present = Pubkey::new_unique();
+ let missing = Pubkey::new_unique();
+ keeper
+ .accounts()
+ .store(&[(present, AccountBuilder::default().lamports(1).build())])
+ .unwrap();
+
+ // The accessor must outlive the iterator that borrows it.
+ let accounts = keeper.accounts();
+ let reserved: Vec<_> = accounts.ensure(&[present, missing]).collect();
+
+ // The resident account is skipped entirely; only the missing one surfaces,
+ // and the first caller to reach it owns the load.
+ assert_eq!(reserved.len(), 1, "only the missing account is reserved");
+ let MissingAccount::Load(load) = &reserved[0] else {
+ panic!("first reservation of a missing account owns the load");
+ };
+ assert_eq!(load.pubkey, missing);
+
+ // The reservation stays live while the load guard is held, so a concurrent
+ // `ensure` of the same account waits instead of racing a second load.
+ let again: Vec<_> = accounts.ensure(&[missing]).collect();
+ assert!(matches!(again.as_slice(), [MissingAccount::Wait(_)]));
+
+ keeper.close().await;
+}
+
+fn reserve_load_and_wait(cache: &Arc, pk: Pubkey) -> (AccountLoad, AccountWait) {
+ let MissingAccount::Load(load) = cache.reserve(pk) else {
+ panic!("first reservation must own the load");
+ };
+ let MissingAccount::Wait(wait) = cache.reserve(pk) else {
+ panic!("concurrent reservation must wait");
+ };
+ (load, wait)
+}
diff --git a/keeper/src/tests/mod.rs b/keeper/src/tests/mod.rs
new file mode 100644
index 00000000..11a3dfae
--- /dev/null
+++ b/keeper/src/tests/mod.rs
@@ -0,0 +1,11 @@
+//! Keeper integration and unit tests.
+//!
+//! These cover the composition layer keeper owns — startup seeding, corruption
+//! recovery, the read-side caches, and subscription fanout — and deliberately
+//! avoid re-testing the accountsdb/ledger internals already covered below it.
+
+mod caches;
+mod recovery;
+mod subscriptions;
+
+use crate::testkit::{TestKeeper, signed_tx};
diff --git a/keeper/src/tests/recovery.rs b/keeper/src/tests/recovery.rs
new file mode 100644
index 00000000..ee18a988
--- /dev/null
+++ b/keeper/src/tests/recovery.rs
@@ -0,0 +1,132 @@
+//! Startup seeding, corruption recovery
+
+use solana_account::{AccountBuilder, AccountMode, ReadableAccount};
+use solana_pubkey::Pubkey;
+use solana_sdk_ids::{loader_v4, sysvar};
+use solana_sysvar::{
+ clock::Clock, epoch_schedule::EpochSchedule, rent::Rent, slot_hashes::SysvarId,
+};
+
+use super::TestKeeper;
+use crate::testkit::{Dirs, archived_snapshot, await_archive, corrupt, keeper_builder};
+
+// Startup seeds the engine's required feature gates, the configured upgradeable
+// programs, and the sysvars, with the exact ownership/rent/clock-offset shape the
+// rest of the engine assumes.
+#[tokio::test]
+async fn seeds_features_programs_and_sysvars() {
+ let dirs = Dirs::default();
+ let mut builder = keeper_builder(&dirs);
+ let program = Pubkey::new_unique();
+ let elf = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
+ builder.programs.insert(program, elf.clone());
+ let keeper = TestKeeper::from_builder(dirs, builder).await;
+ let rent = Rent::default();
+ let accounts = keeper.accounts();
+ let loader = accounts.loader();
+
+ // The engine's required curve25519/precompile/sbpf/sysvar gates are all
+ // active at slot 0, and every active feature is backed by a rent-exempt
+ // feature-gate-owned account.
+ let required = [
+ agave_feature_set::curve25519_syscall_enabled::ID,
+ agave_feature_set::enable_sbpf_v3_deployment_and_execution::ID,
+ agave_feature_set::syscall_parameter_address_restrictions::ID,
+ agave_feature_set::get_sysvar_syscall_enabled::ID,
+ agave_feature_set::ed25519_program_enabled::ID,
+ agave_feature_set::secp256k1_program_enabled::ID,
+ ];
+ for id in required {
+ assert_eq!(
+ keeper.features().active().get(&id),
+ Some(&0),
+ "required gate active at slot 0"
+ );
+ }
+ for (&id, &slot) in keeper.features().active() {
+ assert_eq!(slot, 0, "features activate at slot 0");
+ let acc = loader.load(&id).unwrap().expect("feature account seeded");
+ assert_eq!(acc.owner(), &solana_feature_gate_interface::ID);
+ assert!(acc.lamports() >= rent.minimum_balance(acc.data().len()));
+ }
+
+ // The upgradeable program account carries its ELF verbatim, is executable,
+ // owned by loader_v4 (not the BPF upgradeable loader), and rent-exempt.
+ // Builtins are seeded through the same path with an executable native-loader
+ // account, so they share this shape.
+ let acc = loader.load(&program).unwrap().expect("program seeded");
+ assert!(acc.executable());
+ assert_eq!(acc.owner(), &loader_v4::ID);
+ assert_eq!(acc.data(), elf.as_slice());
+ assert_eq!(acc.lamports(), rent.minimum_balance(elf.len()));
+
+ // The Clock is seeded one slot ahead of the last block; a fresh ledger's last
+ // block defaults to slot 0, so the clock starts at slot 1.
+ let clock: Clock = loader
+ .load(&Clock::id())
+ .unwrap()
+ .expect("clock seeded")
+ .deserialize_data()
+ .unwrap();
+ assert_eq!(clock.slot, 1);
+
+ // Rent and EpochSchedule sysvars are present and sysvar-owned.
+ for id in [Rent::id(), EpochSchedule::id()] {
+ let acc = loader.load(&id).unwrap().expect("sysvar seeded");
+ assert_eq!(acc.owner(), &sysvar::ID);
+ }
+ drop(loader);
+ keeper.close().await;
+}
+
+// A corrupt accountsdb on open is restored from the newest archived snapshot,
+// and the saved corrupt tree is discarded once the restored store revalidates.
+//
+// The marker takes a distinct value in each state the reopen could land on, so
+// the assertion separates all three: 1 is the older snapshot, 2 the newest, and
+// 3 lives only in persisted state (stored after the last archive, so no snapshot
+// holds it). Recovery must yield 2 — reading 3 back would mean the corruption
+// went undetected and nothing was restored at all.
+#[tokio::test]
+async fn recovers_the_newest_snapshot() {
+ let marker = Pubkey::new_unique();
+ let dirs = Dirs::default();
+ let builder = keeper_builder(&dirs);
+ let keeper = TestKeeper::from_builder(dirs, builder.clone()).await;
+
+ // First snapshot captures marker == 1.
+ store_marker(&keeper, marker, 1);
+ keeper.finalize_superblock().expect("first finalize");
+ await_archive(&keeper).await;
+ assert!(
+ archived_snapshot(&keeper).is_some(),
+ "snapshot archived under superblock"
+ );
+ // Second snapshot, in a later superblock, captures marker == 2.
+ store_marker(&keeper, marker, 2);
+ keeper.finalize_superblock().expect("second finalize");
+ await_archive(&keeper).await;
+ // Past every archive: this value is what an un-restored store would keep.
+ store_marker(&keeper, marker, 3);
+ let dirs = keeper.close().await;
+
+ // Corruption must follow the close, whose flush would otherwise republish a
+ // valid checksum over the poisoned word.
+ corrupt(dirs.accounts.path(), 8, 0xABAB_ABAB_ABAB_ABAB);
+
+ let keeper = TestKeeper::from_builder(dirs, builder).await;
+ keeper.accounts().validate().expect("restored store validates");
+ let restored = keeper.accounts().loader().load(&marker).unwrap().expect("marker restored");
+ assert_eq!(restored.lamports(), 2, "newest snapshot wins");
+ // The corrupt tree saved for inspection is removed on successful recovery.
+ assert!(!keeper.dirs.accounts.path().join("CURRENT.bkp").exists());
+
+ keeper.close().await;
+}
+
+/// Stores the recovery marker account at `lamports`, the value each snapshot
+/// captures and recovery must bring back.
+fn store_marker(keeper: &TestKeeper, marker: Pubkey, lamports: u64) {
+ let account = AccountBuilder::default().lamports(lamports).mode(AccountMode::Delegated);
+ keeper.accounts().store(&[(marker, account.build())]).unwrap();
+}
diff --git a/keeper/src/tests/subscriptions.rs b/keeper/src/tests/subscriptions.rs
new file mode 100644
index 00000000..ee063695
--- /dev/null
+++ b/keeper/src/tests/subscriptions.rs
@@ -0,0 +1,121 @@
+//! Subscription fanout primitives, transaction-append dedup
+
+use std::sync::Arc;
+
+use super::{TestKeeper, signed_tx};
+use crate::{
+ ResolvedTransaction,
+ subscriptions::{Multicast, MulticastOneshot, Subscription, Unicast},
+};
+use nucleus::testkit::{V42_ID, signed_view};
+use solana_hash::Hash;
+use solana_instruction::Instruction;
+use solana_keypair::Keypair;
+use solana_transaction_error::TransactionError;
+
+/// Proves unicast exclusivity, persistent fanout, terminal fanout, and slow-receiver removal.
+#[tokio::test]
+async fn subscribers_send_semantics() {
+ let unicast = Arc::new(Unicast::new(1, Subscription::Transactions));
+ let mut unicast_rx = unicast.subscribe().unwrap();
+ assert!(unicast.subscribe().is_err());
+ unicast.send(1).await;
+ let sender = unicast.clone();
+ let send = tokio::spawn(async move { sender.send(2).await });
+ tokio::task::yield_now().await;
+ assert!(!send.is_finished(), "async unicast send waits for capacity");
+ assert_eq!(unicast_rx.recv().await, Some(1));
+ send.await.unwrap();
+ assert_eq!(unicast_rx.recv().await, Some(2));
+
+ unicast.send(3).await;
+ let sender = unicast.clone();
+ let send = std::thread::spawn(move || sender.blocking_send(4));
+ assert_eq!(unicast_rx.recv().await, Some(3));
+ send.join().unwrap();
+ assert_eq!(unicast_rx.recv().await, Some(4));
+ drop(unicast_rx);
+ assert!(unicast.subscribe().is_err());
+
+ let multicast = Multicast::new(1, Subscription::Accounts);
+ multicast.send(&1, &9);
+ let mut first = multicast.subscribe(1).await;
+ let mut second = multicast.subscribe(1).await;
+ multicast.send(&1, &10);
+ assert_eq!(first.recv().await, Some(10));
+ assert_eq!(second.recv().await, Some(10));
+ multicast.send(&1, &11);
+ multicast.send(&1, &12);
+ assert_eq!(first.recv().await, Some(11));
+ assert_eq!(first.recv().await, None);
+ assert_eq!(second.recv().await, Some(11));
+ assert_eq!(second.recv().await, None);
+
+ let oneshot = MulticastOneshot::default();
+ let first = oneshot.subscribe(1).await;
+ let closed = oneshot.subscribe(1).await;
+ drop(closed);
+ oneshot.send_last(&1, &18);
+ assert!(matches!(
+ first.try_recv(),
+ Err(oneshot::TryRecvError::Empty)
+ ));
+ let second = oneshot.subscribe(1).await;
+ oneshot.send_last(&1, &19);
+ assert_eq!(second.await.unwrap(), 19);
+ oneshot.send(&1, &20);
+ assert_eq!(first.await.unwrap(), 20);
+ let third = oneshot.subscribe(1).await;
+ oneshot.send(&1, &21);
+ assert_eq!(third.await.unwrap(), 21);
+}
+
+// Appending reserves the signature while rejection wakes only its own latest
+// waiter. Invalid blockhash is retained as a terminal cached status.
+#[tokio::test]
+async fn append_dedup_and_status_sentinel() {
+ let keeper = TestKeeper::new().await;
+ let (signature, txn) = signed_tx();
+ let slot = keeper.blocks().current_slot();
+ let original = keeper.transactions().subscribe_signature(signature).await;
+
+ // First append writes to the ledger; the duplicate is dropped.
+ assert!(
+ keeper.transactions().append(&txn).await.unwrap(),
+ "first append is accepted"
+ );
+ let duplicate = keeper.transactions().subscribe_signature(signature).await;
+ assert!(
+ !keeper.transactions().append(&txn).await.unwrap(),
+ "duplicate is deduplicated"
+ );
+ let status = duplicate.await.unwrap();
+ assert_eq!(status.result, Err(TransactionError::AlreadyProcessed));
+ assert_eq!(status.slot, slot);
+ assert!(matches!(
+ original.try_recv(),
+ Err(oneshot::TryRecvError::Empty)
+ ));
+
+ // The sentinel makes status() return None from the cache.
+ assert!(keeper.transactions().status(signature).await.unwrap().is_none());
+
+ let payer = Keypair::new();
+ let (signature, view) = signed_view(
+ &payer,
+ [Instruction::new_with_bytes(V42_ID, &[], vec![])],
+ Hash::new_from_array([1; 32]),
+ );
+ let txn =
+ ResolvedTransaction::try_new(view, Some(Default::default()), &Default::default()).unwrap();
+ let rejected = keeper.transactions().subscribe_signature(signature).await;
+ assert!(!keeper.transactions().append(&txn).await.unwrap());
+ let status = rejected.await.unwrap();
+ assert_eq!(status.result, Err(TransactionError::BlockhashNotFound));
+ assert_eq!(status.slot, slot);
+ let cached = keeper.transactions().status(signature).await.unwrap().unwrap();
+ assert_eq!(cached.result, Err(TransactionError::BlockhashNotFound));
+ assert_eq!(cached.slot, slot);
+
+ keeper.close().await;
+}
diff --git a/keeper/src/util.rs b/keeper/src/util.rs
new file mode 100644
index 00000000..d73906ec
--- /dev/null
+++ b/keeper/src/util.rs
@@ -0,0 +1,107 @@
+//! Internal helpers shared by keeper accessors.
+
+use std::sync::Arc;
+
+use ledger::{
+ request::{ReadRequest, RequestPayload, TransactionStatus},
+ schema::{
+ Balances, CompiledInstruction, Cpis, Event, Execution, ExecutionDetails, ExecutionHeader,
+ Instruction, ReturnData,
+ },
+};
+use solana_message::inner_instruction::{InnerInstruction, InnerInstructionsList};
+use solana_signature::Signature;
+use solana_svm::{
+ transaction_balances::BalanceCollector, transaction_execution_result::ExecutedTransaction,
+ transaction_processing_result::TransactionProcessingResultExtensions,
+};
+
+use crate::{FullTransaction, Keeper, Result};
+
+/// Ledger event, status-cache entry, and logs derived from one execution result.
+pub(crate) struct ExecutionCommit {
+ /// First transaction signature used for status notifications.
+ pub(crate) signature: Signature,
+ /// Status stored in the signature cache and sent to subscribers.
+ pub(crate) status: TransactionStatus,
+ /// Ledger event that pairs execution metadata with the appended transaction.
+ pub(crate) event: Event,
+ /// Execution logs shared with account log subscribers.
+ pub(crate) logs: Arc>,
+}
+
+/// Sends a typed read request to the ledger reader and waits for its response.
+pub(crate) async fn request(keeper: &Keeper, params: P, request: F) -> Result
+where
+ F: FnOnce(RequestPayload) -> ReadRequest,
+{
+ let (payload, handle) = RequestPayload::
::new(params);
+ let request = request(payload);
+ keeper.ledger.reader.send_async(request).await?;
+ Ok(handle.recv().await?)
+}
+
+/// Builds the ledger and cache records for a completed transaction execution.
+pub(crate) fn execution_commit(txn: &mut FullTransaction) -> ExecutionCommit {
+ let slot = txn.execution.slot;
+ let result = txn.execution.result.flattened_result();
+ let signature = txn.transaction.signatures()[0];
+
+ let header = ExecutionHeader {
+ signature,
+ slot,
+ result: result.clone(),
+ };
+ let status = TransactionStatus { result, slot };
+ let details = txn
+ .execution
+ .result
+ .as_ref()
+ .ok()
+ .map(|execution| execution_details(execution, txn.execution.balances.take()));
+ let logs = details.as_ref().map(|d| Arc::clone(&d.logs)).unwrap_or_default();
+ let event = Event::Execution(Execution { header, details });
+
+ ExecutionCommit { signature, status, event, logs }
+}
+
+/// Projects SVM execution data into the retained ledger format.
+fn execution_details(
+ execution: &ExecutedTransaction,
+ balances: Option,
+) -> ExecutionDetails {
+ let (pre, post) = balances.map(|bc| bc.into_vecs()).unwrap_or_default();
+ let details = &execution.execution_details;
+
+ ExecutionDetails {
+ fee: execution.loaded_transaction.fee_details.total_fee(),
+ balances: Balances { pre, post },
+ logs: details.log_messages.clone().unwrap_or_default(),
+ compute_units: details.executed_units,
+ return_data: details.return_data.as_ref().map(|rd| ReturnData {
+ program: rd.program_id.to_bytes(),
+ data: rd.data.clone().into(),
+ }),
+ cpi: details.inner_instructions.as_ref().map(cpis),
+ }
+}
+
+/// Projects grouped SVM inner instructions into ledger CPI records.
+fn cpis(groups: &InnerInstructionsList) -> Vec {
+ groups
+ .iter()
+ .map(|group| Cpis(group.iter().map(instruction).collect()))
+ .collect()
+}
+
+/// Projects one SVM inner instruction into the ledger instruction format.
+fn instruction(ix: &InnerInstruction) -> Instruction {
+ Instruction {
+ stack_height: ix.stack_height,
+ compiled: CompiledInstruction {
+ program_index: ix.instruction.program_id_index,
+ accounts: ix.instruction.accounts.clone(),
+ data: ix.instruction.data.clone(),
+ },
+ }
+}
diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml
new file mode 100644
index 00000000..f1077cdd
--- /dev/null
+++ b/ledger/Cargo.toml
@@ -0,0 +1,48 @@
+[package]
+name = "magicblock-ledger"
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+version.workspace = true
+
+[lib]
+name = "ledger"
+
+[features]
+testkit = []
+
+[dependencies]
+nucleus = { workspace = true, features = ["heed", "ledger", "service"] }
+
+bitcode = { workspace = true }
+bytemuck = { workspace = true, features = ["derive", "extern_crate_std"] }
+derive_more = { workspace = true, features = ["deref", "from"] }
+flume = { workspace = true }
+heed = { workspace = true }
+memmap2 = { workspace = true }
+num_cpus = { workspace = true }
+oneshot = { workspace = true, features = ["std"] }
+parking_lot = { workspace = true }
+rustix = { workspace = true, features = ["fs"] }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["sync"] }
+tracing = { workspace = true }
+wincode = { workspace = true }
+zstd = { workspace = true }
+
+agave-transaction-view = { workspace = true, features = ["agave-unstable-api"] }
+solana-pubkey = { workspace = true }
+solana-signature = { workspace = true, features = ["wincode"] }
+solana-transaction-error = { workspace = true, features = ["wincode"] }
+
+[dev-dependencies]
+ledger = { workspace = true, features = ["testkit"] }
+nucleus = { workspace = true, features = ["testkit"] }
+tokio = { workspace = true, features = ["macros", "rt"] }
+
+[lints]
+workspace = true
diff --git a/ledger/README.md b/ledger/README.md
new file mode 100644
index 00000000..a1ac0c60
--- /dev/null
+++ b/ledger/README.md
@@ -0,0 +1,56 @@
+# `magicblock-ledger`
+
+The ledger stores transaction bytes, execution metadata, block boundaries,
+superblock seals, and volatile-state reset markers. History is partitioned into
+self-contained superblock directories so retention removes a complete sealed
+segment without compacting the active store.
+
+```text
+ledger.meta
+superblock-000000001/
+ superblock.meta
+ blockstore.db
+ executions.db
+ index/
+```
+
+`blockstore.db` is a wincode stream. Blockstore decoding permits allocations up
+to the ledger's 25-bit encoded entry-size bound (33,554,431 bytes); larger
+entries are rejected. Execution headers and zstd-compressed bitcode details are
+stored separately in `executions.db`.
+
+## Append and read paths
+
+One appender owns ordered writes. Transaction bytes are appended first and kept
+pending until their execution metadata arrives; only then are transaction and
+account indexes inserted. Every durable sync flushes data and indexes, publishes
+durable file cursors, transfers the accumulated transaction count, and flushes
+ledger metadata. When the sync carries a block boundary, it also publishes that
+block's slot and increments the block count. A seal finalizes the active files
+and rotates to the next superblock. The successor metadata retains the sealed
+snapshot's checksum and cumulative transaction count so it remains
+self-describing after retention removes the preceding blockstore.
+
+Reader requests run on a worker pool. Each worker owns its decode buffers and
+reads only through published cursors. The optional `testkit` feature reduces
+LMDB map sizes and uses one reader worker without changing the on-disk format.
+
+During coordinated shutdown, one queue marker per reader closes the pool after
+earlier requests. A final appender sync flushes every preceding event, reports
+its durability result, and then closes the appender. Intermediate replication
+syncs flush without closing either service, and retained sender clones do not
+delay terminal shutdown.
+
+Replay is superblock-based. The consumer supplies the last sealed superblock
+already reflected in its state, and the reader streams each retained successor
+through the active head in full.
+
+## Retention
+
+At a block boundary, the appender checks used bytes on the ledger filesystem.
+When the configured limit is reached, `Ledger::truncate` removes the oldest
+sealed superblock; the active head is never removed.
+
+The size check assumes the ledger directory is on a dedicated filesystem.
+Unrelated files on that filesystem contribute to the used-byte total and can
+trigger earlier retention.
diff --git a/ledger/src/appender.rs b/ledger/src/appender.rs
new file mode 100644
index 00000000..aa64c13c
--- /dev/null
+++ b/ledger/src/appender.rs
@@ -0,0 +1,389 @@
+//! Ledger append service and writable superblock storage.
+
+use std::{
+ collections::HashMap,
+ path::Path,
+ sync::{Arc, atomic::Ordering::*},
+};
+
+use agave_transaction_view::transaction_view::TransactionView;
+use bitcode::Buffer;
+use flume::Receiver;
+use heed::{Env, RwTxn};
+use nucleus::{
+ Slot,
+ heed::{DatabaseIndex, OptRwTxn, write_txn},
+ ledger::BlockstorePosition,
+ shutdown::{ShutdownHandle, ShutdownReason},
+};
+use solana_signature::Signature;
+use tokio::sync::broadcast::Sender;
+use tracing::{info, warn};
+use wincode::Error;
+use zstd::bulk::Compressor;
+
+use crate::{
+ Ledger, Superblock,
+ error::{LedgerError, Result},
+ index::{Index, Span, TxSpan},
+ metrics::{self, Operation},
+ schema::{
+ Block, BlockstoreEntry, Event, Execution, ExecutionDetails, MAX_EXECUTION_DETAILS_SIZE,
+ SuperblockSeal, TransactionEntry, blockstore,
+ },
+ storage::{AppendFile, MetaMap, SuperblockMeta},
+};
+
+/// Blockstore stream file name inside a superblock.
+pub(crate) const BLOCKSTORE_DB: &str = "blockstore.db";
+/// Execution details file name inside a superblock.
+pub(crate) const EXECUTIONS_DB: &str = "executions.db";
+/// Superblock metadata file name.
+pub(crate) const SUPERBLOCK_META: &str = "superblock.meta";
+
+/// Background service that appends ledger events into the active superblock.
+pub(crate) struct LedgerAppender {
+ /// Shared top-level ledger state.
+ ledger: Arc,
+ /// Writable files for the active superblock.
+ writer: SuperblockWriter,
+ /// Transactions waiting for their matching execution details.
+ pending: HashMap,
+ /// Active superblock index.
+ index: Arc