Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/native-contracts-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | -- | -- |
Expand Down
276 changes: 276 additions & 0 deletions src/Neo/Cryptography/BN254.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
// 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 Neo.Extensions;
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<byte> input)
{
if (input.Length != G1EncodedLength * 2)
throw new ArgumentException("Invalid BN254 add input length", nameof(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<byte> input)
{
if (input.Length != G1EncodedLength + FieldElementLength)
throw new ArgumentException("Invalid BN254 mul input length", nameof(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<byte> input)
{
if (input.Length % PairInputLength != 0)
throw new ArgumentException("Invalid BN254 pairing input length", nameof(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);
Copy link

Choose a reason for hiding this comment

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

Maybe mclBn_millerLoop is better here, ref https://github.com/NethermindEth/nethermind/blob/1d85282f2d88b092469a57673dc8fb9a403b3a13/src/Nethermind/Nethermind.Evm.Precompiles/BN254.cs#L108 and https://github.com/herumi/mcl/blob/e4c8bbe4af0013e00244c661836f3a3676f21024/src/pairing_impl.hpp#L774.

The longer a correct input we have, the more times the finalExp is executed. Anyway, it depends on your design of the procedure.


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<byte> encoded, out mclBnG1 point)
{
point = default;

if (!encoded.NotZero())
return true;

ReadOnlySpan<byte> xBytes = encoded[..FieldElementLength];
fixed (byte* ptr = xBytes)
{
if (Mcl.mclBnFp_setBigEndianMod(ref point.x, (nint)ptr, (nuint)xBytes.Length) != 0)
return false;
}

ReadOnlySpan<byte> 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<byte> encoded, out mclBnFr scalar)
{
scalar = default;

if (!encoded.NotZero())
{
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<byte> encoded, out mclBnG2 point)
{
point = default;

if (!encoded.NotZero())
return true;

Span<byte> 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<byte> 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 void WriteBigEndian(ReadOnlySpan<byte> littleEndian, Span<byte> destination)
{
for (int i = 0; i < littleEndian.Length; ++i)
destination[i] = littleEndian[littleEndian.Length - 1 - i];
}

private static void CopyReversed(ReadOnlySpan<byte> source, Span<byte> 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;
}
}
}
}
2 changes: 2 additions & 0 deletions src/Neo/Neo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageTags>NEO;AntShares;Blockchain;Smart Contract</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Akka" Version="1.5.55" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageReference Include="Nethermind.MclBindings" Version="1.0.2" />
<PackageReference Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
Expand Down
43 changes: 43 additions & 0 deletions src/Neo/SmartContract/Native/CryptoLib.BN254.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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)
Copy link
Member

@AnnaShaleva AnnaShaleva Nov 10, 2025

Choose a reason for hiding this comment

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

We have a divergence from the CryptoLib interface. Existing BLS12-381 methods like bls12381Add and bls12381Mul work with InteropInterface which contains G1/G2/GT point instances under the hood:

public static InteropInterface Bls12381Mul(InteropInterface x, byte[] mul, bool neg)

I think compatibility with an existing CryptoLib interface is important, so it would be good to refactor BN254 methods to work with InteropInterface types instead of raw byte arrays. And we also should introduce bn254Serialize and bn254Deserialize for that.

Given this approach, user will deserialize input points once, then perform a set of operations, then serialize the result (also once per contract method). This approach is more effective when there's a set of BN254 operations (which is usually the case), we just won't perform deserialization/serialization on every BN254 operation.

{
ArgumentNullException.ThrowIfNull(input);

return BN254.Add(input);
}

[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)]
public static byte[] Bn254Mul(byte[] input)
{
ArgumentNullException.ThrowIfNull(input);

return BN254.Mul(input);
}

[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21)]
public static byte[] Bn254Pairing(byte[] input)
{
ArgumentNullException.ThrowIfNull(input);

return BN254.Pairing(input);
}
}
}
3 changes: 3 additions & 0 deletions tests/Neo.UnitTests/Neo.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<None Update="SmartContract\Manifest\TestFile\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="SmartContract\Native\BN254TestVectors\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="GasTests\Fixtures\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading