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
This commit is contained in:
commit
fdf146f329
9 changed files with 213 additions and 2 deletions
5
doc/release-notes-14477.md
Normal file
5
doc/release-notes-14477.md
Normal file
|
@ -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`).
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue