diff --git a/specs/error_state/ErrorCodeStore.md b/specs/error_state/ErrorCodeStore.md index 04a7faa7c..601db78b2 100644 --- a/specs/error_state/ErrorCodeStore.md +++ b/specs/error_state/ErrorCodeStore.md @@ -4,7 +4,7 @@ `ErrorCodeStore` is a combined error code for handling the `CodeStoreOutOfGas` and `MaxCodeSizeExceeded` code store related errors. This type of error only occurs when executing `CREATE`/`CREATE2` opcode or a deployment transaction (tx.to = null). ### EVM behavior -When handling either a CREATE` or a`CREATE2` opcode, the initial bytecode is executed and the current call context is created. The contract bytecode will then be returned through the `RETURN` opcode as the execution result. More particularly, the contract bytecode will be defined as the memory chunk of length `length` starting at offset `offset`, that is the memory located at [`offset`...`offset` + `length`] is stored in the state db. The gas cost for storing the bytecode is: +When handling either a `CREATE` or a`CREATE2` opcode, the initial bytecode is executed and the current call context is created. The contract bytecode will then be returned through the `RETURN` opcode as the execution result. More particularly, the contract bytecode will be defined as the memory chunk of length `length` starting at offset `offset`, that is the memory located at [`offset`...`offset` + `length`] is stored in the state db. The gas cost for storing the bytecode is: ``` let CODE_DEPOSIT_BYTE_COST = 200 diff --git a/specs/error_state/ErrorInvalidCreationCode.md b/specs/error_state/ErrorInvalidCreationCode.md new file mode 100644 index 000000000..7391efbe5 --- /dev/null +++ b/specs/error_state/ErrorInvalidCreationCode.md @@ -0,0 +1,38 @@ +# ErrorInvalidCreationCode state + +## Procedure + +InvalidCreationCode is an error related with code store. This error only occurs when executing `CREATE` or `CREATE2` opcode or contract creation transaction (`tx.to == null`). + +### EVM behavior + +When handling either a `CREATE` or a`CREATE2` opcode, the initial bytecode is executed and the current call context is created. The contract bytecode will then be returned through the `RETURN` opcode as the execution result. More particularly, the contract bytecode will be defined as the memory chunk of length `length` starting at offset `offset`, that is the memory located at [`offset`...`offset` + `length`] is stored in the state db. + + +In bus-mapping, when the executing opcode is `RETURN` and the current call context is `CREATE` or `CREATE2`, check if the first byte of contract code is `0xEF` to identify this error. + +Even though this error occurs in `CREATE` or `CREATE2`, this error should be constrained in the `RETURN` opcode. Since it is easy to get the available memory offset and construct constraints with it. + +Overall it looks as the following: + +- Pop EVM word `offset` from the stack. +- Go to `ErrorInvalidCreationCode` state when the current call context is `CREATE` or `CREATE2` and the first byte of the deployed bytecode is `0xEF`. + +### Constraints + +1. Current opcode is `RETURN` and `is_create` is True. +2. The first byte of contract code is `0xEF`. +3. Current call must be failed. +4. If it's a root call, it transits to `EndTx`. +5. if it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. + +### Lookups + +- Memory lookup. +- Stack reads for `offset`. +- Call context lookups for `is_success` and `rw_counter_end_of_reversion`. +- Restore context lookups for non-root call. + +## Code + +Please refer to `src/zkevm_specs/evm/execution/error_invalid_creation_code.py`. \ No newline at end of file diff --git a/src/zkevm_specs/evm_circuit/execution/__init__.py b/src/zkevm_specs/evm_circuit/execution/__init__.py index aa426cb08..b89863a36 100644 --- a/src/zkevm_specs/evm_circuit/execution/__init__.py +++ b/src/zkevm_specs/evm_circuit/execution/__init__.py @@ -71,6 +71,7 @@ from .error_oog_account_access import * from .error_code_store import * from .error_oog_exp import * +from .error_invalid_creation_code import * from .error_oog_sha3 import * from .error_oog_static_memory_expansion import * from .error_oog_sload_sstore import * @@ -143,6 +144,7 @@ ExecutionState.ErrorMaxCodeSizeExceeded: error_code_store, ExecutionState.ErrorOutOfGasCodeStore: error_code_store, ExecutionState.ErrorOutOfGasEXP: error_oog_exp, + ExecutionState.ErrorInvalidCreationCode: error_invalid_creation_code, ExecutionState.ErrorOutOfGasSHA3: error_oog_sha3, ExecutionState.ErrorOutOfGasAccountAccess: error_oog_account_access, ExecutionState.ErrorOutOfGasStaticMemoryExpansion: error_oog_static_memory_expansion, diff --git a/src/zkevm_specs/evm_circuit/execution/error_invalid_creation_code.py b/src/zkevm_specs/evm_circuit/execution/error_invalid_creation_code.py new file mode 100644 index 000000000..63e6b6235 --- /dev/null +++ b/src/zkevm_specs/evm_circuit/execution/error_invalid_creation_code.py @@ -0,0 +1,31 @@ +from zkevm_specs.evm_circuit.table import RW +from zkevm_specs.util.param import ( + INVALID_FIRST_BYTE_CONTRACT_CODE, + N_BYTES_MEMORY_ADDRESS, +) +from ...util import FQ +from ..instruction import Instruction +from ..opcode import Opcode + + +def error_invalid_creation_code(instruction: Instruction): + opcode = instruction.opcode_lookup(True) + + # opcode must be `RETURN` + instruction.constrain_equal(opcode, Opcode.RETURN) + + # the call must be coming from CREATE or CREATE2 + instruction.constrain_equal(FQ(instruction.curr.is_create), FQ(1)) + + # pop offset from stack only + return_offset = instruction.word_to_fq(instruction.stack_pop(), N_BYTES_MEMORY_ADDRESS) + + # lookup the first byte of deployed bytecode from memory + first_byte = instruction.memory_lookup(RW.Read, return_offset) + + # verify if the first byte is `0xEF`, which is introduced in EIP-3541 + instruction.constrain_equal(first_byte, FQ(INVALID_FIRST_BYTE_CONTRACT_CODE)) + + instruction.constrain_error_state( + instruction.rw_counter_offset + instruction.curr.reversible_write_counter + ) diff --git a/src/zkevm_specs/util/param.py b/src/zkevm_specs/util/param.py index 0226da64b..9b7b2bdd4 100644 --- a/src/zkevm_specs/util/param.py +++ b/src/zkevm_specs/util/param.py @@ -122,6 +122,8 @@ PUBLIC_INPUTS_EXTRA_LEN = 3 * 2 # Length of fields that don't belong to any table PUBLIC_INPUTS_TX_LEN = 10 # Length of tx public data (without calldata) +# EIP-3541, Reject new contract code starting with the 0xEF byte +INVALID_FIRST_BYTE_CONTRACT_CODE = 0xEF # Precompiled contract gas prices diff --git a/tests/evm/test_error_invalild_creation_code.py b/tests/evm/test_error_invalild_creation_code.py new file mode 100644 index 000000000..e37798eb1 --- /dev/null +++ b/tests/evm/test_error_invalild_creation_code.py @@ -0,0 +1,105 @@ +import pytest + +from zkevm_specs.evm_circuit import ( + Block, + Bytecode, + CallContextFieldTag, + ExecutionState, + RWDictionary, + StepState, + Tables, + verify_steps, +) +from zkevm_specs.util import Word +from zkevm_specs.util.param import INVALID_FIRST_BYTE_CONTRACT_CODE + + +TESTING_DATA = ( + (0, True), # is_root + (0, False), # not is_root + (200, True), # is_root + (200, False), # not is_root +) + + +@pytest.mark.parametrize("offset, is_root", TESTING_DATA) +def test_error_invalid_creation_code(offset: int, is_root: bool): + bytecode = Bytecode().push32(32).push32(offset).return_() + bytecode_hash = Word(bytecode.hash()) + + current_call_id = 1 if is_root else 2 + stack_pointer = 1023 + pc = 66 + reversible_write_counter = 2 + rw_counter = 3 if is_root is True else 15 + + # Only need `size` from RETURN opcode + rw_table = RWDictionary(rw_counter).stack_read(current_call_id, stack_pointer, Word(offset)) + + # assign invalid byte `0xEF` to the first byte of memory + rw_table.memory_read(current_call_id, offset, INVALID_FIRST_BYTE_CONTRACT_CODE) + + rw_table.call_context_read(current_call_id, CallContextFieldTag.IsSuccess, 0) + + if not is_root: + # fmt: off + rw_table \ + .call_context_read(current_call_id, CallContextFieldTag.CallerId, 1) \ + .call_context_read(1, CallContextFieldTag.IsRoot, False) \ + .call_context_read(1, CallContextFieldTag.IsCreate, True) \ + .call_context_read(1, CallContextFieldTag.CodeHash, bytecode_hash) \ + .call_context_read(1, CallContextFieldTag.ProgramCounter, pc + 1) \ + .call_context_read(1, CallContextFieldTag.StackPointer, 1024) \ + .call_context_read(1, CallContextFieldTag.GasLeft, 0) \ + .call_context_read(1, CallContextFieldTag.MemorySize, 0) \ + .call_context_read(1, CallContextFieldTag.ReversibleWriteCounter, reversible_write_counter) \ + .call_context_write(1, CallContextFieldTag.LastCalleeId, 2) \ + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) + # fmt: on + + tables = Tables( + block_table=set(Block().table_assignments()), + tx_table=set(), + bytecode_table=set(bytecode.table_assignments()), + rw_table=set(rw_table.rws), + ) + + verify_steps( + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.ErrorInvalidCreationCode, + rw_counter=rw_counter, + call_id=current_call_id, + is_root=is_root, + is_create=True, + code_hash=bytecode_hash, + program_counter=pc, + stack_pointer=stack_pointer, + gas_left=100, + memory_word_size=0, + reversible_write_counter=reversible_write_counter, + ), + StepState( + execution_state=ExecutionState.EndTx, + rw_counter=rw_table.rw_counter + reversible_write_counter, + call_id=1, + gas_left=0, + ) + if is_root is True + else StepState( + execution_state=ExecutionState.STOP, + rw_counter=rw_table.rw_counter + reversible_write_counter, + call_id=1, + is_root=is_root, + is_create=True, + code_hash=bytecode_hash, + program_counter=pc + 1, + stack_pointer=1024, + gas_left=0, + memory_word_size=0, + reversible_write_counter=reversible_write_counter, + ), + ], + )