use std::future::Future;
use std::net::SocketAddr;
use std::time::Duration;
use slither::StreamId;
use slither::constants::AMPLIFICATION_FACTOR;
use slither::error::ConnectionLost;
use slither::shell::Notification;
use slither::testutil::{
Pair, Spied, TestConnection, TestRecvStream, TestSendStream, addr_c, local, settle,
};
const PATIENCE: Duration = Duration::from_secs(5);
const PATIENCE_PTO: Duration = Duration::from_secs(15);
async fn within<F: Future>(fut: F, what: &str) -> F::Output {
match tokio::time::timeout(PATIENCE, fut).await {
Ok(v) => v,
Err(_) => panic!("{what}: still pending after {PATIENCE:?} of virtual time"),
}
}
async fn within_pto<F: Future>(fut: F, what: &str) -> F::Output {
match tokio::time::timeout(PATIENCE_PTO, fut).await {
Ok(v) => v,
Err(_) => panic!("{what}: still pending after {PATIENCE_PTO:?} of virtual time"),
}
}
async fn is_pending<F: Future>(fut: F) -> bool {
tokio::time::timeout(Duration::from_millis(200), fut)
.await
.is_err()
}
fn payload(tag: u8, len: usize) -> Vec<u8> {
(0..len).map(|i| ((i % 251) as u8) ^ tag).collect()
}
async fn write_all(s: &mut TestSendStream, buf: &[u8], what: &str) {
let mut done = 0usize;
while done < buf.len() {
let n = within_pto(s.write(&buf[done..]), what)
.await
.unwrap_or_else(|e| panic!("{what}: write failed with {e:?}"));
assert!(
n >= 1,
"{what}: a blocked write is `Pending`, never `Ok(0)`"
);
done += n;
}
}
async fn read_expect(r: &mut TestRecvStream, want: &[u8], what: &str) {
let mut got = Vec::with_capacity(want.len());
while got.len() < want.len() {
let mut buf = vec![0u8; want.len() - got.len()];
match within_pto(r.read(&mut buf), what).await {
Ok(Some(n)) => {
assert!(n >= 1 && n <= buf.len(), "{what}: read returned {n}");
got.extend_from_slice(&buf[..n]);
}
other => panic!(
"{what}: wanted {} bytes, got {other:?} after {}",
want.len(),
got.len()
),
}
}
if let Some(i) = got.iter().zip(want.iter()).position(|(a, b)| a != b) {
panic!(
"{what}: first differing byte at offset {i}: got {:#04x}, want {:#04x}",
got[i], want[i]
);
}
}
struct Bi {
o_send: TestSendStream,
p_recv: TestRecvStream,
p_send: TestSendStream,
o_recv: TestRecvStream,
#[allow(dead_code)]
id: StreamId,
}
async fn bi_pair(opener: &TestConnection, peer: &TestConnection, what: &str) -> Bi {
let bi = within(opener.open_bi(), what).await.expect("open_bi");
let id = bi.id().expect("an opened stream has an id");
let (mut o_send, o_recv) = bi.split();
write_all(&mut o_send, b"\x00", what).await;
let peer_bi = within(peer.accept_bi(), what).await.expect("accept_bi");
let (p_send, mut p_recv) = peer_bi.split();
read_expect(&mut p_recv, b"\x00", what).await;
Bi {
o_send,
p_recv,
p_send,
o_recv,
id,
}
}
fn bytes_from_to(spied: &[Spied], from: SocketAddr, to: SocketAddr) -> u64 {
spied
.iter()
.filter(|s| s.src == from && s.dst == to)
.map(|s| s.bytes.len() as u64)
.sum()
}
#[tokio::test(start_paused = true)]
async fn s18_a_large_transfer_across_a_roam_completes_promptly() {
local(async {
let pair = Pair::seeded(0x7b_0001);
let (ca, cb) = pair.establish().await;
let a_addr = pair.a.addr();
let b_old = pair.b.addr();
assert_eq!(ca.remote_address(), b_old, "the anchor starts at b");
let bi = bi_pair(&ca, &cb, "a opens, b accepts").await;
let (mut sa, mut rb, mut sb, mut ra) = (bi.o_send, bi.p_recv, bi.p_send, bi.o_recv);
let warm = payload(0x01, 2048);
write_all(&mut sa, &warm, "pre-move a→b").await;
read_expect(&mut rb, &warm, "pre-move b reads").await;
settle().await;
pair.b.rebind(addr_c());
let nudge = payload(0x02, 512);
write_all(&mut sb, &nudge, "post-move b→a").await;
read_expect(&mut ra, &nudge, "post-move a reads").await;
settle().await;
assert_eq!(
ca.remote_address(),
addr_c(),
"S18: the anchor followed the move",
);
let big = payload(0x03, 64 * 1024);
write_all(&mut sa, &big, "post-move a→b (64 KiB)").await;
read_expect(&mut rb, &big, "post-move b reads 64 KiB").await;
let back = payload(0x04, 16 * 1024);
write_all(&mut sb, &back, "post-move b→a (16 KiB)").await;
read_expect(&mut ra, &back, "post-move a reads 16 KiB").await;
settle().await;
assert_eq!(
ca.remote_address(),
addr_c(),
"still anchored where the peer actually is",
);
let _ = a_addr;
})
.await;
}
#[tokio::test(start_paused = true)]
async fn s18_an_address_that_never_answers_stays_under_the_three_times_ceiling() {
local(async {
let pair = Pair::seeded(0x7b_0002);
let tap = pair.net.tap();
let (ca, cb) = pair.establish().await;
let a_addr = pair.a.addr();
let bi = bi_pair(&ca, &cb, "a opens, b accepts").await;
let (mut sa, mut rb, mut sb, mut ra) = (bi.o_send, bi.p_recv, bi.p_send, bi.o_recv);
let warm = payload(0x11, 2048);
write_all(&mut sa, &warm, "pre-move a→b").await;
read_expect(&mut rb, &warm, "pre-move b reads").await;
settle().await;
let _ = tap.drain();
pair.b.rebind(addr_c());
let nudge = payload(0x12, 256);
write_all(&mut sb, &nudge, "post-move b→a").await;
read_expect(&mut ra, &nudge, "post-move a reads").await;
settle().await;
assert_eq!(ca.remote_address(), addr_c(), "the anchor moved");
pair.net.block_path(addr_c(), a_addr);
let big = payload(0x13, 64 * 1024);
let mut offered = 0usize;
while offered < big.len() {
match tokio::time::timeout(Duration::from_secs(2), sa.write(&big[offered..])).await {
Ok(Ok(n)) => offered += n,
Ok(Err(e)) => panic!("write failed with {e:?}"),
Err(_) => break,
}
}
settle().await;
let w = tap.drain();
let sent = bytes_from_to(&w, a_addr, addr_c());
let recv = bytes_from_to(&w, addr_c(), a_addr);
assert!(
sent > 0,
"**working rule 9's other side**: a build that holds everything \
and never escapes satisfies the ceiling for free. §7.3 requires \
the budget to admit *something* — the roaming packet funds 3× \
its own size and the challenge must fit inside that.",
);
assert!(
sent <= AMPLIFICATION_FACTOR * recv,
"§7.3: {sent} bytes sent to an unvalidated address against \
{recv} received from it — the ceiling is {}× and this is {:.2}×. \
This is the reflector the section exists to prevent.",
AMPLIFICATION_FACTOR,
sent as f64 / recv.max(1) as f64,
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn s18_an_address_that_never_answers_dies_as_an_ordinary_timeout() {
local(async {
let pair = Pair::seeded(0x7b_0003);
let (ca, cb) = pair.establish().await;
let a_addr = pair.a.addr();
let bi = bi_pair(&ca, &cb, "a opens, b accepts").await;
let (mut sa, mut rb, mut sb, mut ra) = (bi.o_send, bi.p_recv, bi.p_send, bi.o_recv);
let warm = payload(0x21, 1024);
write_all(&mut sa, &warm, "pre-move a→b").await;
read_expect(&mut rb, &warm, "pre-move b reads").await;
settle().await;
pair.b.rebind(addr_c());
let nudge = payload(0x22, 256);
write_all(&mut sb, &nudge, "post-move b→a").await;
read_expect(&mut ra, &nudge, "post-move a reads").await;
settle().await;
assert_eq!(ca.remote_address(), addr_c(), "the anchor moved");
pair.net.block_path(addr_c(), a_addr);
let lost = match tokio::time::timeout(Duration::from_secs(60), ca.closed()).await {
Ok(v) => v,
Err(_) => panic!(
"the connection outlived DEAD_TIMEOUT at an address that \
never answered — a suppressed death clock is an immortal \
connection, which is worse than a spinning one",
),
};
assert_eq!(
lost,
ConnectionLost::TimedOut,
"§7.5's existing verdict, and **no new variant**: ruling 208 \
adds two frame types and nothing else",
);
})
.await;
}
#[tokio::test(start_paused = true)]
async fn a_validated_roam_produces_exactly_one_notification() {
local(async {
let pair = Pair::seeded(0x7b_0004);
let (ca, cb) = pair.establish().await;
let b_old = pair.b.addr();
let bi = bi_pair(&ca, &cb, "a opens, b accepts").await;
let (mut sa, mut rb, mut sb, mut ra) = (bi.o_send, bi.p_recv, bi.p_send, bi.o_recv);
let warm = payload(0x31, 1024);
write_all(&mut sa, &warm, "pre-move a→b").await;
read_expect(&mut rb, &warm, "pre-move b reads").await;
settle().await;
assert!(
is_pending(ca.notified()).await,
"precondition: nothing has been notified yet",
);
pair.b.rebind(addr_c());
let nudge = payload(0x32, 256);
write_all(&mut sb, &nudge, "post-move b→a").await;
read_expect(&mut ra, &nudge, "post-move a reads").await;
settle().await;
assert_eq!(
within(ca.notified(), "a.notified after the roam")
.await
.expect("the connection is alive"),
Notification::AddressMoved {
from: b_old,
to: addr_c(),
},
"the move itself is the notification",
);
let big = payload(0x33, 32 * 1024);
write_all(&mut sa, &big, "post-move a→b (32 KiB)").await;
read_expect(&mut rb, &big, "post-move b reads 32 KiB").await;
settle().await;
assert!(
is_pending(ca.notified()).await,
"validation is **not** an event: after the address validated and \
32 KiB crossed the new path, no second notification exists. A \
build re-announcing `AddressMoved` on validation lands here.",
);
})
.await;
}