Skip to content

Commit 60ace5d

Browse files
committed
zkevm: add optimized bn128 pairings
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
1 parent 78fa83a commit 60ace5d

File tree

1 file changed

+99
-24
lines changed

1 file changed

+99
-24
lines changed

tests/zkevm/test_worst_compute.py

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from typing import cast
1111

1212
import pytest
13+
from py_ecc.bn128 import G1, G2, multiply
1314

15+
from ethereum_test_base_types.base_types import Bytes
1416
from ethereum_test_forks import Fork
1517
from ethereum_test_tools import (
1618
Address,
@@ -292,30 +294,6 @@ def test_worst_modexp(
292294
],
293295
id="bn128_mul",
294296
),
295-
pytest.param(
296-
0x08,
297-
[
298-
# TODO: the following are only two inputs, but this can be extended
299-
# to more inputs to amortize costs as much as possible. Additionally,
300-
# there might be worse pairings that can be used.
301-
#
302-
# First pairing
303-
"1C76476F4DEF4BB94541D57EBBA1193381FFA7AA76ADA664DD31C16024C43F59",
304-
"3034DD2920F673E204FEE2811C678745FC819B55D3E9D294E45C9B03A76AEF41",
305-
"209DD15EBFF5D46C4BD888E51A93CF99A7329636C63514396B4A452003A35BF7",
306-
"04BF11CA01483BFA8B34B43561848D28905960114C8AC04049AF4B6315A41678",
307-
"2BB8324AF6CFC93537A2AD1A445CFD0CA2A71ACD7AC41FADBF933C2A51BE344D",
308-
"120A2A4CF30C1BF9845F20C6FE39E07EA2CCE61F0C9BB048165FE5E4DE877550",
309-
# Second pairing
310-
"111E129F1CF1097710D41C4AC70FCDFA5BA2023C6FF1CBEAC322DE49D1B6DF7C",
311-
"103188585E2364128FE25C70558F1560F4F9350BAF3959E603CC91486E110936",
312-
"198E9393920D483A7260BFB731FB5D25F1AA493335A9E71297E485B7AEF312C2",
313-
"1800DEEF121F1E76426A00665E5C4479674322D4F75EDADD46DEBD5CD992F6ED",
314-
"090689D0585FF075EC9E99AD690C3395BC4B313370B38EF355ACDADCD122975B",
315-
"12C85EA5DB8C6DEB4AAB71808DCB408FE3D1E7690C43D37B4CE6CC0166FA7DAA",
316-
],
317-
id="bn128_pairing",
318-
),
319297
pytest.param(
320298
Blake2bSpec.BLAKE2_PRECOMPILE_ADDRESS,
321299
[
@@ -955,3 +933,100 @@ def test_empty_block(
955933
post={},
956934
blocks=[Block(txs=[])],
957935
)
936+
937+
938+
@pytest.mark.valid_from("Cancun")
939+
def test_worst_bn128_pairings(
940+
blockchain_test: BlockchainTestFiller,
941+
pre: Alloc,
942+
fork: Fork,
943+
):
944+
"""Test running a block with as many BN128 pairings as possible."""
945+
env = Environment()
946+
947+
base_cost = 45_000
948+
pairing_cost = 34_000
949+
size_per_pairing = 192
950+
951+
gsc = fork.gas_costs()
952+
intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
953+
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
954+
955+
# This is a theoretical maximum number of pairings that can be done in a block.
956+
# It is only used for an upper bound for calculating the optimal number of pairings below.
957+
maximum_number_of_pairings = (env.gas_limit - base_cost) // pairing_cost
958+
959+
# Discover the optimal number of pairings balancing two dimensions:
960+
# 1. Amortize the precompile base cost as much as possible.
961+
# 2. The cost of the memory expansion.
962+
max_pairings = 0
963+
optimal_per_call_num_pairings = 0
964+
for i in range(1, maximum_number_of_pairings + 1):
965+
# We'll pass all pairing arguments via calldata.
966+
available_gas_after_intrinsic = env.gas_limit - intrinsic_gas_calculator(
967+
calldata=[0xFF] * size_per_pairing * i # 0xFF is to indicate non-zero bytes.
968+
)
969+
available_gas_after_expansion = max(
970+
0,
971+
available_gas_after_intrinsic - mem_exp_gas_calculator(new_bytes=i * size_per_pairing),
972+
)
973+
974+
# This is ignoring "glue" opcodes, but helps to have a rough idea of the right
975+
# cutting point.
976+
approx_gas_cost_per_call = gsc.G_WARM_ACCOUNT_ACCESS + base_cost + i * pairing_cost
977+
978+
num_precompile_calls = available_gas_after_expansion // approx_gas_cost_per_call
979+
num_pairings_done = num_precompile_calls * i # Each precompile call does i pairings.
980+
981+
if num_pairings_done > max_pairings:
982+
max_pairings = num_pairings_done
983+
optimal_per_call_num_pairings = i
984+
985+
print(f"{max_pairings=}, {optimal_per_call_num_pairings=}")
986+
987+
calldata = Op.CALLDATACOPY(size=Op.CALLDATASIZE)
988+
attack_block = Op.POP(Op.STATICCALL(Op.GAS, 0x08, 0, Op.CALLDATASIZE, 0, 0))
989+
code = code_loop_precompile_call(calldata, attack_block)
990+
991+
code_address = pre.deploy_contract(code=code)
992+
993+
tx = Transaction(
994+
to=code_address,
995+
gas_limit=env.gas_limit,
996+
data=_generate_bn128_pairs(optimal_per_call_num_pairings, 42),
997+
sender=pre.fund_eoa(),
998+
)
999+
1000+
blockchain_test(
1001+
env=env,
1002+
pre=pre,
1003+
post={},
1004+
blocks=[Block(txs=[tx])],
1005+
)
1006+
1007+
1008+
def _generate_bn128_pairs(n: int, seed: int = 0):
1009+
random.seed(seed)
1010+
calldata = Bytes()
1011+
1012+
for _ in range(n):
1013+
priv_key_g1 = random.randint(1, 2**32 - 1)
1014+
priv_key_g2 = random.randint(1, 2**32 - 1)
1015+
1016+
point_x_affine = multiply(G1, priv_key_g1)
1017+
point_y_affine = multiply(G2, priv_key_g2)
1018+
1019+
g1_x_bytes = point_x_affine[0].n.to_bytes(32, "big")
1020+
g1_y_bytes = point_x_affine[1].n.to_bytes(32, "big")
1021+
g1_serialized = g1_x_bytes + g1_y_bytes
1022+
1023+
g2_x_c1_bytes = point_y_affine[0].coeffs[1].n.to_bytes(32, "big")
1024+
g2_x_c0_bytes = point_y_affine[0].coeffs[0].n.to_bytes(32, "big")
1025+
g2_y_c1_bytes = point_y_affine[1].coeffs[1].n.to_bytes(32, "big")
1026+
g2_y_c0_bytes = point_y_affine[1].coeffs[0].n.to_bytes(32, "big")
1027+
g2_serialized = g2_x_c1_bytes + g2_x_c0_bytes + g2_y_c1_bytes + g2_y_c0_bytes
1028+
1029+
pair_calldata = g1_serialized + g2_serialized
1030+
calldata += pair_calldata
1031+
1032+
return calldata

0 commit comments

Comments
 (0)