From c2604af258b40f2b624a3e6fcb07b21f622baf5f Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Mon, 24 Aug 2026 10:19:11 +0800 Subject: [PATCH] perf(evm): cache repeated balance lookups --- giga/deps/xbank/keeper/send.go | 4 ++ giga/deps/xevm/keeper/balance.go | 7 ++- giga/deps/xevm/state/balance_test.go | 19 +++++++ giga/deps/xevm/state/statedb.go | 18 ++++++- sei-cosmos/types/context.go | 22 ++++---- sei-cosmos/types/evm_balance_cache.go | 65 +++++++++++++++++++++++ sei-cosmos/x/auth/keeper/account.go | 2 + sei-cosmos/x/bank/keeper/send.go | 4 ++ x/evm/keeper/balance.go | 7 ++- x/evm/state/balance_test.go | 75 +++++++++++++++++++++++++++ x/evm/state/statedb.go | 18 ++++++- 11 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 sei-cosmos/types/evm_balance_cache.go diff --git a/giga/deps/xbank/keeper/send.go b/giga/deps/xbank/keeper/send.go index f4c1b8152a..d852943e07 100644 --- a/giga/deps/xbank/keeper/send.go +++ b/giga/deps/xbank/keeper/send.go @@ -293,6 +293,7 @@ func (k BaseSendKeeper) initBalances(ctx sdk.Context, addr sdk.AccAddress, balan accountStore.Set([]byte(balance.Denom), bz) } } + ctx.InvalidateCachedEVMBalance(addr) return nil } @@ -312,6 +313,7 @@ func (k BaseSendKeeper) setBalance(ctx sdk.Context, addr sdk.AccAddress, balance bz := k.cdc.MustMarshal(&balance) accountStore.Set([]byte(balance.Denom), bz) } + ctx.InvalidateCachedEVMBalance(addr) return nil } @@ -320,6 +322,7 @@ func (k BaseSendKeeper) setWeiBalance(ctx sdk.Context, addr sdk.AccAddress, amt store := prefix.NewStore(k.GetKVStore(ctx), types.WeiBalancesPrefix) if amt.IsZero() { store.Delete(addr) + ctx.InvalidateCachedEVMBalance(addr) return nil } val, err := amt.Marshal() @@ -327,6 +330,7 @@ func (k BaseSendKeeper) setWeiBalance(ctx sdk.Context, addr sdk.AccAddress, amt return err } store.Set(addr, val) + ctx.InvalidateCachedEVMBalance(addr) return nil } diff --git a/giga/deps/xevm/keeper/balance.go b/giga/deps/xevm/keeper/balance.go index 4fdcedd223..9d5b8fa209 100644 --- a/giga/deps/xevm/keeper/balance.go +++ b/giga/deps/xevm/keeper/balance.go @@ -8,10 +8,15 @@ import ( ) func (k *Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) *big.Int { + if balance, ok := ctx.GetCachedEVMBalance(addr); ok { + return balance + } denom := k.GetBaseDenom(ctx) allUsei := k.BankKeeper().GetBalance(ctx, addr, denom).Amount lockedUsei := k.BankKeeper().LockedCoins(ctx, addr).AmountOf(denom) // LockedCoins doesn't use iterators usei := allUsei.Sub(lockedUsei) wei := k.BankKeeper().GetWeiBalance(ctx, addr) - return usei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() + balance := usei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() + ctx.SetCachedEVMBalance(addr, balance) + return balance } diff --git a/giga/deps/xevm/state/balance_test.go b/giga/deps/xevm/state/balance_test.go index 802c37dd01..87034b01d1 100644 --- a/giga/deps/xevm/state/balance_test.go +++ b/giga/deps/xevm/state/balance_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/holiman/uint256" testkeeper "github.com/sei-protocol/sei-chain/giga/deps/testutil/keeper" @@ -13,6 +14,24 @@ import ( "github.com/stretchr/testify/require" ) +func TestGetCodeHashCachesBalanceForNoCodeAddress(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper(t) + ctx = ctx.WithBlockTime(time.Now()) + _, evmAddr := testkeeper.MockAddressPair() + db := state.NewDBImpl(ctx, k, false) + meter := db.Ctx().GasMeter() + + before := meter.GasConsumed() + require.Equal(t, common.Hash{}, db.GetCodeHash(evmAddr)) + firstReadCost := meter.GasConsumed() - before + + before = meter.GasConsumed() + require.Equal(t, common.Hash{}, db.GetCodeHash(evmAddr)) + secondReadCost := meter.GasConsumed() - before + + require.Less(t, secondReadCost, firstReadCost) +} + func TestAddBalance(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper(t) ctx = ctx.WithBlockTime(time.Now()) diff --git a/giga/deps/xevm/state/statedb.go b/giga/deps/xevm/state/statedb.go index 7f171de3ac..e8a8fc4732 100644 --- a/giga/deps/xevm/state/statedb.go +++ b/giga/deps/xevm/state/statedb.go @@ -55,6 +55,11 @@ type DBImpl struct { func NewDBImpl(ctx sdk.Context, k EVMKeeper, simulation bool) *DBImpl { feeCollector, _ := k.GetFeeCollectorAddress(ctx) + // Nested DBs reuse the request cache so writes can invalidate balances warmed + // by an outer DB. Each snapshot layer still has a distinct context identity. + if ctx.EVMBalanceCache() == nil { + ctx = ctx.WithEVMBalanceCache(sdk.NewEVMBalanceCache()) + } s := &DBImpl{ ctx: ctx, k: k, @@ -98,6 +103,7 @@ func (s *DBImpl) Cleanup() { s.tempState = nil s.logger = nil s.snapshottedCtxs = nil + s.ctx = s.ctx.WithEVMBalanceCache(nil) clear(s.codeCache) } @@ -107,6 +113,7 @@ func (s *DBImpl) CleanupForTracer() { s.ctx = s.snapshottedCtxs[0] } feeCollector, _ := s.k.GetFeeCollectorAddress(s.Ctx()) + s.ctx = s.ctx.WithEVMBalanceCache(sdk.NewEVMBalanceCache()) s.coinbaseEvmAddress = feeCollector s.tempState = NewTemporaryState() s.journal = []journalEntry{} @@ -121,6 +128,11 @@ func (s *DBImpl) CleanupForTracer() { // CacheMultiStore.Write() on any shared store layer. func (s *DBImpl) ResetForTracer() { feeCollector, _ := s.k.GetFeeCollectorAddress(s.Ctx()) + balanceCache := sdk.NewEVMBalanceCache() + s.ctx = s.ctx.WithEVMBalanceCache(balanceCache) + for i := range s.snapshottedCtxs { + s.snapshottedCtxs[i] = s.snapshottedCtxs[i].WithEVMBalanceCache(balanceCache) + } s.coinbaseEvmAddress = feeCollector s.tempState = NewTemporaryState() s.journal = []journalEntry{} @@ -184,12 +196,16 @@ func (s *DBImpl) GetStorageRoot(common.Address) common.Hash { } func (s *DBImpl) Copy() vm.StateDB { - newCtx := s.ctx.WithMultiStore(s.ctx.MultiStore().CacheMultiStore()).WithEventManager(sdk.NewEventManager()) + balanceCache := sdk.NewEVMBalanceCache() + newCtx := s.ctx.WithMultiStore(s.ctx.MultiStore().CacheMultiStore()).WithEventManager(sdk.NewEventManager()).WithEVMBalanceCache(balanceCache) journal := make([]journalEntry, len(s.journal)) copy(journal, s.journal) snapshots := make([]sdk.Context, len(s.snapshottedCtxs)+1) copy(snapshots, s.snapshottedCtxs) snapshots[len(s.snapshottedCtxs)] = s.ctx + for i := range snapshots { + snapshots[i] = snapshots[i].WithEVMBalanceCache(balanceCache) + } copied := &DBImpl{ ctx: newCtx, snapshottedCtxs: snapshots, diff --git a/sei-cosmos/types/context.go b/sei-cosmos/types/context.go index 7f2d881b96..587040c7a8 100644 --- a/sei-cosmos/types/context.go +++ b/sei-cosmos/types/context.go @@ -28,6 +28,7 @@ and standard additions here would be better just to add to the Context struct type Context struct { ctx context.Context ms MultiStore + multiStoreIdentity *multiStoreIdentity nextMs MultiStore // ms of the next height; only used in tracing nextStoreKeys map[string]struct{} // store key names that should use nextMs header tmproto.Header @@ -62,6 +63,7 @@ type Context struct { evmVmError string // EVM VM error during execution evmEntryViaWasmdPrecompile bool // EVM is entered via wasmd precompile directly evmPrecompileCalledFromDelegateCall bool // EVM precompile is called from a delegate call + evmBalanceCache *EVMBalanceCache messageIndex int // Used to track current message being processed txIndex int @@ -270,15 +272,16 @@ func NewContext(ms MultiStore, header tmproto.Header, isCheckTx bool) Context { // https://github.com/gogo/protobuf/issues/519 header.Time = header.Time.UTC() return Context{ - ctx: context.Background(), - ms: ms, - header: header, - chainID: header.ChainID, - checkTx: isCheckTx, - gasMeter: NewInfiniteGasMeter(1, 1), - minGasPrice: DecCoins{}, - eventManager: NewEventManager(), - evmEventManager: NewEVMEventManager(), + ctx: context.Background(), + ms: ms, + multiStoreIdentity: newMultiStoreIdentity(), + header: header, + chainID: header.ChainID, + checkTx: isCheckTx, + gasMeter: NewInfiniteGasMeter(1, 1), + minGasPrice: DecCoins{}, + eventManager: NewEventManager(), + evmEventManager: NewEVMEventManager(), } } @@ -291,6 +294,7 @@ func (c Context) WithContext(ctx context.Context) Context { // WithMultiStore returns a Context with an updated MultiStore. func (c Context) WithMultiStore(ms MultiStore) Context { c.ms = ms + c.multiStoreIdentity = newMultiStoreIdentity() return c } diff --git a/sei-cosmos/types/evm_balance_cache.go b/sei-cosmos/types/evm_balance_cache.go new file mode 100644 index 0000000000..dd9f456139 --- /dev/null +++ b/sei-cosmos/types/evm_balance_cache.go @@ -0,0 +1,65 @@ +package types + +import "math/big" + +type multiStoreIdentity byte + +func newMultiStoreIdentity() *multiStoreIdentity { + return new(multiStoreIdentity) +} + +// EVMBalanceCache stores spendable EVM balances for one execution request. +type EVMBalanceCache struct { + entries map[string]cachedEVMBalance +} + +type cachedEVMBalance struct { + multiStoreIdentity *multiStoreIdentity + balance big.Int +} + +// NewEVMBalanceCache returns an empty EVM balance cache. +func NewEVMBalanceCache() *EVMBalanceCache { + return &EVMBalanceCache{entries: map[string]cachedEVMBalance{}} +} + +// EVMBalanceCache returns the balance cache attached to the context, if any. +func (c Context) EVMBalanceCache() *EVMBalanceCache { + return c.evmBalanceCache +} + +// WithEVMBalanceCache returns a context that uses cache for EVM balance reads. +func (c Context) WithEVMBalanceCache(cache *EVMBalanceCache) Context { + c.evmBalanceCache = cache + return c +} + +// GetCachedEVMBalance returns the cached balance for addr in the active multistore layer. +func (c Context) GetCachedEVMBalance(addr AccAddress) (*big.Int, bool) { + if c.evmBalanceCache == nil { + return nil, false + } + entry, ok := c.evmBalanceCache.entries[string(addr)] + if !ok || entry.multiStoreIdentity != c.multiStoreIdentity { + return nil, false + } + return new(big.Int).Set(&entry.balance), true +} + +// SetCachedEVMBalance stores balance for addr in the active multistore layer. +func (c Context) SetCachedEVMBalance(addr AccAddress, balance *big.Int) { + if c.evmBalanceCache == nil { + return + } + entry := cachedEVMBalance{multiStoreIdentity: c.multiStoreIdentity} + entry.balance.Set(balance) + c.evmBalanceCache.entries[string(addr)] = entry +} + +// InvalidateCachedEVMBalance removes any cached balance for addr. +func (c Context) InvalidateCachedEVMBalance(addr AccAddress) { + if c.evmBalanceCache == nil { + return + } + delete(c.evmBalanceCache.entries, string(addr)) +} diff --git a/sei-cosmos/x/auth/keeper/account.go b/sei-cosmos/x/auth/keeper/account.go index 5ac79f0f3c..dce0e595ec 100644 --- a/sei-cosmos/x/auth/keeper/account.go +++ b/sei-cosmos/x/auth/keeper/account.go @@ -63,6 +63,7 @@ func (ak AccountKeeper) SetAccount(ctx sdk.Context, acc types.AccountI) { } store.Set(types.AddressStoreKey(addr), bz) + ctx.InvalidateCachedEVMBalance(addr) } // RemoveAccount removes an account for the account mapper store. @@ -71,6 +72,7 @@ func (ak AccountKeeper) RemoveAccount(ctx sdk.Context, acc types.AccountI) { addr := acc.GetAddress() store := ctx.KVStore(ak.key) store.Delete(types.AddressStoreKey(addr)) + ctx.InvalidateCachedEVMBalance(addr) } // IterateAccounts iterates over all the stored accounts and performs a callback function. diff --git a/sei-cosmos/x/bank/keeper/send.go b/sei-cosmos/x/bank/keeper/send.go index 06facbb02c..228ca9bb28 100644 --- a/sei-cosmos/x/bank/keeper/send.go +++ b/sei-cosmos/x/bank/keeper/send.go @@ -315,6 +315,7 @@ func (k BaseSendKeeper) initBalances(ctx sdk.Context, addr sdk.AccAddress, balan accountStore.Set([]byte(balance.Denom), bz) } } + ctx.InvalidateCachedEVMBalance(addr) return nil } @@ -334,6 +335,7 @@ func (k BaseSendKeeper) setBalance(ctx sdk.Context, addr sdk.AccAddress, balance bz := k.cdc.MustMarshal(&balance) accountStore.Set([]byte(balance.Denom), bz) } + ctx.InvalidateCachedEVMBalance(addr) return nil } @@ -342,6 +344,7 @@ func (k BaseSendKeeper) setWeiBalance(ctx sdk.Context, addr sdk.AccAddress, amt store := prefix.NewStore(ctx.KVStore(k.storeKey), types.WeiBalancesPrefix) if amt.IsZero() { store.Delete(addr) + ctx.InvalidateCachedEVMBalance(addr) return nil } val, err := amt.Marshal() @@ -349,6 +352,7 @@ func (k BaseSendKeeper) setWeiBalance(ctx sdk.Context, addr sdk.AccAddress, amt return err } store.Set(addr, val) + ctx.InvalidateCachedEVMBalance(addr) return nil } diff --git a/x/evm/keeper/balance.go b/x/evm/keeper/balance.go index fac6a1ed4b..cdf0680dbe 100644 --- a/x/evm/keeper/balance.go +++ b/x/evm/keeper/balance.go @@ -8,10 +8,15 @@ import ( ) func (k *Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) *big.Int { + if balance, ok := ctx.GetCachedEVMBalance(addr); ok { + return balance + } denom := k.GetBaseDenom(ctx) allUsei := k.BankKeeper().GetBalance(ctx, addr, denom).Amount lockedUsei := k.BankKeeper().LockedCoins(ctx, addr).AmountOf(denom) // LockedCoins doesn't use iterators usei := allUsei.Sub(lockedUsei) wei := k.BankKeeper().GetWeiBalance(ctx, addr) - return usei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() + balance := usei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() + ctx.SetCachedEVMBalance(addr, balance) + return balance } diff --git a/x/evm/state/balance_test.go b/x/evm/state/balance_test.go index c621de944d..6600b3a10f 100644 --- a/x/evm/state/balance_test.go +++ b/x/evm/state/balance_test.go @@ -4,15 +4,90 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" + ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/holiman/uint256" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" + vestingtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" testkeeper "github.com/sei-protocol/sei-chain/testutil/keeper" "github.com/sei-protocol/sei-chain/x/evm/state" "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/stretchr/testify/require" ) +func TestGetCodeHashCachesBalanceForNoCodeAddress(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) + _, evmAddr := testkeeper.MockAddressPair() + db := state.NewDBImpl(ctx, k, false) + meter := db.Ctx().GasMeter() + + before := meter.GasConsumed() + require.Equal(t, common.Hash{}, db.GetCodeHash(evmAddr)) + firstReadCost := meter.GasConsumed() - before + + before = meter.GasConsumed() + require.Equal(t, common.Hash{}, db.GetCodeHash(evmAddr)) + secondReadCost := meter.GasConsumed() - before + + require.Less(t, secondReadCost, firstReadCost) +} + +func TestBalanceCacheTracksWritesAndReverts(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) + _, evmAddr := testkeeper.MockAddressPair() + db := state.NewDBImpl(ctx, k, false) + + require.Equal(t, common.Hash{}, db.GetCodeHash(evmAddr)) + db.AddBalance(evmAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + require.Equal(t, ethtypes.EmptyCodeHash, db.GetCodeHash(evmAddr)) + + revision := db.Snapshot() + db.SubBalance(evmAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + require.Equal(t, common.Hash{}, db.GetCodeHash(evmAddr)) + + db.RevertToSnapshot(revision) + require.Equal(t, ethtypes.EmptyCodeHash, db.GetCodeHash(evmAddr)) +} + +func TestBalanceCacheInvalidatedByAccountWrite(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + now := time.Now() + ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(now) + seiAddr, evmAddr := testkeeper.MockAddressPair() + db := state.NewDBImpl(ctx, k, false) + k.SetAddressMapping(db.Ctx(), seiAddr, evmAddr) + + coins := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.OneInt())) + baseAccount := authtypes.NewBaseAccountWithAddress(seiAddr) + vestingAccount := vestingtypes.NewContinuousVestingAccount(baseAccount, coins, now.Unix(), now.Add(time.Hour).Unix(), nil) + k.AccountKeeper().SetAccount(db.Ctx(), vestingAccount) + require.NoError(t, k.BankKeeper().MintCoins(db.Ctx(), types.ModuleName, coins)) + require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(db.Ctx(), types.ModuleName, seiAddr, coins)) + require.Zero(t, db.GetBalance(evmAddr).Sign()) + + k.AccountKeeper().SetAccount(db.Ctx(), baseAccount) + require.Equal(t, uint256.NewInt(1_000_000_000_000), db.GetBalance(evmAddr)) +} + +func TestBalanceCacheSharedWithNestedStateDB(t *testing.T) { + k := &testkeeper.EVMTestApp.EvmKeeper + ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) + _, evmAddr := testkeeper.MockAddressPair() + outer := state.NewDBImpl(ctx, k, false) + require.Zero(t, outer.GetBalance(evmAddr).Sign()) + + inner := state.NewDBImpl(outer.Ctx(), k, false) + inner.AddBalance(evmAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + _, err := inner.Finalize() + require.NoError(t, err) + + require.Equal(t, uint256.NewInt(1), outer.GetBalance(evmAddr)) +} + func TestAddBalance(t *testing.T) { k := &testkeeper.EVMTestApp.EvmKeeper ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now()) diff --git a/x/evm/state/statedb.go b/x/evm/state/statedb.go index 6fdaf821a5..2346d3f0f9 100644 --- a/x/evm/state/statedb.go +++ b/x/evm/state/statedb.go @@ -59,6 +59,11 @@ type DBImpl struct { func NewDBImpl(ctx sdk.Context, k EVMKeeper, simulation bool) *DBImpl { feeCollector, _ := k.GetFeeCollectorAddress(ctx) + // Nested DBs reuse the request cache so writes can invalidate balances warmed + // by an outer DB. Each snapshot layer still has a distinct context identity. + if ctx.EVMBalanceCache() == nil { + ctx = ctx.WithEVMBalanceCache(sdk.NewEVMBalanceCache()) + } s := &DBImpl{ ctx: ctx, k: k, @@ -106,6 +111,7 @@ func (s *DBImpl) Cleanup() { s.tempState = nil s.logger = nil s.snapshottedCtxs = nil + s.ctx = s.ctx.WithEVMBalanceCache(nil) clear(s.codeCache) } @@ -115,6 +121,7 @@ func (s *DBImpl) CleanupForTracer() { s.ctx = s.snapshottedCtxs[0] } feeCollector, _ := s.k.GetFeeCollectorAddress(s.Ctx()) + s.ctx = s.ctx.WithEVMBalanceCache(sdk.NewEVMBalanceCache()) s.coinbaseEvmAddress = feeCollector s.tempState = NewTemporaryState() s.journal = []journalEntry{} @@ -129,6 +136,11 @@ func (s *DBImpl) CleanupForTracer() { // CacheMultiStore.Write() on any shared store layer. func (s *DBImpl) ResetForTracer() { feeCollector, _ := s.k.GetFeeCollectorAddress(s.Ctx()) + balanceCache := sdk.NewEVMBalanceCache() + s.ctx = s.ctx.WithEVMBalanceCache(balanceCache) + for i := range s.snapshottedCtxs { + s.snapshottedCtxs[i] = s.snapshottedCtxs[i].WithEVMBalanceCache(balanceCache) + } s.coinbaseEvmAddress = feeCollector s.tempState = NewTemporaryState() s.journal = []journalEntry{} @@ -191,12 +203,16 @@ func (s *DBImpl) GetStorageRoot(common.Address) common.Hash { } func (s *DBImpl) Copy() vm.StateDB { - newCtx := s.ctx.WithMultiStore(s.ctx.MultiStore().CacheMultiStore()).WithEventManager(sdk.NewEventManager()) + balanceCache := sdk.NewEVMBalanceCache() + newCtx := s.ctx.WithMultiStore(s.ctx.MultiStore().CacheMultiStore()).WithEventManager(sdk.NewEventManager()).WithEVMBalanceCache(balanceCache) journal := make([]journalEntry, len(s.journal)) copy(journal, s.journal) snapshots := make([]sdk.Context, len(s.snapshottedCtxs)+1) copy(snapshots, s.snapshottedCtxs) snapshots[len(s.snapshottedCtxs)] = s.ctx + for i := range snapshots { + snapshots[i] = snapshots[i].WithEVMBalanceCache(balanceCache) + } copied := &DBImpl{ ctx: newCtx, snapshottedCtxs: snapshots,