Skip to content

Commit fdf146f

Browse files
committed
Merge #14477: Add ability to convert solvability info to descriptor
109699d Add release notes (Pieter Wuille) b65326b Add matching descriptors to scantxoutset output + tests (Pieter Wuille) 16203d5 Add descriptors to listunspent and getaddressinfo + tests (Pieter Wuille) 9b2a25b Add tests for InferDescriptor and Descriptor::IsSolvable (Pieter Wuille) 225bf3e Add Descriptor::IsSolvable() to distinguish addr/raw from others (Pieter Wuille) 4d78bd9 Add support for inferring descriptors from scripts (Pieter Wuille) Pull request description: This PR adds functionality to convert a script to a descriptor, given a `SigningProvider` with the relevant information about public keys and redeemscripts/witnessscripts. The feature is exposed in `listunspent`, `getaddressinfo`, and `scantxoutset` whenever these calls are applied to solvable outputs/addresses. This is not very useful on its own, though when we add RPCs to import descriptors, or sign PSBTs using descriptors, these strings become a compact and standalone way of conveying everything necessary to sign an output (excluding private keys). Unit tests and rudimentary RPC tests are included (more relevant tests can be added once RPCs support descriptors). Fixes #14503. Tree-SHA512: cb36b84a3e0200375b7e06a98c7e750cfaf95cf5de132cad59f7ec3cbd201f739427de0dc108f515be7aca203652089fbf5f24ed283d4553bddf23a3224ab31f
2 parents 0fa3703 + 109699d commit fdf146f

File tree

9 files changed

+213
-2
lines changed

9 files changed

+213
-2
lines changed

doc/release-notes-14477.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Miscellaneous RPC changes
2+
------------
3+
4+
- `getaddressinfo` now reports `solvable`, a boolean indicating whether all information necessary for signing is present in the wallet (ignoring private keys).
5+
- `getaddressinfo`, `listunspent`, and `scantxoutset` have a new output field `desc`, an output descriptor that encapsulates all signing information and key paths for the address (only available when `solvable` is true for `getaddressinfo` and `listunspent`).

src/rpc/blockchain.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2187,6 +2187,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
21872187
" \"txid\" : \"transactionid\", (string) The transaction id\n"
21882188
" \"vout\": n, (numeric) the vout value\n"
21892189
" \"scriptPubKey\" : \"script\", (string) the script key\n"
2190+
" \"desc\" : \"descriptor\", (string) A specialized descriptor for the matched scriptPubKey\n"
21902191
" \"amount\" : x.xxx, (numeric) The total amount in " + CURRENCY_UNIT + " of the unspent output\n"
21912192
" \"height\" : n, (numeric) Height of the unspent transaction output\n"
21922193
" }\n"
@@ -2221,6 +2222,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
22212222
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
22222223
}
22232224
std::set<CScript> needles;
2225+
std::map<CScript, std::string> descriptors;
22242226
CAmount total_in = 0;
22252227

22262228
// loop through the scan objects
@@ -2253,7 +2255,11 @@ UniValue scantxoutset(const JSONRPCRequest& request)
22532255
if (!desc->Expand(i, provider, scripts, provider)) {
22542256
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
22552257
}
2256-
needles.insert(scripts.begin(), scripts.end());
2258+
for (const auto& script : scripts) {
2259+
std::string inferred = InferDescriptor(script, provider)->ToString();
2260+
needles.emplace(script);
2261+
descriptors.emplace(std::move(script), std::move(inferred));
2262+
}
22572263
}
22582264
}
22592265

@@ -2286,6 +2292,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
22862292
unspent.pushKV("txid", outpoint.hash.GetHex());
22872293
unspent.pushKV("vout", (int32_t)outpoint.n);
22882294
unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey.begin(), txo.scriptPubKey.end()));
2295+
unspent.pushKV("desc", descriptors[txo.scriptPubKey]);
22892296
unspent.pushKV("amount", ValueFromAmount(txo.nValue));
22902297
unspent.pushKV("height", (int32_t)coin.nHeight);
22912298

src/script/descriptor.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class AddressDescriptor final : public Descriptor
211211
AddressDescriptor(CTxDestination destination) : m_destination(std::move(destination)) {}
212212

213213
bool IsRange() const override { return false; }
214+
bool IsSolvable() const override { return false; }
214215
std::string ToString() const override { return "addr(" + EncodeDestination(m_destination) + ")"; }
215216
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { out = ToString(); return true; }
216217
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
@@ -229,6 +230,7 @@ class RawDescriptor final : public Descriptor
229230
RawDescriptor(CScript script) : m_script(std::move(script)) {}
230231

231232
bool IsRange() const override { return false; }
233+
bool IsSolvable() const override { return false; }
232234
std::string ToString() const override { return "raw(" + HexStr(m_script.begin(), m_script.end()) + ")"; }
233235
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { out = ToString(); return true; }
234236
bool Expand(int pos, const SigningProvider& arg, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override
@@ -249,6 +251,7 @@ class SingleKeyDescriptor final : public Descriptor
249251
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)) {}
250252

251253
bool IsRange() const override { return m_provider->IsRange(); }
254+
bool IsSolvable() const override { return true; }
252255
std::string ToString() const override { return m_fn_name + "(" + m_provider->ToString() + ")"; }
253256
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
254257
{
@@ -290,6 +293,8 @@ class MultisigDescriptor : public Descriptor
290293
return false;
291294
}
292295

296+
bool IsSolvable() const override { return true; }
297+
293298
std::string ToString() const override
294299
{
295300
std::string ret = strprintf("multi(%i", m_threshold);
@@ -343,6 +348,7 @@ class ConvertorDescriptor : public Descriptor
343348
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)) {}
344349

345350
bool IsRange() const override { return m_descriptor->IsRange(); }
351+
bool IsSolvable() const override { return m_descriptor->IsSolvable(); }
346352
std::string ToString() const override { return m_fn_name + "(" + m_descriptor->ToString() + ")"; }
347353
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
348354
{
@@ -377,6 +383,7 @@ class ComboDescriptor final : public Descriptor
377383
ComboDescriptor(std::unique_ptr<PubkeyProvider> provider) : m_provider(std::move(provider)) {}
378384

379385
bool IsRange() const override { return m_provider->IsRange(); }
386+
bool IsSolvable() const override { return true; }
380387
std::string ToString() const override { return "combo(" + m_provider->ToString() + ")"; }
381388
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
382389
{
@@ -625,6 +632,80 @@ std::unique_ptr<Descriptor> ParseScript(Span<const char>& sp, ParseScriptContext
625632
return nullptr;
626633
}
627634

635+
std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext, const SigningProvider& provider)
636+
{
637+
std::unique_ptr<PubkeyProvider> key_provider = MakeUnique<ConstPubkeyProvider>(pubkey);
638+
KeyOriginInfo info;
639+
if (provider.GetKeyOrigin(pubkey.GetID(), info)) {
640+
return MakeUnique<OriginPubkeyProvider>(std::move(info), std::move(key_provider));
641+
}
642+
return key_provider;
643+
}
644+
645+
std::unique_ptr<Descriptor> InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider)
646+
{
647+
std::vector<std::vector<unsigned char>> data;
648+
txnouttype txntype = Solver(script, data);
649+
650+
if (txntype == TX_PUBKEY) {
651+
CPubKey pubkey(data[0].begin(), data[0].end());
652+
if (pubkey.IsValid()) {
653+
return MakeUnique<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2PKGetScript, "pk");
654+
}
655+
}
656+
if (txntype == TX_PUBKEYHASH) {
657+
uint160 hash(data[0]);
658+
CKeyID keyid(hash);
659+
CPubKey pubkey;
660+
if (provider.GetPubKey(keyid, pubkey)) {
661+
return MakeUnique<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2PKHGetScript, "pkh");
662+
}
663+
}
664+
if (txntype == TX_WITNESS_V0_KEYHASH && ctx != ParseScriptContext::P2WSH) {
665+
uint160 hash(data[0]);
666+
CKeyID keyid(hash);
667+
CPubKey pubkey;
668+
if (provider.GetPubKey(keyid, pubkey)) {
669+
return MakeUnique<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2WPKHGetScript, "wpkh");
670+
}
671+
}
672+
if (txntype == TX_MULTISIG) {
673+
std::vector<std::unique_ptr<PubkeyProvider>> providers;
674+
for (size_t i = 1; i + 1 < data.size(); ++i) {
675+
CPubKey pubkey(data[i].begin(), data[i].end());
676+
providers.push_back(InferPubkey(pubkey, ctx, provider));
677+
}
678+
return MakeUnique<MultisigDescriptor>((int)data[0][0], std::move(providers));
679+
}
680+
if (txntype == TX_SCRIPTHASH && ctx == ParseScriptContext::TOP) {
681+
uint160 hash(data[0]);
682+
CScriptID scriptid(hash);
683+
CScript subscript;
684+
if (provider.GetCScript(scriptid, subscript)) {
685+
auto sub = InferScript(subscript, ParseScriptContext::P2SH, provider);
686+
if (sub) return MakeUnique<ConvertorDescriptor>(std::move(sub), ConvertP2SH, "sh");
687+
}
688+
}
689+
if (txntype == TX_WITNESS_V0_SCRIPTHASH && ctx != ParseScriptContext::P2WSH) {
690+
CScriptID scriptid;
691+
CRIPEMD160().Write(data[0].data(), data[0].size()).Finalize(scriptid.begin());
692+
CScript subscript;
693+
if (provider.GetCScript(scriptid, subscript)) {
694+
auto sub = InferScript(subscript, ParseScriptContext::P2WSH, provider);
695+
if (sub) return MakeUnique<ConvertorDescriptor>(std::move(sub), ConvertP2WSH, "wsh");
696+
}
697+
}
698+
699+
CTxDestination dest;
700+
if (ExtractDestination(script, dest)) {
701+
if (GetScriptForDestination(dest) == script) {
702+
return MakeUnique<AddressDescriptor>(std::move(dest));
703+
}
704+
}
705+
706+
return MakeUnique<RawDescriptor>(script);
707+
}
708+
628709
} // namespace
629710

630711
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out)
@@ -634,3 +715,8 @@ std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProv
634715
if (sp.size() == 0 && ret) return ret;
635716
return nullptr;
636717
}
718+
719+
std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider)
720+
{
721+
return InferScript(script, ParseScriptContext::TOP, provider);
722+
}

src/script/descriptor.h

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ struct Descriptor {
3232
/** Whether the expansion of this descriptor depends on the position. */
3333
virtual bool IsRange() const = 0;
3434

35+
/** Whether this descriptor has all information about signing ignoring lack of private keys.
36+
* This is true for all descriptors except ones that use `raw` or `addr` constructions. */
37+
virtual bool IsSolvable() const = 0;
38+
3539
/** Convert the descriptor back to a string, undoing parsing. */
3640
virtual std::string ToString() const = 0;
3741

@@ -51,5 +55,20 @@ struct Descriptor {
5155
/** Parse a descriptor string. Included private keys are put in out. Returns nullptr if parsing fails. */
5256
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out);
5357

54-
#endif // BITCOIN_SCRIPT_DESCRIPTOR_H
58+
/** Find a descriptor for the specified script, using information from provider where possible.
59+
*
60+
* A non-ranged descriptor which only generates the specified script will be returned in all
61+
* circumstances.
62+
*
63+
* For public keys with key origin information, this information will be preserved in the returned
64+
* descriptor.
65+
*
66+
* - If all information for solving `script` is present in `provider`, a descriptor will be returned
67+
* which is `IsSolvable()` and encapsulates said information.
68+
* - Failing that, if `script` corresponds to a known address type, an "addr()" descriptor will be
69+
* returned (which is not `IsSolvable()`).
70+
* - Failing that, a "raw()" descriptor is returned.
71+
*/
72+
std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider);
5573

74+
#endif // BITCOIN_SCRIPT_DESCRIPTOR_H

src/script/sign.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ struct KeyOriginInfo
2424
{
2525
unsigned char fingerprint[4];
2626
std::vector<uint32_t> path;
27+
28+
friend bool operator==(const KeyOriginInfo& a, const KeyOriginInfo& b)
29+
{
30+
return std::equal(std::begin(a.fingerprint), std::end(a.fingerprint), std::begin(b.fingerprint)) && a.path == b.path;
31+
}
2732
};
2833

2934
/** An interface to be implemented by keystores that support signing. */

src/test/descriptor_tests.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,19 @@ void Check(const std::string& prv, const std::string& pub, int flags, const std:
102102
spend.vout.resize(1);
103103
BOOST_CHECK_MESSAGE(SignSignature(Merge(keys_priv, script_provider), spks[n], spend, 0, 1, SIGHASH_ALL), prv);
104104
}
105+
106+
/* Infer a descriptor from the generated script, and verify its solvability and that it roundtrips. */
107+
auto inferred = InferDescriptor(spks[n], script_provider);
108+
BOOST_CHECK_EQUAL(inferred->IsSolvable(), !(flags & UNSOLVABLE));
109+
std::vector<CScript> spks_inferred;
110+
FlatSigningProvider provider_inferred;
111+
BOOST_CHECK(inferred->Expand(0, provider_inferred, spks_inferred, provider_inferred));
112+
BOOST_CHECK_EQUAL(spks_inferred.size(), 1);
113+
BOOST_CHECK(spks_inferred[0] == spks[n]);
114+
BOOST_CHECK_EQUAL(IsSolvable(provider_inferred, spks_inferred[0]), !(flags & UNSOLVABLE));
115+
BOOST_CHECK(provider_inferred.origins == script_provider.origins);
105116
}
117+
106118
// Test whether the observed key path is present in the 'paths' variable (which contains expected, unobserved paths),
107119
// and then remove it from that set.
108120
for (const auto& origin : script_provider.origins) {

src/wallet/rpcwallet.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <rpc/rawtransaction.h>
2323
#include <rpc/server.h>
2424
#include <rpc/util.h>
25+
#include <script/descriptor.h>
2526
#include <script/sign.h>
2627
#include <shutdown.h>
2728
#include <timedata.h>
@@ -2845,6 +2846,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
28452846
" \"redeemScript\" : n (string) The redeemScript if scriptPubKey is P2SH\n"
28462847
" \"spendable\" : xxx, (bool) Whether we have the private keys to spend this output\n"
28472848
" \"solvable\" : xxx, (bool) Whether we know how to spend this output, ignoring the lack of keys\n"
2849+
" \"desc\" : xxx, (string, only when solvable) A descriptor for spending this output\n"
28482850
" \"safe\" : xxx (bool) Whether this output is considered safe to spend. Unconfirmed transactions\n"
28492851
" from outside keys and unconfirmed replacement transactions are considered unsafe\n"
28502852
" and are not eligible for spending by fundrawtransaction and sendtoaddress.\n"
@@ -2963,6 +2965,10 @@ static UniValue listunspent(const JSONRPCRequest& request)
29632965
entry.pushKV("confirmations", out.nDepth);
29642966
entry.pushKV("spendable", out.fSpendable);
29652967
entry.pushKV("solvable", out.fSolvable);
2968+
if (out.fSolvable) {
2969+
auto descriptor = InferDescriptor(scriptPubKey, *pwallet);
2970+
entry.pushKV("desc", descriptor->ToString());
2971+
}
29662972
entry.pushKV("safe", out.fSafe);
29672973
results.push_back(entry);
29682974
}
@@ -3749,6 +3755,8 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
37493755
" \"ismine\" : true|false, (boolean) If the address is yours or not\n"
37503756
" \"solvable\" : true|false, (boolean) If the address is solvable by the wallet\n"
37513757
" \"iswatchonly\" : true|false, (boolean) If the address is watchonly\n"
3758+
" \"solvable\" : true|false, (boolean) Whether we know how to spend coins sent to this address, ignoring the possible lack of private keys\n"
3759+
" \"desc\" : \"desc\", (string, optional) A descriptor for spending coins sent to this address (only when solvable)\n"
37523760
" \"isscript\" : true|false, (boolean) If the key is a script\n"
37533761
" \"ischange\" : true|false, (boolean) If the address was used for change output\n"
37543762
" \"iswitness\" : true|false, (boolean) If the address is a witness address\n"
@@ -3802,6 +3810,11 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
38023810

38033811
isminetype mine = IsMine(*pwallet, dest);
38043812
ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE));
3813+
bool solvable = IsSolvable(*pwallet, scriptPubKey);
3814+
ret.pushKV("solvable", solvable);
3815+
if (solvable) {
3816+
ret.pushKV("desc", InferDescriptor(scriptPubKey, *pwallet)->ToString());
3817+
}
38053818
ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY));
38063819
ret.pushKV("solvable", IsSolvable(*pwallet, scriptPubKey));
38073820
UniValue detail = DescribeWalletAddress(pwallet, dest);

test/functional/rpc_scantxoutset.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import shutil
1111
import os
1212

13+
def descriptors(out):
14+
return sorted(u['desc'] for u in out['unspents'])
15+
1316
class ScantxoutsetTest(BitcoinTestFramework):
1417
def set_test_params(self):
1518
self.num_nodes = 1
@@ -93,5 +96,10 @@ def run_test(self):
9396
assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288"))
9497
assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672"))
9598

99+
# Test the reported descriptors for a few matches
100+
assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0'/*)", "range": 1499}])), ["pkh([0c5f9a1e/0'/0'/0]026dbd8b2315f296d36e6b6920b1579ca75569464875c7ebe869b536a7d9503c8c)", "pkh([0c5f9a1e/0'/0'/1]033e6f25d76c00bedb3a8993c7d5739ee806397f0529b1b31dda31ef890f19a60c)"])
101+
assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])), ["pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)"])
102+
assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ {"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])), ['pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)', 'pkh([0c5f9a1e/1/1/1500]03832901c250025da2aebae2bfb38d5c703a57ab66ad477f9c578bfbcd78abca6f)', 'pkh([0c5f9a1e/1/1/1]030d820fc9e8211c4169be8530efbc632775d8286167afd178caaf1089b77daba7)'])
103+
96104
if __name__ == '__main__':
97105
ScantxoutsetTest().main()

0 commit comments

Comments
 (0)