Skip to content

Commit 015a363

Browse files
committed
feat: add BIP-322 message signing support for P2MR inputs
Add P2MR (BIP-360) input construction to add_bip322_input. P2MR uses the same sighash as P2TR (BIP-342 common signature message) and the same 2-of-2 checksigverify leaf scripts. The key difference is that P2MR has no internal key (quantum resistance), so tap_scripts cannot be populated (rust-bitcoin's ControlBlock requires an internal key). Instead, we use precomputed leaf hashes from ScriptP2mr and set only tap_key_origins, which is sufficient for signing. Also adds P2MR support to build_output_script_from_pubkeys for BIP-322 verification with pubkeys. Ticket: BTC-3241
1 parent 321fd51 commit 015a363

1 file changed

Lines changed: 48 additions & 7 deletions

File tree

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

Lines changed: 48 additions & 7 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,8 +141,44 @@ 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
}
143-
WalletScripts::P2mr(_) => {
144-
return Err("BIP-322 signing for P2MR is not yet supported".to_string());
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+
);
145182
}
146183
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
147184
// For taproot, sign_path is required
@@ -431,7 +468,7 @@ pub fn verify_bip322_psbt_input(
431468
///
432469
/// # Arguments
433470
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
434-
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
471+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
435472
///
436473
/// # Returns
437474
/// The output script (scriptPubKey)
@@ -461,8 +498,12 @@ fn build_output_script_from_pubkeys(
461498
let script_p2tr = ScriptP2tr::new(pubkeys, true);
462499
Ok(script_p2tr.output_script())
463500
}
501+
"p2mr" => {
502+
let script_p2mr = ScriptP2mr::new(pubkeys);
503+
Ok(script_p2mr.output_script())
504+
}
464505
_ => Err(format!(
465-
"Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2",
506+
"Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2, p2mr",
466507
script_type
467508
)),
468509
}
@@ -540,7 +581,7 @@ fn verify_bip322_tx_structure(tx: &Transaction, input_index: usize) -> Result<()
540581
/// * `input_index` - The index of the input to verify
541582
/// * `message` - The message that was signed
542583
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
543-
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
584+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
544585
/// * `is_script_path` - For taproot types, whether script path was used (None for non-taproot)
545586
/// * `tag` - Optional custom tag for message hashing
546587
///
@@ -617,7 +658,7 @@ pub fn verify_bip322_psbt_input_with_pubkeys(
617658
/// * `input_index` - The index of the input to verify
618659
/// * `message` - The message that was signed
619660
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
620-
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
661+
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
621662
/// * `is_script_path` - For taproot types, whether script path was used (None for non-taproot)
622663
/// * `tag` - Optional custom tag for message hashing
623664
///

0 commit comments

Comments
 (0)