From febedba7890ca350e10d33920fecbe46777fdd36 Mon Sep 17 00:00:00 2001 From: Luis Covarrubias Date: Thu, 8 Jan 2026 11:03:46 -0800 Subject: [PATCH] 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) The unknown map lookup uses rust-bitcoin's raw::Key struct fields: - type_value: 0xfc (proprietary key type) - key: [0x05, 'B', 'I', 'T', 'G', 'O', 0x00] (varint len + identifier + subtype) This is a temporary workaround while BitGoJS uses a mix of utxo-lib and wasm-utxo for Zcash PSBT operations. Ticket: BTC-2917 --- .../fixed_script_wallet/bitgo_psbt/propkv.rs | 124 +++++++++++++++++- 1 file changed, 117 insertions(+), 7 deletions(-) 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 b10547bd..4f41846a 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 @@ -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 { - 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 = [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. @@ -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)); + } }