/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
// cargo test --release --features=net --lib p2p -- --include-ignored
use std::{
collections::{HashMap, HashSet},
net::TcpListener,
panic,
sync::Arc,
};
use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
use rand::{prelude::SliceRandom, rngs::ThreadRng, Rng};
use smol::{channel, future, Executor};
use tracing::{error, info, warn};
use url::Url;
use crate::{
net::{
hosts::HostColor,
message::{GetAddrsMessage, Message},
metering::{MeteringConfiguration, DEFAULT_METERING_CONFIGURATION},
settings::NetworkProfile,
P2p, Settings,
},
system::sleep,
util::logger::{setup_test_logger, Level},
};
fn init_logger() {
let ignored_targets = [
"sled",
"net::protocol_ping",
"net::channel::subscribe_stop()",
"net::hosts",
"net::session",
"net::outbound_session",
"net::inbound_session",
"net::message_publisher",
"net::protocol_address",
"net::protocol_version",
"net::protocol_registry",
"net::protocol_jobs_manager",
"net::channel::send()",
"net::channel::start()",
"net::channel::handle_stop()",
"net::channel::subscribe_msg()",
"net::channel::main_receive_loop()",
"net::tcp",
];
// We check this error so we can execute same file tests in parallel,
// otherwise second one fails to init logger here.
if setup_test_logger(
&ignored_targets,
false,
//Level::Info,
Level::Verbose,
//Level::Debug,
//Level::Trace,
)
.is_err()
{
warn!(target: "test_harness", "Logger already initialized");
}
}
fn get_random_available_port() -> usize {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port.into()
}
fn get_unique_ports(n_nodes: usize) -> Vec {
let mut ports = HashSet::new();
while ports.len() < n_nodes {
ports.insert(get_random_available_port());
}
ports.into_iter().collect()
}
async fn spawn_seed_session(
seed_addr: Url,
ex: Arc>,
n_nodes: usize,
) -> Vec> {
info!("========================================================");
info!("Initializing outbound nodes...");
info!("========================================================");
let mut outbound_instances = vec![];
let ports = get_unique_ports(n_nodes);
let mut profiles = HashMap::new();
profiles.insert(
"tcp".to_string(),
NetworkProfile { outbound_connect_timeout: 2, ..Default::default() },
);
for port in ports {
let settings = Settings {
localnet: true,
inbound_addrs: vec![Url::parse(&format!("tcp://127.0.0.1:{port}")).unwrap()],
external_addrs: vec![Url::parse(&format!("tcp://127.0.0.1:{port}")).unwrap()],
outbound_connections: 2,
outbound_peer_discovery_cooloff_time: 2,
inbound_connections: usize::MAX,
greylist_refinery_interval: 15,
peers: vec![],
seeds: vec![seed_addr.clone()],
node_id: (port).to_string(),
active_profiles: vec!["tcp".to_string()],
profiles: profiles.clone(),
..Default::default()
};
let p2p = P2p::new(settings, ex.clone()).await.unwrap();
outbound_instances.push(p2p);
}
outbound_instances
}
async fn spawn_manual_session(
ex: Arc>,
n_nodes: usize,
n_conns: usize,
) -> Vec> {
info!("========================================================");
info!("Initializing manual nodes...");
info!("========================================================");
let mut manual_instances = vec![];
let mut rng = rand::thread_rng();
let ports = get_unique_ports(n_nodes);
let mut profiles = HashMap::new();
profiles.insert(
"tcp".to_string(),
NetworkProfile { outbound_connect_timeout: 2, ..Default::default() },
);
for i in 0..n_nodes {
let mut peer_indexes_copy: Vec = (0..n_nodes).collect();
peer_indexes_copy.remove(i);
let peer_indexes_to_connect: Vec<_> =
peer_indexes_copy.choose_multiple(&mut rng, n_conns).collect();
let mut peers = vec![];
for &peer_index in peer_indexes_to_connect {
let port = ports[peer_index];
peers.push(Url::parse(&format!("tcp://127.0.0.1:{port}")).unwrap());
}
let inbound_port = ports[i];
let settings = Settings {
localnet: true,
inbound_addrs: vec![Url::parse(&format!("tcp://127.0.0.1:{inbound_port}")).unwrap()],
external_addrs: vec![Url::parse(&format!("tcp://127.0.0.1:{inbound_port}")).unwrap()],
outbound_connections: 2,
outbound_peer_discovery_cooloff_time: 2,
inbound_connections: usize::MAX,
greylist_refinery_interval: 15,
peers,
seeds: vec![],
node_id: inbound_port.to_string(),
active_profiles: vec!["tcp".to_string()],
profiles: profiles.clone(),
..Default::default()
};
let p2p = P2p::new(settings, ex.clone()).await.unwrap();
manual_instances.push(p2p);
}
manual_instances
}
async fn get_random_gold_host(
outbound_instances: &[Arc],
index: usize,
) -> ((Url, u64), usize) {
let random_node = &outbound_instances[index];
let hosts = random_node.hosts();
let external_addr = random_node.settings().read().await.external_addrs[0].clone();
info!("========================================================");
info!("Getting gold addr from node={external_addr}");
info!("========================================================");
let list = hosts.container.hostlists[HostColor::Gold as usize].read().unwrap();
assert!(!list.is_empty());
let position = rand::thread_rng().gen_range(0..list.len());
let entry = &list[position];
(entry.clone(), position)
}
async fn _check_random_hostlist(outbound_instances: &[Arc], rng: &mut ThreadRng) {
let mut urls = HashSet::new();
let random_node = outbound_instances.choose(rng).unwrap();
let external_addr = random_node.settings().read().await.external_addrs[0].clone();
info!("========================================================");
info!("Checking node={external_addr}");
info!("========================================================");
let greylist = random_node.hosts().container.fetch_all(HostColor::Grey);
let whitelist = random_node.hosts().container.fetch_all(HostColor::White);
let goldlist = random_node.hosts().container.fetch_all(HostColor::Gold);
for (url, _) in greylist {
assert!(urls.insert(url));
}
for (url, _) in whitelist {
assert!(urls.insert(url));
}
for (url, _) in goldlist {
assert!(urls.insert(url));
}
assert!(!urls.is_empty());
}
async fn check_all_hostlist(outbound_instances: &Vec>) {
for node in outbound_instances {
let external_addr = &node.settings().read().await.external_addrs[0].clone();
info!("========================================================");
info!("Checking node={external_addr}");
info!("========================================================");
let mut urls = HashSet::new();
let greylist = node.hosts().container.fetch_all(HostColor::Grey);
let whitelist = node.hosts().container.fetch_all(HostColor::White);
let goldlist = node.hosts().container.fetch_all(HostColor::Gold);
for (url, _) in greylist {
assert!(urls.insert(url));
}
for (url, _) in whitelist {
assert!(urls.insert(url));
}
for (url, _) in goldlist {
assert!(urls.insert(url));
}
assert!(!urls.is_empty());
}
}
async fn kill_node(outbound_instances: &Vec>, node: Url) {
for p2p in outbound_instances {
if p2p.settings().read().await.external_addrs[0] == node {
info!("========================================================");
info!("Shutting down node: {}", p2p.settings().read().await.external_addrs[0]);
info!("========================================================");
p2p.stop().await;
}
}
}
macro_rules! test_body {
($real_call:ident, $threads:expr) => {
init_logger();
let ex = Arc::new(Executor::new());
let ex_ = ex.clone();
let (signal, shutdown) = channel::unbounded::<()>();
panic::set_hook(Box::new(|panic_info| {
error!("Panic occurred: {:?}", panic_info);
}));
// Run a thread for each node.
easy_parallel::Parallel::new()
.each(0..$threads, |_| {
let result = std::panic::catch_unwind(|| {
let res = future::block_on(ex.run(shutdown.recv()));
res
});
if let Err(err) = result {
error!("Thread panicked: {:?}", err);
}
})
.finish(|| {
future::block_on(async {
$real_call(ex_).await;
drop(signal);
});
});
};
}
#[test]
fn p2p_test() {
test_body!(p2p_test_real, 5);
}
async fn p2p_test_real(ex: Arc>) {
// Number of nodes to spawn and number of peers each node connects to
const N_NODES: usize = 5;
const N_CONNS: usize = 4;
// ============================================================
// 1. Create a new seed node.
// ============================================================
let seed_port = get_random_available_port();
let seed_addr = Url::parse(&format!("tcp://127.0.0.1:{seed_port}")).unwrap();
let settings = Settings {
localnet: true,
inbound_addrs: vec![seed_addr.clone()],
outbound_connections: 0,
inbound_connections: usize::MAX,
seeds: vec![],
peers: vec![],
active_profiles: vec!["tcp".to_string()],
greylist_refinery_interval: 12,
node_id: "seed".to_string(),
..Default::default()
};
let seed = P2p::new(settings, ex.clone()).await.unwrap();
info!("========================================================");
info!("Starting seed node on {seed_addr}");
info!("========================================================");
seed.clone().start().await.unwrap();
// ============================================================
// 2. Spawn outbound nodes that will connect to the seed node.
// ============================================================
let outbound_instances = spawn_seed_session(seed_addr, ex.clone(), N_NODES).await;
for p2p in &outbound_instances {
info!("========================================================");
info!("Starting node={}", p2p.settings().read().await.external_addrs[0]);
info!("========================================================");
p2p.clone().start().await.unwrap();
}
info!("========================================================");
info!("Waiting 10s for all peers to reach the seed node");
info!("========================================================");
sleep(10).await;
// ===========================================================
// 3. Assert that all nodes have shared their external addr
// with the seed node.
// ===========================================================
let greylist = seed.hosts().container.fetch_all(HostColor::Grey);
assert!(greylist.len() == N_NODES);
info!("========================================================");
info!("Seedsync session successful!");
info!("========================================================");
info!("========================================================");
info!("Waiting 5s for seed node refinery to kick in...");
info!("========================================================");
sleep(5).await;
// ===========================================================
// 4. Assert that seed node has at least one whitelist entry,
// indicating that the refinery process is happening correctly.
// ===========================================================
assert!(!seed.hosts().container.is_empty(HostColor::White));
info!("========================================================");
info!("Checking seed={}", seed.settings().read().await.inbound_addrs[0]);
info!("========================================================");
let mut urls = HashSet::new();
let greylist = seed.hosts().container.fetch_all(HostColor::Grey);
let whitelist = seed.hosts().container.fetch_all(HostColor::White);
let goldlist = seed.hosts().container.fetch_all(HostColor::Gold);
for (url, _) in greylist {
info!("Found grey url: {url}");
assert!(urls.insert(url));
}
for (url, _) in whitelist {
info!("Found white url: {url}");
assert!(urls.insert(url));
}
for (url, _) in goldlist {
info!("Found gold url: {url}");
assert!(urls.insert(url));
}
assert!(!urls.is_empty());
info!("========================================================");
info!("Seed node refinery operating successfully!");
info!("========================================================");
info!("========================================================");
info!("Waiting 10s for seed refinery...");
info!("========================================================");
sleep(10).await;
let whitelist = seed.hosts().container.fetch_all(HostColor::White);
assert!(whitelist.len() >= 2);
// ===========================================================
// 5. Select a random peer and ensure that its hostlist is not
// empty. This ensures the seed node is sharing whitelisted
// nodes around the network.
// ===========================================================
check_all_hostlist(&outbound_instances).await;
info!("========================================================");
info!("Peers successfully received addrs!");
info!("========================================================");
info!("========================================================");
info!("Waiting 5s for outbound loop to connect...");
info!("========================================================");
sleep(5).await;
// ===========================================================
// 6. Select a random gold peer from one of the nodes and kill
// it.
// ===========================================================
info!("========================================================");
info!("Selecting a random gold entry...");
info!("========================================================");
let random_node_index = rand::thread_rng().gen_range(0..outbound_instances.len());
let ((addr, _), _) = get_random_gold_host(&outbound_instances, random_node_index).await;
kill_node(&outbound_instances, addr.clone()).await;
info!("========================================================");
info!("Waiting for greylist downgrade sequence to occur...");
info!("========================================================");
// ===========================================================
// 7. Verify the peer has been removed from the Gold list.
// ===========================================================
outbound_instances[random_node_index]
.hosts()
.container
.contains(HostColor::Grey as usize, &addr);
info!("========================================================");
info!("Greylist downgrade occured successfully!");
info!("========================================================");
info!("========================================================");
info!("Seed session successful! Shutting down seed test...");
info!("========================================================");
// ===========================================================
// 8. Stop the P2P network
// ===========================================================
for p2p in outbound_instances.iter() {
p2p.clone().stop().await;
}
seed.clone().stop().await;
info!("========================================================");
info!("Seed test shutdown complete! Starting manual test...");
info!("========================================================");
let manual_instances = spawn_manual_session(ex.clone(), N_NODES, N_CONNS).await;
for p2p in &manual_instances {
info!("========================================================");
info!("Starting node={}", p2p.settings().read().await.external_addrs[0]);
info!("========================================================");
p2p.clone().start().await.unwrap();
}
info!("========================================================");
info!("Waiting 5s for all manual peers to connect");
info!("========================================================");
sleep(5).await;
info!("========================================================");
info!("Checking manual nodes connected successfully...");
info!("========================================================");
for p2p in manual_instances.clone() {
// We should have (N_CONNS outbound + N_CONNS inbound)
// connections at this point.
info!("========================================================");
info!("Checking manual node={}", p2p.settings().read().await.node_id);
info!("========================================================");
let peers = p2p.hosts().peers();
assert!(peers.len() == N_CONNS * 2);
}
info!("========================================================");
info!("Manual session successful! Shutting down manual test...");
info!("========================================================");
// ===========================================================
// 8. Stop the P2P network
// ===========================================================
for p2p in manual_instances.clone() {
p2p.clone().stop().await;
}
}
#[test]
fn p2p_channel_unsupported_message_type_gets_banned() {
test_body!(p2p_channel_unsupported_message_type_gets_banned_real, 2);
}
async fn p2p_channel_unsupported_message_type_gets_banned_real(ex: Arc>) {
// Test with two nodes directly connected to each other
let manual_instances = spawn_manual_session(ex.clone(), 2, 1).await;
for p2p in &manual_instances {
p2p.clone().start().await.unwrap();
}
// Let's wait for the nodes to connect to each other
sleep(5).await;
let node1_p2p = manual_instances[0].clone();
let node2_p2p = manual_instances[1].clone();
let channel = node1_p2p.hosts().channels().first().unwrap().clone();
// Create a new message type
#[derive(SerialEncodable, SerialDecodable)]
struct CustomMessage(u32);
crate::impl_p2p_message!(
CustomMessage,
"UnsupportedMessage",
0,
0,
DEFAULT_METERING_CONFIGURATION
);
let instance = CustomMessage(23);
channel.send(&instance).await.unwrap();
sleep(1).await;
// Node1 should be banned by Node2
assert_eq!(node2_p2p.hosts().container.fetch_all(HostColor::Black).len(), 1);
node1_p2p.stop().await;
node2_p2p.stop().await;
}
#[test]
fn p2p_channel_invalid_command_length_gets_banned() {
test_body!(p2p_channel_invalid_command_length_gets_banned_real, 2);
}
async fn p2p_channel_invalid_command_length_gets_banned_real(ex: Arc>) {
// Test with two nodes directly connected to each other
let manual_instances = spawn_manual_session(ex.clone(), 2, 1).await;
for p2p in &manual_instances {
p2p.clone().start().await.unwrap();
}
// Let's wait for the nodes to connect to each other
sleep(5).await;
let node1_p2p = manual_instances[0].clone();
let node2_p2p = manual_instances[1].clone();
let channel = node1_p2p.hosts().channels().first().unwrap().clone();
// Create a custom message that has invalid length command name
#[derive(SerialEncodable, SerialDecodable)]
struct CustomMessage(u32);
// The length of COMMAND_NAME is greater than message::MAX_COMMAND_LENGTH, this one is 256
const COMMAND_NAME: &str =
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
crate::impl_p2p_message!(CustomMessage, &COMMAND_NAME, 0, 0, DEFAULT_METERING_CONFIGURATION);
let instance = CustomMessage(23);
channel.send(&instance).await.unwrap();
sleep(1).await;
// Node1 should be banned by Node2
assert_eq!(node2_p2p.hosts().container.fetch_all(HostColor::Black).len(), 1);
node1_p2p.stop().await;
node2_p2p.stop().await;
}
#[test]
fn p2p_channel_invalid_message_length_gets_banned() {
test_body!(p2p_channel_invalid_message_length_gets_banned_real, 2);
}
async fn p2p_channel_invalid_message_length_gets_banned_real(ex: Arc>) {
// Test with two nodes directly connected to each other
let manual_instances = spawn_manual_session(ex.clone(), 2, 1).await;
for p2p in &manual_instances {
p2p.clone().start().await.unwrap();
}
// Let's wait for the nodes to connect to each other
sleep(5).await;
let node1_p2p = manual_instances[0].clone();
let node2_p2p = manual_instances[1].clone();
let channel = node1_p2p.hosts().channels().first().unwrap().clone();
// Let's create a GetAddrsMessage that will be over the GET_ADDRS_MAX_BYTES threshold
let message = GetAddrsMessage { max: 20, transports: vec!["tor".to_string(); 256] };
channel.send(&message).await.unwrap();
sleep(1).await;
// Node1 should be banned by Node2
assert_eq!(node2_p2p.hosts().container.fetch_all(HostColor::Black).len(), 1);
node1_p2p.stop().await;
node2_p2p.stop().await;
}