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

Add huffman taptree constructor #1903

Open
wants to merge 5 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/psbt/bip371.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// <reference types="node" />
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;
/**
Expand Down Expand Up @@ -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;
31 changes: 31 additions & 0 deletions src/psbt/bip371.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.checkTaprootInputForSigs =
exports.createTapTreeUsingHuffmanConstructor =
exports.tapTreeFromList =
exports.tapTreeToList =
exports.tweakInternalPubKey =
Expand All @@ -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;
/**
Expand Down Expand Up @@ -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 =>
Expand Down
9 changes: 9 additions & 0 deletions src/sortutils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Inserts an element into a sorted array.
* @template T
* @param {Array<T>} 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<T>(array: Array<T>, element: T, compare: (a: T, b: T) => number): number;
58 changes: 58 additions & 0 deletions src/sortutils.js
Original file line number Diff line number Diff line change
@@ -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<T>} 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;
7 changes: 7 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
228 changes: 228 additions & 0 deletions test/huffman.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Taptree, number>(); // Map of leaf to actual distance
let currentDistance = 1;
let currentArray: Array<Taptree[] | Taptree> = tree as any;
let nextArray: Array<Taptree[] | Taptree> = [];
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);
};
}