#![cfg(feature = "dandelion")]
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::time::{Duration, Instant};
use blvm_node::network::dandelion::{Clock, DandelionPhase, DandelionRelay};
#[derive(Clone)]
struct TestClock {
now: Instant,
}
impl TestClock {
fn new(start: Instant) -> Self {
Self { now: start }
}
fn advance(&mut self, d: Duration) {
self.now += d;
}
}
impl Clock for TestClock {
fn now(&self) -> Instant {
self.now
}
}
fn peers() -> Vec<String> {
vec!["p1".into(), "p2".into(), "p3".into(), "p4".into()]
}
#[test]
fn no_premature_broadcast_probability_zero() {
let rng = StdRng::seed_from_u64(123);
let clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_secs(60));
let tx = [9u8; 32];
let _ = d.start_stem_phase(tx, "p1".into(), &peers());
d.set_fluff_probability(0.0);
for _ in 0..100 {
assert!(!d.should_fluff(&tx));
}
}
#[test]
fn timed_fluff_occurs_after_timeout() {
let rng = StdRng::seed_from_u64(7);
let mut clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_millis(20));
d.set_fluff_probability(0.0); let tx = [2u8; 32];
let _ = d.start_stem_phase(tx, "p2".into(), &peers());
clock.advance(Duration::from_millis(25));
d.set_clock(clock.clone());
assert!(d.should_fluff(&tx));
}
#[test]
fn max_hops_forces_fluff() {
let rng = StdRng::seed_from_u64(99);
let clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_secs(60));
d.set_fluff_probability(0.0);
d.set_max_stem_hops(1); let tx = [3u8; 32];
let _ = d.start_stem_phase(tx, "p1".into(), &peers());
let _ = d.advance_stem(tx, &peers());
assert!(d.should_fluff(&tx));
}
#[test]
fn probabilistic_fluff_rate_is_reasonable() {
let rng = StdRng::seed_from_u64(2024);
let clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_secs(60));
d.set_fluff_probability(0.2);
let trials = 2000usize;
let mut fluffed = 0usize;
for i in 0..trials {
let tx = [i as u8; 32];
let _ = d.start_stem_phase(tx, "p1".into(), &peers());
if d.should_fluff(&tx) {
fluffed += 1;
}
let _ = d.transition_to_fluff(tx);
}
let rate = fluffed as f64 / trials as f64;
assert!((rate - 0.2).abs() < 0.03, "rate {} out of bounds", rate);
}
#[test]
fn peer_churn_during_stem_is_safe() {
let rng = StdRng::seed_from_u64(55);
let clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_millis(100));
d.set_fluff_probability(0.0);
let tx = [7u8; 32];
let all_peers = peers();
let _ = d.start_stem_phase(tx, "p1".into(), &all_peers);
let next = d.advance_stem(tx, &vec!["p1".into()]);
assert!(next.is_some());
let mut test_clock = clock.clone();
test_clock.advance(Duration::from_millis(200));
d.set_clock(test_clock);
assert!(d.should_fluff(&tx));
}
#[test]
fn duplicate_tx_handling_overwrites_safely() {
let rng = StdRng::seed_from_u64(77);
let clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
let tx = [5u8; 32];
let _ = d.start_stem_phase(tx, "p1".into(), &peers());
let _ = d.start_stem_phase(tx, "p2".into(), &peers()); assert_eq!(d.get_phase(&tx), Some(DandelionPhase::Stem));
}
#[test]
fn bounded_state_after_cleanup() {
let rng = StdRng::seed_from_u64(88);
let mut clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> = DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_millis(5));
d.set_fluff_probability(0.0);
for i in 0..500 {
let tx = [i as u8; 32];
let _ = d.start_stem_phase(tx, "p1".into(), &peers());
}
clock.advance(Duration::from_millis(20));
d.set_clock(clock.clone());
#[test]
fn eclipse_subset_eventual_fluff() {
let rng = StdRng::seed_from_u64(1337);
let mut clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> =
DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_millis(50));
d.set_fluff_probability(0.0);
d.set_max_stem_hops(2);
let tx = [11u8; 32];
let subset = vec!["p1".into(), "p2".into()];
let _ = d.start_stem_phase(tx, "p1".into(), &subset);
let _ = d.advance_stem(tx, &subset);
clock.advance(Duration::from_millis(60));
d.set_clock(clock.clone());
assert!(d.should_fluff(&tx));
}
#[test]
fn rng_extremes_bounds() {
let rng = StdRng::seed_from_u64(4242);
let clock = TestClock::new(Instant::now());
let mut d: DandelionRelay<TestClock> =
DandelionRelay::with_rng_and_clock(rng, clock.clone());
d.set_stem_timeout(Duration::from_secs(60));
d.set_fluff_probability(1.0);
let tx = [13u8; 32];
let _ = d.start_stem_phase(tx, "p3".into(), &peers());
assert!(d.should_fluff(&tx));
}
d.cleanup_expired();
assert!(d.get_stats().stem_transactions < 10);
}