Skip to main content

Module channel_store

Module channel_store 

Source
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§

BuyerPendingSettleStoreHandle
PendingSettleStore over the buyer’s buyer_pending_settle_v1 table, isolated from the seller’s pending_settle_v1 table so the two settle sweeps never finalize each other’s closes (#988). Wraps the same shared PersistentPoolStateStore (one redb file, one handle); hand this to the buyer service as Arc<dyn PendingSettleStore>.
BuyerPoolStoreHandle
BuyerPoolStore adapter over the shared PersistentPoolStateStore.
Deployment
The PaymentPool deployment the store’s seller-side state is bound to. The PaymentPool deployment a row belongs to: the chain and contract address that make up the voucher EIP-712 domain.
PersistentPoolStateStore
redb-backed persistent implementation of PoolStateStore.