Skip to content

Commit 6d1e5ad

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add PSBT finalization support for MuSig2
Add finalize_input, finalize_mut, and finalize methods to BitGoPsbt to support proper finalization of P2TR MuSig2 inputs. Also export BitGoKeyValue and related constants from the propkv module. Includes comprehensive tests with fixtures to verify proper finalization and transaction extraction. Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent f328158 commit 6d1e5ad

File tree

3 files changed

+373
-56
lines changed

3 files changed

+373
-56
lines changed

packages/wasm-utxo/src/bitgo_psbt/mod.rs

Lines changed: 198 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ pub use p2tr_musig2_input::{
1212
parse_musig2_nonces, parse_musig2_partial_sigs, parse_musig2_participants, Musig2Error,
1313
Musig2Input, Musig2PartialSig, Musig2Participants, Musig2PubNonce,
1414
};
15+
pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, BITGO};
1516
pub use sighash::validate_sighash_type;
1617

1718
use crate::{bitgo_psbt::zcash_psbt::ZcashPsbt, networks::Network};
18-
use miniscript::bitcoin::psbt::Psbt;
19+
use miniscript::bitcoin::{psbt::Psbt, secp256k1};
1920

2021
#[derive(Debug)]
2122
pub enum DeserializeError {
@@ -146,6 +147,88 @@ impl BitGoPsbt {
146147
BitGoPsbt::Zcash(zcash_psbt, _network) => zcash_psbt.into_bitcoin_psbt(),
147148
}
148149
}
150+
151+
pub fn finalize_input<C: secp256k1::Verification>(
152+
&mut self,
153+
secp: &secp256k1::Secp256k1<C>,
154+
input_index: usize,
155+
) -> Result<(), String> {
156+
use miniscript::psbt::PsbtExt;
157+
158+
match self {
159+
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => {
160+
// 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)
163+
.map_err(|e| e.to_string())?;
164+
return Ok(());
165+
}
166+
// other inputs can be finalized using the standard miniscript::psbt::finalize_input
167+
psbt.finalize_inp_mut(secp, input_index)
168+
.map_err(|e| e.to_string())?;
169+
Ok(())
170+
}
171+
BitGoPsbt::Zcash(_zcash_psbt, _network) => {
172+
todo!("Zcash PSBT finalization not yet implemented");
173+
}
174+
}
175+
}
176+
177+
/// Finalize all inputs in the PSBT, attempting each input even if some fail.
178+
/// Similar to miniscript::psbt::PsbtExt::finalize_mut.
179+
///
180+
/// # Returns
181+
/// - `Ok(())` if all inputs were successfully finalized
182+
/// - `Err(Vec<String>)` containing error messages for each failed input
183+
///
184+
/// # Note
185+
/// This method will attempt to finalize ALL inputs, collecting errors for any that fail.
186+
/// It does not stop at the first error.
187+
pub fn finalize_mut<C: secp256k1::Verification>(
188+
&mut self,
189+
secp: &secp256k1::Secp256k1<C>,
190+
) -> 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+
};
195+
196+
let mut errors = vec![];
197+
for index in 0..num_inputs {
198+
match self.finalize_input(secp, index) {
199+
Ok(()) => {}
200+
Err(e) => {
201+
errors.push(format!("Input {}: {}", index, e));
202+
}
203+
}
204+
}
205+
206+
if errors.is_empty() {
207+
Ok(())
208+
} else {
209+
Err(errors)
210+
}
211+
}
212+
213+
/// Finalize all inputs and consume the PSBT, returning the finalized PSBT.
214+
/// Similar to miniscript::psbt::PsbtExt::finalize.
215+
///
216+
/// # Returns
217+
/// - `Ok(Psbt)` if all inputs were successfully finalized
218+
/// - `Err(String)` containing a formatted error message if any input failed
219+
pub fn finalize<C: secp256k1::Verification>(
220+
mut self,
221+
secp: &secp256k1::Secp256k1<C>,
222+
) -> Result<Psbt, String> {
223+
match self.finalize_mut(secp) {
224+
Ok(()) => Ok(self.into_psbt()),
225+
Err(errors) => Err(format!(
226+
"Failed to finalize {} input(s): {}",
227+
errors.len(),
228+
errors.join("; ")
229+
)),
230+
}
231+
}
149232
}
150233

151234
#[cfg(test)]
@@ -303,46 +386,18 @@ mod tests {
303386
output.script_pubkey.to_hex_string()
304387
}
305388

306-
fn test_wallet_script_type(
307-
script_type: fixtures::ScriptType,
389+
fn assert_matches_wallet_scripts(
308390
network: Network,
309391
tx_format: fixtures::TxFormat,
392+
fixture: &fixtures::PsbtFixture,
393+
wallet_keys: &RootWalletKeys,
394+
input_index: usize,
395+
input_fixture: &fixtures::PsbtInputFixture,
310396
) -> Result<(), String> {
311-
let fixture = fixtures::load_psbt_fixture_with_format(
312-
network.to_utxolib_name(),
313-
fixtures::SignatureState::Fullsigned,
314-
tx_format,
315-
)
316-
.expect("Failed to load fixture");
317-
let xprvs = fixtures::parse_wallet_keys(&fixture).expect("Failed to parse wallet keys");
318-
let secp = crate::bitcoin::secp256k1::Secp256k1::new();
319-
let wallet_keys = RootWalletKeys::new(
320-
xprvs
321-
.iter()
322-
.map(|x| Xpub::from_priv(&secp, x))
323-
.collect::<Vec<_>>()
324-
.try_into()
325-
.expect("Failed to convert to XpubTriple"),
326-
);
327-
328-
// Check if the script type is supported by the network
329-
let output_script_support = network.output_script_support();
330-
let input_fixture = fixture.find_input_with_script_type(script_type);
331-
if !script_type.is_supported_by(&output_script_support) {
332-
// Script type not supported by network - skip test (no fixture expected)
333-
assert!(
334-
input_fixture.is_err(),
335-
"Expected error for unsupported script type"
336-
);
337-
return Ok(());
338-
}
339-
340-
let (input_index, input_fixture) = input_fixture.unwrap();
341-
342397
let (chain, index) =
343398
parse_fixture_paths(input_fixture).expect("Failed to parse fixture paths");
344399
let scripts = WalletScripts::from_wallet_keys(
345-
&wallet_keys,
400+
wallet_keys,
346401
chain,
347402
index,
348403
&network.output_script_support(),
@@ -421,21 +476,96 @@ mod tests {
421476
));
422477
}
423478
}
479+
Ok(())
480+
}
481+
482+
fn assert_finalize_input(
483+
mut bitgo_psbt: BitGoPsbt,
484+
input_index: usize,
485+
_network: Network,
486+
_tx_format: fixtures::TxFormat,
487+
) -> Result<(), String> {
488+
let secp = crate::bitcoin::secp256k1::Secp256k1::new();
489+
bitgo_psbt
490+
.finalize_input(&secp, input_index)
491+
.map_err(|e| e.to_string())?;
492+
Ok(())
493+
}
494+
495+
fn test_wallet_script_type(
496+
script_type: fixtures::ScriptType,
497+
network: Network,
498+
tx_format: fixtures::TxFormat,
499+
) -> 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+
);
517+
518+
// Check if the script type is supported by the network
519+
let output_script_support = network.output_script_support();
520+
let input_fixture = fixture.find_input_with_script_type(script_type);
521+
if !script_type.is_supported_by(&output_script_support) {
522+
// Script type not supported by network - skip test (no fixture expected)
523+
assert!(
524+
input_fixture.is_err(),
525+
"Expected error for unsupported script type"
526+
);
527+
return Ok(());
528+
}
529+
530+
let (input_index, input_fixture) = input_fixture.unwrap();
531+
532+
assert_matches_wallet_scripts(
533+
network,
534+
tx_format,
535+
&fixture,
536+
&wallet_keys,
537+
input_index,
538+
input_fixture,
539+
)?;
540+
541+
assert_finalize_input(
542+
fixture.to_bitgo_psbt(network).unwrap(),
543+
input_index,
544+
network,
545+
tx_format,
546+
)?;
424547

425548
Ok(())
426549
}
427550

428551
crate::test_psbt_fixtures!(test_p2sh_script_generation_from_fixture, network, format, {
429552
test_wallet_script_type(fixtures::ScriptType::P2sh, network, format).unwrap();
430-
});
553+
}, ignore: [
554+
// TODO: sighash support
555+
BitcoinCash, Ecash, BitcoinGold,
556+
// TODO: zec support
557+
Zcash,
558+
]);
431559

432560
crate::test_psbt_fixtures!(
433561
test_p2sh_p2wsh_script_generation_from_fixture,
434562
network,
435563
format,
436564
{
437565
test_wallet_script_type(fixtures::ScriptType::P2shP2wsh, network, format).unwrap();
438-
}
566+
},
567+
// TODO: sighash support
568+
ignore: [BitcoinGold]
439569
);
440570

441571
crate::test_psbt_fixtures!(
@@ -444,7 +574,9 @@ mod tests {
444574
format,
445575
{
446576
test_wallet_script_type(fixtures::ScriptType::P2wsh, network, format).unwrap();
447-
}
577+
},
578+
// TODO: sighash support
579+
ignore: [BitcoinGold]
448580
);
449581

450582
crate::test_psbt_fixtures!(test_p2tr_script_generation_from_fixture, network, format, {
@@ -469,6 +601,34 @@ mod tests {
469601
}
470602
);
471603

604+
crate::test_psbt_fixtures!(test_extract_transaction, network, format, {
605+
let fixture = fixtures::load_psbt_fixture_with_format(
606+
network.to_utxolib_name(),
607+
fixtures::SignatureState::Fullsigned,
608+
format,
609+
)
610+
.expect("Failed to load fixture");
611+
let bitgo_psbt = fixture
612+
.to_bitgo_psbt(network)
613+
.expect("Failed to convert to BitGo PSBT");
614+
let fixture_extracted_transaction = fixture
615+
.extracted_transaction
616+
.expect("Failed to extract transaction");
617+
618+
// // Use BitGoPsbt::finalize() which handles MuSig2 inputs
619+
let secp = crate::bitcoin::secp256k1::Secp256k1::new();
620+
let finalized_psbt = bitgo_psbt.finalize(&secp).expect("Failed to finalize PSBT");
621+
let extracted_transaction = finalized_psbt
622+
.extract_tx()
623+
.expect("Failed to extract transaction");
624+
use miniscript::bitcoin::consensus::serialize;
625+
let extracted_transaction_hex = hex::encode(serialize(&extracted_transaction));
626+
assert_eq!(
627+
extracted_transaction_hex, fixture_extracted_transaction,
628+
"Extracted transaction should match"
629+
);
630+
}, ignore: [BitcoinGold, BitcoinCash, Ecash, Zcash]);
631+
472632
#[test]
473633
fn test_serialize_bitcoin_psbt() {
474634
// Test that Bitcoin-like PSBTs can be serialized

0 commit comments

Comments
 (0)