Skip to content

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

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).

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.0

Dart SDK ^3.5.0. Imports used below:

import 'package:dart_libp2p/dart_libp2p.dart' hide Option; // Host, MultiAddr, AddrInfo, PeerId, Context, keys, events
import 'package:dart_libp2p/config/config.dart'; // Libp2p, Config, Option
import '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'; // PingService
import 'package:dart_libp2p/p2p/host/basic/basic_host.dart'; // BasicHost extras

The barrel exports a routing Option typedef that clashes with the config Option; hide it (as above) or import config.dart with a prefix.

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
}

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 seed
final again = await generateEd25519KeyPairFromSeed(seed);

Store the file with mode 600. Other key types: generateRsaKeyPair(), generateEcdsaKeyPair() (P-256); Ed25519 is the default and the cheapest.

// 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'),
]),

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.

host.network.connectedness(peer) == Connectedness.connected;
host.network.peers; // connected peer IDs
host.network.connsToPeer(peer); // List<Conn>
conn.remoteMultiaddr.hasProtocol('p2p-circuit'); // relayed?
await host.network.closePeer(peer);
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}');
});
// ... later
await 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(...)).

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, FixedLimiter
import '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));
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.

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.

await host.close(); // stops services, timers, connections, listeners, peerstore

Close everything you started on top first (pubsub, DHT, your own timers), then the host. A host cannot be restarted; build a new one.

  • Libp2p.new_(List<Option>): config + defaults + newNode(); returns an unstarted Host.
  • Config / Libp2p.newConfig(), config.apply(opts), applyDefaults(config) (config/defaults.dart), config.newNode(): the manual path; a Config is consumed by newNode.
  • 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 (the Swarm): 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.
  • host.addrs drops loopback and unspecified addresses. The default AddrsFactory filters 127.0.0.1, ::1, 0.0.0.0 and ::. A host that listens only on 127.0.0.1 has empty addrs, so connect(AddrInfo(id, host.addrs)) fails. In tests use host.network.listenAddresses, or pass Libp2p.addrsFactory((a) => a).
  • Libp2p.new_ does not listen. Call await host.start(); it binds the listen addresses and starts Identify, AutoNAT, hole punching and relay services. Calling network.listen yourself as well listens twice.
  • No DNS dialing. TCP and UDX dial only /ip4 and /ip6. A /dns4/... address is skipped. Resolve the name with InternetAddress.lookup and build an /ip4 address. WebSocket addresses (/ws, /wss, /tls/ws) parse but are not dialable: there is no WebSocket, QUIC or WebTransport transport.
  • Pass the same ConnectionManager to Libp2p.connManager and to every transport. UDXTransport requires one; the examples and tests all share one.
  • Make TCPTransport without resourceManager. Then it uses the host’s manager and a connection is counted once from socket open. Older code passes NullResourceManager(), which counts sockets nowhere (4.6.0).
  • Libp2p.autoNAT is 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 use Libp2p.autoNATv2Server(false) or Libp2p.autoNAT(false). Before 4.3.0 autoNAT(false) was ignored.
  • Key files written before 4.1.3 are lost. Ed25519PrivateKey.marshal() wrote zeros; unmarshal() rejects such a key with a FormatException. 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) before host.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 newStream dials 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.WARNING in production; turn on one logger (for example p2p-holepunch) to debug.
  • Docs drift. Some pages show TcpTransport, UdxTransport, host.services[...], Multiaddr.fromString or newStream without a Context. The real names are TCPTransport, UDXTransport, PingService(host)/BasicHost.pingService, MultiAddr('...') and newStream(peer, pids, Context()).

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.dart and test/mocks/ are not part of the published package; copy patterns, do not import them.
  • 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 Config after newNode(), and never use a host after close().
  • Do not strip the /p2p/<id> check or catch-and-ignore PeerIdMismatchException.
  • 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.

Built by Stephan Februarygithub.com/stephanfebLinkedIn