Skip to content
Open
Show file tree
Hide file tree
Changes from all 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);

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)]
Copy link
Member

Choose a reason for hiding this comment

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

Faun? I thought we need to include them into the upcoming release.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe no hard fork is needed.

public static byte[] Bn254Add(byte[] input)
Copy link
Member

Choose a reason for hiding this comment

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

Also need bn254Equal.

Copy link
Member

Choose a reason for hiding this comment

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

As far as bn254Serialize and bn254Deserialize.

{
ArgumentNullException.ThrowIfNull(input);

return BN254.Add(input);
}

[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)]
[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21)]

Follow bls12381Mul, multiplication is more complicated.

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