From 30dea1bb086f8e263261adc66bccbb681335d5d9 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 25 Jun 2025 22:02:48 +0200 Subject: [PATCH 1/7] feat(zkevm): add log opcode worst case --- tests/zkevm/test_worst_compute.py | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 512a3d9f505..3957aa464f4 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -2257,3 +2257,75 @@ def test_worst_push( post={}, tx=tx, ) + + +@pytest.mark.parametrize( + "opcode", + [ + pytest.param(Op.LOG0, id="log0"), + pytest.param(Op.LOG1, id="log1"), + pytest.param(Op.LOG2, id="log2"), + pytest.param(Op.LOG3, id="log3"), + pytest.param(Op.LOG4, id="log4"), + ], +) +@pytest.mark.parametrize( + "size", + [ + 0, # 0 bytes + 10, # 10 bytes + 100, # 100 bytes + 1 * 1024, # 1KiB + 10 * 1024, # 10KiB + 100 * 1024, # 100KiB + 1024 * 1024, # 1MiB + ], +) +@pytest.mark.parametrize("empty_value", [True, False]) +@pytest.mark.parametrize("fixed_offset", [True, False]) +def test_worst_log_opcodes( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + opcode: Opcode, + empty_value: bool, + size: int, + fixed_offset: bool, +): + """Test running a block with as many LOG opcodes as possible.""" + env = Environment() + max_code_size = fork.max_code_size() + + calldata = ( + Op.PUSH0 + if empty_value + else Op.PUSH32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + ) + topic_count = len(opcode.kwargs or []) - 2 + + offset = 0 if fixed_offset else Op.MOD(Op.GAS, 7) + + code_sequence = ( + Op.DUP1 * topic_count # Push topics + + Op.PUSH32(size) # Push memory size + + Op.PUSH32(offset) # Push memory offset + + opcode # Add the LOG opcode + ) + + code = code_loop_precompile_call(calldata, code_sequence, fork) + assert len(code) <= max_code_size + + code_address = pre.deploy_contract(code=code) + + tx = Transaction( + to=code_address, + gas_limit=env.gas_limit, + sender=pre.fund_eoa(), + ) + + state_test( + env=env, + pre=pre, + post={}, + tx=tx, + ) From 20c207b07d52f00eaea06974bf84172add05bc7d Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Wed, 25 Jun 2025 22:25:42 +0200 Subject: [PATCH 2/7] fix(zkevm): logic issue --- tests/zkevm/test_worst_compute.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 3957aa464f4..f4968167a1c 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -2301,16 +2301,12 @@ def test_worst_log_opcodes( if empty_value else Op.PUSH32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) ) + topic_count = len(opcode.kwargs or []) - 2 - offset = 0 if fixed_offset else Op.MOD(Op.GAS, 7) + offset = Op.PUSH0 if fixed_offset else Op.MOD(Op.GAS, 7) - code_sequence = ( - Op.DUP1 * topic_count # Push topics - + Op.PUSH32(size) # Push memory size - + Op.PUSH32(offset) # Push memory offset - + opcode # Add the LOG opcode - ) + code_sequence = Op.DUP1 * topic_count + Op.PUSH32(size) + offset + opcode code = code_loop_precompile_call(calldata, code_sequence, fork) assert len(code) <= max_code_size From 34749f308129dec55a02c1602aebc9b658e27ae2 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Fri, 27 Jun 2025 22:01:19 +0200 Subject: [PATCH 3/7] refactor(zkevm): update test parameters for log opcodes --- tests/zkevm/test_worst_compute.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index f4968167a1c..7204819b18e 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -2273,22 +2273,17 @@ def test_worst_push( "size", [ 0, # 0 bytes - 10, # 10 bytes - 100, # 100 bytes - 1 * 1024, # 1KiB - 10 * 1024, # 10KiB - 100 * 1024, # 100KiB 1024 * 1024, # 1MiB ], ) -@pytest.mark.parametrize("empty_value", [True, False]) +@pytest.mark.parametrize("empty_topic", [True, False]) @pytest.mark.parametrize("fixed_offset", [True, False]) def test_worst_log_opcodes( state_test: StateTestFiller, pre: Alloc, fork: Fork, opcode: Opcode, - empty_value: bool, + empty_topic: bool, size: int, fixed_offset: bool, ): @@ -2296,11 +2291,7 @@ def test_worst_log_opcodes( env = Environment() max_code_size = fork.max_code_size() - calldata = ( - Op.PUSH0 - if empty_value - else Op.PUSH32(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) - ) + calldata = Op.PUSH0 if empty_topic else Op.PUSH32(2**256 - 1) topic_count = len(opcode.kwargs or []) - 2 From 7fa06ab3e3d729851372fc825558179329c62ad0 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Fri, 27 Jun 2025 22:13:32 +0200 Subject: [PATCH 4/7] refactor(zkevm): move test path for worst log opcodes --- tests/zkevm/test_worst_compute.py | 59 ----------------------- tests/zkevm/test_worst_opcode.py | 79 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 59 deletions(-) create mode 100644 tests/zkevm/test_worst_opcode.py diff --git a/tests/zkevm/test_worst_compute.py b/tests/zkevm/test_worst_compute.py index 7204819b18e..512a3d9f505 100644 --- a/tests/zkevm/test_worst_compute.py +++ b/tests/zkevm/test_worst_compute.py @@ -2257,62 +2257,3 @@ def test_worst_push( post={}, tx=tx, ) - - -@pytest.mark.parametrize( - "opcode", - [ - pytest.param(Op.LOG0, id="log0"), - pytest.param(Op.LOG1, id="log1"), - pytest.param(Op.LOG2, id="log2"), - pytest.param(Op.LOG3, id="log3"), - pytest.param(Op.LOG4, id="log4"), - ], -) -@pytest.mark.parametrize( - "size", - [ - 0, # 0 bytes - 1024 * 1024, # 1MiB - ], -) -@pytest.mark.parametrize("empty_topic", [True, False]) -@pytest.mark.parametrize("fixed_offset", [True, False]) -def test_worst_log_opcodes( - state_test: StateTestFiller, - pre: Alloc, - fork: Fork, - opcode: Opcode, - empty_topic: bool, - size: int, - fixed_offset: bool, -): - """Test running a block with as many LOG opcodes as possible.""" - env = Environment() - max_code_size = fork.max_code_size() - - calldata = Op.PUSH0 if empty_topic else Op.PUSH32(2**256 - 1) - - topic_count = len(opcode.kwargs or []) - 2 - - offset = Op.PUSH0 if fixed_offset else Op.MOD(Op.GAS, 7) - - code_sequence = Op.DUP1 * topic_count + Op.PUSH32(size) + offset + opcode - - code = code_loop_precompile_call(calldata, code_sequence, fork) - assert len(code) <= max_code_size - - code_address = pre.deploy_contract(code=code) - - tx = Transaction( - to=code_address, - gas_limit=env.gas_limit, - sender=pre.fund_eoa(), - ) - - state_test( - env=env, - pre=pre, - post={}, - tx=tx, - ) diff --git a/tests/zkevm/test_worst_opcode.py b/tests/zkevm/test_worst_opcode.py new file mode 100644 index 00000000000..74697f5637c --- /dev/null +++ b/tests/zkevm/test_worst_opcode.py @@ -0,0 +1,79 @@ +""" +abstract: Tests zkEVMs worst-case opcode scenarios. + Tests zkEVMs worst-case opcode scenarios. + +Tests running worst-case opcodes scenarios for zkEVMs. +""" + +import pytest + +from ethereum_test_forks import Fork +from ethereum_test_tools import ( + Alloc, + Environment, + StateTestFiller, + Transaction, +) +from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_vm.opcode import Opcode + +from .helpers import code_loop_precompile_call + + +@pytest.mark.parametrize( + "opcode", + [ + pytest.param(Op.LOG0, id="log0"), + pytest.param(Op.LOG1, id="log1"), + pytest.param(Op.LOG2, id="log2"), + pytest.param(Op.LOG3, id="log3"), + pytest.param(Op.LOG4, id="log4"), + ], +) +@pytest.mark.parametrize( + "size", + [ + 0, # 0 bytes + 1024 * 1024, # 1MiB + ], +) +@pytest.mark.parametrize("empty_topic", [True, False]) +@pytest.mark.parametrize("fixed_offset", [True, False]) +def test_worst_log_opcodes( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + opcode: Opcode, + empty_topic: bool, + size: int, + fixed_offset: bool, +): + """Test running a block with as many LOG opcodes as possible.""" + env = Environment() + max_code_size = fork.max_code_size() + + calldata = Op.PUSH0 if empty_topic else Op.PUSH32(2**256 - 1) + + topic_count = len(opcode.kwargs or []) - 2 + + offset = Op.PUSH0 if fixed_offset else Op.MOD(Op.GAS, 7) + + code_sequence = Op.DUP1 * topic_count + Op.PUSH32(size) + offset + opcode + + code = code_loop_precompile_call(calldata, code_sequence, fork) + assert len(code) <= max_code_size + + code_address = pre.deploy_contract(code=code) + + tx = Transaction( + to=code_address, + gas_limit=env.gas_limit, + sender=pre.fund_eoa(), + ) + + state_test( + env=env, + pre=pre, + post={}, + tx=tx, + ) From 563aa84614894bdebf03277d09db536728dc3c04 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 30 Jun 2025 11:20:14 +0200 Subject: [PATCH 5/7] refactor(zkevm): enhance test with additional parameters --- tests/zkevm/test_worst_opcode.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/zkevm/test_worst_opcode.py b/tests/zkevm/test_worst_opcode.py index 74697f5637c..fb480d0b37c 100644 --- a/tests/zkevm/test_worst_opcode.py +++ b/tests/zkevm/test_worst_opcode.py @@ -10,6 +10,7 @@ from ethereum_test_forks import Fork from ethereum_test_tools import ( Alloc, + Bytecode, Environment, StateTestFiller, Transaction, @@ -39,6 +40,7 @@ ) @pytest.mark.parametrize("empty_topic", [True, False]) @pytest.mark.parametrize("fixed_offset", [True, False]) +@pytest.mark.parametrize("non_zero_data", [True, False]) def test_worst_log_opcodes( state_test: StateTestFiller, pre: Alloc, @@ -47,18 +49,32 @@ def test_worst_log_opcodes( empty_topic: bool, size: int, fixed_offset: bool, + non_zero_data: bool, ): """Test running a block with as many LOG opcodes as possible.""" env = Environment() max_code_size = fork.max_code_size() - calldata = Op.PUSH0 if empty_topic else Op.PUSH32(2**256 - 1) + calldata = Bytecode() - topic_count = len(opcode.kwargs or []) - 2 + # For non-zero data, load into memory. + if non_zero_data: + calldata += Op.CODECOPY(dest_offset=0, offset=0, size=Op.CODESIZE) + + # Push the size value onto the stack and access it using the DUP opcode. + calldata += Op.PUSH3(size) + + # For non-empty topic, push a non-zero value for topic. + calldata += Op.PUSH0 if empty_topic else Op.PUSH32(2**256 - 1) + topic_count = len(opcode.kwargs or []) - 2 offset = Op.PUSH0 if fixed_offset else Op.MOD(Op.GAS, 7) - code_sequence = Op.DUP1 * topic_count + Op.PUSH32(size) + offset + opcode + # Calculate the appropriate DUP opcode based on topic count + # 0 topics -> DUP1, 1 topic -> DUP2, N topics -> DUP(N+1) + size_op = getattr(Op, f"DUP{topic_count + 2}") + + code_sequence = Op.DUP1 * topic_count + size_op + offset + opcode code = code_loop_precompile_call(calldata, code_sequence, fork) assert len(code) <= max_code_size From eeb84ddd997ac8d05e9ce79d862994d25e580102 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Mon, 30 Jun 2025 15:53:58 +0200 Subject: [PATCH 6/7] refactor(zkevm): update test parametrization --- tests/zkevm/test_worst_opcode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/zkevm/test_worst_opcode.py b/tests/zkevm/test_worst_opcode.py index fb480d0b37c..9646d14f076 100644 --- a/tests/zkevm/test_worst_opcode.py +++ b/tests/zkevm/test_worst_opcode.py @@ -32,15 +32,15 @@ ], ) @pytest.mark.parametrize( - "size", + "size,non_zero_data", [ - 0, # 0 bytes - 1024 * 1024, # 1MiB + pytest.param(0, False, id="0_bytes_zero_data"), + pytest.param(1024 * 1024, False, id="1_MiB_zero_data"), # 1 MiB + pytest.param(1024 * 1024, True, id="1_MiB_non_zero_data"), # 1 MiB ], ) @pytest.mark.parametrize("empty_topic", [True, False]) @pytest.mark.parametrize("fixed_offset", [True, False]) -@pytest.mark.parametrize("non_zero_data", [True, False]) def test_worst_log_opcodes( state_test: StateTestFiller, pre: Alloc, From 207adf6affd2f513cff6d85f026f93b998f6f45a Mon Sep 17 00:00:00 2001 From: Mario Vega Date: Mon, 30 Jun 2025 20:23:50 +0200 Subject: [PATCH 7/7] Apply suggestions from code review --- tests/zkevm/test_worst_opcode.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/zkevm/test_worst_opcode.py b/tests/zkevm/test_worst_opcode.py index 9646d14f076..8192407f1b6 100644 --- a/tests/zkevm/test_worst_opcode.py +++ b/tests/zkevm/test_worst_opcode.py @@ -34,19 +34,21 @@ @pytest.mark.parametrize( "size,non_zero_data", [ - pytest.param(0, False, id="0_bytes_zero_data"), - pytest.param(1024 * 1024, False, id="1_MiB_zero_data"), # 1 MiB + pytest.param(0, False, id="0_bytes_data"), + pytest.param(1024 * 1024, False, id="1_MiB_zeros_data"), # 1 MiB pytest.param(1024 * 1024, True, id="1_MiB_non_zero_data"), # 1 MiB ], ) -@pytest.mark.parametrize("empty_topic", [True, False]) +@pytest.mark.parametrize( + "zeros_topic", [pytest.param(True, id="zeros_topic"), pytest.param(False, id="non_zero_topic")] +) @pytest.mark.parametrize("fixed_offset", [True, False]) def test_worst_log_opcodes( state_test: StateTestFiller, pre: Alloc, fork: Fork, opcode: Opcode, - empty_topic: bool, + zeros_topic: bool, size: int, fixed_offset: bool, non_zero_data: bool, @@ -64,8 +66,8 @@ def test_worst_log_opcodes( # Push the size value onto the stack and access it using the DUP opcode. calldata += Op.PUSH3(size) - # For non-empty topic, push a non-zero value for topic. - calldata += Op.PUSH0 if empty_topic else Op.PUSH32(2**256 - 1) + # For non-zeros topic, push a non-zero value for topic. + calldata += Op.PUSH0 if zeros_topic else Op.PUSH32(2**256 - 1) topic_count = len(opcode.kwargs or []) - 2 offset = Op.PUSH0 if fixed_offset else Op.MOD(Op.GAS, 7)