From 099b89805ac597be27135c047ec3d7f101c6e932 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 10 Mar 2022 10:26:57 +0100 Subject: [PATCH 1/2] Add a "tx output spender" index Adds an outpoint -> txid index, which can be used to find which transactions spent a given output. This is extremely useful for Lightning and more generally for layer-2 protocol that rely on chains of unpublished transactions. If enabled, this index will be used by `gettxspendingprevout` when it does not find a spending transaction in the mempool. --- src/CMakeLists.txt | 1 + src/index/txospenderindex.cpp | 115 ++++++++++++++++++++++++++++ src/index/txospenderindex.h | 47 ++++++++++++ src/init.cpp | 11 +++ src/node/caches.cpp | 5 ++ src/node/caches.h | 1 + src/rpc/client.cpp | 1 + src/rpc/mempool.cpp | 54 +++++++++++++ src/rpc/node.cpp | 5 ++ src/test/CMakeLists.txt | 1 + src/test/txospenderindex_tests.cpp | 81 ++++++++++++++++++++ test/functional/rpc_mempool_info.py | 86 ++++++++++++++++++++- 12 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 src/index/txospenderindex.cpp create mode 100644 src/index/txospenderindex.h create mode 100644 src/test/txospenderindex_tests.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 89fdd855a45..1ad59aeb20a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -214,6 +214,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..21aff63ec17 --- /dev/null +++ b/src/index/txospenderindex.cpp @@ -0,0 +1,115 @@ +// Copyright (c) 2022 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 + +// 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; + +/** Access to the txo spender index database (indexes/txospenderindex/) */ +class TxoSpenderIndex::DB : public BaseIndex::DB +{ +public: + explicit DB(size_t n_cache_size, bool f_memory = false, bool f_wipe = false); + + bool WriteSpenderInfos(const std::vector>& items); + bool EraseSpenderInfos(const std::vector& items); +}; + +TxoSpenderIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe) + : BaseIndex::DB(gArgs.GetDataDirNet() / "indexes" / "txospenderindex", n_cache_size, f_memory, f_wipe) +{ +} + +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(n_cache_size, f_memory, f_wipe)) +{ +} + +TxoSpenderIndex::~TxoSpenderIndex() = default; + +bool TxoSpenderIndex::DB::WriteSpenderInfos(const std::vector>& items) +{ + CDBBatch batch(*this); + for (const auto& [outpoint, hash] : items) { + batch.Write(std::pair{DB_TXOSPENDERINDEX, outpoint}, hash); + } + return WriteBatch(batch); +} + +bool TxoSpenderIndex::DB::EraseSpenderInfos(const std::vector& items) +{ + CDBBatch batch(*this); + for (const auto& outpoint : items) { + batch.Erase(std::pair{DB_TXOSPENDERINDEX, outpoint}); + } + return WriteBatch(batch); +} + +bool TxoSpenderIndex::CustomAppend(const interfaces::BlockInfo& block) +{ + std::vector> items; + items.reserve(block.data->vtx.size()); + + for (const auto& tx : block.data->vtx) { + if (tx->IsCoinBase()) { + continue; + } + for (const auto& input : tx->vin) { + items.emplace_back(input.prevout, tx->GetHash()); + } + } + return m_db->WriteSpenderInfos(items); +} + +bool TxoSpenderIndex::CustomRewind(const interfaces::BlockRef& current_tip, const interfaces::BlockRef& new_tip) +{ + LOCK(cs_main); + const CBlockIndex* iter_tip{m_chainstate->m_blockman.LookupBlockIndex(current_tip.hash)}; + const CBlockIndex* new_tip_index{m_chainstate->m_blockman.LookupBlockIndex(new_tip.hash)}; + + do { + CBlock block; + if (!m_chainstate->m_blockman.ReadBlock(block, *iter_tip)) { + LogError("Failed to read block %s from disk\n", iter_tip->GetBlockHash().ToString()); + return false; + } + std::vector items; + items.reserve(block.vtx.size()); + for (const auto& tx : block.vtx) { + if (tx->IsCoinBase()) { + continue; + } + for (const auto& input : tx->vin) { + items.emplace_back(input.prevout); + } + } + if (!m_db->EraseSpenderInfos(items)) { + LogError("Failed to erase indexed data for disconnected block %s from disk\n", iter_tip->GetBlockHash().ToString()); + return false; + } + + iter_tip = iter_tip->GetAncestor(iter_tip->nHeight - 1); + } while (new_tip_index != iter_tip); + + return true; +} + +std::optional TxoSpenderIndex::FindSpender(const COutPoint& txo) const +{ + uint256 tx_hash_out; + if (m_db->Read(std::pair{DB_TXOSPENDERINDEX, txo}, tx_hash_out)) { + return Txid::FromUint256(tx_hash_out); + } + return 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..92a4d8ef6a3 --- /dev/null +++ b/src/index/txospenderindex.h @@ -0,0 +1,47 @@ +// Copyright (c) 2022 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 + +static constexpr bool DEFAULT_TXOSPENDERINDEX{false}; + +/** + * 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 +{ +protected: + class DB; + +private: + const std::unique_ptr m_db; + + bool AllowPrune() const override { return true; } + +protected: + bool CustomAppend(const interfaces::BlockInfo& block) override; + + bool CustomRewind(const interfaces::BlockRef& current_tip, const interfaces::BlockRef& new_tip) 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); + + // Destroys unique_ptr to an incomplete type. + virtual ~TxoSpenderIndex() override; + + std::optional 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 09d9de4edcc..4467d00289d 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -353,6 +354,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 @@ -515,6 +517,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 gettxospender 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.", @@ -1613,6 +1616,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)); @@ -1679,6 +1685,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 8b432637c73..953a51e99d9 100644 --- a/src/node/caches.cpp +++ b/src/node/caches.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,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}; namespace node { CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes) @@ -34,6 +37,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 f24e9cc9103..187fdf0dc8c 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 1b711e3c5b1..03216e3b2b8 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -270,6 +270,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getmempoolancestors", 1, "verbose" }, { "getmempooldescendants", 1, "verbose" }, { "gettxspendingprevout", 0, "outputs" }, + { "gettxspendingprevout", 1, "options" }, { "bumpfee", 1, "options" }, { "bumpfee", 1, "conf_target"}, { "bumpfee", 1, "fee_rate"}, diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index 2b883322aa1..3bce932fcd7 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -602,6 +603,11 @@ static RPCHelpMan gettxspendingprevout() }, }, }, + {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"mempool_only", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true if txospenderindex unavailable, otherwise false"}, "If false and empool lacks a relevant spend, use txospenderindex (throws an exception if not available)."}, + }, + }, }, RPCResult{ RPCResult::Type::ARR, "", "", @@ -611,6 +617,10 @@ static RPCHelpMan gettxspendingprevout() {RPCResult::Type::STR_HEX, "txid", "the transaction id of the checked output"}, {RPCResult::Type::NUM, "vout", "the vout value of the checked output"}, {RPCResult::Type::STR_HEX, "spendingtxid", /*optional=*/true, "the transaction id of the mempool transaction spending this output (omitted if unspent)"}, + {RPCResult::Type::ARR, "warnings", /* optional */ true, "If spendingtxid isn't found in the mempool, and the mempool_only option isn't set explicitly, this will advise of issues using the txospenderindex.", + { + {RPCResult::Type::STR, "", ""}, + }}, }}, } }, @@ -625,6 +635,19 @@ static RPCHelpMan gettxspendingprevout() throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, outputs are missing"); } + std::optional mempool_only; + if (!request.params[1].isNull()) { + const UniValue& options = request.params[1]; + RPCTypeCheckObj(options, + { + {"mempool_only", UniValueType(UniValue::VBOOL)}, + }, + /*fAllowNull=*/true, /*fStrict=*/true); + if (options.exists("mempool_only")) { + mempool_only = options["mempool_only"].get_bool(); + } + } + std::vector prevouts; prevouts.reserve(output_params.size()); @@ -646,6 +669,8 @@ static RPCHelpMan gettxspendingprevout() prevouts.emplace_back(txid, nOutput); } + const bool f_txospenderindex_ready = !mempool_only.value_or(false) && g_txospenderindex && g_txospenderindex->BlockUntilSyncedToCurrentChain(); + const CTxMemPool& mempool = EnsureAnyMemPool(request.context); LOCK(mempool.cs); @@ -659,6 +684,35 @@ static RPCHelpMan gettxspendingprevout() const CTransaction* spendingTx = mempool.GetConflictTx(prevout); if (spendingTx != nullptr) { o.pushKV("spendingtxid", spendingTx->GetHash().ToString()); + } else if (mempool_only.value_or(false)) { + // do nothing, caller has selected to only query the mempool + } else if (g_txospenderindex) { + // no spending tx in mempool, query txospender index + if (auto spending_txid{g_txospenderindex->FindSpender(prevout)}) { + o.pushKV("spendingtxid", spending_txid->GetHex()); + if (!f_txospenderindex_ready) { + // warn if index is not ready as the spending tx that we found may be stale (it may be reorged out) + UniValue warnings(UniValue::VARR); + warnings.push_back("txospenderindex is still being synced."); + o.pushKV("warnings", warnings); + } + } else if (!f_txospenderindex_ready) { + if (mempool_only.has_value()) { // NOTE: caller explicitly set value to false + throw JSONRPCError(RPC_MISC_ERROR, strprintf("No spending tx for the outpoint %s:%d found, and txospenderindex is still being synced.", prevout.hash.GetHex(), prevout.n)); + } else { + UniValue warnings(UniValue::VARR); + warnings.push_back("txospenderindex is still being synced."); + o.pushKV("warnings", warnings); + } + } + } else { + if (mempool_only.has_value()) { // NOTE: caller explicitly set value to false + throw JSONRPCError(RPC_MISC_ERROR, strprintf("No spending tx for the outpoint %s:%d in mempool, and txospenderindex is unavailable.", prevout.hash.GetHex(), prevout.n)); + } else { + UniValue warnings(UniValue::VARR); + warnings.push_back("txospenderindex is unavailable."); + o.pushKV("warnings", warnings); + } } result.push_back(std::move(o)); diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index 5e36273cf49..7e41a9bd660 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -391,6 +392,10 @@ static RPCHelpMan getindexinfo() result.pushKVs(SummaryToJSON(g_coin_stats_index->GetSummary(), index_name)); } + if (g_txospenderindex) { + result.pushKVs(SummaryToJSON(g_txospenderindex->GetSummary(), index_name)); + } + ForEachBlockFilterIndex([&result, &index_name](const BlockFilterIndex& index) { result.pushKVs(SummaryToJSON(index.GetSummary(), index_name)); }); diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 859b9132067..711d0e6b621 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -126,6 +126,7 @@ add_executable(test_bitcoin translation_tests.cpp txdownload_tests.cpp txindex_tests.cpp + txospenderindex_tests.cpp txpackage_tests.cpp txreconciliation_tests.cpp txrequest_tests.cpp diff --git a/src/test/txospenderindex_tests.cpp b/src/test/txospenderindex_tests.cpp new file mode 100644 index 00000000000..3768fd842a4 --- /dev/null +++ b/src/test/txospenderindex_tests.cpp @@ -0,0 +1,81 @@ +// Copyright (c) 2017-2022 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 + +BOOST_AUTO_TEST_SUITE(txospenderindex_tests) + +BOOST_FIXTURE_TEST_CASE(txospenderindex_initial_sync, TestChain100Setup) +{ + TxoSpenderIndex txospenderindex(interfaces::MakeChain(m_node), 1 << 20, true); + BOOST_REQUIRE(txospenderindex.Init()); + + // Mine blocks for coinbase maturity, so we can spend some coinbase outputs in the test. + for (int i = 0; i < 50; i++) { + std::vector no_txns; + CreateAndProcessBlock(no_txns, this->m_coinbase_txns[i]->vout[0].scriptPubKey); + } + std::vector spent(10); + std::vector spender(spent.size()); + + for (size_t i = 0; i < spent.size(); i++) { + spent[i] = COutPoint(this->m_coinbase_txns[i]->GetHash(), 0); + spender[i].version = 1; + spender[i].vin.resize(1); + spender[i].vin[0].prevout.hash = spent[i].hash; + spender[i].vin[0].prevout.n = spent[i].n; + spender[i].vout.resize(1); + spender[i].vout[0].nValue = this->m_coinbase_txns[i]->GetValueOut(); + spender[i].vout[0].scriptPubKey = this->m_coinbase_txns[i]->vout[0].scriptPubKey; + + // Sign: + std::vector vchSig; + const uint256 hash = SignatureHash(this->m_coinbase_txns[i]->vout[0].scriptPubKey, spender[i], 0, SIGHASH_ALL, 0, SigVersion::BASE); + coinbaseKey.Sign(hash, vchSig); + vchSig.push_back((unsigned char)SIGHASH_ALL); + spender[i].vin[0].scriptSig << vchSig; + } + + CreateAndProcessBlock(spender, this->m_coinbase_txns[0]->vout[0].scriptPubKey); + + // Transaction should not be found in the index before it is started. + for (const auto& outpoint : spent) { + BOOST_CHECK(!txospenderindex.FindSpender(outpoint).has_value()); + } + + // BlockUntilSyncedToCurrentChain should return false before txospenderindex is started. + BOOST_CHECK(!txospenderindex.BlockUntilSyncedToCurrentChain()); + + BOOST_REQUIRE(txospenderindex.StartBackgroundSync()); + + // Allow tx index to catch up with the block index. + constexpr auto timeout{10s}; + const auto time_start{SteadyClock::now()}; + while (!txospenderindex.BlockUntilSyncedToCurrentChain()) { + BOOST_REQUIRE(time_start + timeout > SteadyClock::now()); + UninterruptibleSleep(std::chrono::milliseconds{100}); + } + for (size_t i = 0; i < spent.size(); i++) { + BOOST_CHECK_EQUAL(txospenderindex.FindSpender(spent[i]).value(), spender[i].GetHash()); + } + + // It is not safe to stop and destroy the index until it finishes handling + // the last BlockConnected notification. The BlockUntilSyncedToCurrentChain() + // call above is sufficient to ensure this, but the + // SyncWithValidationInterfaceQueue() call below is also needed to ensure + // TSAN always sees the test thread waiting for the notification thread, and + // avoid potential false positive reports. + m_node.validation_signals->SyncWithValidationInterfaceQueue(); + + // shutdown sequence (c.f. Shutdown() in init.cpp) + txospenderindex.Stop(); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/functional/rpc_mempool_info.py b/test/functional/rpc_mempool_info.py index 231d93a7b15..7dc606eeb3c 100755 --- a/test/functional/rpc_mempool_info.py +++ b/test/functional/rpc_mempool_info.py @@ -14,7 +14,12 @@ from test_framework.wallet import MiniWallet class RPCMempoolInfoTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 1 + self.num_nodes = 3 + self.extra_args = [ + ["-txospenderindex", "-whitelist=noban@127.0.0.1"], + ["-txospenderindex", "-whitelist=noban@127.0.0.1"], + ["-whitelist=noban@127.0.0.1"], + ] def run_test(self): self.wallet = MiniWallet(self.nodes[0]) @@ -59,8 +64,11 @@ class RPCMempoolInfoTest(BitcoinTestFramework): assert_equal(txid in mempool, True) self.log.info("Find transactions spending outputs") + # wait until spending transactions are found in the mempool of node 0, 1 and 2 result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC} ]) + self.wait_until(lambda: self.nodes[1].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) == result) + self.wait_until(lambda: self.nodes[2].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) == result) self.log.info("Find transaction spending multiple outputs") result = self.nodes[0].gettxspendingprevout([ {'txid' : txidE, 'vout' : 0}, {'txid' : txidF, 'vout' : 0} ]) @@ -71,6 +79,11 @@ class RPCMempoolInfoTest(BitcoinTestFramework): assert_equal(result, [ {'txid' : txidH, 'vout' : 0} ]) result = self.nodes[0].gettxspendingprevout([ {'txid' : txidA, 'vout' : 5} ]) assert_equal(result, [ {'txid' : txidA, 'vout' : 5} ]) + result = self.nodes[1].gettxspendingprevout([ {'txid' : txidA, 'vout' : 5} ]) + assert_equal(result, [ {'txid' : txidA, 'vout' : 5} ]) + # on node 2 you also get a warning as txospenderindex is not activated + result = self.nodes[2].gettxspendingprevout([ {'txid' : txidA, 'vout' : 5} ]) + assert_equal(result, [ {'txid' : txidA, 'vout' : 5, 'warnings': ['txospenderindex is unavailable.']} ]) self.log.info("Mixed spent and unspent outputs") result = self.nodes[0].gettxspendingprevout([ {'txid' : txidB, 'vout' : 0}, {'txid' : txidG, 'vout' : 3} ]) @@ -94,6 +107,77 @@ class RPCMempoolInfoTest(BitcoinTestFramework): self.log.info("Missing txid") assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].gettxspendingprevout, [{'vout' : 3}]) + self.generate(self.wallet, 1) + # spending transactions are found in the index of nodes 0 and 1 but not node 2 + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC} ]) + result = self.nodes[1].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC} ]) + result = self.nodes[2].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'warnings': ['txospenderindex is unavailable.']}, {'txid' : txidA, 'vout' : 1, 'warnings': ['txospenderindex is unavailable.']} ]) + + + self.log.info("Check that our txospenderindex is updated when a reorg replaces a spending transaction") + confirmed_utxo = self.wallet.get_utxo(mark_as_spent = False) + tx1 = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=1) + self.generate(self.wallet, 1) + # tx1 is confirmed, and indexed in txospenderindex as spending our utxo + assert not tx1["txid"] in self.nodes[0].getrawmempool() + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx1["txid"]} ]) + # replace tx1 with tx2 + self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) + self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) + self.nodes[2].invalidateblock(self.nodes[2].getbestblockhash()) + assert tx1["txid"] in self.nodes[0].getrawmempool() + assert tx1["txid"] in self.nodes[1].getrawmempool() + tx2 = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=2) + assert tx2["txid"] in self.nodes[0].getrawmempool() + + # check that when we find tx2 when we look in the mempool for a tx spending our output + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + + # check that our txospenderindex has been updated + self.generate(self.wallet, 1) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + + self.log.info("Check that our txospenderindex is updated when a reorg cancels a spending transaction") + confirmed_utxo = self.wallet.get_utxo(mark_as_spent = False) + tx1 = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=1) + tx2 = create_tx(utxos_to_spend=[tx1["new_utxos"][0]], num_outputs=1) + # tx1 spends our utxo, tx2 spends tx1 + self.generate(self.wallet, 1) + # tx1 and tx2 are confirmed, and indexed in txospenderindex + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx1["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + # replace tx1 with tx3 + blockhash= self.nodes[0].getbestblockhash() + self.nodes[0].invalidateblock(blockhash) + self.nodes[1].invalidateblock(blockhash) + self.nodes[2].invalidateblock(blockhash) + tx3 = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=2, fee_per_output=2000) + assert tx3["txid"] in self.nodes[0].getrawmempool() + assert not tx1["txid"] in self.nodes[0].getrawmempool() + assert not tx2["txid"] in self.nodes[0].getrawmempool() + # tx2 is not in the mempool anymore, but still in txospender index which has not been rewound yet + result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + txinfo = self.nodes[0].getrawtransaction(tx2["txid"], verbose = True, blockhash = blockhash) + assert_equal(txinfo["confirmations"], 0) + assert_equal(txinfo["in_active_chain"], False) + + self.generate(self.wallet, 1) + # we check that the spending tx for tx1 is now tx3 + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx3["txid"]} ]) + # we check that there is no more spending tx for tx1 + result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ]) + assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0} ]) + if __name__ == '__main__': RPCMempoolInfoTest(__file__).main() From 618c5f08f866c7ebf0244a943d8f77a9c2a2587e Mon Sep 17 00:00:00 2001 From: sstone Date: Mon, 23 Sep 2024 16:14:26 +0200 Subject: [PATCH 2/2] Use smaller keys and values Key was 36 bytes (txid: 32 bytes, output index: 4 bytes) and is now 8 bytes: the siphash of the spent outpoint, keyed with a random key that is created when the index is created (to avoid collision attacks). Value was 32 bytes (txid: 32 bytes), and is now a list of tx positions (9 bytes unless there are collisions which should be extremely rare). --- src/index/txospenderindex.cpp | 152 ++++++++++++++++++++-------- src/index/txospenderindex.h | 14 +-- src/rpc/client.cpp | 2 + src/rpc/mempool.cpp | 19 +++- src/test/txospenderindex_tests.cpp | 4 +- test/functional/rpc_mempool_info.py | 40 ++++---- 6 files changed, 160 insertions(+), 71 deletions(-) diff --git a/src/index/txospenderindex.cpp b/src/index/txospenderindex.cpp index 21aff63ec17..09e44876c02 100644 --- a/src/index/txospenderindex.cpp +++ b/src/index/txospenderindex.cpp @@ -3,9 +3,13 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include +#include #include #include #include +#include +#include #include // LeveLDB key prefix. We only have one key for now but it will make it easier to add others if needed. @@ -13,61 +17,99 @@ constexpr uint8_t DB_TXOSPENDERINDEX{'s'}; std::unique_ptr g_txospenderindex; -/** Access to the txo spender index database (indexes/txospenderindex/) */ -class TxoSpenderIndex::DB : public BaseIndex::DB -{ -public: - explicit DB(size_t n_cache_size, bool f_memory = false, bool f_wipe = false); - - bool WriteSpenderInfos(const std::vector>& items); - bool EraseSpenderInfos(const std::vector& items); -}; - -TxoSpenderIndex::DB::DB(size_t n_cache_size, bool f_memory, bool f_wipe) - : BaseIndex::DB(gArgs.GetDataDirNet() / "indexes" / "txospenderindex", n_cache_size, f_memory, f_wipe) -{ -} - 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(n_cache_size, f_memory, f_wipe)) { + fs::path path{gArgs.GetDataDirNet() / "indexes" / "txospenderindex"}; + fs::create_directories(path); + + m_db = std::make_unique(path / "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()}; + assert(m_db->Write("siphash_key", m_siphash_key)); + } } TxoSpenderIndex::~TxoSpenderIndex() = default; -bool TxoSpenderIndex::DB::WriteSpenderInfos(const std::vector>& items) +uint64_t TxoSpenderIndex::CreateKey(const COutPoint& vout) const { - CDBBatch batch(*this); - for (const auto& [outpoint, hash] : items) { - batch.Write(std::pair{DB_TXOSPENDERINDEX, outpoint}, hash); - } - return WriteBatch(batch); + return SipHashUint256Extra(m_siphash_key.first, m_siphash_key.second, vout.hash.ToUint256(), vout.n); } -bool TxoSpenderIndex::DB::EraseSpenderInfos(const std::vector& items) +bool TxoSpenderIndex::WriteSpenderInfos(const std::vector>& items) { - CDBBatch batch(*this); - for (const auto& outpoint : items) { - batch.Erase(std::pair{DB_TXOSPENDERINDEX, outpoint}); + CDBBatch batch(*m_db); + for (const auto& [outpoint, pos] : items) { + std::vector positions; + std::pair key{DB_TXOSPENDERINDEX, CreateKey(outpoint)}; + if (m_db->Exists(key)) { + if (!m_db->Read(key, positions)) { + LogError("Cannot read current state; tx spender index may be corrupted\n"); + } + } + if (std::find(positions.begin(), positions.end(), pos) == positions.end()) { + positions.push_back(pos); + batch.Write(key, positions); + } } - return WriteBatch(batch); + return m_db->WriteBatch(batch); +} + + +bool TxoSpenderIndex::EraseSpenderInfos(const std::vector& items) +{ + CDBBatch batch(*m_db); + for (const auto& outpoint : items) { + std::vector positions; + std::pair key{DB_TXOSPENDERINDEX, CreateKey(outpoint)}; + if (!m_db->Read(key, positions)) { + LogWarning("Could not read expected entry"); + continue; + } + if (positions.size() > 1) { + // there are collisions: find the position of the tx that spends the outpoint we want to erase + // this is expensive but extremely uncommon + size_t index = std::numeric_limits::max(); + for (size_t i = 0; i < positions.size(); i++) { + CTransactionRef tx; + if (!ReadTransaction(positions[i], tx)) continue; + for (const auto& input : tx->vin) { + if (input.prevout == outpoint) { + index = i; + break; + } + } + } + if (index != std::numeric_limits::max()) { + // remove it from the list + positions.erase(positions.begin() + index); + batch.Write(key, positions); + } + } else { + batch.Erase(key); + } + } + return m_db->WriteBatch(batch); } bool TxoSpenderIndex::CustomAppend(const interfaces::BlockInfo& block) { - std::vector> items; + 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()) { - continue; - } - for (const auto& input : tx->vin) { - items.emplace_back(input.prevout, tx->GetHash()); + if (!tx->IsCoinBase()) { + for (const auto& input : tx->vin) { + items.emplace_back(input.prevout, pos); + } } + pos.nTxOffset += ::GetSerializeSize(TX_WITH_WITNESS(*tx)); } - return m_db->WriteSpenderInfos(items); + + return WriteSpenderInfos(items); } bool TxoSpenderIndex::CustomRewind(const interfaces::BlockRef& current_tip, const interfaces::BlockRef& new_tip) @@ -92,7 +134,7 @@ bool TxoSpenderIndex::CustomRewind(const interfaces::BlockRef& current_tip, cons items.emplace_back(input.prevout); } } - if (!m_db->EraseSpenderInfos(items)) { + if (!EraseSpenderInfos(items)) { LogError("Failed to erase indexed data for disconnected block %s from disk\n", iter_tip->GetBlockHash().ToString()); return false; } @@ -103,13 +145,43 @@ bool TxoSpenderIndex::CustomRewind(const interfaces::BlockRef& current_tip, cons return true; } -std::optional TxoSpenderIndex::FindSpender(const COutPoint& txo) const +bool TxoSpenderIndex::ReadTransaction(const CDiskTxPos& tx_pos, CTransactionRef& tx) const { - uint256 tx_hash_out; - if (m_db->Read(std::pair{DB_TXOSPENDERINDEX, txo}, tx_hash_out)) { - return Txid::FromUint256(tx_hash_out); + AutoFile file{m_chainstate->m_blockman.OpenBlockFile(tx_pos, true)}; + if (file.IsNull()) { + return false; } - return std::nullopt; + CBlockHeader header; + try { + file >> header; + file.seek(tx_pos.nTxOffset, SEEK_CUR); + file >> TX_WITH_WITNESS(tx); + return true; + } catch (const std::exception& e) { + LogError("Deserialize or I/O error - %s\n", e.what()); + return false; + } +} + +CTransactionRef TxoSpenderIndex::FindSpender(const COutPoint& txo) const +{ + std::vector positions; + // read all tx position candidates from the db. there may be index collisions, in which case the db will return more than one tx position + if (!m_db->Read(std::pair{DB_TXOSPENDERINDEX, CreateKey(txo)}, positions)) { + return nullptr; + } + // loop until we find a tx that spends our outpoint + for (const auto& postx : positions) { + CTransactionRef tx; + if (ReadTransaction(postx, tx)) { + for (const auto& input : tx->vin) { + if (input.prevout == txo) { + return tx; + } + } + } + } + return nullptr; } BaseIndex::DB& TxoSpenderIndex::GetDB() const { return *m_db; } diff --git a/src/index/txospenderindex.h b/src/index/txospenderindex.h index 92a4d8ef6a3..af1e0b96058 100644 --- a/src/index/txospenderindex.h +++ b/src/index/txospenderindex.h @@ -6,6 +6,7 @@ #define BITCOIN_INDEX_TXOSPENDERINDEX_H #include +#include static constexpr bool DEFAULT_TXOSPENDERINDEX{false}; @@ -16,13 +17,14 @@ static constexpr bool DEFAULT_TXOSPENDERINDEX{false}; */ class TxoSpenderIndex final : public BaseIndex { -protected: - class DB; - private: - const std::unique_ptr m_db; - + std::unique_ptr m_db; + std::pair m_siphash_key; + uint64_t CreateKey(const COutPoint& vout) const; bool AllowPrune() const override { return true; } + bool WriteSpenderInfos(const std::vector>& items); + bool EraseSpenderInfos(const std::vector& items); + bool ReadTransaction(const CDiskTxPos& pos, CTransactionRef& tx) const; protected: bool CustomAppend(const interfaces::BlockInfo& block) override; @@ -37,7 +39,7 @@ public: // Destroys unique_ptr to an incomplete type. virtual ~TxoSpenderIndex() override; - std::optional FindSpender(const COutPoint& txo) const; + CTransactionRef FindSpender(const COutPoint& txo) const; }; /// The global txo spender index. May be null. diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 03216e3b2b8..f41489c9b18 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -271,6 +271,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "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 3bce932fcd7..f0e3582faea 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -603,9 +603,10 @@ static RPCHelpMan gettxspendingprevout() }, }, }, - {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + {"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "", { {"mempool_only", RPCArg::Type::BOOL, RPCArg::DefaultHint{"true if txospenderindex unavailable, otherwise false"}, "If false and empool lacks a relevant spend, use txospenderindex (throws an exception if not available)."}, + {"return_spending_tx", RPCArg::Type::BOOL, RPCArg::DefaultHint{"false"}, "If true, return the full spending tx."}, }, }, }, @@ -617,6 +618,7 @@ static RPCHelpMan gettxspendingprevout() {RPCResult::Type::STR_HEX, "txid", "the transaction id of the checked output"}, {RPCResult::Type::NUM, "vout", "the vout value of the checked output"}, {RPCResult::Type::STR_HEX, "spendingtxid", /*optional=*/true, "the transaction id of the mempool transaction spending this output (omitted if unspent)"}, + {RPCResult::Type::STR_HEX, "spendingtx", /*optional=*/true, "the transaction spending this output (only if return_spending_tx is set, omitted if unspent)"}, {RPCResult::Type::ARR, "warnings", /* optional */ true, "If spendingtxid isn't found in the mempool, and the mempool_only option isn't set explicitly, this will advise of issues using the txospenderindex.", { {RPCResult::Type::STR, "", ""}, @@ -636,16 +638,21 @@ static RPCHelpMan gettxspendingprevout() } std::optional mempool_only; + std::optional return_spending_tx; if (!request.params[1].isNull()) { const UniValue& options = request.params[1]; RPCTypeCheckObj(options, { {"mempool_only", UniValueType(UniValue::VBOOL)}, + {"return_spending_tx", UniValueType(UniValue::VBOOL)}, }, /*fAllowNull=*/true, /*fStrict=*/true); if (options.exists("mempool_only")) { mempool_only = options["mempool_only"].get_bool(); } + if (options.exists("return_spending_tx")) { + return_spending_tx = options["return_spending_tx"].get_bool(); + } } std::vector prevouts; @@ -684,12 +691,18 @@ static RPCHelpMan gettxspendingprevout() const CTransaction* spendingTx = mempool.GetConflictTx(prevout); if (spendingTx != nullptr) { o.pushKV("spendingtxid", spendingTx->GetHash().ToString()); + if (return_spending_tx) { + o.pushKV("spendingtx", EncodeHexTx(*spendingTx)); + } } else if (mempool_only.value_or(false)) { // do nothing, caller has selected to only query the mempool } else if (g_txospenderindex) { // no spending tx in mempool, query txospender index - if (auto spending_txid{g_txospenderindex->FindSpender(prevout)}) { - o.pushKV("spendingtxid", spending_txid->GetHex()); + if (auto spending_tx{g_txospenderindex->FindSpender(prevout)}) { + o.pushKV("spendingtxid", spending_tx->GetHash().GetHex()); + if (return_spending_tx) { + o.pushKV("spendingtx", EncodeHexTx(*spending_tx)); + } if (!f_txospenderindex_ready) { // warn if index is not ready as the spending tx that we found may be stale (it may be reorged out) UniValue warnings(UniValue::VARR); diff --git a/src/test/txospenderindex_tests.cpp b/src/test/txospenderindex_tests.cpp index 3768fd842a4..157def07b6e 100644 --- a/src/test/txospenderindex_tests.cpp +++ b/src/test/txospenderindex_tests.cpp @@ -47,7 +47,7 @@ BOOST_FIXTURE_TEST_CASE(txospenderindex_initial_sync, TestChain100Setup) // Transaction should not be found in the index before it is started. for (const auto& outpoint : spent) { - BOOST_CHECK(!txospenderindex.FindSpender(outpoint).has_value()); + BOOST_CHECK(!txospenderindex.FindSpender(outpoint)); } // BlockUntilSyncedToCurrentChain should return false before txospenderindex is started. @@ -63,7 +63,7 @@ BOOST_FIXTURE_TEST_CASE(txospenderindex_initial_sync, TestChain100Setup) UninterruptibleSleep(std::chrono::milliseconds{100}); } for (size_t i = 0; i < spent.size(); i++) { - BOOST_CHECK_EQUAL(txospenderindex.FindSpender(spent[i]).value(), spender[i].GetHash()); + BOOST_CHECK_EQUAL(txospenderindex.FindSpender(spent[i])->GetHash(), spender[i].GetHash()); } // It is not safe to stop and destroy the index until it finishes handling diff --git a/test/functional/rpc_mempool_info.py b/test/functional/rpc_mempool_info.py index 7dc606eeb3c..213b1afa7fc 100755 --- a/test/functional/rpc_mempool_info.py +++ b/test/functional/rpc_mempool_info.py @@ -109,11 +109,11 @@ class RPCMempoolInfoTest(BitcoinTestFramework): self.generate(self.wallet, 1) # spending transactions are found in the index of nodes 0 and 1 but not node 2 - result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC} ]) - result = self.nodes[1].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC} ]) - result = self.nodes[2].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA, 'spendingtx' : txA['hex']}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC, 'spendingtx' : txC['hex']} ]) + result = self.nodes[1].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA, 'spendingtx' : txA['hex']}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC, 'spendingtx' : txC['hex']} ]) + result = self.nodes[2].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ], return_spending_tx=True) assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'warnings': ['txospenderindex is unavailable.']}, {'txid' : txidA, 'vout' : 1, 'warnings': ['txospenderindex is unavailable.']} ]) @@ -123,8 +123,8 @@ class RPCMempoolInfoTest(BitcoinTestFramework): self.generate(self.wallet, 1) # tx1 is confirmed, and indexed in txospenderindex as spending our utxo assert not tx1["txid"] in self.nodes[0].getrawmempool() - result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx1["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx1["txid"], 'spendingtx' : tx1['hex']} ]) # replace tx1 with tx2 self.nodes[0].invalidateblock(self.nodes[0].getbestblockhash()) self.nodes[1].invalidateblock(self.nodes[1].getbestblockhash()) @@ -135,13 +135,13 @@ class RPCMempoolInfoTest(BitcoinTestFramework): assert tx2["txid"] in self.nodes[0].getrawmempool() # check that when we find tx2 when we look in the mempool for a tx spending our output - result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"], 'spendingtx' : tx2['hex']} ]) # check that our txospenderindex has been updated self.generate(self.wallet, 1) - result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"], 'spendingtx' : tx2['hex']} ]) self.log.info("Check that our txospenderindex is updated when a reorg cancels a spending transaction") confirmed_utxo = self.wallet.get_utxo(mark_as_spent = False) @@ -150,10 +150,10 @@ class RPCMempoolInfoTest(BitcoinTestFramework): # tx1 spends our utxo, tx2 spends tx1 self.generate(self.wallet, 1) # tx1 and tx2 are confirmed, and indexed in txospenderindex - result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx1["txid"]} ]) - result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx1["txid"], 'spendingtx' : tx1['hex']} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"], 'spendingtx' : tx2['hex']} ]) # replace tx1 with tx3 blockhash= self.nodes[0].getbestblockhash() self.nodes[0].invalidateblock(blockhash) @@ -164,18 +164,18 @@ class RPCMempoolInfoTest(BitcoinTestFramework): assert not tx1["txid"] in self.nodes[0].getrawmempool() assert not tx2["txid"] in self.nodes[0].getrawmempool() # tx2 is not in the mempool anymore, but still in txospender index which has not been rewound yet - result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"], 'spendingtx' : tx2['hex']} ]) txinfo = self.nodes[0].getrawtransaction(tx2["txid"], verbose = True, blockhash = blockhash) assert_equal(txinfo["confirmations"], 0) assert_equal(txinfo["in_active_chain"], False) self.generate(self.wallet, 1) # we check that the spending tx for tx1 is now tx3 - result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ]) - assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx3["txid"]} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0} ], return_spending_tx=True) + assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : tx3["txid"], 'spendingtx' : tx3['hex']} ]) # we check that there is no more spending tx for tx1 - result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ]) + result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ], return_spending_tx=True) assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0} ])