Skip to content
This repository was archived by the owner on Jul 5, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions specs/opcode/F0CREATE_F5CREATE2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CREATE and CREATE2 opcodes

## Procedure

### EVM behavior

The `CREATE` and `CREATE2` opcodes create a new call context at a new address.
They transfer the specified amount of ether from the caller address to the new address and then execute the specified initialization code.
The bytecode of the new address will be updated to be the return bytes of this initialization code.

The following values are popped from the stack:
1. `value` - The amount of ether to transfer from the caller to the new address.
2. `offset` - The memory offset in the caller's memory where the initialization code starts.
3. `size` - The length of the initialization code, in bytes.
4. `salt` - Only for `CREATE2`: salt for the hash that will determine the new address.
If the initialization call is successful, the new address will be pushed to the stack.
If not, 0 will be pushed instead.

The gadget does several things in the caller's call context:
1. Expands memory
2. Add `new_address` to the access list
3. Calculate gas cost for this step.
4. Uses EIP150 to calculate how much gas will remain in the caller's context.

The memory size is calculated as follows:

```
calc_memory_size(offset, length) := 0 if length == 0 else ceil((offset + length) / 32)
```

`new_address` is calculated as follows:

```
new_address := keccak256([0xff] + caller_address + salt + keccak256(initialization_bytecode)) if op_id == CREATE2 else rlp([caller_address, caller_address.nonce])
```

The memory expansion gas cost is:

```
MEMORY_EXPANSION_QUAD_DENOMINATOR := 512
MEMORY_EXPANSION_LINEAR_COEFF := 3
calc_memory_cost(memory_size) := MEMORY_EXPANSION_LINEAR_COEFF * memory_size + floor(memory_size * memory_size / MEMORY_EXPANSION_QUAD_DENOMINATOR)
```

The gas cost charged for the additional `memory_size` is:

```
next.memory_size := max(
curr.memory_size,
memory_size(offset, length),
)
memory_expansion_gas_cost := calc_memory_cost(next.memory_size) - memory_cost(curr.memory_size)
```

The `gas_cost` for the step is:

```
GAS_COST_CREATE := 32000
KECCAK_BYTE_GAS_COST : 6

keccak_gas_cost = KECCAK_BYTE_GAS_COST * size if op_id == CREATE2 else 0
gas_cost = (
GAS_COST_CREATE
+ memory_expansion_gas_cost
+ keccak_gas_cost
)
```

The `callee_gas_left` for new context by rule in EIP150 is calculated like this:

```
gas_available := curr.gas_left - gas_cost
callee_gas_left := min(gas_available - floor(gas_available / 64), gas)
```

In the initialization call context, it does:

1. Transfer `value` from the caller address to the new address
2. Executes the initialization bytecode
3. Update the bytecode of the new address to be the `return_data` of execution

### Circuit behavior

The circuit takes the starting `rw_counter` as next call's `call_id` to make sure each call has a unique `call_id`.

It pops 3 words for `CREATE` and 4 words for `CREATE2` from the stack.
In both cases, it then pushes 1 word to the stack.

It then checks that `new_call.is_persistent = caller.is_persistent and new_call.is_success`.
If the caller is not persistent, we need to propagate the `rw_counter_end_of_reversion` to make sure every state update in the new call has a corresponding reversion.

It stores the current call context by writing the current values `rw_table` and checks that the new call context is setup correctly by reading to `rw_table`, and then does a step state transition to the call and begins the execution o the initialization bytecode.

## Code

Please refer to `src/zkevm_specs/evm/execution/create.py`.
57 changes: 57 additions & 0 deletions src/zkevm_specs/evm/execution/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from ...util import (
FQ,
GAS_COST_ACCOUNT_COLD_ACCESS,
GAS_COST_CALL_WITH_VALUE,
GAS_COST_NEW_ACCOUNT,
GAS_COST_WARM_ACCESS,
N_BYTES_GAS,
CALL_CREATE_DEPTH,
)
from ..instruction import Instruction
from ..opcode import Opcode
from ..table import CallContextFieldTag, AccountFieldTag


def create(instruction: Instruction):
opcode = instruction.opcode_lookup(True)
is_create, is_create2 = instruction.select(opcode, (Opcode.CREATE, Opcode.CREATE2))
instruction.responsible_opcode_lookup(opcode)

value = instruction.stack_pop()
offset = instruction.stack_pop()
size = instruction.stack_pop()
input = instruction.stack_pop()
gas = instruction.stack_push()

depth = instruction.call_context_lookup(CallContextFieldTag.Depth)
tx_caller_address = instruction.call_context_lookup(CallContextFieldTag.CallerAddress)
nonce, nonce_prev = instruction.account_write(parent_caller_address, AccountFieldTag.Nonce)

# ErrDepth constraint
instruction.range_lookup(depth, CALL_CREATE_DEPTH)
# ErrNonceUintOverflow constraint
(is_not_overflow, _) = instruction.compare(nonce, nonce_prev, 8)
instruction.is_zero(is_not_overflow)
# ErrInsufficientBalance constraint
gas_cost = (
instruction.select(
is_warm_access, FQ(GAS_COST_WARM_ACCESS), FQ(GAS_COST_ACCOUNT_COLD_ACCESS)
)
+ has_value
* (
GAS_COST_CALL_WITH_VALUE
# Only CALL opcode could invoke transfer to make empty account into non-empty.
+ is_call * (1 - callee_exists) * GAS_COST_NEW_ACCOUNT
)
+ memory_expansion_gas_cost
)
# Apply EIP 150.
# Note that sufficient gas_left is checked implicitly by constant_divmod.
gas_available = instruction.curr.gas_left - gas_cost
one_64th_gas, _ = instruction.constant_divmod(gas_available, FQ(64), N_BYTES_GAS)
all_but_one_64th_gas = gas_available - one_64th_gas
callee_gas_left = instruction.select(
gas_is_u64,
instruction.min(all_but_one_64th_gas, gas, N_BYTES_GAS),
all_but_one_64th_gas,
)
5 changes: 3 additions & 2 deletions src/zkevm_specs/evm/execution_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,14 @@ class ExecutionState(IntEnum):
ErrorStack = auto()
# For SSTORE, LOG0, LOG1, LOG2, LOG3, LOG4, CREATE, CALL, CREATE2, SELFDESTRUCT
ErrorWriteProtection = auto()
# For CALL, CALLCODE, DELEGATECALL, STATICCALL
# For CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE
ErrorDepth = auto()
# For CALL, CALLCODE
# For CALL, CALLCODE, CREATE
ErrorInsufficientBalance = auto()
# For CREATE, CREATE2
ErrorContractAddressCollision = auto()
ErrorInvalidCreationCode = auto()
ErrorNonceUintOverflow = auto()
# For opcode RETURN which needs to store code when it's is creation
ErrorMaxCodeSizeExceeded = auto()
# For JUMP, JUMPI
Expand Down
16 changes: 16 additions & 0 deletions src/zkevm_specs/evm/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,3 +1054,19 @@ def _append_row(
is_rlc_acc=FQ(is_rlc_acc),
)
)


def generate_contract_address(account: Account) -> bytes:
prefix = [LIST_PREFIX + 0x16]
sender_rlp = [HEX_PREFIX + ADDRESS_LENGTH]
sender_rlp += [x for x in account.address.to_bytes(20, "big")]
prefix += sender_rlp
prefix.append(HEX_PREFIX if account.nonce == 0 else account.nonce)
raw_hash = keccak256(bytearray(prefix))[12:]

return raw_hash[(len(raw_hash) - ADDRESS_LENGTH) :]


ADDRESS_LENGTH = 0x14
HEX_PREFIX = 0x80
LIST_PREFIX = 0xC0
3 changes: 3 additions & 0 deletions src/zkevm_specs/util/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@
# of cells in a step
MAX_COPY_BYTES = 32

# Maximum depth of call/create stack
CALL_CREATE_DEPTH = 1024

# PublicInputs circuit parameters
PUBLIC_INPUTS_BLOCK_LEN = 7 + 256 # Length of block public data
PUBLIC_INPUTS_EXTRA_LEN = 3 # Length of fields that don't belong to any table
Expand Down
21 changes: 21 additions & 0 deletions tests/test_autil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from rlp import decode, encode
from zkevm_specs.evm import Account, generate_contract_address
from zkevm_specs.util import keccak256


def test_contract_address():
sender_addr0 = Account(address=0x970E8128AB834E8EAC17AB8E3812F010678CF791, nonce=0)
sender_addr1 = Account(address=0x970E8128AB834E8EAC17AB8E3812F010678CF791, nonce=1)
sender_addr2 = Account(address=0x970E8128AB834E8EAC17AB8E3812F010678CF791, nonce=2)

expected_caddr0 = Account(address=0x333C3310824B7C685133F2BEDB2CA4B8B4DF633D)
expected_caddr1 = Account(address=0x8BDA78331C916A08481428E4B07C96D3E916D165)
expected_caddr2 = Account(address=0xC9DDEDF451BC62CE88BF9292AFB13DF35B670699)

caddr0 = generate_contract_address(sender_addr0)
caddr1 = generate_contract_address(sender_addr1)
caddr2 = generate_contract_address(sender_addr2)

assert expected_caddr0.address.to_bytes(20, "big") == caddr0
assert expected_caddr1.address.to_bytes(20, "big") == caddr1
assert expected_caddr2.address.to_bytes(20, "big") == caddr2