1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
//! Streaming-decode support types for the paid pull path (#1120, ADR 038).
//!
//! [`PullReader`] adapts a live [`UpstreamPull`] to the bao decoder's
//! byte-reader trait while [`StashedFault`] preserves the pull's typed faults
//! (stall, refusal, voucher rejection) so a misbehaving peer stays scoreable —
//! the decoder itself can only say "the bytes stopped arriving". The streaming
//! decode loops that consume these are
//! [`crate::ranged_store::ClientRangedStore::ingest_stream`], the node's cache
//! admit, and (`test-util`) the in-memory `stream_fetch*` wrappers.
//!
//! [`content_paid_frontier`] maps a paid WIRE-byte watermark back to the
//! content-byte frontier it covers, for the resume-at-paid-frontier gate.
//!
//! [`BlobCache`] is the injected `(hash, range)` cache the
//! [`Streamer`](crate::Streamer) consults and fills. It is pure — it names no
//! blob store — which is what keeps `decdn-client` `iroh-blobs`-free; the
//! default cache is the no-op [`NoCache`].
use DecodeError;
use ;
use ;
use crate::;
/// An [`iroh_io::AsyncStreamReader`] over a live [`UpstreamPull`], so the bao
/// decoder can pull wire bytes on demand instead of being handed a complete
/// buffer.
///
/// A reader never asks past the leg's wire end. The node sends its
/// `StreamEnd` only after the closing voucher, and that voucher goes out in
/// [`UpstreamPull::finish`] once the decode is done, so a read past the end
/// waits on a node that waits on the payer. The bao decoder reads exactly the
/// leg's items and no further.
///
/// # Why the error is stashed rather than propagated
///
/// The reader trait speaks `io::Error`, but `next_chunk` produces the typed
/// errors the reputation layer classifies on (`PullStalled`, `PullTimeout`,
/// `UpstreamRefused`, `UpstreamVoucherRejected`). Collapsing those into an
/// `io::Error` would silently downgrade, say, a peer's mid-stream refusal into
/// an anonymous decode failure and stop it being scored. So the original
/// `anyhow::Error` is parked in `fault` and the trait returns a placeholder; the
/// decode loop that owns the reader
/// ([`crate::ranged_store::ClientRangedStore::ingest_stream`], or the `test-util`
/// in-memory decoder) checks `fault` first and returns the real error, using the
/// decoder's complaint only when there is no parked fault.
/// A reader that may be holding back a richer error than the one the bao decoder
/// will report.
///
/// The decoder can only say "the bytes stopped arriving". When the reader knows
/// WHY — a stalled peer, a mid-stream refusal, a rejected voucher — that reason
/// is what the caller needs in order to score the peer, so it wins.
///
/// Public because it is a supertrait of [`crate::source::BaoRangeReader`]: a
/// [`crate::source::BlobSource`]'s reader must preserve the pull's typed faults
/// so the gap-driven driver (#1608) surfaces them for scoring.
/// A plain in-memory wire buffer has no out-of-band failure mode: whatever the
/// decoder says about it is the whole story.
///
/// Test-only, and gated to say so: it exists purely so the streaming ingest
/// loop can be driven from a fixed wire buffer. A test using it exercises the
/// `None` branch, where the decoder's error is the whole story, so it must not
/// be the only coverage of the fault-precedence rule.
/// Split a bao decode failure into "the peer lied" and "the stream ended early",
/// the taxonomy every decoding consumer and the node-side tee share (ADR 038).
/// Only a hash mismatch is provably corruption; a truncated stream is
/// transport-class and must not tar the peer as a liar.
///
/// `pub(crate)`: [`crate::ranged_store::ClientRangedStore::ingest_stream`] and the
/// in-memory `stream_fetch*` decoder reuse this exact classification rather than
/// duplicating the match.
pub
/// Map a **paid wire-byte watermark** back to the **content-byte frontier** it
/// covers, for a stream that began at content offset `fetch_start` of a
/// `total_bytes`-byte blob (#1497).
///
/// Vouchers pay for **wire** bytes — bao-encoded content PLUS interleaved proof
/// (ADR 038 §Payment metering) — so `paid_wire` (the lane-cumulative bytes an
/// ACCEPTED voucher covered, minus this stream's baseline) is a WIRE quantity.
/// Resuming at `fetch_start + paid_wire` would treat it as a CONTENT offset and,
/// since wire ≥ content, overshoot the true content paid-frontier: the content
/// span between the real frontier and that overshoot would never be billed on the
/// resumed leg (a systematic under-pay), and a large overshoot can even exceed the
/// on-disk content length. This maps `paid_wire` back into content space instead.
///
/// # What it returns, and why it never under-pays
///
/// The result is the **largest chunk-group boundary `C` in `[fetch_start,
/// total_bytes]` whose bao wire cost is fully within `paid_wire`** — i.e. the
/// greatest `C` with `wire([fetch_start, C)) <= paid_wire`, where `wire(range)` is
/// [`bao_encoded_size`] over the real `total_bytes` tree (the identical walk the
/// serve-side encoder and `aligned_wire_len` use, so it equals the bytes actually
/// on the wire, byte-for-byte).
///
/// Because a bao pre-order stream emits every chunk group's proof-and-data before
/// it descends into any later subtree, the wire position at which content offset
/// `C` finishes is exactly `wire([fetch_start, C))` (same tree, same pre-order
/// walk, same node set up to `C`; later subtrees' interior nodes are emitted only
/// after `C`). So `wire([fetch_start, C)) <= paid_wire` proves every byte needed
/// to deliver content `[fetch_start, C)` landed inside the paid wire prefix — that
/// content is paid for. Resuming at `C` therefore re-fetches only `[C, end)`;
/// nothing past a paid frontier is skipped, so it **cannot under-pay**. The only
/// slack is the sub-group tail between `C` and the true (possibly mid-group) paid
/// frontier: that content is re-fetched and re-paid once on the next leg, a
/// bounded over-pay of **strictly less than one chunk group** (16 KiB).
///
/// `fetch_start` MUST be a chunk-group boundary (every resume offset is — it comes
/// from a checkpointed group boundary or a previous call to this function). A `paid_wire` of
/// `0`, or a `total_bytes` of `0`, yields `fetch_start` (nothing paid on this leg
/// ⇒ resume where it began).
/// A boxed, `Send` future returned by the output/cache trait methods below —
/// the same boxed-future async-trait shape [`crate::source::SourceFuture`] uses
/// on the input side, so a sink or cache can be held as `&dyn`.
pub type SinkFuture<'a, T> =
Pin;
/// An injected content cache the `Streamer` consults and fills, keyed by
/// `(hash, offset)` over content bytes.
///
/// Pure by design: the trait names no blob store, which is what keeps
/// `decdn-client` `iroh-blobs`-free — a blob-store-backed implementation lives
/// ABOVE this crate and is injected. The default is the no-op [`NoCache`] (never
/// hits, never stores), so a caller that wants no revisit-reuse pays nothing.
///
/// On a clean finish the `Streamer` tees the whole verified blob to
/// [`put`](BlobCache::put). On a revisit it asks [`get`](BlobCache::get) for the
/// whole blob: a whole-blob hit is served straight from the cache with no fetch,
/// and anything less is treated as a miss and refetched in full — the store's
/// gap-driven resume, not the cache, is what avoids re-pulling a partial prefix
/// today. (Serving a cached PARTIAL prefix and fetching only the complement is a
/// planned enhancement; [`get`](BlobCache::get) already reports a partial hold as
/// a miss so an implementer need not stitch fragments.)
/// The default [`BlobCache`]: never caches. Every [`get`](BlobCache::get) misses
/// and every [`put`](BlobCache::put) discards, so a `Streamer` built with it
/// always fetches the whole range and stores nothing. A caller that wants
/// revisit-reuse injects a real cache above `decdn-client`.
;
// ---------------------------------------------------------------------------
// Test double: an in-memory BlobCache for the `Streamer` and cache unit tests.
// ---------------------------------------------------------------------------
/// The exact key an in-memory [`BlobCache`] entry hits on: `(hash, offset, len)`.
type CacheKey = ;
/// An in-memory [`BlobCache`] that stores each `put` under its exact
/// `(hash, offset, len)` key and hits only on an identical key. The minimum a
/// `Streamer` cache test needs to prove complement-only fetching.
pub