Skip to content
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

Merged
merged 32 commits into from
Aug 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e0338f
Net gas metering for SSTORE without dirty maps
sorpaas Aug 1, 2018
f3ee50c
typo: opcode
sorpaas Aug 1, 2018
5f05bc1
Merge branch 'master' of https://github.com/ethereum/EIPs into sp-sst…
sorpaas Aug 1, 2018
8d21d6b
typo: changed
sorpaas Aug 1, 2018
40c9c4e
Self-assign the PR number 1283
sorpaas Aug 1, 2018
6a9400e
Add a dummy discussion url
sorpaas Aug 1, 2018
7074767
Fix R_sclear loopholes
sorpaas Aug 1, 2018
0fca9b3
Properly handle refund for 0 value issue
sorpaas Aug 1, 2018
f859a90
fix: refund should only be added again if new value is 0
sorpaas Aug 1, 2018
6195cd6
clarify if statement
sorpaas Aug 1, 2018
9b5ece7
Clearly state what () means
sorpaas Aug 1, 2018
2d972d1
typo fix: unnecessary wording "additional"
sorpaas Aug 1, 2018
8f9bb86
fix: should have parent clause if original value is not zero
sorpaas Aug 1, 2018
23ebc94
Remove 15k gas from refund counter instead of deduct it as gas cost
sorpaas Aug 2, 2018
30daaa0
Be more clear on EIP-658 enabled only-commit-storage-changes-at-end-o…
sorpaas Aug 2, 2018
598d42e
Move some discussion comments to motivations section
sorpaas Aug 2, 2018
b8f9659
typo: commons -> common
sorpaas Aug 2, 2018
ba8a613
Be more specific when gas reduction won't happen compared with EIP-1087
sorpaas Aug 2, 2018
f0e1590
typo: duplicate description
sorpaas Aug 2, 2018
39505aa
Add explanation section
sorpaas Aug 3, 2018
49b5f22
becomes -> become
sorpaas Aug 3, 2018
0710ff6
typo: covers -> cover
sorpaas Aug 3, 2018
ddfe7ca
Add state transition diagrams
sorpaas Aug 3, 2018
df15cbd
Fix table formatting
sorpaas Aug 3, 2018
ee38de0
typo: 0 -> `current`
sorpaas Aug 3, 2018
8dd32c9
typo: missing -
sorpaas Aug 3, 2018
e5f00a1
Change state transition table to use `(current, original)` vs `new`
sorpaas Aug 6, 2018
7a18e1b
fix: vertical <-> horizontal
sorpaas Aug 6, 2018
d863e66
Be more specific on usages benefited by this EIP
sorpaas Aug 6, 2018
2daf07f
Merge branch 'master' into sp-sstore-no-dirty-map
Arachnid Aug 7, 2018
3125d0e
Typo fix
sorpaas Aug 7, 2018
a9171ac
Merge branch 'sp-sstore-no-dirty-map' of github.com:sorpaas/eips into…
sorpaas Aug 7, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions EIPS/eip-1283.md
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

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:

  1. new = 0; deducts 5k gas and adds 15k gas to the refund counter; current = 0.
  2. new = 1; adds 4800 gas to the refund counter
  3. Goto 1.

Thus for every 5k gas deducted from the gas meter, we can add 19800 gas to the refund counter.

Copy link
Contributor Author

@sorpaas sorpaas Aug 1, 2018

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.

  1. new = 0, original = 1, current = 1; deducts 5k gas and adds 15k gas to refund counter.
  2. new = 1, original = 1, current = 0; deducts 200 + 15k gas, adds 4800 gas to the refund counter.
  3. original = 1, current = 1; goto 1; for every round 200 + 20k gas is deducted, and 19800 gas in the refund counter.

Copy link
Contributor

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:

  • Unlike the existing proposal, this can result in consuming a lot of gas and adding it to the refund counter. Since refunds are limited to 1/2 the total gas used, you could easily end up paying more than expected if you set a value to and from its original value a number of times.
  • It's hard to follow the logic here. Some diagramming, or breaking down the cases more would help.

Copy link
Contributor Author

@sorpaas sorpaas Aug 2, 2018

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:

  • Backward compatibility is important.
  • We can prove that refund counter will never go below 0.
  • This new semantic of removing gas from refund counter is trivial to implement in most clients.

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:

  • 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 commons usages like reentry locks, same-contract multi-send, etc.

Still working on (2). Let me try to see whether there are clearer ways to describe the spec.

Copy link
Contributor Author

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!

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 added assets/eip-1283/state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.