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