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 fetch and bundle pull are built on this crate. 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 |
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.
§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. Vouchers and pool transactions are signed with a
PrivateKeySigner. - 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. - 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 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 lane the buyer store has not seen resumes from the chain’s
getWatermark. - 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§
- 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 fetch, #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 caller-owned QUIC connection kept warm across many hash fetches
([
connection::WarmConnection]): one dial amortized over every hash, one bi-stream per hash (no wire change), closed once on the handle’s ownDrop; and the command-wide map of one such connection per node (connection::Connections). Diallingcdn/client/v1connections, and a caller-owned connection kept warm across many hashes. - 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/settling payment channels.
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). - 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).
Structs§
- Blob
TooLarge - Typed sentinel for a pull aborted because the bytes that ACTUALLY arrived
crossed the buyer’s
max_blob_size_bytesceiling (#1895). The peer’s signedtotal_bytesclaim never drives a refusal — it is peer-controlled and unverified (StreamResponse::validate()does not bound it) — so the ceiling binds on cumulative 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 stale nonce (#852), deposit exhaustion, or a wrong-channel
mismatch. 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 channel-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 fetch) 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 CLIfetchandbundle pulluse) 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.