Skip to content

Commit

Permalink
Merge bitcoin#29668: prune, rpc: Check undo data when finding prunehe…
Browse files Browse the repository at this point in the history
…ight

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 bitcoin#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
  • Loading branch information
achow101 committed Jul 10, 2024
2 parents 394651f + 8789dc8 commit f4849f6
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 14 deletions.
8 changes: 4 additions & 4 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -594,12 +594,12 @@ bool BlockManager::IsBlockPruned(const CBlockIndex& block)
return m_have_pruned && !(block.nStatus & BLOCK_HAVE_DATA) && (block.nTx > 0);
}

const CBlockIndex* BlockManager::GetFirstStoredBlock(const CBlockIndex& upper_block, const CBlockIndex* lower_block)
const CBlockIndex* BlockManager::GetFirstBlock(const CBlockIndex& upper_block, uint32_t status_mask, const CBlockIndex* lower_block) const
{
AssertLockHeld(::cs_main);
const CBlockIndex* last_block = &upper_block;
assert(last_block->nStatus & BLOCK_HAVE_DATA); // 'upper_block' must have data
while (last_block->pprev && (last_block->pprev->nStatus & BLOCK_HAVE_DATA)) {
assert((last_block->nStatus & status_mask) == status_mask); // 'upper_block' must satisfy the status mask
while (last_block->pprev && ((last_block->pprev->nStatus & status_mask) == status_mask)) {
if (lower_block) {
// Return if we reached the lower_block
if (last_block == lower_block) return lower_block;
Expand All @@ -616,7 +616,7 @@ const CBlockIndex* BlockManager::GetFirstStoredBlock(const CBlockIndex& upper_bl
bool BlockManager::CheckBlockDataAvailability(const CBlockIndex& upper_block, const CBlockIndex& lower_block)
{
if (!(upper_block.nStatus & BLOCK_HAVE_DATA)) return false;
return GetFirstStoredBlock(upper_block, &lower_block) == &lower_block;
return GetFirstBlock(upper_block, BLOCK_HAVE_DATA, &lower_block) == &lower_block;
}

// If we're using -prune with -reindex, then delete block files that will be ignored by the
Expand Down
31 changes: 27 additions & 4 deletions src/node/blockstorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,33 @@ class BlockManager
//! (part of the same chain).
bool CheckBlockDataAvailability(const CBlockIndex& upper_block LIFETIMEBOUND, const CBlockIndex& lower_block LIFETIMEBOUND) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

//! Find the first stored ancestor of start_block immediately after the last
//! pruned ancestor. Return value will never be null. Caller is responsible
//! for ensuring that start_block has data is not pruned.
const CBlockIndex* GetFirstStoredBlock(const CBlockIndex& start_block LIFETIMEBOUND, const CBlockIndex* lower_block=nullptr) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
/**
* @brief Returns the earliest block with specified `status_mask` flags set after
* the latest block _not_ having those flags.
*
* This function starts from `upper_block`, which must have all
* `status_mask` flags set, and iterates backwards through its ancestors. It
* continues as long as each block has all `status_mask` flags set, until
* reaching the oldest ancestor or `lower_block`.
*
* @pre `upper_block` must have all `status_mask` flags set.
* @pre `lower_block` must be null or an ancestor of `upper_block`
*
* @param upper_block The starting block for the search, which must have all
* `status_mask` flags set.
* @param status_mask Bitmask specifying required status flags.
* @param lower_block The earliest possible block to return. If null, the
* search can extend to the genesis block.
*
* @return A non-null pointer to the earliest block between `upper_block`
* and `lower_block`, inclusive, such that every block between the
* returned block and `upper_block` has `status_mask` flags set.
*/
const CBlockIndex* GetFirstBlock(
const CBlockIndex& upper_block LIFETIMEBOUND,
uint32_t status_mask,
const CBlockIndex* lower_block = nullptr
) const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/** True if any block files have ever been pruned. */
bool m_have_pruned = false;
Expand Down
34 changes: 30 additions & 4 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ static RPCHelpMan getblockfrompeer()
"getblockfrompeer",
"Attempt to fetch block from a given peer.\n\n"
"We must have the header for this block, e.g. using submitheader.\n"
"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"
"Subsequent calls for the same block may cause the response from the previous peer to be ignored.\n"
"Peers generally ignore requests for a stale block that they never fully verified, or one that is more than a month old.\n"
"When a peer does not respond with a block, we will disconnect.\n"
Expand Down Expand Up @@ -784,6 +785,32 @@ static RPCHelpMan getblock()
};
}

//! Return height of highest block that has been pruned, or std::nullopt if no blocks have been pruned
std::optional<int> GetPruneHeight(const BlockManager& blockman, const CChain& chain) {
AssertLockHeld(::cs_main);

// Search for the last block missing block data or undo data. Don't let the
// search consider the genesis block, because the genesis block does not
// have undo data, but should not be considered pruned.
const CBlockIndex* first_block{chain[1]};
const CBlockIndex* chain_tip{chain.Tip()};

// If there are no blocks after the genesis block, or no blocks at all, nothing is pruned.
if (!first_block || !chain_tip) return std::nullopt;

// If the chain tip is pruned, everything is pruned.
if (!((chain_tip->nStatus & BLOCK_HAVE_MASK) == BLOCK_HAVE_MASK)) return chain_tip->nHeight;

const auto& first_unpruned{*Assert(blockman.GetFirstBlock(*chain_tip, /*status_mask=*/BLOCK_HAVE_MASK, first_block))};
if (&first_unpruned == first_block) {
// All blocks between first_block and chain_tip have data, so nothing is pruned.
return std::nullopt;
}

// Block before the first unpruned block is the last pruned block.
return Assert(first_unpruned.pprev)->nHeight;
}

static RPCHelpMan pruneblockchain()
{
return RPCHelpMan{"pruneblockchain", "",
Expand Down Expand Up @@ -836,8 +863,7 @@ static RPCHelpMan pruneblockchain()
}

PruneBlockFilesManual(active_chainstate, height);
const CBlockIndex& block{*CHECK_NONFATAL(active_chain.Tip())};
return block.nStatus & BLOCK_HAVE_DATA ? active_chainstate.m_blockman.GetFirstStoredBlock(block)->nHeight - 1 : block.nHeight;
return GetPruneHeight(chainman.m_blockman, active_chain).value_or(-1);
},
};
}
Expand Down Expand Up @@ -1297,8 +1323,8 @@ RPCHelpMan getblockchaininfo()
obj.pushKV("size_on_disk", chainman.m_blockman.CalculateCurrentUsage());
obj.pushKV("pruned", chainman.m_blockman.IsPruneMode());
if (chainman.m_blockman.IsPruneMode()) {
bool has_tip_data = tip.nStatus & BLOCK_HAVE_DATA;
obj.pushKV("pruneheight", has_tip_data ? chainman.m_blockman.GetFirstStoredBlock(tip)->nHeight : tip.nHeight + 1);
const auto prune_height{GetPruneHeight(chainman.m_blockman, active_chainstate.m_chain)};
obj.pushKV("pruneheight", prune_height ? prune_height.value() + 1 : 0);

const bool automatic_pruning{chainman.m_blockman.GetPruneTarget() != BlockManager::PRUNE_TARGET_MANUAL};
obj.pushKV("automatic_pruning", automatic_pruning);
Expand Down
4 changes: 4 additions & 0 deletions src/rpc/blockchain.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class CBlockIndex;
class Chainstate;
class UniValue;
namespace node {
class BlockManager;
struct NodeContext;
} // namespace node

Expand Down Expand Up @@ -57,4 +58,7 @@ UniValue CreateUTXOSnapshot(
const fs::path& path,
const fs::path& tmppath);

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

#endif // BITCOIN_RPC_BLOCKCHAIN_H
34 changes: 34 additions & 0 deletions src/test/blockchain_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#include <boost/test/unit_test.hpp>

#include <chain.h>
#include <node/blockstorage.h>
#include <rpc/blockchain.h>
#include <sync.h>
#include <test/util/setup_common.h>
#include <util/string.h>

Expand Down Expand Up @@ -76,4 +78,36 @@ BOOST_AUTO_TEST_CASE(get_difficulty_for_very_high_target)
TestDifficulty(0x12345678, 5913134931067755359633408.0);
}

//! Prune chain from height down to genesis block and check that
//! GetPruneHeight returns the correct value
static void CheckGetPruneHeight(node::BlockManager& blockman, CChain& chain, int height) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
{
AssertLockHeld(::cs_main);

// Emulate pruning all blocks from `height` down to the genesis block
// by unsetting the `BLOCK_HAVE_DATA` flag from `nStatus`
for (CBlockIndex* it{chain[height]}; it != nullptr && it->nHeight > 0; it = it->pprev) {
it->nStatus &= ~BLOCK_HAVE_DATA;
}

const auto prune_height{GetPruneHeight(blockman, chain)};
BOOST_REQUIRE(prune_height.has_value());
BOOST_CHECK_EQUAL(*prune_height, height);
}

BOOST_FIXTURE_TEST_CASE(get_prune_height, TestChain100Setup)
{
LOCK(::cs_main);
auto& chain = m_node.chainman->ActiveChain();
auto& blockman = m_node.chainman->m_blockman;

// Fresh chain of 100 blocks without any pruned blocks, so std::nullopt should be returned
BOOST_CHECK(!GetPruneHeight(blockman, chain).has_value());

// Start pruning
CheckGetPruneHeight(blockman, chain, 1);
CheckGetPruneHeight(blockman, chain, 99);
CheckGetPruneHeight(blockman, chain, 100);
}

BOOST_AUTO_TEST_SUITE_END()
5 changes: 3 additions & 2 deletions src/test/blockmanager_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <chain.h>
#include <chainparams.h>
#include <clientversion.h>
#include <node/blockstorage.h>
Expand Down Expand Up @@ -113,7 +114,7 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_block_data_availability, TestChain100Setup)
};

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

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

// 3) The last block not pruned is in-between upper-block and the genesis block
BOOST_CHECK_EQUAL(blockman.GetFirstStoredBlock(tip), first_available_block);
BOOST_CHECK_EQUAL(blockman.GetFirstBlock(tip, BLOCK_HAVE_DATA), first_available_block);
BOOST_CHECK(blockman.CheckBlockDataAvailability(tip, *first_available_block));
BOOST_CHECK(!blockman.CheckBlockDataAvailability(tip, *last_pruned_block));
}
Expand Down
18 changes: 18 additions & 0 deletions test/functional/feature_pruning.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
assert_equal,
assert_greater_than,
assert_raises_rpc_error,
try_rpc,
)

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

self.log.info("Test scanblocks can not return pruned data")
self.test_scanblocks_pruned()

self.log.info("Test pruneheight reflects the presence of block and undo data")
self.test_pruneheight_undo_presence()

self.log.info("Done")

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

def test_pruneheight_undo_presence(self):
node = self.nodes[2]
pruneheight = node.getblockchaininfo()["pruneheight"]
fetch_block = node.getblockhash(pruneheight - 1)

self.connect_nodes(1, 2)
peers = node.getpeerinfo()
node.getblockfrompeer(fetch_block, peers[0]["id"])
self.wait_until(lambda: not try_rpc(-1, "Block not available (pruned data)", node.getblock, fetch_block), timeout=5)

new_pruneheight = node.getblockchaininfo()["pruneheight"]
assert_equal(pruneheight, new_pruneheight)

if __name__ == '__main__':
PruneTest().main()

0 comments on commit f4849f6

Please sign in to comment.