use std::collections::HashMap;
use dig_peer_selector::{
Candidate, ContentId, ContentRequest, FailureReason, OutcomeKind, OutcomeResult, PeerId,
PeerSelector, PoolEvent, PoolRemovalReason, Provenance, RangePlanDelta, SelectorConfig,
TransferOutcome, TraversalKind,
};
use dig_dht::CandidateAddr;
const STORE: [u8; 32] = [0x42; 32];
fn content() -> ContentId {
ContentId::store(STORE)
}
fn pid(b: u8) -> PeerId {
PeerId::from_bytes([b; 32])
}
fn candidate(b: u8) -> Candidate {
Candidate::new(pid(b), vec![CandidateAddr::direct("10.0.0.1", 9444)])
}
fn candidate_classed(b: u8, class: TraversalKind) -> Candidate {
let mut c = candidate(b);
c.class = Some(class);
c
}
#[derive(Clone, Copy)]
struct SynthPeer {
throughput: f64,
reliable: bool,
saturation_ceiling: Option<u32>,
}
impl SynthPeer {
fn fast_reliable(tput: f64) -> Self {
SynthPeer {
throughput: tput,
reliable: true,
saturation_ceiling: None,
}
}
fn with_ceiling(tput: f64, ceiling: u32) -> Self {
SynthPeer {
throughput: tput,
reliable: true,
saturation_ceiling: Some(ceiling),
}
}
fn measured(&self, concurrency: u32) -> f64 {
match self.saturation_ceiling {
Some(c) if concurrency > c => self.throughput * (c as f64) / (concurrency as f64),
_ => self.throughput,
}
}
}
fn drive(
sel: &PeerSelector,
topo: &HashMap<u8, SynthPeer>,
parallelism: usize,
rounds: usize,
at_start: u64,
) -> HashMap<u8, usize> {
let mut served: HashMap<u8, usize> = HashMap::new();
let cands: Vec<Candidate> = topo.keys().map(|&b| candidate(b)).collect();
for r in 0..rounds {
let at = at_start + r as u64;
let req = ContentRequest::new(content(), parallelism);
let selection = sel.select(&req, &cands);
for sp in &selection.peers {
let b = sp.peer_id.as_bytes()[0];
let synth = topo[&b];
let conc = sp.max_concurrency;
let tput = synth.measured(conc);
let ok = synth.reliable;
*served.entry(b).or_insert(0) += 1;
let outcome = TransferOutcome {
peer_id: sp.peer_id,
content: content(),
kind: OutcomeKind::Range {
index: r,
offset: 0,
length: 1_000_000,
},
result: if ok {
OutcomeResult::Success
} else {
OutcomeResult::Failure {
reason: FailureReason::Timeout,
}
},
bytes: (tput as u64).max(1),
duration_ms: 1000,
rtt_ms: Some(20),
at,
};
sel.record_outcome(&outcome);
}
}
served
}
#[test]
fn converges_to_fast_reliable() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 7));
let mut topo = HashMap::new();
topo.insert(1u8, SynthPeer::fast_reliable(1_000_000.0)); topo.insert(2u8, SynthPeer::fast_reliable(500_000.0)); topo.insert(3u8, SynthPeer::fast_reliable(50_000.0));
let served = drive(&sel, &topo, 2, 60, 1000);
let fast = served.get(&1).copied().unwrap_or(0);
let slow = served.get(&3).copied().unwrap_or(0);
assert!(
fast > slow,
"fast peer must serve more ranges than slow (fast {fast}, slow {slow})"
);
let final_sel = sel.select(
&ContentRequest::new(content(), 3),
&[candidate(1), candidate(2), candidate(3)],
);
assert_eq!(
final_sel.best().unwrap().peer_id,
pid(1),
"the fast, reliable peer must be ranked best after convergence"
);
}
#[test]
fn load_spread_off_saturating_peer() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 11));
let mut topo = HashMap::new();
topo.insert(1u8, SynthPeer::with_ceiling(2_000_000.0, 2));
topo.insert(2u8, SynthPeer::fast_reliable(800_000.0));
topo.insert(3u8, SynthPeer::fast_reliable(700_000.0));
drive(&sel, &topo, 4, 50, 1000);
let req = ContentRequest::new(content(), 6);
let selection = sel.select(&req, &[candidate(1), candidate(2), candidate(3)]);
assert!(
selection.len() >= 2,
"load must spread across multiple peers, not pile on one (got {} peers)",
selection.len()
);
let top = selection
.peers
.iter()
.find(|p| p.peer_id == pid(1))
.expect("the saturating peer should still be selected");
assert!(
top.max_concurrency <= 4,
"the saturating peer's recommended concurrency must be capped near its learned ceiling, got {}",
top.max_concurrency
);
let total: u32 = selection.peers.iter().map(|p| p.max_concurrency).sum();
assert!(
total > top.max_concurrency,
"excess demand must spill to other peers (total {total}, top cap {})",
top.max_concurrency
);
}
#[test]
fn degradation_adaptation() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 13));
let mut topo = HashMap::new();
topo.insert(1u8, SynthPeer::fast_reliable(1_500_000.0));
topo.insert(2u8, SynthPeer::fast_reliable(600_000.0));
drive(&sel, &topo, 1, 30, 1000);
let before = sel.select(
&ContentRequest::new(content(), 2),
&[candidate(1), candidate(2)],
);
assert_eq!(
before.best().unwrap().peer_id,
pid(1),
"peer 1 best while fast"
);
topo.insert(1u8, SynthPeer::fast_reliable(50_000.0));
drive(&sel, &topo, 1, 20, 2000);
let after = sel.select(
&ContentRequest::new(content(), 2),
&[candidate(1), candidate(2)],
);
assert_eq!(
after.best().unwrap().peer_id,
pid(2),
"ranking must move to the now-faster peer 2 after peer 1 degrades"
);
topo.insert(1u8, SynthPeer::fast_reliable(2_000_000.0));
drive(&sel, &topo, 1, 25, 3000);
let recovered = sel.select(
&ContentRequest::new(content(), 2),
&[candidate(1), candidate(2)],
);
assert_eq!(
recovered.best().unwrap().peer_id,
pid(1),
"a recovered peer must rise again"
);
}
#[test]
fn p99_orientation_beats_naive() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 17));
for i in 0..40 {
let at = 1000u64 + i as u64;
let t1 = if i % 2 == 0 { 2_000_000.0 } else { 100_000.0 };
let t2 = 900_000.0;
for (p, t) in [(1u8, t1), (2u8, t2)] {
sel.record_outcome(&TransferOutcome {
peer_id: pid(p),
content: content(),
kind: OutcomeKind::Range {
index: i,
offset: 0,
length: 1_000_000,
},
result: OutcomeResult::Success,
bytes: t as u64,
duration_ms: 1000,
rtt_ms: Some(20),
at,
});
}
}
let s1 = sel.peer_snapshot(&pid(1)).unwrap();
let s2 = sel.peer_snapshot(&pid(2)).unwrap();
assert!(
s1.throughput_volatility > s2.throughput_volatility,
"the heavy-tailed peer must register higher volatility (p1 {}, p2 {})",
s1.throughput_volatility,
s2.throughput_volatility
);
let mut steady_best_or_second = 0;
for _ in 0..10 {
let sel_now = sel.select(
&ContentRequest::new(content(), 2),
&[candidate(1), candidate(2)],
);
if sel_now.peers.iter().any(|p| p.peer_id == pid(2)) {
steady_best_or_second += 1;
}
}
assert_eq!(
steady_best_or_second, 10,
"the steady peer must remain a first-class source under P99 orientation, never starved"
);
}
#[test]
fn bounded_exploration_of_cold_peers() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 19));
let mut topo = HashMap::new();
for b in 1u8..=5 {
topo.insert(b, SynthPeer::fast_reliable(500_000.0));
}
let served = drive(&sel, &topo, 3, 40, 1000);
for b in 1u8..=5 {
assert!(
served.get(&b).copied().unwrap_or(0) > 0,
"every candidate in an all-cold network must be tried (peer {b} never served)"
);
}
let sel2 = PeerSelector::new(SelectorConfig::deterministic(1000, 23));
let mut warm = HashMap::new();
warm.insert(1u8, SynthPeer::fast_reliable(2_000_000.0));
drive(&sel2, &warm, 1, 30, 1000);
let cands: Vec<Candidate> = (1u8..=8).map(candidate).collect();
let selection = sel2.select(&ContentRequest::new(content(), 6), &cands);
let explore_count = selection.peers.iter().filter(|p| p.exploratory).count();
assert!(
explore_count <= 2,
"cold peers must not crowd out the proven peer (got {explore_count} exploratory of {})",
selection.len()
);
assert!(
selection
.peers
.iter()
.any(|p| p.peer_id == pid(1) && !p.exploratory),
"the proven fast peer must be selected as a non-exploratory source"
);
assert_eq!(
selection.best().unwrap().peer_id,
pid(1),
"the proven fast peer must rank best over cold peers"
);
}
#[test]
fn seed_determinism() {
let run = || -> Vec<Vec<(PeerId, u32, bool)>> {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 99));
let mut topo = HashMap::new();
for b in 1u8..=6 {
topo.insert(
b,
SynthPeer::fast_reliable(300_000.0 + b as f64 * 100_000.0),
);
}
let cands: Vec<Candidate> = (1u8..=6).map(candidate).collect();
let mut history = Vec::new();
for r in 0..20 {
let at = 1000u64 + r as u64;
let selection = sel.select(&ContentRequest::new(content(), 3), &cands);
history.push(
selection
.peers
.iter()
.map(|p| (p.peer_id, p.rank, p.exploratory))
.collect(),
);
for sp in &selection.peers {
sel.record_outcome(&TransferOutcome {
peer_id: sp.peer_id,
content: content(),
kind: OutcomeKind::Range {
index: r,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: (300_000 + sp.peer_id.as_bytes()[0] as u64 * 100_000).max(1),
duration_ms: 1000,
rtt_ms: Some(10),
at,
});
}
}
history
};
let a = run();
let b = run();
assert_eq!(
a, b,
"identical seed + clock + outcome stream must yield identical rankings"
);
}
#[test]
fn anti_gaming_measured_over_advertised() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 29));
let mut topo = HashMap::new();
topo.insert(1u8, SynthPeer::fast_reliable(80_000.0)); topo.insert(2u8, SynthPeer::fast_reliable(1_500_000.0)); drive(&sel, &topo, 1, 30, 1000);
let ranked = sel.select(
&ContentRequest::new(content(), 2),
&[candidate(1), candidate(2)],
);
assert_eq!(
ranked.best().unwrap().peer_id,
pid(2),
"the peer that MEASURES fast must outrank the one that only 'advertises' fast"
);
let sel2 = PeerSelector::new(SelectorConfig::deterministic(1000, 31));
let mut at = 1000u64;
for i in 0..5 {
sel2.record_outcome(&TransferOutcome {
peer_id: pid(3),
content: content(),
kind: OutcomeKind::Range {
index: i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 1_000_000,
duration_ms: 1000,
rtt_ms: Some(10),
at,
});
at += 1;
}
for i in 0..8 {
sel2.record_outcome(&TransferOutcome {
peer_id: pid(3),
content: content(),
kind: OutcomeKind::Range {
index: 100 + i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Failure {
reason: FailureReason::VerificationFailed,
},
bytes: 0,
duration_ms: 0,
rtt_ms: None,
at,
});
at += 1;
}
let ranked2 = sel2.select(
&ContentRequest::new(content(), 2),
&[candidate(3), candidate(4)],
);
assert_eq!(
ranked2.best().unwrap().peer_id,
pid(4),
"a fresh cold peer must rank ahead of a verification-failing (bad/hostile) source"
);
}
#[test]
fn rebalance_replaces_dropped_source() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 37));
let mut topo = HashMap::new();
topo.insert(1u8, SynthPeer::fast_reliable(1_000_000.0));
topo.insert(2u8, SynthPeer::fast_reliable(900_000.0));
drive(&sel, &topo, 2, 30, 1000);
sel.upsert_candidate(&candidate(3));
for i in 0..10 {
sel.record_outcome(&TransferOutcome {
peer_id: pid(3),
content: content(),
kind: OutcomeKind::Range {
index: 200 + i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 950_000,
duration_ms: 1000,
rtt_ms: Some(15),
at: 2000 + i as u64,
});
}
let req = ContentRequest::new(content(), 3);
let need = RangePlanDelta::of_count(3);
let replacement = sel.rebalance(&req, &[pid(1), pid(2)], &need);
assert!(
!replacement.is_empty(),
"rebalance must return a replacement subset for the still-needed ranges"
);
assert!(
replacement.peers.iter().any(|p| p.peer_id == pid(3)),
"rebalance must be able to pick the freshly-learned replacement peer 3"
);
}
#[test]
fn sel_01_public_api_shapes() {
let sel = PeerSelector::new(SelectorConfig::default());
let addr = "203.0.113.5:9444".parse().unwrap();
sel.on_pool_event(&PoolEvent::PeerAdded {
peer_id: pid(1),
addr,
});
sel.on_connection_class(&pid(1), TraversalKind::Direct);
sel.upsert_candidate(&candidate(2));
sel.on_pool_event(&PoolEvent::PeerRemoved {
peer_id: pid(1),
reason: PoolRemovalReason::Disconnected,
});
let req = ContentRequest {
content: content(),
total_length: Some(10_000),
range_count: Some(4),
parallelism: 2,
};
let selection = sel.select(&req, &[candidate(2), candidate(3)]);
for p in &selection.peers {
let _ = (p.peer_id, p.rank, p.max_concurrency, p.exploratory);
}
sel.record_outcome(&TransferOutcome {
peer_id: pid(2),
content: content(),
kind: OutcomeKind::Range {
index: 0,
offset: 0,
length: 2500,
},
result: OutcomeResult::Success,
bytes: 2500,
duration_ms: 100,
rtt_ms: Some(12),
at: 5,
});
sel.record_outcome(&TransferOutcome {
peer_id: pid(3),
content: content(),
kind: OutcomeKind::Request {
total_length: 10_000,
},
result: OutcomeResult::Interrupted { bytes_before: 4000 },
bytes: 4000,
duration_ms: 500,
rtt_ms: None,
at: 6,
});
let _ = sel.rebalance(&req, &[pid(2)], &RangePlanDelta::of_indices([1, 2, 3]));
sel.remove_peer(&pid(3));
let _snap = sel.snapshot();
assert!(sel.registry_size() >= 1);
let _reuse: dig_nat::PeerId = pid(1);
let _content: dig_dht::ContentId = content();
let _prov = Provenance::Dht;
}
#[test]
fn sel_02_registry_churn_and_bounds() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 3));
sel.on_pool_event(&PoolEvent::PeerAdded {
peer_id: pid(1),
addr: "10.0.0.1:1".parse().unwrap(),
});
sel.record_outcome(&TransferOutcome {
peer_id: pid(1),
content: content(),
kind: OutcomeKind::Range {
index: 0,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 500_000,
duration_ms: 1000,
rtt_ms: Some(10),
at: 1000,
});
sel.on_pool_event(&PoolEvent::PeerRemoved {
peer_id: pid(1),
reason: PoolRemovalReason::Disconnected,
});
let snap = sel
.peer_snapshot(&pid(1))
.expect("entry retained across disconnect");
assert!(
snap.throughput_bps.is_some(),
"learned quality retained across disconnect"
);
assert!(!snap.connected);
sel.on_connection_class(&pid(1), TraversalKind::Relayed);
let snap = sel.peer_snapshot(&pid(1)).unwrap();
assert_eq!(snap.connection_class.as_deref(), Some("relayed"));
}
#[test]
fn sel_02_dispatch_then_silence_stays_within_capacity_bound() {
let capacity = 8usize;
let cfg = SelectorConfig {
registry_capacity: capacity,
..SelectorConfig::deterministic(1000, 7)
};
let sel = PeerSelector::new(cfg);
let n = capacity as u8 * 5;
for b in 0..n {
let _ = sel.select(&ContentRequest::new(content(), 3), &[candidate(b)]);
assert!(
sel.registry_size() <= capacity,
"registry must stay bounded at {capacity} after feeding {} silent dispatched peers; got {}",
b + 1,
sel.registry_size()
);
}
assert!(
sel.registry_size() <= capacity,
"final registry size {} must not exceed capacity {capacity}",
sel.registry_size()
);
}
#[test]
fn sel_02_side_maps_are_pruned_on_registry_eviction() {
let capacity = 6usize;
let clock = dig_peer_selector::ClockSource::manual(2000);
let cfg = SelectorConfig {
registry_capacity: capacity,
clock: clock.clone(),
..SelectorConfig::deterministic(2000, 11)
};
let sel = PeerSelector::new(cfg);
let n: u16 = capacity as u16 * 20;
for i in 0..n {
let b = (i % 256) as u8;
clock.advance(dig_peer_selector::DISPATCH_TTL_SECS + 1);
let _ = sel.select(&ContentRequest::new(content(), 2), &[candidate(b)]);
}
let snap = sel.snapshot();
assert!(
snap.registry_size <= capacity,
"registry itself must stay bounded: {}",
snap.registry_size
);
assert!(
snap.last_selected_len <= capacity * 4,
"last_selected must be pruned on eviction, not grow with total peers ever seen ({} entries fed, {} still tracked)",
n,
snap.last_selected_len
);
assert!(
snap.dispatched_len <= capacity * 4,
"dispatched must be pruned on eviction, not grow with total peers ever seen ({} entries fed, {} still tracked)",
n,
snap.dispatched_len
);
}
#[test]
fn sel_07_download_loop_contract() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 41));
for i in 0..6 {
sel.record_outcome(&TransferOutcome {
peer_id: pid(1),
content: content(),
kind: OutcomeKind::Range {
index: i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 1_000_000,
duration_ms: 1000,
rtt_ms: Some(10),
at: 1000 + i as u64,
});
}
let good = sel.peer_snapshot(&pid(1)).unwrap().reliability.unwrap();
sel.record_outcome(&TransferOutcome {
peer_id: pid(1),
content: content(),
kind: OutcomeKind::Range {
index: 99,
offset: 0,
length: 1000,
},
result: OutcomeResult::Failure {
reason: FailureReason::VerificationFailed,
},
bytes: 0,
duration_ms: 0,
rtt_ms: None,
at: 2000,
});
let after_hard = sel.peer_snapshot(&pid(1)).unwrap().reliability.unwrap();
assert!(
after_hard < good,
"a verification failure must drop reliability sharply"
);
assert_eq!(sel.peer_snapshot(&pid(1)).unwrap().hard_failures, 1);
let sel2 = PeerSelector::new(SelectorConfig::deterministic(1000, 43));
for i in 0..6 {
sel2.record_outcome(&TransferOutcome {
peer_id: pid(2),
content: content(),
kind: OutcomeKind::Range {
index: i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 1_000_000,
duration_ms: 1000,
rtt_ms: Some(10),
at: 1000 + i as u64,
});
}
let before = sel2.peer_snapshot(&pid(2)).unwrap().reliability.unwrap();
sel2.record_outcome(&TransferOutcome {
peer_id: pid(2),
content: content(),
kind: OutcomeKind::Range {
index: 50,
offset: 0,
length: 1000,
},
result: OutcomeResult::Failure {
reason: FailureReason::Cancelled,
},
bytes: 0,
duration_ms: 0,
rtt_ms: None,
at: 2000,
});
let after = sel2.peer_snapshot(&pid(2)).unwrap().reliability.unwrap();
assert_eq!(
before, after,
"a host-cancel must not penalize the peer's reliability"
);
}
#[test]
fn sel_08_relayed_prior_does_not_preempt_measurement() {
let sel = PeerSelector::new(SelectorConfig::deterministic(1000, 47));
let relayed_fast = candidate_classed(1, TraversalKind::Relayed);
let direct_slow = candidate_classed(2, TraversalKind::Direct);
sel.upsert_candidate(&relayed_fast);
sel.upsert_candidate(&direct_slow);
for i in 0..25 {
sel.record_outcome(&TransferOutcome {
peer_id: pid(1),
content: content(),
kind: OutcomeKind::Range {
index: i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 1_800_000, duration_ms: 1000,
rtt_ms: Some(10),
at: 1000 + i as u64,
});
sel.record_outcome(&TransferOutcome {
peer_id: pid(2),
content: content(),
kind: OutcomeKind::Range {
index: i,
offset: 0,
length: 1000,
},
result: OutcomeResult::Success,
bytes: 120_000, duration_ms: 1000,
rtt_ms: Some(10),
at: 1000 + i as u64,
});
}
let ranked = sel.select(
&ContentRequest::new(content(), 2),
&[
candidate_classed(1, TraversalKind::Relayed),
candidate_classed(2, TraversalKind::Direct),
],
);
assert_eq!(
ranked.best().unwrap().peer_id,
pid(1),
"a relayed peer that measures fast must outrank a direct peer that measures slow (measured quality dominates the class prior)"
);
}
#[test]
fn sel_10_config_is_wiring_only() {
let cfg = SelectorConfig {
clock: dig_peer_selector::ClockSource::manual(0),
rng_seed: Some(1),
registry_capacity: 10,
};
let sel = PeerSelector::new(cfg);
assert!(sel.registry_size() == 0);
}