Skip to content

Commit 3bb9b49

Browse files
authored
core/vm, params: implement EIP2200, SSTORE optimizations (#19964)
* core/vm, params: implement EIP2200, SSTORE optimizations * core/vm, params: switch EIP2200 to Wei's version
1 parent 9dfca5d commit 3bb9b49

File tree

4 files changed

+142
-1
lines changed

4 files changed

+142
-1
lines changed

core/vm/eips.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
// defined jump tables are not polluted.
2828
func EnableEIP(eipNum int, jt *JumpTable) error {
2929
switch eipNum {
30+
case 2200:
31+
enable2200(jt)
3032
case 1884:
3133
enable1884(jt)
3234
case 1344:
@@ -83,3 +85,8 @@ func opChainID(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memo
8385
stack.push(chainId)
8486
return nil, nil
8587
}
88+
89+
// enable2200 applies EIP-2200 (Rebalance net-metered SSTORE)
90+
func enable2200(jt *JumpTable) {
91+
jt[SSTORE].dynamicGas = gasSStoreEIP2200
92+
}

core/vm/gas_table.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package vm
1818

1919
import (
20+
"errors"
21+
2022
"github.com/ethereum/go-ethereum/common"
2123
"github.com/ethereum/go-ethereum/common/math"
2224
"github.com/ethereum/go-ethereum/params"
@@ -160,6 +162,61 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
160162
return params.NetSstoreDirtyGas, nil
161163
}
162164

165+
// 0. If *gasleft* is less than or equal to 2300, fail the current call.
166+
// 1. If current value equals new value (this is a no-op), SSTORE_NOOP_GAS gas is deducted.
167+
// 2. If current value does not equal new value:
168+
// 2.1. If original value equals current value (this storage slot has not been changed by the current execution context):
169+
// 2.1.1. If original value is 0, SSTORE_INIT_GAS gas is deducted.
170+
// 2.1.2. Otherwise, SSTORE_CLEAN_GAS gas is deducted. If new value is 0, add SSTORE_CLEAR_REFUND to refund counter.
171+
// 2.2. If original value does not equal current value (this storage slot is dirty), SSTORE_DIRTY_GAS gas is deducted. Apply both of the following clauses:
172+
// 2.2.1. If original value is not 0:
173+
// 2.2.1.1. If current value is 0 (also means that new value is not 0), subtract SSTORE_CLEAR_REFUND gas from refund counter. We can prove that refund counter will never go below 0.
174+
// 2.2.1.2. If new value is 0 (also means that current value is not 0), add SSTORE_CLEAR_REFUND gas to refund counter.
175+
// 2.2.2. If original value equals new value (this storage slot is reset):
176+
// 2.2.2.1. If original value is 0, add SSTORE_INIT_REFUND to refund counter.
177+
// 2.2.2.2. Otherwise, add SSTORE_CLEAN_REFUND gas to refund counter.
178+
func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
179+
// If we fail the minimum gas availability invariant, fail (0)
180+
if contract.Gas <= params.SstoreSentryGasEIP2200 {
181+
return 0, errors.New("not enough gas for reentrancy sentry")
182+
}
183+
// Gas sentry honoured, do the actual gas calculation based on the stored value
184+
var (
185+
y, x = stack.Back(1), stack.Back(0)
186+
current = evm.StateDB.GetState(contract.Address(), common.BigToHash(x))
187+
)
188+
value := common.BigToHash(y)
189+
190+
if current == value { // noop (1)
191+
return params.SstoreNoopGasEIP2200, nil
192+
}
193+
original := evm.StateDB.GetCommittedState(contract.Address(), common.BigToHash(x))
194+
if original == current {
195+
if original == (common.Hash{}) { // create slot (2.1.1)
196+
return params.SstoreInitGasEIP2200, nil
197+
}
198+
if value == (common.Hash{}) { // delete slot (2.1.2b)
199+
evm.StateDB.AddRefund(params.SstoreClearRefundEIP2200)
200+
}
201+
return params.SstoreCleanGasEIP2200, nil // write existing slot (2.1.2)
202+
}
203+
if original != (common.Hash{}) {
204+
if current == (common.Hash{}) { // recreate slot (2.2.1.1)
205+
evm.StateDB.SubRefund(params.SstoreClearRefundEIP2200)
206+
} else if value == (common.Hash{}) { // delete slot (2.2.1.2)
207+
evm.StateDB.AddRefund(params.SstoreClearRefundEIP2200)
208+
}
209+
}
210+
if original == value {
211+
if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1)
212+
evm.StateDB.AddRefund(params.SstoreInitRefundEIP2200)
213+
} else { // reset to original existing slot (2.2.2.2)
214+
evm.StateDB.AddRefund(params.SstoreCleanRefundEIP2200)
215+
}
216+
}
217+
return params.SstoreDirtyGasEIP2200, nil // dirty update (2.2)
218+
}
219+
163220
func makeGasLog(n uint64) gasFunc {
164221
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
165222
requestedSize, overflow := bigUint64(stack.Back(1))

core/vm/gas_table_test.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@
1616

1717
package vm
1818

19-
import "testing"
19+
import (
20+
"math"
21+
"math/big"
22+
"testing"
23+
24+
"github.com/ethereum/go-ethereum/common"
25+
"github.com/ethereum/go-ethereum/common/hexutil"
26+
"github.com/ethereum/go-ethereum/core/rawdb"
27+
"github.com/ethereum/go-ethereum/core/state"
28+
"github.com/ethereum/go-ethereum/params"
29+
)
2030

2131
func TestMemoryGasCost(t *testing.T) {
2232
tests := []struct {
@@ -37,3 +47,61 @@ func TestMemoryGasCost(t *testing.T) {
3747
}
3848
}
3949
}
50+
51+
var eip2200Tests = []struct {
52+
original byte
53+
gaspool uint64
54+
input string
55+
used uint64
56+
refund uint64
57+
failure error
58+
}{
59+
{0, math.MaxUint64, "0x60006000556000600055", 1612, 0, nil}, // 0 -> 0 -> 0
60+
{0, math.MaxUint64, "0x60006000556001600055", 20812, 0, nil}, // 0 -> 0 -> 1
61+
{0, math.MaxUint64, "0x60016000556000600055", 20812, 19200, nil}, // 0 -> 1 -> 0
62+
{0, math.MaxUint64, "0x60016000556002600055", 20812, 0, nil}, // 0 -> 1 -> 2
63+
{0, math.MaxUint64, "0x60016000556001600055", 20812, 0, nil}, // 0 -> 1 -> 1
64+
{1, math.MaxUint64, "0x60006000556000600055", 5812, 15000, nil}, // 1 -> 0 -> 0
65+
{1, math.MaxUint64, "0x60006000556001600055", 5812, 4200, nil}, // 1 -> 0 -> 1
66+
{1, math.MaxUint64, "0x60006000556002600055", 5812, 0, nil}, // 1 -> 0 -> 2
67+
{1, math.MaxUint64, "0x60026000556000600055", 5812, 15000, nil}, // 1 -> 2 -> 0
68+
{1, math.MaxUint64, "0x60026000556003600055", 5812, 0, nil}, // 1 -> 2 -> 3
69+
{1, math.MaxUint64, "0x60026000556001600055", 5812, 4200, nil}, // 1 -> 2 -> 1
70+
{1, math.MaxUint64, "0x60026000556002600055", 5812, 0, nil}, // 1 -> 2 -> 2
71+
{1, math.MaxUint64, "0x60016000556000600055", 5812, 15000, nil}, // 1 -> 1 -> 0
72+
{1, math.MaxUint64, "0x60016000556002600055", 5812, 0, nil}, // 1 -> 1 -> 2
73+
{1, math.MaxUint64, "0x60016000556001600055", 1612, 0, nil}, // 1 -> 1 -> 1
74+
{0, math.MaxUint64, "0x600160005560006000556001600055", 40818, 19200, nil}, // 0 -> 1 -> 0 -> 1
75+
{1, math.MaxUint64, "0x600060005560016000556000600055", 10818, 19200, nil}, // 1 -> 0 -> 1 -> 0
76+
{1, 2306, "0x6001600055", 2306, 0, ErrOutOfGas}, // 1 -> 1 (2300 sentry + 2xPUSH)
77+
{1, 2307, "0x6001600055", 806, 0, nil}, // 1 -> 1 (2301 sentry + 2xPUSH)
78+
}
79+
80+
func TestEIP2200(t *testing.T) {
81+
for i, tt := range eip2200Tests {
82+
address := common.BytesToAddress([]byte("contract"))
83+
84+
statedb, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()))
85+
statedb.CreateAccount(address)
86+
statedb.SetCode(address, hexutil.MustDecode(tt.input))
87+
statedb.SetState(address, common.Hash{}, common.BytesToHash([]byte{tt.original}))
88+
statedb.Finalise(true) // Push the state into the "original" slot
89+
90+
vmctx := Context{
91+
CanTransfer: func(StateDB, common.Address, *big.Int) bool { return true },
92+
Transfer: func(StateDB, common.Address, common.Address, *big.Int) {},
93+
}
94+
vmenv := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{ExtraEips: []int{2200}})
95+
96+
_, gas, err := vmenv.Call(AccountRef(common.Address{}), address, nil, tt.gaspool, new(big.Int))
97+
if err != tt.failure {
98+
t.Errorf("test %d: failure mismatch: have %v, want %v", i, err, tt.failure)
99+
}
100+
if used := tt.gaspool - gas; used != tt.used {
101+
t.Errorf("test %d: gas used mismatch: have %v, want %v", i, used, tt.used)
102+
}
103+
if refund := vmenv.StateDB.GetRefund(); refund != tt.refund {
104+
t.Errorf("test %d: gas refund mismatch: have %v, want %v", i, refund, tt.refund)
105+
}
106+
}
107+
}

params/protocol_params.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ const (
5252
NetSstoreResetRefund uint64 = 4800 // Once per SSTORE operation for resetting to the original non-zero value
5353
NetSstoreResetClearRefund uint64 = 19800 // Once per SSTORE operation for resetting to the original zero value
5454

55+
SstoreSentryGasEIP2200 uint64 = 2300 // Minimum gas required to be present for an SSTORE call, not consumed
56+
SstoreNoopGasEIP2200 uint64 = 800 // Once per SSTORE operation if the value doesn't change.
57+
SstoreDirtyGasEIP2200 uint64 = 800 // Once per SSTORE operation if a dirty value is changed.
58+
SstoreInitGasEIP2200 uint64 = 20000 // Once per SSTORE operation from clean zero to non-zero
59+
SstoreInitRefundEIP2200 uint64 = 19200 // Once per SSTORE operation for resetting to the original zero value
60+
SstoreCleanGasEIP2200 uint64 = 5000 // Once per SSTORE operation from clean non-zero to something else
61+
SstoreCleanRefundEIP2200 uint64 = 4200 // Once per SSTORE operation for resetting to the original non-zero value
62+
SstoreClearRefundEIP2200 uint64 = 15000 // Once per SSTORE operation for clearing an originally existing storage slot
63+
5564
JumpdestGas uint64 = 1 // Once per JUMPDEST operation.
5665
EpochDuration uint64 = 30000 // Duration between proof-of-work epochs.
5766

0 commit comments

Comments
 (0)