Merge #14667: Add deriveaddresses RPC util method
595283851
[rpc] util: add deriveaddresses method (Sjors Provoost)
Pull request description:
Usage:
```sh
bitcoin-cli deriveaddresses "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/0)"
[
"bc1qg6ucjz7kgdedam7v5yarecy54uqw82yym06z3q"
] // part of the BIP32 test vector
```
Avoids the need for external (BIP32) libraries to derive an address. Can be used in conjunction with `scantxoutset` as a poor mans wallet. Might be useful to test more complicated future descriptors.
~To keep it as simple as possible it only supports descriptors that result in a single address, so no `combo()` and ranges.~
As discussed recently on IRC it might make sense to put this in a separate utility along with other descriptor and psbt utility functions which don't need a chain or wallet context. However I prefer to leave that to another PR.
Tree-SHA512: b8e53db11a8fd87638cc98766270cc3be9adc4b3e5085798a6a4e2e6ad252bf6d2189346bbb2da72d04d13f7f1e80b5cb88e8039653bea1f150602a876ef7f34
This commit is contained in:
commit
1933e38c1a
5 changed files with 149 additions and 0 deletions
4
doc/release-notes-14667.md
Normal file
4
doc/release-notes-14667.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
New RPC methods
|
||||
------------
|
||||
|
||||
- `deriveaddresses` returns one or more addresses corresponding to an [output descriptor](/doc/descriptors.md).
|
|
@ -68,6 +68,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "sendmany", 4, "subtractfeefrom" },
|
||||
{ "sendmany", 5 , "replaceable" },
|
||||
{ "sendmany", 6 , "conf_target" },
|
||||
{ "deriveaddresses", 1, "begin" },
|
||||
{ "deriveaddresses", 2, "end" },
|
||||
{ "scantxoutset", 1, "scanobjects" },
|
||||
{ "addmultisigaddress", 0, "nrequired" },
|
||||
{ "addmultisigaddress", 1, "keys" },
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include <rpc/blockchain.h>
|
||||
#include <rpc/server.h>
|
||||
#include <rpc/util.h>
|
||||
#include <script/descriptor.h>
|
||||
#include <timedata.h>
|
||||
#include <util/system.h>
|
||||
#include <util/strencodings.h>
|
||||
|
@ -142,6 +143,95 @@ static UniValue createmultisig(const JSONRPCRequest& request)
|
|||
return result;
|
||||
}
|
||||
|
||||
UniValue deriveaddresses(const JSONRPCRequest& request)
|
||||
{
|
||||
if (request.fHelp || request.params.empty() || request.params.size() > 3) {
|
||||
throw std::runtime_error(
|
||||
RPCHelpMan{"deriveaddresses",
|
||||
{"\nDerives one or more addresses corresponding to an output descriptor.\n"
|
||||
"Examples of output descriptors are:\n"
|
||||
" pkh(<pubkey>) P2PKH outputs for the given pubkey\n"
|
||||
" wpkh(<pubkey>) Native segwit P2PKH outputs for the given pubkey\n"
|
||||
" sh(multi(<n>,<pubkey>,<pubkey>,...)) P2SH-multisig outputs for the given threshold and pubkeys\n"
|
||||
" raw(<hex script>) Outputs whose scriptPubKey equals the specified hex scripts\n"
|
||||
"\nIn the above, <pubkey> either refers to a fixed public key in hexadecimal notation, or to an xpub/xprv optionally followed by one\n"
|
||||
"or more path elements separated by \"/\", where \"h\" represents a hardened child key.\n"
|
||||
"For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"},
|
||||
{
|
||||
{"descriptor", RPCArg::Type::STR, /* opt */ false, /* default_val */ "", "The descriptor."},
|
||||
{"begin", RPCArg::Type::NUM, /* opt */ true, /* default_val */ "", "If a ranged descriptor is used, this specifies the beginning of the range to import."},
|
||||
{"end", RPCArg::Type::NUM, /* opt */ true, /* default_val */ "", "If a ranged descriptor is used, this specifies the end of the range to import."}
|
||||
},
|
||||
RPCResult{
|
||||
"[ address ] (array) the derived addresses\n"
|
||||
},
|
||||
RPCExamples{
|
||||
"First three native segwit receive addresses\n" +
|
||||
HelpExampleCli("deriveaddresses", "\"wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)\" 0 2")
|
||||
}}.ToString()
|
||||
);
|
||||
}
|
||||
|
||||
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VNUM, UniValue::VNUM});
|
||||
const std::string desc_str = request.params[0].get_str();
|
||||
|
||||
int range_begin = 0;
|
||||
int range_end = 0;
|
||||
|
||||
if (request.params.size() >= 2) {
|
||||
if (request.params.size() == 2) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Missing range end parameter");
|
||||
}
|
||||
range_begin = request.params[1].get_int();
|
||||
range_end = request.params[2].get_int();
|
||||
if (range_begin < 0) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should be greater or equal than 0");
|
||||
}
|
||||
if (range_begin > range_end) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range end should be equal to or greater than begin");
|
||||
}
|
||||
}
|
||||
|
||||
FlatSigningProvider provider;
|
||||
auto desc = Parse(desc_str, provider);
|
||||
if (!desc) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor"));
|
||||
}
|
||||
|
||||
if (!desc->IsRange() && request.params.size() > 1) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
|
||||
}
|
||||
|
||||
if (desc->IsRange() && request.params.size() == 1) {
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range must be specified for a ranged descriptor");
|
||||
}
|
||||
|
||||
UniValue addresses(UniValue::VARR);
|
||||
|
||||
for (int i = range_begin; i <= range_end; ++i) {
|
||||
std::vector<CScript> scripts;
|
||||
if (!desc->Expand(i, provider, scripts, provider)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys"));
|
||||
}
|
||||
|
||||
for (const CScript &script : scripts) {
|
||||
CTxDestination dest;
|
||||
if (!ExtractDestination(script, dest)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Descriptor does not have a corresponding address"));
|
||||
}
|
||||
|
||||
addresses.push_back(EncodeDestination(dest));
|
||||
}
|
||||
}
|
||||
|
||||
// This should not be possible, but an assert seems overkill:
|
||||
if (addresses.empty()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result");
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
static UniValue verifymessage(const JSONRPCRequest& request)
|
||||
{
|
||||
if (request.fHelp || request.params.size() != 3)
|
||||
|
@ -473,6 +563,7 @@ static const CRPCCommand commands[] =
|
|||
{ "control", "logging", &logging, {"include", "exclude"}},
|
||||
{ "util", "validateaddress", &validateaddress, {"address"} },
|
||||
{ "util", "createmultisig", &createmultisig, {"nrequired","keys","address_type"} },
|
||||
{ "util", "deriveaddresses", &deriveaddresses, {"descriptor", "begin", "end"} },
|
||||
{ "util", "verifymessage", &verifymessage, {"address","signature","message"} },
|
||||
{ "util", "signmessagewithprivkey", &signmessagewithprivkey, {"privkey","message"} },
|
||||
|
||||
|
|
50
test/functional/rpc_deriveaddresses.py
Executable file
50
test/functional/rpc_deriveaddresses.py
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2018 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 deriveaddresses rpc call."""
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import assert_equal, assert_raises_rpc_error
|
||||
|
||||
class DeriveaddressesTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
self.supports_cli = 1
|
||||
|
||||
def run_test(self):
|
||||
assert_raises_rpc_error(-5, "Invalid descriptor", self.nodes[0].deriveaddresses, "a")
|
||||
|
||||
descriptor = "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"
|
||||
address = "bcrt1qjqmxmkpmxt80xz4y3746zgt0q3u3ferr34acd5"
|
||||
|
||||
assert_equal(self.nodes[0].deriveaddresses(descriptor), [address])
|
||||
|
||||
descriptor_pubkey = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/0)"
|
||||
address = "bcrt1qjqmxmkpmxt80xz4y3746zgt0q3u3ferr34acd5"
|
||||
|
||||
assert_equal(self.nodes[0].deriveaddresses(descriptor_pubkey), [address])
|
||||
|
||||
ranged_descriptor = "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)"
|
||||
assert_equal(self.nodes[0].deriveaddresses(ranged_descriptor, 0, 2), [address, "bcrt1qhku5rq7jz8ulufe2y6fkcpnlvpsta7rq4442dy", "bcrt1qpgptk2gvshyl0s9lqshsmx932l9ccsv265tvaq"])
|
||||
|
||||
assert_raises_rpc_error(-8, "Range should not be specified for an un-ranged descriptor", self.nodes[0].deriveaddresses, "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)", 0, 2)
|
||||
|
||||
assert_raises_rpc_error(-8, "Range must be specified for a ranged descriptor", self.nodes[0].deriveaddresses, "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)")
|
||||
|
||||
assert_raises_rpc_error(-8, "Missing range end parameter", self.nodes[0].deriveaddresses, "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", 0)
|
||||
|
||||
assert_raises_rpc_error(-8, "Range end should be equal to or greater than begin", self.nodes[0].deriveaddresses, "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", 2, 0)
|
||||
|
||||
assert_raises_rpc_error(-8, "Range should be greater or equal than 0", self.nodes[0].deriveaddresses, "wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", -1, 0)
|
||||
|
||||
combo_descriptor = "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"
|
||||
assert_equal(self.nodes[0].deriveaddresses(combo_descriptor), ["mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", "mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", address, "2NDvEwGfpEqJWfybzpKPHF2XH3jwoQV3D7x"])
|
||||
|
||||
hardened_without_privkey_descriptor = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1'/1/0)"
|
||||
assert_raises_rpc_error(-5, "Cannot derive script without private keys", self.nodes[0].deriveaddresses, hardened_without_privkey_descriptor)
|
||||
|
||||
bare_multisig_descriptor = "multi(1, tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/0, tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1)"
|
||||
assert_raises_rpc_error(-5, "Descriptor does not have a corresponding address", self.nodes[0].deriveaddresses, bare_multisig_descriptor)
|
||||
|
||||
if __name__ == '__main__':
|
||||
DeriveaddressesTest().main()
|
|
@ -182,6 +182,8 @@ BASE_SCRIPTS = [
|
|||
'feature_filelock.py',
|
||||
'p2p_unrequested_blocks.py',
|
||||
'feature_includeconf.py',
|
||||
'rpc_deriveaddresses.py',
|
||||
'rpc_deriveaddresses.py --usecli',
|
||||
'rpc_scantxoutset.py',
|
||||
'feature_logging.py',
|
||||
'p2p_node_network_limited.py',
|
||||
|
|
Loading…
Reference in a new issue