Example #1
0
 async def initialize_address_manager(self) -> None:
     mkdir(self.peer_db_path.parent)
     self.connection = await aiosqlite.connect(self.peer_db_path)
     self.address_manager_store = await AddressManagerStore.create(self.connection)
     if not await self.address_manager_store.is_empty():
         self.address_manager = await self.address_manager_store.deserialize()
     else:
         await self.address_manager_store.clear()
         self.address_manager = AddressManager()
     self.server.set_received_message_callback(self.update_peer_timestamp_on_message)
Example #2
0
def create_address_manager(metadata: Dict[str, str], nodes: List[Node],
                           new_table_entries: List[Table]) -> AddressManager:
    address_manager: AddressManager = AddressManager()

    # ----- NOTICE -----
    # The following code was taken from the original implementation of
    # AddressManagerStore.deserialize(). The code is duplicated/preserved
    # here to support migration from older versions.
    # ------------------
    address_manager.key = int(metadata["key"])
    address_manager.new_count = int(metadata["new_count"])
    # address_manager.tried_count = int(metadata["tried_count"])
    address_manager.tried_count = 0

    new_table_nodes = [(node_id, info) for node_id, info in nodes
                       if node_id < address_manager.new_count]
    for n, info in new_table_nodes:
        address_manager.map_addr[info.peer_info.host] = n
        address_manager.map_info[n] = info
        info.random_pos = len(address_manager.random_pos)
        address_manager.random_pos.append(n)
    address_manager.id_count = len(new_table_nodes)
    tried_table_nodes = [(node_id, info) for node_id, info in nodes
                         if node_id >= address_manager.new_count]
    # lost_count = 0
    for node_id, info in tried_table_nodes:
        tried_bucket = info.get_tried_bucket(address_manager.key)
        tried_bucket_pos = info.get_bucket_position(address_manager.key, False,
                                                    tried_bucket)
        if address_manager.tried_matrix[tried_bucket][tried_bucket_pos] == -1:
            info.random_pos = len(address_manager.random_pos)
            info.is_tried = True
            id_count = address_manager.id_count
            address_manager.random_pos.append(id_count)
            address_manager.map_info[id_count] = info
            address_manager.map_addr[info.peer_info.host] = id_count
            address_manager.tried_matrix[tried_bucket][
                tried_bucket_pos] = id_count
            address_manager.id_count += 1
            address_manager.tried_count += 1
        # else:
        #    lost_count += 1

    # address_manager.tried_count -= lost_count
    for node_id, bucket in new_table_entries:
        if node_id >= 0 and node_id < address_manager.new_count:
            info = address_manager.map_info[node_id]
            bucket_pos = info.get_bucket_position(address_manager.key, True,
                                                  bucket)
            if address_manager.new_matrix[bucket][
                    bucket_pos] == -1 and info.ref_count < NEW_BUCKETS_PER_ADDRESS:
                info.ref_count += 1
                address_manager.new_matrix[bucket][bucket_pos] = node_id

    for node_id, info in list(address_manager.map_info.items()):
        if not info.is_tried and info.ref_count == 0:
            address_manager.delete_new_entry_(node_id)
    address_manager.load_used_table_positions()

    return address_manager
    async def create_address_manager(cls,
                                     peers_file_path: Path) -> AddressManager:
        """
        Create an address manager using data deserialized from a peers file.
        """
        address_manager: Optional[AddressManager] = None
        if peers_file_path.exists():
            try:
                log.info(f"Loading peers from {peers_file_path}")
                address_manager = await cls._deserialize(peers_file_path)
            except Exception:
                log.exception(
                    f"Unable to create address_manager from {peers_file_path}")

        if address_manager is None:
            log.info("Creating new address_manager")
            address_manager = AddressManager()

        return address_manager
Example #4
0
class FullNodeDiscovery:
    def __init__(
        self,
        server: ChiaServer,
        root_path: Path,
        target_outbound_count: int,
        peer_db_path: str,
        introducer_info: Optional[Dict],
        dns_servers: List[str],
        peer_connect_interval: int,
        log,
    ):
        self.server: ChiaServer = server
        self.message_queue: asyncio.Queue = asyncio.Queue()
        self.is_closed = False
        self.target_outbound_count = target_outbound_count
        self.peer_db_path = path_from_root(root_path, peer_db_path)
        self.dns_servers = dns_servers
        if introducer_info is not None:
            self.introducer_info: Optional[PeerInfo] = PeerInfo(
                introducer_info["host"],
                introducer_info["port"],
            )
        else:
            self.introducer_info = None
        self.peer_connect_interval = peer_connect_interval
        self.log = log
        self.relay_queue = None
        self.address_manager: Optional[AddressManager] = None
        self.connection_time_pretest: Dict = {}
        self.received_count_from_peers: Dict = {}
        self.lock = asyncio.Lock()
        self.connect_peers_task: Optional[asyncio.Task] = None
        self.serialize_task: Optional[asyncio.Task] = None
        self.cleanup_task: Optional[asyncio.Task] = None
        self.initial_wait: int = 0
        self.resolver = dns.asyncresolver.Resolver()
        self.pending_outbound_connections: Set = set()

    async def initialize_address_manager(self) -> None:
        mkdir(self.peer_db_path.parent)
        self.connection = await aiosqlite.connect(self.peer_db_path)
        self.address_manager_store = await AddressManagerStore.create(self.connection)
        if not await self.address_manager_store.is_empty():
            self.address_manager = await self.address_manager_store.deserialize()
        else:
            await self.address_manager_store.clear()
            self.address_manager = AddressManager()
        self.server.set_received_message_callback(self.update_peer_timestamp_on_message)

    async def start_tasks(self) -> None:
        random = Random()
        self.connect_peers_task = asyncio.create_task(self._connect_to_peers(random))
        self.serialize_task = asyncio.create_task(self._periodically_serialize(random))
        self.cleanup_task = asyncio.create_task(self._periodically_cleanup())

    async def _close_common(self) -> None:
        self.is_closed = True
        self.cancel_task_safe(self.connect_peers_task)
        self.cancel_task_safe(self.serialize_task)
        self.cancel_task_safe(self.cleanup_task)
        await self.connection.close()

    def cancel_task_safe(self, task: Optional[asyncio.Task]):
        if task is not None:
            try:
                task.cancel()
            except Exception as e:
                self.log.error(f"Error while canceling task.{e} {task}")

    def add_message(self, message, data):
        self.message_queue.put_nowait((message, data))

    async def on_connect(self, peer: ws.WSChiaConnection):
        if (
            peer.is_outbound is False
            and peer.peer_server_port is not None
            and peer.connection_type is NodeType.FULL_NODE
            and self.server._local_type is NodeType.FULL_NODE
            and self.address_manager is not None
        ):
            timestamped_peer_info = TimestampedPeerInfo(
                peer.peer_host,
                peer.peer_server_port,
                uint64(int(time.time())),
            )
            await self.address_manager.add_to_new_table([timestamped_peer_info], peer.get_peer_info(), 0)
            if self.relay_queue is not None:
                self.relay_queue.put_nowait((timestamped_peer_info, 1))
        if (
            peer.is_outbound
            and peer.peer_server_port is not None
            and peer.connection_type is NodeType.FULL_NODE
            and (self.server._local_type is NodeType.FULL_NODE or self.server._local_type is NodeType.WALLET)
            and self.address_manager is not None
        ):
            msg = make_msg(ProtocolMessageTypes.request_peers, full_node_protocol.RequestPeers())
            await peer.send_message(msg)

    # Updates timestamps each time we receive a message for outbound connections.
    async def update_peer_timestamp_on_message(self, peer: ws.WSChiaConnection):
        if (
            peer.is_outbound
            and peer.peer_server_port is not None
            and peer.connection_type is NodeType.FULL_NODE
            and self.server._local_type is NodeType.FULL_NODE
            and self.address_manager is not None
        ):
            peer_info = peer.get_peer_info()
            if peer_info is None:
                return None
            if peer_info.host not in self.connection_time_pretest:
                self.connection_time_pretest[peer_info.host] = time.time()
            if time.time() - self.connection_time_pretest[peer_info.host] > 600:
                self.connection_time_pretest[peer_info.host] = time.time()
                await self.address_manager.connect(peer_info)

    def _num_needed_peers(self) -> int:
        diff = self.target_outbound_count
        outgoing = self.server.get_outgoing_connections()
        diff -= len(outgoing)
        return diff if diff >= 0 else 0

    """
    Uses the Poisson distribution to determine the next time
    when we'll initiate a feeler connection.
    (https://en.wikipedia.org/wiki/Poisson_distribution)
    """

    def _poisson_next_send(self, now, avg_interval_seconds, random):
        return now + (
            math.log(random.randrange(1 << 48) * -0.0000000000000035527136788 + 1) * avg_interval_seconds * -1000000.0
            + 0.5
        )

    async def _introducer_client(self):
        if self.introducer_info is None:
            return None

        async def on_connect(peer: ws.WSChiaConnection):
            msg = make_msg(ProtocolMessageTypes.request_peers_introducer, introducer_protocol.RequestPeersIntroducer())
            await peer.send_message(msg)

        await self.server.start_client(self.introducer_info, on_connect)

    async def _query_dns(self, dns_address):
        try:
            peers: List[TimestampedPeerInfo] = []
            result = await self.resolver.resolve(qname=dns_address, lifetime=30)
            for ip in result:
                peers.append(
                    TimestampedPeerInfo(
                        ip.to_text(),
                        8444,
                        0,
                    )
                )
            self.log.info(f"Received {len(peers)} peers from DNS seeder.")
            if len(peers) == 0:
                return
            await self._respond_peers_common(full_node_protocol.RespondPeers(peers), None, False)
        except Exception as e:
            self.log.error(f"Exception while querying DNS server: {e}")

    async def start_client_async(self, addr: PeerInfo, is_feeler: bool) -> None:
        try:
            if self.address_manager is None:
                return
            if addr.host in self.pending_outbound_connections:
                return
            self.pending_outbound_connections.add(addr.host)
            client_connected = await self.server.start_client(
                addr,
                on_connect=self.server.on_connect,
                is_feeler=is_feeler,
            )
            if self.server.is_duplicate_or_self_connection(addr):
                # Mark it as a softer attempt, without counting the failures.
                await self.address_manager.attempt(addr, False)
            else:
                if client_connected is True:
                    await self.address_manager.mark_good(addr)
                    await self.address_manager.connect(addr)
                else:
                    await self.address_manager.attempt(addr, True)
            self.pending_outbound_connections.remove(addr.host)
        except Exception as e:
            if addr.host in self.pending_outbound_connections:
                self.pending_outbound_connections.remove(addr.host)
            self.log.error(f"Exception in create outbound connections: {e}")
            self.log.error(f"Traceback: {traceback.format_exc()}")

    async def _connect_to_peers(self, random) -> None:
        next_feeler = self._poisson_next_send(time.time() * 1000 * 1000, 240, random)
        retry_introducers = False
        introducer_attempts: int = 0
        dns_server_index: int = 0
        local_peerinfo: Optional[PeerInfo] = await self.server.get_peer_info()
        last_timestamp_local_info: uint64 = uint64(int(time.time()))
        if self.initial_wait > 0:
            await asyncio.sleep(self.initial_wait)

        introducer_backoff = 1
        while not self.is_closed:
            try:
                assert self.address_manager is not None

                # We don't know any address, connect to the introducer to get some.
                size = await self.address_manager.size()
                if size == 0 or retry_introducers or introducer_attempts == 0:
                    try:
                        await asyncio.sleep(introducer_backoff)
                    except asyncio.CancelledError:
                        return None
                    # Run dual between DNS servers and introducers. One time query DNS server,
                    # next two times query the introducer.
                    if introducer_attempts % 3 == 0 and len(self.dns_servers) > 0:
                        dns_address = self.dns_servers[dns_server_index]
                        dns_server_index = (dns_server_index + 1) % len(self.dns_servers)
                        await self._query_dns(dns_address)
                    else:
                        await self._introducer_client()
                        # there's some delay between receiving the peers from the
                        # introducer until they get incorporated to prevent this
                        # loop for running one more time. Add this delay to ensure
                        # that once we get peers, we stop contacting the introducer.
                        try:
                            await asyncio.sleep(5)
                        except asyncio.CancelledError:
                            return None

                    retry_introducers = False
                    introducer_attempts += 1
                    # keep doubling the introducer delay until we reach 5
                    # minutes
                    if introducer_backoff < 300:
                        introducer_backoff *= 2
                    continue
                else:
                    introducer_backoff = 1

                # Only connect out to one peer per network group (/16 for IPv4).
                groups = []
                full_node_connected = self.server.get_full_node_connections()
                connected = [c.get_peer_info() for c in full_node_connected]
                connected = [c for c in connected if c is not None]
                for conn in full_node_connected:
                    peer = conn.get_peer_info()
                    if peer is None:
                        continue
                    group = peer.get_group()
                    if group not in groups:
                        groups.append(group)

                # Feeler Connections
                #
                # Design goals:
                # * Increase the number of connectable addresses in the tried table.
                #
                # Method:
                # * Choose a random address from new and attempt to connect to it if we can connect
                # successfully it is added to tried.
                # * Start attempting feeler connections only after node finishes making outbound
                # connections.
                # * Only make a feeler connection once every few minutes.

                is_feeler = False
                has_collision = False
                if self._num_needed_peers() == 0:
                    if time.time() * 1000 * 1000 > next_feeler:
                        next_feeler = self._poisson_next_send(time.time() * 1000 * 1000, 240, random)
                        is_feeler = True

                await self.address_manager.resolve_tried_collisions()
                tries = 0
                now = time.time()
                got_peer = False
                addr: Optional[PeerInfo] = None
                max_tries = 50
                if len(groups) < 3:
                    max_tries = 10
                elif len(groups) <= 5:
                    max_tries = 25
                while not got_peer and not self.is_closed:
                    sleep_interval = 1 + len(groups) * 0.5
                    sleep_interval = min(sleep_interval, self.peer_connect_interval)
                    # Special case: try to find our first peer much quicker.
                    if len(groups) == 0:
                        sleep_interval = 0.1
                    try:
                        await asyncio.sleep(sleep_interval)
                    except asyncio.CancelledError:
                        return None
                    tries += 1
                    if tries > max_tries:
                        addr = None
                        retry_introducers = True
                        break
                    info: Optional[ExtendedPeerInfo] = await self.address_manager.select_tried_collision()
                    if info is None:
                        info = await self.address_manager.select_peer(is_feeler)
                    else:
                        has_collision = True
                    if info is None:
                        if not is_feeler:
                            retry_introducers = True
                        break
                    # Require outbound connections, other than feelers,
                    # to be to distinct network groups.
                    addr = info.peer_info
                    if has_collision:
                        break
                    if addr is not None and not addr.is_valid():
                        addr = None
                        continue
                    if not is_feeler and addr.get_group() in groups:
                        addr = None
                        continue
                    if addr in connected:
                        addr = None
                        continue
                    # only consider very recently tried nodes after 30 failed attempts
                    # attempt a node once per 30 minutes if we lack connections to increase the chance
                    # to try all the peer table.
                    if now - info.last_try < 1800 and tries < 30:
                        continue
                    if time.time() - last_timestamp_local_info > 1800 or local_peerinfo is None:
                        local_peerinfo = await self.server.get_peer_info()
                        last_timestamp_local_info = uint64(int(time.time()))
                    if local_peerinfo is not None and addr == local_peerinfo:
                        continue
                    got_peer = True

                disconnect_after_handshake = is_feeler
                if self._num_needed_peers() == 0:
                    disconnect_after_handshake = True
                    retry_introducers = False
                initiate_connection = self._num_needed_peers() > 0 or has_collision or is_feeler
                sleep_interval = 1 + len(groups) * 0.5
                sleep_interval = min(sleep_interval, self.peer_connect_interval)
                # Special case: try to find our first peer much quicker.
                if len(groups) == 0:
                    sleep_interval = 0.1
                if addr is not None and initiate_connection:
                    while len(self.pending_outbound_connections) >= MAX_CONCURRENT_OUTBOUND_CONNECTIONS:
                        self.log.debug(f"Max concurrent outbound connections reached. Retrying in {sleep_interval}s.")
                        await asyncio.sleep(sleep_interval)
                    asyncio.create_task(self.start_client_async(addr, disconnect_after_handshake))
                await asyncio.sleep(sleep_interval)
            except Exception as e:
                self.log.error(f"Exception in create outbound connections: {e}")
                self.log.error(f"Traceback: {traceback.format_exc()}")

    async def _periodically_serialize(self, random: Random):
        while not self.is_closed:
            if self.address_manager is None:
                await asyncio.sleep(10)
                continue
            serialize_interval = random.randint(15 * 60, 30 * 60)
            await asyncio.sleep(serialize_interval)
            async with self.address_manager.lock:
                await self.address_manager_store.serialize(self.address_manager)

    async def _periodically_cleanup(self) -> None:
        while not self.is_closed:
            # Removes entries with timestamp worse than 14 days ago
            # and with a high number of failed attempts.
            # Most likely, the peer left the network,
            # so we can save space in the peer tables.
            cleanup_interval = 1800
            max_timestamp_difference = 14 * 3600 * 24
            max_consecutive_failures = 10
            await asyncio.sleep(cleanup_interval)

            # Perform the cleanup only if we have at least 3 connections.
            full_node_connected = self.server.get_full_node_connections()
            connected = [c.get_peer_info() for c in full_node_connected]
            connected = [c for c in connected if c is not None]
            if self.address_manager is not None and len(connected) >= 3:
                async with self.address_manager.lock:
                    self.address_manager.cleanup(max_timestamp_difference, max_consecutive_failures)

    async def _respond_peers_common(self, request, peer_src, is_full_node) -> None:
        # Check if we got the peers from a full node or from the introducer.
        peers_adjusted_timestamp = []
        is_misbehaving = False
        if len(request.peer_list) > MAX_PEERS_RECEIVED_PER_REQUEST:
            is_misbehaving = True
        if is_full_node:
            if peer_src is None:
                return None
            async with self.lock:
                if peer_src.host not in self.received_count_from_peers:
                    self.received_count_from_peers[peer_src.host] = 0
                self.received_count_from_peers[peer_src.host] += len(request.peer_list)
                if self.received_count_from_peers[peer_src.host] > MAX_TOTAL_PEERS_RECEIVED:
                    is_misbehaving = True
        if is_misbehaving:
            return None
        for peer in request.peer_list:
            if peer.timestamp < 100000000 or peer.timestamp > time.time() + 10 * 60:
                # Invalid timestamp, predefine a bad one.
                current_peer = TimestampedPeerInfo(
                    peer.host,
                    peer.port,
                    uint64(int(time.time() - 5 * 24 * 60 * 60)),
                )
            else:
                current_peer = peer
            if not is_full_node:
                current_peer = TimestampedPeerInfo(
                    peer.host,
                    peer.port,
                    uint64(0),
                )
            peers_adjusted_timestamp.append(current_peer)

        assert self.address_manager is not None

        if is_full_node:
            await self.address_manager.add_to_new_table(peers_adjusted_timestamp, peer_src, 2 * 60 * 60)
        else:
            await self.address_manager.add_to_new_table(peers_adjusted_timestamp, None, 0)
    async def deserialize(self) -> AddressManager:
        address_manager = AddressManager()
        metadata = await self.get_metadata()
        nodes = await self.get_nodes()
        new_table_entries = await self.get_new_table()
        address_manager.clear()

        address_manager.key = int(metadata["key"])
        address_manager.new_count = int(metadata["new_count"])
        # address_manager.tried_count = int(metadata["tried_count"])
        address_manager.tried_count = 0

        new_table_nodes = [(node_id, info) for node_id, info in nodes
                           if node_id < address_manager.new_count]
        for n, info in new_table_nodes:
            address_manager.map_addr[info.peer_info.host] = n
            address_manager.map_info[n] = info
            info.random_pos = len(address_manager.random_pos)
            address_manager.random_pos.append(n)
        address_manager.id_count = len(new_table_nodes)
        tried_table_nodes = [(node_id, info) for node_id, info in nodes
                             if node_id >= address_manager.new_count]
        # lost_count = 0
        for node_id, info in tried_table_nodes:
            tried_bucket = info.get_tried_bucket(address_manager.key)
            tried_bucket_pos = info.get_bucket_position(
                address_manager.key, False, tried_bucket)
            if address_manager.tried_matrix[tried_bucket][
                    tried_bucket_pos] == -1:
                info.random_pos = len(address_manager.random_pos)
                info.is_tried = True
                id_count = address_manager.id_count
                address_manager.random_pos.append(id_count)
                address_manager.map_info[id_count] = info
                address_manager.map_addr[info.peer_info.host] = id_count
                address_manager.tried_matrix[tried_bucket][
                    tried_bucket_pos] = id_count
                address_manager.id_count += 1
                address_manager.tried_count += 1
            # else:
            #    lost_count += 1

        # address_manager.tried_count -= lost_count
        for node_id, bucket in new_table_entries:
            if node_id >= 0 and node_id < address_manager.new_count:
                info = address_manager.map_info[node_id]
                bucket_pos = info.get_bucket_position(address_manager.key,
                                                      True, bucket)
                if address_manager.new_matrix[bucket][
                        bucket_pos] == -1 and info.ref_count < NEW_BUCKETS_PER_ADDRESS:
                    info.ref_count += 1
                    address_manager.new_matrix[bucket][bucket_pos] = node_id

        for node_id, info in list(address_manager.map_info.items()):
            if not info.is_tried and info.ref_count == 0:
                address_manager.delete_new_entry_(node_id)
        address_manager.load_used_table_positions()
        return address_manager
    async def _deserialize(cls, peers_file_path: Path) -> AddressManager:
        """
        Create an address manager using data deserialized from a peers file.
        """
        peer_data: Optional[PeerDataSerialization] = None
        address_manager = AddressManager()
        start_time = timer()
        try:
            peer_data = await cls._read_peers(peers_file_path)
        except Exception:
            log.exception(
                f"Unable to deserialize peers from {peers_file_path}")

        if peer_data is not None:
            metadata: Dict[str, str] = {
                key: value
                for key, value in peer_data.metadata
            }
            nodes: List[Tuple[int, ExtendedPeerInfo]] = [
                (node_id, ExtendedPeerInfo.from_string(info_str))
                for node_id, info_str in peer_data.nodes
            ]
            new_table_entries: List[Tuple[int, int]] = [
                (node_id, bucket) for node_id, bucket in peer_data.new_table
            ]
            log.debug(
                f"Deserializing peer data took {timer() - start_time} seconds")

            address_manager.key = int(metadata["key"])
            address_manager.new_count = int(metadata["new_count"])
            # address_manager.tried_count = int(metadata["tried_count"])
            address_manager.tried_count = 0

            new_table_nodes = [(node_id, info) for node_id, info in nodes
                               if node_id < address_manager.new_count]
            for n, info in new_table_nodes:
                address_manager.map_addr[info.peer_info.host] = n
                address_manager.map_info[n] = info
                info.random_pos = len(address_manager.random_pos)
                address_manager.random_pos.append(n)
            address_manager.id_count = len(new_table_nodes)
            tried_table_nodes = [(node_id, info) for node_id, info in nodes
                                 if node_id >= address_manager.new_count]
            # lost_count = 0
            for node_id, info in tried_table_nodes:
                tried_bucket = info.get_tried_bucket(address_manager.key)
                tried_bucket_pos = info.get_bucket_position(
                    address_manager.key, False, tried_bucket)
                if address_manager.tried_matrix[tried_bucket][
                        tried_bucket_pos] == -1:
                    info.random_pos = len(address_manager.random_pos)
                    info.is_tried = True
                    id_count = address_manager.id_count
                    address_manager.random_pos.append(id_count)
                    address_manager.map_info[id_count] = info
                    address_manager.map_addr[info.peer_info.host] = id_count
                    address_manager.tried_matrix[tried_bucket][
                        tried_bucket_pos] = id_count
                    address_manager.id_count += 1
                    address_manager.tried_count += 1
                # else:
                #    lost_count += 1

            # address_manager.tried_count -= lost_count
            for node_id, bucket in new_table_entries:
                if node_id >= 0 and node_id < address_manager.new_count:
                    info = address_manager.map_info[node_id]
                    bucket_pos = info.get_bucket_position(
                        address_manager.key, True, bucket)
                    if (address_manager.new_matrix[bucket][bucket_pos] == -1
                            and info.ref_count < NEW_BUCKETS_PER_ADDRESS):
                        info.ref_count += 1
                        address_manager.new_matrix[bucket][
                            bucket_pos] = node_id

            for node_id, info in list(address_manager.map_info.items()):
                if not info.is_tried and info.ref_count == 0:
                    address_manager.delete_new_entry_(node_id)

            address_manager.load_used_table_positions()

        return address_manager