Skip to content
7 changes: 7 additions & 0 deletions src/payments/bip341.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions src/payments/bip341.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports.tweakKey =
exports.tapTweakHash =
exports.tapleafHash =
exports.findScriptPath =
exports.calculateScriptTreeMerkleRoot =
exports.toHashTree =
exports.rootHashFromPath =
exports.MAX_TAPTREE_DEPTH =
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/psbt.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,22 @@ 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
*/
export interface HDSignerAsync extends HDSignerBase {
derivePath(path: string): HDSignerAsync;
sign(hash: Buffer): Promise<Buffer>;
tweak(t: Buffer): Signer;
}
export interface Signer {
publicKey: Buffer;
Expand Down
58 changes: 40 additions & 18 deletions src/psbt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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'));
Expand All @@ -551,23 +545,15 @@ 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');
}
const signers = getSignersFromHD(inputIndex, this.data.inputs, hdKeyPair);
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'));
Expand Down Expand Up @@ -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');
}
Expand All @@ -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
Expand Down
157 changes: 157 additions & 0 deletions test/integration/taproot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions ts_src/payments/bip341.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading