diff --git a/app/app.go b/app/app.go index b905550bf7..e1e5e06fe6 100644 --- a/app/app.go +++ b/app/app.go @@ -1513,7 +1513,8 @@ func (app *App) CacheContext(ctx sdk.Context) (sdk.Context, sdk.CacheMultiStore) func (app *App) ExecuteTxsConcurrently(ctx sdk.Context, txs [][]byte, typedTxs []sdk.Tx) ([]*abci.ExecTxResult, sdk.Context) { // Giga only supports synchronous execution for now if app.GigaExecutorEnabled && app.GigaOCCEnabled { - return app.ProcessTXsWithOCCGiga(ctx, txs, typedTxs) + results, _ := app.ProcessTXsWithOCCGiga(ctx, txs, typedTxs) + return results, ctx } else if app.GigaExecutorEnabled { return app.ProcessTxsSynchronousGiga(ctx, txs, typedTxs), ctx } else if !ctx.IsOCCEnabled() { @@ -1593,6 +1594,9 @@ func (app *App) ProcessTXsWithOCCV2(ctx sdk.Context, txs [][]byte, typedTxs []sd // ProcessTXsWithOCCGiga runs the transactions concurrently via OCC, using the Giga executor func (app *App) ProcessTXsWithOCCGiga(ctx sdk.Context, txs [][]byte, typedTxs []sdk.Tx) ([]*abci.ExecTxResult, sdk.Context) { + ms := ctx.MultiStore().CacheMultiStore() + defer ms.Write() + ctx = ctx.WithMultiStore(ms) evmEntries := make([]*sdk.DeliverTxEntry, 0, len(txs)) v2Entries := make([]*sdk.DeliverTxEntry, 0, len(txs)) firstCosmosSeen := false @@ -1679,6 +1683,13 @@ func (app *App) ProcessTXsWithOCCGiga(ctx sdk.Context, txs [][]byte, typedTxs [] return nil, ctx } } + // Flush block-level giga Layer A (gigacachekv wrapping interblock.Cache) to + // the write-through interblock.Cache and on to CommitKVStore. This is + // required because the v2 OCC scheduler writes giga-key winning values into + // Layer A via MVS WriteLatestToStore(), but only WriteGiga() propagates + // those dirty entries to the parent. WriteState() only flushes the regular + // (non-giga) cachekv layer. + ctx.GigaMultiStore().WriteGiga() execResults := make([]*abci.ExecTxResult, 0, len(evmBatchResult)+len(v2BatchResult)) for _, r := range evmBatchResult { diff --git a/sei-cosmos/baseapp/baseapp.go b/sei-cosmos/baseapp/baseapp.go index 258082ff72..e0237b2c34 100644 --- a/sei-cosmos/baseapp/baseapp.go +++ b/sei-cosmos/baseapp/baseapp.go @@ -528,10 +528,21 @@ func (app *BaseApp) Seal() { app.sealed = true } // IsSealed returns true if the BaseApp is sealed and false otherwise. func (app *BaseApp) IsSealed() bool { return app.sealed } +// perStateCacheMultiStore is implemented by store backends that maintain +// isolated inter-block caches for deliverState and processProposalState. When +// app.cms satisfies this interface the two setter helpers use the dedicated +// per-state methods so that in-memory cache state does not leak across states. +// checkState intentionally does not use an inter-block cache. +type perStateCacheMultiStore interface { + CacheMultiStoreForDeliver() sdk.CacheMultiStore + CacheMultiStoreForProcessProposal() sdk.CacheMultiStore +} + // setCheckState sets the BaseApp's checkState with a branched multi-store // (i.e. a CacheMultiStore) and a new Context with the same multi-store branch, // provided header, and minimum gas prices set. It is set on InitChain and reset -// on Commit. +// on Commit. checkState does not use an inter-block cache so that it always +// reads from the last committed state without any cross-state cache pollution. func (app *BaseApp) setCheckState(header tmproto.Header) { ms := app.cms.CacheMultiStore() ctx := sdk.NewContext(ms, header, true).WithMinGasPrices(app.minGasPrices) @@ -554,7 +565,12 @@ func (app *BaseApp) setCheckState(header tmproto.Header) { // and provided header. It is set on InitChain and BeginBlock and set to nil on // Commit. func (app *BaseApp) setDeliverState(header tmproto.Header) { - ms := app.cms.CacheMultiStore() + var ms sdk.CacheMultiStore + if psms, ok := app.cms.(perStateCacheMultiStore); ok { + ms = psms.CacheMultiStoreForDeliver() + } else { + ms = app.cms.CacheMultiStore() + } ctx := sdk.NewContext(ms, header, false) if app.deliverState == nil { app.deliverState = &state{ @@ -569,7 +585,12 @@ func (app *BaseApp) setDeliverState(header tmproto.Header) { } func (app *BaseApp) setProcessProposalState(header tmproto.Header) { - ms := app.cms.CacheMultiStore() + var ms sdk.CacheMultiStore + if psms, ok := app.cms.(perStateCacheMultiStore); ok { + ms = psms.CacheMultiStoreForProcessProposal() + } else { + ms = app.cms.CacheMultiStore() + } ctx := sdk.NewContext(ms, header, false) if app.processProposalState == nil { app.processProposalState = &state{ diff --git a/sei-cosmos/store/cachemulti/store.go b/sei-cosmos/store/cachemulti/store.go index d6f0e8f93c..f2e5e9b16e 100644 --- a/sei-cosmos/store/cachemulti/store.go +++ b/sei-cosmos/store/cachemulti/store.go @@ -29,6 +29,13 @@ type Store struct { gigaStores map[types.StoreKey]types.KVStore gigaKeys []types.StoreKey + // interBlockCaches holds the raw inter-block cache references (one per + // giga-registered store key) propagated from the OCC rootmulti. When a + // child cachemulti is created via CacheMultiStore(), it uses these caches + // directly as the giga parent so that the layer chain is always + // gigacachekv → interblock.Cache → CommitKVStore + // rather than adding an extra gigacachekv level of indirection. + interBlockCaches map[types.StoreKey]types.KVStore traceWriter io.Writer traceContext types.TraceContext @@ -51,6 +58,11 @@ func NewFromKVStore( ) Store { cms := newStoreWithoutGiga(store, stores, keys, gigaKeys, traceWriter, traceContext) + // Preserve the raw inter-block cache references so child cachemultis + // created via CacheMultiStore() can use them directly as giga parents, + // avoiding an extra gigacachekv indirection level. + cms.interBlockCaches = gigaStores + cms.gigaStores = make(map[types.StoreKey]types.KVStore, len(gigaKeys)) for _, key := range gigaKeys { if gigaStore, ok := gigaStores[key]; ok { diff --git a/sei-cosmos/store/interblock/cache.go b/sei-cosmos/store/interblock/cache.go new file mode 100644 index 0000000000..1fbe36998d --- /dev/null +++ b/sei-cosmos/store/interblock/cache.go @@ -0,0 +1,175 @@ +package interblock + +import ( + "bytes" + "io" + "sync" + + "github.com/sei-protocol/sei-chain/sei-cosmos/store/types" +) + +// Cache is a KVStore that persists across block boundaries. It is a +// write-through read cache: every Set/Delete propagates to the parent +// CommitKVStore immediately, so the parent is always authoritative and visible +// to all readers (including the v2/legacy execution phase that follows the +// giga execution phase within the same block). The in-memory map accelerates +// reads by serving recently-written and recently-read keys from memory rather +// than hitting the on-disk CommitKVStore, and this warmth survives across +// block boundaries. +// +// FlushDirty is a no-op because the parent is always current; it exists only +// to satisfy the InterBlockCache interface. UpdateParent must be called after +// each Commit because the underlying CommitKVStore may be reloaded. +type Cache struct { + mtx sync.RWMutex + cache *sync.Map // string key -> *types.CValue (always clean; parent is authoritative) + parent types.KVStore + storeKey types.StoreKey +} + +var _ types.InterBlockCache = (*Cache)(nil) + +func NewCache(parent types.KVStore, storeKey types.StoreKey) *Cache { + return &Cache{ + cache: &sync.Map{}, + parent: parent, + storeKey: storeKey, + } +} + +// UpdateParent refreshes the parent store reference. Must be called at block +// start because the underlying CommitKVStore may be reloaded after Commit. +func (c *Cache) UpdateParent(parent types.KVStore) { + c.mtx.Lock() + defer c.mtx.Unlock() + c.parent = parent +} + +// FlushDirty is a no-op for a write-through cache: every Set/Delete already +// propagated to the parent store immediately, so there is nothing to flush. +// Kept to satisfy the InterBlockCache interface. +func (c *Cache) FlushDirty() {} + +// Get implements types.KVStore. +func (c *Cache) Get(key []byte) []byte { + types.AssertValidKey(key) + if cv, ok := c.cache.Load(string(key)); ok { + return cv.(*types.CValue).Value() + } + c.mtx.RLock() + val := c.parent.Get(key) + c.mtx.RUnlock() + // Populate the cache on miss so subsequent reads of the same key stay + // in memory across transactions and blocks (analogous to sei-v3's + // CacheKVStore populate-on-miss). Only non-nil values are cached; nil + // means the key is absent in the parent and we don't want to mask a + // future write from another path. + if val != nil { + c.cache.Store(string(key), types.NewCValue(val, false)) + } + return val +} + +// Has implements types.KVStore. +func (c *Cache) Has(key []byte) bool { + return c.Get(key) != nil +} + +// Set implements types.KVStore. Write-through: updates both the in-memory +// cache and the parent CommitKVStore so the write is immediately visible to +// all readers, including the v2/legacy execution phase that runs after the +// giga phase within the same block. +func (c *Cache) Set(key, value []byte) { + types.AssertValidKey(key) + types.AssertValidValue(value) + c.cache.Store(string(key), types.NewCValue(value, false)) + c.mtx.RLock() + c.parent.Set(key, value) + c.mtx.RUnlock() +} + +// Delete implements types.KVStore. Write-through: removes from both the +// in-memory cache and the parent CommitKVStore immediately. +func (c *Cache) Delete(key []byte) { + types.AssertValidKey(key) + c.cache.Store(string(key), types.NewCValue(nil, false)) + c.mtx.RLock() + c.parent.Delete(key) + c.mtx.RUnlock() +} + +// DeleteAll implements types.KVStore. +func (c *Cache) DeleteAll(start, end []byte) error { + for _, k := range c.GetAllKeyStrsInRange(start, end) { + c.Delete([]byte(k)) + } + return nil +} + +// GetAllKeyStrsInRange implements types.KVStore. +func (c *Cache) GetAllKeyStrsInRange(start, end []byte) []string { + c.mtx.RLock() + parentKeys := c.parent.GetAllKeyStrsInRange(start, end) + c.mtx.RUnlock() + + keySet := make(map[string]struct{}, len(parentKeys)) + for _, k := range parentKeys { + keySet[k] = struct{}{} + } + c.cache.Range(func(key, value any) bool { + kb := []byte(key.(string)) + if bytes.Compare(kb, start) < 0 || bytes.Compare(kb, end) >= 0 { + return true + } + if value.(*types.CValue).Value() == nil { + delete(keySet, key.(string)) + } else { + keySet[key.(string)] = struct{}{} + } + return true + }) + result := make([]string, 0, len(keySet)) + for k := range keySet { + result = append(result, k) + } + return result +} + +// GetStoreType implements types.Store. +func (c *Cache) GetStoreType() types.StoreType { + c.mtx.RLock() + defer c.mtx.RUnlock() + return c.parent.GetStoreType() +} + +// CacheWrap implements types.Store. +func (c *Cache) CacheWrap(storeKey types.StoreKey) types.CacheWrap { + panic("CacheWrap not supported on InterBlockCache") +} + +// CacheWrapWithTrace implements types.Store. +func (c *Cache) CacheWrapWithTrace(storeKey types.StoreKey, w io.Writer, tc types.TraceContext) types.CacheWrap { + panic("CacheWrapWithTrace not supported on InterBlockCache") +} + +// GetWorkingHash implements types.KVStore. +func (c *Cache) GetWorkingHash() ([]byte, error) { + panic("GetWorkingHash not supported on InterBlockCache") +} + +// VersionExists implements types.KVStore. +func (c *Cache) VersionExists(version int64) bool { + c.mtx.RLock() + defer c.mtx.RUnlock() + return c.parent.VersionExists(version) +} + +// Iterator implements types.KVStore. +func (c *Cache) Iterator(start, end []byte) types.Iterator { + panic("Iterator not supported on InterBlockCache") +} + +// ReverseIterator implements types.KVStore. +func (c *Cache) ReverseIterator(start, end []byte) types.Iterator { + panic("ReverseIterator not supported on InterBlockCache") +} diff --git a/sei-cosmos/store/types/store.go b/sei-cosmos/store/types/store.go index 1933e1681b..bbb2bbc3fb 100644 --- a/sei-cosmos/store/types/store.go +++ b/sei-cosmos/store/types/store.go @@ -458,3 +458,16 @@ type GigaMultiStore interface { IsStoreGiga(key StoreKey) bool SetGigaKVStores(handler func(sk StoreKey, s KVStore) KVStore) MultiStore } + +// InterBlockCache is a KVStore that persists across block boundaries. It wraps +// an underlying parent CommitKVStore with an in-memory write-through cache and +// supports O(dirty) selective flushing rather than full eviction each block. +type InterBlockCache interface { + KVStore + // UpdateParent refreshes the parent store reference. Must be called at + // block start when the underlying CommitKVStore is reloaded after Commit. + UpdateParent(parent KVStore) + // FlushDirty writes only entries modified since the last flush to the + // parent store, then marks them clean without evicting cached entries. + FlushDirty() +} diff --git a/sei-cosmos/storev2/rootmulti/store.go b/sei-cosmos/storev2/rootmulti/store.go index bf1a77bd97..63629cb1c0 100644 --- a/sei-cosmos/storev2/rootmulti/store.go +++ b/sei-cosmos/storev2/rootmulti/store.go @@ -19,6 +19,8 @@ import ( protoio "github.com/gogo/protobuf/io" snapshottypes "github.com/sei-protocol/sei-chain/sei-cosmos/snapshots/types" "github.com/sei-protocol/sei-chain/sei-cosmos/store/cachemulti" + "github.com/sei-protocol/sei-chain/sei-cosmos/store/dbadapter" + "github.com/sei-protocol/sei-chain/sei-cosmos/store/interblock" "github.com/sei-protocol/sei-chain/sei-cosmos/store/mem" "github.com/sei-protocol/sei-chain/sei-cosmos/store/rootmulti" "github.com/sei-protocol/sei-chain/sei-cosmos/store/transient" @@ -46,17 +48,25 @@ var ( ) type Store struct { - mtx sync.RWMutex - scStore sctypes.Committer - ssStore seidbtypes.StateStore - lastCommitInfo *types.CommitInfo - storesParams map[types.StoreKey]storeParams - storeKeys map[string]types.StoreKey - ckvStores map[types.StoreKey]types.CommitKVStore - gigaKeys []string - + mtx sync.RWMutex + scStore sctypes.Committer + ssStore seidbtypes.StateStore + lastCommitInfo *types.CommitInfo + storesParams map[types.StoreKey]storeParams + storeKeys map[string]types.StoreKey + ckvStores map[types.StoreKey]types.CommitKVStore + gigaKeys []string histProofSem chan struct{} histProofLimiter *rate.Limiter + + // interBlockCachesDeliver and interBlockCachesProcessProposal each hold one + // persistent in-memory cache per giga-registered store key for the + // corresponding ABCI state. checkState intentionally has no inter-block + // cache so it always reads from the last committed state. Keeping deliver + // and processProposal caches separate prevents mid-block writes in one + // state from being visible in the other. + interBlockCachesDeliver map[types.StoreKey]*interblock.Cache + interBlockCachesProcessProposal map[types.StoreKey]*interblock.Cache } type VersionedChangesets struct { @@ -131,6 +141,7 @@ func (rs *Store) Commit(bumpVersion bool) types.CommitID { } commitStartTime := time.Now() defer telemetry.MeasureSince(commitStartTime, "storeV2", "sc", "commit", "latency") + if err := rs.flush(); err != nil { panic(err) } @@ -159,11 +170,32 @@ func (rs *Store) Commit(bumpVersion bool) types.CommitID { } } + // Refresh interblock cache parent pointers since CommitKVStore instances + // may have been reloaded above. The caches themselves (and their warm + // entries) are preserved; only the parent reference is updated. + rs.updateInterBlockCacheParents() + rs.lastCommitInfo = convertCommitInfo(rs.scStore.LastCommitInfo()) rs.lastCommitInfo = amendCommitInfo(rs.lastCommitInfo, rs.storesParams) return rs.lastCommitInfo.CommitID() } +// updateInterBlockCacheParents refreshes the parent CommitKVStore pointer in +// every per-state interblock cache. Must be called after rs.ckvStores entries +// are reloaded in Commit() to avoid stale references. +func (rs *Store) updateInterBlockCacheParents() { + for _, caches := range []map[types.StoreKey]*interblock.Cache{ + rs.interBlockCachesDeliver, + rs.interBlockCachesProcessProposal, + } { + for sk, cache := range caches { + if store, ok := rs.ckvStores[sk]; ok { + cache.UpdateParent(store) + } + } + } +} + // Flush all the pending changesets to commit store. func (rs *Store) flush() error { var changeSets []*proto.NamedChangeSet @@ -261,7 +293,17 @@ func (rs *Store) CacheMultiStore() types.CacheMultiStore { } // cacheMultiStoreLocked must be called with rs.mtx held (at least RLock). +// It is used for generic/query purposes and does not attach any per-state +// interblock cache. Use CacheMultiStoreForCheck, CacheMultiStoreForDeliver, or +// CacheMultiStoreForProcessProposal for the three ABCI execution states. func (rs *Store) cacheMultiStoreLocked() types.CacheMultiStore { + return rs.cacheMultiStoreLockedWithCaches(nil) +} + +// cacheMultiStoreLockedWithCaches builds a CacheMultiStore using the provided +// interblock caches as the giga parent layer. Must be called with rs.mtx held +// (at least RLock). Pass nil to omit the interblock cache entirely. +func (rs *Store) cacheMultiStoreLockedWithCaches(interBlockCaches map[types.StoreKey]*interblock.Cache) types.CacheMultiStore { stores := make(map[types.StoreKey]types.CacheWrapper) for k, v := range rs.ckvStores { store := types.KVStore(v) @@ -271,9 +313,38 @@ func (rs *Store) cacheMultiStoreLocked() types.CacheMultiStore { for _, k := range rs.gigaKeys { gigaKeys = append(gigaKeys, rs.storeKeys[k]) } + // When interblock caches are available, use them as the parent stores for + // the giga cachekv.Store layer. This keeps hot EVM state (contract slots, + // balances) warm in memory across blocks, so the MVS WriteLatestToStore() + // flush writes into the interblock cache rather than directly to the + // CommitKVStore. FlushDirty() then propagates the dirty subset to disk at + // Commit time. + if len(interBlockCaches) > 0 { + gigaStores := make(map[types.StoreKey]types.KVStore, len(interBlockCaches)) + for sk, cache := range interBlockCaches { + gigaStores[sk] = cache + } + return cachemulti.NewFromKVStore(dbadapter.Store{DB: nil}, stores, gigaStores, rs.storeKeys, gigaKeys, nil, nil) + } return cachemulti.NewStore(nil, stores, rs.storeKeys, gigaKeys, nil, nil) } +// CacheMultiStoreForDeliver returns a CacheMultiStore backed by the +// deliverState interblock caches for use in setDeliverState. +func (rs *Store) CacheMultiStoreForDeliver() types.CacheMultiStore { + rs.mtx.RLock() + defer rs.mtx.RUnlock() + return rs.cacheMultiStoreLockedWithCaches(rs.interBlockCachesDeliver) +} + +// CacheMultiStoreForProcessProposal returns a CacheMultiStore backed by the +// processProposalState interblock caches for use in setProcessProposalState. +func (rs *Store) CacheMultiStoreForProcessProposal() types.CacheMultiStore { + rs.mtx.RLock() + defer rs.mtx.RUnlock() + return rs.cacheMultiStoreLockedWithCaches(rs.interBlockCachesProcessProposal) +} + // CacheMultiStoreWithVersion Implements interface MultiStore // used to createQueryContext, abci_query or grpc query service. func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStore, error) { @@ -478,9 +549,33 @@ func (rs *Store) LoadVersionAndUpgrade(version int64, upgrades *types.StoreUpgra } else { rs.lastCommitInfo = &types.CommitInfo{} } + rs.initInterBlockCaches() return nil } +// initInterBlockCaches creates two independent interblock.Cache maps — one +// each for deliverState and processProposalState — so that in-memory cache +// state is isolated between those two ABCI execution contexts. checkState +// intentionally has no inter-block cache. Must be called after rs.ckvStores +// is populated (i.e. at the end of LoadVersionAndUpgrade). +func (rs *Store) initInterBlockCaches() { + if len(rs.gigaKeys) == 0 { + return + } + rs.interBlockCachesDeliver = make(map[types.StoreKey]*interblock.Cache, len(rs.gigaKeys)) + rs.interBlockCachesProcessProposal = make(map[types.StoreKey]*interblock.Cache, len(rs.gigaKeys)) + for _, k := range rs.gigaKeys { + sk := rs.storeKeys[k] + if sk == nil { + continue + } + if parent, ok := rs.ckvStores[sk]; ok { + rs.interBlockCachesDeliver[sk] = interblock.NewCache(parent, sk) + rs.interBlockCachesProcessProposal[sk] = interblock.NewCache(parent, sk) + } + } +} + func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, params storeParams) (types.CommitKVStore, error) { switch params.typ { case types.StoreTypeMulti: