1
0
mirror of https://github.com/bitcoin/bips.git synced 2026-03-09 15:53:54 +00:00

BIP360: Pay to Merkle Root (P2MR) (#1670)

Review comments and assistance by:
  Armin Sabouri <armins88@gmail.com>
  D++ <82842780+dplusplus1024@users.noreply.github.com>
  Jameson Lopp <jameson.lopp@gmail.com>
  jbride <jbride2001@yahoo.com>
  Joey Yandle <xoloki@gmail.com>
  Jon Atack <jon@atack.com>
  Jonas Nick <jonasd.nick@gmail.com>
  Kyle Crews <kylecrews@Kyles-Mac-Studio.local>
  Mark "Murch" Erhardt <murch@murch.one>
  notmike-5 <notmike-5@users.noreply.github.com>
  Vojtěch Strnad <43024885+vostrnad@users.noreply.github.com>

Co-authored-by: Ethan Heilman <ethan.r.heilman@gmail.com>
Co-authored-by: Isabel Foxen Duke <110147802+Isabelfoxenduke@users.noreply.github.com>
This commit is contained in:
Hunter Beast
2026-02-11 12:54:26 -08:00
committed by Murch
parent ed7af6ae7e
commit eae7d9fc57
60 changed files with 9494 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
== bitcoin core
=== Two Different Size Limits:
* *MAX_SCRIPT_ELEMENT_SIZE* (in interpreter.cpp line 1882) - This is a consensus rule that limits individual stack elements to 520 bytes. This is what's currently blocking your SLH-DSA signature.
* *MAX_STANDARD_P2MR_STACK_ITEM_SIZE* (in policy.h) - This is a policy rule that limits P2MR stack items to 80 bytes (or 8000 bytes with your change) for standardness.
== P2MR changes to rust-bitcoin
# 1. p2mr module
The p2mr branch of rust-bitcoin includes a new module: `p2mr`.
Source code for this new module can be found [here](https://github.com/jbride/rust-bitcoin/blob/p2mr/bitcoin/src/p2mr/mod.rs).
Highlights of this _p2mr_ module as follows:
## 1.1. p2mrBuilder
This is struct inherits from the rust-bitcoin _TaprootBuilder_.
It has an important modification in that it disables keypath spend.
Similar to its Taproot parent, p2mrBuilder provides functionality to add leaves to a TapTree.
One its TapTree has been fully populated with all leaves, an instance of _p2mrSpendInfo_ can be retrieved from p2mrBuilder.
```
pub struct p2mrBuilder {
inner: TaprootBuilder
}
impl p2mrBuilder {
/// Creates a new p2mr builder.
pub fn new() -> Self {
Self {
inner: TaprootBuilder::new()
}
}
/// Adds a leaf to the p2mr builder.
pub fn add_leaf_with_ver(
self,
depth: u8,
script: ScriptBuf,
leaf_version: LeafVersion,
) -> Result<Self, p2mrError> {
match self.inner.add_leaf_with_ver(depth, script, leaf_version) {
Ok(builder) => Ok(Self { inner: builder }),
Err(_) => Err(p2mrError::LeafAdditionError)
}
}
/// Finalizes the p2mr builder.
pub fn finalize(self) -> Result<p2mrSpendInfo, p2mrError> {
let node_info: NodeInfo = self.inner.try_into_node_info().unwrap();
Ok(p2mrSpendInfo {
merkle_root: Some(node_info.node_hash()),
//script_map: self.inner.script_map().clone(),
})
}
/// Converts the p2mr builder into a Taproot builder.
pub fn into_inner(self) -> TaprootBuilder {
self.inner
}
}
```
## 1.2. p2mrSpendInfo
Provides merkle_root of a completed p2mr TapTree
```
/// A struct for p2mr spend information.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct p2mrSpendInfo {
/// The merkle root of the script path.
pub merkle_root: Option<TapNodeHash>
}
```
## 1.3. p2mrScriptBuf
Allows for creation of a p2mr scriptPubKey UTXO using only the merkle root of a script tree only.
```
/// A wrapper around ScriptBuf for p2mr (Pay to Quantum Resistant Hash) scripts.
pub struct p2mrScriptBuf {
inner: ScriptBuf
}
impl p2mrScriptBuf {
/// Creates a new p2mr script from a ScriptBuf.
pub fn new(inner: ScriptBuf) -> Self {
Self { inner }
}
/// Generates p2mr scriptPubKey output
/// Only accepts the merkle_root (of type TapNodeHash)
/// since keypath spend is disabled in p2mr
pub fn new_p2mr(merkle_root: TapNodeHash) -> Self {
// https://github.com/cryptoquick/bips/blob/p2mr/bip-0360.mediawiki#scriptpubkey
let merkle_root_hash_bytes: [u8; 32] = merkle_root.to_byte_array();
let script = Builder::new()
.push_opcode(OP_PUSHNUM_3)
// automatically pre-fixes with OP_PUSHBYTES_32 (as per size of hash)
.push_slice(&merkle_root_hash_bytes)
.into_script();
p2mrScriptBuf::new(script)
}
/// Returns the script as a reference.
pub fn as_script(&self) -> &Script {
self.inner.as_script()
}
}
```
## 1.4. p2mr Control Block
Closely related to P2TR control block.
Difference being that _internal public key_ is not included.
```
/// A control block for p2mr (Pay to Quantum Resistant Hash) script path spending.
/// This is a simplified version of Taproot's control block that excludes key-related fields.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct p2mrControlBlock {
/// The version of the leaf.
pub leaf_version: LeafVersion,
/// The merkle branch of the leaf.
pub merkle_branch: TaprootMerkleBranch,
}
```
# 2. Witness Program
New p2mr related functions that allow for creation of a new V3 _witness program_ given a merkle_root only.
Found in bitcoin/src/blockdata/script/witness_program.rs
```
/// Creates a [`WitnessProgram`] from a 32 byte merkle root.
fn new_p2mr(program: [u8; 32]) -> Self {
WitnessProgram { version: WitnessVersion::V3, program: ArrayVec::from_slice(&program) }
}
/// Creates a pay to quantum resistant hash address from a merkle root.
pub fn p2mr(merkle_root: Option<TapNodeHash>) -> Self {
let merkle_root = merkle_root.unwrap();
WitnessProgram::new_p2mr(merkle_root.to_byte_array())
}
```
# 3. Address
New _p2mr_ function that allows for creation of a new _p2mr_ Address given a merkle_root only.
Found in bitcoin/src/address/mod.rs
```
/// Creates a pay to quantum resistant hash address from a merkle root.
pub fn p2mr(merkle_root: Option<TapNodeHash>, hrp: impl Into<KnownHrp>) -> Address {
let program = WitnessProgram::p2mr(merkle_root);
Address::from_witness_program(program, hrp)
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1,527 @@
:scrollbar:
:data-uri:
:toc2:
:linkattrs:
= P2MR End-to-End Tutorial
:numbered:
This tutorial is inspired by the link:https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature[script-path-spend-signature] example of the _learnmeabitcoin_ tutorial.
It is customized to create, fund and spend from a P2MR UTXO to a P2WPKH address.
In addition, this tutorial allows for the (un)locking mechanism of the script to optionally use _Post Quantum Cryptography_ (PQC).
The purpose of this tutorial is to demonstrate construction and spending of a link:https://github.com/cryptoquick/bips/blob/p2qrh/bip-0360.mediawiki[bip-360] `p2mr` UTXO (optionally using _Post-Quantum Cryptography_).
The steps outlined in this tutorial are executed using a custom Bitcoin Core instance running either in `regtest` or `signet`.
== Pre-reqs
=== Bitcoin Core
If participating in a workshop, your instructor will provide a bitcoin environment.
Related: your instructor should also provide you with a wallet.
Otherwise, if running this tutorial on your own, follow the instructions in the appendix of this doc: <<build_p2mr>>.
=== Shell Environment
. *docker / podman*
+
NOTE: If you have built the custom `p2mr` enabled Bitcoin Core, you do not need docker (nor podman) installed. Skip this section.
+
This tutorial makes use of a `p2mr` enabled _bitcoin-cli_ utility.
This utility is made available as a docker (or podman) container.
Ensure your host machine has either docker or podman installed.
. *bitcoin-cli* command line utility:
+
NOTE: If you have built the custom `p2mr` enabled Bitcoin Core, you can simply use the `bitcoin-cli` utility found in the `build/bin/` directory. No need to use the _dockerized_ utility described below.
.. You will need a `bitcoin-cli` binary that is `p2mr` enabled.
For this purpose, a docker container with this `bitcoin-cli` utility is provided:
+
-----
docker pull quay.io/jbride2000/bitcoin-cli:p2mr-pqc-0.0.1
-----
.. Configure an alias to the `bitcoin-cli` command that connects to your customized bitcoin-core node.
+
-----
alias b-cli='docker run --rm --network host bitcoin-cli:p2mr-pqc-0.0.1 -rpcconnect=192.168.122.1 -rpcport=18443 -rpcuser=regtest -rpcpassword=regtest'
-----
. *jq*: ensure json parsing utility is link:https://jqlang.org/download/[installed] and available via your $PATH.
. *awk* : standard utility for all Linux distros (often packaged as `gawk`).
. *Rust* development environment with _cargo_ utility. Use link:https://rustup[Rustup] to install.
== Create & Fund P2MR UTXO
The purpose of this workshop is to demonstrate construction and spending of a link:https://github.com/cryptoquick/bips/blob/p2qrh/bip-0360.mediawiki[bip-360] _P2MR_ address (optionally using _Post-Quantum Cryptography_).
In this section of the workshop, you create and fund a P2MR address.
The following depicts the construction of a P2MR _TapTree_ and computation its _scriptPubKey_.
image::images/p2mr_construction.png[]
A P2MR address is created by adding locking scripts to leaves of a _TapTree_.
The locking scripts can use either _Schnorr_ (as per BIP-360) or _SLH-DSA_ (defined in a future BIP) cryptography.
. Set an environment variable specific to your Bitcoin network environment (regtest, signet, etc)
+
[source,bash]
-----
export BITCOIN_NETWORK=regtest
-----
+
Doing so influences the P2MR address that you'll create later in this tutorial.
. Define number of total leaves in tap tree :
+
[source,bash]
-----
export TOTAL_LEAF_COUNT=5
-----
. OPTIONAL: Indicate what type of cryptography to use in the locking scripts of your TapTree leaves.
Valid options are: `MIXED`, `SCHNORR_ONLY`, `SLH_DSA_ONLY`, and `CONCATENATED_SCHNORR_AND_SLH_DSA`.
Default is `MIXED`.
+
[source,bash]
-----
export TAP_TREE_LOCK_TYPE=MIXED
-----
.. If you set _TAP_TREE_LOCK_TYPE=SCHNORR_ONLY_, then the locking script of your TapTree leaves will utilize _Schnorr_ cryptography.
+
Schnorr is not quantum-resistant. However, its signature size is relatively small: 64 bytes.
A _SCHNORR_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_schnorr_only.png[s,300]
.. If you set _TAP_TREE_LOCK_TYPE=SLH_DSA_ONLY_, then the locking script of your TapTree leaves will utilize _SLH-DSA_ cryptography.
A _SLH_DSA_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_slh_dsa_only.png[l,300]
+
SLH_DSA is quantum-resistant. However, the trade-off is the much larger signature size 7,856 bytes when spending.
+
image::images/crypto_key_characteristics.png[]
+
NOTE: PQC cryptography is made available to this BIP-360 reference implementation via the link:https://crates.io/crates/bitcoinpqc[libbitcoinpqc Rust bindings].
.. If you set _MIXED_, then each leaf of the taptree will consist of *either* a Schnorr based locking script or a SLH-DSA based locking script.
A _MIXED_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_mixed.png[t,300]
+
NOTE: The benefit of constructing a taptree with a mixed set of cryptography used as locking scripts in the leaves is articulated nicely in link:https://www.bitmex.com/blog/Taproot%20Quantum%20Spend%20Paths[this article from BitMex].
.. If you set _TAP_TREE_LOCK_TYPE=CONCATENATED_SCHNORR_AND_SLH_DSA_, then the locking script of your TapTree leaves will be secured using both SCHNORR and SLH-DSA cryptography in a concatenated / serial manner.
Private keys for both SCHNORR and SLH-DSA will be needed when unlocking.
A _CONCATENATED_SCHNORR_AND_SLH_DSA_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_concatenated.png[p,300]
. Set the tap leaf index to later use as the unlocking script (when spending) For example, to later spend from the 5th leaf of the tap tree:
+
[source,bash]
-----
export LEAF_TO_SPEND_FROM=4
-----
. Generate a P2MR scripPubKey with multi-leaf taptree:
+
[source,bash]
-----
export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \
&& echo $BITCOIN_ADDRESS_INFO | jq -r .
-----
+
NOTE: In `regtest`, you can expect a P2MR address that starts with: `bcrt1z` .
+
[subs=+quotes]
++++
<details>
<summary><b>What just happened?</b></summary>
The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction with a `p2mr` UTXO as follows:
<ul>
<li>A configurable number of leaves are generated each with their own locking script.</li>
<li>Each of these leaves are added to a Huffman tree that sorts the leaves by weight.</li>
<li>The merkle root of the tree is calculated and subsequently used to generate the p2mr witness program and BIP0350 address.</li>
</ul>
</p>
The source code for the above logic is found in this project: src/lib.rs
</details>
++++
. Only if you previously set `TAP_TREE_LOCK_TYPE=MIXED`, set the environment variable `SPENDING_LEAF_TYPE`.
Valid values are `SHNORR_ONLY` or `SLH_DSA_ONLY` based on the type of locking script that was used in the leaf you will spend from later in this lab. The logs from the previous step tell you the appropriate value to set. For instance:
+
-----
NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows:
export SPENDING_LEAF_TYPE=SCHNORR_ONLY
-----
. Set some env vars (for use in later steps in this tutorial) based on previous result:
+
[source,bash]
-----
export MERKLE_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \
&& export LEAF_SCRIPT_PRIV_KEYS_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_keys_hex' ) \
&& export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \
&& export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \
&& export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \
&& export P2MR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' )
-----
. View tapscript used in target leaf of taptree:
+
[source,bash]
-----
b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm'
-----
+
NOTE: If not using PQC, notice that this script commits to a Schnorr 32-byte x-only public key.
If using PQC, this script commits to a Schnorr 32-byte SLH-DSA pub key and a OP_SUCCESS127 (represented as `OP_SUBSTR`) opcode.
. Fund this P2MR address with the coinbase reward of a newly generated block:
+
Choose from one of the following networks:
.. Regtest
+
If on `regtest` network, execute the following:
+
[source,bash]
-----
export COINBASE_REWARD_TX_ID=$( b-cli -named generatetoaddress nblocks=1 address="$P2MR_ADDR" maxtries=5 | jq -r '.[]' ) \
&& echo $COINBASE_REWARD_TX_ID
-----
+
NOTE: Sometimes Bitcoin Core may not hit a block (even on regtest). If so, just try the above command again.
.. Signet
+
If on `signet` network, then execute the following:
+
[source,bash]
-----
$BITCOIN_SOURCE_DIR/contrib/signet/miner --cli "bitcoin-cli -conf=$HOME/anduro-360/configs/bitcoin.conf.signet" generate \
--address $P2MR_ADDR \
--grind-cmd "$BITCOIN_SOURCE_DIR/build/bin/bitcoin-util grind" \
--min-nbits --set-block-time $(date +%s) \
--poolid "MARA Pool"
-----
. view summary of all txs that have funded P2MR address
+
[source,bash]
-----
export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \
&& echo $P2MR_DESC \
&& b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]'
-----
. grab txid of first tx with unspent funds:
+
[source,bash]
-----
export FUNDING_TX_ID=$( b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' | jq -r '.unspents[0].txid' ) \
&& echo $FUNDING_TX_ID
-----
. Set FUNDING_UTXO_INDEX env var (used later to correctly identify funding UTXO when generating the spending tx)
+
[source,bash]
-----
export FUNDING_UTXO_INDEX=0
-----
. view details of funding UTXO to the P2MR address:
+
[source,bash]
-----
export FUNDING_UTXO=$( b-cli getrawtransaction $FUNDING_TX_ID 1 | jq -r '.vout['''$FUNDING_UTXO_INDEX''']' ) \
&& echo $FUNDING_UTXO | jq -r .
-----
+
NOTE: the above only works when Bitcoin Core is started with the following arg: -txindex
== Spend P2MR UTXO
In the previous section, you created and funded a P2MR UTXO.
That UTXO includes a leaf script locked with a key-pair (optionally based on PQC) known to you.
In this section, you spend from that P2MR UTXO.
Specifically, you will generate an appropriate _SigHash_ and sign it (to create a signature) using the known private key that unlocks the known leaf script of the P2MR UTXO.
For the purpose of this tutorial, you will spend funds to a new P2WPKH utxo. (there is nothing novel about this P2WPKH utxo).
. Determine value (in sats) of the funding P2MR utxo:
+
[source,bash]
-----
export FUNDING_UTXO_AMOUNT_SATS=$(echo $FUNDING_UTXO | jq -r '.value' | awk '{printf "%.0f", $1 * 100000000}') \
&& echo $FUNDING_UTXO_AMOUNT_SATS
-----
. Generate additional blocks.
+
This is necessary if you have only previously generated less than 100 blocks.
+
Otherwise, you may see an error from bitcoin core such as the following when attempting to spend:
+
_bad-txns-premature-spend-of-coinbase, tried to spend coinbase at depth 1_
.. regtest
+
[source,bash]
-----
b-cli -generate 110
-----
.. signet
+
This will involve having the signet miner generate about 110 blocks .... which can take about 10 minutes.
+
The `common/utils` directory of this project provides a script called: link:../../common/utils/signet_miner_loop.sh[signet_miner_loop.sh].
. Referencing the funding tx (via $FUNDING_TX_ID and $FUNDING_UTXO_INDEX), create the spending tx:
+
[source,bash]
-----
export SPEND_DETAILS=$( cargo run --example p2mr_spend )
-----
+
[subs=+quotes]
++++
<details>
<summary><b>What just happened?</b></summary>
The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction that spends from the `p2mr` UTXO as follows:
<ul>
<li>Create a transaction template (aka: SigHash) that serves as the message to be signed.</li>
<li>Using the known private key and the SigHash, create a signature that is capable of unlocking one of the leaf scripts of the P2MR tree.</li>
<li>Add this signature to the witness section of the transaction.</li>
</ul>
</p>
The source code for the above logic is found in this project: src/lib.rs
</details>
++++
. Set environment variables passed to _bitcoin-cli_ when spending:
+
[source,bash]
-----
export RAW_P2MR_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \
&& echo "RAW_P2MR_SPEND_TX = $RAW_P2MR_SPEND_TX" \
&& export SIG_HASH=$( echo $SPEND_DETAILS | jq -r '.sighash' ) \
&& echo "SIG_HASH = $SIG_HASH" \
&& export SIG_BYTES=$( echo $SPEND_DETAILS | jq -r '.sig_bytes' ) \
&& echo "SIG_BYTES = $SIG_BYTES"
-----
. Inspect the spending tx:
+
[source,bash]
-----
b-cli decoderawtransaction $RAW_P2MR_SPEND_TX
-----
+
Pay particular attention to the `vin.txinwitness` field.
Do the three elements (script input, script and control block) of the witness stack for this script path spend make sense ?
What do you observe as the first byte of the `control block` element ?
. Test standardness of the spending tx by sending to local mempool of p2mr enabled Bitcoin Core:
+
[source,bash]
-----
b-cli testmempoolaccept '["'''$RAW_P2MR_SPEND_TX'''"]'
-----
. Submit tx:
+
[source,bash]
-----
export P2MR_SPENDING_TX_ID=$( b-cli sendrawtransaction $RAW_P2MR_SPEND_TX ) \
&& echo $P2MR_SPENDING_TX_ID
-----
+
NOTE: Should return same tx id as was included in $RAW_P2MR_SPEND_TX
== Mine P2MR Spend TX
. View tx in mempool:
+
[source,bash]
-----
b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1
-----
+
NOTE: There will not yet be a field `blockhash` in the response.
. Mine 1 block:
.. regtest:
+
[source,bash]
-----
b-cli -generate 1
-----
.. signet:
+
If on `signet` network, then execute the following:
+
[source,bash]
-----
$BITCOIN_SOURCE_DIR/contrib/signet/miner --cli "bitcoin-cli -conf=$HOME/anduro-360/configs/bitcoin.conf.signet" generate \
--address $P2MR_ADDR \
--grind-cmd "$BITCOIN_SOURCE_DIR/build/bin/bitcoin-util grind" \
--min-nbits --set-block-time $(date +%s) \
--poolid "MARA Pool"
-----
. Obtain `blockhash` field of mined tx:
+
[source,bash]
-----
export BLOCK_HASH=$( b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1 | jq -r '.blockhash' ) \
&& echo $BLOCK_HASH
-----
. View tx in block:
+
[source,bash]
-----
b-cli getblock $BLOCK_HASH | jq -r .tx
-----
== Appendix
[[build_p2mr]]
=== Build P2MR / PQC Enabled Bitcoin Core
The link:https://github.com/jbride/bitcoin/tree/p2mr[p2mr branch] of bitcoin core is needed.
Build instructions for the `p2mr` branch are the same as `master` and is documented link:https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md[here].
As such, the following is an example series of steps (on a Fedora 42 host) to compile and run the `p2mr` branch of bitcoin core:
. Set BITCOIN_SOURCE_DIR
+
-----
export BITCOIN_SOURCE_DIR=/path/to/root/dir/of/cloned/bitcoin/source
-----
. build
+
-----
cmake -B build \
-DWITH_ZMQ=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBUILD_BENCH=ON \
-DBUILD_DAEMON=ON \
-DSANITIZERS=address,undefined
cmake --build build -j$(nproc)
-----
. run in either `regtest` or `signet` mode:
.. regtest:
+
-----
./build/bin/bitcoind -daemon=0 -regtest=1 -txindex -prune=0
-----
.. signet:
+
-----
./build/bin/bitcoind -daemon=0 -signet=1 -txindex -prune=0
-----
+
NOTE: If running in `signet`, your bitcoin core will need to be configured with the `signetchallenge` property.
link:https://edil.com.br/blog/creating-a-custom-bitcoin-signet[This tutorial] provides a nice overview of the topic.
=== libbitcoinpqc build
The `p2mr-pqc` branch of this project includes a dependency on the link:https://crates.io/crates/libbitcoinpqc[libbitcoinpqc crate].
libbitcoinpqc contains native code (C/C++/ASM) and is made available to Rust projects via Rust bindings.
This C/C++/ASM code is provided in the libbitcoinpqc crate as source code (not prebuilt binaries).
Subsequently, the `Cargo` utility needs to build this libbitcoinpqc C native code on your local machine.
You will need to have C development related libraries installed on your local machine.
Every developer or CI machine building `p2mr-ref` must have cmake and a C toolchain installed locally.
==== Linux
. Debian / Ubuntu
+
-----
sudo apt update
sudo apt install cmake build-essential clang libclang-dev
-----
. Fedora / RHEL
+
-----
sudo dnf5 update
sudo dnf5 install cmake make gcc gcc-c++ clang clang-libs llvm-devel
-----
==== OSX
[[bitcoin_core_wallet]]
=== Bitcoin Core Wallet
This tutorial assumes that a bitcoin core wallet is available.
. For example, the following would be sufficient:
+
-----
export W_NAME=anduro
b-cli -named createwallet \
wallet_name=$W_NAME \
descriptors=true \
load_on_startup=true
-----
=== Schnorr + SLH-DSA
-----
<SCHNORR Pub Key> OP_CHECKSIG <SLH-DSA pub key> OP_SUBSTR OP_BOOLAND OP_VERIFY
-----
The logic flow is:
. <SCHNORR Pub Key> OP_CHECKSIG: Verify Schnorr signature against Schnorr pubkey
. <SLH-DSA pub key> OP_SUBSTR: Verify SLH-DSA signature against SLH-DSA pubkey (using OP_SUBSTR for the SLH-DSA verification)
. OP_BOOLAND: Ensure both signature verifications succeeded
. OP_VERIFY: Final verification that the script execution succeeded
. This creates a "both signatures required" locking condition, which is exactly what you want for SCHNORR_AND_SLH_DSA scripts.
===== Sighash bytes
Sighash bytes are appended to each signature, instead of being separate witness elements:
. SlhDsaOnly: SLH-DSA signature + sighash byte appended
. SchnorrOnly: Schnorr signature + sighash byte appended
. SchnorrAndSlhDsa: Schnorr signature (no sighash) + SLH-DSA signature + sighash byte appended to the last signature

View File

@@ -0,0 +1,524 @@
:scrollbar:
:data-uri:
:toc2:
:linkattrs:
= P2MR End-to-End workshop
:numbered:
Welcome to the BIP-360 / _Pay-To-Tap-Script-Hash_ (P2MR) workshop !
In this workshop, you will interact with a custom Signet environment to create, fund and spend from a _P2MR_ address.
_P2MR_ is a new Bitcoin address type defined in link:https://bip360.org/bip360.html[bip-360].
In addition, this workshop allows for the (un)locking mechanism of the leaf scripts of your P2MR address to optionally use _Post Quantum Cryptography_ (PQC).
The use of PQC is alluded to in BIP-360 and will be further defined in future BIPs.
The steps outlined in this workshop are executed using a P2MR/PQC enabled Bitcoin Core instance running on a signet environment.
*The target audience of the workshop is Bitcoin developers and ops personnel.
As such, the workshop makes heavy use of the _bitcoin-cli_ at the command line.*
== Pre-reqs
=== *docker / podman*
This workshop environment is provided as a _docker_ container.
Subsequently, ensure your host machine has either link:https://docs.docker.com/desktop/[docker] or link:https://podman.io/docs/installation[podman] installed.
=== *p2mr_demo* docker container
==== Obtain container
Once docker or podman is installed on your machine, you can obtain the workshop docker container via any of the following:
. Pull from _quay.io_ :
+
[source,bash]
-----
sudo docker pull quay.io/jbride2000/p2mr_demo:0.1
-----
+
NOTE: The container image is 1.76GB in size. This approach may be slow depending on network bandwidth.
. Download container image archive file from local HTTP server:
+
*TO_DO*
. Obtain container image archive file from instructor:
.. Workshop instructors have the container image available via USB thumb drives.
If you feel comfortable with this approach, ask an instructor for a thumb drive.
... Mount the USB thumb drive and copy for the file called: _p2mr_demo-image.tar_.
... Load the container image into your docker environment as per the following:
+
[source,bash]
-----
docker load -i /path/to/p2mr_demo-image.tar
-----
==== Start container
You will need to start the workshop container using the docker infrastructure on your machine.
. If working at the command line, the following is an example to run the container and obtain a command prompt:
+
[source,bash]
-----
sudo docker run -it --rm --entrypoint /bin/bash --network host \
-e RPC_CONNECT=10.21.3.194 \
quay.io/jbride2000/p2mr_demo:0.1
-----
. You should see a _bash_ shell command prompt similar to the following:
+
-----
bip360@0aa9edf3d201:~/bips/bip-0360/ref-impl/rust$ ls
Cargo.lock Cargo.toml README.md docs examples src tests
-----
+
As per the `ls` command seen above, your command prompt path defaults to the link:https://github.com/jbride/bips/tree/p2mr/bip-0360/ref-impl/rust[Rust based reference implementation] for BIP-360.
==== Container contents
Your docker environment already includes a P2MR/PQC enabled `bitcoin-cli` utility.
In addition, an alias to this custom bitcoin-cli utility configured for the signet workshop environment has also been provided.
. You can view this alias as follows (execute all commands within workshop container image):
+
[source,bash]
-----
declare -f b-cli
-----
+
You should see a response similar to the following:
+
-----
b-cli ()
{
/usr/local/bin/bitcoin-cli -rpcconnect=${RPC_CONNECT:-192.168.122.1} -rpcport=${RPC_PORT:-18443} -rpcuser=${RPC_USER:-signet} -rpcpassword=${RPC_PASSWORD:-signet} "$@"
}
-----
. Test interaction between your _b-cli_ utility and the workshop's signet node via the following:
+
[source,bash]
-----
b-cli getnetworkinfo
-----
+
[source,bash]
-----
b-cli getblockcount
-----
. In addition, your docker environment also comes pre-installed with the following utilities needed for this workshop:
. *jq*: json parsing utility
. *awk*
. *Rust* development environment with _cargo_ utility
== Bitcoin Environment
Your workshop instructors have provided you with a _P2MR/PQC_ enabled Bitcoin environment running in _signet_.
You will send RPC commands to this custom Bitcoin node via the _b-cli_of your docker container.
image::images/workshop_deployment_arch.png[]
Via your browser, you will interact with the P2MR enabled _mempool.space_ for the workshop at: link:http://signet.bip360.org[signet.bip360.org].
== Create & Fund P2MR Address
The purpose of this workshop is to demonstrate construction and spending of a link:https://github.com/cryptoquick/bips/blob/p2qrh/bip-0360.mediawiki[bip-360] _P2MR_ address (optionally using _Post-Quantum Cryptography_).
In this section of the workshop, you create and fund a P2MR address.
The following depicts the construction of a P2MR _TapTree_ and computation its _scriptPubKey_.
image::images/p2mr_construction.png[]
A P2MR address is created by adding locking scripts to leaves of a _TapTree_.
The locking scripts can use either _Schnorr_ (as per BIP-360) or _SLH-DSA_ (defined in a future BIP) cryptography.
. Define number of total leaves in tap tree :
+
[source,bash]
-----
export TOTAL_LEAF_COUNT=5
-----
. In your container image, indicate what type of cryptography to use in the locking scripts of your TapTree leaves.
Valid options are: `MIXED`, `SLH_DSA_ONLY`, `SCHNORR_ONLY`, `CONCATENATED_SCHNORR_AND_SLH_DSA`.
Default is `MIXED`.
+
[source,bash]
-----
export TAP_TREE_LOCK_TYPE=MIXED
-----
.. If you set _TAP_TREE_LOCK_TYPE=SCHNORR_ONLY_, then the locking script of your TapTree leaves will utilize _Schnorr_ cryptography.
+
Schnorr is not quantum-resistant. However, its signature size is relatively small: 64 bytes.
A _SCHNORR_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_schnorr_only.png[s,300]
.. If you set _TAP_TREE_LOCK_TYPE=SLH_DSA_ONLY_, then the locking script of your TapTree leaves will utilize _SLH-DSA_ cryptography.
A _SLH_DSA_ONLY_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_slh_dsa_only.png[l,300]
+
SLH_DSA is quantum-resistant. However, the trade-off is the much larger signature size 7,856 bytes when spending.
+
image::images/crypto_key_characteristics.png[]
+
NOTE: PQC cryptography is made available to this BIP-360 reference implementation via the link:https://crates.io/crates/bitcoinpqc[libbitcoinpqc Rust bindings].
.. If you set _MIXED_, then each leaf of the taptree will consist of *either* a Schnorr based locking script or a SLH-DSA based locking script.
A _MIXED_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_mixed.png[t,300]
+
NOTE: The benefit of constructing a taptree with a mixed set of cryptography used as locking scripts in the leaves is articulated nicely in link:https://www.bitmex.com/blog/Taproot%20Quantum%20Spend%20Paths[this article from BitMex].
.. If you set _TAP_TREE_LOCK_TYPE=CONCATENATED_SCHNORR_AND_SLH_DSA_, then the locking script of your TapTree leaves will be secured using both SCHNORR and SLH-DSA cryptography in a concatenated / serial manner.
Private keys for both SCHNORR and SLH-DSA will be needed when unlocking.
A _CONCATENATED_SCHNORR_AND_SLH_DSA_ tap tree with 5 leaves (aka: TOTAL_LEAF_COUNT) could be represented as follows:
+
image::images/tap_tree_concatenated.png[p,300]
. Set the tap leaf index to later use as the unlocking script (when spending) For example, to later spend from the 5th leaf of the tap tree:
+
[source,bash]
-----
export LEAF_TO_SPEND_FROM=4
-----
. Generate a P2MR scripPubKey with multi-leaf taptree:
+
[source,bash]
-----
export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \
&& echo $BITCOIN_ADDRESS_INFO | jq -r .
-----
+
NOTE: In signet, you can expect a P2MR address that starts with the following prefix: `tb1z` .
+
[subs=+quotes]
++++
<details>
<summary><b>What just happened?</b></summary>
The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction with a P2MR UTXO as follows:
<ul>
<li>A configurable number of leaves are generated each with their own locking script.</li>
<li>Each of these leaves are added to a Huffman tree that sorts the leaves by weight.</li>
<li>The merkle root of the tree is calculated and subsequently used to generate the P2MR witness program and BIP0350 address.</li>
</ul>
</p>
</details>
++++
+
The source code for the above logic is found in this project's source file: link:../src/lib.rs[src/lib.rs]
. Only if you previously set `TAP_TREE_LOCK_TYPE=MIXED`, set the environment variable `SPENDING_LEAF_TYPE`. Valid values are `SHNORR_ONLY` or `SLH_DSA_ONLY` based on the type of locking script that was used in the leaf you will spend from later in this lab. The logs from the previous step tell you the appropriate value to set. For instance:
+
-----
NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows:
export SPENDING_LEAF_TYPE=SCHNORR_ONLY
-----
. Fund this P2MR address using workshop's signet faucet
.. In a browser tab, navigate to: link:http://faucet.bip360.org/[faucet.bip360.org].
.. Copy-n-paste the value of `bech32m_address` (found in the json response from the previous step)
+
image::images/faucet_1.png[]
+
Press the `Request` button.
.. The faucet should allocate bitcoin to your address.
+
image::images/faucet_2.png[]
.. Click on the link of your transaction id.
This will take you a detailed view of the transaction.
Scroll down to the the _Inputs & Outputs_ section of the transaction and identify the _vout_ index of funds sent to your _P2MR_ address.
+
image::images/funding_utxo_id.png[]
.. Return back to your terminal and set a _FUNDING_UTXO_INDEX_ environment variable (used later to correctly identify funding UTXO when generating the spending tx)
+
[source,bash]
-----
export FUNDING_UTXO_INDEX=<change me>
-----
.. Return back to your browser tab at navigate to: link:https://signet.bip360.org[signet.bip360.org] and wait until a new block mines the transaction from the faucet that funded your P2MR address.
+
image::images/mempool_next_block.png[]
. Set some env vars (for use in later steps in this workshop) based on previous json result:
+
[source,bash]
-----
export MERKLE_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \
&& export LEAF_SCRIPT_PRIV_KEYS_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_keys_hex' ) \
&& export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \
&& export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \
&& export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \
&& export P2MR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' )
-----
. View tapscript used in target leaf of taptree:
+
[source,bash]
-----
b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm'
-----
+
NOTE: If not using Schnorr crypto, this script commits to a Schnorr 32-byte x-only public key.
If using SLH_DSA, this script commits to a 32-byte SLH-DSA pub key and a OP_SUCCESS127 (represented as `OP_SUBSTR`) opcode.,
If using SCHNORR + SLH_DSA, then you should see a locking script in the leaf similar to the following:
+
-----
886fc1edb7a8a364da65aef57343de451c1449d8a6c5b766fe150667d50d3e80 OP_CHECKSIG 479f93fbd251863c3e3e72da6e26ea82f87313da13090de10e57eca1f8b5e0f3 OP_SUBSTR OP_BOOLAND OP_VERIFY
-----
. view summary of all txs that have funded P2MR address
+
[source,bash]
-----
export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \
&& echo $P2MR_DESC \
&& b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]'
-----
+
NOTE: You will likely have to wait a few minutes until a new block (containing the tx that funds your P2MR address) is mined.
. grab txid of first tx with unspent funds:
+
[source,bash]
-----
export FUNDING_TX_ID=$( b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]' | jq -r '.unspents[0].txid' ) \
&& echo $FUNDING_TX_ID
-----
. view details of funding UTXO to the P2MR address:
+
[source,bash]
-----
export FUNDING_UTXO=$( b-cli getrawtransaction $FUNDING_TX_ID 1 | jq -r '.vout['''$FUNDING_UTXO_INDEX''']' ) \
&& echo $FUNDING_UTXO | jq -r .
-----
== Spend P2MR UTXO
In the previous section, you created and funded a P2MR UTXO.
That UTXO includes a leaf script locked with a key-pair (optionally based on PQC) known to you.
In this section, you spend from that P2MR UTXO.
Specifically, you will generate an appropriate _SigHash_ and sign it (to create a signature) using the known private key that unlocks the known leaf script of the P2MR UTXO.
The target address type that you send funds to is: P2WPKH.
. Determine value (in sats) of the funding P2MR utxo:
+
[source,bash]
-----
export FUNDING_UTXO_AMOUNT_SATS=$(echo $FUNDING_UTXO | jq -r '.value' | awk '{printf "%.0f", $1 * 100000000}') \
&& echo $FUNDING_UTXO_AMOUNT_SATS
-----
. Referencing the funding tx (via $FUNDING_TX_ID and $FUNDING_UTXO_INDEX), create the spending tx:
+
[source,bash]
-----
export SPEND_DETAILS=$( cargo run --example p2mr_spend )
-----
+
[subs=+quotes]
++++
<details>
<summary><b>What just happened?</b></summary>
The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction that spends from the P2MR UTXO as follows:
<ul>
<li>Create a transaction template (aka: SigHash) that serves as the message to be signed.</li>
<li>Using the known private key and the SigHash, create a signature that is capable of unlocking one of the leaf scripts of the P2MR tree.</li>
<li>Add this signature to the witness section of the transaction.</li>
</ul>
</p>
</details>
++++
+
The source code for the above logic is found in this project's source file: link:../src/lib.rs[src/lib.rs]
. Set environment variables passed to _bitcoin-cli_ when spending:
+
[source,bash]
-----
export RAW_P2MR_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \
&& echo "RAW_P2MR_SPEND_TX = $RAW_P2MR_SPEND_TX" \
&& export SIG_HASH=$( echo $SPEND_DETAILS | jq -r '.sighash' ) \
&& echo "SIG_HASH = $SIG_HASH" \
&& export SIG_BYTES=$( echo $SPEND_DETAILS | jq -r '.sig_bytes' ) \
&& echo "SIG_BYTES = $SIG_BYTES"
-----
. Inspect the spending tx:
+
[source,bash]
-----
b-cli decoderawtransaction $RAW_P2MR_SPEND_TX
-----
+
Pay particular attention to the `vin.txinwitness` field.
The following depicts the elements of a P2MR witness stack.
+
image::images/p2mr_witness.png[]
+
Do the three elements (script input, script and control block) of the witness stack for this _script path spend_ correspond ?
What do you observe as the first byte of the `control block` element ?
. Test standardness of the spending tx by sending to local mempool of P2MR enabled Bitcoin Core:
+
[source,bash]
-----
b-cli testmempoolaccept '["'''$RAW_P2MR_SPEND_TX'''"]'
-----
. Submit tx:
+
[source,bash]
-----
export P2MR_SPENDING_TX_ID=$( b-cli sendrawtransaction $RAW_P2MR_SPEND_TX ) \
&& echo $P2MR_SPENDING_TX_ID
-----
+
NOTE: Should return same tx id as was included in $RAW_P2MR_SPEND_TX
== Mine P2MR Spend TX
. View tx in mempool:
+
[source,bash]
-----
b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1
-----
+
NOTE: There will not yet be a field `blockhash` in the response.
. Monitor the mempool.space instance at link:http://signet.bip360.org[signet.bip360.org] until a new block is mined.
. While still in the mempool.space instance at link:http://signet.bip360.org[signet.bip360.org], lookup your tx (denoted by: $P2MR_SPENDING_TX_ID ) in the top-right search bar:
+
image::images/mempool_spending_tx_1.png[]
+
Click on the `Details` button at the top-right of the `Inputs & Outputs` section.
.. Study the elements of the `Witness. Approximately how large is each element of the witness stack?
.. View the values of the `Previous output script` and `Previous output type` fields:
+
image::images/mempool_spending_tx_2.png[]
. Obtain `blockhash` field of mined tx:
+
[source,bash]
-----
export BLOCK_HASH=$( b-cli getrawtransaction $P2MR_SPENDING_TX_ID 1 | jq -r '.blockhash' ) \
&& echo $BLOCK_HASH
-----
. View txs in block:
+
[source,bash]
-----
b-cli getblock $BLOCK_HASH | jq -r .tx
-----
+
You should see your tx (as per $P2MR_SPENDING_TX_ID) in the list.
+
Congratulations!! You have created, funded and spent from a P2MR address.
== Appendix
[[build_p2mr]]
=== Build P2MR / PQC Enabled Bitcoin Core
*FOR THE PURPOSE OF THE WORKSHOP, YOU CAN IGNORE THIS SECTION*
The link:https://github.com/jbride/bitcoin/tree/p2mr[p2mr branch] of bitcoin core is needed.
Build instructions for the `p2mr` branch are the same as `master` and is documented link:https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md[here].
As such, the following is an example series of steps (on a Fedora 42 host) to compile and run the `p2mr` branch of bitcoin core:
. Set BITCOIN_SOURCE_DIR
+
-----
export BITCOIN_SOURCE_DIR=/path/to/root/dir/of/cloned/bitcoin/source
-----
. build
+
-----
cmake -B build \
-DWITH_ZMQ=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBUILD_BENCH=ON \
-DBUILD_DAEMON=ON \
-DSANITIZERS=address,undefined
cmake --build build -j$(nproc)
-----
. run bitcoind in signet mode:
+
-----
./build/bin/bitcoind -daemon=0 -signet=1 -txindex -prune=0
-----
+
NOTE: If running in `signet`, your bitcoin core will need to be configured with the `signetchallenge` property.
link:https://edil.com.br/blog/creating-a-custom-bitcoin-signet[This workshop] provides a nice overview of the topic.
=== libbitcoinpqc Rust bindings
*FOR THE PURPOSE OF THE WORKSHOP, YOU CAN IGNORE THIS SECTION*
The `p2mr-pqc` branch of this project includes a dependency on the link:https://crates.io/crates/libbitcoinpqc[libbitcoinpqc crate].
libbitcoinpqc contains native code (C/C++/ASM) and is made available to Rust projects via Rust bindings.
This C/C++/ASM code is provided in the libbitcoinpqc crate as source code (not prebuilt binaries).
Subsequently, the `Cargo` utility needs to build this libbitcoinpqc C native code on your local machine.
You will need to have C development related libraries installed on your local machine.
Every developer or CI machine building `p2mr-ref` must have cmake and a C toolchain installed locally.
==== Linux
. Debian / Ubuntu
+
-----
sudo apt update
sudo apt install cmake build-essential clang libclang-dev
-----
. Fedora / RHEL
+
-----
sudo dnf5 update
sudo dnf5 install cmake make gcc gcc-c++ clang clang-libs llvm-devel
-----

View File

@@ -0,0 +1,236 @@
:scrollbar:
:data-uri:
:toc2:
:linkattrs:
= P2TR End-to-End Tutorial
:numbered:
This tutorial is inspired from the link:https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature[script-path-spend-signature] example of the _learnmeabitcoin_ tutorial.
It is customized to create, fund and spend from a P2TR UTXO to a P2WPKH address.
Execute in Bitcoin Core `regtest` mode.
== Pre-reqs
=== Bitcoin Core
The link:https://github.com/jbride/bitcoin/tree/p2mr-pqc[p2mr branch] of bitcoin core is needed.
Build instructions for the `p2mr` branch are the same as `master` and is documented link:https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md[here].
As such, the following is an example series of steps (on a Fedora 42 host) to compile and run the `p2mr` branch of bitcoin core:
. build
+
-----
$ cmake -B build \
-DWITH_ZMQ=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBUILD_BENCH=ON \
-DSANITIZERS=address,undefined
$ cmake --build build -j$(nproc)
-----
. run in `regtest` mode
+
-----
$ ./build/bin/bitcoind -daemon=0 -regtest=1 -txindex
-----
=== Shell Environment
. *b-reg* command line alias:
+
Configure an alias to the `bitcoin-cli` command that connects to your customized bitcoin-core node running in `regtest` mode.
. *jq*: ensure json parsing utility is installed and available via your $PATH.
. *awk* : standard utility for all Linux distros (often packaged as `gawk`).
=== Bitcoin Core Wallet
This tutorial assumes that a bitcoin core wallet is available.
For example, the following would be sufficient:
-----
$ export W_NAME=regtest \
&& export WPASS=regtest
$ b-reg -named createwallet \
wallet_name=$W_NAME \
descriptors=true \
passphrase="$WPASS" \
load_on_startup=true
-----
== Fund P2TR UTXO
. OPTIONAL: Define number of leaves in tap tree as well as the tap leaf to later use as the unlocking script:
+
-----
$ export TOTAL_LEAF_COUNT=5 \
&& export LEAF_TO_SPEND_FROM=4
-----
+
NOTE: Defaults are 4 leaves with the first leaf (leaf 0 ) as the script to later use as the unlocking script.
. Generate a P2TR scripPubKey with multi-leaf taptree:
+
-----
$ export BITCOIN_NETWORK=regtest \
&& export BITCOIN_ADDRESS_INFO=$( cargo run --example p2tr_construction ) \
&& echo $BITCOIN_ADDRESS_INFO | jq -r .
-----
+
NOTE: In `regtest`, you can expect a P2TR address that starts with: `bcrt1q` .
+
NOTE: In the context of P2TR, the _tree_root_hex_ from the response is in reference to the _merkle_root_ used in this tutorial.
. Set some env vars (for use in later steps in this tutorial) based on previous result:
+
-----
$ export MERKLE_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \
&& export LEAF_SCRIPT_PRIV_KEYS_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_keys_hex' ) \
&& export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \
&& export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \
&& export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \
&& export P2TR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' )
-----
. View tapscript used in target leaf of taptree:
+
-----
$ b-reg decodescript $LEAF_SCRIPT_HEX | jq -r '.asm'
-----
+
NOTE: Notice that this script commits to a Schnorr 32-byte x-only public key.
. fund this P2TR address with the coinbase reward of a newly generated block:
+
-----
$ export COINBASE_REWARD_TX_ID=$( b-reg -named generatetoaddress 1 $P2TR_ADDR 5 | jq -r '.[]' ) \
&& echo $COINBASE_REWARD_TX_ID
-----
+
NOTE: Sometimes Bitcoin Core may not hit a block (even on regtest). If so, just try the above command again.
. view summary of all txs that have funded P2TR address
+
-----
$ export P2TR_DESC=$( b-reg getdescriptorinfo "addr($P2TR_ADDR)" | jq -r '.descriptor' ) \
&& echo $P2TR_DESC \
&& b-reg scantxoutset start '[{"desc": "'''$P2TR_DESC'''"}]'
-----
. grab txid of first tx with unspent funds:
+
-----
$ export FUNDING_TX_ID=$( b-reg scantxoutset start '[{"desc": "'''$P2TR_DESC'''"}]' | jq -r '.unspents[0].txid' ) \
&& echo $FUNDING_TX_ID
-----
. Set FUNDING_UTXO_INDEX env var (used later to correctly identify funding UTXO when generating the spending tx)
+
-----
$ export FUNDING_UTXO_INDEX=0
-----
. view details of funding UTXO to the P2TR address:
+
-----
$ export FUNDING_UTXO=$( b-reg getrawtransaction $FUNDING_TX_ID 1 | jq -r '.vout['''$FUNDING_UTXO_INDEX''']' ) \
&& echo $FUNDING_UTXO | jq -r .
-----
+
NOTE: the above only works when Bitcoin Core is started with the following arg: -txindex
== Spend P2TR UTXO
. Determine value (in sats) of funding utxo:
+
-----
$ export FUNDING_UTXO_AMOUNT_SATS=$(echo $FUNDING_UTXO | jq -r '.value' | awk '{printf "%.0f", $1 * 100000000}') \
&& echo $FUNDING_UTXO_AMOUNT_SATS
-----
. Generate additional blocks.
+
This is necessary if you have only previously generated less than 100 blocks.
+
-----
$ b-reg -generate 110
-----
+
Otherwise, you may see an error from bitcoin core such as the following when attempting to spend:
+
_bad-txns-premature-spend-of-coinbase, tried to spend coinbase at depth 1_
. Referencing the funding tx (via $FUNDING_TX_ID and $FUNDING_UTXO_INDEX), create the spending tx:
+
-----
$ export SPEND_DETAILS=$( cargo run --example p2tr_spend )
$ export RAW_P2TR_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \
&& echo "RAW_P2TR_SPEND_TX = $RAW_P2TR_SPEND_TX" \
&& export SIG_HASH=$( echo $SPEND_DETAILS | jq -r '.sighash' ) \
&& echo "SIG_HASH = $SIG_HASH" \
&& export SIG_BYTES=$( echo $SPEND_DETAILS | jq -r '.sig_bytes' ) \
&& echo "SIG_BYTES = $SIG_BYTES"
-----
. Inspect the spending tx:
+
-----
$ b-reg decoderawtransaction $RAW_P2TR_SPEND_TX
-----
. Test standardness of the spending tx by sending to local mempool of p2tr enabled Bitcoin Core:
-----
$ b-reg testmempoolaccept '["'''$RAW_P2TR_SPEND_TX'''"]'
-----
. Submit tx:
+
-----
$ export P2TR_SPENDING_TX_ID=$( b-reg sendrawtransaction $RAW_P2TR_SPEND_TX ) \
&& echo $P2TR_SPENDING_TX_ID
-----
+
NOTE: Should return same tx id as was included in $RAW_P2TR_SPEND_TX
== Mine P2TR Spend TX
. View tx in mempool:
+
-----
$ b-reg getrawtransaction $P2TR_SPENDING_TX_ID 1
-----
+
NOTE: There will not yet be a field `blockhash` in the response.
. Mine 1 block:
+
-----
$ b-reg -generate 1
-----
. Obtain `blockhash` field of mined tx:
+
-----
$ export BLOCK_HASH=$( b-reg getrawtransaction $P2TR_SPENDING_TX_ID 1 | jq -r '.blockhash' ) \
&& echo $BLOCK_HASH
-----
. View tx in block:
+
-----
$ b-reg getblock $BLOCK_HASH | jq -r .tx
-----
== TO-DO

View File

@@ -0,0 +1,36 @@
┌───────────────────────┐
│ tapleaf Merkle root │
│ │
└───────────────────────┘
|
┌───────────────────────┐
│ 5 tagged_hash │
│ QuantumRoot │
└───────────|───────────┘
┌───────────|───────────┐
┌───────────────────────────────►│ 4 tagged_hash ◄─────────────────────┐
│ │ TapBranch │ │
│ └───────────────────────┘ │
│ │
│ │
│ │
│ │
│ │
│ ┌─────────────┼───────────┐
│ │ 3 tagged_hash │
│ ┌──►│ TapBranch ◄───────┐
│ │ └─────────────────────────┘ │
│ │ │
│ │ │
┌───────────┼────────────┐ ┌───────┼────────┐ │
┌─────►│ 2 tagged_hash ◄────┐ ┌─────►│ 2 tagged_hash ◄────┐ │
│ │ TapBranch │ │ │ │ TapBranch │ │ │
│ └────────────────────────┘ │ │ └────────────────┘ │ │
│ │ │ │ │
│ │ ┌─────┼────────┐ ┌───────|───-─┐ ┌──────┴──────┐
┌───┼──────────┐ ┌──────────┼──┐ │ 1 tagged_hash│ │1 tagged_hash│ │1 tagged_hash│
│1 tagged_hash │ │1 tagged_hash│ │ Tapleaf │ │ Tapleaf │ │ Tapleaf │
│ Tapleaf │ │ Tapleaf │ └──────────────┘ └─────────────┘ └─────────────┘
└──▲───────────┘ └──────▲──────┘ ▲ ▲ ▲
│ │ │ │ │
version | A script version | B script version | C script version | D script version|E script

View File

@@ -0,0 +1,358 @@
:scrollbar:
:data-uri:
:toc2:
:linkattrs:
= Stack Element Size Performance Tests
:numbered:
== Overview
BIP-0360 proposes an increase in stack element size from current 520 bytes (v0.29) to 8kb.
Subsequently, there is a need to determine the performance and stability related consequences of doing so.
== Regression Tests
The following regression tests failed with `MAX_SCRIPT_ELEMENT_SIZE` set to 8000 .
[cols="1,1,2"]
|===
|feature_taproot.py | line 1338 | Missing error message 'Push value size limit exceeded' from block response 'mandatory-script-verify-flag-failed (Stack size must be exactly one after execution)'
|p2p_filter.py | lines 130-132 | Check that too large data element to add to the filter is rejected
|p2p_segwit.py | lines 1047-1049 | mandatory-script-verify-flag-failed (Push value size limit exceeded)
|rpc_createmultisig.py | lines 75-75 | No exception raised: redeemScript exceeds size limit: 684 > 520"
|===
**Analysis**
These 4 tests explicitly test for a stack element size of 520 and are expected to fail with a stack element size of 8Kb.
Subsequently, no further action needed.
== Performance Tests
=== OP_SHA256
The following Bitcoin script is used to conduct this performance test:
-----
<pre-image array> OP_SHA256 OP_DROP OP_1
-----
When executed, this script adds the pre-image array of arbitrary data to the stack.
Immediately after, a SHA256 hash function pops the pre-image array off the stack, executes a hash and adds the result to the top of the stack.
The `OP_DROP` operation removes the hash result from the stack.
==== Results Summary
[cols="3,1,1,1,1,1,1,1,1,1", options="header"]
|===
| Stack Element Size (Bytes) | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op |miss% | total
| 1 | 637.28 | 1,569,165.30 | 0.3% | 8,736.00 | 1,338.55 | 6.526 | 832.00 | 0.0% | 5.53
| 64 | 794.85 | 1,258,098.46 | 0.4% | 11,107.00 | 1,666.92 | 6.663 | 827.00 | 0.0% | 5.61
| 65 | 831.95 | 1,201,996.30 | 0.5% | 11,144.00 | 1,698.26 | 6.562 | 841.00 | 0.0% | 5.53
| 100 | 794.82 | 1,258,139.86 | 0.2% | 11,139.00 | 1,673.89 | 6.655 | 837.00 | 0.0% | 5.50
| 520 | 1,946.67 | 513,697.88 | 0.2% | 27,681.00 | 4,095.57 | 6.759 | 885.00 | 0.0% | 5.50
| 8000 | 20,958.63 | 47,713.05 | 2.7% | 304,137.02 | 43,789.86 | 6.945 | 1,689.02 | 0.4% | 5.63
|===
**Analysis**
The following observations are made from the performance test:
. **Performance Scaling**: The increase from 520 bytes to 8000 bytes (15.4x size increase) results in approximately 9.8x performance degradation (19,173 ns/op vs 1,947 ns/op).
This represents sub-linear scaling, which suggests the implementation handles large data efficiently.
. **Instruction Count Scaling**: Instructions per operation increase from 27,681 to 285,220 (10.3x increase), closely matching the performance degradation, indicating the bottleneck is primarily computational rather than memory bandwidth.
. **Throughput Impact**: Operations per second decrease from 513,698 op/s to 52,158 op/s, representing a 9.8x reduction in throughput.
. **Cache Efficiency**: The IPC (Instructions Per Cycle) remains relatively stable (6.759 to 7.094), suggesting good CPU pipeline utilization despite the increased data size.
. **Memory Access Patterns**: The branch mis-prediction rate increases slightly (0.0% to 0.4%), indicating minimal impact on branch prediction accuracy.
**key**
[cols="1,6", options="header"]
|===
| Metric | Description
| ns/op | Nanoseconds per operation - average time it takes to complete one benchmark iteration
| op/s | Operations per second - throughput rate showing how many benchmark iterations can be completed per second
| err% | Error percentage - statistical margin of error in the measurement, indicating the reliability of the benchmark results
| ins/op | Instructions per operation - the number of CPU instructions executed for each benchmark iteration
| cyc/op | CPU cycles per operation - the number of CPU clock cycles consumed for each benchmark iteration
| IPC | Instructions per cycle - the ratio of instructions executed per CPU cycle, indicating CPU efficiency and pipeline utilization
| bra/op | Branches per operation - the number of conditional branch instructions executed for each benchmark iteration
| miss% | Branch misprediction percentage - the rate at which the CPU incorrectly predicts branch outcomes, causing pipeline stalls
| total | Total benchmark time - the total wall-clock time spent running the entire benchmark in seconds
|===
==== Detailed Results
===== Stack Element Size = 1 Byte
[cols="2,1,1,1,1,1,1,1,1", options="header"]
|===
|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op |miss% |total
|637.28 |1,569,165.30 |0.3% |8,736.00 |1,338.55 |6.526 |832.00 |0.0% |5.53
|===
===== Stack Element Size = 64 Bytes
[cols="2,1,1,1,1,1,1,1,1", options="header"]
|===
| ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op | miss% | total
| 794.85 | 1,258,098.46 | 0.4% | 11,107.00 | 1,666.92 | 6.663 | 827.00 | 0.0% | 5.61
|===
====== Explanation
Even though 64 bytes doesn't require padding (it's exactly one SHA256 block), the ins/op still increases from 8,736 to 11,107 instructions. Here's why:
. Data Movement Overhead
* 1 byte: Minimal data to copy into the SHA256 processing buffer
* 64 bytes: 64x more data to move from the witness stack into the SHA256 input buffer
* Memory copying operations add instructions
. SHA256 State Initialization
* 1 byte: The 1-byte input gets padded to 64 bytes internally, but the padding is mostly zeros
* 64 bytes: All 64 bytes are actual data that needs to be processed
* The SHA256 algorithm may have different code paths for handling "real" data vs padded data
. Memory Access Patterns
* 1 byte: Single byte access, likely cache-friendly
* 64 bytes: Sequential access to 64 bytes, potentially different memory access patterns
* May trigger different CPU optimizations or cache behavior
. Bit Length Processing
* 1 byte: The SHA256 algorithm needs to set the bit length field (8 bits)
* 64 bytes: The bit length field is 512 bits
* Different bit length values may cause different code paths in the SHA256 implementation
. Loop Unrolling and Optimization
* 1 byte: Compiler might optimize the single-block case differently
* 64 bytes: May use different loop structures or optimization strategies
* The SHA256 implementation might have specialized code paths for different input sizes
. Witness Stack Operations
* 1 byte: Small witness element, minimal stack manipulation
* 64 bytes: Larger witness element, more complex stack operations
* The Bitcoin script interpreter has to handle larger data on the stack
The increase from 8,736 to 11,107 instructions (~27% increase) suggests that even without padding overhead, the additional data movement and processing of "real" data vs padded data adds significant instruction count.
This is a good example of how seemingly small changes in input size can affect the underlying implementation's code paths and optimization strategies.
===== Stack Element Size = 65 Bytes
1 byte more than the SHA256 _block_ size
[cols="2,1,1,1,1,1,1,1,1", options="header"]
|===
|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total
| 831.95 | 1,201,996.30 |0.5% |11,144.00 |1,698.26 | 6.562 |841.00 | 0.0% | 5.53
|===
===== Stack Element Size = 100 Bytes
[cols="2,1,1,1,1,1,1,1,1", options="header"]
|===
|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total
| 794.82 | 1,258,139.86 | 0.2% | 11,139.00 | 1,673.89 | 6.655 | 837.00 | 0.0% | 5.50
|===
===== Stack Element Size = 520 Bytes
[cols="2,1,1,1,1,1,1,1,1", options="header"]
|===
|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total
| 1,946.67 | 513,697.88 | 0.2% | 27,681.00 | 4,095.57 | 6.759 | 885.00 | 0.0% | 5.50
|===
===== Stack Element Size = 8000 Bytes
[cols="2,1,1,1,1,1,1,1,1", options="header"]
|===
|ns/op |op/s |err% |ins/op |cyc/op |IPC |bra/op | miss% | total
| 20,958.63 | 47,713.05 | 2.7% | 304,137.02 | 43,789.86 | 6.945 | 1,689.02 | 0.4% | 5.63
|===
=== OP_DUP OP_SHA256
NOTE: This test is likely irrelevant as per latest BIP-0360: _To prevent OP_DUP from creating an 8 MB stack by duplicating stack elements larger than 520 bytes we define OP_DUP to fail on stack elements larger than 520 bytes_.
This test builds off the previous (involving the hashing of large stack element data) by duplicating that stack element data.
The following Bitcoin script is used to conduct this performance test:
-----
<pre-image array> OP_DUP OP_SHA256 OP_DROP OP_1
-----
When executed, this script adds the pre-image array of arbitrary data to the stack.
Immediately after, a `OP_DUP` operation duplicates the pre-image array on the stack.
Then, a SHA256 hash function pops the pre-image array off the stack, executes a hash and adds the result to the top of the stack.
The `OP_DROP` operation removes the hash result from the stack.
==== Results Summary
[cols="3,1,1,1,1,1,1,1,1,1", options="header"]
|===
| Stack Element Size (Bytes) | ns/op | op/s | err% | ins/op | cyc/op | IPC | bra/op |miss% | total
| 1 | 714.83 | 1,398,937.33 | 0.7% | 9,548.00 | 1,488.22 | 6.416 | 1,012.00 | 0.0% | 5.57
| 64 | 858.44 | 1,164,905.19 | 0.4% | 11,911.00 | 1,800.87 | 6.614 | 999.00 | 0.0% | 5.11
| 65 | 868.40 | 1,151,539.31 | 0.8% | 11,968.00 | 1,814.31 | 6.596 | 1,019.00 | 0.0% | 5.56
| 100 | 864.33 | 1,156,966.91 | 0.4% | 11,963.00 | 1,809.16 | 6.612 | 1,015.00 | 0.0% | 5.49
| 520 | 2,036.64 | 491,005.94 | 0.7% | 28,615.00 | 4,266.27 | 6.707 | 1,073.00 | 0.0% | 5.52
| 8000 | 20,883.10 | 47,885.61 | 0.2% | 306,887.04 | 43,782.35 | 7.009 | 2,089.02 | 0.3% | 5.53
|===
==== Analysis
The following observations are made from the performance test (in comparison to the `OP_SHA256` test):
. OP_DUP Overhead: The OP_DUP operation adds overhead by duplicating the stack element, which requires:
* Memory allocation for the duplicate
* Data copying from the original to the duplicate
* Additional stack manipulation
. Size-Dependent Impact on ns/op:
* For small elements (1-100 bytes): Significant overhead (4.4% to 12.2%)
* For medium elements (520 bytes): Moderate overhead (4.6%)
* For large elements (8000 bytes): Negligible difference (-0.4%)
. Instruction Count Impact:
* 8000 bytes: 304,137 → 306,887 instructions (+2,750 instructions)
* The additional instructions for OP_DUP are relatively small compared to the SHA256 computation
. Memory Operations:
+
The OP_DUP operation primarily affects memory operations rather than computational complexity.
This explains why the impact diminishes with larger data sizes where SHA256 computation dominates the performance.
This analysis shows that the OP_DUP operation has a measurable but manageable performance impact, especially for larger stack elements where the computational overhead of SHA256 dominates the overall execution time.
=== Procedure
* Testing is done using functionality found in the link:https://github.com/jbride/bitcoin/tree/p2mr-pqc[p2mr branch] of Bitcoin Core.
* Compilation of Bitcoin Core is done using the following `cmake` flags:
+
-----
$ cmake \
-B build \
-DWITH_ZMQ=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBUILD_BENCH=ON
$ cmake --build build -j$(nproc)
-----
* Bench tests are conducted similar to the following :
+
-----
$ export PREIMAGE_SIZE_BYTES=8000
$ ./build/bin/bench_bitcoin --filter=VerifySHA256Bench -min-time=5000
-----
== Failure Analysis
Goals:
* Measure stack memory usage to detect overflows or excessive stack growth.
* Monitor heap memory usage to identify increased allocations or leaks caused by larger elements.
* Detect memory errors (e.g., invalid reads/writes, use-after-free) that might arise from modified stack handling.
* Assess performance impacts (e.g., memory allocation overhead) in critical paths like transaction validation.
=== Memory Errors
AddressSanitizer is a fast, compiler-based tool (available in GCC/Clang) for detecting memory errors with lower overhead than Valgrind.
==== Results
No memory errors or leaks were revealed by AddressSanitizer when running the `OP_SHA256` bench test for 30 minutes.
==== Procedure
AddressSanitizer is included with Clang/LLVM
. Compilation of Bitcoin Core is done using the following `cmake` flags:
+
-----
$ cmake -B build \
-DWITH_ZMQ=ON \
-DBUILD_BENCH=ON \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DSANITIZERS=address,undefined
$ cmake --build build -j$(nproc)
-----
. Check that ASan is statically linked to the _bench_bitcoin_ executable:
+
-----
$ nm build/bin/bench_bitcoin | grep asan | more
0000000000148240 T __asan_address_is_poisoned
00000000000a2fe6 t __asan_check_load_add_16_R13
...
000000000316c828 b _ZZN6__asanL18GlobalsByIndicatorEmE20globals_by_indicator
0000000003170ccc b _ZZN6__asanL7AsanDieEvE9num_calls
-----
. Set the following environment variable:
+
-----
$ export ASAN_OPTIONS="halt_on_error=0:detect_leaks=1:log_path=/tmp/asan_logs/asan"
-----
+
Doing so ensures that _address sanitizer_ :
.. avoids halting on the first error
.. is enable memory leak detection
.. writes ASAN related logs to a specified directory
== Test Environment
* Fedora 42
* 8 cores (Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz)
* 32 GB RAM
* OS settings:
+
-----
$ ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 126896
max locked memory (kbytes, -l) 8192
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 126896
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
-----
== Notes
. test with different thread stack sizes (ie: ulimit -s xxxx )