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