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;