From dfffd6795d6c82093f113bd1224a78e365f4555a Mon Sep 17 00:00:00 2001 From: jasonandjay <342690199@qq.com> Date: Wed, 24 Jul 2024 15:53:03 +0800 Subject: [PATCH 1/4] feat: support taproot for signInputHD --- src/payments/bip341.d.ts | 7 +++++ src/payments/bip341.js | 34 ++++++++++++++++++++++++ src/psbt.d.ts | 9 +++++++ src/psbt.js | 33 +++++++++++++++++++++++ ts_src/payments/bip341.ts | 36 +++++++++++++++++++++++++ ts_src/psbt.ts | 56 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 174 insertions(+), 1 deletion(-) 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..8e3f8b50f 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,39 @@ 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; + } + leafHashes.sort((a, b) => a.compare(b)); + const hashes = leafHashes.map(hash => { + return { + hash: bcrypto.taggedHash('TapLeaf', buffer_1.Buffer.from(hash)), + }; + }); + while (hashes.length > 1) { + const nextLevel = []; + for (let i = 0; i < hashes.length; i += 2) { + const left = hashes[i]; + const right = i + 1 === hashes.length ? left : hashes[i + 1]; + nextLevel.push({ + hash: tapBranchHash(left.hash, right.hash), + left, + right, + }); + } + hashes.length = 0; + hashes.push(...nextLevel); + } + return hashes[0].hash; +} +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..1b4c45ab6 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1445,6 +1445,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 +1474,36 @@ 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(node.publicKey)) { + throw new Error('pubkey did not match tapBip32Derivation'); + } + const h = (0, bip341_1.calculateScriptTreeMerkleRoot)(bipDv.leafHashes); + const tweakValue = (0, bip341_1.tapTweakHash)(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/ts_src/payments/bip341.ts b/ts_src/payments/bip341.ts index af9b1f171..bfa4d12ac 100644 --- a/ts_src/payments/bip341.ts +++ b/ts_src/payments/bip341.ts @@ -82,6 +82,42 @@ 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; + } + leafHashes.sort((a, b) => a.compare(b)); + + const hashes = leafHashes.map(hash => { + return { + hash: bcrypto.taggedHash('TapLeaf', NBuffer.from(hash)), + }; + }); + while (hashes.length > 1) { + const nextLevel = []; + for (let i = 0; i < hashes.length; i += 2) { + const left = hashes[i]; + const right = i + 1 === hashes.length ? left : hashes[i + 1]; + nextLevel.push({ + hash: tapBranchHash(left.hash, right.hash), + left, + right, + }); + } + hashes.length = 0; + hashes.push(...nextLevel); + } + return hashes[0].hash; +} + /** * 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..ce1512f38 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 { @@ -1189,6 +1193,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 +1209,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 +1916,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 +1947,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(node.publicKey)) { + throw new Error('pubkey did not match tapBip32Derivation'); + } + const h = calculateScriptTreeMerkleRoot(bipDv!.leafHashes); + const tweakValue = tapTweakHash(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 From 13188a70d19608fd102024dbc372e6f0a79ab530 Mon Sep 17 00:00:00 2001 From: jasonandjay <342690199@qq.com> Date: Wed, 24 Jul 2024 15:55:22 +0800 Subject: [PATCH 2/4] fix: remove conflict default sighashTypes of signInputHD --- src/psbt.js | 22 ++++------------------ ts_src/psbt.ts | 11 ++++------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/psbt.js b/src/psbt.js index 1b4c45ab6..276cdeee0 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')); diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index ce1512f38..9669e607e 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -645,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'); } @@ -670,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) { @@ -703,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'); @@ -720,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) { From 702047b5fc7f9c28a9d1f52e0a18de77fb4eb12b Mon Sep 17 00:00:00 2001 From: jasonandjay <342690199@qq.com> Date: Wed, 24 Jul 2024 15:59:50 +0800 Subject: [PATCH 3/4] feat: add integration test for HDWallet with tapBip32Derivation --- src/payments/bip341.js | 20 ++-- src/psbt.js | 7 +- test/integration/taproot.spec.ts | 157 +++++++++++++++++++++++++++++++ ts_src/payments/bip341.ts | 21 +++-- ts_src/psbt.ts | 4 +- 5 files changed, 191 insertions(+), 18 deletions(-) diff --git a/src/payments/bip341.js b/src/payments/bip341.js index 8e3f8b50f..6ee31df79 100644 --- a/src/payments/bip341.js +++ b/src/payments/bip341.js @@ -68,12 +68,12 @@ exports.toHashTree = toHashTree; */ function calculateScriptTreeMerkleRoot(leafHashes) { if (!leafHashes || leafHashes.length === 0) { - return undefined; + return Buffer.from([]); } leafHashes.sort((a, b) => a.compare(b)); const hashes = leafHashes.map(hash => { return { - hash: bcrypto.taggedHash('TapLeaf', buffer_1.Buffer.from(hash)), + hash, }; }); while (hashes.length > 1) { @@ -81,11 +81,17 @@ function calculateScriptTreeMerkleRoot(leafHashes) { for (let i = 0; i < hashes.length; i += 2) { const left = hashes[i]; const right = i + 1 === hashes.length ? left : hashes[i + 1]; - nextLevel.push({ - hash: tapBranchHash(left.hash, right.hash), - left, - right, - }); + if (i + 1 === hashes.length) { + nextLevel.push({ + hash: left.hash, + left, + right, + }); + } else { + nextLevel.push({ + hash: tapBranchHash(left.hash, right.hash), + }); + } } hashes.length = 0; hashes.push(...nextLevel); diff --git a/src/psbt.js b/src/psbt.js index 276cdeee0..bc91e917d 100644 --- a/src/psbt.js +++ b/src/psbt.js @@ -1481,11 +1481,14 @@ function getTweakSignersFromHD(inputIndex, inputs, hdKeyPair) { } const signers = myDerivations.map(bipDv => { const node = hdKeyPair.derivePath(bipDv.path); - if (!bipDv.pubkey.equals(node.publicKey)) { + 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)(node.publicKey, h); + const tweakValue = (0, bip341_1.tapTweakHash)( + (0, bip371_1.toXOnly)(node.publicKey), + h, + ); return node.tweak(tweakValue); }); return signers; 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 bfa4d12ac..c9c1790c5 100644 --- a/ts_src/payments/bip341.ts +++ b/ts_src/payments/bip341.ts @@ -92,13 +92,13 @@ export function calculateScriptTreeMerkleRoot( leafHashes: Buffer[], ): Buffer | undefined { if (!leafHashes || leafHashes.length === 0) { - return undefined; + return Buffer.from([]); } leafHashes.sort((a, b) => a.compare(b)); const hashes = leafHashes.map(hash => { return { - hash: bcrypto.taggedHash('TapLeaf', NBuffer.from(hash)), + hash, }; }); while (hashes.length > 1) { @@ -106,15 +106,22 @@ export function calculateScriptTreeMerkleRoot( for (let i = 0; i < hashes.length; i += 2) { const left = hashes[i]; const right = i + 1 === hashes.length ? left : hashes[i + 1]; - nextLevel.push({ - hash: tapBranchHash(left.hash, right.hash), - left, - right, - }); + if (i + 1 === hashes.length) { + nextLevel.push({ + hash: left.hash, + left, + right, + }); + } else { + nextLevel.push({ + hash: tapBranchHash(left.hash, right.hash), + }); + } } hashes.length = 0; hashes.push(...nextLevel); } + return hashes[0].hash; } diff --git a/ts_src/psbt.ts b/ts_src/psbt.ts index 9669e607e..e11cdf820 100644 --- a/ts_src/psbt.ts +++ b/ts_src/psbt.ts @@ -1970,11 +1970,11 @@ function getTweakSignersFromHD( const signers: Array = myDerivations.map(bipDv => { const node = hdKeyPair.derivePath(bipDv!.path); - if (!bipDv!.pubkey.equals(node.publicKey)) { + if (!bipDv!.pubkey.equals(toXOnly(node.publicKey))) { throw new Error('pubkey did not match tapBip32Derivation'); } const h = calculateScriptTreeMerkleRoot(bipDv!.leafHashes); - const tweakValue = tapTweakHash(node.publicKey, h); + const tweakValue = tapTweakHash(toXOnly(node.publicKey), h); return node.tweak(tweakValue); }); From 7fc4cf2407fd7000701b244dbb0d5678782108b6 Mon Sep 17 00:00:00 2001 From: jasonandjay <342690199@qq.com> Date: Mon, 5 Aug 2024 21:18:29 +0800 Subject: [PATCH 4/4] feat: optimize calculateScriptTreeMerkleRoot --- src/payments/bip341.js | 40 +++++++++++++---------------------- ts_src/payments/bip341.ts | 44 ++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/src/payments/bip341.js b/src/payments/bip341.js index 6ee31df79..cf12c4760 100644 --- a/src/payments/bip341.js +++ b/src/payments/bip341.js @@ -68,35 +68,25 @@ exports.toHashTree = toHashTree; */ function calculateScriptTreeMerkleRoot(leafHashes) { if (!leafHashes || leafHashes.length === 0) { - return Buffer.from([]); + return undefined; } - leafHashes.sort((a, b) => a.compare(b)); - const hashes = leafHashes.map(hash => { - return { - hash, - }; - }); - while (hashes.length > 1) { + // 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 < hashes.length; i += 2) { - const left = hashes[i]; - const right = i + 1 === hashes.length ? left : hashes[i + 1]; - if (i + 1 === hashes.length) { - nextLevel.push({ - hash: left.hash, - left, - right, - }); - } else { - nextLevel.push({ - hash: tapBranchHash(left.hash, right.hash), - }); - } + 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, + ); } - hashes.length = 0; - hashes.push(...nextLevel); + currentLevel = nextLevel; } - return hashes[0].hash; + return currentLevel[0]; } exports.calculateScriptTreeMerkleRoot = calculateScriptTreeMerkleRoot; /** diff --git a/ts_src/payments/bip341.ts b/ts_src/payments/bip341.ts index c9c1790c5..a579a6155 100644 --- a/ts_src/payments/bip341.ts +++ b/ts_src/payments/bip341.ts @@ -92,37 +92,29 @@ export function calculateScriptTreeMerkleRoot( leafHashes: Buffer[], ): Buffer | undefined { if (!leafHashes || leafHashes.length === 0) { - return Buffer.from([]); + return undefined; } - leafHashes.sort((a, b) => a.compare(b)); - - const hashes = leafHashes.map(hash => { - return { - hash, - }; - }); - while (hashes.length > 1) { + + // 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 < hashes.length; i += 2) { - const left = hashes[i]; - const right = i + 1 === hashes.length ? left : hashes[i + 1]; - if (i + 1 === hashes.length) { - nextLevel.push({ - hash: left.hash, - left, - right, - }); - } else { - nextLevel.push({ - hash: tapBranchHash(left.hash, right.hash), - }); - } + 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, + ); } - hashes.length = 0; - hashes.push(...nextLevel); + currentLevel = nextLevel; } - return hashes[0].hash; + return currentLevel[0]; } /**