2018-08-07 11:02:55 +00:00
|
|
|
---
|
|
|
|
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
|
2018-08-09 10:09:08 +00:00
|
|
|
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.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
This EIP proposes an alternative way for gas metering on SSTORE, using
|
|
|
|
information that is more universially available to most
|
|
|
|
implementations:
|
|
|
|
|
2018-08-09 10:09:08 +00:00
|
|
|
* *Storage slot's original value*.
|
2018-08-07 11:02:55 +00:00
|
|
|
* *Storage slot's current value*.
|
|
|
|
* Refund counter.
|
|
|
|
|
2018-08-11 22:32:09 +00:00
|
|
|
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.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
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.
|
2018-08-12 10:24:14 +00:00
|
|
|
* Exchange storage information between sub call frame and parent call
|
2018-08-07 11:02:55 +00:00
|
|
|
frame, where this information does not need to be persistent outside
|
|
|
|
of a transaction. This includes sub-frame error codes and message
|
|
|
|
passing, etc.
|
|
|
|
|
2018-08-11 22:32:09 +00:00
|
|
|
## Specification
|
2018-08-07 11:02:55 +00:00
|
|
|
|
2018-08-09 10:09:08 +00:00
|
|
|
Definitions of terms are as below:
|
|
|
|
|
|
|
|
* *Storage slot's original value*: This is the value of the storage if
|
2018-08-11 22:32:09 +00:00
|
|
|
a reversion happens on the *current transaction*.
|
2018-08-09 10:09:08 +00:00
|
|
|
* *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.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
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
|
2018-08-11 22:32:09 +00:00
|
|
|
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*.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
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
|
2018-08-11 22:32:09 +00:00
|
|
|
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.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
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
|
2018-08-11 22:32:09 +00:00
|
|
|
slot starts with **Fresh** state.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
### State Transition
|
|
|
|
|
|
|
|
Below is a graph ([by
|
|
|
|
@Arachnid](https://github.com/ethereum/EIPs/pull/1283#issuecomment-410229053))
|
2018-08-11 22:32:09 +00:00
|
|
|
showing possible state transition of gas costs. We ignore **No-op**
|
2018-08-09 10:09:08 +00:00
|
|
|
state because that is trivial:
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
![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
|
|
|
|
|
2018-08-09 10:09:08 +00:00
|
|
|
This EIP mostly archives what a transient storage tries to do
|
|
|
|
(EIP-1087 and EIP-1153), but without the complexity of introducing the
|
2018-08-11 22:32:09 +00:00
|
|
|
concept of "dirty maps", or an extra storage struct.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
2018-08-11 22:32:09 +00:00
|
|
|
* 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.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
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
|
2018-08-11 22:32:09 +00:00
|
|
|
ending balances, it will cost `5000 * 3 + 200 - 4800 = 10400` gas.
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
## 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
|
|
|
|
|
2018-08-23 13:02:19 +00:00
|
|
|
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 |
|
2018-08-07 11:02:55 +00:00
|
|
|
|
|
|
|
## Copyright
|
|
|
|
|
|
|
|
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
|