Skip to content

Commit 25c507b

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): extend PSBT API with signing & introspection methods
Add new methods to PSBT implementation for better handling of signatures: - Add modern signing methods using BIP32/ECPair objects - Add methods to introspect PSBT details (inputs, outputs, signatures) - Add validation methods for signatures including Taproot key path support - Add metadata access methods for transaction properties Issue: BTC-2866 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 7bddfd5 commit 25c507b

6 files changed

Lines changed: 1079 additions & 11 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: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2574,19 +2574,42 @@ impl BitGoPsbt {
25742574
) -> Result<bool, String> {
25752575
match self {
25762576
BitGoPsbt::BitcoinLike(psbt, network) => {
2577+
use miniscript::bitcoin::sighash::SighashCache;
2578+
25772579
let input = &psbt.inputs[input_index];
2580+
let mut cache = SighashCache::new(&psbt.unsigned_tx);
25782581

25792582
// Check for Taproot script path signatures first
25802583
if !input.tap_script_sigs.is_empty() {
2581-
use miniscript::bitcoin::sighash::SighashCache;
2582-
let mut cache = SighashCache::new(&psbt.unsigned_tx);
2583-
return psbt_wallet_input::verify_taproot_script_signature(
2584+
match psbt_wallet_input::verify_taproot_script_signature(
25842585
secp,
25852586
psbt,
25862587
input_index,
25872588
public_key,
25882589
&mut cache,
2589-
);
2590+
) {
2591+
Ok(true) => return Ok(true),
2592+
Ok(false) => {}
2593+
Err(e) => return Err(e),
2594+
}
2595+
}
2596+
2597+
// Check for Taproot key path signature
2598+
if input.tap_key_sig.is_some() {
2599+
let pk = miniscript::bitcoin::PublicKey::from_slice(&public_key.to_bytes())
2600+
.map_err(|e| format!("Failed to convert public key: {}", e))?;
2601+
let (x_only_key, _) = pk.inner.x_only_public_key();
2602+
match psbt_wallet_input::verify_taproot_key_signature(
2603+
secp,
2604+
psbt,
2605+
input_index,
2606+
x_only_key,
2607+
&mut cache,
2608+
) {
2609+
Ok(true) => return Ok(true),
2610+
Ok(false) => {}
2611+
Err(e) => return Err(e),
2612+
}
25902613
}
25912614

25922615
let fork_id = sighash::get_sighash_fork_id(*network);
@@ -2601,20 +2624,43 @@ impl BitGoPsbt {
26012624
)
26022625
}
26032626
BitGoPsbt::Dash(dash_psbt, network) => {
2627+
use miniscript::bitcoin::sighash::SighashCache;
2628+
26042629
let psbt = &dash_psbt.psbt;
26052630
let input = &psbt.inputs[input_index];
2631+
let mut cache = SighashCache::new(&psbt.unsigned_tx);
26062632

26072633
// Check for Taproot script path signatures first
26082634
if !input.tap_script_sigs.is_empty() {
2609-
use miniscript::bitcoin::sighash::SighashCache;
2610-
let mut cache = SighashCache::new(&psbt.unsigned_tx);
2611-
return psbt_wallet_input::verify_taproot_script_signature(
2635+
match psbt_wallet_input::verify_taproot_script_signature(
26122636
secp,
26132637
psbt,
26142638
input_index,
26152639
public_key,
26162640
&mut cache,
2617-
);
2641+
) {
2642+
Ok(true) => return Ok(true),
2643+
Ok(false) => {}
2644+
Err(e) => return Err(e),
2645+
}
2646+
}
2647+
2648+
// Check for Taproot key path signature
2649+
if input.tap_key_sig.is_some() {
2650+
let pk = miniscript::bitcoin::PublicKey::from_slice(&public_key.to_bytes())
2651+
.map_err(|e| format!("Failed to convert public key: {}", e))?;
2652+
let (x_only_key, _) = pk.inner.x_only_public_key();
2653+
match psbt_wallet_input::verify_taproot_key_signature(
2654+
secp,
2655+
psbt,
2656+
input_index,
2657+
x_only_key,
2658+
&mut cache,
2659+
) {
2660+
Ok(true) => return Ok(true),
2661+
Ok(false) => {}
2662+
Err(e) => return Err(e),
2663+
}
26182664
}
26192665

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

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

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ pub fn verify_taproot_script_signature<
164164
if sig_pubkey == &x_only_key {
165165
// Found a signature for this public key, now verify it
166166
// Compute taproot script spend sighash
167-
let prevouts = super::p2tr_musig2_input::collect_prevouts(psbt)
168-
.map_err(|e| format!("Failed to collect prevouts: {}", e))?;
167+
let prevouts = collect_prevouts(psbt)?;
169168

170169
// Find the script for this leaf hash
171170
// tap_scripts is keyed by ControlBlock, so we need to find the matching entry
@@ -207,6 +206,97 @@ pub fn verify_taproot_script_signature<
207206
Ok(false)
208207
}
209208

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+
210300
/// Verifies an ECDSA signature for a given public key in a PSBT input (legacy/SegWit)
211301
///
212302
/// # Arguments

0 commit comments

Comments
 (0)