Skip to content

Commit 8acd12b

Browse files
feat(tests): add more XEN tests
1 parent 6a45a43 commit 8acd12b

File tree

1 file changed

+207
-11
lines changed

1 file changed

+207
-11
lines changed

tests/benchmark/mainnet/test_state_xen.py

Lines changed: 207 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@
1616

1717
import pytest
1818

19-
from ethereum_test_forks import Fork
19+
from ethereum_test_base_types.composite_types import Account
20+
from ethereum_test_forks.helpers import Fork
2021
from ethereum_test_tools import (
21-
Account,
2222
Alloc,
2323
Block,
2424
BlockchainTestFiller,
25-
Environment,
2625
Hash,
2726
Transaction,
2827
While,
29-
compute_create2_address,
3028
)
3129
from ethereum_test_tools import Macros as Om
32-
from ethereum_test_tools.vm.opcode import Opcodes as Op
30+
from ethereum_test_types.block_types import Environment
31+
from ethereum_test_types.helpers import compute_create2_address
32+
from ethereum_test_vm import Opcodes as Op
3333

3434

3535
# TODO: add test which writes to already existing storage
@@ -102,7 +102,7 @@ def test_xen_approve(
102102

103103

104104
@pytest.mark.valid_from("Frontier")
105-
def test_xen_approve_existing_slots(
105+
def test_xen_approve_change_existing_slots(
106106
blockchain_test: BlockchainTestFiller,
107107
pre: Alloc,
108108
):
@@ -114,9 +114,116 @@ def test_xen_approve_existing_slots(
114114
60_000_000 # TODO: currently hardcoded, should be read from `gas_benchmark_value`
115115
)
116116

117+
# 22 Sep 10:08:22 | Processed 23366464 | 207.4 ms | slot 1,734 ms |⛽ Gas gwei: 1.00 .. 1.00 (1.00) .. 1.00
118+
# 22 Sep 10:08:22 | Cleared caches: Rlp
119+
# 22 Sep 10:08:22 | Block 0.0600 ETH 59.96 MGas | 1 txs | calls 7,752 ( 0) | sload 8 | sstore 7,762 | create 0
120+
# 22 Sep 10:08:22 | Block throughput 289.05 MGas/s | 4.8 tps | 4.82 Blk/s | exec code cache 15,508 | new 0 | ops 1,868,419
121+
122+
xen_contract = 0x06450DEE7FD2FB8E39061434BABCFC05599A6FB8
123+
gas_threshold = 40_000
124+
125+
# This test deletes 9599 storage slots from XEN
126+
127+
fn_signature_approve = bytes.fromhex(
128+
"095EA7B3"
129+
) # Function selector of `approve(address,uint256)`
130+
# This code loops until there is less than threshold_gas left and reads two items from calldata:
131+
# The first 32 bytes are interpreted as the start address to start approving for
132+
# The second 32 bytes is the approval amount
133+
# This can thus be used to initialize the approvals (in multiple txs) to write to the storage
134+
# Since initializing storage (from zero to nonzero) is more expensive, this thus has
135+
# to be done over multiple blocks/txs
136+
# The attack block can then target all of the just initialized storage slots to edit
137+
# (This should thus yield more dirty trie nodes than the )
138+
approval_loop_code = (
139+
Om.MSTORE(fn_signature_approve)
140+
+ Op.MSTORE(4 + 32, Op.CALLDATALOAD(32))
141+
+ Op.CALLDATALOAD(0)
142+
+ While(
143+
body=Op.MSTORE(
144+
4, Op.DUP1
145+
) # Put a copy of the topmost stack item in memory (this is the target address)
146+
+ Op.CALL(address=xen_contract, args_offset=0, args_size=4 + 32 + 32)
147+
+ Op.ADD # Add the status of the CALL
148+
# (this should always be 1 unless the `gas_threshold` is too low) to the stack item
149+
# The address and thus target storage slot changes!
150+
+ Op.MSTORE(4 + 32, Op.SUB(Op.MLOAD(4 + 32), Op.GAS)),
151+
condition=Op.GT(Op.GAS, gas_threshold),
152+
)
153+
)
154+
155+
approval_spammer_contract = pre.deploy_contract(code=approval_loop_code)
156+
157+
sender = pre.fund_eoa()
158+
159+
blocks = []
160+
161+
# TODO: calculate these constants based on the gas limit of the benchmark test
162+
start_address = 0x01 # XEN blocks approving the zero address
163+
current_address = start_address
164+
address_incr = 2000
165+
166+
approval_value_fresh = Hash(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE)
167+
approval_value_overwrite = Hash(0)
168+
169+
block_count = 10
170+
171+
for _ in range(block_count):
172+
setup_calldata = Hash(current_address) + approval_value_fresh
173+
setup_tx = Transaction(
174+
to=approval_spammer_contract,
175+
gas_limit=attack_gas_limit,
176+
data=setup_calldata,
177+
sender=sender,
178+
max_priority_fee_per_gas=100,
179+
max_fee_per_gas=10000,
180+
)
181+
blocks.append(Block(txs=[setup_tx]))
182+
183+
current_address += address_incr
184+
185+
attack_calldata = Hash(start_address) + approval_value_overwrite
186+
187+
attack_tx = Transaction(
188+
to=approval_spammer_contract,
189+
gas_limit=attack_gas_limit,
190+
max_priority_fee_per_gas=100,
191+
max_fee_per_gas=10000,
192+
data=attack_calldata,
193+
sender=sender,
194+
)
195+
blocks.append(Block(txs=[attack_tx]))
196+
197+
blockchain_test(
198+
pre=pre,
199+
post={}, # TODO: add sanity checks (succesful tx execution and no out-of-gas)
200+
blocks=blocks,
201+
)
202+
203+
204+
# TODO split this code in all situations: 0->1, 1->2, 1->0
205+
@pytest.mark.valid_from("Frontier")
206+
def test_xen_approve_delete_existing_slots(
207+
blockchain_test: BlockchainTestFiller,
208+
pre: Alloc,
209+
):
210+
"""
211+
Uses the `approve(address,uint256)` method of XEN (ERC20) close to the maximum amount
212+
of slots which could be edited (as opposed to be created) within a single block/transaction.
213+
"""
214+
attack_gas_limit = (
215+
60_000_000 # TODO: currently hardcoded, should be read from `gas_benchmark_value`
216+
)
217+
218+
# 22 Sep 10:08:22 | Processed 23366464 | 207.4 ms | slot 1,734 ms |⛽ Gas gwei: 1.00 .. 1.00 (1.00) .. 1.00
219+
# 22 Sep 10:08:22 | Cleared caches: Rlp
220+
# 22 Sep 10:08:22 | Block 0.0600 ETH 59.96 MGas | 1 txs | calls 7,752 ( 0) | sload 8 | sstore 7,762 | create 0
221+
# 22 Sep 10:08:22 | Block throughput 289.05 MGas/s | 4.8 tps | 4.82 Blk/s | exec code cache 15,508 | new 0 | ops 1,868,419
222+
117223
# Gas limit: 60M, 2424 SSTOREs, 300 MGas/s
118224

119225
xen_contract = 0x06450DEE7FD2FB8E39061434BABCFC05599A6FB8
226+
usdt_contract = 0xDAC17F958D2EE523A2206206994597C13D831EC7 # Used in intermediate blocks to attempt to bust te cache
120227
gas_threshold = 40_000
121228

122229
fn_signature_approve = bytes.fromhex(
@@ -142,25 +249,53 @@ def test_xen_approve_existing_slots(
142249
+ Op.ADD, # Add the status of the CALL
143250
# (this should always be 1 unless the `gas_threshold` is too low) to the stack item
144251
# The address and thus target storage slot changes!
252+
# + Op.MSTORE(4 + 32, Op.SUB(Op.MLOAD(4 + 34), Op.GAS)),
145253
condition=Op.GT(Op.GAS, gas_threshold),
146254
)
147255
)
148256

149257
approval_spammer_contract = pre.deploy_contract(code=approval_loop_code)
150258

259+
usdt_approve_spammer_code = (
260+
Om.MSTORE(fn_signature_approve)
261+
+ Op.MSTORE(4 + 32, 1)
262+
+ Op.SLOAD(0)
263+
+ While(
264+
body=Op.MSTORE(
265+
4, Op.DUP1
266+
) # Put a copy of the topmost stack item in memory (this is the target address)
267+
+ Op.CALL(address=usdt_contract, args_offset=0, args_size=4 + 32 + 32)
268+
+ Op.ADD, # Add the status of the CALL
269+
# (this should always be 1 unless the `gas_threshold` is too low) to the stack item
270+
# The address and thus target storage slot changes!
271+
# + Op.MSTORE(4 + 32, Op.SUB(Op.MLOAD(4 + 34), Op.GAS)),
272+
condition=Op.GT(Op.GAS, gas_threshold),
273+
)
274+
+ Op.PUSH1(0)
275+
+ Op.SSTORE
276+
)
277+
# Set storage to value 1 to avoid paying 20k on the update
278+
usdt_approve_spammer_contract = pre.deploy_contract(
279+
code=usdt_approve_spammer_code, storage={0: 1}
280+
)
281+
151282
sender = pre.fund_eoa()
283+
sender2 = pre.fund_eoa() # More senders to get more chance to get a semi-full block
284+
sender3 = (
285+
pre.fund_eoa()
286+
) # If done from one sender, Nethermind's block builder only includes 1 tx
287+
sender4 = pre.fund_eoa()
288+
sender5 = pre.fund_eoa()
152289

153290
blocks = []
154291

155292
# TODO: calculate these constants based on the gas limit of the benchmark test
156-
start_address = 0x01
293+
start_address = 0x01 # Start at address 1, address 0 cannot be approved
157294
current_address = start_address
158295
address_incr = 2000
159296

160297
approval_value_fresh = Hash(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE)
161-
approval_value_overwrite = Hash(
162-
0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDE
163-
)
298+
approval_value_overwrite = Hash(0)
164299

165300
block_count = 10
166301

@@ -171,20 +306,81 @@ def test_xen_approve_existing_slots(
171306
gas_limit=attack_gas_limit,
172307
data=setup_calldata,
173308
sender=sender,
309+
max_priority_fee_per_gas=100,
310+
max_fee_per_gas=10000,
174311
)
175312
blocks.append(Block(txs=[setup_tx]))
176313

177314
current_address += address_incr
178315

316+
spam_count = 10
317+
318+
for _ in range(spam_count):
319+
# NOTE: USDC does not allow changing the approval value. It first has to be
320+
# set to zero before it changes. We therefore flood USDC with approvals in an
321+
# attempt to bust the cache
322+
spam_tx = Transaction(
323+
to=usdt_approve_spammer_contract,
324+
gas_limit=attack_gas_limit,
325+
sender=sender,
326+
max_priority_fee_per_gas=100,
327+
max_fee_per_gas=10000,
328+
)
329+
blocks.append(Block(txs=[spam_tx]))
330+
179331
attack_calldata = Hash(start_address) + approval_value_overwrite
180332

181333
attack_tx = Transaction(
182334
to=approval_spammer_contract,
183335
gas_limit=attack_gas_limit,
336+
max_priority_fee_per_gas=100,
337+
max_fee_per_gas=10000,
184338
data=attack_calldata,
185339
sender=sender,
186340
)
187-
blocks.append(Block(txs=[attack_tx]))
341+
# Take into account the max refunds (which will be awarded here)
342+
# The previous tx will also not completely use all gas since it jumps out of the loop early
343+
# to avoid that the whole tx OOGs
344+
# TODO: make this attack gas limit dependent
345+
# It should be sufficient to assume full refund and to send the whole block as gas limit initially
346+
# The next tx gas limit is thus (if refund is maximally applied) 20% of the original
347+
# Repeat this until the intrinsic costs cannot be paid
348+
attack_tx_2 = Transaction(
349+
to=approval_spammer_contract,
350+
gas_limit=attack_gas_limit // 5,
351+
max_priority_fee_per_gas=90,
352+
max_fee_per_gas=9000,
353+
data=Hash(8000) + approval_value_overwrite,
354+
sender=sender2,
355+
)
356+
attack_tx_3 = Transaction(
357+
to=approval_spammer_contract,
358+
gas_limit=attack_gas_limit // (5 * 5),
359+
max_priority_fee_per_gas=80,
360+
max_fee_per_gas=8000,
361+
data=Hash(12000) + approval_value_overwrite,
362+
sender=sender3,
363+
)
364+
attack_tx_4 = Transaction(
365+
to=approval_spammer_contract,
366+
gas_limit=attack_gas_limit // (5 * 5 * 5),
367+
max_priority_fee_per_gas=80,
368+
max_fee_per_gas=8000,
369+
data=Hash(16000) + approval_value_overwrite,
370+
sender=sender4,
371+
)
372+
attack_tx_5 = Transaction(
373+
to=approval_spammer_contract,
374+
gas_limit=attack_gas_limit // (5 * 5 * 5 * 5),
375+
max_priority_fee_per_gas=80,
376+
max_fee_per_gas=8000,
377+
data=Hash(18000) + approval_value_overwrite,
378+
sender=sender5,
379+
)
380+
381+
blocks.append(
382+
Block(txs=[attack_tx, attack_tx_2, attack_tx_3, attack_tx_4, attack_tx_5])
383+
) # , #attack_tx_2, attack_tx_3]))
188384

189385
blockchain_test(
190386
pre=pre,

0 commit comments

Comments
 (0)