Skip to content

Commit 0e88949

Browse files
Merge pull request #299 from BitGo/otto/T1-3607-combine-signatures
feat(wasm-utxo): add BitGoPsbt.combineInputs for raw PSBT merge
2 parents 1d5845f + 06a6111 commit 0e88949

5 files changed

Lines changed: 383 additions & 3 deletions

File tree

packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,22 @@ export class BitGoPsbt extends PsbtBase<WasmBitGoPsbt> implements IPsbtWithAddre
931931
this._wasm.combine_musig2_nonces(sourcePsbt.wasm);
932932
}
933933

934+
/**
935+
* Merge all input fields from a raw PSBT (bytes) into this PSBT.
936+
*
937+
* The source bytes are parsed with the underlying bitcoin PSBT deserializer,
938+
* bypassing network-specific validation (e.g. ZCash consensusBranchId), so the
939+
* source may be a stripped PSBT that lacks those fields.
940+
*
941+
* Copies per input: partial_sigs, tap_key_sig, tap_script_sigs, proprietary.
942+
*
943+
* @param otherPsbtBytes - Raw bytes of the PSBT to merge signatures from
944+
* @throws Error if PSBT parsing fails or input counts don't match
945+
*/
946+
combineInputs(otherPsbtBytes: Uint8Array): void {
947+
this._wasm.combine_inputs(otherPsbtBytes);
948+
}
949+
934950
/**
935951
* Finalize all inputs in the PSBT
936952
*

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,52 @@ impl BitGoPsbt {
13001300
Ok(())
13011301
}
13021302

1303+
/// Merge all input fields from a raw PSBT (given as bytes) into this PSBT.
1304+
///
1305+
/// For Zcash PSBTs the source is parsed with the ZEC-aware deserializer (skipping
1306+
/// the ZecConsensusBranchId check so that stripped HSM responses are accepted).
1307+
/// For all other coins the bitcoin PSBT deserializer is used.
1308+
///
1309+
/// Copies per input: partial_sigs, tap_key_sig, tap_script_sigs, proprietary.
1310+
pub fn combine_inputs(&mut self, other_bytes: &[u8]) -> Result<(), String> {
1311+
let raw: Psbt = match self {
1312+
BitGoPsbt::Zcash(_, network) => {
1313+
ZcashBitGoPsbt::deserialize_stripped(other_bytes, *network)
1314+
.map(|z| z.psbt)
1315+
.map_err(|e| format!("Failed to parse PSBT: {}", e))?
1316+
}
1317+
_ => Psbt::deserialize(other_bytes)
1318+
.map_err(|e| format!("Failed to parse PSBT: {}", e))?,
1319+
};
1320+
1321+
let dest = self.psbt_mut();
1322+
1323+
if raw.inputs.len() != dest.inputs.len() {
1324+
return Err(format!(
1325+
"PSBT input count mismatch: source has {}, destination has {}",
1326+
raw.inputs.len(),
1327+
dest.inputs.len()
1328+
));
1329+
}
1330+
1331+
for (src_in, dest_in) in raw.inputs.iter().zip(dest.inputs.iter_mut()) {
1332+
for (k, v) in &src_in.partial_sigs {
1333+
dest_in.partial_sigs.insert(*k, *v);
1334+
}
1335+
if let Some(sig) = src_in.tap_key_sig {
1336+
dest_in.tap_key_sig = Some(sig);
1337+
}
1338+
for (k, v) in &src_in.tap_script_sigs {
1339+
dest_in.tap_script_sigs.insert(*k, *v);
1340+
}
1341+
for (k, v) in &src_in.proprietary {
1342+
dest_in.proprietary.insert(k.clone(), v.clone());
1343+
}
1344+
}
1345+
1346+
Ok(())
1347+
}
1348+
13031349
/// Serialize the PSBT to bytes, using network-specific logic
13041350
pub fn serialize(&self) -> Result<Vec<u8>, SerializeError> {
13051351
match self {

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,21 @@ impl ZcashBitGoPsbt {
204204
Ok(hash.to_byte_array())
205205
}
206206

207+
/// Deserialize a Zcash PSBT from bytes without requiring the ZecConsensusBranchId
208+
/// proprietary key. Used when combining with a stripped HSM response that may not
209+
/// carry the branch ID (the key is only needed for sighash, not for merging sigs).
210+
pub(crate) fn deserialize_stripped(
211+
bytes: &[u8],
212+
network: crate::Network,
213+
) -> Result<Self, super::DeserializeError> {
214+
Self::decode_with_zcash_tx(bytes, network, false)
215+
}
216+
207217
/// Deserialize the PSBT by converting the Zcash transaction to Bitcoin format first
208218
fn decode_with_zcash_tx(
209219
bytes: &[u8],
210220
network: crate::Network,
221+
require_branch_id: bool,
211222
) -> Result<Self, super::DeserializeError> {
212223
let mut r = bytes;
213224

@@ -342,7 +353,7 @@ impl ZcashBitGoPsbt {
342353
let psbt = Psbt::deserialize(&modified_psbt)?;
343354

344355
// Consensus branch ID must be set in the PSBT proprietary map
345-
if super::propkv::get_zec_consensus_branch_id(&psbt).is_none() {
356+
if require_branch_id && super::propkv::get_zec_consensus_branch_id(&psbt).is_none() {
346357
return Err(super::DeserializeError::Network(
347358
"Missing ZecConsensusBranchId in PSBT proprietary map".to_string(),
348359
));
@@ -366,7 +377,7 @@ impl ZcashBitGoPsbt {
366377
bytes: &[u8],
367378
network: crate::Network,
368379
) -> Result<Self, super::DeserializeError> {
369-
Self::decode_with_zcash_tx(bytes, network)
380+
Self::decode_with_zcash_tx(bytes, network, true)
370381
}
371382

372383
/// Convert to a standard Bitcoin PSBT (losing Zcash-specific fields)

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,6 @@ impl BitGoPsbt {
971971
_ => None,
972972
}
973973
}
974-
975974
pub fn get_outputs_with_address(&self) -> Result<JsValue, WasmUtxoError> {
976975
crate::wasm::psbt::get_outputs_with_address_from_psbt(self.psbt.psbt(), self.psbt.network())
977976
}
@@ -1816,6 +1815,20 @@ impl BitGoPsbt {
18161815
.map_err(|e| WasmUtxoError::new(&format!("Failed to combine PSBTs: {}", e)))
18171816
}
18181817

1818+
/// Merge all input fields from a raw PSBT (given as bytes) into this PSBT.
1819+
///
1820+
/// The source bytes are parsed with the underlying bitcoin PSBT deserializer,
1821+
/// bypassing any network-specific validation, so the source may be a partial
1822+
/// PSBT that lacks fields like ZecConsensusBranchId.
1823+
///
1824+
/// # Errors
1825+
/// Returns error if PSBT parsing fails or input counts don't match
1826+
pub fn combine_inputs(&mut self, other_bytes: &[u8]) -> Result<(), WasmUtxoError> {
1827+
self.psbt
1828+
.combine_inputs(other_bytes)
1829+
.map_err(|e| WasmUtxoError::new(&format!("Failed to combine inputs: {}", e)))
1830+
}
1831+
18191832
/// Finalize all inputs in the PSBT
18201833
///
18211834
/// This method attempts to finalize all inputs in the PSBT, computing the final

0 commit comments

Comments
 (0)