Skip to content

feat: implement superblock based ledger crate - #15

Open
bmuddha wants to merge 3 commits into
accountsdbfrom
ledger
Open

feat: implement superblock based ledger crate#15
bmuddha wants to merge 3 commits into
accountsdbfrom
ledger

Conversation

@bmuddha

@bmuddha bmuddha commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

What changed

Implemented the ledger crate with superblock storage, append and reader workers, block/execution schemas, LMDB indexes, request handling, and retention hooks.

Why

The engine needs a durable record of transaction execution, block metadata, and superblock boundaries that can be read independently from account storage.

Closes #7.

Impact

  • Introduces Ledger, LedgerHandle, retained Superblock storage, and reader/appender service wiring.
  • Stores blockstore data, execution details, superblock metadata, and read indexes under per-superblock directories.
  • Adds request/response types for block, range, transaction, and signature reads.

Reviewer notes

Superblocks own their files and indexes so retention can remove whole directories. head is the active superblock, while sealed superblocks remain readable until truncation.

Follow-up

keeper coordinates this ledger with accountsdb upstack.

@bmuddha

bmuddha commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3dbdee1b-220a-45db-b9c7-29f798c4a116

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid input: expected boolean, received string at "reviews.auto_review.enabled"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Added the magicblock-ledger crate to the workspace. The crate implements persistent superblock storage, transaction and execution indexing, background appends, reads, replay, retention, metrics, and end-to-end tests.

Changes

Ledger storage and access

Layer / File(s) Summary
Ledger contracts and package wiring
Cargo.toml, ledger/Cargo.toml, ledger/src/error.rs, ledger/src/schema.rs, ledger/src/request.rs, ledger/README.md
Added the workspace package, ledger wire formats, read request and response types, cancellation and timeout handling, shared errors, and storage lifecycle documentation.
Storage primitives and LMDB indexes
ledger/src/storage.rs, ledger/src/index.rs, ledger/src/tests/index.rs
Added append-file and memory-mapped metadata storage. Added LMDB indexes for blocks, transactions, and account execution spans. Added codec and ordering tests.
Ledger lifecycle and append pipeline
ledger/src/lib.rs, ledger/src/appender.rs, ledger/src/metrics.rs
Added ledger initialization, superblock management, background event processing, synchronization, sealing, rotation, retention, and Prometheus metrics.
Read, replay, and response processing
ledger/src/reader.rs, ledger/src/tests/integration.rs, ledger/src/tests/mod.rs
Added indexed transaction, block, range, account-history, and replay reads. Added decoding, pagination, cancellation, corruption handling, restart, retention, and cross-superblock integration coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LedgerHandle
  participant LedgerAppender
  participant Superblock
  participant Index
  participant LedgerReader

  LedgerHandle->>LedgerAppender: submit transaction and execution events
  LedgerAppender->>Superblock: append blockstore and execution records
  LedgerAppender->>Index: commit transaction, block, and account spans
  LedgerAppender->>Superblock: sync cursors and seal or rotate
  LedgerHandle->>LedgerReader: submit read or replay request
  LedgerReader->>Index: resolve indexed spans
  LedgerReader->>Superblock: read and decode stored records
  LedgerReader-->>LedgerHandle: return response or replay entries
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: implementing a superblock-based ledger crate.
Description check ✅ Passed The description accurately summarizes the ledger implementation and its purpose for transaction and execution tracking.
Linked Issues check ✅ Passed The changes implement the ledger crate and align transaction and execution data with block and slot context as required by issue #7.
Out of Scope Changes check ✅ Passed The workspace updates, documentation, metrics, storage, indexes, workers, and tests directly support the ledger objectives in issue #7.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ledger

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (6)
ledger/src/reader.rs (2)

268-281: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stop the replay loop when the receiver disconnects.

The break at line 276 exits only the inner while loop. The outer for loop then opens the next superblock, decodes an entry, and calls blocking_send again, which fails immediately. Return instead, so a disconnected receiver ends the request at once. replay also never checks request.cancelled(), unlike the other handlers in this file.

♻️ Proposed fix for replay cancellation
         for superblock in self.ledger.iter_after(*superblock) {
             let limit = superblock.meta.cursors.blockstore.load(Acquire);
             let mut reader = BufReader::new((&superblock.blockstore).take(limit));
             while !reader.fill_buf()?.is_empty() {
+                if request.cancelled() {
+                    return Ok(());
+                }
                 let entry = blockstore::decode(&mut reader).map_err(Into::<Error>::into)?;
                 if tx.blocking_send(entry).is_err() {
-                    break;
+                    return Ok(());
                 }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/src/reader.rs` around lines 268 - 281, Update replay to return
immediately when tx.blocking_send(entry) fails, rather than only breaking the
inner while loop. Also check request.cancelled() while processing superblocks
and entries, returning early when cancellation is requested, consistent with the
other handlers.

350-356: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid the full copy of the block byte range.

Line 352 clones self.buffers.blockstore. The range covers every transaction between the two block boundaries, so the copy scales with the block size. The test at ledger/src/tests/integration.rs lines 240-274 already stores a payload above 10 MiB.

Take the buffer out with mem::take, decode from the owned value, and put it back at the end. That keeps the mutable borrow of self available inside the loop without a copy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/src/reader.rs` around lines 350 - 356, Update the block-reading flow
around the cursor loop to replace the clone of self.buffers.blockstore with
mem::take, decoding from the owned buffer while retaining mutable access to self
during iteration. Restore the buffer to self.buffers.blockstore after decoding
completes, including the cancellation path.
ledger/src/lib.rs (1)

130-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add SAFETY comments to the MetaMap::new call sites.

SuperblockWriter::new in ledger/src/appender.rs (lines 268-271) documents the same unsafe contract. Line 134 here and line 256 in Superblock::open call MetaMap::new without any SAFETY note. Add the equivalent justification so every unsafe call site states why the contract holds.

♻️ Proposed documentation for the unsafe call sites
         let meta = directory.join(LEDGER_META);
+        // SAFETY: `LedgerMeta` is a fixed-layout header whose shared fields are
+        // atomics, satisfying `MetaMap::new`'s contract. `directory` is created
+        // above by `create_dir_all`.
         let meta = unsafe { MetaMap::<LedgerMeta>::new(&meta) }?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/src/lib.rs` around lines 130 - 148, Add SAFETY comments immediately
before the unsafe MetaMap::new calls in Ledger::new and Superblock::open,
matching the justification already documented in SuperblockWriter::new and
explaining why the mapped metadata remains valid for the call.

Source: Path instructions

ledger/src/appender.rs (1)

346-348: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the SAFETY justification.

The comment states that the LMDB write transaction is a part of LedgerAppender. It is not. run holds that transaction in a local variable (line 92), and no field of the struct stores it. Name the field that actually blocks the automatic Send implementation, for example the zstd Compressor inside SuperblockWriter, and state why moving it to one thread is sound. The same applies to LedgerReader in ledger/src/reader.rs (lines 469-471), whose comment is accurate about decoder state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/src/appender.rs` around lines 346 - 348, Update the SAFETY comment
above LedgerAppender’s unsafe impl Send to identify the non-Send zstd Compressor
held by SuperblockWriter as the field preventing automatic Send, and state that
moving the appender to its single background thread is sound. Leave
LedgerReader’s existing decoder-state justification unchanged.

Source: Path instructions

ledger/src/tests/integration.rs (1)

57-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the Sync and Reset events.

append closes the sender to end the appender, so no test sends Event::Sync { response, is_final }. The README describes that path at lines 35-39: a final sync flushes preceding events, reports its durability result, and then closes the appender. No test sends Event::Reset either, although write_reset writes a marker and the replay shape check at lines 415-421 has a Reset(_) arm.

Add one test that drives a Sync with is_final set to both values, and one that appends a Reset and asserts the marker appears in replay.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/src/tests/integration.rs` around lines 57 - 72, Add integration tests
covering both branches of Event::Sync by sending a sync event with is_final set
to false and true, asserting the response durability result and final-sync
shutdown behavior. Add a separate test that uses append to write Event::Reset,
then reopens or replays the ledger and verifies the reset marker is present,
reusing the existing replay shape checks and test helpers.

Source: Path instructions

ledger/src/metrics.rs (1)

34-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the source of the ledger_superblocks gauge.

The gauge reads ledger.meta.head(), which is the active superblock id. LedgerMeta also has a distinct superblocks field that holds the retained count (ledger/src/lib.rs lines 186 and ledger/src/appender.rs line 119). The help text, "Current total ledger superblocks allocated from genesis", does not say which of the two values the gauge reports. Either report meta.superblocks for the retained count, or state in the help text that the value is the newest superblock id.

Also applies to: 127-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/src/metrics.rs` around lines 34 - 37, Clarify the ledger_superblocks
metric by either changing its source to LedgerMeta.superblocks for the retained
count or updating the SUPERBLOCKS help text to explicitly identify the value as
the newest active superblock id from meta.head(). Keep the metric name and
surrounding registration unchanged.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ledger/README.md`:
- Around line 31-33: The README description of the optional testkit feature is
incomplete. Update the testkit sentence to mention both effects: reducing the
index LMDB map_size and setting the ledger reader worker count to 1, while
preserving the statement that the on-disk format is unchanged.

In `@ledger/src/appender.rs`:
- Around line 157-169: Bound the lifetime of entries inserted by
write_transaction in self.pending by defining an explicit retention rule. If
transactions may only pair within the current block, clear unpaired entries in
write_block; otherwise enforce age or maximum-count eviction and log each
eviction, while preserving write_execution’s missing-counterpart behavior.
- Around line 142-145: Update the Event::Superblock handling in run to commit
the current open index write transaction before calling write_superblock and
rotate. Ensure the transaction is finalized before the blockstore is sealed,
while preserving the existing superblock-writing and rotation flow.

In `@ledger/src/index.rs`:
- Around line 27-28: Update the INDEX_DBS constant or its documentation so they
agree: Index::new creates three named databases—transactions, slots, and
accounts—so set INDEX_DBS to 3 unless an extra reserved slot is intentional, in
which case document that reservation explicitly.
- Around line 78-83: Enforce the 39-bit offset limit in SuperblockWriter before
deriving spans from AppendFile::cursor. Update write_blockstore and
write_execution to detect a cursor that would exceed the limit, rotate/seal the
current file, and continue with a fresh cursor before calling Span::new; retain
Span::new’s assertions as a defensive check.

In `@ledger/src/lib.rs`:
- Around line 169-191: Guard the entire retention pass in Ledger::truncate with
a dedicated mutex acquired before reading the oldest superblock and held through
purge, removal, metadata updates, and flush. Ensure all callers, including
write_block and public handle access, use this same serialization path so
concurrent truncations cannot process the same superblock or decrement the
counter twice.

In `@ledger/src/reader.rs`:
- Around line 344-350: Guard the previous-boundary lookup in the block-reading
flow by replacing the unguarded slot - 1 calculation with checked subtraction.
Treat a None result from checked_sub as no previous boundary and use start = 0,
while preserving the existing index lookup and offset-plus-size behavior when a
preceding slot exists.
- Around line 240-266: Update blocks to clamp the requested Range<Slot> against
the retained range in self.ledger.meta.range before counting, allocating, or
iterating. Enforce a fixed maximum range length by rejecting or truncating
oversized requests, so range.clone().count(), Vec::with_capacity, and the slot
lookup loop cannot process unbounded caller input.
- Around line 298-309: Update LedgerReader initialization and the execution
method to ensure buffers.details has capacity matching the writer’s
MAX_ENTRY_SIZE/Span::MAX_SIZE contract rather than the current 4 * MB
reservation. Use the frame or upper_bound to derive the required decompressed
capacity, or reserve the established maximum bound before decompress_to_buffer
in execution, while preserving existing decoding behavior.

In `@ledger/src/schema.rs`:
- Around line 138-150: Correct the documentation comments on Cpis and
Instruction: describe Cpis as inner instructions executed from an outer
instruction, and update stack_height to state that it contains the invocation
stack height without calling it optional. Keep the struct fields and types
unchanged.
- Line 25: Rename the public type alias OwnedBlockestoreEntry to
OwnedBlockstoreEntry in schema.rs, and update every reference in request.rs and
the blockstore module to use the corrected name consistently.

In `@ledger/src/storage.rs`:
- Around line 282-289: Add #[repr(C)] to the BlockRange struct so its AtomicU64
fields have a stable C-compatible order when embedded in LedgerMeta and
SuperblockMeta and persisted through MetaMap.
- Around line 174-193: Update the existing-file branch in unsafe fn new to
validate file.metadata().len() before MmapOptions::len(size). If the file is
shorter than size, extend it with set_len(size as u64) before mapping; preserve
the existing mapping and return flow for files already large enough.
- Around line 251-257: Update superblocks() so the calculation of start uses
saturating subtraction for the outer head-minus-count operation, preventing
underflow when separate atomic reads observe a torn state. Preserve the existing
retained-range semantics, including the active head.

---

Nitpick comments:
In `@ledger/src/appender.rs`:
- Around line 346-348: Update the SAFETY comment above LedgerAppender’s unsafe
impl Send to identify the non-Send zstd Compressor held by SuperblockWriter as
the field preventing automatic Send, and state that moving the appender to its
single background thread is sound. Leave LedgerReader’s existing decoder-state
justification unchanged.

In `@ledger/src/lib.rs`:
- Around line 130-148: Add SAFETY comments immediately before the unsafe
MetaMap::new calls in Ledger::new and Superblock::open, matching the
justification already documented in SuperblockWriter::new and explaining why the
mapped metadata remains valid for the call.

In `@ledger/src/metrics.rs`:
- Around line 34-37: Clarify the ledger_superblocks metric by either changing
its source to LedgerMeta.superblocks for the retained count or updating the
SUPERBLOCKS help text to explicitly identify the value as the newest active
superblock id from meta.head(). Keep the metric name and surrounding
registration unchanged.

In `@ledger/src/reader.rs`:
- Around line 268-281: Update replay to return immediately when
tx.blocking_send(entry) fails, rather than only breaking the inner while loop.
Also check request.cancelled() while processing superblocks and entries,
returning early when cancellation is requested, consistent with the other
handlers.
- Around line 350-356: Update the block-reading flow around the cursor loop to
replace the clone of self.buffers.blockstore with mem::take, decoding from the
owned buffer while retaining mutable access to self during iteration. Restore
the buffer to self.buffers.blockstore after decoding completes, including the
cancellation path.

In `@ledger/src/tests/integration.rs`:
- Around line 57-72: Add integration tests covering both branches of Event::Sync
by sending a sync event with is_final set to false and true, asserting the
response durability result and final-sync shutdown behavior. Add a separate test
that uses append to write Event::Reset, then reopens or replays the ledger and
verifies the reset marker is present, reusing the existing replay shape checks
and test helpers.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 55f4d57b-33f7-49f5-9ada-42e48a367f8c

📥 Commits

Reviewing files that changed from the base of the PR and between d4aa6ee and be039f1.

📒 Files selected for processing (15)
  • Cargo.toml
  • ledger/Cargo.toml
  • ledger/README.md
  • ledger/src/appender.rs
  • ledger/src/error.rs
  • ledger/src/index.rs
  • ledger/src/lib.rs
  • ledger/src/metrics.rs
  • ledger/src/reader.rs
  • ledger/src/request.rs
  • ledger/src/schema.rs
  • ledger/src/storage.rs
  • ledger/src/tests/index.rs
  • ledger/src/tests/integration.rs
  • ledger/src/tests/mod.rs

Comment thread ledger/README.md Outdated
Comment thread ledger/src/appender.rs
Comment thread ledger/src/appender.rs
Comment thread ledger/src/index.rs Outdated
Comment thread ledger/src/index.rs
Comment thread ledger/src/schema.rs Outdated
Comment thread ledger/src/schema.rs
Comment thread ledger/src/storage.rs
Comment thread ledger/src/storage.rs
Comment thread ledger/src/storage.rs
@bmuddha

bmuddha commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement ledger crate for transaction and execution tracking

1 participant