async def test_network_find_nodes_api(alice, bob): distances = {0} async with alice.network() as alice_network: async with bob.network() as bob_network: for _ in range(200): enr = ENRFactory() bob.enr_db.set_enr(enr) bob_network.routing_table.update(enr.node_id) distances.add(compute_log_distance(enr.node_id, bob.node_id)) if distances.issuperset({0, 256, 255}): break else: raise Exception("failed") with trio.fail_after(2): enrs = await alice_network.find_nodes(bob.node_id, 0, 255, 256) assert any(enr.node_id == bob.node_id for enr in enrs) response_distances = { compute_log_distance(enr.node_id, bob.node_id) for enr in enrs if enr.node_id != bob.node_id } assert response_distances == {256, 255}
async def test_network_responds_to_find_node_requests(alice, bob): distances = {0} async with alice.network() as alice_network: async with bob.network() as bob_network: for _ in range(200): enr = ENRFactory() bob.enr_db.set_enr(enr) bob_network.routing_table.update(enr.node_id) distances.add(compute_log_distance(enr.node_id, bob.node_id)) if distances.issuperset({0, 256, 255}): break else: raise Exception("failed") with trio.fail_after(2): responses = await alice_network.client.find_nodes( bob.endpoint, bob.node_id, distances=(0, 255, 256), ) assert all( isinstance(response.message, FoundNodesMessage) for response in responses ) response_enrs = tuple( enr for response in responses for enr in response.message.enrs ) response_distances = { compute_log_distance(enr.node_id, bob.node_id) if enr.node_id != bob.node_id else 0 for enr in response_enrs } assert response_distances.issuperset({0, 255, 256})
def _get_nodes_for_exploration(self) -> Iterator[Tuple[NodeID, int]]: candidates = self._get_ordered_candidates() candidate_triplets = sliding_window( 3, caboose(cons(None, candidates), None)) for left_id, node_id, right_id in candidate_triplets: # Filter out nodes that have already been queried if node_id in self.queried: continue elif node_id in self.in_flight: continue # By looking at the two closest *sibling* nodes we can determine # how much of their routing table we need to query. We consider # the maximum logarithmic distance to either neighbor which # guarantees that we look up the region of the network that this # node knows the most about, but avoid querying buckets for which # other nodes are going to have a more complete view. if left_id is None: left_distance = 256 else: left_distance = compute_log_distance(node_id, left_id) if right_id is None: right_distance = 256 else: right_distance = compute_log_distance(node_id, right_id) # We use the maximum distance to ensure that we cover every part of # the address space. yield node_id, max(left_distance, right_distance)
async def test_alexandria_network_find_nodes_api(alice, bob, alice_alexandria_network, bob_alexandria_client): distances = {0} bob_alexandria_routing_table = KademliaRoutingTable( bob.enr.node_id, ROUTING_TABLE_BUCKET_SIZE) for _ in range(200): enr = ENRFactory() bob.enr_db.set_enr(enr) bob_alexandria_routing_table.update(enr.node_id) distances.add(compute_log_distance(enr.node_id, bob.node_id)) if distances.issuperset({0, 256, 255}): break else: raise Exception("failed") async with bob_alexandria_client.subscribe( FindNodesMessage) as subscription: async with trio.open_nursery() as nursery: async def _respond(): request = await subscription.receive() response_enrs = [] for distance in request.message.payload.distances: if distance == 0: response_enrs.append(bob.enr) else: for ( node_id ) in bob_alexandria_routing_table.get_nodes_at_log_distance( distance): response_enrs.append(bob.enr_db.get_enr(node_id)) await bob_alexandria_client.send_found_nodes( request.sender_node_id, request.sender_endpoint, enrs=response_enrs, request_id=request.request_id, ) nursery.start_soon(_respond) with trio.fail_after(2): enrs = await alice_alexandria_network.find_nodes( bob.node_id, 0, 255, 256) assert any(enr.node_id == bob.node_id for enr in enrs) response_distances = { compute_log_distance(enr.node_id, bob.node_id) for enr in enrs if enr.node_id != bob.node_id } assert response_distances == {256, 255}
async def lookup_at_peer( self, peer: NodeID, target: NodeID ) -> Optional[Tuple[ENRAPI, ...]]: self.logger.debug( "Looking up %s at node %s", encode_hex(target), encode_hex(peer) ) distance = compute_log_distance(peer, target) first_attempt = await self.request_nodes(peer, target, distance) if first_attempt is None: self.logger.debug("Lookup with node %s failed", encode_hex(peer)) return None elif len(first_attempt) >= LOOKUP_RETRY_THRESHOLD: self.logger.debug( "Node %s responded with %d nodes with single attempt", encode_hex(peer), len(first_attempt), ) return first_attempt else: second_attempt = await self.request_nodes(peer, target, distance) both_attempts = first_attempt + (second_attempt or ()) self.logger.debug( "Node %s responded with %d nodes in two attempts", encode_hex(peer), len(both_attempts), ) return both_attempts
async def test_v51_rpc_sendFindNodes_web3(bob_node_id_param_w3, bob, bob_network, w3): distances = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) async with bob_network.client.dispatcher.subscribe( FindNodeMessage) as subscription: first_response = await trio.to_thread.run_sync( w3.discv5.send_find_nodes, bob_node_id_param_w3, 0) with trio.fail_after(2): first_receipt = await subscription.receive() assert encode_hex( first_receipt.message.request_id) == first_response.value assert first_receipt.message.distances == (0, ) # request with multiple distances second_response = await trio.to_thread.run_sync( w3.discv5.send_find_nodes, bob_node_id_param_w3, tuple(distances)) with trio.fail_after(2): second_receipt = await subscription.receive() assert encode_hex( second_receipt.message.request_id) == second_response.value assert second_receipt.message.distances == tuple(distances)
def test_at_log_distance(): for i in range(10000): node = NodeIDFactory() distance = random.randint(1, 256) other = at_log_distance(node, distance) actual = compute_log_distance(node, other) assert actual == distance
def at_log_distance(cls, reference: NodeID, log_distance: int) -> NodeID: num_bits = len(reference) * 8 if log_distance > num_bits: raise ValueError( "Log distance must not be greater than number of bits in the node id" ) elif log_distance < 0: raise ValueError("Log distance cannot be negative") num_common_bits = num_bits - log_distance flipped_bit_index = num_common_bits num_random_bits = num_bits - num_common_bits - 1 reference_bits = bytes_to_bits(reference) shared_bits = reference_bits[:num_common_bits] flipped_bit = not reference_bits[flipped_bit_index] random_bits = [ bool(random.randint(0, 1)) for _ in range(flipped_bit_index + 1, flipped_bit_index + 1 + num_random_bits) ] result_bits = tuple(list(shared_bits) + [flipped_bit] + random_bits) result = NodeID(bits_to_bytes(result_bits)) assert compute_log_distance(result, reference) == log_distance return result
async def test_network_stream_find_nodes(alice, bob, alice_network, bob_client): enrs = tuple(ENRFactory() for _ in range(FOUND_NODES_MAX_PAYLOAD_SIZE + 1)) distances = set( [compute_log_distance(enr.node_id, bob.node_id) for enr in enrs]) async with trio.open_nursery() as nursery: async with bob.events.find_nodes_received.subscribe() as subscription: async def _send_response(): find_nodes = await subscription.receive() await bob_client.send_found_nodes( alice.node_id, alice.endpoint, enrs=enrs, request_id=find_nodes.message.request_id, ) nursery.start_soon(_send_response) with trio.fail_after(2): async with alice_network.stream_find_nodes( bob.node_id, bob.endpoint, distances=distances) as resp_aiter: actual_enrs = tuple([resp async for resp in resp_aiter]) assert actual_enrs == enrs nursery.cancel_scope.cancel()
async def test_network_stream_find_nodes_api_validates_response_distances( alice, bob, bob_client, alice_network, response_enr): if response_enr == "own": enr_for_response = bob.enr elif response_enr == "wrong": for _ in range(200): enr_for_response = ENRFactory() if compute_log_distance(enr_for_response.node_id, bob.node_id) == 256: break else: raise Exception("failed") else: raise Exception(f"unsupported param: {response_enr}") async with bob.events.find_nodes_received.subscribe() as subscription: async with trio.open_nursery() as nursery: async def _respond(): request = await subscription.receive() await bob_client.send_found_nodes( alice.node_id, alice.endpoint, enrs=(enr_for_response, ), request_id=request.request_id, ) nursery.start_soon(_respond) with trio.fail_after(REQUEST_RESPONSE_TIMEOUT): with pytest.raises(ValidationError, match="Invalid response: distance="): async with alice_network.stream_find_nodes( bob.node_id, bob.endpoint, distances=[255]) as resp_aiter: tuple([resp async for resp in resp_aiter])
async def test_ping_handler_updates_routing_table( ping_handler_service, inbound_message_channels, outbound_message_channels, local_enr, remote_enr, routing_table, ): distance = compute_log_distance(remote_enr.node_id, local_enr.node_id) other_node_id = NodeIDFactory.at_log_distance(local_enr.node_id, distance) routing_table.update(other_node_id) assert routing_table.get_nodes_at_log_distance(distance) == ( other_node_id, remote_enr.node_id, ) ping = PingMessageFactory() inbound_message = InboundMessageFactory( message=ping, sender_node_id=remote_enr.node_id, ) await inbound_message_channels[0].send(inbound_message) await wait_all_tasks_blocked() assert routing_table.get_nodes_at_log_distance(distance) == ( remote_enr.node_id, other_node_id, )
async def test_v51_rpc_sendFindNodes(make_request, bob_node_id_param, bob, bob_network): distances = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) async with bob_network.client.dispatcher.subscribe( FindNodeMessage) as subscription: single_response = await make_request("discv5_sendFindNodes", [bob_node_id_param, 0]) with trio.fail_after(2): first_receipt = await subscription.receive() assert encode_hex(first_receipt.message.request_id) == single_response assert first_receipt.message.distances == (0, ) # request with multiple distances multiple_response = await make_request( "discv5_sendFindNodes", [bob_node_id_param, tuple(distances)], ) with trio.fail_after(2): second_receipt = await subscription.receive() assert encode_hex( second_receipt.message.request_id) == multiple_response assert second_receipt.message.distances == tuple(distances)
async def find_nodes( self, node_id: NodeID, endpoint: Endpoint, distances: Collection[int], *, request_id: Optional[bytes] = None, ) -> Tuple[InboundMessage[FoundNodesMessage], ...]: request = FindNodesMessage(FindNodesPayload(tuple(distances))) subscription: trio.abc.ReceiveChannel[ InboundMessage[FoundNodesMessage]] # unclear why `subscribe_request` isn't properly carrying the type information async with self.subscribe_request( # type: ignore node_id, endpoint, request, FoundNodesMessage, request_id=request_id, ) as subscription: head_response = await subscription.receive() total = head_response.message.payload.total responses: Tuple[InboundMessage[FoundNodesMessage], ...] if total == 1: responses = (head_response, ) elif total > 1: tail_responses: List[InboundMessage[FoundNodesMessage]] = [] for _ in range(total - 1): tail_responses.append(await subscription.receive()) responses = (head_response, ) + tuple(tail_responses) else: # TODO: this code path needs to be excercised and # probably replaced with some sort of # `SessionTerminated` exception. raise Exception("Invalid `total` counter in response") # Validate that all responses are indeed at one of the # specified distances. for response in responses: for enr in response.message.payload.enrs: if enr.node_id == node_id: if 0 not in distances: raise ValidationError( f"Invalid response: distance=0 expected={distances}" ) else: distance = compute_log_distance(enr.node_id, node_id) if distance not in distances: raise ValidationError( f"Invalid response: distance={distance} expected={distances}" ) return responses
async def find_nodes( self, endpoint: Endpoint, node_id: NodeID, distances: Collection[int], ) -> Tuple[InboundMessage[FoundNodesMessage], ...]: with self._get_request_id(node_id) as request_id: request = AnyOutboundMessage( FindNodeMessage(request_id, tuple(distances)), endpoint, node_id, ) async with self.dispatcher.subscribe_request( request, FoundNodesMessage) as subscription: with trio.fail_after(REQUEST_RESPONSE_TIMEOUT): head_response = await subscription.receive() total = head_response.message.total responses: Tuple[InboundMessage[FoundNodesMessage], ...] if total == 1: responses = (head_response, ) elif total > 1: tail_responses: List[ InboundMessage[FoundNodesMessage]] = [] for _ in range(total - 1): tail_responses.append(await subscription.receive()) responses = (head_response, ) + tuple(tail_responses) else: # TODO: this code path needs to be excercised and # probably replaced with some sort of # `SessionTerminated` exception. raise Exception("Invalid `total` counter in response") # Validate that all responses are indeed at one of the # specified distances. for response in responses: for enr in response.message.enrs: if enr.node_id == node_id: if 0 not in distances: raise ValidationError( f"Invalid response: distance=0 expected={distances}" ) else: distance = compute_log_distance( enr.node_id, node_id) if distance not in distances: raise ValidationError( f"Invalid response: distance={distance} expected={distances}" ) return responses
def validate_found_nodes_distances( enrs: Collection[ENRAPI], local_node_id: NodeID, distances: Collection[int], ) -> None: for enr in enrs: if enr.node_id == local_node_id: if 0 not in distances: raise ValidationError( f"Invalid response: distance=0 expected={distances}") else: distance = compute_log_distance(enr.node_id, local_node_id) if distance not in distances: raise ValidationError( f"Invalid response: distance={distance} expected={distances}" )
async def test_client_request_response_stream_find_nodes_handles_premature_exit( alice, bob, alice_client, bob_client, autojump_clock ): enrs = tuple(ENRFactory() for _ in range(FOUND_NODES_MAX_PAYLOAD_SIZE + 1)) async with trio.open_nursery() as nursery: async with bob.events.find_nodes_received.subscribe() as subscription: enr_batches = partition_enrs( enrs, max_payload_size=FOUND_NODES_MAX_PAYLOAD_SIZE ) first_enr_batch = enr_batches[0] distances = set( [ compute_log_distance(enr.node_id, bob.node_id) for enr in first_enr_batch ] ) num_batches = len(enr_batches) async def _respond(): find_nodes = await subscription.receive() assert num_batches > 1 message = AnyOutboundMessage( FoundNodesMessage( find_nodes.message.request_id, num_batches, first_enr_batch, ), alice.endpoint, alice.node_id, ) await bob_client.dispatcher.send_message(message) nursery.start_soon(_respond) found_nodes_messages = [] with trio.fail_after(REQUEST_RESPONSE_TIMEOUT + 1): async with alice_client.stream_find_nodes( bob.node_id, bob.endpoint, distances=distances ) as resp_aiter: async for resp in resp_aiter: found_nodes_messages.append(resp) # prematurely close the context after first response await resp_aiter.aclose() assert len(found_nodes_messages) == 1 nursery.cancel_scope.cancel()
async def test_client_request_response_stream_find_nodes_timeout_with_incomplete_response( alice, bob, alice_client, bob_client, autojump_clock ): enrs = tuple(ENRFactory() for _ in range(FOUND_NODES_MAX_PAYLOAD_SIZE + 1)) async with trio.open_nursery() as nursery: async with bob.events.find_nodes_received.subscribe() as subscription: enr_batches = partition_enrs( enrs, max_payload_size=FOUND_NODES_MAX_PAYLOAD_SIZE ) first_enr_batch = enr_batches[0] distances = set( [ compute_log_distance(enr.node_id, bob.node_id) for enr in first_enr_batch ] ) num_batches = len(enr_batches) async def _respond(): find_nodes = await subscription.receive() assert num_batches > 1 message = AnyOutboundMessage( FoundNodesMessage( find_nodes.message.request_id, num_batches, first_enr_batch, ), alice.endpoint, alice.node_id, ) # only send first batch of responses, then nothing await bob_client.dispatcher.send_message(message) nursery.start_soon(_respond) with trio.fail_after(REQUEST_RESPONSE_TIMEOUT + 1): with pytest.raises(trio.TooSlowError): async with alice_client.stream_find_nodes( bob.node_id, bob.endpoint, distances=distances ) as resp_aiter: found_nodes_messages = tuple( [resp async for resp in resp_aiter] ) assert len(found_nodes_messages) == 1 nursery.cancel_scope.cancel()
async def test_find_node_handler_sends_remote_enrs( find_node_handler_service, inbound_message_channels, outbound_message_channels, local_enr, remote_enr, ): distance = compute_log_distance(local_enr.node_id, remote_enr.node_id) find_node = FindNodeMessageFactory(distance=distance) inbound_message = InboundMessageFactory(message=find_node) await inbound_message_channels[0].send(inbound_message) await wait_all_tasks_blocked() outbound_message = outbound_message_channels[1].receive_nowait() assert isinstance(outbound_message.message, NodesMessage) assert outbound_message.message.request_id == find_node.request_id assert outbound_message.message.total == 1 assert outbound_message.message.enrs == (remote_enr, )
async def do_lookup(node_id: NodeID) -> None: queried_node_ids.add(node_id) distance = compute_log_distance(node_id, target) try: enrs = await self.find_nodes(node_id, distance) except trio.TooSlowError: unresponsive_node_ids.add(node_id) return for enr in enrs: received_node_ids.add(enr.node_id) try: self.enr_db.set_enr(enr) except OldSequenceNumber: received_enrs.append(self.enr_db.get_enr(enr.node_id)) else: received_enrs.append(enr)
async def do_lookup( node_id: NodeID, send_channel: trio.abc.SendChannel[ENRAPI] ) -> None: """ Perform an individual lookup on the target part of the network from the given `node_id` """ if node_id == target: distance = 0 else: distance = compute_log_distance(node_id, target) try: found_enrs = await network.find_nodes(node_id, distance) except (trio.TooSlowError, MissingEndpointFields, ValidationError): unresponsive_node_ids.add(node_id) unresponsive_cache[node_id] = trio.current_time() return except trio.Cancelled: # We don't add these to the unresponsive cache since they didn't # necessarily exceed the fulle 10s request/response timeout. unresponsive_node_ids.add(node_id) raise for enr in found_enrs: try: network.enr_db.set_enr(enr) except OldSequenceNumber: pass async with condition: new_enrs = tuple( enr for enr in found_enrs if enr.node_id not in received_node_ids ) received_node_ids.update(enr.node_id for enr in new_enrs) for enr in new_enrs: try: await send_channel.send(enr) except (trio.BrokenResourceError, trio.ClosedResourceError): # In the event that the consumer of `recursive_find_nodes` # exits early before the lookup has completed we can end up # operating on a closed channel. return
async def test_client_request_response_stream_find_nodes_inconsistent_message_total( alice, bob, alice_client, bob_client ): enrs = tuple(ENRFactory() for _ in range(FOUND_NODES_MAX_PAYLOAD_SIZE + 1)) distances = set([compute_log_distance(enr.node_id, bob.node_id) for enr in enrs]) async with trio.open_nursery() as nursery: async with bob_client.dispatcher.subscribe(FindNodeMessage) as subscription: enr_batches = partition_enrs( enrs, max_payload_size=FOUND_NODES_MAX_PAYLOAD_SIZE ) num_batches = len(enr_batches) async def _respond(): request = await subscription.receive() assert len(enr_batches) > 1 head_message = AnyOutboundMessage( FoundNodesMessage( request.message.request_id, num_batches, enr_batches[0], ), alice.endpoint, alice.node_id, ) await bob_client.dispatcher.send_message(head_message) invalid_message = AnyOutboundMessage( FoundNodesMessage( request.message.request_id, num_batches + 1, enr_batches[1], ), alice.endpoint, alice.node_id, ) await bob_client.dispatcher.send_message(invalid_message) nursery.start_soon(_respond) with pytest.raises(ValidationError, match="Inconsistent message total"): async with alice_client.stream_find_nodes( bob.node_id, bob.endpoint, distances=distances ) as resp_aiter: tuple([resp async for resp in resp_aiter]) nursery.cancel_scope.cancel()
async def test_alexandria_network_responds_to_find_nodes( alice, bob, alice_alexandria_network, bob_alexandria_network, ): enr = ENRFactory() bob.enr_db.set_enr(enr) bob_alexandria_network.routing_table.update(enr.node_id) enr_distance = compute_log_distance(enr.node_id, bob.node_id) with trio.fail_after(2): enrs = await alice_alexandria_network.find_nodes( bob.node_id, enr_distance, ) assert len(enrs) >= 1 assert any(enr.node_id == enr.node_id for enr in enrs)
async def test_client_request_response_stream_find_nodes_found_nodes( alice, bob, alice_client, bob_client ): enrs = tuple(ENRFactory() for _ in range(FOUND_NODES_MAX_PAYLOAD_SIZE + 1)) distances = set([compute_log_distance(enr.node_id, bob.node_id) for enr in enrs]) async with trio.open_nursery() as nursery: async with bob.events.find_nodes_received.subscribe() as subscription: async def _send_response(): find_nodes = await subscription.receive() await bob_client.send_found_nodes( alice.node_id, alice.endpoint, enrs=enrs, request_id=find_nodes.message.request_id, ) nursery.start_soon(_send_response) with trio.fail_after(2): async with alice_client.stream_find_nodes( bob.node_id, bob.endpoint, distances=distances ) as resp_aiter: found_nodes_messages = tuple([resp async for resp in resp_aiter]) found_node_ids = { enr.node_id for message in found_nodes_messages for enr in message.message.enrs } expected_node_ids = {enr.node_id for enr in enrs} assert found_node_ids == expected_node_ids expected_total = len( partition_enrs(enrs, max_payload_size=FOUND_NODES_MAX_PAYLOAD_SIZE) ) assert set((msg.message.total) for msg in found_nodes_messages) == set( [expected_total] ) nursery.cancel_scope.cancel()
async def test_network_recursive_find_nodes(tester, alice, bob): async with AsyncExitStack() as stack: await stack.enter_async_context(bob.network()) bootnodes = collections.deque((bob.enr,), maxlen=4) nodes = [bob, alice] for _ in range(20): node = tester.node() nodes.append(node) await stack.enter_async_context(node.network(bootnodes=bootnodes)) bootnodes.append(node.enr) # give the the network some time to interconnect. with trio.fail_after(5): for _ in range(1000): await trio.lowlevel.checkpoint() alice_network = await stack.enter_async_context( alice.network(bootnodes=bootnodes) ) # give alice a little time to connect to the network as well with trio.fail_after(5): for _ in range(1000): await trio.lowlevel.checkpoint() target_node_id = secrets.token_bytes(32) node_ids_by_distance = tuple( sorted( tuple(node.enr.node_id for node in nodes), key=lambda node_id: compute_log_distance(target_node_id, node_id), ) ) best_node_ids_by_distance = set(node_ids_by_distance[:3]) with trio.fail_after(10): found_enrs = await alice_network.recursive_find_nodes(target_node_id) found_node_ids = tuple(enr.node_id for enr in found_enrs) # Ensure that one of the three closest node ids was in the returned node ids assert best_node_ids_by_distance.intersection(found_node_ids)
async def test_network_explore(tester, alice): async with AsyncExitStack() as stack: networks = await stack.enter_async_context( tester.alexandria.network_group(8)) # give the the network some time to interconnect. with trio.fail_after(20): for _ in range(1000): await trio.lowlevel.checkpoint() bootnodes = tuple(network.enr_manager.enr for network in networks) alice_network = await stack.enter_async_context( alice.alexandria.network(bootnodes=bootnodes)) # give alice a little time to connect to the network as well with trio.fail_after(20): for _ in range(1000): await trio.lowlevel.checkpoint() assert len(set(alice_network.routing_table.iter_all_random())) == 8 target_node_id = at_log_distance(alice.node_id, 256) node_ids_by_distance = tuple( sorted( tuple(network.local_node_id for network in networks), key=lambda node_id: compute_log_distance( target_node_id, node_id), )) best_node_ids_by_distance = set(node_ids_by_distance[:3]) async with alice_network.explore(target_node_id) as enr_aiter: with trio.fail_after(60): found_enrs = tuple([enr async for enr in enr_aiter]) found_node_ids = tuple(enr.node_id for enr in found_enrs) assert len(found_node_ids) == len(networks) + 1 # Ensure that one of the three closest node ids was in the returned node ids assert best_node_ids_by_distance.intersection(found_node_ids)
async def test_v51_rpc_recursiveFindNodes_web3(tester, bob, w3): async with AsyncExitStack() as stack: await stack.enter_async_context(bob.network()) bootnodes = collections.deque((bob.enr, ), maxlen=4) nodes = [bob] target_node_id = None for _ in range(8): node = tester.node() nodes.append(node) await stack.enter_async_context(node.network(bootnodes=bootnodes)) bootnodes.append(node.enr) if (not target_node_id and compute_log_distance(node.node_id, bob.node_id) < 256): target_node_id = node.node_id # give the the network some time to interconnect. with trio.fail_after(60): for _ in range(1000): await trio.lowlevel.checkpoint() await trio.to_thread.run_sync( w3.discv5.bond, bob.node_id.hex(), ) try: with trio.fail_after(60): found_enrs = await trio.to_thread.run_sync( w3.discv5.recursive_find_nodes, target_node_id) except trio.TooSlowError: # These tests are flakey. Timeouts are expected in the testing # environment so we silently pass on timeouts. This still allows # this test to provide some value in the case that a non-timeout # based error shows up. return # Ensure that one of the three closest node ids was in the returned node ids assert len(found_enrs) > 0 assert isinstance(found_enrs[0], ENR)
async def test_v51_rpc_findNodes_w3(bob_node_id_param, bob, w3): distances = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) # request with positional single distance enrs_at_0 = await trio.to_thread.run_sync(w3.discv5.find_nodes, bob_node_id_param, 0) assert all(isinstance(enr, ENR) for enr in enrs_at_0) # request with multiple distances enrs_at_some_distance = await trio.to_thread.run_sync( w3.discv5.find_nodes, bob_node_id_param, tuple(distances), ) # verify that all of the returned ENR records can be parsed as valid ENRs for enr_repr in enrs_at_some_distance: ENR.from_repr(enr_repr)
async def test_v51_rpc_findNodes(make_request, bob_node_id_param, bob): distances = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) # request with positional single distance enrs_at_0 = await make_request("discv5_findNodes", [bob_node_id_param, 0]) # verify that all of the returned ENR records can be parsed as valid ENRs for enr_repr in enrs_at_0: ENR.from_repr(enr_repr) # request with multiple distances enrs_at_some_distance = await make_request( "discv5_findNodes", [bob_node_id_param, tuple(distances)], ) # verify that all of the returned ENR records can be parsed as valid ENRs for enr_repr in enrs_at_some_distance: ENR.from_repr(enr_repr)
async def test_v51_rpc_sendFoundNodes_web3(bob_node_id_param_w3, bob, bob_network, w3): distances = set() enrs = set() for _ in range(10): enr = ENRFactory() distances.add(compute_log_distance(bob.node_id, enr.node_id)) bob.enr_db.set_enr(enr) enrs.add(repr(enr)) request_id = encode_hex(secrets.token_bytes(4)) single_enr = next(iter(enrs)) async with bob_network.client.dispatcher.subscribe( FoundNodesMessage) as subscription: first_response = await trio.to_thread.run_sync( w3.discv5.send_found_nodes, bob_node_id_param_w3, (single_enr, ), request_id) with trio.fail_after(2): first_receipt = await subscription.receive() assert first_receipt.message.total == first_response.value assert encode_hex(first_receipt.message.request_id) == request_id # request with multiple enrs second_response = await trio.to_thread.run_sync( w3.discv5.send_found_nodes, bob_node_id_param_w3, tuple(enrs), request_id) with trio.fail_after(2): second_receipt = await subscription.receive() assert second_receipt.message.total == second_response.value assert encode_hex(second_receipt.message.request_id) == request_id
async def test_v51_rpc_recursiveFindNodes(tester, bob, make_request): async with AsyncExitStack() as stack: await stack.enter_async_context(bob.network()) bootnodes = collections.deque((bob.enr, ), maxlen=4) nodes = [bob] target_node_id = None for _ in range(8): node = tester.node() nodes.append(node) await stack.enter_async_context(node.network(bootnodes=bootnodes)) bootnodes.append(node.enr) if (not target_node_id and compute_log_distance(node.node_id, bob.node_id) < 256): target_node_id = node.node_id # give the the network some time to interconnect. with trio.fail_after(60): for _ in range(1000): await trio.lowlevel.checkpoint() await make_request("discv5_bond", [bob.node_id.hex()]) try: with trio.fail_after(60): found_enrs = await make_request("discv5_recursiveFindNodes", [target_node_id.hex()]) except trio.TooSlowError: # These tests are flakey. Timeouts are expected in the testing # environment so we silently pass on timeouts. This still allows # this test to provide some value in the case that a non-timeout # based error shows up. return found_enrs = tuple( ENR.from_repr(enr_repr).node_id for enr_repr in found_enrs) assert len(found_enrs) > 0