This repository was archived by the owner on Jul 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 271
Spec ExecutionState::CALL
#82
Merged
han0110
merged 8 commits into
privacy-ethereum:master
from
han0110:feature/call-and-stop
Mar 29, 2022
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
8c3b4ee
feat: implement CALL
han0110 594ac5d
feat: implement branch handling callee with empty code anyway
han0110 dbc455a
refactor: rename ReversionInfo's method rw_counter to rw_counter_of_r…
han0110 faf643b
refactor: check gas_is_uint64 separately
han0110 84ce189
fix: use rlc_encode, fix doc, use select for callee_gas_left
han0110 62da798
feat: always use read on immutable call context
han0110 ecc5817
fix: make caller gas_left correct and improve testing
han0110 45989ff
refactor: index from set directly for fixed table to speedup testing
han0110 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # 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 | ||
| GAS_COST_ACCOUNT_COLD_ACCESS := 2600 | ||
| GAS_COST_CALL_EMPTY_ACCOUNT := 25000 | ||
| GAS_COST_CALL_WITH_VALUE := 9000 | ||
| gas_cost = ( | ||
| GAS_COST_WARM_ACCESS | ||
| + 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 | ||
| ) | ||
| ``` | ||
|
|
||
| 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) | ||
| ``` | ||
|
|
||
| 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`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| from ...util import ( | ||
| FQ, | ||
| N_BYTES_ACCOUNT_ADDRESS, | ||
| N_BYTES_GAS, | ||
| EMPTY_CODE_HASH, | ||
| GAS_COST_WARM_ACCESS, | ||
| GAS_COST_ACCOUNT_COLD_ACCESS, | ||
| GAS_COST_NEW_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 = 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) | ||
|
|
||
| # 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_of_reversion(), | ||
| ) | ||
|
|
||
| # 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_empty_code_hash = instruction.is_equal( | ||
| 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 | ||
| ) | ||
| gas_cost = ( | ||
| instruction.select( | ||
| is_warm_access, FQ(GAS_COST_WARM_ACCESS), FQ(GAS_COST_ACCOUNT_COLD_ACCESS) | ||
| ) | ||
| + has_value * (GAS_COST_CALL_WITH_VALUE + is_account_empty * GAS_COST_NEW_ACCOUNT) | ||
| + memory_expansion_gas_cost | ||
| ) | ||
|
|
||
| # Apply EIP 150. | ||
| # Note that sufficient gas_left is checked implicitly by constant_divmod. | ||
| gas_available = instruction.curr.gas_left - gas_cost | ||
| one_64th_gas, _ = instruction.constant_divmod(gas_available, FQ(64), N_BYTES_GAS) | ||
| all_but_one_64th_gas = gas_available - one_64th_gas | ||
| callee_gas_left = instruction.select( | ||
| gas_is_u64, | ||
| instruction.min(all_but_one_64th_gas, gas, N_BYTES_GAS), | ||
| all_but_one_64th_gas, | ||
| ) | ||
|
|
||
| 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(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 | ||
| 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 [ | ||
| (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)), | ||
| (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), | ||
| 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), | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.