Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deriveaddresses RPC util method #14667

Merged
merged 1 commit into from Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/release-notes-14667.md
@@ -0,0 +1,4 @@
New RPC methods
------------

- `deriveaddresses` returns one or more addresses corresponding to an [output descriptor](/doc/descriptors.md).
2 changes: 2 additions & 0 deletions src/rpc/client.cpp
Expand Up @@ -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" },
Expand Down
91 changes: 91 additions & 0 deletions src/rpc/misc.cpp
Expand Up @@ -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>
Expand Down Expand Up @@ -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."},
Sjors marked this conversation as resolved.
Show resolved Hide resolved
{"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");
Sjors marked this conversation as resolved.
Show resolved Hide resolved
}
if (range_begin > range_end) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range end should be equal to or greater than begin");
}
}
Sjors marked this conversation as resolved.
Show resolved Hide resolved

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;
Sjors marked this conversation as resolved.
Show resolved Hide resolved
}

static UniValue verifymessage(const JSONRPCRequest& request)
{
if (request.fHelp || request.params.size() != 3)
Expand Down Expand Up @@ -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"} },

Expand Down
50 changes: 50 additions & 0 deletions test/functional/rpc_deriveaddresses.py
@@ -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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a wallet roundtrip test like:

wpkh_info = self.nodes[0].getaddressinfo(self.nodes[0].getnewaddress("", "bech32"))
# Trim the witness part of descriptor off, check pkh entry in wallet, match descriptor
pkh_desc = wpkh_info["desc"][1:]
pkh_addr = self.nodes[0].deriveaddresses(pkh_desc)
pkh_info = self.nodes[0].getaddressinfo(pkh_addr)
assert_equal(wpkh_info["hdkeypath"], pkh_info["hdkeypath"])
assert_equal(wpkh_info["pubkey"], pkh_info["pubkey"])

if __name__ == '__main__':
DeriveaddressesTest().main()
2 changes: 2 additions & 0 deletions test/functional/test_runner.py
Expand Up @@ -181,6 +181,8 @@
'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',
Expand Down