def test_sort_contacts_no_longer_than_k(self): """ Ensure that no more than constants.K contacts are returned from the sort_contacts function despite a longer list being passed in. """ contacts = [] for i in range(512): contact = Contact(2 ** i, "192.168.0.%d" % i, 9999, self.version, 0) contacts.append(contact) target_key = long_to_hex(2 ** 256) result = sort_contacts(contacts, target_key) self.assertEqual(constants.K, len(result))
def find_close_nodes(self, key, rpc_node_id=None): """ Finds up to "K" number of known nodes closest to the node/value with the specified key. If rpc_node_id is supplied the referenced node will be excluded from the returned contacts. The result is a list of "K" node contacts of type dht.contact.Contact. Will only return fewer than "K" contacts if not enough contacts are known. The result is ordered from closest to furthest away from the target key. """ bucket_index = self._kbucket_index(key) closest_nodes = self._buckets[bucket_index].get_contacts( constants.K, rpc_node_id) # How far away to jump beyond the containing bucket of the given key. bucket_jump = 1 number_of_buckets = len(self._buckets) # Flags that indicate if it's possible to jump higher or lower through # the buckets. can_go_lower = bucket_index - bucket_jump >= 0 can_go_higher = bucket_index + bucket_jump < number_of_buckets while (len(closest_nodes) < constants.K and (can_go_lower or can_go_higher)): # Continue to fill the closestNodes list with contacts from the # nearest unchecked neighbouring k-buckets. Have chosen to opt for # readability rather than conciseness. if can_go_lower: # Neighbours lower in the key index. remaining_slots = constants.K - len(closest_nodes) jump_to_neighbour = bucket_index - bucket_jump neighbour = self._buckets[jump_to_neighbour] contacts = neighbour.get_contacts(remaining_slots, rpc_node_id) closest_nodes.extend(contacts) can_go_lower = bucket_index - (bucket_jump + 1) >= 0 if can_go_higher: # Neighbours higher in the key index. remaining_slots = constants.K - len(closest_nodes) jump_to_neighbour = bucket_index + bucket_jump neighbour = self._buckets[jump_to_neighbour] contacts = neighbour.get_contacts(remaining_slots, rpc_node_id) closest_nodes.extend(contacts) can_go_higher = (bucket_index + (bucket_jump + 1) < number_of_buckets) bucket_jump += 1 # Order the nodes from closest to furthest away from the target key and # ensure we only return K contacts (in certain circumstances K+1 # results are generated). return sort_contacts(closest_nodes, key)
def test_sort_contacts(self): """ Ensures that the sort_contacts function returns the list ordered in such a way that the contacts closest to the target key are at the head of the list. """ contacts = [] for i in range(512): contact = Contact(2 ** i, "192.168.0.%d" % i, 9999, self.version, 0) contacts.append(contact) target_key = long_to_hex(2 ** 256) result = sort_contacts(contacts, target_key) # Ensure results are in the correct order. def key(node): return distance(node.id, target_key) sorted_nodes = sorted(result, key=key) self.assertEqual(sorted_nodes, result) # Ensure the order is from lowest to highest in terms of distance distances = [distance(x.id, target_key) for x in result] self.assertEqual(sorted(distances), distances)
def _handle_response(self, uuid, contact, response): """ Callback to handle expected responses (unexpected responses result in the remote node being blacklisted and a TypeError being thrown). When a response to a request is returned successfully remove the request from self.pending_requests. If it's a FindValue message and a suitable value is returned (see note at the end of these comments) cancel all the other pending calls in self.pending_requests and fire a callback with with the returned value. If the value is invalid blacklist the node, remove it from self.shortlist and start from step 3 again without cancelling the other pending calls. If a list of closer nodes is returned by a peer add them to self.shortlist and sort - making sure nodes in self.contacted are not mistakenly re-added to the shortlist. If the nearest node in the newly sorted self.shortlist is closer to the target than self.nearest_node then set self.nearest_node to the new closer node and start from step 3 again. If self.nearest_node remains unchanged DO NOT start a new lookup call. If there are no other requests in self.pending_requests then check that the constants.K nearest nodes in the self.contacted set are all closer than the nearest node in self.shortlist. If they are, and it's a FindNode message call back with the constants.K nearest nodes found in the self.contacted set. If the message is a FindValue, errback with a ValueNotFound error. If there are still nearer nodes in self.shortlist to some of those in the constants.K nearest nodes in the self.contacted set then start from step 3 again (forcing the local node to contact the close nodes that have yet to be contacted). Note on validating values: In the future there may be constraints added to the FindValue query (such as only accepting values created after time T). """ # Remove originating request from pending requests. del self.pending_requests[uuid] # Ensure the response is of the expected type[s]. if not ((isinstance(response, Value) and self.message_type == FindValue) or isinstance(response, Nodes)): # Blacklist the problem contact from the routing table (since it # doesn't behave). self._blacklist(contact) raise TypeError("Unexpected response type from %r" % contact) # Is the response the expected Value we're looking for..? if isinstance(response, Value): # Check if it's a suitable value (the key matches) if response.key == self.target: # Ensure the Value has not expired. if response.expires < time.time(): # Do not blacklist expired nodes but simply remove them # from the shortlist (handled by the errback). raise ValueError("Expired value returned by %r" % contact) # Cancel outstanding requests. self._cancel_pending_requests() # Ensure the returning contact is removed from the shortlist # (so it's possible to discern the closest non-returning node) if contact in self.shortlist: self.shortlist.remove(contact) # Success! The correct Value has been found. Fire the instance # with the result. self.callback(response) else: # Blacklist the problem contact from the routing table since # it's not behaving properly. self._blacklist(contact) raise ValueError("Value with wrong key returned by %r" % contact) else: # Otherwise it must be a Nodes message containing closer nodes. # Add the returned nodes to the shortlist. Sort the shortlist in # order of closeness to the target and ensure the shortlist never # gets longer than K. candidate_contacts = [candidate for candidate in response.nodes if candidate not in self.shortlist] self.shortlist = sort_contacts(candidate_contacts + self.shortlist, self.target) # Check if the nearest_node remains unchanged. if self.nearest_node == self.shortlist[0]: # Check for remaining pending requests. if not self.pending_requests: # Check all the candidates in the shortlist have been # contacted. candidates = [candidate for candidate in self.shortlist if candidate in self.contacted] if len(candidates) == len(self.shortlist): # There is a result. if self.message_type == FindValue: # Can't find a value at the key. msg = ("Unable to find value for key: %r" % self.target) self.errback(ValueNotFound(msg)) else: # Success! Found nodes close to the specified # target key. self.callback(self.shortlist) else: # There are still un-contacted peers in the shortlist # so restart the lookup in order to check them. self._lookup() else: # There are still pending requests to complete but do not # restart the lookup pass else: # There is a new nearest node. self.nearest_node = self.shortlist[0] # Restart the lookup given the newly found nodes in the # shortlist. self._lookup()