@@ -94,6 +94,13 @@ impl HTLCWitness {
9494
9595impl Proof {
9696 /// Verify HTLC
97+ ///
98+ /// Per NUT-14, there are two spending pathways:
99+ /// 1. Receiver path (preimage + pubkeys): ALWAYS available
100+ /// 2. Sender/Refund path (refund keys, no preimage): available AFTER locktime
101+ ///
102+ /// The verification tries to determine which path is being used based on
103+ /// the witness provided, then validates accordingly.
97104 pub fn verify_htlc ( & self ) -> Result < ( ) , Error > {
98105 let secret: Secret = self . secret . clone ( ) . try_into ( ) ?;
99106 let spending_conditions: Conditions = secret
@@ -111,65 +118,86 @@ impl Proof {
111118 return Err ( Error :: IncorrectSecretKind ) ;
112119 }
113120
114- // Get the appropriate spending conditions based on locktime
121+ // Get the spending requirements (includes both receiver and refund paths)
115122 let now = unix_time ( ) ;
116123 let requirements =
117124 super :: nut10:: get_pubkeys_and_required_sigs ( & secret, now) . map_err ( Error :: NUT11 ) ?;
118125
119- // While a Witness is usually needed in a P2PK or HTLC proof, it's not
120- // always needed. If we are past the locktime, and there are no refund
121- // keys, then the proofs are anyone-can-spend:
122- // NUT-11: "If the tag locktime is the unix time and the mint's local
123- // clock is greater than locktime, the Proof becomes spendable
124- // by anyone, except if [there are no refund keys]"
125- // Therefore, this function should not extract any Witness unless it
126- // is needed to get a preimage or signatures.
127-
128- // If preimage is needed (before locktime), verify it
129- if requirements. preimage_needed {
130- // Extract HTLC witness
131- let htlc_witness = match & self . witness {
132- Some ( Witness :: HTLCWitness ( witness) ) => witness,
133- _ => return Err ( Error :: IncorrectSecretKind ) ,
134- } ;
135-
136- // Verify preimage using shared function
137- super :: nut10:: verify_htlc_preimage ( htlc_witness, & secret) ?;
138- }
139-
140- if requirements. required_sigs == 0 {
141- return Ok ( ( ) ) ;
142- }
143-
144- // if we get here, the preimage check (if it was needed) has been done
145- // and we know that at least one signature is required. So, we extract
146- // the witness.signatures and count them:
147-
148- // Extract witness signatures
126+ // Try to extract HTLC witness - must be correct type
149127 let htlc_witness = match & self . witness {
150128 Some ( Witness :: HTLCWitness ( witness) ) => witness,
151- _ => return Err ( Error :: IncorrectSecretKind ) ,
129+ _ => {
130+ // Wrong witness type or no witness
131+ // If refund path is available with 0 required sigs, anyone can spend
132+ if let Some ( refund_path) = & requirements. refund_path {
133+ if refund_path. required_sigs == 0 {
134+ return Ok ( ( ) ) ;
135+ }
136+ }
137+ return Err ( Error :: IncorrectSecretKind ) ;
138+ }
152139 } ;
153- let witness_signatures = htlc_witness
154- . signatures
155- . as_ref ( )
156- . ok_or ( Error :: SignaturesNotProvided ) ?;
157140
158- // Convert signatures from strings
159- let signatures: Vec < Signature > = witness_signatures
160- . iter ( )
161- . map ( |s| Signature :: from_str ( s) )
162- . collect :: < Result < Vec < _ > , _ > > ( ) ?;
163-
164- // Count valid signatures using relevant_pubkeys
165- let msg: & [ u8 ] = self . secret . as_bytes ( ) ;
166- let valid_sig_count = valid_signatures ( msg, & requirements. pubkeys , & signatures) ?;
167-
168- // Check if we have enough valid signatures
169- if valid_sig_count >= requirements. required_sigs {
170- Ok ( ( ) )
141+ // Try to verify the preimage and capture the specific error if it fails
142+ let preimage_result = super :: nut10:: verify_htlc_preimage ( htlc_witness, & secret) ;
143+
144+ // Determine which path to use:
145+ // - If preimage is valid → use receiver path (always available)
146+ // - If preimage is invalid/missing → try refund path (if available)
147+ if preimage_result. is_ok ( ) {
148+ // Receiver path: preimage valid, now check signatures against pubkeys
149+ if requirements. required_sigs == 0 {
150+ return Ok ( ( ) ) ;
151+ }
152+
153+ let witness_signatures = htlc_witness
154+ . signatures
155+ . as_ref ( )
156+ . ok_or ( Error :: SignaturesNotProvided ) ?;
157+
158+ let signatures: Vec < Signature > = witness_signatures
159+ . iter ( )
160+ . map ( |s| Signature :: from_str ( s) )
161+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
162+
163+ let msg: & [ u8 ] = self . secret . as_bytes ( ) ;
164+ let valid_sig_count = valid_signatures ( msg, & requirements. pubkeys , & signatures) ?;
165+
166+ if valid_sig_count >= requirements. required_sigs {
167+ Ok ( ( ) )
168+ } else {
169+ Err ( Error :: NUT11 ( super :: nut11:: Error :: SpendConditionsNotMet ) )
170+ }
171+ } else if let Some ( refund_path) = & requirements. refund_path {
172+ // Refund path: preimage not valid/provided, but locktime has passed
173+ // Check signatures against refund keys
174+ if refund_path. required_sigs == 0 {
175+ // Anyone can spend (locktime passed, no refund keys)
176+ return Ok ( ( ) ) ;
177+ }
178+
179+ let witness_signatures = htlc_witness
180+ . signatures
181+ . as_ref ( )
182+ . ok_or ( Error :: SignaturesNotProvided ) ?;
183+
184+ let signatures: Vec < Signature > = witness_signatures
185+ . iter ( )
186+ . map ( |s| Signature :: from_str ( s) )
187+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
188+
189+ let msg: & [ u8 ] = self . secret . as_bytes ( ) ;
190+ let valid_sig_count = valid_signatures ( msg, & refund_path. pubkeys , & signatures) ?;
191+
192+ if valid_sig_count >= refund_path. required_sigs {
193+ Ok ( ( ) )
194+ } else {
195+ Err ( Error :: NUT11 ( super :: nut11:: Error :: SpendConditionsNotMet ) )
196+ }
171197 } else {
172- Err ( Error :: NUT11 ( super :: nut11:: Error :: SpendConditionsNotMet ) )
198+ // No valid preimage and refund path not available (locktime not passed)
199+ // Return the specific error from preimage verification
200+ preimage_result
173201 }
174202 }
175203
@@ -411,25 +439,26 @@ mod tests {
411439
412440 /// Tests that verify_htlc requires BOTH locktime expired AND no refund keys for "anyone can spend".
413441 ///
414- /// This test catches the mutation that replaces `&&` with `||` at line 83.
415- /// The logic should be: (locktime expired AND no refund keys) → anyone can spend.
416- /// If mutated to OR, it would allow spending when locktime passed even if refund keys exist.
442+ /// This test verifies that when locktime has passed and refund keys are present,
443+ /// a signature from the refund keys is required (not anyone-can-spend).
417444 ///
418- /// Mutant testing: Catches mutations that replace `&&` with `||` in the locktime check.
445+ /// Per NUT-14: After locktime, the refund path requires signatures from refund keys.
446+ /// The "anyone can spend" case only applies when locktime passed AND no refund keys.
419447 #[ test]
420448 fn test_htlc_locktime_and_refund_keys_logic ( ) {
421449 use crate :: nuts:: nut01:: PublicKey ;
422450 use crate :: nuts:: nut11:: Conditions ;
423451
424- let preimage_bytes = [ 42u8 ; 32 ] ; // 32-byte preimage
425- let hash = Sha256Hash :: hash ( & preimage_bytes ) ;
452+ let correct_preimage_bytes = [ 42u8 ; 32 ] ; // 32-byte preimage
453+ let hash = Sha256Hash :: hash ( & correct_preimage_bytes ) ;
426454 let hash_str = hash. to_string ( ) ;
427455
456+ // Use WRONG preimage to force using refund path (not receiver path)
457+ let wrong_preimage_bytes = [ 99u8 ; 32 ] ;
458+
428459 // Test: Locktime has passed (locktime=1) but refund keys ARE present
429- // With correct logic (&&): Since refund_keys.is_none() is false, the "anyone can spend"
430- // path is NOT taken, so signature is required
431- // With mutation (||): Since locktime.lt(&unix_time()) is true, it WOULD take the
432- // "anyone can spend" path immediately - WRONG!
460+ // Since we provide wrong preimage, receiver path fails, so we try refund path.
461+ // Refund path with refund keys present should require a signature.
433462 let refund_pubkey = PublicKey :: from_hex (
434463 "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2" ,
435464 )
@@ -448,8 +477,8 @@ mod tests {
448477 let secret: SecretString = nut10_secret. try_into ( ) . unwrap ( ) ;
449478
450479 let htlc_witness = HTLCWitness {
451- preimage : hex:: encode ( & preimage_bytes ) ,
452- signatures : None , // No signature provided
480+ preimage : hex:: encode ( & wrong_preimage_bytes ) , // Wrong preimage!
481+ signatures : None , // No signature provided
453482 } ;
454483
455484 let proof = Proof {
@@ -464,13 +493,15 @@ mod tests {
464493 dleq : None ,
465494 } ;
466495
467- // Should FAIL because even though locktime passed, refund keys are present
468- // so the "anyone can spend" shortcut shouldn't apply. A signature is required.
469- // With && this correctly fails. With || it would incorrectly pass.
496+ // Should FAIL because:
497+ // 1. Wrong preimage means receiver path fails
498+ // 2. Falls back to refund path (locktime passed)
499+ // 3. Refund keys are present, so signature is required
500+ // 4. No signature provided
470501 let result = proof. verify_htlc ( ) ;
471502 assert ! (
472503 result. is_err( ) ,
473- "Should fail when locktime passed but refund keys present without signature"
504+ "Should fail when using refund path with refund keys but no signature"
474505 ) ;
475506 }
476507}
0 commit comments