Merge #20226: wallet, rpc: add listdescriptors command

647b81b709 wallet, rpc: add listdescriptors command (Ivan Metlushko)

Pull request description:

  Looking for concept ACKs

  **Rationale**: allow users to inspect the contents of their newly created descriptor wallets.

  Currently the command only returns xpubs which is not very useful in itself, but there are multiples ways to extend it:
   * add an option to export xprv
   * with #19136 it'll be possible to return normalised descriptors suitable for a watch-only purposes

  The output is compatible with `importdescriptors` command so it could be easily used for backup/recover purposes.

  **Output example:**
  ```json
  [
    {
      "desc": "wpkh(tpubD6NzVbkrYhZ4WW6E2ZETFyNfq2hfF23SKxqSGFvUpPAY58jmmuBybwqwFihAyQPk9KnwTt5516NDZRJ7k5QPeKjy7wuVd5WvXNxwwAs5tUD/*)#nhavpr5h",
      "timestamp": 1296688602,
      "active": false,
      "range": [
        0,
        999
      ],
      "next": 0
    }
  ]
  ```

ACKs for top commit:
  jonatack:
    re-ACK 647b81b709 rebased to master, debug builds cleanly, reviewed diff since last review, tested with a descriptor wallet (and with a legacy wallet)
  achow101:
    re-ACK 647b81b

Tree-SHA512: 51a3620bb17c836c52cecb066d4fa9d5ff418af56809046eaee0528c4dc240a4e90fff5711ba96e399c6664e00b9ee8194e33852b1b9e75af18061296e19a8a7
This commit is contained in:
Samuel Dobson 2021-01-28 13:16:02 +13:00
commit 9deba2de76
No known key found for this signature in database
GPG Key ID: D300116E1C875A3D
4 changed files with 139 additions and 0 deletions

View File

@ -1740,3 +1740,72 @@ RPCHelpMan importdescriptors()
},
};
}
RPCHelpMan listdescriptors()
{
return RPCHelpMan{
"listdescriptors",
"\nList descriptors imported into a descriptor-enabled wallet.",
{},
RPCResult{
RPCResult::Type::ARR, "", "Response is an array of descriptor objects",
{
{RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::STR, "desc", "Descriptor string representation"},
{RPCResult::Type::NUM, "timestamp", "The creation time of the descriptor"},
{RPCResult::Type::BOOL, "active", "Activeness flag"},
{RPCResult::Type::BOOL, "internal", true, "Whether this is internal or external descriptor; defined only for active descriptors"},
{RPCResult::Type::ARR_FIXED, "range", true, "Defined only for ranged descriptors", {
{RPCResult::Type::NUM, "", "Range start inclusive"},
{RPCResult::Type::NUM, "", "Range end inclusive"},
}},
{RPCResult::Type::NUM, "next", true, "The next index to generate addresses from; defined only for ranged descriptors"},
}},
}
},
RPCExamples{
HelpExampleCli("listdescriptors", "") + HelpExampleRpc("listdescriptors", "")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
if (!wallet) return NullUniValue;
if (!wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) {
throw JSONRPCError(RPC_WALLET_ERROR, "listdescriptors is not available for non-descriptor wallets");
}
LOCK(wallet->cs_wallet);
UniValue response(UniValue::VARR);
const auto active_spk_mans = wallet->GetActiveScriptPubKeyMans();
for (const auto& spk_man : wallet->GetAllScriptPubKeyMans()) {
const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man);
if (!desc_spk_man) {
throw JSONRPCError(RPC_WALLET_ERROR, "Unexpected ScriptPubKey manager type.");
}
UniValue spk(UniValue::VOBJ);
LOCK(desc_spk_man->cs_desc_man);
const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
spk.pushKV("desc", wallet_descriptor.descriptor->ToString());
spk.pushKV("timestamp", wallet_descriptor.creation_time);
const bool active = active_spk_mans.count(desc_spk_man) != 0;
spk.pushKV("active", active);
const auto& type = wallet_descriptor.descriptor->GetOutputType();
if (active && type != nullopt) {
spk.pushKV("internal", wallet->GetScriptPubKeyMan(*type, true) == desc_spk_man);
}
if (wallet_descriptor.descriptor->IsRange()) {
UniValue range(UniValue::VARR);
range.push_back(wallet_descriptor.range_start);
range.push_back(wallet_descriptor.range_end - 1);
spk.pushKV("range", range);
spk.pushKV("next", wallet_descriptor.next_index);
}
response.push_back(spk);
}
return response;
},
};
}

View File

@ -4551,6 +4551,7 @@ RPCHelpMan importprunedfunds();
RPCHelpMan removeprunedfunds();
RPCHelpMan importmulti();
RPCHelpMan importdescriptors();
RPCHelpMan listdescriptors();
Span<const CRPCCommand> GetWalletRPCCommands()
{
@ -4589,6 +4590,7 @@ static const CRPCCommand commands[] =
{ "wallet", "importwallet", &importwallet, {"filename"} },
{ "wallet", "keypoolrefill", &keypoolrefill, {"newsize"} },
{ "wallet", "listaddressgroupings", &listaddressgroupings, {} },
{ "wallet", "listdescriptors", &listdescriptors, {} },
{ "wallet", "listlabels", &listlabels, {"purpose"} },
{ "wallet", "listlockunspent", &listlockunspent, {} },
{ "wallet", "listreceivedbyaddress", &listreceivedbyaddress, {"minconf","include_empty","include_watchonly","address_filter"} },

View File

@ -238,6 +238,7 @@ BASE_SCRIPTS = [
'rpc_named_arguments.py',
'wallet_listsinceblock.py',
'wallet_listsinceblock.py --descriptors',
'wallet_listdescriptors.py --descriptors',
'p2p_leak.py',
'wallet_encryption.py',
'wallet_encryption.py --descriptors',

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2021 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 the listdescriptors RPC."""
from test_framework.descriptors import (
descsum_create
)
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
)
class ListDescriptorsTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
self.skip_if_no_sqlite()
# do not create any wallet by default
def init_wallet(self, i):
return
def run_test(self):
node = self.nodes[0]
assert_raises_rpc_error(-18, 'No wallet is loaded.', node.listdescriptors)
self.log.info('Test that the command is not available for legacy wallets.')
node.createwallet(wallet_name='w1', descriptors=False)
assert_raises_rpc_error(-4, 'listdescriptors is not available for non-descriptor wallets', node.listdescriptors)
self.log.info('Test the command for empty descriptors wallet.')
node.createwallet(wallet_name='w2', blank=True, descriptors=True)
assert_equal(0, len(node.get_wallet_rpc('w2').listdescriptors()))
self.log.info('Test the command for a default descriptors wallet.')
node.createwallet(wallet_name='w3', descriptors=True)
result = node.get_wallet_rpc('w3').listdescriptors()
assert_equal(6, len(result))
assert_equal(6, len([d for d in result if d['active']]))
assert_equal(3, len([d for d in result if d['internal']]))
for item in result:
assert item['desc'] != ''
assert item['next'] == 0
assert item['range'] == [0, 0]
assert item['timestamp'] is not None
self.log.info('Test non-active non-range combo descriptor')
node.createwallet(wallet_name='w4', blank=True, descriptors=True)
wallet = node.get_wallet_rpc('w4')
wallet.importdescriptors([{
'desc': descsum_create('combo(' + node.get_deterministic_priv_key().key + ')'),
'timestamp': 1296688602,
}])
expected = [{'active': False,
'desc': 'combo(0227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f)#np574htj',
'timestamp': 1296688602}]
assert_equal(expected, wallet.listdescriptors())
if __name__ == '__main__':
ListDescriptorsTest().main()