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
7 changes: 4 additions & 3 deletions specs/opcode/F1CALL_F2CALLCODE_F4DELEGATECALL_FASTATICCALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ It creates a new sub context as setting caller address to parent caller's and ca
- For opcode `STATICCALL`:
It does not allow any state modifying instructions (is_static == 1) or sending ether to callee in the sub context.

And both `DELEGATECALL` and `STATICCALL` opcodes only pop 6 words from stack `gas`, `callee_address`, `call_data_offset`, `call_data_length`, `return_data_offset` and `return_data_length` (except the third popped word `value` for both `CALL` and `CALLCODE` opcodes).
Both `DELEGATECALL` and `STATICCALL` opcodes only pop 6 words from stack `gas`, `callee_address`, `call_data_offset`, `call_data_length`, `return_data_offset` and `return_data_length` (except the third popped word `value` for both `CALL` and `CALLCODE` opcodes).
There should be no `transfer` invocation for `CALLCODE`, `DELEGATECALL`and `STATICCALL` (only for CALL).

Before switching call context to the new one, it does several things:

Expand All @@ -34,7 +35,7 @@ Before switching call context to the new one, it does several things:
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 (zero for both `DELEGATECALL` and `STATICCALL` opcodes)
6. Check `value` could be transferred (only for `CALL` and `CALLCODE` opcodes)

The memory size is calculated as follows:

Expand Down Expand Up @@ -84,7 +85,7 @@ callee_gas_left := min(gas_available - floor(gas_available / 64), gas)

After switching call context, it does:

1. Transfer `value` (zero for both `DELEGATECALL` and `STATICCALL` opcodes)
1. Transfer `value` (only for `CALL` opcode)
2. Execution
1. If `callee_address` is a precompiled, it runs the pre-defined handler
2. Otherwise, it takes callee's code for execution
Expand Down
67 changes: 46 additions & 21 deletions src/zkevm_specs/evm/execution/callop.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,27 +108,50 @@ def callop(instruction: Instruction):
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
)
if is_call == 1:
# For CALL opcode, verify transfer, and get caller balance before
# transfer to constrain it should be greater than or equal to stack
# `value`.
(_, caller_balance), _ = instruction.transfer(
caller_address, callee_address, value, callee_reversion_info
)
elif is_callcode == 1:
# For CALLCODE opcode, get caller balance to constrain it should be
# greater than or equal to stack `value`.
caller_balance = instruction.account_read(caller_address, AccountFieldTag.Balance)

# Verify gas cost
callee_nonce = instruction.account_read(callee_address, AccountFieldTag.Nonce)
callee_code_hash = instruction.account_read(code_address, AccountFieldTag.CodeHash)
is_empty_code_hash = instruction.is_equal(
callee_code_hash, instruction.rlc_encode(EMPTY_CODE_HASH, 32)
)
is_account_empty = (
instruction.is_zero(callee_nonce)
* instruction.is_zero(callee_balance_prev)
* is_empty_code_hash
)
# For both CALL and CALLCODE opcodes, verify caller balance is greater than
# or equal to stack `value`.
if is_call + is_callcode == 1:
value_lt_caller_balance, value_eq_caller_balance = instruction.compare_word(
value, caller_balance
)
instruction.constrain_zero(1 - value_lt_caller_balance - value_eq_caller_balance)

# Load callee account `exists` value from auxilary witness data.
callee_exists = instruction.curr.aux_data

if callee_exists == 1:
# Get callee code hash.
callee_code_hash = instruction.account_read(code_address, AccountFieldTag.CodeHash)
is_empty_code_hash = instruction.is_equal(
callee_code_hash, instruction.rlc_encode(EMPTY_CODE_HASH, 32)
)
else: # callee_exists == 0
instruction.account_read(code_address, AccountFieldTag.NonExisting)
is_empty_code_hash = FQ(1)

# Verify gas cost.
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)
+ has_value
* (
GAS_COST_CALL_WITH_VALUE
# Only CALL opcode could invoke transfer to make empty account into non-empty.
+ is_call * (1 - callee_exists) * GAS_COST_NEW_ACCOUNT
)
+ memory_expansion_gas_cost
)

Expand Down Expand Up @@ -161,10 +184,11 @@ def callop(instruction: Instruction):
expected_value,
)

# Both CALL and CALLCODE opcodes have an extra stack pop `value`, and
# opcode DELEGATECALL has two extra call context lookups - parent caller
# address and value.
rw_counter_delta = 23 + is_call + is_callcode + is_delegatecall * 2
# For CALL opcode, it has an extra stack pop `value` and two account write for `transfer` call (+3).
# For CALLCODE opcode, it has an extra stack pop `value` and one account read for caller balance (+2).
# For DELEGATECALL opcode, it has two extra call context lookups for current caller address and value (+2).
# No extra lookups for STATICCALL opcode.
rw_counter_delta = 20 + is_call * 3 + is_callcode * 2 + is_delegatecall * 2
stack_pointer_delta = 5 + is_call + is_callcode

instruction.constrain_step_state_transition(
Expand All @@ -181,7 +205,8 @@ def callop(instruction: Instruction):
code_hash=Transition.same(),
)
else:
rw_counter_delta = 43 + is_call + is_callcode + is_delegatecall * 2
# Similar as above comment.
rw_counter_delta = 40 + is_call * 3 + is_callcode * 2 + is_delegatecall * 2
stack_pointer_delta = 5 + is_call + is_callcode

# Save caller's call state
Expand Down
42 changes: 32 additions & 10 deletions tests/evm/test_callop.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def memory_size(offset: int, length: int) -> int:
return 0
return (offset + length + 31) // 32

is_call = 1 if opcode == Opcode.CALL else 0
# Both CALL and CALLCODE opcodes have argument `value` on stack, but no for
# DELEGATECALL or STATICCALL.
has_value = stack.value != 0 if opcode in [Opcode.CALL, Opcode.CALLCODE] else False
Expand All @@ -87,7 +88,12 @@ def memory_size(offset: int, length: int) -> int:
) // 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 + callee.is_empty() * GAS_COST_NEW_ACCOUNT)
+ has_value
* (
GAS_COST_CALL_WITH_VALUE
# Only CALL opcode could invoke transfer to make empty account into non-empty.
+ is_call * callee.is_empty() * GAS_COST_NEW_ACCOUNT
)
+ memory_expansion_gas_cost
)
gas_available = caller_ctx.gas_left - gas_cost
Expand Down Expand Up @@ -184,6 +190,9 @@ def test_callop(
is_call = 1 if opcode == Opcode.CALL else 0
is_callcode = 1 if opcode == Opcode.CALLCODE else 0
is_delegatecall = 1 if opcode == Opcode.DELEGATECALL else 0
is_staticcall = 1 if opcode == Opcode.STATICCALL else 0

callee_exists = 0 if callee.is_empty() else 1

# Set `is_static == 1` for both DELEGATECALL and STATICCALL opcodes, or when
# `stack.value == 0` for both CALL and CALLCODE opcodes.
Expand Down Expand Up @@ -266,10 +275,11 @@ def test_callop(
)
)

# Both CALL and CALLCODE opcodes have an extra stack pop `value`, and opcode
# DELEGATECALL has two extra call context lookups - parent caller address
# and value.
call_id = 23 + is_call + is_callcode + is_delegatecall * 2
# For CALL opcode, it has an extra stack pop `value` and two account write for `transfer` call (+3).
# For CALLCODE opcode, it has an extra stack pop `value` and one account read for caller balance (+2).
# For DELEGATECALL opcode, it has two extra call context lookups for current caller address and value (+2).
# No extra lookups for STATICCALL opcode.
call_id = 20 + is_call * 3 + is_callcode * 2 + is_delegatecall * 2
rw_counter = call_id
next_program_counter = 232 if is_call + is_callcode == 1 else 199
stack_pointer = 1018 - is_call - is_callcode
Expand Down Expand Up @@ -329,11 +339,22 @@ def test_callop(
callee_balance = RLC(callee.balance + value, randomness)

# fmt: off
rw_dictionary \
.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(callee.nonce, randomness)) \
.account_read(code_address, AccountFieldTag.CodeHash, callee_bytecode_hash)
if is_call == 1:
# For `transfer` invocation.
rw_dictionary \
.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)
elif is_callcode == 1:
# Get caller balance to constrain it should be greater than or equal to stack `value`.
rw_dictionary \
.account_read(caller.address, AccountFieldTag.Balance, RLC(caller.balance, randomness))

if callee_exists == 1:
rw_dictionary \
.account_read(code_address, AccountFieldTag.CodeHash, callee_bytecode_hash)
else:
rw_dictionary \
.account_read(code_address, AccountFieldTag.NonExisting, RLC(1, randomness))

if is_empty_code_hash:
rw_dictionary \
Expand Down Expand Up @@ -395,6 +416,7 @@ def test_callop(
gas_left=caller_ctx.gas_left,
memory_size=caller_ctx.memory_size,
reversible_write_counter=caller_ctx.reversible_write_counter,
aux_data=callee_exists,
),
(
StepState(
Expand Down