Skip to main content

decdn_incentive/
lane.rs

1//! Per-lane voucher state tracking.
2//!
3//! A **lane** is a `(pool_id, signer, provider)` triple: one capability signer
4//! spending one pool against one provider node. As a node receives vouchers on
5//! a lane, it keeps the latest one and rejects any that would lower the
6//! cumulative `amount` or `bytes_delivered` — the on-chain `PaymentPool.redeem`
7//! invariants from ADR 003 §Redemption and Close apply equally off-chain (a node
8//! that retains a stale voucher just under-claims at redemption).
9//!
10//! In-memory state alone is insufficient: without persistence a node restart
11//! resets `last_amount` to zero and a client can resubmit a previously-accepted
12//! voucher (issue #527). [`LaneState::apply_voucher`] therefore requires a
13//! [`PoolStateStore`] and records the post-acceptance state to it before
14//! advancing in-memory fields or returning `Ok`; the store makes that record
15//! durable on its own flush cadence — see
16//! [ADR 003 §Off-chain voucher state persistence](https://github.com/decdn/decdn/blob/main/adr/003-payments.md)
17//! and [`crate::store`].
18
19use alloy::dyn_abi::Eip712Domain;
20use alloy::primitives::{Address, B256, U256};
21
22use crate::store::{PoolStateStore, StoreError};
23use crate::voucher::{SignedVoucher, VoucherError};
24
25/// An opaque 32-byte pool identifier — the on-chain `PaymentPool` deposit id
26/// (`keccak256(owner, ownerPoolNonce)`). The buyer modules key their
27/// per-owner pool state by this type; the seller-side lane keys off
28/// [`LaneKey`] instead.
29pub type PoolId = B256;
30
31/// Identifies one voucher lane: a `(pool_id, signer, provider)` triple. This is
32/// the seller-side persistence key — one accepted-voucher watermark per lane.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct LaneKey {
35    /// The pool the vouchers draw from (on-chain `PaymentPool` deposit id).
36    pub pool_id: B256,
37    /// The capability signer authorizing vouchers on this lane.
38    pub signer: Address,
39    /// The provider node the vouchers pay.
40    pub provider: Address,
41}
42
43/// Mutable per-lane state held by the delivering node.
44///
45/// Tracks the latest accepted voucher (`last_*` fields) so subsequent
46/// [`LaneState::apply_voucher`] calls can enforce monotonicity. `cap` is the
47/// capability's spending cap — vouchers exceeding it are invalid because
48/// `PaymentPool.redeem` would itself revert (ADR 003 §Capability delegation).
49///
50/// **Field invariant (#527):** the `last_*` fields MUST only be advanced through
51/// this type's validated advance methods ([`LaneState::apply_voucher`], or a
52/// pure successor from [`LaneState::stage_voucher`] /
53/// [`LaneState::advance_presigned`] that the caller records and swaps in) or
54/// hydrated from a [`PoolStateStore`] (the trusted on-disk path). Direct field
55/// assignment from outside this crate would bypass the voucher-replay guard from
56/// ADR 003 §Off-chain voucher state persistence, so the three replay-critical
57/// fields are **private** and reachable only through the getters
58/// ([`Self::last_amount`] et al.) and those trusted writers; the cross-crate
59/// hydration path (`decdn-node` reading the pool store) goes through
60/// [`Self::hydrate`] rather than a struct literal (#751).
61///
62/// The remaining fields stay `pub` deliberately: `pool_id`/`signer`/`provider`
63/// are immutable identity set once; `cap`, `expiry`, and `registered_until`
64/// are set from the registered capability by trusted node-side writers. They
65/// are not replay-critical (they don't gate the amount/bytes monotonicity the
66/// #527 guard protects), so they don't need the private treatment the
67/// `last_*` fields do.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct LaneState {
70    /// The pool these vouchers draw from (on-chain `PaymentPool` deposit id).
71    pub pool_id: B256,
72    /// The capability signer authorizing vouchers on this lane. Vouchers whose
73    /// signature recovers to a different address are rejected.
74    pub signer: Address,
75    /// The provider node these vouchers pay. A voucher naming a different
76    /// provider is rejected — a capability voucher is scoped to one node.
77    pub provider: Address,
78    /// The cumulative spending cap the node holds (token base units): the
79    /// presented capability's cap, clamped to the signer's on-chain registration
80    /// once the node reads one ([`Self::clamp_to_registration`]). Vouchers MUST
81    /// NOT exceed this value.
82    pub cap: U256,
83    /// The capability expiry the node holds (Unix seconds), clamped like
84    /// [`Self::cap`]. The node handler holds the clock and refuses vouchers at or
85    /// past this; `0` means "unknown / not tracked".
86    pub expiry: u64,
87    /// The observed on-chain capability expiry for this lane's signer
88    /// (`authorized[pool_id][signer].expiry`), in Unix seconds; `0` means
89    /// "unknown / not yet registered". The seller redeemer reads it to skip a
90    /// `CapabilityReg` when the registration is already known and still live.
91    /// Not replay-critical (it gates no amount/bytes monotonicity), so it is
92    /// `pub` like `cap`/`expiry` rather than a private `last_*` field.
93    pub registered_until: u64,
94    /// The owner's EIP-712 signature over the presented `Capability` (`r‖s‖v`,
95    /// exactly 65 bytes), which the seller redeemer submits as the `ownerSig` of
96    /// a `PaymentPool.redeemMany` `CapabilityReg` to register the signer on its
97    /// first on-chain redemption (ADR 003 §Capability delegation). `None` until
98    /// the seller intake path verifies a grant against the pool owner and sets
99    /// it. Kept ON the lane record — not a side table — so it is written in the
100    /// same durable transaction as the voucher frontier it backs and can never
101    /// be durable-out-of-step with it. Set by trusted node-side writers (the
102    /// intake path and the on-disk decoder) like `cap`/`expiry`/`registered_until`,
103    /// not replay-critical, so it is `pub` rather than a private `last_*` field.
104    pub owner_sig: Option<[u8; 65]>,
105    /// The lane's paid cumulative: the on-chain `newPaidCumulative` of the most
106    /// recent `PoolRedeemed` this node observed for it (`U256::ZERO` until the
107    /// first redemption lands). The seller redeemer subtracts it from
108    /// [`Self::owed`] to get the unredeemed value it still has to cash, so a lane
109    /// already redeemed to its owed amount drops out of the plan instead of being
110    /// re-submitted for a silent on-chain no-op (#2052). Monotone on-chain, so it
111    /// only ever advances.
112    ///
113    /// Kept ON the lane record — not a side table or a volatile cache — so it is
114    /// written in the same durable transaction as the voucher frontier it settles
115    /// against and survives a restart: without it a rebooted node forgets every
116    /// redemption behind its log-poller cursor and re-batches those lanes forever.
117    /// Set by the trusted node-side watcher (`PoolRedeemed`) and the on-disk
118    /// decoder, like `owner_sig`/`registered_until`; not replay-critical (it gates
119    /// redemption planning, not voucher acceptance), so it is `pub` rather than a
120    /// private `last_*` field.
121    pub paid_cumulative: U256,
122    /// Cumulative amount of the most-recently-accepted voucher (token base
123    /// units). `U256::ZERO` until the first voucher is applied. Private
124    /// (#527/#751) — read via [`Self::last_amount`].
125    last_amount: U256,
126    /// Cumulative bytes delivered as of the most-recently-accepted voucher.
127    /// Private (#527/#751) — read via [`Self::last_bytes_delivered`].
128    last_bytes_delivered: U256,
129    /// Signature (`r‖s‖v`, exactly 65 bytes) on the most-recently-accepted
130    /// voucher — the `signature` the seller path submits to the on-chain
131    /// `PaymentPool.redeem`. `None` until the first voucher is applied; `Some`
132    /// is always exactly 65 bytes. Same `r‖s‖v` encoding as the
133    /// [`crate::client_bridge`] wire form. Private — read via
134    /// [`Self::last_signature`].
135    last_signature: Option<[u8; 65]>,
136    /// The chain the lane is currently metering against — the epoch opened by
137    /// the most-recently-accepted voucher. [`LaneChain::NONE`] until the first
138    /// voucher, and after any sealed voucher. Private for the same reason as
139    /// the `last_*` fields: `verified_index` is a money-bearing watermark, so
140    /// advancing it outside the validated path would forge payment. Read via
141    /// [`Self::chain`].
142    chain: LaneChain,
143}
144
145/// One metering epoch's hash-chain state on a lane (ADR 003 §Hash-chain
146/// metering (`PayWord`)).
147///
148/// `chain_root` is the head the payer signed into the epoch's voucher, and
149/// `(verified_index, tip)` is the deepest preimage the node has verified under
150/// it — the frontier that turns into money at redemption. `tip` is the preimage
151/// **bytes**, not just the depth: only the payer can produce a value at a given
152/// depth, so a node that kept the index alone would hold an unprovable claim
153/// after a restart, in exactly the abandonment case the chain exists to cover.
154///
155/// A sealed epoch (`chain_root == B256::ZERO`) meters nothing: `chunk_price` is
156/// zero, `verified_index` stays `0`, and `tip` is zero. Nothing hashes to zero,
157/// so no index above `0` can ever be verified against it.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub struct LaneChain {
160    /// Head of this epoch's chain; zero seals the epoch at its anchor amount.
161    pub chain_root: B256,
162    /// What one chunk adds over the anchor, in token base units. Zero on a
163    /// sealed epoch.
164    pub chunk_price: U256,
165    /// Deepest chain index verified under `chain_root`; `0` until the first
166    /// preimage lands.
167    pub verified_index: u8,
168    /// The preimage bytes at `verified_index`, or `chain_root` itself at index
169    /// `0` — which is what makes the walk uniform with no branch.
170    pub tip: B256,
171}
172
173impl LaneChain {
174    /// The empty chain: no root, no price, nothing verified. What a lane holds
175    /// before its first voucher, and what a sealed voucher installs.
176    pub const NONE: Self = Self {
177        chain_root: B256::ZERO,
178        chunk_price: U256::ZERO,
179        verified_index: 0,
180        tip: B256::ZERO,
181    };
182
183    /// A fresh epoch at index 0: the root is its own tip, so a claim settling at
184    /// exactly the anchor amount submits a value the payer already holds and
185    /// walks nothing.
186    #[must_use]
187    pub const fn opened(chain_root: B256, chunk_price: U256) -> Self {
188        Self {
189            chain_root,
190            chunk_price,
191            verified_index: 0,
192            tip: chain_root,
193        }
194    }
195
196    /// What the verified frontier adds over its anchor:
197    /// `verified_index × chunk_price`.
198    #[must_use]
199    pub fn accrued(&self) -> U256 {
200        self.chunk_price * U256::from(self.verified_index)
201    }
202
203    /// What the verified frontier adds over its anchor on the byte axis:
204    /// `verified_index × CHUNK_BYTES`.
205    #[must_use]
206    pub fn accrued_bytes(&self) -> U256 {
207        U256::from(crate::chain::CHUNK_BYTES) * U256::from(self.verified_index)
208    }
209}
210
211/// A complete, self-contained claim a node can redeem: a signed anchor plus the
212/// chain frontier that extends it.
213///
214/// Redemption resolves it as `claimed = amount + verified_index × chunk_price`
215/// over `claimed_bytes = bytes_delivered + verified_index × CHUNK_BYTES`.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct RedeemClaim {
218    /// The anchor voucher's cumulative amount.
219    pub amount: U256,
220    /// The anchor voucher's cumulative bytes delivered.
221    pub bytes_delivered: U256,
222    /// The anchor voucher's `r‖s‖v` signature.
223    pub signature: [u8; 65],
224    /// The chain extending that anchor.
225    pub chain: LaneChain,
226}
227
228impl RedeemClaim {
229    /// The claim's full value: anchor plus verified frontier. This is the
230    /// number the redeem planner compares and the contract recomputes.
231    #[must_use]
232    pub fn value(&self) -> U256 {
233        self.amount + self.chain.accrued()
234    }
235
236    /// The claim's full byte count: anchor plus verified frontier.
237    #[must_use]
238    pub fn bytes_value(&self) -> U256 {
239        self.bytes_delivered + self.chain.accrued_bytes()
240    }
241
242    /// The packed `chainMeter` word this claim submits on-chain — its
243    /// `chunk_price` and `verified_index` in one word (ADR 003 §Voucher
244    /// signatures are compact).
245    ///
246    /// # Errors
247    ///
248    /// [`crate::chain::ChainMeterError`] when `chunk_price` exceeds the
249    /// `uint64` the word gives it, which the contract could not settle anyway.
250    pub fn chain_meter(&self) -> Result<U256, crate::chain::ChainMeterError> {
251        crate::chain::pack_chain_meter(self.chain.chunk_price, self.chain.verified_index)
252    }
253}
254
255impl LaneState {
256    /// Reconstruct lane state from a trusted persistent store (the only
257    /// cross-crate path allowed to set the private replay-critical `last_*`
258    /// fields, #527/#751). `decdn-node`'s pool-store decoder calls this instead
259    /// of a struct literal so the [field invariant](Self) stays
260    /// compiler-enforced. `last_signature` is `None` for a lane with no accepted
261    /// voucher yet and otherwise the exact 65-byte `r‖s‖v` signature.
262    ///
263    /// **Trust boundary.** This is the one constructor that bypasses
264    /// `apply_voucher`'s validation, and several arguments share a type
265    /// (`cap`/`last_amount`/`last_bytes_delivered` are all `U256`;
266    /// `signer`/`provider` are both `Address`), so a transposition compiles. Do
267    /// not add callers without round-trip coverage that pins a signer distinct
268    /// from the provider.
269    ///
270    /// Hydration seeds `registered_until` to `0` (unknown), `owner_sig` to
271    /// `None`, and `paid_cumulative` to `U256::ZERO` regardless of caller-supplied
272    /// `expiry`; the trusted on-disk decoder (`StoredLaneState::into_state`)
273    /// assigns the persisted values on the returned `Self` after construction, the
274    /// seller intake path sets `owner_sig` on the lane it registers, and the
275    /// `PoolRedeemed` watcher advances `paid_cumulative`.
276    #[must_use]
277    #[allow(clippy::too_many_arguments)]
278    pub const fn hydrate(
279        pool_id: B256,
280        signer: Address,
281        provider: Address,
282        cap: U256,
283        expiry: u64,
284        last_amount: U256,
285        last_bytes_delivered: U256,
286        last_signature: Option<[u8; 65]>,
287        chain: LaneChain,
288    ) -> Self {
289        Self {
290            pool_id,
291            signer,
292            provider,
293            cap,
294            expiry,
295            registered_until: 0,
296            owner_sig: None,
297            paid_cumulative: U256::ZERO,
298            last_amount,
299            last_bytes_delivered,
300            last_signature,
301            chain,
302        }
303    }
304
305    /// Hold the lower of this lane's terms and its signer's on-chain registration
306    /// (ADR 003 §Capability delegation). On-chain registration is write-once per
307    /// `(pool, signer)`, so a voucher past the registered `cap` or `expiry`
308    /// redeems to nothing whatever capability the client presented. `cap` and
309    /// `expiry` are the registered `authorized[pool_id][signer]` terms; the lane
310    /// keeps `min(held, registered)` for each and records the registered expiry
311    /// as `registered_until`. Returns whether any field changed.
312    ///
313    /// The chain reads a registered `expiry` of `0` as already expired, while a
314    /// lane `expiry` of `0` means "not tracked". The clamp therefore holds such a
315    /// registration as expiry `1`, a moment that is always past.
316    ///
317    /// A clamped lane holds terms its `owner_sig` does not cover. That is sound:
318    /// the clamp runs only for a registered signer, and on-chain registration
319    /// returns before it checks the signature of a registered signer.
320    pub fn clamp_to_registration(&mut self, cap: u64, expiry: u64) -> bool {
321        let before = (self.cap, self.expiry, self.registered_until);
322        self.cap = self.cap.min(U256::from(cap));
323        let held_expiry = expiry.max(1);
324        self.expiry = if self.expiry == 0 {
325            held_expiry
326        } else {
327            self.expiry.min(held_expiry)
328        };
329        self.registered_until = self.registered_until.max(expiry);
330        before != (self.cap, self.expiry, self.registered_until)
331    }
332
333    /// This lane's persistence key.
334    #[must_use]
335    pub const fn key(&self) -> LaneKey {
336        LaneKey {
337            pool_id: self.pool_id,
338            signer: self.signer,
339            provider: self.provider,
340        }
341    }
342
343    /// Cumulative amount of the most-recently-accepted voucher (`U256::ZERO`
344    /// until the first voucher). See the [field invariant](Self).
345    #[must_use]
346    pub const fn last_amount(&self) -> U256 {
347        self.last_amount
348    }
349
350    /// Cumulative bytes delivered as of the most-recently-accepted voucher.
351    /// See the [field invariant](Self).
352    #[must_use]
353    pub const fn last_bytes_delivered(&self) -> U256 {
354        self.last_bytes_delivered
355    }
356
357    /// The most-recently-accepted voucher's 65-byte `r‖s‖v` signature, or
358    /// `None` if no voucher has been applied. See the [field invariant](Self).
359    #[must_use]
360    pub const fn last_signature(&self) -> Option<&[u8; 65]> {
361        self.last_signature.as_ref()
362    }
363
364    /// The epoch this lane is currently metering against. See the
365    /// [field invariant](Self).
366    #[must_use]
367    pub const fn chain(&self) -> LaneChain {
368        self.chain
369    }
370
371    /// The live epoch expressed as a redeemable claim: the last accepted
372    /// voucher's anchor plus the frontier verified under its root. `None` until
373    /// a voucher has been accepted (there is no signature to submit).
374    #[must_use]
375    pub fn live_claim(&self) -> Option<RedeemClaim> {
376        self.last_signature.map(|signature| RedeemClaim {
377            amount: self.last_amount,
378            bytes_delivered: self.last_bytes_delivered,
379            signature,
380            chain: self.chain,
381        })
382    }
383
384    /// The byte frontier the lane's strongest claim covers — its signed
385    /// cumulative extended by the chunks the chain has proved. Zero on a lane
386    /// holding no claim.
387    ///
388    /// This, not `last_bytes_delivered`, is what the node has actually been paid
389    /// for on the byte axis: a released preimage advances the frontier without
390    /// a signature, so counting only the signed half would leave the node's own
391    /// credit accounting a whole chain behind the money it is owed.
392    #[must_use]
393    pub fn owed_bytes(&self) -> U256 {
394        self.live_claim().map_or(U256::ZERO, |c| c.bytes_value())
395    }
396
397    /// What this lane is **owed**: the value of its strongest claim, or zero if
398    /// it holds none.
399    ///
400    /// Counting only the signed cumulative would understate a lane by up to one
401    /// whole chain, which is what `M` and the redemption floor are sized
402    /// against (ADR 003 §Tracking owed vs. paid).
403    #[must_use]
404    pub fn owed(&self) -> U256 {
405        self.live_claim().map_or(U256::ZERO, |c| c.value())
406    }
407
408    /// Adopt a chain from a voucher that did **not** advance the money — the
409    /// already-satisfied case (ADR 003 §Voucher ordering).
410    ///
411    /// This exists because the first root voucher on a fresh lane sits exactly
412    /// at the watermark: its `amount` is the cumulative the lane already holds,
413    /// because nothing has been metered yet. The money path treats that as
414    /// already-satisfied and advances nothing, which is right — but the voucher
415    /// still has to install the chain, or the reveals that follow would name a
416    /// root the lane never learned and fold nothing.
417    ///
418    /// Two narrow things happen here, and nothing else:
419    ///
420    /// - A lane metering **nothing** adopts the incoming root and price.
421    /// - A lane holding **no signature yet** adopts this voucher as its anchor,
422    ///   with the root and price that voucher carries.
423    ///
424    /// That last one is what makes the chain redeemable at all. A claim is a
425    /// signed anchor extended by a frontier, and the contract rebuilds the
426    /// EIP-712 digest from BOTH halves — so the chain fields and the signature
427    /// have to come from the same voucher. Installing a root beside an older
428    /// voucher's signature yields a claim that recovers the wrong signer and
429    /// reverts `InvalidVoucherSignature` at redemption.
430    ///
431    /// That pairing is also why a re-asserting voucher CANNOT restate the price.
432    /// The price is half the claim — redemption resolves `amount + index ×
433    /// chunk_price` and rebuilds the EIP-712 digest from the price it is handed —
434    /// so moving it under a signature that covered the old one yields a claim
435    /// that recovers the wrong signer, and `redeemMany` reverts
436    /// `InvalidVoucherSignature` for every lane in the batch, not just this one.
437    /// An already-satisfied voucher pays for nothing, so it has no authority to
438    /// reprice the frontier the lane already proved: the lane keeps metering at
439    /// the price its own anchor was signed at. A payer that wants a new price
440    /// rolls, and a rollover advances the money and goes through
441    /// [`Self::advance_presigned`], where the price and the signature that covers
442    /// it are adopted together.
443    ///
444    /// That pairing is what bounds this method. It adopts a **new** root only
445    /// when the incoming voucher re-asserts the lane's own watermark exactly, so
446    /// taking it as the anchor regresses nothing, and only when the live chain
447    /// has proved **nothing**: a chain at index 0 is worth exactly its anchor,
448    /// which this voucher's own signature already covers, so retiring it strands
449    /// no value. A chain with reveals under it is worth more than its anchor, and
450    /// a voucher that did not pay for that difference has no authority to end it.
451    ///
452    /// A genuine rollover always advances the money — its `amount` folds the
453    /// retired chain's frontier in — so it goes through
454    /// [`Self::advance_presigned`] instead, where the fold and the retirement are
455    /// decided together.
456    ///
457    /// Returns `None` when nothing changed, so the caller can skip a needless
458    /// store write.
459    #[must_use]
460    pub fn adopt_chain(&self, signed: &SignedVoucher) -> Option<Self> {
461        let chain_root = signed.voucher.chain_root;
462        if chain_root.is_zero() {
463            return None;
464        }
465        let mut next = self.clone();
466        if self.chain.chain_root == chain_root {
467            // Same epoch re-asserted. The only thing that can change is the
468            // anchor, and only on a lane that holds no signature yet — the first
469            // such voucher becomes the anchor, bringing its own price with it.
470            // A lane that already has a signature takes nothing from this
471            // voucher: adopting its price alone would pair a price with a
472            // signature that never covered it (see above).
473            if self.last_signature.is_some() {
474                return None;
475            }
476            next.chain.chunk_price = signed.voucher.chunk_price;
477            next.last_amount = signed.voucher.amount;
478            next.last_bytes_delivered = signed.voucher.bytes_delivered;
479            next.last_signature = Some(signed.signature.as_bytes());
480            return Some(next);
481        }
482
483        // A NEW root. This is not a corner case: a lane whose previous transfer
484        // closed on a rollover sits at index 0 under the retired root, and the
485        // next transfer's opening voucher re-asserts the same cumulative while
486        // naming a fresh one. Refusing would leave the lane metering a root the
487        // payer has abandoned, and every reveal that followed would fold nothing.
488        if self.chain.verified_index > 0 {
489            return None;
490        }
491        // The root and the signature that commits it are adopted together or not
492        // at all, so the voucher must stand exactly at the lane's watermark.
493        // Anything else is a stale voucher, and taking its anchor would walk the
494        // watermark backwards.
495        if signed.voucher.amount != self.last_amount
496            || signed.voucher.bytes_delivered != self.last_bytes_delivered
497        {
498            return None;
499        }
500        next.chain = LaneChain::opened(chain_root, signed.voucher.chunk_price);
501        next.last_signature = Some(signed.signature.as_bytes());
502        Some(next)
503    }
504
505    /// Fold a verified preimage into the lane, returning the advanced successor
506    /// and what it added (ADR 003 §Concurrent Streams, Rule 2 — deepest wins).
507    ///
508    /// `root` names the epoch the reveal belongs to; the caller learns it from
509    /// the receiving stream's own anchor, which is what makes an unanchored
510    /// reveal decidable per stream rather than ambiguously lane-wide (Rule 1).
511    /// A `root` this lane no longer tracks folds nothing: the reveal is real but
512    /// worthless, since the epoch it extends has been superseded by a signature
513    /// that already folded a frontier at least as deep.
514    ///
515    /// Placement is by index, not arrival order, so a reveal at or below the
516    /// tracked frontier is **already covered**: it advances nothing and — this
517    /// is the load-bearing part — hashes nothing. That is what keeps a
518    /// duplicate or out-of-order reveal free rather than a wasted walk.
519    ///
520    /// Verification hashes forward from the *submitted* value to the tracked
521    /// tip, never from a stored intermediate, so the walk is `index − verified`
522    /// keccaks and the lane keeps no chain state beyond the tip itself.
523    ///
524    /// # Errors
525    ///
526    /// [`PoolError::BadPreimage`] when the value does not reach the tracked tip
527    /// in `index − verified` steps; [`PoolError::CapExceeded`] when the reveal
528    /// would push the claim past the capability's cap (both raised by
529    /// [`Self::advance_preimage_verified`], which this delegates to).
530    pub fn advance_preimage(
531        &self,
532        root: B256,
533        index: u8,
534        preimage: B256,
535    ) -> Result<(Self, PreimageApplied), PoolError> {
536        // The single-lock convenience form: snapshot the frontier, hash forward,
537        // and apply, all against the same `self`. The node's serve path instead
538        // splits these so the keccak walk runs OUTSIDE the per-lane lock (issue
539        // #1792 item 5, [`Self::preimage_frontier`] /
540        // [`Self::advance_preimage_verified`]); this is what the incentive-crate
541        // tests exercise and what any caller without a lock to shed still uses.
542        let walked = self.preimage_frontier(root, index);
543        let walked_ok = match walked {
544            Some((verified_index, tip)) => {
545                crate::chain::verify_forward(preimage, index - verified_index, tip)
546            }
547            // No walk is needed (folds nothing); `advance_preimage_verified`
548            // returns the covered outcome before it consults `walked_ok`.
549            None => false,
550        };
551        self.advance_preimage_verified(root, index, preimage, walked, walked_ok)
552    }
553
554    /// The frontier a reveal at `index` on epoch `root` must hash forward to, or
555    /// `None` when the reveal folds nothing — the root is not the one this lane
556    /// meters against, or `index` sits at or below the tracked frontier.
557    ///
558    /// The seller-side optimistic-walk snapshot (issue #1792 item 5): read this
559    /// under the per-lane lock (it is O(1) and hashes nothing), run
560    /// [`crate::chain::verify_forward`] OUTSIDE the lock against the returned
561    /// `(verified_index, tip)`, then re-lock and commit through
562    /// [`Self::advance_preimage_verified`]. This keeps the up-to-255-keccak walk
563    /// off the lock a payer can otherwise stretch with sparse indices — the same
564    /// discipline the buyer half keeps off its issuance lock.
565    #[must_use]
566    pub fn preimage_frontier(&self, root: B256, index: u8) -> Option<(u8, B256)> {
567        let target = self.chain_slot(root)?;
568        (index > target.verified_index).then_some((target.verified_index, target.tip))
569    }
570
571    /// Fold a preimage whose forward walk the caller ALREADY ran (off the lane
572    /// lock) into the lane, returning the advanced successor and what it added.
573    ///
574    /// `walked` is the `(verified_index, tip)` the caller hashed against —
575    /// [`Self::preimage_frontier`]'s return — and `walked_ok` is what
576    /// [`crate::chain::verify_forward`] answered for it. The caller's result is
577    /// trusted ONLY while the lane's live frontier still equals `walked`: a
578    /// concurrent same-lane sibling that advanced the chain in the gap moved the
579    /// frontier, so this re-hashes against the live tip under the lock (the rare
580    /// race). A reveal a sibling already covered folds nothing, exactly as
581    /// [`Self::advance_preimage`]'s at-or-below-frontier case does. Every other
582    /// check — the tracked-root gate and the spending-cap gate — is O(1) and
583    /// stays under the lock.
584    ///
585    /// # Errors
586    ///
587    /// [`PoolError::BadPreimage`] when the value does not reach the live tip in
588    /// `index − verified` steps; [`PoolError::CapExceeded`] when the reveal would
589    /// push the claim past the capability's cap.
590    pub fn advance_preimage_verified(
591        &self,
592        root: B256,
593        index: u8,
594        preimage: B256,
595        walked: Option<(u8, B256)>,
596        walked_ok: bool,
597    ) -> Result<(Self, PreimageApplied), PoolError> {
598        let Some(target) = self.chain_slot(root) else {
599            return Ok((self.clone(), PreimageApplied::ZERO));
600        };
601        if index <= target.verified_index {
602            return Ok((self.clone(), PreimageApplied::ZERO));
603        }
604        // Trust the off-lock walk only if the frontier it hashed against is still
605        // current; otherwise (a sibling advanced the lane, or the caller walked
606        // nothing) re-hash against the live tip here, under the lock.
607        let verified = match walked {
608            Some((wv, wtip)) if wv == target.verified_index && wtip == target.tip => walked_ok,
609            _ => crate::chain::verify_forward(preimage, index - target.verified_index, target.tip),
610        };
611        if !verified {
612            return Err(PoolError::BadPreimage {
613                index,
614                verified: target.verified_index,
615            });
616        }
617
618        let steps = U256::from(index - target.verified_index);
619        let applied = PreimageApplied {
620            amount_delta: target.chunk_price * steps,
621            bytes_delta: U256::from(crate::chain::CHUNK_BYTES) * steps,
622        };
623
624        let mut next = self.clone();
625        next.chain.verified_index = index;
626        next.chain.tip = preimage;
627        // The value this reveal advances the lane's claim to — the signed anchor
628        // plus the frontier it just proved.
629        let claimed = self.last_amount.saturating_add(next.chain.accrued());
630
631        // The capability's spending cap binds the CLAIM, not the signature, so a
632        // reveal answers for it exactly as a voucher does
633        // ([`Self::advance_presigned`]). A chain extends the claim without a new
634        // signature, so a cap enforced on the voucher axis alone is one the chain
635        // walks straight past — the node would go on crediting deliveries against
636        // a claim the on-chain `redeem` will not pay, and eat the difference.
637        //
638        // Refusing here is also what keeps the payer's recovery working: the reason
639        // maps to `SpendingCapExhausted`, which is watermark-gated, so the reject
640        // carries the bundle the payer needs to raise the cap and resume — the same
641        // route an over-cap voucher already takes.
642        if claimed > self.cap {
643            return Err(PoolError::CapExceeded {
644                cap: self.cap,
645                got: claimed,
646            });
647        }
648        Ok((next, applied))
649    }
650
651    /// The tracked epoch `root` names, if it is the one the lane meters against.
652    ///
653    /// A lane tracks exactly one chain. A reveal naming any other root is real
654    /// but worthless: the epoch it extends was superseded by a signature that
655    /// folded a frontier at least as deep, which is the only way a chain is ever
656    /// retired here (a rollover that folded LESS is refused outright — see
657    /// [`Self::advance_presigned`]).
658    ///
659    /// A zero `root` never matches: it is the sealed sentinel, and nothing
660    /// hashes to zero, so no reveal can extend it.
661    fn chain_slot(&self, root: B256) -> Option<LaneChain> {
662        (!root.is_zero() && root == self.chain.chain_root).then_some(self.chain)
663    }
664
665    /// Validate `signed` against this lane's invariants and, on success, record
666    /// the post-acceptance state to `store` before advancing the in-memory
667    /// `last_*` fields.
668    ///
669    /// Mirrors the on-chain `PaymentPool.redeem` checks:
670    /// - `voucher.pool_id == self.pool_id`
671    /// - `voucher.provider == self.provider`
672    /// - signature recovers to `self.signer`
673    /// - `voucher.amount > self.last_amount`
674    /// - `voucher.bytes_delivered >= self.last_bytes_delivered`
675    /// - `voucher.amount <= self.cap`
676    ///
677    /// Expiry (`now < self.expiry`) is checked by the node handler, which holds
678    /// the clock; this method is time-agnostic.
679    ///
680    /// Ordering of side effects: every check above runs first; if all pass, the
681    /// candidate state is sent to `store.record` and only on `Ok` is in-memory
682    /// state advanced. On any failure — including store failure — `self` is left
683    /// unchanged. This makes `Ok(_)` the protocol-level commit point: further
684    /// bytes are delivered **only after** this method returns `Ok` (ADR 003
685    /// §Off-chain voucher state persistence, issue #527).
686    ///
687    /// # Errors
688    ///
689    /// See [`PoolError`] for the full taxonomy. A persistent-store failure
690    /// surfaces as [`PoolError::Store`].
691    pub fn apply_voucher(
692        &mut self,
693        signed: &SignedVoucher,
694        domain: &Eip712Domain,
695        store: &dyn PoolStateStore,
696    ) -> Result<VoucherApplied, PoolError> {
697        // INVARIANT (#527): validate + advance on a CLONE, record the clone,
698        // swap on `Ok` only. `stage_voucher` produces the advanced successor
699        // without recording; the record-then-swap here is the commit point — the
700        // store takes the row into its working set and flushes it on its own
701        // cadence. Do NOT swap before the `?` on `record`: that's the literal
702        // #527 replay window in code form.
703        let (next, applied) = self.stage_voucher(signed, domain)?;
704        store.record(&next)?;
705        *self = next;
706        Ok(applied)
707    }
708
709    /// Validate `signed` against this state's invariants and return the advanced
710    /// successor state plus its [`VoucherApplied`] — the pure, in-memory half of
711    /// [`Self::apply_voucher`], persisting nothing.
712    ///
713    /// The per-voucher serve loop calls this to verify and advance one voucher
714    /// with no side effect; the caller then records the successor to the buffered
715    /// lane store and swaps it into the live lane. Because vouchers are cumulative
716    /// — each carries the running `amount` / `bytes_delivered` — a later voucher
717    /// supersedes every earlier one.
718    ///
719    /// **Durability.** The caller records the returned state to the lane store,
720    /// which buffers it in memory and makes it durable on a background flush (ADR
721    /// 003 §Off-chain voucher state persistence). A crash loses at most the
722    /// frontier advanced since the last flush, which is safe — an honest client
723    /// resumes forward and an un-redeemed replay stays on-chain-payable — so the
724    /// serve loop advances and delivers without waiting for the fsync; the
725    /// redeemed watermark is floored separately by a flush before every on-chain
726    /// redeem. `stage_voucher` takes `&self` (never mutates the caller's state) so
727    /// an un-recorded candidate is discarded on a rejection without touching the
728    /// live lane.
729    ///
730    /// # Errors
731    ///
732    /// The same validation taxonomy as [`Self::apply_voucher`] **minus**
733    /// [`PoolError::Store`] — no store is touched here.
734    pub fn stage_voucher(
735        &self,
736        signed: &SignedVoucher,
737        domain: &Eip712Domain,
738    ) -> Result<(Self, VoucherApplied), PoolError> {
739        if signed.voucher.pool_id != self.pool_id {
740            return Err(PoolError::WrongPool {
741                expected: self.pool_id,
742                got: signed.voucher.pool_id,
743            });
744        }
745        if signed.voucher.provider != self.provider {
746            return Err(PoolError::WrongProvider {
747                expected: self.provider,
748                got: signed.voucher.provider,
749            });
750        }
751        // Signature check before the amount/bytes monotonicity guards: an
752        // unauthenticated voucher is rejected as `Signature(WrongSigner)`
753        // regardless of the amounts it claims. `verify_signer` recovers a
754        // 65-byte EOA signature via `ecrecover` and rejects non-canonical
755        // high-`s` up front, matching the on-chain verifiable-set (#836).
756        signed
757            .verify_signer(self.signer, domain)
758            .map_err(PoolError::Signature)?;
759        self.advance_presigned(signed)
760    }
761
762    /// The amount/bytes monotonicity + cap half of [`Self::stage_voucher`], for a
763    /// voucher whose signature the caller ALREADY verified against `self.signer`.
764    ///
765    /// The serve loop recovers the (watermark-independent) signature OUTSIDE the
766    /// per-lane lock so concurrent same-lane streams do not serialize on the
767    /// `ecrecover` (#1735), then calls this under the lock to re-check the
768    /// (watermark-dependent) monotonicity guards against the LIVE watermark and
769    /// advance atomically. The monotonicity check and the advance MUST stay under
770    /// one lock hold: two streams reading the same watermark and both advancing
771    /// would lose one voucher.
772    ///
773    /// `pool_id` / `provider` are the lane's own identity and are not re-checked:
774    /// the caller reconstructs `signed` from this lane's pinned identity, so they
775    /// match by construction. Every other check mirrors [`Self::stage_voucher`].
776    ///
777    /// # Errors
778    ///
779    /// [`PoolError::AmountRegression`], [`PoolError::BytesRegression`],
780    /// [`PoolError::CapExceeded`], or [`PoolError::UnderFold`] — the same
781    /// watermark-dependent taxonomy as [`Self::stage_voucher`], minus the
782    /// signature and pool/provider checks.
783    pub fn advance_presigned(
784        &self,
785        signed: &SignedVoucher,
786    ) -> Result<(Self, VoucherApplied), PoolError> {
787        // The caller reconstructs `signed` from this lane's pinned identity, so
788        // `pool_id`/`provider` match by construction and are not re-checked on the
789        // fast path. Guard against a call site that advances a lane with a voucher
790        // built for a DIFFERENT identity (it would silently mutate the watermark).
791        debug_assert_eq!(
792            signed.voucher.pool_id, self.pool_id,
793            "advance_presigned: voucher pool_id must match the lane's"
794        );
795        debug_assert_eq!(
796            signed.voucher.provider, self.provider,
797            "advance_presigned: voucher provider must match the lane's"
798        );
799        if signed.voucher.amount <= self.last_amount {
800            // At exactly the signed anchor, a voucher that opens another chain
801            // has not been settled by any sibling: a sibling that re-sends the
802            // anchor names the live root. With reveals proved on top of the
803            // anchor, it is a rollover that folds none of them. A payer process
804            // that sent reveals and exited before it persisted them resumes
805            // here, so it gets the rejection that carries the fold it owes.
806            // With nothing proved, the live claim is the anchor, and the node
807            // may adopt the new root (see `adopt_chain`).
808            //
809            // A sealed voucher opens no chain. At the anchor it is a sibling's
810            // closing voucher, signed before a sibling opened the live chain at
811            // the same amount and delivered after it, so it stays an ordering
812            // regression.
813            if signed.voucher.amount == self.last_amount
814                && !signed.voucher.chain_root.is_zero()
815                && signed.voucher.chain_root != self.chain.chain_root
816                && let Some(live) = self.live_claim()
817                && live.value() > signed.voucher.amount
818            {
819                return Err(PoolError::UnderFold {
820                    axis: FoldAxis::Amount,
821                    owed: live.value(),
822                    got: signed.voucher.amount,
823                });
824            }
825            return Err(PoolError::AmountRegression {
826                last: self.last_amount,
827                got: signed.voucher.amount,
828            });
829        }
830        if signed.voucher.bytes_delivered < self.last_bytes_delivered {
831            return Err(PoolError::BytesRegression {
832                last: self.last_bytes_delivered,
833                got: signed.voucher.bytes_delivered,
834            });
835        }
836        if signed.voucher.amount > self.cap {
837            return Err(PoolError::CapExceeded {
838                cap: self.cap,
839                got: signed.voucher.amount,
840            });
841        }
842
843        let mut next = self.clone();
844        let amount_delta = signed.voucher.amount - self.last_amount;
845        let bytes_delta = signed.voucher.bytes_delivered - self.last_bytes_delivered;
846
847        // A voucher carrying a DIFFERENT root retires the live epoch, and the
848        // rule that makes that safe is that its `amount` and `bytes_delivered`
849        // must FOLD the frontier the retired chain proved. One that folds less
850        // is refused here, before anything is adopted: it would sign for fewer
851        // chunks than the node holds preimages for, and adopting the new root
852        // would discard the difference.
853        //
854        // Refusing rather than salvaging is deliberate. Within one payer process
855        // it cannot happen: issuance is serialized under the payer's own lock,
856        // and a voucher that folds must also roll, so the folded amount covers
857        // the frontier by construction. A payer reaches it by resuming from a
858        // watermark that trails the node's frontier — a new process after one
859        // that sent reveals and exited before it persisted them. The reason is
860        // watermark-gated, so the rejection carries the bundle that states the
861        // fold owed, the payer folds it and resumes, and the lane's claim is
862        // exactly as strong afterwards as before.
863        //
864        // A voucher that repeats the SAME root is not a rollover at all (a
865        // re-send, or a mid-epoch amount bump), and MUST leave the frontier where
866        // it is; the new epoch starts at index 0 with the root as its own tip, so
867        // a claim at exactly the new `amount` walks nothing.
868        if signed.voucher.chain_root == self.chain.chain_root {
869            // Same epoch: the price is re-asserted by every voucher that names
870            // the root, and the handler has already refused a price that is not
871            // this node's quote, so adopting it keeps the two in step.
872            next.chain.chunk_price = signed.voucher.chunk_price;
873        } else {
874            // The fold binds both axes (ADR 003 §Rollover): a voucher that pays
875            // the whole claim but signs fewer bytes than the frontier proved
876            // would still retire those chunks from the lane's byte claim.
877            if let Some(live) = self.live_claim() {
878                if live.value() > signed.voucher.amount {
879                    return Err(PoolError::UnderFold {
880                        axis: FoldAxis::Amount,
881                        owed: live.value(),
882                        got: signed.voucher.amount,
883                    });
884                }
885                if live.bytes_value() > signed.voucher.bytes_delivered {
886                    return Err(PoolError::UnderFold {
887                        axis: FoldAxis::Bytes,
888                        owed: live.bytes_value(),
889                        got: signed.voucher.bytes_delivered,
890                    });
891                }
892            }
893            next.chain = LaneChain::opened(signed.voucher.chain_root, signed.voucher.chunk_price);
894        }
895
896        next.last_amount = signed.voucher.amount;
897        next.last_bytes_delivered = signed.voucher.bytes_delivered;
898        // Retain the signature so the seller path can submit this exact voucher
899        // to the on-chain `PaymentPool.redeem`. Same `r‖s‖v` encoding as the
900        // wire form in `client_bridge`; `Signature::as_bytes` is exactly 65
901        // bytes.
902        next.last_signature = Some(signed.signature.as_bytes());
903
904        Ok((
905            next,
906            VoucherApplied {
907                amount_delta,
908                bytes_delta,
909            },
910        ))
911    }
912}
913
914/// Outcome of a successful [`LaneState::apply_voucher`].
915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916pub struct VoucherApplied {
917    /// Incremental amount this voucher added over the prior watermark
918    /// (`voucher.amount - last_amount`). Always positive — the amount guard
919    /// rejects a non-increasing voucher.
920    amount_delta: U256,
921    /// Incremental bytes this voucher added over the prior watermark
922    /// (`voucher.bytes_delivered - last_bytes_delivered`). Zero or positive.
923    bytes_delta: U256,
924}
925
926impl VoucherApplied {
927    /// Incremental amount this voucher added over the prior watermark.
928    #[must_use]
929    pub const fn amount_delta(&self) -> U256 {
930        self.amount_delta
931    }
932
933    /// Incremental bytes this voucher added over the prior watermark.
934    #[must_use]
935    pub const fn bytes_delta(&self) -> U256 {
936        self.bytes_delta
937    }
938}
939
940/// Outcome of a [`LaneState::advance_preimage`] fold.
941///
942/// Both deltas are zero for a reveal that advanced nothing — one at or below
943/// the tracked frontier, or one naming a superseded epoch. Neither is an error:
944/// a duplicate or out-of-order reveal is ordinary under concurrent streams, and
945/// the node answers it by continuing to deliver, exactly as for an
946/// already-satisfied voucher.
947#[derive(Debug, Clone, Copy, PartialEq, Eq)]
948pub struct PreimageApplied {
949    /// `steps × chunk_price` — what this reveal added to the lane's claim.
950    amount_delta: U256,
951    /// `steps × CHUNK_BYTES` — the byte frontier the same steps paid for.
952    bytes_delta: U256,
953}
954
955impl PreimageApplied {
956    /// The reveal advanced nothing. Two cases reach it and callers treat them
957    /// identically, because the answer is the same in both: the reveal was at or
958    /// below the tracked frontier (already covered, and nothing was hashed to
959    /// find that out), or it named an epoch the lane no longer tracks (real, but
960    /// superseded by a signature that folded a frontier at least as deep).
961    pub const ZERO: Self = Self {
962        amount_delta: U256::ZERO,
963        bytes_delta: U256::ZERO,
964    };
965
966    /// Amount this reveal added to the lane's claim.
967    #[must_use]
968    pub const fn amount_delta(&self) -> U256 {
969        self.amount_delta
970    }
971
972    /// Bytes this reveal's steps paid for.
973    #[must_use]
974    pub const fn bytes_delta(&self) -> U256 {
975        self.bytes_delta
976    }
977
978    /// `true` when the reveal advanced the lane's claim.
979    #[must_use]
980    pub fn advanced(&self) -> bool {
981        !self.amount_delta.is_zero() || !self.bytes_delta.is_zero()
982    }
983}
984
985/// The axis of a lane claim a rollover voucher has to fold
986/// ([`PoolError::UnderFold`]).
987#[derive(Debug, Clone, Copy, PartialEq, Eq)]
988pub enum FoldAxis {
989    /// The cumulative `amount`.
990    Amount,
991    /// The cumulative `bytes_delivered`.
992    Bytes,
993}
994
995impl std::fmt::Display for FoldAxis {
996    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
997        f.write_str(match self {
998            Self::Amount => "amount",
999            Self::Bytes => "bytes_delivered",
1000        })
1001    }
1002}
1003
1004/// Failure modes for [`LaneState::apply_voucher`].
1005///
1006/// Most in-memory variants map to an on-chain `PaymentPool.redeem` revert;
1007/// off-chain we surface them before they cost gas. The ordering and fold
1008/// guards ([`PoolError::AmountRegression`], [`PoolError::BytesRegression`],
1009/// [`PoolError::UnderFold`]) have no revert — redemption is cumulative — and
1010/// keep the node's own ledger consistent. [`PoolError::Store`] is the
1011/// off-chain-only variant for persistent-store failures (issue #527).
1012///
1013/// `PartialEq`/`Eq` are intentionally not derived: [`StoreError::Io`] wraps
1014/// `std::io::Error`, which is not `PartialEq`. Tests pattern-match on variants
1015/// via the `matches!` macro instead of comparing for equality.
1016#[derive(Debug, thiserror::Error)]
1017pub enum PoolError {
1018    /// `voucher.pool_id` does not match the pool this lane draws from — also
1019    /// the "unknown pool" case surfaced by the caller.
1020    #[error("voucher pool_id {got} does not match expected {expected}")]
1021    WrongPool {
1022        /// The pool this lane draws from.
1023        expected: B256,
1024        /// The pool the voucher named.
1025        got: B256,
1026    },
1027    /// `voucher.provider` names a node other than this one — a capability
1028    /// voucher scoped to one provider redeemed against another.
1029    #[error("voucher provider {got} does not match expected {expected}")]
1030    WrongProvider {
1031        /// This node's provider address.
1032        expected: Address,
1033        /// The provider the voucher named.
1034        got: Address,
1035    },
1036    /// Cumulative amount is at or below the lane's signed watermark — a stale
1037    /// or replayed voucher, or one a concurrent same-lane sibling already
1038    /// settled. `amount` is the sole ordering key (there is no nonce).
1039    #[error("voucher amount {got} not greater than last accepted {last}")]
1040    AmountRegression {
1041        /// The signed watermark the voucher's cumulative amount had to beat.
1042        last: U256,
1043        /// The cumulative amount it carried.
1044        got: U256,
1045    },
1046    /// A voucher under a different `chain_root` retires the live epoch but
1047    /// does not fold the frontier the live chain proved: its `amount` or its
1048    /// `bytes_delivered` falls short of the signed anchor plus `verified_index`
1049    /// chunks (ADR 003 §Chain length and rollover). It is at or above the
1050    /// signed watermark under a root no sibling names — at exactly the anchor,
1051    /// a root that opens a chain, never a sealed one — so no sibling settled
1052    /// it; the payer under-signs chunks the node holds preimages for. The node
1053    /// refuses it and sends the resume bundle that states the fold the payer
1054    /// owes. A payer short only on `bytes_delivered` cannot fold from the
1055    /// bundle, because the bundle does not advance its amount.
1056    #[error("voucher {axis} {got} does not fold the live claim {owed}")]
1057    UnderFold {
1058        /// The axis that falls short. The amount is checked first.
1059        axis: FoldAxis,
1060        /// The lane's live claim on `axis`: signed anchor plus the proved
1061        /// frontier.
1062        owed: U256,
1063        /// The cumulative figure the voucher carried on `axis`.
1064        got: U256,
1065    },
1066    /// Cumulative bytes delivered went down.
1067    #[error("voucher bytes_delivered {got} less than last accepted {last}")]
1068    BytesRegression {
1069        /// Last accepted cumulative bytes.
1070        last: U256,
1071        /// The lower figure the voucher carried.
1072        got: U256,
1073    },
1074    /// Voucher amount exceeds the capability's spending cap — the contract
1075    /// would revert at redemption.
1076    #[error("voucher amount {got} exceeds capability cap {cap}")]
1077    CapExceeded {
1078        /// The capability's spending cap.
1079        cap: U256,
1080        /// The voucher amount that exceeds it.
1081        got: U256,
1082    },
1083    /// A released hash-chain preimage does not reach the tracked tip in
1084    /// `index − verified` steps (ADR 003 §Concurrent Streams, Rule 2).
1085    ///
1086    /// Terminal, and deliberately **not** watermark-gated on the wire: a
1087    /// preimage carries no signature of its own, so a payment watermark cannot
1088    /// repair a hash-chain mismatch. On-chain the same mismatch reverts
1089    /// `BadPreimage`, which is caller error rather than transient state.
1090    #[error("released preimage at index {index} does not reach the tip verified at {verified}")]
1091    BadPreimage {
1092        /// Chain index of the released preimage.
1093        index: u8,
1094        /// Index of the tip the lane has verified to.
1095        verified: u8,
1096    },
1097    /// Signature is malformed or signed by the wrong address.
1098    #[error(transparent)]
1099    Signature(#[from] VoucherError),
1100    /// Lane-store write failed; in-memory state is unchanged. See [`StoreError`]
1101    /// for the underlying cause. This is surfaced to the caller so the client
1102    /// resends the same voucher instead of proceeding on state the store never
1103    /// took (#527, ADR 003 §Off-chain voucher state persistence).
1104    ///
1105    /// PROTOCOL NOTE: this is the only `PoolError` variant for which the client
1106    /// SHOULD retry the same voucher unchanged. The validation variants mean the
1107    /// voucher is permanently invalid for this lane state and retrying is a
1108    /// client bug.
1109    #[error("pool state store failed: {0}")]
1110    Store(#[from] StoreError),
1111}
1112
1113#[cfg(test)]
1114#[allow(clippy::similar_names)] // signer/signed pair up clearly here
1115mod tests;