+
+==Introduction==
+
+===Abstract===
+
+This document proposes a new output type: Pay-to-Merkle-Root (P2MR), via a soft fork. P2MR outputs operate with nearly the same functionality as P2TR (Pay-to-Taproot) outputs, but with the key path spend removed.
+
+Through this modification, P2MR outputs allow developers to use script trees and tapscript in a manner that is:
+
+# resistant to long exposure attacks by Cryptographically Relevant Quantum Computers (CRQCs), and
+# resistant to future cryptanalytic approaches that may compromise the elliptic curve cryptography (ECC) used by Bitcoin.
+
+It is worth noting that proposed P2MR outputs are only resistant to "long exposure attacks" on elliptic curve cryptography; that is, attacks on keys exposed for time periods longer than needed to confirm a spending transaction.
+
+Protection against more sophisticated quantum attacks, including protection against private key recovery from public keys exposed in the mempool while a transaction is waiting to be confirmed (a.k.a. "short exposure attacks"), may require the introduction of post-quantum signatures in Bitcoin. We believe it's worth considering this path in the future and intend to offer a separate proposal for this purpose upon further research.
+
+This document additionally defines "long exposure" and "short exposure" attacks, and other new terminology in the Glossary.
+
+===Copyright===
+
+This document is licensed under the 3-clause BSD license.
+
+===Motivation===
+
+The primary threat to Bitcoin from Cryptographically Relevant Quantum Computers (CRQCs) is their potential to break the key cryptographic assumption which secures the digital signatures used in Bitcoin.A Cryptographically Relevant Quantum Computer is an ''object'' which is only loosely defined by ''characteristics'' in quantum physics as of today. It could be understood in the context of this BIP and in Bitcoin that it's a ''hardware-agnostic'' computer supposed to have the architecture to keep ''coherent'' a sufficient number of logical qubits to be able to run Shor's algorithm in an efficient fashion. More specifically, [https://arxiv.org/pdf/quant-ph/0301141 Shor's algorithm] enables a CRQC to solve the Discrete Logarithm Problem (DLP) exponentially faster than classical methods.Shor's algorithm is believed to need 10^8 operations to break a 256-bit elliptic curve public key. This allows the derivation of private keys from public keys — a process referred to here as quantum key recovery.Meaning, deriving private keys from public keys via Shor's algorithm While it is unclear when or if CRQCs will become viable in the future, we propose the addition of a quantum-resistant, [[#script-tree-output-type|script tree output type]] for those interested in this level of protection.
+
+While some may balk at the potential threat of quantum computers to Bitcoin given their limited functionality to date, some others — including governments, corporations and some existing and potential Bitcoin users — are concerned about their potential for advancement. The Commercial National Security Algorithm Suite (CNSA) 2.0, for instance, has mandated software and networking equipment to be upgraded to post-quantum schemes by 2030, with browsers and operating systems fully upgraded by 2033. Additionally, according to NIST IR 8547, Elliptic Curve Cryptography (ECC) is planned to be disallowed within the US federal government after 2035 (with an exception made for hybrid cryptography, or the use of ECC and post-quantum algorithms together). These kinds of mandates have triggered concern by some ECC users, including some Bitcoin users who prefer to be prepared out of an abundance of caution.
+
+In the most optimistic case, wherein quantum computers never pose a significant risk to ECC, we understand that the possibility of quantum advancement alone may be influencing adoption and broad confidence in the Bitcoin network. In other words, we believe users' fear of quantum computers may be worth addressing regardless of CRQC viability. Given these concerns, we think it's worth considering simple low risk changes that create options for using Bitcoin in a quantum-resistant way.
+
+As a conservative first step in this effort, we propose Pay-to-Merkle-Root (P2MR), a script tree output that can be used in a quantum resistant manner.
+
+===Long Exposure vs Short Exposure Attacks===
+
+For clarity, this proposal specifically mitigates the risk of long exposure attacks on outputs that support tapscript and script trees. While some other Bitcoin output types, such as P2SH, are safe against long exposure attacks, taproot is not and taproot is the only currently activated output type that supports tapscript and script trees.
+
+A long exposure attack is an attack performed on exposed blockchain data, such as exposed public keys, or the scripts of spent outputs. These are likely to be the earliest quantum attacks made possible on Bitcoin, because attackers will have ample time — as much time as vulnerable keys are exposed — to carry out quantum key recovery.
+
+Short exposure attacks, however, require faster quantum computers, because they must occur within the relatively short time that a transaction is unconfirmed in the mempool.
+
+Bitcoin outputs are generally vulnerable to short exposure attacks, as most Bitcoin transactions require revealing the associated public key when spending. Full protection of outputs from short exposure attacks may require the use of post-quantum signature schemes.
+
+Since long exposure attacks on public keys are likely to be the first quantum-enabled threat to Bitcoin, we propose a script tree output that is resistant to long exposure attacks as a first step in hardening Bitcoin against the potential threat of quantum computers.
+
+The following list of output types describes their long exposure attack vulnerability:
+
+{| class="wikitable"
+|-
+! Type
+! Vulnerable
+! Prefix
+! Example
+|-
+| P2PK
+| Yes
+| Varies
+| 02103203b768951584fe9af6d9d9e6ff26a5f76e453212f19ba163774182ab8057f3eac
+|-
+| P2PKH
+| No*
+| 1
+| 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
+|-
+| P2MS
+| Yes
+| Varies
+| 52410496ec45f878b62c46c4be8e336dff7cc58df9b502178cc240e...
+|-
+| P2SH
+| No*
+| 3
+| 3FkhZo7sGNue153xhgqPBcUaBsYvJW6tTx
+|-
+| P2WPKH
+| No*
+| bc1q
+| bc1qsnh5ktku9ztqeqfr89yrqjd05eh58nah884mku
+|-
+| P2WSH
+| No*
+| bc1q
+| bc1qvhu3557twysq2ldn6dut6rmaj3qk04p60h9l79wk4lzgy0ca8mfsnffz65
+|-
+| P2TR
+| Yes
+| bc1p
+| bc1p92aslsnseq786wxfk3ekra90ds9ku47qttupfjsqmmj4z82xdq4q3rr58u
+|-
+| P2MR
+| No*
+| bc1z
+| bc1zzmv50jjgxxhww6ve4g5zpewrkjqhr06fyujpm20tuezdlxmfphcqfc80ve
+|}
+
+The following output types are fundamentally vulnerable to long exposure attacks:
+
+* P2PK outputs (e.g. Satoshi's coins, CPU miners)
+* Reused outputs*
+* Taproot outputs (starts with bc1p)
+
+* Funds in P2PKH, P2SH, P2WPKH, P2WSH, and P2MR outputs can become vulnerable to long exposure quantum attacks anytime their script reveals a public key.
+
+Note: Extended public keys, commonly known as "xpubs," and wallet descriptors also reveal quantum vulnerable public key information. For further clarification on quantum attack vectors, please refer to the [[#Glossary|Glossary of Terms]].
+
+==Design==
+
+Pay-to-Merkle-Root (P2MR) is a proposed new output type that commits to the root of a script tree. It operates with nearly the same functionality as P2TR (Pay-to-Taproot) outputs, but with the quantum vulnerable key path spend removed.
+
+In other words, P2MR outputs commit to the Merkle root of a script tree without committing to an internal key. The script(s) being committed to, however, may contain a key or key-hash.
+
+This output type is designed to offer users protection against long exposure quantum attacks as well as a practical output type with which post-quantum signatures may be used if such signatures are adopted in the future.
+
+Since P2MR outputs have no key path spend, they omit the Taproot internal key. Instead, a P2MR output includes the 32-byte root of the script tree as defined in [[bip-0341.mediawiki|BIP 341]] hashed with the tag "TapBranch" as shown below.
+
+[[File:bip-0360/media/merkletree.png|thumb|Construction of P2MR script tree root, scriptPubkey, and Witness]]
+
+To construct a P2MR output, we follow the process outlined in [[bip-0341.mediawiki|BIP 341]] to compute the final tapbranch hash, which is the merkle root of the script leaves; however, instead of tweaking the internal key with the root of the Merkle tree (as is the case with P2TR outputs), P2MR outputs commit only to final tapbranch hash, which is tagged, "TapBranch".
+
+
+D = tagged_hash("TapLeaf", bytes([leaf_version]) + ser_script(script))
+CD = tagged_hash("TapBranch", C + D)
+CDE = tagged_hash("TapBranch", CD + E)
+ABCDE = tagged_hash("TapBranch", AB + CDE)
+
+
+A P2MR input witness provides the following:
+
+
+initial stack element 0,
+...,
+initial stack element N,
+leaf script,
+control block = [control byte, 32*m byte Merkle path] # m is the depth of the script in the Merkle tree
+
+
+The initial stack elements of P2MR follow the same rules as P2TR script path spends.
+That is, they place elements on the stack to be evaluated by the leaf script.
+
+The control block is a ''1 + 32 * m'' byte array, where the first byte is the control byte and the next ''32 * m'' bytes are the Merkle path to the leaf script. The control byte is the same as the control byte in a P2TR control block, including the 7 bits which are used to specify the leaf version. The parity bit of the control byte is always 1, since P2MR does not have a key path spend. Unlike P2TR, we omit the public key from the control block as it is not needed in P2MR. We maintain support for the optional annex in the witness (see Specification section below for more details).
+
+==Rationale==
+
+Design of the P2MR output type is guided by the following intentions:
+
+1. Minimize changes to the network. We should reuse existing Bitcoin code and preserve existing software behavior, workflows, user expectations and compatibility whenever possible.
+
+P2MR leverages the battle tested P2TR, tapleaf and tapscript code already in Bitcoin, reducing the implementation burden on wallets, exchanges, and libraries that can reuse code they already have. This approach reduces complexity and minimizes implementation risks.
+
+2. Create the safest possible path for the addition of post-quantum signature integrations, in the event that they are used in the future.
+
+Importantly, we are proposing a script tree output, i.e. an output type that supports tapscript, that is resistant to long exposure attacks. While some existing output types are already resistant to long exposure attacks (e.g. P2WSH), no such output type supports tapscript — a feature that may be required for practical implementation of post-quantum signature opcodes.
+
+P2WSH, for instance, does not support tapscript and as such does not support the OP_SUCCESSx opcode update path that will be critical for the integration of post-quantum OP_CHECKSIG opcodes into Bitcoin.OP_SUCCESSx is a mechanism to upgrade tapscript
+
+3. Facilitate gradual integration of quantum resistant features that can be carried out iteratively as quantum computers evolve. This approach encourages responsiveness to the current threat-level, while avoiding heavy-handedness in our reactions to a potential threat.
+
+We designed P2MR with an eye towards integrating post-quantum signatures in the future, without proposing more complex changes while CRQCs are still in their infancy.
+
+===P2MR Trade-Offs===
+
+While P2TR outputs (and the use of key path spend) will remain an option for folks wishing to use them, we aim to be clear about the tradeoffs of using P2MR outputs, which disable the key path spend for the benefit of quantum resistance.
+
+The witness to a P2MR spend is always larger than the witness to a P2TR key path spend. This is because a P2TR key path spend requires only a Schnorr signature in the witness. For P2MR, the witness must include the chosen leaf script, the initial stack, and a control block consisting of the control byte and Merkle path (if any).
+
+That said, the witness to a P2MR spend will always be smaller than the witness to an equivalent P2TR script path spend, because there is no longer any internal key in P2MR that must be revealed in the control block. For a more complete comparison of output type transaction sizes, the "Transaction Size and Fees" section may be reviewed later in this proposal.
+
+Additionally, there is a privacy tradeoff when comparing P2MR and P2TR, which is that users reveal they are spending to a script tree whenever they are using P2MR outputs, since P2MR outputs can only be spent via script path spend. In P2TR when you spend an output as a key path spend, you don't reveal if you have any script path spends. This trade-off only exists when comparing P2TR key path spends to P2MR script path spends; P2TR and P2MR provide the same level of privacy when both are script path spends.
+
+'''Note:''' P2MR and P2TR both provide greater script privacy than P2SH [[bip-0016.mediawiki|BIP 16]] because unused script paths are not revealed.
+
+==Specification==
+
+We define the Pay-to-Merkle-Root (P2MR) output structure as follows:
+
+A P2MR output is similar to a P2TR output (as defined in [[bip-0341.mediawiki|BIP 341]]); however, unlike P2TR outputs, we disable the key path spend for the benefit of quantum resistance by omitting the internal key and the tap tweak step.
+A P2MR output is then a SegWit version 2 byte followed by the Merkle root of the script tree as the witness program.
+
+
+===Address Format===
+
+P2MR outputs use SegWit version 2, resulting in mainnet addresses that start with bc1z, following [[bip-0173.mediawiki|BIP 173]]. Bech32m encoding maps version 2 to the prefix z.
+
+Example P2MR address:
+
+
+bc1zzmv50jjgxxhww6ve4g5zpewrkjqhr06fyujpm20tuezdlxmfphcqfc80ve
+
+
+This commits to a 32-byte script tree Merkle root.
+
+===ScriptPubKey===
+
+The scriptPubKey for a P2MR output is:
+
+
+OP_2 OP_PUSHBYTES_32
+
+
+Where:
+* OP_2 indicates SegWit version 2.
+* is the 32-byte Merkle root of the script tree.
+
+===Script Validation===
+
+A P2MR output is a native SegWit output (see [[bip-0141.mediawiki|BIP 141]]) with version 2 and a 32-byte witness program. For the sake of comparison, we have — as much as possible — copied the language verbatim from the script validation section of [[bip-0341.mediawiki|BIP 341]].
+
+* Let ''q'' be the 32-byte array containing the witness program (the second push in the scriptPubKey) which represents the Merkle root of the script tree.
+* Fail if the witness stack does not have two or more elements.
+* Fail if the witness stack has exactly two elements and the first byte of the last element is 0x50.
+* If there are at least three witness elements, and the first byte of the last element is 0x50, this last element is called ''annex a'' and is removed from the witness stack. The annex (or the lack thereof) is always covered by the signature and contributes to transaction weight, but is otherwise ignored during P2MR validation.
+* There must be at least two witness elements left.
+** Call the second-to-last stack element ''s'', the script (as defined in [[bip-0341.mediawiki|BIP 341]]).
+** The last stack element is called the control block ''c'', and must have length ''1 + 32 * m'', for a value of ''m'' that is an integer between 0 and 128, inclusive. Fail if it does not have such a length.
+** Let ''v = c[0] & 0xfe'' be the ''leaf version'' (as defined in [[bip-0341.mediawiki|BIP 341]]). To maintain ''leaf version'' encoding compatibility the last bit of c[0] is unused and must be 1.Why set the last bit of c[0] to one? Consider a faulty implementation that deserializes the ''leaf version'' as c[0] rather than c[0] & 0xfe for both P2TR and P2MR. If they test against P2MR outputs and require that last bit is 1, this deserialization bug will cause an immediate error.
+** Let ''k0 = hashTapLeaf(v || compact_size(size of s) || s)''; also call it the ''tapleaf hash''.
+** For ''j'' in ''[0,1,...,m-1]'':
+*** Let ''ej = c[1+32j:33+32j]''.
+*** Let ''kj+1'' depend on whether ''kj < ej'' (lexicographically):
+**** If ''kj < ej'': ''kj+1 = hashTapBranch(kj || ej)''.
+**** If ''kj ≥ ej'': ''kj+1 = hashTapBranch(ej || kj)''.
+** Let ''r = km''.
+** If ''q ≠ r'', fail.
+** Execute the script, according to the script rules specified in BIP 342, using the witness stack elements excluding the script ''s'', the control block ''c'', and the annex ''a'' if present, as initial stack. This implies that for the future leaf versions (non-''0xC0'') the execution must succeed.
+
+The steps above follow the script path spend logic from [[bip-0341.mediawiki|BIP 341]] with the following changes:
+
+* The witness program is the the Merkle root of the script tree and not a tweaked public key. This means that we skip directly to the BIP 341 script path validation.
+* We compute the script tree Merkle root ''r'' and compare it directly to the witness program ''q''.
+* The control block is ''1 + 32*m'' bytes, instead of ''33 + 32*m'' bytes.
+
+===Common Signature Message Construction===
+
+The [https://learnmeabitcoin.com/technical/upgrades/taproot/#common-signature-message common signature message] construction for P2MR outputs is exactly the same procedure as defined in [[bip-0342.mediawiki#user-content-Common_Signature_Message_Extension|BIP342 Common Signature Message]].
+
+=== Compatibility with BIP 141 ===
+
+By adhering to the SegWit transaction structure and versioning, P2MR outputs are compatible with existing transaction processing rules. Nodes that do not recognize SegWit version 2 will treat these outputs as anyone-can-spend but generally will not relay or mine such transactions.
+
+===Transaction Size and Fees===
+
+All P2MR and P2TR outputs are always the same size. P2MR inputs can be slightly larger or smaller than their equivalent P2TR inputs, depending on the use of key path vs script path spend in the case of P2TR. Let's consider the cases.
+
+====Comparison with P2TR key path spend====
+
+A P2MR witness will be larger than a P2TR witness when the P2TR output is spent via the key path spend. A witness to a P2TR key path spend is simply a signature. P2MR quantum resistance comes from removing the P2TR key path spend. Every P2MR spend is a P2TR script path spend and so requires a script, its input stack and a control block. Consequently, P2MR loses this size advantage of P2TR key path spends in order to gain quantum resistance. If the script tree only has a single leaf script, no Merkle path is needed in the control block, giving us a minimal size control block of 1 byte.
+
+P2MR witness for depth-0 tree (103 bytes):
+
+
+[count] (1 byte), # Number of elements in the witness
+[size] signature (1 + 64 bytes = 65 bytes),
+leaf script = [size] [OP_PUSHBYTES_32, 32-byte public key, OP_CHECKSIG] (1 + 1 + 32 + 1 bytes = 35 bytes),
+control block = [size] [control byte] [merkle path (empty)] (1 + 1 + 0 bytes = 2 bytes)
+
+
+P2TR key path spend witness (66 bytes):
+
+
+[count] (1 byte), # Number of elements in the witness
+[size] signature (1 + 64 bytes = 65 bytes)
+
+
+Thus, the P2MR witness would be 103 - 66 = 37 bytes larger than a P2TR key path spend witness.
+
+If the Merkle tree has more than a single leaf, then the Merkle path must be included in the control block, increasing the size by ''32 * m'' bytes, where m is the depth of the Merkle tree.
+While script trees do support leaf scripts of different depths, here we assume the Merkle tree has been constructed such that each leaf is at the same depth.
+This would make such witness 37 + 32 * m bytes larger than a P2TR key path spend witness.If ''m >= 8'', then the compact size will use 3 bytes rather than 1 byte
+
+P2MR witness ''(103 + 32*m bytes)'':
+
+
+[count] (1 byte), # Number of elements in the witness
+[size] signature (64 + 1 bytes = 65 bytes),
+leaf script = [size] [OP_PUSHBYTES_32, 32-byte public key, OP_CHECKSIG] (34 + 1 bytes = 35 bytes),
+control block = [size] [control byte] [Merkle path] (1 + 1 + 32*m = 2 + 32*m bytes)
+
+
+====Comparison with P2TR script path spend====
+
+A P2MR witness will be smaller than the witness to an equivalent P2TR script path spend. This is because P2MR does not require inclusion of an internal public key in the control block to unlock and spend an output. For this reason, a P2MR witness will always be 32 bytes smaller than an equivalent P2TR script path spend witness.
+
+==Performance Impact==
+
+P2MR is slightly more computationally performant than P2TR script path spends, as the operations to spend a P2MR output is a strict subset of the operations needed to perform a script path spend on a P2TR output.
+
+==Backward Compatibility==
+
+Older wallets and nodes that have not been made compatible with SegWit version 2 and P2MR will not understand these outputs. Per [[bip-0350.mediawiki|BIP 350]] older wallets should be able to spend funds to SegWit version 2 outputs. Users should ensure they are using updated wallets and nodes to receive P2MR outputs and validate transactions using P2MR outputs. P2MR is fully compatible with tapscript and existing tapscript programs can be used in P2MR outputs without modification. P2MR can also support future scripts with new leaf versions.
+
+==Security==
+
+P2MR outputs provide the same tapscript functionality as P2TR outputs, but with the quantum-vulnerable key path spend removed. The similarity between these output types enables users to easily migrate script trees from P2TR outputs to P2MR outputs for protection against long exposure quantum attacks. Wallets supporting only P2TR key path spends would need to migrate to using script trees. This is a straightforward migration as it only requires moving to a simple OP_CHECKSIG leaf script.
+
+Protection from long exposure quantum attacks does not depend on the activation of post-quantum signatures in Bitcoin, but requires that users do not expose their public keys to attackers via public key reuse or other unsafe practices.
+
+P2MR uses a 256-bit hash output, providing 128 bits of collision resistance and 256 bits of preimage resistance. This is the same level of security as P2WSH specified in [[bip-0141.mediawiki|BIP 141]], which also uses a 256-bit hash output.
+
+P2MR does not, by itself, protect against short exposure quantum attacks, but these attacks can be mitigated by future activation of post-quantum signatures.
+
+Combined with P2MR, post-quantum signature schemes can provide comprehensive quantum resistance to P2MR outputs, including protection from short exposure attacks.
+
+That said, protection against long exposure quantum attacks alone should not be underestimated. It's unlikely that early CRQCs will be fast enough to perform short exposure attacks, making preparedness against long exposure attacks more time-critical.
+
+==Security Considerations for Post-Quantum Signature Schemes==
+
+While this proposal does not include the introduction of post-quantum signature schemes, we think it's worth commenting on security considerations related to this possibility.
+
+Quantum-resistant signature algorithms (e.g. ML-DSA or SLH-DSA) offer different levels of protection and should be scrutinized before use. We are currently researching options for the potential proposal of post-quantum signatures into Bitcoin and encourage others to engage in this research as well.
+
+We also imagine the possibility of introducing multiple post-quantum signatures for redundancy. Balancing the risks of additional complexity with the benefits of signature-type redundancy will be the challenge here.
+
+==Test Vectors and Reference Code==
+
+Test vector data for creation of P2MR UTXOs can be found [https://github.com/bitcoin/bips/blob/master/bip-0360/ref-impl/common/tests/data/P2MR_construction.json here].
+
+These test vectors build off of the test vectors for [[bip-0341.mediawiki|BIP 341]] (Taproot). One important distinction is that the P2MR test vectors do not include keypath spend scenarios.
+
+Also included are test vectors in [https://github.com/bitcoin/bips/tree/master/bip-0360/ref-impl/rust rust implementation] and [https://github.com/bitcoin/bips/tree/master/bip-0360/ref-impl/python python implementation]. One of these tests demonstrates a tapscript that requires a secp256k1 signature to spend the P2MR UTXO (modeled after one of the extremely valuable examples provided by [https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature this Taproot script path spend example]. Similar to BIP 341 test vectors, all signatures are created with an all-zero (0x0000...0000) [[bip-0340.mediawiki|BIP 340]] auxiliary randomness array.
+
+==Related Work==
+
+Below we attempt to summarize some of the ideas discussed on the Bitcoin Development Mailing List that relate to P2MR.
+
+The idea of Taproot with key path spend removed has been discussed a number of times in the Bitcoin community.
+
+For instance, [https://gnusha.org/pi/bitcoindev/CAD5xwhgzR8e5r1e4H-5EH2mSsE1V39dd06+TgYniFnXFSBqLxw@mail.gmail.com/ OP_CAT Makes Bitcoin Quantum Secure] notes that if we disable the key path spend in Taproot and activate OP_CAT [[bip-0347.mediawiki|BIP 347]], we could achieve quantum resistance by using Lamport signatures with OP_CAT.
+
+Lamport and WOTS (Winternitz One-Time Signatures) built from CAT are quantum resistant, but are one-time signatures — meaning, if you sign twice for the same public key, you risk leaking your private key, which is a significant security concern for everyday users.
+
+This would require major changes to wallet behavior and would represent a significant security downgrade. Common practices, such as RBF and CPFP, could risk revealing private keys if no stateless signature scheme is used.
+
+[https://groups.google.com/g/bitcoindev/c/8O857bRSVV8/m/rTrpeFjWDAAJ Trivial QC signatures with clean upgrade path] and [https://groups.google.com/g/bitcoindev/c/oQKezDOc4us/m/T1vSMkZNAAAJ Re: P2QRH / BIP 360 Update] also discuss the possibility of Taproot with key path spend removed. The design of P2MR was partly inspired by these discussions.
+
+Commit-reveal schemes such as [https://gnusha.org/pi/bitcoindev/1518710367.3550.111.camel@mmci.uni-saarland.de/ Re: Transition to post-quantum (2018)] and [https://groups.google.com/g/bitcoindev/c/LpWOcXMcvk8/m/YEiH-kTHAwAJ Post-Quantum commit / reveal Fawkescoin variant as a soft fork (2025)] have been proposed as a way to create cryptocurrencies without public key cryptography. The ideas in this paper were more recently expanded upon by Tadge Dryja in his "[https://www.youtube.com/watch?v=4bzOwYPf1yo Lifeboat]" proposal, which effectively quantum-proofs Bitcoin transactions through a similar pre-commitment scheme designed for Bitcoin.
+
+==Other Methods of Addressing Quantum Vulnerabilities for Cryptocurrencies==
+
+It is worth noting, by way of comparison, that [https://ethresear.ch/t/how-to-hard-fork-to-save-most-users-funds-in-a-quantum-emergency/18901 Vitalik Buterin's proposed solution] to Ethereum's quantum vulnerability is quite different from the approach in this BIP.
+
+His plan involves a hard fork of the chain, reverting all blocks after some sufficient amount of theft, and using STARKs based on BIP 32 seeds to act as the authoritative secret when signing. We believe rollbacks of any kind are an untenable approach for Bitcoin and would likely be impractical to implement.
+
+That said, we believe the use of STARKs (which are quantum-resistant) may prove useful as a method of proving access to external private keys, in the event that the community chooses to burn vulnerable coins as proposed by Jameson Lopp and others in [https://qbip.org/ QBIP].
+
+Discussions related to the burning of coins, and other attempts to slow a potential supply shock caused by quantum-retrieval of vulnerable coins, are out of scope for this proposal. That said, members of our team have separately proposed [https://github.com/cryptoquick/bips/blob/hourglass/bip-hourglass.mediawiki Hourglass] to address this concern and are continuing research on this subject.
+
+==Conclusion==
+
+In this proposal, we adopt a "prepared not scared" approach to the possible advancement of quantum computing and offer Bitcoin users an option for increased protection if they so choose. This BIP does not take a position on any specific quantum computing timeline, but rather proposes a flexible and unobtrusive option for users that wish to mitigate this risk according to their own estimate of the timeline.
+
+This is an issue that has been discussed with some regularity in [https://bitcointalk.org/index.php?topic=133425.0 Bitcoin forums] since at least 2012, and there is clearly user demand for increased quantum protection.
+
+==Glossary==
+
+'''Quantum Key Recovery'''
+
+The derivation of private keys from public keys in elliptic curve cryptography (ECC), made possible by solving the discrete logarithm problem (DLP).
+
+Shor's algorithm, developed by Peter Shor in 1994, is a quantum algorithm that efficiently solves the discrete logarithm problem — potentially made possible by the future viability of cryptographically relevant quantum computers (CRQCs).
+
+'''Long Exposure Attacks'''
+
+Attempts to derive private keys from public keys that are exposed for an extended period of time; that is, longer than the window of time that a public key is generally exposed in the mempool while waiting to be confirmed.
+
+Long exposure attacks give attackers an unlimited amount of time to perform quantum key recovery, as long as funds remain in the output. Poor wallet hygiene (e.g. from address reuse) or use of outputs with exposed public keys (e.g. P2TR outputs) increases vulnerability to long exposure attacks.
+
+'''Short Exposure Attacks'''
+
+Attempts to derive private keys from public keys during the brief period when funds are unconfirmed in the mempool. These attacks cannot be prevented through wallet hygiene, as revealing a public key is necessary for spending.
+
+Protection against short exposure attacks may require post-quantum signature schemes; that said, executing these attacks requires faster CRQCs than those capable of executing long exposure attacks and are therefore viewed as lower-risk than long exposure attacks in the nearer term.
+
+
+'''Script Tree Output Type'''
+
+Script tree output types are a category of output type that support a script tree consisting of leaf scripts. Script tree output types support tapscript and would support any new script language added to bitcoin which is able to be used as a leaf script in a script trees. If Pay-to-Merkle-Root (P2MR) is activated, P2MR would be the second script tree output type in Bitcoin, the other being Pay-to-Taproot (P2TR).
+
+'''Pay-to-Merkle-Root (P2MR)'''
+
+A script tree output type, similar to to Pay-to-Taproot (P2TR), but with the quantum-vulnerable key path spend removed.
+==Footnotes==
+
+
+
+==Changelog==
+
+To help implementers understand updates to this BIP, we keep a list of substantial changes.
+
+* __0.11.0__ (2026-02-10) - Rename BIP from Pay-to-Tapscript-Hash (P2TSH) to Pay-to-Merkle-Root (P2MR)
+* __0.10.3__ (2026-02-06) - Rename tapscript-native output type to script tree output type.
+* __0.10.2__ (2026-01-23) - Fix bug in verification, minor review comments and adopt [[bip-0003.mediawiki|BIP 003]] conventions.
+* __0.10.1__ (2026-01-21) - Terminology and clarity improvements, addressed feedback from reviews.
+* __0.10.0__ (2025-09-17) - Rewrote BIP for clarity and renamed from P2QRH to P2TSH
+* __0.9.0__ (2025-07-20) - Changed the Witness Version from 3 to 2.
+* __0.8.0__ (2025-07-07) - P2QRH is now a P2TR with the vulnerable key path spend removed. Number of PQ signature algorithms supported reduced from three to two. PQ signature algorithm support is now added via opcodes or leaf version.
+* __0.7.0__ (2025-03-18) - Correct inconsistencies in commitment and attestation structure. Switch from Merkle tree commitment to sorted vector hash commitment. Update descriptor format.
+* __0.6.2__ (2025-03-12) - Add verification times for each algorithm. 256 to 128 (NIST V to NIST I). Add key type bitmask. Clarify multisig semantics.
+* __0.6.1__ (2025-02-23) - More points of clarification from review. Update dead link.
+* __0.6.0__ (2025-01-20) - Remove SQIsign from consideration due to significant performance concerns. Refactor language from long range attack to long exposure so as to not be confused with the language around block re-org attacks.
+* __0.5.1__ (2024-12-18) - Assigned BIP number.
+* __0.5.0__ (2024-12-13) - Update to use Merkle tree for attestation commitment. Update LR & SR quantum attack scenarios.
+* __0.4.0__ (2024-12-01) - Add details on attestation structure and parsing.
+* __0.3.0__ (2024-10-21) - Replace XMSS with CRYSTALS-Dilithium due to NIST approval and size constraints.
+* __0.2.2__ (2024-09-30) - Refactor the ECC vs PoW section. Swap quitness for attestation.
+* __0.2.1__ (2024-09-29) - Update section on PoW to include partial-preimage.
+* __0.2.0__ (2024-09-28) - Add Winternitz, XMSS signatures, and security assumption types to PQC table. Omit NIST Level I table. Add spend script specification. Add revealed public key scenario table.
+* __0.1.0__ (2024-09-27) - Initial draft proposal
+
+==Acknowledgements==
+
+This document is inspired by [[bip-0341.mediawiki|BIP 341]], which introduced the design of the P2TR (Taproot) output type using Schnorr signatures.
+
+I'm incredibly grateful to Ethan Heilman for joining as co-author and transforming this BIP into something far more congruent with existing Bitcoin design. Additionally, much gratitude to our most recent co-author, Isabel Foxen Duke, for her thoughtful editing and crafting much of the language in this proposal. I am likewise indebted to those on the Anduro Quantum Working Group who took the time to contribute including Jeff Bride, Michael Casey, and notmike.
+
+Thank you as well to those who took the time to review and contribute, including Jon Atack, Adam Borcany, Ava Chow, Kyle Crews, Pierre-Luc Dallaire-Demers, D++, Mark Erhardt, Jameson Lopp, Antoine Riard, Armin Sabouri, Vojtěch Strnad, Guy Swann, and Joey Yandle.
+
+Whatever inaccuracies may remain are attributable solely to the authors.
diff --git a/packages/wasm-utxo/src/address/bech32.rs b/packages/wasm-utxo/src/address/bech32.rs
index 0431f88acb6..1201ddb3343 100644
--- a/packages/wasm-utxo/src/address/bech32.rs
+++ b/packages/wasm-utxo/src/address/bech32.rs
@@ -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 {
@@ -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)
}
@@ -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(),
))
}
}
diff --git a/packages/wasm-utxo/src/address/mod.rs b/packages/wasm-utxo/src/address/mod.rs
index 661795eedf0..5007dabbd86 100644
--- a/packages/wasm-utxo/src/address/mod.rs
+++ b/packages/wasm-utxo/src/address/mod.rs
@@ -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!(
@@ -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!(
@@ -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!(
@@ -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]
diff --git a/packages/wasm-utxo/src/address/networks.rs b/packages/wasm-utxo/src/address/networks.rs
index cef4f1c4e16..9e86112ff7a 100644
--- a/packages/wasm-utxo/src/address/networks.rs
+++ b/packages/wasm-utxo/src/address/networks.rs
@@ -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,
@@ -76,6 +77,7 @@ impl AddressFormat {
pub struct OutputScriptSupport {
pub segwit: bool,
pub taproot: bool,
+ pub p2mr: bool,
}
impl OutputScriptSupport {
@@ -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 => {
@@ -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(),
@@ -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,
+ }
}
}
@@ -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(),
));
}
@@ -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());
}
@@ -570,6 +596,7 @@ mod tests {
let support_segwit = OutputScriptSupport {
segwit: true,
taproot: false,
+ p2mr: false,
};
assert!(support_segwit.assert_segwit().is_ok());
@@ -577,6 +604,7 @@ mod tests {
let no_support = OutputScriptSupport {
segwit: false,
taproot: false,
+ p2mr: false,
};
let result = no_support.assert_segwit();
assert!(result.is_err());
@@ -592,6 +620,7 @@ mod tests {
let support_taproot = OutputScriptSupport {
segwit: true,
taproot: true,
+ p2mr: false,
};
assert!(support_taproot.assert_taproot().is_ok());
@@ -599,6 +628,7 @@ mod tests {
let no_support = OutputScriptSupport {
segwit: true,
taproot: false,
+ p2mr: false,
};
let result = no_support.assert_taproot();
assert!(result.is_err());
@@ -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());
@@ -640,6 +671,7 @@ mod tests {
let support_segwit = OutputScriptSupport {
segwit: true,
taproot: false,
+ p2mr: false,
};
assert!(support_segwit.assert_support(&p2wpkh_script).is_ok());
@@ -647,6 +679,7 @@ mod tests {
let no_support = OutputScriptSupport {
segwit: false,
taproot: false,
+ p2mr: false,
};
let result = no_support.assert_support(&p2wpkh_script);
assert!(result.is_err());
@@ -685,6 +718,7 @@ mod tests {
let support_taproot = OutputScriptSupport {
segwit: true,
taproot: true,
+ p2mr: false,
};
assert!(support_taproot.assert_support(&p2tr_script).is_ok());
@@ -692,6 +726,7 @@ mod tests {
let no_taproot = OutputScriptSupport {
segwit: true,
taproot: false,
+ p2mr: false,
};
let result = no_taproot.assert_support(&p2tr_script);
assert!(result.is_err());
@@ -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());
diff --git a/packages/wasm-utxo/src/address/utxolib_compat.rs b/packages/wasm-utxo/src/address/utxolib_compat.rs
index 8dca22fde5c..2d488c77734 100644
--- a/packages/wasm-utxo/src/address/utxolib_compat.rs
+++ b/packages/wasm-utxo/src/address/utxolib_compat.rs
@@ -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,
+ }
}
}
diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
index bd80fec66da..9e2847b1e19 100644
--- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
+++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
@@ -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),
diff --git a/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs
index 89bce8b4cee..ce01fd988e5 100644
--- a/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs
+++ b/packages/wasm-utxo/src/fixed_script_wallet/wallet_scripts/mod.rs
@@ -377,6 +377,7 @@ mod tests {
let no_segwit_support = OutputScriptSupport {
segwit: false,
taproot: false,
+ p2mr: false,
};
use OutputScriptType::*;
@@ -410,6 +411,7 @@ mod tests {
let no_taproot_support = OutputScriptSupport {
segwit: true,
taproot: false,
+ p2mr: false,
};
let result = WalletScripts::from_wallet_keys(
diff --git a/packages/wasm-utxo/src/lib.rs b/packages/wasm-utxo/src/lib.rs
index 096a4715abf..4628ec3422f 100644
--- a/packages/wasm-utxo/src/lib.rs
+++ b/packages/wasm-utxo/src/lib.rs
@@ -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)]
diff --git a/packages/wasm-utxo/src/p2mr/mod.rs b/packages/wasm-utxo/src/p2mr/mod.rs
new file mode 100644
index 00000000000..665377594ec
--- /dev/null
+++ b/packages/wasm-utxo/src/p2mr/mod.rs
@@ -0,0 +1,337 @@
+//! BIP-360 Pay-to-Merkle-Root (P2MR) support.
+//!
+//! Implements Merkle tree construction and control block generation for
+//! SegWit v2 P2MR outputs. The tree structure is identical to Taproot
+//! (BIP-341) but without key-path spending or TapTweak.
+//!
+//! See `bips/bip-0360/bip-0360.mediawiki` for the full specification.
+
+use miniscript::bitcoin::hashes::Hash;
+use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash, TapNodeHash};
+use miniscript::bitcoin::{Script, ScriptBuf};
+
+/// Default leaf version for TapScript (BIP 342).
+pub const TAPSCRIPT_LEAF_VERSION: u8 = 0xc0;
+
+/// Convert a raw leaf version byte to `LeafVersion`.
+fn leaf_version_from_u8(v: u8) -> LeafVersion {
+ if v == TAPSCRIPT_LEAF_VERSION {
+ LeafVersion::TapScript
+ } else {
+ LeafVersion::from_consensus(v).expect("valid even leaf version")
+ }
+}
+
+/// Compute the TapLeafHash for a script and leaf version.
+pub fn tap_leaf_hash(script: &[u8], leaf_version: u8) -> [u8; 32] {
+ TapLeafHash::from_script(
+ Script::from_bytes(script),
+ leaf_version_from_u8(leaf_version),
+ )
+ .to_byte_array()
+}
+
+/// Compute the TapBranchHash from two node hashes (lexicographically sorted).
+pub fn tap_branch_hash(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
+ TapNodeHash::from_node_hashes(
+ TapNodeHash::assume_hidden(*a),
+ TapNodeHash::assume_hidden(*b),
+ )
+ .to_byte_array()
+}
+
+/// P2MR control byte: leaf_version with parity bit forced to 1 (BIP-360 consensus rule).
+///
+/// Unlike Taproot where the parity bit encodes the output key's Y parity,
+/// P2MR always sets the LSB to 1 as a distinguisher.
+pub fn p2mr_control_byte(leaf_version: u8) -> u8 {
+ (leaf_version & 0xfe) | 0x01
+}
+
+/// Build the 34-byte P2MR scriptPubKey: `OP_2 (0x52) OP_PUSHBYTES_32 (0x20) `
+pub fn build_p2mr_script_pubkey(merkle_root: &[u8; 32]) -> ScriptBuf {
+ let mut bytes = Vec::with_capacity(34);
+ bytes.push(0x52); // OP_2
+ bytes.push(0x20); // OP_PUSHBYTES_32
+ bytes.extend_from_slice(merkle_root);
+ ScriptBuf::from(bytes)
+}
+
+/// A node in a P2MR script tree (recursive structure matching BIP-360 test vectors).
+///
+/// Trees are specified as either a single leaf or a pair of branches:
+/// - Single leaf: `Leaf { script, leaf_version }`
+/// - Two branches: `Branch(left, right)` where each side is another `ScriptTreeNode`
+///
+/// The BIP-360 test vectors use JSON arrays for branches and objects for leaves:
+/// - `{ "script": "...", "leafVersion": 192 }` → leaf
+/// - `[node, node]` → branch
+#[derive(Debug, Clone)]
+pub enum ScriptTreeNode {
+ Leaf { script: Vec, leaf_version: u8 },
+ Branch(Box, Box),
+}
+
+/// Per-leaf spending info produced by tree construction.
+#[derive(Debug, Clone)]
+pub struct P2mrLeafInfo {
+ /// The TapLeafHash for this leaf.
+ pub leaf_hash: [u8; 32],
+ /// Serialized control block: `control_byte || merkle_path_siblings`.
+ /// Length is `1 + 32 * depth`.
+ pub control_block: Vec,
+}
+
+/// Complete P2MR tree info.
+#[derive(Debug, Clone)]
+pub struct P2mrTreeInfo {
+ /// The 32-byte Merkle root (committed in the scriptPubKey).
+ pub merkle_root: [u8; 32],
+ /// Per-leaf spending info, in left-to-right DFS order.
+ pub leaves: Vec,
+}
+
+/// Intermediate leaf data collected during tree traversal: (leaf_hash, leaf_version, merkle_path).
+type LeafCollector = Vec<([u8; 32], u8, Vec<[u8; 32]>)>;
+
+/// Build a P2MR Merkle tree from a script tree definition.
+///
+/// Returns the Merkle root and per-leaf spending info (leaf hash + control block).
+/// Leaves are returned in left-to-right DFS order matching the input tree structure.
+pub fn build_p2mr_tree(tree: &ScriptTreeNode) -> P2mrTreeInfo {
+ let mut leaves: LeafCollector = Vec::new();
+ let merkle_root = compute_node(tree, &mut leaves);
+
+ let leaf_infos = leaves
+ .into_iter()
+ .map(|(leaf_hash, leaf_version, path)| {
+ let mut control_block = vec![p2mr_control_byte(leaf_version)];
+ for sibling in &path {
+ control_block.extend_from_slice(sibling);
+ }
+ P2mrLeafInfo {
+ leaf_hash,
+ control_block,
+ }
+ })
+ .collect();
+
+ P2mrTreeInfo {
+ merkle_root,
+ leaves: leaf_infos,
+ }
+}
+
+/// Recursively compute the hash of a tree node, collecting leaf info along the way.
+///
+/// Each leaf entry stores (leaf_hash, leaf_version, merkle_path_siblings).
+/// Leaves are output in input DFS order (left subtree before right subtree).
+fn compute_node(node: &ScriptTreeNode, leaves: &mut LeafCollector) -> [u8; 32] {
+ match node {
+ ScriptTreeNode::Leaf {
+ script,
+ leaf_version,
+ } => {
+ let hash = tap_leaf_hash(script, *leaf_version);
+ leaves.push((hash, *leaf_version, Vec::new()));
+ hash
+ }
+ ScriptTreeNode::Branch(left, right) => {
+ let left_start = leaves.len();
+ let left_hash = compute_node(left, leaves);
+ let right_start = leaves.len();
+ let right_hash = compute_node(right, leaves);
+
+ // Add sibling hashes to the merkle proof paths.
+ // Left subtree leaves need right_hash as sibling, and vice versa.
+ for leaf in leaves[left_start..right_start].iter_mut() {
+ leaf.2.push(right_hash);
+ }
+ for leaf in leaves[right_start..].iter_mut() {
+ leaf.2.push(left_hash);
+ }
+
+ tap_branch_hash(&left_hash, &right_hash)
+ }
+ }
+}
+
+/// Verify a P2MR control block against a leaf hash and expected merkle root.
+///
+/// Walks the merkle path in the control block, combining with `tap_branch_hash`
+/// at each step, and checks the result matches the expected root.
+pub fn verify_control_block(
+ leaf_hash: &[u8; 32],
+ control_block: &[u8],
+ expected_root: &[u8; 32],
+) -> bool {
+ if control_block.is_empty() {
+ return false;
+ }
+ // Control block: 1 byte (control_byte) + 32*d bytes (merkle path)
+ let path_bytes = &control_block[1..];
+ if !path_bytes.len().is_multiple_of(32) {
+ return false;
+ }
+
+ let mut current = *leaf_hash;
+ for chunk in path_bytes.chunks_exact(32) {
+ let sibling: [u8; 32] = chunk.try_into().unwrap();
+ current = tap_branch_hash(¤t, &sibling);
+ }
+ current == *expected_root
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn load_fixture() -> serde_json::Value {
+ let content = std::fs::read_to_string("test/fixtures/p2mr/p2mr_construction.json")
+ .expect("Failed to load p2mr_construction.json");
+ serde_json::from_str(&content).expect("Failed to parse fixture")
+ }
+
+ fn get_vector(fixture: &serde_json::Value, id: &str) -> serde_json::Value {
+ fixture["test_vectors"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .find(|v| v["id"].as_str() == Some(id))
+ .unwrap_or_else(|| panic!("Vector '{}' not found", id))
+ .clone()
+ }
+
+ /// Parse a fixture scriptTree node into our ScriptTreeNode.
+ fn parse_fixture_tree(node: &serde_json::Value) -> ScriptTreeNode {
+ if node.is_array() {
+ let arr = node.as_array().unwrap();
+ assert_eq!(arr.len(), 2);
+ ScriptTreeNode::Branch(
+ Box::new(parse_fixture_tree(&arr[0])),
+ Box::new(parse_fixture_tree(&arr[1])),
+ )
+ } else {
+ ScriptTreeNode::Leaf {
+ script: hex::decode(node["script"].as_str().unwrap()).unwrap(),
+ leaf_version: node["leafVersion"].as_u64().unwrap() as u8,
+ }
+ }
+ }
+
+ /// Run a construction test vector: build the tree and verify merkle root,
+ /// leaf hashes, scriptPubKey, and control blocks against the fixture.
+ fn run_construction_vector(id: &str) {
+ let fixture = load_fixture();
+ let vector = get_vector(&fixture, id);
+
+ let script_tree = &vector["given"]["scriptTree"];
+ let tree = parse_fixture_tree(script_tree);
+ let info = build_p2mr_tree(&tree);
+
+ // Verify merkle root
+ let expected_root = vector["intermediary"]["merkleRoot"].as_str().unwrap();
+ assert_eq!(hex::encode(info.merkle_root), expected_root, "merkle root");
+
+ // Verify leaf hashes
+ let expected_leaf_hashes: Vec<&str> = vector["intermediary"]["leafHashes"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|v| v.as_str().unwrap())
+ .collect();
+ assert_eq!(info.leaves.len(), expected_leaf_hashes.len(), "leaf count");
+ let actual_leaf_hashes: Vec = info
+ .leaves
+ .iter()
+ .map(|l| hex::encode(l.leaf_hash))
+ .collect();
+ for expected in &expected_leaf_hashes {
+ assert!(
+ actual_leaf_hashes.contains(&expected.to_string()),
+ "missing leaf hash: {}",
+ expected
+ );
+ }
+
+ // Verify scriptPubKey
+ let expected_spk = vector["expected"]["scriptPubKey"].as_str().unwrap();
+ let spk = build_p2mr_script_pubkey(&info.merkle_root);
+ assert_eq!(hex::encode(spk.as_bytes()), expected_spk, "scriptPubKey");
+
+ // Verify bip350 address if present
+ if let Some(expected_addr) = vector["expected"]["bip350Address"].as_str() {
+ let addr = crate::from_output_script_with_coin(&spk, "btc")
+ .expect("failed to encode P2MR address");
+ assert_eq!(addr, expected_addr, "bip350Address");
+ }
+
+ // Verify control blocks (order-independent, validated cryptographically)
+ let expected_cbs: Vec<&str> = vector["expected"]["scriptPathControlBlocks"]
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|v| v.as_str().unwrap())
+ .collect();
+ assert_eq!(info.leaves.len(), expected_cbs.len(), "control block count");
+
+ // Each generated control block must verify against the merkle root
+ for leaf in &info.leaves {
+ assert!(
+ verify_control_block(&leaf.leaf_hash, &leaf.control_block, &info.merkle_root),
+ "control block for leaf {} doesn't verify",
+ hex::encode(leaf.leaf_hash)
+ );
+ }
+ // Each expected control block must verify against some leaf
+ for cb_hex in &expected_cbs {
+ let cb = hex::decode(cb_hex).unwrap();
+ let verified = info
+ .leaves
+ .iter()
+ .any(|l| verify_control_block(&l.leaf_hash, &cb, &info.merkle_root));
+ assert!(
+ verified,
+ "expected control block doesn't verify: {}",
+ cb_hex
+ );
+ }
+ }
+
+ #[test]
+ fn test_p2mr_control_byte() {
+ assert_eq!(p2mr_control_byte(0xc0), 0xc1);
+ assert_eq!(p2mr_control_byte(0xfa), 0xfb);
+ assert_eq!(p2mr_control_byte(0xc1), 0xc1);
+ }
+
+ #[test]
+ fn test_single_leaf_tree() {
+ run_construction_vector("p2mr_single_leaf_script_tree");
+ }
+
+ #[test]
+ fn test_two_leaf_same_version() {
+ run_construction_vector("p2mr_two_leaf_same_version");
+ }
+
+ #[test]
+ fn test_different_version_leaves() {
+ run_construction_vector("p2mr_different_version_leaves");
+ }
+
+ #[test]
+ fn test_simple_lightning_contract() {
+ run_construction_vector("p2mr_simple_lightning_contract");
+ }
+
+ #[test]
+ fn test_three_leaf_complex() {
+ run_construction_vector("p2mr_three_leaf_complex");
+ }
+
+ #[test]
+ fn test_three_leaf_alternative() {
+ run_construction_vector("p2mr_three_leaf_alternative");
+ }
+}
diff --git a/packages/wasm-utxo/test/address/utxolibCompat.ts b/packages/wasm-utxo/test/address/utxolibCompat.ts
index d2b98d18d45..e3e0546cf86 100644
--- a/packages/wasm-utxo/test/address/utxolibCompat.ts
+++ b/packages/wasm-utxo/test/address/utxolibCompat.ts
@@ -33,7 +33,9 @@ function runTest(network: utxolib.Network, addressFormat?: AddressFormat) {
});
it("should convert to utxolib compatible network", function () {
- for (const fixture of fixtures) {
+ // P2MR is not supported via the utxolib compat layer (only via Network enum)
+ const compatFixtures = fixtures.filter(([type]) => type !== "p2mr");
+ for (const fixture of compatFixtures) {
const [, script, addressRef] = fixture;
const scriptBuf = Buffer.from(script, "hex");
const address = utxolibCompat.fromOutputScript(scriptBuf, network, addressFormat);
diff --git a/packages/wasm-utxo/test/fixtures/address/bitcoin.json b/packages/wasm-utxo/test/fixtures/address/bitcoin.json
index 4c40b8ff540..01b8efbb190 100644
--- a/packages/wasm-utxo/test/fixtures/address/bitcoin.json
+++ b/packages/wasm-utxo/test/fixtures/address/bitcoin.json
@@ -9,11 +9,7 @@
"00141e231c7f9b3415daaa53ee5a7e12e120f00ec212",
"bc1qrc33clumxs2a42jnaed8uyhpyrcqassje6kugr"
],
- [
- "p2sh",
- "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87",
- "33GaWMLihvhqWJ9YNd7xo8diSh6otdfi5w"
- ],
+ ["p2sh", "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87", "33GaWMLihvhqWJ9YNd7xo8diSh6otdfi5w"],
[
"p2shP2wsh",
"a9140c4e25aa3282fa35888f5e1eedb876265328312587",
@@ -44,11 +40,7 @@
"001491b8f56f155030f74259be43dff4d94a6258d84a",
"bc1qjxu02mc42qc0wsjehepalaxeff393kz2mzwu70"
],
- [
- "p2sh",
- "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87",
- "3MDt56c1ME4Vi8aYP9DJdhWngxo3yDxGJb"
- ],
+ ["p2sh", "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87", "3MDt56c1ME4Vi8aYP9DJdhWngxo3yDxGJb"],
[
"p2shP2wsh",
"a914696cab5f237c954fc1fade8c6b234fe93e0e80f287",
@@ -79,11 +71,7 @@
"00140a058aec7588fca80070436b020c352c2891b680",
"bc1qpgzc4mr43r72sqrsgd4syrp49s5frd5qq7et93"
],
- [
- "p2sh",
- "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87",
- "32BkhNhA8a7byNxgz6RR3jrG8T1mCXqiC8"
- ],
+ ["p2sh", "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87", "32BkhNhA8a7byNxgz6RR3jrG8T1mCXqiC8"],
[
"p2shP2wsh",
"a914f7db4f654f1211a63165cfdaf1170e96d433bc1387",
@@ -114,11 +102,7 @@
"00145a8451539186feb4578b4f5613df6991e3078230",
"bc1qt2z9z5u3smltg4utfatp8hmfj83s0q3s62ygc4"
],
- [
- "p2sh",
- "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187",
- "3FZavdFukrNR5aJcL7dmbc18tZ9tBkVtqK"
- ],
+ ["p2sh", "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187", "3FZavdFukrNR5aJcL7dmbc18tZ9tBkVtqK"],
[
"p2shP2wsh",
"a91493f1dd87104175795a1e37f5245461827237a05787",
@@ -138,5 +122,20 @@
"p2trMusig2",
"512085078b6ce45af8c4dea63248a5fae283d8edcca75f186395b237706c4bb42a36",
"bc1ps5rckm8yttuvfh4xxfy2t7hzs0vwmn98tuvx89djxacxcja59gmq8lx0zu"
+ ],
+ [
+ "p2mr",
+ "5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b",
+ "bc1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4ssr0v9k"
+ ],
+ [
+ "p2mr",
+ "5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc",
+ "bc1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxqz20g2y"
+ ],
+ [
+ "p2mr",
+ "5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2",
+ "bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq"
]
]
diff --git a/packages/wasm-utxo/test/fixtures/address/bitcoinPublicSignet.json b/packages/wasm-utxo/test/fixtures/address/bitcoinPublicSignet.json
index 4c0b81780a8..ff7103eb24b 100644
--- a/packages/wasm-utxo/test/fixtures/address/bitcoinPublicSignet.json
+++ b/packages/wasm-utxo/test/fixtures/address/bitcoinPublicSignet.json
@@ -9,11 +9,7 @@
"00141e231c7f9b3415daaa53ee5a7e12e120f00ec212",
"tb1qrc33clumxs2a42jnaed8uyhpyrcqassjnud0ns"
],
- [
- "p2sh",
- "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87",
- "2Mtpna6GkKPDBi5n63kjqR5cyf3JybU8XmH"
- ],
+ ["p2sh", "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87", "2Mtpna6GkKPDBi5n63kjqR5cyf3JybU8XmH"],
[
"p2shP2wsh",
"a9140c4e25aa3282fa35888f5e1eedb876265328312587",
@@ -44,11 +40,7 @@
"001491b8f56f155030f74259be43dff4d94a6258d84a",
"tb1qjxu02mc42qc0wsjehepalaxeff393kz23y409u"
],
- [
- "p2sh",
- "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87",
- "2NCn68qY2xgZquvD64GqBFeW3uK1Dqn2PA4"
- ],
+ ["p2sh", "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87", "2NCn68qY2xgZquvD64GqBFeW3uK1Dqn2PA4"],
[
"p2shP2wsh",
"a914696cab5f237c954fc1fade8c6b234fe93e0e80f287",
@@ -79,11 +71,7 @@
"00140a058aec7588fca80070436b020c352c2891b680",
"tb1qpgzc4mr43r72sqrsgd4syrp49s5frd5q2czc7z"
],
- [
- "p2sh",
- "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87",
- "2Msjxm7dBk2cxBAbEfE3HfgqXLoDw5wBFpQ"
- ],
+ ["p2sh", "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87", "2Msjxm7dBk2cxBAbEfE3HfgqXLoDw5wBFpQ"],
[
"p2shP2wsh",
"a914f7db4f654f1211a63165cfdaf1170e96d433bc1387",
@@ -114,11 +102,7 @@
"00145a8451539186feb4578b4f5613df6991e3078230",
"tb1qt2z9z5u3smltg4utfatp8hmfj83s0q3ssvlmrx"
],
- [
- "p2sh",
- "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187",
- "2N77nzNBwNJsmHMwA1FFeDYzQ6uN3yhET3E"
- ],
+ ["p2sh", "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187", "2N77nzNBwNJsmHMwA1FFeDYzQ6uN3yhET3E"],
[
"p2shP2wsh",
"a91493f1dd87104175795a1e37f5245461827237a05787",
@@ -138,5 +122,20 @@
"p2trMusig2",
"512085078b6ce45af8c4dea63248a5fae283d8edcca75f186395b237706c4bb42a36",
"tb1ps5rckm8yttuvfh4xxfy2t7hzs0vwmn98tuvx89djxacxcja59gmqshsqcn"
+ ],
+ [
+ "p2mr",
+ "5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b",
+ "tb1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4s8terle"
+ ],
+ [
+ "p2mr",
+ "5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc",
+ "tb1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxq4ze8st"
+ ],
+ [
+ "p2mr",
+ "5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2",
+ "tb1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3q4q0p60"
]
-]
\ No newline at end of file
+]
diff --git a/packages/wasm-utxo/test/fixtures/address/testnet.json b/packages/wasm-utxo/test/fixtures/address/testnet.json
index d71b196229b..ff7103eb24b 100644
--- a/packages/wasm-utxo/test/fixtures/address/testnet.json
+++ b/packages/wasm-utxo/test/fixtures/address/testnet.json
@@ -9,11 +9,7 @@
"00141e231c7f9b3415daaa53ee5a7e12e120f00ec212",
"tb1qrc33clumxs2a42jnaed8uyhpyrcqassjnud0ns"
],
- [
- "p2sh",
- "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87",
- "2Mtpna6GkKPDBi5n63kjqR5cyf3JybU8XmH"
- ],
+ ["p2sh", "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87", "2Mtpna6GkKPDBi5n63kjqR5cyf3JybU8XmH"],
[
"p2shP2wsh",
"a9140c4e25aa3282fa35888f5e1eedb876265328312587",
@@ -44,11 +40,7 @@
"001491b8f56f155030f74259be43dff4d94a6258d84a",
"tb1qjxu02mc42qc0wsjehepalaxeff393kz23y409u"
],
- [
- "p2sh",
- "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87",
- "2NCn68qY2xgZquvD64GqBFeW3uK1Dqn2PA4"
- ],
+ ["p2sh", "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87", "2NCn68qY2xgZquvD64GqBFeW3uK1Dqn2PA4"],
[
"p2shP2wsh",
"a914696cab5f237c954fc1fade8c6b234fe93e0e80f287",
@@ -79,11 +71,7 @@
"00140a058aec7588fca80070436b020c352c2891b680",
"tb1qpgzc4mr43r72sqrsgd4syrp49s5frd5q2czc7z"
],
- [
- "p2sh",
- "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87",
- "2Msjxm7dBk2cxBAbEfE3HfgqXLoDw5wBFpQ"
- ],
+ ["p2sh", "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87", "2Msjxm7dBk2cxBAbEfE3HfgqXLoDw5wBFpQ"],
[
"p2shP2wsh",
"a914f7db4f654f1211a63165cfdaf1170e96d433bc1387",
@@ -114,11 +102,7 @@
"00145a8451539186feb4578b4f5613df6991e3078230",
"tb1qt2z9z5u3smltg4utfatp8hmfj83s0q3ssvlmrx"
],
- [
- "p2sh",
- "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187",
- "2N77nzNBwNJsmHMwA1FFeDYzQ6uN3yhET3E"
- ],
+ ["p2sh", "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187", "2N77nzNBwNJsmHMwA1FFeDYzQ6uN3yhET3E"],
[
"p2shP2wsh",
"a91493f1dd87104175795a1e37f5245461827237a05787",
@@ -138,5 +122,20 @@
"p2trMusig2",
"512085078b6ce45af8c4dea63248a5fae283d8edcca75f186395b237706c4bb42a36",
"tb1ps5rckm8yttuvfh4xxfy2t7hzs0vwmn98tuvx89djxacxcja59gmqshsqcn"
+ ],
+ [
+ "p2mr",
+ "5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b",
+ "tb1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4s8terle"
+ ],
+ [
+ "p2mr",
+ "5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc",
+ "tb1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxq4ze8st"
+ ],
+ [
+ "p2mr",
+ "5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2",
+ "tb1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3q4q0p60"
]
]
diff --git a/packages/wasm-utxo/test/fixtures/p2mr/p2mr_construction.json b/packages/wasm-utxo/test/fixtures/p2mr/p2mr_construction.json
new file mode 100644
index 00000000000..5ba9471d120
--- /dev/null
+++ b/packages/wasm-utxo/test/fixtures/p2mr/p2mr_construction.json
@@ -0,0 +1,255 @@
+{
+ "version": 1,
+ "test_vectors": [
+ {
+ "id": "p2tr_using_v2_witness_version_error",
+ "objective": "Tests that a P2TR v2 scriptPubKey fails with use of witness version 2",
+ "given": {
+ "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d",
+ "scriptTree": null
+ },
+ "intermediary": {
+ "merkleRoot": null,
+ "tweak": "b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70",
+ "tweakedPubkey": "53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343"
+ },
+ "expected": {
+ "scriptPubKey": "522053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343",
+ "error": "P2TR requires witness version of 1"
+ }
+ },
+ {
+ "id": "p2mr_missing_leaf_script_tree_error",
+ "objective": "Tests P2MR with missing leaf script tree",
+ "given": {
+ "script_tree": ""
+ },
+ "intermediary": {},
+ "expected": {
+ "error": "P2MR requires a script tree with at least one leaf"
+ }
+ },
+ {
+ "id": "p2mr_single_leaf_script_tree",
+ "objective": "Tests P2MR with single leaf script tree",
+ "given": {
+ "scriptTree": {
+ "id": 0,
+ "script": "20b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007ac",
+ "asm": "b617298552a72ade070667e86ca63b8f5789a9fe8731ef91202a91c9f3459007 OP_CHECKSIG",
+ "leafVersion": 192
+ }
+ },
+ "intermediary": {
+ "leafHashes": ["c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b"],
+ "merkleRoot": "c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b"
+ },
+ "expected": {
+ "scriptPubKey": "5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b",
+ "bip350Address": "bc1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4ssr0v9k",
+ "scriptPathControlBlocks": ["c1"]
+ }
+ },
+ {
+ "id": "p2mr_different_version_leaves",
+ "objective": "Tests P2MR with two script leaves of different versions. TO-DO: currently ignores given leaf version and over-rides. Probably better to throw error",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "20387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48ac",
+ "asm": "387671353e273264c495656e27e39ba899ea8fee3bb69fb2a680e22093447d48 OP_CHECKSIG",
+ "leafVersion": 192
+ },
+ {
+ "id": 1,
+ "script": "06424950333431",
+ "asm": "424950333431",
+ "description": "just pushes the string 'BIP341' onto the stack",
+ "leafVersion": 250
+ }
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7",
+ "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a"
+ ],
+ "merkleRoot": "6c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef"
+ },
+ "expected": {
+ "scriptPubKey": "52206c2dc106ab816b73f9d07e3cd1ef2c8c1256f519748e0813e4edd2405d277bef",
+ "bip350Address": "bc1zdskuzp4ts94h87ws0c7drmev3sf9dagewj8qsylyahfyqhf800hsam4d6e",
+ "scriptPathControlBlocks": [
+ "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a",
+ "c18ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_simple_lightning_contract",
+ "objective": "Tests P2MR with two script leaves that simulate a simple lightning network contract. Reference: https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example-with-tap.md",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "029000b275209997a497d964fc1a62885b05a51166a65a90df00492c8d7cf61d6accf54803beac",
+ "asm": "144 OP_CHECKSEQUENCEVERIFY OP_DROP 9997a497d964fc1a62885b05a51166a65a90df00492c8d7cf61d6accf54803be OP_CHECKSIG",
+ "description": "Alice takes the money after waiting 1 day",
+ "leafVersion": 192
+ },
+ {
+ "id": 1,
+ "script": "a8206c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd533388204edfcf9dfe6c0b5c83d1ab3f78d1b39a46ebac6798e08e19761f5ed89ec83c10ac",
+ "asm": "OP_SHA256 6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333 OP_EQUALVERIFY 4edfcf9dfe6c0b5c83d1ab3f78d1b39a46ebac6798e08e19761f5ed89ec83c10 OP_CHECKSIG",
+ "description": "Bob takes the money whenever he wishes to by revealing the preimage_hash",
+ "leafVersion": 192
+ }
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9",
+ "632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42"
+ ],
+ "merkleRoot": "41646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b"
+ },
+ "expected": {
+ "scriptPubKey": "522041646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b",
+ "bip350Address": "bc1zg9jxlrqlu25kmkkh74r3h387uldfs72wlrz95n60c6j4n4svna4s4lhfhe",
+ "scriptPathControlBlocks": [
+ "c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9",
+ "c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_two_leaf_same_version",
+ "objective": "Tests P2MR with two script leaves of same version",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "2044b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fdac",
+ "asm": "44b178d64c32c4a05cc4f4d1407268f764c940d20ce97abfd44db5c3592b72fd OP_CHECKSIG",
+ "leafVersion": 192
+ },
+ {
+ "id": 1,
+ "script": "07546170726f6f74",
+ "asm": "546170726f6f74",
+ "description": "pushes the string 'Taproot' onto the stack",
+ "leafVersion": 192
+ }
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "64512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89",
+ "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb"
+ ],
+ "merkleRoot": "ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc"
+ },
+ "expected": {
+ "scriptPubKey": "5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc",
+ "bip350Address": "bc1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxqz20g2y",
+ "scriptPathControlBlocks": [
+ "c12cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb",
+ "c164512fecdb5afa04f98839b50e6f0cb7b1e539bf6f205f67934083cdcc3c8d89"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_three_leaf_complex",
+ "objective": "Tests P2MR with a complex three-leaf script tree structure, demonstrating nested script paths and multiple verification options",
+ "given": {
+ "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f",
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "2072ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69ac",
+ "asm": "72ea6adcf1d371dea8fba1035a09f3d24ed5a059799bae114084130ee5898e69 OP_CHECKSIG",
+ "leafVersion": 192
+ },
+ [
+ {
+ "id": 1,
+ "script": "202352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8ac",
+ "asm": "2352d137f2f3ab38d1eaa976758873377fa5ebb817372c71e2c542313d4abda8 OP_CHECKSIG",
+ "leafVersion": 192
+ },
+ {
+ "id": 2,
+ "script": "207337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186aac",
+ "asm": "7337c0dd4253cb86f2c43a2351aadd82cccb12a172cd120452b9bb8324f2186a OP_CHECKSIG",
+ "leafVersion": 192
+ }
+ ]
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817",
+ "ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c",
+ "9e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf6"
+ ],
+ "merkleRoot": "ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2"
+ },
+ "expected": {
+ "scriptPubKey": "5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2",
+ "bip350Address": "bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq",
+ "scriptPathControlBlocks": [
+ "c1ffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553",
+ "c1ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817",
+ "c19e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_three_leaf_alternative",
+ "objective": "Tests another variant of P2MR with three leaves arranged in a different tree structure, showing alternative script path spending options",
+ "given": {
+ "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d",
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac",
+ "asm": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG",
+ "leafVersion": 192
+ },
+ [
+ {
+ "id": 1,
+ "script": "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac",
+ "asm": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG",
+ "leafVersion": 192
+ },
+ {
+ "id": 2,
+ "script": "20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac",
+ "asm": "c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4c OP_CHECKSIG",
+ "leafVersion": 192
+ }
+ ]
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d",
+ "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711",
+ "d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7"
+ ],
+ "merkleRoot": "2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def"
+ },
+ "expected": {
+ "scriptPubKey": "52202f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def",
+ "bip350Address": "bc1z9a4jc5uhkmtgegvwpx3lq5tpv68layaf3pvz64wx7paatvejnhhsv52lcv",
+ "scriptPathControlBlocks": [
+ "c13cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91",
+ "c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d",
+ "c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d"
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/wasm-utxo/test/fixtures/p2mr/p2mr_pqc_construction.json b/packages/wasm-utxo/test/fixtures/p2mr/p2mr_pqc_construction.json
new file mode 100644
index 00000000000..ee0fc48bb0f
--- /dev/null
+++ b/packages/wasm-utxo/test/fixtures/p2mr/p2mr_pqc_construction.json
@@ -0,0 +1,247 @@
+{
+ "version": 1,
+ "test_vectors": [
+ {
+ "id": "p2mr_missing_leaf_script_tree_error",
+ "objective": "Tests P2MR with missing leaf script tree",
+ "given": {
+ "script_tree": ""
+ },
+ "intermediary": {},
+ "expected": {
+ "error": "P2MR requires a script tree with at least one leaf"
+ }
+ },
+ {
+ "id": "p2mr_single_leaf_script_tree",
+ "objective": "Tests P2MR with single leaf script tree",
+ "given": {
+ "scriptTree": {
+ "id": 0,
+ "script": "201f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f247f",
+ "asm": "1f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24 OP_SUBSTR",
+ "priv_key": "3f0213a51771f25c906cd82c656175e4b2a10e85958039d6e358352a2eb62b791f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24",
+ "leafVersion": 192
+ }
+ },
+ "intermediary": {
+ "leafHashes": ["3bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc"],
+ "merkleRoot": "3bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc"
+ },
+ "expected": {
+ "scriptPubKey": "52203bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc",
+ "bip350Address": "bc1z8wcdhrr2mnv8xv9y4ry3hc87rv3a50q4rdhjld8jd9pfcsacmz7qg5szm8",
+ "scriptPathControlBlocks": ["c1"]
+ }
+ },
+ {
+ "id": "p2mr_different_version_leaves",
+ "objective": "Tests P2MR with two script leaves of different versions. TO-DO: currently ignores given leaf version and over-rides. Probably better to throw error",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "201f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f247f",
+ "asm": "1f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24 OP_SUBSTR",
+ "priv_key": "3f0213a51771f25c906cd82c656175e4b2a10e85958039d6e358352a2eb62b791f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24",
+ "leafVersion": 192
+ },
+ {
+ "id": 1,
+ "script": "06424950333431",
+ "asm": "424950333431",
+ "description": "just pushes the string 'BIP341' onto the stack",
+ "leafVersion": 250
+ }
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "3bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc",
+ "f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a"
+ ],
+ "merkleRoot": "1619ce6d22a46dea045c4adf7f5f33d6810d00d0e9c8a4c7ba35db37b915c604"
+ },
+ "expected": {
+ "scriptPubKey": "52201619ce6d22a46dea045c4adf7f5f33d6810d00d0e9c8a4c7ba35db37b915c604",
+ "bip350Address": "bc1zzcvuumfz53k75pzuft0h7hen66qs6qxsa8y2f3a6xhdn0wg4cczq0h84sj",
+ "scriptPathControlBlocks": [
+ "c13bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc",
+ "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_simple_lightning_contract",
+ "objective": "Tests P2MR with two script leaves that simulate a simple lightning network contract. Reference: https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example-with-tap.md",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "029000b275201f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f247f",
+ "asm": "144 OP_CHECKSEQUENCEVERIFY OP_DROP 1f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24 OP_SUBSTR",
+ "priv_key": "3f0213a51771f25c906cd82c656175e4b2a10e85958039d6e358352a2eb62b791f4b1febdc7dfa77c1efacc875b43317b46155e5a564decf475fc369124c5f24",
+ "description": "Alice takes the money after waiting 1 day",
+ "leafVersion": 192
+ },
+ {
+ "id": 1,
+ "script": "a8206c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd533388204c7f98ab73cc36abeb76d6eace6a9d8b0ff69dfe0f4a17e4f94f0898ec52fadd",
+ "asm": "OP_SHA256 6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333 OP_EQUALVERIFY 4c7f98ab73cc36abeb76d6eace6a9d8b0ff69dfe0f4a17e4f94f0898ec52fadd OP_SUBSTR",
+ "priv_key": "49a4214f386240d97ea68efb4268043fd5a55208dcdac18ce5bd3332b8e488944c7f98ab73cc36abeb76d6eace6a9d8b0ff69dfe0f4a17e4f94f0898ec52fadd",
+ "description": "Bob takes the money whenever he wishes to by revealing the preimage_hash",
+ "leafVersion": 192
+ }
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "cfd5fc07ac39947cba799e14f933f20e7c233dea72dc2792f5547c58cdce743e",
+ "a9745ac96d4f3702b78751f1e08f3040fbe6347e7b4f520d22d3f907730cbb7e"
+ ],
+ "merkleRoot": "2794771cd51f215ba3a19fbcdf08c771edb7de782a0c34457e0e9be5d0e4008f"
+ },
+ "expected": {
+ "scriptPubKey": "52202794771cd51f215ba3a19fbcdf08c771edb7de782a0c34457e0e9be5d0e4008f",
+ "bip350Address": "bc1zy728w8x4rus4hgapn77d7zx8w8km0hnc9gxrg3t7p6d7t58yqz8sg0nccq",
+ "scriptPathControlBlocks": [
+ "c1cfd5fc07ac39947cba799e14f933f20e7c233dea72dc2792f5547c58cdce743e",
+ "c1a9745ac96d4f3702b78751f1e08f3040fbe6347e7b4f520d22d3f907730cbb7e"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_two_leaf_same_version",
+ "objective": "Tests P2MR with two script leaves of same version",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "20e6b3dad32fc04095a021f5356163cfc14e15f703831e332c56f6b499801f53447f",
+ "asm": "e6b3dad32fc04095a021f5356163cfc14e15f703831e332c56f6b499801f5344 OP_SUBSTR",
+ "priv_key": "a695d8a1351774d59ed1b980462c2ab8d58b3b7a48f55b114c6a39980dc1c13ee6b3dad32fc04095a021f5356163cfc14e15f703831e332c56f6b499801f5344",
+ "leafVersion": 192
+ },
+ {
+ "id": 1,
+ "script": "07546170726f6f74",
+ "asm": "546170726f6f74",
+ "description": "pushes the string 'Taproot' onto the stack",
+ "leafVersion": 192
+ }
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "9de7eeded7832c28c6f80de76904dd79f98fd302747823b5bc5be440186b0c6d",
+ "2cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb"
+ ],
+ "merkleRoot": "5112b3edfd2c0b717491e9d4888ed2d5dfeaa25115143540e0a08516b68c008c"
+ },
+ "expected": {
+ "scriptPubKey": "52205112b3edfd2c0b717491e9d4888ed2d5dfeaa25115143540e0a08516b68c008c",
+ "bip350Address": "bc1z2yft8m0a9s9hzay3a82g3rkj6h074gj3z52r2s8q5zz3dd5vqzxqngpk2w",
+ "scriptPathControlBlocks": [
+ "c19de7eeded7832c28c6f80de76904dd79f98fd302747823b5bc5be440186b0c6d",
+ "c12cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_three_leaf_complex",
+ "objective": "Tests P2MR with a complex three-leaf script tree structure, demonstrating nested script paths and multiple verification options",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "201d58016252d5a941f84574c4821b5f87e56778b2bbc3b610dfd801fc0250f6cc7f",
+ "asm": "1d58016252d5a941f84574c4821b5f87e56778b2bbc3b610dfd801fc0250f6cc OP_SUBSTR",
+ "priv_key": "2b339b5055fad695f0595d5581fa087455854f64c5443c03d5b4ca53549f12a41d58016252d5a941f84574c4821b5f87e56778b2bbc3b610dfd801fc0250f6cc",
+ "leafVersion": 192
+ },
+ [
+ {
+ "id": 1,
+ "script": "20a3b0dcac08f86306ba04f5fe2a3243ef3a0347c0e2a4529720a18d3cfdce90ad7f",
+ "asm": "a3b0dcac08f86306ba04f5fe2a3243ef3a0347c0e2a4529720a18d3cfdce90ad OP_SUBSTR",
+ "priv_key": "ec29d60c1be9263602906499b5e3c1de9e36cc88cec31154210191a578b92e9da3b0dcac08f86306ba04f5fe2a3243ef3a0347c0e2a4529720a18d3cfdce90ad",
+ "leafVersion": 192
+ },
+ {
+ "id": 2,
+ "script": "20e6ccf03593dbd07eba10403e33586c130889de6e8219da0a9ccbdd46a56a5f367f",
+ "asm": "e6ccf03593dbd07eba10403e33586c130889de6e8219da0a9ccbdd46a56a5f36 OP_SUBSTR",
+ "priv_key": "da04d13f706a6e0d22ac0db7c361f1aaa706a27043efca4edfecfed3125238e1e6ccf03593dbd07eba10403e33586c130889de6e8219da0a9ccbdd46a56a5f36",
+ "leafVersion": 192
+ }
+ ]
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "0840c39e59eda6c9deee687a480cb169130c2f053ed2eb3134511ec1cfd8a2c7",
+ "837ef6677aeb0df2b0de47f45024684cc6ca03bda10fa30bb5bc05a94beb8dd1",
+ "b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a"
+ ],
+ "merkleRoot": "eaf8f557fdb9673de7bb9bad7e7452da9f44a3e65133fdadf2849c55cfb3cf5b"
+ },
+ "expected": {
+ "scriptPubKey": "5220eaf8f557fdb9673de7bb9bad7e7452da9f44a3e65133fdadf2849c55cfb3cf5b",
+ "bip350Address": "bc1zatu024lah9nnmeamnwkhuazjm205fglx2yelmt0jsjw9tnaneadszq7wg7",
+ "scriptPathControlBlocks": [
+ "c1837ef6677aeb0df2b0de47f45024684cc6ca03bda10fa30bb5bc05a94beb8dd1b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a",
+ "c10840c39e59eda6c9deee687a480cb169130c2f053ed2eb3134511ec1cfd8a2c7b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a",
+ "c118781f42f664d67acaf0ce7c6826437e5440eb1789f232af05e9a09fdf547903"
+ ]
+ }
+ },
+ {
+ "id": "p2mr_three_leaf_alternative",
+ "objective": "Tests another variant of P2MR with three leaves arranged in a different tree structure, showing alternative script path spending options",
+ "given": {
+ "scriptTree": [
+ {
+ "id": 0,
+ "script": "20409f78ce0978519ff5d960c4ab595174c7efca11b1f89410a4e98b70cd423e1c7f",
+ "asm": "409f78ce0978519ff5d960c4ab595174c7efca11b1f89410a4e98b70cd423e1c OP_SUBSTR",
+ "priv_key": "c6ec3801b04afa40be6f77f3f7a7b3b7cb8cfe233b0263fb70e2f087b21397c1409f78ce0978519ff5d960c4ab595174c7efca11b1f89410a4e98b70cd423e1c",
+ "leafVersion": 192
+ },
+ [
+ {
+ "id": 1,
+ "script": "209f0955dc884a5c88d1732e017d30e27e8a445889bce5aa42611b7635c5c1073a7f",
+ "asm": "9f0955dc884a5c88d1732e017d30e27e8a445889bce5aa42611b7635c5c1073a OP_SUBSTR",
+ "priv_key": "7524bca170d54231f1246f30fb00f9da2208b1363ba5a7bbaf11fc3b0309e9519f0955dc884a5c88d1732e017d30e27e8a445889bce5aa42611b7635c5c1073a",
+ "leafVersion": 192
+ },
+ {
+ "id": 2,
+ "script": "20c60b10d57c0cdf27e5c751e131ea4301b37abe7384948059256c7f5f1f285a7f7f",
+ "asm": "c60b10d57c0cdf27e5c751e131ea4301b37abe7384948059256c7f5f1f285a7f OP_SUBSTR",
+ "priv_key": "d9d0f17d630b6a538e6a8a036373e7b9e5023a4c08f72dd8c3ef59288b98c079c60b10d57c0cdf27e5c751e131ea4301b37abe7384948059256c7f5f1f285a7f",
+ "leafVersion": 192
+ }
+ ]
+ ]
+ },
+ "intermediary": {
+ "leafHashes": [
+ "52e9326c2bf04d926b7e9f6c7645dd853f3f007b870201de9b814952750c9310",
+ "dcef3ce86cc8cea78c9e00f3d9ef58360cb6ed3cb90ec62efe00b9703854ba5c",
+ "ddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361"
+ ],
+ "merkleRoot": "51e3c1151ba73d9efce801837773331bf9030977242f62dfeb6756795f482409"
+ },
+ "expected": {
+ "scriptPubKey": "522051e3c1151ba73d9efce801837773331bf9030977242f62dfeb6756795f482409",
+ "bip350Address": "bc1z283uz9gm5u7eal8gqxphwuenr0usxzthyshk9hltvat8jh6gysys28twnc",
+ "scriptPathControlBlocks": [
+ "c1dcef3ce86cc8cea78c9e00f3d9ef58360cb6ed3cb90ec62efe00b9703854ba5cddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361",
+ "c152e9326c2bf04d926b7e9f6c7645dd853f3f007b870201de9b814952750c9310ddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361",
+ "c1b45680a7821e4b9450096ab38adbc3c99225af8f6c7ec121a0a5f1ae02893ba3"
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/wasm-utxo/test/p2mr.ts b/packages/wasm-utxo/test/p2mr.ts
new file mode 100644
index 00000000000..3fca64b2874
--- /dev/null
+++ b/packages/wasm-utxo/test/p2mr.ts
@@ -0,0 +1,58 @@
+import * as assert from "assert";
+import * as fs from "node:fs";
+import * as path from "node:path";
+import { fileURLToPath } from "node:url";
+import { dirname } from "node:path";
+import { address } from "../js/index.js";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+interface TestVector {
+ id: string;
+ expected: {
+ scriptPubKey?: string;
+ bip350Address?: string;
+ };
+}
+
+interface FixtureFile {
+ version: number;
+ test_vectors: TestVector[];
+}
+
+function fromHex(hex: string): Uint8Array {
+ return new Uint8Array(Buffer.from(hex, "hex"));
+}
+
+function loadFixture(name: string): FixtureFile {
+ const filePath = path.join(__dirname, "fixtures", "p2mr", `${name}.json`);
+ return JSON.parse(fs.readFileSync(filePath, "utf8")) as FixtureFile;
+}
+
+describe("P2MR (BIP-360) address encoding", () => {
+ const fixture = loadFixture("p2mr_construction");
+
+ for (const vector of fixture.test_vectors) {
+ if (!vector.expected.bip350Address || !vector.expected.scriptPubKey) continue;
+
+ it(`should encode mainnet P2MR address for ${vector.id}`, () => {
+ const scriptPubKey = fromHex(vector.expected.scriptPubKey);
+
+ const addr = address.fromOutputScriptWithCoin(scriptPubKey, "btc");
+ assert.strictEqual(addr, vector.expected.bip350Address);
+
+ const decoded = address.toOutputScriptWithCoin(addr, "btc");
+ assert.deepStrictEqual(Buffer.from(decoded), Buffer.from(scriptPubKey));
+ });
+
+ it(`should encode testnet P2MR address for ${vector.id}`, () => {
+ const scriptPubKey = fromHex(vector.expected.scriptPubKey);
+
+ const addr = address.fromOutputScriptWithCoin(scriptPubKey, "tbtc");
+ assert.ok(addr.startsWith("tb1z"), `Expected tb1z prefix, got ${addr}`);
+ const decoded = address.toOutputScriptWithCoin(addr, "tbtc");
+ assert.deepStrictEqual(Buffer.from(decoded), Buffer.from(scriptPubKey));
+ });
+ }
+});