use std::time::Instant;
use crate::constants;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Amplification {
validated: bool,
challenge: [u8; 8],
sent: u64,
recv: u64,
}
impl Default for Amplification {
fn default() -> Self {
Self::validated()
}
}
impl Amplification {
pub(crate) const fn validated() -> Self {
Self {
validated: true,
challenge: [0u8; 8],
sent: 0,
recv: 0,
}
}
pub(crate) const fn arm(challenge: [u8; 8], credit: u64) -> Self {
Self {
validated: false,
challenge,
sent: 0,
recv: credit,
}
}
pub(crate) fn outstanding_challenge(&self) -> Option<[u8; 8]> {
(!self.validated).then_some(self.challenge)
}
pub(crate) fn admits(&self, len: u64) -> bool {
if self.validated {
return true;
}
self.sent.saturating_add(len) <= constants::AMPLIFICATION_FACTOR.saturating_mul(self.recv)
}
pub(crate) fn room(&self) -> Option<u64> {
(!self.validated).then(|| {
constants::AMPLIFICATION_FACTOR
.saturating_mul(self.recv)
.saturating_sub(self.sent)
})
}
pub(crate) fn on_sent(&mut self, len: u64) {
if self.validated {
return;
}
self.sent = self.sent.saturating_add(len);
}
pub(crate) fn on_recv(&mut self, len: u64) {
if self.validated {
return;
}
self.recv = self.recv.saturating_add(len);
}
pub(crate) fn on_path_response(&mut self, echo: &[u8; 8]) {
if self.validated {
return;
}
let mut diff = 0u8;
for (a, b) in self.challenge.iter().zip(echo) {
diff |= a ^ b;
}
if diff == 0 {
*self = Self::validated();
}
}
pub(crate) fn is_validated(&self) -> bool {
self.validated
}
#[cfg(test)]
pub(crate) fn counters(&self) -> Option<(u64, u64)> {
(!self.validated).then_some((self.sent, self.recv))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Contested {
No,
Pending {
floor: u64,
},
Armed {
floor: u64,
armed_at: Instant,
deadline: Instant,
},
}
impl Contested {
pub(crate) fn is_pending(&self) -> bool {
matches!(self, Contested::Pending { .. })
}
pub(crate) fn floor(&self) -> Option<u64> {
match self {
Contested::No => None,
Contested::Pending { floor } | Contested::Armed { floor, .. } => Some(*floor),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_validated_address_admits_anything_and_counts_nothing() {
let mut budget = Amplification::validated();
assert!(budget.admits(u64::MAX));
budget.on_sent(1_000_000);
assert!(budget.admits(u64::MAX));
assert_eq!(budget.counters(), None);
}
const CHALLENGE: [u8; 8] = [0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18];
#[test]
fn the_gate_is_three_times_received_and_the_boundary_is_admitted() {
let budget = Amplification::arm(CHALLENGE, 100);
assert!(budget.admits(300), "3 × 100 is admitted");
assert!(!budget.admits(301), "one byte past it is held");
}
#[test]
fn spend_reduces_the_headroom_and_a_receive_restores_it() {
let mut budget = Amplification::arm(CHALLENGE, 100);
budget.on_sent(250);
assert!(budget.admits(50));
assert!(!budget.admits(51));
budget.on_recv(100);
assert!(budget.admits(350), "3 × 200 − 250");
assert!(!budget.admits(351));
assert_eq!(budget.counters(), Some((250, 200)));
}
#[test]
fn only_a_response_echoing_this_armings_challenge_validates() {
let mut budget = Amplification::arm(CHALLENGE, 10);
assert_eq!(budget.outstanding_challenge(), Some(CHALLENGE));
let mut near_miss = CHALLENGE;
near_miss[7] ^= 0x01;
budget.on_path_response(&near_miss);
assert!(
!budget.is_validated(),
"eight bytes that are not the challenge prove nothing"
);
budget.on_path_response(&[0u8; 8]);
assert!(!budget.is_validated(), "nor do eight zeroes");
budget.on_path_response(&CHALLENGE);
assert!(budget.is_validated(), "the echo is the proof");
assert_eq!(budget.counters(), None, "validated: no counters remain");
assert_eq!(
budget.outstanding_challenge(),
None,
"nothing is owed to an address that has answered"
);
}
#[test]
fn validation_is_terminal_until_the_next_arming() {
let mut budget = Amplification::arm(CHALLENGE, 10);
budget.on_path_response(&CHALLENGE);
budget.on_path_response(&[0u8; 8]);
budget.on_sent(1_000_000);
assert!(budget.admits(u64::MAX));
assert!(budget.is_validated());
}
#[test]
fn a_response_to_the_previous_arming_is_worthless_after_a_re_arm() {
let stale = CHALLENGE;
let mut fresh = CHALLENGE;
fresh[0] ^= 0xff;
let mut budget = Amplification::arm(fresh, 10);
budget.on_path_response(&stale);
assert!(
!budget.is_validated(),
"the previous arming's bytes are not this arming's question"
);
budget.on_path_response(&fresh);
assert!(budget.is_validated());
}
#[test]
fn the_three_contested_states_are_distinguishable() {
let now = Instant::now();
assert!(!Contested::No.is_pending());
assert_eq!(Contested::No.floor(), None);
let pending = Contested::Pending { floor: 12 };
assert!(pending.is_pending());
assert_eq!(pending.floor(), Some(12));
let armed = Contested::Armed {
floor: 12,
armed_at: now,
deadline: now + constants::KEEPALIVE_TIMEOUT,
};
assert!(!armed.is_pending(), "armed is not pending");
assert_eq!(armed.floor(), Some(12));
assert_ne!(pending, armed, "the pending gap is its own state");
}
}