diff --git a/test/integration/taproot.md b/test/integration/taproot.md index 2db6eef..4010340 100644 --- a/test/integration/taproot.md +++ b/test/integration/taproot.md @@ -25,68 +25,108 @@ A simple keyspend example that is possible with the current API is below. - node >= v14 ```js -const crypto = require('crypto'); +// Run this whole file as async +// Catch any errors at the bottom of the file +// and exit the process with 1 error code +(async () => { +// Order of the curve (N) - 1 +const N_LESS_1 = Buffer.from( + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140', + 'hex' +); +// 1 represented as 32 bytes BE +const ONE = Buffer.from( + '0000000000000000000000000000000000000000000000000000000000000001', + 'hex' +); + +const crypto = require('crypto'); // bitcoinjs-lib v6 const bitcoin = require('bitcoinjs-lib'); // bip32 v3 wraps tiny-secp256k1 const BIP32Wrapper = require('bip32').default; const RegtestUtils = require('regtest-client').RegtestUtils; // tiny-secp256k1 v2 is an ESM module, so we can't "require", and must import async -import('tiny-secp256k1') - .then(async (ecc) => { - // End imports +const ecc = await import('tiny-secp256k1'); +// wrap the bip32 library +const bip32 = BIP32Wrapper(ecc); +// set up dependencies +const APIPASS = process.env.APIPASS || 'satoshi'; +// docker run -d -p 8080:8080 junderw/bitcoinjs-regtest-server +const APIURL = process.env.APIURL || 'http://127.0.0.1:8080/1'; +const regtestUtils = new RegtestUtils({ APIPASS, APIURL }); +// End imports - // set up dependencies - const APIPASS = process.env.APIPASS || 'satoshi'; - // docker run -d -p 8080:8080 junderw/bitcoinjs-regtest-server - const APIURL = process.env.APIURL || 'http://127.0.0.1:8080/1'; - const regtestUtils = new RegtestUtils({ APIPASS, APIURL }); +const myKey = bip32.fromSeed(crypto.randomBytes(64), regtestUtils.network); - const bip32 = BIP32Wrapper(ecc); +const output = createKeySpendOutput(myKey.publicKey); +const address = bitcoin.address.fromOutputScript( + output, + regtestUtils.network +); +// amount from faucet +const amount = 42e4; +// amount to send +const sendAmount = amount - 1e4; +// get faucet +const unspent = await regtestUtils.faucetComplex(output, amount); - const myKey = bip32.fromSeed(crypto.randomBytes(64), regtestUtils.network); - // scriptPubkey - const output = Buffer.concat([ - // witness v1, PUSH_DATA 32 bytes - Buffer.from([0x51, 0x20]), - // x-only pubkey (remove 1 byte y parity) - myKey.publicKey.slice(1, 33), - ]); - const address = bitcoin.address.fromOutputScript( - output, - regtestUtils.network - ); - // amount from faucet - const amount = 42e4; - // amount to send - const sendAmount = amount - 1e4; - // get faucet - const unspent = await regtestUtils.faucetComplex(output, amount); +const tx = createSigned( + myKey, + unspent.txId, + unspent.vout, + sendAmount, + [output], + [amount] +); - const tx = createSigned( - myKey, - unspent.txId, - unspent.vout, - sendAmount, - [output], - [amount] - ); +const hex = tx.toHex(); +console.log('Valid tx sent from:'); +console.log(address); +console.log('tx hex:'); +console.log(hex); +await regtestUtils.broadcast(hex); +await regtestUtils.verify({ + txId: tx.getId(), + address, + vout: 0, + value: sendAmount, +}); - const hex = tx.toHex(); - console.log('Valid tx sent from:'); - console.log(address); - console.log('tx hex:'); - console.log(hex); - await regtestUtils.broadcast(hex); - await regtestUtils.verify({ - txId: tx.getId(), - address, - vout: 0, - value: sendAmount, - }); - }) - .catch(console.error); +// Function for creating a tweaked p2tr key-spend only address +// (This is recommended by BIP341) +function createKeySpendOutput(publicKey) { + // x-only pubkey (remove 1 byte y parity) + const myXOnlyPubkey = publicKey.slice(1, 33); + const commitHash = bitcoin.crypto.taggedHash('TapTweak', myXOnlyPubkey); + const tweakResult = ecc.xOnlyPointAddTweak(myXOnlyPubkey, commitHash); + if (tweakResult === null) throw new Error('Invalid Tweak'); + const { xOnlyPubkey: tweaked } = tweakResult; + // scriptPubkey + return Buffer.concat([ + // witness v1, PUSH_DATA 32 bytes + Buffer.from([0x51, 0x20]), + // x-only tweaked pubkey + tweaked, + ]); +} + +// Function for signing for a tweaked p2tr key-spend only address +// (Required for the above address) +function signTweaked(messageHash, key) { + const privateKey = + key.publicKey[0] === 2 + ? key.privateKey + : ecc.privateAdd(ecc.privateSub(N_LESS_1, key.privateKey), ONE); + const tweakHash = bitcoin.crypto.taggedHash( + 'TapTweak', + key.publicKey.slice(1, 33) + ); + const newPrivateKey = ecc.privateAdd(privateKey, tweakHash); + if (newPrivateKey === null) throw new Error('Invalid Tweak'); + return ecc.signSchnorr(messageHash, newPrivateKey, Buffer.alloc(32)); +} // Function for creating signed tx function createSigned(key, txid, vout, amountToSend, scriptPubkeys, values) { @@ -102,10 +142,15 @@ function createSigned(key, txid, vout, amountToSend, scriptPubkeys, values) { values, // All previous values of all inputs bitcoin.Transaction.SIGHASH_DEFAULT // sighash flag, DEFAULT is schnorr-only (DEFAULT == ALL) ); - const signature = Buffer.from(key.signSchnorr(sighash)); + const signature = Buffer.from(signTweaked(sighash, key)); // witness stack for keypath spend is just the signature. // If sighash is not SIGHASH_DEFAULT (ALL) then you must add 1 byte with sighash value tx.ins[0].witness = [signature]; return tx; } + +})().catch((err) => { + console.error(err); + process.exit(1); +}); ``` \ No newline at end of file