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:
Pieter Wuille
2018-11-27 12:14:33 -08:00
9 changed files with 213 additions and 2 deletions

View File

@@ -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()

View File

@@ -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