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..593e65157 --- /dev/null +++ b/specs/opcode/F0CREATE_F5CREATE2.md @@ -0,0 +1,97 @@ +# 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_WORD_GAS_COST : 6 + +keccak_gas_cost = KECCAK_WORD_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 `callee.is_persistent = caller.is_persistent and caller.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/copy_circuit.py b/src/zkevm_specs/copy_circuit.py index 10d10ba58..adf85062a 100644 --- a/src/zkevm_specs/copy_circuit.py +++ b/src/zkevm_specs/copy_circuit.py @@ -42,7 +42,7 @@ def verify_row(cs: ConstraintSystem, rows: Sequence[CopyCircuitRow]): cs.constrain_equal(rows[0].addr + 1, rows[2].addr) cs.constrain_equal(rows[0].src_addr_end, rows[2].src_addr_end) - # contrain the transition for `rw_counter` and `rwc_inc_left` + # constrain the transition for `rw_counter` and `rwc_inc_left` rw_diff = (1 - rows[0].is_pad) * (rows[0].is_memory + rows[0].is_tx_log) with cs.condition(1 - rows[0].is_last) as cs: # not last row diff --git a/src/zkevm_specs/evm_circuit/execution/__init__.py b/src/zkevm_specs/evm_circuit/execution/__init__.py index b7dcda0c7..24d7ffdf8 100644 --- a/src/zkevm_specs/evm_circuit/execution/__init__.py +++ b/src/zkevm_specs/evm_circuit/execution/__init__.py @@ -52,6 +52,7 @@ from .sdiv_smod import sdiv_smod from .sha3 import sha3 from .shl_shr import shl_shr +from .create import create from .signextend import * from .stop import stop from .return_revert import * @@ -113,6 +114,8 @@ ExecutionState.SAR: sar, ExecutionState.SDIV_SMOD: sdiv_smod, ExecutionState.SHL_SHR: shl_shr, + ExecutionState.CREATE: create, + ExecutionState.CREATE2: create, ExecutionState.STOP: stop, ExecutionState.RETURN: return_revert, ExecutionState.ErrorInvalidJump: invalid_jump, diff --git a/src/zkevm_specs/evm_circuit/execution/create.py b/src/zkevm_specs/evm_circuit/execution/create.py new file mode 100644 index 000000000..dd20b4bfb --- /dev/null +++ b/src/zkevm_specs/evm_circuit/execution/create.py @@ -0,0 +1,200 @@ +from zkevm_specs.evm_circuit.opcode import Opcode +from zkevm_specs.util.arithmetic import FQ, Word, WordOrValue +from zkevm_specs.util.hash import EMPTY_CODE_HASH +from zkevm_specs.util.param import ( + GAS_COST_COPY_SHA3, + GAS_COST_CREATE, + N_BYTES_ACCOUNT_ADDRESS, + N_BYTES_GAS, + N_BYTES_MEMORY_ADDRESS, + N_BYTES_MEMORY_SIZE, + N_BYTES_U64, +) +from ...util import ( + CALL_CREATE_DEPTH, +) +from ..instruction import Instruction, Transition +from ..table import RW, CallContextFieldTag, AccountFieldTag, CopyDataTypeTag + + +def create(instruction: Instruction): + # check opcode is CREATE or CREATE2 + opcode = instruction.opcode_lookup(True) + is_create, is_create2 = instruction.pair_select(opcode, Opcode.CREATE, Opcode.CREATE2) + instruction.responsible_opcode_lookup(opcode) + + # set the caller_id to the current rw_counter + callee_call_id = instruction.curr.rw_counter + + # Stack parameters and result + value_word = instruction.stack_pop() + offset_word = instruction.stack_pop() + size_word = instruction.stack_pop() + salt_word = instruction.stack_pop() if is_create2 == FQ(1) else Word(0) + return_contract_address_word = instruction.stack_push() + + offset = instruction.word_to_fq(offset_word, N_BYTES_MEMORY_ADDRESS) # src_addr + size = instruction.word_to_fq(size_word, N_BYTES_MEMORY_ADDRESS) + + depth = instruction.call_context_lookup(CallContextFieldTag.Depth) + tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) + caller_address = instruction.call_context_lookup(CallContextFieldTag.CallerAddress) + nonce, nonce_prev = instruction.account_write(caller_address, AccountFieldTag.Nonce) + is_success = instruction.call_context_lookup(CallContextFieldTag.IsSuccess) + is_static = instruction.call_context_lookup(CallContextFieldTag.IsStatic) + reversion_info = instruction.reversion_info() + + # calculate contract address + contract_address = ( + instruction.generate_contract_address(caller_address, nonce) + if is_create == 1 + else instruction.generate_CREAET2_contract_address( + caller_address, salt_word, instruction.next.code_hash + ) + ) + + # verify the equality of input `size` and length of calldata + code_size = instruction.bytecode_length(instruction.next.code_hash) + instruction.constrain_equal(code_size, size) + + # verify return contract address + instruction.constrain_equal( + instruction.word_to_fq(return_contract_address_word, N_BYTES_ACCOUNT_ADDRESS), + is_success * contract_address, + ) + + # Verify depth is less than 1024 + instruction.range_lookup(depth, CALL_CREATE_DEPTH) + + # ErrNonceUintOverflow constraint + (is_not_overflow, _) = instruction.compare(nonce, nonce_prev, N_BYTES_U64) + instruction.is_zero(is_not_overflow) + + # add contract address to access list + instruction.add_account_to_access_list(tx_id, contract_address) + + # ErrContractAddressCollision constraint + # code_hash_prev could be either 0 or EMPTY_CODE_HASH + # code_hash should be EMPTY_CODE_HASH to make sure the account is created properly + code_hash, code_hash_prev = instruction.account_write_word( + contract_address, AccountFieldTag.CodeHash + ) + instruction.constrain_in_word( + code_hash_prev, + [Word(0), Word(EMPTY_CODE_HASH)], + ) + instruction.constrain_equal_word(code_hash, Word(EMPTY_CODE_HASH)) + + # Propagate is_persistent + callee_reversion_info = instruction.reversion_info(call_id=callee_call_id) + instruction.constrain_equal( + callee_reversion_info.is_persistent, + reversion_info.is_persistent * is_success.expr(), + ) + + # can't be a STATICCALL + instruction.is_zero(is_static) + + # transfer value from caller to contract address + instruction.transfer(caller_address, contract_address, value_word, callee_reversion_info) + + # gas cost of memory expansion + ( + next_memory_size, + memory_expansion_gas_cost, + ) = instruction.memory_expansion( + offset, + size, + ) + + # CREATE = GAS_COST_CREATE + memory expansion + GAS_COST_CODE_DEPOSIT * len(byte_code) + # CREATE2 = gas cost of CREATE + GAS_COST_COPY_SHA3 * memory_size + # byte_code is only available in `return_revert`, + # so the last part (GAS_COST_CODE_DEPOSIT * len(byte_code)) won't be calculated here + gas_left = instruction.curr.gas_left + gas_cost = GAS_COST_CREATE + memory_expansion_gas_cost + if is_create2 == 1: + memory_size, _ = instruction.constant_divmod(size + FQ(31), FQ(32), N_BYTES_MEMORY_SIZE) + gas_cost += GAS_COST_COPY_SHA3 * memory_size + gas_available = gas_left - gas_cost + + # Apply EIP 150 + one_64th_gas, _ = instruction.constant_divmod(gas_available, FQ(64), N_BYTES_GAS) + all_but_one_64th_gas = gas_available - one_64th_gas + is_u64_gas = instruction.is_zero( + instruction.sum(WordOrValue(gas_left).to_le_bytes()[N_BYTES_GAS:]) + ) + callee_gas_left = instruction.select( + is_u64_gas, + instruction.min(all_but_one_64th_gas, gas_left, N_BYTES_GAS), + all_but_one_64th_gas, + ) + + # copy init_code from memory to bytecode + copy_rwc_inc, _ = instruction.copy_lookup( + instruction.curr.call_id, # src_id + CopyDataTypeTag.Memory, # src_type + instruction.next.code_hash, # dst_id + CopyDataTypeTag.Bytecode, # dst_type + offset, # src_addr + offset + size, # src_addr_boundary + FQ(0), # dst_addr + size, # length + instruction.curr.rw_counter + instruction.rw_counter_offset, + ) + instruction.rw_counter_offset += int(copy_rwc_inc) + + # CREATE: 3 pops and 1 push, stack delta = 2 + # CREATE2: 4 pops and 1 push, stack delta = 3 + stack_pointer_delta = 2 + is_create2 + # Save caller's call state + for field_tag, expected_value in [ + (CallContextFieldTag.ProgramCounter, instruction.curr.program_counter + 1), + ( + CallContextFieldTag.StackPointer, + instruction.curr.stack_pointer + stack_pointer_delta, + ), + (CallContextFieldTag.GasLeft, gas_left - gas_cost - callee_gas_left), + (CallContextFieldTag.MemorySize, next_memory_size), + ( + CallContextFieldTag.ReversibleWriteCounter, + instruction.curr.reversible_write_counter + 1, + ), + ]: + instruction.constrain_equal( + instruction.call_context_lookup(field_tag, RW.Write), + expected_value, + ) + + # Setup next call's context. + for field_tag, expected_value in [ + (CallContextFieldTag.CallerId, instruction.curr.call_id), + (CallContextFieldTag.TxId, tx_id), + (CallContextFieldTag.Depth, depth + 1), + (CallContextFieldTag.CallerAddress, caller_address), + (CallContextFieldTag.CalleeAddress, contract_address), + (CallContextFieldTag.IsSuccess, is_success), + (CallContextFieldTag.IsStatic, FQ(False)), + (CallContextFieldTag.IsRoot, FQ(False)), + (CallContextFieldTag.IsCreate, FQ(True)), + ]: + instruction.constrain_equal( + instruction.call_context_lookup(field_tag, call_id=callee_call_id), + expected_value, + ) + instruction.constrain_equal_word( + instruction.call_context_lookup_word(CallContextFieldTag.CodeHash, call_id=callee_call_id), + code_hash, + ) + + instruction.step_state_transition_to_new_context( + rw_counter=Transition.delta(instruction.rw_counter_offset), + call_id=Transition.to(callee_call_id), + is_root=Transition.to(False), + is_create=Transition.to(True), + code_hash=Transition.to_word(instruction.next.code_hash), + gas_left=Transition.to(callee_gas_left), + # `transfer` includes two balance updates + reversible_write_counter=Transition.to(2), + log_id=Transition.same(), + ) diff --git a/src/zkevm_specs/evm_circuit/execution/return_revert.py b/src/zkevm_specs/evm_circuit/execution/return_revert.py index 8726ea089..70bc140bc 100644 --- a/src/zkevm_specs/evm_circuit/execution/return_revert.py +++ b/src/zkevm_specs/evm_circuit/execution/return_revert.py @@ -1,4 +1,6 @@ -from ...util import EMPTY_HASH, FQ, N_BYTES_MEMORY_ADDRESS, Word +from zkevm_specs.util.arithmetic import Word +from zkevm_specs.util.param import GAS_COST_CODE_DEPOSIT, MAX_CODE_SIZE +from ...util import EMPTY_HASH, FQ, N_BYTES_MEMORY_ADDRESS from ..instruction import Instruction, Transition from ..opcode import Opcode from ..table import CallContextFieldTag, CopyDataTypeTag, AccountFieldTag @@ -24,36 +26,44 @@ def return_revert(instruction: Instruction): rwc_delta = 3 + callee_gas_left = instruction.curr.gas_left if instruction.curr.is_create and is_success: # A. Returns the specified memory chunk as deployment code. - # TODO: Untested case. Test it once create Tx is implemented, and once - # CREATE/CREATE2 are implemented. callee_address = instruction.call_context_lookup(CallContextFieldTag.CalleeAddress) - reversion_info = instruction.reversion_info() code_hash, code_hash_prev = instruction.account_write_word( callee_address, AccountFieldTag.CodeHash ) instruction.constrain_equal_word(code_hash_prev, Word(EMPTY_HASH)) - instruction.constrain_equal_word(code_hash, instruction.curr.aux_data) + instruction.constrain_equal_word(code_hash, instruction.curr.code_hash) + + # verify bytecode size less than 24,576 bytes + instruction.range_lookup(return_length, MAX_CODE_SIZE) + + # gas cost of CREATE = GAS_COST_CREATE + memory expansion + GAS_COST_CODE_DEPOSIT * len(byte_code) + # first two part were handled in create.py + callee_gas_left = callee_gas_left - return_length * GAS_COST_CODE_DEPOSIT # Return a memory chunk as deployment code by copying each byte from # callee's memory to bytecode, using the copy circuit. copy_length = return_length - copy_rwc_inc, _ = instruction.copy_lookup( - instruction.curr.call_id, # src_id - CopyDataTypeTag.Memory, # src_tag - code_hash, # dst_id - CopyDataTypeTag.Bytecode, # dst_tag - return_offset, # src_addr - return_end, # src_addr_boundary - FQ(0), # dst_addr - copy_length, # length - instruction.curr.rw_counter + instruction.rw_counter_offset, - ) - instruction.constrain_equal(copy_rwc_inc, copy_length) # rwc += copy_length - instruction.rw_counter_offset += int(copy_rwc_inc) - rwc_delta += int(copy_length) + if int(return_length) > 0: + copy_rwc_inc, _ = instruction.copy_lookup( + instruction.curr.call_id, # src_id + CopyDataTypeTag.Memory, # src_type + code_hash, # dst_id + CopyDataTypeTag.Bytecode, # dst_type + return_offset, # src_addr + return_end, # src_addr_boundary + FQ(0), # dst_addr + copy_length, # length + instruction.curr.rw_counter + instruction.rw_counter_offset, + ) + instruction.constrain_equal(copy_rwc_inc, copy_length) # rwc += copy_length + instruction.rw_counter_offset += int(copy_rwc_inc) + rwc_delta += int(copy_length) + code_size = instruction.bytecode_length(code_hash) + instruction.constrain_equal(code_size, copy_length) if not instruction.curr.is_root and not instruction.curr.is_create: # D. Returns the specified memory chunk to the caller. @@ -107,6 +117,7 @@ def return_revert(instruction: Instruction): # Do step state transition instruction.constrain_step_state_transition( rw_counter=Transition.delta(rwc_delta + 1), + gas_left=Transition.to(callee_gas_left), call_id=Transition.same(), ) else: @@ -117,5 +128,5 @@ def return_revert(instruction: Instruction): rw_counter_delta=rwc_delta, return_data_offset=return_offset, return_data_length=return_length, - gas_left=instruction.curr.gas_left - memory_expansion_gas, + gas_left=callee_gas_left - memory_expansion_gas, ) # rwc += 12 diff --git a/src/zkevm_specs/evm_circuit/execution_state.py b/src/zkevm_specs/evm_circuit/execution_state.py index 8a5633782..cc25cb5b8 100644 --- a/src/zkevm_specs/evm_circuit/execution_state.py +++ b/src/zkevm_specs/evm_circuit/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_circuit/instruction.py b/src/zkevm_specs/evm_circuit/instruction.py index 66cd79fdc..8a8bd6765 100644 --- a/src/zkevm_specs/evm_circuit/instruction.py +++ b/src/zkevm_specs/evm_circuit/instruction.py @@ -2,6 +2,11 @@ from enum import IntEnum, auto from typing import Optional, Sequence, Tuple, Union, List, cast +from eth_utils import ( + keccak, +) +import rlp # type: ignore + from ..util import ( FQ, IntOrFQ, @@ -163,6 +168,9 @@ def constrain_in(self, lhs: Expression, rhs: List[FQ]): f"Expected value to be in {rhs}, but got {lhs}" ) + def constrain_in_word(self, lhs: Word, rhs: List[Word]): + assert lhs in rhs, ConstraintUnsatFailure(f"Expected word to be in {rhs}, but got {lhs}") + def constrain_bool(self, num: Expression): assert num.expr() in [0, 1], ConstraintUnsatFailure( f"Expected value to be a bool, but got {num}" @@ -268,7 +276,7 @@ def step_state_transition_to_new_context( gas_left=gas_left, reversible_write_counter=reversible_write_counter, log_id=log_id, - # Initailization unconditionally + # Initialization unconditionally program_counter=Transition.to(0), stack_pointer=Transition.to(1024), memory_word_size=Transition.to(0), @@ -762,12 +770,7 @@ def opcode_lookup(self, is_code: bool) -> FQ: return self.opcode_lookup_at(index, is_code) def opcode_lookup_at(self, index: FQ, is_code: bool) -> FQ: - if self.curr.is_root and self.curr.is_create: - raise NotImplementedError( - "The opcode source when is_root and is_create (root creation call) is not determined yet" - ) - else: - return self.bytecode_lookup(self.curr.code_hash, index, FQ(is_code)).expr() + return self.bytecode_lookup(self.curr.code_hash, index, FQ(is_code)).expr() def rw_lookup( self, @@ -1152,6 +1155,22 @@ def memory_copier_gas_cost( self.range_check(gas_cost, N_BYTES_GAS) return gas_cost + def generate_contract_address(self, address: Expression, nonce: Expression) -> Expression: + contract_addr = keccak(rlp.encode([address.expr().n.to_bytes(20, "big"), nonce.expr().n])) + return FQ(int.from_bytes(contract_addr[-20:], "big")) + + def generate_CREAET2_contract_address( + self, address: Expression, salt: Word, code_hash: Word + ) -> Expression: + # keccak256(0xff + sender_address + salt + keccak256(initialisation_code))[12:] + contract_addr = keccak( + b"\xff" + + address.expr().n.to_bytes(20, "big") + + salt.int_value().to_bytes(32, "little") + + code_hash.int_value().to_bytes(32, "little") + ) + return FQ(int.from_bytes(contract_addr[-20:], "big")) + def pow2_lookup(self, value: Expression, pow_lo128: Expression, pow_hi128: Expression): self.fixed_lookup(FixedTableTag.Pow2, value, pow_lo128, pow_hi128) diff --git a/src/zkevm_specs/evm_circuit/table.py b/src/zkevm_specs/evm_circuit/table.py index fc835f208..59fc69bb1 100644 --- a/src/zkevm_specs/evm_circuit/table.py +++ b/src/zkevm_specs/evm_circuit/table.py @@ -24,6 +24,7 @@ class FixedTableTag(IntEnum): Range256 = auto() # value, 0, 0 Range512 = auto() # value, 0, 0 Range1024 = auto() # value, 0, 0 + Range24_576 = auto() # value, 0, 0 SignByte = auto() # value, signbyte, 0 BitwiseAnd = auto() # lhs, rhs, lhs & rhs, 0 BitwiseOr = auto() # lhs, rhs, lhs | rhs, 0 @@ -48,6 +49,8 @@ def table_assignments(self) -> List[FixedTableRow]: return [FixedTableRow(FQ(self), FQ(i), FQ(0), FQ(0)) for i in range(512)] elif self == FixedTableTag.Range1024: return [FixedTableRow(FQ(self), FQ(i), FQ(0), FQ(0)) for i in range(1024)] + elif self == FixedTableTag.Range24_576: + return [FixedTableRow(FQ(self), FQ(i), FQ(0), FQ(0)) for i in range(24576)] elif self == FixedTableTag.SignByte: return [FixedTableRow(FQ(self), FQ(i), FQ((i >> 7) * 0xFF), FQ(0)) for i in range(256)] elif self == FixedTableTag.BitwiseAnd: @@ -114,6 +117,8 @@ def range_table_tag(range: int) -> FixedTableTag: return FixedTableTag.Range512 elif range == 1024: return FixedTableTag.Range1024 + elif range == 24576: + return FixedTableTag.Range24_576 else: raise ValueError( f"Range {range} lookup is not supported yet, please add a new variant Range{range} in FixedTableTag with proper table assignments" diff --git a/src/zkevm_specs/evm_circuit/typing.py b/src/zkevm_specs/evm_circuit/typing.py index 3319238b9..d4d35cc17 100644 --- a/src/zkevm_specs/evm_circuit/typing.py +++ b/src/zkevm_specs/evm_circuit/typing.py @@ -55,6 +55,7 @@ ) from .opcode import get_push_size, Opcode + POW2 = 2**256 @@ -942,7 +943,7 @@ def copy( is_pad = False assert src_addr + i in src_data, f"Cannot find data at the offset {src_addr+i}" value = src_data[src_addr + i] - if src_tag == CopyDataTypeTag.Bytecode: + if src_tag == CopyDataTypeTag.Bytecode or dst_tag == CopyDataTypeTag.Bytecode: value = cast(Tuple[IntOrFQ, IntOrFQ], value) value, is_code = value else: diff --git a/src/zkevm_specs/util/param.py b/src/zkevm_specs/util/param.py index c9c906620..80319b9be 100644 --- a/src/zkevm_specs/util/param.py +++ b/src/zkevm_specs/util/param.py @@ -79,6 +79,8 @@ GAS_COST_ACCESS_LIST_ADDRESS = 2400 # Gas cost of warming up a storage with the access list GAS_COST_ACCESS_LIST_STORAGE = 1900 +# Gas cost of storing a byte of bytecode at contract creation +GAS_COST_CODE_DEPOSIT = 200 # Quotient for max refund of gas used @@ -106,6 +108,12 @@ # of cells in a step MAX_COPY_BYTES = 32 +# max bytecode size, 24576 bytes +MAX_CODE_SIZE = 24576 + +# Maximum depth of call/create stack +CALL_CREATE_DEPTH = 1024 + # PublicInputs circuit parameters PUBLIC_INPUTS_BLOCK_LEN = (7 + 256) * 2 # Length of block public data PUBLIC_INPUTS_EXTRA_LEN = 3 * 2 # Length of fields that don't belong to any table diff --git a/tests/evm/test_create.py b/tests/evm/test_create.py new file mode 100644 index 000000000..18f55ce53 --- /dev/null +++ b/tests/evm/test_create.py @@ -0,0 +1,365 @@ +import pytest +import rlp +from collections import namedtuple +from itertools import chain, product +from common import rand_fq +from zkevm_specs.copy_circuit import verify_copy_table +from zkevm_specs.evm_circuit import ( + Account, + AccountFieldTag, + Block, + Bytecode, + CallContextFieldTag, + ExecutionState, + Opcode, + RWDictionary, + StepState, + Tables, + verify_steps, +) +from zkevm_specs.evm_circuit.table import CopyDataTypeTag +from zkevm_specs.evm_circuit.typing import CopyCircuit +from zkevm_specs.util.hash import EMPTY_CODE_HASH, keccak256 +from zkevm_specs.util.param import GAS_COST_COPY_SHA3, GAS_COST_CREATE +from zkevm_specs.util import Word + +CreateContext = namedtuple( + "CreateContext", + [ + "rw_counter_end_of_reversion", + "is_persistent", + "gas_left", + "memory_word_size", + "reversible_write_counter", + ], + defaults=[0, True, 0, 0, 2], +) +Stack = namedtuple( + "Stack", + ["value", "offset", "size", "salt"], + defaults=[0, 0, 0, 0], +) + + +CALLER = Account(address=0xFE, balance=int(1e20), nonce=10) + + +def gen_bytecode(is_return: bool, offset: int) -> Bytecode: + """Generate bytecode that has 64 bytes of memory initialized and returns with `offset` and `length`""" + bytecode = ( + Bytecode() + .push(0x2222222222222222222222222222222222222222222222222222222222222222, n_bytes=32) + .push(offset, n_bytes=1) + .mstore() + ) + + if is_return: + bytecode.return_() + else: + bytecode.revert() + + return bytecode + + +def calc_gas_cost( + opcode: Opcode, + caller_ctx: CreateContext, + stack: Stack, +): + def memory_size(offset: int, length: int) -> int: + if length == 0: + return 0 + return (offset + length + 31) // 32 + + is_create2 = 1 if opcode == Opcode.CREATE2 else 0 + + offset = stack.offset + size = stack.size + + next_memory_size = max( + memory_size(offset, size), + caller_ctx.memory_word_size, + ) + memory_expansion_gas_cost = ( + next_memory_size * next_memory_size + - caller_ctx.memory_word_size * caller_ctx.memory_word_size + ) // 512 + 3 * (next_memory_size - caller_ctx.memory_word_size) + + # GAS_COST_CODE_DEPOSIT * (bytecode size) is not included here, it should be `return_revert`` + # extra gas cost for CREATE2 + gas_cost = GAS_COST_CREATE + memory_expansion_gas_cost + if is_create2 == 1: + gas_cost += GAS_COST_COPY_SHA3 * memory_size(0, size) + gas_available = caller_ctx.gas_left - gas_cost + all_but_one_64th_gas = gas_available - gas_available // 64 + callee_gas_left = min(all_but_one_64th_gas, caller_ctx.gas_left) + caller_gas_left = caller_ctx.gas_left - (gas_cost + callee_gas_left) + + return ( + caller_gas_left, + callee_gas_left, + next_memory_size, + ) + + +def gen_testing_data(): + opcodes = [ + Opcode.CREATE, + Opcode.CREATE2, + ] + is_return = [ + True, + False, + ] + create_contexts = [ + CreateContext(gas_left=1_000_000, is_persistent=True), + CreateContext(gas_left=1_000_000, is_persistent=False, rw_counter_end_of_reversion=88), + ] + stacks = [ + Stack(value=int(1e18), offset=64, salt=int(12345)), + Stack(offset=200), + Stack(offset=0), + ] + is_warm_accesss = [True, False] + + return [ + ( + opcode, + CALLER, + is_return, + create_contexts, + stack, + is_warm_access, + ) + for opcode, is_return, create_contexts, stack, is_warm_access in product( + opcodes, is_return, create_contexts, stacks, is_warm_accesss + ) + ] + + +TESTING_DATA = gen_testing_data() + + +@pytest.mark.parametrize( + "opcode, caller, is_return, caller_ctx, stack, is_warm_access", + TESTING_DATA, +) +def test_create_create2( + opcode: Opcode, + caller: Account, + is_return: Bytecode, + caller_ctx: CreateContext, + stack: Stack, + is_warm_access: bool, +): + randomness_keccak = rand_fq() + CURRENT_CALL_ID = 1 + + init_codes = gen_bytecode(is_return, stack.offset) + stack = stack._replace(size=len(init_codes.code)) + init_codes_hash = Word(init_codes.hash()) + + init_bytecode = gen_bytecode(is_return, stack.offset) + is_create2 = 1 if opcode == Opcode.CREATE2 else 0 + if is_create2 == 1: + caller_bytecode = init_bytecode.create2( + stack.value, + stack.offset, + stack.size, + stack.salt, + ).stop() + else: # CREATE + caller_bytecode = init_bytecode.create( + stack.value, + stack.offset, + stack.size, + ).stop() + + caller_bytecode_hash = Word(caller_bytecode.hash()) + (caller_gas_left, callee_gas_left, next_memory_size) = calc_gas_cost( + opcode, + caller_ctx, + stack, + ) + + nonce = caller.nonce + is_success = is_return + is_reverted_by_caller = not caller_ctx.is_persistent and is_success + is_reverted_by_callee = not is_success + callee_is_persistent = caller_ctx.is_persistent and is_success + callee_rw_counter_end_of_reversion = ( + 80 + if is_reverted_by_callee + else ( + caller_ctx.rw_counter_end_of_reversion - (caller_ctx.reversible_write_counter + 1) + if is_reverted_by_caller + else 0 + ) + ) + + if is_create2 == 1: + preimage = ( + b"\xff" + + caller.address.to_bytes(20, "big") + + int(stack.salt).to_bytes(32, "little") + + init_codes_hash.int_value().to_bytes(32, "little") + ) + contract_addr = keccak256(preimage) + else: + contract_addr = keccak256(rlp.encode([caller.address.to_bytes(20, "big"), caller.nonce])) + contract_address = int.from_bytes(contract_addr[-20:], "big") + + # can't be a static all + is_static = 0 + + next_call_id = 65 + rw_counter = next_call_id + # CREATE: 33 * 3(push) + 1(CREATE) + 1(mstore) + 33(PUSH32) + 2(PUSH) + 1(RETURN) + # CREATE2: 33 * 4(push) + 1(CREATE2) + 1(mstore) + 33(PUSH32) + 2(PUSH) + 1(RETURN) + next_program_counter = 33 * 4 + 1 + 35 + 1 if is_create2 else 33 * 3 + 1 + 35 + 1 + assert caller_bytecode.code[next_program_counter] == opcode + + # CREATE: 1024 - 3 + 1 = 1022 + # CREATE2: 1024 - 4 + 1 = 1021 + stack_pointer = 1021 - is_create2 + + src_data = dict( + [ + ( + i, + ( + init_codes.code[i - stack.offset], + init_codes.is_code[i - stack.offset], + ) + if i - stack.offset < len(init_codes.code) + else (0, 0), + ) + for i in range(stack.offset, stack.offset + stack.size) + ] + ) + + # fmt: off + # stack + rw_dictionary = ( + RWDictionary(rw_counter) + .stack_read(CURRENT_CALL_ID, 1021 - is_create2, Word(stack.value)) + .stack_read(CURRENT_CALL_ID, 1022 - is_create2, Word(stack.offset)) + .stack_read(CURRENT_CALL_ID, 1023 - is_create2, Word(stack.size)) + ) + if is_create2: + rw_dictionary.stack_read(CURRENT_CALL_ID, 1023, Word(stack.salt)) + rw_dictionary.stack_write(CURRENT_CALL_ID, 1023, Word(contract_address) if is_success else Word(0)) + + # caller's call context + rw_dictionary \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.Depth, 1) \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.TxId, 1) \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.CallerAddress, caller.address) \ + .account_write(caller.address, AccountFieldTag.Nonce, nonce, nonce - 1) \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.IsSuccess, is_success) \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.IsStatic, is_static) \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.RwCounterEndOfReversion, caller_ctx.rw_counter_end_of_reversion) \ + .call_context_read(CURRENT_CALL_ID, CallContextFieldTag.IsPersistent, caller_ctx.is_persistent) \ + .tx_access_list_account_write(CURRENT_CALL_ID, contract_address, True, is_warm_access, rw_counter_of_reversion=None if caller_ctx.is_persistent else caller_ctx.rw_counter_end_of_reversion - caller_ctx.reversible_write_counter) \ + .account_write(contract_address, AccountFieldTag.CodeHash, Word(EMPTY_CODE_HASH), 0) \ + + # callee's reversion_info + rw_dictionary \ + .call_context_read(next_call_id, CallContextFieldTag.RwCounterEndOfReversion, callee_rw_counter_end_of_reversion) \ + .call_context_read(next_call_id, CallContextFieldTag.IsPersistent, callee_is_persistent) + + # For `transfer` invocation. + caller_balance_prev = Word(caller.balance) + callee_balance_prev = Word(0) + caller_balance = Word(caller.balance - stack.value) + callee_balance = Word(stack.value) + rw_dictionary \ + .account_write(caller.address, AccountFieldTag.Balance, caller_balance, caller_balance_prev, rw_counter_of_reversion=None if callee_is_persistent else callee_rw_counter_end_of_reversion) \ + .account_write(contract_address, AccountFieldTag.Balance, callee_balance, callee_balance_prev, rw_counter_of_reversion=None if callee_is_persistent else callee_rw_counter_end_of_reversion - 1) + + # copy_table + copy_circuit = CopyCircuit().copy( + randomness_keccak, + rw_dictionary, + CURRENT_CALL_ID, + CopyDataTypeTag.Memory, + init_codes_hash, + CopyDataTypeTag.Bytecode, + stack.offset, + stack.offset + stack.size, + 0, + stack.size, + src_data, + ) + + # caller's call context + rw_dictionary \ + .call_context_write(CURRENT_CALL_ID, CallContextFieldTag.ProgramCounter, next_program_counter + 1) \ + .call_context_write(CURRENT_CALL_ID, CallContextFieldTag.StackPointer, 1023) \ + .call_context_write(CURRENT_CALL_ID, CallContextFieldTag.GasLeft, caller_gas_left) \ + .call_context_write(CURRENT_CALL_ID, CallContextFieldTag.MemorySize, next_memory_size) \ + .call_context_write(CURRENT_CALL_ID, CallContextFieldTag.ReversibleWriteCounter, caller_ctx.reversible_write_counter + 1) + + # callee's call context + rw_dictionary \ + .call_context_read(next_call_id, CallContextFieldTag.CallerId, CURRENT_CALL_ID) \ + .call_context_read(next_call_id, CallContextFieldTag.TxId, 1) \ + .call_context_read(next_call_id, CallContextFieldTag.Depth, 2) \ + .call_context_read(next_call_id, CallContextFieldTag.CallerAddress, caller.address) \ + .call_context_read(next_call_id, CallContextFieldTag.CalleeAddress, contract_address) \ + .call_context_read(next_call_id, CallContextFieldTag.IsSuccess, is_success) \ + .call_context_read(next_call_id, CallContextFieldTag.IsStatic, is_static) \ + .call_context_read(next_call_id, CallContextFieldTag.IsRoot, False) \ + .call_context_read(next_call_id, CallContextFieldTag.IsCreate, True) \ + .call_context_read(next_call_id, CallContextFieldTag.CodeHash, Word(EMPTY_CODE_HASH)) + # fmt: on + + tables = Tables( + block_table=set(Block().table_assignments()), + tx_table=set(), + bytecode_table=set( + chain( + caller_bytecode.table_assignments(), + init_codes.table_assignments(), + ) + ), + rw_table=set(rw_dictionary.rws), + copy_circuit=copy_circuit.rows, + ) + + verify_copy_table(copy_circuit, tables, randomness_keccak) + + verify_steps( + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.CREATE2 + if is_create2 == 1 + else ExecutionState.CREATE, + rw_counter=rw_counter, + call_id=CURRENT_CALL_ID, + is_root=False, + is_create=True, + code_hash=caller_bytecode_hash, + program_counter=next_program_counter, + stack_pointer=stack_pointer, + gas_left=caller_ctx.gas_left, + memory_word_size=caller_ctx.memory_word_size, + reversible_write_counter=caller_ctx.reversible_write_counter, + ), + ( + StepState( + execution_state=ExecutionState.PUSH, + rw_counter=rw_dictionary.rw_counter, + call_id=next_call_id, + is_root=False, + is_create=True, + code_hash=init_codes_hash, + program_counter=0, + stack_pointer=1024, + gas_left=callee_gas_left, + reversible_write_counter=2, + ) + ), + ], + ) diff --git a/tests/evm/test_return_revert.py b/tests/evm/test_return_revert.py index 278648bd2..e99a37744 100644 --- a/tests/evm/test_return_revert.py +++ b/tests/evm/test_return_revert.py @@ -16,6 +16,9 @@ CopyDataTypeTag, ) from zkevm_specs.copy_circuit import verify_copy_table +from zkevm_specs.evm_circuit.table import AccountFieldTag +from zkevm_specs.util.hash import EMPTY_CODE_HASH +from zkevm_specs.util.param import GAS_COST_CODE_DEPOSIT from zkevm_specs.util import Word @@ -107,8 +110,172 @@ def test_is_root_not_create( ) -# TODO: test_return_is_root_is_create -# TODO: test_return_not_root_is_create +TESTING_DATA_IS_CREATE = ( + (Transaction(), 1_000_000, True, True, 4, 10), # RETURN, ROOT + (Transaction(), 1_000_000, True, False, 4, 10), # REVERT, ROOT + (Transaction(), 1_000_000, False, True, 4, 10), # RETURN, not ROOT + (Transaction(), 1_000_000, False, False, 4, 10), # REVERT, not ROOT +) + + +@pytest.mark.parametrize( + "tx, gas_available, is_root, is_return, return_offset, return_length", + TESTING_DATA_IS_CREATE, +) +def test_is_create( + tx: Transaction, + gas_available: int, + is_root: bool, + is_return: bool, + return_offset: int, + return_length: int, +): + randomness = rand_fq() + CALLEE_ADDRESS = 0xFE + + block = Block() + + init_bytecode = gen_bytecode(is_return, return_offset, return_length) + deployment_bytecode_hash = Word(init_bytecode.hash()) + bytecode_length = len(init_bytecode.code) + + # gas + _, gas_memory_expansion = memory_expansion(0, return_offset + bytecode_length) + gas_cost = bytecode_length * GAS_COST_CODE_DEPOSIT + gas_left = gas_available - gas_cost + + return_offset_word = Word(return_offset) + return_length_word = Word(bytecode_length) + caller_id = 1 + rw_counter = 16 if is_root else 27 + callee_id = rw_counter + + rw_dict = ( + RWDictionary(rw_counter) + .call_context_read(callee_id, CallContextFieldTag.IsSuccess, int(is_return)) + .stack_read(callee_id, 1022, return_offset_word) + .stack_read(callee_id, 1023, return_length_word) + .call_context_read(callee_id, CallContextFieldTag.CalleeAddress, int(CALLEE_ADDRESS)) + .account_write( + CALLEE_ADDRESS, + AccountFieldTag.CodeHash, + deployment_bytecode_hash, + Word(EMPTY_CODE_HASH), + ) + ) + + # copy_table + src_data = dict( + [ + ( + i, + ( + init_bytecode.code[i - return_offset], + init_bytecode.is_code[i - return_offset], + ) + if i - return_offset < len(init_bytecode.code) + else (0, 0), + ) + for i in range(return_offset, return_offset + bytecode_length) + ] + ) + copy_circuit = CopyCircuit().copy( + randomness, + rw_dict, + callee_id, + CopyDataTypeTag.Memory, + deployment_bytecode_hash, + CopyDataTypeTag.Bytecode, + return_offset, + return_offset + bytecode_length, + 0, + bytecode_length, + src_data, + ) + + if is_root: + rw_dict.call_context_read(callee_id, CallContextFieldTag.IsPersistent, int(is_return)) + else: + memory_word_size = ( + 0 if bytecode_length == 0 else (return_offset + bytecode_length + 31) // 32 + ) + rw_dict = ( + rw_dict.call_context_read(callee_id, CallContextFieldTag.CallerId, caller_id) + .call_context_read(caller_id, CallContextFieldTag.IsRoot, is_root) + .call_context_read(caller_id, CallContextFieldTag.IsCreate, True) + .call_context_read(caller_id, CallContextFieldTag.CodeHash, deployment_bytecode_hash) + .call_context_read(caller_id, CallContextFieldTag.ProgramCounter, 40) + .call_context_read(caller_id, CallContextFieldTag.StackPointer, 1022) + .call_context_read(caller_id, CallContextFieldTag.GasLeft, 0) + .call_context_read(caller_id, CallContextFieldTag.MemorySize, memory_word_size) + .call_context_read( + caller_id, + CallContextFieldTag.ReversibleWriteCounter, + len(rw_dict.rws), + ) + .call_context_write(caller_id, CallContextFieldTag.LastCalleeId, callee_id) + .call_context_write( + caller_id, CallContextFieldTag.LastCalleeReturnDataOffset, return_offset + ) + .call_context_write( + caller_id, CallContextFieldTag.LastCalleeReturnDataLength, bytecode_length + ) + ) + + tables = Tables( + block_table=set(block.table_assignments()), + tx_table=set( + chain( + tx.table_assignments(), + Transaction(id=tx.id + 1).table_assignments(), + ) + ), + bytecode_table=set(init_bytecode.table_assignments()), + rw_table=set(rw_dict.rws), + copy_circuit=copy_circuit.rows, + ) + + verify_copy_table(copy_circuit, tables, randomness) + + verify_steps( + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.RETURN, + rw_counter=rw_counter, + call_id=callee_id, + is_root=is_root, + is_create=True, + code_hash=deployment_bytecode_hash, + program_counter=40, + stack_pointer=1022, + gas_left=gas_available, + reversible_write_counter=2, + ), + StepState( + execution_state=ExecutionState.EndTx, + gas_left=gas_left, + rw_counter=len(rw_dict.rws) + 14, + call_id=callee_id, + ) + if is_root + else StepState( + execution_state=ExecutionState.STOP, + # 12 comes from `step_state_transition_to_restored_context` + rw_counter=len(rw_dict.rws) + 14 + 12 - 1, + call_id=caller_id, + is_root=False, + is_create=True, + code_hash=deployment_bytecode_hash, + program_counter=40, + stack_pointer=1022, + gas_left=gas_left - gas_memory_expansion, + memory_word_size=memory_word_size, + reversible_write_counter=len(rw_dict.rws) - 2, + ), + ], + ) + TESTING_DATA_NOT_ROOT_NOT_CREATE = ( (