diff --git a/EIPS/eip-1283.md b/EIPS/eip-1283.md new file mode 100644 index 00000000000000..40bd258c1994b9 --- /dev/null +++ b/EIPS/eip-1283.md @@ -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/). diff --git a/assets/eip-1283/state.png b/assets/eip-1283/state.png new file mode 100644 index 00000000000000..1737d2578630a8 Binary files /dev/null and b/assets/eip-1283/state.png differ