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

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

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

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

View File

@@ -0,0 +1,200 @@
// src/p2mr-example.ts
// Example demonstrating P2MR (Pay-to-Taproot-Script-Hash) address construction
import { payments } from '@jbride/bitcoinjs-lib';
import * as bitcoinCrypto from '@jbride/bitcoinjs-lib/src/crypto';
import * as bscript from '@jbride/bitcoinjs-lib/src/script';
import type { Taptree } from '@jbride/bitcoinjs-lib/src/types';
import ECPairFactory, { type ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import { randomBytes } from 'crypto';
const { p2mr } = 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 P2MR 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: P2MR 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 P2MR payment
const payment = p2mr({
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('P2MR 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('P2MR 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 P2MR address from a script tree with multiple leaves
* This demonstrates a more complex script tree structure.
*/
function example2_multiLeafScriptTree() {
console.log('=== Example 2: P2MR 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 P2MR payment
const payment = p2mr({
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('P2MR 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('P2MR demo - example 2 leaf 1', 'utf8');
const message2 = Buffer.from('P2MR 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 P2MR address from a hash and redeem script
* This demonstrates creating a P2MR when you have the hash directly.
*/
function example3_fromHashAndRedeem() {
console.log('=== Example 3: P2MR 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 P2MR payment
const payment = p2mr({
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('P2MR Address:', payment.address);
console.log('Output script:', bscript.toASM(payment.output!));
const message = Buffer.from('P2MR 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('P2MR Address Construction Examples\n');
console.log('=====================================\n');
example1_simpleScriptTree();
example2_multiLeafScriptTree();
example3_fromHashAndRedeem();
console.log('=====================================');
console.log('All examples completed!');

View File

@@ -0,0 +1,197 @@
#!/usr/bin/env node
/**
* Node.js test script for Bitcoin PQC WASM module (High-Level API)
*
* Usage: node test-npm-package.js
*
* This script tests the high-level TypeScript wrapper API (index.js) from the
* command line, which provides a cleaner interface than the low-level API.
*/
import { randomBytes } from 'node:crypto';
// Load the high-level WASM module
let bitcoinpqc;
let Algorithm;
try {
const module = await import('@jbride/bitcoinpqc-wasm');
bitcoinpqc = module.bitcoinpqc || module.default;
Algorithm = module.Algorithm;
if (!bitcoinpqc || !Algorithm) {
throw new Error('Failed to import bitcoinpqc or Algorithm from @jbride/bitcoinpqc-wasm');
}
} catch (error) {
console.error('Failed to load WASM module:', error);
console.error('Make sure you have installed the @jbride/bitcoinpqc-wasm package before running this test.');
process.exit(1);
}
// Helper function to generate random bytes
function generateRandomBytes(length) {
const array = new Uint8Array(length);
const bytes = randomBytes(length);
array.set(bytes);
return array;
}
// Test function
async function testAlgorithm(algorithm, name) {
console.log(`\nTesting ${name} algorithm:`);
console.log('------------------------');
try {
// Get key and signature sizes
const pkSize = bitcoinpqc.publicKeySize(algorithm);
const skSize = bitcoinpqc.secretKeySize(algorithm);
const sigSize = bitcoinpqc.signatureSize(algorithm);
console.log(`Public key size: ${pkSize} bytes`);
console.log(`Secret key size: ${skSize} bytes`);
console.log(`Signature size: ${sigSize} bytes`);
// Generate random data for key generation
const randomData = generateRandomBytes(128);
// Generate a key pair
const keygenStart = Date.now();
const keypair = bitcoinpqc.generateKeypair(algorithm, randomData);
const keygenDuration = Date.now() - keygenStart;
console.log(`Key generation time: ${keygenDuration} ms`);
// Create a message to sign
const messageText = 'This is a test message for PQC signature verification';
const message = Buffer.from(messageText, 'utf8');
const messageUint8 = new Uint8Array(message);
console.log(`Message to sign: "${messageText}"`);
console.log(`Message length: ${message.length} bytes`);
// Sign the message
const signStart = Date.now();
let signature;
try {
signature = bitcoinpqc.sign(keypair.secretKey, messageUint8, algorithm);
const signDuration = Date.now() - signStart;
console.log(`Signing time: ${signDuration} ms`);
console.log(`Actual signature size: ${signature.size} bytes`);
} catch (error) {
const signDuration = Date.now() - signStart;
console.log(`Signing failed after ${signDuration} ms`);
console.log(`Error: ${error.message}`);
if (algorithm === Algorithm.SLH_DSA_SHAKE_128S) {
console.log('');
console.log('⚠️ NOTE: SLH-DSA-SHAKE-128s signing is currently experiencing');
console.log(' issues when compiled to WebAssembly. This appears to be a');
console.log(' bug in the SPHINCS+ reference implementation when compiled');
console.log(' to WASM. ML-DSA-44 (Dilithium) works correctly.');
console.log('');
console.log(' Key generation succeeded, but signing failed.');
console.log(' This is a known limitation of the browser/WASM build.');
}
throw error;
}
// Verify the signature
const verifyStart = Date.now();
const verifyResult = bitcoinpqc.verify(
keypair.publicKey,
messageUint8,
signature,
algorithm
);
const verifyDuration = Date.now() - verifyStart;
if (verifyResult) {
console.log('Signature verified successfully!');
} else {
console.log('ERROR: Signature verification failed!');
}
console.log(`Verification time: ${verifyDuration} ms`);
// Try to verify with a modified message
const modifiedMessageText = 'This is a MODIFIED message for PQC signature verification';
const modifiedMessage = Buffer.from(modifiedMessageText, 'utf8');
const modifiedMessageUint8 = new Uint8Array(modifiedMessage);
console.log(`Modified message: "${modifiedMessageText}"`);
const modifiedVerifyResult = bitcoinpqc.verify(
keypair.publicKey,
modifiedMessageUint8,
signature,
algorithm
);
if (modifiedVerifyResult) {
console.log('ERROR: Signature verified for modified message!');
} else {
console.log('Correctly rejected signature for modified message');
}
console.log('✓ Test passed!\n');
return true;
} catch (error) {
console.error(`❌ Error: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
return false;
}
}
async function runTests() {
console.log('Bitcoin PQC Library Example (Node.js - High-Level API)');
console.log('======================================================\n');
console.log('This example tests the post-quantum signature algorithms designed for BIP-360 and the Bitcoin QuBit soft fork.');
console.log('Using the high-level TypeScript wrapper API (index.js).\n');
// Initialize the module
try {
console.log('Initializing WASM module...');
await bitcoinpqc.init({
onRuntimeInitialized: () => {
console.log('✓ WASM module initialized successfully!\n');
},
print: (text) => {
// Enable WASM print output for debugging
console.log('WASM:', text);
},
printErr: (text) => {
console.error('WASM Error:', text);
},
// Node.js-specific: provide crypto.getRandomValues
getRandomValues: (arr) => {
const bytes = randomBytes(arr.length);
arr.set(bytes);
return arr;
}
});
} catch (error) {
console.error('Failed to initialize module:', error);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
const results = [];
// Test ML-DSA-44
results.push(await testAlgorithm(Algorithm.ML_DSA_44, 'ML-DSA-44'));
// Test SLH-DSA-Shake-128s
results.push(await testAlgorithm(Algorithm.SLH_DSA_SHAKE_128S, 'SLH-DSA-Shake-128s'));
// Summary
console.log('\n======================================================');
console.log('Test Summary:');
console.log(` ML-DSA-44: ${results[0] ? '✓ PASSED' : '✗ FAILED'}`);
console.log(` SLH-DSA-Shake-128s: ${results[1] ? '✓ PASSED' : '✗ FAILED'}`);
console.log('======================================================\n');
const exitCode = results.every(r => r) ? 0 : 1;
process.exit(exitCode);
}
// Start
runTests();