Releases: blockblaz/zig-libp2p
v0.2.1
Fix: reqresp completes on empty/short responses instead of hanging to timeout
The reqresp requester only ever reacted to response bytes and ignored the stream FIN. When a responder closed the stream with no chunk — the libp2p "I don't have it" reply (zeam blocks_by_root for a root not in its DB, an empty blocks_by_range, the genesis anchor root every node requests) — the request never completed and hung until the embedder's request timeout (8 s in zeam), retrying forever. In a 3-node devnet this was a relentless ~60/min blocks_by_root retry storm that stalled finalization.
Fix: the requester now completes via zquic v1.7.42's rawAppStreamFullyReceived (FIN seen and all bytes up to the final size contiguously reassembled) — empty/complete responses finish in milliseconds; large in-flight responses are never truncated by a FIN that races ahead of the cwnd-queued payload.
- Bumps zquic to v1.7.42.
- Tests: empty-response-ends-fast regression; the 300 KB reqresp test still passes (no truncation).
Full diff: v0.2.0...v0.2.1
v0.2.0
Highlights
This release resolves the zeam devnet fork end-to-end. Live-validated: a 3-node devnet (with a deliberately delayed sync node) converges on a single head with all nodes finalizing; zero KnownInvalidBlock, zero ACK-starvation teardowns.
Fixes (since v0.1.98)
- zquic v1.7.41 (#247) — splits oversized stream writes into per-packet pending entries. Previously any QUIC stream write larger than one 1-RTT packet that queued under congestion/flow control was silently dropped (no retransmit). libp2p reqresp
blocks_by_rangebodies (~250 KB beam blocks) were lost, wedging a delayed node's initial sync into a permanent fork; theKnownInvalidBlocksymptom was the same bug surfacing as truncated-block verify failures. - non-blocking QUIC dials (#245, v0.1.98) —
handleDialno longer blocks the single drive thread (which deadlocked simultaneous mutual dials in the Initial handshake).
Tests
- #248 — large (~300 KB) reqresp regression test over QuicRuntime loopback (guards the oversized-frame fix).
- Mutual-dial regression test.
Full diff: v0.1.99...v0.2.0
v0.1.99
deps: bump zquic to v1.7.41 (#247).
Fixes silent data loss on any QUIC stream write larger than one 1-RTT packet that is queued under congestion/flow control — the oversized pending entry failed to serialize on drain and was discarded, never retransmitted. libp2p reqresp blocks_by_range bodies (~200 KB/block) were lost, wedging a zeam delayed-node's initial sync into a permanent fork. Small writes (gossip chunks, status, single small blocks) were unaffected.
Full diff: v0.1.98...v0.1.99
v0.1.98
Highlights
quic: non-blocking outbound dials — fixes an Initial-handshake deadlock that forked zeam devnets (#245)
QuicRuntime.handleDial previously spun a blocking drive loop for up to 20s per dial on the single drive thread, and never called pollAccept during it. Two peers dialing each other simultaneously (an all-to-all gossip mesh at boot) would both wedge in the QUIC Initial handshake — neither accepted the other's inbound — time out at 20s (stalled_phase=initial), and starve established connections of ACKs. Downstream a zeam 3-node devnet forked early and never finalized.
Dials are now advanced non-blocking by driveLoop each tick alongside the listener, pollAccept, established outbounds, gossip, and host ticks. Live-validated: a 3-node zeam devnet that previously sat at finalized=slot 0 now justifies steadily (slot 0→18 in lockstep with wall-clock). Adds a simultaneous mutual dial regression test.
Also included since v0.1.97
- #239 re-enable previously-skipped tests
- #242 connection-manager: protect/unprotect, expiry-based trim grace, jittered reconnect backoff
- #243 remove permanently-skipped handshake-sizes test
- #244 swarm: hook deadlines, event-queue backpressure policy, deferred command queue (#212)
Full diff: v0.1.97...v0.1.98
v0.1.97
Maintenance and developer-experience release on top of v0.1.96 — no protocol changes; the public API is unchanged. Full cross-impl interop matrix (zig × go-libp2p × rust-libp2p) green.
Tests
- Re-enabled the QUIC TLS remote peer-id loopback test — it now mints a libp2p TLS certificate (RFC 0001) in memory instead of the deprecated
std.fs.cwdfile I/O, and verifies the dialer recovers the listener's peer id over a real loopback handshake. (#239, #235) - Moved the 60s sustained-gossipsub soak out of
zig build testinto an opt-inzig build soak-test(gated bytest_options.enable_soak_tests), keeping the default test run fast. Fixed its assertion: gossipsub does not run the topic validator on the publisher's own messages, so only receivers are checked.
Release process
- Removed release-please; releases are now cut manually. A release is: tag the commit +
gh release create vX.Y.Z --target main --notes …, bumping.versioninbuild.zig.zonand the README install snippet in the same change.build.zig.zon.versionis now accurate again (it had drifted to0.1.87under the old automation). (#238)
Docs
- README Repository layout section + per-folder READMEs for
src/{core,primitives,protocols,transport,security}, reflecting the v0.1.96 layout reorg. (#237)
v0.1.96
Feature-heavy release: four new protocol areas, expanded discovery/NAT support, and a repository-layout reorg. Public API is additive — existing zig_libp2p.* exports are unchanged. Full cross-impl interop matrix (zig × go-libp2p × rust-libp2p) green.
New protocols
- Rendezvous
/rendezvous/1.0.0— namespace-scoped peer discovery: client (register / unregister / discover with cookie paging) and server with a registration store. Registrations are bound to the transport peer via signed peer records. (#234, #209) - mDNS LAN discovery —
_p2p._udp.localmulticast (IPv4 + IPv6),dnsaddrTXT ingestion,peer_discoveredevents,Hostauto-registerKnownPeer. Discovered dial targets are restricted to private/LAN ranges by default (allow_public_addrsopt-in). (#229, #230, #207) - gossipsub v1.1 peer scoring — per-topic time-in-mesh / delivery / invalid-message scoring with decay and gossip/publish/graylist/PX thresholds + opportunistic graft. Opt-in via
peer_scoring_enabled. (#232, #199)
Kademlia DHT
- Host lifecycle wiring: AutoNAT-driven mode promotion, periodic provider republish, routing-table eviction on disconnect. (#228, #203)
- Pluggable record validators with longest-prefix matching + an IPNS validator implementing the spec (IpnsEntry protobuf, DAG-CBOR
data, Ed25519signatureV2, monotonic sequence, EOL expiry), verified byte-for-byte against a go/boxo reference record. (#231, #233, #198)
NAT traversal & relay
- AutoNAT vote aggregation + active probing (sliding-window quorum, real dial-back verification, observed-IP amplification guard). (#226, #206)
- DCUtR auto-trigger on relayed connections with retry/backoff. (#225, #205)
- Circuit Relay v2 reservation auto-refresh +
/p2p-circuitdial path. (#224, #204) - Identify Push auto-triggered on advertisement changes, streamed over the QUIC runtime. (#222, #223, #202)
Repository & docs
- Repository layout rationalized into
core/·primitives/·protocols/, QUIC transport split, vendored deps moved tovendor/,build/helper split + opt-in soak-test. No public-API or import-path changes (@import("zig_libp2p")→src/root.zig). (#236) - README modernized (libp2p-style) with a spec-coverage matrix; links repointed to the
blockblazorg. (#221, #227)
Notes for embedders
- gossipsub peer scoring and mDNS public-addr acceptance are both opt-in; defaults are unchanged.
- The layout reorg keeps every existing
zig_libp2p.*export; no consumer changes required.
v0.1.95
gossipsub: FANOUT + FANOUT_TTL (#200)
Adds gossipsub v1.1 FANOUT so a node can publish to topics it is not subscribed to (e.g. a validator publishing to an attestation subnet it doesn't follow).
- New per-topic
fanoutpeer set keyed by topic; on a fanout publish, up tomesh_nconnected peers are selected (direct-peer / score / backoff aware) and reused until they disconnect or the entry expires. fanout_ttl_msconfig (libp2pFANOUT_TTL, default 60 s); heartbeat prunes stale fanout entries.subscribe()promotes existing fanout peers straight into the topic mesh.- Subscribed-topic local publishes now forward directed to mesh peers (spec-aligned with inbound forwarding) instead of broadcasting to all connected peers.
- Owns its topic keys (consistent with the subs/mesh key-ownership model from v0.1.94's interop SIGSEGV fix).
- 4 new unit tests (target selection, reuse-before-TTL, TTL prune, subscribe promotion).
Closes #200. Full cross-impl interop matrix (zig × go-libp2p × rust-libp2p) green.
Note for embedders: publishing on a subscribed topic is now mesh-directed rather than broadcast — confirm your mesh forms before relying on propagation.
v0.1.94
v0.1.72
Bump zquic to v1.7.24: the client now decodes the QUIC packet Length field and Handshake CRYPTO frame offset/length with permissive (non-minimal-tolerant) varint decoding on the receive path. Fixes the zeam↔lantern (ngtcp2/AWS-LC) handshake stalling at stalled_phase=initial — strict varint decode was silently dropping the trailing coalesced Handshake packet. (Full zquic↔ngtcp2 libp2p interop has a separate open item: AWS-LC rejects zquic's client cert flight with TLS bad_certificate.)
v0.1.41
v0.1.41
First tagged release since v0.1.17 — rolls up everything merged to main between v0.1.17 and v0.1.41. Highlights from PR #192 (the bug fix that motivated this release):
- gossipsub: inbound IHAVE now generates IWANT for unseen ids (libp2p v1.1 §3.4); previously dropped silently, breaking lazy-gossip recovery against rust- and go-libp2p peers.
- gossipsub: GRAFT during active PRUNE back-off now docks the sender's behaviour score (rust-libp2p
P7weight). - autonat: v2 amplification cost is now uniformly sampled across
[min, max]; the previousstd.math.clamp(min, 1, max)always returned the minimum.
See the full changelog below for the 0.1.18 → 0.1.41 history covering security fixes, the /ws WebSocket transport, libp2p-TLS UTCTime parsing fix, AutoNAT v2 + relay (circuit v2) + DCUtR + Kademlia DHT scaffolding, persistent per-peer /meshsub/1.1.0 streams, and matching zquic protocol fixes pulled in along the way.
0.1.41 (2026-06-11)
Fixed
-
gossipsub: handle inbound IHAVE control messages by emitting IWANT for
unseen ids (libp2p gossipsub v1.1 §3.4). Previously zig-libp2p decoded IHAVE
and dropped it on the floor, so lazy-gossip recovery against rust- and
go-libp2p peers was effectively dead: a peer outside our mesh could announce
message ids but we never pulled them, breaking mesh healing after a partition
and degrading dissemination redundancy. The new
runtime.handleIHaveOfferfilters againstpull_fifo+recent_seen,
caps the number of ids accepted per RPC (max_ihave_ids_per_rpc) and the
number requested in the resulting IWANT (max_iwant_ids_per_rpc) — both
defaulting to 5000 to match rust-libp2p — and exposes
iwantTxCount/iwantIdsRequestedCount/ihaveIdsCappedCount. -
gossipsub: dock the sender's behaviour score (configurable via
graft_during_backoff_score_delta, default-50) when an inbound GRAFT
arrives during the peer's own active PRUNE back-off window. rust-libp2p
models this as theP7weight; we previously refused the GRAFT with a
PRUNE+remaining-backoff but applied no penalty, so a misbehaving peer
could flood-GRAFT freely. The unsubscribe-cooldown branch keeps its
original behaviour (no penalty — that path doesn't represent the same
flood pattern). -
autonat: v2 amplification cost is now uniformly sampled across the
full[amplification_min_bytes, amplification_max_bytes]range from a
SplitMix64 step over the peer nonce. The previous shape
std.math.clamp(min, 1, max)always returned exactly the minimum
(30 KiB by default) and silently ignoredamplification_max_bytes,
making the cost trivially predictable to a client and leaving the
spec-mandated range knob dead.
0.1.40 (2026-06-11)
Fixed
- transport/quic_runtime: detect outbound QUIC connection close as soon as
CONNECTION_CLOSEis received, not after the 3×PTO draining deadline. zquic
flipsconn.draining = trueonCONNECTION_CLOSEreceipt but keeps
conn.phaseat.connecteduntil the drain timer reaps the slot — the
client side never reaps at all today. The previous detector only checked
phase == .closed, so a remote-initiated close (e.g. rust-libp2p ending the
session due to gossipsub mesh degrade or transport error) was silent on the
zeam side:outbound_by_peerretained the dead entry, gossip publishes
drained into the void, andconnection_managernever scheduled a redial.
NowdetectOutboundConnectionClosetreatsphase == .closed || draining
as terminal and fireshost.onConnectionClosedimmediately, mirroring the
listener-side close path.
0.1.39 (2026-06-11)
Fixed
- transport/quic_runtime: bind the persistent
/meshsub/1.1.0publish stream
to the outbound (locally-dialed) QUIC connection only. When a rust-libp2p
peer dials first, opening gossip publish on that inbound leg delivers a few
frames then stalls once the dialer leg comes up; zeam→ethlambda gossip now
migrates to the outbound connection and replays SUBSCRIBE there.
0.1.22 (2026-06-10)
Fixed
- transport/quic_runtime: respond to inbound
/ipfs/id/push/1.0.0streams from
rust-libp2p identify (ethlambda opens push after the initial identify exchange).
Previously these streams gotna→ProtocolNegotiationFailedat startup.
Also log accumulated byte count on other inbound handshake failures for easier
diagnosis.
0.1.21 (2026-06-10)
Fixed
- transport/multistream_negotiate, transport/stream_multistream: thread the
framing detected from the multistream offer line through every subsequent
token read. The previous per-token first-byte auto-detection
('/'⇒ legacy, otherwise delimited) collided with go-multistream delimited
framing whenever the varint length byte equalled0x2F = '/'— i.e. a token
of total wire length 47 bytes. The lean consensus protocol id
/leanconsensus/req/blocks_by_root/1/ssz_snappyhas a 46-byte body, so its
delimited wire form starts with'/'and got mis-classified as a legacy
line. The responder mis-parsed the offer and repliednaeven though the
protocol was supported, and the initiator mis-parsed the ack the same way.
In practice rust-libp2p (ethlambda) observedThe remote supports none of the requested protocolsfor everyblocks_by_rootrequest to a
zig-libp2p peer, blocking chain sync.statusandblocks_by_rangewere
unaffected because their wire lengths (39 and 48) don't collide with'/'.
Fixed
- transport/quic_runtime: drain the per-stream multistream-select tail
into the protocol dispatch accumulators (gossipsub, req/resp, relay) before
reading new bytes from the raw recv buffer. rust-libp2p and go-libp2p
routinely flush the protocol ack and the first application bytes in a
single QUIC STREAM frame; those bytes landed inms_tailafter
negotiation and were never forwarded, so inboundblocks_by_root/
statusrequests timed out on the responder (#184).
0.1.19 (2026-06-10)
Fixed
- transport/quic_runtime: persist the multistream-select accumulator across
drive ticks for inbound streams. Previously a partial responder negotiation
(DialFailed) lost the bytes the helper had already pulled from the raw
reader, so the second attempt mis-parsed and the peer sawnafor
legitimate protocols (e.g. rust-libp2pblocks_by_rootrequests).
0.1.18 (2026-06-10)
Fixed
- transport/quic_runtime: use go-multistream delimited framing on outbound
req/resp and publish streams so rust-libp2p responders accept the handshake
(#184). Replace the
legacy two-newline inbound pre-buffer with incremental responder negotiation.
Answer inbound/ipfs/id/1.0.0and/ipfs/ping/1.0.0streams from
rust-libp2p peers.