Merge #353: Support multisig xpub descriptors on Trezor
78a8801f94Support 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: ACK78a8801f94Tree-SHA512: 707e56b3cbac5f21ccdaeba912e751f23fe5fff637c18366cbe76b618840d4ba38fd2442919e2ff61f770c8381e59a6147d385a44d2def5dad07d89a09730eed
This commit is contained in:
commit
914ad3ea66
@ -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():
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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', '\'')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user