@@ -229,16 +229,56 @@ impl BitGoPsbt {
229229 ) ) ,
230230 }
231231 }
232+
233+ /// Sign the PSBT with the provided key.
234+ /// Wraps the underlying PSBT's sign method from miniscript::psbt::PsbtExt.
235+ ///
236+ /// # Type Parameters
237+ /// - `C`: Signing context from secp256k1
238+ /// - `K`: Key type that implements `psbt::GetKey` trait
239+ ///
240+ /// # Returns
241+ /// - `Ok(SigningKeysMap)` on success, mapping input index to keys used for signing
242+ /// - `Err((SigningKeysMap, SigningErrors))` on failure, containing both partial success info and errors
243+ pub fn sign < C , K > (
244+ & mut self ,
245+ k : & K ,
246+ secp : & secp256k1:: Secp256k1 < C > ,
247+ ) -> Result <
248+ miniscript:: bitcoin:: psbt:: SigningKeysMap ,
249+ (
250+ miniscript:: bitcoin:: psbt:: SigningKeysMap ,
251+ miniscript:: bitcoin:: psbt:: SigningErrors ,
252+ ) ,
253+ >
254+ where
255+ C : secp256k1:: Signing + secp256k1:: Verification ,
256+ K : miniscript:: bitcoin:: psbt:: GetKey ,
257+ {
258+ match self {
259+ BitGoPsbt :: BitcoinLike ( ref mut psbt, _network) => psbt. sign ( k, secp) ,
260+ BitGoPsbt :: Zcash ( _zcash_psbt, _network) => {
261+ // Return an error indicating Zcash signing is not implemented
262+ Err ( (
263+ Default :: default ( ) ,
264+ std:: collections:: BTreeMap :: from_iter ( [ (
265+ 0 ,
266+ miniscript:: bitcoin:: psbt:: SignError :: KeyNotFound ,
267+ ) ] ) ,
268+ ) )
269+ }
270+ }
271+ }
232272}
233273
234274#[ cfg( test) ]
235275mod tests {
236276 use super :: * ;
237277 use crate :: fixed_script_wallet:: Chain ;
238- use crate :: fixed_script_wallet:: { RootWalletKeys , WalletScripts } ;
278+ use crate :: fixed_script_wallet:: WalletScripts ;
239279 use crate :: test_utils:: fixtures;
280+ use crate :: test_utils:: fixtures:: assert_hex_eq;
240281 use base64:: engine:: { general_purpose:: STANDARD as BASE64_STANDARD , Engine } ;
241- use miniscript:: bitcoin:: bip32:: Xpub ;
242282 use miniscript:: bitcoin:: consensus:: Decodable ;
243283 use miniscript:: bitcoin:: Transaction ;
244284
@@ -386,18 +426,95 @@ mod tests {
386426 output. script_pubkey . to_hex_string ( )
387427 }
388428
389- fn assert_matches_wallet_scripts (
429+ type PartialSignatures =
430+ std:: collections:: BTreeMap < crate :: bitcoin:: PublicKey , crate :: bitcoin:: ecdsa:: Signature > ;
431+
432+ fn assert_eq_partial_signatures (
433+ actual : & PartialSignatures ,
434+ expected : & PartialSignatures ,
435+ ) -> Result < ( ) , String > {
436+ assert_eq ! (
437+ actual. len( ) ,
438+ expected. len( ) ,
439+ "Partial signatures should match"
440+ ) ;
441+ for ( actual_sig, expected_sig) in actual. iter ( ) . zip ( expected. iter ( ) ) {
442+ assert_eq ! ( actual_sig. 0 , expected_sig. 0 , "Public key should match" ) ;
443+ assert_hex_eq (
444+ & hex:: encode ( actual_sig. 1 . serialize ( ) ) ,
445+ & hex:: encode ( expected_sig. 1 . serialize ( ) ) ,
446+ "Signature" ,
447+ ) ?;
448+ }
449+ Ok ( ( ) )
450+ }
451+
452+ // ensure we can put the first signature (user signature) on an unsigned PSBT
453+ fn assert_half_sign (
454+ script_type : fixtures:: ScriptType ,
455+ unsigned_bitgo_psbt : & BitGoPsbt ,
456+ halfsigned_bitgo_psbt : & BitGoPsbt ,
457+ wallet_keys : & fixtures:: XprvTriple ,
458+ input_index : usize ,
459+ ) -> Result < ( ) , String > {
460+ let user_key = wallet_keys. user_key ( ) ;
461+
462+ // Clone the unsigned PSBT and sign with user key
463+ let mut signed_psbt = unsigned_bitgo_psbt. clone ( ) ;
464+ let secp = secp256k1:: Secp256k1 :: new ( ) ;
465+
466+ // Sign with user key using the new sign method
467+ signed_psbt
468+ . sign ( user_key, & secp)
469+ . map_err ( |( _num_keys, errors) | format ! ( "Failed to sign PSBT: {:?}" , errors) ) ?;
470+
471+ // Extract partial signatures from the signed input
472+ let signed_input = match & signed_psbt {
473+ BitGoPsbt :: BitcoinLike ( psbt, _) => & psbt. inputs [ input_index] ,
474+ BitGoPsbt :: Zcash ( _, _) => {
475+ return Err ( "Zcash signing not yet implemented" . to_string ( ) ) ;
476+ }
477+ } ;
478+
479+ match script_type {
480+ fixtures:: ScriptType :: P2trLegacyScriptPath
481+ | fixtures:: ScriptType :: P2trMusig2ScriptPath => {
482+ assert_eq ! ( signed_input. tap_script_sigs. len( ) , 1 ) ;
483+ // Get expected tap script sig from halfsigned fixture
484+ let expected_tap_script_sig = halfsigned_bitgo_psbt. clone ( ) . into_psbt ( ) . inputs
485+ [ input_index]
486+ . tap_script_sigs
487+ . clone ( ) ;
488+ assert_eq ! ( signed_input. tap_script_sigs, expected_tap_script_sig) ;
489+ }
490+ _ => {
491+ let actual_partial_sigs = signed_input. partial_sigs . clone ( ) ;
492+ // Get expected partial signatures from halfsigned fixture
493+ let expected_partial_sigs = halfsigned_bitgo_psbt. clone ( ) . into_psbt ( ) . inputs
494+ [ input_index]
495+ . partial_sigs
496+ . clone ( ) ;
497+
498+ assert_eq ! ( actual_partial_sigs. len( ) , 1 ) ;
499+ assert_eq_partial_signatures ( & actual_partial_sigs, & expected_partial_sigs) ?;
500+ }
501+ }
502+
503+ Ok ( ( ) )
504+ }
505+
506+ fn assert_full_signed_matches_wallet_scripts (
390507 network : Network ,
391508 tx_format : fixtures:: TxFormat ,
392509 fixture : & fixtures:: PsbtFixture ,
393- wallet_keys : & RootWalletKeys ,
510+ wallet_keys : & fixtures :: XprvTriple ,
394511 input_index : usize ,
395512 input_fixture : & fixtures:: PsbtInputFixture ,
396513 ) -> Result < ( ) , String > {
397514 let ( chain, index) =
398515 parse_fixture_paths ( input_fixture) . expect ( "Failed to parse fixture paths" ) ;
399516 let scripts = WalletScripts :: from_wallet_keys (
400- wallet_keys,
517+ & wallet_keys. to_root_wallet_keys ( ) ,
401518 chain,
402519 index,
403520 & network. output_script_support ( ) ,
@@ -497,58 +614,59 @@ mod tests {
497614 network : Network ,
498615 tx_format : fixtures:: TxFormat ,
499616 ) -> Result < ( ) , String > {
500- let fixture = fixtures:: load_psbt_fixture_with_format (
501- network. to_utxolib_name ( ) ,
502- fixtures:: SignatureState :: Fullsigned ,
503- tx_format,
504- )
505- . expect ( "Failed to load fixture" ) ;
506- let wallet_keys =
507- fixtures:: parse_wallet_keys ( & fixture) . expect ( "Failed to parse wallet keys" ) ;
508- let secp = crate :: bitcoin:: secp256k1:: Secp256k1 :: new ( ) ;
509- let wallet_keys = RootWalletKeys :: new (
510- wallet_keys
511- . iter ( )
512- . map ( |x| Xpub :: from_priv ( & secp, x) )
513- . collect :: < Vec < _ > > ( )
514- . try_into ( )
515- . expect ( "Failed to convert to XpubTriple" ) ,
516- ) ;
617+ let psbt_stages = fixtures:: PsbtStages :: load ( network, tx_format) ?;
618+ let psbt_input_stages =
619+ fixtures:: PsbtInputStages :: from_psbt_stages ( & psbt_stages, script_type) ;
517620
518621 // Check if the script type is supported by the network
519622 let output_script_support = network. output_script_support ( ) ;
520- let input_fixture = fixture. find_input_with_script_type ( script_type) ;
521623 if !script_type. is_supported_by ( & output_script_support) {
522624 // Script type not supported by network - skip test (no fixture expected)
523625 assert ! (
524- input_fixture . is_err( ) ,
626+ psbt_input_stages . is_err( ) ,
525627 "Expected error for unsupported script type"
526628 ) ;
527629 return Ok ( ( ) ) ;
528630 }
529631
530- let ( input_index, input_fixture) = input_fixture. unwrap ( ) ;
632+ let psbt_input_stages = psbt_input_stages. unwrap ( ) ;
633+
634+ if script_type != fixtures:: ScriptType :: P2trMusig2TaprootKeypath {
635+ assert_half_sign (
636+ script_type,
637+ & psbt_stages
638+ . unsigned
639+ . to_bitgo_psbt ( network)
640+ . expect ( "Failed to convert to BitGo PSBT" ) ,
641+ & psbt_stages
642+ . halfsigned
643+ . to_bitgo_psbt ( network)
644+ . expect ( "Failed to convert to BitGo PSBT" ) ,
645+ & psbt_input_stages. wallet_keys ,
646+ psbt_input_stages. input_index ,
647+ ) ?;
648+ }
531649
532- assert_matches_wallet_scripts (
650+ assert_full_signed_matches_wallet_scripts (
533651 network,
534652 tx_format,
535- & fixture ,
536- & wallet_keys,
537- input_index,
538- input_fixture ,
653+ & psbt_stages . fullsigned ,
654+ & psbt_input_stages . wallet_keys ,
655+ psbt_input_stages . input_index ,
656+ & psbt_input_stages . input_fixture_fullsigned ,
539657 ) ?;
540658
541659 assert_finalize_input (
542- fixture . to_bitgo_psbt ( network) . unwrap ( ) ,
543- input_index,
660+ psbt_stages . fullsigned . to_bitgo_psbt ( network) . unwrap ( ) ,
661+ psbt_input_stages . input_index ,
544662 network,
545663 tx_format,
546664 ) ?;
547665
548666 Ok ( ( ) )
549667 }
550668
551- crate :: test_psbt_fixtures!( test_p2sh_script_generation_from_fixture , network, format, {
669+ crate :: test_psbt_fixtures!( test_p2sh_suite , network, format, {
552670 test_wallet_script_type( fixtures:: ScriptType :: P2sh , network, format) . unwrap( ) ;
553671 } , ignore: [
554672 // TODO: sighash support
@@ -558,7 +676,7 @@ mod tests {
558676 ] ) ;
559677
560678 crate :: test_psbt_fixtures!(
561- test_p2sh_p2wsh_script_generation_from_fixture ,
679+ test_p2sh_p2wsh_suite ,
562680 network,
563681 format,
564682 {
@@ -569,7 +687,7 @@ mod tests {
569687 ) ;
570688
571689 crate :: test_psbt_fixtures!(
572- test_p2wsh_script_generation_from_fixture ,
690+ test_p2wsh_suite ,
573691 network,
574692 format,
575693 {
@@ -579,27 +697,24 @@ mod tests {
579697 ignore: [ BitcoinGold ]
580698 ) ;
581699
582- crate :: test_psbt_fixtures!( test_p2tr_script_generation_from_fixture, network, format, {
583- test_wallet_script_type( fixtures:: ScriptType :: P2tr , network, format) . unwrap( ) ;
700+ crate :: test_psbt_fixtures!( test_p2tr_legacy_script_path_suite, network, format, {
701+ test_wallet_script_type( fixtures:: ScriptType :: P2trLegacyScriptPath , network, format)
702+ . unwrap( ) ;
584703 } ) ;
585704
586- crate :: test_psbt_fixtures!(
587- test_p2tr_musig2_script_path_generation_from_fixture,
588- network,
589- format,
590- {
591- test_wallet_script_type( fixtures:: ScriptType :: P2trMusig2 , network, format) . unwrap( ) ;
592- }
593- ) ;
705+ crate :: test_psbt_fixtures!( test_p2tr_musig2_script_path_suite, network, format, {
706+ test_wallet_script_type( fixtures:: ScriptType :: P2trMusig2ScriptPath , network, format)
707+ . unwrap( ) ;
708+ } ) ;
594709
595- crate :: test_psbt_fixtures!(
596- test_p2tr_musig2_key_path_spend_script_generation_from_fixture ,
597- network ,
598- format ,
599- {
600- test_wallet_script_type ( fixtures :: ScriptType :: TaprootKeypath , network , format ) . unwrap ( ) ;
601- }
602- ) ;
710+ crate :: test_psbt_fixtures!( test_p2tr_musig2_key_path_suite , network , format , {
711+ test_wallet_script_type (
712+ fixtures :: ScriptType :: P2trMusig2TaprootKeypath ,
713+ network ,
714+ format ,
715+ )
716+ . unwrap ( ) ;
717+ } ) ;
603718
604719 crate :: test_psbt_fixtures!( test_extract_transaction, network, format, {
605720 let fixture = fixtures:: load_psbt_fixture_with_format(
0 commit comments