From b4e907369ecbd68db88be2800884f716416c85f1 Mon Sep 17 00:00:00 2001 From: Steven Gu Date: Sat, 3 Dec 2022 19:03:18 +0800 Subject: [PATCH] Implement opcode `EXTCODESIZE`. --- specs/opcode/3bEXTCODESIZE.md | 51 +++++++ src/zkevm_specs/evm/execution/__init__.py | 2 + src/zkevm_specs/evm/execution/extcodesize.py | 43 ++++++ tests/evm/test_extcodesize.py | 132 +++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 specs/opcode/3bEXTCODESIZE.md create mode 100644 src/zkevm_specs/evm/execution/extcodesize.py create mode 100644 tests/evm/test_extcodesize.py diff --git a/specs/opcode/3bEXTCODESIZE.md b/specs/opcode/3bEXTCODESIZE.md new file mode 100644 index 000000000..e5433afa7 --- /dev/null +++ b/specs/opcode/3bEXTCODESIZE.md @@ -0,0 +1,51 @@ +# EXTCODESIZE opcode + +## Procedure + +The `EXTCODESIZE` opcode gets code size of the given account. + +## EVM behaviour + +The `EXTCODESIZE` opcode pops `address` (20 bytes of data) off the stack and +pushes the code size of the corresponding account onto the stack. If the given +account doesn't exist (by checking non existing flag), then it will push 0 onto +the stack instead. + +## Circuit behaviour + +1. Construct call context table in rw table. +2. Do bus-mapping lookup for stack read, call context read and account read + operations. +3. Do bus-mapping lookup for transaction access list write and stack write + operations. + +## Constraints + +1. opId = 0x3b +2. State transition: + - gc + 7 (1 stack read, 1 stack write, 3 call context reads, 1 account read, + 1 transaction access list write) + - stack_pointer + 0 (one pop and one push) + - pc + 1 + - gas: + - the accessed `address` is warm: WARM_STORAGE_READ_COST + - the accessed `address` is cold: COLD_ACCOUNT_ACCESS_COST +3. Lookups: 7 bus-mapping lookups + - `address` is popped from the stack. + - 3 from call context for `tx_id`, `rw_counter_end_of_reversion`, and + `is_persistent`. + - `address` is added to the transaction access list if not already present. + - If witness value `exists == 1`, lookup account `code_hash`, then get + `code_size`. Otherwise only lookup the account non-existing proof. + - The EXTCODESIZE result is at the new top of the stack. +4. Additional Constraints + - value `is_warm` matches the gas cost for this opcode. + +## Exceptions + +1. stack underflow: if the stack starts empty +2. out of gas: remaining gas is not enough + +## Code + +Please refer to `src/zkevm_specs/evm/execution/extcodesize.py`. diff --git a/src/zkevm_specs/evm/execution/__init__.py b/src/zkevm_specs/evm/execution/__init__.py index aab5d5c13..db67f9bc7 100644 --- a/src/zkevm_specs/evm/execution/__init__.py +++ b/src/zkevm_specs/evm/execution/__init__.py @@ -38,6 +38,7 @@ from .storage import * from .selfbalance import * from .extcodehash import * +from .extcodesize import * from .log import * from .bitwise import not_opcode from .sdiv_smod import sdiv_smod @@ -87,6 +88,7 @@ ExecutionState.GASPRICE: gasprice, ExecutionState.EXTCODECOPY: extcodecopy, ExecutionState.EXTCODEHASH: extcodehash, + ExecutionState.EXTCODESIZE: extcodesize, ExecutionState.EXP: exp, ExecutionState.LOG: log, ExecutionState.CALL_OP: callop, diff --git a/src/zkevm_specs/evm/execution/extcodesize.py b/src/zkevm_specs/evm/execution/extcodesize.py new file mode 100644 index 000000000..e21ccc9c4 --- /dev/null +++ b/src/zkevm_specs/evm/execution/extcodesize.py @@ -0,0 +1,43 @@ +from ...util import ( + EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, + FQ, + N_BYTES_ACCOUNT_ADDRESS, + N_BYTES_U64, + RLC, +) +from ..instruction import Instruction, Transition +from ..opcode import Opcode +from ..table import AccountFieldTag, CallContextFieldTag + + +def extcodesize(instruction: Instruction): + opcode = instruction.opcode_lookup(True) + instruction.constrain_equal(opcode, Opcode.EXTCODESIZE) + + address = instruction.rlc_to_fq(instruction.stack_pop(), N_BYTES_ACCOUNT_ADDRESS) + + tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) + is_warm = instruction.add_account_to_access_list(tx_id, address, instruction.reversion_info()) + + # Load account `exists` value from auxilary witness data. + exists = instruction.curr.aux_data + + if exists == 1: + code_hash = instruction.account_read(address, AccountFieldTag.CodeHash) + code_size = instruction.bytecode_length(code_hash) + else: # exists == 0 + instruction.account_read(address, AccountFieldTag.NonExisting) + code_size = RLC(0) + + instruction.constrain_equal( + instruction.select(exists, code_size, FQ(0)), + instruction.rlc_to_fq(instruction.stack_push(), N_BYTES_U64), + ) + + instruction.step_state_transition_in_same_context( + opcode, + rw_counter=Transition.delta(7), + program_counter=Transition.delta(1), + stack_pointer=Transition.same(), + dynamic_gas_cost=instruction.select(is_warm, FQ(0), FQ(EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS)), + ) diff --git a/tests/evm/test_extcodesize.py b/tests/evm/test_extcodesize.py new file mode 100644 index 000000000..fea8bcff6 --- /dev/null +++ b/tests/evm/test_extcodesize.py @@ -0,0 +1,132 @@ +import pytest +from itertools import chain +from zkevm_specs.evm import ( + AccountFieldTag, + Block, + Bytecode, + CallContextFieldTag, + ExecutionState, + RWDictionary, + StepState, + Tables, + verify_steps, +) +from zkevm_specs.util import ( + EMPTY_CODE_HASH, + EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, + GAS_COST_WARM_ACCESS, + RLC, + U160, + U256, + keccak256, + rand_address, + rand_bytes, + rand_fq, + rand_range, + rand_word, +) + +TESTING_DATA = [ + (0x30000, bytes(), 0, True, True), # warm empty account + (0x30000, bytes(), 0, False, True), # cold empty account + (0x30000, bytes([10, 40]), 1, True, True), # warm non-empty account + (0x30000, bytes([10, 10, 40]), 1, False, True), # cold non-empty account + (0x30000, bytes(), 1, False, True), # non-empty account with empty code + ( + rand_address(), + rand_bytes(100), + rand_range(2), + rand_range(2), + True, # persistent call + ), + ( + rand_address(), + rand_bytes(100), + rand_range(2), + rand_range(2), + False, # reverted call + ), +] + + +@pytest.mark.parametrize("address, code, exists, is_warm, is_persistent", TESTING_DATA) +def test_extcodesize(address: U160, code: bytes, exists: int, is_warm: bool, is_persistent: bool): + randomness = rand_fq() + + code_hash = int.from_bytes(keccak256(code), "big") + code_size = len(code) if exists == 1 else 0 + + tx_id = 1 + call_id = 1 + + rw_counter_end_of_reversion = 0 + reversible_write_counter = 0 + + rw_dictionary = ( + RWDictionary(1) + .stack_read(call_id, 1023, RLC(address, randomness)) + .call_context_read(tx_id, CallContextFieldTag.TxId, tx_id) + .call_context_read( + tx_id, CallContextFieldTag.RwCounterEndOfReversion, rw_counter_end_of_reversion + ) + .call_context_read(tx_id, CallContextFieldTag.IsPersistent, is_persistent) + .tx_access_list_account_write( + tx_id, + address, + True, + is_warm, + rw_counter_of_reversion=rw_counter_end_of_reversion - reversible_write_counter, + ) + ) + if exists == 1: + rw_dictionary.account_read(address, AccountFieldTag.CodeHash, RLC(code_hash, randomness)) + else: + rw_dictionary.account_read( + address, AccountFieldTag.NonExisting, RLC(1 - exists, randomness) + ) + + rw_table = set(rw_dictionary.stack_write(call_id, 1023, RLC(code_size, randomness)).rws) + + bytecode = Bytecode().extcodesize() + tables = Tables( + block_table=Block(), + tx_table=set(), + bytecode_table=set( + chain( + bytecode.table_assignments(randomness), + Bytecode(code).table_assignments(randomness), + ) + ), + rw_table=rw_table, + ) + + bytecode_hash = RLC(bytecode.hash(), randomness) + verify_steps( + randomness=randomness, + tables=tables, + steps=[ + StepState( + execution_state=ExecutionState.EXTCODESIZE, + rw_counter=1, + call_id=1, + is_root=True, + is_create=False, + code_hash=bytecode_hash, + program_counter=0, + stack_pointer=1023, + gas_left=GAS_COST_WARM_ACCESS + (not is_warm) * EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, + aux_data=exists, + ), + StepState( + execution_state=ExecutionState.STOP if is_persistent else ExecutionState.REVERT, + rw_counter=8, + call_id=1, + is_root=True, + is_create=False, + code_hash=bytecode_hash, + program_counter=1, + stack_pointer=1023, + gas_left=0, + ), + ], + )