Rust P2P Development: From Ping to Gossipsub

Rust’s ownership model and zero-cost abstractions make it an ideal language for implementing P2P network protocols. The Rust implementation of libp2p (rust-libp2p) provides a complete protocol stack from transport to application layer.

Environment Setup

Configure dependencies in Cargo.toml:

toml
1
2
3
4
5
6
7
8
9
[dependencies]
libp2p = { version = "0.53", features = [
    "tokio", "tcp", "quic", "noise", "yamux", "mplex",
    "ping", "identify", "kad", "gossipsub", "relay", "dcutr", "macros",
] }
tokio = { version = "1.0", features = ["full"] }
tokio-stream = "0.1"
futures = "0.3"
anyhow = "1.0"

Create the project:

bash
1
2
3
4
cargo new p2p-tutorial
cd p2p-tutorial
# Paste the dependencies into Cargo.toml
cargo build  # First build takes minutes to compile dependencies

Building a P2P Node

Minimal Ping Node

Ping is libp2p’s “Hello World” — it demonstrates the basic node lifecycle:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
use libp2p::{
    identity, ping::{Behaviour, Config as PingConfig, Event},
    swarm::{SwarmBuilder, SwarmEvent}, Multiaddr, PeerId,
};
use tokio::io::{AsyncBufReadExt, BufReader};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 1. Generate node identity
    let id_keys = identity::Keypair::generate_ed25519();
    let peer_id = PeerId::from(id_keys.public());
    println!("Local Peer ID: {peer_id}");

    // 2. Build transport + security + multiplexing
    let transport = libp2p::tokio_development_transport(id_keys)?;

    // 3. Define network behaviour
    let behaviour = Behaviour::new(PingConfig::default());

    // 4. Create Swarm (network event loop)
    let mut swarm = SwarmBuilder::with_tokio_executor(
        transport, behaviour, peer_id,
    ).build();

    // 5. Start listening
    swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;

    // 6. Interactive: enter peer address to connect
    let mut stdin = BufReader::new(tokio::io::stdin()).lines();
    println!("Enter peer multiaddr to dial (or 'quit' to exit):");

    loop {
        tokio::select! {
            event = swarm.select_next_some() => match event {
                SwarmEvent::NewListenAddr { address, .. } => {
                    println!("Listening on {address}");
                }
                SwarmEvent::Behaviour(Event::Ping { peer, result }) => {
                    println!("Ping to {peer}: {result:?}");
                }
                _ => {}
            },
            line = stdin.next_line() => {
                let line = line?.expect("stdin closed");
                if line == "quit" { break; }
                if let Ok(addr) = line.parse::<Multiaddr>() {
                    swarm.dial(addr)?;
                    println!("Dialing {addr}");
                }
            }
        }
    }

    Ok(())
}

Running and testing:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Terminal 1: Start first node
cargo run
# Output: Local Peer ID: 12D3Koo...
# Output: Listening on /ip4/127.0.0.1/tcp/12345

# Terminal 2: Start second node
cargo run
# Output: Local Peer ID: 12D3Koo...
# Output: Listening on /ip4/127.0.0.1/tcp/54321

# In Terminal 2, enter Terminal 1's listening address:
# /ip4/127.0.0.1/tcp/12345/p2p/12D3Koo... (replace with actual Peer ID)
# Both nodes start pinging each other

This example demonstrates four core components: identity generation, transport construction, behaviour definition, and event loop. tokio::select! simultaneously listens for network events and user input, enabling interactive node management.

Protocol Composition

Real P2P applications typically need multiple protocols running simultaneously. libp2p’s NetworkBehaviour derive macro composes multiple protocols into one:

rust
1
2
3
4
5
6
7
8
#[derive(NetworkBehaviour)]
#[behaviour(out_event = "MyBehaviourEvent")]
struct MyBehaviour {
    ping: ping::Behaviour,
    identify: identify::Behaviour,
    kademlia: kad::Behaviour<kad::store::MemoryStore>,
    gossipsub: gossipsub::Behaviour,
}

Events from each sub-protocol are converted into a unified event type via the From trait, centralizing event handling in a single match block.

Complete Kademlia DHT Implementation

Here’s a complete DHT node with Put/Get operations:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
use libp2p::{identity, kad, swarm::{SwarmBuilder, SwarmEvent}, PeerId};
use std::time::Duration;

async fn run_dht_node() -> anyhow::Result<()> {
    let id_keys = identity::Keypair::generate_ed25519();
    let peer_id = PeerId::from(id_keys.public());
    let transport = libp2p::tokio_development_transport(id_keys.clone())?;

    let store = kad::store::MemoryStore::new(peer_id);
    let mut kademlia = kad::Behaviour::with_config(
        peer_id, store,
        kad::Config::default()
            .set_query_timeout(Duration::from_secs(30))
            .set_replication_interval(Some(Duration::from_secs(3600))),
    );

    // Add IPFS bootstrap nodes
    let bootstrap: Vec<(PeerId, libp2p::Multiaddr)> = vec![
        ("QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN".parse()?,
         "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN".parse()?),
    ];
    for (peer, addr) in bootstrap {
        kademlia.add_address(&peer, addr);
    }
    kademlia.bootstrap()?;

    let mut swarm = SwarmBuilder::with_tokio_executor(
        transport, kademlia, peer_id,
    ).build();
    swarm.listen_on("/ip4/0.0.0.0/tcp/4001".parse()?)?;

    // Wait for bootstrap
    tokio::time::sleep(Duration::from_secs(5)).await;

    // ===== Put: store data =====
    let key = "my-app/greeting".as_bytes().to_vec();
    let value = b"Hello from Rust DHT!".to_vec();
    swarm.behaviour_mut().put_record(
        kad::Record::new(key.clone(), value),
        kad::Quorum::Majority,
    )?;
    println!("Put: stored key 'my-app/greeting'");

    tokio::time::sleep(Duration::from_secs(2)).await;

    // ===== Get: retrieve data =====
    swarm.behaviour_mut().get_record(&key.into());
    println!("Get: requesting key 'my-app/greeting'");

    loop {
        match swarm.select_next_some().await {
            SwarmEvent::Behaviour(kad::Event::OutboundQueryProgressed {
                result: kad::QueryResult::GetRecord(Ok(ok)),
                ..
            }) => {
                for record in ok.records {
                    println!("Got value: {}",
                        String::from_utf8_lossy(&record.record.value));
                }
                break;
            }
            SwarmEvent::Behaviour(kad::Event::OutboundQueryProgressed {
                result: kad::QueryResult::PutRecord(_),
                ..
            }) => {
                println!("Put succeeded");
            }
            _ => {}
        }
    }
    Ok(())
}

Key API notes:

  • put_record(): Stores key-value pair on the K closest nodes to the key
  • get_record(): Retrieves the value for a key from DHT
  • Quorum::Majority: Requires majority of nodes to confirm write
  • OutboundQueryProgressed: DHT query progress event with success/failure results

Gossipsub Publish/Subscribe

Gossipsub is ideal for broadcast messaging. Here’s a complete publish and subscribe example:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
use libp2p::gossipsub::{self, MessageAuthenticity, MessageId, Sha256Topic};
use sha2::{Digest, Sha256};

fn create_gossipsub(id_keys: &identity::Keypair) -> anyhow::Result<gossipsub::Behaviour> {
    let message_id_fn = |message: &gossipsub::Message| {
        let mut hasher = Sha256::new();
        hasher.update(&message.data);
        MessageId::from(hasher.finalize().as_slice())
    };

    let config = gossipsub::ConfigBuilder::default()
        .heartbeat_interval(Duration::from_secs(1))
        .validation_mode(gossipsub::ValidationMode::Strict)
        .message_id_fn(message_id_fn)
        .build()?;

    Ok(gossipsub::Behaviour::new(
        MessageAuthenticity::Signed(id_keys.clone()),
        config,
    )?)
}

// Complete publish + subscribe usage
async fn pubsub_example(
    swarm: &mut Swarm<MyBehaviour>,
) -> anyhow::Result<()> {
    let topic = Sha256Topic::new("p2p-tutorial/chat");

    // Subscribe to topic
    swarm.behaviour_mut().gossipsub.subscribe(&topic)?;
    println!("Subscribed to topic: p2p-tutorial/chat");

    // Publish message
    swarm.behaviour_mut().gossipsub.publish(
        topic.clone(),
        b"Hello P2P World!".to_vec(),
    )?;
    println!("Published: Hello P2P World!");

    // Handle received messages in main event loop:
    // match event {
    //     SwarmEvent::Behaviour(MyBehaviourEvent::Gossipsub(
    //         gossipsub::Event::Message {
    //             propagation_source,
    //             message_id,
    //             message,
    //         }
    //     )) => {
    //         println!("Received: {} from {}",
    //             String::from_utf8_lossy(&message.data),
    //             propagation_source);
    //     }
    // }
    Ok(())
}

NAT Traversal (DCUtR)

When a node is behind NAT, relay and hole punching are needed:

rust
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#[derive(NetworkBehaviour)]
struct HolePunchBehaviour {
    relay_client: relay::client::Behaviour,
    dcutr: dcutr::Behaviour,
}

async fn setup_hole_punching(
    swarm: &mut Swarm<HolePunchBehaviour>,
    relay_peer_id: PeerId,
    relay_addr: Multiaddr,
) -> anyhow::Result<()> {
    let relay_addr = relay_addr
        .with(libp2p::multiaddr::Protocol::P2p(relay_peer_id))
        .with(libp2p::multiaddr::Protocol::P2pCircuit);
    swarm.listen_on(relay_addr)?;
    // Other nodes can now connect through the relay
    // DCUtR automatically attempts direct hole punching,
    // upgrading to direct connection on success
    Ok(())
}

Node Lifecycle

mermaid
flowchart TD
    A["Generate Keypair<br/>identity::Keypair::generate_ed25519()"] --> B["Create Transport<br/>Transport + Security + Mux"]
    B --> C["Define Behaviour<br/>Compose protocols"]
    C --> D["Build Swarm<br/>Event loop core"]
    D --> E["Start Listening<br/>listen_on()"]
    E --> F{"Event Loop<br/>tokio::select!"}
    F -->|"New Connection"| G["Process SwarmEvent<br/>Protocol dispatch"]
    F -->|"User Input"| H["dial / publish<br/>Initiate connection"]
    G --> F
    H --> F

Common Compilation Issues

IssueSolution
feature not foundCheck Cargo.toml features list, ensure required protocols are enabled
unresolved importConfirm libp2p version ≥ 0.53, APIs change between versions
trait bound not satisfiedEnsure NetworkBehaviour’s out_event and all From impls are complete

References