Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (evm) [#740](https://github.com/crypto-org-chain/ethermint/pull/740) fix: missing tx context during vm initialisation
* (evm) [#742](https://github.com/crypto-org-chain/ethermint/pull/742) fix: prevent nil pointer dereference in tracer hooks
* (evm) [#728](https://github.com/crypto-org-chain/ethermint/pull/728) feat: support preinstalls
* (evm) [#722](https://github.com/crypto-org-chain/ethermint/pull/722) feat: support EIP-2935

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

Expand Down
3 changes: 3 additions & 0 deletions proto/ethermint/evm/v1/params.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ message Params {
bool allow_unprotected_txs = 6;
// header_hash_num is the number of header hash to persist.
uint64 header_hash_num = 7;
// historyServeWindow for EIP 2935
uint64 history_serve_window = 8;

}
64 changes: 52 additions & 12 deletions x/evm/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package keeper

import (
"encoding/binary"
"math/big"

"github.com/ethereum/go-ethereum/crypto"
Expand All @@ -31,15 +32,15 @@ import (
"github.com/ethereum/go-ethereum/core/tracing"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
ethparams "github.com/ethereum/go-ethereum/params"
ethermint "github.com/evmos/ethermint/types"
"github.com/evmos/ethermint/x/evm/statedb"
"github.com/evmos/ethermint/x/evm/types"
"github.com/holiman/uint256"
)

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

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

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

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

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

// SetHeaderHash stores the hash of the current block header in the store.
func (k Keeper) SetHeaderHash(ctx sdk.Context) {
store := ctx.KVStore(k.storeKey)
height, err := ethermint.SafeUint64(ctx.BlockHeight())
if err != nil {
panic(err)
acct := k.GetAccount(ctx, ethparams.HistoryStorageAddress)
if acct != nil && acct.IsContract() {
window := types.DefaultHistoryServeWindow
params := k.GetParams(ctx)
if params.HistoryServeWindow > 0 {
window = params.HistoryServeWindow
}
// set current block hash in the contract storage, compatible with EIP-2935
ringIndex := uint64(ctx.BlockHeight()) % window //nolint:gosec // G115 // won't exceed uint64
var key common.Hash
binary.BigEndian.PutUint64(key[24:], ringIndex)
k.SetState(ctx, ethparams.HistoryStorageAddress, key, ctx.HeaderHash())
} else {
// fallback old implementation
store := ctx.KVStore(k.storeKey)
height, err := ethermint.SafeUint64(ctx.BlockHeight())
if err != nil {
panic(err)
}
store.Set(types.GetHeaderHashKey(height), ctx.HeaderHash())
}
store.Set(types.GetHeaderHashKey(height), ctx.HeaderHash())
}

// GetHeaderHash retrieves the hash of a block header from the store by height.
func (k Keeper) GetHeaderHash(ctx sdk.Context, height uint64) []byte {
// GetHeaderHash sets block hash into EIP-2935 compatible storage contract.
func (k Keeper) GetHeaderHash(ctx sdk.Context, height uint64) common.Hash {
// check if history contract has been deployed
acct := k.GetAccount(ctx, ethparams.HistoryStorageAddress)
if acct != nil && acct.IsContract() {
window := types.DefaultHistoryServeWindow
params := k.GetParams(ctx)
if params.HistoryServeWindow > 0 {
window = params.HistoryServeWindow
}

ringIndex := height % window
var key common.Hash
binary.BigEndian.PutUint64(key[24:], ringIndex)
hash := k.GetState(ctx, ethparams.HistoryStorageAddress, key)

if hash.Cmp(common.Hash{}) != 0 {
return hash
}
}
// fall back to old behavior for retro compatibility
// TODO can be removed along with DeleteHeaderHash once HistoryStorage has been filled up in next protocol upgrade
store := ctx.KVStore(k.storeKey)
return store.Get(types.GetHeaderHashKey(height))
hashByte := store.Get(types.GetHeaderHashKey(height))
if len(hashByte) > 0 {
return common.BytesToHash(hashByte)
}
return common.Hash{}
}

// DeleteHeaderHash removes the hash of a block header from the store by height
Expand Down
45 changes: 34 additions & 11 deletions x/evm/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
_ "embed"
"math"
"math/big"
"strings"
"testing"

sdkmath "cosmossdk.io/math"
Expand Down Expand Up @@ -127,7 +128,11 @@ func (suite *KeeperTestSuite) TestGetAccountStorage() {
if tc.malleate != nil {
contractAddr = tc.malleate()
}
i := 0

var results []struct {
addr common.Address
storage types.Storage
}
suite.App.AccountKeeper.IterateAccounts(suite.Ctx, func(account sdk.AccountI) bool {
ethAccount, ok := account.(ethermint.EthAccountI)
if !ok {
Expand All @@ -137,21 +142,39 @@ func (suite *KeeperTestSuite) TestGetAccountStorage() {

addr := ethAccount.EthAddress()
storage := suite.App.EvmKeeper.GetAccountStorage(suite.Ctx, addr)
results = append(results, struct {
addr common.Address
storage types.Storage
}{addr, storage})
return false
})

isPreinstall := func(addr common.Address) bool {
for _, p := range types.DefaultPreinstalls {
if strings.EqualFold(addr.Hex(), p.Address) {
return true
}
}
return false
}

if addr == contractAddr {
s.Require().NotEqual(0, len(storage),
"expected account %d to have non-zero amount of storage slots, got %d",
i, len(storage),
for _, r := range results {
if isPreinstall(r.addr) {
// skip preinstall
continue
}
if r.addr == contractAddr {
suite.Require().NotEqual(0, len(r.storage),
"expected account address %s to have non-zero amount of storage slots, got %d",
r.addr.Hex(), len(r.storage),
)
} else {
s.Require().Len(storage, 0,
"expected account %d to have %d storage slots, got %d",
i, 0, len(storage),
suite.Require().Len(r.storage, 0,
"expected account address %s to have %d storage slots, got %d",
r.addr.Hex(), 0, len(r.storage),
)
}
i++
return false
})
}
})
}
}
Expand Down
48 changes: 27 additions & 21 deletions x/evm/keeper/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"math/big"
"sort"

cmttypes "github.com/cometbft/cometbft/types"

errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -29,7 +31,6 @@ import (
"github.com/evmos/ethermint/x/evm/types"
"github.com/holiman/uint256"

cmttypes "github.com/cometbft/cometbft/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/tracing"
Expand Down Expand Up @@ -95,8 +96,8 @@ func (k *Keeper) NewEVM(

// GetHashFn implements vm.GetHashFunc for Ethermint. It returns hash for 3 cases:
// 1. The requested height matches current block height from the context.
// 2. The requested height is within the valid range, retrieve the hash from GetHeaderHash for heights after sdk50.
// 3. The requested height is within the valid range, retrieve the hash from GetHistoricalInfo for heights before sdk50.
// 2. The requested height is below current block height, follow EIP-2935.
// 3. The requested height is above current block height, return empty
func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc {
return func(num64 uint64) common.Hash {
h, err := ethermint.SafeInt64(num64)
Expand All @@ -114,31 +115,36 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc {
}
}
// Align check with https://github.com/ethereum/go-ethereum/blob/release/1.11/core/vm/instructions.go#L433
headerNum := k.GetParams(ctx).HeaderHashNum
var lower uint64
headerNum := k.GetParams(ctx).HeaderHashNum
if upper <= headerNum {
lower = 0
} else {
lower = upper - headerNum
}
if num64 < lower || num64 >= upper {
return common.Hash{}
}
hash := k.GetHeaderHash(ctx, num64)
if len(hash) > 0 {
return common.BytesToHash(hash)
}
histInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, h)
if err != nil {
k.Logger(ctx).Debug("historical info not found", "height", h, "err", err.Error())
return common.Hash{}
}
header, err := cmttypes.HeaderFromProto(&histInfo.Header)
if err != nil {
k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err)
return common.Hash{}

if upper > num64 {
// The requested height is historical, query EIP-2935 contract storage
headerHash := k.GetHeaderHash(ctx, num64)
if headerHash.Cmp(common.Hash{}) != 0 {
return headerHash
} else if num64 >= lower {
// Pre upgrade case
// In case EIP-2935 is not supported and data cannot be found, we fetch historical info
histInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, h)
if err != nil {
k.Logger(ctx).Debug("historical info not found", "height", h, "err", err.Error())
return common.Hash{}
}
header, err := cmttypes.HeaderFromProto(&histInfo.Header)
if err != nil {
k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err)
return common.Hash{}
}
return common.BytesToHash(header.Hash())
}
}
return common.BytesToHash(header.Hash())
return common.Hash{}
}
}

Expand Down
6 changes: 0 additions & 6 deletions x/evm/keeper/state_transition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,6 @@ func (suite *StateTransitionTestSuite) TestGetHashFn() {
func(_ int64) {},
common.Hash{},
},
{
"height less than header hash num range",
height - evmtypes.DefaultHeaderHashNum - 1,
func(_ int64) {},
common.Hash{},
},
{
"header not found in stores",
height - 1,
Expand Down
3 changes: 1 addition & 2 deletions x/evm/migrations/v4/types/params_v4.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions x/evm/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ var (
DefaultEnableCall = true
// DefaultHeaderHashNum defines the default number of header hash to persist.
DefaultHeaderHashNum = uint64(256)
// DefaultHistoryServeWindow DefaultHeaderHashNum defines the default number of hystorical value to serve for EIP2935.
DefaultHistoryServeWindow = uint64(8192) // same as EIP-2935
)

// NewParams creates a new Params instance
Expand All @@ -61,6 +63,7 @@ func DefaultParams() Params {
ChainConfig: config,
AllowUnprotectedTxs: DefaultAllowUnprotectedTxs,
HeaderHashNum: DefaultHeaderHashNum,
HistoryServeWindow: DefaultHistoryServeWindow,
}
}

Expand Down
Loading
Loading