Skip to content

Commit f4849f6

Browse files
committed
Merge bitcoin/bitcoin#29668: prune, rpc: Check undo data when finding pruneheight
8789dc8 doc: Add note to getblockfrompeer on missing undo data (Fabian Jahr) 4a19750 rpc: Make pruneheight also reflect undo data presence (Fabian Jahr) 96b4fac refactor, blockstorage: Generalize GetFirstStoredBlock (Fabian Jahr) Pull request description: The function `GetFirstStoredBlock()` helps us find the first block for which we have data. So far this function only looked for a block with `BLOCK_HAVE_DATA`. However, this doesn't mean that we also have the undo data of that block, and undo data might be required for what a user would like to do with those blocks. One example of how this might happen is if some blocks were fetched using the `getblockfrompeer` RPC. Blocks fetched from a peer will have data but no undo data. The first commit here allows `GetFirstStoredBlock()` to check for undo data as well by passing a parameter. This alone is useful for #29553 and I would use it there. In the second commit I am applying the undo check to the RPCs that report `pruneheight` to the user. I find this much more intuitive because I think the user expects to be able to do all operations on blocks up until the `pruneheight` but that is not the case if undo data is missing. I personally ran into this once before and now again when testing for assumeutxo when I had used `getblockfrompeer`. The following commit adds test coverage for this change of behavior. The last commit adds a note in the docs of `getblockfrompeer` that undo data will not be available. ACKs for top commit: achow101: ACK 8789dc8 furszy: Code review ACK 8789dc8. stickies-v: ACK 8789dc8 Tree-SHA512: 90ae8bdd07a496ade579aa25240609c61c9ed173ad38d30533f6c631fe674e5a41727478ade69ca4b71a571ad94c9da4b33ebba6b5d8821109313c2de3bdfb3d
2 parents 394651f + 8789dc8 commit f4849f6

File tree

7 files changed

+120
-14
lines changed

7 files changed

+120
-14
lines changed

src/node/blockstorage.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -594,12 +594,12 @@ bool BlockManager::IsBlockPruned(const CBlockIndex& block)
594594
return m_have_pruned && !(block.nStatus & BLOCK_HAVE_DATA) && (block.nTx > 0);
595595
}
596596

597-
const CBlockIndex* BlockManager::GetFirstStoredBlock(const CBlockIndex& upper_block, const CBlockIndex* lower_block)
597+
const CBlockIndex* BlockManager::GetFirstBlock(const CBlockIndex& upper_block, uint32_t status_mask, const CBlockIndex* lower_block) const
598598
{
599599
AssertLockHeld(::cs_main);
600600
const CBlockIndex* last_block = &upper_block;
601-
assert(last_block->nStatus & BLOCK_HAVE_DATA); // 'upper_block' must have data
602-
while (last_block->pprev && (last_block->pprev->nStatus & BLOCK_HAVE_DATA)) {
601+
assert((last_block->nStatus & status_mask) == status_mask); // 'upper_block' must satisfy the status mask
602+
while (last_block->pprev && ((last_block->pprev->nStatus & status_mask) == status_mask)) {
603603
if (lower_block) {
604604
// Return if we reached the lower_block
605605
if (last_block == lower_block) return lower_block;
@@ -616,7 +616,7 @@ const CBlockIndex* BlockManager::GetFirstStoredBlock(const CBlockIndex& upper_bl
616616
bool BlockManager::CheckBlockDataAvailability(const CBlockIndex& upper_block, const CBlockIndex& lower_block)
617617
{
618618
if (!(upper_block.nStatus & BLOCK_HAVE_DATA)) return false;
619-
return GetFirstStoredBlock(upper_block, &lower_block) == &lower_block;
619+
return GetFirstBlock(upper_block, BLOCK_HAVE_DATA, &lower_block) == &lower_block;
620620
}
621621

622622
// If we're using -prune with -reindex, then delete block files that will be ignored by the

src/node/blockstorage.h

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,33 @@ class BlockManager
372372
//! (part of the same chain).
373373
bool CheckBlockDataAvailability(const CBlockIndex& upper_block LIFETIMEBOUND, const CBlockIndex& lower_block LIFETIMEBOUND) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
374374

375-
//! Find the first stored ancestor of start_block immediately after the last
376-
//! pruned ancestor. Return value will never be null. Caller is responsible
377-
//! for ensuring that start_block has data is not pruned.
378-
const CBlockIndex* GetFirstStoredBlock(const CBlockIndex& start_block LIFETIMEBOUND, const CBlockIndex* lower_block=nullptr) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
375+
/**
376+
* @brief Returns the earliest block with specified `status_mask` flags set after
377+
* the latest block _not_ having those flags.
378+
*
379+
* This function starts from `upper_block`, which must have all
380+
* `status_mask` flags set, and iterates backwards through its ancestors. It
381+
* continues as long as each block has all `status_mask` flags set, until
382+
* reaching the oldest ancestor or `lower_block`.
383+
*
384+
* @pre `upper_block` must have all `status_mask` flags set.
385+
* @pre `lower_block` must be null or an ancestor of `upper_block`
386+
*
387+
* @param upper_block The starting block for the search, which must have all
388+
* `status_mask` flags set.
389+
* @param status_mask Bitmask specifying required status flags.
390+
* @param lower_block The earliest possible block to return. If null, the
391+
* search can extend to the genesis block.
392+
*
393+
* @return A non-null pointer to the earliest block between `upper_block`
394+
* and `lower_block`, inclusive, such that every block between the
395+
* returned block and `upper_block` has `status_mask` flags set.
396+
*/
397+
const CBlockIndex* GetFirstBlock(
398+
const CBlockIndex& upper_block LIFETIMEBOUND,
399+
uint32_t status_mask,
400+
const CBlockIndex* lower_block = nullptr
401+
) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
379402

380403
/** True if any block files have ever been pruned. */
381404
bool m_have_pruned = false;

src/rpc/blockchain.cpp

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ static RPCHelpMan getblockfrompeer()
431431
"getblockfrompeer",
432432
"Attempt to fetch block from a given peer.\n\n"
433433
"We must have the header for this block, e.g. using submitheader.\n"
434+
"The block will not have any undo data which can limit the usage of the block data in a context where the undo data is needed.\n"
434435
"Subsequent calls for the same block may cause the response from the previous peer to be ignored.\n"
435436
"Peers generally ignore requests for a stale block that they never fully verified, or one that is more than a month old.\n"
436437
"When a peer does not respond with a block, we will disconnect.\n"
@@ -784,6 +785,32 @@ static RPCHelpMan getblock()
784785
};
785786
}
786787

788+
//! Return height of highest block that has been pruned, or std::nullopt if no blocks have been pruned
789+
std::optional<int> GetPruneHeight(const BlockManager& blockman, const CChain& chain) {
790+
AssertLockHeld(::cs_main);
791+
792+
// Search for the last block missing block data or undo data. Don't let the
793+
// search consider the genesis block, because the genesis block does not
794+
// have undo data, but should not be considered pruned.
795+
const CBlockIndex* first_block{chain[1]};
796+
const CBlockIndex* chain_tip{chain.Tip()};
797+
798+
// If there are no blocks after the genesis block, or no blocks at all, nothing is pruned.
799+
if (!first_block || !chain_tip) return std::nullopt;
800+
801+
// If the chain tip is pruned, everything is pruned.
802+
if (!((chain_tip->nStatus & BLOCK_HAVE_MASK) == BLOCK_HAVE_MASK)) return chain_tip->nHeight;
803+
804+
const auto& first_unpruned{*Assert(blockman.GetFirstBlock(*chain_tip, /*status_mask=*/BLOCK_HAVE_MASK, first_block))};
805+
if (&first_unpruned == first_block) {
806+
// All blocks between first_block and chain_tip have data, so nothing is pruned.
807+
return std::nullopt;
808+
}
809+
810+
// Block before the first unpruned block is the last pruned block.
811+
return Assert(first_unpruned.pprev)->nHeight;
812+
}
813+
787814
static RPCHelpMan pruneblockchain()
788815
{
789816
return RPCHelpMan{"pruneblockchain", "",
@@ -836,8 +863,7 @@ static RPCHelpMan pruneblockchain()
836863
}
837864

838865
PruneBlockFilesManual(active_chainstate, height);
839-
const CBlockIndex& block{*CHECK_NONFATAL(active_chain.Tip())};
840-
return block.nStatus & BLOCK_HAVE_DATA ? active_chainstate.m_blockman.GetFirstStoredBlock(block)->nHeight - 1 : block.nHeight;
866+
return GetPruneHeight(chainman.m_blockman, active_chain).value_or(-1);
841867
},
842868
};
843869
}
@@ -1297,8 +1323,8 @@ RPCHelpMan getblockchaininfo()
12971323
obj.pushKV("size_on_disk", chainman.m_blockman.CalculateCurrentUsage());
12981324
obj.pushKV("pruned", chainman.m_blockman.IsPruneMode());
12991325
if (chainman.m_blockman.IsPruneMode()) {
1300-
bool has_tip_data = tip.nStatus & BLOCK_HAVE_DATA;
1301-
obj.pushKV("pruneheight", has_tip_data ? chainman.m_blockman.GetFirstStoredBlock(tip)->nHeight : tip.nHeight + 1);
1326+
const auto prune_height{GetPruneHeight(chainman.m_blockman, active_chainstate.m_chain)};
1327+
obj.pushKV("pruneheight", prune_height ? prune_height.value() + 1 : 0);
13021328

13031329
const bool automatic_pruning{chainman.m_blockman.GetPruneTarget() != BlockManager::PRUNE_TARGET_MANUAL};
13041330
obj.pushKV("automatic_pruning", automatic_pruning);

src/rpc/blockchain.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class CBlockIndex;
2121
class Chainstate;
2222
class UniValue;
2323
namespace node {
24+
class BlockManager;
2425
struct NodeContext;
2526
} // namespace node
2627

@@ -57,4 +58,7 @@ UniValue CreateUTXOSnapshot(
5758
const fs::path& path,
5859
const fs::path& tmppath);
5960

61+
//! Return height of highest block that has been pruned, or std::nullopt if no blocks have been pruned
62+
std::optional<int> GetPruneHeight(const node::BlockManager& blockman, const CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
63+
6064
#endif // BITCOIN_RPC_BLOCKCHAIN_H

src/test/blockchain_tests.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
#include <boost/test/unit_test.hpp>
66

77
#include <chain.h>
8+
#include <node/blockstorage.h>
89
#include <rpc/blockchain.h>
10+
#include <sync.h>
911
#include <test/util/setup_common.h>
1012
#include <util/string.h>
1113

@@ -76,4 +78,36 @@ BOOST_AUTO_TEST_CASE(get_difficulty_for_very_high_target)
7678
TestDifficulty(0x12345678, 5913134931067755359633408.0);
7779
}
7880

81+
//! Prune chain from height down to genesis block and check that
82+
//! GetPruneHeight returns the correct value
83+
static void CheckGetPruneHeight(node::BlockManager& blockman, CChain& chain, int height) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
84+
{
85+
AssertLockHeld(::cs_main);
86+
87+
// Emulate pruning all blocks from `height` down to the genesis block
88+
// by unsetting the `BLOCK_HAVE_DATA` flag from `nStatus`
89+
for (CBlockIndex* it{chain[height]}; it != nullptr && it->nHeight > 0; it = it->pprev) {
90+
it->nStatus &= ~BLOCK_HAVE_DATA;
91+
}
92+
93+
const auto prune_height{GetPruneHeight(blockman, chain)};
94+
BOOST_REQUIRE(prune_height.has_value());
95+
BOOST_CHECK_EQUAL(*prune_height, height);
96+
}
97+
98+
BOOST_FIXTURE_TEST_CASE(get_prune_height, TestChain100Setup)
99+
{
100+
LOCK(::cs_main);
101+
auto& chain = m_node.chainman->ActiveChain();
102+
auto& blockman = m_node.chainman->m_blockman;
103+
104+
// Fresh chain of 100 blocks without any pruned blocks, so std::nullopt should be returned
105+
BOOST_CHECK(!GetPruneHeight(blockman, chain).has_value());
106+
107+
// Start pruning
108+
CheckGetPruneHeight(blockman, chain, 1);
109+
CheckGetPruneHeight(blockman, chain, 99);
110+
CheckGetPruneHeight(blockman, chain, 100);
111+
}
112+
79113
BOOST_AUTO_TEST_SUITE_END()

src/test/blockmanager_tests.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <chain.h>
56
#include <chainparams.h>
67
#include <clientversion.h>
78
#include <node/blockstorage.h>
@@ -113,7 +114,7 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_availability, TestChain100Setup)
113114
};
114115

115116
// 1) Return genesis block when all blocks are available
116-
BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), chainman->ActiveChain()[0]);
117+
BOOST_CHECK_EQUAL(blockman.GetFirstBlock(tip, BLOCK_HAVE_DATA), chainman->ActiveChain()[0]);
117118
BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *chainman->ActiveChain()[0]));
118119

119120
// 2) Check lower_block when all blocks are available
@@ -127,7 +128,7 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_availability, TestChain100Setup)
127128
func_prune_blocks(last_pruned_block);
128129

129130
// 3) The last block not pruned is in-between upper-block and the genesis block
130-
BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), first_available_block);
131+
BOOST_CHECK_EQUAL(blockman.GetFirstBlock(tip, BLOCK_HAVE_DATA), first_available_block);
131132
BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *first_available_block));
132133
BOOST_CHECK(!blockman.CheckBlockDataAvailability(tip, *last_pruned_block));
133134
}

test/functional/feature_pruning.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
assert_equal,
2626
assert_greater_than,
2727
assert_raises_rpc_error,
28+
try_rpc,
2829
)
2930

3031
# Rescans start at the earliest block up to 2 hours before a key timestamp, so
@@ -479,8 +480,12 @@ def run_test(self):
479480
self.log.info("Test invalid pruning command line options")
480481
self.test_invalid_command_line_options()
481482

483+
self.log.info("Test scanblocks can not return pruned data")
482484
self.test_scanblocks_pruned()
483485

486+
self.log.info("Test pruneheight reflects the presence of block and undo data")
487+
self.test_pruneheight_undo_presence()
488+
484489
self.log.info("Done")
485490

486491
def test_scanblocks_pruned(self):
@@ -494,5 +499,18 @@ def test_scanblocks_pruned(self):
494499
assert_raises_rpc_error(-1, "Block not available (pruned data)", node.scanblocks,
495500
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0, "basic", {"filter_false_positives": True})
496501

502+
def test_pruneheight_undo_presence(self):
503+
node = self.nodes[2]
504+
pruneheight = node.getblockchaininfo()["pruneheight"]
505+
fetch_block = node.getblockhash(pruneheight - 1)
506+
507+
self.connect_nodes(1, 2)
508+
peers = node.getpeerinfo()
509+
node.getblockfrompeer(fetch_block, peers[0]["id"])
510+
self.wait_until(lambda: not try_rpc(-1, "Block not available (pruned data)", node.getblock, fetch_block), timeout=5)
511+
512+
new_pruneheight = node.getblockchaininfo()["pruneheight"]
513+
assert_equal(pruneheight, new_pruneheight)
514+
497515
if __name__ == '__main__':
498516
PruneTest().main()

0 commit comments

Comments
 (0)