|  | 
|  | 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