dart-libp2p-host
Skill
dart-libp2p-host. Use when creating or configuring a dart_libp2p host in Dart (package:dart_libp2p, Libp2p.new_, Config): identity keys, UDX/TCP transports, Noise, Yamux, listen addresses, connect, peerstore, identify, ping, event bus, resource limits, shutdown, tests and go-libp2p interop.SKILL.md (Claude Code, Codex, Cursor) · dart-libp2p-host.md (Boubou) · install:
curl -fsSL https://cryptopeer.net/skills/install.sh | sh -s -- dart-libp2p-host
What it is
Section titled “What it is”dart_libp2p is a Dart port of the libp2p stack: a Host with a peer ID,
transports (UDX over UDP, TCP), Noise security, Yamux multiplexing, Identify,
Ping, a peerstore, an event bus and a resource manager. It interoperates with
go-libp2p over TCP (and over UDX with peers that carry go-udx). This skill
covers building, running and closing a host.
Use another skill for: your own wire protocol on top of the host
(dart-libp2p-custom-protocol), relays, NAT, hole punching and mDNS
(dart-libp2p-nat-relay), GossipSub (dart-libp2p-gossipsub), the DHT
(dart-libp2p-kad-dht), WebRTC or BLE transports (dart-libp2p-webrtc,
dart-libp2p-ble).
Install
Section titled “Install”dependencies: dart_libp2p: ^4.12.0 # Only if you import package:dart_udx yourself (e.g. to share a UDX instance): # dart_udx: ^4.3.0Dart SDK ^3.5.0. Imports used below:
import 'package:dart_libp2p/dart_libp2p.dart' hide Option; // Host, MultiAddr, AddrInfo, PeerId, Context, keys, eventsimport 'package:dart_libp2p/config/config.dart'; // Libp2p, Config, Optionimport 'package:dart_libp2p/p2p/transport/udx_transport.dart';import 'package:dart_libp2p/p2p/transport/tcp_transport.dart';import 'package:dart_libp2p/p2p/transport/connection_manager.dart';import 'package:dart_libp2p/p2p/security/noise/noise_protocol.dart'; // NoiseSecurity (optional: default)import 'package:dart_libp2p/p2p/protocol/ping/ping.dart'; // PingServiceimport 'package:dart_libp2p/p2p/host/basic/basic_host.dart'; // BasicHost extrasThe barrel exports a routing Option typedef that clashes with the config
Option; hide it (as above) or import config.dart with a prefix.
Core workflow
Section titled “Core workflow”Minimal host
Section titled “Minimal host”Future<Host> createHost({required KeyPair identity, int port = 0}) async { final connManager = ConnectionManager(); // share it with the transports final host = await Libp2p.new_([ Libp2p.identity(identity), Libp2p.connManager(connManager), Libp2p.transport(UDXTransport(connManager: connManager)), Libp2p.transport(TCPTransport(connManager: connManager)), // no resourceManager: uses the host's Libp2p.listenAddrs([ MultiAddr('/ip4/0.0.0.0/udp/$port/udx'), MultiAddr('/ip4/0.0.0.0/tcp/$port'), ]), Libp2p.userAgent('my-app/1.0.0'), Libp2p.identifyUserAgent('my-app/1.0.0'), // Noise and Yamux (/yamux/1.0.0) are added by default. ]); await host.start(); // Libp2p.new_ does NOT listen; start() does. return host;}
void main() async { final a = await createHost(identity: await generateEd25519KeyPair()); final b = await createHost(identity: await generateEd25519KeyPair());
// Dial b by its real listen addresses (see Gotchas: host.addrs drops loopback). await a.connect(AddrInfo(b.id, b.network.listenAddresses));
final rtt = await PingService(a).ping(b.id).first; print('rtt ${rtt.rtt} error ${rtt.error}');
await a.close(); await b.close(); // the process exits once every host is closed}Persist the identity
Section titled “Persist the identity”The peer ID comes from the key. A node that others dial by /p2p/<id> (a relay,
a bootstrap node) must keep its key.
// Save: protobuf bytes, the format go-libp2p's crypto.MarshalPrivateKey writes.final bytes = keyPair.privateKey.marshal();// Load:final priv = await Ed25519PrivateKey.unmarshal(bytes);final loaded = KeyPair(priv.publicKey, priv);
// Or keep the 32-byte seed (what the internet_chat demo does):final seed = keyPair.privateKey.raw; // Ed25519 seedfinal again = await generateEd25519KeyPairFromSeed(seed);Store the file with mode 600. Other key types: generateRsaKeyPair(),
generateEcdsaKeyPair() (P-256); Ed25519 is the default and the cheapest.
Addresses to hand out
Section titled “Addresses to hand out”// What other peers should dial (filtered by the AddrsFactory, see Gotchas):for (final a in host.addrs) print('$a/p2p/${host.id.toBase58()}');
// Parse one back:final info = AddrInfo.fromMultiaddr(MultiAddr('/ip4/203.0.113.7/udp/4001/udx/p2p/12D3KooW...'));await host.connect(info);A server whose interface address is private (cloud VM) must announce its public address itself:
Libp2p.addrsFactory((addrs) => [ ...addrs.where((a) => !a.isLoopback() && a.valueForProtocol('ip4') != '0.0.0.0' && a.valueForProtocol('ip6') != '::'), MultiAddr('/ip4/203.0.113.7/udp/4001/udx'), MultiAddr('/ip4/203.0.113.7/tcp/4001'),]),Peerstore and Identify
Section titled “Peerstore and Identify”Identify runs on every new connection, in both directions, and fills the
peerstore. connect() returns after Identify.
final protos = await host.peerStore.protoBook.getProtocols(peer);final addrs = await host.peerStore.addrBook.addrs(peer);final key = await host.peerStore.keyBook.pubKey(peer);final first = await host.peerStore.protoBook.firstSupportedProtocol(peer, ['/app/2.0.0', '/app/1.0.0']);
// Bootstrap address that never expires:await host.peerStore.addrBook.addAddrs(peer, addrs, AddressTTL.permanentAddrTTL);connect() stores the given addresses with a 5-minute TTL. newStream()
dials from the peerstore, so it works only for a connected peer or one whose
addresses are stored.
Connections
Section titled “Connections”host.network.connectedness(peer) == Connectedness.connected;host.network.peers; // connected peer IDshost.network.connsToPeer(peer); // List<Conn>conn.remoteMultiaddr.hasProtocol('p2p-circuit'); // relayed?await host.network.closePeer(peer);Event bus
Section titled “Event bus”final sub = host.eventBus.subscribe([EvtPeerConnectednessChanged, EvtPeerIdentificationCompleted]);sub.stream.listen((dynamic e) { if (e is EvtPeerConnectednessChanged) print('${e.peer} ${e.connectedness}'); if (e is EvtPeerIdentificationCompleted) print('${e.peer} speaks ${e.protocols}');});// ... laterawait sub.close();subscribe is synchronous and returns a Subscription; emitter(Type) is
async and its Emitter must be closed. Other events: EvtLocalReachabilityChanged,
EvtLocalAddressesUpdated, EvtLocalProtocolsUpdated, EvtPeerProtocolsUpdated,
EvtPeerIdentificationFailed, EvtNATDeviceTypeChanged.
EvtPeerConnectednessChanged fires on a peer’s first connection and when its
last one closes. For per-connection callbacks use host.network.notify(NotifyBundle(...)).
Resource limits
Section titled “Resource limits”A host from Libp2p.new_ has go-libp2p’s default limits (1 GiB / 512 FD
budget: 256 connections total, 8 per peer, 68 inbound streams per peer and
protocol). Over a limit you get ResourceLimitExceededException.
import 'package:dart_libp2p/p2p/host/resource_manager/limiter.dart'; // ConfigurableLimiter, LimiterConfig, FixedLimiterimport 'package:dart_libp2p/p2p/host/resource_manager/limit.dart'; // BaseLimit
Libp2p.resourceLimiter(ConfigurableLimiter( LimiterConfig.scaled(memory: 2 * 1024 * 1024 * 1024, fds: 4096))),// or custom: LimiterConfig(peer: BaseLimit(conns: 4), protocolOverrides: {...})// no limits: Libp2p.resourceLimiter(FixedLimiter())// no tracking: Libp2p.resourceManager(NullResourceManager())
await host.network.resourceManager.viewSystem((scope) async => print(scope.stat));Tuning Yamux
Section titled “Tuning Yamux”Libp2p.muxer('/yamux/1.0.0', (conn, isClient) => YamuxSession( conn as TransportConn, const MultiplexerConfig(maxStreams: 256, keepAliveInterval: Duration(seconds: 15)), isClient)),(YamuxSession, MultiplexerConfig: p2p/transport/multiplexing/yamux/session.dart,
p2p/transport/multiplexing/multiplexer.dart.) Defaults: 256 KiB frames,
16 MiB max window, keep-alive 10 s, no pong in 30 s closes the session, stream
open timeout 10 s.
Other options worth knowing
Section titled “Other options worth knowing”Libp2p.dialTimeout (15 s), Libp2p.relayDialTimeout (30 s),
Libp2p.negotiationTimeout (10 s, multistream on newStream),
Libp2p.ping(false), Libp2p.autoNAT(false), Libp2p.autoNATv2Server(false)
(phones), Libp2p.holePunching(bool), Libp2p.connectionGater(BasicConnGater()..blockPeer(id))
(p2p/network/conn_gater.dart), Libp2p.observedAddrActivationThreshold(n).
Relay and NAT options: see dart-libp2p-nat-relay.
Shutdown
Section titled “Shutdown”await host.close(); // stops services, timers, connections, listeners, peerstoreClose everything you started on top first (pubsub, DHT, your own timers), then the host. A host cannot be restarted; build a new one.
Key concepts / API map
Section titled “Key concepts / API map”Libp2p.new_(List<Option>): config + defaults +newNode(); returns an unstartedHost.Config/Libp2p.newConfig(),config.apply(opts),applyDefaults(config)(config/defaults.dart),config.newNode(): the manual path; aConfigis consumed bynewNode.Host:id,addrs,peerStore,network,mux,eventBus,connManager,connect,newStream,setStreamHandler,setStreamHandlerMatch,removeStreamHandler,start,close,holePunchService.BasicHost(the concrete host):pingService,circuitV2Client,autoNATService,publicAddrs,udpNatDeviceType,allAddrs.Network(theSwarm):listenAddresses,peers,conns,connsToPeer,connectedness,closePeer,notify,resourceManager.UDXTransport({required connManager, udxInstance, config}):/ip4|ip6/.../udp/<port>/udx.TCPTransport({resourceManager, connManager, config}):/ip4|ip6/.../tcp/<port>.NoiseSecurity.create(keyPair):/noise(XX). Default when none is given.YamuxSession,MultiplexerConfig:/yamux/1.0.0. Default.AddrInfo(id, addrs),AddrInfo.fromMultiaddr(ma);PeerId.fromString,toBase58().PingService(host).ping(peer):Stream<PingResult>(rtt,error,hasError),/ipfs/ping/1.0.0.- Identify:
/ipfs/id/1.0.0, push/ipfs/id/push/1.0.0, 30 s timeout,IdentifyTimeoutException. Context:withDialPeerTimeout,withForceDirectDial,withForceFreshDial.- Errors (
core/network/errors.dart):ResourceLimitExceededException,PeerIdMismatchException,GaterDisallowedConnectionException.
Gotchas
Section titled “Gotchas”host.addrsdrops loopback and unspecified addresses. The defaultAddrsFactoryfilters127.0.0.1,::1,0.0.0.0and::. A host that listens only on127.0.0.1has emptyaddrs, soconnect(AddrInfo(id, host.addrs))fails. In tests usehost.network.listenAddresses, or passLibp2p.addrsFactory((a) => a).Libp2p.new_does not listen. Callawait host.start(); it binds the listen addresses and starts Identify, AutoNAT, hole punching and relay services. Callingnetwork.listenyourself as well listens twice.- No DNS dialing. TCP and UDX dial only
/ip4and/ip6. A/dns4/...address is skipped. Resolve the name withInternetAddress.lookupand build an/ip4address. WebSocket addresses (/ws,/wss,/tls/ws) parse but are not dialable: there is no WebSocket, QUIC or WebTransport transport. - Pass the same
ConnectionManagertoLibp2p.connManagerand to every transport.UDXTransportrequires one; the examples and tests all share one. - Make
TCPTransportwithoutresourceManager. Then it uses the host’s manager and a connection is counted once from socket open. Older code passesNullResourceManager(), which counts sockets nowhere (4.6.0). Libp2p.autoNATis on by default and starts an AutoNAT v2 server with a second, internal dial-back host (own key). On a phone or in unit tests useLibp2p.autoNATv2Server(false)orLibp2p.autoNAT(false). Before 4.3.0autoNAT(false)was ignored.- Key files written before 4.1.3 are lost.
Ed25519PrivateKey.marshal()wrote zeros;unmarshal()rejects such a key with aFormatException. Generate a new identity. From 4.1.3 marshal/unmarshal match go-libp2p. - RSA/ECDSA peer IDs changed in 3.0.0 and 4.0.0 to match go-libp2p. Re-derive stored non-Ed25519 IDs from the key.
- A dial fails if a different peer answers (
PeerIdMismatchException, 4.11.0). Do not “fix” it by dropping the peer ID. - Handlers registered after
start()reach peers through an Identify push. Register handlers (and start protocols such as the DHT) beforehost.start()so the first Identify already lists them. connect()to a connected peer returns at once; it does not re-dial or re-check the gater. Concurrent dials to one peer join one dial.- Dead connections: a Yamux session closes when keep-alive gets no pong in
30 s; the swarm then removes it and the next
newStreamdials again. Before 4.7.0 a dead UDX path could hang new streams for minutes; stay on >= 4.7.0. - Android / network change: from 4.8.0 a UDX listener whose socket the OS closed is bound again (same port if free). Older versions stopped dialling.
- UDX wire version. dart_libp2p >= 2.0.0 uses dart_udx wire v3, which does not talk to 1.x nodes; a mismatch looks like an unreachable peer. Upgrade both ends together.
- Logging. Routine traffic is logged at FINE. Run at
Level.WARNINGin production; turn on one logger (for examplep2p-holepunch) to debug. - Docs drift. Some pages show
TcpTransport,UdxTransport,host.services[...],Multiaddr.fromStringornewStreamwithout aContext. The real names areTCPTransport,UDXTransport,PingService(host)/BasicHost.pingService,MultiAddr('...')andnewStream(peer, pids, Context()).
Testing
Section titled “Testing”Two real hosts on loopback, as test/protocol/ping_test.dart does:
Future<Host> testHost() async { final kp = await generateEd25519KeyPair(); final cm = ConnectionManager(); final h = await Libp2p.new_([ Libp2p.identity(kp), Libp2p.connManager(cm), Libp2p.transport(UDXTransport(connManager: cm)), Libp2p.listenAddrs([MultiAddr('/ip4/127.0.0.1/udp/0/udx')]), Libp2p.autoNAT(false), // no dial-back host, no probes Libp2p.holePunching(false), ]); await h.start(); return h;}
late Host a, b;setUp(() async { a = await testHost(); b = await testHost(); await a.connect(AddrInfo(b.id, b.network.listenAddresses));});tearDown(() async { await a.close(); await b.close();});test('ping', () async { final r = await PingService(a).ping(b.id).first; expect(r.hasError, isFalse, reason: '${r.error}');}, timeout: const Timeout(Duration(seconds: 30)));- Use TCP (
/ip4/127.0.0.1/tcp/0) instead of UDX when you need to talk to a go-libp2p test peer; vanilla go-libp2p has no UDX. - Always close hosts in
tearDown: an open host keeps timers alive and the test process does not exit. A handler that throws resets only its stream and is logged; it does not fail the test zone (4.0.1). - The repo’s
test/real_net_stack.dartandtest/mocks/are not part of the published package; copy patterns, do not import them.
Red lines
Section titled “Red lines”- Never use
Libp2p.noSecurity()outside a test: connections are then unauthenticated and unencrypted, and no other libp2p peer will accept them. - Never log, commit or send a private key or seed; keep key files mode 600.
- Never reuse a
ConfigafternewNode(), and never use a host afterclose(). - Do not strip the
/p2p/<id>check or catch-and-ignorePeerIdMismatchException. - Do not change the Identify, Ping, Noise or Yamux protocol IDs; go-libp2p peers will not negotiate them.
- Always
close()hosts, subscriptions and emitters you create, or timers and sockets leak and the process never exits.
Reference
Section titled “Reference”- Getting started: https://cryptopeer.net/libp2p/core/getting-started/
- Architecture: https://cryptopeer.net/libp2p/core/architecture/
- Configuration: https://cryptopeer.net/libp2p/core/configuration/
- Host: https://cryptopeer.net/libp2p/core/host/
- Transports: https://cryptopeer.net/libp2p/core/transports/
- Security (Noise, connection gating): https://cryptopeer.net/libp2p/core/security/
- Multiplexing: https://cryptopeer.net/libp2p/core/multiplexing/
- Peerstore: https://cryptopeer.net/libp2p/core/peerstore/
- Event bus: https://cryptopeer.net/libp2p/core/event-bus/
- Resource manager: https://cryptopeer.net/libp2p/core/resource-manager/
- Cookbook: https://cryptopeer.net/libp2p/core/cookbook/
- End-to-end demo: https://cryptopeer.net/demos/internet-chat/
- Source and CHANGELOG: https://github.com/stephanfeb/dart_libp2p