Skip to content

Commit c35f259

Browse files
committed
zkEVM: add keccak attack
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
1 parent a740709 commit c35f259

File tree

1 file changed

+106
-0
lines changed

1 file changed

+106
-0
lines changed

tests/zkevm/test_worst_compute.py

Lines changed: 106 additions & 0 deletions
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+
60_000_000,
30+
100_000_000,
31+
300_000_000,
32+
],
33+
)
34+
def test_worst_keccak(
35+
blockchain_test: BlockchainTestFiller,
36+
pre: Alloc,
37+
fork: Fork,
38+
gas_limit: int,
39+
):
40+
"""Test running a block with as many KECCAK256 permutations as possible."""
41+
env = Environment(gas_limit=gas_limit)
42+
43+
# Intrinsic gas cost is paid once.
44+
intrinsic_gasc_calculator = fork.transaction_intrinsic_cost_calculator()
45+
available_gas = gas_limit - intrinsic_gasc_calculator()
46+
47+
gsc = fork.gas_costs()
48+
mem_exp_gas_calculator = fork.memory_expansion_gas_calculator()
49+
50+
# Discover the optimal input size to maximize keccak-permutations, not keccak calls.
51+
# The complication of the discovery arises from the non-linear gas cost of memory expansion.
52+
max_keccak_perm_per_block = 0
53+
optimal_input_length = 0
54+
for i in range(1, 1_000_000, 32):
55+
iteration_gas_cost = (
56+
2 * gsc.G_VERY_LOW # PUSHN + PUSH1
57+
+ gsc.G_KECCAK_256 # KECCAK256 static cost
58+
+ math.ceil(i / 32) * gsc.G_KECCAK_256_WORD # KECCAK256 dynamic cost
59+
+ gsc.G_BASE # POP
60+
)
61+
available_gas_after_expansion = max(0, available_gas - mem_exp_gas_calculator(new_bytes=i))
62+
num_keccak_calls = available_gas_after_expansion // iteration_gas_cost
63+
num_keccak_permutations = num_keccak_calls * math.ceil(i / KECCAK_RATE)
64+
65+
if num_keccak_permutations > max_keccak_perm_per_block:
66+
max_keccak_perm_per_block = num_keccak_permutations
67+
optimal_input_length = i
68+
69+
# max_iters_loop contains how many keccak calls can be done per loop.
70+
# The loop is as big as possible bounded by the maximum code size.
71+
#
72+
# The loop structure is: JUMPDEST + [attack iteration] + PUSH0 + JUMP
73+
#
74+
# Now calculate available gas for [attack iteration]:
75+
# Numerator = MAX_CODE_SIZE-3. The -3 is for the JUMPDEST, PUSH0 and JUMP.
76+
# Denominator = (PUSHN + PUSH1 + KECCAK256 + POP) + PUSH1_DATA + PUSHN_DATA
77+
# TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the attack,
78+
# whenever this is fixed adjust accordingly.
79+
max_iters_loop = (MAX_CODE_SIZE - 3) // (4 + 1 + (optimal_input_length.bit_length() + 7) // 8)
80+
code = (
81+
Op.JUMPDEST
82+
+ sum([Op.SHA3(0, optimal_input_length) + Op.POP] * max_iters_loop)
83+
+ Op.PUSH0
84+
+ Op.JUMP
85+
)
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)