Skip to content

feat(kv-store): expose read-only lmdb transactions - #25280

Open
spalladino wants to merge 5 commits into
merge-train/spartan-v5from
spl/kv-store-read-only-tx
Open

feat(kv-store): expose read-only lmdb transactions#25280
spalladino wants to merge 5 commits into
merge-train/spartan-v5from
spl/kv-store-read-only-tx

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Context

AztecLMDBStoreV2 only exposes transactionAsync, which opens a WRITE transaction serialized through the writer queue. The TS ReadTransaction gives no snapshot consistency: every GET message opens and aborts a throwaway LMDB read tx in C++, so two consecutive reads can straddle a commit. LMDB supports many concurrent readers that never block the writer, but that capability was not exposed to TS.

Approach

  • Add START_READ_TX/CLOSE_READ_TX messages to the node addon protocol, plus an optional txId on GET and START_CURSOR. The C++ wrapper keeps a registry of live read transactions (mirroring the existing cursor registry), each guarded by a mutex since an LMDB read tx must not be used concurrently across the libuv pool threads; cursors opened against a shared tx serialize on that same mutex.
  • lmdblib::LMDBStore::get gains an overload that reads through a caller-provided read transaction instead of opening its own.
  • On the TS side, store.readOnlyTransaction(callback) opens one C++ read tx and propagates it via AsyncLocalStorage, so plain container reads (map.getAsync, iteration) inside the callback hit the snapshot automatically. Nested calls reuse the ambient read or write tx, matching transactionAsync semantics.
  • Each open read tx takes a slot from the existing maxReaders - 1 semaphore; cursors created inside a snapshot skip slot acquisition since they share the tx's reader slot.
  • Tests prove the snapshot is real: a concurrent write commits (and completes) while the read tx is open, yet stays invisible inside it; forcing the old per-GET behavior makes those tests fail.

API changes

AztecAsyncKVStore gains readOnlyTransaction<T>(callback: () => Promise<T>): Promise<T>: runs the callback against a consistent read-only snapshot without blocking concurrent writers. The sqlite-opfs, indexeddb, and v1 lmdb implementations delegate to their existing transaction machinery (consistent view, no snapshot concurrency).

Adds START_READ_TX / CLOSE_READ_TX messages and an optional txId on GET and
START_CURSOR, so the JS side can hold one LMDB read transaction open across
many reads and iterations and see a single consistent snapshot.

LMDBStore gets a get() overload that reads against a caller-supplied read
transaction. LMDBStoreWrapper keeps a registry of read transactions mirroring
the cursor registry; because a read transaction must never be used by two
threads at once, each one carries a mutex that every get and every cursor
bound to it locks. Cursors record that mutex so advance_cursor serializes
against sibling cursors and gets on the same snapshot.
`store.readOnlyTransaction(cb)` opens a real LMDB read transaction and keeps it
open for the whole callback, so every read inside sees one snapshot. Readers do
not go through the writer queue, so a write can commit while the callback runs
without the callback observing it.

The transaction is propagated through an AsyncLocalStorage, so container reads
(`map.getAsync`, `entriesAsync`, ...) inside the callback hit the snapshot
without being handed the transaction explicitly. Nested calls reuse the
enclosing transaction, matching how `transactionAsync` handles recursion.

Each open snapshot consumes an LMDB reader slot, so it acquires from the same
semaphore as cursors; cursors bound to a snapshot skip acquisition since they
reuse its slot, which the store now tracks per cursor id to avoid
over-releasing on close.

`readOnlyTransaction` is added to `AztecAsyncKVStore`; the other backends have
no snapshot of their own that outlives an operation, so they delegate to their
regular transaction, which gives the callback a consistent view.
@github-actions github-actions Bot added the port-to-next Forward-port this merged PR into next label Aug 21, 2026
The aztec-up bridge_and_claim smoke test intermittently fails with
BBApiException: Failed to verify the generated proof! during ClientIVC
private-kernel proving. This is unrelated to the kv-store/lmdb changes
in this branch and matches the existing pattern of proving-related
flakes already catalogued for this and adjacent aztec-up tests.
The failure was a corrupted one-off bb build artifact replayed from cache, not
a recurring flake; suppressing this signature would mask real prover
regressions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

port-to-next Forward-port this merged PR into next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant