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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BitGoPsbt as WasmBitGoPsbt } from "../wasm/wasm_utxo.js";
import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js";
import { type ECPairArg } from "../ecpair.js";
import { ECPair, type ECPairArg } from "../ecpair.js";
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
import {
IRONWOOD_VERSION_GROUP_ID,
Expand Down Expand Up @@ -394,6 +394,40 @@ export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt {
return Array.from(this.wasm.sign_ironwood_v6(wasmKey, keys.wasm), Number);
}

/**
* Check whether transparent input `inputIndex` carries a valid signature by `key`, over the
* ZIP-244 transparent sighash — the digest v6 (Ironwood) keys actually sign (see
* {@link transparentSighash}). The inherited `BitGoPsbt.verifySignature` digests ZIP-243
* (Sapling) instead, so it would report `false` for a valid v6 signature; this override routes
* to the v6 sighash path instead.
*
* Mirrors the inherited signature: an xpub (BIP32Arg) resolves to a public key via the input's
* `bip32_derivation`; a raw key (ECPairArg) verifies with its public key directly.
*
* @param inputIndex - 0-based transparent input index
* @param key - the signing key: an xpub (BIP32Arg: base58 string, BIP32 instance, or WasmBIP32)
* or an ECPairArg (Uint8Array, ECPair instance, or WasmECPair)
* @returns true if a valid signature by the key's public key exists for the input's
* ZIP-244 transparent sighash
* @throws Error if the input index is out of range, the key cannot be parsed, or the v6
* sighash cannot be computed (e.g. the Ironwood PCZT has not been added yet)
*
* @example
* ```typescript
* // Verify the user's signature over the v6 transparent sighash
* const hasUserSig = psbt.verifySignature(0, userXpub);
* ```
*/
override verifySignature(inputIndex: number, key: BIP32Arg | ECPairArg): boolean {
if (isBIP32Arg(key)) {
return this.wasm.verify_ironwood_v6_signature_with_xpub(inputIndex, BIP32.from(key).wasm);
}

// Otherwise it's an ECPairArg (Uint8Array, ECPair, or WasmECPair)
const wasmECPair = ECPair.from(key).wasm;
return this.wasm.verify_ironwood_v6_signature_with_pub(inputIndex, wasmECPair);
}

/**
* Transaction Extractor role: given the external prover's `proof` bytes, finalize the
* transparent inputs, apply the shielded binding signature, and return the broadcast-ready v6
Expand Down
9 changes: 9 additions & 0 deletions packages/wasm-utxo/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use core::fmt;

use crate::fixed_script_wallet::bitgo_psbt::zcash_psbt::VerifyV6SignatureError;
use crate::fixed_script_wallet::bitgo_psbt::ParseTransactionError;

pub trait WasmErrorCode {
Expand All @@ -26,6 +27,7 @@ pub enum WasmUtxoError {
UnifiedAddress(crate::zcash::unified_address::UnifiedAddressError),
ZcashV6(crate::zcash::v6::ZcashV6Error),
Ironwood(crate::zcash::ironwood_build::IronwoodBuildError),
VerifyV6Signature(VerifyV6SignatureError),
}

impl std::error::Error for WasmUtxoError {}
Expand All @@ -38,6 +40,7 @@ impl fmt::Display for WasmUtxoError {
WasmUtxoError::UnifiedAddress(e) => write!(f, "{}", e),
WasmUtxoError::ZcashV6(e) => write!(f, "{}", e),
WasmUtxoError::Ironwood(e) => write!(f, "{}", e),
WasmUtxoError::VerifyV6Signature(e) => write!(f, "{}", e),
}
}
}
Expand All @@ -50,6 +53,7 @@ impl WasmErrorCode for WasmUtxoError {
WasmUtxoError::UnifiedAddress(e) => e.code(),
WasmUtxoError::ZcashV6(e) => e.code(),
WasmUtxoError::Ironwood(e) => e.code(),
WasmUtxoError::VerifyV6Signature(e) => e.code(),
}
}
}
Expand Down Expand Up @@ -107,6 +111,11 @@ impl From<crate::zcash::ironwood_build::IronwoodBuildError> for WasmUtxoError {
WasmUtxoError::Ironwood(err)
}
}
impl From<VerifyV6SignatureError> for WasmUtxoError {
fn from(err: VerifyV6SignatureError) -> Self {
WasmUtxoError::VerifyV6Signature(err)
}
}

impl WasmUtxoError {
pub fn new(s: &str) -> WasmUtxoError {
Expand Down
73 changes: 72 additions & 1 deletion packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3310,6 +3310,13 @@ impl BitGoPsbt {
)
}
BitGoPsbt::Zcash(zcash_psbt, _network) => {
// v6 (Ironwood) transparent inputs are signed over the ZIP-244 digest, not ZIP-243.
// Verifying here would compute a meaningless Sapling digest and report Ok(false) for
// a valid v6 signature — a wrong false that downstream signature counting reads as
// "not signed yet" — so fail loudly instead, matching `ensure_not_ironwood_v6`.
if zcash_psbt.is_ironwood_v6() {
return Err(zcash_psbt::V6_NOT_SUPPORTED_BY_V4_PATH.to_string());
}
// Use Zcash-specific signature verification with ZIP-243 sighash
let branch_id = propkv::get_zec_consensus_branch_id(&zcash_psbt.psbt)
.ok_or("Missing ZecConsensusBranchId in PSBT")?;
Expand Down Expand Up @@ -3347,7 +3354,9 @@ impl BitGoPsbt {
/// # Returns
/// - `Ok(true)` if a valid signature exists for the derived public key
/// - `Ok(false)` if no signature exists for the derived public key
/// - `Err(String)` if the input index is out of bounds, derivation fails, or verification fails
/// - `Err(String)` if the input index is out of bounds, the PSBT is a Zcash v6 (Ironwood)
/// PSBT (ZIP-243 verification is meaningless there; use `verify_v6_signature_with_xpub`),
/// derivation fails, or verification fails
pub fn verify_signature_with_xpub<C: secp256k1::Verification>(
&self,
secp: &secp256k1::Secp256k1<C>,
Expand All @@ -3361,6 +3370,12 @@ impl BitGoPsbt {
return Err(format!("Input index {} out of bounds", input_index));
}

// v6 (Ironwood): every transparent-input key signs over the ZIP-244 digest, not ZIP-243.
// Verifying here would compute a meaningless Sapling digest and report Ok(false) for a
// valid v6 signature — a wrong false that downstream signature counting reads as "not
// signed yet" — so fail loudly instead, regardless of whether the xpub matches.
self.ensure_not_ironwood_v6()?;

let input = &psbt.inputs[input_index];

// Handle MuSig2 inputs early - they use proprietary fields for partial signatures
Expand Down Expand Up @@ -3441,6 +3456,62 @@ impl BitGoPsbt {
self.verify_signature_with_pubkey(secp, input_index, public_key)
}

/// Verify if a valid signature exists for a given public key at the specified input index,
/// computed over the ZIP-244 v6 (Ironwood) transparent sighash — the v6 (Ironwood)
/// counterpart to [`Self::verify_signature_with_pub`], which digests ZIP-243 (Sapling) for
/// Zcash PSBTs and would report `Ok(false)` for a valid v6 signature.
///
/// Only v6 (Ironwood) Zcash PSBTs are supported; every other PSBT type is rejected.
///
/// # Returns
/// - `Ok(true)` if a valid signature exists for the public key
/// - `Ok(false)` if no signature exists for the public key
/// - `Err(VerifyV6SignatureError)` if the PSBT is not Zcash or not v6, the input index is
/// out of bounds, the Ironwood PCZT is absent, or the sighash cannot be computed
pub fn verify_v6_signature_with_pub<C: secp256k1::Verification>(
&self,
secp: &secp256k1::Secp256k1<C>,
input_index: usize,
pubkey: &secp256k1::PublicKey,
) -> Result<bool, zcash_psbt::VerifyV6SignatureError> {
match self {
BitGoPsbt::Zcash(zcash_psbt, _) => {
zcash_psbt.verify_v6_signature_with_pub(secp, input_index, pubkey)
}
_ => Err(zcash_psbt::VerifyV6SignatureError::NotZcash),
}
}

/// Verify if a valid signature exists for an extended public key at the specified input
/// index, computed over the ZIP-244 v6 (Ironwood) transparent sighash — the v6 (Ironwood)
/// counterpart to [`Self::verify_signature_with_xpub`], which digests ZIP-243 (Sapling) for
/// Zcash PSBTs and would report `Ok(false)` for a valid v6 signature.
///
/// The public key is derived from the xpub using the derivation path found in the PSBT
/// input, then verified. Only v6 (Ironwood) Zcash PSBTs are supported; every other PSBT type
/// is rejected.
///
/// # Returns
/// - `Ok(true)` if a valid signature exists for the derived public key
/// - `Ok(false)` if no matching derivation path exists, or no valid signature exists for the
/// derived public key
/// - `Err(VerifyV6SignatureError)` if the PSBT is not Zcash or not v6, the input index is
/// out of bounds, derivation fails, the Ironwood PCZT is absent, or the sighash cannot be
/// computed
pub fn verify_v6_signature_with_xpub<C: secp256k1::Verification>(
&self,
secp: &secp256k1::Secp256k1<C>,
input_index: usize,
xpub: &miniscript::bitcoin::bip32::Xpub,
) -> Result<bool, zcash_psbt::VerifyV6SignatureError> {
match self {
BitGoPsbt::Zcash(zcash_psbt, _) => {
zcash_psbt.verify_v6_signature_with_xpub(secp, input_index, xpub)
}
_ => Err(zcash_psbt::VerifyV6SignatureError::NotZcash),
}
}

/// Parse outputs with wallet keys to identify which outputs belong to a particular wallet.
///
/// This is useful in cases where we want to identify outputs that belong to a different
Expand Down
13 changes: 13 additions & 0 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,19 @@ pub fn set_ironwood_pczt(psbt: &mut miniscript::bitcoin::psbt::Psbt, bytes: Vec<
pub fn get_ironwood_pczt(psbt: &miniscript::bitcoin::psbt::Psbt) -> Option<Vec<u8>> {
get_zec_v6(psbt, ZecV6KeySubtype::IronwoodPczt)
}
/// Whether the PSBT carries an Ironwood (v6) PCZT bundle, without materializing it.
///
/// Use this instead of `get_ironwood_pczt(...).is_some()` when only presence matters: the getter
/// clones the whole (large) PCZT, which would otherwise happen on every check.
pub fn has_ironwood_pczt(psbt: &miniscript::bitcoin::psbt::Psbt) -> bool {
find_kv_iter(
&psbt.proprietary,
BITGO_ZEC_V6,
Some(ZecV6KeySubtype::IronwoodPczt as u8),
)
.next()
.is_some()
}

/// Remove the serialized Ironwood (v6) PCZT bundle, returning whether one was present.
///
Expand Down
Loading
Loading