pub enum VoucherRejectReason {
Show 16 variants
BadSignature,
WrongSigner,
WrongPool,
WrongProvider,
AmountRegression,
BytesRegression,
SpendingCapExhausted,
CapabilityExpired,
PoolExhausted,
SignerCapExhausted,
BadPreimage,
ChainIndexZero,
UnanchoredPreimage,
ChunkPriceMismatch,
Underpaid,
UnderFold,
}Expand description
Why a Voucher was rejected (ADR 005 §VoucherRejected semantics).
The first seven variants, Self::BadPreimage, and Self::UnderFold
mirror decdn_incentive::PoolError ∪ VoucherError one-to-one; the
handler-side conversion voucher_reject_reason matches those exhaustively
so a new PoolError variant fails to compile until this enum is extended
(ADR 005 §Mirror obligation). The other variants are emitted directly by the
cdn/client/v1 handler: Self::CapabilityExpired fires when the signer’s
capability has passed its expiry; Self::PoolExhausted fires when the
pool’s remaining deposit can no longer fund further credit;
Self::SignerCapExhausted fires when the signer’s shared cap headroom can
no longer cover a serve floor; the three remaining hash-chain reasons
(Self::ChainIndexZero, Self::UnanchoredPreimage,
Self::ChunkPriceMismatch) are raised where the handler holds the
per-stream chain anchor or quoted rate a validation enum cannot see; and
Self::Underpaid fires when the span a voucher adds over the lane’s
accepted watermark pays below the quoted rate_per_mb.
Variant order is the postcard wire discriminant, so a reorder is a
wire-breaking change.
Variants§
BadSignature
Signature malformed (corrupted bytes, non-canonical s, invalid
recovery id). VoucherError::InvalidSignature.
WrongSigner
Signature well-formed but recovers to the wrong signer.
VoucherError::WrongSigner.
WrongPool
voucher.pool_id mismatch (also: unknown pool). PoolError::WrongPool.
WrongProvider
The voucher names a provider node other than the one receiving it —
a capability voucher scoped to one node redeemed against another.
PoolError::WrongProvider.
AmountRegression
Cumulative amount regressed. PoolError::AmountRegression.
BytesRegression
Cumulative bytes delivered regressed. PoolError::BytesRegression.
SpendingCapExhausted
The signer’s remaining spending cap is exhausted — the voucher amount
exceeds what the capability has left to spend (cap − spent).
Recovery: the pool owner raises this signer’s cap or delegates a new
capability. PoolError::CapExceeded.
CapabilityExpired
The signer’s capability has passed its expiry, or lies within the node’s
expiry margin of it. The node’s serve-side check compares its LOCAL wall
clock (operator-settable) plus that margin against expiry, refuses the
voucher or preimage, and stops serving the lane. The margin is one redeem
interval plus a landing slack, so the lane’s claim is final when the
margin starts, and the node’s redeem sweep one second later has time to
land before the expiry (ADR 003 §Revocation). A lane below the redemption
floor can still wait past it. On-chain settlement separately gates redemption on
block.timestamp. Already-earned vouchers stay redeemable until expiry at
settlement. Recovery: the owner mints a fresh capability with a new expiry —
a watermark resync or top-up does not help. Emitted directly by the
cdn/client/v1 handler (the node holds the clock), not via the PoolError
bridge.
PoolExhausted
The pool’s on-chain remaining deposit, minus the refundable floor M and
the pool’s already-committed concurrent floor credit, can no longer fund
further credit for this stream (ADR 003 §Pool solvency). A pool-wide
condition, not this signer’s. Recovery: the pool owner tops up the
deposit. Emitted mid-stream by the serve loop after the client has proved
capability ownership; the open-time equivalent is StreamError::Unfunded,
spoken only to a proven requester. Not watermark-gated.
SignerCapExhausted
The signer’s shared on-chain cap − spent headroom, tracked across every
provider, can no longer cover a serve floor — the signer has drained its
cap at OTHER nodes since this stream was admitted, so further vouchers
here would redeem min(desired, cap − spent) ≈ 0 and the node would eat the
delivered bytes (ADR 003 §Pool solvency). A per-SIGNER condition, distinct
from Self::PoolExhausted: the pool’s remaining can be healthy on other
signers’ budgets while this one signer’s cap is spent. Recovery: the pool
owner raises this signer’s cap or delegates a fresh capability. Emitted
mid-stream after the client has proved capability ownership, so naming the
condition is post-auth and leaks nothing an open-time refusal must hide (the
admit-time equivalent is StreamError::Unfunded, spoken only to a proven
requester). Not watermark-gated.
BadPreimage
A released ChunkPreimage does not hash to the stream’s deepest
verified preimage in index − verified steps (ADR 003 §Concurrent
Streams, Rule 2). A payer bug — a wrong seed, a wrong chain, or a
mis-derived index. On-chain the same mismatch reverts BadPreimage,
which is caller error and not transient state, so this is terminal and
carries no watermark bundle: a preimage has no signature of its own,
and a payment watermark cannot repair a hash-chain mismatch.
ChainIndexZero
A ChunkPreimage arrived with index == 0, which never travels the
wire — index 0 names chain_root and is the settlement case at
redemption. A payer bug; do not retry.
Zero is the whole of it. There is no over-large index to report: the wire
index is a u8 and MAX_CHAIN_LENGTH is 255, so an index past the end
of the chain cannot be encoded. A chain that has run out of indices is
not this reason either — the payer rolls to a fresh chain_root first.
UnanchoredPreimage
A ChunkPreimage arrived on a stream that holds no chain anchor, so
the node cannot name the chain the reveal belongs to (ADR 003
§Concurrent Streams, Rule 1). Per-stream and therefore decidable, which
a lane-wide reading would not be. Not fatal: the payer sends the
current epoch’s chain_root voucher on this stream and resends the
preimage. The resend is free — an at-or-below-watermark voucher is
already-satisfied rather than rejected.
ChunkPriceMismatch
The voucher’s chunk_price is not the node’s quoted rate_per_mb on a
metering voucher, or is non-zero on a sealed one (chain_root == 0).
chunk_price is signed by the payer and a preimage carries no price
of its own, so a voucher signed at the governance floor against a node
quoting ten times that would meter every later chunk at a tenth of the
quote, with no per-tick moment revealing it. The node therefore checks
the price it is being paid before it meters against it (ADR 003 §Chunk
Cadence). A payer bug: re-read rate_per_mb from the StreamResponse
and re-sign; do not retry with the same price.
Underpaid
The voucher advances the lane, but the span it adds over the node’s
last-accepted watermark pays below the quoted rate_per_mb (ADR 003
§Voucher withholding). The node stops delivery and credits nothing.
An honest payer reaches this only when its local watermark has diverged
from the node’s: its cumulative carries bytes the node never accepted a
voucher for, so every later span looks short. The reject therefore carries
the node’s WatermarkBundle. Recovery: the payer verifies the bundle
against its own key, rebases its lane to it once, and retries (ADR 005
§VoucherRejected semantics).
UnderFold
The voucher is at or above the signed anchor and names a chain_root
other than the live one, which retires the lane’s live chain, but its
amount or bytes_delivered does not fold the frontier that chain
proved: the signed anchor plus verified_index chunks (ADR 003 §Chain
length and rollover). At exactly the anchor, only a voucher that opens a
chain qualifies; a sealed one there is Self::AmountRegression.
Accepting it would discard chunks the node holds preimages for, so the
node adopts nothing.
A payer reaches this when it resumes from a watermark that trails the
node’s verified frontier. The reject carries the node’s
WatermarkBundle. Recovery: the payer verifies the bundle against its
own key, reseeds to it with the chain frontier folded in, and retries.
Concurrent streams on the lane get the same bundle; one that finds its
ledger already at the bundle retries without a reseed. A voucher that
already covers the claim’s amount but falls short on
bytes_delivered cannot heal this way — the bundle does not advance the
payer’s amount — so the payer surfaces it as terminal.
PoolError::UnderFold.
Implementations§
Source§impl VoucherRejectReason
impl VoucherRejectReason
Sourcepub const fn is_watermark_gated(self) -> bool
pub const fn is_watermark_gated(self) -> bool
Whether a StreamError::VoucherRejected carrying this reason is
eligible for a WatermarkBundle (issue #1481 §5): exactly five
reasons. The three regression/exhaustion reasons are ones a wallet-less
client cannot distinguish from chain, since its local watermark is the
only thing that could be wrong. Self::Underpaid and
Self::UnderFold are the other two: a payer reaches them only through
that same watermark divergence, and the bundle states the watermark it
has to rebase or fold to.
Every other reason (CapabilityExpired, PoolExhausted,
SignerCapExhausted, the chain reasons) and the signer/pool/provider
mismatches are never eligible — a bundle would not help there, since the
fix is not “resync the watermark”.
Single source of truth for the gate: the node checks this before
attaching a bundle (crates/node/src/handlers/client/voucher.rs) and
the client checks it again before trusting one enough to self-heal
(crates/client/src/lib.rs) — both call this rather than each
keeping their own copy of the match.