Skip to content
Draft
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
430 changes: 430 additions & 0 deletions packages/wasm-utxo/bips/bip-0360/bip-0360.mediawiki

Large diffs are not rendered by default.

30 changes: 18 additions & 12 deletions packages/wasm-utxo/src/address/bech32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
//!
//! Implements BIP 173 (Bech32) and BIP 350 (Bech32m) encoding schemes using the bitcoin crate.
//! - Bech32 is used for witness version 0 (P2WPKH, P2WSH)
//! - Bech32m is used for witness version 1+ (P2TR)
//! - Bech32m is used for witness version 1+ (P2TR, P2MR)

use super::{AddressCodec, AddressError, Result};
use crate::bitcoin::{Script, ScriptBuf, WitnessVersion};

/// Check if a script is a P2MR (BIP-360) witness v2 program.
/// P2MR: OP_2 (0x52) | OP_PUSHBYTES_32 (0x20) | <32-byte merkle root> = 34 bytes
pub(crate) fn is_p2mr(script: &Script) -> bool {
script.len() == 34
&& script.witness_version() == Some(WitnessVersion::V2)
&& script.as_bytes()[1] == 0x20
}

/// Bech32/Bech32m codec for witness addresses
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bech32Codec {
Expand Down Expand Up @@ -35,16 +43,12 @@ pub fn encode_witness_with_custom_hrp(
let hrp = Hrp::parse(hrp_str)
.map_err(|e| AddressError::Bech32Error(format!("Invalid HRP '{}': {}", hrp_str, e)))?;

// Encode based on witness version
let address = if version == WitnessVersion::V0 {
// Use Bech32 for witness version 0
bech32::segwit::encode_v0(hrp, program)
.map_err(|e| AddressError::Bech32Error(format!("Bech32 encoding failed: {}", e)))?
} else {
// Use Bech32m for witness version 1+
bech32::segwit::encode_v1(hrp, program)
.map_err(|e| AddressError::Bech32Error(format!("Bech32m encoding failed: {}", e)))?
};
// Encode using generic segwit encode which handles any witness version.
// v0 uses Bech32, v1+ uses Bech32m (BIP 350).
let version_fe32 = bech32::Fe32::try_from(version.to_num())
.map_err(|e| AddressError::Bech32Error(format!("Invalid witness version: {}", e)))?;
let address = bech32::segwit::encode(hrp, version_fe32, program)
.map_err(|e| AddressError::Bech32Error(format!("Bech32 encoding failed: {}", e)))?;

Ok(address)
}
Expand Down Expand Up @@ -72,9 +76,11 @@ pub fn extract_witness_program(script: &Script) -> Result<(WitnessVersion, &[u8]
));
}
Ok((WitnessVersion::V1, &script.as_bytes()[2..34]))
} else if is_p2mr(script) {
Ok((WitnessVersion::V2, &script.as_bytes()[2..34]))
} else {
Err(AddressError::UnsupportedScriptType(
"Bech32 only supports witness programs (P2WPKH, P2WSH, P2TR)".to_string(),
"Bech32 only supports witness programs (P2WPKH, P2WSH, P2TR, P2MR)".to_string(),
))
}
}
Expand Down
24 changes: 20 additions & 4 deletions packages/wasm-utxo/src/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ mod tests {
let script_obj = Script::from_bytes(script);
if script_obj.is_p2pkh() || script_obj.is_p2sh() {
from_output_script(script_obj, &BITCOIN)
} else if script_obj.is_p2wpkh() || script_obj.is_p2wsh() || script_obj.is_p2tr() {
} else if script_obj.is_p2wpkh()
|| script_obj.is_p2wsh()
|| script_obj.is_p2tr()
|| bech32::is_p2mr(script_obj)
{
from_output_script(script_obj, &BITCOIN_BECH32)
} else {
Err(AddressError::UnsupportedScriptType(format!(
Expand All @@ -310,7 +314,11 @@ mod tests {
let script_obj = Script::from_bytes(script);
if script_obj.is_p2pkh() || script_obj.is_p2sh() {
from_output_script(script_obj, &TESTNET)
} else if script_obj.is_p2wpkh() || script_obj.is_p2wsh() || script_obj.is_p2tr() {
} else if script_obj.is_p2wpkh()
|| script_obj.is_p2wsh()
|| script_obj.is_p2tr()
|| bech32::is_p2mr(script_obj)
{
from_output_script(script_obj, &TESTNET_BECH32)
} else {
Err(AddressError::UnsupportedScriptType(format!(
Expand All @@ -325,7 +333,11 @@ mod tests {
let script_obj = Script::from_bytes(script);
if script_obj.is_p2pkh() || script_obj.is_p2sh() {
from_output_script(script_obj, &LITECOIN)
} else if script_obj.is_p2wpkh() || script_obj.is_p2wsh() || script_obj.is_p2tr() {
} else if script_obj.is_p2wpkh()
|| script_obj.is_p2wsh()
|| script_obj.is_p2tr()
|| bech32::is_p2mr(script_obj)
{
from_output_script(script_obj, &LITECOIN_BECH32)
} else {
Err(AddressError::UnsupportedScriptType(format!(
Expand Down Expand Up @@ -483,7 +495,11 @@ mod tests {
// For networks with both base58 and bech32, choose based on script type
let codec = if script_obj.is_p2pkh() || script_obj.is_p2sh() {
codecs[0]
} else if script_obj.is_p2wpkh() || script_obj.is_p2wsh() || script_obj.is_p2tr() {
} else if script_obj.is_p2wpkh()
|| script_obj.is_p2wsh()
|| script_obj.is_p2tr()
|| bech32::is_p2mr(script_obj)
{
// Use bech32 codec if available (index 1), otherwise fall back to base58
if codecs.len() > 1 {
codecs[1]
Expand Down
42 changes: 39 additions & 3 deletions packages/wasm-utxo/src/address/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! This module bridges the Network enum with address codecs, providing
//! convenient functions to encode/decode addresses using network identifiers.

use super::bech32::is_p2mr;
use super::{
from_output_script, to_output_script_try_codecs, AddressCodec, AddressError, Result, ScriptBuf,
BITCOIN, BITCOIN_BECH32, BITCOIN_CASH, BITCOIN_CASH_CASHADDR, BITCOIN_CASH_TESTNET,
Expand Down Expand Up @@ -76,6 +77,7 @@ impl AddressFormat {
pub struct OutputScriptSupport {
pub segwit: bool,
pub taproot: bool,
pub p2mr: bool,
}

impl OutputScriptSupport {
Expand All @@ -102,6 +104,15 @@ impl OutputScriptSupport {
Ok(())
}

pub(crate) fn assert_p2mr(&self) -> Result<()> {
if !self.p2mr {
return Err(AddressError::UnsupportedScriptType(
"Network does not support P2MR".to_string(),
));
}
Ok(())
}

pub fn assert_support(&self, script: &Script) -> Result<()> {
match script.witness_version() {
None => {
Expand All @@ -113,6 +124,9 @@ impl OutputScriptSupport {
Some(WitnessVersion::V1) => {
self.assert_taproot()?;
}
Some(WitnessVersion::V2) => {
self.assert_p2mr()?;
}
_ => {
return Err(AddressError::UnsupportedScriptType(
"Unsupported witness version".to_string(),
Expand Down Expand Up @@ -170,7 +184,16 @@ impl Network {
// - https://github.com/litecoin-project/litecoin/blob/v0.21.4/src/script/interpreter.h#L129-L131
let taproot = segwit && matches!(self.mainnet(), Network::Bitcoin);

OutputScriptSupport { segwit, taproot }
// P2MR (BIP-360) support:
// Enabled on all Bitcoin networks (mainnet + testnets) for address encoding.
// Backend activation is controlled separately.
let p2mr = matches!(self.mainnet(), Network::Bitcoin);

OutputScriptSupport {
segwit,
taproot,
p2mr,
}
}
}

Expand All @@ -182,12 +205,13 @@ fn get_encode_codec(
) -> Result<&'static dyn AddressCodec> {
network.output_script_support().assert_support(script)?;

let is_witness = script.is_p2wpkh() || script.is_p2wsh() || script.is_p2tr();
let is_witness = script.is_p2wpkh() || script.is_p2wsh() || script.is_p2tr() || is_p2mr(script);
let is_legacy = script.is_p2pkh() || script.is_p2sh();

if !is_witness && !is_legacy {
return Err(AddressError::UnsupportedScriptType(
"Script is not a standard address type (P2PKH, P2SH, P2WPKH, P2WSH, P2TR)".to_string(),
"Script is not a standard address type (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, P2MR)"
.to_string(),
));
}

Expand Down Expand Up @@ -554,12 +578,14 @@ mod tests {
let support_none = OutputScriptSupport {
segwit: false,
taproot: false,
p2mr: false,
};
assert!(support_none.assert_legacy().is_ok());

let support_all = OutputScriptSupport {
segwit: true,
taproot: true,
p2mr: false,
};
assert!(support_all.assert_legacy().is_ok());
}
Expand All @@ -570,13 +596,15 @@ mod tests {
let support_segwit = OutputScriptSupport {
segwit: true,
taproot: false,
p2mr: false,
};
assert!(support_segwit.assert_segwit().is_ok());

// Should fail when segwit is not supported
let no_support = OutputScriptSupport {
segwit: false,
taproot: false,
p2mr: false,
};
let result = no_support.assert_segwit();
assert!(result.is_err());
Expand All @@ -592,13 +620,15 @@ mod tests {
let support_taproot = OutputScriptSupport {
segwit: true,
taproot: true,
p2mr: false,
};
assert!(support_taproot.assert_taproot().is_ok());

// Should fail when taproot is not supported
let no_support = OutputScriptSupport {
segwit: true,
taproot: false,
p2mr: false,
};
let result = no_support.assert_taproot();
assert!(result.is_err());
Expand All @@ -619,6 +649,7 @@ mod tests {
let no_support = OutputScriptSupport {
segwit: false,
taproot: false,
p2mr: false,
};
assert!(no_support.assert_support(&p2pkh_script).is_ok());

Expand All @@ -640,13 +671,15 @@ mod tests {
let support_segwit = OutputScriptSupport {
segwit: true,
taproot: false,
p2mr: false,
};
assert!(support_segwit.assert_support(&p2wpkh_script).is_ok());

// Should fail without segwit support
let no_support = OutputScriptSupport {
segwit: false,
taproot: false,
p2mr: false,
};
let result = no_support.assert_support(&p2wpkh_script);
assert!(result.is_err());
Expand Down Expand Up @@ -685,13 +718,15 @@ mod tests {
let support_taproot = OutputScriptSupport {
segwit: true,
taproot: true,
p2mr: false,
};
assert!(support_taproot.assert_support(&p2tr_script).is_ok());

// Should fail without taproot support (but with segwit)
let no_taproot = OutputScriptSupport {
segwit: true,
taproot: false,
p2mr: false,
};
let result = no_taproot.assert_support(&p2tr_script);
assert!(result.is_err());
Expand All @@ -704,6 +739,7 @@ mod tests {
let no_support = OutputScriptSupport {
segwit: false,
taproot: false,
p2mr: false,
};
let result = no_support.assert_support(&p2tr_script);
assert!(result.is_err());
Expand Down
7 changes: 6 additions & 1 deletion packages/wasm-utxo/src/address/utxolib_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ impl UtxolibNetwork {
.as_ref()
.is_some_and(|bech32| bech32 == "bc" || bech32 == "tb");

OutputScriptSupport { segwit, taproot }
// P2MR not supported via utxolib compat layer (only via Network enum)
OutputScriptSupport {
segwit,
taproot,
p2mr: false,
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4969,9 +4969,9 @@ mod tests {
}

// If both have non_witness_utxo, compare the relevant output
if orig.non_witness_utxo.is_some() && recon.non_witness_utxo.is_some() {
let orig_tx = orig.non_witness_utxo.as_ref().unwrap();
let recon_tx = recon.non_witness_utxo.as_ref().unwrap();
if let (Some(orig_tx), Some(recon_tx)) =
(&orig.non_witness_utxo, &recon.non_witness_utxo)
{
let vout = original_tx.input[idx].previous_output.vout as usize;
assert_eq!(
orig_tx.output.get(vout),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ mod tests {
let no_segwit_support = OutputScriptSupport {
segwit: false,
taproot: false,
p2mr: false,
};

use OutputScriptType::*;
Expand Down Expand Up @@ -410,6 +411,7 @@ mod tests {
let no_taproot_support = OutputScriptSupport {
segwit: true,
taproot: false,
p2mr: false,
};

let result = WalletScripts::from_wallet_keys(
Expand Down
1 change: 1 addition & 0 deletions packages/wasm-utxo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod inscriptions;
pub mod inspect;
pub mod message;
mod networks;
pub mod p2mr;
pub mod paygo;
pub mod psbt_ops;
#[cfg(test)]
Expand Down
Loading
Loading