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"); + +} +