Skip to content

Conversation

@SatsAndSports
Copy link
Contributor

@SatsAndSports SatsAndSports commented Oct 18, 2025

Update 2025-10-25: Abandoning this for various reasons.

Description

While implementing the unidirectional channel proposal, I found that I couldn't swap with P2PK SIG_ALL. To fix that, I had to make more and more changes, especially to ensure that locktime and refunds work as expected.

I mostly ignored HTLCs+SIG_ALL in this. I'll think about that next.

See also the separate comment below with a full description of this PR written by Claude

The problem

There is a validate_sig_flag and verify_sig_all, and they are called from process_swap_request and verify_melt_request. But in reality the code doesn't reach those lines in the case of SIG_ALL

Melt and swap call verify_inputs, which called verify_proofs, and verify_proofs rejects SIG_ALL proofs (as it doesn't know what the outputs are and therefore it can't verify the SIG_ALL).

Therefore, SIG_ALL transactions don't work in melt or swap; they are rejected before it reaches the code which would validate them.

Changes in this PR, to address the SIG_ALL issue:

This PR does the following:

  • call verify_inputs only for SIG_INPUTS proofs, skipping SIG_ALL. Renamed to verify_p2pk_for_sig_input to make this clear. verify_proofs now silently accepts any SIG_ALL input without checking the signature, as that checking is already done elsewhere
  • the checking of SIG_ALL signatures is already done in verify_sig_all. However, before this PR, it worked only with pre-locktime pubkeys; it didn't know that it should use refund keys after the locktime has expired. This PR therefore refactors verify_sig_all such that it uses the locktime to decide which set of keys (refund keys, or [data]+Secret.data.pubkeys) should be used.
  • there was a function called enforce_sig_flag. It included some complex logic, but in practice most of its response was ignored; the rest of the library only used enforce_sig_flag to do the binary test "does this set of proofs include at least one SIG_ALL?". This PR therefore renames it to has_at_least_one_sig_all_proof to simply this.

While I've tested this with P2PKs inside swap and melt, and my Spillman channel code (not in this PR), I haven't thought so much about HTLCs. So maybe more work is needed for HTLCs


Suggested CHANGELOG Updates

  • enabled P2PK with SIG_ALL in swap and melt, including switching to refund keys if the locktime has expired

...

FIXED

  • enabled SIG_ALL for swap and melt, by modifying verify_proofs to accept SIG_ALL proofs. The signature verification is done else where
  • enabled SIG_ALL for post-locktime swaps and melts, by using the refund keys in that case
  • added two tests of SIG_ALL, one for swap and one for melt

Checklist

@SatsAndSports
Copy link
Contributor Author

just doesn't work for me, so I can't really run just final-check. I tried to run the individual parts invidually. So sorry in advance for any formatting issues and so on!

@SatsAndSports SatsAndSports changed the title SIG_ALL: verify_proofs was too strict, blocking swap and mint from using SIG_ALL [broken PR at the moment] SIG_ALL: verify_proofs was too strict, blocking swap and mint from using SIG_ALL Oct 19, 2025
@SatsAndSports
Copy link
Contributor Author

The description above was written by a human. Here's how Claude Code summarizes all these changes:

==============

  This PR implements comprehensive support for SigFlag::SigAll (NUT-11) with locktime-based refund mechanisms. The implementation enables
  payment channel constructs like Spillman channels.                                                     
                                                                                                                                                                                                                   
  Core Architecture                                                                                      
                                                                                                                                                                                                                   
  Two-Level Verification Pattern:                                                                                                                                                                                  
  - Individual proof level (verify_p2pk_for_sig_input): For SigInputs proofs only                        
  - Transaction level (verify_sig_all): For SigAll proofs, verifying signatures over entire transaction
                                                                                                                                                                                                                   
  Key Changes by File                                                                                                                                                                                              
                                                                                                         
  1. crates/cashu/src/nuts/nut11/mod.rs (255 lines changed)                                                                                                                                                        
                                                                                                                                                                                                                   
  Renamed and clarified verification:                                                                    
  - verify_p2pk() → verify_p2pk_for_sig_input()                                                                                                                                                                    
  - Added assertion to enforce it's only called for SigInputs proofs                                                                                                                                               
  - Added extensive tracing for debugging                                                                
                                                                                                                                                                                                                   
  New helper function get_sig_all_required_sigs():                                                       
  - Consolidated locktime logic used by both swap and melt operations                                                                                                                                              
  - Returns (Vec<PublicKey>, u64) where second value is required signature count                         
  - Returns (vec![], 0) for "anyone can spend" after locktime with no refund keys                                                                                                                                  
  - Handles three scenarios:                                                                                                                                                                                       
    - Before locktime: Returns normal pubkeys + num_sigs                                                 
    - After locktime with refund_keys: Returns refund_keys + num_sigs_refund
    - After locktime without refund_keys: Returns (vec![], 0) - anyone can spend                                                                                                                                   
                                                                                                                                                                                                                   
  Simplified transaction detection:                                                                      
  - Replaced enforce_sig_flag() → has_at_least_one_sig_all_proof()                                       
  - Changed from returning complex EnforceSigFlag struct to simple boolean                                                                                                                                         
  - Works with any mix of proof types (regular, P2PK, HTLC)                                                                                                                                                        
                                                                                                         
  Refactored verify_sig_all() for both SwapRequest and MeltRequest:                                                                                                                                                
  - Removed duplicate get_sig_all_required_sigs() methods (eliminated ~120 lines of duplication)
  - Now uses shared helper function with locktime support                                                
  - Clearer variable names: relevant_pubkeys, relevant_num_sigs_required
  - Handles "anyone can spend" case when required_sigs == 0

  Updated tests:
  - Changed all test calls from verify_p2pk() to verify_p2pk_for_sig_input()

  2. crates/cdk/src/mint/mod.rs (290 lines added)

  Enhanced verify_proofs():
  - Added check for SigFlag::SigAll in P2PK proofs
  - Skips individual verification for SigAll proofs (verified at transaction level)
  - Calls verify_p2pk_for_sig_input() only for SigInputs proofs
  - Added extensive tracing throughout

  New comprehensive tests (test_swap_with_sig_all, test_melt_with_sig_all):**
  - Test 2-of-2 multisig with SigAll
  - Verify unsigned requests fail
  - Verify single-signature requests fail
  - Verify requests succeed with both signatures
  - Cover both swap and melt operations

  3. crates/cdk/src/mint/swap.rs (14 lines changed)

  Updated validate_sig_flag():
  - Changed from enforce_sig_flag() to has_at_least_one_sig_all_proof()
  - Added detailed comments explaining when verification happens
  - Calls verify_sig_all() for transaction-level verification

  4. crates/cdk/src/mint/melt.rs (13 lines changed)

  Updated melt verification:
  - Same pattern as swap.rs
  - Changed from enforce_sig_flag() to has_at_least_one_sig_all_proof()
  - Added clarifying comments about verification levels

  Eliminated Code Duplication

  - Before: Both SwapRequest and MeltRequest had identical ~60 line get_sig_all_required_sigs() methods
  - After: Single shared helper function in nut11/mod.rs
  - Savings: ~120 lines of duplicate code removed

  Locktime Refund Logic

  The implementation correctly handles time-based spending conditions:

  1. Before locktime: All parties must sign (e.g., Alice + Bob in 2-of-2)
  2. After locktime with refund_keys: Only refund key holders can spend (e.g., Alice alone)
  3. After locktime without refund_keys: Anyone can spend

  Testing Coverage

  - All existing tests updated and passing
  - New integration tests for SigAll swaps and melts 
  - Tests verify multi-signature requirements work correctly
  - Proper rejection of insufficient signatures

  Benefits

  ✅ Enables payment channels: Spillman channels and similar constructs now supported✅ Cleaner architecture: Clear separation between
  individual and transaction-level verification✅ Better maintainability: Eliminated significant code duplication✅ Locktime support:
  Time-based refund mechanisms work correctly✅ Well-tested: Comprehensive test coverage for SigAll functionality

@SatsAndSports SatsAndSports changed the title [broken PR at the moment] SIG_ALL: verify_proofs was too strict, blocking swap and mint from using SIG_ALL SIG_ALL, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys Oct 19, 2025
@SatsAndSports SatsAndSports changed the title SIG_ALL, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys Oct 19, 2025
Copy link

@kwsantiago kwsantiago left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments. Also you have decent test coverage but consider adding tests for:

  • Locktime expired with refund keys
  • Locktime expired without refund keys (anyone can spend)
  • Mixed SigAll and SigInputs proofs in the same transaction
  • SigAll with insufficient signatures at the transaction level
  • HTLC proofs with the SigAll flag

let msg: &[u8] = self.secret.as_bytes();

// Ensure this is only called for SigInputs proofs
assert_eq!(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert_eq! will panic and crash the mint if the function is called incorrectly. In production, this could be exploited to DoS the mint.

Please replace with proper error handling, something like:

if spending_conditions.sig_flag != SigFlag::SigInputs {
    return Err(Error::IncorrectSigFlag); // or appropriate error type
}

Ok((refund_keys, refund_sigs_required))
} else {
// Locktime passed but no refund keys - anyone can spend (0 sigs required)
Ok((vec![], 0))
Copy link

@kwsantiago kwsantiago Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new get_sig_all_required_sigs() function introduces "anyone can spend" behavior when:

  • Locktime has passed
  • AND no refund keys are present
  • Returns required_sigs = 0 which causes immediate success

Questions

  1. Is this behavior intentional and documented in the NUT-11 spec?
  2. Could existing proofs with locktime-only (no refund keys) become unexpectedly spendable?
  3. Should there be migration warnings for wallets that may have created such proofs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of Nut-11 is that yes, post-locktime proofs without a refund tag are anyone-can-spend:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to add a new function called something like enforce_all_nut10_conditions. This would have full responsibility for checking anything related to NUT-10:

  • is there any proof with a nut-10 secret?
  • if yes, are the conditions (pre-images, locktimes, signatures, ... everything) for the proof satisfied?
  • if there any SIG_ALL, don't forget all the special stuff

This function would NOT be responsible for anything that is unrelated to nut-10:

  • has this proof already beent spent (or PENDING)?
  • was it signed with the correct mint key?

What do you think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. One suggestion is to make sure the function name clearly indicates it returns a bool or Result, verify_nut10_conditions() or validate_nut10_conditions() might be slightly clearer than enforce.


/// Get the signature flag that should be enforced for a set of proofs and the
/// public keys that signatures are valid for
pub fn enforce_sig_flag(proofs: Proofs) -> EnforceSigFlag {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. If any external code was using enforce_sig_flag() or the EnforceSigFlag type, it will break.

Should this be noted in CHANGELOG as a breaking change?

Copy link
Contributor Author

@SatsAndSports SatsAndSports Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I had forgotten that publicly-facing functions shouldn't be changed!

I was focussed on getting P2PK+SIG_ALL to work, before and after the locktime, and I didn't think about other things like that

.unwrap_or_default()
.try_into()?;

if conditions.sig_flag == nuts::nut11::SigFlag::SigAll {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good fix but the conditions are extracted by parsing tags. If the tags are malformed or missing, this could fail silently (empty default). Might want to add validation that tags were actually present for P2PK secrets.

let (relevant_pubkeys, relevant_num_sigs_required) = get_sig_all_required_sigs(self.inputs())?;

if relevant_num_sigs_required == 0 {
return Ok(());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This early exit for "anyone can spend" looks correct but needs to be verified against NUT-11 spec for locktime behavior.

@SatsAndSports SatsAndSports changed the title SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys fix for SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys Oct 21, 2025
@SatsAndSports SatsAndSports changed the title fix for SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys (ABANDONED) fix for SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys Oct 25, 2025
@SatsAndSports SatsAndSports changed the title (ABANDONED) fix for SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys (ABANDONED, replaced by PR 1212) fix for SIG_ALL and P2PK, ensuring it works with swap and melt. Also, after the locktime, support SIG_ALL with refund keys Oct 26, 2025
@SatsAndSports
Copy link
Contributor Author

SatsAndSports commented Oct 26, 2025

I'm abandoning this PR and replacing it with #1212

I wrote this PR to help fix some bugs that were affecting the Spillman channel, but I discovered new bugs later (in this PR, and also in the existing code), so it made sense to redo it.

Also, the 'swap_saga' PR which was just merged also made this PR invalid

Thanks to anyone who reviewed this one, e.g. @kwsantiago , I hope I've addressed your concerns in the new PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants