From 6b4798975478c0790d89ca275fcf32c6c5d9441b Mon Sep 17 00:00:00 2001
From: sucildoss
Date: Fri, 16 Jan 2026 22:10:02 +0530
Subject: [PATCH] feat: updating coordinate examples on js and rust
---
bip-0360/ref-impl/coordinate/js/.gitignore | 77 ++
bip-0360/ref-impl/coordinate/js/README.adoc | 26 +
bip-0360/ref-impl/coordinate/js/package.json | 23 +
.../coordinate/js/src/p2tsh-example.ts | 200 ++++
bip-0360/ref-impl/coordinate/js/tsconfig.json | 45 +
.../coordinate/rust/.cargo/config.toml | 2 +
bip-0360/ref-impl/coordinate/rust/.gitignore | 1 +
bip-0360/ref-impl/coordinate/rust/Cargo.lock | 902 ++++++++++++++++++
bip-0360/ref-impl/coordinate/rust/Cargo.toml | 42 +
bip-0360/ref-impl/coordinate/rust/README.md | 47 +
.../rust/docs/development_notes.adoc | 174 ++++
.../coordinate/rust/docs/p2tr-end-to-end.adoc | 236 +++++
.../rust/docs/p2tsh-end-to-end.adoc | 468 +++++++++
.../coordinate/rust/docs/p2tsh-workshop.adoc | 510 ++++++++++
.../rust/docs/quantum_root_tap_tree.txt | 36 +
.../stack_element_size_performance_tests.adoc | 358 +++++++
.../rust/examples/p2tr_construction.rs | 17 +
.../coordinate/rust/examples/p2tr_spend.rs | 129 +++
.../rust/examples/p2tsh-end-to-end.sh | 30 +
.../rust/examples/p2tsh_construction.rs | 21 +
.../coordinate/rust/examples/p2tsh_spend.rs | 248 +++++
.../rust/examples/schnorr_example.rs | 69 ++
.../examples/slh_dsa_verification_example.rs | 77 ++
.../rust/src/bin/slh_dsa_key_gen.rs | 33 +
.../coordinate/rust/src/data_structures.rs | 505 ++++++++++
.../ref-impl/coordinate/rust/src/error.rs | 11 +
bip-0360/ref-impl/coordinate/rust/src/lib.rs | 672 +++++++++++++
.../rust/tests/p2tsh_construction.rs | 240 +++++
.../rust/tests/p2tsh_pqc_construction.rs | 240 +++++
.../coordinate/rust/tests/p2tsh_spend.rs | 102 ++
30 files changed, 5541 insertions(+)
create mode 100644 bip-0360/ref-impl/coordinate/js/.gitignore
create mode 100644 bip-0360/ref-impl/coordinate/js/README.adoc
create mode 100644 bip-0360/ref-impl/coordinate/js/package.json
create mode 100644 bip-0360/ref-impl/coordinate/js/src/p2tsh-example.ts
create mode 100644 bip-0360/ref-impl/coordinate/js/tsconfig.json
create mode 100644 bip-0360/ref-impl/coordinate/rust/.cargo/config.toml
create mode 100644 bip-0360/ref-impl/coordinate/rust/.gitignore
create mode 100644 bip-0360/ref-impl/coordinate/rust/Cargo.lock
create mode 100644 bip-0360/ref-impl/coordinate/rust/Cargo.toml
create mode 100644 bip-0360/ref-impl/coordinate/rust/README.md
create mode 100644 bip-0360/ref-impl/coordinate/rust/docs/development_notes.adoc
create mode 100644 bip-0360/ref-impl/coordinate/rust/docs/p2tr-end-to-end.adoc
create mode 100644 bip-0360/ref-impl/coordinate/rust/docs/p2tsh-end-to-end.adoc
create mode 100644 bip-0360/ref-impl/coordinate/rust/docs/p2tsh-workshop.adoc
create mode 100644 bip-0360/ref-impl/coordinate/rust/docs/quantum_root_tap_tree.txt
create mode 100644 bip-0360/ref-impl/coordinate/rust/docs/stack_element_size_performance_tests.adoc
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/p2tr_construction.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/p2tr_spend.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/p2tsh-end-to-end.sh
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/p2tsh_construction.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/p2tsh_spend.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/schnorr_example.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/examples/slh_dsa_verification_example.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/src/bin/slh_dsa_key_gen.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/src/data_structures.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/src/error.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/src/lib.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/tests/p2tsh_construction.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/tests/p2tsh_pqc_construction.rs
create mode 100644 bip-0360/ref-impl/coordinate/rust/tests/p2tsh_spend.rs
diff --git a/bip-0360/ref-impl/coordinate/js/.gitignore b/bip-0360/ref-impl/coordinate/js/.gitignore
new file mode 100644
index 0000000000..e891f85aa6
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/js/.gitignore
@@ -0,0 +1,77 @@
+# Dependencies
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# Build outputs
+dist/
+build/
+*.tsbuildinfo
+
+# TypeScript
+*.js.map
+*.d.ts.map
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# IDE / Editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# OS files
+Thumbs.db
+desktop.ini
+
+# Testing
+coverage/
+.nyc_output/
+*.lcov
+
+# Logs
+logs/
+*.log
+
+# Temporary files
+*.tmp
+*.temp
+.cache/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# parcel-bundler cache
+.parcel-cache
+
+# Next.js (if ever used)
+.next
+out
+
+# Vercel (if ever used)
+.vercel
+
+# Turbo (if ever used)
+.turbo
+
+package-lock.json
\ No newline at end of file
diff --git a/bip-0360/ref-impl/coordinate/js/README.adoc b/bip-0360/ref-impl/coordinate/js/README.adoc
new file mode 100644
index 0000000000..186321ccee
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/js/README.adoc
@@ -0,0 +1,26 @@
+
+= BIP 360 Javascript Reference Implementation
+
+This implementation uses `coordinate-lib-js` from the Anduro Project, installed directly from GitHub:
+
+* `coordinate-lib-js`: Bitcoin library for the Coordinate sidechain (Anduro Network)
+* `@jbride/bitcoinpqc-wasm`: Post-quantum cryptography support
+* `ecpair`: Elliptic curve key pair management
+* `tiny-secp256k1`: Secp256k1 elliptic curve operations
+:numbered:
+
+== procedure
+
+-----
+# Verify Node.js version
+$ node --version # Should be v22.x.x or higher
+
+$ npm install
+
+# compile Typecript
+$ npx tsc
+
+
+# run tests
+$ node src/p2tsh-example.ts
+-----
diff --git a/bip-0360/ref-impl/coordinate/js/package.json b/bip-0360/ref-impl/coordinate/js/package.json
new file mode 100644
index 0000000000..82983837cf
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/js/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "js",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@types/node": "^24.10.0",
+ "typescript": "^5.9.3"
+ },
+ "dependencies": {
+ "coordinate-lib-js": "github:AnduroProject/coordinate-lib-js",
+ "@jbride/bitcoinpqc-wasm": "^0.1.1",
+ "ecpair": "^3.0.0",
+ "tiny-secp256k1": "^2.2.4"
+ }
+}
diff --git a/bip-0360/ref-impl/coordinate/js/src/p2tsh-example.ts b/bip-0360/ref-impl/coordinate/js/src/p2tsh-example.ts
new file mode 100644
index 0000000000..736ce2b701
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/js/src/p2tsh-example.ts
@@ -0,0 +1,200 @@
+// src/p2tsh-example.ts
+// Example demonstrating P2TSH (Pay-to-Taproot-Script-Hash) address construction
+
+import { payments } from 'coordinate-lib-js';
+import * as bitcoinCrypto from 'coordinate-lib-js/src/crypto';
+import * as bscript from 'coordinate-lib-js/src/script';
+import type { Taptree } from 'coordinate-lib-js/src/types';
+import ECPairFactory, { type ECPairInterface } from 'ecpair';
+import * as ecc from 'tiny-secp256k1';
+import { randomBytes } from 'crypto';
+
+const { p2tsh } = payments;
+
+// Initialize ECPair with the ECC library
+const ECPair = ECPairFactory(ecc);
+
+// Create a secure RNG function
+const rng = (size: number) => randomBytes(size);
+
+function signAndVerify(
+ keyPair: ECPairInterface,
+ xOnlyPubkey: Uint8Array,
+ message: Buffer,
+) {
+ const hash = Buffer.from(bitcoinCrypto.hash256(message));
+ const schnorrSignature = Buffer.from(keyPair.signSchnorr(hash));
+ const signatureWithSighashDefault = Buffer.concat([schnorrSignature, Buffer.from([0x00])]);
+ const verified = keyPair.verifySchnorr(hash, schnorrSignature);
+
+ return {
+ message,
+ hash,
+ signature: schnorrSignature,
+ signatureWithSighashDefault,
+ verified,
+ };
+}
+
+/**
+ * Example 1: Construct a P2TSH address from a script tree with a single leaf
+ * This is the simplest case - a script tree containing one script.
+ */
+function example1_simpleScriptTree() {
+ console.log('=== Example 1: P2TSH from simple script tree ===');
+
+ // Generate a key pair
+ const keyPair = ECPair.makeRandom({ rng });
+ const pubkey = keyPair.publicKey;
+ const xOnlyPubkey = ecc.xOnlyPointFromPoint(pubkey);
+
+ // Compile the script: x-only pubkey OP_CHECKSIG (BIP342 Schnorr signature)
+ const script = bscript.compile([Buffer.from(xOnlyPubkey), bscript.OPS.OP_CHECKSIG]);
+
+ // Create a script tree with one leaf
+ const scriptTree = {
+ output: script,
+ };
+
+ // Construct the P2TSH payment
+ const payment = p2tsh({
+ scriptTree: scriptTree,
+ });
+
+ console.log('Generated compressed pubkey:', pubkey.toString('hex'));
+ console.log('X-only pubkey:', Buffer.from(xOnlyPubkey).toString('hex'));
+ console.log('Script tree:', { output: bscript.toASM(script) });
+ console.log('P2TSH Address:', payment.address);
+ console.log('Output script:', bscript.toASM(payment.output!));
+ console.log('Merkle root hash:', payment.hash ? Buffer.from(payment.hash).toString('hex') : undefined);
+ const message = Buffer.from('P2TSH demo - example 1', 'utf8');
+ const result = signAndVerify(keyPair, xOnlyPubkey, message);
+
+ console.log('Message:', result.message.toString('utf8'));
+ console.log('Hash256(message):', result.hash.toString('hex'));
+ console.log('Schnorr signature (64-byte):', result.signature.toString('hex'));
+ console.log('Signature + default sighash (65-byte witness element):', result.signatureWithSighashDefault.toString('hex'));
+ console.log('Signature valid:', result.verified);
+ console.log('Witness stack for spend:', [result.signatureWithSighashDefault.toString('hex'), bscript.toASM(script)]);
+ console.log();
+}
+
+/**
+ * Example 2: Construct a P2TSH address from a script tree with multiple leaves
+ * This demonstrates a more complex script tree structure.
+ */
+function example2_multiLeafScriptTree() {
+ console.log('=== Example 2: P2TSH from multi-leaf script tree ===');
+
+ // Generate two different key pairs for the leaves
+ const keyPair1 = ECPair.makeRandom({ rng });
+ const keyPair2 = ECPair.makeRandom({ rng });
+ const pubkey1 = keyPair1.publicKey;
+ const pubkey2 = keyPair2.publicKey;
+ const xOnlyPubkey1 = ecc.xOnlyPointFromPoint(pubkey1);
+ const xOnlyPubkey2 = ecc.xOnlyPointFromPoint(pubkey2);
+
+ const script1 = bscript.compile([Buffer.from(xOnlyPubkey1), bscript.OPS.OP_CHECKSIG]);
+ const script2 = bscript.compile([Buffer.from(xOnlyPubkey2), bscript.OPS.OP_CHECKSIG]);
+
+ // Create a script tree with two leaves (array of two leaf objects)
+ const scriptTree: Taptree = [
+ { output: script1 },
+ { output: script2 },
+ ];
+
+ // Construct the P2TSH payment
+ const payment = p2tsh({
+ scriptTree: scriptTree,
+ });
+
+ console.log('Generated compressed public keys:');
+ console.log(' Pubkey 1:', pubkey1.toString('hex'));
+ console.log(' Pubkey 2:', pubkey2.toString('hex'));
+ console.log('X-only pubkeys:');
+ console.log(' X-only 1:', Buffer.from(xOnlyPubkey1).toString('hex'));
+ console.log(' X-only 2:', Buffer.from(xOnlyPubkey2).toString('hex'));
+ console.log('Script tree leaves:');
+ console.log(' Leaf 1:', bscript.toASM(script1));
+ console.log(' Leaf 2:', bscript.toASM(script2));
+ console.log('P2TSH Address:', payment.address);
+ console.log('Output script:', bscript.toASM(payment.output!));
+ console.log('Merkle root hash:', payment.hash ? Buffer.from(payment.hash).toString('hex') : undefined);
+ const message1 = Buffer.from('P2TSH demo - example 2 leaf 1', 'utf8');
+ const message2 = Buffer.from('P2TSH demo - example 2 leaf 2', 'utf8');
+ const result1 = signAndVerify(keyPair1, xOnlyPubkey1, message1);
+ const result2 = signAndVerify(keyPair2, xOnlyPubkey2, message2);
+
+ console.log('Leaf 1 signature info:');
+ console.log(' Message:', result1.message.toString('utf8'));
+ console.log(' Hash256(message):', result1.hash.toString('hex'));
+ console.log(' Schnorr signature (64-byte):', result1.signature.toString('hex'));
+ console.log(' Signature + default sighash (65-byte):', result1.signatureWithSighashDefault.toString('hex'));
+ console.log(' Signature valid:', result1.verified);
+ console.log(' Witness stack:', [result1.signatureWithSighashDefault.toString('hex'), bscript.toASM(script1)]);
+
+ console.log('Leaf 2 signature info:');
+ console.log(' Message:', result2.message.toString('utf8'));
+ console.log(' Hash256(message):', result2.hash.toString('hex'));
+ console.log(' Schnorr signature (64-byte):', result2.signature.toString('hex'));
+ console.log(' Signature + default sighash (65-byte):', result2.signatureWithSighashDefault.toString('hex'));
+ console.log(' Signature valid:', result2.verified);
+ console.log(' Witness stack:', [result2.signatureWithSighashDefault.toString('hex'), bscript.toASM(script2)]);
+ console.log();
+}
+
+/**
+ * Example 4: Construct a P2TSH address from a hash and redeem script
+ * This demonstrates creating a P2TSH when you have the hash directly.
+ */
+function example3_fromHashAndRedeem() {
+ console.log('=== Example 3: P2TSH from hash and redeem script ===');
+
+ // Generate a key pair
+ const keyPair = ECPair.makeRandom({ rng });
+ const pubkey = keyPair.publicKey;
+ const xOnlyPubkey = ecc.xOnlyPointFromPoint(pubkey);
+ const redeemScript = bscript.compile([Buffer.from(xOnlyPubkey), bscript.OPS.OP_CHECKSIG]);
+
+ // Use a known hash (from test fixtures)
+ const hash = Buffer.from(
+ 'b424dea09f840b932a00373cdcdbd25650b8c3acfe54a9f4a641a286721b8d26',
+ 'hex',
+ );
+
+ // Construct the P2TSH payment
+ const payment = p2tsh({
+ hash: hash,
+ redeem: {
+ output: redeemScript,
+ },
+ });
+
+ console.log('Generated compressed pubkey:', pubkey.toString('hex'));
+ console.log('X-only pubkey:', Buffer.from(xOnlyPubkey).toString('hex'));
+ console.log('Redeem script:', bscript.toASM(redeemScript));
+ console.log('Hash:', hash.toString('hex'));
+ console.log('P2TSH Address:', payment.address);
+ console.log('Output script:', bscript.toASM(payment.output!));
+ const message = Buffer.from('P2TSH demo - example 3', 'utf8');
+ const result = signAndVerify(keyPair, xOnlyPubkey, message);
+
+ console.log('Message:', result.message.toString('utf8'));
+ console.log('Hash256(message):', result.hash.toString('hex'));
+ console.log('Schnorr signature (64-byte):', result.signature.toString('hex'));
+ console.log('Signature + default sighash (65-byte):', result.signatureWithSighashDefault.toString('hex'));
+ console.log('Signature valid:', result.verified);
+ console.log('Witness stack:', [result.signatureWithSighashDefault.toString('hex'), bscript.toASM(redeemScript)]);
+ console.log();
+}
+
+// Run all examples
+console.log('P2TSH Address Construction Examples\n');
+console.log('=====================================\n');
+
+example1_simpleScriptTree();
+example2_multiLeafScriptTree();
+example3_fromHashAndRedeem();
+
+console.log('=====================================');
+console.log('All examples completed!');
diff --git a/bip-0360/ref-impl/coordinate/js/tsconfig.json b/bip-0360/ref-impl/coordinate/js/tsconfig.json
new file mode 100644
index 0000000000..b181afe130
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/js/tsconfig.json
@@ -0,0 +1,45 @@
+{
+ // Visit https://aka.ms/tsconfig to read more about this file
+ "include": [
+ "src/**/*"
+ ],
+ "compilerOptions": {
+ // File Layout
+ "rootDir": "./src",
+ "outDir": "./dist",
+
+ // Environment Settings
+ // See also https://aka.ms/tsconfig/module
+ "module": "nodenext",
+ "target": "esnext",
+ "types": ["node"],
+ // For nodejs:
+ // "lib": ["esnext"],
+
+ // Other Outputs
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": true,
+
+ // Stricter Typechecking Options
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+
+ // Style Options
+ // "noImplicitReturns": true,
+ // "noImplicitOverride": true,
+ // "noUnusedLocals": true,
+ // "noUnusedParameters": true,
+ // "noFallthroughCasesInSwitch": true,
+ // "noPropertyAccessFromIndexSignature": true,
+
+ // Recommended Options
+ "strict": true,
+ "jsx": "react-jsx",
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "noUncheckedSideEffectImports": true,
+ "moduleDetection": "force",
+ "skipLibCheck": true,
+ }
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/.cargo/config.toml b/bip-0360/ref-impl/coordinate/rust/.cargo/config.toml
new file mode 100644
index 0000000000..8bf4e414a4
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/.cargo/config.toml
@@ -0,0 +1,2 @@
+[registries.kellnr-denver-space]
+index = "sparse+https://crates.denver.space/api/v1/crates/"
\ No newline at end of file
diff --git a/bip-0360/ref-impl/coordinate/rust/.gitignore b/bip-0360/ref-impl/coordinate/rust/.gitignore
new file mode 100644
index 0000000000..2eea525d88
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/.gitignore
@@ -0,0 +1 @@
+.env
\ No newline at end of file
diff --git a/bip-0360/ref-impl/coordinate/rust/Cargo.lock b/bip-0360/ref-impl/coordinate/rust/Cargo.lock
new file mode 100644
index 0000000000..9988ecd401
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/Cargo.lock
@@ -0,0 +1,902 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+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.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+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 = "bdk_chain"
+version = "0.23.2-pqc-0.1"
+source = "sparse+https://crates.denver.space/api/v1/crates/"
+checksum = "6ea956ce13678893de26ffce4ead2f28ac1748ccb8058341bf839616cd357ccf"
+dependencies = [
+ "bdk_core",
+ "bitcoin",
+ "miniscript",
+ "serde",
+]
+
+[[package]]
+name = "bdk_core"
+version = "0.6.2"
+source = "sparse+https://crates.denver.space/api/v1/crates/"
+checksum = "a31fd2c38b90b16d97da383ff281f3f5ae636a7767cb067af6c0d90364c30fb6"
+dependencies = [
+ "bitcoin",
+ "hashbrown",
+ "serde",
+]
+
+[[package]]
+name = "bdk_wallet"
+version = "3.0.0-alpha.0-pqc-0.1"
+source = "sparse+https://crates.denver.space/api/v1/crates/"
+checksum = "e7687f76ee256115a2778e4b115bad9b439b87a8fccd52c4358c0f532482cf4c"
+dependencies = [
+ "bdk_chain",
+ "bitcoin",
+ "bitcoinpqc",
+ "miniscript",
+ "rand_core 0.6.4",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "bech32"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
+
+[[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"
+version = "0.32.6"
+source = "sparse+https://crates.denver.space/api/v1/crates/"
+checksum = "b1831b23596c0d9d0d3cb01b059027718add9748f4c1167455c748d632124379"
+dependencies = [
+ "base58ck",
+ "base64",
+ "bech32",
+ "bitcoin-internals",
+ "bitcoin-io",
+ "bitcoin-units",
+ "bitcoin_hashes",
+ "hex-conservative",
+ "hex_lit",
+ "secp256k1 0.29.1",
+ "serde",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
+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 = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[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.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
+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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[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.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "coordinate"
+version = "0.32.6"
+source = "git+https://github.com/anduroproject/rust-coordinate.git?rev=4985721#498572193a3e3d0f251ae8df080d7c1ae9aeb3bd"
+dependencies = [
+ "base58ck",
+ "base64",
+ "bech32",
+ "bitcoin-internals",
+ "bitcoin-io",
+ "bitcoin-units",
+ "bitcoin_hashes",
+ "hex-conservative",
+ "hex_lit",
+ "secp256k1 0.29.1",
+ "serde",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
+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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "serde",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "jiff"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.180"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
+
+[[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.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[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-pqc-0.2"
+source = "sparse+https://crates.denver.space/api/v1/crates/"
+checksum = "4cf5cd2ed677044868fdcfbbc28175d535f5086524674f85938c474258bc60aa"
+dependencies = [
+ "bech32",
+ "bitcoin",
+ "bitcoinpqc",
+ "serde",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "p2tsh-ref"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bdk_wallet",
+ "bitcoinpqc",
+ "coordinate",
+ "env_logger",
+ "hex",
+ "log",
+ "miniscript",
+ "once_cell",
+ "rand 0.9.2",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
+
+[[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.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
+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.5",
+]
+
+[[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.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[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.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[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.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[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.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[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.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
diff --git a/bip-0360/ref-impl/coordinate/rust/Cargo.toml b/bip-0360/ref-impl/coordinate/rust/Cargo.toml
new file mode 100644
index 0000000000..569e8b48e5
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/Cargo.toml
@@ -0,0 +1,42 @@
+[package]
+name = "p2tsh-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-pqc-0.2", registry="kellnr-denver-space" }
+coordinate = { git = "https://github.com/anduroproject/rust-coordinate.git", rev = "4985721", features = [
+"rand-std", "serde", "base64"
+]}
+bitcoinpqc = { version="0.2.0", features = ["serde"] }
+
+# BDK Wallet with P2TSH 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 = "p2tsh" }
+
+# 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" }
diff --git a/bip-0360/ref-impl/coordinate/rust/README.md b/bip-0360/ref-impl/coordinate/rust/README.md
new file mode 100644
index 0000000000..aadf1c321e
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/README.md
@@ -0,0 +1,47 @@
+
+# p2tsh 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-coordinate](https://github.com/anduroproject/rust-coordinate/tree/main) and [rust-miniscript](https://github.com/jbride/rust-miniscript/tree/p2tsh-pqc) customized with p2tsh functionality.
+
+
+1. environment variables
+ ```
+ // Specify Bitcoin network used when generating bip350 (bech32m) address
+ // Options: regtest, testnet, signet
+ // Default: mainnet
+ $ export COORDINATE_NETWORK=
+ ```
+
+1. run a specific test:
+ ```
+ $ cargo test test_p2tsh_single_leaf_script_tree -- --nocapture
+ ```
+
+## Local Development
+
+
+All P2TSH/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 P2TSH/PQC enabled dependencies used in your project, include a "registry" similar to the following:
+
+```bash
+coordinate = { git = "https://github.com/anduroproject/rust-coordinate.git", rev = "4985721", features = [
+"rand-std", "serde", "base64"
+]}
+```
+
+
diff --git a/bip-0360/ref-impl/coordinate/rust/docs/development_notes.adoc b/bip-0360/ref-impl/coordinate/rust/docs/development_notes.adoc
new file mode 100644
index 0000000000..10e81f406d
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/docs/development_notes.adoc
@@ -0,0 +1,174 @@
+
+== coordinate 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_P2TSH_STACK_ITEM_SIZE* (in policy.h) - This is a policy rule that limits P2TSH stack items to 80 bytes (or 8000 bytes with your change) for standardness.
+
+== P2TSH changes to rust-coordinate
+
+# 1. p2tsh module
+
+The p2tsh branch of rust-coordinate includes a new module: `p2tsh`.
+
+Source code for this new module can be found [here](https://github.com/AnduroProject/rust-coordinate/blob/main/coordinate/src/p2tsh/mod.rs).
+
+Highlights of this _p2tsh_ module as follows:
+
+## 1.1. p2tshBuilder
+
+This is struct inherits from the rust-coordinate _TaprootBuilder_.
+It has an important modification in that it disables keypath spend.
+
+Similar to its Taproot parent, p2tshBuilder provides functionality to add leaves to a TapTree.
+One its TapTree has been fully populated with all leaves, an instance of _p2tshSpendInfo_ can be retrieved from p2tshBuilder.
+
+
+```
+pub struct p2tshBuilder {
+ inner: TaprootBuilder
+}
+
+impl p2tshBuilder {
+
+ /// Creates a new p2tsh builder.
+ pub fn new() -> Self {
+ Self {
+ inner: TaprootBuilder::new()
+ }
+ }
+
+ /// Adds a leaf to the p2tsh builder.
+ pub fn add_leaf_with_ver(
+ self,
+ depth: u8,
+ script: ScriptBuf,
+ leaf_version: LeafVersion,
+ ) -> Result {
+ match self.inner.add_leaf_with_ver(depth, script, leaf_version) {
+ Ok(builder) => Ok(Self { inner: builder }),
+ Err(_) => Err(p2tshError::LeafAdditionError)
+ }
+ }
+
+ /// Finalizes the p2tsh builder.
+ pub fn finalize(self) -> Result {
+ let node_info: NodeInfo = self.inner.try_into_node_info().unwrap();
+ Ok(p2tshSpendInfo {
+ merkle_root: Some(node_info.node_hash()),
+ //script_map: self.inner.script_map().clone(),
+ })
+ }
+
+ /// Converts the p2tsh builder into a Taproot builder.
+ pub fn into_inner(self) -> TaprootBuilder {
+ self.inner
+ }
+}
+```
+
+## 1.2. p2tshSpendInfo
+
+Provides merkle_root of a completed p2tsh TapTree
+
+```
+/// A struct for p2tsh spend information.
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct p2tshSpendInfo {
+
+ /// The merkle root of the script path.
+ pub merkle_root: Option
+
+}
+```
+
+## 1.3. p2tshScriptBuf
+
+Allows for creation of a p2tsh scriptPubKey UTXO using only the merkle root of a script tree only.
+
+```
+/// A wrapper around ScriptBuf for p2tsh (Pay to Quantum Resistant Hash) scripts.
+pub struct p2tshScriptBuf {
+ inner: ScriptBuf
+}
+
+impl p2tshScriptBuf {
+ /// Creates a new p2tsh script from a ScriptBuf.
+ pub fn new(inner: ScriptBuf) -> Self {
+ Self { inner }
+ }
+
+ /// Generates p2tsh scriptPubKey output
+ /// Only accepts the merkle_root (of type TapNodeHash)
+ /// since keypath spend is disabled in p2tsh
+ pub fn new_p2tsh(merkle_root: TapNodeHash) -> Self {
+ // https://github.com/cryptoquick/bips/blob/p2tsh/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();
+ p2tshScriptBuf::new(script)
+ }
+
+ /// Returns the script as a reference.
+ pub fn as_script(&self) -> &Script {
+ self.inner.as_script()
+ }
+}
+```
+
+## 1.4. p2tsh Control Block
+
+Closely related to P2TR control block.
+Difference being that _internal public key_ is not included.
+
+
+```
+/// A control block for p2tsh (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 p2tshControlBlock {
+ /// The version of the leaf.
+ pub leaf_version: LeafVersion,
+ /// The merkle branch of the leaf.
+ pub merkle_branch: TaprootMerkleBranch,
+}
+```
+
+# 2. Witness Program
+
+New p2tsh related functions that allow for creation of a new V3 _witness program_ given a merkle_root only.
+
+Found in coordinate/src/blockdata/script/witness_program.rs
+
+```
+/// Creates a [`WitnessProgram`] from a 32 byte merkle root.
+fn new_p2tsh(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 p2tsh(merkle_root: Option) -> Self {
+ let merkle_root = merkle_root.unwrap();
+ WitnessProgram::new_p2tsh(merkle_root.to_byte_array())
+}
+```
+
+# 3. Address
+
+New _p2tsh_ function that allows for creation of a new _p2tsh_ Address given a merkle_root only.
+
+Found in coordinate/src/address/mod.rs
+
+```
+/// Creates a pay to quantum resistant hash address from a merkle root.
+pub fn p2tsh(merkle_root: Option, hrp: impl Into) -> Address {
+ let program = WitnessProgram::p2tsh(merkle_root);
+ Address::from_witness_program(program, hrp)
+}
+```
diff --git a/bip-0360/ref-impl/coordinate/rust/docs/p2tr-end-to-end.adoc b/bip-0360/ref-impl/coordinate/rust/docs/p2tr-end-to-end.adoc
new file mode 100644
index 0000000000..e137f58dfd
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/docs/p2tr-end-to-end.adoc
@@ -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/p2tsh-pqc[p2tsh branch] of bitcoin core is needed.
+
+Build instructions for the `p2tsh` 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 `p2tsh` 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_OF_INTEREST=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 COORDINATE_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
diff --git a/bip-0360/ref-impl/coordinate/rust/docs/p2tsh-end-to-end.adoc b/bip-0360/ref-impl/coordinate/rust/docs/p2tsh-end-to-end.adoc
new file mode 100644
index 0000000000..9b66a411be
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/docs/p2tsh-end-to-end.adoc
@@ -0,0 +1,468 @@
+:scrollbar:
+:data-uri:
+:toc2:
+:linkattrs:
+
+= P2TSH 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 P2TSH 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] `p2tsh` 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: <>.
+
+
+=== Shell Environment
+
+. *docker / podman*
++
+NOTE: If you have built the custom `p2tsh` enabled Bitcoin Core, you do not need docker (nor podman) installed. Skip this section.
++
+This tutorial makes use of a `p2tsh` 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 `p2tsh` 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 `p2tsh` enabled.
+For this purpose, a docker container with this `bitcoin-cli` utility is provided:
++
+-----
+$ docker pull quay.io/jbride2000/bitcoin-cli:p2tsh-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:p2tsh-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 P2TSH UTXO
+
+. Set an environment variable specific to your Bitcoin network environment (regtest, signet, etc)
++
+[source,bash]
+-----
+$ export COORDINATE_NETWORK=regtest
+-----
++
+Doing so influences the P2TSH address that you'll create later in this tutorial.
+
+
+. OPTIONAL: Indicate what type of cryptography to use in the locking scripts of your TapTree leaves.
+Valid options are: `SLH_DSA_ONLY`, `SCHNORR_ONLY`, `SCHNORR_AND_SLH_DSA`.
+Default is `SCHNORR_ONLY`.
++
+[source,bash]
+-----
+$ export LEAF_SCRIPT_TYPE=SLH_DSA_ONLY
+-----
+
+. OPTIONAL: Define number of leaves in tap tree as well as the tap leaf to later use as the unlocking script:
++
+[source,bash]
+-----
+$ export TOTAL_LEAF_COUNT=5 \
+ && export LEAF_OF_INTEREST=4
+-----
++
+NOTE: Defaults are 4 leaves with the first leaf (leaf 0 ) as the script to later use as the unlocking script.
+
+. Generate a P2TSH scripPubKey with multi-leaf taptree:
++
+[source,bash]
+-----
+$ export BITCOIN_ADDRESS_INFO=$( cargo run --example p2tsh_construction ) \
+ && echo $BITCOIN_ADDRESS_INFO | jq -r .
+-----
++
+NOTE: In `regtest`, you can expect a P2TSH address that starts with: `bcrt1z` .
++
+[subs=+quotes]
+++++
+
+What just happened?
+The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction with a `p2tsh` UTXO as follows:
+
+
+ - A configurable number of leaves are generated each with their own locking script.
+ - Each of these leaves are added to a Huffman tree that sorts the leaves by weight.
+ - The merkle root of the tree is calculated and subsequently used to generate the p2tsh witness program and BIP0350 address.
+
+
+The source code for the above logic is found in this project: src/lib.rs
+
+
+++++
+
+. 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 P2TSH_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 P2TSH 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 -regtest -named generatetoaddress nblocks=1 address="$P2TSH_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 $P2TSH_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 P2TSH address
++
+[source,bash]
+-----
+$ export P2TSH_DESC=$( b-cli getdescriptorinfo "addr($P2TSH_ADDR)" | jq -r '.descriptor' ) \
+ && echo $P2TSH_DESC \
+ && b-cli scantxoutset start '[{"desc": "'''$P2TSH_DESC'''"}]'
+-----
+
+. grab txid of first tx with unspent funds:
++
+[source,bash]
+-----
+$ export FUNDING_TX_ID=$( b-cli scantxoutset start '[{"desc": "'''$P2TSH_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 P2TSH 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 P2TSH UTXO
+
+In the previous section, you created and funded a P2TSH 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 P2TSH 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 P2TSH 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 P2TSH 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 p2tsh_spend )
+
+$ export RAW_P2TSH_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \
+ && echo "RAW_P2TSH_SPEND_TX = $RAW_P2TSH_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"
+-----
++
+[subs=+quotes]
+++++
+
+What just happened?
+The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction that spends from the `p2tsh` UTXO as follows:
+
+
+ - Create a transaction template (aka: SigHash) that serves as the message to be signed.
+ - Using the known private key and the SigHash, create a signature that is capable of unlocking one of the leaf scripts of the P2TSH tree.
+ - Add this signature to the witness section of the transaction.
+
+
+The source code for the above logic is found in this project: src/lib.rs
+
+
+++++
+
+. Inspect the spending tx:
++
+[source,bash]
+-----
+$ b-cli decoderawtransaction $RAW_P2TSH_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 p2tsh enabled Bitcoin Core:
++
+[source,bash]
+-----
+$ b-cli testmempoolaccept '["'''$RAW_P2TSH_SPEND_TX'''"]'
+-----
+
+. Submit tx:
++
+[source,bash]
+-----
+$ export P2TSH_SPENDING_TX_ID=$( b-cli sendrawtransaction $RAW_P2TSH_SPEND_TX ) \
+ && echo $P2TSH_SPENDING_TX_ID
+-----
++
+NOTE: Should return same tx id as was included in $RAW_P2TSH_SPEND_TX
+
+== Mine P2TSH Spend TX
+
+. View tx in mempool:
++
+[source,bash]
+-----
+$ b-cli getrawtransaction $P2TSH_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 $P2TSH_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 $P2TSH_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_p2tsh]]
+=== Build P2TSH / PQC Enabled Bitcoin Core
+
+The link:https://github.com/jbride/bitcoin/tree/p2tsh[p2tsh branch] of bitcoin core is needed.
+
+Build instructions for the `p2tsh` 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 `p2tsh` 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 `p2tsh-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 `p2tsh-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
+
+-----
+ OP_CHECKSIG OP_SUBSTR OP_BOOLAND OP_VERIFY
+-----
+
+
+The logic flow is:
+
+. OP_CHECKSIG: Verify Schnorr signature against Schnorr pubkey
+. 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
diff --git a/bip-0360/ref-impl/coordinate/rust/docs/p2tsh-workshop.adoc b/bip-0360/ref-impl/coordinate/rust/docs/p2tsh-workshop.adoc
new file mode 100644
index 0000000000..7ff10b867e
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/docs/p2tsh-workshop.adoc
@@ -0,0 +1,510 @@
+:scrollbar:
+:data-uri:
+:toc2:
+:linkattrs:
+
+= P2TSH End-to-End workshop
+
+:numbered:
+
+Welcome to the BIP-360 / _Pay-To-Tap-Script-Hash_ (P2TSH) workshop !
+
+In this workshop, you will interact with a custom Signet environment to create, fund and spend from a _P2TSH_ address.
+
+_P2TSH_ 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 P2TSH 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 P2TSH/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.
+
+=== *p2tsh_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/p2tsh_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: _p2tsh_demo-image.tar_.
+... Load the container image into your docker environment as per the following:
++
+[source,bash]
+-----
+docker load -i /path/to/p2tsh_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/p2tsh_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/p2tsh/bip-0360/ref-impl/rust[Rust based reference implementation] for BIP-360.
+
+==== Container contents
+Your docker environment already includes a P2TSH/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 _P2TSH/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 P2TSH enabled _mempool.space_ for the workshop at: link:http://signet.bip360.org[signet.bip360.org].
+
+
+== Create & Fund P2TSH 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] _P2TSH_ address (optionally using _Post-Quantum Cryptography_).
+
+In this section of the workshop, you create and fund a P2TSH address.
+
+The following depicts the construction of a P2TSH _TapTree_ and computation its _scriptPubKey_.
+
+image::images/p2tsh_construction.png[]
+
+A P2TSH 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.
+
+. OPTIONAL: In your container image, indicate what type of cryptography to use in the locking scripts of your TapTree leaves.
+Valid options are: `SLH_DSA_ONLY`, `SCHNORR_ONLY`, `SCHNORR_AND_SLH_DSA`.
+Default is `SCHNORR_ONLY`.
++
+[source,bash]
+-----
+$ export LEAF_SCRIPT_TYPE=SLH_DSA_ONLY
+-----
+
+.. If you set _LEAF_SCRIPT_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.
+
+.. If you set _LEAF_SCRIPT_TYPE=SLH_DSA_ONLY_, then the locking script of your TapTree leaves will utilize _SLH-DSA_ cryptography.
++
+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 _LEAF_SCRIPT_TYPE=SCHNORR_AND_SLH_DSA_, then the locking script of your TapTree leaves will be secured using both SCHNORR and SLH-DSA cryptography.
+
+. Define number of total leaves in tap tree :
++
+[source,bash]
+-----
+export TOTAL_LEAF_COUNT=5
+-----
+
+. Set the tap leaf index to later use as the unlocking script (when spending)
++
+[source,bash]
+-----
+export LEAF_OF_INTEREST=4
+-----
++
+NOTE: Defaults is 4 leaves with the first leaf (leaf 0 ) as the script to later use to unlock during spending.
+
+. Generate a P2TSH scripPubKey with multi-leaf taptree:
++
+[source,bash]
+-----
+export BITCOIN_ADDRESS_INFO=$( cargo run --example p2tsh_construction ) \
+ && echo $BITCOIN_ADDRESS_INFO | jq -r .
+-----
++
+NOTE: In signet, you can expect a P2TSH address that starts with the following prefix: `tb1z` .
++
+[subs=+quotes]
+++++
+
+What just happened?
+The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction with a P2TSH UTXO as follows:
+
+
+ - A configurable number of leaves are generated each with their own locking script.
+ - Each of these leaves are added to a Huffman tree that sorts the leaves by weight.
+ - The merkle root of the tree is calculated and subsequently used to generate the P2TSH witness program and BIP0350 address.
+
+
+
+
+++++
++
+The source code for the above logic is found in this project's source file: link:../src/lib.rs[src/lib.rs]
+
+. Fund this P2TSH 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 _P2TSH_ 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=
+-----
+
+.. 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 P2TSH 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 P2TSH_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 P2TSH address
++
+[source,bash]
+-----
+export P2TSH_DESC=$( b-cli getdescriptorinfo "addr($P2TSH_ADDR)" | jq -r '.descriptor' ) \
+ && echo $P2TSH_DESC \
+ && b-cli scantxoutset start '[{"desc": "'''$P2TSH_DESC'''"}]'
+-----
++
+NOTE: You will likely have to wait a few minutes until a new block (containing the tx that funds your P2TSH address) is mined.
+
+. grab txid of first tx with unspent funds:
++
+[source,bash]
+-----
+export FUNDING_TX_ID=$( b-cli scantxoutset start '[{"desc": "'''$P2TSH_DESC'''"}]' | jq -r '.unspents[0].txid' ) \
+ && echo $FUNDING_TX_ID
+-----
+
+. view details of funding UTXO to the P2TSH 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 P2TSH UTXO
+
+In the previous section, you created and funded a P2TSH 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 P2TSH 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 P2TSH UTXO.
+
+The target address type that you send funds to is: P2WPKH.
+
+
+. Determine value (in sats) of the funding P2TSH 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 p2tsh_spend )
+-----
++
+[subs=+quotes]
+++++
+
+What just happened?
+The Rust based reference implementation for BIP-0360 is leveraged to construct a transaction that spends from the P2TSH UTXO as follows:
+
+
+ - Create a transaction template (aka: SigHash) that serves as the message to be signed.
+ - Using the known private key and the SigHash, create a signature that is capable of unlocking one of the leaf scripts of the P2TSH tree.
+ - Add this signature to the witness section of the transaction.
+
+
+
+
+++++
++
+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_P2TSH_SPEND_TX=$( echo $SPEND_DETAILS | jq -r '.tx_hex' ) \
+ && echo "RAW_P2TSH_SPEND_TX = $RAW_P2TSH_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_P2TSH_SPEND_TX
+-----
++
+Pay particular attention to the `vin.txinwitness` field.
+The following depicts the elements of a P2TSH witness stack.
++
+image::images/p2tsh_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 P2TSH enabled Bitcoin Core:
++
+[source,bash]
+-----
+b-cli testmempoolaccept '["'''$RAW_P2TSH_SPEND_TX'''"]'
+-----
+
+. Submit tx:
++
+[source,bash]
+-----
+export P2TSH_SPENDING_TX_ID=$( b-cli sendrawtransaction $RAW_P2TSH_SPEND_TX ) \
+ && echo $P2TSH_SPENDING_TX_ID
+-----
++
+NOTE: Should return same tx id as was included in $RAW_P2TSH_SPEND_TX
+
+== Mine P2TSH Spend TX
+
+. View tx in mempool:
++
+[source,bash]
+-----
+b-cli getrawtransaction $P2TSH_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: $P2TSH_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 $P2TSH_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 $P2TSH_SPENDING_TX_ID) in the list.
++
+Congratulations!! You have created, funded and spent from a P2TSH address.
+
+== Appendix
+
+[[build_p2tsh]]
+=== Build P2TSH / PQC Enabled Bitcoin Core
+
+*FOR THE PURPOSE OF THE WORKSHOP, YOU CAN IGNORE THIS SECTION*
+
+The link:https://github.com/jbride/bitcoin/tree/p2tsh[p2tsh branch] of bitcoin core is needed.
+
+Build instructions for the `p2tsh` 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 `p2tsh` 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 workshop] provides a nice overview of the topic.
+
+=== libbitcoinpqc Rust bindings
+
+*FOR THE PURPOSE OF THE WORKSHOP, YOU CAN IGNORE THIS SECTION*
+
+The `p2tsh-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 `p2tsh-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
+-----
diff --git a/bip-0360/ref-impl/coordinate/rust/docs/quantum_root_tap_tree.txt b/bip-0360/ref-impl/coordinate/rust/docs/quantum_root_tap_tree.txt
new file mode 100644
index 0000000000..f032d13111
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/docs/quantum_root_tap_tree.txt
@@ -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
diff --git a/bip-0360/ref-impl/coordinate/rust/docs/stack_element_size_performance_tests.adoc b/bip-0360/ref-impl/coordinate/rust/docs/stack_element_size_performance_tests.adoc
new file mode 100644
index 0000000000..468760626a
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/docs/stack_element_size_performance_tests.adoc
@@ -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:
+
+-----
+ 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:
+
+-----
+ 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/p2tsh-pqc[p2tsh 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 )
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/p2tr_construction.rs b/bip-0360/ref-impl/coordinate/rust/examples/p2tr_construction.rs
new file mode 100644
index 0000000000..506fc5591a
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/p2tr_construction.rs
@@ -0,0 +1,17 @@
+use p2tsh_ref::{create_p2tr_utxo, create_p2tr_multi_leaf_taptree};
+use p2tsh_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,
+ };
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/p2tr_spend.rs b/bip-0360/ref-impl/coordinate/rust/examples/p2tr_spend.rs
new file mode 100644
index 0000000000..cf60ffd744
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/p2tr_spend.rs
@@ -0,0 +1,129 @@
+use p2tsh_ref::{ pay_to_p2wpkh_tx , verify_schnorr_signature_via_bytes};
+
+use p2tsh_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 = 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::()
+ .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::().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 = 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 = 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 = {
+ 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::>(&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> format expected by the function
+ let leaf_script_priv_keys_bytes: Vec> = vec![leaf_script_priv_key_bytes];
+
+ // ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
+ let leaf_script_bytes: Vec = 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 = 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::().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> 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 = 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;
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/p2tsh-end-to-end.sh b/bip-0360/ref-impl/coordinate/rust/examples/p2tsh-end-to-end.sh
new file mode 100644
index 0000000000..26273f40d2
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/p2tsh-end-to-end.sh
@@ -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_OF_INTEREST=4
+
+b-cli -named createwallet \
+ wallet_name=$W_NAME \
+ descriptors=true \
+ load_on_startup=true
+
+export BITCOIN_ADDRESS_INFO=$( cargo run --example p2tsh_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 P2TSH_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 $P2TSH_ADDR 5 | jq -r '.[]' ) \
+ && echo $COINBASE_REWARD_TX_ID
+
+export P2TSH_DESC=$( b-cli getdescriptorinfo "addr($P2TSH_ADDR)" | jq -r '.descriptor' ) \
+ && echo $P2TSH_DESC \
+ && b-cli scantxoutset start '[{"desc": "'''$P2TSH_DESC'''"}]'
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/p2tsh_construction.rs b/bip-0360/ref-impl/coordinate/rust/examples/p2tsh_construction.rs
new file mode 100644
index 0000000000..1130708903
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/p2tsh_construction.rs
@@ -0,0 +1,21 @@
+use p2tsh_ref::{create_p2tsh_utxo, create_p2tsh_multi_leaf_taptree, parse_leaf_script_type};
+use p2tsh_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 leaf_script_type = parse_leaf_script_type();
+ info!("leaf_script_type: {:?}", leaf_script_type);
+
+ let taptree_return: TaptreeReturn = create_p2tsh_multi_leaf_taptree();
+ let p2tsh_utxo_return: UtxoReturn = create_p2tsh_utxo(taptree_return.clone().tree_root_hex);
+
+ return ConstructionReturn {
+ taptree_return: taptree_return,
+ utxo_return: p2tsh_utxo_return,
+ };
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/p2tsh_spend.rs b/bip-0360/ref-impl/coordinate/rust/examples/p2tsh_spend.rs
new file mode 100644
index 0000000000..fc326f5c45
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/p2tsh_spend.rs
@@ -0,0 +1,248 @@
+use p2tsh_ref::{ pay_to_p2wpkh_tx, verify_schnorr_signature_via_bytes, verify_slh_dsa_via_bytes, parse_leaf_script_type };
+
+use p2tsh_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 = 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::()
+ .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::().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 = 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 = 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!("P2TSH control block size: {}", control_block_bytes.len());
+
+ // LEAF_SCRIPT_TYPE environment variable is required to determine key structure
+ let leaf_script_type: LeafScriptType = parse_leaf_script_type();
+ info!("leaf_script_type: {:?}", leaf_script_type);
+
+ // Parse private keys based on script type
+ let leaf_script_priv_keys_bytes: Vec> = match leaf_script_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::>(&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::>(&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::SchnorrAndSlhDsa => {
+ 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 = 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::NotApplicable => {
+ panic!("LeafScriptType::NotApplicable is not applicable");
+ }
+ };
+
+
+ // ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
+ let leaf_script_bytes: Vec = 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 = 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::().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> instead of Vec
+ spend_output_pubkey_hash_bytes,
+ spend_output_amount_sats,
+ leaf_script_type
+ );
+
+ // Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes
+ let tapleaf_pubkey_bytes: Vec = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec();
+
+ match leaf_script_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::SchnorrAndSlhDsa => {
+ // 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 OP_CHECKSIG OP_PUSHBYTES_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::NotApplicable => {
+ panic!("LeafScriptType::NotApplicable is not applicable");
+ }
+ }
+
+ return result;
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/schnorr_example.rs b/bip-0360/ref-impl/coordinate/rust/examples/schnorr_example.rs
new file mode 100644
index 0000000000..3dafef5313
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/schnorr_example.rs
@@ -0,0 +1,69 @@
+use std::env;
+use log::info;
+use once_cell::sync::Lazy;
+use coordinate::key::{Secp256k1};
+use coordinate::hashes::{sha256::Hash, Hash as HashTrait};
+use coordinate::secp256k1::{Message};
+
+use p2tsh_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> = 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: coordinate::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: coordinate::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);
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/examples/slh_dsa_verification_example.rs b/bip-0360/ref-impl/coordinate/rust/examples/slh_dsa_verification_example.rs
new file mode 100644
index 0000000000..2543cd3bbd
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/examples/slh_dsa_verification_example.rs
@@ -0,0 +1,77 @@
+use std::env;
+use log::info;
+use once_cell::sync::Lazy;
+use coordinate::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 {
+ let mut bytes = vec![0u8; size];
+ rng().fill_bytes(&mut bytes);
+ bytes
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/src/bin/slh_dsa_key_gen.rs b/bip-0360/ref-impl/coordinate/rust/src/bin/slh_dsa_key_gen.rs
new file mode 100644
index 0000000000..999d487d3d
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/src/bin/slh_dsa_key_gen.rs
@@ -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 {
+ let mut bytes = vec![0u8; size];
+ rng().fill_bytes(&mut bytes);
+ bytes
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/src/data_structures.rs b/bip-0360/ref-impl/coordinate/rust/src/data_structures.rs
new file mode 100644
index 0000000000..b6eac17b64
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/src/data_structures.rs
@@ -0,0 +1,505 @@
+use std::collections::HashMap;
+use serde::{Deserialize, Serialize};
+use log::debug;
+
+// Add imports for the unified keypair
+use coordinate::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)
+ SchnorrAndSlhDsa,
+ /// 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::SchnorrAndSlhDsa)
+ }
+
+ /// Check if this script type uses Schnorr
+ pub fn uses_schnorr(&self) -> bool {
+ matches!(self, LeafScriptType::SchnorrOnly | LeafScriptType::SchnorrAndSlhDsa)
+ }
+
+ /// Check if this script type requires both signature types
+ pub fn requires_both(&self) -> bool {
+ matches!(self, LeafScriptType::SchnorrAndSlhDsa)
+ }
+
+ /// Check if this script type is not applicable
+ pub fn is_not_applicable(&self) -> bool {
+ matches!(self, LeafScriptType::NotApplicable)
+ }
+}
+
+#[derive(Debug, Serialize)]
+pub struct TestVectors {
+ pub version: u32,
+ #[serde(rename = "test_vectors")]
+ pub test_vectors: Vec,
+ #[serde(skip, default = "HashMap::new")]
+ pub test_vector_map: HashMap,
+}
+
+impl<'de> Deserialize<'de> for TestVectors {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ #[derive(Deserialize)]
+ struct Helper {
+ version: u32,
+ #[serde(rename = "test_vectors")]
+ test_vectors: Vec,
+ }
+
+ 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 = "scriptTree")]
+ pub script_tree: Option,
+
+ #[serde(rename = "scriptInputs")]
+ pub script_inputs: Option>,
+ #[serde(rename = "scriptHex")]
+ pub script_hex: Option,
+ #[serde(rename = "controlBlock")]
+ pub control_block: Option,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct TestVectorIntermediary {
+
+ #[serde(default)]
+ #[serde(rename = "leafHashes")]
+ pub leaf_hashes: Vec,
+ #[serde(rename = "merkleRoot")]
+ pub merkle_root: Option
+}
+
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct TestVectorExpected {
+ #[serde(rename = "scriptPubKey")]
+ pub script_pubkey: Option,
+ #[serde(rename = "bip350Address")]
+ pub bip350_address: Option,
+ #[serde(default)]
+ #[serde(rename = "scriptPathControlBlocks")]
+ pub script_path_control_blocks: Option>,
+ #[serde(rename = "error")]
+ pub error: Option,
+ #[serde(rename = "address")]
+ pub address: Option,
+ #[serde(default)]
+ pub witness: Option
+}
+
+#[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,
+ right: Box,
+ },
+}
+
+// Add custom deserialize implementation
+impl<'de> Deserialize<'de> for TVScriptTree {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ #[derive(Deserialize)]
+ #[serde(untagged)]
+ enum Helper {
+ Leaf(TVScriptLeaf),
+ Branch(Vec),
+ }
+
+ 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(&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(&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,
+ pub branch_hashes: HashMap,
+}
+
+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(bytes: &Vec, s: S) -> Result
+where
+ S: serde::Serializer,
+{
+ s.serialize_str(&hex::encode(bytes))
+}
+
+fn deserialize_hex<'de, D>(d: D) -> Result, 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,
+ #[serde(serialize_with = "serialize_hex")]
+ #[serde(deserialize_with = "deserialize_hex")]
+ pub sig_bytes: Vec,
+ #[serde(serialize_with = "serialize_hex")]
+ #[serde(deserialize_with = "deserialize_hex")]
+ pub derived_witness_vec: Vec,
+}
+
+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: coordinate::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, // Changed to support multiple private keys
+ pub leaf_script_hex: String,
+ pub tree_root_hex: String,
+ pub control_block_hex: 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,
+ pub slh_dsa_keypair: Option,
+}
+
+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> {
+ 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> {
+ 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()
+ }
+}
+
+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 {
+ 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 {
+ 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,
+ }
+ }
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/src/error.rs b/bip-0360/ref-impl/coordinate/rust/src/error.rs
new file mode 100644
index 0000000000..100a48b7da
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/src/error.rs
@@ -0,0 +1,11 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum P2TSHError {
+ #[error("P2TSH 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),
+}
\ No newline at end of file
diff --git a/bip-0360/ref-impl/coordinate/rust/src/lib.rs b/bip-0360/ref-impl/coordinate/rust/src/lib.rs
new file mode 100644
index 0000000000..df2cfc003b
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/src/lib.rs
@@ -0,0 +1,672 @@
+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 coordinate::hashes::{sha256, Hash};
+use coordinate::key::{Secp256k1, Parity};
+use coordinate::secp256k1::{Message, SecretKey, Keypair, rand::rngs::OsRng, rand::thread_rng, rand::Rng, schnorr::Signature};
+use coordinate::{ 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}, Txid
+};
+
+use coordinate::p2tsh::{P2tshScriptBuf, P2tshBuilder, P2tshSpendInfo, P2tshControlBlock, P2TSH_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};
+
+/* 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> = Lazy::new(Secp256k1::new);
+
+fn create_huffman_tree(leaf_script_type: LeafScriptType) -> (Vec<(u32, ScriptBuf)>, MultiKeypair, ScriptBuf) {
+
+ 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::() {
+ total_leaf_count = parsed_value;
+ }
+ }
+
+ let mut leaf_of_interest: u32 = 0;
+ if let Ok(env_value) = env::var("LEAF_OF_INTEREST") {
+ if let Ok(parsed_value) = env_value.parse::() {
+ leaf_of_interest = parsed_value;
+ }
+ }
+
+ if total_leaf_count < 1 {
+ panic!("total_leaf_count must be greater than 0");
+ }
+ if leaf_of_interest >= total_leaf_count {
+ panic!("leaf_of_interest must be less than total_leaf_count and greater than 0");
+ }
+
+ debug!("Creating multi-leaf taptree with total_leaf_count: {}, leaf_of_interest: {}", total_leaf_count, leaf_of_interest);
+ let mut huffman_entries: Vec<(u32, ScriptBuf)> = vec![];
+ let mut keypairs_of_interest: Option = None;
+ let mut script_buf_of_interest: Option = None;
+ for leaf_index in 0..total_leaf_count {
+ let keypairs: MultiKeypair;
+ let script_buf: ScriptBuf;
+
+ match leaf_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::SchnorrAndSlhDsa => {
+ // 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: OP_CHECKSIG 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::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_of_interest {
+ keypairs_of_interest = Some(keypairs);
+ script_buf_of_interest = Some(script_buf.clone());
+ debug!("Selected leaf: weight: {}, script: {:?}", random_weight, script_buf);
+ }
+ }
+ return (huffman_entries, keypairs_of_interest.unwrap(), script_buf_of_interest.unwrap());
+}
+
+/// Parses the LEAF_SCRIPT_TYPE environment variable and returns the corresponding LeafScriptType.
+/// Defaults to LeafScriptType::SchnorrOnly if the environment variable is not set or has an invalid value.
+pub fn parse_leaf_script_type() -> LeafScriptType {
+ match env::var("LEAF_SCRIPT_TYPE")
+ .unwrap_or_else(|_| "SCHNORR_ONLY".to_string())
+ .as_str() {
+ "SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly,
+ "SCHNORR_ONLY" => LeafScriptType::SchnorrOnly,
+ "SCHNORR_AND_SLH_DSA" => LeafScriptType::SchnorrAndSlhDsa,
+ _ => {
+ error!("Invalid LEAF_SCRIPT_TYPE. Must be one of: SLH_DSA_ONLY, SCHNORR_ONLY, SCHNORR_AND_SLH_DSA");
+ LeafScriptType::SchnorrOnly
+ }
+ }
+}
+
+pub fn create_p2tsh_multi_leaf_taptree() -> TaptreeReturn {
+ let leaf_script_type = parse_leaf_script_type();
+
+ let (huffman_entries, keypairs_of_interest, script_buf_of_interest) = create_huffman_tree(leaf_script_type);
+ let p2tsh_builder: P2tshBuilder = P2tshBuilder::with_huffman_tree(huffman_entries).unwrap();
+
+
+ let p2tsh_spend_info: P2tshSpendInfo = p2tsh_builder.clone().finalize().unwrap();
+ let merkle_root:TapNodeHash = p2tsh_spend_info.merkle_root.unwrap();
+
+
+ let tap_tree: TapTree = p2tsh_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 = p2tsh_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(P2TSH_LEAF_VERSION).unwrap());
+
+ // Convert leaf hash to big-endian for display (like Coordinate 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: P2tshControlBlock = P2tshControlBlock{
+ merkle_branch: merkle_branch.clone(),
+ };
+
+ // Not a requirement here but useful to demonstrate what Coordinate Core does as the verifier when spending from a p2tsh 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,
+ };
+}
+
+pub fn create_p2tr_multi_leaf_taptree(p2tr_internal_pubkey_hex: String) -> TaptreeReturn {
+
+ let (huffman_entries, keypairs_of_interest, script_buf_of_interest) = create_huffman_tree(LeafScriptType::SchnorrOnly);
+
+ let pub_key_string = format!("02{}", p2tr_internal_pubkey_hex);
+ let internal_pubkey: PublicKey = pub_key_string.parse::().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::>(), // secret_bytes returns big endian
+ keypairs_of_interest.public_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::>(), // 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,
+ };
+}
+
+/// Parses the COORDINATE_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_coordinate_network() -> Network {
+ let mut coordinate_network: Network = Network::Regtest;
+
+ // Check for COORDINATE_NETWORK environment variable and override if set
+ if let Ok(network_str) = std::env::var("COORDINATE_NETWORK") {
+ coordinate_network = match network_str.to_lowercase().as_str() {
+ "regtest" => Network::Regtest,
+ "testnet" => Network::Testnet,
+ "signet" => Network::Signet,
+ _ => {
+ debug!("Invalid COORDINATE_NETWORK value '{}', using default Regtest network", network_str);
+ Network::Regtest
+ }
+ };
+ }
+
+ coordinate_network
+}
+
+pub fn create_p2tsh_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 P2TSH address (ie: scriptPubKey)
+ */
+ let script_buf: P2tshScriptBuf = P2tshScriptBuf::new_p2tsh(merkle_root);
+ let script: &Script = script_buf.as_script();
+ let script_pubkey = script.to_hex_string();
+
+ let bitcoin_network = get_coordinate_network();
+
+ // derive bech32m address and verify against test vector
+ // p2tsh address is comprised of network HRP + WitnessProgram (version + program)
+ let bech32m_address = Address::p2tsh(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 p2tsh UTXO details, spend to p2wpkh
+pub fn pay_to_p2wpkh_tx(
+ funding_tx_id_bytes: Vec,
+ funding_utxo_index: u32,
+ funding_utxo_amount_sats: u64,
+ funding_script_pubkey_bytes: Vec,
+ control_block_bytes: Vec,
+ leaf_script_bytes: Vec,
+ leaf_script_priv_keys_bytes: Vec>, // Changed to support multiple private keys
+ spend_output_pubkey_hash_bytes: Vec,
+ 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 = coordinate::TxIn {
+ previous_output: OutPoint {
+ txid: coordinate::Txid::from_slice(&txid_little_endian).unwrap(), // coordinate::Txid expects the bytes in little-endian format
+ vout: funding_utxo_index,
+ asset_id: vec![]
+ },
+ 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: coordinate::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: coordinate::transaction::Version::TWO,
+ assettype: 0,
+ precision: 0,
+ headline: vec![],
+ ticker: vec![],
+ payload: Txid::all_zeros(),
+ payloaddata: vec![],
+ lock_time: coordinate::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: coordinate::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::SchnorrAndSlhDsa => {
+ 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: coordinate::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::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 = 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 = coordinate::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::().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_coordinate_network();
+
+ // 4) derive bech32m address and verify against test vector
+ // p2tsh 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) -> Vec {
+ // 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 containing the encoded bytes
+fn compact_size(n: u64) -> Vec {
+ 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 = coordinate::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 = coordinate::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 {
+ let mut bytes = vec![0u8; size];
+ rng().fill_bytes(&mut bytes);
+ bytes
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_construction.rs b/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_construction.rs
new file mode 100644
index 0000000000..b18ab776fa
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_construction.rs
@@ -0,0 +1,240 @@
+use std::collections::HashSet;
+use coordinate::{Network, ScriptBuf};
+use coordinate::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash};
+use coordinate::p2tsh::{P2tshBuilder, P2tshControlBlock, P2tshSpendInfo};
+use coordinate::hashes::Hash;
+
+use hex;
+use log::debug;
+use once_cell::sync::Lazy;
+
+use p2tsh_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn};
+use p2tsh_ref::error::P2TSHError;
+use p2tsh_ref::{create_p2tsh_utxo, tagged_hash};
+
+// This file contains tests that execute against the BIP360 script-path-only test vectors.
+
+static TEST_VECTORS: Lazy = Lazy::new(|| {
+ let bip360_test_vectors = include_str!("../../../common/tests/data/p2tsh_construction.json");
+ let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap();
+ assert_eq!(test_vectors.version, 1);
+ test_vectors
+});
+
+static P2TSH_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2tsh_missing_leaf_script_tree_error";
+static P2TSH_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2tsh_single_leaf_script_tree";
+static P2TSH_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2tsh_different_version_leaves";
+static P2TSH_TWO_LEAF_SAME_VERSION_TEST: &str = "p2tsh_two_leaf_same_version";
+static P2TSH_THREE_LEAF_COMPLEX_TEST: &str = "p2tsh_three_leaf_complex";
+static P2TSH_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2tsh_three_leaf_alternative";
+static P2TSH_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2tsh_simple_lightning_contract";
+
+// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
+#[test]
+fn test_p2tsh_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(P2TSH_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap();
+ let test_result: anyhow::Result<()> = process_test_vector_p2tsh(test_vector);
+ assert!(matches!(test_result.unwrap_err().downcast_ref::(),
+ Some(P2TSHError::MissingScriptTreeLeaf)));
+}
+
+// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
+#[test]
+fn test_p2tsh_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(P2TSH_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_DIFFERENT_VERSION_LEAVES_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_TWO_LEAF_SAME_VERSION_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_THREE_LEAF_COMPLEX_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_THREE_LEAF_ALTERNATIVE_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+fn process_test_vector_p2tsh(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 p2tsh_builder: P2tshBuilder = P2tshBuilder::new();
+
+ let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new();
+
+ // 1) traverse test vector script tree and add leaves to P2TSH 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)
+ p2tsh_builder = p2tsh_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(P2TSHError::MissingScriptTreeLeaf.into());
+ }
+
+ let spend_info: P2tshSpendInfo = p2tsh_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 = test_vector.intermediary.leaf_hashes.clone();
+ let test_vector_leaf_hash_set: HashSet = 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 = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect();
+ let tap_tree: TapTree = p2tsh_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: P2tshControlBlock = P2tshControlBlock{
+ 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 p2tsh_utxo_return: UtxoReturn = create_p2tsh_utxo(derived_merkle_root.to_string());
+
+ assert_eq!(
+ p2tsh_utxo_return.script_pubkey_hex,
+ *test_vector.expected.script_pubkey.as_ref().unwrap(),
+ "Script pubkey mismatch"
+ );
+ debug!("just passed script_pubkey validation. script_pubkey = {}", p2tsh_utxo_return.script_pubkey_hex);
+
+ let bech32m_address: String = p2tsh_utxo_return.bech32m_address;
+ debug!("derived bech32m address for coordinate_network: {} : {}", p2tsh_utxo_return.bitcoin_network, bech32m_address);
+
+ if p2tsh_utxo_return.bitcoin_network == Network::Bitcoin {
+ assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch.");
+ }
+
+ Ok(())
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_pqc_construction.rs b/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_pqc_construction.rs
new file mode 100644
index 0000000000..5cc00a3eff
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_pqc_construction.rs
@@ -0,0 +1,240 @@
+use std::collections::HashSet;
+use coordinate::{Network, ScriptBuf};
+use coordinate::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash};
+use coordinate::p2tsh::{P2tshBuilder, P2tshControlBlock, P2tshSpendInfo};
+use coordinate::hashes::Hash;
+
+use hex;
+use log::debug;
+use once_cell::sync::Lazy;
+
+use p2tsh_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn};
+use p2tsh_ref::error::P2TSHError;
+use p2tsh_ref::{create_p2tsh_utxo, tagged_hash};
+
+// This file contains tests that execute against the BIP360 script-path-only test vectors.
+
+static TEST_VECTORS: Lazy = Lazy::new(|| {
+ let bip360_test_vectors = include_str!("../../../common/tests/data/p2tsh_pqc_construction.json");
+ let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap();
+ assert_eq!(test_vectors.version, 1);
+ test_vectors
+});
+
+static P2TSH_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2tsh_missing_leaf_script_tree_error";
+static P2TSH_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2tsh_single_leaf_script_tree";
+static P2TSH_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2tsh_different_version_leaves";
+static P2TSH_TWO_LEAF_SAME_VERSION_TEST: &str = "p2tsh_two_leaf_same_version";
+static P2TSH_THREE_LEAF_COMPLEX_TEST: &str = "p2tsh_three_leaf_complex";
+static P2TSH_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2tsh_three_leaf_alternative";
+static P2TSH_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2tsh_simple_lightning_contract";
+
+// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
+#[test]
+fn test_p2tsh_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(P2TSH_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap();
+ let test_result: anyhow::Result<()> = process_test_vector_p2tsh(test_vector);
+ assert!(matches!(test_result.unwrap_err().downcast_ref::(),
+ Some(P2TSHError::MissingScriptTreeLeaf)));
+}
+
+// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
+#[test]
+fn test_p2tsh_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(P2TSH_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_DIFFERENT_VERSION_LEAVES_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_TWO_LEAF_SAME_VERSION_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_THREE_LEAF_COMPLEX_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+#[test]
+fn test_p2tsh_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(P2TSH_THREE_LEAF_ALTERNATIVE_TEST).unwrap();
+ process_test_vector_p2tsh(test_vector).unwrap();
+}
+
+fn process_test_vector_p2tsh(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 p2tsh_builder: P2tshBuilder = P2tshBuilder::new();
+
+ let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new();
+
+ // 1) traverse test vector script tree and add leaves to P2TSH 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)
+ p2tsh_builder = p2tsh_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(P2TSHError::MissingScriptTreeLeaf.into());
+ }
+
+ let spend_info: P2tshSpendInfo = p2tsh_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 = test_vector.intermediary.leaf_hashes.clone();
+ let test_vector_leaf_hash_set: HashSet = 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 = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect();
+ let tap_tree: TapTree = p2tsh_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: P2tshControlBlock = P2tshControlBlock{
+ 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 p2tsh_utxo_return: UtxoReturn = create_p2tsh_utxo(derived_merkle_root.to_string());
+
+ assert_eq!(
+ p2tsh_utxo_return.script_pubkey_hex,
+ *test_vector.expected.script_pubkey.as_ref().unwrap(),
+ "Script pubkey mismatch"
+ );
+ debug!("just passed script_pubkey validation. script_pubkey = {}", p2tsh_utxo_return.script_pubkey_hex);
+
+ let bech32m_address: String = p2tsh_utxo_return.bech32m_address;
+ debug!("derived bech32m address for coordinate_network: {} : {}", p2tsh_utxo_return.bitcoin_network, bech32m_address);
+
+ if p2tsh_utxo_return.bitcoin_network == Network::Bitcoin {
+ assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch.");
+ }
+
+ Ok(())
+}
diff --git a/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_spend.rs b/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_spend.rs
new file mode 100644
index 0000000000..99bf85d7e7
--- /dev/null
+++ b/bip-0360/ref-impl/coordinate/rust/tests/p2tsh_spend.rs
@@ -0,0 +1,102 @@
+use log::info;
+use coordinate::blockdata::witness::Witness;
+
+use p2tsh_ref::{ pay_to_p2wpkh_tx, serialize_script };
+
+use p2tsh_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 = hex::decode("08").unwrap();
+ let leaf_script_bytes: Vec = hex::decode("5887").unwrap();
+ let control_block_bytes: Vec =
+ hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
+ let test_witness_bytes: Vec = 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 = 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 p2tsh 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 =
+ 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 =
+ hex::decode("206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac").unwrap();
+
+ // Modified from learnmeabitcoin example
+ // Changed from c0 to c1 control byte to reflect p2tsh specification: The parity bit of the control byte is always 1 since P2TSH does not have a key-spend path.
+ let input_control_block_bytes: Vec =
+ hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
+
+ let input_script_pubkey_bytes: Vec =
+ hex::decode("5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80")
+ .unwrap();
+ let input_script_priv_key_bytes: Vec = hex::decode("9b8de5d7f20a8ebb026a82babac3aa47a008debbfde5348962b2c46520bd5189").unwrap();
+
+ // Convert to Vec> format expected by the function
+ let input_script_priv_keys_bytes: Vec> = vec![input_script_priv_key_bytes];
+
+
+ // https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
+ let spend_output_pubkey_bytes: Vec = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
+
+ let spend_output_amount_sats: u64 = 15000;
+
+ let test_sighash_bytes: Vec = hex::decode("376a8f9d4ace62c3971363e4612b44328b325040a35ee30dd6f2454a0a017660").unwrap();
+ let test_signature_bytes: Vec = hex::decode("9213ffb0a192a50379eb91c11a915af9ed9ef2af7a2a9aa150bf6a73b9fa840311d38911d8fadc3999aac21c68d974ee173521a954fab7cca983b973574c279b").unwrap();
+
+ // Modified from learnmeabitcoin example
+ // Changed from c0 to c1 control byte to reflect p2tsh specification: The parity bit of the control byte is always 1 since P2TSH does not have a key-spend path.
+ let test_witness_bytes: Vec = hex::decode("9213ffb0a192a50379eb91c11a915af9ed9ef2af7a2a9aa150bf6a73b9fa840311d38911d8fadc3999aac21c68d974ee173521a954fab7cca983b973574c279b01206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0acc1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").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> 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");
+
+}
+