Skip to content
Draft
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
52 changes: 51 additions & 1 deletion giga/deps/store/cachekv.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"sort"
"sync"
"sync/atomic"

"github.com/sei-protocol/sei-chain/sei-cosmos/store/tracekv"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/types"
Expand All @@ -21,6 +22,14 @@ type Store struct {
parent types.KVStore
storeKey types.StoreKey
cacheSize int

// frozen/dirty/readParent implement the same "skip frozen empty layers on
// read" optimization as sei-cosmos/store/cachekv, so that a deep stack of
// empty EVM snapshot layers does not make each Get O(depth). See Freeze and
// readThroughParent there for the full invariant.
frozen atomic.Bool
dirty atomic.Bool
readParent atomic.Pointer[types.KVStore]
}

var _ types.CacheKVStore = (*Store)(nil)
Expand Down Expand Up @@ -50,7 +59,45 @@ func (store *Store) getFromCache(key []byte) []byte {
if cv, ok := store.cache.Load(UnsafeBytesToStr(key)); ok {
return cv.(*types.CValue).Value()
}
return store.parent.Get(key)
return store.readThroughParent().Get(key)
}

// readThroughParent returns the store a cache-missing read must fall through to,
// skipping any run of frozen empty layers. See the equivalently named method in
// sei-cosmos/store/cachekv for the full correctness argument; the invariant is
// identical (a frozen empty layer never gains writes, and RevertToSnapshot
// discards any layer that memoized a skip over a re-exposed one).
func (store *Store) readThroughParent() types.KVStore {
cp, ok := store.parent.(*Store)
if !ok || !cp.frozen.Load() || cp.dirty.Load() {
return store.parent
}
if p := store.readParent.Load(); p != nil {
return *p
}
p := store.parent
for {
next, ok := p.(*Store)
if !ok || !next.frozen.Load() || next.dirty.Load() {
break
}
p = next.parent
}
store.readParent.Store(&p)
return p
}

// Freeze marks the store as superseded by a newer cache layer so deeper layers
// may skip it for reads while it is empty. Opt-in and idempotent.
func (store *Store) Freeze() {
store.frozen.Store(true)
}

// Unfreeze reverts Freeze, marking the store writable again (e.g. when a layer is
// re-exposed by RevertToSnapshot). Deeper stores gate on the parent's live frozen
// bit, so unfreezing here also bypasses any stale memo that skipped this store.
func (store *Store) Unfreeze() {
store.frozen.Store(false)
}

// Get implements types.KVStore.
Expand Down Expand Up @@ -115,6 +162,8 @@ func (store *Store) Write() {

store.cache = &sync.Map{}
store.deleted = &sync.Map{}
store.dirty.Store(false)
store.readParent.Store(nil)
}

// CacheWrap implements CacheWrapper.
Expand Down Expand Up @@ -142,6 +191,7 @@ func (store *Store) setCacheValue(key, value []byte, deleted bool, dirty bool) {
} else {
store.deleted.Delete(keyStr)
}
store.dirty.Store(true)
}

func (store *Store) isDeleted(key string) bool {
Expand Down
57 changes: 57 additions & 0 deletions giga/deps/store/frozen_skip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package store_test

import (
"testing"

"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tm-db"

gigastore "github.com/sei-protocol/sei-chain/giga/deps/store"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/dbadapter"
"github.com/sei-protocol/sei-chain/sei-cosmos/store/types"
)

func bz(s string) []byte { return []byte(s) }

// TestGigaFrozenEmptyLayerSkip verifies that Get through a deep stack of frozen,
// empty giga cache layers returns exactly the base value (the giga store is used
// for the evm/bank stores, so EVM SLOAD at call depth N would otherwise walk N
// layers).
func TestGigaFrozenEmptyLayerSkip(t *testing.T) {
mem := dbadapter.Store{DB: dbm.NewMemDB()}
mem.Set(bz("k1"), bz("v1"))
mem.Set(bz("k2"), bz("v2"))

var parent types.KVStore = mem
layers := make([]*gigastore.Store, 32)
for i := 0; i < 32; i++ {
s := gigastore.NewStore(parent, types.NewKVStoreKey("giga"), types.DefaultCacheSizeLimit)
layers[i] = s
parent = s
}
for i := 0; i < len(layers)-1; i++ {
layers[i].Freeze()
}
top := layers[len(layers)-1]

require.Equal(t, bz("v1"), top.Get(bz("k1")))
require.Equal(t, bz("v2"), top.Get(bz("k2")))
require.Nil(t, top.Get(bz("missing")))
}

// TestGigaFrozenLayerWithWriteNotSkipped verifies a frozen giga layer with a write
// (or delete) still shadows the base and is never skipped.
func TestGigaFrozenLayerWithWriteNotSkipped(t *testing.T) {
mem := dbadapter.Store{DB: dbm.NewMemDB()}
mem.Set(bz("k1"), bz("v1"))
mem.Set(bz("k2"), bz("v2"))

mid := gigastore.NewStore(mem, types.NewKVStoreKey("giga"), types.DefaultCacheSizeLimit)
mid.Set(bz("k1"), bz("override"))
mid.Delete(bz("k2"))
mid.Freeze()

top := gigastore.NewStore(mid, types.NewKVStoreKey("giga"), types.DefaultCacheSizeLimit)
require.Equal(t, bz("override"), top.Get(bz("k1")))
require.Nil(t, top.Get(bz("k2")))
}
20 changes: 19 additions & 1 deletion giga/deps/xevm/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,18 @@ func (s *DBImpl) AnySelfDestructed() bool {
}

func (s *DBImpl) Snapshot() int {
newCtx := s.ctx.WithMultiStore(s.ctx.MultiStore().CacheMultiStore()).WithEventManager(sdk.NewEventManager())
oldMS := s.ctx.MultiStore()
newCtx := s.ctx.WithMultiStore(oldMS.CacheMultiStore()).WithEventManager(sdk.NewEventManager())
// Freeze the superseded layer so deeper layers can skip it for reads while it
// stays empty; this keeps a Cosmos read at call depth N from walking all N
// nested cache layers (quadratic). Never freeze the base layer
// (snapshottedCtxs[0]) — it is the flush target and is read directly by
// GetCommittedState. See x/evm/state/state.go for the full rationale.
if len(s.snapshottedCtxs) > 0 {
if f, ok := oldMS.(interface{ Freeze() }); ok {
f.Freeze()
}
}
s.snapshottedCtxs = append(s.snapshottedCtxs, s.ctx)
s.ctx = newCtx
version := len(s.snapshottedCtxs) - 1
Expand All @@ -130,6 +141,13 @@ func (s *DBImpl) RevertToSnapshot(rev int) {
s.ctx = s.snapshottedCtxs[rev]
s.snapshottedCtxs = s.snapshottedCtxs[:rev]

// The re-exposed layer is the writable top again; unfreeze it so reads no
// longer skip it (Snapshot froze it when it was superseded). See
// x/evm/state/state.go for the rationale.
if f, ok := s.ctx.MultiStore().(interface{ Unfreeze() }); ok {
f.Unfreeze()
}

// Find the watermark index to truncate the journal
watermarkIndex := -1
for i := len(s.journal) - 1; i >= 0; i-- {
Expand Down
Loading
Loading