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)
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
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