Skip to content

Commit 97433ac

Browse files
Merge pull request #275 from BitGo/BTC-2650.support-nonstandard-derivation-paths
feat(wasm-utxo): support nonstandard derivation paths
2 parents beca735 + 91f37a9 commit 97433ac

18 files changed

Lines changed: 725 additions & 712 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,23 @@ export type ParsedInput = {
3636
address: string;
3737
script: Uint8Array;
3838
value: bigint;
39+
/** Set only when the derivation path is chain-standard (chain code encodes script type per BitGo convention). */
3940
scriptId: ScriptId | null;
4041
scriptType: InputScriptType;
4142
sequence: number;
43+
/** Full BIP32 derivation path from the wallet xpub (e.g. "0/1"). Null for replay-protection inputs. */
44+
derivationPath: string | null;
4245
};
4346

4447
export type ParsedOutput = {
4548
address: string | null;
4649
script: Uint8Array;
4750
value: bigint;
51+
/** Set only when the derivation path is chain-standard (chain code encodes script type per BitGo convention). */
4852
scriptId: ScriptId | null;
4953
paygo: boolean;
54+
/** Full BIP32 derivation path from the wallet xpub (e.g. "0/1"). Null for external outputs. */
55+
derivationPath: string | null;
5056
};
5157

5258
export type ParsedTransaction = {

packages/wasm-utxo/src/bip322/bitgo_psbt.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::fixed_script_wallet::bitgo_psbt::{
88
create_bip32_derivation, create_tap_bip32_derivation, find_kv, BitGoKeyValue, BitGoPsbt,
99
ProprietaryKeySubtype,
1010
};
11+
use crate::fixed_script_wallet::wallet_scripts::chain_index_path;
1112
use crate::fixed_script_wallet::wallet_scripts::{
1213
build_multisig_script_2_of_3, build_p2tr_ns_script, ScriptP2mr, ScriptP2tr,
1314
};
@@ -88,8 +89,8 @@ pub fn add_bip322_input(
8889
let chain_enum = Chain::try_from(chain).map_err(|e| format!("Invalid chain: {}", e))?;
8990
let scripts = WalletScripts::from_wallet_keys(
9091
wallet_keys,
91-
chain_enum,
92-
index,
92+
chain_enum.script_type,
93+
&chain_index_path(chain, index),
9394
&network.output_script_support(),
9495
)
9596
.map_err(|e| e.to_string())?;
@@ -190,7 +191,7 @@ pub fn add_bip322_input(
190191

191192
// Derive pubkeys
192193
let derived_keys = wallet_keys
193-
.derive_for_chain_and_index(chain, index)
194+
.derive_path(&chain_index_path(chain, index))
194195
.map_err(|e| format!("Failed to derive keys: {}", e))?;
195196
let pub_triple = to_pub_triple(&derived_keys);
196197

@@ -335,8 +336,8 @@ pub fn verify_bip322_tx_input(
335336
let chain_enum = Chain::try_from(chain).map_err(|e| format!("Invalid chain: {}", e))?;
336337
let scripts = WalletScripts::from_wallet_keys(
337338
wallet_keys,
338-
chain_enum,
339-
index,
339+
chain_enum.script_type,
340+
&chain_index_path(chain, index),
340341
&network.output_script_support(),
341342
)
342343
.map_err(|e| e.to_string())?;
@@ -427,8 +428,8 @@ pub fn verify_bip322_psbt_input(
427428
let chain_enum = Chain::try_from(chain).map_err(|e| format!("Invalid chain: {}", e))?;
428429
let scripts = WalletScripts::from_wallet_keys(
429430
wallet_keys,
430-
chain_enum,
431-
index,
431+
chain_enum.script_type,
432+
&chain_index_path(chain, index),
432433
&network.output_script_support(),
433434
)
434435
.map_err(|e| e.to_string())?;

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

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ 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::{find_kv, BitGoKeyValue, ProprietaryKeySubtype, WasmUtxoVersionInfo, BITGO};
21-
pub use sighash::validate_sighash_type;
20+
pub use propkv::{
21+
find_kv, get_zec_consensus_branch_id, BitGoKeyValue, ProprietaryKeySubtype,
22+
WasmUtxoVersionInfo, BITGO,
23+
};
24+
pub use sighash::{get_sighash_fork_id, validate_sighash_type};
2225
pub use zcash_psbt::{
2326
decode_zcash_transaction_meta, ZcashBitGoPsbt, ZcashTransactionMeta,
2427
ZCASH_SAPLING_VERSION_GROUP_ID,
@@ -104,15 +107,16 @@ pub enum BitGoPsbt {
104107
}
105108

106109
// Re-export types from submodules for convenience
110+
pub use crate::fixed_script_wallet::{ScriptId, ScriptIdWithValue};
107111
pub use psbt_wallet_input::{
108-
InputScriptType, ParsedInput, ReplayProtectionOptions, ScriptId, WalletInputOptions,
112+
InputScriptType, ParsedInput, ReplayProtectionOptions, WalletInputOptions,
109113
};
110114
pub use psbt_wallet_output::ParsedOutput;
111115

112116
/// Describes a single input for `from_half_signed_legacy_transaction`.
113117
pub enum HydrationUnspentInput {
114118
/// A regular wallet input with derivation chain, index, and value.
115-
Wallet(psbt_wallet_input::ScriptIdWithValue),
119+
Wallet(ScriptIdWithValue),
116120
/// A P2SH-P2PK replay protection input. The caller provides the expected pubkey so it can be
117121
/// validated against the redeemScript embedded in the legacy transaction.
118122
ReplayProtection {
@@ -184,7 +188,7 @@ impl std::error::Error for ParseTransactionError {}
184188
/// Get the default sighash type for a network and chain type
185189
fn get_default_sighash_type(
186190
network: Network,
187-
chain: crate::fixed_script_wallet::wallet_scripts::Chain,
191+
chain: crate::fixed_script_wallet::Chain,
188192
) -> miniscript::bitcoin::psbt::PsbtSighashType {
189193
use crate::fixed_script_wallet::wallet_scripts::OutputScriptType;
190194
use miniscript::bitcoin::sighash::{EcdsaSighashType, TapSighashType};
@@ -503,7 +507,7 @@ impl BitGoPsbt {
503507
for (i, (tx_in, unspent)) in tx.input.iter().zip(unspents.iter()).enumerate() {
504508
match unspent {
505509
HydrationUnspentInput::Wallet(sv) => {
506-
let script_id = psbt_wallet_input::ScriptId {
510+
let script_id = ScriptId {
507511
chain: sv.chain,
508512
index: sv.index,
509513
};
@@ -894,11 +898,14 @@ impl BitGoPsbt {
894898
vout: u32,
895899
value: u64,
896900
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
897-
script_id: psbt_wallet_input::ScriptId,
901+
script_id: ScriptId,
898902
options: WalletInputOptions,
899903
) -> Result<(), String> {
900904
use crate::fixed_script_wallet::to_pub_triple;
901-
use crate::fixed_script_wallet::wallet_scripts::{Chain, OutputScriptType, WalletScripts};
905+
use crate::fixed_script_wallet::wallet_scripts::{
906+
chain_index_path, OutputScriptType, WalletScripts,
907+
};
908+
use crate::fixed_script_wallet::Chain;
902909
use miniscript::bitcoin::psbt::Input;
903910
use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash};
904911
use miniscript::bitcoin::{transaction::Sequence, Amount, OutPoint, TxIn, TxOut};
@@ -911,12 +918,12 @@ impl BitGoPsbt {
911918
let chain_enum = Chain::try_from(chain)?;
912919

913920
let derived_keys = wallet_keys
914-
.derive_for_chain_and_index(chain, derivation_index)
921+
.derive_path(&chain_index_path(chain, derivation_index))
915922
.map_err(|e| format!("Failed to derive keys: {}", e))?;
916923
let pub_triple = to_pub_triple(&derived_keys);
917924

918925
let script_support = network.output_script_support();
919-
let scripts = WalletScripts::new(&pub_triple, chain_enum, &script_support)
926+
let scripts = WalletScripts::new(&pub_triple, chain_enum.script_type, &script_support)
920927
.map_err(|e| format!("Failed to create wallet scripts: {}", e))?;
921928

922929
let output_script = scripts.output_script();
@@ -1046,7 +1053,7 @@ impl BitGoPsbt {
10461053
vout: u32,
10471054
value: u64,
10481055
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
1049-
script_id: psbt_wallet_input::ScriptId,
1056+
script_id: ScriptId,
10501057
options: WalletInputOptions,
10511058
) -> Result<usize, String> {
10521059
let network = self.network();
@@ -1070,7 +1077,7 @@ impl BitGoPsbt {
10701077
vout: u32,
10711078
value: u64,
10721079
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
1073-
script_id: psbt_wallet_input::ScriptId,
1080+
script_id: ScriptId,
10741081
options: WalletInputOptions,
10751082
) -> Result<usize, String> {
10761083
let index = self.psbt().inputs.len();
@@ -1101,8 +1108,10 @@ impl BitGoPsbt {
11011108
) -> Result<usize, String> {
11021109
use crate::fixed_script_wallet::to_pub_triple;
11031110
use crate::fixed_script_wallet::wallet_scripts::{
1104-
build_tap_tree_for_output, create_tap_bip32_derivation_for_output, Chain, WalletScripts,
1111+
build_tap_tree_for_output, chain_index_path, create_tap_bip32_derivation_for_output,
1112+
WalletScripts,
11051113
};
1114+
use crate::fixed_script_wallet::Chain;
11061115
use miniscript::bitcoin::psbt::Output;
11071116
use miniscript::bitcoin::{Amount, TxOut};
11081117
use std::convert::TryFrom;
@@ -1113,12 +1122,12 @@ impl BitGoPsbt {
11131122
let chain_enum = Chain::try_from(chain)?;
11141123

11151124
let derived_keys = wallet_keys
1116-
.derive_for_chain_and_index(chain, derivation_index)
1125+
.derive_path(&chain_index_path(chain, derivation_index))
11171126
.map_err(|e| format!("Failed to derive keys: {}", e))?;
11181127
let pub_triple = to_pub_triple(&derived_keys);
11191128

11201129
let script_support = network.output_script_support();
1121-
let scripts = WalletScripts::new(&pub_triple, chain_enum, &script_support)
1130+
let scripts = WalletScripts::new(&pub_triple, chain_enum.script_type, &script_support)
11221131
.map_err(|e| format!("Failed to create wallet scripts: {}", e))?;
11231132

11241133
let output_script = scripts.output_script();
@@ -2382,15 +2391,6 @@ impl BitGoPsbt {
23822391
Ok(())
23832392
}
23842393

2385-
/// Parse inputs with wallet keys and replay protection
2386-
///
2387-
/// # Arguments
2388-
/// - `wallet_keys`: The wallet's root keys for deriving scripts
2389-
/// - `replay_protection`: Scripts that are allowed as inputs without wallet validation
2390-
///
2391-
/// # Returns
2392-
/// - `Ok(Vec<ParsedInput>)` with parsed inputs
2393-
/// - `Err(ParseTransactionError)` if input parsing fails
23942394
fn parse_inputs(
23952395
&self,
23962396
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
@@ -3204,6 +3204,7 @@ pub fn to_wallet_keys(
32043204
use crate::fixed_script_wallet::RootWalletKeys;
32053205

32063206
let inner_psbt = psbt.psbt();
3207+
let network = psbt.network();
32073208

32083209
// Collect non-replay-protection inputs (those with derivation info)
32093210
let wallet_inputs: Vec<_> = inner_psbt
@@ -3230,9 +3231,15 @@ pub fn to_wallet_keys(
32303231
tx_input.previous_output,
32313232
);
32323233
match output_script {
3233-
Ok((script, _value)) => {
3234-
psbt_wallet_input::assert_wallet_input(&wallet_keys, psbt_input, script).is_ok()
3235-
}
3234+
Ok((script, _value)) => crate::fixed_script_wallet::WalletOutputScript::from_psbt(
3235+
&wallet_keys,
3236+
&psbt_input.bip32_derivation,
3237+
&psbt_input.tap_key_origins,
3238+
psbt_input.witness_script.is_some(),
3239+
script,
3240+
network,
3241+
)
3242+
.is_ok_and(|o| o.is_some()),
32363243
Err(_) => false,
32373244
}
32383245
});
@@ -3248,6 +3255,7 @@ pub fn to_wallet_keys(
32483255
#[cfg(test)]
32493256
mod tests {
32503257
use super::*;
3258+
use crate::fixed_script_wallet::wallet_scripts::chain_index_path;
32513259
use crate::fixed_script_wallet::Chain;
32523260
use crate::fixed_script_wallet::RootWalletKeys;
32533261
use crate::fixed_script_wallet::WalletScripts;
@@ -3604,8 +3612,8 @@ mod tests {
36043612
parse_fixture_paths(input_fixture).expect("Failed to parse fixture paths");
36053613
let scripts = WalletScripts::from_wallet_keys(
36063614
&wallet_keys.to_root_wallet_keys(),
3607-
chain,
3608-
index,
3615+
chain.script_type,
3616+
&chain_index_path(chain.value(), index),
36093617
&network.output_script_support(),
36103618
)
36113619
.expect("Failed to create wallet scripts");
@@ -4217,7 +4225,7 @@ mod tests {
42174225
format: fixtures::TxFormat,
42184226
script_type: fixtures::ScriptType,
42194227
) -> Result<(), String> {
4220-
use crate::fixed_script_wallet::bitgo_psbt::psbt_wallet_input::ScriptIdWithValue;
4228+
use crate::fixed_script_wallet::ScriptIdWithValue;
42214229

42224230
let is_p2ms = matches!(
42234231
script_type,

0 commit comments

Comments
 (0)