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
3 changes: 3 additions & 0 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions packages/wasm-utxo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions packages/wasm-utxo/build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
42 changes: 41 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 @@ -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,
Expand Down Expand Up @@ -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<C: secp256k1::Verification>(
&mut self,
secp: &secp256k1::Secp256k1<C>,
Expand Down Expand Up @@ -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());
}
}
107 changes: 107 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 @@ -42,6 +42,7 @@ pub enum ProprietaryKeySubtype {
Musig2PartialSig = 0x03,
PayGoAddressAttestationProof = 0x04,
Bip322Message = 0x05,
WasmUtxoVersion = 0x06,
}

impl ProprietaryKeySubtype {
Expand All @@ -53,6 +54,7 @@ impl ProprietaryKeySubtype {
0x03 => Some(ProprietaryKeySubtype::Musig2PartialSig),
0x04 => Some(ProprietaryKeySubtype::PayGoAddressAttestationProof),
0x05 => Some(ProprietaryKeySubtype::Bip322Message),
0x06 => Some(ProprietaryKeySubtype::WasmUtxoVersion),
_ => None,
}
}
Expand Down Expand Up @@ -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: <version_len: u8><version_bytes><git_hash_bytes (40 hex chars)>
pub fn to_bytes(&self) -> Vec<u8> {
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<Self, String> {
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<u8>) {
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<Self, String> {
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
Expand Down Expand Up @@ -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<u8> = vec![];
assert_eq!(key.key, empty_vec);

let deserialized = WasmUtxoVersionInfo::from_proprietary_kv(&key, &value).unwrap();
assert_eq!(deserialized, version_info);
}
}