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;