Rust P2P
Use this skill for Pluto Rust libp2p work. Bias toward the existing networking
architecture; do not invent a parallel swarm, context, or handler model.
Start Here
Classify the task and read the matching implementation first.
| Area |
Files |
Look for |
| Node runtime |
crates/p2p/src/p2p.rs |
Node, builder closure, listen/dial setup |
| Shared peer state |
crates/p2p/src/p2p_context.rs |
local peer binding, known peers, peer store |
| Common wrapper |
crates/p2p/src/behaviours/pluto.rs |
common behaviours plus inner |
| Connection tracking |
crates/p2p/src/conn_logger.rs |
peer-store updates on connection lifecycle |
| Connection gating |
crates/p2p/src/gater.rs |
allow/deny policy |
| Bootnode |
crates/p2p/src/bootnode.rs |
bootnode run loop and shutdown |
| Relay |
crates/p2p/src/relay.rs |
reservations and relay-route dialing |
| Direct routing |
crates/p2p/src/force_direct.rs |
direct-connection enforcement |
| QUIC upgrade |
crates/p2p/src/quic_upgrade.rs |
TCP-to-QUIC retry/close flow |
| Optional wrapper |
crates/p2p/src/behaviours/optional.rs |
enabled/disabled behaviour routing |
| Framing |
crates/p2p/src/proto.rs |
protobuf read/write helpers and limits |
| Peer info |
crates/peerinfo/src/ |
simple behaviour/handler protocol |
| Parsigex |
crates/parsigex/src/ |
handle -> behaviour -> handler broadcast protocol |
| DKG broadcast |
crates/dkg/src/bcast/ |
command-driven behaviour/handler protocol |
| DKG sync |
crates/dkg/src/sync/ |
long-lived stream, waiters, cancellation |
| Examples |
crates/*/examples/ |
expected public construction patterns |
Before editing, draw the ownership path for the specific protocol:
application / protocol flow
|
| handle, command channel, waiter, or event consumer
v
feature Behaviour shared P2PContext
| ^
| ToSwarm::{Dial, ListenOn, | conn_logger + identify update it
| NotifyHandler, GenerateEvent}
v
PlutoBehaviour wrapper
|
v
libp2p Swarm
|
| connection events + negotiated substreams
v
feature ConnectionHandler
|
| async stream read/write futures
v
protocol state / protocol event
Core Ownership
Node owns the Swarm.
PlutoBehaviour<B> wraps common networking behaviours around feature
behaviour B.
- One node must have one canonical
P2PContext shared by Node,
PlutoBehaviour, and inner behaviours that read peer state.
- Feature
Behaviour owns swarm-level protocol orchestration.
- Feature
ConnectionHandler owns one connection's negotiated streams.
- User-facing handles do not own the swarm; they use channels, waiters, shared
protocol state, or emitted events.
Node And Context
Use Node::new for normal client nodes. It builds TCP/QUIC transports and
includes relay-client support. Use Node::new_server for server-style nodes
that should not include relay-client behaviour.
Rules:
- Pass the intended
P2PContext into Node construction.
- Inside the builder closure, use
builder.p2p_context() for inner behaviours
that need peer state.
- Do not create a fresh/default
P2PContext inside a node builder closure.
- Treat
P2PContext::default() as only suitable at standalone node construction
when no known-peer state is needed and no component must share peer state.
- Do not reintroduce replaceable-context APIs after the builder is created.
- Let
Node bind the local peer ID and preserve LocalPeerIdMismatch fail-fast
behaviour.
filter_private_addrs affects advertised addresses, not listen addresses.
P2PContext is for runtime peer connectivity:
- known peer set,
- local peer ID once bound,
- active/inactive connection records,
- peer addresses learned from identify.
Do not store protocol progress in P2PContext.
Critical failure mode: if conn_logger writes context A while an inner
behaviour reads context B, the inner behaviour will treat connected peers as
disconnected.
PlutoBehaviour
PlutoBehaviour<B> composes common behaviour with feature behaviour:
conn_logger: lifecycle logging, peer-store updates, metrics,
gater: connection allow/deny policy,
identify: address/protocol exchange; Node stores listen addresses,
ping: latency/keepalive metrics,
autonat: reachability,
quic_upgrade: optional TCP -> QUIC upgrade attempts,
inner: feature-specific behaviour.
Composition invariant: #[derive(NetworkBehaviour)] delegates behaviour
methods in struct field order. Keep conn_logger before behaviours that may
read P2PContext.peer_store, so connection events update shared peer state
before later fields handle the same swarm event or get polled afterward.
Behaviour Pattern
A feature Behaviour is a swarm-level coordinator.
Common responsibilities:
- receive user commands through
tokio::sync::mpsc,
- queue
ToSwarm events in VecDeque,
- create a real handler for peers in protocol scope,
- return
dummy::ConnectionHandler when the behaviour has no per-connection
protocol work or when a peer is out of scope,
- inspect
FromSwarm connection/dial events,
- poll retry/routing timers,
- translate handler events into feature events.
poll rules:
- never
.await,
- never block,
- never spin on an immediately failing condition,
- drain ready commands before sleeping,
- emit at most one queued
ToSwarm event per poll,
- return
Poll::Pending when no work is ready.
Use ToSwarm::NotifyHandler when an existing handler should open a substream.
Use ToSwarm::GenerateEvent for application-visible events.
Handler Pattern
A ConnectionHandler is per connection, not per peer.
Common responsibilities:
- define inbound and outbound protocol upgrades,
- handle
FullyNegotiatedInbound by starting inbound stream work,
- handle
FullyNegotiatedOutbound by starting matching outbound stream work,
- report results with
ConnectionHandlerEvent::NotifyBehaviour,
- request outbound streams with
ConnectionHandlerEvent::OutboundSubstreamRequest,
- translate negotiation, timeout, and I/O failures into typed protocol failures.
Do not block in handler poll. Store async stream work as boxed futures or a
FuturesUnordered, then poll them.
Do not assume only one handler exists for a peer. If a protocol requires one
outbound loop per peer, keep an explicit claim in shared protocol state and
release it on every terminal path.
Handles, Waiters, Cancellation
Keep user-facing APIs small and keep swarm internals inside Node.
Existing shapes:
- command handle ->
mpsc -> behaviour (parsigex::Handle,
bcast::Component, sync::Client),
- behaviour event -> caller observes completion/failure (
parsigex, bcast),
- shared protocol state -> wait API (
sync::Server).
Waiter rules:
- Use
Notify to wake waiters after state changes.
- Check terminal error and cancellation before awaiting another notification.
- Use
CancellationToken for long-running waits and protocol tasks that must
stop during shutdown.
- Tests around waiters should still use
tokio::time::timeout; cancellation is
part of the contract, not a replacement for test bounds.
Stream And Framing
Choose the protocol primitive intentionally:
- one stream protocol ->
ReadyUpgrade<StreamProtocol>,
- multiple stream protocols on one handler ->
SelectUpgrade,
- request/response over a negotiated stream -> helpers in
pluto_p2p::proto.
Framing variants:
- varint length framing:
write_protobuf /
read_protobuf_with_max_size,
- fixed
i64 little-endian length framing: write_fixed_size_protobuf /
read_fixed_size_protobuf_with_max_size.
Safety rules:
- Use explicit protocol-specific max sizes where practical.
- Do not allocate from an untrusted frame length before checking the max.
- Add per-message
tokio::time::timeout when a slow peer can otherwise hold a
stream task indefinitely.
- Preserve
io::ErrorKind until retry/terminal decisions are made.
Dialing And Retry
Prefer swarm-owned dialing through ToSwarm::Dial.
Use PeerCondition::DisconnectedAndNotDialing when activation should not open
duplicate dials.
Typical dial-failure handling:
Transport(_): retry only if the protocol still wants reconnect.
NoAddresses: retry only after a delay; identify, relay routing, or
bootstrap may populate addresses later.
DialBackoff: usually let libp2p backoff stand unless the protocol has a
specific recovery path.
NegotiationFailed: usually terminal for that stream protocol; the peer does
not support it.
Reconnect loops need a wakeup path. If a handler releases an outbound claim,
ensure another handler can eventually retry by timer, notification, or a new
swarm event.
Relay And Routing
Relay reservation and relay routing are separate:
MutableRelayReservation dials relay servers directly, waits for relay
connection establishment, then listens on /p2p-circuit addresses.
RelayRouter periodically builds relay circuit addresses for known peers and
queues dials through those relays.
Address rules:
- Dialing a relay server directly: append
/p2p/<relay-id>, not
/p2p-circuit.
- Listening through a relay or dialing a target through a relay: include
/p2p-circuit.
- Do not assume relay peer data is available at startup; mutable relay peers can
resolve later.
Testing
Use the smallest test that exercises the real boundary.
Good boundaries:
- behaviour queueing/retry: unit-test
NetworkBehaviour::poll with
noop_waker_ref,
- handler state machines: poll handlers/futures directly where possible,
- real connectivity: use
Node and real swarms,
- multi-swarm tests: use
#[tokio::test(flavor = "multi_thread")],
- async barriers and shutdown: wrap waits in
tokio::time::timeout,
- ports: prefer
/ip4/127.0.0.1/tcp/0 plus SwarmEvent::NewListenAddr.
Regression tests should cover the contract boundary:
- success path,
- error propagation to waiters or events,
- cancellation/teardown,
- retry or dial-failure behaviour,
- protocol validation failure for authenticated peers.
1---2name: rust-p2p3description: Use when implementing, reviewing, debugging, or explaining Pluto Rust libp2p code: Node/P2PContext ownership, PlutoBehaviour composition, NetworkBehaviour and ConnectionHandler protocols, relay/force-direct/quic-upgrade behaviours, peerinfo/parsigex protocol handlers, DKG protocol handlers, and P2P tests.4---56# Rust P2P78Use this skill for Pluto Rust libp2p work. Bias toward the existing networking9architecture; do not invent a parallel swarm, context, or handler model.1011## Start Here1213Classify the task and read the matching implementation first.1415| Area | Files | Look for |16| --- | --- | --- |17| Node runtime | `crates/p2p/src/p2p.rs` | `Node`, builder closure, listen/dial setup |18| Shared peer state | `crates/p2p/src/p2p_context.rs` | local peer binding, known peers, peer store |19| Common wrapper | `crates/p2p/src/behaviours/pluto.rs` | common behaviours plus `inner` |20| Connection tracking | `crates/p2p/src/conn_logger.rs` | peer-store updates on connection lifecycle |21| Connection gating | `crates/p2p/src/gater.rs` | allow/deny policy |22| Bootnode | `crates/p2p/src/bootnode.rs` | bootnode run loop and shutdown |23| Relay | `crates/p2p/src/relay.rs` | reservations and relay-route dialing |24| Direct routing | `crates/p2p/src/force_direct.rs` | direct-connection enforcement |25| QUIC upgrade | `crates/p2p/src/quic_upgrade.rs` | TCP-to-QUIC retry/close flow |26| Optional wrapper | `crates/p2p/src/behaviours/optional.rs` | enabled/disabled behaviour routing |27| Framing | `crates/p2p/src/proto.rs` | protobuf read/write helpers and limits |28| Peer info | `crates/peerinfo/src/` | simple behaviour/handler protocol |29| Parsigex | `crates/parsigex/src/` | handle -> behaviour -> handler broadcast protocol |30| DKG broadcast | `crates/dkg/src/bcast/` | command-driven behaviour/handler protocol |31| DKG sync | `crates/dkg/src/sync/` | long-lived stream, waiters, cancellation |32| Examples | `crates/*/examples/` | expected public construction patterns |3334Before editing, draw the ownership path for the specific protocol:3536```text37application / protocol flow38 |39 | handle, command channel, waiter, or event consumer40 v41feature Behaviour shared P2PContext42 | ^43 | ToSwarm::{Dial, ListenOn, | conn_logger + identify update it44 | NotifyHandler, GenerateEvent}45 v46PlutoBehaviour wrapper47 |48 v49libp2p Swarm50 |51 | connection events + negotiated substreams52 v53feature ConnectionHandler54 |55 | async stream read/write futures56 v57protocol state / protocol event58```5960## Core Ownership6162- `Node` owns the `Swarm`.63- `PlutoBehaviour<B>` wraps common networking behaviours around feature64 behaviour `B`.65- One node must have one canonical `P2PContext` shared by `Node`,66 `PlutoBehaviour`, and inner behaviours that read peer state.67- Feature `Behaviour` owns swarm-level protocol orchestration.68- Feature `ConnectionHandler` owns one connection's negotiated streams.69- User-facing handles do not own the swarm; they use channels, waiters, shared70 protocol state, or emitted events.7172## Node And Context7374Use `Node::new` for normal client nodes. It builds TCP/QUIC transports and75includes relay-client support. Use `Node::new_server` for server-style nodes76that should not include relay-client behaviour.7778Rules:7980- Pass the intended `P2PContext` into `Node` construction.81- Inside the builder closure, use `builder.p2p_context()` for inner behaviours82 that need peer state.83- Do not create a fresh/default `P2PContext` inside a node builder closure.84- Treat `P2PContext::default()` as only suitable at standalone node construction85 when no known-peer state is needed and no component must share peer state.86- Do not reintroduce replaceable-context APIs after the builder is created.87- Let `Node` bind the local peer ID and preserve `LocalPeerIdMismatch` fail-fast88 behaviour.89- `filter_private_addrs` affects advertised addresses, not listen addresses.9091`P2PContext` is for runtime peer connectivity:9293- known peer set,94- local peer ID once bound,95- active/inactive connection records,96- peer addresses learned from identify.9798Do not store protocol progress in `P2PContext`.99100**Critical failure mode:** if `conn_logger` writes context A while an inner101behaviour reads context B, the inner behaviour will treat connected peers as102disconnected.103104## PlutoBehaviour105106`PlutoBehaviour<B>` composes common behaviour with feature behaviour:107108- `conn_logger`: lifecycle logging, peer-store updates, metrics,109- `gater`: connection allow/deny policy,110- `identify`: address/protocol exchange; `Node` stores listen addresses,111- `ping`: latency/keepalive metrics,112- `autonat`: reachability,113- `quic_upgrade`: optional TCP -> QUIC upgrade attempts,114- `inner`: feature-specific behaviour.115116**Composition invariant:** `#[derive(NetworkBehaviour)]` delegates behaviour117methods in struct field order. Keep `conn_logger` before behaviours that may118read `P2PContext.peer_store`, so connection events update shared peer state119before later fields handle the same swarm event or get polled afterward.120121## Behaviour Pattern122123A feature `Behaviour` is a swarm-level coordinator.124125Common responsibilities:126127- receive user commands through `tokio::sync::mpsc`,128- queue `ToSwarm` events in `VecDeque`,129- create a real handler for peers in protocol scope,130- return `dummy::ConnectionHandler` when the behaviour has no per-connection131 protocol work or when a peer is out of scope,132- inspect `FromSwarm` connection/dial events,133- poll retry/routing timers,134- translate handler events into feature events.135136`poll` rules:137138- never `.await`,139- never block,140- never spin on an immediately failing condition,141- drain ready commands before sleeping,142- emit at most one queued `ToSwarm` event per poll,143- return `Poll::Pending` when no work is ready.144145Use `ToSwarm::NotifyHandler` when an existing handler should open a substream.146Use `ToSwarm::GenerateEvent` for application-visible events.147148## Handler Pattern149150A `ConnectionHandler` is per connection, not per peer.151152Common responsibilities:153154- define inbound and outbound protocol upgrades,155- handle `FullyNegotiatedInbound` by starting inbound stream work,156- handle `FullyNegotiatedOutbound` by starting matching outbound stream work,157- report results with `ConnectionHandlerEvent::NotifyBehaviour`,158- request outbound streams with159 `ConnectionHandlerEvent::OutboundSubstreamRequest`,160- translate negotiation, timeout, and I/O failures into typed protocol failures.161162Do not block in handler `poll`. Store async stream work as boxed futures or a163`FuturesUnordered`, then poll them.164165Do not assume only one handler exists for a peer. If a protocol requires one166outbound loop per peer, keep an explicit claim in shared protocol state and167release it on every terminal path.168169## Handles, Waiters, Cancellation170171Keep user-facing APIs small and keep swarm internals inside `Node`.172173Existing shapes:174175- command handle -> `mpsc` -> behaviour (`parsigex::Handle`,176 `bcast::Component`, `sync::Client`),177- behaviour event -> caller observes completion/failure (`parsigex`, `bcast`),178- shared protocol state -> wait API (`sync::Server`).179180Waiter rules:181182- Use `Notify` to wake waiters after state changes.183- Check terminal error and cancellation before awaiting another notification.184- Use `CancellationToken` for long-running waits and protocol tasks that must185 stop during shutdown.186- Tests around waiters should still use `tokio::time::timeout`; cancellation is187 part of the contract, not a replacement for test bounds.188189## Stream And Framing190191Choose the protocol primitive intentionally:192193- one stream protocol -> `ReadyUpgrade<StreamProtocol>`,194- multiple stream protocols on one handler -> `SelectUpgrade`,195- request/response over a negotiated stream -> helpers in `pluto_p2p::proto`.196197Framing variants:198199- varint length framing: `write_protobuf` /200 `read_protobuf_with_max_size`,201- fixed `i64` little-endian length framing: `write_fixed_size_protobuf` /202 `read_fixed_size_protobuf_with_max_size`.203204Safety rules:205206- Use explicit protocol-specific max sizes where practical.207- Do not allocate from an untrusted frame length before checking the max.208- Add per-message `tokio::time::timeout` when a slow peer can otherwise hold a209 stream task indefinitely.210- Preserve `io::ErrorKind` until retry/terminal decisions are made.211212## Dialing And Retry213214Prefer swarm-owned dialing through `ToSwarm::Dial`.215216Use `PeerCondition::DisconnectedAndNotDialing` when activation should not open217duplicate dials.218219Typical dial-failure handling:220221- `Transport(_)`: retry only if the protocol still wants reconnect.222- `NoAddresses`: retry only after a delay; identify, relay routing, or223 bootstrap may populate addresses later.224- `DialBackoff`: usually let libp2p backoff stand unless the protocol has a225 specific recovery path.226- `NegotiationFailed`: usually terminal for that stream protocol; the peer does227 not support it.228229Reconnect loops need a wakeup path. If a handler releases an outbound claim,230ensure another handler can eventually retry by timer, notification, or a new231swarm event.232233## Relay And Routing234235Relay reservation and relay routing are separate:236237- `MutableRelayReservation` dials relay servers directly, waits for relay238 connection establishment, then listens on `/p2p-circuit` addresses.239- `RelayRouter` periodically builds relay circuit addresses for known peers and240 queues dials through those relays.241242Address rules:243244- Dialing a relay server directly: append `/p2p/<relay-id>`, not245 `/p2p-circuit`.246- Listening through a relay or dialing a target through a relay: include247 `/p2p-circuit`.248- Do not assume relay peer data is available at startup; mutable relay peers can249 resolve later.250251## Testing252253Use the smallest test that exercises the real boundary.254255Good boundaries:256257- behaviour queueing/retry: unit-test `NetworkBehaviour::poll` with258 `noop_waker_ref`,259- handler state machines: poll handlers/futures directly where possible,260- real connectivity: use `Node` and real swarms,261- multi-swarm tests: use `#[tokio::test(flavor = "multi_thread")]`,262- async barriers and shutdown: wrap waits in `tokio::time::timeout`,263- ports: prefer `/ip4/127.0.0.1/tcp/0` plus `SwarmEvent::NewListenAddr`.264265Regression tests should cover the contract boundary:266267- success path,268- error propagation to waiters or events,269- cancellation/teardown,270- retry or dial-failure behaviour,271- protocol validation failure for authenticated peers.