1+ use std:: collections:: HashMap ;
12use wasm_bindgen:: prelude:: * ;
23use wasm_bindgen:: JsValue ;
34
@@ -83,6 +84,9 @@ impl FixedScriptWalletNamespace {
8384#[ wasm_bindgen]
8485pub struct BitGoPsbt {
8586 psbt : crate :: fixed_script_wallet:: bitgo_psbt:: BitGoPsbt ,
87+ // Store FirstRound states per (input_index, xpub_string)
88+ #[ wasm_bindgen( skip) ]
89+ first_rounds : HashMap < ( usize , String ) , musig2:: FirstRound > ,
8690}
8791
8892#[ wasm_bindgen]
@@ -95,7 +99,10 @@ impl BitGoPsbt {
9599 crate :: fixed_script_wallet:: bitgo_psbt:: BitGoPsbt :: deserialize ( bytes, network)
96100 . map_err ( |e| WasmUtxoError :: new ( & format ! ( "Failed to deserialize PSBT: {}" , e) ) ) ?;
97101
98- Ok ( BitGoPsbt { psbt } )
102+ Ok ( BitGoPsbt {
103+ psbt,
104+ first_rounds : HashMap :: new ( ) ,
105+ } )
99106 }
100107
101108 /// Get the unsigned transaction ID
@@ -262,6 +269,108 @@ impl BitGoPsbt {
262269 . map_err ( |e| WasmUtxoError :: new ( & format ! ( "Failed to serialize PSBT: {}" , e) ) )
263270 }
264271
272+ /// Generate and store MuSig2 nonces for all MuSig2 inputs
273+ ///
274+ /// This method generates nonces using the State-Machine API and stores them in the PSBT.
275+ /// The nonces are stored as proprietary fields in the PSBT and will be included when serialized.
276+ /// After ALL participants have generated their nonces, they can sign MuSig2 inputs using
277+ /// sign_with_xpriv().
278+ ///
279+ /// # Arguments
280+ /// * `xpriv` - The extended private key (xpriv) for signing
281+ /// * `session_id_bytes` - Optional 32-byte session ID for nonce generation. **Only allowed on testnets**.
282+ /// On mainnets, a secure random session ID is always generated automatically.
283+ /// Must be unique per signing session.
284+ ///
285+ /// # Returns
286+ /// Ok(()) if nonces were successfully generated and stored
287+ ///
288+ /// # Errors
289+ /// Returns error if:
290+ /// - Nonce generation fails
291+ /// - session_id length is invalid
292+ /// - Custom session_id is provided on a mainnet (security restriction)
293+ ///
294+ /// # Security
295+ /// The session_id MUST be cryptographically random and unique for each signing session.
296+ /// Never reuse a session_id with the same key! On mainnets, session_id is always randomly
297+ /// generated for security. Custom session_id is only allowed on testnets for testing purposes.
298+ pub fn generate_musig2_nonces (
299+ & mut self ,
300+ xpriv : & WasmBIP32 ,
301+ session_id_bytes : Option < Vec < u8 > > ,
302+ ) -> Result < ( ) , WasmUtxoError > {
303+ // Extract Xpriv from WasmBIP32
304+ let xpriv = xpriv. to_xpriv ( ) ?;
305+
306+ // Get the network from the PSBT to check if custom session_id is allowed
307+ let network = self . psbt . network ( ) ;
308+
309+ // Get or generate session ID
310+ let session_id = match session_id_bytes {
311+ Some ( bytes) => {
312+ // Only allow custom session_id on testnets for security
313+ if !network. is_testnet ( ) {
314+ return Err ( WasmUtxoError :: new (
315+ "Custom session_id is only allowed on testnets. On mainnets, session_id is always randomly generated for security."
316+ ) ) ;
317+ }
318+ if bytes. len ( ) != 32 {
319+ return Err ( WasmUtxoError :: new ( & format ! (
320+ "Session ID must be 32 bytes, got {}" ,
321+ bytes. len( )
322+ ) ) ) ;
323+ }
324+ let mut session_id = [ 0u8 ; 32 ] ;
325+ session_id. copy_from_slice ( & bytes) ;
326+ session_id
327+ }
328+ None => {
329+ // Generate secure random session ID
330+ use getrandom:: getrandom;
331+ let mut session_id = [ 0u8 ; 32 ] ;
332+ getrandom ( & mut session_id) . map_err ( |e| {
333+ WasmUtxoError :: new ( & format ! ( "Failed to generate random session ID: {}" , e) )
334+ } ) ?;
335+ session_id
336+ }
337+ } ;
338+
339+ // Derive xpub from xpriv to use as key
340+ let secp = miniscript:: bitcoin:: secp256k1:: Secp256k1 :: new ( ) ;
341+ let xpub = miniscript:: bitcoin:: bip32:: Xpub :: from_priv ( & secp, & xpriv) ;
342+ let xpub_str = xpub. to_string ( ) ;
343+
344+ // Iterate over all inputs and generate nonces for MuSig2 inputs
345+ let input_count = self . psbt . psbt ( ) . unsigned_tx . input . len ( ) ;
346+ for input_index in 0 ..input_count {
347+ // Check if this input is a MuSig2 input
348+ let psbt = self . psbt . psbt ( ) ;
349+ if !crate :: fixed_script_wallet:: bitgo_psbt:: p2tr_musig2_input:: Musig2Input :: is_musig2_input ( & psbt. inputs [ input_index] ) {
350+ continue ;
351+ }
352+
353+ // Generate nonce and get the FirstRound
354+ // The nonce is automatically stored in the PSBT
355+ let ( first_round, _pub_nonce) = self
356+ . psbt
357+ . generate_nonce_first_round ( input_index, & xpriv, session_id)
358+ . map_err ( |e| {
359+ WasmUtxoError :: new ( & format ! (
360+ "Failed to generate nonce for input {}: {}" ,
361+ input_index, e
362+ ) )
363+ } ) ?;
364+
365+ // Store the FirstRound for later use in signing
366+ // Use (input_index, xpub) as key so multiple parties can store their FirstRounds
367+ self . first_rounds
368+ . insert ( ( input_index, xpub_str. clone ( ) ) , first_round) ;
369+ }
370+
371+ Ok ( ( ) )
372+ }
373+
265374 /// Finalize all inputs in the PSBT
266375 ///
267376 /// This method attempts to finalize all inputs in the PSBT, computing the final
0 commit comments