Skip to content

Commit 3d88fb8

Browse files
Merge pull request #123 from BitGo/BTC-2992.encode-wasm-utxo-version
feat(wasm-utxo): add version tracking to PSBTs
2 parents a61c97d + 1232d00 commit 3d88fb8

5 files changed

Lines changed: 191 additions & 1 deletion

File tree

.github/workflows/build-and-test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
- uses: actions/checkout@v4
2828
with:
2929
ref: ${{ github.event.pull_request.head.sha || github.sha }}
30+
fetch-depth: 0
3031

3132
- name: Install Rust
3233
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
@@ -122,6 +123,7 @@ jobs:
122123
- uses: actions/checkout@v4
123124
with:
124125
ref: ${{ github.event.pull_request.head.sha || github.sha }}
126+
fetch-depth: 0
125127

126128
- name: Install Rust
127129
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
@@ -186,6 +188,7 @@ jobs:
186188
- uses: actions/checkout@v4
187189
with:
188190
ref: ${{ github.event.pull_request.head.sha || github.sha }}
191+
fetch-depth: 0
189192

190193
- name: Setup Node
191194
uses: actions/setup-node@v4

packages/wasm-utxo/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ pastey = "0.1"
3434
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
3535
zebra-chain = { version = "3.1", default-features = false }
3636

37+
[build-dependencies]
38+
serde_json = "1.0"
39+
3740
[profile.release]
3841
# this is required to make webpack happy
3942
# https://github.com/webpack/webpack/issues/15566#issuecomment-2558347645

packages/wasm-utxo/build.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::process::Command;
2+
3+
fn main() {
4+
// Extract version from package.json using proper JSON parsing
5+
let package_json =
6+
std::fs::read_to_string("package.json").expect("Failed to read package.json");
7+
8+
let package: serde_json::Value =
9+
serde_json::from_str(&package_json).expect("Failed to parse package.json as JSON");
10+
11+
let version = package
12+
.get("version")
13+
.and_then(|v| v.as_str())
14+
.expect("Failed to find 'version' field in package.json");
15+
16+
println!("cargo:rustc-env=WASM_UTXO_VERSION={}", version);
17+
18+
// Capture git commit hash
19+
let git_hash = Command::new("git")
20+
.args(["rev-parse", "HEAD"])
21+
.output()
22+
.ok()
23+
.and_then(|output| {
24+
if output.status.success() {
25+
String::from_utf8(output.stdout).ok()
26+
} else {
27+
None
28+
}
29+
})
30+
.map(|s| s.trim().to_string())
31+
.unwrap_or_else(|| "unknown".to_string());
32+
33+
println!("cargo:rustc-env=WASM_UTXO_GIT_HASH={}", git_hash);
34+
35+
// Rerun if package.json changes
36+
println!("cargo:rerun-if-changed=package.json");
37+
}

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub mod zcash_psbt;
1717
use crate::Network;
1818
pub use dash_psbt::DashBitGoPsbt;
1919
use miniscript::bitcoin::{psbt::Psbt, secp256k1, CompressedPublicKey, Txid};
20-
pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, BITGO};
20+
pub use propkv::{BitGoKeyValue, ProprietaryKeySubtype, WasmUtxoVersionInfo, BITGO};
2121
pub use sighash::validate_sighash_type;
2222
pub use zcash_psbt::{
2323
decode_zcash_transaction_meta, ZcashBitGoPsbt, ZcashTransactionMeta,
@@ -1225,6 +1225,17 @@ impl BitGoPsbt {
12251225
}
12261226
}
12271227

1228+
/// Set version information in the PSBT's proprietary fields
1229+
///
1230+
/// This embeds the wasm-utxo version and git hash into the PSBT's global
1231+
/// proprietary fields, allowing identification of which library version
1232+
/// processed the PSBT.
1233+
pub fn set_version_info(&mut self) {
1234+
let version_info = WasmUtxoVersionInfo::from_build_info();
1235+
let (key, value) = version_info.to_proprietary_kv();
1236+
self.psbt_mut().proprietary.insert(key, value);
1237+
}
1238+
12281239
pub fn finalize_input<C: secp256k1::Verification>(
12291240
&mut self,
12301241
secp: &secp256k1::Secp256k1<C>,
@@ -4655,4 +4666,33 @@ mod tests {
46554666
.expect("decode extracted tx");
46564667
assert_eq!(decoded.compute_txid(), extracted_tx.compute_txid());
46574668
}
4669+
4670+
#[test]
4671+
fn test_set_version_info() {
4672+
use crate::fixed_script_wallet::test_utils::get_test_wallet_keys;
4673+
use miniscript::bitcoin::psbt::raw::ProprietaryKey;
4674+
4675+
let wallet_keys =
4676+
crate::fixed_script_wallet::RootWalletKeys::new(get_test_wallet_keys("doge_1e19"));
4677+
4678+
let mut psbt = BitGoPsbt::new(Network::Bitcoin, &wallet_keys, Some(2), Some(0));
4679+
4680+
// Set version info
4681+
psbt.set_version_info();
4682+
4683+
// Verify it was set in the proprietary fields
4684+
let version_key = ProprietaryKey {
4685+
prefix: BITGO.to_vec(),
4686+
subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8,
4687+
key: vec![],
4688+
};
4689+
4690+
assert!(psbt.psbt().proprietary.contains_key(&version_key));
4691+
4692+
// Verify the value is correctly formatted
4693+
let value = psbt.psbt().proprietary.get(&version_key).unwrap();
4694+
let version_info = WasmUtxoVersionInfo::from_bytes(value).unwrap();
4695+
assert!(!version_info.version.is_empty());
4696+
assert!(!version_info.git_hash.is_empty());
4697+
}
46584698
}

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub enum ProprietaryKeySubtype {
4242
Musig2PartialSig = 0x03,
4343
PayGoAddressAttestationProof = 0x04,
4444
Bip322Message = 0x05,
45+
WasmUtxoVersion = 0x06,
4546
}
4647

4748
impl ProprietaryKeySubtype {
@@ -53,6 +54,7 @@ impl ProprietaryKeySubtype {
5354
0x03 => Some(ProprietaryKeySubtype::Musig2PartialSig),
5455
0x04 => Some(ProprietaryKeySubtype::PayGoAddressAttestationProof),
5556
0x05 => Some(ProprietaryKeySubtype::Bip322Message),
57+
0x06 => Some(ProprietaryKeySubtype::WasmUtxoVersion),
5658
_ => None,
5759
}
5860
}
@@ -128,6 +130,85 @@ pub fn is_musig2_key(key: &ProprietaryKey) -> bool {
128130
)
129131
}
130132

133+
/// Version information for wasm-utxo operations on PSBTs
134+
#[derive(Debug, Clone, PartialEq, Eq)]
135+
pub struct WasmUtxoVersionInfo {
136+
pub version: String,
137+
pub git_hash: String,
138+
}
139+
140+
impl WasmUtxoVersionInfo {
141+
/// Create a new version info structure
142+
pub fn new(version: String, git_hash: String) -> Self {
143+
Self { version, git_hash }
144+
}
145+
146+
/// Get the version info from compile-time constants
147+
/// Falls back to "unknown" if build.rs hasn't set the environment variables
148+
pub fn from_build_info() -> Self {
149+
Self {
150+
version: option_env!("WASM_UTXO_VERSION")
151+
.unwrap_or("unknown")
152+
.to_string(),
153+
git_hash: option_env!("WASM_UTXO_GIT_HASH")
154+
.unwrap_or("unknown")
155+
.to_string(),
156+
}
157+
}
158+
159+
/// Serialize to bytes for proprietary key-value storage
160+
/// Format: <version_len: u8><version_bytes><git_hash_bytes (40 hex chars)>
161+
pub fn to_bytes(&self) -> Vec<u8> {
162+
let mut bytes = Vec::new();
163+
let version_bytes = self.version.as_bytes();
164+
bytes.push(version_bytes.len() as u8);
165+
bytes.extend_from_slice(version_bytes);
166+
bytes.extend_from_slice(self.git_hash.as_bytes());
167+
bytes
168+
}
169+
170+
/// Deserialize from bytes
171+
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
172+
if bytes.is_empty() {
173+
return Err("Empty version info bytes".to_string());
174+
}
175+
176+
let version_len = bytes[0] as usize;
177+
if bytes.len() < 1 + version_len {
178+
return Err("Invalid version info: not enough bytes for version".to_string());
179+
}
180+
181+
let version = String::from_utf8(bytes[1..1 + version_len].to_vec())
182+
.map_err(|e| format!("Invalid UTF-8 in version: {}", e))?;
183+
184+
let git_hash = String::from_utf8(bytes[1 + version_len..].to_vec())
185+
.map_err(|e| format!("Invalid UTF-8 in git hash: {}", e))?;
186+
187+
Ok(Self { version, git_hash })
188+
}
189+
190+
/// Convert to proprietary key-value pair for PSBT global fields
191+
pub fn to_proprietary_kv(&self) -> (ProprietaryKey, Vec<u8>) {
192+
let key = ProprietaryKey {
193+
prefix: BITGO.to_vec(),
194+
subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8,
195+
key: vec![], // Empty key data - only one version per PSBT
196+
};
197+
(key, self.to_bytes())
198+
}
199+
200+
/// Create from proprietary key-value pair
201+
pub fn from_proprietary_kv(key: &ProprietaryKey, value: &[u8]) -> Result<Self, String> {
202+
if key.prefix.as_slice() != BITGO {
203+
return Err("Not a BITGO proprietary key".to_string());
204+
}
205+
if key.subtype != ProprietaryKeySubtype::WasmUtxoVersion as u8 {
206+
return Err("Not a WasmUtxoVersion proprietary key".to_string());
207+
}
208+
Self::from_bytes(value)
209+
}
210+
}
211+
131212
/// Extract Zcash consensus branch ID from PSBT global proprietary map.
132213
///
133214
/// The consensus branch ID is stored as a 4-byte little-endian u32 value
@@ -239,4 +320,30 @@ mod tests {
239320
assert_eq!(NetworkUpgrade::Nu5.branch_id(), 0xc2d6d0b4);
240321
assert_eq!(NetworkUpgrade::Nu6.branch_id(), 0xc8e71055);
241322
}
323+
324+
#[test]
325+
fn test_version_info_serialization() {
326+
let version_info =
327+
WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string());
328+
329+
let bytes = version_info.to_bytes();
330+
let deserialized = WasmUtxoVersionInfo::from_bytes(&bytes).unwrap();
331+
332+
assert_eq!(deserialized, version_info);
333+
}
334+
335+
#[test]
336+
fn test_version_info_proprietary_kv() {
337+
let version_info =
338+
WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string());
339+
340+
let (key, value) = version_info.to_proprietary_kv();
341+
assert_eq!(key.prefix, b"BITGO");
342+
assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoVersion as u8);
343+
let empty_vec: Vec<u8> = vec![];
344+
assert_eq!(key.key, empty_vec);
345+
346+
let deserialized = WasmUtxoVersionInfo::from_proprietary_kv(&key, &value).unwrap();
347+
assert_eq!(deserialized, version_info);
348+
}
242349
}

0 commit comments

Comments
 (0)