|
| 1 | +# BIP 0327 with BitGo legacy p2tr variant |
| 2 | + |
| 3 | +This directory contains a modified version of the BIP-0327 MuSig2 |
| 4 | +reference implementation by @jonasnick. |
| 5 | + |
| 6 | +The original code was taken from the following file: |
| 7 | +https://github.com/bitcoin/bips/blob/ab9d5b8/bip-0327/reference.py |
| 8 | + |
| 9 | +The modifications add support for an older aggregation method that is |
| 10 | +used at BitGo in a deprecated address type (`p2tr`, chain 30 and 31). |
| 11 | + |
| 12 | +The aggregation method is based on an older version of MuSig2 that predated this PR: |
| 13 | +https://github.com/jonasnick/bips/pull/37 |
| 14 | + |
| 15 | +The recommended address type for taproot at BitGo is `p2trMusig2` (chains 40 and 41), |
| 16 | +which uses the standard MuSig2 aggregation scheme. |
| 17 | + |
| 18 | +## Implementation Differences |
| 19 | + |
| 20 | +### 1. X-Only Pubkey Support |
| 21 | + |
| 22 | +The `key_agg()` function has been enhanced to accept both 33-byte compressed pubkeys and 32-byte x-only pubkeys: |
| 23 | + |
| 24 | +```python |
| 25 | +def key_agg(pubkeys: List[bytes]) -> KeyAggContext: |
| 26 | + for pk in pubkeys: |
| 27 | + if len(pk) != len(pubkeys[0]): |
| 28 | + raise ValueError('all pubkeys must be the same length') |
| 29 | + |
| 30 | + # ... |
| 31 | + for i in range(u): |
| 32 | + # if the pubkey is 32 bytes, it is an xonly pubkey |
| 33 | + if len(pubkeys[i]) == 32: |
| 34 | + P_i = lift_x(pubkeys[i]) |
| 35 | + else: |
| 36 | + P_i = cpoint(pubkeys[i]) |
| 37 | +``` |
| 38 | + |
| 39 | +This allows the implementation to work with both pubkey formats, checking the length to determine the appropriate parsing method. |
| 40 | + |
| 41 | +### 2. Legacy p2tr Aggregation Function |
| 42 | + |
| 43 | +A new function `key_agg_bitgo_p2tr_legacy()` implements the deprecated aggregation method: |
| 44 | + |
| 45 | +```python |
| 46 | +def key_agg_bitgo_p2tr_legacy(pubkeys: List[PlainPk]) -> KeyAggContext: |
| 47 | + # Convert compressed pubkeys to x-only format |
| 48 | + pubkeys = [pk[-32:] for pk in pubkeys] |
| 49 | + |
| 50 | + # Sort keys AFTER x-only conversion |
| 51 | + pubkeys = key_sort(pubkeys) |
| 52 | + |
| 53 | + # Aggregate using standard algorithm |
| 54 | + return key_agg(pubkeys) |
| 55 | +``` |
| 56 | + |
| 57 | +**Key difference**: This method converts pubkeys to x-only format **before** sorting, whereas standard MuSig2 uses full 33-byte compressed keys throughout. This difference stems from the MuSig2 specification change documented in [jonasnick/bips#37](https://github.com/jonasnick/bips/pull/37). |
| 58 | + |
| 59 | +### 3. Enhanced Signing and Verification Functions |
| 60 | + |
| 61 | +Several functions were updated to handle x-only pubkeys properly: |
| 62 | + |
| 63 | +**`get_session_key_agg_coeff()`**: Detects x-only pubkeys and uses appropriate format for coefficient calculation: |
| 64 | + |
| 65 | +```python |
| 66 | +# If pubkeys are x-only, use x-only for coefficient calculation |
| 67 | +if len(pubkeys[0]) == 32: |
| 68 | + pk_for_coeff = pk[-32:] |
| 69 | +else: |
| 70 | + pk_for_coeff = pk |
| 71 | +return key_agg_coeff(pubkeys, pk_for_coeff) |
| 72 | +``` |
| 73 | + |
| 74 | +**`sign()`**: Validates the secnonce against both compressed and x-only pubkey formats: |
| 75 | + |
| 76 | +```python |
| 77 | +if not pk == secnonce[64:97] and not pk[-32:] == secnonce[64:97]: |
| 78 | + raise ValueError('Public key does not match nonce_gen argument') |
| 79 | +``` |
| 80 | + |
| 81 | +**`partial_sig_verify_internal()`**: Handles x-only pubkeys by prepending the appropriate prefix: |
| 82 | + |
| 83 | +```python |
| 84 | +# prepend a 0x02 if the pk is 32 bytes |
| 85 | +P = cpoint(b'\x02' + pk) if len(pk) == 32 else cpoint(pk) |
| 86 | +``` |
| 87 | + |
| 88 | +## Testing Differences |
| 89 | + |
| 90 | +The testing code has been significantly restructured to validate BitGo-specific behavior. |
| 91 | + |
| 92 | +### Refactored Test Helpers |
| 93 | + |
| 94 | +The previous monolithic `test_sign_and_verify_random()` function has been broken down into reusable components: |
| 95 | + |
| 96 | +**`sign_and_verify_with_aggpk()`**: Core signing workflow that: |
| 97 | + |
| 98 | +- Generates nonces for two signers |
| 99 | +- Supports both random nonce generation and deterministic signing |
| 100 | +- Performs partial signature verification |
| 101 | +- Tests nonce reuse protection |
| 102 | +- Verifies the final aggregated signature |
| 103 | + |
| 104 | +**`sign_and_verify_with_keys()`**: Simplified wrapper that generates random tweaks and calls the core signing function. |
| 105 | + |
| 106 | +**`sign_and_verify_with_aggpk_bitgo()`**: BitGo-specific wrapper with no tweaks applied (BitGo doesn't use tweaks in production). |
| 107 | + |
| 108 | +**`sign_and_verify_with_aggpk_bitgo_legacy()`**: Special handler for legacy p2tr that: |
| 109 | + |
| 110 | +- Normalizes secret keys to produce even y-coordinate pubkeys |
| 111 | +- Converts to x-only format |
| 112 | +- Sorts by x-only pubkey order |
| 113 | +- Validates the expected aggregate pubkey |
| 114 | +- Performs full signing workflow |
| 115 | + |
| 116 | +### BitGo-Specific Test Cases |
| 117 | + |
| 118 | +Three new test functions validate BitGo's taproot implementations: |
| 119 | + |
| 120 | +#### `test_agg_bitgo_derive()` |
| 121 | + |
| 122 | +Sanity check that the test fixture private keys correctly derive to their expected public keys. |
| 123 | + |
| 124 | +#### `test_agg_bitgo_p2tr_legacy()` |
| 125 | + |
| 126 | +Tests the legacy p2tr aggregation (chains 30/31): |
| 127 | + |
| 128 | +- Expected aggregate key: `cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa` |
| 129 | +- Verifies order-independence: aggregating `[user, bitgo]` and `[bitgo, user]` produce the same result (due to sorting after x-only conversion) |
| 130 | +- Tests complete signing and verification workflow |
| 131 | + |
| 132 | +#### `test_agg_bitgo_p2tr_musig2()` |
| 133 | + |
| 134 | +Tests the standard MuSig2 aggregation (chains 40/41): |
| 135 | + |
| 136 | +- Expected aggregate key `[user, bitgo]`: `c0e255b4510e041ab81151091d875687a618de314344dff4b73b1bcd366cdbd8` |
| 137 | +- Expected aggregate key `[bitgo, user]`: `e48d309b535811eb0b148c4b0600a10e82e289899429e40aee05577504eca356` |
| 138 | +- Verifies order-dependence: different key orders produce different aggregate keys (standard MuSig2 behavior) |
| 139 | +- Tests both orderings with complete signing workflows |
| 140 | + |
| 141 | +### Shared Test Fixtures |
| 142 | + |
| 143 | +All BitGo tests use consistent keypairs: |
| 144 | + |
| 145 | +```python |
| 146 | +# Private keys from test fixtures |
| 147 | +privkey_user = bytes.fromhex("a07e682489dad68834f7df8a5c8b34f3b9ff9fdd8809e2ba53ae29df65fc146b") |
| 148 | +privkey_bitgo = bytes.fromhex("2d210ff6703d0fae0e9ca91e1d0bbab006b03e8e699f49becbaf554066fa79aa") |
| 149 | + |
| 150 | +# Corresponding public keys |
| 151 | +pubkey_user = PlainPk(bytes.fromhex("02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7")) |
| 152 | +pubkey_bitgo = PlainPk(bytes.fromhex("03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64")) |
| 153 | +``` |
| 154 | + |
| 155 | +**Important note**: These pubkeys have different sort orders depending on whether comparison is done on the full 33-byte compressed format or the 32-byte x-only format. This is precisely why the legacy and standard methods produce different aggregate keys. |
| 156 | + |
| 157 | +## Running Tests |
| 158 | + |
| 159 | +Execute all tests including BitGo-specific ones: |
| 160 | + |
| 161 | +```bash |
| 162 | +cd modules/utxo-lib/bip-0327 |
| 163 | +python3 reference.py |
| 164 | +``` |
| 165 | + |
| 166 | +The test suite runs: |
| 167 | + |
| 168 | +1. Standard BIP327 test vectors (key sorting, aggregation, nonces, signing, tweaks, deterministic signing, signature aggregation) |
| 169 | +2. Random signing/verification tests (6 iterations) |
| 170 | +3. BitGo derivation tests |
| 171 | +4. BitGo legacy p2tr tests |
| 172 | +5. BitGo standard p2trMusig2 tests |
| 173 | + |
| 174 | +## References |
| 175 | + |
| 176 | +- [BIP327 Specification](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) |
| 177 | +- [BIP340 Schnorr Signatures](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) |
| 178 | +- [MuSig2 32-byte to 33-byte key change](https://github.com/jonasnick/bips/pull/37) |
| 179 | +- [Original BIP327 reference implementation](https://github.com/bitcoin/bips/blob/ab9d5b8/bip-0327/reference.py) |
0 commit comments