I think this is buggy, chain tip should be considered pruned if any of the BLOCK_HAVE_MASK flags are missing:
if (!((chain_tip->nStatus & BLOCK_HAVE_MASK) == BLOCK_HAVE_MASK)) return chain_tip->nHeight;
Can be verified by adding unit test to the first commit, which passes on dca1ca1f56 and fails on current HEAD for CheckGetPruneHeight(blockman, chain, 100):
$ ./src/test/test_bitcoin --run_test=blockchain_tests/get_prune_height
...
Running 1 test case...
Assertion failed: ((last_block->nStatus & status_mask) == status_mask), function GetFirstBlock, file blockstorage.cpp, line 602.
unknown location:0: fatal error: in "blockchain_tests/get_prune_height": signal: SIGABRT (application abort requested)
test/blockchain_tests.cpp:93: last checkpoint
<details>
<summary>git diff on dca1ca1f56</summary>
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index 90cee9901d..b7c1bc9673 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -783,7 +783,7 @@ static RPCHelpMan getblock()
}
//! Return height of highest block that has been pruned, or std::nullopt if no blocks have been pruned
-static std::optional<int> GetPruneHeight(BlockManager& blockman, const CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
+std::optional<int> GetPruneHeight(BlockManager& blockman, const CChain& chain) {
AssertLockHeld(::cs_main);
const CBlockIndex* chain_tip{chain.Tip()};
diff --git a/src/rpc/blockchain.h b/src/rpc/blockchain.h
index c2021c3608..d9bf627df5 100644
--- a/src/rpc/blockchain.h
+++ b/src/rpc/blockchain.h
@@ -21,6 +21,7 @@ class CBlockIndex;
class Chainstate;
class UniValue;
namespace node {
+class BlockManager;
struct NodeContext;
} // namespace node
@@ -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(node::BlockManager& blockman, const CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
+
#endif // BITCOIN_RPC_BLOCKCHAIN_H
diff --git a/src/test/blockchain_tests.cpp b/src/test/blockchain_tests.cpp
index be515a9eac..2a63d09795 100644
--- a/src/test/blockchain_tests.cpp
+++ b/src/test/blockchain_tests.cpp
@@ -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>
@@ -74,4 +76,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()
</details>