Skip to content

New p2pk and htlc rules, sig all support#24

Merged
d4rp4t merged 10 commits intoKukks:masterfrom
d4rp4t:chore/new-p2pk-htlc-rules
Jan 18, 2026
Merged

New p2pk and htlc rules, sig all support#24
d4rp4t merged 10 commits intoKukks:masterfrom
d4rp4t:chore/new-p2pk-htlc-rules

Conversation

@d4rp4t
Copy link
Collaborator

@d4rp4t d4rp4t commented Dec 30, 2025

cashubtc/nuts#302
cashubtc/nuts#315

Summary by CodeRabbit

  • New Features

    • Unified signing/verification handler for multi-proof "SIG_ALL" sessions supporting both normal and timelocked refund flows.
  • API Changes

    • HTLC refund behavior refined and preimage omitted when absent in JSON.
    • Refund signature threshold made configurable.
    • Witness generation APIs now return nullable results to signal timelock/refund outcomes.
    • Nut10 proof secrets implement value-based equality.
  • Tests

    • Added tests covering new P2PK rules and SIG_ALL scenarios.
  • Chores

    • Public type name casing normalized.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Renames P2PkBuilder → P2PKBuilder across code and tests; adds RefundSignatureThreshold and validation to P2PKBuilder; extends P2PK/HTLC proof secrets with refund-path APIs, nullable witness returns, and signing helpers; introduces SigAllHandler for SIG_ALL multi-proof signing/verification; adds Nut10ProofSecret value equality and minor serialization tweaks.

Changes

Cohort / File(s) Summary
Builder rename & refund threshold
DotNut/P2PKBuilder.cs, DotNut/HTLCBuilder.cs, DotNut.Demo/Program.cs, DotNut.Tests/UnitTest1.cs
Rename P2PkBuilderP2PKBuilder; add int? RefundSignatureThreshold; update Build/Load to emit/parse n_sigs_refund; update inheritance/usages and add Validate() checks.
P2PK proof secret: signing, blind-signing & verification
DotNut/P2PKProofSecret.cs
Add refund-path API (GetAllowedRefundPubkeys(out int? requiredSignatures)); make GenerateWitness/GenerateBlindWitness return nullable P2PKWitness?; add TrySignPath, VerifyPath, TrySignBlindPath; update VerifyWitnessHash to consider normal and refund paths and null-return semantics.
HTLC proof & witness changes
DotNut/HTLCProofSecret.cs, DotNut/HTLCWitness.cs
HTLCProofSecret: add GetAllowedPubkeys(out int requiredSignatures) and alter refund-pubkey logic to signal applicability; HTLCWitness.Preimage made nullable and annotated to be omitted when null in JSON.
New SIG_ALL orchestration
DotNut/SigAllHandler.cs
New SigAllHandler class providing SIG_ALL orchestration: properties for Proofs, PrivKeys, BlindedMessages, HTLCPreimage, MeltQuoteId; TrySign(out string? witness) and helpers GetMessageToSign, VerifySigAllWitness, ValidateFirstProof, CheckIfEqualToFirst.
Nut10 secret equality
DotNut/Nut10ProofSecret.cs
Added value-based equality: Equals(object), Equals(Nut10ProofSecret), GetHashCode(), and ==/!= operators.
Tests & demo updates
DotNut.Tests/UnitTest1.cs, DotNut.Demo/Program.cs
Tests: added Nut11_New_P2PkRules() and Nut11_SIG_ALL(); Demo: updated P2PK builder usages to P2PKBuilder.
HTLC/JSON serialization & minor formatting
DotNut/HTLCWitness.cs, DotNut/JsonConverters/Nut10SecretJsonConverter.cs
Preimage nullability and JsonIgnore(WhenWritingNull) added; removed extra blank lines in Nut10 converter; no logic changes beyond JSON omission when preimage is null.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant SigAllHandler
    participant P2PKProofSecret
    participant HTLCProofSecret

    Client->>SigAllHandler: TrySign(proofs, privKeys, blindedMessages, HTLCPreimage?, meltQuoteId?)
    SigAllHandler->>SigAllHandler: ValidateFirstProof() / GetMessageToSign()
    alt first proof is P2PK
        SigAllHandler->>P2PKProofSecret: GenerateWitness(message, privKeys)
        P2PKProofSecret->>P2PKProofSecret: TrySignPath(normal allowed keys)
        alt normal path succeeds
            P2PKProofSecret-->>SigAllHandler: P2PKWitness
        else normal fails
            P2PKProofSecret->>P2PKProofSecret: GetAllowedRefundPubkeys()
            P2PKProofSecret->>P2PKProofSecret: TrySignPath(refund keys)
            alt refund succeeds
                P2PKProofSecret-->>SigAllHandler: P2PKWitness
            else
                P2PKProofSecret-->>SigAllHandler: null
            end
        end
    else first proof is HTLC
        SigAllHandler->>HTLCProofSecret: GenerateWitness(message, privKeys, preimage?)
        HTLCProofSecret->>HTLCProofSecret: TrySignPath(normal allowed keys)
        alt normal path succeeds
            HTLCProofSecret-->>SigAllHandler: HTLCWitness
        else
            HTLCProofSecret->>HTLCProofSecret: GetAllowedRefundPubkeys()
            HTLCProofSecret->>HTLCProofSecret: TrySignPath(refund keys)
            alt refund succeeds
                HTLCProofSecret-->>SigAllHandler: HTLCWitness
            else
                HTLCProofSecret-->>SigAllHandler: null
            end
        end
    end
    alt witness produced
        SigAllHandler->>SigAllHandler: Serialize witness JSON
        SigAllHandler-->>Client: success + witness
    else no witness
        SigAllHandler-->>Client: failure + null
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Kukks

Poem

🐰 I hopped through builders with a twitch,
P2PK now gleams without a glitch,
Refund counts nest in tidy rows,
SigAll hums where every signature goes—
A little rabbit cheers the switch! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: new P2PK builder class (P2PKBuilder), new HTLC rules (HTLCProofSecret and HTLCWitness updates), and SIG_ALL support (new SigAllHandler class).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
DotNut/HTLCBuilder.cs (2)

34-43: Missing RefundSignatureThreshold in Load method.

The Load method copies properties from innerbuilder but doesn't include the newly added RefundSignatureThreshold. This could cause HTLC secrets with refund thresholds to lose that information when loaded.

🔎 Proposed fix
         return new HTLCBuilder()
         {
             HashLock = hashLock,
             Lock = innerbuilder.Lock,
             Pubkeys = innerbuilder.Pubkeys,
             RefundPubkeys = innerbuilder.RefundPubkeys,
             SignatureThreshold = innerbuilder.SignatureThreshold,
+            RefundSignatureThreshold = innerbuilder.RefundSignatureThreshold,
             SigFlag = innerbuilder.SigFlag,
             Nonce = innerbuilder.Nonce
         };

53-61: Missing RefundSignatureThreshold in Build method.

Similarly, the Build method creates a new P2PKBuilder but doesn't propagate RefundSignatureThreshold. This means HTLC secrets built through this builder won't include the n_sigs_refund tag even if set.

🔎 Proposed fix
         var innerBuilder = new P2PKBuilder()
         {
             Lock = Lock,
             Pubkeys = Pubkeys.ToArray(),
             RefundPubkeys = RefundPubkeys,
             SignatureThreshold = SignatureThreshold,
+            RefundSignatureThreshold = RefundSignatureThreshold,
             SigFlag = SigFlag,
             Nonce = Nonce
         };
DotNut/HTLCProofSecret.cs (1)

198-210: Potential null reference when Preimage is null.

Since HTLCWitness.Preimage is now nullable, calling VerifyPreimage(htlcWitness.Preimage) at line 204 could pass null to VerifyPreimage(string preimage). The VerifyPreimage method at line 102 calls Convert.FromHexString(preimage) which will throw if preimage is null.

🔎 Proposed fix
     public override bool VerifyWitnessHash(byte[] hash, P2PKWitness witness)
     {
         if (witness is not HTLCWitness htlcWitness)
         {
             return false;
         }
-        if (!VerifyPreimage(htlcWitness.Preimage))
+        if (htlcWitness.Preimage is null || !VerifyPreimage(htlcWitness.Preimage))
         {
             return false;
         }

         return base.VerifyWitnessHash(hash, witness);
     }
🧹 Nitpick comments (4)
DotNut/SigAllHandler.cs (2)

10-14: Consider initializing collection properties to avoid null reference issues.

The Proofs, PrivKeys, and BlindedMessages properties are not initialized and could be null. While TrySign does null-check them, initializing to empty collections would be more defensive.

🔎 Proposed fix
-    public List<Proof> Proofs { get; set; }
-    public List<PrivKey> PrivKeys { get; set; }
-    public List<BlindedMessage> BlindedMessages { get; set; }
+    public List<Proof> Proofs { get; set; } = new();
+    public List<PrivKey> PrivKeys { get; set; } = new();
+    public List<BlindedMessage> BlindedMessages { get; set; } = new();

151-154: Avoid catching generic Exception.

Catching all exceptions masks potential bugs and makes debugging harder. Consider catching specific exceptions that GetMessageToSign might throw.

🔎 Proposed fix
-        catch(Exception ex)
+        catch (ArgumentException)
         {
             return false;
         }
DotNut/P2PKProofSecret.cs (2)

89-109: Consider using a List<string> to avoid O(n²) array allocations.

Using Append().ToArray() inside a loop creates a new array on each iteration. For multi-sig scenarios with many keys, this could be inefficient.

🔎 Proposed optimization
 private (bool IsValid, P2PKWitness Witness) TrySignPath(ECPubKey[] allowedKeys, int requiredSignatures, 
     ECPrivKey[] availableKeys, byte[] msg)
 {
     var allowedKeysSet = new HashSet<ECPubKey>(allowedKeys);
-    var result = new P2PKWitness();
+    var signatures = new List<string>();

     foreach (var privKey in availableKeys)
     {
-        if (result.Signatures.Length >= requiredSignatures)
+        if (signatures.Count >= requiredSignatures)
             break;

         var pubkey = privKey.CreatePubKey();
         if (allowedKeysSet.Contains(pubkey))
         {
             var sig = privKey.SignBIP340(msg);
-            result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray();
+            signatures.Add(sig.ToHex());
         }
     }

-    return (result.Signatures.Length >= requiredSignatures, result);
+    return (signatures.Count >= requiredSignatures, new P2PKWitness { Signatures = signatures.ToArray() });
 }

The same pattern appears in TrySignBlindPath at lines 204 and 214.


246-249: Add explicit non-null cast after filtering.

The Where clause filters out nulls but doesn't change the inferred array type. While the values are guaranteed non-null at runtime, adding an explicit cast improves type safety and clarity.

🔎 Proposed fix
             var sigs = witness.Signatures
                 .Select(s => SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) ? sig : null)
                 .Where(signature => signature is not null)
+                .Cast<SecpSchnorrSignature>()
                 .ToArray();
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a25452 and 2a7a80d.

📒 Files selected for processing (10)
  • DotNut.Demo/Program.cs
  • DotNut.Tests/UnitTest1.cs
  • DotNut.sln
  • DotNut/HTLCBuilder.cs
  • DotNut/HTLCProofSecret.cs
  • DotNut/HTLCWitness.cs
  • DotNut/JsonConverters/Nut10SecretJsonConverter.cs
  • DotNut/P2PKBuilder.cs
  • DotNut/P2PKProofSecret.cs
  • DotNut/SigAllHandler.cs
💤 Files with no reviewable changes (1)
  • DotNut/JsonConverters/Nut10SecretJsonConverter.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.
📚 Learning: 2025-11-29T15:42:55.222Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.

Applied to files:

  • DotNut/HTLCBuilder.cs
  • DotNut/P2PKBuilder.cs
  • DotNut/SigAllHandler.cs
  • DotNut.Tests/UnitTest1.cs
  • DotNut/HTLCProofSecret.cs
  • DotNut/P2PKProofSecret.cs
🧬 Code graph analysis (4)
DotNut.Demo/Program.cs (1)
DotNut/P2PKBuilder.cs (2)
  • P2PKBuilder (6-171)
  • P2PKBuilder (61-111)
DotNut/P2PKBuilder.cs (1)
DotNut/P2PKProofSecret.cs (1)
  • P2PKProofSecret (8-276)
DotNut/SigAllHandler.cs (3)
DotNut/Proof.cs (1)
  • Proof (7-31)
DotNut/BlindedMessage.cs (1)
  • BlindedMessage (5-11)
DotNut/Nut10ProofSecret.cs (1)
  • Nut10ProofSecret (5-18)
DotNut/P2PKProofSecret.cs (3)
DotNut/P2PKBuilder.cs (2)
  • P2PKBuilder (6-171)
  • P2PKBuilder (61-111)
DotNut/Cashu.cs (19)
  • ECPubKey (16-20)
  • ECPubKey (22-26)
  • ECPubKey (27-42)
  • ECPubKey (51-54)
  • ECPubKey (61-64)
  • ECPubKey (66-69)
  • ECPubKey (71-75)
  • ECPubKey (77-81)
  • ECPubKey (128-132)
  • ECPrivKey (56-59)
  • ECPrivKey (84-95)
  • ECPrivKey (145-156)
  • ToBytes (180-185)
  • ToHex (175-178)
  • ToHex (193-196)
  • ToHex (197-200)
  • ToHex (201-204)
  • Cashu (8-205)
  • ComputeZx (134-143)
DotNut/P2PKWitness.cs (1)
  • P2PKWitness (5-8)
🪛 Gitleaks (8.30.0)
DotNut/SigAllHandler.cs

[high] 219-219: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (17)
DotNut.sln (1)

11-12: Consider removing or renaming the ConsoleApp1 project.

The generic name "ConsoleApp1" suggests this may be a temporary development/debugging project. If it's intended for permanent inclusion, consider giving it a more descriptive name. If it was added for local testing, it should likely be excluded from the PR.

DotNut.Demo/Program.cs (1)

478-483: LGTM!

The type rename from P2PkBuilder to P2PKBuilder is correctly applied. The demo code provides good examples of multisig and time-locked P2PK usage.

DotNut.Tests/UnitTest1.cs (2)

358-386: Good test coverage for new P2PK rules.

The test properly validates that after locktime expiry, proofs are spendable on both standard and refund paths per the new rules from cashubtc/nuts#315. The test verifies both paths with appropriate signatures.


388-477: Comprehensive SIG_ALL test coverage.

Excellent test coverage for the new SigAllHandler functionality including:

  • Basic swap request verification
  • Multisig scenarios
  • Refund locktime scenarios
  • HTLC with preimage
  • Melt requests with quote ID

The tests validate both valid and invalid cases, ensuring robust verification logic.

DotNut/HTLCProofSecret.cs (2)

14-19: LGTM!

The GetAllowedPubkeys override properly delegates to the builder and returns the correct signature threshold.


21-37: LGTM!

The refund path logic correctly implements the new behavior:

  • Returns requiredSignatures = 0 with empty array when no refund keys exist (proof spendable without signature after locktime)
  • Uses RefundSignatureThreshold ?? 1 as the default threshold
  • Returns null for requiredSignatures when lock hasn't expired (no refund condition)
DotNut/HTLCWitness.cs (1)

7-10: LGTM!

Making Preimage nullable aligns with the new P2PK rules where HTLC proofs can be spent on the refund path after locktime without requiring the preimage. The JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull) ensures clean serialization.

DotNut/P2PKBuilder.cs (3)

6-17: LGTM!

The class rename to P2PKBuilder follows conventional casing for acronyms. The new RefundSignatureThreshold property cleanly extends the builder for multi-sig refund scenarios.


40-45: LGTM!

The logic correctly gates the n_sigs_refund tag:

  1. Only adds when RefundSignatureThreshold is set
  2. Validates that enough refund keys exist (refundKeys.Length >= refundSignatureThreshold)
  3. Only applies within the Lock.HasValue block, since refund conditions are only relevant with timelocks

86-92: LGTM!

The loading of n_sigs_refund correctly parses the tag value and sets RefundSignatureThreshold. The pattern is consistent with how other tags like n_sigs are loaded.

DotNut/SigAllHandler.cs (2)

197-221: LGTM - Static analysis false positive.

The Gitleaks warning about "Generic API Key" at line 219 is a false positive. The variable secret is a local variable representing a proof secret, not an API key. The naming is appropriate for this domain.


223-229: LGTM!

The CheckIfEqualToFirst helper correctly validates that all proofs share identical Data and Tags, which is required for SIG_ALL signing where all inputs must have matching conditions.

DotNut/P2PKProofSecret.cs (5)

12-19: LGTM!

The naming convention fix from P2PkBuilder to P2PKBuilder is consistent with the updated class name. The GetAllowedPubkeys method correctly delegates to the builder.


21-37: LGTM!

The refund path logic correctly handles three scenarios:

  1. Timelock expired with no refund keys → spendable without signature
  2. Timelock expired with refund keys → requires threshold signatures
  3. Timelock not expired or no lock → refund path unavailable

111-121: Verify that signature verification logic is intentional for multi-sig.

The current implementation counts valid signature-key pairs but doesn't enforce that:

  1. Each signature is only verified against one key
  2. Each key is only used to verify one signature

If duplicate signatures are provided, or if a signature matches multiple keys, the count could be inflated.

Consider whether the protocol requires 1:1 matching between signatures and keys:

 private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, 
     SecpSchnorrSignature[] sigs, byte[] hash)
 {
     if (sigs.Length < requiredSignatures)
         return false;

     var xonlyKeys = allowedKeys.Select(k => k.ToXOnlyPubKey()).ToArray();
-    var validCount = sigs.Count(s => xonlyKeys.Any(xonly => xonly.SigVerifyBIP340(s, hash)));
-
-    return validCount >= requiredSignatures;
+    var usedKeys = new HashSet<int>();
+    var validCount = 0;
+    
+    foreach (var sig in sigs)
+    {
+        for (int i = 0; i < xonlyKeys.Length; i++)
+        {
+            if (usedKeys.Contains(i)) continue;
+            if (xonlyKeys[i].SigVerifyBIP340(sig, hash))
+            {
+                usedKeys.Add(i);
+                validCount++;
+                break;
+            }
+        }
+    }
+    
+    return validCount >= requiredSignatures;
 }

178-222: LGTM!

The blinded path signing logic correctly handles both the positive and negated key paths, which is necessary for BIP340 x-only pubkey compatibility. The slot tracking prevents key reuse.

Note: The same Append().ToArray() allocation pattern at lines 204 and 214 could be optimized as mentioned earlier.


253-268: LGTM!

The verification logic correctly mirrors the signing logic by:

  1. Checking if no signature is required (returns true)
  2. Trying normal path verification first
  3. Falling back to refund path if available and normal path fails

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
DotNut/P2PKProofSecret.cs (2)

73-73: Typo: "notmal" → "normal".

This typo has already been flagged in a previous review.


164-165: Inconsistency resolved.

A previous review flagged this line as inconsistent with line 70, noting that GenerateBlindWitness returned new P2PKWitness() while GenerateWitness returned null. The current code shows both methods now return null when requiredRefundSignatures == 0, making them consistent.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a7a80d and be32ab6.

📒 Files selected for processing (1)
  • DotNut/P2PKProofSecret.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-29T15:42:55.222Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.

Applied to files:

  • DotNut/P2PKProofSecret.cs
🔇 Additional comments (4)
DotNut/P2PKProofSecret.cs (4)

45-91: Approve: Witness generation with refund path fallback.

The nullable return type and dual-path logic (normal → refund fallback) correctly implement the intended semantics. Early return when requiredRefundSignatures == 0 is appropriate (no witness required when refund has no signature threshold). The exception on line 90 ensures callers are notified when neither path can produce sufficient signatures.


93-125: Approve: Well-structured signing and verification helpers.

TrySignPath efficiently matches available keys against allowed keys using a HashSet and accumulates signatures up to the required threshold. VerifyPath correctly counts valid signatures and enforces the threshold. Both methods are clear and correct.


135-183: Approve: Blind witness generation mirrors standard path.

The nullable return types and dual-path fallback logic (normal → refund) correctly mirror the non-blind GenerateWitness behavior. The implementation is consistent and sound.


250-282: Approve: Dual-path witness verification.

The verification logic correctly checks both normal and refund paths, mirroring the generation logic. The early return when requiredRefundSignatures == 0 appropriately treats the "no witness required" case as always valid. Defensive null filtering on line 256-257 prevents issues with malformed signatures.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be32ab6 and 25d65b6.

📒 Files selected for processing (1)
  • DotNut/P2PKProofSecret.cs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.
📚 Learning: 2025-11-29T15:42:55.222Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.

Applied to files:

  • DotNut/P2PKProofSecret.cs
🧬 Code graph analysis (1)
DotNut/P2PKProofSecret.cs (3)
DotNut/P2PKBuilder.cs (2)
  • P2PKBuilder (6-171)
  • P2PKBuilder (61-111)
DotNut/Cashu.cs (19)
  • ECPubKey (16-20)
  • ECPubKey (22-26)
  • ECPubKey (27-42)
  • ECPubKey (51-54)
  • ECPubKey (61-64)
  • ECPubKey (66-69)
  • ECPubKey (71-75)
  • ECPubKey (77-81)
  • ECPubKey (128-132)
  • ECPrivKey (56-59)
  • ECPrivKey (84-95)
  • ECPrivKey (145-156)
  • ToBytes (180-185)
  • ToHex (175-178)
  • ToHex (193-196)
  • ToHex (197-200)
  • ToHex (201-204)
  • Cashu (8-205)
  • ComputeZx (134-143)
DotNut/P2PKWitness.cs (1)
  • P2PKWitness (5-8)
🔇 Additional comments (4)
DotNut/P2PKProofSecret.cs (4)

21-37: LGTM - Timelock logic correctly implemented.

The refund pubkey retrieval logic correctly handles the timelock condition: when Lock >= Now, the timelock hasn't expired yet, so refund keys are not accessible. The special case at lines 30-34 where requiredSignatures = 0 (freely spendable after lock expiry with no refund keys) is also handled appropriately.


45-91: Well-structured witness generation with proper path precedence.

The witness generation flow correctly:

  • Returns null when no witness is required (line 70)
  • Attempts the normal signing path first (lines 73-78)
  • Falls back to the refund path when the timelock has expired and refund keys are available (lines 81-88)
  • Throws a clear exception when neither path can produce sufficient signatures (line 90)

The nullable return type P2PKWitness? appropriately signals to callers when witness generation is not necessary.


186-230: Good defensive programming with slot tracking.

The TrySignBlindPath method correctly uses a HashSet<int> (line 192) to track which key slots have been used, preventing the same slot from being signed multiple times (line 201, 210, 221). This ensures proper multi-signature semantics where each signature must correspond to a distinct key.

This defensive approach should also be applied to the verification logic in VerifyPath to prevent duplicate signature attacks.


250-282: Verification flow structure is sound, but relies on flawed VerifyPath.

The overall verification logic correctly:

  • Filters invalid signatures (line 256)
  • Checks the normal path first (line 266)
  • Falls back to the refund path when applicable (lines 270-274)
  • Returns true when no witness is required (lines 261-264)

However, both verification branches call VerifyPath, which has a critical vulnerability allowing duplicate signatures to bypass the multi-sig threshold. Once VerifyPath is fixed, this method will work correctly.

The security of this verification depends on fixing the duplicate signature issue in VerifyPath (lines 115-125).

@robwoodgate
Copy link

cashubtc/nuts#315 has now been merged, so this can be completed and merged when ready.

@d4rp4t d4rp4t requested a review from Kukks January 8, 2026 20:17
Add Equals() to Nut10ProofSecret
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @DotNut/Nut10ProofSecret.cs:
- Line 54: Remove the unnecessary trailing semicolon after the closing brace in
the Nut10ProofSecret initializer/block (the lone "};" at the end of the
declaration); delete the semicolon so the block ends with just "}" to match C#
syntax for that construct and avoid a stray token.
🧹 Nitpick comments (5)
DotNut/Nut10ProofSecret.cs (1)

18-43: Consider implementing IEquatable<Nut10ProofSecret>.

The equality implementation is correct and thorough. For completeness and to enable generic constraint optimizations (e.g., in Dictionary<Nut10ProofSecret, T>), consider having the class implement the IEquatable<Nut10ProofSecret> interface explicitly:

-public class Nut10ProofSecret
+public class Nut10ProofSecret : IEquatable<Nut10ProofSecret>
DotNut/P2PKBuilder.cs (1)

114-125: Use .Length for arrays and differentiate error messages.

  1. Pubkeys.Count() invokes LINQ; use .Length directly for arrays.
  2. Both validation branches throw the same error message, making it harder to identify which threshold check failed.
♻️ Suggested fix
 private void Validate()
 {
-    if (this.Pubkeys.Count() < SignatureThreshold)
+    if (this.Pubkeys.Length < SignatureThreshold)
     {
-        throw new ArgumentException("Signature threshold bigger than provided pubkeys count!");
+        throw new ArgumentException("SignatureThreshold exceeds the number of Pubkeys.");
     }
     if(this.RefundSignatureThreshold is not null 
        && (RefundPubkeys is null || RefundPubkeys.Length < RefundSignatureThreshold))
     {
-        throw new ArgumentException("Signature threshold bigger than provided pubkeys count!");
+        throw new ArgumentException("RefundSignatureThreshold exceeds the number of RefundPubkeys.");
     }
 }
DotNut/SigAllHandler.cs (3)

10-14: Consider initializing collection properties or marking them nullable.

Proofs, PrivKeys, and BlindedMessages are non-nullable reference types but lack default values. While TrySign null-checks them, callers could inadvertently access uninitialized properties.

♻️ Suggested fix
-    public List<Proof> Proofs { get; set; }
-    public List<PrivKey> PrivKeys { get; set; }
-    public List<BlindedMessage> BlindedMessages { get; set; }
+    public List<Proof> Proofs { get; set; } = new();
+    public List<PrivKey> PrivKeys { get; set; } = new();
+    public List<BlindedMessage> BlindedMessages { get; set; } = new();

151-154: Consider catching specific exceptions or adding logging.

Catching Exception swallows all errors including unexpected ones (e.g., NullReferenceException). For a verification method returning false on failure is acceptable, but catching only expected exceptions (like ArgumentException, JsonException) or adding debug logging would improve debuggability.


190-192: Empty catch block swallows all exceptions.

This catch block silently swallows all exceptions, including unexpected ones. Consider catching only JsonException to avoid masking bugs.

♻️ Suggested fix
-        catch
+        catch (JsonException)
         {
             return false;
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dfdf1f and bdbd374.

📒 Files selected for processing (3)
  • DotNut/Nut10ProofSecret.cs
  • DotNut/P2PKBuilder.cs
  • DotNut/SigAllHandler.cs
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.
📚 Learning: 2025-11-08T01:49:27.299Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 21
File: DotNut/P2PkBuilder.cs:100-107
Timestamp: 2025-11-08T01:49:27.299Z
Learning: In the DotNut codebase, the PrivKey class (DotNut/PrivKey.cs) defines an implicit operator that converts PrivKey to ECPrivKey by returning privKey.Key. This allows PrivKey instances to be passed directly to methods expecting ECPrivKey parameters without explicit conversion.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
📚 Learning: 2025-09-17T17:11:38.738Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 16
File: DotNut/PubKey.cs:41-51
Timestamp: 2025-09-17T17:11:38.738Z
Learning: NBitcoin's ECPubKey (from NBitcoin.Secp256k1) implements value-based equality comparison through operator ==, not reference comparison. The operator compares the underlying cryptographic key material, making it suitable for structural equality implementations.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
📚 Learning: 2025-11-29T15:42:55.222Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
  • DotNut/SigAllHandler.cs
  • DotNut/P2PKBuilder.cs
📚 Learning: 2026-01-11T17:55:03.188Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs:1-32
Timestamp: 2026-01-11T17:55:03.188Z
Learning: In the DotNut codebase, PubKey and PrivKey are defined as internal types in the DotNut namespace (DotNut/PubKey.cs and DotNut/PrivKey.cs), not from NBitcoin.Secp256k1. For files in child namespaces (e.g., DotNut.Abstractions) that reference these types, add an explicit 'using DotNut;' directive at the top. This applies to all DotNut.* .cs files that reference these types; do not assume a global using will bring them in.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
  • DotNut/SigAllHandler.cs
  • DotNut/P2PKBuilder.cs
🧬 Code graph analysis (3)
DotNut/Nut10ProofSecret.cs (2)
DotNut/KeysetId.cs (5)
  • Equals (10-17)
  • Equals (24-27)
  • Equals (29-33)
  • GetHashCode (19-22)
  • GetHashCode (35-38)
DotNut/PubKey.cs (2)
  • Equals (41-46)
  • GetHashCode (48-51)
DotNut/SigAllHandler.cs (3)
DotNut/Proof.cs (1)
  • Proof (7-31)
DotNut/Nut10ProofSecret.cs (1)
  • Nut10ProofSecret (5-76)
DotNut/P2PKBuilder.cs (5)
  • P2PKProofSecret (19-60)
  • P2PKProofSecret (135-140)
  • P2PKProofSecret (143-161)
  • P2PKBuilder (6-185)
  • P2PKBuilder (62-112)
DotNut/P2PKBuilder.cs (1)
DotNut/P2PKProofSecret.cs (1)
  • P2PKProofSecret (8-299)
🔇 Additional comments (8)
DotNut/Nut10ProofSecret.cs (1)

65-74: LGTM!

The equality operators are correctly implemented, properly handling null comparisons and delegating to Equals.

DotNut/P2PKBuilder.cs (4)

6-6: LGTM on class rename.

The rename from P2PkBuilder to P2PKBuilder improves consistency with standard acronym casing conventions.


17-21: LGTM!

Adding RefundSignatureThreshold and calling Validate() at the start of Build() is a solid defensive approach.


40-46: LGTM!

The logic correctly defaults RefundSignatureThreshold to 1 when refund pubkeys are present and only emits the n_sigs_refund tag when the threshold exceeds 1, mirroring the existing n_sigs pattern.


88-93: LGTM!

The n_sigs_refund tag parsing follows the established pattern for other tags.

DotNut/SigAllHandler.cs (3)

19-82: LGTM!

TrySign correctly handles validation, message construction, and branches for HTLC (with preimage) vs P2PK paths, including blind witness generation when P2PkE is present. Based on learnings, the HTLCProofSecret is P2PKProofSecret inheritance is properly leveraged here.


84-130: LGTM!

The message construction correctly enforces SIG_ALL requirements: all proofs must be Nut10Secrets with identical data/tags. The new Nut10ProofSecret equality operator is appropriately used here.


197-221: LGTM!

The validation logic correctly handles HTLC and P2PK secret types via their respective builders. The fallback case (line 211) creates a builder with SigFlag = null, which will intentionally fail the SIG_ALL check on line 214—a safe default for unrecognized secret types.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
DotNut/Nut10ProofSecret.cs (2)

5-5: Consider implementing IEquatable<Nut10ProofSecret>.

When overriding Equals(object) and providing a typed Equals(T) method, implementing IEquatable<T> is the recommended pattern. This enables generic collections and LINQ operations to use the typed equality method directly, avoiding boxing.

♻️ Suggested implementation
-public class Nut10ProofSecret
+public class Nut10ProofSecret : IEquatable<Nut10ProofSecret>

Also applies to: 19-43


45-63: Hash computation flattens array structure, increasing collision likelihood.

The current approach adds all tag strings sequentially without encoding array boundaries. For example, [["a","b"],["c"]] and [["a"],["b","c"]] produce identical hashes. This is technically valid (collisions are allowed), but could impact performance in hash-based collections if such patterns are common.

♻️ Optional: Add structural markers to reduce collisions
         foreach (var tagArray in this.Tags)
         {
+            hash.Add(tagArray?.Length ?? -1); // Mark array boundary
             foreach (var tag in tagArray)
             {
                 hash.Add(tag);
             }
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bdbd374 and 1955fdf.

📒 Files selected for processing (1)
  • DotNut/Nut10ProofSecret.cs
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.
📚 Learning: 2025-11-08T01:49:27.299Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 21
File: DotNut/P2PkBuilder.cs:100-107
Timestamp: 2025-11-08T01:49:27.299Z
Learning: In the DotNut codebase, the PrivKey class (DotNut/PrivKey.cs) defines an implicit operator that converts PrivKey to ECPrivKey by returning privKey.Key. This allows PrivKey instances to be passed directly to methods expecting ECPrivKey parameters without explicit conversion.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
📚 Learning: 2026-01-11T17:55:03.188Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/Abstractions/Interfaces/IMintQuoteBuilder.cs:1-32
Timestamp: 2026-01-11T17:55:03.188Z
Learning: In the DotNut codebase, PubKey and PrivKey are defined as internal types in the DotNut namespace (DotNut/PubKey.cs and DotNut/PrivKey.cs), not from NBitcoin.Secp256k1. For files in child namespaces (e.g., DotNut.Abstractions) that reference these types, add an explicit 'using DotNut;' directive at the top. This applies to all DotNut.* .cs files that reference these types; do not assume a global using will bring them in.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
📚 Learning: 2025-09-17T17:11:38.738Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 16
File: DotNut/PubKey.cs:41-51
Timestamp: 2025-09-17T17:11:38.738Z
Learning: NBitcoin's ECPubKey (from NBitcoin.Secp256k1) implements value-based equality comparison through operator ==, not reference comparison. The operator compares the underlying cryptographic key material, making it suitable for structural equality implementations.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
📚 Learning: 2025-11-29T15:42:55.222Z
Learnt from: d4rp4t
Repo: Kukks/DotNut PR: 23
File: DotNut/NUT11/SigAllHandler.cs:19-77
Timestamp: 2025-11-29T15:42:55.222Z
Learning: In DotNut, HTLCProofSecret inherits from P2PKProofSecret, forming a base-derived relationship where HTLC secrets are a specialized form of P2PK secrets. Type checks using `is P2PKProofSecret` will match both P2PK and HTLC secrets due to this inheritance.

Applied to files:

  • DotNut/Nut10ProofSecret.cs
🧬 Code graph analysis (1)
DotNut/Nut10ProofSecret.cs (2)
DotNut/KeysetId.cs (5)
  • Equals (10-17)
  • Equals (24-27)
  • Equals (29-33)
  • GetHashCode (19-22)
  • GetHashCode (35-38)
DotNut/PubKey.cs (2)
  • Equals (41-46)
  • GetHashCode (48-51)
🔇 Additional comments (1)
DotNut/Nut10ProofSecret.cs (1)

65-74: LGTM!

The equality operators correctly handle null operands and follow the standard C# pattern.

Comment on lines +40 to +42
((this.Tags == null && s.Tags == null) ||
(this.Tags != null && s.Tags != null && this.Tags.Length == s.Tags.Length &&
this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second))));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential NullReferenceException if inner tag arrays are null.

If Tags contains null elements (e.g., new string[][] { new[] {"a"}, null }), both SequenceEqual in Equals and the inner foreach in GetHashCode will throw. If null inner arrays are not valid by design, consider adding a guard or documenting this constraint.

🛡️ Optional defensive fix for Equals
-              this.Tags.Zip(s.Tags).All(pair => pair.First.SequenceEqual(pair.Second))));
+              this.Tags.Zip(s.Tags).All(pair => 
+                  (pair.First == null && pair.Second == null) ||
+                  (pair.First != null && pair.Second != null && pair.First.SequenceEqual(pair.Second)))));

Also applies to: 55-60

@d4rp4t d4rp4t requested a review from Kukks January 18, 2026 17:27
@d4rp4t d4rp4t merged commit 020ae13 into Kukks:master Jan 18, 2026
1 check passed
@d4rp4t d4rp4t deleted the chore/new-p2pk-htlc-rules branch January 18, 2026 21:08
@coderabbitai coderabbitai bot mentioned this pull request Jan 24, 2026
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.

3 participants