From 364dd268d7390aca55ae453b78b0758e344e26e4 Mon Sep 17 00:00:00 2001 From: Temnij Date: Sat, 3 Feb 2024 15:50:22 +0600 Subject: [PATCH] + NistP256; optimised serialization --- Algos/Ed25519HdKey.cs | 2 +- Algos/NistP256HdKey.cs | 71 ++++++++++++++++++++++++++ Algos/Secp256K1HdKey.cs | 6 +-- HdKey.cs | 5 +- Interfaces/IHdKeyAlgo.cs | 3 +- KeyPath.cs | 28 +++++----- NBip32Fast.Benchmark/Program.cs | 66 +++++++++++++++++++++--- NBip32Fast.Tests/DerivationTest.cs | 21 ++++++-- NBip32Fast.Tests/KeyPathElementTest.cs | 3 +- NBip32Fast.csproj | 9 ++-- README.md | 27 ++++++---- 11 files changed, 194 insertions(+), 47 deletions(-) create mode 100644 Algos/NistP256HdKey.cs diff --git a/Algos/Ed25519HdKey.cs b/Algos/Ed25519HdKey.cs index fb83153..ec73c26 100644 --- a/Algos/Ed25519HdKey.cs +++ b/Algos/Ed25519HdKey.cs @@ -29,7 +29,7 @@ public byte[] GetPublic(ReadOnlySpan privateKey) public HdKey Derive(HdKey parent, KeyPathElement index) { - var i = IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x0, parent.Key).AsSpan(); + var i = IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x0, parent.PrivateKey).AsSpan(); return new HdKey(i[..32], i[32..]); } } \ No newline at end of file diff --git a/Algos/NistP256HdKey.cs b/Algos/NistP256HdKey.cs new file mode 100644 index 0000000..2b491d7 --- /dev/null +++ b/Algos/NistP256HdKey.cs @@ -0,0 +1,71 @@ +using System.Security.Cryptography; +using NBip32Fast.Interfaces; +using Nethermind.Int256; +using NistP256Net; + +namespace NBip32Fast.Algos; + +public class NistP256HdKey : IHdKeyAlgo +{ + private static readonly ReadOnlyMemory CurveBytes = new("Nist256p1 seed"u8.ToArray()); + + private static readonly UInt256 N = + UInt256.Parse("115792089210356248762697446949407573529996955224135760342422259061068512044369"); + + public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) + { + var seedCopy = seed.ToArray().AsSpan(); + + while (true) + { + HMACSHA512.HashData(CurveBytes.Span, seedCopy, seedCopy); + + var key = seedCopy[..32]; + var keyInt = new UInt256(key, true); + if (keyInt > N || keyInt.IsZero) continue; + + return new HdKey(key, seedCopy[32..]); + } + } + + public HdKey Derive(HdKey parent, KeyPathElement index) + { + var hash = index.Hardened + ? IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x00, parent.PrivateKey).AsSpan() + : IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, GetPublic(parent.PrivateKey)).AsSpan(); + + var key = hash[..32]; + var cc = hash[32..]; + + var parentKey = new UInt256(parent.PrivateKey, true); + + while (true) + { + var keyInt = new UInt256(key, true); + UInt256.AddMod(keyInt, parentKey, N, out var res); + + if (keyInt > N || res.IsZero) + { + hash = IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x01, cc).AsSpan(); + key = hash[..32]; + cc = hash[32..]; + continue; + } + + var keyBytes = res.ToBigEndian(); + return new HdKey(keyBytes, cc); + } + } + + public byte[] GetPublic(ReadOnlySpan privateKey) + { + return NistP256.GetPublicKey(privateKey); + } + + /* + | Method | Mean | Error | StdDev | + |---------------- |-----------:|---------:|---------:| + | BouncyCastlePub | 1,149.3 us | 12.76 us | 11.31 us | + | NativePub | 143.1 us | 0.97 us | 0.90 us | + */ +} \ No newline at end of file diff --git a/Algos/Secp256K1HdKey.cs b/Algos/Secp256K1HdKey.cs index 04c53a8..95e478f 100644 --- a/Algos/Secp256K1HdKey.cs +++ b/Algos/Secp256K1HdKey.cs @@ -29,13 +29,13 @@ public HdKey GetMasterKeyFromSeed(ReadOnlySpan seed) public HdKey Derive(HdKey parent, KeyPathElement index) { var hash = index.Hardened - ? IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x00, parent.Key).AsSpan() - : IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, GetPublic(parent.Key)).AsSpan(); + ? IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, 0x00, parent.PrivateKey).AsSpan() + : IHdKeyAlgo.Bip32Hash(parent.ChainCode, index, GetPublic(parent.PrivateKey)).AsSpan(); var key = hash[..32]; var cc = hash[32..]; - var parentKey = new UInt256(parent.Key, true); + var parentKey = new UInt256(parent.PrivateKey, true); while (true) { diff --git a/HdKey.cs b/HdKey.cs index f93095c..9dbd3f9 100644 --- a/HdKey.cs +++ b/HdKey.cs @@ -7,9 +7,10 @@ public static class Derivation { public static readonly IHdKeyAlgo Secp256K1 = new Secp256K1HdKey(); public static readonly IHdKeyAlgo Ed25519 = new Ed25519HdKey(); + public static readonly IHdKeyAlgo NistP256 = new NistP256HdKey(); } -public readonly ref struct HdKey(ReadOnlySpan key, ReadOnlySpan cc) +public readonly ref struct HdKey(ReadOnlySpan privateKey, ReadOnlySpan cc) { - public readonly ReadOnlySpan Key = key; + public readonly ReadOnlySpan PrivateKey = privateKey; public readonly ReadOnlySpan ChainCode = cc; } \ No newline at end of file diff --git a/Interfaces/IHdKeyAlgo.cs b/Interfaces/IHdKeyAlgo.cs index f65bb0e..25697fa 100644 --- a/Interfaces/IHdKeyAlgo.cs +++ b/Interfaces/IHdKeyAlgo.cs @@ -27,7 +27,8 @@ public HdKey DeriveFromMasterKey(KeyPath path, HdKey masterKey) public HdKey DeriveFromMasterKey(ReadOnlySpan path, HdKey masterKey) { var result = masterKey; - for (var i = 0; i < path.Length; i++) result = Derive(result, path[i]); + foreach (var t in path) + result = Derive(result, t); return result; } diff --git a/KeyPath.cs b/KeyPath.cs index 6609f10..398f044 100644 --- a/KeyPath.cs +++ b/KeyPath.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace NBip32Fast; @@ -112,13 +113,15 @@ public KeyPathElement(uint number, bool hardened) Number = hardened ? number + HardenedOffset : number; Hardened = hardened; - if (number < 100) + if (number < 100ul) { - Serialized = hardened ? SerCache.HardCache.Span[(int)number] : SerCache.SoftCache.Span[(int)number]; + Serialized = hardened + ? SerCache.HardCache.Span[(int)number] + : SerCache.SoftCache.Span[(int)number]; return; } - Serialized = new ReadOnlyMemory(SerializeUInt32(Number)); + Serialized = SerializeUInt32(Number); } #region Equality @@ -151,15 +154,15 @@ public override int GetHashCode() #endregion [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte[] SerializeUInt32(in uint index) + public static ReadOnlyMemory SerializeUInt32(in uint index) { - return - [ - (byte)((index >> 24) & 0xFF), - (byte)((index >> 16) & 0xFF), - (byte)((index >> 8) & 0xFF), - (byte)((index >> 0) & 0xFF) - ]; + Memory ser = new byte[4]; + MemoryMarshal.Write(ser.Span, in index); + + if (BitConverter.IsLittleEndian) + ser.Span.Reverse(); // change endianness + + return ser; } } @@ -173,8 +176,7 @@ private static ReadOnlyMemory> FillCache(bool hard) var result = new ReadOnlyMemory[100]; for (var i = 0u; i < 100u; i++) { - result[i] = new ReadOnlyMemory( - KeyPathElement.SerializeUInt32(hard ? i + KeyPathElement.HardenedOffset : i)); + result[i] = KeyPathElement.SerializeUInt32(hard ? i + KeyPathElement.HardenedOffset : i); } return new ReadOnlyMemory>(result); diff --git a/NBip32Fast.Benchmark/Program.cs b/NBip32Fast.Benchmark/Program.cs index 05112b8..6f0ebfc 100644 --- a/NBip32Fast.Benchmark/Program.cs +++ b/NBip32Fast.Benchmark/Program.cs @@ -8,7 +8,7 @@ //Console.WriteLine(Convert.ToHexStringLower(test.NBip39FastKey())); //Console.WriteLine(Convert.ToHexStringLower(test.NetezosKey())); -BenchmarkRunner.Run(); +//BenchmarkRunner.Run(); //var test2 = new Ed25519Tests(); //Console.WriteLine(Convert.ToHexStringLower(test2.P3HdKey())); @@ -18,6 +18,11 @@ //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); +//var test3 = new Secp256R1Tests(); +//Console.WriteLine(Convert.ToHexStringLower(test3.NBip39FastKey())); +//Console.WriteLine(Convert.ToHexStringLower(test3.NetezosKey())); +BenchmarkRunner.Run(); + public class Secp256K1Tests { private const string KeyPath = "m/44'/888'/0'/0/0"; @@ -39,7 +44,7 @@ public byte[] NBitcoinKey() [Benchmark] public byte[] NBip39FastKey() { - return NBip32Fast.Derivation.Secp256K1.DerivePath(KeyPath, _seedSpan.Span).Key.ToArray(); + return NBip32Fast.Derivation.Secp256K1.DerivePath(KeyPath, _seedSpan.Span).PrivateKey.ToArray(); } [Benchmark] @@ -58,11 +63,11 @@ .NET SDK 9.0.100-alpha.1.24060.1 DefaultJob : .NET 9.0.0 (9.0.24.5902), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI - | Method | Mean | Error | StdDev | - |-------------- |----------:|----------:|----------:| - | NBitcoinKey | 695.37 us | 6.180 us | 5.781 us | - | NBip39FastKey | 57.17 us | 0.541 us | 0.423 us | - | NetezosKey | 999.42 us | 19.827 us | 22.038 us | + | Method | Mean | Error | StdDev | + |-------------- |------------:|----------:|----------:| + | NBitcoinKey | 704.28 us | 13.124 us | 14.042 us | + | NBip39FastKey | 58.73 us | 1.067 us | 0.945 us | + | NetezosKey | 1,057.24 us | 21.017 us | 29.462 us | // * Hints * Outliers @@ -71,6 +76,51 @@ .NET SDK 9.0.100-alpha.1.24060.1 */ } +public class Secp256R1Tests +{ + private const string KeyPath = "m/44'/888'/0'/0/0"; + private readonly ReadOnlyMemory _seedSpan; + private readonly byte[] _seedBytes; + + public Secp256R1Tests() + { + _seedBytes = RandomNumberGenerator.GetBytes(64); + _seedSpan = new ReadOnlyMemory(_seedBytes); + } + + [Benchmark] + public byte[] NBip39FastKey() + { + return NBip32Fast.Derivation.NistP256.DerivePath(KeyPath, _seedSpan.Span).PrivateKey.ToArray(); + } + + [Benchmark] + public byte[] NetezosKey() + { + return Netezos.Keys.HDKey.FromSeed(_seedBytes, Netezos.Keys.ECKind.NistP256).Derive(KeyPath).Key.GetBytes(); + } + + /* + // * Summary * + + BenchmarkDotNet v0.13.12, Windows 11 (10.0.26020.1000) + 11th Gen Intel Core i7-11700K 3.60GHz, 1 CPU, 16 logical and 8 physical cores + .NET SDK 9.0.100-alpha.1.24060.1 + [Host] : .NET 9.0.0 (9.0.24.5902), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI + DefaultJob : .NET 9.0.0 (9.0.24.5902), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI + + + | Method | Mean | Error | StdDev | + |-------------- |-----------:|---------:|---------:| + | NBip39FastKey | 239.8 us | 1.09 us | 0.96 us | + | NetezosKey | 2,183.8 us | 29.81 us | 27.88 us | + + // * Hints * + Outliers + Secp256R1Tests.NBip39FastKey: Default -> 1 outlier was removed (242.95 us) + */ +} + public class Ed25519Tests { private const string KeyPath = "m/44'/888'/0'/0'/0'"; @@ -92,7 +142,7 @@ public byte[] P3HdKey() [Benchmark] public byte[] NBip32FastKey() { - return NBip32Fast.Derivation.Ed25519.DerivePath(KeyPath, _seedSpan.Span).Key.ToArray(); + return NBip32Fast.Derivation.Ed25519.DerivePath(KeyPath, _seedSpan.Span).PrivateKey.ToArray(); } [Benchmark] diff --git a/NBip32Fast.Tests/DerivationTest.cs b/NBip32Fast.Tests/DerivationTest.cs index 5964d07..d241780 100644 --- a/NBip32Fast.Tests/DerivationTest.cs +++ b/NBip32Fast.Tests/DerivationTest.cs @@ -16,21 +16,34 @@ public class DerivationTest "6144c1daf8222d6dab77e7a20c2f338519b83bd1423602c56c7dfb5e9ea99c02", "55b36970e7ab8434f9b04f1c2e52da7422d2bce7e284ca353419dddfa2e34bdb"); + private static readonly TestCase Case1NistP256 = new( + "m/0'/0/0", + "159dd0bcc8c6982fcbcf53d77a914d32cdae5ae170b5d6d14557f7c141d507f5", + "a96fed72636cb6a63ceb2a2e3a8a8ca8cff872ef4f102ebfa5f28bcc8f823ff5"); + [TestMethod] - public void TestBip39Ed25519() + public void TestBip32Ed25519() { var der1 = Derivation.Ed25519.DerivePath(Case1Ed25519.Path, TestCase.Seed.Span); - Assert.IsTrue(der1.Key.SequenceEqual(Case1Ed25519.Key.Span)); + Assert.IsTrue(der1.PrivateKey.SequenceEqual(Case1Ed25519.Key.Span)); Assert.IsTrue(der1.ChainCode.SequenceEqual(Case1Ed25519.ChainCode.Span)); } [TestMethod] - public void TestBip39Secp256K1() + public void TestBip32Secp256K1() { var der1 = Derivation.Secp256K1.DerivePath(Case1SecP256K1.Path, TestCase.Seed.Span); - Assert.IsTrue(der1.Key.SequenceEqual(Case1SecP256K1.Key.Span)); + Assert.IsTrue(der1.PrivateKey.SequenceEqual(Case1SecP256K1.Key.Span)); Assert.IsTrue(der1.ChainCode.SequenceEqual(Case1SecP256K1.ChainCode.Span)); } + + [TestMethod] + public void TestBip32NistP256() + { + var der1 = Derivation.NistP256.DerivePath(Case1NistP256.Path, TestCase.Seed.Span); + Assert.IsTrue(der1.PrivateKey.SequenceEqual(Case1NistP256.Key.Span)); + Assert.IsTrue(der1.ChainCode.SequenceEqual(Case1NistP256.ChainCode.Span)); + } } internal readonly struct TestCase(in string path, in string keyHex, in string ccHex) diff --git a/NBip32Fast.Tests/KeyPathElementTest.cs b/NBip32Fast.Tests/KeyPathElementTest.cs index c39aac4..3f394a9 100644 --- a/NBip32Fast.Tests/KeyPathElementTest.cs +++ b/NBip32Fast.Tests/KeyPathElementTest.cs @@ -12,6 +12,7 @@ public class KeyPathElementTest [TestMethod] public void TestKeyElement() { - Assert.IsTrue(TestData1.Serialized.Span.SequenceEqual(TestData1Serialized.Span)); + Assert.IsTrue(TestData1.Serialized.Span.SequenceEqual(TestData1Serialized.Span), + $"{TestData1.Number} [h={TestData1.Hardened}] [ser={Convert.ToHexStringLower(TestData1.Serialized.Span)}] != {Convert.ToHexStringLower(TestData1Serialized.Span)}]"); } } \ No newline at end of file diff --git a/NBip32Fast.csproj b/NBip32Fast.csproj index 6f59176..35eaa91 100644 --- a/NBip32Fast.csproj +++ b/NBip32Fast.csproj @@ -8,16 +8,16 @@ preview Temnij High perfomance BIP-32 HD key derivation library for .NET 8. -Supports SecP256k1 and Ed25519 +Supports SecP256k1, Ed25519 and NistP256 (SecP256r1) https://github.com/kzorin52/NBip32Fast https://github.com/kzorin52/NBip32Fast - bip32, bip39, ed25519, secp256k1, hdkey, bip44, bitcoin, crypto + bip32, bip39, ed25519, secp256k1, secp256r1, nistp256, Nist256p1, p256, hdkey, bip44, bitcoin, crypto, ethereum, neo LICENSE.txt README.md - 1.0.3 + 1.0.4 @@ -44,7 +44,8 @@ Supports SecP256k1 and Ed25519 - + + diff --git a/README.md b/README.md index bc93bb6..d2ec039 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ```cs var secp256k1Key = NBip32Fast.Derivation.Secp256K1.DerivePath("m/44'/0'/0'/0/0", seed).Key; var ed25519Key = NBip32Fast.Derivation.Ed25519.DerivePath("m/44'/0'/0'/0'/0'", seed).Key; +var nistP256Key = NBip32Fast.Derivation.NistP256.DerivePath("m/44'/0'/0'/0'/0'", seed).Key; ``` ### Optimised @@ -23,17 +24,23 @@ for (var i = 0u; i < 5u; i++) ## Benchmarks ### SecP256K1 -| Method | Mean | Error | StdDev | -|-------------- |----------:|----------:|----------:| -| NBitcoinKey | 695.37 us | 6.180 us | 5.781 us | -|**NBip39FastKey**| 57.17 us | 0.541 us | 0.423 us | -| NetezosKey | 999.42 us | 19.827 us | 22.038 us | +| Method | Mean | Error | StdDev | +|:--------------|----------:|---------:|---------:| +| NBitcoinKey | 681.74 us | 5.098 us | 4.519 us | +| **NBip39FastKey** | **56.36 us** | 0.409 us | 0.382 us | +| NetezosKey | 957.96 us | 5.120 us | 3.998 us | ### Ed25519 -| Method | Mean | Error | StdDev | -|------------------------ |---------:|----------:|----------:| -| P3HdKey | 9.932 us | 0.1545 us | 0.1290 us | -| **NBip32FastKey** | 7.126 us | 0.0319 us | 0.0266 us | -| NetezosKey | 9.242 us | 0.0867 us | 0.0677 us | +| Method | Mean | Error | StdDev | +|:--------------|---------:|----------:|----------:| +| P3HdKey | 9.413 us | 0.0886 us | 0.0829 us | +| **NBip32FastKey** | **6.944 us** | 0.0498 us | 0.0442 us | +| NetezosKey | 8.934 us | 0.1022 us | 0.0956 us | + +### NistP256 +| Method | Mean | Error | StdDev | +|:--------------|-----------:|---------:|---------:| +| **NBip39FastKey** | **239.8 us** | 1.09 us | 0.96 us | +| NetezosKey | 2,183.8 us | 29.81 us | 27.88 us | [Benchmark code](https://github.com/kzorin52/NBip32Fast/blob/master/NBip32Fast.Benchmark/Program.cs)