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_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_auth_header_preparation(tag, auth_tag, initiator_key, auth_response_key, ephemeral_pubkey): enr = ENR( sequence_number=1, signature=b"", kv_pairs={ b"id": b"v4", } ) 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, message=message, initiator_key=initiator_key, id_nonce_signature=id_nonce_signature, auth_response_key=auth_response_key, enr=enr, ephemeral_pubkey=ephemeral_pubkey ) assert packet.tag == tag assert packet.auth_header.auth_tag == auth_tag assert packet.auth_header.auth_scheme_name == AUTH_SCHEME_NAME assert packet.auth_header.ephemeral_pubkey == ephemeral_pubkey decrypted_auth_response = aesgcm_decrypt( key=auth_response_key, nonce=ZERO_NONCE, cipher_text=packet.auth_header.encrypted_auth_response, authenticated_data=tag, ) decoded_auth_response = rlp.decode(decrypted_auth_response) assert is_list_like(decoded_auth_response) and len(decoded_auth_response) == 2 assert decoded_auth_response[0] == id_nonce_signature assert ENR.deserialize(decoded_auth_response[1]) == enr decrypted_message = aesgcm_decrypt( key=initiator_key, nonce=auth_tag, cipher_text=packet.encrypted_message, authenticated_data=b"".join(( tag, rlp.encode(packet.auth_header), )) ) 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))
async def recv_enr_response(self, node: NodeAPI, payload: Sequence[Any], msg_hash: Hash32) -> None: # The enr_response payload should have at least two elements: request_hash, enr. if len(payload) < 2: self.logger.warning( 'Ignoring ENR_RESPONSE msg with invalid payload: %s', payload) return token, serialized_enr = payload[:2] try: enr = ENR.deserialize(serialized_enr) except DeserializationError as error: raise ValidationError( "ENR in response is not properly encoded") from error try: channel = self.enr_response_channels.get_channel(node) except KeyError: self.logger.debug("Unexpected ENR_RESPONSE from %s", node) return enr.validate_signature() self.logger.debug( "Received ENR %s (%s) with expected response token: %s", enr, enr.items(), encode_hex(token)) try: await channel.send((enr, token)) except trio.BrokenResourceError: # This means the receiver has already closed, probably because it timed out. pass
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)
async def test_file_db_saves_enrs(file_db_dir, file_db): enr = ENRFactory() await file_db.insert(enr) filename = get_enr_filename(enr) assert (file_db_dir / filename).exists() assert (file_db_dir / filename).is_file() assert ENR.from_repr((file_db_dir / filename).read_text()) == enr
def enr(): return ENR( sequence_number=1, signature=b"", kv_pairs={ b"id": b"v4", } )
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 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 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)
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 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 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 _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 from_pubkey_and_addr(cls: Type[TNode], pubkey: datatypes.PublicKey, address: AddressAPI) -> TNode: enr = 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'') return cls(enr)
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=self.tag, ) try: decoded_rlp = rlp.decode(plain_text) except DecodingError as error: 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) != 2: raise ValidationError( f"Auth response is a list of {len(decoded_rlp)} instead of two elements" ) id_nonce_signature, serialized_enr = decoded_rlp 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_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 recv_enr_request(self, node: NodeAPI, payload: Sequence[Any], msg_hash: Hash32) -> None: # The enr_request payload should have at least one element: expiration. if len(payload) < 1: self.logger.warning( 'Ignoring ENR_REQUEST msg with invalid payload: %s', payload) return expiration = payload[0] if self._is_msg_expired(expiration): return # XXX: Maybe reconsider this and accept all ENR requests until we have a persistent # routing store of nodes we've bonded with? Otherwise if a node request our ENR across a # restart, we'll not reply to them. if node not in self.routing: self.logger.info('Ignoring ENR_REQUEST from unknown node %s', node) return enr = await self.get_local_enr() self.logger.debug("Sending local ENR to %s: %s", node, enr) payload = (msg_hash, ENR.serialize(enr)) self.send(node, CMD_ENR_RESPONSE, payload)
def load_enr_file(self, path: pathlib.Path) -> ENR: enr_base64 = path.read_text() enr = ENR.from_repr(enr_base64, self.identity_scheme_registry) return enr
def __setstate__(self, state: Dict[Any, Any]) -> None: self._init(ENR.from_repr(state.pop('enr')))
"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 extract_forkid(enr: ENR) -> ForkID: eth_cap = enr.get(b'eth', None) if eth_cap is not None: [forkid] = rlp.sedes.List([ForkID]).deserialize(eth_cap) return forkid return None
async def do_run(cls, boot_info: BootInfo, event_bus: EndpointAPI) -> None: 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}") await socket.bind(("0.0.0.0", port)) services = ( datagram_sender, datagram_receiver, packet_encoder, packet_decoder, packer, message_dispatcher, endpoint_tracker, routing_table_manager, ) async with trio.open_nursery() as nursery: for service in services: nursery.start_soon(async_service.TrioManager.run_service, service)
def from_enr_repr(cls: Type[TNode], uri: str) -> TNode: return cls(ENR.from_repr(uri))