def validate_and_extract_destination( value: Any) -> Tuple[NodeID, Optional[Endpoint]]: node_id: NodeID endpoint: Optional[Endpoint] if is_hex_node_id(value): node_id = NodeID(decode_hex(value)) endpoint = None elif value.startswith("enode://"): raw_node_id, _, raw_endpoint = value[8:].partition("@") validate_hex_node_id(raw_node_id) validate_endpoint(raw_endpoint) node_id = NodeID(decode_hex(raw_node_id)) raw_ip_address, _, raw_port = raw_endpoint.partition(":") ip_address = ipaddress.ip_address(raw_ip_address) port = int(raw_port) endpoint = Endpoint(ip_address.packed, port) elif value.startswith("enr:"): enr = ENR.from_repr(value) node_id = enr.node_id endpoint = Endpoint.from_enr(enr) else: raise RPCError(f"Unrecognized node identifier: {value}") return node_id, endpoint
async def _manage_routing_table(self) -> None: # First load all the bootnode ENRs into our database for enr in self._bootnodes: try: self.enr_db.set_enr(enr) except OldSequenceNumber: pass # Now repeatedly try to bond with each bootnode until one succeeds. async with trio.open_nursery() as nursery: while self.manager.is_running: for enr in self._bootnodes: if enr.node_id == self.local_node_id: continue endpoint = Endpoint.from_enr(enr) nursery.start_soon(self._bond, enr.node_id, endpoint) with trio.move_on_after(10): await self._routing_table_ready.wait() break # TODO: Need better logic here for more quickly populating the # routing table. Should start off aggressively filling in the # table, only backing off once the table contains some minimum # number of records **or** searching for new records fails to find # new nodes. Maybe use a TokenBucket async for _ in every(30): async with trio.open_nursery() as nursery: target_node_id = NodeID(secrets.token_bytes(32)) found_enrs = await self.recursive_find_nodes(target_node_id) for enr in found_enrs: endpoint = Endpoint.from_enr(enr) nursery.start_soon(self._bond, enr.node_id, endpoint)
async def endpoint_for_node_id(self, node_id: NodeID) -> Endpoint: try: enr = self.enr_db.get_enr(node_id) except KeyError: enr = await self.lookup_enr(node_id) return Endpoint.from_enr(enr)
async def lookup_enr( self, node_id: NodeID, *, enr_seq: int = 0, endpoint: Optional[Endpoint] = None ) -> ENRAPI: if node_id == self.local_node_id: raise Exception(f"Cannot lookup local ENR: node_id={node_id.hex()}") try: enr = self.enr_db.get_enr(node_id) except KeyError: if endpoint is None: # Try to use a recursive network lookup to find the desired # node. async with self.recursive_find_nodes(node_id) as enr_aiter: async for found_enr in enr_aiter: if found_enr.node_id == node_id: endpoint = Endpoint.from_enr(found_enr) break else: # we weren't given an endpoint and we don't have an enr which would give # us an endpoint, there's no way to reach this node. raise KeyError(f"Could not find ENR: node_id={node_id.hex()}") else: if enr.sequence_number >= enr_seq: return enr enr = await self._fetch_enr(node_id, endpoint=endpoint) try: self.enr_db.set_enr(enr) except OldSequenceNumber: pass return enr
def _endpoint_for_enr(self, enr: ENRAPI) -> Endpoint: try: ip_address = enr[IP_V4_ADDRESS_ENR_KEY] port = enr[UDP_PORT_ENR_KEY] except KeyError: raise Exception("Missing endpoint address information: ") return Endpoint(ip_address, port)
def extract_params( self, request: RPCRequest) -> Tuple[NodeID, Optional[Endpoint]]: try: raw_params = request["params"] except KeyError as err: raise RPCError(f"Missiing call params: {err}") if len(raw_params) != 1: raise RPCError(f"`ddht_ping` endpoint expects a single parameter: " f"Got {len(raw_params)} params: {raw_params}") value = raw_params[0] node_id: NodeID endpoint: Optional[Endpoint] if is_hex_node_id(value): node_id = NodeID(decode_hex(value)) endpoint = None elif value.startswith("enode://"): raw_node_id, _, raw_endpoint = value[8:].partition("@") validate_hex_node_id(raw_node_id) validate_endpoint(raw_endpoint) node_id = NodeID(decode_hex(raw_node_id)) raw_ip_address, _, raw_port = raw_endpoint.partition(":") ip_address = ipaddress.ip_address(raw_ip_address) port = int(raw_port) endpoint = Endpoint(ip_address.packed, port) elif value.startswith("enr:"): enr = ENR.from_repr(value) node_id = enr.node_id endpoint = Endpoint.from_enr(enr) else: raise RPCError(f"Unrecognized node identifier: {value}") return node_id, endpoint
async def test_datagram_sender(socket_pair): sending_socket, receiving_socket = socket_pair receiver_endpoint = receiving_socket.getsockname() sender_endpoint = sending_socket.getsockname() send_channel, receive_channel = trio.open_memory_channel(1) async with background_trio_service( DatagramSender(receive_channel, sending_socket)): outbound_datagram = OutboundDatagram( b"some packet", Endpoint(inet_aton(receiver_endpoint[0]), receiver_endpoint[1]), ) await send_channel.send(outbound_datagram) with trio.fail_after(0.5): data, sender = await receiving_socket.recvfrom(1024) assert data == outbound_datagram.datagram assert sender == sender_endpoint
async def ping(self, node_id: NodeID) -> None: local_enr = self.get_local_enr() ping = PingMessage( request_id=self.message_dispatcher.get_free_request_id(node_id), enr_seq=local_enr.sequence_number, ) try: with trio.fail_after(REQUEST_RESPONSE_TIMEOUT): inbound_message = await self.message_dispatcher.request(node_id, ping) except ValueError as value_error: self.logger.debug( "Failed to send ping to %s: %s", encode_hex(node_id), value_error ) except trio.TooSlowError: self.logger.debug("Ping to %s timed out", encode_hex(node_id)) else: if not isinstance(inbound_message.message, PongMessage): self.logger.debug( "Peer %s responded to Ping with %s instead of Pong", encode_hex(node_id), inbound_message.message.__class__.__name__, ) else: self.logger.debug("Received Pong from %s", encode_hex(node_id)) self.update_routing_table(node_id) pong = inbound_message.message local_endpoint = Endpoint( ip_address=pong.packet_ip, port=pong.packet_port ) endpoint_vote = EndpointVote( endpoint=local_endpoint, node_id=node_id, timestamp=time.monotonic() ) await self.endpoint_vote_send_channel.send(endpoint_vote) await self.maybe_request_remote_enr(inbound_message)
async def get_endpoint_from_enr_db(self, receiver_node_id: NodeID) -> Endpoint: try: enr = self.enr_db.get_enr(receiver_node_id) except KeyError: raise ValueError( f"No ENR for peer {encode_hex(receiver_node_id)} known") try: ip_address = enr[IP_V4_ADDRESS_ENR_KEY] except KeyError: raise ValueError( f"ENR for peer {encode_hex(receiver_node_id)} does not contain an IP address" ) try: udp_port = enr[UDP_PORT_ENR_KEY] except KeyError: raise ValueError( f"ENR for peer {encode_hex(receiver_node_id)} does not contain a UDP port" ) return Endpoint(ip_address, udp_port)
async def DatagramReceiver( manager: ManagerAPI, sock: SocketType, inbound_datagram_send_channel: SendChannel[InboundDatagram], ) -> None: """Read datagrams from a socket and send them to a channel.""" logger = logging.getLogger("ddht.DatagramReceiver") async with inbound_datagram_send_channel: while manager.is_running: datagram, ( ip_address, port) = await sock.recvfrom(DISCOVERY_DATAGRAM_BUFFER_SIZE) endpoint = Endpoint(inet_aton(ip_address), port) logger.debug("Received %d bytes from %s", len(datagram), endpoint) inbound_datagram = InboundDatagram(datagram, endpoint) try: await inbound_datagram_send_channel.send(inbound_datagram) except trio.BrokenResourceError: logger.debug( "DatagramReceiver exiting due to `trio.BrokenResourceError`" ) manager.cancel() return
async def run(self) -> None: self.manager.run_daemon_child_service(Canary()) identity_scheme_registry = default_identity_scheme_registry message_type_registry = v51_registry enr_database_file = self._boot_info.base_dir / ENR_DATABASE_FILENAME enr_db: QueryableENRDatabaseAPI = QueryableENRDB( sqlite3.connect(enr_database_file), identity_scheme_registry) local_private_key = get_local_private_key(self._boot_info) enr_manager = ENRManager( enr_db=enr_db, private_key=local_private_key, ) port = self._boot_info.port events = Events() if b"udp" not in enr_manager.enr: enr_manager.update((b"udp", port)) elif enr_manager.enr[b"udp"] != port: enr_manager.update((b"udp", port)) listen_on_ip_address: AnyIPAddress if self._boot_info.listen_on is None: listen_on_ip_address = DEFAULT_LISTEN else: listen_on_ip_address = self._boot_info.listen_on # Update the ENR if an explicit listening address was provided enr_manager.update( (IP_V4_ADDRESS_ENR_KEY, listen_on_ip_address.packed)) listen_on = Endpoint(listen_on_ip_address.packed, self._boot_info.port) if self._boot_info.is_upnp_enabled: upnp_service = UPnPService(port) self.manager.run_daemon_child_service(upnp_service) self.manager.run_daemon_task(self._update_enr_ip_from_upnp, enr_manager, upnp_service) bootnodes = self._boot_info.bootnodes self.client = Client( local_private_key=local_private_key, listen_on=listen_on, enr_db=enr_db, session_cache_size=self._args.session_cache_size, events=events, message_type_registry=message_type_registry, ) self.network = Network( client=self.client, bootnodes=bootnodes, ) if self._boot_info.is_rpc_enabled: handlers = merge( get_core_rpc_handlers(enr_manager, self.network.routing_table), get_v51_rpc_handlers(self.network), ) self.rpc_server = RPCServer(self._boot_info.ipc_path, handlers) self.manager.run_daemon_child_service(self.rpc_server) self.logger.info("Starting DDHT...") self.logger.info("Protocol-Version: %s", self._boot_info.protocol_version.value) self.logger.info("DDHT base dir : %s", self._boot_info.base_dir) self.logger.info("Listening on : %s", listen_on) self.logger.info("Local Node ID : %s", encode_hex(enr_manager.enr.node_id)) self.logger.info( "Local ENR : seq=%d enr=%s", enr_manager.enr.sequence_number, enr_manager.enr, ) self.manager.run_daemon_child_service(self.network) self._ready.set() await self.manager.wait_finished()
async def _manage_routing_table(self) -> None: # First load all the bootnode ENRs into our database for enr in self._bootnodes: try: self.enr_db.set_enr(enr) except OldSequenceNumber: pass # Now repeatedly try to bond with each bootnode until one succeeds. while self.manager.is_running: with trio.move_on_after(20): async with trio.open_nursery() as nursery: for enr in self._bootnodes: if enr.node_id == self.local_node_id: continue endpoint = Endpoint.from_enr(enr) nursery.start_soon(self._bond, enr.node_id, endpoint) await self._routing_table_ready.wait() break # Now we enter into an infinite loop that continually probes the # network to beep the routing table fresh. We both perform completely # random lookups, as well as targeted lookups on the outermost routing # table buckets which are not full. # # The `TokenBucket` allows us to burst at the beginning, making quick # successive probes, then slowing down once the # # TokenBucket starts with 10 tokens, refilling at 1 token every 30 # seconds. token_bucket = TokenBucket(1 / 30, 10) async with trio.open_nursery() as nursery: while self.manager.is_running: await token_bucket.take() # Get the logarithmic distance to the "largest" buckets # that are not full. non_full_bucket_distances = tuple( idx + 1 for idx, bucket in enumerate(self.routing_table.buckets) if len(bucket) < self.routing_table.bucket_size # noqa: E501 )[-16:] # Probe one of the not-full-buckets with a weighted preference # towards the largest buckets. distance_to_probe = weighted_choice(non_full_bucket_distances) target_node_id = at_log_distance(self.local_node_id, distance_to_probe) async with self.recursive_find_nodes(target_node_id) as enr_aiter: async for enr in enr_aiter: if enr.node_id == self.local_node_id: continue try: self.enr_db.set_enr(enr) except OldSequenceNumber: pass nursery.start_soon(self._bond, enr.node_id)
async def run(self) -> None: identity_scheme_registry = default_identity_scheme_registry message_type_registry = v51_registry enr_database_dir = self._boot_info.base_dir / ENR_DATABASE_DIR_NAME enr_database_dir.mkdir(exist_ok=True) enr_db = ENRDB(LevelDB(enr_database_dir), identity_scheme_registry) local_private_key = get_local_private_key(self._boot_info) enr_manager = ENRManager( enr_db=enr_db, private_key=local_private_key, ) port = self._boot_info.port events = Events() if b"udp" not in enr_manager.enr: enr_manager.update((b"udp", port)) listen_on_ip_address: AnyIPAddress if self._boot_info.listen_on is None: listen_on_ip_address = DEFAULT_LISTEN else: listen_on_ip_address = self._boot_info.listen_on # Update the ENR if an explicit listening address was provided enr_manager.update( (IP_V4_ADDRESS_ENR_KEY, listen_on_ip_address.packed)) listen_on = Endpoint(listen_on_ip_address.packed, self._boot_info.port) if self._boot_info.is_upnp_enabled: upnp_service = UPnPService(port) self.manager.run_daemon_child_service(upnp_service) self.manager.run_daemon_task(self._update_enr_ip_from_upnp, enr_manager, upnp_service) bootnodes = self._boot_info.bootnodes client = Client( local_private_key=local_private_key, listen_on=listen_on, enr_db=enr_db, events=events, message_type_registry=message_type_registry, ) network = Network( client=client, bootnodes=bootnodes, ) logger.info("Protocol-Version: %s", self._boot_info.protocol_version.value) logger.info("DDHT base dir: %s", self._boot_info.base_dir) logger.info("Starting discovery service...") logger.info("Listening on %s:%d", listen_on, port) logger.info("Local Node ID: %s", encode_hex(enr_manager.enr.node_id)) logger.info("Local ENR: seq=%d enr=%s", enr_manager.enr.sequence_number, enr_manager.enr) await run_trio_service(network)
def endpoint(self) -> Endpoint: return Endpoint( self.enr[IP_V4_ADDRESS_ENR_KEY], self.enr[UDP_PORT_ENR_KEY], )
def _endpoint_for_node_id(self, node_id: NodeID) -> Endpoint: enr = self.enr_db.get_enr(node_id) return Endpoint.from_enr(enr)