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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions specs/opcode/54SLOAD_55SSTORE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SLOAD & SSTORE op code

## Variables definition

| Name | Value |
| - | - |
| COLD_SLOAD_COST | 2100 |
| WARM_STORAGE_READ_COST | 100 |
| SLOAD_GAS | 100 |
| SSTORE_SET_GAS | 20000 |
| SSTORE_RESET_GAS | 2900 |
| SSTORE_CLEARS_SCHEDULE | 15000 |

## Constraints

1. opcodeId checks
1. opId === OpcodeId(0x54) for `SLOAD`
2. opId === OpcodeId(0x55) for `SSTORE`
2. state transition:
- gc
- `SLOAD`: +5 (2 stack operations + 1 storage reads + 2 access_list reads/writes)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I know where the 2 access_list reads/writes happen ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check the "lookup" section below

- `SSTORE`: +8
- 2 stack operations
- 2 storage reads/writes
- 2 access_list reads/writes
- 2 gas_refund reads/writes
- stack_pointer
- `SLOAD`: remains the same
- `SSTORE`: -2
- pc + 1
- state_write_counter
- `SLOAD`: +1 (access_list)
- `SSTORE`: +3 (for storage, access_list & gas_refund respectively)
- gas:
- `SLOAD`:
- the accessed address is warm: gas + WARM_STORAGE_READ_COST
- the accessed address is cold: gas + COLD_SLOAD_COST
- `SSTORE`:
- the accessed address is warm:
- `current_value == new_value`: gas + SLOAD_GAS
- `current_value != new_value`:
- `original_value == current_value`:
- `original_value == 0`: gas + SSTORE_SET_GAS
- `original_value != 0`: gas + SSTORE_RESET_GAS
- `original_value != current_value`: gas + SLOAD_GAS
- the accessed address is cold:
- `current_value == new_value`: gas + SLOAD_GAS + COLD_SLOAD_COST
- `current_value != new_value`:
- `original_value == current_value`:
- `original_value == 0`: gas + SSTORE_SET_GAS + COLD_SLOAD_COST
- `original_value != 0`: gas + SSTORE_RESET_GAS + COLD_SLOAD_COST
- `original_value != current_value`: gas + SLOAD_GAS + COLD_SLOAD_COST
* gas_refund:
- `SSTORE`:
- `current_value != new_value`:
- `original_value == current_value`:
- `original_value != 0` && `new_value == 0`: gas_refund + SSTORE_CLEARS_SCHEDULE
- `original_value != current_value`:
- `original_value != 0`:
- `current_value == 0`: gas_refund - SSTORE_CLEARS_SCHEDULE
- `new_value == 0`: gas_refund + SSTORE_CLEARS_SCHEDULE
- `original_value == new_value`:
- `original_value == 0`: gas_refund + SSTORE_SET_GAS - SLOAD_GAS
- `original_value != 0`: gas_refund + SSTORE_RESET_GAS - SLOAD_GAS
3. lookups:
- `SLOAD`: 5 busmapping lookups
- stack:
- `address` is popped off the top of the stack
- `value` is pushed on top of the stack
- storage: The 32 bytes of `value` are read from storage at `address`
- access_list: Whether the address is warm (accessed before), mark as warm afterward
- `SSTORE`: 8 busmapping lookups
- stack:
- `address` is popped off the top of the stack
- `value` is popped off the top of the stack
- storage:
- Read the orignal_value and the current_value at `address`
- The 32 bytes of new `value` are written to storage at `address`
- access_list: Whether the address is warm (accessed before), mark as warm afterward
- gas_refund:
- Read the accumulated gas_refund for this tx
- Write the new accumulated gas_refund for this tx

## Exceptions

1. gas out: remaining gas is not enough
2. stack underflow:
- the stack is empty: `1024 == stack_pointer`
- only for `SSTORE`: contains a single value: `1023 == stack_pointer`
3. context error
- only for `SSTORE`: the current execution context is from a `STATICCALL` (since Byzantium fork).
4 changes: 4 additions & 0 deletions src/zkevm_specs/evm/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from .slt_sgt import *
from .callvalue import *
from .calldatasize import *
from .gas import *
from .storage import *


EXECUTION_STATE_IMPL: Dict[ExecutionState, Callable] = {
Expand All @@ -31,4 +33,6 @@
ExecutionState.JUMPI: jumpi,
ExecutionState.PUSH: push,
ExecutionState.SCMP: scmp,
ExecutionState.SLOAD: sload,
ExecutionState.SSTORE: sstore,
}
6 changes: 6 additions & 0 deletions src/zkevm_specs/evm/execution/gas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
COLD_SLOAD_COST = 2100
WARM_STORAGE_READ_COST = 100
SLOAD_GAS = 100
SSTORE_SET_GAS = 20000
SSTORE_RESET_GAS = 2900
SSTORE_CLEARS_SCHEDULE = 15000
107 changes: 107 additions & 0 deletions src/zkevm_specs/evm/execution/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from ..instruction import Instruction, Transition
from ..opcode import Opcode
from ..table import CallContextFieldTag, TxContextFieldTag
from .gas import (
COLD_SLOAD_COST,
WARM_STORAGE_READ_COST,
SLOAD_GAS,
SSTORE_SET_GAS,
SSTORE_RESET_GAS,
SSTORE_CLEARS_SCHEDULE,
)


def sload(instruction: Instruction):
opcode = instruction.opcode_lookup(True)
instruction.constrain_equal(opcode, Opcode.SLOAD)

tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId)
rw_counter_end_of_reversion = instruction.call_context_lookup(CallContextFieldTag.RwCounterEndOfReversion)
is_persistent = instruction.call_context_lookup(CallContextFieldTag.IsPersistent)
tx_callee_address = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CalleeAddress)

storage_key = instruction.stack_pop()
warm, _ = instruction.access_list_account_storage_read(tx_id, tx_callee_address, storage_key)

# TODO: Use intrinsic gas (EIP 2028, 2930)
dynamic_gas_cost = WARM_STORAGE_READ_COST if warm else COLD_SLOAD_COST

instruction.account_storage_read(tx_callee_address, storage_key)
instruction.add_account_storage_to_access_list_with_reversion(
tx_id, tx_callee_address, storage_key, is_persistent, rw_counter_end_of_reversion
)
instruction.stack_push()

instruction.step_state_transition_in_same_context(
opcode,
rw_counter=Transition.delta(5),
program_counter=Transition.delta(1),
stack_pointer=Transition.delta(0),
state_write_counter=Transition.delta(1),
dynamic_gas_cost=dynamic_gas_cost,
)


def sstore(instruction: Instruction):
opcode = instruction.opcode_lookup(True)
instruction.constrain_equal(opcode, Opcode.SSTORE)

tx_id = instruction.call_context_lookup(CallContextFieldTag.TxId)
rw_counter_end_of_reversion = instruction.call_context_lookup(CallContextFieldTag.RwCounterEndOfReversion)
is_persistent = instruction.call_context_lookup(CallContextFieldTag.IsPersistent)
tx_callee_address = instruction.tx_context_lookup(tx_id, TxContextFieldTag.CalleeAddress)

storage_key = instruction.stack_pop()
new_value = instruction.stack_pop()
warm, _ = instruction.access_list_account_storage_read(tx_id, tx_callee_address, storage_key)
current_value, _, txid, original_value = instruction.account_storage_read(tx_callee_address, storage_key)
instruction.constrain_equal(tx_id, txid)

# TODO: Use intrinsic gas (EIP 2028, 2930)
if current_value == new_value:
dynamic_gas_cost = SLOAD_GAS
else:
if original_value == current_value:
if original_value == 0:
dynamic_gas_cost = SSTORE_SET_GAS
else:
dynamic_gas_cost = SSTORE_RESET_GAS
else:
dynamic_gas_cost = SLOAD_GAS
if not warm:
dynamic_gas_cost = dynamic_gas_cost + COLD_SLOAD_COST

gas_refund = instruction.tx_refund_read(tx_id)
if current_value != new_value:
if original_value == current_value:
if original_value != 0 and new_value == 0:
gas_refund = gas_refund + SSTORE_CLEARS_SCHEDULE
else:
if original_value != 0:
if current_value == 0:
gas_refund = gas_refund - SSTORE_CLEARS_SCHEDULE
if new_value == 0:
gas_refund = gas_refund + SSTORE_CLEARS_SCHEDULE
if original_value == new_value:
if original_value == 0:
gas_refund = gas_refund + SSTORE_SET_GAS - SLOAD_GAS
else:
gas_refund = gas_refund + SSTORE_RESET_GAS - SLOAD_GAS

instruction.account_storage_write_with_reversion(
tx_callee_address, storage_key, is_persistent, rw_counter_end_of_reversion
)
instruction.add_account_storage_to_access_list_with_reversion(
tx_id, tx_callee_address, storage_key, is_persistent, rw_counter_end_of_reversion
)
new_gas_refund, _ = instruction.tx_refund_write_with_reversion(tx_id, is_persistent, rw_counter_end_of_reversion)
instruction.constrain_equal(gas_refund, new_gas_refund)

instruction.step_state_transition_in_same_context(
opcode,
rw_counter=Transition.delta(8),
program_counter=Transition.delta(1),
stack_pointer=Transition.delta(2),
state_write_counter=Transition.delta(3),
dynamic_gas_cost=dynamic_gas_cost,
)
50 changes: 50 additions & 0 deletions src/zkevm_specs/evm/instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,22 @@ def tx_refund_read(self, tx_id) -> int:
row = self.rw_lookup(RW.Read, RWTableTag.TxRefund, [tx_id])
return row[-4]

def tx_refund_write_with_reversion(
self,
tx_id: int,
is_persistent: bool,
rw_counter_end_of_reversion: int,
state_write_counter: Optional[int] = None,
) -> Tuple[int, int]:
row = self.state_write_with_reversion(
RWTableTag.TxRefund,
[tx_id],
is_persistent,
rw_counter_end_of_reversion,
state_write_counter,
)
return row[-4], row[-3]

def account_read(self, account_address: int, account_field_tag: AccountFieldTag) -> int:
row = self.rw_lookup(RW.Read, RWTableTag.Account, [account_address, account_field_tag])
return row[-4]
Expand Down Expand Up @@ -508,6 +524,27 @@ def sub_balance_with_reversion(
self.constrain_zero(carry)
return balance, balance_prev

def account_storage_read(self, account_address: int, storage_key: int) -> Tuple[int, int, int, int]:
row = self.rw_lookup(RW.Read, RWTableTag.AccountStorage, [account_address, storage_key, 0])
return row[-4], row[-3], row[-2], row[-1]

def account_storage_write_with_reversion(
self,
account_address: int,
storage_key: int,
is_persistent: bool,
rw_counter_end_of_reversion: int,
state_write_counter: Optional[int] = None,
) -> Tuple[int, int]:
row = self.state_write_with_reversion(
RWTableTag.AccountStorage,
[account_address, storage_key],
is_persistent,
rw_counter_end_of_reversion,
state_write_counter,
)
return row[-4], row[-3]

def add_account_to_access_list(
self,
tx_id: int,
Expand Down Expand Up @@ -537,6 +574,19 @@ def add_account_to_access_list_with_reversion(
)
return row[-4] - row[-3]

def access_list_account_storage_read(
self,
tx_id: int,
account_address: int,
storage_key: int,
) -> Tuple[int, int]:
row = self.rw_lookup(
RW.Read,
RWTableTag.TxAccessListAccountStorage,
[tx_id, account_address, storage_key],
)
return row[-4], row[-3]

def add_account_storage_to_access_list(
self,
tx_id: int,
Expand Down
Loading