@@ -3,7 +3,8 @@ import { describe, expect, it } from "vitest";
33
44import {
55 assertAuthAnchorOpReturn ,
6- findAuthAnchorOpReturn ,
6+ countHtlcOutputs ,
7+ readAuthAnchorOpReturn ,
78} from "../assertAuthAnchorOpReturn" ;
89
910const ANCHOR_HASH = "ab" . repeat ( 32 ) ;
@@ -133,100 +134,104 @@ describe("assertAuthAnchorOpReturn", () => {
133134 } ) ;
134135} ) ;
135136
136- describe ( "findAuthAnchorOpReturn" , ( ) => {
137- it ( "returns {vout, hash} for a single-vault tx (HTLC@0, OP_RETURN@1)" , ( ) => {
138- const txHex = buildTxHex ( [ htlcOutput ( ) , opReturnOutput ( ANCHOR_HASH ) ] ) ;
139- expect ( findAuthAnchorOpReturn ( txHex ) ) . toEqual ( {
140- vout : 1 ,
141- hash : ANCHOR_HASH ,
142- } ) ;
137+ describe ( "countHtlcOutputs" , ( ) => {
138+ it ( "counts 1 for a single-vault auth-anchored tx [HTLC, OP_RETURN, anchor]" , ( ) => {
139+ const txHex = buildTxHex ( [
140+ htlcOutput ( ) ,
141+ opReturnOutput ( ANCHOR_HASH ) ,
142+ htlcOutput ( ) ,
143+ ] ) ;
144+ expect ( countHtlcOutputs ( txHex ) ) . toBe ( 1 ) ;
143145 } ) ;
144146
145- it ( "returns {vout, hash} for a two-vault tx (HTLCs@0,1, OP_RETURN@2)" , ( ) => {
147+ it ( "counts 1 for a single-vault tx with no auth anchor [HTLC, anchor]" , ( ) => {
148+ const txHex = buildTxHex ( [ htlcOutput ( ) , htlcOutput ( ) ] ) ;
149+ expect ( countHtlcOutputs ( txHex ) ) . toBe ( 1 ) ;
150+ } ) ;
151+
152+ it ( "counts 2 for a two-vault auth-anchored tx [HTLC, HTLC, OP_RETURN, anchor]" , ( ) => {
146153 const txHex = buildTxHex ( [
147154 htlcOutput ( ) ,
148155 htlcOutput ( ) ,
149156 opReturnOutput ( ANCHOR_HASH ) ,
157+ htlcOutput ( ) ,
150158 ] ) ;
151- expect ( findAuthAnchorOpReturn ( txHex ) ) . toEqual ( {
152- vout : 2 ,
153- hash : ANCHOR_HASH ,
154- } ) ;
159+ expect ( countHtlcOutputs ( txHex ) ) . toBe ( 2 ) ;
155160 } ) ;
156161
157- it ( "returns {vout, hash} even when followed by a CPFP-anchor-like output" , ( ) => {
158- // Real funded Pre-PegIns also carry a CPFP anchor / change output
159- // after the OP_RETURN. The finder must locate the anchor regardless
160- // of what comes after.
162+ it ( "counts 2 for a two-vault tx with no auth anchor [HTLC, HTLC, anchor]" , ( ) => {
163+ const txHex = buildTxHex ( [ htlcOutput ( ) , htlcOutput ( ) , htlcOutput ( ) ] ) ;
164+ expect ( countHtlcOutputs ( txHex ) ) . toBe ( 2 ) ;
165+ } ) ;
166+
167+ it ( "stops at the first OP_RETURN — a trailing extra OP_RETURN does not change the count" , ( ) => {
168+ // [HTLC, OP_RETURN, OP_RETURN]: the second OP_RETURN sits in the
169+ // last (CPFP-anchor) slot and is irrelevant; the HTLC count is 1.
161170 const txHex = buildTxHex ( [
162171 htlcOutput ( ) ,
163172 opReturnOutput ( ANCHOR_HASH ) ,
164- htlcOutput ( ) ,
173+ opReturnOutput ( OTHER_HASH ) ,
165174 ] ) ;
166- expect ( findAuthAnchorOpReturn ( txHex ) ) . toEqual ( {
167- vout : 1 ,
168- hash : ANCHOR_HASH ,
169- } ) ;
175+ expect ( countHtlcOutputs ( txHex ) ) . toBe ( 1 ) ;
170176 } ) ;
171177
172- it ( "strips a leading 0x prefix from the funded tx hex" , ( ) => {
173- const txHex = buildTxHex ( [ htlcOutput ( ) , opReturnOutput ( ANCHOR_HASH ) ] ) ;
174- expect ( findAuthAnchorOpReturn ( `0x${ txHex } ` ) ) . toEqual ( {
175- vout : 1 ,
176- hash : ANCHOR_HASH ,
177- } ) ;
178+ it ( "throws when the tx has fewer than 2 outputs" , ( ) => {
179+ const txHex = buildTxHex ( [ htlcOutput ( ) ] ) ;
180+ expect ( ( ) => countHtlcOutputs ( txHex ) ) . toThrow ( / a t l e a s t 2 o u t p u t s / ) ;
178181 } ) ;
179182
180- it ( "returns undefined for a legacy (non-auth-anchored) tx with no OP_RETURN " , ( ) => {
181- const txHex = buildTxHex ( [ htlcOutput ( ) , htlcOutput ( ) ] ) ;
182- expect ( findAuthAnchorOpReturn ( txHex ) ) . toBeUndefined ( ) ;
183+ it ( "strips a leading 0x prefix from the funded tx hex " , ( ) => {
184+ const txHex = buildTxHex ( [ htlcOutput ( ) , opReturnOutput ( ANCHOR_HASH ) ] ) ;
185+ expect ( countHtlcOutputs ( `0x ${ txHex } ` ) ) . toBe ( 1 ) ;
183186 } ) ;
187+ } ) ;
184188
185- it ( "throws when more than one OP_RETURN+PUSH32 output is present (ambiguous)" , ( ) => {
186- // A well-formed Pre-PegIn carries exactly one auth-anchor commitment.
187- // Multiple matches are malformed input — refuse rather than guess.
189+ describe ( "readAuthAnchorOpReturn" , ( ) => {
190+ it ( "returns the hash when vout points at an OP_RETURN PUSH32 output" , ( ) => {
188191 const txHex = buildTxHex ( [
189192 htlcOutput ( ) ,
190193 opReturnOutput ( ANCHOR_HASH ) ,
191- opReturnOutput ( OTHER_HASH ) ,
194+ htlcOutput ( ) ,
192195 ] ) ;
193- expect ( ( ) => findAuthAnchorOpReturn ( txHex ) ) . toThrow (
194- / O P _ R E T U R N P U S H 3 2 o u t p u t s / ,
195- ) ;
196+ expect ( readAuthAnchorOpReturn ( txHex , 1 ) ) . toBe ( ANCHOR_HASH ) ;
197+ } ) ;
198+
199+ it ( "returns undefined when the output at vout is not an OP_RETURN (no auth anchor)" , ( ) => {
200+ // [HTLC, anchor] — vout 1 is the CPFP anchor, not an OP_RETURN.
201+ const txHex = buildTxHex ( [ htlcOutput ( ) , htlcOutput ( ) ] ) ;
202+ expect ( readAuthAnchorOpReturn ( txHex , 1 ) ) . toBeUndefined ( ) ;
203+ } ) ;
204+
205+ it ( "returns undefined when vout is out of bounds" , ( ) => {
206+ const txHex = buildTxHex ( [ htlcOutput ( ) , htlcOutput ( ) ] ) ;
207+ expect ( readAuthAnchorOpReturn ( txHex , 5 ) ) . toBeUndefined ( ) ;
196208 } ) ;
197209
198- it ( "ignores OP_RETURN outputs with non-zero value" , ( ) => {
199- // Non-standard OP_RETURNs (non-zero value) cannot have been emitted
200- // by the WASM builder — skip them rather than treat them as a hit.
210+ it ( "reads only the output at vout — an OP_RETURN elsewhere is ignored" , ( ) => {
211+ // OP_RETURN at vout 2, but vout 1 is a plain output → no auth anchor.
201212 const txHex = buildTxHex ( [
202213 htlcOutput ( ) ,
203- opReturnOutput ( ANCHOR_HASH , /* value */ 546 ) ,
214+ htlcOutput ( ) ,
215+ opReturnOutput ( ANCHOR_HASH ) ,
204216 ] ) ;
205- expect ( findAuthAnchorOpReturn ( txHex ) ) . toBeUndefined ( ) ;
217+ expect ( readAuthAnchorOpReturn ( txHex , 1 ) ) . toBeUndefined ( ) ;
206218 } ) ;
207219
208- it ( "ignores OP_RETURN outputs with non-32-byte payloads" , ( ) => {
209- // OP_RETURN PUSH16 <16 bytes> — wrong push opcode, shorter payload.
210- const tooShort = {
211- scriptHex : `6a10${ "ab" . repeat ( 16 ) } ` ,
212- value : 0 ,
213- } ;
214- const txHex = buildTxHex ( [ htlcOutput ( ) , tooShort ] ) ;
215- expect ( findAuthAnchorOpReturn ( txHex ) ) . toBeUndefined ( ) ;
220+ it ( "throws when the output at vout is an OP_RETURN but not a clean 32-byte push" , ( ) => {
221+ // OP_RETURN PUSH16 <16 bytes> — an OP_RETURN, but malformed as an
222+ // auth anchor. Must throw, not silently degrade to "no anchor".
223+ const malformed = { scriptHex : `6a10${ "ab" . repeat ( 16 ) } ` , value : 0 } ;
224+ const txHex = buildTxHex ( [ htlcOutput ( ) , malformed ] ) ;
225+ expect ( ( ) => readAuthAnchorOpReturn ( txHex , 1 ) ) . toThrow ( / m a l f o r m e d / ) ;
216226 } ) ;
217227
218- it ( "returns undefined for unparseable hex" , ( ) => {
219- expect ( findAuthAnchorOpReturn ( "not a real tx hex" ) ) . toBeUndefined ( ) ;
228+ it ( "strips a leading 0x prefix from the funded tx hex" , ( ) => {
229+ const txHex = buildTxHex ( [ htlcOutput ( ) , opReturnOutput ( ANCHOR_HASH ) ] ) ;
230+ expect ( readAuthAnchorOpReturn ( `0x${ txHex } ` , 1 ) ) . toBe ( ANCHOR_HASH ) ;
220231 } ) ;
221232
222233 it ( "normalizes the hash to lowercase regardless of input case" , ( ) => {
223- // OP_RETURN payloads are raw bytes; the hex serialization picks a
224- // case. Normalize at the boundary so downstream byte-equality holds.
225- const upperHash = "CD" . repeat ( 32 ) ;
226- const txHex = buildTxHex ( [ htlcOutput ( ) , opReturnOutput ( upperHash ) ] ) ;
227- expect ( findAuthAnchorOpReturn ( txHex ) ) . toEqual ( {
228- vout : 1 ,
229- hash : "cd" . repeat ( 32 ) ,
230- } ) ;
234+ const txHex = buildTxHex ( [ htlcOutput ( ) , opReturnOutput ( "CD" . repeat ( 32 ) ) ] ) ;
235+ expect ( readAuthAnchorOpReturn ( txHex , 1 ) ) . toBe ( "cd" . repeat ( 32 ) ) ;
231236 } ) ;
232237} ) ;
0 commit comments