1
0
mirror of https://github.com/bitcoin/bips.git synced 2026-03-16 15:55:37 +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,2 @@
[registries.kellnr-denver-space]
index = "sparse+https://crates.denver.space/api/v1/crates/"

905
bip-0360/ref-impl/rust/Cargo.lock generated Normal file
View File

@@ -0,0 +1,905 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "base58ck"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f"
dependencies = [
"bitcoin-internals",
"bitcoin_hashes",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bech32"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitcoin-internals"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2"
dependencies = [
"serde",
]
[[package]]
name = "bitcoin-io"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
[[package]]
name = "bitcoin-p2mr-pqc"
version = "0.32.6-p2mr-pqc.1"
source = "sparse+https://crates.denver.space/api/v1/crates/"
checksum = "ce8a80e619111bf8d228f3f1b169a2487e6e28974f39c8935657640e0214f9ed"
dependencies = [
"base58ck",
"base64",
"bech32",
"bitcoin-internals",
"bitcoin-io",
"bitcoin-units",
"bitcoin_hashes",
"hex-conservative",
"hex_lit",
"secp256k1 0.29.1",
"serde",
]
[[package]]
name = "bitcoin-units"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2"
dependencies = [
"bitcoin-internals",
"serde",
]
[[package]]
name = "bitcoin_hashes"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16"
dependencies = [
"bitcoin-io",
"hex-conservative",
"serde",
]
[[package]]
name = "bitcoinpqc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf74aafaea8106c29daed19657c3952a4f297d44fbd437d5dd697772bb463fc2"
dependencies = [
"bindgen",
"bitmask-enum",
"cmake",
"hex",
"libc",
"secp256k1 0.31.1",
"serde",
]
[[package]]
name = "bitcoinpqc"
version = "0.3.0"
source = "sparse+https://crates.denver.space/api/v1/crates/"
checksum = "5fbdb2a3ebd6701c141909f4b7f16165b848481c5c3f37a8bd7b273304ead5a6"
dependencies = [
"bindgen",
"bitmask-enum",
"cmake",
"hex",
"libc",
"secp256k1 0.31.1",
"serde",
]
[[package]]
name = "bitflags"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bitmask-enum"
version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6cbbb8f56245b5a479b30a62cdc86d26e2f35c2b9f594bc4671654b03851380"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "cc"
version = "1.2.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "cmake"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.7+wasi-0.2.4",
]
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-conservative"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd"
dependencies = [
"arrayvec",
]
[[package]]
name = "hex_lit"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "log"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniscript"
version = "13.0.0-p2mr-pqc-1.0"
source = "sparse+https://crates.denver.space/api/v1/crates/"
checksum = "aae6f58a3f729d916bf610da66391345706d36115a346efd0041be21c4fa058f"
dependencies = [
"bech32",
"bitcoin-p2mr-pqc",
"bitcoinpqc 0.2.0",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "p2mr-ref"
version = "0.1.0"
dependencies = [
"anyhow",
"bitcoin-p2mr-pqc",
"bitcoinpqc 0.3.0",
"env_logger",
"hex",
"log",
"miniscript",
"once_cell",
"rand 0.9.2",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "regex"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "secp256k1"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
dependencies = [
"bitcoin_hashes",
"rand 0.8.5",
"secp256k1-sys 0.10.1",
"serde",
]
[[package]]
name = "secp256k1"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2"
dependencies = [
"bitcoin_hashes",
"rand 0.9.2",
"secp256k1-sys 0.11.0",
"serde",
]
[[package]]
name = "secp256k1-sys"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
dependencies = [
"cc",
]
[[package]]
name = "secp256k1-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38"
dependencies = [
"cc",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "wit-bindgen"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -0,0 +1,40 @@
[package]
name = "p2mr-ref"
version = "0.1.0"
edition = "2024"
[dependencies]
# Dev version of miniscript crate re-exports bitcoin 0.32.6
# view configuration for "kellnr-denver-space":
# cat .cargo/config.toml
miniscript = { version="=13.0.0-p2mr-pqc-1.0", registry="kellnr-denver-space" }
bitcoin = { package = "bitcoin-p2mr-pqc", version="0.32.6-p2mr-pqc.1", features = ["rand-std", "serde", "base64"], registry = "kellnr-denver-space" }
bitcoinpqc = { version="0.3.0", features = ["serde"], registry="kellnr-denver-space" }
# BDK Wallet with P2MR support
#bdk_wallet = { version = "=3.0.0-alpha.0-pqc-0.1", registry = "kellnr-denver-space" }
env_logger = "0.11.5"
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
once_cell = "1.19"
hex = "0.4.3"
anyhow = "1.0.98"
thiserror = "2.0.12"
rand = "0.9"
[patch.crates-io]
#bitcoin = { git = "https://github.com/jbride/rust-bitcoin.git", branch = "p2mr" }
# Verify:
# cargo update
# cargo tree -p bitcoin | more
# bitcoin = { path = "./rust-bitcoin/bitcoin" }
# cargo tree -p miniscript | more
#miniscript = { path = "./rust-miniscript" }
# bitcoinpqc = { path = "./libbitcoinpqc" }

View File

@@ -0,0 +1,45 @@
# p2mr test vectors
This rust project contains the test vectors for BIP-360
## Run Test Vectors
These test vectors are being developed in conjunction with forks of [rust-bitcoin](https://github.com/jbride/rust-bitcoin/tree/p2mr) and [rust-miniscript](https://github.com/jbride/rust-miniscript/tree/p2mr-pqc) customized with p2mr functionality.
1. environment variables
```
// Specify Bitcoin network used when generating bip350 (bech32m) address
// Options: regtest, testnet, signet
// Default: mainnet
$ export BITCOIN_NETWORK=<regtest | testnet | signet >
```
1. run a specific test:
```
$ cargo test test_p2mr_single_leaf_script_tree -- --nocapture
```
## Local Development
All P2MR/PQC enabled bitcoin crates are temporarily available in a custom crate registry at: `https://crates.denver.space`.
These crates will be made available in `crates.io` in the near future.
Subsequently, you will need to execute the following at the root of your rust workspace:
```bash
mkdir .cargo \
&& echo '[registries.kellnr-denver-space]
index = "sparse+https://crates.denver.space/api/v1/crates/"' > .cargo/config
```
Afterwards, for all P2MR/PQC enabled dependencies used in your project, include a "registry" similar to the following:
```bash
bitcoin = { version="0.32.6", registry = "kellnr-denver-space" }
```

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 )

View File

@@ -0,0 +1,30 @@
export BITCOIN_SOURCE_DIR=$HOME/bitcoin
export W_NAME=anduro
export USE_PQC=false
export TOTAL_LEAF_COUNT=5
export LEAF_TO_SPEND_FROM=4
b-cli -named createwallet \
wallet_name=$W_NAME \
descriptors=true \
load_on_startup=true
export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \
&& echo $BITCOIN_ADDRESS_INFO | jq -r .
export QUANTUM_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \
&& export LEAF_SCRIPT_PRIV_KEY_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_key_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' )
b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm'
export COINBASE_REWARD_TX_ID=$( b-cli -named generatetoaddress 1 $P2MR_ADDR 5 | jq -r '.[]' ) \
&& echo $COINBASE_REWARD_TX_ID
export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \
&& echo $P2MR_DESC \
&& b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]'

View File

@@ -0,0 +1,27 @@
use p2mr_ref::{create_p2mr_utxo, create_p2mr_multi_leaf_taptree, tap_tree_lock_type};
use p2mr_ref::data_structures::{UtxoReturn, TaptreeReturn, ConstructionReturn, LeafScriptType};
use std::env;
use log::{info, error};
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
fn main() -> ConstructionReturn {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let tap_tree_lock_type = tap_tree_lock_type();
info!("tap_tree_lock_type: {:?}", tap_tree_lock_type);
let taptree_return: TaptreeReturn = create_p2mr_multi_leaf_taptree();
let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(taptree_return.clone().tree_root_hex);
// Alert user about SPENDING_LEAF_TYPE requirement when using MIXED mode
if tap_tree_lock_type == LeafScriptType::Mixed {
info!("NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows:");
info!(" export SPENDING_LEAF_TYPE={}", taptree_return.leaf_script_type);
}
return ConstructionReturn {
taptree_return: taptree_return,
utxo_return: p2mr_utxo_return,
};
}

View File

@@ -0,0 +1,284 @@
use p2mr_ref::{ pay_to_p2wpkh_tx, verify_schnorr_signature_via_bytes, verify_slh_dsa_via_bytes, tap_tree_lock_type };
use p2mr_ref::data_structures::{SpendDetails, LeafScriptType};
use std::env;
use log::{info, error};
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
fn main() -> SpendDetails {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
// FUNDING_TX_ID environment variable is required
let funding_tx_id: String = env::var("FUNDING_TX_ID")
.unwrap_or_else(|_| {
error!("FUNDING_TX_ID environment variable is required but not set");
std::process::exit(1);
});
let funding_tx_id_bytes: Vec<u8> = hex::decode(funding_tx_id.clone()).unwrap();
// FUNDING_UTXO_AMOUNT_SATS environment variable is required
let funding_utxo_amount_sats: u64 = env::var("FUNDING_UTXO_AMOUNT_SATS")
.unwrap_or_else(|_| {
error!("FUNDING_UTXO_AMOUNT_SATS environment variable is required but not set");
std::process::exit(1);
})
.parse::<u64>()
.unwrap_or_else(|_| {
error!("FUNDING_UTXO_AMOUNT_SATS must be a valid u64 integer");
std::process::exit(1);
});
// The input index of the funding tx
// Allow override via FUNDING_UTXO_INDEX environment variable
let funding_utxo_index: u32 = env::var("FUNDING_UTXO_INDEX")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
info!("Funding tx id: {}, utxo index: {}", funding_tx_id, funding_utxo_index);
// FUNDING_SCRIPT_PUBKEY environment variable is required
let funding_script_pubkey_bytes: Vec<u8> = env::var("FUNDING_SCRIPT_PUBKEY")
.map(|s| hex::decode(s).unwrap())
.unwrap_or_else(|_| {
error!("FUNDING_SCRIPT_PUBKEY environment variable is required but not set");
std::process::exit(1);
});
let control_block_bytes: Vec<u8> = env::var("CONTROL_BLOCK_HEX")
.map(|s| hex::decode(s).unwrap())
.unwrap_or_else(|_| {
error!("CONTROL_BLOCK_HEX environment variable is required but not set");
std::process::exit(1);
});
info!("P2MR control block size: {}", control_block_bytes.len());
// TAP_TREE_LOCK_TYPE environment variable is required to determine key structure
let leaf_script_type: LeafScriptType = tap_tree_lock_type();
info!("leaf_script_type: {:?}", leaf_script_type);
// For Mixed trees, we need to determine the actual leaf type via SPENDING_LEAF_TYPE
let effective_leaf_type: LeafScriptType = if leaf_script_type == LeafScriptType::Mixed {
match env::var("SPENDING_LEAF_TYPE") {
Ok(value) => match value.as_str() {
"SCHNORR_ONLY" => {
info!("SPENDING_LEAF_TYPE: SCHNORR_ONLY");
LeafScriptType::SchnorrOnly
},
"SLH_DSA_ONLY" => {
info!("SPENDING_LEAF_TYPE: SLH_DSA_ONLY");
LeafScriptType::SlhDsaOnly
},
_ => {
error!("Invalid SPENDING_LEAF_TYPE '{}'. Must be SCHNORR_ONLY or SLH_DSA_ONLY", value);
std::process::exit(1);
}
},
Err(_) => {
error!("SPENDING_LEAF_TYPE environment variable is required when TAP_TREE_LOCK_TYPE=MIXED");
error!("Set SPENDING_LEAF_TYPE to the actual type of the leaf being spent (SCHNORR_ONLY or SLH_DSA_ONLY).");
error!("The leaf type is returned in the 'leaf_script_type' field of the tree construction output.");
std::process::exit(1);
}
}
} else {
leaf_script_type
};
// Parse private keys based on effective script type
let leaf_script_priv_keys_bytes: Vec<Vec<u8>> = match effective_leaf_type {
LeafScriptType::SlhDsaOnly => {
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
.unwrap_or_else(|_| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SLH_DSA_ONLY");
std::process::exit(1);
});
// Parse JSON array and extract the first (and only) hex string
let priv_keys_hex: String = serde_json::from_str::<Vec<String>>(&priv_keys_hex_array)
.unwrap_or_else(|_| {
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
std::process::exit(1);
})
.into_iter()
.next()
.unwrap_or_else(|| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty");
std::process::exit(1);
});
let priv_keys_bytes = hex::decode(priv_keys_hex).unwrap();
if priv_keys_bytes.len() != 64 {
error!("SLH-DSA private key must be 64 bytes, got {}", priv_keys_bytes.len());
std::process::exit(1);
}
vec![priv_keys_bytes]
},
LeafScriptType::SchnorrOnly => {
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
.unwrap_or_else(|_| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SCHNORR_ONLY");
std::process::exit(1);
});
// Parse JSON array and extract the first (and only) hex string
let priv_keys_hex: String = serde_json::from_str::<Vec<String>>(&priv_keys_hex_array)
.unwrap_or_else(|_| {
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
std::process::exit(1);
})
.into_iter()
.next()
.unwrap_or_else(|| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty");
std::process::exit(1);
});
let priv_keys_bytes = hex::decode(priv_keys_hex).unwrap();
if priv_keys_bytes.len() != 32 {
error!("Schnorr private key must be 32 bytes, got {}", priv_keys_bytes.len());
std::process::exit(1);
}
vec![priv_keys_bytes]
},
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
.unwrap_or_else(|_| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SCHNORR_AND_SLH_DSA");
std::process::exit(1);
});
// Parse JSON array and extract the hex strings
let priv_keys_hex_vec: Vec<String> = serde_json::from_str(&priv_keys_hex_array)
.unwrap_or_else(|_| {
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
std::process::exit(1);
});
if priv_keys_hex_vec.len() != 2 {
error!("For SCHNORR_AND_SLH_DSA, LEAF_SCRIPT_PRIV_KEYS_HEX must contain exactly 2 hex strings, got {}", priv_keys_hex_vec.len());
std::process::exit(1);
}
let schnorr_priv_key_hex = &priv_keys_hex_vec[0];
let slh_dsa_priv_key_hex = &priv_keys_hex_vec[1];
let schnorr_priv_key_bytes = hex::decode(schnorr_priv_key_hex).unwrap();
let slh_dsa_priv_key_bytes = hex::decode(slh_dsa_priv_key_hex).unwrap();
if schnorr_priv_key_bytes.len() != 32 {
error!("Schnorr private key must be 32 bytes, got {}", schnorr_priv_key_bytes.len());
std::process::exit(1);
}
if slh_dsa_priv_key_bytes.len() != 64 {
error!("SLH-DSA private key must be 64 bytes, got {}", slh_dsa_priv_key_bytes.len());
std::process::exit(1);
}
vec![schnorr_priv_key_bytes, slh_dsa_priv_key_bytes]
},
LeafScriptType::Mixed => {
// This case should never be reached because Mixed is resolved to effective_leaf_type above
unreachable!("Mixed should have been resolved to effective_leaf_type");
},
LeafScriptType::NotApplicable => {
panic!("LeafScriptType::NotApplicable is not applicable");
}
};
// ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
let leaf_script_bytes: Vec<u8> = env::var("LEAF_SCRIPT_HEX")
.map(|s| hex::decode(s).unwrap())
.unwrap_or_else(|_| {
error!("LEAF_SCRIPT_HEX environment variable is required but not set");
std::process::exit(1);
});
// https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
let spend_output_pubkey_hash_bytes: Vec<u8> = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
// OUTPUT_AMOUNT_SATS env var is optional. Default is FUNDING_UTXO_AMOUNT_SATS - 5000 sats
let spend_output_amount_sats: u64 = env::var("OUTPUT_AMOUNT_SATS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(funding_utxo_amount_sats.saturating_sub(5000));
let result: SpendDetails = pay_to_p2wpkh_tx(
funding_tx_id_bytes,
funding_utxo_index,
funding_utxo_amount_sats,
funding_script_pubkey_bytes,
control_block_bytes,
leaf_script_bytes.clone(),
leaf_script_priv_keys_bytes, // Now passing Vec<Vec<u8>> instead of Vec<u8>
spend_output_pubkey_hash_bytes,
spend_output_amount_sats,
effective_leaf_type // Use effective type (resolved from SPENDING_LEAF_TYPE if Mixed)
);
// Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes
let tapleaf_pubkey_bytes: Vec<u8> = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec();
match effective_leaf_type {
LeafScriptType::SlhDsaOnly => {
let is_valid: bool = verify_slh_dsa_via_bytes(&result.sig_bytes, &result.sighash, &tapleaf_pubkey_bytes);
info!("is_valid: {}", is_valid);
},
LeafScriptType::SchnorrOnly => {
let is_valid: bool = verify_schnorr_signature_via_bytes(
&result.sig_bytes,
&result.sighash,
&tapleaf_pubkey_bytes);
info!("is_valid: {}", is_valid);
},
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
// For combined scripts, we need to separate the signatures
// The sig_bytes contains: [schnorr_sig (64 bytes), slh_dsa_sig (7856 bytes)] (raw signatures without sighash)
let schnorr_sig_len = 64; // Schnorr signature is 64 bytes
let slh_dsa_sig_len = 7856; // SLH-DSA signature is 7856 bytes
let expected_min_len = schnorr_sig_len + slh_dsa_sig_len;
if result.sig_bytes.len() < expected_min_len {
error!("Combined signature length is too short: expected at least {}, got {}",
expected_min_len, result.sig_bytes.len());
return result;
}
// Extract Schnorr signature (first 64 bytes)
let schnorr_sig = &result.sig_bytes[..schnorr_sig_len];
// Extract SLH-DSA signature (next 7856 bytes)
let slh_dsa_sig = &result.sig_bytes[schnorr_sig_len..schnorr_sig_len + slh_dsa_sig_len];
// For SCHNORR_AND_SLH_DSA scripts, we need to extract the individual public keys
// The script structure is: OP_PUSHBYTES_32 <schnorr_pubkey(32)> OP_CHECKSIG OP_PUSHBYTES_32 <slh_dsa_pubkey(32)> OP_SUBSTR OP_BOOLAND OP_VERIFY
// So we need to extract the Schnorr pubkey (first 32 bytes after OP_PUSHBYTES_32)
let schnorr_pubkey_bytes = &leaf_script_bytes[1..33]; // Skip OP_PUSHBYTES_32 (0x20), get next 32 bytes
let slh_dsa_pubkey_bytes = &leaf_script_bytes[35..67]; // Skip OP_CHECKSIG (0xac), OP_PUSHBYTES_32 (0x20), get next 32 bytes
// Verify Schnorr signature
let schnorr_is_valid: bool = verify_schnorr_signature_via_bytes(
schnorr_sig,
&result.sighash,
schnorr_pubkey_bytes);
info!("Schnorr signature is_valid: {}", schnorr_is_valid);
// Verify SLH-DSA signature
let slh_dsa_is_valid: bool = verify_slh_dsa_via_bytes(
slh_dsa_sig,
&result.sighash,
slh_dsa_pubkey_bytes);
info!("SLH-DSA signature is_valid: {}", slh_dsa_is_valid);
let both_valid = schnorr_is_valid && slh_dsa_is_valid;
info!("Both signatures valid: {}", both_valid);
}
LeafScriptType::Mixed => {
// This case should never be reached because Mixed is resolved to effective_leaf_type above
unreachable!("Mixed should have been resolved to effective_leaf_type");
}
LeafScriptType::NotApplicable => {
panic!("LeafScriptType::NotApplicable is not applicable");
}
}
return result;
}

View File

@@ -0,0 +1,17 @@
use p2mr_ref::{create_p2tr_utxo, create_p2tr_multi_leaf_taptree};
use p2mr_ref::data_structures::{UtxoReturn, TaptreeReturn, ConstructionReturn};
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
fn main() -> ConstructionReturn {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let internal_pubkey_hex = "924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329".to_string();
let taptree_return: TaptreeReturn = create_p2tr_multi_leaf_taptree(internal_pubkey_hex.clone());
let utxo_return: UtxoReturn = create_p2tr_utxo(taptree_return.clone().tree_root_hex, internal_pubkey_hex);
return ConstructionReturn {
taptree_return: taptree_return,
utxo_return: utxo_return,
};
}

View File

@@ -0,0 +1,129 @@
use p2mr_ref::{ pay_to_p2wpkh_tx , verify_schnorr_signature_via_bytes};
use p2mr_ref::data_structures::{SpendDetails, LeafScriptType};
use std::env;
use log::{info, error};
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
fn main() -> SpendDetails {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
// FUNDING_TX_ID environment variable is required
let funding_tx_id: String = env::var("FUNDING_TX_ID")
.unwrap_or_else(|_| {
error!("FUNDING_TX_ID environment variable is required but not set");
std::process::exit(1);
});
let funding_tx_id_bytes: Vec<u8> = hex::decode(funding_tx_id.clone()).unwrap();
// FUNDING_UTXO_AMOUNT_SATS environment variable is required
let funding_utxo_amount_sats: u64 = env::var("FUNDING_UTXO_AMOUNT_SATS")
.unwrap_or_else(|_| {
error!("FUNDING_UTXO_AMOUNT_SATS environment variable is required but not set");
std::process::exit(1);
})
.parse::<u64>()
.unwrap_or_else(|_| {
error!("FUNDING_UTXO_AMOUNT_SATS must be a valid u64 integer");
std::process::exit(1);
});
// The input index of the funding tx
// Allow override via FUNDING_UTXO_INDEX environment variable
let funding_utxo_index: u32 = env::var("FUNDING_UTXO_INDEX")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(0);
info!("Funding tx id: {}, utxo index: {}", funding_tx_id, funding_utxo_index);
// FUNDING_SCRIPT_PUBKEY environment variable is required
let funding_script_pubkey_bytes: Vec<u8> = env::var("FUNDING_SCRIPT_PUBKEY")
.map(|s| hex::decode(s).unwrap())
.unwrap_or_else(|_| {
error!("FUNDING_SCRIPT_PUBKEY environment variable is required but not set");
std::process::exit(1);
});
let control_block_bytes: Vec<u8> = env::var("CONTROL_BLOCK_HEX")
.map(|s| hex::decode(s).unwrap())
.unwrap_or_else(|_| {
error!("CONTROL_BLOCK_HEX environment variable is required but not set");
std::process::exit(1);
});
info!("P2TR control block size: {}", control_block_bytes.len());
// P2TR only supports Schnorr signatures, so we only need one private key
let leaf_script_priv_key_bytes: Vec<u8> = {
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
.unwrap_or_else(|_| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required but not set");
std::process::exit(1);
});
// Parse JSON array and extract the first (and only) hex string
let priv_keys_hex: String = serde_json::from_str::<Vec<String>>(&priv_keys_hex_array)
.unwrap_or_else(|_| {
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
std::process::exit(1);
})
.into_iter()
.next()
.unwrap_or_else(|| {
error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty");
std::process::exit(1);
});
hex::decode(priv_keys_hex).unwrap()
};
// Validate that the private key is 32 bytes (Schnorr key size)
if leaf_script_priv_key_bytes.len() != 32 {
error!("P2TR private key must be 32 bytes (Schnorr), got {}", leaf_script_priv_key_bytes.len());
std::process::exit(1);
}
// Convert to Vec<Vec<u8>> format expected by the function
let leaf_script_priv_keys_bytes: Vec<Vec<u8>> = vec![leaf_script_priv_key_bytes];
// ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
let leaf_script_bytes: Vec<u8> = env::var("LEAF_SCRIPT_HEX")
.map(|s| hex::decode(s).unwrap())
.unwrap_or_else(|_| {
error!("LEAF_SCRIPT_HEX environment variable is required but not set");
std::process::exit(1);
});
// https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
let spend_output_pubkey_hash_bytes: Vec<u8> = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
// OUTPUT_AMOUNT_SATS env var is optional. Default is FUNDING_UTXO_AMOUNT_SATS - 5000 sats
let spend_output_amount_sats: u64 = env::var("OUTPUT_AMOUNT_SATS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(funding_utxo_amount_sats.saturating_sub(5000));
let result: SpendDetails = pay_to_p2wpkh_tx(
funding_tx_id_bytes,
funding_utxo_index,
funding_utxo_amount_sats,
funding_script_pubkey_bytes,
control_block_bytes,
leaf_script_bytes.clone(),
leaf_script_priv_keys_bytes, // Now passing Vec<Vec<u8>> format
spend_output_pubkey_hash_bytes.clone(),
spend_output_amount_sats,
LeafScriptType::SchnorrOnly
);
// Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes
let tapleaf_pubkey_bytes: Vec<u8> = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec();
let is_valid: bool = verify_schnorr_signature_via_bytes(
&result.sig_bytes,
&result.sighash,
&tapleaf_pubkey_bytes);
info!("is_valid: {}", is_valid);
return result;
}

View File

@@ -0,0 +1,69 @@
use std::env;
use log::info;
use once_cell::sync::Lazy;
use bitcoin::key::{Secp256k1};
use bitcoin::hashes::{sha256::Hash, Hash as HashTrait};
use bitcoin::secp256k1::{Message};
use p2mr_ref::{ acquire_schnorr_keypair, verify_schnorr_signature };
/* Secp256k1 implements the Signing trait when it's initialized in signing mode.
It's important to note that Secp256k1 has different capabilities depending on how it's constructed:
* Secp256k1::new() creates a context capable of both signing and verification
* Secp256k1::signing_only() creates a context that can only sign
* Secp256k1::verification_only() creates a context that can only verify
*/
static SECP: Lazy<Secp256k1<bitcoin::secp256k1::All>> = Lazy::new(Secp256k1::new);
fn main() {
let _ = env_logger::try_init();
// acquire a schnorr keypair (leveraging OS provided random number generator)
let keypair = acquire_schnorr_keypair();
let (secret_key, public_key) = keypair.as_schnorr().unwrap();
let message_bytes = b"hello";
// secp256k1 operates on a 256-bit (32-byte) field, so inputs must be exactly this size
// subsequently, Schnorr signatures on secp256k1 require exactly a 32-byte input (the curve's scalar field size)
let message_hash: Hash = Hash::hash(message_bytes);
let message: Message = Message::from_digest_slice(&message_hash.to_byte_array()).unwrap();
/* The secp256k1 library internally generates a random scalar value (aka: nonce or k-value) for each signature
* Every signature is unique - even if you sign the same message with the same private key multiple times
* The randomness is handled automatically by the secp256k1 implementation
* You get different signatures each time for the same inputs
* The nonce is only needed during signing, not during verification
Schnorr signatures require randomness for security reasons:
* Prevents private key recovery - If the same nonce is used twice, an attacker could potentially derive your private key
* Ensures signature uniqueness - Each signature should be cryptographically distinct
* Protects against replay attacks - Different signatures for the same data
*/
let signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr(&message, &secret_key.keypair(&SECP));
info!("Signature created successfully, size: {}", signature.serialize().len());
//let pubkey = public_key;
/*
* The nonce provides security during signing (prevents private key recovery)
* The nonce is mathematically eliminated during verification
* The verifier only needs public information (signature, message, public key)
*/
let schnorr_valid = verify_schnorr_signature(signature, message, *public_key);
info!("schnorr_valid: {}", schnorr_valid);
let aux_rand = [0u8; 32]; // 32 zero bytes; fine for testing
let signature_aux_rand: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand(
&message,
&secret_key.keypair(&SECP),
&aux_rand
);
info!("aux_rand signature created successfully, size: {}", signature_aux_rand.serialize().len());
let schnorr_valid_aux_rand = verify_schnorr_signature(signature_aux_rand, message, *public_key);
info!("schnorr_valid_aux_rand: {}", schnorr_valid_aux_rand);
}

View File

@@ -0,0 +1,77 @@
use std::env;
use log::info;
use once_cell::sync::Lazy;
use bitcoin::hashes::{sha256::Hash, Hash as HashTrait};
use rand::{rng, RngCore};
use bitcoinpqc::{
generate_keypair, public_key_size, secret_key_size, sign, signature_size, verify, Algorithm, KeyPair,
};
fn main() {
let _ = env_logger::try_init();
/*
In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to:
* Initialize hash function parameters within the key generation
* Seed the Merkle tree construction that forms the public key
* Generate the secret key components that enable signing
*/
let random_data = get_random_bytes(128);
println!("Generated random data of size {}", random_data.len());
let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data)
.expect("Failed to generate SLH-DSA-128S keypair");
let message_bytes = b"SLH-DSA-128S Test Message";
println!("Message to sign: {message_bytes:?}");
/* No need to hash the message
1. Variable Input Size: SPHINCS+ can handle messages of arbitrary length directly
2. Internal Hashing: The SPHINCS+ algorithm internally handles message processing and hashing as part of its design
3. Hash-Based Design: SPHINCS+ is built on hash functions and Merkle trees, so it's designed to work with variable-length inputs
4. No Curve Constraints: Unlike elliptic curve schemes, SPHINCS+ doesn't have fixed field size requirements
SLH-DSA doesn't use nonces like Schnorr does.
With SLH-DSA, randomness is built into the key generation process only ( and not the signing process; ie: SECP256K1)
Thus, no need for aux_rand data fed to the signature function.
The signing algorithm is deterministic and doesn't require random input during signing.
*/
let signature = sign(&keypair.secret_key, message_bytes).expect("Failed to sign with SLH-DSA-128S");
println!(
"Signature created successfully, size: {}",
signature.bytes.len()
);
println!(
"Signature prefix: {:02x?}",
&signature.bytes[..8.min(signature.bytes.len())]
);
// Verify the signature
println!("Verifying signature...");
let result = verify(&keypair.public_key, message_bytes, &signature);
println!("Verification result: {result:?}");
assert!(result.is_ok(), "SLH-DSA-128S signature verification failed");
// Try to verify with a modified message - should fail
let modified_message = b"SLH-DSA-128S Modified Message";
println!("Modified message: {modified_message:?}");
let result = verify(&keypair.public_key, modified_message, &signature);
println!("Verification with modified message result: {result:?}");
assert!(
result.is_err(),
"SLH-DSA-128S verification should fail with modified message"
);
}
fn get_random_bytes(size: usize) -> Vec<u8> {
let mut bytes = vec![0u8; size];
rng().fill_bytes(&mut bytes);
bytes
}

View File

@@ -0,0 +1,33 @@
use std::env;
use log::info;
use rand::{rng, RngCore};
use bitcoinpqc::{
generate_keypair, public_key_size, secret_key_size, Algorithm, KeyPair,
};
fn main() {
let _ = env_logger::try_init();
/*
In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to:
* Initialize hash function parameters within the key generation
* Seed the Merkle tree construction that forms the public key
* Generate the secret key components that enable signing
*/
let random_data = get_random_bytes(128);
println!("Generated random data of size {}", random_data.len());
let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data)
.expect("Failed to generate SLH-DSA-128S keypair");
info!("public key size / value = {}, {}", public_key_size(Algorithm::SLH_DSA_128S), hex::encode(&keypair.public_key.bytes));
info!("private key size / value = {}, {}", secret_key_size(Algorithm::SLH_DSA_128S), hex::encode(&keypair.secret_key.bytes));
}
fn get_random_bytes(size: usize) -> Vec<u8> {
let mut bytes = vec![0u8; size];
rng().fill_bytes(&mut bytes);
bytes
}

View File

@@ -0,0 +1,595 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use log::debug;
// Add imports for the unified keypair
use bitcoin::secp256k1::{SecretKey, XOnlyPublicKey};
use bitcoinpqc::{KeyPair, Algorithm};
/// Enum representing the type of leaf script to create
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LeafScriptType {
/// Script requires only SLH-DSA signature
SlhDsaOnly,
/// Script requires only Schnorr signature
SchnorrOnly,
/// Script requires both Schnorr and SLH-DSA signatures (in that order)
ConcatenatedSchnorrAndSlhDsaSameLeaf,
/// Leaves of TapTree are mixed. Some leaves are locked using Schnorr and others are locked using SLH-DSA
Mixed,
/// Script type is not applicable
NotApplicable,
}
impl LeafScriptType {
/// Check if this script type uses SLH-DSA
pub fn uses_slh_dsa(&self) -> bool {
matches!(self, LeafScriptType::SlhDsaOnly | LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf)
}
/// Check if this script type uses Schnorr
pub fn uses_schnorr(&self) -> bool {
matches!(self, LeafScriptType::SchnorrOnly | LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf)
}
/// Check if this script type requires both signature types
pub fn requires_both(&self) -> bool {
matches!(self, LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf)
}
/// Check if TapTree uses Schnorr for some leaves and SLH-DSA for others
pub fn uses_mixed(&self) -> bool {
matches!(self, LeafScriptType::Mixed)
}
/// Check if this script type is not applicable
pub fn is_not_applicable(&self) -> bool {
matches!(self, LeafScriptType::NotApplicable)
}
/// Convert to string representation for serialization
pub fn to_string(&self) -> String {
match self {
LeafScriptType::SlhDsaOnly => "SLH_DSA_ONLY".to_string(),
LeafScriptType::SchnorrOnly => "SCHNORR_ONLY".to_string(),
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => "CONCATENATED_SCHNORR_AND_SLH_DSA".to_string(),
LeafScriptType::Mixed => "MIXED".to_string(),
LeafScriptType::NotApplicable => "NOT_APPLICABLE".to_string(),
}
}
/// Parse from string representation
pub fn from_string(s: &str) -> Self {
match s {
"SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly,
"SCHNORR_ONLY" => LeafScriptType::SchnorrOnly,
"CONCATENATED_SCHNORR_AND_SLH_DSA" => LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf,
"MIXED" => LeafScriptType::Mixed,
_ => LeafScriptType::NotApplicable,
}
}
}
#[derive(Debug, Serialize)]
pub struct TestVectors {
pub version: u32,
#[serde(rename = "test_vectors")]
pub test_vectors: Vec<TestVector>,
#[serde(skip, default = "HashMap::new")]
pub test_vector_map: HashMap<String, TestVector>,
}
impl<'de> Deserialize<'de> for TestVectors {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
version: u32,
#[serde(rename = "test_vectors")]
test_vectors: Vec<TestVector>,
}
let helper = Helper::deserialize(deserializer)?;
let mut test_vector_map = HashMap::new();
for test in helper.test_vectors.iter() {
test_vector_map.insert(test.id.clone(), test.clone());
}
Ok(TestVectors {
version: helper.version,
test_vectors: helper.test_vectors,
test_vector_map,
})
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TestVector {
pub id: String,
pub objective: String,
pub given: TestVectorGiven,
pub intermediary: TestVectorIntermediary,
pub expected: TestVectorExpected,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TestVectorGiven {
#[serde(rename = "internalPubkey")]
pub internal_pubkey: Option<String>,
#[serde(rename = "scriptTree")]
pub script_tree: Option<TVScriptTree>,
#[serde(rename = "scriptInputs")]
pub script_inputs: Option<Vec<String>>,
#[serde(rename = "scriptHex")]
pub script_hex: Option<String>,
#[serde(rename = "controlBlock")]
pub control_block: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TestVectorIntermediary {
#[serde(default)]
#[serde(rename = "leafHashes")]
pub leaf_hashes: Vec<String>,
#[serde(rename = "merkleRoot")]
pub merkle_root: Option<String>
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TestVectorExpected {
#[serde(rename = "scriptPubKey")]
pub script_pubkey: Option<String>,
#[serde(rename = "bip350Address")]
pub bip350_address: Option<String>,
#[serde(default)]
#[serde(rename = "scriptPathControlBlocks")]
pub script_path_control_blocks: Option<Vec<String>>,
#[serde(rename = "error")]
pub error: Option<String>,
#[serde(rename = "address")]
pub address: Option<String>,
#[serde(default)]
pub witness: Option<String>
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TVScriptLeaf {
pub id: u8,
pub script: String,
#[serde(rename = "leafVersion")]
pub leaf_version: u8,
}
// Taproot script trees are binary trees, so each branch should have exactly two children.
#[derive(Debug, Serialize, Clone)]
pub enum TVScriptTree {
Leaf(TVScriptLeaf),
Branch {
// Box is used because Rust needs to know the exact size of types at compile time.
// Without it, we'd have an infinitely size recursive type.
// The enum itself is on the stack, but the Box fields within the Branch variant store pointers to heap-allocated ScriptTree values.
left: Box<TVScriptTree>,
right: Box<TVScriptTree>,
},
}
// Add custom deserialize implementation
impl<'de> Deserialize<'de> for TVScriptTree {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Helper {
Leaf(TVScriptLeaf),
Branch(Vec<TVScriptTree>),
}
match Helper::deserialize(deserializer)? {
Helper::Leaf(leaf) => Ok(TVScriptTree::Leaf(leaf)),
Helper::Branch(v) => {
assert!(v.len() == 2, "Branch must have exactly two children");
let mut iter = v.into_iter();
Ok(TVScriptTree::Branch {
left: Box::new(iter.next().unwrap()),
right: Box::new(iter.next().unwrap()),
})
}
}
}
}
// Add this enum before the TVScriptTree implementation
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Direction {
Left,
Right,
Root,
}
impl TVScriptTree {
/// Implements a "post-order" traversal as follows: left, right, branch
pub fn traverse_with_depth<F: FnMut(&TVScriptTree, u8, Direction)>(&self, depth: u8, direction: Direction, f: &mut F) {
match self {
TVScriptTree::Branch { left, right } => {
right.traverse_with_depth(depth, Direction::Right, f); // Pass Right for right subtree
left.traverse_with_depth(depth, Direction::Left, f); // Pass Left for left subtree
f(self, depth, direction); // Pass the current node's direction
}
TVScriptTree::Leaf { .. } => {
f(self, depth, direction);
}
}
}
/// Traverses the tree visiting right subtree leaves first, then left subtree leaves.
/// Depth increases by 1 at each branch level.
/*
root (depth 0)
/ \
L0 (depth 1) (subtree) (depth 1)
/ \
L1 (depth 2) L2 (depth 2)
The new traversal will visit:
L1 at depth 2 -> L2 at depth 2 -> L0 at depth 1
*/
pub fn traverse_with_right_subtree_first<F: FnMut(&TVScriptTree, u8, Direction)>(&self, depth: u8, direction: Direction, f: &mut F) {
match self {
TVScriptTree::Branch { left, right } => {
let next_depth = depth + 1;
// Visit right subtree first
right.traverse_with_right_subtree_first(next_depth, Direction::Right, f);
// Then visit left subtree
left.traverse_with_right_subtree_first(next_depth, Direction::Left, f);
}
TVScriptTree::Leaf { .. } => {
f(self, depth, direction);
}
}
}
}
impl std::fmt::Display for Direction {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Direction::Left => write!(f, "L"),
Direction::Right => write!(f, "R"),
Direction::Root => write!(f, "Root"),
}
}
}
pub struct ScriptTreeHashCache {
pub leaf_hashes: HashMap<String, String>,
pub branch_hashes: HashMap<u8, String>,
}
impl ScriptTreeHashCache {
pub fn new() -> Self {
Self {
leaf_hashes: HashMap::new(),
branch_hashes: HashMap::new(),
}
}
pub fn set_leaf_hash(&mut self, branch_id: u8, direction: Direction, hash: String) {
let key = format!("{branch_id}_{direction}");
debug!("set_leaf_hash: key: {}, hash: {}", key, hash);
self.leaf_hashes.insert(key, hash);
}
pub fn set_branch_hash(&mut self, branch_id: u8, hash: String) {
self.branch_hashes.insert(branch_id, hash);
}
}
fn serialize_hex<S>(bytes: &Vec<u8>, s: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
s.serialize_str(&hex::encode(bytes))
}
fn deserialize_hex<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(d)?;
hex::decode(s).map_err(serde::de::Error::custom)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpendDetails {
pub tx_hex: String,
#[serde(serialize_with = "serialize_hex")]
#[serde(deserialize_with = "deserialize_hex")]
pub sighash: Vec<u8>,
#[serde(serialize_with = "serialize_hex")]
#[serde(deserialize_with = "deserialize_hex")]
pub sig_bytes: Vec<u8>,
#[serde(serialize_with = "serialize_hex")]
#[serde(deserialize_with = "deserialize_hex")]
pub derived_witness_vec: Vec<u8>,
}
impl std::process::Termination for SpendDetails {
fn report(self) -> std::process::ExitCode {
if let Ok(json) = serde_json::to_string_pretty(&self) {
println!("{}", json);
} else {
println!("{:?}", self);
}
std::process::ExitCode::SUCCESS
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UtxoReturn {
pub script_pubkey_hex: String,
pub bech32m_address: String,
pub bitcoin_network: bitcoin::Network,
}
impl std::process::Termination for UtxoReturn {
fn report(self) -> std::process::ExitCode {
if let Ok(json) = serde_json::to_string_pretty(&self) {
println!("{}", json);
} else {
println!("{:?}", self);
}
std::process::ExitCode::SUCCESS
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaptreeReturn {
pub leaf_script_priv_keys_hex: Vec<String>, // Changed to support multiple private keys
pub leaf_script_hex: String,
pub tree_root_hex: String,
pub control_block_hex: String,
/// The script type of the leaf being returned (needed for spending)
pub leaf_script_type: String,
}
impl std::process::Termination for TaptreeReturn {
fn report(self) -> std::process::ExitCode {
if let Ok(json) = serde_json::to_string_pretty(&self) {
println!("{}", json);
} else {
println!("{:?}", self);
}
std::process::ExitCode::SUCCESS
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstructionReturn {
pub taptree_return: TaptreeReturn,
pub utxo_return: UtxoReturn,
}
impl std::process::Termination for ConstructionReturn {
fn report(self) -> std::process::ExitCode {
if let Ok(json) = serde_json::to_string_pretty(&self) {
println!("{}", json);
} else {
println!("{:?}", self);
}
std::process::ExitCode::SUCCESS
}
}
/// A unified keypair that can contain either a Schnorr keypair or an SLH-DSA keypair
#[derive(Debug, Clone)]
pub enum UnifiedKeypair {
Schnorr(SecretKey, XOnlyPublicKey),
SlhDsa(KeyPair),
}
/// A container for multiple keypairs that can be used in a single leaf script
#[derive(Debug, Clone)]
pub struct MultiKeypair {
pub schnorr_keypair: Option<UnifiedKeypair>,
pub slh_dsa_keypair: Option<UnifiedKeypair>,
}
impl MultiKeypair {
/// Create a new MultiKeypair with only a Schnorr keypair
pub fn new_schnorr_only(schnorr_keypair: UnifiedKeypair) -> Self {
Self {
schnorr_keypair: Some(schnorr_keypair),
slh_dsa_keypair: None,
}
}
/// Create a new MultiKeypair with only an SLH-DSA keypair
pub fn new_slh_dsa_only(slh_dsa_keypair: UnifiedKeypair) -> Self {
Self {
schnorr_keypair: None,
slh_dsa_keypair: Some(slh_dsa_keypair),
}
}
/// Create a new MultiKeypair with both keypairs
pub fn new_combined(schnorr_keypair: UnifiedKeypair, slh_dsa_keypair: UnifiedKeypair) -> Self {
Self {
schnorr_keypair: Some(schnorr_keypair),
slh_dsa_keypair: Some(slh_dsa_keypair),
}
}
/// Get all secret key bytes for serialization (in order: schnorr, then slh_dsa if present)
pub fn secret_key_bytes(&self) -> Vec<Vec<u8>> {
let mut result = Vec::new();
if let Some(ref schnorr) = self.schnorr_keypair {
result.push(schnorr.secret_key_bytes());
}
if let Some(ref slh_dsa) = self.slh_dsa_keypair {
result.push(slh_dsa.secret_key_bytes());
}
result
}
/// Get all public key bytes for script construction (in order: schnorr, then slh_dsa if present)
pub fn public_key_bytes(&self) -> Vec<Vec<u8>> {
let mut result = Vec::new();
if let Some(ref schnorr) = self.schnorr_keypair {
result.push(schnorr.public_key_bytes());
}
if let Some(ref slh_dsa) = self.slh_dsa_keypair {
result.push(slh_dsa.public_key_bytes());
}
result
}
/// Check if this contains a Schnorr keypair
pub fn has_schnorr(&self) -> bool {
self.schnorr_keypair.is_some()
}
/// Check if this contains an SLH-DSA keypair
pub fn has_slh_dsa(&self) -> bool {
self.slh_dsa_keypair.is_some()
}
/// Get the Schnorr keypair if present
pub fn schnorr_keypair(&self) -> Option<&UnifiedKeypair> {
self.schnorr_keypair.as_ref()
}
/// Get the SLH-DSA keypair if present
pub fn slh_dsa_keypair(&self) -> Option<&UnifiedKeypair> {
self.slh_dsa_keypair.as_ref()
}
}
/// Information about a single leaf in a mixed-type tree
/// Used when different leaves in the same tree use different algorithms
#[derive(Debug, Clone)]
pub struct MixedLeafInfo {
/// The leaf index in the tree
pub leaf_index: u32,
/// The script type for this specific leaf
pub leaf_script_type: LeafScriptType,
/// The keypairs for this leaf
pub keypairs: MultiKeypair,
/// The script for this leaf
pub script: Vec<u8>,
}
impl MixedLeafInfo {
/// Create a new MixedLeafInfo for a Schnorr-only leaf
pub fn new_schnorr(leaf_index: u32, keypairs: MultiKeypair, script: Vec<u8>) -> Self {
Self {
leaf_index,
leaf_script_type: LeafScriptType::SchnorrOnly,
keypairs,
script,
}
}
/// Create a new MixedLeafInfo for an SLH-DSA-only leaf
pub fn new_slh_dsa(leaf_index: u32, keypairs: MultiKeypair, script: Vec<u8>) -> Self {
Self {
leaf_index,
leaf_script_type: LeafScriptType::SlhDsaOnly,
keypairs,
script,
}
}
/// Create a new MixedLeafInfo for a combined Schnorr+SLH-DSA leaf
pub fn new_combined(leaf_index: u32, keypairs: MultiKeypair, script: Vec<u8>) -> Self {
Self {
leaf_index,
leaf_script_type: LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf,
keypairs,
script,
}
}
/// Get the secret key bytes for this leaf
pub fn secret_key_bytes(&self) -> Vec<Vec<u8>> {
self.keypairs.secret_key_bytes()
}
/// Get the public key bytes for this leaf
pub fn public_key_bytes(&self) -> Vec<Vec<u8>> {
self.keypairs.public_key_bytes()
}
}
impl UnifiedKeypair {
/// Create a new Schnorr keypair
pub fn new_schnorr(secret_key: SecretKey, public_key: XOnlyPublicKey) -> Self {
UnifiedKeypair::Schnorr(secret_key, public_key)
}
/// Create a new SLH-DSA keypair
pub fn new_slh_dsa(keypair: KeyPair) -> Self {
UnifiedKeypair::SlhDsa(keypair)
}
/// Get the secret key bytes for serialization
pub fn secret_key_bytes(&self) -> Vec<u8> {
match self {
UnifiedKeypair::Schnorr(secret_key, _) => secret_key.secret_bytes().to_vec(),
UnifiedKeypair::SlhDsa(keypair) => keypair.secret_key.bytes.clone(),
}
}
/// Get the public key bytes for script construction
pub fn public_key_bytes(&self) -> Vec<u8> {
match self {
UnifiedKeypair::Schnorr(_, public_key) => public_key.serialize().to_vec(),
UnifiedKeypair::SlhDsa(keypair) => keypair.public_key.bytes.clone(),
}
}
/// Get the algorithm type
pub fn algorithm(&self) -> &'static str {
match self {
UnifiedKeypair::Schnorr(_, _) => "Schnorr",
UnifiedKeypair::SlhDsa(_) => "SLH-DSA",
}
}
/// Check if this is a Schnorr keypair
pub fn is_schnorr(&self) -> bool {
matches!(self, UnifiedKeypair::Schnorr(_, _))
}
/// Check if this is an SLH-DSA keypair
pub fn is_slh_dsa(&self) -> bool {
matches!(self, UnifiedKeypair::SlhDsa(_))
}
/// Get the underlying Schnorr keypair if this is a Schnorr keypair
pub fn as_schnorr(&self) -> Option<(&SecretKey, &XOnlyPublicKey)> {
match self {
UnifiedKeypair::Schnorr(secret_key, public_key) => Some((secret_key, public_key)),
_ => None,
}
}
/// Get the underlying SLH-DSA keypair if this is an SLH-DSA keypair
pub fn as_slh_dsa(&self) -> Option<&KeyPair> {
match self {
UnifiedKeypair::SlhDsa(keypair) => Some(keypair),
_ => None,
}
}
}

View File

@@ -0,0 +1,16 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum P2MRError {
#[error("P2TR requires witness version of 1")]
P2trRequiresWitnessVersion1,
#[error("P2MR requires a script tree with at least one leaf")]
MissingScriptTreeLeaf,
// We can add more specific error variants here as needed
#[error("Invalid script tree structure: {0}")]
InvalidScriptTree(String),
}

View File

@@ -0,0 +1,721 @@
pub mod data_structures;
pub mod error;
use log::{debug, info, error};
use std::env;
use std::io::Write;
use rand::{rng, RngCore};
use once_cell::sync::Lazy;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::key::{Secp256k1, Parity};
use bitcoin::secp256k1::{Message, SecretKey, Keypair, rand::rngs::OsRng, rand::thread_rng, rand::Rng, schnorr::Signature};
use bitcoin::{ Amount, TxOut, WPubkeyHash,
Address, Network, OutPoint,
blockdata::witness::Witness,
Script, ScriptBuf, XOnlyPublicKey, PublicKey,
sighash::{SighashCache, TapSighashType, Prevouts, TapSighash},
taproot::{LeafVersion, NodeInfo, TapLeafHash, TapNodeHash, TapTree, ScriptLeaves, TaprootMerkleBranch, TaprootBuilder, TaprootSpendInfo, ControlBlock},
transaction::{Transaction, Sequence}
};
use bitcoin::p2mr::{P2mrScriptBuf, P2mrBuilder, P2mrSpendInfo, P2mrControlBlock, P2MR_LEAF_VERSION};
use bitcoinpqc::{
generate_keypair, public_key_size, secret_key_size, Algorithm, KeyPair, sign, verify,
};
use data_structures::{SpendDetails, UtxoReturn, TaptreeReturn, UnifiedKeypair, MultiKeypair, LeafScriptType, MixedLeafInfo};
/* Secp256k1 implements the Signing trait when it's initialized in signing mode.
It's important to note that Secp256k1 has different capabilities depending on how it's constructed:
* Secp256k1::new() creates a context capable of both signing and verification
* Secp256k1::signing_only() creates a context that can only sign
* Secp256k1::verification_only() creates a context that can only verify
*/
static SECP: Lazy<Secp256k1<bitcoin::secp256k1::All>> = Lazy::new(Secp256k1::new);
/// Creates a Huffman tree with leaves of the specified script type.
///
/// For Mixed type, leaves alternate between Schnorr (even indices) and SLH-DSA (odd indices).
/// The LEAF_TO_SPEND_FROM_TYPE env var can override the type for the leaf of interest.
///
/// Returns: (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type)
fn create_huffman_tree(leaf_script_type: LeafScriptType) -> (Vec<(u32, ScriptBuf)>, MultiKeypair, ScriptBuf, LeafScriptType) {
let mut total_leaf_count: u32 = 1;
if let Ok(env_value) = env::var("TOTAL_LEAF_COUNT") {
if let Ok(parsed_value) = env_value.parse::<u32>() {
total_leaf_count = parsed_value;
}
}
let mut leaf_to_spend_from: u32 = 0;
if let Ok(env_value) = env::var("LEAF_TO_SPEND_FROM") {
if let Ok(parsed_value) = env_value.parse::<u32>() {
leaf_to_spend_from = parsed_value;
}
}
// For Mixed mode, allow overriding the type of the leaf of interest
let leaf_to_spend_from_type: Option<LeafScriptType> = if leaf_script_type == LeafScriptType::Mixed {
env::var("LEAF_TO_SPEND_FROM_TYPE").ok().map(|s| LeafScriptType::from_string(&s))
} else {
None
};
if total_leaf_count < 1 {
panic!("total_leaf_count must be greater than 0");
}
if leaf_to_spend_from >= total_leaf_count {
panic!("leaf_to_spend_from must be less than total_leaf_count and greater than 0");
}
debug!("Creating multi-leaf taptree with total_leaf_count: {}, leaf_to_spend_from: {}", total_leaf_count, leaf_to_spend_from);
let mut huffman_entries: Vec<(u32, ScriptBuf)> = vec![];
let mut keypairs_of_interest: Option<MultiKeypair> = None;
let mut script_buf_of_interest: Option<ScriptBuf> = None;
let mut actual_leaf_type_of_interest: LeafScriptType = leaf_script_type;
for leaf_index in 0..total_leaf_count {
let keypairs: MultiKeypair;
let script_buf: ScriptBuf;
// Determine the effective script type for this leaf
let effective_script_type = if leaf_script_type == LeafScriptType::Mixed {
// For Mixed mode, check if this is the leaf of interest with an override
if leaf_index == leaf_to_spend_from && leaf_to_spend_from_type.is_some() {
leaf_to_spend_from_type.unwrap()
} else {
// Default pattern: even indices use Schnorr, odd indices use SLH-DSA
if leaf_index % 2 == 0 {
LeafScriptType::SchnorrOnly
} else {
LeafScriptType::SlhDsaOnly
}
}
} else {
leaf_script_type
};
match effective_script_type {
LeafScriptType::SchnorrOnly => {
let schnorr_keypair = acquire_schnorr_keypair();
keypairs = MultiKeypair::new_schnorr_only(schnorr_keypair);
let pubkey_bytes = keypairs.schnorr_keypair().unwrap().public_key_bytes();
// OP_PUSHBYTES_32 <32-byte xonly pubkey> OP_CHECKSIG
let mut script_buf_bytes = vec![0x20];
script_buf_bytes.extend_from_slice(&pubkey_bytes);
script_buf_bytes.push(0xac); // OP_CHECKSIG
script_buf = ScriptBuf::from_bytes(script_buf_bytes);
},
LeafScriptType::SlhDsaOnly => {
let slh_dsa_keypair = acquire_slh_dsa_keypair();
keypairs = MultiKeypair::new_slh_dsa_only(slh_dsa_keypair);
let pubkey_bytes = keypairs.slh_dsa_keypair().unwrap().public_key_bytes();
// OP_PUSHBYTES_32 <32-byte pubkey> OP_SUBSTR
let mut script_buf_bytes = vec![0x20];
script_buf_bytes.extend_from_slice(&pubkey_bytes);
script_buf_bytes.push(0x7f); // OP_SUBSTR
script_buf = ScriptBuf::from_bytes(script_buf_bytes);
},
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
// For combined scripts, we need both keypairs
let schnorr_keypair = acquire_schnorr_keypair();
let slh_dsa_keypair = acquire_slh_dsa_keypair();
keypairs = MultiKeypair::new_combined(schnorr_keypair, slh_dsa_keypair);
let schnorr_pubkey = keypairs.schnorr_keypair().unwrap().public_key_bytes();
let slh_dsa_pubkey = keypairs.slh_dsa_keypair().unwrap().public_key_bytes();
// Debug: Print the private key used for script construction
info!("SLH-DSA DEBUG: Script construction using private key: {}", hex::encode(keypairs.slh_dsa_keypair().unwrap().secret_key_bytes()));
info!("SLH-DSA DEBUG: Script construction using public key: {}", hex::encode(&slh_dsa_pubkey));
// Combined script: <Schnorr_PubKey> OP_CHECKSIG <SLH_DSA_PubKey> OP_SUBSTR OP_BOOLAND OP_VERIFY
let mut script_buf_bytes = vec![0x20]; // OP_PUSHBYTES_32
script_buf_bytes.extend_from_slice(&schnorr_pubkey);
script_buf_bytes.push(0xac); // OP_CHECKSIG
script_buf_bytes.push(0x20); // OP_PUSHBYTES_32
script_buf_bytes.extend_from_slice(&slh_dsa_pubkey);
script_buf_bytes.push(0x7f); // OP_SUBSTR
script_buf_bytes.push(0x9a); // OP_BOOLAND
script_buf_bytes.push(0x69); // OP_VERIFY
script_buf = ScriptBuf::from_bytes(script_buf_bytes);
}
LeafScriptType::Mixed => {
// This shouldn't happen as Mixed is resolved to a specific type above
panic!("LeafScriptType::Mixed should have been resolved to a specific type");
}
LeafScriptType::NotApplicable => {
panic!("LeafScriptType::NotApplicable is not applicable");
}
}
let random_weight = thread_rng().gen_range(0..total_leaf_count);
let huffman_entry = (random_weight, script_buf.clone());
huffman_entries.push(huffman_entry);
if leaf_index == leaf_to_spend_from {
keypairs_of_interest = Some(keypairs);
script_buf_of_interest = Some(script_buf.clone());
actual_leaf_type_of_interest = effective_script_type;
debug!("Selected leaf {}: type: {:?}, weight: {}, script: {:?}",
leaf_index, effective_script_type, random_weight, script_buf);
}
}
return (huffman_entries, keypairs_of_interest.unwrap(), script_buf_of_interest.unwrap(), actual_leaf_type_of_interest);
}
/// Parses the TAP_TREE_LOCK_TYPE environment variable and returns the corresponding LeafScriptType.
/// Defaults to LeafScriptType::SchnorrOnly if the environment variable is not set.
/// Exits with error code 1 if an invalid value is provided.
///
/// Supported values:
/// - SLH_DSA_ONLY: All leaves use SLH-DSA signatures
/// - SCHNORR_ONLY: All leaves use Schnorr signatures
/// - CONCATENATED_SCHNORR_AND_SLH_DSA: All leaves require both Schnorr and SLH-DSA signatures
/// - MIXED: Different leaves use different algorithms (Schnorr or SLH-DSA) (default)
pub fn tap_tree_lock_type() -> LeafScriptType {
match env::var("TAP_TREE_LOCK_TYPE") {
Ok(value) => match value.as_str() {
"SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly,
"SCHNORR_ONLY" => LeafScriptType::SchnorrOnly,
"CONCATENATED_SCHNORR_AND_SLH_DSA" => LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf,
"MIXED" => LeafScriptType::Mixed,
_ => {
error!("Invalid TAP_TREE_LOCK_TYPE '{}'. Must be one of: SLH_DSA_ONLY, SCHNORR_ONLY, CONCATENATED_SCHNORR_AND_SLH_DSA, MIXED", value);
std::process::exit(1);
}
},
Err(_) => {
// Default to Mixed if not set
LeafScriptType::Mixed
}
}
}
pub fn create_p2mr_multi_leaf_taptree() -> TaptreeReturn {
let leaf_script_type = tap_tree_lock_type();
let (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) = create_huffman_tree(leaf_script_type);
let p2mr_builder: P2mrBuilder = P2mrBuilder::with_huffman_tree(huffman_entries).unwrap();
let p2mr_spend_info: P2mrSpendInfo = p2mr_builder.clone().finalize().unwrap();
let merkle_root:TapNodeHash = p2mr_spend_info.merkle_root.unwrap();
let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap();
let mut script_leaves: ScriptLeaves = tap_tree.script_leaves();
let script_leaf = script_leaves
.find(|leaf| leaf.script() == script_buf_of_interest.as_script())
.expect("Script leaf not found");
let merkle_root_node_info: NodeInfo = p2mr_builder.clone().into_inner().try_into_node_info().unwrap();
let merkle_root: TapNodeHash = merkle_root_node_info.node_hash();
let leaf_hash: TapLeafHash = TapLeafHash::from_script(script_leaf.script(), LeafVersion::from_consensus(P2MR_LEAF_VERSION).unwrap());
// Convert leaf hash to big-endian for display (like Bitcoin Core)
let mut leaf_hash_bytes = leaf_hash.as_raw_hash().to_byte_array().to_vec();
leaf_hash_bytes.reverse();
info!("leaf_hash: {}, merkle_root: {}, merkle_root: {}",
hex::encode(leaf_hash_bytes),
merkle_root,
merkle_root);
let leaf_script = script_leaf.script();
let merkle_branch: &TaprootMerkleBranch = script_leaf.merkle_branch();
info!("Leaf script: {}, merkle branch: {:?}", leaf_script, merkle_branch);
let control_block: P2mrControlBlock = P2mrControlBlock{
merkle_branch: merkle_branch.clone(),
};
// Not a requirement here but useful to demonstrate what Bitcoin Core does as the verifier when spending from a p2mr UTXO
control_block.verify_script_in_merkle_root_path(leaf_script, merkle_root);
let control_block_hex: String = hex::encode(control_block.serialize());
return TaptreeReturn {
leaf_script_priv_keys_hex: keypairs_of_interest.secret_key_bytes()
.into_iter()
.map(|bytes| hex::encode(bytes))
.collect(),
leaf_script_hex: leaf_script.to_hex_string(),
tree_root_hex: hex::encode(merkle_root.to_byte_array()),
control_block_hex: control_block_hex,
leaf_script_type: actual_leaf_type.to_string(),
};
}
pub fn create_p2tr_multi_leaf_taptree(p2tr_internal_pubkey_hex: String) -> TaptreeReturn {
let (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) = create_huffman_tree(LeafScriptType::SchnorrOnly);
let pub_key_string = format!("02{}", p2tr_internal_pubkey_hex);
let internal_pubkey: PublicKey = pub_key_string.parse::<PublicKey>().unwrap();
let internal_xonly_pubkey: XOnlyPublicKey = internal_pubkey.inner.into();
let p2tr_builder: TaprootBuilder = TaprootBuilder::with_huffman_tree(huffman_entries).unwrap();
let p2tr_spend_info: TaprootSpendInfo = p2tr_builder.clone().finalize(&SECP, internal_xonly_pubkey).unwrap();
let merkle_root: TapNodeHash = p2tr_spend_info.merkle_root().unwrap();
// During taproot construction, the internal key is "tweaked" by adding a scalar (the tap tweak hash) to it.
// If this tweaking operation results in a public key w/ an odd Y-coordinate, the parity bit is set to 1.
// When spending via script path, the verifier needs to know whether the output key has an even or odd Y-coordinate to properly reconstruct & verify the internal key.
// The internal key can be recovered from the output key using the parity bit and the merkle root.
let output_key_parity: Parity = p2tr_spend_info.output_key_parity();
let output_key: XOnlyPublicKey = p2tr_spend_info.output_key().into();
info!("keypairs_of_interest: \n\tsecret_bytes: {:?} \n\tpubkeys: {:?} \n\tmerkle_root: {}",
keypairs_of_interest.secret_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::<Vec<_>>(), // secret_bytes returns big endian
keypairs_of_interest.public_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::<Vec<_>>(), // serialize returns little endian
merkle_root);
let tap_tree: TapTree = p2tr_builder.clone().try_into_taptree().unwrap();
let mut script_leaves: ScriptLeaves = tap_tree.script_leaves();
let script_leaf = script_leaves
.find(|leaf| leaf.script() == script_buf_of_interest.as_script())
.expect("Script leaf not found");
let leaf_script = script_leaf.script().to_hex_string();
let merkle_branch: &TaprootMerkleBranch = script_leaf.merkle_branch();
debug!("Leaf script: {}, merkle branch: {:?}", leaf_script, merkle_branch);
let control_block: ControlBlock = ControlBlock{
leaf_version: LeafVersion::TapScript,
output_key_parity: output_key_parity,
internal_key: internal_xonly_pubkey,
merkle_branch: merkle_branch.clone(),
};
let control_block_hex: String = hex::encode(control_block.serialize());
// Not a requirement but useful to demonstrate what Bitcoin Core does as the verifier when spending from a p2tr UTXO
let verify: bool = verify_taproot_commitment(control_block_hex.clone(), output_key, script_leaf.script());
info!("verify_taproot_commitment: {}", verify);
return TaptreeReturn {
leaf_script_priv_keys_hex: keypairs_of_interest.secret_key_bytes()
.into_iter()
.map(|bytes| hex::encode(bytes))
.collect(),
leaf_script_hex: leaf_script,
tree_root_hex: hex::encode(merkle_root.to_byte_array()),
control_block_hex: control_block_hex,
leaf_script_type: actual_leaf_type.to_string(),
};
}
/// Parses the BITCOIN_NETWORK environment variable and returns the corresponding Network.
/// Defaults to Network::Regtest if the environment variable is not set or has an invalid value.
pub fn get_bitcoin_network() -> Network {
let mut bitcoin_network: Network = Network::Regtest;
// Check for BITCOIN_NETWORK environment variable and override if set
if let Ok(network_str) = std::env::var("BITCOIN_NETWORK") {
bitcoin_network = match network_str.to_lowercase().as_str() {
"regtest" => Network::Regtest,
"testnet" => Network::Testnet,
"signet" => Network::Signet,
_ => {
debug!("Invalid BITCOIN_NETWORK value '{}', using default Regtest network", network_str);
Network::Regtest
}
};
}
bitcoin_network
}
pub fn create_p2mr_utxo(merkle_root_hex: String) -> UtxoReturn {
let merkle_root_bytes= hex::decode(merkle_root_hex.clone()).unwrap();
let merkle_root: TapNodeHash = TapNodeHash::from_byte_array(merkle_root_bytes.try_into().unwrap());
/* commit (in scriptPubKey) to the merkle root of all the script path leaves. ie:
This output key is what gets committed to in the final P2MR address (ie: scriptPubKey)
*/
let script_buf: P2mrScriptBuf = P2mrScriptBuf::new_p2mr(merkle_root);
let script: &Script = script_buf.as_script();
let script_pubkey = script.to_hex_string();
let bitcoin_network = get_bitcoin_network();
// derive bech32m address and verify against test vector
// p2mr address is comprised of network HRP + WitnessProgram (version + program)
let bech32m_address = Address::p2mr(Some(merkle_root), bitcoin_network);
return UtxoReturn {
script_pubkey_hex: script_pubkey,
bech32m_address: bech32m_address.to_string(),
bitcoin_network,
};
}
// Given script path p2tr or p2mr UTXO details, spend to p2wpkh
pub fn pay_to_p2wpkh_tx(
funding_tx_id_bytes: Vec<u8>,
funding_utxo_index: u32,
funding_utxo_amount_sats: u64,
funding_script_pubkey_bytes: Vec<u8>,
control_block_bytes: Vec<u8>,
leaf_script_bytes: Vec<u8>,
leaf_script_priv_keys_bytes: Vec<Vec<u8>>, // Changed to support multiple private keys
spend_output_pubkey_hash_bytes: Vec<u8>,
spend_output_amount_sats: u64,
leaf_script_type: LeafScriptType
) -> SpendDetails {
let mut txid_little_endian = funding_tx_id_bytes.clone(); // initially in big endian format
txid_little_endian.reverse(); // convert to little endian format
// vin: Create TxIn from the input utxo
// Details of this input tx are not known at this point
let input_tx_in = bitcoin::TxIn {
previous_output: OutPoint {
txid: bitcoin::Txid::from_slice(&txid_little_endian).unwrap(), // bitcoin::Txid expects the bytes in little-endian format
vout: funding_utxo_index,
},
script_sig: ScriptBuf::new(), // Empty for segwit transactions - script goes in witness
sequence: Sequence::MAX, // Default sequence, allows immediate spending (no RBF or timelock)
witness: bitcoin::Witness::new(), // Empty for now, will be filled with signature and pubkey after signing
};
let spend_wpubkey_hash = WPubkeyHash::from_byte_array(spend_output_pubkey_hash_bytes.try_into().unwrap());
let spend_output: TxOut = TxOut {
value: Amount::from_sat(spend_output_amount_sats),
script_pubkey: ScriptBuf::new_p2wpkh(&spend_wpubkey_hash),
};
// The spend tx to eventually be signed and broadcast
let mut unsigned_spend_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
input: vec![input_tx_in],
output: vec![spend_output],
};
// Create the leaf hash
let leaf_script = ScriptBuf::from_bytes(leaf_script_bytes.clone());
let leaf_hash: TapLeafHash = TapLeafHash::from_script(&leaf_script, LeafVersion::TapScript);
/* prevouts parameter tells the sighash algorithm:
1. The value of each input being spent (needed for fee calculation and sighash computation)
2. The scriptPubKey of each input being spent (ie: type of output & how to validate the spend)
*/
let prevouts = vec![TxOut {
value: Amount::from_sat(funding_utxo_amount_sats),
script_pubkey: ScriptBuf::from_bytes(funding_script_pubkey_bytes.clone()),
}];
info!("prevouts: {:?}", prevouts);
let spending_tx_input_index = 0;
// Create SighashCache
// At this point, sighash_cache does not know the values and type of input UTXO
let mut tapscript_sighash_cache = SighashCache::new(&mut unsigned_spend_tx);
// Compute the sighash
let tapscript_sighash: TapSighash = tapscript_sighash_cache.taproot_script_spend_signature_hash(
spending_tx_input_index, // input_index
&Prevouts::All(&prevouts),
leaf_hash,
TapSighashType::All
).unwrap();
info!("sighash: {:?}", tapscript_sighash);
let spend_msg = Message::from(tapscript_sighash);
let mut derived_witness: Witness = Witness::new();
let mut sig_bytes = Vec::new();
match leaf_script_type {
LeafScriptType::SlhDsaOnly => {
if leaf_script_priv_keys_bytes.len() != 1 {
panic!("SlhDsaOnly requires exactly one private key");
}
let secret_key: bitcoinpqc::SecretKey = bitcoinpqc::SecretKey::try_from_slice(
Algorithm::SLH_DSA_128S, &leaf_script_priv_keys_bytes[0]).unwrap();
let signature = sign(&secret_key, spend_msg.as_ref()).expect("Failed to sign with SLH-DSA-128S");
debug!("SlhDsaOnly signature.bytes: {:?}", signature.bytes.len());
let mut sig_bytes_with_sighash = signature.bytes.clone();
sig_bytes_with_sighash.push(TapSighashType::All as u8);
derived_witness.push(&sig_bytes_with_sighash);
sig_bytes = signature.bytes;
},
LeafScriptType::SchnorrOnly => {
if leaf_script_priv_keys_bytes.len() != 1 {
panic!("SchnorrOnly requires exactly one private key");
}
// assumes bytes are in big endian format
let secret_key = SecretKey::from_slice(&leaf_script_priv_keys_bytes[0]).unwrap();
// Spending a p2tr UTXO thus using Schnorr signature
// The aux_rand parameter ensures that signing the same message with the same key produces the same signature
// Otherwise (without providing aux_rand), the secp256k1 library internally generates a random nonce for each signature
let signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand(
&spend_msg,
&secret_key.keypair(&SECP),
&[0u8; 32] // 32 zero bytes of auxiliary random data
);
sig_bytes = signature.serialize().to_vec();
let mut sig_bytes_with_sighash = sig_bytes.clone();
sig_bytes_with_sighash.push(TapSighashType::All as u8);
derived_witness.push(&sig_bytes_with_sighash);
debug!("SchnorrOnly signature bytes: {:?}", sig_bytes.len());
},
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
if leaf_script_priv_keys_bytes.len() != 2 {
panic!("SchnorrAndSlhDsa requires exactly two private keys (Schnorr first, then SLH-DSA)");
}
// Generate Schnorr signature (first key)
let schnorr_secret_key = SecretKey::from_slice(&leaf_script_priv_keys_bytes[0]).unwrap();
let schnorr_signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand(
&spend_msg,
&schnorr_secret_key.keypair(&SECP),
&[0u8; 32] // 32 zero bytes of auxiliary random data
);
// Build combined signature for return value (without sighash bytes)
let mut combined_sig_bytes = schnorr_signature.serialize().to_vec();
debug!("SchnorrAndSlhDsa schnorr_sig_bytes: {:?}", combined_sig_bytes.len());
// Generate SLH-DSA signature (second key)
let slh_dsa_secret_key: bitcoinpqc::SecretKey = bitcoinpqc::SecretKey::try_from_slice(
Algorithm::SLH_DSA_128S, &leaf_script_priv_keys_bytes[1]).unwrap();
// Debug: Print the private key being used for signature creation
info!("SLH-DSA DEBUG: Using private key for signature creation: {}", hex::encode(&leaf_script_priv_keys_bytes[1]));
let slh_dsa_signature = sign(&slh_dsa_secret_key, spend_msg.as_ref()).expect("Failed to sign with SLH-DSA-128S");
debug!("SchnorrAndSlhDsa slh_dsa_signature.bytes: {:?}", slh_dsa_signature.bytes.len());
// Add SLH-DSA signature to combined signature for return value
combined_sig_bytes.extend_from_slice(&slh_dsa_signature.bytes);
sig_bytes = combined_sig_bytes;
// Build witness with sighash bytes
let mut witness_sig_bytes = schnorr_signature.serialize().to_vec();
witness_sig_bytes.push(TapSighashType::All as u8);
witness_sig_bytes.extend_from_slice(&slh_dsa_signature.bytes);
witness_sig_bytes.push(TapSighashType::All as u8);
derived_witness.push(&witness_sig_bytes);
}
LeafScriptType::Mixed => {
// Mixed is not a valid type for spending - the actual leaf type should be used
panic!("LeafScriptType::Mixed is not valid for spending. Use the actual leaf type (SchnorrOnly or SlhDsaOnly).");
}
LeafScriptType::NotApplicable => {
panic!("LeafScriptType::NotApplicable is not applicable");
}
}
// Note: sighash byte is now appended to signatures, not as separate witness element
derived_witness.push(&leaf_script_bytes);
derived_witness.push(&control_block_bytes);
let derived_witness_vec: Vec<u8> = derived_witness.iter().flatten().cloned().collect();
// Update the witness data for the tx's first input (index 0)
*tapscript_sighash_cache.witness_mut(spending_tx_input_index).unwrap() = derived_witness;
// Get the signed transaction.
let signed_tx_obj: &mut Transaction = tapscript_sighash_cache.into_transaction();
let tx_hex = bitcoin::consensus::encode::serialize_hex(&signed_tx_obj);
return SpendDetails {
tx_hex,
sighash: tapscript_sighash.as_byte_array().to_vec(),
sig_bytes: sig_bytes,
derived_witness_vec: derived_witness_vec,
};
}
pub fn create_p2tr_utxo(merkle_root_hex: String, internal_pubkey_hex: String) -> UtxoReturn {
let merkle_root_bytes= hex::decode(merkle_root_hex.clone()).unwrap();
let merkle_root: TapNodeHash = TapNodeHash::from_byte_array(merkle_root_bytes.try_into().unwrap());
let pub_key_string = format!("02{}", internal_pubkey_hex);
let internal_pubkey: PublicKey = pub_key_string.parse::<PublicKey>().unwrap();
let internal_xonly_pubkey: XOnlyPublicKey = internal_pubkey.inner.into();
let script_buf: ScriptBuf = ScriptBuf::new_p2tr(&SECP, internal_xonly_pubkey, Option::Some(merkle_root));
let script: &Script = script_buf.as_script();
let script_pubkey = script.to_hex_string();
let bitcoin_network = get_bitcoin_network();
// 4) derive bech32m address and verify against test vector
// p2mr address is comprised of network HRP + WitnessProgram (version + program)
let bech32m_address = Address::p2tr(
&SECP,
internal_xonly_pubkey,
Option::Some(merkle_root),
bitcoin_network
);
return UtxoReturn {
script_pubkey_hex: script_pubkey,
bech32m_address: bech32m_address.to_string(),
bitcoin_network,
};
}
// https://learnmeabitcoin.com/technical/upgrades/taproot/#examples
pub fn tagged_hash(tag: &str, data: &[u8]) -> String {
// Create a hash of the tag first
let tag_hash = sha256::Hash::hash(tag.as_bytes());
// Create preimage: tag_hash || tag_hash || message
// tag_hash is prefixed twice so that the prefix is 64 bytes in total
let mut preimage = sha256::Hash::engine();
preimage.write_all(&tag_hash.to_byte_array()).unwrap(); // First tag hash
preimage.write_all(&tag_hash.to_byte_array()).unwrap(); // Second tag hash
preimage.write_all(data).unwrap(); // Message data
let hash = sha256::Hash::from_engine(preimage).to_byte_array();
hex::encode(hash)
}
pub fn serialize_script(script: &Vec<u8>) -> Vec<u8> {
// get length of script as number of bytes
let length = script.len();
// return script with compact size prepended
let mut result = compact_size(length as u64);
result.extend_from_slice(&script);
result
}
/// Encodes an integer into Bitcoin's compact size format
/// Returns a Vec<u8> containing the encoded bytes
fn compact_size(n: u64) -> Vec<u8> {
if n <= 252 {
vec![n as u8]
} else if n <= 0xffff {
let mut result = vec![0xfd];
result.extend_from_slice(&(n as u16).to_le_bytes());
result
} else if n <= 0xffffffff {
let mut result = vec![0xfe];
result.extend_from_slice(&(n as u32).to_le_bytes());
result
} else {
let mut result = vec![0xff];
result.extend_from_slice(&n.to_le_bytes());
result
}
}
pub fn acquire_schnorr_keypair() -> UnifiedKeypair {
/* OsRng typically draws from the OS's entropy pool (hardware random num generators, system events, etc), ie:
* 1. $ cat /proc/sys/kernel/random/entropy_avail
* 2. $ sudo dmesg | grep -i "random\|rng\|entropy"
The Linux kernel's RNG (/dev/random and /dev/urandom) typically combines multiple entropy sources: ie:
* Hardware RNG (if available)
* CPU RNG instructions (RDRAND/RDSEED)
* Hardware events (disk I/O, network packets, keyboard/mouse input)
* Timer jitter
* Interrupt timing
*/
let keypair = Keypair::new(&SECP, &mut OsRng);
let privkey: SecretKey = keypair.secret_key();
let pubkey: (XOnlyPublicKey, Parity) = XOnlyPublicKey::from_keypair(&keypair);
UnifiedKeypair::new_schnorr(privkey, pubkey.0)
}
pub fn verify_schnorr_signature_via_bytes(signature: &[u8], message: &[u8], pubkey_bytes: &[u8]) -> bool {
// schnorr is 64 bytes so remove possible trailing Sighash Type byte if present
let mut sig_bytes = signature.to_vec();
if sig_bytes.len() == 65 {
sig_bytes.pop(); // Remove the last byte
}
let signature = bitcoin::secp256k1::schnorr::Signature::from_slice(&sig_bytes).unwrap();
let message = Message::from_digest_slice(message).unwrap();
let pubkey = XOnlyPublicKey::from_slice(pubkey_bytes).unwrap();
verify_schnorr_signature(signature, message, pubkey)
}
pub fn verify_slh_dsa_via_bytes(signature: &[u8], message: &[u8], pubkey_bytes: &[u8]) -> bool {
// Remove possible trailing Sighash Type byte if present (SLH-DSA-128S is 7856 bytes, so 7857 would indicate SIGHASH byte)
let mut sig_bytes = signature.to_vec();
if sig_bytes.len() == 7857 {
sig_bytes.pop(); // Remove the last byte
}
info!("verify_slh_dsa_via_bytes: signature length: {:?}, message: {:?}, pubkey_bytes: {:?}",
sig_bytes.len(),
hex::encode(message),
hex::encode(pubkey_bytes));
let signature = bitcoinpqc::Signature::try_from_slice(Algorithm::SLH_DSA_128S, &sig_bytes).unwrap();
let public_key: bitcoinpqc::PublicKey = bitcoinpqc::PublicKey::try_from_slice(Algorithm::SLH_DSA_128S, pubkey_bytes).unwrap();
verify(&public_key, message, &signature).is_ok()
}
pub fn verify_schnorr_signature(mut signature: Signature, message: Message, pubkey: XOnlyPublicKey) -> bool {
// schnorr is 64 bytes so remove possible trailing Sighash Type byte if present
if signature.serialize().to_vec().len() == 65 {
let mut sig_bytes = signature.serialize().to_vec();
sig_bytes.pop(); // Remove the last byte
signature = bitcoin::secp256k1::schnorr::Signature::from_slice(&sig_bytes).unwrap();
}
let is_valid: bool = SECP.verify_schnorr(&signature, &message, &pubkey).is_ok();
if !is_valid {
error!("verify schnorr failed:\n\tsignature: {:?}\n\tmessage: {:?}\n\tpubkey: {:?}",
signature,
message,
hex::encode(pubkey.serialize()));
}
is_valid
}
/* 1. Re-constructs merkle_root from merkle_path (found in control_block) and provided script.
2. Determines the parity of the output key via the control byte (found in the control block).
- the parity bit indicates whether the output key has an even or odd Y-coordinate
3. Computes the tap tweak hash using the internal key and reconstructed merkle root.
- tap_tweak_hash = tagged_hash("TapTweak", internal_key || merkle_root)
4. Verifies that the provided output key can be derived from the internal key using the tweak.
- tap_tweak_hash = tagged_hash("TapTweak", internal_key || merkle_root)
5. This proves the script is committed to in the taptree described by the output key.
*/
pub fn verify_taproot_commitment(control_block_hex: String, output_key: XOnlyPublicKey, script: &Script) -> bool {
let control_block_bytes = hex::decode(control_block_hex).unwrap();
let control_block: ControlBlock = ControlBlock::decode(&control_block_bytes).unwrap();
return control_block.verify_taproot_commitment(&SECP, output_key, script);
}
fn acquire_slh_dsa_keypair() -> UnifiedKeypair {
/*
In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to:
* Initialize hash function parameters within the key generation
* Seed the Merkle tree construction that forms the public key
* Generate the secret key components that enable signing
*/
let random_data = get_random_bytes(128);
let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data)
.expect("Failed to generate SLH-DSA-128S keypair");
UnifiedKeypair::new_slh_dsa(keypair)
}
fn get_random_bytes(size: usize) -> Vec<u8> {
let mut bytes = vec![0u8; size];
rng().fill_bytes(&mut bytes);
bytes
}

View File

@@ -0,0 +1,262 @@
use std::collections::HashSet;
use bitcoin::{Network, ScriptBuf};
use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash};
use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo};
use bitcoin::hashes::Hash;
use hex;
use log::debug;
use once_cell::sync::Lazy;
use p2mr_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn};
use p2mr_ref::error::P2MRError;
use p2mr_ref::{create_p2mr_utxo, tagged_hash};
// This file contains tests that execute against the BIP360 script-path-only test vectors.
static TEST_VECTORS: Lazy<TestVectors> = Lazy::new(|| {
let bip360_test_vectors = include_str!("../../common/tests/data/p2mr_construction.json");
let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap();
assert_eq!(test_vectors.version, 1);
test_vectors
});
static P2TR_USING_V2_WITNESS_VERSION_ERROR: &str = "p2tr_using_v2_witness_version_error";
static P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2mr_missing_leaf_script_tree_error";
static P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2mr_single_leaf_script_tree";
static P2MR_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2mr_different_version_leaves";
static P2MR_TWO_LEAF_SAME_VERSION_TEST: &str = "p2mr_two_leaf_same_version";
static P2MR_THREE_LEAF_COMPLEX_TEST: &str = "p2mr_three_leaf_complex";
static P2MR_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2mr_three_leaf_alternative";
static P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2mr_simple_lightning_contract";
#[test]
fn test_p2tr_using_v2_witness_version_error() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2TR_USING_V2_WITNESS_VERSION_ERROR).unwrap();
let test_result: anyhow::Result<()> = process_test_vector_p2tr(test_vector);
assert!(matches!(test_result.unwrap_err().downcast_ref::<P2MRError>(),
Some(P2MRError::P2trRequiresWitnessVersion1)));
}
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
#[test]
fn test_p2mr_missing_leaf_script_tree_error() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap();
let test_result: anyhow::Result<()> = process_test_vector_p2mr(test_vector);
assert!(matches!(test_result.unwrap_err().downcast_ref::<P2MRError>(),
Some(P2MRError::MissingScriptTreeLeaf)));
}
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
#[test]
fn test_p2mr_single_leaf_script_tree() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_different_version_leaves() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_DIFFERENT_VERSION_LEAVES_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_simple_lightning_contract() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_two_leaf_same_version() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_TWO_LEAF_SAME_VERSION_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_three_leaf_complex() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_COMPLEX_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_three_leaf_alternative() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_ALTERNATIVE_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
fn process_test_vector_p2tr(test_vector: &TestVector) -> anyhow::Result<()> {
let script_pubkey_hex = test_vector.expected.script_pubkey.as_ref().unwrap();
let script_pubkey_bytes = hex::decode(script_pubkey_hex).unwrap();
if script_pubkey_bytes[0] != 0x51 {
return Err(P2MRError::P2trRequiresWitnessVersion1.into());
}
Ok(())
}
fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> {
let tv_script_tree: Option<&TVScriptTree> = test_vector.given.script_tree.as_ref();
let mut tv_leaf_count: u8 = 0;
let mut current_branch_id: u8 = 0;
// TaprootBuilder expects the addition of each leaf script with its associated depth
// It then constructs the binary tree in DFS order, sorting siblings lexicographically & combining them via BIP341's tapbranch_hash
// Use of TaprootBuilder avoids user error in constructing branches manually and ensures Merkle tree correctness and determinism
let mut p2mr_builder: P2mrBuilder = P2mrBuilder::new();
let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new();
// 1) traverse test vector script tree and add leaves to P2MR builder
if let Some(script_tree) = tv_script_tree {
script_tree.traverse_with_right_subtree_first(0, Direction::Root,&mut |node, depth, direction| {
if let TVScriptTree::Leaf(tv_leaf) = node {
let tv_leaf_script_bytes = hex::decode(&tv_leaf.script).unwrap();
// NOTE: IOT to execute script_info.control_block(..), will add these to a vector
let tv_leaf_script_buf = ScriptBuf::from_bytes(tv_leaf_script_bytes.clone());
let tv_leaf_version = LeafVersion::from_consensus(tv_leaf.leaf_version).unwrap();
control_block_data.push((tv_leaf_script_buf.clone(), tv_leaf_version));
let mut modified_depth = depth + 1;
if direction == Direction::Root {
modified_depth = depth;
}
debug!("traverse_with_depth: leaf_count: {}, depth: {}, modified_depth: {}, direction: {}, tv_leaf_script: {}",
tv_leaf_count, depth, modified_depth, direction, tv_leaf.script);
// NOTE: Some of the the test vectors in this project specify leaves with non-standard versions (ie: 250 / 0xfa)
p2mr_builder = p2mr_builder.clone().add_leaf_with_ver(depth, tv_leaf_script_buf.clone(), tv_leaf_version)
.unwrap_or_else(|e| {
panic!("Failed to add leaf: {:?}", e);
});
tv_leaf_count += 1;
} else if let TVScriptTree::Branch { left, right } = node {
// No need to calculate branch hash.
// TaprootBuilder does this for us.
debug!("branch_count: {}, depth: {}, direction: {}", current_branch_id, depth, direction);
current_branch_id += 1;
}
});
}else {
return Err(P2MRError::MissingScriptTreeLeaf.into());
}
let spend_info: P2mrSpendInfo = p2mr_builder.clone()
.finalize()
.unwrap_or_else(|e| {
panic!("finalize failed: {:?}", e);
});
let derived_merkle_root: TapNodeHash = spend_info.merkle_root.unwrap();
// 2) verify derived merkle root against test vector
let test_vector_merkle_root = test_vector.intermediary.merkle_root.as_ref().unwrap();
assert_eq!(
derived_merkle_root.to_string(),
*test_vector_merkle_root,
"Merkle root mismatch"
);
debug!("just passed merkle root validation: {}", test_vector_merkle_root);
let test_vector_leaf_hashes_vec: Vec<String> = test_vector.intermediary.leaf_hashes.clone();
let test_vector_leaf_hash_set: HashSet<String> = test_vector_leaf_hashes_vec.iter().cloned().collect();
let test_vector_control_blocks_vec = &test_vector.expected.script_path_control_blocks;
let test_vector_control_blocks_set: HashSet<String> = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect();
let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap();
let script_leaves: ScriptLeaves = tap_tree.script_leaves();
// TO-DO: Investigate why the ordering of script leaves seems to be reverse of test vectors.
// 3) Iterate through leaves of derived script tree and verify both script leaf hashes and control blocks
for derived_leaf in script_leaves {
let version = derived_leaf.version();
let script = derived_leaf.script();
let merkle_branch: &TaprootMerkleBranch = derived_leaf.merkle_branch();
let derived_leaf_hash: TapLeafHash = TapLeafHash::from_script(script, version);
let leaf_hash = hex::encode(derived_leaf_hash.as_raw_hash().to_byte_array());
assert!(
test_vector_leaf_hash_set.contains(&leaf_hash),
"Leaf hash not found in expected set for {}", leaf_hash
);
debug!("just passed leaf_hash validation: {}", leaf_hash);
// Each leaf in the script tree has a corresponding control block.
// Specific to P2TR, the 3 sections of the control block (control byte, public key & merkle path) are highlighted here:
// https://learnmeabitcoin.com/technical/upgrades/taproot/#script-path-spend-control-block
// The control block, which includes the Merkle path, must be 33 + 32 * n bytes, where n is the number of Merkle path hashes (n ≥ 0).
// There is no consensus limit on n, but large Merkle trees increase the witness size, impacting block weight.
// NOTE: Control blocks could have also been obtained from spend_info.control_block(..) using the data in control_block_data
debug!("merkle_branch nodes: {:?}", merkle_branch);
let derived_control_block: P2mrControlBlock = P2mrControlBlock{
merkle_branch: merkle_branch.clone(),
};
let serialized_control_block = derived_control_block.serialize();
debug!("derived_control_block: {:?}, merkle_branch size: {}, control_block size: {}, serialized size: {}",
derived_control_block,
merkle_branch.len(),
derived_control_block.size(),
serialized_control_block.len());
let derived_serialized_control_block = hex::encode(serialized_control_block);
assert!(
test_vector_control_blocks_set.contains(&derived_serialized_control_block),
"Control block mismatch: {}, expected: {:?}", derived_serialized_control_block, test_vector_control_blocks_set
);
debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block);
}
let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(derived_merkle_root.to_string());
assert_eq!(
p2mr_utxo_return.script_pubkey_hex,
*test_vector.expected.script_pubkey.as_ref().unwrap(),
"Script pubkey mismatch"
);
debug!("just passed script_pubkey validation. script_pubkey = {}", p2mr_utxo_return.script_pubkey_hex);
let bech32m_address: String = p2mr_utxo_return.bech32m_address;
debug!("derived bech32m address for bitcoin_network: {} : {}", p2mr_utxo_return.bitcoin_network, bech32m_address);
if p2mr_utxo_return.bitcoin_network == Network::Bitcoin {
assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch.");
}
Ok(())
}

View File

@@ -0,0 +1,240 @@
use std::collections::HashSet;
use bitcoin::{Network, ScriptBuf};
use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash};
use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo};
use bitcoin::hashes::Hash;
use hex;
use log::debug;
use once_cell::sync::Lazy;
use p2mr_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn};
use p2mr_ref::error::P2MRError;
use p2mr_ref::{create_p2mr_utxo, tagged_hash};
// This file contains tests that execute against the BIP360 script-path-only test vectors.
static TEST_VECTORS: Lazy<TestVectors> = Lazy::new(|| {
let bip360_test_vectors = include_str!("../../common/tests/data/p2mr_pqc_construction.json");
let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap();
assert_eq!(test_vectors.version, 1);
test_vectors
});
static P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2mr_missing_leaf_script_tree_error";
static P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2mr_single_leaf_script_tree";
static P2MR_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2mr_different_version_leaves";
static P2MR_TWO_LEAF_SAME_VERSION_TEST: &str = "p2mr_two_leaf_same_version";
static P2MR_THREE_LEAF_COMPLEX_TEST: &str = "p2mr_three_leaf_complex";
static P2MR_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2mr_three_leaf_alternative";
static P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2mr_simple_lightning_contract";
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
#[test]
fn test_p2mr_pqc_missing_leaf_script_tree_error() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap();
let test_result: anyhow::Result<()> = process_test_vector_p2mr(test_vector);
assert!(matches!(test_result.unwrap_err().downcast_ref::<P2MRError>(),
Some(P2MRError::MissingScriptTreeLeaf)));
}
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
#[test]
fn test_p2mr_pqc_single_leaf_script_tree() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_pqc_different_version_leaves() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_DIFFERENT_VERSION_LEAVES_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_pqc_simple_lightning_contract() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_pqc_two_leaf_same_version() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_TWO_LEAF_SAME_VERSION_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_pqc_three_leaf_complex() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_COMPLEX_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
#[test]
fn test_p2mr_pqc_three_leaf_alternative() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let test_vectors = &*TEST_VECTORS;
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_ALTERNATIVE_TEST).unwrap();
process_test_vector_p2mr(test_vector).unwrap();
}
fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> {
let tv_script_tree: Option<&TVScriptTree> = test_vector.given.script_tree.as_ref();
let mut tv_leaf_count: u8 = 0;
let mut current_branch_id: u8 = 0;
// TaprootBuilder expects the addition of each leaf script with its associated depth
// It then constructs the binary tree in DFS order, sorting siblings lexicographically & combining them via BIP341's tapbranch_hash
// Use of TaprootBuilder avoids user error in constructing branches manually and ensures Merkle tree correctness and determinism
let mut p2mr_builder: P2mrBuilder = P2mrBuilder::new();
let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new();
// 1) traverse test vector script tree and add leaves to P2MR builder
if let Some(script_tree) = tv_script_tree {
script_tree.traverse_with_right_subtree_first(0, Direction::Root,&mut |node, depth, direction| {
if let TVScriptTree::Leaf(tv_leaf) = node {
let tv_leaf_script_bytes = hex::decode(&tv_leaf.script).unwrap();
// NOTE: IOT to execute script_info.control_block(..), will add these to a vector
let tv_leaf_script_buf = ScriptBuf::from_bytes(tv_leaf_script_bytes.clone());
let tv_leaf_version = LeafVersion::from_consensus(tv_leaf.leaf_version).unwrap();
control_block_data.push((tv_leaf_script_buf.clone(), tv_leaf_version));
let mut modified_depth = depth + 1;
if direction == Direction::Root {
modified_depth = depth;
}
debug!("traverse_with_depth: leaf_count: {}, depth: {}, modified_depth: {}, direction: {}, tv_leaf_script: {}",
tv_leaf_count, depth, modified_depth, direction, tv_leaf.script);
// NOTE: Some of the the test vectors in this project specify leaves with non-standard versions (ie: 250 / 0xfa)
p2mr_builder = p2mr_builder.clone().add_leaf_with_ver(depth, tv_leaf_script_buf.clone(), tv_leaf_version)
.unwrap_or_else(|e| {
panic!("Failed to add leaf: {:?}", e);
});
tv_leaf_count += 1;
} else if let TVScriptTree::Branch { left, right } = node {
// No need to calculate branch hash.
// TaprootBuilder does this for us.
debug!("branch_count: {}, depth: {}, direction: {}", current_branch_id, depth, direction);
current_branch_id += 1;
}
});
}else {
return Err(P2MRError::MissingScriptTreeLeaf.into());
}
let spend_info: P2mrSpendInfo = p2mr_builder.clone()
.finalize()
.unwrap_or_else(|e| {
panic!("finalize failed: {:?}", e);
});
let derived_merkle_root: TapNodeHash = spend_info.merkle_root.unwrap();
// 2) verify derived merkle root against test vector
let test_vector_merkle_root = test_vector.intermediary.merkle_root.as_ref().unwrap();
assert_eq!(
derived_merkle_root.to_string(),
*test_vector_merkle_root,
"Merkle root mismatch"
);
debug!("just passed merkle root validation: {}", test_vector_merkle_root);
let test_vector_leaf_hashes_vec: Vec<String> = test_vector.intermediary.leaf_hashes.clone();
let test_vector_leaf_hash_set: HashSet<String> = test_vector_leaf_hashes_vec.iter().cloned().collect();
let test_vector_control_blocks_vec = &test_vector.expected.script_path_control_blocks;
let test_vector_control_blocks_set: HashSet<String> = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect();
let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap();
let script_leaves: ScriptLeaves = tap_tree.script_leaves();
// TO-DO: Investigate why the ordering of script leaves seems to be reverse of test vectors.
// 3) Iterate through leaves of derived script tree and verify both script leaf hashes and control blocks
for derived_leaf in script_leaves {
let version = derived_leaf.version();
let script = derived_leaf.script();
let merkle_branch: &TaprootMerkleBranch = derived_leaf.merkle_branch();
let derived_leaf_hash: TapLeafHash = TapLeafHash::from_script(script, version);
let leaf_hash = hex::encode(derived_leaf_hash.as_raw_hash().to_byte_array());
assert!(
test_vector_leaf_hash_set.contains(&leaf_hash),
"Leaf hash not found in expected set for {}", leaf_hash
);
debug!("just passed leaf_hash validation: {}", leaf_hash);
// Each leaf in the script tree has a corresponding control block.
// Specific to P2TR, the 3 sections of the control block (control byte, public key & merkle path) are highlighted here:
// https://learnmeabitcoin.com/technical/upgrades/taproot/#script-path-spend-control-block
// The control block, which includes the Merkle path, must be 33 + 32 * n bytes, where n is the number of Merkle path hashes (n ≥ 0).
// There is no consensus limit on n, but large Merkle trees increase the witness size, impacting block weight.
// NOTE: Control blocks could have also been obtained from spend_info.control_block(..) using the data in control_block_data
debug!("merkle_branch nodes: {:?}", merkle_branch);
let derived_control_block: P2mrControlBlock = P2mrControlBlock{
merkle_branch: merkle_branch.clone(),
};
let serialized_control_block = derived_control_block.serialize();
debug!("derived_control_block: {:?}, merkle_branch size: {}, control_block size: {}, serialized size: {}",
derived_control_block,
merkle_branch.len(),
derived_control_block.size(),
serialized_control_block.len());
let derived_serialized_control_block = hex::encode(serialized_control_block);
assert!(
test_vector_control_blocks_set.contains(&derived_serialized_control_block),
"Control block mismatch: {}, expected: {:?}", derived_serialized_control_block, test_vector_control_blocks_set
);
debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block);
}
let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(derived_merkle_root.to_string());
assert_eq!(
p2mr_utxo_return.script_pubkey_hex,
*test_vector.expected.script_pubkey.as_ref().unwrap(),
"Script pubkey mismatch"
);
debug!("just passed script_pubkey validation. script_pubkey = {}", p2mr_utxo_return.script_pubkey_hex);
let bech32m_address: String = p2mr_utxo_return.bech32m_address;
debug!("derived bech32m address for bitcoin_network: {} : {}", p2mr_utxo_return.bitcoin_network, bech32m_address);
if p2mr_utxo_return.bitcoin_network == Network::Bitcoin {
assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch.");
}
Ok(())
}

View File

@@ -0,0 +1,102 @@
use log::info;
use bitcoin::blockdata::witness::Witness;
use p2mr_ref::{ pay_to_p2wpkh_tx, serialize_script };
use p2mr_ref::data_structures::{SpendDetails, LeafScriptType};
/* The rust-bitcoin crate does not provide a single high-level API that builds the full Taproot script-path witness stack for you.
It does expose all the necessary types and primitives to build it manually and correctly.
*/
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
#[test]
fn test_script_path_spend_simple() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let script_inputs_count = hex::decode("03").unwrap();
let script_inputs_bytes: Vec<u8> = hex::decode("08").unwrap();
let leaf_script_bytes: Vec<u8> = hex::decode("5887").unwrap();
let control_block_bytes: Vec<u8> =
hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
let test_witness_bytes: Vec<u8> = hex::decode(
"03010802588721c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329",
)
.unwrap();
let mut derived_witness: Witness = Witness::new();
derived_witness.push(script_inputs_count);
derived_witness.push(serialize_script(&script_inputs_bytes));
derived_witness.push(serialize_script(&leaf_script_bytes));
derived_witness.push(serialize_script(&control_block_bytes));
info!("witness: {:?}", derived_witness);
let derived_witness_vec: Vec<u8> = derived_witness.iter().flatten().cloned().collect();
assert_eq!(derived_witness_vec, test_witness_bytes);
}
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
// Spends from a p2mr UTXO to a p2wpk UTXO
#[test]
fn test_script_path_spend_signatures() {
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
let funding_tx_id_bytes: Vec<u8> =
hex::decode("d1c40446c65456a9b11a9dddede31ee34b8d3df83788d98f690225d2958bfe3c").unwrap();
// The input index of the funding tx
let funding_tx_index: u32 = 0;
let funding_utxo_amount_sats: u64 = 20000;
// OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
let input_leaf_script_bytes: Vec<u8> =
hex::decode("206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac").unwrap();
// Modified from learnmeabitcoin example
// Changed from c0 to c1 control byte to reflect p2mr specification: The parity bit of the control byte is always 1 since P2MR does not have a key-spend path.
let input_control_block_bytes: Vec<u8> =
hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
let input_script_pubkey_bytes: Vec<u8> =
hex::decode("5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80")
.unwrap();
let input_script_priv_key_bytes: Vec<u8> = hex::decode("9b8de5d7f20a8ebb026a82babac3aa47a008debbfde5348962b2c46520bd5189").unwrap();
// Convert to Vec<Vec<u8>> format expected by the function
let input_script_priv_keys_bytes: Vec<Vec<u8>> = vec![input_script_priv_key_bytes];
// https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
let spend_output_pubkey_bytes: Vec<u8> = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
let spend_output_amount_sats: u64 = 15000;
let test_sighash_bytes: Vec<u8> = hex::decode("752453d473e511a0da2097d664d69fe5eb89d8d9d00eab924b42fc0801a980c9").unwrap();
let test_signature_bytes: Vec<u8> = hex::decode("01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f").unwrap();
// Modified from learnmeabitcoin example
// Changed from c0 to c1 control byte to reflect p2mr specification: The parity bit of the control byte is always 1 since P2MR does not have a key-spend path.
let test_witness_bytes: Vec<u8> = hex::decode("01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f01206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0acc1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
let result: SpendDetails = pay_to_p2wpkh_tx(funding_tx_id_bytes,
funding_tx_index,
funding_utxo_amount_sats,
input_script_pubkey_bytes,
input_control_block_bytes,
input_leaf_script_bytes,
input_script_priv_keys_bytes, // Now passing Vec<Vec<u8>> format
spend_output_pubkey_bytes,
spend_output_amount_sats,
LeafScriptType::SchnorrOnly // This test uses a Schnorr signature
);
assert_eq!(result.sighash.as_slice(), test_sighash_bytes.as_slice(), "sighash mismatch");
assert_eq!(result.sig_bytes, test_signature_bytes, "signature mismatch");
assert_eq!(result.derived_witness_vec, test_witness_bytes, "derived_witness mismatch");
}