def test_equality(identity_scheme_registry): base_kwargs = { "sequence_number": 0, "kv_pairs": { b"id": b"mock", b"key1": b"value1", b"key2": b"value2", }, "signature": b"signature", "identity_scheme_registry": identity_scheme_registry, } base_enr = ENR(**base_kwargs) equal_enr = ENR(**base_kwargs) enr_different_sequence_number = ENR( **assoc(base_kwargs, "sequence_number", 1)) enr_different_kv_pairs = ENR( **assoc_in(base_kwargs, ("kv_pairs", b"key1"), b"value2"), ) enr_different_signature = ENR( **assoc(base_kwargs, "signature", b"different-signature")) assert base_enr == base_enr assert equal_enr == base_enr assert enr_different_sequence_number != base_enr assert enr_different_kv_pairs != base_enr assert enr_different_signature != base_enr
def test_auth_header_preparation(tag, auth_tag, id_nonce, initiator_key, auth_response_key, ephemeral_public_key): enr = ENR( sequence_number=1, signature=b"", kv_pairs={ b"id": b"v4", b"secp256k1": b"\x02" * 33, } ) message = PingMessage( request_id=5, enr_seq=enr.sequence_number, ) id_nonce_signature = b"\x00" * 32 packet = AuthHeaderPacket.prepare( tag=tag, auth_tag=auth_tag, id_nonce=id_nonce, message=message, initiator_key=initiator_key, id_nonce_signature=id_nonce_signature, auth_response_key=auth_response_key, enr=enr, ephemeral_public_key=ephemeral_public_key ) assert packet.tag == tag assert packet.auth_header.auth_tag == auth_tag assert packet.auth_header.id_nonce == id_nonce assert packet.auth_header.auth_scheme_name == AUTH_SCHEME_NAME assert packet.auth_header.ephemeral_public_key == ephemeral_public_key decrypted_auth_response = aesgcm_decrypt( key=auth_response_key, nonce=ZERO_NONCE, cipher_text=packet.auth_header.encrypted_auth_response, authenticated_data=b"", ) decoded_auth_response = rlp.decode(decrypted_auth_response) assert is_list_like(decoded_auth_response) and len(decoded_auth_response) == 3 assert decoded_auth_response[0] == int_to_big_endian(AUTH_RESPONSE_VERSION) assert decoded_auth_response[1] == id_nonce_signature assert ENR.deserialize(decoded_auth_response[2]) == enr decrypted_message = aesgcm_decrypt( key=initiator_key, nonce=auth_tag, cipher_text=packet.encrypted_message, authenticated_data=tag, ) assert decrypted_message[0] == message.message_type assert rlp.decode(decrypted_message[1:], PingMessage) == message
def test_mapping_interface(identity_scheme_registry): kv_pairs = { b"id": b"mock", b"key1": b"value1", b"key2": b"value2", } enr = ENR( signature=b"", sequence_number=0, kv_pairs=kv_pairs, identity_scheme_registry=identity_scheme_registry, ) for key, value in kv_pairs.items(): assert key in enr assert enr[key] == value assert enr.get(key) == value not_a_key = b"key3" assert not_a_key not in kv_pairs assert not_a_key not in enr enr.get(not_a_key) is None assert enr.get(not_a_key, b"default") == b"default" assert tuple(enr.keys()) == tuple(kv_pairs.keys()) assert tuple(enr.values()) == tuple(kv_pairs.values()) assert tuple(enr.items()) == tuple(kv_pairs.items()) assert len(enr) == len(kv_pairs) assert tuple(iter(enr)) == tuple(iter(kv_pairs))
def test_signature_scheme_selection(mock_identity_scheme, identity_scheme_registry): mock_enr = ENR(0, {b"id": b"mock"}, b"", identity_scheme_registry) assert mock_enr.identity_scheme is mock_identity_scheme v4_enr = ENR(0, { b"id": b"v4", b"secp256k1": b"\x02" * 33 }, b"", identity_scheme_registry) assert v4_enr.identity_scheme is V4IdentityScheme with pytest.raises(ValidationError): ENR(0, {b"id": b"other"}, b"", identity_scheme_registry)
def test_extract_forkid(): enr = ENR.from_repr( "enr:-Jq4QO5zEyIBU5lSa9iaen0A2xUB5_IVrCi1DbyASTTnLV5RJan6aGPr8kU0p0MYKU5YezZgdSUE" "-GOBEio6Ultyf1Aog2V0aMrJhGN2AZCDGfCggmlkgnY0gmlwhF4_wLuJc2VjcDI1NmsxoQOt7cA_B_Kg" "nQ5RmwyA6ji8M1Y0jfINItRGbOOwy7XgbIN0Y3CCdl-DdWRwgnZf") assert extract_forkid(enr) == ForkID(hash=to_bytes(hexstr='0x63760190'), next=1700000)
def enr(): return ENR(sequence_number=1, signature=b"", kv_pairs={ b"id": b"v4", b"secp256k1": PrivateKey(b"\x01" * 32).public_key.to_compressed_bytes(), })
def test_real_life_test_vector(): enr = ENR.from_repr(REAL_LIFE_TEST_DATA["repr"]) assert enr.sequence_number == REAL_LIFE_TEST_DATA["sequence_number"] assert enr.public_key == REAL_LIFE_TEST_DATA["public_key"] assert enr.node_id == REAL_LIFE_TEST_DATA["node_id"] assert enr.identity_scheme is REAL_LIFE_TEST_DATA["identity_scheme"] assert dict(enr) == REAL_LIFE_TEST_DATA["kv_pairs"] assert repr(enr) == REAL_LIFE_TEST_DATA["repr"]
def decrypt_auth_response( self, auth_response_key: AES128Key) -> Tuple[bytes, Optional[ENR]]: """Extract id nonce signature and optional ENR from auth header packet.""" plain_text = aesgcm_decrypt( key=auth_response_key, nonce=ZERO_NONCE, cipher_text=self.auth_header.encrypted_auth_response, authenticated_data=b"", ) try: decoded_rlp = rlp.decode(plain_text) except DecodingError: raise ValidationError( f"Auth response does not contain valid RLP: {encode_hex(plain_text)}" ) if not is_list_like(decoded_rlp): raise ValidationError( f"Auth response contains bytes instead of list: {encode_hex(decoded_rlp)}" ) if len(decoded_rlp) != 3: raise ValidationError( f"Auth response is a list of {len(decoded_rlp)} instead of three elements" ) version_bytes, id_nonce_signature, serialized_enr = decoded_rlp if not is_bytes(version_bytes): raise ValidationError( f"Version is a list instead of big endian encoded integer: {version_bytes}" ) version_int = big_endian_to_int(version_bytes) if version_int != AUTH_RESPONSE_VERSION: raise ValidationError( f"Expected auth response version {AUTH_RESPONSE_VERSION}, but got {version_int}" ) if not is_bytes(id_nonce_signature): raise ValidationError( f"Id nonce signature is a list instead of bytes: {id_nonce_signature}" ) if not is_list_like(serialized_enr): raise ValidationError( f"ENR is bytes instead of list: {encode_hex(serialized_enr)}") if len(serialized_enr) == 0: enr = None else: try: enr = ENR.deserialize(serialized_enr) except DeserializationError as error: raise ValidationError( "ENR in auth response is not properly encoded") from error return id_nonce_signature, enr
def test_inititialization(identity_scheme_registry): valid_sequence_number = 0 valid_kv_pairs = {b"id": b"mock"} valid_signature = b"" # signature is not validated during initialization assert UnsignedENR( sequence_number=valid_sequence_number, kv_pairs=valid_kv_pairs, identity_scheme_registry=identity_scheme_registry, ) assert ENR( sequence_number=valid_sequence_number, kv_pairs=valid_kv_pairs, signature=valid_signature, identity_scheme_registry=identity_scheme_registry, ) with pytest.raises(ValidationError): UnsignedENR( sequence_number=valid_sequence_number, kv_pairs={b"no-id": b""}, identity_scheme_registry=identity_scheme_registry, ) with pytest.raises(ValidationError): ENR( sequence_number=valid_sequence_number, kv_pairs={b"no-id": b""}, signature=valid_signature, identity_scheme_registry=identity_scheme_registry, ) with pytest.raises(ValidationError): UnsignedENR( sequence_number=-1, kv_pairs=valid_kv_pairs, identity_scheme_registry=identity_scheme_registry, ) with pytest.raises(ValidationError): ENR( sequence_number=-1, kv_pairs=valid_kv_pairs, signature=valid_signature, identity_scheme_registry=identity_scheme_registry, )
def test_repr(mock_identity_scheme, identity_scheme_registry): unsigned_enr = UnsignedENR(0, {b"id": b"mock"}, identity_scheme_registry) enr = unsigned_enr.to_signed_enr(b"\x00" * 32) base64_encoded_enr = base64.urlsafe_b64encode(rlp.encode(enr)) represented_enr = repr(enr) assert represented_enr.startswith("enr:") assert base64_encoded_enr.rstrip(b"=").decode() == represented_enr[4:] assert ENR.from_repr(represented_enr, identity_scheme_registry) == enr
def test_enr_v4_compat_signature_validation(): private_key = PrivateKey(b"\x11" * 32) enr = ENR(0, { b"id": b"v4-compat", b"secp256k1": private_key.public_key.to_compressed_bytes(), b"key1": b"value1", }, signature=b'') V4CompatIdentityScheme.validate_enr_signature(enr)
async def test_enr_response_handler_does_not_crash_on_invalid_responses(): discovery = MockDiscoveryService([]) token = b'' invalid_enr = b'garbage' payload = [token, invalid_enr] await discovery.recv_enr_response(discovery.this_node, payload, b'') enr = ENRFactory() enr._kv_pairs.pop(b'secp256k1') with pytest.raises(ValidationError): ENR.deserialize(ENR.serialize(enr)) payload = [token, ENR.serialize(enr)] await discovery.recv_enr_response(discovery.this_node, payload, b'') enr = ENRFactory() enr._signature = b'garbage' with pytest.raises(eth_keys.exceptions.ValidationError): enr.validate_signature() payload = [token, ENR.serialize(enr)] await discovery.recv_enr_response(discovery.this_node, payload, b'')
def create_stub_enr(pubkey: datatypes.PublicKey, address: AddressAPI) -> ENR: return ENR( 0, { IDENTITY_SCHEME_ENR_KEY: V4CompatIdentityScheme.id, V4CompatIdentityScheme.public_key_enr_key: pubkey.to_compressed_bytes(), IP_V4_ADDRESS_ENR_KEY: address.ip_packed, UDP_PORT_ENR_KEY: address.udp_port, TCP_PORT_ENR_KEY: address.tcp_port, }, signature=b'')
def test_signature_validation(mock_identity_scheme, identity_scheme_registry): unsigned_enr = UnsignedENR(0, {b"id": b"mock"}, identity_scheme_registry) private_key = b"\x00" * 32 enr = unsigned_enr.to_signed_enr(private_key) enr.validate_signature() invalid_signature = b"\xff" * 64 invalid_enr = ENR(enr.sequence_number, dict(enr), invalid_signature, identity_scheme_registry=identity_scheme_registry) with pytest.raises(ValidationError): invalid_enr.validate_signature() with pytest.raises(ValidationError): ENR( 0, {b"id": b"unknown"}, b"", identity_scheme_registry=identity_scheme_registry, )
def enrToMultiAddress(_enr): knode = KNode.from_enr_repr(_enr) return { "enode": knode.uri(), "enrdata": { "address": knode.address, "pubkey": knode.pubkey, "id": knode.id }, "enritems": ENR.from_repr(_enr).items(), "multiaddr": Handler.enodeToMultiAddress(knode.uri()) }
def test_enr_signature_validation(): private_key = PrivateKey(b"\x11" * 32) unsigned_enr = UnsignedENR( 0, { b"id": b"v4", b"secp256k1": private_key.public_key.to_compressed_bytes(), b"key1": b"value1", }) enr = unsigned_enr.to_signed_enr(private_key.to_bytes()) V4IdentityScheme.validate_enr_signature(enr) forged_enr = ENR(enr.sequence_number, dict(enr), b"\x00" * 64) with pytest.raises(ValidationError): V4IdentityScheme.validate_enr_signature(forged_enr)
def test_official_test_vector(): enr = ENR.from_repr( OFFICIAL_TEST_DATA["repr"]) # use default identity scheme registry assert enr.sequence_number == OFFICIAL_TEST_DATA["sequence_number"] assert dict(enr) == OFFICIAL_TEST_DATA["kv_pairs"] assert enr.public_key == OFFICIAL_TEST_DATA["public_key"] assert enr.node_id == OFFICIAL_TEST_DATA["node_id"] assert enr.identity_scheme is OFFICIAL_TEST_DATA["identity_scheme"] assert repr(enr) == OFFICIAL_TEST_DATA["repr"] unsigned_enr = UnsignedENR(enr.sequence_number, dict(enr)) reconstructed_enr = unsigned_enr.to_signed_enr( OFFICIAL_TEST_DATA["private_key"]) assert reconstructed_enr == enr
def _init(self, enr: ENR) -> None: try: ip = enr[IP_V4_ADDRESS_ENR_KEY] udp_port = enr[UDP_PORT_ENR_KEY] except KeyError: self._address = None else: tcp_port = enr.get(TCP_PORT_ENR_KEY, udp_port) self._address = Address(ip, udp_port, tcp_port) # FIXME: ENRs may use different pubkey formats and this would break, so instead of storing # a PublicKey with a certain format here we should simply use the APIs in the # ENR.identity_scheme for the crypto related operations. self._pubkey = keys.PublicKey.from_compressed_bytes(enr.public_key) self._id = NodeID(keccak(self.pubkey.to_bytes())) self._id_int = big_endian_to_int(self.id) self._enr = enr
def test_serialization_roundtrip(identity_scheme_registry): original_enr = ENR( sequence_number=0, kv_pairs={ b"id": b"mock", b"key2": b"value2", # wrong order so that serialization is forced to fix this b"key1": b"value1", }, signature=b"", identity_scheme_registry=identity_scheme_registry, ) encoded = rlp.encode(original_enr) recovered_enr = rlp.decode( encoded, ENR, identity_scheme_registry=identity_scheme_registry, ) assert recovered_enr == original_enr
async def do_run(self, event_bus: EndpointAPI) -> None: boot_info = self._boot_info identity_scheme_registry = default_identity_scheme_registry message_type_registry = default_message_type_registry nodedb_dir = get_nodedb_dir(boot_info) nodedb_dir.mkdir(exist_ok=True) node_db = NodeDB(default_identity_scheme_registry, LevelDB(nodedb_dir)) local_private_key = get_local_private_key(boot_info) local_enr = await get_local_enr(boot_info, node_db, local_private_key) local_node_id = local_enr.node_id routing_table = KademliaRoutingTable(local_node_id, NUM_ROUTING_TABLE_BUCKETS) node_db.set_enr(local_enr) for enr_repr in boot_info.args.discovery_boot_enrs or (): enr = ENR.from_repr(enr_repr) node_db.set_enr(enr) routing_table.update(enr.node_id) port = boot_info.args.discovery_port socket = trio.socket.socket(family=trio.socket.AF_INET, type=trio.socket.SOCK_DGRAM) outgoing_datagram_channels = trio.open_memory_channel[ OutgoingDatagram](0) incoming_datagram_channels = trio.open_memory_channel[ IncomingDatagram](0) outgoing_packet_channels = trio.open_memory_channel[OutgoingPacket](0) incoming_packet_channels = trio.open_memory_channel[IncomingPacket](0) outgoing_message_channels = trio.open_memory_channel[OutgoingMessage]( 0) incoming_message_channels = trio.open_memory_channel[IncomingMessage]( 0) endpoint_vote_channels = trio.open_memory_channel[EndpointVote](0) # types ignored due to https://github.com/ethereum/async-service/issues/5 datagram_sender = DatagramSender( # type: ignore outgoing_datagram_channels[1], socket) datagram_receiver = DatagramReceiver( # type: ignore socket, incoming_datagram_channels[0]) packet_encoder = PacketEncoder( # type: ignore outgoing_packet_channels[1], outgoing_datagram_channels[0]) packet_decoder = PacketDecoder( # type: ignore incoming_datagram_channels[1], incoming_packet_channels[0]) packer = Packer( local_private_key=local_private_key.to_bytes(), local_node_id=local_node_id, node_db=node_db, message_type_registry=message_type_registry, incoming_packet_receive_channel=incoming_packet_channels[1], incoming_message_send_channel=incoming_message_channels[0], outgoing_message_receive_channel=outgoing_message_channels[1], outgoing_packet_send_channel=outgoing_packet_channels[0], ) message_dispatcher = MessageDispatcher( node_db=node_db, incoming_message_receive_channel=incoming_message_channels[1], outgoing_message_send_channel=outgoing_message_channels[0], ) endpoint_tracker = EndpointTracker( local_private_key=local_private_key.to_bytes(), local_node_id=local_node_id, node_db=node_db, identity_scheme_registry=identity_scheme_registry, vote_receive_channel=endpoint_vote_channels[1], ) routing_table_manager = RoutingTableManager( local_node_id=local_node_id, routing_table=routing_table, message_dispatcher=message_dispatcher, node_db=node_db, outgoing_message_send_channel=outgoing_message_channels[0], endpoint_vote_send_channel=endpoint_vote_channels[0], ) logger.info(f"Starting discovery, listening on port {port}") logger.info(f"Local Node ID: {encode_hex(local_enr.node_id)}") logger.info(f"Local ENR: {local_enr}") services = ( datagram_sender, datagram_receiver, packet_encoder, packet_decoder, packer, message_dispatcher, endpoint_tracker, routing_table_manager, ) await socket.bind(("0.0.0.0", port)) with socket: async with trio.open_nursery() as nursery: for service in services: nursery.start_soon(async_service.TrioManager.run_service, service)
"request_id": 0x01, "total": 0x01, "enrs": [], }, decode_hex("0x04c30101c0"), ], [ NodesMessage, { "request_id": 0x01, "total": 0x01, "enrs": [ ENR.from_repr( "enr:-HW4QBzimRxkmT18hMKaAL3IcZF1UcfTMPyi3Q1pxwZZbcZVRI8DC5infUAB_UauARLOJtYTxa" "agKoGmIjzQxO2qUygBgmlkgnY0iXNlY3AyNTZrMaEDymNMrg1JrLQB2KTGtv6MVbcNEVv0AHacwUAP" "MljNMTg"), ENR.from_repr( "enr:-HW4QNfxw543Ypf4HXKXdYxkyzfcxcO-6p9X986WldfVpnVTQX1xlTnWrktEWUbeTZnmgOuAY_" "KUhbVV1Ft98WoYUBMBgmlkgnY0iXNlY3AyNTZrMaEDDiy3QkHAxPyOgWbxp5oF1bDdlYE6dLCUUp8x" "fVw50jU"), ] }, decode_hex( "0x04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc6" "55448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa532801826964827634897365" "63703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875" "b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b44" "5946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e" "2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235"), ],
def from_enr_repr(cls: Type[TNode], uri: str) -> TNode: return cls(ENR.from_repr(uri))
def __setstate__(self, state: Dict[Any, Any]) -> None: self._init(ENR.from_repr(state.pop('enr')))