Skip to content

Commit a49f2bf

Browse files
tcoratgerclaude
andauthored
test(networking): add Discovery v5 routing distance test vectors (#632)
Extends NetworkingCodecTest with xor_distance and log2_distance codec handlers. Adds 14 test vectors for the Kademlia distance functions that underpin peer discovery and k-bucket assignment. XOR distance vectors: self=0 (identity), symmetry, max distance (inverted IDs), adjacent IDs, high bit difference. Log2 distance vectors: self=0, spec nodes (bucket 253), max (bucket 256), and bucket boundary progression (1, 2, 8, 9, 256). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f64b103 commit a49f2bf

2 files changed

Lines changed: 125 additions & 1 deletion

File tree

packages/testing/src/consensus_testing/test_fixtures/networking_codec.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
TalkReq,
1919
TalkResp,
2020
)
21+
from lean_spec.subspecs.networking.discovery.routing import log2_distance, xor_distance
2122
from lean_spec.subspecs.networking.enr.enr import ENR
2223
from lean_spec.subspecs.networking.gossipsub.message import GossipsubMessage
2324
from lean_spec.subspecs.networking.gossipsub.rpc import (
@@ -40,7 +41,7 @@
4041
encode_request,
4142
)
4243
from lean_spec.subspecs.networking.transport.peer_id import KeyType, PeerId, PublicKeyProto
43-
from lean_spec.subspecs.networking.types import SeqNumber
44+
from lean_spec.subspecs.networking.types import NodeId, SeqNumber
4445
from lean_spec.subspecs.networking.varint import decode_varint, encode_varint
4546

4647
from .base import BaseConsensusFixture
@@ -108,6 +109,10 @@ def make_fixture(self) -> "NetworkingCodecTest":
108109
output = self._make_snappy_block()
109110
case "snappy_frame":
110111
output = self._make_snappy_frame()
112+
case "xor_distance":
113+
output = self._make_xor_distance()
114+
case "log2_distance":
115+
output = self._make_log2_distance()
111116
case _:
112117
raise ValueError(f"Unknown codec: {self.codec_name}")
113118
return self.model_copy(update={"output": output})
@@ -214,6 +219,20 @@ def _make_snappy_frame(self) -> dict[str, Any]:
214219
"uncompressedLength": len(data),
215220
}
216221

222+
def _make_xor_distance(self) -> dict[str, Any]:
223+
"""Compute XOR distance between two node IDs."""
224+
node_a = NodeId(_from_hex(self.input["nodeA"]))
225+
node_b = NodeId(_from_hex(self.input["nodeB"]))
226+
distance = xor_distance(node_a, node_b)
227+
return {"distance": hex(distance)}
228+
229+
def _make_log2_distance(self) -> dict[str, Any]:
230+
"""Compute log2 of XOR distance for k-bucket assignment."""
231+
node_a = NodeId(_from_hex(self.input["nodeA"]))
232+
node_b = NodeId(_from_hex(self.input["nodeB"]))
233+
distance = int(log2_distance(node_a, node_b))
234+
return {"distance": distance}
235+
217236
def _make_enr(self) -> dict[str, Any]:
218237
"""Parse an ENR string, re-serialize, assert roundtrip, extract properties."""
219238
enr_string = self.input["enrString"]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Test vectors for Discovery v5 distance computations.
2+
3+
XOR distance and log2 distance are the foundation of Kademlia routing.
4+
Every client must compute identical distances for correct peer discovery
5+
and k-bucket assignment.
6+
"""
7+
8+
import pytest
9+
from consensus_testing import NetworkingCodecTestFiller
10+
11+
pytestmark = pytest.mark.valid_until("Devnet")
12+
13+
# Official devp2p spec node IDs.
14+
NODE_A = "0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb"
15+
NODE_B = "0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9"
16+
17+
ZERO = "0x" + "00" * 32
18+
MAX = "0x" + "ff" * 32
19+
ONE = "0x" + "00" * 31 + "01"
20+
TWO = "0x" + "00" * 31 + "02"
21+
HIGH_BIT = "0x80" + "00" * 31
22+
LOW_BYTE_80 = "0x" + "00" * 31 + "80"
23+
BUCKET_9 = "0x" + "00" * 30 + "0100"
24+
25+
26+
# --- XOR distance ---
27+
28+
29+
def test_xor_distance_self(networking_codec: NetworkingCodecTestFiller) -> None:
30+
"""XOR distance to self is always zero (identity property)."""
31+
networking_codec(codec_name="xor_distance", input={"nodeA": NODE_A, "nodeB": NODE_A})
32+
33+
34+
def test_xor_distance_symmetric(networking_codec: NetworkingCodecTestFiller) -> None:
35+
"""d(A, B) == d(B, A) (symmetry property)."""
36+
networking_codec(codec_name="xor_distance", input={"nodeA": NODE_A, "nodeB": NODE_B})
37+
38+
39+
def test_xor_distance_symmetric_reverse(
40+
networking_codec: NetworkingCodecTestFiller,
41+
) -> None:
42+
"""Reverse order produces identical distance."""
43+
networking_codec(codec_name="xor_distance", input={"nodeA": NODE_B, "nodeB": NODE_A})
44+
45+
46+
def test_xor_distance_max(networking_codec: NetworkingCodecTestFiller) -> None:
47+
"""XOR of zero and all-ones produces maximum distance (2^256 - 1)."""
48+
networking_codec(codec_name="xor_distance", input={"nodeA": ZERO, "nodeB": MAX})
49+
50+
51+
def test_xor_distance_adjacent(networking_codec: NetworkingCodecTestFiller) -> None:
52+
"""XOR of 0x01 and 0x02 is 0x03 (lowest bits)."""
53+
networking_codec(codec_name="xor_distance", input={"nodeA": ONE, "nodeB": TWO})
54+
55+
56+
def test_xor_distance_high_bit(networking_codec: NetworkingCodecTestFiller) -> None:
57+
"""Single high bit difference. Distance = 2^255."""
58+
networking_codec(codec_name="xor_distance", input={"nodeA": HIGH_BIT, "nodeB": ZERO})
59+
60+
61+
# --- Log2 distance (k-bucket assignment) ---
62+
63+
64+
def test_log2_distance_self(networking_codec: NetworkingCodecTestFiller) -> None:
65+
"""Log2 distance to self is 0."""
66+
networking_codec(codec_name="log2_distance", input={"nodeA": NODE_A, "nodeB": NODE_A})
67+
68+
69+
def test_log2_distance_spec_nodes(
70+
networking_codec: NetworkingCodecTestFiller,
71+
) -> None:
72+
"""Log2 distance between official spec nodes A and B is 253."""
73+
networking_codec(codec_name="log2_distance", input={"nodeA": NODE_A, "nodeB": NODE_B})
74+
75+
76+
def test_log2_distance_max(networking_codec: NetworkingCodecTestFiller) -> None:
77+
"""Maximum log2 distance is 256 (all bits differ)."""
78+
networking_codec(codec_name="log2_distance", input={"nodeA": ZERO, "nodeB": MAX})
79+
80+
81+
def test_log2_distance_bucket_1(networking_codec: NetworkingCodecTestFiller) -> None:
82+
"""Single lowest bit difference lands in bucket 1."""
83+
networking_codec(codec_name="log2_distance", input={"nodeA": ZERO, "nodeB": ONE})
84+
85+
86+
def test_log2_distance_bucket_2(networking_codec: NetworkingCodecTestFiller) -> None:
87+
"""Bit 1 set lands in bucket 2."""
88+
networking_codec(codec_name="log2_distance", input={"nodeA": ZERO, "nodeB": TWO})
89+
90+
91+
def test_log2_distance_bucket_8(networking_codec: NetworkingCodecTestFiller) -> None:
92+
"""Byte boundary: 0x80 in last byte lands in bucket 8."""
93+
networking_codec(codec_name="log2_distance", input={"nodeA": ZERO, "nodeB": LOW_BYTE_80})
94+
95+
96+
def test_log2_distance_bucket_9(networking_codec: NetworkingCodecTestFiller) -> None:
97+
"""0x0100 in last two bytes lands in bucket 9."""
98+
networking_codec(codec_name="log2_distance", input={"nodeA": ZERO, "nodeB": BUCKET_9})
99+
100+
101+
def test_log2_distance_bucket_256(
102+
networking_codec: NetworkingCodecTestFiller,
103+
) -> None:
104+
"""Highest bit only (0x80 in first byte) lands in bucket 256."""
105+
networking_codec(codec_name="log2_distance", input={"nodeA": ZERO, "nodeB": HIGH_BIT})

0 commit comments

Comments
 (0)