Skip to content

Commit 164da0c

Browse files
authored
feat: eip-2935 support (#722)
* eip-2935 support # Conflicts: # x/evm/migrations/v4/types/params_v4.pb.go * revert proto * add more comments * fix conversion * add changelog * fix test * add fallback old behavior * add more comments * fix proto * add logs * fix GetHashFn * remove log * fix historical data * fix lint * small optimize * fix tests * fix changelog
1 parent 5e0e7ba commit 164da0c

File tree

9 files changed

+187
-80
lines changed

9 files changed

+187
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
4141
* (evm) [#740](https://github.com/crypto-org-chain/ethermint/pull/740) fix: missing tx context during vm initialisation
4242
* (evm) [#742](https://github.com/crypto-org-chain/ethermint/pull/742) fix: prevent nil pointer dereference in tracer hooks
4343
* (evm) [#728](https://github.com/crypto-org-chain/ethermint/pull/728) feat: support preinstalls
44+
* (evm) [#722](https://github.com/crypto-org-chain/ethermint/pull/722) feat: support EIP-2935
4445

4546
## [v0.22.0] - 2025-08-12
4647

proto/ethermint/evm/v1/params.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ message Params {
2424
bool allow_unprotected_txs = 6;
2525
// header_hash_num is the number of header hash to persist.
2626
uint64 header_hash_num = 7;
27+
// historyServeWindow for EIP 2935
28+
uint64 history_serve_window = 8;
29+
2730
}

x/evm/keeper/keeper.go

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package keeper
1717

1818
import (
19+
"encoding/binary"
1920
"math/big"
2021

2122
"github.com/ethereum/go-ethereum/crypto"
@@ -31,15 +32,15 @@ import (
3132
"github.com/ethereum/go-ethereum/core/tracing"
3233
ethtypes "github.com/ethereum/go-ethereum/core/types"
3334
"github.com/ethereum/go-ethereum/core/vm"
34-
"github.com/ethereum/go-ethereum/params"
35+
ethparams "github.com/ethereum/go-ethereum/params"
3536
ethermint "github.com/evmos/ethermint/types"
3637
"github.com/evmos/ethermint/x/evm/statedb"
3738
"github.com/evmos/ethermint/x/evm/types"
3839
"github.com/holiman/uint256"
3940
)
4041

4142
// CustomContractFn defines a custom precompiled contract generator with ctx, rules and returns a precompiled contract.
42-
type CustomContractFn func(sdk.Context, params.Rules) vm.PrecompiledContract
43+
type CustomContractFn func(sdk.Context, ethparams.Rules) vm.PrecompiledContract
4344

4445
// GasNoLimit is the value for keeper.queryMaxGasLimit in case there is no limit
4546
const GasNoLimit = 0
@@ -216,7 +217,7 @@ func (k *Keeper) PostTxProcessing(ctx sdk.Context, msg *core.Message, receipt *e
216217
}
217218

218219
// Tracer return a default vm.Tracer based on current keeper state
219-
func (k Keeper) Tracer(ctx sdk.Context, msg core.Message, ethCfg *params.ChainConfig) *tracing.Hooks {
220+
func (k Keeper) Tracer(ctx sdk.Context, msg core.Message, ethCfg *ethparams.ChainConfig) *tracing.Hooks {
220221
return types.NewTracer(k.tracer, msg, ethCfg, ctx.BlockHeight(), uint64(ctx.BlockTime().Unix())) //#nosec G115 -- int overflow is not a concern here
221222
}
222223

@@ -278,7 +279,7 @@ func (k *Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string)
278279
// - `nil`: london hardfork not enabled.
279280
// - `0`: london hardfork enabled but feemarket is not enabled.
280281
// - `n`: both london hardfork and feemarket are enabled.
281-
func (k Keeper) GetBaseFee(ctx sdk.Context, ethCfg *params.ChainConfig) *big.Int {
282+
func (k Keeper) GetBaseFee(ctx sdk.Context, ethCfg *ethparams.ChainConfig) *big.Int {
282283
return k.getBaseFee(ctx, types.IsLondon(ethCfg, ctx.BlockHeight()))
283284
}
284285

@@ -322,18 +323,57 @@ func (k Keeper) AddTransientGasUsed(ctx sdk.Context, gasUsed uint64) (uint64, er
322323

323324
// SetHeaderHash stores the hash of the current block header in the store.
324325
func (k Keeper) SetHeaderHash(ctx sdk.Context) {
325-
store := ctx.KVStore(k.storeKey)
326-
height, err := ethermint.SafeUint64(ctx.BlockHeight())
327-
if err != nil {
328-
panic(err)
326+
acct := k.GetAccount(ctx, ethparams.HistoryStorageAddress)
327+
if acct != nil && acct.IsContract() {
328+
window := types.DefaultHistoryServeWindow
329+
params := k.GetParams(ctx)
330+
if params.HistoryServeWindow > 0 {
331+
window = params.HistoryServeWindow
332+
}
333+
// set current block hash in the contract storage, compatible with EIP-2935
334+
ringIndex := uint64(ctx.BlockHeight()) % window //nolint:gosec // G115 // won't exceed uint64
335+
var key common.Hash
336+
binary.BigEndian.PutUint64(key[24:], ringIndex)
337+
k.SetState(ctx, ethparams.HistoryStorageAddress, key, ctx.HeaderHash())
338+
} else {
339+
// fallback old implementation
340+
store := ctx.KVStore(k.storeKey)
341+
height, err := ethermint.SafeUint64(ctx.BlockHeight())
342+
if err != nil {
343+
panic(err)
344+
}
345+
store.Set(types.GetHeaderHashKey(height), ctx.HeaderHash())
329346
}
330-
store.Set(types.GetHeaderHashKey(height), ctx.HeaderHash())
331347
}
332348

333-
// GetHeaderHash retrieves the hash of a block header from the store by height.
334-
func (k Keeper) GetHeaderHash(ctx sdk.Context, height uint64) []byte {
349+
// GetHeaderHash sets block hash into EIP-2935 compatible storage contract.
350+
func (k Keeper) GetHeaderHash(ctx sdk.Context, height uint64) common.Hash {
351+
// check if history contract has been deployed
352+
acct := k.GetAccount(ctx, ethparams.HistoryStorageAddress)
353+
if acct != nil && acct.IsContract() {
354+
window := types.DefaultHistoryServeWindow
355+
params := k.GetParams(ctx)
356+
if params.HistoryServeWindow > 0 {
357+
window = params.HistoryServeWindow
358+
}
359+
360+
ringIndex := height % window
361+
var key common.Hash
362+
binary.BigEndian.PutUint64(key[24:], ringIndex)
363+
hash := k.GetState(ctx, ethparams.HistoryStorageAddress, key)
364+
365+
if hash.Cmp(common.Hash{}) != 0 {
366+
return hash
367+
}
368+
}
369+
// fall back to old behavior for retro compatibility
370+
// TODO can be removed along with DeleteHeaderHash once HistoryStorage has been filled up in next protocol upgrade
335371
store := ctx.KVStore(k.storeKey)
336-
return store.Get(types.GetHeaderHashKey(height))
372+
hashByte := store.Get(types.GetHeaderHashKey(height))
373+
if len(hashByte) > 0 {
374+
return common.BytesToHash(hashByte)
375+
}
376+
return common.Hash{}
337377
}
338378

339379
// DeleteHeaderHash removes the hash of a block header from the store by height

x/evm/keeper/keeper_test.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
_ "embed"
55
"math"
66
"math/big"
7+
"strings"
78
"testing"
89

910
sdkmath "cosmossdk.io/math"
@@ -127,7 +128,11 @@ func (suite *KeeperTestSuite) TestGetAccountStorage() {
127128
if tc.malleate != nil {
128129
contractAddr = tc.malleate()
129130
}
130-
i := 0
131+
132+
var results []struct {
133+
addr common.Address
134+
storage types.Storage
135+
}
131136
suite.App.AccountKeeper.IterateAccounts(suite.Ctx, func(account sdk.AccountI) bool {
132137
ethAccount, ok := account.(ethermint.EthAccountI)
133138
if !ok {
@@ -137,21 +142,39 @@ func (suite *KeeperTestSuite) TestGetAccountStorage() {
137142

138143
addr := ethAccount.EthAddress()
139144
storage := suite.App.EvmKeeper.GetAccountStorage(suite.Ctx, addr)
145+
results = append(results, struct {
146+
addr common.Address
147+
storage types.Storage
148+
}{addr, storage})
149+
return false
150+
})
151+
152+
isPreinstall := func(addr common.Address) bool {
153+
for _, p := range types.DefaultPreinstalls {
154+
if strings.EqualFold(addr.Hex(), p.Address) {
155+
return true
156+
}
157+
}
158+
return false
159+
}
140160

141-
if addr == contractAddr {
142-
s.Require().NotEqual(0, len(storage),
143-
"expected account %d to have non-zero amount of storage slots, got %d",
144-
i, len(storage),
161+
for _, r := range results {
162+
if isPreinstall(r.addr) {
163+
// skip preinstall
164+
continue
165+
}
166+
if r.addr == contractAddr {
167+
suite.Require().NotEqual(0, len(r.storage),
168+
"expected account address %s to have non-zero amount of storage slots, got %d",
169+
r.addr.Hex(), len(r.storage),
145170
)
146171
} else {
147-
s.Require().Len(storage, 0,
148-
"expected account %d to have %d storage slots, got %d",
149-
i, 0, len(storage),
172+
suite.Require().Len(r.storage, 0,
173+
"expected account address %s to have %d storage slots, got %d",
174+
r.addr.Hex(), 0, len(r.storage),
150175
)
151176
}
152-
i++
153-
return false
154-
})
177+
}
155178
})
156179
}
157180
}

x/evm/keeper/state_transition.go

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"math/big"
2222
"sort"
2323

24+
cmttypes "github.com/cometbft/cometbft/types"
25+
2426
errorsmod "cosmossdk.io/errors"
2527
sdkmath "cosmossdk.io/math"
2628
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -29,7 +31,6 @@ import (
2931
"github.com/evmos/ethermint/x/evm/types"
3032
"github.com/holiman/uint256"
3133

32-
cmttypes "github.com/cometbft/cometbft/types"
3334
"github.com/ethereum/go-ethereum/common"
3435
"github.com/ethereum/go-ethereum/core"
3536
"github.com/ethereum/go-ethereum/core/tracing"
@@ -95,8 +96,8 @@ func (k *Keeper) NewEVM(
9596

9697
// GetHashFn implements vm.GetHashFunc for Ethermint. It returns hash for 3 cases:
9798
// 1. The requested height matches current block height from the context.
98-
// 2. The requested height is within the valid range, retrieve the hash from GetHeaderHash for heights after sdk50.
99-
// 3. The requested height is within the valid range, retrieve the hash from GetHistoricalInfo for heights before sdk50.
99+
// 2. The requested height is below current block height, follow EIP-2935.
100+
// 3. The requested height is above current block height, return empty
100101
func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc {
101102
return func(num64 uint64) common.Hash {
102103
h, err := ethermint.SafeInt64(num64)
@@ -114,31 +115,36 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc {
114115
}
115116
}
116117
// Align check with https://github.com/ethereum/go-ethereum/blob/release/1.11/core/vm/instructions.go#L433
117-
headerNum := k.GetParams(ctx).HeaderHashNum
118118
var lower uint64
119+
headerNum := k.GetParams(ctx).HeaderHashNum
119120
if upper <= headerNum {
120121
lower = 0
121122
} else {
122123
lower = upper - headerNum
123124
}
124-
if num64 < lower || num64 >= upper {
125-
return common.Hash{}
126-
}
127-
hash := k.GetHeaderHash(ctx, num64)
128-
if len(hash) > 0 {
129-
return common.BytesToHash(hash)
130-
}
131-
histInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, h)
132-
if err != nil {
133-
k.Logger(ctx).Debug("historical info not found", "height", h, "err", err.Error())
134-
return common.Hash{}
135-
}
136-
header, err := cmttypes.HeaderFromProto(&histInfo.Header)
137-
if err != nil {
138-
k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err)
139-
return common.Hash{}
125+
126+
if upper > num64 {
127+
// The requested height is historical, query EIP-2935 contract storage
128+
headerHash := k.GetHeaderHash(ctx, num64)
129+
if headerHash.Cmp(common.Hash{}) != 0 {
130+
return headerHash
131+
} else if num64 >= lower {
132+
// Pre upgrade case
133+
// In case EIP-2935 is not supported and data cannot be found, we fetch historical info
134+
histInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, h)
135+
if err != nil {
136+
k.Logger(ctx).Debug("historical info not found", "height", h, "err", err.Error())
137+
return common.Hash{}
138+
}
139+
header, err := cmttypes.HeaderFromProto(&histInfo.Header)
140+
if err != nil {
141+
k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err)
142+
return common.Hash{}
143+
}
144+
return common.BytesToHash(header.Hash())
145+
}
140146
}
141-
return common.BytesToHash(header.Hash())
147+
return common.Hash{}
142148
}
143149
}
144150

x/evm/keeper/state_transition_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,6 @@ func (suite *StateTransitionTestSuite) TestGetHashFn() {
169169
func(_ int64) {},
170170
common.Hash{},
171171
},
172-
{
173-
"height less than header hash num range",
174-
height - evmtypes.DefaultHeaderHashNum - 1,
175-
func(_ int64) {},
176-
common.Hash{},
177-
},
178172
{
179173
"header not found in stores",
180174
height - 1,

x/evm/migrations/v4/types/params_v4.pb.go

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x/evm/types/params.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ var (
3636
DefaultEnableCall = true
3737
// DefaultHeaderHashNum defines the default number of header hash to persist.
3838
DefaultHeaderHashNum = uint64(256)
39+
// DefaultHistoryServeWindow DefaultHeaderHashNum defines the default number of hystorical value to serve for EIP2935.
40+
DefaultHistoryServeWindow = uint64(8192) // same as EIP-2935
3941
)
4042

4143
// NewParams creates a new Params instance
@@ -61,6 +63,7 @@ func DefaultParams() Params {
6163
ChainConfig: config,
6264
AllowUnprotectedTxs: DefaultAllowUnprotectedTxs,
6365
HeaderHashNum: DefaultHeaderHashNum,
66+
HistoryServeWindow: DefaultHistoryServeWindow,
6467
}
6568
}
6669

0 commit comments

Comments
 (0)