Skip to content

Peer to Peer

The Karak Peer to Peer (P2P) library KarakP2P is a Rust library that provides a peer-to-peer (P2P) networking solution using the libp2p framework. It combines Gossipsub for publish-subscribe messaging and Kademlia for distributed hash table (DHT) functionality. This library allows you to create decentralized applications with efficient message passing and peer discovery.

Create a KarakP2P Instance

To create and start a KarakP2P node, use the create_and_start_swarm method:

pub fn create_and_start_swarm(
    topic: &str,
    listen_addr: Multiaddr,
    bootstrap_addrs: Vec<P2PNode>,
    termination_receiver: oneshot::Receiver<()>,
    message_receiver: mpsc::Receiver<GossipMessage<M>>,
    idle_timeout_duration: u64,
) -> Result<Self, KarakP2PError>
  • topic: The Gossipsub topic to subscribe to.
  • listen_addr: The multiaddress to listen on.
  • bootstrap_addrs: A list of known peers to connect to initially.
  • termination_receiver: A channel receiver for graceful shutdown.
  • message_receiver: A channel receiver for outgoing messages.
  • idle_timeout_duration: The duration in seconds for idle connection timeout.
let (termination_sender, termination_receiver) = oneshot::channel();
let (message_sender, message_receiver) = mpsc::channel(100);
 
let mut karak_p2p = KarakP2P::create_and_start_swarm(
    "your_topic",
    "/ip4/127.0.0.1/tcp/0".parse()?,
    vec![],
    termination_receiver,
    message_receiver,
    60
)?;

Starting the P2P Node

The start_listening method takes a closure that handles incoming messages. This method runs in a loop, processing network events and incoming/outgoing messages.

pub async fn start_listening<F, Fut>(
        &mut self,
        on_incoming_message: F,
    ) -> Result<(), KarakP2PError>
where
    F: Fn(PeerId, MessageId, Message) -> Fut + Send + Sync + 'static,
    Fut: Future<Output = ()> + Send,
  • on_incoming_message: is a closure that takes in a peer_id, message_id, and message. This closure should be used to write the logic for handling incoming messages.
karak_p2p.start_listening(|peer_id, message_id, message| {
    println!("Received message from {:?}: {:?}", peer_id, message);
}).await?;

Publishing Messages

Since the P2P library requires a continously running loop, we use a MPSC channel to send messages. The message sender in the above example is message_sender that we created before the create_and_start_swarm function.

let message = GossipMessage::new("your_topic".to_string(), "Hello, P2P world!".to_string());
message_sender.send(message).await?;

Graceful Shutdown

For graceful shutdown we again use a channel, this time a oneshot channel. The termination_sender that we created before the create_and_start_swarm function is how this channel can be created.

termination_sender.send(())?;

This will terminate the loop and close the P2P node.

Network Setup and Bootstrapping

Initial Setup and Bootnode

  • The first node in your network acts as a bootnode.
  • This node should be set up with a public IP address and a known, fixed port.
  • The bootnode initially won't be connected to any other nodes.
let bootnode = KarakP2P::create_and_start_swarm(
    "main_topic",
    "/ip4/PUBLIC_IP/tcp/KNOWN_PORT".parse()?,
    vec![], // Empty bootstrap list for the bootnode
    termination_receiver,
    message_receiver,
    60
)?;
 
// Start listening on the bootnode
bootnode.start_listening(|peer_id, message_id, message| {
    // Handle incoming messages
}).await?;

Public IP Requirement

  • For the P2P network to function correctly over the internet, each node should have a public IP address.
  • If running behind a NAT, ensure proper port forwarding is set up for the node's listening port.
let bootnode_addr = "/ip4/BOOTNODE_PUBLIC_IP/tcp/BOOTNODE_PORT".parse()?;
let bootnode_peer_id = "BOOTNODE_PEER_ID".parse()?;
 
let new_node = KarakP2P::create_and_start_swarm(
    "main_topic",
    "/ip4/0.0.0.0/tcp/0".parse()?, // Listen on all interfaces, random port
    vec![P2PNode {
        peer_id: bootnode_peer_id,
        address: bootnode_addr,
    }],
    termination_receiver,
    message_receiver,
    60
)?;
 
// Start listening on the new node
new_node.start_listening(|peer_id, message_id, message| {
    // Handle incoming messages
}).await?;

Connecting Additional Nodes

  • Subsequent nodes should use the bootnode's address as their initial bootstrap peer.
  • Provide the bootnode's public IP, port, and PeerId when setting up additional nodes.

Network Growth

  • As more nodes join the network, they will discover additional peers through the Kademlia DHT.
  • The network becomes more resilient and efficient as the number of connected peers increases.

Firewall Considerations

  • Ensure that the chosen ports are open in your firewall for both incoming and outgoing traffic.
  • Typically, you'll need to allow TCP traffic for the libp2p transport.

Advanced Usage

Custom Message Types

The KarakP2P struct is generic over the message type M, allowing you to use custom message types as long as they implement AsRef<[u8]>:

#[derive(Serialize, Deserialize)]
struct CustomMessage {
    content: String,
    timestamp: u64,
}
 
impl AsRef<[u8]> for CustomMessage {
    fn as_ref(&self) -> &[u8] {
        // Implement serialization here
    }
}
 
let karak_p2p: KarakP2P<CustomMessage> = // ... create instance