Skip to content

Commit a56fdad

Browse files
committed
Sign MuSig
1 parent a1458d9 commit a56fdad

File tree

4 files changed

+121
-25
lines changed

4 files changed

+121
-25
lines changed

hwilib/devices/ledger.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
createClient,
3838
NewClient,
3939
LegacyClient,
40+
MusigPubNonce,
41+
MusigPartialSignature,
4042
TransportClient,
4143
)
4244
from .ledger_bitcoin.client_base import ApduException
@@ -374,25 +376,46 @@ def process_origin(origin: KeyOriginInfo) -> None:
374376
for idx, yielded in input_sigs:
375377
psbt_in = psbt2.inputs[idx]
376378

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

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

387-
if is_wit and wit_ver >= 1:
388-
# TODO: Deal with script path signatures
389-
# For now, assume key path signature
390-
psbt_in.tap_key_sig = yielded.signature
396+
psbt_in.musig2_partial_sigs[psbt_key] = yielded.partial_signature
391397
else:
392-
psbt_in.partial_sigs[yielded.pubkey] = yielded.signature
398+
utxo = None
399+
if psbt_in.witness_utxo:
400+
utxo = psbt_in.witness_utxo
401+
if psbt_in.non_witness_utxo:
402+
assert psbt_in.prev_out is not None
403+
utxo = psbt_in.non_witness_utxo.vout[psbt_in.prev_out]
404+
assert utxo is not None
405+
406+
is_wit, wit_ver, _ = utxo.is_witness()
407+
408+
if is_wit and wit_ver >= 1:
409+
# TODO: Deal with script path signatures
410+
# For now, assume key path signature
411+
psbt_in.tap_key_sig = yielded.signature
412+
else:
413+
psbt_in.partial_sigs[yielded.pubkey] = yielded.signature
393414

394415
# Extract the sigs from psbt2 and put them into tx
395416
for sig_in, psbt_in in zip(psbt2.inputs, tx.inputs):
417+
psbt_in.musig2_pub_nonces.update(sig_in.musig2_pub_nonces)
418+
psbt_in.musig2_partial_sigs.update(sig_in.musig2_partial_sigs)
396419
psbt_in.partial_sigs.update(sig_in.partial_sigs)
397420
psbt_in.tap_script_sigs.update(sig_in.tap_script_sigs)
398421
if len(sig_in.tap_key_sig) != 0 and len(psbt_in.tap_key_sig) == 0:

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, MusigPubNonce, MusigPartialSignature, PartialSignature, SignPsbtYieldedObject, TransportClient
99
from .client_legacy import LegacyClient
1010
from .errors import UnknownDeviceError
1111
from .exception import DeviceException, NotSupportedError
@@ -52,17 +52,54 @@ def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObje
5252
res_buffer = BytesIO(res)
5353
input_index_or_tag = read_varint(res_buffer)
5454

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
55+
if input_index_or_tag == CCMD_YIELD_MUSIG_PUBNONCE_TAG:
56+
input_index = read_varint(res_buffer)
57+
pubnonce = res_buffer.read(66)
58+
participant_pk = res_buffer.read(33)
59+
aggregate_pubkey = res_buffer.read(33)
60+
tapleaf_hash = res_buffer.read()
61+
if len(tapleaf_hash) == 0:
62+
tapleaf_hash = None
63+
64+
return (
65+
input_index,
66+
MusigPubNonce(
67+
participant_pubkey=participant_pk,
68+
aggregate_pubkey=aggregate_pubkey,
69+
tapleaf_hash=tapleaf_hash,
70+
pubnonce=pubnonce
71+
)
72+
)
73+
elif input_index_or_tag == CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG:
74+
input_index = read_varint(res_buffer)
75+
partial_signature = res_buffer.read(32)
76+
participant_pk = res_buffer.read(33)
77+
aggregate_pubkey = res_buffer.read(33)
78+
tapleaf_hash = res_buffer.read()
79+
if len(tapleaf_hash) == 0:
80+
tapleaf_hash = None
81+
82+
return (
83+
input_index,
84+
MusigPartialSignature(
85+
participant_pubkey=participant_pk,
86+
aggregate_pubkey=aggregate_pubkey,
87+
tapleaf_hash=tapleaf_hash,
88+
partial_signature=partial_signature
89+
)
90+
)
91+
else:
92+
# other values follow an encoding without an explicit tag, where the
93+
# first element is the input index. All the signature types are implemented
94+
# by the PartialSignature type (not to be confused with the musig Partial Signature).
95+
input_index = input_index_or_tag
5996

60-
pubkey_augm_len = read_uint(res_buffer, 8)
61-
pubkey_augm = res_buffer.read(pubkey_augm_len)
97+
pubkey_augm_len = read_uint(res_buffer, 8)
98+
pubkey_augm = res_buffer.read(pubkey_augm_len)
6299

63-
signature = res_buffer.read()
100+
signature = res_buffer.read()
64101

65-
return((input_index, _make_partial_signature(pubkey_augm, signature)))
102+
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: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,42 @@ class PartialSignature:
6464
signature: bytes
6565
tapleaf_hash: Optional[bytes] = None
6666

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

68-
SignPsbtYieldedObject = Union[PartialSignature]
101+
SignPsbtYieldedObject = Union[PartialSignature,
102+
MusigPubNonce, MusigPartialSignature]
69103

70104
class Client:
71105
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)