Skip to content

Commit 3cf20b7

Browse files
feat(tests): add initial XEN test
1 parent 61f8ac9 commit 3cf20b7

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""
2+
abstract: Tests practical scenarios on Mainnet with the XEN (which has a big state) contract
3+
4+
Tests practical scenarios on Mainnet with the XEN (which has a big state) contract.
5+
This currently has one situation, but will be expanded with other scenarios.
6+
The goal is to bloat as much of the big state of XEN as possible. XEN has a big state trie.
7+
We therefore want to do as much state situations (either read or write: likely write is
8+
the most expensive situation).
9+
NOTE: this is thus NOT the worst-case scenario, since we can remove the overhead execution
10+
computations for XEN and only do state operations on an account with a big state attached to it.
11+
This therefore only tests the practical, "real life" and most likely scenario.
12+
However, with enough funds (to bloat a contract state), this is thus not the worst scenario.
13+
"""
14+
15+
import math
16+
17+
import pytest
18+
19+
from ethereum_test_forks import Fork
20+
from ethereum_test_tools import (
21+
Account,
22+
Alloc,
23+
Block,
24+
BlockchainTestFiller,
25+
Environment,
26+
Hash,
27+
Transaction,
28+
While,
29+
compute_create2_address,
30+
)
31+
from ethereum_test_tools import Macros as Om
32+
from ethereum_test_tools.vm.opcode import Opcodes as Op
33+
34+
# TODO
35+
# The current test does only claimRank(1) and then waits `SECONDS_IN_DAY = 3_600 * 24;` plus 1
36+
# (see https://etherscan.io/token/0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8#code) and then
37+
# claimMintReward() from CREATE2-create proxy accounts (to save gas).
38+
# This might not be the worst scenario, for instance `claimMintRewardAndShare(address,uint256)`
39+
# might yield even worse scenarios (or scenarios regarding "staking")
40+
# These scenarios will be added.
41+
42+
43+
# TODO: set correct fork, XEN might reject on historical forks due to e.g. non-existent opcodes
44+
# NOTE: deploy both XEN (0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8)
45+
# and Math (0x4bBA9B6B49f3dFA6615f079E9d66B0AA68B04A4d) in prestate for the Mainnet scenario!
46+
@pytest.mark.valid_from("Frontier")
47+
def test_xen_claimrank_and_mint(
48+
blockchain_test: BlockchainTestFiller,
49+
fork: Fork,
50+
pre: Alloc,
51+
env: Environment,
52+
gas_benchmark_value: int,
53+
):
54+
"""Simple XEN scenario to claimRank(1) and claimMintReward()."""
55+
attack_gas_limit = gas_benchmark_value
56+
fee_recipient = pre.fund_eoa(amount=1)
57+
58+
# timestamp to use for the initial block. Timestamp of later blocks are manually added/changed.
59+
timestamp = 12
60+
61+
# TODO: adjust this to the right amount of the actual performance test block
62+
num_xen = 10
63+
64+
# NOTE: these contracts MUST be specified for this test to work
65+
# TODO: check how/if EEST enforces this
66+
xen_contract = pre.deploy_contract("", label="XEN_CONTRACT")
67+
# NOTE: from the test perspective this contract should not be specified
68+
# However, the XEN contract needs the Math contract. If this is not provided, the transaction
69+
# will likely revert ("fail"). This is not what we want. We want state bloat!
70+
pre.deploy_contract("", label="MATH_CONTRACT")
71+
72+
# This is after (!!) deployment (so step 2, not 1): claimMintReward()
73+
calldata_claim_mint_reward = bytes.fromhex("52c7f8dc")
74+
after_initcode_callata = Om.MSTORE(bytes.fromhex("52c7f8dc")) + Op.CALL(
75+
address=xen_contract, args_size=len(calldata_claim_mint_reward)
76+
)
77+
78+
# Calldata for claimRank(1)
79+
calldata_claim_rank = bytes.fromhex(
80+
"9ff054df0000000000000000000000000000000000000000000000000000000000000001"
81+
)
82+
83+
# claimRank(1) and deposits the code to claimMintReward() if this contract is called
84+
initcode = (
85+
Om.MSTORE(calldata_claim_rank)
86+
+ Op.CALL(address=xen_contract, args_size=len(calldata_claim_rank))
87+
+ Om.MSTORE(after_initcode_callata)
88+
+ Op.RETURN(0, len(after_initcode_callata))
89+
)
90+
91+
# Template code that will be used to deploy a large number of contracts.
92+
initcode_address = pre.deploy_contract(code=initcode)
93+
94+
# Calculate the number of contracts that can be deployed with the available gas.
95+
gas_costs = fork.gas_costs()
96+
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
97+
loop_cost = (
98+
gas_costs.G_KECCAK_256 # KECCAK static cost
99+
+ math.ceil(85 / 32) * gas_costs.G_KECCAK_256_WORD # KECCAK dynamic cost for CREATE2
100+
+ gas_costs.G_VERY_LOW * 3 # ~MSTOREs+ADDs
101+
+ gas_costs.G_COLD_ACCOUNT_ACCESS # CALL to self-destructing contract
102+
+ gas_costs.G_SELF_DESTRUCT
103+
+ 63 # ~Gluing opcodes
104+
)
105+
final_storage_gas = (
106+
gas_costs.G_STORAGE_RESET + gas_costs.G_COLD_SLOAD + (gas_costs.G_VERY_LOW * 2)
107+
)
108+
memory_expansion_cost = fork().memory_expansion_gas_calculator()(new_bytes=96)
109+
base_costs = (
110+
intrinsic_gas_cost_calc()
111+
+ (gas_costs.G_VERY_LOW * 12) # 8 PUSHs + 4 MSTOREs
112+
+ final_storage_gas
113+
+ memory_expansion_cost
114+
)
115+
num_contracts = num_xen # TODO: edit this to construct as much contracts as possible to
116+
# `claimMintReward()` as the performance test.
117+
expected_benchmark_gas_used = num_contracts * loop_cost + base_costs
118+
119+
# Create a factory that deployes a new SELFDESTRUCT contract instance pre-funded depending on
120+
# the value_bearing parameter. We use CREATE2 so the caller contract can easily reproduce
121+
# the addresses in a loop for CALLs.
122+
factory_code = (
123+
Op.EXTCODECOPY(
124+
address=initcode_address,
125+
dest_offset=0,
126+
offset=0,
127+
size=Op.EXTCODESIZE(initcode_address),
128+
)
129+
+ Op.MSTORE(
130+
0,
131+
Op.CREATE2(
132+
offset=0,
133+
size=Op.EXTCODESIZE(initcode_address),
134+
salt=Op.SLOAD(0),
135+
),
136+
)
137+
+ Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1))
138+
+ Op.RETURN(0, 32)
139+
)
140+
141+
factory_address = pre.deploy_contract(code=factory_code)
142+
143+
factory_caller_code = Op.CALLDATALOAD(0) + While(
144+
body=Op.POP(Op.CALL(address=factory_address)),
145+
condition=Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
146+
)
147+
factory_caller_address = pre.deploy_contract(code=factory_caller_code)
148+
149+
contracts_deployment_tx = Transaction(
150+
to=factory_caller_address,
151+
gas_limit=env.gas_limit,
152+
data=Hash(num_contracts),
153+
sender=pre.fund_eoa(),
154+
)
155+
156+
code = (
157+
# Setup memory for later CREATE2 address generation loop.
158+
# 0xFF+[Address(20bytes)]+[seed(32bytes)]+[initcode keccak(32bytes)]
159+
Op.MSTORE(0, factory_address)
160+
+ Op.MSTORE8(32 - 20 - 1, 0xFF)
161+
+ Op.MSTORE(32, 0) # NOTE: this memory location is used as start index of the contracts.
162+
+ Op.MSTORE(64, initcode.keccak256())
163+
+ Op.CALLDATALOAD(0)
164+
# Main loop
165+
+ While(
166+
body=Op.POP(Op.CALL(address=Op.SHA3(32 - 20 - 1, 85)))
167+
+ Op.MSTORE(32, Op.ADD(Op.MLOAD(32), 1)),
168+
# Loop over `CALLDATALOAD` contracts
169+
condition=Op.PUSH1(1) + Op.SWAP1 + Op.SUB + Op.DUP1 + Op.ISZERO + Op.ISZERO,
170+
)
171+
+ Op.SSTORE(0, 42) # Done for successful tx execution assertion below.
172+
)
173+
assert len(code) <= fork.max_code_size()
174+
175+
# The 0 storage slot is initialize to avoid creation costs in SSTORE above.
176+
code_addr = pre.deploy_contract(code=code, storage={0: 1})
177+
opcode_tx = Transaction(
178+
to=code_addr,
179+
data=Hash(num_contracts),
180+
gas_limit=attack_gas_limit,
181+
sender=pre.fund_eoa(),
182+
)
183+
184+
post = {
185+
factory_address: Account(storage={0: num_contracts}),
186+
code_addr: Account(storage={0: 42}), # Check for successful execution.
187+
}
188+
deployed_contract_addresses = []
189+
for i in range(num_contracts):
190+
deployed_contract_address = compute_create2_address(
191+
address=factory_address,
192+
salt=i,
193+
initcode=initcode,
194+
)
195+
post[deployed_contract_address] = Account(nonce=1)
196+
deployed_contract_addresses.append(deployed_contract_address)
197+
198+
setup_block = Block(txs=[contracts_deployment_tx], timestamp=timestamp)
199+
blockchain_test(
200+
pre=pre,
201+
post=post,
202+
blocks=[
203+
setup_block,
204+
Block(
205+
txs=[opcode_tx],
206+
fee_recipient=fee_recipient,
207+
# Set timestamp such that XEN bond matures
208+
# See `MIN_TERM` constant in XEN source
209+
timestamp=timestamp + 3_600 * 24,
210+
),
211+
],
212+
exclude_full_post_state_in_output=True,
213+
expected_benchmark_gas_used=expected_benchmark_gas_used,
214+
)

0 commit comments

Comments
 (0)