--- 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. We provided two possible specification versions here. * For both of the two versions, we don't suffer from the optimization limitation of EIP-1087, and it never costs more gases compared with current scheme. * For Version I, it covers most common usages, and we have two edge cases it do not cover, one of which may potentially be useful. * For Version II, it covers nearly all usages for a transient storage. We only have one rare edge case (resetting a storage back to its original value and then set it again) not covered. Clients that are easy to implement EIP-1087 will also be easy to implement Version II. 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. * 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. * Passing storage information from parent call frame to sub call frame, where this information does not need to be persistent outside of a transaction. This is covered by Version II but not Version I. ## Specification (Version I) Definitions of terms are as below: * *Storage slot's original value*: This is the value of the storage if a call/create reversion happens on the *current VM execution context*. * *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. ## Specification (Version II) The same as Version I, except we change the definition of *original value* to: * *Storage slot's original value*: This is the value of the storage if a reversion happens on the *current transaction*. ## 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. For Version I, 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 Version I, 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. This refund is tracked on Version II, and issued properly. 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 for Version I, 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 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. One limitation is that for some edge cases dirtiness will not be tracked: * For Version I, the first SSTORE for a storage slot on a sub-call frame for the same contract won't benefit from gas reduction. For Version II, this type of gas reduction is properly tracked and applied. * 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 * Parity Ethereum: [Version I PR](https://github.com/paritytech/parity-ethereum/pull/9319). ## Copyright Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).