Skip to content

Commit 91f37a9

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): refactor script derivation to use full paths
Refactor derivation to use full BIP32 paths instead of separate chain and index parameters. Centralize chain/index path construction in `chain_index_path()` helper. Replace `from_chain_and_index()` with generic `derive_path()` that accepts any derivation path. Cache results using the last two Normal path components. Move `Chain`, `Scope`, and `ScriptId` types to new `script_id` module. Add `WalletOutputScript` type to encapsulate script type + derivation path matching. Implement `from_psbt()` method to match PSBT metadata against wallet keys. Add `derivationPath` field to `ParsedInput` and `ParsedOutput` types, containing full BIP32 path (e.g. "0/1"). Make `scriptId` nullable to indicate when derivation path is nonstandard or external. Update JSDoc comments to clarify field semantics. Issue: BTC-2650 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 7951015 commit 91f37a9

18 files changed

Lines changed: 717 additions & 720 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: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,16 @@ pub enum BitGoPsbt {
107107
}
108108

109109
// Re-export types from submodules for convenience
110+
pub use crate::fixed_script_wallet::{ScriptId, ScriptIdWithValue};
110111
pub use psbt_wallet_input::{
111-
InputScriptType, ParsedInput, ReplayProtectionOptions, ScriptId, WalletInputOptions,
112+
InputScriptType, ParsedInput, ReplayProtectionOptions, WalletInputOptions,
112113
};
113114
pub use psbt_wallet_output::ParsedOutput;
114115

115116
/// Describes a single input for `from_half_signed_legacy_transaction`.
116117
pub enum HydrationUnspentInput {
117118
/// A regular wallet input with derivation chain, index, and value.
118-
Wallet(psbt_wallet_input::ScriptIdWithValue),
119+
Wallet(ScriptIdWithValue),
119120
/// A P2SH-P2PK replay protection input. The caller provides the expected pubkey so it can be
120121
/// validated against the redeemScript embedded in the legacy transaction.
121122
ReplayProtection {
@@ -187,7 +188,7 @@ impl std::error::Error for ParseTransactionError {}
187188
/// Get the default sighash type for a network and chain type
188189
fn get_default_sighash_type(
189190
network: Network,
190-
chain: crate::fixed_script_wallet::wallet_scripts::Chain,
191+
chain: crate::fixed_script_wallet::Chain,
191192
) -> miniscript::bitcoin::psbt::PsbtSighashType {
192193
use crate::fixed_script_wallet::wallet_scripts::OutputScriptType;
193194
use miniscript::bitcoin::sighash::{EcdsaSighashType, TapSighashType};
@@ -506,7 +507,7 @@ impl BitGoPsbt {
506507
for (i, (tx_in, unspent)) in tx.input.iter().zip(unspents.iter()).enumerate() {
507508
match unspent {
508509
HydrationUnspentInput::Wallet(sv) => {
509-
let script_id = psbt_wallet_input::ScriptId {
510+
let script_id = ScriptId {
510511
chain: sv.chain,
511512
index: sv.index,
512513
};
@@ -897,11 +898,14 @@ impl BitGoPsbt {
897898
vout: u32,
898899
value: u64,
899900
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
900-
script_id: psbt_wallet_input::ScriptId,
901+
script_id: ScriptId,
901902
options: WalletInputOptions,
902903
) -> Result<(), String> {
903904
use crate::fixed_script_wallet::to_pub_triple;
904-
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;
905909
use miniscript::bitcoin::psbt::Input;
906910
use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash};
907911
use miniscript::bitcoin::{transaction::Sequence, Amount, OutPoint, TxIn, TxOut};
@@ -914,12 +918,12 @@ impl BitGoPsbt {
914918
let chain_enum = Chain::try_from(chain)?;
915919

916920
let derived_keys = wallet_keys
917-
.derive_for_chain_and_index(chain, derivation_index)
921+
.derive_path(&chain_index_path(chain, derivation_index))
918922
.map_err(|e| format!("Failed to derive keys: {}", e))?;
919923
let pub_triple = to_pub_triple(&derived_keys);
920924

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

925929
let output_script = scripts.output_script();
@@ -1049,7 +1053,7 @@ impl BitGoPsbt {
10491053
vout: u32,
10501054
value: u64,
10511055
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
1052-
script_id: psbt_wallet_input::ScriptId,
1056+
script_id: ScriptId,
10531057
options: WalletInputOptions,
10541058
) -> Result<usize, String> {
10551059
let network = self.network();
@@ -1073,7 +1077,7 @@ impl BitGoPsbt {
10731077
vout: u32,
10741078
value: u64,
10751079
wallet_keys: &crate::fixed_script_wallet::RootWalletKeys,
1076-
script_id: psbt_wallet_input::ScriptId,
1080+
script_id: ScriptId,
10771081
options: WalletInputOptions,
10781082
) -> Result<usize, String> {
10791083
let index = self.psbt().inputs.len();
@@ -1104,8 +1108,10 @@ impl BitGoPsbt {
11041108
) -> Result<usize, String> {
11051109
use crate::fixed_script_wallet::to_pub_triple;
11061110
use crate::fixed_script_wallet::wallet_scripts::{
1107-
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,
11081113
};
1114+
use crate::fixed_script_wallet::Chain;
11091115
use miniscript::bitcoin::psbt::Output;
11101116
use miniscript::bitcoin::{Amount, TxOut};
11111117
use std::convert::TryFrom;
@@ -1116,12 +1122,12 @@ impl BitGoPsbt {
11161122
let chain_enum = Chain::try_from(chain)?;
11171123

11181124
let derived_keys = wallet_keys
1119-
.derive_for_chain_and_index(chain, derivation_index)
1125+
.derive_path(&chain_index_path(chain, derivation_index))
11201126
.map_err(|e| format!("Failed to derive keys: {}", e))?;
11211127
let pub_triple = to_pub_triple(&derived_keys);
11221128

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

11271133
let output_script = scripts.output_script();
@@ -2385,15 +2391,6 @@ impl BitGoPsbt {
23852391
Ok(())
23862392
}
23872393

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

32093206
let inner_psbt = psbt.psbt();
3207+
let network = psbt.network();
32103208

32113209
// Collect non-replay-protection inputs (those with derivation info)
32123210
let wallet_inputs: Vec<_> = inner_psbt
@@ -3233,9 +3231,15 @@ pub fn to_wallet_keys(
32333231
tx_input.previous_output,
32343232
);
32353233
match output_script {
3236-
Ok((script, _value)) => {
3237-
psbt_wallet_input::assert_wallet_input(&wallet_keys, psbt_input, script).is_ok()
3238-
}
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()),
32393243
Err(_) => false,
32403244
}
32413245
});
@@ -3251,6 +3255,7 @@ pub fn to_wallet_keys(
32513255
#[cfg(test)]
32523256
mod tests {
32533257
use super::*;
3258+
use crate::fixed_script_wallet::wallet_scripts::chain_index_path;
32543259
use crate::fixed_script_wallet::Chain;
32553260
use crate::fixed_script_wallet::RootWalletKeys;
32563261
use crate::fixed_script_wallet::WalletScripts;
@@ -3607,8 +3612,8 @@ mod tests {
36073612
parse_fixture_paths(input_fixture).expect("Failed to parse fixture paths");
36083613
let scripts = WalletScripts::from_wallet_keys(
36093614
&wallet_keys.to_root_wallet_keys(),
3610-
chain,
3611-
index,
3615+
chain.script_type,
3616+
&chain_index_path(chain.value(), index),
36123617
&network.output_script_support(),
36133618
)
36143619
.expect("Failed to create wallet scripts");
@@ -4220,7 +4225,7 @@ mod tests {
42204225
format: fixtures::TxFormat,
42214226
script_type: fixtures::ScriptType,
42224227
) -> Result<(), String> {
4223-
use crate::fixed_script_wallet::bitgo_psbt::psbt_wallet_input::ScriptIdWithValue;
4228+
use crate::fixed_script_wallet::ScriptIdWithValue;
42244229

42254230
let is_p2ms = matches!(
42264231
script_type,

0 commit comments

Comments
 (0)