Skip to content

Commit 151600b

Browse files
committed
Swap in descriptors support into scantxoutset
1 parent 0652c32 commit 151600b

File tree

2 files changed

+49
-125
lines changed

2 files changed

+49
-125
lines changed

src/rpc/blockchain.cpp

Lines changed: 46 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <policy/policy.h>
2121
#include <primitives/transaction.h>
2222
#include <rpc/server.h>
23+
#include <script/descriptor.h>
2324
#include <streams.h>
2425
#include <sync.h>
2526
#include <txdb.h>
@@ -1984,67 +1985,36 @@ class CoinsViewScanReserver
19841985
}
19851986
};
19861987

1987-
static const char *g_default_scantxoutset_script_types[] = { "P2PKH", "P2SH_P2WPKH", "P2WPKH" };
1988-
1989-
enum class OutputScriptType {
1990-
UNKNOWN,
1991-
P2PK,
1992-
P2PKH,
1993-
P2SH_P2WPKH,
1994-
P2WPKH
1995-
};
1996-
1997-
static inline OutputScriptType GetOutputScriptTypeFromString(const std::string& outputtype)
1998-
{
1999-
if (outputtype == "P2PK") return OutputScriptType::P2PK;
2000-
else if (outputtype == "P2PKH") return OutputScriptType::P2PKH;
2001-
else if (outputtype == "P2SH_P2WPKH") return OutputScriptType::P2SH_P2WPKH;
2002-
else if (outputtype == "P2WPKH") return OutputScriptType::P2WPKH;
2003-
else return OutputScriptType::UNKNOWN;
2004-
}
2005-
2006-
CTxDestination GetDestinationForKey(const CPubKey& key, OutputScriptType type)
2007-
{
2008-
switch (type) {
2009-
case OutputScriptType::P2PKH: return key.GetID();
2010-
case OutputScriptType::P2SH_P2WPKH:
2011-
case OutputScriptType::P2WPKH: {
2012-
if (!key.IsCompressed()) return key.GetID();
2013-
CTxDestination witdest = WitnessV0KeyHash(key.GetID());
2014-
if (type == OutputScriptType::P2SH_P2WPKH) {
2015-
CScript witprog = GetScriptForDestination(witdest);
2016-
return CScriptID(witprog);
2017-
} else {
2018-
return witdest;
2019-
}
2020-
}
2021-
default: assert(false);
2022-
}
2023-
}
2024-
20251988
UniValue scantxoutset(const JSONRPCRequest& request)
20261989
{
20271990
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
20281991
throw std::runtime_error(
20291992
"scantxoutset <action> ( <scanobjects> )\n"
2030-
"\nScans the unspent transaction output set for possible entries that matches common scripts of given public keys.\n"
2031-
"Using addresses as scanobjects will _not_ detect unspent P2PK txouts\n"
1993+
"\nScans the unspent transaction output set for entries that match certain output descriptors.\n"
1994+
"Examples of output descriptors are:\n"
1995+
" addr(<address>) Outputs whose scriptPubKey corresponds to the specified address (does not include P2PK)\n"
1996+
" raw(<hex script>) Outputs whose scriptPubKey equals the specified hex scripts\n"
1997+
" combo(<pubkey>) P2PK, P2PKH, P2WPKH, and P2SH-P2WPKH outputs for the given pubkey\n"
1998+
" pkh(<pubkey>) P2PKH outputs for the given pubkey\n"
1999+
" sh(multi(<n>,<pubkey>,<pubkey>,...)) P2SH-multisig outputs for the given threshold and pubkeys\n"
2000+
"\nIn the above, <pubkey> either refers to a fixed public key in hexadecimal notation, or to an xpub/xprv optionally followed by one\n"
2001+
"or more path elements separated by \"/\", and optionally ending in \"/*\" or \"/*'\" to specify all unhardened or hardened child keys.\n"
2002+
"In the latter case, a range needs to be specified by below if different from 1000.\n"
2003+
"For more information on output descriptors, see the documentation at TODO\n"
20322004
"\nArguments:\n"
20332005
"1. \"action\" (string, required) The action to execute\n"
20342006
" \"start\" for starting a scan\n"
20352007
" \"abort\" for aborting the current scan (returns true when abort was successful)\n"
20362008
" \"status\" for progress report (in %) of the current scan\n"
2037-
"2. \"scanobjects\" (array, optional) Array of scan objects (only one object type per scan object allowed)\n"
2038-
" [\n"
2039-
" { \"address\" : \"<address>\" }, (string, optional) Bitcoin address\n"
2040-
" { \"script\" : \"<scriptPubKey>\" }, (string, optional) HEX encoded script (scriptPubKey)\n"
2041-
" { \"pubkey\" : (object, optional) Public key\n"
2042-
" {\n"
2043-
" \"pubkey\" : \"<pubkey\">, (string, required) HEX encoded public key\n"
2044-
" \"script_types\" : [ ... ], (array, optional) Array of script-types to derive from the pubkey (possible values: \"P2PK\", \"P2PKH\", \"P2SH-P2WPKH\", \"P2WPKH\")\n"
2045-
" }\n"
2009+
"2. \"scanobjects\" (array, required) Array of scan objects\n"
2010+
" [ Every scan object is either a string descriptor or an object:\n"
2011+
" \"descriptor\", (string, optional) An output descriptor\n"
2012+
" { (object, optional) An object with output descriptor and metadata\n"
2013+
" \"desc\": \"descriptor\", (string, required) An output descriptor\n"
2014+
" \"range\": n, (numeric, optional) Up to what child index HD chains should be explored (default: 1000)\n"
20462015
" },\n"
2047-
" ]\n"
2016+
" ...\n"
2017+
" ]\n"
20482018
"\nResult:\n"
20492019
"{\n"
20502020
" \"unspents\": [\n"
@@ -2090,79 +2060,35 @@ UniValue scantxoutset(const JSONRPCRequest& request)
20902060

20912061
// loop through the scan objects
20922062
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
2093-
if (!scanobject.isObject()) {
2094-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid scan object");
2095-
}
2096-
UniValue address_uni = find_value(scanobject, "address");
2097-
UniValue pubkey_uni = find_value(scanobject, "pubkey");
2098-
UniValue script_uni = find_value(scanobject, "script");
2099-
2100-
// make sure only one object type is present
2101-
if (1 != !address_uni.isNull() + !pubkey_uni.isNull() + !script_uni.isNull()) {
2102-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Only one object type is allowed per scan object");
2103-
} else if (!address_uni.isNull() && !address_uni.isStr()) {
2104-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"address\" must contain a single string as value");
2105-
} else if (!pubkey_uni.isNull() && !pubkey_uni.isObject()) {
2106-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"pubkey\" must contain an object as value");
2107-
} else if (!script_uni.isNull() && !script_uni.isStr()) {
2108-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scanobject \"script\" must contain a single string as value");
2109-
} else if (address_uni.isStr()) {
2110-
// type: address
2111-
// decode destination and derive the scriptPubKey
2112-
// add the script to the scan containers
2113-
CTxDestination dest = DecodeDestination(address_uni.get_str());
2114-
if (!IsValidDestination(dest)) {
2115-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address");
2116-
}
2117-
CScript script = GetScriptForDestination(dest);
2118-
assert(!script.empty());
2119-
needles.insert(script);
2120-
} else if (pubkey_uni.isObject()) {
2121-
// type: pubkey
2122-
// derive script(s) according to the script_type parameter
2123-
UniValue script_types_uni = find_value(pubkey_uni, "script_types");
2124-
UniValue pubkeydata_uni = find_value(pubkey_uni, "pubkey");
2125-
2126-
// check the script types and use the default if not provided
2127-
if (!script_types_uni.isNull() && !script_types_uni.isArray()) {
2128-
throw JSONRPCError(RPC_INVALID_PARAMETER, "script_types must be an array");
2129-
} else if (script_types_uni.isNull()) {
2130-
// use the default script types
2131-
script_types_uni = UniValue(UniValue::VARR);
2132-
for (const char *t : g_default_scantxoutset_script_types) {
2133-
script_types_uni.push_back(t);
2134-
}
2135-
}
2136-
2137-
// check the acctual pubkey
2138-
if (!pubkeydata_uni.isStr() || !IsHex(pubkeydata_uni.get_str())) {
2139-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Public key must be hex encoded");
2140-
}
2141-
CPubKey pubkey(ParseHexV(pubkeydata_uni, "pubkey"));
2142-
if (!pubkey.IsFullyValid()) {
2143-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key");
2063+
std::string desc_str;
2064+
int range = 1000;
2065+
if (scanobject.isStr()) {
2066+
desc_str = scanobject.get_str();
2067+
} else if (scanobject.isObject()) {
2068+
UniValue desc_uni = find_value(scanobject, "desc");
2069+
if (desc_uni.isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor needs to be provided in scan object");
2070+
desc_str = desc_uni.get_str();
2071+
UniValue range_uni = find_value(scanobject, "range");
2072+
if (!range_uni.isNull()) {
2073+
range = range_uni.get_int();
2074+
if (range < 0 || range > 1000000) throw JSONRPCError(RPC_INVALID_PARAMETER, "range out of range");
21442075
}
2076+
} else {
2077+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
2078+
}
21452079

2146-
// loop through the script types and derive the script
2147-
for (const UniValue& script_type_uni : script_types_uni.get_array().getValues()) {
2148-
OutputScriptType script_type = GetOutputScriptTypeFromString(script_type_uni.get_str());
2149-
if (script_type == OutputScriptType::UNKNOWN) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid script type");
2150-
CScript script;
2151-
if (script_type == OutputScriptType::P2PK) {
2152-
// support legacy P2PK scripts
2153-
script << ToByteVector(pubkey) << OP_CHECKSIG;
2154-
} else {
2155-
script = GetScriptForDestination(GetDestinationForKey(pubkey, script_type));
2156-
}
2157-
assert(!script.empty());
2158-
needles.insert(script);
2080+
FlatSigningProvider provider;
2081+
auto desc = Parse(desc_str, provider);
2082+
if (!desc) {
2083+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
2084+
}
2085+
if (!desc->IsRange()) range = 0;
2086+
for (int i = 0; i <= range; ++i) {
2087+
std::vector<CScript> scripts;
2088+
if (!desc->Expand(i, provider, scripts, provider)) {
2089+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
21592090
}
2160-
} else if (script_uni.isStr()) {
2161-
// type: script
2162-
// check and add the script to the scan containers (needles array)
2163-
CScript script(ParseHexV(script_uni, "script"));
2164-
// TODO: check script: max length, has OP, is unspenable etc.
2165-
needles.insert(script);
2091+
needles.insert(scripts.begin(), scripts.end());
21662092
}
21672093
}
21682094

test/functional/rpc_scantxoutset.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,11 @@ def run_test(self):
3636

3737
self.restart_node(0, ['-nowallet'])
3838
self.log.info("Test if we have found the non HD unspent outputs.")
39-
assert_equal(self.nodes[0].scantxoutset("start", [ {"pubkey": {"pubkey": pubk1}}, {"pubkey": {"pubkey": pubk2}}, {"pubkey": {"pubkey": pubk3}}])['total_amount'], 6)
40-
assert_equal(self.nodes[0].scantxoutset("start", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"address": addr_BECH32}])['total_amount'], 6)
41-
assert_equal(self.nodes[0].scantxoutset("start", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"pubkey": {"pubkey": pubk3}} ])['total_amount'], 6)
39+
assert_equal(self.nodes[0].scantxoutset("start", [ "combo(" + pubk1 + ")", "combo(" + pubk2 + ")", "combo(" + pubk3 + ")"])['total_amount'], 6)
40+
assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "addr(" + addr_BECH32 + ")"])['total_amount'], 6)
41+
assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "combo(" + pubk3 + ")"])['total_amount'], 6)
4242

4343
self.log.info("Test invalid parameters.")
44-
assert_raises_rpc_error(-8, 'Scanobject "pubkey" must contain an object as value', self.nodes[0].scantxoutset, "start", [ {"pubkey": pubk1}]) #missing pubkey object
45-
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
4644

4745
if __name__ == '__main__':
4846
ScantxoutsetTest().main()

0 commit comments

Comments
 (0)