Repository: bitcoin/bitcoin
File: src/rpc/blockchain.cpp (C++)

Code before:
    TxVerbosity tx_verbosity;
    if (verbosity == 1) {
        tx_verbosity = TxVerbosity::SHOW_TXID;
    } else if (verbosity == 2) {
        tx_verbosity = TxVerbosity::SHOW_DETAILS;
    } else {
        tx_verbosity = TxVerbosity::SHOW_DETAILS_AND_PREVOUT;
    }

    return blockToJSON(chainman.m_blockman, block, *tip, *pblockindex, tx_verbosity);
},
    };
}

static RPCHelpMan pruneblockchain()
{
    return RPCHelpMan{"pruneblockchain", "",
                {
                    {"height", RPCArg::Type::NUM, RPCArg::Optional::NO, "The block height to prune up to. May be set to a discrete height, or to a " + UNIX_EPOCH_TIME + "\n"
            "                  to prune blocks whose block time is at least 2 hours older than the provided timestamp."},
                },
                RPCResult{
                    RPCResult::Type::NUM, "", "Height of the last block pruned"},
                RPCExamples{
                    HelpExampleCli("pruneblockchain", "1000")
            + HelpExampleRpc("pruneblockchain", "1000")
                },
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
    ChainstateManager& chainman = EnsureAnyChainman(request.context);
    if (!chainman.m_blockman.IsPruneMode()) {
        throw JSONRPCError(RPC_MISC_ERROR, "Cannot prune blocks because node is not in prune mode.");
    }

    LOCK(cs_main);
    Chainstate& active_chainstate = chainman.ActiveChainstate();
    CChain& active_chain = active_chainstate.m_chain;

    int heightParam = request.params[0].getInt<int>();
    if (heightParam < 0) {
        throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative block height.");
    }

    // Height value more than a billion is too high to be a block height, and
    // too low to be a block time (corresponds to timestamp from Sep 2001).
    if (heightParam > 1000000000) {
        // Add a 2 hour buffer to include blocks which might have had old timestamps
        const CBlockIndex* pindex = active_chain.FindEarliestAtLeast(heightParam - TIMESTAMP_WINDOW, 0);
        if (!pindex) {
            throw JSONRPCError(RPC_INVALID_PARAMETER, "Could not find block with at least the specified timestamp.");
        }

Review comment (bug):
I think this is buggy, chain tip should be considered pruned if any of the `BLOCK_HAVE_MASK` flags are missing:

```suggestion
    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
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>


Code after:
    } else if (verbosity == 2) {
        tx_verbosity = TxVerbosity::SHOW_DETAILS;
    } else {
        tx_verbosity = TxVerbosity::SHOW_DETAILS_AND_PREVOUT;
    }

    return blockToJSON(chainman.m_blockman, block, *tip, *pblockindex, tx_verbosity);
},
    };
}

//! 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", "",
                {
                    {"height", RPCArg::Type::NUM, RPCArg::Optional::NO, "The block height to prune up to. May be set to a discrete height, or to a " + UNIX_EPOCH_TIME + "\n"
            "                  to prune blocks whose block time is at least 2 hours older than the provided timestamp."},
                },
                RPCResult{
                    RPCResult::Type::NUM, "", "Height of the last block pruned"},
                RPCExamples{
                    HelpExampleCli("pruneblockchain", "1000")
            + HelpExampleRpc("pruneblockchain", "1000")
                },
        [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue