Skip to content

Commit 3febf15

Browse files
feat(tests): read max code size from fork / scale gas limit as appropriate for test
1 parent 86eb727 commit 3febf15

File tree

1 file changed

+44
-25
lines changed

1 file changed

+44
-25
lines changed

tests/zkevm/test_worst_bytecode.py

Lines changed: 44 additions & 25 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,45 @@ 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)
71-
attack_gas_limit = Environment().gas_limit
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
71+
attack_gas_limit = 100_000_000
72+
max_contract_size = fork.max_code_size() or 24 * 1024 # Uses Spurious Dragon limit by default
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+
# TODO: verify this, I think we can just do `code_deposit_gas_minimum * 2 * num_contracts`?
103+
# and then take the max of this and the attack gas limit
104+
env = Environment(gas_limit=attack_gas_limit * 2 * (code_deposit_gas_minimum // loop_cost + 1))
72105

73106
# The initcode will take its address as a starting point to the input to the keccak
74107
# hash function.
@@ -86,13 +119,13 @@ def test_worst_bytecode_single_opcode(
86119
)
87120
+ Op.POP
88121
),
89-
condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE),
122+
condition=Op.LT(Op.MSIZE, max_contract_size),
90123
)
91124
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
92125
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
93126
# intended.
94127
+ Op.MSTORE8(0, 0x00)
95-
+ Op.RETURN(0, MAX_CONTRACT_SIZE)
128+
+ Op.RETURN(0, max_contract_size)
96129
)
97130
initcode_address = pre.deploy_contract(code=initcode)
98131

@@ -127,24 +160,10 @@ def test_worst_bytecode_single_opcode(
127160
)
128161
factory_caller_address = pre.deploy_contract(code=factory_caller_code)
129162

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-
144163
contracts_deployment_tx = Transaction(
145164
to=factory_caller_address,
146165
gas_limit=env.gas_limit,
147-
gas_price=10**9,
166+
gas_price=10**6,
148167
data=Hash(num_contracts),
149168
sender=pre.fund_eoa(),
150169
)
@@ -180,11 +199,11 @@ def test_worst_bytecode_single_opcode(
180199
)
181200
)
182201

183-
if len(attack_code) > MAX_CONTRACT_SIZE:
202+
if len(attack_code) > max_contract_size:
184203
# TODO: A workaround could be to split the opcode code into multiple contracts
185204
# and call them in sequence.
186205
raise ValueError(
187-
f"Code size {len(attack_code)} exceeds maximum code size {MAX_CONTRACT_SIZE}"
206+
f"Code size {len(attack_code)} exceeds maximum code size {max_contract_size}"
188207
)
189208
opcode_address = pre.deploy_contract(code=attack_code)
190209
opcode_tx = Transaction(

0 commit comments

Comments
 (0)