This commit is contained in:
Fabrice Drouin 2024-04-29 04:34:57 +02:00 committed by GitHub
commit a6b23426ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 409 additions and 1 deletions

View File

@ -165,6 +165,7 @@ BITCOIN_CORE_H = \
index/blockfilterindex.h \
index/coinstatsindex.h \
index/disktxpos.h \
index/txospenderindex.h \
index/txindex.h \
indirectmap.h \
init.h \
@ -398,6 +399,7 @@ libbitcoin_node_a_SOURCES = \
index/base.cpp \
index/blockfilterindex.cpp \
index/coinstatsindex.cpp \
index/txospenderindex.cpp \
index/txindex.cpp \
init.cpp \
kernel/chain.cpp \

View File

@ -158,6 +158,7 @@ BITCOIN_TESTS =\
test/transaction_tests.cpp \
test/translation_tests.cpp \
test/txindex_tests.cpp \
test/txospenderindex_tests.cpp \
test/txpackage_tests.cpp \
test/txreconciliation_tests.cpp \
test/txrequest_tests.cpp \

View File

@ -0,0 +1,110 @@
// 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 <chainparams.h>
#include <common/args.h>
#include <index/disktxpos.h>
#include <index/txospenderindex.h>
#include <logging.h>
#include <node/blockstorage.h>
#include <validation.h>
constexpr uint8_t DB_TXOSPENDERINDEX{'s'};
std::unique_ptr<TxoSpenderIndex> 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<std::pair<COutPoint, uint256>>& items);
bool EraseSpenderInfos(const std::vector<COutPoint>& 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<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
: BaseIndex(std::move(chain), "txospenderindex"), m_db(std::make_unique<TxoSpenderIndex::DB>(n_cache_size, f_memory, f_wipe))
{
}
TxoSpenderIndex::~TxoSpenderIndex() {}
bool TxoSpenderIndex::DB::WriteSpenderInfos(const std::vector<std::pair<COutPoint, uint256>>& items)
{
CDBBatch batch(*this);
for (const auto& [outpoint, hash] : items) {
batch.Write(std::make_pair(DB_TXOSPENDERINDEX, outpoint), hash);
}
return WriteBatch(batch);
}
bool TxoSpenderIndex::DB::EraseSpenderInfos(const std::vector<COutPoint>& items)
{
CDBBatch batch(*this);
for (const auto& outpoint : items) {
batch.Erase(std::make_pair(DB_TXOSPENDERINDEX, outpoint));
}
return WriteBatch(batch);
}
bool TxoSpenderIndex::CustomAppend(const interfaces::BlockInfo& block)
{
std::vector<std::pair<COutPoint, uint256>> 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::BlockKey& current_tip, const interfaces::BlockKey& 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.ReadBlockFromDisk(block, *iter_tip)) {
LogError("Failed to read block %s from disk\n", iter_tip->GetBlockHash().ToString());
return false;
}
std::vector<COutPoint> items;
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;
}
bool TxoSpenderIndex::FindSpender(const COutPoint& txo, uint256& tx_hash) const
{
return m_db->Read(std::make_pair(DB_TXOSPENDERINDEX, txo), tx_hash);
}
BaseIndex::DB& TxoSpenderIndex::GetDB() const { return *m_db; }

View File

@ -0,0 +1,48 @@
// 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 <index/base.h>
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<DB> m_db;
bool AllowPrune() const override { return true; }
protected:
bool CustomAppend(const interfaces::BlockInfo& block) override;
bool CustomRewind(const interfaces::BlockKey& current_tip, const interfaces::BlockKey& new_tip) override;
BaseIndex::DB& GetDB() const override;
public:
/// Constructs the index, which becomes available to be queried.
explicit TxoSpenderIndex(std::unique_ptr<interfaces::Chain> chain, size_t n_cache_size, bool f_memory = false, bool f_wipe = false);
// Destructor is declared because this class contains a unique_ptr to an incomplete type.
virtual ~TxoSpenderIndex() override;
bool FindSpender(const COutPoint& txo, uint256& tx_hash) const;
};
/// The global txo spender index. May be null.
extern std::unique_ptr<TxoSpenderIndex> g_txospenderindex;
#endif // BITCOIN_INDEX_TXOSPENDERINDEX_H

View File

@ -30,6 +30,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>
@ -335,6 +336,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
@ -501,6 +503,7 @@ void SetupServerArgs(ArgsManager& argsman)
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 gettxospender 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.",
@ -1509,6 +1512,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) {
LogPrintf("* Using %.1f MiB for transaction index database\n", cache_sizes.tx_index * (1.0 / 1024 / 1024));
}
if (args.GetBoolArg("-txospenderindex", DEFAULT_TXOSPENDERINDEX)) {
LogPrintf("* Using %.1f MiB for transaction output spender index database\n", cache_sizes.txospender_index * (1.0 / 1024 / 1024));
}
for (BlockFilterType filter_type : g_enabled_filter_types) {
LogPrintf("* Using %.1f MiB for %s block filter index database\n",
cache_sizes.filter_index * (1.0 / 1024 / 1024), BlockFilterTypeName(filter_type));
@ -1646,6 +1652,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), cache_sizes.txospender_index, false, fReindex);
node.indexes.emplace_back(g_txospenderindex.get());
}
for (const auto& filter_type : g_enabled_filter_types) {
InitBlockFilterIndex([&]{ return interfaces::MakeChain(node); }, filter_type, cache_sizes.filter_index, false, fReindex);
node.indexes.emplace_back(GetBlockFilterIndex(filter_type));

View File

@ -6,6 +6,7 @@
#include <common/args.h>
#include <index/txindex.h>
#include <index/txospenderindex.h>
#include <txdb.h>
namespace node {
@ -19,6 +20,8 @@ CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes)
nTotalCache -= sizes.block_tree_db;
sizes.tx_index = std::min(nTotalCache / 8, args.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? nMaxTxIndexCache << 20 : 0);
nTotalCache -= sizes.tx_index;
sizes.txospender_index = std::min(nTotalCache / 8, args.GetBoolArg("-txospenderindex", DEFAULT_TXOSPENDERINDEX) ? nMaxTxoSpenderIndexCache << 20 : 0);
nTotalCache -= sizes.txospender_index;
sizes.filter_index = 0;
if (n_indexes > 0) {
int64_t max_cache = std::min(nTotalCache / 8, max_filter_index_cache << 20);

View File

@ -16,6 +16,7 @@ struct CacheSizes {
int64_t coins_db;
int64_t coins;
int64_t tx_index;
int64_t txospender_index;
int64_t filter_index;
};
CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes = 0);

View File

@ -261,6 +261,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"},

View File

@ -9,6 +9,7 @@
#include <chainparams.h>
#include <core_io.h>
#include <index/txospenderindex.h>
#include <kernel/mempool_entry.h>
#include <node/mempool_persist_args.h>
#include <policy/rbf.h>
@ -594,6 +595,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 true, txospenderindex will not be used even if mempool lacks a relevant spend. If false and txospenderindex is unavailable, throws an exception if any outpoint lacks a mempool spend."},
},
},
},
RPCResult{
RPCResult::Type::ARR, "", "",
@ -603,6 +609,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, "", ""},
}},
}},
}
},
@ -617,6 +627,19 @@ static RPCHelpMan gettxspendingprevout()
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, outputs are missing");
}
std::optional<bool> 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<COutPoint> prevouts;
prevouts.reserve(output_params.size());
@ -638,6 +661,11 @@ static RPCHelpMan gettxspendingprevout()
prevouts.emplace_back(txid, nOutput);
}
bool f_txospenderindex_ready{false};
if (g_txospenderindex && !mempool_only.value_or(false)) {
f_txospenderindex_ready = g_txospenderindex->BlockUntilSyncedToCurrentChain();
}
const CTxMemPool& mempool = EnsureAnyMemPool(request.context);
LOCK(mempool.cs);
@ -651,6 +679,36 @@ 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
} else if (g_txospenderindex) {
// no spending tx in mempool, query txospender index
uint256 spendingtxid;
if (g_txospenderindex->FindSpender(prevout, spendingtxid)) {
o.pushKV("spendingtxid", spendingtxid.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: value is false here
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: value is false here
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(o);

View File

@ -12,6 +12,7 @@
#include <index/blockfilterindex.h>
#include <index/coinstatsindex.h>
#include <index/txindex.h>
#include <index/txospenderindex.h>
#include <interfaces/chain.h>
#include <interfaces/echo.h>
#include <interfaces/init.h>
@ -394,6 +395,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));
});

View File

@ -0,0 +1,82 @@
// 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 <chainparams.h>
#include <index/txospenderindex.h>
#include <test/util/setup_common.h>
#include <util/time.h>
#include <validation.h>
#include <boost/test/unit_test.hpp>
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<CMutableTransaction> no_txns;
CreateAndProcessBlock(no_txns, this->m_coinbase_txns[i]->vout[0].scriptPubKey);
}
std::vector<COutPoint> spent(10);
std::vector<CMutableTransaction> spender(spent.size());
for (size_t i = 0; i < spent.size(); i++) {
spent[i] = COutPoint(this->m_coinbase_txns[i]->GetHash(), 0);
spender[i].nVersion = 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<unsigned char> 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);
uint256 txid;
// Transaction should not be found in the index before it is started.
for (const auto& outpoint : spent) {
BOOST_CHECK(!txospenderindex.FindSpender(outpoint, txid));
}
// 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(txospenderindex.FindSpender(spent[i], txid) && txid == 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()

View File

@ -35,6 +35,8 @@ static const int64_t nMaxBlockDBCache = 2;
// Unlike for the UTXO database, for the txindex scenario the leveldb cache make
// a meaningful difference: https://github.com/bitcoin/bitcoin/pull/8273#issuecomment-229601991
static const int64_t nMaxTxIndexCache = 1024;
//! Max memory allocated to the txo spender cache
static const int64_t nMaxTxoSpenderIndexCache = 1024;
//! Max memory allocated to all block filter index caches combined in MiB.
static const int64_t max_filter_index_cache = 1024;
//! Max memory allocated to coin DB specific cache (MiB)

View File

@ -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().main()