88//!
99
1010use miniscript:: bitcoin:: CompressedPublicKey ;
11+ use musig2:: KeyAggContext ;
1112
1213use crate :: bitcoin:: hashes:: { sha256, Hash , HashEngine } ;
1314use crate :: bitcoin:: secp256k1:: { Parity , PublicKey , Scalar , Secp256k1 , XOnlyPublicKey } ;
@@ -187,6 +188,50 @@ pub fn key_agg_p2tr_musig2(pubkeys: &[CompressedPublicKey]) -> Result<[u8; 32],
187188 key_agg ( & pubkey_bytes)
188189}
189190
191+ /// P2TR MuSig2 key aggregation using external musig2 crate.
192+ ///
193+ /// This function uses the external `musig2` crate to perform BIP327-compliant
194+ /// key aggregation. It should produce identical results to `key_agg_p2tr_musig2`.
195+ pub fn key_agg_p2tr_musig2_external_crate (
196+ pubkeys : & [ CompressedPublicKey ] ,
197+ ) -> Result < [ u8 ; 32 ] , BitGoMusigError > {
198+ if pubkeys. len ( ) < 2 {
199+ return Err ( BitGoMusigError :: InvalidPubkeyCount (
200+ "At least two pubkeys are required for MuSig key aggregation" . to_string ( ) ,
201+ ) ) ;
202+ }
203+
204+ // Check for duplicate keys
205+ let first = & pubkeys[ 0 ] ;
206+ let has_distinct = pubkeys. iter ( ) . skip ( 1 ) . any ( |pk| pk != first) ;
207+ if !has_distinct {
208+ return Err ( BitGoMusigError :: InvalidPubkeyCount (
209+ "All pubkeys are identical - MuSig requires at least two distinct keys" . to_string ( ) ,
210+ ) ) ;
211+ }
212+
213+ // Convert CompressedPublicKey to musig2::secp256k1::PublicKey
214+ let secp_pubkeys: Result < Vec < musig2:: secp256k1:: PublicKey > , _ > = pubkeys
215+ . iter ( )
216+ . enumerate ( )
217+ . map ( |( i, cpk) | {
218+ musig2:: secp256k1:: PublicKey :: from_slice ( & cpk. to_bytes ( ) ) . map_err ( |e| {
219+ BitGoMusigError :: InvalidPubkey ( format ! ( "Invalid pubkey at index {}: {}" , i, e) )
220+ } )
221+ } )
222+ . collect ( ) ;
223+ let secp_pubkeys = secp_pubkeys?;
224+
225+ // Use musig2 crate for key aggregation
226+ let key_agg_ctx = KeyAggContext :: new ( secp_pubkeys) . map_err ( |e| {
227+ BitGoMusigError :: AggregationFailed ( format ! ( "KeyAggContext creation failed: {}" , e) )
228+ } ) ?;
229+
230+ // Get the aggregated x-only public key
231+ let agg_pk: musig2:: secp256k1:: XOnlyPublicKey = key_agg_ctx. aggregated_pubkey ( ) ;
232+ Ok ( agg_pk. serialize ( ) )
233+ }
234+
190235#[ cfg( test) ]
191236mod tests {
192237 use super :: * ;
@@ -201,63 +246,100 @@ mod tests {
201246 . serialize ( )
202247 }
203248
249+ /// Test keys used across multiple tests
250+ struct TestKeys {
251+ user : CompressedPublicKey ,
252+ bitgo : CompressedPublicKey ,
253+ backup : CompressedPublicKey ,
254+ }
255+
256+ fn get_test_keys ( ) -> TestKeys {
257+ TestKeys {
258+ user : pubkey_from_hex (
259+ "02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7" ,
260+ ) ,
261+ bitgo : pubkey_from_hex (
262+ "03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64" ,
263+ ) ,
264+ backup : pubkey_from_hex (
265+ "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" ,
266+ ) ,
267+ }
268+ }
269+
270+ /// Expected fixtures for key aggregation tests
271+ struct AggregationFixtures {
272+ p2tr_legacy : [ u8 ; 32 ] ,
273+ p2tr_musig2_forward : [ u8 ; 32 ] ,
274+ p2tr_musig2_reverse : [ u8 ; 32 ] ,
275+ }
276+
277+ fn get_aggregation_fixtures ( ) -> AggregationFixtures {
278+ AggregationFixtures {
279+ p2tr_legacy : pubkey_from_hex_xonly (
280+ "cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa" ,
281+ ) ,
282+ p2tr_musig2_forward : pubkey_from_hex_xonly (
283+ "c0e255b4510e041ab81151091d875687a618de314344dff4b73b1bcd366cdbd8" ,
284+ ) ,
285+ p2tr_musig2_reverse : pubkey_from_hex_xonly (
286+ "e48d309b535811eb0b148c4b0600a10e82e289899429e40aee05577504eca356" ,
287+ ) ,
288+ }
289+ }
290+
291+ /// Assert that aggregation result matches expected fixture
292+ fn assert_aggregation ( result : [ u8 ; 32 ] , expected : [ u8 ; 32 ] , msg : & str ) {
293+ assert_eq ! ( result, expected, "{}" , msg) ;
294+ }
295+
204296 #[ test]
205297 fn test_bitgo_p2tr_aggregation ( ) {
206298 // Test matching the Python test_agg_bitgo function
207299 // This is the algorithm used by the bitgo 'p2tr' output script type (chain 30, 31)
208-
209- let pubkey_user =
210- pubkey_from_hex ( "02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7" ) ;
211- let pubkey_bitgo =
212- pubkey_from_hex ( "03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64" ) ;
213- let expected_internal_pubkey_p2tr = pubkey_from_hex_xonly (
214- "cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa" ,
215- ) ;
216- let expected_internal_pubkey_p2tr_musig2 = pubkey_from_hex_xonly (
217- "c0e255b4510e041ab81151091d875687a618de314344dff4b73b1bcd366cdbd8" ,
218- ) ;
219- let expected_internal_pubkey_p2tr_musig2_reverse = pubkey_from_hex_xonly (
220- "e48d309b535811eb0b148c4b0600a10e82e289899429e40aee05577504eca356" ,
221- ) ;
300+ let keys = get_test_keys ( ) ;
301+ let fixtures = get_aggregation_fixtures ( ) ;
222302
223303 // Test 1: bitgo_p2tr_legacy aggregation using xonly conversion + sort
224- let result = key_agg_bitgo_p2tr_legacy ( & [ pubkey_user, pubkey_bitgo] ) . unwrap ( ) ;
225- assert_eq ! (
226- result, expected_internal_pubkey_p2tr,
227- "p2tr legacy aggregation mismatch"
304+ let result = key_agg_bitgo_p2tr_legacy ( & [ keys. user , keys. bitgo ] ) . unwrap ( ) ;
305+ assert_aggregation (
306+ result,
307+ fixtures. p2tr_legacy ,
308+ "p2tr legacy aggregation mismatch" ,
228309 ) ;
229310
230311 // Test 2: bitgo_p2tr_legacy aggregation in reverse order should give same result (because sort=true)
231- let result = key_agg_bitgo_p2tr_legacy ( & [ pubkey_bitgo, pubkey_user] ) . unwrap ( ) ;
232- assert_eq ! (
233- result, expected_internal_pubkey_p2tr,
234- "p2tr legacy aggregation (reverse) mismatch"
312+ let result = key_agg_bitgo_p2tr_legacy ( & [ keys. bitgo , keys. user ] ) . unwrap ( ) ;
313+ assert_aggregation (
314+ result,
315+ fixtures. p2tr_legacy ,
316+ "p2tr legacy aggregation (reverse) mismatch" ,
235317 ) ;
236318
237319 // Test 3: p2tr_musig2 aggregation using standard BIP327
238- let result = key_agg_p2tr_musig2 ( & [ pubkey_user, pubkey_bitgo] ) . unwrap ( ) ;
239- assert_eq ! (
240- result, expected_internal_pubkey_p2tr_musig2,
241- "p2trMusig2 aggregation mismatch"
320+ let result = key_agg_p2tr_musig2 ( & [ keys. user , keys. bitgo ] ) . unwrap ( ) ;
321+ assert_aggregation (
322+ result,
323+ fixtures. p2tr_musig2_forward ,
324+ "p2trMusig2 aggregation mismatch" ,
242325 ) ;
243326
244327 // Test 4: p2tr_musig2 aggregation in reverse order gives different result (because sort=false)
245- let result = key_agg_p2tr_musig2 ( & [ pubkey_bitgo , pubkey_user ] ) . unwrap ( ) ;
246- assert_eq ! (
247- result. to_vec ( ) ,
248- expected_internal_pubkey_p2tr_musig2_reverse ,
249- "p2trMusig2 aggregation (reverse) mismatch"
328+ let result = key_agg_p2tr_musig2 ( & [ keys . bitgo , keys . user ] ) . unwrap ( ) ;
329+ assert_aggregation (
330+ result,
331+ fixtures . p2tr_musig2_reverse ,
332+ "p2trMusig2 aggregation (reverse) mismatch" ,
250333 ) ;
251334 }
252335
253336 #[ test]
254337 fn test_identical_keys_error ( ) {
255338 // Test that aggregating identical keys returns an error
256- let pubkey_user =
257- pubkey_from_hex ( "02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7" ) ;
339+ let keys = get_test_keys ( ) ;
258340
259341 // All keys are identical - should error
260- let result = key_agg_bitgo_p2tr_legacy ( & [ pubkey_user , pubkey_user ] ) ;
342+ let result = key_agg_bitgo_p2tr_legacy ( & [ keys . user , keys . user ] ) ;
261343 assert ! (
262344 result. is_err( ) ,
263345 "Expected error when all keys are identical"
@@ -268,7 +350,7 @@ mod tests {
268350 ) ;
269351
270352 // Same for p2tr_musig2
271- let result = key_agg_p2tr_musig2 ( & [ pubkey_user , pubkey_user ] ) ;
353+ let result = key_agg_p2tr_musig2 ( & [ keys . user , keys . user ] ) ;
272354 assert ! (
273355 result. is_err( ) ,
274356 "Expected error when all keys are identical"
@@ -278,4 +360,77 @@ mod tests {
278360 "Expected InvalidPubkeyCount error"
279361 ) ;
280362 }
363+
364+ #[ test]
365+ fn test_external_crate_matches_internal_implementation ( ) {
366+ // Test that the external musig2 crate produces the same results as our internal implementation
367+ let keys = get_test_keys ( ) ;
368+ let fixtures = get_aggregation_fixtures ( ) ;
369+
370+ // Test 1: Same order should produce same results
371+ let result_internal = key_agg_p2tr_musig2 ( & [ keys. user , keys. bitgo ] ) . unwrap ( ) ;
372+ let result_external = key_agg_p2tr_musig2_external_crate ( & [ keys. user , keys. bitgo ] ) . unwrap ( ) ;
373+ assert_aggregation (
374+ result_internal,
375+ fixtures. p2tr_musig2_forward ,
376+ "Internal implementation mismatch" ,
377+ ) ;
378+ assert_aggregation (
379+ result_external,
380+ fixtures. p2tr_musig2_forward ,
381+ "External crate mismatch" ,
382+ ) ;
383+
384+ // Test 2: Reverse order should produce same results (but different from test 1)
385+ let result_internal_reverse = key_agg_p2tr_musig2 ( & [ keys. bitgo , keys. user ] ) . unwrap ( ) ;
386+ let result_external_reverse =
387+ key_agg_p2tr_musig2_external_crate ( & [ keys. bitgo , keys. user ] ) . unwrap ( ) ;
388+ assert_aggregation (
389+ result_internal_reverse,
390+ fixtures. p2tr_musig2_reverse ,
391+ "Internal implementation (reverse) mismatch" ,
392+ ) ;
393+ assert_aggregation (
394+ result_external_reverse,
395+ fixtures. p2tr_musig2_reverse ,
396+ "External crate (reverse) mismatch" ,
397+ ) ;
398+
399+ // Test 3: Verify order matters for both implementations
400+ assert_ne ! (
401+ result_internal, result_internal_reverse,
402+ "Different key order should produce different results"
403+ ) ;
404+ assert_ne ! (
405+ result_external, result_external_reverse,
406+ "Different key order should produce different results for external crate"
407+ ) ;
408+ }
409+
410+ #[ test]
411+ fn test_external_crate_identical_keys_error ( ) {
412+ // Test that the external crate also rejects identical keys
413+ let keys = get_test_keys ( ) ;
414+
415+ let result = key_agg_p2tr_musig2_external_crate ( & [ keys. user , keys. user ] ) ;
416+ assert ! (
417+ result. is_err( ) ,
418+ "External crate should error when all keys are identical"
419+ ) ;
420+ }
421+
422+ #[ test]
423+ fn test_external_crate_with_three_keys ( ) {
424+ // Test with three keys to ensure it works with more than 2 keys
425+ let keys = get_test_keys ( ) ;
426+
427+ let result_internal = key_agg_p2tr_musig2 ( & [ keys. user , keys. bitgo , keys. backup ] ) . unwrap ( ) ;
428+ let result_external =
429+ key_agg_p2tr_musig2_external_crate ( & [ keys. user , keys. bitgo , keys. backup ] ) . unwrap ( ) ;
430+
431+ assert_eq ! (
432+ result_internal, result_external,
433+ "External crate should match internal implementation with 3 keys"
434+ ) ;
435+ }
281436}
0 commit comments