diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 4406e29..aed9c25 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 faa1446..469a0d8 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 0000000..b90e872 --- /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"); +} 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 086e7dc..c58dd66 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, @@ -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()); + } } 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 b10547b..c386f33 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); + } }