From 4d78bd93b5bdf886e743022e80f4edb8a982cf0d Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 12 Oct 2018 18:22:22 -0700 Subject: [PATCH 1/6] Add support for inferring descriptors from scripts --- src/script/descriptor.cpp | 79 +++++++++++++++++++++++++++++++++++++++ src/script/descriptor.h | 17 ++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 478797e9587..c4980240926 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -625,6 +625,80 @@ std::unique_ptr ParseScript(Span& sp, ParseScriptContext return nullptr; } +std::unique_ptr InferPubkey(const CPubKey& pubkey, ParseScriptContext, const SigningProvider& provider) +{ + std::unique_ptr key_provider = MakeUnique(pubkey); + KeyOriginInfo info; + if (provider.GetKeyOrigin(pubkey.GetID(), info)) { + return MakeUnique(std::move(info), std::move(key_provider)); + } + return key_provider; +} + +std::unique_ptr InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider) +{ + std::vector> data; + txnouttype txntype = Solver(script, data); + + if (txntype == TX_PUBKEY) { + CPubKey pubkey(data[0].begin(), data[0].end()); + if (pubkey.IsValid()) { + return MakeUnique(InferPubkey(pubkey, ctx, provider), P2PKGetScript, "pk"); + } + } + if (txntype == TX_PUBKEYHASH) { + uint160 hash(data[0]); + CKeyID keyid(hash); + CPubKey pubkey; + if (provider.GetPubKey(keyid, pubkey)) { + return MakeUnique(InferPubkey(pubkey, ctx, provider), P2PKHGetScript, "pkh"); + } + } + if (txntype == TX_WITNESS_V0_KEYHASH && ctx != ParseScriptContext::P2WSH) { + uint160 hash(data[0]); + CKeyID keyid(hash); + CPubKey pubkey; + if (provider.GetPubKey(keyid, pubkey)) { + return MakeUnique(InferPubkey(pubkey, ctx, provider), P2WPKHGetScript, "wpkh"); + } + } + if (txntype == TX_MULTISIG) { + std::vector> providers; + for (size_t i = 1; i + 1 < data.size(); ++i) { + CPubKey pubkey(data[i].begin(), data[i].end()); + providers.push_back(InferPubkey(pubkey, ctx, provider)); + } + return MakeUnique((int)data[0][0], std::move(providers)); + } + if (txntype == TX_SCRIPTHASH && ctx == ParseScriptContext::TOP) { + uint160 hash(data[0]); + CScriptID scriptid(hash); + CScript subscript; + if (provider.GetCScript(scriptid, subscript)) { + auto sub = InferScript(subscript, ParseScriptContext::P2SH, provider); + if (sub) return MakeUnique(std::move(sub), ConvertP2SH, "sh"); + } + } + if (txntype == TX_WITNESS_V0_SCRIPTHASH && ctx != ParseScriptContext::P2WSH) { + CScriptID scriptid; + CRIPEMD160().Write(data[0].data(), data[0].size()).Finalize(scriptid.begin()); + CScript subscript; + if (provider.GetCScript(scriptid, subscript)) { + auto sub = InferScript(subscript, ParseScriptContext::P2WSH, provider); + if (sub) return MakeUnique(std::move(sub), ConvertP2WSH, "wsh"); + } + } + + CTxDestination dest; + if (ExtractDestination(script, dest)) { + if (GetScriptForDestination(dest) == script) { + return MakeUnique(std::move(dest)); + } + } + + return MakeUnique(script); +} + } // namespace std::unique_ptr Parse(const std::string& descriptor, FlatSigningProvider& out) @@ -634,3 +708,8 @@ std::unique_ptr Parse(const std::string& descriptor, FlatSigningProv if (sp.size() == 0 && ret) return ret; return nullptr; } + +std::unique_ptr InferDescriptor(const CScript& script, const SigningProvider& provider) +{ + return InferScript(script, ParseScriptContext::TOP, provider); +} diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 87e07369c7f..e44a6ef3c3b 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -51,5 +51,20 @@ struct Descriptor { /** Parse a descriptor string. Included private keys are put in out. Returns nullptr if parsing fails. */ std::unique_ptr Parse(const std::string& descriptor, FlatSigningProvider& out); -#endif // BITCOIN_SCRIPT_DESCRIPTOR_H +/** Find a descriptor for the specified script, using information from provider where possible. + * + * A non-ranged descriptor which only generates the specified script will be returned in all + * circumstances. + * + * For public keys with key origin information, this information will be preserved in the returned + * descriptor. + * + * - If all information for solving `script` is present in `provider`, a descriptor will be returned + * which is `IsSolvable()` and encapsulates said information. + * - Failing that, if `script` corresponds to a known address type, an "addr()" descriptor will be + * returned (which is not `IsSolvable()`). + * - Failing that, a "raw()" descriptor is returned. + */ +std::unique_ptr InferDescriptor(const CScript& script, const SigningProvider& provider); +#endif // BITCOIN_SCRIPT_DESCRIPTOR_H From 225bf3e3b0a89a285da451cd589be148324039ab Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Fri, 12 Oct 2018 18:27:00 -0700 Subject: [PATCH 2/6] Add Descriptor::IsSolvable() to distinguish addr/raw from others --- src/script/descriptor.cpp | 7 +++++++ src/script/descriptor.h | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index c4980240926..e3f9381da0a 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -211,6 +211,7 @@ public: AddressDescriptor(CTxDestination destination) : m_destination(std::move(destination)) {} bool IsRange() const override { return false; } + bool IsSolvable() 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& output_scripts, FlatSigningProvider& out) const override @@ -229,6 +230,7 @@ public: RawDescriptor(CScript script) : m_script(std::move(script)) {} bool IsRange() const override { return false; } + bool IsSolvable() 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& output_scripts, FlatSigningProvider& out) const override @@ -249,6 +251,7 @@ public: SingleKeyDescriptor(std::unique_ptr prov, const std::function& 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(); } + bool IsSolvable() const override { return true; } std::string ToString() const override { return m_fn_name + "(" + m_provider->ToString() + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { @@ -290,6 +293,8 @@ public: return false; } + bool IsSolvable() const override { return true; } + std::string ToString() const override { std::string ret = strprintf("multi(%i", m_threshold); @@ -343,6 +348,7 @@ public: ConvertorDescriptor(std::unique_ptr descriptor, const std::function& 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(); } + bool IsSolvable() const override { return m_descriptor->IsSolvable(); } std::string ToString() const override { return m_fn_name + "(" + m_descriptor->ToString() + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { @@ -377,6 +383,7 @@ public: ComboDescriptor(std::unique_ptr provider) : m_provider(std::move(provider)) {} bool IsRange() const override { return m_provider->IsRange(); } + bool IsSolvable() const override { return true; } std::string ToString() const override { return "combo(" + m_provider->ToString() + ")"; } bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { diff --git a/src/script/descriptor.h b/src/script/descriptor.h index e44a6ef3c3b..0111972f85d 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -32,6 +32,10 @@ struct Descriptor { /** Whether the expansion of this descriptor depends on the position. */ virtual bool IsRange() const = 0; + /** Whether this descriptor has all information about signing ignoring lack of private keys. + * This is true for all descriptors except ones that use `raw` or `addr` constructions. */ + virtual bool IsSolvable() const = 0; + /** Convert the descriptor back to a string, undoing parsing. */ virtual std::string ToString() const = 0; From 9b2a25b13f81a45ff59a6a4adde595404a6c062a Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sat, 13 Oct 2018 10:45:15 -0700 Subject: [PATCH 3/6] Add tests for InferDescriptor and Descriptor::IsSolvable --- src/script/sign.h | 5 +++++ src/test/descriptor_tests.cpp | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/script/sign.h b/src/script/sign.h index 689501269d0..3cbf402764a 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -24,6 +24,11 @@ struct KeyOriginInfo { unsigned char fingerprint[4]; std::vector path; + + friend bool operator==(const KeyOriginInfo& a, const KeyOriginInfo& b) + { + return std::equal(std::begin(a.fingerprint), std::end(a.fingerprint), std::begin(b.fingerprint)) && a.path == b.path; + } }; /** An interface to be implemented by keystores that support signing. */ diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index f3083bab4a4..7e9d14a8ef7 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -102,7 +102,19 @@ void Check(const std::string& prv, const std::string& pub, int flags, const std: spend.vout.resize(1); BOOST_CHECK_MESSAGE(SignSignature(Merge(keys_priv, script_provider), spks[n], spend, 0, 1, SIGHASH_ALL), prv); } + + /* Infer a descriptor from the generated script, and verify its solvability and that it roundtrips. */ + auto inferred = InferDescriptor(spks[n], script_provider); + BOOST_CHECK_EQUAL(inferred->IsSolvable(), !(flags & UNSOLVABLE)); + std::vector spks_inferred; + FlatSigningProvider provider_inferred; + BOOST_CHECK(inferred->Expand(0, provider_inferred, spks_inferred, provider_inferred)); + BOOST_CHECK_EQUAL(spks_inferred.size(), 1); + BOOST_CHECK(spks_inferred[0] == spks[n]); + BOOST_CHECK_EQUAL(IsSolvable(provider_inferred, spks_inferred[0]), !(flags & UNSOLVABLE)); + BOOST_CHECK(provider_inferred.origins == script_provider.origins); } + // Test whether the observed key path is present in the 'paths' variable (which contains expected, unobserved paths), // and then remove it from that set. for (const auto& origin : script_provider.origins) { From 16203d5df755fe8fc0c5ddc026714d87b504ff24 Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Sat, 13 Oct 2018 13:32:18 -0700 Subject: [PATCH 4/6] Add descriptors to listunspent and getaddressinfo + tests --- src/wallet/rpcwallet.cpp | 13 ++++++ test/functional/wallet_address_types.py | 56 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index dc39ba3001a..46259535acd 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -20,6 +20,7 @@ #include #include #include +#include