Skip to content

Commit 1006e55

Browse files
jochem-brouwerchfastmarioevz
authored andcommitted
feat(tests): Update benchmark test to query newly introduced max_code_size() from fork config (ethereum#1649)
* feat(forks): introduce max code size / max initcode size * feat(tests): read max code size from fork / scale gas limit as appropriate for test * Update src/ethereum_test_forks/base_fork.py Co-authored-by: Paweł Bylica <chfast@gmail.com> * Update src/ethereum_test_forks/base_fork.py Co-authored-by: Paweł Bylica <chfast@gmail.com> * Update src/ethereum_test_forks/forks/forks.py Co-authored-by: Paweł Bylica <chfast@gmail.com> * feat(tests): fix type error complaints * feat(tests): make tests dependent on MAX_CODE_SIZE now depend on the fork-based max code size * feat(tests): use a more logical gas limit formula for contract deployment * feat(tests): default (init)code size limit to first limit set * feat(tests): remove EIP 7907 from Osaka * feat(tests): make typecheck happy * feat(tests): do not use fallback for code size being returned `None` anymore * feat(tests): read code size from forks from newly added tests after rebase * Update tests/zkevm/test_worst_bytecode.py Co-authored-by: Mario Vega <marioevz@gmail.com> * feat(tests): insert max_code_size for modarith zkevm test --------- Co-authored-by: Paweł Bylica <chfast@gmail.com> Co-authored-by: Mario Vega <marioevz@gmail.com>
1 parent 0f41f22 commit 1006e55

File tree

5 files changed

+160
-96
lines changed

5 files changed

+160
-96
lines changed

src/ethereum_test_forks/base_fork.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,18 @@ def evm_code_types(cls, block_number: int = 0, timestamp: int = 0) -> List[EVMCo
471471
"""Return list of EVM code types supported by the fork."""
472472
pass
473473

474+
@classmethod
475+
@abstractmethod
476+
def max_code_size(cls) -> int:
477+
"""Return the maximum code size allowed to be deployed in a contract creation."""
478+
pass
479+
480+
@classmethod
481+
@abstractmethod
482+
def max_initcode_size(cls) -> int:
483+
"""Return the maximum initcode size allowed to be used in a contract creation."""
484+
pass
485+
474486
@classmethod
475487
@abstractmethod
476488
def call_opcodes(

src/ethereum_test_forks/forks/forks.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,18 @@ def evm_code_types(cls, block_number: int = 0, timestamp: int = 0) -> List[EVMCo
369369
"""At Genesis, only legacy EVM code is supported."""
370370
return [EVMCodeType.LEGACY]
371371

372+
@classmethod
373+
def max_code_size(cls) -> int:
374+
"""At genesis, there is no upper bound for code size (bounded by block gas limit)."""
375+
"""However, the default is set to the limit of EIP-170 (Spurious Dragon)"""
376+
return 0x6000
377+
378+
@classmethod
379+
def max_initcode_size(cls) -> int:
380+
"""At genesis, there is no upper bound for initcode size."""
381+
"""However, the default is set to the limit of EIP-3860 (Shanghai)"""
382+
return 0xC000
383+
372384
@classmethod
373385
def call_opcodes(
374386
cls, block_number: int = 0, timestamp: int = 0
@@ -634,6 +646,12 @@ def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[Address]
634646
block_number, timestamp
635647
)
636648

649+
@classmethod
650+
def max_code_size(cls) -> int:
651+
# NOTE: Move this to Spurious Dragon once this fork is introduced. See EIP-170.
652+
"""At Spurious Dragon, an upper bound was introduced for max contract code size."""
653+
return 0x6000
654+
637655
@classmethod
638656
def call_opcodes(
639657
cls, block_number: int = 0, timestamp: int = 0
@@ -860,6 +878,11 @@ def engine_new_payload_version(
860878
"""From Shanghai, new payload calls must use version 2."""
861879
return 2
862880

881+
@classmethod
882+
def max_initcode_size(cls) -> int:
883+
"""From Shanghai, the initcode size is now limited. See EIP-3860."""
884+
return 0xC000
885+
863886
@classmethod
864887
def valid_opcodes(
865888
cls,

tests/zkevm/test_worst_bytecode.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
REFERENCE_SPEC_GIT_PATH = "TODO"
2828
REFERENCE_SPEC_VERSION = "TODO"
2929

30-
MAX_CONTRACT_SIZE = 24 * 1024 # TODO: This could be a fork property
31-
3230
XOR_TABLE_SIZE = 256
3331
XOR_TABLE = [Hash(i).sha256() for i in range(XOR_TABLE_SIZE)]
3432

@@ -65,10 +63,43 @@ def test_worst_bytecode_single_opcode(
6563
The test is performed in the last block of the test, and the entire block gas limit is
6664
consumed by repeated opcode executions.
6765
"""
68-
# We use 100G gas limit to be able to deploy a large number of contracts in a single block,
69-
# avoiding bloating the number of preparing blocks in the test.
70-
env = Environment(gas_limit=100_000_000_000)
66+
# The attack gas limit is the gas limit which the target tx will use
67+
# The test will scale the block gas limit to setup the contracts accordingly to be
68+
# able to pay for the contract deposit. This has to take into account the 200 gas per byte,
69+
# but also the quadratic memory expansion costs which have to be paid each time the
70+
# memory is being setup
7171
attack_gas_limit = Environment().gas_limit
72+
max_contract_size = fork.max_code_size()
73+
74+
gas_costs = fork.gas_costs()
75+
76+
# Calculate the absolute minimum gas costs to deploy the contract
77+
# This does not take into account setting up the actual memory (using KECCAK256 and XOR)
78+
# so the actual costs of deploying the contract is higher
79+
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
80+
memory_gas_minimum = memory_expansion_gas_calculator(new_bytes=len(bytes(max_contract_size)))
81+
code_deposit_gas_minimum = (
82+
fork.gas_costs().G_CODE_DEPOSIT_BYTE * max_contract_size + memory_gas_minimum
83+
)
84+
85+
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
86+
# Calculate the loop cost of the attacker to query one address
87+
loop_cost = (
88+
gas_costs.G_KECCAK_256 # KECCAK static cost
89+
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
90+
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
91+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Opcode cost
92+
+ 30 # ~Gluing opcodes
93+
)
94+
# Calculate the number of contracts to be targeted
95+
num_contracts = (
96+
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
97+
attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4
98+
) // loop_cost
99+
100+
# Set the block gas limit to a relative high value to ensure the code deposit tx
101+
# fits in the block (there is enough gas available in the block to execute this)
102+
env = Environment(gas_limit=code_deposit_gas_minimum * 2 * num_contracts)
72103

73104
# The initcode will take its address as a starting point to the input to the keccak
74105
# hash function.
@@ -86,13 +117,13 @@ def test_worst_bytecode_single_opcode(
86117
)
87118
+ Op.POP
88119
),
89-
condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE),
120+
condition=Op.LT(Op.MSIZE, max_contract_size),
90121
)
91122
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
92123
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
93124
# intended.
94125
+ Op.MSTORE8(0, 0x00)
95-
+ Op.RETURN(0, MAX_CONTRACT_SIZE)
126+
+ Op.RETURN(0, max_contract_size)
96127
)
97128
initcode_address = pre.deploy_contract(code=initcode)
98129

@@ -127,24 +158,10 @@ def test_worst_bytecode_single_opcode(
127158
)
128159
factory_caller_address = pre.deploy_contract(code=factory_caller_code)
129160

130-
gas_costs = fork.gas_costs()
131-
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
132-
loop_cost = (
133-
gas_costs.G_KECCAK_256 # KECCAK static cost
134-
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
135-
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
136-
+ gas_costs.G_COLD_ACCOUNT_ACCESS # Opcode cost
137-
+ 30 # ~Gluing opcodes
138-
)
139-
num_contracts = (
140-
# Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
141-
attack_gas_limit - intrinsic_gas_cost_calc() - gas_costs.G_VERY_LOW * 4
142-
) // loop_cost
143-
144161
contracts_deployment_tx = Transaction(
145162
to=factory_caller_address,
146163
gas_limit=env.gas_limit,
147-
gas_price=10**9,
164+
gas_price=10**6,
148165
data=Hash(num_contracts),
149166
sender=pre.fund_eoa(),
150167
)
@@ -180,11 +197,11 @@ def test_worst_bytecode_single_opcode(
180197
)
181198
)
182199

183-
if len(attack_code) > MAX_CONTRACT_SIZE:
200+
if len(attack_code) > max_contract_size:
184201
# TODO: A workaround could be to split the opcode code into multiple contracts
185202
# and call them in sequence.
186203
raise ValueError(
187-
f"Code size {len(attack_code)} exceeds maximum code size {MAX_CONTRACT_SIZE}"
204+
f"Code size {len(attack_code)} exceeds maximum code size {max_contract_size}"
188205
)
189206
opcode_address = pre.deploy_contract(code=attack_code)
190207
opcode_tx = Transaction(

0 commit comments

Comments
 (0)