From ca128b1356f9387aeb2c990de2c5959c7fd5d7a1 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 5 Sep 2024 13:48:41 -0400 Subject: [PATCH] feat(BTC): add taproot multisig address generation --- core/src/apps/bitcoin/addresses.py | 25 ++++++++++++++-- core/src/apps/bitcoin/common.py | 24 ++++++++++++++++ core/src/apps/bitcoin/multisig.py | 21 ++++++++++++++ core/src/apps/bitcoin/scripts.py | 46 ++++++++++++++++++++++++++++-- 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/core/src/apps/bitcoin/addresses.py b/core/src/apps/bitcoin/addresses.py index 6d358b5552..7c82a14469 100644 --- a/core/src/apps/bitcoin/addresses.py +++ b/core/src/apps/bitcoin/addresses.py @@ -7,8 +7,16 @@ from trezor.wire import ProcessError from apps.common import address_type -from .common import ecdsa_hash_pubkey, encode_bech32_address -from .scripts import output_script_native_segwit, write_output_script_multisig +from .common import ( + ecdsa_hash_pubkey, + encode_bech32_address, + p2tr_multisig_tweaked_pubkey, +) +from .multisig import multisig_get_dummy_pubkey +from .scripts import ( + output_script_native_segwit, + write_output_script_multisig, +) if TYPE_CHECKING: from trezor.crypto import bip32 @@ -69,7 +77,11 @@ def get_address( raise ProcessError("Taproot not enabled on this coin") if multisig is not None: - raise ProcessError("Multisig not supported for taproot") + pubkeys = multisig_get_pubkeys(multisig) + dummy_pubkey = multisig_get_dummy_pubkey(multisig) + return _address_multisig_p2tr( + pubkeys, dummy_pubkey, multisig.m, coin.bech32_prefix + ) return _address_p2tr(node_public_key, coin) @@ -116,6 +128,13 @@ def _address_multisig_p2wsh(pubkeys: list[bytes], m: int, hrp: str) -> str: return _address_p2wsh(witness_script_h.get_digest(), hrp) +def _address_multisig_p2tr( + pubkeys: list[bytes], dummy_pubkey: bytes, m: int, hrp: str +) -> str: + _, output_pubkey = p2tr_multisig_tweaked_pubkey(pubkeys, dummy_pubkey, m) + return encode_bech32_address(hrp, 1, output_pubkey) + + def address_pkh(pubkey: bytes, coin: CoinInfo) -> str: s = address_type.tobytes(coin.address_type) + coin.script_hash(pubkey).digest() return base58.encode_check(bytes(s), coin.b58_hash) diff --git a/core/src/apps/bitcoin/common.py b/core/src/apps/bitcoin/common.py index d8426375dc..d1af53d94d 100644 --- a/core/src/apps/bitcoin/common.py +++ b/core/src/apps/bitcoin/common.py @@ -60,6 +60,9 @@ BIP32_WALLET_DEPTH = const(2) # Bitcoin opcodes OP_0 = const(0x00) OP_1 = const(0x51) +OP_CHECKSIG = const(0xAC) +OP_CHECKSIGADD = const(0xBA) +OP_NUMEQUAL = const(0x9C) # supported witness versions for bech32 addresses _BECH32_WITVERS = (0, 1) @@ -103,6 +106,8 @@ NONSEGWIT_INPUT_SCRIPT_TYPES = ( InputScriptType.SPENDMULTISIG, ) +LEAF_VERSION = const(0xC0) + def ecdsa_sign(node: bip32.HDNode, digest: bytes) -> bytes: from trezor.crypto import der @@ -119,6 +124,25 @@ def bip340_sign(node: bip32.HDNode, digest: bytes) -> bytes: return bip340.sign(output_private_key, digest) +def p2tr_multisig_leaf_hash(pubkeys: list[bytes], m: int) -> bytes: + from .scripts import write_output_script_multisig_taproot + + hash_writer = tagged_hashwriter(b"TapLeaf") + hash_writer.append(LEAF_VERSION) + + write_output_script_multisig_taproot(hash_writer, pubkeys, m) + + return hash_writer.get_digest() + + +def p2tr_multisig_tweaked_pubkey( + pubkeys: list[bytes], internal_pubkey: bytes, m: int +) -> tuple[int, bytes]: + leaf_hash = p2tr_multisig_leaf_hash(pubkeys, m) + + return bip340.tweak_public_key(internal_pubkey[1:], leaf_hash) + + def ecdsa_hash_pubkey(pubkey: bytes, coin: CoinInfo) -> bytes: from trezor.utils import ensure diff --git a/core/src/apps/bitcoin/multisig.py b/core/src/apps/bitcoin/multisig.py index 205fccc392..563dbf9ec3 100644 --- a/core/src/apps/bitcoin/multisig.py +++ b/core/src/apps/bitcoin/multisig.py @@ -85,6 +85,27 @@ def multisig_get_pubkey(n: HDNodeType, p: paths.Bip32Path) -> bytes: return node.public_key() +def multisig_get_dummy_pubkey(multisig: MultisigRedeemScriptType) -> bytes: + from trezor.crypto import bip32 + + # The following encodes this xpub into an HDNode. It is the NUMS point suggested + # in BIP341, with a chaincode of 32 0 bytes. Deriving a pubkey from this node + # results in a provably unspendable pubkey. + # https://delvingbitcoin.org/t/unspendable-keys-in-descriptors/304 + # xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6QgnecKFpJFPpdzxKrwoaZoV44qAJewsc4kX9vGaCaBExuvJH57 + node = bip32.HDNode( + depth=0, + fingerprint=2084970077, + child_num=0, + chain_code=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + public_key=b"\x02P\x92\x9bt\xc1\xa0IT\xb7\x8bK`5\xe9z^\x07\x8aZ\x0f(\xec\x96\xd5G\xbf\xee\x9a\xce\x80:\xc0", + ) + + for i in multisig.address_n: + node.derive(i, True) + return node.public_key() + + def multisig_get_pubkeys(multisig: MultisigRedeemScriptType) -> list[bytes]: validate_multisig(multisig) if multisig.nodes: diff --git a/core/src/apps/bitcoin/scripts.py b/core/src/apps/bitcoin/scripts.py index 1a61888997..d6b76bbda4 100644 --- a/core/src/apps/bitcoin/scripts.py +++ b/core/src/apps/bitcoin/scripts.py @@ -9,8 +9,16 @@ from apps.common.readers import read_compact_size from apps.common.writers import write_compact_size from . import common -from .common import SigHashType -from .multisig import multisig_get_pubkeys, multisig_pubkey_index +from .common import ( + SigHashType, + OP_CHECKSIG, + OP_CHECKSIGADD, + OP_NUMEQUAL, +) +from .multisig import ( + multisig_get_pubkeys, + multisig_pubkey_index, +) from .readers import read_memoryview_prefixed, read_op_push from .writers import ( write_bytes_fixed, @@ -591,6 +599,40 @@ def parse_output_script_multisig(script: bytes) -> tuple[list[memoryview], int]: return public_keys, threshold +# Taproot Multisig +# === + + +def write_output_script_multisig_taproot( + w: Writer, + pubkeys: Sequence[bytes | memoryview], + m: int, +) -> None: + n = len(pubkeys) + if n < 1 or n > 15 or m < 1 or m > 15 or m > n: + raise DataError("Invalid multisig parameters") + for pubkey in pubkeys: + if len(pubkey) != 33: + raise DataError("Invalid multisig parameters") + + write_compact_size(w, output_script_multisig_taproot_length(pubkeys)) + + iterator = iter(pubkeys) + append_pubkey(w, next(iterator)[1:]) + w.append(OP_CHECKSIG) + for p in iterator: + append_pubkey(w, p[1:]) + w.append(OP_CHECKSIGADD) + w.append(0x50 + m) # numbers 1 to 16 are pushed as 0x50 + value + w.append(OP_NUMEQUAL) + + +def output_script_multisig_taproot_length(pubkeys: Sequence[bytes | memoryview]) -> int: + return ( + len(pubkeys) * (1 + 32 + 1) + 1 + 1 + ) # see write_output_script_multisig_taproot + + # OP_RETURN # ===