use std::net::SocketAddr;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use crate::constants;
use crate::core::connection::stream_id::Dir;
use crate::core::connection::testfix::{
Drained, Pair, Solo, WIRE_PATH_CHALLENGE, WIRE_PATH_RESPONSE, Wire, a_addr, assert_alive,
b_addr, drain, path_challenge_frame, path_frame_with_body, path_response_frame, put, t0, v4,
write_all,
};
use crate::core::connection::{ConnEvent, ConnOutput};
use crate::error::ConnectionLost;
fn origin() -> Instant {
static ORIGIN: OnceLock<Instant> = OnceLock::new();
*ORIGIN.get_or_init(t0)
}
fn c_addr() -> SocketAddr {
v4(9, 41_000)
}
fn d_addr() -> SocketAddr {
v4(11, 42_000)
}
fn ack_frame(largest: u64) -> Vec<u8> {
let mut out = Vec::new();
put(&mut out, constants::FRAME_ACK);
put(&mut out, largest);
put(&mut out, 0); put(&mut out, 0); put(&mut out, 0); out
}
fn ping_frame() -> Vec<u8> {
let mut out = Vec::new();
put(&mut out, constants::FRAME_PING);
out
}
fn highest_sealed(solo: &Solo) -> u64 {
let next = solo
.conn
.next_counter()
.expect("a live session has a next counter");
assert!(
next >= 1,
"the core has sealed nothing, so no ACK can cover anything — the \
test's setup, not the core, is wrong",
);
next - 1
}
fn room(solo: &Solo) -> u64 {
let (sent, recv) = solo
.conn
.amplification_budget()
.expect("§7.3: the address is unvalidated, so a budget is armed");
(constants::AMPLIFICATION_FACTOR * recv).saturating_sub(sent)
}
fn challenges(frames: &[Wire]) -> Vec<[u8; 8]> {
frames
.iter()
.filter_map(|f| match f {
Wire::PathChallenge(v) => Some(*v),
_ => None,
})
.collect()
}
fn responses(frames: &[Wire]) -> Vec<[u8; 8]> {
frames
.iter()
.filter_map(|f| match f {
Wire::PathResponse(v) => Some(*v),
_ => None,
})
.collect()
}
fn roam_and_collect(solo: &mut Solo, now: Instant, to: SocketAddr) -> Vec<[u8; 8]> {
let d = solo.deliver_from(now, to, &ping_frame());
let mut frames = solo.drain_frames(&d);
let later = now + constants::MAX_ACK_DELAY + Duration::from_millis(1);
solo.conn.handle_timeout(later);
let d = drain(&mut solo.conn);
frames.extend(solo.drain_frames(&d));
challenges(&frames)
}
#[test]
fn a_path_challenge_body_of_seven_bytes_is_a_structural_error() {
let mut solo = Solo::installed_at(origin());
let d = solo.deliver(
origin(),
&path_frame_with_body(WIRE_PATH_CHALLENGE, &[0xAB; 7]),
);
assert_eq!(
d.closed(),
Some(ConnectionLost::ProtocolViolation {
code: constants::PROTOCOL_VIOLATION
}),
"§8.4: fewer than 8 bytes after the type byte is the frame's only \
structural error, and §8.2 answers it with PROTOCOL_VIOLATION",
);
}
#[test]
fn a_path_response_body_of_seven_bytes_is_a_structural_error() {
let mut solo = Solo::installed_at(origin());
let d = solo.deliver(
origin(),
&path_frame_with_body(WIRE_PATH_RESPONSE, &[0xAB; 7]),
);
assert_eq!(
d.closed(),
Some(ConnectionLost::ProtocolViolation {
code: constants::PROTOCOL_VIOLATION
}),
"§8.4's structural error is stated for both path frames",
);
}
#[test]
fn a_path_challenge_body_of_eight_bytes_parses() {
let mut solo = Solo::installed_at(origin());
let value = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
let d = solo.deliver(origin(), &path_challenge_frame(value));
assert_alive(&d);
let frames = solo.drain_frames(&d);
assert_eq!(
responses(&frames),
vec![value],
"§8.4: a received PATH_CHALLENGE obliges a PATH_RESPONSE carrying \
its eight bytes **verbatim**",
);
}
#[test]
fn a_path_challenge_does_not_consume_the_rest_of_the_packet() {
let mut solo = Solo::installed_at(origin());
let value = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
let mut frames = path_challenge_frame(value);
frames.extend_from_slice(&[constants::FRAME_PADDING as u8]);
let d = solo.deliver(origin(), &frames);
assert_alive(&d);
let out = solo.drain_frames(&d);
assert_eq!(
responses(&out),
vec![value],
"the challenge is exactly eight bytes wide; the trailing PADDING is \
a separate frame and not part of its body",
);
}
#[test]
fn the_response_echoes_the_challenge_byte_for_byte() {
let mut solo = Solo::installed_at(origin());
let value = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04];
let d = solo.deliver(origin(), &path_challenge_frame(value));
let frames = solo.drain_frames(&d);
let got = responses(&frames);
assert_eq!(got.len(), 1, "exactly one response to one challenge");
assert_eq!(got[0], value, "verbatim — not reversed, not re-drawn");
assert_ne!(got[0], [0u8; 8], "and not a zeroed placeholder");
}
#[test]
fn a_validated_connection_still_answers_a_challenge() {
let mut solo = Solo::installed_at(origin());
assert_eq!(
solo.conn.amplification_budget(),
None,
"precondition: dialled ⇒ validated ⇒ no challenge of our own",
);
let value = [0x9a; 8];
let d = solo.deliver(origin(), &path_challenge_frame(value));
let frames = solo.drain_frames(&d);
assert_eq!(
responses(&frames),
vec![value],
"the obligation is unconditional — it is not gated on our having \
armed, roamed, or expected anything",
);
}
#[test]
fn an_ack_covering_everything_validates_nothing() {
let mut solo = Solo::installed_from_msg1_at(origin());
assert!(
solo.conn.amplification_budget().is_some(),
"precondition: a msg1-anchored connection begins unvalidated \
(ruling 200)",
);
let now = origin() + Duration::from_millis(10);
let _ = solo.deliver(now, &ping_frame());
let sealed = highest_sealed(&solo);
let now = now + Duration::from_millis(10);
let d = solo.deliver(now, &ack_frame(sealed));
assert_alive(&d);
assert!(
solo.conn.amplification_budget().is_some(),
"**ruling 208**: `largest` is not evidence of receipt, it is an \
assertion by whoever holds the key, and the peer holds the key. \
A build that still routes ACKs into `Amplification` fails here \
and passes every other test in this file.",
);
}
#[test]
fn a_forged_ack_from_the_new_address_does_not_lift_the_budget() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let _ = solo.deliver_from(now, c_addr(), &ping_frame());
let armed = solo
.conn
.amplification_budget()
.expect("the roam arms the budget");
let sealed = highest_sealed(&solo);
let now = now + Duration::from_millis(20);
let d = solo.deliver_from(now, c_addr(), &ack_frame(sealed));
assert_alive(&d);
let still = solo
.conn
.amplification_budget()
.expect("**ruling 208**: the forged ACK must not disarm the budget");
assert!(
still.1 > armed.1,
"the ACK's bytes still fund the budget (§7.3, ruling 169): {} → {}",
armed.1,
still.1,
);
}
#[test]
fn a_mismatched_path_response_validates_nothing_and_is_not_an_error() {
let mut solo = Solo::installed_from_msg1_at(origin());
let now = origin() + Duration::from_millis(10);
let d = solo.deliver(now, &path_response_frame([0xff; 8]));
assert_alive(&d);
assert_eq!(
d.closed(),
None,
"§8.4: a mismatched response is a semantic no-op — PROTOCOL_VIOLATION \
here would be a keyless remote kill primitive",
);
assert!(
solo.conn.amplification_budget().is_some(),
"an invented response is what an off-path attacker's guess looks \
like, and it must validate nothing",
);
}
#[test]
fn the_matching_path_response_validates_and_disarms_the_budget() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let issued = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(
issued.len(),
1,
"the roam draws and offers exactly one challenge (§7.3: one per \
arming)",
);
assert!(
solo.conn.amplification_budget().is_some(),
"unvalidated until the echo arrives",
);
let now = now + Duration::from_millis(20);
let d = solo.deliver_from(now, c_addr(), &path_response_frame(issued[0]));
assert_alive(&d);
assert_eq!(
solo.conn.amplification_budget(),
None,
"§7.3: the address validates and the budget disarms at the first \
authenticated, window-fresh packet from it carrying a matching \
PATH_RESPONSE",
);
}
#[test]
fn a_challenge_from_a_superseded_arming_no_longer_validates() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let first = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(first.len(), 1, "one challenge for the first arming");
let now = now + Duration::from_millis(20);
let second = roam_and_collect(&mut solo, now, d_addr());
assert_eq!(second.len(), 1, "one challenge for the second arming");
let now = now + Duration::from_millis(20);
let d = solo.deliver_from(now, d_addr(), &path_response_frame(first[0]));
assert_alive(&d);
assert!(
solo.conn.amplification_budget().is_some(),
"§7.3: 'any earlier challenge is discarded and a PATH_RESPONSE \
echoing it validates nothing thereafter' (§13.6's roam-seam table). \
A build keeping a *set* of live challenges fails here.",
);
let now = now + Duration::from_millis(20);
let _ = solo.deliver_from(now, d_addr(), &path_response_frame(second[0]));
assert_eq!(
solo.conn.amplification_budget(),
None,
"the *current* arming's challenge still validates",
);
}
#[test]
fn an_echo_from_the_old_address_does_not_validate_the_new_one() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let issued = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(issued.len(), 1);
let now = now + Duration::from_millis(20);
let d = solo.deliver_from(now, a_addr(), &path_response_frame(issued[0]));
assert_alive(&d);
assert!(
solo.conn.amplification_budget().is_some(),
"the echo did not come from the address under validation",
);
}
#[test]
fn each_arming_draws_a_fresh_challenge() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let first = roam_and_collect(&mut solo, now, c_addr());
let now = now + Duration::from_millis(20);
let second = roam_and_collect(&mut solo, now, d_addr());
let now = now + Duration::from_millis(20);
let third = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(first.len(), 1);
assert_eq!(second.len(), 1);
assert_eq!(third.len(), 1);
assert_ne!(first[0], second[0], "a new arming draws new bytes");
assert_ne!(
first[0], third[0],
"and a roam **back** to a previously-challenged address draws new \
bytes too — otherwise a peer banks the earlier response",
);
assert_ne!(second[0], third[0]);
}
#[test]
fn the_same_arming_re_offers_the_same_challenge() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let first = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(first.len(), 1);
let now = now + Duration::from_millis(20);
let _ = solo.deliver_from(now, c_addr(), &ping_frame());
let now = now + constants::MAX_ACK_DELAY + Duration::from_millis(1);
solo.conn.handle_timeout(now);
let d = drain(&mut solo.conn);
let again = challenges(&solo.drain_frames(&d));
assert_eq!(
again.len(),
1,
"the standing obligation re-offers the challenge while the arming \
lasts (§8.7)",
);
assert_eq!(
again[0], first[0],
"**the same** eight bytes: ruling 208 fixes one challenge per \
*arming*, not per transmission",
);
}
#[test]
fn the_challenge_stops_being_offered_once_the_address_validates() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let issued = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(issued.len(), 1);
let now = now + Duration::from_millis(20);
let _ = solo.deliver_from(now, c_addr(), &path_response_frame(issued[0]));
assert_eq!(solo.conn.amplification_budget(), None, "validated");
let now = now + Duration::from_millis(20);
let after = roam_and_collect(&mut solo, now, c_addr());
assert!(
after.is_empty(),
"a validated address is owed no challenge: {after:?}",
);
}
#[test]
fn two_challenges_in_one_packet_produce_one_response_to_the_newest() {
let mut solo = Solo::installed_at(origin());
let older = [0x0a; 8];
let newer = [0x0b; 8];
let mut frames = path_challenge_frame(older);
frames.extend_from_slice(&path_challenge_frame(newer));
let d = solo.deliver(origin(), &frames);
assert_alive(&d);
let out = solo.drain_frames(&d);
let got = responses(&out);
assert_eq!(
got.len(),
1,
"§17.5 budgets one outstanding response, never a list: {got:?}",
);
assert_eq!(
got[0], newer,
"the **newer** challenge wins — it is the only one whose answer can \
still validate anything",
);
}
#[test]
fn a_response_obligation_survives_the_roam_that_created_it() {
let mut solo = Solo::installed_at(origin());
let value = [0x5c; 8];
let now = origin() + Duration::from_millis(10);
let d = solo.deliver_from(now, c_addr(), &path_challenge_frame(value));
assert_alive(&d);
let frames = solo.drain_frames(&d);
assert_eq!(
responses(&frames),
vec![value],
"the obligation is kept across §13.6's roam seam and is sent to the \
new endpoint",
);
assert!(
solo.conn.amplification_budget().is_some(),
"our arming and the peer's answer are separate obligations",
);
}
#[test]
fn a_closing_connection_does_not_credit_a_third_address() {
let mut solo = Solo::installed_from_msg1_at(origin());
let now = origin() + Duration::from_millis(10);
solo.conn.close(now, constants::NO_ERROR, b"");
let _ = drain(&mut solo.conn);
let before = solo
.conn
.amplification_budget()
.expect("still unvalidated while closing");
let now = now + Duration::from_millis(10);
let _ = solo.deliver_from(now, d_addr(), &ping_frame());
let after = solo.conn.amplification_budget().expect("still unvalidated");
assert_eq!(
after.1, before.1,
"§7.3 funds the budget from bytes **received from** the unvalidated \
address; this datagram came from somewhere else",
);
}
#[test]
fn a_live_connection_still_credits_the_address_it_roams_to() {
let mut solo = Solo::installed_from_msg1_at(origin());
let now = origin() + Duration::from_millis(10);
let d = solo.deliver_from(now, d_addr(), &ping_frame());
assert_eq!(
d.count_events(|e| matches!(e, ConnEvent::AddressMoved { .. })),
1,
"the live path roams, which is what the closing path must not do",
);
let (_, credited) = solo
.conn
.amplification_budget()
.expect("the roam re-arms the budget");
assert_eq!(
credited,
(constants::DATA_HEADER_LEN + 1 + constants::AEAD_TAG_LEN) as u64,
"the roaming packet — a 1-byte PING plaintext — and only it, credits \
the fresh counter",
);
}
#[test]
fn an_ack_still_clears_a_pending_contested_mark() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let _ = solo.deliver_from(now, c_addr(), &ping_frame());
let now = now + Duration::from_millis(10);
solo.conn.mark_contested(now);
let floor = solo
.conn
.contested()
.floor()
.expect("the mark records a probe floor");
let now = now + Duration::from_millis(10);
let d = solo.deliver_from(now, c_addr(), &ack_frame(floor));
assert_alive(&d);
assert_eq!(
solo.conn.contested().floor(),
None,
"**ruling 176**: an ACK covering the probe floor is one of the two \
exits from the pending state, and ruling 208 does not touch it. A \
build that deleted `on_ack_coverage` wholesale to remove ruling \
168's machinery fails here and nowhere else.",
);
assert!(
solo.conn.amplification_budget().is_some(),
"one ACK, two floors, and after ruling 208 it feeds exactly one of \
them",
);
}
#[test]
fn a_pending_contested_probe_does_not_block_the_challenge() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let issued = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(issued.len(), 1, "the arming's challenge");
let now = now + Duration::from_millis(10);
let r = solo.conn.open(Dir::Uni).expect("a uni stream");
let _ = write_all(&mut solo.conn, now, r, &[0x5u8; 512]);
let _ = drain(&mut solo.conn);
assert!(
room(&solo) < 31,
"premise: §7.3 refuses even a bare PING, so the mark must park \
(room {})",
room(&solo),
);
let now = now + Duration::from_millis(10);
solo.conn.mark_contested(now);
assert!(
solo.conn.contested().is_pending(),
"precondition: the mark is pending, not yet transmitted",
);
let now = now + Duration::from_millis(10);
let d = solo.deliver_from(now, c_addr(), &ping_frame());
let packets = solo.packets(&d);
let probes: Vec<&Vec<Wire>> = packets
.iter()
.filter(|p| p.iter().any(|f| matches!(f, Wire::Ping)))
.collect();
assert_eq!(
probes.len(),
1,
"premise: the pending probe leaves here, exactly once. Packets: \
{packets:?}",
);
assert!(
probes[0]
.iter()
.any(|f| matches!(f, Wire::PathChallenge(v) if *v == issued[0])),
"**rulings 215 and 250**: the pump's early return *\"may not block \
the one frame that ends the state it is protecting\"*, and the \
resolution is that the probe **carries** it — same eight bytes, \
one packet, one header, one tag. On the pre-250 build this packet \
is `[Ping]` and the challenge left in a datagram above it. \
Packets: {packets:?}",
);
}
#[test]
fn the_challenge_precedes_the_probes_ping_in_the_packet_carrying_both() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let _ = roam_and_collect(&mut solo, now, c_addr());
let now = now + Duration::from_millis(10);
solo.conn.mark_contested(now);
let now = now + Duration::from_millis(10);
let d = solo.deliver_from(now, c_addr(), &ping_frame());
let packets = solo.packets(&d);
let carrying = packets
.iter()
.find(|p| p.iter().any(|f| matches!(f, Wire::Ping)))
.expect("some packet carries the probe's PING");
let ping_at = carrying
.iter()
.position(|f| matches!(f, Wire::Ping))
.expect("just found it");
let challenge_at = carrying
.iter()
.position(|f| matches!(f, Wire::PathChallenge(_)))
.unwrap_or_else(|| {
panic!(
"ruling 250: the probe coalesces the owed `PATH_CHALLENGE`, \
so the packet carrying the PING carries it too. The \
pre-250 pre-pass sent it in a datagram of its own and this \
packet is `[Ping]`. Packets: {packets:?}"
)
});
assert!(
challenge_at < ping_at,
"§8.5 (ruling 208, packing order — *not* §7.3's admission rank, \
which rulings 215 and 250 leave with the probe above the \
challenge): the path frames are first among the control frames, \
ahead of the probe's PING. Packet: {carrying:?}",
);
}
#[test]
fn a_packet_leaving_for_an_unvalidated_address_carries_the_challenge() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let _ = solo.deliver_from(now, c_addr(), &ping_frame());
assert!(
solo.conn.amplification_budget().is_some(),
"precondition: unvalidated",
);
let r = solo.conn.open(Dir::Uni).expect("a uni stream");
let now = now + Duration::from_millis(10);
let _ = write_all(&mut solo.conn, now, r, &[0x7u8; 512]);
let d = drain(&mut solo.conn);
let frames = solo.drain_frames(&d);
assert!(
!challenges(&frames).is_empty(),
"§7.3's no-deadlock proof needs the escape **inside** the packet the \
budget allowed: an ACK no longer ends the scarcity, so a packet \
that merely elicits is not an escape. Frames: {frames:?}",
);
}
#[test]
fn a_ninety_byte_budget_still_lets_the_address_validate() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let _ = solo.deliver_from(now, c_addr(), &[]);
let (_, credited) = solo
.conn
.amplification_budget()
.expect("the keepalive roam arms the budget");
assert_eq!(
credited,
(constants::DATA_HEADER_LEN + constants::AEAD_TAG_LEN) as u64,
"precondition: a 30-byte credit, so a 90-byte budget",
);
let r = solo.conn.open(Dir::Uni).expect("a uni stream");
let now = now + Duration::from_millis(10);
let _ = write_all(&mut solo.conn, now, r, &[0x9u8; 2048]);
let d = drain(&mut solo.conn);
let frames = solo.drain_frames(&d);
let issued = challenges(&frames);
assert!(
!issued.is_empty(),
"the budget admits a challenge datagram (39 B against 90 B) and the \
pump must size to it: {frames:?}",
);
let now = now + Duration::from_millis(20);
let _ = solo.deliver_from(now, c_addr(), &path_response_frame(issued[0]));
assert_eq!(
solo.conn.amplification_budget(),
None,
"one round trip ends the scarcity — which is what the budget is for",
);
}
#[test]
fn an_idle_unvalidated_connection_owing_nothing_emits_no_challenge() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let d = solo.deliver_from(now, c_addr(), &[]);
let frames = solo.drain_frames(&d);
assert!(
challenges(&frames).is_empty(),
"boundary (1): nothing is owed, so no packet is manufactured to \
carry a challenge: {frames:?}",
);
let now = now + Duration::from_millis(50);
solo.conn.handle_timeout(now);
let d = drain(&mut solo.conn);
let frames = solo.drain_frames(&d);
assert!(
challenges(&frames).is_empty(),
"still nothing owed, still no challenge: {frames:?}",
);
}
#[test]
fn a_held_keepalive_never_announces_a_deadline_in_the_past() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let _ = solo.deliver_from(now, c_addr(), &[]);
assert!(
solo.conn.amplification_budget().is_some(),
"precondition: the budget is armed and scarce",
);
let now = now + constants::KEEPALIVE_TIMEOUT + Duration::from_secs(1);
solo.conn.handle_timeout(now);
let d = drain(&mut solo.conn);
match d.deadline {
None => {}
Some(at) => assert!(
at > now,
"**ruling 141's spin class**: a core announced a deadline at or \
before `now` ({:?} before now), which the driver sleeps on and \
which completes immediately, re-firing the same timer forever",
now.saturating_duration_since(at),
),
}
solo.conn
.handle_timeout(now + constants::DEAD_TIMEOUT + Duration::from_secs(1));
let d = drain(&mut solo.conn);
assert_eq!(
d.closed(),
Some(ConnectionLost::TimedOut),
"§7.5's death clock is **not** suppressed: an address that never \
answers still kills the session at DEAD_TIMEOUT (`CONTRACT-7b.md` \
§1.5 — 'Nothing new. No new timer, no new event, no new error \
variant.')",
);
}
#[test]
fn an_unanswered_challenge_dies_as_an_ordinary_timeout() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let issued = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(issued.len(), 1, "a challenge went out and is unanswered");
let now = now + constants::DEAD_TIMEOUT + Duration::from_secs(1);
solo.conn.handle_timeout(now);
let d = drain(&mut solo.conn);
assert_eq!(
d.closed(),
Some(ConnectionLost::TimedOut),
"the pre-168 behaviour for an address that never answers, and the \
correct one: an address that cannot answer is one to stop sending to",
);
}
#[test]
fn two_real_cores_validate_a_roamed_address() {
let mut pair = Pair::installed_at(origin());
let r = pair.a.open(Dir::Uni).expect("a uni stream");
let now = origin() + Duration::from_millis(10);
let _ = write_all(&mut pair.a, now, r, &[0x3u8; 256]);
let now = now + Duration::from_millis(10);
let (_, db) = pair.step_from(now, c_addr(), b_addr());
let moved = db.count_events(|e| matches!(e, ConnEvent::AddressMoved { .. }));
assert_eq!(moved, 1, "B sees the move");
assert!(
pair.b.amplification_budget().is_some(),
"B arms a budget against A's new address",
);
let now = now + Duration::from_millis(20);
let _ = pair.pump_from(now, c_addr(), b_addr());
assert_eq!(
pair.b.amplification_budget(),
None,
"**the end-to-end property**: B challenged, A echoed, and B's cap \
lifted. A build whose emitter and parser share one mistake — a \
swapped code point, a reversed body — passes every one-sided test \
above and fails here.",
);
assert_eq!(
pair.b.remote_address(),
Some(c_addr()),
"and it validated the address it actually moved to",
);
}
#[test]
fn no_core_announces_a_past_deadline_across_a_roam() {
let mut pair = Pair::installed_at(origin());
let r = pair.a.open(Dir::Uni).expect("a uni stream");
let mut now = origin() + Duration::from_millis(10);
let _ = write_all(&mut pair.a, now, r, &[0x3u8; 4096]);
for _ in 0..8 {
now += Duration::from_millis(25);
let (da, db) = pair.step_from(now, c_addr(), b_addr());
for (side, d) in [("A", &da), ("B", &db)] {
if let Some(at) = d.deadline {
assert!(
at > now,
"{side} announced a deadline at or before `now` — \
ruling 141's spin class",
);
}
}
}
}
#[test]
fn path_validation_emits_no_new_connection_event() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let d = solo.deliver_from(now, c_addr(), &ping_frame());
let issued = challenges(&solo.drain_frames(&d));
assert_eq!(issued.len(), 1);
let now = now + Duration::from_millis(20);
let d = solo.deliver_from(now, c_addr(), &path_response_frame(issued[0]));
let events = count_all_events(&d);
let moved = d.count_events(|e| matches!(e, ConnEvent::AddressMoved { .. }));
assert_eq!(
events, moved,
"validation is not an event: the only events on this drain are the \
ones §16.4 already defines. Outputs: {:?}",
d.outs,
);
}
fn count_all_events(d: &Drained) -> usize {
d.outs
.iter()
.filter(|o| matches!(o, ConnOutput::Event(_)))
.count()
}
#[test]
fn a_pto_probe_to_an_unvalidated_address_carries_the_challenge() {
let mut solo = Solo::installed_at(origin());
let now = origin() + Duration::from_millis(10);
let issued = roam_and_collect(&mut solo, now, c_addr());
assert_eq!(issued.len(), 1, "the arming offers its challenge once");
let d = drain(&mut solo.conn);
let deadline = d.deadline.expect("an unanswered challenge arms a deadline");
solo.conn.handle_timeout(deadline);
let d = drain(&mut solo.conn);
let frames = solo.drain_frames(&d);
assert_eq!(
challenges(&frames),
vec![issued[0]],
"§8.7: `PATH_CHALLENGE` is **owed for as long as the arming lasts** \
and is re-emitted with the **same** eight bytes. The probe carried \
{frames:?} instead — a build that emits the challenge once and lets \
loss end it is the *\"sent once and lost forever\"* §8.7 says \
§7.3's no-deadlock argument cannot survive.",
);
assert!(
!frames.iter().any(|f| matches!(f, Wire::Ping)),
"§13.4's PING is owed only when the first three stages produced \
nothing that elicits; the challenge elicits, so no PING is owed \
beside it. Frames: {frames:?}",
);
}
#[test]
fn two_sessions_at_one_peer_address_hold_two_independent_budgets() {
let mut a = Solo::installed_from_msg1_at(origin());
let mut b = Solo::installed_from_msg1_at(origin());
let armed_a = a.conn.amplification_budget().expect("A is unvalidated");
let armed_b = b.conn.amplification_budget().expect("B is unvalidated");
assert_eq!(
armed_a, armed_b,
"precondition: two identically armed budgets, so any later \
difference is something one of them did",
);
let (spent, credit) = armed_a;
assert!(
credit > 0,
"precondition: the msg1 anchor funded each budget, so each has a \
non-zero cap of its own to be independent *of*",
);
let cap = constants::AMPLIFICATION_FACTOR * credit;
assert!(
spent < cap,
"precondition: each has spent only its own response packet \
({spent} of {cap}) and has room left to be exhausted",
);
let now = origin() + Duration::from_millis(10);
let ra = a.conn.open(Dir::Uni).expect("a uni stream");
let rb = b.conn.open(Dir::Uni).expect("a uni stream");
let _ = write_all(&mut a.conn, now, ra, &[0x5u8; 4096]);
let da = drain(&mut a.conn);
assert!(
!da.transmits().is_empty(),
"premise: A's own budget admitted something",
);
for t in da.transmits() {
assert_eq!(
t.to,
a_addr(),
"precondition: A is anchored at the address B is anchored at",
);
}
assert!(
room(&a) < 31,
"premise: A has spent its whole 3× cap and now refuses even a bare \
31-byte PING datagram (room {}, cap {cap})",
room(&a),
);
assert_eq!(
b.conn.amplification_budget(),
Some(armed_b),
"§7.3: B's counters are its own. A shared counter shows A's spending \
here, and this is the only assertion in the suite that sees it.",
);
let _ = write_all(&mut b.conn, now, rb, &[0x5u8; 200]);
let db = drain(&mut b.conn);
assert!(
!db.transmits().is_empty(),
"§7.3, Appendix B O43e: *exhausting one must not throttle the \
other*. B has spent nothing of its own and must still be \
admitted: {:?}",
db.outs,
);
for t in db.transmits() {
assert_eq!(t.to, a_addr(), "and B's packets go to the same address");
}
let (b_spent, b_recv) = b.conn.amplification_budget().expect("still unvalidated");
assert_eq!(b_recv, credit, "B's received side never moved");
assert!(
b_spent > spent && b_spent <= cap,
"B spent inside **its own** cap: {b_spent} of {cap}",
);
let before_a = a.conn.amplification_budget().expect("still unvalidated");
let later = now + Duration::from_millis(10);
let db = b.deliver(later, &[constants::FRAME_PADDING as u8; 600]);
let (_, b_recv) = b.conn.amplification_budget().expect("still unvalidated");
assert!(
b_recv >= credit + 600,
"premise: the receive really did fund **B** — its received counter \
moved from {credit} to {b_recv}",
);
assert!(
db.transmits().is_empty(),
"premise: and B spent none of it, so the credit is still there to \
be wrongly spent by A: {:?}",
db.outs,
);
assert!(
room(&b) > 31,
"premise: B's own window is genuinely open again (room {})",
room(&b),
);
assert_eq!(
a.conn.amplification_budget(),
Some(before_a),
"§7.3: *funding one must not credit the other*. A shared `recv` \
counter moves A's here.",
);
let (a_sent, _) = before_a;
assert!(
a_sent < 4096,
"premise (review N1): A must still hold unsent stream bytes for the \
behavioural half to test anything — stream payload on the wire \
cannot exceed A's total wire spend ({a_sent}), which is below the \
4096 written",
);
a.conn.flush(later);
let da = drain(&mut a.conn);
assert!(
da.transmits().is_empty(),
"A's budget is still exhausted, so nothing may leave it on credit \
**B** earned: {:?}",
da.outs,
);
}