Expand description
Disk-backed PoolStateStore for the node runtime.
Implements PoolStateStore against a set of redb database files under
<data_dir>, one file per durable write family:
lanes.redb— seller lane frontier (lane_state_v1). Each record also carries its lane’s owner-signed capability material (owner_sig), so the redeemer’s registration payload lands in the same durable write as the frontier it backs. The file also holds the deployment stamp (deployment_v1).settle.redb— the seller and buyer pending-settle sets.checkpoint.redb— the settlement watcher’s scan checkpoints.buyer.redb— the buyer pool state and its owner index.
Each file has its own redb write-transaction slot, so a commit in one
family never waits on an unrelated commit in another: the periodic lane
flush, a settlement checkpoint, and a buyer top-up all proceed on
independent writer slots. Families are
never written in one transaction — redb forbids a transaction spanning two
Database handles — so a crash between two family commits can leave them at
different watermarks, which every family already tolerates (each records and
recovers on its own terms).
The seller-side state is bound to one PaymentPool deployment
(Deployment). Pool ids repeat across deployments, so open() drops the
lane rows, pending settles and watcher checkpoints a different deployment
wrote before it reads any of them. The buyer pool table carries its own
per-row deployment tag (the same chain id and contract address) and is not
touched.
The lane table is buffered in memory: open()
hydrates the working set from disk, record/forget mutate that working
set only, and an explicit flush() call writes every dirty lane and
applies every tombstone in one fsynced commit (redb’s default
redb::Durability::Immediate, set explicitly here so a future redb
default change doesn’t silently weaken the durability guarantee). A crash
between two flushes loses the unflushed lane advances; the caller decides
the flush cadence.
The owner-signed capability material (owner_sig) is a field of the lane
record, not a side table. It is written by the same record/flush path as
the frontier, in the same fsynced transaction, so it can never be durable-out-
of-step with the frontier it backs: whenever a lane’s frontier is on disk, its
owner_sig is too, and a forget deletes both in one row removal. A crash
between two flushes loses the lane’s unflushed advances AND its owner_sig
together — never one without the other — and both recover the same way: the
frontier is superseded by the signer’s next, higher voucher, and the material
is re-supplied because the client re-sends its capability on the next request
and intake re-writes it onto the lane (#1906).
See decdn_incentive::store for the trait contract and
ADR 003 §Off-chain voucher state persistence for the protocol rule
this implements: a node MUST advance (last_amount, last_bytes_delivered)
for the lane before it continues delivery, and mirror the advance to disk on
a background timer. Without the on-disk mirror a restart re-opens the lane at
amount zero and a client can replay a previously-accepted voucher for a
second byte delivery (issue #527).
The record persists the latest voucher’s signature (so the seller
redemption path can submit it to the on-chain PaymentPool.redeem after a
restart without forfeiting the claim), the capability’s spending cap, its
expiry (so the node stops serving once the grant lapses), the owner’s
signature over the capability (owner_sig, the ownerSig the redeemer
submits to register the signer on-chain), and the lane’s paid cumulative
(paid_cumulative, the on-chain redeemed watermark the redeemer subtracts
from owed — persisting it stops a restart from re-submitting already-redeemed
lanes, #2052). All of these are plain fields of the one StoredLaneState
record — there is a single on-disk format and no older shape to decode.
The seller table above is defined here. The buyer pool table (#744) is
not: its record codec and every one of its operations live in
decdn_incentive::buyer_pool_table, shared with the client’s
RedbBuyerPoolStore (#1246). This file contributes only the buyer table’s
wiring — the node holds it in its own buyer.redb, so a buyer top-up
commits on a writer slot separate from the seller lane flush.
Structs§
- Buyer
Pending Settle Store Handle PendingSettleStoreover the buyer’sbuyer_pending_settle_v1table, isolated from the seller’spending_settle_v1table so the two settle sweeps never finalize each other’s closes (#988). Wraps the same sharedPersistentPoolStateStore(one redb file, one handle); hand this to the buyer service asArc<dyn PendingSettleStore>.- Buyer
Pool Store Handle BuyerPoolStoreadapter over the sharedPersistentPoolStateStore.- Deployment
- The
PaymentPooldeployment the store’s seller-side state is bound to. ThePaymentPooldeployment a row belongs to: the chain and contract address that make up the voucher EIP-712 domain. - Persistent
Pool State Store redb-backed persistent implementation ofPoolStateStore.