Skip to content

Commit b5cd525

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

File tree

1 file changed

+100
-24
lines changed

1 file changed

+100
-24
lines changed

tests/zkevm/test_worst_compute.py

Lines changed: 100 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
[
@@ -938,3 +916,101 @@ def test_worst_mod(
938916
post={},
939917
blocks=[Block(txs=[tx])],
940918
)
919+
920+
921+
@pytest.mark.valid_from("Cancun")
922+
def test_worst_bn128_pairings(
923+
blockchain_test: BlockchainTestFiller,
924+
pre: Alloc,
925+
fork: Fork,
926+
):
927+
"""Test running a block with as many BN128 pairings as possible."""
928+
env = Environment()
929+
930+
base_cost = 45_000
931+
pairing_cost = 34_000
932+
size_per_pairing = 192
933+
934+
gsc = fork.gas_costs()
935+
intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
936+
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
937+
938+
# This is a theoretical maximum number of pairings that can be done in a block.
939+
# It is only used for an upper bound for calculating the optimal number of pairings below.
940+
maximum_number_of_pairings = (env.gas_limit - base_cost) // pairing_cost
941+
print(f"max_pairings: {maximum_number_of_pairings}")
942+
943+
# Discover the optimal number of pairings balancing two dimensions:
944+
# 1. Amortize the precompile base cost as much as possible.
945+
# 2. The cost of the memory expansion.
946+
max_pairings = 0
947+
optimal_per_call_num_pairings = 0
948+
for i in range(1, maximum_number_of_pairings + 1):
949+
# We'll pass all pairing arguments via calldata.
950+
available_gas_after_intrinsic = env.gas_limit - intrinsic_gas_calculator(
951+
calldata=[0xFF] * size_per_pairing * i # 0xFF is to indicate non-zero bytes.
952+
)
953+
available_gas_after_expansion = max(
954+
0,
955+
available_gas_after_intrinsic - mem_exp_gas_calculator(new_bytes=i * size_per_pairing),
956+
)
957+
958+
# This is ignoring "glue" opcodes, but helps to have a rough idea of the right
959+
# cutting point.
960+
approx_gas_cost_per_call = gsc.G_WARM_ACCOUNT_ACCESS + base_cost + i * pairing_cost
961+
962+
num_precompile_calls = available_gas_after_expansion // approx_gas_cost_per_call
963+
num_pairings_done = num_precompile_calls * i # Each precompile call does i pairings.
964+
965+
if num_pairings_done > max_pairings:
966+
max_pairings = num_pairings_done
967+
optimal_per_call_num_pairings = i
968+
969+
print(f"{max_pairings=}, {optimal_per_call_num_pairings=}")
970+
971+
calldata = Op.CALLDATACOPY(size=Op.CALLDATASIZE)
972+
attack_block = Op.POP(Op.STATICCALL(Op.GAS, 0x08, 0, Op.CALLDATASIZE, 0, 0))
973+
code = code_loop_precompile_call(calldata, attack_block)
974+
975+
code_address = pre.deploy_contract(code=code)
976+
977+
tx = Transaction(
978+
to=code_address,
979+
gas_limit=env.gas_limit,
980+
data=_generate_bn128_pairs(optimal_per_call_num_pairings, 42),
981+
sender=pre.fund_eoa(),
982+
)
983+
984+
blockchain_test(
985+
env=env,
986+
pre=pre,
987+
post={},
988+
blocks=[Block(txs=[tx])],
989+
)
990+
991+
992+
def _generate_bn128_pairs(n: int, seed: int = 0):
993+
random.seed(seed)
994+
calldata = Bytes()
995+
996+
for _ in range(n):
997+
priv_key_g1 = random.randint(1, 2**32 - 1)
998+
priv_key_g2 = random.randint(1, 2**32 - 1)
999+
1000+
point_x_affine = multiply(G1, priv_key_g1)
1001+
point_y_affine = multiply(G2, priv_key_g2)
1002+
1003+
g1_x_bytes = point_x_affine[0].n.to_bytes(32, "big")
1004+
g1_y_bytes = point_x_affine[1].n.to_bytes(32, "big")
1005+
g1_serialized = g1_x_bytes + g1_y_bytes
1006+
1007+
g2_x_c1_bytes = point_y_affine[0].coeffs[1].n.to_bytes(32, "big")
1008+
g2_x_c0_bytes = point_y_affine[0].coeffs[0].n.to_bytes(32, "big")
1009+
g2_y_c1_bytes = point_y_affine[1].coeffs[1].n.to_bytes(32, "big")
1010+
g2_y_c0_bytes = point_y_affine[1].coeffs[0].n.to_bytes(32, "big")
1011+
g2_serialized = g2_x_c1_bytes + g2_x_c0_bytes + g2_y_c1_bytes + g2_y_c0_bytes
1012+
1013+
pair_calldata = g1_serialized + g2_serialized
1014+
calldata += pair_calldata
1015+
1016+
return calldata

0 commit comments

Comments
 (0)