Skip to content

Commit 3f59e99

Browse files
authored
Merge pull request #247 from BitGo/BTC-3241-bip360-p2mr-fixed-script
feat: add P2MR fixed-script wallet integration
2 parents be276f1 + 015a363 commit 3f59e99

10 files changed

Lines changed: 447 additions & 15 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FixedScriptWalletNamespace } from "../wasm/wasm_utxo.js";
88
import type { OutputScriptType } from "./scriptType.js";
99

1010
/** All valid chain codes as a const tuple */
11-
export const chainCodes = [0, 1, 10, 11, 20, 21, 30, 31, 40, 41] as const;
11+
export const chainCodes = [0, 1, 10, 11, 20, 21, 30, 31, 40, 41, 360, 361] as const;
1212

1313
/** A valid chain code value */
1414
export type ChainCode = (typeof chainCodes)[number];

packages/wasm-utxo/src/address/networks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ impl OutputScriptSupport {
142142
OutputScriptType::P2sh => true, // all networks support legacy scripts
143143
OutputScriptType::P2shP2wsh | OutputScriptType::P2wsh => self.segwit,
144144
OutputScriptType::P2trLegacy | OutputScriptType::P2trMusig2 => self.taproot,
145+
OutputScriptType::P2mr => self.p2mr,
145146
}
146147
}
147148
}

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

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ use crate::fixed_script_wallet::bitgo_psbt::{
88
ProprietaryKeySubtype,
99
};
1010
use crate::fixed_script_wallet::wallet_scripts::{
11-
build_multisig_script_2_of_3, build_p2tr_ns_script, ScriptP2tr,
11+
build_multisig_script_2_of_3, build_p2tr_ns_script, ScriptP2mr, ScriptP2tr,
1212
};
1313
use crate::fixed_script_wallet::{to_pub_triple, Chain, PubTriple, RootWalletKeys, WalletScripts};
1414
use crate::networks::Network;
1515

16+
use miniscript::bitcoin::hashes::Hash;
1617
use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash};
1718
use miniscript::bitcoin::{Amount, ScriptBuf, Transaction, TxIn, TxOut};
1819

@@ -140,6 +141,45 @@ pub fn add_bip322_input(
140141
create_bip32_derivation(wallet_keys, chain, index);
141142
inner_psbt.inputs[input_index].witness_script = Some(script.witness_script.clone());
142143
}
144+
WalletScripts::P2mr(script) => {
145+
// P2MR is always script-path (no key-path). Same sighash as P2TR
146+
// (BIP-360 reuses BIP-342 common signature message).
147+
//
148+
// Unlike P2trLegacy, we use the precomputed leaf hashes from ScriptP2mr
149+
// rather than re-deriving keys and rebuilding scripts. The tree is fixed:
150+
// leaf[0]: user+bitgo (key indices {0,2})
151+
// leaf[1]: user+backup (key indices {0,1})
152+
// leaf[2]: backup+bitgo (key indices {1,2})
153+
//
154+
// tap_scripts is skipped because P2MR control blocks (no internal key)
155+
// can't be represented as rust-bitcoin's ControlBlock type.
156+
let (signer_idx, cosigner_idx) =
157+
sign_path.ok_or("signer and cosigner are required for p2mr inputs")?;
158+
159+
let mut pair = [signer_idx, cosigner_idx];
160+
pair.sort();
161+
let leaf_idx = match pair {
162+
[0, 2] => 0,
163+
[0, 1] => 1,
164+
[1, 2] => 2,
165+
_ => {
166+
return Err(format!(
167+
"Invalid signer pair: ({}, {})",
168+
signer_idx, cosigner_idx
169+
))
170+
}
171+
};
172+
173+
let leaf_hash = TapLeafHash::from_byte_array(script.leaves[leaf_idx].leaf_hash);
174+
175+
inner_psbt.inputs[input_index].tap_key_origins = create_tap_bip32_derivation(
176+
wallet_keys,
177+
chain,
178+
index,
179+
&[signer_idx, cosigner_idx],
180+
Some(leaf_hash),
181+
);
182+
}
143183
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
144184
// For taproot, sign_path is required
145185
let (signer_idx, cosigner_idx) =
@@ -428,7 +468,7 @@ pub fn verify_bip322_psbt_input(
428468
///
429469
/// # Arguments
430470
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
431-
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
471+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
432472
///
433473
/// # Returns
434474
/// The output script (scriptPubKey)
@@ -458,8 +498,12 @@ fn build_output_script_from_pubkeys(
458498
let script_p2tr = ScriptP2tr::new(pubkeys, true);
459499
Ok(script_p2tr.output_script())
460500
}
501+
"p2mr" => {
502+
let script_p2mr = ScriptP2mr::new(pubkeys);
503+
Ok(script_p2mr.output_script())
504+
}
461505
_ => Err(format!(
462-
"Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2",
506+
"Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2, p2mr",
463507
script_type
464508
)),
465509
}
@@ -537,7 +581,7 @@ fn verify_bip322_tx_structure(tx: &Transaction, input_index: usize) -> Result<()
537581
/// * `input_index` - The index of the input to verify
538582
/// * `message` - The message that was signed
539583
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
540-
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
584+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
541585
/// * `is_script_path` - For taproot types, whether script path was used (None for non-taproot)
542586
/// * `tag` - Optional custom tag for message hashing
543587
///
@@ -614,7 +658,7 @@ pub fn verify_bip322_psbt_input_with_pubkeys(
614658
/// * `input_index` - The index of the input to verify
615659
/// * `message` - The message that was signed
616660
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
617-
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
661+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
618662
/// * `is_script_path` - For taproot types, whether script path was used (None for non-taproot)
619663
/// * `tag` - Optional custom tag for message hashing
620664
///

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,9 @@ impl BitGoPsbt {
10141014
create_bip32_derivation(wallet_keys, chain, derivation_index);
10151015
psbt_input.witness_script = Some(script.witness_script.clone());
10161016
}
1017+
WalletScripts::P2mr(_) => {
1018+
return Err("P2MR PSBT input signing is not yet supported".to_string());
1019+
}
10171020
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
10181021
let sign_path = options.sign_path.ok_or_else(|| {
10191022
"sign_path is required for p2tr/p2trMusig2 inputs".to_string()
@@ -1168,6 +1171,20 @@ impl BitGoPsbt {
11681171
create_bip32_derivation(wallet_keys, chain, derivation_index);
11691172
psbt_output.witness_script = Some(script.witness_script.clone());
11701173
}
1174+
WalletScripts::P2mr(_) => {
1175+
// P2MR uses the same leaf structure as P2TR legacy (3 leaves, no musig2).
1176+
// We reuse taproot PSBT fields (tap_tree, tap_key_origins) since
1177+
// all tested PSBT parsers accept them on witness v2 outputs.
1178+
// No tap_internal_key (P2MR has no internal key or tweak).
1179+
psbt_output.tap_tree = Some(build_tap_tree_for_output(&pub_triple, false));
1180+
psbt_output.tap_key_origins = create_tap_bip32_derivation_for_output(
1181+
wallet_keys,
1182+
chain,
1183+
derivation_index,
1184+
&pub_triple,
1185+
false,
1186+
);
1187+
}
11711188
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
11721189
let is_musig2 = matches!(scripts, WalletScripts::P2trMusig2(_));
11731190

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,7 @@ pub enum InputScriptType {
751751
P2trLegacy,
752752
P2trMusig2ScriptPath,
753753
P2trMusig2KeyPath,
754+
P2mr,
754755
}
755756

756757
impl InputScriptType {
@@ -769,6 +770,7 @@ impl InputScriptType {
769770
Ok(InputScriptType::P2trMusig2KeyPath)
770771
}
771772
}
773+
OutputScriptType::P2mr => Ok(InputScriptType::P2mr),
772774
}
773775
}
774776

0 commit comments

Comments
 (0)