Skip to main content
TAPLE Core uses libp2p for peer-to-peer networking, providing flexible transport options and efficient peer discovery through Kademlia DHT.

Network Settings

Configure the P2P network through NetworkSettings (from core/src/commons/settings/mod.rs:18-38):
pub struct NetworkSettings {
    /// Multiaddr to listen on
    pub listen_addr: Vec<ListenAddr>,
    
    /// Bootstrap nodes to connect to
    pub known_nodes: Vec<String>,
    
    /// External addresses to advertise
    pub external_address: Vec<String>,
}

impl Default for NetworkSettings {
    fn default() -> Self {
        Self {
            listen_addr: vec![ListenAddr::default()],
            known_nodes: Vec::<String>::new(),
            external_address: vec![],
        }
    }
}

Listen Addresses

TAPLE supports multiple transport protocols through the ListenAddr enum.

Available Transports

From core/src/commons/settings/mod.rs:44-57:
pub enum ListenAddr {
    /// In-memory addressing (for testing)
    Memory { port: Option<u32> },
    
    /// IPv4 address
    IP4 {
        addr: Option<std::net::Ipv4Addr>,
        port: Option<u32>,
    },
    
    /// IPv6 address
    IP6 {
        addr: Option<std::net::Ipv6Addr>,
        port: Option<u32>,
    },
}

Default Configuration

The default listen address is 0.0.0.0:40040 (from core/src/commons/settings/mod.rs:59-66):
impl Default for ListenAddr {
    fn default() -> Self {
        Self::IP4 {
            addr: Some(std::net::Ipv4Addr::new(0, 0, 0, 0)),
            port: Some(40040),
        }
    }
}

MultiAddr Format

Addresses are represented as MultiAddr strings:
// IPv4
"/ip4/0.0.0.0/tcp/40040"
"/ip4/192.168.1.100/tcp/50000"

// IPv6  
"/ip6/::/tcp/40040"
"/ip6/::1/tcp/50000"

// Memory (testing only)
"/memory/12345"
Parse from string (from core/src/commons/settings/mod.rs:124-223):
let listen_addr = ListenAddr::try_from("/ip4/0.0.0.0/tcp/40040".to_string())?;

Network Processor

The NetworkProcessor manages the libp2p swarm and handles network events.

Initialization

From core/src/network/network.rs:173-235:
pub fn new(
    addr: Vec<ListenAddr>,
    bootstrap_nodes: Vec<(PeerId, Multiaddr)>,
    event_sender: mpsc::Sender<NetworkEvent>,
    controller_mc: KeyPair,
    token: CancellationToken,
    notification_tx: tokio::sync::mpsc::Sender<Notification>,
    external_addresses: Vec<Multiaddr>,
) -> Result<Self, Box<dyn Error>> {
    // Validate transport protocol
    let transport_protocol = check_listen_addr_integrity(&addr)?;
    
    // Get public key and create libp2p keypair
    let public_key = controller_mc.public_key_bytes();
    let local_key = {
        let sk = ed25519::SecretKey::from_bytes(controller_mc.secret_key_bytes())
            .expect("we always pass 32 bytes");
        Keypair::Ed25519(sk.into())
    };
    
    // Create authenticated noise keypair
    let noise_key: noise::AuthenticKeypair<noise::X25519Spec> =
        noise::Keypair::<noise::X25519Spec>::new()
            .into_authentic(&local_key)
            .expect("Signing libp2p-noise static DH keypair failed.");
    
    // Create transport
    let transport = create_transport_by_protocol(transport_protocol, noise_key);
    let peer_id = local_key.public().to_peer_id();
    
    // Build swarm
    let swarm = SwarmBuilder::new(
        transport,
        TapleNetworkBehavior::new(local_key, bootstrap_nodes.clone()),
        peer_id,
    )
    .executor(Box::new(|fut| {
        tokio::spawn(fut);
    }))
    .build();
    
    // Create channels
    let (command_sender, command_receiver) = mpsc::channel(10000);
    
    Ok(Self {
        node_public_key: public_key,
        addr,
        swarm,
        command_sender,
        command_receiver,
        event_sender,
        pendings: HashMap::new(),
        active_get_querys: HashSet::new(),
        token,
        notification_tx,
        bootstrap_nodes,
        pending_bootstrap_nodes: HashMap::new(),
        bootstrap_retries_steam: futures::stream::futures_unordered::FuturesUnordered::new(),
        external_addresses,
    })
}

Transport Protocols

TAPLE creates different transports based on the listen address type.

TCP Transport (Production)

From core/src/network/network.rs:666-702:
fn create_ip4_ip6_transport(
    noise_key: noise::AuthenticKeypair<noise::X25519Spec>,
) -> Boxed<(PeerId, StreamMuxerBox)> {
    let transport = TokioTcpConfig::new()
        .nodelay(true)
        .upgrade(upgrade::Version::V1)
        .authenticate(noise::NoiseConfig::xx(noise_key.clone()).into_authenticated())
        .multiplex(mplex::MplexConfig::new())
        .boxed();
    
    // Add DNS resolution
    match dns::GenDnsConfig::system(transport) {
        Ok(t) => t.boxed(),
        Err(_) => {
            // Fallback to Cloudflare DNS
            let transport = TokioTcpConfig::new()
                .nodelay(true)
                .upgrade(upgrade::Version::V1)
                .authenticate(noise::NoiseConfig::xx(noise_key.clone()).into_authenticated())
                .multiplex(mplex::MplexConfig::new())
                .boxed();
            
            match dns::GenDnsConfig::custom(
                transport,
                dns::ResolverConfig::cloudflare(),
                dns::ResolverOpts::default(),
            ) {
                Ok(t) => t.boxed(),
                Err(_) => TokioTcpConfig::new()
                    .nodelay(true)
                    .upgrade(upgrade::Version::V1)
                    .authenticate(noise::NoiseConfig::xx(noise_key.clone()).into_authenticated())
                    .multiplex(mplex::MplexConfig::new())
                    .boxed(),
            }
        }
    }
}

Memory Transport (Testing)

From core/src/network/network.rs:704-716:
ListenProtocols::Memory => MemoryTransport
    .upgrade(upgrade::Version::V1)
    .authenticate(noise::NoiseConfig::xx(noise_key.clone()).into_authenticated())
    .multiplex(yamux::YamuxConfig::default())
    .boxed()

Network Behavior

TAPLE uses a composed behavior combining routing (Kademlia) and messaging (Tell protocol). From core/src/network/network.rs:63-95:
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "NetworkComposedEvent")]
pub struct TapleNetworkBehavior {
    routing: RoutingBehaviour,
    tell: TellBehaviour,
}

impl TapleNetworkBehavior {
    pub fn new(local_key: Keypair, bootstrap_nodes: Vec<(PeerId, Multiaddr)>) -> Self {
        let routing = RoutingBehaviour::new(local_key, bootstrap_nodes);
        let tell = TellBehaviour::new(
            100000,
            Duration::from_secs(10),
            Duration::from_secs(10)
        );
        TapleNetworkBehavior { routing, tell }
    }
}

Peer Discovery

TAPLE uses Kademlia DHT for peer discovery and routing.

Bootstrap Nodes

Connect to bootstrap nodes on startup (from core/src/network/network.rs:262-266):
for (_peer_id, addr) in self.bootstrap_nodes.iter() {
    let Ok(()) = self.swarm.dial(addr.to_owned()) else {
        panic!("Connection with bootstrap failed");
    };
}
Bootstrap node format:
let bootstrap_nodes = vec![
    "/ip4/35.181.1.20/tcp/40040/p2p/12D3KooWLXexpg81PjdjnrhmHUxN7U5EtfXJgr9cahei1SJ9Ub3B"
        .to_string(),
];

Automatic Retry

Failed bootstrap connections are automatically retried (from core/src/network/network.rs:349-367):
SwarmEvent::OutgoingConnectionError { error, peer_id } => {
    if let Some(peer_id) = peer_id {
        // Remove failed route
        self.swarm.behaviour_mut().tell.remove_route(&peer_id);
        
        // Check if this was a bootstrap node
        if let Some((id, multiaddr)) =
            self.bootstrap_nodes.iter().find(|(id, _)| *id == peer_id)
        {
            // Add to pending retry list
            self.pending_bootstrap_nodes
                .insert(*id, multiaddr.to_owned());
            
            // Schedule retry if not already scheduled
            if self.bootstrap_retries_steam.len() == 0 {
                self.bootstrap_retries_steam
                    .push(tokio::time::sleep(Duration::from_millis(30000)));
            }
        }
    }
}

Closest Peer Query

Find peers through DHT queries (from core/src/network/network.rs:612-625):
if let None = self.active_get_querys.get(&peer_id) {
    // Make query to find peer
    self.active_get_querys.insert(peer_id.clone());
    let query_id = self
        .swarm
        .behaviour_mut()
        .routing
        .get_closest_peers(peer_id.clone());
    
    debug!(
        "Query get_record {:?} to send request to {:?}",
        query_id, peer_id
    );
}

Message Sending

Messages are sent through the Tell protocol with automatic routing.

Send Command

From core/src/network/network.rs:575-640:
Command::SendMessage { receptor, message } => {
    // Check if we are the receptor (loopback)
    if receptor == self.node_public_key {
        self.event_sender
            .send(NetworkEvent::MessageReceived { message })
            .await
            .expect("Event receiver not to be dropped.");
        return;
    }
    
    // Convert public key to PeerId
    let peer_id = match libp2p::identity::ed25519::PublicKey::decode(&receptor) {
        Ok(public_key) => {
            let public_key = libp2p::core::PublicKey::Ed25519(public_key);
            PeerId::from_public_key(&public_key)
        }
        Err(_error) => {
            log::error!("Invalid controller ID");
            return;
        }
    };
    
    // Check if we have the peer's address
    let addresses_of_peer = self.swarm.behaviour_mut().addresses_of_peer(&peer_id);
    if !addresses_of_peer.is_empty() {
        // Send immediately
        self.swarm
            .behaviour_mut()
            .tell
            .send_message(&peer_id, &message);
        return;
    }
    
    // Query DHT for peer if not already querying
    if let None = self.active_get_querys.get(&peer_id) {
        self.active_get_querys.insert(peer_id.clone());
        self.swarm
            .behaviour_mut()
            .routing
            .get_closest_peers(peer_id.clone());
    }
    
    // Queue message until peer is found
    match self.pendings.get_mut(&peer_id) {
        Some(pending_list) => {
            if pending_list.len() >= 100 {
                pending_list.pop_front();
            }
            pending_list.push_back(message);
        }
        None => {
            let mut pendings = VecDeque::new();
            pendings.push_back(message);
            self.pendings.insert(peer_id, pendings);
        }
    }
}

External Addresses

Advertise external addresses for NAT traversal (from core/src/network/network.rs:244-248):
for external_address in self.external_addresses.clone().into_iter() {
    self.swarm
        .add_external_address(external_address, AddressScore::Infinite);
}
Do not mix Memory and TCP transports in the same node. The network processor validates that all listen addresses use the same protocol type.

Configuration Examples

Development (Single Node)

let mut settings = Settings::default();
settings.network.listen_addr = vec![
    ListenAddr::IP4 {
        addr: Some(std::net::Ipv4Addr::new(127, 0, 0, 1)),
        port: Some(40040),
    }
];
settings.network.known_nodes = vec![];

Production (With Bootstrap)

let mut settings = Settings::default();
settings.network.listen_addr = vec![
    ListenAddr::IP4 {
        addr: Some(std::net::Ipv4Addr::new(0, 0, 0, 0)),
        port: Some(40040),
    }
];
settings.network.known_nodes = vec![
    "/ip4/35.181.1.20/tcp/40040/p2p/12D3KooWLXexpg81PjdjnrhmHUxN7U5EtfXJgr9cahei1SJ9Ub3B"
        .to_string(),
];
settings.network.external_address = vec![
    "/ip4/203.0.113.42/tcp/40040".to_string(),
];

Testing (Memory Transport)

let mut settings = Settings::default();
settings.network.listen_addr = vec![
    ListenAddr::Memory { port: Some(12345) }
];

Best Practices

Port Configuration

  • Use ports above 1024 to avoid requiring root privileges
  • Default port is 40040 - change if running multiple nodes on same host
  • Ensure firewall rules allow incoming connections

Bootstrap Nodes

  • Use at least 2-3 bootstrap nodes for redundancy
  • Bootstrap nodes should have static IPs or DNS names
  • Include your own bootstrap nodes for private networks

External Addresses

  • Configure external addresses if behind NAT
  • Use DNS names for dynamic IPs
  • Test connectivity from outside your network

Security

  • All connections use Noise protocol encryption
  • Ed25519 keys for node identity
  • Authenticated transport prevents impersonation

Build docs developers (and LLMs) love