From 8c3b4ee73ad2f7b4ef4f738918a41454e9f0b63d Mon Sep 17 00:00:00 2001 From: han0110 Date: Thu, 20 Jan 2022 21:36:36 +0800 Subject: [PATCH 1/8] feat: implement CALL --- specs/opcode/F1CALL.md | 97 ++++++++ src/zkevm_specs/evm/execution/__init__.py | 2 + src/zkevm_specs/evm/execution/call.py | 167 +++++++++++++ src/zkevm_specs/evm/instruction.py | 4 +- src/zkevm_specs/evm/typing.py | 9 + tests/evm/test_call.py | 271 ++++++++++++++++++++++ 6 files changed, 549 insertions(+), 1 deletion(-) create mode 100644 specs/opcode/F1CALL.md create mode 100644 src/zkevm_specs/evm/execution/call.py create mode 100644 tests/evm/test_call.py diff --git a/specs/opcode/F1CALL.md b/specs/opcode/F1CALL.md new file mode 100644 index 000000000..01f7eb3d2 --- /dev/null +++ b/specs/opcode/F1CALL.md @@ -0,0 +1,97 @@ +# CALL opcode + +## Procedure + +### EVM behavior + +The `CALL` opcode transfer specified amount of ether to callee and creates a new call context and switch to it. This is done by popping serveral words from stack: + +1. `gas` - The amount of gas caller want to give to callee (capped by rule in EIP150) +2. `callee_address` - The ether recipient whose code is to be executed (by taking the 20 LSB of popped word) +3. `value` - The amount of ether to be transfered +4. `call_data_offset` - The offset of call_data chunk in caller's memory as call_data for callee +5. `call_data_length` - The length of call_data chunk +6. `return_data_offset` - The offset of return_data chunk in caller's memory, which will be set to return_data from callee after call +7. `return_data_length` - The length of return_data chunk + +Before switching call context to the new one, it does several things: + +1. Expand memory +2. Add `callee_address` into access list +3. Calculate `gas_cost` and check `gas_left` is enough +4. Calculate `callee_gas_left` for new context by rule in EIP150 +5. Check `depth` is less than `1024` +6. Check `value` could be transfer + +The memory size is calculated as follows: + +``` +calc_memory_size(offset, length) := ceil((offset + length) / 32) if length != 0 else 0 +``` + +The memory expansion gas cost is calculated like this: + +``` +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 op is the additional `memory_size` needed: + +``` +next.memory_size := max( + curr.memory_size, + memory_size(call_data_offset, call_data_length), + memory_size(return_data_offset, return_data_length), +) +memory_expansion_gas_cost := calc_memory_cost(next.memory_size) - memory_cost(curr.memory_size) +``` + +The `gas_cost` is calculated like this: + +``` +GAS_COST_WARM_ACCESS := 100 +EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS := 2500 +GAS_COST_CALL_EMPTY_ACCOUNT := 25000 +GAS_COST_CALL_WITH_VALUE := 9000 +gas_cost = ( + GAS_COST_WARM_ACCESS + + is_cold_access * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS + + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT + + has_value * GAS_COST_CALL_WITH_VALUE + + memory_expansion_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 := max(gas_available - floor(gas_available / 64), gas) +``` + +After switching call context, it does: + +1. Transfer `value` +2. Execution + 1. If `callee_address` is a precompiled, it runs the pre-defined handler + 2. Otherwise, it takes callee's code for execution +3. Copy `return_data` of execution to caller specified memory chunk +4. Push `1` to stack if it succeeds, otherwise push `0` and revert everything done after switching call context + +### Circuit behavior + +The circuit takes current `rw_counter` as next call's `call_id` to make sure each call has a unique `call_id`. + +It pops the 7 words from stack, and take `result` of execution from prover to and push it to stack instantly. The reason it pushes the `result` before execution is to avoid the redundancy that every terminating `ExecutionState` needs to do the push. And it can do this because the `result` will also be in call context and checked in terminating `ExecutionState`s. + +It then checks the new call `is_persistent` only if current `is_persistent` and `result` of execution is success. If the new call is not persistent is due to current's call is not persistent, we need to propagate the `rw_counter_end_of_reversion` to make sure every state update has a corresponding reversion. + +Finally it stores current call context by writting to `rw_table` and checks the new call context is setup correctly by reading to `rw_table`, then does step state transition to a initialized one and begin the execution. + +In the end of execution, the terminating `ExecutionState` like `RETURN`, `REVERT` will copy the `return_data` to caller specified chunk. + +## Code + +Please refer to `src/zkevm_specs/evm/execution/call.py`. diff --git a/src/zkevm_specs/evm/execution/__init__.py b/src/zkevm_specs/evm/execution/__init__.py index 3967a3030..ef5606449 100644 --- a/src/zkevm_specs/evm/execution/__init__.py +++ b/src/zkevm_specs/evm/execution/__init__.py @@ -13,6 +13,7 @@ from .block_coinbase import * from .block_timestamp import * from .block_number import * +from .call import * from .calldatasize import * from .caller import * from .callvalue import * @@ -59,4 +60,5 @@ ExecutionState.EXTCODEHASH: extcodehash, ExecutionState.CopyToLog: copy_to_log, ExecutionState.LOG: log, + ExecutionState.CALL: call, } diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py new file mode 100644 index 000000000..348cf428a --- /dev/null +++ b/src/zkevm_specs/evm/execution/call.py @@ -0,0 +1,167 @@ +from ...util import ( + FQ, + RLC, + N_BYTES_ACCOUNT_ADDRESS, + N_BYTES_GAS, + EMPTY_CODE_HASH, + GAS_COST_WARM_ACCESS, + EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, + GAS_COST_CALL_EMPTY_ACCOUNT, + GAS_COST_CALL_WITH_VALUE, + GAS_STIPEND_CALL_WITH_VALUE, +) +from ..instruction import Instruction, Transition +from ..table import RW, CallContextFieldTag, AccountFieldTag +from ..precompiled import PrecompiledAddress + + +def call(instruction: Instruction): + instruction.responsible_opcode_lookup(instruction.opcode_lookup(True)) + + callee_call_id = instruction.curr.rw_counter + + tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) + reversion_info = instruction.reversion_info() + caller_address = instruction.call_context_lookup(CallContextFieldTag.CalleeAddress) + is_static = instruction.call_context_lookup(CallContextFieldTag.IsStatic) + depth = instruction.call_context_lookup(CallContextFieldTag.Depth) + + # Verify depth is less than 1024 + instruction.range_lookup(depth, 1024) + + # Lookup values from stack + gas_rlc = instruction.stack_pop() + callee_address_rlc = instruction.stack_pop() + value = instruction.stack_pop() + cd_offset_rlc = instruction.stack_pop() + cd_length_rlc = instruction.stack_pop() + rd_offset_rlc = instruction.stack_pop() + rd_length_rlc = instruction.stack_pop() + is_success = instruction.stack_push() + + # Verify is_success is a bool + instruction.constrain_bool(is_success) + + # Recomposition of random linear combination to integer + callee_address, _ = instruction.rlc_to_fq_unchecked(callee_address_rlc, N_BYTES_ACCOUNT_ADDRESS) + gas, gas_is_u64 = instruction.rlc_to_fq_unchecked(gas_rlc, N_BYTES_GAS) + cd_offset, cd_length = instruction.memory_offset_and_length(cd_offset_rlc, cd_length_rlc) + rd_offset, rd_length = instruction.memory_offset_and_length(rd_offset_rlc, rd_length_rlc) + + # Verify memory expansion + next_memory_size, memory_expansion_gas_cost = instruction.memory_expansion_dynamic_length( + cd_offset, + cd_length, + rd_offset, + rd_length, + ) + + # Add callee to access list + is_warm_access = instruction.add_account_to_access_list(tx_id, callee_address, reversion_info) + + # Propagate rw_counter_end_of_reversion and 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() + ) + is_reverted_by_caller = is_success.expr() == FQ(1) and reversion_info.is_persistent == FQ(0) + if is_reverted_by_caller: + # Propagate rw_counter_end_of_reversion when callee succeeds but one of callers revert at some point. + # Note that we subtract it with current caller's state_write_counter as callee's endpoint, where caller's + # state_write_counter here is added by 1 due to adding callee to access list + instruction.constrain_equal( + callee_reversion_info.rw_counter_end_of_reversion, + reversion_info.rw_counter_end_of_reversion - reversion_info.state_write_counter, + ) + + # Check not is_static if call has value + has_value = 1 - instruction.is_zero(value) + instruction.constrain_zero(has_value * is_static) + + # Verify transfer + _, (_, callee_balance_prev) = instruction.transfer( + caller_address, callee_address, value, callee_reversion_info + ) + + # Verify gas cost + callee_nonce = instruction.account_read(callee_address, AccountFieldTag.Nonce) + callee_code_hash = instruction.account_read(callee_address, AccountFieldTag.CodeHash) + is_zero_nonce = instruction.is_zero(callee_nonce) + is_zero_balance = instruction.is_zero(callee_balance_prev) + is_empty_code_hash = instruction.is_equal( + callee_code_hash, RLC(EMPTY_CODE_HASH, instruction.randomness) + ) + is_account_empty = is_zero_nonce * is_zero_balance * is_empty_code_hash + gas_cost = ( + GAS_COST_WARM_ACCESS + + (1 - is_warm_access) * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS + + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT + + has_value * GAS_COST_CALL_WITH_VALUE + + 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 = all_but_one_64th_gas + if gas_is_u64: + callee_gas_left = instruction.min(callee_gas_left, gas, N_BYTES_GAS) + + if callee_address in list(PrecompiledAddress): + # TODO: Handle precompile + raise NotImplementedError + else: + # Save caller's call state + for (field_tag, expected_value) in [ + (CallContextFieldTag.IsRoot, FQ(instruction.curr.is_root)), + (CallContextFieldTag.IsCreate, FQ(instruction.curr.is_create)), + (CallContextFieldTag.CodeSource, instruction.curr.code_source.expr()), + (CallContextFieldTag.ProgramCounter, instruction.curr.program_counter + 1), + (CallContextFieldTag.StackPointer, instruction.curr.stack_pointer + 6), + (CallContextFieldTag.GasLeft, instruction.curr.gas_left - gas_cost - callee_gas_left), + (CallContextFieldTag.MemorySize, next_memory_size), + (CallContextFieldTag.StateWriteCounter, instruction.curr.state_write_counter + 1), + ]: + instruction.constrain_equal( + instruction.call_context_lookup(field_tag, RW.Write), + expected_value, + ) + + # Setup next call's context. Note that RwCounterEndOfReversion, IsPersistent + # have been checked above. + for (field_tag, expected_value) in [ + (CallContextFieldTag.CallerId, instruction.curr.call_id), + (CallContextFieldTag.TxId, tx_id.expr()), + (CallContextFieldTag.Depth, depth.expr() + 1), + (CallContextFieldTag.CallerAddress, caller_address.expr()), + (CallContextFieldTag.CalleeAddress, callee_address), + (CallContextFieldTag.CallDataOffset, cd_offset), + (CallContextFieldTag.CallDataLength, cd_length), + (CallContextFieldTag.ReturnDataOffset, rd_offset), + (CallContextFieldTag.ReturnDataLength, rd_length), + (CallContextFieldTag.Value, value.expr()), + (CallContextFieldTag.IsSuccess, is_success.expr()), + (CallContextFieldTag.IsStatic, is_static.expr()), + (CallContextFieldTag.LastCalleeId, FQ(0)), + (CallContextFieldTag.LastCalleeReturnDataOffset, FQ(0)), + (CallContextFieldTag.LastCalleeReturnDataLength, FQ(0)), + ]: + instruction.constrain_equal( + instruction.call_context_lookup(field_tag, call_id=callee_call_id), + expected_value, + ) + + # Give gas stipend if value is not zero + callee_gas_left += has_value * GAS_STIPEND_CALL_WITH_VALUE + + instruction.step_state_transition_to_new_context( + rw_counter=Transition.delta(44), + call_id=Transition.to(callee_call_id), + is_root=Transition.to(False), + is_create=Transition.to(False), + code_source=Transition.to(callee_code_hash), + gas_left=Transition.to(callee_gas_left), + state_write_counter=Transition.to(2), + ) diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index 4d85e3d80..ff59c6154 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -496,7 +496,9 @@ def reversion_info(self, call_id: Expression = None) -> ReversionInfo: ) is_persistent = self.call_context_lookup(CallContextFieldTag.IsPersistent, call_id=call_id) return ReversionInfo( - rw_counter_end_of_reversion, is_persistent, self.curr.state_write_counter + rw_counter_end_of_reversion, + is_persistent, + self.curr.state_write_counter if call_id is None else FQ(0), ) def stack_pop(self) -> RLC: diff --git a/src/zkevm_specs/evm/typing.py b/src/zkevm_specs/evm/typing.py index 7ffbde178..c5dcd8183 100644 --- a/src/zkevm_specs/evm/typing.py +++ b/src/zkevm_specs/evm/typing.py @@ -341,6 +341,15 @@ def call_context_read( RW.Read, RWTableTag.CallContext, key1=FQ(call_id), key2=FQ(field_tag), value=value ) + def call_context_write( + self, call_id: IntOrFQ, field_tag: CallContextFieldTag, value: Union[int, FQ, RLC] + ) -> RWDictionary: + if isinstance(value, int): + value = FQ(value) + return self._append( + RW.Write, RWTableTag.CallContext, key1=FQ(call_id), key2=FQ(field_tag), value=value + ) + def tx_refund_read(self, tx_id: IntOrFQ, refund: IntOrFQ) -> RWDictionary: return self._append( RW.Read, RWTableTag.TxRefund, key1=FQ(tx_id), value=FQ(refund), value_prev=FQ(refund) diff --git a/tests/evm/test_call.py b/tests/evm/test_call.py new file mode 100644 index 000000000..29714da98 --- /dev/null +++ b/tests/evm/test_call.py @@ -0,0 +1,271 @@ +import pytest +from collections import namedtuple +from itertools import chain + +from zkevm_specs.evm import ( + ExecutionState, + StepState, + verify_steps, + Tables, + AccountFieldTag, + CallContextFieldTag, + Block, + Account, + Bytecode, + RWDictionary, +) +from zkevm_specs.util import rand_fq, RLC, EMPTY_CODE_HASH + +CallContext = namedtuple( + "CallContext", + [ + "rw_counter_end_of_reversion", + "is_persistent", + "is_static", + "gas_left", + "memory_size", + "state_write_counter", + ], + defaults=[0, True, False, 0, 0, 2], +) +Stack = namedtuple( + "Stack", + ["gas", "value", "cd_offset", "cd_length", "rd_offset", "rd_length"], + defaults=[0, 0, 0, 0, 0, 0], +) +Expected = namedtuple( + "Expected", + ["caller_gas_left", "callee_gas_left", "caller_memory_size"], +) + +RETURN_BYTECODE = Bytecode().return_(0, 0) +REVERT_BYTECODE = Bytecode().revert(0, 0) + +CALLER = Account(address=0xFE, balance=int(1e20)) +CALLEE_WITH_NOTHING = Account(address=0xFF) +CALLEE_WITH_BALANCE = Account(address=0xFF, balance=int(1e18)) +CALLEE_WITH_RETURN_BYTECODE = Account(address=0xFF, code=RETURN_BYTECODE) +CALLEE_WITH_REVERT_BYTECODE = Account(address=0xFF, code=REVERT_BYTECODE) + +TESTING_DATA = ( + # Transfer 1 ether to empty account, successfully + ( + CALLER, + CALLEE_WITH_NOTHING, + CallContext(gas_left=37000, is_persistent=True, is_static=False), + Stack(value=int(1e18)), + Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), + ), + # Transfer 1 ether to non-empty account, successfully + ( + CALLER, + CALLEE_WITH_BALANCE, + CallContext(gas_left=12000, is_persistent=True, is_static=False), + Stack(value=int(1e18)), + Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), + ), + # Transfer 1 ether to contract, caller reverts, callee succeeds + ( + CALLER, + CALLEE_WITH_RETURN_BYTECODE, + CallContext( + gas_left=12000, rw_counter_end_of_reversion=88, is_persistent=False, is_static=False + ), + Stack(value=int(1e18)), + Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), + ), + # Transfer 1 ether to contract, caller succeeds, callee reverts + ( + CALLER, + CALLEE_WITH_REVERT_BYTECODE, + CallContext(gas_left=12000, is_persistent=True, is_static=False), + Stack(value=int(1e18)), + Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), + ), + # Transfer 1 ether to contract, caller reverts, callee reverts + ( + CALLER, + CALLEE_WITH_REVERT_BYTECODE, + CallContext( + gas_left=12000, rw_counter_end_of_reversion=88, is_persistent=False, is_static=False + ), + Stack(value=int(1e18)), + Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), + ), + # Call contract with 0 gas in stack + ( + CALLER, + CALLEE_WITH_RETURN_BYTECODE, + CallContext(gas_left=3000, is_persistent=True, is_static=False), + Stack(), + Expected(caller_gas_left=400, callee_gas_left=0, caller_memory_size=0), + ), + # Call contract with gas less than cap in stack + ( + CALLER, + CALLEE_WITH_RETURN_BYTECODE, + CallContext(gas_left=3000, is_persistent=True, is_static=False), + Stack(gas=100), + Expected(caller_gas_left=300, callee_gas_left=100, caller_memory_size=0), + ), + # Call contract with gas greater than cap in stack + ( + CALLER, + CALLEE_WITH_RETURN_BYTECODE, + CallContext(gas_left=3000, is_persistent=True, is_static=False), + Stack(gas=400), + Expected(caller_gas_left=6, callee_gas_left=394, caller_memory_size=0), + ), + # Call contract with memory expansion by call data + ( + CALLER, + CALLEE_WITH_RETURN_BYTECODE, + CallContext(gas_left=3000, is_persistent=True, is_static=False), + Stack(cd_offset=64, cd_length=32, rd_offset=0, rd_length=32), + Expected(caller_gas_left=391, callee_gas_left=0, caller_memory_size=3), + ), + # Call contract with memory expansion by return data + ( + CALLER, + CALLEE_WITH_RETURN_BYTECODE, + CallContext(gas_left=3000, is_persistent=True, is_static=False), + Stack(cd_offset=0, cd_length=32, rd_offset=64, rd_length=32), + Expected(caller_gas_left=391, callee_gas_left=0, caller_memory_size=3), + ), +) + + +@pytest.mark.parametrize("caller, callee, caller_ctx, stack, expected", TESTING_DATA) +def test_call( + caller: Account, callee: Account, caller_ctx: CallContext, stack: Stack, expected: Expected +): + randomness = rand_fq() + + caller_balance_prev = RLC(caller.balance, randomness) + callee_balance_prev = RLC(callee.balance, randomness) + caller_balance = RLC(caller.balance - stack.value, randomness) + callee_balance = RLC(callee.balance + stack.value, randomness) + caller_bytecode = ( + Bytecode() + .call( + stack.gas, + callee.address, + stack.value, + stack.cd_offset, + stack.cd_length, + stack.rd_offset, + stack.rd_length, + ) + .stop() + ) + caller_bytecode_hash = RLC(caller_bytecode.hash(), randomness) + callee_bytecode_hash = RLC(callee.code_hash(), randomness) + + is_success = False if callee is CALLEE_WITH_REVERT_BYTECODE else True + 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.state_write_counter + 1) + if is_reverted_by_caller + else 0 + ) + ) + + tables = Tables( + block_table=set(Block().table_assignments(randomness)), + tx_table=set(), + bytecode_table=set( + chain( + caller_bytecode.table_assignments(randomness), + callee.code.table_assignments(randomness), + ) + ), + rw_table=set( + # fmt: off + RWDictionary(24) + .call_context_read(1, CallContextFieldTag.TxId, 1) + .call_context_read(1, CallContextFieldTag.RwCounterEndOfReversion, caller_ctx.rw_counter_end_of_reversion) + .call_context_read(1, CallContextFieldTag.IsPersistent, caller_ctx.is_persistent) + .call_context_read(1, CallContextFieldTag.CalleeAddress, caller.address) + .call_context_read(1, CallContextFieldTag.IsStatic, False) + .call_context_read(1, CallContextFieldTag.Depth, 1) + .stack_read(1, 1017, RLC(stack.gas, randomness)) + .stack_read(1, 1018, RLC(callee.address, randomness)) + .stack_read(1, 1019, RLC(stack.value, randomness)) + .stack_read(1, 1020, RLC(stack.cd_offset, randomness)) + .stack_read(1, 1021, RLC(stack.cd_length, randomness)) + .stack_read(1, 1022, RLC(stack.rd_offset, randomness)) + .stack_read(1, 1023, RLC(stack.rd_length, randomness)) + .stack_write(1, 1023, RLC(is_success, randomness)) + .tx_access_list_account_write(1, callee.address, 1, 0, caller_ctx.rw_counter_end_of_reversion - caller_ctx.state_write_counter) + .call_context_read(24, CallContextFieldTag.RwCounterEndOfReversion, callee_rw_counter_end_of_reversion) + .call_context_read(24, CallContextFieldTag.IsPersistent, callee_is_persistent) + .account_write(caller.address, AccountFieldTag.Balance, caller_balance, caller_balance_prev, callee_rw_counter_end_of_reversion) + .account_write(callee.address, AccountFieldTag.Balance, callee_balance, callee_balance_prev, callee_rw_counter_end_of_reversion - 1) + .account_read(callee.address, AccountFieldTag.Nonce, RLC(0, randomness)) + .account_read(callee.address, AccountFieldTag.CodeHash, callee_bytecode_hash) + .call_context_write(1, CallContextFieldTag.IsRoot, True) + .call_context_write(1, CallContextFieldTag.IsCreate, False) + .call_context_write(1, CallContextFieldTag.CodeSource, caller_bytecode_hash) + .call_context_write(1, CallContextFieldTag.ProgramCounter, 232) + .call_context_write(1, CallContextFieldTag.StackPointer, 1023) + .call_context_write(1, CallContextFieldTag.GasLeft, expected.caller_gas_left) + .call_context_write(1, CallContextFieldTag.MemorySize, expected.caller_memory_size) + .call_context_write(1, CallContextFieldTag.StateWriteCounter, caller_ctx.state_write_counter + 1) + .call_context_read(24, CallContextFieldTag.CallerId, 1) + .call_context_read(24, CallContextFieldTag.TxId, 1) + .call_context_read(24, CallContextFieldTag.Depth, 2) + .call_context_read(24, CallContextFieldTag.CallerAddress, caller.address) + .call_context_read(24, CallContextFieldTag.CalleeAddress, callee.address) + .call_context_read(24, CallContextFieldTag.CallDataOffset, stack.cd_offset) + .call_context_read(24, CallContextFieldTag.CallDataLength, stack.cd_length) + .call_context_read(24, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) + .call_context_read(24, CallContextFieldTag.ReturnDataLength, stack.rd_length) + .call_context_read(24, CallContextFieldTag.Value, RLC(stack.value, randomness)) + .call_context_read(24, CallContextFieldTag.IsSuccess, is_success) + .call_context_read(24, CallContextFieldTag.IsStatic, caller_ctx.is_static) + .call_context_read(24, CallContextFieldTag.LastCalleeId, 0) + .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataOffset, 0) + .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataLength, 0) + .rws + # fmt: on + ), + ) + + verify_steps( + randomness=randomness, + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.CALL, + rw_counter=24, + call_id=1, + is_root=True, + is_create=False, + code_source=caller_bytecode_hash, + program_counter=231, + stack_pointer=1017, + gas_left=caller_ctx.gas_left, + memory_size=caller_ctx.memory_size, + state_write_counter=caller_ctx.state_write_counter, + ), + StepState( + execution_state=ExecutionState.STOP + if callee.code_hash() == EMPTY_CODE_HASH + else ExecutionState.PUSH, + rw_counter=68, + call_id=24, + is_root=False, + is_create=False, + code_source=callee_bytecode_hash, + program_counter=0, + stack_pointer=1024, + gas_left=expected.callee_gas_left, + state_write_counter=2, + ), + ], + ) From 594ac5daa020402608f787c9b7df0c35358dd337 Mon Sep 17 00:00:00 2001 From: han0110 Date: Mon, 14 Mar 2022 23:50:47 +0800 Subject: [PATCH 2/8] feat: implement branch handling callee with empty code anyway --- src/zkevm_specs/evm/execution/call.py | 28 +++++ src/zkevm_specs/evm/typing.py | 4 + tests/evm/test_call.py | 175 ++++++++++++++++---------- 3 files changed, 143 insertions(+), 64 deletions(-) diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py index 348cf428a..53bcdc7c8 100644 --- a/src/zkevm_specs/evm/execution/call.py +++ b/src/zkevm_specs/evm/execution/call.py @@ -112,6 +112,34 @@ def call(instruction: Instruction): if callee_address in list(PrecompiledAddress): # TODO: Handle precompile raise NotImplementedError + elif is_empty_code_hash == FQ(1): + # Make sure call is successful + instruction.constrain_equal(is_success, FQ(1)) + + # Empty return_data + for (field_tag, expected_value) in [ + (CallContextFieldTag.LastCalleeId, FQ(0)), + (CallContextFieldTag.LastCalleeReturnDataOffset, FQ(0)), + (CallContextFieldTag.LastCalleeReturnDataLength, FQ(0)), + ]: + instruction.constrain_equal( + instruction.call_context_lookup(field_tag, RW.Write), + expected_value, + ) + + instruction.constrain_step_state_transition( + rw_counter=Transition.delta(24), + program_counter=Transition.delta(1), + stack_pointer=Transition.delta(6), + gas_left=Transition.delta(-gas_cost), + memory_size=Transition.to(next_memory_size), + state_write_counter=Transition.delta(3), + # Always stay same + call_id=Transition.same(), + is_root=Transition.same(), + is_create=Transition.same(), + code_source=Transition.same(), + ) else: # Save caller's call state for (field_tag, expected_value) in [ diff --git a/src/zkevm_specs/evm/typing.py b/src/zkevm_specs/evm/typing.py index c5dcd8183..eda5e814c 100644 --- a/src/zkevm_specs/evm/typing.py +++ b/src/zkevm_specs/evm/typing.py @@ -14,6 +14,7 @@ keccak256, GAS_COST_TX_CALL_DATA_PER_NON_ZERO_BYTE, GAS_COST_TX_CALL_DATA_PER_ZERO_BYTE, + EMPTY_CODE_HASH, ) from .table import ( RW, @@ -301,6 +302,9 @@ def code_hash(self) -> U256: def storage_trie_hash(self) -> U256: raise NotImplementedError("Trie has not been implemented") + def is_empty(self) -> bool: + return self.nonce == 0 and self.balance == 0 and self.code_hash() == EMPTY_CODE_HASH + class RWDictionary: rw_counter: int diff --git a/tests/evm/test_call.py b/tests/evm/test_call.py index 29714da98..32a84f51c 100644 --- a/tests/evm/test_call.py +++ b/tests/evm/test_call.py @@ -15,6 +15,12 @@ RWDictionary, ) from zkevm_specs.util import rand_fq, RLC, EMPTY_CODE_HASH +from zkevm_specs.util.param import ( + EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, + GAS_COST_CALL_EMPTY_ACCOUNT, + GAS_COST_CALL_WITH_VALUE, + GAS_COST_WARM_ACCESS, +) CallContext = namedtuple( "CallContext", @@ -38,12 +44,13 @@ ["caller_gas_left", "callee_gas_left", "caller_memory_size"], ) +STOP_BYTECODE = Bytecode().stop() RETURN_BYTECODE = Bytecode().return_(0, 0) REVERT_BYTECODE = Bytecode().revert(0, 0) CALLER = Account(address=0xFE, balance=int(1e20)) CALLEE_WITH_NOTHING = Account(address=0xFF) -CALLEE_WITH_BALANCE = Account(address=0xFF, balance=int(1e18)) +CALLEE_WITH_STOP_BYTECODE_AND_BALANCE = Account(address=0xFF, code=STOP_BYTECODE, balance=int(1e18)) CALLEE_WITH_RETURN_BYTECODE = Account(address=0xFF, code=RETURN_BYTECODE) CALLEE_WITH_REVERT_BYTECODE = Account(address=0xFF, code=REVERT_BYTECODE) @@ -59,7 +66,7 @@ # Transfer 1 ether to non-empty account, successfully ( CALLER, - CALLEE_WITH_BALANCE, + CALLEE_WITH_STOP_BYTECODE_AND_BALANCE, CallContext(gas_left=12000, is_persistent=True, is_static=False), Stack(value=int(1e18)), Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), @@ -174,6 +181,80 @@ def test_call( else 0 ) ) + is_warm_access = False + is_account_empty = callee.is_empty() + has_value = stack.value != 0 + memory_expansion_gas_cost = ( + expected.caller_memory_size * expected.caller_memory_size + - caller_ctx.memory_size * caller_ctx.memory_size + ) // 512 + 3 * (expected.caller_memory_size - caller_ctx.memory_size) + gas_cost = ( + GAS_COST_WARM_ACCESS + + (1 - is_warm_access) * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS + + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT + + has_value * GAS_COST_CALL_WITH_VALUE + + memory_expansion_gas_cost + ) + + # fmt: off + rw_dictionary = ( + RWDictionary(24) + .call_context_read(1, CallContextFieldTag.TxId, 1) + .call_context_read(1, CallContextFieldTag.RwCounterEndOfReversion, caller_ctx.rw_counter_end_of_reversion) + .call_context_read(1, CallContextFieldTag.IsPersistent, caller_ctx.is_persistent) + .call_context_read(1, CallContextFieldTag.CalleeAddress, caller.address) + .call_context_read(1, CallContextFieldTag.IsStatic, False) + .call_context_read(1, CallContextFieldTag.Depth, 1) + .stack_read(1, 1017, RLC(stack.gas, randomness)) + .stack_read(1, 1018, RLC(callee.address, randomness)) + .stack_read(1, 1019, RLC(stack.value, randomness)) + .stack_read(1, 1020, RLC(stack.cd_offset, randomness)) + .stack_read(1, 1021, RLC(stack.cd_length, randomness)) + .stack_read(1, 1022, RLC(stack.rd_offset, randomness)) + .stack_read(1, 1023, RLC(stack.rd_length, randomness)) + .stack_write(1, 1023, RLC(is_success, randomness)) + .tx_access_list_account_write(1, callee.address, 1, 0, caller_ctx.rw_counter_end_of_reversion - caller_ctx.state_write_counter) + .call_context_read(24, CallContextFieldTag.RwCounterEndOfReversion, callee_rw_counter_end_of_reversion) + .call_context_read(24, CallContextFieldTag.IsPersistent, callee_is_persistent) + .account_write(caller.address, AccountFieldTag.Balance, caller_balance, caller_balance_prev, callee_rw_counter_end_of_reversion) + .account_write(callee.address, AccountFieldTag.Balance, callee_balance, callee_balance_prev, callee_rw_counter_end_of_reversion - 1) + .account_read(callee.address, AccountFieldTag.Nonce, RLC(0, randomness)) + .account_read(callee.address, AccountFieldTag.CodeHash, callee_bytecode_hash) + ) + # fmt: on + + # fmt: off + if callee.code_hash() == EMPTY_CODE_HASH: + rw_dictionary \ + .call_context_write(1, CallContextFieldTag.LastCalleeId, 0) \ + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) + else: + rw_dictionary \ + .call_context_write(1, CallContextFieldTag.IsRoot, True) \ + .call_context_write(1, CallContextFieldTag.IsCreate, False) \ + .call_context_write(1, CallContextFieldTag.CodeSource, caller_bytecode_hash) \ + .call_context_write(1, CallContextFieldTag.ProgramCounter, 232) \ + .call_context_write(1, CallContextFieldTag.StackPointer, 1023) \ + .call_context_write(1, CallContextFieldTag.GasLeft, expected.caller_gas_left) \ + .call_context_write(1, CallContextFieldTag.MemorySize, expected.caller_memory_size) \ + .call_context_write(1, CallContextFieldTag.StateWriteCounter, caller_ctx.state_write_counter + 1) \ + .call_context_read(24, CallContextFieldTag.CallerId, 1) \ + .call_context_read(24, CallContextFieldTag.TxId, 1) \ + .call_context_read(24, CallContextFieldTag.Depth, 2) \ + .call_context_read(24, CallContextFieldTag.CallerAddress, caller.address) \ + .call_context_read(24, CallContextFieldTag.CalleeAddress, callee.address) \ + .call_context_read(24, CallContextFieldTag.CallDataOffset, stack.cd_offset) \ + .call_context_read(24, CallContextFieldTag.CallDataLength, stack.cd_length) \ + .call_context_read(24, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) \ + .call_context_read(24, CallContextFieldTag.ReturnDataLength, stack.rd_length) \ + .call_context_read(24, CallContextFieldTag.Value, RLC(stack.value, randomness)) \ + .call_context_read(24, CallContextFieldTag.IsSuccess, is_success) \ + .call_context_read(24, CallContextFieldTag.IsStatic, caller_ctx.is_static) \ + .call_context_read(24, CallContextFieldTag.LastCalleeId, 0) \ + .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ + .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataLength, 0) + # fmt: on tables = Tables( block_table=set(Block().table_assignments(randomness)), @@ -184,56 +265,7 @@ def test_call( callee.code.table_assignments(randomness), ) ), - rw_table=set( - # fmt: off - RWDictionary(24) - .call_context_read(1, CallContextFieldTag.TxId, 1) - .call_context_read(1, CallContextFieldTag.RwCounterEndOfReversion, caller_ctx.rw_counter_end_of_reversion) - .call_context_read(1, CallContextFieldTag.IsPersistent, caller_ctx.is_persistent) - .call_context_read(1, CallContextFieldTag.CalleeAddress, caller.address) - .call_context_read(1, CallContextFieldTag.IsStatic, False) - .call_context_read(1, CallContextFieldTag.Depth, 1) - .stack_read(1, 1017, RLC(stack.gas, randomness)) - .stack_read(1, 1018, RLC(callee.address, randomness)) - .stack_read(1, 1019, RLC(stack.value, randomness)) - .stack_read(1, 1020, RLC(stack.cd_offset, randomness)) - .stack_read(1, 1021, RLC(stack.cd_length, randomness)) - .stack_read(1, 1022, RLC(stack.rd_offset, randomness)) - .stack_read(1, 1023, RLC(stack.rd_length, randomness)) - .stack_write(1, 1023, RLC(is_success, randomness)) - .tx_access_list_account_write(1, callee.address, 1, 0, caller_ctx.rw_counter_end_of_reversion - caller_ctx.state_write_counter) - .call_context_read(24, CallContextFieldTag.RwCounterEndOfReversion, callee_rw_counter_end_of_reversion) - .call_context_read(24, CallContextFieldTag.IsPersistent, callee_is_persistent) - .account_write(caller.address, AccountFieldTag.Balance, caller_balance, caller_balance_prev, callee_rw_counter_end_of_reversion) - .account_write(callee.address, AccountFieldTag.Balance, callee_balance, callee_balance_prev, callee_rw_counter_end_of_reversion - 1) - .account_read(callee.address, AccountFieldTag.Nonce, RLC(0, randomness)) - .account_read(callee.address, AccountFieldTag.CodeHash, callee_bytecode_hash) - .call_context_write(1, CallContextFieldTag.IsRoot, True) - .call_context_write(1, CallContextFieldTag.IsCreate, False) - .call_context_write(1, CallContextFieldTag.CodeSource, caller_bytecode_hash) - .call_context_write(1, CallContextFieldTag.ProgramCounter, 232) - .call_context_write(1, CallContextFieldTag.StackPointer, 1023) - .call_context_write(1, CallContextFieldTag.GasLeft, expected.caller_gas_left) - .call_context_write(1, CallContextFieldTag.MemorySize, expected.caller_memory_size) - .call_context_write(1, CallContextFieldTag.StateWriteCounter, caller_ctx.state_write_counter + 1) - .call_context_read(24, CallContextFieldTag.CallerId, 1) - .call_context_read(24, CallContextFieldTag.TxId, 1) - .call_context_read(24, CallContextFieldTag.Depth, 2) - .call_context_read(24, CallContextFieldTag.CallerAddress, caller.address) - .call_context_read(24, CallContextFieldTag.CalleeAddress, callee.address) - .call_context_read(24, CallContextFieldTag.CallDataOffset, stack.cd_offset) - .call_context_read(24, CallContextFieldTag.CallDataLength, stack.cd_length) - .call_context_read(24, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) - .call_context_read(24, CallContextFieldTag.ReturnDataLength, stack.rd_length) - .call_context_read(24, CallContextFieldTag.Value, RLC(stack.value, randomness)) - .call_context_read(24, CallContextFieldTag.IsSuccess, is_success) - .call_context_read(24, CallContextFieldTag.IsStatic, caller_ctx.is_static) - .call_context_read(24, CallContextFieldTag.LastCalleeId, 0) - .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataOffset, 0) - .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataLength, 0) - .rws - # fmt: on - ), + rw_table=set(rw_dictionary.rws), ) verify_steps( @@ -253,19 +285,34 @@ def test_call( memory_size=caller_ctx.memory_size, state_write_counter=caller_ctx.state_write_counter, ), - StepState( - execution_state=ExecutionState.STOP + ( + StepState( + execution_state=ExecutionState.STOP, + rw_counter=rw_dictionary.rw_counter, + call_id=1, + is_root=True, + is_create=False, + code_source=caller_bytecode_hash, + program_counter=232, + stack_pointer=1023, + gas_left=caller_ctx.gas_left - gas_cost, + state_write_counter=caller_ctx.state_write_counter + 3, + ) if callee.code_hash() == EMPTY_CODE_HASH - else ExecutionState.PUSH, - rw_counter=68, - call_id=24, - is_root=False, - is_create=False, - code_source=callee_bytecode_hash, - program_counter=0, - stack_pointer=1024, - gas_left=expected.callee_gas_left, - state_write_counter=2, + else StepState( + execution_state=ExecutionState.STOP + if callee.code == STOP_BYTECODE + else ExecutionState.PUSH, + rw_counter=rw_dictionary.rw_counter, + call_id=24, + is_root=False, + is_create=False, + code_source=callee_bytecode_hash, + program_counter=0, + stack_pointer=1024, + gas_left=expected.callee_gas_left, + state_write_counter=2, + ) ), ], ) From dbc455a2c2e48dc65006aecf2236dde603034bd5 Mon Sep 17 00:00:00 2001 From: han0110 Date: Thu, 17 Mar 2022 16:38:44 +0800 Subject: [PATCH 3/8] refactor: rename ReversionInfo's method rw_counter to rw_counter_of_reversion --- src/zkevm_specs/evm/execution/call.py | 2 +- src/zkevm_specs/evm/instruction.py | 21 ++++++++++++--------- tests/evm/test_call.py | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py index 53bcdc7c8..5103616e4 100644 --- a/src/zkevm_specs/evm/execution/call.py +++ b/src/zkevm_specs/evm/execution/call.py @@ -71,7 +71,7 @@ def call(instruction: Instruction): # state_write_counter here is added by 1 due to adding callee to access list instruction.constrain_equal( callee_reversion_info.rw_counter_end_of_reversion, - reversion_info.rw_counter_end_of_reversion - reversion_info.state_write_counter, + reversion_info.rw_counter_of_reversion(), ) # Check not is_static if call has value diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index ff59c6154..8497748a6 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -82,10 +82,10 @@ def __init__( self.is_persistent = is_persistent.expr() self.state_write_counter = state_write_counter.expr() - def rw_counter(self) -> FQ: - rw_counter = self.rw_counter_end_of_reversion - self.state_write_counter + def rw_counter_of_reversion(self) -> FQ: + rw_counter_of_reversion = self.rw_counter_end_of_reversion - self.state_write_counter self.state_write_counter += 1 - return rw_counter + return rw_counter_of_reversion class Instruction: @@ -466,9 +466,9 @@ def state_write( row = self.rw_lookup(RW.Write, tag, key1, key2, key3, value, value_prev, aux0, aux1) - if reversion_info is not None and reversion_info.is_persistent == 0: + if reversion_info is not None and reversion_info.is_persistent == FQ(0): self.tables.rw_lookup( - rw_counter=reversion_info.rw_counter(), + rw_counter=reversion_info.rw_counter_of_reversion(), rw=FQ(RW.Write), tag=FQ(tag), key1=row.key1, @@ -491,10 +491,13 @@ def call_context_lookup( return self.rw_lookup(rw, RWTableTag.CallContext, call_id, FQ(field_tag)).value def reversion_info(self, call_id: Expression = None) -> ReversionInfo: - rw_counter_end_of_reversion = self.call_context_lookup( - CallContextFieldTag.RwCounterEndOfReversion, call_id=call_id - ) - is_persistent = self.call_context_lookup(CallContextFieldTag.IsPersistent, call_id=call_id) + [rw_counter_end_of_reversion, is_persistent] = [ + self.call_context_lookup(tag, call_id=call_id) + for tag in [ + CallContextFieldTag.RwCounterEndOfReversion, + CallContextFieldTag.IsPersistent, + ] + ] return ReversionInfo( rw_counter_end_of_reversion, is_persistent, diff --git a/tests/evm/test_call.py b/tests/evm/test_call.py index 32a84f51c..5ac42275b 100644 --- a/tests/evm/test_call.py +++ b/tests/evm/test_call.py @@ -213,11 +213,11 @@ def test_call( .stack_read(1, 1022, RLC(stack.rd_offset, randomness)) .stack_read(1, 1023, RLC(stack.rd_length, randomness)) .stack_write(1, 1023, RLC(is_success, randomness)) - .tx_access_list_account_write(1, callee.address, 1, 0, caller_ctx.rw_counter_end_of_reversion - caller_ctx.state_write_counter) + .tx_access_list_account_write(1, callee.address, 1, 0, rw_counter_of_reversion=None if caller_ctx.is_persistent else caller_ctx.rw_counter_end_of_reversion - caller_ctx.state_write_counter) .call_context_read(24, CallContextFieldTag.RwCounterEndOfReversion, callee_rw_counter_end_of_reversion) .call_context_read(24, CallContextFieldTag.IsPersistent, callee_is_persistent) - .account_write(caller.address, AccountFieldTag.Balance, caller_balance, caller_balance_prev, callee_rw_counter_end_of_reversion) - .account_write(callee.address, AccountFieldTag.Balance, callee_balance, callee_balance_prev, callee_rw_counter_end_of_reversion - 1) + .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(callee.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) .account_read(callee.address, AccountFieldTag.Nonce, RLC(0, randomness)) .account_read(callee.address, AccountFieldTag.CodeHash, callee_bytecode_hash) ) From faf643bbc98de5ec442a349a953b98bc31434cfc Mon Sep 17 00:00:00 2001 From: han0110 Date: Thu, 17 Mar 2022 20:25:22 +0800 Subject: [PATCH 4/8] refactor: check gas_is_uint64 separately --- src/zkevm_specs/evm/execution/call.py | 5 +++-- src/zkevm_specs/evm/instruction.py | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py index 5103616e4..248993979 100644 --- a/src/zkevm_specs/evm/execution/call.py +++ b/src/zkevm_specs/evm/execution/call.py @@ -43,8 +43,9 @@ def call(instruction: Instruction): instruction.constrain_bool(is_success) # Recomposition of random linear combination to integer - callee_address, _ = instruction.rlc_to_fq_unchecked(callee_address_rlc, N_BYTES_ACCOUNT_ADDRESS) - gas, gas_is_u64 = instruction.rlc_to_fq_unchecked(gas_rlc, N_BYTES_GAS) + callee_address = instruction.rlc_to_fq_unchecked(callee_address_rlc, N_BYTES_ACCOUNT_ADDRESS) + gas = instruction.rlc_to_fq_unchecked(gas_rlc, N_BYTES_GAS) + gas_is_u64 = instruction.is_zero(instruction.sum(gas_rlc.le_bytes[N_BYTES_GAS:])) cd_offset, cd_length = instruction.memory_offset_and_length(cd_offset_rlc, cd_length_rlc) rd_offset, rd_length = instruction.memory_offset_and_length(rd_offset_rlc, rd_length_rlc) diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index 8497748a6..3bdb64c19 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -333,10 +333,8 @@ def mul_word_by_u64(self, multiplicand: RLC, multiplier: Expression) -> Tuple[RL return self.rlc_encode(product_bytes), FQ(quotient_hi) - def rlc_to_fq_unchecked(self, word: RLC, n_bytes: int) -> Tuple[FQ, FQ]: - return self.bytes_to_fq(word.le_bytes[:n_bytes]), self.is_zero( - self.sum(word.le_bytes[n_bytes:]) - ) + def rlc_to_fq_unchecked(self, word: RLC, n_bytes: int) -> FQ: + return self.bytes_to_fq(word.le_bytes[:n_bytes]) def rlc_to_fq_exact(self, word: RLC, n_bytes: int) -> FQ: if any(word.le_bytes[n_bytes:]): From 84ce18944c7dd57cb3535dba7ef563bd1d9d3592 Mon Sep 17 00:00:00 2001 From: han0110 Date: Fri, 18 Mar 2022 00:13:54 +0800 Subject: [PATCH 5/8] fix: use rlc_encode, fix doc, use select for callee_gas_left --- specs/opcode/F1CALL.md | 2 +- src/zkevm_specs/evm/execution/call.py | 19 +++++++++++-------- src/zkevm_specs/evm/execution/extcodehash.py | 4 +--- src/zkevm_specs/evm/instruction.py | 4 ++-- src/zkevm_specs/util/arithmetic.py | 13 +++++-------- tests/evm/test_push.py | 2 +- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/specs/opcode/F1CALL.md b/specs/opcode/F1CALL.md index 01f7eb3d2..ad376fb82 100644 --- a/specs/opcode/F1CALL.md +++ b/specs/opcode/F1CALL.md @@ -68,7 +68,7 @@ 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 := max(gas_available - floor(gas_available / 64), gas) +callee_gas_left := min(gas_available - floor(gas_available / 64), gas) ``` After switching call context, it does: diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py index 248993979..36b7a47a2 100644 --- a/src/zkevm_specs/evm/execution/call.py +++ b/src/zkevm_specs/evm/execution/call.py @@ -1,6 +1,5 @@ from ...util import ( FQ, - RLC, N_BYTES_ACCOUNT_ADDRESS, N_BYTES_GAS, EMPTY_CODE_HASH, @@ -87,12 +86,14 @@ def call(instruction: Instruction): # Verify gas cost callee_nonce = instruction.account_read(callee_address, AccountFieldTag.Nonce) callee_code_hash = instruction.account_read(callee_address, AccountFieldTag.CodeHash) - is_zero_nonce = instruction.is_zero(callee_nonce) - is_zero_balance = instruction.is_zero(callee_balance_prev) is_empty_code_hash = instruction.is_equal( - callee_code_hash, RLC(EMPTY_CODE_HASH, instruction.randomness) + callee_code_hash, instruction.rlc_encode(EMPTY_CODE_HASH) + ) + is_account_empty = ( + instruction.is_zero(callee_nonce) + * instruction.is_zero(callee_balance_prev) + * is_empty_code_hash ) - is_account_empty = is_zero_nonce * is_zero_balance * is_empty_code_hash gas_cost = ( GAS_COST_WARM_ACCESS + (1 - is_warm_access) * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS @@ -106,9 +107,11 @@ def call(instruction: Instruction): 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 = all_but_one_64th_gas - if gas_is_u64: - callee_gas_left = instruction.min(callee_gas_left, gas, N_BYTES_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, + ) if callee_address in list(PrecompiledAddress): # TODO: Handle precompile diff --git a/src/zkevm_specs/evm/execution/extcodehash.py b/src/zkevm_specs/evm/execution/extcodehash.py index ced422074..fbfcbe01f 100644 --- a/src/zkevm_specs/evm/execution/extcodehash.py +++ b/src/zkevm_specs/evm/execution/extcodehash.py @@ -21,9 +21,7 @@ def extcodehash(instruction: Instruction): is_empty = ( instruction.is_zero(nonce) * instruction.is_zero(balance) - * instruction.is_equal( - code_hash, instruction.rlc_encode(EMPTY_CODE_HASH.to_bytes(64, "little")) - ) + * instruction.is_equal(code_hash, instruction.rlc_encode(EMPTY_CODE_HASH)) ) instruction.constrain_equal( diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index 3bdb64c19..abb6e6a84 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -350,8 +350,8 @@ def bytes_to_fq(self, value: bytes) -> FQ: assert len(value) <= MAX_N_BYTES, "Too many bytes to composite an integer in field" return FQ(int.from_bytes(value, "little")) - def rlc_encode(self, value: bytes) -> RLC: - return RLC(value, self.randomness) + def rlc_encode(self, value: Union[int, bytes], n_bytes: int = 32) -> RLC: + return RLC(value, self.randomness, n_bytes) def range_lookup(self, value: Expression, range: int): self.fixed_lookup(FixedTableTag.range_table_tag(range), value) diff --git a/src/zkevm_specs/util/arithmetic.py b/src/zkevm_specs/util/arithmetic.py index 3bd5325c2..88f9f0c03 100644 --- a/src/zkevm_specs/util/arithmetic.py +++ b/src/zkevm_specs/util/arithmetic.py @@ -35,16 +35,13 @@ class RLC: value: FQ le_bytes: bytes - def __init__( - self, value: Union[int, bytes], randomness: FQ = FQ(0), n_bytes: int = None - ) -> None: + def __init__(self, value: Union[int, bytes], randomness: FQ = FQ(0), n_bytes: int = 32) -> None: if isinstance(value, int): - value = value.to_bytes(32, "little") + value = value.to_bytes(n_bytes, "little") - if n_bytes is not None: - if len(value) > n_bytes: - raise ValueError(f"RLC expects to have {n_bytes} bytes, but got {len(value)} bytes") - value = value.ljust(n_bytes, b"\x00") + if len(value) > n_bytes: + raise ValueError(f"RLC expects to have {n_bytes} bytes, but got {len(value)} bytes") + value = value.ljust(n_bytes, b"\x00") self.value = FQ.linear_combine(value, randomness) self.le_bytes = value diff --git a/tests/evm/test_push.py b/tests/evm/test_push.py index f73e9c7c9..0de6612e5 100644 --- a/tests/evm/test_push.py +++ b/tests/evm/test_push.py @@ -27,7 +27,7 @@ def test_push(value_be_bytes: bytes): randomness = rand_fq() - value = RLC(bytes(reversed(value_be_bytes)), randomness, 32) + value = RLC(bytes(reversed(value_be_bytes)), randomness) bytecode = Bytecode().push(value_be_bytes, n_bytes=len(value_be_bytes)) bytecode_hash = RLC(bytecode.hash(), randomness) From 62da7988fa104a8f697a6cffa9c35f78acbc1043 Mon Sep 17 00:00:00 2001 From: han0110 Date: Tue, 22 Mar 2022 15:23:15 +0800 Subject: [PATCH 6/8] feat: always use read on immutable call context --- src/zkevm_specs/evm/execution/begin_tx.py | 5 ++++- src/zkevm_specs/evm/execution/call.py | 6 +++--- src/zkevm_specs/evm/table.py | 6 +++--- tests/evm/test_begin_tx.py | 5 ++++- tests/evm/test_call.py | 8 ++++---- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/zkevm_specs/evm/execution/begin_tx.py b/src/zkevm_specs/evm/execution/begin_tx.py index 4fc956810..f2f392220 100644 --- a/src/zkevm_specs/evm/execution/begin_tx.py +++ b/src/zkevm_specs/evm/execution/begin_tx.py @@ -83,13 +83,16 @@ def begin_tx(instruction: Instruction): (CallContextFieldTag.LastCalleeId, FQ(0)), (CallContextFieldTag.LastCalleeReturnDataOffset, FQ(0)), (CallContextFieldTag.LastCalleeReturnDataLength, FQ(0)), + (CallContextFieldTag.IsRoot, FQ(True)), + (CallContextFieldTag.IsCreate, FQ(False)), + (CallContextFieldTag.CodeSource, code_hash), ]: instruction.constrain_equal( instruction.call_context_lookup(tag, call_id=call_id), value ) instruction.step_state_transition_to_new_context( - rw_counter=Transition.delta(19), + rw_counter=Transition.delta(22), call_id=Transition.to(call_id), is_root=Transition.to(True), is_create=Transition.to(False), diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py index 36b7a47a2..570b9f957 100644 --- a/src/zkevm_specs/evm/execution/call.py +++ b/src/zkevm_specs/evm/execution/call.py @@ -147,9 +147,6 @@ def call(instruction: Instruction): else: # Save caller's call state for (field_tag, expected_value) in [ - (CallContextFieldTag.IsRoot, FQ(instruction.curr.is_root)), - (CallContextFieldTag.IsCreate, FQ(instruction.curr.is_create)), - (CallContextFieldTag.CodeSource, instruction.curr.code_source.expr()), (CallContextFieldTag.ProgramCounter, instruction.curr.program_counter + 1), (CallContextFieldTag.StackPointer, instruction.curr.stack_pointer + 6), (CallContextFieldTag.GasLeft, instruction.curr.gas_left - gas_cost - callee_gas_left), @@ -179,6 +176,9 @@ def call(instruction: Instruction): (CallContextFieldTag.LastCalleeId, FQ(0)), (CallContextFieldTag.LastCalleeReturnDataOffset, FQ(0)), (CallContextFieldTag.LastCalleeReturnDataLength, FQ(0)), + (CallContextFieldTag.IsRoot, FQ(False)), + (CallContextFieldTag.IsCreate, FQ(False)), + (CallContextFieldTag.CodeSource, callee_code_hash.expr()), ]: instruction.constrain_equal( instruction.call_context_lookup(field_tag, call_id=callee_call_id), diff --git a/src/zkevm_specs/evm/table.py b/src/zkevm_specs/evm/table.py index 172bae02b..6e1fd3cc8 100644 --- a/src/zkevm_specs/evm/table.py +++ b/src/zkevm_specs/evm/table.py @@ -199,6 +199,9 @@ class CallContextFieldTag(IntEnum): IsSuccess = auto() # to peek result in the future IsPersistent = auto() # to know if current call is within reverted call or not IsStatic = auto() # to know if state modification is within static call or not + IsRoot = auto() + IsCreate = auto() + CodeSource = auto() # The following are read-only data inside a call like previous section for # opcode RETURNDATASIZE and RETURNDATACOPY, except they will be updated when @@ -213,9 +216,6 @@ class CallContextFieldTag(IntEnum): # Note that stack and memory could also be included here, but since they # need extra constraints on their data format, so we separate them to be # different kinds of RWTableTag. - IsRoot = auto() - IsCreate = auto() - CodeSource = auto() ProgramCounter = auto() StackPointer = auto() GasLeft = auto() diff --git a/tests/evm/test_begin_tx.py b/tests/evm/test_begin_tx.py index 591adf2a3..20f842fff 100644 --- a/tests/evm/test_begin_tx.py +++ b/tests/evm/test_begin_tx.py @@ -130,6 +130,9 @@ def test_begin_tx(tx: Transaction, callee: Account, is_success: bool): .call_context_read(1, CallContextFieldTag.LastCalleeId, 0) .call_context_read(1, CallContextFieldTag.LastCalleeReturnDataOffset, 0) .call_context_read(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) + .call_context_read(1, CallContextFieldTag.IsRoot, True) + .call_context_read(1, CallContextFieldTag.IsCreate, False) + .call_context_read(1, CallContextFieldTag.CodeSource, bytecode_hash) .rws, # fmt: on ), @@ -147,7 +150,7 @@ def test_begin_tx(tx: Transaction, callee: Account, is_success: bool): execution_state=ExecutionState.EndTx if callee.code_hash() == EMPTY_CODE_HASH else ExecutionState.PUSH, - rw_counter=20, + rw_counter=23, call_id=1, is_root=True, is_create=False, diff --git a/tests/evm/test_call.py b/tests/evm/test_call.py index 5ac42275b..bf6e220bc 100644 --- a/tests/evm/test_call.py +++ b/tests/evm/test_call.py @@ -231,9 +231,6 @@ def test_call( .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) else: rw_dictionary \ - .call_context_write(1, CallContextFieldTag.IsRoot, True) \ - .call_context_write(1, CallContextFieldTag.IsCreate, False) \ - .call_context_write(1, CallContextFieldTag.CodeSource, caller_bytecode_hash) \ .call_context_write(1, CallContextFieldTag.ProgramCounter, 232) \ .call_context_write(1, CallContextFieldTag.StackPointer, 1023) \ .call_context_write(1, CallContextFieldTag.GasLeft, expected.caller_gas_left) \ @@ -253,7 +250,10 @@ def test_call( .call_context_read(24, CallContextFieldTag.IsStatic, caller_ctx.is_static) \ .call_context_read(24, CallContextFieldTag.LastCalleeId, 0) \ .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ - .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataLength, 0) + .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataLength, 0) \ + .call_context_read(24, CallContextFieldTag.IsRoot, False) \ + .call_context_read(24, CallContextFieldTag.IsCreate, False) \ + .call_context_read(24, CallContextFieldTag.CodeSource, callee_bytecode_hash) # fmt: on tables = Tables( From ecc5817c14a67ef87703d0b35f3cc1c46748cc2f Mon Sep 17 00:00:00 2001 From: han0110 Date: Wed, 23 Mar 2022 00:27:42 +0800 Subject: [PATCH 7/8] fix: make caller gas_left correct and improve testing --- specs/opcode/F1CALL.md | 7 +- src/zkevm_specs/evm/execution/call.py | 14 +- src/zkevm_specs/evm/instruction.py | 6 +- src/zkevm_specs/util/param.py | 12 +- tests/evm/test_call.py | 204 ++++++++++++-------------- 5 files changed, 114 insertions(+), 129 deletions(-) diff --git a/specs/opcode/F1CALL.md b/specs/opcode/F1CALL.md index ad376fb82..6197f22cb 100644 --- a/specs/opcode/F1CALL.md +++ b/specs/opcode/F1CALL.md @@ -52,14 +52,13 @@ The `gas_cost` is calculated like this: ``` GAS_COST_WARM_ACCESS := 100 -EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS := 2500 +GAS_COST_ACCOUNT_COLD_ACCESS := 2600 GAS_COST_CALL_EMPTY_ACCOUNT := 25000 GAS_COST_CALL_WITH_VALUE := 9000 gas_cost = ( GAS_COST_WARM_ACCESS - + is_cold_access * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS - + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT - + has_value * GAS_COST_CALL_WITH_VALUE + + GAS_COST_WARM_ACCESS if is_warm_access else GAS_COST_ACCOUNT_COLD_ACCESS + + has_value * (GAS_COST_CALL_WITH_VALUE + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT) + memory_expansion_gas_cost ) ``` diff --git a/src/zkevm_specs/evm/execution/call.py b/src/zkevm_specs/evm/execution/call.py index 570b9f957..e8b279b36 100644 --- a/src/zkevm_specs/evm/execution/call.py +++ b/src/zkevm_specs/evm/execution/call.py @@ -4,8 +4,8 @@ N_BYTES_GAS, EMPTY_CODE_HASH, GAS_COST_WARM_ACCESS, - EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, - GAS_COST_CALL_EMPTY_ACCOUNT, + GAS_COST_ACCOUNT_COLD_ACCESS, + GAS_COST_NEW_ACCOUNT, GAS_COST_CALL_WITH_VALUE, GAS_STIPEND_CALL_WITH_VALUE, ) @@ -95,10 +95,10 @@ def call(instruction: Instruction): * is_empty_code_hash ) gas_cost = ( - GAS_COST_WARM_ACCESS - + (1 - is_warm_access) * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS - + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT - + has_value * GAS_COST_CALL_WITH_VALUE + instruction.select( + is_warm_access, FQ(GAS_COST_WARM_ACCESS), FQ(GAS_COST_ACCOUNT_COLD_ACCESS) + ) + + has_value * (GAS_COST_CALL_WITH_VALUE + is_account_empty * GAS_COST_NEW_ACCOUNT) + memory_expansion_gas_cost ) @@ -135,7 +135,7 @@ def call(instruction: Instruction): rw_counter=Transition.delta(24), program_counter=Transition.delta(1), stack_pointer=Transition.delta(6), - gas_left=Transition.delta(-gas_cost), + gas_left=Transition.delta(has_value * GAS_STIPEND_CALL_WITH_VALUE - gas_cost), memory_size=Transition.to(next_memory_size), state_write_counter=Transition.delta(3), # Always stay same diff --git a/src/zkevm_specs/evm/instruction.py b/src/zkevm_specs/evm/instruction.py index abb6e6a84..ea6b21b5b 100644 --- a/src/zkevm_specs/evm/instruction.py +++ b/src/zkevm_specs/evm/instruction.py @@ -190,7 +190,7 @@ def constrain_step_state_transition(self, **kwargs: Transition): if isinstance(transition.value, int): transition.value = FQ(transition.value) assert next.expr() == curr.expr() + transition.value.expr(), ConstraintUnsatFailure( - f"State {key} should transit to {curr} + {transition.value}, but got {next}" + f"State {key} should transit to {curr} + {transition.value} ({curr + transition.value}), but got {next}" ) elif transition.kind == TransitionKind.To: if isinstance(transition.value, int): @@ -370,8 +370,8 @@ def fixed_lookup( self, tag: FixedTableTag, value0: Expression, - value1: Expression = None, - value2: Expression = None, + value1: Expression = FQ(0), + value2: Expression = FQ(0), ) -> FixedTableRow: return self.tables.fixed_lookup(FQ(tag), value0, value1, value2) diff --git a/src/zkevm_specs/util/param.py b/src/zkevm_specs/util/param.py index 1bd8299af..ad7ecbb05 100644 --- a/src/zkevm_specs/util/param.py +++ b/src/zkevm_specs/util/param.py @@ -53,14 +53,14 @@ GAS_COST_TX_CALL_DATA_PER_ZERO_BYTE = 4 # Gas cost of accessing account or storage slot, EIP 2929 GAS_COST_WARM_ACCESS = 100 -# Extra gas cost of not-yet-accessed account -EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS = 2500 -# Extra gas cost of not-yet-accessed storage slot -EXTRA_GAS_COST_STORAGE_SLOT_COLD_ACCESS = 2000 +# Gas cost of accessing not-yet-accessed account +GAS_COST_ACCOUNT_COLD_ACCESS = 2600 +# Extra gas cost of accessing not-yet-accessed account +EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS = GAS_COST_ACCOUNT_COLD_ACCESS - GAS_COST_WARM_ACCESS # Gas cost of calling with non-zero value GAS_COST_CALL_WITH_VALUE = 9000 -# Gas cost of calling empty account -GAS_COST_CALL_EMPTY_ACCOUNT = 25000 +# Gas cost of turning empty account into non-empty account +GAS_COST_NEW_ACCOUNT = 25000 # Gas stipend given if call with non-zero value GAS_STIPEND_CALL_WITH_VALUE = 2300 diff --git a/tests/evm/test_call.py b/tests/evm/test_call.py index bf6e220bc..fbc1aef7d 100644 --- a/tests/evm/test_call.py +++ b/tests/evm/test_call.py @@ -1,3 +1,4 @@ +import itertools import pytest from collections import namedtuple from itertools import chain @@ -16,10 +17,11 @@ ) from zkevm_specs.util import rand_fq, RLC, EMPTY_CODE_HASH from zkevm_specs.util.param import ( - EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, - GAS_COST_CALL_EMPTY_ACCOUNT, + GAS_COST_NEW_ACCOUNT, GAS_COST_CALL_WITH_VALUE, GAS_COST_WARM_ACCESS, + GAS_COST_ACCOUNT_COLD_ACCESS, + GAS_STIPEND_CALL_WITH_VALUE, ) CallContext = namedtuple( @@ -27,12 +29,11 @@ [ "rw_counter_end_of_reversion", "is_persistent", - "is_static", "gas_left", "memory_size", "state_write_counter", ], - defaults=[0, True, False, 0, 0, 2], + defaults=[0, True, 0, 0, 2], ) Stack = namedtuple( "Stack", @@ -41,7 +42,7 @@ ) Expected = namedtuple( "Expected", - ["caller_gas_left", "callee_gas_left", "caller_memory_size"], + ["caller_gas_left", "callee_gas_left", "next_memory_size"], ) STOP_BYTECODE = Bytecode().stop() @@ -54,97 +55,95 @@ CALLEE_WITH_RETURN_BYTECODE = Account(address=0xFF, code=RETURN_BYTECODE) CALLEE_WITH_REVERT_BYTECODE = Account(address=0xFF, code=REVERT_BYTECODE) -TESTING_DATA = ( - # Transfer 1 ether to empty account, successfully - ( - CALLER, + +def expected(callee: Account, caller_ctx: CallContext, stack: Stack, is_warm_access: bool): + def memory_size(offset: int, length: int) -> int: + if length == 0: + return 0 + return (offset + length + 31) // 32 + + is_account_empty = callee.is_empty() + has_value = stack.value != 0 + next_memory_size = max( + memory_size(stack.cd_offset, stack.cd_length), + memory_size(stack.rd_offset, stack.rd_length), + caller_ctx.memory_size, + ) + memory_expansion_gas_cost = ( + next_memory_size * next_memory_size - caller_ctx.memory_size * caller_ctx.memory_size + ) // 512 + 3 * (next_memory_size - caller_ctx.memory_size) + gas_cost = ( + (GAS_COST_WARM_ACCESS if is_warm_access else GAS_COST_ACCOUNT_COLD_ACCESS) + + has_value * (GAS_COST_CALL_WITH_VALUE + is_account_empty * GAS_COST_NEW_ACCOUNT) + + memory_expansion_gas_cost + ) + 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, stack.gas) + caller_gas_left = caller_ctx.gas_left - ( + gas_cost - has_value * GAS_STIPEND_CALL_WITH_VALUE + if callee.code_hash() == EMPTY_CODE_HASH + else gas_cost + callee_gas_left + ) + + return Expected( + caller_gas_left=caller_gas_left, + callee_gas_left=callee_gas_left + has_value * GAS_STIPEND_CALL_WITH_VALUE, + next_memory_size=next_memory_size, + ) + + +def gen_testing_data(): + callees = [ CALLEE_WITH_NOTHING, - CallContext(gas_left=37000, is_persistent=True, is_static=False), - Stack(value=int(1e18)), - Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), - ), - # Transfer 1 ether to non-empty account, successfully - ( - CALLER, CALLEE_WITH_STOP_BYTECODE_AND_BALANCE, - CallContext(gas_left=12000, is_persistent=True, is_static=False), - Stack(value=int(1e18)), - Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), - ), - # Transfer 1 ether to contract, caller reverts, callee succeeds - ( - CALLER, CALLEE_WITH_RETURN_BYTECODE, - CallContext( - gas_left=12000, rw_counter_end_of_reversion=88, is_persistent=False, is_static=False - ), - Stack(value=int(1e18)), - Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), - ), - # Transfer 1 ether to contract, caller succeeds, callee reverts - ( - CALLER, CALLEE_WITH_REVERT_BYTECODE, - CallContext(gas_left=12000, is_persistent=True, is_static=False), - Stack(value=int(1e18)), - Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), - ), - # Transfer 1 ether to contract, caller reverts, callee reverts - ( - CALLER, - CALLEE_WITH_REVERT_BYTECODE, - CallContext( - gas_left=12000, rw_counter_end_of_reversion=88, is_persistent=False, is_static=False - ), - Stack(value=int(1e18)), - Expected(caller_gas_left=400, callee_gas_left=2300, caller_memory_size=0), - ), - # Call contract with 0 gas in stack - ( - CALLER, - CALLEE_WITH_RETURN_BYTECODE, - CallContext(gas_left=3000, is_persistent=True, is_static=False), + ] + call_contexts = [ + CallContext(gas_left=100000, is_persistent=True), + CallContext(gas_left=100000, is_persistent=True, memory_size=8, state_write_counter=5), + CallContext(gas_left=100000, is_persistent=False, rw_counter_end_of_reversion=88), + ] + stacks = [ Stack(), - Expected(caller_gas_left=400, callee_gas_left=0, caller_memory_size=0), - ), - # Call contract with gas less than cap in stack - ( - CALLER, - CALLEE_WITH_RETURN_BYTECODE, - CallContext(gas_left=3000, is_persistent=True, is_static=False), + Stack(value=int(1e18)), Stack(gas=100), - Expected(caller_gas_left=300, callee_gas_left=100, caller_memory_size=0), - ), - # Call contract with gas greater than cap in stack - ( - CALLER, - CALLEE_WITH_RETURN_BYTECODE, - CallContext(gas_left=3000, is_persistent=True, is_static=False), - Stack(gas=400), - Expected(caller_gas_left=6, callee_gas_left=394, caller_memory_size=0), - ), - # Call contract with memory expansion by call data - ( - CALLER, - CALLEE_WITH_RETURN_BYTECODE, - CallContext(gas_left=3000, is_persistent=True, is_static=False), - Stack(cd_offset=64, cd_length=32, rd_offset=0, rd_length=32), - Expected(caller_gas_left=391, callee_gas_left=0, caller_memory_size=3), - ), - # Call contract with memory expansion by return data - ( - CALLER, - CALLEE_WITH_RETURN_BYTECODE, - CallContext(gas_left=3000, is_persistent=True, is_static=False), - Stack(cd_offset=0, cd_length=32, rd_offset=64, rd_length=32), - Expected(caller_gas_left=391, callee_gas_left=0, caller_memory_size=3), - ), -) + Stack(gas=100000), + Stack(cd_offset=64, cd_length=320, rd_offset=0, rd_length=32), + Stack(cd_offset=0, cd_length=32, rd_offset=64, rd_length=320), + Stack(cd_offset=0xFFFFFF, cd_length=0, rd_offset=0xFFFFFF, rd_length=0), + ] + is_warm_accesss = [True, False] + + return [ + ( + CALLER, + callee, + call_context, + stack, + is_warm_access, + expected(callee, call_context, stack, is_warm_access), + ) + for callee, call_context, stack, is_warm_access in itertools.product( + callees, call_contexts, stacks, is_warm_accesss + ) + ] + + +TESTING_DATA = gen_testing_data() -@pytest.mark.parametrize("caller, callee, caller_ctx, stack, expected", TESTING_DATA) +@pytest.mark.parametrize( + "caller, callee, caller_ctx, stack, is_warm_access, expected", TESTING_DATA +) def test_call( - caller: Account, callee: Account, caller_ctx: CallContext, stack: Stack, expected: Expected + caller: Account, + callee: Account, + caller_ctx: CallContext, + stack: Stack, + is_warm_access: bool, + expected: Expected, ): randomness = rand_fq() @@ -181,20 +180,6 @@ def test_call( else 0 ) ) - is_warm_access = False - is_account_empty = callee.is_empty() - has_value = stack.value != 0 - memory_expansion_gas_cost = ( - expected.caller_memory_size * expected.caller_memory_size - - caller_ctx.memory_size * caller_ctx.memory_size - ) // 512 + 3 * (expected.caller_memory_size - caller_ctx.memory_size) - gas_cost = ( - GAS_COST_WARM_ACCESS - + (1 - is_warm_access) * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS - + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT - + has_value * GAS_COST_CALL_WITH_VALUE - + memory_expansion_gas_cost - ) # fmt: off rw_dictionary = ( @@ -213,12 +198,12 @@ def test_call( .stack_read(1, 1022, RLC(stack.rd_offset, randomness)) .stack_read(1, 1023, RLC(stack.rd_length, randomness)) .stack_write(1, 1023, RLC(is_success, randomness)) - .tx_access_list_account_write(1, callee.address, 1, 0, rw_counter_of_reversion=None if caller_ctx.is_persistent else caller_ctx.rw_counter_end_of_reversion - caller_ctx.state_write_counter) + .tx_access_list_account_write(1, callee.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.state_write_counter) .call_context_read(24, CallContextFieldTag.RwCounterEndOfReversion, callee_rw_counter_end_of_reversion) .call_context_read(24, CallContextFieldTag.IsPersistent, callee_is_persistent) .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(callee.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) - .account_read(callee.address, AccountFieldTag.Nonce, RLC(0, randomness)) + .account_read(callee.address, AccountFieldTag.Nonce, RLC(callee.nonce, randomness)) .account_read(callee.address, AccountFieldTag.CodeHash, callee_bytecode_hash) ) # fmt: on @@ -228,26 +213,26 @@ def test_call( rw_dictionary \ .call_context_write(1, CallContextFieldTag.LastCalleeId, 0) \ .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ - .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) else: rw_dictionary \ .call_context_write(1, CallContextFieldTag.ProgramCounter, 232) \ .call_context_write(1, CallContextFieldTag.StackPointer, 1023) \ .call_context_write(1, CallContextFieldTag.GasLeft, expected.caller_gas_left) \ - .call_context_write(1, CallContextFieldTag.MemorySize, expected.caller_memory_size) \ + .call_context_write(1, CallContextFieldTag.MemorySize, expected.next_memory_size) \ .call_context_write(1, CallContextFieldTag.StateWriteCounter, caller_ctx.state_write_counter + 1) \ .call_context_read(24, CallContextFieldTag.CallerId, 1) \ .call_context_read(24, CallContextFieldTag.TxId, 1) \ .call_context_read(24, CallContextFieldTag.Depth, 2) \ .call_context_read(24, CallContextFieldTag.CallerAddress, caller.address) \ .call_context_read(24, CallContextFieldTag.CalleeAddress, callee.address) \ - .call_context_read(24, CallContextFieldTag.CallDataOffset, stack.cd_offset) \ + .call_context_read(24, CallContextFieldTag.CallDataOffset, stack.cd_offset if stack.cd_length != 0 else 0) \ .call_context_read(24, CallContextFieldTag.CallDataLength, stack.cd_length) \ - .call_context_read(24, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) \ + .call_context_read(24, CallContextFieldTag.ReturnDataOffset, stack.rd_offset if stack.rd_length != 0 else 0) \ .call_context_read(24, CallContextFieldTag.ReturnDataLength, stack.rd_length) \ .call_context_read(24, CallContextFieldTag.Value, RLC(stack.value, randomness)) \ .call_context_read(24, CallContextFieldTag.IsSuccess, is_success) \ - .call_context_read(24, CallContextFieldTag.IsStatic, caller_ctx.is_static) \ + .call_context_read(24, CallContextFieldTag.IsStatic, False) \ .call_context_read(24, CallContextFieldTag.LastCalleeId, 0) \ .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ .call_context_read(24, CallContextFieldTag.LastCalleeReturnDataLength, 0) \ @@ -295,7 +280,8 @@ def test_call( code_source=caller_bytecode_hash, program_counter=232, stack_pointer=1023, - gas_left=caller_ctx.gas_left - gas_cost, + gas_left=expected.caller_gas_left, + memory_size=expected.next_memory_size, state_write_counter=caller_ctx.state_write_counter + 3, ) if callee.code_hash() == EMPTY_CODE_HASH From 45989ff3d0cd315416792bbafcfc06f4b7b8cbc5 Mon Sep 17 00:00:00 2001 From: han0110 Date: Wed, 23 Mar 2022 00:28:33 +0800 Subject: [PATCH 8/8] refactor: index from set directly for fixed table to speedup testing --- src/zkevm_specs/evm/table.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/zkevm_specs/evm/table.py b/src/zkevm_specs/evm/table.py index 6e1fd3cc8..22cd620bb 100644 --- a/src/zkevm_specs/evm/table.py +++ b/src/zkevm_specs/evm/table.py @@ -342,8 +342,8 @@ def fixed_lookup( self, tag: Expression, value0: Expression, - value1: Expression = None, - value2: Expression = None, + value1: Expression = FQ(0), + value2: Expression = FQ(0), ) -> FixedTableRow: query = { "tag": tag, @@ -351,7 +351,10 @@ def fixed_lookup( "value1": value1, "value2": value2, } - return _lookup(FixedTableRow, self.fixed_table, query) + row = FixedTableRow(tag, value0, value1, value2) + if row not in self.fixed_table: + raise LookupUnsatFailure(FixedTableRow.__name__, query) + return row def block_lookup( self, field_tag: Expression, block_number: Expression = FQ(0)