Skip to content

Commit b12c726

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 b12c726

File tree

2 files changed

+125
-17
lines changed

2 files changed

+125
-17
lines changed

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

Lines changed: 109 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,63 @@ 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,
392454
unsigned_bitgo_psbt: &BitGoPsbt,
455+
halfsigned_bitgo_psbt: &BitGoPsbt,
393456
wallet_keys: &fixtures::XprvTriple,
394457
input_index: usize,
395-
input_fixture: &fixtures::PsbtInputFixture,
396-
halfsigned_fixture: &fixtures::PsbtInputFixture,
397458
) -> Result<(), String> {
398459
let user_key = wallet_keys.user_key();
460+
461+
// Clone the unsigned PSBT and sign with user key
462+
let mut signed_psbt = unsigned_bitgo_psbt.clone();
463+
let secp = secp256k1::Secp256k1::new();
464+
465+
// Sign with user key using the new sign method
466+
signed_psbt
467+
.sign(user_key, &secp)
468+
.map_err(|(_num_keys, errors)| format!("Failed to sign PSBT: {:?}", errors))?;
469+
470+
// Extract partial signatures from the signed input
471+
let signed_input = match &signed_psbt {
472+
BitGoPsbt::BitcoinLike(psbt, _) => &psbt.inputs[input_index],
473+
BitGoPsbt::Zcash(_, _) => {
474+
return Err("Zcash signing not yet implemented".to_string());
475+
}
476+
};
477+
let actual_partial_sigs = signed_input.partial_sigs.clone();
478+
479+
// Get expected partial signatures from halfsigned fixture
480+
let expected_partial_sigs = halfsigned_bitgo_psbt.clone().into_psbt().inputs[input_index]
481+
.partial_sigs
482+
.clone();
483+
484+
assert_eq_partial_signatures(&actual_partial_sigs, &expected_partial_sigs)?;
485+
399486
Ok(())
400487
}
401488

@@ -527,18 +614,23 @@ mod tests {
527614

528615
let psbt_input_stages = psbt_input_stages.unwrap();
529616

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-
)?;
617+
if script_type != fixtures::ScriptType::TaprootKeypath
618+
&& script_type != fixtures::ScriptType::P2trMusig2
619+
&& script_type != fixtures::ScriptType::P2tr
620+
{
621+
assert_half_sign(
622+
&psbt_stages
623+
.unsigned
624+
.to_bitgo_psbt(network)
625+
.expect("Failed to convert to BitGo PSBT"),
626+
&psbt_stages
627+
.halfsigned
628+
.to_bitgo_psbt(network)
629+
.expect("Failed to convert to BitGo PSBT"),
630+
&psbt_input_stages.wallet_keys,
631+
psbt_input_stages.input_index,
632+
)?;
633+
}
542634

543635
assert_full_signed_matches_wallet_scripts(
544636
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)