Skip to main content

decdn_client/
ledger.rs

1//! Per-lane voucher accounting. [`next_voucher`] is the pure cumulative math;
2//! [`PoolLedger`] serializes voucher *issuance* across concurrent streams that
3//! draw on one pool lane. Continued delivery is acceptance (ADR 003/005): a sent
4//! voucher commits optimistically — the send itself is the commit — and only an
5//! explicit rejection rewinds it. There is no ack to wait for, so the issuance
6//! lock spans compute → sign → send and nothing more, and parallel streams to
7//! one provider never serialize their payments behind each other's round trips.
8
9use std::future::Future;
10use std::time::Duration;
11
12use alloy::primitives::{B256, U256};
13use decdn_incentive::chain::{CHUNK_BYTES, MAX_CHAIN_LENGTH};
14use decdn_protocol::MB_BYTES;
15use decdn_protocol::client::WatermarkBundle;
16use tokio::sync::Mutex;
17
18/// A lane's cumulative voucher state: the absolute totals carried by the most
19/// recent voucher. Both advance monotonically over the lane's lifetime. There
20/// is no nonce — `amount` is the sole monotone ordering and replay key (ADR 005
21/// §Voucher wire format).
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub struct Cumulative {
24    /// Cumulative lane bytes paid for.
25    pub bytes: U256,
26    /// Cumulative lane amount paid (token base units).
27    pub amount: U256,
28}
29
30impl Cumulative {
31    /// Componentwise sum — the anchor plus what the chain has accrued on top of
32    /// it, which together are what the lane is owed.
33    #[must_use]
34    const fn plus(self, other: Self) -> Self {
35        Self {
36            bytes: self.bytes.saturating_add(other.bytes),
37            amount: self.amount.saturating_add(other.amount),
38        }
39    }
40
41    /// Componentwise saturating difference — what is left of an accrual once a
42    /// voucher has folded part of it.
43    #[must_use]
44    const fn minus(self, other: Self) -> Self {
45        Self {
46            bytes: self.bytes.saturating_sub(other.bytes),
47            amount: self.amount.saturating_sub(other.amount),
48        }
49    }
50}
51
52impl From<&WatermarkBundle> for Cumulative {
53    /// Decode a wallet-less resume bundle (issue #1481) into the full lane claim
54    /// it represents, in the shape [`PoolLedger`] tracks.
55    ///
56    /// **This folds the chain half.** A bundle reports two things: the signed
57    /// anchor the node last accepted, and the frontier its chain has proved on
58    /// top of that anchor. Decoding only the anchor would throw away
59    /// `verified_index` chunks the node already holds preimages for — and the
60    /// resuming signer would then re-sign from the anchor, open a fresh root
61    /// beside a frontier the node is still metering, and watch every reveal that
62    /// followed fold nothing (the node keeps the old root: a chain with reveals
63    /// under it is worth more than the anchor a lagging voucher offers for it).
64    /// So the fold is not an optimisation; it is what makes a resume converge.
65    ///
66    /// It is the same fold a rollover performs — `amount + verified_index ×
67    /// chunk_price`, `bytes + verified_index × CHUNK_BYTES` — and the node's
68    /// `watermark_bundle_for_reject` documents it as the signer's side of the
69    /// bargain (ADR 005 §Watermark bundle). A bundle carrying no chain
70    /// (`verified_index == 0`, the sealed or never-metered case) folds nothing
71    /// and decodes to the anchor alone.
72    ///
73    /// Infallible: the fold is computed in `U256`, so the `u64` inputs cannot
74    /// overflow it and a caller can re-seed from a bundle without a `Result`.
75    ///
76    /// It is also unauthenticated, and deliberately so — this is arithmetic, not
77    /// a trust boundary. `verified_index` is a number the node writes and no
78    /// signature covers, so a bundle reaches this conversion only through
79    /// `resumable_watermark`, which proves the anchor against the client's own
80    /// signature and the frontier against the bundle's `tip` before any of it
81    /// becomes money. Do not fold a bundle that has not been through that gate.
82    fn from(bundle: &WatermarkBundle) -> Self {
83        let proved = U256::from(bundle.verified_index);
84        Self {
85            bytes: U256::from(bundle.bytes_delivered)
86                .saturating_add(proved.saturating_mul(U256::from(CHUNK_BYTES))),
87            amount: U256::from(bundle.amount)
88                .saturating_add(proved.saturating_mul(U256::from(bundle.chunk_price))),
89        }
90    }
91}
92
93/// Compute the next voucher's absolute totals from the live cumulative and the
94/// `delta_bytes` newly delivered (on any stream) since the last voucher. The
95/// amount delta is `ceil(delta_bytes * rate_per_mb / 1 MiB)` so each voucher's
96/// own delta covers its own bytes at the advertised rate (the node checks deltas).
97#[must_use]
98fn next_voucher(cur: &Cumulative, delta_bytes: u64, rate_per_mb: u64) -> Cumulative {
99    let amount_delta = U256::from(delta_bytes)
100        .saturating_mul(U256::from(rate_per_mb))
101        .div_ceil(U256::from(MB_BYTES));
102    Cumulative {
103        bytes: cur.bytes.saturating_add(U256::from(delta_bytes)),
104        amount: cur.amount.saturating_add(amount_delta),
105    }
106}
107
108/// One lane's live hash-chain epoch: the payer half of the `PayWord` meter
109/// (ADR 003 §Hash-chain metering).
110///
111/// The seed is **drawn, never stored and never reproduced** — 32 fresh bytes at
112/// open, held here for as long as the chain meters and dropped with it. Two
113/// chains therefore share a root only with probability 2⁻²⁵⁶, which is the
114/// whole of the no-reuse property: there is no derivation input to bind, no
115/// counter to persist, and nothing secret at rest.
116///
117/// A chain never outlives the process that drew it. Resumption after a restart
118/// folds the frontier the node proved into a signed amount and opens a fresh
119/// chain (ADR 003 §Resumption folds), so re-deriving an old seed is not a
120/// capability this type gives up — it is one nothing asks for.
121///
122/// The whole 256-entry ladder is materialised at open. One pass of
123/// `MAX_CHAIN_LENGTH` keccaks fills it (`preimages[255] = seed`, each earlier
124/// entry one more hash, `preimages[0] = root`), which turns every later release
125/// into an array read. The alternative — re-hashing from the seed per release —
126/// would put up to 255 keccaks under the issuance lock on the delivery path, to
127/// save 8 KiB per lane.
128#[derive(Debug)]
129struct ChainEpoch {
130    /// `preimages[k]` is the value released at index `k`; `preimages[0]` is the
131    /// `chain_root` the voucher commits.
132    preimages: Box<[B256; 256]>,
133    /// What one chunk adds over the anchor. Fixed for the epoch by the voucher
134    /// that opened it, and the other half of the seed.
135    chunk_price: U256,
136    /// The deepest index released so far. `0` means only the root exists, which
137    /// is the state a fresh epoch opens in.
138    released: u8,
139}
140
141impl ChainEpoch {
142    fn open(chunk_price: U256) -> Self {
143        let seed = decdn_incentive::chain::random_seed();
144        let mut preimages = Box::new([B256::ZERO; 256]);
145        let mut acc = seed;
146        // Walk down from the seed at index 255 to the root at index 0, so the
147        // ladder costs one pass rather than one pass per entry.
148        for index in (0..=usize::from(MAX_CHAIN_LENGTH)).rev() {
149            if let Some(slot) = preimages.get_mut(index) {
150                *slot = acc;
151            }
152            acc = alloy::primitives::keccak256(acc);
153        }
154        Self {
155            preimages,
156            chunk_price,
157            released: 0,
158        }
159    }
160
161    /// The head this epoch commits — the value at index 0.
162    fn root(&self) -> B256 {
163        self.preimages.first().copied().unwrap_or(B256::ZERO)
164    }
165
166    /// The next index to release, or `None` once the epoch is spent and the
167    /// payer must roll to a fresh root. The increment is lazy: `released` sits at
168    /// `MAX_CHAIN_LENGTH` (`u8::MAX`) once the ladder is spent, where an eager
169    /// `+ 1` overflows before the guard can refuse it.
170    fn next_index(&self) -> Option<u8> {
171        (self.released < MAX_CHAIN_LENGTH).then(|| self.released + 1)
172    }
173}
174
175/// The committed watermark plus the one-step rewind and the ambiguous in-flight
176/// voucher, under a single lock so a reader never catches a half-applied update.
177/// All three are read from a `Drop` impl, so the lock is a `std::sync::Mutex` —
178/// see [`PoolLedger::committed`].
179#[derive(Debug)]
180struct Pipeline {
181    /// What the chain has accrued SINCE the last signature: `released ×
182    /// chunk_price` on the money axis and `released × CHUNK_BYTES` on the byte
183    /// axis (ADR 003 §Hash-chain metering).
184    ///
185    /// Kept apart from `committed` — which stays the **signed anchor**, mirroring
186    /// exactly what the node stores as its lane watermark — because a chain
187    /// extends an anchor rather than replacing it. Folding accrual into the
188    /// anchor early would make every later signature an implicit fold, and a
189    /// fold that does not also roll to a fresh root is counted twice: once in
190    /// the signed amount, and again by the frontier the node still holds under
191    /// that same root.
192    accrued: Cumulative,
193    /// The accrual as of the previous signature, so a rejection rewinds both
194    /// halves of the claim together.
195    prev_accrued: Cumulative,
196    /// The proof most recently put on the wire for this lane, identified — not
197    /// merely classified — so a rejection rewinds the thing that was actually
198    /// refused and nothing else.
199    ///
200    /// A rejection names no proof. Under the chain the lane emits two kinds and
201    /// they rewind differently — a refused voucher un-commits the anchor, a
202    /// refused reveal only takes back one chunk of accrual — so guessing wrong
203    /// walks the anchor backwards below what the node accepted, and every later
204    /// voucher then regresses. And the lane is shared: a rejection is read on
205    /// the stream it arrived on, while a sibling may have issued since, so the
206    /// KIND alone is not enough to tell whether the refused proof is still the
207    /// one this field holds. See [`PoolLedger::resolve_reject`].
208    last_proof: Option<LastProof>,
209    /// The highest voucher whose send SUCCEEDED — presumed accepted, because
210    /// continued delivery IS acceptance (ADR 005: only rejection is signalled).
211    /// Advanced optimistically on each successful [`PoolLedger::issue`] and on a
212    /// [`PoolLedger::confirm_armed_stamped`], rewound one step by
213    /// [`PoolLedger::resolve_reject`], and hard-set by [`PoolLedger::reseed`].
214    committed: Cumulative,
215    /// The committed watermark BEFORE the most-recent successful voucher, so a
216    /// later [`PoolLedger::resolve_reject`] can un-commit exactly that voucher —
217    /// the one the node declared it never took. A reject terminates the stream
218    /// (`handlers/client/wire.rs::write_reject`), and the self-heal path reseeds
219    /// from the authenticated bundle immediately after, so a single-step rewind
220    /// covers what the settlement window needs.
221    prev: Option<Cumulative>,
222    /// A voucher ARMED for send whose send did NOT confirm — an ambiguous error,
223    /// or a future dropped mid-await. `committed` never advanced to it, but
224    /// [`PoolLedger::settlement`] reports it (settle high — the upstream persists
225    /// before it would reject, ADR 003), so a pull dropped inside the send still
226    /// persists what the upstream may hold. Cleared on the next successful
227    /// [`PoolLedger::issue`], a matching [`PoolLedger::confirm_armed_stamped`] or
228    /// [`PoolLedger::resolve_reject`], or a [`PoolLedger::reseed`].
229    armed: Option<Armed>,
230    /// How many times a rebase has moved this ledger to a node's watermark
231    /// behind it on `amount`. Every voucher is stamped with the generation it
232    /// was signed under (`StreamProof::Voucher`), so an `Underpaid` or
233    /// `BytesRegression` rejection of a voucher signed before the latest
234    /// rebase is recognisably stale.
235    generation: u64,
236    /// The watermark the latest rebase moved to, until the
237    /// caller records it ([`PoolLedger::take_unsaved_rebase`]). A monotone
238    /// advance refuses a lower watermark, so the next persist overwrites the
239    /// lane record with this anchor once, then advances from it.
240    unsaved_rebase: Option<Cumulative>,
241    /// The node watermark the latest heal acted on: the bundle a
242    /// [`PoolLedger::reseed`], [`PoolLedger::rebase`], or
243    /// [`PoolLedger::retire_unadopted_chain`] took. Sibling streams on one lane
244    /// take the same rejection with the same bundle. The first heals the lane,
245    /// and this lets the rest see that it did, so they leave alone the chain
246    /// the healed lane opened next.
247    healed_from: Option<Cumulative>,
248    /// The lane claim, in bytes, that this ledger's own proofs grow from: the
249    /// seed's bytes, moved by every reseed and rebase ([`Pipeline::heal`]).
250    /// [`PoolLedger::paid_bytes`] is the committed claim above it.
251    baseline: U256,
252}
253
254impl Pipeline {
255    /// Move the committed watermark to the node's watermark `cum`, and move
256    /// [`Self::baseline`] so that [`PoolLedger::paid_bytes`] stays what this
257    /// ledger added to the claim the node can redeem.
258    ///
259    /// A watermark above ours carries payment this ledger did not make, so
260    /// the baseline rises by the same jump. A watermark below ours drops
261    /// claims the node never accepted. They were never redeemable, so they
262    /// leave the count. When the watermark is also below the baseline, the
263    /// claims between the two were never collected either: this ledger signs
264    /// them again, and they count as its payment, so the baseline falls to
265    /// the watermark.
266    fn heal(&mut self, cum: Cumulative) {
267        let from = self.committed.plus(self.accrued).bytes;
268        self.baseline = if cum.bytes >= from {
269            self.baseline.saturating_add(cum.bytes.saturating_sub(from))
270        } else {
271            self.baseline.min(cum.bytes)
272        };
273        self.overwrite(cum);
274        self.healed_from = Some(cum);
275    }
276
277    /// Overwrite the committed watermark with `cum` and clear everything that
278    /// was relative to the old one: accrual, rewind, and the armed voucher. The
279    /// node has just told us its authoritative watermark, so none of it holds.
280    fn overwrite(&mut self, cum: Cumulative) {
281        self.committed = cum;
282        self.accrued = Cumulative::default();
283        self.prev = None;
284        self.prev_accrued = Cumulative::default();
285        self.armed = None;
286        self.last_proof = None;
287    }
288
289    /// What the lane owes, reported high: the anchor plus the accrual, or the
290    /// armed voucher when it claims more. See [`PoolLedger::settlement`].
291    fn settlement(&self) -> Cumulative {
292        let owed = self.committed.plus(self.accrued);
293        match self.armed {
294            Some(armed) if armed.voucher.amount > owed.amount => armed.voucher,
295            _ => owed,
296        }
297    }
298}
299
300/// A voucher whose send did not confirm, together with the accrual it folded.
301#[derive(Debug, Clone, Copy)]
302struct Armed {
303    /// The cumulative the voucher signs.
304    voucher: Cumulative,
305    /// The accrual outstanding when the voucher was armed — what committing it
306    /// takes out of `accrued`. Reveals released after the arming stay in
307    /// `accrued`, because the voucher never signed them.
308    folded: Cumulative,
309}
310
311/// What the most recent voucher did to the lane's chain slot, and therefore what
312/// a rejection of that voucher has to undo.
313#[derive(Debug)]
314enum Displaced {
315    /// The voucher left the chain slot alone — a re-anchor, a residual, an open
316    /// on a lane that already had one. A rejection restores nothing.
317    Nothing,
318    /// The voucher REPLACED the chain slot; a rejection puts this value back.
319    /// Itself empty when what it replaced was no chain at all (an open on a bare
320    /// lane), which a rejection must restore just as faithfully.
321    Replaced(Option<ChainEpoch>),
322}
323
324/// Which proof last went out, named exactly, and what undoing it costs.
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326enum LastProof {
327    /// A signed voucher, named by the cumulative it claims. Rewinding it
328    /// restores the previous anchor and hands back the accrual that voucher
329    /// folded.
330    Voucher { amount: U256 },
331    /// One released preimage, named by its chain and depth, and worth
332    /// `chunk_price` on the money axis and one `CHUNK_BYTES` on the byte axis.
333    /// Rewinding it takes back exactly that and leaves the signed anchor alone.
334    Reveal {
335        chain_root: B256,
336        index: u8,
337        chunk_price: U256,
338    },
339}
340
341/// The proof one stream put on the wire, in the terms that identify it on the
342/// lane — what a stream must hand [`PoolLedger::resolve_reject`] to say which
343/// proof the rejection it just read was for.
344///
345/// Both names are unique on a lane by construction: issuance is serialized, so
346/// cumulative amounts strictly increase and no two vouchers share one, and a
347/// chain's indices strictly deepen so no two reveals share a `(root, index)`.
348/// That is what lets the ledger tell "the proof I am being told about is still
349/// the last thing this lane did" from "a sibling has moved on since".
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
351pub(crate) enum StreamProof {
352    /// A signed voucher claiming this cumulative amount.
353    Voucher {
354        /// The `amount` the voucher was signed over.
355        amount: U256,
356        /// The ledger generation it was signed under ([`PoolLedger::generation`]).
357        generation: u64,
358    },
359    /// A preimage released at this depth on this chain.
360    Reveal {
361        /// The chain the reveal extends, named by its root.
362        chain_root: B256,
363        /// The depth released.
364        index: u8,
365    },
366}
367
368/// What [`PoolLedger::rebase`] did with a node's watermark.
369#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub enum Rebase {
371    /// The committed watermark moved to the node's, from `from`.
372    Rebased {
373        /// The committed-plus-accrued watermark the ledger held before.
374        from: Cumulative,
375    },
376    /// The rejected voucher was signed before the latest rebase, so its
377    /// rejection says nothing about the healed anchor. Nothing moved.
378    Stale,
379    /// The watermark is not one this rebase moves to (see
380    /// [`PoolLedger::rebase`] and the `BytesRegression` heal). Nothing moved.
381    Refused,
382}
383
384/// What a voucher should do with the lane's hash chain.
385///
386/// A chain is not a second payment object: the lane has one voucher whose
387/// cumulative `amount` is the settlement anchor, and the chain is an optional
388/// extension that advances that anchor without another signature. So every
389/// voucher makes exactly one of these three statements about it.
390///
391/// There is no "seal" statement, because sealing is not a decision a caller
392/// makes: [`Self::Keep`] on a lane that meters nothing already commits
393/// [`ChainCommit::SEALED`], which is every voucher a sub-chunk transfer ever
394/// sends.
395#[derive(Clone, Copy, Debug, PartialEq, Eq)]
396pub enum EpochAction {
397    /// Commit to whatever the lane already meters against — the live epoch's
398    /// root, or a **sealed** section if it holds none. Opens nothing.
399    ///
400    /// This is what a per-stream re-anchor and a closing residual voucher both
401    /// use. Re-asserting a root the node already holds is free, because a
402    /// metering voucher at or below the watermark is already-satisfied and pays
403    /// no whole chunk, so the node never rejects it,
404    /// and settling a partial trailing chunk this way leaves the chain live for
405    /// every sibling stream on the lane — which sealing would not.
406    Keep,
407    /// Commit to the live epoch, opening one if the lane meters nothing yet.
408    /// A stream sends this before its first reveal.
409    Open,
410    /// Retire the live epoch and commit to a fresh root. The payer rolls when it
411    /// exhausts a chain, and may roll earlier at its own discretion; either way
412    /// the fold is the frontier actually reached, never a flat 255.
413    Roll,
414}
415
416/// The chain section a voucher signs: what the payer commits to, and what the
417/// node checks its own quoted rate against.
418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
419pub struct ChainCommit {
420    /// The chain head, or zero on a sealed voucher.
421    pub chain_root: B256,
422    /// What one chunk adds over the anchor, or zero on a sealed voucher.
423    pub chunk_price: U256,
424}
425
426impl ChainCommit {
427    /// The sealed section: uniformly zero, which is what lets the node's check
428    /// be `chain_root == 0 ⟺ chunk_price == 0` with no branch on either side.
429    pub const SEALED: Self = Self {
430        chain_root: B256::ZERO,
431        chunk_price: U256::ZERO,
432    };
433}
434
435/// A preimage the payer has released, and where it sits.
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437pub struct Released {
438    /// The released value.
439    pub preimage: B256,
440    /// Its depth in the chain. Always `1..=MAX_CHAIN_LENGTH` — index 0 names the
441    /// root and proves nothing the voucher does not already say, so it never
442    /// travels the wire.
443    pub index: u8,
444    /// The chain it belongs to, named by its root — so a caller can tell whether
445    /// the stream it is sending on has already carried that chain's root voucher.
446    /// The root IS the chain's identity: each chain draws its own secret, so a
447    /// fresh chain always has a fresh root.
448    pub chain_root: B256,
449}
450
451/// Outcome of one [`PoolLedger::meter`] tick.
452#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453pub enum Metered {
454    /// A preimage went out and the lane advanced by one chunk.
455    Released(Released),
456    /// The epoch has no index left (or the lane holds no chain). Nothing was
457    /// sent; the caller signs a rollover voucher and meters again.
458    Exhausted,
459    /// The lane meters a chain other than the one the stream is anchored to: a
460    /// sibling rolled the lane after the stream anchored. Nothing was sent; the
461    /// caller re-anchors the stream and meters again.
462    Moved,
463}
464
465/// One lane's live voucher ledger, shared by every concurrent stream that draws
466/// on it. Voucher *issuance* is serialized through the async `issuance` mutex —
467/// held across compute → sign → send so vouchers reach the node in strict
468/// cumulative order — and released the instant the send returns, because there
469/// is no ack to wait for (implicit acceptance, ADR 005). The committed watermark
470/// advances the moment a send succeeds; a mid-stream `VoucherRejected` rewinds
471/// it through `resolve_reject`.
472#[derive(Debug)]
473pub struct PoolLedger {
474    /// Serializes issuance (compute the next cumulative → sign → send). Its
475    /// guarded value is `()`: the ordering it enforces lives in `pipeline`, and
476    /// this is only the token that makes the compute-and-send critical section
477    /// mutually exclusive across concurrent issuers. Held across an `await` (the
478    /// send), so it is a `tokio::sync::Mutex`, never the sync one below.
479    issuance: Mutex<()>,
480    /// The committed watermark + rewind + armed voucher. A `std::sync::Mutex`
481    /// because it is read from `Drop` (which cannot await): a pull can end by
482    /// being DROPPED, not only by returning, and this is the record of money
483    /// already spent (#1145 review). Held for a few instructions at a time and
484    /// never across an await, so it cannot deadlock with the issuance lock.
485    pipeline: std::sync::Mutex<Pipeline>,
486    /// The lane's live hash-chain epoch. Guarded by its own sync mutex for the
487    /// same reason `pipeline` is, and only ever touched while the `issuance`
488    /// lock is held — the payer is one process, so it serializes the chain
489    /// index and the rollover decision under a local lock while bytes stream
490    /// concurrently (ADR 003 §Concurrent Streams).
491    ///
492    /// `None` on a lane that has not opened a chain yet: a transfer smaller
493    /// than one chunk never meters, and settles through a single sealed
494    /// amount-voucher.
495    epoch: std::sync::Mutex<Option<ChainEpoch>>,
496    /// The chain the most recent voucher DISPLACED, so a rejection can put it
497    /// back. `None` means that voucher displaced nothing; `Some(x)` restores `x`
498    /// (itself `None` for a seal, which displaces a chain with no chain).
499    ///
500    /// A rollover replaces the chain BEFORE the send, because the voucher has to
501    /// carry the new root to be signed at all. If the node then refuses it, the
502    /// payer would otherwise be left metering a chain the node never adopted: its
503    /// reveals name a root the lane does not track and fold nothing, and its next
504    /// re-anchor states a root the node will not accept over a frontier it has
505    /// already proved. Keeping one step of history costs one ladder and makes the
506    /// rejection exactly undoable.
507    ///
508    /// Locked AFTER `epoch` wherever both are taken, which is the only order
509    /// either is ever acquired in.
510    retired: std::sync::Mutex<Displaced>,
511    /// Ceiling on a single voucher send. The issuance lock is held across the
512    /// send on purpose — vouchers must reach the node in strict cumulative order
513    /// — but that means one send is on the critical path of EVERY concurrent
514    /// pull sharing this lane. Without a bound, an upstream that stops reading
515    /// wedges issuance for the whole lane forever, and no error is ever raised,
516    /// so per-blob failover never fires. When a send exceeds this the method
517    /// returns an error, releasing the lock and letting the pull leg fail over
518    /// to another provider. A timed-out send is treated exactly like any other
519    /// ambiguous send: the voucher stays armed and `settlement` settles high.
520    send_deadline: Duration,
521    /// Content spans a lane delivered and did not pay for when it ended on a
522    /// funding refusal: `(hash, start, end)`. ADR 003's credit window lets the
523    /// node stream ahead of the voucher that pays for it, so the store can
524    /// hold these bytes while the lane owes them. When this lane runs again,
525    /// its next drive takes them and bills them again ([`Self::take_unpaid`]).
526    /// They live only on this lane. A fetch that completes without this lane
527    /// running again leaves them unpaid, and so does a replaced pool or a
528    /// rotated key, which builds a new lane. An unpaid span is the node's
529    /// credit-window loss, at most one credit window per lane, as for any
530    /// client that ends mid-stream.
531    unpaid: std::sync::Mutex<Vec<([u8; 32], u64, u64)>>,
532}
533
534/// Default ceiling on one voucher send (see [`PoolLedger::send_deadline`]). A
535/// voucher or preimage is a tiny frame on a low-volume stream, so a send that
536/// takes this long means the upstream has stopped acknowledging at the
537/// transport level, not that it is merely slow. Generous enough never to fire on
538/// a healthy-but-loaded peer, short enough to bound the wedge into a failover.
539pub(crate) const VOUCHER_SEND_DEADLINE: Duration = Duration::from_secs(20);
540
541/// Merge `(start, end)` spans into disjoint `(start, len)` ranges in ascending
542/// order. Overlapping or adjacent spans join; empty spans drop.
543pub(crate) fn merge_spans(mut spans: Vec<(u64, u64)>) -> Vec<(u64, u64)> {
544    spans.retain(|&(start, end)| start < end);
545    spans.sort_unstable();
546    let mut merged: Vec<(u64, u64)> = Vec::with_capacity(spans.len());
547    for (start, end) in spans {
548        match merged.last_mut() {
549            Some(last) if start <= last.1 => last.1 = last.1.max(end),
550            _ => merged.push((start, end)),
551        }
552    }
553    merged
554        .into_iter()
555        .map(|(start, end)| (start, end - start))
556        .collect()
557}
558
559impl PoolLedger {
560    /// Build a ledger seeded from the lane's persisted cumulative state (the
561    /// last voucher issued on earlier streams/invocations). Pass
562    /// `Cumulative::default()` for a brand-new lane, and for the unpaid legs —
563    /// the node's own-origin `BackendSource` quotes rate 0, so it never prices,
564    /// signs, or meters anything.
565    ///
566    /// The cumulative is the only seed there is. A chain draws its own secret at
567    /// open and is never resumed across a restart, so this constructor takes no
568    /// lane identity and no key material: what the ledger inherits from a
569    /// previous process is the money it owes, never the chain it owed it under.
570    #[must_use]
571    pub fn new(seed: Cumulative) -> Self {
572        Self {
573            issuance: Mutex::new(()),
574            pipeline: std::sync::Mutex::new(Pipeline {
575                committed: seed,
576                prev: None,
577                armed: None,
578                accrued: Cumulative::default(),
579                prev_accrued: Cumulative::default(),
580                last_proof: None,
581                generation: 0,
582                unsaved_rebase: None,
583                healed_from: None,
584                baseline: seed.bytes,
585            }),
586            epoch: std::sync::Mutex::new(None),
587            retired: std::sync::Mutex::new(Displaced::Nothing),
588            send_deadline: VOUCHER_SEND_DEADLINE,
589            unpaid: std::sync::Mutex::new(Vec::new()),
590        }
591    }
592
593    /// Record that this lane delivered `[start, end)` of `hash` and did not pay
594    /// for it. An empty span records nothing.
595    pub(crate) fn note_unpaid(&self, hash: [u8; 32], start: u64, end: u64) {
596        if start >= end {
597            return;
598        }
599        self.unpaid
600            .lock()
601            .unwrap_or_else(std::sync::PoisonError::into_inner)
602            .push((hash, start, end));
603    }
604
605    /// Take every span of `hash` this lane delivered and did not pay for, as
606    /// `(start, len)` in ascending order. Overlapping or adjacent spans merge.
607    pub(crate) fn take_unpaid(&self, hash: [u8; 32]) -> Vec<(u64, u64)> {
608        let mut spans: Vec<(u64, u64)> = Vec::new();
609        self.unpaid
610            .lock()
611            .unwrap_or_else(std::sync::PoisonError::into_inner)
612            .retain(|&(h, start, end)| {
613                if h == hash {
614                    spans.push((start, end));
615                    false
616                } else {
617                    true
618                }
619            });
620        merge_spans(spans)
621    }
622
623    #[cfg(test)]
624    /// Override the per-send voucher-send deadline. Used by tests to exercise
625    /// the timeout without waiting the production ceiling; production ledgers
626    /// keep `VOUCHER_SEND_DEADLINE`.
627    #[must_use]
628    pub(crate) const fn with_send_deadline(mut self, send_deadline: Duration) -> Self {
629        self.send_deadline = send_deadline;
630        self
631    }
632
633    /// Run one voucher send under [`PoolLedger::send_deadline`]. On timeout it
634    /// returns an error rather than blocking the issuance lock forever, so a
635    /// stalled upstream becomes a bounded per-blob failover instead of a
636    /// lane-wide wedge. The caller treats the error like any send failure: the
637    /// armed voucher is left in place and `committed` does not advance.
638    async fn under_deadline<Fut>(&self, send: Fut) -> anyhow::Result<()>
639    where
640        Fut: Future<Output = anyhow::Result<()>>,
641    {
642        match tokio::time::timeout(self.send_deadline, send).await {
643            Ok(result) => result,
644            Err(_) => Err(anyhow::anyhow!(
645                "voucher send exceeded {:?}; failing the lane to trigger failover",
646                self.send_deadline
647            )),
648        }
649    }
650
651    /// Lock the pipeline, recovering the inner value on poison. A poisoned lock
652    /// means a prior holder panicked mid-write; the guarded value is money, so
653    /// recover it rather than propagate — refusing to read here would throw away
654    /// the very watermark the mirror exists to save.
655    fn pipeline(&self) -> std::sync::MutexGuard<'_, Pipeline> {
656        self.pipeline
657            .lock()
658            .unwrap_or_else(std::sync::PoisonError::into_inner)
659    }
660
661    /// Read the committed cumulative WITHOUT awaiting — the drop-safe reader, and
662    /// the only one a `Drop` impl can call. It is the highest voucher whose send
663    /// succeeded (implicit acceptance) or that a terminal `StreamEnd` confirmed
664    /// (`confirm_armed_stamped`), plus the accrual since, so it is exactly what a
665    /// *completed* pull persists.
666    #[must_use]
667    pub fn committed(&self) -> Cumulative {
668        let pipeline = self.pipeline();
669        pipeline.committed.plus(pipeline.accrued)
670    }
671
672    /// The lane bytes this ledger paid for since its seed: how far it grew
673    /// the cumulative claim the node can redeem. Each voucher and hash-chain
674    /// reveal adds the bytes it pays for, and a rejection of that same proof
675    /// takes them back. Content delivered
676    /// twice is paid for twice and counts twice.
677    ///
678    /// A [`Self::reseed`] jump is payment this ledger did not make, so it
679    /// does not count. A rebase drops claims the node never accepted, so they
680    /// do not count either; claims below the seed that this ledger signs
681    /// again after a rebase do count.
682    ///
683    /// An armed voucher whose send did not confirm counts only once a later
684    /// voucher builds on it, a `StreamEnd` confirms it, or a retired chain's
685    /// fold commits it. [`Self::settlement`] counts it from the start.
686    #[must_use]
687    pub fn paid_bytes(&self) -> U256 {
688        let pipeline = self.pipeline();
689        pipeline
690            .committed
691            .plus(pipeline.accrued)
692            .bytes
693            .saturating_sub(pipeline.baseline)
694    }
695
696    /// The cumulative a cancelled pull must PERSIST — the drop-path counterpart of
697    /// [`Self::committed`], and what a `Drop` guard should actually write (#1122).
698    ///
699    /// It is [`Self::committed`], except when a voucher is still armed with its
700    /// send unconfirmed, in which case it is the HIGHER of the two by `amount`
701    /// (the monotone key). Settling high is the safe direction and it is correct:
702    /// an armed voucher pays for bytes the upstream ALREADY DELIVERED, and the
703    /// upstream persists a voucher before it would reject it, so an ambiguous
704    /// failure may leave the upstream holding it — under-reporting would strand
705    /// the deposit. A voucher the upstream explicitly REJECTED is never here —
706    /// `resolve_reject` clears `armed` and rewinds `committed` — so this
707    /// cannot inflate our cumulative for bytes the upstream declined.
708    #[must_use]
709    pub fn settlement(&self) -> Cumulative {
710        self.pipeline().settlement()
711    }
712
713    /// Issue one voucher for `delta_bytes` newly delivered since the last voucher
714    /// and SEND it. The send IS the commit: on a successful send the committed
715    /// watermark advances to the new voucher (continued delivery is acceptance,
716    /// ADR 005), and there is no ack to wait for.
717    ///
718    /// Holds the issuance lock across compute → arm → `exchange`, so concurrent
719    /// issuers on the shared ledger sign strictly increasing cumulatives. The
720    /// voucher is ARMED (recorded as the in-flight candidate) *before* `exchange`,
721    /// so a future dropped inside the send still has [`Self::settlement`] report
722    /// what we owe. An `exchange` error leaves the voucher armed (its fate is
723    /// ambiguous: a partial write may have reached the upstream), so `settlement`
724    /// settles high while `committed` does not advance.
725    ///
726    /// `exchange` receives the next [`Cumulative`] (the values to sign and send);
727    /// the caller owns signing + framing so this module stays free of EIP-712 /
728    /// wire types.
729    #[doc(hidden)]
730    pub async fn issue<F, Fut>(
731        &self,
732        delta_bytes: u64,
733        rate_per_mb: u64,
734        epoch: EpochAction,
735        exchange: F,
736    ) -> anyhow::Result<Cumulative>
737    where
738        F: FnOnce(Cumulative, ChainCommit) -> Fut,
739        Fut: Future<Output = anyhow::Result<()>>,
740    {
741        self.issue_stamped(delta_bytes, rate_per_mb, epoch, exchange)
742            .await
743            .map(|(next, _generation)| next)
744    }
745
746    /// [`Self::issue`], also returning the ledger generation the voucher was
747    /// signed under. Read under the issuance lock, which [`Self::rebase`] also
748    /// takes, so the generation cannot move between signing and reporting it.
749    ///
750    /// # Errors
751    ///
752    /// As [`Self::issue`].
753    pub(crate) async fn issue_stamped<F, Fut>(
754        &self,
755        delta_bytes: u64,
756        rate_per_mb: u64,
757        epoch: EpochAction,
758        exchange: F,
759    ) -> anyhow::Result<(Cumulative, u64)>
760    where
761        F: FnOnce(Cumulative, ChainCommit) -> Fut,
762        Fut: Future<Output = anyhow::Result<()>>,
763    {
764        // Serialize issuance. Held across the send below.
765        let _issuing = self.issuance.lock().await;
766
767        // Read the accrual and the frontier this voucher builds on under one
768        // pipeline lock, so the basis cannot shift between reading it and
769        // recording it. Build on the SETTLEMENT frontier, not the anchor alone:
770        // a voucher already on the wire must be exceeded, or we re-sign a
771        // cumulative the upstream may hold.
772        let (frontier, accrued) = {
773            let pipeline = self.pipeline();
774            (pipeline.settlement(), pipeline.accrued)
775        };
776
777        // **A voucher that folds must also roll.** Signing the accrued frontier
778        // into `amount` is what retires the chain that proved it, so the voucher
779        // MUST carry a fresh root — otherwise the node keeps the old root's
780        // frontier alongside an amount that already folded it in, and the same
781        // chunks are counted twice (ADR 003 §Rollover: the fold and the fresh
782        // root are one step, not two).
783        //
784        // A voucher with nothing accrued folds nothing, so it commits to
785        // whatever the caller asked for: an opening anchor, a free re-anchor
786        // that re-asserts a root the node already holds, or a sealed close.
787        let epoch = if accrued.amount.is_zero() && accrued.bytes.is_zero() {
788            epoch
789        } else {
790            EpochAction::Roll
791        };
792        let next = next_voucher(&frontier, delta_bytes, rate_per_mb);
793        let commit = self.commit_epoch(epoch, rate_per_mb);
794        self.pipeline().armed = Some(Armed {
795            voucher: next,
796            folded: accrued,
797        });
798        // Send. On ANY error the voucher stays armed and the anchor does not
799        // advance: a send failure is as ambiguous as a drop, so `settlement`
800        // settles high. A rejection is NOT an issuance outcome — it arrives later
801        // as a `StreamError` message and is disarmed via `resolve_reject`.
802        self.under_deadline(exchange(next, commit)).await?;
803        // The send succeeded: commit optimistically. The accrual is now folded
804        // into the signed anchor, so it resets to zero — and the previous pair
805        // is remembered so a later `resolve_reject` can un-commit exactly this
806        // voucher, both halves together.
807        let generation = {
808            let mut pipeline = self.pipeline();
809            pipeline.prev = Some(pipeline.committed);
810            pipeline.prev_accrued = pipeline.accrued;
811            pipeline.committed = next;
812            pipeline.accrued = Cumulative::default();
813            pipeline.armed = None;
814            pipeline.last_proof = Some(LastProof::Voucher {
815                amount: next.amount,
816            });
817            pipeline.generation
818        };
819        Ok((next, generation))
820    }
821
822    #[cfg(test)]
823    /// [`Self::confirm_armed_stamped`] without the generation.
824    pub(crate) async fn confirm_armed(&self, amount: U256) -> Option<Cumulative> {
825        self.confirm_armed_stamped(amount)
826            .await
827            .map(|(confirmed, _generation)| confirmed)
828    }
829
830    /// Commit the armed voucher signing `amount` as if its send had confirmed.
831    ///
832    /// For when the upstream's terminal `StreamEnd` arrives after this stream's
833    /// voucher write failed. An honest node ends a stream only once every interval
834    /// — the closing voucher included — is credited, so its `StreamEnd` implies it
835    /// holds the voucher. Trusting a dishonest one costs nothing:
836    /// [`Self::settlement`] already reports the armed voucher. The epoch already
837    /// rolled when the voucher was armed, so this is the commit a successful
838    /// [`Self::issue`] makes, and payment-based completion sees the leg paid
839    /// through its end instead of re-pulling and re-billing the tail.
840    ///
841    /// Two things differ from the in-`issue` commit, because the issuance lock
842    /// was released between the failed send and this call:
843    ///
844    /// - **Only the fold comes out of `accrued`.** A sibling stream on the shared
845    ///   lane may have released reveals on the new chain since the voucher was
846    ///   armed. The voucher never signed them, so they stay accrued.
847    /// - **Only the named voucher commits.** `armed` belongs to the lane, not the
848    ///   stream: a sibling's failed voucher may have been armed on top of this
849    ///   one. A `StreamEnd` read on this stream says nothing about that voucher.
850    ///
851    /// Returns the confirmed cumulative and the ledger generation the voucher was
852    /// signed under, or `None` when the armed voucher does not sign `amount`: a later issue, reject, or reseed has already cleared it, or
853    /// a sibling's voucher replaced it. The ledger is then left untouched, and
854    /// [`Self::settlement`] still settles high on whatever is armed.
855    ///
856    /// A [`Self::rebase`] clears the armed voucher, so one that is still armed
857    /// here was signed under the current generation.
858    pub(crate) async fn confirm_armed_stamped(&self, amount: U256) -> Option<(Cumulative, u64)> {
859        let _issuing = self.issuance.lock().await;
860        let mut pipeline = self.pipeline();
861        let armed = pipeline
862            .armed
863            .filter(|armed| armed.voucher.amount == amount)?;
864        pipeline.armed = None;
865        pipeline.prev = Some(pipeline.committed);
866        pipeline.prev_accrued = armed.folded;
867        pipeline.committed = armed.voucher;
868        pipeline.accrued = pipeline.accrued.minus(armed.folded);
869        pipeline.last_proof = Some(LastProof::Voucher { amount });
870        Some((armed.voucher, pipeline.generation))
871    }
872
873    /// Re-state the lane's signed anchor under the live root, so a stream that
874    /// has not carried a voucher for this chain can place its reveals (ADR 003
875    /// §Concurrent Streams, Rule 1).
876    ///
877    /// This is deliberately NOT [`Self::issue`] with a zero delta. Every voucher
878    /// the ledger issues builds on the settlement frontier, which folds whatever
879    /// the chain has accrued — and a voucher that folds must also roll. A
880    /// zero-delta re-anchor issued that way would therefore retire the very chain
881    /// it meant to join: it strands every sibling stream's in-flight reveals under
882    /// the retired root, where they credit nothing, and it buys a fresh signature
883    /// plus a 256-keccak ladder for a message whose whole job is to be free.
884    ///
885    /// So this re-states `committed` exactly — the anchor the node already holds —
886    /// and folds nothing. The node reads it as already-satisfied, takes no new
887    /// money from it, and anchors the receiving stream to the root it names.
888    /// Nothing is armed, no watermark moves, and `last_proof` is untouched: the
889    /// voucher claims nothing, so there is nothing a later rejection could rewind
890    /// and nothing an ambiguous send could strand.
891    ///
892    /// Returns the root the stream is now anchored to, or `None` when the lane
893    /// meters no chain — there is nothing to anchor to, and nothing is sent.
894    ///
895    /// Holds the issuance lock across read → send, so the root it states is the
896    /// root that was live when it was sent: a sibling rolling concurrently either
897    /// precedes this voucher entirely or follows it.
898    pub(crate) async fn reanchor<F, Fut>(&self, exchange: F) -> anyhow::Result<Option<B256>>
899    where
900        F: FnOnce(Cumulative, ChainCommit) -> Fut,
901        Fut: Future<Output = anyhow::Result<()>>,
902    {
903        let _issuing = self.issuance.lock().await;
904        let Some(commit) = self.epoch().as_ref().map(|live| ChainCommit {
905            chain_root: live.root(),
906            chunk_price: live.chunk_price,
907        }) else {
908            return Ok(None);
909        };
910        let anchor = self.pipeline().committed;
911        self.under_deadline(exchange(anchor, commit)).await?;
912        Ok(Some(commit.chain_root))
913    }
914
915    /// Apply an [`EpochAction`] and report what the resulting voucher commits.
916    ///
917    /// Callers hold the issuance lock, which is what makes "read the epoch,
918    /// maybe replace it, report it" one indivisible step against the concurrent
919    /// streams sharing this lane.
920    fn commit_epoch(&self, action: EpochAction, rate_per_mb: u64) -> ChainCommit {
921        let mut slot = self.epoch();
922        // What this voucher puts in the chain slot, or `None` to leave it alone.
923        // A lane holding no chain reports `ChainCommit::SEALED` either way, which
924        // is what makes the sealed shape something the ledger arrives at rather
925        // than something a caller asks for.
926        let replacement = match action {
927            EpochAction::Keep => None,
928            EpochAction::Open if slot.is_some() => None,
929            EpochAction::Open | EpochAction::Roll => Some(Some(open_epoch(rate_per_mb))),
930        };
931        // Record what this voucher displaced — including "nothing", so a rejected
932        // voucher that left the chain alone does not restore some earlier one.
933        *self.retired() = match replacement {
934            Some(next) => Displaced::Replaced(std::mem::replace(&mut *slot, next)),
935            None => Displaced::Nothing,
936        };
937        slot.as_ref()
938            .map_or(ChainCommit::SEALED, |live| ChainCommit {
939                chain_root: live.root(),
940                chunk_price: live.chunk_price,
941            })
942    }
943
944    /// Lock the epoch slot, recovering the inner value on poison — same reason
945    /// as [`Self::pipeline`].
946    fn epoch(&self) -> std::sync::MutexGuard<'_, Option<ChainEpoch>> {
947        self.epoch
948            .lock()
949            .unwrap_or_else(std::sync::PoisonError::into_inner)
950    }
951
952    /// Lock the displaced-chain slot — same poison recovery as [`Self::epoch`],
953    /// and always taken after it.
954    fn retired(&self) -> std::sync::MutexGuard<'_, Displaced> {
955        self.retired
956            .lock()
957            .unwrap_or_else(std::sync::PoisonError::into_inner)
958    }
959
960    /// The root of the live chain, or `None` on a lane metering nothing. A stream
961    /// compares this against the chain it last anchored itself to.
962    #[must_use]
963    #[doc(hidden)]
964    pub fn chain_root(&self) -> Option<B256> {
965        self.epoch().as_ref().map(ChainEpoch::root)
966    }
967
968    /// Meter one delivered chunk: release the next preimage and SEND it.
969    ///
970    /// This is the tick the whole design exists for — one keccak-ladder read,
971    /// one 33-byte message, no signature, no acknowledgement, and nothing
972    /// durable to write before the node sends the next chunk. The released value
973    /// is self-proving: nobody derives a deeper preimage from a shallower one
974    /// without the seed, so it IS the receipt for every chunk below it.
975    ///
976    /// Released only AFTER the payer has received and verified the chunk it pays
977    /// for, which is what keeps the payer's exposure at zero.
978    ///
979    /// Holds the issuance lock across compute → send, exactly as [`Self::issue`]
980    /// does, so concurrent streams on one lane release strictly deepening
981    /// indices and never two values at the same depth.
982    ///
983    /// `anchored` is the root the calling stream last carried a voucher for.
984    /// The node places a reveal against that root, so a reveal from any other
985    /// chain folds nothing there. The check runs under the issuance lock, which
986    /// is the only place the live root cannot move: a sibling can roll the lane
987    /// between the stream's anchor and this tick. Returns [`Metered::Moved`]
988    /// **without sending anything** when the live root differs.
989    ///
990    /// Returns [`Metered::Exhausted`] **without sending anything** when the
991    /// epoch has no index left. The caller then signs a rollover voucher
992    /// ([`EpochAction::Roll`]) — whose amount already folds this chain's
993    /// frontier, since every release advanced the committed cumulative — and
994    /// meters again.
995    ///
996    /// # Errors
997    ///
998    /// Propagates an `exchange` failure. The index is NOT consumed on a failed
999    /// send: an unreleased preimage proves nothing, so re-releasing the same
1000    /// index later is both safe and correct.
1001    #[doc(hidden)]
1002    pub async fn meter<F, Fut>(
1003        &self,
1004        anchored: Option<B256>,
1005        exchange: F,
1006    ) -> anyhow::Result<Metered>
1007    where
1008        F: FnOnce(Released) -> Fut,
1009        Fut: Future<Output = anyhow::Result<()>>,
1010    {
1011        let _issuing = self.issuance.lock().await;
1012        let (released, price) = {
1013            let slot = self.epoch();
1014            let Some(live) = slot.as_ref() else {
1015                return Ok(Metered::Exhausted);
1016            };
1017            if anchored != Some(live.root()) {
1018                return Ok(Metered::Moved);
1019            }
1020            let Some(index) = live.next_index() else {
1021                return Ok(Metered::Exhausted);
1022            };
1023            let Some(preimage) = live.preimages.get(usize::from(index)).copied() else {
1024                return Ok(Metered::Exhausted);
1025            };
1026            (
1027                Released {
1028                    preimage,
1029                    index,
1030                    chain_root: live.root(),
1031                },
1032                live.chunk_price,
1033            )
1034        };
1035
1036        self.under_deadline(exchange(released)).await?;
1037
1038        // The reveal is out. Advance the lane's worth by exactly one chunk on
1039        // both axes — the node credits the same, because it derives both from
1040        // the same two protocol constants rather than from anything on the wire.
1041        {
1042            let mut slot = self.epoch();
1043            if let Some(live) = slot.as_mut() {
1044                live.released = released.index;
1045            }
1046        }
1047        {
1048            let mut pipeline = self.pipeline();
1049            pipeline.accrued.amount = pipeline.accrued.amount.saturating_add(price);
1050            pipeline.accrued.bytes = pipeline
1051                .accrued
1052                .bytes
1053                .saturating_add(U256::from(CHUNK_BYTES));
1054            pipeline.last_proof = Some(LastProof::Reveal {
1055                chain_root: released.chain_root,
1056                index: released.index,
1057                chunk_price: price,
1058            });
1059        }
1060        Ok(Metered::Released(released))
1061    }
1062
1063    /// Wallet-less self-heal (issue #1481): overwrite the committed watermark to `cum` — typically
1064    /// [`Cumulative::from`] a [`WatermarkBundle`] the node attached to a watermark-gated rejection
1065    /// — and clear the rewind + armed state, since the node has just told us its authoritative
1066    /// watermark. The next [`Self::issue`] builds on `cum`, matching what the node will accept
1067    /// next.
1068    ///
1069    /// This is a hard overwrite, not a monotonic bump: the caller's prior local
1070    /// state was wrong (a wallet-less client has no reliable on-chain source for
1071    /// its watermark until settlement), so the bundle — signer-verified by the
1072    /// node before it was sent, and re-verified against the client's own key by
1073    /// `resumable_watermark` before it reaches here — is authoritative. Callers
1074    /// MUST only pass a cumulative sourced from such a bundle.
1075    ///
1076    /// Returns `false` — leaving the ledger untouched — if `cum` does not ADVANCE past
1077    /// [`Self::committed`]'s `amount` (the anchor plus the accrual). Reseeding heals a watermark
1078    /// that has fallen BEHIND what the node holds; a bundle at or behind `committed` proves
1079    /// nothing, and applying it would REGRESS the watermark and re-sign a spent amount. Guarded
1080    /// here rather than only at the call sites because monotonicity is the ledger's invariant to
1081    /// keep.
1082    #[must_use]
1083    #[doc(hidden)]
1084    pub fn reseed(&self, cum: Cumulative) -> bool {
1085        {
1086            let mut pipeline = self.pipeline();
1087            if cum.amount <= pipeline.committed.plus(pipeline.accrued).amount {
1088                return false;
1089            }
1090            pipeline.heal(cum);
1091        }
1092        self.retire_chain();
1093        true
1094    }
1095
1096    /// Move the committed watermark DOWN to `cum` — the node's last-accepted
1097    /// watermark from a [`WatermarkBundle`] on an `Underpaid` rejection.
1098    ///
1099    /// An `Underpaid` rejection says this ledger has run AHEAD of the node: it
1100    /// holds vouchers the node never accepted (a send that committed
1101    /// optimistically, then persisted across a restart), so every voucher it
1102    /// signs measures a span the node sees as short, and the lane wedges.
1103    /// Rebasing to the node's watermark is the only way out.
1104    ///
1105    /// Moving down is safe for the payer. The bundle is the node's accepted
1106    /// state, and the caller admits it only after `resumable_watermark` proves
1107    /// the anchor against our own signature. Redemption is cumulative and pays
1108    /// the highest voucher it is shown, so re-signing from a lower anchor can
1109    /// never make the payer pay more than it has already signed for.
1110    ///
1111    /// `proof_generation` is the generation the rejected voucher was signed
1112    /// under. A voucher signed before the latest rebase draws a stale `Underpaid`
1113    /// — it measured its span from the anchor this ledger has already left — so
1114    /// that rejection is [`Rebase::Stale`] and moves nothing. Rebasing on it
1115    /// would re-sign amounts the node has since accepted from the healed anchor,
1116    /// with different bytes, which the node refuses as a `BytesRegression`. A
1117    /// rejection of a voucher from the current generation
1118    /// is a fresh divergence, and rebases again. `None` means the generation is
1119    /// unknown, and counts as current.
1120    ///
1121    /// Holds the issuance lock, so no voucher is mid-send across the rebase: a
1122    /// send in flight completes first, and nothing signed from the old anchor
1123    /// can commit over the new one.
1124    ///
1125    /// Returns [`Rebase::Refused`] — leaving the ledger untouched — when `cum`
1126    /// is ahead of the committed watermark's `amount` (that is
1127    /// [`Self::reseed`]'s case) or equal to the committed watermark (an echo).
1128    /// A `cum` equal on `amount` but behind on `bytes` rebases.
1129    #[doc(hidden)]
1130    pub async fn rebase(&self, cum: Cumulative, proof_generation: Option<u64>) -> Rebase {
1131        // A watermark ahead on `amount` is `reseed`'s case, and one equal to
1132        // ours is an echo that proves nothing. Anything else is the node's
1133        // accepted state behind ours — including one equal on `amount` but
1134        // behind on `bytes`, which still makes every span we sign underpay.
1135        self.rebase_if(cum, proof_generation, |from| {
1136            cum.amount <= from.amount && cum != from
1137        })
1138        .await
1139    }
1140
1141    /// Move the committed watermark to `cum` — the node's last-accepted
1142    /// watermark from a [`WatermarkBundle`] on a `BytesRegression` — when
1143    /// `cum` is at or behind the ledger on `amount` and ahead of it on
1144    /// `bytes`.
1145    ///
1146    /// That shape says the lane resumed from a lower anchor than the node
1147    /// holds, and priced its spans from there: its `amount` passed the node's
1148    /// anchor while its bytes still trailed it. The rebase is safe for the
1149    /// payer for the reason [`Self::rebase`] gives: `cum` is a voucher the
1150    /// payer signed.
1151    ///
1152    /// The bytes test runs under the issuance lock, beside the generation
1153    /// test, so a sibling voucher that commits past `cum` while this call
1154    /// waits makes it [`Rebase::Refused`] instead of moving the ledger below
1155    /// a voucher the node accepted.
1156    pub(crate) async fn rebase_ahead_on_bytes(
1157        &self,
1158        cum: Cumulative,
1159        proof_generation: Option<u64>,
1160    ) -> Rebase {
1161        self.rebase_if(cum, proof_generation, |from| {
1162            cum.amount <= from.amount && cum.bytes > from.bytes
1163        })
1164        .await
1165    }
1166
1167    /// The shared body of [`Self::rebase`] and [`Self::rebase_ahead_on_bytes`]:
1168    /// under the issuance lock, refuse a stale generation, then move to `cum`
1169    /// when `admits` accepts the committed-plus-accrued watermark it would
1170    /// leave.
1171    async fn rebase_if(
1172        &self,
1173        cum: Cumulative,
1174        proof_generation: Option<u64>,
1175        admits: impl FnOnce(Cumulative) -> bool,
1176    ) -> Rebase {
1177        let _issuing = self.issuance.lock().await;
1178        let from = {
1179            let mut pipeline = self.pipeline();
1180            if proof_generation.is_some_and(|g| g < pipeline.generation) {
1181                return Rebase::Stale;
1182            }
1183            let from = pipeline.committed.plus(pipeline.accrued);
1184            if !admits(from) {
1185                return Rebase::Refused;
1186            }
1187            pipeline.heal(cum);
1188            pipeline.generation = pipeline.generation.saturating_add(1);
1189            pipeline.unsaved_rebase = Some(cum);
1190            from
1191        };
1192        self.retire_chain();
1193        Rebase::Rebased { from }
1194    }
1195
1196    /// The generation vouchers are currently signed under: how many times
1197    /// a rebase has moved this ledger to a node's watermark.
1198    #[must_use]
1199    #[doc(hidden)]
1200    pub fn generation(&self) -> u64 {
1201        self.pipeline().generation
1202    }
1203
1204    /// Take the watermark the latest rebase moved to, if no
1205    /// persist has recorded it yet. The caller overwrites the lane record with it
1206    /// once and advances from there; every later persist is a monotone advance
1207    /// again. Taking it clears it, so a second persist sees `None`.
1208    #[must_use]
1209    pub fn take_unsaved_rebase(&self) -> Option<Cumulative> {
1210        self.pipeline().unsaved_rebase.take()
1211    }
1212
1213    /// Retire a live chain the node refused to adopt, after an `UnderFold`
1214    /// rejection whose `bundle` our ledger already covers on both axes. Returns
1215    /// the retired root, or `None` when nothing moved.
1216    ///
1217    /// A payer that resumes below the node's live claim opens a fresh root, and
1218    /// the node refuses it `UnderFold`. The rejection lands after the stream has
1219    /// released reveals under that root, and it rewinds only the last one. So
1220    /// the ledger can cover the bundle and still meter the refused root. A
1221    /// reseed does not apply, because the bundle does not advance the ledger. A
1222    /// retry would re-anchor under the same root and draw the same rejection.
1223    ///
1224    /// This folds the pipeline to [`Self::settlement`] and drops the chain, so
1225    /// the next stream opens a fresh root over the whole fold. A new process
1226    /// does the same thing: it starts from the persisted settlement with no
1227    /// chain. The node adopts that voucher, because it folds at least the
1228    /// bundle's claim. The fold leaves [`Self::settlement`] where it was, so
1229    /// the persisted watermark advances as usual and needs no
1230    /// [`Self::take_unsaved_rebase`].
1231    ///
1232    /// Nothing moves when a heal has already taken this bundle or a later one.
1233    /// A sibling stream that took the same rejection first has healed the lane,
1234    /// and the live chain is then one the healed lane opened since. Nothing
1235    /// moves on a lane that meters no chain either.
1236    ///
1237    /// A chain retired this way may be one the node did adopt. That costs one
1238    /// more opening voucher and is otherwise safe. The fold is the frontier the
1239    /// payer reached, so it covers every reveal the node can hold under the
1240    /// retired root. A sibling anchored to that root opens a fresh one before
1241    /// its next reveal, as after a reseed.
1242    ///
1243    /// Holds the issuance lock, so no proof is mid-send across the fold.
1244    pub(crate) async fn retire_unadopted_chain(&self, bundle: Cumulative) -> Option<B256> {
1245        let _issuing = self.issuance.lock().await;
1246        let root = self.chain_root()?;
1247        {
1248            let mut pipeline = self.pipeline();
1249            if pipeline
1250                .healed_from
1251                .is_some_and(|h| h.amount >= bundle.amount && h.bytes >= bundle.bytes)
1252            {
1253                return None;
1254            }
1255            let settlement = pipeline.settlement();
1256            pipeline.overwrite(settlement);
1257            pipeline.healed_from = Some(bundle);
1258        }
1259        self.retire_chain();
1260        Some(root)
1261    }
1262
1263    /// Drop the live chain after an overwrite of the committed watermark.
1264    fn retire_chain(&self) {
1265        // Retire the live chain, for the same reason a folding voucher must roll:
1266        // `cum` came from a bundle and therefore already folds that bundle's
1267        // `verified_index` into its amount. Keeping the epoch would leave the next
1268        // voucher re-committing a root whose frontier the new anchor has absorbed,
1269        // and the node would count those chunks twice — once in the amount, once
1270        // again under the root it never retired. The next `EpochAction::Open`
1271        // draws a fresh chain against the healed anchor.
1272        //
1273        // Sequenced after the pipeline borrow ends rather than nested inside it:
1274        // the two guards are independent and this keeps them from ever being held
1275        // together, which is what makes the no-deadlock argument in the field docs
1276        // hold without reasoning about order.
1277        *self.epoch() = None;
1278        *self.retired() = Displaced::Nothing;
1279    }
1280
1281    /// Resolve `rejected` — the proof this stream just read a `VoucherRejected`
1282    /// for — as explicitly refused, undoing exactly what that proof claimed.
1283    /// Called by the receive loop. A rejection is the upstream declaring it never
1284    /// took the proof, so — unlike an ambiguous failure — it must not be settled
1285    /// optimistically (that would inflate our cumulative for bytes the upstream
1286    /// refused to be paid for).
1287    ///
1288    /// Returns `false` if there was nothing to rewind: a spurious rejection, or
1289    /// one for a proof the lane has already moved past.
1290    ///
1291    /// # Why the caller names the proof
1292    ///
1293    /// The wire rejection names nothing, and a lane emits two kinds of proof that
1294    /// rewind differently. A refused VOUCHER un-commits the anchor. A refused
1295    /// REVEAL must not: the anchor it extends was accepted, and the last voucher
1296    /// may be many chunks back. Rewinding `committed → prev` on a refused reveal
1297    /// un-commits a voucher the node is still holding, and the payer's anchor then
1298    /// sits permanently below the node's — every voucher it signs afterwards
1299    /// regresses, and a re-anchor states a cumulative the node passed long ago.
1300    ///
1301    /// Reading the KIND off the lane is not enough, because the lane is shared.
1302    /// A rejection is read on the stream that earned it, and a sibling stream
1303    /// issuing in the meantime replaces what the lane last did: a reveal refused
1304    /// on stream A, arriving after stream B's rollover was accepted, would find a
1305    /// Voucher in the slot and un-commit B's ACCEPTED anchor — the exact
1306    /// permanent divergence the kind-split exists to prevent. So the stream hands
1307    /// back the proof it sent, by name, and the rewind happens only while that
1308    /// proof is still the last thing the lane did.
1309    ///
1310    /// # Why a stale rejection rewinds nothing
1311    ///
1312    /// It is not merely the safe answer, it is the right one. For the lane to
1313    /// have moved on, a later proof must have been ACCEPTED — the node keeps
1314    /// delivering, and continued delivery is acceptance. A later voucher folded
1315    /// the refused reveal's accrual into a signed amount the node took, and a
1316    /// later reveal proved a depth that pays for every chunk below it. Either
1317    /// way the money the rejection would claw back is money the node has since
1318    /// been granted by a proof it did not refuse. Taking it back would put the
1319    /// payer's anchor below the node's, which is the failure this whole method
1320    /// is shaped to avoid.
1321    ///
1322    /// A rejected reveal gives back exactly one chunk of accrual AND the index it
1323    /// released. Both, because the index is the accounting: a claim is
1324    /// `anchor + index × chunk_price`, so a payer that gave back the money but let
1325    /// the next reveal go one deeper would be charged for the depth it skipped —
1326    /// the node prices the gap, the payer never accrued it, and the two diverge by
1327    /// exactly one chunk from then on. Re-releasing the rewound depth is safe: the
1328    /// node refused it, so it holds no preimage at that index.
1329    pub(crate) fn resolve_reject(&self, rejected: StreamProof) -> bool {
1330        let mut pipeline = self.pipeline();
1331        match (rejected, pipeline.last_proof) {
1332            (
1333                StreamProof::Reveal { chain_root, index },
1334                Some(LastProof::Reveal {
1335                    chain_root: last_root,
1336                    index: last_index,
1337                    chunk_price,
1338                }),
1339            ) if chain_root == last_root && index == last_index => {
1340                pipeline.last_proof = None;
1341                pipeline.accrued.amount = pipeline.accrued.amount.saturating_sub(chunk_price);
1342                pipeline.accrued.bytes = pipeline
1343                    .accrued
1344                    .bytes
1345                    .saturating_sub(U256::from(CHUNK_BYTES));
1346                drop(pipeline);
1347                // Give the index back too — see the doc above. Sequenced after the
1348                // pipeline guard is released so the two locks are never held
1349                // together, as in `reseed`. Guarded on the root as well: a sibling
1350                // that rolled between the release and this rewind left a different
1351                // chain in the slot, whose index this reveal never advanced.
1352                if let Some(live) = self.epoch().as_mut()
1353                    && live.root() == chain_root
1354                {
1355                    live.released = live.released.saturating_sub(1);
1356                }
1357                true
1358            }
1359            (
1360                StreamProof::Voucher { amount, .. },
1361                Some(LastProof::Voucher {
1362                    amount: last_amount,
1363                }),
1364            ) if amount == last_amount => {
1365                pipeline.last_proof = None;
1366                // The voucher went out and committed, so it is not also armed —
1367                // a successful `issue` disarms. Clear it only if this rejection
1368                // names the armed voucher instead, which is the ambiguous send
1369                // the upstream has now explicitly refused.
1370                let Some(prev) = pipeline.prev.take() else {
1371                    return false;
1372                };
1373                // Rewind BOTH halves. The rejected voucher folded whatever had
1374                // accrued at the time it was signed, so restoring the anchor
1375                // without giving that accrual back would silently forget chunks
1376                // the node has already been shown preimages for.
1377                //
1378                // The fold comes back ON TOP of whatever has accrued SINCE —
1379                // reveals released after that signature are still released, and
1380                // a released preimage cannot be taken back. Overwriting with
1381                // `prev_accrued` alone would drop them, and the payer would
1382                // then believe it owes less than the node can already redeem.
1383                pipeline.committed = prev;
1384                pipeline.accrued = pipeline
1385                    .accrued
1386                    .plus(std::mem::take(&mut pipeline.prev_accrued));
1387                drop(pipeline);
1388                // Put back the chain this voucher displaced. A rejected
1389                // rollover already swapped the chain in — the voucher had to
1390                // carry the new root to be signed — so leaving it would have
1391                // the payer metering a chain the node refused, whose reveals
1392                // fold nothing on a lane still tracking the old root.
1393                let mut epoch = self.epoch();
1394                if let Displaced::Replaced(chain) =
1395                    std::mem::replace(&mut *self.retired(), Displaced::Nothing)
1396                {
1397                    *epoch = chain;
1398                }
1399                true
1400            }
1401            // The refused voucher never committed: its send was ambiguous, so the
1402            // anchor never advanced and disarming IS the whole rewind.
1403            (StreamProof::Voucher { amount, .. }, _)
1404                if pipeline
1405                    .armed
1406                    .is_some_and(|armed| armed.voucher.amount == amount) =>
1407            {
1408                pipeline.armed = None;
1409                true
1410            }
1411            _ => false,
1412        }
1413    }
1414}
1415
1416/// Draw the next chain, priced at the node's quoted rate.
1417///
1418/// Each chain gets its own 32-byte secret from the OS, so no two chains this
1419/// payer opens — across providers, pools, its own sibling signers, or the
1420/// successive chains of one lane — share a root. That is the whole defence
1421/// against the cross-lane preimage spend, and it is payer-side by design: reuse
1422/// costs the payer and pays the node, so no node-side rule would protect anyone
1423/// who chose to run without it (ADR 003 §One chain per lane).
1424fn open_epoch(rate_per_mb: u64) -> ChainEpoch {
1425    ChainEpoch::open(U256::from(rate_per_mb))
1426}
1427
1428#[cfg(test)]
1429mod tests;