Merge #353: Support multisig xpub descriptors on Trezor

78a8801f94 Support multisig xpub descriptors (benk10)

Pull request description:

  Trezor Model T has an option when displaying a multisig address to also show all xpubs used as cosigners of the multisig. Currently, the display address function did not support using xpubs, so here I added support in Trezor for such descriptors, which will make xpubs show up correctly.

ACKs for top commit:
  achow101:
    ACK 78a8801f94

Tree-SHA512: 707e56b3cbac5f21ccdaeba912e751f23fe5fff637c18366cbe76b618840d4ba38fd2442919e2ff61f770c8381e59a6147d385a44d2def5dad07d89a09730eed
This commit is contained in:
Andrew Chow 2020-08-15 17:50:04 -04:00
commit 914ad3ea66
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41
9 changed files with 56 additions and 9 deletions

View File

@ -236,15 +236,18 @@ def displayaddress(client, path=None, desc=None, sh_wpkh=False, wpkh=False, rede
if descriptor.sh or descriptor.sh_wsh or descriptor.wsh:
path = ''
redeem_script = format(80 + int(descriptor.multisig_M), 'x')
xpubs_descriptor = False
for i in range(0, descriptor.multisig_N):
path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i] + ','
path += descriptor.origin_fingerprint[i] + descriptor.origin_path[i]
if not descriptor.path_suffix[i]:
redeem_script += '21' + descriptor.base_key[i]
else:
return {'error': 'Multisig descriptor must include all pubkeys', 'code': BAD_ARGUMENT}
path += descriptor.path_suffix[i]
xpubs_descriptor = True
path += ','
path = path[0:-1]
redeem_script += format(80 + descriptor.multisig_N, 'x') + 'ae'
return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script)
return client.display_address(path, descriptor.sh_wpkh or descriptor.sh_wsh, descriptor.wpkh or descriptor.wsh, redeem_script, descriptor=descriptor if xpubs_descriptor else None)
if descriptor.m_path is None:
return {'error': 'Descriptor missing origin info: ' + desc, 'code': BAD_ARGUMENT}
if descriptor.origin_fingerprint != client.get_master_fingerprint_hex():

View File

@ -1,3 +1,4 @@
# mypy: ignore-errors
import re
# From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp
@ -82,6 +83,12 @@ class Descriptor:
if origin_path and not isinstance(origin_path, list):
self.m_path_base = "m" + origin_path
self.m_path = "m" + origin_path + (path_suffix or "")
elif isinstance(origin_path, list):
self.m_path_base = []
self.m_path = []
for i in range(0, len(origin_path)):
self.m_path_base.append("m" + origin_path[i])
self.m_path.append("m" + origin_path[i] + (path_suffix[i] or ""))
@classmethod
def parse(cls, desc, testnet=False):

View File

@ -210,7 +210,7 @@ class ColdcardClient(HardwareWalletClient):
# Display address of specified type on the device.
@coldcard_exception
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
self.device.check_mitm()
keypath = keypath.replace('h', '\'')
keypath = keypath.replace('H', '\'')

View File

@ -551,7 +551,7 @@ class DigitalbitboxClient(HardwareWalletClient):
return {"signature": base64.b64encode(compact_sig).decode('utf-8')}
# Display address of specified type on the device.
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
raise UnavailableActionError('The Digital Bitbox does not have a screen to display addresses on')
# Setup a new device

View File

@ -343,7 +343,7 @@ class LedgerClient(HardwareWalletClient):
# Display address of specified type on the device. Only supports single-key based addresses.
@ledger_exception
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
if not check_keypath(keypath):
raise BadArgumentError("Invalid keypath")
if redeem_script is not None:

View File

@ -410,11 +410,20 @@ class TrezorClient(HardwareWalletClient):
# Display address of specified type on the device.
@trezor_exception
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None):
def display_address(self, keypath, p2sh_p2wpkh, bech32, redeem_script=None, descriptor=None):
self._check_unlocked()
# descriptor means multisig with xpubs
if descriptor:
pubkeys = []
xpub = ExtendedKey()
for i in range(0, descriptor.multisig_N):
xpub.deserialize(descriptor.base_key[i])
hd_node = proto.HDNodeType(depth=xpub.depth, fingerprint=int.from_bytes(xpub.parent_fingerprint, 'big'), child_num=xpub.child_num, chain_code=xpub.chaincode, public_key=xpub.pubkey)
pubkeys.append(proto.HDNodePathType(node=hd_node, address_n=tools.parse_path('m' + descriptor.path_suffix[i])))
multisig = proto.MultisigRedeemScriptType(m=int(descriptor.multisig_M), signatures=[b''] * int(descriptor.multisig_N), pubkeys=pubkeys)
# redeem_script means p2sh/multisig
if redeem_script:
elif redeem_script:
# Get multisig object required by Trezor's get_address
multisig = parse_multisig(bytes.fromhex(redeem_script))
if not multisig[0]:

View File

@ -1,6 +1,7 @@
from typing import Dict, Optional, Union
from .base58 import get_xpub_fingerprint_hex
from .descriptor import Descriptor
from .serializations import PSBT
@ -77,6 +78,7 @@ class HardwareWalletClient(object):
p2sh_p2wpkh: bool,
bech32: bool,
redeem_script: Optional[str] = None,
descriptor: Optional[Descriptor] = None,
) -> Dict[str, str]:
"""Display and return the address of specified type.

View File

@ -16,6 +16,18 @@ class TestDescriptor(unittest.TestCase):
self.assertEqual(desc.testnet, True)
self.assertEqual(desc.m_path, "m/84'/1'/0'/0/0")
def test_parse_multisig_descriptor_with_origin(self):
desc = Descriptor.parse("wsh(multi(2,[00000001/48'/0'/0'/2']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48'/0'/0'/2']tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))", True)
self.assertIsNotNone(desc)
self.assertEqual(desc.wsh, True)
self.assertEqual(desc.origin_fingerprint, ["00000001", "00000002"])
self.assertEqual(desc.origin_path, ["/48'/0'/0'/2'", "/48'/0'/0'/2'"])
self.assertEqual(desc.base_key, ["tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B", "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty"])
self.assertEqual(desc.path_suffix, ["/0/0", "/0/0"])
self.assertEqual(desc.testnet, True)
self.assertEqual(desc.m_path_base, ["m/48'/0'/0'/2'", "m/48'/0'/0'/2'"])
self.assertEqual(desc.m_path, ["m/48'/0'/0'/2'/0/0", "m/48'/0'/0'/2'/0/0"])
def test_parse_descriptor_without_origin(self):
desc = Descriptor.parse("wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)", True)
self.assertIsNotNone(desc)

View File

@ -13,6 +13,7 @@ import unittest
from authproxy import AuthServiceProxy, JSONRPCException
from hwilib.base58 import xpub_to_pub_hex
from hwilib.cli import process_commands
from hwilib.descriptor import AddChecksum
from hwilib.serializations import PSBT
# Class for emulator control
@ -657,11 +658,24 @@ class TestDisplayAddress(DeviceTestCase):
sh_wsh_multi_addr = self.wrpc.getaddressesbylabel("shwshmulti-display-desc").popitem()[0]
wsh_multi_addr = self.wrpc.getaddressesbylabel("wshmulti-display-desc").popitem()[0]
# need to replace `'` with `h` and to remove checksome for the stdin option to work
# need to replace `'` with `h` and to remove checksum for the stdin option to work
sh_multi_desc = sh_multi_desc.replace("'", "h").split('#')[0]
sh_wsh_multi_desc = sh_wsh_multi_desc.replace("'", "h").split('#')[0]
wsh_multi_desc = wsh_multi_desc.replace("'", "h").split('#')[0]
# descriptor with xpubs
if self.full_type == 'trezor_t':
account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/45h/0h/0h/2h'])['xpub']
desc = 'wsh(multi(2,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/0/0,[' + self.fingerprint + '/45h/0h/0h/2h]' + account_xpub + '/1/0))'
result = self.do_command(self.dev_args + ['displayaddress', '--desc', desc])
self.assertNotIn('error', result)
self.assertNotIn('code', result)
self.assertIn('address', result)
addr = self.wrpc.deriveaddresses(AddChecksum(desc))[0]
# removes prefix and checksum since regtest gives
# prefix `bcrt` on Bitcoin Core while wallets return testnet `tb` prefix
self.assertEqual(addr[4:58], result['address'][2:56])
# legacy
result = self.do_command(self.dev_args + ['displayaddress', '--desc', sh_multi_desc])
self.assertNotIn('error', result)