use libp2p::{Multiaddr, PeerId};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use tenzro_network::{
MessagePayload, NetworkConfig, NetworkMessage, NetworkService, TenzroNetworkService,
ValidatorRegistry,
};
use tokio::time::timeout;
const CONSENSUS_TOPIC: &str = "tenzro/consensus";
const MESH_WAIT: Duration = Duration::from_secs(30);
const RECV_TIMEOUT: Duration = Duration::from_secs(10);
fn tcp_addrs(addrs: &[Multiaddr]) -> Vec<Multiaddr> {
use libp2p::multiaddr::Protocol;
addrs
.iter()
.filter(|a| a.iter().any(|p| matches!(p, Protocol::Tcp(_))))
.cloned()
.collect()
}
async fn spawn_node() -> (TenzroNetworkService, Vec<Multiaddr>) {
let config = NetworkConfig::local();
let service = TenzroNetworkService::new(config)
.await
.expect("network service spawned");
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
loop {
let addrs = service
.listen_addresses()
.await
.expect("listen_addresses() succeeds");
let tcp = tcp_addrs(&addrs);
if !tcp.is_empty() {
return (service, tcp);
}
if tokio::time::Instant::now() >= deadline {
panic!(
"node never bound a TCP listen address after 5s; addrs={:?}",
addrs
);
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
#[tokio::test]
async fn two_node_consensus_mesh_forms_and_broadcasts() {
let _ = tracing_subscriber::fmt::try_init();
let (node_a, addrs_a) = spawn_node().await;
let (node_b, _addrs_b) = spawn_node().await;
tracing::info!("Node A addrs: {:?}", addrs_a);
let mut rx_a = node_a
.subscribe(CONSENSUS_TOPIC)
.await
.expect("A subscribed");
let mut rx_b = node_b
.subscribe(CONSENSUS_TOPIC)
.await
.expect("B subscribed");
let dial_target = addrs_a[0].clone();
node_b
.dial(dial_target.clone())
.await
.expect("B dialed A");
let count_a = node_a
.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.expect("A mesh wait");
let count_b = node_b
.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.expect("B mesh wait");
assert!(
count_a >= 1,
"Node A's mesh on {} never reached >= 1 peer (got {}). \
This reproduces the testnet stall.",
CONSENSUS_TOPIC,
count_a
);
assert!(
count_b >= 1,
"Node B's mesh on {} never reached >= 1 peer (got {}). \
This reproduces the testnet stall.",
CONSENSUS_TOPIC,
count_b
);
let outbound = NetworkMessage::new(MessagePayload::Ping);
let outbound_id = outbound.message_id.clone();
node_a
.broadcast(CONSENSUS_TOPIC, outbound)
.await
.expect("A broadcast Ping");
let received = timeout(RECV_TIMEOUT, rx_b.recv())
.await
.expect("B received within timeout")
.expect("B's subscription channel still open");
assert_eq!(
received.message_id, outbound_id,
"B should receive the exact message A broadcast"
);
let echo = timeout(Duration::from_millis(500), rx_a.recv()).await;
assert!(
echo.is_err(),
"publisher should not receive its own broadcast"
);
}
#[tokio::test]
async fn four_node_consensus_mesh_full_propagation() {
let _ = tracing_subscriber::fmt::try_init();
let (n0, addrs_0) = spawn_node().await;
let (n1, _) = spawn_node().await;
let (n2, _) = spawn_node().await;
let (n3, _) = spawn_node().await;
let mut rx0 = n0.subscribe(CONSENSUS_TOPIC).await.expect("n0 sub");
let mut rx1 = n1.subscribe(CONSENSUS_TOPIC).await.expect("n1 sub");
let mut rx2 = n2.subscribe(CONSENSUS_TOPIC).await.expect("n2 sub");
let mut rx3 = n3.subscribe(CONSENSUS_TOPIC).await.expect("n3 sub");
let target = addrs_0[0].clone();
n1.dial(target.clone()).await.expect("n1 dialed n0");
n2.dial(target.clone()).await.expect("n2 dialed n0");
n3.dial(target.clone()).await.expect("n3 dialed n0");
let m0 = n0.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
let m1 = n1.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
let m2 = n2.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
let m3 = n3.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
assert!(m0 >= 1, "n0 mesh empty (got {})", m0);
assert!(m1 >= 1, "n1 mesh empty (got {})", m1);
assert!(m2 >= 1, "n2 mesh empty (got {})", m2);
assert!(m3 >= 1, "n3 mesh empty (got {})", m3);
let outbound = NetworkMessage::new(MessagePayload::Ping);
let id = outbound.message_id.clone();
n0.broadcast(CONSENSUS_TOPIC, outbound)
.await
.expect("n0 broadcast");
for (label, rx) in [
("n1", &mut rx1),
("n2", &mut rx2),
("n3", &mut rx3),
] {
let msg = timeout(RECV_TIMEOUT, rx.recv())
.await
.unwrap_or_else(|_| panic!("{} did not receive within {:?}", label, RECV_TIMEOUT))
.unwrap_or_else(|| panic!("{} subscription channel closed", label));
assert_eq!(msg.message_id, id, "{} got wrong message", label);
}
let echo = timeout(Duration::from_millis(500), rx0.recv()).await;
assert!(echo.is_err(), "n0 should not echo its own broadcast");
}
struct StaticValidatorRegistry(HashSet<PeerId>);
impl ValidatorRegistry for StaticValidatorRegistry {
fn is_validator(&self, peer_id: &PeerId) -> bool {
self.0.contains(peer_id)
}
fn validator_peer_ids(&self) -> HashSet<PeerId> {
self.0.clone()
}
}
#[tokio::test]
async fn four_node_with_validator_registry_propagates() {
let _ = tracing_subscriber::fmt::try_init();
let (n0, addrs_0) = spawn_node().await;
let (n1, _) = spawn_node().await;
let (n2, _) = spawn_node().await;
let (n3, _) = spawn_node().await;
let pid0 = n0.local_peer_id().await.unwrap();
let pid1 = n1.local_peer_id().await.unwrap();
let pid2 = n2.local_peer_id().await.unwrap();
let pid3 = n3.local_peer_id().await.unwrap();
let mut set = HashSet::new();
set.insert(pid0);
set.insert(pid1);
set.insert(pid2);
set.insert(pid3);
let registry = Arc::new(StaticValidatorRegistry(set));
n0.set_validator_registry(registry.clone()).await.unwrap();
n1.set_validator_registry(registry.clone()).await.unwrap();
n2.set_validator_registry(registry.clone()).await.unwrap();
n3.set_validator_registry(registry.clone()).await.unwrap();
let mut rx1 = n1.subscribe(CONSENSUS_TOPIC).await.unwrap();
let mut rx2 = n2.subscribe(CONSENSUS_TOPIC).await.unwrap();
let mut rx3 = n3.subscribe(CONSENSUS_TOPIC).await.unwrap();
let _rx0 = n0.subscribe(CONSENSUS_TOPIC).await.unwrap();
let target = addrs_0[0].clone();
n1.dial(target.clone()).await.unwrap();
n2.dial(target.clone()).await.unwrap();
n3.dial(target.clone()).await.unwrap();
let m0 = n0.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
let m1 = n1.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
let m2 = n2.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
let m3 = n3.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
assert!(m0 >= 1 && m1 >= 1 && m2 >= 1 && m3 >= 1,
"mesh formation failed with registry installed: m0={}, m1={}, m2={}, m3={}",
m0, m1, m2, m3);
let outbound = NetworkMessage::new(MessagePayload::Ping);
let id = outbound.message_id.clone();
n0.broadcast(CONSENSUS_TOPIC, outbound).await.unwrap();
for (label, rx) in [("n1", &mut rx1), ("n2", &mut rx2), ("n3", &mut rx3)] {
let msg = timeout(RECV_TIMEOUT, rx.recv())
.await
.unwrap_or_else(|_| {
panic!(
"{} did not receive within {:?} — validator-registry gate may be blocking",
label, RECV_TIMEOUT
)
})
.unwrap_or_else(|| panic!("{} channel closed", label));
assert_eq!(msg.message_id, id);
}
}
#[tokio::test]
async fn validator_registry_blocks_unknown_publisher() {
let _ = tracing_subscriber::fmt::try_init();
let (publisher, addrs_pub) = spawn_node().await;
let (receiver, _) = spawn_node().await;
let receiver_pid = receiver.local_peer_id().await.unwrap();
let mut set = HashSet::new();
set.insert(receiver_pid);
let registry = Arc::new(StaticValidatorRegistry(set));
receiver
.set_validator_registry(registry)
.await
.unwrap();
let mut rx = receiver.subscribe(CONSENSUS_TOPIC).await.unwrap();
let _publisher_rx = publisher.subscribe(CONSENSUS_TOPIC).await.unwrap();
receiver.dial(addrs_pub[0].clone()).await.unwrap();
let m_pub = publisher
.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
let m_recv = receiver
.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
assert!(m_pub >= 1 && m_recv >= 1, "mesh did not form: pub={}, recv={}", m_pub, m_recv);
let outbound = NetworkMessage::new(MessagePayload::Ping);
publisher
.broadcast(CONSENSUS_TOPIC, outbound)
.await
.expect("publisher broadcast (gossipsub layer succeeds)");
let result = timeout(Duration::from_secs(2), rx.recv()).await;
assert!(
result.is_err(),
"Receiver delivered a message from a non-validator publisher — \
the validator-only topic gate is NOT enforcing as designed. \
Got: {:?}",
result
);
}
struct DynamicValidatorRegistry {
inner: Arc<dashmap::DashMap<PeerId, ()>>,
}
impl ValidatorRegistry for DynamicValidatorRegistry {
fn is_validator(&self, peer_id: &PeerId) -> bool {
self.inner.contains_key(peer_id)
}
fn validator_peer_ids(&self) -> HashSet<PeerId> {
self.inner.iter().map(|e| *e.key()).collect()
}
fn try_add_validator(&self, peer_id: &PeerId) {
if !self.inner.contains_key(peer_id) {
self.inner.insert(*peer_id, ());
tracing::info!(peer = %peer_id, "DynamicValidatorRegistry admitted peer");
}
}
}
#[tokio::test]
async fn dynamic_admission_via_identify_propagates_consensus() {
let _ = tracing_subscriber::fmt::try_init();
let (n0, addrs_0) = spawn_node().await;
let (n1, _) = spawn_node().await;
let (n2, _) = spawn_node().await;
let (n3, _) = spawn_node().await;
for n in [&n0, &n1, &n2, &n3] {
let reg = Arc::new(DynamicValidatorRegistry {
inner: Arc::new(dashmap::DashMap::new()),
});
n.set_validator_registry(reg).await.unwrap();
}
let mut rx1 = n1.subscribe(CONSENSUS_TOPIC).await.unwrap();
let mut rx2 = n2.subscribe(CONSENSUS_TOPIC).await.unwrap();
let mut rx3 = n3.subscribe(CONSENSUS_TOPIC).await.unwrap();
let target = addrs_0[0].clone();
n1.dial(target.clone()).await.unwrap();
n2.dial(target.clone()).await.unwrap();
n3.dial(target.clone()).await.unwrap();
n0.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
n1.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
n2.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
n3.wait_for_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT).await.unwrap();
tokio::time::sleep(Duration::from_secs(2)).await;
let outbound = NetworkMessage::new(MessagePayload::Ping);
let id = outbound.message_id.clone();
n0.broadcast(CONSENSUS_TOPIC, outbound).await.unwrap();
for (label, rx) in [("n1", &mut rx1), ("n2", &mut rx2), ("n3", &mut rx3)] {
let msg = timeout(RECV_TIMEOUT, rx.recv())
.await
.unwrap_or_else(|_| {
panic!(
"{} never received via dynamic admission — identify path is broken",
label
)
})
.unwrap_or_else(|| panic!("{} channel closed", label));
assert_eq!(msg.message_id, id);
}
}
#[tokio::test]
async fn wait_for_admitted_mesh_is_sufficient_first_publish_gate() {
let _ = tracing_subscriber::fmt::try_init();
let (n0, addrs_0) = spawn_node().await;
let (n1, _) = spawn_node().await;
let (n2, _) = spawn_node().await;
let (n3, _) = spawn_node().await;
for n in [&n0, &n1, &n2, &n3] {
let reg = Arc::new(DynamicValidatorRegistry {
inner: Arc::new(dashmap::DashMap::new()),
});
n.set_validator_registry(reg).await.unwrap();
}
let mut rx1 = n1.subscribe(CONSENSUS_TOPIC).await.unwrap();
let mut rx2 = n2.subscribe(CONSENSUS_TOPIC).await.unwrap();
let mut rx3 = n3.subscribe(CONSENSUS_TOPIC).await.unwrap();
let target = addrs_0[0].clone();
n1.dial(target.clone()).await.unwrap();
n2.dial(target.clone()).await.unwrap();
n3.dial(target.clone()).await.unwrap();
let a0 = n0
.wait_for_admitted_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
let a1 = n1
.wait_for_admitted_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
let a2 = n2
.wait_for_admitted_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
let a3 = n3
.wait_for_admitted_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
assert!(
a0 >= 1 && a1 >= 1 && a2 >= 1 && a3 >= 1,
"wait_for_admitted_mesh did not converge: a0={}, a1={}, a2={}, a3={} \
— this would mean identify is not firing or registry is not getting populated.",
a0, a1, a2, a3
);
let outbound = NetworkMessage::new(MessagePayload::Ping);
let id = outbound.message_id.clone();
n0.broadcast(CONSENSUS_TOPIC, outbound).await.unwrap();
for (label, rx) in [("n1", &mut rx1), ("n2", &mut rx2), ("n3", &mut rx3)] {
let msg = timeout(RECV_TIMEOUT, rx.recv())
.await
.unwrap_or_else(|_| {
panic!(
"{} did not receive after wait_for_admitted_mesh succeeded — \
this means the gate fired prematurely",
label
)
})
.unwrap_or_else(|| panic!("{} channel closed", label));
assert_eq!(msg.message_id, id);
}
}
#[tokio::test]
async fn wait_for_admitted_mesh_permissive_when_no_registry() {
let _ = tracing_subscriber::fmt::try_init();
let (n0, addrs_0) = spawn_node().await;
let (n1, _) = spawn_node().await;
let mut rx1 = n1.subscribe(CONSENSUS_TOPIC).await.unwrap();
let _rx0 = n0.subscribe(CONSENSUS_TOPIC).await.unwrap();
n1.dial(addrs_0[0].clone()).await.unwrap();
let admitted = n0
.wait_for_admitted_mesh(CONSENSUS_TOPIC, 1, MESH_WAIT)
.await
.unwrap();
assert!(
admitted >= 1,
"permissive mode (no registry) should fall back to mesh count, got {}",
admitted
);
let outbound = NetworkMessage::new(MessagePayload::Ping);
let id = outbound.message_id.clone();
n0.broadcast(CONSENSUS_TOPIC, outbound).await.unwrap();
let msg = timeout(RECV_TIMEOUT, rx1.recv())
.await
.expect("permissive mode delivery within timeout")
.expect("channel still open");
assert_eq!(msg.message_id, id);
}