use std::collections::HashSet;
use std::net::SocketAddr;
pub use irontide_core::{ChokingAlgorithm, SeedChokingAlgorithm};
#[derive(Debug, Clone)]
pub(crate) struct PeerInfo {
pub addr: SocketAddr,
pub download_rate: u64,
pub upload_rate: u64,
pub interested: bool,
pub upload_only: bool,
pub is_seed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ChokeDecision {
pub to_unchoke: Vec<SocketAddr>,
pub to_choke: Vec<SocketAddr>,
}
pub(crate) trait ChokerStrategy: Send + Sync {
fn decide(
&mut self,
peers: &[PeerInfo],
unchoke_slots: usize,
seed_mode: bool,
) -> ChokeDecision;
fn rotate_optimistic(&mut self, peers: &[PeerInfo]);
fn observe_throughput(&mut self, _throughput: u64) {}
#[allow(dead_code)] fn dynamic_slots(&self) -> Option<usize> {
None
}
}
pub(crate) struct FixedSlotsStrategy {
optimistic_peer: Option<SocketAddr>,
seed_algorithm: SeedChokingAlgorithm,
round_robin_offset: usize,
unchoked: HashSet<SocketAddr>,
}
impl FixedSlotsStrategy {
pub fn new(seed_algorithm: SeedChokingAlgorithm) -> Self {
Self {
optimistic_peer: None,
seed_algorithm,
round_robin_offset: 0,
unchoked: HashSet::new(),
}
}
fn select_optimistic(
&self,
interested: &[&PeerInfo],
regular_unchokes: &[SocketAddr],
) -> Option<SocketAddr> {
if let Some(opt) = self.optimistic_peer {
let still_interested = interested.iter().any(|p| p.addr == opt && !p.upload_only);
let already_regular = regular_unchokes.contains(&opt);
if still_interested && !already_regular {
return Some(opt);
}
}
interested
.iter()
.find(|p| !regular_unchokes.contains(&p.addr) && !p.upload_only)
.map(|p| p.addr)
}
fn sort_seed_mode(
&mut self,
interested: &mut [&PeerInfo],
unchoke_slots: usize,
incumbents: &HashSet<SocketAddr>,
) {
match self.seed_algorithm {
SeedChokingAlgorithm::FastestUpload => {
interested.sort_by_key(|p| {
std::cmp::Reverse((p.upload_rate, incumbents.contains(&p.addr)))
});
}
SeedChokingAlgorithm::RoundRobin => {
interested.sort_by_key(|p| p.addr);
if !interested.is_empty() {
let offset = self.round_robin_offset % interested.len();
interested.rotate_left(offset);
self.round_robin_offset =
self.round_robin_offset.wrapping_add(unchoke_slots) % interested.len();
}
}
SeedChokingAlgorithm::AntiLeech => {
interested.sort_by(|a, b| match (a.is_seed, b.is_seed) {
(false, true) => std::cmp::Ordering::Less,
(true, false) => std::cmp::Ordering::Greater,
_ => {
let ka = (a.upload_rate, incumbents.contains(&a.addr));
let kb = (b.upload_rate, incumbents.contains(&b.addr));
kb.cmp(&ka)
}
});
}
}
}
}
impl ChokerStrategy for FixedSlotsStrategy {
fn decide(
&mut self,
peers: &[PeerInfo],
unchoke_slots: usize,
seed_mode: bool,
) -> ChokeDecision {
let all_addrs: Vec<SocketAddr> = peers.iter().map(|p| p.addr).collect();
let incumbents = std::mem::take(&mut self.unchoked);
let mut interested: Vec<&PeerInfo> = peers.iter().filter(|p| p.interested).collect();
if seed_mode {
self.sort_seed_mode(&mut interested, unchoke_slots, &incumbents);
} else {
interested.sort_by_key(|p| {
std::cmp::Reverse((p.download_rate, incumbents.contains(&p.addr)))
});
}
let regular_count = unchoke_slots.min(interested.len());
let regular_unchokes: Vec<SocketAddr> =
interested[..regular_count].iter().map(|p| p.addr).collect();
self.unchoked = regular_unchokes.iter().copied().collect();
let optimistic = self.select_optimistic(&interested, ®ular_unchokes);
self.optimistic_peer = optimistic;
let mut to_unchoke = regular_unchokes;
if let Some(opt) = optimistic
&& !to_unchoke.contains(&opt)
{
to_unchoke.push(opt);
}
let to_choke: Vec<SocketAddr> = all_addrs
.into_iter()
.filter(|a| !to_unchoke.contains(a))
.collect();
ChokeDecision {
to_unchoke,
to_choke,
}
}
fn rotate_optimistic(&mut self, peers: &[PeerInfo]) {
let mut interested: Vec<&PeerInfo> = peers
.iter()
.filter(|p| p.interested && !p.upload_only)
.collect();
interested.sort_by_key(|p| p.download_rate);
self.optimistic_peer = interested
.iter()
.find(|p| Some(p.addr) != self.optimistic_peer)
.map(|p| p.addr);
}
}
pub(crate) struct RateBasedStrategy {
inner: FixedSlotsStrategy,
dynamic_slots: usize,
prev_throughput: u64,
upload_rate_limit: u64,
min_slots: usize,
max_slots: usize,
}
impl RateBasedStrategy {
pub fn new(
seed_algorithm: SeedChokingAlgorithm,
upload_rate_limit: u64,
min_slots: usize,
max_slots: usize,
) -> Self {
Self {
inner: FixedSlotsStrategy::new(seed_algorithm),
dynamic_slots: min_slots.max(2),
prev_throughput: 0,
upload_rate_limit,
min_slots,
max_slots,
}
}
fn observe_throughput_inner(&mut self, throughput: u64) {
if self.prev_throughput == 0 && throughput > 0 {
self.prev_throughput = throughput;
return;
}
if throughput > self.prev_throughput {
let at_capacity =
self.upload_rate_limit > 0 && throughput > self.upload_rate_limit * 90 / 100;
if !at_capacity && self.dynamic_slots < self.max_slots {
self.dynamic_slots += 1;
}
} else if self.prev_throughput > 0 {
let threshold = self.prev_throughput * 90 / 100;
if throughput < threshold && self.dynamic_slots > self.min_slots {
self.dynamic_slots -= 1;
}
}
if self.upload_rate_limit > 0
&& throughput < self.upload_rate_limit
&& self.dynamic_slots > 0
{
let per_slot_avg = throughput / self.dynamic_slots as u64;
if per_slot_avg > 0 {
let headroom = self.upload_rate_limit - throughput;
if headroom > per_slot_avg && self.dynamic_slots < self.max_slots {
self.dynamic_slots += 1;
}
}
}
self.prev_throughput = throughput;
}
#[cfg(test)]
pub fn current_slots(&self) -> usize {
self.dynamic_slots
}
}
impl ChokerStrategy for RateBasedStrategy {
fn decide(
&mut self,
peers: &[PeerInfo],
_unchoke_slots: usize,
seed_mode: bool,
) -> ChokeDecision {
self.inner.decide(peers, self.dynamic_slots, seed_mode)
}
fn rotate_optimistic(&mut self, peers: &[PeerInfo]) {
self.inner.rotate_optimistic(peers);
}
fn observe_throughput(&mut self, throughput: u64) {
self.observe_throughput_inner(throughput);
}
fn dynamic_slots(&self) -> Option<usize> {
Some(self.dynamic_slots)
}
}
pub(crate) struct Choker {
strategy: Box<dyn ChokerStrategy>,
unchoke_slots: usize,
seed_mode: bool,
#[allow(dead_code)] choking_algorithm: ChokingAlgorithm,
}
impl Choker {
#[cfg(test)]
pub fn new(unchoke_slots: usize) -> Self {
Self {
strategy: Box::new(FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload)),
unchoke_slots,
seed_mode: false,
choking_algorithm: ChokingAlgorithm::FixedSlots,
}
}
pub fn with_algorithms(
unchoke_slots: usize,
seed_algorithm: SeedChokingAlgorithm,
choking_algorithm: ChokingAlgorithm,
upload_rate_limit: u64,
min_slots: usize,
max_slots: usize,
) -> Self {
let strategy: Box<dyn ChokerStrategy> = match choking_algorithm {
ChokingAlgorithm::FixedSlots => Box::new(FixedSlotsStrategy::new(seed_algorithm)),
ChokingAlgorithm::RateBased => Box::new(RateBasedStrategy::new(
seed_algorithm,
upload_rate_limit,
min_slots,
max_slots,
)),
};
Self {
strategy,
unchoke_slots,
seed_mode: false,
choking_algorithm,
}
}
pub fn set_seed_mode(&mut self, seed_mode: bool) {
self.seed_mode = seed_mode;
}
pub fn set_unchoke_slots(&mut self, n: usize) {
self.unchoke_slots = n;
}
#[allow(dead_code)] pub fn unchoke_slots(&self) -> usize {
self.unchoke_slots
}
pub fn observe_throughput(&mut self, throughput: u64) {
self.strategy.observe_throughput(throughput);
}
#[cfg(test)]
pub fn choking_algorithm(&self) -> ChokingAlgorithm {
self.choking_algorithm
}
pub fn decide(&mut self, peers: &[PeerInfo]) -> ChokeDecision {
self.strategy
.decide(peers, self.unchoke_slots, self.seed_mode)
}
pub fn rotate_optimistic(&mut self, peers: &[PeerInfo]) {
self.strategy.rotate_optimistic(peers);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn addr(port: u16) -> SocketAddr {
format!("127.0.0.1:{port}").parse().unwrap()
}
fn peer(port: u16, download_rate: u64, interested: bool) -> PeerInfo {
PeerInfo {
addr: addr(port),
download_rate,
upload_rate: 0,
interested,
upload_only: false,
is_seed: false,
}
}
fn seed_peer(port: u16, upload_rate: u64, interested: bool) -> PeerInfo {
PeerInfo {
addr: addr(port),
download_rate: 0,
upload_rate,
interested,
upload_only: false,
is_seed: false,
}
}
#[test]
fn unchoke_top_n() {
let mut choker = Choker::new(4);
let peers = vec![
peer(6881, 100, true),
peer(6882, 500, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 400, true),
peer(6886, 50, true),
];
let decision = choker.decide(&peers);
assert!(decision.to_unchoke.contains(&addr(6882)));
assert!(decision.to_unchoke.contains(&addr(6885)));
assert!(decision.to_unchoke.contains(&addr(6883)));
assert!(decision.to_unchoke.contains(&addr(6884)));
assert_eq!(decision.to_unchoke.len(), 5);
assert_eq!(decision.to_choke.len(), 1);
}
#[test]
fn optimistic_rotation() {
let mut choker = Choker::new(4);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 100, true),
peer(6886, 50, true),
];
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 5);
let regular = [addr(6881), addr(6882), addr(6883), addr(6884)];
let opt: Vec<_> = decision
.to_unchoke
.iter()
.filter(|a| !regular.contains(a))
.copied()
.collect();
assert_eq!(opt.len(), 1);
let first_opt = opt[0];
assert!(first_opt == addr(6885) || first_opt == addr(6886));
choker.rotate_optimistic(&peers);
let decision2 = choker.decide(&peers);
let opt2: Vec<_> = decision2
.to_unchoke
.iter()
.filter(|a| !regular.contains(a))
.copied()
.collect();
assert_eq!(opt2.len(), 1);
assert_ne!(opt2[0], first_opt);
}
#[test]
fn fewer_peers_than_slots() {
let mut choker = Choker::new(4);
let peers = vec![peer(6881, 100, true), peer(6882, 200, true)];
let decision = choker.decide(&peers);
assert!(decision.to_unchoke.contains(&addr(6881)));
assert!(decision.to_unchoke.contains(&addr(6882)));
assert_eq!(decision.to_unchoke.len(), 2);
assert!(decision.to_choke.is_empty());
}
#[test]
fn no_interested_peers() {
let mut choker = Choker::new(4);
let peers = vec![
peer(6881, 100, false),
peer(6882, 200, false),
peer(6883, 300, false),
];
let decision = choker.decide(&peers);
assert!(decision.to_unchoke.is_empty());
assert_eq!(decision.to_choke.len(), 3);
assert!(decision.to_choke.contains(&addr(6881)));
assert!(decision.to_choke.contains(&addr(6882)));
assert!(decision.to_choke.contains(&addr(6883)));
}
#[test]
fn choke_below_threshold() {
let mut choker = Choker::new(2);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
peer(6883, 100, true),
peer(6884, 50, true),
peer(6885, 200, false), ];
let decision = choker.decide(&peers);
assert!(decision.to_unchoke.contains(&addr(6881)));
assert!(decision.to_unchoke.contains(&addr(6882)));
assert_eq!(decision.to_unchoke.len(), 3);
assert_eq!(decision.to_choke.len(), 2);
assert!(decision.to_choke.contains(&addr(6885)));
}
#[test]
fn set_unchoke_slots_changes_capacity() {
let mut choker = Choker::new(2);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 100, true),
];
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 3);
choker.set_unchoke_slots(4);
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 5);
}
#[test]
fn set_unchoke_slots_caps_regular_set_at_m224_value() {
let peers: Vec<PeerInfo> = (0u16..10)
.map(|i| peer(6881 + i, 1_000 - u64::from(i) * 100, true))
.collect();
let mut choker = Choker::new(1);
let decision = choker.decide(&peers);
assert_eq!(
decision.to_unchoke.len(),
2,
"cap=1 must yield 1 regular + 1 optimistic"
);
choker.set_unchoke_slots(3);
let decision = choker.decide(&peers);
assert_eq!(
decision.to_unchoke.len(),
4,
"cap=3 must yield 3 regular + 1 optimistic"
);
choker.set_unchoke_slots(8);
let decision = choker.decide(&peers);
assert_eq!(
decision.to_unchoke.len(),
9,
"cap=8 must yield 8 regular + 1 optimistic"
);
}
#[test]
fn seed_mode_unchokes_by_upload_rate() {
let mut choker = Choker::new(2);
choker.set_seed_mode(true);
let peers = vec![
seed_peer(6881, 100, true),
seed_peer(6882, 500, true),
seed_peer(6883, 300, true),
seed_peer(6884, 200, true),
];
let decision = choker.decide(&peers);
assert!(decision.to_unchoke.contains(&addr(6882)));
assert!(decision.to_unchoke.contains(&addr(6883)));
assert_eq!(decision.to_unchoke.len(), 3);
}
#[test]
fn upload_only_excluded_from_optimistic() {
let mut choker = Choker::new(2);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
PeerInfo {
addr: addr(6883),
download_rate: 100,
upload_rate: 0,
interested: true,
upload_only: true,
is_seed: false,
},
PeerInfo {
addr: addr(6884),
download_rate: 50,
upload_rate: 0,
interested: true,
upload_only: true,
is_seed: false,
},
];
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 2);
assert!(decision.to_unchoke.contains(&addr(6881)));
assert!(decision.to_unchoke.contains(&addr(6882)));
assert!(decision.to_choke.contains(&addr(6883)));
assert!(decision.to_choke.contains(&addr(6884)));
}
#[test]
fn upload_only_still_regular_unchoked() {
let mut choker = Choker::new(2);
choker.set_seed_mode(true);
let peers = vec![
PeerInfo {
addr: addr(6881),
download_rate: 0,
upload_rate: 500,
interested: true,
upload_only: true, is_seed: false,
},
seed_peer(6882, 300, true),
seed_peer(6883, 100, true),
];
let decision = choker.decide(&peers);
assert!(decision.to_unchoke.contains(&addr(6881)));
assert!(decision.to_unchoke.contains(&addr(6882)));
}
#[test]
fn seed_choking_algorithm_default() {
assert_eq!(
SeedChokingAlgorithm::default(),
SeedChokingAlgorithm::FastestUpload
);
}
#[test]
fn choking_algorithm_default() {
assert_eq!(ChokingAlgorithm::default(), ChokingAlgorithm::FixedSlots);
}
#[test]
fn seed_choking_algorithm_serde_round_trip() {
for variant in [
SeedChokingAlgorithm::FastestUpload,
SeedChokingAlgorithm::RoundRobin,
SeedChokingAlgorithm::AntiLeech,
] {
let json = serde_json::to_string(&variant).unwrap();
let decoded: SeedChokingAlgorithm = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, variant);
}
}
#[test]
fn choking_algorithm_serde_round_trip() {
for variant in [ChokingAlgorithm::FixedSlots, ChokingAlgorithm::RateBased] {
let json = serde_json::to_string(&variant).unwrap();
let decoded: ChokingAlgorithm = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, variant);
}
}
#[test]
fn fixed_slots_strategy_leech_mode() {
let mut strategy = FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload);
let peers = vec![
peer(6881, 100, true),
peer(6882, 500, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 400, true),
peer(6886, 50, true),
];
let decision = strategy.decide(&peers, 4, false);
assert!(decision.to_unchoke.contains(&addr(6882)));
assert!(decision.to_unchoke.contains(&addr(6885)));
assert!(decision.to_unchoke.contains(&addr(6883)));
assert!(decision.to_unchoke.contains(&addr(6884)));
assert_eq!(decision.to_unchoke.len(), 5);
assert_eq!(decision.to_choke.len(), 1);
}
#[test]
fn fixed_slots_round_robin_rotates() {
let mut strategy = FixedSlotsStrategy::new(SeedChokingAlgorithm::RoundRobin);
let peers = vec![
seed_peer(6881, 100, true),
seed_peer(6882, 200, true),
seed_peer(6883, 300, true),
seed_peer(6884, 400, true),
seed_peer(6885, 500, true),
];
let d1 = strategy.decide(&peers, 2, true);
let d2 = strategy.decide(&peers, 2, true);
let set1 = d1.to_unchoke;
let set2 = d2.to_unchoke;
assert_ne!(set1, set2, "round-robin should rotate unchoke set");
}
#[test]
fn fixed_slots_anti_leech_prefers_non_seeds() {
let mut strategy = FixedSlotsStrategy::new(SeedChokingAlgorithm::AntiLeech);
let peers = vec![
PeerInfo {
addr: addr(6881),
download_rate: 0,
upload_rate: 500,
interested: true,
upload_only: false,
is_seed: true,
},
PeerInfo {
addr: addr(6882),
download_rate: 0,
upload_rate: 400,
interested: true,
upload_only: false,
is_seed: true,
},
PeerInfo {
addr: addr(6883),
download_rate: 0,
upload_rate: 100,
interested: true,
upload_only: false,
is_seed: false,
},
PeerInfo {
addr: addr(6884),
download_rate: 0,
upload_rate: 50,
interested: true,
upload_only: false,
is_seed: false,
},
];
let decision = strategy.decide(&peers, 2, true);
assert!(
decision.to_unchoke.contains(&addr(6883)),
"non-seed peer 6883 should be unchoked"
);
assert!(
decision.to_unchoke.contains(&addr(6884)),
"non-seed peer 6884 should be unchoked"
);
}
#[test]
fn rate_based_starts_at_min_slots() {
let strategy = RateBasedStrategy::new(
SeedChokingAlgorithm::FastestUpload,
0, 3, 10, );
assert_eq!(strategy.current_slots(), 3);
let strategy2 = RateBasedStrategy::new(
SeedChokingAlgorithm::FastestUpload,
0,
1, 10,
);
assert_eq!(strategy2.current_slots(), 2);
}
#[test]
fn rate_based_increases_slots_on_throughput_increase() {
let mut strategy = RateBasedStrategy::new(
SeedChokingAlgorithm::FastestUpload,
0, 2, 10, );
assert_eq!(strategy.current_slots(), 2);
strategy.observe_throughput_inner(1000);
assert_eq!(strategy.current_slots(), 2);
strategy.observe_throughput_inner(1500);
assert_eq!(strategy.current_slots(), 3);
}
#[test]
fn rate_based_decreases_slots_on_throughput_drop() {
let mut strategy = RateBasedStrategy::new(
SeedChokingAlgorithm::FastestUpload,
0, 2, 10, );
strategy.observe_throughput_inner(1000);
strategy.observe_throughput_inner(2000);
assert_eq!(strategy.current_slots(), 3);
strategy.observe_throughput_inner(3000);
assert_eq!(strategy.current_slots(), 4);
strategy.observe_throughput_inner(2000);
assert_eq!(strategy.current_slots(), 3);
}
#[test]
fn rate_based_respects_min_max() {
let mut strategy = RateBasedStrategy::new(
SeedChokingAlgorithm::FastestUpload,
0, 2, 3, );
strategy.observe_throughput_inner(1000);
strategy.observe_throughput_inner(2000);
assert_eq!(strategy.current_slots(), 3);
strategy.observe_throughput_inner(5000);
assert_eq!(strategy.current_slots(), 3);
strategy.observe_throughput_inner(1000);
assert_eq!(strategy.current_slots(), 2);
strategy.observe_throughput_inner(100);
assert_eq!(strategy.current_slots(), 2);
}
#[test]
fn rate_based_does_not_add_at_capacity() {
let mut strategy = RateBasedStrategy::new(
SeedChokingAlgorithm::FastestUpload,
10_000, 2,
10,
);
strategy.observe_throughput_inner(5_000);
assert_eq!(strategy.current_slots(), 2);
strategy.observe_throughput_inner(9_500);
assert_eq!(strategy.current_slots(), 2);
}
#[test]
fn rate_based_ignores_external_unchoke_slots() {
let mut strategy = RateBasedStrategy::new(SeedChokingAlgorithm::FastestUpload, 0, 2, 10);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 100, true),
];
let decision = strategy.decide(&peers, 10, false);
assert_eq!(decision.to_unchoke.len(), 3);
}
#[test]
fn choker_with_round_robin() {
let mut choker = Choker::with_algorithms(
2,
SeedChokingAlgorithm::RoundRobin,
ChokingAlgorithm::FixedSlots,
0,
2,
10,
);
choker.set_seed_mode(true);
let peers = vec![
seed_peer(6881, 100, true),
seed_peer(6882, 200, true),
seed_peer(6883, 300, true),
seed_peer(6884, 400, true),
seed_peer(6885, 500, true),
];
let d1 = choker.decide(&peers);
let d2 = choker.decide(&peers);
let set1 = d1.to_unchoke;
let set2 = d2.to_unchoke;
assert_ne!(set1, set2, "round-robin dispatcher should rotate");
}
#[test]
fn choker_with_rate_based() {
let mut choker = Choker::with_algorithms(
4, SeedChokingAlgorithm::FastestUpload,
ChokingAlgorithm::RateBased,
0,
2,
10,
);
assert_eq!(choker.choking_algorithm(), ChokingAlgorithm::RateBased);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 100, true),
];
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 3);
choker.observe_throughput(1000);
choker.observe_throughput(2000);
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 4); }
#[test]
fn choker_unchoke_slots_getter() {
let mut choker = Choker::new(4);
assert_eq!(choker.unchoke_slots(), 4);
choker.set_unchoke_slots(7);
assert_eq!(choker.unchoke_slots(), 7);
choker.set_unchoke_slots(0);
assert_eq!(choker.unchoke_slots(), 0);
}
#[test]
fn choker_new_is_backward_compatible() {
let mut choker = Choker::new(4);
assert_eq!(choker.choking_algorithm(), ChokingAlgorithm::FixedSlots);
let peers = vec![
peer(6881, 500, true),
peer(6882, 400, true),
peer(6883, 300, true),
peer(6884, 200, true),
peer(6885, 100, true),
peer(6886, 50, true),
];
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 5);
assert!(decision.to_unchoke.contains(&addr(6882)));
assert!(decision.to_unchoke.contains(&addr(6885)));
assert!(decision.to_unchoke.contains(&addr(6883)));
assert!(decision.to_unchoke.contains(&addr(6884)));
choker.set_seed_mode(true);
choker.set_unchoke_slots(2);
let decision = choker.decide(&peers);
assert_eq!(decision.to_unchoke.len(), 3); }
#[test]
fn leech_incumbent_wins_exact_rate_tie() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload);
let cycle1 = vec![
peer(6881, 1000, true), peer(6882, 900, true), peer(6883, 10, true), peer(6884, 5, true), ];
let d1 = s.decide(&cycle1, 2, false);
assert!(d1.to_unchoke.contains(&addr(6881)));
assert!(d1.to_unchoke.contains(&addr(6882)));
let cycle2 = vec![
peer(6881, 1000, true), peer(6882, 900, true), peer(6885, 900, true), peer(6883, 10, true), peer(6884, 5, true), ];
let d2 = s.decide(&cycle2, 2, false);
assert!(d2.to_unchoke.contains(&addr(6881)), "A retained");
assert!(
d2.to_unchoke.contains(&addr(6882)),
"B incumbent wins the exact rate tie"
);
assert!(
d2.to_choke.contains(&addr(6885)),
"tying challenger C blocked from regular slot"
);
}
#[test]
fn leech_decisive_challenger_takes_slot() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload);
let cycle1 = vec![
peer(6881, 1000, true), peer(6882, 900, true), peer(6883, 10, true), peer(6884, 5, true), ];
let _ = s.decide(&cycle1, 2, false);
let cycle2 = vec![
peer(6881, 1000, true), peer(6882, 900, true), peer(6885, 1500, true), peer(6883, 10, true), peer(6884, 5, true), ];
let d2 = s.decide(&cycle2, 2, false);
assert!(
d2.to_unchoke.contains(&addr(6885)),
"decisive challenger C wins a slot"
);
assert!(
d2.to_unchoke.contains(&addr(6881)),
"A retained (second-fastest)"
);
assert!(
d2.to_choke.contains(&addr(6882)),
"B displaced by decisive challenger"
);
}
#[test]
fn leech_cold_incumbent_evicted() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload);
let cycle1 = vec![
peer(6881, 1000, true), peer(6882, 900, true), peer(6883, 10, true), peer(6884, 5, true), ];
let _ = s.decide(&cycle1, 2, false);
let cycle2 = vec![
peer(6881, 1000, true), peer(6882, 0, true), peer(6885, 50, true), peer(6883, 10, true), peer(6884, 5, true), ];
let d2 = s.decide(&cycle2, 2, false);
assert!(d2.to_unchoke.contains(&addr(6881)), "A retained");
assert!(
d2.to_unchoke.contains(&addr(6885)),
"challenger takes the cold incumbent's slot"
);
assert!(
d2.to_choke.contains(&addr(6882)),
"cold incumbent B evicted despite incumbency"
);
}
#[test]
fn leech_incumbent_wins_tie_regardless_of_input_order() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload);
let cycle1 = vec![
peer(6882, 100, true), peer(6883, 1, true), ];
let _ = s.decide(&cycle1, 1, false);
let cycle2 = vec![
peer(6881, 100, true), peer(6882, 100, true), peer(6883, 1, true), ];
let d2 = s.decide(&cycle2, 1, false);
assert!(
d2.to_unchoke.contains(&addr(6882)),
"incumbent wins the tie despite the challenger being listed first"
);
assert!(
d2.to_choke.contains(&addr(6881)),
"tying challenger blocked despite favourable input order"
);
}
#[test]
fn seed_fastest_upload_incumbent_keeps_slot() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::FastestUpload);
let cycle1 = vec![
seed_peer(6881, 1000, true), seed_peer(6882, 900, true), seed_peer(6883, 10, true), seed_peer(6884, 5, true), ];
let d1 = s.decide(&cycle1, 2, true);
assert!(d1.to_unchoke.contains(&addr(6881)));
assert!(d1.to_unchoke.contains(&addr(6882)));
let cycle2 = vec![
seed_peer(6881, 1000, true), seed_peer(6882, 900, true), seed_peer(6885, 900, true), seed_peer(6883, 10, true), seed_peer(6884, 5, true), ];
let d2 = s.decide(&cycle2, 2, true);
assert!(d2.to_unchoke.contains(&addr(6881)), "A retained");
assert!(
d2.to_unchoke.contains(&addr(6882)),
"seed incumbent B wins the exact upload-rate tie"
);
assert!(
d2.to_choke.contains(&addr(6885)),
"tying seed challenger C blocked from regular slot"
);
}
#[test]
fn seed_round_robin_keeps_rotating() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::RoundRobin);
let peers = vec![
seed_peer(6881, 100, true),
seed_peer(6882, 100, true),
seed_peer(6883, 100, true),
seed_peer(6884, 100, true),
seed_peer(6885, 100, true),
seed_peer(6886, 100, true),
];
let d1 = s.decide(&peers, 2, true);
let d2 = s.decide(&peers, 2, true);
let mut u1 = d1.to_unchoke;
let mut u2 = d2.to_unchoke;
u1.sort();
u2.sort();
assert_ne!(
u1, u2,
"RoundRobin must keep rotating the unchoke set across cycles"
);
}
#[test]
fn seed_anti_leech_incumbent_keeps_slot() {
let mut s = FixedSlotsStrategy::new(SeedChokingAlgorithm::AntiLeech);
let cycle1 = vec![
seed_peer(6881, 1000, true), seed_peer(6882, 900, true), seed_peer(6883, 10, true), seed_peer(6884, 5, true), ];
let d1 = s.decide(&cycle1, 2, true);
assert!(d1.to_unchoke.contains(&addr(6881)));
assert!(d1.to_unchoke.contains(&addr(6882)));
let cycle2 = vec![
seed_peer(6881, 1000, true), seed_peer(6882, 900, true), seed_peer(6885, 900, true), seed_peer(6883, 10, true), seed_peer(6884, 5, true), ];
let d2 = s.decide(&cycle2, 2, true);
assert!(d2.to_unchoke.contains(&addr(6881)), "A retained");
assert!(
d2.to_unchoke.contains(&addr(6882)),
"AntiLeech incumbent B wins the exact upload-rate tie"
);
assert!(
d2.to_choke.contains(&addr(6885)),
"tying AntiLeech challenger C blocked from regular slot"
);
}
}