From a3f877a667fdaf44be4b58282b2a567f297c7654 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 17 Oct 2025 22:08:07 +0200 Subject: [PATCH 1/7] Pay to blinded key (p2bk) --- DotNut/Encoding/CashuTokenV4Encoder.cs | 18 ++++- DotNut/P2PKProofSecret.cs | 26 +++++++ DotNut/P2PkBuilder.cs | 98 +++++++++++++++++++++++--- DotNut/Proof.cs | 5 +- 4 files changed, 134 insertions(+), 13 deletions(-) diff --git a/DotNut/Encoding/CashuTokenV4Encoder.cs b/DotNut/Encoding/CashuTokenV4Encoder.cs index b86fed5..9e1d4d5 100644 --- a/DotNut/Encoding/CashuTokenV4Encoder.cs +++ b/DotNut/Encoding/CashuTokenV4Encoder.cs @@ -51,6 +51,17 @@ public CBORObject ToCBORObject(CashuToken token) proofItem.Add("w", proof.Witness); } + if (proof.P2PkR is not null) + { + var rs = CBORObject.NewArray(); + foreach (var r in proof.P2PkR) + { + rs.Add(r); + } + + proofItem.Add("pr", rs); + } + proofSetItemArray.Add(proofItem); } @@ -98,7 +109,12 @@ public CashuToken FromCBORObject(CBORObject obj) R = ECPrivKey.Create(cborDLEQ["r"].GetByteString()) } : null, - Id = id + Id = id, + + P2PkR = proof.GetOrDefault("pr", null)?.Values + .Select(pr => pr.AsString()) + .ToArray() + }); }).ToList() } diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index e7fd8af..72adb56 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -27,19 +27,45 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) public virtual P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) { + if (proof.P2PkR is not null) + { + var rs = proof.P2PkR.Select(r=>new PrivKey(r).Key).ToList(); + return GenerateWitness(proof.Secret.GetBytes(), keys); + } return GenerateWitness(proof.Secret.GetBytes(), keys); } + public virtual P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] keys) { return GenerateWitness(message.B_.Key.ToBytes(), keys); } + public virtual P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, ECPrivKey[] p2pkBs) + { + var hash = SHA256.HashData(msg); + return GenerateWitness(ECPrivKey.Create(hash), keys, p2pkBs); + } + 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, ECPrivKey[] p2pkBs) + { + if (p2pkBs.Length != keys.Length) + { + throw new ArgumentException("Every P2Pk Blidning factor must have corresponding privkey!"); + } + + for (var i = 0; i < keys.Length; i++) + { + keys[i] = keys[i].sec.Add(p2pkBs[i].sec).ToPrivateKey(); + } + return GenerateWitness(hash, keys); + } + public virtual P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { var msg = hash.ToBytes(); diff --git a/DotNut/P2PkBuilder.cs b/DotNut/P2PkBuilder.cs index d5e98d2..2c537e9 100644 --- a/DotNut/P2PkBuilder.cs +++ b/DotNut/P2PkBuilder.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; using NBitcoin.Secp256k1; namespace DotNut; @@ -8,44 +9,46 @@ public class P2PkBuilder public DateTimeOffset? Lock { get; set; } public ECPubKey[]? RefundPubkeys { get; set; } public int SignatureThreshold { get; set; } = 1; + public ECPubKey[] Pubkeys { get; set; } + //SIG_INPUTS, SIG_ALL public string? SigFlag { get; set; } - public string? Nonce { get; set; } - + public P2PKProofSecret Build() { var tags = new List(); - if(Pubkeys.Length > 1) + if (Pubkeys.Length > 1) { - tags.Add(new[] {"pubkeys"}.Concat(Pubkeys.Skip(1).Select(p => p.ToHex())).ToArray()); + tags.Add(new[] { "pubkeys" }.Concat(Pubkeys.Skip(1).Select(p => p.ToHex())).ToArray()); } + if (!string.IsNullOrEmpty(SigFlag)) { - tags.Add(new[] {"sigflag", SigFlag}); + tags.Add(new[] { "sigflag", SigFlag }); } if (Lock.HasValue) { - tags.Add(new[] {"locktime", Lock.Value.ToUnixTimeSeconds().ToString()}); + tags.Add(new[] { "locktime", Lock.Value.ToUnixTimeSeconds().ToString() }); if (RefundPubkeys?.Any() is true) { - tags.Add(new[] {"refund"}.Concat(RefundPubkeys.Select(p => p.ToHex())) + tags.Add(new[] { "refund" }.Concat(RefundPubkeys.Select(p => p.ToHex())) .ToArray()); } } if (SignatureThreshold > 1 && Pubkeys.Length >= SignatureThreshold) { - tags.Add(new[] {"n_sigs", SignatureThreshold.ToString()}); + tags.Add(new[] { "n_sigs", SignatureThreshold.ToString() }); } - + return new P2PKProofSecret() { Data = Pubkeys.First().ToHex(), - Nonce = Nonce?? RandomNumberGenerator.GetHexString(32, true), + Nonce = Nonce ?? RandomNumberGenerator.GetHexString(32, true), Tags = tags.ToArray() }; } @@ -63,6 +66,7 @@ public static P2PkBuilder Load(P2PKProofSecret proofSecret) { builder.Pubkeys = [primaryPubkey]; } + var rawUnixTs = proofSecret.Tags?.FirstOrDefault(strings => strings.FirstOrDefault() == "locktime")?.Skip(1) ?.FirstOrDefault(); builder.Lock = rawUnixTs is not null && long.TryParse(rawUnixTs, out var unixTs) @@ -88,8 +92,80 @@ public static P2PkBuilder Load(P2PKProofSecret proofSecret) { builder.SignatureThreshold = nSigsValue; } + builder.Nonce = proofSecret.Nonce; return builder; } + + + /// + /// Overload, every p2pkSecret will contain blinded pubkeys + /// + /// + /// + public P2PKProofSecret Build(out ECPrivKey[] p2pkRs) + { + var rs = new List(); + for(int i = 0; i < (Pubkeys.Length + RefundPubkeys.Length); i++) + { + var r = new PrivKey(RandomNumberGenerator.GetHexString(64)); + rs.Add(r); + } + p2pkRs = rs.ToArray(); + _blindPubkeys(p2pkRs); + return this.Build(); + } + + public static P2PkBuilder Load(P2PKProofSecret proofSecret, ECPrivKey[]? p2pkRs) + { + var builder = Load(proofSecret); + if (p2pkRs == null || p2pkRs.Length == 0) + { + return builder; + } + builder._unblindPubkeys(p2pkRs); + return builder; + } + + private void _blindPubkeys(ECPrivKey[] privkeys) + { + if (Pubkeys.Length + RefundPubkeys?.Length != privkeys.Length) + { + throw new ArgumentException("Invalid P2Pk blinding factors length length"); + } + + for (var i = 0; i < privkeys.Length; i++) + { + if (i >= Pubkeys.Length) + { + Pubkeys[i] = Pubkeys[i].AddTweak(privkeys[i].CreatePubKey().ToBytes()); + continue; + } + + RefundPubkeys[i - Pubkeys.Length] = + RefundPubkeys[i - Pubkeys.Length].AddTweak(privkeys[i].CreatePubKey().ToBytes()); + } + } + private void _unblindPubkeys(ECPrivKey[] privkeys) + { + if (Pubkeys.Length + RefundPubkeys.Length != privkeys.Length) + { + throw new ArgumentException("Invalid "); + } + + for (var i = 0; i < privkeys.Length; i++) + { + if (i >= Pubkeys.Length) + { + Pubkeys[i] = Pubkeys[i].Q.ToGroupElementJacobian() + .Add(privkeys[i].CreatePubKey().Q.Negate()).ToPubkey(); + + continue; + } + + + RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_(RefundPubkeys[i - Pubkeys.Length], privkeys[i]); + } + } } \ No newline at end of file diff --git a/DotNut/Proof.cs b/DotNut/Proof.cs index 9829bb0..5148ffe 100644 --- a/DotNut/Proof.cs +++ b/DotNut/Proof.cs @@ -20,9 +20,12 @@ public class Proof [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Witness { get; set; } - [JsonPropertyName("dleq")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DLEQProof? DLEQ { get; set; } + [JsonPropertyName("p2pk_r")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string[]? P2PkR { get; set; } // must not be exposed to mint. strip when possible + } \ No newline at end of file From 312100413357e1eb964c8108e35862c59eddc065 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Fri, 17 Oct 2025 22:22:44 +0200 Subject: [PATCH 2/7] fix --- DotNut/P2PKProofSecret.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 72adb56..45639f6 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -29,8 +29,8 @@ public virtual P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) { if (proof.P2PkR is not null) { - var rs = proof.P2PkR.Select(r=>new PrivKey(r).Key).ToList(); - return GenerateWitness(proof.Secret.GetBytes(), keys); + var rs = proof.P2PkR.Select(r=>new PrivKey(r).Key).ToArray(); + return GenerateWitness(proof.Secret.GetBytes(), keys, rs); } return GenerateWitness(proof.Secret.GetBytes(), keys); } From 459601678e317ca423b985f95c43c470da6a60c1 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sat, 8 Nov 2025 01:31:14 +0100 Subject: [PATCH 3/7] p2bk again --- DotNut.Tests/UnitTest1.cs | 160 +++++++++++++++++++++++++ DotNut/Cashu.cs | 65 +++++++++- DotNut/Encoding/CashuTokenV4Encoder.cs | 15 +-- DotNut/KeysetId.cs | 8 +- DotNut/NUT13/BIP32.cs | 3 +- DotNut/P2PKProofSecret.cs | 118 +++++++++++++----- DotNut/P2PkBuilder.cs | 85 ++++++------- DotNut/Proof.cs | 4 +- 8 files changed, 358 insertions(+), 100 deletions(-) diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index 5019d9f..ba87502 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -1,3 +1,5 @@ +using System.Runtime.Intrinsics.Arm; +using System.Security.Cryptography; using System.Text.Json; using DotNut.ApiModels; using DotNut.NBitcoin.BIP39; @@ -650,4 +652,162 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response() Assert.Equal(1640995200, response3.Expiry); Assert.Null(response3.PaymentPreimage); } + private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); + + [Fact] + public void Nut26Tests() + { + // sender ephemeral keypair + var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); + var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); + + Assert.Equal(E.Key.ToString()?.ToLowerInvariant(), e.Key.CreatePubKey().ToString().ToLowerInvariant()); + + // receiver keypair + var p = new PrivKey("ad37e8abd800be3e8272b14045873f4353327eedeb702b72ddcc5c5adff5129c"); + var P = new PubKey("02771fed6cb88aaac38b8b32104a942bf4b8f4696bc361171b3c7d06fa2ebddf06"); + + Assert.Equal(P.Key.ToString().ToLowerInvariant(), p.Key.CreatePubKey().ToString().ToLowerInvariant()); + + var kid = new KeysetId("009a1f293253e41e"); + + var zx = "40d6ba4430a6dfa915bb441579b0f4dee032307434e9957a092bbca73151df8b"; + Assert.Equal(zx, Convert.ToHexString(Cashu.ComputeZx(e, P)).ToLowerInvariant()); + Assert.Equal(zx, Convert.ToHexString(Cashu.ComputeZx(p, E)).ToLowerInvariant()); + + string[] rs = + [ + "41b5f15975f787bd5bd8d91753cbbe56d0d7aface851b1063e8011f68551862d", + "c4d68c79b8676841f767bcd53437af3f43d51b205f351d5cdfe5cb866ec41494", + "04ecf53095882f28965f267e46d2c555f15bcd74c3a84f42cf0de8ebfb712c7c", + "4163bc31b3087901b8b28249213b0ecc447cee3ea1f0c04e4dd5934e0c3f78ad", + "f5d6d20c399887f29bdda771660f87226e3a0d4ef36a90f40d3f717085957b60", + "f275404a115cd720ee099f5d6b7d5dc705d1c95ac6ae01c917031b64f7dccc72", + "39dffa9f0160bcda63920305fc12f88d824f5b654970dbd579c08367c12fcd78", + "3331338e87608c7f36265c9b52bb5ebeac1bb3e2220d2682370f4b7c09dccd4b", + "44947bd36c0200fb5d5d05187861364f6b666aac8ce37b368e27f01cea7cf147", + "cf4e69842833e0dab8a7302933d648fee98de80284af2d7ead71b420a8f0ebde", + "3638eae8a9889bbd96769637526010b34cd1e121805eaaaaa0602405529ca92f" + ]; + + for (int i = 0; i <= 10; i++) + { + + var extraByte = Cashu.CheckRiOverflow(Convert.FromHexString(zx), kid.GetBytes(), i); + var ri = (PrivKey)Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i, extraByte); + Assert.Equal(rs[i], ri.ToString()); + } + + + string[] blindedPublicKeys = + [ + "03f221b62aa21ee45982d14505de2b582716ae95c265168f586dc547f0ea8f135f", + "0299692178029fe08c49e8123bb0e84d6e960b27f82c8aed43013526489d46c0d5", + "03ae189850bda004f9723e17372c99ff9df9e29750d2147d40efb45ac8ab2cdd2c", + "03109838d718fbe02e9458ffa423f25bae0388146542534f8e2a094de6f7b697fa", + "0339d5ed7ea93292e60a4211b2daf20dff53f050835614643a43edccc35c8313db", + "0237861efcd52fe959bce07c33b5607aeae0929749b8339f68ba4365f2fb5d2d8d", + "026d5500988a62cde23096047db61e9fb5ef2fea5c521019e23862108ea4e14d72", + "039024fd20b26e73143509537d7c18595cfd101da4b18bb86ddd30e944aac6ef1b", + "03017ec4218ca2ed0fbe050e3f1a91221407bf8c896b803a891c3a52d162867ef8", + "0380dc0d2c79249e47b5afb61b7d40e37b9b0370ec7c80b50c62111021b886ab31", + "0261a8a32e718f5f27610a2b7c2069d6bab05d1ead7da21aa9dd2a3c758bdf6479" + ]; + //it's the same blinding as with computeB_ + for (int i = 0; i <= 10; i++) + { + Assert.Equal(blindedPublicKeys[i], ((PubKey)Cashu.ComputeB_(P, new PrivKey(rs[i]))).ToString()); + } + + string[] skStd = + [ + "eeedda054df845fbde4b8a579952fd9a240a2e9ad3c1dc791c4c6e51654698c9", + "720e75259068268079da6e1579beee83dc58bd279b5ca893fddfc9547e82e5ef", + "b224dddc6d88ed6718d1d7be8c5a0499448e4c62af187ab5acda4546db663f18", + "ee9ba4dd8b0937403b25338966c24e0f97af6d2c8d60ebc12ba1efa8ec348b49", + "a30ebab8119946311e5058b1ab96c66706bdaf562f921c2b2b396f3e95544cbb", + "9fad28f5e95d955f707c509db1049d0b9e556b6202d58d0034fd1933079b9dcd", + "e717e34ad9617b18e604b446419a37d0d581da5334e10748578cdfc2a124e014", + "e0691c3a5f614abdb8990ddb98429e01ff4e32d00d7d51f514dba7d6e9d1dfe7", + "f1cc647f4402bf39dfcfb658bde87592be98e99a7853a6a96bf44c77ca7203e3", + "7c86523000349f193b19e169795d884382118a09c0d6b8b5cb6bb1eeb8afbd39", + "e370d394818959fc18e9477797e74ff6a004600f6bced61d7e2c80603291bbcb" + ]; + + for (int i = 0; i <= 10; i++) + { + var extraByte = Cashu.CheckRiOverflow(Convert.FromHexString(zx), kid.GetBytes(), i); + var ri = Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i, extraByte); + var derivedKey = p.Key.TweakAdd(ri.ToBytes()); + + Assert.Equal(skStd[i], Convert.ToHexString(derivedKey.ToBytes()).ToLowerInvariant()); + } + + string[] skNeg = + [ + "947e08ad9df6c97ed96627d70e447f1238540da5ac2a25cf208614287592b4d2", + "179ea3cde066aa0374f50b94eeb06ffbf0a29c3273c4f1ea02196f2b8ecf01f8", + "57b50c84bd8770ea13ec753e014b861158d82b6d8780c40bb113eb1debb25b21", + "942bd385db07bac3363fd108dbb3cf87abf94c3765c935172fdb957ffc80a752", + "489ee9606197c9b4196af631208847df1b078e6107fa65812f731515a5a068c4", + "453d579e395c18e26b96ee1d25f61e83b29f4a6cdb3dd6563936bf0a17e7b9d6", + "8ca811f3295ffe9be11f51c5b68bb948e9cbb95e0d49509e5bc68599b170fc1d", + "85f94ae2af5fce40b3b3ab5b0d341f7a139811dae5e59b4b19154dadfa1dfbf0", + "975c9327940142bcdaea53d832d9f70ad2e2c8a550bbefff702df24edabe1fec", + "221680d85033229c36347ee8ee4f09bb965b6914993f020bcfa557c5c8fbd942", + "8901023cd187dd7f1403e4f70cd8d16eb44e3f1a44371f738266263742ddd7d4", + ]; + + for (int i = 0; i <= 10; i++) + { + var extraByte = Cashu.CheckRiOverflow(Convert.FromHexString(zx), kid.GetBytes(), i); + var ri = Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i, extraByte); + var derivedKeyNeg = p.Key.sec.Negate().Add(ri.sec).ToPrivateKey(); + + Assert.Equal(skNeg[i], Convert.ToHexString(derivedKeyNeg.ToBytes()).ToLowerInvariant()); + } + + } + + [Fact] + public void Nut26_Flow() + { + var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); + var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); + + var secretKey = + ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")); + + var signing_key_two = + ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); + + var signing_key_three = + ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); + + var keysetId = new KeysetId("009a1f293253e41e"); + + var conditions = new P2PkBuilder() + { + Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), + Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, + RefundPubkeys = new[] {secretKey.CreatePubKey()}, + SignatureThreshold = 2, + SigFlag = "SIG_INPUTS" + }; + var p2pkProofSecret = conditions.Build(keysetId, e); + + var secret = new Nut10Secret(P2PKProofSecret.Key, p2pkProofSecret); + + var proof = new Proof() + { + Id = keysetId, + Amount = 0, + Secret = secret, + C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), + P2PkE = E + }; + var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key_two, signing_key_three}, keysetId, E); + + Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); + } } \ No newline at end of file diff --git a/DotNut/Cashu.cs b/DotNut/Cashu.cs index bb28dcd..7eb4c40 100644 --- a/DotNut/Cashu.cs +++ b/DotNut/Cashu.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Numerics; +using System.Text; using NBitcoin.Secp256k1; using SHA256 = System.Security.Cryptography.SHA256; @@ -8,6 +9,10 @@ public static class Cashu { private static readonly byte[] DOMAIN_SEPARATOR = "Secp256k1_HashToCurve_Cashu_"u8.ToArray(); + private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray(); + + internal static readonly BigInteger N = + BigInteger.Parse("115792089237316195423570985008687907852837564279074904382605163141518161494337"); public static ECPubKey MessageToCurve(string message) { var hash = Encoding.UTF8.GetBytes(message); @@ -126,23 +131,73 @@ public static ECPubKey ComputeC(ECPubKey C_, ECPrivKey r, ECPubKey A) return C_.Q.ToGroupElementJacobian().Add((A.Q * r.sec).ToGroupElement().Negate()).ToPubkey(); } - private static byte[] Concat(params byte[][] arrays) + public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) + { + var x = (e.sec * P.Q).ToGroupElement().x; + if (!ECXOnlyPubKey.TryCreate(x, Context.Instance, out var xOnly)) + { + // should never happen + throw new InvalidOperationException("Could not create xOnly pubkey"); + } + return xOnly.ToBytes(); + } + + public static bool CheckRiOverflow(byte[] Zx, byte[] keysetId, int i) + { + var hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)])); + var hashValue = new BigInteger(hash); + + if (hashValue == 0 || hashValue.CompareTo(N) != -1) + { + return true; + } + + return false; + } + + public static ECPrivKey ComputeRi(byte[] Zx, byte[] keysetId, int i, bool extraByte) { - return arrays.Aggregate((a, b) => a.Concat(b).ToArray()); + // ECPrivkey.Create wont throw exception as long as user will take care about extra byte. + // See: CheckRiOverflow + byte[] hash; + if (!extraByte) + { + hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)])); + return ECPrivKey.Create(hash); + } + hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)], [0xff])); + return ECPrivKey.Create(hash); } + + + private static byte[] Concat(params byte[][] arrays) + { + int totalLength = arrays.Sum(a => a?.Length ?? 0); + var result = new byte[totalLength]; + int offset = 0; + + foreach (var arr in arrays) + { + if (arr == null || arr.Length == 0) continue; + Buffer.BlockCopy(arr, 0, result, offset, arr.Length); + offset += arr.Length; + } + return result; + } + public static string ToHex(this ECPrivKey key) { return Convert.ToHexString(key.ToBytes()).ToLower(); } - + public static byte[] ToBytes(this ECPrivKey key) { Span output = stackalloc byte[32]; key.WriteToSpan(output); return output.ToArray(); } - + public static byte[] ToUncompressedBytes(this ECPubKey key) { Span output = stackalloc byte[65]; diff --git a/DotNut/Encoding/CashuTokenV4Encoder.cs b/DotNut/Encoding/CashuTokenV4Encoder.cs index 9e1d4d5..480cfd4 100644 --- a/DotNut/Encoding/CashuTokenV4Encoder.cs +++ b/DotNut/Encoding/CashuTokenV4Encoder.cs @@ -51,15 +51,9 @@ public CBORObject ToCBORObject(CashuToken token) proofItem.Add("w", proof.Witness); } - if (proof.P2PkR is not null) + if (proof.P2PkE?.Key is not null) { - var rs = CBORObject.NewArray(); - foreach (var r in proof.P2PkR) - { - rs.Add(r); - } - - proofItem.Add("pr", rs); + proofItem.Add("pe", Convert.FromHexString(proof.P2PkE.Key.ToString())); } proofSetItemArray.Add(proofItem); @@ -111,9 +105,8 @@ public CashuToken FromCBORObject(CBORObject obj) : null, Id = id, - P2PkR = proof.GetOrDefault("pr", null)?.Values - .Select(pr => pr.AsString()) - .ToArray() + P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE? + ECPubKey.Create(p2pkE.GetByteString()) : null }); }).ToList() diff --git a/DotNut/KeysetId.cs b/DotNut/KeysetId.cs index 21b45d2..f3f07fe 100644 --- a/DotNut/KeysetId.cs +++ b/DotNut/KeysetId.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Text.Encodings.Web; +using System.Text.Json.Serialization; using DotNut.JsonConverters; namespace DotNut; @@ -74,4 +75,9 @@ public byte GetVersion() string versionStr = _id.Substring(0, 2); return Convert.ToByte(versionStr, 16); } + + public byte[] GetBytes() + { + return Convert.FromHexString(_id); + } } \ No newline at end of file diff --git a/DotNut/NUT13/BIP32.cs b/DotNut/NUT13/BIP32.cs index 850efcf..2b8c4c8 100644 --- a/DotNut/NUT13/BIP32.cs +++ b/DotNut/NUT13/BIP32.cs @@ -12,8 +12,7 @@ public class BIP32 : IHdKeyAlgo public static readonly IHdKeyAlgo Instance = new BIP32(); private static readonly byte[] CurveBytes = "Bitcoin seed"u8.ToArray(); - private static readonly BigInteger N = - BigInteger .Parse("115792089237316195423570985008687907852837564279074904382605163141518161494337"); + private static readonly BigInteger N = Cashu.N; private BIP32() { diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 45639f6..8ecec62 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Text.Json.Serialization; using NBitcoin.Secp256k1; using SHA256 = System.Security.Cryptography.SHA256; @@ -23,15 +24,10 @@ public virtual ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) requiredSignatures = builder.SignatureThreshold; return builder.Pubkeys; } - + public virtual P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) { - if (proof.P2PkR is not null) - { - var rs = proof.P2PkR.Select(r=>new PrivKey(r).Key).ToArray(); - return GenerateWitness(proof.Secret.GetBytes(), keys, rs); - } return GenerateWitness(proof.Secret.GetBytes(), keys); } @@ -39,33 +35,13 @@ public virtual P2PKWitness GenerateWitness(BlindedMessage message, ECPrivKey[] k { return GenerateWitness(message.B_.Key.ToBytes(), keys); } - - public virtual P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys, ECPrivKey[] p2pkBs) - { - var hash = SHA256.HashData(msg); - return GenerateWitness(ECPrivKey.Create(hash), keys, p2pkBs); - } - + 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, ECPrivKey[] p2pkBs) - { - if (p2pkBs.Length != keys.Length) - { - throw new ArgumentException("Every P2Pk Blidning factor must have corresponding privkey!"); - } - - for (var i = 0; i < keys.Length; i++) - { - keys[i] = keys[i].sec.Add(p2pkBs[i].sec).ToPrivateKey(); - } - return GenerateWitness(hash, keys); - } - + public virtual P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { var msg = hash.ToBytes(); @@ -98,6 +74,90 @@ public virtual P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) return result; } + + public P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + { + return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, P2PkE); + } + + public P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + { + return GenerateBlindWitness(message.B_.Key.ToBytes(), keys, keysetId, P2PkE); + } + + public P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + { + var hash = SHA256.HashData(msg); + return GenerateBlindWitness(ECPrivKey.Create(hash), keys, keysetId, P2PkE); + } + + public P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + { + if (Key != "P2PK") + { + throw new InvalidOperationException("Only P2PK is supported"); + } + var msg = hash.ToBytes(); + var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); + var keysRequiredLeft = requiredSignatures; + var availableKeysLeft = keys; + var result = new P2PKWitness(); + + var keysetIdBytes = keysetId.GetBytes(); + var pubkeysTotalCount = Builder.Pubkeys.Length + Builder.RefundPubkeys?.Length; + + HashSet usedSlots = new(); + + while (keysRequiredLeft > 0 && availableKeysLeft.Any()) + { + var key = availableKeysLeft.First(); + for (int i = 0; i < pubkeysTotalCount; i++) + { + if (usedSlots.Contains(i)) + { + continue; + } + + var Zx = Cashu.ComputeZx(key, P2PkE); + var shouldAddBytes = Cashu.CheckRiOverflow(Zx, keysetIdBytes, i); + var ri = Cashu.ComputeRi(Zx, keysetIdBytes, i, shouldAddBytes); + + 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)) + { + usedSlots.Add(i); + var sig = tweakedPrivkey.SignBIP340(msg); + tweakedPrivkey.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); + result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); + availableKeysLeft = availableKeysLeft.Except(new[] {key}).ToArray(); + keysRequiredLeft = requiredSignatures - result.Signatures.Length; + break; + } + + if (allowedKeys.Contains(tweakedPubkeyNeg)) + { + usedSlots.Add(i); + var sig = tweakedPrivkeyNeg.SignBIP340(msg); + tweakedPrivkeyNeg.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); + result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); + availableKeysLeft = availableKeysLeft.Except(new[] {key}).ToArray(); + keysRequiredLeft = requiredSignatures - result.Signatures.Length; + break; + + } + } + } + if (keysRequiredLeft > 0) + throw new InvalidOperationException("Not enough valid keys to sign"); + return result; + } + + public virtual bool VerifyWitness(string message, P2PKWitness witness) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(message)); diff --git a/DotNut/P2PkBuilder.cs b/DotNut/P2PkBuilder.cs index 2c537e9..a10b79f 100644 --- a/DotNut/P2PkBuilder.cs +++ b/DotNut/P2PkBuilder.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; -using System.Security.Cryptography; +using System.Security.Cryptography; using NBitcoin.Secp256k1; namespace DotNut; @@ -98,74 +97,60 @@ public static P2PkBuilder Load(P2PKProofSecret proofSecret) return builder; } - - /// - /// Overload, every p2pkSecret will contain blinded pubkeys - /// - /// - /// - public P2PKProofSecret Build(out ECPrivKey[] p2pkRs) + + //For sig_inputs, generates random p2pk_e for each input + public P2PKProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) { - var rs = new List(); - for(int i = 0; i < (Pubkeys.Length + RefundPubkeys.Length); i++) - { - var r = new PrivKey(RandomNumberGenerator.GetHexString(64)); - rs.Add(r); - } - p2pkRs = rs.ToArray(); - _blindPubkeys(p2pkRs); - return this.Build(); + var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); + p2pkE = e.Key.CreatePubKey(); + return Build(keysetId, e); } - public static P2PkBuilder Load(P2PKProofSecret proofSecret, ECPrivKey[]? p2pkRs) + //For sig_all, p2pk_e must be provided + public P2PKProofSecret Build(KeysetId keysetId, ECPrivKey p2pke) { - var builder = Load(proofSecret); - if (p2pkRs == null || p2pkRs.Length == 0) + var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; + var rs = new List(); + bool extraByte = false; + + var keysetIdBytes = keysetId.GetBytes(); + + var e = p2pke; + + for (int i = 0; i < pubkeys.Length; i++) { - return builder; + var Zx = Cashu.ComputeZx(e, pubkeys[i]); + if (i == 0) + { + extraByte = Cashu.CheckRiOverflow(Zx, keysetIdBytes, i); + } + + var Ri = Cashu.ComputeRi(Zx, keysetIdBytes, i, extraByte); + rs.Add(Ri); } - builder._unblindPubkeys(p2pkRs); - return builder; + _blindPubkeys(rs.ToArray()); + return Build(); } - private void _blindPubkeys(ECPrivKey[] privkeys) + private void _blindPubkeys(ECPrivKey[] rs) { - if (Pubkeys.Length + RefundPubkeys?.Length != privkeys.Length) + if (Pubkeys.Length + RefundPubkeys?.Length != rs.Length) { throw new ArgumentException("Invalid P2Pk blinding factors length length"); } - for (var i = 0; i < privkeys.Length; i++) + for (var i = 0; i < rs.Length; i++) { - if (i >= Pubkeys.Length) + if (i < Pubkeys.Length) { - Pubkeys[i] = Pubkeys[i].AddTweak(privkeys[i].CreatePubKey().ToBytes()); + Pubkeys[i] = Cashu.ComputeB_(Pubkeys[i], rs[i]); continue; } - RefundPubkeys[i - Pubkeys.Length] = - RefundPubkeys[i - Pubkeys.Length].AddTweak(privkeys[i].CreatePubKey().ToBytes()); - } - } - private void _unblindPubkeys(ECPrivKey[] privkeys) - { - if (Pubkeys.Length + RefundPubkeys.Length != privkeys.Length) - { - throw new ArgumentException("Invalid "); - } - - for (var i = 0; i < privkeys.Length; i++) - { - if (i >= Pubkeys.Length) + if (RefundPubkeys != null) { - Pubkeys[i] = Pubkeys[i].Q.ToGroupElementJacobian() - .Add(privkeys[i].CreatePubKey().Q.Negate()).ToPubkey(); - - continue; + RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_(RefundPubkeys[i - Pubkeys.Length], rs[i]); } - - - RefundPubkeys[i - Pubkeys.Length] = Cashu.ComputeB_(RefundPubkeys[i - Pubkeys.Length], privkeys[i]); } } } \ No newline at end of file diff --git a/DotNut/Proof.cs b/DotNut/Proof.cs index 5148ffe..5c9dd78 100644 --- a/DotNut/Proof.cs +++ b/DotNut/Proof.cs @@ -24,8 +24,8 @@ public class Proof [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DLEQProof? DLEQ { get; set; } - [JsonPropertyName("p2pk_r")] + [JsonPropertyName("p2pk_e")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string[]? P2PkR { get; set; } // must not be exposed to mint. strip when possible + public PubKey? P2PkE { get; set; } // must not be exposed to mint } \ No newline at end of file From f252908a79f9f1ea59fda9797a8f8db7d8559c21 Mon Sep 17 00:00:00 2001 From: d4r <50369025+d4rp4t@users.noreply.github.com> Date: Sat, 8 Nov 2025 02:33:17 +0100 Subject: [PATCH 4/7] Update DotNut/P2PkBuilder.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- DotNut/P2PkBuilder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DotNut/P2PkBuilder.cs b/DotNut/P2PkBuilder.cs index a10b79f..e3cf14b 100644 --- a/DotNut/P2PkBuilder.cs +++ b/DotNut/P2PkBuilder.cs @@ -134,9 +134,10 @@ public P2PKProofSecret Build(KeysetId keysetId, ECPrivKey p2pke) private void _blindPubkeys(ECPrivKey[] rs) { - if (Pubkeys.Length + RefundPubkeys?.Length != rs.Length) + var expectedLength = Pubkeys.Length + (RefundPubkeys?.Length ?? 0); + if (expectedLength != rs.Length) { - throw new ArgumentException("Invalid P2Pk blinding factors length length"); + throw new ArgumentException("Invalid P2Pk blinding factors length"); } for (var i = 0; i < rs.Length; i++) From f9cfc01720fa2bd0d3ebac9c2a0865a912a6dc7c Mon Sep 17 00:00:00 2001 From: d4r <50369025+d4rp4t@users.noreply.github.com> Date: Sat, 8 Nov 2025 02:35:13 +0100 Subject: [PATCH 5/7] Update DotNut/P2PKProofSecret.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- DotNut/P2PKProofSecret.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 8ecec62..368bf06 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -104,7 +104,7 @@ public P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, Keyset var result = new P2PKWitness(); var keysetIdBytes = keysetId.GetBytes(); - var pubkeysTotalCount = Builder.Pubkeys.Length + Builder.RefundPubkeys?.Length; + var pubkeysTotalCount = Builder.Pubkeys.Length + (Builder.RefundPubkeys?.Length ?? 0); HashSet usedSlots = new(); From d5bd5f2977abbf4c7d0a73d48168b870550d280a Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sat, 8 Nov 2025 02:47:11 +0100 Subject: [PATCH 6/7] prevent infinite loop --- DotNut/P2PKProofSecret.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index 368bf06..abd90af 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -111,6 +111,8 @@ public P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, Keyset while (keysRequiredLeft > 0 && availableKeysLeft.Any()) { var key = availableKeysLeft.First(); + var remainingKeys = availableKeysLeft.Skip(1).ToArray(); + for (int i = 0; i < pubkeysTotalCount; i++) { if (usedSlots.Contains(i)) @@ -134,7 +136,6 @@ public P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, Keyset var sig = tweakedPrivkey.SignBIP340(msg); tweakedPrivkey.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); - availableKeysLeft = availableKeysLeft.Except(new[] {key}).ToArray(); keysRequiredLeft = requiredSignatures - result.Signatures.Length; break; } @@ -145,12 +146,12 @@ public P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, Keyset var sig = tweakedPrivkeyNeg.SignBIP340(msg); tweakedPrivkeyNeg.CreateXOnlyPubKey().SigVerifyBIP340(sig, msg); result.Signatures = result.Signatures.Append(sig.ToHex()).ToArray(); - availableKeysLeft = availableKeysLeft.Except(new[] {key}).ToArray(); keysRequiredLeft = requiredSignatures - result.Signatures.Length; break; } } + availableKeysLeft = remainingKeys; } if (keysRequiredLeft > 0) throw new InvalidOperationException("Not enough valid keys to sign"); From bc0cc667f3986944f02713be9499bc57644ae816 Mon Sep 17 00:00:00 2001 From: d4rp4t Date: Sat, 8 Nov 2025 16:10:49 +0100 Subject: [PATCH 7/7] fix --- DotNut.Tests/UnitTest1.cs | 68 ++++++++++++++++++++++++++++++--------- DotNut/Cashu.cs | 24 +++----------- DotNut/HTLCBuilder.cs | 10 ++++++ DotNut/HTLCProofSecret.cs | 68 +++++++++++++++++++++++++++++++++++++++ DotNut/P2PKProofSecret.cs | 31 ++++++++++-------- DotNut/P2PkBuilder.cs | 17 +++++----- 6 files changed, 162 insertions(+), 56 deletions(-) diff --git a/DotNut.Tests/UnitTest1.cs b/DotNut.Tests/UnitTest1.cs index ba87502..5cc8335 100644 --- a/DotNut.Tests/UnitTest1.cs +++ b/DotNut.Tests/UnitTest1.cs @@ -692,9 +692,7 @@ public void Nut26Tests() for (int i = 0; i <= 10; i++) { - - var extraByte = Cashu.CheckRiOverflow(Convert.FromHexString(zx), kid.GetBytes(), i); - var ri = (PrivKey)Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i, extraByte); + var ri = (PrivKey)Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i); Assert.Equal(rs[i], ri.ToString()); } @@ -736,8 +734,7 @@ public void Nut26Tests() for (int i = 0; i <= 10; i++) { - var extraByte = Cashu.CheckRiOverflow(Convert.FromHexString(zx), kid.GetBytes(), i); - var ri = Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i, extraByte); + var ri = Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i); var derivedKey = p.Key.TweakAdd(ri.ToBytes()); Assert.Equal(skStd[i], Convert.ToHexString(derivedKey.ToBytes()).ToLowerInvariant()); @@ -760,8 +757,7 @@ public void Nut26Tests() for (int i = 0; i <= 10; i++) { - var extraByte = Cashu.CheckRiOverflow(Convert.FromHexString(zx), kid.GetBytes(), i); - var ri = Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i, extraByte); + var ri = Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i); var derivedKeyNeg = p.Key.sec.Negate().Add(ri.sec).ToPrivateKey(); Assert.Equal(skNeg[i], Convert.ToHexString(derivedKeyNeg.ToBytes()).ToLowerInvariant()); @@ -772,29 +768,69 @@ public void Nut26Tests() [Fact] public void Nut26_Flow() { + // sender generates ephermal keypair var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca"); var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c"); - var secretKey = - ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")); - - var signing_key_two = + + // receiver privkeys, with corresponding pubkeys that will get blinded + var signing_key = ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); + var signing_key_two = + ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); + + var refundPubkey = + ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")).CreatePubKey(); - var signing_key_three = + var keysetId = new KeysetId("009a1f293253e41e"); + + var conditions = new P2PkBuilder() + { + Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), + Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, + RefundPubkeys = new[] {refundPubkey}, + SignatureThreshold = 2, + SigFlag = "SIG_INPUTS" + }; + var p2pkProofSecret = conditions.BuildBlinded(keysetId, e); + + var secret = new Nut10Secret(P2PKProofSecret.Key, p2pkProofSecret); + + var proof = new Proof() + { + Id = keysetId, + Amount = 0, + Secret = secret, + C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), + P2PkE = E + }; + var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, keysetId, E); + + Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); + } + + [Fact] + public void Nut26_Flow_WithRandomE() + { + var signing_key = + ECPrivKey.Create(Convert.FromHexString("0000000000000000000000000000000000000000000000000000000000000001")); + var signing_key_two = ECPrivKey.Create(Convert.FromHexString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")); + + var refundPubkey = + ECPrivKey.Create(Convert.FromHexString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")).CreatePubKey(); var keysetId = new KeysetId("009a1f293253e41e"); var conditions = new P2PkBuilder() { Lock = DateTimeOffset.FromUnixTimeSeconds(21000000000), - Pubkeys = new[] {signing_key_two.CreatePubKey(), signing_key_three.CreatePubKey()}, - RefundPubkeys = new[] {secretKey.CreatePubKey()}, + Pubkeys = new[] {signing_key.CreatePubKey(), signing_key_two.CreatePubKey()}, + RefundPubkeys = new[] {refundPubkey}, SignatureThreshold = 2, SigFlag = "SIG_INPUTS" }; - var p2pkProofSecret = conditions.Build(keysetId, e); + var p2pkProofSecret = conditions.BuildBlinded(keysetId, out var E); var secret = new Nut10Secret(P2PKProofSecret.Key, p2pkProofSecret); @@ -806,7 +842,7 @@ public void Nut26_Flow() C = "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904".ToPubKey(), P2PkE = E }; - var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key_two, signing_key_three}, keysetId, E); + var witness = p2pkProofSecret.GenerateBlindWitness(proof, new[] {signing_key, signing_key_two}, keysetId, E); Assert.True(p2pkProofSecret.VerifyWitness(secret, witness)); } diff --git a/DotNut/Cashu.cs b/DotNut/Cashu.cs index 7eb4c40..3b32947 100644 --- a/DotNut/Cashu.cs +++ b/DotNut/Cashu.cs @@ -142,30 +142,16 @@ public static byte[] ComputeZx(ECPrivKey e, ECPubKey P) return xOnly.ToBytes(); } - public static bool CheckRiOverflow(byte[] Zx, byte[] keysetId, int i) + public static ECPrivKey ComputeRi(byte[] Zx, byte[] keysetId, int i) { - var hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)])); + byte[] hash; + + hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)])); var hashValue = new BigInteger(hash); - if (hashValue == 0 || hashValue.CompareTo(N) != -1) { - return true; - } - - return false; - } - - public static ECPrivKey ComputeRi(byte[] Zx, byte[] keysetId, int i, bool extraByte) - { - // ECPrivkey.Create wont throw exception as long as user will take care about extra byte. - // See: CheckRiOverflow - byte[] hash; - if (!extraByte) - { - hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)])); - return ECPrivKey.Create(hash); + hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)], [0xff])); } - hash = SHA256.HashData(Concat(P2BK_PREFIX, Zx, keysetId, [(byte)(i & 0xFF)], [0xff])); return ECPrivKey.Create(hash); } diff --git a/DotNut/HTLCBuilder.cs b/DotNut/HTLCBuilder.cs index 51c2f08..7e8fe60 100644 --- a/DotNut/HTLCBuilder.cs +++ b/DotNut/HTLCBuilder.cs @@ -45,4 +45,14 @@ public static HTLCBuilder Load(HTLCProofSecret proofSecret) Tags = p2pkProof.Tags }; } + + public new HTLCProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) + { + throw new NotImplementedException(); + } + + public HTLCProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/DotNut/HTLCProofSecret.cs b/DotNut/HTLCProofSecret.cs index b34cacb..5d98d77 100644 --- a/DotNut/HTLCProofSecret.cs +++ b/DotNut/HTLCProofSecret.cs @@ -24,6 +24,8 @@ public override ECPubKey[] GetAllowedPubkeys(out int requiredSignatures) return builder.Pubkeys; } + + public HTLCWitness GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage) { return GenerateWitness(proof.Secret.GetBytes(), keys, Encoding.UTF8.GetBytes(preimage)); @@ -53,6 +55,37 @@ public HTLCWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] prei }; } + + + public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId) + { + throw new NotImplementedException(); + } + public HTLCWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, + ECPubKey P2PkE) + { + throw new NotImplementedException(); + } + + public HTLCWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE) + { + throw new NotImplementedException(); + } + + public HTLCWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, + ECPubKey P2PkE) + { + throw new NotImplementedException(); + } + + public HTLCWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, + ECPubKey P2PkE) + { + throw new NotImplementedException(); + } + + + public bool VerifyPreimage(string preimage) { return Builder.HashLock.ToBytes().SequenceEqual(SHA256.HashData(Encoding.UTF8.GetBytes(preimage))); @@ -74,6 +107,7 @@ public bool VerifyWitness(ISecret secret, HTLCWitness witness) return VerifyWitness(secret.GetBytes(), witness); } + [Obsolete("Use GenerateWitness(Proof proof, ECPrivKey[] keys, string preimage)")] public override P2PKWitness GenerateWitness(Proof proof, ECPrivKey[] keys) @@ -92,6 +126,40 @@ public override P2PKWitness GenerateWitness(byte[] msg, ECPrivKey[] keys) { throw new InvalidOperationException("Use GenerateWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage)"); } + + + [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId)")] + public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId) + { + throw new InvalidOperationException(); + } + + [Obsolete("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + { + throw new InvalidOperationException("Use GenerateBlindWitness(Proof proof, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + } + + [Obsolete("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, KeysetId keysetId, + ECPubKey P2PkE) + { + throw new InvalidOperationException("Use GenerateBlindWitness(BlindedMessage message, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + } + + [Obsolete("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + { + throw new InvalidOperationException("Use GenerateBlindWitness(byte[] msg, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + } + + [Obsolete("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)")] + public override P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, + ECPubKey P2PkE) + { + throw new InvalidOperationException("Use GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, byte[] preimage, KeysetId keysetId, ECPubKey P2PkE)"); + } + public override P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) { diff --git a/DotNut/P2PKProofSecret.cs b/DotNut/P2PKProofSecret.cs index abd90af..412b1b8 100644 --- a/DotNut/P2PKProofSecret.cs +++ b/DotNut/P2PKProofSecret.cs @@ -1,5 +1,4 @@ -using System.Security.Cryptography.X509Certificates; -using System.Text; +using System.Text; using System.Text.Json.Serialization; using NBitcoin.Secp256k1; using SHA256 = System.Security.Cryptography.SHA256; @@ -74,29 +73,36 @@ public virtual P2PKWitness GenerateWitness(ECPrivKey hash, ECPrivKey[] keys) return result; } + /* + * ========================= + * NUT-XX Pay to blinded key + * ========================= + */ - public P2PKWitness GenerateBlindWitness(Proof proof, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + 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) { return GenerateBlindWitness(proof.Secret.GetBytes(), keys, keysetId, P2PkE); - } + } - public 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 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 P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) + public virtual P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, KeysetId keysetId, ECPubKey P2PkE) { - if (Key != "P2PK") - { - throw new InvalidOperationException("Only P2PK is supported"); - } var msg = hash.ToBytes(); var allowedKeys = GetAllowedPubkeys(out var requiredSignatures); var keysRequiredLeft = requiredSignatures; @@ -121,8 +127,7 @@ public P2PKWitness GenerateBlindWitness(ECPrivKey hash, ECPrivKey[] keys, Keyset } var Zx = Cashu.ComputeZx(key, P2PkE); - var shouldAddBytes = Cashu.CheckRiOverflow(Zx, keysetIdBytes, i); - var ri = Cashu.ComputeRi(Zx, keysetIdBytes, i, shouldAddBytes); + var ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); var tweakedPrivkey = key.TweakAdd(ri.ToBytes()); var tweakedPubkey = tweakedPrivkey.CreatePubKey(); diff --git a/DotNut/P2PkBuilder.cs b/DotNut/P2PkBuilder.cs index e3cf14b..4fc02f6 100644 --- a/DotNut/P2PkBuilder.cs +++ b/DotNut/P2PkBuilder.cs @@ -98,16 +98,22 @@ public static P2PkBuilder Load(P2PKProofSecret proofSecret) } + /* + * ========================= + * NUT-XX Pay to blinded key + * ========================= + */ + //For sig_inputs, generates random p2pk_e for each input public P2PKProofSecret BuildBlinded(KeysetId keysetId, out ECPubKey p2pkE) { var e = new PrivKey(RandomNumberGenerator.GetHexString(64)); p2pkE = e.Key.CreatePubKey(); - return Build(keysetId, e); + return BuildBlinded(keysetId, e); } //For sig_all, p2pk_e must be provided - public P2PKProofSecret Build(KeysetId keysetId, ECPrivKey p2pke) + public P2PKProofSecret BuildBlinded(KeysetId keysetId, ECPrivKey p2pke) { var pubkeys = RefundPubkeys != null ? Pubkeys.Concat(RefundPubkeys).ToArray() : Pubkeys; var rs = new List(); @@ -120,12 +126,7 @@ public P2PKProofSecret Build(KeysetId keysetId, ECPrivKey p2pke) for (int i = 0; i < pubkeys.Length; i++) { var Zx = Cashu.ComputeZx(e, pubkeys[i]); - if (i == 0) - { - extraByte = Cashu.CheckRiOverflow(Zx, keysetIdBytes, i); - } - - var Ri = Cashu.ComputeRi(Zx, keysetIdBytes, i, extraByte); + var Ri = Cashu.ComputeRi(Zx, keysetIdBytes, i); rs.Add(Ri); } _blindPubkeys(rs.ToArray());