diff --git a/specs/opcode/41COINBASE-45GASLIMIT_48BASEFEE.md b/specs/opcode/41COINBASE_45GASLIMIT_48BASEFEE.md similarity index 100% rename from specs/opcode/41COINBASE-45GASLIMIT_48BASEFEE.md rename to specs/opcode/41COINBASE_45GASLIMIT_48BASEFEE.md diff --git a/specs/opcode/F0CREATE_F5CREATE2.md b/specs/opcode/F0CREATE_F5CREATE2.md new file mode 100644 index 000000000..9ea25b466 --- /dev/null +++ b/specs/opcode/F0CREATE_F5CREATE2.md @@ -0,0 +1,96 @@ +# CREATE and CREATE2 opcodes + +## Procedure + +### EVM behavior + +The `CREATE` and `CREATE2` opcodes create a new call context at a new address. +They transfer the specified amount of ether from the caller address to the new address and then execute the specified initialization code. +The bytecode of the new address will be updated to be the return bytes of this initialization code. + +The following values are popped from the stack: +1. `value` - The amount of ether to transfer from the caller to the new address. +2. `offset` - The memory offset in the caller's memory where the initialization code starts. +3. `size` - The length of the initialization code, in bytes. +4. `salt` - Only for `CREATE2`: salt for the hash that will determine the new address. +If the initialization call is successful, the new address will be pushed to the stack. +If not, 0 will be pushed instead. + +The gadget does several things in the caller's call context: +1. Expands memory +2. Add `new_address` to the access list +3. Calculate gas cost for this step. +4. Uses EIP150 to calculate how much gas will remain in the caller's context. + +The memory size is calculated as follows: + +``` +calc_memory_size(offset, length) := 0 if length == 0 else ceil((offset + length) / 32) +``` + +`new_address` is calculated as follows: + +``` +new_address := keccak256([0xff] + caller_address + salt + keccak256(initialization_bytecode)) if op_id == CREATE2 else rlp([caller_address, caller_address.nonce]) +``` + +The memory expansion gas cost is: + +``` +MEMORY_EXPANSION_QUAD_DENOMINATOR := 512 +MEMORY_EXPANSION_LINEAR_COEFF := 3 +calc_memory_cost(memory_size) := MEMORY_EXPANSION_LINEAR_COEFF * memory_size + floor(memory_size * memory_size / MEMORY_EXPANSION_QUAD_DENOMINATOR) +``` + +The gas cost charged for the additional `memory_size` is: + +``` +next.memory_size := max( + curr.memory_size, + memory_size(offset, length), +) +memory_expansion_gas_cost := calc_memory_cost(next.memory_size) - memory_cost(curr.memory_size) +``` + +The `gas_cost` for the step is: + +``` +GAS_COST_CREATE := 32000 +KECCAK_BYTE_GAS_COST : 6 + +keccak_gas_cost = KECCAK_BYTE_GAS_COST * size if op_id == CREATE2 else 0 +gas_cost = ( + GAS_COST_CREATE + + memory_expansion_gas_cost + + keccak_gas_cost +) +``` + +The `callee_gas_left` for new context by rule in EIP150 is calculated like this: + +``` +gas_available := curr.gas_left - gas_cost +callee_gas_left := min(gas_available - floor(gas_available / 64), gas) +``` + +In the initialization call context, it does: + +1. Transfer `value` from the caller address to the new address +2. Executes the initialization bytecode +3. Update the bytecode of the new address to be the `return_data` of execution + +### Circuit behavior + +The circuit takes the starting `rw_counter` as next call's `call_id` to make sure each call has a unique `call_id`. + +It pops 3 words for `CREATE` and 4 words for `CREATE2` from the stack. +In both cases, it then pushes 1 word to the stack. + +It then checks that `new_call.is_persistent = caller.is_persistent and new_call.is_success`. +If the caller is not persistent, we need to propagate the `rw_counter_end_of_reversion` to make sure every state update in the new call has a corresponding reversion. + +It stores the current call context by writing the current values `rw_table` and checks that the new call context is setup correctly by reading to `rw_table`, and then does a step state transition to the call and begins the execution o the initialization bytecode. + +## Code + +Please refer to `src/zkevm_specs/evm/execution/create.py`. diff --git a/src/zkevm_specs/evm/execution/create.py b/src/zkevm_specs/evm/execution/create.py new file mode 100644 index 000000000..646663ce7 --- /dev/null +++ b/src/zkevm_specs/evm/execution/create.py @@ -0,0 +1,57 @@ +from ...util import ( + FQ, + GAS_COST_ACCOUNT_COLD_ACCESS, + GAS_COST_CALL_WITH_VALUE, + GAS_COST_NEW_ACCOUNT, + GAS_COST_WARM_ACCESS, + N_BYTES_GAS, + CALL_CREATE_DEPTH, +) +from ..instruction import Instruction +from ..opcode import Opcode +from ..table import CallContextFieldTag, AccountFieldTag + + +def create(instruction: Instruction): + opcode = instruction.opcode_lookup(True) + is_create, is_create2 = instruction.select(opcode, (Opcode.CREATE, Opcode.CREATE2)) + instruction.responsible_opcode_lookup(opcode) + + value = instruction.stack_pop() + offset = instruction.stack_pop() + size = instruction.stack_pop() + input = instruction.stack_pop() + gas = instruction.stack_push() + + depth = instruction.call_context_lookup(CallContextFieldTag.Depth) + tx_caller_address = instruction.call_context_lookup(CallContextFieldTag.CallerAddress) + nonce, nonce_prev = instruction.account_write(parent_caller_address, AccountFieldTag.Nonce) + + # ErrDepth constraint + instruction.range_lookup(depth, CALL_CREATE_DEPTH) + # ErrNonceUintOverflow constraint + (is_not_overflow, _) = instruction.compare(nonce, nonce_prev, 8) + instruction.is_zero(is_not_overflow) + # ErrInsufficientBalance constraint + gas_cost = ( + instruction.select( + is_warm_access, FQ(GAS_COST_WARM_ACCESS), FQ(GAS_COST_ACCOUNT_COLD_ACCESS) + ) + + has_value + * ( + GAS_COST_CALL_WITH_VALUE + # Only CALL opcode could invoke transfer to make empty account into non-empty. + + is_call * (1 - callee_exists) * GAS_COST_NEW_ACCOUNT + ) + + memory_expansion_gas_cost + ) + # Apply EIP 150. + # Note that sufficient gas_left is checked implicitly by constant_divmod. + gas_available = instruction.curr.gas_left - gas_cost + one_64th_gas, _ = instruction.constant_divmod(gas_available, FQ(64), N_BYTES_GAS) + all_but_one_64th_gas = gas_available - one_64th_gas + callee_gas_left = instruction.select( + gas_is_u64, + instruction.min(all_but_one_64th_gas, gas, N_BYTES_GAS), + all_but_one_64th_gas, + ) diff --git a/src/zkevm_specs/evm/execution_state.py b/src/zkevm_specs/evm/execution_state.py index d23ce1a93..cecb96161 100644 --- a/src/zkevm_specs/evm/execution_state.py +++ b/src/zkevm_specs/evm/execution_state.py @@ -85,13 +85,14 @@ class ExecutionState(IntEnum): ErrorStack = auto() # For SSTORE, LOG0, LOG1, LOG2, LOG3, LOG4, CREATE, CALL, CREATE2, SELFDESTRUCT ErrorWriteProtection = auto() - # For CALL, CALLCODE, DELEGATECALL, STATICCALL + # For CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE ErrorDepth = auto() - # For CALL, CALLCODE + # For CALL, CALLCODE, CREATE ErrorInsufficientBalance = auto() # For CREATE, CREATE2 ErrorContractAddressCollision = auto() ErrorInvalidCreationCode = auto() + ErrorNonceUintOverflow = auto() # For opcode RETURN which needs to store code when it's is creation ErrorMaxCodeSizeExceeded = auto() # For JUMP, JUMPI diff --git a/src/zkevm_specs/evm/typing.py b/src/zkevm_specs/evm/typing.py index 6b93b3c67..121f9459a 100644 --- a/src/zkevm_specs/evm/typing.py +++ b/src/zkevm_specs/evm/typing.py @@ -1054,3 +1054,19 @@ def _append_row( is_rlc_acc=FQ(is_rlc_acc), ) ) + + +def generate_contract_address(account: Account) -> bytes: + prefix = [LIST_PREFIX + 0x16] + sender_rlp = [HEX_PREFIX + ADDRESS_LENGTH] + sender_rlp += [x for x in account.address.to_bytes(20, "big")] + prefix += sender_rlp + prefix.append(HEX_PREFIX if account.nonce == 0 else account.nonce) + raw_hash = keccak256(bytearray(prefix))[12:] + + return raw_hash[(len(raw_hash) - ADDRESS_LENGTH) :] + + +ADDRESS_LENGTH = 0x14 +HEX_PREFIX = 0x80 +LIST_PREFIX = 0xC0 diff --git a/src/zkevm_specs/util/param.py b/src/zkevm_specs/util/param.py index 8cb29fcf4..63b03307d 100644 --- a/src/zkevm_specs/util/param.py +++ b/src/zkevm_specs/util/param.py @@ -106,6 +106,9 @@ # of cells in a step MAX_COPY_BYTES = 32 +# Maximum depth of call/create stack +CALL_CREATE_DEPTH = 1024 + # PublicInputs circuit parameters PUBLIC_INPUTS_BLOCK_LEN = 7 + 256 # Length of block public data PUBLIC_INPUTS_EXTRA_LEN = 3 # Length of fields that don't belong to any table diff --git a/tests/test_autil.py b/tests/test_autil.py new file mode 100644 index 000000000..48070ccbd --- /dev/null +++ b/tests/test_autil.py @@ -0,0 +1,21 @@ +from rlp import decode, encode +from zkevm_specs.evm import Account, generate_contract_address +from zkevm_specs.util import keccak256 + + +def test_contract_address(): + sender_addr0 = Account(address=0x970E8128AB834E8EAC17AB8E3812F010678CF791, nonce=0) + sender_addr1 = Account(address=0x970E8128AB834E8EAC17AB8E3812F010678CF791, nonce=1) + sender_addr2 = Account(address=0x970E8128AB834E8EAC17AB8E3812F010678CF791, nonce=2) + + expected_caddr0 = Account(address=0x333C3310824B7C685133F2BEDB2CA4B8B4DF633D) + expected_caddr1 = Account(address=0x8BDA78331C916A08481428E4B07C96D3E916D165) + expected_caddr2 = Account(address=0xC9DDEDF451BC62CE88BF9292AFB13DF35B670699) + + caddr0 = generate_contract_address(sender_addr0) + caddr1 = generate_contract_address(sender_addr1) + caddr2 = generate_contract_address(sender_addr2) + + assert expected_caddr0.address.to_bytes(20, "big") == caddr0 + assert expected_caddr1.address.to_bytes(20, "big") == caddr1 + assert expected_caddr2.address.to_bytes(20, "big") == caddr2