mirror of https://github.com/status-im/EIPs.git
EIP-1283: Net gas metering for SSTORE without dirty maps (#1283)
* Net gas metering for SSTORE without dirty maps * typo: opcode * typo: changed * Self-assign the PR number 1283 * Add a dummy discussion url * Fix R_sclear loopholes * Properly handle refund for 0 value issue * fix: refund should only be added again if new value is 0 * clarify if statement * Clearly state what () means * typo fix: unnecessary wording "additional" * fix: should have parent clause if original value is not zero * Remove 15k gas from refund counter instead of deduct it as gas cost * Be more clear on EIP-658 enabled only-commit-storage-changes-at-end-of-block optimizations * Move some discussion comments to motivations section * typo: commons -> common * Be more specific when gas reduction won't happen compared with EIP-1087 * typo: duplicate description * Add explanation section * becomes -> become * typo: covers -> cover * Add state transition diagrams * Fix table formatting * typo: 0 -> `current` * typo: missing - * Change state transition table to use `(current, original)` vs `new` * fix: vertical <-> horizontal * Be more specific on usages benefited by this EIP * Typo fix
This commit is contained in:
parent
4f8f1cbd1d
commit
d087ea8910
|
@ -0,0 +1,226 @@
|
|||
---
|
||||
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, some implementations do the optimization to
|
||||
only commit storage 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. For EIP-1087,
|
||||
they will need to keep a separate dirty map to keep track of gas
|
||||
costs. This adds additional memory consumptions.
|
||||
|
||||
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*. This is the value of the storage if
|
||||
a call/create reversion happens on the current VM execution
|
||||
context. It is universially available because all clients need to
|
||||
keep track of call/create reversion.
|
||||
* *Storage slot's current value*.
|
||||
* Refund counter.
|
||||
|
||||
This EIP indeed has edge cases where it may consume more gases
|
||||
compared with EIP-1087 (see Rationale), but it can be worth the trade
|
||||
off:
|
||||
|
||||
* We don't suffer from the optimization limitation of EIP-1087. 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 those implementations, we
|
||||
cannot efficiently iterate over a transaction's storage change slots
|
||||
without doing a full diff of the trie.
|
||||
* It never costs more gases compared with current scheme.
|
||||
* It covers most common usages.
|
||||
|
||||
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.
|
||||
* Passing storage information from sub call frame to 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
|
||||
|
||||
Term *original value* is as defined in Motivation. *Current value*
|
||||
refers to the storage slot value before SSTORE happens. *New value*
|
||||
refers to the storage slot value after SSTORE 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 either on current frame, or on a sub-call
|
||||
frame for the same contract. 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, either on
|
||||
current frame or on a sub-call frame for the same contract. 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 (either on current call frame or a sub-call frame for the
|
||||
same contract). When going from **Fresh** to **Dirty**, we charge the
|
||||
gas cost the same as current scheme.
|
||||
|
||||
When entering a sub-call frame, a previously-marked **Dirty** storage
|
||||
slot will again become **Fresh**, but only for this sub-call
|
||||
frame. Note that we don't charge any more gases compared with current
|
||||
scheme in this case.
|
||||
|
||||
In current call frame, a **Dirty** storage slot can be reset back to
|
||||
**Fresh** via a SSTORE opcode either on current call frame or a
|
||||
sub-call frame. For current call frame, this dirtiness is tracked, so
|
||||
we can issue refunds. For sub-call frame, it is not possible to track
|
||||
this dirtiness reset, so the refunds (for *current call frame*'s
|
||||
initial SSTORE from **Fresh** to **Dirty**) are not issued. In the
|
||||
case where refunds are not issued, the gas cost is the same as the
|
||||
current scheme.
|
||||
|
||||
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, either on current call frame or a
|
||||
sub-call frame.
|
||||
|
||||
### State Transition
|
||||
|
||||
Below is a graph ([by
|
||||
@Arachnid](https://github.com/ethereum/EIPs/pull/1283#issuecomment-410229053))
|
||||
showing possible state transition of gas costs. Note that this applies
|
||||
to current call frame only, and 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 EIP-1087 tries to do, but without the
|
||||
complexity of introducing the concept of "dirty maps". One limitation
|
||||
is that for some edge cases dirtiness will not be tracked:
|
||||
|
||||
* The first SSTORE for a storage slot on a sub-call frame for the same
|
||||
contract won't benefit from gas reduction.
|
||||
* If a storage slot is changed, and it's reset to its original
|
||||
value. The next SSTORE to the same storage slot won't benefit from
|
||||
gas reduction.
|
||||
|
||||
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
|
||||
* If the token contract has multi-send function, it will cost
|
||||
`5000 * 3 + 200 - 4800 = 10400` gas.
|
||||
* If this transfer from A to B to C is invoked by a third-party
|
||||
contract, and the token contract has no multi-send function, then
|
||||
it won't benefit from this EIP's gas reduction.
|
||||
|
||||
## 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
|
||||
|
||||
To be added.
|
||||
|
||||
## Implementation
|
||||
|
||||
To be added.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
|
Binary file not shown.
After Width: | Height: | Size: 172 KiB |
Loading…
Reference in New Issue