Skip to content

Commit fce88dd

Browse files
jsignmarioevz
andauthored
new(tests/zkevm): keccak worst-case (#1497)
* zkEVM: add keccak attack Signed-off-by: Ignacio Hagopian <[email protected]> * lints Signed-off-by: Ignacio Hagopian <[email protected]> * only leave 36M gas limit Signed-off-by: Ignacio Hagopian <[email protected]> * lints Signed-off-by: Ignacio Hagopian <[email protected]> * feedback Signed-off-by: Ignacio Hagopian <[email protected]> * Update tests/zkevm/test_worst_compute.py Co-authored-by: Mario Vega <[email protected]> * cleanup Signed-off-by: Ignacio Hagopian <[email protected]> * lints Signed-off-by: Ignacio Hagopian <[email protected]> --------- Signed-off-by: Ignacio Hagopian <[email protected]> Co-authored-by: Mario Vega <[email protected]>
1 parent 453cf09 commit fce88dd

File tree

1 file changed

+106
-0
lines changed

1 file changed

+106
-0
lines changed

tests/zkevm/test_worst_compute.py

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
abstract: Tests zkEVMs worst-case compute scenarios.
3+
Tests zkEVMs worst-case compute scenarios.
4+
5+
Tests running worst-case compute opcodes and precompile scenarios for zkEVMs.
6+
"""
7+
8+
import math
9+
10+
import pytest
11+
12+
from ethereum_test_forks import Fork
13+
from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction
14+
from ethereum_test_tools.vm.opcode import Opcodes as Op
15+
16+
REFERENCE_SPEC_GIT_PATH = "TODO"
17+
REFERENCE_SPEC_VERSION = "TODO"
18+
19+
MAX_CODE_SIZE = 24 * 1024
20+
KECCAK_RATE = 136
21+
22+
23+
@pytest.mark.zkevm
24+
@pytest.mark.valid_from("Cancun")
25+
@pytest.mark.parametrize(
26+
"gas_limit",
27+
[
28+
36_000_000,
29+
],
30+
)
31+
def test_worst_keccak(
32+
blockchain_test: BlockchainTestFiller,
33+
pre: Alloc,
34+
fork: Fork,
35+
gas_limit: int,
36+
):
37+
"""Test running a block with as many KECCAK256 permutations as possible."""
38+
env = Environment(gas_limit=gas_limit)
39+
40+
# Intrinsic gas cost is paid once.
41+
intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator()
42+
available_gas = gas_limit - intrinsic_gas_calculator()
43+
44+
gsc = fork.gas_costs()
45+
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
46+
47+
# Discover the optimal input size to maximize keccak-permutations, not keccak calls.
48+
# The complication of the discovery arises from the non-linear gas cost of memory expansion.
49+
max_keccak_perm_per_block = 0
50+
optimal_input_length = 0
51+
for i in range(1, 1_000_000, 32):
52+
iteration_gas_cost = (
53+
2 * gsc.G_VERY_LOW # PUSHN + PUSH1
54+
+ gsc.G_KECCAK_256 # KECCAK256 static cost
55+
+ math.ceil(i / 32) * gsc.G_KECCAK_256_WORD # KECCAK256 dynamic cost
56+
+ gsc.G_BASE # POP
57+
)
58+
# From the available gas, we substract the mem expansion costs considering we know the
59+
# current input size length i.
60+
available_gas_after_expansion = max(0, available_gas - mem_exp_gas_calculator(new_bytes=i))
61+
# Calculate how many calls we can do.
62+
num_keccak_calls = available_gas_after_expansion // iteration_gas_cost
63+
# KECCAK does 1 permutation every 136 bytes.
64+
num_keccak_permutations = num_keccak_calls * math.ceil(i / KECCAK_RATE)
65+
66+
# If we found an input size that is better (reg permutations/gas), then save it.
67+
if num_keccak_permutations > max_keccak_perm_per_block:
68+
max_keccak_perm_per_block = num_keccak_permutations
69+
optimal_input_length = i
70+
71+
# max_iters_loop contains how many keccak calls can be done per loop.
72+
# The loop is as big as possible bounded by the maximum code size.
73+
#
74+
# The loop structure is: JUMPDEST + [attack iteration] + PUSH0 + JUMP
75+
#
76+
# Now calculate available gas for [attack iteration]:
77+
# Numerator = MAX_CODE_SIZE-3. The -3 is for the JUMPDEST, PUSH0 and JUMP.
78+
# Denominator = (PUSHN + PUSH1 + KECCAK256 + POP) + PUSH1_DATA + PUSHN_DATA
79+
# TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the
80+
# attack, whenever this is fixed adjust accordingly.
81+
start_code = Op.JUMPDEST + Op.PUSH20[optimal_input_length]
82+
loop_code = Op.POP(Op.SHA3(Op.PUSH0, Op.DUP1))
83+
end_code = Op.POP + Op.JUMP(Op.PUSH0)
84+
max_iters_loop = (MAX_CODE_SIZE - (len(start_code) + len(end_code))) // len(loop_code)
85+
code = start_code + (loop_code * max_iters_loop) + end_code
86+
if len(code) > MAX_CODE_SIZE:
87+
# Must never happen, but keep it as a sanity check.
88+
raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}")
89+
90+
code_address = pre.deploy_contract(code=bytes(code))
91+
92+
tx = Transaction(
93+
to=code_address,
94+
gas_limit=gas_limit,
95+
gas_price=10,
96+
sender=pre.fund_eoa(),
97+
data=[],
98+
value=0,
99+
)
100+
101+
blockchain_test(
102+
env=env,
103+
pre=pre,
104+
post={},
105+
blocks=[Block(txs=[tx])],
106+
)

0 commit comments

Comments
 (0)