diff --git a/fillers/eips/__init__.py b/fillers/eips/__init__.py new file mode 100644 index 00000000000..a0d7959b419 --- /dev/null +++ b/fillers/eips/__init__.py @@ -0,0 +1,3 @@ +""" +Cross-client Ethereum Improvement Proposal Tests +""" diff --git a/fillers/eips/eip3651.py b/fillers/eips/eip3651.py new file mode 100644 index 00000000000..9373325aa13 --- /dev/null +++ b/fillers/eips/eip3651.py @@ -0,0 +1,340 @@ +""" +Test EIP-3651: Warm COINBASE +EIP: https://eips.ethereum.org/EIPS/eip-3651 +Source tests: https://github.com/ethereum/tests/pull/1082 +""" +from typing import Dict + +from ethereum_test_tools import ( + Account, + CodeGasMeasure, + Environment, + StateTest, + TestAddress, + Transaction, + Yul, + is_fork, + test_from, + to_address, + to_hash, +) + + +@test_from(fork="merged") +def test_warm_coinbase_call_out_of_gas(fork): + """ + Test warm coinbase. + """ + env = Environment( + coinbase="0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + difficulty=0x20000, + gas_limit=10000000000, + number=1, + timestamp=1000, + ) + + caller_code = Yul( + """ + { + // Depending on the called contract here, the subcall will perform + // another call/delegatecall/staticcall/callcode that will only + // succeed if coinbase is considered warm by default + // (post-Shanghai). + let calladdr := calldataload(0) + + // Amount of gas required to make a call to a warm account. + // Calling a cold account with this amount of gas results in + // exception. + let callgas := 100 + + switch calladdr + case 0x100 { + // Extra: COINBASE + 6xPUSH1 + DUP6 + 2xPOP + callgas := add(callgas, 27) + } + case 0x200 { + // Extra: COINBASE + 6xPUSH1 + DUP6 + 2xPOP + callgas := add(callgas, 27) + } + case 0x300 { + // Extra: COINBASE + 5xPUSH1 + DUP6 + 2xPOP + callgas := add(callgas, 24) + } + case 0x400 { + // Extra: COINBASE + 5xPUSH1 + DUP6 + 2xPOP + callgas := add(callgas, 24) + } + // Call and save result + sstore(0, call(callgas, calladdr, 0, 0, 0, 0, 0)) + } + """ + ) + + call_code = Yul( + """ + { + let cb := coinbase() + pop(call(0, cb, 0, 0, 0, 0, 0)) + } + """ + ) + + callcode_code = Yul( + """ + { + let cb := coinbase() + pop(callcode(0, cb, 0, 0, 0, 0, 0)) + } + """ + ) + + delegatecall_code = Yul( + """ + { + let cb := coinbase() + pop(delegatecall(0, cb, 0, 0, 0, 0)) + } + """ + ) + + staticcall_code = Yul( + """ + { + let cb := coinbase() + pop(staticcall(0, cb, 0, 0, 0, 0)) + } + """ + ) + + pre = { + TestAddress: Account(balance=1000000000000000000000), + "0xcccccccccccccccccccccccccccccccccccccccc": Account( + code=caller_code + ), + to_address(0x100): Account(code=call_code), + to_address(0x200): Account(code=callcode_code), + to_address(0x300): Account(code=delegatecall_code), + to_address(0x400): Account(code=staticcall_code), + } + + for i, data in enumerate( + [to_hash(x) for x in range(0x100, 0x400 + 1, 0x100)] + ): + + tx = Transaction( + ty=0x0, + data=data, + chain_id=0x0, + nonce=0, + to="0xcccccccccccccccccccccccccccccccccccccccc", + gas_limit=100000000, + gas_price=10, + protected=False, + ) + + post = {} + + if is_fork(fork=fork, which="shanghai"): + post["0xcccccccccccccccccccccccccccccccccccccccc"] = Account( + storage={ + # On shanghai and beyond, calls with only 100 gas to + # coinbase will succeed. + 0: 1, + } + ) + else: + post["0xcccccccccccccccccccccccccccccccccccccccc"] = Account( + storage={ + # Before shanghai, calls with only 100 gas to + # coinbase will fail. + 0: 0, + } + ) + + yield StateTest(env=env, pre=pre, post=post, txs=[tx]) + + +@test_from(fork="merged") +def test_warm_coinbase_gas_usage(fork): + """ + Test gas usage of different opcodes assuming warm coinbase. + """ + env = Environment( + coinbase="0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba", + difficulty=0x20000, + gas_limit=10000000000, + number=1, + timestamp=1000, + ) + + # List of opcodes that are affected by + gas_measured_opcodes: Dict[str, CodeGasMeasure] = { + "EXTCODESIZE": CodeGasMeasure( + code=bytes( + [ + 0x41, # addr: COINBASE + 0x3B, # EXTCODESIZE + ] + ), + overhead_cost=2, + extra_stack_items=1, + ), + "EXTCODECOPY": CodeGasMeasure( + code=bytes( + [ + 0x60, # length + 0x00, + 0x60, # offset + 0x00, + 0x60, # offset + 0x00, + 0x41, # addr: COINBASE + 0x3C, # EXTCODECOPY + ] + ), + overhead_cost=2 + 3 + 3 + 3, + ), + "EXTCODEHASH": CodeGasMeasure( + code=bytes( + [ + 0x41, # addr: COINBASE + 0x3F, # EXTCODEHASH + ] + ), + overhead_cost=2, + extra_stack_items=1, + ), + "BALANCE": CodeGasMeasure( + code=bytes( + [ + 0x41, # addr: COINBASE + 0x31, # BALANCE + ] + ), + overhead_cost=2, + extra_stack_items=1, + ), + "CALL": CodeGasMeasure( + code=bytes( + [ + 0x60, # returnLength + 0x00, + 0x60, # returnOffset + 0x00, + 0x60, # argsLength + 0x00, + 0x60, # argsOffset + 0x00, + 0x60, # value + 0x00, + 0x41, # addr: COINBASE + 0x60, # gas + 0xFF, + 0xF1, # CALL + ] + ), + overhead_cost=3 + 2 + 3 + 3 + 3 + 3 + 3, + extra_stack_items=1, + ), + "CALLCODE": CodeGasMeasure( + code=bytes( + [ + 0x60, # returnLength + 0x00, + 0x60, # returnOffset + 0x00, + 0x60, # argsLength + 0x00, + 0x60, # argsOffset + 0x00, + 0x60, # value + 0x00, + 0x41, # addr: COINBASE + 0x60, # gas + 0xFF, + 0xF2, # CALLCODE + ] + ), + overhead_cost=3 + 2 + 3 + 3 + 3 + 3 + 3, + extra_stack_items=1, + ), + "DELEGATECALL": CodeGasMeasure( + code=bytes( + [ + 0x60, # returnLength + 0x00, + 0x60, # returnOffset + 0x00, + 0x60, # argsLength + 0x00, + 0x60, # argsOffset + 0x00, + 0x41, # addr: COINBASE + 0x60, # gas + 0xFF, + 0xF4, # DELEGATECALL + ] + ), + overhead_cost=3 + 2 + 3 + 3 + 3 + 3, + extra_stack_items=1, + ), + "STATICCALL": CodeGasMeasure( + code=bytes( + [ + 0x60, # returnLength + 0x00, + 0x60, # returnOffset + 0x00, + 0x60, # argsLength + 0x00, + 0x60, # argsOffset + 0x00, + 0x41, # addr: COINBASE + 0x60, # gas + 0xFF, + 0xFA, # STATICCALL + ] + ), + overhead_cost=3 + 2 + 3 + 3 + 3 + 3, + extra_stack_items=1, + ), + } + + for opcode in gas_measured_opcodes: + measure_address = to_address(0x100) + pre = { + TestAddress: Account(balance=1000000000000000000000), + measure_address: Account( + code=gas_measured_opcodes[opcode], + ), + } + + if is_fork(fork, "shanghai"): + expected_gas = 100 # Warm account access cost after EIP-3651 + else: + expected_gas = 2600 # Cold account access cost before EIP-3651 + + post = { + measure_address: Account( + storage={ + 0x00: expected_gas, + } + ) + } + tx = Transaction( + ty=0x0, + chain_id=0x0, + nonce=0, + to=measure_address, + gas_limit=100000000, + gas_price=10, + protected=False, + ) + + yield StateTest( + env=env, + pre=pre, + post=post, + txs=[tx], + name="warm_coinbase_opcode_" + opcode.lower(), + ) diff --git a/fillers/eips/eip3855.py b/fillers/eips/eip3855.py new file mode 100644 index 00000000000..d3a909bbac1 --- /dev/null +++ b/fillers/eips/eip3855.py @@ -0,0 +1,198 @@ +""" +Test EIP-3855: PUSH0 Instruction +EIP: https://eips.ethereum.org/EIPS/eip-3855 +Source tests: https://github.com/ethereum/tests/pull/1033 +""" + +from ethereum_test_tools import ( + Account, + CodeGasMeasure, + Environment, + StateTest, + TestAddress, + Transaction, + Yul, + test_from, + to_address, +) + + +@test_from(fork="shanghai", eips=[3855]) +def test_push0(fork): + """ + Test push0 opcode. + """ + env = Environment() + + pre = {TestAddress: Account(balance=1000000000000000000000)} + post = {} + + addr_1 = to_address(0x100) + addr_2 = to_address(0x200) + + # Entry point for all test cases is the same address + tx = Transaction( + to=addr_1, + gas_limit=100000, + ) + + """ + Test case 1: Simple PUSH0 as key to SSTORE + """ + code = bytes( + [ + 0x60, # PUSH1 + 0x01, + 0x5F, # PUSH0 + 0x55, # SSTORE + ] + ) + + pre[addr_1] = Account(code=code) + post[addr_1] = Account(storage={0x00: 0x01}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_key_sstore" + ) + + """ + Test case 2: Fill stack with PUSH0, then OR all values and save using + SSTORE + """ + code = bytes([0x5F] * 1024) # PUSH0 + code += bytes([0x17] * 1023) # OR + code += bytes( + [ + 0x60, # PUSH1 + 0x01, + 0x90, # SWAP1 + 0x55, # SSTORE + ] + ) + + pre[addr_1] = Account(code=code) + post[addr_1] = Account(storage={0x00: 0x01}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_fill_stack" + ) + + """ + Test case 3: Stack overflow by using PUSH0 1025 times + """ + code = bytes( + [ + 0x60, # PUSH1 + 0x01, + 0x5F, # PUSH0 + 0x55, # SSTORE + ] + ) + code += bytes([0x5F] * 1025) # PUSH0, stack overflow + + pre[addr_1] = Account(code=code) + post[addr_1] = Account(storage={0x00: 0x00}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_stack_overflow" + ) + + """ + Test case 4: Update already existing storage value + """ + code = bytes( + [ + 0x60, # PUSH1 + 0x02, + 0x5F, # PUSH0 + 0x55, # SSTORE + 0x5F, # PUSH0 + 0x60, # PUSH1 + 0x01, + 0x55, # SSTORE + ] + ) + + pre[addr_1] = Account(code=code, storage={0x00: 0x0A, 0x01: 0x0A}) + post[addr_1] = Account(storage={0x00: 0x02, 0x01: 0x00}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_storage_overwrite" + ) + + """ + Test case 5: PUSH0 during staticcall + """ + code_1 = Yul( + """ + { + sstore(0, staticcall(100000, 0x200, 0, 0, 0, 0)) + sstore(0, 1) + returndatacopy(0x1f, 0, 1) + sstore(1, mload(0)) + } + """ + ) + code_2 = bytes( + [ + 0x60, # PUSH1 + 0xFF, + 0x5F, # PUSH0 + 0x53, # MSTORE8 + 0x60, # PUSH1 + 0x01, + 0x60, # PUSH1 + 0x00, + 0xF3, # RETURN + ] + ) + + pre[addr_1] = Account(code=code_1) + pre[addr_2] = Account(code=code_2) + post[addr_1] = Account(storage={0x00: 0x01, 0x01: 0xFF}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_during_staticcall" + ) + + del pre[addr_2] + + """ + Test case 6: Jump to a JUMPDEST next to a PUSH0, must succeed. + """ + code = bytes( + [ + 0x60, # PUSH1 + 0x04, + 0x56, # JUMP + 0x5F, # PUSH0 + 0x5B, # JUMPDEST + 0x60, # PUSH1 + 0x01, + 0x5F, # PUSH0 + 0x55, # SSTORE + 0x00, # STOP + ] + ) + + pre[addr_1] = Account(code=code) + post[addr_1] = Account(storage={0x00: 0x01}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_before_jumpdest" + ) + + """ + Test case 7: PUSH0 gas cost + """ + code = CodeGasMeasure( + code=bytes([0x5F]), # PUSH0 + extra_stack_items=1, + ) + + pre[addr_1] = Account(code=code) + post[addr_1] = Account(storage={0x00: 0x02}) + + yield StateTest( + env=env, pre=pre, post=post, txs=[tx], name="push0_gas_cost" + ) diff --git a/fillers/eips/eip3860.py b/fillers/eips/eip3860.py new file mode 100644 index 00000000000..f21c6ed6466 --- /dev/null +++ b/fillers/eips/eip3860.py @@ -0,0 +1,703 @@ +""" +Test EIP-3860: Limit and meter initcode +EIP: https://eips.ethereum.org/EIPS/eip-3860 +Source tests: https://github.com/ethereum/tests/pull/990 + https://github.com/ethereum/tests/pull/1012 +""" + + +from typing import Any, Dict + +from ethereum_test_tools import ( + Account, + Block, + BlockchainTest, + Environment, + Initcode, + StateTest, + TestAddress, + Transaction, + Yul, + ceiling_division, + compute_create2_address, + compute_create_address, + eip_2028_transaction_data_cost, + test_from, + to_address, +) + +""" +General constants used for testing purposes +""" + +MAX_INITCODE_SIZE = 49152 +INITCODE_WORD_COST = 2 +KECCAK_WORD_COST = 6 +INITCODE_RESULTING_DEPLOYED_CODE = bytes([0x00]) + +BASE_TRANSACTION_GAS = 21000 +CREATE_CONTRACT_BASE_GAS = 32000 +GAS_OPCODE_GAS = 2 +PUSH_DUP_OPCODE_GAS = 3 + +""" +Helper functions +""" + + +def calculate_initcode_word_cost(length: int) -> int: + """ + Calculates the added word cost on contract creation added by the + length of the initcode based on the formula: + INITCODE_WORD_COST * ceil(len(initcode) / 32) + """ + return INITCODE_WORD_COST * ceiling_division(length, 32) + + +def calculate_create2_word_cost(length: int) -> int: + """ + Calculates the added word cost on contract creation added by the + hashing of the initcode during create2 contract creation. + """ + return KECCAK_WORD_COST * ceiling_division(length, 32) + + +def calculate_create_tx_intrinsic_cost( + initcode: Initcode, eip_3860_active: bool +) -> int: + """ + Calcultes the intrinsic gas cost of a transaction that contains initcode + and creates a contract + """ + cost = ( + BASE_TRANSACTION_GAS # G_transaction + + CREATE_CONTRACT_BASE_GAS # G_transaction_create + + eip_2028_transaction_data_cost( + initcode.assemble() + ) # Transaction calldata cost + ) + if eip_3860_active: + cost += calculate_initcode_word_cost(len(initcode.assemble())) + return cost + + +def calculate_create_tx_execution_cost( + initcode: Initcode, + eip_3860_active: bool, +) -> int: + """ + Calculates the total execution gas cost of a transaction that + contains initcode and creates a contract + """ + cost = calculate_create_tx_intrinsic_cost( + initcode=initcode, eip_3860_active=eip_3860_active + ) + cost += initcode.deployment_gas + cost += initcode.execution_gas + return cost + + +""" +Initcode templates used throughout the tests +""" +INITCODE_ONES_MAX_LIMIT = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=MAX_INITCODE_SIZE, + padding_byte=0x01, + name="max_size_ones_initcode", +) + +INITCODE_ZEROS_MAX_LIMIT = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=MAX_INITCODE_SIZE, + padding_byte=0x00, + name="max_size_zeros_initcode", +) + +INITCODE_ONES_OVER_LIMIT = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=MAX_INITCODE_SIZE + 1, + padding_byte=0x01, + name="over_limit_ones_initcode", +) + +INITCODE_ZEROS_OVER_LIMIT = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=MAX_INITCODE_SIZE + 1, + padding_byte=0x00, + name="over_limit_zeros_initcode", +) + +INITCODE_ZEROS_32_BYTES = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=32, + padding_byte=0x00, + name="32_bytes_initcode", +) + +INITCODE_ZEROS_33_BYTES = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=33, + padding_byte=0x00, + name="33_bytes_initcode", +) + +INITCODE_ZEROS_49120_BYTES = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=49120, + padding_byte=0x00, + name="49120_bytes_initcode", +) + +INITCODE_ZEROS_49121_BYTES = Initcode( + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=49121, + padding_byte=0x00, + name="49121_bytes_initcode", +) + +EMPTY_INITCODE = Initcode( + deploy_code=bytes(), + name="empty_initcode", +) +EMPTY_INITCODE.bytecode = bytes() +EMPTY_INITCODE.deployment_gas = 0 +EMPTY_INITCODE.execution_gas = 0 + +SINGLE_BYTE_INITCODE = Initcode( + deploy_code=bytes(), + name="single_byte_initcode", +) +SINGLE_BYTE_INITCODE.bytecode = bytes([0x00]) +SINGLE_BYTE_INITCODE.deployment_gas = 0 +SINGLE_BYTE_INITCODE.execution_gas = 0 + +""" +Test cases using a contract creating transaction +""" + + +def generate_tx_initcode_limit_test_cases( + initcode: Initcode, + eip_3860_active: bool, +): + """ + Generates a BlockchainTest based on the provided `initcode` and + its length. + """ + env = Environment() + + pre = { + TestAddress: Account(balance=1000000000000000000000), + } + + post: Dict[Any, Any] = {} + + created_contract_address = compute_create_address( + address=TestAddress, + nonce=0, + ) + + tx = Transaction( + nonce=0, + to=None, + data=initcode, + gas_limit=10000000, + gas_price=10, + ) + + block = Block(txs=[tx]) + + if len(initcode.assemble()) > MAX_INITCODE_SIZE and eip_3860_active: + # Initcode is above the max size, tx inclusion in the block makes + # it invalid. + post[created_contract_address] = Account.NONEXISTENT + tx.error = "max initcode size exceeded" + block.exception = "max initcode size exceeded" + else: + # Initcode is at or below the max size, tx inclusion in the block + # is ok and the contract is successfully created. + post[created_contract_address] = Account(code="0x00") + + yield BlockchainTest( + pre=pre, + post=post, + blocks=[block], + genesis_environment=env, + name=f"initcode_tx_{initcode.name}", + ) + + +@test_from(fork="shanghai", eips=[3860]) +def test_initcode_limit_contract_creating_tx(fork): + """ + Test creating a contract using a transaction using an initcode that is + on/over the max allowed limit. + """ + yield from generate_tx_initcode_limit_test_cases( + initcode=INITCODE_ZEROS_MAX_LIMIT, + eip_3860_active=True, + ) + yield from generate_tx_initcode_limit_test_cases( + initcode=INITCODE_ONES_MAX_LIMIT, + eip_3860_active=True, + ) + yield from generate_tx_initcode_limit_test_cases( + initcode=INITCODE_ZEROS_OVER_LIMIT, + eip_3860_active=True, + ) + yield from generate_tx_initcode_limit_test_cases( + initcode=INITCODE_ONES_OVER_LIMIT, + eip_3860_active=True, + ) + + +def generate_gas_cost_test_cases( + initcode: Initcode, + eip_3860_active: bool, +): + """ + Generates 4 test cases that verify the intrinsic gas cost of a + contract creating transaction: + 1) Test with exact intrinsic gas, contract create fails, + but tx is valid. + 2) Test with exact intrinsic gas minus one, contract create fails + and tx is invalid. + 3) Test with exact execution gas minus one, contract create fails, + but tx is valid. + 4) Test with exact execution gas, contract create succeeds. + + Initcode must be within valid EIP-3860 length. + """ + # Common setup to all test cases + env = Environment() + pre = { + TestAddress: Account(balance=1000000000000000000000), + } + post: Dict[Any, Any] = {} + created_contract_address = compute_create_address( + address=TestAddress, + nonce=0, + ) + + # Calculate both the intrinsic tx gas cost and the total execution + # gas cost, used throughout all tests + exact_tx_intrinsic_gas = calculate_create_tx_intrinsic_cost( + initcode, eip_3860_active + ) + exact_tx_execution_gas = calculate_create_tx_execution_cost( + initcode, + eip_3860_active, + ) + + """ + Test case 1: Test with exact intrinsic gas, contract create fails, + but tx is valid. + """ + tx = Transaction( + nonce=0, + to=None, + data=initcode, + gas_limit=exact_tx_intrinsic_gas, + gas_price=10, + ) + block = Block(txs=[tx]) + if exact_tx_execution_gas == exact_tx_intrinsic_gas: + # Special scenario where the execution of the initcode and + # gas cost to deploy are zero + post[created_contract_address] = Account(code=initcode.deploy_code) + else: + post[created_contract_address] = Account.NONEXISTENT + + yield BlockchainTest( + pre=pre, + post=post, + blocks=[block], + genesis_environment=env, + name=f"{initcode.name}_tx_exact_intrinsic_gas", + ) + + """ + Test case 2: Test with exact intrinsic gas minus one, contract create fails + and tx is invalid. + """ + tx = Transaction( + nonce=0, + to=None, + data=initcode, + gas_limit=exact_tx_intrinsic_gas - 1, + gas_price=10, + error="intrinsic gas too low", + ) + block = Block( + txs=[tx], + exception="intrinsic gas too low", + ) + post[created_contract_address] = Account.NONEXISTENT + + yield BlockchainTest( + pre=pre, + post=post, + blocks=[block], + genesis_environment=env, + name=f"{initcode.name}_tx_under_intrinsic_gas", + ) + + """ + Test case 3: Test with exact execution gas minus one, contract create + fails, but tx is valid. + """ + if exact_tx_execution_gas == exact_tx_intrinsic_gas: + # Test case is virtually equal to previous + pass + else: + tx = Transaction( + nonce=0, + to=None, + data=initcode, + gas_limit=exact_tx_execution_gas - 1, + gas_price=10, + ) + block = Block(txs=[tx]) + post[created_contract_address] = Account.NONEXISTENT + + yield BlockchainTest( + pre=pre, + post=post, + blocks=[block], + genesis_environment=env, + name=f"{initcode.name}_tx_under_execution_gas", + ) + + """ + Test case 4: Test with exact execution gas, contract create succeeds. + """ + tx = Transaction( + nonce=0, + to=None, + data=initcode, + gas_limit=exact_tx_execution_gas, + gas_price=10, + ) + block = Block(txs=[tx]) + post[created_contract_address] = Account(code=initcode.deploy_code) + + yield BlockchainTest( + pre=pre, + post=post, + blocks=[block], + genesis_environment=env, + name=f"{initcode.name}_tx_exact_execution_gas", + ) + + +@test_from(fork="shanghai", eips=[3860]) +def test_initcode_limit_contract_creating_tx_gas_usage(fork): + """ + Test EIP-3860 Limit Initcode Gas Usage for a contract + creating transaction, using different initcode lengths. + """ + yield from generate_gas_cost_test_cases( + initcode=INITCODE_ZEROS_MAX_LIMIT, + eip_3860_active=True, + ) + + yield from generate_gas_cost_test_cases( + initcode=INITCODE_ONES_MAX_LIMIT, + eip_3860_active=True, + ) + + # Test cases to verify the initcode word cost limits + + yield from generate_gas_cost_test_cases( + initcode=EMPTY_INITCODE, + eip_3860_active=True, + ) + + yield from generate_gas_cost_test_cases( + initcode=SINGLE_BYTE_INITCODE, + eip_3860_active=True, + ) + + yield from generate_gas_cost_test_cases( + initcode=INITCODE_ZEROS_32_BYTES, + eip_3860_active=True, + ) + + yield from generate_gas_cost_test_cases( + initcode=INITCODE_ZEROS_33_BYTES, + eip_3860_active=True, + ) + + yield from generate_gas_cost_test_cases( + initcode=INITCODE_ZEROS_49120_BYTES, + eip_3860_active=True, + ) + + yield from generate_gas_cost_test_cases( + initcode=INITCODE_ZEROS_49121_BYTES, + eip_3860_active=True, + ) + + +""" +Test cases using the CREATE opcode +""" + + +def generate_create_opcode_initcode_test_cases( + opcode: str, + initcode: Initcode, + eip_3860_active: bool, +): + """ + Generates a StateTest using the `CREATE`/`CREATE2` opcode based on the + provided `initcode`, its executing cost, and the deployed code. + """ + env = Environment() + + if opcode == "create": + code = Yul( + """ + { + let contract_length := calldatasize() + calldatacopy(0, 0, contract_length) + let gas1 := gas() + let res := create(0, 0, contract_length) + let gas2 := gas() + sstore(0, res) + sstore(1, sub(gas1, gas2)) + } + """ + ) + created_contract_address = compute_create_address( + address=0x100, + nonce=1, + ) + + elif opcode == "create2": + code = Yul( + """ + { + let contract_length := calldatasize() + calldatacopy(0, 0, contract_length) + let gas1 := gas() + let res := create2(0, 0, contract_length, 0) + let gas2 := gas() + sstore(0, res) + sstore(1, sub(gas1, gas2)) + } + """ + ) + created_contract_address = compute_create2_address( + address=0x100, + salt=0, + initcode=initcode.assemble(), + ) + else: + raise Exception("invalid opcode for generator") + + pre = { + TestAddress: Account(balance=1000000000000000000000), + to_address(0x100): Account( + code=code, + nonce=1, + ), + } + + post: Dict[Any, Any] = {} + + tx = Transaction( + nonce=0, + to=to_address(0x100), + data=initcode, + gas_limit=10000000, + gas_price=10, + ) + + # Calculate the expected gas of the contract creation operation + expected_gas_usage = ( + CREATE_CONTRACT_BASE_GAS + GAS_OPCODE_GAS + (3 * PUSH_DUP_OPCODE_GAS) + ) + if opcode == "create2": + # Extra PUSH operation + expected_gas_usage += PUSH_DUP_OPCODE_GAS + + if len(initcode.assemble()) > MAX_INITCODE_SIZE and eip_3860_active: + post[created_contract_address] = Account.NONEXISTENT + post[to_address(0x100)] = Account( + nonce=1, + storage={ + 0: 0, + 1: expected_gas_usage, + }, + ) + else: + # The initcode is only executed if the length check succeeds + expected_gas_usage += initcode.execution_gas + # The code is only deployed if the length check succeeds + expected_gas_usage += initcode.deployment_gas + + if opcode == "create2": + # CREATE2 hashing cost should only be deducted if the initcode + # does not exceed the max length + expected_gas_usage += calculate_create2_word_cost( + len(initcode.assemble()) + ) + + if eip_3860_active: + # Initcode word cost is only deducted if the length check succeeds + expected_gas_usage += calculate_initcode_word_cost( + len(initcode.assemble()) + ) + + post[created_contract_address] = Account(code=initcode.deploy_code) + post[to_address(0x100)] = Account( + nonce=2, + storage={ + 0: created_contract_address, + 1: expected_gas_usage, + }, + ) + + yield StateTest( + env=env, + pre=pre, + post=post, + txs=[tx], + name=f"{opcode}_opcode_{initcode.name}", + ) + + +@test_from(fork="shanghai", eips=[3860]) +def test_initcode_limit_create_opcode(fork): + """ + Test creating a contract using the CREATE opcode with an initcode that is + on/over the max allowed limit. + """ + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ZEROS_MAX_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ONES_MAX_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ZEROS_OVER_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ONES_OVER_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=EMPTY_INITCODE, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=SINGLE_BYTE_INITCODE, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ZEROS_32_BYTES, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ZEROS_33_BYTES, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ZEROS_49120_BYTES, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create", + initcode=INITCODE_ZEROS_49121_BYTES, + eip_3860_active=True, + ) + + +@test_from(fork="shanghai", eips=[3860]) +def test_initcode_limit_create2_opcode(fork): + """ + Test creating a contract using the CREATE2 opcode with an initcode that is + on/over the max allowed limit. + """ + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ZEROS_MAX_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ONES_MAX_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ZEROS_OVER_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ONES_OVER_LIMIT, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=EMPTY_INITCODE, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=SINGLE_BYTE_INITCODE, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ZEROS_32_BYTES, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ZEROS_33_BYTES, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ZEROS_49120_BYTES, + eip_3860_active=True, + ) + + yield from generate_create_opcode_initcode_test_cases( + opcode="create2", + initcode=INITCODE_ZEROS_49121_BYTES, + eip_3860_active=True, + ) diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 7c05bb3d1f6..af887708806 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -3,21 +3,25 @@ tests. """ -from .code import Code, Yul +from .code import Code, CodeGasMeasure, Initcode, Yul from .common import ( Account, Block, - CodeGasMeasure, Environment, JSONEncoder, TestAddress, Transaction, + ceiling_division, + compute_create2_address, + compute_create_address, + eip_2028_transaction_data_cost, to_address, to_hash, ) from .filling.decorators import test_from, test_only from .filling.fill import fill_test from .spec import BlockchainTest, StateTest +from .vm.fork import is_fork __all__ = ( "Account", @@ -26,16 +30,22 @@ "Code", "CodeGasMeasure", "Environment", + "Initcode", "JSONEncoder", "StateTest", "Storage", "TestAddress", "Transaction", "Yul", + "ceiling_division", + "compute_create2_address", + "compute_create_address", "fill_test", + "is_fork", "test_from", "test_only", "to_address", "to_hash", + "eip_2028_transaction_data_cost", "verify_post_alloc", ) diff --git a/src/ethereum_test_tools/code/__init__.py b/src/ethereum_test_tools/code/__init__.py index 8e2fbccb90f..57818006918 100644 --- a/src/ethereum_test_tools/code/__init__.py +++ b/src/ethereum_test_tools/code/__init__.py @@ -2,10 +2,13 @@ Code related utilities and classes. """ from .code import Code, code_to_bytes, code_to_hex +from .generators import CodeGasMeasure, Initcode from .yul import Yul __all__ = ( "Code", + "CodeGasMeasure", + "Initcode", "Yul", "code_to_bytes", "code_to_hex", diff --git a/src/ethereum_test_tools/code/code.py b/src/ethereum_test_tools/code/code.py index c17b9f886a9..62f4a310d67 100644 --- a/src/ethereum_test_tools/code/code.py +++ b/src/ethereum_test_tools/code/code.py @@ -2,31 +2,31 @@ Code object that is an interface to different assembler/compiler backends. """ +from dataclasses import dataclass from re import sub -from typing import Union +from typing import Optional, Union -class Code(object): +@dataclass(kw_only=True) +class Code: """ Generic code object. """ - bytecode: bytes | None = None - - def __init__(self, code: bytes | str | None): - if code is not None: - if type(code) is bytes: - self.bytecode = code - elif type(code) is str: - if code.startswith("0x"): - code = code[2:] - self.bytecode = bytes.fromhex(code) - else: - raise TypeError("code has invalid type") + bytecode: Optional[bytes] = None + """ + bytes array that represents the bytecode of this object. + """ + name: Optional[str] = None + """ + Name used to describe this code. + Usually used to add extra information to a test case. + """ def assemble(self) -> bytes: """ - Assembles using `eas`. + Transform the Code object into bytes. + Normally will be overriden by the classes that inherit this class. """ if self.bytecode is None: return bytes() @@ -37,13 +37,13 @@ def __add__(self, other: Union[str, bytes, "Code"]) -> "Code": """ Adds two code objects together, by converting both to bytes first. """ - return Code(code_to_bytes(self) + code_to_bytes(other)) + return Code(bytecode=(code_to_bytes(self) + code_to_bytes(other))) def __radd__(self, other: Union[str, bytes, "Code"]) -> "Code": """ Adds two code objects together, by converting both to bytes first. """ - return Code(code_to_bytes(other) + code_to_bytes(self)) + return Code(bytecode=(code_to_bytes(other) + code_to_bytes(self))) def code_to_bytes(code: str | bytes | Code) -> bytes: diff --git a/src/ethereum_test_tools/code/generators.py b/src/ethereum_test_tools/code/generators.py new file mode 100644 index 00000000000..fcb0df680d5 --- /dev/null +++ b/src/ethereum_test_tools/code/generators.py @@ -0,0 +1,186 @@ +""" +Code generating classes and functions. +""" + +from dataclasses import dataclass +from typing import Optional + +from ..common.helpers import ceiling_division +from .code import Code, code_to_bytes + +GAS_PER_DEPLOYED_CODE_BYTE = 0xC8 + + +class Initcode(Code): + """ + Helper class used to generate initcode for the specified deployment code. + + The execution gas cost of the initcode is calculated, and also the + deployment gas costs for the deployed code. + + The initcode can be padded to a certain length if necessary, which + does not affect the deployed code. + + Other costs such as the CREATE2 hashing costs or the initcode_word_cost + of EIP-3860 are *not* taken into account by any of these calculated + costs. + """ + + deploy_code: bytes | str | Code + """ + Bytecode to be deployed by the initcode. + """ + execution_gas: int + """ + Gas cost of executing the initcode, without considering deployment gas + costs. + """ + deployment_gas: int + """ + Gas cost of deploying the cost, subtracted after initcode execution, + """ + + def __init__( + self, + *, + deploy_code: str | bytes | Code, + initcode_length: Optional[int] = None, + padding_byte: int = 0x00, + name: Optional[str] = None, + ): + """ + Generate legacy initcode that inits a contract with the specified code. + The initcode can be padded to a specified length for testing purposes. + """ + self.execution_gas = 0 + self.deploy_code = deploy_code + deploy_code_bytes = code_to_bytes(self.deploy_code) + code_length = len(deploy_code_bytes) + + initcode = bytearray() + + # PUSH2: length= + initcode.append(0x61) + initcode += code_length.to_bytes(length=2, byteorder="big") + self.execution_gas += 3 + + # PUSH1: offset=0 + initcode.append(0x60) + initcode.append(0x00) + self.execution_gas += 3 + + # DUP2 + initcode.append(0x81) + self.execution_gas += 3 + + # PUSH1: initcode_length=11 (constant) + initcode.append(0x60) + initcode.append(0x0B) + self.execution_gas += 3 + + # DUP3 + initcode.append(0x82) + self.execution_gas += 3 + + # CODECOPY: destinationOffset=0, offset=0, length + initcode.append(0x39) + self.execution_gas += ( + 3 + + (3 * ceiling_division(code_length, 32)) + + (3 * code_length) + + ((code_length * code_length) // 512) + ) + + # RETURN: offset=0, length + initcode.append(0xF3) + self.execution_gas += 0 + + pre_padding_bytes = bytes(initcode) + deploy_code_bytes + + if initcode_length is not None: + if len(pre_padding_bytes) > initcode_length: + raise Exception("Invalid specified length for initcode") + + padding_bytes = bytes( + [padding_byte] * (initcode_length - len(pre_padding_bytes)) + ) + else: + padding_bytes = bytes() + + self.deployment_gas = GAS_PER_DEPLOYED_CODE_BYTE * len( + deploy_code_bytes + ) + + super().__init__(bytecode=pre_padding_bytes + padding_bytes, name=name) + + +@dataclass(kw_only=True) +class CodeGasMeasure(Code): + """ + Helper class used to generate bytecode that measures gas usage of a + bytecode, taking into account and subtracting any extra overhead gas costs + required to execute. + By default, the result gas calculation is saved to storage key 0. + """ + + code: bytes | str | Code + """ + Bytecode to be executed to measure the gas usage. + """ + overhead_cost: int = 0 + """ + Extra gas cost to be subtracted from extra operations. + """ + extra_stack_items: int = 0 + """ + Extra stack items that remain at the end of the execution. + To be considered when subtracting the value of the previous GAS operation, + and to be popped at the end of the execution. + """ + sstore_key: int = 0 + """ + Storage key to save the gas used. + """ + + def assemble(self) -> bytes: + """ + Assemble the bytecode that measures gas usage. + """ + res = bytes() + res += bytes( + [ + 0x5A, # GAS + ] + ) + res += code_to_bytes(self.code) # Execute code to measure its gas cost + res += bytes( + [ + 0x5A, # GAS + ] + ) + # We need to swap and pop for each extra stack item that remained from + # the execution of the code + res += ( + bytes( + [ + 0x90, # SWAP1 + 0x50, # POP + ] + ) + * self.extra_stack_items + ) + res += bytes( + [ + 0x90, # SWAP1 + 0x03, # SUB + 0x60, # PUSH1 + self.overhead_cost + 2, # Overhead cost + GAS opcode price + 0x90, # SWAP1 + 0x03, # SUB + 0x60, # PUSH1 + self.sstore_key, # -> SSTORE key + 0x55, # SSTORE + 0x00, # STOP + ] + ) + return res diff --git a/src/ethereum_test_tools/common/__init__.py b/src/ethereum_test_tools/common/__init__.py index 1ad4fc235ea..899cdfcfa4d 100644 --- a/src/ethereum_test_tools/common/__init__.py +++ b/src/ethereum_test_tools/common/__init__.py @@ -8,7 +8,14 @@ TestAddress, TestPrivateKey, ) -from .helpers import CodeGasMeasure, to_address, to_hash +from .helpers import ( + ceiling_division, + compute_create2_address, + compute_create_address, + eip_2028_transaction_data_cost, + to_address, + to_hash, +) from .types import ( Account, Block, @@ -29,7 +36,6 @@ "AddrAA", "AddrBB", "Block", - "CodeGasMeasure", "EmptyTrieRoot", "Environment", "Fixture", @@ -40,6 +46,10 @@ "TestAddress", "TestPrivateKey", "Transaction", + "ceiling_division", + "compute_create2_address", + "compute_create_address", + "eip_2028_transaction_data_cost", "str_or_none", "to_address", "to_hash", diff --git a/src/ethereum_test_tools/common/helpers.py b/src/ethereum_test_tools/common/helpers.py index cf833033440..0cbb5c8aa90 100644 --- a/src/ethereum_test_tools/common/helpers.py +++ b/src/ethereum_test_tools/common/helpers.py @@ -2,9 +2,77 @@ Helper functions/classes used to generate Ethereum tests. """ -from dataclasses import dataclass +from ethereum.crypto.hash import keccak256 +from ethereum.rlp import encode -from ..code import Code, code_to_bytes +""" +Helper functions +""" + + +def ceiling_division(a: int, b: int) -> int: + """ + Calculates the ceil without using floating point. + Used by many of the EVM's formulas + """ + return -(a // -b) + + +def compute_create_address(address: str | int, nonce: int) -> str: + """ + Compute address of the resulting contract created using a transaction + or the `CREATE` opcode. + """ + if type(address) is str: + if address.startswith("0x"): + address = address[2:] + address_bytes = bytes.fromhex(address) + elif type(address) is int: + address_bytes = address.to_bytes(length=20, byteorder="big") + if nonce == 0: + nonce_bytes = bytes() + else: + nonce_bytes = nonce.to_bytes(length=1, byteorder="big") + hash = keccak256(encode([address_bytes, nonce_bytes])) + return "0x" + hash[-20:].hex() + + +def compute_create2_address( + address: str | int, salt: int, initcode: bytes +) -> str: + """ + Compute address of the resulting contract created using the `CREATE2` + opcode. + """ + ff = bytes([0xFF]) + if type(address) is str: + if address.startswith("0x"): + address = address[2:] + address_bytes = bytes.fromhex(address) + elif type(address) is int: + address_bytes = address.to_bytes(length=20, byteorder="big") + salt_bytes = salt.to_bytes(length=32, byteorder="big") + initcode_hash = keccak256(initcode) + hash = keccak256(ff + address_bytes + salt_bytes + initcode_hash) + return "0x" + hash[-20:].hex() + + +def eip_2028_transaction_data_cost(data: bytes | str) -> int: + """ + Calculates the cost of a given data as part of a transaction, based on the + costs specified in EIP-2028: https://eips.ethereum.org/EIPS/eip-2028 + """ + if type(data) is str: + if data.startswith("0x"): + data = data[2:] + data = bytes.fromhex(data) + cost = 0 + for b in data: + if b == 0: + cost += 4 + else: + cost += 16 + return cost def to_address(input: int | str) -> str: @@ -29,75 +97,3 @@ def to_hash(input: int | str) -> str: if type(input) is int: return "0x" + input.to_bytes(32, "big").hex() raise Exception("invalid type to convert to hash") - - -@dataclass(kw_only=True) -class CodeGasMeasure(Code): - """ - Helper class used to generate bytecode that measures gas usage of a - bytecode, taking into account and subtracting any extra overhead gas costs - required to execute. - By default, the result gas calculation is saved to storage key 0. - """ - - code: bytes | str | Code - """ - Bytecode to be executed to measure the gas usage. - """ - overhead_cost: int = 0 - """ - Extra gas cost to be subtracted from extra operations. - """ - extra_stack_items: int = 0 - """ - Extra stack items that remain at the end of the execution. - To be considered when subtracting the value of the previous GAS operation, - and to be popped at the end of the execution. - """ - sstore_key: int = 0 - """ - Storage key to save the gas used. - """ - - def assemble(self) -> bytes: - """ - Assemble the bytecode that measures gas usage. - """ - res = bytes() - res += bytes( - [ - 0x5A, # GAS - ] - ) - res += code_to_bytes(self.code) # Execute code to measure its gas cost - res += bytes( - [ - 0x5A, # GAS - ] - ) - # We need to swap and pop for each extra stack item that remained from - # the execution of the code - res += ( - bytes( - [ - 0x90, # SWAP1 - 0x50, # POP - ] - ) - * self.extra_stack_items - ) - res += bytes( - [ - 0x90, # SWAP1 - 0x03, # SUB - 0x60, # PUSH1 - self.overhead_cost + 2, # Overhead cost + GAS opcode price - 0x90, # SWAP1 - 0x03, # SUB - 0x60, # PUSH1 - self.sstore_key, # -> SSTORE key - 0x55, # SSTORE - 0x00, # STOP - ] - ) - return res diff --git a/src/ethereum_test_tools/common/types.py b/src/ethereum_test_tools/common/types.py index 64f6ba9019c..fba70561f0a 100644 --- a/src/ethereum_test_tools/common/types.py +++ b/src/ethereum_test_tools/common/types.py @@ -122,10 +122,15 @@ def __init__(self, key: int, want: int, got: int, *args): def __str__(self): """Print exception string""" - return "incorrect value for key {0}: want {1}, got {2}".format( + return ( + "incorrect value for key {0}: want {1} (dec:{2})," + + " got {3} (dec:{4})" + ).format( Storage.key_value_to_string(self.key), Storage.key_value_to_string(self.want), + self.want, Storage.key_value_to_string(self.got), + self.got, ) @staticmethod diff --git a/src/ethereum_test_tools/filling/fill.py b/src/ethereum_test_tools/filling/fill.py index ae28bfa7625..a1126504490 100644 --- a/src/ethereum_test_tools/filling/fill.py +++ b/src/ethereum_test_tools/filling/fill.py @@ -1,6 +1,7 @@ """ Filler object definitions. """ +from copy import copy from typing import List, Mapping, Optional from evm_block_builder import BlockBuilder @@ -40,9 +41,18 @@ def fill_test( genesis = test.make_genesis(b11r, t8n, fork) - (blocks, head, alloc) = test.make_blocks( - b11r, t8n, genesis, fork, reward=get_reward(fork), eips=eips - ) + try: + (blocks, head, alloc) = test.make_blocks( + b11r, + t8n, + genesis, + fork, + reward=get_reward(fork), + eips=eips, + ) + except Exception as e: + print(f"Exception during test '{test.name}'") + raise e fixture = Fixture( blocks=blocks, @@ -51,7 +61,7 @@ def fill_test( fork="+".join([fork] + [str(eip) for eip in eips]) if eips is not None else fork, - pre_state=test.pre, + pre_state=copy(test.pre), post_state=alloc, seal_engine=engine, name=test.name, diff --git a/src/ethereum_test_tools/tests/test_code.py b/src/ethereum_test_tools/tests/test_code.py index a3ab411edc4..7b7cdc650d3 100644 --- a/src/ethereum_test_tools/tests/test_code.py +++ b/src/ethereum_test_tools/tests/test_code.py @@ -2,23 +2,29 @@ Test suite for `ethereum_test.code` module. """ -from ..code import Code, Yul +import pytest + +from ..code import Code, Initcode, Yul, code_to_bytes def test_code(): """ Test `ethereum_test.types.code`. """ - assert Code("").assemble() == bytes() - assert Code("0x").assemble() == bytes() - assert Code("0x01").assemble() == bytes.fromhex("01") - assert Code("01").assemble() == bytes.fromhex("01") + assert code_to_bytes("") == bytes() + assert code_to_bytes("0x") == bytes() + assert code_to_bytes("0x01") == bytes.fromhex("01") + assert code_to_bytes("01") == bytes.fromhex("01") - assert (Code("0x01") + "0x02").assemble() == bytes.fromhex("0102") - assert ("0x01" + Code("0x02")).assemble() == bytes.fromhex("0102") - assert ("0x01" + Code("0x02") + "0x03").assemble() == bytes.fromhex( - "010203" - ) + assert ( + Code(bytecode=code_to_bytes("0x01")) + "0x02" + ).assemble() == bytes.fromhex("0102") + assert ( + "0x01" + Code(bytecode=code_to_bytes("0x02")) + ).assemble() == bytes.fromhex("0102") + assert ( + "0x01" + Code(bytecode=code_to_bytes("0x02")) + "0x03" + ).assemble() == bytes.fromhex("010203") def test_yul(): @@ -99,3 +105,69 @@ def test_yul(): expected_bytecode += bytes.fromhex("55") assert Yul(long_code).assemble() == expected_bytecode + + +@pytest.mark.parametrize( + "initcode,bytecode", + [ + ( + Initcode(deploy_code=bytes()), + bytes( + [ + 0x61, + 0x00, + 0x00, + 0x60, + 0x00, + 0x81, + 0x60, + 0x0B, + 0x82, + 0x39, + 0xF3, + ] + ), + ), + ( + Initcode(deploy_code=bytes(), initcode_length=20), + bytes( + [ + 0x61, + 0x00, + 0x00, + 0x60, + 0x00, + 0x81, + 0x60, + 0x0B, + 0x82, + 0x39, + 0xF3, + ] + + [0x00] * 9 # padding + ), + ), + ( + Initcode(deploy_code=bytes([0x00]), initcode_length=20), + bytes( + [ + 0x61, + 0x00, + 0x01, + 0x60, + 0x00, + 0x81, + 0x60, + 0x0B, + 0x82, + 0x39, + 0xF3, + ] + + [0x00] + + [0x00] * 8 # padding + ), + ), + ], +) +def test_initcode(initcode: Initcode, bytecode: bytes): + assert initcode.assemble() == bytecode diff --git a/src/ethereum_test_tools/vm/fork.py b/src/ethereum_test_tools/vm/fork.py index e08cac11087..0147543c39c 100644 --- a/src/ethereum_test_tools/vm/fork.py +++ b/src/ethereum_test_tools/vm/fork.py @@ -22,6 +22,7 @@ "london", "arrow glacier", "merged", + "shanghai", ] @@ -70,6 +71,18 @@ def is_merged(fork: str) -> bool: return i >= forks.index("merged") +def is_fork(fork: str, which: str) -> bool: + """ + Returns `True` if `fork` is `which` or beyond, `False otherwise. + """ + fork_lower = fork.lower() + if fork_lower not in forks: + return False + + i = forks.index(fork_lower) + return i >= forks.index(which.lower()) + + def get_reward(fork: str) -> int: """ Returns the expected reward amount in wei of a given fork diff --git a/src/evm_transition_tool/__init__.py b/src/evm_transition_tool/__init__.py index 966c3146633..81dd5b0dd53 100644 --- a/src/evm_transition_tool/__init__.py +++ b/src/evm_transition_tool/__init__.py @@ -230,6 +230,7 @@ def version(self) -> str: "london": "London", "arrow glacier": "ArrowGlacier", "merged": "Merged", + "shanghai": "Shanghai", } fork_list = list(fork_map.keys()) diff --git a/whitelist.txt b/whitelist.txt index b69af8a1fb5..97c810e0bc5 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -37,6 +37,7 @@ utils validator vm +byteorder delitem dirname fromhex @@ -50,6 +51,7 @@ lllc solc yul +keccak sha3 stop @@ -88,6 +90,7 @@ balance origin caller callvalue +calldata calldataload calldatasize calldatacopy @@ -96,6 +99,7 @@ codecopy gasprice extcodesize extcodecopy +initcode returndatasize returndatacopy extcodehash