diff --git a/src/lean_spec/__main__.py b/src/lean_spec/__main__.py index dcde40ac..109d5299 100644 --- a/src/lean_spec/__main__.py +++ b/src/lean_spec/__main__.py @@ -34,7 +34,6 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.genesis import GenesisConfig -from lean_spec.subspecs.networking import compute_subnet_id from lean_spec.subspecs.networking.client import LiveNetworkEventSource from lean_spec.subspecs.networking.gossipsub import GossipTopic from lean_spec.subspecs.networking.reqresp.message import Status @@ -98,13 +97,12 @@ def resolve_bootnode(bootnode: str) -> str: if not enr.verify_signature(): raise ValueError(f"ENR signature verification failed: {enr}") - # ENR.multiaddr() returns None when the record lacks IP or TCP port. + # ENR.multiaddr() returns None when the record lacks IP or UDP port. # - # This happens with discovery-only ENRs that only contain UDP info. - # We require TCP for libp2p connections. + # We require UDP for QUIC connections. multiaddr = enr.multiaddr() if multiaddr is None: - raise ValueError(f"ENR has no TCP connection info: {enr}") + raise ValueError(f"ENR has no UDP connection info: {enr}") return multiaddr # Already a multiaddr string. Pass through without validation. @@ -495,7 +493,7 @@ async def run_node( subnet_id = 0 logger.info("No local validator id; subscribing to attestation subnet %d", subnet_id) else: - subnet_id = compute_subnet_id(validator_id, ATTESTATION_COMMITTEE_COUNT) + subnet_id = validator_id.compute_subnet_id(ATTESTATION_COMMITTEE_COUNT) attestation_subnet_topic = str(GossipTopic.attestation_subnet(GOSSIP_FORK_DIGEST, subnet_id)) event_source.subscribe_gossip_topic(attestation_subnet_topic) logger.info("Subscribed to gossip topics: %s, %s", block_topic, attestation_subnet_topic) diff --git a/src/lean_spec/subspecs/containers/validator.py b/src/lean_spec/subspecs/containers/validator.py index be3380c6..e2f2492c 100644 --- a/src/lean_spec/subspecs/containers/validator.py +++ b/src/lean_spec/subspecs/containers/validator.py @@ -24,6 +24,17 @@ def is_valid(self, num_validators: int) -> bool: """Check if this index is within valid bounds for a registry of given size.""" return int(self) < num_validators + def compute_subnet_id(self, num_committees: "int | Uint64") -> int: + """Compute the attestation subnet id for this validator. + + Args: + num_committees: Positive number of committees. + + Returns: + An integer subnet id in 0..(num_committees-1). + """ + return int(self) % int(num_committees) + class ValidatorIndices(SSZList[ValidatorIndex]): """List of validator indices up to registry limit.""" diff --git a/src/lean_spec/subspecs/forkchoice/store.py b/src/lean_spec/subspecs/forkchoice/store.py index e371259d..a0ef1330 100644 --- a/src/lean_spec/subspecs/forkchoice/store.py +++ b/src/lean_spec/subspecs/forkchoice/store.py @@ -30,7 +30,6 @@ from lean_spec.subspecs.containers.attestation.attestation import SignedAggregatedAttestation from lean_spec.subspecs.containers.block import BlockLookup from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.networking import compute_subnet_id from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import ( AggregatedSignatureProof, @@ -403,10 +402,10 @@ def on_gossip_attestation( if is_aggregator: assert self.validator_id is not None, "Current validator ID must be set for aggregation" - current_validator_subnet = compute_subnet_id( - self.validator_id, ATTESTATION_COMMITTEE_COUNT + current_validator_subnet = self.validator_id.compute_subnet_id( + ATTESTATION_COMMITTEE_COUNT ) - attester_subnet = compute_subnet_id(validator_id, ATTESTATION_COMMITTEE_COUNT) + attester_subnet = validator_id.compute_subnet_id(ATTESTATION_COMMITTEE_COUNT) if current_validator_subnet != attester_subnet: # Not part of our committee; ignore for committee aggregation. pass @@ -661,11 +660,11 @@ def on_block( # as the current validator. if self.validator_id is not None: proposer_validator_id = proposer_attestation.validator_id - proposer_subnet_id = compute_subnet_id( - proposer_validator_id, ATTESTATION_COMMITTEE_COUNT + proposer_subnet_id = proposer_validator_id.compute_subnet_id( + ATTESTATION_COMMITTEE_COUNT ) - current_validator_subnet_id = compute_subnet_id( - self.validator_id, ATTESTATION_COMMITTEE_COUNT + current_validator_subnet_id = self.validator_id.compute_subnet_id( + ATTESTATION_COMMITTEE_COUNT ) if proposer_subnet_id == current_validator_subnet_id: proposer_sig_key = SignatureKey( diff --git a/src/lean_spec/subspecs/networking/__init__.py b/src/lean_spec/subspecs/networking/__init__.py index 23f99226..dcd3024a 100644 --- a/src/lean_spec/subspecs/networking/__init__.py +++ b/src/lean_spec/subspecs/networking/__init__.py @@ -32,7 +32,6 @@ PeerDisconnectedEvent, PeerStatusEvent, ) -from .subnet import compute_subnet_id from .transport import PeerId from .types import DomainType, ForkDigest, ProtocolId @@ -74,5 +73,4 @@ "ForkDigest", "PeerId", "ProtocolId", - "compute_subnet_id", ] diff --git a/src/lean_spec/subspecs/networking/enr/enr.py b/src/lean_spec/subspecs/networking/enr/enr.py index 81d38b26..f81c077e 100644 --- a/src/lean_spec/subspecs/networking/enr/enr.py +++ b/src/lean_spec/subspecs/networking/enr/enr.py @@ -128,24 +128,12 @@ def ip6(self) -> str | None: return ":".join(ip_bytes[i : i + 2].hex() for i in range(0, 16, 2)) return None - @property - def tcp_port(self) -> int | None: - """TCP port (applies to both IPv4 and IPv6 unless tcp6 is set).""" - port = self.get(keys.TCP) - return int.from_bytes(port, "big") if port else None - @property def udp_port(self) -> int | None: """UDP port for discovery (applies to both unless udp6 is set).""" port = self.get(keys.UDP) return int.from_bytes(port, "big") if port else None - @property - def tcp6_port(self) -> int | None: - """IPv6-specific TCP port. Falls back to tcp_port if not set.""" - port = self.get(keys.TCP6) - return int.from_bytes(port, "big") if port else None - @property def udp6_port(self) -> int | None: """IPv6-specific UDP port. Falls back to udp_port if not set.""" @@ -153,11 +141,11 @@ def udp6_port(self) -> int | None: return int.from_bytes(port, "big") if port else None def multiaddr(self) -> Multiaddr | None: - """Construct multiaddress from endpoint info.""" - if self.ip4 and self.tcp_port: - return f"/ip4/{self.ip4}/tcp/{self.tcp_port}" - if self.ip6 and self.tcp_port: - return f"/ip6/{self.ip6}/tcp/{self.tcp_port}" + """Construct QUIC multiaddress from endpoint info.""" + if self.ip4 and self.udp_port: + return f"/ip4/{self.ip4}/udp/{self.udp_port}/quic-v1" + if self.ip6 and self.udp_port: + return f"/ip6/{self.ip6}/udp/{self.udp_port}/quic-v1" return None @property @@ -340,8 +328,6 @@ def __str__(self) -> str: parts = [f"ENR(seq={self.seq}"] if self.ip4: parts.append(f"ip={self.ip4}") - if self.tcp_port: - parts.append(f"tcp={self.tcp_port}") if self.udp_port: parts.append(f"udp={self.udp_port}") if eth2 := self.eth2_data: diff --git a/src/lean_spec/subspecs/networking/enr/keys.py b/src/lean_spec/subspecs/networking/enr/keys.py index a39f09a9..6b83764f 100644 --- a/src/lean_spec/subspecs/networking/enr/keys.py +++ b/src/lean_spec/subspecs/networking/enr/keys.py @@ -23,18 +23,12 @@ IP: Final[EnrKey] = "ip" """IPv4 address (4 bytes).""" -TCP: Final[EnrKey] = "tcp" -"""TCP port (big-endian integer).""" - UDP: Final[EnrKey] = "udp" """UDP port for discovery (big-endian integer).""" IP6: Final[EnrKey] = "ip6" """IPv6 address (16 bytes).""" -TCP6: Final[EnrKey] = "tcp6" -"""IPv6-specific TCP port.""" - UDP6: Final[EnrKey] = "udp6" """IPv6-specific UDP port.""" diff --git a/src/lean_spec/subspecs/networking/subnet.py b/src/lean_spec/subspecs/networking/subnet.py deleted file mode 100644 index 8a3c8fd1..00000000 --- a/src/lean_spec/subspecs/networking/subnet.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Subnet helpers for networking. - -Provides a small utility to compute a validator's attestation subnet id from -its validator index and number of committees. -""" - -from __future__ import annotations - -from lean_spec.types import Uint64 - - -def compute_subnet_id(validator_index: Uint64, num_committees: Uint64) -> int: - """Compute the attestation subnet id for a validator. - - Args: - validator_index: Non-negative validator index . - num_committees: Positive number of committees. - - Returns: - An integer subnet id in 0..(num_committees-1). - """ - subnet_id = validator_index % num_committees - return subnet_id diff --git a/src/lean_spec/subspecs/node/node.py b/src/lean_spec/subspecs/node/node.py index 1b612d7a..eb245c1f 100644 --- a/src/lean_spec/subspecs/node/node.py +++ b/src/lean_spec/subspecs/node/node.py @@ -30,7 +30,6 @@ from lean_spec.subspecs.containers.validator import ValidatorIndex from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.networking import NetworkEventSource, NetworkService -from lean_spec.subspecs.networking.subnet import compute_subnet_id from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.sync import BlockCache, NetworkRequester, PeerManager, SyncService from lean_spec.subspecs.validator import ValidatorRegistry, ValidatorService @@ -261,7 +260,7 @@ def from_genesis(cls, config: NodeConfig) -> Node: # Create a wrapper for publish_attestation that computes the subnet_id # from the validator_id in the attestation async def publish_attestation_wrapper(attestation: SignedAttestation) -> None: - subnet_id = compute_subnet_id(attestation.validator_id, ATTESTATION_COMMITTEE_COUNT) + subnet_id = attestation.validator_id.compute_subnet_id(ATTESTATION_COMMITTEE_COUNT) await network_service.publish_attestation(attestation, subnet_id) validator_service = ValidatorService( diff --git a/tests/lean_spec/subspecs/networking/enr/test_enr.py b/tests/lean_spec/subspecs/networking/enr/test_enr.py index 7b0bbd82..5ae3275c 100644 --- a/tests/lean_spec/subspecs/networking/enr/test_enr.py +++ b/tests/lean_spec/subspecs/networking/enr/test_enr.py @@ -97,20 +97,17 @@ def test_official_enr_is_valid(self) -> None: enr = ENR.from_string(OFFICIAL_ENR_STRING) assert enr.is_valid() - def test_official_enr_no_tcp_port(self) -> None: - """Official ENR does not have TCP port.""" - enr = ENR.from_string(OFFICIAL_ENR_STRING) - assert enr.tcp_port is None - def test_official_enr_no_ipv6(self) -> None: """Official ENR does not have IPv6 address.""" enr = ENR.from_string(OFFICIAL_ENR_STRING) assert enr.ip6 is None - def test_official_enr_no_multiaddr(self) -> None: - """Official ENR has no multiaddr (no TCP port).""" + def test_official_enr_has_quic_multiaddr(self) -> None: + """Official ENR has QUIC multiaddr (has UDP port).""" enr = ENR.from_string(OFFICIAL_ENR_STRING) - assert enr.multiaddr() is None + multiaddr = enr.multiaddr() + assert multiaddr is not None + assert multiaddr == f"/ip4/{OFFICIAL_IPV4}/udp/{OFFICIAL_UDP_PORT}/quic-v1" def test_official_enr_node_id(self) -> None: """Official ENR node ID matches keccak256(uncompressed_pubkey). @@ -422,24 +419,6 @@ def test_udp_port_returns_none_when_missing(self) -> None: ) assert enr.udp_port is None - def test_tcp_port_extracts_correctly(self) -> None: - """tcp_port extracts port number from big-endian bytes.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=Uint64(1), - pairs={keys.ID: b"v4", keys.TCP: (9000).to_bytes(2, "big")}, - ) - assert enr.tcp_port == 9000 - - def test_tcp_port_returns_none_when_missing(self) -> None: - """tcp_port returns None when 'tcp' key is absent.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=Uint64(1), - pairs={keys.ID: b"v4"}, - ) - assert enr.tcp_port is None - class TestValidationMethods: """Tests for ENR validation methods.""" @@ -516,21 +495,21 @@ def test_construction_fails_for_wrong_signature_length(self) -> None: class TestMultiaddrGeneration: """Tests for multiaddr() method.""" - def test_multiaddr_with_ipv4_and_tcp(self) -> None: - """multiaddr() generates correct format with IPv4 and TCP.""" + def test_multiaddr_with_ipv4_and_udp(self) -> None: + """multiaddr() generates QUIC format with IPv4 and UDP.""" enr = ENR( signature=Bytes64(b"\x00" * 64), seq=Uint64(1), pairs={ keys.ID: b"v4", keys.IP: b"\xc0\xa8\x01\x01", # 192.168.1.1 - keys.TCP: (9000).to_bytes(2, "big"), + keys.UDP: (9000).to_bytes(2, "big"), }, ) - assert enr.multiaddr() == "/ip4/192.168.1.1/tcp/9000" + assert enr.multiaddr() == "/ip4/192.168.1.1/udp/9000/quic-v1" - def test_multiaddr_with_ipv6_and_tcp(self) -> None: - """multiaddr() generates correct format with IPv6 and TCP.""" + def test_multiaddr_with_ipv6_and_udp(self) -> None: + """multiaddr() generates QUIC format with IPv6 and UDP.""" ipv6_bytes = b"\x00" * 15 + b"\x01" # ::1 enr = ENR( signature=Bytes64(b"\x00" * 64), @@ -538,20 +517,19 @@ def test_multiaddr_with_ipv6_and_tcp(self) -> None: pairs={ keys.ID: b"v4", keys.IP6: ipv6_bytes, - keys.TCP: (9000).to_bytes(2, "big"), + keys.UDP: (9000).to_bytes(2, "big"), }, ) - assert enr.multiaddr() == "/ip6/0000:0000:0000:0000:0000:0000:0000:0001/tcp/9000" + assert enr.multiaddr() == "/ip6/0000:0000:0000:0000:0000:0000:0000:0001/udp/9000/quic-v1" - def test_multiaddr_returns_none_without_tcp(self) -> None: - """multiaddr() returns None when TCP port is absent.""" + def test_multiaddr_returns_none_without_udp(self) -> None: + """multiaddr() returns None when UDP port is absent.""" enr = ENR( signature=Bytes64(b"\x00" * 64), seq=Uint64(1), pairs={ keys.ID: b"v4", keys.IP: b"\xc0\xa8\x01\x01", - keys.UDP: (30303).to_bytes(2, "big"), # UDP, not TCP }, ) assert enr.multiaddr() is None @@ -561,7 +539,7 @@ def test_multiaddr_returns_none_without_ip(self) -> None: enr = ENR( signature=Bytes64(b"\x00" * 64), seq=Uint64(1), - pairs={keys.ID: b"v4", keys.TCP: (9000).to_bytes(2, "big")}, + pairs={keys.ID: b"v4", keys.UDP: (9000).to_bytes(2, "big")}, ) assert enr.multiaddr() is None @@ -574,10 +552,10 @@ def test_multiaddr_prefers_ipv4_over_ipv6(self) -> None: keys.ID: b"v4", keys.IP: b"\xc0\xa8\x01\x01", # 192.168.1.1 keys.IP6: b"\x00" * 15 + b"\x01", # ::1 - keys.TCP: (9000).to_bytes(2, "big"), + keys.UDP: (9000).to_bytes(2, "big"), }, ) - assert enr.multiaddr() == "/ip4/192.168.1.1/tcp/9000" + assert enr.multiaddr() == "/ip4/192.168.1.1/udp/9000/quic-v1" class TestStringRepresentation: @@ -603,16 +581,6 @@ def test_str_includes_ip(self) -> None: result = str(enr) assert "192.168.1.1" in result - def test_str_includes_tcp_port(self) -> None: - """__str__() includes TCP port when present.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=Uint64(1), - pairs={keys.ID: b"v4", keys.TCP: (9000).to_bytes(2, "big")}, - ) - result = str(enr) - assert "tcp=9000" in result - def test_str_includes_udp_port(self) -> None: """__str__() includes UDP port when present.""" enr = ENR( @@ -675,7 +643,7 @@ def test_has_missing_key(self) -> None: pairs={keys.ID: b"v4"}, ) assert not enr.has(keys.IP) - assert not enr.has(keys.TCP) + assert not enr.has(keys.UDP) assert not enr.has(keys.ETH2) @@ -703,7 +671,6 @@ def test_enr_with_only_required_fields(self) -> None: enr = ENR.from_string(f"enr:{b64_content}") assert enr.is_valid() assert enr.ip4 is None - assert enr.tcp_port is None assert enr.udp_port is None def test_enr_with_ipv6_only(self) -> None: @@ -723,7 +690,7 @@ def test_enr_with_ipv6_only(self) -> None: ipv6_bytes, b"secp256k1", b"\x02" + b"\x00" * 32, - b"tcp", + b"udp", (9000).to_bytes(2, "big"), ] ) @@ -732,14 +699,15 @@ def test_enr_with_ipv6_only(self) -> None: enr = ENR.from_string(f"enr:{b64_content}") assert enr.ip4 is None assert enr.ip6 is not None - assert enr.tcp_port == 9000 - # multiaddr should use IPv6 + assert enr.udp_port == 9000 + # multiaddr should use IPv6 with QUIC multiaddr = enr.multiaddr() assert multiaddr is not None assert "/ip6/" in multiaddr + assert "/quic-v1" in multiaddr - def test_enr_with_both_tcp_and_udp(self) -> None: - """ENR with both TCP and UDP ports parses correctly.""" + def test_enr_with_udp_port(self) -> None: + """ENR with UDP port generates QUIC multiaddr correctly.""" import base64 from lean_spec.types.rlp import encode_rlp @@ -754,8 +722,6 @@ def test_enr_with_both_tcp_and_udp(self) -> None: b"\xc0\xa8\x01\x01", b"secp256k1", b"\x02" + b"\x00" * 32, - b"tcp", - (9000).to_bytes(2, "big"), b"udp", (30303).to_bytes(2, "big"), ] @@ -763,9 +729,8 @@ def test_enr_with_both_tcp_and_udp(self) -> None: b64_content = base64.urlsafe_b64encode(rlp_data).decode("utf-8").rstrip("=") enr = ENR.from_string(f"enr:{b64_content}") - assert enr.tcp_port == 9000 assert enr.udp_port == 30303 - assert enr.multiaddr() == "/ip4/192.168.1.1/tcp/9000" + assert enr.multiaddr() == "/ip4/192.168.1.1/udp/30303/quic-v1" def test_sequence_number_zero(self) -> None: """ENR with sequence number 0 is valid.""" @@ -1267,7 +1232,7 @@ def test_roundtrip_preserves_all_fields(self) -> None: b"\xc0\xa8\x01\x01", b"secp256k1", b"\x02" + b"\x00" * 32, - b"tcp", + b"udp", (9000).to_bytes(2, "big"), ] ) @@ -1278,7 +1243,7 @@ def test_roundtrip_preserves_all_fields(self) -> None: assert enr1.seq == enr2.seq == Uint64(0x42) assert enr1.ip4 == enr2.ip4 == "192.168.1.1" - assert enr1.tcp_port == enr2.tcp_port == 9000 + assert enr1.udp_port == enr2.udp_port == 9000 assert enr1.identity_scheme == enr2.identity_scheme == "v4" def test_to_string_produces_valid_enr_format(self) -> None: @@ -1415,28 +1380,7 @@ def test_node_id_none_without_public_key(self) -> None: class TestIPv6Ports: - """Tests for tcp6_port and udp6_port properties.""" - - def test_tcp6_port_extracts_correctly(self) -> None: - """tcp6_port extracts IPv6-specific TCP port.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=Uint64(1), - pairs={ - keys.ID: b"v4", - keys.TCP6: (9001).to_bytes(2, "big"), - }, - ) - assert enr.tcp6_port == 9001 - - def test_tcp6_port_returns_none_when_missing(self) -> None: - """tcp6_port returns None when tcp6 key is absent.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=Uint64(1), - pairs={keys.ID: b"v4"}, - ) - assert enr.tcp6_port is None + """Tests for udp6_port property.""" def test_udp6_port_extracts_correctly(self) -> None: """udp6_port extracts IPv6-specific UDP port.""" @@ -1459,20 +1403,16 @@ def test_udp6_port_returns_none_when_missing(self) -> None: ) assert enr.udp6_port is None - def test_ipv6_ports_independent_of_ipv4(self) -> None: - """IPv6 ports are independent from IPv4 ports.""" + def test_ipv6_udp_port_independent_of_ipv4(self) -> None: + """IPv6 UDP port is independent from IPv4 UDP port.""" enr = ENR( signature=Bytes64(b"\x00" * 64), seq=Uint64(1), pairs={ keys.ID: b"v4", - keys.TCP: (9000).to_bytes(2, "big"), - keys.TCP6: (9001).to_bytes(2, "big"), keys.UDP: (30303).to_bytes(2, "big"), keys.UDP6: (30304).to_bytes(2, "big"), }, ) - assert enr.tcp_port == 9000 - assert enr.tcp6_port == 9001 assert enr.udp_port == 30303 assert enr.udp6_port == 30304 diff --git a/tests/lean_spec/subspecs/networking/enr/test_keys.py b/tests/lean_spec/subspecs/networking/enr/test_keys.py index f46722bd..7da5f7cc 100644 --- a/tests/lean_spec/subspecs/networking/enr/test_keys.py +++ b/tests/lean_spec/subspecs/networking/enr/test_keys.py @@ -15,9 +15,7 @@ def test_network_keys(self) -> None: """Network keys have correct values.""" assert keys.IP == "ip" assert keys.IP6 == "ip6" - assert keys.TCP == "tcp" assert keys.UDP == "udp" - assert keys.TCP6 == "tcp6" assert keys.UDP6 == "udp6" def test_ethereum_keys(self) -> None: diff --git a/tests/lean_spec/test_cli.py b/tests/lean_spec/test_cli.py index bb4a5f51..52ebc636 100644 --- a/tests/lean_spec/test_cli.py +++ b/tests/lean_spec/test_cli.py @@ -47,8 +47,8 @@ def _sign_enr_content(content_items: list[bytes]) -> bytes: return r.to_bytes(32, "big") + s.to_bytes(32, "big") -def _make_enr_with_tcp(ip_bytes: bytes, tcp_port: int) -> str: - """Create a properly signed ENR string with IPv4 and TCP port.""" +def _make_enr_with_udp(ip_bytes: bytes, udp_port: int) -> str: + """Create a properly signed ENR string with IPv4 and UDP port.""" # Content items (keys must be sorted). content_items: list[bytes] = [ b"\x01", # seq = 1 @@ -58,8 +58,8 @@ def _make_enr_with_tcp(ip_bytes: bytes, tcp_port: int) -> str: ip_bytes, b"secp256k1", _TEST_COMPRESSED_PUBKEY, - b"tcp", - tcp_port.to_bytes(2, "big"), + b"udp", + udp_port.to_bytes(2, "big"), ] signature = _sign_enr_content(content_items) @@ -68,8 +68,8 @@ def _make_enr_with_tcp(ip_bytes: bytes, tcp_port: int) -> str: return f"enr:{b64_content}" -def _make_enr_with_ipv6_tcp(ip6_bytes: bytes, tcp_port: int) -> str: - """Create a properly signed ENR string with IPv6 and TCP port.""" +def _make_enr_with_ipv6_udp(ip6_bytes: bytes, udp_port: int) -> str: + """Create a properly signed ENR string with IPv6 and UDP port.""" content_items: list[bytes] = [ b"\x01", # seq = 1 b"id", @@ -78,8 +78,8 @@ def _make_enr_with_ipv6_tcp(ip6_bytes: bytes, tcp_port: int) -> str: ip6_bytes, b"secp256k1", _TEST_COMPRESSED_PUBKEY, - b"tcp", - tcp_port.to_bytes(2, "big"), + b"udp", + udp_port.to_bytes(2, "big"), ] signature = _sign_enr_content(content_items) @@ -88,8 +88,8 @@ def _make_enr_with_ipv6_tcp(ip6_bytes: bytes, tcp_port: int) -> str: return f"enr:{b64_content}" -def _make_enr_without_tcp(ip_bytes: bytes) -> str: - """Create a properly signed ENR string with IPv4 but no TCP port (UDP only).""" +def _make_enr_without_udp(ip_bytes: bytes) -> str: + """Create a properly signed ENR string with IPv4 but no UDP port.""" content_items: list[bytes] = [ b"\x01", # seq = 1 b"id", @@ -98,8 +98,6 @@ def _make_enr_without_tcp(ip_bytes: bytes) -> str: ip_bytes, b"secp256k1", _TEST_COMPRESSED_PUBKEY, - b"udp", - (30303).to_bytes(2, "big"), # UDP only, no TCP ] signature = _sign_enr_content(content_items) @@ -109,13 +107,13 @@ def _make_enr_without_tcp(ip_bytes: bytes) -> str: # Pre-built test ENRs -ENR_WITH_TCP = _make_enr_with_tcp(b"\xc0\xa8\x01\x01", 9000) # 192.168.1.1:9000 -ENR_WITH_IPV6_TCP = _make_enr_with_ipv6_tcp(b"\x00" * 15 + b"\x01", 9000) # ::1:9000 -ENR_WITHOUT_TCP = _make_enr_without_tcp(b"\xc0\xa8\x01\x01") # 192.168.1.1, UDP only +ENR_WITH_UDP = _make_enr_with_udp(b"\xc0\xa8\x01\x01", 9000) # 192.168.1.1:9000 +ENR_WITH_IPV6_UDP = _make_enr_with_ipv6_udp(b"\x00" * 15 + b"\x01", 9000) # ::1:9000 +ENR_WITHOUT_UDP = _make_enr_without_udp(b"\xc0\xa8\x01\x01") # 192.168.1.1, no UDP -# Valid multiaddr strings -MULTIADDR_IPV4 = "/ip4/127.0.0.1/tcp/9000" -MULTIADDR_IPV6 = "/ip6/::1/tcp/9000" +# Valid multiaddr strings (QUIC format) +MULTIADDR_IPV4 = "/ip4/127.0.0.1/udp/9000/quic-v1" +MULTIADDR_IPV6 = "/ip6/::1/udp/9000/quic-v1" class TestIsEnrString: @@ -131,7 +129,7 @@ def test_enr_prefix_minimal(self) -> None: def test_enr_with_valid_content(self) -> None: """Full valid ENR string returns True.""" - assert is_enr_string(ENR_WITH_TCP) is True + assert is_enr_string(ENR_WITH_UDP) is True def test_multiaddr_not_detected(self) -> None: """Multiaddr string returns False.""" @@ -174,22 +172,22 @@ def test_resolve_arbitrary_multiaddr_unchanged(self) -> None: arbitrary = "/some/arbitrary/path" assert resolve_bootnode(arbitrary) == arbitrary - def test_resolve_valid_enr_with_tcp(self) -> None: - """ENR with IPv4+TCP extracts multiaddr correctly.""" - result = resolve_bootnode(ENR_WITH_TCP) - assert result == "/ip4/192.168.1.1/tcp/9000" + def test_resolve_valid_enr_with_udp(self) -> None: + """ENR with IPv4+UDP extracts QUIC multiaddr correctly.""" + result = resolve_bootnode(ENR_WITH_UDP) + assert result == "/ip4/192.168.1.1/udp/9000/quic-v1" def test_resolve_enr_ipv6(self) -> None: - """ENR with IPv6+TCP extracts multiaddr correctly.""" - result = resolve_bootnode(ENR_WITH_IPV6_TCP) + """ENR with IPv6+UDP extracts QUIC multiaddr correctly.""" + result = resolve_bootnode(ENR_WITH_IPV6_UDP) # IPv6 loopback ::1 formatted as full hex assert "/ip6/" in result - assert "/tcp/9000" in result + assert "/udp/9000/quic-v1" in result - def test_resolve_enr_without_tcp_raises(self) -> None: - """ENR without TCP port raises ValueError.""" - with pytest.raises(ValueError, match=r"no TCP connection info"): - resolve_bootnode(ENR_WITHOUT_TCP) + def test_resolve_enr_without_udp_raises(self) -> None: + """ENR without UDP port raises ValueError.""" + with pytest.raises(ValueError, match=r"no UDP connection info"): + resolve_bootnode(ENR_WITHOUT_UDP) def test_resolve_invalid_enr_raises(self) -> None: """Malformed ENR raises ValueError.""" @@ -209,19 +207,19 @@ def test_resolve_enr_prefix_only_raises(self) -> None: def test_resolve_enr_with_different_ports(self) -> None: """ENR resolution handles various port numbers.""" # Port 30303 - enr_30303 = _make_enr_with_tcp(b"\x7f\x00\x00\x01", 30303) + enr_30303 = _make_enr_with_udp(b"\x7f\x00\x00\x01", 30303) result = resolve_bootnode(enr_30303) - assert result == "/ip4/127.0.0.1/tcp/30303" + assert result == "/ip4/127.0.0.1/udp/30303/quic-v1" # Port 1 (minimum valid) - enr_1 = _make_enr_with_tcp(b"\x7f\x00\x00\x01", 1) + enr_1 = _make_enr_with_udp(b"\x7f\x00\x00\x01", 1) result = resolve_bootnode(enr_1) - assert result == "/ip4/127.0.0.1/tcp/1" + assert result == "/ip4/127.0.0.1/udp/1/quic-v1" # Port 65535 (maximum) - enr_max = _make_enr_with_tcp(b"\x7f\x00\x00\x01", 65535) + enr_max = _make_enr_with_udp(b"\x7f\x00\x00\x01", 65535) result = resolve_bootnode(enr_max) - assert result == "/ip4/127.0.0.1/tcp/65535" + assert result == "/ip4/127.0.0.1/udp/65535/quic-v1" def test_resolve_enr_with_different_ips(self) -> None: """ENR resolution handles various IPv4 addresses.""" @@ -231,9 +229,9 @@ def test_resolve_enr_with_different_ips(self) -> None: (b"\x0a\x00\x00\x01", "10.0.0.1"), ] for ip_bytes, expected_ip in test_cases: - enr = _make_enr_with_tcp(ip_bytes, 9000) + enr = _make_enr_with_udp(ip_bytes, 9000) result = resolve_bootnode(enr) - assert result == f"/ip4/{expected_ip}/tcp/9000" + assert result == f"/ip4/{expected_ip}/udp/9000/quic-v1" class TestMixedBootnodes: @@ -243,22 +241,22 @@ def test_mixed_bootnodes_list(self) -> None: """Process a list containing both ENR and multiaddr.""" bootnodes = [ MULTIADDR_IPV4, - ENR_WITH_TCP, - "/ip4/10.0.0.1/tcp/8000", + ENR_WITH_UDP, + "/ip4/10.0.0.1/udp/8000/quic-v1", ] resolved = [resolve_bootnode(b) for b in bootnodes] assert resolved[0] == MULTIADDR_IPV4 - assert resolved[1] == "/ip4/192.168.1.1/tcp/9000" - assert resolved[2] == "/ip4/10.0.0.1/tcp/8000" + assert resolved[1] == "/ip4/192.168.1.1/udp/9000/quic-v1" + assert resolved[2] == "/ip4/10.0.0.1/udp/8000/quic-v1" def test_filter_invalid_enrs(self) -> None: """Demonstrate filtering out invalid ENRs from a bootnode list.""" bootnodes = [ MULTIADDR_IPV4, - ENR_WITHOUT_TCP, # Invalid - no TCP - ENR_WITH_TCP, + ENR_WITHOUT_UDP, # Invalid - no UDP + ENR_WITH_UDP, ] resolved = [] @@ -270,7 +268,7 @@ def test_filter_invalid_enrs(self) -> None: assert len(resolved) == 2 assert resolved[0] == MULTIADDR_IPV4 - assert resolved[1] == "/ip4/192.168.1.1/tcp/9000" + assert resolved[1] == "/ip4/192.168.1.1/udp/9000/quic-v1" # =============================================================================