66
77import pytest
88
9+ from ethereum_test_base_types import Address
910from ethereum_test_benchmark .benchmark_code_generator import JumpLoopGenerator
1011from ethereum_test_forks import Fork
1112from ethereum_test_tools import (
3233XOR_TABLE = [Hash (i ).sha256 () for i in range (XOR_TABLE_SIZE )]
3334
3435
35- @pytest .mark .parametrize (
36- "opcode" ,
37- [
38- Op .EXTCODESIZE ,
39- Op .EXTCODEHASH ,
40- Op .CALL ,
41- Op .CALLCODE ,
42- Op .DELEGATECALL ,
43- Op .STATICCALL ,
44- Op .EXTCODECOPY ,
45- ],
46- )
47- def test_worst_bytecode_single_opcode (
48- blockchain_test : BlockchainTestFiller ,
49- pre : Alloc ,
50- fork : Fork ,
51- opcode : Op ,
52- env : Environment ,
53- gas_benchmark_value : int ,
54- ):
55- """
56- Test a block execution where a single opcode execution maxes out the gas
57- limit, and the opcodes access a huge amount of contract code.
58-
59- We first use a single block to deploy a factory contract that will be used
60- to deploy a large number of contracts.
61-
62- This is done to avoid having a big pre-allocation size for the test.
63-
64- The test is performed in the last block of the test, and the entire block
65- gas limit is consumed by repeated opcode executions.
66- """
67- # The attack gas limit is the gas limit which the target tx will use The
68- # test will scale the block gas limit to setup the contracts accordingly to
69- # be able to pay for the contract deposit. This has to take into account
70- # the 200 gas per byte, but also the quadratic memory expansion costs which
71- # have to be paid each time the memory is being setup
72- attack_gas_limit = gas_benchmark_value
73- max_contract_size = fork .max_code_size ()
74-
75- gas_costs = fork .gas_costs ()
76-
77- # Calculate the absolute minimum gas costs to deploy the contract This does
78- # not take into account setting up the actual memory (using KECCAK256 and
79- # XOR) so the actual costs of deploying the contract is higher
80- memory_expansion_gas_calculator = fork .memory_expansion_gas_calculator ()
81- memory_gas_minimum = memory_expansion_gas_calculator (new_bytes = len (bytes (max_contract_size )))
82- code_deposit_gas_minimum = (
83- fork .gas_costs ().G_CODE_DEPOSIT_BYTE * max_contract_size + memory_gas_minimum
84- )
85-
86- intrinsic_gas_cost_calc = fork .transaction_intrinsic_cost_calculator ()
87- # Calculate the loop cost of the attacker to query one address
88- loop_cost = (
89- gas_costs .G_KECCAK_256 # KECCAK static cost
90- + math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic
91- # cost for CREATE2
92- + gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
93- + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
94- + 30 # ~Gluing opcodes
95- )
96- # Calculate the number of contracts to be targeted
97- num_contracts = (
98- # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
99- attack_gas_limit - intrinsic_gas_cost_calc () - gas_costs .G_VERY_LOW * 4
100- ) // loop_cost
101-
102- # Set the block gas limit to a relative high value to ensure the code
103- # deposit tx fits in the block (there is enough gas available in the block
104- # to execute this)
105- minimum_gas_limit = code_deposit_gas_minimum * 2 * num_contracts
106- if env .gas_limit < minimum_gas_limit :
107- raise Exception (
108- f"`BENCHMARKING_MAX_GAS` ({ env .gas_limit } ) is no longer enough to support this test, "
109- f"which requires { minimum_gas_limit } gas for its setup. Update the value or consider "
110- "optimizing gas usage during the setup phase of this test."
111- )
112-
113- # The initcode will take its address as a starting point to the input to
114- # the keccak hash function. It will reuse the output of the hash function
115- # in a loop to create a large amount of seemingly random code, until it
116- # reaches the maximum contract size.
36+ def deploy_initcode_template (pre : Alloc , fork : Fork ) -> tuple [Address , Bytecode ]:
37+ """Deploy the initcode template contract."""
38+ # The initcode will take its address as a starting point to the input to the keccak
39+ # hash function.
40+ # It will reuse the output of the hash function in a loop to create a large amount of
41+ # seemingly random code, until it reaches the maximum contract size.
11742 initcode = (
11843 Op .MSTORE (0 , Op .ADDRESS )
11944 + While (
@@ -127,18 +52,21 @@ def test_worst_bytecode_single_opcode(
12752 )
12853 + Op .POP
12954 ),
130- condition = Op .LT (Op .MSIZE , max_contract_size ),
55+ condition = Op .LT (Op .MSIZE , fork . max_code_size () ),
13156 )
13257 # Despite the whole contract has random bytecode, we make the first
13358 # opcode be a STOP so CALL-like attacks return as soon as possible,
13459 # while EXTCODE(HASH|SIZE) work as intended.
13560 + Op .MSTORE8 (0 , 0x00 )
136- + Op .RETURN (0 , max_contract_size )
61+ + Op .RETURN (0 , fork . max_code_size () )
13762 )
138- initcode_address = pre .deploy_contract (code = initcode )
63+ return pre .deploy_contract (code = initcode ), initcode
64+
13965
140- # The factory contract will simply use the initcode that is already
141- # deployed, and create a new contract and return its address if successful.
66+ def deploy_factory_contract (pre : Alloc , fork : Fork , initcode_address : Address ) -> Address :
67+ """Deploy the factory contract."""
68+ # The factory contract will simply use the initcode that is already deployed,
69+ # and create a new contract and return its address if successful.
14270 factory_code = (
14371 Op .EXTCODECOPY (
14472 address = initcode_address ,
@@ -158,75 +86,200 @@ def test_worst_bytecode_single_opcode(
15886 + Op .SSTORE (0 , Op .ADD (Op .SLOAD (0 ), 1 ))
15987 + Op .RETURN (0 , 32 )
16088 )
161- factory_address = pre .deploy_contract (code = factory_code )
89+ return pre .deploy_contract (code = factory_code )
16290
163- # The factory caller will call the factory contract N times, creating N new
164- # contracts. Calldata should contain the N value.
91+
92+ def deploy_factory_caller_contract (pre : Alloc , fork : Fork , factory_address : Address ) -> Address :
93+ """Deploy the factory caller contract."""
94+ # The factory caller will call the factory contract N times, creating N new contracts.
95+ # Calldata should contain the N value.
16596 factory_caller_code = Op .CALLDATALOAD (0 ) + While (
16697 body = Op .POP (Op .CALL (address = factory_address )),
16798 condition = Op .PUSH1 (1 ) + Op .SWAP1 + Op .SUB + Op .DUP1 + Op .ISZERO + Op .ISZERO ,
16899 )
169- factory_caller_address = pre .deploy_contract (code = factory_caller_code )
170100
171- contracts_deployment_tx = Transaction (
172- to = factory_caller_address ,
173- gas_limit = env .gas_limit ,
174- gas_price = 10 ** 6 ,
175- data = Hash (num_contracts ),
176- sender = pre .fund_eoa (),
101+ return pre .deploy_contract (code = factory_caller_code )
102+
103+
104+ def deploy_attack_contract (
105+ pre : Alloc , fork : Fork , factory_address : Address , initcode : Bytecode , opcode : Op
106+ ) -> Address :
107+ """Deploy the attack contract."""
108+ # Setup memory for later CREATE2 address generation loop.
109+ # 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
110+ setup = (
111+ Op .MSTORE (0 , factory_address )
112+ + Op .MSTORE8 (32 - 20 - 1 , 0xFF )
113+ + Op .CALLDATACOPY (dest_offset = 32 , offset = 0 , size = 32 )
114+ + Op .MSTORE (64 , initcode .keccak256 ())
177115 )
178116
179- post = {}
180- deployed_contract_addresses = []
181- for i in range (num_contracts ):
182- deployed_contract_address = compute_create2_address (
183- address = factory_address ,
184- salt = i ,
185- initcode = initcode ,
186- )
187- post [deployed_contract_address ] = Account (nonce = 1 )
188- deployed_contract_addresses .append (deployed_contract_address )
117+ # setup_cost: G_VERY_LOW * 9 (PUSH) + G_VERY_LOW * 3 (MSTORE) + G_VERY_LOW (CALLDATACOPY)
189118
119+ # Attack call
190120 attack_call = Bytecode ()
191121 if opcode == Op .EXTCODECOPY :
192122 attack_call = Op .EXTCODECOPY (address = Op .SHA3 (32 - 20 - 1 , 85 ), dest_offset = 96 , size = 1000 )
193123 else :
194124 # For the rest of the opcodes, we can use the same generic attack call
195125 # since all only minimally need the `address` of the target.
196126 attack_call = Op .POP (opcode (address = Op .SHA3 (32 - 20 - 1 , 85 )))
197- attack_code = (
198- # Setup memory for later CREATE2 address generation loop.
199- # 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
200- Op .MSTORE (0 , factory_address )
201- + Op .MSTORE8 (32 - 20 - 1 , 0xFF )
202- + Op .MSTORE (32 , 0 )
203- + Op .MSTORE (64 , initcode .keccak256 ())
204- # Main loop
205- + While (
206- body = attack_call + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )),
207- )
127+
128+ attack_code = setup + While (
129+ body = attack_call + Op .MSTORE (32 , Op .ADD (Op .MLOAD (32 ), 1 )),
208130 )
209131
210- if len (attack_code ) > max_contract_size :
211- # TODO: A workaround could be to split the opcode code into multiple
212- # contracts and call them in sequence.
213- raise ValueError (
214- f"Code size { len (attack_code )} exceeds maximum code size { max_contract_size } "
215- )
216- opcode_address = pre .deploy_contract (code = attack_code )
217- opcode_tx = Transaction (
218- to = opcode_address ,
219- gas_limit = attack_gas_limit ,
220- gas_price = 10 ** 9 ,
221- sender = pre .fund_eoa (),
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 * ~MSTOREs+ADDs
136+ # + gas_costs.G_COLD_ACCOUNT_ACCESS Opcode cost
137+ # + 30 ~Gluing opcodes
138+ # )
139+
140+ return pre .deploy_contract (code = attack_code )
141+
142+
143+ @pytest .mark .parametrize (
144+ "opcode" ,
145+ [
146+ Op .EXTCODESIZE ,
147+ Op .EXTCODEHASH ,
148+ Op .CALL ,
149+ Op .CALLCODE ,
150+ Op .DELEGATECALL ,
151+ Op .STATICCALL ,
152+ Op .EXTCODECOPY ,
153+ ],
154+ )
155+ def test_worst_bytecode_single_opcode (
156+ blockchain_test : BlockchainTestFiller ,
157+ pre : Alloc ,
158+ fork : Fork ,
159+ opcode : Op ,
160+ env : Environment ,
161+ gas_benchmark_value : int ,
162+ tx_gas_limit_cap : int ,
163+ ):
164+ """
165+ Test a block execution where a single opcode execution maxes out the gas limit,
166+ and the opcodes access a huge amount of contract code.
167+
168+ We first use a single block to deploy a factory contract that will be used to deploy
169+ a large number of contracts.
170+
171+ This is done to avoid having a big pre-allocation size for the test.
172+
173+ The test is performed in the last block of the test, and the entire block gas limit is
174+ consumed by repeated opcode executions.
175+ """
176+ iteration_count = gas_benchmark_value // tx_gas_limit_cap
177+
178+ # The attack gas limit is the gas limit which the target tx will use
179+ # The test will scale the block gas limit to setup the contracts accordingly to be
180+ # able to pay for the contract deposit. This has to take into account the 200 gas per byte,
181+ # but also the quadratic memory expansion costs which have to be paid each time the
182+ # memory is being setup
183+ max_contract_size = fork .max_code_size ()
184+
185+ gas_costs = fork .gas_costs ()
186+
187+ intrinsic_gas_cost_calc = fork .transaction_intrinsic_cost_calculator ()
188+ setup_cost = gas_costs .G_VERY_LOW * 13
189+ # Calculate the loop cost of the attacker to query one address
190+ loop_cost = (
191+ gas_costs .G_KECCAK_256 # KECCAK static cost
192+ + math .ceil (85 / 32 ) * gas_costs .G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
193+ + gas_costs .G_VERY_LOW * 3 # ~MSTOREs+ADDs
194+ + gas_costs .G_COLD_ACCOUNT_ACCESS # Opcode cost
195+ + 30 # ~Gluing opcodes
222196 )
223197
198+ total_contracts = 0
199+ gas_remaining = gas_benchmark_value
200+ for _ in range (iteration_count ):
201+ gas_available = min (tx_gas_limit_cap , gas_remaining )
202+ total_contracts += (
203+ # Base available gas = GAS_LIMIT - intrinsic - (out of loop MSTOREs)
204+ gas_available - intrinsic_gas_cost_calc () - setup_cost
205+ ) // loop_cost
206+ gas_remaining -= gas_available
207+
208+ # Deployment Phase - Deploy factory contract
209+ initcode_address , initcode = deploy_initcode_template (pre , fork )
210+ factory_address = deploy_factory_contract (pre , fork , initcode_address )
211+ factory_caller_address = deploy_factory_caller_contract (pre , fork , factory_address )
212+
213+ # Deployment Phase - Deploy N contracts
214+
215+ # Calculate the absolute minimum gas costs to deploy the contract
216+ # This does not take into account setting up the actual memory (using KECCAK256 and XOR)
217+ # so the actual costs of deploying the contract is higher
218+ memory_expansion_gas_calculator = fork .memory_expansion_gas_calculator ()
219+ memory_gas_minimum = memory_expansion_gas_calculator (new_bytes = len (bytes (max_contract_size )))
220+ code_deposit_gas_minimum = (
221+ fork .gas_costs ().G_CODE_DEPOSIT_BYTE * max_contract_size + memory_gas_minimum
222+ )
223+
224+ contracts_deployment_txs = []
225+ deployment_cost_per_iteration = code_deposit_gas_minimum * 3
226+ deployed_contract_num = 0
227+ gas_remaining = gas_benchmark_value
228+
229+ while deployed_contract_num < total_contracts :
230+ gas_available = min (tx_gas_limit_cap , gas_remaining )
231+ gas_remaining -= gas_available
232+ num = (gas_available - intrinsic_gas_cost_calc ()) // deployment_cost_per_iteration
233+ deployed_contract_num += num
234+ contracts_deployment_txs .append (
235+ Transaction (
236+ to = factory_caller_address ,
237+ gas_limit = gas_available ,
238+ data = Hash (num ),
239+ sender = pre .fund_eoa (),
240+ )
241+ )
242+
243+ # Attack Phase
244+ opcode_address = deploy_attack_contract (pre , fork , factory_address , initcode , opcode )
245+
246+ opcode_txs = []
247+ gas_remaining = gas_benchmark_value
248+ access_contract_index = 0
249+
250+ for _ in range (iteration_count ):
251+ gas_available = min (tx_gas_limit_cap , gas_remaining )
252+ gas_remaining -= gas_available
253+ num = (gas_available - intrinsic_gas_cost_calc () - setup_cost ) // loop_cost
254+ opcode_txs .append (
255+ Transaction (
256+ to = opcode_address ,
257+ data = Hash (access_contract_index ),
258+ gas_limit = gas_available ,
259+ sender = pre .fund_eoa (),
260+ )
261+ )
262+ access_contract_index += num
263+
264+ # Post State Verification
265+ post = {}
266+ deployed_contract_addresses = []
267+ for i in range (total_contracts ):
268+ deployed_contract_address = compute_create2_address (
269+ address = factory_address ,
270+ salt = i ,
271+ initcode = initcode ,
272+ )
273+ post [deployed_contract_address ] = Account (nonce = 1 )
274+ deployed_contract_addresses .append (deployed_contract_address )
275+
276+ # Blockchain Test Execution
224277 blockchain_test (
225278 pre = pre ,
226279 post = post ,
227280 blocks = [
228- Block (txs = [ contracts_deployment_tx ] ),
229- Block (txs = [ opcode_tx ] ),
281+ Block (txs = contracts_deployment_txs ),
282+ Block (txs = opcode_txs ),
230283 ],
231284 exclude_full_post_state_in_output = True ,
232285 )
0 commit comments