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.