Skip to content

Commit b71f990

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

File tree

9 files changed

+899
-1
lines changed

9 files changed

+899
-1
lines changed

src/Neo/Cryptography/BN254.cs

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

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)