33//! This module provides PSBT deserialization that works across different
44//! bitcoin-like networks, including those with non-standard transaction formats.
55
6- mod p2tr_musig2_input;
6+ pub mod p2tr_musig2_input;
7+ #[ cfg( test) ]
8+ mod p2tr_musig2_input_utxolib;
79mod propkv;
810mod sighash;
911mod zcash_psbt;
1012
11- pub use p2tr_musig2_input:: {
12- parse_musig2_nonces, parse_musig2_partial_sigs, parse_musig2_participants, Musig2Error ,
13- Musig2Input , Musig2PartialSig , Musig2Participants , Musig2PubNonce ,
14- } ;
13+ use crate :: { bitgo_psbt:: zcash_psbt:: ZcashPsbt , networks:: Network } ;
14+
15+ use miniscript:: bitcoin:: { psbt:: Psbt , secp256k1, CompressedPublicKey } ;
1516pub use propkv:: { BitGoKeyValue , ProprietaryKeySubtype , BITGO } ;
1617pub use sighash:: validate_sighash_type;
1718
18- use crate :: { bitgo_psbt:: zcash_psbt:: ZcashPsbt , networks:: Network } ;
19- use miniscript:: bitcoin:: { psbt:: Psbt , secp256k1} ;
20-
2119#[ derive( Debug ) ]
2220pub enum DeserializeError {
2321 /// Standard bitcoin consensus decoding error
@@ -133,6 +131,13 @@ impl BitGoPsbt {
133131 }
134132 }
135133
134+ pub fn network ( & self ) -> Network {
135+ match self {
136+ BitGoPsbt :: BitcoinLike ( _, network) => * network,
137+ BitGoPsbt :: Zcash ( _, network) => * network,
138+ }
139+ }
140+
136141 /// Serialize the PSBT to bytes, using network-specific logic
137142 pub fn serialize ( & self ) -> Result < Vec < u8 > , SerializeError > {
138143 match self {
@@ -148,6 +153,28 @@ impl BitGoPsbt {
148153 }
149154 }
150155
156+ /// Get a reference to the underlying PSBT
157+ ///
158+ /// This works for both BitcoinLike and Zcash PSBTs, returning a reference
159+ /// to the inner Bitcoin-compatible PSBT structure.
160+ pub fn psbt ( & self ) -> & Psbt {
161+ match self {
162+ BitGoPsbt :: BitcoinLike ( ref psbt, _network) => psbt,
163+ BitGoPsbt :: Zcash ( ref zcash_psbt, _network) => & zcash_psbt. psbt ,
164+ }
165+ }
166+
167+ /// Get a mutable reference to the underlying PSBT
168+ ///
169+ /// This works for both BitcoinLike and Zcash PSBTs, returning a reference
170+ /// to the inner Bitcoin-compatible PSBT structure.
171+ pub fn psbt_mut ( & mut self ) -> & mut Psbt {
172+ match self {
173+ BitGoPsbt :: BitcoinLike ( ref mut psbt, _network) => psbt,
174+ BitGoPsbt :: Zcash ( ref mut zcash_psbt, _network) => & mut zcash_psbt. psbt ,
175+ }
176+ }
177+
151178 pub fn finalize_input < C : secp256k1:: Verification > (
152179 & mut self ,
153180 secp : & secp256k1:: Secp256k1 < C > ,
@@ -158,9 +185,10 @@ impl BitGoPsbt {
158185 match self {
159186 BitGoPsbt :: BitcoinLike ( ref mut psbt, _network) => {
160187 // Use custom bitgo p2trMusig2 input finalization for MuSig2 inputs
161- if Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
162- Musig2Input :: finalize_input ( psbt, secp , input_index)
188+ if p2tr_musig2_input :: Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
189+ let mut ctx = p2tr_musig2_input :: Musig2Context :: new ( psbt, input_index)
163190 . map_err ( |e| e. to_string ( ) ) ?;
191+ ctx. finalize_input ( secp) . map_err ( |e| e. to_string ( ) ) ?;
164192 return Ok ( ( ) ) ;
165193 }
166194 // other inputs can be finalized using the standard miniscript::psbt::finalize_input
@@ -188,10 +216,7 @@ impl BitGoPsbt {
188216 & mut self ,
189217 secp : & secp256k1:: Secp256k1 < C > ,
190218 ) -> Result < ( ) , Vec < String > > {
191- let num_inputs = match self {
192- BitGoPsbt :: BitcoinLike ( psbt, _network) => psbt. inputs . len ( ) ,
193- BitGoPsbt :: Zcash ( zcash_psbt, _network) => zcash_psbt. psbt . inputs . len ( ) ,
194- } ;
219+ let num_inputs = self . psbt ( ) . inputs . len ( ) ;
195220
196221 let mut errors = vec ! [ ] ;
197222 for index in 0 ..num_inputs {
@@ -230,6 +255,108 @@ impl BitGoPsbt {
230255 }
231256 }
232257
258+ /// Helper function to create a MuSig2 context for an input
259+ ///
260+ /// This validates that:
261+ /// 1. The PSBT is BitcoinLike (not Zcash)
262+ /// 2. The input index is valid
263+ /// 3. The input is a MuSig2 input
264+ ///
265+ /// Returns a Musig2Context for the specified input
266+ fn musig2_context < ' a > (
267+ & ' a mut self ,
268+ input_index : usize ,
269+ ) -> Result < p2tr_musig2_input:: Musig2Context < ' a > , String > {
270+ if self . network ( ) . mainnet ( ) != Network :: Bitcoin {
271+ return Err ( "MuSig2 not supported for non-Bitcoin networks" . to_string ( ) ) ;
272+ }
273+
274+ if matches ! ( self , BitGoPsbt :: Zcash ( _, _) ) {
275+ return Err ( "MuSig2 not supported for Zcash" . to_string ( ) ) ;
276+ }
277+
278+ let psbt = self . psbt_mut ( ) ;
279+ if input_index >= psbt. inputs . len ( ) {
280+ return Err ( format ! ( "Input index {} out of bounds" , input_index) ) ;
281+ }
282+
283+ // Validate this is a MuSig2 input
284+ if !p2tr_musig2_input:: Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
285+ return Err ( format ! ( "Input {} is not a MuSig2 input" , input_index) ) ;
286+ }
287+
288+ // Create and return the context
289+ p2tr_musig2_input:: Musig2Context :: new ( psbt, input_index) . map_err ( |e| e. to_string ( ) )
290+ }
291+
292+ /// Set the counterparty's (BitGo's) nonce in the PSBT
293+ ///
294+ /// # Arguments
295+ /// * `input_index` - The index of the MuSig2 input
296+ /// * `participant_pub_key` - The counterparty's public key
297+ /// * `pub_nonce` - The counterparty's public nonce
298+ pub fn set_counterparty_nonce (
299+ & mut self ,
300+ input_index : usize ,
301+ participant_pub_key : CompressedPublicKey ,
302+ pub_nonce : musig2:: PubNonce ,
303+ ) -> Result < ( ) , String > {
304+ let mut ctx = self . musig2_context ( input_index) ?;
305+ let tap_output_key = ctx. musig2_input ( ) . participants . tap_output_key ;
306+
307+ // Set the nonce
308+ ctx. set_nonce ( participant_pub_key, tap_output_key, pub_nonce)
309+ . map_err ( |e| e. to_string ( ) )
310+ }
311+
312+ /// Generate and set a user nonce for a MuSig2 input using State-Machine API
313+ ///
314+ /// This method uses the State-Machine API from the musig2 crate, which encapsulates
315+ /// the SecNonce internally to prevent accidental reuse. This is the recommended
316+ /// production API.
317+ ///
318+ /// # Arguments
319+ /// * `input_index` - The index of the MuSig2 input
320+ /// * `xpriv` - The user's extended private key (will be derived for the input)
321+ /// * `session_id` - 32-byte session ID (use rand::thread_rng().gen() in production)
322+ ///
323+ /// # Returns
324+ /// A tuple of (FirstRound, PubNonce) - keep FirstRound secret for signing later,
325+ /// send PubNonce to the counterparty
326+ pub fn generate_nonce_first_round (
327+ & mut self ,
328+ input_index : usize ,
329+ xpriv : & miniscript:: bitcoin:: bip32:: Xpriv ,
330+ session_id : [ u8 ; 32 ] ,
331+ ) -> Result < ( musig2:: FirstRound , musig2:: PubNonce ) , String > {
332+ let mut ctx = self . musig2_context ( input_index) ?;
333+ ctx. generate_nonce_first_round ( xpriv, session_id)
334+ . map_err ( |e| e. to_string ( ) )
335+ }
336+
337+ /// Sign a MuSig2 input using State-Machine API
338+ ///
339+ /// This method uses the State-Machine API from the musig2 crate. The FirstRound
340+ /// from nonce generation encapsulates the secret nonce, preventing reuse.
341+ ///
342+ /// # Arguments
343+ /// * `input_index` - The index of the MuSig2 input
344+ /// * `first_round` - The FirstRound from generate_nonce_first_round()
345+ /// * `xpriv` - The user's extended private key
346+ ///
347+ /// # Returns
348+ /// Ok(()) if the signature was successfully created and added to the PSBT
349+ pub fn sign_with_first_round (
350+ & mut self ,
351+ input_index : usize ,
352+ first_round : musig2:: FirstRound ,
353+ xpriv : & miniscript:: bitcoin:: bip32:: Xpriv ,
354+ ) -> Result < ( ) , String > {
355+ let mut ctx = self . musig2_context ( input_index) ?;
356+ ctx. sign_with_first_round ( first_round, xpriv)
357+ . map_err ( |e| e. to_string ( ) )
358+ }
359+
233360 /// Sign the PSBT with the provided key.
234361 /// Wraps the underlying PSBT's sign method from miniscript::psbt::PsbtExt.
235362 ///
@@ -454,22 +581,36 @@ mod tests {
454581 script_type : fixtures:: ScriptType ,
455582 unsigned_bitgo_psbt : & BitGoPsbt ,
456583 halfsigned_bitgo_psbt : & BitGoPsbt ,
457- wallet_keys : & fixtures:: XprvTriple ,
584+ xpriv_triple : & fixtures:: XprvTriple ,
458585 input_index : usize ,
459586 ) -> Result < ( ) , String > {
460- let user_key = wallet_keys . user_key ( ) ;
587+ let user_xpriv = xpriv_triple . user_key ( ) ;
461588
462589 // Clone the unsigned PSBT and sign with user key
463- let mut signed_psbt = unsigned_bitgo_psbt. clone ( ) ;
590+ let mut unsigned_bitgo_psbt = unsigned_bitgo_psbt. clone ( ) ;
464591 let secp = secp256k1:: Secp256k1 :: new ( ) ;
465592
593+ if script_type == fixtures:: ScriptType :: P2trMusig2TaprootKeypath {
594+ // MuSig2 keypath: set nonces and sign with user key
595+ p2tr_musig2_input:: assert_set_nonce_and_sign_musig2_keypath (
596+ xpriv_triple,
597+ & mut unsigned_bitgo_psbt,
598+ halfsigned_bitgo_psbt,
599+ input_index,
600+ ) ?;
601+
602+ // MuSig2 inputs use proprietary key values for partial signatures,
603+ // not standard PSBT partial_sigs, so we're done
604+ return Ok ( ( ) ) ;
605+ }
606+
466607 // Sign with user key using the new sign method
467- signed_psbt
468- . sign ( user_key , & secp)
608+ unsigned_bitgo_psbt
609+ . sign ( user_xpriv , & secp)
469610 . map_err ( |( _num_keys, errors) | format ! ( "Failed to sign PSBT: {:?}" , errors) ) ?;
470611
471612 // Extract partial signatures from the signed input
472- let signed_input = match & signed_psbt {
613+ let signed_input = match & unsigned_bitgo_psbt {
473614 BitGoPsbt :: BitcoinLike ( psbt, _) => & psbt. inputs [ input_index] ,
474615 BitGoPsbt :: Zcash ( _, _) => {
475616 return Err ( "Zcash signing not yet implemented" . to_string ( ) ) ;
@@ -631,21 +772,19 @@ mod tests {
631772
632773 let psbt_input_stages = psbt_input_stages. unwrap ( ) ;
633774
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- }
775+ assert_half_sign (
776+ script_type,
777+ & psbt_stages
778+ . unsigned
779+ . to_bitgo_psbt ( network)
780+ . expect ( "Failed to convert to BitGo PSBT" ) ,
781+ & psbt_stages
782+ . halfsigned
783+ . to_bitgo_psbt ( network)
784+ . expect ( "Failed to convert to BitGo PSBT" ) ,
785+ & psbt_input_stages. wallet_keys ,
786+ psbt_input_stages. input_index ,
787+ ) ?;
649788
650789 assert_full_signed_matches_wallet_scripts (
651790 network,
0 commit comments