aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPieter Wuille <pieter.wuille@gmail.com>2018-11-27 12:14:33 -0800
committerPieter Wuille <pieter.wuille@gmail.com>2018-11-27 12:22:41 -0800
commitfdf146f3293c487afdc4d6d9f6b64099aa8bd28a (patch)
treea1daad986612ac06c94d5fb5ddaf0ce7304503b5
parent0fa3703c17573a3cf05c73ef828f381234b456f7 (diff)
parent109699dd33c72539fc33619f7836b8088f63182c (diff)
downloadbitcoin-fdf146f3293c487afdc4d6d9f6b64099aa8bd28a.tar.xz
Merge #14477: Add ability to convert solvability info to descriptor
109699dd33 Add release notes (Pieter Wuille) b65326b562 Add matching descriptors to scantxoutset output + tests (Pieter Wuille) 16203d5df7 Add descriptors to listunspent and getaddressinfo + tests (Pieter Wuille) 9b2a25b13f Add tests for InferDescriptor and Descriptor::IsSolvable (Pieter Wuille) 225bf3e3b0 Add Descriptor::IsSolvable() to distinguish addr/raw from others (Pieter Wuille) 4d78bd93b5 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
-rw-r--r--doc/release-notes-14477.md5
-rw-r--r--src/rpc/blockchain.cpp9
-rw-r--r--src/script/descriptor.cpp86
-rw-r--r--src/script/descriptor.h21
-rw-r--r--src/script/sign.h5
-rw-r--r--src/test/descriptor_tests.cpp12
-rw-r--r--src/wallet/rpcwallet.cpp13
-rwxr-xr-xtest/functional/rpc_scantxoutset.py8
-rwxr-xr-xtest/functional/wallet_address_types.py56
9 files changed, 213 insertions, 2 deletions
diff --git a/doc/release-notes-14477.md b/doc/release-notes-14477.md
new file mode 100644
index 0000000000..bb8c0a623e
--- /dev/null
+++ b/doc/release-notes-14477.md
@@ -0,0 +1,5 @@
+Miscellaneous RPC changes
+------------
+
+- `getaddressinfo` now reports `solvable`, a boolean indicating whether all information necessary for signing is present in the wallet (ignoring private keys).
+- `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`).
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index e3d9357358..403e3e397c 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -2187,6 +2187,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
" \"txid\" : \"transactionid\", (string) The transaction id\n"
" \"vout\": n, (numeric) the vout value\n"
" \"scriptPubKey\" : \"script\", (string) the script key\n"
+ " \"desc\" : \"descriptor\", (string) A specialized descriptor for the matched scriptPubKey\n"
" \"amount\" : x.xxx, (numeric) The total amount in " + CURRENCY_UNIT + " of the unspent output\n"
" \"height\" : n, (numeric) Height of the unspent transaction output\n"
" }\n"
@@ -2221,6 +2222,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan already in progress, use action \"abort\" or \"status\"");
}
std::set<CScript> needles;
+ std::map<CScript, std::string> descriptors;
CAmount total_in = 0;
// loop through the scan objects
@@ -2253,7 +2255,11 @@ UniValue scantxoutset(const JSONRPCRequest& request)
if (!desc->Expand(i, provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
}
- needles.insert(scripts.begin(), scripts.end());
+ for (const auto& script : scripts) {
+ std::string inferred = InferDescriptor(script, provider)->ToString();
+ needles.emplace(script);
+ descriptors.emplace(std::move(script), std::move(inferred));
+ }
}
}
@@ -2286,6 +2292,7 @@ UniValue scantxoutset(const JSONRPCRequest& request)
unspent.pushKV("txid", outpoint.hash.GetHex());
unspent.pushKV("vout", (int32_t)outpoint.n);
unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey.begin(), txo.scriptPubKey.end()));
+ unspent.pushKV("desc", descriptors[txo.scriptPubKey]);
unspent.pushKV("amount", ValueFromAmount(txo.nValue));
unspent.pushKV("height", (int32_t)coin.nHeight);
diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
index d343972c40..ca80d3451f 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<CScript>& 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<CScript>& output_scripts, FlatSigningProvider& out) const override
@@ -249,6 +251,7 @@ 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(); }
+ 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> 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(); }
+ 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<PubkeyProvider> 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
{
@@ -625,6 +632,80 @@ std::unique_ptr<Descriptor> ParseScript(Span<const char>& sp, ParseScriptContext
return nullptr;
}
+std::unique_ptr<PubkeyProvider> InferPubkey(const CPubKey& pubkey, ParseScriptContext, const SigningProvider& provider)
+{
+ std::unique_ptr<PubkeyProvider> key_provider = MakeUnique<ConstPubkeyProvider>(pubkey);
+ KeyOriginInfo info;
+ if (provider.GetKeyOrigin(pubkey.GetID(), info)) {
+ return MakeUnique<OriginPubkeyProvider>(std::move(info), std::move(key_provider));
+ }
+ return key_provider;
+}
+
+std::unique_ptr<Descriptor> InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider)
+{
+ std::vector<std::vector<unsigned char>> data;
+ txnouttype txntype = Solver(script, data);
+
+ if (txntype == TX_PUBKEY) {
+ CPubKey pubkey(data[0].begin(), data[0].end());
+ if (pubkey.IsValid()) {
+ return MakeUnique<SingleKeyDescriptor>(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<SingleKeyDescriptor>(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<SingleKeyDescriptor>(InferPubkey(pubkey, ctx, provider), P2WPKHGetScript, "wpkh");
+ }
+ }
+ if (txntype == TX_MULTISIG) {
+ std::vector<std::unique_ptr<PubkeyProvider>> 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<MultisigDescriptor>((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<ConvertorDescriptor>(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<ConvertorDescriptor>(std::move(sub), ConvertP2WSH, "wsh");
+ }
+ }
+
+ CTxDestination dest;
+ if (ExtractDestination(script, dest)) {
+ if (GetScriptForDestination(dest) == script) {
+ return MakeUnique<AddressDescriptor>(std::move(dest));
+ }
+ }
+
+ return MakeUnique<RawDescriptor>(script);
+}
+
} // namespace
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
if (sp.size() == 0 && ret) return ret;
return nullptr;
}
+
+std::unique_ptr<Descriptor> 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 87e07369c7..0111972f85 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;
@@ -51,5 +55,20 @@ struct Descriptor {
/** 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
+/** 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<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider);
+#endif // BITCOIN_SCRIPT_DESCRIPTOR_H
diff --git a/src/script/sign.h b/src/script/sign.h
index a478f49789..20c7203b26 100644
--- a/src/script/sign.h
+++ b/src/script/sign.h
@@ -24,6 +24,11 @@ struct KeyOriginInfo
{
unsigned char fingerprint[4];
std::vector<uint32_t> 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 e1ef619313..0e98f5a826 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<CScript> 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) {
diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp
index d4806b4c6b..b4c21631ab 100644
--- a/src/wallet/rpcwallet.cpp
+++ b/src/wallet/rpcwallet.cpp
@@ -22,6 +22,7 @@
#include <rpc/rawtransaction.h>
#include <rpc/server.h>
#include <rpc/util.h>
+#include <script/descriptor.h>
#include <script/sign.h>
#include <shutdown.h>
#include <timedata.h>
@@ -2845,6 +2846,7 @@ static UniValue listunspent(const JSONRPCRequest& request)
" \"redeemScript\" : n (string) The redeemScript if scriptPubKey is P2SH\n"
" \"spendable\" : xxx, (bool) Whether we have the private keys to spend this output\n"
" \"solvable\" : xxx, (bool) Whether we know how to spend this output, ignoring the lack of keys\n"
+ " \"desc\" : xxx, (string, only when solvable) A descriptor for spending this output\n"
" \"safe\" : xxx (bool) Whether this output is considered safe to spend. Unconfirmed transactions\n"
" from outside keys and unconfirmed replacement transactions are considered unsafe\n"
" and are not eligible for spending by fundrawtransaction and sendtoaddress.\n"
@@ -2963,6 +2965,10 @@ static UniValue listunspent(const JSONRPCRequest& request)
entry.pushKV("confirmations", out.nDepth);
entry.pushKV("spendable", out.fSpendable);
entry.pushKV("solvable", out.fSolvable);
+ if (out.fSolvable) {
+ auto descriptor = InferDescriptor(scriptPubKey, *pwallet);
+ entry.pushKV("desc", descriptor->ToString());
+ }
entry.pushKV("safe", out.fSafe);
results.push_back(entry);
}
@@ -3749,6 +3755,8 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
" \"ismine\" : true|false, (boolean) If the address is yours or not\n"
" \"solvable\" : true|false, (boolean) If the address is solvable by the wallet\n"
" \"iswatchonly\" : true|false, (boolean) If the address is watchonly\n"
+ " \"solvable\" : true|false, (boolean) Whether we know how to spend coins sent to this address, ignoring the possible lack of private keys\n"
+ " \"desc\" : \"desc\", (string, optional) A descriptor for spending coins sent to this address (only when solvable)\n"
" \"isscript\" : true|false, (boolean) If the key is a script\n"
" \"ischange\" : true|false, (boolean) If the address was used for change output\n"
" \"iswitness\" : true|false, (boolean) If the address is a witness address\n"
@@ -3802,6 +3810,11 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
isminetype mine = IsMine(*pwallet, dest);
ret.pushKV("ismine", bool(mine & ISMINE_SPENDABLE));
+ bool solvable = IsSolvable(*pwallet, scriptPubKey);
+ ret.pushKV("solvable", solvable);
+ if (solvable) {
+ ret.pushKV("desc", InferDescriptor(scriptPubKey, *pwallet)->ToString());
+ }
ret.pushKV("iswatchonly", bool(mine & ISMINE_WATCH_ONLY));
ret.pushKV("solvable", IsSolvable(*pwallet, scriptPubKey));
UniValue detail = DescribeWalletAddress(pwallet, dest);
diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py
index 881b839a4e..11b4db6ec5 100755
--- a/test/functional/rpc_scantxoutset.py
+++ b/test/functional/rpc_scantxoutset.py
@@ -10,6 +10,9 @@ from decimal import Decimal
import shutil
import os
+def descriptors(out):
+ return sorted(u['desc'] for u in out['unspents'])
+
class ScantxoutsetTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
@@ -93,5 +96,10 @@ class ScantxoutsetTest(BitcoinTestFramework):
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"))
+ # Test the reported descriptors for a few matches
+ 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)"])
+ assert_equal(descriptors(self.nodes[0].scantxoutset("start", [ "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"])), ["pkh([0c5f9a1e/1/1/0]03e1c5b6e650966971d7e71ef2674f80222752740fc1dfd63bbbd220d2da9bd0fb)"])
+ 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)'])
+
if __name__ == '__main__':
ScantxoutsetTest().main()
diff --git a/test/functional/wallet_address_types.py b/test/functional/wallet_address_types.py
index 0f75045c9d..bafa556aad 100755
--- a/test/functional/wallet_address_types.py
+++ b/test/functional/wallet_address_types.py
@@ -99,6 +99,8 @@ class AddressTypeTest(BitcoinTestFramework):
"""Run sanity checks on an address."""
info = self.nodes[node].getaddressinfo(address)
assert(self.nodes[node].validateaddress(address)['isvalid'])
+ assert_equal(info.get('solvable'), True)
+
if not multisig and typ == 'legacy':
# P2PKH
assert(not info['isscript'])
@@ -146,6 +148,47 @@ class AddressTypeTest(BitcoinTestFramework):
# Unknown type
assert(False)
+ def test_desc(self, node, address, multisig, typ, utxo):
+ """Run sanity checks on a descriptor reported by getaddressinfo."""
+ info = self.nodes[node].getaddressinfo(address)
+ assert('desc' in info)
+ assert_equal(info['desc'], utxo['desc'])
+ assert(self.nodes[node].validateaddress(address)['isvalid'])
+
+ # Use a ridiculously roundabout way to find the key origin info through
+ # the PSBT logic. However, this does test consistency between the PSBT reported
+ # fingerprints/paths and the descriptor logic.
+ psbt = self.nodes[node].createpsbt([{'txid':utxo['txid'], 'vout':utxo['vout']}],[{address:0.00010000}])
+ psbt = self.nodes[node].walletprocesspsbt(psbt, False, "ALL", True)
+ decode = self.nodes[node].decodepsbt(psbt['psbt'])
+ key_descs = {}
+ for deriv in decode['inputs'][0]['bip32_derivs']:
+ assert_equal(len(deriv['master_fingerprint']), 8)
+ assert_equal(deriv['path'][0], 'm')
+ key_descs[deriv['pubkey']] = '[' + deriv['master_fingerprint'] + deriv['path'][1:] + ']' + deriv['pubkey']
+
+ if not multisig and typ == 'legacy':
+ # P2PKH
+ assert_equal(info['desc'], "pkh(%s)" % key_descs[info['pubkey']])
+ elif not multisig and typ == 'p2sh-segwit':
+ # P2SH-P2WPKH
+ assert_equal(info['desc'], "sh(wpkh(%s))" % key_descs[info['pubkey']])
+ elif not multisig and typ == 'bech32':
+ # P2WPKH
+ assert_equal(info['desc'], "wpkh(%s)" % key_descs[info['pubkey']])
+ elif typ == 'legacy':
+ # P2SH-multisig
+ assert_equal(info['desc'], "sh(multi(2,%s,%s))" % (key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]]))
+ elif typ == 'p2sh-segwit':
+ # P2SH-P2WSH-multisig
+ assert_equal(info['desc'], "sh(wsh(multi(2,%s,%s)))" % (key_descs[info['embedded']['pubkeys'][0]], key_descs[info['embedded']['pubkeys'][1]]))
+ elif typ == 'bech32':
+ # P2WSH-multisig
+ assert_equal(info['desc'], "wsh(multi(2,%s,%s))" % (key_descs[info['pubkeys'][0]], key_descs[info['pubkeys'][1]]))
+ else:
+ # Unknown type
+ assert(False)
+
def test_change_output_type(self, node_sender, destinations, expected_type):
txid = self.nodes[node_sender].sendmany(dummy="", amounts=dict.fromkeys(destinations, 0.001))
raw_tx = self.nodes[node_sender].getrawtransaction(txid)
@@ -198,6 +241,7 @@ class AddressTypeTest(BitcoinTestFramework):
self.log.debug("Old balances are {}".format(old_balances))
to_send = (old_balances[from_node] / 101).quantize(Decimal("0.00000001"))
sends = {}
+ addresses = {}
self.log.debug("Prepare sends")
for n, to_node in enumerate(range(from_node, from_node + 4)):
@@ -228,6 +272,7 @@ class AddressTypeTest(BitcoinTestFramework):
# Output entry
sends[address] = to_send * 10 * (1 + n)
+ addresses[to_node] = (address, typ)
self.log.debug("Sending: {}".format(sends))
self.nodes[from_node].sendmany("", sends)
@@ -244,6 +289,17 @@ class AddressTypeTest(BitcoinTestFramework):
self.nodes[5].generate(1)
sync_blocks(self.nodes)
+ # Verify that the receiving wallet contains a UTXO with the expected address, and expected descriptor
+ for n, to_node in enumerate(range(from_node, from_node + 4)):
+ to_node %= 4
+ found = False
+ for utxo in self.nodes[to_node].listunspent():
+ if utxo['address'] == addresses[to_node][0]:
+ found = True
+ self.test_desc(to_node, addresses[to_node][0], multisig, addresses[to_node][1], utxo)
+ break
+ assert found
+
new_balances = self.get_balances()
self.log.debug("Check new balances: {}".format(new_balances))
# We don't know what fee was set, so we can only check bounds on the balance of the sending node