Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MuSig2 support #294

Merged
merged 57 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
f962150
Added parsing for musig(); generalized key placeholders in wallet pol…
bigspider Oct 8, 2024
5efbc36
Rename "key placeholder" with "key expression" where appropriate; add…
bigspider Oct 8, 2024
8e24994
Refactored policy_node_keyexpr_t to explicitly label which of the uni…
bigspider Oct 9, 2024
3dbf464
Add PSBT constants related to MuSig2; deleted unused constant
bigspider Oct 9, 2024
92f1b54
Moved secp256k1 constants to a separate module
bigspider Feb 28, 2024
4e6bae5
Added address generation tests for musig
bigspider Feb 29, 2024
654e1e2
Made crypto_tr_lift_x and crypto_tr_tagged_hash functions public
bigspider Feb 29, 2024
bbde126
Musig key aggregation and address generation
bigspider Feb 29, 2024
7b7af76
Compute aggregate xpub for musig() in descriptors in the python clien…
bigspider Mar 1, 2024
4ebfc25
Add musig2 fields to PSBT class
bigspider Apr 9, 2024
9f48eb8
Added python standalone implementation of MuSig2 signing, and tests
bigspider Apr 17, 2024
7f28011
Add 'tweak' output parameter to bip32_CKDpub; exposed BIP341 constants
bigspider Oct 9, 2024
acc0530
Add parsing of Musig2 pubnonces and partial signatures as yielded val…
bigspider May 17, 2024
3327a8e
MuSig2 signing, rounds 1 and 2
bigspider Oct 9, 2024
41be6f1
Update musig() specs, and fix psbt processing
bigspider Oct 9, 2024
c15335c
Fix psbt-level musig signing session logic
bigspider Jul 15, 2024
b0e02d0
Modularize and extract the musig session handling from sign_psbt.c
bigspider Jul 15, 2024
1b3061b
Persistent storage for musig psbt signing sessions
bigspider May 30, 2024
27c95dc
Add ragger navigation to musig sign_psbt tests
bigspider May 31, 2024
16261f5
Update sanity checks for musig key expressions
bigspider Jul 15, 2024
5b51ab8
Add architecture docs for MuSig2
bigspider Jun 3, 2024
3418b60
Reference musig docs in musig session module
bigspider Jun 3, 2024
8e13101
Add const qualifiers, and asserts guarding against overflows
bigspider Jun 4, 2024
ad377fb
Expose new types in python client
bigspider Jun 4, 2024
38a9e80
Update BIP_MUSIG_CHAINCODE ==> BIP_328_CHAINCODE
bigspider Oct 9, 2024
aea89fd
Fix read_change_and_index_from_psbt_bip32_derivation incorrectly abor…
bigspider Nov 4, 2024
13abab8
Removed unused argument; deleted commented out check
bigspider Nov 6, 2024
5fcdcdf
Generalized count_internal_keys in the test suite to count_internal_k…
bigspider Nov 5, 2024
1192d10
Support BIP-389 multipath descriptors in get_descriptor
bigspider Nov 5, 2024
6c8fb7e
Updated e2e tests to use deterministic xprivs in bitcoin-core
bigspider Nov 5, 2024
4189823
Refactor code of is_policy_sane for clarity; improved comments
bigspider Nov 6, 2024
712bfa7
[WIP] Musig2 e2e tests
bigspider Nov 5, 2024
34f2746
[CI] Use custom image for bitcoin from achow101's branch with MuSig2 …
bigspider Nov 7, 2024
ce066d7
Reduce maximum supported number of keys in musig to 5
bigspider Nov 12, 2024
ac4a48c
Move sign_psbt_cache to global space to reduce stack usage
bigspider Nov 18, 2024
9645d25
Add array of all internal key expressions in sign_psbt_state_t
bigspider Nov 18, 2024
6184d44
Refactor input_keys_callback output_keys_callback to match against al…
bigspider Nov 18, 2024
c5a3b92
Added test for incomplete matching of BIP32 derivation paths in polic…
bigspider Nov 19, 2024
1d25a27
Test that only paths for which a key is present are indeed signed
bigspider Nov 19, 2024
6d858b4
Fixup: musig e2e tests
bigspider Nov 19, 2024
3f556d8
Detect if the PSBT has at least a PSBT_IN_MUSIG2_PUB_NONCE field
bigspider Nov 20, 2024
b6ce98c
Moved MuSig2 Round 1 out of the signing flow. Allow it without user c…
bigspider Nov 21, 2024
392b942
Only compute the aggregate key once for each key expression
bigspider Nov 21, 2024
752aaa8
Fix wrong documentation for get_extended_pubkey; renamed to get_exten…
bigspider Nov 21, 2024
e6b07ce
Refactor most of the MuSig2-related code out of sign_psbt.c
bigspider Nov 21, 2024
568153f
Use qsort for sorting
bigspider Nov 29, 2024
715b2be
Update protocol documentation for the YIELD client command of sign_psbt
bigspider Nov 29, 2024
bd286b3
Nits and code comment improvements
bigspider Nov 29, 2024
b551ed4
Add an explicit initializer for the musigsession signing state.
bigspider Nov 29, 2024
4a2a3c5
Update ragger snapshots for NanoS+ and NanoX
bigspider Nov 29, 2024
08a2ac0
Cleanup some tests; remove debug prints
bigspider Nov 29, 2024
44646bc
Add missing python test dependencies
bigspider Nov 29, 2024
5ffb64d
Simplify CI: only e2e tests need a custom image compiled with bitcoin…
bigspider Dec 2, 2024
f2b0f90
Bump version to 2.4.0-rc; add changelog entry for release 2.4.0
bigspider Dec 2, 2024
6f50e7a
Add note about nonce reuse in the implementation of compute_rand_i_j
bigspider Dec 3, 2024
6489700
Nits from PR review
bigspider Dec 9, 2024
2d22ebe
Apply suggestions from code review
bigspider Dec 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/build_and_functional_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1
with:
download_app_binaries_artifact: "compiled_app_binaries"

container_image: "ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin-musig2:latest"
8 changes: 4 additions & 4 deletions .github/workflows/ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ jobs:
runs-on: ubuntu-latest

container:
image: ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin:latest
image: ghcr.io/ledgerhq/speculos:latest
ports:
- 1234:1234
- 9999:9999
Expand Down Expand Up @@ -168,7 +168,7 @@ jobs:
runs-on: ubuntu-latest

container:
image: ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin:latest
image: ghcr.io/ledgerhq/speculos:latest
ports:
- 1234:1234
- 9999:9999
Expand All @@ -195,7 +195,7 @@ jobs:
runs-on: ubuntu-latest

container:
image: ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin:latest
image: ghcr.io/ledgerhq/speculos:latest
ports:
- 1234:1234
- 9999:9999
Expand Down Expand Up @@ -232,7 +232,7 @@ jobs:
runs-on: ubuntu-latest

container:
image: ghcr.io/ledgerhq/app-bitcoin-new/speculos-bitcoin:latest
image: ghcr.io/ledgerhq/speculos:latest
ports:
- 1234:1234
- 9999:9999
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Dates are in `dd-mm-yyyy` format.

## [2.4.0] - TBD

### Added

- Support for `musig()` key expressions in taproot wallet policies.

### Changed

- For wallet policies with multiple internal spending paths, the app will only sign for key expressions for which the corresponding `PSBT_IN_BIP32_DERIVATION` or `PSBT_IN_TAP_BIP32_DERIVATION` is present in the PSBT. This improves performance when signing for certain spending paths is not desired.

## [2.3.0] - 26-08-2024

### Added
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ PATH_SLIP21_APP_LOAD_PARAMS = "LEDGER-Wallet policy"

# Application version
APPVERSION_M = 2
APPVERSION_N = 3
APPVERSION_N = 4
APPVERSION_P = 0
APPVERSION_SUFFIX = # if not empty, appended at the end. Do not add a dash.
APPVERSION_SUFFIX = rc # if not empty, appended at the end. Do not add a dash.

ifeq ($(APPVERSION_SUFFIX),)
APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"
Expand Down
5 changes: 4 additions & 1 deletion bitcoin_client/ledger_bitcoin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

"""Ledger Nano Bitcoin app client"""

from .client_base import Client, TransportClient, PartialSignature
from .client_base import Client, TransportClient, PartialSignature, MusigPubNonce, MusigPartialSignature, SignPsbtYieldedObject
from .client import createClient
from .common import Chain

Expand All @@ -13,6 +13,9 @@
"Client",
"TransportClient",
"PartialSignature",
"MusigPubNonce",
"MusigPartialSignature",
"SignPsbtYieldedObject",
"createClient",
"Chain",
"AddressType",
Expand Down
177 changes: 177 additions & 0 deletions bitcoin_client/ledger_bitcoin/bip0327.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# extracted from the BIP327 reference implementation: https://github.com/bitcoin/bips/blob/b3701faef2bdb98a0d7ace4eedbeefa2da4c89ed/bip-0327/reference.py

# Only contains the key aggregation part of the library

# The code in this source file is distributed under the BSD-3-Clause.

# autopep8: off

from typing import List, Optional, Tuple, NewType, NamedTuple
import hashlib

#
# The following helper functions were copied from the BIP-340 reference implementation:
# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py
#

p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

# Points are tuples of X and Y coordinates and the point at infinity is
# represented by the None keyword.
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)

Point = Tuple[int, int]

# This implementation can be sped up by storing the midstate after hashing
# tag_hash instead of rehashing it all the time.
def tagged_hash(tag: str, msg: bytes) -> bytes:
tag_hash = hashlib.sha256(tag.encode()).digest()
return hashlib.sha256(tag_hash + tag_hash + msg).digest()

def is_infinite(P: Optional[Point]) -> bool:
return P is None

def x(P: Point) -> int:
assert not is_infinite(P)
return P[0]

def y(P: Point) -> int:
assert not is_infinite(P)
return P[1]

def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]:
if P1 is None:
return P2
if P2 is None:
return P1
if (x(P1) == x(P2)) and (y(P1) != y(P2)):
return None
if P1 == P2:
lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p
else:
lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p
x3 = (lam * lam - x(P1) - x(P2)) % p
return (x3, (lam * (x(P1) - x3) - y(P1)) % p)

def point_mul(P: Optional[Point], n: int) -> Optional[Point]:
R = None
for i in range(256):
if (n >> i) & 1:
R = point_add(R, P)
P = point_add(P, P)
return R

def bytes_from_int(x: int) -> bytes:
return x.to_bytes(32, byteorder="big")

def lift_x(b: bytes) -> Optional[Point]:
x = int_from_bytes(b)
if x >= p:
return None
y_sq = (pow(x, 3, p) + 7) % p
y = pow(y_sq, (p + 1) // 4, p)
if pow(y, 2, p) != y_sq:
return None
return (x, y if y & 1 == 0 else p-y)

def int_from_bytes(b: bytes) -> int:
return int.from_bytes(b, byteorder="big")

def has_even_y(P: Point) -> bool:
assert not is_infinite(P)
return y(P) % 2 == 0

#
# End of helper functions copied from BIP-340 reference implementation.
#

PlainPk = NewType('PlainPk', bytes)
XonlyPk = NewType('XonlyPk', bytes)

# There are two types of exceptions that can be raised by this implementation:
# - ValueError for indicating that an input doesn't conform to some function
# precondition (e.g. an input array is the wrong length, a serialized
# representation doesn't have the correct format).
# - InvalidContributionError for indicating that a signer (or the
# aggregator) is misbehaving in the protocol.
#
# Assertions are used to (1) satisfy the type-checking system, and (2) check for
# inconvenient events that can't happen except with negligible probability (e.g.
# output of a hash function is 0) and can't be manually triggered by any
# signer.

# This exception is raised if a party (signer or nonce aggregator) sends invalid
# values. Actual implementations should not crash when receiving invalid
# contributions. Instead, they should hold the offending party accountable.
class InvalidContributionError(Exception):
def __init__(self, signer, contrib):
bigspider marked this conversation as resolved.
Show resolved Hide resolved
self.signer = signer
# contrib is one of "pubkey", "pubnonce", "aggnonce", or "psig".
self.contrib = contrib

infinity = None

def xbytes(P: Point) -> bytes:
return bytes_from_int(x(P))

def cbytes(P: Point) -> bytes:
a = b'\x02' if has_even_y(P) else b'\x03'
return a + xbytes(P)

def point_negate(P: Optional[Point]) -> Optional[Point]:
if P is None:
return P
return (x(P), p - y(P))

def cpoint(x: bytes) -> Point:
if len(x) != 33:
raise ValueError('x is not a valid compressed point.')
P = lift_x(x[1:33])
if P is None:
raise ValueError('x is not a valid compressed point.')
if x[0] == 2:
return P
elif x[0] == 3:
P = point_negate(P)
assert P is not None
return P
else:
raise ValueError('x is not a valid compressed point.')

KeyAggContext = NamedTuple('KeyAggContext', [('Q', Point),
('gacc', int),
('tacc', int)])

def key_agg(pubkeys: List[PlainPk]) -> KeyAggContext:
pk2 = get_second_key(pubkeys)
u = len(pubkeys)
Q = infinity
for i in range(u):
try:
P_i = cpoint(pubkeys[i])
except ValueError:
raise InvalidContributionError(i, "pubkey")
a_i = key_agg_coeff_internal(pubkeys, pubkeys[i], pk2)
Q = point_add(Q, point_mul(P_i, a_i))
# Q is not the point at infinity except with negligible probability.
assert(Q is not None)
gacc = 1
tacc = 0
return KeyAggContext(Q, gacc, tacc)

def hash_keys(pubkeys: List[PlainPk]) -> bytes:
return tagged_hash('KeyAgg list', b''.join(pubkeys))

def get_second_key(pubkeys: List[PlainPk]) -> PlainPk:
u = len(pubkeys)
for j in range(1, u):
if pubkeys[j] != pubkeys[0]:
return pubkeys[j]
return PlainPk(b'\x00'*33)

def key_agg_coeff_internal(pubkeys: List[PlainPk], pk_: PlainPk, pk2: PlainPk) -> int:
L = hash_keys(pubkeys)
if pk_ == pk2:
return 1
return int_from_bytes(tagged_hash('KeyAgg coefficient', L + pk_)) % n
Loading
Loading