Read the wire version the network actually sends

luxfi/zap v1.2.6 — the version luxfi/node ships — emits wire version 2 by
default, and Go's Parse accepts 1 and 2 alike. This layer accepted only 1, so
every frame luxd sends was refused at the header.

The module header already says the codec constants come from zap.wire "so they
can never drift", and the canonical zap.wire had this right: VERSION1,
VERSION2, and a parse that admits both. The drift was here — the guard was
re-implemented locally against the single emitted VERSION instead of the
accepted pair. Source both constants from the canonical wire and admit what it
admits, so the claim in the header is true rather than aspirational.

Add the vectors that would have caught it. The suite round-tripped this layer
against itself; these are bytes captured from github.com/luxfi/zap v1.2.6 by
calling EncodeNodeIDHandshake, so they fail if this layer and the Go module
ever disagree. They also pin the accepted set exactly — 0 and 3 refused —
because admitting a frame the network refuses is as wrong as refusing one it
accepts, and pin that our handshake equals Go's in every byte but the version.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-28 23:38:41 -07:00
co-authored by Hanzo Dev
parent 074facbba5
commit 95df0efbcf
2 changed files with 72 additions and 4 deletions
+11 -3
View File
@@ -26,7 +26,13 @@ from zap import wire as _zw
# Codec constants are sourced from the canonical wire so they can never drift.
ZAP_MAGIC = _zw.MAGIC # b"ZAP\x00"
HEADER_SIZE = _zw.HEADER_SIZE # 16
VERSION = _zw.VERSION # 1
# Both accepted versions come from the canonical wire, so the set this layer
# admits is the set ``zap.wire.parse`` admits — which is the set Go's
# ``zap.Parse`` admits. luxd emits VERSION2 by default, so a reader that
# recognises only VERSION1 cannot read the live network.
VERSION1 = _zw.VERSION1 # 1 — legacy schema
VERSION2 = _zw.VERSION2 # 2 — current; what luxd emits
VERSION = _zw.VERSION # the version this layer emits
ALIGNMENT = _zw.ALIGNMENT # 8
MAX_MESSAGE_SIZE = 10 * 1024 * 1024 # 10 MB
@@ -76,8 +82,10 @@ class Message:
if data[:4] != ZAP_MAGIC:
raise ValueError(f"Bad ZAP magic: {data[:4]!r}")
ver = struct.unpack_from("<H", data, 4)[0]
if ver != VERSION:
raise ValueError(f"Unsupported ZAP version: {ver} (expected {VERSION})")
if ver not in (VERSION1, VERSION2):
raise ValueError(
f"Unsupported ZAP version: {ver} (expected {VERSION1} or {VERSION2})"
)
return cls(data)
@property
+61 -1
View File
@@ -8,6 +8,8 @@ from hanzo_zap.wire import (
ZAP_MAGIC,
HEADER_SIZE,
VERSION,
VERSION1,
VERSION2,
MSG_TYPE_CLOUD,
REQ_FLAG_REQ,
REQ_FLAG_RESP,
@@ -43,7 +45,11 @@ def test_zap_magic():
def test_version():
assert VERSION == 1
assert VERSION1 == 1
assert VERSION2 == 2
# What this layer emits. Reading is wider than writing: see
# test_reads_the_go_oracles_v2_handshake.
assert VERSION == VERSION1
def test_msg_type_cloud():
@@ -338,3 +344,57 @@ async def test_full_message_through_frames():
assert peer == "frame-test-node"
finally:
await cleanup()
# ── Conformance to the Go oracle ─────────────────────────────────────────
#
# Go is the network. These are not hand-written vectors: they are the output
# of github.com/luxfi/zap v1.2.6 — the version luxfi/node ships — captured by
# calling zap.EncodeNodeIDHandshake. luxd's default builder emits VERSION2, so
# a reader that admits only VERSION1 cannot read the live network at all.
GO_HANDSHAKE_V2 = bytes.fromhex(
"5a41500002000000100000005000000068616e7a6f2d6e6f646500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000"
)
GO_HANDSHAKE_NODEID_V2 = bytes.fromhex(
"5a4150000200000010000000500000004e6f646549442d415572454270666e6f595569436348465851646a3764415a363453396d534d5a73000000000000000000000000000000000000000028000000"
)
def test_reads_the_go_oracles_v2_handshake():
"""The regression test for the break: luxd emits VERSION2."""
for wire, want in (
(GO_HANDSHAKE_V2, "hanzo-node"),
(GO_HANDSHAKE_NODEID_V2, "NodeID-AUrEBpfnoYUiCcHFXQdj7dAZ64S9mSMZs"),
):
assert len(wire) == 80
msg = Message.parse(wire)
assert msg.version == VERSION2
assert parse_handshake(msg) == want
def test_version_acceptance_matches_go():
"""Go's Parse accepts exactly {1, 2}. Not 0, not 3.
Accepting a frame the network refuses is as wrong as refusing one it
accepts, so the bounds are exact rather than merely permissive.
"""
for version, accepted in ((0, False), (1, True), (2, True), (3, False)):
wire = bytearray(GO_HANDSHAKE_V2)
wire[4] = version
if accepted:
assert Message.parse(bytes(wire)).version == version
else:
with pytest.raises(ValueError):
Message.parse(bytes(wire))
def test_payload_is_byte_identical_to_go():
"""Our handshake equals Go's in every byte but the version.
That is what makes flipping the emitter to VERSION2 a one-byte change
rather than a re-encoding, and it proves the arena layout agrees.
"""
want = bytearray(GO_HANDSHAKE_V2)
want[4] = VERSION1
assert build_handshake("hanzo-node") == bytes(want)