Swap in descriptors support into scantxoutset

This commit is contained in:
Pieter Wuille 2018-07-17 13:41:28 -07:00
parent 0652c3284f
commit 151600bb49
2 changed files with 49 additions and 125 deletions

View File

@ -20,6 +20,7 @@
#include <policy/policy.h> #include <policy/policy.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <rpc/server.h> #include <rpc/server.h>
#include <script/descriptor.h>
#include <streams.h> #include <streams.h>
#include <sync.h> #include <sync.h>
#include <txdb.h> #include <txdb.h>
@ -1984,66 +1985,35 @@ public:
} }
}; };
static const char *g_default_scantxoutset_script_types[] = { "P2PKH", "P2SH_P2WPKH", "P2WPKH" };
enum class OutputScriptType {
UNKNOWN,
P2PK,
P2PKH,
P2SH_P2WPKH,
P2WPKH
};
static inline OutputScriptType GetOutputScriptTypeFromString(const std::string& outputtype)
{
if (outputtype == "P2PK") return OutputScriptType::P2PK;
else if (outputtype == "P2PKH") return OutputScriptType::P2PKH;
else if (outputtype == "P2SH_P2WPKH") return OutputScriptType::P2SH_P2WPKH;
else if (outputtype == "P2WPKH") return OutputScriptType::P2WPKH;
else return OutputScriptType::UNKNOWN;
}
CTxDestination GetDestinationForKey(const CPubKey& key, OutputScriptType type)
{
switch (type) {
case OutputScriptType::P2PKH: return key.GetID();
case OutputScriptType::P2SH_P2WPKH:
case OutputScriptType::P2WPKH: {
if (!key.IsCompressed()) return key.GetID();
CTxDestination witdest = WitnessV0KeyHash(key.GetID());
if (type == OutputScriptType::P2SH_P2WPKH) {
CScript witprog = GetScriptForDestination(witdest);
return CScriptID(witprog);
} else {
return witdest;
}
}
default: assert(false);
}
}
UniValue scantxoutset(const JSONRPCRequest& request) UniValue scantxoutset(const JSONRPCRequest& request)
{ {
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error( throw std::runtime_error(
"scantxoutset <action> ( <scanobjects> )\n" "scantxoutset <action> ( <scanobjects> )\n"
"\nScans the unspent transaction output set for possible entries that matches common scripts of given public keys.\n" "\nScans the unspent transaction output set for entries that match certain output descriptors.\n"
"Using addresses as scanobjects will _not_ detect unspent P2PK txouts\n" "Examples of output descriptors are:\n"
" addr(<address>) Outputs whose scriptPubKey corresponds to the specified address (does not include P2PK)\n"
" raw(<hex script>) Outputs whose scriptPubKey equals the specified hex scripts\n"
" combo(<pubkey>) P2PK, P2PKH, P2WPKH, and P2SH-P2WPKH outputs for the given pubkey\n"
" pkh(<pubkey>) P2PKH outputs for the given pubkey\n"
" sh(multi(<n>,<pubkey>,<pubkey>,...)) P2SH-multisig outputs for the given threshold and pubkeys\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 \"/\", and optionally ending in \"/*\" or \"/*'\" to specify all unhardened or hardened child keys.\n"
"In the latter case, a range needs to be specified by below if different from 1000.\n"
"For more information on output descriptors, see the documentation at TODO\n"
"\nArguments:\n" "\nArguments:\n"
"1. \"action\" (string, required) The action to execute\n" "1. \"action\" (string, required) The action to execute\n"
" \"start\" for starting a scan\n" " \"start\" for starting a scan\n"
" \"abort\" for aborting the current scan (returns true when abort was successful)\n" " \"abort\" for aborting the current scan (returns true when abort was successful)\n"
" \"status\" for progress report (in %) of the current scan\n" " \"status\" for progress report (in %) of the current scan\n"
"2. \"scanobjects\" (array, optional) Array of scan objects (only one object type per scan object allowed)\n" "2. \"scanobjects\" (array, required) Array of scan objects\n"
" [\n" " [ Every scan object is either a string descriptor or an object:\n"
" { \"address\" : \"<address>\" }, (string, optional) Bitcoin address\n" " \"descriptor\", (string, optional) An output descriptor\n"
" { \"script\" : \"<scriptPubKey>\" }, (string, optional) HEX encoded script (scriptPubKey)\n" " { (object, optional) An object with output descriptor and metadata\n"
" { \"pubkey\" : (object, optional) Public key\n" " \"desc\": \"descriptor\", (string, required) An output descriptor\n"
" {\n" " \"range\": n, (numeric, optional) Up to what child index HD chains should be explored (default: 1000)\n"
" \"pubkey\" : \"<pubkey\">, (string, required) HEX encoded public key\n"
" \"script_types\" : [ ... ], (array, optional) Array of script-types to derive from the pubkey (possible values: \"P2PK\", \"P2PKH\", \"P2SH-P2WPKH\", \"P2WPKH\")\n"
" }\n"
" },\n" " },\n"
" ...\n"
" ]\n" " ]\n"
"\nResult:\n" "\nResult:\n"
"{\n" "{\n"
@ -2090,79 +2060,35 @@ UniValue scantxoutset(const JSONRPCRequest& request)
// loop through the scan objects // loop through the scan objects
for (const UniValue& scanobject : request.params[1].get_array().getValues()) { for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
if (!scanobject.isObject()) { std::string desc_str;
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid scan object"); int range = 1000;
if (scanobject.isStr()) {
desc_str = scanobject.get_str();
} else if (scanobject.isObject()) {
UniValue desc_uni = find_value(scanobject, "desc");
if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object");
desc_str = desc_uni.get_str();
UniValue range_uni = find_value(scanobject, "range");
if (!range_uni.isNull()) {
range = range_uni.get_int();
if (range < 0 || range > 1000000) throw JSONRPCError(RPC_INVALID_PARAMETER, "range out of range");
} }
UniValue address_uni = find_value(scanobject, "address");
UniValue pubkey_uni = find_value(scanobject, "pubkey");
UniValue script_uni = find_value(scanobject, "script");
// make sure only one object type is present
if (1 != !address_uni.isNull() + !pubkey_uni.isNull() + !script_uni.isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Only one object type is allowed per scan object");
} else if (!address_uni.isNull() && !address_uni.isStr()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"address\" must contain a single string as value");
} else if (!pubkey_uni.isNull() && !pubkey_uni.isObject()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"pubkey\" must contain an object as value");
} else if (!script_uni.isNull() && !script_uni.isStr()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"script\" must contain a single string as value");
} else if (address_uni.isStr()) {
// type: address
// decode destination and derive the scriptPubKey
// add the script to the scan containers
CTxDestination dest = DecodeDestination(address_uni.get_str());
if (!IsValidDestination(dest)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address");
}
CScript script = GetScriptForDestination(dest);
assert(!script.empty());
needles.insert(script);
} else if (pubkey_uni.isObject()) {
// type: pubkey
// derive script(s) according to the script_type parameter
UniValue script_types_uni = find_value(pubkey_uni, "script_types");
UniValue pubkeydata_uni = find_value(pubkey_uni, "pubkey");
// check the script types and use the default if not provided
if (!script_types_uni.isNull() && !script_types_uni.isArray()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "script_types must be an array");
} else if (script_types_uni.isNull()) {
// use the default script types
script_types_uni = UniValue(UniValue::VARR);
for (const char *t : g_default_scantxoutset_script_types) {
script_types_uni.push_back(t);
}
}
// check the acctual pubkey
if (!pubkeydata_uni.isStr() || !IsHex(pubkeydata_uni.get_str())) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Public key must be hex encoded");
}
CPubKey pubkey(ParseHexV(pubkeydata_uni, "pubkey"));
if (!pubkey.IsFullyValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key");
}
// loop through the script types and derive the script
for (const UniValue& script_type_uni : script_types_uni.get_array().getValues()) {
OutputScriptType script_type = GetOutputScriptTypeFromString(script_type_uni.get_str());
if (script_type == OutputScriptType::UNKNOWN) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid script type");
CScript script;
if (script_type == OutputScriptType::P2PK) {
// support legacy P2PK scripts
script << ToByteVector(pubkey) << OP_CHECKSIG;
} else { } else {
script = GetScriptForDestination(GetDestinationForKey(pubkey, script_type)); throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
} }
assert(!script.empty());
needles.insert(script); FlatSigningProvider provider;
auto desc = Parse(desc_str, provider);
if (!desc) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
} }
} else if (script_uni.isStr()) { if (!desc->IsRange()) range = 0;
// type: script for (int i = 0; i <= range; ++i) {
// check and add the script to the scan containers (needles array) std::vector<CScript> scripts;
CScript script(ParseHexV(script_uni, "script")); if (!desc->Expand(i, provider, scripts, provider)) {
// TODO: check script: max length, has OP, is unspenable etc. throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
needles.insert(script); }
needles.insert(scripts.begin(), scripts.end());
} }
} }

View File

@ -36,13 +36,11 @@ class ScantxoutsetTest(BitcoinTestFramework):
self.restart_node(0, ['-nowallet']) self.restart_node(0, ['-nowallet'])
self.log.info("Test if we have found the non HD unspent outputs.") self.log.info("Test if we have found the non HD unspent outputs.")
assert_equal(self.nodes[0].scantxoutset("start", [ {"pubkey": {"pubkey": pubk1}}, {"pubkey": {"pubkey": pubk2}}, {"pubkey": {"pubkey": pubk3}}])['total_amount'], 6) assert_equal(self.nodes[0].scantxoutset("start", [ "combo(" + pubk1 + ")", "combo(" + pubk2 + ")", "combo(" + pubk3 + ")"])['total_amount'], 6)
assert_equal(self.nodes[0].scantxoutset("start", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"address": addr_BECH32}])['total_amount'], 6) assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "addr(" + addr_BECH32 + ")"])['total_amount'], 6)
assert_equal(self.nodes[0].scantxoutset("start", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"pubkey": {"pubkey": pubk3}} ])['total_amount'], 6) assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "combo(" + pubk3 + ")"])['total_amount'], 6)
self.log.info("Test invalid parameters.") self.log.info("Test invalid parameters.")
assert_raises_rpc_error(-8, 'Scanobject "pubkey" must contain an object as value', self.nodes[0].scantxoutset, "start", [ {"pubkey": pubk1}]) #missing pubkey object
assert_raises_rpc_error(-8, 'Scanobject "address" must contain a single string as value', self.nodes[0].scantxoutset, "start", [ {"address": {"address": addr_P2SH_SEGWIT}}]) #invalid object for address object
if __name__ == '__main__': if __name__ == '__main__':
ScantxoutsetTest().main() ScantxoutsetTest().main()