Skip to content
Closed
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
124 changes: 117 additions & 7 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,58 @@ pub fn is_musig2_key(key: &ProprietaryKey) -> bool {
/// The consensus branch ID is stored as a 4-byte little-endian u32 value
/// under the BitGo proprietary key with subtype `ZecConsensusBranchId` (0x00).
///
/// This function checks both the parsed `proprietary` map (where wasm-utxo stores it)
/// and the raw `unknown` map (where utxolib stores it) for compatibility.
///
/// # Temporary Compatibility Note
///
/// The fallback to the `unknown` map is a **temporary workaround** needed because
/// BitGoJS currently uses a mix of `utxo-lib` (TypeScript) and `wasm-utxo` (Rust/WASM)
/// for PSBT operations. When `utxo-lib` serializes a PSBT, it stores proprietary keys
/// in a format that ends up in the raw `unknown` map when deserialized by rust-bitcoin,
/// rather than the parsed `proprietary` map.
///
/// Once BitGoJS fully migrates to `wasm-utxo` for all Zcash PSBT operations, this
/// fallback can be removed and the function can return to only checking `proprietary`.
///
/// # Returns
/// - `Some(u32)` if the consensus branch ID is present and valid
/// - `None` if the key is not present or the value is malformed
pub fn get_zec_consensus_branch_id(psbt: &miniscript::bitcoin::psbt::Psbt) -> Option<u32> {
let kv = find_kv(
// First try the proprietary map (where wasm-utxo stores it)
if let Some(kv) = find_kv(
ProprietaryKeySubtype::ZecConsensusBranchId,
&psbt.proprietary,
)
.next()?;
if kv.value.len() == 4 {
let bytes: [u8; 4] = kv.value.as_slice().try_into().ok()?;
Some(u32::from_le_bytes(bytes))
} else {
None
.next()
{
if kv.value.len() == 4 {
let bytes: [u8; 4] = kv.value.as_slice().try_into().ok()?;
return Some(u32::from_le_bytes(bytes));
}
}

// TEMPORARY: Also check the unknown map (where utxolib stores it as raw key-value pairs)
// This is needed for compatibility while BitGoJS uses a mix of utxo-lib and wasm-utxo.
// The key format from utxolib is: 0xfc + varint(5) + "BITGO" + 0x00
// In rust-bitcoin's raw::Key struct:
// - type_value: u8 = 0xfc (proprietary key type)
// - key: Vec<u8> = [0x05, 'B', 'I', 'T', 'G', 'O', 0x00] (varint len + identifier + subtype)
let expected_key_data: &[u8] = &[
0x05, // length of identifier (varint)
b'B', b'I', b'T', b'G', b'O', // "BITGO"
0x00, // ZecConsensusBranchId subtype
];

for (key, value) in &psbt.unknown {
// Check if this is a proprietary key (0xfc) with the expected key data
if key.type_value == 0xfc && key.key.as_slice() == expected_key_data && value.len() == 4 {
let bytes: [u8; 4] = value.as_slice().try_into().ok()?;
return Some(u32::from_le_bytes(bytes));
}
}

None
}

/// Set Zcash consensus branch ID in PSBT global proprietary map.
Expand Down Expand Up @@ -239,4 +276,77 @@ mod tests {
assert_eq!(NetworkUpgrade::Nu5.branch_id(), 0xc2d6d0b4);
assert_eq!(NetworkUpgrade::Nu6.branch_id(), 0xc8e71055);
}

#[test]
fn test_zec_consensus_branch_id_from_unknown_map() {
use crate::zcash::NetworkUpgrade;
use miniscript::bitcoin::psbt::raw::Key;
use miniscript::bitcoin::psbt::Psbt;
use miniscript::bitcoin::Transaction;

// Create a minimal PSBT
let tx = Transaction {
version: miniscript::bitcoin::transaction::Version::TWO,
lock_time: miniscript::bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![],
output: vec![],
};
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();

// Initially no branch ID
assert_eq!(get_zec_consensus_branch_id(&psbt), None);

// Simulate how utxolib stores the consensus branch ID in the unknown map
// In rust-bitcoin's raw::Key struct:
// - type_value: 0xfc (proprietary key type)
// - key: [0x05, 'B', 'I', 'T', 'G', 'O', 0x00] (varint len + identifier + subtype)
let utxolib_key = Key {
type_value: 0xfc, // proprietary key type
key: vec![
0x05, // length of identifier (varint)
b'B', b'I', b'T', b'G', b'O', // "BITGO"
0x00, // ZecConsensusBranchId subtype
],
};

let nu5_branch_id = NetworkUpgrade::Nu5.branch_id();
let value = nu5_branch_id.to_le_bytes().to_vec();
psbt.unknown.insert(utxolib_key, value);

// Should be retrievable from the unknown map
assert_eq!(get_zec_consensus_branch_id(&psbt), Some(nu5_branch_id));
}

#[test]
fn test_zec_consensus_branch_id_proprietary_takes_precedence() {
use crate::zcash::NetworkUpgrade;
use miniscript::bitcoin::psbt::raw::Key;
use miniscript::bitcoin::psbt::Psbt;
use miniscript::bitcoin::Transaction;

// Create a minimal PSBT
let tx = Transaction {
version: miniscript::bitcoin::transaction::Version::TWO,
lock_time: miniscript::bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![],
output: vec![],
};
let mut psbt = Psbt::from_unsigned_tx(tx).unwrap();

// Set one value in the unknown map (utxolib format)
let utxolib_key = Key {
type_value: 0xfc,
key: vec![0x05, b'B', b'I', b'T', b'G', b'O', 0x00],
};
let sapling_branch_id = NetworkUpgrade::Sapling.branch_id();
psbt.unknown
.insert(utxolib_key, sapling_branch_id.to_le_bytes().to_vec());

// Set a different value in the proprietary map (wasm-utxo format)
let nu5_branch_id = NetworkUpgrade::Nu5.branch_id();
set_zec_consensus_branch_id(&mut psbt, nu5_branch_id);

// The proprietary map should take precedence
assert_eq!(get_zec_consensus_branch_id(&psbt), Some(nu5_branch_id));
}
}