Skip to content

Commit 552755a

Browse files
authored
Use mcl to boost alt_bn128 precompiles performance (#3747)
* USe mcl to boost alt_bn128 precompiles performance |Operation | Nim-bn254 | mcl-x64 | mcl-avx512 | nim->mcl-x64(%) | nim->mcl-avx512(%) | |:------------|---------------:|--------------:|--------------:|----------------:|-------------------:| |G1 + G1 | 511.494.241 | 4.714.393 | 4.834.672 | 99,08 | 99,05 | |G1 * FR | 718.740.303 | 70.999.718 | 71.017.095 | 90,12 | 90,12 | |G1 G2 pairing| 25.122.616.596 | 5.310.392.374 | 4.661.134.042 | 78,86 | 81,45 | * Fix nvp CI * linux arm64 mcl uses prebuild .o * Fix mcl projectPath for cross compilation * Revert unnecesary changes * bump nim-mcl
1 parent d0a0ae6 commit 552755a

File tree

8 files changed

+326
-124
lines changed

8 files changed

+326
-124
lines changed

.github/workflows/nimbus_verified_proxy.yml

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ on:
3434
- 'Makefile'
3535
- 'nimbus.nimble'
3636

37+
workflow_dispatch:
38+
3739
concurrency: # Cancel stale PR builds (but not push builds)
3840
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
3941
cancel-in-progress: true
@@ -67,28 +69,9 @@ jobs:
6769
rocksdb-cache: true
6870
eest-cache: false
6971

70-
- name: Run verified proxy tests (Windows)
71-
if: runner.os == 'Windows'
72+
- name: Run verified proxy tests
7273
run: |
7374
gcc --version
74-
DEFAULT_MAKE_FLAGS="-j${ncpu}"
75-
mingw32-make ${DEFAULT_MAKE_FLAGS} nimbus_verified_proxy libverifproxy
76-
build/nimbus_verified_proxy.exe --help
77-
mingw32-make ${DEFAULT_MAKE_FLAGS} nimbus-verified-proxy-test
78-
79-
- name: Run verified proxy tests (Linux)
80-
if: runner.os == 'Linux'
81-
run: |
82-
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:/usr/local/lib"
83-
DEFAULT_MAKE_FLAGS="-j${ncpu}"
84-
env CC=gcc make ${DEFAULT_MAKE_FLAGS} nimbus_verified_proxy libverifproxy
85-
build/nimbus_verified_proxy --help
86-
# CC is needed to select correct compiler 32/64 bit
87-
env CC=gcc CXX=g++ make ${DEFAULT_MAKE_FLAGS} nimbus-verified-proxy-test
88-
89-
- name: Run verified proxy tests (Macos)
90-
if: runner.os == 'Macos'
91-
run: |
9275
DEFAULT_MAKE_FLAGS="-j${ncpu}"
9376
make ${DEFAULT_MAKE_FLAGS} nimbus_verified_proxy libverifproxy
9477
build/nimbus_verified_proxy --help

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,7 @@
226226
path = vendor/nim-minilru
227227
url = https://github.com/status-im/nim-minilru.git
228228
branch = master
229+
[submodule "vendor/nim-mcl"]
230+
path = vendor/nim-mcl
231+
url = https://github.com/status-im/nim-mcl
232+
branch = main

config.nims

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ elif defined(riscv64):
9494
# and seems to be the minimum extensions needed to build.
9595
switch("passC", "-march=rv64gc")
9696
switch("passL", "-march=rv64gc")
97+
elif defined(linux) and defined(arm64):
98+
# clang can't handle "-march=native"
99+
switch("passC", "-march=armv8-a")
100+
switch("passL", "-march=armv8-a")
97101
else:
98102
switch("passC", "-march=native")
99103
switch("passL", "-march=native")

execution_chain/compile_info.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@
1414
const chronicles_line_numbers {.strdefine.} = "0"
1515
when chronicles_line_numbers notin ["0", "off"]:
1616
{.hint: "*** Compiling with logger line numbers enabled".}
17+
18+
const enable_mcl_lib* {.booldefine.} = true
19+
when enable_mcl_lib:
20+
{.hint: "*** Compiling with mcl library".}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# nimbus-execution-client
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms
9+
10+
{.push raises: [].}
11+
12+
import
13+
results,
14+
stew/assign2,
15+
bncurve/arith,
16+
mcl/bn_abi,
17+
./evm_errors,
18+
./types
19+
20+
# one time initialization
21+
doAssert(mclBn_init(MCL_BN_SNARK1, MCLBN_COMPILED_TIME_VAR) == 0.cint)
22+
23+
const
24+
ioMode = MCLBN_IO_SERIALIZE or MCLBN_IO_BIG_ENDIAN
25+
26+
func isAllZero(data: openArray[byte]): bool =
27+
for c in data:
28+
if c != 0: return false
29+
true
30+
31+
# deserialize Fp from 32 byte big-endian number.
32+
func deserialize(x: var BnFp, buf: openArray[byte]): bool =
33+
mclBnFp_setStr(x.addr, cast[ptr char](buf[0].addr), 32, ioMode) == 0.cint
34+
35+
func deserialize(x: var BnFp2, buf: openArray[byte]): bool =
36+
deserialize(x.d[1], buf) and deserialize(x.d[0], buf.toOpenArray(32, buf.len-1))
37+
38+
func deserialize(x: var BnFr, buf: openArray[byte]): bool =
39+
mclBnFr_setBigEndianMod(x.addr, buf[0].addr, 32.mclSize) == 0
40+
41+
func deserialize(P: var BnG1, buf: openArray[byte]): bool =
42+
if buf.isAllZero:
43+
mclBnG1_clear(P.addr)
44+
return true
45+
46+
if not deserialize(P.x, buf) or not deserialize(P.y, buf.toOpenArray(32, buf.len-1)):
47+
return false
48+
49+
mclBnFp_setInt32(P.z.addr, 1.cint)
50+
mclBnG1_isValid(P.addr) == 1.cint
51+
52+
func deserialize(P: var BnG2, buf: openArray[byte]): bool =
53+
if buf.isAllZero:
54+
mclBnG2_clear(P.addr)
55+
return true
56+
57+
if not deserialize(P.x, buf) or not deserialize(P.y, buf.toOpenArray(64, buf.len-1)):
58+
return false
59+
60+
mclBnFp_setInt32(P.z.d[0].addr, 1.cint)
61+
mclBnFp_clear(P.z.d[1].addr)
62+
mclBnG2_isValid(P.addr) == 1.cint
63+
64+
# serialize Fp as 32 byte big-endian number.
65+
func serialize(buf: var openArray[byte], x: BnFp): bool =
66+
# sigh, getStr output buf is zero terminated
67+
var tmp: array[33, byte]
68+
result = mclBnFp_getStr(cast[ptr char](tmp[0].addr), 32, x.addr, ioMode) == 32.mclSize
69+
assign(buf.toOpenArray(0, 31), tmp.toOpenArray(0, 31))
70+
71+
# Serialize P.x|P.y.
72+
# Set _buf to all zeros if P == 0.
73+
func serialize(buf: var openArray[byte], P: BnG1): bool =
74+
if mclBnG1_isZero(P.addr) == 1.cint:
75+
zeroMem(buf[0].addr, 64)
76+
return true
77+
78+
var Pn {.noinit.}: BnG1
79+
mclBnG1_normalize(Pn.addr, P.addr)
80+
serialize(buf, Pn.x) and serialize(buf.toOpenArray(32, buf.len-1), Pn.y)
81+
82+
func bn256ecAddImpl*(c: Computation): EvmResultVoid =
83+
var
84+
input: array[128, byte]
85+
p1 {.noinit.}: BnG1
86+
p2 {.noinit.}: BnG1
87+
apo {.noinit.}: BnG1
88+
89+
# Padding data
90+
let len = min(c.msg.data.len, 128) - 1
91+
assign(input.toOpenArray(0, len), c.msg.data.toOpenArray(0, len))
92+
93+
if not p1.deserialize(input.toOpenArray(0, 63)):
94+
return err(prcErr(PrcInvalidPoint))
95+
96+
if not p2.deserialize(input.toOpenArray(64, 127)):
97+
return err(prcErr(PrcInvalidPoint))
98+
99+
mclBnG1_add(apo.addr, p1.addr, p2.addr)
100+
101+
c.output.setLen(64)
102+
if not serialize(c.output, apo):
103+
zeroMem(c.output[0].addr, 64)
104+
105+
ok()
106+
107+
func bn256ecMulImpl*(c: Computation): EvmResultVoid =
108+
var
109+
input: array[96, byte]
110+
p1 {.noinit.}: BnG1
111+
fr {.noinit.}: BnFr
112+
apo {.noinit.}: BnG1
113+
114+
# Padding data
115+
let len = min(c.msg.data.len, 96) - 1
116+
assign(input.toOpenArray(0, len), c.msg.data.toOpenArray(0, len))
117+
118+
if not p1.deserialize(input.toOpenArray(0, 63)):
119+
return err(prcErr(PrcInvalidPoint))
120+
121+
if not fr.deserialize(input.toOpenArray(64, 95)):
122+
return err(prcErr(PrcInvalidPoint))
123+
124+
mclBnG1_mul(apo.addr, p1.addr, fr.addr)
125+
126+
c.output.setLen(64)
127+
if not serialize(c.output, apo):
128+
zeroMem(c.output[0].addr, 64)
129+
130+
ok()
131+
132+
func bn256ecPairingImpl*(c: Computation): EvmResultVoid =
133+
let msglen = c.msg.data.len
134+
if msglen == 0:
135+
# we can discard here because we supply buffer of proper size
136+
c.output.setLen(32)
137+
discard BNU256.one().toBytesBE(c.output)
138+
else:
139+
# Calculate number of pairing pairs
140+
let count = msglen div 192
141+
# Pairing accumulator
142+
var
143+
acc {.noinit.}: BnGT
144+
one {.noinit.}: BnGT
145+
tmp {.noinit.}: BnGT
146+
147+
mclBnGT_setInt(acc.addr, 1.mclInt)
148+
mclBnGT_setInt(one.addr, 1.mclInt)
149+
150+
var
151+
p1 {.noinit.}: BnG1
152+
p2 {.noinit.}: BnG2
153+
154+
for i in 0..<count:
155+
let s = i * 192
156+
157+
# Loading AffinePoint[G1], bytes from [0..63]
158+
if not p1.deserialize(c.msg.data.toOpenArray(s, s+63)):
159+
return err(prcErr(PrcInvalidPoint))
160+
161+
# Loading AffinePoint[G2], bytes from [64..191]
162+
if not p2.deserialize(c.msg.data.toOpenArray(s+64, s+191)):
163+
return err(prcErr(PrcInvalidPoint))
164+
165+
# Accumulate pairing result
166+
mclBn_pairing(tmp.addr, p1.addr, p2.addr)
167+
mclBnGT_mul(acc.addr, acc.addr, tmp.addr)
168+
169+
c.output.setLen(32)
170+
if mclBnGT_isEqual(acc.addr, one.addr) == 1.cint:
171+
# we can discard here because we supply buffer of proper size
172+
discard BNU256.one().toBytesBE(c.output)
173+
174+
ok()
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# nimbus-execution-client
2+
# Copyright (c) 2025 Status Research & Development GmbH
3+
# Licensed under either of
4+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
5+
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
6+
# at your option.
7+
# This file may not be copied, modified, or distributed except according to
8+
# those terms
9+
10+
{.push raises: [].}
11+
12+
import
13+
results,
14+
stew/assign2,
15+
bncurve/[fields, groups],
16+
./evm_errors,
17+
./types
18+
19+
func simpleDecode(dst: var FQ2, src: openArray[byte]): bool {.noinit.} =
20+
# bypassing FQ2.fromBytes
21+
# because we want to check `value > modulus`
22+
result = false
23+
if dst.c1.fromBytes(src.toOpenArray(0, 31)) and
24+
dst.c0.fromBytes(src.toOpenArray(32, 63)):
25+
result = true
26+
27+
template simpleDecode(dst: var FQ, src: openArray[byte]): bool =
28+
fromBytes(dst, src)
29+
30+
func getPoint[T: G1|G2](_: typedesc[T], data: openArray[byte]): EvmResult[Point[T]] =
31+
when T is G1:
32+
const nextOffset = 32
33+
var px, py: FQ
34+
else:
35+
const nextOffset = 64
36+
var px, py: FQ2
37+
38+
if not px.simpleDecode(data.toOpenArray(0, nextOffset - 1)):
39+
return err(prcErr(PrcInvalidPoint))
40+
if not py.simpleDecode(data.toOpenArray(nextOffset, nextOffset * 2 - 1)):
41+
return err(prcErr(PrcInvalidPoint))
42+
43+
if px.isZero() and py.isZero():
44+
ok(T.zero())
45+
else:
46+
var ap: AffinePoint[T]
47+
if not ap.init(px, py):
48+
return err(prcErr(PrcInvalidPoint))
49+
ok(ap.toJacobian())
50+
51+
func getFR(data: openArray[byte]): EvmResult[FR] =
52+
var res: FR
53+
if not res.fromBytes2(data):
54+
return err(prcErr(PrcInvalidPoint))
55+
ok(res)
56+
57+
func bn256ecAddImpl*(c: Computation): EvmResultVoid =
58+
var
59+
input: array[128, byte]
60+
# Padding data
61+
let len = min(c.msg.data.len, 128) - 1
62+
assign(input.toOpenArray(0, len), c.msg.data.toOpenArray(0, len))
63+
let
64+
p1 = ? G1.getPoint(input.toOpenArray(0, 63))
65+
p2 = ? G1.getPoint(input.toOpenArray(64, 127))
66+
apo = (p1 + p2).toAffine()
67+
68+
c.output.setLen(64)
69+
if isSome(apo):
70+
# we can discard here because we supply proper buffer
71+
discard apo.get().toBytes(c.output)
72+
73+
ok()
74+
75+
func bn256ecMulImpl*(c: Computation): EvmResultVoid =
76+
var
77+
input: array[96, byte]
78+
# Padding data
79+
let len = min(c.msg.data.len, 96) - 1
80+
assign(input.toOpenArray(0, len), c.msg.data.toOpenArray(0, len))
81+
let
82+
p1 = ? G1.getPoint(input.toOpenArray(0, 63))
83+
fr = ? getFR(input.toOpenArray(64, 95))
84+
apo = (p1 * fr).toAffine()
85+
86+
c.output.setLen(64)
87+
if isSome(apo):
88+
# we can discard here because we supply buffer of proper size
89+
discard apo.get().toBytes(c.output)
90+
91+
ok()
92+
93+
func bn256ecPairingImpl*(c: Computation): EvmResultVoid =
94+
let msglen = c.msg.data.len
95+
if msglen == 0:
96+
# we can discard here because we supply buffer of proper size
97+
c.output.setLen(32)
98+
discard BNU256.one().toBytesBE(c.output)
99+
else:
100+
# Calculate number of pairing pairs
101+
let count = msglen div 192
102+
# Pairing accumulator
103+
var acc = FQ12.one()
104+
105+
for i in 0..<count:
106+
let
107+
s = i * 192
108+
# Loading AffinePoint[G1], bytes from [0..63]
109+
p1 = ?G1.getPoint(c.msg.data.toOpenArray(s, s + 63))
110+
# Loading AffinePoint[G2], bytes from [64..191]
111+
p2 = ?G2.getPoint(c.msg.data.toOpenArray(s + 64, s + 191))
112+
113+
# Accumulate pairing result
114+
acc = acc * pairing(p1, p2)
115+
116+
c.output.setLen(32)
117+
if acc == FQ12.one():
118+
# we can discard here because we supply buffer of proper size
119+
discard BNU256.one().toBytesBE(c.output)
120+
121+
ok()

0 commit comments

Comments
 (0)