Skip to content
This repository was archived by the owner on Jul 5, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions specs/opcode/F1CALL.md
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`.
2 changes: 2 additions & 0 deletions src/zkevm_specs/evm/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -59,4 +60,5 @@
ExecutionState.EXTCODEHASH: extcodehash,
ExecutionState.CopyToLog: copy_to_log,
ExecutionState.LOG: log,
ExecutionState.CALL: call,
}
5 changes: 4 additions & 1 deletion src/zkevm_specs/evm/execution/begin_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
199 changes: 199 additions & 0 deletions src/zkevm_specs/evm/execution/call.py
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(
Comment thread
ed255 marked this conversation as resolved.
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),
)
4 changes: 1 addition & 3 deletions src/zkevm_specs/evm/execution/extcodehash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading