Skip to main content

Crate decdn_client

Crate decdn_client 

Source
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 toUse
Save one blob, or a set of blobs, to files as fast as possible, and resume a partial downloadDownloader
Read one blob in order, and pay only for what your reader reachesStreamer

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:

  1. Load the buyer key. Vouchers and pool transactions are signed with a PrivateKeySigner.
  2. 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 moment open_pool returns.
  3. Bring up an endpoint with endpoint::client_endpoint.
  4. 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 whose has_blob and 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.
  5. Build one lane per holder. Pin a PoolContext to the holder’s provider address with buyer_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 with PoolContext::new_ledger, and wrap both in a PeerSource inside a StreamCandidate::new. Cap the lane’s rate at the rate the node signed in its probe answer (effective_rate_ceiling).
  6. Fetch with Downloader::fetch_to_paths or Streamer::open, over a StaticSources of the lanes. Every lane streams at once unless Downloader::max_lanes caps it.
  7. Record what each lane paid, whatever the outcome: build a VoucherProgress from the ledger’s settlement, and apply the buyer_pool::ProgressWrite it 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 VerifiedReader or 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_mb given to PeerSource::new is 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 your Funder (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 a NoAffordableSource, “funding needed”.
  • Delegated credentials. A delegate that pays under someone else’s pool hands the face a CredentialSlot and watches its FundingEvents. 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 typed FundingNeeded.
  • 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 StreamCandidate pays 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_mb of 0 means no ceiling, so a node can quote low on the probe and high on the stream. Pass effective_rate_ceiling of 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::open returns a reader and a StreamDrive. Run your read inside StreamDrive::alongside: the drive pays and drains open legs, and it must not wait behind a blocked consumer.
  • Progress is in content bytes. A ProgressCallback on 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 PaymentPool open kernel (#940), shared by the node service and the CLI. Buyer-side PaymentPool open 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 own Drop; and the command-wide map of one such connection per node (connection::Connections). Dialling cdn/client/v1 connections, 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 from CapacityBond.getRegisteredNodes so a client can pick a node instead of being handed an explicit --node-id/--addr/--provider-address. Read from fetch::discover_provider and from the discovery branch of bundle_pull::bundle_pull; the ranking half is also used by fetch::probe_and_rank. decdn probe deliberately still requires an explicit target.
downloader
The Downloader consumption 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. The Downloader consumption 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 the missing_ranges of 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 Endpoint construction: relay + discovery resolution for the cdn/client/v1 and cdn/probe/v1 dial paths (#935/#936). Shared relay resolution for the one-shot client commands (fetch, probe).
probe
Reusable cdn/probe/v1 client. Reusable cdn/probe/v1 client.
provider
Wallet-filled HTTP provider builder for opening/settling payment channels. HTTP provider builders for the decdn CLI 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::RecoveryGate and 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) and source::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, and Funder, 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 Streamer consumption 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. The Streamer consumption 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 the Downloader).

Structs§

BlobTooLarge
Typed sentinel for a pull aborted because the bytes that ACTUALLY arrived crossed the buyer’s max_blob_size_bytes ceiling (#1895). The peer’s signed total_bytes claim 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 can downcast_ref and 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. Display carries BlobTooLarge so 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 — amount is the sole monotone ordering and replay key (ADR 005 §Voucher wire format).
HashMismatch
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-tree decoder fed as bytes arrive, not a whole-blob re-hash, so this fires the moment any group’s proof mismatches. Returned (via anyhow) by this crate’s decoding consumers — [ClientRangedStore::ingest_stream] and the test-util in-memory decoder — so callers can downcast_ref to classify corruption (e.g. a reputation Corruption outcome) without matching on the error message string. The Display text is kept stable for logs and the existing requester tests. The node’s cache admit reports the same fault as CacheError::VerifyFailed / CacheError::HashMismatch, which decdn-node’s is_bao_corruption folds together with this type.
PoolContext
Per-lane context the requester needs to sign pool vouchers.
PoolLedger
One lane’s live voucher ledger, shared by every concurrent stream that draws on it. Voucher issuance is serialized through the async issuance mutex — 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-stream VoucherRejected rewinds it through resolve_reject.
PullDeadlines
The bounds on a pull, each matched to the stage it governs (#1134, #1797).
PullStalled
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:
PullTimeout
Typed sentinel for the buyer’s own per-candidate pull deadline firing (#857). Returned (not a bare string) so the pull orchestrator can downcast_ref and 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. Display keeps the stable timed out text for logs (and for the !contains("timed out") negative assertion in node_to_node_pull_through’s deadline test).
RateAboveCeiling
Typed sentinel for a server that signed an open-stage StreamResponse (ok == true) quoting a per-MB rate_per_mb above 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.
UpstreamRefused
Typed sentinel for an upstream refusing delivery up front — a StreamResponse with body.ok == false (#1144). Carries the wire StreamError verbatim (mirroring UpstreamVoucherRejected’s typed reason, not a lossy stringification) so the pull orchestrator can downcast_ref and give each refusal the verdict it deserves, rather than folding every one into “unreachable”.
UpstreamVoucherRejected
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 the Upstream prefix to disambiguate from the protocol-level StreamError::VoucherRejected reason enum, whose reason it carries verbatim (the Copy VoucherRejectReason, not a lossy stringification) so a future caller can branch on retry-vs-top-up-vs-abandon without re-parsing a message. Display keeps the stable voucher rejected text for logs.
VoucherProgress
The lane’s committed voucher watermark, returned by [UpstreamPull::finish] / [UpstreamPull::abort] / [UpstreamPull::progress] and threaded through the test-util stream_fetch_tracked wrapper as an out-param, so the caller can persist what it paid (#852).

Enums§

DeadlineError
A PullDeadlines whose 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 0 as “unbounded” on each input (the probe-relative bound and the absolute config bound are both optional). Returns 0 only when BOTH are unbounded.

Type Aliases§

ProgressCallback
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: received is the content position the verifying decoder has reached and expected is the blob’s content total_bytes, constant across the pull — so a bar keyed on the two fills to exactly 100%. drive / acquire (the resumable crate::driver path the CLI fetch and bundle pull use) additionally emit the already-present resume base (base_present) once before streaming begins; the test-util stream_fetch_tracked_with_progress wrapper reports the same unit from its in-memory decoder.