EIPs/EIPS/eip-1283.md

223 lines
9.9 KiB
Markdown
Raw Normal View History

---
eip: 1283
title: Net gas metering for SSTORE without dirty maps
author: Wei Tang (@sorpaas)
discussions-to: https://github.com/sorpaas/EIPs/issues/1
status: Draft
type: Standards Track
category: Core
created: 2018-08-01
---
## Abstract
This EIP proposes net gas metering changes for SSTORE opcode, as an
alternative for EIP-1087. It tries to be friendlier to implementations
that uses different opetimiazation strategies for storage change
caches.
## Motivation
EIP-1087 proposes a way to adjust gas metering for SSTORE opcode,
enabling new usages on this opcodes where it is previously too
expensive. However, EIP-1087 requires keeping a dirty map for storage
changes, and implictly makes the assumption that a transaction's
storage changes are committed to the storage trie at the end of a
transaction. This works well for some implementations, but not for
others. After EIP-658, an efficient storage cache implementation would
probably use an in-memory trie (without RLP encoding/decoding) or
other immutable data structures to keep track of storage changes, and
only commit changes at the end of a block. For them, it is possible to
know a storage's original value and current value, but it is not
possible to iterate over all storage changes without incur additional
memory or processing costs.
This EIP proposes an alternative way for gas metering on SSTORE, using
information that is more universially available to most
implementations:
* *Storage slot's original value*.
* *Storage slot's current value*.
* Refund counter.
For the specification provided here:
* We don't suffer from the optimization limitation of EIP-1087, and it
never costs more gas compared with current scheme.
* It covers all usages for a transient storage. Clients that are easy
to implement EIP-1087 will also be easy to implement this
specification. Some other clients might require a little bit extra
refactoring on this. Nonetheless, no extra memory or processing cost
is needed on runtime.
Usages that benefits from this EIP's gas reduction scheme includes:
* Subsequent storage write operations within the same call frame. This
includes reentry locks, same-contract multi-send, etc.
* Exchange storage information between sub call frame and parent call
frame, where this information does not need to be persistent outside
of a transaction. This includes sub-frame error codes and message
passing, etc.
## Specification
Definitions of terms are as below:
* *Storage slot's original value*: This is the value of the storage if
a reversion happens on the *current transaction*.
* *Storage slot's current value*: This is the value of the storage
before SSTORE operation happens.
* *Storage slot's new value*: This is the value of the storage after
SSTORE operation happens.
Replace SSTORE opcode gas cost calculation (including refunds) with
the following logic:
* If *current value* equals *new value* (this is a no-op), 200 gas is
deducted.
* If *current value* does not equal *new value*
* If *original value* equals *current value* (this storage slot has
not been changed by the current execution context)
* If *original value* is 0, 20000 gas is deducted.
* Otherwise, 5000 gas is deducted. If *new value* is 0, add 15000
gas to refund counter.
* If *original value* does not equal *current value* (this storage
slot is dirty), 200 gas is deducted. Apply both of the following
clauses.
* If *original value* is not 0
* If *current value* is 0 (also means that *new value* is not
0), remove 15000 gas from refund counter. We can prove that
refund counter will never go below 0.
* If *new value* is 0 (also means that *current value* is not
0), add 15000 gas to refund counter.
* If *original value* equals *new value* (this storage slot is
reset)
* If *original value* is 0, add 19800 gas to refund counter.
* Otherwise, add 4800 gas to refund counter.
Refund counter works as before -- it is limited to half of the gas
consumed.
## Explanation
The new gas cost scheme for SSTORE is divided to three different
types:
* **No-op**: the virtual machine does not need to do anything. This is
the case if *current value* equals *new value*.
* **Fresh**: this storage slot has not been changed, or has been reset
to its original value. This is the case if *current value* does not
equal *new value*, and *original value* equals *current value*.
* **Dirty**: this storage slot has already been changed. This is the
case if *current value* does not equal *new value*, and *original
value* does not equal *current value*.
We can see that the above three types cover all possible variations of
*original value*, *current value*, and *new value*.
**No-op** is a trivial operation. Below we only consider cases for
**Fresh** and **Dirty**.
All initial (not-**No-op**) SSTORE on a particular storage slot starts
with **Fresh**. After that, it will become **Dirty** if the value has
been changed. When going from **Fresh** to **Dirty**, we charge the
gas cost the same as current scheme. A **Dirty** storage slot can be
reset back to **Fresh** via a SSTORE opcode. This will trigger a
refund.
When a storage slot remains at **Dirty**, we charge 200 gas. In this
case, we would also need to keep track of `R_SCLEAR` refunds -- if we
already issued the refund but it no longer applies (*current value* is
0), then removes this refund from the refund counter. If we didn't
issue the refund but it applies now (*new value* is 0), then adds this
refund to the refund counter. It is not possible where a refund is not
issued but we remove the refund in the above case, because all storage
slot starts with **Fresh** state.
### State Transition
Below is a graph ([by
@Arachnid](https://github.com/ethereum/EIPs/pull/1283#issuecomment-410229053))
showing possible state transition of gas costs. We ignore **No-op**
state because that is trivial:
![State Transition](../assets/eip-1283/state.png)
Below are table version of the above diagram. Vertical shows the *new
value* being set, and horizontal shows the state of *original value*
and *current value*.
When *original value* is 0:
| | A (`current=orig=0`) | B (`current!=orig`) |
|----|----------------------|--------------------------|
| ~0 | B; 20k gas | B; 200 gas |
| 0 | A; 200 gas | A; 200 gas, 19.8k refund |
When *original value* is not 0:
| | X (`current=orig!=0`) | Y (`current!=orig`) | Z (`current=0`) |
|-------------|-----------------------|-------------------------|---------------------------|
| `orig` | X; 200 gas | X; 200 gas, 4.8k refund | X; 200 gas, -10.2k refund |
| `~orig, ~0` | Y; 5k gas | Y; 200 gas | Y; 200 gas, -15k refund |
| 0 | Z; 5k gas, 15k refund | Z; 200 gas, 15k refund | Z; 200 gas |
## Rationale
This EIP mostly archives what a transient storage tries to do
(EIP-1087 and EIP-1153), but without the complexity of introducing the
concept of "dirty maps", or an extra storage struct.
* For *absolute gas used* (that is, actual *gas used* minus *refund*),
this EIP is equivalent to EIP-1087 for all cases.
* For one particular case, where a storage slot is changed, reset to
its original value, and then changed again, EIP-1283 would move more
gases to refund counter compared with EIP-1087.
Examine examples provided in EIP-1087's Motivation:
* If a contract with empty storage sets slot 0 to 1, then back to 0,
it will be charged `20000 + 200 - 19800 = 400` gas.
* A contract with empty storage that increments slot 0 5 times will be
charged `20000 + 5 * 200 = 21000` gas.
* A balance transfer from account A to account B followed by a
transfer from B to C, with all accounts having nonzero starting and
ending balances, it will cost `5000 * 3 + 200 - 4800 = 10400` gas.
## Backwards Compatibility
This EIP requires a hard fork to implement. No gas cost increase is
anticipated, and many contract will see gas reduction.
## Test Cases
Below we provide 17 test cases. 15 of them covering consecutive two
`SSTORE` operations are based on work [by
@chfast](https://github.com/ethereum/tests/issues/483). Two additional
case with three `SSTORE` operations is used to test the case when a
slot is reset and then set again.
| Code | Used Gas | Refund | Original | 1st | 2nd | 3rd |
|------------------------------------|----------|--------|----------|-----|-----|-----|
| `0x60006000556000600055` | 412 | 0 | 0 | 0 | 0 | |
| `0x60006000556001600055` | 20212 | 0 | 0 | 0 | 1 | |
| `0x60016000556000600055` | 20212 | 19800 | 0 | 1 | 0 | |
| `0x60016000556002600055` | 20212 | 0 | 0 | 1 | 2 | |
| `0x60016000556001600055` | 20212 | 0 | 0 | 1 | 1 | |
| `0x60006000556000600055` | 5212 | 15000 | 1 | 0 | 0 | |
| `0x60006000556001600055` | 5212 | 4800 | 1 | 0 | 1 | |
| `0x60006000556002600055` | 5212 | 0 | 1 | 0 | 2 | |
| `0x60026000556000600055` | 5212 | 15000 | 1 | 2 | 0 | |
| `0x60026000556003600055` | 5212 | 0 | 1 | 2 | 3 | |
| `0x60026000556001600055` | 5212 | 4800 | 1 | 2 | 1 | |
| `0x60026000556002600055` | 5212 | 0 | 1 | 2 | 2 | |
| `0x60016000556000600055` | 5212 | 15000 | 1 | 1 | 0 | |
| `0x60016000556002600055` | 5212 | 0 | 1 | 1 | 2 | |
| `0x60016000556001600055` | 412 | 0 | 1 | 1 | 1 | |
| `0x600160005560006000556001600055` | 40218 | 19800 | 0 | 1 | 0 | 1 |
| `0x600060005560016000556000600055` | 10218 | 19800 | 1 | 0 | 1 | 0 |
## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).