From 2c1c07689a510be8391db566502999063adc8865 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 30 Jan 2026 11:59:54 +0100 Subject: [PATCH 1/3] feat(wasm-utxo): add WASM version info to PSBTs Create a new WasmUtxoVersionInfo class to encode version and git hash information in PSBTs. This allows tracking which version of the WASM library created or modified a PSBT, supporting debugging and compatibility verification. Issue: BTC-2992 Co-authored-by: llm-git --- .../src/fixed_script_wallet/bitgo_psbt/mod.rs | 2 +- .../fixed_script_wallet/bitgo_psbt/propkv.rs | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs index 086e7dc3..f23ceefc 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs @@ -16,7 +16,7 @@ pub mod zcash_psbt; use crate::Network; pub use dash_psbt::DashBitGoPsbt; use miniscript::bitcoin::{psbt::Psbt, secp256k1, CompressedPublicKey, Txid}; -pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, BITGO}; +pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, WasmUtxoVersionInfo, BITGO}; pub use sighash::validate_sighash_type; pub use zcash_psbt::{ decode_zcash_transaction_meta, ZcashBitGoPsbt, ZcashTransactionMeta, diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs index b10547bd..c386f336 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs @@ -42,6 +42,7 @@ pub enum ProprietaryKeySubtype { Musig2PartialSig = 0x03, PayGoAddressAttestationProof = 0x04, Bip322Message = 0x05, + WasmUtxoVersion = 0x06, } impl ProprietaryKeySubtype { @@ -53,6 +54,7 @@ impl ProprietaryKeySubtype { 0x03 => Some(ProprietaryKeySubtype::Musig2PartialSig), 0x04 => Some(ProprietaryKeySubtype::PayGoAddressAttestationProof), 0x05 => Some(ProprietaryKeySubtype::Bip322Message), + 0x06 => Some(ProprietaryKeySubtype::WasmUtxoVersion), _ => None, } } @@ -128,6 +130,85 @@ pub fn is_musig2_key(key: &ProprietaryKey) -> bool { ) } +/// Version information for wasm-utxo operations on PSBTs +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WasmUtxoVersionInfo { + pub version: String, + pub git_hash: String, +} + +impl WasmUtxoVersionInfo { + /// Create a new version info structure + pub fn new(version: String, git_hash: String) -> Self { + Self { version, git_hash } + } + + /// Get the version info from compile-time constants + /// Falls back to "unknown" if build.rs hasn't set the environment variables + pub fn from_build_info() -> Self { + Self { + version: option_env!("WASM_UTXO_VERSION") + .unwrap_or("unknown") + .to_string(), + git_hash: option_env!("WASM_UTXO_GIT_HASH") + .unwrap_or("unknown") + .to_string(), + } + } + + /// Serialize to bytes for proprietary key-value storage + /// Format: + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + let version_bytes = self.version.as_bytes(); + bytes.push(version_bytes.len() as u8); + bytes.extend_from_slice(version_bytes); + bytes.extend_from_slice(self.git_hash.as_bytes()); + bytes + } + + /// Deserialize from bytes + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err("Empty version info bytes".to_string()); + } + + let version_len = bytes[0] as usize; + if bytes.len() < 1 + version_len { + return Err("Invalid version info: not enough bytes for version".to_string()); + } + + let version = String::from_utf8(bytes[1..1 + version_len].to_vec()) + .map_err(|e| format!("Invalid UTF-8 in version: {}", e))?; + + let git_hash = String::from_utf8(bytes[1 + version_len..].to_vec()) + .map_err(|e| format!("Invalid UTF-8 in git hash: {}", e))?; + + Ok(Self { version, git_hash }) + } + + /// Convert to proprietary key-value pair for PSBT global fields + pub fn to_proprietary_kv(&self) -> (ProprietaryKey, Vec) { + let key = ProprietaryKey { + prefix: BITGO.to_vec(), + subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8, + key: vec![], // Empty key data - only one version per PSBT + }; + (key, self.to_bytes()) + } + + /// Create from proprietary key-value pair + pub fn from_proprietary_kv(key: &ProprietaryKey, value: &[u8]) -> Result { + if key.prefix.as_slice() != BITGO { + return Err("Not a BITGO proprietary key".to_string()); + } + if key.subtype != ProprietaryKeySubtype::WasmUtxoVersion as u8 { + return Err("Not a WasmUtxoVersion proprietary key".to_string()); + } + Self::from_bytes(value) + } +} + /// Extract Zcash consensus branch ID from PSBT global proprietary map. /// /// The consensus branch ID is stored as a 4-byte little-endian u32 value @@ -239,4 +320,30 @@ mod tests { assert_eq!(NetworkUpgrade::Nu5.branch_id(), 0xc2d6d0b4); assert_eq!(NetworkUpgrade::Nu6.branch_id(), 0xc8e71055); } + + #[test] + fn test_version_info_serialization() { + let version_info = + WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string()); + + let bytes = version_info.to_bytes(); + let deserialized = WasmUtxoVersionInfo::from_bytes(&bytes).unwrap(); + + assert_eq!(deserialized, version_info); + } + + #[test] + fn test_version_info_proprietary_kv() { + let version_info = + WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string()); + + let (key, value) = version_info.to_proprietary_kv(); + assert_eq!(key.prefix, b"BITGO"); + assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoVersion as u8); + let empty_vec: Vec = vec![]; + assert_eq!(key.key, empty_vec); + + let deserialized = WasmUtxoVersionInfo::from_proprietary_kv(&key, &value).unwrap(); + assert_eq!(deserialized, version_info); + } } From 50a515238a881d4e6be42f9d7972f32cb5b5eedb Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 30 Jan 2026 12:12:52 +0100 Subject: [PATCH 2/3] feat(wasm-utxo): add version info to PSBT proprietary fields Add set_version_info method to BitGoPsbt that embeds the wasm-utxo version and git hash into the PSBT's proprietary fields. This allows easy identification of which library version processed the PSBT. Issue: BTC-2992 Co-authored-by: llm-git --- .../src/fixed_script_wallet/bitgo_psbt/mod.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs index f23ceefc..c58dd66d 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs @@ -1178,6 +1178,17 @@ impl BitGoPsbt { } } + /// Set version information in the PSBT's proprietary fields + /// + /// This embeds the wasm-utxo version and git hash into the PSBT's global + /// proprietary fields, allowing identification of which library version + /// processed the PSBT. + pub fn set_version_info(&mut self) { + let version_info = WasmUtxoVersionInfo::from_build_info(); + let (key, value) = version_info.to_proprietary_kv(); + self.psbt_mut().proprietary.insert(key, value); + } + pub fn finalize_input( &mut self, secp: &secp256k1::Secp256k1, @@ -4426,4 +4437,33 @@ mod tests { .expect("decode extracted tx"); assert_eq!(decoded.compute_txid(), extracted_tx.compute_txid()); } + + #[test] + fn test_set_version_info() { + use crate::fixed_script_wallet::test_utils::get_test_wallet_keys; + use miniscript::bitcoin::psbt::raw::ProprietaryKey; + + let wallet_keys = + crate::fixed_script_wallet::RootWalletKeys::new(get_test_wallet_keys("doge_1e19")); + + let mut psbt = BitGoPsbt::new(Network::Bitcoin, &wallet_keys, Some(2), Some(0)); + + // Set version info + psbt.set_version_info(); + + // Verify it was set in the proprietary fields + let version_key = ProprietaryKey { + prefix: BITGO.to_vec(), + subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8, + key: vec![], + }; + + assert!(psbt.psbt().proprietary.contains_key(&version_key)); + + // Verify the value is correctly formatted + let value = psbt.psbt().proprietary.get(&version_key).unwrap(); + let version_info = WasmUtxoVersionInfo::from_bytes(value).unwrap(); + assert!(!version_info.version.is_empty()); + assert!(!version_info.git_hash.is_empty()); + } } From 1232d004a3b807ca0ac4ac6e7809a53d4839e17c Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 30 Jan 2026 12:16:05 +0100 Subject: [PATCH 3/3] feat(wasm-utxo): add build script to embed version info at compile time Add a build script that extracts the version from package.json and captures the git commit hash during build. These values are made available as environment variables in the Rust code, providing better traceability for deployments. Also update GitHub workflows to use fetch-depth: 0 to ensure git history is available for the build script. Issue: BTC-2992 Co-authored-by: llm-git --- .github/workflows/build-and-test.yaml | 3 +++ packages/wasm-utxo/Cargo.toml | 3 +++ packages/wasm-utxo/build.rs | 37 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 packages/wasm-utxo/build.rs diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 4406e29f..aed9c258 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -27,6 +27,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 - name: Install Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 @@ -122,6 +123,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 - name: Install Rust uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 @@ -186,6 +188,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 diff --git a/packages/wasm-utxo/Cargo.toml b/packages/wasm-utxo/Cargo.toml index faa14467..469a0d86 100644 --- a/packages/wasm-utxo/Cargo.toml +++ b/packages/wasm-utxo/Cargo.toml @@ -34,6 +34,9 @@ pastey = "0.1" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] zebra-chain = { version = "3.1", default-features = false } +[build-dependencies] +serde_json = "1.0" + [profile.release] # this is required to make webpack happy # https://github.com/webpack/webpack/issues/15566#issuecomment-2558347645 diff --git a/packages/wasm-utxo/build.rs b/packages/wasm-utxo/build.rs new file mode 100644 index 00000000..b90e8726 --- /dev/null +++ b/packages/wasm-utxo/build.rs @@ -0,0 +1,37 @@ +use std::process::Command; + +fn main() { + // Extract version from package.json using proper JSON parsing + let package_json = + std::fs::read_to_string("package.json").expect("Failed to read package.json"); + + let package: serde_json::Value = + serde_json::from_str(&package_json).expect("Failed to parse package.json as JSON"); + + let version = package + .get("version") + .and_then(|v| v.as_str()) + .expect("Failed to find 'version' field in package.json"); + + println!("cargo:rustc-env=WASM_UTXO_VERSION={}", version); + + // Capture git commit hash + let git_hash = Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .ok() + .and_then(|output| { + if output.status.success() { + String::from_utf8(output.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + println!("cargo:rustc-env=WASM_UTXO_GIT_HASH={}", git_hash); + + // Rerun if package.json changes + println!("cargo:rerun-if-changed=package.json"); +}