Skip to content

Releases: blockblaz/zig-libp2p

v0.2.1

19 Jun 22:45

Choose a tag to compare

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

19 Jun 20:40

Choose a tag to compare

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_range bodies (~250 KB beam blocks) were lost, wedging a delayed node's initial sync into a permanent fork; the KnownInvalidBlock symptom was the same bug surfacing as truncated-block verify failures.
  • non-blocking QUIC dials (#245, v0.1.98) — handleDial no 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

19 Jun 18:50
b79f39c

Choose a tag to compare

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

19 Jun 15:39

Choose a tag to compare

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

19 Jun 08:53
76a1093

Choose a tag to compare

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.cwd file 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 test into an opt-in zig build soak-test (gated by test_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 .version in build.zig.zon and the README install snippet in the same change. build.zig.zon .version is now accurate again (it had drifted to 0.1.87 under 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

18 Jun 23:01
7e3eb6c

Choose a tag to compare

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.local multicast (IPv4 + IPv6), dnsaddr TXT ingestion, peer_discovered events, Host auto-registerKnownPeer. Discovered dial targets are restricted to private/LAN ranges by default (allow_public_addrs opt-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, Ed25519 signatureV2, 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-circuit dial 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 to vendor/, 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 blockblaz org. (#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

17 Jun 15:20
bcb2f40

Choose a tag to compare

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 fanout peer set keyed by topic; on a fanout publish, up to mesh_n connected peers are selected (direct-peer / score / backoff aware) and reused until they disconnect or the entry expires.
  • fanout_ttl_ms config (libp2p FANOUT_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

17 Jun 15:14
a5fb5e2

Choose a tag to compare

Bumps zquic to v1.7.40: fixes the u5 integer-overflow panic in the 1-RTT PN-candidate sweep that crashed QUIC nodes on undecryptable datagrams >31 bytes. (#220)

v0.1.72

13 Jun 22:41

Choose a tag to compare

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

11 Jun 17:57
344d115

Choose a tag to compare

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 P7 weight).
  • autonat: v2 amplification cost is now uniformly sampled across [min, max]; the previous std.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.handleIHaveOffer filters against pull_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 the P7 weight; 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 ignored amplification_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_CLOSE is received, not after the 3×PTO draining deadline. zquic
    flips conn.draining = true on CONNECTION_CLOSE receipt but keeps
    conn.phase at .connected until 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_peer retained the dead entry, gossip publishes
    drained into the void, and connection_manager never scheduled a redial.
    Now detectOutboundConnectionClose treats phase == .closed || draining
    as terminal and fires host.onConnectionClosed immediately, mirroring the
    listener-side close path.

0.1.39 (2026-06-11)

Fixed

  • transport/quic_runtime: bind the persistent /meshsub/1.1.0 publish 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.0 streams from
    rust-libp2p identify (ethlambda opens push after the initial identify exchange).
    Previously these streams got naProtocolNegotiationFailed at 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 equalled 0x2F = '/' — i.e. a token
    of total wire length 47 bytes. The lean consensus protocol id
    /leanconsensus/req/blocks_by_root/1/ssz_snappy has 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 replied na even though the
    protocol was supported, and the initiator mis-parsed the ack the same way.
    In practice rust-libp2p (ethlambda) observed The remote supports none of the requested protocols for every blocks_by_root request to a
    zig-libp2p peer, blocking chain sync. status and blocks_by_range were
    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 in ms_tail after
    negotiation and were never forwarded, so inbound blocks_by_root /
    status requests 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 saw na for
    legitimate protocols (e.g. rust-libp2p blocks_by_root requests).

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.0 and /ipfs/ping/1.0.0 streams from
    rust-libp2p peers.