Example #1
0
    def remove(self, node_id: NodeID) -> None:
        """Remove a node from the routing table if it is present.

        If possible, the node will be replaced with the newest entry in the replacement cache.
        """
        bucket_index, bucket, replacement_cache = self.get_index_bucket_and_replacement_cache(
            node_id, )

        in_bucket = node_id in bucket
        in_replacement_cache = node_id in replacement_cache

        if in_bucket:
            bucket.remove(node_id)
            if replacement_cache:
                replacement_node_id = replacement_cache.popleft()
                self.logger.debug(
                    "Replacing %s from bucket %d with %s from replacement cache",
                    humanize_node_id(node_id),
                    bucket_index,
                    humanize_node_id(replacement_node_id),
                )
                bucket.append(replacement_node_id)
            else:
                self.logger.debug(
                    "Removing %s from bucket %d without replacement",
                    humanize_node_id(node_id),
                    bucket_index,
                )

        if in_replacement_cache:
            self.logger.debug(
                "Removing %s from replacement cache of bucket %d",
                humanize_node_id(node_id),
                bucket_index,
            )
            replacement_cache.remove(node_id)

        if not in_bucket and not in_replacement_cache:
            self.logger.debug(
                "Not removing %s as it is neither present in the bucket nor the replacement cache",
                humanize_node_id(node_id),
                bucket_index,
            )

        # bucket_update_order should only contain non-empty buckets, so remove it if necessary
        if not bucket:
            try:
                self.bucket_update_order.remove(bucket_index)
            except ValueError:
                pass
Example #2
0
    def update(self, node_id: NodeID) -> Optional[NodeID]:
        """Insert a node into the routing table or move it to the top if already present.

        If the bucket is already full, the node id will be added to the replacement cache and
        the oldest node is returned as an eviction candidate. Otherwise, the return value is
        `None`.
        """
        if node_id == self.center_node_id:
            raise ValueError("Cannot insert center node into routing table")

        bucket_index, bucket, replacement_cache = self.get_index_bucket_and_replacement_cache(
            node_id, )

        is_bucket_full = len(bucket) >= self.bucket_size
        is_node_in_bucket = node_id in bucket

        if not is_node_in_bucket and not is_bucket_full:
            self.logger.debug("Adding %s to bucket %d",
                              humanize_node_id(node_id), bucket_index)
            self.update_bucket_unchecked(node_id)
            eviction_candidate = None
        elif is_node_in_bucket:
            self.logger.debug("Updating %s in bucket %d",
                              humanize_node_id(node_id), bucket_index)
            self.update_bucket_unchecked(node_id)
            eviction_candidate = None
        elif not is_node_in_bucket and is_bucket_full:
            if node_id not in replacement_cache:
                self.logger.debug(
                    "Adding %s to replacement cache of bucket %d",
                    humanize_node_id(node_id),
                    bucket_index,
                )
            else:
                self.logger.debug(
                    "Updating %s in replacement cache of bucket %d",
                    humanize_node_id(node_id),
                    bucket_index,
                )
                replacement_cache.remove(node_id)
            replacement_cache.appendleft(node_id)
            eviction_candidate = bucket[-1]
        else:
            raise Exception("unreachable")

        return eviction_candidate
Example #3
0
        async def do_lookup(peer: Node) -> None:
            self.logger.debug(
                "Looking up %s via node %s",
                humanize_node_id(target_id),
                humanize_node_id(peer.node_id),
            )
            distance = compute_log_distance(peer.node_id, target_id)

            try:
                with trio.fail_after(FIND_NODES_TIMEOUT):
                    found_nodes = await self.single_lookup(
                        peer,
                        distance=distance,
                    )
            except trio.TooSlowError:
                unresponsive_node_ids.add(peer.node_id)
            else:
                if len(found_nodes) == 0:
                    unresponsive_node_ids.add(peer.node_id)
                else:
                    received_nodes[peer.node_id].add(peer.endpoint)
                    for node in found_nodes:
                        received_nodes[node.node_id].add(node.endpoint)
Example #4
0
 async def _monitor_endpoints(self) -> None:
     """
     Listen for completed handshakes and record the nodes in the routing
     table as well as the endpoint database.
     """
     async with self.client.events.handshake_complete.subscribe(
     ) as subscription:
         while self.manager.is_running:
             session = await subscription.receive()
             self.logger.debug(
                 'recording node and endpoint: %s@%s',
                 humanize_node_id(session.remote_node_id),
                 session.remote_endpoint,
             )
             self.endpoint_db.set_endpoint(session.remote_node_id,
                                           session.remote_endpoint)
             self.routing_table.update(session.remote_node_id)
Example #5
0
    async def _lookup_occasionally(self) -> None:
        async with trio.open_nursery() as nursery:
            async for _ in every(self.config.LOOKUP_INTERVAL):  # noqa: F841
                if self.routing_table.is_empty:
                    self.logger.debug(
                        'Aborting scheduled lookup due to empty routing table')
                    continue

                target_node_id = NodeID(secrets.randbits(256))
                found_nodes = await self.network.iterative_lookup(
                    target_node_id)
                self.logger.debug(
                    'Lookup for %s yielded %d nodes',
                    humanize_node_id(target_node_id),
                    len(found_nodes),
                )
                for node in found_nodes:
                    if node.node_id == self.client.local_node_id:
                        continue
                    nursery.start_soon(self.network.verify_and_add, node)
Example #6
0
 async def _periodic_report_routing_table_status(self) -> None:
     async for _ in every(300, 10):  # noqa: F841
         routing_stats = self.routing_table.get_stats()
         if routing_stats.full_buckets:
             full_buckets = '/'.join(
                 (str(index) for index in routing_stats.full_buckets))
         else:
             full_buckets = 'None'
         content_stats = self.content_manager.get_stats()
         self.logger.debug(
             ("\n"
              "###################[%s]#####################\n"
              "       RoutingTable(bucket_size=%d, num_buckets=%d):\n"
              "         - %d nodes\n"
              "         - full buckets: %s\n"
              "         - %d nodes in replacement cache\n"
              "       ContentDB():\n"
              "         - durable: %d\n"
              "         - ephemeral-db: %d (%d / %d)\n"
              "         - ephemeral-index: %d / %d\n"
              "         - cache-db: %d (%d / %d)\n"
              "         - cache-index: %d / %d\n"
              "####################################################"),
             humanize_node_id(self.client.local_node_id),
             routing_stats.bucket_size,
             routing_stats.num_buckets,
             routing_stats.total_nodes,
             full_buckets,
             routing_stats.num_in_replacement_cache,
             content_stats.durable_item_count,
             content_stats.ephemeral_db_count,
             content_stats.ephemeral_db_capacity,
             content_stats.ephemeral_db_total_capacity,
             content_stats.ephemeral_index_capacity,
             content_stats.ephemeral_index_total_capacity,
             content_stats.cache_db_count,
             content_stats.cache_db_capacity,
             content_stats.cache_db_total_capacity,
             content_stats.cache_index_capacity,
             content_stats.cache_index_total_capacity,
         )
Example #7
0
async def test_application(bootnode, base_db_path):
    bootnodes = (Node(bootnode.client.local_node_id, bootnode.client.listen_on),)

    connected_nodes = []

    async def monitor_bootnode():
        async with bootnode.client.events.handshake_complete.subscribe() as subscription:
            async for session in subscription:
                logger.info('NODE_CONNECTED_TO_BOOTNODE: %s', humanize_node_id(session.remote_node_id))  # noqa: E501
                connected_nodes.append(session.remote_node_id)

    config = KademliaConfig(
        LOOKUP_INTERVAL=20,
        ANNOUNCE_INTERVAL=30,
        ANNOUNCE_CONCURRENCY=1,
        storage_config=StorageConfig(
            ephemeral_storage_size=1024,
            ephemeral_index_size=500,
            cache_storage_size=1024,
            cache_index_size=100
        ),
    )

    async with AsyncExitStack() as stack:
        for i in range(16):
            # small delay between starting each client.
            await trio.sleep(random.random())
            # content database
            durable_db = make_durable_db(base_db_path / f"client-{i}")
            app = ApplicationFactory(
                bootnodes=bootnodes,
                durable_db=durable_db,
                config=config,
            )
            logger.info('CLIENT-%d: %s', i, humanize_node_id(app.client.local_node_id))
            await stack.enter_async_context(background_trio_service(app))
        await trio.sleep_forever()
Example #8
0
    async def run(self) -> None:
        # Run the subscription manager with gets fed all decoded inbound
        # messages and dispatches them to individual subscriptions.
        manager = self.manager.run_daemon_child_service(self.message_dispatcher)
        await manager.wait_started()

        listener = DatagramListener(
            self.listen_on,
            self._inbound_datagram_send_channel,
            self._outbound_datagram_receive_channel,
            events=self.events,
        )
        async with background_trio_service(listener):
            await listener.wait_listening()
            await self.events.listening.trigger(self.listen_on)

            self.manager.run_daemon_task(
                self._handle_inbound_datagrams,
                self._inbound_datagram_receive_channel,
            )
            self.manager.run_daemon_task(
                self._handle_outbound_messages,
                self._outbound_message_receive_channel,
            )
            self.manager.run_daemon_task(
                self._handle_outbound_packets,
                self._outbound_packet_receive_channel,
            )
            self.manager.run_daemon_task(self._periodically_ping_sessions)
            self.logger.info(
                'Client running: %s@%s',
                humanize_node_id(self.local_node_id),
                self.listen_on,
            )

            self._ready.set()
            await self.manager.wait_finished()
Example #9
0
async def bootnode(base_db_path):
    config = KademliaConfig(
        LOOKUP_INTERVAL=20,
        ANNOUNCE_INTERVAL=30,
        ANNOUNCE_CONCURRENCY=1,
        storage_config=StorageConfig(
            ephemeral_storage_size=1024,
            ephemeral_index_size=500,
            cache_storage_size=1024,
            cache_index_size=100
        ),
        can_initialize_network_skip_graph=True,
    )
    durable_db = make_durable_db(base_db_path / f"bootnode")

    bootnode = ApplicationFactory(
        durable_db=durable_db,
        config=config,
    )
    logger.info('BOOTNODE: %s', humanize_node_id(bootnode.client.local_node_id))
    async with bootnode.client.events.listening.subscribe() as listening:
        async with background_trio_service(bootnode):
            await listening.receive()
            yield bootnode
Example #10
0
    async def iterative_lookup(
        self,
        target_id: NodeID,
        filter_self: bool = True,
    ) -> Tuple[Node, ...]:
        self.logger.debug("Starting looking up @ %s",
                          humanize_node_id(target_id))

        # tracks the nodes that have already been queried
        queried_node_ids: Set[NodeID] = set()
        # keeps track of the nodes that are unresponsive
        unresponsive_node_ids: Set[NodeID] = set()
        # accumulator of all of the valid responses received
        received_nodes: DefaultDict[
            NodeID, Set[Endpoint]] = collections.defaultdict(set)

        async def do_lookup(peer: Node) -> None:
            self.logger.debug(
                "Looking up %s via node %s",
                humanize_node_id(target_id),
                humanize_node_id(peer.node_id),
            )
            distance = compute_log_distance(peer.node_id, target_id)

            try:
                with trio.fail_after(FIND_NODES_TIMEOUT):
                    found_nodes = await self.single_lookup(
                        peer,
                        distance=distance,
                    )
            except trio.TooSlowError:
                unresponsive_node_ids.add(peer.node_id)
            else:
                if len(found_nodes) == 0:
                    unresponsive_node_ids.add(peer.node_id)
                else:
                    received_nodes[peer.node_id].add(peer.endpoint)
                    for node in found_nodes:
                        received_nodes[node.node_id].add(node.endpoint)

        @to_tuple
        def get_endpoints(node_id: NodeID) -> Iterator[Endpoint]:
            try:
                yield self.endpoint_db.get_endpoint(node_id)
            except KeyError:
                pass

            yield from received_nodes[node_id]

        for lookup_round_number in itertools.count():
            received_node_ids = tuple(received_nodes.keys())
            candidates = iter_closest_nodes(target_id, self.routing_table,
                                            received_node_ids)
            responsive_candidates = itertools.dropwhile(
                lambda node: node in unresponsive_node_ids,
                candidates,
            )
            closest_k_candidates = take(self.routing_table.bucket_size,
                                        responsive_candidates)
            closest_k_unqueried_candidates = (
                candidate for candidate in closest_k_candidates
                if candidate not in queried_node_ids)
            nodes_to_query = tuple(
                take(
                    LOOKUP_CONCURRENCY_FACTOR,
                    closest_k_unqueried_candidates,
                ))

            if nodes_to_query:
                self.logger.debug(
                    "Starting lookup round %d for %s",
                    lookup_round_number + 1,
                    humanize_node_id(target_id),
                )
                queried_node_ids.update(nodes_to_query)
                async with trio.open_nursery() as nursery:
                    for peer_id in nodes_to_query:
                        if peer_id == self.client.local_node_id:
                            continue
                        for endpoint in get_endpoints(peer_id):
                            nursery.start_soon(do_lookup,
                                               Node(peer_id, endpoint))
            else:
                self.logger.debug(
                    "Lookup for %s finished in %d rounds",
                    humanize_node_id(target_id),
                    lookup_round_number,
                )
                break

        found_nodes = tuple(
            Node(node_id, endpoint)
            for node_id, endpoints in received_nodes.items()
            for endpoint in endpoints
            if (not filter_self or node_id != self.client.local_node_id))
        sorted_found_nodes = tuple(
            sorted(
                found_nodes,
                key=lambda node: compute_distance(self.client.local_node_id,
                                                  node.node_id),
            ))
        self.logger.debug(
            "Finished looking up %s in %d rounds: Found %d nodes after querying %d nodes",
            humanize_node_id(target_id),
            lookup_round_number,
            len(found_nodes),
            len(queried_node_ids),
        )
        return sorted_found_nodes
Example #11
0
 async def monitor_bootnode():
     async with bootnode.client.events.handshake_complete.subscribe() as subscription:
         async for session in subscription:
             logger.info('NODE_CONNECTED_TO_BOOTNODE: %s', humanize_node_id(session.remote_node_id))  # noqa: E501
             connected_nodes.append(session.remote_node_id)