From 7fa851fba8570ef256317f7d5759aa3de9088bf1 Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Fri, 31 Dec 2021 16:25:35 +0100 Subject: [PATCH 1/2] rpc: Pruned nodes can not fetch unsynced blocks While a node is still catching up to the tip that it is aware of via the headers, the user can currently use to fetch blocks close to the tip. These blocks are stored in the current block/rev file which otherwise contains blocks the node is receiving as part of the syncing process. This creates a problem for pruned nodes: The files containing a fetched block are not pruned during syncing because they contain a block close to the tip. This means the entire file will not be pruned until the tip have moved on far enough from the fetched block. In extreme cases with heavy pruning (550) and multiple blocks being fetched this could mean that the disc usage far exceeds what the user expects, potentially running out of space. --- src/rpc/blockchain.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index f2186c131fd..4fbed3da60b 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -453,6 +453,12 @@ static RPCHelpMan getblockfrompeer() throw JSONRPCError(RPC_MISC_ERROR, "Block header missing"); } + // Fetching blocks before the node has syncing past their height can prevent block files from + // being pruned, so we avoid it if the node is in prune mode. + if (index->nHeight > chainman.ActiveChain().Tip()->nHeight && node::fPruneMode) { + throw JSONRPCError(RPC_MISC_ERROR, "In prune mode, only blocks that the node has already synced previously can be fetched from a peer"); + } + const bool block_has_data = WITH_LOCK(::cs_main, return index->nStatus & BLOCK_HAVE_DATA); if (block_has_data) { throw JSONRPCError(RPC_MISC_ERROR, "Block already downloaded"); From 5826bf546e83478947edbdf49978414f0b69eb1a Mon Sep 17 00:00:00 2001 From: Fabian Jahr Date: Sun, 2 Jan 2022 17:04:58 +0100 Subject: [PATCH 2/2] test: Add test for getblockfrompeer on syncing pruned nodes --- test/functional/rpc_getblockfrompeer.py | 32 ++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py index a7628b55915..8986b65eace 100755 --- a/test/functional/rpc_getblockfrompeer.py +++ b/test/functional/rpc_getblockfrompeer.py @@ -5,7 +5,12 @@ """Test the getblockfrompeer RPC.""" from test_framework.authproxy import JSONRPCException -from test_framework.messages import NODE_WITNESS +from test_framework.messages import ( + CBlock, + from_hex, + msg_headers, + NODE_WITNESS, +) from test_framework.p2p import ( P2P_SERVICES, P2PInterface, @@ -16,6 +21,7 @@ from test_framework.util import ( assert_raises_rpc_error, ) + class GetBlockFromPeerTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 2 @@ -78,6 +84,30 @@ class GetBlockFromPeerTest(BitcoinTestFramework): self.log.info("Don't fetch blocks we already have") assert_raises_rpc_error(-1, "Block already downloaded", self.nodes[0].getblockfrompeer, short_tip, peer_0_peer_1_id) + self.log.info("Don't fetch blocks while the node has not synced past it yet") + # For this test we need node 1 in prune mode and as a side effect this also disconnects + # the nodes which is also necessary for the rest of the test. + self.restart_node(1, ["-prune=550"]) + + # Generate a block on the disconnected node that the pruning node is not connected to + blockhash = self.generate(self.nodes[0], 1, sync_fun=self.no_op)[0] + block_hex = self.nodes[0].getblock(blockhash=blockhash, verbosity=0) + block = from_hex(CBlock(), block_hex) + + # Connect a P2PInterface to the pruning node and have it submit only the header of the + # block that the pruning node has not seen + node1_interface = self.nodes[1].add_p2p_connection(P2PInterface()) + node1_interface.send_message(msg_headers([block])) + + # Get the peer id of the P2PInterface from the pruning node + node1_peers = self.nodes[1].getpeerinfo() + assert_equal(len(node1_peers), 1) + node1_interface_id = node1_peers[0]["id"] + + # Trying to fetch this block from the P2PInterface should not be possible + error_msg = "In prune mode, only blocks that the node has already synced previously can be fetched from a peer" + assert_raises_rpc_error(-1, error_msg, self.nodes[1].getblockfrompeer, blockhash, node1_interface_id) + if __name__ == '__main__': GetBlockFromPeerTest().main()