Skip to content

Commit

Permalink
+ NistP256; optimised serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
kzorin52 committed Feb 3, 2024
1 parent fbd0027 commit 364dd26
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 47 deletions.
2 changes: 1 addition & 1 deletion Algos/Ed25519HdKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public byte[] GetPublic(ReadOnlySpan<byte> 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..]);
}
}
71 changes: 71 additions & 0 deletions Algos/NistP256HdKey.cs
Original file line number Diff line number Diff line change
@@ -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<byte> CurveBytes = new("Nist256p1 seed"u8.ToArray());

private static readonly UInt256 N =
UInt256.Parse("115792089210356248762697446949407573529996955224135760342422259061068512044369");

public HdKey GetMasterKeyFromSeed(ReadOnlySpan<byte> 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<byte> 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 |
*/
}
6 changes: 3 additions & 3 deletions Algos/Secp256K1HdKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ public HdKey GetMasterKeyFromSeed(ReadOnlySpan<byte> 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)
{
Expand Down
5 changes: 3 additions & 2 deletions HdKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> key, ReadOnlySpan<byte> cc)
public readonly ref struct HdKey(ReadOnlySpan<byte> privateKey, ReadOnlySpan<byte> cc)
{
public readonly ReadOnlySpan<byte> Key = key;
public readonly ReadOnlySpan<byte> PrivateKey = privateKey;
public readonly ReadOnlySpan<byte> ChainCode = cc;
}
3 changes: 2 additions & 1 deletion Interfaces/IHdKeyAlgo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public HdKey DeriveFromMasterKey(KeyPath path, HdKey masterKey)
public HdKey DeriveFromMasterKey(ReadOnlySpan<KeyPathElement> 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;
}
Expand Down
28 changes: 15 additions & 13 deletions KeyPath.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace NBip32Fast;
Expand Down Expand Up @@ -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<byte>(SerializeUInt32(Number));
Serialized = SerializeUInt32(Number);
}

#region Equality
Expand Down Expand Up @@ -151,15 +154,15 @@ public override int GetHashCode()
#endregion

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] SerializeUInt32(in uint index)
public static ReadOnlyMemory<byte> SerializeUInt32(in uint index)
{
return
[
(byte)((index >> 24) & 0xFF),
(byte)((index >> 16) & 0xFF),
(byte)((index >> 8) & 0xFF),
(byte)((index >> 0) & 0xFF)
];
Memory<byte> ser = new byte[4];
MemoryMarshal.Write(ser.Span, in index);

if (BitConverter.IsLittleEndian)
ser.Span.Reverse(); // change endianness

return ser;
}
}

Expand All @@ -173,8 +176,7 @@ private static ReadOnlyMemory<ReadOnlyMemory<byte>> FillCache(bool hard)
var result = new ReadOnlyMemory<byte>[100];
for (var i = 0u; i < 100u; i++)
{
result[i] = new ReadOnlyMemory<byte>(
KeyPathElement.SerializeUInt32(hard ? i + KeyPathElement.HardenedOffset : i));
result[i] = KeyPathElement.SerializeUInt32(hard ? i + KeyPathElement.HardenedOffset : i);
}

return new ReadOnlyMemory<ReadOnlyMemory<byte>>(result);
Expand Down
66 changes: 58 additions & 8 deletions NBip32Fast.Benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//Console.WriteLine(Convert.ToHexStringLower(test.NBip39FastKey()));
//Console.WriteLine(Convert.ToHexStringLower(test.NetezosKey()));

BenchmarkRunner.Run<Secp256K1Tests>();
//BenchmarkRunner.Run<Secp256K1Tests>();

//var test2 = new Ed25519Tests();
//Console.WriteLine(Convert.ToHexStringLower(test2.P3HdKey()));
Expand All @@ -18,6 +18,11 @@
//BenchmarkRunner.Run<Ed25519Tests>();
//BenchmarkRunner.Run<SerCacheTest>();

//var test3 = new Secp256R1Tests();
//Console.WriteLine(Convert.ToHexStringLower(test3.NBip39FastKey()));
//Console.WriteLine(Convert.ToHexStringLower(test3.NetezosKey()));
BenchmarkRunner.Run<Secp256R1Tests>();

public class Secp256K1Tests
{
private const string KeyPath = "m/44'/888'/0'/0/0";
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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<byte> _seedSpan;
private readonly byte[] _seedBytes;

public Secp256R1Tests()
{
_seedBytes = RandomNumberGenerator.GetBytes(64);
_seedSpan = new ReadOnlyMemory<byte>(_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'";
Expand All @@ -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]
Expand Down
21 changes: 17 additions & 4 deletions NBip32Fast.Tests/DerivationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion NBip32Fast.Tests/KeyPathElementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)}]");
}
}
9 changes: 5 additions & 4 deletions NBip32Fast.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
<LangVersion>preview</LangVersion>
<Authors>Temnij</Authors>
<Description>High perfomance BIP-32 HD key derivation library for .NET 8.
Supports SecP256k1 and Ed25519</Description>
Supports SecP256k1, Ed25519 and NistP256 (SecP256r1)</Description>
<PackageProjectUrl>https://github.com/kzorin52/NBip32Fast</PackageProjectUrl>
<RepositoryUrl>https://github.com/kzorin52/NBip32Fast</RepositoryUrl>
<PackageTags>bip32, bip39, ed25519, secp256k1, hdkey, bip44, bitcoin, crypto</PackageTags>
<PackageTags>bip32, bip39, ed25519, secp256k1, secp256r1, nistp256, Nist256p1, p256, hdkey, bip44, bitcoin, crypto, ethereum, neo</PackageTags>
</PropertyGroup>

<PropertyGroup>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Version>1.0.3</Version>
<Version>1.0.4</Version>
</PropertyGroup>

<ItemGroup>
Expand All @@ -44,7 +44,8 @@ Supports SecP256k1 and Ed25519</Description>

<ItemGroup>
<PackageReference Include="Nethermind.Crypto.SecP256k1" Version="1.1.1" />
<PackageReference Include="Nethermind.Numerics.Int256" Version="1.1.1" />
<PackageReference Include="Nethermind.Numerics.Int256" Version="1.2.0" />
<PackageReference Include="NistP256Net" Version="1.0.0" />
<PackageReference Include="NSec.Cryptography" Version="23.9.0-preview.3" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 364dd26

Please sign in to comment.