Pure Zig implementation of the Ethereum Node Discovery Protocol v5 (discv5). The goal is a small library suitable for embedding in clients and tooling, with a single Zig dependency (zig-varint) for shared varint encoding.
Latest release: v0.1.0 — semantic version in build.zig.zon must match the git tag (without the v prefix).
The codebase is modular and 0.1.0 includes a working local node (node): inbound handleReceive, outbound handshake initiation, session cache, encrypted PING/PONG, FINDNODE/NODES (cached peer ENRs, chunked replies), and TALKREQ/TALKRESP (default echo). Routing follows discv5-theory (e.g. FINDNODE only returns ping_replied peers). Optional ingress rate limits and egress limits in the UDP pump share ingress_limit.
Experimental / incomplete relative to the full spec: non-final topic advertisement message types (0x07–0x0a) are gated behind -Dexperimental_topic_wire (default on); turn it off for strict “unknown type” rejection. Production hardening, interoperability against every client, and topic-advertisement wire finalization are still evolving.
| Area | Module | Notes |
|---|---|---|
| Shared errors | errors |
Common error sets (includes NotImplemented for unused API placeholders) |
| Varint | varint |
Unsigned LEB128 (u64), minimal encoding, strict decode |
| RLP | rlp |
Strings and lists for devp2p payloads |
| Wire | wire |
MessageKind + re-exports of varint, packet, message, message_crypto, etc. |
| Message | message |
Ordinary message RLP encode/decode; REGTOPIC/TICKET/REGCONFIRMATION/TOPICQUERY (0x07–0x0a) decode only when -Dexperimental_topic_wire=true (default). Encode helpers exist for tooling. |
| Message crypto | message_crypto |
AES-128-GCM for ordinary message ciphertext (spec section 2.3) |
| ENR | enr |
EIP-778 textual enr: decode (base64url + RLP layout checks) |
| Handshake | handshake (alias crypto) |
HKDF session keys, identity-proof SHA-256 |
| Identity v4 | identity_v4 |
Compressed pubkey, ECDH (eph), ECDSA identity proof (64-byte raw signature) |
| Packet | packet |
UDP bounds, header unmask, static header + auth layouts |
| Routing | routing |
Kademlia table: 256 buckets (k=16), per-bucket replacement cache, LRU/MRU, closest + FINDNODE export |
| Session | session |
LRU session table (node id + UDP endpoint), cached keys, GCM nonce helper |
| Topic | topic |
Topic table per discv5-theory: FIFO queues, per-topic and global caps, target_ad_lifetime purge, registration wait hints |
| Ingress limits | ingress_limit |
Sliding-window datagram limits (per-peer and global); used by Node and udp_runtime egress |
| Node | node |
Discovery node state machine: sessions, challenges, encrypted messages, routing integration, configurable TTLs and caps |
| UDP runtime | udp_runtime |
libc IPv4 UDP socket helpers and receive/emit loop into Node.handleReceive (link_libc required on the module) |
- Zig 0.16.0 or newer (see
build.zig.zon).
This package follows Semantic Versioning. The current version is declared in build.zig.zon (version field) and should be bumped on release.
zig build test
zig fmt --check .Build option (non-final topic advertisement wire):
zig build test -Dexperimental_topic_wire=falseAdd zig_discv5 as a path or URL dependency in your build.zig.zon, then import the module in build.zig:
const discv5 = b.dependency("zig_discv5", .{
.target = target,
.optimize = optimize,
}).module("zig_discv5");Exact dependency shape depends on whether you consume the package from a git URL, tarball, or local path.
.github/workflows/ci.yml— on pushes and PRs tomain: format check,zig build test, and the same tests with-Dexperimental_topic_wire=false.
Manual release (creates a GitHub Release and tag at the current commit):
- Ensure
versioninbuild.zig.zonmatches the tag without thevprefix (e.g. tagv0.1.0↔ version0.1.0). - Actions → Release → Run workflow → set tag (default
v0.1.0).
Workflow: .github/workflows/release.yml.
- discv5.md — overview
- discv5-wire.md — wire encoding
- discv5-theory.md — algorithms