Skip to content

Commit 71a72e0

Browse files
tcoratgerclaude
andauthored
refactor(testing): unify duplicated hex codec helpers (leanEthereum#905)
The same 0x-prefixed hex decoder was copy-pasted under three forms across the test fixtures: _unhex in the gossipsub handler, _from_hex in the networking codec, and an inline bytes.fromhex(...removeprefix) in the ssz fixture. The networking codec also carried its own _to_hex inverse, with inline "0x" + x.hex() encodes scattered alongside. Introduce a single shared hex codec module exposing to_hex and from_hex, and route all three fixtures through it (audit finding FIX-03). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b17c82d commit 71a72e0

4 files changed

Lines changed: 60 additions & 65 deletions

File tree

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

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from unittest.mock import patch
77

88
from consensus_testing.test_fixtures.base import BaseConsensusFixture, BaseTestSpec
9+
from consensus_testing.test_fixtures.hex_codec import from_hex, to_hex
910
from lean_spec.base import StrictBaseModel
1011
from lean_spec.node.networking import PeerId
1112
from lean_spec.node.networking.gossipsub.behavior import GossipsubBehavior, PeerState
@@ -43,11 +44,6 @@ def _peer_id(name: str) -> PeerId:
4344
return PeerId.from_base58(name)
4445

4546

46-
def _unhex(hex_str: str) -> bytes:
47-
"""Decode a 0x-prefixed hex string to bytes."""
48-
return bytes.fromhex(hex_str.removeprefix("0x"))
49-
50-
5147
class GossipsubMeshParameters(StrictBaseModel):
5248
"""Mesh degree parameters the handler runs under."""
5349

@@ -204,19 +200,19 @@ def build_rpc(self) -> RPC:
204200
control_components["ihave"] = [
205201
ControlIHave(
206202
topic_id=TopicId(ihave.topic_id),
207-
message_ids=[_unhex(message_id) for message_id in ihave.message_ids],
203+
message_ids=[from_hex(message_id) for message_id in ihave.message_ids],
208204
)
209205
for ihave in self.ihave
210206
]
211207
if self.iwant:
212208
control_components["iwant"] = [
213-
ControlIWant(message_ids=[_unhex(message_id) for message_id in iwant.message_ids])
209+
ControlIWant(message_ids=[from_hex(message_id) for message_id in iwant.message_ids])
214210
for iwant in self.iwant
215211
]
216212
if self.idontwant:
217213
control_components["idontwant"] = [
218214
ControlIDontWant(
219-
message_ids=[_unhex(message_id) for message_id in idontwant.message_ids]
215+
message_ids=[from_hex(message_id) for message_id in idontwant.message_ids]
220216
)
221217
for idontwant in self.idontwant
222218
]
@@ -225,7 +221,7 @@ def build_rpc(self) -> RPC:
225221
publish=[
226222
Message(
227223
topic=TopicId(message.topic),
228-
data=_unhex(message.data) if message.data else b"",
224+
data=from_hex(message.data) if message.data else b"",
229225
)
230226
for message in self.publish
231227
],
@@ -432,7 +428,7 @@ async def _execute(self) -> GossipsubExpectation:
432428

433429
# IDONTWANT suppresses forwarding to peers that already have the message.
434430
for message_id_hex in peer_configuration.dont_want_ids:
435-
peer_state.dont_want_ids.add(MessageId(_unhex(message_id_hex)))
431+
peer_state.dont_want_ids.add(MessageId(from_hex(message_id_hex)))
436432
behavior._peers[peer_id] = peer_state
437433

438434
# Mesh topology determines who receives forwarded messages.
@@ -449,7 +445,7 @@ async def _execute(self) -> GossipsubExpectation:
449445
# Duplicate messages are silently dropped; IHAVE for seen IDs
450446
# does not trigger an IWANT response.
451447
for message_id_hex in self.initial_state.seen_message_ids:
452-
behavior.seen_cache.add(MessageId(_unhex(message_id_hex)), Timestamp(self.now))
448+
behavior.seen_cache.add(MessageId(from_hex(message_id_hex)), Timestamp(self.now))
453449

454450
# Message cache holds full message payloads for IWANT responses.
455451
#
@@ -458,9 +454,9 @@ async def _execute(self) -> GossipsubExpectation:
458454
for cached_message in self.initial_state.cached_messages:
459455
message = GossipsubMessage(
460456
topic=cached_message.topic.encode("utf-8"),
461-
raw_data=_unhex(cached_message.data),
457+
raw_data=from_hex(cached_message.data),
462458
)
463-
message._cached_id = MessageId(_unhex(cached_message.message_id))
459+
message._cached_id = MessageId(from_hex(cached_message.message_id))
464460
behavior.message_cache.put(TopicId(cached_message.topic), message)
465461

466462
# Build the incoming RPC from the event.
@@ -491,7 +487,7 @@ async def _execute(self) -> GossipsubExpectation:
491487

492488
publish = (
493489
[
494-
SentPublish(topic=str(message.topic), data="0x" + message.data.hex())
490+
SentPublish(topic=str(message.topic), data=to_hex(message.data))
495491
for message in rpc.publish
496492
]
497493
if rpc.publish
@@ -518,9 +514,7 @@ async def _execute(self) -> GossipsubExpectation:
518514
iwant=(
519515
[
520516
SentMessageIdentifiers(
521-
message_ids=[
522-
"0x" + message_id.hex() for message_id in iwant.message_ids
523-
]
517+
message_ids=[to_hex(message_id) for message_id in iwant.message_ids]
524518
)
525519
for iwant in rpc.control.iwant
526520
]
@@ -531,7 +525,7 @@ async def _execute(self) -> GossipsubExpectation:
531525
[
532526
SentMessageIdentifiers(
533527
message_ids=[
534-
"0x" + message_id.hex() for message_id in idontwant.message_ids
528+
to_hex(message_id) for message_id in idontwant.message_ids
535529
]
536530
)
537531
for idontwant in rpc.control.idontwant
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Shared 0x-prefixed hex codec for test fixtures."""
2+
3+
4+
def to_hex(data: bytes) -> str:
5+
"""Format raw bytes as a 0x-prefixed hex string."""
6+
return "0x" + data.hex()
7+
8+
9+
def from_hex(hex_string: str) -> bytes:
10+
"""Decode a 0x-prefixed hex string to bytes."""
11+
return bytes.fromhex(hex_string.removeprefix("0x"))

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

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pydantic import Field
66

77
from consensus_testing.test_fixtures.base import BaseConsensusFixture, BaseTestSpec
8+
from consensus_testing.test_fixtures.hex_codec import from_hex, to_hex
89
from lean_spec.base import StrictBaseModel
910
from lean_spec.node.networking.enr.enr import ENR
1011
from lean_spec.node.networking.gossipsub.message import GossipsubMessage
@@ -32,16 +33,6 @@
3233
from lean_spec.spec.forks import SubnetId
3334

3435

35-
def _to_hex(data: bytes) -> str:
36-
"""Format raw bytes as a 0x-prefixed hex string."""
37-
return "0x" + data.hex()
38-
39-
40-
def _from_hex(hex_str: str) -> bytes:
41-
"""Parse a 0x-prefixed hex string into raw bytes."""
42-
return bytes.fromhex(hex_str.removeprefix("0x"))
43-
44-
4536
class EncodedOutput(StrictBaseModel):
4637
"""Reference encoding for a roundtrip vector."""
4738

@@ -79,7 +70,7 @@ def run(self) -> VarintOutput:
7970
)
8071
assert byte_length == len(encoded), f"Length: {byte_length} != {len(encoded)}"
8172

82-
return VarintOutput(encoded=_to_hex(encoded), byte_length=byte_length)
73+
return VarintOutput(encoded=to_hex(encoded), byte_length=byte_length)
8374

8475

8576
class GossipTopicOutput(StrictBaseModel):
@@ -166,9 +157,9 @@ class GossipMessageIdentifier(StrictBaseModel):
166157
def run(self) -> GossipMessageIdentifierOutput:
167158
"""Compute the identifier: SHA256(domain + uint64_le(len(topic)) + topic + data)[:20]."""
168159
message_id = GossipsubMessage.compute_id(
169-
_from_hex(self.topic), _from_hex(self.data), domain=_from_hex(self.domain)
160+
from_hex(self.topic), from_hex(self.data), domain=from_hex(self.domain)
170161
)
171-
return GossipMessageIdentifierOutput(message_id=_to_hex(message_id))
162+
return GossipMessageIdentifierOutput(message_id=to_hex(message_id))
172163

173164

174165
class RpcSubscriptionSpec(StrictBaseModel):
@@ -205,12 +196,12 @@ class RpcMessageSpec(StrictBaseModel):
205196
def build(self) -> Message:
206197
"""Convert to the wire-format message."""
207198
return Message(
208-
from_peer=_from_hex(self.from_peer) if self.from_peer else b"",
209-
data=_from_hex(self.data) if self.data else b"",
210-
seqno=_from_hex(self.seqno) if self.seqno else b"",
199+
from_peer=from_hex(self.from_peer) if self.from_peer else b"",
200+
data=from_hex(self.data) if self.data else b"",
201+
seqno=from_hex(self.seqno) if self.seqno else b"",
211202
topic=TopicId(self.topic),
212-
signature=_from_hex(self.signature) if self.signature else b"",
213-
key=_from_hex(self.key) if self.key else b"",
203+
signature=from_hex(self.signature) if self.signature else b"",
204+
key=from_hex(self.key) if self.key else b"",
214205
)
215206

216207

@@ -279,14 +270,12 @@ def build(self) -> ControlMessage:
279270
ihave=[
280271
ControlIHave(
281272
topic_id=TopicId(ihave.topic_id),
282-
message_ids=[_from_hex(message_id) for message_id in ihave.message_ids],
273+
message_ids=[from_hex(message_id) for message_id in ihave.message_ids],
283274
)
284275
for ihave in self.ihave
285276
],
286277
iwant=[
287-
ControlIWant(
288-
message_ids=[_from_hex(message_id) for message_id in iwant.message_ids]
289-
)
278+
ControlIWant(message_ids=[from_hex(message_id) for message_id in iwant.message_ids])
290279
for iwant in self.iwant
291280
],
292281
graft=[ControlGraft(topic_id=TopicId(graft.topic_id)) for graft in self.graft],
@@ -296,7 +285,7 @@ def build(self) -> ControlMessage:
296285
],
297286
idontwant=[
298287
ControlIDontWant(
299-
message_ids=[_from_hex(message_id) for message_id in idontwant.message_ids]
288+
message_ids=[from_hex(message_id) for message_id in idontwant.message_ids]
300289
)
301290
for idontwant in self.idontwant
302291
],
@@ -334,7 +323,7 @@ def run(self) -> EncodedOutput:
334323
re_encoded = RPC.decode(encoded).encode()
335324
assert encoded == re_encoded, "RPC roundtrip produced different bytes"
336325

337-
return EncodedOutput(encoded=_to_hex(encoded))
326+
return EncodedOutput(encoded=to_hex(encoded))
338327

339328

340329
class ReqRespRequestRoundtrip(StrictBaseModel):
@@ -348,14 +337,14 @@ class ReqRespRequestRoundtrip(StrictBaseModel):
348337

349338
def run(self) -> EncodedOutput:
350339
"""Encode the request, decode it back, and emit the reference bytes."""
351-
ssz_data = _from_hex(self.ssz_data)
340+
ssz_data = from_hex(self.ssz_data)
352341
encoded = encode_request(ssz_data)
353342

354343
# Decode must recover the original SSZ bytes.
355344
decoded = decode_request(encoded)
356345
assert decoded == ssz_data, "Request roundtrip produced different bytes"
357346

358-
return EncodedOutput(encoded=_to_hex(encoded))
347+
return EncodedOutput(encoded=to_hex(encoded))
359348

360349

361350
class ReqRespResponseRoundtrip(StrictBaseModel):
@@ -373,15 +362,15 @@ class ReqRespResponseRoundtrip(StrictBaseModel):
373362
def run(self) -> EncodedOutput:
374363
"""Encode the response, decode it back, and emit the reference bytes."""
375364
code = ResponseCode(self.response_code)
376-
ssz_data = _from_hex(self.ssz_data)
365+
ssz_data = from_hex(self.ssz_data)
377366
encoded = code.encode(ssz_data)
378367

379368
# Decode must recover both the response code and SSZ bytes.
380369
decoded_code, decoded_data = ResponseCode.decode(encoded)
381370
assert decoded_code == code, f"Code mismatch: {decoded_code} != {code}"
382371
assert decoded_data == ssz_data, "Response roundtrip produced different bytes"
383372

384-
return EncodedOutput(encoded=_to_hex(encoded))
373+
return EncodedOutput(encoded=to_hex(encoded))
385374

386375

387376
class ResponseChunkSpec(StrictBaseModel):
@@ -424,9 +413,9 @@ def run(self) -> ResponseStreamOutput:
424413
"""Encode every chunk back-to-back and emit the reference stream."""
425414
buffer = bytearray()
426415
for chunk in self.chunks:
427-
buffer.extend(ResponseCode(chunk.response_code).encode(_from_hex(chunk.ssz_data)))
416+
buffer.extend(ResponseCode(chunk.response_code).encode(from_hex(chunk.ssz_data)))
428417
return ResponseStreamOutput(
429-
encoded=_to_hex(bytes(buffer)),
418+
encoded=to_hex(bytes(buffer)),
430419
chunk_count=len(self.chunks),
431420
)
432421

@@ -516,19 +505,19 @@ def run(self) -> EnrOutput:
516505
eth2_data = enr.eth2_data
517506
attestation_subnets = enr.attestation_subnets
518507
return EnrOutput(
519-
rlp=_to_hex(rlp_bytes),
508+
rlp=to_hex(rlp_bytes),
520509
seq=int(enr.seq),
521510
identity_scheme=enr.identity_scheme,
522-
node_id=_to_hex(enr.node_id) if enr.node_id else None,
523-
public_key=_to_hex(enr.public_key) if enr.public_key else None,
511+
node_id=to_hex(enr.node_id) if enr.node_id else None,
512+
public_key=to_hex(enr.public_key) if enr.public_key else None,
524513
ip4=enr.ip4 if enr.ip4 else None,
525514
udp_port=int(enr.udp_port) if enr.udp_port is not None else None,
526515
quic_port=int(enr.quic_port) if enr.quic_port is not None else None,
527516
multiaddr=str(enr.multiaddr()) if enr.multiaddr() is not None else None,
528517
eth2_data=(
529518
EnrEth2DataOutput(
530-
fork_digest=_to_hex(eth2_data.fork_digest),
531-
next_fork_version=_to_hex(eth2_data.next_fork_version),
519+
fork_digest=to_hex(eth2_data.fork_digest),
520+
next_fork_version=to_hex(eth2_data.next_fork_version),
532521
next_fork_epoch=int(eth2_data.next_fork_epoch),
533522
)
534523
if eth2_data is not None
@@ -576,7 +565,7 @@ def run(self) -> PeerIdentifierOutput:
576565
"rsa": KeyType.RSA,
577566
}
578567
protobuf = PublicKeyProtobuf(
579-
key_type=key_type_map[self.key_type], key_data=_from_hex(self.public_key)
568+
key_type=key_type_map[self.key_type], key_data=from_hex(self.public_key)
580569
)
581570
peer_id = PeerId.from_public_key(protobuf)
582571
peer_id_string = str(peer_id)
@@ -586,7 +575,7 @@ def run(self) -> PeerIdentifierOutput:
586575
assert roundtrip == peer_id, "PeerId Base58 roundtrip failed"
587576

588577
return PeerIdentifierOutput(
589-
protobuf_encoded=_to_hex(protobuf.encode()),
578+
protobuf_encoded=to_hex(protobuf.encode()),
590579
peer_id=peer_id_string,
591580
)
592581

@@ -615,14 +604,14 @@ class SnappyBlockRoundtrip(StrictBaseModel):
615604

616605
def run(self) -> SnappyBlockOutput:
617606
"""Compress, decompress back, and emit the reference bytes."""
618-
uncompressed_bytes = _from_hex(self.data)
607+
uncompressed_bytes = from_hex(self.data)
619608
compressed = compress(uncompressed_bytes)
620609

621610
decompressed = decompress(compressed)
622611
assert decompressed == uncompressed_bytes, "Snappy block roundtrip produced different bytes"
623612

624613
return SnappyBlockOutput(
625-
compressed=_to_hex(compressed),
614+
compressed=to_hex(compressed),
626615
compressed_length=len(compressed),
627616
uncompressed_length=len(uncompressed_bytes),
628617
)
@@ -652,14 +641,14 @@ class SnappyFrameRoundtrip(StrictBaseModel):
652641

653642
def run(self) -> SnappyFrameOutput:
654643
"""Compress with framing, decompress back, and emit the reference bytes."""
655-
uncompressed_bytes = _from_hex(self.data)
644+
uncompressed_bytes = from_hex(self.data)
656645
framed = frame_compress(uncompressed_bytes)
657646

658647
decompressed = frame_decompress(framed)
659648
assert decompressed == uncompressed_bytes, "Snappy frame roundtrip produced different bytes"
660649

661650
return SnappyFrameOutput(
662-
framed=_to_hex(framed),
651+
framed=to_hex(framed),
663652
framed_length=len(framed),
664653
uncompressed_length=len(uncompressed_bytes),
665654
)
@@ -704,7 +693,7 @@ def attempt_decode(self) -> Exception | None:
704693
"enr": ENR.from_rlp,
705694
}
706695
try:
707-
decoders[self.decoder](_from_hex(self.raw_bytes))
696+
decoders[self.decoder](from_hex(self.raw_bytes))
708697
except Exception as exception:
709698
return exception
710699
return None

0 commit comments

Comments
 (0)