Skip to content

Commit 9f39008

Browse files
Merge pull request #120 from BitGo/BTC-2866.wasm-psbt-sign
feat(wasm-utxo): improve PSBT API and Taproot signature verification
2 parents 3d88fb8 + 25c507b commit 9f39008

6 files changed

Lines changed: 1088 additions & 13 deletions

File tree

packages/wasm-utxo/js/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,31 @@ declare module "./wasm/wasm_utxo.js" {
6060
}
6161

6262
interface WrapPsbt {
63+
// Signing methods (legacy - kept for backwards compatibility)
6364
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
6465
signWithPrv(this: WrapPsbt, prv: Uint8Array): SignPsbtResult;
66+
67+
// Signing methods (new - using WasmBIP32/WasmECPair)
68+
signAll(this: WrapPsbt, key: WasmBIP32): SignPsbtResult;
69+
signAllWithEcpair(this: WrapPsbt, key: WasmECPair): SignPsbtResult;
70+
71+
// Introspection methods
72+
inputCount(): number;
73+
outputCount(): number;
74+
getPartialSignatures(inputIndex: number): Array<{
75+
pubkey: Uint8Array;
76+
signature: Uint8Array;
77+
}>;
78+
hasPartialSignatures(inputIndex: number): boolean;
79+
80+
// Validation methods
81+
validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean;
82+
verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean;
83+
84+
// Metadata methods
85+
unsignedTxId(): string;
86+
lockTime(): number;
87+
version(): number;
6588
}
6689
}
6790

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

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,16 +2632,42 @@ impl BitGoPsbt {
26322632
) -> Result<bool, String> {
26332633
match self {
26342634
BitGoPsbt::BitcoinLike(psbt, network) => {
2635+
use miniscript::bitcoin::sighash::SighashCache;
2636+
26352637
let input = &psbt.inputs[input_index];
2638+
let mut cache = SighashCache::new(&psbt.unsigned_tx);
26362639

26372640
// Check for Taproot script path signatures first
26382641
if !input.tap_script_sigs.is_empty() {
2639-
return psbt_wallet_input::verify_taproot_script_signature(
2642+
match psbt_wallet_input::verify_taproot_script_signature(
26402643
secp,
26412644
psbt,
26422645
input_index,
26432646
public_key,
2644-
);
2647+
&mut cache,
2648+
) {
2649+
Ok(true) => return Ok(true),
2650+
Ok(false) => {}
2651+
Err(e) => return Err(e),
2652+
}
2653+
}
2654+
2655+
// Check for Taproot key path signature
2656+
if input.tap_key_sig.is_some() {
2657+
let pk = miniscript::bitcoin::PublicKey::from_slice(&public_key.to_bytes())
2658+
.map_err(|e| format!("Failed to convert public key: {}", e))?;
2659+
let (x_only_key, _) = pk.inner.x_only_public_key();
2660+
match psbt_wallet_input::verify_taproot_key_signature(
2661+
secp,
2662+
psbt,
2663+
input_index,
2664+
x_only_key,
2665+
&mut cache,
2666+
) {
2667+
Ok(true) => return Ok(true),
2668+
Ok(false) => {}
2669+
Err(e) => return Err(e),
2670+
}
26452671
}
26462672

26472673
let fork_id = sighash::get_sighash_fork_id(*network);
@@ -2656,17 +2682,43 @@ impl BitGoPsbt {
26562682
)
26572683
}
26582684
BitGoPsbt::Dash(dash_psbt, network) => {
2685+
use miniscript::bitcoin::sighash::SighashCache;
2686+
26592687
let psbt = &dash_psbt.psbt;
26602688
let input = &psbt.inputs[input_index];
2689+
let mut cache = SighashCache::new(&psbt.unsigned_tx);
26612690

26622691
// Check for Taproot script path signatures first
26632692
if !input.tap_script_sigs.is_empty() {
2664-
return psbt_wallet_input::verify_taproot_script_signature(
2693+
match psbt_wallet_input::verify_taproot_script_signature(
26652694
secp,
26662695
psbt,
26672696
input_index,
26682697
public_key,
2669-
);
2698+
&mut cache,
2699+
) {
2700+
Ok(true) => return Ok(true),
2701+
Ok(false) => {}
2702+
Err(e) => return Err(e),
2703+
}
2704+
}
2705+
2706+
// Check for Taproot key path signature
2707+
if input.tap_key_sig.is_some() {
2708+
let pk = miniscript::bitcoin::PublicKey::from_slice(&public_key.to_bytes())
2709+
.map_err(|e| format!("Failed to convert public key: {}", e))?;
2710+
let (x_only_key, _) = pk.inner.x_only_public_key();
2711+
match psbt_wallet_input::verify_taproot_key_signature(
2712+
secp,
2713+
psbt,
2714+
input_index,
2715+
x_only_key,
2716+
&mut cache,
2717+
) {
2718+
Ok(true) => return Ok(true),
2719+
Ok(false) => {}
2720+
Err(e) => return Err(e),
2721+
}
26702722
}
26712723

26722724
let fork_id = sighash::get_sighash_fork_id(*network);

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,23 @@ pub fn derive_pubkey_from_input<C: secp256k1::Verification>(
131131
/// - `psbt`: The PSBT containing the transaction and inputs
132132
/// - `input_index`: The index of the input to verify
133133
/// - `public_key`: The compressed public key to verify the signature for
134+
/// - `cache`: Mutable reference to a SighashCache for computing sighash (can be reused for bulk verification)
134135
///
135136
/// # Returns
136137
/// - `Ok(true)` if a valid Schnorr signature exists for the public key
137138
/// - `Ok(false)` if no signature exists or verification fails
138139
/// - `Err(String)` if required data is missing or computation fails
139-
pub fn verify_taproot_script_signature<C: secp256k1::Verification>(
140+
pub fn verify_taproot_script_signature<
141+
C: secp256k1::Verification,
142+
T: std::borrow::Borrow<miniscript::bitcoin::Transaction>,
143+
>(
140144
secp: &secp256k1::Secp256k1<C>,
141145
psbt: &miniscript::bitcoin::psbt::Psbt,
142146
input_index: usize,
143147
public_key: miniscript::bitcoin::CompressedPublicKey,
148+
cache: &mut miniscript::bitcoin::sighash::SighashCache<T>,
144149
) -> Result<bool, String> {
145-
use miniscript::bitcoin::{
146-
hashes::Hash, sighash::Prevouts, sighash::SighashCache, TapLeafHash, XOnlyPublicKey,
147-
};
150+
use miniscript::bitcoin::{hashes::Hash, sighash::Prevouts, TapLeafHash, XOnlyPublicKey};
148151

149152
let input = &psbt.inputs[input_index];
150153

@@ -160,11 +163,8 @@ pub fn verify_taproot_script_signature<C: secp256k1::Verification>(
160163
for ((sig_pubkey, leaf_hash), signature) in &input.tap_script_sigs {
161164
if sig_pubkey == &x_only_key {
162165
// Found a signature for this public key, now verify it
163-
let mut cache = SighashCache::new(&psbt.unsigned_tx);
164-
165166
// Compute taproot script spend sighash
166-
let prevouts = super::p2tr_musig2_input::collect_prevouts(psbt)
167-
.map_err(|e| format!("Failed to collect prevouts: {}", e))?;
167+
let prevouts = collect_prevouts(psbt)?;
168168

169169
// Find the script for this leaf hash
170170
// tap_scripts is keyed by ControlBlock, so we need to find the matching entry
@@ -206,6 +206,97 @@ pub fn verify_taproot_script_signature<C: secp256k1::Verification>(
206206
Ok(false)
207207
}
208208

209+
/// Collect all prevouts (funding outputs) from PSBT inputs
210+
///
211+
/// This helper extracts the TxOut for each input from either witness_utxo or non_witness_utxo.
212+
/// Required for computing sighashes in taproot transactions.
213+
///
214+
/// # Arguments
215+
/// - `psbt`: The PSBT containing the inputs
216+
///
217+
/// # Returns
218+
/// - `Ok(Vec<TxOut>)` with all prevouts
219+
/// - `Err(String)` if any input is missing UTXO data
220+
pub fn collect_prevouts(
221+
psbt: &miniscript::bitcoin::psbt::Psbt,
222+
) -> Result<Vec<miniscript::bitcoin::TxOut>, String> {
223+
let tx = &psbt.unsigned_tx;
224+
psbt.inputs
225+
.iter()
226+
.enumerate()
227+
.map(|(i, input)| {
228+
if let Some(witness_utxo) = &input.witness_utxo {
229+
Ok(witness_utxo.clone())
230+
} else if let Some(non_witness_utxo) = &input.non_witness_utxo {
231+
let output_index = tx.input[i].previous_output.vout as usize;
232+
non_witness_utxo
233+
.output
234+
.get(output_index)
235+
.cloned()
236+
.ok_or_else(|| format!("Output index {} out of bounds", output_index))
237+
} else {
238+
Err(format!("Missing UTXO data for input {}", i))
239+
}
240+
})
241+
.collect()
242+
}
243+
244+
/// Verifies a Taproot key path signature for a given x-only public key in a PSBT input
245+
///
246+
/// # Arguments
247+
/// - `secp`: Secp256k1 context for signature verification
248+
/// - `psbt`: The PSBT containing the transaction and inputs
249+
/// - `input_index`: The index of the input to verify
250+
/// - `x_only_key`: The x-only public key to verify the signature for
251+
/// - `cache`: Mutable reference to a SighashCache for computing sighash (can be reused for bulk verification)
252+
///
253+
/// # Returns
254+
/// - `Ok(true)` if a valid Schnorr signature exists for the public key
255+
/// - `Ok(false)` if no signature exists or verification fails
256+
/// - `Err(String)` if required data is missing or computation fails
257+
pub fn verify_taproot_key_signature<
258+
C: secp256k1::Verification,
259+
T: std::borrow::Borrow<miniscript::bitcoin::Transaction>,
260+
>(
261+
secp: &secp256k1::Secp256k1<C>,
262+
psbt: &miniscript::bitcoin::psbt::Psbt,
263+
input_index: usize,
264+
x_only_key: miniscript::bitcoin::XOnlyPublicKey,
265+
cache: &mut miniscript::bitcoin::sighash::SighashCache<T>,
266+
) -> Result<bool, String> {
267+
use miniscript::bitcoin::{hashes::Hash, sighash::Prevouts};
268+
269+
let input = &psbt.inputs[input_index];
270+
271+
// Check if there's a taproot key path signature
272+
let sig = match &input.tap_key_sig {
273+
Some(sig) => sig,
274+
None => return Ok(false),
275+
};
276+
277+
// Verify that the tap_internal_key matches the provided x_only_key
278+
match &input.tap_internal_key {
279+
Some(tap_internal_key) if tap_internal_key == &x_only_key => {}
280+
Some(_) => return Ok(false), // Key mismatch
281+
None => return Ok(false), // No tap_internal_key
282+
}
283+
284+
// Collect prevouts for taproot sighash
285+
let prevouts = collect_prevouts(psbt)?;
286+
287+
// Compute taproot key spend sighash
288+
let sighash = cache
289+
.taproot_key_spend_signature_hash(input_index, &Prevouts::All(&prevouts), sig.sighash_type)
290+
.map_err(|e| format!("Failed to compute taproot sighash: {}", e))?;
291+
292+
// Verify Schnorr signature
293+
let message = secp256k1::Message::from_digest(sighash.to_byte_array());
294+
match secp.verify_schnorr(&sig.signature, &message, &x_only_key) {
295+
Ok(()) => Ok(true),
296+
Err(_) => Ok(false),
297+
}
298+
}
299+
209300
/// Verifies an ECDSA signature for a given public key in a PSBT input (legacy/SegWit)
210301
///
211302
/// # Arguments

0 commit comments

Comments
 (0)