use dig_dht::{CandidateAddr, ContentId, ProviderRecord};
use dig_nat::{PeerId, TraversalKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
pub enum Provenance {
Dht,
Gossip,
Pex,
Nat,
Manual,
}
impl Provenance {
pub(crate) fn evidence(self) -> u8 {
match self {
Provenance::Manual => 0,
Provenance::Pex => 1,
Provenance::Dht => 2,
Provenance::Gossip => 3,
Provenance::Nat => 4,
}
}
}
#[derive(Debug, Clone)]
pub struct ContentRequest {
pub content: ContentId,
pub total_length: Option<u64>,
pub range_count: Option<usize>,
pub parallelism: usize,
}
impl ContentRequest {
pub fn new(content: ContentId, parallelism: usize) -> Self {
ContentRequest {
content,
total_length: None,
range_count: None,
parallelism: parallelism.max(1),
}
}
pub fn effective_parallelism(&self) -> usize {
self.parallelism.max(1)
}
}
#[derive(Debug, Clone)]
pub struct Candidate {
pub peer_id: PeerId,
pub addresses: Vec<CandidateAddr>,
pub class: Option<TraversalKind>,
}
impl Candidate {
pub fn new(peer_id: PeerId, addresses: Vec<CandidateAddr>) -> Self {
Candidate {
peer_id,
addresses,
class: None,
}
}
pub fn from_provider_record(rec: &ProviderRecord) -> Option<Self> {
PeerId::from_hex(&rec.provider_peer_id)
.map(|peer_id| Candidate::new(peer_id, rec.addresses.clone()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectedPeer {
pub peer_id: PeerId,
pub rank: u32,
pub max_concurrency: u32,
pub exploratory: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Selection {
pub peers: Vec<SelectedPeer>,
}
impl Selection {
pub fn empty() -> Self {
Selection { peers: Vec::new() }
}
pub fn is_empty(&self) -> bool {
self.peers.is_empty()
}
pub fn len(&self) -> usize {
self.peers.len()
}
pub fn best(&self) -> Option<&SelectedPeer> {
self.peers.first()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutcomeKind {
Range {
index: usize,
offset: u64,
length: u64,
},
Request {
total_length: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutcomeResult {
Success,
Failure {
reason: FailureReason,
},
Interrupted {
bytes_before: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureReason {
Timeout,
Transport,
Unavailable,
VerificationFailed,
Cancelled,
Other,
}
impl FailureReason {
pub fn is_hard(self) -> bool {
matches!(self, FailureReason::VerificationFailed)
}
pub fn blames_peer(self) -> bool {
!matches!(self, FailureReason::Cancelled)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransferOutcome {
pub peer_id: PeerId,
pub content: ContentId,
pub kind: OutcomeKind,
pub result: OutcomeResult,
pub bytes: u64,
pub duration_ms: u64,
pub rtt_ms: Option<u64>,
pub at: u64,
}
impl TransferOutcome {
pub fn throughput_bps(&self) -> Option<f64> {
if self.bytes == 0 || self.duration_ms == 0 {
return None;
}
Some((self.bytes as f64) * 1000.0 / (self.duration_ms as f64))
}
pub fn is_success(&self) -> bool {
matches!(self.result, OutcomeResult::Success)
}
pub fn is_hard_failure(&self) -> bool {
matches!(
self.result,
OutcomeResult::Failure {
reason: FailureReason::VerificationFailed
}
)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RangePlanDelta {
pub needed_ranges: Vec<usize>,
}
impl RangePlanDelta {
pub fn of_count(count: usize) -> Self {
RangePlanDelta {
needed_ranges: (0..count).collect(),
}
}
pub fn of_indices(indices: impl IntoIterator<Item = usize>) -> Self {
RangePlanDelta {
needed_ranges: indices.into_iter().collect(),
}
}
pub fn len(&self) -> usize {
self.needed_ranges.len()
}
pub fn is_empty(&self) -> bool {
self.needed_ranges.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pid(b: u8) -> PeerId {
PeerId::from_bytes([b; 32])
}
const S: [u8; 32] = [0x11; 32];
#[test]
fn provenance_evidence_orders_live_links_above_hints() {
assert!(Provenance::Nat.evidence() > Provenance::Gossip.evidence());
assert!(Provenance::Gossip.evidence() > Provenance::Dht.evidence());
assert!(Provenance::Dht.evidence() > Provenance::Pex.evidence());
assert!(Provenance::Pex.evidence() > Provenance::Manual.evidence());
}
#[test]
fn content_request_clamps_parallelism_to_at_least_one() {
let r = ContentRequest::new(ContentId::store(S), 0);
assert_eq!(r.effective_parallelism(), 1);
let r = ContentRequest::new(ContentId::store(S), 5);
assert_eq!(r.effective_parallelism(), 5);
}
#[test]
fn candidate_from_provider_record_round_trips_peer_id() {
let rec = ProviderRecord::new(
&dig_dht::ContentId::store(S).to_key(),
&dig_dht::PeerId::from_bytes([7; 32]),
vec![CandidateAddr::direct("203.0.113.7", 9444)],
1000,
);
let cand = Candidate::from_provider_record(&rec).unwrap();
assert_eq!(cand.peer_id, pid(7));
assert_eq!(cand.addresses.len(), 1);
}
#[test]
fn throughput_derived_only_from_measured_bytes_and_duration() {
let o = TransferOutcome {
peer_id: pid(1),
content: ContentId::store(S),
kind: OutcomeKind::Range {
index: 0,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 1000,
duration_ms: 1000,
rtt_ms: Some(20),
at: 5,
};
assert_eq!(o.throughput_bps(), Some(1000.0));
assert!(o.is_success());
assert!(!o.is_hard_failure());
}
#[test]
fn zero_duration_or_zero_bytes_has_no_throughput() {
let mut o = TransferOutcome {
peer_id: pid(1),
content: ContentId::store(S),
kind: OutcomeKind::Request { total_length: 0 },
result: OutcomeResult::Success,
bytes: 0,
duration_ms: 1000,
rtt_ms: None,
at: 1,
};
assert_eq!(o.throughput_bps(), None);
o.bytes = 1000;
o.duration_ms = 0;
assert_eq!(o.throughput_bps(), None);
}
#[test]
fn verification_failed_is_hard_and_blames_peer() {
assert!(FailureReason::VerificationFailed.is_hard());
assert!(FailureReason::VerificationFailed.blames_peer());
assert!(!FailureReason::Timeout.is_hard());
assert!(!FailureReason::Cancelled.blames_peer());
assert!(FailureReason::Timeout.blames_peer());
}
#[test]
fn range_plan_delta_constructors() {
assert_eq!(RangePlanDelta::of_count(3).needed_ranges, vec![0, 1, 2]);
assert_eq!(RangePlanDelta::of_indices([4, 9]).needed_ranges, vec![4, 9]);
assert!(RangePlanDelta::default().is_empty());
assert_eq!(RangePlanDelta::of_count(2).len(), 2);
}
#[test]
fn selection_helpers() {
let mut s = Selection::empty();
assert!(s.is_empty());
assert!(s.best().is_none());
s.peers.push(SelectedPeer {
peer_id: pid(1),
rank: 0,
max_concurrency: 2,
exploratory: false,
});
assert_eq!(s.len(), 1);
assert_eq!(s.best().unwrap().peer_id, pid(1));
}
}