Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/wasm-utxo/js/fixedScriptWallet/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FixedScriptWalletNamespace } from "../wasm/wasm_utxo.js";
import type { OutputScriptType } from "./scriptType.js";

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

/** A valid chain code value */
export type ChainCode = (typeof chainCodes)[number];
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/src/address/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ impl OutputScriptSupport {
OutputScriptType::P2sh => true, // all networks support legacy scripts
OutputScriptType::P2shP2wsh | OutputScriptType::P2wsh => self.segwit,
OutputScriptType::P2trLegacy | OutputScriptType::P2trMusig2 => self.taproot,
OutputScriptType::P2mr => self.p2mr,
}
}
}
Expand Down
54 changes: 49 additions & 5 deletions packages/wasm-utxo/src/bip322/bitgo_psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ use crate::fixed_script_wallet::bitgo_psbt::{
ProprietaryKeySubtype,
};
use crate::fixed_script_wallet::wallet_scripts::{
build_multisig_script_2_of_3, build_p2tr_ns_script, ScriptP2tr,
build_multisig_script_2_of_3, build_p2tr_ns_script, ScriptP2mr, ScriptP2tr,
};
use crate::fixed_script_wallet::{to_pub_triple, Chain, PubTriple, RootWalletKeys, WalletScripts};
use crate::networks::Network;

use miniscript::bitcoin::hashes::Hash;
use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash};
use miniscript::bitcoin::{Amount, ScriptBuf, Transaction, TxIn, TxOut};

Expand Down Expand Up @@ -140,6 +141,45 @@ pub fn add_bip322_input(
create_bip32_derivation(wallet_keys, chain, index);
inner_psbt.inputs[input_index].witness_script = Some(script.witness_script.clone());
}
WalletScripts::P2mr(script) => {
// P2MR is always script-path (no key-path). Same sighash as P2TR
// (BIP-360 reuses BIP-342 common signature message).
//
// Unlike P2trLegacy, we use the precomputed leaf hashes from ScriptP2mr
// rather than re-deriving keys and rebuilding scripts. The tree is fixed:
// leaf[0]: user+bitgo (key indices {0,2})
// leaf[1]: user+backup (key indices {0,1})
// leaf[2]: backup+bitgo (key indices {1,2})
//
// tap_scripts is skipped because P2MR control blocks (no internal key)
// can't be represented as rust-bitcoin's ControlBlock type.
let (signer_idx, cosigner_idx) =
sign_path.ok_or("signer and cosigner are required for p2mr inputs")?;

let mut pair = [signer_idx, cosigner_idx];
pair.sort();
let leaf_idx = match pair {
[0, 2] => 0,
[0, 1] => 1,
[1, 2] => 2,
_ => {
return Err(format!(
"Invalid signer pair: ({}, {})",
signer_idx, cosigner_idx
))
}
};

let leaf_hash = TapLeafHash::from_byte_array(script.leaves[leaf_idx].leaf_hash);

inner_psbt.inputs[input_index].tap_key_origins = create_tap_bip32_derivation(
wallet_keys,
chain,
index,
&[signer_idx, cosigner_idx],
Some(leaf_hash),
);
}
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
// For taproot, sign_path is required
let (signer_idx, cosigner_idx) =
Expand Down Expand Up @@ -428,7 +468,7 @@ pub fn verify_bip322_psbt_input(
///
/// # Arguments
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
///
/// # Returns
/// The output script (scriptPubKey)
Expand Down Expand Up @@ -458,8 +498,12 @@ fn build_output_script_from_pubkeys(
let script_p2tr = ScriptP2tr::new(pubkeys, true);
Ok(script_p2tr.output_script())
}
"p2mr" => {
let script_p2mr = ScriptP2mr::new(pubkeys);
Ok(script_p2mr.output_script())
}
_ => Err(format!(
"Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2",
"Unknown script type '{}'. Expected: p2sh, p2shP2wsh, p2wsh, p2tr, p2trMusig2, p2mr",
script_type
)),
}
Expand Down Expand Up @@ -537,7 +581,7 @@ fn verify_bip322_tx_structure(tx: &Transaction, input_index: usize) -> Result<()
/// * `input_index` - The index of the input to verify
/// * `message` - The message that was signed
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
/// * `is_script_path` - For taproot types, whether script path was used (None for non-taproot)
/// * `tag` - Optional custom tag for message hashing
///
Expand Down Expand Up @@ -614,7 +658,7 @@ pub fn verify_bip322_psbt_input_with_pubkeys(
/// * `input_index` - The index of the input to verify
/// * `message` - The message that was signed
/// * `pubkeys` - The three wallet pubkeys [user, backup, bitgo]
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2"
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr", "p2trMusig2", "p2mr"
/// * `is_script_path` - For taproot types, whether script path was used (None for non-taproot)
/// * `tag` - Optional custom tag for message hashing
///
Expand Down
17 changes: 17 additions & 0 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,9 @@ impl BitGoPsbt {
create_bip32_derivation(wallet_keys, chain, derivation_index);
psbt_input.witness_script = Some(script.witness_script.clone());
}
WalletScripts::P2mr(_) => {
return Err("P2MR PSBT input signing is not yet supported".to_string());
}
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
let sign_path = options.sign_path.ok_or_else(|| {
"sign_path is required for p2tr/p2trMusig2 inputs".to_string()
Expand Down Expand Up @@ -1168,6 +1171,20 @@ impl BitGoPsbt {
create_bip32_derivation(wallet_keys, chain, derivation_index);
psbt_output.witness_script = Some(script.witness_script.clone());
}
WalletScripts::P2mr(_) => {
// P2MR uses the same leaf structure as P2TR legacy (3 leaves, no musig2).
// We reuse taproot PSBT fields (tap_tree, tap_key_origins) since
// all tested PSBT parsers accept them on witness v2 outputs.
// No tap_internal_key (P2MR has no internal key or tweak).
psbt_output.tap_tree = Some(build_tap_tree_for_output(&pub_triple, false));
psbt_output.tap_key_origins = create_tap_bip32_derivation_for_output(
wallet_keys,
chain,
derivation_index,
&pub_triple,
false,
);
}
WalletScripts::P2trLegacy(script) | WalletScripts::P2trMusig2(script) => {
let is_musig2 = matches!(scripts, WalletScripts::P2trMusig2(_));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@ pub enum InputScriptType {
P2trLegacy,
P2trMusig2ScriptPath,
P2trMusig2KeyPath,
P2mr,
}

impl InputScriptType {
Expand All @@ -769,6 +770,7 @@ impl InputScriptType {
Ok(InputScriptType::P2trMusig2KeyPath)
}
}
OutputScriptType::P2mr => Ok(InputScriptType::P2mr),
}
}

Expand Down
Loading
Loading