mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-03-02 01:36:13 +00:00
Merge bitcoin/bitcoin#24539: Add a "tx output spender" index
0b96b9c600e0dd946fd4d0e827e7f7cbef7a571a Minimize mempool lock, sync txo spender index only when and if needed (sstone) 3d82ec5bdd019cf1c048c41fe44faa855fcb8b53 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 0b96b9c600e0dd946fd4d0e827e7f7cbef7a571a sedited: Re-ACK 0b96b9c600e0dd946fd4d0e827e7f7cbef7a571a fjahr: ACK 0b96b9c600e0dd946fd4d0e827e7f7cbef7a571a w0xlt: reACK 0b96b9c600e0dd946fd4d0e827e7f7cbef7a571a Tree-SHA512: 95c2c313ef4086e7d5bf1cf1a3c7b91cfe2bb1a0dcb4c9d3aa8a6e5bfde66aaca48d85a1f1251a780523c3e4356ec8a97fe6f5c7145bc6ccb6f820b26716ae01
This commit is contained in:
commit
1a54886b63
@ -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`<sup>[\[2\]](#note2)</sup> | Blockfilter index filters for the basic filtertype; *optional*, used if `-blockfilterindex=basic`
|
||||
`indexes/coinstatsindex/db/` | LevelDB database | Coinstats index; *optional*, used if `-coinstatsindex=1`
|
||||
|
||||
14
doc/release-notes-24539.md
Normal file
14
doc/release-notes-24539.md
Normal file
@ -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)
|
||||
@ -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
|
||||
|
||||
184
src/index/txospenderindex.cpp
Normal file
184
src/index/txospenderindex.cpp
Normal file
@ -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 <index/txospenderindex.h>
|
||||
|
||||
#include <common/args.h>
|
||||
#include <crypto/siphash.h>
|
||||
#include <dbwrapper.h>
|
||||
#include <flatfile.h>
|
||||
#include <index/base.h>
|
||||
#include <index/disktxpos.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <logging.h>
|
||||
#include <node/blockstorage.h>
|
||||
#include <primitives/block.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <random.h>
|
||||
#include <serialize.h>
|
||||
#include <streams.h>
|
||||
#include <tinyformat.h>
|
||||
#include <uint256.h>
|
||||
#include <util/fs.h>
|
||||
#include <validation.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <ios>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
/* 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<TxoSpenderIndex> 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<interfaces::Chain> chain, size_t n_cache_size, bool f_memory, bool f_wipe)
|
||||
: BaseIndex(std::move(chain), "txospenderindex"), m_db{std::make_unique<DB>(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<uint64_t, uint64_t> siphash_key, const COutPoint& vout)
|
||||
{
|
||||
return PresaltedSipHasher(siphash_key.first, siphash_key.second)(vout.hash.ToUint256(), vout.n);
|
||||
}
|
||||
|
||||
static DBKey CreateKey(std::pair<uint64_t, uint64_t> siphash_key, const COutPoint& vout, const CDiskTxPos& pos)
|
||||
{
|
||||
return DBKey(CreateKeyPrefix(siphash_key, vout), pos);
|
||||
}
|
||||
|
||||
void TxoSpenderIndex::WriteSpenderInfos(const std::vector<std::pair<COutPoint, CDiskTxPos>>& 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<std::pair<COutPoint, CDiskTxPos>>& 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<std::pair<COutPoint, CDiskTxPos>> BuildSpenderPositions(const interfaces::BlockInfo& block)
|
||||
{
|
||||
std::vector<std::pair<COutPoint, CDiskTxPos>> 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<TxoSpender, std::string> 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::optional<TxoSpender>, std::string> TxoSpenderIndex::FindSpender(const COutPoint& txo) const
|
||||
{
|
||||
const uint64_t prefix{CreateKeyPrefix(m_siphash_key, txo)};
|
||||
std::unique_ptr<CDBIterator> 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::optional<TxoSpender>, std::string>(std::nullopt);
|
||||
}
|
||||
|
||||
BaseIndex::DB& TxoSpenderIndex::GetDB() const { return *m_db; }
|
||||
66
src/index/txospenderindex.h
Normal file
66
src/index/txospenderindex.h
Normal file
@ -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 <index/base.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <uint256.h>
|
||||
#include <util/expected.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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<BaseIndex::DB> m_db;
|
||||
std::pair<uint64_t, uint64_t> m_siphash_key;
|
||||
bool AllowPrune() const override { return false; }
|
||||
void WriteSpenderInfos(const std::vector<std::pair<COutPoint, CDiskTxPos>>& items);
|
||||
void EraseSpenderInfos(const std::vector<std::pair<COutPoint, CDiskTxPos>>& items);
|
||||
util::Expected<TxoSpender, std::string> 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<interfaces::Chain> chain, size_t n_cache_size, bool f_memory = false, bool f_wipe = false);
|
||||
|
||||
util::Expected<std::optional<TxoSpender>, std::string> FindSpender(const COutPoint& txo) const;
|
||||
};
|
||||
|
||||
/// The global txo spender index. May be null.
|
||||
extern std::unique_ptr<TxoSpenderIndex> g_txospenderindex;
|
||||
|
||||
|
||||
#endif // BITCOIN_INDEX_TXOSPENDERINDEX_H
|
||||
13
src/init.cpp
13
src/init.cpp
@ -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));
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <common/args.h>
|
||||
#include <common/system.h>
|
||||
#include <index/txindex.h>
|
||||
#include <index/txospenderindex.h>
|
||||
#include <kernel/caches.h>
|
||||
#include <logging.h>
|
||||
#include <node/interface_ui.h>
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"},
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
#include <common/args.h>
|
||||
#include <consensus/validation.h>
|
||||
#include <core_io.h>
|
||||
#include <index/txospenderindex.h>
|
||||
#include <kernel/mempool_entry.h>
|
||||
#include <net_processing.h>
|
||||
#include <netbase.h>
|
||||
@ -30,6 +31,7 @@
|
||||
#include <util/time.h>
|
||||
#include <util/vector.h>
|
||||
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
@ -895,7 +897,7 @@ static RPCHelpMan getmempoolentry()
|
||||
static RPCHelpMan gettxspendingprevout()
|
||||
{
|
||||
return RPCHelpMan{"gettxspendingprevout",
|
||||
"Scans the mempool to find transactions spending any of the given outputs",
|
||||
"Scans the mempool (and the txospenderindex, if available) to find transactions spending any of the given outputs",
|
||||
{
|
||||
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The transaction outputs that we want to check, and within each, the txid (string) vout (numeric).",
|
||||
{
|
||||
@ -907,6 +909,12 @@ static RPCHelpMan gettxspendingprevout()
|
||||
},
|
||||
},
|
||||
},
|
||||
{"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 mempool 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."},
|
||||
},
|
||||
},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::ARR, "", "",
|
||||
@ -916,12 +924,15 @@ 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::STR_HEX, "blockhash", /*optional=*/true, "the hash of the spending block (omitted if unspent or the spending tx is not confirmed)"},
|
||||
}},
|
||||
}
|
||||
},
|
||||
RPCExamples{
|
||||
HelpExampleCli("gettxspendingprevout", "\"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":3}]\"")
|
||||
+ HelpExampleRpc("gettxspendingprevout", "\"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":3}]\"")
|
||||
+ HelpExampleCliNamed("gettxspendingprevout", {{"outputs", "[{\"txid\":\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\",\"vout\":3}]"}, {"return_spending_tx", true}})
|
||||
},
|
||||
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
|
||||
{
|
||||
@ -929,8 +940,22 @@ static RPCHelpMan gettxspendingprevout()
|
||||
if (output_params.empty()) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, outputs are missing");
|
||||
}
|
||||
const UniValue options{request.params[1].isNull() ? UniValue::VOBJ : request.params[1]};\
|
||||
RPCTypeCheckObj(options,
|
||||
{
|
||||
{"mempool_only", UniValueType(UniValue::VBOOL)},
|
||||
{"return_spending_tx", UniValueType(UniValue::VBOOL)},
|
||||
}, /*fAllowNull=*/true, /*fStrict=*/true);
|
||||
|
||||
std::vector<COutPoint> prevouts;
|
||||
const bool mempool_only{options.exists("mempool_only") ? options["mempool_only"].get_bool() : !g_txospenderindex};
|
||||
const bool return_spending_tx{options.exists("return_spending_tx") ? options["return_spending_tx"].get_bool() : false};
|
||||
|
||||
struct Entry {
|
||||
const COutPoint prevout;
|
||||
const UniValue& input;
|
||||
UniValue output;
|
||||
};
|
||||
std::vector<Entry> prevouts;
|
||||
prevouts.reserve(output_params.size());
|
||||
|
||||
for (unsigned int idx = 0; idx < output_params.size(); idx++) {
|
||||
@ -947,25 +972,56 @@ static RPCHelpMan gettxspendingprevout()
|
||||
if (nOutput < 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative");
|
||||
}
|
||||
|
||||
prevouts.emplace_back(txid, nOutput);
|
||||
prevouts.emplace_back(COutPoint{txid, uint32_t(nOutput)}, o, UniValue{});
|
||||
}
|
||||
|
||||
const CTxMemPool& mempool = EnsureAnyMemPool(request.context);
|
||||
LOCK(mempool.cs);
|
||||
|
||||
UniValue result{UniValue::VARR};
|
||||
|
||||
for (const COutPoint& prevout : prevouts) {
|
||||
UniValue o(UniValue::VOBJ);
|
||||
o.pushKV("txid", prevout.hash.ToString());
|
||||
o.pushKV("vout", prevout.n);
|
||||
|
||||
const CTransaction* spendingTx = mempool.GetConflictTx(prevout);
|
||||
if (spendingTx != nullptr) {
|
||||
o.pushKV("spendingtxid", spendingTx->GetHash().ToString());
|
||||
// search the mempool first
|
||||
bool missing_from_mempool{false};
|
||||
{
|
||||
const CTxMemPool& mempool = EnsureAnyMemPool(request.context);
|
||||
LOCK(mempool.cs);
|
||||
for (auto& entry : prevouts) {
|
||||
const CTransaction* spendingTx = mempool.GetConflictTx(entry.prevout);
|
||||
if (spendingTx != nullptr) {
|
||||
UniValue o{entry.input};
|
||||
o.pushKV("spendingtxid", spendingTx->GetHash().ToString());
|
||||
if (return_spending_tx) {
|
||||
o.pushKV("spendingtx", EncodeHexTx(*spendingTx));
|
||||
}
|
||||
entry.output = std::move(o);
|
||||
} else {
|
||||
missing_from_mempool = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if search is not limited to the mempool and no spender was found for an outpoint, search the txospenderindex
|
||||
// we call g_txospenderindex->BlockUntilSyncedToCurrentChain() only if g_txospenderindex is going to be used
|
||||
UniValue result{UniValue::VARR};
|
||||
bool txospenderindex_ready{mempool_only || !missing_from_mempool || (g_txospenderindex && g_txospenderindex->BlockUntilSyncedToCurrentChain())};
|
||||
for (auto& entry : prevouts) {
|
||||
if (!entry.output.isNull()) {
|
||||
result.push_back(std::move(entry.output));
|
||||
continue;
|
||||
}
|
||||
UniValue o{entry.input};
|
||||
if (mempool_only) {
|
||||
// do nothing, caller has selected to only query the mempool
|
||||
} else if (!txospenderindex_ready) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No spending tx for the outpoint %s:%d in mempool, and txospenderindex is unavailable.", entry.prevout.hash.GetHex(), entry.prevout.n));
|
||||
} else {
|
||||
// no spending tx in mempool, query txospender index
|
||||
const auto spender{g_txospenderindex->FindSpender(entry.prevout)};
|
||||
if (!spender) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, spender.error());
|
||||
}
|
||||
if (spender.value()) {
|
||||
o.pushKV("spendingtxid", spender.value()->tx->GetHash().GetHex());
|
||||
o.pushKV("blockhash", spender.value()->block_hash.GetHex());
|
||||
if (return_spending_tx) {
|
||||
o.pushKV("spendingtx", EncodeHexTx(*spender.value()->tx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push_back(std::move(o));
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,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>
|
||||
@ -397,6 +398,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));
|
||||
});
|
||||
|
||||
@ -116,6 +116,7 @@ add_executable(test_bitcoin
|
||||
txdownload_tests.cpp
|
||||
txgraph_tests.cpp
|
||||
txindex_tests.cpp
|
||||
txospenderindex_tests.cpp
|
||||
txpackage_tests.cpp
|
||||
txreconciliation_tests.cpp
|
||||
txrequest_tests.cpp
|
||||
|
||||
77
src/test/txospenderindex_tests.cpp
Normal file
77
src/test/txospenderindex_tests.cpp
Normal file
@ -0,0 +1,77 @@
|
||||
// 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 <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].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<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;
|
||||
}
|
||||
|
||||
CBlock block = 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).value());
|
||||
}
|
||||
|
||||
// BlockUntilSyncedToCurrentChain should return false before txospenderindex is started.
|
||||
BOOST_CHECK(!txospenderindex.BlockUntilSyncedToCurrentChain());
|
||||
|
||||
txospenderindex.Sync();
|
||||
for (size_t i = 0; i < spent.size(); i++) {
|
||||
const auto tx_spender{txospenderindex.FindSpender(spent[i])};
|
||||
BOOST_REQUIRE(tx_spender.has_value());
|
||||
BOOST_REQUIRE(tx_spender->has_value());
|
||||
BOOST_CHECK_EQUAL((*tx_spender)->tx->GetHash(), spender[i].GetHash());
|
||||
BOOST_CHECK_EQUAL((*tx_spender)->block_hash, block.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()
|
||||
@ -79,7 +79,7 @@ class InitTest(BitcoinTestFramework):
|
||||
if self.is_wallet_compiled():
|
||||
lines_to_terminate_after.append(b'Verifying wallet')
|
||||
|
||||
args = ['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1']
|
||||
args = ['-txindex=1', '-blockfilterindex=1', '-coinstatsindex=1', '-txospenderindex=1']
|
||||
for terminate_line in lines_to_terminate_after:
|
||||
self.log.info(f"Starting node and will terminate after line {terminate_line}")
|
||||
with node.busy_wait_for_debug_log([terminate_line]):
|
||||
@ -133,6 +133,11 @@ class InitTest(BitcoinTestFramework):
|
||||
'error_message': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
|
||||
'startup_args': ['-txindex=1'],
|
||||
},
|
||||
{
|
||||
'filepath_glob': 'indexes/txospenderindex/db/MANIFEST*',
|
||||
'error_message': 'LevelDB error: Corruption: CURRENT points to a non-existent file',
|
||||
'startup_args': ['-txospenderindex=1'],
|
||||
},
|
||||
# Removing these files does not result in a startup error:
|
||||
# 'indexes/blockfilter/basic/*.dat', 'indexes/blockfilter/basic/db/*.*', 'indexes/coinstatsindex/db/*.*',
|
||||
# 'indexes/txindex/*.log', 'indexes/txindex/CURRENT', 'indexes/txindex/LOCK'
|
||||
@ -174,6 +179,11 @@ class InitTest(BitcoinTestFramework):
|
||||
'error_message': 'LevelDB error: Corruption',
|
||||
'startup_args': ['-txindex=1'],
|
||||
},
|
||||
{
|
||||
'filepath_glob': 'indexes/txospenderindex/db/*',
|
||||
'error_message': 'LevelDB error: Corruption',
|
||||
'startup_args': ['-txospenderindex=1'],
|
||||
},
|
||||
# Perturbing these files does not result in a startup error:
|
||||
# 'indexes/blockfilter/basic/*.dat', 'indexes/txindex/MANIFEST*', 'indexes/txindex/LOCK'
|
||||
]
|
||||
@ -183,6 +193,7 @@ class InitTest(BitcoinTestFramework):
|
||||
err_fragment = round_info['error_message']
|
||||
startup_args = round_info['startup_args']
|
||||
target_files = list(node.chain_path.glob(file_patt))
|
||||
assert target_files, f"Failed to find expected files: {file_patt}"
|
||||
|
||||
for target_file in target_files:
|
||||
self.log.info(f"Deleting file to ensure failure {target_file}")
|
||||
@ -209,6 +220,7 @@ class InitTest(BitcoinTestFramework):
|
||||
for dir in dirs:
|
||||
shutil.copytree(node.chain_path / dir, node.chain_path / f"{dir}_bak")
|
||||
target_files = list(node.chain_path.glob(file_patt))
|
||||
assert target_files, f"Failed to find expected files: {file_patt}"
|
||||
|
||||
for target_file in target_files:
|
||||
self.log.info(f"Perturbing file to ensure failure {target_file}")
|
||||
|
||||
186
test/functional/rpc_gettxspendingprevout.py
Executable file
186
test/functional/rpc_gettxspendingprevout.py
Executable file
@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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.
|
||||
"""Test gettxspendingprevout RPC."""
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
from test_framework.wallet import MiniWallet
|
||||
|
||||
|
||||
class GetTxSpendingPrevoutTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 3
|
||||
self.noban_tx_relay = True
|
||||
self.extra_args = [
|
||||
["-txospenderindex"],
|
||||
["-txospenderindex"],
|
||||
[],
|
||||
]
|
||||
|
||||
def run_test(self):
|
||||
self.wallet = MiniWallet(self.nodes[0])
|
||||
confirmed_utxo = self.wallet.get_utxo()
|
||||
|
||||
# Create a tree of unconfirmed transactions in the mempool:
|
||||
# txA
|
||||
# / \
|
||||
# / \
|
||||
# / \
|
||||
# / \
|
||||
# / \
|
||||
# txB txC
|
||||
# / \ / \
|
||||
# / \ / \
|
||||
# txD txE txF txG
|
||||
# \ /
|
||||
# \ /
|
||||
# txH
|
||||
|
||||
def create_tx(**kwargs):
|
||||
return self.wallet.send_self_transfer_multi(
|
||||
from_node=self.nodes[0],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
txA = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=2)
|
||||
txB = create_tx(utxos_to_spend=[txA["new_utxos"][0]], num_outputs=2)
|
||||
txC = create_tx(utxos_to_spend=[txA["new_utxos"][1]], num_outputs=2)
|
||||
txD = create_tx(utxos_to_spend=[txB["new_utxos"][0]], num_outputs=1)
|
||||
txE = create_tx(utxos_to_spend=[txB["new_utxos"][1]], num_outputs=1)
|
||||
txF = create_tx(utxos_to_spend=[txC["new_utxos"][0]], num_outputs=2)
|
||||
txG = create_tx(utxos_to_spend=[txC["new_utxos"][1]], num_outputs=1)
|
||||
txH = create_tx(utxos_to_spend=[txE["new_utxos"][0],txF["new_utxos"][0]], num_outputs=1)
|
||||
txidA, txidB, txidC, txidD, txidE, txidF, txidG, txidH = [
|
||||
tx["txid"] for tx in [txA, txB, txC, txD, txE, txF, txG, txH]
|
||||
]
|
||||
|
||||
mempool = self.nodes[0].getrawmempool()
|
||||
assert_equal(len(mempool), 8)
|
||||
for txid in [txidA, txidB, txidC, txidD, txidE, txidF, txidG, txidH]:
|
||||
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} ], mempool_only=True) == result)
|
||||
|
||||
self.log.info("Find transaction spending multiple outputs")
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidE, 'vout' : 0}, {'txid' : txidF, 'vout' : 0} ])
|
||||
assert_equal(result, [ {'txid' : txidE, 'vout' : 0, 'spendingtxid' : txidH}, {'txid' : txidF, 'vout' : 0, 'spendingtxid' : txidH} ])
|
||||
|
||||
self.log.info("Find no transaction when output is unspent")
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidH, 'vout' : 0} ])
|
||||
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} ])
|
||||
result = self.nodes[2].gettxspendingprevout([ {'txid' : txidA, 'vout' : 5} ])
|
||||
assert_equal(result, [ {'txid' : txidA, 'vout' : 5} ])
|
||||
|
||||
self.log.info("Mixed spent and unspent outputs")
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidB, 'vout' : 0}, {'txid' : txidG, 'vout' : 3} ])
|
||||
assert_equal(result, [ {'txid' : txidB, 'vout' : 0, 'spendingtxid' : txidD}, {'txid' : txidG, 'vout' : 3} ])
|
||||
|
||||
self.log.info("Unknown input fields")
|
||||
assert_raises_rpc_error(-3, "Unexpected key unknown", self.nodes[0].gettxspendingprevout, [{'txid' : txidC, 'vout' : 1, 'unknown' : 42}])
|
||||
|
||||
self.log.info("Invalid vout provided")
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", self.nodes[0].gettxspendingprevout, [{'txid' : txidA, 'vout' : -1}])
|
||||
|
||||
self.log.info("Invalid txid provided")
|
||||
assert_raises_rpc_error(-3, "JSON value of type number for field txid is not of expected type string", self.nodes[0].gettxspendingprevout, [{'txid' : 42, 'vout' : 0}])
|
||||
|
||||
self.log.info("Missing outputs")
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, outputs are missing", self.nodes[0].gettxspendingprevout, [])
|
||||
|
||||
self.log.info("Missing vout")
|
||||
assert_raises_rpc_error(-3, "Missing vout", self.nodes[0].gettxspendingprevout, [{'txid' : txidA}])
|
||||
|
||||
self.log.info("Missing txid")
|
||||
assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].gettxspendingprevout, [{'vout' : 3}])
|
||||
|
||||
blockhash = self.generate(self.wallet, 1)[0]
|
||||
# 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} ], return_spending_tx=True)
|
||||
assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA, 'blockhash' : blockhash, 'spendingtx' : txA['hex']}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC, 'blockhash' : blockhash, '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, 'blockhash' : blockhash, 'spendingtx' : txA['hex']}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC, 'blockhash' : blockhash, '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}, {'txid' : txidA, 'vout' : 1}])
|
||||
|
||||
# spending transaction is not found if we only search the mempool
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ], return_spending_tx=True, mempool_only=True)
|
||||
assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1}])
|
||||
|
||||
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)
|
||||
blockhash = self.generate(self.wallet, 1)[0]
|
||||
# tx1 is confirmed, and indexed in txospenderindex as spending our utxo
|
||||
assert tx1["txid"] not in self.nodes[0].getrawmempool()
|
||||
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"], 'blockhash' : blockhash, 'spendingtx' : tx1['hex']} ])
|
||||
# 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} ], 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
|
||||
blockhash = self.generate(self.wallet, 1)[0]
|
||||
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"], 'blockhash' : blockhash, '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)
|
||||
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
|
||||
blockhash = self.generate(self.wallet, 1)[0]
|
||||
# tx1 and tx2 are confirmed, and indexed in txospenderindex
|
||||
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"], 'blockhash' : blockhash, '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"], 'blockhash' : blockhash, 'spendingtx' : tx2['hex']} ])
|
||||
# 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 tx1["txid"] not in self.nodes[0].getrawmempool()
|
||||
assert tx2["txid"] not 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} ], return_spending_tx=True)
|
||||
assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0, 'spendingtxid' : tx2["txid"], 'blockhash' : blockhash, '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)
|
||||
|
||||
blockhash = self.generate(self.wallet, 1)[0]
|
||||
# we check that the spending tx for tx1 is now tx3
|
||||
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"], 'blockhash' : blockhash, 'spendingtx' : tx3['hex']} ])
|
||||
# we check that there is no more spending tx for tx1
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : tx1['txid'], 'vout' : 0} ], return_spending_tx=True)
|
||||
assert_equal(result, [ {'txid' : tx1['txid'], 'vout' : 0} ])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
GetTxSpendingPrevoutTest(__file__).main()
|
||||
@ -1,99 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2014-present The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test RPCs that retrieve information from the mempool."""
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_raises_rpc_error,
|
||||
)
|
||||
from test_framework.wallet import MiniWallet
|
||||
|
||||
|
||||
class RPCMempoolInfoTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
|
||||
def run_test(self):
|
||||
self.wallet = MiniWallet(self.nodes[0])
|
||||
confirmed_utxo = self.wallet.get_utxo()
|
||||
|
||||
# Create a tree of unconfirmed transactions in the mempool:
|
||||
# txA
|
||||
# / \
|
||||
# / \
|
||||
# / \
|
||||
# / \
|
||||
# / \
|
||||
# txB txC
|
||||
# / \ / \
|
||||
# / \ / \
|
||||
# txD txE txF txG
|
||||
# \ /
|
||||
# \ /
|
||||
# txH
|
||||
|
||||
def create_tx(**kwargs):
|
||||
return self.wallet.send_self_transfer_multi(
|
||||
from_node=self.nodes[0],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
txA = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=2)
|
||||
txB = create_tx(utxos_to_spend=[txA["new_utxos"][0]], num_outputs=2)
|
||||
txC = create_tx(utxos_to_spend=[txA["new_utxos"][1]], num_outputs=2)
|
||||
txD = create_tx(utxos_to_spend=[txB["new_utxos"][0]], num_outputs=1)
|
||||
txE = create_tx(utxos_to_spend=[txB["new_utxos"][1]], num_outputs=1)
|
||||
txF = create_tx(utxos_to_spend=[txC["new_utxos"][0]], num_outputs=2)
|
||||
txG = create_tx(utxos_to_spend=[txC["new_utxos"][1]], num_outputs=1)
|
||||
txH = create_tx(utxos_to_spend=[txE["new_utxos"][0],txF["new_utxos"][0]], num_outputs=1)
|
||||
txidA, txidB, txidC, txidD, txidE, txidF, txidG, txidH = [
|
||||
tx["txid"] for tx in [txA, txB, txC, txD, txE, txF, txG, txH]
|
||||
]
|
||||
|
||||
mempool = self.nodes[0].getrawmempool()
|
||||
assert_equal(len(mempool), 8)
|
||||
for txid in [txidA, txidB, txidC, txidD, txidE, txidF, txidG, txidH]:
|
||||
assert_equal(txid in mempool, True)
|
||||
|
||||
self.log.info("Find transactions spending outputs")
|
||||
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.log.info("Find transaction spending multiple outputs")
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidE, 'vout' : 0}, {'txid' : txidF, 'vout' : 0} ])
|
||||
assert_equal(result, [ {'txid' : txidE, 'vout' : 0, 'spendingtxid' : txidH}, {'txid' : txidF, 'vout' : 0, 'spendingtxid' : txidH} ])
|
||||
|
||||
self.log.info("Find no transaction when output is unspent")
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidH, 'vout' : 0} ])
|
||||
assert_equal(result, [ {'txid' : txidH, 'vout' : 0} ])
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidA, 'vout' : 5} ])
|
||||
assert_equal(result, [ {'txid' : txidA, 'vout' : 5} ])
|
||||
|
||||
self.log.info("Mixed spent and unspent outputs")
|
||||
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidB, 'vout' : 0}, {'txid' : txidG, 'vout' : 3} ])
|
||||
assert_equal(result, [ {'txid' : txidB, 'vout' : 0, 'spendingtxid' : txidD}, {'txid' : txidG, 'vout' : 3} ])
|
||||
|
||||
self.log.info("Unknown input fields")
|
||||
assert_raises_rpc_error(-3, "Unexpected key unknown", self.nodes[0].gettxspendingprevout, [{'txid' : txidC, 'vout' : 1, 'unknown' : 42}])
|
||||
|
||||
self.log.info("Invalid vout provided")
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", self.nodes[0].gettxspendingprevout, [{'txid' : txidA, 'vout' : -1}])
|
||||
|
||||
self.log.info("Invalid txid provided")
|
||||
assert_raises_rpc_error(-3, "JSON value of type number for field txid is not of expected type string", self.nodes[0].gettxspendingprevout, [{'txid' : 42, 'vout' : 0}])
|
||||
|
||||
self.log.info("Missing outputs")
|
||||
assert_raises_rpc_error(-8, "Invalid parameter, outputs are missing", self.nodes[0].gettxspendingprevout, [])
|
||||
|
||||
self.log.info("Missing vout")
|
||||
assert_raises_rpc_error(-3, "Missing vout", self.nodes[0].gettxspendingprevout, [{'txid' : txidA}])
|
||||
|
||||
self.log.info("Missing txid")
|
||||
assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].gettxspendingprevout, [{'vout' : 3}])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
RPCMempoolInfoTest(__file__).main()
|
||||
@ -100,7 +100,7 @@ class RpcMiscTest(BitcoinTestFramework):
|
||||
assert_equal(node.getindexinfo(), {})
|
||||
|
||||
# Restart the node with indices and wait for them to sync
|
||||
self.restart_node(0, ["-txindex", "-blockfilterindex", "-coinstatsindex"])
|
||||
self.restart_node(0, ["-txindex", "-blockfilterindex", "-coinstatsindex", "-txospenderindex"])
|
||||
self.wait_until(lambda: all(i["synced"] for i in node.getindexinfo().values()))
|
||||
|
||||
# Returns a list of all running indices by default
|
||||
@ -111,10 +111,11 @@ class RpcMiscTest(BitcoinTestFramework):
|
||||
"txindex": values,
|
||||
"basic block filter index": values,
|
||||
"coinstatsindex": values,
|
||||
"txospenderindex": values
|
||||
}
|
||||
)
|
||||
# Specifying an index by name returns only the status of that index
|
||||
for i in {"txindex", "basic block filter index", "coinstatsindex"}:
|
||||
for i in {"txindex", "basic block filter index", "coinstatsindex", "txospenderindex"}:
|
||||
assert_equal(node.getindexinfo(i), {i: values})
|
||||
|
||||
# Specifying an unknown index name returns an empty result
|
||||
|
||||
@ -373,7 +373,7 @@ BASE_SCRIPTS = [
|
||||
'feature_presegwit_node_upgrade.py',
|
||||
'feature_settings.py',
|
||||
'rpc_getdescriptorinfo.py',
|
||||
'rpc_mempool_info.py',
|
||||
'rpc_gettxspendingprevout.py',
|
||||
'rpc_help.py',
|
||||
'feature_framework_testshell.py',
|
||||
'tool_rpcauth.py',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user