diff --git a/doc/files.md b/doc/files.md
index 12c6cefbc6d..27fe5615ddd 100644
--- a/doc/files.md
+++ b/doc/files.md
@@ -55,6 +55,7 @@ Subdirectory | File(s) | Description
`blocks/` | `xor.dat` | Rolling XOR pattern for block and undo data files
`chainstate/` | LevelDB database | Blockchain state (a compact representation of all currently unspent transaction outputs (UTXOs) and metadata about the transactions they are from)
`indexes/txindex/` | LevelDB database | Transaction index; *optional*, used if `-txindex=1`
+`indexes/txospenderindex/` | LevelDB database | Transaction spender index; *optional*, used if `-txospenderindex=1`
`indexes/blockfilter/basic/db/` | LevelDB database | Blockfilter index LevelDB database for the basic filtertype; *optional*, used if `-blockfilterindex=basic`
`indexes/blockfilter/basic/` | `fltrNNNNN.dat`[\[2\]](#note2) | Blockfilter index filters for the basic filtertype; *optional*, used if `-blockfilterindex=basic`
`indexes/coinstatsindex/db/` | LevelDB database | Coinstats index; *optional*, used if `-coinstatsindex=1`
diff --git a/doc/release-notes-24539.md b/doc/release-notes-24539.md
new file mode 100644
index 00000000000..63b4d70eafd
--- /dev/null
+++ b/doc/release-notes-24539.md
@@ -0,0 +1,14 @@
+New settings
+------------
+- `-txospenderindex` enables the creation of a transaction output spender
+ index that, if present, will be scanned by `gettxspendingprevout` if a
+ spending transaction was not found in the mempool.
+ (#24539)
+
+Updated RPCs
+------------
+- `gettxspendingprevout` has 2 new optional arguments: `mempool_only` and `return_spending_tx`.
+ If `mempool_only` is true it will limit scans to the mempool even if `txospenderindex` is available.
+ If `return_spending_tx` is true, the full spending tx will be returned.
+ In addition if `txospenderindex` is available and a confirmed spending transaction is found,
+ its block hash will be returned. (#24539)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 2d64ee88fb4..ad18115bbc5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -193,6 +193,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
index/blockfilterindex.cpp
index/coinstatsindex.cpp
index/txindex.cpp
+ index/txospenderindex.cpp
init.cpp
kernel/chain.cpp
kernel/checks.cpp
diff --git a/src/index/txospenderindex.cpp b/src/index/txospenderindex.cpp
new file mode 100644
index 00000000000..d451bb1e0a4
--- /dev/null
+++ b/src/index/txospenderindex.cpp
@@ -0,0 +1,184 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* The database is used to find the spending transaction of a given utxo.
+ * For every input of every transaction it stores a key that is a pair(siphash(input outpoint), transaction location on disk) and an empty value.
+ * To find the spending transaction of an outpoint, we perform a range query on siphash(outpoint), and for each returned key load the transaction
+ * and return it if it does spend the provided outpoint.
+ */
+
+// LevelDB key prefix. We only have one key for now but it will make it easier to add others if needed.
+constexpr uint8_t DB_TXOSPENDERINDEX{'s'};
+
+std::unique_ptr g_txospenderindex;
+
+struct DBKey {
+ uint64_t hash;
+ CDiskTxPos pos;
+
+ explicit DBKey(const uint64_t& hash_in, const CDiskTxPos& pos_in) : hash(hash_in), pos(pos_in) {}
+
+ SERIALIZE_METHODS(DBKey, obj)
+ {
+ uint8_t prefix{DB_TXOSPENDERINDEX};
+ READWRITE(prefix);
+ if (prefix != DB_TXOSPENDERINDEX) {
+ throw std::ios_base::failure("Invalid format for spender index DB key");
+ }
+ READWRITE(obj.hash);
+ READWRITE(obj.pos);
+ }
+};
+
+TxoSpenderIndex::TxoSpenderIndex(std::unique_ptr chain, size_t n_cache_size, bool f_memory, bool f_wipe)
+ : BaseIndex(std::move(chain), "txospenderindex"), m_db{std::make_unique(gArgs.GetDataDirNet() / "indexes" / "txospenderindex" / "db", n_cache_size, f_memory, f_wipe)}
+{
+ if (!m_db->Read("siphash_key", m_siphash_key)) {
+ FastRandomContext rng(false);
+ m_siphash_key = {rng.rand64(), rng.rand64()};
+ m_db->Write("siphash_key", m_siphash_key, /*fSync=*/ true);
+ }
+}
+
+interfaces::Chain::NotifyOptions TxoSpenderIndex::CustomOptions()
+{
+ interfaces::Chain::NotifyOptions options;
+ options.disconnect_data = true;
+ return options;
+}
+
+static uint64_t CreateKeyPrefix(std::pair siphash_key, const COutPoint& vout)
+{
+ return PresaltedSipHasher(siphash_key.first, siphash_key.second)(vout.hash.ToUint256(), vout.n);
+}
+
+static DBKey CreateKey(std::pair siphash_key, const COutPoint& vout, const CDiskTxPos& pos)
+{
+ return DBKey(CreateKeyPrefix(siphash_key, vout), pos);
+}
+
+void TxoSpenderIndex::WriteSpenderInfos(const std::vector>& items)
+{
+ CDBBatch batch(*m_db);
+ for (const auto& [outpoint, pos] : items) {
+ DBKey key(CreateKey(m_siphash_key, outpoint, pos));
+ // key is hash(spent outpoint) | disk pos, value is empty
+ batch.Write(key, "");
+ }
+ m_db->WriteBatch(batch);
+}
+
+
+void TxoSpenderIndex::EraseSpenderInfos(const std::vector>& items)
+{
+ CDBBatch batch(*m_db);
+ for (const auto& [outpoint, pos] : items) {
+ batch.Erase(CreateKey(m_siphash_key, outpoint, pos));
+ }
+ m_db->WriteBatch(batch);
+}
+
+static std::vector> BuildSpenderPositions(const interfaces::BlockInfo& block)
+{
+ std::vector> items;
+ items.reserve(block.data->vtx.size());
+
+ CDiskTxPos pos({block.file_number, block.data_pos}, GetSizeOfCompactSize(block.data->vtx.size()));
+ for (const auto& tx : block.data->vtx) {
+ if (!tx->IsCoinBase()) {
+ for (const auto& input : tx->vin) {
+ items.emplace_back(input.prevout, pos);
+ }
+ }
+ pos.nTxOffset += ::GetSerializeSize(TX_WITH_WITNESS(*tx));
+ }
+
+ return items;
+}
+
+
+bool TxoSpenderIndex::CustomAppend(const interfaces::BlockInfo& block)
+{
+ WriteSpenderInfos(BuildSpenderPositions(block));
+ return true;
+}
+
+bool TxoSpenderIndex::CustomRemove(const interfaces::BlockInfo& block)
+{
+ EraseSpenderInfos(BuildSpenderPositions(block));
+ return true;
+}
+
+util::Expected TxoSpenderIndex::ReadTransaction(const CDiskTxPos& tx_pos) const
+{
+ AutoFile file{m_chainstate->m_blockman.OpenBlockFile(tx_pos, /*fReadOnly=*/true)};
+ if (file.IsNull()) {
+ return util::Unexpected("cannot open block");
+ }
+ CBlockHeader header;
+ TxoSpender spender;
+ try {
+ file >> header;
+ file.seek(tx_pos.nTxOffset, SEEK_CUR);
+ file >> TX_WITH_WITNESS(spender.tx);
+ spender.block_hash = header.GetHash();
+ return spender;
+ } catch (const std::exception& e) {
+ return util::Unexpected(e.what());
+ }
+}
+
+util::Expected, std::string> TxoSpenderIndex::FindSpender(const COutPoint& txo) const
+{
+ const uint64_t prefix{CreateKeyPrefix(m_siphash_key, txo)};
+ std::unique_ptr it(m_db->NewIterator());
+ DBKey key(prefix, CDiskTxPos());
+
+ // find all keys that start with the outpoint hash, load the transaction at the location specified in the key
+ // and return it if it does spend the provided outpoint
+ for (it->Seek(std::pair{DB_TXOSPENDERINDEX, prefix}); it->Valid() && it->GetKey(key) && key.hash == prefix; it->Next()) {
+ if (const auto spender{ReadTransaction(key.pos)}) {
+ for (const auto& input : spender->tx->vin) {
+ if (input.prevout == txo) {
+ return std::optional{*spender};
+ }
+ }
+ } else {
+ LogError("Deserialize or I/O error - %s", spender.error());
+ return util::Unexpected{strprintf("IO error finding spending tx for outpoint %s:%d.", txo.hash.GetHex(), txo.n)};
+ }
+ }
+ return util::Expected, std::string>(std::nullopt);
+}
+
+BaseIndex::DB& TxoSpenderIndex::GetDB() const { return *m_db; }
diff --git a/src/index/txospenderindex.h b/src/index/txospenderindex.h
new file mode 100644
index 00000000000..dce1cec385d
--- /dev/null
+++ b/src/index/txospenderindex.h
@@ -0,0 +1,66 @@
+
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_INDEX_TXOSPENDERINDEX_H
+#define BITCOIN_INDEX_TXOSPENDERINDEX_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct CDiskTxPos;
+
+static constexpr bool DEFAULT_TXOSPENDERINDEX{false};
+
+struct TxoSpender {
+ CTransactionRef tx;
+ uint256 block_hash;
+};
+
+/**
+ * TxoSpenderIndex is used to look up which transaction spent a given output.
+ * The index is written to a LevelDB database and, for each input of each transaction in a block,
+ * records the outpoint that is spent and the hash of the spending transaction.
+ */
+class TxoSpenderIndex final : public BaseIndex
+{
+private:
+ std::unique_ptr m_db;
+ std::pair m_siphash_key;
+ bool AllowPrune() const override { return false; }
+ void WriteSpenderInfos(const std::vector>& items);
+ void EraseSpenderInfos(const std::vector>& items);
+ util::Expected ReadTransaction(const CDiskTxPos& pos) const;
+
+protected:
+ interfaces::Chain::NotifyOptions CustomOptions() override;
+
+ bool CustomAppend(const interfaces::BlockInfo& block) override;
+
+ bool CustomRemove(const interfaces::BlockInfo& block) override;
+
+ BaseIndex::DB& GetDB() const override;
+
+public:
+ explicit TxoSpenderIndex(std::unique_ptr chain, size_t n_cache_size, bool f_memory = false, bool f_wipe = false);
+
+ util::Expected, std::string> FindSpender(const COutPoint& txo) const;
+};
+
+/// The global txo spender index. May be null.
+extern std::unique_ptr g_txospenderindex;
+
+
+#endif // BITCOIN_INDEX_TXOSPENDERINDEX_H
diff --git a/src/init.cpp b/src/init.cpp
index e2cdb9c6477..52699d7980b 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -27,6 +27,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -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=", "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=",
strprintf("Maintain an index of compact filters by block (default: %s, values: %s).", DEFAULT_BLOCKFILTERINDEX, ListBlockFilterTypes()) +
" If is not supplied or if = 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(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));
diff --git a/src/node/caches.cpp b/src/node/caches.cpp
index ecff3c62836..5e285e8907b 100644
--- a/src/node/caches.cpp
+++ b/src/node/caches.cpp
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -22,6 +23,8 @@
static constexpr size_t MAX_TX_INDEX_CACHE{1024_MiB};
//! Max memory allocated to all block filter index caches combined in bytes.
static constexpr size_t MAX_FILTER_INDEX_CACHE{1024_MiB};
+//! Max memory allocated to tx spenderindex DB specific cache in bytes.
+static constexpr size_t MAX_TXOSPENDER_INDEX_CACHE{1024_MiB};
//! Maximum dbcache size on 32-bit systems.
static constexpr size_t MAX_32BIT_DBCACHE{1024_MiB};
@@ -44,6 +47,8 @@ CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes)
IndexCacheSizes index_sizes;
index_sizes.tx_index = std::min(total_cache / 8, args.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? MAX_TX_INDEX_CACHE : 0);
total_cache -= index_sizes.tx_index;
+ index_sizes.txospender_index = std::min(total_cache / 8, args.GetBoolArg("-txospenderindex", DEFAULT_TXOSPENDERINDEX) ? MAX_TXOSPENDER_INDEX_CACHE : 0);
+ total_cache -= index_sizes.txospender_index;
if (n_indexes > 0) {
size_t max_cache = std::min(total_cache / 8, MAX_FILTER_INDEX_CACHE);
index_sizes.filter_index = max_cache / n_indexes;
diff --git a/src/node/caches.h b/src/node/caches.h
index 2cf526b298e..8ea2fbffc0b 100644
--- a/src/node/caches.h
+++ b/src/node/caches.h
@@ -21,6 +21,7 @@ namespace node {
struct IndexCacheSizes {
size_t tx_index{0};
size_t filter_index{0};
+ size_t txospender_index{0};
};
struct CacheSizes {
IndexCacheSizes index;
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp
index 7398a8ed617..8422af1541b 100644
--- a/src/rpc/client.cpp
+++ b/src/rpc/client.cpp
@@ -312,6 +312,9 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getmempoolancestors", 1, "verbose" },
{ "getmempooldescendants", 1, "verbose" },
{ "gettxspendingprevout", 0, "outputs" },
+ { "gettxspendingprevout", 1, "options" },
+ { "gettxspendingprevout", 1, "mempool_only" },
+ { "gettxspendingprevout", 1, "return_spending_tx" },
{ "bumpfee", 1, "options" },
{ "bumpfee", 1, "conf_target"},
{ "bumpfee", 1, "fee_rate"},
diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp
index 12eef3ce755..d603612facc 100644
--- a/src/rpc/mempool.cpp
+++ b/src/rpc/mempool.cpp
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -30,6 +31,7 @@
#include
#include
+#include