Implements libp2p Kademlia DHT wire codecs, routing table, iterative lookups, and record storage (#93). Host lifecycle wiring (#203) connects AutoNAT mode promotion, provider republish, and routing-table eviction on disconnect. Spec: libp2p/kad-dht.
| ID | Use |
|---|---|
/ipfs/kad/1.0.0 |
Default public DHT |
/lan/kad/1.0.0 |
LAN-scoped DHT |
Import via zig_libp2p.kad_dht:
| Module | Purpose |
|---|---|
keyspace |
sha256 keys, XOR distance, common-prefix length |
routing_table |
CPL-indexed k-buckets (k=20), LRU eviction per bucket |
wire |
Length-prefixed protobuf Message / Record / Peer codec |
record_store |
Value + provider records with TTL (default 24 h) |
record_validator |
Prefix-registered PUT_VALUE validators (accept / reject / ignore) (#198) |
ipns_validator |
Built-in /ipns/ validator (IpnsEntry protobuf, DAG-CBOR data, Ed25519 signatureV2, monotonic sequence, EOL expiry) |
query |
Iterative findNode / findProviders (alpha=3 default) |
server |
Inbound RPC handler on std.Io streams |
client |
Bootstrap + high-level lookups |
mode |
Client vs server mode (maps from AutoNAT NatStatus) |
| Parameter | Default |
|---|---|
| k (replication) | 20 |
| alpha (concurrency) | 3 |
| Provider TTL | 24 h |
| Provider republish | 12 h |
Wire a kad_dht.Client into Host via setKadDhtClient:
var kad = try zl.kad_dht.Client.init(allocator, local_id_b58, .{}, query_peer);
host.setKadDhtClient(&kad);When AutoNAT is enabled, Host.runPeriodicTicks promotes or demotes DHT mode:
- Public →
kad.setMode(.server) - Private / unknown →
kad.setMode(.client)
On each heartbeat, Host also:
- Calls
kad.republishProvidersfor local provider keys past the republish window (uses Identify listen addrs). - On
onConnectionClosed, callskad.onPeerDisconnectedso dead peers are removed from the routing table.
Outbound ADD_PROVIDER fan-out uses the embedder-supplied QueryPeerFn on the client.
Transport remains embedder-owned when not using bundled QUIC kad streams:
- Server mode: negotiate
/ipfs/kad/1.0.0, dispatch stream toServer.handleStream. Only server-mode peers are inserted into remote routing tables. - Client mode: use
Client+QueryPeerFnto open streams, write length-prefixed requests, read responses. - Bootstrap:
Client.bootstrapseeds configured peers then runsfindNode(local_id). - Provider ads:
Client.announceProvideroraddLocalProvider+ periodicrepublishProviders. - AutoNAT integration:
kad_dht.modeFromNatStatus(autonat_client.natStatus())or rely on Host glue whensetKadDhtClientis set.
const zl = @import("zig_libp2p");
fn queryPeer(ctx: ?*anyopaque, peer_id: []const u8, req: zl.kad_dht.MessageView, out: *zl.kad_dht.MessageOwned) !void {
_ = ctx;
_ = peer_id;
_ = req;
_ = out;
// Dial peer, negotiate kad protocol, exchange framed messages.
}
var client = try zl.kad_dht.Client.init(allocator, local_id, .{}, queryPeer);
try client.bootstrap(&boot_peers, now_ms);
const providers = try client.findProviders(content_key, now_ms);
defer client.freeProviders(providers);examples/kad_dht_membuf.zig — FIND_NODE round-trip through in-memory buffers (CI smoke-run).
#93 (library MVP): routing table, wire codec, iterative lookups, provider store, bootstrap API.
#203 (lifecycle):
- AutoNAT status change →
kad.setModevia Host glue (#206 + #203). - Periodic provider republish via
republishProviders/providersNeedingRepublish. onConnectionClosed→ routing-table peer eviction.- In-memory integration tests for advertise / lookup / republish / mode promotion.
Live-network acceptance (bootstrap.libp2p.io, cross-impl interop) remains embedder/manual validation.
#198 (record validators):
RecordValidatorregistry with longest-prefix matching.RecordStore.putValueconsults validators before storage; rejects incrementValidationStats.- Optional
Server.Config.on_validation_rejecthook for peer-score docking. /ipns/validator per the IPNS record spec: parses theIpnsEntryprotobuf and DAG-CBORdata, verifies Ed25519signatureV2over"ipns-signature:" ‖ dataagainst the key inlined in the name, enforces monotonicSequence, and rejects records past their EOLValidity. Ed25519 names only.
var reg = zl.kad_dht.RecordValidator.init(allocator);
defer reg.deinit();
try zl.kad_dht.ipns_validator.register(®, &allocator);
var stats: zl.kad_dht.ValidationStats = .{};
var server = try zl.kad_dht.Server.init(allocator, local_id, .{
.records = .{ .validators = ®, .validation_stats = &stats },
});