Skip to content

Commit 14e9116

Browse files
committed
feat(BTC): add taproot multisig input signing
1 parent 5e25df3 commit 14e9116

File tree

7 files changed

+104
-16
lines changed

7 files changed

+104
-16
lines changed

core/src/apps/bitcoin/common.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ def from_int(cls, sighash_type: int) -> "SigHashType":
7070
InputScriptType.SPENDMULTISIG,
7171
InputScriptType.SPENDP2SHWITNESS,
7272
InputScriptType.SPENDWITNESS,
73+
InputScriptType.SPENDTAPROOT,
7374
)
7475
MULTISIG_OUTPUT_SCRIPT_TYPES = (
7576
OutputScriptType.PAYTOMULTISIG,
7677
OutputScriptType.PAYTOP2SHWITNESS,
7778
OutputScriptType.PAYTOWITNESS,
79+
OutputScriptType.PAYTOTAPROOT,
7880
)
7981

8082
CHANGE_OUTPUT_TO_INPUT_SCRIPT_TYPES: dict[OutputScriptType, InputScriptType] = {
@@ -120,9 +122,11 @@ def ecdsa_sign(node: bip32.HDNode, digest: bytes) -> bytes:
120122
return sigder
121123

122124

123-
def bip340_sign(node: bip32.HDNode, digest: bytes) -> bytes:
125+
def bip340_sign(node: bip32.HDNode, digest: bytes, tweak: bool = True) -> bytes:
124126
internal_private_key = node.private_key()
125-
output_private_key = bip340.tweak_secret_key(internal_private_key)
127+
output_private_key = (
128+
bip340.tweak_secret_key(internal_private_key) if tweak else internal_private_key
129+
)
126130
return bip340.sign(output_private_key, digest)
127131

128132

core/src/apps/bitcoin/scripts.py

+43
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
OP_CHECKSIG,
1515
OP_CHECKSIGADD,
1616
OP_NUMEQUAL,
17+
GENERATOR,
18+
LEAF_VERSION,
19+
p2tr_multisig_tweaked_pubkey,
1720
)
1821
from .multisig import multisig_get_pubkeys, multisig_pubkey_index
1922
from .readers import read_memoryview_prefixed, read_op_push
@@ -600,6 +603,46 @@ def parse_output_script_multisig(script: bytes) -> tuple[list[memoryview], int]:
600603
# ===
601604

602605

606+
def write_witness_multisig_taproot(
607+
w: Writer,
608+
multisig: MultisigRedeemScriptType,
609+
signature: bytes,
610+
signature_index: int,
611+
sighash_type: SigHashType,
612+
) -> None:
613+
from .multisig import multisig_get_pubkey_count
614+
615+
# get other signatures, stretch with zero byte to the number of the pubkeys
616+
signatures = multisig.signatures + [0x00] * (
617+
multisig_get_pubkey_count(multisig) - len(multisig.signatures)
618+
)
619+
620+
# fill in our signature
621+
if signatures[signature_index] != 0x00:
622+
raise DataError("Invalid multisig parameters")
623+
signatures[signature_index] = signature
624+
625+
# signatures + redeem script + control block
626+
num_of_witness_items = len(signatures) + 1 + 1
627+
write_compact_size(w, num_of_witness_items)
628+
629+
for s in signatures:
630+
if s != 0x00:
631+
write_signature_prefixed(w, s, sighash_type) # size of the witness included
632+
else:
633+
w.append(0x00)
634+
635+
# redeem script
636+
pubkeys = multisig_get_pubkeys(multisig)
637+
write_output_script_multisig_taproot(w, pubkeys, multisig.m)
638+
639+
# control block
640+
write_compact_size(w, len(GENERATOR) + 1)
641+
parity, _ = p2tr_multisig_tweaked_pubkey(pubkeys, multisig.m)
642+
w.append(LEAF_VERSION + parity)
643+
w.extend(GENERATOR)
644+
645+
603646
def write_output_script_multisig_taproot(
604647
w: Writer,
605648
pubkeys: Sequence[bytes | memoryview],

core/src/apps/bitcoin/sign_tx/bitcoin.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from apps.common.writers import write_compact_size
1111

1212
from .. import addresses, common, multisig, scripts, writers
13-
from ..common import SigHashType, ecdsa_sign, input_is_external
13+
from ..common import SigHashType, ecdsa_sign, input_is_external, p2tr_multisig_leaf_hash
1414
from ..ownership import verify_nonownership
1515
from ..verification import SignatureVerifier
1616
from . import helpers
@@ -642,14 +642,22 @@ def sign_bip143_input(self, i: int, txi: TxInput) -> tuple[bytes, bytes]:
642642
def sign_taproot_input(self, i: int, txi: TxInput) -> bytes:
643643
from ..common import bip340_sign
644644

645+
if txi.multisig:
646+
public_keys = multisig.multisig_get_pubkeys(txi.multisig)
647+
threshold = txi.multisig.m
648+
leaf_hash = p2tr_multisig_leaf_hash(public_keys, threshold)
649+
else:
650+
leaf_hash = None
651+
645652
sigmsg_digest = self.tx_info.sig_hasher.hash341(
646-
i,
647-
self.tx_info.tx,
648-
self.get_sighash_type(txi),
653+
i, self.tx_info.tx, self.get_sighash_type(txi), leaf_hash
649654
)
650655

651656
node = self.keychain.derive(txi.address_n)
652-
return bip340_sign(node, sigmsg_digest)
657+
public_key = node.public_key()
658+
signature = bip340_sign(node, sigmsg_digest, not txi.multisig)
659+
660+
return public_key, signature
653661

654662
async def sign_segwit_input(self, i: int) -> None:
655663
# STAGE_REQUEST_SEGWIT_WITNESS in legacy
@@ -660,11 +668,24 @@ async def sign_segwit_input(self, i: int) -> None:
660668
raise ProcessError("Transaction has changed during signing")
661669

662670
if txi.script_type == InputScriptType.SPENDTAPROOT:
663-
signature = self.sign_taproot_input(i, txi)
671+
public_key, signature = self.sign_taproot_input(i, txi)
664672
if self.serialize:
665-
scripts.write_witness_p2tr(
666-
self.serialized_tx, signature, self.get_sighash_type(txi)
667-
)
673+
if txi.multisig:
674+
# find out place of our signature based on the pubkey
675+
signature_index = multisig.multisig_pubkey_index(
676+
txi.multisig, public_key
677+
)
678+
scripts.write_witness_multisig_taproot(
679+
self.serialized_tx,
680+
txi.multisig,
681+
signature,
682+
signature_index,
683+
self.get_sighash_type(txi),
684+
)
685+
else:
686+
scripts.write_witness_p2tr(
687+
self.serialized_tx, signature, self.get_sighash_type(txi)
688+
)
668689
else:
669690
public_key, signature = self.sign_bip143_input(i, txi)
670691
if self.serialize:

core/src/apps/bitcoin/sign_tx/sig_hasher.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def hash341(
3737
i: int,
3838
tx: SignTx | PrevTx,
3939
sighash_type: SigHashType,
40+
leaf_hash: bytes | None,
4041
) -> bytes: ...
4142

4243
def hash_zip244(
@@ -132,9 +133,10 @@ def hash341(
132133
i: int,
133134
tx: SignTx | PrevTx,
134135
sighash_type: SigHashType,
136+
leaf_hash: bytes | None,
135137
) -> bytes:
136138
from ..common import tagged_hashwriter
137-
from ..writers import write_uint8
139+
from ..writers import write_uint8, write_uint32
138140

139141
h_sigmsg = tagged_hashwriter(b"TapSighash")
140142

@@ -165,12 +167,18 @@ def hash341(
165167
# sha_outputs
166168
write_bytes_fixed(h_sigmsg, self.h_outputs.get_digest(), TX_HASH_SIZE)
167169

168-
# spend_type 0 (no tapscript message extension, no annex)
169-
write_uint8(h_sigmsg, 0)
170+
# spend_type, no annex support for now
171+
spend_type = 0 if leaf_hash is None else 2
172+
write_uint8(h_sigmsg, spend_type)
170173

171174
# input_index
172175
write_uint32(h_sigmsg, i)
173176

177+
if leaf_hash is not None:
178+
write_bytes_fixed(h_sigmsg, leaf_hash, TX_HASH_SIZE)
179+
write_uint8(h_sigmsg, 0)
180+
write_uint32(h_sigmsg, 0xFFFFFFFF)
181+
174182
return h_sigmsg.get_digest()
175183

176184
def hash_zip244(

core/src/apps/bitcoin/sign_tx/tx_weight.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
_TXSIZE_SCHNORR_SIGNATURE = const(64)
3636
# size of a multiscript without pubkey (1 M, 1 N, 1 checksig)
3737
_TXSIZE_MULTISIGSCRIPT = const(3)
38+
# size of a taproot multiscript without pubkey (1 M, 1 numequal, 1 + 33 control block)
39+
_TXSIZE_MULTISIGSCRIPT_TAPROOT = const(36)
3840
# size of a p2wpkh script (1 version, 1 push, 20 hash)
3941
_TXSIZE_WITNESSPKHASH = const(22)
4042
# size of a p2wsh script (1 version, 1 push, 32 hash)
@@ -72,10 +74,18 @@ def input_script_size(cls, i: TxInput) -> int:
7274
pass
7375

7476
if multisig:
77+
n = len(multisig.nodes) if multisig.nodes else len(multisig.pubkeys)
7578
if script_type == IST.SPENDTAPROOT:
76-
raise wire.ProcessError("Multisig not supported for taproot")
79+
multisig_script_size = _TXSIZE_MULTISIGSCRIPT_TAPROOT + n * (
80+
1 + 1 + _TXSIZE_PUBKEY
81+
)
82+
multisig_script_size += cls.compact_size_len(multisig_script_size)
83+
return (
84+
multisig_script_size
85+
+ multisig.m * (1 + _TXSIZE_SCHNORR_SIGNATURE)
86+
+ (n - multisig.m)
87+
)
7788

78-
n = len(multisig.nodes) if multisig.nodes else len(multisig.pubkeys)
7989
multisig_script_size = _TXSIZE_MULTISIGSCRIPT + n * (1 + _TXSIZE_PUBKEY)
8090
if script_type in common.SEGWIT_INPUT_SCRIPT_TYPES:
8191
multisig_script_size += cls.compact_size_len(multisig_script_size)

core/src/apps/bitcoin/sign_tx/zcash_v4.py

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def hash341(
107107
i: int,
108108
tx: SignTx | PrevTx,
109109
sighash_type: SigHashType,
110+
leaf_hash: bytes | None,
110111
) -> bytes:
111112
raise NotImplementedError
112113

core/src/apps/zcash/hasher.py

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def hash341(
118118
i: int,
119119
tx: SignTx | PrevTx,
120120
sighash_type: SigHashType,
121+
leaf_hash: bytes | None,
121122
) -> bytes:
122123
raise NotImplementedError
123124

0 commit comments

Comments
 (0)