Skip to content

Commit

Permalink
Merge #547: Add AddressType.TAP and be able to fetch Taproot descript…
Browse files Browse the repository at this point in the history
…ors from getdescriptors and getkeypool

3f74cc7 Do not show Taproot things for devices without Taproot support (Andrew Chow)
7950173 hwwclient: Add a function to check whether the device supports Taproot (Andrew Chow)
054ce4d displayaddress: Check against xonly pubkey too (Andrew Chow)
b47b7c8 tests: check tr() descriptors from getkeypool (Andrew Chow)
a7528b8 Have getdescriptors fetch for all address types (Andrew Chow)
8928c12 Cleanup Ledger displayaddress address type handling (Andrew Chow)
1312817 Handle tr() descriptors in displayaddress (Andrew Chow)
4c438cc Handle AddressType.TAP for getkeypool and getdescriptors (Andrew Chow)
08ab51b Add a derivation path for AddressType.TAP (Andrew Chow)
19aedce Add AddressType.TAP (Andrew Chow)
15f691f Implement bech32m (Andrew Chow)

Pull request description:

  Adds a `AddressType.TAP` which can be used as `tap` in `displayaddress` and anywhere else `--addr-type` is used. Implements the bech32m encoding scheme. Also adds `tr()` descriptors to `getdescriptors` and `getkeypool` output.

  Depends on #545

ACKs for top commit:
  Sjors:
    utACK 3f74cc7

Tree-SHA512: 94059fc5bcc22567e1d1efc58849235686635384d9e6f8b80bd510c956f10bec17259a6b0c2c012cd60c55d383e2915179da9cc7c28ff2fc36dd74b8c00303a6
  • Loading branch information
achow101 committed Dec 20, 2021
2 parents e0a6c5c + 3f74cc7 commit 84842b5
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 91 deletions.
13 changes: 13 additions & 0 deletions hwilib/_base58.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ def xpub_to_pub_hex(xpub: str) -> str:
pubkey = data[-37:-4]
return hexlify(pubkey).decode()


def xpub_to_xonly_pub_hex(xpub: str) -> str:
"""
Get the public key as a string from the extended public key.
:param xpub: The extended pubkey
:return: The pubkey hex string
"""
data = decode(xpub)
pubkey = data[-36:-4]
return hexlify(pubkey).decode()


def xpub_main_2_test(xpub: str) -> str:
"""
Convert an extended pubkey from mainnet version to testnet version.
Expand Down
49 changes: 34 additions & 15 deletions hwilib/_bech32.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Reference implementation for Bech32 and segwit addresses.
"""

from enum import Enum
from typing import (
List,
Optional,
Expand All @@ -34,6 +35,13 @@


CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32_CONST = 1
BECH32M_CONST = 0x2bc830a3

class Encoding(Enum):
"""Enumeration type to list the various supported encodings."""
BECH32 = 1
BECH32M = 2


def bech32_polymod(values: List[int]) -> int:
Expand All @@ -53,40 +61,48 @@ def bech32_hrp_expand(hrp: str) -> List[int]:
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]


def bech32_verify_checksum(hrp: str, data: List[int]) -> bool:
def bech32_verify_checksum(hrp: str, data: List[int]) -> Optional[Encoding]:
"""Verify a checksum given HRP and converted data characters."""
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
check = bech32_polymod(bech32_hrp_expand(hrp) + data)
if check == BECH32_CONST:
return Encoding.BECH32
elif check == BECH32M_CONST:
return Encoding.BECH32M
else:
return None


def bech32_create_checksum(hrp: str, data: List[int]) -> List[int]:
def bech32_create_checksum(encoding: Encoding, hrp: str, data: List[int]) -> List[int]:
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]


def bech32_encode(hrp: str, data: List[int]) -> str:
def bech32_encode(encoding: Encoding, hrp: str, data: List[int]) -> str:
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data)
combined = data + bech32_create_checksum(encoding, hrp, data)
return hrp + '1' + ''.join([CHARSET[d] for d in combined])


def bech32_decode(bech: str) -> Tuple[Optional[str], Optional[List[int]]]:
def bech32_decode(bech: str) -> Tuple[Optional[Encoding], Optional[str], Optional[List[int]]]:
"""Validate a Bech32 string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
(bech.lower() != bech and bech.upper() != bech)):
return (None, None)
return (None, None, None)
bech = bech.lower()
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
return (None, None)
return (None, None, None)
if not all(x in CHARSET for x in bech[pos + 1:]):
return (None, None)
return (None, None, None)
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos + 1:]]
if not bech32_verify_checksum(hrp, data):
return (None, None)
return (hrp, data[:-6])
encoding = bech32_verify_checksum(hrp, data)
if encoding is None:
return (None, None, None)
return (encoding, hrp, data[:-6])


def convertbits(data: Union[bytes, List[int]], frombits: int, tobits: int, pad: bool = True) -> Optional[List[int]]:
Expand Down Expand Up @@ -114,7 +130,7 @@ def convertbits(data: Union[bytes, List[int]], frombits: int, tobits: int, pad:

def decode(hrp: str, addr: str) -> Tuple[Optional[int], Optional[List[int]]]:
"""Decode a segwit address."""
hrpgot, data = bech32_decode(addr)
encoding, hrpgot, data = bech32_decode(addr)
if hrpgot != hrp or hrpgot is None or data is None:
return (None, None)
decoded = convertbits(data[1:], 5, 8, False)
Expand All @@ -124,15 +140,18 @@ def decode(hrp: str, addr: str) -> Tuple[Optional[int], Optional[List[int]]]:
return (None, None)
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
return (None, None)
if (data[0] == 0 and encoding != Encoding.BECH32) or (data[0] != 0 and encoding != Encoding.BECH32M):
return (None, None)
return (data[0], decoded)


def encode(hrp: str, witver: int, witprog: bytes) -> Optional[str]:
"""Encode a segwit address."""
encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
conv_bits = convertbits(witprog, 8, 5)
if conv_bits is None:
return None
ret = bech32_encode(hrp, [witver] + conv_bits)
ret = bech32_encode(encoding, hrp, [witver] + conv_bits)
if decode(hrp, ret) == (None, None):
return None
return ret
36 changes: 24 additions & 12 deletions hwilib/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import logging
import platform

from ._base58 import xpub_to_pub_hex
from ._base58 import xpub_to_pub_hex, xpub_to_xonly_pub_hex
from .key import (
get_bip44_purpose,
get_bip44_chain,
Expand All @@ -42,6 +42,7 @@
Descriptor,
parse_descriptor,
MultisigDescriptor,
TRDescriptor,
PKHDescriptor,
PubkeyProvider,
SHDescriptor,
Expand Down Expand Up @@ -289,8 +290,6 @@ def getdescriptor(
:return: The descriptor constructed given the above arguments and key fetched from the device
:raises: BadArgumentError: if an argument is malformed or missing.
"""
is_wpkh = addr_type is AddressType.WIT
is_sh_wpkh = addr_type is AddressType.SH_WIT

parsed_path = []
if not path:
Expand Down Expand Up @@ -336,12 +335,18 @@ def getdescriptor(
client.xpub_cache[path_base] = client.get_pubkey_at_path(path_base).to_string()

pubkey = PubkeyProvider(origin, client.xpub_cache.get(path_base, ""), path_suffix)
if is_wpkh:
return WPKHDescriptor(pubkey)
elif is_sh_wpkh:
if addr_type is AddressType.LEGACY:
return PKHDescriptor(pubkey)
elif addr_type is AddressType.SH_WIT:
return SHDescriptor(WPKHDescriptor(pubkey))
elif addr_type is AddressType.WIT:
return WPKHDescriptor(pubkey)
elif addr_type is AddressType.TAP:
if not client.can_sign_taproot():
raise UnavailableActionError("Device does not support Taproot")
return TRDescriptor(pubkey)
else:
return PKHDescriptor(pubkey)
raise ValueError("Unknown address type")

def getkeypool(
client: HardwareWalletClient,
Expand All @@ -365,16 +370,21 @@ def getkeypool(
:param internal: Whether the dictionary should indicate that the descriptor should be for change addresses
:param keypool: Whether the dictionary should indicate that the dsecriptor should be added to the Bitcoin Core keypool/addresspool
:param account: The BIP 44 account to use if ``path`` is not specified
:param sh_wpkh: Whether to return a descriptor specifying p2sh-segwit addresses
:param wpkh: Whether to return a descriptor specifying native segwit addresses
:param addr_type: The address type
:param addr_all: Whether to return a multiple descriptors for every address type
:return: The dictionary containing the descriptor and all of the arguments for ``importmulti`` or ``importdescriptors``
:raises: BadArgumentError: if an argument is malformed or missing.
"""
supports_taproot = client.can_sign_taproot()

addr_types = [addr_type]
if addr_all:
addr_types = list(AddressType)
elif not supports_taproot and addr_type == AddressType.TAP:
raise UnavailableActionError("Device does not support Taproot")

if not supports_taproot and AddressType.TAP in addr_types:
del addr_types[addr_types.index(AddressType.TAP)]

# When no specific path or internal-ness is specified, create standard types
chains: List[Dict[str, Any]] = []
Expand Down Expand Up @@ -406,7 +416,7 @@ def getdescriptors(

for internal in [False, True]:
descriptors = []
for addr_type in (AddressType.LEGACY, AddressType.SH_WIT, AddressType.WIT):
for addr_type in list(AddressType):
try:
desc = getdescriptor(client, master_fpr=master_fpr, internal=internal, addr_type=addr_type, account=account)
except UnavailableActionError:
Expand Down Expand Up @@ -461,19 +471,21 @@ def displayaddress(
addr_type = AddressType.WIT
return {"address": client.display_multisig_address(addr_type, descriptor)}
is_wpkh = isinstance(descriptor, WPKHDescriptor)
if isinstance(descriptor, PKHDescriptor) or is_wpkh:
if isinstance(descriptor, PKHDescriptor) or is_wpkh or isinstance(descriptor, TRDescriptor):
pubkey = descriptor.pubkeys[0]
if pubkey.origin is None:
raise BadArgumentError(f"Descriptor missing origin info: {desc}")
if pubkey.origin.fingerprint != client.get_master_fingerprint():
raise BadArgumentError(f"Descriptor fingerprint does not match device: {desc}")
xpub = client.get_pubkey_at_path(pubkey.origin.get_derivation_path()).to_string()
if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub):
if pubkey.pubkey != xpub and pubkey.pubkey != xpub_to_pub_hex(xpub) and pubkey.pubkey != xpub_to_xonly_pub_hex(xpub):
raise BadArgumentError(f"Key in descriptor does not match device: {desc}")
if is_sh and is_wpkh:
addr_type = AddressType.SH_WIT
elif not is_sh and is_wpkh:
addr_type = AddressType.WIT
elif isinstance(descriptor, TRDescriptor):
addr_type = AddressType.TAP
return {"address": client.display_singlesig_address(pubkey.get_full_derivation_path(0), addr_type)}
raise BadArgumentError("Missing both path and descriptor")

Expand Down
1 change: 1 addition & 0 deletions hwilib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class AddressType(Enum):
LEGACY = 1 #: Legacy address type. P2PKH for single sig, P2SH for scripts.
WIT = 2 #: Native segwit v0 address type. P2WPKH for single sig, P2WPSH for scripts.
SH_WIT = 3 #: Nested segwit v0 address type. P2SH-P2WPKH for single sig, P2SH-P2WPSH for scripts.
TAP = 4 #: Segwit v1 Taproot address type. P2TR always.

def __str__(self) -> str:
return self.name.lower()
Expand Down
10 changes: 10 additions & 0 deletions hwilib/devices/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@ def display_singlesig_address(
raise UnavailableActionError(
"The BitBox02 does not support legacy p2pkh addresses"
)
elif addr_type == AddressType.TAP:
raise UnavailableActionError("BitBox02 does not support displaying Taproot addresses yet")
else:
raise BadArgumentError("Unknown address type")
address = self.init().btc_address(
Expand Down Expand Up @@ -866,3 +868,11 @@ def restore_device(

bb02.restore_from_mnemonic()
return True

def can_sign_taproot(self) -> bool:
"""
The BitBox02 does not support Taproot yet.
:returns: False, always
"""
return False
11 changes: 11 additions & 0 deletions hwilib/devices/coldcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ def display_singlesig_address(
addr_fmt = AF_P2WPKH
elif addr_type == AddressType.LEGACY:
addr_fmt = AF_CLASSIC
elif addr_type == AddressType.TAP:
raise UnavailableActionError("Coldcard does not support displaying Taproot addresses yet")
else:
raise BadArgumentError("Unknown address type")

Expand Down Expand Up @@ -386,6 +388,15 @@ def toggle_passphrase(self) -> bool:
"""
raise UnavailableActionError('The Coldcard does not support toggling passphrase from the host')

def can_sign_taproot(self) -> bool:
"""
The Coldard does not support Taproot yet.
:returns: False, always
"""
return False


def enumerate(password: str = "") -> List[Dict[str, Any]]:
results = []
devices = hid.enumerate(COINKITE_VID, CKCC_PID)
Expand Down
9 changes: 9 additions & 0 deletions hwilib/devices/digitalbitbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,15 @@ def toggle_passphrase(self) -> bool:
"""
raise UnavailableActionError('The Digital Bitbox does not support toggling passphrase from the host')

def can_sign_taproot(self) -> bool:
"""
The BitBox01 does not support Taproot as it is no longer supported by the manufacturer
:returns: False, always
"""
return False


def enumerate(password: str = "") -> List[Dict[str, Any]]:
results = []
devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID)
Expand Down
10 changes: 10 additions & 0 deletions hwilib/devices/jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,16 @@ def toggle_passphrase(self) -> bool:
"""
raise UnavailableActionError('Blockstream Jade does not support toggling passphrase from the host')

@jade_exception
def can_sign_taproot(self) -> bool:
"""
Blockstream Jade does not currently support Taproot.
:returns: False, always
"""
return False


def enumerate(password: str = '') -> List[Dict[str, Any]]:
results = []

Expand Down
8 changes: 8 additions & 0 deletions hwilib/devices/keepkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ def __init__(self, path: str, password: str = "", expert: bool = False) -> None:
if self.simulator:
self.client.debug.map_type_to_class_override[KeepkeyDebugLinkState.MESSAGE_WIRE_TYPE] = KeepkeyDebugLinkState

def can_sign_taproot(self) -> bool:
"""
The KeepKey does not support Taproot yet.
:returns: False, always
"""
return False


def enumerate(password: str = "") -> List[Dict[str, Any]]:
results = []
Expand Down
25 changes: 23 additions & 2 deletions hwilib/devices/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,18 @@ def display_singlesig_address(
) -> str:
if not check_keypath(keypath):
raise BadArgumentError("Invalid keypath")
p2sh_p2wpkh = addr_type == AddressType.SH_WIT
bech32 = addr_type == AddressType.WIT
bech32 = False
p2sh_p2wpkh = False
if addr_type == AddressType.SH_WIT:
p2sh_p2wpkh = True
elif addr_type == AddressType.WIT:
bech32 = True
elif addr_type == AddressType.LEGACY:
pass
elif addr_type == AddressType.TAP:
raise UnavailableActionError("Ledger does not support displaying Taproot addresses yet")
else:
raise BadArgumentError("Unknown address type")
output = self.app.getWalletPublicKey(keypath[2:], True, p2sh_p2wpkh or bech32, bech32)
assert isinstance(output["address"], str)
return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'<address>')". This extracts the actual address to work around this.
Expand Down Expand Up @@ -425,6 +435,17 @@ def toggle_passphrase(self) -> bool:
"""
raise UnavailableActionError('The Ledger Nano S and X do not support toggling passphrase from the host')

@ledger_exception
def can_sign_taproot(self) -> bool:
"""
Ledgers support Taproot if the Bitcoin App version greater than 2.0.0.
However HWI does not implement Taproot support for the Ledger yet.
:returns: False, always
"""
return False


def enumerate(password: str = '') -> List[Dict[str, Any]]:
results = []
devices = []
Expand Down

0 comments on commit 84842b5

Please sign in to comment.