Merge bitcoin/bitcoin#24539: Add a "tx output spender" index

0b96b9c600 Minimize mempool lock, sync txo spender index only when and if needed (sstone)
3d82ec5bdd Add a "tx output spender" index (sstone)

Pull request description:

  This PR adds a new "tx output spender" index, which allows users to query which tx spent a given outpoint with the `gettxspendingprevout` RPC call that was added by https://github.com/bitcoin/bitcoin/pull/24408.

  Such an index would be extremely useful for Lightning, and probably for most layer-2 protocols that rely on chains of unpublished transactions.

  UPDATE: this PR is ready for review and issues have been addressed:
  - using a watch-only wallet instead would not work if there is a significant number of outpoints to watch (see https://github.com/bitcoin/bitcoin/pull/24539#issuecomment-1276595646)
  - this PR does not require `-txindex` anymore

  We use a composite key with 2 parts (suggested by romanz): hash(spent outpoint) and tx position, with an empty value. Average composite key size is 15 bytes.

  The spending tx can optionally be returned by `gettxspendingprevout` (even it `-txindex is not set`).

ACKs for top commit:
  hodlinator:
    re-ACK 0b96b9c600
  sedited:
    Re-ACK 0b96b9c600
  fjahr:
    ACK 0b96b9c600
  w0xlt:
    reACK 0b96b9c600

Tree-SHA512: 95c2c313ef4086e7d5bf1cf1a3c7b91cfe2bb1a0dcb4c9d3aa8a6e5bfde66aaca48d85a1f1251a780523c3e4356ec8a97fe6f5c7145bc6ccb6f820b26716ae01
This commit is contained in:
merge-script
2026-02-20 09:27:17 +01:00
18 changed files with 648 additions and 121 deletions

View File

@@ -27,6 +27,7 @@
#include <index/blockfilterindex.h>
#include <index/coinstatsindex.h>
#include <index/txindex.h>
#include <index/txospenderindex.h>
#include <init/common.h>
#include <interfaces/chain.h>
#include <interfaces/init.h>
@@ -364,6 +365,7 @@ void Shutdown(NodeContext& node)
// Stop and delete all indexes only after flushing background callbacks.
for (auto* index : node.indexes) index->Stop();
if (g_txindex) g_txindex.reset();
if (g_txospenderindex) g_txospenderindex.reset();
if (g_coin_stats_index) g_coin_stats_index.reset();
DestroyAllBlockFilterIndexes();
node.indexes.clear(); // all instances are nullptr now
@@ -528,6 +530,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
argsman.AddArg("-shutdownnotify=<cmd>", "Execute command immediately before beginning shutdown. The need for shutdown may be urgent, so be careful not to delay it long (if the command doesn't require interaction with the server, consider having it fork into the background).", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#endif
argsman.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-txospenderindex", strprintf("Maintain a transaction output spender index, used by the gettxspendingprevout rpc call (default: %u)", DEFAULT_TXOSPENDERINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-blockfilterindex=<type>",
strprintf("Maintain an index of compact filters by block (default: %s, values: %s).", DEFAULT_BLOCKFILTERINDEX, ListBlockFilterTypes()) +
" If <type> is not supplied or if <type> = 1, indexes for all known types are enabled.",
@@ -998,6 +1001,8 @@ bool AppInitParameterInteraction(const ArgsManager& args)
if (args.GetIntArg("-prune", 0)) {
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX))
return InitError(_("Prune mode is incompatible with -txindex."));
if (args.GetBoolArg("-txospenderindex", DEFAULT_TXOSPENDERINDEX))
return InitError(_("Prune mode is incompatible with -txospenderindex."));
if (args.GetBoolArg("-reindex-chainstate", false)) {
return InitError(_("Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead."));
}
@@ -1824,6 +1829,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
LogInfo("* Using %.1f MiB for transaction index database", index_cache_sizes.tx_index * (1.0 / 1024 / 1024));
}
if (args.GetBoolArg("-txospenderindex", DEFAULT_TXOSPENDERINDEX)) {
LogInfo("* Using %.1f MiB for transaction output spender index database", index_cache_sizes.txospender_index * (1.0 / 1024 / 1024));
}
for (BlockFilterType filter_type : g_enabled_filter_types) {
LogInfo("* Using %.1f MiB for %s block filter index database",
index_cache_sizes.filter_index * (1.0 / 1024 / 1024), BlockFilterTypeName(filter_type));
@@ -1892,6 +1900,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
node.indexes.emplace_back(g_txindex.get());
}
if (args.GetBoolArg("-txospenderindex", DEFAULT_TXOSPENDERINDEX)) {
g_txospenderindex = std::make_unique<TxoSpenderIndex>(interfaces::MakeChain(node), index_cache_sizes.txospender_index, false, do_reindex);
node.indexes.emplace_back(g_txospenderindex.get());
}
for (const auto& filter_type : g_enabled_filter_types) {
InitBlockFilterIndex([&]{ return interfaces::MakeChain(node); }, filter_type, index_cache_sizes.filter_index, false, do_reindex);
node.indexes.emplace_back(GetBlockFilterIndex(filter_type));