A minimal libp2p implementation
Peer-to-peer.
Keep it simple.
Connect applications over authenticated QUIC and TCP.
Connection attempt
Many routes.
One outcome.
Call connect once. Every route races inside one attempt, and your loop hears how it ends exactly once.
- attempt
- pendingsettled
- path
- none yetRelayed · provisionalPunched
node.wait()- connect(peer) → ConnectId
- wait → ConnectionEstablishedcircuit
- wait → Nat(PathEstablished)Relayed
- wait → ConnectionEstablisheddirect
- wait → Nat(PathUpgraded)→ Punched
- wait → ConnectSettledConnected
- wait → ConnectionClosedcircuit
The order for this run. An attempt that settles on the relay can still see Nat(PathUpgraded) after ConnectSettled.
your code
use minip2p::EndpointWaitOutcome::*; loop { match node.wait(deadline)? { Event(event) => handle(event), Deadline | Interrupted => {} }}Endpoint core
sans-I/O · no sockets · no clock
Transport adapter
std · owns the I/O
Sans-I/O
You drive
the loop.
Nothing runs behind your back. Sockets, clocks and timers live in adapters; the protocols are deterministic state machines that only move when you call wait.
Same core everywhere
From a server to a microcontroller.
Transports
QUIC and TCP
QUIC over quinn-proto with libp2p TLS certificates, and TCP upgraded with Noise XX and Yamux. One transport set routes each address to the transport that serves its shape.
Embedded
no_std + alloc
The core crates build for thumbv7em with no operating system. TCP runs over smoltcp, and mDNS discovery comes with it.
Bindings
Node and React Native
One binding-agnostic FFI core, wrapped by napi-rs for Node and UniFFI for React Native, under a shared TypeScript SDK.
Quickstart
Connect, then drive.
Add minip2p-rs or @minip2p/node, then dial a peer, wait until it is ready, and ping it. Both snippets are shown verbatim from the repository, and CI compiles both: the Rust dialer and the TypeScript example you can run with Node.
use std::time::{Duration, Instant};
use minip2p::{ConnectOutcome, Endpoint, EndpointEvent, EndpointWaitOutcome, PeerAddr};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// A complete peer address, e.g. /ip4/127.0.0.1/udp/4001/quic-v1/p2p/12D3KooW...
let target: PeerAddr = std::env::args()
.nth(1)
.ok_or("usage: dialer <peer-address>")?
.parse()?;
let peer = target.peer_id().clone();
let mut node = Endpoint::builder()
.agent_version("minip2p-hello/dialer")
.listen_default()?
.bind()?;
// One Connection attempt, one Connect ID, one terminal event.
let connect_id = node.connect(target)?;
let mut deadline = Instant::now() + Duration::from_secs(10);
loop {
let event = match node.wait(deadline)? {
EndpointWaitOutcome::Event(event) => event,
// Another thread woke the wait; nothing to service here.
EndpointWaitOutcome::Deadline => {
return Err("peer did not answer in time".into());
}
_ => continue, // interrupted by a WaitHandle
};
match event {
EndpointEvent::ConnectSettled {
connect_id: id,
outcome,
..
} if id == connect_id => {
if !matches!(outcome, ConnectOutcome::Connected { .. }) {
return Err(format!("connect failed: {outcome:?}").into());
}
}
EndpointEvent::PeerReady { peer_id, .. } if peer_id == peer => {
node.ping(&peer)?;
// The ping gets its own budget, however long setup took.
deadline = Instant::now() + Duration::from_secs(5);
}
EndpointEvent::PingRttMeasured { peer_id, rtt_ms, .. } if peer_id == peer => {
println!("peer={peer} rtt={rtt_ms}ms");
return Ok(());
}
// Everything else is unrelated to this exchange.
_ => {}
}
}
}import { generateSecretKey, Minip2p } from "@minip2p/node";
const address = process.argv.at(2);
if (!address || process.argv.length !== 3) {
throw new Error(
"Usage: pnpm run ping <multiaddress printed by the listener>"
);
}
const endpoint = Minip2p.create({
agentVersion: "minip2p-nodejs-example/0.1.0",
// Bind both transports so either printed listener address can be dialed.
listen: ["/ip4/127.0.0.1/udp/0/quic-v1", "/ip4/127.0.0.1/tcp/0"],
secretKey: generateSecretKey(),
});
try {
const connected = await endpoint.connect(address, { timeoutMs: 10_000 });
await endpoint.waitPeerReady(connected.peerId, { timeoutMs: 10_000 });
const rttMs = await endpoint.ping(connected.peerId, { timeoutMs: 5000 });
console.log(`Connected to ${connected.peerId} over ${connected.path.kind}`);
console.log(`Ping: ${rttMs} ms`);
} finally {
endpoint.close();
}