Expand description
On-chain PaymentPool seller-settlement service (ADR 003).
The node is the provider (payee) on its voucher lanes. This service closes
the on-chain half of the paid-delivery loop that the off-chain voucher layer
(decdn_incentive) and the cdn/client/v1 handler leave open. A pool is
owner-owned and owner-closed: the node NEVER closes, disputes, or settles a
pool. Its only settlement primitive is redemption — redeem for a single
lane, redeemMany for a batch — plus register-once of each signer’s
capability on that signer’s first redemption.
- Paid-watermark watcher. The paid side of every lane is driven by
consuming
PoolRedeemedevents filtered on this node’s ownprovideraddress (ADR 003 § Tracking owed vs. paid): each event sets the lane’s paid cumulative tonewPaidCumulativeand mirrors it onto the durableLaneState::paid_cumulativerow.PoolToppedUpre-drives a pool’s lanes (a dry pool may have leftowed > paid), andPoolReclaimedforgets the pool’s lanes. The watcher reconciles like every other chain watcher — enumeratePoolRedeemedfrom a pinned block, then tail live, resyncing on a missed range — so paid is rebuilt from the event log, never guessed. That forward rebuild starts at the poller’s persisted cursor, so bootstrap first rehydrates the paid cache from the durable lane rows: a restart carries forward every redemption behind the cursor instead of re-submitting those lanes for silent on-chain no-ops (#2052). The same scan also folds the serve path’s pool solvency/funder projection (crate::pool_view::PoolProjection):PoolOpened/PoolToppedUpset a pool’s{owner, deposit},PoolRedeemedfor every provider draws down its pool-widetotalRedeemed, andPoolReclaimeddrops it — so a serve request reads{owner, remaining}in-memory instead of through agetPooleth_call. - Redemption (per-chunk floor + on-shutdown). On a redeem hint (a
LaneKey) emitted by the voucher-accept path, the node reads the lane’s owed voucher and its cached paid watermark and plans the lane for redemption — with no chain read. A signer this node has not yet registered attaches aCapabilityRegfrom its heldowner_sig; on-chain registration is idempotent, and the first landed redemption persistsregistered_untilso later passes attach nothing. A low-frequency self-tick sweeps every persisted lane, packs the planned lanes into chunks, and submits a chunk — oneredeemMany— only once the aggregate unredeemed value across that chunk’s lanes clears a configurable floor, so a dropped hint never strands a lane whose chunk has cleared the floor. The same sweep also runs at each serve cutoff: one second after the voucher and preimage paths start to refuse a lane whose capability expiry is near. The lane’s claim is final there, and the sweep has about one redeem interval to start the redemption and the landing slack to land it before the contract stops paying the capability. On a restart, a lane whose cutoff passed while the node was down is swept at once. A value that stays below the floor is not redeemed by the cutoff sweep; a later sweep before the landing slack can still pack it with other lanes. The node flushes the lane store durable after planning a chunk and before submitting it, so post-crash on-diskowed ≥ submitted. - Redeem before reclaim. A pool is owner-owned and owner-closed; the node
never closes, disputes, or settles. An owner’s
closePoolonly starts the grace window (ADR 003 § Owner reclaims before a node redeems). The node runs no force-redeem on close: the self-tick sweep runs far inside the 48h grace floor (redeem_interval_secsis capped well below it at config load), so it redeems every lane worth the gas before the owner canreclaim. A lane below the per-chunk floor is left for the owner to reclaim — spending more gas than it recovers is not worth it — and a node offline for the whole grace window forfeits its unredeemed vouchers, a node-ops failure, not a protocol gap. The sink does foldPoolCloseInitiatedinto the projection (marking the poolClosingwith its deadline) so the redeemer’s solvency gate drops a drained lane, or one whose deadline falls within the landing slack, instead of submitting aredeemManythat revertsPoolClosed.
Buyer-side openPool/topUp/reclaim (node→node cache-miss pulls) is out of
scope here. The paid-watermark watcher is a Route the runtime registers on
the shared multiplexed poller (which owns the loop, cursor, and shutdown); the
service itself owns only the redeemer JoinHandle, aborted on shutdown or
drop.
Structs§
- Debounced
Checkpoint Store - Debouncing decorator over a
KeyedCheckpointStore(#784, keyed in #1092). Buffersrecord_checkpointin memory perCheckpointKeyand forwards a durable write for a key only when its buffered block is at leastflush_blocksahead of that key’s last persisted value or at leastflush_intervalhas elapsed since that key’s last durable write — cutting the per-block fsync amplification a watcher’s live tail would otherwise pay on a backlog drain. - Paid
Watermarks - In-memory index of every lane’s paid-cumulative watermark, keyed by
LaneKey, read by the redeemer to computeunredeemed = owed − paid(ADR 003 § Tracking owed vs. paid). Written by thePoolRedeemedwatcher, which mirrors every update onto the durableLaneState::paid_cumulativerow in the same breath. The durable field is the source of truth; this cache is rehydrated from it at bootstrap (PoolSettlementService::bootstrap) so a restart carries forward redemptions that landed before the log-poller’s persisted cursor, instead of re-submitting those lanes for silent on-chain no-ops (#2052). - Pool
Settlement Service - Seller-side
PaymentPoolsettlement service. Generic over the alloyProvider(a wallet-filled provider is required for theredeem/redeemManywrite path). Cheap to construct; owns its background tasks. - Resolving
Pool View - A
PoolViewthat confirms a pool on-chain before a serve is admitted.
Constants§
- REDEEM_
HINT_ CAPACITY - Capacity of the redeem-hint channel. Hints are advisory (a missed hint only delays a redemption until the next voucher or self-tick sweep), so a bounded channel that drops on overflow is acceptable — sized for a burst of concurrent lanes without backpressuring the voucher-accept path.