diff --git a/src/psbt/bip371.d.ts b/src/psbt/bip371.d.ts
index fae6e756b..f90a82cd7 100644
--- a/src/psbt/bip371.d.ts
+++ b/src/psbt/bip371.d.ts
@@ -1,5 +1,5 @@
///
-import { Taptree } from '../types';
+import { Taptree, HuffmanTapTreeNode } from '../types';
import { PsbtInput, PsbtOutput, TapLeaf } from 'bip174/src/lib/interfaces';
export declare const toXOnly: (pubKey: Buffer) => Buffer;
/**
@@ -38,4 +38,10 @@ export declare function tapTreeToList(tree: Taptree): TapLeaf[];
* @returns the corresponding taptree, or throws an exception if the tree cannot be reconstructed
*/
export declare function tapTreeFromList(leaves?: TapLeaf[]): Taptree;
+/**
+ * Construct a Taptree where the leaves with the highest likelihood of use are closer to the root.
+ * @param nodes A list of nodes where each element contains a weight (likelihood of use) and
+ * a node which could be a Tapleaf or a branch in a Taptree
+ */
+export declare function createTapTreeUsingHuffmanConstructor(nodes: HuffmanTapTreeNode[]): Taptree;
export declare function checkTaprootInputForSigs(input: PsbtInput, action: string): boolean;
diff --git a/src/psbt/bip371.js b/src/psbt/bip371.js
index 61196ead2..332019a4f 100644
--- a/src/psbt/bip371.js
+++ b/src/psbt/bip371.js
@@ -1,6 +1,7 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.checkTaprootInputForSigs =
+ exports.createTapTreeUsingHuffmanConstructor =
exports.tapTreeFromList =
exports.tapTreeToList =
exports.tweakInternalPubKey =
@@ -18,6 +19,7 @@ const psbtutils_1 = require('./psbtutils');
const bip341_1 = require('../payments/bip341');
const payments_1 = require('../payments');
const psbtutils_2 = require('./psbtutils');
+const sortutils_1 = require('../sortutils');
const toXOnly = pubKey => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33));
exports.toXOnly = toXOnly;
/**
@@ -155,6 +157,35 @@ function tapTreeFromList(leaves = []) {
return instertLeavesInTree(leaves);
}
exports.tapTreeFromList = tapTreeFromList;
+/**
+ * Construct a Taptree where the leaves with the highest likelihood of use are closer to the root.
+ * @param nodes A list of nodes where each element contains a weight (likelihood of use) and
+ * a node which could be a Tapleaf or a branch in a Taptree
+ */
+function createTapTreeUsingHuffmanConstructor(nodes) {
+ if (nodes.length === 0)
+ throw new Error('Cannot create taptree from empty list.');
+ const compare = (a, b) => a.weight - b.weight;
+ const sortedNodes = [...nodes].sort(compare); // Sort array in ascending order of weight
+ let newNode;
+ let nodeA, nodeB;
+ while (sortedNodes.length > 1) {
+ // Construct a new node from the two nodes with the least weight
+ nodeA = sortedNodes.shift(); // There will always be an element to pop
+ nodeB = sortedNodes.shift(); // because loop ends when length <= 1
+ newNode = {
+ weight: nodeA.weight + nodeB.weight,
+ node: [nodeA.node, nodeB.node],
+ };
+ // Place newNode back into array
+ (0, sortutils_1.insertIntoSortedArray)(sortedNodes, newNode, compare);
+ }
+ // Last node is the root node
+ const root = sortedNodes.shift();
+ return root.node;
+}
+exports.createTapTreeUsingHuffmanConstructor =
+ createTapTreeUsingHuffmanConstructor;
function checkTaprootInputForSigs(input, action) {
const sigs = extractTaprootSigs(input);
return sigs.some(sig =>
diff --git a/src/sortutils.d.ts b/src/sortutils.d.ts
new file mode 100644
index 000000000..0a95c33ef
--- /dev/null
+++ b/src/sortutils.d.ts
@@ -0,0 +1,9 @@
+/**
+ * Inserts an element into a sorted array.
+ * @template T
+ * @param {Array} array - The sorted array to insert into.
+ * @param {T} element - The element to insert.
+ * @param {(a: T, b: T) => number} compare - The comparison function used to sort the array.
+ * @returns {number} The index at which the element was inserted.
+ */
+export declare function insertIntoSortedArray(array: Array, element: T, compare: (a: T, b: T) => number): number;
diff --git a/src/sortutils.js b/src/sortutils.js
new file mode 100644
index 000000000..03f0c9013
--- /dev/null
+++ b/src/sortutils.js
@@ -0,0 +1,58 @@
+'use strict';
+Object.defineProperty(exports, '__esModule', { value: true });
+exports.insertIntoSortedArray = void 0;
+/**
+ * Inserts an element into a sorted array.
+ * @template T
+ * @param {Array} array - The sorted array to insert into.
+ * @param {T} element - The element to insert.
+ * @param {(a: T, b: T) => number} compare - The comparison function used to sort the array.
+ * @returns {number} The index at which the element was inserted.
+ */
+function insertIntoSortedArray(array, element, compare) {
+ let high = array.length - 1;
+ let low = 0;
+ let mid;
+ let highElement, lowElement, midElement;
+ let compareHigh, compareLow, compareMid;
+ let targetIndex;
+ while (targetIndex === undefined) {
+ if (high < low) {
+ targetIndex = low;
+ continue;
+ }
+ mid = Math.floor((low + high) / 2);
+ highElement = array[high];
+ lowElement = array[low];
+ midElement = array[mid];
+ compareHigh = compare(element, highElement);
+ compareLow = compare(element, lowElement);
+ compareMid = compare(element, midElement);
+ if (low === high) {
+ // Target index is either to the left or right of element at low
+ if (compareLow <= 0) targetIndex = low;
+ else targetIndex = low + 1;
+ continue;
+ }
+ if (compareHigh >= 0) {
+ // Target index is to the right of high
+ low = high;
+ continue;
+ }
+ if (compareLow <= 0) {
+ // Target index is to the left of low
+ high = low;
+ continue;
+ }
+ if (compareMid <= 0) {
+ // Target index is to the left of mid
+ high = mid;
+ continue;
+ }
+ // Target index is to the right of mid
+ low = mid + 1;
+ }
+ array.splice(targetIndex, 0, element);
+ return targetIndex;
+}
+exports.insertIntoSortedArray = insertIntoSortedArray;
diff --git a/src/types.d.ts b/src/types.d.ts
index 0b8a02f9c..416994770 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -26,6 +26,13 @@ export declare function isTapleaf(o: any): o is Tapleaf;
* The tree has no balancing requirements.
*/
export type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf;
+export interface HuffmanTapTreeNode {
+ /**
+ * weight is the sum of the weight of all children under this node
+ */
+ weight: number;
+ node: Taptree;
+}
export declare function isTaptree(scriptTree: any): scriptTree is Taptree;
export interface TinySecp256k1Interface {
isXOnlyPoint(p: Uint8Array): boolean;
diff --git a/test/huffman.spec.ts b/test/huffman.spec.ts
new file mode 100644
index 000000000..9855d9218
--- /dev/null
+++ b/test/huffman.spec.ts
@@ -0,0 +1,228 @@
+import * as assert from 'assert';
+import { describe, it } from 'mocha';
+import { HuffmanTapTreeNode, Taptree } from '../src/types';
+import { createTapTreeUsingHuffmanConstructor } from '../src/psbt/bip371';
+
+describe('Taptree using Huffman Constructor', () => {
+ const scriptBuff = Buffer.from('');
+
+ it('test empty array', () => {
+ assert.throws(() => createTapTreeUsingHuffmanConstructor([]), {
+ message: 'Cannot create taptree from empty list.',
+ });
+ });
+
+ it(
+ 'should return only one node for a single leaf',
+ testLeafDistances([{ weight: 1, node: { output: scriptBuff } }], [0]),
+ );
+
+ it(
+ 'should return a balanced tree for a list of scripts with equal weights',
+ testLeafDistances(
+ [
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ ],
+ [2, 2, 2, 2],
+ ),
+ );
+
+ it(
+ 'should return an optimal binary tree for a list of scripts with weights [1, 2, 3, 4, 5]',
+ testLeafDistances(
+ [
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 2,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 3,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 4,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 5,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ ],
+ [3, 3, 2, 2, 2],
+ ),
+ );
+
+ it(
+ 'should return an optimal binary tree for a list of scripts with weights [1, 2, 3, 3]',
+ testLeafDistances(
+ [
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 2,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 3,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 3,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ ],
+ [3, 3, 2, 1],
+ ),
+ );
+
+ it(
+ 'should return an optimal binary tree for a list of scripts with some negative weights: [1, 2, 3, -3]',
+ testLeafDistances(
+ [
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 2,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 3,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: -3,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ ],
+ [3, 2, 1, 3],
+ ),
+ );
+
+ it(
+ 'should return an optimal binary tree for a list of scripts with some weights specified as infinity',
+ testLeafDistances(
+ [
+ {
+ weight: 1,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: Number.POSITIVE_INFINITY,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: 3,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ {
+ weight: Number.NEGATIVE_INFINITY,
+ node: {
+ output: scriptBuff,
+ },
+ },
+ ],
+ [3, 1, 2, 3],
+ ),
+ );
+});
+
+function testLeafDistances(
+ input: HuffmanTapTreeNode[],
+ expectedDistances: number[],
+) {
+ return () => {
+ const tree = createTapTreeUsingHuffmanConstructor(input);
+
+ if (!Array.isArray(tree)) {
+ // tree is just one node
+ assert.deepEqual([0], expectedDistances);
+ return;
+ }
+
+ const leaves = input.map(value => value.node);
+
+ const map = new Map(); // Map of leaf to actual distance
+ let currentDistance = 1;
+ let currentArray: Array = tree as any;
+ let nextArray: Array = [];
+ while (currentArray.length > 0) {
+ currentArray.forEach(value => {
+ if (Array.isArray(value)) {
+ nextArray = nextArray.concat(value);
+ return;
+ }
+ map.set(value, currentDistance);
+ });
+
+ currentDistance += 1; // New level
+ currentArray = nextArray;
+ nextArray = [];
+ }
+
+ const actualDistances = leaves.map(value => map.get(value));
+ assert.deepEqual(actualDistances, expectedDistances);
+ };
+}
diff --git a/test/integration/taproot.spec.ts b/test/integration/taproot.spec.ts
index 550a34608..6abdc8c0d 100644
--- a/test/integration/taproot.spec.ts
+++ b/test/integration/taproot.spec.ts
@@ -7,8 +7,13 @@ import { describe, it } from 'mocha';
import { PsbtInput, TapLeafScript } from 'bip174/src/lib/interfaces';
import { regtestUtils } from './_regtest';
import * as bitcoin from '../..';
-import { Taptree } from '../../src/types';
-import { toXOnly, tapTreeToList, tapTreeFromList } from '../../src/psbt/bip371';
+import { Taptree, HuffmanTapTreeNode } from '../../src/types';
+import {
+ toXOnly,
+ tapTreeToList,
+ tapTreeFromList,
+ createTapTreeUsingHuffmanConstructor,
+} from '../../src/psbt/bip371';
import { witnessStackToScriptWitness } from '../../src/psbt/psbtutils';
import { TapLeaf } from 'bip174/src/lib/interfaces';
@@ -598,6 +603,139 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
});
}
});
+
+ it('can verify script-tree built using huffman constructor', async () => {
+ const internalKey = bip32.fromSeed(rng(64), regtest);
+ const leafKey = bip32.fromSeed(rng(64), regtest);
+
+ const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString(
+ 'hex',
+ )} OP_CHECKSIG`;
+ const leafScript = bitcoin.script.fromASM(leafScriptAsm);
+
+ const nodes: HuffmanTapTreeNode[] = [
+ {
+ weight: 5,
+ node: {
+ output: bitcoin.script.fromASM(
+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG',
+ ),
+ },
+ },
+ {
+ weight: 5,
+ node: {
+ output: bitcoin.script.fromASM(
+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG',
+ ),
+ },
+ },
+ {
+ weight: 1,
+ node: {
+ output: bitcoin.script.fromASM(
+ '2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG',
+ ),
+ },
+ },
+ {
+ weight: 1,
+ node: {
+ output: bitcoin.script.fromASM(
+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG',
+ ),
+ },
+ },
+ {
+ weight: 4,
+ node: {
+ output: bitcoin.script.fromASM(
+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG',
+ ),
+ },
+ },
+ {
+ weight: 4,
+ node: {
+ output: bitcoin.script.fromASM(
+ '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG',
+ ),
+ },
+ },
+ {
+ weight: 7,
+ node: {
+ output: leafScript,
+ },
+ },
+ ];
+
+ const scriptTree: Taptree = createTapTreeUsingHuffmanConstructor(nodes);
+
+ const redeem = {
+ output: leafScript,
+ redeemVersion: 192,
+ };
+
+ const { output, witness } = bitcoin.payments.p2tr({
+ internalPubkey: toXOnly(internalKey.publicKey),
+ scriptTree,
+ redeem,
+ 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! },
+ });
+ psbt.updateInput(0, {
+ tapLeafScript: [
+ {
+ leafVersion: redeem.redeemVersion,
+ script: redeem.output,
+ controlBlock: witness![witness!.length - 1],
+ },
+ ],
+ });
+
+ const sendInternalKey = bip32.fromSeed(rng(64), regtest);
+ const sendPubKey = toXOnly(sendInternalKey.publicKey);
+ const { address: sendAddress } = bitcoin.payments.p2tr({
+ internalPubkey: sendPubKey,
+ scriptTree,
+ network: regtest,
+ });
+
+ psbt.addOutput({
+ value: sendAmount,
+ address: sendAddress!,
+ tapInternalKey: sendPubKey,
+ tapTree: { leaves: tapTreeToList(scriptTree) },
+ });
+
+ psbt.signInput(0, leafKey);
+ psbt.finalizeInput(0);
+ const tx = psbt.extractTransaction();
+ const rawTx = tx.toBuffer();
+ const hex = rawTx.toString('hex');
+
+ await regtestUtils.broadcast(hex);
+ await regtestUtils.verify({
+ txId: tx.getId(),
+ address: sendAddress!,
+ vout: 0,
+ value: sendAmount,
+ });
+ });
});
function buildLeafIndexFinalizer(
diff --git a/test/sortutils.spec.ts b/test/sortutils.spec.ts
new file mode 100644
index 000000000..403681680
--- /dev/null
+++ b/test/sortutils.spec.ts
@@ -0,0 +1,27 @@
+import * as assert from 'assert';
+import { describe, it } from 'mocha';
+import { insertIntoSortedArray } from '../src/sortutils';
+
+describe('insertIntoSortedArray', () => {
+ it('insert 1 into []', testList([], 1, [1]));
+
+ it('insert 2 into [1]', testList([1], 2, [1, 2]));
+
+ it('insert 1 into [2]', testList([2], 1, [1, 2]));
+
+ it('insert 3 into [1, 2]', testList([1, 2], 3, [1, 2, 3]));
+
+ it('insert 2 into [1, 3]', testList([1, 3], 2, [1, 2, 3]));
+
+ it('insert 1 into [2, 3]', testList([2, 3], 1, [1, 2, 3]));
+
+ it('insert 2 into [1, 2, 3]', testList([1, 2, 3], 2, [1, 2, 2, 3]));
+});
+
+function testList(input: number[], insert: number, expected: number[]) {
+ return () => {
+ const compare = (a: number, b: number) => a - b;
+ insertIntoSortedArray(input, insert, compare);
+ assert.deepEqual(input, expected);
+ };
+}
diff --git a/ts_src/psbt/bip371.ts b/ts_src/psbt/bip371.ts
index 375d35cc6..115f0f455 100644
--- a/ts_src/psbt/bip371.ts
+++ b/ts_src/psbt/bip371.ts
@@ -1,4 +1,10 @@
-import { Taptree, Tapleaf, isTapleaf, isTaptree } from '../types';
+import {
+ Taptree,
+ Tapleaf,
+ isTapleaf,
+ isTaptree,
+ HuffmanTapTreeNode,
+} from '../types';
import {
PsbtInput,
PsbtOutput,
@@ -26,6 +32,7 @@ import {
import { p2tr } from '../payments';
import { signatureBlocksAction } from './psbtutils';
+import { insertIntoSortedArray } from '../sortutils';
export const toXOnly = (pubKey: Buffer) =>
pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);
@@ -196,6 +203,40 @@ export function tapTreeFromList(leaves: TapLeaf[] = []): Taptree {
return instertLeavesInTree(leaves);
}
+/**
+ * Construct a Taptree where the leaves with the highest likelihood of use are closer to the root.
+ * @param nodes A list of nodes where each element contains a weight (likelihood of use) and
+ * a node which could be a Tapleaf or a branch in a Taptree
+ */
+export function createTapTreeUsingHuffmanConstructor(
+ nodes: HuffmanTapTreeNode[],
+): Taptree {
+ if (nodes.length === 0)
+ throw new Error('Cannot create taptree from empty list.');
+
+ const compare = (a: HuffmanTapTreeNode, b: HuffmanTapTreeNode) =>
+ a.weight - b.weight;
+ const sortedNodes = [...nodes].sort(compare); // Sort array in ascending order of weight
+
+ let newNode: HuffmanTapTreeNode;
+ let nodeA: HuffmanTapTreeNode, nodeB: HuffmanTapTreeNode;
+ while (sortedNodes.length > 1) {
+ // Construct a new node from the two nodes with the least weight
+ nodeA = sortedNodes.shift()!; // There will always be an element to pop
+ nodeB = sortedNodes.shift()!; // because loop ends when length <= 1
+ newNode = {
+ weight: nodeA.weight + nodeB.weight,
+ node: [nodeA.node, nodeB.node],
+ };
+ // Place newNode back into array
+ insertIntoSortedArray(sortedNodes, newNode, compare);
+ }
+
+ // Last node is the root node
+ const root = sortedNodes.shift()!;
+ return root.node;
+}
+
export function checkTaprootInputForSigs(
input: PsbtInput,
action: string,
diff --git a/ts_src/sortutils.ts b/ts_src/sortutils.ts
new file mode 100644
index 000000000..d7b5a0c34
--- /dev/null
+++ b/ts_src/sortutils.ts
@@ -0,0 +1,66 @@
+/**
+ * Inserts an element into a sorted array.
+ * @template T
+ * @param {Array} array - The sorted array to insert into.
+ * @param {T} element - The element to insert.
+ * @param {(a: T, b: T) => number} compare - The comparison function used to sort the array.
+ * @returns {number} The index at which the element was inserted.
+ */
+export function insertIntoSortedArray(
+ array: Array,
+ element: T,
+ compare: (a: T, b: T) => number,
+) {
+ let high = array.length - 1;
+ let low = 0;
+ let mid;
+ let highElement, lowElement, midElement;
+ let compareHigh, compareLow, compareMid;
+ let targetIndex;
+ while (targetIndex === undefined) {
+ if (high < low) {
+ targetIndex = low;
+ continue;
+ }
+
+ mid = Math.floor((low + high) / 2);
+
+ highElement = array[high];
+ lowElement = array[low];
+ midElement = array[mid];
+
+ compareHigh = compare(element, highElement);
+ compareLow = compare(element, lowElement);
+ compareMid = compare(element, midElement);
+
+ if (low === high) {
+ // Target index is either to the left or right of element at low
+ if (compareLow <= 0) targetIndex = low;
+ else targetIndex = low + 1;
+ continue;
+ }
+
+ if (compareHigh >= 0) {
+ // Target index is to the right of high
+ low = high;
+ continue;
+ }
+ if (compareLow <= 0) {
+ // Target index is to the left of low
+ high = low;
+ continue;
+ }
+
+ if (compareMid <= 0) {
+ // Target index is to the left of mid
+ high = mid;
+ continue;
+ }
+
+ // Target index is to the right of mid
+ low = mid + 1;
+ }
+
+ array.splice(targetIndex, 0, element);
+ return targetIndex;
+}
diff --git a/ts_src/types.ts b/ts_src/types.ts
index 2457ec240..edd0a742c 100644
--- a/ts_src/types.ts
+++ b/ts_src/types.ts
@@ -93,6 +93,14 @@ export function isTapleaf(o: any): o is Tapleaf {
*/
export type Taptree = [Taptree | Tapleaf, Taptree | Tapleaf] | Tapleaf;
+export interface HuffmanTapTreeNode {
+ /**
+ * weight is the sum of the weight of all children under this node
+ */
+ weight: number;
+ node: Taptree;
+}
+
export function isTaptree(scriptTree: any): scriptTree is Taptree {
if (!Array(scriptTree)) return isTapleaf(scriptTree);
if (scriptTree.length !== 2) return false;