From f67c2e0c49e6c65c8bdbbd37dc8bdf925e538394 Mon Sep 17 00:00:00 2001 From: Kimi Wu Date: Thu, 3 Aug 2023 15:36:47 +0800 Subject: [PATCH] Feat/#446 implement ErrorReturnDataOutOfBound error state (#479) * doc: MD spec finished * feat: impl ErrorReturnDataOutOfBound * test: test cases finished * chore: fix for review feedbacks --- .../error_state/ErrorReturnDataOutOfBound.md | 28 ++++ .../evm_circuit/execution/__init__.py | 2 + .../error_return_data_out_of_bound.py | 32 +++++ .../test_error_return_data_out_of_bound.py | 124 ++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 specs/error_state/ErrorReturnDataOutOfBound.md create mode 100644 src/zkevm_specs/evm_circuit/execution/error_return_data_out_of_bound.py create mode 100644 tests/evm/test_error_return_data_out_of_bound.py diff --git a/specs/error_state/ErrorReturnDataOutOfBound.md b/specs/error_state/ErrorReturnDataOutOfBound.md new file mode 100644 index 000000000..a83e4e7ff --- /dev/null +++ b/specs/error_state/ErrorReturnDataOutOfBound.md @@ -0,0 +1,28 @@ +# ErrorReturnDataOutOfBound state + +## Procedure + +This type of error only occurs when executing op code is `RETURNDATACOPY`. + +### EVM behavior + +The `RETURNDATACOPY` opcode pops `memOffset`,`dataOffset`, and `length` from the stack. A return data out of bound error is thrown if one of the following condition is met: +1. `dataOffset`is u64 overflow, +2. `dataOffset` + `length` is u64 overflow, +3. `dataOffset` + `length` is larger than the length of last callee's return data. + +### Constraints +1. current opcode is `RETURNDATACOPY` +2. at least one of below conditions is met: + - `dataOffset`is u64 overflow, + - `dataOffset` + `length` is u64 overflow + - `dataOffset` + `length` is larger than the length of last callee's return data +3. common error constraints: + - current call fails. + - constrain `rw_counter_end_of_reversion = rw_counter_end_of_step + reversible_counter`. + - If it's a root call, it transits to `EndTx`. + - if it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. + +## Code + + Please refer to src/zkevm_specs/evm_circuit/execution/error_return_data_out_of_bound.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 d7d24da7b..aa426cb08 100644 --- a/src/zkevm_specs/evm_circuit/execution/__init__.py +++ b/src/zkevm_specs/evm_circuit/execution/__init__.py @@ -66,6 +66,7 @@ from .error_oog_dynamic_memory_expansion import * from .error_oog_memory_copy import * from .error_oog_log import * +from .error_return_data_out_of_bound import * from .error_write_protection import * from .error_oog_account_access import * from .error_code_store import * @@ -146,6 +147,7 @@ ExecutionState.ErrorOutOfGasAccountAccess: error_oog_account_access, ExecutionState.ErrorOutOfGasStaticMemoryExpansion: error_oog_static_memory_expansion, ExecutionState.ErrorOutOfGasSloadSstore: error_oog_sload_sstore, + ExecutionState.ErrorReturnDataOutOfBound: error_return_data_out_of_bound, # ExecutionState.ECRECOVER: , # ExecutionState.SHA256: , # ExecutionState.RIPEMD160: , diff --git a/src/zkevm_specs/evm_circuit/execution/error_return_data_out_of_bound.py b/src/zkevm_specs/evm_circuit/execution/error_return_data_out_of_bound.py new file mode 100644 index 000000000..f08736122 --- /dev/null +++ b/src/zkevm_specs/evm_circuit/execution/error_return_data_out_of_bound.py @@ -0,0 +1,32 @@ +from zkevm_specs.evm_circuit.table import CallContextFieldTag, RW +from zkevm_specs.util.param import MAX_N_BYTES +from ..instruction import Instruction +from ..opcode import Opcode + + +def error_return_data_out_of_bound(instruction: Instruction): + opcode = instruction.opcode_lookup(True) + instruction.constrain_equal(opcode, Opcode.RETURNDATACOPY) + + # skip first stack value, `memOffset` + data_offset = instruction.word_to_fq(instruction.stack_lookup(RW.Read, 1), MAX_N_BYTES) + length = instruction.word_to_fq(instruction.stack_lookup(RW.Read, 2), MAX_N_BYTES) + + # get the length of last callee's return data + return_data_length = instruction.call_context_lookup( + CallContextFieldTag.LastCalleeReturnDataLength, RW.Read + ) + + # verify if this call meets any one of error conditions + end = data_offset + length + is_data_offset_u64_overflow = instruction.is_u64_overflow(data_offset) + is_end_u64_overflow = instruction.is_u64_overflow(end) + is_end_over_return_data_len, _ = instruction.compare(return_data_length, end, MAX_N_BYTES) + + instruction.constrain_not_zero( + is_data_offset_u64_overflow + is_end_u64_overflow + is_end_over_return_data_len + ) + + instruction.constrain_error_state( + instruction.rw_counter_offset + instruction.curr.reversible_write_counter + ) diff --git a/tests/evm/test_error_return_data_out_of_bound.py b/tests/evm/test_error_return_data_out_of_bound.py new file mode 100644 index 000000000..dd9f38d4b --- /dev/null +++ b/tests/evm/test_error_return_data_out_of_bound.py @@ -0,0 +1,124 @@ +import pytest + +from zkevm_specs.util import Word +from zkevm_specs.evm_circuit import ( + ExecutionState, + StepState, + verify_steps, + Tables, + CallContextFieldTag, + Block, + Bytecode, + RWDictionary, +) +from zkevm_specs.util.param import MAX_U64 + + +TESTING_DATA = ( + ## is_root is True + # is_data_offset_u64_overflow is True + (MAX_U64 + 1, 0, True), + # is_end_u64_overflow is True + (MAX_U64, 1, True), + (1, MAX_U64, True), + (0, MAX_U64 + 1, True), + # is_end_over_return_data_len is True (as we set return_data_length to 320) + (321, 0, True), + (320, 1, True), + ## Same cases, with is_root set to False + (MAX_U64 + 1, 0, False), + (MAX_U64, 1, False), + (1, MAX_U64, False), + (0, MAX_U64 + 1, False), + (321, 0, False), + (320, 1, False), +) + + +@pytest.mark.parametrize("data_offset, length, is_root", TESTING_DATA) +def test_error_return_data_out_of_bound( + data_offset: int, + length: int, + is_root: bool, +): + bytecode = Bytecode().push32(32).push32(data_offset).push32(length).returndatacopy() + bytecode_hash = Word(bytecode.hash()) + + current_call_id = 1 if is_root else 2 + stack_pointer = 1021 + pc = 99 + reversible_write_counter = 2 + rw_counter = 4 if is_root is True else 16 + + # Only need `size` from RETURN opcode + rw_table = ( + RWDictionary(rw_counter) + .stack_read(current_call_id, stack_pointer + 1, Word(data_offset)) + .stack_read(current_call_id, stack_pointer + 2, Word(length)) + ) + + rw_table.call_context_read(current_call_id, CallContextFieldTag.LastCalleeReturnDataLength, 320) + + 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, False) \ + .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.ErrorReturnDataOutOfBound, + rw_counter=rw_counter, + call_id=current_call_id, + is_root=is_root, + is_create=False, + 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, + ) + 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=False, + code_hash=bytecode_hash, + program_counter=pc + 1, + stack_pointer=1024, + gas_left=0, + memory_word_size=0, + reversible_write_counter=reversible_write_counter, + ), + ], + )