Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Feat/#498 integrate precompiles into callop (#508)
Browse files Browse the repository at this point in the history
* feat: callop and PrecompileGadget (draft)

* feat: precompile oog

* feat: remove rlc in callop for PrecompileGadget

* test: refactor callop test for precompiles

* test: complete testing for precompiles in callop

* chore: add comments

* feat: complete precompile checks in precompile_gadget

* Update src/zkevm_specs/evm_circuit/execution/callop.py

Co-authored-by: Chih Cheng Liang <chihchengliang@gmail.com>

* fix: incorrect caller/callee call context

* fix: ecRecover allows input len is not 128 bytes

* feat: add copy rlc for precompiles input/output

* feat: add input/output rlc data check in ecRecover (PoC)

* refactor ecrecover rlc input

---------

Co-authored-by: Chih Cheng Liang <chihchengliang@gmail.com>
  • Loading branch information
KimiWu123 and ChihChengLiang committed Feb 21, 2024
1 parent b871e6b commit 163f3c0
Show file tree
Hide file tree
Showing 10 changed files with 715 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- `DELEGATECALL` creates a new sub context as setting caller address to parent caller's and callee address to current caller's, but with the code of the given account (callee). In particular the current `sender` (parent caller) and `value` remain the same.
- `STATICCALL` does not allow any state modifying instructions (is_static == 1) or sending ether to callee in the sub context.

These are done by popping serveral words from stack:
These are done by popping several 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)
Expand Down
3 changes: 3 additions & 0 deletions src/zkevm_specs/evm_circuit/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
from .precompiles.ecadd import *
from .precompiles.ecpairing import *
from .precompiles.ecmul import *
from .precompiles.error_oog_precompile import *


EXECUTION_STATE_IMPL: Dict[ExecutionState, Callable] = {
Expand Down Expand Up @@ -156,6 +157,8 @@
ExecutionState.ErrorOutOfGasSloadSstore: error_oog_sload_sstore,
ExecutionState.ErrorReturnDataOutOfBound: error_return_data_out_of_bound,
ExecutionState.ErrorOutOfGasCREATE: error_oog_create,
ExecutionState.ErrorOutOfGasPrecompile: error_oog_precompile,
# Precompiles
ExecutionState.ECRECOVER: ecRecover,
# ExecutionState.SHA256: ,
# ExecutionState.RIPEMD160: ,
Expand Down
134 changes: 130 additions & 4 deletions src/zkevm_specs/evm_circuit/execution/callop.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from zkevm_specs.evm_circuit.util.call_gadget import CallGadget
from zkevm_specs.util.param import N_BYTES_GAS, N_BYTES_STACK
from zkevm_specs.evm_circuit.util.precompile_gadget import PrecompileGadget
from zkevm_specs.util.hash import EMPTY_CODE_HASH
from zkevm_specs.util.param import N_BYTES_GAS, N_BYTES_MEMORY_WORD_SIZE, N_BYTES_STACK
from ...util import FQ, GAS_STIPEND_CALL_WITH_VALUE, Word, WordOrValue
from ..instruction import Instruction, Transition
from ..opcode import Opcode
from ..table import RW, CallContextFieldTag, AccountFieldTag
from ..table import RW, CallContextFieldTag, AccountFieldTag, CopyDataTypeTag
from ..execution_state import precompile_execution_states


Expand Down Expand Up @@ -115,13 +117,14 @@ def callop(instruction: Instruction):

# Make sure the state transition to ExecutionState for precompile if and
# only if the callee address is one of precompile
is_precompile = instruction.precompile(callee_address)
is_precompile = instruction.precompile(call.callee_address)
instruction.constrain_equal(
is_precompile, FQ(instruction.next.execution_state in precompile_execution_states())
)

stack_pointer_delta = 5 + is_call + is_callcode
no_callee_code = call.is_empty_code_hash + call.callee_not_exists
# precheck fails or callee has no code
if is_precheck_ok is False or (no_callee_code == FQ(1) and is_precompile == FQ(0)):
# Empty return_data
for field_tag, expected_value in [
Expand All @@ -147,7 +150,130 @@ def callop(instruction: Instruction):
is_create=Transition.same(),
code_hash=Transition.same_word(),
)
else:
# precompiles call
elif is_precheck_ok and is_precompile == FQ.one():
precompile_input_len: FQ = instruction.curr.aux_data[0]
precompile_return_length: FQ = instruction.curr.aux_data[1]
min_rd_copy_size = min(precompile_return_length, call.rd_length.n)

# precompiles have no code
instruction.constrain_equal(no_callee_code, FQ.one())
# precompiles address must be warm
instruction.constrain_equal(is_warm_access, FQ.one())

# Setup next call's context.
for field_tag, expected_value in [
(CallContextFieldTag.IsSuccess, call.is_success),
(CallContextFieldTag.CalleeAddress, callee_address_word),
(CallContextFieldTag.CallerId, instruction.curr.call_id),
(CallContextFieldTag.CallDataOffset, call.cd_offset),
(CallContextFieldTag.CallDataLength, call.cd_length),
(CallContextFieldTag.ReturnDataOffset, call.rd_offset),
(CallContextFieldTag.ReturnDataLength, call.rd_length),
]:
instruction.constrain_equal_word(
instruction.call_context_lookup_word(field_tag, RW.Write, callee_call_id),
WordOrValue(expected_value),
)

# Save caller's call state
for field_tag, expected_value in [
(CallContextFieldTag.ProgramCounter, instruction.curr.program_counter + 1),
(
CallContextFieldTag.StackPointer,
instruction.curr.stack_pointer + stack_pointer_delta,
),
(CallContextFieldTag.GasLeft, instruction.curr.gas_left - gas_cost - callee_gas_left),
(CallContextFieldTag.MemorySize, call.next_memory_size),
(
CallContextFieldTag.ReversibleWriteCounter,
instruction.curr.reversible_write_counter + 1,
),
(CallContextFieldTag.LastCalleeId, callee_call_id),
(CallContextFieldTag.LastCalleeReturnDataOffset, FQ.zero()),
(CallContextFieldTag.LastCalleeReturnDataLength, FQ(precompile_return_length)),
]:
instruction.constrain_equal(
instruction.call_context_lookup(field_tag, RW.Write),
expected_value,
)

### copy table lookup here
### is to rlc input and output to have an easy way to verify data

# RLC precompile input from memory
rw_counter_inc = instruction.rw_counter_offset
input_copy_rwc_inc = FQ.zero()
if precompile_input_len != FQ(0):
input_copy_rwc_inc, _ = instruction.copy_lookup(
instruction.curr.call_id,
CopyDataTypeTag.Memory,
callee_call_id,
CopyDataTypeTag.RlcAcc,
call.cd_offset,
FQ(call.cd_offset + precompile_input_len),
FQ.zero(),
FQ(precompile_input_len),
instruction.curr.rw_counter + rw_counter_inc,
)
rw_counter_inc += input_copy_rwc_inc

# RLC precompile output from memory
output_copy_rwc_inc = FQ.zero()
if call.is_success == FQ.one() and precompile_return_length != FQ.zero():
output_copy_rwc_inc, _ = instruction.copy_lookup(
callee_call_id,
CopyDataTypeTag.Memory,
callee_call_id,
CopyDataTypeTag.RlcAcc,
FQ.zero(),
FQ(precompile_return_length),
FQ.zero(),
FQ(precompile_return_length),
instruction.curr.rw_counter + rw_counter_inc,
)
rw_counter_inc += output_copy_rwc_inc

# Verify data copy from precompiles
return_copy_rwc_inc = FQ.zero()
if call.is_success == FQ.one() and precompile_return_length != FQ.zero():
return_copy_rwc_inc, _ = instruction.copy_lookup(
callee_call_id,
CopyDataTypeTag.Memory,
instruction.curr.call_id,
CopyDataTypeTag.Memory,
FQ.zero(),
FQ(min_rd_copy_size),
call.rd_offset,
FQ(min_rd_copy_size),
instruction.curr.rw_counter + rw_counter_inc,
)
rw_counter_inc += return_copy_rwc_inc

precompile_memory_word_size, _ = instruction.constant_divmod(
FQ(min_rd_copy_size + 31), FQ(32), N_BYTES_MEMORY_WORD_SIZE
)

# Give gas stipend if value is not zero
callee_gas_left += has_value * GAS_STIPEND_CALL_WITH_VALUE

instruction.constrain_step_state_transition(
rw_counter=Transition.delta(rw_counter_inc),
call_id=Transition.to(callee_call_id),
is_root=Transition.to(False),
is_create=Transition.to(False),
code_hash=Transition.to_word(Word(EMPTY_CODE_HASH)),
gas_left=Transition.to(callee_gas_left),
reversible_write_counter=Transition.to(2),
program_counter=Transition.delta(1),
stack_pointer=Transition.same(),
memory_word_size=Transition.to(precompile_memory_word_size),
)

PrecompileGadget(
instruction, call.callee_address, FQ(precompile_return_length), call.cd_length
)
else: # precheck is ok and callee has code
# Save caller's call state
for field_tag, expected_value in [
(CallContextFieldTag.ProgramCounter, instruction.curr.program_counter + 1),
Expand Down
39 changes: 34 additions & 5 deletions src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
from dataclasses import dataclass
from zkevm_specs.evm_circuit.instruction import Instruction
from zkevm_specs.evm_circuit.table import (
CallContextFieldTag,
FixedTableTag,
RW,
)
from zkevm_specs.util import FQ, Word, EcrecoverGas
from zkevm_specs.util.arithmetic import RLC

SECP256K1N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141


@dataclass(frozen=True)
class PrecompileAuxData:
msg_hash: Word
sig_v: Word
sig_r: Word
sig_s: Word
recovered_addr: FQ
input_rlc: FQ
output_rlc: FQ


def ecRecover(instruction: Instruction):
is_success = instruction.call_context_lookup(CallContextFieldTag.IsSuccess, RW.Read)
address_word = instruction.call_context_lookup_word(CallContextFieldTag.CalleeAddress)
Expand All @@ -21,14 +34,30 @@ def ecRecover(instruction: Instruction):
)

# Get msg_hash, signature and recovered address from aux_data
msg_hash: Word = instruction.curr.aux_data[0]
sig_v: Word = instruction.curr.aux_data[1]
sig_r: Word = instruction.curr.aux_data[2]
sig_s: Word = instruction.curr.aux_data[3]
recovered_addr: FQ = instruction.curr.aux_data[4]
aux_data: PrecompileAuxData = instruction.curr.aux_data[0]
msg_hash = aux_data.msg_hash
sig_v = aux_data.sig_v
sig_r = aux_data.sig_r
sig_s = aux_data.sig_s
recovered_addr = aux_data.recovered_addr
keccak_randomness: FQ = instruction.curr.aux_data[1]

is_recovered = FQ(instruction.is_zero(recovered_addr) != FQ(1))

# Verify input and output
input_bytes = bytearray(b"")
input_bytes.extend(msg_hash.int_value().to_bytes(32, "little"))
input_bytes.extend(sig_v.int_value().to_bytes(32, "little"))
input_bytes.extend(sig_r.int_value().to_bytes(32, "little"))
input_bytes.extend(sig_s.int_value().to_bytes(32, "little"))
input_rlc = RLC(bytes(reversed(input_bytes)), keccak_randomness, n_bytes=128).expr()
instruction.constrain_equal(aux_data.input_rlc, input_rlc)

output_rlc = RLC(
bytes(reversed(recovered_addr.n.to_bytes(32, "little"))), keccak_randomness, n_bytes=32
).expr()
instruction.constrain_equal(aux_data.output_rlc, output_rlc)

# is_success is always true
# ref: https://github.com/ethereum/execution-specs/blob/master/src/ethereum/shanghai/vm/precompiled_contracts/ecrecover.py
instruction.constrain_equal(is_success, FQ(1))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from zkevm_specs.evm_circuit.execution.precompiles.ecpairing import BYTES_PER_PAIRING
from zkevm_specs.evm_circuit.instruction import Instruction
from zkevm_specs.evm_circuit.precompile import Precompile
from zkevm_specs.evm_circuit.table import CallContextFieldTag
from zkevm_specs.util import FQ
from zkevm_specs.util.param import N_BYTES_GAS, Bn254PairingPerPointGas, IdentityPerWordGas


def error_oog_precompile(instruction: Instruction):
address_word = instruction.call_context_lookup_word(CallContextFieldTag.CalleeAddress)
address = instruction.word_to_address(address_word)
calldata_len = instruction.call_context_lookup(CallContextFieldTag.CallDataLength)

# the address must be one of precompiles
instruction.constrain_equal(instruction.precompile(address), FQ.one())

# TODO: Handle OOG of SHA256, RIPEMD160, BIGMODEXP and BLAKE2F.
### total gas cost
# constant gas cost
precompile = Precompile(address)
gas_cost = precompile.base_gas_cost()
# dynamic gas cost
if precompile == Precompile.BN254PAIRING:
pairs = calldata_len / BYTES_PER_PAIRING
gas_cost += Bn254PairingPerPointGas * pairs
elif precompile == Precompile.DATACOPY:
gas_cost += instruction.memory_copier_gas_cost(calldata_len, FQ(0), IdentityPerWordGas)

# check gas left is less than total gas required
insufficient_gas, _ = instruction.compare(instruction.curr.gas_left, gas_cost, N_BYTES_GAS)
instruction.constrain_equal(insufficient_gas, FQ(1))

instruction.constrain_error_state(
instruction.rw_counter_offset + instruction.curr.reversible_write_counter
)
2 changes: 2 additions & 0 deletions src/zkevm_specs/evm_circuit/execution_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class ExecutionState(IntEnum):
# For CREATE and CREATE2 opcodes which may run out of gas.
ErrorOutOfGasCREATE = auto()
ErrorOutOfGasSELFDESTRUCT = auto()
# OOG case of precompiles
ErrorOutOfGasPrecompile = auto()

# Precompile's successful cases
ECRECOVER = auto()
Expand Down
4 changes: 4 additions & 0 deletions src/zkevm_specs/evm_circuit/precompile.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def execution_state(self) -> ExecutionState:
def base_gas_cost(self) -> int:
return PRECOMPILE_INFO_MAP[self].base_gas

@classmethod
def len(cls) -> int:
return len(cls)


class PrecompileInfo:
"""
Expand Down
38 changes: 38 additions & 0 deletions src/zkevm_specs/evm_circuit/util/precompile_gadget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from zkevm_specs.evm_circuit.precompile import Precompile
from ...util import FQ
from ..instruction import Instruction


class PrecompileGadget:
address: FQ

def __init__(
self,
instruction: Instruction,
callee_addr: FQ,
precompile_return_len: FQ,
calldata_len: FQ,
):
# next execution state must be one of precompiles
instruction.constrain_equal(instruction.precompile(callee_addr), FQ.one())

### precompiles' specific constraints
precompile = Precompile(callee_addr)
if precompile == Precompile.DATACOPY:
# input length is the same as return data length
instruction.constrain_equal(precompile_return_len, calldata_len)
elif precompile == Precompile.ECRECOVER:
# The input different from 128 is allowed and is then right padded with zeros
# We only ensure hat the return length is either 32 or 0.
is_128 = instruction.is_equal(precompile_return_len, FQ(32))
is_zero = instruction.is_equal(precompile_return_len, FQ.zero())
instruction.constrain_equal(is_128 + is_zero, FQ.one())
elif precompile == Precompile.BN254ADD:
# input length is 128 bytes
instruction.constrain_equal(calldata_len, FQ(128))
elif precompile == Precompile.BN254SCALARMUL:
# input length is 96 bytes
instruction.constrain_equal(calldata_len, FQ(96))
elif precompile == Precompile.BN254PAIRING:
# input length is 192 * n bytes
instruction.constrain_equal(FQ(calldata_len.n % 192), FQ.zero())
22 changes: 20 additions & 2 deletions tests/evm/precompiles/test_ecRecover.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
Tables,
verify_steps,
)
from zkevm_specs.evm_circuit.execution.precompiles.ecrecover import SECP256K1N
from zkevm_specs.evm_circuit.execution.precompiles.ecrecover import PrecompileAuxData, SECP256K1N
from zkevm_specs.util import (
Word,
FQ,
)
from zkevm_specs.evm_circuit.table import SigTableRow
from zkevm_specs.util.arithmetic import RLC


def gen_testing_data():
Expand Down Expand Up @@ -49,6 +50,8 @@ def gen_testing_data():

TESTING_DATA = gen_testing_data()

randomness_keccak = rand_fq()


@pytest.mark.parametrize(
"caller_ctx, msg_hash, v, r, s, address",
Expand All @@ -72,12 +75,27 @@ def test_ecRecover(
return_data_offset = 0
return_data_length = 0x20 if recovered else 0

aux_data = [
input_bytes = bytearray(b"")
input_bytes.extend(msg_hash)
input_bytes.extend((v + 27).to_bytes(32, "little"))
input_bytes.extend(r.to_bytes(32, "little"))
input_bytes.extend(s.to_bytes(32, "little"))
input_rlc = RLC(bytes(reversed(input_bytes)), randomness_keccak, n_bytes=128).expr()
output_bytes = int.from_bytes(address, "big").to_bytes(32, "little")
output_rlc = RLC(bytes(reversed(output_bytes)), randomness_keccak, n_bytes=32).expr()
aux_data = PrecompileAuxData(
Word(msg_hash),
Word(v + 27),
Word(r),
Word(s),
FQ(int.from_bytes(address, "big")),
input_rlc,
output_rlc,
)

aux_data = [
aux_data,
randomness_keccak,
]

# assign sig_table
Expand Down
Loading

0 comments on commit 163f3c0

Please sign in to comment.