Skip to content

Commit ec2a844

Browse files
committed
ledger: sign MuSig2
This adds support handling public nonces and partial signatures.
1 parent 5d52156 commit ec2a844

File tree

4 files changed

+125
-28
lines changed

4 files changed

+125
-28
lines changed

hwilib/devices/ledger.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
LegacyClient,
4141
TransportClient,
4242
)
43-
from .ledger_bitcoin.client_base import ApduException
43+
from .ledger_bitcoin.client_base import ApduException, MusigPubNonce, MusigPartialSignature
4444
from .ledger_bitcoin.exception import NotSupportedError
4545
from .ledger_bitcoin.wallet import (
4646
MultisigWallet,
@@ -372,31 +372,54 @@ def process_origin(origin: KeyOriginInfo) -> None:
372372
if not is_wit:
373373
psbt_in.witness_utxo = None
374374

375-
input_sigs = self.client.sign_psbt(psbt2, wallet, wallet_hmac)
375+
res = self.client.sign_psbt(psbt2, wallet, wallet_hmac)
376376

377-
for idx, yielded in input_sigs:
377+
for idx, yielded in res:
378378
psbt_in = psbt2.inputs[idx]
379379

380-
utxo = None
381-
if psbt_in.witness_utxo:
382-
utxo = psbt_in.witness_utxo
383-
if psbt_in.non_witness_utxo:
384-
assert psbt_in.prev_out is not None
385-
utxo = psbt_in.non_witness_utxo.vout[psbt_in.prev_out]
386-
assert utxo is not None
380+
if isinstance(yielded, MusigPubNonce):
381+
psbt_key = (
382+
yielded.participant_pubkey,
383+
yielded.aggregate_pubkey,
384+
yielded.tapleaf_hash
385+
)
386+
387+
assert len(yielded.aggregate_pubkey) == 33
387388

388-
is_wit, wit_ver, _ = utxo.is_witness()
389+
psbt_in.musig2_pub_nonces[psbt_key] = yielded.pubnonce
390+
elif isinstance(yielded, MusigPartialSignature):
391+
psbt_key = (
392+
yielded.participant_pubkey,
393+
yielded.aggregate_pubkey,
394+
yielded.tapleaf_hash
395+
)
389396

390-
if is_wit and wit_ver >= 1:
391-
# TODO: Deal with script path signatures
392-
# For now, assume key path signature
393-
psbt_in.tap_key_sig = yielded.signature
397+
psbt_in.musig2_partial_sigs[psbt_key] = yielded.partial_signature
394398
else:
395-
psbt_in.partial_sigs[yielded.pubkey] = yielded.signature
399+
utxo = None
400+
if psbt_in.witness_utxo:
401+
utxo = psbt_in.witness_utxo
402+
if psbt_in.non_witness_utxo:
403+
assert psbt_in.prev_out is not None
404+
utxo = psbt_in.non_witness_utxo.vout[psbt_in.prev_out]
405+
assert utxo is not None
406+
407+
is_wit, wit_ver, _ = utxo.is_witness()
408+
409+
if is_wit and wit_ver >= 1:
410+
if yielded.tapleaf_hash is None:
411+
psbt_in.tap_key_sig = yielded.signature
412+
else:
413+
psbt_in.tap_script_sigs[(yielded.pubkey, yielded.tapleaf_hash)] = yielded.signature
414+
415+
else:
416+
psbt_in.partial_sigs[yielded.pubkey] = yielded.signature
396417

397418
# Extract the sigs from psbt2 and put them into tx
398419
for sig_in, psbt_in in zip(psbt2.inputs, psbt.inputs):
399420
psbt_in.partial_sigs.update(sig_in.partial_sigs)
421+
psbt_in.musig2_pub_nonces.update(sig_in.musig2_pub_nonces)
422+
psbt_in.musig2_partial_sigs.update(sig_in.musig2_partial_sigs)
400423
psbt_in.tap_script_sigs.update(sig_in.tap_script_sigs)
401424
if len(sig_in.tap_key_sig) != 0 and len(psbt_in.tap_key_sig) == 0:
402425
psbt_in.tap_key_sig = sig_in.tap_key_sig

hwilib/devices/ledger_bitcoin/client.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from .command_builder import BitcoinCommandBuilder, BitcoinInsType
66
from ...common import Chain
7-
from .client_command import ClientCommandInterpreter
8-
from .client_base import Client, PartialSignature, SignPsbtYieldedObject, TransportClient
7+
from .client_command import ClientCommandInterpreter, CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, CCMD_YIELD_MUSIG_PUBNONCE_TAG
8+
from .client_base import Client, MusigPartialSignature, MusigPubNonce, PartialSignature, SignPsbtYieldedObject, TransportClient
99
from .client_legacy import LegacyClient
1010
from .errors import UnknownDeviceError
1111
from .exception import DeviceException, NotSupportedError
@@ -51,18 +51,55 @@ def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSign
5151
def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObject]:
5252
res_buffer = BytesIO(res)
5353
input_index_or_tag = read_varint(res_buffer)
54+
if input_index_or_tag == CCMD_YIELD_MUSIG_PUBNONCE_TAG:
55+
input_index = read_varint(res_buffer)
56+
pubnonce = res_buffer.read(66)
57+
participant_pk = res_buffer.read(33)
58+
aggregate_pubkey = res_buffer.read(33)
59+
tapleaf_hash = res_buffer.read()
60+
if len(tapleaf_hash) == 0:
61+
tapleaf_hash = None
62+
63+
return (
64+
input_index,
65+
MusigPubNonce(
66+
participant_pubkey=participant_pk,
67+
aggregate_pubkey=aggregate_pubkey,
68+
tapleaf_hash=tapleaf_hash,
69+
pubnonce=pubnonce
70+
)
71+
)
72+
elif input_index_or_tag == CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG:
73+
input_index = read_varint(res_buffer)
74+
partial_signature = res_buffer.read(32)
75+
participant_pk = res_buffer.read(33)
76+
aggregate_pubkey = res_buffer.read(33)
77+
tapleaf_hash = res_buffer.read()
78+
if len(tapleaf_hash) == 0:
79+
tapleaf_hash = None
80+
81+
return (
82+
input_index,
83+
MusigPartialSignature(
84+
participant_pubkey=participant_pk,
85+
aggregate_pubkey=aggregate_pubkey,
86+
tapleaf_hash=tapleaf_hash,
87+
partial_signature=partial_signature
88+
)
89+
)
90+
else:
91+
# other values follow an encoding without an explicit tag, where the
92+
# first element is the input index. All the signature types are implemented
93+
# by the PartialSignature type (not to be confused with the musig Partial Signature).
94+
input_index = input_index_or_tag
5495

55-
# values follow an encoding without an explicit tag, where the
56-
# first element is the input index. All the signature types are implemented
57-
# by the PartialSignature type (not to be confused with the musig Partial Signature).
58-
input_index = input_index_or_tag
96+
pubkey_augm_len = read_uint(res_buffer, 8)
97+
pubkey_augm = res_buffer.read(pubkey_augm_len)
5998

60-
pubkey_augm_len = read_uint(res_buffer, 8)
61-
pubkey_augm = res_buffer.read(pubkey_augm_len)
99+
signature = res_buffer.read()
62100

63-
signature = res_buffer.read()
101+
return((input_index, _make_partial_signature(pubkey_augm, signature)))
64102

65-
return((input_index, _make_partial_signature(pubkey_augm, signature)))
66103

67104
def read_uint(buf: BytesIO,
68105
bit_len: int,

hwilib/devices/ledger_bitcoin/client_base.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,42 @@ class PartialSignature:
6565
tapleaf_hash: Optional[bytes] = None
6666

6767

68-
SignPsbtYieldedObject = Union[PartialSignature]
68+
@dataclass(frozen=True)
69+
class MusigPubNonce:
70+
"""Represents a pubnonce returned by sign_psbt during the first round of a Musig2 signing session.
71+
72+
It always contains
73+
- the participant_pubkey, a 33-byte compressed pubkey;
74+
- aggregate_pubkey, the 33-byte compressed pubkey key that is the aggregate of all the participant
75+
pubkeys, with the necessary tweaks; its x-only version is the key present in the Script;
76+
- the 66-byte pubnonce.
77+
78+
The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise.
79+
"""
80+
participant_pubkey: bytes
81+
aggregate_pubkey: bytes
82+
tapleaf_hash: Optional[bytes]
83+
pubnonce: bytes
84+
85+
86+
@dataclass(frozen=True)
87+
class MusigPartialSignature:
88+
"""Represents a partial signature returned by sign_psbt during the second round of a Musig2 signing session.
89+
90+
It always contains
91+
- the participant_pubkey, a 33-byte compressed pubkey;
92+
- aggregate_pubkey, the 33-byte compressed pubkey key that is the aggregate of all the participant
93+
pubkeys, with the necessary tweaks; its x-only version is the key present in the Script;
94+
- the partial_signature, the 32-byte partial signature for this participant.
95+
96+
The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise
97+
"""
98+
participant_pubkey: bytes
99+
aggregate_pubkey: bytes
100+
tapleaf_hash: Optional[bytes]
101+
partial_signature: bytes
102+
103+
SignPsbtYieldedObject = Union[PartialSignature, MusigPubNonce, MusigPartialSignature]
69104

70105
class Client:
71106
def __init__(self, transport_client: TransportClient, chain: Chain = Chain.MAIN) -> None:

hwilib/devices/ledger_bitcoin/client_command.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class ClientCommandCode(IntEnum):
4646
GET_MERKLE_LEAF_INDEX = 0x42
4747
GET_MORE_ELEMENTS = 0xA0
4848

49+
CCMD_YIELD_MUSIG_PUBNONCE_TAG = 0xFFFFFFFF
50+
CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG = 0xFFFFFFFE
4951

5052
class ClientCommand:
5153
def execute(self, request: bytes) -> bytes:
@@ -350,7 +352,7 @@ def add_known_mapping(self, mapping: Mapping[bytes, bytes]) -> None:
350352
of a mapping of bytes to bytes.
351353
352354
Adds the Merkle tree of the list of keys, and the Merkle tree of the list of corresponding
353-
values, with the same semantics as the `add_known_list` applied separately to the two lists.
355+
values, with the same semantics as the `add_known_list` applied separately to the two lists.
354356
355357
Parameters
356358
----------

0 commit comments

Comments
 (0)