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:
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:
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:
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:
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:
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:
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 keyget_record(): Retrieves the value for a key from DHTQuorum::Majority: Requires majority of nodes to confirm writeOutboundQueryProgressed: 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:
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:
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
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
| Issue | Solution |
|---|
feature not found | Check Cargo.toml features list, ensure required protocols are enabled |
unresolved import | Confirm libp2p version ≥ 0.53, APIs change between versions |
trait bound not satisfied | Ensure NetworkBehaviour’s out_event and all From impls are complete |
References