Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions DotNut.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Runtime.Intrinsics.Arm;
using System.Security.Cryptography;
using System.Text.Json;
using DotNut.ApiModels;
using DotNut.NBitcoin.BIP39;
Expand Down Expand Up @@ -650,4 +652,198 @@ public void NullExpiryTests_PostMeltQuoteBolt11Response()
Assert.Equal(1640995200, response3.Expiry);
Assert.Null(response3.PaymentPreimage);
}
private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove duplicate P2BK_PREFIX constant.

This duplicates the P2BK_PREFIX defined in DotNut/Cashu.cs (line 12). Tests should reference the production constant to ensure they stay in sync.

Remove this field and update tests to use Cashu.P2BK_PREFIX instead:

-    private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray();
-
     [Fact]
     public void Nut26Tests()

Then update test code that references P2BK_PREFIX to use the fully qualified name or add a using static directive.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static readonly byte[] P2BK_PREFIX = "Cashu_P2BK_v1"u8.ToArray();
[Fact]
public void Nut26Tests()
🤖 Prompt for AI Agents
In DotNut.Tests/UnitTest1.cs around line 655, remove the duplicated private
static readonly byte[] P2BK_PREFIX field and update all test references to use
the production constant instead (Cashu.P2BK_PREFIX); either replace usages with
the fully qualified Cashu.P2BK_PREFIX or add a using static directive for
DotNut.Cashu at the top of the test file so tests reference the single source of
truth.


[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 ri = (PrivKey)Cashu.ComputeRi(Convert.FromHexString(zx), kid.GetBytes(), i);
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 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());
}

string[] skNeg =
[
"947e08ad9df6c97ed96627d70e447f1238540da5ac2a25cf208614287592b4d2",
"179ea3cde066aa0374f50b94eeb06ffbf0a29c3273c4f1ea02196f2b8ecf01f8",
"57b50c84bd8770ea13ec753e014b861158d82b6d8780c40bb113eb1debb25b21",
"942bd385db07bac3363fd108dbb3cf87abf94c3765c935172fdb957ffc80a752",
"489ee9606197c9b4196af631208847df1b078e6107fa65812f731515a5a068c4",
"453d579e395c18e26b96ee1d25f61e83b29f4a6cdb3dd6563936bf0a17e7b9d6",
"8ca811f3295ffe9be11f51c5b68bb948e9cbb95e0d49509e5bc68599b170fc1d",
"85f94ae2af5fce40b3b3ab5b0d341f7a139811dae5e59b4b19154dadfa1dfbf0",
"975c9327940142bcdaea53d832d9f70ad2e2c8a550bbefff702df24edabe1fec",
"221680d85033229c36347ee8ee4f09bb965b6914993f020bcfa557c5c8fbd942",
"8901023cd187dd7f1403e4f70cd8d16eb44e3f1a44371f738266263742ddd7d4",
];

for (int i = 0; i <= 10; i++)
{
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());
}

}

[Fact]
public void Nut26_Flow()
{
// sender generates ephermal keypair
var e = new PrivKey("1cedb9df0c6872188b560ace9e35fd55c2532d53e19ae65b46159073886482ca");
var E = new PubKey("02a8cda4cf448bfce9a9e46e588c06ea1780fcb94e3bbdf3277f42995d403a8b0c");


// 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 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.CreatePubKey(), signing_key_two.CreatePubKey()},
RefundPubkeys = new[] {refundPubkey},
SignatureThreshold = 2,
SigFlag = "SIG_INPUTS"
};
var p2pkProofSecret = conditions.BuildBlinded(keysetId, out var 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));
}
}
51 changes: 46 additions & 5 deletions DotNut/Cashu.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Numerics;
using System.Text;
using NBitcoin.Secp256k1;
using SHA256 = System.Security.Cryptography.SHA256;

Expand All @@ -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);
Expand Down Expand Up @@ -126,23 +131,59 @@ 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)
{
return arrays.Aggregate((a, b) => a.Concat(b).ToArray());
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 ECPrivKey ComputeRi(byte[] Zx, byte[] keysetId, int i)
{
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)
{
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<byte> output = stackalloc byte[32];
key.WriteToSpan(output);
return output.ToArray();
}

public static byte[] ToUncompressedBytes(this ECPubKey key)
{
Span<byte> output = stackalloc byte[65];
Expand Down
11 changes: 10 additions & 1 deletion DotNut/Encoding/CashuTokenV4Encoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public CBORObject ToCBORObject(CashuToken token)
proofItem.Add("w", proof.Witness);
}

if (proof.P2PkE?.Key is not null)
{
proofItem.Add("pe", Convert.FromHexString(proof.P2PkE.Key.ToString()));
}

proofSetItemArray.Add(proofItem);
}

Expand Down Expand Up @@ -98,7 +103,11 @@ public CashuToken FromCBORObject(CBORObject obj)
R = ECPrivKey.Create(cborDLEQ["r"].GetByteString())
}
: null,
Id = id
Id = id,

P2PkE = proof.GetOrDefault("pe", null) is { } p2pkE?
ECPubKey.Create(p2pkE.GetByteString()) : null

});
}).ToList()
}
Expand Down
10 changes: 10 additions & 0 deletions DotNut/HTLCBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Loading