Skip to content

Commit

Permalink
Merge #544: trezor: Implement Taproot support
Browse files Browse the repository at this point in the history
2669bce tests: each device explicitly lists signtx cases (Andrew Chow)
b9093d1 trezor: Disallow external inputs when signing with tr (Andrew Chow)
c37acd4 tests: Taproot signing tests (Andrew Chow)
ab189db tests: Add Trezor to SUPPORTS_TAPROOT (Andrew Chow)
6ae50be trezor: Sign Taproot inputs (Andrew Chow)
9fe19f4 trezor: Allow taproot addresses for displayaddress (Andrew Chow)
f168058 trezor: Implement can_sign_taproot (Andrew Chow)

Pull request description:

  Implements Taproot support for Trezor devices.

  Depends on #545, #546, #547, and #548

  No tests yet as they would need bitcoin/bitcoin#22558 merged into Bitcoin Core.

Top commit has no ACKs.

Tree-SHA512: 608e554efe459772465a4066679d108ae6e81f1063a3e4b67832140f199d7565ff46f13740a401875a2b54bc94828dbc77054b0203bdca93d4c439ee829d07eb
  • Loading branch information
achow101 committed Feb 1, 2022
2 parents 6871946 + 2669bce commit 985ec97
Show file tree
Hide file tree
Showing 10 changed files with 1,469 additions and 58 deletions.
79 changes: 59 additions & 20 deletions hwilib/devices/trezor.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@

Device = Union[hid.HidTransport, webusb.WebUsbTransport, udp.UdpTransport]

ECDSA_SCRIPT_TYPES = [
messages.InputScriptType.SPENDADDRESS,
messages.InputScriptType.SPENDMULTISIG,
messages.InputScriptType.SPENDWITNESS,
messages.InputScriptType.SPENDP2SHWITNESS,
]
SCHNORR_SCRIPT_TYPES = [
messages.InputScriptType.SPENDTAPROOT,
]


# Only handles up to 15 of 15
def parse_multisig(script: bytes, tx_xpubs: Dict[bytes, KeyOriginInfo], psbt_scope: Union[PartiallySignedInput, PartiallySignedOutput]) -> Tuple[bool, Optional[messages.MultisigRedeemScriptType]]:
Expand Down Expand Up @@ -351,6 +361,7 @@ def sign_tx(self, tx: PSBT) -> PSBT:
# Prepare inputs
inputs = []
to_ignore = [] # Note down which inputs whose signatures we're going to ignore
has_tr = False
for input_num, (psbt_in, txin) in py_enumerate(list(zip(tx.inputs, tx.tx.vin))):
txinputtype = messages.TxInputType(
prev_hash=ser_uint256(txin.prevout.hash)[::-1],
Expand Down Expand Up @@ -381,13 +392,16 @@ def sign_tx(self, tx: PSBT) -> PSBT:
p2sh = True

# Check segwit
is_wit, _, _ = is_witness(scriptcode)
is_wit, wit_ver, _ = is_witness(scriptcode)

if is_wit:
if p2sh:
txinputtype.script_type = messages.InputScriptType.SPENDP2SHWITNESS
else:
txinputtype.script_type = messages.InputScriptType.SPENDWITNESS
if wit_ver == 0:
if p2sh:
txinputtype.script_type = messages.InputScriptType.SPENDP2SHWITNESS
else:
txinputtype.script_type = messages.InputScriptType.SPENDWITNESS
elif wit_ver == 1:
txinputtype.script_type = messages.InputScriptType.SPENDTAPROOT
else:
txinputtype.script_type = messages.InputScriptType.SPENDADDRESS
txinputtype.amount = utxo.nValue
Expand Down Expand Up @@ -433,16 +447,29 @@ def ignore_input() -> None:
found = False # Whether we have found a key to sign with
found_in_sigs = False # Whether we have found one of our keys in the signatures
our_keys = 0
for key in psbt_in.hd_keypaths.keys():
keypath = psbt_in.hd_keypaths[key]
if keypath.fingerprint == master_fp:
if key in psbt_in.partial_sigs: # This key already has a signature
found_in_sigs = True
continue
if not found: # This key does not have a signature and we don't have a key to sign with yet
txinputtype.address_n = keypath.path
found = True
our_keys += 1
if txinputtype.script_type in ECDSA_SCRIPT_TYPES:
for key in psbt_in.hd_keypaths.keys():
keypath = psbt_in.hd_keypaths[key]
if keypath.fingerprint == master_fp:
if key in psbt_in.partial_sigs: # This key already has a signature
found_in_sigs = True
continue
if not found: # This key does not have a signature and we don't have a key to sign with yet
txinputtype.address_n = keypath.path
found = True
our_keys += 1
elif txinputtype.script_type in SCHNORR_SCRIPT_TYPES:
if len(psbt_in.tap_key_sig) > 0:
found_in_sigs = True
else:
for key, (leaf_hashes, origin) in psbt_in.tap_bip32_paths.items():
# TODO: Support script path signing
if key == psbt_in.tap_internal_key and origin.fingerprint == master_fp:
has_tr = True
txinputtype.address_n = origin.path
found = True
our_keys += 1
break

# Determine if we need to do more passes to sign everything
if our_keys > passes:
Expand All @@ -459,6 +486,10 @@ def ignore_input() -> None:
# append to inputs
inputs.append(txinputtype)

# Cannot sign transactions that have external inputs (to_ignore is not empty) and would sign taproot inputs
if has_tr and len(to_ignore) > 0:
raise BadArgumentError("Trezor cannot sign taproot inputs when the transaction also has external inputs")

# address version byte
if self.chain != Chain.MAIN:
p2pkh_version = b'\x6f'
Expand Down Expand Up @@ -568,6 +599,10 @@ def ignore_input() -> None:
if fp == master_fp and pubkey not in psbt_in.partial_sigs:
psbt_in.partial_sigs[pubkey] = sig + b'\x01'
break
if len(psbt_in.tap_internal_key) > 0 and len(psbt_in.tap_key_sig) == 0:
# Assume key path sig
# TODO: Deal with script path sig
psbt_in.tap_key_sig = sig

p += 1

Expand Down Expand Up @@ -596,7 +631,9 @@ def display_singlesig_address(
elif addr_type == AddressType.LEGACY:
script_type = messages.InputScriptType.SPENDADDRESS
elif addr_type == AddressType.TAP:
raise UnavailableActionError("Trezor does not support displaying Taproot addresses yet")
if not self.can_sign_taproot():
raise UnavailableActionError("This device does not support displaying Taproot addresses")
script_type = messages.InputScriptType.SPENDTAPROOT
else:
raise BadArgumentError("Unknown address type")

Expand Down Expand Up @@ -761,13 +798,15 @@ def toggle_passphrase(self) -> bool:
@trezor_exception
def can_sign_taproot(self) -> bool:
"""
Trezor T supports Taproot in firmware versions greater than (not including) 2.4.2.
Trezor One supports Taproot in firmware versions greater than (not including) 1.10.3.
However HWI does not implement Taproot support for any Trezor devices yet.
Trezor T supports Taproot since firmware version 2.4.3.
Trezor One supports Taproot since firmware version 1.10.4.
:returns: False, always.
"""
return False
self._prepare_device()
if self.client.features.model == "T":
return bool(self.client.version >= (2, 4, 3))
return bool(self.client.version >= (1, 10, 4))


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

0 comments on commit 985ec97

Please sign in to comment.