1- // Taproot-specific key aggregation and taptree logic as defined in:
2- // https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
3- // https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
1+ /**
2+ * This module provides taproot utilities including the legacy MuSig2 key aggregation
3+ * algorithm used by BitGo's deprecated `p2tr` script type (chains 30, 31).
4+ *
5+ * ## Legacy p2tr vs Standard p2trMusig2
6+ *
7+ * BitGo supports two taproot address types:
8+ *
9+ * 1. **Legacy `p2tr` (chains 30, 31) - DEPRECATED**
10+ * - Uses the `aggregateMuSigPubkeys()` function in this module
11+ * - Based on an older MuSig2 variant that predates BIP327
12+ * - Expects 32-byte x-only pubkeys with even Y coordinates
13+ * - Sorts keys AFTER x-only conversion
14+ * - Corresponds to MuSig2 before the 32-byte to 33-byte key change
15+ * - See: https://github.com/jonasnick/bips/pull/37
16+ *
17+ * 2. **Standard `p2trMusig2` (chains 40, 41) - RECOMMENDED**
18+ * - Uses the `@brandonblack/musig` library (standard BIP327 implementation)
19+ * - Uses full 33-byte compressed pubkeys throughout aggregation
20+ * - Key order affects the resulting aggregate key
21+ * - Fully compatible with the BIP327 specification
22+ *
23+ * ## Key Difference
24+ *
25+ * The critical difference is **when x-only conversion happens**:
26+ * - Legacy: Converts to x-only BEFORE sorting (produces order-independent keys)
27+ * - Standard: Uses 33-byte keys throughout (key order matters)
28+ *
29+ * For the same two pubkeys, these methods produce DIFFERENT aggregate keys because
30+ * the sort order differs between 33-byte and 32-byte representations.
31+ *
32+ * See `modules/utxo-lib/bip-0327/README.md` for detailed comparison and test cases.
33+ */
434
535import { TapTree as PsbtTapTree , TapLeaf as PsbtTapLeaf } from 'bip174/src/lib/interfaces' ;
636import assert = require( 'assert' ) ;
@@ -27,59 +57,114 @@ export interface TinySecp256k1Interface {
2757}
2858
2959/**
30- * Aggregates a list of public keys into a single MuSig2* public key
31- * according to the MuSig2 paper.
60+ * Aggregates a list of public keys into a single public key using the legacy MuSig2 algorithm.
61+ *
62+ * This implements the deprecated key aggregation method used by BitGo's `p2tr` script type
63+ * (chains 30, 31). It corresponds to an older variant of MuSig2 that predates the change
64+ * from 32-byte to 33-byte keys in the BIP327 specification.
65+ *
66+ * ## Algorithm
67+ *
68+ * The implementation follows the MuSig2 key aggregation scheme:
69+ *
70+ * ```
71+ * P = sum_i (μ_i * P_i)
72+ * ```
73+ *
74+ * where:
75+ * - `P_i` is the public key of the i-th signer
76+ * - `μ_i` is the MuSig coefficient computed as:
77+ * - `L = TaggedHash("KeyAgg list", P_1 || P_2 || ... || P_n)`
78+ * - `μ_i = TaggedHash("KeyAgg coefficient", L || P_i)` for most keys
79+ * - `μ_i = 1` for the second unique key (optimization to save an exponentiation)
80+ *
81+ * ## Key Characteristics (Legacy Variant)
82+ *
83+ * 1. **X-only pubkeys**: Expects 32-byte x-only pubkeys (assumes even Y coordinates)
84+ * 2. **Pre-aggregation sorting**: Sorts keys in ascending order BEFORE aggregation
85+ * 3. **Order-independent**: Due to sorting, key order doesn't affect the result
86+ * 4. **33-byte internal representation**: Internally prepends 0x02 prefix for elliptic curve operations
87+ *
88+ * ## Differences from Standard MuSig2 (BIP327)
89+ *
90+ * The standard MuSig2 implementation (`@brandonblack/musig` library):
91+ * - Uses 33-byte compressed pubkeys throughout
92+ * - Does NOT sort keys before aggregation (key order matters)
93+ * - Produces different aggregate keys even for the same set of pubkeys
94+ *
95+ * ## Reference Implementation
96+ *
97+ * This corresponds to `key_agg_bitgo_p2tr_legacy()` in the Python reference implementation
98+ * at `modules/utxo-lib/bip-0327/reference.py`.
99+ *
32100 * @param ecc Elliptic curve implementation
33- * @param pubkeys The list of pub keys to aggregate
34- * @returns a 32 byte Buffer representing the aggregate key
101+ * @param pubkeys List of 32-byte x-only public keys to aggregate (must have even Y coordinates)
102+ * @returns 32-byte Buffer representing the x-only aggregate public key
103+ * @throws {Error } if fewer than 2 pubkeys provided or elliptic curve operations fail
104+ *
105+ * @see modules/utxo-lib/bip-0327/README.md for detailed comparison with standard MuSig2
106+ * @see https://github.com/jonasnick/bips/pull/37 for the MuSig2 specification change
35107 */
36108export function aggregateMuSigPubkeys ( ecc : TinySecp256k1Interface , pubkeys : Buffer [ ] ) : Uint8Array {
37109 // TODO: Consider enforcing key uniqueness.
38110 assert ( pubkeys . length > 1 , 'at least two pubkeys are required for musig key aggregation' ) ;
39111
40- // Sort the keys in ascending order
112+ // LEGACY BEHAVIOR: Sort the keys in ascending order BEFORE aggregation.
113+ // This makes the aggregate key independent of input order.
114+ // Standard MuSig2 (BIP327) does NOT sort, making key order significant.
41115 pubkeys . sort ( Buffer . compare ) ;
42116
43- // In MuSig all signers contribute key material to a single signing key,
44- // using the equation
45- //
46- // P = sum_i µ_i * P_i
47- //
48- // where `P_i` is the public key of the `i`th signer and `µ_i` is a so-called
49- // _MuSig coefficient_ computed according to the following equation
50- //
51- // L = H(P_1 || P_2 || ... || P_n)
52- // µ_i = H(L || P_i)
53-
117+ // Compute the "KeyAgg list" hash L = TaggedHash("KeyAgg list", P_1 || P_2 || ... || P_n)
118+ // This hash is used to derive the MuSig coefficients for each pubkey.
119+ // In the reference implementation (reference.py), this is done in hash_keys().
54120 const L = bcrypto . taggedHash ( 'KeyAgg list' , Buffer . concat ( pubkeys ) ) ;
55121
122+ // Find the second unique pubkey in the sorted list.
123+ // This key (and any keys identical to it) will use coefficient μ = 1 as an optimization.
124+ // This saves an expensive point multiplication (see MuSig2* appendix in the MuSig2 paper).
125+ // In the reference implementation, this is get_second_key().
56126 const secondUniquePubkey = pubkeys . find ( ( pubkey ) => ! pubkeys [ 0 ] . equals ( pubkey ) ) ;
57127
128+ // For each pubkey P_i, compute the tweaked pubkey μ_i * P_i
129+ // where μ_i is the MuSig coefficient for that key.
58130 const tweakedPubkeys : Uint8Array [ ] = pubkeys . map ( ( pubkey ) => {
131+ // Convert 32-byte x-only pubkey to 33-byte compressed format by prepending 0x02.
132+ // This assumes an even Y coordinate (standard for x-only pubkeys in taproot).
133+ // In the reference implementation, this is done implicitly in key_agg() via lift_x().
59134 const xyPubkey = Buffer . concat ( [ EVEN_Y_COORD_PREFIX , pubkey ] ) ;
60135
61136 if ( secondUniquePubkey !== undefined && secondUniquePubkey . equals ( pubkey ) ) {
62- // The second unique key in the pubkey list given to ''KeyAgg'' (as well
63- // as any keys identical to this key) gets the constant KeyAgg
64- // coefficient 1 which saves an exponentiation (see the MuSig2* appendix
65- // in the MuSig2 paper) .
137+ // Optimization: The second unique key gets coefficient μ = 1.
138+ // This means μ_i * P_i = 1 * P_i = P_i (no multiplication needed).
139+ // This saves an expensive elliptic curve point multiplication operation.
140+ // See key_agg_coeff_internal() in reference.py where it returns 1 for pk_ == pk2 .
66141 return xyPubkey ;
67142 }
68143
144+ // Compute the MuSig coefficient: μ_i = TaggedHash("KeyAgg coefficient", L || P_i)
145+ // In the reference implementation, this is key_agg_coeff_internal().
69146 const c = bcrypto . taggedHash ( 'KeyAgg coefficient' , Buffer . concat ( [ L , pubkey ] ) ) ;
70147
148+ // Compute the tweaked pubkey: μ_i * P_i (elliptic curve point multiplication)
71149 const tweakedPubkey = ecc . pointMultiply ( xyPubkey , c ) ;
72150 if ( ! tweakedPubkey ) {
73151 throw new Error ( 'Failed to multiply pubkey by coefficient' ) ;
74152 }
75153 return tweakedPubkey ;
76154 } ) ;
155+
156+ // Sum all the tweaked pubkeys to get the aggregate pubkey:
157+ // P = sum_i (μ_i * P_i) = μ_1*P_1 + μ_2*P_2 + ... + μ_n*P_n
158+ // This is elliptic curve point addition.
77159 const aggregatePubkey = tweakedPubkeys . reduce ( ( prev , curr ) => {
78160 const next = ecc . pointAdd ( prev , curr ) ;
79161 if ( ! next ) throw new Error ( 'Failed to sum pubkeys' ) ;
80162 return next ;
81163 } ) ;
82164
165+ // Convert the aggregate pubkey from 33-byte compressed format back to 32-byte x-only format
166+ // by removing the first byte (the Y coordinate prefix).
167+ // In the reference implementation, this is done by get_xonly_pk() which calls xbytes().
83168 return aggregatePubkey . slice ( 1 ) ;
84169}
85170
0 commit comments