def test_invalid_key_length(self): self.assertRaises(ValueError, Distance, b'1' * 47) self.assertRaises(ValueError, Distance, b'1' * 49) self.assertRaises(ValueError, Distance, b'') self.assertRaises(ValueError, Distance(b'0' * 48), b'1' * 47) self.assertRaises(ValueError, Distance(b'0' * 48), b'1' * 49) self.assertRaises(ValueError, Distance(b'0' * 48), b'')
def __init__(self, loop: asyncio.BaseEventLoop, peer_manager: 'PeerManager', routing_table: 'TreeRoutingTable', protocol: 'KademliaProtocol', key: bytes, bottom_out_limit: typing.Optional[int] = 2, max_results: typing.Optional[int] = constants.k, exclude: typing.Optional[typing.List[typing.Tuple[str, int]]] = None, shortlist: typing.Optional[typing.List['KademliaPeer']] = None): if len(key) != constants.hash_length: raise ValueError("invalid key length: %i" % len(key)) self.loop = loop self.peer_manager = peer_manager self.routing_table = routing_table self.protocol = protocol self.key = key self.bottom_out_limit = bottom_out_limit self.max_results = max_results self.exclude = exclude or [] self.shortlist: typing.List['KademliaPeer'] = get_shortlist(routing_table, key, shortlist) self.active: typing.Set['KademliaPeer'] = set() self.contacted: typing.Set[typing.Tuple[str, int]] = set() self.distance = Distance(key) self.closest_peer: typing.Optional['KademliaPeer'] = None if not self.shortlist else self.shortlist[0] self.prev_closest_peer: typing.Optional['KademliaPeer'] = None self.iteration_queue = asyncio.Queue(loop=self.loop) self.running_probes: typing.Set[asyncio.Task] = set() self.iteration_count = 0 self.bottom_out_count = 0 self.running = False self.tasks: typing.List[asyncio.Task] = [] self.delayed_calls: typing.List[asyncio.Handle] = []
def should_split(self, bucket_index: int, to_add: bytes) -> bool: # https://stackoverflow.com/questions/32129978/highly-unbalanced-kademlia-routing-table/32187456#32187456 if bucket_index < self._split_buckets_under_index: return True contacts = self.get_peers() distance = Distance(self._parent_node_id) contacts.sort(key=lambda c: distance(c.node_id)) kth_contact = contacts[-1] if len(contacts) < constants.k else contacts[constants.k - 1] return distance(to_add) < distance(kth_contact.node_id)
async def peer_search(self, node_id: bytes, count=constants.k, max_results=constants.k*2, bottom_out_limit=20) -> typing.List['KademliaPeer']: accumulated: typing.List['KademliaPeer'] = [] async with self.peer_search_junction(node_id, max_results=max_results, bottom_out_limit=bottom_out_limit) as junction: async for peers in junction: accumulated.extend(peers) distance = Distance(node_id) accumulated.sort(key=lambda peer: distance(peer.node_id)) return accumulated[:count]
def find_close_peers(self, key: bytes, count: typing.Optional[int] = None, sender_node_id: typing.Optional[bytes] = None) -> typing.List['KademliaPeer']: exclude = [self._parent_node_id] if sender_node_id: exclude.append(sender_node_id) count = count or constants.k distance = Distance(key) contacts = self.get_peers() contacts = [c for c in contacts if c.node_id not in exclude] if contacts: contacts.sort(key=lambda c: distance(c.node_id)) return contacts[:min(count, len(contacts))] return []
def get_peers(self, count=-1, exclude_contact=None, sort_distance_to=None) -> typing.List['KademliaPeer']: """ Returns a list containing up to the first count number of contacts @param count: The amount of contacts to return (if 0 or less, return all contacts) @type count: int @param exclude_contact: A node node_id to exclude; if this contact is in the list of returned values, it will be discarded before returning. If a C{str} is passed as this argument, it must be the contact's ID. @type exclude_contact: str @param sort_distance_to: Sort distance to the node_id, defaulting to the parent node node_id. If False don't sort the contacts @raise IndexError: If the number of requested contacts is too large @return: Return up to the first count number of contacts in a list If no contacts are present an empty is returned @rtype: list """ peers = [ peer for peer in self.peers if peer.node_id != exclude_contact ] # Return all contacts in bucket if count <= 0: count = len(peers) # Get current contact number current_len = len(peers) # If count greater than k - return only k contacts if count > constants.k: count = constants.k if not current_len: return peers if sort_distance_to is False: pass else: sort_distance_to = sort_distance_to or self._node_id peers.sort(key=lambda c: Distance(sort_distance_to)(c.node_id)) return peers[:min(current_len, count)]
def __init__(self, peer_manager: 'PeerManager', range_min: int, range_max: int, node_id: bytes): """ @param range_min: The lower boundary for the range in the n-bit ID space covered by this k-bucket @param range_max: The upper boundary for the range in the ID space covered by this k-bucket """ self._peer_manager = peer_manager self.last_accessed = 0 self.range_min = range_min self.range_max = range_max self.peers: typing.List['KademliaPeer'] = [] self._node_id = node_id self._distance_to_self = Distance(node_id)
def get_shortlist(routing_table: 'TreeRoutingTable', key: bytes, shortlist: typing.Optional[typing.List['KademliaPeer']]) -> typing.List['KademliaPeer']: """ If not provided, initialize the shortlist of peers to probe to the (up to) k closest peers in the routing table :param routing_table: a TreeRoutingTable :param key: a 48 byte hash :param shortlist: optional manually provided shortlist, this is done during bootstrapping when there are no peers in the routing table. During bootstrap the shortlist is set to be the seed nodes. """ if len(key) != constants.hash_length: raise ValueError("invalid key length: %i" % len(key)) if not shortlist: shortlist = routing_table.find_close_peers(key) distance = Distance(key) shortlist.sort(key=lambda peer: distance(peer.node_id), reverse=True) return shortlist
async def peer_search( self, node_id: bytes, count=constants.k, max_results=constants.k * 2, bottom_out_limit=20, shortlist: typing.Optional[typing.List['KademliaPeer']] = None ) -> typing.List['KademliaPeer']: peers = [] async for iteration_peers in self.get_iterative_node_finder( node_id, shortlist=shortlist, bottom_out_limit=bottom_out_limit, max_results=max_results): peers.extend(iteration_peers) distance = Distance(node_id) peers.sort(key=lambda peer: distance(peer.node_id)) return peers[:count]
def midpoint_id_in_bucket_range(self, bucket_index: int) -> bytes: half = int((self.buckets[bucket_index].range_max - self.buckets[bucket_index].range_min) // 2) return Distance(self._parent_node_id)( int(self.buckets[bucket_index].range_min + half).to_bytes(constants.hash_length, 'big') ).to_bytes(constants.hash_length, 'big')
def random_id_in_bucket_range(self, bucket_index: int) -> bytes: random_id = int(random.randrange(self.buckets[bucket_index].range_min, self.buckets[bucket_index].range_max)) return Distance( self._parent_node_id )(random_id.to_bytes(constants.hash_length, 'big')).to_bytes(constants.hash_length, 'big')