async def test_basic_sync(self, two_nodes): num_blocks = 100 blocks = bt.get_consecutive_blocks(test_constants, num_blocks, [], 10) full_node_1, full_node_2, server_1, server_2 = two_nodes for i in range(1, num_blocks): async for _ in full_node_1.block(peer_protocol.Block(blocks[i])): pass await server_2.start_client( PeerInfo(server_1._host, uint16(server_1._port)), None) await asyncio.sleep(2) # Allow connections to get made start_unf = time.time() while time.time() - start_unf < 60: # The second node should eventually catch up to the first one, and have the # same tip at height num_blocks - 1. if (max( [h.height for h in full_node_2.blockchain.get_current_tips() ]) == num_blocks - 1): print( f"Time taken to sync {num_blocks} is {time.time() - start_unf}" ) return await asyncio.sleep(0.1) raise Exception("Took too long to process blocks")
async def test_blocks_load(self, two_nodes): num_blocks = 100 full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = bt.get_consecutive_blocks(test_constants, num_blocks, [], 10) await server_2.start_client( PeerInfo(server_1._host, uint16(server_1._port)), None ) await asyncio.sleep(2) # Allow connections to get made start_unf = time.time() for i in range(1, num_blocks): msg = Message("block", peer_protocol.Block(blocks[i])) server_1.push_message( OutboundMessage(NodeType.FULL_NODE, msg, Delivery.BROADCAST) ) while time.time() - start_unf < 100: if ( max([h.height for h in full_node_2.blockchain.get_current_tips()]) == num_blocks - 1 ): print( f"Time taken to process {num_blocks} is {time.time() - start_unf}" ) return await asyncio.sleep(0.1) raise Exception("Took too long to process blocks")
async def proof_of_time_finished( self, request: timelord_protocol.ProofOfTimeFinished ) -> OutboundMessageGenerator: """ A proof of time, received by a peer timelord. We can use this to complete a block, and call the block routine (which handles propagation and verification of blocks). """ async with self.store.lock: dict_key = ( request.proof.challenge_hash, request.proof.number_of_iterations, ) unfinished_block_obj: Optional[ FullBlock] = await self.store.get_unfinished_block(dict_key) if not unfinished_block_obj: log.warning( f"Received a proof of time that we cannot use to complete a block {dict_key}" ) return prev_full_block = await self.store.get_block( unfinished_block_obj.prev_header_hash) assert prev_full_block prev_block: HeaderBlock = prev_full_block.header_block difficulty: uint64 = self.blockchain.get_next_difficulty( unfinished_block_obj.prev_header_hash) assert prev_block.challenge challenge: Challenge = Challenge( request.proof.challenge_hash, unfinished_block_obj.header_block.proof_of_space.get_hash(), request.proof.output.get_hash(), uint32(prev_block.challenge.height + 1), uint64(prev_block.challenge.total_weight + difficulty), uint64(prev_block.challenge.total_iters + request.proof.number_of_iterations), ) new_header_block = HeaderBlock( unfinished_block_obj.header_block.proof_of_space, request.proof, challenge, unfinished_block_obj.header_block.header, ) new_full_block: FullBlock = FullBlock(new_header_block, unfinished_block_obj.body) async with self.store.lock: sync_mode = await self.store.get_sync_mode() if sync_mode: async with self.store.lock: await self.store.add_potential_future_block(new_full_block) else: async for msg in self.block(peer_protocol.Block(new_full_block)): yield msg
async def test1(self): store = FullNodeStore("fndb_test") await store._clear_database() blocks = bt.get_consecutive_blocks(test_constants, 10, [], 10) b: Blockchain = Blockchain(test_constants) await store.add_block(blocks[0]) await b.initialize({}) for i in range(1, 9): assert (await b.receive_block(blocks[i] )) == ReceiveBlockResult.ADDED_TO_HEAD await store.add_block(blocks[i]) full_node_1 = FullNode(store, b) server_1 = ChiaServer(21234, full_node_1, NodeType.FULL_NODE) _ = await server_1.start_server("127.0.0.1", None) full_node_1._set_server(server_1) full_node_2 = FullNode(store, b) server_2 = ChiaServer(21235, full_node_2, NodeType.FULL_NODE) full_node_2._set_server(server_2) await server_2.start_client(PeerInfo("127.0.0.1", uint16(21234)), None) await asyncio.sleep(2) # Allow connections to get made num_unfinished_blocks = 1000 start_unf = time.time() for i in range(num_unfinished_blocks): msg = Message("unfinished_block", peer_protocol.UnfinishedBlock(blocks[9])) server_1.push_message( OutboundMessage(NodeType.FULL_NODE, msg, Delivery.BROADCAST)) # Send the whole block ast the end so we can detect when the node is done block_msg = Message("block", peer_protocol.Block(blocks[9])) server_1.push_message( OutboundMessage(NodeType.FULL_NODE, block_msg, Delivery.BROADCAST)) while time.time() - start_unf < 300: if max([h.height for h in b.get_current_tips()]) == 9: print( f"Time taken to process {num_unfinished_blocks} is {time.time() - start_unf}" ) server_1.close_all() server_2.close_all() await server_1.await_closed() await server_2.await_closed() return await asyncio.sleep(0.1) server_1.close_all() server_2.close_all() await server_1.await_closed() await server_2.await_closed() raise Exception("Took too long to process blocks")
async def request_block( self, request_block: peer_protocol.RequestBlock ) -> OutboundMessageGenerator: block: Optional[FullBlock] = await self.store.get_block( request_block.header_hash) if block is not None: yield OutboundMessage( NodeType.FULL_NODE, Message("block", peer_protocol.Block(block)), Delivery.RESPOND, )
async def test2(self): num_blocks = 100 store = FullNodeStore("fndb_test") await store._clear_database() blocks = bt.get_consecutive_blocks(test_constants, num_blocks, [], 10) b: Blockchain = Blockchain(test_constants) await store.add_block(blocks[0]) await b.initialize({}) full_node_1 = FullNode(store, b) server_1 = ChiaServer(21236, full_node_1, NodeType.FULL_NODE) _ = await server_1.start_server("127.0.0.1", None) full_node_1._set_server(server_1) full_node_2 = FullNode(store, b) server_2 = ChiaServer(21237, full_node_2, NodeType.FULL_NODE) full_node_2._set_server(server_2) await server_2.start_client(PeerInfo("127.0.0.1", uint16(21236)), None) await asyncio.sleep(2) # Allow connections to get made start_unf = time.time() for i in range(1, num_blocks): msg = Message("block", peer_protocol.Block(blocks[i])) server_1.push_message( OutboundMessage(NodeType.FULL_NODE, msg, Delivery.BROADCAST)) while time.time() - start_unf < 300: if max([h.height for h in b.get_current_tips()]) == num_blocks - 1: print( f"Time taken to process {num_blocks} is {time.time() - start_unf}" ) server_1.close_all() server_2.close_all() await server_1.await_closed() await server_2.await_closed() return await asyncio.sleep(0.1) server_1.close_all() server_2.close_all() await server_1.await_closed() await server_2.await_closed() raise Exception("Took too long to process blocks")
async def _finish_sync(self) -> OutboundMessageGenerator: """ Finalize sync by setting sync mode to False, clearing all sync information, and adding any final blocks that we have finalized recently. """ async with self.store.lock: potential_fut_blocks = ( await self.store.get_potential_future_blocks()).copy() await self.store.set_sync_mode(False) await self.store.clear_sync_info() for block in potential_fut_blocks: if self._shut_down: return async for msg in self.block(peer_protocol.Block(block)): yield msg # Update farmers and timelord with most recent information async for msg in self._send_challenges_to_timelords(): yield msg async for msg in self._send_tips_to_farmers(): yield msg
async def _on_connect(self) -> OutboundMessageGenerator: """ Whenever we connect to another node, send them our current heads. Also send heads to farmers and challenges to timelords. """ blocks: List[FullBlock] = [] async with self.store.lock: heads: List[HeaderBlock] = self.blockchain.get_current_tips() for h in heads: block = await self.store.get_block(h.header.get_hash()) assert block blocks.append(block) for block in blocks: request = peer_protocol.Block(block) yield OutboundMessage(NodeType.FULL_NODE, Message("block", request), Delivery.RESPOND) # Update farmers and timelord with most recent information async for msg in self._send_challenges_to_timelords(Delivery.RESPOND): yield msg async for msg in self._send_tips_to_farmers(Delivery.RESPOND): yield msg
async def block(self, block: peer_protocol.Block) -> OutboundMessageGenerator: """ Receive a full block from a peer full node (or ourselves). """ header_hash = block.block.header_block.header.get_hash() # Adds the block to seen, and check if it's seen before if self.store.seen_block(header_hash): return async with self.store.lock: if await self.store.get_sync_mode(): # Add the block to our potential tips list await self.store.add_potential_tip(block.block) return # Tries to add the block to the blockchain added: ReceiveBlockResult = await self.blockchain.receive_block( block.block) # Always immediately add the block to the database, after updating blockchain state if (added == ReceiveBlockResult.ADDED_AS_ORPHAN or added == ReceiveBlockResult.ADDED_TO_HEAD): await self.store.add_block(block.block) if added == ReceiveBlockResult.ALREADY_HAVE_BLOCK: return elif added == ReceiveBlockResult.INVALID_BLOCK: log.warning( f"Block {header_hash} at height {block.block.height} is invalid." ) return elif added == ReceiveBlockResult.DISCONNECTED_BLOCK: log.warning(f"Disconnected block {header_hash}") async with self.store.lock: tip_height = min([ head.height for head in self.blockchain.get_current_tips() ]) if (block.block.height > tip_height + self.config["sync_blocks_behind_threshold"]): async with self.store.lock: if await self.store.get_sync_mode(): return await self.store.clear_sync_info() await self.store.add_potential_tip(block.block) await self.store.set_sync_mode(True) log.info( f"We are too far behind this block. Our height is {tip_height} and block is at " f"{block.block.height}") try: # Performs sync, and catch exceptions so we don't close the connection async for ret_msg in self._sync(): yield ret_msg except asyncio.CancelledError: log.warning("Syncing failed, CancelledError") except BaseException as e: log.warning(f"Error {type(e)}{e} with syncing") finally: async for ret_msg in self._finish_sync(): yield ret_msg elif block.block.height >= tip_height - 3: log.info( f"We have received a disconnected block at height {block.block.height}, current tip is {tip_height}" ) msg = Message( "request_block", peer_protocol.RequestBlock(block.block.prev_header_hash), ) async with self.store.lock: await self.store.add_disconnected_block(block.block) yield OutboundMessage(NodeType.FULL_NODE, msg, Delivery.RESPOND) return elif added == ReceiveBlockResult.ADDED_TO_HEAD: # Only propagate blocks which extend the blockchain (becomes one of the heads) ips_changed: bool = False async with self.store.lock: log.info( f"Updated heads, new heights: {[b.height for b in self.blockchain.get_current_tips()]}" ) difficulty = self.blockchain.get_next_difficulty( block.block.prev_header_hash) next_vdf_ips = self.blockchain.get_next_ips( block.block.header_hash) log.info(f"Difficulty {difficulty} IPS {next_vdf_ips}") if next_vdf_ips != await self.store.get_proof_of_time_estimate_ips( ): await self.store.set_proof_of_time_estimate_ips( next_vdf_ips) ips_changed = True if ips_changed: rate_update = farmer_protocol.ProofOfTimeRate(next_vdf_ips) log.info(f"Sending proof of time rate {next_vdf_ips}") yield OutboundMessage( NodeType.FARMER, Message("proof_of_time_rate", rate_update), Delivery.BROADCAST, ) self.store.clear_seen_unfinished_blocks() self.store.clear_seen_blocks() assert block.block.header_block.proof_of_time assert block.block.header_block.challenge pos_quality = (block.block.header_block.proof_of_space. verify_and_get_quality()) farmer_request = farmer_protocol.ProofOfSpaceFinalized( block.block.header_block.challenge.get_hash(), block.block.height, block.block.weight, pos_quality, difficulty, ) timelord_request = timelord_protocol.ChallengeStart( block.block.header_block.challenge.get_hash(), block.block.header_block.challenge.total_weight, ) # Tell timelord to stop previous challenge and start with new one yield OutboundMessage( NodeType.TIMELORD, Message("challenge_start", timelord_request), Delivery.BROADCAST, ) # Tell full nodes about the new block yield OutboundMessage( NodeType.FULL_NODE, Message("block", block), Delivery.BROADCAST_TO_OTHERS, ) # Tell farmer about the new block yield OutboundMessage( NodeType.FARMER, Message("proof_of_space_finalized", farmer_request), Delivery.BROADCAST, ) elif added == ReceiveBlockResult.ADDED_AS_ORPHAN: assert block.block.header_block.proof_of_time assert block.block.header_block.challenge log.info( f"Received orphan block of height {block.block.header_block.challenge.height}" ) else: # Should never reach here, all the cases are covered assert False # Recursively process the next block if we have it # This code path is reached if added == ADDED_AS_ORPHAN or ADDED_TO_HEAD async with self.store.lock: next_block: Optional[ FullBlock] = await self.store.get_disconnected_block_by_prev( block.block.header_hash) if next_block is not None: async for ret_msg in self.block(peer_protocol.Block(next_block)): yield ret_msg async with self.store.lock: # Removes all temporary data for old blocks lowest_tip = min(tip.height for tip in self.blockchain.get_current_tips()) clear_height = uint32(max(0, lowest_tip - 30)) await self.store.clear_candidate_blocks_below(clear_height) await self.store.clear_unfinished_blocks_below(clear_height) await self.store.clear_disconnected_blocks_below(clear_height)