From 3188ee3aa59a03adecf3f0ca14528e971e6a7db0 Mon Sep 17 00:00:00 2001 From: jimmy Date: Mon, 22 Sep 2025 11:46:57 +0800 Subject: [PATCH 1/3] Add bn254 native contract bindings and tests --- src/Neo/Cryptography/BN254.cs | 277 ++++++++++++++++++ src/Neo/Neo.csproj | 1 + .../SmartContract/Native/CryptoLib.BN254.cs | 49 ++++ .../SmartContract/Native/UT_CryptoLib.cs | 134 +++++++++ .../SmartContract/Native/UT_NativeContract.cs | 2 +- 5 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 src/Neo/Cryptography/BN254.cs create mode 100644 src/Neo/SmartContract/Native/CryptoLib.BN254.cs diff --git a/src/Neo/Cryptography/BN254.cs b/src/Neo/Cryptography/BN254.cs new file mode 100644 index 0000000000..f6cececb77 --- /dev/null +++ b/src/Neo/Cryptography/BN254.cs @@ -0,0 +1,277 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BN254.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Nethermind.MclBindings; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Neo.Cryptography +{ + public static class BN254 + { + public const int FieldElementLength = 32; + public const int G1EncodedLength = 64; + public const int PairInputLength = 192; + + private static readonly object s_sync = new(); + private static bool s_initialized; + + public static byte[] Add(ReadOnlySpan input) + { + EnsureInitialized(); + + if (!TryDeserializeG1(input[..G1EncodedLength], out var first)) + return new byte[G1EncodedLength]; + + if (!TryDeserializeG1(input[G1EncodedLength..], out var second)) + return new byte[G1EncodedLength]; + + mclBnG1 result = default; + Mcl.mclBnG1_add(ref result, first, second); + Mcl.mclBnG1_normalize(ref result, result); + + return SerializeG1(result); + } + + public static byte[] Mul(ReadOnlySpan input) + { + EnsureInitialized(); + + if (!TryDeserializeG1(input[..G1EncodedLength], out var basePoint)) + return new byte[G1EncodedLength]; + + if (!TryDeserializeScalar(input[G1EncodedLength..], out var scalar)) + return new byte[G1EncodedLength]; + + mclBnG1 result = default; + Mcl.mclBnG1_mul(ref result, basePoint, scalar); + Mcl.mclBnG1_normalize(ref result, result); + + return SerializeG1(result); + } + + public static byte[] Pairing(ReadOnlySpan input) + { + EnsureInitialized(); + + if (input.Length == 0) + return SuccessWord(); + + int pairCount = input.Length / PairInputLength; + bool hasEffectivePair = false; + + mclBnGT accumulator = default; + Mcl.mclBnGT_setInt32(ref accumulator, 1); + + for (int pairIndex = 0; pairIndex < pairCount; pairIndex++) + { + int offset = pairIndex * PairInputLength; + var g1Slice = input.Slice(offset, G1EncodedLength); + var g2Slice = input.Slice(offset + G1EncodedLength, 2 * G1EncodedLength); + + if (!TryDeserializeG1(g1Slice, out var g1)) + return new byte[FieldElementLength]; + + if (!TryDeserializeG2(g2Slice, out var g2)) + return new byte[FieldElementLength]; + + if (Mcl.mclBnG1_isZero(g1) == 1 || Mcl.mclBnG2_isZero(g2) == 1) + continue; + + hasEffectivePair = true; + + mclBnGT current = default; + Mcl.mclBn_pairing(ref current, g1, g2); + + if (Mcl.mclBnGT_isValid(current) == 0) + return new byte[FieldElementLength]; + + mclBnGT temp = accumulator; + Mcl.mclBnGT_mul(ref accumulator, temp, current); + } + + if (!hasEffectivePair) + return SuccessWord(); + + return Mcl.mclBnGT_isOne(accumulator) == 1 ? SuccessWord() : new byte[FieldElementLength]; + } + + private static unsafe bool TryDeserializeG1(ReadOnlySpan encoded, out mclBnG1 point) + { + point = default; + + if (IsAllZero(encoded)) + return true; + + ReadOnlySpan xBytes = encoded[..FieldElementLength]; + fixed (byte* ptr = xBytes) + { + if (Mcl.mclBnFp_setBigEndianMod(ref point.x, (nint)ptr, (nuint)xBytes.Length) != 0) + return false; + } + + ReadOnlySpan yBytes = encoded[FieldElementLength..]; + fixed (byte* ptr = yBytes) + { + if (Mcl.mclBnFp_setBigEndianMod(ref point.y, (nint)ptr, (nuint)yBytes.Length) != 0) + return false; + } + + Mcl.mclBnFp_setInt32(ref point.z, 1); + + return Mcl.mclBnG1_isValid(point) == 1; + } + + private static unsafe bool TryDeserializeScalar(ReadOnlySpan encoded, out mclBnFr scalar) + { + scalar = default; + + if (IsAllZero(encoded)) + { + Mcl.mclBnFr_clear(ref scalar); + return true; + } + + fixed (byte* ptr = encoded) + { + if (Mcl.mclBnFr_setBigEndianMod(ref scalar, (nint)ptr, (nuint)encoded.Length) == -1) + return false; + } + + return Mcl.mclBnFr_isValid(scalar) == 1; + } + + private static unsafe bool TryDeserializeG2(ReadOnlySpan encoded, out mclBnG2 point) + { + point = default; + + if (IsAllZero(encoded)) + return true; + + Span scratch = stackalloc byte[FieldElementLength]; + + var realSegment = encoded.Slice(FieldElementLength, FieldElementLength); + CopyReversed(realSegment, scratch); + fixed (byte* ptr = scratch) + { + if (Mcl.mclBnFp_deserialize(ref point.x.d0, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) + return false; + } + + var imagSegment = encoded[..FieldElementLength]; + CopyReversed(imagSegment, scratch); + fixed (byte* ptr = scratch) + { + if (Mcl.mclBnFp_deserialize(ref point.x.d1, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) + return false; + } + + var yReal = encoded.Slice(3 * FieldElementLength, FieldElementLength); + CopyReversed(yReal, scratch); + fixed (byte* ptr = scratch) + { + if (Mcl.mclBnFp_deserialize(ref point.y.d0, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) + return false; + } + + var yImag = encoded.Slice(2 * FieldElementLength, FieldElementLength); + CopyReversed(yImag, scratch); + fixed (byte* ptr = scratch) + { + if (Mcl.mclBnFp_deserialize(ref point.y.d1, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) + return false; + } + + Mcl.mclBnFp_setInt32(ref point.z.d0, 1); + + return true; + } + + private static unsafe byte[] SerializeG1(in mclBnG1 point) + { + var output = new byte[G1EncodedLength]; + + if (Mcl.mclBnG1_isZero(point) == 1) + return output; + + Span scratch = stackalloc byte[FieldElementLength]; + + fixed (byte* ptr = scratch) + { + if (Mcl.mclBnFp_getLittleEndian((nint)ptr, (nuint)scratch.Length, point.x) == UIntPtr.Zero) + throw new ArgumentException("Failed to serialize BN254 point"); + } + + WriteBigEndian(scratch, output.AsSpan(0, FieldElementLength)); + + fixed (byte* ptr = scratch) + { + if (Mcl.mclBnFp_getLittleEndian((nint)ptr, (nuint)scratch.Length, point.y) == UIntPtr.Zero) + throw new ArgumentException("Failed to serialize BN254 point"); + } + + WriteBigEndian(scratch, output.AsSpan(FieldElementLength, FieldElementLength)); + + return output; + } + + private static byte[] SuccessWord() + { + var output = new byte[FieldElementLength]; + output[^1] = 1; + return output; + } + + private static bool IsAllZero(ReadOnlySpan data) + { + for (int i = 0; i < data.Length; ++i) + { + if (data[i] != 0) + return false; + } + + return true; + } + + private static void WriteBigEndian(ReadOnlySpan littleEndian, Span destination) + { + for (int i = 0; i < littleEndian.Length; ++i) + destination[i] = littleEndian[littleEndian.Length - 1 - i]; + } + + private static void CopyReversed(ReadOnlySpan source, Span destination) + { + for (int i = 0; i < source.Length; ++i) + destination[i] = source[source.Length - 1 - i]; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void EnsureInitialized() + { + if (s_initialized) + return; + + lock (s_sync) + { + if (s_initialized) + return; + + if (Mcl.mclBn_init(Mcl.MCL_BN_SNARK1, Mcl.MCLBN_COMPILED_TIME_VAR) != 0) + throw new InvalidOperationException("BN254 initialization failed"); + + Mcl.mclBn_setETHserialization(1); + + s_initialized = true; + } + } + } +} diff --git a/src/Neo/Neo.csproj b/src/Neo/Neo.csproj index 837011162c..c90c82f610 100644 --- a/src/Neo/Neo.csproj +++ b/src/Neo/Neo.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Neo/SmartContract/Native/CryptoLib.BN254.cs b/src/Neo/SmartContract/Native/CryptoLib.BN254.cs new file mode 100644 index 0000000000..69695d40e3 --- /dev/null +++ b/src/Neo/SmartContract/Native/CryptoLib.BN254.cs @@ -0,0 +1,49 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// CryptoLib.BN254.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography; +using System; + +namespace Neo.SmartContract.Native +{ + partial class CryptoLib + { + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)] + public static byte[] Bn254Add(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length != BN254.G1EncodedLength * 2) + throw new ArgumentException("Invalid BN254 add input length", nameof(input)); + + return BN254.Add(input); + } + + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)] + public static byte[] Bn254Mul(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length != BN254.G1EncodedLength + BN254.FieldElementLength) + throw new ArgumentException("Invalid BN254 mul input length", nameof(input)); + + return BN254.Mul(input); + } + + [ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21)] + public static byte[] Bn254Pairing(byte[] input) + { + ArgumentNullException.ThrowIfNull(input); + if (input.Length % BN254.PairInputLength != 0) + throw new ArgumentException("Invalid BN254 pairing input length", nameof(input)); + + return BN254.Pairing(input); + } + } +} diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index e70b35083d..145b4097e5 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -56,6 +56,21 @@ public class UT_CryptoLib private readonly byte[] g2 = s_g2Hex.HexToBytes(); private readonly byte[] gt = s_gtHex.HexToBytes(); + private const string Bn254G1X = "1"; + private const string Bn254G1Y = "2"; + private const string Bn254G2XIm = "1800deef121f1e7641a819fe67140f7f8f87b140996fbbd1ba87fb145641f404"; + private const string Bn254G2XRe = "198e9393920d483a7260bfb731fb5db382322bc5b47fbf6c80f6321231df581"; + private const string Bn254G2YIm = "12c85ea5db8c6deb43baf7868f1c5341fd8ed84a82f89ed36e80b6a4a8dd22e1"; + private const string Bn254G2YRe = "090689d0585ff0756c27a122072274f89d4d1c6d2f9d3af03d86c6b29b53e2b"; + private const string Bn254DoubleX = "030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3"; + private const string Bn254DoubleY = "15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4"; + private const string Bn254PairingPositive = + "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002203e205db4f19b37b60121b83a7333706db86431c6d835849957ed8c3928ad7927dc7234fd11d3e8c36c59277c3e6f149d5cd3cfa9a62aee49f8130962b4b3b9195e8aa5b7827463722b8c153931579d3505566b4edf48d498e185f0509de15204bb53b8977e5f92a0bc372742c4830944a59b4fe6b1c0466e2a6dad122b5d2e104316c97997c17267a1bb67365523b4388e1306d66ea6e4d8f4a4a4b65f5c7d06e286b49c56f6293b2cea30764f0d5eabe5817905468a41f09b77588f692e8b081070efe3d4913dde35bba2513c426d065dee815c478700cef07180fb6146182432428b1490a4f25053d4c20c8723a73de6f0681bd3a8fca41008a6c3c288252d50f18403272e96c10135f96db0f8d0aec25033ebdffb88d2e7956c9bb198ec072462211ebc0a2f042f993d5bd76caf4adb5e99610dcf7c1d992595e6976aa3"; + private const string Bn254PairingNegative = + "0x142c9123c08a0d7f66d95f3ad637a06b95700bc525073b75610884ef45416e1610104c796f40bfeef3588e996c040d2a88c0b4b85afd2578327b99413c6fe820198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d"; + private const string Bn254PairingInvalidG1 = + "0x00000000000000000000000000000000000000000000000000000000000000000000000000be00be00bebebebebebe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + private readonly byte[] notG1 = "8123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" @@ -1152,6 +1167,103 @@ public void TestVerifyWithEd25519() Assert.IsFalse(CallVerifyWithEd25519(message, invalidPublicKey, signature)); } + [TestMethod] + public void TestBn254Add() + { + byte[] input = new byte[128]; + WriteBn254Field(Bn254G1X, input, 0); + WriteBn254Field(Bn254G1Y, input, 32); + WriteBn254Field(Bn254G1X, input, 64); + WriteBn254Field(Bn254G1Y, input, 96); + + byte[] result = CryptoLib.Bn254Add(input); + + byte[] expected = new byte[64]; + WriteBn254Field(Bn254DoubleX, expected, 0); + WriteBn254Field(Bn254DoubleY, expected, 32); + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public void TestBn254Mul() + { + byte[] input = new byte[96]; + WriteBn254Field(Bn254G1X, input, 0); + WriteBn254Field(Bn254G1Y, input, 32); + WriteBn254Field("2", input, 64); + + byte[] result = CryptoLib.Bn254Mul(input); + + byte[] expected = new byte[64]; + WriteBn254Field(Bn254DoubleX, expected, 0); + WriteBn254Field(Bn254DoubleY, expected, 32); + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public void TestBn254PairingGenerator() + { + byte[] input = new byte[192]; + WriteBn254Field(Bn254G1X, input, 0); + WriteBn254Field(Bn254G1Y, input, 32); + WriteBn254Field(Bn254G2XIm, input, 64); + WriteBn254Field(Bn254G2XRe, input, 96); + WriteBn254Field(Bn254G2YIm, input, 128); + WriteBn254Field(Bn254G2YRe, input, 160); + + byte[] result = CryptoLib.Bn254Pairing(input); + + Assert.IsTrue(result.All(b => b == 0)); + } + + [TestMethod] + public void TestBn254PairingEmpty() + { + byte[] result = CryptoLib.Bn254Pairing(Array.Empty()); + Assert.IsTrue(result.Take(result.Length - 1).All(b => b == 0)); + Assert.AreEqual(1, result[^1]); + } + + [TestMethod] + public void TestBn254PairingVectors() + { + // Vectors sourced from ethereum/tests (MIT) GeneralStateTests/stZeroKnowledge/ecpairing_inputs.json + // commit c67e485ff8b5be9abc8ad15345ec21aa22e290d9 labels 5 (positive), 0 (negative), 38 (invalid_g1_point) + var cases = new (string Hex, bool ExpectedSuccess, string Label)[] + { + (Bn254PairingPositive, true, "positive"), + (Bn254PairingNegative, false, "negative"), + (Bn254PairingInvalidG1, false, "invalid_g1_point"), + }; + + foreach (var (hex, expectedSuccess, label) in cases) + { + byte[] input = HexToBytes(hex); + byte[] result = CryptoLib.Bn254Pairing(input); + + Assert.AreEqual(32, result.Length, label); + if (expectedSuccess) + { + Assert.AreEqual(1, result[^1], label); + Assert.IsTrue(result.Take(result.Length - 1).All(b => b == 0), label); + } + else + { + Assert.IsTrue(result.All(b => b == 0), label); + } + } + } + + [TestMethod] + public void TestBn254InvalidInputs() + { + Assert.ThrowsExactly(() => CryptoLib.Bn254Add(Array.Empty())); + Assert.ThrowsExactly(() => CryptoLib.Bn254Mul(Array.Empty())); + Assert.ThrowsExactly(() => CryptoLib.Bn254Pairing(new byte[1])); + } + private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] signature) { var snapshot = TestBlockchain.GetTestSnapshotCache(); @@ -1174,5 +1286,27 @@ private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] sign return engine.ResultStack.Pop().GetBoolean(); } } + + private static void WriteBn254Field(string hex, byte[] buffer, int offset) + { + var field = Bn254Field(hex); + Buffer.BlockCopy(field, 0, buffer, offset, field.Length); + } + + private static byte[] Bn254Field(string hex) + { + if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + hex = hex[2..]; + if (hex.Length > 64) + throw new ArgumentOutOfRangeException(nameof(hex)); + return hex.PadLeft(64, '0').HexToBytes(); + } + + private static byte[] HexToBytes(string hex) + { + if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + hex = hex[2..]; + return Convert.FromHexString(hex); + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs index 40026d9783..be899373b1 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_NativeContract.cs @@ -42,7 +42,7 @@ public void TestSetup() { {"ContractManagement", """{"id":-1,"updatecounter":0,"hash":"0xfffdc93764dbaddd97c48f252a53ea4643faa3fd","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":3581846399},"manifest":{"name":"ContractManagement","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"deploy","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Array","offset":0,"safe":false},{"name":"deploy","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Array","offset":7,"safe":false},{"name":"destroy","parameters":[],"returntype":"Void","offset":14,"safe":false},{"name":"getContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Array","offset":21,"safe":true},{"name":"getContractById","parameters":[{"name":"id","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getContractHashes","parameters":[],"returntype":"InteropInterface","offset":35,"safe":true},{"name":"getMinimumDeploymentFee","parameters":[],"returntype":"Integer","offset":42,"safe":true},{"name":"hasMethod","parameters":[{"name":"hash","type":"Hash160"},{"name":"method","type":"String"},{"name":"pcount","type":"Integer"}],"returntype":"Boolean","offset":49,"safe":true},{"name":"isContract","parameters":[{"name":"hash","type":"Hash160"}],"returntype":"Boolean","offset":56,"safe":true},{"name":"setMinimumDeploymentFee","parameters":[{"name":"value","type":"Integer"}],"returntype":"Void","offset":63,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"}],"returntype":"Void","offset":70,"safe":false},{"name":"update","parameters":[{"name":"nefFile","type":"ByteArray"},{"name":"manifest","type":"ByteArray"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false}],"events":[{"name":"Deploy","parameters":[{"name":"Hash","type":"Hash160"}]},{"name":"Update","parameters":[{"name":"Hash","type":"Hash160"}]},{"name":"Destroy","parameters":[{"name":"Hash","type":"Hash160"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}""" }, {"StdLib", """{"id":-2,"updatecounter":0,"hash":"0xacce6fd80d44e1796aa0c2c625e9e4e0ce39efc0","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":2426471238},"manifest":{"name":"StdLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"atoi","parameters":[{"name":"value","type":"String"}],"returntype":"Integer","offset":0,"safe":true},{"name":"atoi","parameters":[{"name":"value","type":"String"},{"name":"base","type":"Integer"}],"returntype":"Integer","offset":7,"safe":true},{"name":"base58CheckDecode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":14,"safe":true},{"name":"base58CheckEncode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":21,"safe":true},{"name":"base58Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":28,"safe":true},{"name":"base58Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":35,"safe":true},{"name":"base64Decode","parameters":[{"name":"s","type":"String"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"base64Encode","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"String","offset":49,"safe":true},{"name":"base64UrlDecode","parameters":[{"name":"s","type":"String"}],"returntype":"String","offset":56,"safe":true},{"name":"base64UrlEncode","parameters":[{"name":"data","type":"String"}],"returntype":"String","offset":63,"safe":true},{"name":"deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"Any","offset":70,"safe":true},{"name":"hexDecode","parameters":[{"name":"str","type":"String"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"hexEncode","parameters":[{"name":"bytes","type":"ByteArray"}],"returntype":"String","offset":84,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"}],"returntype":"String","offset":91,"safe":true},{"name":"itoa","parameters":[{"name":"value","type":"Integer"},{"name":"base","type":"Integer"}],"returntype":"String","offset":98,"safe":true},{"name":"jsonDeserialize","parameters":[{"name":"json","type":"ByteArray"}],"returntype":"Any","offset":105,"safe":true},{"name":"jsonSerialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":112,"safe":true},{"name":"memoryCompare","parameters":[{"name":"str1","type":"ByteArray"},{"name":"str2","type":"ByteArray"}],"returntype":"Integer","offset":119,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"}],"returntype":"Integer","offset":126,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"}],"returntype":"Integer","offset":133,"safe":true},{"name":"memorySearch","parameters":[{"name":"mem","type":"ByteArray"},{"name":"value","type":"ByteArray"},{"name":"start","type":"Integer"},{"name":"backward","type":"Boolean"}],"returntype":"Integer","offset":140,"safe":true},{"name":"serialize","parameters":[{"name":"item","type":"Any"}],"returntype":"ByteArray","offset":147,"safe":true},{"name":"strLen","parameters":[{"name":"str","type":"String"}],"returntype":"Integer","offset":154,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"}],"returntype":"Array","offset":161,"safe":true},{"name":"stringSplit","parameters":[{"name":"str","type":"String"},{"name":"separator","type":"String"},{"name":"removeEmptyEntries","type":"Boolean"}],"returntype":"Array","offset":168,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, - {"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":174904780},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":35,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":77,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":84,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, + {"CryptoLib", """{"id":-3,"updatecounter":0,"hash":"0x726cb6e0cd8628a1350a611384688911ab75f51b","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQA==","checksum":1841570703},"manifest":{"name":"CryptoLib","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"bls12381Add","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"InteropInterface","offset":0,"safe":true},{"name":"bls12381Deserialize","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"InteropInterface","offset":7,"safe":true},{"name":"bls12381Equal","parameters":[{"name":"x","type":"InteropInterface"},{"name":"y","type":"InteropInterface"}],"returntype":"Boolean","offset":14,"safe":true},{"name":"bls12381Mul","parameters":[{"name":"x","type":"InteropInterface"},{"name":"mul","type":"ByteArray"},{"name":"neg","type":"Boolean"}],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"bls12381Pairing","parameters":[{"name":"g1","type":"InteropInterface"},{"name":"g2","type":"InteropInterface"}],"returntype":"InteropInterface","offset":28,"safe":true},{"name":"bls12381Serialize","parameters":[{"name":"g","type":"InteropInterface"}],"returntype":"ByteArray","offset":35,"safe":true},{"name":"bn254Add","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":42,"safe":true},{"name":"bn254Mul","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":49,"safe":true},{"name":"bn254Pairing","parameters":[{"name":"input","type":"ByteArray"}],"returntype":"ByteArray","offset":56,"safe":true},{"name":"keccak256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":63,"safe":true},{"name":"murmur32","parameters":[{"name":"data","type":"ByteArray"},{"name":"seed","type":"Integer"}],"returntype":"ByteArray","offset":70,"safe":true},{"name":"recoverSecp256K1","parameters":[{"name":"messageHash","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"ByteArray","offset":77,"safe":true},{"name":"ripemd160","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":84,"safe":true},{"name":"sha256","parameters":[{"name":"data","type":"ByteArray"}],"returntype":"ByteArray","offset":91,"safe":true},{"name":"verifyWithECDsa","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"},{"name":"curveHash","type":"Integer"}],"returntype":"Boolean","offset":98,"safe":true},{"name":"verifyWithEd25519","parameters":[{"name":"message","type":"ByteArray"},{"name":"pubkey","type":"ByteArray"},{"name":"signature","type":"ByteArray"}],"returntype":"Boolean","offset":105,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"LedgerContract", """{"id":-4,"updatecounter":0,"hash":"0xda65b600f7124ce6c79950c1772a36403104f2be","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":1110259869},"manifest":{"name":"LedgerContract","groups":[],"features":{},"supportedstandards":[],"abi":{"methods":[{"name":"currentHash","parameters":[],"returntype":"Hash256","offset":0,"safe":true},{"name":"currentIndex","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getBlock","parameters":[{"name":"indexOrHash","type":"ByteArray"}],"returntype":"Array","offset":14,"safe":true},{"name":"getTransaction","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":21,"safe":true},{"name":"getTransactionFromBlock","parameters":[{"name":"blockIndexOrHash","type":"ByteArray"},{"name":"txIndex","type":"Integer"}],"returntype":"Array","offset":28,"safe":true},{"name":"getTransactionHeight","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":35,"safe":true},{"name":"getTransactionSigners","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Array","offset":42,"safe":true},{"name":"getTransactionVMState","parameters":[{"name":"hash","type":"Hash256"}],"returntype":"Integer","offset":49,"safe":true}],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"NeoToken", """{"id":-5,"updatecounter":0,"hash":"0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0AQQRr3e2dA","checksum":1991619121},"manifest":{"name":"NeoToken","groups":[],"features":{},"supportedstandards":["NEP-17","NEP-27"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"getAccountState","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Array","offset":14,"safe":true},{"name":"getAllCandidates","parameters":[],"returntype":"InteropInterface","offset":21,"safe":true},{"name":"getCandidateVote","parameters":[{"name":"pubKey","type":"PublicKey"}],"returntype":"Integer","offset":28,"safe":true},{"name":"getCandidates","parameters":[],"returntype":"Array","offset":35,"safe":true},{"name":"getCommittee","parameters":[],"returntype":"Array","offset":42,"safe":true},{"name":"getCommitteeAddress","parameters":[],"returntype":"Hash160","offset":49,"safe":true},{"name":"getGasPerBlock","parameters":[],"returntype":"Integer","offset":56,"safe":true},{"name":"getNextBlockValidators","parameters":[],"returntype":"Array","offset":63,"safe":true},{"name":"getRegisterPrice","parameters":[],"returntype":"Integer","offset":70,"safe":true},{"name":"onNEP17Payment","parameters":[{"name":"from","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Void","offset":77,"safe":false},{"name":"registerCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":84,"safe":false},{"name":"setGasPerBlock","parameters":[{"name":"gasPerBlock","type":"Integer"}],"returntype":"Void","offset":91,"safe":false},{"name":"setRegisterPrice","parameters":[{"name":"registerPrice","type":"Integer"}],"returntype":"Void","offset":98,"safe":false},{"name":"symbol","parameters":[],"returntype":"String","offset":105,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":112,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":119,"safe":false},{"name":"unclaimedGas","parameters":[{"name":"account","type":"Hash160"},{"name":"end","type":"Integer"}],"returntype":"Integer","offset":126,"safe":true},{"name":"unregisterCandidate","parameters":[{"name":"pubkey","type":"PublicKey"}],"returntype":"Boolean","offset":133,"safe":false},{"name":"vote","parameters":[{"name":"account","type":"Hash160"},{"name":"voteTo","type":"PublicKey"}],"returntype":"Boolean","offset":140,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]},{"name":"CandidateStateChanged","parameters":[{"name":"pubkey","type":"PublicKey"},{"name":"registered","type":"Boolean"},{"name":"votes","type":"Integer"}]},{"name":"Vote","parameters":[{"name":"account","type":"Hash160"},{"name":"from","type":"PublicKey"},{"name":"to","type":"PublicKey"},{"name":"amount","type":"Integer"}]},{"name":"CommitteeChanged","parameters":[{"name":"old","type":"Array"},{"name":"new","type":"Array"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, {"GasToken", """{"id":-6,"updatecounter":0,"hash":"0xd2a4cff31913016155e38e474a2c06d08be276cf","nef":{"magic":860243278,"compiler":"neo-core-v3.0","source":"","tokens":[],"script":"EEEa93tnQBBBGvd7Z0AQQRr3e2dAEEEa93tnQBBBGvd7Z0A=","checksum":2663858513},"manifest":{"name":"GasToken","groups":[],"features":{},"supportedstandards":["NEP-17"],"abi":{"methods":[{"name":"balanceOf","parameters":[{"name":"account","type":"Hash160"}],"returntype":"Integer","offset":0,"safe":true},{"name":"decimals","parameters":[],"returntype":"Integer","offset":7,"safe":true},{"name":"symbol","parameters":[],"returntype":"String","offset":14,"safe":true},{"name":"totalSupply","parameters":[],"returntype":"Integer","offset":21,"safe":true},{"name":"transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"},{"name":"data","type":"Any"}],"returntype":"Boolean","offset":28,"safe":false}],"events":[{"name":"Transfer","parameters":[{"name":"from","type":"Hash160"},{"name":"to","type":"Hash160"},{"name":"amount","type":"Integer"}]}]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"extra":null}}"""}, From f459beffc47348b9951eb7f8101362bd2719e04f Mon Sep 17 00:00:00 2001 From: Jimmy Date: Fri, 26 Sep 2025 20:47:12 +0800 Subject: [PATCH 2/3] Update src/Neo/Cryptography/BN254.cs Co-authored-by: Will <201105916+Wi1l-B0t@users.noreply.github.com> --- src/Neo/Cryptography/BN254.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo/Cryptography/BN254.cs b/src/Neo/Cryptography/BN254.cs index f6cececb77..e4958b3fab 100644 --- a/src/Neo/Cryptography/BN254.cs +++ b/src/Neo/Cryptography/BN254.cs @@ -23,7 +23,7 @@ public static class BN254 public const int PairInputLength = 192; private static readonly object s_sync = new(); - private static bool s_initialized; + private static volatile bool s_initialized; public static byte[] Add(ReadOnlySpan input) { From 07fab37c31be0016197a0dd423b4be0378a56191 Mon Sep 17 00:00:00 2001 From: jimmy Date: Fri, 24 Oct 2025 01:18:05 +0800 Subject: [PATCH 3/3] Implement managed BN254 arithmetic --- docs/native-contracts-api.md | 3 + src/Neo/Cryptography/BN254.Managed.Curves.cs | 475 ++++++++++++++++++ .../Cryptography/BN254.Managed.Extensions.cs | 326 ++++++++++++ src/Neo/Cryptography/BN254.Managed.Fields.cs | 241 +++++++++ src/Neo/Cryptography/BN254.cs | 232 +++------ src/Neo/Neo.csproj | 1 - .../SmartContract/Native/UT_CryptoLib.cs | 82 ++- 7 files changed, 1200 insertions(+), 160 deletions(-) create mode 100644 src/Neo/Cryptography/BN254.Managed.Curves.cs create mode 100644 src/Neo/Cryptography/BN254.Managed.Extensions.cs create mode 100644 src/Neo/Cryptography/BN254.Managed.Fields.cs diff --git a/docs/native-contracts-api.md b/docs/native-contracts-api.md index d89d261bf3..dc69b5df0e 100644 --- a/docs/native-contracts-api.md +++ b/docs/native-contracts-api.md @@ -79,6 +79,9 @@ When calling a native contract method by transaction script, there are several t | bls12381Add | Add operation of two points. | InteropInterface(*x*), InteropInterface(*y*) | InteropInterface | 1<<19 | 0 | -- | -- | | bls12381Mul | Mul operation of gt point and multiplier | InteropInterface(*x*), Byte[](*mul*), Boolean(*neg*) | InteropInterface | 1<<21 | 0 | -- | -- | | bls12381Pairing | Pairing operation of g1 and g2 | InteropInterface(*g1*), InteropInterface(*g2*) | InteropInterface | 1<<23 | 0 | -- | -- | +| bn254Add | -- | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Gorgon | +| bn254Mul | -- | Byte[](*input*) | Byte[] | 1<<19 | 0 | -- | HF_Gorgon | +| bn254Pairing | -- | Byte[](*input*) | Byte[] | 1<<21 | 0 | -- | HF_Gorgon | | recoverSecp256K1 | Recovers the public key from a secp256k1 signature in a single byte array format. | Byte[](*messageHash*), Byte[](*signature*) | Byte[] | 1<<15 | 0 | -- | HF_Echidna | | ripemd160 | Computes the hash value for the specified byte array using the ripemd160 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | | sha256 | Computes the hash value for the specified byte array using the sha256 algorithm. | Byte[](*data*) | Byte[] | 1<<15 | 0 | -- | -- | diff --git a/src/Neo/Cryptography/BN254.Managed.Curves.cs b/src/Neo/Cryptography/BN254.Managed.Curves.cs new file mode 100644 index 0000000000..3c2a23d493 --- /dev/null +++ b/src/Neo/Cryptography/BN254.Managed.Curves.cs @@ -0,0 +1,475 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BN254.Managed.Curves.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using System; + +namespace Neo.Cryptography +{ + internal static partial class BN254Managed + { + #region CurvePoint (G1) + + internal sealed class CurvePoint + { + public Fp X; + public Fp Y; + public Fp Z; + public Fp T; + + public static CurvePoint Infinity => new CurvePoint(Fp.Zero, Fp.One, Fp.Zero, Fp.Zero); + + private CurvePoint(Fp x, Fp y, Fp z, Fp t) + { + X = x; + Y = y; + Z = z; + T = t; + } + + public CurvePoint() + : this(Fp.Zero, Fp.One, Fp.Zero, Fp.Zero) + { + } + + public static CurvePoint FromG1(ECPoint point) + { + if (point.IsInfinity) + return Infinity.Clone(); + + var affine = point.Normalize(); + return new CurvePoint( + new Fp(affine.AffineXCoord.ToBigInteger()), + new Fp(affine.AffineYCoord.ToBigInteger()), + Fp.One, + Fp.One); + } + + public CurvePoint Clone() => new CurvePoint(X, Y, Z, T); + + public bool IsInfinity => Z.IsZero; + + public void MakeAffine() + { + if (IsInfinity) + return; + + if (Z.Equals(Fp.One)) + { + T = Fp.One; + return; + } + + var zInv = Z.Invert(); + var zInv2 = zInv.Square(); + var zInv3 = zInv2.Multiply(zInv); + + X = X.Multiply(zInv2); + Y = Y.Multiply(zInv3); + Z = Fp.One; + T = Fp.One; + } + } + + #endregion + + #region TwistPoint (G2) + + internal sealed class TwistPoint + { + public static TwistPoint Infinity => new TwistPoint(Fp2.Zero, Fp2.One, Fp2.Zero, Fp2.Zero); + + public Fp2 X; + public Fp2 Y; + public Fp2 Z; + public Fp2 T; + + public bool IsInfinity => Z.IsZero; + + public TwistPoint(Fp2 x, Fp2 y, Fp2 z, Fp2 t) + { + X = x; + Y = y; + Z = z; + T = t; + } + + public TwistPoint() + : this(Fp2.Zero, Fp2.One, Fp2.Zero, Fp2.Zero) + { + } + + public TwistPoint Clone() => new TwistPoint(X, Y, Z, T); + + public TwistPoint Negate() => new TwistPoint(X, Y.Negate(), Z, T); + + public void MakeAffine() + { + if (IsInfinity) + return; + + if (Z.Equals(Fp2.One)) + { + T = Fp2.One; + return; + } + + var zInv = Z.Invert(); + var zInv2 = zInv.Square(); + var zInv3 = zInv2.Multiply(zInv); + + X = X.Multiply(zInv2); + Y = Y.Multiply(zInv3); + Z = Fp2.One; + T = Fp2.One; + } + + public TwistPoint FrobeniusMap1() + { + var x = X.Conjugate().Multiply(XiToPMinus1Over3); + var y = Y.Conjugate().Multiply(XiToPMinus1Over2); + return new TwistPoint(x, y, Fp2.One, Fp2.One); + } + + public TwistPoint FrobeniusMap2() + { + var x = X.Multiply(XiToPSquaredMinus1Over3); + return new TwistPoint(x, Y, Fp2.One, Fp2.One); + } + + public static bool TryParse(ReadOnlySpan bytes, out TwistPoint point) + { + point = Infinity.Clone(); + + if (IsAllZero(bytes)) + { + return true; + } + + if (bytes.Length != FieldElementLength * 4) + return false; + + var xImag = new BigInteger(1, bytes[..FieldElementLength]); + var xReal = new BigInteger(1, bytes[FieldElementLength..(2 * FieldElementLength)]); + var yImag = new BigInteger(1, bytes[(2 * FieldElementLength)..(3 * FieldElementLength)]); + var yReal = new BigInteger(1, bytes[(3 * FieldElementLength)..]); + + var twist = new TwistPoint( + new Fp2(new Fp(xImag), new Fp(xReal)), + new Fp2(new Fp(yImag), new Fp(yReal)), + Fp2.One, + Fp2.One); + + if (!twist.IsOnCurve()) + return false; + + if (!twist.IsOnCurve(true)) + return false; + + point = twist; + return true; + } + + public bool IsOnCurve(bool enforceSubgroup = false) + { + var affine = Clone(); + affine.MakeAffine(); + + var lhs = affine.Y.Square(); + var rhs = affine.X.Square().Multiply(affine.X).Add(TwistB); + if (!lhs.Equals(rhs)) + return false; + + if (!enforceSubgroup) + return true; + + var subgroup = MultiplyScalar(affine, GroupOrder); + return subgroup.IsInfinity; + } + + public static TwistPoint Add(TwistPoint a, TwistPoint b) + { + if (a.IsInfinity) + return b.Clone(); + if (b.IsInfinity) + return a.Clone(); + + var z12 = a.Z.Square(); + var z22 = b.Z.Square(); + + var u1 = a.X.Multiply(z22); + var u2 = b.X.Multiply(z12); + + var t = b.Z.Multiply(z22); + var s1 = a.Y.Multiply(t); + + t = a.Z.Multiply(z12); + var s2 = b.Y.Multiply(t); + + var h = u2.Subtract(u1); + var xEqual = h.IsZero; + + var rTemp = s2.Subtract(s1); + var yEqual = rTemp.IsZero; + + if (xEqual && yEqual) + return Double(a); + + var i = h.Add(h).Square(); + var j = h.Multiply(i); + + var r = rTemp.Add(rTemp); + var v = u1.Multiply(i); + + var x = r.Square().Subtract(j).Subtract(v.Add(v)); + + var z = a.Z.Add(b.Z); + z = z.Square().Subtract(z12).Subtract(z22); + z = z.Multiply(h); + + var y = v.Subtract(x).Multiply(r); + var s1j = s1.Multiply(j); + s1j = s1j.Add(s1j); + y = y.Subtract(s1j); + + return new TwistPoint(x, y, z, z.Square()); + } + + public static TwistPoint Double(TwistPoint a) + { + if (a.IsInfinity) + return a.Clone(); + + var A = a.X.Square(); + var B = a.Y.Square(); + var C = B.Square(); + + var t = a.X.Add(B); + var t2 = t.Square(); + t = t2.Subtract(A); + t2 = t.Subtract(C); + var d = t2.Add(t2); + + var e = A.Add(A); + e = e.Add(A); + var f = e.Square(); + + var X3 = f.Subtract(d.Add(d)); + + var Z3 = a.Y.Multiply(a.Z); + Z3 = Z3.Add(Z3); + + var temp = C.Add(C); + var temp2 = temp.Add(temp); + temp = temp2.Add(temp2); + + var Y3 = d.Subtract(X3); + var tmp = e.Multiply(Y3); + Y3 = tmp.Subtract(temp); + + return new TwistPoint(X3, Y3, Z3, Z3.Square()); + } + + public static TwistPoint MultiplyScalar(TwistPoint point, BigInteger scalar) + { + if (scalar.SignValue <= 0) + return Infinity.Clone(); + + var sum = Infinity.Clone(); + var temp = Infinity.Clone(); + + for (int i = scalar.BitLength; i >= 0; i--) + { + temp = Double(sum); + if (scalar.TestBit(i)) + sum = Add(temp, point); + else + sum = temp; + } + + return sum; + } + } + + #endregion + + #region Line functions and Miller loop + + internal static (Fp2 ell0, Fp2 ell1, Fp2 ell2, TwistPoint next) LineFunctionDouble(TwistPoint r, CurvePoint q) + { + var A = r.X.Square(); + var B = r.Y.Square(); + var C = B.Square(); + + var D = r.X.Add(B); + D = D.Square().Subtract(A).Subtract(C); + D = D.Add(D); + + var E = A.Add(A); + E = E.Add(A); + + var G = E.Square(); + + var outX = G.Subtract(D); + outX = outX.Subtract(D); + + var outZ = r.Y.Add(r.Z); + outZ = outZ.Square().Subtract(B).Subtract(r.T); + + var outY = D.Subtract(outX); + outY = outY.Multiply(E); + var t = C.Add(C); + t = t.Add(t); + t = t.Add(t); + outY = outY.Subtract(t); + + var outT = outZ.Square(); + + var ell1 = E.Multiply(r.T); + ell1 = ell1.Add(ell1); + ell1 = ell1.Negate().Multiply(q.X); + + var ell0 = r.X.Add(E); + ell0 = ell0.Square().Subtract(A).Subtract(G); + var tmp = B.Add(B); + tmp = tmp.Add(tmp); + ell0 = ell0.Subtract(tmp); + + var ell2 = outZ.Multiply(r.T); + ell2 = ell2.Add(ell2).Multiply(q.Y); + + var next = new TwistPoint(outX, outY, outZ, outT); + return (ell0, ell1, ell2, next); + } + + internal static (Fp2 ell0, Fp2 ell1, Fp2 ell2, TwistPoint next) LineFunctionAdd(TwistPoint r, TwistPoint p, CurvePoint q, Fp2 r2) + { + var B = p.X.Multiply(r.T); + + var D = p.Y.Add(r.Z); + D = D.Square().Subtract(r2).Subtract(r.T); + D = D.Multiply(r.T); + + var H = B.Subtract(r.X); + var I = H.Square(); + + var E = I.Add(I); + E = E.Add(E); + + var J = H.Multiply(E); + + var L1 = D.Subtract(r.Y); + L1 = L1.Subtract(r.Y); + + var V = r.X.Multiply(E); + + var outX = L1.Square(); + outX = outX.Subtract(J); + outX = outX.Subtract(V); + outX = outX.Subtract(V); + + var outZ = r.Z.Add(H); + outZ = outZ.Square().Subtract(r.T).Subtract(I); + + var outY = V.Subtract(outX); + outY = outY.Multiply(L1); + var t = r.Y.Multiply(J); + t = t.Add(t); + outY = outY.Subtract(t); + + var outT = outZ.Square(); + + var sum = p.Y.Add(outZ); + sum = sum.Square().Subtract(r2).Subtract(outT); + + var ell0 = L1.Multiply(p.X); + ell0 = ell0.Add(ell0).Subtract(sum); + + var ell2 = outZ.Multiply(q.Y); + ell2 = ell2.Add(ell2); + + var ell1 = L1.Negate().Multiply(q.X); + ell1 = ell1.Add(ell1); + + var next = new TwistPoint(outX, outY, outZ, outT); + return (ell0, ell1, ell2, next); + } + + internal static Fp12 MillerLoop(TwistPoint q, CurvePoint p) + { + var ret = Fp12.One; + + var a = q.Clone(); + a.MakeAffine(); + + var b = p.Clone(); + b.MakeAffine(); + + var minusA = a.Negate(); + var r = a.Clone(); + + var r2 = r.Y.Square(); + + for (int i = SixUPlus2NAF.Length - 1; i > 0; i--) + { + var (ell0, ell1, ell2, newR) = LineFunctionDouble(r, b); + if (i != SixUPlus2NAF.Length - 1) + ret = Fp12.Square(ret); + ret = Fp12.MulLine(ret, ell0, ell1, ell2); + r = newR; + + var naf = SixUPlus2NAF[i - 1]; + if (naf == 0) + continue; + + var addend = naf > 0 ? a : minusA; + (ell0, ell1, ell2, newR) = LineFunctionAdd(r, addend, b, r2); + ret = Fp12.MulLine(ret, ell0, ell1, ell2); + r = newR; + } + + var q1 = a.FrobeniusMap1(); + var q2 = a.FrobeniusMap2(); + + var (e10, e11, e12, newR1) = LineFunctionAdd(r, q1, b, q1.Y.Square()); + ret = Fp12.MulLine(ret, e10, e11, e12); + r = newR1; + + var (e20, e21, e22, _) = LineFunctionAdd(r, q2, b, q2.Y.Square()); + ret = Fp12.MulLine(ret, e20, e21, e22); + + return ret; + } + + internal static Fp12 OptimalAtePairing(TwistPoint q, CurvePoint p) + { + if (q.IsInfinity || p.IsInfinity) + return Fp12.One; + + var miller = MillerLoop(q, p); + return Fp12.FinalExponentiation(miller); + } + + #endregion + + private static bool IsValidFieldElement(BigInteger value) => + value.SignValue >= 0 && value.CompareTo(FieldModulus) < 0; + + private static readonly sbyte[] SixUPlus2NAF = new sbyte[] + { + 0, 0, 0, 1, 0, 1, 0, -1, 0, 0, 1, -1, 0, 0, 1, 0, + 0, 1, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 0, 0, 1, 1, + 1, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, -1, 0, 0, 1, + 1, 0, 0, -1, 0, 0, 0, 1, 1, 0, -1, 0, 0, 1, 0, 1, 1 + }; + } +} diff --git a/src/Neo/Cryptography/BN254.Managed.Extensions.cs b/src/Neo/Cryptography/BN254.Managed.Extensions.cs new file mode 100644 index 0000000000..339bf6152a --- /dev/null +++ b/src/Neo/Cryptography/BN254.Managed.Extensions.cs @@ -0,0 +1,326 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BN254.Managed.Extensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Math; +using System; + +namespace Neo.Cryptography +{ + internal static partial class BN254Managed + { + #region Fp6 + + internal readonly struct Fp6 : IEquatable + { + public Fp2 X { get; } + public Fp2 Y { get; } + public Fp2 Z { get; } + + public static Fp6 Zero => new(Fp2.Zero, Fp2.Zero, Fp2.Zero); + public static Fp6 One => new(Fp2.Zero, Fp2.Zero, Fp2.One); + + public bool IsZero => X.IsZero && Y.IsZero && Z.IsZero; + + public Fp6(Fp2 x, Fp2 y, Fp2 z) + { + X = x; + Y = y; + Z = z; + } + + public Fp6 Add(Fp6 other) => + new(X.Add(other.X), Y.Add(other.Y), Z.Add(other.Z)); + + public Fp6 Subtract(Fp6 other) => + new(X.Subtract(other.X), Y.Subtract(other.Y), Z.Subtract(other.Z)); + + public Fp6 Negate() => new(X.Negate(), Y.Negate(), Z.Negate()); + + public Fp6 Multiply(Fp6 other) + { + var v0 = Z.Multiply(other.Z); + var v1 = Y.Multiply(other.Y); + var v2 = X.Multiply(other.X); + + var tz = X.Add(Y).Multiply(other.X.Add(other.Y)); + tz = tz.Subtract(v1).Subtract(v2); + tz = tz.MultiplyByXi().Add(v0); + + var ty = Y.Add(Z).Multiply(other.Y.Add(other.Z)); + ty = ty.Subtract(v0).Subtract(v1).Add(v2.MultiplyByXi()); + + var tx = X.Add(Z).Multiply(other.X.Add(other.Z)); + tx = tx.Subtract(v0).Add(v1).Subtract(v2); + + return new Fp6(tx, ty, tz); + } + + public Fp6 Square() + { + var v0 = Z.Square(); + var v1 = Y.Square(); + var v2 = X.Square(); + + var c0 = X.Add(Y); + c0 = c0.Square().Subtract(v1).Subtract(v2); + c0 = c0.MultiplyByXi().Add(v0); + + var c1 = Y.Add(Z); + c1 = c1.Square().Subtract(v0).Subtract(v1).Add(v2.MultiplyByXi()); + + var c2 = X.Add(Z); + c2 = c2.Square().Subtract(v0).Add(v1).Subtract(v2); + + return new Fp6(c2, c1, c0); + } + + public Fp6 MulTau() + { + var tz = X.MultiplyByXi(); + return new Fp6(Y, Z, tz); + } + + public Fp6 Multiply(Fp2 scalar) => + new(X.Multiply(scalar), Y.Multiply(scalar), Z.Multiply(scalar)); + + public Fp6 Multiply(Fp scalar) => + new(X.Multiply(scalar), Y.Multiply(scalar), Z.Multiply(scalar)); + + public Fp6 Invert() + { + if (IsZero) + throw new DivideByZeroException(); + + var t1 = X.Multiply(Y).MultiplyByXi(); + + var A = Z.Square().Subtract(t1); + + var B = X.Square().MultiplyByXi(); + var t2 = Y.Multiply(Z); + B = B.Subtract(t2); + + var C = Y.Square().Subtract(X.Multiply(Z)); + + var F = C.Multiply(Y).MultiplyByXi(); + F = F.Add(A.Multiply(Z)); + F = F.Add(B.Multiply(X).MultiplyByXi()); + + var finv = F.Invert(); + + return new Fp6(C.Multiply(finv), B.Multiply(finv), A.Multiply(finv)); + } + + public Fp6 Frobenius() + { + var x = X.Conjugate().Multiply(XiTo2PMinus2Over3); + var y = Y.Conjugate().Multiply(XiToPMinus1Over3); + var z = Z.Conjugate(); + return new Fp6(x, y, z); + } + + public Fp6 FrobeniusP2() + { + var x = X.Multiply(XiTo2PSquaredMinus2Over3); + var y = Y.Multiply(XiToPSquaredMinus1Over3); + return new Fp6(x, y, Z); + } + + public Fp6 FrobeniusP4() + { + var x = X.Multiply(XiToPSquaredMinus1Over3); + var y = Y.Multiply(XiTo2PSquaredMinus2Over3); + return new Fp6(x, y, Z); + } + + public bool Equals(Fp6 other) => X.Equals(other.X) && Y.Equals(other.Y) && Z.Equals(other.Z); + public override bool Equals(object obj) => obj is Fp6 other && Equals(other); + public override int GetHashCode() => HashCode.Combine(X, Y, Z); + public override string ToString() => $"({X}, {Y}, {Z})"; + } + + #endregion + + #region Fp12 + + internal readonly struct Fp12 : IEquatable + { + public Fp6 X { get; } + public Fp6 Y { get; } + + public static Fp12 Zero => new(Fp6.Zero, Fp6.Zero); + public static Fp12 One => new(Fp6.Zero, Fp6.One); + + public bool IsUnity => X.IsZero && Y.Equals(Fp6.One); + public bool IsZero => X.IsZero && Y.IsZero; + + public Fp12(Fp6 x, Fp6 y) + { + X = x; + Y = y; + } + + public static Fp12 Add(Fp12 a, Fp12 b) => new(a.X.Add(b.X), a.Y.Add(b.Y)); + + public static Fp12 Multiply(Fp12 a, Fp12 b) + { + var tx = a.X.Multiply(b.Y).Add(b.X.Multiply(a.Y)); + var ty = a.Y.Multiply(b.Y).Add(a.X.Multiply(b.X).MulTau()); + return new Fp12(tx, ty); + } + + public static Fp12 Multiply(Fp12 value, Fp6 scalar) => + new(value.X.Multiply(scalar), value.Y.Multiply(scalar)); + + public static Fp12 Square(Fp12 a) + { + var v0 = a.X.Multiply(a.Y); + var tmp = a.X.MulTau().Add(a.Y); + var ty = a.X.Add(a.Y).Multiply(tmp).Subtract(v0).Subtract(v0.MulTau()); + var tx = v0.Add(v0); + return new Fp12(tx, ty); + } + + public static Fp12 Invert(Fp12 a) + { + if (a.IsZero) + throw new DivideByZeroException(); + + var t1 = a.X.Square(); + var t2 = a.Y.Square(); + t1 = t1.MulTau(); + t1 = t2.Subtract(t1); + var t3 = t1.Invert(); + + var x = a.X.Negate().Multiply(t3); + var y = a.Y.Multiply(t3); + return new Fp12(x, y); + } + + public static Fp12 Conjugate(Fp12 value) => new(value.X.Negate(), value.Y); + + public static Fp12 MulLine(Fp12 f, Fp2 ell0, Fp2 ell1, Fp2 ell2) + { + var a2 = new Fp6(Fp2.Zero, ell0, ell1).Multiply(f.X); + var t3 = f.Y.Multiply(ell2); + + var t = ell1.Add(ell2); + var t2 = new Fp6(Fp2.Zero, ell0, t); + var sum = new Fp6( + f.X.X.Add(f.Y.X), + f.X.Y.Add(f.Y.Y), + f.X.Z.Add(f.Y.Z)); + + var newX = sum.Multiply(t2).Subtract(a2).Subtract(t3); + var newY = t3.Add(a2.MulTau()); + + return new Fp12(newX, newY); + } + + public static Fp12 Frobenius(Fp12 a) + { + var x = a.X.Frobenius(); + var y = a.Y.Frobenius(); + x = x.Multiply(XiToPMinus1Over6); + return new Fp12(x, y); + } + + public static Fp12 FrobeniusP2(Fp12 a) + { + var x = a.X.FrobeniusP2(); + x = x.Multiply(XiToPSquaredMinus1Over6); + var y = a.Y.FrobeniusP2(); + return new Fp12(x, y); + } + + public static Fp12 FrobeniusP4(Fp12 a) + { + var x = a.X.FrobeniusP4(); + x = x.Multiply(XiToPSquaredMinus1Over3); + var y = a.Y.FrobeniusP4(); + return new Fp12(x, y); + } + + public static Fp12 Exp(Fp12 value, BigInteger exponent) + { + var result = One; + var baseVal = value; + var e = exponent; + + while (e.SignValue > 0) + { + if (e.TestBit(0)) + result = Multiply(result, baseVal); + + baseVal = Square(baseVal); + e = e.ShiftRight(1); + } + + return result; + } + + public static Fp12 FinalExponentiation(Fp12 input) + { + var t1 = Conjugate(input); + var inv = Invert(input); + t1 = Multiply(t1, inv); + + var t2 = FrobeniusP2(t1); + t1 = Multiply(t1, t2); + + var fp = Frobenius(t1); + var fp2 = FrobeniusP2(t1); + var fp3 = Frobenius(fp2); + + var fu = Exp(t1, AteLoopParameter); + var fu2 = Exp(fu, AteLoopParameter); + var fu3 = Exp(fu2, AteLoopParameter); + + var y3 = Frobenius(fu); + var fu2p = Frobenius(fu2); + var fu3p = Frobenius(fu3); + var y2 = FrobeniusP2(fu2); + + var y0 = Multiply(fp, Multiply(fp2, fp3)); + var y1 = Conjugate(t1); + var y5 = Conjugate(fu2); + y3 = Conjugate(y3); + var y4 = Conjugate(Multiply(fu, fu2p)); + var y6 = Conjugate(Multiply(fu3, fu3p)); + + var t0 = Square(y6); + t0 = Multiply(t0, y4); + t0 = Multiply(t0, y5); + + var t1Tmp = Multiply(y3, y5); + t1Tmp = Multiply(t1Tmp, t0); + + t0 = Multiply(t0, y2); + t1Tmp = Square(t1Tmp); + t1Tmp = Multiply(t1Tmp, t0); + t1Tmp = Square(t1Tmp); + + var t2Tmp = Multiply(t1Tmp, y1); + var t3Tmp = Multiply(t1Tmp, y0); + t2Tmp = Square(t2Tmp); + t2Tmp = Multiply(t2Tmp, t3Tmp); + + return t2Tmp; + } + + public bool Equals(Fp12 other) => X.Equals(other.X) && Y.Equals(other.Y); + public override bool Equals(object obj) => obj is Fp12 other && Equals(other); + public override int GetHashCode() => HashCode.Combine(X, Y); + public override string ToString() => $"({X}, {Y})"; + } + + #endregion + } +} diff --git a/src/Neo/Cryptography/BN254.Managed.Fields.cs b/src/Neo/Cryptography/BN254.Managed.Fields.cs new file mode 100644 index 0000000000..c9afeffd27 --- /dev/null +++ b/src/Neo/Cryptography/BN254.Managed.Fields.cs @@ -0,0 +1,241 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// BN254.Managed.Fields.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Org.BouncyCastle.Math; +using System; + +namespace Neo.Cryptography +{ + internal static partial class BN254Managed + { + internal const int FieldElementLength = 32; + + internal static readonly BigInteger FieldModulus = new("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47", 16); + internal static readonly BigInteger GroupOrder = new("30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001", 16); + internal static readonly BigInteger AteLoopParameter = new("4965661367192848881"); + + internal static readonly Fp XiToPSquaredMinus1Over3 = new(new BigInteger("30644e72e131a0295e6dd9e7e0acccb0c28f069fbb966e3de4bd44e5607cfd48", 16)); + internal static readonly Fp XiTo2PSquaredMinus2Over3 = new(new BigInteger("000000000000000059e26bcea0d48bacd4f263f1acdb5c4f5763473177fffffe", 16)); + internal static readonly Fp XiToPSquaredMinus1Over6 = new(new BigInteger("30644e72e131a0295e6dd9e7e0acccb0c28f069fbb966e3de4bd44e5607cfd49", 16)); + + internal static readonly Fp2 XiToPMinus1Over6 = Fp2.FromHex( + "246996f3b4fae7e6a6327cfe12150b8e747992778eeec7e5ca5cf05f80f362ac", + "1284b71c2865a7dfe8b99fdd76e68b605c521e08292f2176d60b35dadcc9e470"); + + internal static readonly Fp2 XiToPMinus1Over3 = Fp2.FromHex( + "16c9e55061ebae204ba4cc8bd75a079432ae2a1d0b7c9dce1665d51c640fcba2", + "2fb347984f7911f74c0bec3cf559b143b78cc310c2c3330c99e39557176f553d"); + + internal static readonly Fp2 XiToPMinus1Over2 = Fp2.FromHex( + "07c03cbcac41049a0704b5a7ec796f2b21807dc98fa25bd282d37f632623b0e3", + "063cf305489af5dcdc5ec698b6e2f9b9dbaae0eda9c95998dc54014671a0135a"); + + internal static readonly Fp2 XiTo2PMinus2Over3 = Fp2.FromHex( + "2c145edbe7fd8aee9f3a80b03b0b1c923685d2ea1bdec763c13b4711cd2b8126", + "05b54f5e64eea80180f3c0b75a181e84d33365f7be94ec72848a1f55921ea762"); + + internal static bool IsAllZero(ReadOnlySpan data) + { + foreach (var b in data) + { + if (b != 0) + return false; + } + + return true; + } + + internal static readonly Fp2 TwistB; + internal static readonly TwistPoint GeneratorTwist; + + static BN254Managed() + { + TwistB = Fp2.FromHex( + "009713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d2", + "2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5"); + + GeneratorTwist = new TwistPoint( + Fp2.FromHex("198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2", + "1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed"), + Fp2.FromHex("090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b", + "12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"), + Fp2.One, + Fp2.One); + } + + #region Fp + + internal readonly struct Fp : IEquatable + { + public BigInteger Value { get; } + + public static Fp Zero => new(BigInteger.Zero); + public static Fp One => new(BigInteger.One); + + public bool IsZero => Value.SignValue == 0; + + public Fp(BigInteger value) + { + Value = Reduce(value); + } + + public Fp(long value) + { + Value = Reduce(BigInteger.ValueOf(value)); + } + + public Fp Add(Fp other) => new(Value.Add(other.Value)); + + public Fp Subtract(Fp other) => new(Value.Subtract(other.Value)); + + public Fp Multiply(Fp other) => new(Value.Multiply(other.Value)); + + public Fp Multiply(long scalar) => new(Value.Multiply(BigInteger.ValueOf(scalar))); + + public Fp Square() => Multiply(this); + + public Fp Negate() + { + if (IsZero) + return this; + return new Fp(FieldModulus.Subtract(Value)); + } + + public Fp Invert() + { + if (IsZero) + throw new DivideByZeroException(); + return new Fp(Value.ModInverse(FieldModulus)); + } + + public byte[] ToBigEndian() + { + var bytes = Value.ToByteArrayUnsigned(); + if (bytes.Length == FieldElementLength) + return bytes; + + var result = new byte[FieldElementLength]; + Array.Copy(bytes, 0, result, FieldElementLength - bytes.Length, bytes.Length); + return result; + } + + public static Fp FromBigEndian(ReadOnlySpan data) + { + if (data.Length != FieldElementLength) + throw new ArgumentException("BN254 field elements must be 32 bytes", nameof(data)); + + var buffer = new byte[data.Length]; + data.CopyTo(buffer); + return new Fp(new BigInteger(1, buffer)); + } + + public bool Equals(Fp other) => Value.Equals(other.Value); + + public override bool Equals(object obj) => obj is Fp other && Equals(other); + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(16); + + private static BigInteger Reduce(BigInteger value) + { + var result = value.Mod(FieldModulus); + if (result.SignValue < 0) + result = result.Add(FieldModulus); + return result; + } + } + + #endregion + + #region Fp2 + + internal readonly struct Fp2 : IEquatable + { + public Fp Imag { get; } + public Fp Real { get; } + + public static Fp2 Zero => new(Fp.Zero, Fp.Zero); + public static Fp2 One => new(Fp.Zero, Fp.One); + + public bool IsZero => Imag.IsZero && Real.IsZero; + + public Fp2(Fp imag, Fp real) + { + Imag = imag; + Real = real; + } + + public static Fp2 FromHex(string imagHex, string realHex) => + new(new Fp(new BigInteger(imagHex, 16)), new Fp(new BigInteger(realHex, 16))); + + public Fp2 Add(Fp2 other) => new(Imag.Add(other.Imag), Real.Add(other.Real)); + + public Fp2 Subtract(Fp2 other) => new(Imag.Subtract(other.Imag), Real.Subtract(other.Real)); + + public Fp2 Negate() => new(Imag.Negate(), Real.Negate()); + + public Fp2 Multiply(Fp2 other) + { + // (xi + y)(x'i + y') = (x*y' + y*x')i + (y*y' - x*x') + var imag = Imag.Multiply(other.Real).Add(Real.Multiply(other.Imag)); + var real = Real.Multiply(other.Real).Subtract(Imag.Multiply(other.Imag)); + return new Fp2(imag, real); + } + + public Fp2 Multiply(Fp scalar) => new(Imag.Multiply(scalar), Real.Multiply(scalar)); + + public Fp2 Square() + { + // Complex squaring algorithm: + // (xi + y)^2 = (x + y)(y - x) + 2ixy + var t0 = Real.Subtract(Imag); + var t1 = Real.Add(Imag); + var real = t0.Multiply(t1); + var imag = Imag.Multiply(Real).Multiply(2); + return new Fp2(imag, real); + } + + public Fp2 MultiplyByXi() + { + // xi = i + 9 in the Go reference + var imag = Imag.Multiply(9).Add(Real); + var real = Real.Multiply(9).Subtract(Imag); + return new Fp2(imag, real); + } + + public Fp2 Conjugate() => new(Imag.Negate(), Real); + + public Fp2 Invert() + { + if (IsZero) + throw new DivideByZeroException(); + + var t0 = Real.Multiply(Real); + var t1 = Imag.Multiply(Imag); + var denom = t0.Add(t1).Invert(); + return new Fp2(Imag.Negate().Multiply(denom), Real.Multiply(denom)); + } + + public Fp2 Multiply(long scalar) => new(Imag.Multiply(scalar), Real.Multiply(scalar)); + + public bool Equals(Fp2 other) => Imag.Equals(other.Imag) && Real.Equals(other.Real); + + public override bool Equals(object obj) => obj is Fp2 other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Imag, Real); + + public override string ToString() => $"({Imag}, {Real})"; + } + + #endregion + } +} diff --git a/src/Neo/Cryptography/BN254.cs b/src/Neo/Cryptography/BN254.cs index e4958b3fab..53da6fdff8 100644 --- a/src/Neo/Cryptography/BN254.cs +++ b/src/Neo/Cryptography/BN254.cs @@ -9,10 +9,9 @@ // Redistribution and use in source and binary forms with or without // modifications are permitted. -using Nethermind.MclBindings; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Neo.Cryptography { @@ -22,55 +21,47 @@ public static class BN254 public const int G1EncodedLength = 64; public const int PairInputLength = 192; - private static readonly object s_sync = new(); - private static volatile bool s_initialized; + private static readonly ECCurve s_curve = new FpCurve(BN254Managed.FieldModulus, BigInteger.Zero, BigInteger.ValueOf(3), BN254Managed.GroupOrder, BigInteger.Zero); + private static readonly ECPoint s_infinity = s_curve.Infinity; public static byte[] Add(ReadOnlySpan input) { - EnsureInitialized(); - - if (!TryDeserializeG1(input[..G1EncodedLength], out var first)) + if (!TryParseG1(input[..G1EncodedLength], out var first)) return new byte[G1EncodedLength]; - if (!TryDeserializeG1(input[G1EncodedLength..], out var second)) + if (!TryParseG1(input[G1EncodedLength..], out var second)) return new byte[G1EncodedLength]; - mclBnG1 result = default; - Mcl.mclBnG1_add(ref result, first, second); - Mcl.mclBnG1_normalize(ref result, result); - - return SerializeG1(result); + var sum = first.Add(second).Normalize(); + return SerializeG1(sum); } public static byte[] Mul(ReadOnlySpan input) { - EnsureInitialized(); - - if (!TryDeserializeG1(input[..G1EncodedLength], out var basePoint)) + if (!TryParseG1(input[..G1EncodedLength], out var basePoint)) return new byte[G1EncodedLength]; if (!TryDeserializeScalar(input[G1EncodedLength..], out var scalar)) return new byte[G1EncodedLength]; - mclBnG1 result = default; - Mcl.mclBnG1_mul(ref result, basePoint, scalar); - Mcl.mclBnG1_normalize(ref result, result); + if (basePoint.IsInfinity || scalar.SignValue == 0) + return new byte[G1EncodedLength]; + var result = basePoint.Multiply(scalar).Normalize(); return SerializeG1(result); } public static byte[] Pairing(ReadOnlySpan input) { - EnsureInitialized(); - if (input.Length == 0) return SuccessWord(); + if (input.Length % PairInputLength != 0) + throw new ArgumentException("Invalid BN254 pairing input length", nameof(input)); + int pairCount = input.Length / PairInputLength; bool hasEffectivePair = false; - - mclBnGT accumulator = default; - Mcl.mclBnGT_setInt32(ref accumulator, 1); + var accumulator = BN254Managed.Fp12.One; for (int pairIndex = 0; pairIndex < pairCount; pairIndex++) { @@ -78,152 +69,112 @@ public static byte[] Pairing(ReadOnlySpan input) var g1Slice = input.Slice(offset, G1EncodedLength); var g2Slice = input.Slice(offset + G1EncodedLength, 2 * G1EncodedLength); - if (!TryDeserializeG1(g1Slice, out var g1)) - return new byte[FieldElementLength]; + if (!TryParseG1(g1Slice, out var g1)) + return FailureWord(); + + if (!BN254Managed.TwistPoint.TryParse(g2Slice, out var g2)) + return FailureWord(); - if (!TryDeserializeG2(g2Slice, out var g2)) - return new byte[FieldElementLength]; + if (!g2.IsOnCurve(true)) + return FailureWord(); - if (Mcl.mclBnG1_isZero(g1) == 1 || Mcl.mclBnG2_isZero(g2) == 1) + if (g1.IsInfinity || g2.IsInfinity) continue; hasEffectivePair = true; - mclBnGT current = default; - Mcl.mclBn_pairing(ref current, g1, g2); - - if (Mcl.mclBnGT_isValid(current) == 0) - return new byte[FieldElementLength]; - - mclBnGT temp = accumulator; - Mcl.mclBnGT_mul(ref accumulator, temp, current); + var curvePoint = BN254Managed.CurvePoint.FromG1(g1); + var miller = BN254Managed.MillerLoop(g2.Clone(), curvePoint); + accumulator = BN254Managed.Fp12.Multiply(accumulator, miller); } if (!hasEffectivePair) return SuccessWord(); - return Mcl.mclBnGT_isOne(accumulator) == 1 ? SuccessWord() : new byte[FieldElementLength]; + var final = BN254Managed.Fp12.FinalExponentiation(accumulator); + if (!final.Equals(BN254Managed.Fp12.One)) + return FailureWord(); + + return SuccessWord(); } - private static unsafe bool TryDeserializeG1(ReadOnlySpan encoded, out mclBnG1 point) + private static byte[] FailureWord() => new byte[FieldElementLength]; + + private static bool TryParseG1(ReadOnlySpan encoded, out ECPoint point) { - point = default; + point = s_infinity; + + if (encoded.Length != G1EncodedLength) + return false; if (IsAllZero(encoded)) return true; - ReadOnlySpan xBytes = encoded[..FieldElementLength]; - fixed (byte* ptr = xBytes) - { - if (Mcl.mclBnFp_setBigEndianMod(ref point.x, (nint)ptr, (nuint)xBytes.Length) != 0) - return false; - } + var x = new BigInteger(1, encoded[..FieldElementLength].ToArray()); + var y = new BigInteger(1, encoded[FieldElementLength..].ToArray()); + + if (!IsValidFieldElement(x) || !IsValidFieldElement(y)) + return false; - ReadOnlySpan yBytes = encoded[FieldElementLength..]; - fixed (byte* ptr = yBytes) + try { - if (Mcl.mclBnFp_setBigEndianMod(ref point.y, (nint)ptr, (nuint)yBytes.Length) != 0) + var candidate = s_curve.CreatePoint(x, y); + if (!candidate.IsValid()) return false; - } - - Mcl.mclBnFp_setInt32(ref point.z, 1); - return Mcl.mclBnG1_isValid(point) == 1; - } - - private static unsafe bool TryDeserializeScalar(ReadOnlySpan encoded, out mclBnFr scalar) - { - scalar = default; + if (!candidate.Multiply(BN254Managed.GroupOrder).IsInfinity) + return false; - if (IsAllZero(encoded)) - { - Mcl.mclBnFr_clear(ref scalar); + point = candidate.Normalize(); return true; } - - fixed (byte* ptr = encoded) + catch (ArgumentException) { - if (Mcl.mclBnFr_setBigEndianMod(ref scalar, (nint)ptr, (nuint)encoded.Length) == -1) - return false; + return false; } - - return Mcl.mclBnFr_isValid(scalar) == 1; } - private static unsafe bool TryDeserializeG2(ReadOnlySpan encoded, out mclBnG2 point) + private static bool TryDeserializeScalar(ReadOnlySpan encoded, out BigInteger scalar) { - point = default; + scalar = BigInteger.Zero; + + if (encoded.Length != FieldElementLength) + return false; if (IsAllZero(encoded)) return true; - Span scratch = stackalloc byte[FieldElementLength]; - - var realSegment = encoded.Slice(FieldElementLength, FieldElementLength); - CopyReversed(realSegment, scratch); - fixed (byte* ptr = scratch) - { - if (Mcl.mclBnFp_deserialize(ref point.x.d0, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) - return false; - } - - var imagSegment = encoded[..FieldElementLength]; - CopyReversed(imagSegment, scratch); - fixed (byte* ptr = scratch) - { - if (Mcl.mclBnFp_deserialize(ref point.x.d1, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) - return false; - } - - var yReal = encoded.Slice(3 * FieldElementLength, FieldElementLength); - CopyReversed(yReal, scratch); - fixed (byte* ptr = scratch) - { - if (Mcl.mclBnFp_deserialize(ref point.y.d0, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) - return false; - } - - var yImag = encoded.Slice(2 * FieldElementLength, FieldElementLength); - CopyReversed(yImag, scratch); - fixed (byte* ptr = scratch) - { - if (Mcl.mclBnFp_deserialize(ref point.y.d1, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero) - return false; - } + scalar = new BigInteger(1, encoded.ToArray()); - Mcl.mclBnFp_setInt32(ref point.z.d0, 1); + if (scalar.SignValue <= 0 || scalar.CompareTo(BN254Managed.GroupOrder) >= 0) + return false; return true; } - private static unsafe byte[] SerializeG1(in mclBnG1 point) + private static byte[] SerializeG1(ECPoint point) { var output = new byte[G1EncodedLength]; - if (Mcl.mclBnG1_isZero(point) == 1) + if (point.IsInfinity) return output; - Span scratch = stackalloc byte[FieldElementLength]; - - fixed (byte* ptr = scratch) - { - if (Mcl.mclBnFp_getLittleEndian((nint)ptr, (nuint)scratch.Length, point.x) == UIntPtr.Zero) - throw new ArgumentException("Failed to serialize BN254 point"); - } - - WriteBigEndian(scratch, output.AsSpan(0, FieldElementLength)); - - fixed (byte* ptr = scratch) - { - if (Mcl.mclBnFp_getLittleEndian((nint)ptr, (nuint)scratch.Length, point.y) == UIntPtr.Zero) - throw new ArgumentException("Failed to serialize BN254 point"); - } + var affine = point.Normalize(); + var xBytes = affine.AffineXCoord.ToBigInteger().ToByteArrayUnsigned(); + var yBytes = affine.AffineYCoord.ToBigInteger().ToByteArrayUnsigned(); - WriteBigEndian(scratch, output.AsSpan(FieldElementLength, FieldElementLength)); + CopyToFixed(xBytes, output.AsSpan(0, FieldElementLength)); + CopyToFixed(yBytes, output.AsSpan(FieldElementLength, FieldElementLength)); return output; } + private static void CopyToFixed(ReadOnlySpan source, Span destination) + { + destination.Clear(); + source.CopyTo(destination[(destination.Length - source.Length)..]); + } + private static byte[] SuccessWord() { var output = new byte[FieldElementLength]; @@ -233,45 +184,16 @@ private static byte[] SuccessWord() private static bool IsAllZero(ReadOnlySpan data) { - for (int i = 0; i < data.Length; ++i) + foreach (var b in data) { - if (data[i] != 0) + if (b != 0) return false; } return true; } - private static void WriteBigEndian(ReadOnlySpan littleEndian, Span destination) - { - for (int i = 0; i < littleEndian.Length; ++i) - destination[i] = littleEndian[littleEndian.Length - 1 - i]; - } - - private static void CopyReversed(ReadOnlySpan source, Span destination) - { - for (int i = 0; i < source.Length; ++i) - destination[i] = source[source.Length - 1 - i]; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void EnsureInitialized() - { - if (s_initialized) - return; - - lock (s_sync) - { - if (s_initialized) - return; - - if (Mcl.mclBn_init(Mcl.MCL_BN_SNARK1, Mcl.MCLBN_COMPILED_TIME_VAR) != 0) - throw new InvalidOperationException("BN254 initialization failed"); - - Mcl.mclBn_setETHserialization(1); - - s_initialized = true; - } - } + private static bool IsValidFieldElement(BigInteger value) => + value.SignValue >= 0 && value.CompareTo(BN254Managed.FieldModulus) < 0; } } diff --git a/src/Neo/Neo.csproj b/src/Neo/Neo.csproj index 10ee8b9c82..abb5261da8 100644 --- a/src/Neo/Neo.csproj +++ b/src/Neo/Neo.csproj @@ -8,7 +8,6 @@ - diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs index 145b4097e5..d738d22334 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_CryptoLib.cs @@ -58,10 +58,10 @@ public class UT_CryptoLib private const string Bn254G1X = "1"; private const string Bn254G1Y = "2"; - private const string Bn254G2XIm = "1800deef121f1e7641a819fe67140f7f8f87b140996fbbd1ba87fb145641f404"; - private const string Bn254G2XRe = "198e9393920d483a7260bfb731fb5db382322bc5b47fbf6c80f6321231df581"; - private const string Bn254G2YIm = "12c85ea5db8c6deb43baf7868f1c5341fd8ed84a82f89ed36e80b6a4a8dd22e1"; - private const string Bn254G2YRe = "090689d0585ff0756c27a122072274f89d4d1c6d2f9d3af03d86c6b29b53e2b"; + private const string Bn254G2XIm = "198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2"; + private const string Bn254G2XRe = "1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed"; + private const string Bn254G2YIm = "090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b"; + private const string Bn254G2YRe = "12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"; private const string Bn254DoubleX = "030644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3"; private const string Bn254DoubleY = "15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4"; private const string Bn254PairingPositive = @@ -70,6 +70,10 @@ public class UT_CryptoLib "0x142c9123c08a0d7f66d95f3ad637a06b95700bc525073b75610884ef45416e1610104c796f40bfeef3588e996c040d2a88c0b4b85afd2578327b99413c6fe820198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed275dc4a288d1afb3cbb1ac09187524c7db36395df7be3b99e673b13a075a65ec1d9befcd05a5323e6da4d435f3b617cdb3af83285c2df711ef39c01571827f9d"; private const string Bn254PairingInvalidG1 = "0x00000000000000000000000000000000000000000000000000000000000000000000000000be00be00bebebebebebe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + private const string Bn254PairingInvalidG2 = + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffff0000000000000000ffffffffffffffffffff"; + private const string Bn254PairingInvalidG2Subgroup = + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000070a77c19a07df2e666ea36f7879462c0a78ebbdf5c70b3dd35d438dc58f0d9d0a2dd5b476a606a8243e7e879bddaa8086ba658087aacc4d986a10c74dd9e7742d46618f9d516e0f07a59d3c97f5e167a1b49ebe9fb30dd05bded8185a545420"; private readonly byte[] notG1 = @@ -1213,6 +1217,8 @@ public void TestBn254PairingGenerator() WriteBn254Field(Bn254G2YIm, input, 128); WriteBn254Field(Bn254G2YRe, input, 160); + Assert.IsTrue(BN254Managed.TwistPoint.TryParse(input.AsSpan(64, 128), out _), "parse_g2"); + byte[] result = CryptoLib.Bn254Pairing(input); Assert.IsTrue(result.All(b => b == 0)); @@ -1236,12 +1242,15 @@ public void TestBn254PairingVectors() (Bn254PairingPositive, true, "positive"), (Bn254PairingNegative, false, "negative"), (Bn254PairingInvalidG1, false, "invalid_g1_point"), + (Bn254PairingInvalidG2, false, "invalid_g2_point"), + (Bn254PairingInvalidG2Subgroup, false, "invalid_g2_subgroup"), }; foreach (var (hex, expectedSuccess, label) in cases) { byte[] input = HexToBytes(hex); byte[] result = CryptoLib.Bn254Pairing(input); + Console.WriteLine($"{label}: {Convert.ToHexString(result)}"); Assert.AreEqual(32, result.Length, label); if (expectedSuccess) @@ -1256,6 +1265,47 @@ public void TestBn254PairingVectors() } } + [TestMethod] + public void TestBn254G2GeneratorSubgroup() + { + var x = BN254Managed.Fp2.FromHex(Bn254G2XIm, Bn254G2XRe); + var y = BN254Managed.Fp2.FromHex(Bn254G2YIm, Bn254G2YRe); + var generator = new BN254Managed.TwistPoint(x, y, BN254Managed.Fp2.One, BN254Managed.Fp2.One); + Assert.IsTrue(generator.IsOnCurve(), "Generator not on curve"); + + var multiple = BN254Managed.TwistPoint.MultiplyScalar(generator.Clone(), BN254Managed.GroupOrder); + if (!multiple.IsInfinity) + { + multiple.MakeAffine(); + Assert.Fail($"Generator not in subgroup: X={multiple.X}, Y={multiple.Y}"); + } + } + + private static Org.BouncyCastle.Math.EC.ECPoint ParseG1(ReadOnlySpan encoded) + { + if (encoded.Length != BN254.G1EncodedLength) + throw new ArgumentException(nameof(encoded)); + + if (BN254Managed.IsAllZero(encoded)) + return new Org.BouncyCastle.Math.EC.FpCurve( + BN254Managed.FieldModulus, + Org.BouncyCastle.Math.BigInteger.Zero, + Org.BouncyCastle.Math.BigInteger.ValueOf(3), + BN254Managed.GroupOrder, + Org.BouncyCastle.Math.BigInteger.Zero).Infinity; + + var x = new Org.BouncyCastle.Math.BigInteger(1, encoded[..BN254.FieldElementLength].ToArray()); + var y = new Org.BouncyCastle.Math.BigInteger(1, encoded[BN254.FieldElementLength..].ToArray()); + var curve = new Org.BouncyCastle.Math.EC.FpCurve( + BN254Managed.FieldModulus, + Org.BouncyCastle.Math.BigInteger.Zero, + Org.BouncyCastle.Math.BigInteger.ValueOf(3), + BN254Managed.GroupOrder, + Org.BouncyCastle.Math.BigInteger.Zero); + var point = curve.CreatePoint(x, y); + return point.Normalize(); + } + [TestMethod] public void TestBn254InvalidInputs() { @@ -1264,6 +1314,30 @@ public void TestBn254InvalidInputs() Assert.ThrowsExactly(() => CryptoLib.Bn254Pairing(new byte[1])); } + [TestMethod] + public void TestBn254PairingPositiveFinal() + { + var input = HexToBytes(Bn254PairingPositive); + var pairCount = input.Length / BN254.PairInputLength; + var accumulator = BN254Managed.Fp12.One; + + for (int pairIndex = 0; pairIndex < pairCount; pairIndex++) + { + var g1Slice = input.AsSpan(pairIndex * BN254.PairInputLength, BN254.G1EncodedLength); + var g2Slice = input.AsSpan(pairIndex * BN254.PairInputLength + BN254.G1EncodedLength, 2 * BN254.G1EncodedLength); + + var g1 = ParseG1(g1Slice); + Assert.IsTrue(BN254Managed.TwistPoint.TryParse(g2Slice, out var g2)); + + var qp = BN254Managed.CurvePoint.FromG1(g1); + var miller = BN254Managed.MillerLoop(g2.Clone(), qp); + accumulator = BN254Managed.Fp12.Multiply(accumulator, miller); + } + + var final = BN254Managed.Fp12.FinalExponentiation(accumulator); + Assert.AreEqual(BN254Managed.Fp12.One, final, "Pairing accumulator mismatch for positive vector"); + } + private bool CallVerifyWithEd25519(byte[] message, byte[] publicKey, byte[] signature) { var snapshot = TestBlockchain.GetTestSnapshotCache();