Skip to content

Commit 9973b98

Browse files
committed
Add BN254 native contract
1 parent 9f10963 commit 9973b98

File tree

9 files changed

+889
-1
lines changed

9 files changed

+889
-1
lines changed

src/Neo/Cryptography/BN254.cs

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Copyright (C) 2015-2025 The Neo Project.
2+
//
3+
// BN254.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
using Neo.Extensions;
13+
using Nethermind.MclBindings;
14+
using System;
15+
using System.Runtime.CompilerServices;
16+
using System.Runtime.InteropServices;
17+
18+
namespace Neo.Cryptography
19+
{
20+
public static class BN254
21+
{
22+
public const int FieldElementLength = 32;
23+
public const int G1EncodedLength = 64;
24+
public const int PairInputLength = 192;
25+
26+
private static readonly object s_sync = new();
27+
private static bool s_initialized;
28+
29+
public static byte[] Add(ReadOnlySpan<byte> input)
30+
{
31+
if (input.Length != G1EncodedLength * 2)
32+
throw new ArgumentException("Invalid BN254 add input length", nameof(input));
33+
34+
EnsureInitialized();
35+
36+
if (!TryDeserializeG1(input[..G1EncodedLength], out var first))
37+
return new byte[G1EncodedLength];
38+
39+
if (!TryDeserializeG1(input[G1EncodedLength..], out var second))
40+
return new byte[G1EncodedLength];
41+
42+
mclBnG1 result = default;
43+
Mcl.mclBnG1_add(ref result, first, second);
44+
Mcl.mclBnG1_normalize(ref result, result);
45+
46+
return SerializeG1(result);
47+
}
48+
49+
public static byte[] Mul(ReadOnlySpan<byte> input)
50+
{
51+
if (input.Length != G1EncodedLength + FieldElementLength)
52+
throw new ArgumentException("Invalid BN254 mul input length", nameof(input));
53+
54+
EnsureInitialized();
55+
56+
if (!TryDeserializeG1(input[..G1EncodedLength], out var basePoint))
57+
return new byte[G1EncodedLength];
58+
59+
if (!TryDeserializeScalar(input[G1EncodedLength..], out var scalar))
60+
return new byte[G1EncodedLength];
61+
62+
mclBnG1 result = default;
63+
Mcl.mclBnG1_mul(ref result, basePoint, scalar);
64+
Mcl.mclBnG1_normalize(ref result, result);
65+
66+
return SerializeG1(result);
67+
}
68+
69+
public static byte[] Pairing(ReadOnlySpan<byte> input)
70+
{
71+
if (input.Length % PairInputLength != 0)
72+
throw new ArgumentException("Invalid BN254 pairing input length", nameof(input));
73+
74+
EnsureInitialized();
75+
76+
if (input.Length == 0)
77+
return SuccessWord();
78+
79+
int pairCount = input.Length / PairInputLength;
80+
bool hasEffectivePair = false;
81+
82+
mclBnGT accumulator = default;
83+
Mcl.mclBnGT_setInt32(ref accumulator, 1);
84+
85+
for (int pairIndex = 0; pairIndex < pairCount; pairIndex++)
86+
{
87+
int offset = pairIndex * PairInputLength;
88+
var g1Slice = input.Slice(offset, G1EncodedLength);
89+
var g2Slice = input.Slice(offset + G1EncodedLength, 2 * G1EncodedLength);
90+
91+
if (!TryDeserializeG1(g1Slice, out var g1))
92+
return new byte[FieldElementLength];
93+
94+
if (!TryDeserializeG2(g2Slice, out var g2))
95+
return new byte[FieldElementLength];
96+
97+
if (Mcl.mclBnG1_isZero(g1) == 1 || Mcl.mclBnG2_isZero(g2) == 1)
98+
continue;
99+
100+
hasEffectivePair = true;
101+
102+
mclBnGT current = default;
103+
Mcl.mclBn_pairing(ref current, g1, g2);
104+
105+
if (Mcl.mclBnGT_isValid(current) == 0)
106+
return new byte[FieldElementLength];
107+
108+
mclBnGT temp = accumulator;
109+
Mcl.mclBnGT_mul(ref accumulator, temp, current);
110+
}
111+
112+
if (!hasEffectivePair)
113+
return SuccessWord();
114+
115+
return Mcl.mclBnGT_isOne(accumulator) == 1 ? SuccessWord() : new byte[FieldElementLength];
116+
}
117+
118+
private static unsafe bool TryDeserializeG1(ReadOnlySpan<byte> encoded, out mclBnG1 point)
119+
{
120+
point = default;
121+
122+
if (!encoded.NotZero())
123+
return true;
124+
125+
ReadOnlySpan<byte> xBytes = encoded[..FieldElementLength];
126+
fixed (byte* ptr = xBytes)
127+
{
128+
if (Mcl.mclBnFp_setBigEndianMod(ref point.x, (nint)ptr, (nuint)xBytes.Length) != 0)
129+
return false;
130+
}
131+
132+
ReadOnlySpan<byte> yBytes = encoded[FieldElementLength..];
133+
fixed (byte* ptr = yBytes)
134+
{
135+
if (Mcl.mclBnFp_setBigEndianMod(ref point.y, (nint)ptr, (nuint)yBytes.Length) != 0)
136+
return false;
137+
}
138+
139+
Mcl.mclBnFp_setInt32(ref point.z, 1);
140+
141+
return Mcl.mclBnG1_isValid(point) == 1;
142+
}
143+
144+
private static unsafe bool TryDeserializeScalar(ReadOnlySpan<byte> encoded, out mclBnFr scalar)
145+
{
146+
scalar = default;
147+
148+
if (!encoded.NotZero())
149+
{
150+
Mcl.mclBnFr_clear(ref scalar);
151+
return true;
152+
}
153+
154+
fixed (byte* ptr = encoded)
155+
{
156+
if (Mcl.mclBnFr_setBigEndianMod(ref scalar, (nint)ptr, (nuint)encoded.Length) == -1)
157+
return false;
158+
}
159+
160+
return Mcl.mclBnFr_isValid(scalar) == 1;
161+
}
162+
163+
private static unsafe bool TryDeserializeG2(ReadOnlySpan<byte> encoded, out mclBnG2 point)
164+
{
165+
point = default;
166+
167+
if (!encoded.NotZero())
168+
return true;
169+
170+
Span<byte> scratch = stackalloc byte[FieldElementLength];
171+
172+
var realSegment = encoded.Slice(FieldElementLength, FieldElementLength);
173+
CopyReversed(realSegment, scratch);
174+
fixed (byte* ptr = scratch)
175+
{
176+
if (Mcl.mclBnFp_deserialize(ref point.x.d0, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero)
177+
return false;
178+
}
179+
180+
var imagSegment = encoded[..FieldElementLength];
181+
CopyReversed(imagSegment, scratch);
182+
fixed (byte* ptr = scratch)
183+
{
184+
if (Mcl.mclBnFp_deserialize(ref point.x.d1, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero)
185+
return false;
186+
}
187+
188+
var yReal = encoded.Slice(3 * FieldElementLength, FieldElementLength);
189+
CopyReversed(yReal, scratch);
190+
fixed (byte* ptr = scratch)
191+
{
192+
if (Mcl.mclBnFp_deserialize(ref point.y.d0, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero)
193+
return false;
194+
}
195+
196+
var yImag = encoded.Slice(2 * FieldElementLength, FieldElementLength);
197+
CopyReversed(yImag, scratch);
198+
fixed (byte* ptr = scratch)
199+
{
200+
if (Mcl.mclBnFp_deserialize(ref point.y.d1, (nint)ptr, (nuint)scratch.Length) == UIntPtr.Zero)
201+
return false;
202+
}
203+
204+
Mcl.mclBnFp_setInt32(ref point.z.d0, 1);
205+
206+
return true;
207+
}
208+
209+
private static unsafe byte[] SerializeG1(in mclBnG1 point)
210+
{
211+
var output = new byte[G1EncodedLength];
212+
213+
if (Mcl.mclBnG1_isZero(point) == 1)
214+
return output;
215+
216+
Span<byte> scratch = stackalloc byte[FieldElementLength];
217+
218+
fixed (byte* ptr = scratch)
219+
{
220+
if (Mcl.mclBnFp_getLittleEndian((nint)ptr, (nuint)scratch.Length, point.x) == UIntPtr.Zero)
221+
throw new ArgumentException("Failed to serialize BN254 point");
222+
}
223+
224+
WriteBigEndian(scratch, output.AsSpan(0, FieldElementLength));
225+
226+
fixed (byte* ptr = scratch)
227+
{
228+
if (Mcl.mclBnFp_getLittleEndian((nint)ptr, (nuint)scratch.Length, point.y) == UIntPtr.Zero)
229+
throw new ArgumentException("Failed to serialize BN254 point");
230+
}
231+
232+
WriteBigEndian(scratch, output.AsSpan(FieldElementLength, FieldElementLength));
233+
234+
return output;
235+
}
236+
237+
private static byte[] SuccessWord()
238+
{
239+
var output = new byte[FieldElementLength];
240+
output[^1] = 1;
241+
return output;
242+
}
243+
244+
private static void WriteBigEndian(ReadOnlySpan<byte> littleEndian, Span<byte> destination)
245+
{
246+
for (int i = 0; i < littleEndian.Length; ++i)
247+
destination[i] = littleEndian[littleEndian.Length - 1 - i];
248+
}
249+
250+
private static void CopyReversed(ReadOnlySpan<byte> source, Span<byte> destination)
251+
{
252+
for (int i = 0; i < source.Length; ++i)
253+
destination[i] = source[source.Length - 1 - i];
254+
}
255+
256+
[MethodImpl(MethodImplOptions.NoInlining)]
257+
private static void EnsureInitialized()
258+
{
259+
if (s_initialized)
260+
return;
261+
262+
lock (s_sync)
263+
{
264+
if (s_initialized)
265+
return;
266+
267+
if (Mcl.mclBn_init(Mcl.MCL_BN_SNARK1, Mcl.MCLBN_COMPILED_TIME_VAR) != 0)
268+
throw new InvalidOperationException("BN254 initialization failed");
269+
270+
Mcl.mclBn_setETHserialization(1);
271+
272+
s_initialized = true;
273+
}
274+
}
275+
}
276+
}

src/Neo/Neo.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="Akka" Version="1.5.55" />
1111
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
12+
<PackageReference Include="Nethermind.MclBindings" Version="1.0.2" />
1213
<PackageReference Include="K4os.Compression.LZ4" Version="1.3.8" />
1314
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
1415
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (C) 2015-2025 The Neo Project.
2+
//
3+
// CryptoLib.BN254.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
using Neo.Cryptography;
13+
using System;
14+
15+
namespace Neo.SmartContract.Native
16+
{
17+
partial class CryptoLib
18+
{
19+
[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)]
20+
public static byte[] Bn254Add(byte[] input)
21+
{
22+
ArgumentNullException.ThrowIfNull(input);
23+
24+
return BN254.Add(input);
25+
}
26+
27+
[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 19)]
28+
public static byte[] Bn254Mul(byte[] input)
29+
{
30+
ArgumentNullException.ThrowIfNull(input);
31+
32+
return BN254.Mul(input);
33+
}
34+
35+
[ContractMethod(Hardfork.HF_Gorgon, CpuFee = 1 << 21)]
36+
public static byte[] Bn254Pairing(byte[] input)
37+
{
38+
ArgumentNullException.ThrowIfNull(input);
39+
40+
return BN254.Pairing(input);
41+
}
42+
}
43+
}

tests/Neo.UnitTests/Neo.UnitTests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
<None Update="SmartContract\Manifest\TestFile\**">
1818
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
1919
</None>
20+
<None Update="SmartContract\Native\BN254TestVectors\**">
21+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
22+
</None>
2023
<None Update="GasTests\Fixtures\**">
2124
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2225
</None>

0 commit comments

Comments
 (0)