├── src └── zkevm_specs │ ├── py.typed │ ├── evm_circuit │ ├── util │ │ ├── __init__.py │ │ └── memory_gadget.py │ ├── __init__.py │ ├── execution │ │ ├── error_stack.py │ │ ├── pop.py │ │ ├── error_invalid_opcode.py │ │ ├── msize.py │ │ ├── iszero.py │ │ ├── gas.py │ │ ├── codesize.py │ │ ├── not_.py │ │ ├── add_sub.py │ │ ├── callvalue.py │ │ ├── address.py │ │ ├── origin.py │ │ ├── error_oog_constant.py │ │ ├── caller.py │ │ ├── calldatasize.py │ │ ├── gasprice.py │ │ ├── jump.py │ │ ├── selfbalance.py │ │ ├── bitwise.py │ │ ├── returndatasize.py │ │ ├── jumpi.py │ │ ├── extcodehash.py │ │ ├── comparator.py │ │ ├── block_ctx.py │ │ ├── byte.py │ │ ├── balance.py │ │ ├── push.py │ │ ├── blockhash.py │ │ ├── extcodesize.py │ │ ├── error_invalid_jump.py │ │ ├── slt_sgt.py │ │ ├── memory.py │ │ ├── codecopy.py │ │ ├── error_oog_call.py │ │ ├── sha3.py │ │ ├── returndatacopy.py │ │ ├── exp.py │ │ ├── error_write_protection.py │ │ ├── addmod.py │ │ ├── calldataload.py │ │ ├── extcodecopy.py │ │ ├── mulmod.py │ │ ├── stop.py │ │ ├── signextend.py │ │ ├── calldatacopy.py │ │ └── dataCopy.py │ ├── main.py │ ├── precompile.py │ └── step.py │ ├── util │ ├── __init__.py │ ├── typing.py │ └── hash.py │ └── __init__.py ├── CODEOWNERS ├── specs ├── super_circuit.png ├── tx_circuit.rev1.png ├── public_inputs_deprecated.rev1.png ├── opcode │ ├── 00STOP.md │ ├── 15ISZERO.md │ ├── 59MSIZE.md │ ├── 38CODESIZE.md │ ├── 50POP.md │ ├── 19NOT.md │ ├── 80DUPX.md │ ├── 46CHAINID.md │ ├── 47SELFBALANCE.md │ ├── 5AGAS.md │ ├── 3aGASPRICE.md │ ├── 40BLOCKHASH.md │ ├── 3dRETURNDATASIZE.md │ ├── 1aBYTE.md │ ├── 30ADDRESS.md │ ├── 33CALLER.md │ ├── 34CALLVALUE.md │ ├── 32ORIGIN.md │ ├── 36CALLDATASIZE.md │ ├── 90SWAPX.md │ ├── 16AND_17OR_18XOR.md │ ├── 56JUMP.md │ ├── 5BJUMPDEST.md │ ├── 01ADD_03SUB.md │ ├── 0bSIGNEXTEND.md │ ├── 57JUMPI.md │ ├── 3fEXTCODEHASH.md │ ├── 08ADDMOD.md │ ├── 10LT_11GT_14EQ.md │ ├── 39CODECOPY.md │ ├── 41COINBASE_45GASLIMIT_48BASEFEE.md │ ├── 31BALANCE.md │ ├── 0AEXP.md │ ├── 3bEXTCODESIZE.md │ ├── 35CALLDATALOAD.md │ ├── 3cEXTCODECOPY.md │ ├── 12SLT_13SGT.md │ ├── 09MULMOD.md │ ├── 37CALLDATACOPY.md │ ├── 3eRETURNDATACOPY.md │ ├── 1bSHL_1cSHR.md │ └── A0_A4LOG.md ├── error_state │ ├── ErrorInvalidOpcode.md │ ├── ErrorInvalidJump.md │ ├── ErrorStackOverflow_Underflow.md │ ├── ErrorOutOfGasLog.md │ ├── ErrorOutOfGasConstant.md │ ├── ErrorOutOfGasEXP.md │ ├── ErrorWriteProtection.md │ ├── ErrorOutOfGasCall.md │ ├── ErrorOutOfGasStaticMemoryExpansion.md │ ├── ErrorOutOfGasDynamicMemoryExpansion.md │ ├── ErrorOutOfGasSloadSstore.md │ └── ErrorCodeStore.md ├── precompile │ └── 04dataCopy.md ├── lookup.md ├── introduction.md └── super_circuit.md ├── .gitignore ├── tests ├── conftest.py └── evm │ ├── test_codesize.py │ ├── test_pop.py │ ├── test_msize.py │ ├── test_not.py │ ├── test_returndatasize.py │ ├── test_iszero.py │ ├── test_jump.py │ ├── test_callvalue.py │ ├── test_caller.py │ ├── test_address.py │ ├── test_calldatasize.py │ ├── test_origin.py │ ├── test_push.py │ ├── test_gasprice.py │ ├── test_selfbalance.py │ ├── test_gas.py │ ├── test_byte.py │ ├── test_mulmod.py │ ├── test_addmod.py │ ├── test_add_sub.py │ ├── test_bitwise.py │ ├── test_blockhash.py │ └── test_signextend.py ├── pyproject.toml ├── Makefile ├── .github └── workflows │ └── python-package.yml ├── README.md ├── LICENSE └── setup.cfg /src/zkevm_specs/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @appliedzkp/zkevm-reviewers -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .memory_gadget import BufferReaderGadget 2 | -------------------------------------------------------------------------------- /specs/super_circuit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scroll-tech/zkevm-specs/HEAD/specs/super_circuit.png -------------------------------------------------------------------------------- /specs/tx_circuit.rev1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scroll-tech/zkevm-specs/HEAD/specs/tx_circuit.rev1.png -------------------------------------------------------------------------------- /specs/public_inputs_deprecated.rev1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scroll-tech/zkevm-specs/HEAD/specs/public_inputs_deprecated.rev1.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | .python-version 4 | .mypy_cache 5 | .pytest_cache 6 | build 7 | .idea 8 | *.DS_Store 9 | .vscode 10 | -------------------------------------------------------------------------------- /src/zkevm_specs/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .arithmetic import * 2 | from .constraint_system import * 3 | from .hash import * 4 | from .param import * 5 | from .typing import * 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # This will make pytest rewrite the asserts in `src/zkevm_specs/` so that they 4 | # print the values used in the assertions automatically. 5 | pytest.register_assert_rewrite("zkevm_specs") 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.black] 9 | line-length = 100 10 | target-version = ['py39'] 11 | include = '\.pyi?$' 12 | -------------------------------------------------------------------------------- /src/zkevm_specs/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bytecode_circuit 2 | from . import copy_circuit 3 | from . import evm_circuit 4 | from . import exp_circuit 5 | from . import state_circuit 6 | from . import tx_circuit 7 | from . import util 8 | from . import pi_circuit 9 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/__init__.py: -------------------------------------------------------------------------------- 1 | from .execution import * 2 | from .execution_state import * 3 | from .main import * 4 | from .opcode import * 5 | from .precompile import * 6 | from .step import * 7 | from .table import * 8 | from .typing import * 9 | from .util import * 10 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/error_stack.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction 2 | 3 | 4 | def error_stack(instruction: Instruction): 5 | # retrieve op code associated to stack error 6 | opcode = instruction.opcode_lookup(True) 7 | instruction.responsible_opcode_lookup(opcode, instruction.curr.stack_pointer) 8 | 9 | instruction.constrain_error_state(1 + instruction.curr.reversible_write_counter.n) 10 | -------------------------------------------------------------------------------- /src/zkevm_specs/util/typing.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | 3 | U8 = NewType("U8", int) 4 | U64 = NewType("U64", int) 5 | U128 = NewType("U128", int) 6 | U160 = NewType("U160", int) 7 | U256 = NewType("U256", int) 8 | 9 | 10 | def is_circuit_code(func): 11 | """ 12 | A no-op decorator just to mark the function 13 | """ 14 | 15 | def wrapper(*args, **kargs): 16 | return func(*args, **kargs) 17 | 18 | return wrapper 19 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/pop.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | 3 | 4 | def pop(instruction: Instruction): 5 | opcode = instruction.opcode_lookup(True) 6 | 7 | y = instruction.stack_pop() 8 | 9 | instruction.step_state_transition_in_same_context( 10 | opcode, 11 | rw_counter=Transition.delta(1), 12 | program_counter=Transition.delta(1), 13 | stack_pointer=Transition.delta(1), 14 | ) 15 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/error_invalid_opcode.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction 2 | 3 | 4 | # Gadget for invalid opcodes. It verifies by a fixed lookup for ResponsibleOpcode. 5 | def error_invalid_opcode(instruction: Instruction): 6 | # Fixed lookup for invalid opcode. 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.responsible_opcode_lookup(opcode) 9 | 10 | instruction.constrain_error_state(1 + instruction.curr.reversible_write_counter.n) 11 | -------------------------------------------------------------------------------- /src/zkevm_specs/util/hash.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from Crypto.Hash import keccak 3 | 4 | from .typing import U256 5 | 6 | 7 | def keccak256(data: Union[str, bytes, bytearray]) -> bytes: 8 | if isinstance(data, str): 9 | data = bytes.fromhex(data) 10 | return keccak.new(digest_bits=256).update(data).digest() 11 | 12 | 13 | EMPTY_HASH: U256 = U256(int.from_bytes(keccak256(""), "big")) 14 | EMPTY_CODE_HASH: U256 = EMPTY_HASH 15 | EMPTY_TRIE_HASH: U256 = U256(int.from_bytes(keccak256("80"), "big")) 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: ## Display this help screen 2 | @grep -h \ 3 | -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 4 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 5 | 6 | install: # Install the Python packages 7 | pip3 install .[test,lint] 8 | 9 | fmt: ## Format the code 10 | black . 11 | 12 | lint: ## Check whether the code is formatted correctly 13 | black . --check 14 | flake8 . 15 | 16 | type: ## Check the typing of the Python code 17 | MYPATH=src mypy . 18 | 19 | test: ## Run tests 20 | pytest --doctest-modules 21 | 22 | 23 | .PHONY: help install fmt lint test 24 | -------------------------------------------------------------------------------- /specs/opcode/00STOP.md: -------------------------------------------------------------------------------- 1 | # STOP opcode 2 | 3 | ## Procedure 4 | 5 | ### EVM behavior 6 | 7 | The `STOP` opcode terminates the call, then: 8 | 9 | 1. If it's a root call, it ends the execution. 10 | 2. Otherwise, it restores caller's context and switch to it. 11 | 12 | ### Circuit behavior 13 | 14 | The circuit first checks the `result` in call context is indeed success. Then: 15 | 16 | 1. If it's a root call, it transits to `EndTx`. 17 | 2. Otherwise, it restores caller's context by reading to `rw_table`, then does step state transition to it. 18 | 19 | ## Code 20 | 21 | Please refer to `src/zkevm_specs/evm/execution/stop.py`. 22 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/msize.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ...util import N_BYTES_WORD, Word 3 | 4 | 5 | def msize(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | 8 | instruction.constrain_equal_word( 9 | Word.from_lo(instruction.curr.memory_word_size * N_BYTES_WORD), instruction.stack_push() 10 | ) 11 | 12 | instruction.step_state_transition_in_same_context( 13 | opcode, 14 | rw_counter=Transition.delta(1), 15 | program_counter=Transition.delta(1), 16 | stack_pointer=Transition.delta(-1), 17 | ) 18 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/iszero.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ...util import Word 3 | 4 | 5 | def iszero(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | 8 | value = instruction.stack_pop() 9 | 10 | instruction.constrain_equal_word( 11 | Word.from_lo(instruction.is_zero_word(value)), 12 | instruction.stack_push(), 13 | ) 14 | 15 | instruction.step_state_transition_in_same_context( 16 | opcode, 17 | rw_counter=Transition.delta(2), 18 | program_counter=Transition.delta(1), 19 | stack_pointer=Transition.same(), 20 | ) 21 | -------------------------------------------------------------------------------- /specs/opcode/15ISZERO.md: -------------------------------------------------------------------------------- 1 | # ISZERO opcode 2 | 3 | ## Procedure 4 | 5 | Pop an EVM word `value` from the stack. If it is zero, push `1` back to the stack. Otherwise push `0` to stack. 6 | 7 | ## Constraints 8 | 9 | 1. state transition: 10 | - gc + 2 (1 stack read + 1 stack write) 11 | - stack_pointer + 0 (one pop and one push) 12 | - pc + 1 13 | - gas + 3 14 | 2. Lookups: 2 busmapping lookups 15 | - `value` is at the top of the stack 16 | - `result`, is at the new top of the stack 17 | 18 | ## Exceptions 19 | 20 | 1. stack underflow: 21 | - the stack is empty: `1024 == stack_pointer` 22 | 2. out of gas: remaining gas is not enough 23 | 24 | ## Code 25 | 26 | See `src/zkevm_specs/evm/execution/iszero.py` 27 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/gas.py: -------------------------------------------------------------------------------- 1 | from ...util import Word 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | 5 | 6 | def gas(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.GAS) 9 | 10 | instruction.constrain_equal_word( 11 | Word.from_lo(instruction.curr.gas_left - Opcode.GAS.constant_gas_cost()), 12 | instruction.stack_push(), 13 | ) 14 | 15 | instruction.step_state_transition_in_same_context( 16 | opcode, 17 | rw_counter=Transition.delta(1), 18 | program_counter=Transition.delta(1), 19 | stack_pointer=Transition.delta(-1), 20 | ) 21 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/codesize.py: -------------------------------------------------------------------------------- 1 | from ...util import Word 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | 5 | 6 | def codesize(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.CODESIZE) 9 | 10 | code_size = instruction.bytecode_length(instruction.curr.code_hash) 11 | 12 | instruction.constrain_equal_word(Word.from_lo(code_size), instruction.stack_push()) 13 | 14 | instruction.step_state_transition_in_same_context( 15 | opcode, 16 | rw_counter=Transition.delta(1), 17 | program_counter=Transition.delta(1), 18 | stack_pointer=Transition.delta(-1), 19 | ) 20 | -------------------------------------------------------------------------------- /specs/error_state/ErrorInvalidOpcode.md: -------------------------------------------------------------------------------- 1 | # ErrorInvalidOpcode state 2 | 3 | ## Procedure 4 | 5 | `ErrorInvalidOpcode` may occur when an invalid opcode is encountered in any step during the execution. 6 | 7 | ### EVM behavior 8 | 9 | 1. If it's a root call, it ends the execution. 10 | 2. Otherwise, it restores caller's context and switch to it. 11 | 12 | ### Constraints 13 | 14 | 1. Do a fixed lookup for `FixedTableTag.ResponsibleOpcode`. 15 | 2. Current call must be failed. Do a call context lookup for `CallContextFieldTag.IsSuccess`. 16 | 3. If it's a root call, it transits to `EndTx`. 17 | 4. If it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 18 | 19 | ## Code 20 | 21 | Please refer to `src/zkevm_specs/evm/execution/error_invalid_opcode.py`. 22 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/not_.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition, FixedTableTag 2 | from ...util import FQ 3 | 4 | 5 | def not_opcode(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | 8 | a = instruction.stack_pop() 9 | a_le_bytes = a.to_le_bytes() 10 | b = instruction.stack_push() 11 | b_le_bytes = b.to_le_bytes() 12 | 13 | for i in range(32): 14 | byte_a = FQ(a_le_bytes[i]) 15 | byte_b = FQ(b_le_bytes[i]) 16 | instruction.fixed_lookup(FixedTableTag.BitwiseXor, byte_a, byte_b, FQ(255)) 17 | 18 | instruction.step_state_transition_in_same_context( 19 | opcode, 20 | rw_counter=Transition.delta(2), 21 | program_counter=Transition.delta(1), 22 | stack_pointer=Transition.same(), 23 | ) 24 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | make install 28 | - name: Lint 29 | run: make lint 30 | - name: Type check 31 | run: make type 32 | - name: Test with pytest 33 | run: make test 34 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/add_sub.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | 4 | 5 | def add_sub(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | 8 | is_sub, _ = instruction.pair_select(opcode, Opcode.SUB, Opcode.ADD) 9 | 10 | a = instruction.stack_pop() 11 | b = instruction.stack_pop() 12 | c = instruction.stack_push() 13 | 14 | instruction.constrain_equal_word( 15 | instruction.add_words([instruction.select_word(is_sub, c, a), b])[0], 16 | instruction.select_word(is_sub, a, c), 17 | ) 18 | 19 | instruction.step_state_transition_in_same_context( 20 | opcode, 21 | rw_counter=Transition.delta(3), 22 | program_counter=Transition.delta(1), 23 | stack_pointer=Transition.delta(1), 24 | ) 25 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/callvalue.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import CallContextFieldTag 3 | from ..opcode import Opcode 4 | 5 | 6 | def callvalue(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.CALLVALUE) 9 | 10 | # check [rw_table, call_context] table for call value and compare against 11 | # stack top after push. 12 | instruction.constrain_equal_word( 13 | instruction.call_context_lookup_word(CallContextFieldTag.Value), 14 | instruction.stack_push(), 15 | ) 16 | 17 | instruction.step_state_transition_in_same_context( 18 | opcode, 19 | rw_counter=Transition.delta(2), 20 | program_counter=Transition.delta(1), 21 | stack_pointer=Transition.delta(-1), 22 | ) 23 | -------------------------------------------------------------------------------- /specs/opcode/59MSIZE.md: -------------------------------------------------------------------------------- 1 | # MSIZE opcode 2 | 3 | ## Procedure 4 | 5 | The `MSIZE` opcode returns the memory size of the current execution and places it on the top of the stack. The memory size returned by the opcode is in bytes, while internally the memory size is stored in words, so the opcode needs to multiply the state variable by 32. 6 | 7 | ## Constraints 8 | 9 | 1. opId = OpcodeId(0x59) 10 | 2. state transition: 11 | - gc + 1 (1 stack write) 12 | - stack_pointer - 1 13 | - pc + 1 14 | - gas + 2 15 | 3. lookups: 1 busmapping lookup 16 | - `value` is pushed on top of the stack for `MSIZE` 17 | 18 | ## Exceptions 19 | 20 | 1. out of gas: the remaining gas is not enough 21 | 2. stack overflow: when stack is full, which means stack pointer is 0 before msize opcode 22 | 23 | ## Code 24 | 25 | Please refer to `src/zkevm_specs/evm/execution/msize.py`. 26 | -------------------------------------------------------------------------------- /specs/opcode/38CODESIZE.md: -------------------------------------------------------------------------------- 1 | # CODESIZE opcode 2 | 3 | ## Procedure 4 | 5 | ### EVM behaviour 6 | 7 | The `CODESIZE` opcode pushes the size of code running in the current environment to the top of the stack. 8 | 9 | ### Circuit behaviour 10 | 11 | 1. Lookup the code size from the bytecode table 12 | 2. Do busmapping lookup for stack write operation 13 | 14 | ## Constraints 15 | 16 | 1. opId = 0x38 17 | 2. State transition: 18 | - gc + 1 (1 stack write) 19 | - stack_pointer - 1 20 | - pc + 1 21 | - gas + 2 22 | 3. Lookups: 2 23 | - `codesize` (bytecode_length) from the bytecode table 24 | - `codesize` is on top of stack 25 | 26 | ## Exceptions 27 | 28 | 1. stack overflow: stack is full, stack pointer = 0 29 | 2. out of gas: remaining gas is not enough 30 | 31 | ## Code 32 | 33 | Please refer to [codesize.py](src/zkevm_specs/evm/execution/codesize.py). 34 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/address.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import CallContextFieldTag 3 | from ..opcode import Opcode 4 | 5 | 6 | def address(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.ADDRESS) 9 | 10 | address_word = instruction.call_context_lookup_word(CallContextFieldTag.CalleeAddress) 11 | # Get callee address from call context and compare with stack top after push. 12 | instruction.constrain_equal_word( 13 | address_word, 14 | instruction.stack_push(), 15 | ) 16 | 17 | instruction.step_state_transition_in_same_context( 18 | opcode, 19 | rw_counter=Transition.delta(2), 20 | program_counter=Transition.delta(1), 21 | stack_pointer=Transition.delta(-1), 22 | ) 23 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/origin.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | from ..table import CallContextFieldTag, TxContextFieldTag 4 | 5 | 6 | def origin(instruction: Instruction): 7 | tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) 8 | 9 | opcode = instruction.opcode_lookup(True) 10 | instruction.constrain_equal(opcode, Opcode.ORIGIN) 11 | 12 | address_word = instruction.tx_context_lookup_word(tx_id, TxContextFieldTag.CallerAddress) 13 | instruction.constrain_equal_word( 14 | address_word, 15 | instruction.stack_push(), 16 | ) 17 | 18 | instruction.step_state_transition_in_same_context( 19 | opcode, 20 | rw_counter=Transition.delta(2), 21 | program_counter=Transition.delta(1), 22 | stack_pointer=Transition.delta(-1), 23 | ) 24 | -------------------------------------------------------------------------------- /specs/opcode/50POP.md: -------------------------------------------------------------------------------- 1 | # POP opcode 2 | 3 | ## Procedure 4 | 5 | A stack initalize empty with stack pointer to 1024, pop operation can only happen when stack is not empty, and it will increase by 1 of stack pointer. 6 | 7 | Even the popped value will never be used, it still does lookup to ensure the stack pointer is in range, since we don't explicitly verify stack pointer is in range each step, we instead let state circuit to verify that. 8 | 9 | ## Constraints 10 | 11 | 1. opId = OpcodeId(0x50) 12 | 2. state transition: 13 | - gc + 1 14 | - stack_pointer + 1 15 | - pc + 1 16 | - gas + 2 17 | 3. Lookups: 1 busmapping lookups 18 | - A value is indeed at the top of the stack 19 | 20 | ## Exceptions 21 | 22 | 1. stack underflow: when stack is empty 23 | 2. gas out: remaining gas is not enough 24 | 25 | ## Code 26 | 27 | Please refer to `src/zkevm-specs/evm/execution/pop.py`. 28 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/error_oog_constant.py: -------------------------------------------------------------------------------- 1 | from ...util import FQ 2 | from ..instruction import Instruction, FixedTableTag 3 | from ..opcode import Opcode 4 | from ...util import N_BYTES_GAS 5 | 6 | 7 | def error_oog_constant(instruction: Instruction): 8 | # retrieve op code associated to oog constant error 9 | opcode = instruction.opcode_lookup(True) 10 | const_gas_entry = instruction.fixed_lookup( 11 | FixedTableTag.OpcodeConstantGas, opcode, FQ(Opcode(opcode.expr().n).constant_gas_cost()) 12 | ) 13 | 14 | # check gas left is less than const gas required 15 | gas_not_enough, _ = instruction.compare( 16 | instruction.curr.gas_left, const_gas_entry.value1, N_BYTES_GAS 17 | ) 18 | instruction.constrain_equal(gas_not_enough, FQ(1)) 19 | 20 | instruction.constrain_error_state(1 + instruction.curr.reversible_write_counter.n) 21 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/caller.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import CallContextFieldTag 3 | from ..opcode import Opcode 4 | 5 | 6 | def caller(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.CALLER) 9 | 10 | address_word = instruction.call_context_lookup_word(CallContextFieldTag.CallerAddress) 11 | # check [rw_table, call_context] table for caller address and compare with 12 | # stack top after push 13 | instruction.constrain_equal_word( 14 | address_word, 15 | instruction.stack_push(), 16 | ) 17 | 18 | instruction.step_state_transition_in_same_context( 19 | opcode, 20 | rw_counter=Transition.delta(2), 21 | program_counter=Transition.delta(1), 22 | stack_pointer=Transition.delta(-1), 23 | ) 24 | -------------------------------------------------------------------------------- /specs/opcode/19NOT.md: -------------------------------------------------------------------------------- 1 | # NOT opcode 2 | 3 | ## Procedure 4 | 5 | Pop one EVM word `a` from the stack and push output `b` onto the stack. 6 | 7 | We prove that `a = NOT b` by showing that the byte-wise relation 8 | `a[i] XOR b[i] == 255` holds for each of `i = 0..32`. 9 | 10 | ## Constraints 11 | 12 | 1. opcodeId checks 13 | - opId == OpcodeId(0x19) 14 | 2. state transition: 15 | - gc + 2 16 | - stack_pointer unchanged 17 | - pc + 1 18 | - gas + 3 19 | 3. Lookups: 2 busmapping lookups and 32 fixed table lookups 20 | - `a` is at the top of the stack 21 | - `b`, the result, is at the new top of the stack 22 | - Apply the lookup to 32 tuples of `a, b` chunks, `(a[i], b[i]), i = 0..32`. 23 | 24 | ## Exceptions 25 | 26 | 1. stack underflow: `stack_pointer = 1024` 27 | 2. out of gas: remaining gas is not enough 28 | 29 | ## Code 30 | 31 | See `src/zkevm_specs/evm/execution/bitwise.py` 32 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/calldatasize.py: -------------------------------------------------------------------------------- 1 | from ...util import Word 2 | from ..instruction import Instruction, Transition 3 | from ..table import CallContextFieldTag 4 | from ..opcode import Opcode 5 | 6 | 7 | def calldatasize(instruction: Instruction): 8 | opcode = instruction.opcode_lookup(True) 9 | instruction.constrain_equal(opcode, Opcode.CALLDATASIZE) 10 | 11 | # check [rw_table, call_context] table for call data length and compare 12 | # against stack top after push. 13 | instruction.constrain_equal_word( 14 | Word.from_lo(instruction.call_context_lookup(CallContextFieldTag.CallDataLength)), 15 | instruction.stack_push(), 16 | ) 17 | 18 | instruction.step_state_transition_in_same_context( 19 | opcode, 20 | rw_counter=Transition.delta(2), 21 | program_counter=Transition.delta(1), 22 | stack_pointer=Transition.delta(-1), 23 | ) 24 | -------------------------------------------------------------------------------- /specs/opcode/80DUPX.md: -------------------------------------------------------------------------------- 1 | # DUPX opcode 2 | 3 | ## Procedure 4 | 5 | dupx represents op codes of dup1....dup16. which picks up value at 'x' position inside the stack, then push the value to stack. 6 | 7 | ## Constraints 8 | 9 | 1. opId = OpcodeId(0x80...0x8F) 10 | 2. state transition: 11 | - gc + 2 (read + write) 12 | - stack_pointer - 1 13 | - pc + 1 14 | - gas + 3 15 | 3. lookups: 1 range lookup + 2 bussmapping lookups: 16 | - position 'x' range from \[1..16\] 17 | - operand must come from position 'x' inside 18 | - stack 19 | - position 'x' value equals stack top value when this operation done 20 | 21 | ## Exceptions 22 | 23 | 1. gas out: remaining gas is not enough 24 | 2. stack overflow: when stack is full, which means stack pointer is 0 before dupx 25 | 3. stack underflow: when stack length is less than dup position 'x' 26 | 27 | ## Code 28 | 29 | Please refer to `src/zkevm_specs/opcode/stack.py`. 30 | -------------------------------------------------------------------------------- /specs/error_state/ErrorInvalidJump.md: -------------------------------------------------------------------------------- 1 | # ErrorInvalidJump state 2 | 3 | ## Procedure 4 | 5 | This type of error only occurs when executing op code is `JUMP` or `JUMPI`. 6 | 7 | ### EVM behavior 8 | 9 | Pop one EVM word `dest` from the stack, then go to `ErrorInvalidJump` state when 10 | one of the followings occurs: 11 | 12 | - `dest` is not within code length range 13 | - `dest` is a not `JUMPDEST` code , or data section of PUSH* 14 | 15 | ### Constraints 16 | 17 | 1. `dest` is out of range or not`JUMPDEST` code 18 | 2. Current call must be failed. 19 | 3. If it's a root call, it transits to `EndTx` 20 | 4. If it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 21 | 22 | ### Lookups 23 | 24 | - Byte code lookup. 25 | - Call Context lookup `CallContextFieldTag.IsSuccess` 26 | 27 | ## Code 28 | 29 | Please refer to `src/zkevm_specs/evm/execution/error_Invalid_jump.py`. 30 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/gasprice.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | from ..table import CallContextFieldTag, TxContextFieldTag 4 | 5 | 6 | def gasprice(instruction: Instruction): 7 | tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) 8 | 9 | opcode = instruction.opcode_lookup(True) 10 | instruction.constrain_equal(opcode, Opcode.GASPRICE) 11 | 12 | # fetch gasPrice from rw table 13 | # fetch from the Tx context table the gasPrice 14 | instruction.constrain_equal_word( 15 | instruction.tx_context_lookup_word(tx_id, TxContextFieldTag.GasPrice), 16 | instruction.stack_push(), 17 | ) 18 | 19 | instruction.step_state_transition_in_same_context( 20 | opcode, 21 | rw_counter=Transition.delta(2), 22 | program_counter=Transition.delta(1), 23 | stack_pointer=Transition.delta(-1), 24 | ) 25 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/jump.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | 4 | 5 | def jump(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | instruction.constrain_equal(opcode, Opcode.JUMP) 8 | 9 | # Do not check 'dest' is within MaxCodeSize(24576) range in successful case 10 | # as byte code lookup can ensure it. 11 | dest_word = instruction.stack_pop() 12 | instruction.constrain_zero(dest_word.hi.expr()) 13 | dest = dest_word.lo.expr() 14 | 15 | # Verify `dest` is code within byte code table 16 | instruction.constrain_equal(Opcode.JUMPDEST, instruction.opcode_lookup_at(dest, True)) 17 | 18 | instruction.step_state_transition_in_same_context( 19 | opcode, 20 | rw_counter=Transition.delta(1), 21 | program_counter=Transition.to(dest), 22 | stack_pointer=Transition.delta(1), 23 | ) 24 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/selfbalance.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import AccountFieldTag, CallContextFieldTag 3 | from ..opcode import Opcode 4 | 5 | 6 | def selfbalance(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.SELFBALANCE) 9 | 10 | callee_address_word = instruction.call_context_lookup_word(CallContextFieldTag.CalleeAddress) 11 | callee_address = instruction.word_to_address(callee_address_word) 12 | balance = instruction.account_read_word(callee_address, AccountFieldTag.Balance) 13 | instruction.constrain_equal_word(instruction.stack_push(), balance) 14 | 15 | instruction.step_state_transition_in_same_context( 16 | opcode, 17 | rw_counter=Transition.delta(3), 18 | program_counter=Transition.delta(1), 19 | stack_pointer=Transition.delta(-1), 20 | ) 21 | -------------------------------------------------------------------------------- /specs/error_state/ErrorStackOverflow_Underflow.md: -------------------------------------------------------------------------------- 1 | # ErrorStackOverflow & underflow state 2 | 3 | ## Procedure 4 | ### EVM behavior 5 | 6 | stack overflow and underflow error can happen within any step which involves stack operation. 7 | each op code have fixed `min_stack_pointer` & `max_stack_pointer`. if current stack pointer < `min_stack_pointer`, 8 | overflow error happens, if current stack pointer > `max_stack_pointer`, underflow error happens. 9 | 10 | when any one type error occurs: 11 | 1. If it's a root call, it ends the execution. 12 | 2. Otherwise, it restores caller's context and switch to it. 13 | 14 | ### Circuit behavior 15 | 16 | 1. Do a bytecode lookup to get `opcode`. 17 | 2. Do a fixed lookup for `FixedTableTag.ResponsibleOpcode` with `opcode` and auxiliary `stack_pointer` to make sure it's indeed in pre-built pairs of `ErrorStack`. 18 | 19 | ## Code 20 | 21 | Please refer to `src/zkevm_specs/evm/execution/error_stack.py`. 22 | -------------------------------------------------------------------------------- /specs/opcode/46CHAINID.md: -------------------------------------------------------------------------------- 1 | # CHAINID opcode 2 | 3 | ## Procedure 4 | 5 | The `CHAINID` opcode gets the chain id where a transaction is meant executed at. 6 | 7 | ## EVM behaviour 8 | 9 | The `CHAINID` opcode loads a 256-bit value from the chain configuration (the chain id), and pushes it to the stack. 10 | 11 | ## Circuit behaviour 12 | 13 | 1. Construct Block Context table 14 | 2. Bus-mapping lookup for stack write operation 15 | 16 | ## Constraints 17 | 18 | 1. opId == 0x46 19 | 2. State transition: 20 | - gc + 1 (1 stack write) 21 | - `stack_pointer` - 1 22 | - pc + 1 23 | - gas + 2 24 | 3. Lookups: 25 | - `chainid` is on the top of stack 26 | - `chainid` is in the block context table 27 | 28 | ## Exceptions 29 | 30 | 1. Stack overflow: stack is full, stack pointer = 0 31 | 2. out of gas: remaining gas is not enough 32 | 33 | ## Code 34 | 35 | Please refer to [`block_ctx.py`](src/zkevm_specs/evm/execution/block_ctx.py) 36 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/bitwise.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | from ..table import FixedTableTag 4 | from ...util import FQ 5 | 6 | 7 | def bitwise(instruction: Instruction): 8 | opcode = instruction.opcode_lookup(True) 9 | 10 | a = instruction.stack_pop() 11 | b = instruction.stack_pop() 12 | c = instruction.stack_push() 13 | 14 | a8s = a.to_le_bytes() 15 | b8s = b.to_le_bytes() 16 | c8s = c.to_le_bytes() 17 | 18 | tag = FixedTableTag.BitwiseAnd + (opcode.n - Opcode.AND) 19 | 20 | for idx in range(32): 21 | instruction.fixed_lookup(FixedTableTag(tag), FQ(a8s[idx]), FQ(b8s[idx]), FQ(c8s[idx])) 22 | 23 | instruction.step_state_transition_in_same_context( 24 | opcode, 25 | rw_counter=Transition.delta(3), 26 | program_counter=Transition.delta(1), 27 | stack_pointer=Transition.delta(1), 28 | ) 29 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasLog.md: -------------------------------------------------------------------------------- 1 | # ErrorOutOfGasLog state 2 | 3 | ## Procedure 4 | ### EVM behavior 5 | use `LOGN` denotes [log0, .., log4]. `N` stands for `topic_count` of circuit, that is 6 | `N = topic_count = opcode - Opcode.LOG0`. 7 | For this gadget, the core is to calculate gas required as following: 8 | 9 | ` let gas_cost = GAS_COST_LOG 10 | + GAS_COST_LOG * (opcode - Opcode.LOG0) 11 | + GAS_COST_LOGDATA * msize 12 | + memory_expansion_gas` 13 | 14 | ### Constraints 15 | 1. stack reads `mstart`, `msize` for gas calculation 16 | 2. `gas_left < gas_cost`. 17 | 3. error common constraint: 18 | - current call must be failed. 19 | - rw_counter_end_of_reversion constraint 20 | - If it's a root call, it transits to `EndTx`. 21 | - if it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 22 | 23 | ## Code 24 | TODO: add python code after circuit merges! -------------------------------------------------------------------------------- /specs/opcode/47SELFBALANCE.md: -------------------------------------------------------------------------------- 1 | # SELFBALANCE opcode 2 | 3 | ## Procedure 4 | 5 | The `SELFBALANCE` opcode pushes the balance (32 bytes of data) of the currently executing address onto the stack. 6 | 7 | ## Circuit behaviour 8 | 9 | 1. Construct call context table in rw table 10 | 2. Do busmapping lookup for stack write operation 11 | 12 | ## Constraints 13 | 14 | 1. opId = 0x47 15 | 2. State transition: 16 | - gc + 3 (1 stack write, 1 account balance read, and 1 callee address read) 17 | - stack_pointer - 1 18 | - pc + 1 19 | - gas + 5 20 | 3. Lookups: 3 21 | - `callee_address` is the callee address of the call context 22 | - `self_balance` is the balance of `callee_address` 23 | - `self_balance` is on the top of stack 24 | 25 | ## Exceptions 26 | 27 | 1. stack overflow: stack is full, stack pointer = 0 28 | 2. out of gas: remaining gas is not enough 29 | 30 | ## Code 31 | 32 | Please refer to `src/zkevm_specs/evm/execution/selfbalance.py`. 33 | -------------------------------------------------------------------------------- /specs/opcode/5AGAS.md: -------------------------------------------------------------------------------- 1 | # GAS opcode 2 | 3 | ## Procedure 4 | 5 | The `GAS` opcode gets the amount of available gas, including the correspoding reduction for the cost of the instruction itself. 6 | 7 | ## EVM behaviour 8 | 9 | The `GAS` opcode loads a 64-bit value for the available gas from the current state, and pushes it to the stack. 10 | 11 | ## Circuit behaviour 12 | 13 | 1. bus-mapping lookup for stack write operation 14 | 2. compare against current state's gas left 15 | 16 | ## Constraints 17 | 18 | 1. opId == 0x5A 19 | 2. State transition: 20 | - gc + 1 (1 stack write) 21 | - `stack_pointer` - 1 22 | - pc + 1 23 | - gas + 2 24 | 3. Lookups: 25 | - `gas` is on the top of stack 26 | 4. Others: 27 | - Equality constraint: `gas == curr.gas_left - 2` 28 | 29 | ## Exceptions 30 | 31 | 1. Stack overflow: stack is full, stack pointer = 0 32 | 2. out of gas: remaining gas is not enough 33 | 34 | ## Code 35 | 36 | Please refer to [`gas.py`](src/zkevm_specs/evm/execution/gas.py) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zkevm Specifications 2 | 3 | [![Python package](https://github.com/privacy-scaling-explorations/zkevm-specs/actions/workflows/python-package.yml/badge.svg)](https://github.com/privacy-scaling-explorations/zkevm-specs/actions/workflows/python-package.yml) 4 | 5 | The project aims to define a validity snark proof for Ethereum transactions. 6 | 7 | ## The Written Specification 8 | 9 | We recommend the reader to start with [Introduction](./specs/introduction.md) 10 | 11 | ## Python Executable Specification 12 | 13 | We provide the [Beacon Chain](https://github.com/ethereum/eth2.0-specs) style Python executable specification to help implementors figure out the specified behavior. 14 | 15 | Installing dependencies(Python 3.9 is required) 16 | 17 | ``` 18 | make install 19 | ``` 20 | 21 | Run the tests 22 | 23 | ``` 24 | make test 25 | ``` 26 | 27 | ## Implementations 28 | 29 | See [privacy-scaling-explorations/zkevm-circuits](https://github.com/privacy-scaling-explorations/zkevm-circuits) 30 | -------------------------------------------------------------------------------- /specs/opcode/3aGASPRICE.md: -------------------------------------------------------------------------------- 1 | # GASPRICE opcode 2 | 3 | ## Procedure 4 | 5 | The `GASPRICE` opcode gets the price of gas in the current environment. 6 | 7 | ## EVM behaviour 8 | 9 | The `GASPRICE` opcode loads a 256-bit value from the Tx context (the gas price), and pushes it to the stack. 10 | 11 | ## Circuit behaviour 12 | 13 | 1. Construct Tx Context table 14 | 2. Bus-mapping lookup for stack write operation 15 | 16 | ## Constraints 17 | 18 | 1. opId == 0x3A 19 | 2. State transition: 20 | - gc + 2 (1 stack write + 1 Call Context read) 21 | - `stack_pointer` - 1 22 | - pc + 1 23 | - gas + 2 24 | 3. Lookups: 25 | - `tx_id` is on current call context 26 | - `gasprice` is on the top of stack 27 | - `gasprice` is in the transaction context table 28 | 29 | ## Exceptions 30 | 31 | 1. Stack overflow: stack is full, stack pointer = 0 32 | 2. out of gas: remaining gas is not enough 33 | 34 | ## Code 35 | 36 | Please refer to [`gasprice.py`](src/zkevm_specs/evm/execution/gasprice.py) 37 | -------------------------------------------------------------------------------- /specs/opcode/40BLOCKHASH.md: -------------------------------------------------------------------------------- 1 | # BLOCKHASH opcode 2 | 3 | ## Procedure 4 | 5 | The `BLOCKHASH` opcode pops `block_number` off the stack and pushes the hash from chosen block. If the chosen block number is not in valid range (last 256 blocks, not including current block), it will push 0 onto the stack. 6 | 7 | ## Constraints 8 | 9 | 1. opId == 0x40 10 | 2. State transition: 11 | - gc + 2 (1 stack read + 1 stack write) 12 | - stack_pointer + 0 (one pop and one push) 13 | - pc + 1 14 | - gas + 20 15 | 3. Lookups: 3 16 | - `block_number` is popped from the stack. 17 | - `current_block_number` is in the block context table. 18 | - `block_hash` is in the block context table. 19 | 4. Additional Constraint 20 | - `block_number` should be in valid range. 21 | 22 | ## Exceptions 23 | 24 | 1. stack underflow: if the stack starts empty 25 | 2. out of gas: remaining gas is not enough 26 | 27 | ## Code 28 | 29 | Please refer to [blockhash.py](src/zkevm-specs/evm/execution/blockhash.py). 30 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasConstant.md: -------------------------------------------------------------------------------- 1 | # ErrorOutOfGasConstant state 2 | 3 | ## Procedure 4 | 5 | For opcodes which have non-zero constant gas cost, this type of error occurs when gas left of 6 | current step is less than the required constant gas. 7 | 8 | ### EVM behavior 9 | 10 | 1. If it's a root call, it ends the execution. 11 | 2. Otherwise, it restores caller's context and switch to it. 12 | 13 | ### Constraints 14 | 15 | 1. `remaining_gas_left < step_constant_gas_required`. 16 | 2. Current call must be failed. 17 | 3. If it's a root call, it transits to `EndTx`, and the call's `IsPersistent` must be false 18 | 4. If it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 19 | 20 | ### Lookups 21 | 22 | - 1 Call Context lookup `CallContextFieldTag.IsSuccess`. 23 | - 1 Call Context lookup `CallContextFieldTag.IsPersistent` (Only if Current instruction is root.). 24 | 25 | ## Code 26 | 27 | Please refer to `src/zkevm_specs/evm/execution/oog_constant.py`. 28 | -------------------------------------------------------------------------------- /specs/opcode/3dRETURNDATASIZE.md: -------------------------------------------------------------------------------- 1 | # RETURNDATASIZE opcode 2 | 3 | ## Procedure 4 | 5 | ### EVM behaviour 6 | 7 | The `RETURNDATASIZE` opcode gets size of output data from the previous call from the current environment. 8 | 9 | ### Circuit behaviour 10 | 11 | 1. Construct call context table in rw table. 12 | 2. Do a busmapping lookup for CallContext last Call's ReturnDataLength read. 13 | 3. Do a busmapping lookup for a stack write operation corresponding to the RETURNDATASIZE result. 14 | 15 | ## Constraints 16 | 17 | 1. opId == 0x3D 18 | 2. State transition: 19 | - gc + 2 20 | - stack_pointer - 1 21 | - pc + 1 22 | - gas + 2 23 | 3. Lookups: 2 24 | - ReturnDataLength is in the rw table {call context, call ID, ReturnDataLength}. 25 | - ReturnDataLength is on top of stack. 26 | 27 | ## Exceptions 28 | 29 | 1. stack overflow: stack is full, stack pointer = 0 30 | 2. out of gas: remaining gas is not enough 31 | 32 | ## Code 33 | 34 | Please refer to [returndatasize.py](src/zkevm_specs/evm/execution/returndatasize.py). 35 | -------------------------------------------------------------------------------- /specs/opcode/1aBYTE.md: -------------------------------------------------------------------------------- 1 | # BYTE opcode 2 | 3 | ## Procedure 4 | 5 | The `BYTE` opcode retrieves a single byte from a value. This is done by popping two values, first the `index` and then the `value`, from the stack. The byte at position `index`, with the position starting at `0` at the MSB in `value`, will be copied to the LSB of `result`. `result` is then pushed on the stack. If `index >= 32` no byte will be copied and `result` will be `0`. 6 | 7 | ## Constraints 8 | 9 | 1. opId = OpcodeId(0x1a) 10 | 2. state transition: 11 | - gc + 3 (2 stack reads + 1 stack write) 12 | - stack_pointer + 1 13 | - pc + 1 14 | - gas + 3 15 | 3. lookups: 3 busmapping lookups 16 | - `index` is at the top of the stack 17 | - `value` is at the new top of the stack 18 | - `result` is at the new top of the stack 19 | 20 | ## Exceptions 21 | 22 | 1. stack underflow: the stack is empty or only contains a single value 23 | 2. out of gas: the remaining gas is not enough 24 | 25 | ## Code 26 | 27 | Please refer to `src/zkevm_specs/opcode/byte.py`. 28 | -------------------------------------------------------------------------------- /specs/opcode/30ADDRESS.md: -------------------------------------------------------------------------------- 1 | # ADDRESS opcode 2 | 3 | ## Procedure 4 | 5 | The `ADDRESS` opcode gets the address of currently executing account. 6 | 7 | ## EVM behaviour 8 | 9 | The `ADDRESS` opcode loads the callee address (20 bytes of data) from call 10 | context, then pushes this address to the stack. 11 | 12 | ## Circuit behaviour 13 | 14 | 1. Construct call context table in rw table 15 | 2. Do busmapping lookup for call context callee read operation 16 | 3. Do busmapping lookup for stack write operation 17 | 18 | ## Constraints 19 | 20 | 1. opId = 0x30 21 | 2. State transition: 22 | - gc + 2 (1 stack write, 1 call context read) 23 | - stack_pointer - 1 24 | - pc + 1 25 | - gas - 2 26 | 3. Lookups: 2 27 | - `address` is in the rw table {call context, call ID, callee} 28 | - `address` is on top of stack 29 | 30 | ## Exceptions 31 | 32 | 1. stack overflow: stack is full, stack pointer = 0 33 | 2. out of gas: remaining gas is not enough 34 | 35 | ## Code 36 | 37 | Please refer to `src/zkevm_specs/evm/execution/address.py`. 38 | -------------------------------------------------------------------------------- /specs/opcode/33CALLER.md: -------------------------------------------------------------------------------- 1 | # CALLER opcode 2 | 3 | ## Procedure 4 | 5 | The `CALLER` opcode gets the caller address (msg.caller) from the current call. 6 | 7 | ## EVM behaviour 8 | 9 | The `CALLER` opcode loads an `address` (20 bytes of data) from call context, 10 | then pushes the `address` to the stack. 11 | 12 | ## Circuit behaviour 13 | 14 | 1. Construct call context table in rw table 15 | 2. Do busmapping lookup for call context caller read operation 16 | 3. Do busmapping lookup for stack write operation 17 | 18 | ## Constraints 19 | 20 | 1. opId = 0x33 21 | 2. State transition: 22 | - gc + 2 (1 stack write, 1 call context read) 23 | - stack_pointer - 1 24 | - pc + 1 25 | - gas + 2 26 | 3. Lookups: 2 27 | - `address` is in the rw table {call context, call ID, caller} 28 | - `address` is on top of stack 29 | 30 | ## Exceptions 31 | 32 | 1. stack overflow: stack is full, stack pointer = 0 33 | 2. out of gas: remaining gas is not enough 34 | 35 | ## Code 36 | 37 | Please refer to `src/zkevm_specs/evm/execution/caller.py`. 38 | -------------------------------------------------------------------------------- /specs/opcode/34CALLVALUE.md: -------------------------------------------------------------------------------- 1 | # CALLVALUE opcode 2 | 3 | ## Procedure 4 | 5 | The `CALLVALUE` opcode gets the call value (msg.value) from the current call. 6 | 7 | ## EVM behaviour 8 | 9 | The `CALLVALUE` opcode loads a `word` (32 bytes of data) from call context -> 10 | call value, then pushes the `word` to the stack. 11 | 12 | ## Circuit behaviour 13 | 14 | 1. Construct call context table in rw table 15 | 2. Do busmapping lookup for call context call value read operation 16 | 3. Do busmapping lookup for stack write operation 17 | 18 | ## Constraints 19 | 20 | 1. opId = 0x34 21 | 2. State transition: 22 | - gc + 2 (1 stack write, 1 call context read) 23 | - stack_pointer - 1 24 | - pc + 1 25 | - gas + 2 26 | 3. Lookups: 2 27 | - `word` is in the rw table {call context, call ID, call value} 28 | - `word` is on top of stack 29 | 30 | ## Exceptions 31 | 32 | 1. stack overflow: stack is full, stack pointer = 0 33 | 2. out of gas: remaining gas is not enough 34 | 35 | ## Code 36 | 37 | Please refer to `src/zkevm_specs/evm/execution/callvalue.py`. 38 | -------------------------------------------------------------------------------- /specs/opcode/32ORIGIN.md: -------------------------------------------------------------------------------- 1 | # ORIGIN opcode 2 | 3 | ## Procedure 4 | 5 | The `ORIGIN` opcode gets the execution origination address (tx.origin). 6 | 7 | ## EVM behaviour 8 | 9 | The `ORIGIN` opcode loads the sender address (20 bytes) of the transaction that originated this execution, then pushes the address to the stack. 10 | 11 | ## Circuit behaviour 12 | 13 | 1. Lookup call context in rw table for the txID 14 | 2. Lookup txID in Tx Table for the CallerAddress 15 | 3. Lookup stack write operation for the address 16 | 17 | ## Constraints 18 | 19 | 1. opId = 0x32 20 | 2. State transition: 21 | - gc + 2 (1 stack write, 1 call context read) 22 | - stack_pointer - 1 23 | - pc + 1 24 | - gas + 2 25 | 3. Lookups: 3 26 | - `tx_id` is on current call context 27 | - `address` is in the Tx Table 28 | - `address` is on the top of stack 29 | 30 | ## Exceptions 31 | 32 | 1. stack overflow: stack is full, stack pointer = 0 33 | 2. out of gas: remaining gas is not enough 34 | 35 | ## Code 36 | 37 | Please refer to [`origin.py`](src/zkevm_specs/evm/execution/origin.py) 38 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/returndatasize.py: -------------------------------------------------------------------------------- 1 | from ..opcode import Opcode 2 | from ...util import Word, FQ 3 | from ..instruction import Instruction, Transition 4 | from ..table import CallContextFieldTag 5 | from ..opcode import Opcode 6 | 7 | 8 | def returndatasize(instruction: Instruction): 9 | opcode = instruction.opcode_lookup(True) 10 | instruction.constrain_equal(opcode, Opcode.RETURNDATASIZE) 11 | 12 | # check [rw_table, call_context] table for last call return data length and compare 13 | # against stack top after push. 14 | instruction.constrain_equal_word( 15 | Word( 16 | ( 17 | instruction.call_context_lookup(CallContextFieldTag.LastCalleeReturnDataLength), 18 | FQ(0), 19 | ) 20 | ), 21 | instruction.stack_push(), 22 | ) 23 | 24 | instruction.step_state_transition_in_same_context( 25 | opcode, 26 | rw_counter=Transition.delta(2), 27 | program_counter=Transition.delta(1), 28 | stack_pointer=Transition.delta(-1), 29 | ) 30 | -------------------------------------------------------------------------------- /specs/opcode/36CALLDATASIZE.md: -------------------------------------------------------------------------------- 1 | # CALLDATASIZE opcode 2 | 3 | ## Procedure 4 | 5 | The `CALLDATASIZE` opcode gets the call data size (msg.data.size) from the current call. 6 | 7 | ## EVM behaviour 8 | 9 | The `CALLDATASIZE` opcode loads a `u64` (5 bytes of data) from call context -> 10 | call data length, then pushes the `u64` to the stack. 11 | 12 | ## Circuit behaviour 13 | 14 | 1. Construct call context table in rw table 15 | 2. Do busmapping lookup for call context call data length read operation 16 | 3. Do busmapping lookup for stack write operation 17 | 18 | ## Constraints 19 | 20 | 1. opId = 0x36 21 | 2. State transition: 22 | - gc + 2 (1 stack write, 1 call context read) 23 | - stack_pointer - 1 24 | - pc + 1 25 | - gas + 2 26 | 3. Lookups: 2 27 | - `u64` is in the rw table {call context, call ID, call data length} 28 | - `u64` is on top of stack 29 | 30 | ## Exceptions 31 | 32 | 1. stack overflow: stack is full, stack pointer = 0 33 | 2. out of gas: remaining gas is not enough 34 | 35 | ## Code 36 | 37 | Please refer to `src/zkevm_specs/evm/execution/calldatasize.py`. 38 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasEXP.md: -------------------------------------------------------------------------------- 1 | # ErrorOutOfGasEXP state for EXP OOG error 2 | 3 | ## Procedure 4 | 5 | Handle the corresponding out of gas error for `EXP`. 6 | 7 | ### EVM behavior 8 | 9 | The EXP gas cost is calculated as: 10 | 11 | ``` 12 | # `exponent.bits()` returns the least number of bits needed to represent exponent. 13 | exponent_byte_size = (exponent.bits() + 7) // 8 14 | 15 | gas_cost = exponent_byte_size * 50 + 10 16 | ``` 17 | 18 | ### Constraints 19 | 20 | 1. Constrain `gas_left < gas_cost`. 21 | 2. Current call must fail. 22 | 3. If it's a root call, it transits to `EndTx`. 23 | 4. If it isn't a root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 24 | 5. Constrain `rw_counter_end_of_reversion = rw_counter_end_of_step + reversible_counter`. 25 | 26 | ### Lookups 27 | 28 | 4 basic bus-mapping lookups + restore context lookups (for non-root call): 29 | 30 | 1. 2 stack read for `base` and `exponent`. 31 | 2. 2 call context lookups for `is_success` and `rw_counter_end_of_reversion`. 32 | 3. Restore context lookups for non-root call. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Scroll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /specs/precompile/04dataCopy.md: -------------------------------------------------------------------------------- 1 | # dataCopy precompile 2 | 3 | ## Procedure 4 | 5 | The `dataCopy` precompile returns its input. 6 | 7 | ### Circuit behavior 8 | 9 | 1. Do a busmapping lookup for CallContext CalleeAddress. 10 | 2. Do a busmapping lookup for CallContext CallerId. 11 | 3. Do a busmapping lookup for CallContext CallDataOffset. 12 | 4. Do a busmapping lookup for CallContext CallDataLength. 13 | 5. Do a busmapping lookup for CallContext ReturnDataOffset. 14 | 6. Do a busmapping lookup for CallContext ReturnDataLength. 15 | 7. Do a CopyTable lookup to verify the copy from calldata to current call context memory. 16 | 8. Do a CopyTable lookup to verify the copy from calldata to precompile call context memory. 17 | 18 | ### Gas cost 19 | 20 | The gas cost of `dataCopy` precompile consists of two parts: 21 | 22 | 1. A constant gas cost: `15 gas` 23 | 2. A dynamic gas cost: cost of copying (variable depending on the `size` copied to memory) 24 | 25 | ## Constraints 26 | 27 | 1. prId == 0x04 28 | 2. state transition: 29 | - rw_counter + 5 + 2 * `copy_length` 30 | - gas + 15 + dynamic cost (copier cost) 31 | 32 | ## Code 33 | 34 | Please refer to `src/zkevm_specs/native/execution/dataCopy.py`. 35 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = zkevm-specs 3 | version = 0.0.1 4 | author = zkevm-team 5 | description = Zkevm specifications 6 | long_description = file: README.md 7 | long_description_content_type = text/markdown 8 | url = https://github.com/privacy-scaling-explorations/zkevm-specs/ 9 | project_urls = 10 | Bug Tracker = https://github.com/privacy-scaling-explorations/zkevm-specs/issues 11 | classifiers = 12 | Programming Language :: Python :: 3 13 | License :: OSI Approved :: MIT License 14 | Operating System :: OS Independent 15 | 16 | [options] 17 | package_dir = 18 | = src 19 | packages = find: 20 | python_requires = >=3.9 21 | install_requires = 22 | pycryptodome==3.14.1 23 | py-ecc==6.0.0 24 | eth-keys==0.4.0 25 | rlp==3.0.0 26 | eth-utils==2.0.0 27 | 28 | [options.packages.find] 29 | where = src 30 | 31 | [options.extras_require] 32 | test = 33 | pytest >= 7.0.0 34 | lint = 35 | black >= 22.3.0 36 | mypy >= 0.931 37 | flake8 >= 4.0.1 38 | 39 | [flake8] 40 | max-line-length = 100 41 | select = F401 42 | per-file-ignores = 43 | # imported but unused 44 | __init__.py: F401 45 | 46 | [mypy] 47 | exclude = ['build'] 48 | ignore_missing_imports = true 49 | -------------------------------------------------------------------------------- /specs/opcode/90SWAPX.md: -------------------------------------------------------------------------------- 1 | # SWAPX op code 2 | 3 | ## Procedure 4 | 5 | Swapx represents op codes of swap1....swap16. which swaps the top of the stack with the 'x+1'th last element. For example, SWAP3 means swaping the top of the stack with the 4th last element 6 | 7 | ## Constraints 8 | 9 | 1. opId = OpcodeId(0x90...0x9F) 10 | 2. state transition:\ 11 | gc + 4 (2 reads + 2 writes)\ 12 | stack_pointer\ 13 | pc + 1\ 14 | gas + 3 15 | 3. lookups: 1 range lookup + 4 bussmapping lookups:\ 16 | position 'x' range from \[1..16\]\ 17 | first operand must come from top of stack\ 18 | second operand must come from position 'x+1' inside stack\ 19 | when this operation done, the top stack value must equals 'x+1'th value before 20 | when this operation done, the 'x+1'th stack value must equals the top value before 21 | 22 | ## Exceptions 23 | 24 | 1. gas out:\ 25 | remaining gas is not enough 26 | 2. stack underflow:\ 27 | when stack length is less than swap position 'x + 1', i.e stack have 3 elements but the op is swap3, swap4, ..., swap16. Underflow condition for `swap_x`: `0 <= x + stack_pointer - 1024 <= 16`, which requires a `Range17` lookup check. 28 | 29 | ## Code 30 | 31 | refer to src/zkevm_specs/opcode/stack.py 32 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/jumpi.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | from zkevm_specs.util import FQ 4 | 5 | 6 | def jumpi(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | instruction.constrain_equal(opcode, Opcode.JUMPI) 9 | 10 | # Do not check 'dest' is within MaxCodeSize(24576) range in successful case 11 | # as byte code lookup can ensure it. 12 | dest_word = instruction.stack_pop() 13 | instruction.constrain_zero(dest_word.hi.expr()) 14 | dest = dest_word.lo.expr() 15 | 16 | cond = instruction.stack_pop() 17 | 18 | # check `cond` is zero or not 19 | if instruction.is_zero_word(cond): 20 | pc_diff = FQ(1) 21 | else: 22 | pc_diff = dest - instruction.curr.program_counter 23 | # assert Opcode.JUMPDEST == instruction.opcode_lookup_at(dest_value, True) 24 | instruction.constrain_equal(Opcode.JUMPDEST, instruction.opcode_lookup_at(dest, True)) 25 | 26 | instruction.step_state_transition_in_same_context( 27 | opcode, 28 | rw_counter=Transition.delta(2), 29 | program_counter=Transition.delta(pc_diff), 30 | stack_pointer=Transition.delta(2), 31 | ) 32 | -------------------------------------------------------------------------------- /specs/error_state/ErrorWriteProtection.md: -------------------------------------------------------------------------------- 1 | # ErrorWriteProtection state 2 | 3 | ## Procedure 4 | State modifying opcodes, which include `[SSTORE, CREATE, CREATE2, CALL, SELFDESTRUCT, LOG0, LOG1, LOG2, LOG3, LOG4]`, throw write protection errors when executed in a read-only call context (static call). See [EIP-214](https://eips.ethereum.org/EIPS/eip-214). 5 | 6 | ### EVM behavior 7 | A write protection error is thrown if all the following conditions are met: 8 | - State modifying opcodes run in a read-only context. 9 | - If the opcode is `CALL` and the `value` argument is non-zero. 10 | 11 | ### Constraints 12 | 1. constrain this error happens in one of op codes `[SSTORE, CREATE, CREATE2, CALL, SELFDESTRUCT, LOG0, LOG1, LOG2, LOG3, LOG4]` 13 | 2. current call context must be readonly (this call must be a internal call since it requires at least one `staticcall` ahead). 14 | 3. for `CALL` op code, do stack read & check `value` is not zero. 15 | 4. common error steps constraints: 16 | - current call must be failed. 17 | - rw_counter_end_of_reversion constraint 18 | - it restores caller's context by reading to `rw_table`, then does step state transition to it. 19 | 20 | ## Code 21 | Please refer to `src/zkevm_specs/evm/execution/error_write_protection.py`. -------------------------------------------------------------------------------- /specs/lookup.md: -------------------------------------------------------------------------------- 1 | # Plookup Table 2 | 3 | We use the [halo2 lookup table](https://zcash.github.io/halo2/design/proving-system/lookup.html) as a primitive to check if some adviced values are a row of a table. 4 | 5 | Example: 6 | 7 | This table has columns "a", "b", and "c". Where "a" and "b" are possible combinations of 0, 1, 2. "c" is the logic AND operations on "a" and "b". 8 | 9 | | a | b | c | 10 | | --- | --- | --- | 11 | | 0 | 0 | 0 | 12 | | 0 | 1 | 0 | 13 | | 0 | 2 | 0 | 14 | | 1 | 0 | 0 | 15 | | 1 | 1 | 1 | 16 | | 1 | 2 | 0 | 17 | | 2 | 0 | 0 | 18 | | 2 | 1 | 0 | 19 | | 2 | 2 | 2 | 20 | 21 | We can use the table to check the AND operation constraints on some variable x and y in the circuit by proving "x", "y", and "x & y" are cells of a row in the table. 22 | 23 | ## Fixed Table 24 | 25 | Rows of a fixed table are determined "before" proving time. 26 | 27 | The AND operation table is an example. 28 | 29 | ## Variable Table 30 | 31 | Rows of a variable table are determined "at" proving time. 32 | 33 | It allows prover to create their own table. An example would be the prover can witness a key-value mapping as a variable table. Note that we need extra check to guarantee the uniqueness of the mapping key. 34 | -------------------------------------------------------------------------------- /specs/opcode/16AND_17OR_18XOR.md: -------------------------------------------------------------------------------- 1 | # AND, OR, XOR opcodes 2 | 3 | ## Procedure 4 | 5 | Pop two EVM words `a` and `b` from the stack, and the output `c` is pushed to 6 | the stack. 7 | 8 | `a`, `b`, and `c` are all EVM words. We break three EVM words into 32 bytes and 9 | apply the lookup to the 32 chunks of `a`, `b`, and `c` to see if 10 | `a[i] OP b[i] == c[i]` holds for `i = 0..32`, where `OP` belongs to 11 | `[AND, OR, XOR]`. 12 | 13 | ## Constraints 14 | 15 | 1. opcodeId checks 16 | - opId == OpcodeId(0x16) for `AND` 17 | - opId == OpcodeId(0x17) for `OR` 18 | - opId == OpcodeId(0x18) for `XOR` 19 | 2. state transition: 20 | - gc + 3 21 | - stack_pointer + 1 22 | - pc + 1 23 | - gas + 3 24 | 3. Lookups: 35 busmapping lookups 25 | - `a` is at the top of the stack 26 | - `b` is at the second position of the stack 27 | - `c`, the result, is at the new top of the stack 28 | - Apply the lookup to 32 tuples of `a, b, c` chunks, 29 | `(a[i], b[i], c[i]), i = 0..32`, with opcode corresponding table 30 | (`BitwiseAnd`, `BitwiseOr`, and `BitwiseXor`). 31 | 32 | ## Exceptions 33 | 34 | 1. stack underflow: `1023 <= stack_pointer <= 1024` 35 | 2. out of gas: remaining gas is not enough 36 | 37 | ## Code 38 | 39 | See `src/zkevm_specs/evm/execution/bitwise.py` 40 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/extcodehash.py: -------------------------------------------------------------------------------- 1 | from ...util import EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, FQ 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | from ..table import AccountFieldTag, CallContextFieldTag 5 | 6 | 7 | def extcodehash(instruction: Instruction): 8 | opcode = instruction.opcode_lookup(True) 9 | instruction.constrain_equal(opcode, Opcode.EXTCODEHASH) 10 | 11 | address = instruction.word_to_address(instruction.stack_pop()) 12 | 13 | tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) 14 | is_warm = instruction.add_account_to_access_list(tx_id, address, instruction.reversion_info()) 15 | 16 | # We already define code_hash to be 0 when the account doesn't exist. 17 | code_hash = instruction.account_read_word(address, AccountFieldTag.CodeHash) 18 | 19 | instruction.constrain_equal_word( 20 | code_hash, 21 | instruction.stack_push(), 22 | ) 23 | 24 | instruction.step_state_transition_in_same_context( 25 | opcode, 26 | rw_counter=Transition.delta(7), 27 | program_counter=Transition.delta(1), 28 | stack_pointer=Transition.same(), 29 | dynamic_gas_cost=instruction.select(is_warm, FQ(0), FQ(EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS)), 30 | ) 31 | -------------------------------------------------------------------------------- /specs/opcode/56JUMP.md: -------------------------------------------------------------------------------- 1 | # JUMP opcode 2 | 3 | ## Procedure 4 | 5 | JUMP is an op code regarding flow control of evm. it takes the value of top stack 6 | to use as the destination, which changes program counter to it. 7 | 8 | ### EVM behavior 9 | 10 | Pop one EVM word `dest` from the stack. then do the followings: 11 | 12 | - check `dest` is within code length range 13 | - check `dest` is a `JUMPDEST` and not data section of PUSH\* 14 | - set program counter = `dest` 15 | 16 | ### Circuit behavior 17 | 18 | 1. construct byte code table with tuple (code_hash, index, bytecode, is_code) in each row, the table validity will be ensured by another byte code circuit. 19 | 2. basic constraints: 20 | 21 | - opId == OpcodeId(0x56) 22 | - gc + 1 (1 stack read) 23 | - pc = `dest` 24 | - stack_pointer + 1 25 | - gas + 8 26 | 27 | 3. Lookups: 1 busmapping lookups + 1 byte code lookup 28 | 29 | - lookup `dest` at the top of the stack 30 | - lookup `dest` position is `JUMPDEST` code in byte code table 31 | 32 | ## Exceptions 33 | 34 | 1. stack underflow: when stack is empty `stack_pointer = 1024` 35 | 2. out of gas: remaining gas is not enough 36 | 3. invalid jump (AKA ErrInvalidJump):\ 37 | the `dest` is not `JUMPDEST` or not code or out of range 38 | 39 | ## Code 40 | 41 | refer to src/zkevm_specs/evm/execution/jump.py 42 | -------------------------------------------------------------------------------- /specs/opcode/5BJUMPDEST.md: -------------------------------------------------------------------------------- 1 | # JUMPDEST opcode 2 | 3 | ## Procedure 4 | 5 | JUMPDEST is a special opcode which only marks an address that can be jumped to. In other words, it is metadata to annotate possible jump destinations. 6 | 7 | For example, in the below opcode sequences, JUMPDEST marks '004D' can be jumped to, if there is no JUMPDEST annotation, Jumping to '004D' will fail as jump opcode will require JUMPDEST as a valid destination. 8 | 9 | ``` 10 | 004D 5B JUMPDEST 11 | 004E 34 CALLVALUE 12 | 004F 80 DUP1 13 | 0050 15 ISZERO 14 | 0051 60 PUSH1 0x58 15 | 0053 57 *JUMPI 16 | 0054 60 PUSH1 0x00 17 | 0056 80 DUP1 18 | 0057 FD *REVERT 19 | 0058 5B JUMPDEST 20 | 0059 50 POP 21 | 005A 60 PUSH1 0x62 22 | 005C 60 PUSH1 0x04 23 | 005E 35 CALLDATALOAD 24 | 005F 60 PUSH1 0x86 25 | 0061 56 *JUMP" 26 | ``` 27 | 28 | ## Constraints 29 | 30 | 1. opId = OpcodeId(0x5B) 31 | 2. state transition: 32 | - gc 33 | - stack_pointer 34 | - pc + 1 35 | - gas + 1 36 | 3. lookups: 37 | none (since there is no operands required) 38 | 39 | ## Exceptions 40 | 41 | 1. gas out: remaining gas is not enough 42 | 43 | ## Code 44 | 45 | none 46 | -------------------------------------------------------------------------------- /specs/opcode/01ADD_03SUB.md: -------------------------------------------------------------------------------- 1 | # ADD and SUB opcodes 2 | 3 | ## Procedure 4 | 5 | ### EVM behavior 6 | 7 | Pop two EVM words `a` and `b` from the stack. Compute 8 | 9 | - if it's `ADD` opcode, compute `c = (a + b) % 2**256`, and push `c` back to the stack 10 | - if it's `SUB` opcode, compute `c = (a - b) % 2**256`, and push `c` back to the stack 11 | 12 | ### Circuit behavior 13 | 14 | The AddGadget takes argument of `a: [u8;32]`, `x: [u8;32]`, `y: [u8;32]`, `is_sub: bool`. 15 | 16 | It always computes `y = (a + x) % 2**256`, 17 | 18 | - when it's ADD (`is_sub == False`), we annotate stack as \[a, x, ...\] and \[y, ...\], 19 | - when it's SUB (`is_sub == True`), we annotate stack as \[a, y, ...\] and \[x, ...\]. 20 | 21 | ## Constraints 22 | 23 | 1. opcodeId checks 24 | 1. opId === OpcodeId(0x01) for `ADD` 25 | 2. opId === OpcodeId(0x03) for `SUB` 26 | 2. state transition: 27 | - gc + 3 28 | - stack_pointer + 1 29 | - pc + 1 30 | - gas + 3 31 | 3. Lookups: 3 busmapping lookups 32 | - `a` is at the top of the stack 33 | - `b` is at the second position of the stack 34 | - `c`, the result, is at the new top of the stack 35 | 36 | ## Exceptions 37 | 38 | 1. stack underflow: `1023 <= stack_pointer <= 1024` 39 | 2. out of gas: remaining gas is not enough 40 | 41 | ## Code 42 | 43 | See `src/zkevm_specs/opcode/add_sub.py` 44 | -------------------------------------------------------------------------------- /specs/introduction.md: -------------------------------------------------------------------------------- 1 | # ZKEVM Introduction 2 | 3 | Currently, every Ethereum node must validate every transaction in the Ethereum virtual machine. This means that every transaction adds work everyone must do to verify Ethereum's history. Worse still, each transaction needs to be verified by every new node. This makes the work required for each new node to sync to the network grow constantly. We want to build a proof of validity for the Ethereum blocks to avoid this. There are two goals: 4 | 1. Make a zkrollup that supports smart contracts 5 | 2. Create a proof of validity for every Ethereum block 6 | 7 | This means making a proof of validity for EVM + state reads / writes + signatures. 8 | 9 | To simplify we separate our proofs into two components: 10 | 1. **[State proof](./state-proof.md)**: State/memory/stack ops have been performed correctly. This does not check if the correct location has been read/written. We allow our prover to pick any location here and in the EVM proof confirm it is correct. 11 | 2. **[EVM proof](./evm-proof.md)**: This checks that the correct opcode is called at the correct time. It checks the validity of these opcodes and confirms that each of these opcodes and the state proof both performed the correct operations. 12 | 13 | Only after verifying both proofs are valid, we have confidence that Ethereum block is executed correctly. 14 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/comparator.py: -------------------------------------------------------------------------------- 1 | from ...util import FQ, Word 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | 5 | 6 | def cmp(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | is_eq = instruction.is_equal(opcode, Opcode.EQ) 10 | is_gt = instruction.is_equal(opcode, Opcode.GT) 11 | 12 | a = instruction.stack_pop() 13 | b = instruction.stack_pop() 14 | c = instruction.stack_push() 15 | 16 | # swap a and b if the opcode is GT 17 | (aa, bb) = (b, a) if is_gt == 1 else (a, b) 18 | 19 | a_lo, a_hi = aa.to_lo_hi() 20 | b_lo, b_hi = bb.to_lo_hi() 21 | 22 | # `a[0..16] <= b[0..16]` 23 | lt_lo, eq_lo = instruction.compare(a_lo, b_lo, 16) 24 | 25 | # `a[16..32] <= b[16..32]` 26 | lt_hi, eq_hi = instruction.compare(a_hi, b_hi, 16) 27 | 28 | lt = instruction.select(lt_hi, FQ(1), eq_hi * lt_lo) 29 | eq = eq_lo * eq_hi 30 | 31 | result = eq if is_eq == 1 else lt 32 | 33 | instruction.constrain_equal_word( 34 | Word.from_lo(FQ(result)), 35 | c, 36 | ) 37 | 38 | instruction.step_state_transition_in_same_context( 39 | opcode, 40 | rw_counter=Transition.delta(3), 41 | program_counter=Transition.delta(1), 42 | stack_pointer=Transition.delta(1), 43 | ) 44 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/block_ctx.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import BlockContextFieldTag 3 | from ..opcode import Opcode 4 | 5 | 6 | def blockctx(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | # get block context op element 10 | if opcode == Opcode.COINBASE: 11 | op = BlockContextFieldTag.Coinbase 12 | elif opcode == Opcode.TIMESTAMP: 13 | op = BlockContextFieldTag.Timestamp 14 | elif opcode == Opcode.NUMBER: 15 | op = BlockContextFieldTag.Number 16 | elif opcode == Opcode.GASLIMIT: 17 | op = BlockContextFieldTag.GasLimit 18 | elif opcode == Opcode.DIFFICULTY: 19 | op = BlockContextFieldTag.Difficulty 20 | elif opcode == Opcode.BASEFEE: 21 | op = BlockContextFieldTag.BaseFee 22 | elif opcode == Opcode.CHAINID: 23 | op = BlockContextFieldTag.ChainId 24 | ctx_word = instruction.block_context_lookup_word(op) 25 | 26 | # check block table for corresponding op data 27 | instruction.constrain_equal_word(ctx_word, instruction.stack_push()) 28 | 29 | instruction.step_state_transition_in_same_context( 30 | opcode, 31 | rw_counter=Transition.delta(1), 32 | program_counter=Transition.delta(1), 33 | stack_pointer=Transition.delta(-1), 34 | ) 35 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/byte.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ...util import FQ, Word 3 | 4 | 5 | def byte(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | 8 | a = instruction.stack_pop() 9 | b = instruction.stack_pop() 10 | c = instruction.stack_push() 11 | 12 | index = a.to_le_bytes() 13 | value = b.to_le_bytes() 14 | 15 | # Any index >= 32 always returns all zeros 16 | is_msb_sum_zero = instruction.is_zero(FQ(sum(index[1:]))) 17 | 18 | # Byte 0: 19 | # Check byte per byte if we need to copy the value 20 | # to result. We're only directly checking the LSB byte 21 | # of the index here, so also make sure the byte 22 | # is only copied when index < 32. 23 | is_byte_selected = [instruction.is_equal(FQ(index[0]), FQ(31 - idx)) for idx in range(32)] 24 | 25 | selected_byte = FQ(0) 26 | for cell, is_selected in zip(value, is_byte_selected): 27 | selected_byte += is_selected * is_msb_sum_zero * FQ(cell) 28 | 29 | instruction.constrain_equal_word( 30 | Word.from_lo(selected_byte), 31 | c, 32 | ) 33 | 34 | instruction.step_state_transition_in_same_context( 35 | opcode, 36 | rw_counter=Transition.delta(3), 37 | program_counter=Transition.delta(1), 38 | stack_pointer=Transition.delta(1), 39 | ) 40 | -------------------------------------------------------------------------------- /specs/opcode/0bSIGNEXTEND.md: -------------------------------------------------------------------------------- 1 | # SIGNEXTEND opcode 2 | 3 | ## Procedure 4 | 5 | The `SIGNEXTEND` opcode extends the length of a signed integer. This is done by popping two values, first the `index` and then the `value`, from the stack. The byte at position `index`, with the position starting at `0` at the LSB in `value`, will be used to read the sign from (with the sign being the most significant bit). All bytes following the selected byte (`> index`) will be replaced by a value depending on this sign: 6 | 7 | - Sign is `0`: all bytes `> index` will be replaced with `0x00` 8 | - Sign is `1`: all bytes `> index` will be replaced with `0xFF` 9 | 10 | `result` is then pushed on the stack. If `index >= 31` no bytes will change and `result == value`. 11 | 12 | ## Constraints 13 | 14 | 1. opId = OpcodeId(0x0b) 15 | 2. state transition: 16 | - gc + 3 (2 stack reads + 1 stack write) 17 | - stack_pointer + 1 18 | - pc + 1 19 | - gas + 5 20 | 3. lookups: 1 fixed lookup + 3 busmapping lookups 21 | - The sign from the selected byte 22 | - `index` is at the top of the stack 23 | - `value` is at the new top of the stack 24 | - `result` is at the new top of the stack 25 | 26 | ## Exceptions 27 | 28 | 1. stack underflow: the stack is empty or only contains a single value 29 | 2. out of gas: the remaining gas is not enough 30 | 31 | ## Code 32 | 33 | Please refer to `src/zkevm_specs/opcode/signextend.py`. 34 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasCall.md: -------------------------------------------------------------------------------- 1 | # ErrorOutOfGasCall state 2 | 3 | ## Procedure 4 | 5 | Handle the corresponding out of gas errors for `CALL`, `CALLCODE`, `DELEGATECALL` and `STATICCALL` opcodes. 6 | 7 | ### EVM behavior 8 | 9 | For this gadget, the core is to calculate gas required, there are multiple kinds of gas 10 | consumes in above call related opcodes: 11 | 1. memory expansion gas cost 12 | 2. gas cost if new account creates 13 | 3. transfer fee if has value 14 | 4. account access list cost (warm/cold) 15 | 16 | below is the total gas cost calculation which from [Spec of call related opcodes](../opcode/F1CALL_F4DELEGATECALL_FASTATICCALL.md). 17 | ``` 18 | GAS_COST_WARM_ACCESS := 100 19 | GAS_COST_ACCOUNT_COLD_ACCESS := 2600 20 | GAS_COST_CALL_EMPTY_ACCOUNT := 25000 21 | GAS_COST_CALL_WITH_VALUE := 9000 22 | gas_cost = ( 23 | GAS_COST_WARM_ACCESS if is_warm_access else GAS_COST_ACCOUNT_COLD_ACCESS 24 | + has_value * (GAS_COST_CALL_WITH_VALUE + is_account_empty * GAS_COST_CALL_EMPTY_ACCOUNT) 25 | + memory_expansion_gas_cost 26 | ) 27 | ``` 28 | 29 | ### Constraints 30 | 31 | 1. `gas_left < gas_cost`. 32 | 2. Current call must be failed. 33 | 3. If it's a root call, it transits to `EndTx`. 34 | 4. If it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 35 | 36 | ## Code 37 | 38 | Please refer to `src/zkevm_specs/evm/execution/oog_call.py`. 39 | -------------------------------------------------------------------------------- /specs/opcode/57JUMPI.md: -------------------------------------------------------------------------------- 1 | # JUMPI opcode 2 | 3 | ## Procedure 4 | 5 | JUMPI is an op code regarding flow control of evm. it pops two values at the top of the stack and conditionally changes program counter . 6 | 7 | ### EVM behavior 8 | 9 | Pop two EVM words `dest` and `cond` from the stack. then do the followings: 10 | 11 | 1. check `cond` is zero 12 | 13 | - set pc = pc + 1 14 | 15 | 2. check `cond` is not zero 16 | 17 | - check `dest` is within code length range 18 | - check `dest` is a JUMPDEST 19 | - check `dest` is not data section of PUSH\* 20 | - set pc = `dest` 21 | 22 | ### Circuit behavior 23 | 24 | 1. Construct byte code table with tuple of (code_hash, index, bytecode, is_code) in each row, the table validity will be ensured by another byte code circuit. 25 | 2. basic constraints: 26 | 27 | - opId === OpcodeId(0x57) 28 | - gc + 2 (2 stack read) 29 | - pc = pc + 1 if `cond` is zero else `dest` 30 | - stack_pointer + 2 31 | - gas + 10 32 | 33 | 3. Lookups: 34 | 35 | - lookup `dest` and `cond` are the top two of the stack 36 | - Conditional lookup `dest` position is `JUMPDEST` code in byte code table 37 | when `cond` is not zero 38 | 39 | ## Exceptions 40 | 41 | 1. stack underflow: when stack is empty ` stack_pointer in [1023,1024]` 42 | 2. out of gas: remaining gas is not enough 43 | 3. invalid jump (AKA ErrInvalidJump):\ 44 | the `dest` is not JUMPDEST or not code or out of range 45 | 46 | ## Code 47 | 48 | Refer to src/zkevm_specs/evm/execution/jumpi.py 49 | -------------------------------------------------------------------------------- /specs/opcode/3fEXTCODEHASH.md: -------------------------------------------------------------------------------- 1 | # EXTCODEHASH opcode 2 | 3 | ## Procedure 4 | 5 | The `EXTCODEHASH` opcode pops `address` off the stack and pushes the code hash 6 | of the corresponding account onto the stack. If the corresponding account 7 | doesn't exist then it will push 0 onto the stack instead. Since we use 8 | `code_hash = 0` to encode non-existing accounts, we can use the lookup result 9 | directly. 10 | 11 | ## Constraints 12 | 13 | 1. opId = 0x3f 14 | 2. State transition: 15 | - gc + 7 (1 stack read, 1 stack write, 3 call context reads, 1 account read, 16 | 1 transaction access list write) 17 | - stack_pointer + 0 (one pop and one push) 18 | - pc + 1 19 | - gas: 20 | - the accessed `address` is warm: WARM_STORAGE_READ_COST 21 | - the accessed `address` is cold: COLD_ACCOUNT_ACCESS_COST 22 | 3. Lookups: 7 busmapping lookups 23 | - `address` is popped from the stack 24 | - 3 from call context for `tx_id`, `rw_counter_end_of_reversion`, and 25 | `is_persistent`. 26 | - `address` is added to the transaction access list if not already present. 27 | - lookup account `code_hash`. 28 | - The EXTCODEHASH result is at the new top of the stack. 29 | 4. Additional Constraints 30 | - value `is_warm` matches the gas cost for this opcode. 31 | 32 | ## Exceptions 33 | 34 | 1. stack underflow: if the stack starts empty 35 | 2. out of gas: remaining gas is not enough 36 | 37 | ## Code 38 | 39 | Please refer to `src/zkevm_specs/evm/execution/extcodehash.py`. 40 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/balance.py: -------------------------------------------------------------------------------- 1 | from ...util import EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, FQ, Word 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | from ..table import AccountFieldTag, CallContextFieldTag 5 | 6 | 7 | def balance(instruction: Instruction): 8 | opcode = instruction.opcode_lookup(True) 9 | instruction.constrain_equal(opcode, Opcode.BALANCE) 10 | 11 | address = instruction.word_to_address(instruction.stack_pop()) 12 | 13 | tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) 14 | is_warm = instruction.add_account_to_access_list(tx_id, address, instruction.reversion_info()) 15 | 16 | # Check account existence with code_hash != 0 17 | exists = FQ(1) - instruction.is_zero_word( 18 | instruction.account_read_word(address, AccountFieldTag.CodeHash) 19 | ) 20 | 21 | balance = ( 22 | instruction.account_read_word(address, AccountFieldTag.Balance) if exists == 1 else Word(0) 23 | ) 24 | 25 | instruction.constrain_equal_word( 26 | instruction.select_word(exists, balance, Word(0)), 27 | instruction.stack_push(), 28 | ) 29 | 30 | instruction.step_state_transition_in_same_context( 31 | opcode, 32 | rw_counter=Transition.delta(7 + exists.n), 33 | program_counter=Transition.delta(1), 34 | stack_pointer=Transition.same(), 35 | dynamic_gas_cost=instruction.select(is_warm, FQ(0), FQ(EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS)), 36 | ) 37 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/push.py: -------------------------------------------------------------------------------- 1 | from ...util import N_BYTES_PROGRAM_COUNTER 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | 5 | 6 | def push(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | num_pushed = opcode - Opcode.PUSH1 + 1 9 | code_length = instruction.bytecode_length(instruction.curr.code_hash) 10 | code_length_left = code_length - instruction.curr.program_counter - 1 11 | is_out_of_bound, _ = instruction.compare(code_length_left, num_pushed, N_BYTES_PROGRAM_COUNTER) 12 | num_padding = is_out_of_bound * (num_pushed - code_length_left) 13 | 14 | value = instruction.stack_push() 15 | value_le_bytes = value.to_le_bytes() 16 | is_pushed = instruction.continuous_selectors(num_pushed, 32) 17 | is_padding = instruction.continuous_selectors(num_padding, 32) 18 | 19 | for idx in range(32): 20 | index = instruction.curr.program_counter + num_pushed - idx 21 | if is_pushed[idx] * (1 - is_padding[idx]) == 1: 22 | instruction.constrain_equal( 23 | value_le_bytes[idx], instruction.opcode_lookup_at(index, False) 24 | ) 25 | else: 26 | instruction.constrain_zero(value_le_bytes[idx]) 27 | 28 | instruction.step_state_transition_in_same_context( 29 | opcode, 30 | rw_counter=Transition.delta(1), 31 | program_counter=Transition.delta(1 + num_pushed), 32 | stack_pointer=Transition.delta(-1), 33 | ) 34 | -------------------------------------------------------------------------------- /specs/opcode/08ADDMOD.md: -------------------------------------------------------------------------------- 1 | # ADDMOD opcode 2 | 3 | ## Procedure 4 | 5 | ### EVM behavior 6 | 7 | Pop three EVM words `a`, `b` and `N` from the stack. 8 | 9 | If n is zero 10 | push 0 into the stack 11 | else 12 | compute `r = (a + b) mod N`, and push `r` into to the stack 13 | 14 | ### Circuit behavior 15 | 16 | The AddModGadget takes argument of `a: [u8;32]`, `b: [u8;32]`, `N: [u8;32]` and keeps a cell for storing `d :[u8;32]` 17 | 18 | - Witness `(a_reduced,k) ← (a % N, a // N)` 19 | - Witness `(r,d) ← ( (a_reduced + b) % N, (a_reduced + b ) // N)` 20 | - Check `a == a_reduced + k * N` (a_reduced + k * N should not overflow 256 bits) 21 | - Check `a_reduced + b == d * N + r` in 512bit space (1) 22 | - Check `r `r==0`, if n is zero 26 | 27 | - witness `r ← (a_reduced + b) % 2^256` to satisfy &1 28 | - deactivate &2 29 | 30 | ## Constraints 31 | 32 | 1. opcodeId checks 33 | opId === OpcodeId(0x08) 34 | 2. state transition: 35 | - gc + 4 36 | - stack_pointer + 2 37 | - pc + 1 38 | - gas + 8 39 | 3. Lookups: 4 busmapping lookups 40 | - `a` is at the top of the stack 41 | - `b` is at the second position of the stack 42 | - `n` is at the third position of the stack 43 | - `r`, the result, is at the new top of the stack 44 | 45 | ## Exceptions 46 | 47 | 1. stack underflow: `1022 <= stack_pointer <= 1024` 48 | 2. out of gas: remaining gas is not enough 49 | 50 | ## Code 51 | 52 | See `src/zkevm_specs/opcode/addmod.py` 53 | -------------------------------------------------------------------------------- /tests/evm/test_codesize.py: -------------------------------------------------------------------------------- 1 | from zkevm_specs.evm_circuit import ( 2 | Bytecode, 3 | ExecutionState, 4 | RWDictionary, 5 | StepState, 6 | Tables, 7 | verify_steps, 8 | ) 9 | from zkevm_specs.util import Word 10 | 11 | 12 | def test_codesize(): 13 | bytecode = Bytecode().codesize().stop() 14 | codesize = len(bytecode.code) 15 | bytecode_hash = Word(bytecode.hash()) 16 | 17 | tables = Tables( 18 | block_table=set(), 19 | tx_table=set(), 20 | bytecode_table=set(bytecode.table_assignments()), 21 | rw_table=set(RWDictionary(9).stack_write(1, 1023, Word(codesize)).rws), 22 | ) 23 | 24 | verify_steps( 25 | tables=tables, 26 | steps=[ 27 | StepState( 28 | execution_state=ExecutionState.CODESIZE, 29 | rw_counter=9, 30 | call_id=1, 31 | is_root=True, 32 | is_create=False, 33 | code_hash=bytecode_hash, 34 | program_counter=0, 35 | stack_pointer=1024, 36 | gas_left=2, 37 | ), 38 | StepState( 39 | execution_state=ExecutionState.STOP, 40 | rw_counter=10, 41 | call_id=1, 42 | is_root=True, 43 | is_create=False, 44 | code_hash=bytecode_hash, 45 | program_counter=1, 46 | stack_pointer=1023, 47 | gas_left=0, 48 | ), 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/blockhash.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import BlockContextFieldTag 3 | from ...util import FQ, N_BYTES_U64, WordOrValue 4 | 5 | 6 | def blockhash(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | block_number = instruction.word_to_u64(instruction.stack_pop()) 10 | 11 | current_block_number = instruction.block_context_lookup(BlockContextFieldTag.Number) 12 | # get value that was pushed to stack (RLC-encoded) 13 | block_hash = instruction.stack_push() 14 | 15 | # comparing block_number and current_block_number 16 | block_lt, _ = instruction.compare(block_number, current_block_number, N_BYTES_U64) 17 | diff_lt, _ = instruction.compare(current_block_number, FQ(256) + block_number, 2) 18 | 19 | # get the expected block hash depending on the above conditions (RLC-encoded) 20 | if instruction.is_equal(block_lt * diff_lt, FQ.one()) == FQ.one(): 21 | expected_block_hash = instruction.block_context_lookup_word( 22 | BlockContextFieldTag.HistoryHash, 23 | block_number, 24 | ) 25 | else: 26 | expected_block_hash = WordOrValue(FQ(0)) 27 | 28 | # block hash is as expected 29 | instruction.constrain_equal_word(block_hash, expected_block_hash) 30 | 31 | instruction.step_state_transition_in_same_context( 32 | opcode, 33 | rw_counter=Transition.delta(2), 34 | program_counter=Transition.delta(1), 35 | stack_pointer=Transition.same(), 36 | ) 37 | -------------------------------------------------------------------------------- /specs/opcode/10LT_11GT_14EQ.md: -------------------------------------------------------------------------------- 1 | # LT & GT & EQ opcodes 2 | 3 | ## Procedure 4 | 5 | The `LT`, `GT` and `EQ` opcodes compare the top two values on the stack, and push the 6 | result (0 or 1) back to the stack. 7 | 8 | We provide extra witness `c8s = b8s - a8s` to check if `a8s < b8s`. 9 | The circuit needs to check two constraints for `LT` opcode. 10 | 11 | 1. When result is 1, `c8s` cannot be equal to 0, or equivalently the sum of all limbs in `c8s` cannot be equal to 0. 12 | 13 | 2. we check if `a8s + c8s` equals to `b8s` or `b8s + 2^256`. 14 | For the former case, the result is 1 as `a8s < b8s`; while for the latter case, the result is 0 as the sum of `a8s` and `c8s` overflows when `a8s > b8s`. 15 | 16 | For `GT` opcode, we can simply swap the order of `a8s` and `b8s` and re-use the `LT` circuit. 17 | 18 | ## Constraints 19 | 20 | 1. opId = OpcodeId(0x10 | 0x11 | 0x14) 21 | 2. Constraints: 22 | - `sum(c8s) != 0` when `result = 1` 23 | - `a[15:0] + c[15:0] = b[15:0] + carry * 2^128` 24 | - `a[31:16] + c[31:16] + carry = b[31:16] + (1 - result[0]) * (sum(c8s) == 0) * 2^128` 25 | 3. state transition: 26 | - gc + 3 (2 stack reads + 1 stack write) 27 | - stack_pointer + 1 28 | - pc + 1 29 | - gas + 3 30 | 4. lookups: 3 busmapping lookups 31 | - `a` is at the top of the stack 32 | - `b` is at the new top of the stack 33 | - `result` is at the new top of the stack 34 | 35 | ## Exceptions 36 | 37 | 1. stack underflow: the stack is empty or only contains one element 38 | 2. out of gas: the remaining gas is not enough 39 | 40 | ## Code 41 | 42 | Please refer to `src/zkevm_specs/opcode/lt_gt.py`. 43 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/extcodesize.py: -------------------------------------------------------------------------------- 1 | from ...util import ( 2 | EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS, 3 | FQ, 4 | Word, 5 | ) 6 | from ..instruction import Instruction, Transition 7 | from ..opcode import Opcode 8 | from ..table import AccountFieldTag, CallContextFieldTag 9 | 10 | 11 | def extcodesize(instruction: Instruction): 12 | opcode = instruction.opcode_lookup(True) 13 | instruction.constrain_equal(opcode, Opcode.EXTCODESIZE) 14 | 15 | address = instruction.word_to_address(instruction.stack_pop()) 16 | 17 | tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) 18 | is_warm = instruction.add_account_to_access_list(tx_id, address, instruction.reversion_info()) 19 | 20 | code_hash = instruction.account_read_word(address, AccountFieldTag.CodeHash) 21 | # Check account existence with code_hash != 0 22 | exists = FQ(1) - instruction.is_zero_word(code_hash) 23 | 24 | if exists == 1: 25 | code_size = instruction.bytecode_length(code_hash) 26 | else: # exists == 0 27 | code_size = FQ(0) 28 | 29 | instruction.constrain_equal_word( 30 | Word.from_lo(instruction.select(exists, code_size, FQ(0))), 31 | instruction.stack_push(), 32 | ) 33 | 34 | instruction.step_state_transition_in_same_context( 35 | opcode, 36 | rw_counter=Transition.delta(7), 37 | program_counter=Transition.delta(1), 38 | stack_pointer=Transition.same(), 39 | dynamic_gas_cost=instruction.select(is_warm, FQ(0), FQ(EXTRA_GAS_COST_ACCOUNT_COLD_ACCESS)), 40 | reversible_write_counter=Transition.delta(1), 41 | ) 42 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/error_invalid_jump.py: -------------------------------------------------------------------------------- 1 | from ...util import FQ 2 | from ..instruction import Instruction 3 | from ..opcode import Opcode 4 | from ...util import N_BYTES_PROGRAM_COUNTER 5 | 6 | 7 | def error_invalid_jump(instruction: Instruction): 8 | opcode = instruction.opcode_lookup(True) 9 | # current executing op code must be JUMP or JUMPI 10 | instruction.constrain_in(opcode, [FQ(Opcode.JUMP), FQ(Opcode.JUMPI)]) 11 | _, is_jumpi = instruction.pair_select(opcode, Opcode.JUMP, Opcode.JUMPI) 12 | code_length = instruction.bytecode_length(instruction.curr.code_hash) 13 | dest = instruction.stack_pop() 14 | # if `JUMPI`, pop `condition` 15 | if is_jumpi == FQ(1): 16 | condition = instruction.stack_pop() 17 | # if condition is zero, jump will not happen, so constrain condition not zero 18 | instruction.constrain_not_zero_word(condition) 19 | # lookup value from bytecode table. N_BYTES_PROGRAM_COUNTER is 64 bits 20 | dest_value = instruction.word_to_u64(dest) 21 | 22 | within_range, _ = instruction.compare(dest_value, code_length, N_BYTES_PROGRAM_COUNTER) 23 | 24 | # if not out of range, check `dest` is invalid 25 | if within_range == FQ(1): 26 | value, is_code = instruction.bytecode_lookup_pair(instruction.curr.code_hash, dest_value) 27 | # value is not `JUMPDEST` or `is_code` is false 28 | is_jump_dest = value == Opcode.JUMPDEST 29 | instruction.constrain_zero(is_code * FQ(is_jump_dest)) 30 | 31 | instruction.constrain_error_state( 32 | 2 + is_jumpi.n + instruction.curr.reversible_write_counter.n 33 | ) 34 | -------------------------------------------------------------------------------- /tests/evm/test_pop.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | Bytecode, 5 | ExecutionState, 6 | StepState, 7 | Tables, 8 | verify_steps, 9 | RWDictionary, 10 | ) 11 | from zkevm_specs.util import Word, U256 12 | from common import rand_word 13 | 14 | TESTING_DATA = ( 15 | rand_word(), 16 | rand_word(), 17 | rand_word(), 18 | ) 19 | 20 | 21 | @pytest.mark.parametrize("y", TESTING_DATA) 22 | def test_pop(y: U256): 23 | bytecode = Bytecode().pop().stop() 24 | bytecode_hash = Word(bytecode.hash()) 25 | 26 | tables = Tables( 27 | block_table=set(), 28 | tx_table=set(), 29 | bytecode_table=set(bytecode.table_assignments()), 30 | rw_table=set(RWDictionary(1).stack_read(1, 1023, Word(y)).rws), 31 | ) 32 | 33 | verify_steps( 34 | tables=tables, 35 | steps=[ 36 | StepState( 37 | execution_state=ExecutionState.POP, 38 | rw_counter=1, 39 | call_id=1, 40 | is_root=True, 41 | is_create=False, 42 | code_hash=bytecode_hash, 43 | program_counter=0, 44 | stack_pointer=1023, 45 | gas_left=2, 46 | ), 47 | StepState( 48 | execution_state=ExecutionState.STOP, 49 | rw_counter=2, 50 | call_id=1, 51 | is_root=True, 52 | is_create=False, 53 | code_hash=bytecode_hash, 54 | program_counter=1, 55 | stack_pointer=1024, 56 | gas_left=0, 57 | ), 58 | ], 59 | ) 60 | -------------------------------------------------------------------------------- /specs/super_circuit.md: -------------------------------------------------------------------------------- 1 | # Super Circuit 2 | 3 | The zkEVM is composed of many circuits, each of which is responsible for checking some EVM aspect. 4 | These circuits need to be aggregated or combined together somehow. We could say that the Super 5 | Circuit is one of the possible strategies to combine all other sub-circuits. 6 | 7 | The Super Circuit approach is quite simple, all sub-circuits are put together and their tables are 8 | shared among all of them. It contains the following circuits: 9 | - [x] EVM Circuit 10 | - [x] State Circuit 11 | - [ ] MPT Circuit 12 | - [x] Keccak Circuit 13 | - [x] Tx Circuit 14 | - [x] Bytecode Circuit 15 | - [x] Copy Circuit 16 | - [ ] Block Circuit 17 | - [x] PublicInputs Circuit 18 | 19 | and the following tables: 20 | - [x] RW Table 21 | - [x] MPT Table 22 | - [x] Keccak Table 23 | - [x] Tx Table 24 | - [x] Bytecode Table 25 | - [x] Copy Table 26 | - [x] Block Table 27 | 28 | The following diagram shows the relation between these circuits and tables: 29 | ![](./super_circuit.png) 30 | 31 | **Note**: There are some smaller sub-circuits, like the ECDSA circuit and opcode related 32 | sub-circuits do not interact with other tables and circuits in a way that affects the Super 33 | Circuits, so they are not shown in the diagram for clarity. 34 | 35 | Putting together all sub-circuits in one allows them to easily make lookups to whatever table is 36 | necessary without the need of having them copied. The main drawback is that the Super Circuit proof 37 | is very large, and therefore its verification is costly. To alleviate this problem a new circuit is 38 | introduced: the Root Circuit, whose function is to verify the Super Circuit proof, generating a much 39 | smaller proof which is cheaper to verify. 40 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/slt_sgt.py: -------------------------------------------------------------------------------- 1 | from ...util import FQ 2 | from ..instruction import Instruction, Transition 3 | from ..opcode import Opcode 4 | 5 | 6 | def scmp(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | is_sgt, _ = instruction.pair_select(opcode, Opcode.SGT, Opcode.SLT) 10 | 11 | a = instruction.stack_pop() 12 | b = instruction.stack_pop() 13 | c = instruction.stack_push() 14 | 15 | # swap a and b if the opcode is SGT 16 | aa = b if is_sgt == 1 else a 17 | bb = a if is_sgt == 1 else b 18 | 19 | # decode RLC to bytes for a and b 20 | a8s = aa.to_le_bytes() 21 | b8s = bb.to_le_bytes() 22 | c8s = c.to_le_bytes() 23 | 24 | a_lo, a_hi = aa.to_lo_hi() 25 | b_lo, b_hi = bb.to_lo_hi() 26 | assert c8s[31] == 0 27 | cc = instruction.bytes_to_fq(c8s[:31]) 28 | 29 | a_lt_b_lo, _ = instruction.compare(a_lo, b_lo, 16) 30 | a_lt_b_hi, a_eq_b_hi = instruction.compare(a_hi, b_hi, 16) 31 | 32 | a_lt_b = instruction.select( 33 | a_lt_b_hi, FQ(1), instruction.select(a_eq_b_hi * a_lt_b_lo, FQ(1), FQ(0)) 34 | ) 35 | 36 | # a < 0 and b >= 0 => a < b == true 37 | if a8s[31].n >= 128 and b8s[31].n < 128: 38 | instruction.constrain_equal(cc, FQ(1)) 39 | # b < 0 and a >= 0 => a < b == false 40 | elif b8s[31].n >= 128 and a8s[31].n < 128: 41 | instruction.constrain_equal(cc, FQ(0)) 42 | # (a < 0 and b < 0) or (a >= 0 and b >= 0) 43 | else: 44 | instruction.constrain_equal(cc, a_lt_b) 45 | 46 | instruction.step_state_transition_in_same_context( 47 | opcode, 48 | rw_counter=Transition.delta(3), 49 | program_counter=Transition.delta(1), 50 | stack_pointer=Transition.delta(1), 51 | ) 52 | -------------------------------------------------------------------------------- /specs/opcode/39CODECOPY.md: -------------------------------------------------------------------------------- 1 | # CODECOPY opcode 2 | 3 | ## Procedure 4 | 5 | The `CODECOPY` opcode pops `memory_offset`, `code_offset` and `size` from the stack. 6 | It then copies `size` bytes of code running in the current environment from an offset `code_offset` to the memory at the address `memory_offset`. For out-of-bound scenarios where `size > len(code) - code_offset`, EVM pads 0 to the end of the copied bytes. 7 | 8 | The gas cost of `CODECOPY` opcode consists of two parts: 9 | 10 | 1. A constant gas cost: `3 gas` 11 | 2. A dynamic gas cost: cost of memory expansion and copying (variable depending on the `size` copied to memory) 12 | 13 | ## Circuit Behaviour 14 | 15 | The `CODECOPY` circuit constrains the values popped from stack, call context/account read lookups and CopyTable lookups to verify the copy of bytes. The copy of a dynamic number of bytes is verified by the CopyCircuit outside the `CODECOPY` gadget. 16 | 17 | ## Constraints 18 | 19 | 1. opId = 0x39 20 | 2. State Transitions: 21 | - rw_counter -> rw_counter + 3 (3 stack reads) 22 | - stack_pointer -> stack_pointer + 3 23 | - pc -> pc + 1 24 | - gas -> 3 + dynamic_cost (memory expansion and copier cost when `size > 0`) 25 | - memory_size 26 | - `prev_memory_size` if `size = 0` 27 | - `max(prev_memory_size, (memory_offset + size + 31) / 32)` if `size > 0` 28 | 3. Lookups: 29 | - `memory_offset` is at the top of the stack 30 | - `code_offset` is at the second position of the stack 31 | - `size` is at the third position of the stack 32 | - `code_size` from the bytecode table 33 | 34 | ## Exceptions 35 | 36 | 1. Stack Underflow: `1021 <= stack_pointer <= 1024` 37 | 2. Out-of-Gas: remaining gas is not enough 38 | 39 | ## Code 40 | 41 | Please refer to `src/zkevm_specs/evm/execution/codecopy.py` 42 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/memory.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | from ..table import RW 4 | from ...util import FQ 5 | 6 | 7 | def memory(instruction: Instruction): 8 | opcode = instruction.opcode_lookup(True) 9 | 10 | address = instruction.word_to_address(instruction.stack_pop()) 11 | 12 | is_mload = instruction.is_equal(opcode, Opcode.MLOAD) 13 | is_mstore8 = instruction.is_equal(opcode, Opcode.MSTORE8) 14 | is_store = FQ(1) - is_mload 15 | is_not_mstore8 = FQ(1) - is_mstore8 16 | 17 | value = instruction.stack_push() if is_mload == FQ(1) else instruction.stack_pop() 18 | value_le_bytes = value.to_le_bytes() 19 | 20 | memory_offset = instruction.curr.memory_word_size 21 | next_memory_size, memory_expansion_gas_cost = instruction.memory_expansion( 22 | memory_offset, address.expr() + FQ(1) + (is_not_mstore8 * FQ(31)) 23 | ) 24 | 25 | if is_mstore8 == FQ(1): 26 | instruction.is_equal(instruction.memory_lookup(RW.Write, address), FQ(value_le_bytes[0])) 27 | 28 | if is_not_mstore8 == FQ(1): 29 | for idx in range(32): 30 | instruction.is_equal( 31 | instruction.memory_lookup( 32 | RW.Write if is_store == FQ(1) else RW.Read, address.expr() + idx 33 | ), 34 | FQ(value_le_bytes[31 - idx]), 35 | ) 36 | 37 | instruction.step_state_transition_in_same_context( 38 | opcode, 39 | rw_counter=Transition.delta(34 - (is_mstore8 * 31)), 40 | program_counter=Transition.delta(1), 41 | stack_pointer=Transition.delta(is_store * 2), 42 | memory_word_size=Transition.to(next_memory_size), 43 | dynamic_gas_cost=memory_expansion_gas_cost, 44 | ) 45 | -------------------------------------------------------------------------------- /specs/opcode/41COINBASE_45GASLIMIT_48BASEFEE.md: -------------------------------------------------------------------------------- 1 | # block context op codes 2 | 3 | ## Procedure 4 | 5 | The block context opcodes get the corresponding op data from current block context, and then push it to the stack. The `opcodes` contain `[COINBASE, TIMESTAMP, NUMBER, DIFFICULTY, GASLIMIT, BASEFEE]`. 6 | 7 | ## EVM behavior 8 | 9 | The opcode loads the corresponding op n bytes of data from block context. 10 | then push it to the stack. 11 | 12 | n bytes length and RLC encoding: 13 | 14 | - `COINBASE` 20 bytes length 15 | - `TIMESTAMP` 8 bytes length 16 | - `NUMBER` 8 bytes length 17 | - `DIFFICULTY` 32 bytes length, RLC 18 | - `GASLIMIT` 8 bytes length 19 | - `BASEFEE` 32 bytes length, RLC 20 | 21 | ## Circuit behavior 22 | 23 | 1. construct block context table 24 | 2. do busmapping lookup for stack write operation 25 | 3. other implicit check: bytes length 26 | 27 | ## Constraints 28 | 29 | 1. opcodeId checks 30 | - opId == OpcodeId(0x41) for `COINBASE` 31 | - opId == OpcodeId(0x42) for `TIMESTAMP` 32 | - opId == OpcodeId(0x43) for `NUMBER` 33 | - opId == OpcodeId(0x44) for `DIFFICULTY` 34 | - opId == OpcodeId(0x45) for `GASLIMIT` 35 | - opId == OpcodeId(0x48) for `BASEFEE` 36 | 2. State transition: 37 | - gc + 1 (1 stack write) 38 | - stack_pointer - 1 39 | - pc + 1 40 | - gas + 2 41 | 3. Lookups: 2 42 | - `OP` is on the top of stack 43 | - `OP` is in the block context table 44 | 4. Others: 45 | 46 | - `COINBASE` 20 bytes length 47 | - `TIMESTAMP` 8 bytes length 48 | - `NUMBER` 8 bytes length 49 | - `DIFFICULTY` 32 bytes length, RLC 50 | - `GASLIMIT` 8 bytes length 51 | - `BASEFEE` 32 bytes length, RLC 52 | 53 | ## Exceptions 54 | 55 | 1. stack overflow: stack is full, stack pointer = 0 56 | 2. out of gas: remaining gas is not enough 57 | 58 | ## Code 59 | 60 | Please refer to `src/zkevm_specs/evm/execution/block_ctx.py`. 61 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/codecopy.py: -------------------------------------------------------------------------------- 1 | from ...util import N_BYTES_MEMORY_ADDRESS, FQ 2 | from ..instruction import Instruction, Transition 3 | from ..table import CopyDataTypeTag 4 | 5 | 6 | def codecopy(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | memory_offset_word, code_offset_word, size_word = ( 10 | instruction.stack_pop(), 11 | instruction.stack_pop(), 12 | instruction.stack_pop(), 13 | ) 14 | 15 | memory_offset, size = instruction.memory_offset_and_length(memory_offset_word, size_word) 16 | code_offset = instruction.word_to_fq(code_offset_word, N_BYTES_MEMORY_ADDRESS) 17 | 18 | code_size = instruction.bytecode_length(instruction.curr.code_hash) 19 | 20 | next_memory_size, memory_expansion_gas_cost = instruction.memory_expansion_dynamic_length( 21 | memory_offset, size 22 | ) 23 | gas_cost = instruction.memory_copier_gas_cost(size, memory_expansion_gas_cost) 24 | 25 | if instruction.is_zero(size) == FQ(0): 26 | copy_rwc_inc, _ = instruction.copy_lookup( 27 | instruction.curr.code_hash, 28 | CopyDataTypeTag.Bytecode, 29 | instruction.curr.call_id, 30 | CopyDataTypeTag.Memory, 31 | code_offset, 32 | code_size, 33 | memory_offset, 34 | size, 35 | instruction.curr.rw_counter + instruction.rw_counter_offset, 36 | ) 37 | else: 38 | copy_rwc_inc = FQ(0) 39 | 40 | instruction.step_state_transition_in_same_context( 41 | opcode, 42 | rw_counter=Transition.delta(instruction.rw_counter_offset + copy_rwc_inc), 43 | program_counter=Transition.delta(1), 44 | stack_pointer=Transition.delta(3), 45 | memory_word_size=Transition.to(next_memory_size), 46 | dynamic_gas_cost=gas_cost, 47 | ) 48 | -------------------------------------------------------------------------------- /specs/opcode/31BALANCE.md: -------------------------------------------------------------------------------- 1 | # BALANCE opcode 2 | 3 | ## Procedure 4 | 5 | The `BALANCE` opcode gets balance of the given account. 6 | 7 | ## EVM behaviour 8 | 9 | The `BALANCE` opcode pops `address` (20 bytes of data) off the stack and pushes 10 | the balance of the corresponding account onto the stack. If the given account 11 | doesn't exist (by checking non existing flag), then it will push 0 onto the 12 | stack instead. 13 | 14 | ## Circuit behaviour 15 | 16 | 1. Construct call context table in rw table. 17 | 2. Do bus-mapping lookup for stack read, call context read and account read 18 | operations. 19 | 3. Do bus-mapping lookup for transaction access list write and stack write 20 | operations. 21 | 22 | ## Constraints 23 | 24 | 1. opId = 0x31 25 | 2. State transition: 26 | - gc + 7 (1 stack read, 1 stack write, 3 call context reads, 1/2 account reads, 27 | 1 transaction access list write) 28 | - stack_pointer + 0 (one pop and one push) 29 | - pc + 1 30 | - gas: 31 | - the accessed `address` is warm: GAS_COST_WARM_ACCESS 32 | - the accessed `address` is cold: GAS_COST_ACCOUNT_COLD_ACCESS 33 | 3. Lookups: 7/8 busmapping lookups 34 | - `address` is at top of the stack. 35 | - 3 reads from call context for `tx_id`, `rw_counter_end_of_reversion`, and 36 | `is_persistent`. 37 | - `address` is added to the transaction access list if not already present. 38 | - 1 account `code_hash` lookup to determine account existence. 39 | - If account exists, lookup account `balance`. 40 | - The BALANCE result is at the new top of the stack. 41 | 4. Additional Constraints 42 | - value `is_warm` matches the gas cost for this opcode. 43 | 44 | ## Exceptions 45 | 46 | 1. stack underflow: if the stack starts empty 47 | 2. out of gas: remaining gas is not enough 48 | 49 | ## Code 50 | 51 | Please refer to `src/zkevm_specs/evm/execution/balance.py`. 52 | -------------------------------------------------------------------------------- /tests/evm/test_msize.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, N_BYTES_WORD 13 | 14 | TESTING_DATA = [i for i in range(0, 7)] 15 | 16 | 17 | @pytest.mark.parametrize("memory_word_size", TESTING_DATA) 18 | def test_msize(memory_word_size: int): 19 | value = memory_word_size * N_BYTES_WORD 20 | value = Word(value) 21 | 22 | bytecode = Bytecode().msize().stop() 23 | bytecode_hash = Word(bytecode.hash()) 24 | 25 | tables = Tables( 26 | block_table=set(Block().table_assignments()), 27 | tx_table=set(), 28 | bytecode_table=set(bytecode.table_assignments()), 29 | rw_table=set(RWDictionary(9).stack_write(1, 1022, value).rws), 30 | ) 31 | 32 | verify_steps( 33 | tables=tables, 34 | steps=[ 35 | StepState( 36 | execution_state=ExecutionState.MSIZE, 37 | rw_counter=9, 38 | call_id=1, 39 | is_root=True, 40 | is_create=False, 41 | code_hash=bytecode_hash, 42 | program_counter=0, 43 | stack_pointer=1023, 44 | memory_word_size=memory_word_size, 45 | gas_left=2, 46 | ), 47 | StepState( 48 | execution_state=ExecutionState.STOP, 49 | rw_counter=10, 50 | call_id=1, 51 | is_root=True, 52 | is_create=False, 53 | code_hash=bytecode_hash, 54 | program_counter=1, 55 | stack_pointer=1022, 56 | memory_word_size=memory_word_size, 57 | gas_left=0, 58 | ), 59 | ], 60 | ) 61 | -------------------------------------------------------------------------------- /tests/evm/test_not.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word 13 | 14 | from common import rand_word 15 | 16 | NOT_TESTING_DATA = [ 17 | 0, 18 | 0x030201, 19 | 0x090807, 20 | (1 << 256) - 1, 21 | (1 << 256) - 0x030201, 22 | rand_word(), 23 | ] 24 | 25 | 26 | @pytest.mark.parametrize("a", NOT_TESTING_DATA) 27 | def test_not(a: int): 28 | b = Word(a ^ ((1 << 256) - 1)) 29 | a = Word(a) 30 | 31 | bytecode = Bytecode().not_(a).stop() 32 | bytecode_hash = Word(bytecode.hash()) 33 | 34 | tables = Tables( 35 | block_table=set(Block().table_assignments()), 36 | tx_table=set(), 37 | bytecode_table=set(bytecode.table_assignments()), 38 | rw_table=set(RWDictionary(9).stack_read(1, 1023, a).stack_write(1, 1023, b).rws), 39 | ) 40 | 41 | verify_steps( 42 | tables=tables, 43 | steps=[ 44 | StepState( 45 | execution_state=ExecutionState.NOT, 46 | rw_counter=9, 47 | call_id=1, 48 | is_root=True, 49 | is_create=False, 50 | code_hash=bytecode_hash, 51 | program_counter=33, 52 | stack_pointer=1023, 53 | gas_left=3, 54 | ), 55 | StepState( 56 | execution_state=ExecutionState.STOP, 57 | rw_counter=11, 58 | call_id=1, 59 | is_root=True, 60 | is_create=False, 61 | code_hash=bytecode_hash, 62 | program_counter=34, 63 | stack_pointer=1023, 64 | gas_left=0, 65 | ), 66 | ], 67 | ) 68 | -------------------------------------------------------------------------------- /specs/opcode/0AEXP.md: -------------------------------------------------------------------------------- 1 | # EXP opcode 2 | 3 | ## EVM Behaviour 4 | 5 | The `EXP` opcode performs an exponential operation. It reads the integer base `base` and integer exponent `exponent` from the top of the stack and pushes the exponentiation result, i.e. `base ^ exponent (mod 2^256)` to the top of the stack. 6 | 7 | ## Constraints 8 | 9 | 1. opId == 0x0A 10 | 2. State Transition: 11 | - rw_counter += 3 12 | - stack_pointer += 1 13 | - pc += 1 14 | - gas -= static_gas + dynamic_gas, where: 15 | - `static_gas = 10` 16 | - `dynamic_gas = 50 * byte_size(exponent)` 17 | 3. Lookups: 18 | - `base` is at the top of the stack (Read from RW Table) 19 | - `exponent` is at the second position of the stack (Read from RW Table) 20 | - `exponentiation` is at the top of the stack (Write to RW Table) 21 | - if `exponent == 2`: 22 | - Do a lookup to exponentiation table for 23 | ``` 24 | ( 25 | is_step=1, 26 | is_last=1, 27 | base_limbs, 28 | exponent_lo_hi, 29 | exponentiation_lo_hi, 30 | ) 31 | ``` 32 | - if `exponent > 2`: 33 | - Do a lookup to exponentiation table for: 34 | ``` 35 | ( 36 | is_step=1, 37 | is_last=0, 38 | base_limbs, 39 | exponent_lo_hi, 40 | exponentiation_lo_hi, 41 | ) 42 | ``` 43 | - Do a lookup to exponentiation table for: 44 | ``` 45 | ( 46 | is_step=1, 47 | is_last=1, 48 | base_limbs, 49 | exponent_lo_hi=[2, 0], 50 | base_sq_lo_hi, 51 | ) 52 | ``` 53 | where `base_sq_lo_hi` are the 128-bit low-high parts of `base * base (mod 2^256)` 54 | 55 | ## Exceptions 56 | 57 | 1. Stack underflow: The stack is empty or contains only one element. 58 | 2. Out of gas: The gas available is not sufficient to cover the cost of the exponentiation operation. 59 | 60 | ## Code 61 | 62 | Please refer to [EXP gadget](../../src/zkevm_specs/evm/execution/exp.py) 63 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/util/memory_gadget.py: -------------------------------------------------------------------------------- 1 | from ...util import N_BYTES_MEMORY_ADDRESS, FQ, Expression 2 | from ..instruction import Instruction 3 | 4 | 5 | class BufferReaderGadget: 6 | def __init__( 7 | self, inst: Instruction, max_bytes: int, addr_start: FQ, addr_end: FQ, bytes_left: FQ 8 | ): 9 | self.instruction = inst 10 | self.selectors = inst.continuous_selectors(bytes_left, max_bytes) 11 | # Here we are just generating witness data, no need to use inst here 12 | self.bound_dist = [FQ(max(0, addr_end.n - addr_start.n - i)) for i in range(max_bytes)] 13 | self.bound_dist_is_zero = [inst.is_zero(bound_dist) for bound_dist in self.bound_dist] 14 | 15 | # constraint on bound_dist[0] 16 | inst.constrain_equal( 17 | self.bound_dist[0], addr_end - inst.min(addr_end, addr_start, N_BYTES_MEMORY_ADDRESS) 18 | ) 19 | # constraints on bound_dist[1:] 20 | for i in range(1, max_bytes): 21 | diff = self.bound_dist[i - 1] - self.bound_dist[i] 22 | # diff == 0 if bound_dist[i - 1] == 0; otherwise 1 23 | inst.constrain_equal( 24 | diff, inst.select(self.bound_dist_is_zero[i - 1], FQ.zero(), FQ.one()) 25 | ) 26 | 27 | def constrain_byte(self, idx: int, byte: Expression): 28 | # bytes[idx] == 0 when selectors[idx] == 0 29 | self.instruction.constrain_zero(byte * (1 - self.selectors[idx])) 30 | # bytes[idx] == 0 when bound_dist[idx] == 0 31 | self.instruction.constrain_zero(byte * self.bound_dist_is_zero[idx]) 32 | 33 | def num_bytes(self) -> FQ: 34 | return FQ(sum(self.selectors)) 35 | 36 | def has_data(self, idx: int) -> FQ: 37 | return self.selectors[idx] 38 | 39 | def read_flag(self, idx: int) -> FQ: 40 | return self.selectors[idx] * (1 - self.bound_dist_is_zero[idx]) 41 | -------------------------------------------------------------------------------- /specs/opcode/3bEXTCODESIZE.md: -------------------------------------------------------------------------------- 1 | # EXTCODESIZE opcode 2 | 3 | ## Procedure 4 | 5 | The `EXTCODESIZE` opcode gets code size of the given account. 6 | 7 | ## EVM behaviour 8 | 9 | The `EXTCODESIZE` opcode pops `address` (20 bytes of data) off the stack and 10 | pushes the code size of the corresponding account onto the stack. If the given 11 | account doesn't exist (by checking that `code_hash == 0`), then it will push 0 12 | onto the stack instead. 13 | 14 | ## Circuit behaviour 15 | 16 | 1. Construct call context table in rw table. 17 | 2. Do bus-mapping lookup for stack read, call context read and account read 18 | operations. 19 | 3. Do bus-mapping lookup for transaction access list write and stack write 20 | operations. 21 | 22 | ## Constraints 23 | 24 | 1. opId = 0x3b 25 | 2. State transition: 26 | - gc + 7 (1 stack read, 1 stack write, 3 call context reads, 1 account read, 27 | 1 transaction access list write) 28 | - stack_pointer + 0 (one pop and one push) 29 | - pc + 1 30 | - gas: 31 | - the accessed `address` is warm: WARM_STORAGE_READ_COST 32 | - the accessed `address` is cold: COLD_ACCOUNT_ACCESS_COST 33 | 3. Lookups: 7 bus-mapping lookups 34 | - `address` is popped from the stack. 35 | - 3 from call context for `tx_id`, `rw_counter_end_of_reversion`, and 36 | `is_persistent`. 37 | - `address` is added to the transaction access list if not already present. 38 | - Lookup account `code_hash`, then get `code_size` if `code_hash != 0`, 39 | otherwise skip the `code_size` lookup. 40 | - The EXTCODESIZE result is at the new top of the stack. 41 | 4. Additional Constraints 42 | - value `is_warm` matches the gas cost for this opcode. 43 | 44 | ## Exceptions 45 | 46 | 1. stack underflow: if the stack starts empty 47 | 2. out of gas: remaining gas is not enough 48 | 49 | ## Code 50 | 51 | Please refer to `src/zkevm_specs/evm/execution/extcodesize.py`. 52 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasStaticMemoryExpansion.md: -------------------------------------------------------------------------------- 1 | # ErrorOOGStaticMemoryExpansion state 2 | 3 | ## Procedure 4 | 5 | Handle the corresponding out of gas errors for `MLOAD`, `MSTORE` and `MSTORE8` opcodes. 6 | 7 | ### EVM behavior 8 | 9 | For the current `go-ethereum` code, the out of gas error may occur for `constant gas` or `dynamic gas`. The gas cost is calculated as: 10 | 11 | ``` 12 | gas_cost = constant_gas + dynamic_gas 13 | ``` 14 | 15 | The constant gas is same for `MLOAD`, `MSTORE` and `MSTORE8`. 16 | 17 | ``` 18 | constant_gas = 3 19 | ``` 20 | 21 | They are also same for dynamic gas calculation. The dynamic gas is calculated as: 22 | 23 | ``` 24 | dynamic_gas = memory_expansion_cost 25 | ``` 26 | 27 | The memory expansion gas cost is calculated as: 28 | 29 | ``` 30 | next_memory_word_size = (destination_offset + copy_byte_size + 31) // 32 31 | # `current_memory_word_size` is fetched from the execution step. 32 | if next_memory_word_size <= current_memory_word_size: 33 | memory_expansion_gas_cost = 0 34 | else: 35 | memory_expansion_gas_cost = ( 36 | 3 * (next_memory_word_size - current_memory_word_size) 37 | + next_memory_word_size * next_memory_word_size // 512 38 | - current_memory_word_size * current_memory_word_size // 512 39 | ) 40 | ``` 41 | 42 | ### Constraints 43 | 44 | 1. Current opcode is one of `MLOAD`, `MSTORE` and `MSTORE8`. 45 | 2. Constrain `gas_left < gas_cost`. 46 | 3. Current call must fail. 47 | 4. If it's a root call, it transits to `EndTx`. 48 | 5. If it isn't a root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 49 | 6. Constrain `rw_counter_end_of_reversion = rw_counter_end_of_step + reversible_counter`. 50 | 51 | ### Lookups 52 | 53 | 1. `1` stack pop. 54 | 2. `2` call context lookups for `is_success` and `rw_counter_end_of_reversion`. 55 | 56 | And restore context lookups for non-root call. -------------------------------------------------------------------------------- /tests/evm/test_returndatasize.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | CallContextFieldTag, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, U64 13 | 14 | 15 | TESTING_DATA = ( 16 | 0x00, 17 | 0x10, 18 | 0x302010, 19 | ) 20 | 21 | 22 | @pytest.mark.parametrize("returndatasize", TESTING_DATA) 23 | def test_returndatasize(returndatasize: U64): 24 | bytecode = Bytecode().returndatasize() 25 | bytecode_hash = Word(bytecode.hash()) 26 | 27 | tables = Tables( 28 | block_table=set(), 29 | tx_table=set(), 30 | bytecode_table=set(bytecode.table_assignments()), 31 | rw_table=set( 32 | RWDictionary(9) 33 | .call_context_read(1, CallContextFieldTag.LastCalleeReturnDataLength, returndatasize) 34 | .stack_write(1, 1023, Word(returndatasize)) 35 | .rws 36 | ), 37 | ) 38 | 39 | verify_steps( 40 | tables=tables, 41 | steps=[ 42 | StepState( 43 | execution_state=ExecutionState.RETURNDATASIZE, 44 | rw_counter=9, 45 | call_id=1, 46 | is_root=True, 47 | is_create=False, 48 | code_hash=bytecode_hash, 49 | program_counter=0, 50 | stack_pointer=1024, 51 | gas_left=2, 52 | ), 53 | StepState( 54 | execution_state=ExecutionState.STOP, 55 | rw_counter=11, 56 | call_id=1, 57 | is_root=True, 58 | is_create=False, 59 | code_hash=bytecode_hash, 60 | program_counter=1, 61 | stack_pointer=1023, 62 | gas_left=0, 63 | ), 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/error_oog_call.py: -------------------------------------------------------------------------------- 1 | from zkevm_specs.evm_circuit.util.call_gadget import CallGadget 2 | from ...util import FQ 3 | from ..instruction import Instruction 4 | from ..table import CallContextFieldTag 5 | from ...util import N_BYTES_GAS 6 | from ..opcode import Opcode 7 | 8 | 9 | # Handle the corresponding out of gas errors for CALL, CALLCODE, DELEGATECALL 10 | # and STATICCALL opcodes. 11 | def error_oog_call(instruction: Instruction): 12 | # retrieve op code associated to oog call error 13 | opcode = instruction.opcode_lookup(True) 14 | is_call, is_callcode, is_delegatecall, is_staticcall = instruction.multiple_select( 15 | opcode, (Opcode.CALL, Opcode.CALLCODE, Opcode.DELEGATECALL, Opcode.STATICCALL) 16 | ) 17 | 18 | # Constrain opcode must be CALL, CALLCODE, DELEGATECALL or STATICCALL. 19 | instruction.constrain_equal(is_call + is_callcode + is_delegatecall + is_staticcall, FQ(1)) 20 | 21 | tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId) 22 | 23 | # init CallGadget to handle stack vars. 24 | call = CallGadget(instruction, FQ(0), is_call, is_callcode, is_delegatecall, is_staticcall) 25 | 26 | # TODO: handle PrecompiledContract oog cases 27 | 28 | # Add callee to access list 29 | is_warm_access = instruction.read_account_to_access_list(tx_id, call.callee_address) 30 | 31 | # verify gas cost 32 | gas_cost = call.gas_cost(instruction, is_warm_access) 33 | 34 | # verify gas is insufficient 35 | gas_not_enough, _ = instruction.compare(instruction.curr.gas_left, gas_cost, N_BYTES_GAS) 36 | instruction.constrain_equal(gas_not_enough, FQ(1)) 37 | 38 | # Both CALL and CALLCODE opcodes have an extra stack pop `value` relative to 39 | # DELEGATECALL and STATICCALL. 40 | instruction.constrain_error_state( 41 | 11 + is_call.n + is_callcode.n + instruction.curr.reversible_write_counter.n 42 | ) 43 | -------------------------------------------------------------------------------- /tests/evm/test_iszero.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word 13 | 14 | 15 | TESTING_DATA = ( 16 | bytes([0]), 17 | bytes([7]), 18 | ) 19 | 20 | 21 | @pytest.mark.parametrize("value_be_bytes", TESTING_DATA) 22 | def test_iszero(value_be_bytes: bytes): 23 | value = int.from_bytes(value_be_bytes, "big") 24 | result = 0x1 if value == 0x0 else 0x0 25 | value = Word(value) 26 | result = Word(result) 27 | 28 | bytecode = Bytecode().push1(value_be_bytes).iszero().stop() 29 | bytecode_hash = Word(bytecode.hash()) 30 | 31 | tables = Tables( 32 | block_table=set(Block().table_assignments()), 33 | tx_table=set(), 34 | bytecode_table=set(bytecode.table_assignments()), 35 | rw_table=set(RWDictionary(9).stack_read(1, 1023, value).stack_write(1, 1023, result).rws), 36 | ) 37 | 38 | verify_steps( 39 | tables=tables, 40 | steps=[ 41 | StepState( 42 | execution_state=ExecutionState.ISZERO, 43 | rw_counter=9, 44 | call_id=1, 45 | is_root=True, 46 | is_create=False, 47 | code_hash=bytecode_hash, 48 | program_counter=2, 49 | stack_pointer=1023, 50 | gas_left=3, 51 | ), 52 | StepState( 53 | execution_state=ExecutionState.STOP, 54 | rw_counter=11, 55 | call_id=1, 56 | is_root=True, 57 | is_create=False, 58 | code_hash=bytecode_hash, 59 | program_counter=3, 60 | stack_pointer=1023, 61 | gas_left=0, 62 | ), 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /tests/evm/test_jump.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | Opcode, 7 | verify_steps, 8 | Tables, 9 | Block, 10 | Bytecode, 11 | RWDictionary, 12 | ) 13 | from zkevm_specs.util import Word 14 | 15 | 16 | TESTING_DATA = ((Opcode.JUMP, 7),) 17 | 18 | 19 | @pytest.mark.parametrize("opcode, dest", TESTING_DATA) 20 | def test_jump(opcode: Opcode, dest: int): 21 | dest_bytes = dest.to_bytes(1, "little") 22 | block = Block() 23 | # Jumps to PC=7 24 | # PUSH1 80 PUSH1 40 PUSH1 07 JUMP JUMPDEST STOP 25 | bytecode = Bytecode().push1(0x80).push1(0x40).push1(dest_bytes).jump().jumpdest().stop() 26 | bytecode_hash = Word(bytecode.hash()) 27 | 28 | tables = Tables( 29 | block_table=set(block.table_assignments()), 30 | tx_table=set(), 31 | bytecode_table=set(bytecode.table_assignments()), 32 | rw_table=set(RWDictionary(9).stack_read(1, 1021, Word(dest)).rws), 33 | ) 34 | 35 | verify_steps( 36 | tables=tables, 37 | steps=[ 38 | StepState( 39 | execution_state=ExecutionState.JUMP, 40 | rw_counter=9, 41 | call_id=1, 42 | is_root=True, 43 | is_create=False, 44 | code_hash=bytecode_hash, 45 | program_counter=6, 46 | stack_pointer=1021, 47 | gas_left=8, 48 | ), 49 | StepState( 50 | execution_state=ExecutionState.STOP, 51 | rw_counter=10, 52 | call_id=1, 53 | is_root=True, 54 | is_create=False, 55 | code_hash=bytecode_hash, 56 | program_counter=int.from_bytes(dest_bytes, "little"), 57 | stack_pointer=1022, 58 | gas_left=0, 59 | ), 60 | ], 61 | ) 62 | -------------------------------------------------------------------------------- /tests/evm/test_callvalue.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | CallContextFieldTag, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, U256 13 | 14 | 15 | TESTING_DATA = ( 16 | 0x00, 17 | 0x10, 18 | 0x302010, 19 | 0xF0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F, 20 | ) 21 | 22 | 23 | @pytest.mark.parametrize("callvalue", TESTING_DATA) 24 | def test_callvalue(callvalue: U256): 25 | bytecode = Bytecode().callvalue() 26 | bytecode_hash = Word(bytecode.hash()) 27 | 28 | tables = Tables( 29 | block_table=set(), 30 | tx_table=set(), 31 | bytecode_table=set(bytecode.table_assignments()), 32 | rw_table=set( 33 | RWDictionary(9) 34 | .call_context_read(1, CallContextFieldTag.Value, Word(callvalue)) 35 | .stack_write(1, 1023, Word(callvalue)) 36 | .rws 37 | ), 38 | ) 39 | 40 | verify_steps( 41 | tables=tables, 42 | steps=[ 43 | StepState( 44 | execution_state=ExecutionState.CALLVALUE, 45 | rw_counter=9, 46 | call_id=1, 47 | is_root=True, 48 | is_create=False, 49 | code_hash=bytecode_hash, 50 | program_counter=0, 51 | stack_pointer=1024, 52 | gas_left=2, 53 | ), 54 | StepState( 55 | execution_state=ExecutionState.STOP, 56 | rw_counter=11, 57 | call_id=1, 58 | is_root=True, 59 | is_create=False, 60 | code_hash=bytecode_hash, 61 | program_counter=1, 62 | stack_pointer=1023, 63 | gas_left=0, 64 | ), 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /specs/opcode/35CALLDATALOAD.md: -------------------------------------------------------------------------------- 1 | # CALLDATALOAD opcode 2 | 3 | ## Procedure 4 | 5 | The `CALLDATALOAD` opcode gets input data of current environment. 6 | 7 | ## EVM Behaviour 8 | 9 | Stack input is the byte offset to read call data from. Stack output is a 32-byte value starting from the given offset of the call data. All bytes after the end of the call data are set to `0`. 10 | 11 | ## Constraints 12 | 13 | 1. opId == 0x35 14 | 2. State Transition: 15 | - if is_root_call: 16 | - rw_counter += 4 (1 stack read, 2 call context read, 1 stack write) 17 | - if is_internal_call: 18 | - rw_counter += rw_counter_offset ∈ {5, 6, ..., 36, 37} (1 stack read, 3 call context reads, i ∈ {0, 1, ..., 31, 32} memory reads, 1 stack write) 19 | - stack_pointer unchanged 20 | - pc + 1 21 | - gas - 3 22 | 3. Lookups: 23 | - `offset` is at the top of the stack 24 | - if is_root_call (where `src_addr = offset`): 25 | - `tx_id` is in the RW table (call context) 26 | - `calldata_length` is in the RW table (call context) 27 | - i ∈ {0, 1, ..., 31, 32} lookups for `i in range(32)`: if `buffer.read_flag(i)` then the i'th byte of the element on top of the stack `calldata_word[i]` is in the TX table {tx id, call data, src_addr + i} 28 | - if is_internal_call (where `src_addr = offset + calldata_offset`): 29 | - `calldata_length` is in the RW table (call context) 30 | - `calldata_offset` is in the RW table (call context) 31 | - `caller_id` is in the RW table (call context) 32 | - i ∈ {0, 1, ..., 31, 32} lookups for `i in range(32)`: if `buffer.read_flag(i)` then the i'th byte of the element on top of the stack `calldata_word[i]` is in the RW table {memory, src_addr + i, caller_id} 33 | 34 | ## Exceptions 35 | 36 | 1. Stack underflow: stack is empty, stack pointer = 1024 37 | 2. Out of gas: remaining gas is not enough for this opcode 38 | 39 | ## Code 40 | 41 | Please refer to `src/zkevm_specs/evm/execution/calldataload.py`. 42 | -------------------------------------------------------------------------------- /tests/evm/test_caller.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | CallContextFieldTag, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, U160 13 | from common import rand_address 14 | 15 | 16 | TESTING_DATA = ( 17 | 0x00, 18 | 0x10, 19 | 0x030201, 20 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 21 | rand_address(), 22 | ) 23 | 24 | 25 | @pytest.mark.parametrize("caller", TESTING_DATA) 26 | def test_caller(caller: U160): 27 | bytecode = Bytecode().caller() 28 | bytecode_hash = Word(bytecode.hash()) 29 | 30 | tables = Tables( 31 | block_table=set(), 32 | tx_table=set(), 33 | bytecode_table=set(bytecode.table_assignments()), 34 | rw_table=set( 35 | RWDictionary(9) 36 | .call_context_read(1, CallContextFieldTag.CallerAddress, Word(caller)) 37 | .stack_write(1, 1023, Word(caller)) 38 | .rws 39 | ), 40 | ) 41 | 42 | verify_steps( 43 | tables=tables, 44 | steps=[ 45 | StepState( 46 | execution_state=ExecutionState.CALLER, 47 | rw_counter=9, 48 | call_id=1, 49 | is_root=True, 50 | is_create=False, 51 | code_hash=bytecode_hash, 52 | program_counter=0, 53 | stack_pointer=1024, 54 | gas_left=2, 55 | ), 56 | StepState( 57 | execution_state=ExecutionState.STOP, 58 | rw_counter=11, 59 | call_id=1, 60 | is_root=True, 61 | is_create=False, 62 | code_hash=bytecode_hash, 63 | program_counter=1, 64 | stack_pointer=1023, 65 | gas_left=0, 66 | ), 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /tests/evm/test_address.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | Bytecode, 5 | CallContextFieldTag, 6 | ExecutionState, 7 | RWDictionary, 8 | StepState, 9 | Tables, 10 | verify_steps, 11 | ) 12 | from zkevm_specs.util import Word, U160 13 | from common import rand_address 14 | 15 | TESTING_DATA = ( 16 | 0x00, 17 | 0x10, 18 | 0x030201, 19 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 20 | rand_address(), 21 | ) 22 | 23 | 24 | @pytest.mark.parametrize("address", TESTING_DATA) 25 | def test_address(address: U160): 26 | bytecode = Bytecode().address() 27 | bytecode_hash = Word(bytecode.hash()) 28 | 29 | tables = Tables( 30 | block_table=set(), 31 | tx_table=set(), 32 | bytecode_table=set(bytecode.table_assignments()), 33 | rw_table=set( 34 | RWDictionary(9) 35 | .call_context_read(1, CallContextFieldTag.CalleeAddress, Word(address)) 36 | .stack_write(1, 1023, Word(address)) 37 | .rws 38 | ), 39 | ) 40 | 41 | verify_steps( 42 | tables=tables, 43 | steps=[ 44 | StepState( 45 | execution_state=ExecutionState.ADDRESS, 46 | rw_counter=9, 47 | call_id=1, 48 | is_root=True, 49 | is_create=False, 50 | code_hash=bytecode_hash, 51 | program_counter=0, 52 | stack_pointer=1024, 53 | gas_left=2, 54 | ), 55 | StepState( 56 | execution_state=ExecutionState.STOP, 57 | rw_counter=11, 58 | call_id=1, 59 | is_root=True, 60 | is_create=False, 61 | code_hash=bytecode_hash, 62 | program_counter=1, 63 | stack_pointer=1023, 64 | gas_left=0, 65 | ), 66 | ], 67 | ) 68 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasDynamicMemoryExpansion.md: -------------------------------------------------------------------------------- 1 | # ErrorOOGDynamicMemoryExpansion state 2 | 3 | ## Procedure 4 | 5 | Handle the corresponding out of gas errors for `CREATE`, `RETURN` and `REVERT` opcodes. 6 | 7 | ### EVM behavior 8 | 9 | For the current `go-ethereum` code, the out of gas error may occur for `constant gas` or `dynamic gas`. The gas cost is calculated as: 10 | 11 | ``` 12 | gas_cost = constant_gas + dynamic_gas 13 | ``` 14 | 15 | The constant gas is different for `CREATE`, `RETURN` and `REVERT`. 16 | 17 | ``` 18 | if opcode == CREATE: 19 | constant_gas = 32000 20 | else: 21 | constant_gas = 0 22 | ``` 23 | 24 | The dynamic gas is calculated as: 25 | 26 | ``` 27 | dynamic_gas = memory_expansion_cost 28 | ``` 29 | 30 | The memory expansion gas cost is calculated as: 31 | 32 | ``` 33 | next_memory_word_size = (destination_offset + copy_byte_size + 31) // 32 34 | # `current_memory_word_size` is fetched from the execution step. 35 | if next_memory_word_size <= current_memory_word_size: 36 | memory_expansion_gas_cost = 0 37 | else: 38 | memory_expansion_gas_cost = ( 39 | 3 * (next_memory_word_size - current_memory_word_size) 40 | + next_memory_word_size * next_memory_word_size // 512 41 | - current_memory_word_size * current_memory_word_size // 512 42 | ) 43 | ``` 44 | 45 | ### Constraints 46 | 47 | 1. Current opcode is one of `CREATE`, `RETURN` and `REVERT`. 48 | 2. Constrain `gas_left < gas_cost`. 49 | 3. Current call must fail. 50 | 4. If it's a root call, it transits to `EndTx`. 51 | 5. If it isn't a root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 52 | 6. Constrain `rw_counter_end_of_reversion = rw_counter_end_of_step + reversible_counter`. 53 | 54 | ### Lookups 55 | 56 | 1. `3` stack pop for `CREATE` and `2` stack pop for `RETURN` and `REVERT`. 57 | 2. `2` call context lookups for `is_success` and `rw_counter_end_of_reversion`. 58 | 59 | And restore context lookups for non-root call. 60 | -------------------------------------------------------------------------------- /tests/evm/test_calldatasize.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | CallContextFieldTag, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, U64 13 | 14 | 15 | TESTING_DATA = ( 16 | 0x00, 17 | 0x10, 18 | 0x302010, 19 | ) 20 | 21 | 22 | @pytest.mark.parametrize("calldatasize", TESTING_DATA) 23 | def test_calldatasize(calldatasize: U64): 24 | bytecode = Bytecode().calldatasize() 25 | bytecode_hash = Word(bytecode.hash()) 26 | 27 | tables = Tables( 28 | block_table=set(), 29 | tx_table=set(), 30 | bytecode_table=set(bytecode.table_assignments()), 31 | rw_table=set( 32 | RWDictionary(9) 33 | .call_context_read(1, CallContextFieldTag.CallDataLength, calldatasize) 34 | .stack_write( 35 | 1, 36 | 1023, 37 | Word( 38 | calldatasize, 39 | ), 40 | ) 41 | .rws 42 | ), 43 | ) 44 | 45 | verify_steps( 46 | tables=tables, 47 | steps=[ 48 | StepState( 49 | execution_state=ExecutionState.CALLDATASIZE, 50 | rw_counter=9, 51 | call_id=1, 52 | is_root=True, 53 | is_create=False, 54 | code_hash=bytecode_hash, 55 | program_counter=0, 56 | stack_pointer=1024, 57 | gas_left=2, 58 | ), 59 | StepState( 60 | execution_state=ExecutionState.STOP, 61 | rw_counter=11, 62 | call_id=1, 63 | is_root=True, 64 | is_create=False, 65 | code_hash=bytecode_hash, 66 | program_counter=1, 67 | stack_pointer=1023, 68 | gas_left=0, 69 | ), 70 | ], 71 | ) 72 | -------------------------------------------------------------------------------- /tests/evm/test_origin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | Bytecode, 5 | CallContextFieldTag, 6 | ExecutionState, 7 | StepState, 8 | Tables, 9 | Transaction, 10 | verify_steps, 11 | RWDictionary, 12 | ) 13 | from zkevm_specs.util import Word, U256 14 | from common import rand_address 15 | 16 | TESTING_DATA = ( 17 | 0x00, 18 | 0x10, 19 | 0x302010, 20 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 21 | rand_address(), 22 | ) 23 | 24 | 25 | @pytest.mark.parametrize("origin", TESTING_DATA) 26 | def test_origin(origin: U256): 27 | tx = Transaction(caller_address=origin) 28 | 29 | bytecode = Bytecode().origin().stop() 30 | bytecode_hash = Word(bytecode.hash()) 31 | 32 | tables = Tables( 33 | block_table=set(), 34 | tx_table=set(tx.table_assignments()), 35 | bytecode_table=set(bytecode.table_assignments()), 36 | rw_table=set( 37 | RWDictionary(9) 38 | .call_context_read(1, CallContextFieldTag.TxId, tx.id) 39 | .stack_write(1, 1023, Word(origin)) 40 | .rws 41 | ), 42 | ) 43 | 44 | verify_steps( 45 | tables=tables, 46 | steps=[ 47 | StepState( 48 | execution_state=ExecutionState.ORIGIN, 49 | rw_counter=9, 50 | call_id=1, 51 | is_root=True, 52 | is_create=False, 53 | code_hash=bytecode_hash, 54 | program_counter=0, 55 | stack_pointer=1024, 56 | gas_left=2, 57 | ), 58 | StepState( 59 | execution_state=ExecutionState.STOP, 60 | rw_counter=11, 61 | call_id=1, 62 | is_root=True, 63 | is_create=False, 64 | code_hash=bytecode_hash, 65 | program_counter=1, 66 | stack_pointer=1023, 67 | gas_left=0, 68 | ), 69 | ], 70 | ) 71 | -------------------------------------------------------------------------------- /specs/opcode/3cEXTCODECOPY.md: -------------------------------------------------------------------------------- 1 | # EXTCODECOPY opcode 2 | 3 | ## Procedure 4 | 5 | The `EXTCODECOPY` opcode pops `address`, `memory_offset`, `code_offset` and `size` from the stack. It then copies `size` bytes of code of `address` from an offset `code_offset` to the memory at the address `memory_offset`. For out-of-bound scenarios where `size > len(code) - code_offset`, EVM pads 0 to the end of the copied bytes. 6 | 7 | The gas cost of `EXTCODECOPY` opcode is dynamic and is based on the cost of memory expansion and copying (variable depending on the `size` copied to memory) and accessing the address (variable depending on whether the `address` is warm or not) 8 | 9 | ## Circuit Behaviour 10 | 11 | The `EXTCODECOPY` circuit constrains the values popped from stack, call context/address read lookups and CopyTable lookups to verify the copy of bytes. The copy of a dynamic number of bytes is verified by the CopyCircuit outside the `EXTCODECOPY` gadget. 12 | 13 | ## Constraints 14 | 15 | 1. opId = 0x3c 16 | 2. State transition: 17 | - stack pointer + 4 18 | - pc + 1 19 | - gas -> dynamic gas cost 20 | - memory_size 21 | - `prev_memory_size` if `size = 0` 22 | - `max(prev_memory_size, (memory_offset + size + 31) / 32)` if `size > 0` 23 | 3. Lookups: 10 24 | - `address` is at the top of the stack 25 | - `memory_offset` is at the second position of the stack 26 | - `code_offset` is at the third position of the stack 27 | - `size` is at the fourth position of the stack 28 | - `code_hash` from the address (will be 0 when the account doesn't exist). 29 | - `code_size` from the bytecode table if account exists (`code_hash != 0`) 30 | - `tx_id`, `rw_counter_end_of_reversion`, `is_persistent` from call context 31 | - `address` is added to the transaction access list if not already present 32 | 33 | ## Exceptions 34 | 35 | 1. Stack Underflow: `1020 <= stack pointer <= 1024` 36 | 2. Out-of-Gas: remaining gas is not enough 37 | 38 | ## Code 39 | 40 | Please refer to [extcodecopy](src/zkevm_specs/evm/execution/extcodecopy.py). 41 | -------------------------------------------------------------------------------- /tests/evm/test_push.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word 13 | from common import rand_bytes 14 | 15 | TESTING_DATA = tuple( 16 | [ 17 | (bytes([1])), 18 | (bytes([2, 1])), 19 | (bytes([i for i in range(31, 0, -1)])), 20 | (bytes([i for i in range(32, 0, -1)])), 21 | ] 22 | + [(rand_bytes(i + 1)) for i in range(32)] 23 | ) 24 | 25 | 26 | @pytest.mark.parametrize("value_be_bytes", TESTING_DATA) 27 | def test_push(value_be_bytes: bytes): 28 | value = Word(int.from_bytes(value_be_bytes, "big")) 29 | 30 | bytecode = Bytecode().push(value_be_bytes, n_bytes=len(value_be_bytes)) 31 | bytecode_hash = Word(bytecode.hash()) 32 | 33 | tables = Tables( 34 | block_table=set(Block().table_assignments()), 35 | tx_table=set(), 36 | bytecode_table=set(bytecode.table_assignments()), 37 | rw_table=set(RWDictionary(8).stack_write(1, 1023, value).rws), 38 | ) 39 | 40 | verify_steps( 41 | tables=tables, 42 | steps=[ 43 | StepState( 44 | execution_state=ExecutionState.PUSH, 45 | rw_counter=8, 46 | call_id=1, 47 | is_root=True, 48 | is_create=False, 49 | code_hash=bytecode_hash, 50 | program_counter=0, 51 | stack_pointer=1024, 52 | gas_left=3, 53 | ), 54 | StepState( 55 | execution_state=ExecutionState.STOP, 56 | rw_counter=9, 57 | call_id=1, 58 | is_root=True, 59 | is_create=False, 60 | code_hash=bytecode_hash, 61 | program_counter=1 + len(value_be_bytes), 62 | stack_pointer=1023, 63 | gas_left=0, 64 | ), 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /tests/evm/test_gasprice.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | Block, 5 | Bytecode, 6 | CallContextFieldTag, 7 | ExecutionState, 8 | StepState, 9 | Tables, 10 | Transaction, 11 | verify_steps, 12 | RWDictionary, 13 | ) 14 | from zkevm_specs.util import Word, U256 15 | 16 | TESTING_DATA = ( 17 | 0x00, 18 | 0x10, 19 | 0x302010, 20 | 0xF0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F, 21 | ) 22 | 23 | 24 | @pytest.mark.parametrize("gasprice", TESTING_DATA) 25 | def test_gasprice(gasprice: U256): 26 | tx = Transaction(gas_price=gasprice) 27 | 28 | bytecode = Bytecode().gasprice().stop() 29 | bytecode_hash = Word(bytecode.hash()) 30 | 31 | tables = Tables( 32 | block_table=set(Block().table_assignments()), 33 | tx_table=set(tx.table_assignments()), 34 | bytecode_table=set(bytecode.table_assignments()), 35 | rw_table=set( 36 | RWDictionary(9) 37 | .call_context_read(1, CallContextFieldTag.TxId, tx.id) 38 | .stack_write(1, 1023, Word(gasprice)) 39 | .rws 40 | ), 41 | ) 42 | 43 | verify_steps( 44 | tables=tables, 45 | steps=[ 46 | StepState( 47 | execution_state=ExecutionState.GASPRICE, 48 | rw_counter=9, 49 | call_id=1, 50 | is_root=True, 51 | is_create=False, 52 | code_hash=bytecode_hash, 53 | program_counter=0, 54 | stack_pointer=1024, 55 | gas_left=2, 56 | ), 57 | StepState( 58 | execution_state=ExecutionState.STOP, 59 | rw_counter=11, 60 | call_id=1, 61 | is_root=True, 62 | is_create=False, 63 | code_hash=bytecode_hash, 64 | program_counter=1, 65 | stack_pointer=1023, 66 | gas_left=0, 67 | ), 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /tests/evm/test_selfbalance.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | CallContextFieldTag, 11 | AccountFieldTag, 12 | RWDictionary, 13 | ) 14 | from zkevm_specs.util import Word, U256, U160 15 | from common import rand_address, rand_word 16 | 17 | 18 | TESTING_DATA = [(0, 0), (0, 10), (rand_address(), rand_word())] 19 | 20 | 21 | @pytest.mark.parametrize("callee_address, balance", TESTING_DATA) 22 | def test_selfbalance(callee_address: U160, balance: U256): 23 | bytecode = Bytecode().selfbalance() 24 | bytecode_hash = Word(bytecode.hash()) 25 | 26 | tables = Tables( 27 | block_table=Block(), 28 | tx_table=set(), 29 | bytecode_table=set(bytecode.table_assignments()), 30 | rw_table=set( 31 | RWDictionary(9) 32 | .call_context_read(1, CallContextFieldTag.CalleeAddress, Word(callee_address)) 33 | .account_read(callee_address, AccountFieldTag.Balance, Word(balance)) 34 | .stack_write(1, 1023, Word(balance)) 35 | .rws 36 | ), 37 | ) 38 | 39 | verify_steps( 40 | tables=tables, 41 | steps=[ 42 | StepState( 43 | execution_state=ExecutionState.SELFBALANCE, 44 | rw_counter=9, 45 | call_id=1, 46 | is_root=True, 47 | is_create=False, 48 | code_hash=bytecode_hash, 49 | program_counter=0, 50 | stack_pointer=1024, 51 | gas_left=5, 52 | ), 53 | StepState( 54 | execution_state=ExecutionState.STOP, 55 | rw_counter=12, 56 | call_id=1, 57 | is_root=True, 58 | is_create=False, 59 | code_hash=bytecode_hash, 60 | program_counter=1, 61 | stack_pointer=1023, 62 | gas_left=0, 63 | ), 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /tests/evm/test_gas.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | Block, 5 | Bytecode, 6 | ExecutionState, 7 | StepState, 8 | Tables, 9 | Transaction, 10 | verify_steps, 11 | RWDictionary, 12 | ) 13 | from zkevm_specs.util import Word 14 | from common import rand_range 15 | 16 | # Start with different values for `gas` before calling the `GAS` opcode. 17 | TESTING_DATA = tuple([i for i in range(2, 10)] + [rand_range(2**64) for i in range(0, 10)]) 18 | 19 | 20 | @pytest.mark.parametrize("gas", TESTING_DATA) 21 | def test_gas(gas: int): 22 | tx = Transaction() 23 | 24 | bytecode = Bytecode().gas().stop() 25 | bytecode_hash = Word(bytecode.hash()) 26 | 27 | # since the GAS opcode returns the value of available gas after deducting the cost 28 | # of calling the GAS opcode itself, we should expect gas_left = gas - 2 29 | gas_left = gas - 2 30 | 31 | tables = Tables( 32 | block_table=set(Block().table_assignments()), 33 | tx_table=set(tx.table_assignments()), 34 | bytecode_table=set(bytecode.table_assignments()), 35 | rw_table=set(RWDictionary(2).stack_write(1, 1023, Word(gas_left)).rws), 36 | ) 37 | 38 | verify_steps( 39 | tables=tables, 40 | steps=[ 41 | StepState( 42 | execution_state=ExecutionState.GAS, 43 | rw_counter=2, 44 | call_id=1, 45 | is_root=True, 46 | is_create=False, 47 | code_hash=bytecode_hash, 48 | program_counter=0, 49 | stack_pointer=1024, 50 | gas_left=gas, 51 | ), 52 | StepState( 53 | execution_state=ExecutionState.STOP, 54 | rw_counter=3, 55 | call_id=1, 56 | is_root=True, 57 | is_create=False, 58 | code_hash=bytecode_hash, 59 | program_counter=1, 60 | stack_pointer=1023, 61 | gas_left=gas_left, 62 | ), 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /tests/evm/test_byte.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from random import randrange 3 | from zkevm_specs.util import Word 4 | from zkevm_specs.evm_circuit import ( 5 | ExecutionState, 6 | StepState, 7 | verify_steps, 8 | Tables, 9 | Block, 10 | Bytecode, 11 | RWDictionary, 12 | ) 13 | 14 | 15 | def gen_test_data(): 16 | u256_max = 115792089237316195423570985008687907853269984665640564039457584007913129639935 17 | x = randrange(u256_max) 18 | return [(i, x, (x >> (248 - i * 8)) & 0xFF) for i in range(1, 32)] 19 | 20 | 21 | @pytest.mark.parametrize("a, b, c", gen_test_data()) 22 | def test_byte(a: int, b: int, c: int): 23 | a = Word(a) 24 | b = Word(b) 25 | c = Word(c) 26 | 27 | bytecode = Bytecode().byte(a, b).stop() 28 | bytecode_hash = Word(bytecode.hash()) 29 | 30 | tables = Tables( 31 | block_table=set(Block().table_assignments()), 32 | tx_table=set(), 33 | bytecode_table=set(bytecode.table_assignments()), 34 | rw_table=set( 35 | RWDictionary(9) 36 | .stack_read(1, 1022, a) 37 | .stack_read(1, 1023, b) 38 | .stack_write(1, 1023, c) 39 | .rws 40 | ), 41 | ) 42 | 43 | verify_steps( 44 | tables=tables, 45 | steps=[ 46 | StepState( 47 | execution_state=ExecutionState.BYTE, 48 | rw_counter=9, 49 | call_id=1, 50 | is_root=True, 51 | is_create=False, 52 | code_hash=bytecode_hash, 53 | program_counter=66, 54 | stack_pointer=1022, 55 | gas_left=3, 56 | ), 57 | StepState( 58 | execution_state=ExecutionState.STOP, 59 | rw_counter=12, 60 | call_id=1, 61 | is_root=True, 62 | is_create=False, 63 | code_hash=bytecode_hash, 64 | program_counter=67, 65 | stack_pointer=1023, 66 | gas_left=0, 67 | ), 68 | ], 69 | ) 70 | -------------------------------------------------------------------------------- /specs/error_state/ErrorOutOfGasSloadSstore.md: -------------------------------------------------------------------------------- 1 | # ErrorOutOfGasSloadSstore state for both SLOAD and SSTORE OOG errors 2 | 3 | ## Procedure 4 | 5 | Handle the corresponding out of gas errors for both `SLOAD` and `SSTORE` opcodes. 6 | 7 | ### EVM behavior 8 | 9 | For the current `go-ethereum` code, the out of gas error may occur for `constant gas` or `dynamic gas`. 10 | 11 | #### SLOAD gas cost 12 | 13 | For this gadget, SLOAD gas cost is calculated with EIP-2929 as: 14 | ``` 15 | if is_warm_access: 16 | gas_cost = GAS_COST_WARM_ACCESS # 100 17 | else: 18 | gas_cost = COLD_SLOAD_COST # 2100 19 | ``` 20 | 21 | #### SSTORE gas cost 22 | 23 | For this gadget, SSTORE gas cost is calculated with EIP-3529 as: 24 | ``` 25 | if value == value_prev: 26 | gas_cost = SLOAD_GAS # 100 27 | else: 28 | if value_prev == original_value: 29 | if original_value == 0: 30 | gas_cost = SSTORE_SET_GAS # 20000 31 | else: 32 | gas_cost = SSTORE_RESET_GAS # 2900 33 | else: 34 | gas_cost = SLOAD_GAS # 100 35 | 36 | if not is_warm_access: 37 | gas_cost += COLD_SLOAD_COST # 2100 38 | ``` 39 | 40 | #### SSTORE reentrancy sentry 41 | 42 | For SSTORE, the OOG error occurs when the gas left is less than or equal to `SSTORE_SENTRY` (2300). 43 | 44 | ### Constraints 45 | 46 | 1. For SLOAD, constrain `gas_left < gas_cost`. 47 | 2. For SSTORE, constrain `gas_left < gas_cost` or `gas_left <= SSTORE_SENTRY`. 48 | 3. Only for SSTORE, constrain `is_static == false`. 49 | 4. Current call must fail. 50 | 5. If it's a root call, it transits to `EndTx`. 51 | 6. If it isn't a root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 52 | 7. Constrain `rw_counter_end_of_reversion = rw_counter_end_of_step + reversible_counter`. 53 | 54 | ### Lookups 55 | 56 | 7 bus-mapping lookups for SLOAD and 9 for SSTORE: 57 | 58 | 1. 5 call context lookups for `tx_id`, `is_static`, `callee_address`, `is_success` and `rw_counter_end_of_reversion`. 59 | 2. 1 stack read for `storage_key`. 60 | 3. 1 account storage access list read. 61 | 4. Only for SSTORE, 1 stack read for `value_to_store`. 62 | 5. Only for SSTORE, 1 account storage read. 63 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/sha3.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import CopyDataTypeTag 3 | from zkevm_specs.util import FQ, GAS_COST_COPY_SHA3 4 | 5 | 6 | def sha3(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | # byte offset in memory. 10 | offset = instruction.stack_pop() 11 | # byte size to read in memory. 12 | size = instruction.stack_pop() 13 | # sha3 value pushed to stack. 14 | sha3_value = instruction.stack_push() 15 | 16 | # convert RLC encoded stack elements to FQ. 17 | memory_offset, length = instruction.memory_offset_and_length(offset, size) 18 | 19 | if instruction.is_zero(length) == FQ.zero(): 20 | copy_rwc_inc, rlc_acc = instruction.copy_lookup( 21 | instruction.curr.call_id, 22 | CopyDataTypeTag.Memory, 23 | instruction.curr.call_id, 24 | CopyDataTypeTag.RlcAcc, 25 | memory_offset, 26 | memory_offset + length, 27 | FQ.zero(), 28 | length, 29 | instruction.curr.rw_counter + instruction.rw_counter_offset, 30 | ) 31 | else: 32 | copy_rwc_inc, rlc_acc = FQ.zero(), FQ.zero() 33 | 34 | keccak256_output = instruction.keccak_lookup(length, rlc_acc) 35 | instruction.constrain_equal_word( 36 | keccak256_output, 37 | sha3_value, 38 | ) 39 | 40 | # calculate memory expansion gas costs. 41 | next_memory_size, memory_expansion_gas_cost = instruction.memory_expansion_dynamic_length( 42 | memory_offset, length 43 | ) 44 | gas_cost = instruction.memory_copier_gas_cost( 45 | length, memory_expansion_gas_cost, GAS_COST_COPY_SHA3 46 | ) 47 | 48 | instruction.step_state_transition_in_same_context( 49 | opcode, 50 | rw_counter=Transition.delta(instruction.rw_counter_offset + copy_rwc_inc), 51 | program_counter=Transition.delta(1), 52 | stack_pointer=Transition.delta(1), # 2 stack reads and 1 stack write 53 | memory_word_size=Transition.to(next_memory_size), 54 | dynamic_gas_cost=gas_cost, 55 | ) 56 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..util import FQ 4 | from .execution import EXECUTION_STATE_IMPL 5 | from .execution_state import ExecutionState 6 | from .instruction import Instruction 7 | from .step import StepState 8 | from .table import Tables 9 | 10 | 11 | DUMMY_STEP_STATE = StepState(ExecutionState.EndBlock, rw_counter=-1) 12 | 13 | 14 | def verify_steps( 15 | tables: Tables, 16 | steps: List[StepState], 17 | begin_with_first_step: bool = False, 18 | end_with_last_step: bool = False, 19 | success: bool = True, 20 | ): 21 | if end_with_last_step: 22 | steps.append(DUMMY_STEP_STATE) 23 | 24 | exception = None 25 | for idx, (curr, next) in enumerate(zip(steps, steps[1:])): 26 | try: 27 | verify_step( 28 | Instruction( 29 | tables=tables, 30 | curr=curr, 31 | next=next, 32 | is_first_step=begin_with_first_step and idx == 0, 33 | is_last_step=end_with_last_step and idx == len(steps) - 2, 34 | ) 35 | ) 36 | except AssertionError as e: 37 | exception = e 38 | break 39 | if success: 40 | if exception: 41 | raise exception 42 | assert exception is None 43 | else: 44 | assert exception is not None 45 | 46 | 47 | def verify_step(instruction: Instruction): 48 | if instruction.is_first_step: 49 | instruction.constrain_in( 50 | instruction.curr.execution_state, 51 | [FQ(ExecutionState.BeginTx), FQ(ExecutionState.EndBlock)], 52 | ) 53 | instruction.constrain_equal(instruction.curr.rw_counter, FQ(1)) 54 | 55 | if instruction.is_last_step: 56 | instruction.constrain_equal(instruction.curr.execution_state, ExecutionState.EndBlock) 57 | else: 58 | instruction.constrain_execution_state_transition() 59 | 60 | if instruction.curr.execution_state in EXECUTION_STATE_IMPL: 61 | EXECUTION_STATE_IMPL[instruction.curr.execution_state](instruction) 62 | else: 63 | raise NotImplementedError 64 | -------------------------------------------------------------------------------- /specs/opcode/12SLT_13SGT.md: -------------------------------------------------------------------------------- 1 | # SLT & SGT opcodes 2 | 3 | ## Procedure 4 | 5 | The `SLT` and `SGT` opcodes compare the top two values on the stack, and push the result (0 or 1) back to the stack. 6 | 7 | The stack inputs `a` and `b` are 256-bits signed integers with the most significant bit being the sign (1 for negative and 0 for positive). The 256-bits are in the two's complement form of representing signed integers. 8 | 9 | #### Circuit Behaviour 10 | 11 | The `SignedComparatorGadget` takes arguments `a: [u8; 32]`, `b: [u8; 32]` and `is_sgt: bool`. 12 | 13 | It returns the result of `a < b` where: 14 | 15 | - `Stack = [b, a]` if `is_sgt == false` 16 | - `Stack = [a, b]` if `is_sgt == true` 17 | 18 | We basically swap the stack inputs if `is_sgt == true`, so that we only need to compare `a < b` in our gadget. 19 | 20 | The gadget is constructed with the following logic: 21 | 22 | ```python 23 | # a < 0 and b >= 0 24 | if a[31] >= 128 and b[31] < 128: 25 | result = 1 26 | # b < 0 and a >= 0 27 | elif b[31] >= 128 and a[31] < 128: 28 | result = 0 29 | # (a < 0 and b < 0) or (a >= 0 and b >= 0) 30 | else: 31 | if a_hi < b_hi: 32 | result = 1 33 | elif a_hi == b_hi and a_lo < b_lo: 34 | result = 1 35 | else: 36 | result = 0 37 | ``` 38 | 39 | where: 40 | 41 | - `a[31]` and `b[31]` represent the most significant bytes of `a` and `b` respectively. 42 | - `a[31] >= 128` (same for `b`) signifies that `a` (same for `b`) is a negative number. 43 | - `a_hi = a[16..32]` and `a_lo = a[0..16]` (same for `b`) with `a` (same for `b`) being represented in the little-endian form. 44 | 45 | ## Constraints 46 | 47 | - `OpcodeId` check: 48 | - opId === OpcodeId(0x12) for `SLT` 49 | - opId === OpcodeId(0x13) for `SGT` 50 | - State Transition: 51 | - gc -> gc + 3 52 | - stack pointer -> stack pointer + 1 53 | - pc -> pc + 1 54 | - gas -> gas + 3 55 | - Lookups: 56 | - `a` is at the top of the stack 57 | - `b` is at the second position of the stack 58 | - `result` is the new top of the stack 59 | 60 | ## Exceptions 61 | 62 | 1. Stack underflow: `1023 <= stack pointer <= 1024` 63 | 2. Out of gas: gas left \< 3 64 | 65 | ## Code 66 | 67 | See [`slt_sgt.py`](src/zkevm_specs/evm/execution/slt_sgt.py) 68 | -------------------------------------------------------------------------------- /tests/evm/test_mulmod.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word 13 | 14 | MAXU256 = (2**256) - 1 15 | 16 | 17 | TESTING_DATA = [ 18 | (1, 1, 2), 19 | (1, 1, 0), 20 | (0, 2, 3), 21 | (MAXU256, MAXU256, MAXU256), 22 | (MAXU256, MAXU256, 1), 23 | (MAXU256, 1, MAXU256), 24 | (MAXU256, 2, 2), 25 | (0, 0, 0), 26 | ] 27 | 28 | 29 | @pytest.mark.parametrize("a, b, n", TESTING_DATA) 30 | def test_mulmod(a: int, b: int, n: int): 31 | if n == 0: 32 | r = Word(0) 33 | else: 34 | r = Word((a * b) % n) 35 | 36 | a = Word(a) 37 | b = Word(b) 38 | n = Word(n) 39 | 40 | bytecode = Bytecode().mulmod(a, b, n).stop() 41 | bytecode_hash = Word(bytecode.hash()) 42 | 43 | tables = Tables( 44 | block_table=set(Block().table_assignments()), 45 | tx_table=set(), 46 | bytecode_table=set(bytecode.table_assignments()), 47 | rw_table=set( 48 | RWDictionary(9) 49 | .stack_read(1, 1021, a) 50 | .stack_read(1, 1022, b) 51 | .stack_read(1, 1023, n) 52 | .stack_write(1, 1023, r) 53 | .rws 54 | ), 55 | ) 56 | 57 | verify_steps( 58 | tables=tables, 59 | steps=[ 60 | StepState( 61 | execution_state=ExecutionState.MULMOD, 62 | rw_counter=9, 63 | call_id=1, 64 | is_root=True, 65 | is_create=False, 66 | code_hash=bytecode_hash, 67 | program_counter=99, 68 | stack_pointer=1021, 69 | gas_left=8, 70 | ), 71 | StepState( 72 | execution_state=ExecutionState.STOP, 73 | rw_counter=13, 74 | call_id=1, 75 | is_root=True, 76 | is_create=False, 77 | code_hash=bytecode_hash, 78 | program_counter=100, 79 | stack_pointer=1023, 80 | gas_left=0, 81 | ), 82 | ], 83 | ) 84 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/returndatacopy.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..table import CallContextFieldTag, RW, CopyDataTypeTag 3 | from zkevm_specs.util import N_BYTES_MEMORY_WORD_SIZE 4 | 5 | 6 | def returndatacopy(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | memory_offset_word, offset_word, size_word = ( 10 | instruction.stack_pop(), 11 | instruction.stack_pop(), 12 | instruction.stack_pop(), 13 | ) 14 | 15 | last_callee_id = instruction.call_context_lookup(CallContextFieldTag.LastCalleeId) 16 | return_data_length = instruction.call_context_lookup( 17 | CallContextFieldTag.LastCalleeReturnDataLength, RW.Read 18 | ) 19 | return_data_offset = instruction.call_context_lookup( 20 | CallContextFieldTag.LastCalleeReturnDataOffset, RW.Read 21 | ) 22 | 23 | # out-of-bound-check 24 | instruction.range_check( 25 | return_data_length 26 | - (instruction.word_to_fq(offset_word, 8) + instruction.word_to_fq(size_word, 8)), 27 | N_BYTES_MEMORY_WORD_SIZE, 28 | ) 29 | 30 | memory_offset, size = instruction.memory_offset_and_length(memory_offset_word, size_word) 31 | next_memory_size, memory_expansion_gas_cost = instruction.memory_expansion_dynamic_length( 32 | memory_offset, size 33 | ) 34 | gas_cost = instruction.memory_copier_gas_cost(size, memory_expansion_gas_cost) 35 | 36 | copy_rwc_inc, _ = instruction.copy_lookup( 37 | last_callee_id, 38 | CopyDataTypeTag.Memory, 39 | instruction.curr.call_id, 40 | CopyDataTypeTag.Memory, 41 | return_data_offset, 42 | return_data_offset + size, 43 | memory_offset, 44 | size, 45 | instruction.curr.rw_counter + instruction.rw_counter_offset, 46 | ) 47 | 48 | assert copy_rwc_inc == size * 2 49 | instruction.step_state_transition_in_same_context( 50 | opcode, 51 | rw_counter=Transition.delta(instruction.rw_counter_offset + copy_rwc_inc), 52 | program_counter=Transition.delta(1), 53 | stack_pointer=Transition.delta(3), 54 | memory_word_size=Transition.to(next_memory_size), 55 | dynamic_gas_cost=gas_cost, 56 | ) 57 | -------------------------------------------------------------------------------- /tests/evm/test_addmod.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word 13 | 14 | MAXU256 = (2**256) - 1 15 | 16 | TESTING_DATA = [ 17 | (MAXU256, MAXU256, 0), 18 | (MAXU256, MAXU256, 1), 19 | (MAXU256, MAXU256, MAXU256), 20 | (MAXU256, 0, MAXU256), 21 | (MAXU256, 1, MAXU256), 22 | (MAXU256, 1, 1), 23 | (MAXU256, 0, 0), 24 | (0, 1, 0), 25 | (0, 0, 0), 26 | ] 27 | 28 | 29 | @pytest.mark.parametrize("a, b, n", TESTING_DATA) 30 | def test_addmod(a: int, b: int, n: int): 31 | if n == 0: 32 | r = Word(0) 33 | else: 34 | r = Word((a + b) % n) 35 | 36 | a = Word(a) 37 | b = Word(b) 38 | n = Word(n) 39 | 40 | bytecode = Bytecode().addmod(a, b, n).stop() 41 | bytecode_hash = Word(bytecode.hash()) 42 | 43 | tables = Tables( 44 | block_table=set(Block().table_assignments()), 45 | tx_table=set(), 46 | bytecode_table=set(bytecode.table_assignments()), 47 | rw_table=set( 48 | RWDictionary(9) 49 | .stack_read(1, 1021, a) 50 | .stack_read(1, 1022, b) 51 | .stack_read(1, 1023, n) 52 | .stack_write(1, 1023, r) 53 | .rws 54 | ), 55 | ) 56 | 57 | verify_steps( 58 | tables=tables, 59 | steps=[ 60 | StepState( 61 | execution_state=ExecutionState.ADDMOD, 62 | rw_counter=9, 63 | call_id=1, 64 | is_root=True, 65 | is_create=False, 66 | code_hash=bytecode_hash, 67 | program_counter=99, 68 | stack_pointer=1021, 69 | gas_left=8, 70 | ), 71 | StepState( 72 | execution_state=ExecutionState.STOP, 73 | rw_counter=13, 74 | call_id=1, 75 | is_root=True, 76 | is_create=False, 77 | code_hash=bytecode_hash, 78 | program_counter=100, 79 | stack_pointer=1023, 80 | gas_left=0, 81 | ), 82 | ], 83 | ) 84 | -------------------------------------------------------------------------------- /tests/evm/test_add_sub.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | Opcode, 7 | verify_steps, 8 | Tables, 9 | Block, 10 | Bytecode, 11 | RWDictionary, 12 | ) 13 | from zkevm_specs.util import Word 14 | from common import generate_nasty_tests, rand_word 15 | 16 | 17 | TESTING_DATA = [ 18 | (Opcode.ADD, 0x030201, 0x060504), 19 | (Opcode.SUB, 0x090705, 0x060504), 20 | (Opcode.ADD, rand_word(), rand_word()), 21 | (Opcode.SUB, rand_word(), rand_word()), 22 | ] 23 | 24 | generate_nasty_tests(TESTING_DATA, (Opcode.ADD, Opcode.SUB)) 25 | 26 | 27 | @pytest.mark.parametrize("opcode, a, b", TESTING_DATA) 28 | def test_add_sub(opcode: Opcode, a: int, b: int): 29 | c = Word((a + b if opcode == Opcode.ADD else a - b) % 2**256) 30 | a = Word(a) 31 | b = Word(b) 32 | 33 | bytecode = Bytecode().add(a, b).stop() if opcode == Opcode.ADD else Bytecode().sub(a, b).stop() 34 | bytecode_hash = Word(bytecode.hash()) 35 | 36 | tables = Tables( 37 | block_table=set(Block().table_assignments()), 38 | tx_table=set(), 39 | bytecode_table=set(bytecode.table_assignments()), 40 | rw_table=set( 41 | RWDictionary(9) 42 | .stack_read(1, 1022, a) 43 | .stack_read(1, 1023, b) 44 | .stack_write(1, 1023, c) 45 | .rws 46 | ), 47 | ) 48 | 49 | verify_steps( 50 | tables=tables, 51 | steps=[ 52 | StepState( 53 | execution_state=ExecutionState.ADD, 54 | rw_counter=9, 55 | call_id=1, 56 | is_root=True, 57 | is_create=False, 58 | code_hash=bytecode_hash, 59 | program_counter=66, 60 | stack_pointer=1022, 61 | gas_left=3, 62 | ), 63 | StepState( 64 | execution_state=ExecutionState.STOP, 65 | rw_counter=12, 66 | call_id=1, 67 | is_root=True, 68 | is_create=False, 69 | code_hash=bytecode_hash, 70 | program_counter=67, 71 | stack_pointer=1023, 72 | gas_left=0, 73 | ), 74 | ], 75 | ) 76 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/exp.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from zkevm_specs.util import FQ, GAS_COST_EXP_PER_BYTE, Word 3 | 4 | 5 | def exp(instruction: Instruction): 6 | opcode = instruction.opcode_lookup(True) 7 | 8 | base = instruction.stack_pop() 9 | exponent = instruction.stack_pop() 10 | exponentiation = instruction.stack_push() 11 | 12 | base_lo, base_hi = base.to_lo_hi() 13 | exponent_lo, exponent_hi = exponent.to_lo_hi() 14 | exponentiation_lo, exponentiation_hi = exponentiation.to_lo_hi() 15 | 16 | exponent_is_zero = instruction.is_zero(exponent_hi) * instruction.is_zero(exponent_lo) 17 | exponent_is_one = instruction.is_zero(exponent_hi) * instruction.is_equal(exponent_lo, FQ.one()) 18 | 19 | if exponent_is_zero == FQ.one(): 20 | instruction.constrain_equal(exponentiation_lo, FQ.one()) 21 | instruction.constrain_zero(exponentiation_hi) 22 | elif exponent_is_one == FQ.one(): 23 | instruction.constrain_equal(exponentiation_lo, base_lo) 24 | instruction.constrain_equal(exponentiation_hi, base_hi) 25 | else: 26 | base_limbs = base.to_64s() 27 | identifier = FQ(instruction.curr.rw_counter + instruction.rw_counter_offset) 28 | single_step = instruction.is_zero(exponent_hi) * instruction.is_equal(exponent_lo, FQ(2)) 29 | 30 | # lookup to enforce the is_first step 31 | res = instruction.exp_lookup(identifier, single_step, base_limbs, exponent) 32 | # lookup to enforce the is_last step 33 | int_res = instruction.exp_lookup(identifier, FQ.one(), base_limbs, Word((FQ(2), FQ.zero()))) 34 | # intermediary result should be base^2 35 | # constrain base * base + 0 == base^2 36 | instruction.mul_add_words(base, base, Word(0), int_res) 37 | 38 | # constrain exponentiation result to what we looked up from the exp table. 39 | instruction.constrain_equal_word(res, exponentiation) 40 | 41 | exponent_byte_size = instruction.byte_size(exponent) 42 | dynamic_gas_cost = GAS_COST_EXP_PER_BYTE * exponent_byte_size 43 | 44 | instruction.step_state_transition_in_same_context( 45 | opcode, 46 | program_counter=Transition.delta(1), 47 | rw_counter=Transition.delta(3), 48 | stack_pointer=Transition.delta(1), 49 | dynamic_gas_cost=dynamic_gas_cost, 50 | ) 51 | -------------------------------------------------------------------------------- /specs/opcode/09MULMOD.md: -------------------------------------------------------------------------------- 1 | # MULMOD opcode 2 | 3 | ## Procedure 4 | 5 | ### EVM behavior 6 | 7 | 8 | Pop 3 EVM words `a`, `b` and `N` from the stack. 9 | 10 | If `N` is 0: 11 | push 0 into the stack. 12 | else: 13 | compute `r= (a * b) mod N` and push `r` into the stack. 14 | 15 | *Note* 16 | All intermediate calculations of this operation are not subject to the 2^256 modulo. 17 | 18 | ### Circuit behavior 19 | 20 | The MulModGadget takes arguments: 21 | - `a: [u8;32]` 22 | - `b: [u8;32]`, 23 | - `r: [u8;32]`, 24 | - `N: [u8;32]`, 25 | and keeps 5 words for storing: 26 | - `a_reduced: [u8;32]` , 27 | - `k: [u8;32]`, 28 | - `e: [u8;32]`, 29 | - `d: [u8;32]` 30 | - `zero: [u8;32]`. 31 | 32 | 33 | Witness `a_reduced ← a` if `n!=0` else `a_reduced ← 0` 34 | Witness `(e, d) ← ( (a_reduced * b) % 2^256, (a_reduced * b ) // 2^256)` 35 | Witness `(r, k) ← ( (a_reduced * b) % N, (a_reduced * b ) // N)` 36 | 37 | 1. Check `a_reduced = a mod N`. 38 | which uses `ModGadget` that in turn checks: 39 | - Check the equality ` j * N + a_reduced == a ` 40 | - Check (`a_reduced = 0` and `N == 0`) or `a_reduced < N` 41 | 42 | 2. Check `r = a * b mod N` 43 | which uses 2 `MulAddWords512Gadget` to check: 44 | ` a_reduced * b = k * N + r` in 2 steps 45 | - `a_reduced * b + zero == d * 2^256 + e` 46 | - `k * N + r == d * 2^256 + e` 47 | 48 | 1 `IsZeroGadget` and 1 `LtWordsGadget` that check: 49 | `(r == 0 and N == 0)` or `(r < N)` 50 | 51 | 52 | #### Note 53 | 54 | The first step on the computation, reducing `a` mod `N`, is taken in order 55 | to prevent overflow in the factor `k`, which could happen for high values 56 | of `a` and `b` and low `N`. This steps ensures: 57 | 58 | $$ 59 | k \leq \frac{(a \text{ mod } n) \cdot b }{n } \leq \frac{ (n-1) \cdot b}{n} < b \leq MAXU256 60 | $$ 61 | 62 | ## Constraints 63 | 64 | 1. opcodeID checks 65 | opId == OpcodeId(0x09) 66 | 2. state transition: 67 | - gc + 4 68 | - stack_pointer +2 69 | - pc + 1 70 | - gas + 8 71 | 3. Lookups: 4 busmapping lookups 72 | - `a` is on top of the stack. 73 | - `b` is in the second position of the stack. 74 | - `N` is in the third position of the stack. 75 | - `r`, the result is on top of the new stack. 76 | 77 | 78 | ## Exceptions 79 | 80 | 1. stack undeflow: `1022 <= stack_pointer <= 1024`. 81 | 2. out of gas: Remaining gas is not enough. 82 | 83 | See `src/zkevm_specs/opcode/mulmod.py` 84 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/error_write_protection.py: -------------------------------------------------------------------------------- 1 | from zkevm_specs.evm_circuit.table import RW, CallContextFieldTag 2 | 3 | from ...util import FQ 4 | from ..instruction import Instruction 5 | from ..opcode import Opcode 6 | 7 | 8 | # There are some op codes which modify state. 9 | # There are `[SSTORE, CREATE, CREATE2, CALL, SELFDESTRUCT, LOG0, LOG1, LOG2, LOG3, LOG4]`. 10 | # When execution call context is read only (static call) and internal call, 11 | # these op codes running will encounter write protection error 12 | def error_write_protection(instruction: Instruction): 13 | # retrieve op code associated to oog constant error 14 | opcode = instruction.opcode_lookup(True) 15 | ( 16 | is_sstore, 17 | is_create, 18 | is_create2, 19 | is_call, 20 | is_selfdestruct, 21 | is_log0, 22 | is_log1, 23 | is_log2, 24 | is_log3, 25 | is_log4, 26 | ) = instruction.multiple_select( 27 | opcode, 28 | ( 29 | Opcode.SSTORE, 30 | Opcode.CREATE, 31 | Opcode.CREATE2, 32 | Opcode.CALL, 33 | Opcode.SELFDESTRUCT, 34 | Opcode.LOG0, 35 | Opcode.LOG1, 36 | Opcode.LOG2, 37 | Opcode.LOG3, 38 | Opcode.LOG4, 39 | ), 40 | ) 41 | 42 | # Spec 1. opcode must be [SSTORE, CREATE, CREATE2, CALL, SELFDESTRUCT, LOG0, LOG1, LOG2, LOG3, LOG4]. 43 | instruction.constrain_equal( 44 | is_sstore 45 | + is_create 46 | + is_create2 47 | + is_call 48 | + is_selfdestruct 49 | + is_log0 50 | + is_log1 51 | + is_log2 52 | + is_log3 53 | + is_log4, 54 | FQ(1), 55 | ) 56 | 57 | # Spec 2. 58 | # current call context must be readonly 59 | is_static = instruction.call_context_lookup(CallContextFieldTag.IsStatic) 60 | instruction.constrain_equal(is_static, FQ(1)) 61 | 62 | # Spec 3. for `CALL` op code, `value` must be non-zero 63 | if is_call == FQ(1): 64 | # the first 2 stacks are `gas`` and `callee_address` and the 3rd one is `value` 65 | value = instruction.stack_lookup(RW.Read, 2) 66 | instruction.constrain_not_zero_word(value) 67 | 68 | # There is one rw lookup in `constrain_error_state` 69 | instruction.constrain_error_state(instruction.rw_counter_offset + 1) 70 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/precompile.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from typing import Final, Dict, Tuple, List 3 | 4 | from ..util.param import * 5 | from .execution_state import ExecutionState 6 | 7 | 8 | class Precompile(IntEnum): 9 | ECRECOVER = 0x01 10 | SHA256 = 0x02 11 | RIPEMD160 = 0x03 12 | DATACOPY = 0x04 13 | BIGMODEXP = 0x05 14 | BN256ADD = 0x06 15 | BN256SCALARMUL = 0x07 16 | BN256PAIRING = 0x08 17 | BLAKE2F = 0x09 18 | 19 | def execution_state(self) -> ExecutionState: 20 | return PRECOMPILE_INFO_MAP[self].execution_state 21 | 22 | def base_gas_cost(self) -> int: 23 | return PRECOMPILE_INFO_MAP[self].base_gas 24 | 25 | 26 | class PrecompileInfo: 27 | """ 28 | Precompile information. 29 | """ 30 | 31 | base_gas: int 32 | execution_state: ExecutionState 33 | 34 | def __init__( 35 | self, 36 | base_gas: int, 37 | execution_state: ExecutionState, 38 | ) -> None: 39 | self.base_gas = base_gas 40 | self.execution_state = execution_state 41 | 42 | 43 | PRECOMPILE_INFO_MAP: Final[Dict[Precompile, PrecompileInfo]] = dict( 44 | { 45 | Precompile.ECRECOVER: PrecompileInfo(EcrecoverGas, ExecutionState.ECRECOVER), 46 | Precompile.SHA256: PrecompileInfo(Sha256BaseGas, ExecutionState.SHA256), 47 | Precompile.RIPEMD160: PrecompileInfo(Ripemd160BaseGas, ExecutionState.RIPEMD160), 48 | Precompile.DATACOPY: PrecompileInfo(IdentityBaseGas, ExecutionState.DATACOPY), 49 | Precompile.BIGMODEXP: PrecompileInfo(BigModExpBaseGas, ExecutionState.BIGMODEXP), 50 | Precompile.BN256ADD: PrecompileInfo(Bn256AddGas, ExecutionState.BN256_ADD), 51 | Precompile.BN256SCALARMUL: PrecompileInfo( 52 | Bn256ScalarMulGas, ExecutionState.BN256_SCALAR_MUL 53 | ), 54 | Precompile.BN256PAIRING: PrecompileInfo(Bn256PairingBaseGas, ExecutionState.BN256_PAIRING), 55 | Precompile.BLAKE2F: PrecompileInfo(Blake2fBaseGas, ExecutionState.BLAKE2F), 56 | } 57 | ) 58 | 59 | 60 | def valid_precompiles() -> List[Precompile]: 61 | return list(Precompile) 62 | 63 | 64 | def precompile_info_pairs() -> List[Tuple[ExecutionState, Precompile, int]]: 65 | pairs = [] 66 | for precompile in valid_precompiles(): 67 | pairs.append((precompile.execution_state(), precompile, precompile.base_gas_cost())) 68 | return pairs 69 | -------------------------------------------------------------------------------- /tests/evm/test_bitwise.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from random import randrange 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | Opcode, 7 | verify_steps, 8 | Tables, 9 | Block, 10 | Bytecode, 11 | RWDictionary, 12 | ) 13 | from zkevm_specs.util import Word 14 | 15 | 16 | def gen_test_data(): 17 | u256_max = 115792089237316195423570985008687907853269984665640564039457584007913129639935 18 | a = randrange(u256_max) 19 | b = randrange(u256_max) 20 | 21 | op_and = a & b 22 | op_or = a | b 23 | op_xor = a ^ b 24 | 25 | return [(Opcode.AND, a, b, op_and), (Opcode.OR, a, b, op_or), (Opcode.XOR, a, b, op_xor)] 26 | 27 | 28 | @pytest.mark.parametrize("opcode, a, b, c", gen_test_data()) 29 | def test_byte(opcode: Opcode, a: int, b: int, c: int): 30 | a = Word(a) 31 | b = Word(b) 32 | c = Word(c) 33 | 34 | bytecode = ( 35 | Bytecode().anD(a, b) 36 | if opcode == Opcode.AND 37 | else Bytecode().oR(a, b) 38 | if opcode == Opcode.OR 39 | else Bytecode().xor(a, b) 40 | ) 41 | bytecode_hash = Word(bytecode.hash()) 42 | 43 | tables = Tables( 44 | block_table=set(Block().table_assignments()), 45 | tx_table=set(), 46 | bytecode_table=set(bytecode.table_assignments()), 47 | rw_table=set( 48 | RWDictionary(9) 49 | .stack_read(1, 1022, a) 50 | .stack_read(1, 1023, b) 51 | .stack_write(1, 1023, c) 52 | .rws 53 | ), 54 | ) 55 | 56 | verify_steps( 57 | tables=tables, 58 | steps=[ 59 | StepState( 60 | execution_state=ExecutionState.BITWISE, 61 | rw_counter=9, 62 | call_id=1, 63 | is_root=True, 64 | is_create=False, 65 | code_hash=bytecode_hash, 66 | program_counter=66, 67 | stack_pointer=1022, 68 | gas_left=3, 69 | ), 70 | StepState( 71 | execution_state=ExecutionState.STOP, 72 | rw_counter=12, 73 | call_id=1, 74 | is_root=True, 75 | is_create=False, 76 | code_hash=bytecode_hash, 77 | program_counter=67, 78 | stack_pointer=1023, 79 | gas_left=0, 80 | ), 81 | ], 82 | ) 83 | -------------------------------------------------------------------------------- /tests/evm/test_blockhash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from zkevm_specs.evm_circuit import ( 4 | ExecutionState, 5 | StepState, 6 | verify_steps, 7 | Tables, 8 | Block, 9 | Bytecode, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, U64, U256, Sequence, keccak256 13 | 14 | TESTING_DATA = [ 15 | # valid range 16 | (3, [keccak256(bytes(i)) for i in range(3)], 1, True), 17 | (261, [keccak256(bytes(i)) for i in range(5, 261)], 260, True), 18 | # invalid range 19 | (3, [keccak256(bytes(i)) for i in range(3)], 4, False), 20 | (258, [keccak256(bytes(i)) for i in range(256)], 1, False), 21 | ] 22 | 23 | 24 | @pytest.mark.parametrize("current_number, history_hashes, block_number, is_valid", TESTING_DATA) 25 | def test_blockhash( 26 | current_number: U64, history_hashes: Sequence[U256], block_number: U64, is_valid: bool 27 | ): 28 | block = Block(number=current_number, history_hashes=history_hashes) 29 | bytecode = Bytecode().blockhash() 30 | 31 | bytecode_hash = Word(bytecode.hash()) 32 | 33 | result = keccak256(bytes(block_number)) if is_valid else 0 34 | 35 | call_id = 1 36 | rw_table = set( 37 | RWDictionary(8) 38 | .stack_read(call_id, 1023, Word(block_number)) 39 | .stack_write(call_id, 1023, Word(result)) 40 | .rws 41 | ) 42 | 43 | tables = Tables( 44 | block_table=set(block.table_assignments()), 45 | tx_table=set(), 46 | bytecode_table=set(bytecode.table_assignments()), 47 | rw_table=rw_table, 48 | ) 49 | 50 | verify_steps( 51 | tables=tables, 52 | steps=[ 53 | StepState( 54 | execution_state=ExecutionState.BLOCKHASH, 55 | rw_counter=8, 56 | call_id=1, 57 | is_root=True, 58 | is_create=False, 59 | code_hash=bytecode_hash, 60 | program_counter=0, 61 | stack_pointer=1023, 62 | gas_left=20, 63 | ), 64 | StepState( 65 | execution_state=ExecutionState.STOP, 66 | rw_counter=10, 67 | call_id=1, 68 | is_root=True, 69 | is_create=False, 70 | code_hash=bytecode_hash, 71 | program_counter=1, 72 | stack_pointer=1023, 73 | gas_left=0, 74 | ), 75 | ], 76 | ) 77 | -------------------------------------------------------------------------------- /specs/opcode/37CALLDATACOPY.md: -------------------------------------------------------------------------------- 1 | # CALLDATACOPY opcode 2 | 3 | ## Procedure 4 | 5 | The `CALLDATACOPY` opcode pops `memory_offset`, `data_offset`, and `length` from the stack. 6 | It then copies `length` bytes from call data buffer at the offset `data_offset` to the memory at the 7 | address `memory_offset`. EVM also pads 0 to the end of call data buffer in the case of out-of-bound 8 | access to the call data buffer. 9 | 10 | 1. A const gas cost `3 gas` 11 | 2. A dynamic cost: the memory expansion cost and copier cost in terms of the number of 12 | words copied. Note that when `length = 0`, the memory expansion cost is 0 regardless of 13 | `memory_offset` value. 14 | 15 | ## Circuit behaviour 16 | 17 | Because the `length` is dynamic, it requires multiple step slots to fully verify the `CALLDATACOPY`. 18 | In the `CALLDATACOPY` circuit, it only constrains the stack pops, state transition, and lookups to 19 | retrieve the additional information such as tx id, call data offset, etc. 20 | Then the gadget transits to an internal state called `CopyToMemory`, which can loop itself for 21 | copying data to the memory if `length > 0`. 22 | 23 | ## Constraints 24 | 25 | 1. opId = 0x37 26 | 2. State transition: 27 | - rw_counter 28 | - +5 for root calls (3 stack read, 2 call context reads) 29 | - +6 for internal calls (3 stack read, 3 call context reads) 30 | - stack_pointer + 3 31 | - pc + 1 32 | - gas + 3 + dynamic cost (memory expansion and copier cost when `length > 0`) 33 | - memory_size 34 | - `max(prev_memory_size, (memory_offset + length + 31) / 32)` if `length > 0` 35 | - `prev_memory_size` if `length = 0`. 36 | 3. Lookups: 5 (root calls) or 6 (internal calls) 37 | - `memory_offset` is at the top of the stack 38 | - `data_offset` is at the second position of the stack 39 | - `length` is at the third position of the stack 40 | - for root calls (`is_root = 1`): 41 | - `tx_id` is in the rw table (call context) 42 | - `call_data_length` is in the rw table (call context) 43 | - for internal calls (`is_root = 0`): 44 | - `caller_id` is in the rw table (call context) 45 | - `call_data_length` is in the rw table (call context) 46 | - `call_data_offset` is in the rw table (call context) 47 | 48 | ## Exceptions 49 | 50 | 1. stack underflow: `1021 <= stack_pointer <= 1024` 51 | 2. out of gas: remaining gas is not enough 52 | 53 | ## Code 54 | 55 | Please refer to `src/zkevm_specs/evm/execution/calldatacopy.py`. 56 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/addmod.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction, Transition 2 | from ..opcode import Opcode 3 | from zkevm_specs.util import FQ, Word 4 | 5 | 6 | # Returns 1 when a is lower than b, 0 otherwise 7 | def lt_u256(instruction: Instruction, a: Word, b: Word) -> FQ: 8 | # decode RLC to bytes for a and b 9 | a_lo, a_hi = a.to_lo_hi() 10 | b_lo, b_hi = b.to_lo_hi() 11 | 12 | a_lt_b_lo, _ = instruction.compare(a_lo, b_lo, 16) 13 | a_lt_b_hi, a_eq_b_hi = instruction.compare(a_hi, b_hi, 16) 14 | 15 | a_lt_b = instruction.select( 16 | a_lt_b_hi, FQ(1), instruction.select(a_eq_b_hi * a_lt_b_lo, FQ(1), FQ(0)) 17 | ) 18 | 19 | return a_lt_b 20 | 21 | 22 | def addmod(instruction: Instruction): 23 | opcode = instruction.opcode_lookup(True) 24 | instruction.constrain_equal(opcode, Opcode.ADDMOD) 25 | 26 | a = instruction.stack_pop() 27 | b = instruction.stack_pop() 28 | n = instruction.stack_pop() 29 | pushed_r = instruction.stack_push() 30 | 31 | # witness 32 | if n.int_value() == 0: 33 | a_reduced = a.int_value() 34 | k = 0 35 | d = 0 36 | r = Word((a_reduced + b.int_value()) % (2**256)) 37 | else: 38 | a_reduced = a.int_value() % n.int_value() 39 | k = a.int_value() // n.int_value() 40 | d = (a_reduced + b.int_value()) // n.int_value() 41 | r = pushed_r 42 | 43 | # check a == a_reduced + k * n 44 | overflow = instruction.mul_add_words(Word(k), n, Word(a_reduced), a) 45 | instruction.constrain_zero(overflow) 46 | 47 | # check a_reduced + b ≡ d * n + r in 512 bit space 48 | a_reduced_plus_b, overflow = instruction.add_words([Word(a_reduced), b]) 49 | instruction.mul_add_words_512( 50 | Word(d), n, r, Word.from_lo(overflow) if n.int_value() > 0 else Word(0), a_reduced_plus_b 51 | ) 52 | 53 | # check that r= 256 always returns the same value 17 | is_msb_sum_zero = instruction.is_zero(FQ(sum(index_le_bytes[1:32]))) 18 | sign_byte = ( 19 | FQ((value_le_bytes[index_le_bytes[0].n].n >> 7) * 0xFF) 20 | if index_le_bytes[0].n < 31 21 | else FQ(0) 22 | ) 23 | selectors = [FQ(index_le_bytes[0].n >= i) for i in range(31)] 24 | is_byte_selected = [FQ(index_le_bytes[0].n == i) for i in range(31)] 25 | 26 | # Check byte per byte to see if the byte was selected. 27 | # We're only directly checking the LSB byte 28 | # of the index here, so also make sure the byte 29 | # is only copied when index < 256. 30 | # There is no need to check the MSB, even if the MSB is selected 31 | # no bytes need to be changed (so this loops only up to 31). 32 | selected_byte = FQ(0) 33 | for i in range(31): 34 | is_selected = is_byte_selected[i] * is_msb_sum_zero 35 | selected_byte += value_le_bytes[i] * is_selected 36 | # Verify the selector 37 | instruction.is_equal(is_selected + (selectors[i - 1] if i > 0 else FQ(0)), selectors[i]) 38 | 39 | # Lookup the sign byte which will be used for doing the extending 40 | instruction.sign_byte_lookup(selected_byte, sign_byte) 41 | 42 | # Byte 0 always remains the same. 43 | # All other bytes need to be changed to the sign byte when the selector is enabled. 44 | # When a byte was selected all the **following** bytes need to be replaced, 45 | # (hence the `selectors[i-1]`). 46 | for idx in range(32): 47 | if idx == 0: 48 | instruction.is_equal(FQ(result_le_bytes[idx]), FQ(value_le_bytes[idx])) 49 | else: 50 | instruction.is_equal( 51 | FQ(result_le_bytes[idx]), 52 | sign_byte if selectors[idx - 1] == FQ(1) else FQ(value_le_bytes[idx]), 53 | ) 54 | 55 | instruction.step_state_transition_in_same_context( 56 | opcode, 57 | rw_counter=Transition.delta(3), 58 | program_counter=Transition.delta(1), 59 | stack_pointer=Transition.delta(1), 60 | ) 61 | -------------------------------------------------------------------------------- /tests/evm/test_signextend.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import random 3 | from common import rand_word 4 | from zkevm_specs.evm_circuit import ( 5 | Bytecode, 6 | ExecutionState, 7 | StepState, 8 | Tables, 9 | verify_steps, 10 | RWDictionary, 11 | ) 12 | from zkevm_specs.util import Word, U256 13 | 14 | 15 | def generate_tests_data(): 16 | test_data = [] 17 | for _ in range(32): 18 | byte_num = random.randint(0, 32) 19 | value = rand_word() 20 | if byte_num > 31: 21 | test_data.append((byte_num, value, value)) 22 | else: 23 | value_bytes = value.to_bytes(32, "big") 24 | value_bytes = value_bytes[31 - int(byte_num) :] 25 | sign_bit = value_bytes[0] >> 7 26 | if sign_bit == 0: 27 | test_data.append((byte_num, value, int.from_bytes(value_bytes, "big"))) 28 | else: 29 | num_bytes_prepend = 32 - (byte_num + 1) 30 | result = int.from_bytes(bytearray([0xFF] * num_bytes_prepend) + value_bytes, "big") 31 | test_data.append((byte_num, value, result)) 32 | return test_data 33 | 34 | 35 | @pytest.mark.parametrize("b, x, y", generate_tests_data()) 36 | def test_pop(b: U256, x: U256, y: U256): 37 | bytecode = Bytecode().signextend(b, x).stop() 38 | bytecode_hash = Word(bytecode.hash()) 39 | 40 | tables = Tables( 41 | block_table=set(), 42 | tx_table=set(), 43 | bytecode_table=set(bytecode.table_assignments()), 44 | rw_table=set( 45 | RWDictionary(1) 46 | .stack_read(1, 1022, Word(b)) 47 | .stack_read(1, 1023, Word(x)) 48 | .stack_write(1, 1023, Word(y)) 49 | .rws 50 | ), 51 | ) 52 | 53 | verify_steps( 54 | tables=tables, 55 | steps=[ 56 | StepState( 57 | execution_state=ExecutionState.SIGNEXTEND, 58 | rw_counter=1, 59 | call_id=1, 60 | is_root=True, 61 | is_create=False, 62 | code_hash=bytecode_hash, 63 | program_counter=66, 64 | stack_pointer=1022, 65 | gas_left=5, 66 | ), 67 | StepState( 68 | execution_state=ExecutionState.STOP, 69 | rw_counter=4, 70 | call_id=1, 71 | is_root=True, 72 | is_create=False, 73 | code_hash=bytecode_hash, 74 | program_counter=67, 75 | stack_pointer=1023, 76 | gas_left=0, 77 | ), 78 | ], 79 | ) 80 | -------------------------------------------------------------------------------- /specs/opcode/3eRETURNDATACOPY.md: -------------------------------------------------------------------------------- 1 | # RETURNDATACOPY opcode 2 | 3 | ## Procedure 4 | 5 | The `RETURNDATACOPY` opcode pops `dest_offset`, `offset` and `size` from the stack. 6 | It then copies `size` bytes of return data in the current environment from `return_data_offset` at an `offset` to the memory at address `dest_offset`. For out-of-bound scenarios where `size > return_data_size - offset`, EVM reverts the current context. 7 | 8 | The gas cost of `RETURNDATACOPY` opcode consists of two parts: 9 | 10 | 1. A constant gas cost: `3 gas` 11 | 2. A dynamic gas cost: cost of memory expansion and copying (variable depending on the `size` copied to memory) 12 | 13 | ### EVM behaviour 14 | 15 | The `RETURNDATACOPY` opcode copies output data from the previous call to memory. If there is no previous call, the length of `RETURNDATACOPY` is bounded to 0 (returndatasize). Evm checks gas, stack and length boundary when copying, and reverts context if any exception happens. 16 | 17 | ### Circuit behaviour 18 | 19 | 1. Do a busmapping lookup for a stack read of dest_offset. 20 | 2. Do a busmapping lookup for a stack read of offset. 21 | 3. Do a busmapping lookup for a stack read of size. 22 | 4. Do a busmapping lookup for a CallContext read of last Callee's ID. 23 | 5. Do a busmapping lookup for a CallContext read of last Callee's ReturnDataLength. 24 | 6. Do a busmapping lookup for a CallContext read of last Callee's ReturnDataOffset. 25 | 7. Do a CopyTable lookup to verify the copy of bytes. The copy of a dynamic number of bytes is verified by the CopyCircuit outside the `RETURNDATACOPY` gadget. 26 | 27 | ## Constraints 28 | 29 | 1. opId == 0x3E 30 | 2. State transition: 31 | - rw_counter -> rw_counter + 3 (stack read) + 3 (last callee id & return data param read) + size * 2 (copy == 1 mem read + 1 mem write) 32 | - stack pointer + 3 33 | - pc + 1 34 | - gas -> dynamic gas cost 35 | - memory_size 36 | - `prev_memory_size` if `size = 0` 37 | - `max(prev_memory_size, (memory_offset + size + 31) / 32)` if `size > 0` 38 | 3. Lookups: 6 39 | - `memory_offset` is at the 1st position of the stack 40 | - `data_offset` is at the 2nd position of the stack 41 | - `size` is at the 3rd position of the stack 42 | - `last_callee_id` is in last callee context 43 | - `return_data_offset` is in last callee context 44 | - `return_data_size` is in last callee context 45 | 46 | ## Exceptions 47 | 48 | 1. stack underflow: `1022 <= stack_pointer <= 1024` 49 | 2. out of gas: remaining gas is not enough 50 | 3. copy overflow: data_offset + size > return_data_size 51 | 52 | ## Code 53 | 54 | Please refer to [returndatacopy.py](src/zkevm_specs/evm/execution/returndatacopy.py). 55 | -------------------------------------------------------------------------------- /specs/error_state/ErrorCodeStore.md: -------------------------------------------------------------------------------- 1 | # ErrorCodeStore state 2 | 3 | ## Procedure 4 | `ErrorCodeStore` is for two original code store related errors: `CodeStoreOutOfGas` and 5 | `MaxCodeSizeExceeded`. 6 | this type of error only occurs when executing create(create,create2) op code or tx deploy 7 | transaction(tx.to = null). 8 | 9 | ### EVM behavior 10 | When handling a CREATE-kind transaction, Initial bytecode opcodes will run and current call context is created. 11 | The final bytecode opcodes to store for new contract is the `RETURN` opcode of init codes result. 12 | 13 | `RETURN` opcode returns memory [`offset`...`offset` + `length`] content as bytecodes to store into state db. 14 | for returned bytecodes, store them cost additional gas. 15 | 16 | `let CODE_DEPOSIT_BYTE_COST = 200 17 | code_store_cost = CODE_DEPOSIT_BYTE_COST * bytecodes' length 18 | ` 19 | 20 | if `code_store_cost` > gas left, it is `CodeStoreOutOfGas` case. 21 | if returned bytecodes' length > `MAXCODESIZE` allowed in evm, it is 22 | `MaxCodeSizeExceeded` case. 23 | 24 | In circuit bus mapping side, check these two code store errors in [here](https://github.com/privacy-scaling-explorations/zkevm-circuits/blob/main/bus-mapping/src/circuit_input_builder/input_state_ref.rs#L1148&L1155) 25 | when executing opcode is `RETURN` and call context is creating(`call.is_create == true`) meanwhile. 26 | 27 | Even though errors occur in `CREATE` kind opcodes, it is special not checking error 28 | in executing opcode `CREATE` directly. 29 | Circuit implementation takes similar strategy, not constrain error directly in CREATE opcodes, but 30 | in `RETURN` step context. Following this way it easy to get the key property state `length` and construct constraints against it. 31 | 32 | Overall it looks like the following: 33 | - Pop EVM word `offset` and `length` from the stack, 34 | - Go to `ErrorCodeStore` state when call context is being created & 35 | select which one of the followings occurs: 36 | 37 | 1. Storing `length` of bytecodes runs out of gas. 38 | 2. `length` of bytecodes exceeds `MAXCODESIZE`. 39 | 40 | ### Constraints 41 | 1. `code_store_cost` > gas_left or `length` > `MAXCODESIZE` 42 | 2. Current call must be failed. 43 | 3. Current call is not static call. 44 | 4. If it's a root call, it transits to `EndTx` 45 | 5. if it is not root call, it restores caller's context by reading to `rw_table`, then does step state transition to it. 46 | 47 | ### Lookups 48 | - Byte code lookup. 49 | - stack reads for `offset` and `length`. 50 | - call context lookups for `is_success` and `rw_counter_end_of_reversion`. 51 | - Restore context lookups for non-root call. 52 | 53 | ## Code 54 | TODO: add after circuit merge first. -------------------------------------------------------------------------------- /specs/opcode/1bSHL_1cSHR.md: -------------------------------------------------------------------------------- 1 | # SHL and SHR opcodes 2 | 3 | ## Procedure 4 | 5 | ### EVM behavior 6 | 7 | Pop two EVM words `shift` and `a` from the stack, and push `b` to the stack, where `b` is computed as 8 | 9 | - for opcode SHL, `shift` is a number of bits to shift to the left, compute `b = (a * 2^shift) % 2^256` when `shift < 256` otherwise `b = 0` 10 | - for opcode SHR, `shift` is a number of bits to shift to the right, compute `b = a // 2^shift` when `shift < 256` otherwise `b = 0` 11 | 12 | ### Circuit behavior 13 | 14 | To prove the SHL and SHR opcodes, we first construct a `MulAddWordsGadget` that proves `quotient * divisor + remainder = dividend (% 2^256)` where quotient, divisor, remainder and dividend are all 256-bit words. Reference `02MUL_04DIV_06MOD.md` for details about `MulAddWordsGadget`. 15 | 16 | Based on different opcode cases, we constrain the stack pops and pushes as follows 17 | 18 | - for opcode SHL, two stack pops are shift and quotient, when `divisor = 2^shift` if `shift < 256` and 0 otherwise. The stack push is dividend if `shift < 256` and 0 otherwise. 19 | - for opcode SHR, two stack pops are shift and dividend, when `divisor = 2^shift` if `shift < 256` and 0 otherwise. The stack push is quotient if `shift < 256` and 0 otherwise. 20 | 21 | The opcode circuit also adds some extra constraints: 22 | 23 | - contrain `shift == shift.cells[0]` when `divisor != 0`. 24 | - use a `LtWordGadget` to constrain `remainder < divisor` when `divisor != 0`. 25 | - if the opcode is SHL, constrain `remainder == 0`. 26 | - if the opcode is SHR, constrain `overflow == 0` in `MulAddWordsGadget`. 27 | 28 | ## Constraints 29 | 30 | 1. opcodeId checks 31 | - opId === OpcodeId(0x1b) for SHL 32 | - opId === OpcodeId(0x1c) for SHR 33 | 2. state transition: 34 | - gc + 3 35 | - stack_pointer + 1 36 | - pc + 1 37 | - gas + 3 38 | 3. Lookups: 1 pow2 lookup + 3 busmapping lookups 39 | - divisor lookup in pow2 table (where 0≤shf0<256) 40 | - when `shf0 < 128`, constrain `divisor_lo == 2^shf0`. 41 | - when `shf0 >= 128`, constrain `divisor_hi == 2^(shf0 - 128)`. 42 | - top of the stack 43 | - when opcode is SHL, quotient is at the top of the stack. 44 | - when opcode is SHR, dividend is at the top of the stack. 45 | - shift is at the second position of the stack when `divisor = 2^shift`. 46 | - new top of the stack 47 | - when opcode is SHL, dividend is at the new top of the stack. 48 | - when opcode is SHR, quotient is at the new top of the stack if `divisor != 0` otherwise 0. 49 | 50 | ## Exceptions 51 | 52 | 1. stack underflow: `1023 <= stack_pointer <= 1024` 53 | 2. out of gas: remaining gas is not enough 54 | 55 | ## Code 56 | 57 | See `src/zkevm_specs/evm/execution/shl_shr.py` 58 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/calldatacopy.py: -------------------------------------------------------------------------------- 1 | from ...util import N_BYTES_MEMORY_ADDRESS, FQ, Expression 2 | from ..instruction import Instruction, Transition 3 | from ..table import RW, CallContextFieldTag, CopyDataTypeTag 4 | 5 | 6 | def calldatacopy(instruction: Instruction): 7 | opcode = instruction.opcode_lookup(True) 8 | 9 | memory_offset_word = instruction.stack_pop() 10 | data_offset_word = instruction.stack_pop() 11 | length_word = instruction.stack_pop() 12 | 13 | # convert rlc to FQ 14 | memory_offset, length = instruction.memory_offset_and_length(memory_offset_word, length_word) 15 | data_offset = instruction.word_to_fq(data_offset_word, N_BYTES_MEMORY_ADDRESS) 16 | 17 | if instruction.curr.is_root: 18 | src_id = instruction.call_context_lookup(CallContextFieldTag.TxId, RW.Read) 19 | call_data_length = instruction.call_context_lookup( 20 | CallContextFieldTag.CallDataLength, RW.Read 21 | ) 22 | call_data_offset: Expression = FQ.zero() 23 | else: 24 | src_id = instruction.call_context_lookup(CallContextFieldTag.CallerId, RW.Read) 25 | call_data_length = instruction.call_context_lookup( 26 | CallContextFieldTag.CallDataLength, RW.Read 27 | ) 28 | call_data_offset = instruction.call_context_lookup( 29 | CallContextFieldTag.CallDataOffset, RW.Read 30 | ) 31 | 32 | next_memory_size, memory_expansion_gas_cost = instruction.memory_expansion_dynamic_length( 33 | memory_offset, length 34 | ) 35 | gas_cost = instruction.memory_copier_gas_cost(length, memory_expansion_gas_cost) 36 | 37 | src_tag = instruction.select( 38 | FQ(instruction.curr.is_root), FQ(CopyDataTypeTag.TxCalldata), FQ(CopyDataTypeTag.Memory) 39 | ) 40 | if instruction.is_zero(length) == 0: 41 | copy_rwc_inc, _ = instruction.copy_lookup( 42 | src_id, 43 | CopyDataTypeTag(src_tag.n), 44 | instruction.curr.call_id, 45 | CopyDataTypeTag.Memory, 46 | call_data_offset.expr() + data_offset.expr(), 47 | call_data_offset.expr() + call_data_length.expr(), 48 | memory_offset, 49 | length, 50 | instruction.curr.rw_counter + instruction.rw_counter_offset, 51 | ) 52 | else: 53 | copy_rwc_inc = FQ(0) 54 | 55 | instruction.step_state_transition_in_same_context( 56 | opcode, 57 | rw_counter=Transition.delta(instruction.rw_counter_offset + copy_rwc_inc), 58 | program_counter=Transition.delta(1), 59 | stack_pointer=Transition.delta(3), 60 | memory_word_size=Transition.to(next_memory_size), 61 | dynamic_gas_cost=gas_cost, 62 | ) 63 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/execution/dataCopy.py: -------------------------------------------------------------------------------- 1 | from ..instruction import Instruction 2 | from ..table import ( 3 | CallContextFieldTag, 4 | FixedTableTag, 5 | RW, 6 | CopyDataTypeTag, 7 | ) 8 | from ...util import FQ, IdentityBaseGas, IdentityPerWordGas 9 | 10 | 11 | def dataCopy(instruction: Instruction): 12 | address_word = instruction.call_context_lookup_word(CallContextFieldTag.CalleeAddress) 13 | address = instruction.word_to_address(address_word) 14 | instruction.fixed_lookup( 15 | FixedTableTag.PrecompileInfo, 16 | FQ(instruction.curr.execution_state), 17 | address, 18 | FQ(IdentityBaseGas), 19 | ) 20 | 21 | caller_id = instruction.call_context_lookup(CallContextFieldTag.CallerId, RW.Read) 22 | call_data_offset = instruction.call_context_lookup(CallContextFieldTag.CallDataOffset, RW.Read) 23 | call_data_length = instruction.call_context_lookup(CallContextFieldTag.CallDataLength, RW.Read) 24 | return_data_offset = instruction.call_context_lookup( 25 | CallContextFieldTag.ReturnDataOffset, RW.Read 26 | ) 27 | return_data_length = instruction.call_context_lookup( 28 | CallContextFieldTag.ReturnDataLength, RW.Read 29 | ) 30 | 31 | # Copy current call data to return data 32 | size = call_data_length 33 | 34 | gas_cost = FQ(IdentityBaseGas) + instruction.memory_copier_gas_cost( 35 | call_data_length, FQ(0), IdentityPerWordGas 36 | ) 37 | 38 | copy_rwc_inc, _ = instruction.copy_lookup( 39 | caller_id, 40 | CopyDataTypeTag.Memory, 41 | caller_id, 42 | CopyDataTypeTag.Memory, 43 | call_data_offset, 44 | call_data_offset.expr() + size, 45 | return_data_offset, 46 | return_data_offset.expr() + return_data_length, 47 | instruction.curr.rw_counter + instruction.rw_counter_offset, 48 | ) 49 | 50 | # Copy current call data to next call context memory 51 | copy_rwc_inc, _ = instruction.copy_lookup( 52 | caller_id, 53 | CopyDataTypeTag.Memory, 54 | instruction.curr.call_id, 55 | CopyDataTypeTag.Memory, 56 | call_data_offset, 57 | call_data_offset.expr() + size, 58 | FQ(0), 59 | return_data_length, 60 | instruction.curr.rw_counter + instruction.rw_counter_offset + copy_rwc_inc, 61 | ) 62 | instruction.rw_counter_offset += 4 * int(size.expr()) 63 | 64 | # Restore caller state to next StepState 65 | instruction.step_state_transition_to_restored_context( 66 | rw_counter_delta=instruction.rw_counter_offset, 67 | return_data_offset=FQ(0), 68 | return_data_length=size, 69 | gas_left=instruction.curr.gas_left - gas_cost, 70 | caller_id=caller_id, 71 | ) 72 | -------------------------------------------------------------------------------- /specs/opcode/A0_A4LOG.md: -------------------------------------------------------------------------------- 1 | # Log op codes 2 | 3 | ## Procedure 4 | 5 | there are five log related op codes of evm as below: 6 | 7 | | id | name | comment | 8 | | --- | -----| ------- | 9 | | A0 | LOG0 | Append log record with no topics | 10 | | A1 | LOG1 | Append log record with one topic | 11 | | A2 | LOG2 | Append log record with two topics | 12 | | A3 | LOG3 | Append log record with three topics | 13 | | A4 | LOG4 | Append log record with four topics | 14 | 15 | denote `LOGN` where `N` in `[0,4]` meaning topic count. 16 | 17 | ## EVM behavior 18 | 19 | 1. LogN first pops two elements `mStart`, `mSize` from stack, then 20 | pops `N` topics from stack. 21 | 2. Copy memory data from `mStart` address with `mSize` length 22 | 3. Construct log entry and append to log collections of state 23 | 24 | ## Circuit behavior 25 | 26 | 1. constrain `mStart`, `mSize` pop from stack 27 | 2. constrain contract address in CallContext & TxLog 28 | 3. constrain call context is not static call 29 | 4. constrain topics pop from stack and add into TxLog topic field, topic count is correct 30 | 5. constrain memory data \[`mStart`, `mStart` + `mSize`\], copied to tx log data field, 31 | it takes use of one or more `CopyToLog` inner gadgets as one `CopyToLog` gadget can only handle fixed 32 | amount of bytes. 33 | 34 | ## Constraints 35 | 36 | 1. opId = 0xA0...0xA4 37 | 38 | 2. State transition: 39 | 40 | - gc : 41 | when is_persistent = false, 42 | gc + ( 2 + `N` stack reads + 3 callcontext reads + `mSize` memory reads) 43 | when is_persistent = true, 44 | gc + ( 2 + `N` stack reads + 3 callcontext reads + `mSize` memory reads) + (`N` + 1 + `mSize`) log lookups: 45 | 46 | - stack_pointer + 2 + `N` 47 | - pc + 1 48 | - reversible_write_counter + 1: 49 | - log_index + 1 50 | - memory size to expansion 51 | - reversible_write_counter + 1 52 | - gas - dynamic_gas 53 | (dynamic_gas = 375 * `N` + 8 * `msize` + memory_expansion_cost) 54 | 55 | 3. Lookups: 56 | 57 | 1. stack: 58 | 59 | - `mStart` and `mSize`are on the top of stack 60 | - `N` topic items of stack 61 | 62 | 2. memory: 63 | 64 | - memory data (dynamic length) come from address `mStart` with `mSize` length 65 | 66 | 3. storage: 67 | 68 | - `N` topics lookup in Tx log entry 69 | - `msize` data lookup in Tx log entry 70 | - `CalleeAddress` lookup in CallContext 71 | - `contract_address` lookup in Tx log entry 72 | 73 | 4. others: call context is not static call 74 | 75 | ## Exceptions 76 | 77 | 1. stack underflow:\ 78 | (1024 - `stack_pointer`) `<` (2 + `N`) 79 | 2. out of gas: remaining gas is not enough 80 | 3. error call context within `STATICCALL` 81 | 82 | ## Code 83 | 84 | Refer to src/zkevm_specs/evm/execution/log.py 85 | -------------------------------------------------------------------------------- /src/zkevm_specs/evm_circuit/step.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | from .execution_state import ExecutionState 3 | from ..util import FQ, Word 4 | 5 | 6 | class StepState: 7 | """ 8 | Step state EVM circuit tracks step by step and used to ensure the 9 | execution trace is verified continuously and chronologically. 10 | It includes fields that are used from beginning to end like is_root, 11 | is_create and code_hash. 12 | It also includes call's mutable states which change almost every step like 13 | program_counter and stack_pointer. 14 | """ 15 | 16 | execution_state: ExecutionState 17 | rw_counter: FQ 18 | call_id: FQ 19 | 20 | is_root: bool 21 | is_create: bool 22 | 23 | # code_hash represents the bytecode hash of the bytecode. This is straightforward 24 | # for a contract call that does not create a contract. For a creation call, 25 | # we already populate the bytecode table with the contract creation code either 26 | # from the tx calldata (in the case of a root call) or from the caller's memory 27 | # (in the case of an internal call). 28 | code_hash: Word 29 | 30 | # The following fields change almost every step. 31 | program_counter: FQ 32 | stack_pointer: FQ 33 | gas_left: FQ 34 | 35 | # The following fields could be further moved into rw_table if we find them 36 | # not often used. 37 | memory_word_size: FQ 38 | reversible_write_counter: FQ 39 | 40 | # log index of current tx/receipt, this field maybe moved if we find them 41 | # not often used. 42 | log_id: FQ 43 | 44 | # Auxilary witness data needed by gadgets 45 | aux_data: Any 46 | 47 | def __init__( 48 | self, 49 | execution_state: ExecutionState, 50 | rw_counter: int, 51 | call_id: int = 0, 52 | is_root: bool = False, 53 | is_create: bool = False, 54 | code_hash: Word = Word(0), 55 | program_counter: int = 0, 56 | stack_pointer: int = 1024, 57 | gas_left: int = 0, 58 | memory_word_size: int = 0, 59 | reversible_write_counter: int = 0, 60 | log_id: int = 0, 61 | aux_data: Optional[Any] = None, 62 | ) -> None: 63 | self.execution_state = execution_state 64 | self.rw_counter = FQ(rw_counter) 65 | self.call_id = FQ(call_id) 66 | self.is_root = is_root 67 | self.is_create = is_create 68 | self.code_hash = code_hash 69 | self.program_counter = FQ(program_counter) 70 | self.stack_pointer = FQ(stack_pointer) 71 | self.gas_left = FQ(gas_left) 72 | self.memory_word_size = FQ(memory_word_size) 73 | self.reversible_write_counter = FQ(reversible_write_counter) 74 | self.log_id = FQ(log_id) 75 | self.aux_data = aux_data 76 | --------------------------------------------------------------------------------