From ed1cb053f54232687db72fa1bd70d1b717c7a9b1 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Sun, 17 Dec 2023 14:01:29 +0800 Subject: [PATCH 01/13] feat: callop and PrecompileGadget (draft) --- .../evm_circuit/execution/callop.py | 138 +++++++++++++++++- .../evm_circuit/execution_state.py | 3 + src/zkevm_specs/evm_circuit/precompile.py | 4 + .../evm_circuit/util/precompile_gadget.py | 28 ++++ 4 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/zkevm_specs/evm_circuit/util/precompile_gadget.py diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index 9aa5f94fe..4cdb7c3e1 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -1,9 +1,12 @@ +from zkevm_specs.evm_circuit.precompile import Precompile from zkevm_specs.evm_circuit.util.call_gadget import CallGadget +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_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 @@ -114,14 +117,18 @@ 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) + # only if the callee address is one of precompiles + is_zero_address = instruction.is_zero(callee_address) + # +1 is for convenience, we don't need to care the 2nd return (which is 'eq' case) + is_within_precompiles_addr, _ = instruction.compare(callee_address, Precompile.len() + 1, 2) + is_precompile = is_zero_address == FQ.zero() and is_within_precompiles_addr == FQ.one() 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 [ @@ -147,7 +154,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_return_length: FQ = instruction.curr.aux_data[0] + precompile_input_len: FQ = instruction.curr.aux_data[1] + input_rwc: FQ = instruction.curr.aux_data[5] + output_rwc: FQ = instruction.curr.aux_data[6] + return_data_rwc: FQ = instruction.curr.aux_data[6] + + min_rd_copy_size = min(precompile_return_length.n, call.rd_length) + + # precompiles have on 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), + (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( + instruction.call_context_lookup(field_tag, RW.Write), + 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, callee_call_id), + expected_value, + ) + + ### copy table lookup here + ### is to rlc input and output to have an easy way to verify data in PrecompileGadget + + # RLC precompile input from memory + input_copy_rwc_inc = input_rlc = FQ.zero() + if precompile_input_len != FQ(0): + input_copy_rwc_inc, input_rlc = instruction.copy_lookup( + instruction.curr.call_id, + CopyDataTypeTag.Memory, + callee_call_id, + CopyDataTypeTag.RlcAcc, + call.cd_offset, + call.cd_offset + precompile_input_len, + FQ.zero(), + precompile_input_len, + input_rwc, + ) + + # RLC precompile output from memory + output_copy_rwc_inc = output_rlc = FQ.zero() + if call.is_success == FQ.one() and precompile_return_length != FQ.zero(): + output_copy_rwc_inc, output_rlc = instruction.copy_lookup( + callee_call_id, + CopyDataTypeTag.Memory, + callee_call_id, + CopyDataTypeTag.RlcAcc, + FQ.zero(), + precompile_return_length, + FQ.zero(), + precompile_return_length, + output_rwc, + ) + + # 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(), + min_rd_copy_size, + call.rd_offset, + min_rd_copy_size, + return_data_rwc, + ) + ### + + # Give gas stipend if value is not zero + callee_gas_left += has_value * GAS_STIPEND_CALL_WITH_VALUE + + rwc = ( + instruction.rw_counter_offset + + input_copy_rwc_inc + + output_copy_rwc_inc + + return_copy_rwc_inc + ) + instruction.step_state_transition_to_new_context( + rw_counter=Transition.delta(rwc), + call_id=Transition.to(callee_call_id), + is_root=Transition.to(False), + is_create=Transition.to(False), + code_hash=Transition.to_word(EMPTY_CODE_HASH), + gas_left=Transition.to(callee_gas_left), + reversible_write_counter=Transition.to(2), + log_id=Transition.same(), + ) + + PrecompileGadget(instruction, callee_address, input_rlc, output_rlc) + + 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), diff --git a/src/zkevm_specs/evm_circuit/execution_state.py b/src/zkevm_specs/evm_circuit/execution_state.py index 98c5a7240..b39bcca19 100644 --- a/src/zkevm_specs/evm_circuit/execution_state.py +++ b/src/zkevm_specs/evm_circuit/execution_state.py @@ -135,6 +135,9 @@ class ExecutionState(IntEnum): BN254_PAIRING = auto() BLAKE2F = auto() + # OOG case of precompiles + ErrorOutofGasPrecompile = auto() + def expr(self) -> FQ: return FQ(self) diff --git a/src/zkevm_specs/evm_circuit/precompile.py b/src/zkevm_specs/evm_circuit/precompile.py index 6c4ac0637..d5450f1bd 100644 --- a/src/zkevm_specs/evm_circuit/precompile.py +++ b/src/zkevm_specs/evm_circuit/precompile.py @@ -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: """ diff --git a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py new file mode 100644 index 000000000..76ebed047 --- /dev/null +++ b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py @@ -0,0 +1,28 @@ +from ...util import FQ +from ..instruction import Instruction + + +# PrecompileGadget helps execution state transition between callop and precompiles +# We only verify the data (input and output data of precompiles) consistence between transitions. +class PrecompileGadget: + address: FQ + + def __init__( + self, + instruction: Instruction, + callee_addr: FQ, + input_rlc: FQ, + output_rlc: FQ, + ): + # next execution state must be one of precompile execution states + instruction.constrain_equal(instruction.precompile(callee_addr), FQ.one()) + + # verify current data is the same as the ones in next execution state + next_input_rlc: FQ = instruction.next.aux_data[0] + next_output_rlc: FQ = instruction.next.aux_data[1] + instruction.constrain_equal(input_rlc, next_input_rlc) + instruction.constrain_equal(output_rlc, next_output_rlc) + + # FIXME, Q: do we need return_data_rlc? what is the diff between output?? + + # FIXME how to connect rlced input, output with real in/out in `word`, or we don't have to? From df05ae2bc42db88a0a93de29a5250164cab19f5a Mon Sep 17 00:00:00 2001 From: KimiWu Date: Tue, 19 Dec 2023 10:51:24 +0800 Subject: [PATCH 02/13] feat: precompile oog --- .../evm_circuit/execution/__init__.py | 3 ++ .../precompiles/error_oog_precompile.py | 35 +++++++++++++++++++ .../evm_circuit/execution_state.py | 5 ++- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 src/zkevm_specs/evm_circuit/execution/precompiles/error_oog_precompile.py diff --git a/src/zkevm_specs/evm_circuit/execution/__init__.py b/src/zkevm_specs/evm_circuit/execution/__init__.py index 4bf17c1bc..f8020423c 100644 --- a/src/zkevm_specs/evm_circuit/execution/__init__.py +++ b/src/zkevm_specs/evm_circuit/execution/__init__.py @@ -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] = { @@ -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: , diff --git a/src/zkevm_specs/evm_circuit/execution/precompiles/error_oog_precompile.py b/src/zkevm_specs/evm_circuit/execution/precompiles/error_oog_precompile.py new file mode 100644 index 000000000..e785af14d --- /dev/null +++ b/src/zkevm_specs/evm_circuit/execution/precompiles/error_oog_precompile.py @@ -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 + ) diff --git a/src/zkevm_specs/evm_circuit/execution_state.py b/src/zkevm_specs/evm_circuit/execution_state.py index b39bcca19..a3fe40ba3 100644 --- a/src/zkevm_specs/evm_circuit/execution_state.py +++ b/src/zkevm_specs/evm_circuit/execution_state.py @@ -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() @@ -135,9 +137,6 @@ class ExecutionState(IntEnum): BN254_PAIRING = auto() BLAKE2F = auto() - # OOG case of precompiles - ErrorOutofGasPrecompile = auto() - def expr(self) -> FQ: return FQ(self) From 9ddd4a0536ad1737266ca6a8f08681b9a1272d00 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Tue, 19 Dec 2023 14:01:30 +0800 Subject: [PATCH 03/13] feat: remove rlc in callop for PrecompileGadget --- .../evm_circuit/execution/callop.py | 42 +------------------ .../evm_circuit/util/precompile_gadget.py | 23 ++++------ 2 files changed, 11 insertions(+), 54 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index 4cdb7c3e1..e26a87536 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -206,39 +206,6 @@ def callop(instruction: Instruction): expected_value, ) - ### copy table lookup here - ### is to rlc input and output to have an easy way to verify data in PrecompileGadget - - # RLC precompile input from memory - input_copy_rwc_inc = input_rlc = FQ.zero() - if precompile_input_len != FQ(0): - input_copy_rwc_inc, input_rlc = instruction.copy_lookup( - instruction.curr.call_id, - CopyDataTypeTag.Memory, - callee_call_id, - CopyDataTypeTag.RlcAcc, - call.cd_offset, - call.cd_offset + precompile_input_len, - FQ.zero(), - precompile_input_len, - input_rwc, - ) - - # RLC precompile output from memory - output_copy_rwc_inc = output_rlc = FQ.zero() - if call.is_success == FQ.one() and precompile_return_length != FQ.zero(): - output_copy_rwc_inc, output_rlc = instruction.copy_lookup( - callee_call_id, - CopyDataTypeTag.Memory, - callee_call_id, - CopyDataTypeTag.RlcAcc, - FQ.zero(), - precompile_return_length, - FQ.zero(), - precompile_return_length, - output_rwc, - ) - # Verify data copy from precompiles return_copy_rwc_inc = FQ.zero() if call.is_success == FQ.one() and precompile_return_length != FQ.zero(): @@ -258,12 +225,7 @@ def callop(instruction: Instruction): # Give gas stipend if value is not zero callee_gas_left += has_value * GAS_STIPEND_CALL_WITH_VALUE - rwc = ( - instruction.rw_counter_offset - + input_copy_rwc_inc - + output_copy_rwc_inc - + return_copy_rwc_inc - ) + rwc = instruction.rw_counter_offset + return_copy_rwc_inc instruction.step_state_transition_to_new_context( rw_counter=Transition.delta(rwc), call_id=Transition.to(callee_call_id), @@ -275,7 +237,7 @@ def callop(instruction: Instruction): log_id=Transition.same(), ) - PrecompileGadget(instruction, callee_address, input_rlc, output_rlc) + PrecompileGadget(instruction, callee_address, precompile_return_length, call.cd_length) else: # precheck is ok and callee has code # Save caller's call state diff --git a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py index 76ebed047..8b7b990f8 100644 --- a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py +++ b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py @@ -1,9 +1,8 @@ +from zkevm_specs.evm_circuit.precompile import Precompile from ...util import FQ from ..instruction import Instruction -# PrecompileGadget helps execution state transition between callop and precompiles -# We only verify the data (input and output data of precompiles) consistence between transitions. class PrecompileGadget: address: FQ @@ -11,18 +10,14 @@ def __init__( self, instruction: Instruction, callee_addr: FQ, - input_rlc: FQ, - output_rlc: FQ, + precompile_return_len: FQ, + calldata_len: FQ, ): - # next execution state must be one of precompile execution states + # next execution state must be one of precompiles instruction.constrain_equal(instruction.precompile(callee_addr), FQ.one()) - # verify current data is the same as the ones in next execution state - next_input_rlc: FQ = instruction.next.aux_data[0] - next_output_rlc: FQ = instruction.next.aux_data[1] - instruction.constrain_equal(input_rlc, next_input_rlc) - instruction.constrain_equal(output_rlc, next_output_rlc) - - # FIXME, Q: do we need return_data_rlc? what is the diff between output?? - - # FIXME how to connect rlced input, output with real in/out in `word`, or we don't have to? + ### precompiles' specific constraints + precompile = Precompile(callee_addr) + if precompile == Precompile.DATACOPY: + # input length is the same as return data length + instruction.constrain_equal(calldata_len, precompile_return_len) From 7e6e2ad7c4f7c840d7dc117224bb00f458842761 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 20 Dec 2023 17:55:00 +0800 Subject: [PATCH 04/13] test: refactor callop test for precompiles --- .../evm_circuit/execution/callop.py | 45 ++-- tests/evm/test_callop.py | 240 ++++++++++++++++-- 2 files changed, 242 insertions(+), 43 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index e26a87536..a0da0a2b9 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -1,8 +1,7 @@ -from zkevm_specs.evm_circuit.precompile import Precompile from zkevm_specs.evm_circuit.util.call_gadget import CallGadget 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_STACK +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 @@ -117,11 +116,8 @@ def callop(instruction: Instruction): ) # Make sure the state transition to ExecutionState for precompile if and - # only if the callee address is one of precompiles - is_zero_address = instruction.is_zero(callee_address) - # +1 is for convenience, we don't need to care the 2nd return (which is 'eq' case) - is_within_precompiles_addr, _ = instruction.compare(callee_address, Precompile.len() + 1, 2) - is_precompile = is_zero_address == FQ.zero() and is_within_precompiles_addr == FQ.one() + # only if the callee address is one of precompile + is_precompile = instruction.precompile(callee_address) instruction.constrain_equal( is_precompile, FQ(instruction.next.execution_state in precompile_execution_states()) ) @@ -156,13 +152,8 @@ def callop(instruction: Instruction): ) # precompiles call elif is_precheck_ok and is_precompile == FQ.one(): - precompile_return_length: FQ = instruction.curr.aux_data[0] - precompile_input_len: FQ = instruction.curr.aux_data[1] - input_rwc: FQ = instruction.curr.aux_data[5] - output_rwc: FQ = instruction.curr.aux_data[6] - return_data_rwc: FQ = instruction.curr.aux_data[6] - - min_rd_copy_size = min(precompile_return_length.n, call.rd_length) + precompile_return_length = instruction.curr.aux_data[0] + min_rd_copy_size = min(precompile_return_length, call.rd_length.n) # precompiles have on code instruction.constrain_equal(no_callee_code, FQ.one()) @@ -172,16 +163,16 @@ def callop(instruction: Instruction): # Setup next call's context. for field_tag, expected_value in [ (CallContextFieldTag.IsSuccess, call.is_success), - (CallContextFieldTag.CalleeAddress, callee_address), + (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( - instruction.call_context_lookup(field_tag, RW.Write), - expected_value, + instruction.constrain_equal_word( + instruction.call_context_lookup_word(field_tag, RW.Write), + WordOrValue(expected_value), ) # Save caller's call state @@ -215,26 +206,30 @@ def callop(instruction: Instruction): instruction.curr.call_id, CopyDataTypeTag.Memory, FQ.zero(), - min_rd_copy_size, + FQ(min_rd_copy_size), call.rd_offset, - min_rd_copy_size, - return_data_rwc, + FQ(min_rd_copy_size), + instruction.curr.rw_counter + instruction.rw_counter_offset, ) - ### + 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 rwc = instruction.rw_counter_offset + return_copy_rwc_inc - instruction.step_state_transition_to_new_context( + instruction.constrain_step_state_transition( rw_counter=Transition.delta(rwc), call_id=Transition.to(callee_call_id), is_root=Transition.to(False), is_create=Transition.to(False), - code_hash=Transition.to_word(EMPTY_CODE_HASH), + code_hash=Transition.to_word(Word(EMPTY_CODE_HASH)), gas_left=Transition.to(callee_gas_left), reversible_write_counter=Transition.to(2), - log_id=Transition.same(), + program_counter=Transition.delta(1), + stack_pointer=Transition.same(), + memory_word_size=Transition.to(precompile_memory_word_size), ) PrecompileGadget(instruction, callee_address, precompile_return_length, call.cd_length) diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index 4dd9dd418..5922047e7 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -14,6 +14,8 @@ Tables, verify_steps, ) +from zkevm_specs.evm_circuit.table import CopyDataTypeTag +from zkevm_specs.evm_circuit.typing import CopyCircuit from zkevm_specs.util import ( EMPTY_CODE_HASH, GAS_COST_ACCOUNT_COLD_ACCESS, @@ -24,7 +26,8 @@ Word, U256, ) -from common import CallContext +from common import CallContext, rand_fq + Stack = namedtuple( "Stack", @@ -138,10 +141,7 @@ def gen_testing_data(): return [ ( opcode, - CALLER, callee, - PARENT_CALLER, - PARENT_VALUE, call_context, stack, is_warm_access, @@ -166,26 +166,23 @@ def gen_testing_data(): TESTING_DATA = gen_testing_data() -@pytest.mark.parametrize( - "opcode, caller, callee, parent_caller, parent_value, caller_ctx, stack, is_warm_access, depth, expected", - TESTING_DATA, -) -def test_callop( +def callop_test_template( opcode: Opcode, - caller: Account, callee: Account, - parent_caller: Account, - parent_value: int, caller_ctx: CallContext, stack: Stack, is_warm_access: bool, depth: int, + is_precompile: bool, expected: Expected, ): 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 + + caller = CALLER + parent_caller = PARENT_CALLER + parent_value = PARENT_VALUE # Set `is_static == 1` for both DELEGATECALL and STATICCALL opcodes, or when # `stack.value == 0` for both CALL and CALLCODE opcodes. @@ -247,15 +244,13 @@ def test_callop( .stop() ) - caller_bytecode_hash = Word(caller_bytecode.hash()) - callee_bytecode = callee.code callee_bytecode_hash = callee_bytecode.hash() - if not callee.is_empty(): + if not callee.is_empty() and not is_precompile: is_empty_code_hash = callee_bytecode_hash == EMPTY_CODE_HASH else: is_empty_code_hash = True - callee_bytecode_hash = Word(callee_bytecode_hash if not callee.is_empty() else 0) + callee_bytecode_hash = Word(callee_bytecode_hash if not is_empty_code_hash else 0) # Only check balance and stack depth is_precheck_ok = caller.balance >= value and depth < 1025 @@ -346,11 +341,28 @@ def test_callop( .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) - if is_precheck_ok is False or is_empty_code_hash: + if (is_precheck_ok is False or is_empty_code_hash) and is_precompile is False: rw_dictionary \ .call_context_write(1, CallContextFieldTag.LastCalleeId, 0) \ .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) + elif is_precompile: + rw_dictionary \ + .call_context_write(1, CallContextFieldTag.IsSuccess, True) \ + .call_context_write(1, CallContextFieldTag.CalleeAddress, Word(callee.address)) \ + .call_context_write(1, CallContextFieldTag.CallerId, 1) \ + .call_context_write(1, CallContextFieldTag.CallDataOffset, stack.cd_offset) \ + .call_context_write(1, CallContextFieldTag.CallDataLength, stack.cd_length) \ + .call_context_write(1, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) \ + .call_context_write(1, CallContextFieldTag.ReturnDataLength, stack.rd_length) \ + .call_context_write(call_id, CallContextFieldTag.ProgramCounter, next_program_counter) \ + .call_context_write(call_id, CallContextFieldTag.StackPointer, 1023) \ + .call_context_write(call_id, CallContextFieldTag.GasLeft, expected.caller_gas_left) \ + .call_context_write(call_id, CallContextFieldTag.MemorySize, expected.next_memory_size) \ + .call_context_write(call_id, CallContextFieldTag.ReversibleWriteCounter, caller_ctx.reversible_write_counter + 1) \ + .call_context_write(call_id, CallContextFieldTag.LastCalleeId, call_id) \ + .call_context_write(call_id, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ + .call_context_write(call_id, CallContextFieldTag.LastCalleeReturnDataLength, stack.rd_length) else: rw_dictionary \ .call_context_write(1, CallContextFieldTag.ProgramCounter, next_program_counter) \ @@ -378,6 +390,48 @@ def test_callop( .call_context_read(call_id, CallContextFieldTag.CodeHash, callee_bytecode_hash) # fmt: on + return ( + caller_bytecode, + callee_bytecode, + call_id, + next_program_counter, + stack_pointer, + rw_dictionary, + is_precheck_ok, + is_empty_code_hash, + ) + + +@pytest.mark.parametrize( + "opcode, callee, caller_ctx, stack, is_warm_access, depth, expected", + TESTING_DATA, +) +def test_callop( + opcode: Opcode, + callee: Account, + caller_ctx: CallContext, + stack: Stack, + is_warm_access: bool, + depth: int, + expected: Expected, +): + ( + caller_bytecode, + callee_bytecode, + call_id, + next_program_counter, + stack_pointer, + rw_dictionary, + is_precheck_ok, + is_empty_code_hash, + ) = callop_test_template( + opcode, callee, caller_ctx, stack, is_warm_access, depth, False, expected + ) + + caller_bytecode_hash = Word(caller_bytecode.hash()) + callee_bytecode_hash = Word(callee_bytecode.hash() if not callee.is_empty() else 0) + rw_counter = call_id + tables = Tables( block_table=set(Block().table_assignments()), tx_table=set(), @@ -439,3 +493,153 @@ def test_callop( ), ], ) + + +def gen_precompile_testing_data(): + opcodes = [ + Opcode.CALL, + # Opcode.CALLCODE, + # Opcode.DELEGATECALL, + # Opcode.STATICCALL, + ] + precompiles = [ + ( + ExecutionState.ECRECOVER, + Account( + address=1, + code=Bytecode() + .push32(0x456E9AEA5E197A1F1AF7A3E85A3212FA4049A3BA34C2289B4C860FC0B0C64EF3) + .push1(0) + .mstore() + .push1(28) # v + .push1(0x20) + .mstore() + .push32(0x9242685BF161793CC25603C231BC2F568EB630EA16AA137D2664AC8038825608) # r + .push1(0x40) + .mstore() + .push32(0x4F8AE3BD7535248D0BD448298CC2E2071E56992D0774DC340C368AE950852ADA) # s + .push1(0x60) + .mstore(), + ), + Stack(cd_offset=0, cd_length=0x80, rd_offset=0, rd_length=0x20), + ) + ] + + return [(opcode, callee) for opcode, callee in product(opcodes, precompiles)] + + +PRECOMPILE_TESTING_DATA = gen_precompile_testing_data() + +PRECOMPILE_RETURN_DATA = [0x01] * 64 + + +@pytest.mark.parametrize( + "opcode, precompile", + PRECOMPILE_TESTING_DATA, +) +def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): + randomness_keccak = rand_fq() + + exe_state = precompile[0] + callee = precompile[1] + stack = precompile[2] + caller_ctx = CallContext(gas_left=100000) + expectation = expected( + opcode, + callee.code_hash(), + CALLER if opcode in [opcode.CALLCODE, Opcode.DELEGATECALL] else callee, + caller_ctx, + stack, + True, + True, + ) + + ( + caller_bytecode, + callee_bytecode, + call_id, + next_program_counter, + stack_pointer, + rw_dictionary, + _, + _, + ) = callop_test_template( + opcode, + callee, + caller_ctx, + stack, + True, + 1, + True, + expectation, + ) + + caller_bytecode_hash = Word(caller_bytecode.hash()) + rw_counter = call_id + + src_data = dict( + [ + (i, PRECOMPILE_RETURN_DATA[i] if i < len(PRECOMPILE_RETURN_DATA) else 0) + for i in range(0, stack.rd_length) + ] + ) + copy_circuit = CopyCircuit().copy( + randomness_keccak, + rw_dictionary, + call_id, + CopyDataTypeTag.Memory, + 1, + CopyDataTypeTag.Memory, + 0, + stack.rd_length, + 0, + stack.rd_length, + src_data, + ) + + tables = Tables( + block_table=set(Block().table_assignments()), + tx_table=set(), + withdrawal_table=set(), + bytecode_table=set( + chain( + caller_bytecode.table_assignments(), + callee_bytecode.table_assignments(), + ) + ), + rw_table=set(rw_dictionary.rws), + copy_circuit=copy_circuit.rows, + ) + + verify_steps( + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.CALL_OP, + rw_counter=rw_counter, + call_id=1, + is_root=True, + is_create=False, + code_hash=caller_bytecode_hash, + program_counter=next_program_counter - 1, + stack_pointer=stack_pointer, + gas_left=caller_ctx.gas_left, + memory_word_size=caller_ctx.memory_word_size, + reversible_write_counter=caller_ctx.reversible_write_counter, + aux_data=[stack.rd_length], + ), + StepState( + execution_state=exe_state, + rw_counter=rw_dictionary.rw_counter, + call_id=call_id, + is_root=False, + is_create=False, + code_hash=Word(EMPTY_CODE_HASH), + program_counter=next_program_counter, + stack_pointer=stack_pointer, + gas_left=expectation.callee_gas_left, + reversible_write_counter=2, + memory_word_size=int((stack.rd_length + 31) / 32), + ), + ], + ) From f130ab8b938546881f4bff9a6304979b02851674 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Thu, 21 Dec 2023 14:52:05 +0800 Subject: [PATCH 05/13] test: complete testing for precompiles in callop --- .../evm_circuit/execution/callop.py | 6 +- .../evm_circuit/util/precompile_gadget.py | 2 +- tests/evm/test_callop.py | 116 +++++++++++++++++- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index a0da0a2b9..4e8e59212 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -117,7 +117,7 @@ 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()) ) @@ -232,7 +232,9 @@ def callop(instruction: Instruction): memory_word_size=Transition.to(precompile_memory_word_size), ) - PrecompileGadget(instruction, callee_address, precompile_return_length, call.cd_length) + 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 diff --git a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py index 8b7b990f8..008782466 100644 --- a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py +++ b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py @@ -20,4 +20,4 @@ def __init__( precompile = Precompile(callee_addr) if precompile == Precompile.DATACOPY: # input length is the same as return data length - instruction.constrain_equal(calldata_len, precompile_return_len) + instruction.constrain_equal(precompile_return_len, calldata_len) diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index 5922047e7..ed8f4b876 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -14,6 +14,7 @@ Tables, verify_steps, ) +from zkevm_specs.evm_circuit.precompile import Precompile from zkevm_specs.evm_circuit.table import CopyDataTypeTag from zkevm_specs.evm_circuit.typing import CopyCircuit from zkevm_specs.util import ( @@ -498,15 +499,15 @@ def test_callop( def gen_precompile_testing_data(): opcodes = [ Opcode.CALL, - # Opcode.CALLCODE, - # Opcode.DELEGATECALL, - # Opcode.STATICCALL, + Opcode.CALLCODE, + Opcode.DELEGATECALL, + Opcode.STATICCALL, ] precompiles = [ ( ExecutionState.ECRECOVER, Account( - address=1, + address=Precompile.ECRECOVER, code=Bytecode() .push32(0x456E9AEA5E197A1F1AF7A3E85A3212FA4049A3BA34C2289B4C860FC0B0C64EF3) .push1(0) @@ -522,7 +523,112 @@ def gen_precompile_testing_data(): .mstore(), ), Stack(cd_offset=0, cd_length=0x80, rd_offset=0, rd_length=0x20), - ) + ), + ( + ExecutionState.DATACOPY, + Account( + address=Precompile.DATACOPY, + code=Bytecode().push16(0x0123456789ABCDEF0123456789ABCDEF).push1(0).mstore(), + ), + Stack(cd_offset=0, cd_length=0x20, rd_offset=0, rd_length=0x20), + ), + ( + ExecutionState.BN254_ADD, + Account( + address=Precompile.BN254ADD, + code=Bytecode() + .push1(1) # x1 + .push1(0) + .mstore() + .push1(2) # y1 + .push1(0x20) + .mstore() + .push1(1) # x2 + .push1(0x40) + .mstore() + .push1(2) # y2 + .push1(0x60) + .mstore(), + ), + Stack(cd_offset=0, cd_length=0x80, rd_offset=0, rd_length=0x40), + ), + ( + ExecutionState.BN254_SCALAR_MUL, + Account( + address=Precompile.BN254SCALARMUL, + code=Bytecode() + .push1(1) # x1 + .push1(0) + .mstore() + .push1(2) # y1 + .push1(0x20) + .mstore() + .push1(2) # s + .push1(0x40) + .mstore(), + ), + Stack(cd_offset=0, cd_length=0x60, rd_offset=0, rd_length=0x40), + ), + ( + ExecutionState.BN254_PAIRING, + Account( + address=Precompile.BN254PAIRING, + code=Bytecode() + .push32(0x2CF44499D5D27BB186308B7AF7AF02AC5BC9EEB6A3D147C186B21FB1B76E18DA) # g1 x1 + .push1(0) + .mstore() + .push32(0x2C0F001F52110CCFE69108924926E45F0B0C868DF0E7BDE1FE16D3242DC715F6) # g1 y1 + .push1(0x20) + .mstore() + .push1(1) # g1 x2 + .push1(0x40) + .mstore() + .push32(0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD45) # g1 y2 + .push1(0x60) + .mstore() + .push32( + 0x1FB19BB476F6B9E44E2A32234DA8212F61CD63919354BC06AEF31E3CFAFF3EBC + ) # g2 x1_1 + .push1(0x80) + .mstore() + .push32( + 0x22606845FF186793914E03E21DF544C34FFE2F2F3504DE8A79D9159ECA2D98D9 + ) # g2 x1_2 + .push1(0xA0) + .mstore() + .push32( + 0x2BD368E28381E8ECCB5FA81FC26CF3F048EEA9ABFDD85D7ED3AB3698D63E4F90 + ) # g2 y1_1 + .push1(0xC0) + .mstore() + .push32( + 0x2FE02E47887507ADF0FF1743CBAC6BA291E66F59BE6BD763950BB16041A0A85E + ) # g2 y1_2 + .push1(0xE0) + .mstore() + .push32( + 0x1971FF0471B09FA93CAAF13CBF443C1AEDE09CC4328F5A62AAD45F40EC133EB4 + ) # g2 x2_1 + .push2(0x0100) + .mstore() + .push32( + 0x091058A3141822985733CBDDDFED0FD8D6C104E9E9EFF40BF5ABFEF9AB163BC7 + ) # g2 x2_2 + .push2(0x0120) + .mstore() + .push32( + 0x2A23AF9A5CE2BA2796C1F4E453A370EB0AF8C212D9DC9ACD8FC02C2E907BAEA2 + ) # g2 y2_1 + .push2(0x0140) + .mstore() + .push32( + 0x23A8EB0B0996252CB548A4487DA97B02422EBC0E834613F954DE6C7E0AFDC1FC + ) # g2 y2_2 + .push2(0x0160) + .mstore(), + ), + Stack(cd_offset=0, cd_length=0x20, rd_offset=0, rd_length=0x160), + ), ] return [(opcode, callee) for opcode, callee in product(opcodes, precompiles)] From f8afa4ef62028bc96a1655f3847fbd2bce61713f Mon Sep 17 00:00:00 2001 From: KimiWu Date: Thu, 21 Dec 2023 16:08:40 +0800 Subject: [PATCH 06/13] chore: add comments --- ..._F2CALLCODE_F4DELEGATECALL_FASTATICCALL.md | 2 +- .../evm_circuit/execution/callop.py | 1 - tests/evm/test_callop.py | 140 +++++++++--------- 3 files changed, 74 insertions(+), 69 deletions(-) diff --git a/specs/opcode/F1CALL_F2CALLCODE_F4DELEGATECALL_FASTATICCALL.md b/specs/opcode/F1CALL_F2CALLCODE_F4DELEGATECALL_FASTATICCALL.md index 77d4e1806..505065ed4 100644 --- a/specs/opcode/F1CALL_F2CALLCODE_F4DELEGATECALL_FASTATICCALL.md +++ b/specs/opcode/F1CALL_F2CALLCODE_F4DELEGATECALL_FASTATICCALL.md @@ -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) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index 4e8e59212..dbeccfd48 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -235,7 +235,6 @@ def callop(instruction: Instruction): 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 [ diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index ed8f4b876..fee73f5b2 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -107,66 +107,6 @@ def memory_size(offset: int, length: int) -> int: ) -def gen_testing_data(): - opcodes = [ - Opcode.CALL, - Opcode.CALLCODE, - Opcode.DELEGATECALL, - Opcode.STATICCALL, - ] - callees = [ - CALLEE_WITH_NOTHING, - CALLEE_WITH_STOP_BYTECODE_AND_BALANCE, - CALLEE_WITH_RETURN_BYTECODE, - CALLEE_WITH_REVERT_BYTECODE, - ] - call_contexts = [ - CallContext( - gas_left=100000, is_persistent=True, memory_word_size=8, reversible_write_counter=5 - ), - CallContext( - gas_left=100000, - is_persistent=False, - rw_counter_end_of_reversion=88, - reversible_write_counter=2, - ), - ] - stacks = [ - Stack(), - Stack(value=int(1e18), gas=100000), - Stack(value=int(1e18), gas=100, cd_offset=64, cd_length=320, rd_offset=0, rd_length=32), - Stack(cd_offset=0xFFFFFF, cd_length=0, rd_offset=0xFFFFFF, rd_length=0), - ] - is_warm_access = [True, False] - depths = [1, 1024, 1025] - return [ - ( - opcode, - callee, - call_context, - stack, - is_warm_access, - depth, - expected( - opcode, - callee.code_hash(), - # `callee = caller` for both CALLCODE and DELEGATECALL opcodes. - CALLER if opcode in [opcode.CALLCODE, Opcode.DELEGATECALL] else callee, - call_context, - stack, - is_warm_access, - CALLER.balance >= stack.value and depth < 1025, - ), - ) - for opcode, callee, call_context, stack, is_warm_access, depth in product( - opcodes, callees, call_contexts, stacks, is_warm_access, depths - ) - ] - - -TESTING_DATA = gen_testing_data() - - def callop_test_template( opcode: Opcode, callee: Account, @@ -403,6 +343,69 @@ def callop_test_template( ) +# +# testing for callop +# +def gen_testing_data(): + opcodes = [ + Opcode.CALL, + Opcode.CALLCODE, + Opcode.DELEGATECALL, + Opcode.STATICCALL, + ] + callees = [ + CALLEE_WITH_NOTHING, + CALLEE_WITH_STOP_BYTECODE_AND_BALANCE, + CALLEE_WITH_RETURN_BYTECODE, + CALLEE_WITH_REVERT_BYTECODE, + ] + call_contexts = [ + CallContext( + gas_left=100000, is_persistent=True, memory_word_size=8, reversible_write_counter=5 + ), + CallContext( + gas_left=100000, + is_persistent=False, + rw_counter_end_of_reversion=88, + reversible_write_counter=2, + ), + ] + stacks = [ + Stack(), + Stack(value=int(1e18), gas=100000), + Stack(value=int(1e18), gas=100, cd_offset=64, cd_length=320, rd_offset=0, rd_length=32), + Stack(cd_offset=0xFFFFFF, cd_length=0, rd_offset=0xFFFFFF, rd_length=0), + ] + is_warm_access = [True, False] + depths = [1, 1024, 1025] + return [ + ( + opcode, + callee, + call_context, + stack, + is_warm_access, + depth, + expected( + opcode, + callee.code_hash(), + # `callee = caller` for both CALLCODE and DELEGATECALL opcodes. + CALLER if opcode in [opcode.CALLCODE, Opcode.DELEGATECALL] else callee, + call_context, + stack, + is_warm_access, + CALLER.balance >= stack.value and depth < 1025, + ), + ) + for opcode, callee, call_context, stack, is_warm_access, depth in product( + opcodes, callees, call_contexts, stacks, is_warm_access, depths + ) + ] + + +TESTING_DATA = gen_testing_data() + + @pytest.mark.parametrize( "opcode, callee, caller_ctx, stack, is_warm_access, depth, expected", TESTING_DATA, @@ -496,6 +499,10 @@ def test_callop( ) +# +# callop for precompiles +# +# TODO add testing data for SHA256, RIPEMD160, BIGMODEXP and BLAKE2F def gen_precompile_testing_data(): opcodes = [ Opcode.CALL, @@ -635,7 +642,6 @@ def gen_precompile_testing_data(): PRECOMPILE_TESTING_DATA = gen_precompile_testing_data() - PRECOMPILE_RETURN_DATA = [0x01] * 64 @@ -656,8 +662,8 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): CALLER if opcode in [opcode.CALLCODE, Opcode.DELEGATECALL] else callee, caller_ctx, stack, - True, - True, + True, # is_warm + True, # is_precheck_ok ) ( @@ -674,9 +680,9 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): callee, caller_ctx, stack, - True, - 1, - True, + True, # is_warm + 1, # stack depth + True, # is_precompile expectation, ) @@ -724,7 +730,7 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): execution_state=ExecutionState.CALL_OP, rw_counter=rw_counter, call_id=1, - is_root=True, + is_root=False, is_create=False, code_hash=caller_bytecode_hash, program_counter=next_program_counter - 1, From 17c326c8301f2e035cf004eb725de83f9bc8b7fa Mon Sep 17 00:00:00 2001 From: KimiWu Date: Tue, 30 Jan 2024 21:33:16 +0900 Subject: [PATCH 07/13] feat: complete precompile checks in precompile_gadget --- .../evm_circuit/util/precompile_gadget.py | 13 +++++++++++++ tests/evm/test_callop.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py index 008782466..3fd2e9f15 100644 --- a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py +++ b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py @@ -21,3 +21,16 @@ def __init__( 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: + # input length is 128 bytes + instruction.constrain_equal(calldata_len, FQ(128)) + 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 + print(f"{calldata_len}") + instruction.constrain_equal(FQ(calldata_len.n % 192), FQ.zero()) diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index fee73f5b2..450c825fd 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -634,7 +634,7 @@ def gen_precompile_testing_data(): .push2(0x0160) .mstore(), ), - Stack(cd_offset=0, cd_length=0x20, rd_offset=0, rd_length=0x160), + Stack(cd_offset=0, cd_length=0x180, rd_offset=0, rd_length=0x160), ), ] From bedd91c951363b715a6c9da6a50b62592d830863 Mon Sep 17 00:00:00 2001 From: Kimi Wu Date: Wed, 7 Feb 2024 17:59:27 +0800 Subject: [PATCH 08/13] Update src/zkevm_specs/evm_circuit/execution/callop.py Co-authored-by: Chih Cheng Liang --- src/zkevm_specs/evm_circuit/execution/callop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index dbeccfd48..b5d6c18a5 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -155,7 +155,7 @@ def callop(instruction: Instruction): precompile_return_length = instruction.curr.aux_data[0] min_rd_copy_size = min(precompile_return_length, call.rd_length.n) - # precompiles have on code + # 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()) From 46f951ebea16fb708a59abccbfa7bd27a035d382 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 7 Feb 2024 18:15:54 +0800 Subject: [PATCH 09/13] fix: incorrect caller/callee call context --- .../evm_circuit/execution/callop.py | 4 +-- tests/evm/test_callop.py | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index b5d6c18a5..116a88afa 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -171,7 +171,7 @@ def callop(instruction: Instruction): (CallContextFieldTag.ReturnDataLength, call.rd_length), ]: instruction.constrain_equal_word( - instruction.call_context_lookup_word(field_tag, RW.Write), + instruction.call_context_lookup_word(field_tag, RW.Write, callee_call_id), WordOrValue(expected_value), ) @@ -193,7 +193,7 @@ def callop(instruction: Instruction): (CallContextFieldTag.LastCalleeReturnDataLength, FQ(precompile_return_length)), ]: instruction.constrain_equal( - instruction.call_context_lookup(field_tag, RW.Write, callee_call_id), + instruction.call_context_lookup(field_tag, RW.Write), expected_value, ) diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index 450c825fd..a8a227c86 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -289,21 +289,21 @@ def callop_test_template( .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, 0) elif is_precompile: rw_dictionary \ - .call_context_write(1, CallContextFieldTag.IsSuccess, True) \ - .call_context_write(1, CallContextFieldTag.CalleeAddress, Word(callee.address)) \ - .call_context_write(1, CallContextFieldTag.CallerId, 1) \ - .call_context_write(1, CallContextFieldTag.CallDataOffset, stack.cd_offset) \ - .call_context_write(1, CallContextFieldTag.CallDataLength, stack.cd_length) \ - .call_context_write(1, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) \ - .call_context_write(1, CallContextFieldTag.ReturnDataLength, stack.rd_length) \ - .call_context_write(call_id, CallContextFieldTag.ProgramCounter, next_program_counter) \ - .call_context_write(call_id, CallContextFieldTag.StackPointer, 1023) \ - .call_context_write(call_id, CallContextFieldTag.GasLeft, expected.caller_gas_left) \ - .call_context_write(call_id, CallContextFieldTag.MemorySize, expected.next_memory_size) \ - .call_context_write(call_id, CallContextFieldTag.ReversibleWriteCounter, caller_ctx.reversible_write_counter + 1) \ - .call_context_write(call_id, CallContextFieldTag.LastCalleeId, call_id) \ - .call_context_write(call_id, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ - .call_context_write(call_id, CallContextFieldTag.LastCalleeReturnDataLength, stack.rd_length) + .call_context_write(call_id, CallContextFieldTag.IsSuccess, True) \ + .call_context_write(call_id, CallContextFieldTag.CalleeAddress, Word(callee.address)) \ + .call_context_write(call_id, CallContextFieldTag.CallerId, 1) \ + .call_context_write(call_id, CallContextFieldTag.CallDataOffset, stack.cd_offset) \ + .call_context_write(call_id, CallContextFieldTag.CallDataLength, stack.cd_length) \ + .call_context_write(call_id, CallContextFieldTag.ReturnDataOffset, stack.rd_offset) \ + .call_context_write(call_id, CallContextFieldTag.ReturnDataLength, stack.rd_length) \ + .call_context_write(1, CallContextFieldTag.ProgramCounter, next_program_counter) \ + .call_context_write(1, CallContextFieldTag.StackPointer, 1023) \ + .call_context_write(1, CallContextFieldTag.GasLeft, expected.caller_gas_left) \ + .call_context_write(1, CallContextFieldTag.MemorySize, expected.next_memory_size) \ + .call_context_write(1, CallContextFieldTag.ReversibleWriteCounter, caller_ctx.reversible_write_counter + 1) \ + .call_context_write(1, CallContextFieldTag.LastCalleeId, call_id) \ + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataOffset, 0) \ + .call_context_write(1, CallContextFieldTag.LastCalleeReturnDataLength, stack.rd_length) else: rw_dictionary \ .call_context_write(1, CallContextFieldTag.ProgramCounter, next_program_counter) \ @@ -481,9 +481,9 @@ def test_callop( ) if is_empty_code_hash or is_precheck_ok is False else StepState( - execution_state=ExecutionState.STOP - if callee.code == STOP_BYTECODE - else ExecutionState.PUSH, + execution_state=( + ExecutionState.STOP if callee.code == STOP_BYTECODE else ExecutionState.PUSH + ), rw_counter=rw_dictionary.rw_counter, call_id=call_id, is_root=False, From 3cf679c68644488840d756cd3dadac1f2ce1f293 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 14 Feb 2024 10:32:33 +0800 Subject: [PATCH 10/13] fix: ecRecover allows input len is not 128 bytes --- src/zkevm_specs/evm_circuit/util/precompile_gadget.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py index 3fd2e9f15..25c990592 100644 --- a/src/zkevm_specs/evm_circuit/util/precompile_gadget.py +++ b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py @@ -22,8 +22,11 @@ def __init__( # input length is the same as return data length instruction.constrain_equal(precompile_return_len, calldata_len) elif precompile == Precompile.ECRECOVER: - # input length is 128 bytes - instruction.constrain_equal(calldata_len, FQ(128)) + # 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)) @@ -32,5 +35,4 @@ def __init__( instruction.constrain_equal(calldata_len, FQ(96)) elif precompile == Precompile.BN254PAIRING: # input length is 192 * n bytes - print(f"{calldata_len}") instruction.constrain_equal(FQ(calldata_len.n % 192), FQ.zero()) From b5816afe36977ad1b665848e5e47107f2254e5c8 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 14 Feb 2024 12:59:21 +0800 Subject: [PATCH 11/13] feat: add copy rlc for precompiles input/output --- .../evm_circuit/execution/callop.py | 46 ++++++++- tests/evm/test_callop.py | 95 ++++++++++++++----- 2 files changed, 115 insertions(+), 26 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/execution/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index 116a88afa..ca58eb5d2 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -152,7 +152,8 @@ def callop(instruction: Instruction): ) # precompiles call elif is_precheck_ok and is_precompile == FQ.one(): - precompile_return_length = instruction.curr.aux_data[0] + 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 @@ -197,6 +198,42 @@ def callop(instruction: Instruction): 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(): @@ -209,8 +246,10 @@ def callop(instruction: Instruction): FQ(min_rd_copy_size), call.rd_offset, FQ(min_rd_copy_size), - instruction.curr.rw_counter + instruction.rw_counter_offset, + 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 ) @@ -218,9 +257,8 @@ def callop(instruction: Instruction): # Give gas stipend if value is not zero callee_gas_left += has_value * GAS_STIPEND_CALL_WITH_VALUE - rwc = instruction.rw_counter_offset + return_copy_rwc_inc instruction.constrain_step_state_transition( - rw_counter=Transition.delta(rwc), + rw_counter=Transition.delta(rw_counter_inc), call_id=Transition.to(callee_call_id), is_root=Transition.to(False), is_create=Transition.to(False), diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index a8a227c86..96420c559 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -332,6 +332,7 @@ def callop_test_template( # fmt: on return ( + is_success, caller_bytecode, callee_bytecode, call_id, @@ -420,6 +421,7 @@ def test_callop( expected: Expected, ): ( + _, caller_bytecode, callee_bytecode, call_id, @@ -642,6 +644,8 @@ def gen_precompile_testing_data(): PRECOMPILE_TESTING_DATA = gen_precompile_testing_data() +PRECOMPILE_INPUT_DATA = [0x01] * 384 +PRECOMPILE_OUTPUT_DATA = [0x01] * 64 PRECOMPILE_RETURN_DATA = [0x01] * 64 @@ -651,6 +655,7 @@ def gen_precompile_testing_data(): ) def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): randomness_keccak = rand_fq() + caller_id = 1 exe_state = precompile[0] callee = precompile[1] @@ -667,6 +672,7 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): ) ( + is_success, caller_bytecode, callee_bytecode, call_id, @@ -689,25 +695,68 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): caller_bytecode_hash = Word(caller_bytecode.hash()) rw_counter = call_id - src_data = dict( - [ - (i, PRECOMPILE_RETURN_DATA[i] if i < len(PRECOMPILE_RETURN_DATA) else 0) - for i in range(0, stack.rd_length) - ] - ) - copy_circuit = CopyCircuit().copy( - randomness_keccak, - rw_dictionary, - call_id, - CopyDataTypeTag.Memory, - 1, - CopyDataTypeTag.Memory, - 0, - stack.rd_length, - 0, - stack.rd_length, - src_data, - ) + if stack.cd_length != 0: + input_data = dict( + [ + (i, PRECOMPILE_INPUT_DATA[i] if i < len(PRECOMPILE_INPUT_DATA) else 0) + for i in range(0, stack.cd_length) + ] + ) + copy_input_rlc = CopyCircuit().copy( + randomness_keccak, + rw_dictionary, + caller_id, + CopyDataTypeTag.Memory, + call_id, + CopyDataTypeTag.RlcAcc, + stack.cd_offset, + stack.cd_offset + stack.cd_length, + 0, + stack.cd_length, + input_data, + ) + + if is_success is True and stack.rd_length != 0: + output_data = dict( + [ + (i, PRECOMPILE_OUTPUT_DATA[i] if i < len(PRECOMPILE_OUTPUT_DATA) else 0) + for i in range(0, stack.rd_length) + ] + ) + copy_output_rlc = CopyCircuit().copy( + randomness_keccak, + rw_dictionary, + call_id, + CopyDataTypeTag.Memory, + call_id, + CopyDataTypeTag.RlcAcc, + stack.cd_offset, + stack.cd_offset + stack.rd_length, + 0, + stack.rd_length, + output_data, + ) + + if is_success is True and stack.rd_length != 0: + return_data = dict( + [ + (i, PRECOMPILE_RETURN_DATA[i] if i < len(PRECOMPILE_RETURN_DATA) else 0) + for i in range(0, stack.rd_length) + ] + ) + copy_return_data = CopyCircuit().copy( + randomness_keccak, + rw_dictionary, + call_id, + CopyDataTypeTag.Memory, + caller_id, + CopyDataTypeTag.Memory, + 0, + stack.rd_length, + 0, + stack.rd_length, + return_data, + ) tables = Tables( block_table=set(Block().table_assignments()), @@ -720,16 +769,18 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): ) ), rw_table=set(rw_dictionary.rws), - copy_circuit=copy_circuit.rows, + copy_circuit=copy_input_rlc.rows + copy_output_rlc.rows + copy_return_data.rows, ) + aux_data = [stack.cd_length, stack.rd_length] + verify_steps( tables=tables, steps=[ StepState( execution_state=ExecutionState.CALL_OP, rw_counter=rw_counter, - call_id=1, + call_id=caller_id, is_root=False, is_create=False, code_hash=caller_bytecode_hash, @@ -738,7 +789,7 @@ def test_callop_precompiles(opcode: Opcode, precompile: tuple[Account, Stack]): gas_left=caller_ctx.gas_left, memory_word_size=caller_ctx.memory_word_size, reversible_write_counter=caller_ctx.reversible_write_counter, - aux_data=[stack.rd_length], + aux_data=aux_data, ), StepState( execution_state=exe_state, From f3eee6f6f45b079af55251945d59e20a0d517b58 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Wed, 14 Feb 2024 16:09:19 +0800 Subject: [PATCH 12/13] feat: add input/output rlc data check in ecRecover (PoC) --- .../execution/precompiles/ecrecover.py | 24 +++++++++++++++++++ tests/evm/precompiles/test_ecRecover.py | 17 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py b/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py index 779726ada..7d424c4de 100644 --- a/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py +++ b/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from zkevm_specs.evm_circuit.instruction import Instruction from zkevm_specs.evm_circuit.table import ( CallContextFieldTag, @@ -5,10 +6,17 @@ RW, ) from zkevm_specs.util import FQ, Word, EcrecoverGas +from zkevm_specs.util.arithmetic import RLC SECP256K1N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +@dataclass(frozen=True) +class PrecompileRlcData: + 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) @@ -26,9 +34,25 @@ def ecRecover(instruction: Instruction): sig_r: Word = instruction.curr.aux_data[2] sig_s: Word = instruction.curr.aux_data[3] recovered_addr: FQ = instruction.curr.aux_data[4] + rlc_data: PrecompileRlcData = instruction.curr.aux_data[5] + keccak_randomness: FQ = instruction.curr.aux_data[6] 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(rlc_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(rlc_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)) diff --git a/tests/evm/precompiles/test_ecRecover.py b/tests/evm/precompiles/test_ecRecover.py index 9bf610a46..140fd7d2b 100644 --- a/tests/evm/precompiles/test_ecRecover.py +++ b/tests/evm/precompiles/test_ecRecover.py @@ -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 PrecompileRlcData, 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(): @@ -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", @@ -72,12 +75,24 @@ def test_ecRecover( return_data_offset = 0 return_data_length = 0x20 if recovered else 0 + 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() + rlc_data = PrecompileRlcData(input_rlc, output_rlc) + aux_data = [ Word(msg_hash), Word(v + 27), Word(r), Word(s), FQ(int.from_bytes(address, "big")), + rlc_data, + randomness_keccak, ] # assign sig_table From 2e5f9263b74ced37ad3d9aea02874ed90bb85bb4 Mon Sep 17 00:00:00 2001 From: KimiWu Date: Sat, 17 Feb 2024 12:12:45 +0800 Subject: [PATCH 13/13] refactor ecrecover rlc input --- .../execution/precompiles/ecrecover.py | 25 +++++++++++-------- tests/evm/precompiles/test_ecRecover.py | 13 ++++++---- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py b/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py index 7d424c4de..3547c2356 100644 --- a/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py +++ b/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py @@ -12,7 +12,12 @@ @dataclass(frozen=True) -class PrecompileRlcData: +class PrecompileAuxData: + msg_hash: Word + sig_v: Word + sig_r: Word + sig_s: Word + recovered_addr: FQ input_rlc: FQ output_rlc: FQ @@ -29,13 +34,13 @@ 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] - rlc_data: PrecompileRlcData = instruction.curr.aux_data[5] - keccak_randomness: FQ = instruction.curr.aux_data[6] + 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)) @@ -46,12 +51,12 @@ def ecRecover(instruction: Instruction): 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(rlc_data.input_rlc, input_rlc) + 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(rlc_data.output_rlc, output_rlc) + 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 diff --git a/tests/evm/precompiles/test_ecRecover.py b/tests/evm/precompiles/test_ecRecover.py index 140fd7d2b..a4da821e6 100644 --- a/tests/evm/precompiles/test_ecRecover.py +++ b/tests/evm/precompiles/test_ecRecover.py @@ -14,7 +14,7 @@ Tables, verify_steps, ) -from zkevm_specs.evm_circuit.execution.precompiles.ecrecover import PrecompileRlcData, SECP256K1N +from zkevm_specs.evm_circuit.execution.precompiles.ecrecover import PrecompileAuxData, SECP256K1N from zkevm_specs.util import ( Word, FQ, @@ -83,15 +83,18 @@ def test_ecRecover( 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() - rlc_data = PrecompileRlcData(input_rlc, output_rlc) - - aux_data = [ + aux_data = PrecompileAuxData( Word(msg_hash), Word(v + 27), Word(r), Word(s), FQ(int.from_bytes(address, "big")), - rlc_data, + input_rlc, + output_rlc, + ) + + aux_data = [ + aux_data, randomness_keccak, ]