Skip to content

Commit e4ecb9c

Browse files
committed
feat: read ZecConsensusBranchId from PSBT unknown map
When utxo-lib serializes a Zcash PSBT, the ZecConsensusBranchId proprietary key ends up in the raw `unknown` map rather than the parsed `proprietary` map when deserialized by rust-bitcoin. This caused KeyNotFound errors during signing when using wasm-utxo with PSBTs created by utxo-lib. Modified get_zec_consensus_branch_id() to check both maps: 1. First check the `proprietary` map (where wasm-utxo stores it) 2. Fall back to the `unknown` map (where utxo-lib stores it) This is a temporary workaround while BitGoJS uses a mix of utxo-lib and wasm-utxo for Zcash PSBT operations. Ticket: BTC-2917
1 parent ca862f7 commit e4ecb9c

1 file changed

Lines changed: 116 additions & 7 deletions

File tree

  • packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt

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

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,21 +133,58 @@ pub fn is_musig2_key(key: &ProprietaryKey) -> bool {
133133
/// The consensus branch ID is stored as a 4-byte little-endian u32 value
134134
/// under the BitGo proprietary key with subtype `ZecConsensusBranchId` (0x00).
135135
///
136+
/// This function checks both the parsed `proprietary` map (where wasm-utxo stores it)
137+
/// and the raw `unknown` map (where utxolib stores it) for compatibility.
138+
///
139+
/// # Temporary Compatibility Note
140+
///
141+
/// The fallback to the `unknown` map is a **temporary workaround** needed because
142+
/// BitGoJS currently uses a mix of `utxo-lib` (TypeScript) and `wasm-utxo` (Rust/WASM)
143+
/// for PSBT operations. When `utxo-lib` serializes a PSBT, it stores proprietary keys
144+
/// in a format that ends up in the raw `unknown` map when deserialized by rust-bitcoin,
145+
/// rather than the parsed `proprietary` map.
146+
///
147+
/// Once BitGoJS fully migrates to `wasm-utxo` for all Zcash PSBT operations, this
148+
/// fallback can be removed and the function can return to only checking `proprietary`.
149+
///
136150
/// # Returns
137151
/// - `Some(u32)` if the consensus branch ID is present and valid
138152
/// - `None` if the key is not present or the value is malformed
139153
pub fn get_zec_consensus_branch_id(psbt: &miniscript::bitcoin::psbt::Psbt) -> Option<u32> {
140-
let kv = find_kv(
154+
// First try the proprietary map (where wasm-utxo stores it)
155+
if let Some(kv) = find_kv(
141156
ProprietaryKeySubtype::ZecConsensusBranchId,
142157
&psbt.proprietary,
143158
)
144-
.next()?;
145-
if kv.value.len() == 4 {
146-
let bytes: [u8; 4] = kv.value.as_slice().try_into().ok()?;
147-
Some(u32::from_le_bytes(bytes))
148-
} else {
149-
None
159+
.next()
160+
{
161+
if kv.value.len() == 4 {
162+
let bytes: [u8; 4] = kv.value.as_slice().try_into().ok()?;
163+
return Some(u32::from_le_bytes(bytes));
164+
}
150165
}
166+
167+
// TEMPORARY: Also check the unknown map (where utxolib stores it as raw key-value pairs)
168+
// This is needed for compatibility while BitGoJS uses a mix of utxo-lib and wasm-utxo.
169+
// The key format from utxolib is: 0xfc + varint(5) + "BITGO" + 0x00
170+
// In rust-bitcoin's raw::Key struct:
171+
// - type_value: u8 = 0xfc (proprietary key type)
172+
// - key: Vec<u8> = [0x05, 'B', 'I', 'T', 'G', 'O', 0x00] (varint len + identifier + subtype)
173+
let expected_key_data: &[u8] = &[
174+
0x05, // length of identifier (varint)
175+
b'B', b'I', b'T', b'G', b'O', // "BITGO"
176+
0x00, // ZecConsensusBranchId subtype
177+
];
178+
179+
for (key, value) in &psbt.unknown {
180+
// Check if this is a proprietary key (0xfc) with the expected key data
181+
if key.type_value == 0xfc && key.key.as_slice() == expected_key_data && value.len() == 4 {
182+
let bytes: [u8; 4] = value.as_slice().try_into().ok()?;
183+
return Some(u32::from_le_bytes(bytes));
184+
}
185+
}
186+
187+
None
151188
}
152189

153190
/// Set Zcash consensus branch ID in PSBT global proprietary map.
@@ -239,4 +276,76 @@ mod tests {
239276
assert_eq!(NetworkUpgrade::Nu5.branch_id(), 0xc2d6d0b4);
240277
assert_eq!(NetworkUpgrade::Nu6.branch_id(), 0xc8e71055);
241278
}
279+
280+
#[test]
281+
fn test_zec_consensus_branch_id_from_unknown_map() {
282+
use crate::zcash::NetworkUpgrade;
283+
use miniscript::bitcoin::psbt::raw::Key;
284+
use miniscript::bitcoin::psbt::Psbt;
285+
use miniscript::bitcoin::Transaction;
286+
287+
// Create a minimal PSBT
288+
let tx = Transaction {
289+
version: miniscript::bitcoin::transaction::Version::TWO,
290+
lock_time: miniscript::bitcoin::locktime::absolute::LockTime::ZERO,
291+
input: vec![],
292+
output: vec![],
293+
};
294+
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();
295+
296+
// Initially no branch ID
297+
assert_eq!(get_zec_consensus_branch_id(&psbt), None);
298+
299+
// Simulate how utxolib stores the consensus branch ID in the unknown map
300+
// In rust-bitcoin's raw::Key struct:
301+
// - type_value: 0xfc (proprietary key type)
302+
// - key: [0x05, 'B', 'I', 'T', 'G', 'O', 0x00] (varint len + identifier + subtype)
303+
let utxolib_key = Key {
304+
type_value: 0xfc, // proprietary key type
305+
key: vec![
306+
0x05, // length of identifier (varint)
307+
b'B', b'I', b'T', b'G', b'O', // "BITGO"
308+
0x00, // ZecConsensusBranchId subtype
309+
],
310+
};
311+
312+
let nu5_branch_id = NetworkUpgrade::Nu5.branch_id();
313+
let value = nu5_branch_id.to_le_bytes().to_vec();
314+
psbt.unknown.insert(utxolib_key, value);
315+
316+
// Should be retrievable from the unknown map
317+
assert_eq!(get_zec_consensus_branch_id(&psbt), Some(nu5_branch_id));
318+
}
319+
320+
#[test]
321+
fn test_zec_consensus_branch_id_proprietary_takes_precedence() {
322+
use crate::zcash::NetworkUpgrade;
323+
use miniscript::bitcoin::psbt::raw::Key;
324+
use miniscript::bitcoin::psbt::Psbt;
325+
use miniscript::bitcoin::Transaction;
326+
327+
// Create a minimal PSBT
328+
let tx = Transaction {
329+
version: miniscript::bitcoin::transaction::Version::TWO,
330+
lock_time: miniscript::bitcoin::locktime::absolute::LockTime::ZERO,
331+
input: vec![],
332+
output: vec![],
333+
};
334+
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();
335+
336+
// Set one value in the unknown map (utxolib format)
337+
let utxolib_key = Key {
338+
type_value: 0xfc,
339+
key: vec![0x05, b'B', b'I', b'T', b'G', b'O', 0x00],
340+
};
341+
let sapling_branch_id = NetworkUpgrade::Sapling.branch_id();
342+
psbt.unknown.insert(utxolib_key, sapling_branch_id.to_le_bytes().to_vec());
343+
344+
// Set a different value in the proprietary map (wasm-utxo format)
345+
let nu5_branch_id = NetworkUpgrade::Nu5.branch_id();
346+
set_zec_consensus_branch_id(&mut psbt, nu5_branch_id);
347+
348+
// The proprietary map should take precedence
349+
assert_eq!(get_zec_consensus_branch_id(&psbt), Some(nu5_branch_id));
350+
}
242351
}

0 commit comments

Comments
 (0)