Skip to content

Commit 72438b6

Browse files
committed
Add tests for createTapTreeUsingHuffmanConstructor
1 parent 87f103f commit 72438b6

File tree

2 files changed

+368
-2
lines changed

2 files changed

+368
-2
lines changed

test/huffman.spec.ts

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import * as assert from 'assert';
2+
import { describe, it } from 'mocha';
3+
import { HuffmanTapTreeNode, Taptree } from '../src/types';
4+
import { createTapTreeUsingHuffmanConstructor } from '../src/psbt/bip371';
5+
6+
describe('Taptree using Huffman Constructor', () => {
7+
const scriptBuff = Buffer.from('');
8+
9+
it('test empty array', () => {
10+
assert.throws(() => createTapTreeUsingHuffmanConstructor([]), {
11+
message: 'Cannot create taptree from empty list.',
12+
});
13+
});
14+
15+
it(
16+
'should return only one node for a single leaf',
17+
testLeafDistances([{ weight: 1, node: { output: scriptBuff } }], [0]),
18+
);
19+
20+
it(
21+
'should return a balanced tree for a list of scripts with equal weights',
22+
testLeafDistances(
23+
[
24+
{
25+
weight: 1,
26+
node: {
27+
output: scriptBuff,
28+
},
29+
},
30+
{
31+
weight: 1,
32+
node: {
33+
output: scriptBuff,
34+
},
35+
},
36+
{
37+
weight: 1,
38+
node: {
39+
output: scriptBuff,
40+
},
41+
},
42+
{
43+
weight: 1,
44+
node: {
45+
output: scriptBuff,
46+
},
47+
},
48+
],
49+
[2, 2, 2, 2],
50+
),
51+
);
52+
53+
it(
54+
'should return an optimal binary tree for a list of scripts with weights [1, 2, 3, 4, 5]',
55+
testLeafDistances(
56+
[
57+
{
58+
weight: 1,
59+
node: {
60+
output: scriptBuff,
61+
},
62+
},
63+
{
64+
weight: 2,
65+
node: {
66+
output: scriptBuff,
67+
},
68+
},
69+
{
70+
weight: 3,
71+
node: {
72+
output: scriptBuff,
73+
},
74+
},
75+
{
76+
weight: 4,
77+
node: {
78+
output: scriptBuff,
79+
},
80+
},
81+
{
82+
weight: 5,
83+
node: {
84+
output: scriptBuff,
85+
},
86+
},
87+
],
88+
[3, 3, 2, 2, 2],
89+
),
90+
);
91+
92+
it(
93+
'should return an optimal binary tree for a list of scripts with weights [1, 2, 3, 3]',
94+
testLeafDistances(
95+
[
96+
{
97+
weight: 1,
98+
node: {
99+
output: scriptBuff,
100+
},
101+
},
102+
{
103+
weight: 2,
104+
node: {
105+
output: scriptBuff,
106+
},
107+
},
108+
{
109+
weight: 3,
110+
node: {
111+
output: scriptBuff,
112+
},
113+
},
114+
{
115+
weight: 3,
116+
node: {
117+
output: scriptBuff,
118+
},
119+
},
120+
],
121+
[3, 3, 2, 1],
122+
),
123+
);
124+
125+
it(
126+
'should return an optimal binary tree for a list of scripts with some negative weights: [1, 2, 3, -3]',
127+
testLeafDistances(
128+
[
129+
{
130+
weight: 1,
131+
node: {
132+
output: scriptBuff,
133+
},
134+
},
135+
{
136+
weight: 2,
137+
node: {
138+
output: scriptBuff,
139+
},
140+
},
141+
{
142+
weight: 3,
143+
node: {
144+
output: scriptBuff,
145+
},
146+
},
147+
{
148+
weight: -3,
149+
node: {
150+
output: scriptBuff,
151+
},
152+
},
153+
],
154+
[3, 2, 1, 3],
155+
),
156+
);
157+
158+
it(
159+
'should return an optimal binary tree for a list of scripts with some weights specified as infinity',
160+
testLeafDistances(
161+
[
162+
{
163+
weight: 1,
164+
node: {
165+
output: scriptBuff,
166+
},
167+
},
168+
{
169+
weight: Number.POSITIVE_INFINITY,
170+
node: {
171+
output: scriptBuff,
172+
},
173+
},
174+
{
175+
weight: 3,
176+
node: {
177+
output: scriptBuff,
178+
},
179+
},
180+
{
181+
weight: Number.NEGATIVE_INFINITY,
182+
node: {
183+
output: scriptBuff,
184+
},
185+
},
186+
],
187+
[3, 1, 2, 3],
188+
),
189+
);
190+
});
191+
192+
function testLeafDistances(
193+
input: HuffmanTapTreeNode[],
194+
expectedDistances: number[],
195+
) {
196+
return () => {
197+
const tree = createTapTreeUsingHuffmanConstructor(input);
198+
199+
if (!Array.isArray(tree)) {
200+
// tree is just one node
201+
assert.deepEqual([0], expectedDistances);
202+
return;
203+
}
204+
205+
const leaves = input.map(value => value.node);
206+
207+
const map = new Map<Taptree, number>(); // Map of leaf to actual distance
208+
let currentDistance = 1;
209+
let currentArray: Array<Taptree[] | Taptree> = tree as any;
210+
let nextArray: Array<Taptree[] | Taptree> = [];
211+
while (currentArray.length > 0) {
212+
currentArray.forEach(value => {
213+
if (Array.isArray(value)) {
214+
nextArray = nextArray.concat(value);
215+
return;
216+
}
217+
map.set(value, currentDistance);
218+
});
219+
220+
currentDistance += 1; // New level
221+
currentArray = nextArray;
222+
nextArray = [];
223+
}
224+
225+
const actualDistances = leaves.map(value => map.get(value));
226+
assert.deepEqual(actualDistances, expectedDistances);
227+
};
228+
}

test/integration/taproot.spec.ts

+140-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import { describe, it } from 'mocha';
77
import { PsbtInput, TapLeafScript } from 'bip174/src/lib/interfaces';
88
import { regtestUtils } from './_regtest';
99
import * as bitcoin from '../..';
10-
import { Taptree } from '../../src/types';
11-
import { toXOnly, tapTreeToList, tapTreeFromList } from '../../src/psbt/bip371';
10+
import { Taptree, HuffmanTapTreeNode } from '../../src/types';
11+
import {
12+
toXOnly,
13+
tapTreeToList,
14+
tapTreeFromList,
15+
createTapTreeUsingHuffmanConstructor,
16+
} from '../../src/psbt/bip371';
1217
import { witnessStackToScriptWitness } from '../../src/psbt/psbtutils';
1318
import { TapLeaf } from 'bip174/src/lib/interfaces';
1419

@@ -598,6 +603,139 @@ describe('bitcoinjs-lib (transaction with taproot)', () => {
598603
});
599604
}
600605
});
606+
607+
it('can verify script-tree built using huffman constructor', async () => {
608+
const internalKey = bip32.fromSeed(rng(64), regtest);
609+
const leafKey = bip32.fromSeed(rng(64), regtest);
610+
611+
const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString(
612+
'hex',
613+
)} OP_CHECKSIG`;
614+
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
615+
616+
const nodes: HuffmanTapTreeNode[] = [
617+
{
618+
weight: 5,
619+
node: {
620+
output: bitcoin.script.fromASM(
621+
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG',
622+
),
623+
},
624+
},
625+
{
626+
weight: 5,
627+
node: {
628+
output: bitcoin.script.fromASM(
629+
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG',
630+
),
631+
},
632+
},
633+
{
634+
weight: 1,
635+
node: {
636+
output: bitcoin.script.fromASM(
637+
'2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG',
638+
),
639+
},
640+
},
641+
{
642+
weight: 1,
643+
node: {
644+
output: bitcoin.script.fromASM(
645+
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG',
646+
),
647+
},
648+
},
649+
{
650+
weight: 4,
651+
node: {
652+
output: bitcoin.script.fromASM(
653+
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG',
654+
),
655+
},
656+
},
657+
{
658+
weight: 4,
659+
node: {
660+
output: bitcoin.script.fromASM(
661+
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG',
662+
),
663+
},
664+
},
665+
{
666+
weight: 7,
667+
node: {
668+
output: leafScript,
669+
},
670+
},
671+
];
672+
673+
const scriptTree: Taptree = createTapTreeUsingHuffmanConstructor(nodes);
674+
675+
const redeem = {
676+
output: leafScript,
677+
redeemVersion: 192,
678+
};
679+
680+
const { output, witness } = bitcoin.payments.p2tr({
681+
internalPubkey: toXOnly(internalKey.publicKey),
682+
scriptTree,
683+
redeem,
684+
network: regtest,
685+
});
686+
687+
// amount from faucet
688+
const amount = 42e4;
689+
// amount to send
690+
const sendAmount = amount - 1e4;
691+
// get faucet
692+
const unspent = await regtestUtils.faucetComplex(output!, amount);
693+
694+
const psbt = new bitcoin.Psbt({ network: regtest });
695+
psbt.addInput({
696+
hash: unspent.txId,
697+
index: 0,
698+
witnessUtxo: { value: amount, script: output! },
699+
});
700+
psbt.updateInput(0, {
701+
tapLeafScript: [
702+
{
703+
leafVersion: redeem.redeemVersion,
704+
script: redeem.output,
705+
controlBlock: witness![witness!.length - 1],
706+
},
707+
],
708+
});
709+
710+
const sendInternalKey = bip32.fromSeed(rng(64), regtest);
711+
const sendPubKey = toXOnly(sendInternalKey.publicKey);
712+
const { address: sendAddress } = bitcoin.payments.p2tr({
713+
internalPubkey: sendPubKey,
714+
scriptTree,
715+
network: regtest,
716+
});
717+
718+
psbt.addOutput({
719+
value: sendAmount,
720+
address: sendAddress!,
721+
tapInternalKey: sendPubKey,
722+
tapTree: { leaves: tapTreeToList(scriptTree) },
723+
});
724+
725+
psbt.signInput(0, leafKey);
726+
psbt.finalizeInput(0);
727+
const tx = psbt.extractTransaction();
728+
const rawTx = tx.toBuffer();
729+
const hex = rawTx.toString('hex');
730+
731+
await regtestUtils.broadcast(hex);
732+
await regtestUtils.verify({
733+
txId: tx.getId(),
734+
address: sendAddress!,
735+
vout: 0,
736+
value: sendAmount,
737+
});
738+
});
601739
});
602740

603741
function buildLeafIndexFinalizer(

0 commit comments

Comments
 (0)