Expand description
The deCDN client: fetch content-addressed blobs from deCDN nodes, pay for them per megabyte from a USDC payment pool, and verify every byte against its BLAKE3 hash as it arrives.
The decdn CLI’s get is built on this crate, and its bundle route is a
thin caller of bundle::BundlePuller. So is the node’s
own cache-miss pull: a node that misses in its cache is a paying client of
the node upstream of it.
§Choose an entry point
| You want to | Use |
|---|---|
| Save one blob, or a set of blobs, to files as fast as possible, and resume a partial download | Downloader |
| Read one blob in order, and pay only for what your reader reaches | Streamer |
| Pull a whole bundle manifest into a directory, with discovery, funding, resume and dedup handled | bundle::BundlePuller |
The Downloader stripes a blob
across every holder at once, and writes each verified range at its offset in
a .partial file beside the destination. A rerun fetches only the ranges
that file does not hold yet. The Streamer fetches at most one read-ahead
window (PullConfig::read_ahead_bytes) ahead of its reader, so a reader
that stops early stops the spend within that window. The
bundle::BundlePuller runs every step below for you over one pool: it
takes the chain (session::ChainConfig), a buyer-pool store, and a
bundle::Funding, and records each lane’s payment itself.
§Before you fetch
A fetch needs a funded payment pool, an iroh endpoint, and one paid lane per
node that holds the blob. The download and stream examples in this
crate’s examples/ directory run the whole sequence:
- Load the buyer key as a
PrivateKeySigner. It owns the pool and signs the pool transactions. Open the buyer store, and derive the key this data dir signs vouchers with (buyer_pool::OwnedPayer::for_store): two data dirs on one wallet then pay on disjoint lanes of one pool. - Reuse or open a pool. Read the pool the buyer store already records
(
decdn_incentive::BuyerPoolStore::get_by_owner). If there is none, approve the deposit (buyer_pool::ensure_allowance), open a pool (buyer_pool::open_pool), and record the returned state at once: the deposit is escrowed from the momentopen_poolreturns. Then resolve thebuyer_pool::UnrecordedEscrowreturned beside it, so a handle you passed names the open until the record commits. - Bring up an endpoint with
endpoint::client_endpoint. - Find the holders. Read the registered nodes
(
discovery::bootstrap_nodes), pick a sample (discovery::select_candidates), and probe each one (probe::probe_once). Verify every answer (probe::verify_probe_response) before it counts, and drop an answer whosehas_bloband coverage disagree. A probe answer’s size (discovery::Probed::total_bytes) is an unsigned hint: pass it as the fetch’s first size claim, which the fetch grows or shrinks as verified bytes land. - Build one lane per holder. Pin a
PoolContextto the holder’s provider address withbuyer_pool::self_owned_lane_ctx, resuming at what the(pool, lane key, provider)lane has already paid, and bind it to your endpoint (PoolContext::bind_endpoint). A node that misses the blob in its cache serves an unbound lane nothing. Create its ledger withPoolContext::new_ledger, and wrap both in aPeerSourceinside aStreamCandidate::new. Cap the lane’s rate at the rate the node signed in its probe answer (effective_rate_ceiling). - Fetch with
Downloader::fetch_to_pathsorStreamer::open, over aStaticSourcesof the lanes. Every lane streams at once unlessDownloader::max_lanescaps it. - Record what each lane paid, whatever the outcome: build a
VoucherProgressfrom the ledger’s settlement, and apply thebuyer_pool::ProgressWriteit calls for to the buyer store.
§What the crate guarantees
- Every byte is verified. Delivery is BLAKE3/bao verified streaming.
The decoder checks each chunk group against the hash as it lands, so a
corrupt range aborts the pull at that group. Nothing unverified reaches a
VerifiedReaderor a finished file. - You pay only for verified bytes. You pay per chunk
(
decdn_protocol::CHUNK_BYTES) once the decoder has verified it. A lying node is paid for the bytes before its first bad group and for nothing after, never for the size it claimed. - Quotes are checked against your ceiling. A stream response whose
signed rate exceeds the
max_rate_per_mbgiven toPeerSource::newis refused before any payment (RateAboveCeiling). - Recovery is free. Every lane draws on one shared pool. A source that
faults cools and returns, and its range goes to another holder meanwhile.
A fetch ends on done, a fatal fault, or the stop policy
(
StopPolicy). No delivered byte is fetched or paid for twice.
§What stays yours
- Persistence. The faces never write the buyer store. Record every lane’s payment after each fetch (step 7). A lane you do not record resumes from a stale watermark next time, and its provider rejects the vouchers.
- Funding policy. A source whose next voucher the deposit cannot cover,
or that refuses the pool as
Unfunded, waits for the deposit to rise. Once no other source serves and a fresh discovery finds nothing new, the fetch runs one funding recovery step through yourFunder(Funder::recover): it may top the pool up, open a new pool (PoolReplaced), or report that it has no way to add funds. A further step needs a newly verified byte since the last one. When no step is allowed or possible, the fetch fails with aNoAffordableSource, “funding needed”. - Delegated credentials. A delegate that pays under someone else’s
pool hands the face a
CredentialSlotand watches itsFundingEvents. Swap a new key and capability into the slot when the capability runs low: the fetch retires the old lanes at a voucher boundary and goes on under the new key. Without a swap, a delegated fetch ends with a typedFundingNeeded. - Which holders to use. Discovery gives candidates; ordering and
admission (
discovery::admit_sources) are the caller’s choice.
§Mistakes to avoid
- One candidate per provider. Each
StreamCandidatepays one(signer, provider)lane. Two candidates for one provider sign competing vouchers on the same lane. - Always set a rate ceiling. A
max_rate_per_mbof0means no ceiling, so a node can quote low on the probe and high on the stream. Passeffective_rate_ceilingof the probed rate and any absolute cap you hold. - Never resume a lane below its on-chain watermark. A voucher at or below
the watermark redeems nothing, so the provider streams bytes it can never
cash. A self-owned lane the buyer store has not seen resumes from the
chain’s
getWatermark. A delegated lane has no watermark on chain until a redemption records it, so one the store has not seen starts at zero and heals from the watermark bundle the node returns (ADR 003 § Capability delegation). - Keep the stream drive running.
Streamer::openreturns a reader and aStreamDrive. Run your read insideStreamDrive::alongside: the drive pays and drains open legs, and it must not wait behind a blocked consumer. - Progress is in content bytes. A
ProgressCallbackon the faces reports verified content bytes against the blob’s size.
§How delivery works
Each lane sends a StreamRequest, verifies the signed StreamResponse,
and then reads ChunkData and pays as it goes. It releases one hash-chain
preimage per delivered CHUNK_BYTES chunk, and signs a voucher to open a
chain, to roll one, and to settle a residual shorter than a chunk.
The ChunkData payload is bao’s interleaved verified-stream encoding, not
raw bytes. Every consumer feeds it to a bao-tree verifying decoder as it
arrives. The decoder checks every chunk group against the requested root. A
range fetched from any offset therefore verifies on its own, with no
dependency on earlier bytes (ADR 038).
§Stability
The documented items are the SDK, and the public-API snapshot in
tests/public_api.rs guards them. Their enums and the error types a fetch
returns are #[non_exhaustive], so a new variant or field is not a
breaking change. Items hidden from these docs are the pull engine’s seams
that the node and the decdn CLI drive directly. They carry no stability
promise.
Re-exports§
pub use config::PullConfig;pub use connection::Connections;pub use credential::CREDENTIAL_SWAP_WAIT;pub use credential::CapabilityCause;pub use credential::Credential;pub use credential::CredentialMismatch;pub use credential::CredentialSlot;pub use credential::FundingEvent;pub use credential::FundingNeeded;pub use downloader::DownloadTarget;pub use downloader::Downloader;pub use driver::PoolExhausted;pub use recovery::RecoveryGate;pub use sink::BlobCache;pub use sink::NoCache;pub use sink::SinkFuture;pub use source::Funder;pub use source::PeerSource;pub use source::PoolReplaced;pub use source::Recovery;pub use source::SourceFuture;pub use source_set::Holder;pub use source_set::NoAffordableSource;pub use source_set::NoNodeWillServe;pub use source_set::NoSourceHasBlob;pub use source_set::StaticSources;pub use stop::ClockHold;pub use stop::GaveUp;pub use stop::ProgressClock;pub use stop::StopPolicy;pub use streamer::LiveReader;pub use streamer::StreamCandidate;pub use streamer::StreamDrive;pub use streamer::Streamer;pub use streamer::VerifiedReader;
Modules§
- bundle
- Pull a whole bundle: every blob a bundle manifest names, into an output
directory, over the paid
cdn/client/v1path. - buyer_
pool - Buyer-side
PaymentPoolopen kernel (#940), shared by the node service and the CLI. Buyer-sidePaymentPoolopen kernel, shared by the node’s node-to-node cache-miss buyer (#744) and the CLI (decdn get, #940). - config
- Zero-config tunables for the consumption faces over the paid pull engine
(
config::PullConfig, #1848): every knob defaults, so a caller overrides only what it must, and construction needs no network or chain access. Zero-config tunables for the consumption faces over the paid pull engine (Downloader,Streamer, #1848). - connection
- A QUIC connection kept warm across many requests on one ALPN
([
connection::WarmConnection]): one dial amortized over every request, one bi-stream per request (no wire change), closed once on the handle’s ownDrop; and the map of one such connection per node on one ALPN (connection::Connections), shared by a CLI command or by a node’s outbound pull legs and DHT calls. Dialling connections, and connections kept warm across many requests. - credential
- A delegated fetch’s voucher credential, which the application swaps when the capability runs low, and the funding signal that says when. The delegate’s voucher credential and its funding signal (ADR 039 § Client SDK funding signal, ADR 003 § Funding recovery).
- discovery
- Client-side node discovery (#936): read + select the active node set from
CapacityBond.getRegisteredNodes, then rank probed blob-holders. Client-side node discovery (#936): read the active node set fromCapacityBond.getRegisteredNodesso a client can pick a node instead of being handed an explicit--node-id/--addr/--provider-address. Read fromfetch::discover_providerand from the discovery branch ofbundle_pull::bundle_pull; the ranking half is also used byfetch::probe_and_rank.decdn probedeliberately still requires an explicit target. - downloader
- The
Downloaderconsumption face (#1848): fetch a set of content-addressed blobs (a bundle, or a single blob) to files in a directory, out-of-order and at full throughput, so every byte is bao-verified and a resumed fetch re-pulls only the missing ranges. TheDownloaderconsumption face (#1848): fetch a set of content-addressed blobs — a bundle, or a single blob — to files in a directory. - driver
- The #1608 gap-driven fetch driver: [
driver::drive] fills only themissing_rangesof a request, paying the minimum, by folding the resume / reseed loop behind the [pacer::Pacer] axis. The #1608 gap-driven, range-minimized fetch driver. - endpoint
- One-shot client
Endpointconstruction: relay + discovery resolution for thecdn/client/v1andcdn/probe/v1dial paths (#935/#936). Shared relay resolution for the one-shot client commands (fetch,probe). - probe
- Reusable
cdn/probe/v1client. Reusablecdn/probe/v1client. - provider
- Wallet-filled HTTP provider builder for opening, topping up, closing and reclaiming
payment pools.
HTTP provider builders for the
decdnCLI and SDK clients — a wallet-filled builder that signs and sends transactions, and a wallet-free builder for reads that run before a keystore is unlocked — plus the retryable-vs-permanent classifier for a failed chain read (is_permanent_contract_error,is_permanent_rpc_error). - recovery
- The funding recovery step at the exhausted candidate set (ADR 003 § Funding
recovery): the per-fetch
recovery::RecoveryGateand its progress rule. The funding recovery step at the exhausted candidate set (ADR 003 § Funding recovery, ADR 039 § Failure handling). - session
- What a paid pull runs under: the deployment it pays on, where it keeps its local state, how it funds its pool, and how it finds the holders.
- sink
- Streaming-decode support types for the paid pull path (#1120, ADR 038).
- source
- The sourcing and funding seams of a fetch (#1608): [
source::BlobSource] (raw-bao byte source for a range) andsource::Funder(the injected funding recovery seam), plus scripted test doubles. The sourcing and funding seams of a fetch (#1608): [BlobSource], a dumb producer of raw interleaved bao bytes for a contiguous range, andFunder, the injected funding recovery seam. - source_
set - The sources of one blob (ADR 039 § Source set and selection):
[
source_set::SourceSet] tracks each holder’s health, lane, and retry timing behind the injected [source_set::SourceProvider] seam. The sources of one blob (ADR 039 § Source set and selection). - stop
- When a command gives up (ADR 039 § Failure handling: reassign-only tail): the progress clock and the stop policy that watches it. When a command gives up (ADR 039 § Failure handling: reassign-only tail, the stop policy).
- streamer
- The
Streamerconsumption face (#1848): stream one blob’s verified, contiguous front to a consumer as it arrives, paced by consumption and bounded to one read-ahead window ahead of the read cursor. A fetch-like single-blob face over the same engine, with no chunk dedup. TheStreamerconsumption face (#1848): stream one blob’s verified, contiguous front to a consumer AS IT ARRIVES, paced by how fast the consumer reads — a fetch-like, single-blob face (no chunk dedup, unlike theDownloader). - uri
decdn:URIs (#2449): one string names a blob, a whole bundle, or one file inside a bundle, with an optional namespace routing hint.
Structs§
- Blob
TooLarge - Typed sentinel for a pull ended by the buyer’s
max_blob_size_bytescap (#1895). The cap bounds how many bytes of one blob the buyer holds, not which offsets it reads: a range anywhere in a blob larger than the cap lands while the bytes held stay within it. The peer’s signedtotal_bytesclaim never drives a refusal — it is peer-controlled and unverified (StreamResponse::validate()does not bound it) — so the cap binds on RECEIVED, BLAKE3-verified bytes instead. Returned (not a bare string) so the pull orchestrator candowncast_refand classify it as a buyer-side policy rejection — distinct from a hash mismatch or an unreachable peer — rather than mis-attributing it to the provider’s reputation.DisplaycarriesBlobTooLargeso logs and the requester tests can match on it. - Cumulative
- A lane’s cumulative voucher state: the absolute totals carried by the most
recent voucher. Both advance monotonically over the lane’s lifetime. There
is no nonce —
amountis the sole monotone ordering and replay key (ADR 005 §Voucher wire format). - Hash
Mismatch - The upstream delivered bytes that failed bao verification against the
requested content root — a paid-but-corrupt delivery (the content-addressing
invariant, ADR 014/038). Under ADR 038 the verifier is the per-chunk-group
bao-treedecoder fed as bytes arrive, not a whole-blob re-hash, so this fires the moment any group’s proof mismatches. Returned (viaanyhow) by this crate’s decoding consumers — [ClientRangedStore::ingest_stream] and thetest-utilin-memory decoder — so callers candowncast_refto classify corruption (e.g. a reputationCorruptionoutcome) without matching on the error message string. TheDisplaytext is kept stable for logs and the existing requester tests. The node’s cache admit reports the same fault asCacheError::VerifyFailed/CacheError::HashMismatch, whichdecdn-node’sis_bao_corruptionfolds together with this type. - Pool
Context - Per-lane context the requester needs to sign pool vouchers.
- Pool
Ledger - One lane’s live voucher ledger, shared by every concurrent stream that draws
on it. Voucher issuance is serialized through the async
issuancemutex — held across compute → sign → send so vouchers reach the node in strict cumulative order — and released the instant the send returns, because there is no ack to wait for (implicit acceptance, ADR 005). The committed watermark advances the moment a send succeeds; a mid-streamVoucherRejectedrewinds it throughresolve_reject. - Pull
Deadlines - The bounds on a pull, each matched to the stage it governs (#1134, #1797).
- Pull
Stalled - Typed sentinel for an upstream that went SILENT mid-stream — bytes were flowing, and
then no byte of progress for the stall budget (#1134). Distinct from
PullTimeout, which is a bound of OURS elapsing, and the distinction is load-bearing: - Pull
Timeout - Typed sentinel for the buyer’s own per-candidate pull deadline firing (#857).
Returned (not a bare string) so the pull orchestrator can
downcast_refand recognize that the timeout is OUR local deadline — a possibly mis-sized configuration value — not evidence the provider is unreachable, and so must not tar the provider’s local reputation score.Displaykeeps the stabletimed outtext for logs (and for the!contains("timed out")negative assertion innode_to_node_pull_through’s deadline test). - Rate
Above Ceiling - Typed sentinel for a server that signed an open-stage
StreamResponse(ok == true) quoting a per-MBrate_per_mbabove the buyer’s effective ceiling (#1375). The buyer aborts before paying, so the profitable “quote low on the probe, quote high on the stream, then deliver” bait-and-switch never gets paid. - Upstream
Refused - Typed sentinel for an upstream refusing delivery up front — a
StreamResponsewithbody.ok == false(#1144). Carries the wireStreamErrorverbatim (mirroringUpstreamVoucherRejected’s typedreason, not a lossy stringification) so the pull orchestrator candowncast_refand give each refusal the verdict it deserves, rather than folding every one into “unreachable”. - Upstream
Voucher Rejected - Typed sentinel for the upstream rejecting a voucher we presented mid-stream
(#857) — e.g. a cumulative amount regression (#852), deposit exhaustion, or
a wrong pool. This is OUR payment-side fault, not the provider’s, so the pull
orchestrator
downcast_refs it to skip the candidate WITHOUT recording a reputation observation (mirroring the buyer pool-open-failure arm). Named with theUpstreamprefix to disambiguate from the protocol-levelStreamError::VoucherRejectedreason enum, whosereasonit carries verbatim (theCopyVoucherRejectReason, not a lossy stringification) so a future caller can branch on retry-vs-top-up-vs-abandon without re-parsing a message.Displaykeeps the stablevoucher rejectedtext for logs. - Voucher
Progress - The lane’s committed voucher watermark, returned by [
UpstreamPull::finish] / [UpstreamPull::abort] / [UpstreamPull::progress] and threaded through thetest-utilstream_fetch_trackedwrapper as an out-param, so the caller can persist what it paid (#852).
Enums§
- Deadline
Error - A
PullDeadlineswhose bounds cannot do their job. Carries the values so a CLI can render the arithmetic back to the user rather than just saying “invalid”.
Functions§
- effective_
rate_ ceiling - Combine the two buyer rate ceilings into one effective bound, treating
0as “unbounded” on each input (the probe-relative bound and the absolute config bound are both optional). Returns0only when BOTH are unbounded.
Type Aliases§
- Progress
Callback - Delivery-progress callback: invoked with
(received, expected)after each verified leaf lands, so a caller (e.g.decdn get) can render a progress bar. Both slots are content bytes:receivedis the content position the verifying decoder has reached andexpectedis the blob’s contenttotal_bytes, constant across the pull — so a bar keyed on the two fills to exactly 100%.drive/acquire(the resumablecrate::driverpath the CLIgetuses) additionally emit the already-present resume base (base_present) once before streaming begins; thetest-utilstream_fetch_tracked_with_progresswrapper reports the same unit from its in-memory decoder.