Skip to content

Commit

Permalink
feat(BTC): add taproot multisig input signing
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewtoth committed Sep 6, 2024
1 parent 5e25df3 commit 14e9116
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 16 deletions.
8 changes: 6 additions & 2 deletions core/src/apps/bitcoin/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand Down Expand Up @@ -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)


Expand Down
43 changes: 43 additions & 0 deletions core/src/apps/bitcoin/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand Down
39 changes: 30 additions & 9 deletions core/src/apps/bitcoin/sign_tx/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
14 changes: 11 additions & 3 deletions core/src/apps/bitcoin/sign_tx/sig_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def hash341(
i: int,
tx: SignTx | PrevTx,
sighash_type: SigHashType,
leaf_hash: bytes | None,
) -> bytes: ...

def hash_zip244(
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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(
Expand Down
14 changes: 12 additions & 2 deletions core/src/apps/bitcoin/sign_tx/tx_weight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions core/src/apps/bitcoin/sign_tx/zcash_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def hash341(
i: int,
tx: SignTx | PrevTx,
sighash_type: SigHashType,
leaf_hash: bytes | None,
) -> bytes:
raise NotImplementedError

Expand Down
1 change: 1 addition & 0 deletions core/src/apps/zcash/hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def hash341(
i: int,
tx: SignTx | PrevTx,
sighash_type: SigHashType,
leaf_hash: bytes | None,
) -> bytes:
raise NotImplementedError

Expand Down

0 comments on commit 14e9116

Please sign in to comment.