@@ -204,6 +204,71 @@ impl BitGoPsbt {
204204 }
205205 }
206206
207+ /// Combine/merge data from another PSBT into this one
208+ ///
209+ /// This method copies MuSig2 nonces and signatures (proprietary key-value pairs) from the
210+ /// source PSBT to this PSBT. This is useful for merging PSBTs during the nonce exchange
211+ /// and signature collection phases.
212+ ///
213+ /// # Arguments
214+ /// * `source_psbt` - The source PSBT containing data to merge
215+ ///
216+ /// # Returns
217+ /// Ok(()) if data was successfully merged
218+ ///
219+ /// # Errors
220+ /// Returns error if networks don't match
221+ pub fn combine_musig2_nonces ( & mut self , source_psbt : & BitGoPsbt ) -> Result < ( ) , String > {
222+ // Check network match
223+ if self . network ( ) != source_psbt. network ( ) {
224+ return Err ( format ! (
225+ "Network mismatch: destination is {}, source is {}" ,
226+ self . network( ) ,
227+ source_psbt. network( )
228+ ) ) ;
229+ }
230+
231+ let source = source_psbt. psbt ( ) ;
232+ let dest = self . psbt_mut ( ) ;
233+
234+ // Check that both PSBTs have the same number of inputs
235+ if source. inputs . len ( ) != dest. inputs . len ( ) {
236+ return Err ( format ! (
237+ "PSBT input count mismatch: source has {} inputs, destination has {}" ,
238+ source. inputs. len( ) ,
239+ dest. inputs. len( )
240+ ) ) ;
241+ }
242+
243+ // Copy MuSig2 nonces and partial signatures (proprietary key-values with BITGO identifier)
244+ for ( source_input, dest_input) in source. inputs . iter ( ) . zip ( dest. inputs . iter_mut ( ) ) {
245+ // Only process if the input is a MuSig2 input
246+ if !p2tr_musig2_input:: Musig2Input :: is_musig2_input ( source_input) {
247+ continue ;
248+ }
249+
250+ // Parse nonces from source input using native Musig2 functions
251+ let nonces = p2tr_musig2_input:: parse_musig2_nonces ( source_input)
252+ . map_err ( |e| format ! ( "Failed to parse MuSig2 nonces from source: {}" , e) ) ?;
253+
254+ // Copy each nonce to the destination input
255+ for nonce in nonces {
256+ let ( key, value) = nonce. to_key_value ( ) . to_key_value ( ) ;
257+ dest_input. proprietary . insert ( key, value) ;
258+ }
259+
260+ // Also copy partial signatures if present
261+ // Partial sigs are stored as tap_script_sigs in the PSBT input
262+ for ( control_block, leaf_script) in & source_input. tap_script_sigs {
263+ dest_input
264+ . tap_script_sigs
265+ . insert ( * control_block, * leaf_script) ;
266+ }
267+ }
268+
269+ Ok ( ( ) )
270+ }
271+
207272 /// Serialize the PSBT to bytes, using network-specific logic
208273 pub fn serialize ( & self ) -> Result < Vec < u8 > , SerializeError > {
209274 match self {
@@ -426,6 +491,146 @@ impl BitGoPsbt {
426491 . map_err ( |e| e. to_string ( ) )
427492 }
428493
494+ /// Sign a single input with a raw private key
495+ ///
496+ /// This method signs a specific input using the provided private key. It automatically
497+ /// detects the input type and uses the appropriate signing method:
498+ /// - Replay protection inputs (P2SH-P2PK): Signs with legacy P2SH sighash
499+ /// - Regular inputs: Uses standard PSBT signing
500+ /// - MuSig2 inputs: Returns error (requires FirstRound state, use sign_with_first_round)
501+ ///
502+ /// # Arguments
503+ /// - `input_index`: The index of the input to sign
504+ /// - `privkey`: The private key to sign with
505+ ///
506+ /// # Returns
507+ /// - `Ok(())` if signing was successful
508+ /// - `Err(String)` if signing fails or input type is not supported
509+ pub fn sign_with_privkey (
510+ & mut self ,
511+ input_index : usize ,
512+ privkey : & secp256k1:: SecretKey ,
513+ ) -> Result < ( ) , String > {
514+ use miniscript:: bitcoin:: {
515+ ecdsa:: Signature as EcdsaSignature , hashes:: Hash , sighash:: SighashCache , PublicKey ,
516+ } ;
517+
518+ // Get network before mutable borrow
519+ let network = self . network ( ) ;
520+ let is_testnet = network. is_testnet ( ) ;
521+
522+ let psbt = self . psbt_mut ( ) ;
523+
524+ // Check bounds
525+ if input_index >= psbt. inputs . len ( ) {
526+ return Err ( format ! (
527+ "Input index {} out of bounds (total inputs: {})" ,
528+ input_index,
529+ psbt. inputs. len( )
530+ ) ) ;
531+ }
532+
533+ // Check if this is a MuSig2 input
534+ if p2tr_musig2_input:: Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
535+ return Err (
536+ "MuSig2 inputs cannot be signed with raw privkey. Use sign_with_first_round instead."
537+ . to_string ( ) ,
538+ ) ;
539+ }
540+
541+ let secp = secp256k1:: Secp256k1 :: new ( ) ;
542+
543+ // Derive public key from private key
544+ let public_key = PublicKey :: from_slice (
545+ & secp256k1:: PublicKey :: from_secret_key ( & secp, privkey) . serialize ( ) ,
546+ )
547+ . map_err ( |e| format ! ( "Failed to derive public key: {}" , e) ) ?;
548+
549+ // Check if this is a replay protection input (P2SH-P2PK)
550+ if let Some ( redeem_script) = & psbt. inputs [ input_index] . redeem_script . clone ( ) {
551+ // Try to extract pubkey from redeem script
552+ if let Ok ( redeem_pubkey) = Self :: extract_pubkey_from_p2pk_redeem_script ( redeem_script) {
553+ // This is a replay protection input - verify the derived pubkey matches
554+ if public_key != redeem_pubkey {
555+ return Err (
556+ "Public key mismatch: derived pubkey does not match redeem_script pubkey"
557+ . to_string ( ) ,
558+ ) ;
559+ }
560+
561+ // Sign the replay protection input with legacy P2SH sighash
562+ let sighash_type = miniscript:: bitcoin:: sighash:: EcdsaSighashType :: All ;
563+ let cache = SighashCache :: new ( & psbt. unsigned_tx ) ;
564+ let sighash = cache
565+ . legacy_signature_hash ( input_index, redeem_script, sighash_type. to_u32 ( ) )
566+ . map_err ( |e| format ! ( "Failed to compute sighash: {}" , e) ) ?;
567+
568+ // Create ECDSA signature
569+ let message = secp256k1:: Message :: from_digest ( sighash. to_byte_array ( ) ) ;
570+ let signature = secp. sign_ecdsa ( & message, privkey) ;
571+ let ecdsa_sig = EcdsaSignature {
572+ signature,
573+ sighash_type,
574+ } ;
575+
576+ // Add signature to partial_sigs
577+ psbt. inputs [ input_index]
578+ . partial_sigs
579+ . insert ( public_key, ecdsa_sig) ;
580+
581+ return Ok ( ( ) ) ;
582+ }
583+ }
584+
585+ // For regular inputs (non-RP, non-MuSig2), use standard signing via miniscript
586+ // This will handle legacy, SegWit, and Taproot script path inputs
587+ match self {
588+ BitGoPsbt :: BitcoinLike ( ref mut psbt, _network) => {
589+ // Create a key provider that returns our single key
590+ // Convert SecretKey to PrivateKey for the GetKey trait
591+ // Note: The network parameter is only used for WIF serialization, not for signing
592+ let bitcoin_network = if is_testnet {
593+ miniscript:: bitcoin:: Network :: Testnet
594+ } else {
595+ miniscript:: bitcoin:: Network :: Bitcoin
596+ } ;
597+ let private_key = miniscript:: bitcoin:: PrivateKey :: new ( * privkey, bitcoin_network) ;
598+ let key_map = std:: collections:: BTreeMap :: from_iter ( [ ( public_key, private_key) ] ) ;
599+
600+ // Sign the PSBT
601+ let result = psbt. sign ( & key_map, & secp) ;
602+
603+ // Check if our specific input was signed
604+ match result {
605+ Ok ( signing_keys) => {
606+ if signing_keys. contains_key ( & input_index) {
607+ Ok ( ( ) )
608+ } else {
609+ Err ( format ! (
610+ "Input {} was not signed (no key found or already signed)" ,
611+ input_index
612+ ) )
613+ }
614+ }
615+ Err ( ( partial_success, errors) ) => {
616+ // Check if there's an error for our specific input
617+ if let Some ( error) = errors. get ( & input_index) {
618+ Err ( format ! ( "Failed to sign input {}: {:?}" , input_index, error) )
619+ } else if partial_success. contains_key ( & input_index) {
620+ // Input was signed successfully despite other errors
621+ Ok ( ( ) )
622+ } else {
623+ Err ( format ! ( "Input {} was not signed" , input_index) )
624+ }
625+ }
626+ }
627+ }
628+ BitGoPsbt :: Zcash ( _zcash_psbt, _network) => {
629+ Err ( "Zcash signing not yet implemented" . to_string ( ) )
630+ }
631+ }
632+ }
633+
429634 /// Sign the PSBT with the provided key.
430635 /// Wraps the underlying PSBT's sign method from miniscript::psbt::PsbtExt.
431636 ///
0 commit comments