Skip to main content

dig_peer_selector/
lib.rs

1//! # dig-peer-selector — the self-optimizing peer-selection middleware of the DIG Node
2//!
3//! The selector is a pure **decision + learning** layer. It sits between [`dig-download`] (the
4//! executor that fetches bytes over [`dig-nat`] mTLS) and the peer-discovery layers ([`dig-dht`],
5//! [`dig-gossip`]/`dig-pex`, [`dig-nat`]), and answers one question — *"of these candidate peers,
6//! which subset should serve this content, and in what order?"* — learning the answer from the
7//! **real, measured outcome** of every transfer it influenced. It has **no user-facing
8//! configuration**: every tradeoff (saturation point, relayed penalty, decay) is self-tuned from
9//! observed data.
10//!
11//! This crate is the authoritative implementation of `SPEC.md` (version `1`). The SPEC is the
12//! contract; this documentation summarizes it — read `SPEC.md` for the normative statements.
13//!
14//! ## The closed loop
15//!
16//! 1. The node calls [`dig_dht::DhtService::find_providers`] and maps the [`ProviderRecord`]s into
17//!    [`Candidate`]s, then asks the selector to [`select`](PeerSelector::select) the best subset for a
18//!    [`ContentRequest`].
19//! 2. `dig-download` executes the multi-source byte-range transfer over `dig-nat` mTLS mux streams.
20//! 3. Every per-range / per-request [`TransferOutcome`] streams back via
21//!    [`record_outcome`](PeerSelector::record_outcome), updating the models **in real time** so the
22//!    next `select` — and a mid-transfer [`rebalance`](PeerSelector::rebalance) — is smarter.
23//!
24//! ## What is learned (measured-only, non-gameable)
25//!
26//! A peer's quality is refined EXCLUSIVELY from measured outcomes (SPEC §3, §9.2): there is no input
27//! path by which a peer raises its own score, and **observed capacity always overrides advertised**
28//! (SPEC §9.3). Throughput/RTT are recency-weighted estimators whose decay is derived from each
29//! peer's observed volatility — no baked constant (SPEC §3.2, §4.3). The scorer learns a per-class
30//! **saturation point** (anti-thundering-herd, SPEC §4.1) and an adaptive **relayed penalty**
31//! (SPEC §4.2), and orients toward **minimizing P99 request latency** (SPEC §4.4).
32//!
33//! ## Boundaries (what the selector is NOT)
34//!
35//! It never queries the DHT, runs the gossip pool, opens a socket/TLS session/mux stream, or
36//! fetches/verifies/persists bytes — those belong to `dig-dht`, `dig-gossip`, `dig-nat`, and
37//! `dig-download` respectively (SPEC §1.2). The selector only *reads their outputs* or *drives their
38//! choices*. It re-uses the transport-verified [`PeerId`] (`= SHA-256(TLS SPKI DER)`) and the
39//! `dig-dht` content/candidate types verbatim — it defines no parallel identity (SPEC §5, §11).
40//!
41//! ## Implementers' note — how `dig-node` embeds the selector (P3 integration, digstore)
42//!
43//! The selector is the **source-selection seam** between `dig-dht` and `dig-download`. `dig-node`
44//! wires it as follows (the wiring lives in the node, NOT this crate — SPEC §6.1, §7.5):
45//!
46//! 1. **Construct** one [`PeerSelector`] per node: `PeerSelector::new(SelectorConfig::default())`.
47//! 2. **Feed the registry** continuously:
48//!    - subscribe to `dig_gossip::GossipHandle::subscribe_pool_events()` and forward each event —
49//!      convert `dig_gossip::PoolEvent` → [`PoolEvent`] with a trivial 1:1 field map (identical
50//!      shapes; see [`pool_event`]) — into [`on_pool_event`](PeerSelector::on_pool_event);
51//!    - on each established `dig-nat` connection, call
52//!      [`on_connection_class`](PeerSelector::on_connection_class) with its
53//!      [`dig_nat::TraversalKind`];
54//!    - optionally seed from a `connected_pool_peers()` snapshot at startup via
55//!      [`upsert_candidate`](PeerSelector::upsert_candidate).
56//! 3. **Per content want**, call `find_providers(&content)`, map each [`ProviderRecord`] into a
57//!    [`Candidate`] (via [`Candidate::from_provider_record`]), and call
58//!    [`select`](PeerSelector::select). Use each [`SelectedPeer`]'s `max_concurrency` as the per-peer
59//!    concurrent-range cap in `dig-download` (replacing its built-in `pick_source` heuristic).
60//! 4. **Translate the `dig-download` `DownloadEvent` stream into outcomes as it flows** (SPEC §6.2):
61//!    map `RangeCompleted` → a `Range` [`TransferOutcome`] with `result: Success`; `RangeFailed` →
62//!    `Failure { reason }` (an integrity/verify failure → [`FailureReason::VerificationFailed`]);
63//!    optionally `Completed` → a `Request` outcome for whole-request P99 learning. Call
64//!    [`record_outcome`](PeerSelector::record_outcome) for each. A `Paused` is NOT a failure — do not
65//!    record one (SPEC §6.4).
66//! 5. **On a dropped source / relocate**, call [`rebalance`](PeerSelector::rebalance) with the still-
67//!    active peers and the still-needed ranges to get a replacement subset (SPEC §5.5). On resume,
68//!    `select`/`rebalance` only the ranges NOT in `DownloadState::done_ranges` (SPEC §6.4).
69//!
70//! ## DigPeer hand-off — how consumers establish connections
71//!
72//! The selector returns *ranked peer identities* (each [`SelectedPeer`] carries a [`PeerId`]); it does
73//! NOT establish connections. The node is responsible for **connecting** to each selected peer via
74//! the [`dig-peer`] crate: construct a [`dig_peer::PeerTarget`] from the `peer_id` + addresses, then
75//! call [`dig_peer::DigPeer::connect`] to establish mTLS and get a [`dig_peer::Connected`] for the
76//! actual streams (SPEC §6.1, §11). The selector's role ends at selection; the transport is entirely
77//! the node's (and `dig-download`'s via `dig-nat`) responsibility.
78//!
79//! [`dig-peer`]: https://github.com/DIG-Network/dig-peer
80//! [`dig_peer::PeerTarget`]: https://docs.rs/dig-peer/latest/dig_peer/struct.PeerTarget.html
81//! [`dig_peer::DigPeer::connect`]: https://docs.rs/dig-peer/latest/dig_peer/struct.DigPeer.html#method.connect
82//! [`dig_peer::Connected`]: https://docs.rs/dig-peer/latest/dig_peer/struct.Connected.html
83//!
84//! Because `TransferOutcome`/`Selection` are defined here structurally, this crate does NOT depend on
85//! `dig-download` — avoiding a dependency cycle; the event→outcome mapping lives in the node adapter
86//! (SPEC §11).
87//!
88//! [`dig-download`]: https://github.com/DIG-Network/dig-download
89//! [`dig-dht`]: https://github.com/DIG-Network/dig-dht
90//! [`dig-nat`]: https://github.com/DIG-Network/dig-nat
91//! [`dig-gossip`]: https://github.com/DIG-Network/dig-gossip
92//! [`ProviderRecord`]: dig_dht::ProviderRecord
93
94#![forbid(unsafe_code)]
95#![warn(missing_docs)]
96
97pub mod config;
98pub mod engine;
99pub mod observe;
100pub mod pool_event;
101pub mod quality;
102pub mod registry;
103pub mod scoring;
104pub mod types;
105
106// ---- The frozen public surface (SPEC §11) --------------------------------------------------------
107
108pub use config::{ClockSource, SelectorConfig, DEFAULT_REGISTRY_CAPACITY};
109pub use engine::PeerSelector;
110pub use observe::{peer_id_hex, PeerSnapshot, SelectorSnapshot};
111pub use pool_event::{PoolEvent, PoolRemovalReason};
112pub use quality::{Estimate, PeerQuality, Reliability};
113pub use registry::{FeedResult, PeerEntry, DISPATCH_TTL_SECS};
114pub use scoring::{PeerClass, RelayModel, SaturationModel};
115pub use types::{
116    Candidate, ContentRequest, FailureReason, OutcomeKind, OutcomeResult, Provenance,
117    RangePlanDelta, SelectedPeer, Selection, TransferOutcome,
118};
119
120// ---- Re-used from the sibling crates (NOT redefined — SPEC §5, §11) ------------------------------
121
122/// The candidate/content types re-used from `dig-dht` (SPEC §7.1): what is fetched + how a provider
123/// is addressed.
124pub use dig_dht::{AddressKind, CandidateAddr, ContentId, ProviderRecord};
125/// The transport-verified peer identity (`peer_id = SHA-256(TLS SPKI DER)`), re-used from `dig-nat`.
126pub use dig_nat::PeerId;
127/// The `dig-nat` connection-class ladder the selector reads observationally (SPEC §7.3).
128pub use dig_nat::TraversalKind;