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/__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/callop.py b/src/zkevm_specs/evm_circuit/execution/callop.py index 9aa5f94fe..ca58eb5d2 100644 --- a/src/zkevm_specs/evm_circuit/execution/callop.py +++ b/src/zkevm_specs/evm_circuit/execution/callop.py @@ -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 @@ -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 [ @@ -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), diff --git a/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py b/src/zkevm_specs/evm_circuit/execution/precompiles/ecrecover.py index 779726ada..3547c2356 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,22 @@ 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) @@ -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)) 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 98c5a7240..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() 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..25c990592 --- /dev/null +++ b/src/zkevm_specs/evm_circuit/util/precompile_gadget.py @@ -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()) diff --git a/tests/evm/precompiles/test_ecRecover.py b/tests/evm/precompiles/test_ecRecover.py index 9bf610a46..a4da821e6 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 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(): @@ -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,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 diff --git a/tests/evm/test_callop.py b/tests/evm/test_callop.py index 4dd9dd418..96420c559 100644 --- a/tests/evm/test_callop.py +++ b/tests/evm/test_callop.py @@ -14,6 +14,9 @@ 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 ( EMPTY_CODE_HASH, GAS_COST_ACCOUNT_COLD_ACCESS, @@ -24,7 +27,8 @@ Word, U256, ) -from common import CallContext +from common import CallContext, rand_fq + Stack = namedtuple( "Stack", @@ -103,89 +107,23 @@ 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, - CALLER, - callee, - PARENT_CALLER, - PARENT_VALUE, - 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, 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 +185,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 +282,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(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) \ @@ -378,6 +331,113 @@ def test_callop( .call_context_read(call_id, CallContextFieldTag.CodeHash, callee_bytecode_hash) # fmt: on + return ( + is_success, + caller_bytecode, + callee_bytecode, + call_id, + next_program_counter, + stack_pointer, + rw_dictionary, + is_precheck_ok, + is_empty_code_hash, + ) + + +# +# 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, +) +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(), @@ -423,9 +483,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, @@ -439,3 +499,310 @@ def test_callop( ), ], ) + + +# +# callop for precompiles +# +# TODO add testing data for SHA256, RIPEMD160, BIGMODEXP and BLAKE2F +def gen_precompile_testing_data(): + opcodes = [ + Opcode.CALL, + Opcode.CALLCODE, + Opcode.DELEGATECALL, + Opcode.STATICCALL, + ] + precompiles = [ + ( + ExecutionState.ECRECOVER, + Account( + address=Precompile.ECRECOVER, + 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), + ), + ( + 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=0x180, rd_offset=0, rd_length=0x160), + ), + ] + + return [(opcode, callee) for opcode, callee in product(opcodes, precompiles)] + + +PRECOMPILE_TESTING_DATA = gen_precompile_testing_data() +PRECOMPILE_INPUT_DATA = [0x01] * 384 +PRECOMPILE_OUTPUT_DATA = [0x01] * 64 +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() + caller_id = 1 + + 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, # is_warm + True, # is_precheck_ok + ) + + ( + is_success, + caller_bytecode, + callee_bytecode, + call_id, + next_program_counter, + stack_pointer, + rw_dictionary, + _, + _, + ) = callop_test_template( + opcode, + callee, + caller_ctx, + stack, + True, # is_warm + 1, # stack depth + True, # is_precompile + expectation, + ) + + caller_bytecode_hash = Word(caller_bytecode.hash()) + rw_counter = call_id + + 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()), + 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_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=caller_id, + is_root=False, + 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=aux_data, + ), + 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), + ), + ], + )