Skip to content
minip2p
Esc
↑↓navigate↵open⌘Jpreview

A minimal libp2p implementation

Peer-to-peer.
Keep it simple.

Connect applications over authenticated QUIC and TCP.

How it works

Connection attempt

Many routes.
One outcome.

Call connect once. Every route races inside one attempt, and your loop hears how it ends exactly once.

Traverse NAT

relayrelay
NATNAT
✕
youpeerpeer ✓
attempt
pendingsettled
path
none yetRelayed · provisionalPunched
your loopone event per node.wait()
  1. connect(peer) → ConnectId
  2. wait → ConnectionEstablishedcircuit
  3. wait → Nat(PathEstablished)Relayed
  4. wait → ConnectionEstablisheddirect
  5. wait → Nat(PathUpgraded)→ Punched
  6. wait → ConnectSettledConnected
  7. 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 => {} }}
wait(deadline)
Nat(PathUpgraded)

Endpoint core

sans-I/O · no sockets · no clock

identifypingnatrelaygossipsubdiscovery
sleep on socket + timer
bytes + now

Transport adapter

std · owns the I/O

UDP socketclock
datagram from the peer

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.

Drive events

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();
}