diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..9c97586 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Code owners are automatically requested for review on every pull request. +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +* @Marchhill diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c963c6e..2710934 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -124,7 +124,11 @@ jobs: needs: [build-linux, build-macos, build-windows] env: BUILD_CONFIG: release - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - name: Check out blst-bindings repository uses: actions/checkout@v5 @@ -136,6 +140,7 @@ jobs: - name: Move artifacts working-directory: src/Nethermind.Crypto.Bls/runtimes + shell: bash run: | mv -f linux-arm64/libblst.so linux-arm64/native/libblst.so mv -f linux-x64/libblst.so linux-x64/native/libblst.so @@ -151,20 +156,18 @@ jobs: run: dotnet restore - name: Build - working-directory: src/Nethermind.Crypto.Bls - run: > - dotnet build -c ${{ env.BUILD_CONFIG }} --no-restore - ${{ inputs.preview && format('-p:VersionSuffix=preview.{0}', github.run_number) || '' }} + working-directory: src + run: dotnet build -c ${{ env.BUILD_CONFIG }} --no-restore ${{ inputs.preview && format('-p:VersionSuffix=preview.{0}', github.run_number) || '' }} - name: Test working-directory: src/Nethermind.Crypto.Test run: dotnet test -c ${{ env.BUILD_CONFIG }} --no-restore - name: Publish - if: ${{ inputs.publish }} + if: ${{ inputs.publish && matrix.os == 'ubuntu-latest' }} working-directory: src/Nethermind.Crypto.Bls run: | - dotnet pack -c ${{ env.BUILD_CONFIG }} --no-build + dotnet pack -c ${{ env.BUILD_CONFIG }} --no-build ${{ inputs.preview && format('-p:VersionSuffix=preview.{0}', github.run_number) || '' }} dotnet nuget push bin/${{ env.BUILD_CONFIG }}/*.nupkg \ -k ${{ github.event.inputs.feed == 'Production' && secrets.NUGET_API_KEY || secrets.NUGETTEST_API_KEY }} \ -s ${{ github.event.inputs.feed == 'Production' && 'https://api.nuget.org/v3/index.json' || 'https://apiint.nugettest.org/v3/index.json' }} diff --git a/README.md b/README.md index 580adbf..1dd5ede 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,17 @@ [![Nethermind.Crypto.Bls](https://img.shields.io/nuget/v/Nethermind.Crypto.Bls)](https://www.nuget.org/packages/Nethermind.Crypto.Bls) C# bindings for the [Supranational blst library](https://github.com/supranational/blst), supporting operations on the BLS12-381 curve and BLS signatures. + +## Development + +Run the tests: + +```sh +dotnet test src -c release +``` + +Run the benchmarks (optionally filtered by class, e.g. `--filter '*MsmBenchmarks*'`): + +```sh +dotnet run --project src/Nethermind.Crypto.Bls.Bench -c release +``` diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 71092e5..a2e8cc5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -13,7 +13,7 @@ Nethermind Demerzel Solutions Limited $(Commit.Substring(0, 8)) - 1.0.5 + 1.1.0 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index bf30432..ad3bba8 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,6 +3,7 @@ true + diff --git a/src/Nethermind.Crypto.Bls.Bench/BlsBenchmarks.cs b/src/Nethermind.Crypto.Bls.Bench/BlsBenchmarks.cs new file mode 100644 index 0000000..8429cae --- /dev/null +++ b/src/Nethermind.Crypto.Bls.Bench/BlsBenchmarks.cs @@ -0,0 +1,394 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using System; +using BenchmarkDotNet.Attributes; +using Nethermind.Crypto; + +namespace Nethermind.Crypto.Bench; + +using G1 = Bls.P1; +using G2 = Bls.P2; +using G1Affine = Bls.P1Affine; +using G2Affine = Bls.P2Affine; +using GT = Bls.PT; + +public static class BenchmarkData +{ + public static readonly byte[] Dst = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"u8.ToArray(); + + public static byte[] RandomScalar(Random rng) + { + byte[] scalar = new byte[32]; + rng.NextBytes(scalar); + scalar[31] &= 0x1f; // keep the little-endian value below the group order + return scalar; + } +} + +[MemoryDiagnoser] +public class KeyBenchmarks +{ + private readonly byte[] _ikm = new byte[32]; + private readonly byte[] _sk = new byte[32]; + private readonly long[] _pk = new long[G1.Sz]; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + rng.NextBytes(_ikm); + new Bls.SecretKey(_sk).Keygen(_ikm); + } + + [Benchmark] + public long Keygen() + { + new Bls.SecretKey(_sk).Keygen(_ikm); + return _sk[0]; + } + + [Benchmark] + public long SkToPkG1() + { + new G1(_pk).FromSk(new Bls.SecretKey(_sk)); + return _pk[0]; + } +} + +[MemoryDiagnoser] +public class SignatureBenchmarks +{ + private readonly byte[] _msg = new byte[32]; + private readonly byte[] _sk = new byte[32]; + private readonly long[] _pkAffine = new long[G1Affine.Sz]; + private readonly long[] _sigAffine = new long[G2Affine.Sz]; + private readonly long[] _hash = new long[G2.Sz]; + private readonly long[] _scratch = new long[G2.Sz]; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + rng.NextBytes(_msg); + + byte[] ikm = new byte[32]; + rng.NextBytes(ikm); + Bls.SecretKey sk = new(_sk); + sk.Keygen(ikm); + + new G1Affine(new G1(sk)).Point.CopyTo(_pkAffine); + G2 sig = new G2(_hash).HashTo(_msg, BenchmarkData.Dst); + new G2Affine(sig.Dup().SignWith(sk)).Point.CopyTo(_sigAffine); + } + + [Benchmark] + public long HashToG2() + { + new G2(_scratch).HashTo(_msg, BenchmarkData.Dst); + return _scratch[0]; + } + + [Benchmark] + public long SignPrehashed() + { + _hash.CopyTo(_scratch.AsSpan()); + new G2(_scratch).SignWith(new Bls.SecretKey(_sk)); + return _scratch[0]; + } + + [Benchmark] + public bool Verify() + { + Bls.Pairing ctx = new(true, BenchmarkData.Dst); + ctx.Aggregate(new G1Affine(_pkAffine), new G2Affine(_sigAffine), _msg); + ctx.Commit(); + return ctx.FinalVerify(); + } +} + +[MemoryDiagnoser] +public class G1Benchmarks +{ + private readonly byte[] _scalar = new byte[32]; + private readonly long[] _point = new long[G1.Sz]; + private readonly long[] _affine = new long[G1Affine.Sz]; + private readonly long[] _scratch = new long[G1.Sz]; + private byte[] _compressed = []; + private byte[] _serialized = []; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + BenchmarkData.RandomScalar(rng).CopyTo(_scalar.AsSpan()); + + G1 p = G1.Generator(_point).Mult(_scalar); + new G1Affine(p).Point.CopyTo(_affine); + _compressed = p.Compress(); + _serialized = p.Serialize(); + } + + [Benchmark] + public long Mult() + { + _point.CopyTo(_scratch.AsSpan()); + new G1(_scratch).Mult(_scalar); + return _scratch[0]; + } + + [Benchmark] + public long Add() + { + _point.CopyTo(_scratch.AsSpan()); + new G1(_scratch).Add(new G1Affine(_affine)); + return _scratch[0]; + } + + [Benchmark] + public bool DecodeCompressedValidated() + => new G1Affine(_scratch).TryDecode(_compressed, out _); + + [Benchmark] + public bool DecodeUncompressedValidated() + => new G1Affine(_scratch).TryDecode(_serialized, out _); + + [Benchmark] + public bool DecodeUncompressedRaw() + => new G1(_scratch).TryDecode(_serialized, out _); + + [Benchmark] + public bool SubgroupCheck() + => new G1(_point).InGroup(); + + [Benchmark] + public byte[] Compress() + => new G1(_point).Compress(); +} + +[MemoryDiagnoser] +public class G2Benchmarks +{ + private readonly byte[] _scalar = new byte[32]; + private readonly long[] _point = new long[G2.Sz]; + private readonly long[] _affine = new long[G2Affine.Sz]; + private readonly long[] _scratch = new long[G2.Sz]; + private byte[] _compressed = []; + private byte[] _serialized = []; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + BenchmarkData.RandomScalar(rng).CopyTo(_scalar.AsSpan()); + + G2 p = G2.Generator(_point).Mult(_scalar); + new G2Affine(p).Point.CopyTo(_affine); + _compressed = p.Compress(); + _serialized = p.Serialize(); + } + + [Benchmark] + public long Mult() + { + _point.CopyTo(_scratch.AsSpan()); + new G2(_scratch).Mult(_scalar); + return _scratch[0]; + } + + [Benchmark] + public long Add() + { + _point.CopyTo(_scratch.AsSpan()); + new G2(_scratch).Add(new G2Affine(_affine)); + return _scratch[0]; + } + + [Benchmark] + public bool DecodeCompressedValidated() + => new G2Affine(_scratch).TryDecode(_compressed, out _); + + [Benchmark] + public bool DecodeUncompressedValidated() + => new G2Affine(_scratch).TryDecode(_serialized, out _); + + [Benchmark] + public bool DecodeUncompressedRaw() + => new G2(_scratch).TryDecode(_serialized, out _); + + [Benchmark] + public bool SubgroupCheck() + => new G2(_point).InGroup(); + + [Benchmark] + public byte[] Compress() + => new G2(_point).Compress(); +} + +[MemoryDiagnoser] +public class PairingBenchmarks +{ + private readonly long[] _p = new long[G1Affine.Sz]; + private readonly long[] _q = new long[G2Affine.Sz]; + private readonly long[] _lines = new long[68 * 6 * 6]; + private readonly long[] _millerOut = new long[GT.Sz]; + private readonly long[] _scratch = new long[GT.Sz]; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + G1Affine p = new(G1.Generator().Mult(BenchmarkData.RandomScalar(rng))); + G2Affine q = new(G2.Generator().Mult(BenchmarkData.RandomScalar(rng))); + p.Point.CopyTo(_p); + q.Point.CopyTo(_q); + q.PrecomputeLines(_lines); + new GT(_millerOut).MillerLoop(q, p); + } + + [Benchmark] + public long MillerLoop() + { + new GT(_scratch).MillerLoop(new G2Affine(_q), new G1Affine(_p)); + return _scratch[0]; + } + + [Benchmark] + public long MillerLoopWithPrecomputedLines() + { + new GT(_scratch).MillerLoopLines(_lines, new G1Affine(_p)); + return _scratch[0]; + } + + [Benchmark] + public long PrecomputeLines() + { + new G2Affine(_q).PrecomputeLines(_lines); + return _lines[0]; + } + + [Benchmark] + public long FinalExp() + { + _millerOut.CopyTo(_scratch.AsSpan()); + new GT(_scratch).FinalExp(); + return _scratch[0]; + } + + [Benchmark] + public bool FinalVerify() + => GT.FinalVerify(new GT(_millerOut), new GT(_millerOut)); +} + +[MemoryDiagnoser] +public class MsmBenchmarks +{ + [Params(16, 128, 512)] + public int Npoints; + + private long[] _g1Points = []; + private long[] _g2Points = []; + private long[] _g1Affines = []; + private long[] _g2Affines = []; + private byte[] _scalars = []; + private readonly long[] _g1Result = new long[G1.Sz]; + private readonly long[] _g2Result = new long[G2.Sz]; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + _g1Points = new long[Npoints * G1.Sz]; + _g2Points = new long[Npoints * G2.Sz]; + _g1Affines = new long[Npoints * G1Affine.Sz]; + _g2Affines = new long[Npoints * G2Affine.Sz]; + _scalars = new byte[Npoints * 32]; + + for (int i = 0; i < Npoints; i++) + { + byte[] scalar = BenchmarkData.RandomScalar(rng); + G1 p = G1.Generator(_g1Points.AsSpan(i * G1.Sz)).Mult(scalar); + G2 q = G2.Generator(_g2Points.AsSpan(i * G2.Sz)).Mult(scalar); + new G1Affine(p).Point.CopyTo(_g1Affines.AsSpan(i * G1Affine.Sz)); + new G2Affine(q).Point.CopyTo(_g2Affines.AsSpan(i * G2Affine.Sz)); + BenchmarkData.RandomScalar(rng).CopyTo(_scalars.AsSpan(i * 32)); + } + } + + [Benchmark] + public long G1MultiMult() + { + new G1(_g1Result).MultiMult(_g1Points, _scalars, Npoints); + return _g1Result[0]; + } + + [Benchmark] + public long G2MultiMult() + { + new G2(_g2Result).MultiMult(_g2Points, _scalars, Npoints); + return _g2Result[0]; + } + + [Benchmark] + public long G1MultiMultAffine() + { + new G1(_g1Result).MultiMultAffine(_g1Affines, _scalars, Npoints); + return _g1Result[0]; + } + + [Benchmark] + public long G2MultiMultAffine() + { + new G2(_g2Result).MultiMultAffine(_g2Affines, _scalars, Npoints); + return _g2Result[0]; + } +} + +[MemoryDiagnoser] +public class PairingCheckBenchmarks +{ + [Params(2, 8, 16)] + public int Npairs; + + private long[] _qAffines = []; + private long[] _pAffines = []; + private readonly long[] _acc = new long[GT.Sz]; + private readonly long[] _tmp = new long[GT.Sz]; + + [GlobalSetup] + public void Setup() + { + Random rng = new(42); + _qAffines = new long[Npairs * G2Affine.Sz]; + _pAffines = new long[Npairs * G1Affine.Sz]; + + for (int i = 0; i < Npairs; i++) + { + G1Affine p = new(G1.Generator().Mult(BenchmarkData.RandomScalar(rng))); + G2Affine q = new(G2.Generator().Mult(BenchmarkData.RandomScalar(rng))); + p.Point.CopyTo(_pAffines.AsSpan(i * G1Affine.Sz)); + q.Point.CopyTo(_qAffines.AsSpan(i * G2Affine.Sz)); + } + } + + [Benchmark(Baseline = true)] + public bool SequentialMillerLoops() + { + GT acc = GT.One(_acc); + for (int i = 0; i < Npairs; i++) + { + GT t = new(_tmp); + t.MillerLoop(new G2Affine(_qAffines.AsSpan(i * G2Affine.Sz)), new G1Affine(_pAffines.AsSpan(i * G1Affine.Sz))); + acc.Mul(t); + } + return acc.FinalExp().IsOne(); + } + + [Benchmark] + public bool BatchedMillerLoopN() + { + GT acc = new(_acc); + acc.MillerLoopN(_qAffines, _pAffines, Npairs); + return acc.FinalExp().IsOne(); + } +} diff --git a/src/Nethermind.Crypto.Bls.Bench/Nethermind.Crypto.Bls.Bench.csproj b/src/Nethermind.Crypto.Bls.Bench/Nethermind.Crypto.Bls.Bench.csproj new file mode 100644 index 0000000..1b9f2a0 --- /dev/null +++ b/src/Nethermind.Crypto.Bls.Bench/Nethermind.Crypto.Bls.Bench.csproj @@ -0,0 +1,14 @@ + + + Exe + false + + + + + + + + + + diff --git a/src/Nethermind.Crypto.Bls.Bench/Program.cs b/src/Nethermind.Crypto.Bls.Bench/Program.cs new file mode 100644 index 0000000..5f9092c --- /dev/null +++ b/src/Nethermind.Crypto.Bls.Bench/Program.cs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + +public partial class Program +{ } diff --git a/src/Nethermind.Crypto.Bls.slnx b/src/Nethermind.Crypto.Bls.slnx index 87cc7d9..e01f9cf 100644 --- a/src/Nethermind.Crypto.Bls.slnx +++ b/src/Nethermind.Crypto.Bls.slnx @@ -3,6 +3,7 @@ + diff --git a/src/Nethermind.Crypto.Bls/Bls.cs b/src/Nethermind.Crypto.Bls/Bls.cs index adfa432..fd769b5 100644 --- a/src/Nethermind.Crypto.Bls/Bls.cs +++ b/src/Nethermind.Crypto.Bls/Bls.cs @@ -7,13 +7,14 @@ // SPDX-License-Identifier: Apache-2.0 using System; -using System.Text; +using System.Buffers; +using System.IO; using System.Numerics; -using System.Runtime.InteropServices; using System.Reflection; -using System.IO; -using size_t = nuint; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using size_t = nuint; namespace Nethermind.Crypto; @@ -53,7 +54,7 @@ private static nint LoadLibrary(string libraryName, Assembly assembly, DllImport } var arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); - var path = Path.Combine("runtimes", $"{platform}-{arch}", "native", libraryName); + var path = Path.Combine(AppContext.BaseDirectory, "runtimes", $"{platform}-{arch}", "native", libraryName); return NativeLibrary.Load(path, assembly, searchPath); } @@ -71,11 +72,11 @@ public enum ERROR public class BlsException(ERROR err) : ApplicationException { - private readonly ERROR code = err; + public ERROR Error { get; } = err; public override string Message { - get => code switch + get => Error switch { ERROR.BADENCODING => "bad encoding", ERROR.POINTNOTONCURVE => "point not on curve", @@ -96,55 +97,60 @@ public enum ByteOrder } [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_keygen(Span key, ReadOnlySpan IKM, size_t IKM_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_keygen(Span key, ReadOnlySpan IKM, size_t IKM_len, ReadOnlySpan info, size_t info_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_keygen_v3(Span key, ReadOnlySpan IKM, size_t IKM_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_keygen_v3(Span key, ReadOnlySpan IKM, size_t IKM_len, ReadOnlySpan info, size_t info_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_keygen_v4_5(Span key, ReadOnlySpan IKM, size_t IKM_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_keygen_v4_5(Span key, ReadOnlySpan IKM, size_t IKM_len, ReadOnlySpan salt, size_t salt_len, ReadOnlySpan info, size_t info_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_keygen_v5(Span key, ReadOnlySpan IKM, size_t IKM_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_keygen_v5(Span key, ReadOnlySpan IKM, size_t IKM_len, ReadOnlySpan salt, size_t salt_len, ReadOnlySpan info, size_t info_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_derive_master_eip2333(Span key, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_derive_master_eip2333(Span key, ReadOnlySpan IKM, size_t IKM_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_derive_child_eip2333(Span key, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_derive_child_eip2333(Span key, ReadOnlySpan master, uint child_index); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_scalar_from_bendian(Span ret, ReadOnlySpan key); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_scalar_from_bendian(Span ret, ReadOnlySpan key); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_bendian_from_scalar(Span ret, ReadOnlySpan key); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_bendian_from_scalar(Span ret, ReadOnlySpan key); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_sk_check(ReadOnlySpan key); + private static partial bool blst_sk_check(ReadOnlySpan key); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_scalar_from_lendian(Span key, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_scalar_from_lendian(Span key, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_lendian_from_scalar(Span key, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_lendian_from_scalar(Span key, ReadOnlySpan inp); public readonly ref struct SecretKey { public readonly ReadOnlySpan Key { get => _key; } private readonly Span _key; + /// + /// Wraps caller-provided storage as the key without copying or validation. The bytes are + /// interpreted in blst's internal little-endian scalar form; to parse an encoded key use + /// . + /// [OverloadResolutionPriority(1)] public SecretKey(Span key) { @@ -171,18 +177,21 @@ public SecretKey(scoped ReadOnlySpan inp, ByteOrder order = ByteOrder.BigE public readonly void Keygen(scoped ReadOnlySpan IKM, string info = "") { + ValidateIkm(IKM); ReadOnlySpan info_bytes = Encoding.UTF8.GetBytes(info); blst_keygen(_key, IKM, (size_t)IKM.Length, info_bytes, (size_t)info_bytes.Length); } public readonly void KeygenV3(scoped ReadOnlySpan IKM, string info = "") { + ValidateIkm(IKM); ReadOnlySpan info_bytes = Encoding.UTF8.GetBytes(info); blst_keygen_v3(_key, IKM, (size_t)IKM.Length, info_bytes, (size_t)info_bytes.Length); } public readonly void KeygenV45(scoped ReadOnlySpan IKM, string salt, string info = "") { + ValidateIkm(IKM); ReadOnlySpan salt_bytes = Encoding.UTF8.GetBytes(salt); ReadOnlySpan info_bytes = Encoding.UTF8.GetBytes(info); blst_keygen_v4_5(_key, IKM, (size_t)IKM.Length, @@ -191,15 +200,28 @@ public readonly void KeygenV45(scoped ReadOnlySpan IKM, string salt, strin } public readonly void KeygenV5(scoped ReadOnlySpan IKM, scoped ReadOnlySpan salt, string info = "") { + ValidateIkm(IKM); ReadOnlySpan info_bytes = Encoding.UTF8.GetBytes(info); blst_keygen_v5(_key, IKM, (size_t)IKM.Length, salt, (size_t)salt.Length, info_bytes, (size_t)info_bytes.Length); } + + // blst silently returns an all-zero key when given less than 32 bytes of key material + private static void ValidateIkm(ReadOnlySpan IKM) + { + if (IKM.Length < 32) + { + throw new ArgumentException("Insufficient key material. Expected at least 32 bytes.", nameof(IKM)); + } + } public readonly void KeygenV5(scoped ReadOnlySpan IKM, string salt, string info = "") => KeygenV5(IKM, Encoding.UTF8.GetBytes(salt), info); public readonly void DeriveMasterEip2333(scoped ReadOnlySpan IKM) - => blst_derive_master_eip2333(_key, IKM, (size_t)IKM.Length); + { + ValidateIkm(IKM); + blst_derive_master_eip2333(_key, IKM, (size_t)IKM.Length); + } public SecretKey(SecretKey master, uint childIndex) { _key = new byte[32]; @@ -250,37 +272,42 @@ public readonly byte[] ToLendian() } [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_scalar_from_be_bytes(Span ret, ReadOnlySpan inp, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_scalar_from_be_bytes(Span ret, ReadOnlySpan inp, size_t inp_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_scalar_from_le_bytes(Span ret, ReadOnlySpan inp, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_scalar_from_le_bytes(Span ret, ReadOnlySpan inp, size_t inp_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_sk_add_n_check(Span ret, ReadOnlySpan a, + private static partial bool blst_sk_add_n_check(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_sk_sub_n_check(Span ret, ReadOnlySpan a, + private static partial bool blst_sk_sub_n_check(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_sk_mul_n_check(Span ret, ReadOnlySpan a, + private static partial bool blst_sk_mul_n_check(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_sk_inverse(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_sk_inverse(Span ret, ReadOnlySpan a); public readonly ref struct Scalar { public readonly ReadOnlySpan Val { get => _val; } private readonly Span _val; + /// + /// Wraps caller-provided storage as the scalar without copying or validation. The bytes are + /// interpreted in blst's internal little-endian form; to parse an encoded value use + /// . + /// [OverloadResolutionPriority(1)] public Scalar(Span val) { @@ -378,50 +405,42 @@ public readonly Scalar Inverse() [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_p1_affine_sizeof(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_p1_affine_sizeof(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_p1_deserialize(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_p1_deserialize(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_affine_serialize(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_affine_serialize(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_affine_compress(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_affine_compress(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_to_affine(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_to_affine(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_affine_on_curve(ReadOnlySpan point); + private static partial bool blst_p1_affine_on_curve(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_affine_in_g1(ReadOnlySpan point); + private static partial bool blst_p1_affine_in_g1(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_affine_is_inf(ReadOnlySpan point); + private static partial bool blst_p1_affine_is_inf(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_affine_is_equal(ReadOnlySpan a, ReadOnlySpan b); + private static partial bool blst_p1_affine_is_equal(ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial IntPtr blst_p1_generator(); - - // [LibraryImport(LibraryName)] - // [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - // static private partial ERROR blst_core_verify_pk_in_g2(ReadOnlySpan pk, ReadOnlySpan sig, - // [MarshalAs(UnmanagedType.Bool)] bool hash_or_encode, - // ReadOnlySpan msg, size_t msg_len, - // ReadOnlySpan dst, size_t dst_len, - // ReadOnlySpan aug, size_t aug_len); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial nint blst_p1_generator(); public readonly ref struct P1Affine { @@ -483,6 +502,18 @@ public void Decode(scoped ReadOnlySpan inp) } } + /// Decodes raw affine coordinates without any validation, as in . + public void Decode(ReadOnlySpan fp1, ReadOnlySpan fp2) + { + if (fp1.Length != 48 || fp2.Length != 48) + { + throw new ArgumentException("Invalid input length. Expected 48 bytes for each field element."); + } + + blst_fp_from_bendian(_point, fp1); + blst_fp_from_bendian(_point[6..], fp2); + } + public P1Affine(P1 jacobian) : this() => blst_p1_to_affine(_point, jacobian.Point); @@ -510,123 +541,107 @@ public bool IsInf() public bool IsEqual(P1Affine p) => blst_p1_affine_is_equal(_point, p._point); - // ERROR core_verify(P2_Affine pk, bool hash_or_encode, - // #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - // byte[] msg, string DST = "", byte[] aug = null) - // #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - // { - // byte[] dst = Encoding.UTF8.GetBytes(DST); - // return blst_core_verify_pk_in_g2(pk.point, point, - // hash_or_encode, - // msg, (size_t)msg.Length, - // dst, (size_t)dst.Length, - // aug, (size_t)(aug != null ? aug.Length : 0)); - // } - public static P1Affine Generator() { long[] res = new long[Sz]; return Generator(res); } - public unsafe static P1Affine Generator(Span p) + public static unsafe P1Affine Generator(Span p) { - int s = (int)blst_p1_affine_sizeof(); - fixed (long* dest = p) - { - Buffer.MemoryCopy((byte*)blst_p1_generator(), dest, s, s); - } - return new(p); + P1Affine res = new(p); + new ReadOnlySpan((void*)blst_p1_generator(), Sz).CopyTo(res._point); + return res; } } [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_fp_from_bendian(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_fp_from_bendian(Span ret, ReadOnlySpan a); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_p1_sizeof(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_p1_sizeof(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_serialize(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_serialize(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_compress(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_compress(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_from_affine(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_from_affine(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_on_curve(ReadOnlySpan point); + private static partial bool blst_p1_on_curve(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_in_g1(ReadOnlySpan point); + private static partial bool blst_p1_in_g1(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_is_inf(ReadOnlySpan point); + private static partial bool blst_p1_is_inf(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p1_is_equal(ReadOnlySpan a, ReadOnlySpan b); + private static partial bool blst_p1_is_equal(ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_sk_to_pk_in_g1(Span ret, ReadOnlySpan SK); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_sk_to_pk_in_g1(Span ret, ReadOnlySpan SK); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_map_to_g1(Span ret, ReadOnlySpan u, ReadOnlySpan v); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_map_to_g1(Span ret, ReadOnlySpan u, ReadOnlySpan v); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_encode_to_g1(Span ret, ReadOnlySpan msg, size_t msg_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_encode_to_g1(Span ret, ReadOnlySpan msg, size_t msg_len, ReadOnlySpan dst, size_t dst_len, ReadOnlySpan aug, size_t aug_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_hash_to_g1(Span ret, ReadOnlySpan msg, size_t msg_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_hash_to_g1(Span ret, ReadOnlySpan msg, size_t msg_len, ReadOnlySpan dst, size_t dst_len, ReadOnlySpan aug, size_t aug_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_sign_pk_in_g2(Span ret, ReadOnlySpan hash, ReadOnlySpan SK); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_sign_pk_in_g2(Span ret, ReadOnlySpan hash, ReadOnlySpan SK); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_mult(Span ret, ReadOnlySpan a, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_mult(Span ret, ReadOnlySpan a, ReadOnlySpan scalar, size_t nbits); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_cneg(Span ret, [MarshalAs(UnmanagedType.Bool)] bool cbit); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_cneg(Span ret, [MarshalAs(UnmanagedType.Bool)] bool cbit); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_add_or_double(Span ret, ReadOnlySpan a, ReadOnlySpan b); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_add_or_double(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_add_or_double_affine(Span ret, ReadOnlySpan a, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_add_or_double_affine(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p1_double(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p1_double(Span ret, ReadOnlySpan a); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_p1s_mult_pippenger_scratch_sizeof(size_t npoints); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_p1s_mult_pippenger_scratch_sizeof(size_t npoints); // void blst_p1s_mult_pippenger(blst_p1 *ret, const blst_p1_affine *const points[], // size_t npoints, const byte *const scalars[], // size_t nbits, limb_t *scratch); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static unsafe partial void blst_p1s_mult_pippenger(Span ret, long** points, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe partial void blst_p1s_mult_pippenger(Span ret, long** points, size_t npoints, byte** scalars, size_t nbits, long* scratch); // void blst_p1s_to_affine(blst_p1_affine dst[], const blst_p1 *const points[], // size_t npoints); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static unsafe partial void blst_p1s_to_affine(Span dst, long** points, size_t npoints); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe partial void blst_p1s_to_affine(Span dst, long** points, size_t npoints); public readonly ref struct P1 { @@ -667,6 +682,13 @@ public P1(scoped ReadOnlySpan inp) : this() public readonly void Zero() => _point.Clear(); + /// + /// Decodes a 48-byte compressed or 96-byte uncompressed point. + /// Unlike , the uncompressed path reads the two field + /// elements as-is with no validation (no canonicality, flag-bit, curve or subgroup checks), + /// so that callers implementing protocols such as EIP-2537 can validate separately. + /// Call / where validation is required. + /// public bool TryDecode(scoped ReadOnlySpan inp, out ERROR err) { int len = inp.Length; @@ -697,6 +719,7 @@ public bool TryDecode(scoped ReadOnlySpan inp, out ERROR err) return true; } + /// Decodes raw affine coordinates without any validation, as in . public void Decode(ReadOnlySpan fp1, ReadOnlySpan fp2) { if (fp1.Length != 48 || fp2.Length != 48) @@ -744,11 +767,16 @@ public readonly bool IsInf() public readonly bool IsEqual(P1 p) => blst_p1_is_equal(_point, p._point); - public readonly P1 MapTo(ReadOnlySpan fp) + public readonly P1 MapTo(scoped ReadOnlySpan fp) { - long[] u = new long[6]; + if (fp.Length != 48) + { + throw new ArgumentException("Invalid input length. Expected 48 bytes.", nameof(fp)); + } + + Span u = stackalloc long[6]; blst_fp_from_bendian(u, fp); - blst_map_to_g1(_point, u, null); + blst_map_to_g1(_point, u, default); return this; } public readonly P1 HashTo(scoped ReadOnlySpan msg, ReadOnlySpan DST = default, ReadOnlySpan aug = default) @@ -822,45 +850,94 @@ public readonly P1 Mult(in BigInteger scalar) return this; } - private readonly void PrepareMult(ref Scalar scalar) + /// + /// Multi-scalar multiplication over Jacobian points stored + /// contiguously in with 32-byte little-endian scalars stored + /// contiguously in . Points at infinity must be filtered out + /// by the caller. + /// + public readonly unsafe P1 MultiMult(scoped ReadOnlySpan rawPoints, scoped ReadOnlySpan rawScalars, int npoints) { - byte[] val = PrepareMult(new(scalar.ToBendian(), true, true)); - scalar.FromBendian(val); - } + ArgumentOutOfRangeException.ThrowIfNegative(npoints); + if (rawPoints.Length < npoints * Sz) + { + throw new ArgumentException($"Insufficient points for the given count. Expected {npoints * Sz} longs.", nameof(rawPoints)); + } + if (rawScalars.Length < npoints * 32) + { + throw new ArgumentException($"Insufficient scalars for the given count. Expected {npoints * 32} bytes.", nameof(rawScalars)); + } - private readonly unsafe P1 MultiMultRawAffines(long* rawAffinesPtr, scoped Span rawScalars, int npoints) - { - fixed (byte* rawScalarsPtr = rawScalars) + // blst does not support an empty multiplication; the result is the point at infinity + if (npoints == 0) { - long*[] rawAffinesWrapper = [rawAffinesPtr, null]; - byte*[] rawScalarsWrapper = [rawScalarsPtr, null]; + Zero(); + return this; + } - size_t scratchSize = blst_p1s_mult_pippenger_scratch_sizeof((size_t)npoints) / sizeof(long); - Span scratch = new long[(int)scratchSize]; + int affinesLen = npoints * P1Affine.Sz; + long[] affines = ArrayPool.Shared.Rent(affinesLen); - fixed (long** rawAffinesWrapperPtr = rawAffinesWrapper) - fixed (byte** rawScalarsWrapperPtr = rawScalarsWrapper) - fixed (long* scratchPtr = scratch) - blst_p1s_mult_pippenger(_point, rawAffinesWrapperPtr, (size_t)npoints, rawScalarsWrapperPtr, 256, scratchPtr); + try + { + // a [ptr, null] argument tells blst to read all points from one contiguous buffer + fixed (long* rawPointsPtr = rawPoints) + { + long** points = stackalloc long*[2] { rawPointsPtr, null }; + blst_p1s_to_affine(affines.AsSpan(0, affinesLen), points, (size_t)npoints); + } + + return MultiMultAffine(affines.AsSpan(0, affinesLen), rawScalars, npoints); + } + finally + { + ArrayPool.Shared.Return(affines); } - return this; } - // points at infinity should be filtered out, scalars little endian - public readonly unsafe P1 MultiMult(scoped Span rawPoints, scoped Span rawScalars, int npoints) + /// + /// As , but over affine points, skipping the Jacobian-to-affine + /// batch conversion. + /// + public readonly unsafe P1 MultiMultAffine(scoped ReadOnlySpan rawAffines, scoped ReadOnlySpan rawScalars, int npoints) { - Span rawAffines = new long[npoints * 12]; + ArgumentOutOfRangeException.ThrowIfNegative(npoints); + if (rawAffines.Length < npoints * P1Affine.Sz) + { + throw new ArgumentException($"Insufficient points for the given count. Expected {npoints * P1Affine.Sz} longs.", nameof(rawAffines)); + } + if (rawScalars.Length < npoints * 32) + { + throw new ArgumentException($"Insufficient scalars for the given count. Expected {npoints * 32} bytes.", nameof(rawScalars)); + } - fixed (long* rawPointsPtr = rawPoints) + // blst does not support an empty multiplication; the result is the point at infinity + if (npoints == 0) { - long*[] rawPointsWrapper = [rawPointsPtr, null]; + Zero(); + return this; + } + + int scratchLen = (int)(blst_p1s_mult_pippenger_scratch_sizeof((size_t)npoints) / sizeof(long)); + long[] scratch = ArrayPool.Shared.Rent(scratchLen); - fixed (long** rawPointsWrapperPtr = rawPointsWrapper) - blst_p1s_to_affine(rawAffines, rawPointsWrapperPtr, (size_t)npoints); + try + { + fixed (long* affinesPtr = rawAffines) + fixed (byte* scalarsPtr = rawScalars) + fixed (long* scratchPtr = scratch) + { + long** points = stackalloc long*[2] { affinesPtr, null }; + byte** scalars = stackalloc byte*[2] { scalarsPtr, null }; + blst_p1s_mult_pippenger(_point, points, (size_t)npoints, scalars, 256, scratchPtr); + } + } + finally + { + ArrayPool.Shared.Return(scratch); } - fixed (long* rawAffinesPtr = rawAffines) - return MultiMultRawAffines(rawAffinesPtr, rawScalars, npoints); + return this; } public readonly P1 Cneg(bool flag) { blst_p1_cneg(_point, flag); return this; } public readonly P1 Neg() { blst_p1_cneg(_point, true); return this; } @@ -877,31 +954,28 @@ public static P1 Generator() return Generator(res); } - public unsafe static P1 Generator(Span p) + public static unsafe P1 Generator(Span p) { - int s = (int)blst_p1_sizeof(); - fixed (long* dest = p) - { - Buffer.MemoryCopy((byte*)blst_p1_generator(), dest, s, s); - } - return new(p); + P1 res = new(p); + new ReadOnlySpan((void*)blst_p1_generator(), Sz).CopyTo(res._point); + return res; } } public static P1 G1() => P1.Generator(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_aggregated_in_g1(Span fp12, ReadOnlySpan p); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_aggregated_in_g1(Span fp12, ReadOnlySpan p); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_pairing_aggregate_pk_in_g1(Span fp12, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_pairing_aggregate_pk_in_g1(Span fp12, ReadOnlySpan pk, ReadOnlySpan sig, ReadOnlySpan msg, size_t msg_len, ReadOnlySpan aug, size_t aug_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_pairing_mul_n_aggregate_pk_in_g1(Span fp12, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_pairing_mul_n_aggregate_pk_in_g1(Span fp12, ReadOnlySpan pk, ReadOnlySpan sig, ReadOnlySpan scalar, size_t nbits, ReadOnlySpan msg, size_t msg_len, @@ -909,55 +983,47 @@ static private partial ERROR blst_pairing_mul_n_aggregate_pk_in_g1(Span fp [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_p2_affine_sizeof(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_p2_affine_sizeof(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_p2_deserialize(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_p2_deserialize(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_affine_serialize(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_affine_serialize(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_affine_compress(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_affine_compress(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_to_affine(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_to_affine(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_affine_on_curve(ReadOnlySpan point); + private static partial bool blst_p2_affine_on_curve(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_affine_in_g2(ReadOnlySpan point); + private static partial bool blst_p2_affine_in_g2(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_affine_is_inf(ReadOnlySpan point); + private static partial bool blst_p2_affine_is_inf(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_affine_is_equal(ReadOnlySpan a, ReadOnlySpan b); + private static partial bool blst_p2_affine_is_equal(ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial IntPtr blst_p2_generator(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial nint blst_p2_generator(); //void blst_precompute_lines(blst_fp6 Qlines[68], const blst_p2_affine *Q); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_precompute_lines(Span qlines, ReadOnlySpan q); - - // [LibraryImport(LibraryName)] - // [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - // static private partial ERROR blst_core_verify_pk_in_g1(ReadOnlySpan pk, ReadOnlySpan sig, - // [MarshalAs(UnmanagedType.Bool)] bool hash_or_encode, - // ReadOnlySpan msg, size_t msg_len, - // ReadOnlySpan dst, size_t dst_len, - // ReadOnlySpan aug, size_t aug_len); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_precompute_lines(Span qlines, ReadOnlySpan q); public readonly ref struct P2Affine { @@ -1020,6 +1086,20 @@ public void Decode(scoped ReadOnlySpan inp) } } + /// Decodes raw affine coordinates without any validation, as in . + public void Decode(ReadOnlySpan fp1, ReadOnlySpan fp2, ReadOnlySpan fp3, ReadOnlySpan fp4) + { + if (fp1.Length != 48 || fp2.Length != 48 || fp3.Length != 48 || fp4.Length != 48) + { + throw new ArgumentException("Invalid input length. Expected 48 bytes for each field element."); + } + + blst_fp_from_bendian(_point, fp1); + blst_fp_from_bendian(_point[6..], fp2); + blst_fp_from_bendian(_point[12..], fp3); + blst_fp_from_bendian(_point[18..], fp4); + } + public P2Affine(P2 jacobian) : this() { blst_p2_to_affine(_point, jacobian.Point); } @@ -1050,117 +1130,101 @@ public readonly bool IsEqual(P2Affine p) public readonly void PrecomputeLines(Span qlines) => blst_precompute_lines(qlines, _point); - // readonly ERROR core_verify(P1Affine pk, bool hash_or_encode, - // #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - // byte[] msg, string DST = "", byte[] aug = null) - // #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - // { - // byte[] dst = Encoding.UTF8.GetBytes(DST); - // return blst_core_verify_pk_in_g1(pk.Point, Point, - // hash_or_encode, - // msg, (size_t)msg.Length, - // dst, (size_t)dst.Length, - // aug, (size_t)(aug != null ? aug.Length : 0)); - // } - public static P2Affine Generator() { long[] res = new long[Sz]; return Generator(res); } - public unsafe static P2Affine Generator(Span p) + public static unsafe P2Affine Generator(Span p) { - int s = (int)blst_p2_affine_sizeof(); - fixed (long* dest = p) - { - Buffer.MemoryCopy((byte*)blst_p2_generator(), dest, s, s); - } - return new(p); + P2Affine res = new(p); + new ReadOnlySpan((void*)blst_p2_generator(), Sz).CopyTo(res._point); + return res; } } [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_p2_sizeof(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_p2_sizeof(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_serialize(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_serialize(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_compress(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_compress(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_from_affine(Span ret, ReadOnlySpan inp); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_from_affine(Span ret, ReadOnlySpan inp); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_on_curve(ReadOnlySpan point); + private static partial bool blst_p2_on_curve(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_in_g2(ReadOnlySpan point); + private static partial bool blst_p2_in_g2(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_is_inf(ReadOnlySpan point); + private static partial bool blst_p2_is_inf(ReadOnlySpan point); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_p2_is_equal(ReadOnlySpan a, ReadOnlySpan b); + private static partial bool blst_p2_is_equal(ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_sk_to_pk_in_g2(Span ret, ReadOnlySpan SK); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_sk_to_pk_in_g2(Span ret, ReadOnlySpan SK); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_map_to_g2(Span ret, ReadOnlySpan u, ReadOnlySpan v); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_map_to_g2(Span ret, ReadOnlySpan u, ReadOnlySpan v); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_encode_to_g2(Span ret, ReadOnlySpan msg, size_t msg_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_encode_to_g2(Span ret, ReadOnlySpan msg, size_t msg_len, ReadOnlySpan dst, size_t dst_len, ReadOnlySpan aug, size_t aug_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_hash_to_g2(Span ret, ReadOnlySpan msg, size_t msg_len, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_hash_to_g2(Span ret, ReadOnlySpan msg, size_t msg_len, ReadOnlySpan dst, size_t dst_len, ReadOnlySpan aug, size_t aug_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_sign_pk_in_g1(Span ret, ReadOnlySpan hash, ReadOnlySpan SK); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_sign_pk_in_g1(Span ret, ReadOnlySpan hash, ReadOnlySpan SK); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_mult(Span ret, ReadOnlySpan a, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_mult(Span ret, ReadOnlySpan a, ReadOnlySpan scalar, size_t nbits); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_cneg(Span ret, [MarshalAs(UnmanagedType.Bool)] bool cbit); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_cneg(Span ret, [MarshalAs(UnmanagedType.Bool)] bool cbit); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_add_or_double(Span ret, ReadOnlySpan a, ReadOnlySpan b); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_add_or_double(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_add_or_double_affine(Span ret, ReadOnlySpan a, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_add_or_double_affine(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_p2_double(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_p2_double(Span ret, ReadOnlySpan a); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_p2s_mult_pippenger_scratch_sizeof(size_t npoints); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_p2s_mult_pippenger_scratch_sizeof(size_t npoints); // void blst_p2s_mult_pippenger(blst_p2 *ret, const blst_p2_affine *const points[], // size_t npoints, const byte *const scalars[], // size_t nbits, limb_t *scratch); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static unsafe partial void blst_p2s_mult_pippenger(Span ret, long** points, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe partial void blst_p2s_mult_pippenger(Span ret, long** points, size_t npoints, byte** scalars, size_t nbits, long* scratch); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static unsafe partial void blst_p2s_to_affine(Span dst, long** points, size_t npoints); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe partial void blst_p2s_to_affine(Span dst, long** points, size_t npoints); public readonly ref struct P2 { @@ -1201,6 +1265,13 @@ public P2(P2Affine affine) : this() public readonly void Zero() => _point.Clear(); + /// + /// Decodes a 96-byte compressed or 192-byte uncompressed point. + /// Unlike , the uncompressed path reads the four field + /// elements as-is with no validation (no canonicality, flag-bit, curve or subgroup checks), + /// so that callers implementing protocols such as EIP-2537 can validate separately. + /// Call / where validation is required. + /// public bool TryDecode(scoped ReadOnlySpan inp, out ERROR err) { int len = inp.Length; @@ -1233,6 +1304,7 @@ public bool TryDecode(scoped ReadOnlySpan inp, out ERROR err) return true; } + /// Decodes raw affine coordinates without any validation, as in . public void Decode(ReadOnlySpan fp1, ReadOnlySpan fp2, ReadOnlySpan fp3, ReadOnlySpan fp4) { if (fp1.Length != 48 || fp2.Length != 48 || fp3.Length != 48 || fp4.Length != 48) @@ -1279,17 +1351,17 @@ public readonly bool IsInf() public readonly bool IsEqual(P2 p) => blst_p2_is_equal(_point, p._point); - public readonly unsafe P2 MapTo(scoped ReadOnlySpan c0, scoped ReadOnlySpan c1) + public readonly P2 MapTo(scoped ReadOnlySpan c0, scoped ReadOnlySpan c1) { - Span u0 = stackalloc long[6]; - Span u1 = stackalloc long[6]; - - blst_fp_from_bendian(u0, c0); - blst_fp_from_bendian(u1, c1); - - Span u = [.. u0, .. u1]; + if (c0.Length != 48 || c1.Length != 48) + { + throw new ArgumentException("Invalid input length. Expected 48 bytes for each field element."); + } - blst_map_to_g2(_point, u, null); + Span u = stackalloc long[12]; + blst_fp_from_bendian(u, c0); + blst_fp_from_bendian(u[6..], c1); + blst_map_to_g2(_point, u, default); return this; } public readonly P2 HashTo(scoped ReadOnlySpan msg, scoped ReadOnlySpan DST = default, scoped ReadOnlySpan aug = default) @@ -1351,39 +1423,94 @@ public readonly P2 Mult(in BigInteger scalar) blst_p2_mult(_point, _point, val, (size_t)(len * 8)); return this; } - private readonly unsafe P2 MultiMultRawAffines(long* rawAffinesPtr, scoped Span rawScalars, int npoints) + /// + /// Multi-scalar multiplication over Jacobian points stored + /// contiguously in with 32-byte little-endian scalars stored + /// contiguously in . Points at infinity must be filtered out + /// by the caller. + /// + public readonly unsafe P2 MultiMult(scoped ReadOnlySpan rawPoints, scoped ReadOnlySpan rawScalars, int npoints) { - fixed (byte* rawScalarsPtr = rawScalars) + ArgumentOutOfRangeException.ThrowIfNegative(npoints); + if (rawPoints.Length < npoints * Sz) + { + throw new ArgumentException($"Insufficient points for the given count. Expected {npoints * Sz} longs.", nameof(rawPoints)); + } + if (rawScalars.Length < npoints * 32) { - long*[] rawAffinesWrapper = [rawAffinesPtr, null]; - byte*[] rawScalarsWrapper = [rawScalarsPtr, null]; + throw new ArgumentException($"Insufficient scalars for the given count. Expected {npoints * 32} bytes.", nameof(rawScalars)); + } - size_t scratchSize = blst_p2s_mult_pippenger_scratch_sizeof((size_t)npoints) / sizeof(long); - Span scratch = new long[(int)scratchSize]; + // blst does not support an empty multiplication; the result is the point at infinity + if (npoints == 0) + { + Zero(); + return this; + } - fixed (long** rawAffinesWrapperPtr = rawAffinesWrapper) - fixed (byte** rawScalarsWrapperPtr = rawScalarsWrapper) - fixed (long* scratchPtr = scratch) - blst_p2s_mult_pippenger(_point, rawAffinesWrapperPtr, (size_t)npoints, rawScalarsWrapperPtr, 256, scratchPtr); + int affinesLen = npoints * P2Affine.Sz; + long[] affines = ArrayPool.Shared.Rent(affinesLen); + + try + { + // a [ptr, null] argument tells blst to read all points from one contiguous buffer + fixed (long* rawPointsPtr = rawPoints) + { + long** points = stackalloc long*[2] { rawPointsPtr, null }; + blst_p2s_to_affine(affines.AsSpan(0, affinesLen), points, (size_t)npoints); + } + + return MultiMultAffine(affines.AsSpan(0, affinesLen), rawScalars, npoints); + } + finally + { + ArrayPool.Shared.Return(affines); } - return this; } - // points at infinity should be filtered out, scalars little endian - public readonly unsafe P2 MultiMult(scoped Span rawPoints, scoped Span rawScalars, int npoints) + /// + /// As , but over affine points, skipping the Jacobian-to-affine + /// batch conversion. + /// + public readonly unsafe P2 MultiMultAffine(scoped ReadOnlySpan rawAffines, scoped ReadOnlySpan rawScalars, int npoints) { - Span rawAffines = new long[npoints * 24]; + ArgumentOutOfRangeException.ThrowIfNegative(npoints); + if (rawAffines.Length < npoints * P2Affine.Sz) + { + throw new ArgumentException($"Insufficient points for the given count. Expected {npoints * P2Affine.Sz} longs.", nameof(rawAffines)); + } + if (rawScalars.Length < npoints * 32) + { + throw new ArgumentException($"Insufficient scalars for the given count. Expected {npoints * 32} bytes.", nameof(rawScalars)); + } - fixed (long* rawPointsPtr = rawPoints) + // blst does not support an empty multiplication; the result is the point at infinity + if (npoints == 0) { - long*[] rawPointsWrapper = [rawPointsPtr, null]; + Zero(); + return this; + } + + int scratchLen = (int)(blst_p2s_mult_pippenger_scratch_sizeof((size_t)npoints) / sizeof(long)); + long[] scratch = ArrayPool.Shared.Rent(scratchLen); - fixed (long** rawPointsWrapperPtr = rawPointsWrapper) - blst_p2s_to_affine(rawAffines, rawPointsWrapperPtr, (size_t)npoints); + try + { + fixed (long* affinesPtr = rawAffines) + fixed (byte* scalarsPtr = rawScalars) + fixed (long* scratchPtr = scratch) + { + long** points = stackalloc long*[2] { affinesPtr, null }; + byte** scalars = stackalloc byte*[2] { scalarsPtr, null }; + blst_p2s_mult_pippenger(_point, points, (size_t)npoints, scalars, 256, scratchPtr); + } + } + finally + { + ArrayPool.Shared.Return(scratch); } - fixed (long* rawAffinesPtr = rawAffines) - return MultiMultRawAffines(rawAffinesPtr, rawScalars, npoints); + return this; } public readonly P2 Cneg(bool flag) { blst_p2_cneg(_point, flag); return this; } public readonly P2 Neg() { blst_p2_cneg(_point, true); return this; } @@ -1400,31 +1527,28 @@ public static P2 Generator() return Generator(res); } - public unsafe static P2 Generator(Span p) + public static unsafe P2 Generator(Span p) { - int s = (int)blst_p2_sizeof(); - fixed (long* dest = p) - { - Buffer.MemoryCopy((byte*)blst_p2_generator(), dest, s, s); - } - return new(p); + P2 res = new(p); + new ReadOnlySpan((void*)blst_p2_generator(), Sz).CopyTo(res._point); + return res; } } public static P2 G2() => P2.Generator(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_aggregated_in_g2(Span fp12, ReadOnlySpan p); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_aggregated_in_g2(Span fp12, ReadOnlySpan p); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_pairing_aggregate_pk_in_g2(Span fp12, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_pairing_aggregate_pk_in_g2(Span fp12, ReadOnlySpan pk, ReadOnlySpan sig, ReadOnlySpan msg, size_t msg_len, ReadOnlySpan aug, size_t aug_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_pairing_mul_n_aggregate_pk_in_g2(Span fp12, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_pairing_mul_n_aggregate_pk_in_g2(Span fp12, ReadOnlySpan pk, ReadOnlySpan sig, ReadOnlySpan scalar, size_t nbits, ReadOnlySpan msg, size_t msg_len, @@ -1432,51 +1556,57 @@ static private partial ERROR blst_pairing_mul_n_aggregate_pk_in_g2(Span fp [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_fp12_sizeof(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_fp12_sizeof(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_miller_loop(Span fp12, ReadOnlySpan q, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_miller_loop(Span fp12, ReadOnlySpan q, ReadOnlySpan p); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_fp12_is_one(ReadOnlySpan fp12); + private static partial bool blst_fp12_is_one(ReadOnlySpan fp12); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_fp12_is_equal(ReadOnlySpan a, ReadOnlySpan b); + private static partial bool blst_fp12_is_equal(ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_fp12_sqr(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_fp12_sqr(Span ret, ReadOnlySpan a); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_fp12_mul(Span ret, ReadOnlySpan a, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_fp12_mul(Span ret, ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_final_exp(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_final_exp(Span ret, ReadOnlySpan a); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_fp12_finalverify(ReadOnlySpan a, ReadOnlySpan b); + private static partial bool blst_fp12_finalverify(ReadOnlySpan a, ReadOnlySpan b); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial IntPtr blst_fp12_one(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial nint blst_fp12_one(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_fp12_in_group(ReadOnlySpan a); + private static partial bool blst_fp12_in_group(ReadOnlySpan a); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_bendian_from_fp12(Span ret, ReadOnlySpan a); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_bendian_from_fp12(Span ret, ReadOnlySpan a); // void blst_miller_loop_lines(blst_fp12 *ret, const blst_fp6 Qlines[68], // const blst_p1_affine *P); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_miller_loop_lines(Span ret, ReadOnlySpan qlines, ReadOnlySpan p); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_miller_loop_lines(Span ret, ReadOnlySpan qlines, ReadOnlySpan p); + + // void blst_miller_loop_n(blst_fp12 *ret, const blst_p2_affine *const Qs[], + // const blst_p1_affine *const Ps[], size_t n); + [LibraryImport(LibraryName)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe partial void blst_miller_loop_n(Span ret, long** Qs, long** Ps, size_t n); public readonly ref struct PT { @@ -1550,44 +1680,79 @@ public static PT One() return One(res); } - public unsafe static PT One(Span p) + public static unsafe PT One(Span p) { - int s = (int)blst_fp12_sizeof(); - fixed (long* dest = p) - { - Buffer.MemoryCopy((byte*)blst_fp12_one(), dest, s, s); - } - return new(p); + PT res = new(p); + new ReadOnlySpan((void*)blst_fp12_one(), Sz).CopyTo(res._fp12); + return res; } public void MillerLoopLines(ReadOnlySpan qlines, P1Affine p) { blst_miller_loop_lines(_fp12, qlines, p.Point); } + + /// + /// Computes the product of the Miller loops of point pairs in a + /// single batched pass, sharing the Fp12 squarings across pairs. The points are stored + /// contiguously as affine points in and ; + /// pairs where either point is at infinity must be filtered out by the caller. + /// Substantially faster than multiplying separate + /// results when > 1. + /// + public readonly unsafe PT MillerLoopN(scoped ReadOnlySpan qAffines, scoped ReadOnlySpan pAffines, int npairs) + { + ArgumentOutOfRangeException.ThrowIfNegative(npairs); + if (qAffines.Length < npairs * P2Affine.Sz) + { + throw new ArgumentException($"Insufficient points for the given count. Expected {npairs * P2Affine.Sz} longs.", nameof(qAffines)); + } + if (pAffines.Length < npairs * P1Affine.Sz) + { + throw new ArgumentException($"Insufficient points for the given count. Expected {npairs * P1Affine.Sz} longs.", nameof(pAffines)); + } + + // blst does not write the output for an empty batch; the empty product is one + if (npairs == 0) + { + One(_fp12); + return this; + } + + // a [ptr, null] argument tells blst to read all points from one contiguous buffer + fixed (long* q = qAffines) + fixed (long* p = pAffines) + { + long** qs = stackalloc long*[2] { q, null }; + long** ps = stackalloc long*[2] { p, null }; + blst_miller_loop_n(_fp12, qs, ps, (size_t)npairs); + } + return this; + } } [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial size_t blst_pairing_sizeof(); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial size_t blst_pairing_sizeof(); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_pairing_init(Span ctx, [MarshalAs(UnmanagedType.Bool)] bool hash_or_encode, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_pairing_init(Span ctx, [MarshalAs(UnmanagedType.Bool)] bool hash_or_encode, ref long dst, size_t dst_len); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_pairing_commit(Span ctx); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_pairing_commit(Span ctx); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial ERROR blst_pairing_merge(Span ctx, ReadOnlySpan ctx1); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial ERROR blst_pairing_merge(Span ctx, ReadOnlySpan ctx1); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] [return: MarshalAs(UnmanagedType.Bool)] - static private partial bool blst_pairing_finalverify(ReadOnlySpan ctx, ReadOnlySpan sig); + private static partial bool blst_pairing_finalverify(ReadOnlySpan ctx, ReadOnlySpan sig); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial void blst_pairing_raw_aggregate(Span ctx, ReadOnlySpan q, + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial void blst_pairing_raw_aggregate(Span ctx, ReadOnlySpan q, ReadOnlySpan p); [LibraryImport(LibraryName)] - [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] - static private partial IntPtr blst_pairing_as_fp12(ReadOnlySpan ctx); + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + private static partial nint blst_pairing_as_fp12(ReadOnlySpan ctx); public readonly ref struct Pairing { @@ -1596,6 +1761,11 @@ public readonly ref struct Pairing private readonly Span _ctx; private static readonly int _sz = (int)blst_pairing_sizeof() / sizeof(long); + // without this, new Pairing() would skip the constructor below and produce an instance + // backed by a null buffer, crashing the process on first use + public Pairing() : this(false) + { } + public Pairing(bool hashOrEncode = false, scoped ReadOnlySpan DST = default) { int dst_len = DST.Length; @@ -1604,6 +1774,10 @@ public Pairing(bool hashOrEncode = false, scoped ReadOnlySpan DST = defaul Span dst = new byte[add_len * sizeof(long)]; DST.CopyTo(dst); + // The DST is stored in the tail of the context array, immediately after the context + // struct. blst detects a DST placed at ctx + sizeof(ctx) and re-derives its address + // from the context pointer on every use, so this stays valid even if the GC moves + // the array between calls. _ctx = new long[Sz + add_len]; for (int i = 0; i < add_len; i++) @@ -1661,6 +1835,13 @@ public readonly void Merge(in Pairing a) public readonly bool FinalVerify(PT sig) { return blst_pairing_finalverify(_ctx, sig.Fp12); } + /// + /// Verifies against the signatures accumulated via the sig arguments of + /// calls. + /// + public readonly bool FinalVerify() + { return blst_pairing_finalverify(_ctx, default); } + public readonly void RawAggregate(P2Affine q, P1Affine p) { blst_pairing_raw_aggregate(_ctx, q.Point, p.Point); } public void RawAggregate(P1Affine p, P2Affine q) @@ -1672,10 +1853,15 @@ public readonly void RawAggregate(P2 q, P1 p) } public void RawAggregate(P1 p, P2 q) => RawAggregate(q, p); - public readonly PT AsFp12() + public readonly unsafe PT AsFp12() { - long[] res = new long[Sz]; - Marshal.Copy(blst_pairing_as_fp12(_ctx), res, 0, Sz); + long[] res = new long[PT.Sz]; + // keep the context pinned while copying: blst_pairing_as_fp12 returns a pointer + // into the context, which would be left dangling if the GC moved the array + fixed (long* ctx = _ctx) + { + new ReadOnlySpan((void*)blst_pairing_as_fp12(_ctx), PT.Sz).CopyTo(res); + } return new(res); } } diff --git a/src/Nethermind.Crypto.Bls/Nethermind.Crypto.Bls.csproj b/src/Nethermind.Crypto.Bls/Nethermind.Crypto.Bls.csproj index 0f99fd9..d1b1ade 100644 --- a/src/Nethermind.Crypto.Bls/Nethermind.Crypto.Bls.csproj +++ b/src/Nethermind.Crypto.Bls/Nethermind.Crypto.Bls.csproj @@ -13,7 +13,7 @@ README.md cryptography elliptic-curve bls git - https://github.com/nethermindeth/bls-bindings + https://github.com/nethermindeth/blst-bindings snupkg diff --git a/src/Nethermind.Crypto.Test/BlsPointTests.cs b/src/Nethermind.Crypto.Test/BlsPointTests.cs new file mode 100644 index 0000000..6bc0bdc --- /dev/null +++ b/src/Nethermind.Crypto.Test/BlsPointTests.cs @@ -0,0 +1,411 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using System; +using System.Numerics; +using NUnit.Framework; + +namespace Nethermind.Crypto.Test; + +using G1 = Bls.P1; +using G2 = Bls.P2; +using G1Affine = Bls.P1Affine; +using G2Affine = Bls.P2Affine; +using GT = Bls.PT; + +public class BlsPointTests +{ + private const string G1GeneratorCompressed = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"; + private const string G2GeneratorCompressed = "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"; + + [Test] + public void GeneratorsMatchKnownEncodings() + { + Assert.Multiple(() => + { + Assert.That(Convert.ToHexStringLower(G1.Generator().Compress()), Is.EqualTo(G1GeneratorCompressed)); + Assert.That(Convert.ToHexStringLower(G1Affine.Generator().Compress()), Is.EqualTo(G1GeneratorCompressed)); + Assert.That(Convert.ToHexStringLower(G2.Generator().Compress()), Is.EqualTo(G2GeneratorCompressed)); + Assert.That(Convert.ToHexStringLower(G2Affine.Generator().Compress()), Is.EqualTo(G2GeneratorCompressed)); + }); + } + + [Test] + public void GeneratorRejectsShortBuffer() + { + Assert.Multiple(() => + { + Assert.That(() => { _ = G1.Generator(new long[G1.Sz - 1]); }, Throws.ArgumentException); + Assert.That(() => { _ = G1Affine.Generator(new long[G1Affine.Sz - 1]); }, Throws.ArgumentException); + Assert.That(() => { _ = G2.Generator(new long[G2.Sz - 1]); }, Throws.ArgumentException); + Assert.That(() => { _ = G2Affine.Generator(new long[G2Affine.Sz - 1]); }, Throws.ArgumentException); + Assert.That(() => { _ = GT.One(new long[GT.Sz - 1]); }, Throws.ArgumentException); + }); + } + + [Test] + public void SerializeRoundtripG1() + { + G1 p = G1.Generator().Mult(12345); + + G1 fromSerialized = new(p.Serialize()); + Assert.That(fromSerialized.IsEqual(p)); + + G1 fromCompressed = new(p.Compress()); + Assert.That(fromCompressed.IsEqual(p)); + + G1Affine affine = p.ToAffine(); + Assert.That(affine.Serialize(), Is.EqualTo(p.Serialize())); + Assert.That(affine.Compress(), Is.EqualTo(p.Compress())); + Assert.That(new G1Affine(p.Serialize()).IsEqual(affine)); + Assert.That(new G1Affine(p.Compress()).IsEqual(affine)); + } + + [Test] + public void SerializeRoundtripG2() + { + G2 p = G2.Generator().Mult(12345); + + G2 fromSerialized = new(p.Serialize()); + Assert.That(fromSerialized.IsEqual(p)); + + G2 fromCompressed = new(p.Compress()); + Assert.That(fromCompressed.IsEqual(p)); + + G2Affine affine = p.ToAffine(); + Assert.That(affine.Serialize(), Is.EqualTo(p.Serialize())); + Assert.That(affine.Compress(), Is.EqualTo(p.Compress())); + Assert.That(new G2Affine(p.Serialize()).IsEqual(affine)); + Assert.That(new G2Affine(p.Compress()).IsEqual(affine)); + } + + [Test] + public void InfinityRoundtrip() + { + byte[] compressedInf = new byte[48]; + compressedInf[0] = 0xc0; + + G1Affine p = new(compressedInf); + Assert.That(p.IsInf()); + Assert.That(p.Compress(), Is.EqualTo(compressedInf)); + + byte[] serializedInf = p.Serialize(); + Assert.That(serializedInf[0], Is.EqualTo(0x40)); + Assert.That(serializedInf[1..], Has.All.EqualTo(0)); + } + + [TestCase("00", Bls.ERROR.BADENCODING, TestName = "AffineDecodeErrors(wrong length)")] + [TestCase("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + Bls.ERROR.BADENCODING, TestName = "AffineDecodeErrors(non-canonical x)")] + [TestCase("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", + Bls.ERROR.POINTNOTINGROUP, TestName = "AffineDecodeErrors(on curve outside subgroup)")] + public void AffineDecodeErrors(string hex, Bls.ERROR expected) + { + byte[] encoded = Convert.FromHexString(hex); + + G1Affine p = new(); + Assert.That(p.TryDecode(encoded, out Bls.ERROR err), Is.False); + Assert.That(err, Is.EqualTo(expected)); + Assert.That( + () => { _ = new G1Affine(encoded); }, + Throws.TypeOf().With.Property("Error").EqualTo(expected)); + } + + [Test] + public void UnvalidatedDecodeAllowsPointOutsideSubgroup() + { + // (0, 2) is on the curve but not in the subgroup; the Jacobian decode is unvalidated by design + byte[] outsideSubgroup = new byte[96]; + outsideSubgroup[95] = 0x02; + + G1 p = new(outsideSubgroup); + Assert.That(p.OnCurve()); + Assert.That(p.InGroup(), Is.False); + } + + [Test] + public void MapToRejectsWrongLength() + { + Assert.Multiple(() => + { + Assert.That(() => { _ = new G1().MapTo(new byte[47]); }, Throws.ArgumentException); + Assert.That(() => { _ = new G2().MapTo(new byte[47], new byte[48]); }, Throws.ArgumentException); + Assert.That(() => { _ = new G2().MapTo(new byte[48], new byte[47]); }, Throws.ArgumentException); + }); + } + + [Test] + public void MultIdentities() + { + G1 g = G1.Generator(); + + Assert.That(g.Dup().Mult(BigInteger.One).IsEqual(g)); + Assert.That(g.Dup().Mult(BigInteger.Zero).IsInf()); + Assert.That(g.Dup().Mult(2).IsEqual(g.Dup().Dbl())); + Assert.That(g.Dup().Mult(3).IsEqual(g.Dup().Add(g).Add(g))); + Assert.That(g.Dup().Mult(BigInteger.MinusOne).IsEqual(g.Dup().Neg())); + Assert.That(g.Dup().Add(g.Dup().Neg()).IsInf()); + Assert.That(g.Dup().Cneg(false).IsEqual(g)); + } + + [Test] + public void MultAgreesAcrossScalarRepresentations() + { + BigInteger k = BigInteger.Parse("31415926535897932384626433832795028841971693993751058209749445923"); + byte[] bendian = k.ToByteArray(isUnsigned: true, isBigEndian: true); + + Bls.Scalar scalar = new(bendian, Bls.ByteOrder.BigEndian); + + G1 viaBigInteger = G1.Generator().Mult(k); + G1 viaScalar = G1.Generator().Mult(scalar); + G1 viaBytes = G1.Generator().Mult(scalar.ToLendian()); + + Assert.That(viaScalar.IsEqual(viaBigInteger)); + Assert.That(viaBytes.IsEqual(viaBigInteger)); + } + + [Test] + public void ScalarArithmetic() + { + Bls.Scalar a = new(BigInteger.Parse("123456789123456789").ToByteArray(isUnsigned: true, isBigEndian: true), Bls.ByteOrder.BigEndian); + Bls.Scalar b = new(BigInteger.Parse("987654321987654321").ToByteArray(isUnsigned: true, isBigEndian: true), Bls.ByteOrder.BigEndian); + + Assert.That((a + b - b).ToBendian(), Is.EqualTo(a.ToBendian())); + Assert.That((a * b / b).ToBendian(), Is.EqualTo(a.ToBendian())); + + Bls.Scalar one = a / a; + byte[] expectedOne = new byte[32]; + expectedOne[31] = 1; + Assert.That(one.ToBendian(), Is.EqualTo(expectedOne)); + + // multiplication in the scalar field matches multiplication on the curve + Assert.That(G1.Generator().Mult(a * b).IsEqual(G1.Generator().Mult(a).Mult(b))); + } + + [Test] + public void ScalarReducesModOrder() + { + // the group order r reduces to zero + byte[] r = Convert.FromHexString("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"); + Bls.Scalar scalar = new(r, Bls.ByteOrder.BigEndian); + Assert.That(scalar.ToBendian(), Has.All.EqualTo(0)); + } + + [TestCase("", + "052926add2207b76ca4fa57a8734416c8dc95e24501772c814278700eed6d1e4e8cf62d9c09db0fac349612b759e79a1" + + "08ba738453bfed09cb546dbb0783dbb3a5f1f566ed67bb6be0e8c67e2e81a4cc68ee29813bb7994998f3eae0c9c6a265")] + [TestCase("abc", + "03567bc5ef9c690c2ab2ecdf6a96ef1c139cc0b2f284dca0a9a7943388a49a3aee664ba5379a7655d3c68900be2f6903" + + "0b9c15f3fe6e5cf4211f346271d7b01c8f3b28be689c8429c85b67af215533311f0b8dfaaa154fa6b88176c229f2885d")] + public void HashToG1MatchesRfc9380TestVectors(string msg, string expected) + { + // RFC 9380 J.9.1 (BLS12381G1_XMD:SHA-256_SSWU_RO_) + byte[] dst = "QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"u8.ToArray(); + + G1 p = new G1().HashTo(System.Text.Encoding.ASCII.GetBytes(msg), dst); + Assert.That(Convert.ToHexStringLower(p.Serialize()), Is.EqualTo(expected)); + } + + [TestCase("", + "05cb8437535e20ecffaef7752baddf98034139c38452458baeefab379ba13dff5bf5dd71b72418717047f5b0f37da03d" + + "0141ebfbdca40eb85b87142e130ab689c673cf60f1a3e98d69335266f30d9b8d4ac44c1038e9dcdd5393faf5c41fb78a" + + "12424ac32561493f3fe3c260708a12b7c620e7be00099a974e259ddc7d1f6395c3c811cdd19f1e8dbf3e9ecfdcbab8d6" + + "0503921d7f6a12805e72940b963c0cf3471c7b2a524950ca195d11062ee75ec076daf2d4bc358c4b190c0c98064fdd92")] + [TestCase("abc", + "139cddbccdc5e91b9623efd38c49f81a6f83f175e80b06fc374de9eb4b41dfe4ca3a230ed250fbe3a2acf73a41177fd8" + + "02c2d18e033b960562aae3cab37a27ce00d80ccd5ba4b7fe0e7a210245129dbec7780ccc7954725f4168aff2787776e6" + + "00aa65dae3c8d732d10ecd2c50f8a1baf3001578f71c694e03866e9f3d49ac1e1ce70dd94a733534f106d4cec0eddd16" + + "1787327b68159716a37440985269cf584bcb1e621d3a7202be6ea05c4cfe244aeb197642555a0645fb87bf7466b2ba48")] + public void HashToG2MatchesRfc9380TestVectors(string msg, string expected) + { + // RFC 9380 J.10.1 (BLS12381G2_XMD:SHA-256_SSWU_RO_); serialized as x.c1 || x.c0 || y.c1 || y.c0 + byte[] dst = "QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_"u8.ToArray(); + + G2 p = new G2().HashTo(System.Text.Encoding.ASCII.GetBytes(msg), dst); + Assert.That(Convert.ToHexStringLower(p.Serialize()), Is.EqualTo(expected)); + } + + [Test] + public void MultiMultMatchesSingleMult() + { + const int npoints = 4; + long[] rawPoints = new long[npoints * G1.Sz]; + byte[] rawScalars = new byte[npoints * 32]; + + G1 expected = new(); + for (int i = 0; i < npoints; i++) + { + BigInteger k = BigInteger.Pow(31, i + 1); + G1 p = G1.Generator(rawPoints.AsSpan(i * G1.Sz)).Mult(i + 2); + expected.Add(p.Dup().Mult(k)); + k.TryWriteBytes(rawScalars.AsSpan(i * 32), out _, isUnsigned: true); + } + + G1 res = new G1().MultiMult(rawPoints, rawScalars, npoints); + Assert.That(res.IsEqual(expected)); + } + + [Test] + public void MultiMultWithNoPointsIsInfinity() + { + G1 g1 = G1.Generator().MultiMult([], [], 0); + Assert.That(g1.IsInf()); + + G2 g2 = G2.Generator().MultiMult([], [], 0); + Assert.That(g2.IsInf()); + } + + [Test] + public void MultiMultRejectsBadArguments() + { + Assert.Multiple(() => + { + Assert.That(() => { _ = new G1().MultiMult([], [], -1); }, Throws.TypeOf()); + Assert.That(() => { _ = new G1().MultiMult(new long[G1.Sz], new byte[64], 2); }, Throws.ArgumentException); + Assert.That(() => { _ = new G1().MultiMult(new long[2 * G1.Sz], new byte[32], 2); }, Throws.ArgumentException); + Assert.That(() => { _ = new G2().MultiMult([], [], -1); }, Throws.TypeOf()); + Assert.That(() => { _ = new G2().MultiMult(new long[G2.Sz], new byte[64], 2); }, Throws.ArgumentException); + Assert.That(() => { _ = new G2().MultiMult(new long[2 * G2.Sz], new byte[32], 2); }, Throws.ArgumentException); + }); + } + + [Test] + public void GtOperations() + { + GT one = GT.One(); + Assert.That(one.IsOne()); + + GT q = new(G1Affine.Generator(), G2Affine.Generator()); + // a raw Miller loop output only lands in the r-order subgroup after the final exponentiation + Assert.That(q.InGroup(), Is.False); + Assert.That(q.Dup().FinalExp().InGroup()); + Assert.That(q.Dup().Sqr().IsEqual(q.Dup().Mul(q))); + Assert.That(q.Dup().Mul(one).IsEqual(q)); + Assert.That(q.ToBendian(), Has.Length.EqualTo(576)); + } + + [Test] + public void PairingAsFp12MatchesMillerLoop() + { + Bls.Pairing ctx = new(); + ctx.RawAggregate(G2Affine.Generator(), G1Affine.Generator()); + ctx.Commit(); + + GT direct = new(G2Affine.Generator(), G1Affine.Generator()); + Assert.That(ctx.AsFp12().IsEqual(direct)); + } + + [Test] + public void PairingIsBilinear() + { + // e(a*g1, b*g2) == e(b*g1, a*g2) + GT lhs = new(G1.Generator().Mult(1234).ToAffine(), G2.Generator().Mult(5678).ToAffine()); + GT rhs = new(G1.Generator().Mult(5678).ToAffine(), G2.Generator().Mult(1234).ToAffine()); + Assert.That(GT.FinalVerify(lhs, rhs)); + } + + [Test] + public void AffineRawDecodeMatchesValidatedDecode() + { + G1Affine g1 = new(G1.Generator().Mult(424242)); + byte[] g1Serialized = g1.Serialize(); + G1Affine g1Raw = new(); + g1Raw.Decode(g1Serialized.AsSpan(0, 48), g1Serialized.AsSpan(48)); + Assert.That(g1Raw.IsEqual(g1)); + Assert.That(g1Raw.OnCurve()); + + // serialized as x.c1 || x.c0 || y.c1 || y.c0; Decode takes x.c0, x.c1, y.c0, y.c1 + G2Affine g2 = new(G2.Generator().Mult(424242)); + byte[] g2Serialized = g2.Serialize(); + G2Affine g2Raw = new(); + g2Raw.Decode(g2Serialized.AsSpan(48, 48), g2Serialized.AsSpan(0, 48), g2Serialized.AsSpan(144, 48), g2Serialized.AsSpan(96, 48)); + Assert.That(g2Raw.IsEqual(g2)); + Assert.That(g2Raw.OnCurve()); + } + + [Test] + public void AffineRawDecodeRejectsWrongLength() + { + Assert.Multiple(() => + { + Assert.That(() => new G1Affine().Decode(new byte[47], new byte[48]), Throws.ArgumentException); + Assert.That(() => new G2Affine().Decode(new byte[48], new byte[48], new byte[48], new byte[47]), Throws.ArgumentException); + }); + } + + [Test] + public void MillerLoopNMatchesSequentialProduct() + { + const int npairs = 3; + long[] qAffines = new long[npairs * G2Affine.Sz]; + long[] pAffines = new long[npairs * G1Affine.Sz]; + + GT sequential = GT.One(); + for (int i = 0; i < npairs; i++) + { + G1Affine p = new(G1.Generator().Mult(1234 + i)); + G2Affine q = new(G2.Generator().Mult(5678 + i)); + p.Point.CopyTo(pAffines.AsSpan(i * G1Affine.Sz)); + q.Point.CopyTo(qAffines.AsSpan(i * G2Affine.Sz)); + sequential.Mul(new GT(q, p)); + } + + GT batched = new(new long[GT.Sz]); + batched.MillerLoopN(qAffines, pAffines, npairs); + Assert.That(batched.IsEqual(sequential)); + } + + [Test] + public void MillerLoopNWithNoPairsIsOne() + { + GT res = new(new long[GT.Sz]); + res.MillerLoopN([], [], 0); + Assert.That(res.IsOne()); + } + + [Test] + public void MillerLoopNRejectsBadArguments() + { + Assert.Multiple(() => + { + Assert.That(() => { _ = new GT(new long[GT.Sz]).MillerLoopN([], [], -1); }, Throws.TypeOf()); + Assert.That(() => { _ = new GT(new long[GT.Sz]).MillerLoopN(new long[G2Affine.Sz], new long[G1Affine.Sz], 2); }, Throws.ArgumentException); + Assert.That(() => { _ = new GT(new long[GT.Sz]).MillerLoopN(new long[2 * G2Affine.Sz], new long[G1Affine.Sz], 2); }, Throws.ArgumentException); + }); + } + + [Test] + public void MultiMultAffineMatchesMultiMult() + { + const int npoints = 4; + long[] rawPoints = new long[npoints * G1.Sz]; + long[] rawAffines = new long[npoints * G1Affine.Sz]; + long[] rawPointsG2 = new long[npoints * G2.Sz]; + long[] rawAffinesG2 = new long[npoints * G2Affine.Sz]; + byte[] rawScalars = new byte[npoints * 32]; + + for (int i = 0; i < npoints; i++) + { + G1 p = G1.Generator(rawPoints.AsSpan(i * G1.Sz)).Mult(i + 2); + new G1Affine(p).Point.CopyTo(rawAffines.AsSpan(i * G1Affine.Sz)); + G2 q = G2.Generator(rawPointsG2.AsSpan(i * G2.Sz)).Mult(i + 2); + new G2Affine(q).Point.CopyTo(rawAffinesG2.AsSpan(i * G2Affine.Sz)); + BigInteger.Pow(31, i + 1).TryWriteBytes(rawScalars.AsSpan(i * 32), out _, isUnsigned: true); + } + + G1 viaJacobian = new G1().MultiMult(rawPoints, rawScalars, npoints); + G1 viaAffine = new G1().MultiMultAffine(rawAffines, rawScalars, npoints); + Assert.That(viaAffine.IsEqual(viaJacobian)); + + G2 viaJacobianG2 = new G2().MultiMult(rawPointsG2, rawScalars, npoints); + G2 viaAffineG2 = new G2().MultiMultAffine(rawAffinesG2, rawScalars, npoints); + Assert.That(viaAffineG2.IsEqual(viaJacobianG2)); + } + + [Test] + public void MultiMultAffineWithNoPointsIsInfinity() + { + Assert.That(G1.Generator().MultiMultAffine([], [], 0).IsInf()); + Assert.That(G2.Generator().MultiMultAffine([], [], 0).IsInf()); + } +} diff --git a/src/Nethermind.Crypto.Test/BlsSignatureTests.cs b/src/Nethermind.Crypto.Test/BlsSignatureTests.cs new file mode 100644 index 0000000..1176358 --- /dev/null +++ b/src/Nethermind.Crypto.Test/BlsSignatureTests.cs @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: MIT + +using System; +using System.Numerics; +using NUnit.Framework; + +namespace Nethermind.Crypto.Test; + +using G1 = Bls.P1; +using G2 = Bls.P2; +using G1Affine = Bls.P1Affine; +using GT = Bls.PT; + +public class BlsSignatureTests +{ + // ciphersuite for "minimal pubkey size" BLS signatures (pk in G1, sig in G2) + private static readonly byte[] Dst = "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"u8.ToArray(); + + private static readonly byte[] Ikm = Convert.FromHexString("263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3"); + + [Test] + public void KeygenIsDeterministic() + { + Bls.SecretKey a = new(Ikm, ""); + Bls.SecretKey b = new(Ikm, ""); + Assert.That(a.ToBendian(), Is.EqualTo(b.ToBendian())); + Assert.That(a.ToBendian(), Has.Some.Not.EqualTo(0)); + } + + [Test] + public void KeygenInfoIsUsed() + { + Bls.SecretKey a = new(Ikm, ""); + Bls.SecretKey b = new(Ikm, "info"); + Assert.That(a.ToBendian(), Is.Not.EqualTo(b.ToBendian())); + } + + [Test] + public void KeygenRejectsShortKeyMaterial() + { + // blst silently produces a zero key for short IKM, which must not be exposed + byte[] shortIkm = new byte[31]; + Assert.Multiple(() => + { + Assert.That(() => new Bls.SecretKey().Keygen(shortIkm), Throws.ArgumentException); + Assert.That(() => new Bls.SecretKey().KeygenV3(shortIkm), Throws.ArgumentException); + Assert.That(() => new Bls.SecretKey().KeygenV45(shortIkm, "salt"), Throws.ArgumentException); + Assert.That(() => new Bls.SecretKey().KeygenV5(shortIkm, "salt"), Throws.ArgumentException); + Assert.That(() => new Bls.SecretKey().DeriveMasterEip2333(shortIkm), Throws.ArgumentException); + }); + } + + [Test] + public void SecretKeyEncodingRoundtrip() + { + byte[] bendian = new byte[32]; + bendian[31] = 0x2a; + bendian[0] = 0x2a; + + Bls.SecretKey sk = new(bendian, Bls.ByteOrder.BigEndian); + Assert.That(sk.ToBendian(), Is.EqualTo(bendian)); + + byte[] lendian = sk.ToLendian(); + Array.Reverse(lendian); + Assert.That(lendian, Is.EqualTo(bendian)); + + Bls.SecretKey fromLendian = new(sk.ToLendian(), Bls.ByteOrder.LittleEndian); + Assert.That(fromLendian.ToBendian(), Is.EqualTo(bendian)); + } + + [TestCase("0000000000000000000000000000000000000000000000000000000000000000")] // zero + [TestCase("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001")] // group order r + [TestCase("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")] // > r + public void SecretKeyRejectsOutOfRangeValues(string hex) + { + byte[] encoded = Convert.FromHexString(hex); + Assert.That( + () => { Bls.SecretKey sk = new(encoded, Bls.ByteOrder.BigEndian); }, + Throws.TypeOf().With.Property("Error").EqualTo(Bls.ERROR.BADENCODING)); + } + + [Test] + public void SecretKeyAcceptsMaxValidValue() + { + // r - 1 is the largest valid secret key + byte[] rMinusOne = Convert.FromHexString("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000"); + Bls.SecretKey sk = new(rMinusOne, Bls.ByteOrder.BigEndian); + Assert.That(sk.ToBendian(), Is.EqualTo(rMinusOne)); + } + + [Test] + public void Eip2333DeriveMasterAndChild() + { + // test case 0 from EIP-2333 + byte[] seed = Convert.FromHexString("c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"); + byte[] expectedMaster = ToBendian32("6083874454709270928345386274498605044986640685124978867557563392430687146096"); + byte[] expectedChild = ToBendian32("20397789859736650942317412262472558107875392172444076792671091975210932703118"); + + Bls.SecretKey master = new(); + master.DeriveMasterEip2333(seed); + Assert.That(master.ToBendian(), Is.EqualTo(expectedMaster)); + + Bls.SecretKey child = new(master, 0); + Assert.That(child.ToBendian(), Is.EqualTo(expectedChild)); + } + + [Test] + public void SignAndVerify() + { + byte[] msg = "message to sign"u8.ToArray(); + + Bls.SecretKey sk = new(Ikm, ""); + G1 pk = new(sk); + G2 sig = new G2().HashTo(msg, Dst).SignWith(sk); + + Assert.That(Verify(pk, sig, msg), Is.True); + } + + [Test] + public void VerifyRejectsWrongMessage() + { + byte[] msg = "message to sign"u8.ToArray(); + byte[] wrongMsg = "some other message"u8.ToArray(); + + Bls.SecretKey sk = new(Ikm, ""); + G1 pk = new(sk); + G2 sig = new G2().HashTo(msg, Dst).SignWith(sk); + + Assert.That(Verify(pk, sig, wrongMsg), Is.False); + } + + [Test] + public void VerifyRejectsWrongKey() + { + byte[] msg = "message to sign"u8.ToArray(); + + Bls.SecretKey sk = new(Ikm, ""); + Bls.SecretKey wrongSk = new(Ikm, "different"); + G1 wrongPk = new(wrongSk); + G2 sig = new G2().HashTo(msg, Dst).SignWith(sk); + + Assert.That(Verify(wrongPk, sig, msg), Is.False); + } + + [Test] + public void PairingVerifiesSignature() + { + byte[] msg = "message to sign"u8.ToArray(); + + Bls.SecretKey sk = new(Ikm, ""); + G1 pk = new(sk); + G2 sig = new G2().HashTo(msg, Dst).SignWith(sk); + + Bls.Pairing ctx = new(true, Dst); + Bls.ERROR err = ctx.Aggregate(pk.ToAffine(), sig.ToAffine(), msg); + Assert.That(err, Is.EqualTo(Bls.ERROR.SUCCESS)); + ctx.Commit(); + Assert.That(ctx.FinalVerify(), Is.True); + } + + [Test] + public void PairingRejectsInvalidSignature() + { + byte[] msg = "message to sign"u8.ToArray(); + + Bls.SecretKey sk = new(Ikm, ""); + G1 pk = new(sk); + G2 sig = new G2().HashTo("some other message"u8, Dst).SignWith(sk); + + Bls.Pairing ctx = new(true, Dst); + Bls.ERROR err = ctx.Aggregate(pk.ToAffine(), sig.ToAffine(), msg); + Assert.That(err, Is.EqualTo(Bls.ERROR.SUCCESS)); + ctx.Commit(); + Assert.That(ctx.FinalVerify(), Is.False); + } + + [Test] + public void PairingVerifiesMultipleSignatures() + { + Bls.Pairing ctx = new(true, Dst); + + for (int i = 0; i < 3; i++) + { + byte[] msg = [(byte)i, 0xab, 0xcd]; + Bls.SecretKey sk = new(Ikm, $"signer{i}"); + G1 pk = new(sk); + G2 sig = new G2().HashTo(msg, Dst).SignWith(sk); + + Bls.ERROR err = ctx.Aggregate(pk.ToAffine(), sig.ToAffine(), msg); + Assert.That(err, Is.EqualTo(Bls.ERROR.SUCCESS)); + } + + ctx.Commit(); + Assert.That(ctx.FinalVerify(), Is.True); + } + + [Test] + public void PairingRejectsInfinitePublicKey() + { + byte[] msg = "message to sign"u8.ToArray(); + + Bls.SecretKey sk = new(Ikm, ""); + G2 sig = new G2().HashTo(msg, Dst).SignWith(sk); + + Bls.Pairing ctx = new(true, Dst); + Bls.ERROR err = ctx.Aggregate(new G1Affine(), sig.ToAffine(), msg); + Assert.That(err, Is.EqualTo(Bls.ERROR.PKISINFINITY)); + } + + [Test] + public void AggregatedSignatureVerifies() + { + byte[] msg = "message to sign"u8.ToArray(); + + // aggregate three signatures over the same message and verify against the sum of the keys + G1 aggPk = new(); + G2 aggSig = new(); + for (int i = 0; i < 3; i++) + { + Bls.SecretKey sk = new(Ikm, $"signer{i}"); + aggPk.Aggregate(new G1(sk).ToAffine()); + aggSig.Aggregate(new G2().HashTo(msg, Dst).SignWith(sk).ToAffine()); + } + + Assert.That(Verify(aggPk, aggSig, msg), Is.True); + } + + [Test] + public void AggregateRejectsPointOutsideSubgroup() + { + // (0, 2) is on the G1 curve but not in the subgroup + byte[] bytes = new byte[96]; + bytes[95] = 0x02; + + Assert.That( + () => + { + G1 point = new(bytes); + new G1().Aggregate(point.ToAffine()); + }, + Throws.TypeOf().With.Property("Error").EqualTo(Bls.ERROR.POINTNOTINGROUP)); + } + + // e(pk, H(msg)) == e(g1, sig) + private static bool Verify(G1 pk, G2 sig, ReadOnlySpan msg) + { + G2 hash = new G2().HashTo(msg, Dst); + GT lhs = new(pk.ToAffine(), hash.ToAffine()); + GT rhs = new(G1Affine.Generator(), sig.ToAffine()); + return GT.FinalVerify(lhs, rhs); + } + + private static byte[] ToBendian32(string dec) + { + byte[] bytes = BigInteger.Parse(dec).ToByteArray(isUnsigned: true, isBigEndian: true); + byte[] padded = new byte[32]; + bytes.CopyTo(padded, 32 - bytes.Length); + return padded; + } +}