From 14e91168ecb779590ae43b8dea9aa16c3157faeb Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 5 Sep 2024 13:49:19 -0400 Subject: [PATCH] feat(BTC): add taproot multisig input signing --- core/src/apps/bitcoin/common.py | 8 +++- core/src/apps/bitcoin/scripts.py | 43 +++++++++++++++++++++ core/src/apps/bitcoin/sign_tx/bitcoin.py | 39 ++++++++++++++----- core/src/apps/bitcoin/sign_tx/sig_hasher.py | 14 +++++-- core/src/apps/bitcoin/sign_tx/tx_weight.py | 14 ++++++- core/src/apps/bitcoin/sign_tx/zcash_v4.py | 1 + core/src/apps/zcash/hasher.py | 1 + 7 files changed, 104 insertions(+), 16 deletions(-) diff --git a/core/src/apps/bitcoin/common.py b/core/src/apps/bitcoin/common.py index 14182c3ce1d..ee987a08525 100644 --- a/core/src/apps/bitcoin/common.py +++ b/core/src/apps/bitcoin/common.py @@ -70,11 +70,13 @@ def from_int(cls, sighash_type: int) -> "SigHashType": InputScriptType.SPENDMULTISIG, InputScriptType.SPENDP2SHWITNESS, InputScriptType.SPENDWITNESS, + InputScriptType.SPENDTAPROOT, ) MULTISIG_OUTPUT_SCRIPT_TYPES = ( OutputScriptType.PAYTOMULTISIG, OutputScriptType.PAYTOP2SHWITNESS, OutputScriptType.PAYTOWITNESS, + OutputScriptType.PAYTOTAPROOT, ) CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES: dict[OutputScriptType, InputScriptType] = { @@ -120,9 +122,11 @@ def ecdsa_sign(node: bip32.HDNode, digest: bytes) -> bytes: return sigder -def bip340_sign(node: bip32.HDNode, digest: bytes) -> bytes: +def bip340_sign(node: bip32.HDNode, digest: bytes, tweak: bool = True) -> bytes: internal_private_key = node.private_key() - output_private_key = bip340.tweak_secret_key(internal_private_key) + output_private_key = ( + bip340.tweak_secret_key(internal_private_key) if tweak else internal_private_key + ) return bip340.sign(output_private_key, digest) diff --git a/core/src/apps/bitcoin/scripts.py b/core/src/apps/bitcoin/scripts.py index b8d8b38baad..ef74f4ce8b9 100644 --- a/core/src/apps/bitcoin/scripts.py +++ b/core/src/apps/bitcoin/scripts.py @@ -14,6 +14,9 @@ OP_CHECKSIG, OP_CHECKSIGADD, OP_NUMEQUAL, + GENERATOR, + LEAF_VERSION, + p2tr_multisig_tweaked_pubkey, ) from .multisig import multisig_get_pubkeys, multisig_pubkey_index from .readers import read_memoryview_prefixed, read_op_push @@ -600,6 +603,46 @@ def parse_output_script_multisig(script: bytes) -> tuple[list[memoryview], int]: # === +def write_witness_multisig_taproot( + w: Writer, + multisig: MultisigRedeemScriptType, + signature: bytes, + signature_index: int, + sighash_type: SigHashType, +) -> None: + from .multisig import multisig_get_pubkey_count + + # get other signatures, stretch with zero byte to the number of the pubkeys + signatures = multisig.signatures + [0x00] * ( + multisig_get_pubkey_count(multisig) - len(multisig.signatures) + ) + + # fill in our signature + if signatures[signature_index] != 0x00: + raise DataError("Invalid multisig parameters") + signatures[signature_index] = signature + + # signatures + redeem script + control block + num_of_witness_items = len(signatures) + 1 + 1 + write_compact_size(w, num_of_witness_items) + + for s in signatures: + if s != 0x00: + write_signature_prefixed(w, s, sighash_type) # size of the witness included + else: + w.append(0x00) + + # redeem script + pubkeys = multisig_get_pubkeys(multisig) + write_output_script_multisig_taproot(w, pubkeys, multisig.m) + + # control block + write_compact_size(w, len(GENERATOR) + 1) + parity, _ = p2tr_multisig_tweaked_pubkey(pubkeys, multisig.m) + w.append(LEAF_VERSION + parity) + w.extend(GENERATOR) + + def write_output_script_multisig_taproot( w: Writer, pubkeys: Sequence[bytes | memoryview], diff --git a/core/src/apps/bitcoin/sign_tx/bitcoin.py b/core/src/apps/bitcoin/sign_tx/bitcoin.py index 08c3aea3919..cc1e9d39467 100644 --- a/core/src/apps/bitcoin/sign_tx/bitcoin.py +++ b/core/src/apps/bitcoin/sign_tx/bitcoin.py @@ -10,7 +10,7 @@ from apps.common.writers import write_compact_size from .. import addresses, common, multisig, scripts, writers -from ..common import SigHashType, ecdsa_sign, input_is_external +from ..common import SigHashType, ecdsa_sign, input_is_external, p2tr_multisig_leaf_hash from ..ownership import verify_nonownership from ..verification import SignatureVerifier from . import helpers @@ -642,14 +642,22 @@ def sign_bip143_input(self, i: int, txi: TxInput) -> tuple[bytes, bytes]: def sign_taproot_input(self, i: int, txi: TxInput) -> bytes: from ..common import bip340_sign + if txi.multisig: + public_keys = multisig.multisig_get_pubkeys(txi.multisig) + threshold = txi.multisig.m + leaf_hash = p2tr_multisig_leaf_hash(public_keys, threshold) + else: + leaf_hash = None + sigmsg_digest = self.tx_info.sig_hasher.hash341( - i, - self.tx_info.tx, - self.get_sighash_type(txi), + i, self.tx_info.tx, self.get_sighash_type(txi), leaf_hash ) node = self.keychain.derive(txi.address_n) - return bip340_sign(node, sigmsg_digest) + public_key = node.public_key() + signature = bip340_sign(node, sigmsg_digest, not txi.multisig) + + return public_key, signature async def sign_segwit_input(self, i: int) -> None: # STAGE_REQUEST_SEGWIT_WITNESS in legacy @@ -660,11 +668,24 @@ async def sign_segwit_input(self, i: int) -> None: raise ProcessError("Transaction has changed during signing") if txi.script_type == InputScriptType.SPENDTAPROOT: - signature = self.sign_taproot_input(i, txi) + public_key, signature = self.sign_taproot_input(i, txi) if self.serialize: - scripts.write_witness_p2tr( - self.serialized_tx, signature, self.get_sighash_type(txi) - ) + if txi.multisig: + # find out place of our signature based on the pubkey + signature_index = multisig.multisig_pubkey_index( + txi.multisig, public_key + ) + scripts.write_witness_multisig_taproot( + self.serialized_tx, + txi.multisig, + signature, + signature_index, + self.get_sighash_type(txi), + ) + else: + scripts.write_witness_p2tr( + self.serialized_tx, signature, self.get_sighash_type(txi) + ) else: public_key, signature = self.sign_bip143_input(i, txi) if self.serialize: diff --git a/core/src/apps/bitcoin/sign_tx/sig_hasher.py b/core/src/apps/bitcoin/sign_tx/sig_hasher.py index 83ee92d1636..27c5b3bcc8b 100644 --- a/core/src/apps/bitcoin/sign_tx/sig_hasher.py +++ b/core/src/apps/bitcoin/sign_tx/sig_hasher.py @@ -37,6 +37,7 @@ def hash341( i: int, tx: SignTx | PrevTx, sighash_type: SigHashType, + leaf_hash: bytes | None, ) -> bytes: ... def hash_zip244( @@ -132,9 +133,10 @@ def hash341( i: int, tx: SignTx | PrevTx, sighash_type: SigHashType, + leaf_hash: bytes | None, ) -> bytes: from ..common import tagged_hashwriter - from ..writers import write_uint8 + from ..writers import write_uint8, write_uint32 h_sigmsg = tagged_hashwriter(b"TapSighash") @@ -165,12 +167,18 @@ def hash341( # sha_outputs write_bytes_fixed(h_sigmsg, self.h_outputs.get_digest(), TX_HASH_SIZE) - # spend_type 0 (no tapscript message extension, no annex) - write_uint8(h_sigmsg, 0) + # spend_type, no annex support for now + spend_type = 0 if leaf_hash is None else 2 + write_uint8(h_sigmsg, spend_type) # input_index write_uint32(h_sigmsg, i) + if leaf_hash is not None: + write_bytes_fixed(h_sigmsg, leaf_hash, TX_HASH_SIZE) + write_uint8(h_sigmsg, 0) + write_uint32(h_sigmsg, 0xFFFFFFFF) + return h_sigmsg.get_digest() def hash_zip244( diff --git a/core/src/apps/bitcoin/sign_tx/tx_weight.py b/core/src/apps/bitcoin/sign_tx/tx_weight.py index a1b44dbd6d3..c25d1c23c46 100644 --- a/core/src/apps/bitcoin/sign_tx/tx_weight.py +++ b/core/src/apps/bitcoin/sign_tx/tx_weight.py @@ -35,6 +35,8 @@ _TXSIZE_SCHNORR_SIGNATURE = const(64) # size of a multiscript without pubkey (1 M, 1 N, 1 checksig) _TXSIZE_MULTISIGSCRIPT = const(3) +# size of a taproot multiscript without pubkey (1 M, 1 numequal, 1 + 33 control block) +_TXSIZE_MULTISIGSCRIPT_TAPROOT = const(36) # size of a p2wpkh script (1 version, 1 push, 20 hash) _TXSIZE_WITNESSPKHASH = const(22) # size of a p2wsh script (1 version, 1 push, 32 hash) @@ -72,10 +74,18 @@ def input_script_size(cls, i: TxInput) -> int: pass if multisig: + n = len(multisig.nodes) if multisig.nodes else len(multisig.pubkeys) if script_type == IST.SPENDTAPROOT: - raise wire.ProcessError("Multisig not supported for taproot") + multisig_script_size = _TXSIZE_MULTISIGSCRIPT_TAPROOT + n * ( + 1 + 1 + _TXSIZE_PUBKEY + ) + multisig_script_size += cls.compact_size_len(multisig_script_size) + return ( + multisig_script_size + + multisig.m * (1 + _TXSIZE_SCHNORR_SIGNATURE) + + (n - multisig.m) + ) - n = len(multisig.nodes) if multisig.nodes else len(multisig.pubkeys) multisig_script_size = _TXSIZE_MULTISIGSCRIPT + n * (1 + _TXSIZE_PUBKEY) if script_type in common.SEGWIT_INPUT_SCRIPT_TYPES: multisig_script_size += cls.compact_size_len(multisig_script_size) diff --git a/core/src/apps/bitcoin/sign_tx/zcash_v4.py b/core/src/apps/bitcoin/sign_tx/zcash_v4.py index 38a6ecc100e..72d3190c4e0 100644 --- a/core/src/apps/bitcoin/sign_tx/zcash_v4.py +++ b/core/src/apps/bitcoin/sign_tx/zcash_v4.py @@ -107,6 +107,7 @@ def hash341( i: int, tx: SignTx | PrevTx, sighash_type: SigHashType, + leaf_hash: bytes | None, ) -> bytes: raise NotImplementedError diff --git a/core/src/apps/zcash/hasher.py b/core/src/apps/zcash/hasher.py index 350681f0e78..5ac4db61a7a 100644 --- a/core/src/apps/zcash/hasher.py +++ b/core/src/apps/zcash/hasher.py @@ -118,6 +118,7 @@ def hash341( i: int, tx: SignTx | PrevTx, sighash_type: SigHashType, + leaf_hash: bytes | None, ) -> bytes: raise NotImplementedError