dig_download/lib.rs
1//! # dig-download — the node-side multi-source download orchestrator for the DIG Node peer network
2//!
3//! `dig-download` answers **"get me this content, fast and verified."** Given a [`ContentId`] (store
4//! / root / capsule / resource) it runs the normative L7 multi-source flow: **locate** the holders in
5//! the DHT, **confirm** them with `dig.getAvailability`, **fan** different byte ranges across
6//! different holders **simultaneously** (`dig.fetchRange` over dig-nat mux streams), **verify** each
7//! range independently against the capsule's chain-anchored merkle root, **rebalance** around slow /
8//! dropped / bad sources, and **reassemble** the verified bytes in order into the node's store — with
9//! **pause + resume** that never re-fetches an already-verified range. It is the node engine that
10//! supersedes the retired browser-side `dig-download-utility`.
11//!
12//! ## The public surface
13//!
14//! - [`Downloader`] — built once from injected dependencies, then [`download`](Downloader::download)ed
15//! against many content ids. Returns a [`DownloadHandle`] (progress event stream +
16//! [`pause`](DownloadHandle::pause) / [`resume`](DownloadHandle::resume) /
17//! [`cancel`](DownloadHandle::cancel) + [`join`](DownloadHandle::join)).
18//! - Trait boundaries (the injection seams — real impls over dig-dht/dig-nat, or the in-memory
19//! [`testkit`]):
20//! - [`ProviderLocator`] — "which peers hold this?" ([`DhtProviderLocator`] over dig-dht).
21//! - [`RangeTransport`] — fetch a range / availability from a peer ([`NatRangeTransport`] over
22//! dig-nat).
23//! - [`Sink`] — where verified bytes land ([`FileSink`] stages to `<target>.download.tmp` and
24//! atomically finalizes; dig-node supplies a store-backed sink).
25//! - [`StateStore`] — persist per-range resume progress ([`InMemoryStateStore`] /
26//! [`FileStateStore`]).
27//! - [`Verifier`] / [`ProofVerifier`] — per-range + chain-anchored integrity ([`MerkleVerifier`];
28//! dig-node injects the digstore proof verifier to bind to the on-chain root).
29//! - [`onion`] — **onion mode**: a transfer carried back through the hops that carried the ask, as an
30//! [`OnionRangeTransport`] over an injected [`OnionChannel`] (the layered transport is `dig-onion`'s).
31//! Delivery changes; trust does not — an onion-delivered byte faces the same per-range and
32//! chain-anchored checks as a directly fetched one. Includes the byte-denominated
33//! [`StreamRelayConfig`] that bounds what a hop spends carrying someone else's transfer, and which is
34//! OFF by default.
35//! - [`gc`] — reap stale `.download.tmp` staging files, never a live/paused-resumable one
36//! ([`ActiveDownloads`] + [`TmpGc`]; run [`Downloader::gc`] on an interval like dig-dht's provider
37//! `gc()`).
38//!
39//! ## Integrity model (L7 §9)
40//!
41//! Two checks, two moments. **Per range, immediately:** the returned bytes cover whole chunk(s) whose
42//! lengths match the resource's `chunk_lens`, and the declared generation `root` matches — a
43//! truncated / mis-sized / wrong-generation source is caught the instant its range arrives and the
44//! range is re-fetched elsewhere. **Whole resource, at completion:** `resource_leaf =
45//! SHA-256(concatenated chunk ciphertexts)` is the leaf committed under the chain-anchored `root`
46//! (via an injected [`ProofVerifier`]). Whichever mix of peers served the ranges, they all verify
47//! against the same on-chain root.
48//!
49//! ## Implementers' note — wiring dig-download into dig-node
50//!
51//! dig-node owns the runtime context the trait boundaries abstract, and constructs a [`Downloader`]
52//! from it:
53//!
54//! 1. **Locator** — build a [`dig_dht::DhtService`] (its dig-nat transport + bootstrap peers from the
55//! relay introducer / gossip pool), wrap it in [`DhtProviderLocator::new`], `Arc` it.
56//! 2. **Transport** — build a [`NatRangeTransport::new`] from the node's
57//! [`dig_nat::NodeCert`] (its CA-signed mTLS identity, minted by dig-tls's
58//! `NodeCert::load_or_generate`) + [`dig_nat::NatConfig`] + `network_id`; it dials providers over
59//! the NAT-traversal ladder and runs `dig.getAvailability` / `dig.fetchRange`.
60//! 3. **Verifier** — [`MerkleVerifier::with_proof_verifier`] with the **digstore merkle-proof
61//! verifier** (the store crate owns the proof byte format) so the whole-resource check binds to the
62//! chain-anchored root. This is the ONLY production constructor: there is no fail-open default, so a
63//! node cannot accidentally run without the on-chain binding. (The explicitly-named,
64//! `#[doc(hidden)]` `MerkleVerifier::insecure_structural_only` enforces per-range + structural
65//! integrity only and is for tests / deliberate opt-in.)
66//! 4. **Sink** — per download, a [`FileSink::new(final_path)`](FileSink) (stages to
67//! `<final_path>.download.tmp`, atomically renames on finalize), OR a digstore-backed [`Sink`] that
68//! writes the capsule/resource ciphertext into the store and finalizes on install.
69//! 5. **State store** — a [`FileStateStore`] under the download/cache dir (survives restarts).
70//! 6. **Construct + drive** — `Downloader::new(locator, transport, verifier, state_store, config)`,
71//! then `let handle = downloader.download(content_id, sink, opts);` and drive it:
72//! `handle.next_event()` for progress, `handle.pause()/resume()/cancel()`, `handle.join().await`
73//! for the result. On startup and on an interval, call `downloader.gc(download_dir, ttl)` to reap
74//! abandoned staging files (`downloader.active_downloads()` protects live/paused ones).
75//!
76//! A content-want handler thus becomes: derive the [`ContentId`], pick a sink, `download(...)`, and
77//! surface progress — the crate does discovery, multi-source fan-out, verification, retry, and
78//! resume.
79
80#![forbid(unsafe_code)]
81#![warn(missing_docs)]
82
83pub mod addr;
84pub mod error;
85pub mod gc;
86pub mod locate;
87pub mod module;
88pub mod onion;
89pub mod orchestrator;
90pub mod plan;
91pub mod progress;
92pub mod queue;
93pub mod read_ladder;
94pub mod select;
95pub mod sink;
96pub mod source;
97// The in-memory test harness is compiled ONLY for this crate's own tests or behind the explicit
98// `testkit` feature — it ships the fail-OPEN doubles, which have no place in a production build.
99#[cfg(any(test, feature = "testkit"))]
100pub mod testkit;
101pub mod throttle;
102pub mod verify;
103
104// Re-export the dig-nat wire shapes that appear in this crate's public trait signatures
105// (`RangeTransport`, `OnionChannel`), so a consumer implementing a transport names ONE copy of each
106// shape rather than adding its own dig-nat dependency and risking a version skew across the seam —
107// the `ModuleInfo` skew class recorded in Cargo.toml, which cost six diagnosis rounds on #836.
108pub use dig_nat::{AvailabilityItem, AvailabilityResponse, RangeRequest};
109
110// Re-export the content id from dig-dht so consumers use ONE `ContentId` type across locate +
111// download (no divergent shape).
112pub use dig_dht::{ContentId, ProviderRecord};
113
114pub use addr::{candidate_socket, dial_candidates, AddrError, MAX_DIAL_CANDIDATES};
115pub use error::{
116 hex64_or_sentinel, sanitize_untrusted_text, DownloadError, VerifyError,
117 MAX_ERROR_CONTEXT_CHARS, MAX_ERROR_REASON_CHARS,
118};
119pub use gc::{ActiveDownloads, GcConfig, TmpGc};
120pub use locate::{DhtProviderLocator, ProviderLocator};
121pub use module::{
122 module_content_id, module_download_key, ModuleAnchor, ModuleAnchorVerifier,
123 ModuleDownloadConfig, ModuleDownloader, ModuleReader, ModuleTransport, DEFAULT_MAX_MODULE_SIZE,
124 MAX_DESCRIPTOR_ATTEMPTS, MAX_MODULE_CHUNK_COUNT,
125};
126// The fail-OPEN anchor verifier is a TEST double, not part of the production surface (#1576 gate): it
127// exists only under `cfg(test)` / the `testkit` feature so a consumer build cannot bypass the module
128// pull's sole root of trust.
129#[cfg(any(test, feature = "testkit"))]
130pub use module::AcceptAnyModuleAnchor;
131// Re-export the wire descriptor so consumers use ONE `ModuleInfo` shape (the dig-rpc-protocol
132// byte-contract) across the module pull — no divergent local copy (#1576).
133pub use dig_rpc_protocol::types::ModuleInfo;
134pub use onion::{
135 decide_relay_stream, HopPath, HopPathError, InboundStream, OnionChannel, OnionRangeTransport,
136 StreamRelayConfig, StreamRelayDecision, StreamRelayRefusal, DEFAULT_MAX_BYTES_PER_STREAM,
137 DEFAULT_RELAY_BYTES_PER_WINDOW, MAX_HOP_PATH,
138};
139pub use orchestrator::{
140 download_key, DownloadConfig, DownloadHandle, DownloadOptions, Downloader,
141 DEFAULT_RANGE_TIMEOUT, DEFAULT_REFRESH_INTERVAL,
142};
143pub use plan::{plan_ranges, ChunkLayout, Range, RangeState};
144pub use progress::{
145 DownloadEvent, DownloadProgress, DownloadState, FileStateStore, InMemoryStateStore, StateStore,
146};
147pub use queue::{DownloadQueue, QueuedHandle, DEFAULT_MAX_ACTIVE_DOWNLOADS};
148#[cfg(feature = "http-probe")]
149pub use read_ladder::HttpHealthProbe;
150pub use read_ladder::{
151 local_urls, override_source, resolve_node, CachedResolver, HealthProbe, LocalRung,
152 OverrideInputs, OverrideSource, ResolvedNode, ResolvedTier, TransportMode,
153 DEFAULT_LOCAL_NODE_PORT, DEFAULT_PROBE_TIMEOUT, DIG_LOCAL_HOST, RPC_DIG_NET,
154};
155pub use select::{
156 CandidateRef, NullSelector, RangeOutcome, RangeResult, SelectPlan, SelectRequest,
157 SourceSelector,
158};
159pub use sink::{staging_path_for, FileSink, InMemorySink, Sink, STATE_SUFFIX, TMP_SUFFIX};
160pub use source::{
161 assemble_range_stream, drain_trailer_bounded, FetchedRange, NatRangeTransport, RangeMeta,
162 RangeTransport, SourceHealth, SourceTracker,
163};
164pub use throttle::FcfsRateLimiter;
165pub use verify::{
166 MerkleVerifier, ProofVerifier, ResourceCommitment, ResourceHasher, StructuralOnlyProofVerifier,
167 Verifier,
168};