Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions specs/opcode/3bEXTCODESIZE.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 2 additions & 0 deletions src/zkevm_specs/evm/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions src/zkevm_specs/evm/execution/extcodesize.py
Original file line number Diff line number Diff line change
@@ -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)),
)
132 changes: 132 additions & 0 deletions tests/evm/test_extcodesize.py
Original file line number Diff line number Diff line change
@@ -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,
),
],
)