Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support taproot for signAllInputsHD #2137

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading