Merge bitcoin/bitcoin#23927: rpc: Pruning nodes can not fetch blocks before syncing past their height

5826bf546e test: Add test for getblockfrompeer on syncing pruned nodes (Fabian Jahr)
7fa851fba8 rpc: Pruned nodes can not fetch unsynced blocks (Fabian Jahr)

Pull request description:

  This PR prevents `getblockfrompeer` from getting used on blocks that the node has not synced past yet if the node is in running in prune mode.

  ### Problem

  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 or at the tip. These blocks are stored in the block/rev file that 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 (~130MB) will not be pruned until the tip has moved on far enough from the fetched block. In extreme cases with heavy pruning (like 550) and multiple blocks being fetched this could mean that the disc usage far exceeds what the user expects, potentially running out of space.

  ### Approach

  There would be certainly other approaches that could fix the problem while still allowing the current behavior, but all of the ideas I came up with seemed like overkill for a niche problem on a new RPC where it's still unclear how and how much it will be used.

  ### Testing

  So far I did not see a simple enough way to test this I am still looking into it and if it's complex will potentially add it in a follow-up. What would be needed is a way to have a node fetch headers but not sync the blocks yet, that seems like a pattern that could be generally useful.

  To manually reproduce the problematic behavior:
  1. Start a node with current `master` with `-prune=550` and an empty/new datadir, Testnet and Mainnet should both work.
  2. While the node is syncing run `getblockfrompeer` on the current tip and a few other recent blocks.
  3. Go to your datadir and observe the blocks folder: There should be a few full `blk*.dat` and `rev*.dat` files that are not being pruned. When you "pinned" a few of these files the blocks folder should be significantly above the target size of 550MB.

ACKs for top commit:
  Sjors:
    utACK 5826bf546e
  achow101:
    ACK 5826bf546e
  aureleoules:
    tACK 5826bf546e

Tree-SHA512: aa3f477ec755a9df2331c047cb10b3cd08292522bf6ad7a36a7ea36d7eba4894b84de8bd23003c9baea5ac0c53b77142c3c2819ae7528cece9d10a0d06c850d8
pull/26395/head
Andrew Chow 2 years ago
commit 88502ecf08
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41

@ -459,6 +459,12 @@ static RPCHelpMan getblockfrompeer()
throw JSONRPCError(RPC_MISC_ERROR, "Block header missing"); 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); const bool block_has_data = WITH_LOCK(::cs_main, return index->nStatus & BLOCK_HAVE_DATA);
if (block_has_data) { if (block_has_data) {
throw JSONRPCError(RPC_MISC_ERROR, "Block already downloaded"); throw JSONRPCError(RPC_MISC_ERROR, "Block already downloaded");

@ -5,7 +5,12 @@
"""Test the getblockfrompeer RPC.""" """Test the getblockfrompeer RPC."""
from test_framework.authproxy import JSONRPCException 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 ( from test_framework.p2p import (
P2P_SERVICES, P2P_SERVICES,
P2PInterface, P2PInterface,
@ -16,6 +21,7 @@ from test_framework.util import (
assert_raises_rpc_error, assert_raises_rpc_error,
) )
class GetBlockFromPeerTest(BitcoinTestFramework): class GetBlockFromPeerTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.num_nodes = 2 self.num_nodes = 2
@ -81,6 +87,30 @@ class GetBlockFromPeerTest(BitcoinTestFramework):
self.log.info("Don't fetch blocks we already have") 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) 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__': if __name__ == '__main__':
GetBlockFromPeerTest().main() GetBlockFromPeerTest().main()

Loading…
Cancel
Save