mod common;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use bytes::Bytes;
use common::init_tracing;
use grapevine::{Identity, Node, NodeConfigBuilder, Payload, Tcp};
async fn wait_for_delivery(
label: &str,
delivered: &Arc<Mutex<Vec<Bytes>>>,
predicate: impl Fn(&[Bytes]) -> bool,
) {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let satisfied = predicate(&delivered.lock().expect("record lock"));
if satisfied {
return;
}
assert!(Instant::now() < deadline, "timed out waiting for {label}");
tokio::time::sleep(Duration::from_millis(25)).await;
}
}
#[tokio::test(flavor = "multi_thread")]
async fn forged_origin_message_is_rejected() {
init_tracing();
let delivered: Arc<Mutex<Vec<Bytes>>> = Arc::new(Mutex::new(Vec::new()));
let recorder = Arc::clone(&delivered);
let victim = Node::new(NodeConfigBuilder::new().build().expect("victim config"))
.await
.expect("create victim");
victim
.on_message(move |_origin, data| {
recorder.lock().expect("record lock").push(data);
})
.await;
victim.start().await.expect("start victim");
let victim_addr = victim.local_addr().await.expect("victim address");
let honest = Node::new(
NodeConfigBuilder::new()
.add_bootstrap_peer(victim_addr)
.build()
.expect("honest config"),
)
.await
.expect("create honest");
honest.start().await.expect("start honest");
let honest_addr = honest.local_addr().await.expect("honest address");
let deadline = Instant::now() + Duration::from_secs(5);
while honest.peers().await.is_empty() || victim.peers().await.is_empty() {
assert!(Instant::now() < deadline, "A and B never connected");
tokio::time::sleep(Duration::from_millis(25)).await;
}
honest
.broadcast(Bytes::from_static(b"legit-1"))
.await
.expect("broadcast legit-1");
wait_for_delivery("B to deliver legit-1", &delivered, |records| {
records.iter().any(|m| m == "legit-1")
})
.await;
let forger_identity = Identity::generate();
let forged = forger_identity
.author(
honest_addr,
999,
Payload::Application(Bytes::from_static(b"forged")),
)
.expect("author forged message");
let attacker = Tcp::new();
attacker
.connect(victim_addr)
.await
.expect("attacker connects");
attacker
.send(victim_addr, forged)
.await
.expect("attacker sends forged frame");
honest
.broadcast(Bytes::from_static(b"legit-2"))
.await
.expect("broadcast legit-2");
wait_for_delivery("B to deliver legit-2", &delivered, |records| {
records.iter().any(|m| m == "legit-2")
})
.await;
tokio::time::sleep(Duration::from_millis(200)).await;
attacker.shutdown().await;
honest.shutdown().await.ok();
victim.shutdown().await.ok();
let records = delivered.lock().expect("record lock");
assert!(
records.iter().any(|m| m == "legit-1") && records.iter().any(|m| m == "legit-2"),
"legitimate messages from the pinned origin are still delivered"
);
assert!(
!records.iter().any(|m| m == "forged"),
"a message spoofing a pinned origin must be rejected, got {records:?}"
);
}