11import { toXOnly } from 'bitcoinjs-lib' ;
2+ import { tapScriptFinalizer } from 'bitcoinjs-lib/src/psbt/bip371' ;
3+ import {
4+ tapleafHash ,
5+ LEAF_VERSION_TAPSCRIPT ,
6+ } from 'bitcoinjs-lib/src/payments/bip341' ;
7+ import type { PsbtInput , TapScriptSig } from 'bip174' ;
28import * as assert from 'assert' ;
9+ import * as tools from 'uint8array-tools' ;
310
411describe ( 'toXOnly' , ( ) => {
512 it ( 'should return the input if the pubKey length is 32' , ( ) => {
@@ -21,3 +28,322 @@ describe('toXOnly', () => {
2128 assert . deepStrictEqual ( result , pubKey . slice ( 1 , 33 ) ) ; // Expect the sliced array
2229 } ) ;
2330} ) ;
31+
32+ describe ( 'tapScriptFinalizer' , ( ) => {
33+ // Helper to create a basic tapLeafScript
34+ const createTapLeafScript = (
35+ script : Uint8Array ,
36+ controlBlock : Uint8Array ,
37+ ) => ( {
38+ script,
39+ controlBlock,
40+ leafVersion : LEAF_VERSION_TAPSCRIPT ,
41+ } ) ;
42+
43+ // Helper to create a tapScriptSig
44+ const createTapScriptSig = (
45+ pubkey : Uint8Array ,
46+ signature : Uint8Array ,
47+ leafHash : Uint8Array ,
48+ ) : TapScriptSig => ( {
49+ pubkey,
50+ signature,
51+ leafHash,
52+ } ) ;
53+
54+ describe ( 'successful finalization' , ( ) => {
55+ it ( 'should finalize a taproot input with valid signatures' , ( ) => {
56+ const script = new Uint8Array ( [
57+ 0x20 ,
58+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
59+ 0xac ,
60+ ] ) ; // Simple script with pubkey
61+ const controlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
62+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
63+
64+ const leafHash = tapleafHash ( {
65+ output : script ,
66+ version : LEAF_VERSION_TAPSCRIPT ,
67+ } ) ;
68+
69+ const pubkey = new Uint8Array ( 32 ) . fill ( 1 ) ;
70+ const signature = new Uint8Array ( 64 ) . fill ( 3 ) ;
71+ const tapScriptSig = createTapScriptSig ( pubkey , signature , leafHash ) ;
72+
73+ const input : PsbtInput = {
74+ tapLeafScript : [ tapLeafScript ] ,
75+ tapScriptSig : [ tapScriptSig ] ,
76+ } ;
77+
78+ const result = tapScriptFinalizer ( 0 , input ) ;
79+
80+ assert . ok ( result . finalScriptWitness ) ;
81+ assert . ok ( result . finalScriptWitness instanceof Uint8Array ) ;
82+ assert . ok ( result . finalScriptWitness . length > 0 ) ;
83+ } ) ;
84+
85+ it ( 'should finalize with a specific tapLeafHashToFinalize' , ( ) => {
86+ const script1 = new Uint8Array ( [
87+ 0x20 ,
88+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
89+ 0xac ,
90+ ] ) ;
91+ const script2 = new Uint8Array ( [
92+ 0x20 ,
93+ ...new Uint8Array ( 32 ) . fill ( 5 ) ,
94+ 0xac ,
95+ ] ) ;
96+ const controlBlock1 = new Uint8Array ( 33 ) . fill ( 2 ) ;
97+ const controlBlock2 = new Uint8Array ( 65 ) . fill ( 3 ) ; // Longer control block
98+
99+ const tapLeafScript1 = createTapLeafScript ( script1 , controlBlock1 ) ;
100+ const tapLeafScript2 = createTapLeafScript ( script2 , controlBlock2 ) ;
101+
102+ const leafHash1 = tapleafHash ( {
103+ output : script1 ,
104+ version : LEAF_VERSION_TAPSCRIPT ,
105+ } ) ;
106+ const leafHash2 = tapleafHash ( {
107+ output : script2 ,
108+ version : LEAF_VERSION_TAPSCRIPT ,
109+ } ) ;
110+
111+ const pubkey = new Uint8Array ( 32 ) . fill ( 1 ) ;
112+ const signature1 = new Uint8Array ( 64 ) . fill ( 3 ) ;
113+ const signature2 = new Uint8Array ( 64 ) . fill ( 4 ) ;
114+
115+ const tapScriptSig1 = createTapScriptSig ( pubkey , signature1 , leafHash1 ) ;
116+ const tapScriptSig2 = createTapScriptSig ( pubkey , signature2 , leafHash2 ) ;
117+
118+ const input : PsbtInput = {
119+ tapLeafScript : [ tapLeafScript1 , tapLeafScript2 ] ,
120+ tapScriptSig : [ tapScriptSig1 , tapScriptSig2 ] ,
121+ } ;
122+
123+ // Finalize with specific leaf hash (should use script2)
124+ const result = tapScriptFinalizer ( 0 , input , leafHash2 ) ;
125+
126+ assert . ok ( result . finalScriptWitness ) ;
127+ assert . ok ( result . finalScriptWitness instanceof Uint8Array ) ;
128+ } ) ;
129+
130+ it ( 'should select the tapleaf with shortest control block when multiple are available' , ( ) => {
131+ const script1 = new Uint8Array ( [
132+ 0x20 ,
133+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
134+ 0xac ,
135+ ] ) ;
136+ const script2 = new Uint8Array ( [
137+ 0x20 ,
138+ ...new Uint8Array ( 32 ) . fill ( 5 ) ,
139+ 0xac ,
140+ ] ) ;
141+ const shortControlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
142+ const longControlBlock = new Uint8Array ( 65 ) . fill ( 3 ) ;
143+
144+ const tapLeafScript1 = createTapLeafScript ( script1 , longControlBlock ) ;
145+ const tapLeafScript2 = createTapLeafScript ( script2 , shortControlBlock ) ;
146+
147+ const leafHash1 = tapleafHash ( {
148+ output : script1 ,
149+ version : LEAF_VERSION_TAPSCRIPT ,
150+ } ) ;
151+ const leafHash2 = tapleafHash ( {
152+ output : script2 ,
153+ version : LEAF_VERSION_TAPSCRIPT ,
154+ } ) ;
155+
156+ const pubkey = new Uint8Array ( 32 ) . fill ( 1 ) ;
157+ const signature1 = new Uint8Array ( 64 ) . fill ( 3 ) ;
158+ const signature2 = new Uint8Array ( 64 ) . fill ( 4 ) ;
159+
160+ const tapScriptSig1 = createTapScriptSig ( pubkey , signature1 , leafHash1 ) ;
161+ const tapScriptSig2 = createTapScriptSig ( pubkey , signature2 , leafHash2 ) ;
162+
163+ const input : PsbtInput = {
164+ tapLeafScript : [ tapLeafScript1 , tapLeafScript2 ] ,
165+ tapScriptSig : [ tapScriptSig1 , tapScriptSig2 ] ,
166+ } ;
167+
168+ // Should select script2 because it has shorter control block
169+ const result = tapScriptFinalizer ( 0 , input ) ;
170+
171+ assert . ok ( result . finalScriptWitness ) ;
172+ } ) ;
173+ } ) ;
174+
175+ describe ( 'error cases' , ( ) => {
176+ it ( 'should throw error when no tapScriptSig is provided' , ( ) => {
177+ const script = new Uint8Array ( [
178+ 0x20 ,
179+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
180+ 0xac ,
181+ ] ) ;
182+ const controlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
183+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
184+
185+ const input : PsbtInput = {
186+ tapLeafScript : [ tapLeafScript ] ,
187+ tapScriptSig : [ ] , // Empty signatures
188+ } ;
189+
190+ assert . throws (
191+ ( ) => tapScriptFinalizer ( 5 , input ) ,
192+ / C a n n o t f i n a l i z e t a p r o o t i n p u t # 5 \. N o t a p l e a f s c r i p t s i g n a t u r e p r o v i d e d \. / ,
193+ ) ;
194+ } ) ;
195+
196+ it ( 'should throw error when tapScriptSig is undefined' , ( ) => {
197+ const script = new Uint8Array ( [
198+ 0x20 ,
199+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
200+ 0xac ,
201+ ] ) ;
202+ const controlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
203+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
204+
205+ const input : PsbtInput = {
206+ tapLeafScript : [ tapLeafScript ] ,
207+ // tapScriptSig is undefined
208+ } ;
209+
210+ assert . throws (
211+ ( ) => tapScriptFinalizer ( 3 , input ) ,
212+ / C a n n o t f i n a l i z e t a p r o o t i n p u t # 3 \. N o t a p l e a f s c r i p t s i g n a t u r e p r o v i d e d \. / ,
213+ ) ;
214+ } ) ;
215+
216+ it ( 'should throw error when signature for tapleaf script is not found' , ( ) => {
217+ const script = new Uint8Array ( [
218+ 0x20 ,
219+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
220+ 0xac ,
221+ ] ) ;
222+ const controlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
223+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
224+
225+ const leafHash = tapleafHash ( {
226+ output : script ,
227+ version : LEAF_VERSION_TAPSCRIPT ,
228+ } ) ;
229+
230+ // Create a signature with a different (wrong) leaf hash
231+ const wrongLeafHash = new Uint8Array ( 32 ) . fill ( 99 ) ;
232+ const pubkey = new Uint8Array ( 32 ) . fill ( 1 ) ;
233+ const signature = new Uint8Array ( 64 ) . fill ( 3 ) ;
234+ const tapScriptSig = createTapScriptSig ( pubkey , signature , wrongLeafHash ) ;
235+
236+ const input : PsbtInput = {
237+ tapLeafScript : [ tapLeafScript ] ,
238+ tapScriptSig : [ tapScriptSig ] , // Signature with wrong leaf hash
239+ } ;
240+
241+ assert . throws (
242+ ( ) => tapScriptFinalizer ( 2 , input ) ,
243+ / C a n n o t f i n a l i z e t a p r o o t i n p u t # 2 \. S i g n a t u r e f o r t a p l e a f s c r i p t n o t f o u n d \. / ,
244+ ) ;
245+ } ) ;
246+
247+ it ( 'should throw error when specific tapLeafHashToFinalize is not found' , ( ) => {
248+ const script = new Uint8Array ( [
249+ 0x20 ,
250+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
251+ 0xac ,
252+ ] ) ;
253+ const controlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
254+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
255+
256+ const leafHash = tapleafHash ( {
257+ output : script ,
258+ version : LEAF_VERSION_TAPSCRIPT ,
259+ } ) ;
260+
261+ const pubkey = new Uint8Array ( 32 ) . fill ( 1 ) ;
262+ const signature = new Uint8Array ( 64 ) . fill ( 3 ) ;
263+ const tapScriptSig = createTapScriptSig ( pubkey , signature , leafHash ) ;
264+
265+ const input : PsbtInput = {
266+ tapLeafScript : [ tapLeafScript ] ,
267+ tapScriptSig : [ tapScriptSig ] ,
268+ } ;
269+
270+ // Request a different leaf hash that doesn't exist
271+ const nonExistentLeafHash = new Uint8Array ( 32 ) . fill ( 88 ) ;
272+
273+ assert . throws (
274+ ( ) => tapScriptFinalizer ( 7 , input , nonExistentLeafHash ) ,
275+ / C a n n o t f i n a l i z e t a p r o o t i n p u t # 7 \. S i g n a t u r e f o r t a p l e a f s c r i p t n o t f o u n d \. / ,
276+ ) ;
277+ } ) ;
278+
279+ it ( 'should throw error when witness stack construction fails' , ( ) => {
280+ const script = new Uint8Array ( [
281+ 0x20 ,
282+ ...new Uint8Array ( 32 ) . fill ( 1 ) ,
283+ 0xac ,
284+ ] ) ;
285+ const controlBlock = new Uint8Array ( 33 ) . fill ( 2 ) ;
286+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
287+
288+ const leafHash = tapleafHash ( {
289+ output : script ,
290+ version : LEAF_VERSION_TAPSCRIPT ,
291+ } ) ;
292+
293+ const pubkey = new Uint8Array ( 32 ) . fill ( 1 ) ;
294+ const signature = new Uint8Array ( 64 ) . fill ( 3 ) ;
295+ const tapScriptSig = createTapScriptSig ( pubkey , signature , leafHash ) ;
296+
297+ const input : PsbtInput = {
298+ tapLeafScript : [ tapLeafScript ] ,
299+ tapScriptSig : [ tapScriptSig ] ,
300+ } ;
301+
302+ // This test verifies the try-catch block wraps errors properly
303+ const result = tapScriptFinalizer ( 0 , input ) ;
304+ assert . ok ( result . finalScriptWitness ) ;
305+ } ) ;
306+ } ) ;
307+
308+ describe ( 'signature sorting' , ( ) => {
309+ it ( 'should handle multiple signatures for the same leaf' , ( ) => {
310+ // Create a script that expects multiple signatures
311+ const pubkey1 = new Uint8Array ( 32 ) . fill ( 1 ) ;
312+ const pubkey2 = new Uint8Array ( 32 ) . fill ( 2 ) ;
313+
314+ // Script with two pubkeys
315+ const script = tools . concat ( [
316+ new Uint8Array ( [ 0x20 ] ) ,
317+ pubkey1 ,
318+ new Uint8Array ( [ 0x20 ] ) ,
319+ pubkey2 ,
320+ new Uint8Array ( [ 0xac ] ) ,
321+ ] ) ;
322+
323+ const controlBlock = new Uint8Array ( 33 ) . fill ( 3 ) ;
324+ const tapLeafScript = createTapLeafScript ( script , controlBlock ) ;
325+
326+ const leafHash = tapleafHash ( {
327+ output : script ,
328+ version : LEAF_VERSION_TAPSCRIPT ,
329+ } ) ;
330+
331+ const signature1 = new Uint8Array ( 64 ) . fill ( 10 ) ;
332+ const signature2 = new Uint8Array ( 64 ) . fill ( 20 ) ;
333+
334+ const tapScriptSig1 = createTapScriptSig ( pubkey1 , signature1 , leafHash ) ;
335+ const tapScriptSig2 = createTapScriptSig ( pubkey2 , signature2 , leafHash ) ;
336+
337+ const input : PsbtInput = {
338+ tapLeafScript : [ tapLeafScript ] ,
339+ tapScriptSig : [ tapScriptSig1 , tapScriptSig2 ] ,
340+ } ;
341+
342+ const result = tapScriptFinalizer ( 0 , input ) ;
343+
344+ assert . ok ( result . finalScriptWitness ) ;
345+ assert . ok ( result . finalScriptWitness instanceof Uint8Array ) ;
346+ assert . ok ( result . finalScriptWitness . length > 0 ) ;
347+ } ) ;
348+ } ) ;
349+ } ) ;
0 commit comments