use std::sync::Arc;
use std::time::Duration;
use agent_uri_attestation::Verifier;
use agent_uri_dht::DhtError;
use futures::StreamExt;
use libp2p::kad::store::MemoryStore;
use libp2p::swarm::SwarmEvent;
use libp2p::{Swarm, identify, identity, kad, noise, tcp, yamux};
use tokio::sync::mpsc;
use crate::config::Libp2pConfig;
use crate::dht::Libp2pDht;
use crate::worker::{Behaviour, Worker};
const LISTEN_READY_TIMEOUT: Duration = Duration::from_secs(10);
const COMMAND_QUEUE: usize = 64;
pub async fn start(
config: Libp2pConfig,
verifier: Verifier,
keypair: identity::Keypair,
) -> Result<Libp2pDht, DhtError> {
let local_peer = libp2p::PeerId::from(keypair.public());
let mut swarm = build_swarm(&config, keypair)?;
let expected = config.listen_addresses.len();
for address in &config.listen_addresses {
swarm.listen_on(address.clone()).map_err(|error| {
DhtError::unavailable(format!("cannot listen on {address}: {error}"))
})?;
}
if expected > 0 {
await_listeners(&mut swarm, expected).await?;
swarm.behaviour_mut().kad.set_mode(Some(kad::Mode::Server));
}
let verifier = Arc::new(verifier);
let (commands, receiver) = mpsc::channel(COMMAND_QUEUE);
let worker = Worker::new(swarm, receiver, Arc::clone(&verifier), config.clone());
tokio::spawn(worker.run());
Ok(Libp2pDht::new(commands, local_peer, verifier, config))
}
fn build_swarm(
config: &Libp2pConfig,
keypair: identity::Keypair,
) -> Result<Swarm<Behaviour>, DhtError> {
let kad_config = kademlia_config(config);
let builder = libp2p::SwarmBuilder::with_existing_identity(keypair)
.with_tokio()
.with_tcp(
tcp::Config::default().nodelay(true),
noise::Config::new,
yamux::Config::default,
)
.map_err(|error| DhtError::unavailable(format!("cannot build transport: {error}")))?
.with_behaviour(|key| {
let peer = libp2p::PeerId::from(key.public());
Behaviour {
kad: kad::Behaviour::with_config(peer, MemoryStore::new(peer), kad_config),
identify: identify::Behaviour::new(identify::Config::new(
"/agent-uri/id/1.0.0".to_string(),
key.public(),
)),
}
})
.map_err(|error| DhtError::unavailable(format!("cannot build behaviour: {error}")))?
.with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_mins(1)));
Ok(builder.build())
}
fn kademlia_config(config: &Libp2pConfig) -> kad::Config {
let mut kad_config = kad::Config::new(config.protocol.clone());
kad_config
.set_record_filtering(kad::StoreInserts::FilterBoth)
.set_replication_factor(config.replication_factor)
.set_query_timeout(config.query_timeout)
.set_record_ttl(Some(config.max_record_ttl))
.set_publication_interval(Some(config.republication_interval))
.set_replication_interval(Some(config.republication_interval));
kad_config
}
async fn await_listeners(swarm: &mut Swarm<Behaviour>, expected: usize) -> Result<(), DhtError> {
let ready = async {
let mut seen = 0usize;
while seen < expected {
if let SwarmEvent::NewListenAddr { .. } = swarm.select_next_some().await {
seen += 1;
}
}
};
tokio::time::timeout(LISTEN_READY_TIMEOUT, ready)
.await
.map_err(|_| DhtError::unavailable("no listen address became ready"))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_node_reports_the_port_the_os_gave_it() {
let node = start(
Libp2pConfig::default().listening_on("/ip4/127.0.0.1/tcp/0".parse().unwrap()),
Verifier::new(),
identity::Keypair::generate_ed25519(),
)
.await
.unwrap();
let addresses = node.listen_addresses().await.unwrap();
assert_eq!(addresses.len(), 1);
assert!(!addresses[0].to_string().ends_with("/tcp/0"));
}
#[tokio::test]
async fn a_dial_address_carries_the_peer_identity() {
let node = start(
Libp2pConfig::default().listening_on("/ip4/127.0.0.1/tcp/0".parse().unwrap()),
Verifier::new(),
identity::Keypair::generate_ed25519(),
)
.await
.unwrap();
let dial = node.dial_addresses().await.unwrap();
assert!(dial[0].as_str().contains("/p2p/"));
}
#[tokio::test]
async fn a_client_node_needs_no_listen_address() {
let node = start(
Libp2pConfig::default(),
Verifier::new(),
identity::Keypair::generate_ed25519(),
)
.await
.unwrap();
assert!(node.listen_addresses().await.unwrap().is_empty());
}
}