diff --git a/src/payments/bip341.d.ts b/src/payments/bip341.d.ts index 676021e4c..82e43b383 100644 --- a/src/payments/bip341.d.ts +++ b/src/payments/bip341.d.ts @@ -34,6 +34,13 @@ export declare function rootHashFromPath(controlBlock: Buffer, leafHash: Buffer) * @param scriptTree - the tree of scripts to pairwise hash. */ export declare function toHashTree(scriptTree: Taptree): HashTree; +/** + * Calculates the Merkle root from an array of Taproot leaf hashes. + * + * @param {Buffer[]} leafHashes - Array of Taproot leaf hashes. + * @returns {Buffer} - The Merkle root. + */ +export declare function calculateScriptTreeMerkleRoot(leafHashes: Buffer[]): Buffer | undefined; /** * Given a HashTree, finds the path from a particular hash to the root. * @param node - the root of the tree diff --git a/src/payments/bip341.js b/src/payments/bip341.js index 926af6bf2..cf12c4760 100644 --- a/src/payments/bip341.js +++ b/src/payments/bip341.js @@ -4,6 +4,7 @@ exports.tweakKey = exports.tapTweakHash = exports.tapleafHash = exports.findScriptPath = + exports.calculateScriptTreeMerkleRoot = exports.toHashTree = exports.rootHashFromPath = exports.MAX_TAPTREE_DEPTH = @@ -59,6 +60,35 @@ function toHashTree(scriptTree) { }; } exports.toHashTree = toHashTree; +/** + * Calculates the Merkle root from an array of Taproot leaf hashes. + * + * @param {Buffer[]} leafHashes - Array of Taproot leaf hashes. + * @returns {Buffer} - The Merkle root. + */ +function calculateScriptTreeMerkleRoot(leafHashes) { + if (!leafHashes || leafHashes.length === 0) { + return undefined; + } + // sort the leaf nodes + leafHashes.sort(Buffer.compare); + // create the initial hash node + let currentLevel = leafHashes; + // build Merkle Tree + while (currentLevel.length > 1) { + const nextLevel = []; + for (let i = 0; i < currentLevel.length; i += 2) { + const left = currentLevel[i]; + const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : left; + nextLevel.push( + i + 1 < currentLevel.length ? tapBranchHash(left, right) : left, + ); + } + currentLevel = nextLevel; + } + return currentLevel[0]; +} +exports.calculateScriptTreeMerkleRoot = calculateScriptTreeMerkleRoot; /** * Given a HashTree, finds the path from a particular hash to the root. * @param node - the root of the tree diff --git a/src/psbt.d.ts b/src/psbt.d.ts index d350ca12b..a3c7abd93 100644 --- a/src/psbt.d.ts +++ b/src/psbt.d.ts @@ -160,6 +160,14 @@ export interface HDSigner extends HDSignerBase { * Return a 64 byte signature (32 byte r and 32 byte s in that order) */ sign(hash: Buffer): Buffer; + /** + * Adjusts a keypair for Taproot payments by applying a tweak to derive the internal key. + * + * In Taproot, a keypair may need to be tweaked to produce an internal key that conforms to the Taproot script. + * This tweak process involves modifying the original keypair based on a specific tweak value to ensure compatibility + * with the Taproot address format and functionality. + */ + tweak(t: Buffer): Signer; } /** * Same as above but with async sign method @@ -167,6 +175,7 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; + tweak(t: Buffer): Signer; } export interface Signer { publicKey: Buffer; diff --git a/src/psbt.js b/src/psbt.js index b071f374f..bc91e917d 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -500,10 +500,7 @@ class Psbt { } return validationResultCount > 0; } - signAllInputsHD( - hdKeyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], - ) { + signAllInputsHD(hdKeyPair, sighashTypes) { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } @@ -521,10 +518,7 @@ class Psbt { } return this; } - signAllInputsHDAsync( - hdKeyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], - ) { + signAllInputsHDAsync(hdKeyPair, sighashTypes) { return new Promise((resolve, reject) => { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { return reject(new Error('Need HDSigner to sign input')); @@ -551,11 +545,7 @@ class Psbt { }); }); } - signInputHD( - inputIndex, - hdKeyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], - ) { + signInputHD(inputIndex, hdKeyPair, sighashTypes) { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } @@ -563,11 +553,7 @@ class Psbt { signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes)); return this; } - signInputHDAsync( - inputIndex, - hdKeyPair, - sighashTypes = [transaction_1.Transaction.SIGHASH_ALL], - ) { + signInputHDAsync(inputIndex, hdKeyPair, sighashTypes) { return new Promise((resolve, reject) => { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { return reject(new Error('Need HDSigner to sign input')); @@ -1445,6 +1431,9 @@ function getScriptFromInput(inputIndex, input, cache) { } function getSignersFromHD(inputIndex, inputs, hdKeyPair) { const input = (0, utils_1.checkForInput)(inputs, inputIndex); + if ((0, bip371_1.isTaprootInput)(input)) { + return getTweakSignersFromHD(inputIndex, inputs, hdKeyPair); + } if (!input.bip32Derivation || input.bip32Derivation.length === 0) { throw new Error('Need bip32Derivation to sign with HD'); } @@ -1471,6 +1460,39 @@ function getSignersFromHD(inputIndex, inputs, hdKeyPair) { }); return signers; } +function getTweakSignersFromHD(inputIndex, inputs, hdKeyPair) { + const input = (0, utils_1.checkForInput)(inputs, inputIndex); + if (!input.tapBip32Derivation || input.tapBip32Derivation.length === 0) { + throw new Error('Need tapBip32Derivation to sign with HD'); + } + const myDerivations = input.tapBip32Derivation + .map(bipDv => { + if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { + return bipDv; + } else { + return; + } + }) + .filter(v => !!v); + if (myDerivations.length === 0) { + throw new Error( + 'Need one tapBip32Derivation masterFingerprint to match the HDSigner fingerprint', + ); + } + const signers = myDerivations.map(bipDv => { + const node = hdKeyPair.derivePath(bipDv.path); + if (!bipDv.pubkey.equals((0, bip371_1.toXOnly)(node.publicKey))) { + throw new Error('pubkey did not match tapBip32Derivation'); + } + const h = (0, bip341_1.calculateScriptTreeMerkleRoot)(bipDv.leafHashes); + const tweakValue = (0, bip341_1.tapTweakHash)( + (0, bip371_1.toXOnly)(node.publicKey), + h, + ); + return node.tweak(tweakValue); + }); + return signers; +} function getSortedSigs(script, partialSig) { const p2ms = payments.p2ms({ output: script }); // for each pubkey in order of p2ms script diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts index 12448623e..5da2e35fe 100644 --- a/test/integration/taproot.spec.ts +++ b/test/integration/taproot.spec.ts @@ -217,6 +217,163 @@ describe('bitcoinjs-lib (transaction with taproot)', () => { }); }); + it('can create (and broadcast via 3PBP) a taproot key-path spend Transaction of HD wallet by tapBip32Derivation', async () => { + const root = bip32.fromSeed(rng(64), regtest); + const path = `m/86'/0'/0'/0/0`; + const child = root.derivePath(path); + const internalKey = toXOnly(child.publicKey); + + const { output, address } = bitcoin.payments.p2tr({ + internalPubkey: internalKey, + network: regtest, + }); + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + tapInternalKey: internalKey, + tapBip32Derivation: [ + { + masterFingerprint: root.fingerprint, + pubkey: internalKey, + path, + leafHashes: [], + }, + ], + }); + + psbt.addOutput({ + value: sendAmount, + address: address!, + tapInternalKey: internalKey, + }); + + await psbt.signAllInputsHD(root); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address, + vout: 0, + value: sendAmount, + }); + }); + + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction with 3 leaves of HD wallet by tapBip32Derivation', async () => { + // const root = bip32.fromSeed(rng(64), regtest); + const mnemonic = + 'praise you muffin lion enable neck grocery crumble super myself license ghost'; + const seed = bip39.mnemonicToSeedSync(mnemonic); + const root = bip32.fromSeed(seed, regtest); + const path = `m/86'/0'/0'/0/0`; + const child = root.derivePath(path); + const internalKey = toXOnly(child.publicKey); + + const leafA = { + version: LEAF_VERSION_TAPSCRIPT, + output: bitcoin.script.fromASM( + `${internalKey.toString('hex')} OP_CHECKSIG`, + ), + }; + const leafB = { + version: LEAF_VERSION_TAPSCRIPT, + output: bitcoin.script.fromASM( + `${internalKey.toString('hex')} OP_CHECKSIG`, + ), + }; + const leafC = { + version: LEAF_VERSION_TAPSCRIPT, + output: bitcoin.script.fromASM( + `${internalKey.toString('hex')} OP_CHECKSIG`, + ), + }; + const scriptTree: Taptree = [ + { + output: leafA.output, + }, + [ + { + output: leafB.output, + }, + { + output: leafC.output, + }, + ], + ]; + + const payment = bitcoin.payments.p2tr({ + internalPubkey: internalKey, + scriptTree, + network: regtest, + }); + + const { output, address } = payment; + + // amount from faucet + const amount = 42e4; + // amount to send + const sendAmount = amount - 1e4; + // get faucet + const unspent = await regtestUtils.faucetComplex(output!, amount); + + const psbt = new bitcoin.Psbt({ network: regtest }); + const leafHashes = [ + tapleafHash(leafA), + tapleafHash(leafB), + tapleafHash(leafC), + ]; + psbt.addInput({ + hash: unspent.txId, + index: 0, + witnessUtxo: { value: amount, script: output! }, + tapInternalKey: internalKey, + tapBip32Derivation: [ + { + masterFingerprint: root.fingerprint, + pubkey: internalKey, + path, + leafHashes, + }, + ], + }); + + psbt.addOutput({ + value: sendAmount, + script: output!, + }); + + await psbt.signAllInputsHD(root); + + psbt.finalizeAllInputs(); + const tx = psbt.extractTransaction(); + const rawTx = tx.toBuffer(); + + const hex = rawTx.toString('hex'); + + await regtestUtils.broadcast(hex); + await regtestUtils.verify({ + txId: tx.getId(), + address, + vout: 0, + value: sendAmount, + }); + }); + it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIG', async () => { const internalKey = bip32.fromSeed(rng(64), regtest); const leafKey = bip32.fromSeed(rng(64), regtest); diff --git a/ts_src/payments/bip341.ts b/ts_src/payments/bip341.ts index af9b1f171..a579a6155 100644 --- a/ts_src/payments/bip341.ts +++ b/ts_src/payments/bip341.ts @@ -82,6 +82,41 @@ export function toHashTree(scriptTree: Taptree): HashTree { }; } +/** + * Calculates the Merkle root from an array of Taproot leaf hashes. + * + * @param {Buffer[]} leafHashes - Array of Taproot leaf hashes. + * @returns {Buffer} - The Merkle root. + */ +export function calculateScriptTreeMerkleRoot( + leafHashes: Buffer[], +): Buffer | undefined { + if (!leafHashes || leafHashes.length === 0) { + return undefined; + } + + // sort the leaf nodes + leafHashes.sort(Buffer.compare); + + // create the initial hash node + let currentLevel = leafHashes; + + // build Merkle Tree + while (currentLevel.length > 1) { + const nextLevel = []; + for (let i = 0; i < currentLevel.length; i += 2) { + const left = currentLevel[i]; + const right = i + 1 < currentLevel.length ? currentLevel[i + 1] : left; + nextLevel.push( + i + 1 < currentLevel.length ? tapBranchHash(left, right) : left, + ); + } + currentLevel = nextLevel; + } + + return currentLevel[0]; +} + /** * Given a HashTree, finds the path from a particular hash to the root. * @param node - the root of the tree diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index c73574035..e11cdf820 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -19,7 +19,11 @@ import { fromOutputScript, toOutputScript } from './address'; import { cloneBuffer, reverseBuffer } from './bufferutils'; import { bitcoin as btcNetwork, Network } from './networks'; import * as payments from './payments'; -import { tapleafHash } from './payments/bip341'; +import { + calculateScriptTreeMerkleRoot, + tapleafHash, + tapTweakHash, +} from './payments/bip341'; import * as bscript from './script'; import { Output, Transaction } from './transaction'; import { @@ -641,10 +645,7 @@ export class Psbt { return validationResultCount > 0; } - signAllInputsHD( - hdKeyPair: HDSigner, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], - ): this { + signAllInputsHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); } @@ -666,7 +667,7 @@ export class Psbt { signAllInputsHDAsync( hdKeyPair: HDSigner | HDSignerAsync, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes?: number[], ): Promise { return new Promise((resolve, reject): any => { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { @@ -699,7 +700,7 @@ export class Psbt { signInputHD( inputIndex: number, hdKeyPair: HDSigner, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes?: number[], ): this { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { throw new Error('Need HDSigner to sign input'); @@ -716,7 +717,7 @@ export class Psbt { signInputHDAsync( inputIndex: number, hdKeyPair: HDSigner | HDSignerAsync, - sighashTypes: number[] = [Transaction.SIGHASH_ALL], + sighashTypes?: number[], ): Promise { return new Promise((resolve, reject): any => { if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) { @@ -1189,6 +1190,14 @@ export interface HDSigner extends HDSignerBase { * Return a 64 byte signature (32 byte r and 32 byte s in that order) */ sign(hash: Buffer): Buffer; + /** + * Adjusts a keypair for Taproot payments by applying a tweak to derive the internal key. + * + * In Taproot, a keypair may need to be tweaked to produce an internal key that conforms to the Taproot script. + * This tweak process involves modifying the original keypair based on a specific tweak value to ensure compatibility + * with the Taproot address format and functionality. + */ + tweak(t: Buffer): Signer; } /** @@ -1197,6 +1206,7 @@ export interface HDSigner extends HDSignerBase { export interface HDSignerAsync extends HDSignerBase { derivePath(path: string): HDSignerAsync; sign(hash: Buffer): Promise; + tweak(t: Buffer): Signer; } export interface Signer { @@ -1903,6 +1913,10 @@ function getSignersFromHD( hdKeyPair: HDSigner | HDSignerAsync, ): Array { const input = checkForInput(inputs, inputIndex); + if (isTaprootInput(input)) { + return getTweakSignersFromHD(inputIndex, inputs, hdKeyPair); + } + if (!input.bip32Derivation || input.bip32Derivation.length === 0) { throw new Error('Need bip32Derivation to sign with HD'); } @@ -1930,6 +1944,43 @@ function getSignersFromHD( return signers; } +function getTweakSignersFromHD( + inputIndex: number, + inputs: PsbtInput[], + hdKeyPair: HDSigner | HDSignerAsync, +): Array { + const input = checkForInput(inputs, inputIndex); + if (!input.tapBip32Derivation || input.tapBip32Derivation.length === 0) { + throw new Error('Need tapBip32Derivation to sign with HD'); + } + const myDerivations = input.tapBip32Derivation + .map(bipDv => { + if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) { + return bipDv; + } else { + return; + } + }) + .filter(v => !!v); + if (myDerivations.length === 0) { + throw new Error( + 'Need one tapBip32Derivation masterFingerprint to match the HDSigner fingerprint', + ); + } + + const signers: Array = myDerivations.map(bipDv => { + const node = hdKeyPair.derivePath(bipDv!.path); + if (!bipDv!.pubkey.equals(toXOnly(node.publicKey))) { + throw new Error('pubkey did not match tapBip32Derivation'); + } + const h = calculateScriptTreeMerkleRoot(bipDv!.leafHashes); + const tweakValue = tapTweakHash(toXOnly(node.publicKey), h); + + return node.tweak(tweakValue); + }); + return signers; +} + function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] { const p2ms = payments.p2ms({ output: script }); // for each pubkey in order of p2ms script