From 24bfe4e1a076dee7a45f6cfc22bf623e778a7287 Mon Sep 17 00:00:00 2001 From: furszy Date: Fri, 26 May 2023 17:44:10 -0300 Subject: [PATCH 1/2] RPC: introduce 'getblockfileinfo' RPC command Simple command to retrieve information about a certain block file --- src/node/blockstorage.cpp | 2 +- src/rpc/blockchain.cpp | 40 +++++++++++++++++++++++++++++++++++++++ src/rpc/client.cpp | 1 + src/test/fuzz/rpc.cpp | 1 + 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index f8f1aab551..75abfae8e7 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -644,7 +644,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 6d2b84cb6c..8a8a96d007 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2869,6 +2869,45 @@ 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; + } + }; +} void RegisterBlockchainRPCCommands(CRPCTable& t) { @@ -2896,6 +2935,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &dumptxoutset}, {"blockchain", &loadtxoutset}, {"blockchain", &getchainstates}, + {"hidden", &getblockfileinfo}, {"hidden", &invalidateblock}, {"hidden", &reconsiderblock}, {"hidden", &waitfornewblock}, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 49820f25a3..865d215ce6 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -242,6 +242,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "verifychain", 1, "nblocks" }, { "getblockstats", 0, "hash_or_height" }, { "getblockstats", 1, "stats" }, + { "getblockfileinfo", 0, "file_number" }, { "pruneblockchain", 0, "height" }, { "keypoolrefill", 0, "newsize" }, { "getrawmempool", 0, "verbose" }, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 270cab58e2..e428237abf 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -120,6 +120,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getblockfrompeer", // when no peers are connected, no p2p message is sent "getblockhash", "getblockheader", + "getblockfileinfo", "getblockstats", "getblocktemplate", "getchaintips", From 5090771f326fcccdd3ea91b7fbc86f0db1561bf8 Mon Sep 17 00:00:00 2001 From: furszy Date: Fri, 26 May 2023 17:58:14 -0300 Subject: [PATCH 2/2] test: rpc_getblockfrompeer.py, remove magic numbers usage Instead of hardcoding the `pruneblockchain()` heights, use 'getblockfileinfo' to obtain the highest block number of each of the block files. Making the test more robust and readable by stating which file is being pruned at every point of time (the goal is to mimic how the automatic pruning process work). --- test/functional/rpc_getblockfrompeer.py | 38 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py index 1ab1023cf1..9f48b6da51 100755 --- a/test/functional/rpc_getblockfrompeer.py +++ b/test/functional/rpc_getblockfrompeer.py @@ -124,10 +124,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") @@ -138,18 +149,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)