Skip to content

Commit 4185570

Browse files
committed
Add RPC to get mempool txs spending outputs
We add an RPC to fetch the mempool transactions spending given outpoints. Without this RPC, application developers would need to first call `getrawmempool` which returns a long list of `txid`, then fetch each of these txs individually to check whether they spend the given outpoint(s). This RPC can later be enriched to also find confirmed transactions instead of being restricted to mempool transactions.
1 parent 1ad5d50 commit 4185570

File tree

7 files changed

+200
-0
lines changed

7 files changed

+200
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
New RPCs
2+
--------
3+
4+
- A new `gettxspendingprevout` RPC has been added, which scans the mempool to find
5+
transactions spending any of the given outpoints. (#24408)

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
173173
{ "setwalletflag", 1, "value" },
174174
{ "getmempoolancestors", 1, "verbose" },
175175
{ "getmempooldescendants", 1, "verbose" },
176+
{ "gettxspendingprevout", 0, "outputs" },
176177
{ "bumpfee", 1, "options" },
177178
{ "psbtbumpfee", 1, "options" },
178179
{ "logging", 0, "include" },

src/rpc/mempool.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,89 @@ static RPCHelpMan getmempoolentry()
587587
};
588588
}
589589

590+
static RPCHelpMan gettxspendingprevout()
591+
{
592+
return RPCHelpMan{"gettxspendingprevout",
593+
"Scans the mempool to find transactions spending any of the given outputs",
594+
{
595+
{"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The transaction outputs that we want to check, and within each, the txid (string) vout (numeric).",
596+
{
597+
{"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "",
598+
{
599+
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
600+
{"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"},
601+
},
602+
},
603+
},
604+
},
605+
},
606+
RPCResult{
607+
RPCResult::Type::ARR, "", "",
608+
{
609+
{RPCResult::Type::OBJ, "", "",
610+
{
611+
{RPCResult::Type::STR_HEX, "txid", "the transaction id of the checked output"},
612+
{RPCResult::Type::NUM, "vout", "the vout value of the checked output"},
613+
{RPCResult::Type::STR_HEX, "spendingtxid", /*optional=*/true, "the transaction id of the mempool transaction spending this output (omitted if unspent)"},
614+
}},
615+
}
616+
},
617+
RPCExamples{
618+
HelpExampleCli("gettxspendingprevout", "\"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":3}]\"")
619+
+ HelpExampleRpc("gettxspendingprevout", "\"[{\\\"txid\\\":\\\"a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0\\\",\\\"vout\\\":3}]\"")
620+
},
621+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
622+
{
623+
RPCTypeCheckArgument(request.params[0], UniValue::VARR);
624+
const UniValue& output_params = request.params[0];
625+
if (output_params.empty()) {
626+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, outputs are missing");
627+
}
628+
629+
std::vector<COutPoint> prevouts;
630+
prevouts.reserve(output_params.size());
631+
632+
for (unsigned int idx = 0; idx < output_params.size(); idx++) {
633+
const UniValue& o = output_params[idx].get_obj();
634+
635+
RPCTypeCheckObj(o,
636+
{
637+
{"txid", UniValueType(UniValue::VSTR)},
638+
{"vout", UniValueType(UniValue::VNUM)},
639+
}, /*fAllowNull=*/false, /*fStrict=*/true);
640+
641+
const uint256 txid(ParseHashO(o, "txid"));
642+
const int nOutput = find_value(o, "vout").get_int();
643+
if (nOutput < 0) {
644+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, vout cannot be negative");
645+
}
646+
647+
prevouts.emplace_back(txid, nOutput);
648+
}
649+
650+
const CTxMemPool& mempool = EnsureAnyMemPool(request.context);
651+
LOCK(mempool.cs);
652+
653+
UniValue result{UniValue::VARR};
654+
655+
for (const COutPoint& prevout : prevouts) {
656+
UniValue o(UniValue::VOBJ);
657+
o.pushKV("txid", prevout.hash.ToString());
658+
o.pushKV("vout", (uint64_t)prevout.n);
659+
660+
const CTransaction* spendingTx = mempool.GetConflictTx(prevout);
661+
if (spendingTx != nullptr) {
662+
o.pushKV("spendingtxid", spendingTx->GetHash().ToString());
663+
}
664+
665+
result.push_back(o);
666+
}
667+
668+
return result;
669+
},
670+
};
671+
}
672+
590673
UniValue MempoolInfoToJSON(const CTxMemPool& pool)
591674
{
592675
// Make sure this call is atomic in the pool.
@@ -677,6 +760,7 @@ void RegisterMempoolRPCCommands(CRPCTable& t)
677760
{"blockchain", &getmempoolancestors},
678761
{"blockchain", &getmempooldescendants},
679762
{"blockchain", &getmempoolentry},
763+
{"blockchain", &gettxspendingprevout},
680764
{"blockchain", &getmempoolinfo},
681765
{"blockchain", &getrawmempool},
682766
{"blockchain", &savemempool},

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
128128
"getmempoolancestors",
129129
"getmempooldescendants",
130130
"getmempoolentry",
131+
"gettxspendingprevout",
131132
"getmempoolinfo",
132133
"getmininginfo",
133134
"getnettotals",

test/functional/mempool_packages.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ def run_test(self):
100100
entry = self.nodes[0].getmempoolentry(x)
101101
assert_equal(entry, mempool[x])
102102

103+
# Check that gettxspendingprevout is consistent with getrawmempool
104+
witnesstx = self.nodes[0].gettransaction(txid=x, verbose=True)['decoded']
105+
for tx_in in witnesstx["vin"]:
106+
spending_result = self.nodes[0].gettxspendingprevout([ {'txid' : tx_in["txid"], 'vout' : tx_in["vout"]} ])
107+
assert_equal(spending_result, [ {'txid' : tx_in["txid"], 'vout' : tx_in["vout"], 'spendingtxid' : x} ])
108+
103109
# Check that the descendant calculations are correct
104110
assert_equal(entry['descendantcount'], descendant_count)
105111
descendant_fees += entry['fees']['base']

test/functional/rpc_mempool_info.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2014-2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test RPCs that retrieve information from the mempool."""
6+
7+
from test_framework.blocktools import COINBASE_MATURITY
8+
from test_framework.test_framework import BitcoinTestFramework
9+
from test_framework.util import (
10+
assert_equal,
11+
assert_raises_rpc_error,
12+
)
13+
from test_framework.wallet import MiniWallet
14+
15+
16+
class RPCMempoolInfoTest(BitcoinTestFramework):
17+
def set_test_params(self):
18+
self.num_nodes = 1
19+
20+
def run_test(self):
21+
self.wallet = MiniWallet(self.nodes[0])
22+
self.generate(self.wallet, COINBASE_MATURITY + 1)
23+
self.wallet.rescan_utxos()
24+
confirmed_utxo = self.wallet.get_utxo()
25+
26+
# Create a tree of unconfirmed transactions in the mempool:
27+
# txA
28+
# / \
29+
# / \
30+
# / \
31+
# / \
32+
# / \
33+
# txB txC
34+
# / \ / \
35+
# / \ / \
36+
# txD txE txF txG
37+
# \ /
38+
# \ /
39+
# txH
40+
41+
def create_tx(**kwargs):
42+
return self.wallet.send_self_transfer_multi(
43+
from_node=self.nodes[0],
44+
**kwargs,
45+
)
46+
47+
txA = create_tx(utxos_to_spend=[confirmed_utxo], num_outputs=2)
48+
txB = create_tx(utxos_to_spend=[txA["new_utxos"][0]], num_outputs=2)
49+
txC = create_tx(utxos_to_spend=[txA["new_utxos"][1]], num_outputs=2)
50+
txD = create_tx(utxos_to_spend=[txB["new_utxos"][0]], num_outputs=1)
51+
txE = create_tx(utxos_to_spend=[txB["new_utxos"][1]], num_outputs=1)
52+
txF = create_tx(utxos_to_spend=[txC["new_utxos"][0]], num_outputs=2)
53+
txG = create_tx(utxos_to_spend=[txC["new_utxos"][1]], num_outputs=1)
54+
txH = create_tx(utxos_to_spend=[txE["new_utxos"][0],txF["new_utxos"][0]], num_outputs=1)
55+
txidA, txidB, txidC, txidD, txidE, txidF, txidG, txidH = [
56+
tx["txid"] for tx in [txA, txB, txC, txD, txE, txF, txG, txH]
57+
]
58+
59+
mempool = self.nodes[0].getrawmempool()
60+
assert_equal(len(mempool), 8)
61+
for txid in [txidA, txidB, txidC, txidD, txidE, txidF, txidG, txidH]:
62+
assert_equal(txid in mempool, True)
63+
64+
self.log.info("Find transactions spending outputs")
65+
result = self.nodes[0].gettxspendingprevout([ {'txid' : confirmed_utxo['txid'], 'vout' : 0}, {'txid' : txidA, 'vout' : 1} ])
66+
assert_equal(result, [ {'txid' : confirmed_utxo['txid'], 'vout' : 0, 'spendingtxid' : txidA}, {'txid' : txidA, 'vout' : 1, 'spendingtxid' : txidC} ])
67+
68+
self.log.info("Find transaction spending multiple outputs")
69+
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidE, 'vout' : 0}, {'txid' : txidF, 'vout' : 0} ])
70+
assert_equal(result, [ {'txid' : txidE, 'vout' : 0, 'spendingtxid' : txidH}, {'txid' : txidF, 'vout' : 0, 'spendingtxid' : txidH} ])
71+
72+
self.log.info("Find no transaction when output is unspent")
73+
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidH, 'vout' : 0} ])
74+
assert_equal(result, [ {'txid' : txidH, 'vout' : 0} ])
75+
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidA, 'vout' : 5} ])
76+
assert_equal(result, [ {'txid' : txidA, 'vout' : 5} ])
77+
78+
self.log.info("Mixed spent and unspent outputs")
79+
result = self.nodes[0].gettxspendingprevout([ {'txid' : txidB, 'vout' : 0}, {'txid' : txidG, 'vout' : 3} ])
80+
assert_equal(result, [ {'txid' : txidB, 'vout' : 0, 'spendingtxid' : txidD}, {'txid' : txidG, 'vout' : 3} ])
81+
82+
self.log.info("Unknown input fields")
83+
assert_raises_rpc_error(-3, "Unexpected key unknown", self.nodes[0].gettxspendingprevout, [{'txid' : txidC, 'vout' : 1, 'unknown' : 42}])
84+
85+
self.log.info("Invalid vout provided")
86+
assert_raises_rpc_error(-8, "Invalid parameter, vout cannot be negative", self.nodes[0].gettxspendingprevout, [{'txid' : txidA, 'vout' : -1}])
87+
88+
self.log.info("Invalid txid provided")
89+
assert_raises_rpc_error(-3, "Expected type string for txid, got number", self.nodes[0].gettxspendingprevout, [{'txid' : 42, 'vout' : 0}])
90+
91+
self.log.info("Missing outputs")
92+
assert_raises_rpc_error(-8, "Invalid parameter, outputs are missing", self.nodes[0].gettxspendingprevout, [])
93+
94+
self.log.info("Missing vout")
95+
assert_raises_rpc_error(-3, "Missing vout", self.nodes[0].gettxspendingprevout, [{'txid' : txidA}])
96+
97+
self.log.info("Missing txid")
98+
assert_raises_rpc_error(-3, "Missing txid", self.nodes[0].gettxspendingprevout, [{'vout' : 3}])
99+
100+
101+
if __name__ == '__main__':
102+
RPCMempoolInfoTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@
329329
'feature_settings.py',
330330
'rpc_getdescriptorinfo.py',
331331
'rpc_mempool_entry_fee_fields_deprecation.py',
332+
'rpc_mempool_info.py',
332333
'rpc_help.py',
333334
'feature_dirsymlinks.py',
334335
'feature_help.py',

0 commit comments

Comments
 (0)