From 1e6cade45c2056d46242cbd7b5eb11df38b3b2da Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Tue, 15 Apr 2025 20:17:35 -0300 Subject: [PATCH 1/5] zkevm: add bytecode worst case Signed-off-by: Ignacio Hagopian --- tests/zkevm/__init__.py | 3 ++ tests/zkevm/test_worst_bytecode.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/zkevm/__init__.py create mode 100644 tests/zkevm/test_worst_bytecode.py diff --git a/tests/zkevm/__init__.py b/tests/zkevm/__init__.py new file mode 100644 index 00000000000..304df80e5fb --- /dev/null +++ b/tests/zkevm/__init__.py @@ -0,0 +1,3 @@ +""" +abstract: Tests for zkVMs +""" diff --git a/tests/zkevm/test_worst_bytecode.py b/tests/zkevm/test_worst_bytecode.py new file mode 100644 index 00000000000..693ff1724ed --- /dev/null +++ b/tests/zkevm/test_worst_bytecode.py @@ -0,0 +1,59 @@ +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_CONTRACT_SIZE = 24 * 1024 +GAS_LIMIT = 36_000_000 +MAX_NUM_CONTRACT_CALLS = (GAS_LIMIT - 21_000) // (3 + 2600) + + +@pytest.mark.zkevm +@pytest.mark.valid_from("Cancun") +@pytest.mark.parametrize( + "num_called_contracts", + [ + 1, + # 10, + # MAX_NUM_CONTRACT_CALLS + ], +) +def test_worst_bytecode( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + num_called_contracts : int, +): + """ + Test a block execution calling contracts with the maximum size of bytecode. + """ + env = Environment(gas_limit=GAS_LIMIT) + + contract_addrs = [] + for i in range(num_called_contracts): + code = Op.JUMPDEST * (MAX_CONTRACT_SIZE - 1 - 10) + Op.PUSH10(i) + contract_addrs.append(pre.deploy_contract(code=code)) + + attack_code = sum([Op.EXTCODESIZE(contract_addrs[i]) for i in range(num_called_contracts)]) + attack_contract = pre.deploy_contract(code=attack_code) + + tx = Transaction( + to=attack_contract, + 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 3a511844e928c78fd14a22e0a7f83524de826d3a Mon Sep 17 00:00:00 2001 From: raxhvl Date: Sat, 19 Apr 2025 12:31:06 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(tests):=20ren?= =?UTF-8?q?ame=20zkevm=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...t_worst_bytecode.py => test_proof_size.py} | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) rename tests/zkevm/{test_worst_bytecode.py => test_proof_size.py} (55%) diff --git a/tests/zkevm/test_worst_bytecode.py b/tests/zkevm/test_proof_size.py similarity index 55% rename from tests/zkevm/test_worst_bytecode.py rename to tests/zkevm/test_proof_size.py index 693ff1724ed..a12e14e3a81 100644 --- a/tests/zkevm/test_worst_bytecode.py +++ b/tests/zkevm/test_proof_size.py @@ -1,15 +1,36 @@ +""" +A series of stress tests that assess the ability of a specific +opcode or precompile to process large proofs in a block. + +A large proof is created by deploying a contract with +maximum allowed bytecode. + +This proof is then "ingested" by an opcode or precompile. + +These tests first compute the "ingestion cost" for the proof. +Next, they attempt to perform the maximum number of possible +ingestions in a block. +""" + 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" REFERENCE_SPEC_VERSION = "TODO" -MAX_CONTRACT_SIZE = 24 * 1024 + +############################## +# # +# Config # +# # +############################## GAS_LIMIT = 36_000_000 + +KiB = 1024 +CONTRACT_BYTECODE_MAX_SIZE = 24 * KiB MAX_NUM_CONTRACT_CALLS = (GAS_LIMIT - 21_000) // (3 + 2600) @@ -23,20 +44,18 @@ # MAX_NUM_CONTRACT_CALLS ], ) -def test_worst_bytecode( +def test_via_opcode( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, - num_called_contracts : int, + num_called_contracts: int, ): - """ - Test a block execution calling contracts with the maximum size of bytecode. - """ + """Test zkEVM proof size limits using a specific opcode.""" env = Environment(gas_limit=GAS_LIMIT) contract_addrs = [] for i in range(num_called_contracts): - code = Op.JUMPDEST * (MAX_CONTRACT_SIZE - 1 - 10) + Op.PUSH10(i) + code = Op.JUMPDEST * (CONTRACT_BYTECODE_MAX_SIZE - 1 - 10) + Op.PUSH10(i) contract_addrs.append(pre.deploy_contract(code=code)) attack_code = sum([Op.EXTCODESIZE(contract_addrs[i]) for i in range(num_called_contracts)]) From cd3229e500c81b3ea01086facc845e334471e8dd Mon Sep 17 00:00:00 2001 From: raxhvl Date: Sat, 19 Apr 2025 14:29:28 +0000 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20feat(tests):=20zkevm=20-=20extr?= =?UTF-8?q?act=20proofs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/zkevm/test_proof_size.py | 66 +++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/tests/zkevm/test_proof_size.py b/tests/zkevm/test_proof_size.py index a12e14e3a81..510a30026da 100644 --- a/tests/zkevm/test_proof_size.py +++ b/tests/zkevm/test_proof_size.py @@ -2,20 +2,29 @@ A series of stress tests that assess the ability of a specific opcode or precompile to process large proofs in a block. -A large proof is created by deploying a contract with +Large proofs are created by deploying contracts with maximum allowed bytecode. -This proof is then "ingested" by an opcode or precompile. +These proofs are then "ingested" by an opcode or precompile. -These tests first compute the "ingestion cost" for the proof. -Next, they attempt to perform the maximum number of possible -ingestions in a block. +First, the tests compute the "ingestion cost" for the proof. +Next, they attempt to ingest the maximum possible number of +proofs in a block with a given gas limit. """ +from typing import List + import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction +from ethereum_test_tools import ( + Address, + Alloc, + Block, + BlockchainTestFiller, + Environment, + Transaction, +) from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "TODO" @@ -27,15 +36,38 @@ # Config # # # ############################## -GAS_LIMIT = 36_000_000 - +GAS_LIMITS = [ + 30_000_000, +] KiB = 1024 CONTRACT_BYTECODE_MAX_SIZE = 24 * KiB -MAX_NUM_CONTRACT_CALLS = (GAS_LIMIT - 21_000) // (3 + 2600) +############################## +# # +# Test Helpers # +# # +############################## +def get_proofs(pre: Alloc, proof_count: int) -> List[Address]: + """ + Generate a list of proof addresses by deploying contracts + with maximum allowed bytecode size. + """ + proofs = [] + for i in range(proof_count): + code = Op.JUMPDEST * (CONTRACT_BYTECODE_MAX_SIZE - 1 - 10) + Op.PUSH10(i) + proofs.append(pre.deploy_contract(code=code)) + return proofs + + +############################## +# # +# Test Cases # +# # +############################## @pytest.mark.zkevm @pytest.mark.valid_from("Cancun") +@pytest.mark.parametrize("gas_limit", GAS_LIMITS) @pytest.mark.parametrize( "num_called_contracts", [ @@ -48,22 +80,22 @@ def test_via_opcode( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, + gas_limit: int, num_called_contracts: int, ): """Test zkEVM proof size limits using a specific opcode.""" - env = Environment(gas_limit=GAS_LIMIT) + env = Environment(gas_limit=gas_limit) - contract_addrs = [] - for i in range(num_called_contracts): - code = Op.JUMPDEST * (CONTRACT_BYTECODE_MAX_SIZE - 1 - 10) + Op.PUSH10(i) - contract_addrs.append(pre.deploy_contract(code=code)) + # MAX_NUM_CONTRACT_CALLS = (gas_limit - 21_000) // (3 + 2600) - attack_code = sum([Op.EXTCODESIZE(contract_addrs[i]) for i in range(num_called_contracts)]) + proof_addresses = get_proofs(pre, num_called_contracts) + + attack_code = sum([Op.EXTCODESIZE(proof_addresses[i]) for i in range(num_called_contracts)]) attack_contract = pre.deploy_contract(code=attack_code) tx = Transaction( to=attack_contract, - gas_limit=GAS_LIMIT, + gas_limit=gas_limit, gas_price=10, sender=pre.fund_eoa(), data=[], @@ -76,3 +108,5 @@ def test_via_opcode( post={}, blocks=[Block(txs=[tx])], ) + + # TODO: Assert that ingestion cost is computed correctly From ad4e4e90939eeed42d71daf45407c4eed04882b1 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Sun, 20 Apr 2025 13:57:16 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=A8=20feat(zkevm):=20zkevm=20-=20proo?= =?UTF-8?q?f=20eater?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/zkevm/test_proof_size.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/zkevm/test_proof_size.py b/tests/zkevm/test_proof_size.py index 510a30026da..a19db04330d 100644 --- a/tests/zkevm/test_proof_size.py +++ b/tests/zkevm/test_proof_size.py @@ -60,6 +60,13 @@ def get_proofs(pre: Alloc, proof_count: int) -> List[Address]: return proofs +def get_proof_eater(pre: Alloc, proof_count: int) -> Address: + """Generate a proof eater contract that ingests a given proofs.""" + proofs = get_proofs(pre, proof_count) + code = sum([Op.EXTCODESIZE(proofs[i]) for i in range(proof_count)]) + return pre.deploy_contract(code=code) + + ############################## # # # Test Cases # @@ -88,13 +95,8 @@ def test_via_opcode( # MAX_NUM_CONTRACT_CALLS = (gas_limit - 21_000) // (3 + 2600) - proof_addresses = get_proofs(pre, num_called_contracts) - - attack_code = sum([Op.EXTCODESIZE(proof_addresses[i]) for i in range(num_called_contracts)]) - attack_contract = pre.deploy_contract(code=attack_code) - tx = Transaction( - to=attack_contract, + to=get_proof_eater(pre, num_called_contracts), gas_limit=gas_limit, gas_price=10, sender=pre.fund_eoa(), From 96685834898a03000f9c24f7f6198a4fb48feb38 Mon Sep 17 00:00:00 2001 From: raxhvl Date: Mon, 21 Apr 2025 09:01:55 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=A8=20feat:=20zkevm=20-=20nom=20nom?= =?UTF-8?q?=20nom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/zkevm/__init__.py | 4 +- tests/zkevm/test_proof_size.py | 110 ++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/tests/zkevm/__init__.py b/tests/zkevm/__init__.py index 304df80e5fb..3c04cc76c5b 100644 --- a/tests/zkevm/__init__.py +++ b/tests/zkevm/__init__.py @@ -1,3 +1 @@ -""" -abstract: Tests for zkVMs -""" +"""Tests for zkEVM.""" diff --git a/tests/zkevm/test_proof_size.py b/tests/zkevm/test_proof_size.py index a19db04330d..cfebae7690f 100644 --- a/tests/zkevm/test_proof_size.py +++ b/tests/zkevm/test_proof_size.py @@ -5,23 +5,21 @@ Large proofs are created by deploying contracts with maximum allowed bytecode. -These proofs are then "ingested" by an opcode or precompile. - -First, the tests compute the "ingestion cost" for the proof. -Next, they attempt to ingest the maximum possible number of -proofs in a block with a given gas limit. +Then, a pacman contract (ᗧ•••) consumes the maximum possible number of +proofs in a block with a given gas limit using an opcode or precompile. """ -from typing import List +from dataclasses import dataclass +from typing import Callable import pytest -from ethereum_test_forks import Fork from ethereum_test_tools import ( Address, Alloc, Block, BlockchainTestFiller, + Bytecode, Environment, Transaction, ) @@ -30,44 +28,70 @@ REFERENCE_SPEC_GIT_PATH = "TODO" REFERENCE_SPEC_VERSION = "TODO" - ############################## # # # Config # # # ############################## -GAS_LIMITS = [ - 30_000_000, -] -KiB = 1024 -CONTRACT_BYTECODE_MAX_SIZE = 24 * KiB +MAX_CODE_SIZE = 24 * 1024 # 24 KiB +GAS_LIMITS = [30_000_000, 60_000_000, 100_000_000, 300_000_000] -############################## -# # -# Test Helpers # -# # -############################## -def get_proofs(pre: Alloc, proof_count: int) -> List[Address]: +@dataclass +class Consumer: """ - Generate a list of proof addresses by deploying contracts - with maximum allowed bytecode size. + Consumer configuration for eating proofs. + Each consumer has a specific gas appetite and bytecode. """ - proofs = [] - for i in range(proof_count): - code = Op.JUMPDEST * (CONTRACT_BYTECODE_MAX_SIZE - 1 - 10) + Op.PUSH10(i) - proofs.append(pre.deploy_contract(code=code)) - return proofs - -def get_proof_eater(pre: Alloc, proof_count: int) -> Address: - """Generate a proof eater contract that ingests a given proofs.""" - proofs = get_proofs(pre, proof_count) - code = sum([Op.EXTCODESIZE(proofs[i]) for i in range(proof_count)]) - return pre.deploy_contract(code=code) + opcode: Op + """The opcode this consumer uses to eat proofs.""" + gas_cost: int + """The gas appetite for consuming a single proof.""" + generate_bytecode: Callable[[Address], Bytecode] + """A lambda function that generates the bytecode for the eating proof.""" + + def __str__(self) -> str: + """Str repr.""" + return f"{self.opcode}" + + def create_pacman(self, pre: Alloc, gas_limit: int) -> Address: + """ + Generate a pacman contract that ingests proofs using this opcode. + ᗧ••• nom nom nom. + """ + proof_count = (gas_limit - 21_000) // self.gas_cost + proof_count = 5 # TODO: REMOVE THIS LIMIT + + # Generate proofs + proofs = [ + pre.deploy_contract(code=Op.JUMPDEST * (MAX_CODE_SIZE - 11) + Op.PUSH10(i)) + for i in range(proof_count) + ] + + # Generate bytecode using the opcode's configuration + code = sum(self.generate_bytecode(proof) for proof in proofs) + if not code: + raise ValueError("Generated code is empty") + + return pre.deploy_contract(code=code) + + +CONSUMERS = [ + Consumer( + opcode=Op.EXTCODEHASH, + gas_cost=2603, # PUSH20[3] + EXTCODEHASH[2600] + generate_bytecode=lambda proof: Op.EXTCODEHASH(proof), + ), + Consumer( + opcode=Op.EXTCODESIZE, + gas_cost=2603, # PUSH20[3] + EXTCODESIZE[2600] + generate_bytecode=lambda proof: Op.EXTCODESIZE(proof), + ), +] -############################## +############################# # # # Test Cases # # # @@ -75,28 +99,16 @@ def get_proof_eater(pre: Alloc, proof_count: int) -> Address: @pytest.mark.zkevm @pytest.mark.valid_from("Cancun") @pytest.mark.parametrize("gas_limit", GAS_LIMITS) -@pytest.mark.parametrize( - "num_called_contracts", - [ - 1, - # 10, - # MAX_NUM_CONTRACT_CALLS - ], -) +@pytest.mark.parametrize("consumer", CONSUMERS, ids=str) def test_via_opcode( blockchain_test: BlockchainTestFiller, pre: Alloc, - fork: Fork, gas_limit: int, - num_called_contracts: int, + consumer: Consumer, ): """Test zkEVM proof size limits using a specific opcode.""" - env = Environment(gas_limit=gas_limit) - - # MAX_NUM_CONTRACT_CALLS = (gas_limit - 21_000) // (3 + 2600) - tx = Transaction( - to=get_proof_eater(pre, num_called_contracts), + to=consumer.create_pacman(pre, gas_limit), gas_limit=gas_limit, gas_price=10, sender=pre.fund_eoa(), @@ -105,7 +117,7 @@ def test_via_opcode( ) blockchain_test( - env=env, + env=Environment(gas_limit=gas_limit), pre=pre, post={}, blocks=[Block(txs=[tx])],