Skip to content

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 signing sessions (supports both standard and time-locked flows).
    • Configurable refund signature threshold for refund paths.
  • API Changes

    • Witness generation APIs now return nullable results to signal timelock/refund outcomes.
    • HTLC exposes allowed pubkeys and refined refund behavior; preimage omitted when absent.
  • Tests

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

    • Public type name casing normalized across library.

✏️ 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, adds refund-signature-threshold support, changes HTLC refund/pubkey exposure and witness nullability, extends P2PK proof secret with refund/blind signing and verification paths (nullable witnesses), and adds SigAllHandler for SIG_ALL multi-proof signing/verification.

Changes

Cohort / File(s) Summary
Builder rename & refund threshold
DotNut/P2PKBuilder.cs, DotNut/HTLCBuilder.cs, DotNut.Demo/Program.cs, DotNut.Tests/UnitTest1.cs
Class renamed P2PkBuilderP2PKBuilder; added int? RefundSignatureThreshold; Build/Load updated to emit/parse n_sigs_refund; updated usages and inheritance.
P2PK proof secret: signing, blind-signing & verification
DotNut/P2PKProofSecret.cs
Added GetAllowedRefundPubkeys(out int? requiredSignatures); changed GenerateWitness/GenerateBlindWitness overloads to return nullable P2PKWitness?; added TrySignPath, VerifyPath, TrySignBlindPath; VerifyWitnessHash updated to check normal and refund paths and support null returns.
HTLC proof & witness changes
DotNut/HTLCProofSecret.cs, DotNut/HTLCWitness.cs
HTLCProofSecret: new GetAllowedPubkeys(out int requiredSignatures) and adjusted refund-pubkey logic to indicate refund applicability; HTLCWitness.Preimage made nullable and annotated to omit when null in JSON.
New SIG_ALL orchestration
DotNut/SigAllHandler.cs
New SigAllHandler class: properties for Proofs, PrivKeys, BlindedMessages, HTLCPreimage, MeltQuoteId; TrySign(out string? witness) to build message and produce serialized witness; static GetMessageToSign, VerifySigAllWitness overloads, ValidateFirstProof, CheckIfEqualToFirst. Supports P2PK and HTLC flows.
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 behavior added; removed extra blank lines in Nut10 converter; no behavioral change beyond serialization omission when preimage is null.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant SigAllHandler
    participant P2PKProofSecret
    participant HTLCProofSecret

    rect rgb(235,245,255)
    Note over Client,SigAllHandler: SIG_ALL signing orchestration
    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
    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 sings where signatures go—
A tiny rabbit clap, a happy switch! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.71% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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: renaming P2PkBuilder to P2PKBuilder, adding refund/HTLC-related rules, and introducing new SigAllHandler for SIG_ALL support.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 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 (7)
DotNut/P2PKProofSecret.cs (7)

12-12: LGTM: Builder property updated to use consistent naming.

The rename from P2PkBuilder to P2PKBuilder improves naming consistency.


45-91: LGTM: Dual-path witness generation is well-structured.

The method correctly implements the normal/refund path logic:

  • Returns null when no witness is needed (refund without signature)
  • Attempts normal path first, then refund path if applicable
  • Appropriately throws when neither path can collect enough signatures

The nullable return type properly reflects that witnesses may not be required in certain conditions.


93-113: LGTM: Efficient signature collection with proper early exit.

The TrySignPath helper uses a HashSet for efficient key lookup and breaks early once the required signature threshold is met. The logic correctly accumulates valid signatures from available keys.


125-173: LGTM: Blind witness generation follows the same dual-path pattern.

The blind witness generation correctly mirrors the normal witness generation logic, with proper handling of both normal and refund paths for blinded keys. The nullable return and error handling are consistent.


176-220: LGTM: Blind path signing with proper slot tracking.

The TrySignBlindPath helper correctly implements blinded key signing with:

  • usedSlots HashSet to ensure each key slot is used at most once
  • Both positive and negative tweak verification (necessary for blind signatures)
  • Early exit optimizations when threshold is met

The slot-tracking mechanism prevents any duplicate signature issues in the blinded path.


240-272: LGTM: Witness verification correctly implements dual-path logic.

The verification method properly:

  • Filters out invalid signatures from parsing failures
  • Returns true when no witness is required (refund without signature)
  • Validates against both normal and refund paths
  • Returns true if either path validates successfully

The verification logic correctly mirrors the witness generation logic.


274-297: Excellent: Duplicate signature bypass is now properly prevented.

The VerifyPath helper correctly addresses the critical security issue flagged in previous reviews. The implementation now:

  • Tracks which key indices have been used via usedKeyIndices HashSet
  • Ensures each signature maps to a distinct public key
  • Prevents an attacker from using the same signature multiple times to meet the threshold

This fix properly secures the multi-signature verification and eliminates the bypass vulnerability.


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
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