-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EIP-1283: Net gas metering for SSTORE without dirty maps #1283
Changes from 26 commits
6e0338f
f3ee50c
5f05bc1
8d21d6b
40c9c4e
6a9400e
7074767
0fca9b3
f859a90
6195cd6
9b5ece7
2d972d1
8f9bb86
23ebc94
30daaa0
598d42e
b8f9659
ba8a613
f0e1590
39505aa
49b5f22
0710ff6
ddfe7ca
df15cbd
ee38de0
8dd32c9
e5f00a1
7a18e1b
d863e66
2daf07f
3125d0e
a9171ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
--- | ||
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. 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 like reentry locks, same-contract | ||
multi-send, 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. Horizontal shows the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you have 'horizontal' and 'vertical' reversed here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops! Fixed! |
||
next state, and vertical shows the current state. | ||
|
||
| | `current=orig=0` | `current!=orig` | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't this be clearer if one axis had |
||
|------------------|--------------------------|--------------------| | ||
| `current=orig=0` | 0; 200 gas | ~0; 20k gas | | ||
| `current!=orig` | 0; 200 gas, 19.8k refund | `current`; 200 gas | | ||
|
||
| | `current=orig!=0` | `current!=orig` | `current=0` | | ||
|-------------------|--------------------------------|-------------------------------|------------------------| | ||
| `current=orig!=0` | `orig`; 200 gas | `~orig`, ~0; 5k gas | 0; 5k gas, 15k refund | | ||
| `current!=orig` | `orig`; 200 gas, 4.8k refund | `~orig`; 200 gas | 0; 200 gas, 15k refund | | ||
| `current=0` | `orig`; 200 gas, -10.2k refund | `~orig`; 200 gas, -15k refund | 0; 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/). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following this process, starting with original = 1 and current = 1:
Thus for every 5k gas deducted from the gas meter, we can add 19800 gas to the refund counter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed! Added an additional clause when storage slot is dirty. If original value is not 0, if current value is 0 (also means that new value is not 0, because of the parent clause "If current value does not equal new value"), deduct 15000 gas.
new = 0, original = 1, current = 1
; deducts 5k gas and adds 15k gas to refund counter.new = 1, original = 1, current = 0
; deducts200 + 15k
gas, adds 4800 gas to the refund counter.original = 1, current = 1
; goto 1; for every round200 + 20k
gas is deducted, and19800
gas in the refund counter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems reasonable. A couple of critiques:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I think (1) is a really good point. If there're cases where it can cost a lot most gases compared with the current scheme, then we may break backward compatibility for existing contracts. So in 23ebc94 I changed the clause from deducting 15k gas to removing 15k gas from refund counter. In this way, we will never consume more gases compared with current scheme. Indeed, this adds a new semantics for gas refunds (we never removed gas from refund counter before), so I'm definitely open to better ideas on this. The current rationale/assumption for this is:
Talking about other cases where it may cost more gases compared with EIP-1087 -- it happens with sub-call frames to the same contract where we cannot track the dirtiness. But I still think it may be worth the trade off because:
Still working on (2). Let me try to see whether there are clearer ways to describe the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an explanation section to make the logic more clearer!