From a1f2153b8df3bbfd9bb78d7b937edd57ccb0effb Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:00:54 +0200 Subject: [PATCH 01/54] Add BloatNet tests Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- .../prague/eip8047_bloatnet/test_bloatnet.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/prague/eip8047_bloatnet/test_bloatnet.py diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py new file mode 100644 index 00000000000..0bf2d3fb192 --- /dev/null +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -0,0 +1,59 @@ +""" +abstract: Tests [EIP-8047 BloatNet](https://eips.ethereum.org/EIPS/eip-8047) + Test cases for [EIP-8047 BloatNet](https://eips.ethereum.org/EIPS/eip-8047)]. +""" + +import pytest + +from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Transaction +from ethereum_test_tools.vm.opcode import Opcodes as Op + +REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" +REFERENCE_SPEC_VERSION = "DUMMY_VERSION" + + +@pytest.mark.valid_from("Prague") +def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc): + """ + A test that calls a contract with many SSTOREs + + The first block will have many SSTORES that go from 0 -> 1 + and the 2nd block will have many SSTORES that go from 1 -> 2 + """ + # One gotcha is ensuring that the transaction `gas_limit` is set high + # enough to cover the gas cost of the contract execution. + + storage_slot: int = 1 + + sstore_code = Op.PUSH0 + for _ in range(100000): + sstore_code = sstore_code + Op.SSTORE(storage_slot, 1) # NOTE: this will probably push some value on the stack, but I want to increase it to reduce the amount of gas and the size of the bytecode + storage_slot += 1 + sstore_code = Op.POP + + sender = pre.fund_eoa() + print(sender) + contract_address = pre.deploy_contract( + code=sstore_code, + storage={}, + ) + + tx_0_1 = Transaction( + to=contract_address, + gas_limit=30000000, + data=b"", + value=0, + sender=sender, + ) + tx_1_2 = Transaction( + to=contract_address, + gas_limit=30000000, + data=b"", + value=0, + sender=sender, + ) + + # TODO: Modify post-state allocations here. + post = {contract_address: Account(storage={storage_slot: 0x2})} + + blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1]), Block(txs=[tx_1_2])], post=post) From 02d65b460eded007e0e03e7ace8aad2762cf7537 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:33:24 +0200 Subject: [PATCH 02/54] try building the contract Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- .../prague/eip8047_bloatnet/test_bloatnet.py | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py index 0bf2d3fb192..26cf7289a14 100644 --- a/tests/prague/eip8047_bloatnet/test_bloatnet.py +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -7,35 +7,51 @@ from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_forks import Fork REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" REFERENCE_SPEC_VERSION = "DUMMY_VERSION" @pytest.mark.valid_from("Prague") -def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc): +def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): """ A test that calls a contract with many SSTOREs The first block will have many SSTORES that go from 0 -> 1 and the 2nd block will have many SSTORES that go from 1 -> 2 """ - # One gotcha is ensuring that the transaction `gas_limit` is set high - # enough to cover the gas cost of the contract execution. + # Get gas costs for the current fork + gas_costs = fork.gas_costs() storage_slot: int = 1 + storage = {} + + totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + POP + gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 - for _ in range(100000): - sstore_code = sstore_code + Op.SSTORE(storage_slot, 1) # NOTE: this will probably push some value on the stack, but I want to increase it to reduce the amount of gas and the size of the bytecode + i = 0 + while totalgas + gas_increment < 30_000_000: + totalgas += gas_increment + print(f"increment={gas_increment} < totalgas={totalgas} i={i}") + if i < 256: + sstore_code = sstore_code + Op.PUSH1(i) + else: + sstore_code = sstore_code + Op.PUSH2(i) + + sstore_code = sstore_code + Op.PUSH1(1) + Op.SSTORE(unchecked=True) + + storage[storage_slot] = 0x1 storage_slot += 1 - sstore_code = Op.POP + i += 1 + sstore_code = sstore_code + Op.POP sender = pre.fund_eoa() print(sender) contract_address = pre.deploy_contract( code=sstore_code, - storage={}, + storage=storage, ) tx_0_1 = Transaction( @@ -45,15 +61,13 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc): value=0, sender=sender, ) - tx_1_2 = Transaction( - to=contract_address, - gas_limit=30000000, - data=b"", - value=0, - sender=sender, - ) - - # TODO: Modify post-state allocations here. - post = {contract_address: Account(storage={storage_slot: 0x2})} + # tx_1_2 = Transaction( + # to=contract_address, + # gas_limit=30000000, + # data=b"", + # value=0, + # sender=sender, + # ) - blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1]), Block(txs=[tx_1_2])], post=post) + post = {contract_address: Account(storage=storage)} + blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1])], post=post) From e721cc635af591fa129a9027db4fb85042c1ded7 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:17:10 +0200 Subject: [PATCH 03/54] fix: SSTORE 0 -> 1 match all values in the state Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- tests/prague/eip8047_bloatnet/test_bloatnet.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py index 26cf7289a14..276aafb9b8e 100644 --- a/tests/prague/eip8047_bloatnet/test_bloatnet.py +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -24,23 +24,25 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) # Get gas costs for the current fork gas_costs = fork.gas_costs() - storage_slot: int = 1 + storage_slot: int = 0 storage = {} + GasLimit = 30_000_000 # Default gas limit seems to be >90M in this env totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + POP gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 i = 0 - while totalgas + gas_increment < 30_000_000: + while totalgas + gas_increment < GasLimit: totalgas += gas_increment print(f"increment={gas_increment} < totalgas={totalgas} i={i}") + sstore_code = sstore_code + Op.PUSH1(1) if i < 256: sstore_code = sstore_code + Op.PUSH1(i) else: sstore_code = sstore_code + Op.PUSH2(i) - sstore_code = sstore_code + Op.PUSH1(1) + Op.SSTORE(unchecked=True) + sstore_code = sstore_code + Op.SSTORE(unchecked=True) storage[storage_slot] = 0x1 storage_slot += 1 @@ -56,7 +58,7 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) tx_0_1 = Transaction( to=contract_address, - gas_limit=30000000, + gas_limit=GasLimit, data=b"", value=0, sender=sender, From d1cad258b6f58499014402371a7b4468ab8e4f7f Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:43:20 +0200 Subject: [PATCH 04/54] add the tx for 0 -> 1 and 1 -> 2 Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- .../prague/eip8047_bloatnet/test_bloatnet.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py index 276aafb9b8e..9e0c4357057 100644 --- a/tests/prague/eip8047_bloatnet/test_bloatnet.py +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -29,14 +29,14 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) storage = {} GasLimit = 30_000_000 # Default gas limit seems to be >90M in this env - totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + POP + totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + CALLDATALOAD gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD - sstore_code = Op.PUSH0 + sstore_code = Op.PUSH0 + Op.CALLDATALOAD + Op.DUP1 i = 0 while totalgas + gas_increment < GasLimit: totalgas += gas_increment - print(f"increment={gas_increment} < totalgas={totalgas} i={i}") - sstore_code = sstore_code + Op.PUSH1(1) + # print(f"increment={gas_increment} < totalgas={totalgas} i={i}") + sstore_code = sstore_code + Op.DUP1 if i < 256: sstore_code = sstore_code + Op.PUSH1(i) else: @@ -44,10 +44,10 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) sstore_code = sstore_code + Op.SSTORE(unchecked=True) - storage[storage_slot] = 0x1 + storage[storage_slot] = 0x02 << 248 storage_slot += 1 i += 1 - sstore_code = sstore_code + Op.POP + sstore_code = sstore_code + Op.POP # Drop last value on the stack sender = pre.fund_eoa() print(sender) @@ -59,17 +59,18 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) tx_0_1 = Transaction( to=contract_address, gas_limit=GasLimit, - data=b"", + data=b'\x01', # Single byte 0x01 + value=0, + sender=sender, + ) + tx_1_2 = Transaction( + to=contract_address, + gas_limit=30000000, + data=b'\x02', # Single byte 0x02, turns into 0x2000000000000000000000000000000000000000000000000000000000000000 value=0, sender=sender, ) - # tx_1_2 = Transaction( - # to=contract_address, - # gas_limit=30000000, - # data=b"", - # value=0, - # sender=sender, - # ) post = {contract_address: Account(storage=storage)} - blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1])], post=post) + + blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1, tx_1_2])], post=post) From 16f6d3017d8f3dae3aff9a1dd3adaa7ab2e1a59a Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:03:07 +0200 Subject: [PATCH 05/54] fix: linter issues Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- tests/prague/eip8047_bloatnet/test_bloatnet.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py index 9e0c4357057..259fa0146a1 100644 --- a/tests/prague/eip8047_bloatnet/test_bloatnet.py +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -5,18 +5,18 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op -from ethereum_test_forks import Fork REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" -REFERENCE_SPEC_VERSION = "DUMMY_VERSION" - +REFERENCE_SPEC_VERSION = "0.1" +GAS_LIMIT = 30_000_000 # Default gas limit seems to be >90M in this env @pytest.mark.valid_from("Prague") def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): """ - A test that calls a contract with many SSTOREs + A test that calls a contract with many SSTOREs. The first block will have many SSTORES that go from 0 -> 1 and the 2nd block will have many SSTORES that go from 1 -> 2 @@ -27,13 +27,12 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) storage_slot: int = 0 storage = {} - GasLimit = 30_000_000 # Default gas limit seems to be >90M in this env totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + CALLDATALOAD gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD + Op.DUP1 i = 0 - while totalgas + gas_increment < GasLimit: + while totalgas + gas_increment < GAS_LIMIT: totalgas += gas_increment # print(f"increment={gas_increment} < totalgas={totalgas} i={i}") sstore_code = sstore_code + Op.DUP1 @@ -58,15 +57,15 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) tx_0_1 = Transaction( to=contract_address, - gas_limit=GasLimit, + gas_limit=GAS_LIMIT, data=b'\x01', # Single byte 0x01 value=0, sender=sender, ) tx_1_2 = Transaction( to=contract_address, - gas_limit=30000000, - data=b'\x02', # Single byte 0x02, turns into 0x2000000000000000000000000000000000000000000000000000000000000000 + gas_limit=GAS_LIMIT, + data=b'\x02', # Single byte 0x02, turns into 0x02 << 248 value=0, sender=sender, ) From 374e08a4e68e4b36f20775f4fbcd864ffac8983a Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 14 Aug 2025 21:05:27 +0200 Subject: [PATCH 06/54] remove more whitespaces Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> remove leftover single whitespace :| --- tests/prague/eip8047_bloatnet/test_bloatnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py index 259fa0146a1..797973ea7d2 100644 --- a/tests/prague/eip8047_bloatnet/test_bloatnet.py +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -40,9 +40,9 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) sstore_code = sstore_code + Op.PUSH1(i) else: sstore_code = sstore_code + Op.PUSH2(i) - + sstore_code = sstore_code + Op.SSTORE(unchecked=True) - + storage[storage_slot] = 0x02 << 248 storage_slot += 1 i += 1 From 333c8769d79ebe76d747eabde7e1fc37e1969765 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Fri, 15 Aug 2025 14:01:58 +0200 Subject: [PATCH 07/54] fix formatting --- tests/prague/eip8047_bloatnet/test_bloatnet.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/prague/eip8047_bloatnet/test_bloatnet.py index 797973ea7d2..0fa28ce7fcf 100644 --- a/tests/prague/eip8047_bloatnet/test_bloatnet.py +++ b/tests/prague/eip8047_bloatnet/test_bloatnet.py @@ -11,7 +11,8 @@ REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" REFERENCE_SPEC_VERSION = "0.1" -GAS_LIMIT = 30_000_000 # Default gas limit seems to be >90M in this env +GAS_LIMIT = 30_000_000 # Default gas limit seems to be >90M in this env + @pytest.mark.valid_from("Prague") def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): @@ -29,7 +30,7 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) storage = {} totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + CALLDATALOAD - gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD + gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD + Op.DUP1 i = 0 while totalgas + gas_increment < GAS_LIMIT: @@ -46,7 +47,7 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) storage[storage_slot] = 0x02 << 248 storage_slot += 1 i += 1 - sstore_code = sstore_code + Op.POP # Drop last value on the stack + sstore_code = sstore_code + Op.POP # Drop last value on the stack sender = pre.fund_eoa() print(sender) @@ -58,14 +59,14 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) tx_0_1 = Transaction( to=contract_address, gas_limit=GAS_LIMIT, - data=b'\x01', # Single byte 0x01 + data=b"\x01", # Single byte 0x01 value=0, sender=sender, ) tx_1_2 = Transaction( to=contract_address, gas_limit=GAS_LIMIT, - data=b'\x02', # Single byte 0x02, turns into 0x02 << 248 + data=b"\x02", # Single byte 0x02, turns into 0x02 << 248 value=0, sender=sender, ) From 79a95b827746ac2d1601e00d1bc064a714d5759d Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:33:33 +0200 Subject: [PATCH 08/54] move to benchmarks Signed-off-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- tests/{prague/eip8047_bloatnet => benchmark}/test_bloatnet.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{prague/eip8047_bloatnet => benchmark}/test_bloatnet.py (100%) diff --git a/tests/prague/eip8047_bloatnet/test_bloatnet.py b/tests/benchmark/test_bloatnet.py similarity index 100% rename from tests/prague/eip8047_bloatnet/test_bloatnet.py rename to tests/benchmark/test_bloatnet.py From 8131e980ed041f605b83bb0e04700e050fa70227 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:17:14 +0200 Subject: [PATCH 09/54] fix linter value --- tests/benchmark/test_bloatnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 0fa28ce7fcf..fdf23605a1f 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -6,7 +6,7 @@ import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Transaction +from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Storage, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" @@ -27,7 +27,7 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) storage_slot: int = 0 - storage = {} + storage = Storage() totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + CALLDATALOAD gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD From 5f805fd36bc137cb27876eeb8eb38c2fa5463da6 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Fri, 22 Aug 2025 17:07:21 +0200 Subject: [PATCH 10/54] use the gas limit from the environment --- tests/benchmark/test_bloatnet.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index fdf23605a1f..0b01a223d9c 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -6,13 +6,11 @@ import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Storage, Transaction +from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Environment, Storage, Transaction from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" REFERENCE_SPEC_VERSION = "0.1" -GAS_LIMIT = 30_000_000 # Default gas limit seems to be >90M in this env - @pytest.mark.valid_from("Prague") def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): @@ -33,7 +31,7 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD + Op.DUP1 i = 0 - while totalgas + gas_increment < GAS_LIMIT: + while totalgas + gas_increment < Environment().gas_limit: totalgas += gas_increment # print(f"increment={gas_increment} < totalgas={totalgas} i={i}") sstore_code = sstore_code + Op.DUP1 @@ -58,14 +56,14 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) tx_0_1 = Transaction( to=contract_address, - gas_limit=GAS_LIMIT, + gas_limit=Environment().gas_limit, data=b"\x01", # Single byte 0x01 value=0, sender=sender, ) tx_1_2 = Transaction( to=contract_address, - gas_limit=GAS_LIMIT, + gas_limit=Environment().gas_limit, data=b"\x02", # Single byte 0x02, turns into 0x02 << 248 value=0, sender=sender, From 090a400178a6e0a3c83b2cd2b5a4c8afd5df2719 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:31:28 -0400 Subject: [PATCH 11/54] parameterize the written value in SSTORE --- tests/benchmark/test_bloatnet.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 0b01a223d9c..26492539c8d 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -13,7 +13,8 @@ REFERENCE_SPEC_VERSION = "0.1" @pytest.mark.valid_from("Prague") -def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): +@pytest.mark.parametrize("final_storage_value", [0x02 << 248, 0x02]) +def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, final_storage_value: int): """ A test that calls a contract with many SSTOREs. @@ -42,7 +43,7 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) sstore_code = sstore_code + Op.SSTORE(unchecked=True) - storage[storage_slot] = 0x02 << 248 + storage[storage_slot] = final_storage_value storage_slot += 1 i += 1 sstore_code = sstore_code + Op.POP # Drop last value on the stack @@ -57,14 +58,14 @@ def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork) tx_0_1 = Transaction( to=contract_address, gas_limit=Environment().gas_limit, - data=b"\x01", # Single byte 0x01 + data=(final_storage_value//2).to_bytes(32, 'big').rstrip(b'\x00'), value=0, sender=sender, ) tx_1_2 = Transaction( to=contract_address, gas_limit=Environment().gas_limit, - data=b"\x02", # Single byte 0x02, turns into 0x02 << 248 + data=final_storage_value.to_bytes(32, 'big').rstrip(b'\x00'), value=0, sender=sender, ) From cd02a02163f3f5f5453cdd16f72e13c085486c93 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:36:08 -0400 Subject: [PATCH 12/54] fix linter issues --- tests/benchmark/test_bloatnet.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 26492539c8d..6559bdb416f 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -6,7 +6,15 @@ import pytest from ethereum_test_forks import Fork -from ethereum_test_tools import Account, Alloc, Block, BlockchainTestFiller, Environment, Storage, Transaction +from ethereum_test_tools import ( + Account, + Alloc, + Block, + BlockchainTestFiller, + Environment, + Storage, + Transaction, +) from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" @@ -14,7 +22,9 @@ @pytest.mark.valid_from("Prague") @pytest.mark.parametrize("final_storage_value", [0x02 << 248, 0x02]) -def test_bloatnet(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, final_storage_value: int): +def test_bloatnet( + blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, final_storage_value: int +): """ A test that calls a contract with many SSTOREs. From 1f3c381de3758182280d7781d32406734aae12bd Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:43:50 -0400 Subject: [PATCH 13/54] update CHANGELOG.md --- docs/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1d419042a88..8716c3b978b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -131,6 +131,7 @@ Users can select any of the artifacts depending on their testing needs for their ### πŸ§ͺ Test Cases +- ✨ [BloatNet](bloatnet.info)/Multidimensional Metering: Add benchmarks to be used as part of the BloatNet project and also for Multidimensional Metering. - ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): Add additional test cases for modular comparison. - πŸ”€ Refactored `BLOBHASH` opcode context tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1637](https://github.com/ethereum/execution-spec-tests/pull/1637)). - πŸ”€ Refactored `SELFDESTRUCT` opcode collision tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1643](https://github.com/ethereum/execution-spec-tests/pull/1643)). From f6def7e3ab2739da990bce2dcb16318a3cb5c985 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:39:45 -0400 Subject: [PATCH 14/54] fix format --- tests/benchmark/test_bloatnet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 6559bdb416f..c683aaf0367 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -20,6 +20,7 @@ REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" REFERENCE_SPEC_VERSION = "0.1" + @pytest.mark.valid_from("Prague") @pytest.mark.parametrize("final_storage_value", [0x02 << 248, 0x02]) def test_bloatnet( @@ -68,14 +69,14 @@ def test_bloatnet( tx_0_1 = Transaction( to=contract_address, gas_limit=Environment().gas_limit, - data=(final_storage_value//2).to_bytes(32, 'big').rstrip(b'\x00'), + data=(final_storage_value // 2).to_bytes(32, "big").rstrip(b"\x00"), value=0, sender=sender, ) tx_1_2 = Transaction( to=contract_address, gas_limit=Environment().gas_limit, - data=final_storage_value.to_bytes(32, 'big').rstrip(b'\x00'), + data=final_storage_value.to_bytes(32, "big").rstrip(b"\x00"), value=0, sender=sender, ) From 7e20a501e92f1960cccbba48bcfd06a1f52e76c2 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:02:51 -0400 Subject: [PATCH 15/54] simplify syntax --- tests/benchmark/test_bloatnet.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index c683aaf0367..a69f0a4f69c 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -39,20 +39,13 @@ def test_bloatnet( storage = Storage() - totalgas = gas_costs.G_BASE * 2 # Initial gas for PUSH0 + CALLDATALOAD + totalgas = gas_costs.G_BASE * 3 + gas_costs.G_VERY_LOW # Initial gas for PUSH0 + CALLDATALOAD + DUP1 + POP (at the end) gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD + Op.DUP1 i = 0 while totalgas + gas_increment < Environment().gas_limit: totalgas += gas_increment - # print(f"increment={gas_increment} < totalgas={totalgas} i={i}") - sstore_code = sstore_code + Op.DUP1 - if i < 256: - sstore_code = sstore_code + Op.PUSH1(i) - else: - sstore_code = sstore_code + Op.PUSH2(i) - - sstore_code = sstore_code + Op.SSTORE(unchecked=True) + sstore_code = sstore_code + Op.SSTORE(i, Op.DUP4) storage[storage_slot] = final_storage_value storage_slot += 1 From c24ad35a780b0de8efc95e229e35e19df0ed5ae8 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:32:01 -0400 Subject: [PATCH 16/54] fix: start with an empty contract storage --- tests/benchmark/test_bloatnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index a69f0a4f69c..82b3d65d2bc 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -56,7 +56,7 @@ def test_bloatnet( print(sender) contract_address = pre.deploy_contract( code=sstore_code, - storage=storage, + storage=Storage(), ) tx_0_1 = Transaction( From fc27e53f0cf3ceeb8249b897f5a283ad18cd323d Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:04:37 -0400 Subject: [PATCH 17/54] more fixes, but the result is still incorrect --- tests/benchmark/test_bloatnet.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 82b3d65d2bc..bd03369afc3 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -35,21 +35,19 @@ def test_bloatnet( # Get gas costs for the current fork gas_costs = fork.gas_costs() - storage_slot: int = 0 storage = Storage() - totalgas = gas_costs.G_BASE * 3 + gas_costs.G_VERY_LOW # Initial gas for PUSH0 + CALLDATALOAD + DUP1 + POP (at the end) + totalgas = gas_costs.G_BASE * 2 + gas_costs.G_VERY_LOW # Initial gas for PUSH0 + CALLDATALOAD + POP (at the end) gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD - sstore_code = Op.PUSH0 + Op.CALLDATALOAD + Op.DUP1 - i = 0 + sstore_code = Op.PUSH0 + Op.CALLDATALOAD + storage_slot: int = 0 while totalgas + gas_increment < Environment().gas_limit: totalgas += gas_increment - sstore_code = sstore_code + Op.SSTORE(i, Op.DUP4) - + sstore_code = sstore_code + Op.SSTORE(Op.DUP2, storage_slot) storage[storage_slot] = final_storage_value storage_slot += 1 - i += 1 + sstore_code = sstore_code + Op.POP # Drop last value on the stack sender = pre.fund_eoa() From 7d872624aa1104b50faf70ede6d83cbc79926e9c Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:22:00 -0400 Subject: [PATCH 18/54] fix: finally fix the tests --- tests/benchmark/test_bloatnet.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index bd03369afc3..633f6f0df07 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -35,16 +35,19 @@ def test_bloatnet( # Get gas costs for the current fork gas_costs = fork.gas_costs() + # this is only used for computing the intinsic gas + data = final_storage_value.to_bytes(32, "big").rstrip(b"\x00") storage = Storage() totalgas = gas_costs.G_BASE * 2 + gas_costs.G_VERY_LOW # Initial gas for PUSH0 + CALLDATALOAD + POP (at the end) + totalgas = totalgas + fork.transaction_intrinsic_cost_calculator()(calldata=data); gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD storage_slot: int = 0 while totalgas + gas_increment < Environment().gas_limit: totalgas += gas_increment - sstore_code = sstore_code + Op.SSTORE(Op.DUP2, storage_slot) + sstore_code = sstore_code + Op.SSTORE(storage_slot, Op.DUP1) storage[storage_slot] = final_storage_value storage_slot += 1 From 8556014c1d630431fd35345d0afb40ca3328cf2a Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:05:47 +0200 Subject: [PATCH 19/54] linter fix --- tests/benchmark/test_bloatnet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 633f6f0df07..64c7a2022ae 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -40,8 +40,9 @@ def test_bloatnet( storage = Storage() - totalgas = gas_costs.G_BASE * 2 + gas_costs.G_VERY_LOW # Initial gas for PUSH0 + CALLDATALOAD + POP (at the end) - totalgas = totalgas + fork.transaction_intrinsic_cost_calculator()(calldata=data); + # Initial gas for PUSH0 + CALLDATALOAD + POP (at the end) + totalgas = gas_costs.G_BASE * 2 + gas_costs.G_VERY_LOW + totalgas = totalgas + fork.transaction_intrinsic_cost_calculator()(calldata=data) gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD storage_slot: int = 0 From 326915ea5c05735d2d9de3959657c318d57dccae Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:03:17 +0200 Subject: [PATCH 20/54] add SLOAD tests --- tests/benchmark/test_bloatnet.py | 91 +++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 64c7a2022ae..7e468e2a461 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -1,10 +1,11 @@ """ -abstract: Tests [EIP-8047 BloatNet](https://eips.ethereum.org/EIPS/eip-8047) - Test cases for [EIP-8047 BloatNet](https://eips.ethereum.org/EIPS/eip-8047)]. +abstract: Tests [BloatNet](https://bloatnet.info) + Test cases for [BloatNet](https://bloatnet.info). """ import pytest +from ethereum_test_base_types import HashInt from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, @@ -79,3 +80,89 @@ def test_bloatnet( post = {contract_address: Account(storage=storage)} blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1, tx_1_2])], post=post) + + +# Warm reads are very cheap, which means you can really fill a block +# with them. Only fill the block by a factor of SPEEDUP. +SPEEDUP: int = 100 + + +@pytest.mark.valid_from("Prague") +def test_bloatnet_sload_warm(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): + """Test that loads warm storage locations many times.""" + gas_costs = fork.gas_costs() + + # Pre-fill storage with values + num_slots = 100 # Number of storage slots to warm up + storage = Storage({HashInt(i): HashInt(0xDEADBEEF + i) for i in range(num_slots)}) + + # Calculate gas costs + totalgas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") + + # First pass - warm up all slots (cold access) + warmup_gas = num_slots * (gas_costs.G_COLD_SLOAD + gas_costs.G_BASE) + totalgas += warmup_gas + + # Calculate how many warm loads we can fit + gas_increment = gas_costs.G_WARM_SLOAD + gas_costs.G_BASE # Warm SLOAD + POP + remaining_gas = Environment().gas_limit - totalgas + num_warm_loads = remaining_gas // (SPEEDUP * gas_increment) + + # Build the complete code: warmup + repeated warm loads + sload_code = Op.SLOAD(0) + Op.POP if num_slots > 0 else Op.STOP + for i in range(1, num_slots): + sload_code = sload_code + Op.SLOAD(i) + Op.POP + for i in range(num_warm_loads): + sload_code = sload_code + Op.SLOAD(i % num_slots) + Op.POP + + sender = pre.fund_eoa() + contract_address = pre.deploy_contract( + code=sload_code, + storage=storage, + ) + + tx = Transaction( + to=contract_address, + gas_limit=Environment().gas_limit, + data=b"", + value=0, + sender=sender, + ) + + post = {contract_address: Account(storage=storage)} + blockchain_test(pre=pre, blocks=[Block(txs=[tx])], post=post) + + +@pytest.mark.valid_from("Prague") +def test_bloatnet_sload_cold(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): + """Test that loads many different cold storage locations.""" + gas_costs = fork.gas_costs() + + # Calculate gas costs and max slots + totalgas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") + # PUSH + Cold SLOAD + POP + gas_increment = gas_costs.G_VERY_LOW + gas_costs.G_COLD_SLOAD + gas_costs.G_BASE + max_slots = (Environment().gas_limit - totalgas) // gas_increment + + # Build storage and code for all slots + storage = Storage({HashInt(i): HashInt(0xC0FFEE + i) for i in range(max_slots)}) + sload_code = Op.SLOAD(0) + Op.POP if max_slots > 0 else Op.STOP + for i in range(1, max_slots): + sload_code = sload_code + Op.SLOAD(i) + Op.POP + + sender = pre.fund_eoa() + contract_address = pre.deploy_contract( + code=sload_code, + storage=storage, + ) + + tx = Transaction( + to=contract_address, + gas_limit=Environment().gas_limit, + data=b"", + value=0, + sender=sender, + ) + + post = {contract_address: Account(storage=storage)} + blockchain_test(pre=pre, blocks=[Block(txs=[tx])], post=post) From 1f8e62a344d8b68f737c110c3dddad01e938102d Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 29 Aug 2025 12:16:06 +0200 Subject: [PATCH 21/54] test(benchmark): implement CREATE2 addressing for bloatnet tests - Add CREATE2 deterministic address calculation to overcome 24KB bytecode limit - Fix While loop condition to properly iterate through contracts - Account for memory expansion costs in gas calculations - Add safety margins (50k gas reserve, 98% utilization) for stability - Tests now scale to any gas limit without bytecode constraints - Achieve 98% gas utilization with 10M and 20M gas limits --- tests/benchmark/test_bloatnet.py | 442 ++++++++++++++++++++++++++++++- 1 file changed, 431 insertions(+), 11 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 7e468e2a461..cfedb669fa0 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -9,23 +9,90 @@ from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, + Address, Alloc, Block, BlockchainTestFiller, - Environment, + Bytecode, + Hash, Storage, Transaction, + While, + keccak256, ) from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" REFERENCE_SPEC_VERSION = "0.1" +# Constants for CREATE2 address calculation +CREATE2_PREFIX = 0xFF +CREATE2_MEMORY_OFFSET = 0x1000 # Offset for CREATE2 calculation to avoid collision + + +def calculate_create2_address(deployer_address, salt: int, init_code: bytes): + """Calculate CREATE2 deterministic address.""" + # Handle both string and Address types + if isinstance(deployer_address, str): + addr_hex = deployer_address[2:] if deployer_address.startswith("0x") else deployer_address + deployer_bytes = bytes.fromhex(addr_hex) + else: + # Assume it's an Address object with bytes representation + deployer_bytes = bytes(deployer_address) + salt_bytes = salt.to_bytes(32, "big") + init_code_hash = keccak256(init_code) + + packed = bytes([CREATE2_PREFIX]) + deployer_bytes + salt_bytes + init_code_hash + address_hash = keccak256(packed) + return Address(address_hash[-20:]) + + +def generate_create2_address_calculation( + factory_address, + init_code_hash: bytes, +) -> Bytecode: + """Generate EVM bytecode to calculate a CREATE2 address with salt on stack.""" + code = Bytecode() + + # Memory layout at CREATE2_MEMORY_OFFSET: + # [0xFF][factory_address][salt][init_code_hash] + base = CREATE2_MEMORY_OFFSET + + # Store 0xFF prefix at memory[base] + code += Op.PUSH1(CREATE2_PREFIX) + Op.PUSH2(base) + Op.MSTORE8 + + # Store factory address at memory[base+1:base+21] + # Handle both string and Address types + if isinstance(factory_address, str): + addr_hex = factory_address[2:] if factory_address.startswith("0x") else factory_address + factory_bytes = bytes.fromhex(addr_hex) + else: + # Assume it's an Address object with bytes representation + factory_bytes = bytes(factory_address) + code += Op.PUSH20(int.from_bytes(factory_bytes, "big")) + Op.PUSH2(base + 1) + Op.MSTORE + + # Store salt at memory[base+21:base+53] (base+0x15) + # Assumes salt is already on stack + code += Op.PUSH2(base + 0x15) + Op.MSTORE + + # Store init code hash at memory[base+53:base+85] (base+0x35) + code += Op.PUSH32(int.from_bytes(init_code_hash, "big")) + Op.PUSH2(base + 0x35) + Op.MSTORE + + # Calculate keccak256 of 85 bytes starting at memory[base] + code += Op.PUSH1(0x55) + Op.PUSH2(base) + Op.SHA3 + + # The address is the last 20 bytes (already on stack) + return code + @pytest.mark.valid_from("Prague") @pytest.mark.parametrize("final_storage_value", [0x02 << 248, 0x02]) def test_bloatnet( - blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, final_storage_value: int + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + final_storage_value: int, + gas_benchmark_value: int, ): """ A test that calls a contract with many SSTOREs. @@ -47,7 +114,7 @@ def test_bloatnet( gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD sstore_code = Op.PUSH0 + Op.CALLDATALOAD storage_slot: int = 0 - while totalgas + gas_increment < Environment().gas_limit: + while totalgas + gas_increment < gas_benchmark_value: totalgas += gas_increment sstore_code = sstore_code + Op.SSTORE(storage_slot, Op.DUP1) storage[storage_slot] = final_storage_value @@ -64,14 +131,14 @@ def test_bloatnet( tx_0_1 = Transaction( to=contract_address, - gas_limit=Environment().gas_limit, + gas_limit=gas_benchmark_value, data=(final_storage_value // 2).to_bytes(32, "big").rstrip(b"\x00"), value=0, sender=sender, ) tx_1_2 = Transaction( to=contract_address, - gas_limit=Environment().gas_limit, + gas_limit=gas_benchmark_value, data=final_storage_value.to_bytes(32, "big").rstrip(b"\x00"), value=0, sender=sender, @@ -88,7 +155,9 @@ def test_bloatnet( @pytest.mark.valid_from("Prague") -def test_bloatnet_sload_warm(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): +def test_bloatnet_sload_warm( + blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int +): """Test that loads warm storage locations many times.""" gas_costs = fork.gas_costs() @@ -105,7 +174,7 @@ def test_bloatnet_sload_warm(blockchain_test: BlockchainTestFiller, pre: Alloc, # Calculate how many warm loads we can fit gas_increment = gas_costs.G_WARM_SLOAD + gas_costs.G_BASE # Warm SLOAD + POP - remaining_gas = Environment().gas_limit - totalgas + remaining_gas = gas_benchmark_value - totalgas num_warm_loads = remaining_gas // (SPEEDUP * gas_increment) # Build the complete code: warmup + repeated warm loads @@ -123,7 +192,7 @@ def test_bloatnet_sload_warm(blockchain_test: BlockchainTestFiller, pre: Alloc, tx = Transaction( to=contract_address, - gas_limit=Environment().gas_limit, + gas_limit=gas_benchmark_value, data=b"", value=0, sender=sender, @@ -134,7 +203,9 @@ def test_bloatnet_sload_warm(blockchain_test: BlockchainTestFiller, pre: Alloc, @pytest.mark.valid_from("Prague") -def test_bloatnet_sload_cold(blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork): +def test_bloatnet_sload_cold( + blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int +): """Test that loads many different cold storage locations.""" gas_costs = fork.gas_costs() @@ -142,7 +213,7 @@ def test_bloatnet_sload_cold(blockchain_test: BlockchainTestFiller, pre: Alloc, totalgas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") # PUSH + Cold SLOAD + POP gas_increment = gas_costs.G_VERY_LOW + gas_costs.G_COLD_SLOAD + gas_costs.G_BASE - max_slots = (Environment().gas_limit - totalgas) // gas_increment + max_slots = (gas_benchmark_value - totalgas) // gas_increment # Build storage and code for all slots storage = Storage({HashInt(i): HashInt(0xC0FFEE + i) for i in range(max_slots)}) @@ -158,7 +229,7 @@ def test_bloatnet_sload_cold(blockchain_test: BlockchainTestFiller, pre: Alloc, tx = Transaction( to=contract_address, - gas_limit=Environment().gas_limit, + gas_limit=gas_benchmark_value, data=b"", value=0, sender=sender, @@ -166,3 +237,352 @@ def test_bloatnet_sload_cold(blockchain_test: BlockchainTestFiller, pre: Alloc, post = {contract_address: Account(storage=storage)} blockchain_test(pre=pre, blocks=[Block(txs=[tx])], post=post) + + +# Storage slot for success flag +SUCCESS_FLAG_SLOT = 0 +SUCCESS_FLAG_VALUE = 42 +EXTCODECOPY_MEMORY_OFFSET = 0x1000 # 4KB offset to avoid conflicts with address storage + + +@pytest.mark.valid_from("Prague") +def test_bloatnet_extcodesize_balance( + blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int +): + """ + Test that maximizes I/O reads by combining cold BALANCE with warm EXTCODESIZE calls. + + This test uses CREATE2 deterministic addressing to avoid bytecode size limitations. + It deploys many 24kB contracts with unique bytecode, then: + 1. Calls BALANCE on all contracts (cold access) to warm them and fill cache + 2. Calls EXTCODESIZE on all contracts (warm access) hoping cache evictions force re-reads + + Goal: Maximum I/O read operations with minimum gas consumption. + """ + gas_costs = fork.gas_costs() + max_contract_size = fork.max_code_size() + + # Calculate costs for the attack transaction + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") + + # Calculate memory expansion cost for CREATE2 address calculation + # CREATE2 uses memory at offset 0x1000 for 85 bytes + memory_expansion_cost = fork.memory_expansion_gas_calculator()( + new_bytes=CREATE2_MEMORY_OFFSET + 85 + ) + + # Cost per iteration with CREATE2 address calculation + # Additional cost for CREATE2 address calculation (~75 gas) + create2_calc_cost = ( + gas_costs.G_VERY_LOW * 8 # Memory operations (MSTORE/MSTORE8) + + 30 + + 6 * 3 # KECCAK256 for 85 bytes + + gas_costs.G_VERY_LOW * 10 # Stack operations + ) + + cost_per_iteration = ( + create2_calc_cost # CREATE2 address calculation + + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE + + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (same account) + + gas_costs.G_BASE * 2 # POPs for results + + gas_costs.G_VERY_LOW * 3 # Loop overhead (DUP, PUSH1, ADD) + ) + + # Access costs in attack transaction + final_storage_cost = ( + gas_costs.G_STORAGE_RESET + gas_costs.G_COLD_SLOAD + gas_costs.G_VERY_LOW * 2 + ) + available_gas_for_access = ( + gas_benchmark_value - intrinsic_gas - final_storage_cost - memory_expansion_cost + ) + + # Calculate maximum contracts we can access + # Reserve some gas for the final SSTORE operation + reserved_gas = 50000 # Reserve 50k gas for final operations and safety + available_gas_for_loops = available_gas_for_access - reserved_gas + # Use 98% of calculated contracts (less conservative) + num_contracts = int(available_gas_for_loops // cost_per_iteration * 0.98) + + # Generate unique bytecode for deployment (will be same for all to simplify init_code_hash) + deploy_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety + pattern_count = 0 + while len(deploy_bytecode) < max_contract_size - 100: + unique_value = Hash(pattern_count) + deploy_bytecode += Op.PUSH32[unique_value] + Op.POP + pattern_count += 1 + while len(deploy_bytecode) < max_contract_size: + deploy_bytecode += Op.JUMPDEST + + assert len(deploy_bytecode) == max_contract_size, ( + f"Contract size mismatch: {len(deploy_bytecode)}" + ) + + # Init code that returns the bytecode + init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + deploy_bytecode + init_code_hash = keccak256(bytes(init_code)) + + # Factory address that would deploy contracts with CREATE2 + factory_address = pre.deploy_contract(code=Op.STOP) # Simple factory placeholder + + # Pre-deploy all contracts at CREATE2 addresses using sequential salts + deployed_contracts = [] # Track for post-state validation + for salt in range(num_contracts): + # Calculate the CREATE2 address + create2_addr = calculate_create2_address(factory_address, salt, bytes(init_code)) + # Deploy at the calculated address + pre[create2_addr] = Account(code=deploy_bytecode) + deployed_contracts.append(create2_addr) + + # Create the attack contract that calculates CREATE2 addresses and calls operations + attack_code = Bytecode() + + # Pre-compute the CREATE2 calculation bytecode + create2_calc = generate_create2_address_calculation(factory_address, init_code_hash) + + # Main loop: iterate through all contracts + attack_code += Op.PUSH0 # Counter starts at 0 + attack_code += While( + body=( + # Calculate CREATE2 address for current salt (counter) + Op.DUP1 # Duplicate counter (salt) + + create2_calc # Calculate CREATE2 address (consumes salt, leaves address) + # Call BALANCE (cold access) - this warms the account + + Op.DUP1 # Duplicate address + + Op.BALANCE # Get balance + + Op.POP # Discard balance result + # Call EXTCODESIZE (warm access) - hoping cache was evicted + + Op.EXTCODESIZE # Get code size (address already on stack) + + Op.POP # Discard code size result + # Increment counter + + Op.PUSH1[1] + + Op.ADD + ), + condition=Op.GT(num_contracts, Op.DUP1), # num_contracts > counter + ) + attack_code += Op.POP # Clean up counter + # Store success flag for validation + attack_code += Op.SSTORE(SUCCESS_FLAG_SLOT, SUCCESS_FLAG_VALUE) + + # Pre-initialize storage slot 0 to avoid cold SSTORE cost + attack_address = pre.deploy_contract(code=attack_code, storage={SUCCESS_FLAG_SLOT: 1}) + + # Attack transaction + attack_tx = Transaction( + to=attack_address, + gas_limit=gas_benchmark_value, + sender=pre.fund_eoa(), + ) + + # Post-state validation + post = { + # Verify attack completed successfully + attack_address: Account(storage={SUCCESS_FLAG_SLOT: SUCCESS_FLAG_VALUE}), + } + + # Verify all pre-deployed contracts still exist with their code intact + # We check that they have nonce=1 (contracts were deployed) + for contract_address in deployed_contracts: + post[contract_address] = Account( + nonce=1, # Contract exists and was deployed + ) + + blockchain_test( + pre=pre, + blocks=[ + Block(txs=[attack_tx]), # Execute the attack + ], + post=post, + exclude_full_post_state_in_output=True, # Reduce output size + ) + + +@pytest.mark.valid_from("Prague") +def test_bloatnet_extcodecopy_balance( + blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int +): + """ + Test that maximizes actual I/O reads using BALANCE + EXTCODECOPY combination. + + This test uses CREATE2 deterministic addressing to avoid bytecode size limitations. + It achieves maximum data reads from disk by: + 1. Pre-deploying many 24KB contracts with unique bytecode + 2. Calling BALANCE (cold) to warm the account - reads metadata (~200 bytes) + 3. Calling EXTCODECOPY (warm) to read the full 24KB bytecode + + The BALANCE + EXTCODECOPY pattern is optimal because: + - BALANCE warms the account, reducing EXTCODECOPY base cost from 2600 to 100 + - EXTCODECOPY forces reading the actual bytecode from disk (not just metadata) + - Total cost: ~5129 gas per contract for 24KB of data read (including CREATE2 calculation) + + This test reads 123x more data per contract than BALANCE + EXTCODESIZE pattern. + """ + gas_costs = fork.gas_costs() + max_contract_size = fork.max_code_size() + + # Calculate costs for the attack transaction + intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") + + # Calculate memory expansion cost for CREATE2 address calculation + # CREATE2 uses memory at offset 0x1000 for 85 bytes + memory_expansion_cost = fork.memory_expansion_gas_calculator()( + new_bytes=CREATE2_MEMORY_OFFSET + 85 + ) + + # Cost per iteration with CREATE2 address calculation + # Additional cost for CREATE2 address calculation (~75 gas) + create2_calc_cost = ( + gas_costs.G_VERY_LOW * 8 # Memory operations (MSTORE/MSTORE8) + + 30 + + 6 * 3 # KECCAK256 for 85 bytes + + gas_costs.G_VERY_LOW * 10 # Stack operations + ) + + # EXTCODECOPY copies full 24KB to memory + words_to_copy = (max_contract_size + 31) // 32 # 768 words for 24KB + cost_per_iteration = ( + create2_calc_cost # CREATE2 address calculation + + gas_costs.G_VERY_LOW # DUP1 for address + + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) + + gas_costs.G_BASE # POP balance result + + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) + + gas_costs.G_COPY * words_to_copy # Copy cost (3 * 768 = 2304) + + gas_costs.G_BASE # POP address + + gas_costs.G_VERY_LOW * 3 # Loop overhead (PUSH1, ADD, DUP for condition) + ) + + # Calculate memory expansion cost for EXTCODECOPY destination + # EXTCODECOPY writes 24KB at offset 0x1000 + extcodecopy_memory_expansion = fork.memory_expansion_gas_calculator()( + new_bytes=EXTCODECOPY_MEMORY_OFFSET + max_contract_size + ) + # CREATE2 calculation also uses memory (already accounted in memory_expansion_cost) + + # Access costs in attack transaction + final_storage_cost = ( + gas_costs.G_STORAGE_RESET + gas_costs.G_COLD_SLOAD + gas_costs.G_VERY_LOW * 2 + ) + available_gas_for_access = ( + gas_benchmark_value + - intrinsic_gas + - final_storage_cost + - memory_expansion_cost + - extcodecopy_memory_expansion + ) + + # Calculate maximum contracts we can access + # This test scales automatically: ~1000 contracts at 90M gas, ~10,000 at 900M gas + # Reserve some gas for the final SSTORE operation + reserved_gas = 50000 # Reserve 50k gas for final operations and safety + available_gas_for_loops = available_gas_for_access - reserved_gas + # Use 98% of calculated contracts (less conservative) + num_contracts = int(available_gas_for_loops // cost_per_iteration * 0.98) + + # Generate base bytecode template + base_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety + pattern_count = 0 + while len(base_bytecode) < max_contract_size - 100: + unique_value = Hash(pattern_count) + base_bytecode += Op.PUSH32[unique_value] + Op.POP + pattern_count += 1 + while len(base_bytecode) < max_contract_size: + base_bytecode += Op.JUMPDEST + + assert len(base_bytecode) == max_contract_size, ( + f"Base bytecode size mismatch: {len(base_bytecode)}" + ) + + # Init code that returns the bytecode + init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + base_bytecode + init_code_hash = keccak256(bytes(init_code)) + + # Factory address for CREATE2 + factory_address = pre.deploy_contract(code=Op.STOP) # Simple factory placeholder + + # Pre-deploy all contracts at CREATE2 addresses with unique bytecode + deployed_contracts = [] # Track for post-state validation + for salt in range(num_contracts): + # Generate unique bytecode for this contract + bytecode = Bytecode(Op.STOP) + pattern_count = 0 + while len(bytecode) < max_contract_size - 100: + unique_value = Hash(salt * 1000000 + pattern_count) + bytecode += Op.PUSH32[unique_value] + Op.POP + pattern_count += 1 + while len(bytecode) < max_contract_size: + bytecode += Op.JUMPDEST + + assert len(bytecode) == max_contract_size, f"Contract size mismatch: {len(bytecode)}" + + # Calculate CREATE2 address using the base init code + # Note: In real implementation, each contract has unique bytecode but same init structure + create2_addr = calculate_create2_address(factory_address, salt, bytes(init_code)) + + # Deploy at the calculated address with unique bytecode + pre[create2_addr] = Account(code=bytecode) + deployed_contracts.append(create2_addr) + + # Create the attack contract that calculates CREATE2 addresses and calls operations + attack_code = Bytecode() + + # Pre-compute the CREATE2 calculation bytecode + create2_calc = generate_create2_address_calculation(factory_address, init_code_hash) + + # Main loop: BALANCE + EXTCODECOPY for each contract + attack_code += Op.PUSH0 # Counter starts at 0 + attack_code += While( + body=( + # Calculate CREATE2 address for current salt (counter) + Op.DUP1 # Duplicate counter (salt) + + create2_calc # Calculate CREATE2 address (consumes salt, leaves address) + # Call BALANCE (cold access) - warms the account + + Op.DUP1 # Duplicate address for BALANCE + + Op.BALANCE # Get balance (cold, 2600 gas) + + Op.POP # Discard balance result + # Call EXTCODECOPY (warm access) - reads full 24KB bytecode + + Op.EXTCODECOPY( + address=Op.DUP1, # Use the same address (now warm) + dest_offset=EXTCODECOPY_MEMORY_OFFSET, # Copy to high memory + offset=0, # Start from beginning of bytecode + size=max_contract_size, # Copy full 24KB + ) + + Op.POP # Clean up address + # Increment counter + + Op.PUSH1[1] + + Op.ADD + ), + condition=Op.GT(num_contracts, Op.DUP1), # Fixed: num_contracts > counter + ) + attack_code += Op.POP # Clean up counter + # Store success flag for validation + attack_code += Op.SSTORE(SUCCESS_FLAG_SLOT, SUCCESS_FLAG_VALUE) + + # Pre-initialize storage slot 0 to avoid cold SSTORE cost + attack_address = pre.deploy_contract(code=attack_code, storage={SUCCESS_FLAG_SLOT: 1}) + + # Attack transaction + attack_tx = Transaction( + to=attack_address, + gas_limit=gas_benchmark_value, + sender=pre.fund_eoa(), + ) + + # Post-state validation + post = { + # Verify attack completed successfully + attack_address: Account(storage={SUCCESS_FLAG_SLOT: SUCCESS_FLAG_VALUE}), + } + + # Verify all pre-deployed contracts still exist + for contract_address in deployed_contracts: + post[contract_address] = Account( + nonce=1, # Contract exists and was deployed + ) + + blockchain_test( + pre=pre, + blocks=[ + Block(txs=[attack_tx]), # Execute the attack + ], + post=post, + exclude_full_post_state_in_output=True, # Reduce output size + ) From 8babb1302d78ed9fd1b0de39c1ba1b7395461cc1 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 11 Sep 2025 12:09:11 +0200 Subject: [PATCH 22/54] refactor(benchmark): optimize gas calculations in bloatnet tests - Remove gas reserve and 98% utilization logic for contract calculations - Directly calculate the number of contracts based on available gas - Introduce precise expected gas usage calculations for better accuracy - Ensure tests scale effectively without unnecessary constraints --- tests/benchmark/test_bloatnet.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index cfedb669fa0..fd2004c711b 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -297,11 +297,7 @@ def test_bloatnet_extcodesize_balance( ) # Calculate maximum contracts we can access - # Reserve some gas for the final SSTORE operation - reserved_gas = 50000 # Reserve 50k gas for final operations and safety - available_gas_for_loops = available_gas_for_access - reserved_gas - # Use 98% of calculated contracts (less conservative) - num_contracts = int(available_gas_for_loops // cost_per_iteration * 0.98) + num_contracts = int(available_gas_for_access // cost_per_iteration) # Generate unique bytecode for deployment (will be same for all to simplify init_code_hash) deploy_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety @@ -386,6 +382,14 @@ def test_bloatnet_extcodesize_balance( nonce=1, # Contract exists and was deployed ) + # Calculate expected gas usage precisely + expected_benchmark_gas_used = ( + intrinsic_gas + + memory_expansion_cost + + (num_contracts * cost_per_iteration) + + final_storage_cost + ) + blockchain_test( pre=pre, blocks=[ @@ -393,6 +397,7 @@ def test_bloatnet_extcodesize_balance( ], post=post, exclude_full_post_state_in_output=True, # Reduce output size + expected_benchmark_gas_used=expected_benchmark_gas_used, ) @@ -471,11 +476,7 @@ def test_bloatnet_extcodecopy_balance( # Calculate maximum contracts we can access # This test scales automatically: ~1000 contracts at 90M gas, ~10,000 at 900M gas - # Reserve some gas for the final SSTORE operation - reserved_gas = 50000 # Reserve 50k gas for final operations and safety - available_gas_for_loops = available_gas_for_access - reserved_gas - # Use 98% of calculated contracts (less conservative) - num_contracts = int(available_gas_for_loops // cost_per_iteration * 0.98) + num_contracts = int(available_gas_for_access // cost_per_iteration) # Generate base bytecode template base_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety @@ -578,6 +579,15 @@ def test_bloatnet_extcodecopy_balance( nonce=1, # Contract exists and was deployed ) + # Calculate expected gas usage precisely + expected_benchmark_gas_used = ( + intrinsic_gas + + memory_expansion_cost + + extcodecopy_memory_expansion + + (num_contracts * cost_per_iteration) + + final_storage_cost + ) + blockchain_test( pre=pre, blocks=[ @@ -585,4 +595,5 @@ def test_bloatnet_extcodecopy_balance( ], post=post, exclude_full_post_state_in_output=True, # Reduce output size + expected_benchmark_gas_used=expected_benchmark_gas_used, ) From e70132bd21656635f3387481b9394a8ba5bc3892 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 11 Sep 2025 14:27:06 +0200 Subject: [PATCH 23/54] refactor(benchmark): bloatnet tests with unique bytecode for I/O optimization - Update tests to generate unique bytecode for each contract, maximizing I/O reads during benchmarks. - Clarify comments regarding bytecode generation and its impact on gas costs. - Ensure CREATE2 addresses are calculated consistently using a base bytecode template. - Improve test descriptions to reflect the changes in contract deployment strategy. --- tests/benchmark/test_bloatnet.py | 73 +++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index fd2004c711b..e1d3e2cda83 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -253,11 +253,17 @@ def test_bloatnet_extcodesize_balance( Test that maximizes I/O reads by combining cold BALANCE with warm EXTCODESIZE calls. This test uses CREATE2 deterministic addressing to avoid bytecode size limitations. + Each contract has UNIQUE bytecode to prevent client-side storage deduplication and + cache optimization. Without unique bytecode, clients could store it once and reference + it for all contracts, defeating the I/O benchmark purpose. + It deploys many 24kB contracts with unique bytecode, then: 1. Calls BALANCE on all contracts (cold access) to warm them and fill cache 2. Calls EXTCODESIZE on all contracts (warm access) hoping cache evictions force re-reads Goal: Maximum I/O read operations with minimum gas consumption. + Note: Bytecode generation happens during test setup (not in the attack transaction), + so unique bytecode doesn't affect gas costs - we still access the same number of contracts. """ gas_costs = fork.gas_costs() max_contract_size = fork.max_code_size() @@ -299,33 +305,52 @@ def test_bloatnet_extcodesize_balance( # Calculate maximum contracts we can access num_contracts = int(available_gas_for_access // cost_per_iteration) - # Generate unique bytecode for deployment (will be same for all to simplify init_code_hash) - deploy_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety + # Generate a base bytecode template for init_code_hash calculation + # We'll use this to compute CREATE2 addresses consistently + base_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety pattern_count = 0 - while len(deploy_bytecode) < max_contract_size - 100: + while len(base_bytecode) < max_contract_size - 100: unique_value = Hash(pattern_count) - deploy_bytecode += Op.PUSH32[unique_value] + Op.POP + base_bytecode += Op.PUSH32[unique_value] + Op.POP pattern_count += 1 - while len(deploy_bytecode) < max_contract_size: - deploy_bytecode += Op.JUMPDEST + while len(base_bytecode) < max_contract_size: + base_bytecode += Op.JUMPDEST - assert len(deploy_bytecode) == max_contract_size, ( - f"Contract size mismatch: {len(deploy_bytecode)}" + assert len(base_bytecode) == max_contract_size, ( + f"Base bytecode size mismatch: {len(base_bytecode)}" ) - # Init code that returns the bytecode - init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + deploy_bytecode + # Init code that returns the bytecode (used for CREATE2 address calculation) + init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + base_bytecode init_code_hash = keccak256(bytes(init_code)) # Factory address that would deploy contracts with CREATE2 factory_address = pre.deploy_contract(code=Op.STOP) # Simple factory placeholder - # Pre-deploy all contracts at CREATE2 addresses using sequential salts + # Pre-deploy all contracts at CREATE2 addresses with UNIQUE bytecode + # Note: Each contract gets different bytecode to prevent storage deduplication. + # Hash(salt * 1000000 + pattern_count) ensures unique values per contract. + # This doesn't affect gas costs since bytecode generation happens in test setup. deployed_contracts = [] # Track for post-state validation for salt in range(num_contracts): - # Calculate the CREATE2 address + # Generate unique bytecode for this specific contract + deploy_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety + pattern_count = 0 + while len(deploy_bytecode) < max_contract_size - 100: + # Use salt to make each contract's bytecode unique + unique_value = Hash(salt * 1000000 + pattern_count) + deploy_bytecode += Op.PUSH32[unique_value] + Op.POP + pattern_count += 1 + while len(deploy_bytecode) < max_contract_size: + deploy_bytecode += Op.JUMPDEST + + assert len(deploy_bytecode) == max_contract_size, ( + f"Contract size mismatch: {len(deploy_bytecode)}" + ) + + # Calculate the CREATE2 address (using base init_code for consistent addresses) create2_addr = calculate_create2_address(factory_address, salt, bytes(init_code)) - # Deploy at the calculated address + # Deploy at the calculated address with unique bytecode pre[create2_addr] = Account(code=deploy_bytecode) deployed_contracts.append(create2_addr) @@ -408,6 +433,10 @@ def test_bloatnet_extcodecopy_balance( """ Test that maximizes actual I/O reads using BALANCE + EXTCODECOPY combination. + Unlike the EXTCODESIZE test, this test gives each contract UNIQUE bytecode. + This ensures EXTCODECOPY actually reads different data from each contract, + maximizing the I/O load on the client's storage system. + This test uses CREATE2 deterministic addressing to avoid bytecode size limitations. It achieves maximum data reads from disk by: 1. Pre-deploying many 24KB contracts with unique bytecode @@ -478,7 +507,8 @@ def test_bloatnet_extcodecopy_balance( # This test scales automatically: ~1000 contracts at 90M gas, ~10,000 at 900M gas num_contracts = int(available_gas_for_access // cost_per_iteration) - # Generate base bytecode template + # Generate a base bytecode template for init_code_hash calculation + # We'll use this to compute CREATE2 addresses consistently base_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety pattern_count = 0 while len(base_bytecode) < max_contract_size - 100: @@ -492,20 +522,24 @@ def test_bloatnet_extcodecopy_balance( f"Base bytecode size mismatch: {len(base_bytecode)}" ) - # Init code that returns the bytecode + # Init code that returns the bytecode (used for CREATE2 address calculation) init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + base_bytecode init_code_hash = keccak256(bytes(init_code)) # Factory address for CREATE2 factory_address = pre.deploy_contract(code=Op.STOP) # Simple factory placeholder - # Pre-deploy all contracts at CREATE2 addresses with unique bytecode + # Pre-deploy all contracts at CREATE2 addresses with UNIQUE bytecode + # Each contract gets different bytecode to maximize I/O when EXTCODECOPY reads them. + # Hash(salt * 1000000 + pattern_count) ensures unique data per contract. + # This forces actual disk reads instead of cache hits during the benchmark. deployed_contracts = [] # Track for post-state validation for salt in range(num_contracts): - # Generate unique bytecode for this contract - bytecode = Bytecode(Op.STOP) + # Generate unique bytecode for this specific contract + bytecode = Bytecode(Op.STOP) # First byte is STOP for safety pattern_count = 0 while len(bytecode) < max_contract_size - 100: + # Use salt to make each contract's bytecode unique unique_value = Hash(salt * 1000000 + pattern_count) bytecode += Op.PUSH32[unique_value] + Op.POP pattern_count += 1 @@ -514,8 +548,7 @@ def test_bloatnet_extcodecopy_balance( assert len(bytecode) == max_contract_size, f"Contract size mismatch: {len(bytecode)}" - # Calculate CREATE2 address using the base init code - # Note: In real implementation, each contract has unique bytecode but same init structure + # Calculate CREATE2 address (using base init_code for consistent addresses) create2_addr = calculate_create2_address(factory_address, salt, bytes(init_code)) # Deploy at the calculated address with unique bytecode From 0e889d782b6315a2886f1dbe91513f80194d53be Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 11 Sep 2025 16:05:40 +0200 Subject: [PATCH 24/54] refactor(benchmark): replace custom CREATE2 address calculation with utility function - Remove the custom `calculate_create2_address` function in favor of the `compute_create2_address` utility. - Update tests to utilize the new utility for consistent CREATE2 address calculations. - Simplify code by eliminating unnecessary complexity in address calculation logic. - Ensure that the CREATE2 prefix is directly set to 0xFF in the memory operation for clarity. --- tests/benchmark/test_bloatnet.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index e1d3e2cda83..5125a0450fc 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -9,7 +9,6 @@ from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, - Address, Alloc, Block, BlockchainTestFiller, @@ -18,6 +17,7 @@ Storage, Transaction, While, + compute_create2_address, keccak256, ) from ethereum_test_tools.vm.opcode import Opcodes as Op @@ -26,27 +26,9 @@ REFERENCE_SPEC_VERSION = "0.1" # Constants for CREATE2 address calculation -CREATE2_PREFIX = 0xFF CREATE2_MEMORY_OFFSET = 0x1000 # Offset for CREATE2 calculation to avoid collision -def calculate_create2_address(deployer_address, salt: int, init_code: bytes): - """Calculate CREATE2 deterministic address.""" - # Handle both string and Address types - if isinstance(deployer_address, str): - addr_hex = deployer_address[2:] if deployer_address.startswith("0x") else deployer_address - deployer_bytes = bytes.fromhex(addr_hex) - else: - # Assume it's an Address object with bytes representation - deployer_bytes = bytes(deployer_address) - salt_bytes = salt.to_bytes(32, "big") - init_code_hash = keccak256(init_code) - - packed = bytes([CREATE2_PREFIX]) + deployer_bytes + salt_bytes + init_code_hash - address_hash = keccak256(packed) - return Address(address_hash[-20:]) - - def generate_create2_address_calculation( factory_address, init_code_hash: bytes, @@ -59,7 +41,7 @@ def generate_create2_address_calculation( base = CREATE2_MEMORY_OFFSET # Store 0xFF prefix at memory[base] - code += Op.PUSH1(CREATE2_PREFIX) + Op.PUSH2(base) + Op.MSTORE8 + code += Op.PUSH1(0xFF) + Op.PUSH2(base) + Op.MSTORE8 # Store factory address at memory[base+1:base+21] # Handle both string and Address types @@ -349,7 +331,9 @@ def test_bloatnet_extcodesize_balance( ) # Calculate the CREATE2 address (using base init_code for consistent addresses) - create2_addr = calculate_create2_address(factory_address, salt, bytes(init_code)) + create2_addr = compute_create2_address( + address=factory_address, salt=salt, initcode=bytes(init_code) + ) # Deploy at the calculated address with unique bytecode pre[create2_addr] = Account(code=deploy_bytecode) deployed_contracts.append(create2_addr) @@ -549,7 +533,9 @@ def test_bloatnet_extcodecopy_balance( assert len(bytecode) == max_contract_size, f"Contract size mismatch: {len(bytecode)}" # Calculate CREATE2 address (using base init_code for consistent addresses) - create2_addr = calculate_create2_address(factory_address, salt, bytes(init_code)) + create2_addr = compute_create2_address( + address=factory_address, salt=salt, initcode=bytes(init_code) + ) # Deploy at the calculated address with unique bytecode pre[create2_addr] = Account(code=bytecode) From e4583b67da46703c0e2fb10dfa9c64873075b110 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 17 Sep 2025 15:58:19 +0200 Subject: [PATCH 25/54] CREATE2 factory approach working --- scripts/deploy_bloatnet_simple.py | 222 +++++++++++ scripts/deploy_create2_factory.py | 113 ++++++ scripts/test_create2.py | 91 +++++ tests/benchmark/test_bloatnet.py | 636 ++++++------------------------ 4 files changed, 539 insertions(+), 523 deletions(-) create mode 100644 scripts/deploy_bloatnet_simple.py create mode 100644 scripts/deploy_create2_factory.py create mode 100644 scripts/test_create2.py diff --git a/scripts/deploy_bloatnet_simple.py b/scripts/deploy_bloatnet_simple.py new file mode 100644 index 00000000000..4e1ae3af974 --- /dev/null +++ b/scripts/deploy_bloatnet_simple.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +""" +CREATE2 deployment script for bloatnet benchmark contracts. +Uses a factory pattern to deploy contracts at deterministic addresses. + +Based on the pattern from EIP-7997, this deploys contracts using CREATE2 +so they can be accessed from any account and reused across tests. +""" + +import argparse +import sys +import subprocess +import json +from web3 import Web3 +from eth_utils import keccak + +def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = None): + """Deploy contracts using CREATE2 factory pattern.""" + + # Connect to Geth + w3 = Web3(Web3.HTTPProvider(rpc_url)) + if not w3.is_connected(): + print(f"Failed to connect to {rpc_url}") + sys.exit(1) + + test_account = w3.eth.accounts[0] + print(f"Using test account: {test_account}") + print(f"Balance: {w3.eth.get_balance(test_account) / 10**18:.4f} ETH") + + # Step 1: Check if factory exists, if not deploy it + if factory_address: + # Verify factory exists at the provided address + factory_code = w3.eth.get_code(factory_address) + if len(factory_code) > 0: + print(f"\nUsing existing CREATE2 factory at: {factory_address}") + else: + print(f"\nNo factory found at {factory_address}") + print("Deploying new CREATE2 factory...") + + # Call the deploy_create2_factory.py script + try: + result = subprocess.run( + [sys.executable, "scripts/deploy_create2_factory.py", "--rpc-url", rpc_url], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("Failed to deploy factory:") + print(result.stderr) + sys.exit(1) + + # Extract factory address from output + for line in result.stdout.split('\n'): + if 'Factory address:' in line: + factory_address = line.split(':')[1].strip() + print(f"Factory deployed at: {factory_address}") + break + else: + print("Could not extract factory address from deployment output") + sys.exit(1) + + except Exception as e: + print(f"Error deploying factory: {e}") + sys.exit(1) + else: + print("\nNo factory address provided, deploying new CREATE2 factory...") + + # Call the deploy_create2_factory.py script + try: + result = subprocess.run( + [sys.executable, "scripts/deploy_create2_factory.py", "--rpc-url", rpc_url], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("Failed to deploy factory:") + print(result.stderr) + sys.exit(1) + + # Extract factory address from output + for line in result.stdout.split('\n'): + if 'Factory address:' in line: + factory_address = line.split(':')[1].strip() + print(f"Factory deployed at: {factory_address}") + break + else: + print("Could not extract factory address from deployment output") + sys.exit(1) + + except Exception as e: + print(f"Error deploying factory: {e}") + sys.exit(1) + + # Step 2: Generate init code for 24KB contracts + # Init code copies runtime bytecode to memory and returns it + init_code = bytearray() + + # Push bytecode size, push code offset, push memory offset, CODECOPY, push size, push offset, RETURN + bytecode_size = 24576 + init_size = 13 # Size of init code instructions + + # PUSH2 bytecode_size, PUSH1 init_size, PUSH1 0, CODECOPY + init_code.extend([ + 0x61, (bytecode_size >> 8) & 0xFF, bytecode_size & 0xFF, # PUSH2 bytecode_size + 0x60, init_size, # PUSH1 init_size (offset where runtime code starts) + 0x60, 0x00, # PUSH1 0 (dest in memory) + 0x39, # CODECOPY + ]) + + # PUSH2 bytecode_size, PUSH1 0, RETURN + init_code.extend([ + 0x61, (bytecode_size >> 8) & 0xFF, bytecode_size & 0xFF, # PUSH2 bytecode_size + 0x60, 0x00, # PUSH1 0 (offset in memory) + 0xF3 # RETURN + ]) + + # Generate unique 24KB runtime bytecode to prevent deduplication + runtime_bytecode = bytearray([0x00]) # Start with STOP + + # Fill with unique pattern + pattern_count = 0 + while len(runtime_bytecode) < bytecode_size - 100: + # Use a simple pattern that's still unique per contract + unique_value = keccak(f"bloatnet_{pattern_count}".encode()) + runtime_bytecode.append(0x7F) # PUSH32 + runtime_bytecode.extend(unique_value[:31]) # Use 31 bytes of hash + runtime_bytecode.append(0x50) # POP + pattern_count += 1 + + # Fill rest with JUMPDEST + while len(runtime_bytecode) < bytecode_size: + runtime_bytecode.append(0x5B) + + # Combine init code and runtime bytecode + full_init_code = bytes(init_code) + bytes(runtime_bytecode) + init_code_hash = keccak(full_init_code) + + print(f"\nInit code hash: 0x{init_code_hash.hex()}") + print(f"Init code size: {len(full_init_code)} bytes") + + # Step 3: Deploy contracts using factory + print(f"\nDeploying {num_contracts} contracts using CREATE2...") + + deployed = [] + for salt in range(num_contracts): + if salt % 100 == 0: + print(f"Progress: {salt}/{num_contracts}") + + # Factory expects: salt (32 bytes) + bytecode + call_data = ( + salt.to_bytes(32, 'big') + # salt + full_init_code # the init code + ) + + try: + tx_hash = w3.eth.send_transaction({ + 'from': test_account, + 'to': factory_address, + 'data': '0x' + call_data.hex(), + 'gas': 10000000 + }) + + receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=10) + + if receipt.status == 1: + # Calculate CREATE2 address + create2_input = ( + b'\xff' + + bytes.fromhex(factory_address[2:]) + + salt.to_bytes(32, 'big') + + init_code_hash + ) + contract_address = '0x' + keccak(create2_input)[-20:].hex() + deployed.append(contract_address) + + if (salt + 1) % 100 == 0 or salt == num_contracts - 1: + print(f" [{salt+1}/{num_contracts}] Deployed at {contract_address}") + else: + print(f" [{salt+1}/{num_contracts}] Failed") + + except Exception as e: + print(f" [{salt+1}/{num_contracts}] Error: {e}") + break + + print(f"\nDeployed {len(deployed)} contracts") + + if deployed: + print(f"\nContract addresses:") + print(f"First: {deployed[0]}") + print(f"Last: {deployed[-1]}") + + print(f"\nFor test configuration:") + print(f"FACTORY_ADDRESS = Address(\"{factory_address}\")") + print(f"INIT_CODE_HASH = bytes.fromhex(\"{init_code_hash.hex()}\")") + print(f"NUM_CONTRACTS = {len(deployed)}") + +def main(): + parser = argparse.ArgumentParser(description="Deploy bloatnet contracts using CREATE2") + parser.add_argument( + "--num-contracts", + type=int, + default=100, + help="Number of contracts to deploy" + ) + parser.add_argument( + "--rpc-url", + default="http://127.0.0.1:8545", + help="RPC URL" + ) + parser.add_argument( + "--factory-address", + default=None, + help="CREATE2 factory address (deploys new one if not provided or if address doesn't have code)" + ) + + args = parser.parse_args() + deploy_contracts(args.rpc_url, args.num_contracts, args.factory_address) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/deploy_create2_factory.py b/scripts/deploy_create2_factory.py new file mode 100644 index 00000000000..292fc419ea4 --- /dev/null +++ b/scripts/deploy_create2_factory.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +""" +Deploy a simple CREATE2 factory for benchmark tests. +This factory can be reused across all tests and allows deterministic addresses. +""" + +import argparse +from web3 import Web3 +from eth_utils import keccak + +def deploy_factory(rpc_url: str): + """Deploy a minimal CREATE2 factory.""" + + # Connect to Geth + w3 = Web3(Web3.HTTPProvider(rpc_url)) + if not w3.is_connected(): + print(f"Failed to connect to {rpc_url}") + return None + + test_account = w3.eth.accounts[0] + print(f"Using test account: {test_account}") + + # Minimal CREATE2 factory bytecode + # Takes salt (first 32 bytes) and bytecode (rest) from calldata + # Returns the deployed address + factory_bytecode = bytes([ + # Runtime code + 0x60, 0x00, # PUSH1 0x00 (value for CREATE2) + 0x60, 0x00, # PUSH1 0x00 (salt position in calldata) + 0x35, # CALLDATALOAD (load salt) + 0x60, 0x20, # PUSH1 0x20 (bytecode starts at position 32) + 0x36, # CALLDATASIZE + 0x60, 0x20, # PUSH1 0x20 + 0x03, # SUB (bytecode length = calldatasize - 32) + 0x80, # DUP1 (duplicate bytecode length) + 0x60, 0x20, # PUSH1 0x20 (source position in calldata) + 0x60, 0x00, # PUSH1 0x00 (dest position in memory) + 0x37, # CALLDATACOPY (copy bytecode to memory) + 0xF5, # CREATE2 (value=0, mem_offset=0, mem_size, salt) + 0x60, 0x00, # PUSH1 0x00 + 0x52, # MSTORE (store address at position 0) + 0x60, 0x20, # PUSH1 0x20 + 0x60, 0x00, # PUSH1 0x00 + 0xF3, # RETURN (return the address) + ]) + + # Deploy the factory + print("\nDeploying CREATE2 factory...") + tx_hash = w3.eth.send_transaction({ + 'from': test_account, + 'data': '0x' + factory_bytecode.hex(), + 'gas': 3000000 + }) + + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt.status != 1: + print("Failed to deploy factory") + return None + + factory_address = receipt.contractAddress + print(f"βœ… Factory deployed at: {factory_address}") + + # Test the factory with a simple contract + print("\nTesting factory...") + test_bytecode = bytes([0x00]) # Simple STOP opcode + test_salt = 0 + + calldata = test_salt.to_bytes(32, 'big') + test_bytecode + + # Use eth_call to get the address that would be created + result = w3.eth.call({ + 'to': factory_address, + 'data': '0x' + calldata.hex() + }) + + if result: + test_addr = '0x' + result[-20:].hex() + print(f"Test deployment would create: {test_addr}") + + # Calculate expected CREATE2 address + expected = keccak( + b'\xff' + + bytes.fromhex(factory_address[2:]) + + test_salt.to_bytes(32, 'big') + + keccak(test_bytecode) + )[-20:] + expected_addr = '0x' + expected.hex() + print(f"Expected CREATE2 address: {expected_addr}") + + return factory_address + +def main(): + parser = argparse.ArgumentParser(description="Deploy CREATE2 factory") + parser.add_argument( + "--rpc-url", + default="http://127.0.0.1:8545", + help="RPC URL (default: http://127.0.0.1:8545)" + ) + + args = parser.parse_args() + factory_address = deploy_factory(args.rpc_url) + + if factory_address: + print("\n" + "="*60) + print("Factory deployed successfully!") + print(f"Factory address: {factory_address}") + print("\nAdd this to your test configuration:") + print(f'FACTORY_ADDRESS = Address("{factory_address}")') + print("="*60) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/test_create2.py b/scripts/test_create2.py new file mode 100644 index 00000000000..62349e8f289 --- /dev/null +++ b/scripts/test_create2.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Test CREATE2 factory deployment.""" + +from web3 import Web3 +from eth_utils import keccak + +# Connect to Geth +w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) +if not w3.is_connected(): + print("Failed to connect to Geth") + exit(1) + +test_account = w3.eth.accounts[0] +print(f"Using test account: {test_account}") +print(f"Balance: {w3.eth.get_balance(test_account) / 10**18:.4f} ETH") + +# Simple CREATE2 factory that returns the deployed address +factory_bytecode = ( + # Constructor: return runtime code + "60" + "1f" # PUSH1 0x1F (runtime size = 31) + "80" # DUP1 + "60" + "0a" # PUSH1 0x0A (runtime offset) + "60" + "00" # PUSH1 0x00 (memory dest) + "39" # CODECOPY + "60" + "00" # PUSH1 0x00 (return offset) + "f3" # RETURN + + # Runtime: minimal CREATE2 + "36" # CALLDATASIZE + "60" + "00" # PUSH1 0x00 + "60" + "00" # PUSH1 0x00 + "37" # CALLDATACOPY (copy all calldata to memory) + + "60" + "00" # PUSH1 0x00 (salt - using 0 for simplicity) + "36" # CALLDATASIZE (size of init code) + "60" + "00" # PUSH1 0x00 (offset in memory) + "60" + "00" # PUSH1 0x00 (value) + "f5" # CREATE2 + + "60" + "00" # PUSH1 0x00 + "52" # MSTORE (store address at 0) + "60" + "20" # PUSH1 0x20 + "60" + "00" # PUSH1 0x00 + "f3" # RETURN (return address) +) + +# Deploy factory +factory_tx = w3.eth.send_transaction({ + 'from': test_account, + 'data': '0x' + factory_bytecode, + 'gas': 3000000 +}) + +factory_receipt = w3.eth.wait_for_transaction_receipt(factory_tx) +if factory_receipt.status != 1: + print("Failed to deploy factory") + exit(1) + +factory_address = factory_receipt.contractAddress +print(f"\nFactory deployed at: {factory_address}") + +# Create simple contract bytecode (just returns 42) +simple_bytecode = "602a60005260206000f3" # PUSH1 42, PUSH1 0, MSTORE, PUSH1 32, PUSH1 0, RETURN + +# Deploy using factory +print("\nDeploying contract via CREATE2...") +deploy_tx = w3.eth.send_transaction({ + 'from': test_account, + 'to': factory_address, + 'data': '0x' + simple_bytecode, + 'gas': 1000000 +}) + +deploy_receipt = w3.eth.wait_for_transaction_receipt(deploy_tx) +print(f"Transaction status: {deploy_receipt.status}") + +# Get return value (the deployed address) +result = w3.eth.call({ + 'to': factory_address, + 'data': '0x' + simple_bytecode +}) + +if result: + deployed_addr = '0x' + result[-20:].hex() + print(f"Contract deployed at: {deployed_addr}") + + # Verify by checking code + code = w3.eth.get_code(deployed_addr) + print(f"Deployed code length: {len(code)} bytes") +else: + print("No return value from factory") \ No newline at end of file diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/test_bloatnet.py index 5125a0450fc..74e7fcb9d65 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/test_bloatnet.py @@ -1,618 +1,208 @@ """ -abstract: Tests [BloatNet](https://bloatnet.info) - Test cases for [BloatNet](https://bloatnet.info). +abstract: Optimized BloatNet tests using CREATE2 pre-deployed contracts. + + This test uses CREATE2 factory-deployed contracts at deterministic addresses. + Deploy contracts using: python3 scripts/deploy_bloatnet_simple.py --num-contracts 1838 + + The CREATE2 pattern allows: + - Contracts to be deployed from any account + - Reuse across different test scenarios + - Deterministic addresses regardless of deployer nonce """ import pytest -from ethereum_test_base_types import HashInt from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, + Address, Alloc, Block, BlockchainTestFiller, Bytecode, - Hash, - Storage, Transaction, - While, - compute_create2_address, keccak256, ) from ethereum_test_tools.vm.opcode import Opcodes as Op -REFERENCE_SPEC_GIT_PATH = "DUMMY/eip-DUMMY.md" -REFERENCE_SPEC_VERSION = "0.1" - -# Constants for CREATE2 address calculation -CREATE2_MEMORY_OFFSET = 0x1000 # Offset for CREATE2 calculation to avoid collision - - -def generate_create2_address_calculation( - factory_address, - init_code_hash: bytes, -) -> Bytecode: - """Generate EVM bytecode to calculate a CREATE2 address with salt on stack.""" - code = Bytecode() - - # Memory layout at CREATE2_MEMORY_OFFSET: - # [0xFF][factory_address][salt][init_code_hash] - base = CREATE2_MEMORY_OFFSET - - # Store 0xFF prefix at memory[base] - code += Op.PUSH1(0xFF) + Op.PUSH2(base) + Op.MSTORE8 +REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" +REFERENCE_SPEC_VERSION = "1.0" - # Store factory address at memory[base+1:base+21] - # Handle both string and Address types - if isinstance(factory_address, str): - addr_hex = factory_address[2:] if factory_address.startswith("0x") else factory_address - factory_bytes = bytes.fromhex(addr_hex) - else: - # Assume it's an Address object with bytes representation - factory_bytes = bytes(factory_address) - code += Op.PUSH20(int.from_bytes(factory_bytes, "big")) + Op.PUSH2(base + 1) + Op.MSTORE +# Configuration for CREATE2 pre-deployed contracts +# These values must match what the deployment script generates +# CREATE2 factory address +FACTORY_ADDRESS = Address("0x07EADb2f6b02Bb9fE994c0AeFf106625c0d3C93f") +# Init code hash from deployment +INIT_CODE_HASH = bytes.fromhex("8eba8b9d04aea4e6f3d6601ad364021e8e37b6282a0f57e40c635ba7f80aa0cb") +NUM_DEPLOYED_CONTRACTS = 1838 # Exact contracts needed for 5M gas test - # Store salt at memory[base+21:base+53] (base+0x15) - # Assumes salt is already on stack - code += Op.PUSH2(base + 0x15) + Op.MSTORE - # Store init code hash at memory[base+53:base+85] (base+0x35) - code += Op.PUSH32(int.from_bytes(init_code_hash, "big")) + Op.PUSH2(base + 0x35) + Op.MSTORE - - # Calculate keccak256 of 85 bytes starting at memory[base] - code += Op.PUSH1(0x55) + Op.PUSH2(base) + Op.SHA3 - - # The address is the last 20 bytes (already on stack) - return code +def calculate_create2_address(factory: Address, salt: int, init_code_hash: bytes) -> Address: + """Calculate CREATE2 address from factory, salt, and init code hash.""" + create2_input = b"\xff" + bytes(factory) + salt.to_bytes(32, "big") + init_code_hash + addr_bytes = keccak256(create2_input)[12:] + return Address(addr_bytes) @pytest.mark.valid_from("Prague") -@pytest.mark.parametrize("final_storage_value", [0x02 << 248, 0x02]) -def test_bloatnet( +def test_bloatnet_balance_extcodesize( blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, - final_storage_value: int, gas_benchmark_value: int, ): """ - A test that calls a contract with many SSTOREs. + BloatNet test using BALANCE + EXTCODESIZE pattern. - The first block will have many SSTORES that go from 0 -> 1 - and the 2nd block will have many SSTORES that go from 1 -> 2 + This test: + 1. Uses 100+ pre-deployed 24KB contracts + 2. Calls BALANCE (cold) then EXTCODESIZE (warm) on each + 3. Maximizes cache eviction by accessing many contracts + 4. Runs with benchmark gas values (5M, 50M, 500M) """ - # Get gas costs for the current fork - gas_costs = fork.gas_costs() - - # this is only used for computing the intinsic gas - data = final_storage_value.to_bytes(32, "big").rstrip(b"\x00") - - storage = Storage() - - # Initial gas for PUSH0 + CALLDATALOAD + POP (at the end) - totalgas = gas_costs.G_BASE * 2 + gas_costs.G_VERY_LOW - totalgas = totalgas + fork.transaction_intrinsic_cost_calculator()(calldata=data) - gas_increment = gas_costs.G_VERY_LOW * 2 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD - sstore_code = Op.PUSH0 + Op.CALLDATALOAD - storage_slot: int = 0 - while totalgas + gas_increment < gas_benchmark_value: - totalgas += gas_increment - sstore_code = sstore_code + Op.SSTORE(storage_slot, Op.DUP1) - storage[storage_slot] = final_storage_value - storage_slot += 1 - - sstore_code = sstore_code + Op.POP # Drop last value on the stack - - sender = pre.fund_eoa() - print(sender) - contract_address = pre.deploy_contract( - code=sstore_code, - storage=Storage(), - ) - - tx_0_1 = Transaction( - to=contract_address, - gas_limit=gas_benchmark_value, - data=(final_storage_value // 2).to_bytes(32, "big").rstrip(b"\x00"), - value=0, - sender=sender, - ) - tx_1_2 = Transaction( - to=contract_address, - gas_limit=gas_benchmark_value, - data=final_storage_value.to_bytes(32, "big").rstrip(b"\x00"), - value=0, - sender=sender, - ) - - post = {contract_address: Account(storage=storage)} - - blockchain_test(pre=pre, blocks=[Block(txs=[tx_0_1, tx_1_2])], post=post) - - -# Warm reads are very cheap, which means you can really fill a block -# with them. Only fill the block by a factor of SPEEDUP. -SPEEDUP: int = 100 - - -@pytest.mark.valid_from("Prague") -def test_bloatnet_sload_warm( - blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int -): - """Test that loads warm storage locations many times.""" gas_costs = fork.gas_costs() - # Pre-fill storage with values - num_slots = 100 # Number of storage slots to warm up - storage = Storage({HashInt(i): HashInt(0xDEADBEEF + i) for i in range(num_slots)}) - # Calculate gas costs - totalgas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - - # First pass - warm up all slots (cold access) - warmup_gas = num_slots * (gas_costs.G_COLD_SLOAD + gas_costs.G_BASE) - totalgas += warmup_gas - - # Calculate how many warm loads we can fit - gas_increment = gas_costs.G_WARM_SLOAD + gas_costs.G_BASE # Warm SLOAD + POP - remaining_gas = gas_benchmark_value - totalgas - num_warm_loads = remaining_gas // (SPEEDUP * gas_increment) - - # Build the complete code: warmup + repeated warm loads - sload_code = Op.SLOAD(0) + Op.POP if num_slots > 0 else Op.STOP - for i in range(1, num_slots): - sload_code = sload_code + Op.SLOAD(i) + Op.POP - for i in range(num_warm_loads): - sload_code = sload_code + Op.SLOAD(i % num_slots) + Op.POP - - sender = pre.fund_eoa() - contract_address = pre.deploy_contract( - code=sload_code, - storage=storage, - ) - - tx = Transaction( - to=contract_address, - gas_limit=gas_benchmark_value, - data=b"", - value=0, - sender=sender, - ) - - post = {contract_address: Account(storage=storage)} - blockchain_test(pre=pre, blocks=[Block(txs=[tx])], post=post) - - -@pytest.mark.valid_from("Prague") -def test_bloatnet_sload_cold( - blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int -): - """Test that loads many different cold storage locations.""" - gas_costs = fork.gas_costs() - - # Calculate gas costs and max slots - totalgas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # PUSH + Cold SLOAD + POP - gas_increment = gas_costs.G_VERY_LOW + gas_costs.G_COLD_SLOAD + gas_costs.G_BASE - max_slots = (gas_benchmark_value - totalgas) // gas_increment - - # Build storage and code for all slots - storage = Storage({HashInt(i): HashInt(0xC0FFEE + i) for i in range(max_slots)}) - sload_code = Op.SLOAD(0) + Op.POP if max_slots > 0 else Op.STOP - for i in range(1, max_slots): - sload_code = sload_code + Op.SLOAD(i) + Op.POP - - sender = pre.fund_eoa() - contract_address = pre.deploy_contract( - code=sload_code, - storage=storage, - ) - - tx = Transaction( - to=contract_address, - gas_limit=gas_benchmark_value, - data=b"", - value=0, - sender=sender, - ) - - post = {contract_address: Account(storage=storage)} - blockchain_test(pre=pre, blocks=[Block(txs=[tx])], post=post) - - -# Storage slot for success flag -SUCCESS_FLAG_SLOT = 0 -SUCCESS_FLAG_VALUE = 42 -EXTCODECOPY_MEMORY_OFFSET = 0x1000 # 4KB offset to avoid conflicts with address storage - - -@pytest.mark.valid_from("Prague") -def test_bloatnet_extcodesize_balance( - blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int -): - """ - Test that maximizes I/O reads by combining cold BALANCE with warm EXTCODESIZE calls. - - This test uses CREATE2 deterministic addressing to avoid bytecode size limitations. - Each contract has UNIQUE bytecode to prevent client-side storage deduplication and - cache optimization. Without unique bytecode, clients could store it once and reference - it for all contracts, defeating the I/O benchmark purpose. - - It deploys many 24kB contracts with unique bytecode, then: - 1. Calls BALANCE on all contracts (cold access) to warm them and fill cache - 2. Calls EXTCODESIZE on all contracts (warm access) hoping cache evictions force re-reads - - Goal: Maximum I/O read operations with minimum gas consumption. - Note: Bytecode generation happens during test setup (not in the attack transaction), - so unique bytecode doesn't affect gas costs - we still access the same number of contracts. - """ - gas_costs = fork.gas_costs() - max_contract_size = fork.max_code_size() - - # Calculate costs for the attack transaction intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Calculate memory expansion cost for CREATE2 address calculation - # CREATE2 uses memory at offset 0x1000 for 85 bytes - memory_expansion_cost = fork.memory_expansion_gas_calculator()( - new_bytes=CREATE2_MEMORY_OFFSET + 85 - ) - - # Cost per iteration with CREATE2 address calculation - # Additional cost for CREATE2 address calculation (~75 gas) - create2_calc_cost = ( - gas_costs.G_VERY_LOW * 8 # Memory operations (MSTORE/MSTORE8) - + 30 - + 6 * 3 # KECCAK256 for 85 bytes - + gas_costs.G_VERY_LOW * 10 # Stack operations - ) - - cost_per_iteration = ( - create2_calc_cost # CREATE2 address calculation - + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE - + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (same account) - + gas_costs.G_BASE * 2 # POPs for results - + gas_costs.G_VERY_LOW * 3 # Loop overhead (DUP, PUSH1, ADD) - ) - - # Access costs in attack transaction - final_storage_cost = ( - gas_costs.G_STORAGE_RESET + gas_costs.G_COLD_SLOAD + gas_costs.G_VERY_LOW * 2 - ) - available_gas_for_access = ( - gas_benchmark_value - intrinsic_gas - final_storage_cost - memory_expansion_cost + # Cost per contract access + cost_per_contract = ( + 3 # PUSH20 for address + + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) + + gas_costs.G_BASE # POP balance + + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100) + + gas_costs.G_BASE # POP code size ) - # Calculate maximum contracts we can access - num_contracts = int(available_gas_for_access // cost_per_iteration) - - # Generate a base bytecode template for init_code_hash calculation - # We'll use this to compute CREATE2 addresses consistently - base_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety - pattern_count = 0 - while len(base_bytecode) < max_contract_size - 100: - unique_value = Hash(pattern_count) - base_bytecode += Op.PUSH32[unique_value] + Op.POP - pattern_count += 1 - while len(base_bytecode) < max_contract_size: - base_bytecode += Op.JUMPDEST - - assert len(base_bytecode) == max_contract_size, ( - f"Base bytecode size mismatch: {len(base_bytecode)}" - ) + # Calculate how many contracts to access + available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup + num_contracts = min(int(available_gas // cost_per_contract), NUM_DEPLOYED_CONTRACTS) - # Init code that returns the bytecode (used for CREATE2 address calculation) - init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + base_bytecode - init_code_hash = keccak256(bytes(init_code)) - - # Factory address that would deploy contracts with CREATE2 - factory_address = pre.deploy_contract(code=Op.STOP) # Simple factory placeholder - - # Pre-deploy all contracts at CREATE2 addresses with UNIQUE bytecode - # Note: Each contract gets different bytecode to prevent storage deduplication. - # Hash(salt * 1000000 + pattern_count) ensures unique values per contract. - # This doesn't affect gas costs since bytecode generation happens in test setup. - deployed_contracts = [] # Track for post-state validation - for salt in range(num_contracts): - # Generate unique bytecode for this specific contract - deploy_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety - pattern_count = 0 - while len(deploy_bytecode) < max_contract_size - 100: - # Use salt to make each contract's bytecode unique - unique_value = Hash(salt * 1000000 + pattern_count) - deploy_bytecode += Op.PUSH32[unique_value] + Op.POP - pattern_count += 1 - while len(deploy_bytecode) < max_contract_size: - deploy_bytecode += Op.JUMPDEST - - assert len(deploy_bytecode) == max_contract_size, ( - f"Contract size mismatch: {len(deploy_bytecode)}" - ) + # Generate attack contract with unrolled loop + attack_code = Bytecode() - # Calculate the CREATE2 address (using base init_code for consistent addresses) - create2_addr = compute_create2_address( - address=factory_address, salt=salt, initcode=bytes(init_code) + # Pre-calculate all contract addresses using CREATE2 + for i in range(num_contracts): + addr = calculate_create2_address(FACTORY_ADDRESS, i, INIT_CODE_HASH) + attack_code += ( + Op.PUSH20[int.from_bytes(bytes(addr), "big")] + + Op.DUP1 + + Op.BALANCE + + Op.POP + + Op.EXTCODESIZE + + Op.POP ) - # Deploy at the calculated address with unique bytecode - pre[create2_addr] = Account(code=deploy_bytecode) - deployed_contracts.append(create2_addr) - # Create the attack contract that calculates CREATE2 addresses and calls operations - attack_code = Bytecode() + # Success marker (benchmark tests typically run out of gas, but this is for smaller tests) + attack_code += Op.PUSH1[1] + Op.PUSH0 + Op.SSTORE - # Pre-compute the CREATE2 calculation bytecode - create2_calc = generate_create2_address_calculation(factory_address, init_code_hash) - - # Main loop: iterate through all contracts - attack_code += Op.PUSH0 # Counter starts at 0 - attack_code += While( - body=( - # Calculate CREATE2 address for current salt (counter) - Op.DUP1 # Duplicate counter (salt) - + create2_calc # Calculate CREATE2 address (consumes salt, leaves address) - # Call BALANCE (cold access) - this warms the account - + Op.DUP1 # Duplicate address - + Op.BALANCE # Get balance - + Op.POP # Discard balance result - # Call EXTCODESIZE (warm access) - hoping cache was evicted - + Op.EXTCODESIZE # Get code size (address already on stack) - + Op.POP # Discard code size result - # Increment counter - + Op.PUSH1[1] - + Op.ADD - ), - condition=Op.GT(num_contracts, Op.DUP1), # num_contracts > counter - ) - attack_code += Op.POP # Clean up counter - # Store success flag for validation - attack_code += Op.SSTORE(SUCCESS_FLAG_SLOT, SUCCESS_FLAG_VALUE) - - # Pre-initialize storage slot 0 to avoid cold SSTORE cost - attack_address = pre.deploy_contract(code=attack_code, storage={SUCCESS_FLAG_SLOT: 1}) + # Deploy attack contract + attack_address = pre.deploy_contract(code=attack_code) # Attack transaction - attack_tx = Transaction( + tx = Transaction( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), ) - # Post-state validation + # Post-state: just verify attack contract exists + # Benchmark tests run out of gas, so no success flag post = { - # Verify attack completed successfully - attack_address: Account(storage={SUCCESS_FLAG_SLOT: SUCCESS_FLAG_VALUE}), + attack_address: Account(storage={}), } - # Verify all pre-deployed contracts still exist with their code intact - # We check that they have nonce=1 (contracts were deployed) - for contract_address in deployed_contracts: - post[contract_address] = Account( - nonce=1, # Contract exists and was deployed - ) - - # Calculate expected gas usage precisely - expected_benchmark_gas_used = ( - intrinsic_gas - + memory_expansion_cost - + (num_contracts * cost_per_iteration) - + final_storage_cost - ) - blockchain_test( pre=pre, - blocks=[ - Block(txs=[attack_tx]), # Execute the attack - ], + blocks=[Block(txs=[tx])], post=post, - exclude_full_post_state_in_output=True, # Reduce output size - expected_benchmark_gas_used=expected_benchmark_gas_used, ) @pytest.mark.valid_from("Prague") -def test_bloatnet_extcodecopy_balance( - blockchain_test: BlockchainTestFiller, pre: Alloc, fork: Fork, gas_benchmark_value: int +def test_bloatnet_balance_extcodecopy( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + fork: Fork, + gas_benchmark_value: int, ): """ - Test that maximizes actual I/O reads using BALANCE + EXTCODECOPY combination. + BloatNet test using BALANCE + EXTCODECOPY for maximum I/O. - Unlike the EXTCODESIZE test, this test gives each contract UNIQUE bytecode. - This ensures EXTCODECOPY actually reads different data from each contract, - maximizing the I/O load on the client's storage system. + This test forces actual bytecode reads from disk by: + 1. Using BALANCE (cold) to warm the account + 2. Using EXTCODECOPY (warm) to read the full 24KB bytecode - This test uses CREATE2 deterministic addressing to avoid bytecode size limitations. - It achieves maximum data reads from disk by: - 1. Pre-deploying many 24KB contracts with unique bytecode - 2. Calling BALANCE (cold) to warm the account - reads metadata (~200 bytes) - 3. Calling EXTCODECOPY (warm) to read the full 24KB bytecode - - The BALANCE + EXTCODECOPY pattern is optimal because: - - BALANCE warms the account, reducing EXTCODECOPY base cost from 2600 to 100 - - EXTCODECOPY forces reading the actual bytecode from disk (not just metadata) - - Total cost: ~5129 gas per contract for 24KB of data read (including CREATE2 calculation) - - This test reads 123x more data per contract than BALANCE + EXTCODESIZE pattern. + This pattern reads ~123x more data per gas than EXTCODESIZE. """ gas_costs = fork.gas_costs() max_contract_size = fork.max_code_size() - # Calculate costs for the attack transaction + # Calculate costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Calculate memory expansion cost for CREATE2 address calculation - # CREATE2 uses memory at offset 0x1000 for 85 bytes - memory_expansion_cost = fork.memory_expansion_gas_calculator()( - new_bytes=CREATE2_MEMORY_OFFSET + 85 - ) + # Memory expansion for EXTCODECOPY + max_memory_needed = max_contract_size * 10 # Limit to 10 contracts worth of memory + memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=max_memory_needed) - # Cost per iteration with CREATE2 address calculation - # Additional cost for CREATE2 address calculation (~75 gas) - create2_calc_cost = ( - gas_costs.G_VERY_LOW * 8 # Memory operations (MSTORE/MSTORE8) - + 30 - + 6 * 3 # KECCAK256 for 85 bytes - + gas_costs.G_VERY_LOW * 10 # Stack operations - ) - - # EXTCODECOPY copies full 24KB to memory + # Cost per contract with EXTCODECOPY words_to_copy = (max_contract_size + 31) // 32 # 768 words for 24KB - cost_per_iteration = ( - create2_calc_cost # CREATE2 address calculation - + gas_costs.G_VERY_LOW # DUP1 for address + cost_per_contract = ( + 3 # PUSH20 for address + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) - + gas_costs.G_BASE # POP balance result + + gas_costs.G_BASE # POP balance + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) + gas_costs.G_COPY * words_to_copy # Copy cost (3 * 768 = 2304) - + gas_costs.G_BASE # POP address - + gas_costs.G_VERY_LOW * 3 # Loop overhead (PUSH1, ADD, DUP for condition) - ) - - # Calculate memory expansion cost for EXTCODECOPY destination - # EXTCODECOPY writes 24KB at offset 0x1000 - extcodecopy_memory_expansion = fork.memory_expansion_gas_calculator()( - new_bytes=EXTCODECOPY_MEMORY_OFFSET + max_contract_size + + gas_costs.G_BASE * 4 # PUSH operations and POP ) - # CREATE2 calculation also uses memory (already accounted in memory_expansion_cost) - # Access costs in attack transaction - final_storage_cost = ( - gas_costs.G_STORAGE_RESET + gas_costs.G_COLD_SLOAD + gas_costs.G_VERY_LOW * 2 - ) - available_gas_for_access = ( - gas_benchmark_value - - intrinsic_gas - - final_storage_cost - - memory_expansion_cost - - extcodecopy_memory_expansion + # Calculate how many contracts to access + available_gas = gas_benchmark_value - intrinsic_gas - memory_cost - 1000 + num_contracts = min( + int(available_gas // cost_per_contract), + NUM_DEPLOYED_CONTRACTS, + 10, # Limit to avoid excessive memory usage in test ) - # Calculate maximum contracts we can access - # This test scales automatically: ~1000 contracts at 90M gas, ~10,000 at 900M gas - num_contracts = int(available_gas_for_access // cost_per_iteration) - - # Generate a base bytecode template for init_code_hash calculation - # We'll use this to compute CREATE2 addresses consistently - base_bytecode = Bytecode(Op.STOP) # First byte is STOP for safety - pattern_count = 0 - while len(base_bytecode) < max_contract_size - 100: - unique_value = Hash(pattern_count) - base_bytecode += Op.PUSH32[unique_value] + Op.POP - pattern_count += 1 - while len(base_bytecode) < max_contract_size: - base_bytecode += Op.JUMPDEST - - assert len(base_bytecode) == max_contract_size, ( - f"Base bytecode size mismatch: {len(base_bytecode)}" - ) - - # Init code that returns the bytecode (used for CREATE2 address calculation) - init_code = Op.CODECOPY(0, 0, Op.CODESIZE) + Op.RETURN(0, Op.CODESIZE) + base_bytecode - init_code_hash = keccak256(bytes(init_code)) - - # Factory address for CREATE2 - factory_address = pre.deploy_contract(code=Op.STOP) # Simple factory placeholder - - # Pre-deploy all contracts at CREATE2 addresses with UNIQUE bytecode - # Each contract gets different bytecode to maximize I/O when EXTCODECOPY reads them. - # Hash(salt * 1000000 + pattern_count) ensures unique data per contract. - # This forces actual disk reads instead of cache hits during the benchmark. - deployed_contracts = [] # Track for post-state validation - for salt in range(num_contracts): - # Generate unique bytecode for this specific contract - bytecode = Bytecode(Op.STOP) # First byte is STOP for safety - pattern_count = 0 - while len(bytecode) < max_contract_size - 100: - # Use salt to make each contract's bytecode unique - unique_value = Hash(salt * 1000000 + pattern_count) - bytecode += Op.PUSH32[unique_value] + Op.POP - pattern_count += 1 - while len(bytecode) < max_contract_size: - bytecode += Op.JUMPDEST - - assert len(bytecode) == max_contract_size, f"Contract size mismatch: {len(bytecode)}" - - # Calculate CREATE2 address (using base init_code for consistent addresses) - create2_addr = compute_create2_address( - address=factory_address, salt=salt, initcode=bytes(init_code) - ) - - # Deploy at the calculated address with unique bytecode - pre[create2_addr] = Account(code=bytecode) - deployed_contracts.append(create2_addr) - - # Create the attack contract that calculates CREATE2 addresses and calls operations + # Generate attack contract attack_code = Bytecode() + mem_offset = 0 + + # Access each contract using CREATE2 addresses + for i in range(num_contracts): + addr = calculate_create2_address(FACTORY_ADDRESS, i, INIT_CODE_HASH) + attack_code += ( + Op.PUSH20[int.from_bytes(bytes(addr), "big")] + + Op.DUP1 + + Op.BALANCE + + Op.POP + # EXTCODECOPY(addr, mem_offset, 0, 24KB) + + Op.PUSH2[max_contract_size] # size + + Op.PUSH1[0] # code offset + + Op.PUSH3[mem_offset] # memory offset + + Op.DUP4 # address + + Op.EXTCODECOPY + + Op.POP # clean up address + ) + mem_offset += max_contract_size - # Pre-compute the CREATE2 calculation bytecode - create2_calc = generate_create2_address_calculation(factory_address, init_code_hash) - - # Main loop: BALANCE + EXTCODECOPY for each contract - attack_code += Op.PUSH0 # Counter starts at 0 - attack_code += While( - body=( - # Calculate CREATE2 address for current salt (counter) - Op.DUP1 # Duplicate counter (salt) - + create2_calc # Calculate CREATE2 address (consumes salt, leaves address) - # Call BALANCE (cold access) - warms the account - + Op.DUP1 # Duplicate address for BALANCE - + Op.BALANCE # Get balance (cold, 2600 gas) - + Op.POP # Discard balance result - # Call EXTCODECOPY (warm access) - reads full 24KB bytecode - + Op.EXTCODECOPY( - address=Op.DUP1, # Use the same address (now warm) - dest_offset=EXTCODECOPY_MEMORY_OFFSET, # Copy to high memory - offset=0, # Start from beginning of bytecode - size=max_contract_size, # Copy full 24KB - ) - + Op.POP # Clean up address - # Increment counter - + Op.PUSH1[1] - + Op.ADD - ), - condition=Op.GT(num_contracts, Op.DUP1), # Fixed: num_contracts > counter - ) - attack_code += Op.POP # Clean up counter - # Store success flag for validation - attack_code += Op.SSTORE(SUCCESS_FLAG_SLOT, SUCCESS_FLAG_VALUE) - - # Pre-initialize storage slot 0 to avoid cold SSTORE cost - attack_address = pre.deploy_contract(code=attack_code, storage={SUCCESS_FLAG_SLOT: 1}) + # Deploy attack contract + attack_address = pre.deploy_contract(code=attack_code) # Attack transaction - attack_tx = Transaction( + tx = Transaction( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), ) - # Post-state validation + # Post-state post = { - # Verify attack completed successfully - attack_address: Account(storage={SUCCESS_FLAG_SLOT: SUCCESS_FLAG_VALUE}), + attack_address: Account(storage={}), } - # Verify all pre-deployed contracts still exist - for contract_address in deployed_contracts: - post[contract_address] = Account( - nonce=1, # Contract exists and was deployed - ) - - # Calculate expected gas usage precisely - expected_benchmark_gas_used = ( - intrinsic_gas - + memory_expansion_cost - + extcodecopy_memory_expansion - + (num_contracts * cost_per_iteration) - + final_storage_cost - ) - blockchain_test( pre=pre, - blocks=[ - Block(txs=[attack_tx]), # Execute the attack - ], + blocks=[Block(txs=[tx])], post=post, - exclude_full_post_state_in_output=True, # Reduce output size - expected_benchmark_gas_used=expected_benchmark_gas_used, ) From 06f9a63628fdc304a3ef431a6f5a9541d0d71fb6 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 17 Sep 2025 23:01:02 +0200 Subject: [PATCH 26/54] Version with EIP-7997 model working --- tests/benchmark/bloatnet/README.md | 105 ++++++++++++++ .../bloatnet}/deploy_bloatnet_simple.py | 132 ++++++++++-------- .../bloatnet}/deploy_create2_factory.py | 94 +++++++------ .../benchmark/{ => bloatnet}/test_bloatnet.py | 48 ++++++- 4 files changed, 273 insertions(+), 106 deletions(-) create mode 100644 tests/benchmark/bloatnet/README.md rename {scripts => tests/benchmark/bloatnet}/deploy_bloatnet_simple.py (65%) rename {scripts => tests/benchmark/bloatnet}/deploy_create2_factory.py (53%) rename tests/benchmark/{ => bloatnet}/test_bloatnet.py (78%) diff --git a/tests/benchmark/bloatnet/README.md b/tests/benchmark/bloatnet/README.md new file mode 100644 index 00000000000..40d9eae5525 --- /dev/null +++ b/tests/benchmark/bloatnet/README.md @@ -0,0 +1,105 @@ +# BloatNet Benchmark Tests setup guide + +## Overview + +The Bloatnet benchmarks work on the following fashion: +1. They usually require a previously-deployed state (usually quite large) which the benchmarks +will interact with. +2. The deployment script helpers help deploying the required bytecode for the specific tests. +3. The outpus of the deployment scripts get hardcoded into the codebase such that the benchmarks can interact with them. + +## Gas Cost Constants + +### BALANCE + EXTCODESIZE Pattern +**Gas per contract: 2,707** +- `PUSH20` (address): 3 gas +- `BALANCE` (cold access): 2,600 gas +- `POP`: 2 gas +- `EXTCODESIZE` (warm): 100 gas +- `POP`: 2 gas + +### BALANCE + EXTCODECOPY Pattern +**Gas per contract: ~5,007** +- `PUSH20` (address): 3 gas +- `BALANCE` (cold access): 2,600 gas +- `POP`: 2 gas +- `EXTCODECOPY` setup: ~100 gas +- `EXTCODECOPY` (24KB): ~2,300 gas +- `POP`: 2 gas + +## Required Contracts Calculation Example: + +### For BALANCE + EXTCODESIZE: +| Gas Limit | Contracts Needed | Calculation | +| --------- | ---------------- | ------------------- | +| 5M | 1,838 | 5,000,000 Γ· 2,707 | +| 50M | 18,380 | 50,000,000 Γ· 2,707 | +| 150M | 55,403 | 150,000,000 Γ· 2,707 | + +### For BALANCE + EXTCODECOPY: +| Gas Limit | Contracts Needed | Calculation | +| --------- | ---------------- | ------------------- | +| 5M | 998 | 5,000,000 Γ· 5,007 | +| 50M | 9,986 | 50,000,000 Γ· 5,007 | +| 150M | 29,958 | 150,000,000 Γ· 5,007 | + +You can see the associated attack constants inside of the tests in `bloatnet/test_bloatnet.py` + +## Quick Start: 150M Gas Attack + +### 1. Deploy CREATE2 Factory (you can use an already deployed one if preferred) + +```bash +# One-time setup - deploy the CREATE2 factory +python3 tests/benchmark/bloatnet/deploy_create2_factory.py + +# Output will show: +# Factory deployed at: 0x... <-- Save this address +``` + +### 2. Deploy Contracts + +Calculate the number of contracts needed for your test: +- For 150M gas BALANCE+EXTCODESIZE: 55,403 contracts +- For 150M gas BALANCE+EXTCODECOPY: 29,958 contracts + +_The suggestion is to deploy enough contracts to cover for the max_gas you plan to use in your tests/benchmarks_ + +```bash +# Deploy contracts for 150M gas EXTCODESIZE test +python3 tests/benchmark/bloatnet/deploy_bloatnet_simple.py \ + --num-contracts 55403 \ + --factory-address 0x... # Use factory address from step 2 + +# Note the output: +# FACTORY_ADDRESS = Address("0x...") +# INIT_CODE_HASH = bytes.fromhex("...") +# NUM_CONTRACTS = 55403 +``` + +### 3. Update Test Configuration + +Edit `tests/benchmark/bloatnet/test_bloatnet.py` and update: + +```python +FACTORY_ADDRESS = Address("0x...") # From deployment output +INIT_CODE_HASH = bytes.fromhex("...") # From deployment output +NUM_DEPLOYED_CONTRACTS = 55403 # Actual deployed count +``` + +### 5. Run Benchmark Tests + +```bash +# Run with specific gas values (in millions) +uv run fill --fork=Prague --gas-benchmark-values=150 \ + tests/benchmark/bloatnet/test_bloatnet.py --clean + +# With EVM traces for analysis +uv run fill --fork=Prague --gas-benchmark-values=150 \ + --evm-dump-dir=traces/ --traces \ + tests/benchmark/bloatnet/test_bloatnet.py + +# Multiple gas values +uv run fill --fork=Prague --gas-benchmark-values=5,50,150 \ + tests/benchmark/bloatnet/test_bloatnet.py +``` diff --git a/scripts/deploy_bloatnet_simple.py b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py similarity index 65% rename from scripts/deploy_bloatnet_simple.py rename to tests/benchmark/bloatnet/deploy_bloatnet_simple.py index 4e1ae3af974..fb317642a01 100644 --- a/scripts/deploy_bloatnet_simple.py +++ b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py @@ -8,15 +8,16 @@ """ import argparse -import sys +import os import subprocess -import json -from web3 import Web3 +import sys + from eth_utils import keccak +from web3 import Web3 + def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = None): """Deploy contracts using CREATE2 factory pattern.""" - # Connect to Geth w3 = Web3(Web3.HTTPProvider(rpc_url)) if not w3.is_connected(): @@ -39,10 +40,14 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No # Call the deploy_create2_factory.py script try: + # Get the directory of this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + factory_script = os.path.join(script_dir, "deploy_create2_factory.py") + result = subprocess.run( - [sys.executable, "scripts/deploy_create2_factory.py", "--rpc-url", rpc_url], + [sys.executable, factory_script, "--rpc-url", rpc_url], capture_output=True, - text=True + text=True, ) if result.returncode != 0: @@ -51,9 +56,9 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No sys.exit(1) # Extract factory address from output - for line in result.stdout.split('\n'): - if 'Factory address:' in line: - factory_address = line.split(':')[1].strip() + for line in result.stdout.split("\n"): + if "Factory address:" in line: + factory_address = line.split(":")[1].strip() print(f"Factory deployed at: {factory_address}") break else: @@ -68,10 +73,14 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No # Call the deploy_create2_factory.py script try: + # Get the directory of this script + script_dir = os.path.dirname(os.path.abspath(__file__)) + factory_script = os.path.join(script_dir, "deploy_create2_factory.py") + result = subprocess.run( - [sys.executable, "scripts/deploy_create2_factory.py", "--rpc-url", rpc_url], + [sys.executable, factory_script, "--rpc-url", rpc_url], capture_output=True, - text=True + text=True, ) if result.returncode != 0: @@ -80,9 +89,9 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No sys.exit(1) # Extract factory address from output - for line in result.stdout.split('\n'): - if 'Factory address:' in line: - factory_address = line.split(':')[1].strip() + for line in result.stdout.split("\n"): + if "Factory address:" in line: + factory_address = line.split(":")[1].strip() print(f"Factory deployed at: {factory_address}") break else: @@ -97,24 +106,35 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No # Init code copies runtime bytecode to memory and returns it init_code = bytearray() - # Push bytecode size, push code offset, push memory offset, CODECOPY, push size, push offset, RETURN + # Init code: PUSH2 size, PUSH1 offset, PUSH1 dest, CODECOPY, PUSH2 size, PUSH1 0, RETURN bytecode_size = 24576 init_size = 13 # Size of init code instructions # PUSH2 bytecode_size, PUSH1 init_size, PUSH1 0, CODECOPY - init_code.extend([ - 0x61, (bytecode_size >> 8) & 0xFF, bytecode_size & 0xFF, # PUSH2 bytecode_size - 0x60, init_size, # PUSH1 init_size (offset where runtime code starts) - 0x60, 0x00, # PUSH1 0 (dest in memory) - 0x39, # CODECOPY - ]) + init_code.extend( + [ + 0x61, + (bytecode_size >> 8) & 0xFF, + bytecode_size & 0xFF, # PUSH2 bytecode_size + 0x60, + init_size, # PUSH1 init_size (offset where runtime code starts) + 0x60, + 0x00, # PUSH1 0 (dest in memory) + 0x39, # CODECOPY + ] + ) # PUSH2 bytecode_size, PUSH1 0, RETURN - init_code.extend([ - 0x61, (bytecode_size >> 8) & 0xFF, bytecode_size & 0xFF, # PUSH2 bytecode_size - 0x60, 0x00, # PUSH1 0 (offset in memory) - 0xF3 # RETURN - ]) + init_code.extend( + [ + 0x61, + (bytecode_size >> 8) & 0xFF, + bytecode_size & 0xFF, # PUSH2 bytecode_size + 0x60, + 0x00, # PUSH1 0 (offset in memory) + 0xF3, # RETURN + ] + ) # Generate unique 24KB runtime bytecode to prevent deduplication runtime_bytecode = bytearray([0x00]) # Start with STOP @@ -150,73 +170,71 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No # Factory expects: salt (32 bytes) + bytecode call_data = ( - salt.to_bytes(32, 'big') + # salt - full_init_code # the init code + salt.to_bytes(32, "big") # salt + + full_init_code # the init code ) try: - tx_hash = w3.eth.send_transaction({ - 'from': test_account, - 'to': factory_address, - 'data': '0x' + call_data.hex(), - 'gas': 10000000 - }) + tx_hash = w3.eth.send_transaction( + { + "from": test_account, + "to": factory_address, + "data": "0x" + call_data.hex(), + "gas": 10000000, + } + ) receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=10) if receipt.status == 1: # Calculate CREATE2 address create2_input = ( - b'\xff' + - bytes.fromhex(factory_address[2:]) + - salt.to_bytes(32, 'big') + - init_code_hash + b"\xff" + + bytes.fromhex(factory_address[2:]) + + salt.to_bytes(32, "big") + + init_code_hash ) - contract_address = '0x' + keccak(create2_input)[-20:].hex() + contract_address = "0x" + keccak(create2_input)[-20:].hex() deployed.append(contract_address) if (salt + 1) % 100 == 0 or salt == num_contracts - 1: - print(f" [{salt+1}/{num_contracts}] Deployed at {contract_address}") + print(f" [{salt + 1}/{num_contracts}] Deployed at {contract_address}") else: - print(f" [{salt+1}/{num_contracts}] Failed") + print(f" [{salt + 1}/{num_contracts}] Failed") except Exception as e: - print(f" [{salt+1}/{num_contracts}] Error: {e}") + print(f" [{salt + 1}/{num_contracts}] Error: {e}") break print(f"\nDeployed {len(deployed)} contracts") if deployed: - print(f"\nContract addresses:") + print("\nContract addresses:") print(f"First: {deployed[0]}") print(f"Last: {deployed[-1]}") - print(f"\nFor test configuration:") - print(f"FACTORY_ADDRESS = Address(\"{factory_address}\")") - print(f"INIT_CODE_HASH = bytes.fromhex(\"{init_code_hash.hex()}\")") + print("\nFor test configuration:") + print(f'FACTORY_ADDRESS = Address("{factory_address}")') + print(f'INIT_CODE_HASH = bytes.fromhex("{init_code_hash.hex()}")') print(f"NUM_CONTRACTS = {len(deployed)}") + def main(): + """Execute the deployment script.""" parser = argparse.ArgumentParser(description="Deploy bloatnet contracts using CREATE2") parser.add_argument( - "--num-contracts", - type=int, - default=100, - help="Number of contracts to deploy" - ) - parser.add_argument( - "--rpc-url", - default="http://127.0.0.1:8545", - help="RPC URL" + "--num-contracts", type=int, default=100, help="Number of contracts to deploy" ) + parser.add_argument("--rpc-url", default="http://127.0.0.1:8545", help="RPC URL") parser.add_argument( "--factory-address", default=None, - help="CREATE2 factory address (deploys new one if not provided or if address doesn't have code)" + help="CREATE2 factory address (deploys new one if not provided)", ) args = parser.parse_args() deploy_contracts(args.rpc_url, args.num_contracts, args.factory_address) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/deploy_create2_factory.py b/tests/benchmark/bloatnet/deploy_create2_factory.py similarity index 53% rename from scripts/deploy_create2_factory.py rename to tests/benchmark/bloatnet/deploy_create2_factory.py index 292fc419ea4..dad3e66200a 100644 --- a/scripts/deploy_create2_factory.py +++ b/tests/benchmark/bloatnet/deploy_create2_factory.py @@ -5,12 +5,13 @@ """ import argparse -from web3 import Web3 + from eth_utils import keccak +from web3 import Web3 + def deploy_factory(rpc_url: str): """Deploy a minimal CREATE2 factory.""" - # Connect to Geth w3 = Web3(Web3.HTTPProvider(rpc_url)) if not w3.is_connected(): @@ -23,34 +24,43 @@ def deploy_factory(rpc_url: str): # Minimal CREATE2 factory bytecode # Takes salt (first 32 bytes) and bytecode (rest) from calldata # Returns the deployed address - factory_bytecode = bytes([ - # Runtime code - 0x60, 0x00, # PUSH1 0x00 (value for CREATE2) - 0x60, 0x00, # PUSH1 0x00 (salt position in calldata) - 0x35, # CALLDATALOAD (load salt) - 0x60, 0x20, # PUSH1 0x20 (bytecode starts at position 32) - 0x36, # CALLDATASIZE - 0x60, 0x20, # PUSH1 0x20 - 0x03, # SUB (bytecode length = calldatasize - 32) - 0x80, # DUP1 (duplicate bytecode length) - 0x60, 0x20, # PUSH1 0x20 (source position in calldata) - 0x60, 0x00, # PUSH1 0x00 (dest position in memory) - 0x37, # CALLDATACOPY (copy bytecode to memory) - 0xF5, # CREATE2 (value=0, mem_offset=0, mem_size, salt) - 0x60, 0x00, # PUSH1 0x00 - 0x52, # MSTORE (store address at position 0) - 0x60, 0x20, # PUSH1 0x20 - 0x60, 0x00, # PUSH1 0x00 - 0xF3, # RETURN (return the address) - ]) + factory_bytecode = bytes( + [ + # Runtime code + 0x60, + 0x00, # PUSH1 0x00 (value for CREATE2) + 0x60, + 0x00, # PUSH1 0x00 (salt position in calldata) + 0x35, # CALLDATALOAD (load salt) + 0x60, + 0x20, # PUSH1 0x20 (bytecode starts at position 32) + 0x36, # CALLDATASIZE + 0x60, + 0x20, # PUSH1 0x20 + 0x03, # SUB (bytecode length = calldatasize - 32) + 0x80, # DUP1 (duplicate bytecode length) + 0x60, + 0x20, # PUSH1 0x20 (source position in calldata) + 0x60, + 0x00, # PUSH1 0x00 (dest position in memory) + 0x37, # CALLDATACOPY (copy bytecode to memory) + 0xF5, # CREATE2 (value=0, mem_offset=0, mem_size, salt) + 0x60, + 0x00, # PUSH1 0x00 + 0x52, # MSTORE (store address at position 0) + 0x60, + 0x20, # PUSH1 0x20 + 0x60, + 0x00, # PUSH1 0x00 + 0xF3, # RETURN (return the address) + ] + ) # Deploy the factory print("\nDeploying CREATE2 factory...") - tx_hash = w3.eth.send_transaction({ - 'from': test_account, - 'data': '0x' + factory_bytecode.hex(), - 'gas': 3000000 - }) + tx_hash = w3.eth.send_transaction( + {"from": test_account, "data": "0x" + factory_bytecode.hex(), "gas": 3000000} + ) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) @@ -66,48 +76,48 @@ def deploy_factory(rpc_url: str): test_bytecode = bytes([0x00]) # Simple STOP opcode test_salt = 0 - calldata = test_salt.to_bytes(32, 'big') + test_bytecode + calldata = test_salt.to_bytes(32, "big") + test_bytecode # Use eth_call to get the address that would be created - result = w3.eth.call({ - 'to': factory_address, - 'data': '0x' + calldata.hex() - }) + result = w3.eth.call({"to": factory_address, "data": "0x" + calldata.hex()}) if result: - test_addr = '0x' + result[-20:].hex() + test_addr = "0x" + result[-20:].hex() print(f"Test deployment would create: {test_addr}") # Calculate expected CREATE2 address expected = keccak( - b'\xff' + - bytes.fromhex(factory_address[2:]) + - test_salt.to_bytes(32, 'big') + - keccak(test_bytecode) + b"\xff" + + bytes.fromhex(factory_address[2:]) + + test_salt.to_bytes(32, "big") + + keccak(test_bytecode) )[-20:] - expected_addr = '0x' + expected.hex() + expected_addr = "0x" + expected.hex() print(f"Expected CREATE2 address: {expected_addr}") return factory_address + def main(): + """Execute the factory deployment script.""" parser = argparse.ArgumentParser(description="Deploy CREATE2 factory") parser.add_argument( "--rpc-url", default="http://127.0.0.1:8545", - help="RPC URL (default: http://127.0.0.1:8545)" + help="RPC URL (default: http://127.0.0.1:8545)", ) args = parser.parse_args() factory_address = deploy_factory(args.rpc_url) if factory_address: - print("\n" + "="*60) + print("\n" + "=" * 60) print("Factory deployed successfully!") print(f"Factory address: {factory_address}") print("\nAdd this to your test configuration:") print(f'FACTORY_ADDRESS = Address("{factory_address}")') - print("="*60) + print("=" * 60) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/benchmark/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py similarity index 78% rename from tests/benchmark/test_bloatnet.py rename to tests/benchmark/bloatnet/test_bloatnet.py index 74e7fcb9d65..b70ab4fde0e 100644 --- a/tests/benchmark/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -28,13 +28,19 @@ REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" +# Gas cost constants - used to calculate required contracts for any gas limit +# See README.md for detailed breakdown of these costs +GAS_PER_CONTRACT_BALANCE_EXTCODESIZE = 2707 # BALANCE(cold) + EXTCODESIZE(warm) +GAS_PER_CONTRACT_BALANCE_EXTCODECOPY = 5007 # BALANCE(cold) + EXTCODECOPY(warm, 24KB) + # Configuration for CREATE2 pre-deployed contracts # These values must match what the deployment script generates -# CREATE2 factory address -FACTORY_ADDRESS = Address("0x07EADb2f6b02Bb9fE994c0AeFf106625c0d3C93f") -# Init code hash from deployment -INIT_CODE_HASH = bytes.fromhex("8eba8b9d04aea4e6f3d6601ad364021e8e37b6282a0f57e40c635ba7f80aa0cb") -NUM_DEPLOYED_CONTRACTS = 1838 # Exact contracts needed for 5M gas test +# UPDATE THESE VALUES after running deploy_bloatnet_simple.py +FACTORY_ADDRESS = Address("0x07EADb2f6b02Bb9fE994c0AeFf106625c0d3C93f") # UPDATE THIS +INIT_CODE_HASH = bytes.fromhex( + "8eba8b9d04aea4e6f3d6601ad364021e8e37b6282a0f57e40c635ba7f80aa0cb" +) # UPDATE THIS +NUM_DEPLOYED_CONTRACTS = 1838 # UPDATE THIS - current setup for 5M gas def calculate_create2_address(factory: Address, salt: int, init_code_hash: bytes) -> Address: @@ -76,7 +82,21 @@ def test_bloatnet_balance_extcodesize( # Calculate how many contracts to access available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup - num_contracts = min(int(available_gas // cost_per_contract), NUM_DEPLOYED_CONTRACTS) + contracts_needed = int(available_gas // cost_per_contract) + num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) + + # Log the calculation for transparency + if contracts_needed > NUM_DEPLOYED_CONTRACTS: + import warnings + + warnings.warn( + f"Test needs {contracts_needed} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas, " + f"but only {NUM_DEPLOYED_CONTRACTS} are deployed. " + f"Deploy {contracts_needed - NUM_DEPLOYED_CONTRACTS} more contracts " + f"for full test coverage.", + stacklevel=2, + ) # Generate attack contract with unrolled loop attack_code = Bytecode() @@ -158,12 +178,26 @@ def test_bloatnet_balance_extcodecopy( # Calculate how many contracts to access available_gas = gas_benchmark_value - intrinsic_gas - memory_cost - 1000 + contracts_needed = int(available_gas // cost_per_contract) num_contracts = min( - int(available_gas // cost_per_contract), + contracts_needed, NUM_DEPLOYED_CONTRACTS, 10, # Limit to avoid excessive memory usage in test ) + # Log the calculation for transparency + if contracts_needed > NUM_DEPLOYED_CONTRACTS: + import warnings + + warnings.warn( + f"Test needs {contracts_needed} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas, " + f"but only {NUM_DEPLOYED_CONTRACTS} are deployed. " + f"Deploy {contracts_needed - NUM_DEPLOYED_CONTRACTS} more contracts " + f"for full test coverage.", + stacklevel=2, + ) + # Generate attack contract attack_code = Bytecode() mem_offset = 0 From 49c134318464bab3ad6ba99fb70f2683a7039ee9 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Wed, 17 Sep 2025 23:23:03 +0200 Subject: [PATCH 27/54] refactor(benchmark): imrpove contract deployment script with interactive selection and bytecode generation - Introduced interactive contract type selection for deploying contracts in the bloatnet benchmark. - Added support for multiple contract types: max_size_24kb, sload_heavy, storage_heavy, and custom. - Refactored bytecode generation functions to improve clarity and maintainability. - Updated README to reflect changes in deployment process and contract types. - Ensured proper handling of factory deployment and transaction receipt checks. --- pyproject.toml | 1 + tests/benchmark/bloatnet/README.md | 48 ++- .../bloatnet/deploy_bloatnet_simple.py | 345 +++++++++++++----- .../bloatnet/deploy_create2_factory.py | 27 +- 4 files changed, 300 insertions(+), 121 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 188cfc81ac5..99b6ea86837 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,7 @@ exclude = [ '^fixtures/', '^logs/', '^site/', + '^tests/benchmark/bloatnet/deploy_.*\.py$', ] plugins = ["pydantic.mypy"] diff --git a/tests/benchmark/bloatnet/README.md b/tests/benchmark/bloatnet/README.md index 40d9eae5525..ef7a0b36d04 100644 --- a/tests/benchmark/bloatnet/README.md +++ b/tests/benchmark/bloatnet/README.md @@ -6,7 +6,7 @@ The Bloatnet benchmarks work on the following fashion: 1. They usually require a previously-deployed state (usually quite large) which the benchmarks will interact with. 2. The deployment script helpers help deploying the required bytecode for the specific tests. -3. The outpus of the deployment scripts get hardcoded into the codebase such that the benchmarks can interact with them. +3. The outputs of the deployment scripts get hardcoded into the codebase such that the benchmarks can interact with them. ## Gas Cost Constants @@ -47,7 +47,7 @@ You can see the associated attack constants inside of the tests in `bloatnet/tes ## Quick Start: 150M Gas Attack -### 1. Deploy CREATE2 Factory (you can use an already deployed one if preferred) +### 1. Deploy CREATE2 Factory (you can use an already deployed one if preferred and therefore, skip this step) ```bash # One-time setup - deploy the CREATE2 factory @@ -59,24 +59,52 @@ python3 tests/benchmark/bloatnet/deploy_create2_factory.py ### 2. Deploy Contracts -Calculate the number of contracts needed for your test: +The deployment script is interactive and will guide you through selecting the appropriate contract type for your benchmark. + +#### Contract Types Available + +1. **max_size_24kb**: 24KB contracts filled with unique bytecode (EXTCODE_ type of tests) +2. **sload_heavy**: Contracts optimized for SLOAD benchmarking +3. **storage_heavy**: Contracts with pre-initialized storage +4. **custom**: Custom bytecode (for future extensions) + +#### Calculate Contracts Needed + +Before running the deployment, calculate the number of contracts needed: - For 150M gas BALANCE+EXTCODESIZE: 55,403 contracts - For 150M gas BALANCE+EXTCODECOPY: 29,958 contracts -_The suggestion is to deploy enough contracts to cover for the max_gas you plan to use in your tests/benchmarks_ +_Deploy enough contracts to cover the max gas you plan to use in your tests/benchmarks._ + +#### Running the Deployment ```bash -# Deploy contracts for 150M gas EXTCODESIZE test +# Run the interactive deployment script python3 tests/benchmark/bloatnet/deploy_bloatnet_simple.py \ --num-contracts 55403 \ - --factory-address 0x... # Use factory address from step 2 + --factory-address 0x... # Use factory address from step 1 +``` + +#### Deployment Output -# Note the output: -# FACTORY_ADDRESS = Address("0x...") -# INIT_CODE_HASH = bytes.fromhex("...") -# NUM_CONTRACTS = 55403 +After successful deployment, the script will: + +1. Display the configuration needed for tests: +```python +=== Configuration for max_size_24kb tests === +CONTRACT_TYPE = "max_size_24kb" +FACTORY_ADDRESS = Address("0x...") +INIT_CODE_HASH = bytes.fromhex("...") +NUM_DEPLOYED_CONTRACTS = 55403 +``` + +2. Save the configuration to a file: +``` +Configuration saved to: bloatnet_config_max_size_24kb.txt ``` +This file contains all the values needed to update your test configuration. + ### 3. Update Test Configuration Edit `tests/benchmark/bloatnet/test_bloatnet.py` and update: diff --git a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py index fb317642a01..74079926ee3 100644 --- a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py +++ b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py @@ -11,98 +11,31 @@ import os import subprocess import sys +from typing import Dict, List, Optional, Tuple from eth_utils import keccak from web3 import Web3 -def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = None): - """Deploy contracts using CREATE2 factory pattern.""" - # Connect to Geth - w3 = Web3(Web3.HTTPProvider(rpc_url)) - if not w3.is_connected(): - print(f"Failed to connect to {rpc_url}") - sys.exit(1) +class ContractType: + """Define contract types for different benchmarks.""" - test_account = w3.eth.accounts[0] - print(f"Using test account: {test_account}") - print(f"Balance: {w3.eth.get_balance(test_account) / 10**18:.4f} ETH") + MAX_SIZE_24KB = "max_size_24kb" + SLOAD_HEAVY = "sload_heavy" + STORAGE_HEAVY = "storage_heavy" + CUSTOM = "custom" - # Step 1: Check if factory exists, if not deploy it - if factory_address: - # Verify factory exists at the provided address - factory_code = w3.eth.get_code(factory_address) - if len(factory_code) > 0: - print(f"\nUsing existing CREATE2 factory at: {factory_address}") - else: - print(f"\nNo factory found at {factory_address}") - print("Deploying new CREATE2 factory...") - - # Call the deploy_create2_factory.py script - try: - # Get the directory of this script - script_dir = os.path.dirname(os.path.abspath(__file__)) - factory_script = os.path.join(script_dir, "deploy_create2_factory.py") - - result = subprocess.run( - [sys.executable, factory_script, "--rpc-url", rpc_url], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - print("Failed to deploy factory:") - print(result.stderr) - sys.exit(1) - - # Extract factory address from output - for line in result.stdout.split("\n"): - if "Factory address:" in line: - factory_address = line.split(":")[1].strip() - print(f"Factory deployed at: {factory_address}") - break - else: - print("Could not extract factory address from deployment output") - sys.exit(1) - - except Exception as e: - print(f"Error deploying factory: {e}") - sys.exit(1) - else: - print("\nNo factory address provided, deploying new CREATE2 factory...") - - # Call the deploy_create2_factory.py script - try: - # Get the directory of this script - script_dir = os.path.dirname(os.path.abspath(__file__)) - factory_script = os.path.join(script_dir, "deploy_create2_factory.py") - - result = subprocess.run( - [sys.executable, factory_script, "--rpc-url", rpc_url], - capture_output=True, - text=True, - ) - if result.returncode != 0: - print("Failed to deploy factory:") - print(result.stderr) - sys.exit(1) - - # Extract factory address from output - for line in result.stdout.split("\n"): - if "Factory address:" in line: - factory_address = line.split(":")[1].strip() - print(f"Factory deployed at: {factory_address}") - break - else: - print("Could not extract factory address from deployment output") - sys.exit(1) +CONTRACT_DESCRIPTIONS = { + ContractType.MAX_SIZE_24KB: "24KB contracts filled with unique bytecode (standard bloatnet)", + ContractType.SLOAD_HEAVY: "Contracts optimized for SLOAD benchmarking", + ContractType.STORAGE_HEAVY: "Contracts with pre-initialized storage", + ContractType.CUSTOM: "Custom bytecode (provide your own)", +} - except Exception as e: - print(f"Error deploying factory: {e}") - sys.exit(1) - # Step 2: Generate init code for 24KB contracts +def generate_max_size_bytecode(salt: int = 0) -> Tuple[bytes, bytes]: + """Generate 24KB contract bytecode for standard bloatnet tests.""" # Init code copies runtime bytecode to memory and returns it init_code = bytearray() @@ -143,7 +76,7 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No pattern_count = 0 while len(runtime_bytecode) < bytecode_size - 100: # Use a simple pattern that's still unique per contract - unique_value = keccak(f"bloatnet_{pattern_count}".encode()) + unique_value = keccak(f"bloatnet_{salt}_{pattern_count}".encode()) runtime_bytecode.append(0x7F) # PUSH32 runtime_bytecode.extend(unique_value[:31]) # Use 31 bytes of hash runtime_bytecode.append(0x50) # POP @@ -153,40 +86,210 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No while len(runtime_bytecode) < bytecode_size: runtime_bytecode.append(0x5B) - # Combine init code and runtime bytecode full_init_code = bytes(init_code) + bytes(runtime_bytecode) - init_code_hash = keccak(full_init_code) + return full_init_code, keccak(full_init_code) + + +def generate_sload_heavy_bytecode(salt: int = 0) -> Tuple[bytes, bytes]: + """Generate contracts optimized for SLOAD benchmarking.""" + # Runtime bytecode that performs many SLOAD operations + runtime_bytecode = bytearray() + + # Store some values during deployment + for i in range(10): + # PUSH1 value, PUSH1 key, SSTORE + runtime_bytecode.extend([0x60, i * 2, 0x60, i, 0x55]) + + # Main runtime: series of SLOAD operations + for i in range(100): + # PUSH1 key, SLOAD, POP + runtime_bytecode.extend([0x60, i % 10, 0x54, 0x50]) + + # Final STOP + runtime_bytecode.append(0x00) + + # Create init code that deploys this runtime + runtime_size = len(runtime_bytecode) + init_size = 13 + + init_code = bytearray() + init_code.extend( + [ + 0x61, + (runtime_size >> 8) & 0xFF, + runtime_size & 0xFF, # PUSH2 runtime_size + 0x60, + init_size, # PUSH1 init_size + 0x60, + 0x00, # PUSH1 0 + 0x39, # CODECOPY + 0x61, + (runtime_size >> 8) & 0xFF, + runtime_size & 0xFF, # PUSH2 runtime_size + 0x60, + 0x00, # PUSH1 0 + 0xF3, # RETURN + ] + ) + + full_init_code = bytes(init_code) + bytes(runtime_bytecode) + return full_init_code, keccak(full_init_code) + + +def select_contract_type() -> str: + """Interactive contract type selection.""" + print("\n=== Contract Type Selection ===") + print("Select the type of contracts to deploy:\n") + + options = list(CONTRACT_DESCRIPTIONS.keys()) + for i, (key, desc) in enumerate(CONTRACT_DESCRIPTIONS.items(), 1): + print(f"{i}. {key}: {desc}") + + while True: + try: + choice = input(f"\nEnter choice (1-{len(options)}): ").strip() + idx = int(choice) - 1 + if 0 <= idx < len(options): + selected = options[idx] + print(f"\nSelected: {selected}") + return selected + else: + print(f"Please enter a number between 1 and {len(options)}") + except (ValueError, KeyboardInterrupt): + print("\nExiting...") + sys.exit(0) + + +def get_bytecode_generator(contract_type: str): + """Get the appropriate bytecode generator for the contract type.""" + generators = { + ContractType.MAX_SIZE_24KB: generate_max_size_bytecode, + ContractType.SLOAD_HEAVY: generate_sload_heavy_bytecode, + # Add more generators as needed + } + + generator = generators.get(contract_type) + if not generator: + print(f"Error: No generator implemented for {contract_type}") + if contract_type == ContractType.CUSTOM: + print("Custom bytecode deployment not yet implemented") + sys.exit(1) + + return generator + + +def deploy_factory(rpc_url: str) -> str: + """Deploy CREATE2 factory if needed.""" + print("\nDeploying CREATE2 factory...") + + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + factory_script = os.path.join(script_dir, "deploy_create2_factory.py") + + result = subprocess.run( + [sys.executable, factory_script, "--rpc-url", rpc_url], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + print("Failed to deploy factory:") + print(result.stderr) + sys.exit(1) + + # Extract factory address from output + for line in result.stdout.split("\n"): + if "Factory address:" in line: + factory_address = line.split(":")[1].strip() + print(f"Factory deployed at: {factory_address}") + return factory_address + + print("Could not extract factory address from deployment output") + sys.exit(1) + + except Exception as e: + print(f"Error deploying factory: {e}") + sys.exit(1) + + +def deploy_contracts( + rpc_url: str, + num_contracts: int, + contract_type: str, + factory_address: Optional[str] = None, +): + """Deploy contracts using CREATE2 factory pattern.""" + # Connect to Geth + w3 = Web3(Web3.HTTPProvider(rpc_url)) + if not w3.is_connected(): + print(f"Failed to connect to {rpc_url}") + sys.exit(1) + + test_account = w3.eth.accounts[0] + print(f"Using test account: {test_account}") + print(f"Balance: {w3.eth.get_balance(test_account) / 10**18:.4f} ETH") - print(f"\nInit code hash: 0x{init_code_hash.hex()}") - print(f"Init code size: {len(full_init_code)} bytes") + # Check/deploy factory + if factory_address: + factory_code = w3.eth.get_code(Web3.to_checksum_address(factory_address)) + if len(factory_code) > 0: + print(f"\nUsing existing CREATE2 factory at: {factory_address}") + else: + print(f"\nNo factory found at {factory_address}") + factory_address = deploy_factory(rpc_url) + else: + factory_address = deploy_factory(rpc_url) - # Step 3: Deploy contracts using factory - print(f"\nDeploying {num_contracts} contracts using CREATE2...") + # Get bytecode generator + bytecode_generator = get_bytecode_generator(contract_type) + + # Generate sample to show info + sample_init_code, sample_hash = bytecode_generator(0) + + print(f"\nContract Type: {contract_type}") + print(f"Init code size: {len(sample_init_code)} bytes") + print(f"Sample init code hash: 0x{sample_hash.hex()}") + + confirm = input(f"\nProceed with deploying {num_contracts} {contract_type} contracts? (y/n): ") + if confirm.lower() != "y": + print("Deployment cancelled") + sys.exit(0) + + # Deploy contracts + print(f"\nDeploying {num_contracts} {contract_type} contracts using CREATE2...") deployed = [] + init_code_hashes: Dict[str, List[int]] = {} # Track different init code hashes if they vary + for salt in range(num_contracts): if salt % 100 == 0: print(f"Progress: {salt}/{num_contracts}") + # Generate bytecode for this specific salt + full_init_code, init_code_hash = bytecode_generator(salt) + + # Track unique init code hashes + hash_hex = init_code_hash.hex() + if hash_hex not in init_code_hashes: + init_code_hashes[hash_hex] = [] + init_code_hashes[hash_hex].append(salt) + # Factory expects: salt (32 bytes) + bytecode - call_data = ( - salt.to_bytes(32, "big") # salt - + full_init_code # the init code - ) + call_data = salt.to_bytes(32, "big") + full_init_code try: tx_hash = w3.eth.send_transaction( { "from": test_account, "to": factory_address, - "data": "0x" + call_data.hex(), + "data": bytes.fromhex(call_data.hex()), "gas": 10000000, } ) receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=10) - if receipt.status == 1: + if receipt["status"] == 1: # Calculate CREATE2 address create2_input = ( b"\xff" @@ -213,19 +316,50 @@ def deploy_contracts(rpc_url: str, num_contracts: int, factory_address: str = No print(f"First: {deployed[0]}") print(f"Last: {deployed[-1]}") - print("\nFor test configuration:") + print(f"\n=== Configuration for {contract_type} tests ===") + print(f'CONTRACT_TYPE = "{contract_type}"') print(f'FACTORY_ADDRESS = Address("{factory_address}")') - print(f'INIT_CODE_HASH = bytes.fromhex("{init_code_hash.hex()}")') - print(f"NUM_CONTRACTS = {len(deployed)}") + + if len(init_code_hashes) == 1: + # All contracts have the same init code hash + hash_hex = list(init_code_hashes.keys())[0] + print(f'INIT_CODE_HASH = bytes.fromhex("{hash_hex}")') + else: + # Multiple init code hashes (rare but possible) + print("# Multiple init code hashes detected:") + for hash_hex, salts in init_code_hashes.items(): + print(f"# Hash {hash_hex[:8]}... used for salts: {salts[:5]}...") + + print(f"NUM_DEPLOYED_CONTRACTS = {len(deployed)}") + + # Save configuration to file + config_file = f"bloatnet_config_{contract_type}.txt" + with open(config_file, "w") as f: + f.write(f"# Configuration for {contract_type} benchmarks\n") + f.write(f'CONTRACT_TYPE = "{contract_type}"\n') + f.write(f'FACTORY_ADDRESS = Address("{factory_address}")\n') + if len(init_code_hashes) == 1: + hash_hex = list(init_code_hashes.keys())[0] + f.write(f'INIT_CODE_HASH = bytes.fromhex("{hash_hex}")\n') + f.write(f"NUM_DEPLOYED_CONTRACTS = {len(deployed)}\n") + + print(f"\nConfiguration saved to: {config_file}") def main(): """Execute the deployment script.""" parser = argparse.ArgumentParser(description="Deploy bloatnet contracts using CREATE2") parser.add_argument( - "--num-contracts", type=int, default=100, help="Number of contracts to deploy" + "--num-contracts", + type=int, + default=100, + help="Number of contracts to deploy", + ) + parser.add_argument( + "--rpc-url", + default="http://127.0.0.1:8545", + help="RPC URL", ) - parser.add_argument("--rpc-url", default="http://127.0.0.1:8545", help="RPC URL") parser.add_argument( "--factory-address", default=None, @@ -233,7 +367,16 @@ def main(): ) args = parser.parse_args() - deploy_contracts(args.rpc_url, args.num_contracts, args.factory_address) + + # Always run in interactive mode - user selects contract type + contract_type = select_contract_type() + + deploy_contracts( + args.rpc_url, + args.num_contracts, + contract_type, + args.factory_address, + ) if __name__ == "__main__": diff --git a/tests/benchmark/bloatnet/deploy_create2_factory.py b/tests/benchmark/bloatnet/deploy_create2_factory.py index dad3e66200a..9195029725f 100644 --- a/tests/benchmark/bloatnet/deploy_create2_factory.py +++ b/tests/benchmark/bloatnet/deploy_create2_factory.py @@ -59,16 +59,16 @@ def deploy_factory(rpc_url: str): # Deploy the factory print("\nDeploying CREATE2 factory...") tx_hash = w3.eth.send_transaction( - {"from": test_account, "data": "0x" + factory_bytecode.hex(), "gas": 3000000} + {"from": test_account, "data": bytes.fromhex(factory_bytecode.hex()), "gas": 3000000} ) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - if receipt.status != 1: + if receipt["status"] != 1: print("Failed to deploy factory") return None - factory_address = receipt.contractAddress + factory_address = receipt["contractAddress"] print(f"βœ… Factory deployed at: {factory_address}") # Test the factory with a simple contract @@ -79,19 +79,26 @@ def deploy_factory(rpc_url: str): calldata = test_salt.to_bytes(32, "big") + test_bytecode # Use eth_call to get the address that would be created - result = w3.eth.call({"to": factory_address, "data": "0x" + calldata.hex()}) + if factory_address: + result = w3.eth.call({"to": factory_address, "data": bytes.fromhex(calldata.hex())}) + else: + print("Factory address is None") + return None if result: test_addr = "0x" + result[-20:].hex() print(f"Test deployment would create: {test_addr}") # Calculate expected CREATE2 address - expected = keccak( - b"\xff" - + bytes.fromhex(factory_address[2:]) - + test_salt.to_bytes(32, "big") - + keccak(test_bytecode) - )[-20:] + if factory_address: + expected = keccak( + b"\xff" + + bytes.fromhex(factory_address[2:]) + + test_salt.to_bytes(32, "big") + + keccak(test_bytecode) + )[-20:] + else: + expected = b"" expected_addr = "0x" + expected.hex() print(f"Expected CREATE2 address: {expected_addr}") From 2875cf4b2e5ead54425d2a4a63f5adef0dc1b51a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 18 Sep 2025 10:19:58 +0200 Subject: [PATCH 28/54] delete: remove obsolete test_create2.py script This was commited unintentionally --- scripts/test_create2.py | 91 ----------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 scripts/test_create2.py diff --git a/scripts/test_create2.py b/scripts/test_create2.py deleted file mode 100644 index 62349e8f289..00000000000 --- a/scripts/test_create2.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -"""Test CREATE2 factory deployment.""" - -from web3 import Web3 -from eth_utils import keccak - -# Connect to Geth -w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545")) -if not w3.is_connected(): - print("Failed to connect to Geth") - exit(1) - -test_account = w3.eth.accounts[0] -print(f"Using test account: {test_account}") -print(f"Balance: {w3.eth.get_balance(test_account) / 10**18:.4f} ETH") - -# Simple CREATE2 factory that returns the deployed address -factory_bytecode = ( - # Constructor: return runtime code - "60" + "1f" # PUSH1 0x1F (runtime size = 31) - "80" # DUP1 - "60" + "0a" # PUSH1 0x0A (runtime offset) - "60" + "00" # PUSH1 0x00 (memory dest) - "39" # CODECOPY - "60" + "00" # PUSH1 0x00 (return offset) - "f3" # RETURN - - # Runtime: minimal CREATE2 - "36" # CALLDATASIZE - "60" + "00" # PUSH1 0x00 - "60" + "00" # PUSH1 0x00 - "37" # CALLDATACOPY (copy all calldata to memory) - - "60" + "00" # PUSH1 0x00 (salt - using 0 for simplicity) - "36" # CALLDATASIZE (size of init code) - "60" + "00" # PUSH1 0x00 (offset in memory) - "60" + "00" # PUSH1 0x00 (value) - "f5" # CREATE2 - - "60" + "00" # PUSH1 0x00 - "52" # MSTORE (store address at 0) - "60" + "20" # PUSH1 0x20 - "60" + "00" # PUSH1 0x00 - "f3" # RETURN (return address) -) - -# Deploy factory -factory_tx = w3.eth.send_transaction({ - 'from': test_account, - 'data': '0x' + factory_bytecode, - 'gas': 3000000 -}) - -factory_receipt = w3.eth.wait_for_transaction_receipt(factory_tx) -if factory_receipt.status != 1: - print("Failed to deploy factory") - exit(1) - -factory_address = factory_receipt.contractAddress -print(f"\nFactory deployed at: {factory_address}") - -# Create simple contract bytecode (just returns 42) -simple_bytecode = "602a60005260206000f3" # PUSH1 42, PUSH1 0, MSTORE, PUSH1 32, PUSH1 0, RETURN - -# Deploy using factory -print("\nDeploying contract via CREATE2...") -deploy_tx = w3.eth.send_transaction({ - 'from': test_account, - 'to': factory_address, - 'data': '0x' + simple_bytecode, - 'gas': 1000000 -}) - -deploy_receipt = w3.eth.wait_for_transaction_receipt(deploy_tx) -print(f"Transaction status: {deploy_receipt.status}") - -# Get return value (the deployed address) -result = w3.eth.call({ - 'to': factory_address, - 'data': '0x' + simple_bytecode -}) - -if result: - deployed_addr = '0x' + result[-20:].hex() - print(f"Contract deployed at: {deployed_addr}") - - # Verify by checking code - code = w3.eth.get_code(deployed_addr) - print(f"Deployed code length: {len(code)} bytes") -else: - print("No return value from factory") \ No newline at end of file From b634ca3c5d0704b7e6c3211da5d9c3b18cba5e88 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 18 Sep 2025 10:23:23 +0200 Subject: [PATCH 29/54] refactor(benchmark): optimize gas calculations for BALANCE + EXTCODECOPY pattern - Updated the README to reflect the optimized gas cost for the BALANCE + EXTCODECOPY pattern, reducing it from ~5,007 to ~2,710 gas per contract. - Modified the test_bloatnet_balance_extcodecopy function to read only 1 byte from the end of the bytecode, minimizing gas costs while maximizing contract targeting. - Adjusted calculations for the number of contracts needed based on the new cost per contract, ensuring accurate benchmarks. --- tests/benchmark/bloatnet/README.md | 19 +++++++------ tests/benchmark/bloatnet/test_bloatnet.py | 34 +++++++++-------------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/tests/benchmark/bloatnet/README.md b/tests/benchmark/bloatnet/README.md index ef7a0b36d04..674a895950d 100644 --- a/tests/benchmark/bloatnet/README.md +++ b/tests/benchmark/bloatnet/README.md @@ -18,15 +18,18 @@ will interact with. - `EXTCODESIZE` (warm): 100 gas - `POP`: 2 gas -### BALANCE + EXTCODECOPY Pattern -**Gas per contract: ~5,007** +### BALANCE + EXTCODECOPY(single-byte) Pattern +**Gas per contract: ~2,710** - `PUSH20` (address): 3 gas - `BALANCE` (cold access): 2,600 gas - `POP`: 2 gas -- `EXTCODECOPY` setup: ~100 gas -- `EXTCODECOPY` (24KB): ~2,300 gas +- `EXTCODECOPY` (warm, 1 byte): 100 gas (base) + 3 gas (copy 1 byte) - `POP`: 2 gas +Note: Reading just 1 byte (specifically the last byte at offset 24575) forces the client +to load the entire 24KB contract from disk while minimizing gas cost. This allows +targeting nearly as many contracts as the EXTCODESIZE pattern while forcing maximum I/O. + ## Required Contracts Calculation Example: ### For BALANCE + EXTCODESIZE: @@ -36,12 +39,12 @@ will interact with. | 50M | 18,380 | 50,000,000 Γ· 2,707 | | 150M | 55,403 | 150,000,000 Γ· 2,707 | -### For BALANCE + EXTCODECOPY: +### For BALANCE + EXTCODECOPY (Optimized): | Gas Limit | Contracts Needed | Calculation | | --------- | ---------------- | ------------------- | -| 5M | 998 | 5,000,000 Γ· 5,007 | -| 50M | 9,986 | 50,000,000 Γ· 5,007 | -| 150M | 29,958 | 150,000,000 Γ· 5,007 | +| 5M | 1,845 | 5,000,000 Γ· 2,710 | +| 50M | 18,450 | 50,000,000 Γ· 2,710 | +| 150M | 55,350 | 150,000,000 Γ· 2,710 | You can see the associated attack constants inside of the tests in `bloatnet/test_bloatnet.py` diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py index b70ab4fde0e..45a8d4b3df0 100644 --- a/tests/benchmark/bloatnet/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -151,9 +151,11 @@ def test_bloatnet_balance_extcodecopy( This test forces actual bytecode reads from disk by: 1. Using BALANCE (cold) to warm the account - 2. Using EXTCODECOPY (warm) to read the full 24KB bytecode + 2. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode - This pattern reads ~123x more data per gas than EXTCODESIZE. + Reading just 1 byte (specifically the last byte) forces the client to load + the entire contract from disk while minimizing gas cost, allowing us to + target many more contracts than copying the full 24KB. """ gas_costs = fork.gas_costs() max_contract_size = fork.max_code_size() @@ -161,29 +163,20 @@ def test_bloatnet_balance_extcodecopy( # Calculate costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Memory expansion for EXTCODECOPY - max_memory_needed = max_contract_size * 10 # Limit to 10 contracts worth of memory - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=max_memory_needed) - - # Cost per contract with EXTCODECOPY - words_to_copy = (max_contract_size + 31) // 32 # 768 words for 24KB + # Cost per contract with EXTCODECOPY (copying just 1 byte) cost_per_contract = ( 3 # PUSH20 for address + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) + gas_costs.G_BASE # POP balance + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) - + gas_costs.G_COPY * words_to_copy # Copy cost (3 * 768 = 2304) + + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3 * 1 = 3) + gas_costs.G_BASE * 4 # PUSH operations and POP ) # Calculate how many contracts to access - available_gas = gas_benchmark_value - intrinsic_gas - memory_cost - 1000 + available_gas = gas_benchmark_value - intrinsic_gas - 1000 contracts_needed = int(available_gas // cost_per_contract) - num_contracts = min( - contracts_needed, - NUM_DEPLOYED_CONTRACTS, - 10, # Limit to avoid excessive memory usage in test - ) + num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) # Log the calculation for transparency if contracts_needed > NUM_DEPLOYED_CONTRACTS: @@ -200,7 +193,6 @@ def test_bloatnet_balance_extcodecopy( # Generate attack contract attack_code = Bytecode() - mem_offset = 0 # Access each contract using CREATE2 addresses for i in range(num_contracts): @@ -210,15 +202,15 @@ def test_bloatnet_balance_extcodecopy( + Op.DUP1 + Op.BALANCE + Op.POP - # EXTCODECOPY(addr, mem_offset, 0, 24KB) - + Op.PUSH2[max_contract_size] # size - + Op.PUSH1[0] # code offset - + Op.PUSH3[mem_offset] # memory offset + # EXTCODECOPY(addr, mem_offset, last_byte_offset, 1) + # Read the LAST byte of the contract to force full load from disk + + Op.PUSH1[1] # size (1 byte) + + Op.PUSH2[max_contract_size - 1] # code offset (last byte) + + Op.PUSH2[i] # memory offset (unique per contract to avoid overlap) + Op.DUP4 # address + Op.EXTCODECOPY + Op.POP # clean up address ) - mem_offset += max_contract_size # Deploy attack contract attack_address = pre.deploy_contract(code=attack_code) From 774c56c7b3a64f9fbc3d514c7897149555a1479d Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 18 Sep 2025 10:24:20 +0200 Subject: [PATCH 30/54] refactor(benchmark): support non-fixed max_codesize --- tests/benchmark/bloatnet/deploy_bloatnet_simple.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py index 74079926ee3..7e8df580fc7 100644 --- a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py +++ b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py @@ -34,13 +34,18 @@ class ContractType: } -def generate_max_size_bytecode(salt: int = 0) -> Tuple[bytes, bytes]: - """Generate 24KB contract bytecode for standard bloatnet tests.""" +def generate_max_size_bytecode(salt: int = 0, max_code_size: int = 24576) -> Tuple[bytes, bytes]: + """Generate max-size contract bytecode for standard bloatnet tests. + + Args: + salt: Unique salt for generating unique bytecode + max_code_size: Maximum contract size (default 24576 bytes for mainnet) + """ # Init code copies runtime bytecode to memory and returns it init_code = bytearray() # Init code: PUSH2 size, PUSH1 offset, PUSH1 dest, CODECOPY, PUSH2 size, PUSH1 0, RETURN - bytecode_size = 24576 + bytecode_size = max_code_size init_size = 13 # Size of init code instructions # PUSH2 bytecode_size, PUSH1 init_size, PUSH1 0, CODECOPY From 6e6863a66aabf8083cd06cc6b0d2b6bb98e10556 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 18 Sep 2025 10:58:12 +0200 Subject: [PATCH 31/54] chore: Remove all 24kB "hardcoded" refs --- tests/benchmark/bloatnet/README.md | 3 +- .../bloatnet/deploy_bloatnet_simple.py | 34 ++++++++++++------- tests/benchmark/bloatnet/test_bloatnet.py | 6 ++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/tests/benchmark/bloatnet/README.md b/tests/benchmark/bloatnet/README.md index 674a895950d..764292521f0 100644 --- a/tests/benchmark/bloatnet/README.md +++ b/tests/benchmark/bloatnet/README.md @@ -85,7 +85,8 @@ _Deploy enough contracts to cover the max gas you plan to use in your tests/benc # Run the interactive deployment script python3 tests/benchmark/bloatnet/deploy_bloatnet_simple.py \ --num-contracts 55403 \ - --factory-address 0x... # Use factory address from step 1 + --factory-address 0x... \ # Use factory address from step 1 + --max-code-size 24576 # Optional: specify max contract size (default: 24576) ``` #### Deployment Output diff --git a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py index 7e8df580fc7..1601039a7fc 100644 --- a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py +++ b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py @@ -165,23 +165,23 @@ def select_contract_type() -> str: sys.exit(0) -def get_bytecode_generator(contract_type: str): - """Get the appropriate bytecode generator for the contract type.""" - generators = { - ContractType.MAX_SIZE_24KB: generate_max_size_bytecode, - ContractType.SLOAD_HEAVY: generate_sload_heavy_bytecode, - # Add more generators as needed - } - - generator = generators.get(contract_type) - if not generator: +def get_bytecode_generator(contract_type: str, max_code_size: int): + """Get the appropriate bytecode generator for the contract type. + + Args: + contract_type: Type of contract to generate + max_code_size: Maximum contract size in bytes + """ + if contract_type == ContractType.MAX_SIZE_24KB: + return lambda salt: generate_max_size_bytecode(salt, max_code_size) + elif contract_type == ContractType.SLOAD_HEAVY: + return lambda salt: generate_sload_heavy_bytecode(salt) + else: print(f"Error: No generator implemented for {contract_type}") if contract_type == ContractType.CUSTOM: print("Custom bytecode deployment not yet implemented") sys.exit(1) - return generator - def deploy_factory(rpc_url: str) -> str: """Deploy CREATE2 factory if needed.""" @@ -222,6 +222,7 @@ def deploy_contracts( num_contracts: int, contract_type: str, factory_address: Optional[str] = None, + max_code_size: int = 24576, ): """Deploy contracts using CREATE2 factory pattern.""" # Connect to Geth @@ -246,7 +247,7 @@ def deploy_contracts( factory_address = deploy_factory(rpc_url) # Get bytecode generator - bytecode_generator = get_bytecode_generator(contract_type) + bytecode_generator = get_bytecode_generator(contract_type, max_code_size) # Generate sample to show info sample_init_code, sample_hash = bytecode_generator(0) @@ -370,6 +371,12 @@ def main(): default=None, help="CREATE2 factory address (deploys new one if not provided)", ) + parser.add_argument( + "--max-code-size", + type=int, + default=24576, + help="Maximum contract code size in bytes (default: 24576 for mainnet/Prague fork)", + ) args = parser.parse_args() @@ -381,6 +388,7 @@ def main(): args.num_contracts, contract_type, args.factory_address, + args.max_code_size, ) diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py index 45a8d4b3df0..5a90127f68b 100644 --- a/tests/benchmark/bloatnet/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -31,7 +31,7 @@ # Gas cost constants - used to calculate required contracts for any gas limit # See README.md for detailed breakdown of these costs GAS_PER_CONTRACT_BALANCE_EXTCODESIZE = 2707 # BALANCE(cold) + EXTCODESIZE(warm) -GAS_PER_CONTRACT_BALANCE_EXTCODECOPY = 5007 # BALANCE(cold) + EXTCODECOPY(warm, 24KB) +GAS_PER_CONTRACT_BALANCE_EXTCODECOPY = 2710 # BALANCE(cold) + EXTCODECOPY(warm, 1 byte) - optimized # Configuration for CREATE2 pre-deployed contracts # These values must match what the deployment script generates @@ -61,7 +61,7 @@ def test_bloatnet_balance_extcodesize( BloatNet test using BALANCE + EXTCODESIZE pattern. This test: - 1. Uses 100+ pre-deployed 24KB contracts + 1. Uses 100+ pre-deployed max-size contracts 2. Calls BALANCE (cold) then EXTCODESIZE (warm) on each 3. Maximizes cache eviction by accessing many contracts 4. Runs with benchmark gas values (5M, 50M, 500M) @@ -155,7 +155,7 @@ def test_bloatnet_balance_extcodecopy( Reading just 1 byte (specifically the last byte) forces the client to load the entire contract from disk while minimizing gas cost, allowing us to - target many more contracts than copying the full 24KB. + target many more contracts than copying the full contract. """ gas_costs = fork.gas_costs() max_contract_size = fork.max_code_size() From f2cd5f9b2f5afc70ca1e3d1af55e2abf3b850d63 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 18 Sep 2025 11:29:23 +0200 Subject: [PATCH 32/54] fix: pre-commit lint hooks --- tests/benchmark/bloatnet/deploy_bloatnet_simple.py | 8 ++++++-- tests/benchmark/bloatnet/test_bloatnet.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py index 1601039a7fc..cd1daa07cf6 100644 --- a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py +++ b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py @@ -35,11 +35,13 @@ class ContractType: def generate_max_size_bytecode(salt: int = 0, max_code_size: int = 24576) -> Tuple[bytes, bytes]: - """Generate max-size contract bytecode for standard bloatnet tests. + """ + Generate max-size contract bytecode for standard bloatnet tests. Args: salt: Unique salt for generating unique bytecode max_code_size: Maximum contract size (default 24576 bytes for mainnet) + """ # Init code copies runtime bytecode to memory and returns it init_code = bytearray() @@ -166,11 +168,13 @@ def select_contract_type() -> str: def get_bytecode_generator(contract_type: str, max_code_size: int): - """Get the appropriate bytecode generator for the contract type. + """ + Get the appropriate bytecode generator for the contract type. Args: contract_type: Type of contract to generate max_code_size: Maximum contract size in bytes + """ if contract_type == ContractType.MAX_SIZE_24KB: return lambda salt: generate_max_size_bytecode(salt, max_code_size) diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py index 5a90127f68b..ca12bf5684f 100644 --- a/tests/benchmark/bloatnet/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -31,7 +31,8 @@ # Gas cost constants - used to calculate required contracts for any gas limit # See README.md for detailed breakdown of these costs GAS_PER_CONTRACT_BALANCE_EXTCODESIZE = 2707 # BALANCE(cold) + EXTCODESIZE(warm) -GAS_PER_CONTRACT_BALANCE_EXTCODECOPY = 2710 # BALANCE(cold) + EXTCODECOPY(warm, 1 byte) - optimized +# BALANCE(cold) + EXTCODECOPY(warm, 1 byte) - optimized +GAS_PER_CONTRACT_BALANCE_EXTCODECOPY = 2710 # Configuration for CREATE2 pre-deployed contracts # These values must match what the deployment script generates From cf2c7c694aef83d214db1fae9ab0f39a164bcf27 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 18 Sep 2025 11:31:31 +0200 Subject: [PATCH 33/54] push updated deploy_create2_factory refactored with EEST as dep --- .../deploy_create2_factory_refactored.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py diff --git a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py new file mode 100644 index 00000000000..6c2443ea9ff --- /dev/null +++ b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Deploy a simple CREATE2 factory for benchmark tests (REFACTORED with EEST Op codes). +This factory can be reused across all tests and allows deterministic addresses. + +This version uses EEST Op code tooling for better readability. +""" + +import argparse +import sys +from pathlib import Path + +# Add parent directories to path to import EEST tools +sys.path.insert(0, str(Path(__file__).parents[3])) + +try: + from eth_utils import keccak + from web3 import Web3 + + from ethereum_test_tools.vm.opcode import Opcodes as Op +except ImportError as e: + print(f"Error: Missing dependencies - {e}") + print("This refactored version requires the EEST framework.") + print("Run: uv sync --all-extras") + sys.exit(1) + + +def deploy_factory(rpc_url: str): + """Deploy a minimal CREATE2 factory using EEST Op codes.""" + # Connect to Geth + w3 = Web3(Web3.HTTPProvider(rpc_url)) + if not w3.is_connected(): + print(f"Failed to connect to {rpc_url}") + return None + + test_account = w3.eth.accounts[0] + print(f"Using test account: {test_account}") + + # Build CREATE2 factory bytecode using EEST Op codes + # This factory: + # 1. Takes salt (first 32 bytes) from calldata + # 2. Takes bytecode (rest) from calldata + # 3. Deploys via CREATE2 + # 4. Returns the deployed address + + factory_code = ( + # Load salt from calldata[0:32] + Op.PUSH0 # offset 0 + + Op.CALLDATALOAD # load 32 bytes from calldata[0] + # Calculate bytecode length (calldatasize - 32) + + Op.PUSH1(32) # salt size + + Op.CALLDATASIZE # total calldata size + + Op.SUB # bytecode_len = calldatasize - 32 + # Copy bytecode from calldata[32:] to memory[0:] + + Op.DUP1 # duplicate bytecode_len for CREATE2 + + Op.PUSH1(32) # source offset in calldata + + Op.PUSH0 # dest offset in memory + + Op.CALLDATACOPY # copy bytecode to memory + # CREATE2(value=0, mem_offset=0, mem_size=bytecode_len, salt) + # Stack: [salt, bytecode_len] + + Op.PUSH0 # value = 0 + + Op.SWAP2 # Stack: [bytecode_len, 0, salt] + + Op.PUSH0 # mem_offset = 0 + + Op.SWAP1 # Stack: [bytecode_len, 0, 0, salt] + + Op.SWAP3 # Stack: [salt, 0, 0, bytecode_len] + + Op.SWAP2 # Stack: [0, salt, 0, bytecode_len] + + Op.SWAP1 # Stack: [salt, 0, 0, bytecode_len] + + Op.CREATE2 # Deploy contract + # Store address in memory and return it + + Op.PUSH0 # memory offset 0 + + Op.MSTORE # store address at memory[0:32] + + Op.PUSH1(32) # return 32 bytes + + Op.PUSH0 # from memory offset 0 + + Op.RETURN # return the address + ) + + # Convert Op code object to bytes + factory_bytecode = bytes(factory_code) + + print(f"\nFactory bytecode ({len(factory_bytecode)} bytes):") + print(f"0x{factory_bytecode.hex()}") + + # Deploy the factory + print("\nDeploying CREATE2 factory...") + tx_hash = w3.eth.send_transaction( + {"from": test_account, "data": factory_bytecode, "gas": 3000000} + ) + + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt["status"] != 1: + print("Failed to deploy factory") + return None + + factory_address = receipt["contractAddress"] + print(f"βœ… Factory deployed at: {factory_address}") + + # Test the factory with a simple contract + print("\nTesting factory...") + test_bytecode = bytes([0x00]) # Simple STOP opcode + test_salt = 0 + + calldata = test_salt.to_bytes(32, "big") + test_bytecode + + # Use eth_call to get the address that would be created + result = w3.eth.call({"to": factory_address, "data": calldata}) + + if result: + test_addr = "0x" + result[-20:].hex() + print(f"Test deployment would create: {test_addr}") + + # Calculate expected CREATE2 address + expected = keccak( + b"\xff" + + bytes.fromhex(factory_address[2:]) + + test_salt.to_bytes(32, "big") + + keccak(test_bytecode) + )[-20:] + expected_addr = "0x" + expected.hex() + print(f"Expected CREATE2 address: {expected_addr}") + + return factory_address + + +def main(): + """Execute the factory deployment script.""" + parser = argparse.ArgumentParser(description="Deploy CREATE2 factory (EEST refactored)") + parser.add_argument( + "--rpc-url", + default="http://127.0.0.1:8545", + help="RPC URL (default: http://127.0.0.1:8545)", + ) + + args = parser.parse_args() + factory_address = deploy_factory(args.rpc_url) + + if factory_address: + print("\n" + "=" * 60) + print("Factory deployed successfully!") + print(f"Factory address: {factory_address}") + print("\nAdd this to your test configuration:") + print(f'FACTORY_ADDRESS = Address("{factory_address}")') + print("=" * 60) + + +if __name__ == "__main__": + main() From a862f765f97c30bb258939d5609d7c303c5211f3 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 19 Sep 2025 10:08:52 +0200 Subject: [PATCH 34/54] refactor(benchmark): enhance CREATE2 factory deployment and testing - Updated the deploy_create2_factory_refactored.py script to improve the deployment of a CREATE2 factory with an initcode template, allowing for dynamic contract address generation. - Modified test_bloatnet.py to support on-the-fly CREATE2 address generation, optimizing gas costs and improving test accuracy. - Adjusted gas cost calculations in the README to reflect the new deployment approach, ensuring accurate benchmarks for BloatNet tests. --- tests/benchmark/bloatnet/README.md | 127 +++++----- .../deploy_create2_factory_refactored.py | 235 ++++++++++++------ tests/benchmark/bloatnet/test_bloatnet.py | 197 ++++++++------- 3 files changed, 330 insertions(+), 229 deletions(-) diff --git a/tests/benchmark/bloatnet/README.md b/tests/benchmark/bloatnet/README.md index 764292521f0..3da2743c9a6 100644 --- a/tests/benchmark/bloatnet/README.md +++ b/tests/benchmark/bloatnet/README.md @@ -2,29 +2,31 @@ ## Overview -The Bloatnet benchmarks work on the following fashion: -1. They usually require a previously-deployed state (usually quite large) which the benchmarks -will interact with. -2. The deployment script helpers help deploying the required bytecode for the specific tests. -3. The outputs of the deployment scripts get hardcoded into the codebase such that the benchmarks can interact with them. +This README pretends to be a guide for any user that wants to run the bloatnet test/benchmark suite in any network. +BloatNet bench cases can be seen in: https://hackmd.io/9icZeLN7R0Sk5mIjKlZAHQ. +The idea of all these tests is to stress client implementations to find out where the limits of processing are focusing specifically on state-related operations. + +In this document you will find a guide that will help you deploy all the setup contracts required by the benchmarks in `/benchmarks/bloatnet`. ## Gas Cost Constants ### BALANCE + EXTCODESIZE Pattern -**Gas per contract: 2,707** -- `PUSH20` (address): 3 gas +**Gas per contract: ~2,772** +- `SHA3` (CREATE2 address generation): 30 gas (static) + 18 gas (dynamic for 85 bytes) - `BALANCE` (cold access): 2,600 gas - `POP`: 2 gas - `EXTCODESIZE` (warm): 100 gas - `POP`: 2 gas +- Memory operations and loop overhead: ~20 gas -### BALANCE + EXTCODECOPY(single-byte) Pattern -**Gas per contract: ~2,710** -- `PUSH20` (address): 3 gas +### BALANCE + EXTCODECOPY(single-byte) Pattern +**Gas per contract: ~2,775** +- `SHA3` (CREATE2 address generation): 30 gas (static) + 18 gas (dynamic for 85 bytes) - `BALANCE` (cold access): 2,600 gas - `POP`: 2 gas - `EXTCODECOPY` (warm, 1 byte): 100 gas (base) + 3 gas (copy 1 byte) -- `POP`: 2 gas +- Memory operations: 4 gas +- Loop overhead: ~20 gas Note: Reading just 1 byte (specifically the last byte at offset 24575) forces the client to load the entire 24KB contract from disk while minimizing gas cost. This allows @@ -35,103 +37,108 @@ targeting nearly as many contracts as the EXTCODESIZE pattern while forcing maxi ### For BALANCE + EXTCODESIZE: | Gas Limit | Contracts Needed | Calculation | | --------- | ---------------- | ------------------- | -| 5M | 1,838 | 5,000,000 Γ· 2,707 | -| 50M | 18,380 | 50,000,000 Γ· 2,707 | -| 150M | 55,403 | 150,000,000 Γ· 2,707 | +| 1M | 352 | 1,000,000 Γ· 2,772 | +| 5M | 1,769 | 5,000,000 Γ· 2,772 | +| 50M | 17,690 | 50,000,000 Γ· 2,772 | +| 150M | 53,071 | 150,000,000 Γ· 2,772 | -### For BALANCE + EXTCODECOPY (Optimized): +### For BALANCE + EXTCODECOPY: | Gas Limit | Contracts Needed | Calculation | | --------- | ---------------- | ------------------- | -| 5M | 1,845 | 5,000,000 Γ· 2,710 | -| 50M | 18,450 | 50,000,000 Γ· 2,710 | -| 150M | 55,350 | 150,000,000 Γ· 2,710 | +| 1M | 352 | 1,000,000 Γ· 2,775 | +| 5M | 1,768 | 5,000,000 Γ· 2,775 | +| 50M | 17,684 | 50,000,000 Γ· 2,775 | +| 150M | 53,053 | 150,000,000 Γ· 2,775 | -You can see the associated attack constants inside of the tests in `bloatnet/test_bloatnet.py` +The CREATE2 address generation adds ~48 gas per contract but eliminates memory limitations in test framework. ## Quick Start: 150M Gas Attack -### 1. Deploy CREATE2 Factory (you can use an already deployed one if preferred and therefore, skip this step) +### 1. Deploy CREATE2 Factory with Initcode Template ```bash -# One-time setup - deploy the CREATE2 factory -python3 tests/benchmark/bloatnet/deploy_create2_factory.py +# Deploy the factory and initcode template (one-time setup) +python3 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py # Output will show: # Factory deployed at: 0x... <-- Save this address +# Init code hash: 0x... <-- Save this hash ``` ### 2. Deploy Contracts -The deployment script is interactive and will guide you through selecting the appropriate contract type for your benchmark. - -#### Contract Types Available - -1. **max_size_24kb**: 24KB contracts filled with unique bytecode (EXTCODE_ type of tests) -2. **sload_heavy**: Contracts optimized for SLOAD benchmarking -3. **storage_heavy**: Contracts with pre-initialized storage -4. **custom**: Custom bytecode (for future extensions) +Deploy contracts using the factory. Each contract will be unique due to ADDRESS-based randomness in the initcode. #### Calculate Contracts Needed Before running the deployment, calculate the number of contracts needed: -- For 150M gas BALANCE+EXTCODESIZE: 55,403 contracts -- For 150M gas BALANCE+EXTCODECOPY: 29,958 contracts +- For 150M gas BALANCE+EXTCODESIZE: 53,071 contracts +- For 150M gas BALANCE+EXTCODECOPY: 53,053 contracts _Deploy enough contracts to cover the max gas you plan to use in your tests/benchmarks._ #### Running the Deployment ```bash -# Run the interactive deployment script -python3 tests/benchmark/bloatnet/deploy_bloatnet_simple.py \ - --num-contracts 55403 \ - --factory-address 0x... \ # Use factory address from step 1 - --max-code-size 24576 # Optional: specify max contract size (default: 24576) +# Deploy contracts for 150M gas attack +python3 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py \ + --deploy-contracts 53100 + +# For smaller tests (e.g., 1M gas) +python3 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py \ + --deploy-contracts 370 ``` #### Deployment Output -After successful deployment, the script will: +After successful deployment, the script will display: -1. Display the configuration needed for tests: -```python -=== Configuration for max_size_24kb tests === -CONTRACT_TYPE = "max_size_24kb" -FACTORY_ADDRESS = Address("0x...") -INIT_CODE_HASH = bytes.fromhex("...") -NUM_DEPLOYED_CONTRACTS = 55403 -``` - -2. Save the configuration to a file: ``` -Configuration saved to: bloatnet_config_max_size_24kb.txt +βœ… Successfully deployed 53100 contracts +NUM_DEPLOYED_CONTRACTS = 53100 ``` -This file contains all the values needed to update your test configuration. - ### 3. Update Test Configuration -Edit `tests/benchmark/bloatnet/test_bloatnet.py` and update: +Edit `tests/benchmark/bloatnet/test_bloatnet.py` and update with values from deployment: ```python -FACTORY_ADDRESS = Address("0x...") # From deployment output -INIT_CODE_HASH = bytes.fromhex("...") # From deployment output -NUM_DEPLOYED_CONTRACTS = 55403 # Actual deployed count +FACTORY_ADDRESS = Address("0x...") # From step 1 output +INIT_CODE_HASH = bytes.fromhex("...") # From step 1 output +NUM_DEPLOYED_CONTRACTS = 53100 # From step 2 output ``` -### 5. Run Benchmark Tests +### 4. Run Benchmark Tests +#### Generate Test Fixtures ```bash # Run with specific gas values (in millions) uv run fill --fork=Prague --gas-benchmark-values=150 \ tests/benchmark/bloatnet/test_bloatnet.py --clean -# With EVM traces for analysis +# Multiple gas values +uv run fill --fork=Prague --gas-benchmark-values=1,5,50,150 \ + tests/benchmark/bloatnet/test_bloatnet.py +``` + +#### Execute Against Live Client +```bash +# Start a test node (e.g., Geth) +geth --dev --http --http.api eth,web3,net,debug + +# Run tests +uv run execute remote --rpc-endpoint http://127.0.0.1:8545 \ + --rpc-chain-id 1337 --rpc-seed-key 0x0000000000000000000000000000000000000000000000000000000000000001 \ + tests/benchmark/bloatnet/test_bloatnet.py \ + --fork=Prague --gas-benchmark-values=150 -v +``` + +#### With EVM Traces for Analysis +```bash uv run fill --fork=Prague --gas-benchmark-values=150 \ --evm-dump-dir=traces/ --traces \ tests/benchmark/bloatnet/test_bloatnet.py -# Multiple gas values -uv run fill --fork=Prague --gas-benchmark-values=5,50,150 \ - tests/benchmark/bloatnet/test_bloatnet.py +# Analyze opcodes executed +jq -r '.opName' traces/**/*.jsonl | sort | uniq -c ``` diff --git a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py index 6c2443ea9ff..f4a5ce64867 100644 --- a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py +++ b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 """ -Deploy a simple CREATE2 factory for benchmark tests (REFACTORED with EEST Op codes). -This factory can be reused across all tests and allows deterministic addresses. +Deploy a CREATE2 factory for on-the-fly contract address generation in BloatNet tests. -This version uses EEST Op code tooling for better readability. +This factory uses a constant initcode that generates unique 24KB contracts by: +1. Using ADDRESS opcode for pseudo-randomness (within the factory's context) +2. Expanding randomness with SHA3 and XOR operations +3. Creating max-size contracts with deterministic CREATE2 addresses """ import argparse @@ -24,123 +26,204 @@ print("Run: uv sync --all-extras") sys.exit(1) +# XOR table for pseudo-random bytecode generation (reused from test_worst_bytecode.py) +XOR_TABLE_SIZE = 256 +XOR_TABLE = [keccak(i.to_bytes(32, "big")) for i in range(XOR_TABLE_SIZE)] +MAX_CONTRACT_SIZE = 24576 # 24KB + + +def build_initcode() -> bytes: + """Build the initcode that generates unique 24KB contracts using ADDRESS for randomness.""" + from ethereum_test_tools import While + + # This initcode follows the pattern from test_worst_bytecode.py: + # 1. Uses ADDRESS as initial seed for pseudo-randomness (creates uniqueness per deployment) + # 2. Expands to 24KB using SHA3 and XOR operations + # 3. Sets first byte to STOP for quick CALL returns + initcode = ( + # Store ADDRESS as initial seed - THIS IS CRITICAL FOR UNIQUENESS + Op.MSTORE(0, Op.ADDRESS) + # Loop to expand bytecode using SHA3 and XOR operations + + While( + body=( + Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) + # Use XOR table to expand without excessive SHA3 calls + + sum( + (Op.PUSH32[xor_value] + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) + for xor_value in XOR_TABLE + ) + + Op.POP + ), + condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE), + ) + # Set first byte to STOP for efficient CALL handling + + Op.MSTORE8(0, 0x00) + # Return the full contract + + Op.RETURN(0, MAX_CONTRACT_SIZE) + ) + return bytes(initcode) + -def deploy_factory(rpc_url: str): - """Deploy a minimal CREATE2 factory using EEST Op codes.""" +def deploy_factory_and_initcode(rpc_url: str): + """Deploy the initcode template and factory that uses it.""" # Connect to Geth w3 = Web3(Web3.HTTPProvider(rpc_url)) if not w3.is_connected(): print(f"Failed to connect to {rpc_url}") - return None + return None, None test_account = w3.eth.accounts[0] print(f"Using test account: {test_account}") - # Build CREATE2 factory bytecode using EEST Op codes - # This factory: - # 1. Takes salt (first 32 bytes) from calldata - # 2. Takes bytecode (rest) from calldata - # 3. Deploys via CREATE2 - # 4. Returns the deployed address + # Build the initcode + initcode = build_initcode() + print(f"\nInitcode size: {len(initcode)} bytes") + print(f"Initcode (first 100 bytes): 0x{initcode[:100].hex()}...") + + # Deploy the initcode as a contract that the factory can copy from + print("\nDeploying initcode template contract...") + tx_hash = w3.eth.send_transaction({"from": test_account, "data": initcode, "gas": 10000000}) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt["status"] != 1: + print("Failed to deploy initcode template") + return None, None + initcode_address = receipt["contractAddress"] + print(f"βœ… Initcode template deployed at: {initcode_address}") + + # Build the factory contract following the pattern from test_worst_bytecode.py + # The factory: + # 1. Copies the initcode from the template contract + # 2. Uses incrementing salt from storage for CREATE2 + # 3. Returns the created contract address factory_code = ( - # Load salt from calldata[0:32] - Op.PUSH0 # offset 0 - + Op.CALLDATALOAD # load 32 bytes from calldata[0] - # Calculate bytecode length (calldatasize - 32) - + Op.PUSH1(32) # salt size - + Op.CALLDATASIZE # total calldata size - + Op.SUB # bytecode_len = calldatasize - 32 - # Copy bytecode from calldata[32:] to memory[0:] - + Op.DUP1 # duplicate bytecode_len for CREATE2 - + Op.PUSH1(32) # source offset in calldata - + Op.PUSH0 # dest offset in memory - + Op.CALLDATACOPY # copy bytecode to memory - # CREATE2(value=0, mem_offset=0, mem_size=bytecode_len, salt) - # Stack: [salt, bytecode_len] - + Op.PUSH0 # value = 0 - + Op.SWAP2 # Stack: [bytecode_len, 0, salt] - + Op.PUSH0 # mem_offset = 0 - + Op.SWAP1 # Stack: [bytecode_len, 0, 0, salt] - + Op.SWAP3 # Stack: [salt, 0, 0, bytecode_len] - + Op.SWAP2 # Stack: [0, salt, 0, bytecode_len] - + Op.SWAP1 # Stack: [salt, 0, 0, bytecode_len] - + Op.CREATE2 # Deploy contract - # Store address in memory and return it - + Op.PUSH0 # memory offset 0 - + Op.MSTORE # store address at memory[0:32] - + Op.PUSH1(32) # return 32 bytes - + Op.PUSH0 # from memory offset 0 - + Op.RETURN # return the address + # Copy initcode from template to memory + Op.EXTCODECOPY( + address=initcode_address, + dest_offset=0, + offset=0, + size=Op.EXTCODESIZE(initcode_address), + ) + # Store the result of CREATE2 + + Op.MSTORE( + 0, + Op.CREATE2( + value=0, + offset=0, + size=Op.EXTCODESIZE(initcode_address), + salt=Op.SLOAD(0), + ), + ) + # Increment salt for next call + + Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) + # Return created address + + Op.RETURN(0, 32) ) - # Convert Op code object to bytes factory_bytecode = bytes(factory_code) - - print(f"\nFactory bytecode ({len(factory_bytecode)} bytes):") - print(f"0x{factory_bytecode.hex()}") + print(f"\nFactory bytecode size: {len(factory_bytecode)} bytes") + print(f"Factory bytecode: 0x{factory_bytecode.hex()}") # Deploy the factory print("\nDeploying CREATE2 factory...") tx_hash = w3.eth.send_transaction( {"from": test_account, "data": factory_bytecode, "gas": 3000000} ) - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) if receipt["status"] != 1: print("Failed to deploy factory") - return None + return None, None factory_address = receipt["contractAddress"] print(f"βœ… Factory deployed at: {factory_address}") - # Test the factory with a simple contract - print("\nTesting factory...") - test_bytecode = bytes([0x00]) # Simple STOP opcode - test_salt = 0 + # Calculate init code hash for CREATE2 address calculation + init_code_hash = keccak(initcode) + print(f"\nInit code hash: 0x{init_code_hash.hex()}") - calldata = test_salt.to_bytes(32, "big") + test_bytecode + return factory_address, init_code_hash.hex() - # Use eth_call to get the address that would be created - result = w3.eth.call({"to": factory_address, "data": calldata}) - if result: - test_addr = "0x" + result[-20:].hex() - print(f"Test deployment would create: {test_addr}") +def deploy_contracts(rpc_url: str, factory_address: str, num_contracts: int): + """Deploy multiple contracts using the factory.""" + w3 = Web3(Web3.HTTPProvider(rpc_url)) + if not w3.is_connected(): + print(f"Failed to connect to {rpc_url}") + return False + + test_account = w3.eth.accounts[0] + print(f"\nDeploying {num_contracts} contracts via factory...") + + # Batch size for deployments + batch_size = 100 + deployed_count = 0 + + for batch_start in range(0, num_contracts, batch_size): + batch_end = min(batch_start + batch_size, num_contracts) + current_batch = batch_end - batch_start - # Calculate expected CREATE2 address - expected = keccak( - b"\xff" - + bytes.fromhex(factory_address[2:]) - + test_salt.to_bytes(32, "big") - + keccak(test_bytecode) - )[-20:] - expected_addr = "0x" + expected.hex() - print(f"Expected CREATE2 address: {expected_addr}") + batch_num = batch_start // batch_size + 1 + print(f"Deploying batch {batch_num}: contracts {batch_start}-{batch_end - 1}...") - return factory_address + for i in range(current_batch): + try: + tx_hash = w3.eth.send_transaction( + {"from": test_account, "to": factory_address, "gas": 10000000} + ) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60) + if receipt["status"] == 1: + deployed_count += 1 + else: + print(f" ⚠️ Failed to deploy contract {batch_start + i}") + except Exception as e: + print(f" ⚠️ Error deploying contract {batch_start + i}: {e}") + + print(f" βœ… Deployed {deployed_count}/{batch_end} contracts") + + return deployed_count == num_contracts def main(): """Execute the factory deployment script.""" - parser = argparse.ArgumentParser(description="Deploy CREATE2 factory (EEST refactored)") + parser = argparse.ArgumentParser(description="Deploy CREATE2 factory for BloatNet tests") parser.add_argument( "--rpc-url", default="http://127.0.0.1:8545", help="RPC URL (default: http://127.0.0.1:8545)", ) + parser.add_argument( + "--deploy-contracts", type=int, metavar="N", help="Deploy N contracts using the factory" + ) args = parser.parse_args() - factory_address = deploy_factory(args.rpc_url) - - if factory_address: - print("\n" + "=" * 60) - print("Factory deployed successfully!") - print(f"Factory address: {factory_address}") - print("\nAdd this to your test configuration:") - print(f'FACTORY_ADDRESS = Address("{factory_address}")') - print("=" * 60) + + # Deploy factory and initcode template + factory_address, init_code_hash = deploy_factory_and_initcode(args.rpc_url) + + if not factory_address: + print("\n❌ Failed to deploy factory") + sys.exit(1) + + print("\n" + "=" * 60) + print("Factory deployed successfully!") + print(f"Factory address: {factory_address}") + print(f"Init code hash: 0x{init_code_hash}") + print("\nAdd this to your test configuration:") + print(f'FACTORY_ADDRESS = Address("{factory_address}")') + print(f'INIT_CODE_HASH = bytes.fromhex("{init_code_hash}")') + + # Deploy contracts if requested + if args.deploy_contracts: + success = deploy_contracts(args.rpc_url, factory_address, args.deploy_contracts) + if success: + print(f"\nβœ… Successfully deployed {args.deploy_contracts} contracts") + print(f"NUM_DEPLOYED_CONTRACTS = {args.deploy_contracts}") + else: + print("\n⚠️ Some contracts failed to deploy") + + print("=" * 60) if __name__ == "__main__": diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py index ca12bf5684f..480f5513e58 100644 --- a/tests/benchmark/bloatnet/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -1,13 +1,8 @@ """ -abstract: Optimized BloatNet tests using CREATE2 pre-deployed contracts. +abstract: BloatNet bench cases extracted from https://hackmd.io/9icZeLN7R0Sk5mIjKlZAHQ. - This test uses CREATE2 factory-deployed contracts at deterministic addresses. - Deploy contracts using: python3 scripts/deploy_bloatnet_simple.py --num-contracts 1838 - - The CREATE2 pattern allows: - - Contracts to be deployed from any account - - Reuse across different test scenarios - - Deterministic addresses regardless of deployer nonce + The idea of all these tests is to stress client implementations to find out where the limits of + processing are focusing specifically on state-related operations. """ import pytest @@ -19,36 +14,22 @@ Alloc, Block, BlockchainTestFiller, - Bytecode, Transaction, - keccak256, + While, ) from ethereum_test_tools.vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" -# Gas cost constants - used to calculate required contracts for any gas limit -# See README.md for detailed breakdown of these costs -GAS_PER_CONTRACT_BALANCE_EXTCODESIZE = 2707 # BALANCE(cold) + EXTCODESIZE(warm) -# BALANCE(cold) + EXTCODECOPY(warm, 1 byte) - optimized -GAS_PER_CONTRACT_BALANCE_EXTCODECOPY = 2710 -# Configuration for CREATE2 pre-deployed contracts -# These values must match what the deployment script generates -# UPDATE THESE VALUES after running deploy_bloatnet_simple.py -FACTORY_ADDRESS = Address("0x07EADb2f6b02Bb9fE994c0AeFf106625c0d3C93f") # UPDATE THIS +# Configuration for CREATE2 factory - UPDATE THESE after running deploy script +# These values come from deploy_create2_factory_refactored.py output +FACTORY_ADDRESS = Address("0x847a04f0a1FfC4E68CC80e7D870Eb4eC51235CE8") # UPDATE THIS INIT_CODE_HASH = bytes.fromhex( - "8eba8b9d04aea4e6f3d6601ad364021e8e37b6282a0f57e40c635ba7f80aa0cb" + "9e1a230bdc29e66a6027083ec52c6724e7e6cac4a8e59c1c9a852c0a1e954b45" ) # UPDATE THIS -NUM_DEPLOYED_CONTRACTS = 1838 # UPDATE THIS - current setup for 5M gas - - -def calculate_create2_address(factory: Address, salt: int, init_code_hash: bytes) -> Address: - """Calculate CREATE2 address from factory, salt, and init code hash.""" - create2_input = b"\xff" + bytes(factory) + salt.to_bytes(32, "big") + init_code_hash - addr_bytes = keccak256(create2_input)[12:] - return Address(addr_bytes) +NUM_DEPLOYED_CONTRACTS = 370 # UPDATE THIS - number of contracts deployed via factory @pytest.mark.valid_from("Prague") @@ -59,34 +40,37 @@ def test_bloatnet_balance_extcodesize( gas_benchmark_value: int, ): """ - BloatNet test using BALANCE + EXTCODESIZE pattern. + BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2 address generation. This test: - 1. Uses 100+ pre-deployed max-size contracts - 2. Calls BALANCE (cold) then EXTCODESIZE (warm) on each - 3. Maximizes cache eviction by accessing many contracts - 4. Runs with benchmark gas values (5M, 50M, 500M) + 1. Assumes contracts are already deployed via the factory (salt 0 to N-1) + 2. Generates CREATE2 addresses dynamically during execution + 3. Calls BALANCE (cold) then EXTCODESIZE (warm) on each + 4. Maximizes cache eviction by accessing many contracts """ gas_costs = fork.gas_costs() # Calculate gas costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Cost per contract access + # Cost per contract access with CREATE2 address generation cost_per_contract = ( - 3 # PUSH20 for address + gas_costs.G_KECCAK_256 # SHA3 static cost for address generation + + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) + gas_costs.G_BASE # POP balance + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100) + gas_costs.G_BASE # POP code size + + 20 # Overhead for memory operations and loop control ) # Calculate how many contracts to access available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup contracts_needed = int(available_gas // cost_per_contract) + + # Limit to actually deployed contracts num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) - # Log the calculation for transparency if contracts_needed > NUM_DEPLOYED_CONTRACTS: import warnings @@ -94,48 +78,58 @@ def test_bloatnet_balance_extcodesize( f"Test needs {contracts_needed} contracts for " f"{gas_benchmark_value / 1_000_000:.1f}M gas, " f"but only {NUM_DEPLOYED_CONTRACTS} are deployed. " - f"Deploy {contracts_needed - NUM_DEPLOYED_CONTRACTS} more contracts " - f"for full test coverage.", + f"Deploy {contracts_needed} contracts for full test coverage.", stacklevel=2, ) - # Generate attack contract with unrolled loop - attack_code = Bytecode() - - # Pre-calculate all contract addresses using CREATE2 - for i in range(num_contracts): - addr = calculate_create2_address(FACTORY_ADDRESS, i, INIT_CODE_HASH) - attack_code += ( - Op.PUSH20[int.from_bytes(bytes(addr), "big")] - + Op.DUP1 - + Op.BALANCE - + Op.POP - + Op.EXTCODESIZE - + Op.POP + # Generate attack contract with on-the-fly CREATE2 address calculation + attack_code = ( + # Setup memory for CREATE2 address generation + # Memory layout: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + Op.MSTORE(0, FACTORY_ADDRESS) + + Op.MSTORE8(32 - 20 - 1, 0xFF) # Prefix for CREATE2 + + Op.MSTORE(32, 0) # Initial salt (start from 0) + + Op.MSTORE(64, INIT_CODE_HASH) # Init code hash + # Limit counter for number of contracts to access + + Op.PUSH2(num_contracts) + # Main attack loop + + While( + body=( + # Generate CREATE2 address: keccak256(0xFF + factory + salt + init_code_hash) + Op.SHA3(32 - 20 - 1, 85) # Hash 85 bytes starting from 0xFF + # The address is now on the stack + + Op.DUP1 # Duplicate for EXTCODESIZE + + Op.BALANCE # Cold access + + Op.POP + + Op.EXTCODESIZE # Warm access + + Op.POP + # Increment salt for next iteration + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + ), + # Continue while we haven't reached the limit + condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, ) - - # Success marker (benchmark tests typically run out of gas, but this is for smaller tests) - attack_code += Op.PUSH1[1] + Op.PUSH0 + Op.SSTORE + + Op.POP # Clean up counter + ) # Deploy attack contract attack_address = pre.deploy_contract(code=attack_code) - # Attack transaction - tx = Transaction( + # Run the attack + attack_tx = Transaction( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), ) # Post-state: just verify attack contract exists - # Benchmark tests run out of gas, so no success flag post = { attack_address: Account(storage={}), } blockchain_test( pre=pre, - blocks=[Block(txs=[tx])], + blocks=[Block(txs=[attack_tx])], post=post, ) @@ -148,15 +142,13 @@ def test_bloatnet_balance_extcodecopy( gas_benchmark_value: int, ): """ - BloatNet test using BALANCE + EXTCODECOPY for maximum I/O. + BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2 address generation. This test forces actual bytecode reads from disk by: - 1. Using BALANCE (cold) to warm the account - 2. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode - - Reading just 1 byte (specifically the last byte) forces the client to load - the entire contract from disk while minimizing gas cost, allowing us to - target many more contracts than copying the full contract. + 1. Assumes contracts are already deployed via the factory + 2. Generating CREATE2 addresses dynamically during execution + 3. Using BALANCE (cold) to warm the account + 4. Using EXTCODECOPY (warm) to read 1 byte from the END of the bytecode """ gas_costs = fork.gas_costs() max_contract_size = fork.max_code_size() @@ -164,22 +156,25 @@ def test_bloatnet_balance_extcodecopy( # Calculate costs intrinsic_gas = fork.transaction_intrinsic_cost_calculator()(calldata=b"") - # Cost per contract with EXTCODECOPY (copying just 1 byte) + # Cost per contract with EXTCODECOPY and CREATE2 address generation cost_per_contract = ( - 3 # PUSH20 for address + gas_costs.G_KECCAK_256 # SHA3 static cost for address generation + + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) + gas_costs.G_BASE # POP balance + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) - + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3 * 1 = 3) + + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3) + gas_costs.G_BASE * 4 # PUSH operations and POP + + 20 # Overhead ) # Calculate how many contracts to access available_gas = gas_benchmark_value - intrinsic_gas - 1000 contracts_needed = int(available_gas // cost_per_contract) + + # Limit to actually deployed contracts num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) - # Log the calculation for transparency if contracts_needed > NUM_DEPLOYED_CONTRACTS: import warnings @@ -187,37 +182,53 @@ def test_bloatnet_balance_extcodecopy( f"Test needs {contracts_needed} contracts for " f"{gas_benchmark_value / 1_000_000:.1f}M gas, " f"but only {NUM_DEPLOYED_CONTRACTS} are deployed. " - f"Deploy {contracts_needed - NUM_DEPLOYED_CONTRACTS} more contracts " - f"for full test coverage.", + f"Deploy {contracts_needed} contracts for full test coverage.", stacklevel=2, ) - # Generate attack contract - attack_code = Bytecode() - - # Access each contract using CREATE2 addresses - for i in range(num_contracts): - addr = calculate_create2_address(FACTORY_ADDRESS, i, INIT_CODE_HASH) - attack_code += ( - Op.PUSH20[int.from_bytes(bytes(addr), "big")] - + Op.DUP1 - + Op.BALANCE - + Op.POP - # EXTCODECOPY(addr, mem_offset, last_byte_offset, 1) - # Read the LAST byte of the contract to force full load from disk - + Op.PUSH1[1] # size (1 byte) - + Op.PUSH2[max_contract_size - 1] # code offset (last byte) - + Op.PUSH2[i] # memory offset (unique per contract to avoid overlap) - + Op.DUP4 # address - + Op.EXTCODECOPY - + Op.POP # clean up address + # Generate attack contract with on-the-fly CREATE2 address calculation + attack_code = ( + # Setup memory for CREATE2 address generation + Op.MSTORE(0, FACTORY_ADDRESS) + + Op.MSTORE8(32 - 20 - 1, 0xFF) + + Op.MSTORE(32, 0) # Initial salt (start from 0) + + Op.MSTORE(64, INIT_CODE_HASH) + # Counter for number of contracts + + Op.PUSH2(num_contracts) + # Main attack loop + + While( + body=( + # Generate CREATE2 address + Op.SHA3(32 - 20 - 1, 85) + # The address is now on the stack + + Op.DUP1 # Duplicate for later operations + + Op.BALANCE # Cold access + + Op.POP + # EXTCODECOPY(addr, mem_offset, last_byte_offset, 1) + # Read the LAST byte to force full contract load + + Op.PUSH1(1) # size (1 byte) + + Op.PUSH2(max_contract_size - 1) # code offset (last byte) + # Use salt as memory offset to avoid overlap + + Op.MLOAD(32) # Get current salt + + Op.PUSH2(96) # Base memory offset + + Op.ADD # Unique memory position + + Op.DUP4 # address (duplicated earlier) + + Op.EXTCODECOPY + + Op.POP # Clean up address + # Increment salt + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + ), + # Continue while counter > 0 + condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, ) + + Op.POP # Clean up counter + ) # Deploy attack contract attack_address = pre.deploy_contract(code=attack_code) - # Attack transaction - tx = Transaction( + # Run the attack + attack_tx = Transaction( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), @@ -230,6 +241,6 @@ def test_bloatnet_balance_extcodecopy( blockchain_test( pre=pre, - blocks=[Block(txs=[tx])], + blocks=[Block(txs=[attack_tx])], post=post, ) From 55396fba5c2af78b347c54c58a9b2df13f4e676f Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 19 Sep 2025 10:53:35 +0200 Subject: [PATCH 35/54] remove: old_deploy_factory script --- .../bloatnet/deploy_create2_factory.py | 130 ------------------ 1 file changed, 130 deletions(-) delete mode 100644 tests/benchmark/bloatnet/deploy_create2_factory.py diff --git a/tests/benchmark/bloatnet/deploy_create2_factory.py b/tests/benchmark/bloatnet/deploy_create2_factory.py deleted file mode 100644 index 9195029725f..00000000000 --- a/tests/benchmark/bloatnet/deploy_create2_factory.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -""" -Deploy a simple CREATE2 factory for benchmark tests. -This factory can be reused across all tests and allows deterministic addresses. -""" - -import argparse - -from eth_utils import keccak -from web3 import Web3 - - -def deploy_factory(rpc_url: str): - """Deploy a minimal CREATE2 factory.""" - # Connect to Geth - w3 = Web3(Web3.HTTPProvider(rpc_url)) - if not w3.is_connected(): - print(f"Failed to connect to {rpc_url}") - return None - - test_account = w3.eth.accounts[0] - print(f"Using test account: {test_account}") - - # Minimal CREATE2 factory bytecode - # Takes salt (first 32 bytes) and bytecode (rest) from calldata - # Returns the deployed address - factory_bytecode = bytes( - [ - # Runtime code - 0x60, - 0x00, # PUSH1 0x00 (value for CREATE2) - 0x60, - 0x00, # PUSH1 0x00 (salt position in calldata) - 0x35, # CALLDATALOAD (load salt) - 0x60, - 0x20, # PUSH1 0x20 (bytecode starts at position 32) - 0x36, # CALLDATASIZE - 0x60, - 0x20, # PUSH1 0x20 - 0x03, # SUB (bytecode length = calldatasize - 32) - 0x80, # DUP1 (duplicate bytecode length) - 0x60, - 0x20, # PUSH1 0x20 (source position in calldata) - 0x60, - 0x00, # PUSH1 0x00 (dest position in memory) - 0x37, # CALLDATACOPY (copy bytecode to memory) - 0xF5, # CREATE2 (value=0, mem_offset=0, mem_size, salt) - 0x60, - 0x00, # PUSH1 0x00 - 0x52, # MSTORE (store address at position 0) - 0x60, - 0x20, # PUSH1 0x20 - 0x60, - 0x00, # PUSH1 0x00 - 0xF3, # RETURN (return the address) - ] - ) - - # Deploy the factory - print("\nDeploying CREATE2 factory...") - tx_hash = w3.eth.send_transaction( - {"from": test_account, "data": bytes.fromhex(factory_bytecode.hex()), "gas": 3000000} - ) - - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - - if receipt["status"] != 1: - print("Failed to deploy factory") - return None - - factory_address = receipt["contractAddress"] - print(f"βœ… Factory deployed at: {factory_address}") - - # Test the factory with a simple contract - print("\nTesting factory...") - test_bytecode = bytes([0x00]) # Simple STOP opcode - test_salt = 0 - - calldata = test_salt.to_bytes(32, "big") + test_bytecode - - # Use eth_call to get the address that would be created - if factory_address: - result = w3.eth.call({"to": factory_address, "data": bytes.fromhex(calldata.hex())}) - else: - print("Factory address is None") - return None - - if result: - test_addr = "0x" + result[-20:].hex() - print(f"Test deployment would create: {test_addr}") - - # Calculate expected CREATE2 address - if factory_address: - expected = keccak( - b"\xff" - + bytes.fromhex(factory_address[2:]) - + test_salt.to_bytes(32, "big") - + keccak(test_bytecode) - )[-20:] - else: - expected = b"" - expected_addr = "0x" + expected.hex() - print(f"Expected CREATE2 address: {expected_addr}") - - return factory_address - - -def main(): - """Execute the factory deployment script.""" - parser = argparse.ArgumentParser(description="Deploy CREATE2 factory") - parser.add_argument( - "--rpc-url", - default="http://127.0.0.1:8545", - help="RPC URL (default: http://127.0.0.1:8545)", - ) - - args = parser.parse_args() - factory_address = deploy_factory(args.rpc_url) - - if factory_address: - print("\n" + "=" * 60) - print("Factory deployed successfully!") - print(f"Factory address: {factory_address}") - print("\nAdd this to your test configuration:") - print(f'FACTORY_ADDRESS = Address("{factory_address}")') - print("=" * 60) - - -if __name__ == "__main__": - main() From 240c0422be63cadb84307f27ce4ac5936002a59c Mon Sep 17 00:00:00 2001 From: CPerezz Date: Mon, 22 Sep 2025 14:32:43 +0200 Subject: [PATCH 36/54] chore: address PR review fixes --- .../bloatnet/deploy_create2_factory_refactored.py | 2 +- tests/benchmark/bloatnet/test_bloatnet.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py index f4a5ce64867..ab03a537460 100644 --- a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py +++ b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py @@ -19,7 +19,7 @@ from eth_utils import keccak from web3 import Web3 - from ethereum_test_tools.vm.opcode import Opcodes as Op + from ethereum_test_vm.opcode import Opcodes as Op except ImportError as e: print(f"Error: Missing dependencies - {e}") print("This refactored version requires the EEST framework.") diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py index 480f5513e58..91d8e7c7989 100644 --- a/tests/benchmark/bloatnet/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -5,6 +5,8 @@ processing are focusing specifically on state-related operations. """ +import warnings + import pytest from ethereum_test_forks import Fork @@ -17,7 +19,7 @@ Transaction, While, ) -from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_vm.opcode import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" @@ -72,8 +74,6 @@ def test_bloatnet_balance_extcodesize( num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) if contracts_needed > NUM_DEPLOYED_CONTRACTS: - import warnings - warnings.warn( f"Test needs {contracts_needed} contracts for " f"{gas_benchmark_value / 1_000_000:.1f}M gas, " @@ -131,6 +131,7 @@ def test_bloatnet_balance_extcodesize( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, + skip_gas_used_validation=True, ) @@ -176,8 +177,6 @@ def test_bloatnet_balance_extcodecopy( num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) if contracts_needed > NUM_DEPLOYED_CONTRACTS: - import warnings - warnings.warn( f"Test needs {contracts_needed} contracts for " f"{gas_benchmark_value / 1_000_000:.1f}M gas, " @@ -243,4 +242,5 @@ def test_bloatnet_balance_extcodecopy( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, + skip_gas_used_validation=True, ) From aead28c4d4953209a0988b39905b1e0ca3eb354b Mon Sep 17 00:00:00 2001 From: CPerezz Date: Mon, 22 Sep 2025 15:09:54 +0200 Subject: [PATCH 37/54] fix(benchmark): correct import path for ethereum_test_vm --- tests/benchmark/bloatnet/deploy_create2_factory_refactored.py | 2 +- tests/benchmark/bloatnet/test_bloatnet.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py index ab03a537460..94d75694421 100644 --- a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py +++ b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py @@ -19,7 +19,7 @@ from eth_utils import keccak from web3 import Web3 - from ethereum_test_vm.opcode import Opcodes as Op + from ethereum_test_vm import Opcodes as Op except ImportError as e: print(f"Error: Missing dependencies - {e}") print("This refactored version requires the EEST framework.") diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_bloatnet.py index 91d8e7c7989..2aec8689876 100644 --- a/tests/benchmark/bloatnet/test_bloatnet.py +++ b/tests/benchmark/bloatnet/test_bloatnet.py @@ -19,7 +19,7 @@ Transaction, While, ) -from ethereum_test_vm.opcode import Opcodes as Op +from ethereum_test_vm import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" From a4fac3b59fdaa4209eb7e7b1bb3f9f074926032a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Mon, 22 Sep 2025 16:06:03 +0200 Subject: [PATCH 38/54] chore(benchmark): update according to review comments Also, renamed the test file to include only multi-opcode tests there and have a more clean directory for future test inclusions. --- docs/CHANGELOG.md | 2 +- pyproject.toml | 17 +- tests/benchmark/bloatnet/README.md | 144 ------- tests/benchmark/bloatnet/__init__.py | 1 + .../bloatnet/deploy_bloatnet_simple.py | 400 ------------------ .../deploy_create2_factory_refactored.py | 230 ---------- .../{test_bloatnet.py => test_mutiopcode.py} | 0 7 files changed, 9 insertions(+), 785 deletions(-) delete mode 100644 tests/benchmark/bloatnet/README.md create mode 100644 tests/benchmark/bloatnet/__init__.py delete mode 100644 tests/benchmark/bloatnet/deploy_bloatnet_simple.py delete mode 100644 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py rename tests/benchmark/bloatnet/{test_bloatnet.py => test_mutiopcode.py} (100%) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6966ac0f21d..3f22da6277f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -227,7 +227,7 @@ Users can select any of the artifacts depending on their benchmarking or testing ### πŸ§ͺ Test Cases -- ✨ [BloatNet](bloatnet.info)/Multidimensional Metering: Add benchmarks to be used as part of the BloatNet project and also for Multidimensional Metering. +- ✨ [BloatNet](https://bloatnet.info)/Multidimensional Metering: Add benchmarks to be used as part of the BloatNet project and also for Multidimensional Metering. - ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): Add additional test cases for modular comparison. - πŸ”€ Refactored `BLOBHASH` opcode context tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1637](https://github.com/ethereum/execution-spec-tests/pull/1637)). - πŸ”€ Refactored `SELFDESTRUCT` opcode collision tests to use the `pre_alloc` plugin in order to avoid contract and EOA address collisions ([#1643](https://github.com/ethereum/execution-spec-tests/pull/1643)). diff --git a/pyproject.toml b/pyproject.toml index cfdc93efa81..6e665dbdecc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ docs = [ "mkdocstrings-python>=1.0.0,<2", "pillow>=10.0.1,<11", "pyspelling>=2.8.2,<3", - "lxml>=6.0.0,<7", # needs to be >= 6.0 for pypy + "lxml>=6.0.0,<7", # needs to be >= 6.0 for pypy "setuptools==78.0.2", ] @@ -146,7 +146,6 @@ exclude = [ '^fixtures/', '^logs/', '^site/', - '^tests/benchmark/bloatnet/deploy_.*\.py$', ] plugins = ["pydantic.mypy"] @@ -158,15 +157,13 @@ ignore-words-list = "ingenuous" [tool.pytest.ini_options] console_output_style = "count" minversion = "7.0" -python_files = [ - "test_*.py" -] -testpaths = [ - "src" -] +python_files = ["test_*.py"] +testpaths = ["src"] addopts = [ - "-p", "pytester", - "-p", "pytest_plugins.eels_resolver", + "-p", + "pytester", + "-p", + "pytest_plugins.eels_resolver", "--ignore=src/pytest_plugins/consume/test_cache.py", "--ignore=src/pytest_plugins/consume/direct/", "--ignore=src/pytest_plugins/consume/simulators/", diff --git a/tests/benchmark/bloatnet/README.md b/tests/benchmark/bloatnet/README.md deleted file mode 100644 index 3da2743c9a6..00000000000 --- a/tests/benchmark/bloatnet/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# BloatNet Benchmark Tests setup guide - -## Overview - -This README pretends to be a guide for any user that wants to run the bloatnet test/benchmark suite in any network. -BloatNet bench cases can be seen in: https://hackmd.io/9icZeLN7R0Sk5mIjKlZAHQ. -The idea of all these tests is to stress client implementations to find out where the limits of processing are focusing specifically on state-related operations. - -In this document you will find a guide that will help you deploy all the setup contracts required by the benchmarks in `/benchmarks/bloatnet`. - -## Gas Cost Constants - -### BALANCE + EXTCODESIZE Pattern -**Gas per contract: ~2,772** -- `SHA3` (CREATE2 address generation): 30 gas (static) + 18 gas (dynamic for 85 bytes) -- `BALANCE` (cold access): 2,600 gas -- `POP`: 2 gas -- `EXTCODESIZE` (warm): 100 gas -- `POP`: 2 gas -- Memory operations and loop overhead: ~20 gas - -### BALANCE + EXTCODECOPY(single-byte) Pattern -**Gas per contract: ~2,775** -- `SHA3` (CREATE2 address generation): 30 gas (static) + 18 gas (dynamic for 85 bytes) -- `BALANCE` (cold access): 2,600 gas -- `POP`: 2 gas -- `EXTCODECOPY` (warm, 1 byte): 100 gas (base) + 3 gas (copy 1 byte) -- Memory operations: 4 gas -- Loop overhead: ~20 gas - -Note: Reading just 1 byte (specifically the last byte at offset 24575) forces the client -to load the entire 24KB contract from disk while minimizing gas cost. This allows -targeting nearly as many contracts as the EXTCODESIZE pattern while forcing maximum I/O. - -## Required Contracts Calculation Example: - -### For BALANCE + EXTCODESIZE: -| Gas Limit | Contracts Needed | Calculation | -| --------- | ---------------- | ------------------- | -| 1M | 352 | 1,000,000 Γ· 2,772 | -| 5M | 1,769 | 5,000,000 Γ· 2,772 | -| 50M | 17,690 | 50,000,000 Γ· 2,772 | -| 150M | 53,071 | 150,000,000 Γ· 2,772 | - -### For BALANCE + EXTCODECOPY: -| Gas Limit | Contracts Needed | Calculation | -| --------- | ---------------- | ------------------- | -| 1M | 352 | 1,000,000 Γ· 2,775 | -| 5M | 1,768 | 5,000,000 Γ· 2,775 | -| 50M | 17,684 | 50,000,000 Γ· 2,775 | -| 150M | 53,053 | 150,000,000 Γ· 2,775 | - -The CREATE2 address generation adds ~48 gas per contract but eliminates memory limitations in test framework. - -## Quick Start: 150M Gas Attack - -### 1. Deploy CREATE2 Factory with Initcode Template - -```bash -# Deploy the factory and initcode template (one-time setup) -python3 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py - -# Output will show: -# Factory deployed at: 0x... <-- Save this address -# Init code hash: 0x... <-- Save this hash -``` - -### 2. Deploy Contracts - -Deploy contracts using the factory. Each contract will be unique due to ADDRESS-based randomness in the initcode. - -#### Calculate Contracts Needed - -Before running the deployment, calculate the number of contracts needed: -- For 150M gas BALANCE+EXTCODESIZE: 53,071 contracts -- For 150M gas BALANCE+EXTCODECOPY: 53,053 contracts - -_Deploy enough contracts to cover the max gas you plan to use in your tests/benchmarks._ - -#### Running the Deployment - -```bash -# Deploy contracts for 150M gas attack -python3 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py \ - --deploy-contracts 53100 - -# For smaller tests (e.g., 1M gas) -python3 tests/benchmark/bloatnet/deploy_create2_factory_refactored.py \ - --deploy-contracts 370 -``` - -#### Deployment Output - -After successful deployment, the script will display: - -``` -βœ… Successfully deployed 53100 contracts -NUM_DEPLOYED_CONTRACTS = 53100 -``` - -### 3. Update Test Configuration - -Edit `tests/benchmark/bloatnet/test_bloatnet.py` and update with values from deployment: - -```python -FACTORY_ADDRESS = Address("0x...") # From step 1 output -INIT_CODE_HASH = bytes.fromhex("...") # From step 1 output -NUM_DEPLOYED_CONTRACTS = 53100 # From step 2 output -``` - -### 4. Run Benchmark Tests - -#### Generate Test Fixtures -```bash -# Run with specific gas values (in millions) -uv run fill --fork=Prague --gas-benchmark-values=150 \ - tests/benchmark/bloatnet/test_bloatnet.py --clean - -# Multiple gas values -uv run fill --fork=Prague --gas-benchmark-values=1,5,50,150 \ - tests/benchmark/bloatnet/test_bloatnet.py -``` - -#### Execute Against Live Client -```bash -# Start a test node (e.g., Geth) -geth --dev --http --http.api eth,web3,net,debug - -# Run tests -uv run execute remote --rpc-endpoint http://127.0.0.1:8545 \ - --rpc-chain-id 1337 --rpc-seed-key 0x0000000000000000000000000000000000000000000000000000000000000001 \ - tests/benchmark/bloatnet/test_bloatnet.py \ - --fork=Prague --gas-benchmark-values=150 -v -``` - -#### With EVM Traces for Analysis -```bash -uv run fill --fork=Prague --gas-benchmark-values=150 \ - --evm-dump-dir=traces/ --traces \ - tests/benchmark/bloatnet/test_bloatnet.py - -# Analyze opcodes executed -jq -r '.opName' traces/**/*.jsonl | sort | uniq -c -``` diff --git a/tests/benchmark/bloatnet/__init__.py b/tests/benchmark/bloatnet/__init__.py new file mode 100644 index 00000000000..0f0656c8326 --- /dev/null +++ b/tests/benchmark/bloatnet/__init__.py @@ -0,0 +1 @@ +"""BloatNet benchmark tests for Ethereum execution spec tests.""" diff --git a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py b/tests/benchmark/bloatnet/deploy_bloatnet_simple.py deleted file mode 100644 index cd1daa07cf6..00000000000 --- a/tests/benchmark/bloatnet/deploy_bloatnet_simple.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -""" -CREATE2 deployment script for bloatnet benchmark contracts. -Uses a factory pattern to deploy contracts at deterministic addresses. - -Based on the pattern from EIP-7997, this deploys contracts using CREATE2 -so they can be accessed from any account and reused across tests. -""" - -import argparse -import os -import subprocess -import sys -from typing import Dict, List, Optional, Tuple - -from eth_utils import keccak -from web3 import Web3 - - -class ContractType: - """Define contract types for different benchmarks.""" - - MAX_SIZE_24KB = "max_size_24kb" - SLOAD_HEAVY = "sload_heavy" - STORAGE_HEAVY = "storage_heavy" - CUSTOM = "custom" - - -CONTRACT_DESCRIPTIONS = { - ContractType.MAX_SIZE_24KB: "24KB contracts filled with unique bytecode (standard bloatnet)", - ContractType.SLOAD_HEAVY: "Contracts optimized for SLOAD benchmarking", - ContractType.STORAGE_HEAVY: "Contracts with pre-initialized storage", - ContractType.CUSTOM: "Custom bytecode (provide your own)", -} - - -def generate_max_size_bytecode(salt: int = 0, max_code_size: int = 24576) -> Tuple[bytes, bytes]: - """ - Generate max-size contract bytecode for standard bloatnet tests. - - Args: - salt: Unique salt for generating unique bytecode - max_code_size: Maximum contract size (default 24576 bytes for mainnet) - - """ - # Init code copies runtime bytecode to memory and returns it - init_code = bytearray() - - # Init code: PUSH2 size, PUSH1 offset, PUSH1 dest, CODECOPY, PUSH2 size, PUSH1 0, RETURN - bytecode_size = max_code_size - init_size = 13 # Size of init code instructions - - # PUSH2 bytecode_size, PUSH1 init_size, PUSH1 0, CODECOPY - init_code.extend( - [ - 0x61, - (bytecode_size >> 8) & 0xFF, - bytecode_size & 0xFF, # PUSH2 bytecode_size - 0x60, - init_size, # PUSH1 init_size (offset where runtime code starts) - 0x60, - 0x00, # PUSH1 0 (dest in memory) - 0x39, # CODECOPY - ] - ) - - # PUSH2 bytecode_size, PUSH1 0, RETURN - init_code.extend( - [ - 0x61, - (bytecode_size >> 8) & 0xFF, - bytecode_size & 0xFF, # PUSH2 bytecode_size - 0x60, - 0x00, # PUSH1 0 (offset in memory) - 0xF3, # RETURN - ] - ) - - # Generate unique 24KB runtime bytecode to prevent deduplication - runtime_bytecode = bytearray([0x00]) # Start with STOP - - # Fill with unique pattern - pattern_count = 0 - while len(runtime_bytecode) < bytecode_size - 100: - # Use a simple pattern that's still unique per contract - unique_value = keccak(f"bloatnet_{salt}_{pattern_count}".encode()) - runtime_bytecode.append(0x7F) # PUSH32 - runtime_bytecode.extend(unique_value[:31]) # Use 31 bytes of hash - runtime_bytecode.append(0x50) # POP - pattern_count += 1 - - # Fill rest with JUMPDEST - while len(runtime_bytecode) < bytecode_size: - runtime_bytecode.append(0x5B) - - full_init_code = bytes(init_code) + bytes(runtime_bytecode) - return full_init_code, keccak(full_init_code) - - -def generate_sload_heavy_bytecode(salt: int = 0) -> Tuple[bytes, bytes]: - """Generate contracts optimized for SLOAD benchmarking.""" - # Runtime bytecode that performs many SLOAD operations - runtime_bytecode = bytearray() - - # Store some values during deployment - for i in range(10): - # PUSH1 value, PUSH1 key, SSTORE - runtime_bytecode.extend([0x60, i * 2, 0x60, i, 0x55]) - - # Main runtime: series of SLOAD operations - for i in range(100): - # PUSH1 key, SLOAD, POP - runtime_bytecode.extend([0x60, i % 10, 0x54, 0x50]) - - # Final STOP - runtime_bytecode.append(0x00) - - # Create init code that deploys this runtime - runtime_size = len(runtime_bytecode) - init_size = 13 - - init_code = bytearray() - init_code.extend( - [ - 0x61, - (runtime_size >> 8) & 0xFF, - runtime_size & 0xFF, # PUSH2 runtime_size - 0x60, - init_size, # PUSH1 init_size - 0x60, - 0x00, # PUSH1 0 - 0x39, # CODECOPY - 0x61, - (runtime_size >> 8) & 0xFF, - runtime_size & 0xFF, # PUSH2 runtime_size - 0x60, - 0x00, # PUSH1 0 - 0xF3, # RETURN - ] - ) - - full_init_code = bytes(init_code) + bytes(runtime_bytecode) - return full_init_code, keccak(full_init_code) - - -def select_contract_type() -> str: - """Interactive contract type selection.""" - print("\n=== Contract Type Selection ===") - print("Select the type of contracts to deploy:\n") - - options = list(CONTRACT_DESCRIPTIONS.keys()) - for i, (key, desc) in enumerate(CONTRACT_DESCRIPTIONS.items(), 1): - print(f"{i}. {key}: {desc}") - - while True: - try: - choice = input(f"\nEnter choice (1-{len(options)}): ").strip() - idx = int(choice) - 1 - if 0 <= idx < len(options): - selected = options[idx] - print(f"\nSelected: {selected}") - return selected - else: - print(f"Please enter a number between 1 and {len(options)}") - except (ValueError, KeyboardInterrupt): - print("\nExiting...") - sys.exit(0) - - -def get_bytecode_generator(contract_type: str, max_code_size: int): - """ - Get the appropriate bytecode generator for the contract type. - - Args: - contract_type: Type of contract to generate - max_code_size: Maximum contract size in bytes - - """ - if contract_type == ContractType.MAX_SIZE_24KB: - return lambda salt: generate_max_size_bytecode(salt, max_code_size) - elif contract_type == ContractType.SLOAD_HEAVY: - return lambda salt: generate_sload_heavy_bytecode(salt) - else: - print(f"Error: No generator implemented for {contract_type}") - if contract_type == ContractType.CUSTOM: - print("Custom bytecode deployment not yet implemented") - sys.exit(1) - - -def deploy_factory(rpc_url: str) -> str: - """Deploy CREATE2 factory if needed.""" - print("\nDeploying CREATE2 factory...") - - try: - script_dir = os.path.dirname(os.path.abspath(__file__)) - factory_script = os.path.join(script_dir, "deploy_create2_factory.py") - - result = subprocess.run( - [sys.executable, factory_script, "--rpc-url", rpc_url], - capture_output=True, - text=True, - ) - - if result.returncode != 0: - print("Failed to deploy factory:") - print(result.stderr) - sys.exit(1) - - # Extract factory address from output - for line in result.stdout.split("\n"): - if "Factory address:" in line: - factory_address = line.split(":")[1].strip() - print(f"Factory deployed at: {factory_address}") - return factory_address - - print("Could not extract factory address from deployment output") - sys.exit(1) - - except Exception as e: - print(f"Error deploying factory: {e}") - sys.exit(1) - - -def deploy_contracts( - rpc_url: str, - num_contracts: int, - contract_type: str, - factory_address: Optional[str] = None, - max_code_size: int = 24576, -): - """Deploy contracts using CREATE2 factory pattern.""" - # Connect to Geth - w3 = Web3(Web3.HTTPProvider(rpc_url)) - if not w3.is_connected(): - print(f"Failed to connect to {rpc_url}") - sys.exit(1) - - test_account = w3.eth.accounts[0] - print(f"Using test account: {test_account}") - print(f"Balance: {w3.eth.get_balance(test_account) / 10**18:.4f} ETH") - - # Check/deploy factory - if factory_address: - factory_code = w3.eth.get_code(Web3.to_checksum_address(factory_address)) - if len(factory_code) > 0: - print(f"\nUsing existing CREATE2 factory at: {factory_address}") - else: - print(f"\nNo factory found at {factory_address}") - factory_address = deploy_factory(rpc_url) - else: - factory_address = deploy_factory(rpc_url) - - # Get bytecode generator - bytecode_generator = get_bytecode_generator(contract_type, max_code_size) - - # Generate sample to show info - sample_init_code, sample_hash = bytecode_generator(0) - - print(f"\nContract Type: {contract_type}") - print(f"Init code size: {len(sample_init_code)} bytes") - print(f"Sample init code hash: 0x{sample_hash.hex()}") - - confirm = input(f"\nProceed with deploying {num_contracts} {contract_type} contracts? (y/n): ") - if confirm.lower() != "y": - print("Deployment cancelled") - sys.exit(0) - - # Deploy contracts - print(f"\nDeploying {num_contracts} {contract_type} contracts using CREATE2...") - - deployed = [] - init_code_hashes: Dict[str, List[int]] = {} # Track different init code hashes if they vary - - for salt in range(num_contracts): - if salt % 100 == 0: - print(f"Progress: {salt}/{num_contracts}") - - # Generate bytecode for this specific salt - full_init_code, init_code_hash = bytecode_generator(salt) - - # Track unique init code hashes - hash_hex = init_code_hash.hex() - if hash_hex not in init_code_hashes: - init_code_hashes[hash_hex] = [] - init_code_hashes[hash_hex].append(salt) - - # Factory expects: salt (32 bytes) + bytecode - call_data = salt.to_bytes(32, "big") + full_init_code - - try: - tx_hash = w3.eth.send_transaction( - { - "from": test_account, - "to": factory_address, - "data": bytes.fromhex(call_data.hex()), - "gas": 10000000, - } - ) - - receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=10) - - if receipt["status"] == 1: - # Calculate CREATE2 address - create2_input = ( - b"\xff" - + bytes.fromhex(factory_address[2:]) - + salt.to_bytes(32, "big") - + init_code_hash - ) - contract_address = "0x" + keccak(create2_input)[-20:].hex() - deployed.append(contract_address) - - if (salt + 1) % 100 == 0 or salt == num_contracts - 1: - print(f" [{salt + 1}/{num_contracts}] Deployed at {contract_address}") - else: - print(f" [{salt + 1}/{num_contracts}] Failed") - - except Exception as e: - print(f" [{salt + 1}/{num_contracts}] Error: {e}") - break - - print(f"\nDeployed {len(deployed)} contracts") - - if deployed: - print("\nContract addresses:") - print(f"First: {deployed[0]}") - print(f"Last: {deployed[-1]}") - - print(f"\n=== Configuration for {contract_type} tests ===") - print(f'CONTRACT_TYPE = "{contract_type}"') - print(f'FACTORY_ADDRESS = Address("{factory_address}")') - - if len(init_code_hashes) == 1: - # All contracts have the same init code hash - hash_hex = list(init_code_hashes.keys())[0] - print(f'INIT_CODE_HASH = bytes.fromhex("{hash_hex}")') - else: - # Multiple init code hashes (rare but possible) - print("# Multiple init code hashes detected:") - for hash_hex, salts in init_code_hashes.items(): - print(f"# Hash {hash_hex[:8]}... used for salts: {salts[:5]}...") - - print(f"NUM_DEPLOYED_CONTRACTS = {len(deployed)}") - - # Save configuration to file - config_file = f"bloatnet_config_{contract_type}.txt" - with open(config_file, "w") as f: - f.write(f"# Configuration for {contract_type} benchmarks\n") - f.write(f'CONTRACT_TYPE = "{contract_type}"\n') - f.write(f'FACTORY_ADDRESS = Address("{factory_address}")\n') - if len(init_code_hashes) == 1: - hash_hex = list(init_code_hashes.keys())[0] - f.write(f'INIT_CODE_HASH = bytes.fromhex("{hash_hex}")\n') - f.write(f"NUM_DEPLOYED_CONTRACTS = {len(deployed)}\n") - - print(f"\nConfiguration saved to: {config_file}") - - -def main(): - """Execute the deployment script.""" - parser = argparse.ArgumentParser(description="Deploy bloatnet contracts using CREATE2") - parser.add_argument( - "--num-contracts", - type=int, - default=100, - help="Number of contracts to deploy", - ) - parser.add_argument( - "--rpc-url", - default="http://127.0.0.1:8545", - help="RPC URL", - ) - parser.add_argument( - "--factory-address", - default=None, - help="CREATE2 factory address (deploys new one if not provided)", - ) - parser.add_argument( - "--max-code-size", - type=int, - default=24576, - help="Maximum contract code size in bytes (default: 24576 for mainnet/Prague fork)", - ) - - args = parser.parse_args() - - # Always run in interactive mode - user selects contract type - contract_type = select_contract_type() - - deploy_contracts( - args.rpc_url, - args.num_contracts, - contract_type, - args.factory_address, - args.max_code_size, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py b/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py deleted file mode 100644 index 94d75694421..00000000000 --- a/tests/benchmark/bloatnet/deploy_create2_factory_refactored.py +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env python3 -""" -Deploy a CREATE2 factory for on-the-fly contract address generation in BloatNet tests. - -This factory uses a constant initcode that generates unique 24KB contracts by: -1. Using ADDRESS opcode for pseudo-randomness (within the factory's context) -2. Expanding randomness with SHA3 and XOR operations -3. Creating max-size contracts with deterministic CREATE2 addresses -""" - -import argparse -import sys -from pathlib import Path - -# Add parent directories to path to import EEST tools -sys.path.insert(0, str(Path(__file__).parents[3])) - -try: - from eth_utils import keccak - from web3 import Web3 - - from ethereum_test_vm import Opcodes as Op -except ImportError as e: - print(f"Error: Missing dependencies - {e}") - print("This refactored version requires the EEST framework.") - print("Run: uv sync --all-extras") - sys.exit(1) - -# XOR table for pseudo-random bytecode generation (reused from test_worst_bytecode.py) -XOR_TABLE_SIZE = 256 -XOR_TABLE = [keccak(i.to_bytes(32, "big")) for i in range(XOR_TABLE_SIZE)] -MAX_CONTRACT_SIZE = 24576 # 24KB - - -def build_initcode() -> bytes: - """Build the initcode that generates unique 24KB contracts using ADDRESS for randomness.""" - from ethereum_test_tools import While - - # This initcode follows the pattern from test_worst_bytecode.py: - # 1. Uses ADDRESS as initial seed for pseudo-randomness (creates uniqueness per deployment) - # 2. Expands to 24KB using SHA3 and XOR operations - # 3. Sets first byte to STOP for quick CALL returns - initcode = ( - # Store ADDRESS as initial seed - THIS IS CRITICAL FOR UNIQUENESS - Op.MSTORE(0, Op.ADDRESS) - # Loop to expand bytecode using SHA3 and XOR operations - + While( - body=( - Op.SHA3(Op.SUB(Op.MSIZE, 32), 32) - # Use XOR table to expand without excessive SHA3 calls - + sum( - (Op.PUSH32[xor_value] + Op.XOR + Op.DUP1 + Op.MSIZE + Op.MSTORE) - for xor_value in XOR_TABLE - ) - + Op.POP - ), - condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE), - ) - # Set first byte to STOP for efficient CALL handling - + Op.MSTORE8(0, 0x00) - # Return the full contract - + Op.RETURN(0, MAX_CONTRACT_SIZE) - ) - return bytes(initcode) - - -def deploy_factory_and_initcode(rpc_url: str): - """Deploy the initcode template and factory that uses it.""" - # Connect to Geth - w3 = Web3(Web3.HTTPProvider(rpc_url)) - if not w3.is_connected(): - print(f"Failed to connect to {rpc_url}") - return None, None - - test_account = w3.eth.accounts[0] - print(f"Using test account: {test_account}") - - # Build the initcode - initcode = build_initcode() - print(f"\nInitcode size: {len(initcode)} bytes") - print(f"Initcode (first 100 bytes): 0x{initcode[:100].hex()}...") - - # Deploy the initcode as a contract that the factory can copy from - print("\nDeploying initcode template contract...") - tx_hash = w3.eth.send_transaction({"from": test_account, "data": initcode, "gas": 10000000}) - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - - if receipt["status"] != 1: - print("Failed to deploy initcode template") - return None, None - - initcode_address = receipt["contractAddress"] - print(f"βœ… Initcode template deployed at: {initcode_address}") - - # Build the factory contract following the pattern from test_worst_bytecode.py - # The factory: - # 1. Copies the initcode from the template contract - # 2. Uses incrementing salt from storage for CREATE2 - # 3. Returns the created contract address - factory_code = ( - # Copy initcode from template to memory - Op.EXTCODECOPY( - address=initcode_address, - dest_offset=0, - offset=0, - size=Op.EXTCODESIZE(initcode_address), - ) - # Store the result of CREATE2 - + Op.MSTORE( - 0, - Op.CREATE2( - value=0, - offset=0, - size=Op.EXTCODESIZE(initcode_address), - salt=Op.SLOAD(0), - ), - ) - # Increment salt for next call - + Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) - # Return created address - + Op.RETURN(0, 32) - ) - - factory_bytecode = bytes(factory_code) - print(f"\nFactory bytecode size: {len(factory_bytecode)} bytes") - print(f"Factory bytecode: 0x{factory_bytecode.hex()}") - - # Deploy the factory - print("\nDeploying CREATE2 factory...") - tx_hash = w3.eth.send_transaction( - {"from": test_account, "data": factory_bytecode, "gas": 3000000} - ) - receipt = w3.eth.wait_for_transaction_receipt(tx_hash) - - if receipt["status"] != 1: - print("Failed to deploy factory") - return None, None - - factory_address = receipt["contractAddress"] - print(f"βœ… Factory deployed at: {factory_address}") - - # Calculate init code hash for CREATE2 address calculation - init_code_hash = keccak(initcode) - print(f"\nInit code hash: 0x{init_code_hash.hex()}") - - return factory_address, init_code_hash.hex() - - -def deploy_contracts(rpc_url: str, factory_address: str, num_contracts: int): - """Deploy multiple contracts using the factory.""" - w3 = Web3(Web3.HTTPProvider(rpc_url)) - if not w3.is_connected(): - print(f"Failed to connect to {rpc_url}") - return False - - test_account = w3.eth.accounts[0] - print(f"\nDeploying {num_contracts} contracts via factory...") - - # Batch size for deployments - batch_size = 100 - deployed_count = 0 - - for batch_start in range(0, num_contracts, batch_size): - batch_end = min(batch_start + batch_size, num_contracts) - current_batch = batch_end - batch_start - - batch_num = batch_start // batch_size + 1 - print(f"Deploying batch {batch_num}: contracts {batch_start}-{batch_end - 1}...") - - for i in range(current_batch): - try: - tx_hash = w3.eth.send_transaction( - {"from": test_account, "to": factory_address, "gas": 10000000} - ) - receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60) - if receipt["status"] == 1: - deployed_count += 1 - else: - print(f" ⚠️ Failed to deploy contract {batch_start + i}") - except Exception as e: - print(f" ⚠️ Error deploying contract {batch_start + i}: {e}") - - print(f" βœ… Deployed {deployed_count}/{batch_end} contracts") - - return deployed_count == num_contracts - - -def main(): - """Execute the factory deployment script.""" - parser = argparse.ArgumentParser(description="Deploy CREATE2 factory for BloatNet tests") - parser.add_argument( - "--rpc-url", - default="http://127.0.0.1:8545", - help="RPC URL (default: http://127.0.0.1:8545)", - ) - parser.add_argument( - "--deploy-contracts", type=int, metavar="N", help="Deploy N contracts using the factory" - ) - - args = parser.parse_args() - - # Deploy factory and initcode template - factory_address, init_code_hash = deploy_factory_and_initcode(args.rpc_url) - - if not factory_address: - print("\n❌ Failed to deploy factory") - sys.exit(1) - - print("\n" + "=" * 60) - print("Factory deployed successfully!") - print(f"Factory address: {factory_address}") - print(f"Init code hash: 0x{init_code_hash}") - print("\nAdd this to your test configuration:") - print(f'FACTORY_ADDRESS = Address("{factory_address}")') - print(f'INIT_CODE_HASH = bytes.fromhex("{init_code_hash}")') - - # Deploy contracts if requested - if args.deploy_contracts: - success = deploy_contracts(args.rpc_url, factory_address, args.deploy_contracts) - if success: - print(f"\nβœ… Successfully deployed {args.deploy_contracts} contracts") - print(f"NUM_DEPLOYED_CONTRACTS = {args.deploy_contracts}") - else: - print("\n⚠️ Some contracts failed to deploy") - - print("=" * 60) - - -if __name__ == "__main__": - main() diff --git a/tests/benchmark/bloatnet/test_bloatnet.py b/tests/benchmark/bloatnet/test_mutiopcode.py similarity index 100% rename from tests/benchmark/bloatnet/test_bloatnet.py rename to tests/benchmark/bloatnet/test_mutiopcode.py From 93a3e0653e7b3e3cd5d0b29588601c82b92ed6b9 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 23 Sep 2025 18:22:32 +0200 Subject: [PATCH 39/54] refactor(benchmark): remove hardcoded parameters storing inside factory stub - Fixed offset at which we COPYCODE - Removed hardcoded values and added comments for clarity on factory storage layout and contract generation. --- tests/benchmark/bloatnet/test_mutiopcode.py | 202 ++++++++++++++------ 1 file changed, 148 insertions(+), 54 deletions(-) diff --git a/tests/benchmark/bloatnet/test_mutiopcode.py b/tests/benchmark/bloatnet/test_mutiopcode.py index 2aec8689876..d7d0c5a8bb2 100644 --- a/tests/benchmark/bloatnet/test_mutiopcode.py +++ b/tests/benchmark/bloatnet/test_mutiopcode.py @@ -5,33 +5,33 @@ processing are focusing specifically on state-related operations. """ -import warnings - import pytest from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, - Address, Alloc, Block, BlockchainTestFiller, Transaction, While, ) -from ethereum_test_vm import Opcodes as Op +from ethereum_test_vm import Bytecode, Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" -# Configuration for CREATE2 factory - UPDATE THESE after running deploy script -# These values come from deploy_create2_factory_refactored.py output -FACTORY_ADDRESS = Address("0x847a04f0a1FfC4E68CC80e7D870Eb4eC51235CE8") # UPDATE THIS -INIT_CODE_HASH = bytes.fromhex( - "9e1a230bdc29e66a6027083ec52c6724e7e6cac4a8e59c1c9a852c0a1e954b45" -) # UPDATE THIS -NUM_DEPLOYED_CONTRACTS = 370 # UPDATE THIS - number of contracts deployed via factory +# CREATE2 FACTORY: +# - Pre-deployed contract that creates 24KB contracts via CREATE2 +# - Uses counter (slot 0) as salt for deterministic addresses +# - Stores init code hash (slot 1) for CREATE2 address calculation +# - Each call to factory deploys one contract and increments counter +# - Address calculation: keccak256(0xFF + factory_addr + salt + init_code_hash)[12:] +# - Storage layout: +# - Slot 0: Number of deployed contracts (counter) +# - Slot 1: Init code hash (32 bytes) - hash of the initcode used for CREATE2 +# - The factory MUST store the correct init code hash that was used to deploy contracts\ @pytest.mark.valid_from("Prague") @@ -66,37 +66,78 @@ def test_bloatnet_balance_extcodesize( + 20 # Overhead for memory operations and loop control ) - # Calculate how many contracts to access + # Calculate how many contracts to access based on available gas available_gas = gas_benchmark_value - intrinsic_gas - 1000 # Reserve for cleanup contracts_needed = int(available_gas // cost_per_contract) - # Limit to actually deployed contracts - num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) + # Deploy factory using stub contract - NO HARDCODED VALUES + # The stub "bloatnet_factory" must be provided via --address-stubs flag + # The factory at that address MUST have: + # - Slot 0: Number of deployed contracts + # - Slot 1: Init code hash for CREATE2 address calculation + factory_address = pre.deploy_contract( + code=Bytecode(), # Required parameter, but will be ignored for stubs + stub="bloatnet_factory", + ) - if contracts_needed > NUM_DEPLOYED_CONTRACTS: - warnings.warn( - f"Test needs {contracts_needed} contracts for " - f"{gas_benchmark_value / 1_000_000:.1f}M gas, " - f"but only {NUM_DEPLOYED_CONTRACTS} are deployed. " - f"Deploy {contracts_needed} contracts for full test coverage.", - stacklevel=2, - ) + # Log test requirements - actual deployed count will be read from factory storage + print( + f"Test needs {contracts_needed} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas. " + f"Factory storage will be checked during execution." + ) - # Generate attack contract with on-the-fly CREATE2 address calculation + # Build attack contract that reads config from factory and performs attack attack_code = ( + # Read number of deployed contracts from factory storage slot 0 + Op.PUSH1(0) # Storage slot 0 + + Op.PUSH20(factory_address) # Factory address + + Op.SLOAD # Load num_deployed_contracts + + Op.DUP1 # Keep a copy for later use + + # Read init code hash from factory storage slot 1 + + Op.PUSH1(1) # Storage slot 1 + + Op.PUSH20(factory_address) # Factory address + + Op.SLOAD # Load init_code_hash + # Setup memory for CREATE2 address generation # Memory layout: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) - Op.MSTORE(0, FACTORY_ADDRESS) - + Op.MSTORE8(32 - 20 - 1, 0xFF) # Prefix for CREATE2 - + Op.MSTORE(32, 0) # Initial salt (start from 0) - + Op.MSTORE(64, INIT_CODE_HASH) # Init code hash - # Limit counter for number of contracts to access - + Op.PUSH2(num_contracts) - # Main attack loop + + Op.PUSH20(factory_address) + + Op.PUSH1(0) + + Op.MSTORE # Store factory address at memory position 0 + + Op.PUSH1(0xFF) + + Op.PUSH1(11) # Position for 0xFF prefix (32 - 20 - 1) + + Op.MSTORE8 # Store 0xFF prefix + + + Op.PUSH1(0) # Initial salt value + + Op.PUSH1(32) + + Op.MSTORE # Store salt at position 32 + + # Stack now has: [num_contracts, init_code_hash] + + Op.PUSH1(64) + + Op.MSTORE # Store init_code_hash at position 64 + + # Stack now has: [num_contracts] + # Calculate how many contracts we can actually access with available gas + + Op.GAS # Get current gas + + Op.PUSH2(cost_per_contract) # Gas per contract access + + Op.DIV # Calculate max possible contracts + + Op.DUP2 # Get num_deployed_contracts + + Op.GT # Check if we can access all deployed contracts + + Op.PUSH1(0) # Jump destination for no limit + + Op.JUMPI + + Op.POP # Remove num_deployed_contracts + + Op.GAS + + Op.PUSH2(cost_per_contract) + + Op.DIV # Use gas-limited count + + Op.JUMPDEST + # Main attack loop - stack has [num_contracts_to_access] + While( body=( # Generate CREATE2 address: keccak256(0xFF + factory + salt + init_code_hash) - Op.SHA3(32 - 20 - 1, 85) # Hash 85 bytes starting from 0xFF + Op.PUSH1(85) # Size to hash (1 + 20 + 32 + 32) + + Op.PUSH1(11) # Start position (0xFF prefix) + + Op.SHA3 # Generate CREATE2 address # The address is now on the stack + Op.DUP1 # Duplicate for EXTCODESIZE + Op.BALANCE # Cold access @@ -104,7 +145,12 @@ def test_bloatnet_balance_extcodesize( + Op.EXTCODESIZE # Warm access + Op.POP # Increment salt for next iteration - + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + + Op.PUSH1(32) # Salt position + + Op.MLOAD # Load current salt + + Op.PUSH1(1) + + Op.ADD # Increment + + Op.PUSH1(32) + + Op.MSTORE # Store back ), # Continue while we haven't reached the limit condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, @@ -173,32 +219,74 @@ def test_bloatnet_balance_extcodecopy( available_gas = gas_benchmark_value - intrinsic_gas - 1000 contracts_needed = int(available_gas // cost_per_contract) - # Limit to actually deployed contracts - num_contracts = min(contracts_needed, NUM_DEPLOYED_CONTRACTS) + # Deploy factory using stub contract - NO HARDCODED VALUES + # The stub "bloatnet_factory" must be provided via --address-stubs flag + # The factory at that address MUST have: + # - Slot 0: Number of deployed contracts + # - Slot 1: Init code hash for CREATE2 address calculation + factory_address = pre.deploy_contract( + code=Bytecode(), # Required parameter, but will be ignored for stubs + stub="bloatnet_factory", + ) - if contracts_needed > NUM_DEPLOYED_CONTRACTS: - warnings.warn( - f"Test needs {contracts_needed} contracts for " - f"{gas_benchmark_value / 1_000_000:.1f}M gas, " - f"but only {NUM_DEPLOYED_CONTRACTS} are deployed. " - f"Deploy {contracts_needed} contracts for full test coverage.", - stacklevel=2, - ) + # Log test requirements - actual deployed count will be read from factory storage + print( + f"Test needs {contracts_needed} contracts for " + f"{gas_benchmark_value / 1_000_000:.1f}M gas. " + f"Factory storage will be checked during execution." + ) - # Generate attack contract with on-the-fly CREATE2 address calculation + # Build attack contract that reads config from factory and performs attack attack_code = ( + # Read number of deployed contracts from factory storage slot 0 + Op.PUSH1(0) # Storage slot 0 + + Op.PUSH20(factory_address) # Factory address + + Op.SLOAD # Load num_deployed_contracts + + Op.DUP1 # Keep a copy for later use + + # Read init code hash from factory storage slot 1 + + Op.PUSH1(1) # Storage slot 1 + + Op.PUSH20(factory_address) # Factory address + + Op.SLOAD # Load init_code_hash + # Setup memory for CREATE2 address generation - Op.MSTORE(0, FACTORY_ADDRESS) - + Op.MSTORE8(32 - 20 - 1, 0xFF) - + Op.MSTORE(32, 0) # Initial salt (start from 0) - + Op.MSTORE(64, INIT_CODE_HASH) - # Counter for number of contracts - + Op.PUSH2(num_contracts) - # Main attack loop + # Memory layout: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + + Op.PUSH20(factory_address) + + Op.PUSH1(0) + + Op.MSTORE # Store factory address at memory position 0 + + Op.PUSH1(0xFF) + + Op.PUSH1(11) # Position for 0xFF prefix (32 - 20 - 1) + + Op.MSTORE8 # Store 0xFF prefix + + + Op.PUSH1(0) # Initial salt value + + Op.PUSH1(32) + + Op.MSTORE # Store salt at position 32 + + # Stack now has: [num_contracts, init_code_hash] + + Op.PUSH1(64) + + Op.MSTORE # Store init_code_hash at position 64 + + # Stack now has: [num_contracts] + # Calculate how many contracts we can actually access with available gas + + Op.GAS # Get current gas + + Op.PUSH2(cost_per_contract) # Gas per contract access with EXTCODECOPY + + Op.DIV # Calculate max possible contracts + + Op.DUP2 # Get num_deployed_contracts + + Op.GT # Check if we can access all deployed contracts + + Op.PUSH1(0) # Jump destination for no limit + + Op.JUMPI + + Op.POP # Remove num_deployed_contracts + + Op.GAS + + Op.PUSH2(cost_per_contract) + + Op.DIV # Use gas-limited count + + Op.JUMPDEST + # Main attack loop - stack has [num_contracts_to_access] + While( body=( # Generate CREATE2 address - Op.SHA3(32 - 20 - 1, 85) + Op.PUSH1(85) # Size to hash (1 + 20 + 32 + 32) + + Op.PUSH1(11) # Start position (0xFF prefix) + + Op.SHA3 # Generate CREATE2 address # The address is now on the stack + Op.DUP1 # Duplicate for later operations + Op.BALANCE # Cold access @@ -208,14 +296,20 @@ def test_bloatnet_balance_extcodecopy( + Op.PUSH1(1) # size (1 byte) + Op.PUSH2(max_contract_size - 1) # code offset (last byte) # Use salt as memory offset to avoid overlap - + Op.MLOAD(32) # Get current salt + + Op.PUSH1(32) # Salt position + + Op.MLOAD # Get current salt + Op.PUSH2(96) # Base memory offset + Op.ADD # Unique memory position + Op.DUP4 # address (duplicated earlier) + Op.EXTCODECOPY + Op.POP # Clean up address - # Increment salt - + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) + # Increment salt for next iteration + + Op.PUSH1(32) # Salt position + + Op.MLOAD # Load current salt + + Op.PUSH1(1) + + Op.ADD # Increment + + Op.PUSH1(32) + + Op.MSTORE # Store back ), # Continue while counter > 0 condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, From e0742a9511a5a6f134fb97cbe431a3c48e32f74a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 23 Sep 2025 18:26:39 +0200 Subject: [PATCH 40/54] chore: update pyproject.toml configuration --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e665dbdecc..a51d95d6c3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,10 +160,6 @@ minversion = "7.0" python_files = ["test_*.py"] testpaths = ["src"] addopts = [ - "-p", - "pytester", - "-p", - "pytest_plugins.eels_resolver", "--ignore=src/pytest_plugins/consume/test_cache.py", "--ignore=src/pytest_plugins/consume/direct/", "--ignore=src/pytest_plugins/consume/simulators/", From d3be62727037225ac92d126bed140fa9016cf6ef Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 23 Sep 2025 18:28:13 +0200 Subject: [PATCH 41/54] refactor: rename test_mutiopcode.py to test_muti_opcode.py for consistency --- .../bloatnet/{test_mutiopcode.py => test_muti_opcode.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/benchmark/bloatnet/{test_mutiopcode.py => test_muti_opcode.py} (100%) diff --git a/tests/benchmark/bloatnet/test_mutiopcode.py b/tests/benchmark/bloatnet/test_muti_opcode.py similarity index 100% rename from tests/benchmark/bloatnet/test_mutiopcode.py rename to tests/benchmark/bloatnet/test_muti_opcode.py From 164ad8bdb91e57e31e665529fceb5920216095d2 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 23 Sep 2025 23:13:17 +0200 Subject: [PATCH 42/54] fix: correct import sorting in test_muti_opcode.py to fix CI lint error --- tests/benchmark/bloatnet/test_muti_opcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/benchmark/bloatnet/test_muti_opcode.py b/tests/benchmark/bloatnet/test_muti_opcode.py index d7d0c5a8bb2..0b13fa9014f 100644 --- a/tests/benchmark/bloatnet/test_muti_opcode.py +++ b/tests/benchmark/bloatnet/test_muti_opcode.py @@ -16,7 +16,8 @@ Transaction, While, ) -from ethereum_test_vm import Bytecode, Opcodes as Op +from ethereum_test_vm import Bytecode +from ethereum_test_vm import Opcodes as Op REFERENCE_SPEC_GIT_PATH = "DUMMY/bloatnet.md" REFERENCE_SPEC_VERSION = "1.0" From ee29d20a0d5c5a2f00830495d33d547696656d54 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Tue, 23 Sep 2025 23:26:46 +0200 Subject: [PATCH 43/54] fix(benchmark): rename test file to fix typo Rename test_muti_opcode.py to test_multi_opcode.py to fix filename typo --- .../bloatnet/{test_muti_opcode.py => test_multi_opcode.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/benchmark/bloatnet/{test_muti_opcode.py => test_multi_opcode.py} (100%) diff --git a/tests/benchmark/bloatnet/test_muti_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py similarity index 100% rename from tests/benchmark/bloatnet/test_muti_opcode.py rename to tests/benchmark/bloatnet/test_multi_opcode.py From 97c4efcfbc32378d6fc92deb0c389e51f1c8b6de Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 25 Sep 2025 20:52:06 +0200 Subject: [PATCH 44/54] fix(benchmark): update BloatNet tests to use factory's getConfig() method Replace direct storage access with STATICCALL to factory's getConfig() method in both test_bloatnet_balance_extcodesize and test_bloatnet_balance_extcodecopy. Changes: - Use STATICCALL to retrieve configuration from factory instead of SLOAD - Add proper error handling for failed configuration calls - Remove gas-limiting calculations, allowing tests to run until gas exhaustion - Store configuration data in memory positions 96 and 128 for cleaner access This makes the tests more robust and better aligned with the factory's public interface, avoiding direct storage access assumptions. --- tests/benchmark/bloatnet/test_multi_opcode.py | 102 ++++++++---------- 1 file changed, 44 insertions(+), 58 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 0b13fa9014f..db0b2b8839f 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -90,49 +90,42 @@ def test_bloatnet_balance_extcodesize( # Build attack contract that reads config from factory and performs attack attack_code = ( - # Read number of deployed contracts from factory storage slot 0 - Op.PUSH1(0) # Storage slot 0 - + Op.PUSH20(factory_address) # Factory address - + Op.SLOAD # Load num_deployed_contracts - + Op.DUP1 # Keep a copy for later use - - # Read init code hash from factory storage slot 1 - + Op.PUSH1(1) # Storage slot 1 - + Op.PUSH20(factory_address) # Factory address - + Op.SLOAD # Load init_code_hash - + # Call getConfig() on factory to get num_deployed_contracts and init_code_hash + # STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize) + Op.PUSH1(64) # retSize (64 bytes = 2 * 32) + + Op.PUSH1(96) # retOffset (store result at memory position 96) + + Op.PUSH1(0) # argsSize (no calldata for getConfig) + + Op.PUSH1(0) # argsOffset + + Op.PUSH20(factory_address) # factory address + + Op.GAS # Use all available gas + + Op.STATICCALL # Call getConfig() + # Check if call succeeded + + Op.ISZERO + + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) + + Op.JUMPI + # Load results from memory + # Memory[96:128] = num_deployed_contracts + # Memory[128:160] = init_code_hash + + Op.PUSH1(96) # Memory position for num_deployed_contracts + + Op.MLOAD # Load num_deployed_contracts + + Op.PUSH1(128) # Memory position for init_code_hash + + Op.MLOAD # Load init_code_hash # Setup memory for CREATE2 address generation - # Memory layout: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + # Memory layout at position 0: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + Op.PUSH20(factory_address) + Op.PUSH1(0) + Op.MSTORE # Store factory address at memory position 0 + Op.PUSH1(0xFF) + Op.PUSH1(11) # Position for 0xFF prefix (32 - 20 - 1) + Op.MSTORE8 # Store 0xFF prefix - + Op.PUSH1(0) # Initial salt value + Op.PUSH1(32) + Op.MSTORE # Store salt at position 32 - # Stack now has: [num_contracts, init_code_hash] + Op.PUSH1(64) + Op.MSTORE # Store init_code_hash at position 64 - # Stack now has: [num_contracts] - # Calculate how many contracts we can actually access with available gas - + Op.GAS # Get current gas - + Op.PUSH2(cost_per_contract) # Gas per contract access - + Op.DIV # Calculate max possible contracts - + Op.DUP2 # Get num_deployed_contracts - + Op.GT # Check if we can access all deployed contracts - + Op.PUSH1(0) # Jump destination for no limit - + Op.JUMPI - + Op.POP # Remove num_deployed_contracts - + Op.GAS - + Op.PUSH2(cost_per_contract) - + Op.DIV # Use gas-limited count - + Op.JUMPDEST - # Main attack loop - stack has [num_contracts_to_access] + # Main attack loop - iterate through all deployed contracts + While( body=( # Generate CREATE2 address: keccak256(0xFF + factory + salt + init_code_hash) @@ -239,49 +232,42 @@ def test_bloatnet_balance_extcodecopy( # Build attack contract that reads config from factory and performs attack attack_code = ( - # Read number of deployed contracts from factory storage slot 0 - Op.PUSH1(0) # Storage slot 0 - + Op.PUSH20(factory_address) # Factory address - + Op.SLOAD # Load num_deployed_contracts - + Op.DUP1 # Keep a copy for later use - - # Read init code hash from factory storage slot 1 - + Op.PUSH1(1) # Storage slot 1 - + Op.PUSH20(factory_address) # Factory address - + Op.SLOAD # Load init_code_hash - + # Call getConfig() on factory to get num_deployed_contracts and init_code_hash + # STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize) + Op.PUSH1(64) # retSize (64 bytes = 2 * 32) + + Op.PUSH1(96) # retOffset (store result at memory position 96) + + Op.PUSH1(0) # argsSize (no calldata for getConfig) + + Op.PUSH1(0) # argsOffset + + Op.PUSH20(factory_address) # factory address + + Op.GAS # Use all available gas + + Op.STATICCALL # Call getConfig() + # Check if call succeeded + + Op.ISZERO + + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) + + Op.JUMPI + # Load results from memory + # Memory[96:128] = num_deployed_contracts + # Memory[128:160] = init_code_hash + + Op.PUSH1(96) # Memory position for num_deployed_contracts + + Op.MLOAD # Load num_deployed_contracts + + Op.PUSH1(128) # Memory position for init_code_hash + + Op.MLOAD # Load init_code_hash # Setup memory for CREATE2 address generation - # Memory layout: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + # Memory layout at position 0: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + Op.PUSH20(factory_address) + Op.PUSH1(0) + Op.MSTORE # Store factory address at memory position 0 + Op.PUSH1(0xFF) + Op.PUSH1(11) # Position for 0xFF prefix (32 - 20 - 1) + Op.MSTORE8 # Store 0xFF prefix - + Op.PUSH1(0) # Initial salt value + Op.PUSH1(32) + Op.MSTORE # Store salt at position 32 - # Stack now has: [num_contracts, init_code_hash] + Op.PUSH1(64) + Op.MSTORE # Store init_code_hash at position 64 - # Stack now has: [num_contracts] - # Calculate how many contracts we can actually access with available gas - + Op.GAS # Get current gas - + Op.PUSH2(cost_per_contract) # Gas per contract access with EXTCODECOPY - + Op.DIV # Calculate max possible contracts - + Op.DUP2 # Get num_deployed_contracts - + Op.GT # Check if we can access all deployed contracts - + Op.PUSH1(0) # Jump destination for no limit - + Op.JUMPI - + Op.POP # Remove num_deployed_contracts - + Op.GAS - + Op.PUSH2(cost_per_contract) - + Op.DIV # Use gas-limited count - + Op.JUMPDEST - # Main attack loop - stack has [num_contracts_to_access] + # Main attack loop - iterate through all deployed contracts + While( body=( # Generate CREATE2 address From 4dc48760052b197963360aeb60f72636b61a7b82 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 25 Sep 2025 22:46:27 +0200 Subject: [PATCH 45/54] refactor(benchmark): enhance BloatNet test documentation and gas cost calculations --- tests/benchmark/bloatnet/test_multi_opcode.py | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index db0b2b8839f..9b791da4081 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -23,16 +23,29 @@ REFERENCE_SPEC_VERSION = "1.0" -# CREATE2 FACTORY: -# - Pre-deployed contract that creates 24KB contracts via CREATE2 -# - Uses counter (slot 0) as salt for deterministic addresses -# - Stores init code hash (slot 1) for CREATE2 address calculation -# - Each call to factory deploys one contract and increments counter -# - Address calculation: keccak256(0xFF + factory_addr + salt + init_code_hash)[12:] -# - Storage layout: -# - Slot 0: Number of deployed contracts (counter) -# - Slot 1: Init code hash (32 bytes) - hash of the initcode used for CREATE2 -# - The factory MUST store the correct init code hash that was used to deploy contracts\ +# BLOATNET ARCHITECTURE: +# +# [Initcode Contract] [Factory Contract] [24KB Contracts] +# (9.5KB) (116B) (N x 24KB each) +# β”‚ β”‚ β”‚ +# β”‚ EXTCODECOPY β”‚ CREATE2(salt++) β”‚ +# └──────────────► β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ί Contract_0 +# β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ί Contract_1 +# β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ί Contract_2 +# └──────────────────► Contract_N +# +# [Attack Contract] ──STATICCALL──► [Factory.getConfig()] +# β”‚ returns: (N, hash) +# └─► Loop(i=0 to N): +# 1. Generate CREATE2 addr: keccak256(0xFF|factory|i|hash)[12:] +# 2. BALANCE(addr) β†’ 2600 gas (cold access) +# 3. EXTCODESIZE(addr) β†’ 100 gas (warm access) +# +# HOW IT WORKS: +# 1. Factory uses EXTCODECOPY to load initcode, avoiding PC-relative jump issues +# 2. Each CREATE2 deployment produces unique 24KB bytecode (via ADDRESS opcode) +# 3. All contracts share same initcode hash for deterministic address calculation +# 4. Attack rapidly accesses all contracts, stressing client's state handling @pytest.mark.valid_from("Prague") @@ -58,13 +71,18 @@ def test_bloatnet_balance_extcodesize( # Cost per contract access with CREATE2 address generation cost_per_contract = ( - gas_costs.G_KECCAK_256 # SHA3 static cost for address generation - + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes) + gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) + + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) - + gas_costs.G_BASE # POP balance + + gas_costs.G_BASE # POP balance (2) + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100) - + gas_costs.G_BASE # POP code size - + 20 # Overhead for memory operations and loop control + + gas_costs.G_BASE # POP code size (2) + + gas_costs.G_BASE # DUP1 before BALANCE (3) + + gas_costs.G_VERYLOW * 4 # PUSH1 operations (4 * 3) + + gas_costs.G_LOW # MLOAD for salt (3) + + gas_costs.G_VERYLOW # ADD for increment (3) + + gas_costs.G_LOW # MSTORE salt back (3) + + 10 # While loop overhead ) # Calculate how many contracts to access based on available gas @@ -199,14 +217,19 @@ def test_bloatnet_balance_extcodecopy( # Cost per contract with EXTCODECOPY and CREATE2 address generation cost_per_contract = ( - gas_costs.G_KECCAK_256 # SHA3 static cost for address generation - + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes) + gas_costs.G_KECCAK_256 # SHA3 static cost for address generation (30) + + gas_costs.G_KECCAK_256_WORD * 3 # SHA3 dynamic cost (85 bytes = 3 words * 6) + gas_costs.G_COLD_ACCOUNT_ACCESS # Cold BALANCE (2600) - + gas_costs.G_BASE # POP balance + + gas_costs.G_BASE # POP balance (2) + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3) - + gas_costs.G_BASE * 4 # PUSH operations and POP - + 20 # Overhead + + gas_costs.G_BASE * 2 # DUP1 before BALANCE, DUP4 for address (6) + + gas_costs.G_VERYLOW * 8 # PUSH operations (8 * 3 = 24) + + gas_costs.G_LOW * 2 # MLOAD for salt twice (6) + + gas_costs.G_VERYLOW * 2 # ADD operations (6) + + gas_costs.G_LOW # MSTORE salt back (3) + + gas_costs.G_BASE # POP after EXTCODECOPY (2) + + 10 # While loop overhead ) # Calculate how many contracts to access From d7c79f0380cf1696a7e98d136b4a9f0740d94e5a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 25 Sep 2025 22:55:52 +0200 Subject: [PATCH 46/54] revert: restore pyproject.toml to match main branch Remove all changes to pyproject.toml to align with upstream main branch. This ensures CI compatibility and prevents configuration conflicts. --- pyproject.toml | 12 +++++++++--- tests/benchmark/bloatnet/test_multi_opcode.py | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a51d95d6c3b..8525cd71df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ docs = [ "mkdocstrings-python>=1.0.0,<2", "pillow>=10.0.1,<11", "pyspelling>=2.8.2,<3", - "lxml>=6.0.0,<7", # needs to be >= 6.0 for pypy + "lxml>=6.0.0,<7", # needs to be >= 6.0 for pypy "setuptools==78.0.2", ] @@ -157,9 +157,15 @@ ignore-words-list = "ingenuous" [tool.pytest.ini_options] console_output_style = "count" minversion = "7.0" -python_files = ["test_*.py"] -testpaths = ["src"] +python_files = [ + "test_*.py" +] +testpaths = [ + "src" +] addopts = [ + "-p", "pytester", + "-p", "pytest_plugins.eels_resolver", "--ignore=src/pytest_plugins/consume/test_cache.py", "--ignore=src/pytest_plugins/consume/direct/", "--ignore=src/pytest_plugins/consume/simulators/", diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 9b791da4081..1cde0afadce 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -78,9 +78,9 @@ def test_bloatnet_balance_extcodesize( + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODESIZE (100) + gas_costs.G_BASE # POP code size (2) + gas_costs.G_BASE # DUP1 before BALANCE (3) - + gas_costs.G_VERYLOW * 4 # PUSH1 operations (4 * 3) + + gas_costs.G_VERY_LOW * 4 # PUSH1 operations (4 * 3) + gas_costs.G_LOW # MLOAD for salt (3) - + gas_costs.G_VERYLOW # ADD for increment (3) + + gas_costs.G_VERY_LOW # ADD for increment (3) + gas_costs.G_LOW # MSTORE salt back (3) + 10 # While loop overhead ) @@ -224,9 +224,9 @@ def test_bloatnet_balance_extcodecopy( + gas_costs.G_WARM_ACCOUNT_ACCESS # Warm EXTCODECOPY base (100) + gas_costs.G_COPY * 1 # Copy cost for 1 byte (3) + gas_costs.G_BASE * 2 # DUP1 before BALANCE, DUP4 for address (6) - + gas_costs.G_VERYLOW * 8 # PUSH operations (8 * 3 = 24) + + gas_costs.G_VERY_LOW * 8 # PUSH operations (8 * 3 = 24) + gas_costs.G_LOW * 2 # MLOAD for salt twice (6) - + gas_costs.G_VERYLOW * 2 # ADD operations (6) + + gas_costs.G_VERY_LOW * 2 # ADD operations (6) + gas_costs.G_LOW # MSTORE salt back (3) + gas_costs.G_BASE # POP after EXTCODECOPY (2) + 10 # While loop overhead From d3671fa33cebed7509f497a38794f64ba88bd273 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 25 Sep 2025 23:39:44 +0200 Subject: [PATCH 47/54] fix(benchmark): resolve W505 doc line length issues in test_multi_opcode.py Fixed all documentation and comment lines exceeding 79 characters to comply with lint requirements. --- tests/benchmark/bloatnet/test_multi_opcode.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 1cde0afadce..e8e0da725a0 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -1,8 +1,9 @@ """ abstract: BloatNet bench cases extracted from https://hackmd.io/9icZeLN7R0Sk5mIjKlZAHQ. - The idea of all these tests is to stress client implementations to find out where the limits of - processing are focusing specifically on state-related operations. + The idea of all these tests is to stress client implementations to find out + where the limits of processing are focusing specifically on state-related + operations. """ import pytest @@ -42,9 +43,9 @@ # 3. EXTCODESIZE(addr) β†’ 100 gas (warm access) # # HOW IT WORKS: -# 1. Factory uses EXTCODECOPY to load initcode, avoiding PC-relative jump issues -# 2. Each CREATE2 deployment produces unique 24KB bytecode (via ADDRESS opcode) -# 3. All contracts share same initcode hash for deterministic address calculation +# 1. Factory uses EXTCODECOPY to load initcode, avoiding PC-relative jumps +# 2. Each CREATE2 deployment produces unique 24KB bytecode (via ADDRESS) +# 3. All contracts share same initcode hash for deterministic addresses # 4. Attack rapidly accesses all contracts, stressing client's state handling @@ -56,7 +57,8 @@ def test_bloatnet_balance_extcodesize( gas_benchmark_value: int, ): """ - BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2 address generation. + BloatNet test using BALANCE + EXTCODESIZE with "on-the-fly" CREATE2 + address generation. This test: 1. Assumes contracts are already deployed via the factory (salt 0 to N-1) @@ -99,7 +101,7 @@ def test_bloatnet_balance_extcodesize( stub="bloatnet_factory", ) - # Log test requirements - actual deployed count will be read from factory storage + # Log test requirements - deployed count read from factory storage print( f"Test needs {contracts_needed} contracts for " f"{gas_benchmark_value / 1_000_000:.1f}M gas. " @@ -108,7 +110,7 @@ def test_bloatnet_balance_extcodesize( # Build attack contract that reads config from factory and performs attack attack_code = ( - # Call getConfig() on factory to get num_deployed_contracts and init_code_hash + # Call getConfig() on factory to get num_deployed and init_code_hash # STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize) Op.PUSH1(64) # retSize (64 bytes = 2 * 32) + Op.PUSH1(96) # retOffset (store result at memory position 96) @@ -129,7 +131,7 @@ def test_bloatnet_balance_extcodesize( + Op.PUSH1(128) # Memory position for init_code_hash + Op.MLOAD # Load init_code_hash # Setup memory for CREATE2 address generation - # Memory layout at position 0: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + # Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32) + Op.PUSH20(factory_address) + Op.PUSH1(0) + Op.MSTORE # Store factory address at memory position 0 @@ -146,7 +148,7 @@ def test_bloatnet_balance_extcodesize( # Main attack loop - iterate through all deployed contracts + While( body=( - # Generate CREATE2 address: keccak256(0xFF + factory + salt + init_code_hash) + # Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash) Op.PUSH1(85) # Size to hash (1 + 20 + 32 + 32) + Op.PUSH1(11) # Start position (0xFF prefix) + Op.SHA3 # Generate CREATE2 address @@ -201,7 +203,8 @@ def test_bloatnet_balance_extcodecopy( gas_benchmark_value: int, ): """ - BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2 address generation. + BloatNet test using BALANCE + EXTCODECOPY with on-the-fly CREATE2 + address generation. This test forces actual bytecode reads from disk by: 1. Assumes contracts are already deployed via the factory @@ -246,7 +249,7 @@ def test_bloatnet_balance_extcodecopy( stub="bloatnet_factory", ) - # Log test requirements - actual deployed count will be read from factory storage + # Log test requirements - deployed count read from factory storage print( f"Test needs {contracts_needed} contracts for " f"{gas_benchmark_value / 1_000_000:.1f}M gas. " @@ -255,7 +258,7 @@ def test_bloatnet_balance_extcodecopy( # Build attack contract that reads config from factory and performs attack attack_code = ( - # Call getConfig() on factory to get num_deployed_contracts and init_code_hash + # Call getConfig() on factory to get num_deployed and init_code_hash # STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize) Op.PUSH1(64) # retSize (64 bytes = 2 * 32) + Op.PUSH1(96) # retOffset (store result at memory position 96) @@ -276,7 +279,7 @@ def test_bloatnet_balance_extcodecopy( + Op.PUSH1(128) # Memory position for init_code_hash + Op.MLOAD # Load init_code_hash # Setup memory for CREATE2 address generation - # Memory layout at position 0: 0xFF + factory_address(20) + salt(32) + init_code_hash(32) + # Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32) + Op.PUSH20(factory_address) + Op.PUSH1(0) + Op.MSTORE # Store factory address at memory position 0 From 8590357e52fd0bfefded97bb6307c12fad52dfba Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 26 Sep 2025 10:41:46 +0200 Subject: [PATCH 48/54] refactor(benchmark): simplify STATICCALL usage in BloatNet tests. --- tests/benchmark/bloatnet/test_multi_opcode.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index e8e0da725a0..4a6e12c1c7d 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -111,14 +111,14 @@ def test_bloatnet_balance_extcodesize( # Build attack contract that reads config from factory and performs attack attack_code = ( # Call getConfig() on factory to get num_deployed and init_code_hash - # STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize) - Op.PUSH1(64) # retSize (64 bytes = 2 * 32) - + Op.PUSH1(96) # retOffset (store result at memory position 96) - + Op.PUSH1(0) # argsSize (no calldata for getConfig) - + Op.PUSH1(0) # argsOffset - + Op.PUSH20(factory_address) # factory address - + Op.GAS # Use all available gas - + Op.STATICCALL # Call getConfig() + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, + ) # Check if call succeeded + Op.ISZERO + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) @@ -259,14 +259,14 @@ def test_bloatnet_balance_extcodecopy( # Build attack contract that reads config from factory and performs attack attack_code = ( # Call getConfig() on factory to get num_deployed and init_code_hash - # STATICCALL(gas, addr, argsOffset, argsSize, retOffset, retSize) - Op.PUSH1(64) # retSize (64 bytes = 2 * 32) - + Op.PUSH1(96) # retOffset (store result at memory position 96) - + Op.PUSH1(0) # argsSize (no calldata for getConfig) - + Op.PUSH1(0) # argsOffset - + Op.PUSH20(factory_address) # factory address - + Op.GAS # Use all available gas - + Op.STATICCALL # Call getConfig() + Op.STATICCALL( + gas=Op.GAS, + address=factory_address, + args_offset=0, + args_size=0, + ret_offset=96, + ret_size=64, + ) # Check if call succeeded + Op.ISZERO + Op.PUSH2(0x1000) # Jump to error handler if failed (far jump) From c044cb3d0365394865d8f2300539b317146b57fe Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 26 Sep 2025 10:44:31 +0200 Subject: [PATCH 49/54] feat(benchmark): add gas exhaustion validation using expected_receipt Implement solution to address reviewer's concern about test validation by using EEST's expected_receipt feature to validate that benchmarks consume all gas. Changes: - Add TransactionReceipt import - Add expected_receipt to both test transactions validating gas_used equals gas_limit - Remove skip_gas_used_validation flag as validation is now explicit This ensures tests can distinguish between: - Early failure from invalid jump (~50K gas) indicating setup issues - Full gas exhaustion (all gas consumed) indicating successful benchmark run The invalid jump remains as a fail-fast mechanism for STATICCALL failures, while expected_receipt validates the benchmark actually executed. --- tests/benchmark/bloatnet/test_multi_opcode.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 4a6e12c1c7d..e5334788585 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -15,6 +15,7 @@ Block, BlockchainTestFiller, Transaction, + TransactionReceipt, While, ) from ethereum_test_vm import Bytecode @@ -180,6 +181,11 @@ def test_bloatnet_balance_extcodesize( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), + # Validate that all gas is consumed (benchmark runs to exhaustion) + # If STATICCALL fails and hits invalid jump, only ~50K gas used -> test fails + expected_receipt=TransactionReceipt( + gas_used=gas_benchmark_value, # Must consume all gas + ), ) # Post-state: just verify attack contract exists @@ -191,7 +197,7 @@ def test_bloatnet_balance_extcodesize( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, - skip_gas_used_validation=True, + # Gas validation is now handled by expected_receipt ) @@ -338,6 +344,11 @@ def test_bloatnet_balance_extcodecopy( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), + # Validate that all gas is consumed (benchmark runs to exhaustion) + # If STATICCALL fails and hits invalid jump, only ~50K gas used -> test fails + expected_receipt=TransactionReceipt( + gas_used=gas_benchmark_value, # Must consume all gas + ), ) # Post-state @@ -349,5 +360,5 @@ def test_bloatnet_balance_extcodecopy( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, - skip_gas_used_validation=True, + # Gas validation is now handled by expected_receipt ) From ad6b424f19bbecefeabd7657ae01b895dff88b97 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 26 Sep 2025 10:46:43 +0200 Subject: [PATCH 50/54] fix(benchmark): restore skip_gas_used_validation flag Re-add skip_gas_used_validation=True to both blockchain_test calls as it was accidentally removed. This flag is still needed alongside the expected_receipt validation. --- tests/benchmark/bloatnet/test_multi_opcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index e5334788585..ce827911747 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -197,7 +197,7 @@ def test_bloatnet_balance_extcodesize( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, - # Gas validation is now handled by expected_receipt + skip_gas_used_validation=True, ) @@ -360,5 +360,5 @@ def test_bloatnet_balance_extcodecopy( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, - # Gas validation is now handled by expected_receipt + skip_gas_used_validation=True, ) From 2947788ca19a35d521e5723f3c555ff3dcdfdfc1 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 26 Sep 2025 10:54:55 +0200 Subject: [PATCH 51/54] refactor(benchmark): improve readability using kwargs syntax for opcodes Apply reviewer suggestions to use more readable kwargs syntax for memory and stack operations throughout both test functions. Changes: - Use Op.MLOAD(offset) instead of Op.PUSH1(offset) + Op.MLOAD - Use Op.MSTORE(offset, value) for cleaner memory writes - Use Op.SHA3(offset, length) for hash operations - Use Op.POP(Op.BALANCE) and Op.POP(Op.EXTCODESIZE) for cleaner stack ops - Combine increment operations into single Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) This makes the bytecode generation more concise and easier to understand. --- tests/benchmark/bloatnet/test_multi_opcode.py | 79 +++++-------------- 1 file changed, 21 insertions(+), 58 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index ce827911747..3e2b9b7cfac 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -127,45 +127,27 @@ def test_bloatnet_balance_extcodesize( # Load results from memory # Memory[96:128] = num_deployed_contracts # Memory[128:160] = init_code_hash - + Op.PUSH1(96) # Memory position for num_deployed_contracts - + Op.MLOAD # Load num_deployed_contracts - + Op.PUSH1(128) # Memory position for init_code_hash - + Op.MLOAD # Load init_code_hash + + Op.MLOAD(96) # Load num_deployed_contracts + + Op.MLOAD(128) # Load init_code_hash # Setup memory for CREATE2 address generation # Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32) - + Op.PUSH20(factory_address) - + Op.PUSH1(0) - + Op.MSTORE # Store factory address at memory position 0 - + Op.PUSH1(0xFF) - + Op.PUSH1(11) # Position for 0xFF prefix (32 - 20 - 1) - + Op.MSTORE8 # Store 0xFF prefix - + Op.PUSH1(0) # Initial salt value - + Op.PUSH1(32) - + Op.MSTORE # Store salt at position 32 + + Op.MSTORE(0, factory_address) # Store factory address at memory position 0 + + Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1) + + Op.MSTORE(32, 0) # Store salt at position 32 # Stack now has: [num_contracts, init_code_hash] - + Op.PUSH1(64) - + Op.MSTORE # Store init_code_hash at position 64 + + Op.MSTORE(64, Op.SWAP1) # Store init_code_hash at memory[64], swap keeps count on top # Stack now has: [num_contracts] # Main attack loop - iterate through all deployed contracts + While( body=( # Generate CREATE2 addr: keccak256(0xFF+factory+salt+hash) - Op.PUSH1(85) # Size to hash (1 + 20 + 32 + 32) - + Op.PUSH1(11) # Start position (0xFF prefix) - + Op.SHA3 # Generate CREATE2 address + Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96] # The address is now on the stack + Op.DUP1 # Duplicate for EXTCODESIZE - + Op.BALANCE # Cold access - + Op.POP - + Op.EXTCODESIZE # Warm access - + Op.POP + + Op.POP(Op.BALANCE) # Cold access + + Op.POP(Op.EXTCODESIZE) # Warm access # Increment salt for next iteration - + Op.PUSH1(32) # Salt position - + Op.MLOAD # Load current salt - + Op.PUSH1(1) - + Op.ADD # Increment - + Op.PUSH1(32) - + Op.MSTORE # Store back + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt ), # Continue while we haven't reached the limit condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, @@ -280,55 +262,36 @@ def test_bloatnet_balance_extcodecopy( # Load results from memory # Memory[96:128] = num_deployed_contracts # Memory[128:160] = init_code_hash - + Op.PUSH1(96) # Memory position for num_deployed_contracts - + Op.MLOAD # Load num_deployed_contracts - + Op.PUSH1(128) # Memory position for init_code_hash - + Op.MLOAD # Load init_code_hash + + Op.MLOAD(96) # Load num_deployed_contracts + + Op.MLOAD(128) # Load init_code_hash # Setup memory for CREATE2 address generation # Memory layout at 0: 0xFF + factory_addr(20) + salt(32) + hash(32) - + Op.PUSH20(factory_address) - + Op.PUSH1(0) - + Op.MSTORE # Store factory address at memory position 0 - + Op.PUSH1(0xFF) - + Op.PUSH1(11) # Position for 0xFF prefix (32 - 20 - 1) - + Op.MSTORE8 # Store 0xFF prefix - + Op.PUSH1(0) # Initial salt value - + Op.PUSH1(32) - + Op.MSTORE # Store salt at position 32 + + Op.MSTORE(0, factory_address) # Store factory address at memory position 0 + + Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1) + + Op.MSTORE(32, 0) # Store salt at position 32 # Stack now has: [num_contracts, init_code_hash] - + Op.PUSH1(64) - + Op.MSTORE # Store init_code_hash at position 64 + + Op.MSTORE(64, Op.SWAP1) # Store init_code_hash at memory[64], swap keeps count on top # Stack now has: [num_contracts] # Main attack loop - iterate through all deployed contracts + While( body=( # Generate CREATE2 address - Op.PUSH1(85) # Size to hash (1 + 20 + 32 + 32) - + Op.PUSH1(11) # Start position (0xFF prefix) - + Op.SHA3 # Generate CREATE2 address + Op.SHA3(11, 85) # Generate CREATE2 address from memory[11:96] # The address is now on the stack + Op.DUP1 # Duplicate for later operations - + Op.BALANCE # Cold access - + Op.POP + + Op.POP(Op.BALANCE) # Cold access # EXTCODECOPY(addr, mem_offset, last_byte_offset, 1) # Read the LAST byte to force full contract load + Op.PUSH1(1) # size (1 byte) + Op.PUSH2(max_contract_size - 1) # code offset (last byte) # Use salt as memory offset to avoid overlap - + Op.PUSH1(32) # Salt position - + Op.MLOAD # Get current salt - + Op.PUSH2(96) # Base memory offset - + Op.ADD # Unique memory position + + Op.MLOAD(32) # Get current salt from position 32 + + Op.ADD(96) # Add base memory offset for unique position + Op.DUP4 # address (duplicated earlier) + Op.EXTCODECOPY + Op.POP # Clean up address # Increment salt for next iteration - + Op.PUSH1(32) # Salt position - + Op.MLOAD # Load current salt - + Op.PUSH1(1) - + Op.ADD # Increment - + Op.PUSH1(32) - + Op.MSTORE # Store back + + Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)) # Increment and store salt ), # Continue while counter > 0 condition=Op.DUP1 + Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO, From bf665c542031c759ba5a45e0392abb2684612917 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 26 Sep 2025 11:42:45 +0200 Subject: [PATCH 52/54] fix(benchmark): shorten comment lines to meet doc length limit --- tests/benchmark/bloatnet/test_multi_opcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 3e2b9b7cfac..4d245191d44 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -164,7 +164,7 @@ def test_bloatnet_balance_extcodesize( gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), # Validate that all gas is consumed (benchmark runs to exhaustion) - # If STATICCALL fails and hits invalid jump, only ~50K gas used -> test fails + # If STATICCALL fails, only ~50K gas used -> test fails expected_receipt=TransactionReceipt( gas_used=gas_benchmark_value, # Must consume all gas ), @@ -308,7 +308,7 @@ def test_bloatnet_balance_extcodecopy( gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), # Validate that all gas is consumed (benchmark runs to exhaustion) - # If STATICCALL fails and hits invalid jump, only ~50K gas used -> test fails + # If STATICCALL fails, only ~50K gas used -> test fails expected_receipt=TransactionReceipt( gas_used=gas_benchmark_value, # Must consume all gas ), From 6841f094712a92a31a97b9cbec220a4d173fcfda Mon Sep 17 00:00:00 2001 From: CPerezz Date: Sat, 27 Sep 2025 15:11:30 +0200 Subject: [PATCH 53/54] fix(benchmark): correct MSTORE operation to store init_code_hash properly --- tests/benchmark/bloatnet/test_multi_opcode.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 4d245191d44..794ceb7e1a1 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -135,7 +135,8 @@ def test_bloatnet_balance_extcodesize( + Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1) + Op.MSTORE(32, 0) # Store salt at position 32 # Stack now has: [num_contracts, init_code_hash] - + Op.MSTORE(64, Op.SWAP1) # Store init_code_hash at memory[64], swap keeps count on top + + Op.PUSH1(64) # Push memory position + + Op.MSTORE # Store init_code_hash at memory[64] # Stack now has: [num_contracts] # Main attack loop - iterate through all deployed contracts + While( @@ -270,7 +271,8 @@ def test_bloatnet_balance_extcodecopy( + Op.MSTORE8(11, 0xFF) # Store 0xFF prefix at position (32 - 20 - 1) + Op.MSTORE(32, 0) # Store salt at position 32 # Stack now has: [num_contracts, init_code_hash] - + Op.MSTORE(64, Op.SWAP1) # Store init_code_hash at memory[64], swap keeps count on top + + Op.PUSH1(64) # Push memory position + + Op.MSTORE # Store init_code_hash at memory[64] # Stack now has: [num_contracts] # Main attack loop - iterate through all deployed contracts + While( From d1b868d4c11a3129b54dd048283b5919c58113a2 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Sat, 27 Sep 2025 15:23:26 +0200 Subject: [PATCH 54/54] fix(benchmark): address review comments - remove redundant validation and fix ADD syntax --- tests/benchmark/bloatnet/test_multi_opcode.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/benchmark/bloatnet/test_multi_opcode.py b/tests/benchmark/bloatnet/test_multi_opcode.py index 794ceb7e1a1..4d27d6fca49 100644 --- a/tests/benchmark/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/bloatnet/test_multi_opcode.py @@ -15,7 +15,6 @@ Block, BlockchainTestFiller, Transaction, - TransactionReceipt, While, ) from ethereum_test_vm import Bytecode @@ -164,11 +163,6 @@ def test_bloatnet_balance_extcodesize( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), - # Validate that all gas is consumed (benchmark runs to exhaustion) - # If STATICCALL fails, only ~50K gas used -> test fails - expected_receipt=TransactionReceipt( - gas_used=gas_benchmark_value, # Must consume all gas - ), ) # Post-state: just verify attack contract exists @@ -180,7 +174,6 @@ def test_bloatnet_balance_extcodesize( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, - skip_gas_used_validation=True, ) @@ -287,8 +280,7 @@ def test_bloatnet_balance_extcodecopy( + Op.PUSH1(1) # size (1 byte) + Op.PUSH2(max_contract_size - 1) # code offset (last byte) # Use salt as memory offset to avoid overlap - + Op.MLOAD(32) # Get current salt from position 32 - + Op.ADD(96) # Add base memory offset for unique position + + Op.ADD(Op.MLOAD(32), 96) # Add base memory offset for unique position + Op.DUP4 # address (duplicated earlier) + Op.EXTCODECOPY + Op.POP # Clean up address @@ -309,11 +301,6 @@ def test_bloatnet_balance_extcodecopy( to=attack_address, gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), - # Validate that all gas is consumed (benchmark runs to exhaustion) - # If STATICCALL fails, only ~50K gas used -> test fails - expected_receipt=TransactionReceipt( - gas_used=gas_benchmark_value, # Must consume all gas - ), ) # Post-state @@ -325,5 +312,4 @@ def test_bloatnet_balance_extcodecopy( pre=pre, blocks=[Block(txs=[attack_tx])], post=post, - skip_gas_used_validation=True, )