From d8f168d886e182add948ae9acbd436ef2743d1dd Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sun, 21 Dec 2025 20:26:47 +0100 Subject: [PATCH 1/9] New P2PK conditions, rename P2Pk -> P2PK --- DotNut.Demo/Program.cs | 4 +- DotNut.sln | 14 ++ DotNut/HTLCBuilder.cs | 10 +- DotNut/HTLCProofSecret.cs | 20 +- DotNut/HTLCWitness.cs | 5 +- .../Nut10SecretJsonConverter.cs | 3 - DotNut/{P2PkBuilder.cs => P2PKBuilder.cs} | 23 +- DotNut/P2PKProofSecret.cs | 216 ++++++++++++------ 8 files changed, 204 insertions(+), 91 deletions(-) rename DotNut/{P2PkBuilder.cs => P2PKBuilder.cs} (85%) diff --git a/DotNut.Demo/Program.cs b/DotNut.Demo/Program.cs index 486c442..20f68d9 100644 --- a/DotNut.Demo/Program.cs +++ b/DotNut.Demo/Program.cs @@ -475,7 +475,7 @@ private static async Task P2PKDemo() // Create a 1-of-2 multisig P2PK secret Console.WriteLine("\n🏗️ Creating 1-of-2 multisig P2PK:"); - var p2pkBuilder = new P2PkBuilder + var p2pkBuilder = new P2PKBuilder { Pubkeys = new[] { pubKey1, pubKey2 }, SignatureThreshold = 1, // 1-of-2 multisig @@ -491,7 +491,7 @@ private static async Task P2PKDemo() // Create a time-locked P2PK Console.WriteLine("\n⏰ Creating time-locked P2PK:"); - var timeLockedBuilder = new P2PkBuilder + var timeLockedBuilder = new P2PKBuilder { Pubkeys = new[] { pubKey1 }, SignatureThreshold = 1, diff --git a/DotNut.sln b/DotNut.sln index c8a95fd..f648970 100644 --- a/DotNut.sln +++ b/DotNut.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Nostr", "DotNut.Nost EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Demo", "DotNut.Demo\DotNut.Demo.csproj", "{305097F3-A4E5-4511-8E4E-0C4C12A953C6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{6D20E7A3-784A-4394-AD7C-703AEF5A44CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +68,18 @@ Global {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x64.Build.0 = Release|Any CPU {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x86.ActiveCfg = Release|Any CPU {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x86.Build.0 = Release|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x64.Build.0 = Debug|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x86.Build.0 = Debug|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|Any CPU.Build.0 = Release|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x64.ActiveCfg = Release|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x64.Build.0 = Release|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x86.ActiveCfg = Release|Any CPU + {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DotNut/HTLCBuilder.cs b/DotNut/HTLCBuilder.cs index f5427c0..f236a71 100644 --- a/DotNut/HTLCBuilder.cs +++ b/DotNut/HTLCBuilder.cs @@ -2,13 +2,13 @@ namespace DotNut; -public class HTLCBuilder : P2PkBuilder +public class HTLCBuilder : P2PKBuilder { public string HashLock { get; set; } /* - * ugly hack to reuse P2PkBuilder for HTLCs. - * P2PkBuilder expects a pubkey in `data` field, but we need to store a hashlock instead + * ugly hack to reuse P2PKBuilder for HTLCs. + * P2PKBuilder expects a pubkey in `data` field, but we need to store a hashlock instead * * we inject a dummy pubkey so the loader doesn’t break, then remove it after load/build. */ @@ -29,7 +29,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) Tags = proofSecret.Tags }; - var innerbuilder = P2PkBuilder.Load(tempProof); + var innerbuilder = P2PKBuilder.Load(tempProof); innerbuilder.Pubkeys = innerbuilder.Pubkeys.Except([_dummy.Key]).ToArray(); return new HTLCBuilder() { @@ -50,7 +50,7 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) { throw new ArgumentException("HashLock must be 32 bytes (64 chars hex)", nameof(HashLock)); } - var innerBuilder = new P2PkBuilder() + var innerBuilder = new P2PKBuilder() { Lock = Lock, Pubkeys = Pubkeys.ToArray(), diff --git a/DotNut/HTLCProofSecret.cs b/DotNut/HTLCProofSecret.cs index 96a6d10..1e646ca 100644 --- a/DotNut/HTLCProofSecret.cs +++ b/DotNut/HTLCProofSecret.cs @@ -12,16 +12,28 @@ public class HTLCProofSecret : P2PKProofSecret [JsonIgnore] public HTLCBuilder Builder => HTLCBuilder.Load(this); public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) + { + var builder = Builder; + requiredSignatures = builder.SignatureThreshold; + return builder.Pubkeys; + } + + public override ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) { - requiredSignatures = Math.Min(builder.RefundPubkeys?.Length ?? 0, 1); - return builder.RefundPubkeys ?? Array.Empty(); + if (builder.RefundPubkeys == null) + { + requiredSignatures = 0; // proof is spendable without any signature + return []; + } + requiredSignatures = builder.RefundSignatureThreshold ?? 1; + return [..builder.RefundPubkeys??[]]; } - requiredSignatures = builder.SignatureThreshold; - return builder.Pubkeys; + requiredSignatures = null; // there's no refund condition :/ + return []; } diff --git a/DotNut/HTLCWitness.cs b/DotNut/HTLCWitness.cs index 6f37bcd..46fb6cd 100644 --- a/DotNut/HTLCWitness.cs +++ b/DotNut/HTLCWitness.cs @@ -4,5 +4,8 @@ namespace DotNut; public class HTLCWitness: P2PKWitness { - [JsonPropertyName("preimage")] public string Preimage { get; set; } + // this field is nullable now, because after locktime expiry only signatures are needed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("preimage")] + public string? Preimage { get; set; } } \ No newline at end of file diff --git a/DotNut/JsonConverters/Nut10SecretJsonConverter.cs b/DotNut/JsonConverters/Nut10SecretJsonConverter.cs index 1f33bd1..e38a874 100644 --- a/DotNut/JsonConverters/Nut10SecretJsonConverter.cs +++ b/DotNut/JsonConverters/Nut10SecretJsonConverter.cs @@ -5,9 +5,6 @@ namespace DotNut.JsonConverters; public class Nut10SecretJsonConverter : JsonConverter { - - - public override Nut10Secret? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if(reader.TokenType == JsonTokenType.Null) diff --git a/DotNut/P2PkBuilder.cs b/DotNut/P2PKBuilder.cs similarity index 85% rename from DotNut/P2PkBuilder.cs rename to DotNut/P2PKBuilder.cs index 4fc02f6..55ad0f9 100644 --- a/DotNut/P2PkBuilder.cs +++ b/DotNut/P2PKBuilder.cs @@ -3,7 +3,7 @@ namespace DotNut; -public class P2PkBuilder +public class P2PKBuilder { public DateTimeOffset? Lock { get; set; } public ECPubKey[]? RefundPubkeys { get; set; } @@ -14,6 +14,7 @@ public class P2PkBuilder //SIG_INPUTS, SIG_ALL public string? SigFlag { get; set; } public string? Nonce { get; set; } + public int? RefundSignatureThreshold { get; set; } public P2PKProofSecret Build() { @@ -36,14 +37,19 @@ public P2PKProofSecret Build() tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())) .ToArray()); } + if (RefundSignatureThreshold is { } refundSignatureThreshold + && RefundPubkeys is {} refundKeys + && refundKeys.Length >= refundSignatureThreshold) + { + tags.Add(new[] {"n_sigs_refund", refundSignatureThreshold.ToString() }); + } } if (SignatureThreshold > 1 && Pubkeys.Length >= SignatureThreshold) { tags.Add(new[] { "n_sigs", SignatureThreshold.ToString() }); } - - + return new P2PKProofSecret() { Data = Pubkeys.First().ToHex(), @@ -52,9 +58,9 @@ public P2PKProofSecret Build() }; } - public static P2PkBuilder Load(P2PKProofSecret proofSecret) + public static P2PKBuilder Load(P2PKProofSecret proofSecret) { - var builder = new P2PkBuilder(); + var builder = new P2PKBuilder(); var primaryPubkey = proofSecret.Data.ToPubKey(); var pubkeys = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "pubkeys"); if (pubkeys is not null && pubkeys.Length > 1) @@ -77,6 +83,13 @@ public static P2PkBuilder Load(P2PKProofSecret proofSecret) { builder.RefundPubkeys = refund.Skip(1).Select(s => s.ToPubKey()).ToArray(); } + + var nSigsRefund = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "n_sigs_refund")? + .Skip(1)?.FirstOrDefault(); + if (!string.IsNullOrEmpty(nSigsRefund) && int.TryParse(nSigsRefund, out var nSigsRefundValue)) + { + builder.RefundSignatureThreshold = nSigsRefundValue; + } var sigFlag = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "sigflag")?.Skip(1) ?.FirstOrDefault(); diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 412b1b8..7180c1b 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -9,158 +9,216 @@ public class P2PKProofSecret : Nut10ProofSecret { public const string Key = "P2PK"; - [JsonIgnore] P2PkBuilder Builder => P2PkBuilder.Load(this); + [JsonIgnore] P2PKBuilder Builder => P2PKBuilder.Load(this); public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) + { + var builder = Builder; + requiredSignatures = builder.SignatureThreshold; + return builder.Pubkeys; + } + + public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) { - requiredSignatures = Math.Min(builder.RefundPubkeys?.Length ?? 0, 1); - return builder.RefundPubkeys ?? Array.Empty(); + if (builder.RefundPubkeys == null) + { + requiredSignatures = 0; // proof is spendable without any signature + return []; + } + requiredSignatures = builder.RefundSignatureThreshold ?? 1; + return builder.RefundPubkeys ?? []; } - requiredSignatures = builder.SignatureThreshold; - return builder.Pubkeys; + requiredSignatures = null; // there's no refund condition, or timelock didn't expire yet :/ + return []; } - - public virtual P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) + /* + * ====================================================================== * + * If any of these returns null witness - well, witness is not necessary * + * ====================================================================== * + */ + + public virtual P2PKWitness? GenerateWitness(Proof proof, ECPrivKey[] keys) { return GenerateWitness(proof.Secret.GetBytes(), keys); } - public virtual P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) + public virtual P2PKWitness? GenerateWitness(BlindedMessage message, ECPrivKey[] keys) { return GenerateWitness(message.B_.Key.ToBytes(), keys); } - public virtual P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) + public virtual P2PKWitness? GenerateWitness(byte[] msg, ECPrivKey[] keys) { var hash = SHA256.HashData(msg); return GenerateWitness(ECPrivKey.Create(hash), keys); } - public virtual P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) + public virtual P2PKWitness? GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { var msg = hash.ToBytes(); - //filter out keys that matter + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); - var keysRequiredLeft = requiredSignatures; - var availableKeysLeft = keys; + var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); + + if (requiredRefundSignatures == 0) + { + return null; + } + + // try notmal path + var (isValid, result) = TrySignPath(allowedKeys.ToArray(), requiredSignatures, keys, msg); + if (isValid) + return result; + + // if it's after locktime - try refund path + if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) + { + (isValid, result) = TrySignPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, msg); + if (isValid) + return result; + } + + throw new InvalidOperationException("Not enough valid keys to sign!"); + } + + private (bool IsValid, P2PKWitness Witness) TrySignPath(ECPubKey[] allowedKeys, int requiredSignatures, + ECPrivKey[] availableKeys, byte[] msg) + { + var allowedKeysSet = new HashSet(allowedKeys); var result = new P2PKWitness(); - while (keysRequiredLeft > 0 && availableKeysLeft.Any()) + + foreach (var privKey in availableKeys) { - var key = availableKeysLeft.First(); - var pubkey = key.CreatePubKey(); - var isAllowed = allowedKeys.Any(p => p == pubkey); - if (isAllowed) - { - var sig = key.SignBIP340(msg); + if (result.Signatures.Length >= requiredSignatures) + break; - - key.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); + var pubkey = privKey.CreatePubKey(); + if (allowedKeysSet.Contains(pubkey)) + { + var sig = privKey.SignBIP340(msg); result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); } - - availableKeysLeft = availableKeysLeft.Except(new[] {key}).ToArray(); - keysRequiredLeft = requiredSignatures - result.Signatures.Length; } - if (keysRequiredLeft > 0) - throw new InvalidOperationException("Not enough valid keys to sign"); + return (result.Signatures.Length >= requiredSignatures, result); + } + + private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, + SecpSchnorrSignature[] sigs, byte[] hash) + { + if (sigs.Length < requiredSignatures) + return false; - return result; + var xonlyKeys = allowedKeys.Select(k => k.ToXOnlyPubKey()).ToArray(); + var validCount = sigs.Count(s => xonlyKeys.Any(xonly => xonly.SigVerifyBIP340(s, hash))); + + return validCount >= requiredSignatures; } + + /* * ========================= * NUT-XX Pay to blinded key * ========================= */ - public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) + public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) { ArgumentNullException.ThrowIfNull(proof.P2PkE); return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, proof.P2PkE); } - public virtual P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness? GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) { return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, P2PkE); } - public virtual P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness? GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) { return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, keysetId, P2PkE); } - public virtual P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness? GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) { var hash = SHA256.HashData(msg); return GenerateBlindWitness(ECPrivKey.Create(hash), keys, keysetId, P2PkE); } - public virtual P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness? GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) { var msg = hash.ToBytes(); + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); - var keysRequiredLeft = requiredSignatures; - var availableKeysLeft = keys; - var result = new P2PKWitness(); + var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); + + if (requiredRefundSignatures == 0) + return new P2PKWitness(); + + var (isValid, result) = TrySignBlindPath(allowedKeys.ToArray(), requiredSignatures, keys, keysetId, P2PkE, msg); + if (isValid) + return result; + + if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) + { + (isValid, result) = TrySignBlindPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, keysetId, P2PkE, msg); + if (isValid) + return result; + } + + throw new InvalidOperationException("Not enough valid keys to sign any blind path"); + } + + private (bool IsValid, P2PKWitness Witness) TrySignBlindPath(ECPubKey[] allowedKeys, int requiredSignatures, + ECPrivKey[] availableKeys, KeysetId keysetId, ECPubKey P2PkE, byte[] msg) + { + var allowedKeysSet = new HashSet(allowedKeys); + var result = new P2PKWitness(); var keysetIdBytes = keysetId.GetBytes(); - var pubkeysTotalCount = Builder.Pubkeys.Length + (Builder.RefundPubkeys?.Length ?? 0); - - HashSet usedSlots = new(); - - while (keysRequiredLeft > 0 && availableKeysLeft.Any()) + var usedSlots = new HashSet(); + + foreach (var key in availableKeys) { - var key = availableKeysLeft.First(); - var remainingKeys = availableKeysLeft.Skip(1).ToArray(); + if (result.Signatures.Length >= requiredSignatures) + break; - for (int i = 0; i < pubkeysTotalCount; i++) + for (int i = 0; i < allowedKeys.Length; i++) { - if (usedSlots.Contains(i)) - { - continue; - } - + if (usedSlots.Contains(i)) continue; + var Zx = Cashu.ComputeZx(key, P2PkE); var ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); - var tweakedPrivkey = key.TweakAdd(ri.ToBytes()); var tweakedPubkey = tweakedPrivkey.CreatePubKey(); - - var tweakedPrivkeyNeg = key.sec.Negate().Add(ri.sec).ToPrivateKey(); - var tweakedPubkeyNeg = tweakedPrivkeyNeg.CreatePubKey(); - - if (allowedKeys.Contains(tweakedPubkey)) + + if (allowedKeysSet.Contains(tweakedPubkey)) { usedSlots.Add(i); var sig = tweakedPrivkey.SignBIP340(msg); - tweakedPrivkey.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); - keysRequiredLeft = requiredSignatures - result.Signatures.Length; break; } - if (allowedKeys.Contains(tweakedPubkeyNeg)) + var tweakedPrivkeyNeg = key.sec.Negate().Add(ri.sec).ToPrivateKey(); + var tweakedPubkeyNeg = tweakedPrivkeyNeg.CreatePubKey(); + + if (allowedKeysSet.Contains(tweakedPubkeyNeg)) { usedSlots.Add(i); var sig = tweakedPrivkeyNeg.SignBIP340(msg); - tweakedPrivkeyNeg.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); - keysRequiredLeft = requiredSignatures - result.Signatures.Length; break; - } } - availableKeysLeft = remainingKeys; } - if (keysRequiredLeft > 0) - throw new InvalidOperationException("Not enough valid keys to sign"); - return result; + + return (result.Signatures.Length >= requiredSignatures, result); } @@ -185,18 +243,34 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) { try { - var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); - if (witness.Signatures.Length < requiredSignatures) - return false; var sigs = witness.Signatures .Select(s => SecpSchnorrSignature.TryCreate(Convert.FromHexString(s), out var sig) ? sig : null) - .Where(signature => signature is not null).ToArray(); - return sigs.Count(s => allowedKeys.Any(p => p.ToXOnlyPubKey().SigVerifyBIP340(s, hash))) >= - requiredSignatures; + .Where(signature => signature is not null) + .ToArray(); + + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); + var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); + if (requiredRefundSignatures == 0) + { + return true; + } + + if (VerifyPath(allowedKeys.ToArray(), requiredSignatures, sigs, hash)) + return true; + + + if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) + { + if (VerifyPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, sigs, hash)) + return true; + } + + return false; } catch (Exception e) { return false; } } + } \ No newline at end of file From da40c13a1db8cfd7a95e27635f427406c9dea9df Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 30 Dec 2025 21:43:31 +0100 Subject: [PATCH 2/9] sig_all support --- DotNut/SigAllHandler.cs | 230 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 DotNut/SigAllHandler.cs diff --git a/DotNut/SigAllHandler.cs b/DotNut/SigAllHandler.cs new file mode 100644 index 0000000..6abd34f --- /dev/null +++ b/DotNut/SigAllHandler.cs @@ -0,0 +1,230 @@ +using System.Text; +using System.Text.Json; +using NBitcoin.Secp256k1; + +namespace DotNut; + +//Handles both P2PK and HTLC (if preimage added) +public class SigAllHandler +{ + public List Proofs { get; set; } + public List PrivKeys { get; set; } + public List BlindedMessages { get; set; } + public string? HTLCPreimage { get; set; } + public string? MeltQuoteId { get; set; } + + private Nut10ProofSecret? _firstProofSecret; + + + public bool TrySign(out string? witness) + { + witness = null; + + if ( BlindedMessages is null || Proofs is null || PrivKeys is null || + BlindedMessages.Count == 0 || Proofs.Count == 0 || PrivKeys.Count == 0) + { + return false; + } + + byte[] msg; + try + { + var msgStr = GetMessageToSign(Proofs.ToArray(), BlindedMessages.ToArray(), MeltQuoteId); + if (!ValidateFirstProof(Proofs[0], out var sec) || sec is null) + { + return false; + } + _firstProofSecret = sec; + msg = Encoding.UTF8.GetBytes(msgStr); + } + catch (ArgumentException) + { + return false; + } + + if (_firstProofSecret is not P2PKProofSecret fps) + { + return false; + } + + P2PKWitness witnessObj; + if (fps is HTLCProofSecret s && HTLCPreimage is {} preimage) + { + if (Proofs.First().P2PkE is { } E) + { + witnessObj = s.GenerateBlindWitness(msg, + PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Convert.FromHexString(preimage), + Proofs[0].Id, + E + ); + witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); + return true; + } + witnessObj = + s.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), + Convert.FromHexString(preimage) + ); + witness = JsonSerializer.Serialize((HTLCWitness)witnessObj); + return true; + } + + + if (Proofs.First().P2PkE is { } e2) + { + witnessObj = fps.GenerateBlindWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray(), Proofs[0].Id, e2); + witness = JsonSerializer.Serialize(witnessObj); + return true; + } + witnessObj = fps.GenerateWitness(msg, PrivKeys.Select(pk => (ECPrivKey)pk).ToArray()); + witness = JsonSerializer.Serialize(witnessObj); + return true; + } + + public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, string? meltQuoteId = null) + { + if (inputs is null || inputs.Length == 0) + { + throw new ArgumentException("At least one proof is required for SIG_ALL.", nameof(inputs)); + } + if (outputs is null || outputs.Length == 0) + { + throw new ArgumentException("At least one blinded output is required for SIG_ALL.", nameof(outputs)); + } + if (!ValidateFirstProof(inputs[0], out var firstSecret)) + { + throw new ArgumentException("Provided first proof is invalid"); + } + var msg = new StringBuilder(); + + for (var i = 0; i < inputs.Length; i++) + { + var p = inputs[i]; + + if (p.Secret is not Nut10Secret nut10) + { + throw new ArgumentException("When signing sig_all, every proof must be a nut 10 secret."); + } + + if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) + { + throw new ArgumentException("When signing sig_all, every proof must have identical tags and data."); + } + // serialize as raw object + var secret = JsonSerializer.Serialize((object)p.Secret); + msg.Append(secret); + msg.Append(p.C); + } + + foreach (var b in outputs) + { + msg.Append(b.Amount); + msg.Append(b.B_); + } + + if (meltQuoteId is not null) + { + msg.Append(meltQuoteId); + } + return msg.ToString(); + } + + public static bool VerifySigAllWitness( + Proof[] proofs, + BlindedMessage[] blindedMessages, + P2PKWitness witness, + string? meltQuoteId = null) + { + if (proofs is null || proofs.Length == 0) + { + return false; + } + byte[] msg; + try + { + var msgStr = meltQuoteId is null + ? GetMessageToSign(proofs, blindedMessages) + : GetMessageToSign(proofs, blindedMessages, meltQuoteId); + + msg = Encoding.UTF8.GetBytes(msgStr); + } + catch(Exception ex) + { + return false; + } + + if (proofs[0].Secret is not Nut10Secret nut10) + return false; + + return nut10.ProofSecret switch + { + HTLCProofSecret htlcs => htlcs.VerifyWitness(msg, witness), + P2PKProofSecret p2pks => p2pks.VerifyWitness(msg, witness), + _ => false + }; + } + + public static bool VerifySigAllWitness(Proof[] proofs, BlindedMessage[] blindedMessages, string? meltQuoteId = null) + { + if (proofs is null || proofs.Length == 0) + { + return false; + } + var firstProof = proofs.FirstOrDefault(); + if (firstProof?.Secret is not Nut10Secret { ProofSecret: var proofSecret } || firstProof.Witness is null) + return false; + + P2PKWitness? witness; + try + { + var htlcWitness = JsonSerializer.Deserialize(firstProof.Witness); + if (htlcWitness?.Preimage is not null) + { + witness = htlcWitness; + } + else + { + witness = JsonSerializer.Deserialize(firstProof.Witness); + } + } + catch + { + return false; + } + return witness is not null && VerifySigAllWitness(proofs, blindedMessages, witness, meltQuoteId); + } + + private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? secret) + { + secret = null; + + if (firstProof.Secret is not Nut10Secret nut10) + { + return false; + } + + var builder = nut10.ProofSecret switch + { + HTLCProofSecret htlcs => HTLCBuilder.Load(htlcs), + P2PKProofSecret p2pks => P2PKBuilder.Load(p2pks), + // won't throw exception if there will be a new type of nut10 secret, but will return false + _ => new P2PKBuilder(){SigFlag = null} + }; + + if (builder.SigFlag != "SIG_ALL") + { + return false; + } + + secret = nut10.ProofSecret; + return true; + } + + private static bool CheckIfEqualToFirst(Nut10ProofSecret first, Nut10ProofSecret other) => + first is { } a && other is { } b && + a.Data == b.Data && + ((a.Tags == null && b.Tags == null) || + (a.Tags != null && b.Tags != null && + a.Tags.Length == b.Tags.Length && + a.Tags.Zip(b.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); +} \ No newline at end of file From 2a7a80d7c5fc09c6d7c1a178afd933f431e2ad75 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 30 Dec 2025 21:43:51 +0100 Subject: [PATCH 3/9] fix tests --- DotNut.Tests/UnitTest1.cs | 141 +++++++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 11 deletions(-) diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index 5cc8335..4cb4807 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -1,5 +1,3 @@ -using System.Runtime.Intrinsics.Arm; -using System.Security.Cryptography; using System.Text.Json; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; @@ -271,7 +269,7 @@ public void Nut11_Signatures() var signing_key_three = ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, @@ -293,7 +291,8 @@ public void Nut11_Signatures() var witness = p2pkProofSecret.GenerateWitness(proof, new[] {signing_key_two, signing_key_three}); proof.Witness = JsonSerializer.Serialize(witness); Assert.True(p2pkProofSecret.VerifyWitness(proof.Secret, witness)); - + + // SIG_INPUTS var valid1 = "{\"amount\":1,\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"id\":\"009a1f293253e41e\",\"witness\":\"{\\\"signatures\\\":[\\\"60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383\\\"]}\"}"; @@ -305,8 +304,7 @@ public void Nut11_Signatures() Assert.True(valid1ProofSecretp2pkValue.VerifyWitness(valid1Proof.Secret, valid1ProofWitnessP2pk)); var invalid1 = - @"{""amount"":1,""secret"":""[\""P2PK\"",{\""nonce\"":\""0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\"",\""data\"":\""0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\"",\""tags\"":[[\""pubkeys\"",\""0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\"",\""02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\""],[\""n_sigs\"",\""2\""],[\""sigflag\"",\""SIG_INPUTS\""]]}]"",""C"":""02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904"",""id"":""009a1f293253e41e"",""witness"":""{\""signatures\"":[\""83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\""]}""}"; - + "{\n \"amount\": 1,\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\n \"C\": \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\n \"id\": \"009a1f293253e41e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\"]}\"\n}"; var invalid1Proof = JsonSerializer.Deserialize(invalid1); var invalid1ProofSecret = Assert.IsType(invalid1Proof.Secret); Assert.Equal(P2PKProofSecret.Key, invalid1ProofSecret!.Key); @@ -336,7 +334,7 @@ public void Nut11_Signatures() invalidMultisigProofWitnessP2pk)); var validProofRefund = - "{\"amount\":1,\"id\":\"009a1f293253e41e\",\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"902685f492ef3bb2ca35a47ddbba484a3365d143b9776d453947dcbf1ddf9689\\\",\\\"data\\\":\\\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"locktime\\\",\\\"21\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"witness\":\"{\\\"signatures\\\":[\\\"710507b4bc202355c91ea3c147c0d0189c75e179d995e566336afd759cb342bcad9a593345f559d9b9e108ac2c9b5bd9f0b4b6a295028a98606a0a2e95eb54f7\\\"]}\"}"; + "{\n \"amount\": 64,\n \"C\": \"0257353051c02e2d650dede3159915c8be123ba4f47cf33183c7fedd20bd91a79b\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4bc88ee09d1886c7461d45da205ca3274e1e3d9da2667c4865045cb18265a407\\\",\\\"data\\\":\\\"03d5edeb839be873df2348785506d36565f3b8f390fb931709a422b5a247ddefb1\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"refund\\\",\\\"0234ad87e907e117db1590cc20a3942ffdfd5137aa563d36095d5cf5f96bada122\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"b316c2ff9c15f0c5c3d230e99ad94bc76a11dfccbdc820366a3db7210288f22ef6cedcded1152904ec31056d1d5176d83a2d96df5cd4ff86afdde1c90c63af5e\\\"]}\"\n}"; var validProofRefundParsed = JsonSerializer.Deserialize(validProofRefund); var validProofRefundSecret = Assert.IsType(validProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, validProofRefundSecret!.Key); @@ -347,7 +345,7 @@ public void Nut11_Signatures() var invalidProofRefund = - "{\"amount\":1,\"id\":\"009a1f293253e41e\",\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"64c46e5d30df27286166814b71b5d69801704f23a7ad626b05688fbdb48dcc98\\\",\\\"data\\\":\\\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"locktime\\\",\\\"21\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"witness\":\"{\\\"signatures\\\":[\\\"f661d3dc046d636d47cb3d06586da42c498f0300373d1c2a4f417a44252cdf3809bce207c8888f934dba0d2b1671f1b8622d526840f2d5883e571b462630c1ff\\\"]}\"}"; + "{\n \"amount\": 64,\n \"C\": \"0215865e3b30bdf6f5cdc1ee2c33379d5629bdf2eff2595603d939ff8c65d80586\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0c3d085898f1abf2b5521035f4d0f4ecf68c6a5109f6bc836833a1188f06be65\\\",\\\"data\\\":\\\"03206e0d488387a816bbafd957be51b073432c6c7a403ec4c2a0b27647326c5150\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"99999999999\\\"],[\\\"refund\\\",\\\"026acbcd0fff3a424499c83ec892d3155c9d1984438659f448d9d0f1af3e92276a\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"e5b10d7627ab39bd0cefa219c63752a0026aa5ae754b91a0c7ee2596222f87942c442aca2957166a6b468350c09c9968792784d2ae7c42fc91739b55689f4c7a\\\"]}\"\n}"; var invalidProofRefundParsed = JsonSerializer.Deserialize(invalidProofRefund); var invalidProofRefundSecret = Assert.IsType(invalidProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, invalidProofRefundSecret!.Key); @@ -357,6 +355,127 @@ public void Nut11_Signatures() invalidProofRefundWitnessP2pk)); } + [Fact] + public void Nut11_New_P2PkRules() + { + // since https://github.com/cashubtc/nuts/pull/315 p2pk and htlc behavior will be changed. After locktime, the + // proof will be spendable on both (refund and normal) paths. + + var spendableProof = + "{\n \"amount\": 64,\n \"C\": \"02d7cd858d866fca404b5cb1ffd813946e6d19efa1af00d654080fd20266bdc0b1\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"395162bf2d0add3c66aea9f22c45251dbee6e04bd9282addbb366a94cd4fb482\\\",\\\"data\\\":\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"pubkeys\\\",\\\"0229a91adec8dd9badb228c628a07fc1bf707a9b7d95dd505c490b1766fa7dc541\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"]]}]\"\n}"; + var spendableProofParsed = JsonSerializer.Deserialize(spendableProof); + Assert.NotNull(spendableProofParsed); + var spendableProofSecret = Assert.IsType(spendableProofParsed.Secret); + Assert.Equal(P2PKProofSecret.Key, spendableProofSecret.Key); + var secretValue = Assert.IsType(spendableProofSecret.ProofSecret); + + // "standard path" witness, n_sigs = 2. + // since locktime is expired, it would fail under old conditions. now the proof should remain spendable + var validWitness1 = + "{\"signatures\":[\"6a4dd46f929b4747efe7380d655be5cfc0ea943c679a409ea16d4e40968ce89de885d995937d5b85f24fa33a25df10990c5e11d5397199d779d5cf87d42f6627\",\"0c266fffe2ea2358fb93b5d30dfbcefe52a5bb53d6c85f37d54723613224a256165d20dd095768f168ab2e97bc5a879f7c2a84eee8963c9bcedcd39552dbe093\"]}"; + var validWitness1Parsed = JsonSerializer.Deserialize(validWitness1); + Assert.NotNull(validWitness1Parsed); + Assert.True(secretValue.VerifyWitness(spendableProofParsed.Secret, validWitness1Parsed)); + + // "refund path" witness, n_sigs_refund is omitted, so it's 1 by default + var validWitness2 = + "{\"signatures\":[\"d39631363480adf30433ee25c7cec28237e02b4808d4143469d4f390d4eae6ec97d18ba3cc6494ab1d04372f0838426ea296f25cb4bd8bddb296adc292eeaa96\"]}"; + var validWitness2Parsed = JsonSerializer.Deserialize(validWitness2); + Assert.NotNull(validWitness2Parsed); + Assert.True(secretValue.VerifyWitness(spendableProofParsed.Secret, validWitness2Parsed)); + } + + [Fact] + public void Nut11_SIG_ALL() + { + var swapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var swapRequestParsed = JsonSerializer.Deserialize(swapRequest); + var msgToSign = "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"; + Assert.Equal(msgToSign, SigAllHandler.GetMessageToSign(swapRequestParsed.Inputs, swapRequestParsed.Outputs)); + + var signedSwapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var signedSwapRequestParsed = JsonSerializer.Deserialize(signedSwapRequest); + Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs)); + var witness = JsonSerializer.Deserialize(signedSwapRequestParsed.Inputs.First().Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs, witness)); + + var validSwapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestParsed = JsonSerializer.Deserialize(validSwapRequest); + var witness1 = JsonSerializer.Deserialize(validSwapRequestParsed?.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs, witness1)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs)); + + var invalidSwapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"fa6dd3fac9086c153878dec90b9e37163d38ff2ecf8b37db6470e9d185abbbae\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"024d232312bab25af2e73f41d56864d378edca9109ae8f76e1030e02e585847786\",\n \"witness\": \"{\\\"signatures\\\":[\\\"27b4d260a1186e3b62a26c0d14ffeab3b9f7c3889e78707b8fd3836b473a00601afbd53a2288ad20a624a8bbe3344453215ea075fc0ce479dd8666fd3d9162cc\\\"]}\"\n },\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4007b21fc5f5b1d4920bc0a08b158d98fd0fb2b0b0262b57ff53c6c5d6c2ae8c\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"122222222222222\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02417400f2af09772219c831501afcbab4efb3b2e75175635d5474069608deb641\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"02c0d4fce02a7a0f09e3f1bca952db910b17e81a7ebcbce62cd8dcfb127d21e37b\"\n }\n ]\n}"; + var invalidSwapRequestParsed = JsonSerializer.Deserialize(invalidSwapRequest); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs)); + var witness2 = JsonSerializer.Deserialize(invalidSwapRequestParsed?.Inputs[0].Witness); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs, witness2)); + + var validSwapRequestMultisig = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"04bfd885fc982d553711092d037fdceb7320fd8f96b0d4fd6d31a65b83b94272\\\",\\\"data\\\":\\\"0275e78025b558dbe6cb8fdd032a2e7613ca14fda5c1f4c4e3427f5077a7bd90e4\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"035163650bbd5ed4be7693f40f340346ba548b941074e9138b67ef6c42755f3449\\\",\\\"02817d22a8edc44c4141e192995a7976647c335092199f9e076a170c7336e2f5cc\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03866a09946562482c576ca989d06371e412b221890804c7da8887d321380755be\",\n \"witness\": \"{\\\"signatures\\\":[\\\"be1d72c5ca16a93c5a34f25ec63ce632ddc3176787dac363321af3fd0f55d1927e07451bc451ffe5c682d76688ea9925d7977dffbb15bd79763b527f474734b0\\\",\\\"669d6d10d7ed35395009f222f6c7bdc28a378a1ebb72ee43117be5754648501da3bedf2fd6ff0c7849ac92683538c60af0af504102e40f2d8daca8e08b1ca16b\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestMultisigParsed = JsonSerializer.Deserialize(validSwapRequestMultisig); + var witness3 = JsonSerializer.Deserialize(validSwapRequestMultisigParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs, witness3)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs)); + + var validSwapRequestMultisigRefundLocktime = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"9ea35553beb18d553d0a53120d0175a0991ca6109370338406eed007b26eacd1\\\",\\\"data\\\":\\\"02af21e09300af92e7b48c48afdb12e22933738cfb9bba67b27c00c679aae3ec25\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02637c19143c58b2c58bd378400a7b82bdc91d6dedaeb803b28640ef7d28a887ac\\\",\\\"0345c7fdf7ec7c8e746cca264bf27509eb4edb9ac421f8fbfab1dec64945a4d797\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03dd83536fbbcbb74ccb3c87147df26753fd499cc2c095f74367fff0fb459c312e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"23b58ef28cd22f3dff421121240ddd621deee83a3bc229fd67019c2e338d91e2c61577e081e1375dbab369307bba265e887857110ca3b4bd949211a0a298805f\\\",\\\"7e75948ef1513564fdcecfcbd389deac67c730f7004f8631ba90c0844d3e8c0cf470b656306877df5141f65fd3b7e85445a8452c3323ab273e6d0d44843817ed\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestMultisigRefundLocktimeParsed = + JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktime); + var witness5 = JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktimeParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs, witness5)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs)); + + var validSwapRequestHTLC = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"d730dd70cd7ec6e687829857de8e70aab2b970712f4dbe288343eca20e63c28c\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0350cda8a1d5257dbd6ba8401a9a27384b9ab699e636e986101172167799469b14\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03ff6567e2e6c31db5cb7189dab2b5121930086791c93899e4eff3dda61cb57273\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"a4c00a9ad07f9936e404494fda99a9b935c82d7c053173b304b8663124c81d4b00f64a225f5acf41043ca52b06382722bd04ded0fbeb0fcc404eed3b24778b88\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestHTLCParsed = + JsonSerializer.Deserialize(validSwapRequestHTLC); + var witness6 = JsonSerializer.Deserialize(validSwapRequestHTLCParsed.Inputs[0].Witness); + var b = SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs, + witness6); + Assert.True(b); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs)); + + var invalidSwapRequestHTLC = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"512c4045f12fdfd6f55059669c189e040c37c1ce2f8be104ed6aec296acce4e9\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"03ba83defd31c63f8841d188f0d41b5bb3af1bb3c08d0ba46f8f1d26a4d45e8cad\\\"],[\\\"locktime\\\",\\\"4854185133\\\"],[\\\"refund\\\",\\\"032f1008a79c722e93a1b4b853f85f38283f9ef74ee4c5c91293eb1cc3c5e46e34\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02207abeff828146f1fc3909c74613d5605bd057f16791994b3c91f045b39a6939\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"7816d57871bde5be2e4281065dbe5b15f641d8f1ed9437a3ae556464d6f9b8a0a2e6660337a915f2c26dce1453a416daf682b8fb593b67a0750fce071e0759b9\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; + var invalidSwapRequestHTLCParsed = JsonSerializer.Deserialize(invalidSwapRequestHTLC); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs)); + var witness7 = JsonSerializer.Deserialize(invalidSwapRequestHTLCParsed.Inputs[0].Witness); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs, witness7)); + + var validSwapRequestHTLCMultisig = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"c9b0fabb8007c0db4bef64d5d128cdcf3c79e8bb780c3294adf4c88e96c32647\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"039e6ec7e922abb4162235b3a42965eb11510b07b7461f6b1a17478b1c9c64d100\\\"],[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02ce1bbd2c9a4be8029c9a6435ad601c45677f5cde81f8a7f0ed535e0039d0eb6c\\\",\\\"03c43c00ff57f63cfa9e732f0520c342123e21331d0121139f1b636921eeec095f\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"0344b6f1471cf18a8cbae0e624018c816be5e3a9b04dcb7689f64173c1ae90a3a5\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"98e21672d409cc782c720f203d8284f0af0c8713f18167499f9f101b7050c3e657fb0e57478ebd8bd561c31aa6c30f4cd20ec38c73f5755b7b4ddee693bca5a5\\\",\\\"693f40129dbf905ed9c8008081c694f72a36de354f9f4fa7a61b389cf781f62a0ae0586612fb2eb504faaf897fefb6742309186117f4743bcebcb8e350e975e2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestHTLCMultisigParsed = JsonSerializer.Deserialize(validSwapRequestHTLCMultisig); + var witness8 = JsonSerializer.Deserialize(validSwapRequestHTLCMultisigParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs, witness8)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs)); + + var meltRequest = + "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var meltRequestParsed = JsonSerializer.Deserialize(meltRequest); + var msg2 = + "[\"P2PK\",{\"nonce\":\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\",\"data\":\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b0038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0"; + Assert.Equal(SigAllHandler.GetMessageToSign(meltRequestParsed.Inputs, meltRequestParsed.Outputs, meltRequestParsed.Quote), msg2); + + var meltRequestValid = + "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var meltRequestValidParsed = JsonSerializer.Deserialize(meltRequestValid); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, meltRequestValidParsed.Quote)); + var witness9 = JsonSerializer.Deserialize(meltRequestValidParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, witness9, meltRequestValidParsed.Quote)); + + var meltRequestMultisig = + "{\n \"quote\": \"Db3qEMVwFN2tf_1JxbZp29aL5cVXpSMIwpYfyOVF\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"68d7822538740e4f9c9ebf5183ef6c4501c7a9bca4e509ce2e41e1d62e7b8a99\\\",\\\"data\\\":\\\"0394e841bd59aeadce16380df6174cb29c9fea83b0b65b226575e6d73cc5a1bd59\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"033d892d7ad2a7d53708b7a5a2af101cbcef69522bd368eacf55fcb4f1b0494058\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03a70c42ec9d7192422c7f7a3ad017deda309fb4a2453fcf9357795ea706cc87a9\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ed739970d003f703da2f101a51767b63858f4894468cc334be04aa3befab1617a81e3eef093441afb499974152d279e59d9582a31dc68adbc17ffc22a2516086\\\",\\\"f9efe1c70eb61e7ad8bd615c50ff850410a4135ea73ba5fd8e12a734743ad045e575e9e76ea5c52c8e7908d3ad5c0eaae93337e5c11109e52848dc328d6757a2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var meltRequestMultisigParsed = JsonSerializer.Deserialize(meltRequestMultisig); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, meltRequestMultisigParsed.Quote)); + var witness10 = JsonSerializer.Deserialize(meltRequestMultisigParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, witness10, meltRequestMultisigParsed.Quote)); + } + [Fact] public void Nut12Tests_Hash_e() { @@ -396,7 +515,7 @@ public void Nut12Tests_ProofDLEQ() public void Nut14Tests_HTLCSecret() { var htlcSecretStr = - "[\n \"HTLC\",\n {\n \"nonce\": \"da62796403af76c80cd6ce9153ed3746\",\n \"data\": \"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54\",\n \"tags\": [\n [\n \"pubkeys\",\n \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\"\n ],\n [\n \"locktime\",\n \"1689418329\"\n ], \n [\n \"refund\",\n \"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\"\n ]\n ]\n }\n]"; + "[\n \"HTLC\",\n {\n \"nonce\": \"da62796403af76c80cd6ce9153ed3746\",\n \"data\": \"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c\",\n \"tags\": [\n [\n \"pubkeys\",\n \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\"\n ],\n [\n \"locktime\",\n \"1689418329\"\n ], \n [\n \"refund\",\n \"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\"\n ]\n ]\n }\n]"; var secret = JsonSerializer.Deserialize(htlcSecretStr); var nut10Secret = Assert.IsType(secret); Assert.Equal(HTLCProofSecret.Key, nut10Secret.Key); @@ -784,7 +903,7 @@ public void Nut26_Flow() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, @@ -822,7 +941,7 @@ public void Nut26_Flow_WithRandomE() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, From 7c6100d4609b83a9fa3596a6e4a7b85bf2793e6f Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 30 Dec 2025 21:43:51 +0100 Subject: [PATCH 4/9] fix tests --- DotNut.Tests/UnitTest1.cs | 141 +++++++++++++++++++++++++++++++++++--- DotNut.sln | 14 ---- 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index 5cc8335..4cb4807 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -1,5 +1,3 @@ -using System.Runtime.Intrinsics.Arm; -using System.Security.Cryptography; using System.Text.Json; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; @@ -271,7 +269,7 @@ public void Nut11_Signatures() var signing_key_three = ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, @@ -293,7 +291,8 @@ public void Nut11_Signatures() var witness = p2pkProofSecret.GenerateWitness(proof, new[] {signing_key_two, signing_key_three}); proof.Witness = JsonSerializer.Serialize(witness); Assert.True(p2pkProofSecret.VerifyWitness(proof.Secret, witness)); - + + // SIG_INPUTS var valid1 = "{\"amount\":1,\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"id\":\"009a1f293253e41e\",\"witness\":\"{\\\"signatures\\\":[\\\"60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383\\\"]}\"}"; @@ -305,8 +304,7 @@ public void Nut11_Signatures() Assert.True(valid1ProofSecretp2pkValue.VerifyWitness(valid1Proof.Secret, valid1ProofWitnessP2pk)); var invalid1 = - @"{""amount"":1,""secret"":""[\""P2PK\"",{\""nonce\"":\""0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\"",\""data\"":\""0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\"",\""tags\"":[[\""pubkeys\"",\""0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\"",\""02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\""],[\""n_sigs\"",\""2\""],[\""sigflag\"",\""SIG_INPUTS\""]]}]"",""C"":""02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904"",""id"":""009a1f293253e41e"",""witness"":""{\""signatures\"":[\""83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\""]}""}"; - + "{\n \"amount\": 1,\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\n \"C\": \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\n \"id\": \"009a1f293253e41e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\\\"]}\"\n}"; var invalid1Proof = JsonSerializer.Deserialize(invalid1); var invalid1ProofSecret = Assert.IsType(invalid1Proof.Secret); Assert.Equal(P2PKProofSecret.Key, invalid1ProofSecret!.Key); @@ -336,7 +334,7 @@ public void Nut11_Signatures() invalidMultisigProofWitnessP2pk)); var validProofRefund = - "{\"amount\":1,\"id\":\"009a1f293253e41e\",\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"902685f492ef3bb2ca35a47ddbba484a3365d143b9776d453947dcbf1ddf9689\\\",\\\"data\\\":\\\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"locktime\\\",\\\"21\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"witness\":\"{\\\"signatures\\\":[\\\"710507b4bc202355c91ea3c147c0d0189c75e179d995e566336afd759cb342bcad9a593345f559d9b9e108ac2c9b5bd9f0b4b6a295028a98606a0a2e95eb54f7\\\"]}\"}"; + "{\n \"amount\": 64,\n \"C\": \"0257353051c02e2d650dede3159915c8be123ba4f47cf33183c7fedd20bd91a79b\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4bc88ee09d1886c7461d45da205ca3274e1e3d9da2667c4865045cb18265a407\\\",\\\"data\\\":\\\"03d5edeb839be873df2348785506d36565f3b8f390fb931709a422b5a247ddefb1\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"refund\\\",\\\"0234ad87e907e117db1590cc20a3942ffdfd5137aa563d36095d5cf5f96bada122\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"b316c2ff9c15f0c5c3d230e99ad94bc76a11dfccbdc820366a3db7210288f22ef6cedcded1152904ec31056d1d5176d83a2d96df5cd4ff86afdde1c90c63af5e\\\"]}\"\n}"; var validProofRefundParsed = JsonSerializer.Deserialize(validProofRefund); var validProofRefundSecret = Assert.IsType(validProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, validProofRefundSecret!.Key); @@ -347,7 +345,7 @@ public void Nut11_Signatures() var invalidProofRefund = - "{\"amount\":1,\"id\":\"009a1f293253e41e\",\"secret\":\"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"64c46e5d30df27286166814b71b5d69801704f23a7ad626b05688fbdb48dcc98\\\",\\\"data\\\":\\\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\",\\\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\\\"],[\\\"locktime\\\",\\\"21\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\\\"],[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]]}]\",\"C\":\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\",\"witness\":\"{\\\"signatures\\\":[\\\"f661d3dc046d636d47cb3d06586da42c498f0300373d1c2a4f417a44252cdf3809bce207c8888f934dba0d2b1671f1b8622d526840f2d5883e571b462630c1ff\\\"]}\"}"; + "{\n \"amount\": 64,\n \"C\": \"0215865e3b30bdf6f5cdc1ee2c33379d5629bdf2eff2595603d939ff8c65d80586\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"0c3d085898f1abf2b5521035f4d0f4ecf68c6a5109f6bc836833a1188f06be65\\\",\\\"data\\\":\\\"03206e0d488387a816bbafd957be51b073432c6c7a403ec4c2a0b27647326c5150\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"99999999999\\\"],[\\\"refund\\\",\\\"026acbcd0fff3a424499c83ec892d3155c9d1984438659f448d9d0f1af3e92276a\\\"]]}]\",\n \"witness\": \"{\\\"signatures\\\":[\\\"e5b10d7627ab39bd0cefa219c63752a0026aa5ae754b91a0c7ee2596222f87942c442aca2957166a6b468350c09c9968792784d2ae7c42fc91739b55689f4c7a\\\"]}\"\n}"; var invalidProofRefundParsed = JsonSerializer.Deserialize(invalidProofRefund); var invalidProofRefundSecret = Assert.IsType(invalidProofRefundParsed.Secret); Assert.Equal(P2PKProofSecret.Key, invalidProofRefundSecret!.Key); @@ -357,6 +355,127 @@ public void Nut11_Signatures() invalidProofRefundWitnessP2pk)); } + [Fact] + public void Nut11_New_P2PkRules() + { + // since https://github.com/cashubtc/nuts/pull/315 p2pk and htlc behavior will be changed. After locktime, the + // proof will be spendable on both (refund and normal) paths. + + var spendableProof = + "{\n \"amount\": 64,\n \"C\": \"02d7cd858d866fca404b5cb1ffd813946e6d19efa1af00d654080fd20266bdc0b1\",\n \"id\": \"001b6c716bf42c7e\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"395162bf2d0add3c66aea9f22c45251dbee6e04bd9282addbb366a94cd4fb482\\\",\\\"data\\\":\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"21\\\"],[\\\"pubkeys\\\",\\\"0229a91adec8dd9badb228c628a07fc1bf707a9b7d95dd505c490b1766fa7dc541\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"refund\\\",\\\"03ab50a667926fac858bac540766254c14b2b0334d10e8ec766455310224bbecf4\\\",\\\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\\\"]]}]\"\n}"; + var spendableProofParsed = JsonSerializer.Deserialize(spendableProof); + Assert.NotNull(spendableProofParsed); + var spendableProofSecret = Assert.IsType(spendableProofParsed.Secret); + Assert.Equal(P2PKProofSecret.Key, spendableProofSecret.Key); + var secretValue = Assert.IsType(spendableProofSecret.ProofSecret); + + // "standard path" witness, n_sigs = 2. + // since locktime is expired, it would fail under old conditions. now the proof should remain spendable + var validWitness1 = + "{\"signatures\":[\"6a4dd46f929b4747efe7380d655be5cfc0ea943c679a409ea16d4e40968ce89de885d995937d5b85f24fa33a25df10990c5e11d5397199d779d5cf87d42f6627\",\"0c266fffe2ea2358fb93b5d30dfbcefe52a5bb53d6c85f37d54723613224a256165d20dd095768f168ab2e97bc5a879f7c2a84eee8963c9bcedcd39552dbe093\"]}"; + var validWitness1Parsed = JsonSerializer.Deserialize(validWitness1); + Assert.NotNull(validWitness1Parsed); + Assert.True(secretValue.VerifyWitness(spendableProofParsed.Secret, validWitness1Parsed)); + + // "refund path" witness, n_sigs_refund is omitted, so it's 1 by default + var validWitness2 = + "{\"signatures\":[\"d39631363480adf30433ee25c7cec28237e02b4808d4143469d4f390d4eae6ec97d18ba3cc6494ab1d04372f0838426ea296f25cb4bd8bddb296adc292eeaa96\"]}"; + var validWitness2Parsed = JsonSerializer.Deserialize(validWitness2); + Assert.NotNull(validWitness2Parsed); + Assert.True(secretValue.VerifyWitness(spendableProofParsed.Secret, validWitness2Parsed)); + } + + [Fact] + public void Nut11_SIG_ALL() + { + var swapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var swapRequestParsed = JsonSerializer.Deserialize(swapRequest); + var msgToSign = "[\"P2PK\",{\"nonce\":\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\",\"data\":\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd2038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"; + Assert.Equal(msgToSign, SigAllHandler.GetMessageToSign(swapRequestParsed.Inputs, swapRequestParsed.Outputs)); + + var signedSwapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var signedSwapRequestParsed = JsonSerializer.Deserialize(signedSwapRequest); + Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs)); + var witness = JsonSerializer.Deserialize(signedSwapRequestParsed.Inputs.First().Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(signedSwapRequestParsed.Inputs, signedSwapRequestParsed.Outputs, witness)); + + var validSwapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"c7f280eb55c1e8564e03db06973e94bc9b666d9e1ca42ad278408fe625950303\\\",\\\"data\\\":\\\"030d8acedfe072c9fa449a1efe0817157403fbec460d8e79f957966056e5dd76c1\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ce017ca25b1b97df2f72e4b49f69ac26a240ce14b3690a8fe619d41ccc42d3c1282e073f85acd36dc50011638906f35b56615f24e4d03e8effe8257f6a808538\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestParsed = JsonSerializer.Deserialize(validSwapRequest); + var witness1 = JsonSerializer.Deserialize(validSwapRequestParsed?.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs, witness1)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestParsed.Inputs, validSwapRequestParsed.Outputs)); + + var invalidSwapRequest = + "{\n \"inputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"fa6dd3fac9086c153878dec90b9e37163d38ff2ecf8b37db6470e9d185abbbae\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"024d232312bab25af2e73f41d56864d378edca9109ae8f76e1030e02e585847786\",\n \"witness\": \"{\\\"signatures\\\":[\\\"27b4d260a1186e3b62a26c0d14ffeab3b9f7c3889e78707b8fd3836b473a00601afbd53a2288ad20a624a8bbe3344453215ea075fc0ce479dd8666fd3d9162cc\\\"]}\"\n },\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"4007b21fc5f5b1d4920bc0a08b158d98fd0fb2b0b0262b57ff53c6c5d6c2ae8c\\\",\\\"data\\\":\\\"033b42b04e659fed13b669f8b16cdaffc3ee5738608810cf97a7631d09bd01399d\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"122222222222222\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02417400f2af09772219c831501afcbab4efb3b2e75175635d5474069608deb641\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"02c0d4fce02a7a0f09e3f1bca952db910b17e81a7ebcbce62cd8dcfb127d21e37b\"\n }\n ]\n}"; + var invalidSwapRequestParsed = JsonSerializer.Deserialize(invalidSwapRequest); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs)); + var witness2 = JsonSerializer.Deserialize(invalidSwapRequestParsed?.Inputs[0].Witness); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestParsed.Inputs, invalidSwapRequestParsed.Outputs, witness2)); + + var validSwapRequestMultisig = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"04bfd885fc982d553711092d037fdceb7320fd8f96b0d4fd6d31a65b83b94272\\\",\\\"data\\\":\\\"0275e78025b558dbe6cb8fdd032a2e7613ca14fda5c1f4c4e3427f5077a7bd90e4\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"035163650bbd5ed4be7693f40f340346ba548b941074e9138b67ef6c42755f3449\\\",\\\"02817d22a8edc44c4141e192995a7976647c335092199f9e076a170c7336e2f5cc\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03866a09946562482c576ca989d06371e412b221890804c7da8887d321380755be\",\n \"witness\": \"{\\\"signatures\\\":[\\\"be1d72c5ca16a93c5a34f25ec63ce632ddc3176787dac363321af3fd0f55d1927e07451bc451ffe5c682d76688ea9925d7977dffbb15bd79763b527f474734b0\\\",\\\"669d6d10d7ed35395009f222f6c7bdc28a378a1ebb72ee43117be5754648501da3bedf2fd6ff0c7849ac92683538c60af0af504102e40f2d8daca8e08b1ca16b\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestMultisigParsed = JsonSerializer.Deserialize(validSwapRequestMultisig); + var witness3 = JsonSerializer.Deserialize(validSwapRequestMultisigParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs, witness3)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigParsed.Inputs, validSwapRequestMultisigParsed.Outputs)); + + var validSwapRequestMultisigRefundLocktime = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"9ea35553beb18d553d0a53120d0175a0991ca6109370338406eed007b26eacd1\\\",\\\"data\\\":\\\"02af21e09300af92e7b48c48afdb12e22933738cfb9bba67b27c00c679aae3ec25\\\",\\\"tags\\\":[[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02637c19143c58b2c58bd378400a7b82bdc91d6dedaeb803b28640ef7d28a887ac\\\",\\\"0345c7fdf7ec7c8e746cca264bf27509eb4edb9ac421f8fbfab1dec64945a4d797\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03dd83536fbbcbb74ccb3c87147df26753fd499cc2c095f74367fff0fb459c312e\",\n \"witness\": \"{\\\"signatures\\\":[\\\"23b58ef28cd22f3dff421121240ddd621deee83a3bc229fd67019c2e338d91e2c61577e081e1375dbab369307bba265e887857110ca3b4bd949211a0a298805f\\\",\\\"7e75948ef1513564fdcecfcbd389deac67c730f7004f8631ba90c0844d3e8c0cf470b656306877df5141f65fd3b7e85445a8452c3323ab273e6d0d44843817ed\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestMultisigRefundLocktimeParsed = + JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktime); + var witness5 = JsonSerializer.Deserialize(validSwapRequestMultisigRefundLocktimeParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs, witness5)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestMultisigRefundLocktimeParsed.Inputs, validSwapRequestMultisigRefundLocktimeParsed.Outputs)); + + var validSwapRequestHTLC = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"d730dd70cd7ec6e687829857de8e70aab2b970712f4dbe288343eca20e63c28c\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"0350cda8a1d5257dbd6ba8401a9a27384b9ab699e636e986101172167799469b14\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03ff6567e2e6c31db5cb7189dab2b5121930086791c93899e4eff3dda61cb57273\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"a4c00a9ad07f9936e404494fda99a9b935c82d7c053173b304b8663124c81d4b00f64a225f5acf41043ca52b06382722bd04ded0fbeb0fcc404eed3b24778b88\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestHTLCParsed = + JsonSerializer.Deserialize(validSwapRequestHTLC); + var witness6 = JsonSerializer.Deserialize(validSwapRequestHTLCParsed.Inputs[0].Witness); + var b = SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs, + witness6); + Assert.True(b); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCParsed.Inputs, validSwapRequestHTLCParsed.Outputs)); + + var invalidSwapRequestHTLC = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"512c4045f12fdfd6f55059669c189e040c37c1ce2f8be104ed6aec296acce4e9\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"03ba83defd31c63f8841d188f0d41b5bb3af1bb3c08d0ba46f8f1d26a4d45e8cad\\\"],[\\\"locktime\\\",\\\"4854185133\\\"],[\\\"refund\\\",\\\"032f1008a79c722e93a1b4b853f85f38283f9ef74ee4c5c91293eb1cc3c5e46e34\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02207abeff828146f1fc3909c74613d5605bd057f16791994b3c91f045b39a6939\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"7816d57871bde5be2e4281065dbe5b15f641d8f1ed9437a3ae556464d6f9b8a0a2e6660337a915f2c26dce1453a416daf682b8fb593b67a0750fce071e0759b9\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n },\n {\n \"amount\": 1,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"03afe7c87e32d436f0957f1d70a2bca025822a84a8623e3a33aed0a167016e0ca5\"\n }\n ]\n}"; + var invalidSwapRequestHTLCParsed = JsonSerializer.Deserialize(invalidSwapRequestHTLC); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs)); + var witness7 = JsonSerializer.Deserialize(invalidSwapRequestHTLCParsed.Inputs[0].Witness); + Assert.False(SigAllHandler.VerifySigAllWitness(invalidSwapRequestHTLCParsed.Inputs, invalidSwapRequestHTLCParsed.Outputs, witness7)); + + var validSwapRequestHTLCMultisig = + "{\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"HTLC\\\",{\\\"nonce\\\":\\\"c9b0fabb8007c0db4bef64d5d128cdcf3c79e8bb780c3294adf4c88e96c32647\\\",\\\"data\\\":\\\"ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"039e6ec7e922abb4162235b3a42965eb11510b07b7461f6b1a17478b1c9c64d100\\\"],[\\\"locktime\\\",\\\"1\\\"],[\\\"refund\\\",\\\"02ce1bbd2c9a4be8029c9a6435ad601c45677f5cde81f8a7f0ed535e0039d0eb6c\\\",\\\"03c43c00ff57f63cfa9e732f0520c342123e21331d0121139f1b636921eeec095f\\\"],[\\\"n_sigs_refund\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"0344b6f1471cf18a8cbae0e624018c816be5e3a9b04dcb7689f64173c1ae90a3a5\",\n \"witness\": \"{\\\"preimage\\\":\\\"0000000000000000000000000000000000000000000000000000000000000001\\\",\\\"signatures\\\":[\\\"98e21672d409cc782c720f203d8284f0af0c8713f18167499f9f101b7050c3e657fb0e57478ebd8bd561c31aa6c30f4cd20ec38c73f5755b7b4ddee693bca5a5\\\",\\\"693f40129dbf905ed9c8008081c694f72a36de354f9f4fa7a61b389cf781f62a0ae0586612fb2eb504faaf897fefb6742309186117f4743bcebcb8e350e975e2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var validSwapRequestHTLCMultisigParsed = JsonSerializer.Deserialize(validSwapRequestHTLCMultisig); + var witness8 = JsonSerializer.Deserialize(validSwapRequestHTLCMultisigParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs, witness8)); + Assert.True(SigAllHandler.VerifySigAllWitness(validSwapRequestHTLCMultisigParsed.Inputs, validSwapRequestHTLCMultisigParsed.Outputs)); + + var meltRequest = + "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var meltRequestParsed = JsonSerializer.Deserialize(meltRequest); + var msg2 = + "[\"P2PK\",{\"nonce\":\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\",\"data\":\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\",\"tags\":[[\"sigflag\",\"SIG_ALL\"]]}]02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b0038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0"; + Assert.Equal(SigAllHandler.GetMessageToSign(meltRequestParsed.Inputs, meltRequestParsed.Outputs, meltRequestParsed.Quote), msg2); + + var meltRequestValid = + "{\n \"quote\": \"cF8911fzT88aEi1d-6boZZkq5lYxbUSVs-HbJxK0\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"bbf9edf441d17097e39f5095a3313ba24d3055ab8a32f758ff41c10d45c4f3de\\\",\\\"data\\\":\\\"029116d32e7da635c8feeb9f1f4559eb3d9b42d400f9d22a64834d89cde0eb6835\\\",\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"02a9d461ff36448469dccf828fa143833ae71c689886ac51b62c8d61ddaa10028b\",\n \"witness\": \"{\\\"signatures\\\":[\\\"478224fbe715e34f78cb33451db6fcf8ab948afb8bd04ff1a952c92e562ac0f7c1cb5e61809410635be0aa94d0448f7f7959bd5762cc3802b0a00ff58b2da747\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var meltRequestValidParsed = JsonSerializer.Deserialize(meltRequestValid); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, meltRequestValidParsed.Quote)); + var witness9 = JsonSerializer.Deserialize(meltRequestValidParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestValidParsed.Inputs, meltRequestValidParsed.Outputs, witness9, meltRequestValidParsed.Quote)); + + var meltRequestMultisig = + "{\n \"quote\": \"Db3qEMVwFN2tf_1JxbZp29aL5cVXpSMIwpYfyOVF\",\n \"inputs\": [\n {\n \"amount\": 2,\n \"id\": \"00bfa73302d12ffd\",\n \"secret\": \"[\\\"P2PK\\\",{\\\"nonce\\\":\\\"68d7822538740e4f9c9ebf5183ef6c4501c7a9bca4e509ce2e41e1d62e7b8a99\\\",\\\"data\\\":\\\"0394e841bd59aeadce16380df6174cb29c9fea83b0b65b226575e6d73cc5a1bd59\\\",\\\"tags\\\":[[\\\"pubkeys\\\",\\\"033d892d7ad2a7d53708b7a5a2af101cbcef69522bd368eacf55fcb4f1b0494058\\\"],[\\\"n_sigs\\\",\\\"2\\\"],[\\\"sigflag\\\",\\\"SIG_ALL\\\"]]}]\",\n \"C\": \"03a70c42ec9d7192422c7f7a3ad017deda309fb4a2453fcf9357795ea706cc87a9\",\n \"witness\": \"{\\\"signatures\\\":[\\\"ed739970d003f703da2f101a51767b63858f4894468cc334be04aa3befab1617a81e3eef093441afb499974152d279e59d9582a31dc68adbc17ffc22a2516086\\\",\\\"f9efe1c70eb61e7ad8bd615c50ff850410a4135ea73ba5fd8e12a734743ad045e575e9e76ea5c52c8e7908d3ad5c0eaae93337e5c11109e52848dc328d6757a2\\\"]}\"\n }\n ],\n \"outputs\": [\n {\n \"amount\": 0,\n \"id\": \"00bfa73302d12ffd\",\n \"B_\": \"038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39\"\n }\n ]\n}"; + var meltRequestMultisigParsed = JsonSerializer.Deserialize(meltRequestMultisig); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, meltRequestMultisigParsed.Quote)); + var witness10 = JsonSerializer.Deserialize(meltRequestMultisigParsed.Inputs[0].Witness); + Assert.True(SigAllHandler.VerifySigAllWitness(meltRequestMultisigParsed.Inputs, meltRequestMultisigParsed.Outputs, witness10, meltRequestMultisigParsed.Quote)); + } + [Fact] public void Nut12Tests_Hash_e() { @@ -396,7 +515,7 @@ public void Nut12Tests_ProofDLEQ() public void Nut14Tests_HTLCSecret() { var htlcSecretStr = - "[\n \"HTLC\",\n {\n \"nonce\": \"da62796403af76c80cd6ce9153ed3746\",\n \"data\": \"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54\",\n \"tags\": [\n [\n \"pubkeys\",\n \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\"\n ],\n [\n \"locktime\",\n \"1689418329\"\n ], \n [\n \"refund\",\n \"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\"\n ]\n ]\n }\n]"; + "[\n \"HTLC\",\n {\n \"nonce\": \"da62796403af76c80cd6ce9153ed3746\",\n \"data\": \"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c\",\n \"tags\": [\n [\n \"pubkeys\",\n \"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\"\n ],\n [\n \"locktime\",\n \"1689418329\"\n ], \n [\n \"refund\",\n \"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\"\n ]\n ]\n }\n]"; var secret = JsonSerializer.Deserialize(htlcSecretStr); var nut10Secret = Assert.IsType(secret); Assert.Equal(HTLCProofSecret.Key, nut10Secret.Key); @@ -784,7 +903,7 @@ public void Nut26_Flow() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, @@ -822,7 +941,7 @@ public void Nut26_Flow_WithRandomE() var keysetId = new KeysetId("009a1f293253e41e"); - var conditions = new P2PkBuilder() + var conditions = new P2PKBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, diff --git a/DotNut.sln b/DotNut.sln index f648970..c8a95fd 100644 --- a/DotNut.sln +++ b/DotNut.sln @@ -8,8 +8,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Nostr", "DotNut.Nost EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNut.Demo", "DotNut.Demo\DotNut.Demo.csproj", "{305097F3-A4E5-4511-8E4E-0C4C12A953C6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{6D20E7A3-784A-4394-AD7C-703AEF5A44CB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,18 +66,6 @@ Global {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x64.Build.0 = Release|Any CPU {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x86.ActiveCfg = Release|Any CPU {305097F3-A4E5-4511-8E4E-0C4C12A953C6}.Release|x86.Build.0 = Release|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x64.ActiveCfg = Debug|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x64.Build.0 = Debug|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x86.ActiveCfg = Debug|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Debug|x86.Build.0 = Debug|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|Any CPU.Build.0 = Release|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x64.ActiveCfg = Release|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x64.Build.0 = Release|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x86.ActiveCfg = Release|Any CPU - {6D20E7A3-784A-4394-AD7C-703AEF5A44CB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 2fa23d29573d24ffb1fdba25ce30711ba3d24c6a Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 30 Dec 2025 22:02:09 +0100 Subject: [PATCH 5/9] code rabbit fixes --- DotNut/P2PKProofSecret.cs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 7180c1b..11e6824 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -21,19 +21,19 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; - if (builder.Lock.HasValue && builder.Lock.Value.ToUnixTimeSeconds() < DateTimeOffset.Now.ToUnixTimeSeconds()) + if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() <= DateTimeOffset.Now.ToUnixTimeSeconds()) { - if (builder.RefundPubkeys == null) - { - requiredSignatures = 0; // proof is spendable without any signature - return []; - } - requiredSignatures = builder.RefundSignatureThreshold ?? 1; - return builder.RefundPubkeys ?? []; + requiredSignatures = null; // there's no refund condition, or timelock didn't expire yet :/ + return []; } - - requiredSignatures = null; // there's no refund condition, or timelock didn't expire yet :/ - return []; + + if (builder.RefundPubkeys == null) + { + requiredSignatures = 0; // proof is spendable without any signature + return []; + } + requiredSignatures = builder.RefundSignatureThreshold ?? 1; + return builder.RefundPubkeys ?? []; } /* @@ -70,17 +70,21 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) return null; } - // try notmal path + // try normal path var (isValid, result) = TrySignPath(allowedKeys.ToArray(), requiredSignatures, keys, msg); if (isValid) + { return result; + } // if it's after locktime - try refund path if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { (isValid, result) = TrySignPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, msg); if (isValid) + { return result; + } } throw new InvalidOperationException("Not enough valid keys to sign!"); @@ -158,17 +162,21 @@ private bool VerifyPath(ECPubKey[] allowedKeys, int requiredSignatures, var allowedRefundKeys = GetAllowedRefundPubkeys(out var requiredRefundSignatures); if (requiredRefundSignatures == 0) - return new P2PKWitness(); + return null; var (isValid, result) = TrySignBlindPath(allowedKeys.ToArray(), requiredSignatures, keys, keysetId, P2PkE, msg); if (isValid) + { return result; + } if (requiredRefundSignatures.HasValue && allowedRefundKeys.Any()) { (isValid, result) = TrySignBlindPath(allowedRefundKeys.ToArray(), requiredRefundSignatures.Value, keys, keysetId, P2PkE, msg); if (isValid) + { return result; + } } throw new InvalidOperationException("Not enough valid keys to sign any blind path"); From 25d65b60c6d8cc41adc27a2dd4a5a35ffb2a3fa2 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 30 Dec 2025 22:14:51 +0100 Subject: [PATCH 6/9] fix comparision bug --- DotNut/P2PKProofSecret.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 11e6824..0452fe1 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -21,7 +21,7 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) { var builder = Builder; - if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() <= DateTimeOffset.Now.ToUnixTimeSeconds()) + if (!builder.Lock.HasValue || builder.Lock.Value.ToUnixTimeSeconds() >= DateTimeOffset.Now.ToUnixTimeSeconds()) { requiredSignatures = null; // there's no refund condition, or timelock didn't expire yet :/ return []; From 8dfdf1ff5f731e29752bceeb444a2f6a2c2ca3f7 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Wed, 31 Dec 2025 00:04:50 +0100 Subject: [PATCH 7/9] don't get rekt by validating the same signaturee --- DotNut/P2PKProofSecret.cs | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 0452fe1..8f78277 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -112,17 +112,7 @@ public virtual ECPubKey[] GetAllowedRefundPubkeys(out int? requiredSignatures) return (result.Signatures.Length >= requiredSignatures, result); } - 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; - } @@ -280,5 +270,30 @@ public virtual bool VerifyWitnessHash(byte[] hash, P2PKWitness witness) return false; } } + + 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 usedKeyIndices = new HashSet(); + + foreach (var sig in sigs) + { + for (int i = 0; i < xonlyKeys.Length; i++) + { + if (!usedKeyIndices.Contains(i) && xonlyKeys[i].SigVerifyBIP340(sig, hash)) + { + usedKeyIndices.Add(i); + break; + } + } + } + + return usedKeyIndices.Count >= requiredSignatures; + } } \ No newline at end of file From bdbd3742a5ed45b16a05119b3beadcf7e8381bd4 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Tue, 13 Jan 2026 01:10:08 +0100 Subject: [PATCH 8/9] fix P2PKBuilder.cs Add Equals() to Nut10ProofSecret --- DotNut/Nut10ProofSecret.cs | 60 +++++++++++++++++++++++++++++++++++++- DotNut/P2PKBuilder.cs | 20 +++++++++++-- DotNut/SigAllHandler.cs | 10 +------ 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/DotNut/Nut10ProofSecret.cs b/DotNut/Nut10ProofSecret.cs index e1a345b..75c0451 100644 --- a/DotNut/Nut10ProofSecret.cs +++ b/DotNut/Nut10ProofSecret.cs @@ -4,7 +4,6 @@ namespace DotNut; public class Nut10ProofSecret { - [JsonPropertyName("nonce")] public string Nonce { get; set; } @@ -15,4 +14,63 @@ public class Nut10ProofSecret [JsonPropertyName("tags")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string[][]? Tags { get; set; } + + public override bool Equals(object obj) => this.Equals(obj as Nut10ProofSecret); + public bool Equals(Nut10ProofSecret s) + { + if (s is null) + { + return false; + } + + if (Object.ReferenceEquals(this, s)) + { + return true; + } + + if (this.GetType() != s.GetType()) + { + return false; + } + + + return + this.Nonce == s.Nonce && + this.Data == s.Data && + ((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)))); + } + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(this.Nonce); + hash.Add(this.Data); + + if (this.Tags == null) + { + return hash.ToHashCode(); + }; + foreach (var tagArray in this.Tags) + { + foreach (var tag in tagArray) + { + hash.Add(tag); + } + } + return hash.ToHashCode(); + } + + public static bool operator ==(Nut10ProofSecret first, Nut10ProofSecret second) + { + if (first is null) + { + return second is null; + } + return first.Equals(second); + } + + public static bool operator !=(Nut10ProofSecret first, Nut10ProofSecret second) => !(first == second); + } \ No newline at end of file diff --git a/DotNut/P2PKBuilder.cs b/DotNut/P2PKBuilder.cs index 55ad0f9..f2196e3 100644 --- a/DotNut/P2PKBuilder.cs +++ b/DotNut/P2PKBuilder.cs @@ -18,6 +18,7 @@ public class P2PKBuilder public P2PKProofSecret Build() { + Validate(); var tags = new List(); if (Pubkeys.Length > 1) { @@ -36,10 +37,10 @@ public P2PKProofSecret Build() { tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())) .ToArray()); + RefundSignatureThreshold ??= 1; + } - if (RefundSignatureThreshold is { } refundSignatureThreshold - && RefundPubkeys is {} refundKeys - && refundKeys.Length >= refundSignatureThreshold) + if (RefundSignatureThreshold is { } refundSignatureThreshold and > 1) { tags.Add(new[] {"n_sigs_refund", refundSignatureThreshold.ToString() }); } @@ -110,6 +111,19 @@ public static P2PKBuilder Load(P2PKProofSecret proofSecret) return builder; } + private void Validate() + { + if (this.Pubkeys.Count() < SignatureThreshold) + { + throw new ArgumentException("Signature threshold bigger than provided pubkeys count!"); + } + if(this.RefundSignatureThreshold is not null + && (RefundPubkeys is null || RefundPubkeys.Length < RefundSignatureThreshold)) + { + throw new ArgumentException("Signature threshold bigger than provided pubkeys count!"); + } + } + /* * ========================= diff --git a/DotNut/SigAllHandler.cs b/DotNut/SigAllHandler.cs index 6abd34f..641383c 100644 --- a/DotNut/SigAllHandler.cs +++ b/DotNut/SigAllHandler.cs @@ -106,7 +106,7 @@ public static string GetMessageToSign(Proof[] inputs, BlindedMessage[] outputs, throw new ArgumentException("When signing sig_all, every proof must be a nut 10 secret."); } - if (!CheckIfEqualToFirst(firstSecret, nut10.ProofSecret)) + if (firstSecret != nut10.ProofSecret) { throw new ArgumentException("When signing sig_all, every proof must have identical tags and data."); } @@ -219,12 +219,4 @@ private static bool ValidateFirstProof(Proof firstProof, out Nut10ProofSecret? s secret = nut10.ProofSecret; return true; } - - private static bool CheckIfEqualToFirst(Nut10ProofSecret first, Nut10ProofSecret other) => - first is { } a && other is { } b && - a.Data == b.Data && - ((a.Tags == null && b.Tags == null) || - (a.Tags != null && b.Tags != null && - a.Tags.Length == b.Tags.Length && - a.Tags.Zip(b.Tags).All(pair => pair.First.SequenceEqual(pair.Second)))); } \ No newline at end of file From 1955fdf5659265ce2eb62e61af910e2b2e7eaa0b Mon Sep 17 00:00:00 2001 From: d4r <50369025+d4rp4t@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:34:21 +0100 Subject: [PATCH 9/9] Update DotNut/Nut10ProofSecret.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- DotNut/Nut10ProofSecret.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/Nut10ProofSecret.cs b/DotNut/Nut10ProofSecret.cs index 75c0451..3389ba1 100644 --- a/DotNut/Nut10ProofSecret.cs +++ b/DotNut/Nut10ProofSecret.cs @@ -51,7 +51,7 @@ public override int GetHashCode() if (this.Tags == null) { return hash.ToHashCode(); - }; + } foreach (var tagArray in this.Tags) { foreach (var tag in tagArray)