diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index fe3db4cefa..23a5edb9b2 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -755,7 +755,7 @@ void BlockManager::CleanupBlockRevFiles() const CBlockFileInfo* BlockManager::GetBlockFileInfo(size_t n) { LOCK(cs_LastBlockFile); - + if (n > m_blockfile_info.size()-1) return nullptr; return &m_blockfile_info.at(n); } diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 86a238d03b..34cf569457 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -3111,6 +3111,46 @@ return RPCHelpMan{ }; } +static RPCHelpMan getblockfileinfo() +{ + return RPCHelpMan{ + "getblockfileinfo", + "Retrieves information about a certain block file.", + { + {"file_number", RPCArg::Type::NUM, RPCArg::Optional::NO, "block file number"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "blocks_num", "the number of blocks stored in the file"}, + {RPCResult::Type::NUM, "lowest_block", "the height of the lowest block inside the file"}, + {RPCResult::Type::NUM, "highest_block", "the height of the highest block inside the file"}, + {RPCResult::Type::NUM, "data_size", "the number of used bytes in the block file"}, + {RPCResult::Type::NUM, "undo_size", "the number of used bytes in the undo file"}, + } + }, + RPCExamples{ HelpExampleCli("getblockfileinfo", "0") }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + NodeContext& node = EnsureAnyNodeContext(request.context); + + int block_num = request.params[0].getInt(); + if (block_num < 0) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid block number"); + + CBlockFileInfo* info = node.chainman->m_blockman.GetBlockFileInfo(block_num); + if (!info) throw JSONRPCError(RPC_INVALID_PARAMETER, "block file not found"); + + UniValue result(UniValue::VOBJ); + result.pushKV("blocks_num", info->nBlocks); + result.pushKV("lowest_block", info->nHeightFirst); + result.pushKV("highest_block", info->nHeightLast); + result.pushKV("data_size", info->nSize); + result.pushKV("undo_size", info->nUndoSize); + + return result; + } + }; +} + static RPCHelpMan getblocklocations() { return RPCHelpMan{"getblocklocations", @@ -3211,6 +3251,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &dumptxoutset}, {"hidden", &loadtxoutset}, {"blockchain", &getchainstates}, + {"hidden", &getblockfileinfo}, {"hidden", &invalidateblock}, {"hidden", &reconsiderblock}, {"hidden", &waitfornewblock}, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 05fb99e22a..3e126067cf 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -258,6 +258,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "verifychain", 1, "nblocks" }, { "getblockstats", 0, "hash_or_height" }, { "getblockstats", 1, "stats" }, + { "getblockfileinfo", 0, "file_number" }, { "setprunelock", 1, "lock_info" }, { "pruneblockchain", 0, "height" }, { "keypoolrefill", 0, "newsize" }, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index c3ae8015dc..261772d9b0 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -121,6 +121,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getblockfrompeer", // when no peers are connected, no p2p message is sent "getblockhash", "getblockheader", + "getblockfileinfo", "getblocklocations", "getblockstats", "getblocktemplate", diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py index 884a672178..903a776b28 100755 --- a/test/functional/rpc_getblockfrompeer.py +++ b/test/functional/rpc_getblockfrompeer.py @@ -135,10 +135,21 @@ class GetBlockFromPeerTest(BitcoinTestFramework): # We need to generate more blocks to be able to prune self.generate(self.nodes[0], 400, sync_fun=self.no_op) self.sync_blocks([self.nodes[0], pruned_node]) - pruneheight = pruned_node.pruneblockchain(300) - assert_equal(pruneheight, 248) + + # The goal now will be to mimic the automatic pruning process and verify what happens when we fetch an historic + # block at any point of time. + # + # Starting with three blocks files. The pruning process will prune them one by one. And, at the second pruning + # event, the test will fetch the past block. Which will be stored at the latest block file. Which can only be + # pruned when the latest block file is full (in this case, the third one), and a new one is created. + + # First prune event, prune first block file + highest_pruned_block_num = pruned_node.getblockfileinfo(0)["highest_block"] + pruneheight = pruned_node.pruneblockchain(highest_pruned_block_num + 1) + assert_equal(pruneheight, highest_pruned_block_num) # Ensure the block is actually pruned - pruned_block = self.nodes[0].getblockhash(2) + fetch_block_num = 2 + pruned_block = self.nodes[0].getblockhash(fetch_block_num) assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block) self.log.info("Fetch pruned block") @@ -149,18 +160,29 @@ class GetBlockFromPeerTest(BitcoinTestFramework): self.wait_until(lambda: self.check_for_block(node=2, hash=pruned_block), timeout=1) assert_equal(result, {}) + # Validate that the re-fetched block was stored at the last, current, block file + assert_equal(fetch_block_num, pruned_node.getblockfileinfo(2)["lowest_block"]) + self.log.info("Fetched block persists after next pruning event") self.generate(self.nodes[0], 250, sync_fun=self.no_op) self.sync_blocks([self.nodes[0], pruned_node]) - pruneheight += 251 - assert_equal(pruned_node.pruneblockchain(700), pruneheight) + + # Second prune event, prune second block file + highest_pruned_block_num = pruned_node.getblockfileinfo(1)["highest_block"] + pruneheight = pruned_node.pruneblockchain(highest_pruned_block_num + 1) + assert_equal(pruneheight, highest_pruned_block_num) + # As the re-fetched block is in the third file, and we just pruned the second one, 'getblock' must work. assert_equal(pruned_node.getblock(pruned_block)["hash"], "36c56c5b5ebbaf90d76b0d1a074dcb32d42abab75b7ec6fa0ffd9b4fbce8f0f7") - self.log.info("Fetched block can be pruned again when prune height exceeds the height of the tip at the time when the block was fetched") + self.log.info("Re-fetched block can be pruned again when a new block file is created") self.generate(self.nodes[0], 250, sync_fun=self.no_op) self.sync_blocks([self.nodes[0], pruned_node]) - pruneheight += 250 - assert_equal(pruned_node.pruneblockchain(1000), pruneheight) + + # Third prune event, prune third block file + highest_pruned_block_num = pruned_node.getblockfileinfo(2)["highest_block"] + pruneheight = pruned_node.pruneblockchain(highest_pruned_block_num + 1) + assert_equal(pruneheight, highest_pruned_block_num) + # and check that the re-fetched block file is now pruned assert_raises_rpc_error(-1, "Block not available (pruned data)", pruned_node.getblock, pruned_block)