Skip to main content

Module payment_settlement

Module payment_settlement 

Source
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 PoolRedeemed events filtered on this node’s own provider address (ADR 003 § Tracking owed vs. paid): each event sets the lane’s paid cumulative to newPaidCumulative and mirrors it onto the durable LaneState::paid_cumulative row. PoolToppedUp re-drives a pool’s lanes (a dry pool may have left owed > paid), and PoolReclaimed forgets the pool’s lanes. The watcher reconciles like every other chain watcher — enumerate PoolRedeemed from 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/PoolToppedUp set a pool’s {owner, deposit}, PoolRedeemed for every provider draws down its pool-wide totalRedeemed, and PoolReclaimed drops it — so a serve request reads {owner, remaining} in-memory instead of through a getPool eth_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 a CapabilityReg from its held owner_sig; on-chain registration is idempotent, and the first landed redemption persists registered_until so later passes attach nothing. A low-frequency self-tick sweeps every persisted lane, packs the planned lanes into chunks, and submits a chunk — one redeemMany — 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-disk owed ≥ submitted.
  • Redeem before reclaim. A pool is owner-owned and owner-closed; the node never closes, disputes, or settles. An owner’s closePool only 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_secs is capped well below it at config load), so it redeems every lane worth the gas before the owner can reclaim. 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 fold PoolCloseInitiated into the projection (marking the pool Closing with its deadline) so the redeemer’s solvency gate drops a drained lane, or one whose deadline falls within the landing slack, instead of submitting a redeemMany that reverts PoolClosed.

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§

DebouncedCheckpointStore
Debouncing decorator over a KeyedCheckpointStore (#784, keyed in #1092). Buffers record_checkpoint in memory per CheckpointKey and forwards a durable write for a key only when its buffered block is at least flush_blocks ahead of that key’s last persisted value or at least flush_interval has 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.
PaidWatermarks
In-memory index of every lane’s paid-cumulative watermark, keyed by LaneKey, read by the redeemer to compute unredeemed = owed − paid (ADR 003 § Tracking owed vs. paid). Written by the PoolRedeemed watcher, which mirrors every update onto the durable LaneState::paid_cumulative row 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).
PoolSettlementService
Seller-side PaymentPool settlement service. Generic over the alloy Provider (a wallet-filled provider is required for the redeem / redeemMany write path). Cheap to construct; owns its background tasks.
ResolvingPoolView
A PoolView that 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.