Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,31 @@ declare module "./wasm/wasm_utxo.js" {
}

interface WrapPsbt {
// Signing methods (legacy - kept for backwards compatibility)
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
signWithPrv(this: WrapPsbt, prv: Uint8Array): SignPsbtResult;

// Signing methods (new - using WasmBIP32/WasmECPair)
signAll(this: WrapPsbt, key: WasmBIP32): SignPsbtResult;
signAllWithEcpair(this: WrapPsbt, key: WasmECPair): SignPsbtResult;

// Introspection methods
inputCount(): number;
outputCount(): number;
getPartialSignatures(inputIndex: number): Array<{
pubkey: Uint8Array;
signature: Uint8Array;
}>;
hasPartialSignatures(inputIndex: number): boolean;

// Validation methods
validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean;
verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean;

// Metadata methods
unsignedTxId(): string;
lockTime(): number;
version(): number;
}
}

Expand Down
60 changes: 56 additions & 4 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2574,16 +2574,42 @@ impl BitGoPsbt {
) -> Result<bool, String> {
match self {
BitGoPsbt::BitcoinLike(psbt, network) => {
use miniscript::bitcoin::sighash::SighashCache;

let input = &psbt.inputs[input_index];
let mut cache = SighashCache::new(&psbt.unsigned_tx);

// Check for Taproot script path signatures first
if !input.tap_script_sigs.is_empty() {
return psbt_wallet_input::verify_taproot_script_signature(
match psbt_wallet_input::verify_taproot_script_signature(
secp,
psbt,
input_index,
public_key,
);
&mut cache,
) {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(e) => return Err(e),
}
}

// Check for Taproot key path signature
if input.tap_key_sig.is_some() {
let pk = miniscript::bitcoin::PublicKey::from_slice(&public_key.to_bytes())
.map_err(|e| format!("Failed to convert public key: {}", e))?;
let (x_only_key, _) = pk.inner.x_only_public_key();
match psbt_wallet_input::verify_taproot_key_signature(
secp,
psbt,
input_index,
x_only_key,
&mut cache,
) {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(e) => return Err(e),
}
}

let fork_id = sighash::get_sighash_fork_id(*network);
Expand All @@ -2598,17 +2624,43 @@ impl BitGoPsbt {
)
}
BitGoPsbt::Dash(dash_psbt, network) => {
use miniscript::bitcoin::sighash::SighashCache;

let psbt = &dash_psbt.psbt;
let input = &psbt.inputs[input_index];
let mut cache = SighashCache::new(&psbt.unsigned_tx);

// Check for Taproot script path signatures first
if !input.tap_script_sigs.is_empty() {
return psbt_wallet_input::verify_taproot_script_signature(
match psbt_wallet_input::verify_taproot_script_signature(
secp,
psbt,
input_index,
public_key,
);
&mut cache,
) {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(e) => return Err(e),
}
}

// Check for Taproot key path signature
if input.tap_key_sig.is_some() {
let pk = miniscript::bitcoin::PublicKey::from_slice(&public_key.to_bytes())
.map_err(|e| format!("Failed to convert public key: {}", e))?;
let (x_only_key, _) = pk.inner.x_only_public_key();
match psbt_wallet_input::verify_taproot_key_signature(
secp,
psbt,
input_index,
x_only_key,
&mut cache,
) {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(e) => return Err(e),
}
}

let fork_id = sighash::get_sighash_fork_id(*network);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,20 +131,23 @@ pub fn derive_pubkey_from_input<C: secp256k1::Verification>(
/// - `psbt`: The PSBT containing the transaction and inputs
/// - `input_index`: The index of the input to verify
/// - `public_key`: The compressed public key to verify the signature for
/// - `cache`: Mutable reference to a SighashCache for computing sighash (can be reused for bulk verification)
///
/// # Returns
/// - `Ok(true)` if a valid Schnorr signature exists for the public key
/// - `Ok(false)` if no signature exists or verification fails
/// - `Err(String)` if required data is missing or computation fails
pub fn verify_taproot_script_signature<C: secp256k1::Verification>(
pub fn verify_taproot_script_signature<
C: secp256k1::Verification,
T: std::borrow::Borrow<miniscript::bitcoin::Transaction>,
>(
secp: &secp256k1::Secp256k1<C>,
psbt: &miniscript::bitcoin::psbt::Psbt,
input_index: usize,
public_key: miniscript::bitcoin::CompressedPublicKey,
cache: &mut miniscript::bitcoin::sighash::SighashCache<T>,
) -> Result<bool, String> {
use miniscript::bitcoin::{
hashes::Hash, sighash::Prevouts, sighash::SighashCache, TapLeafHash, XOnlyPublicKey,
};
use miniscript::bitcoin::{hashes::Hash, sighash::Prevouts, TapLeafHash, XOnlyPublicKey};

let input = &psbt.inputs[input_index];

Expand All @@ -160,11 +163,8 @@ pub fn verify_taproot_script_signature<C: secp256k1::Verification>(
for ((sig_pubkey, leaf_hash), signature) in &input.tap_script_sigs {
if sig_pubkey == &x_only_key {
// Found a signature for this public key, now verify it
let mut cache = SighashCache::new(&psbt.unsigned_tx);

// Compute taproot script spend sighash
let prevouts = super::p2tr_musig2_input::collect_prevouts(psbt)
.map_err(|e| format!("Failed to collect prevouts: {}", e))?;
let prevouts = collect_prevouts(psbt)?;

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

/// Collect all prevouts (funding outputs) from PSBT inputs
///
/// This helper extracts the TxOut for each input from either witness_utxo or non_witness_utxo.
/// Required for computing sighashes in taproot transactions.
///
/// # Arguments
/// - `psbt`: The PSBT containing the inputs
///
/// # Returns
/// - `Ok(Vec<TxOut>)` with all prevouts
/// - `Err(String)` if any input is missing UTXO data
pub fn collect_prevouts(
psbt: &miniscript::bitcoin::psbt::Psbt,
) -> Result<Vec<miniscript::bitcoin::TxOut>, String> {
let tx = &psbt.unsigned_tx;
psbt.inputs
.iter()
.enumerate()
.map(|(i, input)| {
if let Some(witness_utxo) = &input.witness_utxo {
Ok(witness_utxo.clone())
} else if let Some(non_witness_utxo) = &input.non_witness_utxo {
let output_index = tx.input[i].previous_output.vout as usize;
non_witness_utxo
.output
.get(output_index)
.cloned()
.ok_or_else(|| format!("Output index {} out of bounds", output_index))
} else {
Err(format!("Missing UTXO data for input {}", i))
}
})
.collect()
}

/// Verifies a Taproot key path signature for a given x-only public key in a PSBT input
///
/// # Arguments
/// - `secp`: Secp256k1 context for signature verification
/// - `psbt`: The PSBT containing the transaction and inputs
/// - `input_index`: The index of the input to verify
/// - `x_only_key`: The x-only public key to verify the signature for
/// - `cache`: Mutable reference to a SighashCache for computing sighash (can be reused for bulk verification)
///
/// # Returns
/// - `Ok(true)` if a valid Schnorr signature exists for the public key
/// - `Ok(false)` if no signature exists or verification fails
/// - `Err(String)` if required data is missing or computation fails
pub fn verify_taproot_key_signature<
C: secp256k1::Verification,
T: std::borrow::Borrow<miniscript::bitcoin::Transaction>,
>(
secp: &secp256k1::Secp256k1<C>,
psbt: &miniscript::bitcoin::psbt::Psbt,
input_index: usize,
x_only_key: miniscript::bitcoin::XOnlyPublicKey,
cache: &mut miniscript::bitcoin::sighash::SighashCache<T>,
) -> Result<bool, String> {
use miniscript::bitcoin::{hashes::Hash, sighash::Prevouts};

let input = &psbt.inputs[input_index];

// Check if there's a taproot key path signature
let sig = match &input.tap_key_sig {
Some(sig) => sig,
None => return Ok(false),
};

// Verify that the tap_internal_key matches the provided x_only_key
match &input.tap_internal_key {
Some(tap_internal_key) if tap_internal_key == &x_only_key => {}
Some(_) => return Ok(false), // Key mismatch
None => return Ok(false), // No tap_internal_key
}

// Collect prevouts for taproot sighash
let prevouts = collect_prevouts(psbt)?;

// Compute taproot key spend sighash
let sighash = cache
.taproot_key_spend_signature_hash(input_index, &Prevouts::All(&prevouts), sig.sighash_type)
.map_err(|e| format!("Failed to compute taproot sighash: {}", e))?;

// Verify Schnorr signature
let message = secp256k1::Message::from_digest(sighash.to_byte_array());
match secp.verify_schnorr(&sig.signature, &message, &x_only_key) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}

/// Verifies an ECDSA signature for a given public key in a PSBT input (legacy/SegWit)
///
/// # Arguments
Expand Down
Loading