Skip to content

Commit 48bab09

Browse files
achow101vijaydasmp
authored andcommitted
Merge bitcoin#25504: RPC: allow to track coins by parent descriptors
a6b0c1f doc: add releases notes for 25504 (listsinceblock updates) (Antoine Poinsot) 0fd2d14 rpc: add an include_change parameter to listsinceblock (Antoine Poinsot) 55f98d0 rpc: output parent wallet descriptors for coins in listunspent (Antoine Poinsot) b724476 rpc: output wallet descriptors for received entries in listsinceblock (Antoine Poinsot) 55a82ea wallet: allow to fetch the wallet descriptors for a given Script (Antoine Poinsot) Pull request description: Wallet descriptors are useful for applications using the Bitcoin Core wallet as a backend for tracking coins, as they allow to track coins for multiple descriptors in a single wallet. However there is no information currently given for such applications to link a coin with an imported descriptor, severely limiting the possibilities for such applications of using multiple descriptors in a single wallet. This PR outputs the matching imported descriptor(s) for a given received coin in `listsinceblock` (and friends). It comes from a need for an application i'm working on, but i think it's something any software using `bitcoind` to track multiple descriptors in a single wallet would have eventually. For instance i'm thinking about the BDK project. Currently, the way to achieve this is to import raw addresses with labels and to have your application be responsible for wallet things like the gap limit. I'll add this to the output of `listunspent` too if this gets a few Concept ACKs. ACKs for top commit: instagibbs: ACK bitcoin@a6b0c1f achow101: re-ACK a6b0c1f Tree-SHA512: 7a5850e8de98b439ddede2cb72de0208944f8cda67272e8b8037678738d55b7a5272375be808b0f7d15def4904430e089dafdcc037436858ff3292c5f8b75e37
1 parent fd43e8c commit 48bab09

11 files changed

+145
-6
lines changed

doc/release-notes-25504.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Updated RPCs
2+
------------
3+
4+
- The `listsinceblock`, `listtransactions` and `gettransaction` output now contain a new
5+
`parent_descs` field for every "receive" entry.
6+
- A new optional `include_change` parameter was added to the `listsinceblock` command.

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
8686
{ "listsinceblock", 1, "target_confirmations" },
8787
{ "listsinceblock", 2, "include_watchonly" },
8888
{ "listsinceblock", 3, "include_removed" },
89+
{ "listsinceblock", 4, "include_change" },
8990
{ "sendmany", 1, "amounts" },
9091
{ "sendmany", 2, "minconf" },
9192
{ "sendmany", 3, "addlocked" },

src/wallet/receive.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,8 @@ CAmount CWalletTx::GetAvailableCredit(bool fUseCache, const isminefilter& filter
218218
}
219219

220220
void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived,
221-
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) const
221+
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
222+
bool include_change) const
222223
{
223224
nFee = 0;
224225
listReceived.clear();
@@ -243,8 +244,7 @@ void CWalletTx::GetAmounts(std::list<COutputEntry>& listReceived,
243244
// 2) the output is to us (received)
244245
if (nDebit > 0)
245246
{
246-
// Don't report 'change' txouts
247-
if (pwallet->IsChange(txout))
247+
if (!include_change && pwallet->IsChange(txout))
248248
continue;
249249
}
250250
else if (!(fIsMine & filter))

src/wallet/rpc/util.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,12 @@ std::string LabelFromValue(const UniValue& value)
119119
throw JSONRPCError(RPC_WALLET_INVALID_LABEL_NAME, "Invalid label name");
120120
return label;
121121
}
122+
123+
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry)
124+
{
125+
UniValue parent_descs(UniValue::VARR);
126+
for (const auto& desc: wallet.GetWalletDescriptors(script_pubkey)) {
127+
parent_descs.push_back(desc.descriptor->ToString());
128+
}
129+
entry.pushKV("parent_descs", parent_descs);
130+
}

src/wallet/rpc/util.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define BITCOIN_WALLET_RPC_UTIL_H
77

88
#include <context.h>
9+
#include <script/script.h>
910

1011
#include <memory>
1112
#include <string>
@@ -36,5 +37,7 @@ const LegacyScriptPubKeyMan& EnsureConstLegacyScriptPubKeyMan(const CWallet& wal
3637
bool GetAvoidReuseFlag(const CWallet& wallet, const UniValue& param);
3738
bool ParseIncludeWatchonly(const UniValue& include_watchonly, const CWallet& wallet);
3839
std::string LabelFromValue(const UniValue& value);
40+
//! Fetch parent descriptors of this scriptPubKey.
41+
void PushParentDescriptors(const CWallet& wallet, const CScript& script_pubkey, UniValue& entry);
3942

4043
#endif // BITCOIN_WALLET_RPC_UTIL_H

src/wallet/rpcwallet.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,13 +1196,16 @@ static void MaybePushAddress(UniValue & entry, const CTxDestination &dest)
11961196
* @param filter_ismine The "is mine" filter flags.
11971197
* @param filter_label Optional label string to filter incoming transactions.
11981198
*/
1199-
static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong, UniValue& ret, const isminefilter& filter_ismine, const std::string* filter_label) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
1199+
static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nMinDepth, bool fLong,
1200+
UniValue& ret, const isminefilter& filter_ismine, const std::string* filter_label,
1201+
bool include_change = false)
1202+
EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
12001203
{
12011204
CAmount nFee;
12021205
std::list<COutputEntry> listReceived;
12031206
std::list<COutputEntry> listSent;
12041207

1205-
wtx.GetAmounts(listReceived, listSent, nFee, filter_ismine);
1208+
wtx.GetAmounts(listReceived, listSent, nFee, filter_ismine, include_change);
12061209

12071210
bool involvesWatchonly = wtx.IsFromMe(ISMINE_WATCH_ONLY);
12081211

@@ -1250,6 +1253,7 @@ static void ListTransactions(const CWallet& wallet, const CWalletTx& wtx, int nM
12501253
entry.pushKV("involvesWatchonly", true);
12511254
}
12521255
MaybePushAddress(entry, r.destination);
1256+
PushParentDescriptors(wallet, wtx.tx->vout.at(r.vout).scriptPubKey, entry);
12531257
if (wtx.IsCoinBase())
12541258
{
12551259
if (wtx.GetDepthInMainChain() < 1)
@@ -3068,6 +3072,9 @@ static RPCHelpMan listunspent()
30683072
{RPCResult::Type::BOOL, "spendable", "Whether we have the private keys to spend this output"},
30693073
{RPCResult::Type::BOOL, "solvable", "Whether we know how to spend this output, ignoring the lack of keys"},
30703074
{RPCResult::Type::STR, "desc", "(only when solvable) A descriptor for spending this output"},
3075+
{RPCResult::Type::ARR, "parent_descs", /*optional=*/false, "List of parent descriptors for the scriptPubKey of this coin.", {
3076+
{RPCResult::Type::STR, "desc", "The descriptor string."},
3077+
}},
30713078
{RPCResult::Type::BOOL, "reused", /* optional*/ true, "(only present if avoid_reuse is set) Whether this output is reused/dirty (sent to an address that was previously spent from)"},
30723079
{RPCResult::Type::BOOL, "safe", "Whether this output is considered safe to spend. Unconfirmed transactions"
30733080
"from outside keys and unconfirmed replacement transactions are considered unsafe\n"
@@ -3244,6 +3251,7 @@ static RPCHelpMan listunspent()
32443251
entry.pushKV("desc", descriptor->ToString());
32453252
}
32463253
}
3254+
PushParentDescriptors(*pwallet, scriptPubKey, entry);
32473255
if (avoid_reuse) entry.pushKV("reused", reused);
32483256
entry.pushKV("safe", out.fSafe);
32493257
entry.pushKV("coinjoin_rounds", pwallet->GetRealOutpointCoinJoinRounds(COutPoint(out.tx->GetHash(), out.i)));

src/wallet/transaction.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,8 @@ class CWalletTx
312312
}
313313

314314
void GetAmounts(std::list<COutputEntry>& listReceived,
315-
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter) const;
315+
std::list<COutputEntry>& listSent, CAmount& nFee, const isminefilter& filter,
316+
bool include_change) const;
316317

317318
bool IsFromMe(const isminefilter& filter) const
318319
{

src/wallet/wallet.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4183,6 +4183,18 @@ std::unique_ptr<SigningProvider> CWallet::GetSolvingProvider(const CScript& scri
41834183
return nullptr;
41844184
}
41854185

4186+
std::vector<WalletDescriptor> CWallet::GetWalletDescriptors(const CScript& script) const
4187+
{
4188+
std::vector<WalletDescriptor> descs;
4189+
for (const auto spk_man: GetScriptPubKeyMans(script)) {
4190+
if (const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man)) {
4191+
LOCK(desc_spk_man->cs_desc_man);
4192+
descs.push_back(desc_spk_man->GetWalletDescriptor());
4193+
}
4194+
}
4195+
return descs;
4196+
}
4197+
41864198
LegacyScriptPubKeyMan* CWallet::GetLegacyScriptPubKeyMan() const
41874199
{
41884200
if (IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {

src/wallet/wallet.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
10131013
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script) const;
10141014
std::unique_ptr<SigningProvider> GetSolvingProvider(const CScript& script, SignatureData& sigdata) const;
10151015

1016+
//! Get the wallet descriptors for a script.
1017+
std::vector<WalletDescriptor> GetWalletDescriptors(const CScript& script) const;
1018+
10161019
//! Get the LegacyScriptPubKeyMan which is used for all types, internal, and external.
10171020
LegacyScriptPubKeyMan* GetLegacyScriptPubKeyMan() const;
10181021
LegacyScriptPubKeyMan* GetOrCreateLegacyScriptPubKeyMan();

test/functional/wallet_basic.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from itertools import product
88

99
from test_framework.blocktools import COINBASE_MATURITY
10+
from test_framework.descriptors import descsum_create
1011
from test_framework.test_framework import BitcoinTestFramework
1112
from test_framework.util import (
1213
assert_array_result,
@@ -740,6 +741,38 @@ def run_test(self):
740741
txid_feeReason_four = self.nodes[2].sendmany(dummy='', amounts={address: 5}, verbose=False)
741742
assert_equal(self.nodes[2].gettransaction(txid_feeReason_four)['txid'], txid_feeReason_four)
742743

744+
self.log.info("Testing 'listunspent' outputs the parent descriptor(s) of coins")
745+
# Create two multisig descriptors, and send a UTxO each.
746+
multi_a = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YBNjUo96Jxd1u4XKWgnoc7LsA1jz3Yc2NiDbhtfBhaBtemB73n9V5vtJHwU6FVXwggTbeoJWQ1rzdz8ysDuQkpnaHyvnvzR/*,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*))")
747+
multi_b = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*,tpubD6NzVbkrYhZ4Y2RLiuEzNQkntjmsLpPYDm3LTRBYynUQtDtpzeUKAcb9sYthSFL3YR74cdFgF5mW8yKxv2W2CWuZDFR2dUpE5PF9kbrVXNZ/*))")
748+
addr_a = self.nodes[0].deriveaddresses(multi_a, 0)[0]
749+
addr_b = self.nodes[0].deriveaddresses(multi_b, 0)[0]
750+
txid_a = self.nodes[0].sendtoaddress(addr_a, 0.01)
751+
txid_b = self.nodes[0].sendtoaddress(addr_b, 0.01)
752+
self.generate(self.nodes[0], 1, sync_fun=self.no_op)
753+
# Now import the descriptors, make sure we can identify on which descriptor each coin was received.
754+
self.nodes[0].createwallet(wallet_name="wo", descriptors=True, disable_private_keys=True)
755+
wo_wallet = self.nodes[0].get_wallet_rpc("wo")
756+
wo_wallet.importdescriptors([
757+
{
758+
"desc": multi_a,
759+
"active": False,
760+
"timestamp": "now",
761+
},
762+
{
763+
"desc": multi_b,
764+
"active": False,
765+
"timestamp": "now",
766+
},
767+
])
768+
coins = wo_wallet.listunspent(minconf=0)
769+
assert_equal(len(coins), 2)
770+
coin_a = next(c for c in coins if c["txid"] == txid_a)
771+
assert_equal(coin_a["parent_descs"][0], multi_a)
772+
coin_b = next(c for c in coins if c["txid"] == txid_b)
773+
assert_equal(coin_b["parent_descs"][0], multi_b)
774+
self.nodes[0].unloadwallet("wo")
775+
743776

744777
if __name__ == '__main__':
745778
WalletTest().main()

test/functional/wallet_listsinceblock.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from test_framework.address import key_to_p2pkh
88
from test_framework.blocktools import COINBASE_MATURITY
9+
from test_framework.descriptors import descsum_create
910
from test_framework.key import ECKey
1011
from test_framework.test_framework import BitcoinTestFramework
1112
from test_framework.messages import BIP125_SEQUENCE_NUMBER
@@ -41,6 +42,8 @@ def run_test(self):
4142
self.test_double_send()
4243
self.double_spends_filtered()
4344
self.test_targetconfirmations()
45+
self.test_desc()
46+
self.test_send_to_self()
4447

4548
def test_no_blockhash(self):
4649
self.log.info("Test no blockhash")
@@ -388,5 +391,65 @@ def double_spends_filtered(self):
388391
assert_equal(original_found, False)
389392
assert_equal(double_found, False)
390393

394+
def test_desc(self):
395+
"""Make sure we can track coins by descriptor."""
396+
self.log.info("Test descriptor lookup by scriptPubKey.")
397+
398+
# Create a watchonly wallet tracking two multisig descriptors.
399+
multi_a = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YBNjUo96Jxd1u4XKWgnoc7LsA1jz3Yc2NiDbhtfBhaBtemB73n9V5vtJHwU6FVXwggTbeoJWQ1rzdz8ysDuQkpnaHyvnvzR/*,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*))")
400+
multi_b = descsum_create("wsh(multi(1,tpubD6NzVbkrYhZ4YHdDGMAYGaWxMSC1B6tPRTHuU5t3BcfcS3nrF523iFm5waFd1pP3ZvJt4Jr8XmCmsTBNx5suhcSgtzpGjGMASR3tau1hJz4/*,tpubD6NzVbkrYhZ4Y2RLiuEzNQkntjmsLpPYDm3LTRBYynUQtDtpzeUKAcb9sYthSFL3YR74cdFgF5mW8yKxv2W2CWuZDFR2dUpE5PF9kbrVXNZ/*))")
401+
self.nodes[0].createwallet(wallet_name="wo", descriptors=True, disable_private_keys=True)
402+
wo_wallet = self.nodes[0].get_wallet_rpc("wo")
403+
wo_wallet.importdescriptors([
404+
{
405+
"desc": multi_a,
406+
"active": False,
407+
"timestamp": "now",
408+
},
409+
{
410+
"desc": multi_b,
411+
"active": False,
412+
"timestamp": "now",
413+
},
414+
])
415+
416+
# Send a coin to each descriptor.
417+
assert_equal(len(wo_wallet.listsinceblock()["transactions"]), 0)
418+
addr_a = self.nodes[0].deriveaddresses(multi_a, 0)[0]
419+
addr_b = self.nodes[0].deriveaddresses(multi_b, 0)[0]
420+
self.nodes[2].sendtoaddress(addr_a, 1)
421+
self.nodes[2].sendtoaddress(addr_b, 2)
422+
self.generate(self.nodes[2], 1)
423+
424+
# We can identify on which descriptor each coin was received.
425+
coins = wo_wallet.listsinceblock()["transactions"]
426+
assert_equal(len(coins), 2)
427+
coin_a = next(c for c in coins if c["amount"] == 1)
428+
assert_equal(coin_a["parent_descs"][0], multi_a)
429+
coin_b = next(c for c in coins if c["amount"] == 2)
430+
assert_equal(coin_b["parent_descs"][0], multi_b)
431+
432+
def test_send_to_self(self):
433+
"""We can make listsinceblock output our change outputs."""
434+
self.log.info("Test the inclusion of change outputs in the output.")
435+
436+
# Create a UTxO paying to one of our change addresses.
437+
block_hash = self.nodes[2].getbestblockhash()
438+
addr = self.nodes[2].getrawchangeaddress()
439+
self.nodes[2].sendtoaddress(addr, 1)
440+
441+
# If we don't list change, we won't have an entry for it.
442+
coins = self.nodes[2].listsinceblock(blockhash=block_hash)["transactions"]
443+
assert not any(c["address"] == addr for c in coins)
444+
445+
# Now if we list change, we'll get both the send (to a change address) and
446+
# the actual change.
447+
res = self.nodes[2].listsinceblock(blockhash=block_hash, include_change=True)
448+
coins = [entry for entry in res["transactions"] if entry["category"] == "receive"]
449+
assert_equal(len(coins), 2)
450+
assert any(c["address"] == addr for c in coins)
451+
assert all(self.nodes[2].getaddressinfo(c["address"])["ischange"] for c in coins)
452+
453+
391454
if __name__ == '__main__':
392455
ListSinceBlockTest().main()

0 commit comments

Comments
 (0)