Repository: bitcoin/bitcoin
File: test/functional/p2p_node_network_limited.py (Python)

Code before:
        self.log.info("Check that the localservices is as expected.")
        assert_equal(int(self.nodes[0].getnetworkinfo()['localservices'], 16), expected_services)

        self.log.info("Mine enough blocks to reach the NODE_NETWORK_LIMITED range.")
        self.connect_nodes(0, 1)
        blocks = self.generate(self.nodes[1], 292, sync_fun=lambda: self.sync_blocks([self.nodes[0], self.nodes[1]]))

        self.log.info("Make sure we can max retrieve block at tip-288.")
        node.send_getdata_for_block(blocks[1])  # last block in valid range
        node.wait_for_block(int(blocks[1], 16), timeout=3)

        self.log.info("Requesting block at height 2 (tip-289) must fail (ignored).")
        node.send_getdata_for_block(blocks[0])  # first block outside of the 288+2 limit
        node.wait_for_disconnect(5)

        self.log.info("Check local address relay, do a fresh connection.")
        self.nodes[0].disconnect_p2ps()
        node1 = self.nodes[0].add_p2p_connection(P2PIgnoreInv())
        node1.send_message(msg_verack())

        node1.wait_for_addr()
        #must relay address with NODE_NETWORK_LIMITED
        assert_equal(node1.firstAddrnServices, expected_services)

        self.nodes[0].disconnect_p2ps()

        # connect unsynced node 2 with pruned NODE_NETWORK_LIMITED peer
        # because node 2 is in IBD and node 0 is a NODE_NETWORK_LIMITED peer, sync must not be possible
        self.connect_nodes(0, 2)
        try:
            self.sync_blocks([self.nodes[0], self.nodes[2]], timeout=5)
        except Exception:
            pass
        # node2 must remain at height 0
        assert_equal(self.nodes[2].getblockheader(self.nodes[2].getbestblockhash())['height'], 0)

        # now connect also to node 1 (non pruned)
        self.connect_nodes(1, 2)

        # sync must be possible
        self.sync_blocks()

        # disconnect all peers
        self.disconnect_all()

        # mine 10 blocks on node 0 (pruned node)
        self.generate(self.nodes[0], 10, sync_fun=self.no_op)

        # connect node1 (non pruned) with node0 (pruned) and check if the can sync
        self.connect_nodes(0, 1)


Review comment (bug):
This sleep is non-deterministic. The good thing is that it will not cause sporadic test failures, but still, it could occasionally report success even if the full node requests an old block and gets disconnected (false positive). Also it adds unconditional 3 seconds to the tests execution time.

Is it possible to lookup something in the log or via RPC to cross check that the full node has made a decision not to request blocks from the pruned node?

Code after:
        # Connect and generate block to ensure IBD=false
        self.connect_nodes(1, 0)
        self.connect_nodes(1, 2)
        self.generate(miner, 1)

        # Verify peers are out of IBD
        for node in self.nodes:
            assert not node.getblockchaininfo()['initialblockdownload']

        # Isolate full_node (the node will remain out of IBD)
        full_node.setnetworkactive(False)
        self.wait_until(lambda: len(full_node.getpeerinfo()) == 0)

        # Mine blocks and sync the pruned node. Surpass the NETWORK_NODE_LIMITED threshold.
        # Blocks deeper than the threshold are considered "historical blocks"
        num_historial_blocks = 12
        self.generate(miner, NODE_NETWORK_LIMITED_MIN_BLOCKS + num_historial_blocks, sync_fun=self.no_op)
        self.sync_blocks([miner, pruned_node])

        # Connect full_node to prune_node and check peers don't disconnect right away.
        # (they will disconnect if full_node, which is chain-wise behind, request blocks
        # older than NODE_NETWORK_LIMITED_MIN_BLOCKS)
        start_height_full_node = full_node.getblockcount()
        full_node.setnetworkactive(True)
        self.connect_nodes(2, 0)
        assert_equal(len(full_node.getpeerinfo()), 1)

        # Wait until the full_node is headers-wise sync
        best_block_hash = pruned_node.getbestblockhash()
        self.wait_until(lambda: next(filter(lambda x: x['hash'] == best_block_hash, full_node.getchaintips()))['status'] == "headers-only")

        # Now, since the node aims to download a window of 1024 blocks,
        # ensure it requests the blocks below the threshold only (with a
        # 2-block buffer). And also, ensure it does not request any
        # historical block.
        tip_height = pruned_node.getblockcount()
        limit_buffer = 2
        # Prevent races by waiting for the tip to arrive first
        self.wait_until(lambda: not try_rpc(-1, "Block not found", full_node.getblock, pruned_node.getbestblockhash()))
        for height in range(start_height_full_node + 1, tip_height + 1):
            if height <= tip_height - (NODE_NETWORK_LIMITED_MIN_BLOCKS - limit_buffer):
                assert_raises_rpc_error(-1, "Block not found on disk", full_node.getblock, pruned_node.getblockhash(height))
            else:
                full_node.getblock(pruned_node.getblockhash(height))  # just assert it does not throw an exception

        # Lastly, ensure the full_node is not sync and verify it can get synced by
        # establishing a connection with another full node capable of providing them.
        assert_equal(full_node.getblockcount(), start_height_full_node)
        self.connect_nodes(2, 1)
        self.sync_blocks([miner, full_node])