aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWladimir J. van der Laan <laanwj@gmail.com>2018-08-01 18:07:08 +0200
committerWladimir J. van der Laan <laanwj@gmail.com>2018-08-01 20:06:17 +0200
commitf030410e88f11c5ff1ce6c80b463a1c7f6d39830 (patch)
tree0910131e7ce1f7d2e184d4629898217cd9d4557e
parentc88529a178d5ca719ebab597a4c4c3437327b2f6 (diff)
parentf6b7fc349ccf9cfbeb7e91e19c20e2a2fcc9026f (diff)
downloadbitcoin-f030410e88f11c5ff1ce6c80b463a1c7f6d39830.tar.xz
Merge #13697: Support output descriptors in scantxoutset
f6b7fc349ccf9cfbeb7e91e19c20e2a2fcc9026f Support h instead of ' in hardened descriptor paths (Pieter Wuille) fddea672eb8f63012f2e9ce04fa477e5d4140750 Add experimental warning to scantxoutset (Jonas Schnelli) 6495849bfd362d6a2f128bac5982fa9e3e2e3396 [QA] Extend tests to more combinations (Pieter Wuille) 1af237faefc316bd708e25d6901ee6f17b706e57 [QA] Add xpub range tests in scantxoutset tests (Jonas Schnelli) 151600bb4972f7bab4ed4a03d1f67c38e081fefe Swap in descriptors support into scantxoutset (Pieter Wuille) 0652c3284fe12941b28624dbbf5e0862c5d0dbc3 Descriptor tests (Pieter Wuille) fe8a7dcd78cfeedc9a7c705e91384f793822912b Output descriptors module (Pieter Wuille) e54d76044b3a2c625e53f2116c5f6a7c40105d5d Add simple FlatSigningProvider (Pieter Wuille) 29943a904a11607787d28b1f4288f500bd076dde Add more methods to Span class (Pieter Wuille) Pull request description: As promised, here is an implementation of my output descriptor concept (https://gist.github.com/sipa/e3d23d498c430bb601c5bca83523fa82) and integration within the `scantxoutset` RPC that was just added through #12196. It changes the RPC to use descriptors for everything; I hope the interface is simple enough to encompass all use cases. It includes support for P2PK, P2PKH, P2WPKH, P2SH, P2WSH, multisig, xpubs, xprvs, and chains of keys - combined in every possible way. Tree-SHA512: 63b54a96e7a72f5b04a8d645b8517d43ecd6a65a41f9f4e593931ce725a8845ab0baa1e9db6a7243190d8ac841f6e7e2f520d98c539312d78f7fd687d2c7b88f
-rw-r--r--src/Makefile.am2
-rw-r--r--src/Makefile.test.include1
-rw-r--r--src/rpc/blockchain.cpp168
-rw-r--r--src/script/descriptor.cpp566
-rw-r--r--src/script/descriptor.h102
-rw-r--r--src/script/sign.cpp30
-rw-r--r--src/script/sign.h13
-rw-r--r--src/span.h20
-rw-r--r--src/test/descriptor_tests.cpp163
-rwxr-xr-xtest/functional/rpc_scantxoutset.py60
10 files changed, 994 insertions, 131 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 60ecf07a59..d1693fa85c 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -158,6 +158,7 @@ BITCOIN_CORE_H = \
rpc/register.h \
rpc/util.h \
scheduler.h \
+ script/descriptor.h \
script/ismine.h \
script/sigcache.h \
script/sign.h \
@@ -387,6 +388,7 @@ libbitcoin_common_a_SOURCES = \
policy/feerate.cpp \
protocol.cpp \
scheduler.cpp \
+ script/descriptor.cpp \
script/ismine.cpp \
script/sign.cpp \
script/standard.cpp \
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index 02f2063504..6f401636f5 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -47,6 +47,7 @@ BITCOIN_TESTS =\
test/crypto_tests.cpp \
test/cuckoocache_tests.cpp \
test/denialofservice_tests.cpp \
+ test/descriptor_tests.cpp \
test/getarg_tests.cpp \
test/hash_tests.cpp \
test/key_io_tests.cpp \
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index 012e3e3ac1..46dec4ca6e 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -20,6 +20,7 @@
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <rpc/server.h>
+#include <script/descriptor.h>
#include <streams.h>
#include <sync.h>
#include <txdb.h>
@@ -1984,67 +1985,38 @@ 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)
{
if (request.fHelp || request.params.size() < 1 || request.params.size() > 2)
throw std::runtime_error(
"scantxoutset <action> ( <scanobjects> )\n"
- "\nScans the unspent transaction output set for possible entries that matches common scripts of given public keys.\n"
- "Using addresses as scanobjects will _not_ detect unspent P2PK txouts\n"
+ "\nEXPERIMENTAL warning: this call may be removed or changed in future releases.\n"
+ "\nScans the unspent transaction output set for entries that match certain output descriptors.\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 \"/*\" (unhardened), or \"/*'\" or \"/*h\" (hardened) to specify all\n"
+ "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"
"1. \"action\" (string, required) The action to execute\n"
" \"start\" for starting a scan\n"
" \"abort\" for aborting the current scan (returns true when abort was successful)\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"
- " [\n"
- " { \"address\" : \"<address>\" }, (string, optional) Bitcoin address\n"
- " { \"script\" : \"<scriptPubKey>\" }, (string, optional) HEX encoded script (scriptPubKey)\n"
- " { \"pubkey\" : (object, optional) Public key\n"
- " {\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"
+ "2. \"scanobjects\" (array, required) Array of scan objects\n"
+ " [ Every scan object is either a string descriptor or an object:\n"
+ " \"descriptor\", (string, optional) An output descriptor\n"
+ " { (object, optional) An object with output descriptor and metadata\n"
+ " \"desc\": \"descriptor\", (string, required) An output descriptor\n"
+ " \"range\": n, (numeric, optional) Up to what child index HD chains should be explored (default: 1000)\n"
" },\n"
- " ]\n"
+ " ...\n"
+ " ]\n"
"\nResult:\n"
"{\n"
" \"unspents\": [\n"
@@ -2090,79 +2062,35 @@ UniValue scantxoutset(const JSONRPCRequest& request)
// loop through the scan objects
for (const UniValue& scanobject : request.params[1].get_array().getValues()) {
- if (!scanobject.isObject()) {
- throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid scan object");
- }
- 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");
+ std::string desc_str;
+ 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");
}
+ } else {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
+ }
- // 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 {
- script = GetScriptForDestination(GetDestinationForKey(pubkey, script_type));
- }
- 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));
+ }
+ if (!desc->IsRange()) range = 0;
+ for (int i = 0; i <= range; ++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: '%s'", desc_str));
}
- } else if (script_uni.isStr()) {
- // type: script
- // check and add the script to the scan containers (needles array)
- CScript script(ParseHexV(script_uni, "script"));
- // TODO: check script: max length, has OP, is unspenable etc.
- needles.insert(script);
+ needles.insert(scripts.begin(), scripts.end());
}
}
diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
new file mode 100644
index 0000000000..f366b99ec3
--- /dev/null
+++ b/src/script/descriptor.cpp
@@ -0,0 +1,566 @@
+// 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.
+
+#include <script/descriptor.h>
+
+#include <key_io.h>
+#include <pubkey.h>
+#include <script/script.h>
+#include <script/standard.h>
+
+#include <span.h>
+#include <util.h>
+#include <utilstrencodings.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace {
+
+////////////////////////////////////////////////////////////////////////////
+// Internal representation //
+////////////////////////////////////////////////////////////////////////////
+
+typedef std::vector<uint32_t> KeyPath;
+
+std::string FormatKeyPath(const KeyPath& path)
+{
+ std::string ret;
+ for (auto i : path) {
+ ret += strprintf("/%i", (i << 1) >> 1);
+ if (i >> 31) ret += '\'';
+ }
+ return ret;
+}
+
+/** Interface for public key objects in descriptors. */
+struct PubkeyProvider
+{
+ virtual ~PubkeyProvider() = default;
+
+ /** Derive a public key. */
+ virtual bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const = 0;
+
+ /** Whether this represent multiple public keys at different positions. */
+ virtual bool IsRange() const = 0;
+
+ /** Get the size of the generated public key(s) in bytes (33 or 65). */
+ virtual size_t GetSize() const = 0;
+
+ /** Get the descriptor string form. */
+ virtual std::string ToString() const = 0;
+
+ /** Get the descriptor string form including private data (if available in arg). */
+ virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;
+};
+
+/** An object representing a parsed constant public key in a descriptor. */
+class ConstPubkeyProvider final : public PubkeyProvider
+{
+ CPubKey m_pubkey;
+
+public:
+ ConstPubkeyProvider(const CPubKey& pubkey) : m_pubkey(pubkey) {}
+ bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const override
+ {
+ out = m_pubkey;
+ return true;
+ }
+ bool IsRange() const override { return false; }
+ size_t GetSize() const override { return m_pubkey.size(); }
+ std::string ToString() const override { return HexStr(m_pubkey.begin(), m_pubkey.end()); }
+ bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
+ {
+ CKey key;
+ if (!arg.GetKey(m_pubkey.GetID(), key)) return false;
+ ret = EncodeSecret(key);
+ return true;
+ }
+};
+
+enum class DeriveType {
+ NO,
+ UNHARDENED,
+ HARDENED,
+};
+
+/** An object representing a parsed extended public key in a descriptor. */
+class BIP32PubkeyProvider final : public PubkeyProvider
+{
+ CExtPubKey m_extkey;
+ KeyPath m_path;
+ DeriveType m_derive;
+
+ bool GetExtKey(const SigningProvider& arg, CExtKey& ret) const
+ {
+ CKey key;
+ if (!arg.GetKey(m_extkey.pubkey.GetID(), key)) return false;
+ ret.nDepth = m_extkey.nDepth;
+ std::copy(m_extkey.vchFingerprint, m_extkey.vchFingerprint + 4, ret.vchFingerprint);
+ ret.nChild = m_extkey.nChild;
+ ret.chaincode = m_extkey.chaincode;
+ ret.key = key;
+ return true;
+ }
+
+ bool IsHardened() const
+ {
+ if (m_derive == DeriveType::HARDENED) return true;
+ for (auto entry : m_path) {
+ if (entry >> 31) return true;
+ }
+ return false;
+ }
+
+public:
+ BIP32PubkeyProvider(const CExtPubKey& extkey, KeyPath path, DeriveType derive) : m_extkey(extkey), m_path(std::move(path)), m_derive(derive) {}
+ bool IsRange() const override { return m_derive != DeriveType::NO; }
+ size_t GetSize() const override { return 33; }
+ bool GetPubKey(int pos, const SigningProvider& arg, CPubKey& out) const override
+ {
+ if (IsHardened()) {
+ CExtKey key;
+ if (!GetExtKey(arg, key)) return false;
+ for (auto entry : m_path) {
+ key.Derive(key, entry);
+ }
+ if (m_derive == DeriveType::UNHARDENED) key.Derive(key, pos);
+ if (m_derive == DeriveType::HARDENED) key.Derive(key, pos | 0x80000000UL);
+ out = key.Neuter().pubkey;
+ } else {
+ // TODO: optimize by caching
+ CExtPubKey key = m_extkey;
+ for (auto entry : m_path) {
+ key.Derive(key, entry);
+ }
+ if (m_derive == DeriveType::UNHARDENED) key.Derive(key, pos);
+ assert(m_derive != DeriveType::HARDENED);
+ out = key.pubkey;
+ }
+ return true;
+ }
+ std::string ToString() const override
+ {
+ std::string ret = EncodeExtPubKey(m_extkey) + FormatKeyPath(m_path);
+ if (IsRange()) {
+ ret += "/*";
+ if (m_derive == DeriveType::HARDENED) ret += '\'';
+ }
+ return ret;
+ }
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
+ {
+ CExtKey key;
+ if (!GetExtKey(arg, key)) return false;
+ out = EncodeExtKey(key) + FormatKeyPath(m_path);
+ if (IsRange()) {
+ out += "/*";
+ if (m_derive == DeriveType::HARDENED) out += '\'';
+ }
+ return true;
+ }
+};
+
+/** A parsed addr(A) descriptor. */
+class AddressDescriptor final : public Descriptor
+{
+ CTxDestination m_destination;
+
+public:
+ AddressDescriptor(CTxDestination destination) : m_destination(std::move(destination)) {}
+
+ bool IsRange() const override { return false; }
+ std::string ToString() const override { return "addr(" + EncodeDestination(m_destination) + ")"; }
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { out = ToString(); return true; }
+ bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
+ {
+ output_scripts = std::vector<CScript>{GetScriptForDestination(m_destination)};
+ return true;
+ }
+};
+
+/** A parsed raw(H) descriptor. */
+class RawDescriptor final : public Descriptor
+{
+ CScript m_script;
+
+public:
+ RawDescriptor(CScript script) : m_script(std::move(script)) {}
+
+ bool IsRange() const override { return false; }
+ std::string ToString() const override { return "raw(" + HexStr(m_script.begin(), m_script.end()) + ")"; }
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { out = ToString(); return true; }
+ bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
+ {
+ output_scripts = std::vector<CScript>{m_script};
+ return true;
+ }
+};
+
+/** A parsed pk(P), pkh(P), or wpkh(P) descriptor. */
+class SingleKeyDescriptor final : public Descriptor
+{
+ const std::function<CScript(const CPubKey&)> m_script_fn;
+ const std::string m_fn_name;
+ std::unique_ptr<PubkeyProvider> m_provider;
+
+public:
+ SingleKeyDescriptor(std::unique_ptr<PubkeyProvider> prov, const std::function<CScript(const CPubKey&)>& fn, const std::string& name) : m_script_fn(fn), m_fn_name(name), m_provider(std::move(prov)) {}
+
+ bool IsRange() const override { return m_provider->IsRange(); }
+ std::string ToString() const override { return m_fn_name + "(" + m_provider->ToString() + ")"; }
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
+ {
+ std::string ret;
+ if (!m_provider->ToPrivateString(arg, ret)) return false;
+ out = m_fn_name + "(" + std::move(ret) + ")";
+ return true;
+ }
+ bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
+ {
+ CPubKey key;
+ if (!m_provider->GetPubKey(pos, arg, key)) return false;
+ output_scripts = std::vector<CScript>{m_script_fn(key)};
+ out.pubkeys.emplace(key.GetID(), std::move(key));
+ return true;
+ }
+};
+
+CScript P2PKHGetScript(const CPubKey& pubkey) { return GetScriptForDestination(pubkey.GetID()); }
+CScript P2PKGetScript(const CPubKey& pubkey) { return GetScriptForRawPubKey(pubkey); }
+CScript P2WPKHGetScript(const CPubKey& pubkey) { return GetScriptForDestination(WitnessV0KeyHash(pubkey.GetID())); }
+
+/** A parsed multi(...) descriptor. */
+class MultisigDescriptor : public Descriptor
+{
+ int m_threshold;
+ std::vector<std::unique_ptr<PubkeyProvider>> m_providers;
+
+public:
+ MultisigDescriptor(int threshold, std::vector<std::unique_ptr<PubkeyProvider>> providers) : m_threshold(threshold), m_providers(std::move(providers)) {}
+
+ bool IsRange() const override
+ {
+ for (const auto& p : m_providers) {
+ if (p->IsRange()) return true;
+ }
+ return false;
+ }
+
+ std::string ToString() const override
+ {
+ std::string ret = strprintf("multi(%i", m_threshold);
+ for (const auto& p : m_providers) {
+ ret += "," + p->ToString();
+ }
+ return std::move(ret) + ")";
+ }
+
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
+ {
+ std::string ret = strprintf("multi(%i", m_threshold);
+ for (const auto& p : m_providers) {
+ std::string sub;
+ if (!p->ToPrivateString(arg, sub)) return false;
+ ret += "," + std::move(sub);
+ }
+ out = std::move(ret) + ")";
+ return true;
+ }
+
+ bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
+ {
+ std::vector<CPubKey> pubkeys;
+ pubkeys.reserve(m_providers.size());
+ for (const auto& p : m_providers) {
+ CPubKey key;
+ if (!p->GetPubKey(pos, arg, key)) return false;
+ pubkeys.push_back(key);
+ }
+ for (const CPubKey& key : pubkeys) {
+ out.pubkeys.emplace(key.GetID(), std::move(key));
+ }
+ output_scripts = std::vector<CScript>{GetScriptForMultisig(m_threshold, pubkeys)};
+ return true;
+ }
+};
+
+/** A parsed sh(S) or wsh(S) descriptor. */
+class ConvertorDescriptor : public Descriptor
+{
+ const std::function<CScript(const CScript&)> m_convert_fn;
+ const std::string m_fn_name;
+ std::unique_ptr<Descriptor> m_descriptor;
+
+public:
+ ConvertorDescriptor(std::unique_ptr<Descriptor> descriptor, const std::function<CScript(const CScript&)>& fn, const std::string& name) : m_convert_fn(fn), m_fn_name(name), m_descriptor(std::move(descriptor)) {}
+
+ bool IsRange() const override { return m_descriptor->IsRange(); }
+ std::string ToString() const override { return m_fn_name + "(" + m_descriptor->ToString() + ")"; }
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
+ {
+ std::string ret;
+ if (!m_descriptor->ToPrivateString(arg, ret)) return false;
+ out = m_fn_name + "(" + std::move(ret) + ")";
+ return true;
+ }
+ bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
+ {
+ std::vector<CScript> sub;
+ if (!m_descriptor->Expand(pos, arg, sub, out)) return false;
+ output_scripts.clear();
+ for (const auto& script : sub) {
+ CScriptID id(script);
+ out.scripts.emplace(CScriptID(script), script);
+ output_scripts.push_back(m_convert_fn(script));
+ }
+ return true;
+ }
+};
+
+CScript ConvertP2SH(const CScript& script) { return GetScriptForDestination(CScriptID(script)); }
+CScript ConvertP2WSH(const CScript& script) { return GetScriptForDestination(WitnessV0ScriptHash(script)); }
+
+/** A parsed combo(P) descriptor. */
+class ComboDescriptor final : public Descriptor
+{
+ std::unique_ptr<PubkeyProvider> m_provider;
+
+public:
+ ComboDescriptor(std::unique_ptr<PubkeyProvider> provider) : m_provider(std::move(provider)) {}
+
+ bool IsRange() const override { return m_provider->IsRange(); }
+ std::string ToString() const override { return "combo(" + m_provider->ToString() + ")"; }
+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
+ {
+ std::string ret;
+ if (!m_provider->ToPrivateString(arg, ret)) return false;
+ out = "combo(" + std::move(ret) + ")";
+ return true;
+ }
+ bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
+ {
+ CPubKey key;
+ if (!m_provider->GetPubKey(pos, arg, key)) return false;
+ CKeyID keyid = key.GetID();
+ {
+ CScript p2pk = GetScriptForRawPubKey(key);
+ CScript p2pkh = GetScriptForDestination(keyid);
+ output_scripts = std::vector<CScript>{std::move(p2pk), std::move(p2pkh)};
+ out.pubkeys.emplace(keyid, key);
+ }
+ if (key.IsCompressed()) {
+ CScript p2wpkh = GetScriptForDestination(WitnessV0KeyHash(keyid));
+ CScriptID p2wpkh_id(p2wpkh);
+ CScript p2sh_p2wpkh = GetScriptForDestination(p2wpkh_id);
+ out.scripts.emplace(p2wpkh_id, p2wpkh);
+ output_scripts.push_back(std::move(p2wpkh));
+ output_scripts.push_back(std::move(p2sh_p2wpkh));
+ }
+ return true;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+// Parser //
+////////////////////////////////////////////////////////////////////////////
+
+enum class ParseScriptContext {
+ TOP,
+ P2SH,
+ P2WSH,
+};
+
+/** Parse a constant. If succesful, sp is updated to skip the constant and return true. */
+bool Const(const std::string& str, Span<const char>& sp)
+{
+ if ((size_t)sp.size() >= str.size() && std::equal(str.begin(), str.end(), sp.begin())) {
+ sp = sp.subspan(str.size());
+ return true;
+ }
+ return false;
+}
+
+/** Parse a function call. If succesful, sp is updated to be the function's argument(s). */
+bool Func(const std::string& str, Span<const char>& sp)
+{
+ if ((size_t)sp.size() >= str.size() + 2 && sp[str.size()] == '(' && sp[sp.size() - 1] == ')' && std::equal(str.begin(), str.end(), sp.begin())) {
+ sp = sp.subspan(str.size() + 1, sp.size() - str.size() - 2);
+ return true;
+ }
+ return false;
+}
+
+/** Return the expression that sp begins with, and update sp to skip it. */
+Span<const char> Expr(Span<const char>& sp)
+{
+ int level = 0;
+ auto it = sp.begin();
+ while (it != sp.end()) {
+ if (*it == '(') {
+ ++level;
+ } else if (level && *it == ')') {
+ --level;
+ } else if (level == 0 && (*it == ')' || *it == ',')) {
+ break;
+ }
+ ++it;
+ }
+ Span<const char> ret = sp.first(it - sp.begin());
+ sp = sp.subspan(it - sp.begin());
+ return ret;
+}
+
+/** Split a string on every instance of sep, returning a vector. */
+std::vector<Span<const char>> Split(const Span<const char>& sp, char sep)
+{
+ std::vector<Span<const char>> ret;
+ auto it = sp.begin();
+ auto start = it;
+ while (it != sp.end()) {
+ if (*it == sep) {
+ ret.emplace_back(start, it);
+ start = it + 1;
+ }
+ ++it;
+ }
+ ret.emplace_back(start, it);
+ return ret;
+}
+
+/** Parse a key path, being passed a split list of elements (the first element is ignored). */
+bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out)
+{
+ for (size_t i = 1; i < split.size(); ++i) {
+ Span<const char> elem = split[i];
+ bool hardened = false;
+ if (elem.size() > 0 && (elem[elem.size() - 1] == '\'' || elem[elem.size() - 1] == 'h')) {
+ elem = elem.first(elem.size() - 1);
+ hardened = true;
+ }
+ uint32_t p;
+ if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p) || p > 0x7FFFFFFFUL) return false;
+ out.push_back(p | (((uint32_t)hardened) << 31));
+ }
+ return true;
+}
+
+std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
+{
+ auto split = Split(sp, '/');
+ std::string str(split[0].begin(), split[0].end());
+ if (split.size() == 1) {
+ if (IsHex(str)) {
+ std::vector<unsigned char> data = ParseHex(str);
+ CPubKey pubkey(data);
+ if (pubkey.IsFullyValid() && (permit_uncompressed || pubkey.IsCompressed())) return MakeUnique<ConstPubkeyProvider>(pubkey);
+ }
+ CKey key = DecodeSecret(str);
+ if (key.IsValid() && (permit_uncompressed || key.IsCompressed())) {
+ CPubKey pubkey = key.GetPubKey();
+ out.keys.emplace(pubkey.GetID(), key);
+ return MakeUnique<ConstPubkeyProvider>(pubkey);
+ }
+ }
+ CExtKey extkey = DecodeExtKey(str);
+ CExtPubKey extpubkey = DecodeExtPubKey(str);
+ if (!extkey.key.IsValid() && !extpubkey.pubkey.IsValid()) return nullptr;
+ KeyPath path;
+ DeriveType type = DeriveType::NO;
+ if (split.back() == MakeSpan("*").first(1)) {
+ split.pop_back();
+ type = DeriveType::UNHARDENED;
+ } else if (split.back() == MakeSpan("*'").first(2) || split.back() == MakeSpan("*h").first(2)) {
+ split.pop_back();
+ type = DeriveType::HARDENED;
+ }
+ if (!ParseKeyPath(split, path)) return nullptr;
+ if (extkey.key.IsValid()) {
+ extpubkey = extkey.Neuter();
+ out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key);
+ }
+ return MakeUnique<BIP32PubkeyProvider>(extpubkey, std::move(path), type);
+}
+
+/** Parse a script in a particular context. */
+std::unique_ptr<Descriptor> ParseScript(Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out)
+{
+ auto expr = Expr(sp);
+ if (Func("pk", expr)) {
+ auto pubkey = ParsePubkey(expr, ctx != ParseScriptContext::P2WSH, out);
+ if (!pubkey) return nullptr;
+ return MakeUnique<SingleKeyDescriptor>(std::move(pubkey), P2PKGetScript, "pk");
+ }
+ if (Func("pkh", expr)) {
+ auto pubkey = ParsePubkey(expr, ctx != ParseScriptContext::P2WSH, out);
+ if (!pubkey) return nullptr;
+ return MakeUnique<SingleKeyDescriptor>(std::move(pubkey), P2PKHGetScript, "pkh");
+ }
+ if (ctx == ParseScriptContext::TOP && Func("combo", expr)) {
+ auto pubkey = ParsePubkey(expr, true, out);
+ if (!pubkey) return nullptr;
+ return MakeUnique<ComboDescriptor>(std::move(pubkey));
+ }
+ if (Func("multi", expr)) {
+ auto threshold = Expr(expr);
+ uint32_t thres;
+ std::vector<std::unique_ptr<PubkeyProvider>> providers;
+ if (!ParseUInt32(std::string(threshold.begin(), threshold.end()), &thres)) return nullptr;
+ size_t script_size = 0;
+ while (expr.size()) {
+ if (!Const(",", expr)) return nullptr;
+ auto arg = Expr(expr);
+ auto pk = ParsePubkey(arg, ctx != ParseScriptContext::P2WSH, out);
+ if (!pk) return nullptr;
+ script_size += pk->GetSize() + 1;
+ providers.emplace_back(std::move(pk));
+ }
+ if (providers.size() < 1 || providers.size() > 16 || thres < 1 || thres > providers.size()) return nullptr;
+ if (ctx == ParseScriptContext::TOP) {
+ if (providers.size() > 3) return nullptr; // Not more than 3 pubkeys for raw multisig
+ }
+ if (ctx == ParseScriptContext::P2SH) {
+ if (script_size + 3 > 520) return nullptr; // Enforce P2SH script size limit
+ }
+ return MakeUnique<MultisigDescriptor>(thres, std::move(providers));
+ }
+ if (ctx != ParseScriptContext::P2WSH && Func("wpkh", expr)) {
+ auto pubkey = ParsePubkey(expr, false, out);
+ if (!pubkey) return nullptr;
+ return MakeUnique<SingleKeyDescriptor>(std::move(pubkey), P2WPKHGetScript, "wpkh");
+ }
+ if (ctx == ParseScriptContext::TOP && Func("sh", expr)) {
+ auto desc = ParseScript(expr, ParseScriptContext::P2SH, out);
+ if (!desc || expr.size()) return nullptr;
+ return MakeUnique<ConvertorDescriptor>(std::move(desc), ConvertP2SH, "sh");
+ }
+ if (ctx != ParseScriptContext::P2WSH && Func("wsh", expr)) {
+ auto desc = ParseScript(expr, ParseScriptContext::P2WSH, out);
+ if (!desc || expr.size()) return nullptr;
+ return MakeUnique<ConvertorDescriptor>(std::move(desc), ConvertP2WSH, "wsh");
+ }
+ if (ctx == ParseScriptContext::TOP && Func("addr", expr)) {
+ CTxDestination dest = DecodeDestination(std::string(expr.begin(), expr.end()));
+ if (!IsValidDestination(dest)) return nullptr;
+ return MakeUnique<AddressDescriptor>(std::move(dest));
+ }
+ if (ctx == ParseScriptContext::TOP && Func("raw", expr)) {
+ std::string str(expr.begin(), expr.end());
+ if (!IsHex(str)) return nullptr;
+ auto bytes = ParseHex(str);
+ return MakeUnique<RawDescriptor>(CScript(bytes.begin(), bytes.end()));
+ }
+ return nullptr;
+}
+
+} // namespace
+
+std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out)
+{
+ Span<const char> sp(descriptor.data(), descriptor.size());
+ auto ret = ParseScript(sp, ParseScriptContext::TOP, out);
+ if (sp.size() == 0 && ret) return ret;
+ return nullptr;
+}
diff --git a/src/script/descriptor.h b/src/script/descriptor.h
new file mode 100644
index 0000000000..e079c72e92
--- /dev/null
+++ b/src/script/descriptor.h
@@ -0,0 +1,102 @@
+// 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.
+
+#ifndef BITCOIN_SCRIPT_DESCRIPTOR_H
+#define BITCOIN_SCRIPT_DESCRIPTOR_H
+
+#include <script/script.h>
+#include <script/sign.h>
+
+#include <vector>
+
+// Descriptors are strings that describe a set of scriptPubKeys, together with
+// all information necessary to solve them. By combining all information into
+// one, they avoid the need to separately import keys and scripts.
+//
+// Descriptors may be ranged, which occurs when the public keys inside are
+// specified in the form of HD chains (xpubs).
+//
+// Descriptors always represent public information - public keys and scripts -
+// but in cases where private keys need to be conveyed along with a descriptor,
+// they can be included inside by changing public keys to private keys (WIF
+// format), and changing xpubs by xprvs.
+//
+// 1. Examples
+//
+// A P2PK descriptor with a fixed public key:
+// - pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)
+//
+// A P2SH-P2WSH-P2PKH descriptor with a fixed public key:
+// - sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))
+//
+// A bare 1-of-2 multisig descriptor:
+// - multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)
+//
+// A chain of P2PKH outputs (this needs the corresponding private key to derive):
+// - pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1'/2/*)
+//
+// 2. Grammar description:
+//
+// X: xpub or xprv encoded extended key
+// I: decimal encoded integer
+// H: Hex encoded byte array
+// A: Address in P2PKH, P2SH, or Bech32 encoding
+//
+// S (Scripts):
+// * pk(P): Pay-to-pubkey (P2PK) output for public key P.
+// * pkh(P): Pay-to-pubkey-hash (P2PKH) output for public key P.
+// * wpkh(P): Pay-to-witness-pubkey-hash (P2WPKH) output for public key P.
+// * sh(S): Pay-to-script-hash (P2SH) output for script S
+// * wsh(S): Pay-to-witness-script-hash (P2WSH) output for script S
+// * combo(P): combination of P2PK, P2PKH, P2WPKH, and P2SH-P2WPKH for public key P.
+// * multi(I,L): k-of-n multisig for given public keys
+// * addr(A): Output to address
+// * raw(H): scriptPubKey with raw bytes
+//
+// P (Public keys):
+// * H: fixed public key (or WIF-encoded private key)
+// * E: extended public key
+// * E/*: (ranged) all unhardened direct children of an extended public key
+// * E/*': (ranged) all hardened direct children of an extended public key
+//
+// L (Comma-separated lists of public keys):
+// * P
+// * L,P
+//
+// E (Extended public keys):
+// * X
+// * E/I: unhardened child
+// * E/I': hardened child
+// * E/Ih: hardened child (alternative notation)
+//
+// The top level is S.
+
+/** Interface for parsed descriptor objects. */
+struct Descriptor {
+ virtual ~Descriptor() = default;
+
+ /** Whether the expansion of this descriptor depends on the position. */
+ virtual bool IsRange() const = 0;
+
+ /** Convert the descriptor back to a string, undoing parsing. */
+ virtual std::string ToString() const = 0;
+
+ /** Convert the descriptor to a private string. This fails if the provided provider does not have the relevant private keys. */
+ virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0;
+
+ /** Expand a descriptor at a specified position.
+ *
+ * pos: the position at which to expand the descriptor. If IsRange() is false, this is ignored.
+ * provider: the provider to query for private keys in case of hardened derivation.
+ * output_script: the expanded scriptPubKeys will be put here.
+ * out: scripts and public keys necessary for solving the expanded scriptPubKeys will be put here (may be equal to provider).
+ */
+ virtual bool Expand(int pos, const SigningProvider& provider, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const = 0;
+};
+
+/** Parse a descriptor string. Included private keys are put in out. Returns nullptr if parsing fails. */
+std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out);
+
+#endif // BITCOIN_SCRIPT_DESCRIPTOR_H
+
diff --git a/src/script/sign.cpp b/src/script/sign.cpp
index d10b1c4fd7..fa09adbaf8 100644
--- a/src/script/sign.cpp
+++ b/src/script/sign.cpp
@@ -11,7 +11,6 @@
#include <script/standard.h>
#include <uint256.h>
-
typedef std::vector<unsigned char> valtype;
MutableTransactionSignatureCreator::MutableTransactionSignatureCreator(const CMutableTransaction* txToIn, unsigned int nInIn, const CAmount& amountIn, int nHashTypeIn) : txTo(txToIn), nIn(nInIn), nHashType(nHashTypeIn), amount(amountIn), checker(txTo, nIn, amountIn) {}
@@ -437,6 +436,18 @@ public:
return true;
}
};
+
+template<typename M, typename K, typename V>
+bool LookupHelper(const M& map, const K& key, V& value)
+{
+ auto it = map.find(key);
+ if (it != map.end()) {
+ value = it->second;
+ return true;
+ }
+ return false;
+}
+
}
const BaseSignatureCreator& DUMMY_SIGNATURE_CREATOR = DummySignatureCreator();
@@ -460,7 +471,6 @@ bool IsSolvable(const SigningProvider& provider, const CScript& script)
return false;
}
-
bool PartiallySignedTransaction::IsNull() const
{
return !tx && inputs.empty() && outputs.empty() && unknown.empty();
@@ -618,3 +628,19 @@ bool PublicOnlySigningProvider::GetPubKey(const CKeyID &address, CPubKey& pubkey
{
return m_provider->GetPubKey(address, pubkey);
}
+
+bool FlatSigningProvider::GetCScript(const CScriptID& scriptid, CScript& script) const { return LookupHelper(scripts, scriptid, script); }
+bool FlatSigningProvider::GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const { return LookupHelper(pubkeys, keyid, pubkey); }
+bool FlatSigningProvider::GetKey(const CKeyID& keyid, CKey& key) const { return LookupHelper(keys, keyid, key); }
+
+FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvider& b)
+{
+ FlatSigningProvider ret;
+ ret.scripts = a.scripts;
+ ret.scripts.insert(b.scripts.begin(), b.scripts.end());
+ ret.pubkeys = a.pubkeys;
+ ret.pubkeys.insert(b.pubkeys.begin(), b.pubkeys.end());
+ ret.keys = a.keys;
+ ret.keys.insert(b.keys.begin(), b.keys.end());
+ return ret;
+}
diff --git a/src/script/sign.h b/src/script/sign.h
index d12d0b5874..96ef59fbe8 100644
--- a/src/script/sign.h
+++ b/src/script/sign.h
@@ -43,6 +43,19 @@ public:
bool GetPubKey(const CKeyID &address, CPubKey& pubkey) const;
};
+struct FlatSigningProvider final : public SigningProvider
+{
+ std::map<CScriptID, CScript> scripts;
+ std::map<CKeyID, CPubKey> pubkeys;
+ std::map<CKeyID, CKey> keys;
+
+ bool GetCScript(const CScriptID& scriptid, CScript& script) const override;
+ bool GetPubKey(const CKeyID& keyid, CPubKey& pubkey) const override;
+ bool GetKey(const CKeyID& keyid, CKey& key) const override;
+};
+
+FlatSigningProvider Merge(const FlatSigningProvider& a, const FlatSigningProvider& b);
+
/** Interface for signature creators. */
class BaseSignatureCreator {
public:
diff --git a/src/span.h b/src/span.h
index 707fc21918..77de059fa6 100644
--- a/src/span.h
+++ b/src/span.h
@@ -7,6 +7,7 @@
#include <type_traits>
#include <cstddef>
+#include <algorithm>
/** A Span is an object that can refer to a contiguous sequence of objects.
*
@@ -21,9 +22,25 @@ class Span
public:
constexpr Span() noexcept : m_data(nullptr), m_size(0) {}
constexpr Span(C* data, std::ptrdiff_t size) noexcept : m_data(data), m_size(size) {}
+ constexpr Span(C* data, C* end) noexcept : m_data(data), m_size(end - data) {}
constexpr C* data() const noexcept { return m_data; }
+ constexpr C* begin() const noexcept { return m_data; }
+ constexpr C* end() const noexcept { return m_data + m_size; }
constexpr std::ptrdiff_t size() const noexcept { return m_size; }
+ constexpr C& operator[](std::ptrdiff_t pos) const noexcept { return m_data[pos]; }
+
+ constexpr Span<C> subspan(std::ptrdiff_t offset) const noexcept { return Span<C>(m_data + offset, m_size - offset); }
+ constexpr Span<C> subspan(std::ptrdiff_t offset, std::ptrdiff_t count) const noexcept { return Span<C>(m_data + offset, count); }
+ constexpr Span<C> first(std::ptrdiff_t count) const noexcept { return Span<C>(m_data, count); }
+ constexpr Span<C> last(std::ptrdiff_t count) const noexcept { return Span<C>(m_data + m_size - count, count); }
+
+ friend constexpr bool operator==(const Span& a, const Span& b) noexcept { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()); }
+ friend constexpr bool operator!=(const Span& a, const Span& b) noexcept { return !(a == b); }
+ friend constexpr bool operator<(const Span& a, const Span& b) noexcept { return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); }
+ friend constexpr bool operator<=(const Span& a, const Span& b) noexcept { return !(b < a); }
+ friend constexpr bool operator>(const Span& a, const Span& b) noexcept { return (b < a); }
+ friend constexpr bool operator>=(const Span& a, const Span& b) noexcept { return !(a < b); }
};
/** Create a span to a container exposing data() and size().
@@ -34,6 +51,9 @@ public:
*
* std::span will have a constructor that implements this functionality directly.
*/
+template<typename A, int N>
+constexpr Span<A> MakeSpan(A (&a)[N]) { return Span<A>(a, N); }
+
template<typename V>
constexpr Span<typename std::remove_pointer<decltype(std::declval<V>().data())>::type> MakeSpan(V& v) { return Span<typename std::remove_pointer<decltype(std::declval<V>().data())>::type>(v.data(), v.size()); }
diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp
new file mode 100644
index 0000000000..f189222be8
--- /dev/null
+++ b/src/test/descriptor_tests.cpp
@@ -0,0 +1,163 @@
+// 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.
+
+#include <vector>
+#include <string>
+#include <script/sign.h>
+#include <script/standard.h>
+#include <test/test_bitcoin.h>
+#include <boost/test/unit_test.hpp>
+#include <script/descriptor.h>
+#include <utilstrencodings.h>
+
+namespace {
+
+void CheckUnparsable(const std::string& prv, const std::string& pub)
+{
+ FlatSigningProvider keys_priv, keys_pub;
+ auto parse_priv = Parse(prv, keys_priv);
+ auto parse_pub = Parse(pub, keys_pub);
+ BOOST_CHECK(!parse_priv);
+ BOOST_CHECK(!parse_pub);
+}
+
+constexpr int DEFAULT = 0;
+constexpr int RANGE = 1; // Expected to be ranged descriptor
+constexpr int HARDENED = 2; // Derivation needs access to private keys
+constexpr int UNSOLVABLE = 4; // This descriptor is not expected to be solvable
+constexpr int SIGNABLE = 8; // We can sign with this descriptor (this is not true when actual BIP32 derivation is used, as that's not integrated in our signing code)
+
+std::string MaybeUseHInsteadOfApostrophy(std::string ret)
+{
+ if (InsecureRandBool()) {
+ while (true) {
+ auto it = ret.find("'");
+ if (it != std::string::npos) {
+ ret[it] = 'h';
+ } else {
+ break;
+ }
+ }
+ }
+ return ret;
+}
+
+void Check(const std::string& prv, const std::string& pub, int flags, const std::vector<std::vector<std::string>>& scripts)
+{
+ FlatSigningProvider keys_priv, keys_pub;
+
+ // Check that parsing succeeds.
+ auto parse_priv = Parse(MaybeUseHInsteadOfApostrophy(prv), keys_priv);
+ auto parse_pub = Parse(MaybeUseHInsteadOfApostrophy(pub), keys_pub);
+ BOOST_CHECK(parse_priv);
+ BOOST_CHECK(parse_pub);
+
+ // Check private keys are extracted from the private version but not the public one.
+ BOOST_CHECK(keys_priv.keys.size());
+ BOOST_CHECK(!keys_pub.keys.size());
+
+ // Check that both versions serialize back to the public version.
+ std::string pub1 = parse_priv->ToString();
+ std::string pub2 = parse_priv->ToString();
+ BOOST_CHECK_EQUAL(pub, pub1);
+ BOOST_CHECK_EQUAL(pub, pub2);
+
+ // Check that both can be serialized with private key back to the private version, but not without private key.
+ std::string prv1, prv2;
+ BOOST_CHECK(parse_priv->ToPrivateString(keys_priv, prv1));
+ BOOST_CHECK_EQUAL(prv, prv1);
+ BOOST_CHECK(!parse_priv->ToPrivateString(keys_pub, prv1));
+ BOOST_CHECK(parse_pub->ToPrivateString(keys_priv, prv1));
+ BOOST_CHECK_EQUAL(prv, prv1);
+ BOOST_CHECK(!parse_pub->ToPrivateString(keys_pub, prv1));
+
+ // Check whether IsRange on both returns the expected result
+ BOOST_CHECK_EQUAL(parse_pub->IsRange(), (flags & RANGE) != 0);
+ BOOST_CHECK_EQUAL(parse_priv->IsRange(), (flags & RANGE) != 0);
+
+
+ // Is not ranged descriptor, only a single result is expected.
+ if (!(flags & RANGE)) assert(scripts.size() == 1);
+
+ size_t max = (flags & RANGE) ? scripts.size() : 3;
+ for (size_t i = 0; i < max; ++i) {
+ const auto& ref = scripts[(flags & RANGE) ? i : 0];
+ for (int t = 0; t < 2; ++t) {
+ FlatSigningProvider key_provider = (flags & HARDENED) ? keys_priv : keys_pub;
+ FlatSigningProvider script_provider;
+ std::vector<CScript> spks;
+ BOOST_CHECK((t ? parse_priv : parse_pub)->Expand(i, key_provider, spks, script_provider));
+ BOOST_CHECK_EQUAL(spks.size(), ref.size());
+ for (size_t n = 0; n < spks.size(); ++n) {
+ BOOST_CHECK_EQUAL(ref[n], HexStr(spks[n].begin(), spks[n].end()));
+ BOOST_CHECK_EQUAL(IsSolvable(Merge(key_provider, script_provider), spks[n]), (flags & UNSOLVABLE) == 0);
+
+ if (flags & SIGNABLE) {
+ CMutableTransaction spend;
+ spend.vin.resize(1);
+ spend.vout.resize(1);
+ BOOST_CHECK_MESSAGE(SignSignature(Merge(keys_priv, script_provider), spks[n], spend, 0, 1, SIGHASH_ALL), prv);
+ }
+ }
+
+ }
+ }
+}
+
+}
+
+BOOST_FIXTURE_TEST_SUITE(descriptor_tests, BasicTestingSetup)
+
+BOOST_AUTO_TEST_CASE(descriptor_test)
+{
+ // Basic single-key compressed
+ Check("combo(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "combo(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, {{"2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac","76a9149a1c78a507689f6f54b847ad1cef1e614ee23f1e88ac","00149a1c78a507689f6f54b847ad1cef1e614ee23f1e","a91484ab21b1b2fd065d4504ff693d832434b6108d7b87"}});
+ Check("pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, {{"2103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bdac"}});
+ Check("pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, {{"76a9149a1c78a507689f6f54b847ad1cef1e614ee23f1e88ac"}});
+ Check("wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", SIGNABLE, {{"00149a1c78a507689f6f54b847ad1cef1e614ee23f1e"}});
+ Check("sh(wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, {{"a91484ab21b1b2fd065d4504ff693d832434b6108d7b87"}});
+
+ // Basic single-key uncompressed
+ Check("combo(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "combo(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, {{"4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235ac","76a914b5bd079c4d57cc7fc28ecf8213a6b791625b818388ac"}});
+ Check("pk(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "pk(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, {{"4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235ac"}});
+ Check("pkh(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "pkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, {{"76a914b5bd079c4d57cc7fc28ecf8213a6b791625b818388ac"}});
+ CheckUnparsable("wpkh(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "wpkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)"); // No uncompressed keys in witness
+ CheckUnparsable("wsh(pk(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss))", "wsh(pk(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235))"); // No uncompressed keys in witness
+ CheckUnparsable("sh(wpkh(5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss))", "sh(wpkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235))"); // No uncompressed keys in witness
+
+ // Some unconventional single-key constructions
+ Check("sh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, {{"a9141857af51a5e516552b3086430fd8ce55f7c1a52487"}});
+ Check("sh(pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, {{"a9141a31ad23bf49c247dd531a623c2ef57da3c400c587"}});
+ Check("wsh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, {{"00202e271faa2325c199d25d22e1ead982e45b64eeb4f31e73dbdf41bd4b5fec23fa"}});
+ Check("wsh(pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", SIGNABLE, {{"0020338e023079b91c58571b20e602d7805fb808c22473cbc391a41b1bd3a192e75b"}});
+ Check("sh(wsh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "sh(wsh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))", SIGNABLE, {{"a91472d0c5a3bfad8c3e7bd5303a72b94240e80b6f1787"}});
+ Check("sh(wsh(pkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "sh(wsh(pkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))", SIGNABLE, {{"a914b61b92e2ca21bac1e72a3ab859a742982bea960a87"}});
+
+ // Versions with BIP32 derivations
+ Check("combo(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)", "combo(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)", SIGNABLE, {{"2102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0ac","76a91431a507b815593dfc51ffc7245ae7e5aee304246e88ac","001431a507b815593dfc51ffc7245ae7e5aee304246e","a9142aafb926eb247cb18240a7f4c07983ad1f37922687"}});
+ Check("pk(xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0)", "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)", DEFAULT, {{"210379e45b3cf75f9c5f9befd8e9506fb962f6a9d185ac87001ec44a8d3df8d4a9e3ac"}});
+ Check("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0)", HARDENED, {{"76a914ebdc90806a9c4356c1c88e42216611e1cb4c1c1788ac"}});
+ Check("wpkh(xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*)", "wpkh(xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*)", RANGE, {{"0014326b2249e3a25d5dc60935f044ee835d090ba859"},{"0014af0bd98abc2f2cae66e36896a39ffe2d32984fb7"},{"00141fa798efd1cbf95cebf912c031b8a4a6e9fb9f27"}});
+ Check("sh(wpkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", RANGE | HARDENED, {{"a9149a4d9901d6af519b2a23d4a2f51650fcba87ce7b87"},{"a914bed59fc0024fae941d6e20a3b44a109ae740129287"},{"a9148483aa1116eb9c05c482a72bada4b1db24af654387"}});
+ Check("combo(xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334/*)", "combo(xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV/*)", RANGE, {{"2102df12b7035bdac8e3bab862a3a83d06ea6b17b6753d52edecba9be46f5d09e076ac","76a914f90e3178ca25f2c808dc76624032d352fdbdfaf288ac","0014f90e3178ca25f2c808dc76624032d352fdbdfaf2","a91408f3ea8c68d4a7585bf9e8bda226723f70e445f087"},{"21032869a233c9adff9a994e4966e5b821fd5bac066da6c3112488dc52383b4a98ecac","76a914a8409d1b6dfb1ed2a3e8aa5e0ef2ff26b15b75b788ac","0014a8409d1b6dfb1ed2a3e8aa5e0ef2ff26b15b75b7","a91473e39884cb71ae4e5ac9739e9225026c99763e6687"}});
+ CheckUnparsable("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483648)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483648)"); // BIP 32 path element overflow
+
+ // Multisig constructions
+ Check("multi(1,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "multi(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, {{"512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea23552ae"}});
+ Check("sh(multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))", "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))", DEFAULT, {{"a91445a9a622a8b0a1269944be477640eedc447bbd8487"}});
+ Check("wsh(multi(2,xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0,xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*'))", "wsh(multi(2,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", HARDENED | RANGE, {{"0020b92623201f3bb7c3771d45b2ad1d0351ea8fbf8cfe0a0e570264e1075fa1948f"},{"002036a08bbe4923af41cf4316817c93b8d37e2f635dd25cfff06bd50df6ae7ea203"},{"0020a96e7ab4607ca6b261bfe3245ffda9c746b28d3f59e83d34820ec0e2b36c139c"}});
+ Check("sh(wsh(multi(16,KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy,KwGNz6YCCQtYvFzMtrC6D3tKTKdBBboMrLTsjr2NYVBwapCkn7Mr,KxogYhiNfwxuswvXV66eFyKcCpm7dZ7TqHVqujHAVUjJxyivxQ9X,L2BUNduTSyZwZjwNHynQTF14mv2uz2NRq5n5sYWTb4FkkmqgEE9f,L1okJGHGn1kFjdXHKxXjwVVtmCMR2JA5QsbKCSpSb7ReQjezKeoD,KxDCNSST75HFPaW5QKpzHtAyaCQC7p9Vo3FYfi2u4dXD1vgMiboK,L5edQjFtnkcf5UWURn6UuuoFrabgDQUHdheKCziwN42aLwS3KizU,KzF8UWFcEC7BYTq8Go1xVimMkDmyNYVmXV5PV7RuDicvAocoPB8i,L3nHUboKG2w4VSJ5jYZ5CBM97oeK6YuKvfZxrefdShECcjEYKMWZ,KyjHo36dWkYhimKmVVmQTq3gERv3pnqA4xFCpvUgbGDJad7eS8WE,KwsfyHKRUTZPQtysN7M3tZ4GXTnuov5XRgjdF2XCG8faAPmFruRF,KzCUbGhN9LJhdeFfL9zQgTJMjqxdBKEekRGZX24hXdgCNCijkkap,KzgpMBwwsDLwkaC5UrmBgCYaBD2WgZ7PBoGYXR8KT7gCA9UTN5a3,KyBXTPy4T7YG4q9tcAM3LkvfRpD1ybHMvcJ2ehaWXaSqeGUxEdkP,KzJDe9iwJRPtKP2F2AoN6zBgzS7uiuAwhWCfGdNeYJ3PC1HNJ8M8,L1xbHrxynrqLKkoYc4qtoQPx6uy5qYXR5ZDYVYBSRmCV5piU3JG9)))","sh(wsh(multi(16,03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600,0362a74e399c39ed5593852a30147f2959b56bb827dfa3e60e464b02ccf87dc5e8,0261345b53de74a4d721ef877c255429961b7e43714171ac06168d7e08c542a8b8,02da72e8b46901a65d4374fe6315538d8f368557dda3a1dcf9ea903f3afe7314c8,0318c82dd0b53fd3a932d16e0ba9e278fcc937c582d5781be626ff16e201f72286,0297ccef1ef99f9d73dec9ad37476ddb232f1238aff877af19e72ba04493361009,02e502cfd5c3f972fe9a3e2a18827820638f96b6f347e54d63deb839011fd5765d,03e687710f0e3ebe81c1037074da939d409c0025f17eb86adb9427d28f0f7ae0e9,02c04d3a5274952acdbc76987f3184b346a483d43be40874624b29e3692c1df5af,02ed06e0f418b5b43a7ec01d1d7d27290fa15f75771cb69b642a51471c29c84acd,036d46073cbb9ffee90473f3da429abc8de7f8751199da44485682a989a4bebb24,02f5d1ff7c9029a80a4e36b9a5497027ef7f3e73384a4a94fbfe7c4e9164eec8bc,02e41deffd1b7cce11cde209a781adcffdabd1b91c0ba0375857a2bfd9302419f3,02d76625f7956a7fc505ab02556c23ee72d832f1bac391bcd2d3abce5710a13d06,0399eb0a5487515802dc14544cf10b3666623762fbed2ec38a3975716e2c29c232)))", SIGNABLE, {{"a9147fc63e13dc25e8a95a3cee3d9a714ac3afd96f1e87"}});
+ CheckUnparsable("sh(multi(16,KzoAz5CanayRKex3fSLQ2BwJpN7U52gZvxMyk78nDMHuqrUxuSJy,KwGNz6YCCQtYvFzMtrC6D3tKTKdBBboMrLTsjr2NYVBwapCkn7Mr,KxogYhiNfwxuswvXV66eFyKcCpm7dZ7TqHVqujHAVUjJxyivxQ9X,L2BUNduTSyZwZjwNHynQTF14mv2uz2NRq5n5sYWTb4FkkmqgEE9f,L1okJGHGn1kFjdXHKxXjwVVtmCMR2JA5QsbKCSpSb7ReQjezKeoD,KxDCNSST75HFPaW5QKpzHtAyaCQC7p9Vo3FYfi2u4dXD1vgMiboK,L5edQjFtnkcf5UWURn6UuuoFrabgDQUHdheKCziwN42aLwS3KizU,KzF8UWFcEC7BYTq8Go1xVimMkDmyNYVmXV5PV7RuDicvAocoPB8i,L3nHUboKG2w4VSJ5jYZ5CBM97oeK6YuKvfZxrefdShECcjEYKMWZ,KyjHo36dWkYhimKmVVmQTq3gERv3pnqA4xFCpvUgbGDJad7eS8WE,KwsfyHKRUTZPQtysN7M3tZ4GXTnuov5XRgjdF2XCG8faAPmFruRF,KzCUbGhN9LJhdeFfL9zQgTJMjqxdBKEekRGZX24hXdgCNCijkkap,KzgpMBwwsDLwkaC5UrmBgCYaBD2WgZ7PBoGYXR8KT7gCA9UTN5a3,KyBXTPy4T7YG4q9tcAM3LkvfRpD1ybHMvcJ2ehaWXaSqeGUxEdkP,KzJDe9iwJRPtKP2F2AoN6zBgzS7uiuAwhWCfGdNeYJ3PC1HNJ8M8,L1xbHrxynrqLKkoYc4qtoQPx6uy5qYXR5ZDYVYBSRmCV5piU3JG9))","sh(multi(16,03669b8afcec803a0d323e9a17f3ea8e68e8abe5a278020a929adbec52421adbd0,0260b2003c386519fc9eadf2b5cf124dd8eea4c4e68d5e154050a9346ea98ce600,0362a74e399c39ed5593852a30147f2959b56bb827dfa3e60e464b02ccf87dc5e8,0261345b53de74a4d721ef877c255429961b7e43714171ac06168d7e08c542a8b8,02da72e8b46901a65d4374fe6315538d8f368557dda3a1dcf9ea903f3afe7314c8,0318c82dd0b53fd3a932d16e0ba9e278fcc937c582d5781be626ff16e201f72286,0297ccef1ef99f9d73dec9ad37476ddb232f1238aff877af19e72ba04493361009,02e502cfd5c3f972fe9a3e2a18827820638f96b6f347e54d63deb839011fd5765d,03e687710f0e3ebe81c1037074da939d409c0025f17eb86adb9427d28f0f7ae0e9,02c04d3a5274952acdbc76987f3184b346a483d43be40874624b29e3692c1df5af,02ed06e0f418b5b43a7ec01d1d7d27290fa15f75771cb69b642a51471c29c84acd,036d46073cbb9ffee90473f3da429abc8de7f8751199da44485682a989a4bebb24,02f5d1ff7c9029a80a4e36b9a5497027ef7f3e73384a4a94fbfe7c4e9164eec8bc,02e41deffd1b7cce11cde209a781adcffdabd1b91c0ba0375857a2bfd9302419f3,02d76625f7956a7fc505ab02556c23ee72d832f1bac391bcd2d3abce5710a13d06,0399eb0a5487515802dc14544cf10b3666623762fbed2ec38a3975716e2c29c232))"); // P2SH does not fit 16 compressed pubkeys in a redeemscript
+
+ // Check for invalid nesting of structures
+ CheckUnparsable("sh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "sh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)"); // P2SH needs a script, not a key
+ CheckUnparsable("sh(combo(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "sh(combo(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))"); // Old must be top level
+ CheckUnparsable("wsh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "wsh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)"); // P2WSH needs a script, not a key
+ CheckUnparsable("wsh(wpkh(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(wpkh(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))"); // Cannot embed witness inside witness
+ CheckUnparsable("wsh(sh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "wsh(sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))"); // Cannot embed P2SH inside P2WSH
+ CheckUnparsable("sh(sh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "sh(sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))"); // Cannot embed P2SH inside P2SH
+ CheckUnparsable("wsh(wsh(pk(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)))", "wsh(wsh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)))"); // Cannot embed P2WSH inside P2WSH
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py
index ce5d4da9e7..11c35b9f08 100755
--- a/test/functional/rpc_scantxoutset.py
+++ b/test/functional/rpc_scantxoutset.py
@@ -23,9 +23,25 @@ class ScantxoutsetTest(BitcoinTestFramework):
pubk2 = self.nodes[0].getaddressinfo(addr_LEGACY)['pubkey']
addr_BECH32 = self.nodes[0].getnewaddress("", "bech32")
pubk3 = self.nodes[0].getaddressinfo(addr_BECH32)['pubkey']
- self.nodes[0].sendtoaddress(addr_P2SH_SEGWIT, 1)
- self.nodes[0].sendtoaddress(addr_LEGACY, 2)
- self.nodes[0].sendtoaddress(addr_BECH32, 3)
+ self.nodes[0].sendtoaddress(addr_P2SH_SEGWIT, 0.001)
+ self.nodes[0].sendtoaddress(addr_LEGACY, 0.002)
+ self.nodes[0].sendtoaddress(addr_BECH32, 0.004)
+
+ #send to child keys of tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK
+ self.nodes[0].sendtoaddress("mkHV1C6JLheLoUSSZYk7x3FH5tnx9bu7yc", 0.008) # (m/0'/0'/0')
+ self.nodes[0].sendtoaddress("mipUSRmJAj2KrjSvsPQtnP8ynUon7FhpCR", 0.016) # (m/0'/0'/1')
+ self.nodes[0].sendtoaddress("n37dAGe6Mq1HGM9t4b6rFEEsDGq7Fcgfqg", 0.032) # (m/0'/0'/1500')
+ self.nodes[0].sendtoaddress("mqS9Rpg8nNLAzxFExsgFLCnzHBsoQ3PRM6", 0.064) # (m/0'/0'/0)
+ self.nodes[0].sendtoaddress("mnTg5gVWr3rbhHaKjJv7EEEc76ZqHgSj4S", 0.128) # (m/0'/0'/1)
+ self.nodes[0].sendtoaddress("mketCd6B9U9Uee1iCsppDJJBHfvi6U6ukC", 0.256) # (m/0'/0'/1500)
+ self.nodes[0].sendtoaddress("mj8zFzrbBcdaWXowCQ1oPZ4qioBVzLzAp7", 0.512) # (m/1/1/0')
+ self.nodes[0].sendtoaddress("mfnKpKQEftniaoE1iXuMMePQU3PUpcNisA", 1.024) # (m/1/1/1')
+ self.nodes[0].sendtoaddress("mou6cB1kaP1nNJM1sryW6YRwnd4shTbXYQ", 2.048) # (m/1/1/1500')
+ self.nodes[0].sendtoaddress("mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", 4.096) # (m/1/1/0)
+ self.nodes[0].sendtoaddress("mxp7w7j8S1Aq6L8StS2PqVvtt4HGxXEvdy", 8.192) # (m/1/1/1)
+ self.nodes[0].sendtoaddress("mpQ8rokAhp1TAtJQR6F6TaUmjAWkAWYYBq", 16.384) # (m/1/1/1500)
+
+
self.nodes[0].generate(1)
self.log.info("Stop node, remove wallet, mine again some blocks...")
@@ -36,13 +52,39 @@ class ScantxoutsetTest(BitcoinTestFramework):
self.restart_node(0, ['-nowallet'])
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", [ {"address": addr_P2SH_SEGWIT}, {"address": addr_LEGACY}, {"address": 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", [ "pkh(" + pubk1 + ")", "pkh(" + pubk2 + ")", "pkh(" + pubk3 + ")"])['total_amount'], Decimal("0.002"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "wpkh(" + pubk1 + ")", "wpkh(" + pubk2 + ")", "wpkh(" + pubk3 + ")"])['total_amount'], Decimal("0.004"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "sh(wpkh(" + pubk1 + "))", "sh(wpkh(" + pubk2 + "))", "sh(wpkh(" + pubk3 + "))"])['total_amount'], Decimal("0.001"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(" + pubk1 + ")", "combo(" + pubk2 + ")", "combo(" + pubk3 + ")"])['total_amount'], Decimal("0.007"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "addr(" + addr_BECH32 + ")"])['total_amount'], Decimal("0.007"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "addr(" + addr_P2SH_SEGWIT + ")", "addr(" + addr_LEGACY + ")", "combo(" + pubk3 + ")"])['total_amount'], Decimal("0.007"))
- 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
+ self.log.info("Test extended key derivation.")
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/0h)"])['total_amount'], Decimal("0.008"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/1h)"])['total_amount'], Decimal("0.016"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500')"])['total_amount'], Decimal("0.032"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0h/0)"])['total_amount'], Decimal("0.064"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/1)"])['total_amount'], Decimal("0.128"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/1500)"])['total_amount'], Decimal("0.256"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*h)", "range": 1499}])['total_amount'], Decimal("0.024"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0'/*h)", "range": 1500}])['total_amount'], Decimal("0.056"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])['total_amount'], Decimal("0.192"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0'/0h/*)", "range": 1500}])['total_amount'], Decimal("0.448"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0')"])['total_amount'], Decimal("0.512"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1')"])['total_amount'], Decimal("1.024"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500h)"])['total_amount'], Decimal("2.048"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])['total_amount'], Decimal("4.096"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1)"])['total_amount'], Decimal("8.192"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/1500)"])['total_amount'], Decimal("16.384"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/0)"])['total_amount'], Decimal("4.096"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1)"])['total_amount'], Decimal("8.192"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/1500)"])['total_amount'], Decimal("16.384"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1499}])['total_amount'], Decimal("1.536"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*')", "range": 1500}])['total_amount'], Decimal("3.584"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288"))
+ assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672"))
if __name__ == '__main__':
ScantxoutsetTest().main()