/* 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 .
*/
use std::{
collections::HashMap,
sync::{
atomic::{AtomicU16, Ordering},
Arc, OnceLock,
},
};
use darkfi_sdk::{crypto::pasta_prelude::PrimeField, pasta::pallas};
use sled_overlay::sled;
use smol::{channel, future, Executor};
use url::Url;
use crate::{
error::Result,
event_graph::{proto::ProtocolEventGraph, Event, EventGraph, EventGraphConfig, EventGraphPtr},
net::{session::SESSION_DEFAULT, settings::NetworkProfile, P2p, Settings},
};
pub fn test_pregenerated_identity_commitments() -> Vec<[u8; 32]> {
vec![pallas::Base::from(0x4556_4752_u64).to_repr()]
}
pub fn test_config() -> EventGraphConfig {
EventGraphConfig {
initial_genesis: 1_704_067_200_000, // 2024-01-01 UTC
hours_rotation: 0,
genesis_contents: b"darkfi-test-graph".to_vec(),
pregenerated_identity_commitments: test_pregenerated_identity_commitments(),
max_dags: Some(24),
}
}
/// Bounded-mode config for tests that exercise [`DagStore`]
/// directly (without constructing an [`EventGraph`]).
///
/// Uses `hours_rotation = 1` so `DagStore::new` populates the
/// 24-slot rotation ring (vs the single-slot path under
/// `hours_rotation = 0`). Safe because no `EventGraph` is built,
/// so there's no prune task to leak.
pub fn bounded_dag_store_config() -> EventGraphConfig {
EventGraphConfig { hours_rotation: 1, ..test_config() }
}
/// Archive-mode config: never evicts old DAGs and discovers
/// existing trees from sled on construction. Like
/// [`bounded_dag_store_config`] this is for `DagStore`-direct
/// tests only.
pub fn archive_config() -> EventGraphConfig {
EventGraphConfig { max_dags: None, ..bounded_dag_store_config() }
}
/// Initialise tracing-subscriber once per process. Safe to call
/// multiple times. Tests that want to see log output can call this
/// at the top of their body.
pub fn init_logger() {
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_test_writer()
.try_init();
});
}
/// Process-wide [`ZkKeys`].
fn shared_zk_keys() -> Arc {
use crate::event_graph::rln::{
ZkKeys, RLN2_REGISTER_ZKBIN, RLN2_SIGNAL_ZKBIN, RLN2_SLASH_ZKBIN,
};
static SHARED: OnceLock> = OnceLock::new();
SHARED
.get_or_init(|| {
// Hash the three .zk.bin blobs to derive a stable per-version
// cache directory.
let mut hasher = blake3::Hasher::new();
hasher.update(RLN2_REGISTER_ZKBIN);
hasher.update(RLN2_SIGNAL_ZKBIN);
hasher.update(RLN2_SLASH_ZKBIN);
let zkbin_hash = hasher.finalize().to_hex();
let cache_dir =
std::env::temp_dir().join(format!("darkfi-test-zk-cache-{}", &zkbin_hash[..16]));
let db = sled::Config::new().path(&cache_dir).open().unwrap_or_else(|e| {
panic!(
"failed to open shared ZK key sled DB at {}: {e}\n\
(if the cache is corrupted, run `rm -rf {}`)",
cache_dir.display(),
cache_dir.display(),
)
});
let keys = ZkKeys::build_and_load(&db).expect("failed to build shared ZK keys");
Arc::new(keys)
})
.clone()
}
pub async fn make_eg() -> EventGraphPtr {
make_eg_with_config(test_config()).await
}
/// Construct an [`EventGraph`] with a caller-provided test config.
pub async fn make_eg_with_config(config: EventGraphConfig) -> EventGraphPtr {
let sled_db = sled::Config::new().temporary(true).open().unwrap();
make_eg_with_config_and_db(config, sled_db).await
}
/// Construct an [`EventGraph`] with a caller-provided config and sled DB.
pub async fn make_eg_with_config_and_db(
config: EventGraphConfig,
sled_db: sled::Db,
) -> EventGraphPtr {
let ex = Arc::new(Executor::new());
let p2p = P2p::new(Settings::default(), ex.clone()).await.unwrap();
EventGraph::with_zk_keys(p2p, sled_db, "/tmp".into(), false, config, shared_zk_keys(), ex)
.await
.unwrap()
}
/// Number of nodes a `make_network` call brings up.
pub const N_NODES: usize = 5;
/// Outbound peer count per node.
pub const N_CONNS: usize = 2;
/// Allocate a fresh non-overlapping TCP port range for one
/// `make_network` call. Process-wide counter so parallel tests
/// never collide.
fn alloc_port_base() -> u16 {
static NEXT: AtomicU16 = AtomicU16::new(13_400);
NEXT.fetch_add(N_NODES as u16, Ordering::SeqCst)
}
/// Spawn one `EventGraph` node on a local port, peered with the
/// given `peer_offsets` (relative to `port_base`).
async fn spawn_node(
port_base: u16,
port_offset: usize,
peer_offsets: Vec,
ex: Arc>,
) -> EventGraphPtr {
let mut profiles = HashMap::new();
profiles.insert(
"tcp".to_string(),
NetworkProfile { outbound_connect_timeout: 2, ..Default::default() },
);
let inbound =
vec![Url::parse(&format!("tcp://127.0.0.1:{}", port_base + port_offset as u16)).unwrap()];
let peers: Vec<_> = peer_offsets
.iter()
.map(|p| Url::parse(&format!("tcp://127.0.0.1:{}", port_base + *p as u16)).unwrap())
.collect();
let settings = Settings {
localnet: true,
inbound_addrs: inbound,
outbound_connections: 0,
inbound_connections: usize::MAX,
peers,
active_profiles: vec!["tcp".to_string()],
profiles,
..Default::default()
};
let p2p = P2p::new(settings, ex.clone()).await.unwrap();
let sled_db = sled::Config::new().temporary(true).open().unwrap();
let eg = EventGraph::with_zk_keys(
p2p.clone(),
sled_db,
"/tmp".into(),
false,
test_config(),
shared_zk_keys(),
ex.clone(),
)
.await
.unwrap();
// Mark synced so protocol handlers accept events during tests.
eg.synced.store(true, Ordering::Release);
let eg_weak = Arc::downgrade(&eg);
p2p.protocol_registry()
.register(SESSION_DEFAULT, move |channel, _| {
let eg_weak = eg_weak.clone();
async move {
let eg =
eg_weak.upgrade().expect("EventGraph dropped before protocol factory invoked");
ProtocolEventGraph::init(eg, channel).await.unwrap()
}
})
.await;
eg
}
/// Bootstrap an N-node ring, start the P2P stacks, and wait 5
/// seconds for connections to converge.
///
/// Each call gets a fresh non-overlapping port range, so multiple
/// `make_network` invocations can run in parallel.
pub async fn make_network(ex: Arc>) -> Vec {
use rand::{prelude::SliceRandom, rngs::ThreadRng};
let port_base = alloc_port_base();
let mut rng: ThreadRng = rand::thread_rng();
let idxs: Vec = (0..N_NODES).collect();
let mut nodes = vec![];
for i in 0..N_NODES {
let mut others = idxs.clone();
others.remove(i);
let conns: Vec = others.choose_multiple(&mut rng, N_CONNS).copied().collect();
nodes.push(spawn_node(port_base, i, conns, ex.clone()).await);
}
for eg in &nodes {
eg.p2p.clone().start().await.unwrap();
}
crate::system::sleep(5).await;
nodes
}
/// Stop every node's P2P stack. Call at end of multi-node tests.
pub async fn shutdown_network(nodes: &[EventGraphPtr]) {
for eg in nodes {
eg.p2p.clone().stop().await;
}
}
/// Run a multi-node test body on an executor sized for `N_NODES`.
pub fn run_multi_node_test(body: F)
where
F: FnOnce(Arc>) -> Fut,
Fut: std::future::Future