Skip to content
This repository was archived by the owner on Jul 5, 2024. It is now read-only.
Merged
121 changes: 36 additions & 85 deletions src/zkevm_specs/evm/execution/callop.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
from zkevm_specs.evm.util.call_gadget import CallGadget
from zkevm_specs.util.param import N_BYTES_GAS
from ...util import (
EMPTY_CODE_HASH,
FQ,
GAS_COST_ACCOUNT_COLD_ACCESS,
GAS_COST_CALL_WITH_VALUE,
GAS_COST_NEW_ACCOUNT,
GAS_COST_WARM_ACCESS,
GAS_STIPEND_CALL_WITH_VALUE,
N_BYTES_ACCOUNT_ADDRESS,
N_BYTES_GAS,
RLC,
)
from ..instruction import Instruction, Transition
Expand Down Expand Up @@ -44,57 +39,35 @@ def callop(instruction: Instruction):
# Verify depth is less than 1024
instruction.range_lookup(depth, 1024)

# Lookup values from stack
gas_rlc = instruction.stack_pop()
code_address_rlc = instruction.stack_pop()
# The third stack pop `value` is not present for both DELEGATECALL and
# STATICCALL opcodes.
value = instruction.stack_pop() if is_call + is_callcode == 1 else RLC(0)
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
code_address = instruction.rlc_to_fq(code_address_rlc, N_BYTES_ACCOUNT_ADDRESS)

call = CallGadget(instruction, FQ(1), is_call, is_callcode, is_delegatecall)
# For opcode CALLCODE:
# - callee_address = caller_address
#
# For opcode DELEGATECALL:
# - callee_address = caller_address
# - caller_address = parent_caller_address
#
# Variable `code_address` will be used to get code hash.
callee_address = instruction.select(is_callcode + is_delegatecall, caller_address, code_address)
callee_address = instruction.select(
is_callcode + is_delegatecall, caller_address, call.callee_address
)
caller_address = instruction.select(is_delegatecall, parent_caller_address, caller_address)

gas = instruction.rlc_to_fq(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_address` to access list
is_warm_access = instruction.add_account_to_access_list(
tx_id, call.callee_address, reversion_info
)

# Add `code_address` to access list
is_warm_access = instruction.add_account_to_access_list(tx_id, code_address, reversion_info)
# Check not is_static if call has value
has_value = call.has_value
instruction.constrain_zero(has_value * is_static)

# 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()
callee_reversion_info.is_persistent,
reversion_info.is_persistent * call.is_success,
)
is_reverted_by_caller = is_success.expr() == FQ(1) and reversion_info.is_persistent == FQ(0)
is_reverted_by_caller = call.is_success == 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 reversible_write_counter as callee's endpoint, where caller's
Expand All @@ -104,16 +77,12 @@ def callop(instruction: Instruction):
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)

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
caller_address, callee_address, call.value, callee_reversion_info
)
elif is_callcode == 1:
# For CALLCODE opcode, get caller balance to constrain it should be
Expand All @@ -124,43 +93,24 @@ def callop(instruction: Instruction):
# 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
call.value, caller_balance
)
instruction.constrain_zero(1 - value_lt_caller_balance - value_eq_caller_balance)

callee_code_hash = instruction.account_read(code_address, AccountFieldTag.CodeHash)
# Check calle account existence with code_hash != 0
callee_exists = FQ(1) - instruction.is_zero(callee_code_hash)

if callee_exists == 1:
is_empty_code_hash = instruction.is_equal(
callee_code_hash, instruction.rlc_encode(EMPTY_CODE_HASH, 32)
)
else: # callee_exists == 0
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
# 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
gas_cost = call.gas_cost(
instruction,
is_warm_access,
is_call, # Only CALL opcode could invoke transfer to make empty account into non-empty.
)

# 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),
call.is_u64_gas,
instruction.min(all_but_one_64th_gas, call.gas, N_BYTES_GAS),
all_but_one_64th_gas,
)

Expand All @@ -171,9 +121,10 @@ def callop(instruction: Instruction):
is_precompile, FQ(instruction.next.execution_state in precompile_execution_states())
)

if is_empty_code_hash == FQ(1) and is_precompile == FQ(0):
no_callee_code = call.is_empty_code_hash + call.callee_not_exists
if no_callee_code == FQ(1) and is_precompile == FQ(0):
# Make sure call is successful
instruction.constrain_equal(is_success, FQ(1))
instruction.constrain_equal(call.is_success, FQ(1))

# Empty return_data
for (field_tag, expected_value) in [
Expand All @@ -198,7 +149,7 @@ def callop(instruction: Instruction):
program_counter=Transition.delta(1),
stack_pointer=Transition.delta(stack_pointer_delta),
gas_left=Transition.delta(has_value * GAS_STIPEND_CALL_WITH_VALUE - gas_cost),
memory_size=Transition.to(next_memory_size),
memory_size=Transition.to(call.next_memory_size),
reversible_write_counter=Transition.delta(3),
# Always stay same
call_id=Transition.same(),
Expand All @@ -219,7 +170,7 @@ def callop(instruction: Instruction):
instruction.curr.stack_pointer + stack_pointer_delta,
),
(CallContextFieldTag.GasLeft, instruction.curr.gas_left - gas_cost - callee_gas_left),
(CallContextFieldTag.MemorySize, next_memory_size),
(CallContextFieldTag.MemorySize, call.next_memory_size),
(
CallContextFieldTag.ReversibleWriteCounter,
instruction.curr.reversible_write_counter + 1,
Expand All @@ -238,22 +189,22 @@ def callop(instruction: Instruction):
(CallContextFieldTag.Depth, depth.expr() + 1),
(CallContextFieldTag.CallerAddress, caller_address.expr()),
(CallContextFieldTag.CalleeAddress, callee_address.expr()),
(CallContextFieldTag.CallDataOffset, cd_offset),
(CallContextFieldTag.CallDataLength, cd_length),
(CallContextFieldTag.ReturnDataOffset, rd_offset),
(CallContextFieldTag.ReturnDataLength, rd_length),
(CallContextFieldTag.CallDataOffset, call.cd_offset),
(CallContextFieldTag.CallDataLength, call.cd_length),
(CallContextFieldTag.ReturnDataOffset, call.rd_offset),
(CallContextFieldTag.ReturnDataLength, call.rd_length),
(
CallContextFieldTag.Value,
instruction.select(is_delegatecall, parent_call_value.expr(), value.expr()),
instruction.select(is_delegatecall, parent_call_value.expr(), call.value.expr()),
),
(CallContextFieldTag.IsSuccess, is_success.expr()),
(CallContextFieldTag.IsSuccess, call.is_success),
(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.CodeHash, callee_code_hash.expr()),
(CallContextFieldTag.CodeHash, call.callee_code_hash),
]:
instruction.constrain_equal(
instruction.call_context_lookup(field_tag, call_id=callee_call_id),
Expand All @@ -268,7 +219,7 @@ def callop(instruction: Instruction):
call_id=Transition.to(callee_call_id),
is_root=Transition.to(False),
is_create=Transition.to(False),
code_hash=Transition.to(callee_code_hash),
code_hash=Transition.to(call.callee_code_hash),
gas_left=Transition.to(callee_gas_left),
reversible_write_counter=Transition.to(2),
log_id=Transition.same(),
Expand Down
66 changes: 10 additions & 56 deletions src/zkevm_specs/evm/execution/oog_call.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
from ...util import (
FQ,
N_BYTES_ACCOUNT_ADDRESS,
EMPTY_CODE_HASH,
GAS_COST_WARM_ACCESS,
GAS_COST_ACCOUNT_COLD_ACCESS,
GAS_COST_CALL_WITH_VALUE,
GAS_COST_NEW_ACCOUNT,
)
from zkevm_specs.evm.util.call_gadget import CallGadget
from ...util import FQ
from ..instruction import Instruction, Transition
from ..table import CallContextFieldTag, AccountFieldTag
from ..table import CallContextFieldTag
from ..execution_state import ExecutionState
from ...util import N_BYTES_GAS
from ..opcode import Opcode
Expand All @@ -21,56 +14,17 @@ def oog_call(instruction: Instruction):
instruction.constrain_equal(opcode, Opcode.CALL)

tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId)
instruction.call_context_lookup(CallContextFieldTag.IsStatic)
# Lookup values from stack
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()
instruction.constrain_zero(is_success)

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,
)

has_value = 1 - instruction.is_zero(value)
callee_address = instruction.rlc_to_fq(callee_address_rlc, N_BYTES_ACCOUNT_ADDRESS)
# init CallGadget to handle stack vars.
call = CallGadget(instruction, FQ(0), FQ(1), FQ(0), FQ(0))

# TODO: handle PrecompiledContract oog cases

# Add callee to access list
is_warm_access = instruction.read_account_to_access_list(tx_id, callee_address)

# lookup balance of callee
callee_balance = instruction.account_read(callee_address, AccountFieldTag.Balance)
# Verify gas cost
callee_nonce = instruction.account_read(callee_address, AccountFieldTag.Nonce)
callee_code_hash = instruction.account_read(callee_address, AccountFieldTag.CodeHash)
is_warm_access = instruction.read_account_to_access_list(tx_id, call.callee_address)

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) * 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
)
# verify gas cost
gas_cost = call.gas_cost(instruction, is_warm_access)

# verify gas is insufficient
gas_not_enough, _ = instruction.compare(instruction.curr.gas_left, gas_cost, N_BYTES_GAS)
Expand All @@ -89,14 +43,14 @@ def oog_call(instruction: Instruction):
if instruction.curr.is_root:
# Do step state transition
instruction.constrain_step_state_transition(
rw_counter=Transition.delta(15),
rw_counter=Transition.delta(12),
call_id=Transition.same(),
)
else:
# when it is internal call, need to restore caller's state as finishing this call.
# Restore caller state to next StepState
instruction.step_state_transition_to_restored_context(
rw_counter_delta=15,
rw_counter_delta=12,
return_data_offset=FQ(0),
return_data_length=FQ(0),
gas_left=instruction.curr.gas_left,
Expand Down
Loading