Skip to content

Commit c2aa81d

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add sign method to BitGoPsbt
Implement a sign method for the BitGoPsbt struct that wraps the underlying miniscript PSBT signing capability. This allows direct signing of transactions with keys that implement the GetKey trait and is tested with existing fixtures. Issue: BTC-2652 Co-authored-by: llm-git <[email protected]>
1 parent 9169c5a commit c2aa81d

File tree

2 files changed

+129
-17
lines changed

2 files changed

+129
-17
lines changed

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

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,55 @@ 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)]
235275
mod 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};
241282
use miniscript::bitcoin::consensus::Decodable;
242283
use miniscript::bitcoin::Transaction;
@@ -385,17 +426,65 @@ mod tests {
385426
output.script_pubkey.to_hex_string()
386427
}
387428

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+
388452
// ensure we can put the first signature (user signature) on an unsigned PSBT
389453
fn assert_half_sign(
390-
network: Network,
391-
tx_format: fixtures::TxFormat,
454+
_network: Network,
455+
_tx_format: fixtures::TxFormat,
392456
unsigned_bitgo_psbt: &BitGoPsbt,
457+
halfsigned_bitgo_psbt: &BitGoPsbt,
393458
wallet_keys: &fixtures::XprvTriple,
394459
input_index: usize,
395-
input_fixture: &fixtures::PsbtInputFixture,
396-
halfsigned_fixture: &fixtures::PsbtInputFixture,
397460
) -> Result<(), String> {
398461
let user_key = wallet_keys.user_key();
462+
463+
// Clone the unsigned PSBT and sign with user key
464+
let mut signed_psbt = unsigned_bitgo_psbt.clone();
465+
let secp = secp256k1::Secp256k1::new();
466+
467+
// Sign with user key using the new sign method
468+
signed_psbt
469+
.sign(user_key, &secp)
470+
.map_err(|(_num_keys, errors)| format!("Failed to sign PSBT: {:?}", errors))?;
471+
472+
// Extract partial signatures from the signed input
473+
let signed_input = match &signed_psbt {
474+
BitGoPsbt::BitcoinLike(psbt, _) => &psbt.inputs[input_index],
475+
BitGoPsbt::Zcash(_, _) => {
476+
return Err("Zcash signing not yet implemented".to_string());
477+
}
478+
};
479+
let actual_partial_sigs = signed_input.partial_sigs.clone();
480+
481+
// Get expected partial signatures from halfsigned fixture
482+
let expected_partial_sigs = halfsigned_bitgo_psbt.clone().into_psbt().inputs[input_index]
483+
.partial_sigs
484+
.clone();
485+
486+
assert_eq_partial_signatures(&actual_partial_sigs, &expected_partial_sigs)?;
487+
399488
Ok(())
400489
}
401490

@@ -527,18 +616,25 @@ mod tests {
527616

528617
let psbt_input_stages = psbt_input_stages.unwrap();
529618

530-
assert_half_sign(
531-
network,
532-
tx_format,
533-
&psbt_stages
534-
.unsigned
535-
.to_bitgo_psbt(network)
536-
.expect("Failed to convert to BitGo PSBT"),
537-
&psbt_input_stages.wallet_keys,
538-
psbt_input_stages.input_index,
539-
&psbt_input_stages.input_fixture_unsigned,
540-
&psbt_input_stages.input_fixture_halfsigned,
541-
)?;
619+
if script_type != fixtures::ScriptType::TaprootKeypath
620+
&& script_type != fixtures::ScriptType::P2trMusig2
621+
&& script_type != fixtures::ScriptType::P2tr
622+
{
623+
assert_half_sign(
624+
network,
625+
tx_format,
626+
&psbt_stages
627+
.unsigned
628+
.to_bitgo_psbt(network)
629+
.expect("Failed to convert to BitGo PSBT"),
630+
&psbt_stages
631+
.halfsigned
632+
.to_bitgo_psbt(network)
633+
.expect("Failed to convert to BitGo PSBT"),
634+
&psbt_input_stages.wallet_keys,
635+
psbt_input_stages.input_index,
636+
)?;
637+
}
542638

543639
assert_full_signed_matches_wallet_scripts(
544640
network,

packages/wasm-utxo/src/fixed_script_wallet/test_utils/fixtures.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,22 @@ pub enum PsbtInputFixture {
397397
P2shP2pk(P2shP2pkInput),
398398
}
399399

400+
impl PsbtInputFixture {
401+
/// Get partial signatures from PSBT input fixtures that support them.
402+
/// Returns None for input types that don't use ECDSA partial signatures (e.g., Taproot).
403+
pub fn partial_sigs(&self) -> Option<&Vec<PartialSig>> {
404+
match self {
405+
PsbtInputFixture::P2sh(fixture) => Some(&fixture.partial_sig),
406+
PsbtInputFixture::P2shP2wsh(fixture) => Some(&fixture.partial_sig),
407+
PsbtInputFixture::P2wsh(fixture) => Some(&fixture.partial_sig),
408+
PsbtInputFixture::P2shP2pk(fixture) => Some(&fixture.partial_sig),
409+
PsbtInputFixture::P2trLegacy(_)
410+
| PsbtInputFixture::P2trMusig2ScriptPath(_)
411+
| PsbtInputFixture::P2trMusig2KeyPath(_) => None,
412+
}
413+
}
414+
}
415+
400416
// Finalized input type structs (depend on helper types above)
401417

402418
#[derive(Debug, Clone, Deserialize, Serialize)]

0 commit comments

Comments
 (0)