From c35f25999b08eb736a0bc5e772f8777dcebe5876 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 14:56:10 -0300 Subject: [PATCH 1/8] zkEVM: add keccak attack Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 106 ++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/zkevm/test_worst_compute.py diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py new file mode 100644 index 00000000000..baf9d74cd77 --- /dev/null +++ b/tests/zkevm/test_worst_compute.py @@ -0,0 +1,106 @@ +""" +abstract: Tests zkEVMs worst-case compute scenarios. + Tests zkEVMs worst-case compute scenarios. + +Tests running worst-case compute opcodes and precompile scenarios for zkEVMs. +""" + +import math + +import pytest + +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction +from ethereum_test_tools.vm.opcode import Opcodes as Op + +REFERENCE_SPEC_GIT_PATH = "TODO" +REFERENCE_SPEC_VERSION = "TODO" + +MAX_CODE_SIZE = 24 * 1024 +KECCAK_RATE = 136 + + +@pytest.mark.zkevm +@pytest.mark.valid_from("Cancun") +@pytest.mark.parametrize( + "gas_limit", + [ + 36_000_000, + 60_000_000, + 100_000_000, + 300_000_000, + ], +) +def test_worst_keccak( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_limit: int, +): + """Test running a block with as many KECCAK256 permutations as possible.""" + env = Environment(gas_limit=gas_limit) + + # Intrinsic gas cost is paid once. + intrinsic_gasc_calculator = fork.transaction_intrinsic_cost_calculator() + available_gas = gas_limit - intrinsic_gasc_calculator() + + gsc = fork.gas_costs() + mem_exp_gas_calculator = fork.memory_expansion_gas_calculator() + + # Discover the optimal input size to maximize keccak-permutations, not keccak calls. + # The complication of the discovery arises from the non-linear gas cost of memory expansion. + max_keccak_perm_per_block = 0 + optimal_input_length = 0 + for i in range(1, 1_000_000, 32): + iteration_gas_cost = ( + 2 * gsc.G_VERY_LOW # PUSHN + PUSH1 + + gsc.G_KECCAK_256 # KECCAK256 static cost + + math.ceil(i / 32) * gsc.G_KECCAK_256_WORD # KECCAK256 dynamic cost + + gsc.G_BASE # POP + ) + available_gas_after_expansion = max(0, available_gas - mem_exp_gas_calculator(new_bytes=i)) + num_keccak_calls = available_gas_after_expansion // iteration_gas_cost + num_keccak_permutations = num_keccak_calls * math.ceil(i / KECCAK_RATE) + + if num_keccak_permutations > max_keccak_perm_per_block: + max_keccak_perm_per_block = num_keccak_permutations + optimal_input_length = i + + # max_iters_loop contains how many keccak calls can be done per loop. + # The loop is as big as possible bounded by the maximum code size. + # + # The loop structure is: JUMPDEST + [attack iteration] + PUSH0 + JUMP + # + # Now calculate available gas for [attack iteration]: + # Numerator = MAX_CODE_SIZE-3. The -3 is for the JUMPDEST, PUSH0 and JUMP. + # Denominator = (PUSHN + PUSH1 + KECCAK256 + POP) + PUSH1_DATA + PUSHN_DATA + # TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the attack, + # whenever this is fixed adjust accordingly. + max_iters_loop = (MAX_CODE_SIZE - 3) // (4 + 1 + (optimal_input_length.bit_length() + 7) // 8) + code = ( + Op.JUMPDEST + + sum([Op.SHA3(0, optimal_input_length) + Op.POP] * max_iters_loop) + + Op.PUSH0 + + Op.JUMP + ) + if len(code) > MAX_CODE_SIZE: + # Must never happen, but keep it as a sanity check. + raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}") + + code_address = pre.deploy_contract(code=bytes(code)) + + tx = Transaction( + to=code_address, + gas_limit=gas_limit, + gas_price=10, + sender=pre.fund_eoa(), + data=[], + value=0, + ) + + blockchain_test( + env=env, + pre=pre, + post={}, + blocks=[Block(txs=[tx])], + ) From d3deb143606ee0559f2cf091f832d98fb3acd08c Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 14:57:52 -0300 Subject: [PATCH 2/8] lints Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index baf9d74cd77..a1b36690b32 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -74,8 +74,8 @@ def test_worst_keccak( # Now calculate available gas for [attack iteration]: # Numerator = MAX_CODE_SIZE-3. The -3 is for the JUMPDEST, PUSH0 and JUMP. # Denominator = (PUSHN + PUSH1 + KECCAK256 + POP) + PUSH1_DATA + PUSHN_DATA - # TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the attack, - # whenever this is fixed adjust accordingly. + # TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the + # attack, whenever this is fixed adjust accordingly. max_iters_loop = (MAX_CODE_SIZE - 3) // (4 + 1 + (optimal_input_length.bit_length() + 7) // 8) code = ( Op.JUMPDEST From d016275ffd3c671d7694c6c14f936760a6e9ec43 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 15:31:45 -0300 Subject: [PATCH 3/8] only leave 36M gas limit Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index a1b36690b32..606b64be4fc 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -10,7 +10,8 @@ import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction +from ethereum_test_tools import (Alloc, Block, BlockchainTestFiller, + Environment, Transaction) from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "TODO" @@ -26,9 +27,6 @@ "gas_limit", [ 36_000_000, - 60_000_000, - 100_000_000, - 300_000_000, ], ) def test_worst_keccak( From 13c0f2e9c02ad8e354b514384a5e6a5532684b56 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 15:46:29 -0300 Subject: [PATCH 4/8] lints Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 606b64be4fc..a3be48cf182 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -10,8 +10,7 @@ import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import (Alloc, Block, BlockchainTestFiller, - Environment, Transaction) +from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "TODO" From fa934a8ef255d3e6e7fcd2eef5cbaf31ff9da168 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 17:04:22 -0300 Subject: [PATCH 5/8] feedback Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index a3be48cf182..30989a1f534 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -6,6 +6,7 @@ """ import math +import time import pytest @@ -38,12 +39,14 @@ def test_worst_keccak( env = Environment(gas_limit=gas_limit) # Intrinsic gas cost is paid once. - intrinsic_gasc_calculator = fork.transaction_intrinsic_cost_calculator() - available_gas = gas_limit - intrinsic_gasc_calculator() + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() + available_gas = gas_limit - intrinsic_gas_calculator() gsc = fork.gas_costs() mem_exp_gas_calculator = fork.memory_expansion_gas_calculator() + start = time.time() # Start time in seconds (float) + # Discover the optimal input size to maximize keccak-permutations, not keccak calls. # The complication of the discovery arises from the non-linear gas cost of memory expansion. max_keccak_perm_per_block = 0 @@ -55,14 +58,23 @@ def test_worst_keccak( + math.ceil(i / 32) * gsc.G_KECCAK_256_WORD # KECCAK256 dynamic cost + gsc.G_BASE # POP ) + # From the available gas, we substract the mem expansion costs considering we know the current input size length i. available_gas_after_expansion = max(0, available_gas - mem_exp_gas_calculator(new_bytes=i)) + # Calculate how many calls we can do. num_keccak_calls = available_gas_after_expansion // iteration_gas_cost + # KECCAK does 1 permutation every 136 bytes. num_keccak_permutations = num_keccak_calls * math.ceil(i / KECCAK_RATE) + # If we found an input size that is better (reg permutations/gas), then save it. if num_keccak_permutations > max_keccak_perm_per_block: max_keccak_perm_per_block = num_keccak_permutations optimal_input_length = i + end = time.time() # End time + + # Calculate duration in milliseconds + duration_ms = (end - start) * 1000 + print(f"Execution time: {duration_ms:.2f} ms") # max_iters_loop contains how many keccak calls can be done per loop. # The loop is as big as possible bounded by the maximum code size. # From fcf838fba4b8001f74f2edea5515636d64bd82dc Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 17:16:13 -0300 Subject: [PATCH 6/8] Update tests/zkevm/test_worst_compute.py Co-authored-by: Mario Vega --- tests/zkevm/test_worst_compute.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 30989a1f534..c0877ee4808 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -85,12 +85,14 @@ def test_worst_keccak( # Denominator = (PUSHN + PUSH1 + KECCAK256 + POP) + PUSH1_DATA + PUSHN_DATA # TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the # attack, whenever this is fixed adjust accordingly. - max_iters_loop = (MAX_CODE_SIZE - 3) // (4 + 1 + (optimal_input_length.bit_length() + 7) // 8) + start_code = Op.JUMPDEST + Op.PUSH20[optimal_input_length] + loop_code = Op.POP(Op.SHA3(Op.PUSH0, Op.DUP1)) + end_code = Op.POP + Op.JUMP(Op.PUSH0) + max_iters_loop = (MAX_CODE_SIZE - (len(start_code) + len(end_code))) // len(loop_code) code = ( - Op.JUMPDEST - + sum([Op.SHA3(0, optimal_input_length) + Op.POP] * max_iters_loop) - + Op.PUSH0 - + Op.JUMP + start_code + + (loop_code * max_iters_loop) + + end_code ) if len(code) > MAX_CODE_SIZE: # Must never happen, but keep it as a sanity check. From 66e032e7057da680b6e0ac7b03771479112312b5 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 17:16:53 -0300 Subject: [PATCH 7/8] cleanup Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index c0877ee4808..3520d9ec49b 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -6,7 +6,6 @@ """ import math -import time import pytest @@ -45,8 +44,6 @@ def test_worst_keccak( gsc = fork.gas_costs() mem_exp_gas_calculator = fork.memory_expansion_gas_calculator() - start = time.time() # Start time in seconds (float) - # Discover the optimal input size to maximize keccak-permutations, not keccak calls. # The complication of the discovery arises from the non-linear gas cost of memory expansion. max_keccak_perm_per_block = 0 @@ -70,11 +67,6 @@ def test_worst_keccak( max_keccak_perm_per_block = num_keccak_permutations optimal_input_length = i - end = time.time() # End time - - # Calculate duration in milliseconds - duration_ms = (end - start) * 1000 - print(f"Execution time: {duration_ms:.2f} ms") # max_iters_loop contains how many keccak calls can be done per loop. # The loop is as big as possible bounded by the maximum code size. # @@ -89,11 +81,7 @@ def test_worst_keccak( loop_code = Op.POP(Op.SHA3(Op.PUSH0, Op.DUP1)) end_code = Op.POP + Op.JUMP(Op.PUSH0) max_iters_loop = (MAX_CODE_SIZE - (len(start_code) + len(end_code))) // len(loop_code) - code = ( - start_code - + (loop_code * max_iters_loop) - + end_code - ) + code = start_code + (loop_code * max_iters_loop) + end_code if len(code) > MAX_CODE_SIZE: # Must never happen, but keep it as a sanity check. raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}") From 9bebb72733a733e32dc66f77659752fac4d29879 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 1 May 2025 17:21:20 -0300 Subject: [PATCH 8/8] lints Signed-off-by: Ignacio Hagopian --- tests/zkevm/test_worst_compute.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 3520d9ec49b..fd45db4bcab 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -55,7 +55,8 @@ def test_worst_keccak( + math.ceil(i / 32) * gsc.G_KECCAK_256_WORD # KECCAK256 dynamic cost + gsc.G_BASE # POP ) - # From the available gas, we substract the mem expansion costs considering we know the current input size length i. + # From the available gas, we substract the mem expansion costs considering we know the + # current input size length i. available_gas_after_expansion = max(0, available_gas - mem_exp_gas_calculator(new_bytes=i)) # Calculate how many calls we can do. num_keccak_calls = available_gas_after_expansion // iteration_gas_cost