From 1ed2f69d4d4bb7d1f99f0e7a088d64d057757e54 Mon Sep 17 00:00:00 2001 From: yihuang Date: Mon, 25 Mar 2024 09:34:37 +0800 Subject: [PATCH 01/13] Problem: CacheWrapWithTrace api is not used (#207) Solution: - remove the api changelog --- store/CHANGELOG.md | 6 ++++++ store/cachekv/store.go | 7 ------- store/cachemulti/store.go | 5 ----- store/dbadapter/store.go | 8 -------- store/dbadapter/store_test.go | 3 --- store/gaskv/store.go | 7 ------- store/gaskv/store_test.go | 1 - store/iavl/store.go | 7 ------- store/iavl/store_test.go | 3 --- store/listenkv/store.go | 8 -------- store/listenkv/store_test.go | 5 ----- store/mem/mem_test.go | 3 --- store/mem/store.go | 8 -------- store/prefix/store.go | 7 ------- store/prefix/store_test.go | 3 --- store/rootmulti/store.go | 5 ----- store/rootmulti/store_test.go | 6 ++---- store/tracekv/store.go | 6 ------ store/tracekv/store_test.go | 5 ----- store/types/store.go | 6 ------ 20 files changed, 8 insertions(+), 101 deletions(-) diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 53bf82923d6d..94738cea814b 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -23,6 +23,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog +## [Unreleased] + +### Improvements + +* [#207](https://github.com/crypto-org-chain/cosmos-sdk/pull/207) Remove api CacheWrapWithTrace. + ## v1.1.2 (March 31, 2025) ### Bug Fixes diff --git a/store/cachekv/store.go b/store/cachekv/store.go index 08cfc2b325f6..9c062c8ce619 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -2,7 +2,6 @@ package cachekv import ( "bytes" - "io" "sort" "sync" @@ -12,7 +11,6 @@ import ( "cosmossdk.io/store/cachekv/internal" "cosmossdk.io/store/internal/conv" "cosmossdk.io/store/internal/kv" - "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" ) @@ -166,11 +164,6 @@ func (store *Store) CacheWrap() types.CacheWrap { return NewStore(store) } -// CacheWrapWithTrace implements the CacheWrapper interface. -func (store *Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap { - return NewStore(tracekv.NewStore(store, w, tc)) -} - //---------------------------------------- // Iteration diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index a2bd800265d0..0159502c93b8 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -131,11 +131,6 @@ func (cms Store) CacheWrap() types.CacheWrap { return cms.CacheMultiStore().(types.CacheWrap) } -// CacheWrapWithTrace implements the CacheWrapper interface. -func (cms Store) CacheWrapWithTrace(_ io.Writer, _ types.TraceContext) types.CacheWrap { - return cms.CacheWrap() -} - // Implements MultiStore. func (cms Store) CacheMultiStore() types.CacheMultiStore { return newCacheMultiStoreFromCMS(cms) diff --git a/store/dbadapter/store.go b/store/dbadapter/store.go index 013e26df2030..804ed4d38d8b 100644 --- a/store/dbadapter/store.go +++ b/store/dbadapter/store.go @@ -1,12 +1,9 @@ package dbadapter import ( - "io" - dbm "github.com/cosmos/cosmos-db" "cosmossdk.io/store/cachekv" - "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" ) @@ -81,10 +78,5 @@ func (dsa Store) CacheWrap() types.CacheWrap { return cachekv.NewStore(dsa) } -// CacheWrapWithTrace implements KVStore. -func (dsa Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap { - return cachekv.NewStore(tracekv.NewStore(dsa, w, tc)) -} - // dbm.DB implements KVStore so we can CacheKVStore it. var _ types.KVStore = Store{} diff --git a/store/dbadapter/store_test.go b/store/dbadapter/store_test.go index 303170bde266..8dfe5e5d266f 100644 --- a/store/dbadapter/store_test.go +++ b/store/dbadapter/store_test.go @@ -80,7 +80,4 @@ func TestCacheWraps(t *testing.T) { cacheWrapper := store.CacheWrap() require.IsType(t, &cachekv.Store{}, cacheWrapper) - - cacheWrappedWithTrace := store.CacheWrapWithTrace(nil, nil) - require.IsType(t, &cachekv.Store{}, cacheWrappedWithTrace) } diff --git a/store/gaskv/store.go b/store/gaskv/store.go index e0f96af7151e..59247d80008a 100644 --- a/store/gaskv/store.go +++ b/store/gaskv/store.go @@ -1,8 +1,6 @@ package gaskv import ( - "io" - "cosmossdk.io/store/types" ) @@ -87,11 +85,6 @@ func (gs *Store) CacheWrap() types.CacheWrap { panic("cannot CacheWrap a GasKVStore") } -// CacheWrapWithTrace implements the KVStore interface. -func (gs *Store) CacheWrapWithTrace(_ io.Writer, _ types.TraceContext) types.CacheWrap { - panic("cannot CacheWrapWithTrace a GasKVStore") -} - func (gs *Store) iterator(start, end []byte, ascending bool) types.Iterator { var parent types.Iterator if ascending { diff --git a/store/gaskv/store_test.go b/store/gaskv/store_test.go index 354832d17c40..d4fd70778cd9 100644 --- a/store/gaskv/store_test.go +++ b/store/gaskv/store_test.go @@ -24,7 +24,6 @@ func TestGasKVStoreBasic(t *testing.T) { require.Equal(t, types.StoreTypeDB, st.GetStoreType()) require.Panics(t, func() { st.CacheWrap() }) - require.Panics(t, func() { st.CacheWrapWithTrace(nil, nil) }) require.Panics(t, func() { st.Set(nil, []byte("value")) }, "setting a nil key should panic") require.Panics(t, func() { st.Set([]byte(""), []byte("value")) }, "setting an empty key should panic") diff --git a/store/iavl/store.go b/store/iavl/store.go index 6965a5afb1c7..e4fd17f9c7d0 100644 --- a/store/iavl/store.go +++ b/store/iavl/store.go @@ -3,7 +3,6 @@ package iavl import ( "errors" "fmt" - "io" cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" dbm "github.com/cosmos/cosmos-db" @@ -16,7 +15,6 @@ import ( "cosmossdk.io/store/internal/kv" "cosmossdk.io/store/metrics" pruningtypes "cosmossdk.io/store/pruning/types" - "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" "cosmossdk.io/store/wrapper" ) @@ -188,11 +186,6 @@ func (st *Store) CacheWrap() types.CacheWrap { return cachekv.NewStore(st) } -// CacheWrapWithTrace implements the Store interface. -func (st *Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap { - return cachekv.NewStore(tracekv.NewStore(st, w, tc)) -} - // Implements types.KVStore. func (st *Store) Set(key, value []byte) { types.AssertValidKey(key) diff --git a/store/iavl/store_test.go b/store/iavl/store_test.go index e32fb141165d..c3993f2cac51 100644 --- a/store/iavl/store_test.go +++ b/store/iavl/store_test.go @@ -661,9 +661,6 @@ func TestCacheWraps(t *testing.T) { cacheWrapper := store.CacheWrap() require.IsType(t, &cachekv.Store{}, cacheWrapper) - - cacheWrappedWithTrace := store.CacheWrapWithTrace(nil, nil) - require.IsType(t, &cachekv.Store{}, cacheWrappedWithTrace) } func TestChangeSets(t *testing.T) { diff --git a/store/listenkv/store.go b/store/listenkv/store.go index b08a6e395071..343be611b9f0 100644 --- a/store/listenkv/store.go +++ b/store/listenkv/store.go @@ -1,8 +1,6 @@ package listenkv import ( - "io" - "cosmossdk.io/store/types" ) @@ -134,9 +132,3 @@ func (s *Store) GetStoreType() types.StoreType { func (s *Store) CacheWrap() types.CacheWrap { panic("cannot CacheWrap a ListenKVStore") } - -// CacheWrapWithTrace implements the KVStore interface. It panics as a -// Store cannot be cache wrapped. -func (s *Store) CacheWrapWithTrace(_ io.Writer, _ types.TraceContext) types.CacheWrap { - panic("cannot CacheWrapWithTrace a ListenKVStore") -} diff --git a/store/listenkv/store_test.go b/store/listenkv/store_test.go index 51b88912c2e1..1f9ca4b2942e 100644 --- a/store/listenkv/store_test.go +++ b/store/listenkv/store_test.go @@ -274,8 +274,3 @@ func TestListenKVStoreCacheWrap(t *testing.T) { store := newEmptyListenKVStore(nil) require.Panics(t, func() { store.CacheWrap() }) } - -func TestListenKVStoreCacheWrapWithTrace(t *testing.T) { - store := newEmptyListenKVStore(nil) - require.Panics(t, func() { store.CacheWrapWithTrace(nil, nil) }) -} diff --git a/store/mem/mem_test.go b/store/mem/mem_test.go index 6595b45dce17..23eb731fe648 100644 --- a/store/mem/mem_test.go +++ b/store/mem/mem_test.go @@ -30,9 +30,6 @@ func TestStore(t *testing.T) { cacheWrapper := db.CacheWrap() require.IsType(t, &cachekv.Store{}, cacheWrapper) - - cacheWrappedWithTrace := db.CacheWrapWithTrace(nil, nil) - require.IsType(t, &cachekv.Store{}, cacheWrappedWithTrace) } func TestCommit(t *testing.T) { diff --git a/store/mem/store.go b/store/mem/store.go index b819d7536302..2b2bc228ddcd 100644 --- a/store/mem/store.go +++ b/store/mem/store.go @@ -1,14 +1,11 @@ package mem import ( - "io" - dbm "github.com/cosmos/cosmos-db" "cosmossdk.io/store/cachekv" "cosmossdk.io/store/dbadapter" pruningtypes "cosmossdk.io/store/pruning/types" - "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" ) @@ -41,11 +38,6 @@ func (s Store) CacheWrap() types.CacheWrap { return cachekv.NewStore(s) } -// CacheWrapWithTrace implements KVStore. -func (s Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap { - return cachekv.NewStore(tracekv.NewStore(s, w, tc)) -} - // Commit performs a no-op as entries are persistent between commitments. func (s *Store) Commit() (id types.CommitID) { return } diff --git a/store/prefix/store.go b/store/prefix/store.go index 32b9e8247e2c..10f50b3ea795 100644 --- a/store/prefix/store.go +++ b/store/prefix/store.go @@ -3,10 +3,8 @@ package prefix import ( "bytes" "errors" - "io" "cosmossdk.io/store/cachekv" - "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" ) @@ -52,11 +50,6 @@ func (s Store) CacheWrap() types.CacheWrap { return cachekv.NewStore(s) } -// CacheWrapWithTrace implements the KVStore interface. -func (s Store) CacheWrapWithTrace(w io.Writer, tc types.TraceContext) types.CacheWrap { - return cachekv.NewStore(tracekv.NewStore(s, w, tc)) -} - // Implements KVStore func (s Store) Get(key []byte) []byte { res := s.parent.Get(s.key(key)) diff --git a/store/prefix/store_test.go b/store/prefix/store_test.go index 738835770425..818733033e09 100644 --- a/store/prefix/store_test.go +++ b/store/prefix/store_test.go @@ -445,7 +445,4 @@ func TestCacheWraps(t *testing.T) { cacheWrapper := store.CacheWrap() require.IsType(t, &cachekv.Store{}, cacheWrapper) - - cacheWrappedWithTrace := store.CacheWrapWithTrace(nil, nil) - require.IsType(t, &cachekv.Store{}, cacheWrappedWithTrace) } diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index b9987455097a..043d8c2b8bc2 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -551,11 +551,6 @@ func (rs *Store) CacheWrap() types.CacheWrap { return rs.CacheMultiStore().(types.CacheWrap) } -// CacheWrapWithTrace implements the CacheWrapper interface. -func (rs *Store) CacheWrapWithTrace(_ io.Writer, _ types.TraceContext) types.CacheWrap { - return rs.CacheWrap() -} - // CacheMultiStore creates ephemeral branch of the multi-store and returns a CacheMultiStore. // It implements the MultiStore interface. func (rs *Store) CacheMultiStore() types.CacheMultiStore { diff --git a/store/rootmulti/store_test.go b/store/rootmulti/store_test.go index 495a1a8af3ed..69a02281cd59 100644 --- a/store/rootmulti/store_test.go +++ b/store/rootmulti/store_test.go @@ -17,6 +17,7 @@ import ( sdkmaps "cosmossdk.io/store/internal/maps" "cosmossdk.io/store/metrics" pruningtypes "cosmossdk.io/store/pruning/types" + "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" ) @@ -702,9 +703,6 @@ func TestCacheWraps(t *testing.T) { cacheWrapper := multi.CacheWrap() require.IsType(t, cachemulti.Store{}, cacheWrapper) - - cacheWrappedWithTrace := multi.CacheWrapWithTrace(nil, nil) - require.IsType(t, cachemulti.Store{}, cacheWrappedWithTrace) } func TestTraceConcurrency(t *testing.T) { @@ -722,7 +720,7 @@ func TestTraceConcurrency(t *testing.T) { cms := multi.CacheMultiStore() store1 := cms.GetKVStore(key) - cw := store1.CacheWrapWithTrace(b, tc) + cw := tracekv.NewStore(store1.CacheWrap().(types.KVStore), b, tc) _ = cw require.NotNil(t, store1) diff --git a/store/tracekv/store.go b/store/tracekv/store.go index ba6df431da16..7df8b0f97ba7 100644 --- a/store/tracekv/store.go +++ b/store/tracekv/store.go @@ -167,12 +167,6 @@ func (tkv *Store) CacheWrap() types.CacheWrap { panic("cannot CacheWrap a TraceKVStore") } -// CacheWrapWithTrace implements the KVStore interface. It panics as a -// Store cannot be branched. -func (tkv *Store) CacheWrapWithTrace(_ io.Writer, _ types.TraceContext) types.CacheWrap { - panic("cannot CacheWrapWithTrace a TraceKVStore") -} - // writeOperation writes a KVStore operation to the underlying io.Writer as // JSON-encoded data where the key/value pair is base64 encoded. func writeOperation(w io.Writer, op operation, tc types.TraceContext, key, value []byte) { diff --git a/store/tracekv/store_test.go b/store/tracekv/store_test.go index 2c42734baefd..d276accd530a 100644 --- a/store/tracekv/store_test.go +++ b/store/tracekv/store_test.go @@ -285,8 +285,3 @@ func TestTraceKVStoreCacheWrap(t *testing.T) { store := newEmptyTraceKVStore(nil) require.Panics(t, func() { store.CacheWrap() }) } - -func TestTraceKVStoreCacheWrapWithTrace(t *testing.T) { - store := newEmptyTraceKVStore(nil) - require.Panics(t, func() { store.CacheWrapWithTrace(nil, nil) }) -} diff --git a/store/types/store.go b/store/types/store.go index 0adf42d2b0ab..f74cfbc440ec 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -295,17 +295,11 @@ type CacheWrap interface { // CacheWrap recursively wraps again. CacheWrap() CacheWrap - - // CacheWrapWithTrace recursively wraps again with tracing enabled. - CacheWrapWithTrace(w io.Writer, tc TraceContext) CacheWrap } type CacheWrapper interface { // CacheWrap branches a store. CacheWrap() CacheWrap - - // CacheWrapWithTrace branches a store with tracing enabled. - CacheWrapWithTrace(w io.Writer, tc TraceContext) CacheWrap } func (cid CommitID) IsZero() bool { From a437f20e70b612c68da767e65fadc9e0b02a31d3 Mon Sep 17 00:00:00 2001 From: yihuang Date: Mon, 25 Mar 2024 14:12:34 +0800 Subject: [PATCH 02/13] Support object store (#206) generic interface generic btree generic cachekv generic transient store support ObjStore changelog Update CHANGELOG.md Signed-off-by: yihuang object store key Apply review suggestions fix merge conflict fix snapshot revert dependers --- .github/workflows/test.yml | 10 +- CHANGELOG.md | 8 ++ baseapp/baseapp.go | 11 ++ go.mod | 1 + go.sum | 2 - runtime/store.go | 11 +- server/mock/store.go | 8 +- store/CHANGELOG.md | 1 + store/cachekv/internal/mergeiterator.go | 43 ++++--- store/cachekv/search_benchmark_test.go | 12 +- store/cachekv/store.go | 119 +++++++++++------- store/cachemulti/store.go | 45 +++++-- store/gaskv/store.go | 116 +++++++++++------ store/iavl/tree.go | 4 +- .../internal => internal/btree}/btree.go | 48 +++---- .../internal => internal/btree}/btree_test.go | 8 +- .../btree}/memiterator.go | 39 +++--- store/internal/btreeadaptor.go | 52 ++++++++ store/listenkv/store.go | 3 +- store/listenkv/store_test.go | 2 +- store/rootmulti/store.go | 93 +++++++++----- store/rootmulti/store_test.go | 33 ++--- store/tracekv/store.go | 3 +- store/tracekv/store_test.go | 2 +- store/transient/store.go | 63 +++++++--- store/types/store.go | 91 +++++++++++--- store/types/validity.go | 11 +- tests/go.mod | 1 + types/context.go | 5 + x/group/internal/orm/testsupport.go | 4 +- 30 files changed, 584 insertions(+), 265 deletions(-) rename store/{cachekv/internal => internal/btree}/btree.go (63%) rename store/{cachekv/internal => internal/btree}/btree_test.go (98%) rename store/{cachekv/internal => internal/btree}/memiterator.go (64%) create mode 100644 store/internal/btreeadaptor.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be73e99e03db..d19500f196ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -376,7 +376,15 @@ jobs: if: env.GIT_DIFF run: | cd collections - go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic -tags='norace ledger test_ledger_mock' ./... + go test -mod=readonly -timeout 30m -coverprofile=coverage.out -covermode=atomic -tags='norace ledger test_ledger_mock rocksdb_build' ./... + - name: sonarcloud + if: ${{ env.GIT_DIFF && !github.event.pull_request.draft && env.SONAR_TOKEN != null }} + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + projectBaseDir: collections/ test-cosmovisor: runs-on: depot-ubuntu-22.04-4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b05d57f0ce..1752ce366647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,13 @@ Ref: https://keepachangelog.com/en/1.0.0/ # Changelog +## [Unreleased] + +### Features + +* (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Add `TxExecutor` baseapp option, add `TxIndex`/`TxCount`/`MsgIndex`/`BlockGasUsed` fields to `Context, to support tx parallel execution. +* (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Support mount object store in baseapp, add `ObjectStore` api in context.. + ## [v0.53.0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.53.0) - 2025-04-29 ### Features @@ -64,6 +71,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * The `InflationCalculationFn` argument to `mint.NewAppModule()` is now ignored and must be nil. To set a custom `InflationCalculationFn` on the default minter, use `mintkeeper.WithMintFn(mintkeeper.DefaultMintFn(customInflationFn))`. * (api) [#24428](https://github.com/cosmos/cosmos-sdk/pull/24428) Add block height to response headers + ### Improvements * (client) [#24561](https://github.com/cosmos/cosmos-sdk/pull/24561) TimeoutTimestamp flag has been changed to TimeoutDuration, which now sets the timeout timestamp of unordered transactions to the current time + duration passed. diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index c10fa15936a9..19bfdf853eb5 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -356,6 +356,17 @@ func (app *BaseApp) MountMemoryStores(keys map[string]*storetypes.MemoryStoreKey } } +// MountObjectStores mounts all transient object stores with the BaseApp's internal +// commit multi-store. +func (app *BaseApp) MountObjectStores(keys map[string]*storetypes.ObjectStoreKey) { + skeys := maps.Keys(keys) + sort.Strings(skeys) + for _, key := range skeys { + memKey := keys[key] + app.MountStore(memKey, storetypes.StoreTypeObject) + } +} + // MountStore mounts a store to the provided key in the BaseApp multistore, // using the default DB. func (app *BaseApp) MountStore(key storetypes.StoreKey, typ storetypes.StoreType) { diff --git a/go.mod b/go.mod index 5e07a5aca3cc..c0ef561d92f4 100644 --- a/go.mod +++ b/go.mod @@ -177,6 +177,7 @@ require ( // Below are the long-lived replace of the Cosmos SDK replace ( + cosmossdk.io/store => ./store // use cosmos fork of keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 // dgrijalva/jwt-go is deprecated and doesn't receive security updates. diff --git a/go.sum b/go.sum index 9a8d291432a5..1005f51fac55 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,6 @@ cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U= cosmossdk.io/math v1.5.3/go.mod h1:uqcZv7vexnhMFJF+6zh9EWdm/+Ylyln34IvPnBauPCQ= cosmossdk.io/schema v1.1.0 h1:mmpuz3dzouCoyjjcMcA/xHBEmMChN+EHh8EHxHRHhzE= cosmossdk.io/schema v1.1.0/go.mod h1:Gb7pqO+tpR+jLW5qDcNOSv0KtppYs7881kfzakguhhI= -cosmossdk.io/store v1.1.2 h1:3HOZG8+CuThREKv6cn3WSohAc6yccxO3hLzwK6rBC7o= -cosmossdk.io/store v1.1.2/go.mod h1:60rAGzTHevGm592kFhiUVkNC9w7gooSEn5iUBPzHQ6A= cosmossdk.io/x/tx v0.14.0 h1:hB3O25kIcyDW/7kMTLMaO8Ripj3yqs5imceVd6c/heA= cosmossdk.io/x/tx v0.14.0/go.mod h1:Tn30rSRA1PRfdGB3Yz55W4Sn6EIutr9xtMKSHij+9PM= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= diff --git a/runtime/store.go b/runtime/store.go index 4cfe131c5071..965bdafd7372 100644 --- a/runtime/store.go +++ b/runtime/store.go @@ -2,9 +2,6 @@ package runtime import ( "context" - "io" - - dbm "github.com/cosmos/cosmos-db" "cosmossdk.io/core/store" storetypes "cosmossdk.io/store/types" @@ -112,10 +109,6 @@ func (kvStoreAdapter) CacheWrap() storetypes.CacheWrap { panic("unimplemented") } -func (kvStoreAdapter) CacheWrapWithTrace(w io.Writer, tc storetypes.TraceContext) storetypes.CacheWrap { - panic("unimplemented") -} - func (kvStoreAdapter) GetStoreType() storetypes.StoreType { panic("unimplemented") } @@ -150,7 +143,7 @@ func (s kvStoreAdapter) Set(key, value []byte) { } } -func (s kvStoreAdapter) Iterator(start, end []byte) dbm.Iterator { +func (s kvStoreAdapter) Iterator(start, end []byte) storetypes.Iterator { it, err := s.store.Iterator(start, end) if err != nil { panic(err) @@ -158,7 +151,7 @@ func (s kvStoreAdapter) Iterator(start, end []byte) dbm.Iterator { return it } -func (s kvStoreAdapter) ReverseIterator(start, end []byte) dbm.Iterator { +func (s kvStoreAdapter) ReverseIterator(start, end []byte) storetypes.Iterator { it, err := s.store.ReverseIterator(start, end) if err != nil { panic(err) diff --git a/server/mock/store.go b/server/mock/store.go index 60aa64ca7462..4bd897fc681a 100644 --- a/server/mock/store.go +++ b/server/mock/store.go @@ -114,6 +114,10 @@ func (ms multiStore) GetKVStore(key storetypes.StoreKey) storetypes.KVStore { return ms.kv[key] } +func (ms multiStore) GetObjKVStore(storetypes.StoreKey) storetypes.ObjKVStore { + panic("not implemented") +} + func (ms multiStore) GetStore(key storetypes.StoreKey) storetypes.Store { panic("not implemented") } @@ -182,10 +186,6 @@ func (kv kvStore) CacheWrap() storetypes.CacheWrap { panic("not implemented") } -func (kv kvStore) CacheWrapWithTrace(w io.Writer, tc storetypes.TraceContext) storetypes.CacheWrap { - panic("not implemented") -} - func (kv kvStore) CacheWrapWithListeners(_ storetypes.StoreKey, _ []storetypes.MemoryListener) storetypes.CacheWrap { panic("not implemented") } diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 94738cea814b..8f7b99d786c4 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -28,6 +28,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements * [#207](https://github.com/crypto-org-chain/cosmos-sdk/pull/207) Remove api CacheWrapWithTrace. +* [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Support object store. ## v1.1.2 (March 31, 2025) diff --git a/store/cachekv/internal/mergeiterator.go b/store/cachekv/internal/mergeiterator.go index 58e9497b3028..c9a1e7d3a677 100644 --- a/store/cachekv/internal/mergeiterator.go +++ b/store/cachekv/internal/mergeiterator.go @@ -14,21 +14,24 @@ import ( // cache shadows (overrides) the parent. // // TODO: Optimize by memoizing. -type cacheMergeIterator struct { - parent types.Iterator - cache types.Iterator +type cacheMergeIterator[V any] struct { + parent types.GIterator[V] + cache types.GIterator[V] ascending bool valid bool + + isZero func(V) bool } -var _ types.Iterator = (*cacheMergeIterator)(nil) +var _ types.Iterator = (*cacheMergeIterator[[]byte])(nil) -func NewCacheMergeIterator(parent, cache types.Iterator, ascending bool) types.Iterator { - iter := &cacheMergeIterator{ +func NewCacheMergeIterator[V any](parent, cache types.GIterator[V], ascending bool, isZero func(V) bool) types.GIterator[V] { + iter := &cacheMergeIterator[V]{ parent: parent, cache: cache, ascending: ascending, + isZero: isZero, } iter.valid = iter.skipUntilExistsOrInvalid() @@ -37,17 +40,17 @@ func NewCacheMergeIterator(parent, cache types.Iterator, ascending bool) types.I // Domain implements Iterator. // Returns parent domain because cache and parent domains are the same. -func (iter *cacheMergeIterator) Domain() (start, end []byte) { +func (iter *cacheMergeIterator[V]) Domain() (start, end []byte) { return iter.parent.Domain() } // Valid implements Iterator. -func (iter *cacheMergeIterator) Valid() bool { +func (iter *cacheMergeIterator[V]) Valid() bool { return iter.valid } // Next implements Iterator -func (iter *cacheMergeIterator) Next() { +func (iter *cacheMergeIterator[V]) Next() { iter.assertValid() switch { @@ -74,7 +77,7 @@ func (iter *cacheMergeIterator) Next() { } // Key implements Iterator -func (iter *cacheMergeIterator) Key() []byte { +func (iter *cacheMergeIterator[V]) Key() []byte { iter.assertValid() // If parent is invalid, get the cache key. @@ -104,7 +107,7 @@ func (iter *cacheMergeIterator) Key() []byte { } // Value implements Iterator -func (iter *cacheMergeIterator) Value() []byte { +func (iter *cacheMergeIterator[V]) Value() V { iter.assertValid() // If parent is invalid, get the cache value. @@ -134,7 +137,7 @@ func (iter *cacheMergeIterator) Value() []byte { } // Close implements Iterator -func (iter *cacheMergeIterator) Close() error { +func (iter *cacheMergeIterator[V]) Close() error { err1 := iter.cache.Close() if err := iter.parent.Close(); err != nil { return err @@ -145,7 +148,7 @@ func (iter *cacheMergeIterator) Close() error { // Error returns an error if the cacheMergeIterator is invalid defined by the // Valid method. -func (iter *cacheMergeIterator) Error() error { +func (iter *cacheMergeIterator[V]) Error() error { if !iter.Valid() { return errors.New("invalid cacheMergeIterator") } @@ -155,14 +158,14 @@ func (iter *cacheMergeIterator) Error() error { // If not valid, panics. // NOTE: May have side-effect of iterating over cache. -func (iter *cacheMergeIterator) assertValid() { +func (iter *cacheMergeIterator[V]) assertValid() { if err := iter.Error(); err != nil { panic(err) } } // Like bytes.Compare but opposite if not ascending. -func (iter *cacheMergeIterator) compare(a, b []byte) int { +func (iter *cacheMergeIterator[V]) compare(a, b []byte) int { if iter.ascending { return bytes.Compare(a, b) } @@ -175,9 +178,9 @@ func (iter *cacheMergeIterator) compare(a, b []byte) int { // If the current cache item is not a delete item, does nothing. // If `until` is nil, there is no limit, and cache may end up invalid. // CONTRACT: cache is valid. -func (iter *cacheMergeIterator) skipCacheDeletes(until []byte) { +func (iter *cacheMergeIterator[V]) skipCacheDeletes(until []byte) { for iter.cache.Valid() && - iter.cache.Value() == nil && + iter.isZero(iter.cache.Value()) && (until == nil || iter.compare(iter.cache.Key(), until) < 0) { iter.cache.Next() } @@ -186,7 +189,7 @@ func (iter *cacheMergeIterator) skipCacheDeletes(until []byte) { // Fast forwards cache (or parent+cache in case of deleted items) until current // item exists, or until iterator becomes invalid. // Returns whether the iterator is valid. -func (iter *cacheMergeIterator) skipUntilExistsOrInvalid() bool { +func (iter *cacheMergeIterator[V]) skipUntilExistsOrInvalid() bool { for { // If parent is invalid, fast-forward cache. if !iter.parent.Valid() { @@ -211,7 +214,7 @@ func (iter *cacheMergeIterator) skipUntilExistsOrInvalid() bool { case 0: // parent == cache. // Skip over if cache item is a delete. valueC := iter.cache.Value() - if valueC == nil { + if iter.isZero(valueC) { iter.parent.Next() iter.cache.Next() @@ -223,7 +226,7 @@ func (iter *cacheMergeIterator) skipUntilExistsOrInvalid() bool { case 1: // cache < parent // Skip over if cache item is a delete. valueC := iter.cache.Value() - if valueC == nil { + if iter.isZero(valueC) { iter.skipCacheDeletes(keyP) continue } diff --git a/store/cachekv/search_benchmark_test.go b/store/cachekv/search_benchmark_test.go index ecdc86a8e43a..f0e29bc4ec18 100644 --- a/store/cachekv/search_benchmark_test.go +++ b/store/cachekv/search_benchmark_test.go @@ -4,7 +4,7 @@ import ( "strconv" "testing" - "cosmossdk.io/store/cachekv/internal" + "cosmossdk.io/store/internal/btree" ) func BenchmarkLargeUnsortedMisses(b *testing.B) { @@ -22,23 +22,23 @@ func BenchmarkLargeUnsortedMisses(b *testing.B) { } func generateStore() *Store { - cache := map[string]*cValue{} + cache := map[string]*cValue[[]byte]{} unsorted := map[string]struct{}{} for i := 0; i < 5000; i++ { key := "A" + strconv.Itoa(i) unsorted[key] = struct{}{} - cache[key] = &cValue{} + cache[key] = &cValue[[]byte]{} } for i := 0; i < 5000; i++ { key := "Z" + strconv.Itoa(i) unsorted[key] = struct{}{} - cache[key] = &cValue{} + cache[key] = &cValue[[]byte]{} } - return &Store{ + return &GStore[[]byte]{ cache: cache, unsortedCache: unsorted, - sortedCache: internal.NewBTree(), + sortedCache: btree.NewBTree[[]byte](), } } diff --git a/store/cachekv/store.go b/store/cachekv/store.go index 9c062c8ce619..9f7ff680db60 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -9,46 +9,70 @@ import ( "cosmossdk.io/math" "cosmossdk.io/store/cachekv/internal" + "cosmossdk.io/store/internal/btree" "cosmossdk.io/store/internal/conv" - "cosmossdk.io/store/internal/kv" "cosmossdk.io/store/types" ) // cValue represents a cached value. // If dirty is true, it indicates the cached value is different from the underlying value. -type cValue struct { - value []byte +type cValue[V any] struct { + value V dirty bool } -// Store wraps an in-memory cache around an underlying types.KVStore. -type Store struct { - mtx sync.Mutex - cache map[string]*cValue - unsortedCache map[string]struct{} - sortedCache internal.BTree // always ascending sorted - parent types.KVStore +type kvPair[V any] struct { + Key []byte + Value V } +type Store = GStore[[]byte] + var _ types.CacheKVStore = (*Store)(nil) -// NewStore creates a new Store object func NewStore(parent types.KVStore) *Store { - return &Store{ - cache: make(map[string]*cValue), + return NewGStore( + parent, + func(v []byte) bool { return v == nil }, + func(v []byte) int { return len(v) }, + ) +} + +// GStore wraps an in-memory cache around an underlying types.KVStore. +type GStore[V any] struct { + mtx sync.Mutex + cache map[string]*cValue[V] + unsortedCache map[string]struct{} + sortedCache btree.BTree[V] // always ascending sorted + parent types.GKVStore[V] + + // isZero is a function that returns true if the value is considered "zero", for []byte and pointers the zero value + // is `nil`, zero value is not allowed to set to a key, and it's returned if the key is not found. + isZero func(V) bool + zeroValue V + // valueLen validates the value before it's set + valueLen func(V) int +} + +// NewStore creates a new Store object +func NewGStore[V any](parent types.GKVStore[V], isZero func(V) bool, valueLen func(V) int) *GStore[V] { + return &GStore[V]{ + cache: make(map[string]*cValue[V]), unsortedCache: make(map[string]struct{}), - sortedCache: internal.NewBTree(), + sortedCache: btree.NewBTree[V](), parent: parent, + isZero: isZero, + valueLen: valueLen, } } // GetStoreType implements Store. -func (store *Store) GetStoreType() types.StoreType { +func (store *GStore[V]) GetStoreType() types.StoreType { return store.parent.GetStoreType() } // Get implements types.KVStore. -func (store *Store) Get(key []byte) (value []byte) { +func (store *GStore[V]) Get(key []byte) (value V) { store.mtx.Lock() defer store.mtx.Unlock() @@ -65,10 +89,17 @@ func (store *Store) Get(key []byte) (value []byte) { return value } +func (store *GStore[V]) assertValidValue(value V) { + if store.isZero(value) { + panic("value is nil") + } + types.AssertValidValueLength(store.valueLen(value)) +} + // Set implements types.KVStore. -func (store *Store) Set(key, value []byte) { +func (store *GStore[V]) Set(key []byte, value V) { types.AssertValidKey(key) - types.AssertValidValue(value) + store.assertValidValue(value) store.mtx.Lock() defer store.mtx.Unlock() @@ -76,28 +107,28 @@ func (store *Store) Set(key, value []byte) { } // Has implements types.KVStore. -func (store *Store) Has(key []byte) bool { +func (store *GStore[V]) Has(key []byte) bool { value := store.Get(key) - return value != nil + return !store.isZero(value) } // Delete implements types.KVStore. -func (store *Store) Delete(key []byte) { +func (store *GStore[V]) Delete(key []byte) { types.AssertValidKey(key) store.mtx.Lock() defer store.mtx.Unlock() - store.setCacheValue(key, nil, true) + store.setCacheValue(key, store.zeroValue, true) } -func (store *Store) resetCaches() { +func (store *GStore[V]) resetCaches() { if len(store.cache) > 100_000 { // Cache is too large. We likely did something linear time // (e.g. Epoch block, Genesis block, etc). Free the old caches from memory, and let them get re-allocated. // TODO: In a future CacheKV redesign, such linear workloads should get into a different cache instantiation. // 100_000 is arbitrarily chosen as it solved Osmosis' InitGenesis RAM problem. - store.cache = make(map[string]*cValue) + store.cache = make(map[string]*cValue[V]) store.unsortedCache = make(map[string]struct{}) } else { // Clear the cache using the map clearing idiom @@ -110,22 +141,22 @@ func (store *Store) resetCaches() { delete(store.unsortedCache, key) } } - store.sortedCache = internal.NewBTree() + store.sortedCache = btree.NewBTree[V]() } // Implements Cachetypes.KVStore. -func (store *Store) Write() { +func (store *GStore[V]) Write() { store.mtx.Lock() defer store.mtx.Unlock() if len(store.cache) == 0 && len(store.unsortedCache) == 0 { - store.sortedCache = internal.NewBTree() + store.sortedCache = btree.NewBTree[V]() return } type cEntry struct { key string - val *cValue + val *cValue[V] } // We need a copy of all of the keys. @@ -150,7 +181,7 @@ func (store *Store) Write() { // be sure if the underlying store might do a save with the byteslice or // not. Once we get confirmation that .Delete is guaranteed not to // save the byteslice, then we can assume only a read-only copy is sufficient. - if obj.val.value != nil { + if !store.isZero(obj.val.value) { // It already exists in the parent, hence update it. store.parent.Set([]byte(obj.key), obj.val.value) } else { @@ -160,24 +191,24 @@ func (store *Store) Write() { } // CacheWrap implements CacheWrapper. -func (store *Store) CacheWrap() types.CacheWrap { - return NewStore(store) +func (store *GStore[V]) CacheWrap() types.CacheWrap { + return NewGStore(store, store.isZero, store.valueLen) } //---------------------------------------- // Iteration // Iterator implements types.KVStore. -func (store *Store) Iterator(start, end []byte) types.Iterator { +func (store *GStore[V]) Iterator(start, end []byte) types.GIterator[V] { return store.iterator(start, end, true) } // ReverseIterator implements types.KVStore. -func (store *Store) ReverseIterator(start, end []byte) types.Iterator { +func (store *GStore[V]) ReverseIterator(start, end []byte) types.GIterator[V] { return store.iterator(start, end, false) } -func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator { +func (store *GStore[V]) iterator(start, end []byte, ascending bool) types.GIterator[V] { store.mtx.Lock() defer store.mtx.Unlock() @@ -186,7 +217,7 @@ func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator { var ( err error - parent, cache types.Iterator + parent, cache types.GIterator[V] ) if ascending { @@ -200,7 +231,7 @@ func (store *Store) iterator(start, end []byte, ascending bool) types.Iterator { panic(err) } - return internal.NewCacheMergeIterator(parent, cache, ascending) + return internal.NewCacheMergeIterator(parent, cache, ascending, store.isZero) } func findStartIndex(strL []string, startQ string) int { @@ -286,7 +317,7 @@ const ( const minSortSize = 1024 // Constructs a slice of dirty items, to use w/ memIterator. -func (store *Store) dirtyItems(start, end []byte) { +func (store *GStore[V]) dirtyItems(start, end []byte) { startStr, endStr := conv.UnsafeBytesToStr(start), conv.UnsafeBytesToStr(end) if end != nil && startStr > endStr { // Nothing to do here. @@ -294,7 +325,7 @@ func (store *Store) dirtyItems(start, end []byte) { } n := len(store.unsortedCache) - unsorted := make([]*kv.Pair, 0) + unsorted := make([]*kvPair[V], 0) // If the unsortedCache is too big, its costs too much to determine // whats in the subset we are concerned about. // If you are interleaving iterator calls with writes, this can easily become an @@ -306,7 +337,7 @@ func (store *Store) dirtyItems(start, end []byte) { // dbm.IsKeyInDomain is nil safe and returns true iff key is greater than start if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) { cacheValue := store.cache[key] - unsorted = append(unsorted, &kv.Pair{Key: []byte(key), Value: cacheValue.value}) + unsorted = append(unsorted, &kvPair[V]{Key: []byte(key), Value: cacheValue.value}) } } store.clearUnsortedCacheSubset(unsorted, stateUnsorted) @@ -349,18 +380,18 @@ func (store *Store) dirtyItems(start, end []byte) { } } - kvL := make([]*kv.Pair, 0, 1+endIndex-startIndex) + kvL := make([]*kvPair[V], 0, 1+endIndex-startIndex) for i := startIndex; i <= endIndex; i++ { key := strL[i] cacheValue := store.cache[key] - kvL = append(kvL, &kv.Pair{Key: []byte(key), Value: cacheValue.value}) + kvL = append(kvL, &kvPair[V]{Key: []byte(key), Value: cacheValue.value}) } // kvL was already sorted so pass it in as is. store.clearUnsortedCacheSubset(kvL, stateAlreadySorted) } -func (store *Store) clearUnsortedCacheSubset(unsorted []*kv.Pair, sortState sortState) { +func (store *GStore[V]) clearUnsortedCacheSubset(unsorted []*kvPair[V], sortState sortState) { n := len(store.unsortedCache) if len(unsorted) == n { // This pattern allows the Go compiler to emit the map clearing idiom for the entire map. for key := range store.unsortedCache { @@ -389,9 +420,9 @@ func (store *Store) clearUnsortedCacheSubset(unsorted []*kv.Pair, sortState sort // Only entrypoint to mutate store.cache. // A `nil` value means a deletion. -func (store *Store) setCacheValue(key, value []byte, dirty bool) { +func (store *GStore[V]) setCacheValue(key []byte, value V, dirty bool) { keyStr := conv.UnsafeBytesToStr(key) - store.cache[keyStr] = &cValue{ + store.cache[keyStr] = &cValue[V]{ value: value, dirty: dirty, } diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index 0159502c93b8..67ccec653b24 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -7,7 +7,6 @@ import ( dbm "github.com/cosmos/cosmos-db" - "cosmossdk.io/store/cachekv" "cosmossdk.io/store/dbadapter" "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" @@ -25,7 +24,7 @@ const storeNameCtxKey = "store_name" // NOTE: a Store (and MultiStores in general) should never expose the // keys for the substores. type Store struct { - db types.CacheKVStore + db types.CacheWrap stores map[types.StoreKey]types.CacheWrap keys map[string]types.StoreKey @@ -39,11 +38,11 @@ var _ types.CacheMultiStore = Store{} // CacheWrapper objects and a KVStore as the database. Each CacheWrapper store // is a branched store. func NewFromKVStore( - store types.KVStore, stores map[types.StoreKey]types.CacheWrapper, + store types.CacheWrapper, stores map[types.StoreKey]types.CacheWrapper, keys map[string]types.StoreKey, traceWriter io.Writer, traceContext types.TraceContext, ) Store { cms := Store{ - db: cachekv.NewStore(store), + db: store.CacheWrap(), stores: make(map[types.StoreKey]types.CacheWrap, len(stores)), keys: keys, traceWriter: traceWriter, @@ -52,13 +51,16 @@ func NewFromKVStore( for key, store := range stores { if cms.TracingEnabled() { - tctx := cms.traceContext.Clone().Merge(types.TraceContext{ - storeNameCtxKey: key.Name(), - }) - - store = tracekv.NewStore(store.(types.KVStore), cms.traceWriter, tctx) + // only support tracing on KVStore. + if kvstore, ok := store.(types.KVStore); ok { + tctx := cms.traceContext.Clone().Merge(types.TraceContext{ + storeNameCtxKey: key.Name(), + }) + + store = tracekv.NewStore(kvstore, cms.traceWriter, tctx) + } } - cms.stores[key] = cachekv.NewStore(store.(types.KVStore)) + cms.stores[key] = store.CacheWrap() } return cms @@ -154,11 +156,28 @@ func (cms Store) GetStore(key types.StoreKey) types.Store { return s.(types.Store) } -// GetKVStore returns an underlying KVStore by key. -func (cms Store) GetKVStore(key types.StoreKey) types.KVStore { +func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { store := cms.stores[key] if key == nil || store == nil { panic(fmt.Sprintf("kv store with key %v has not been registered in stores", key)) } - return store.(types.KVStore) + return store +} + +// GetKVStore returns an underlying KVStore by key. +func (cms Store) GetKVStore(key types.StoreKey) types.KVStore { + store, ok := cms.getCacheWrap(key).(types.KVStore) + if !ok { + panic(fmt.Sprintf("store with key %v is not KVStore", key)) + } + return store +} + +// GetObjKVStore returns an underlying KVStore by key. +func (cms Store) GetObjKVStore(key types.StoreKey) types.ObjKVStore { + store, ok := cms.getCacheWrap(key).(types.ObjKVStore) + if !ok { + panic(fmt.Sprintf("store with key %v is not ObjKVStore", key)) + } + return store } diff --git a/store/gaskv/store.go b/store/gaskv/store.go index 59247d80008a..7aab63b0863a 100644 --- a/store/gaskv/store.go +++ b/store/gaskv/store.go @@ -1,65 +1,102 @@ package gaskv -import ( - "cosmossdk.io/store/types" -) +import "cosmossdk.io/store/types" + +// ObjectValueLength is the emulated number of bytes for storing transient objects in gas accounting. +const ObjectValueLength = 16 var _ types.KVStore = &Store{} -// Store applies gas tracking to an underlying KVStore. It implements the +type Store = GStore[[]byte] + +func NewStore(parent types.KVStore, gasMeter types.GasMeter, gasConfig types.GasConfig) *Store { + return NewGStore(parent, gasMeter, gasConfig, + func(v []byte) bool { return v == nil }, + func(v []byte) int { return len(v) }, + ) +} + +type ObjStore = GStore[any] + +func NewObjStore(parent types.ObjKVStore, gasMeter types.GasMeter, gasConfig types.GasConfig) *ObjStore { + return NewGStore(parent, gasMeter, gasConfig, + func(v any) bool { return v == nil }, + func(v any) int { return ObjectValueLength }, + ) +} + +// GStore applies gas tracking to an underlying KVStore. It implements the // KVStore interface. -type Store struct { +type GStore[V any] struct { gasMeter types.GasMeter gasConfig types.GasConfig - parent types.KVStore + parent types.GKVStore[V] + + isZero func(V) bool + valueLen func(V) int } -// NewStore returns a reference to a new GasKVStore. -func NewStore(parent types.KVStore, gasMeter types.GasMeter, gasConfig types.GasConfig) *Store { - kvs := &Store{ +// NewGStore returns a reference to a new GasKVStore. +func NewGStore[V any]( + parent types.GKVStore[V], + gasMeter types.GasMeter, + gasConfig types.GasConfig, + isZero func(V) bool, + valueLen func(V) int, +) *GStore[V] { + kvs := &GStore[V]{ gasMeter: gasMeter, gasConfig: gasConfig, parent: parent, + isZero: isZero, + valueLen: valueLen, } return kvs } // Implements Store. -func (gs *Store) GetStoreType() types.StoreType { +func (gs *GStore[V]) GetStoreType() types.StoreType { return gs.parent.GetStoreType() } // Implements KVStore. -func (gs *Store) Get(key []byte) (value []byte) { +func (gs *GStore[V]) Get(key []byte) (value V) { gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostFlat, types.GasReadCostFlatDesc) value = gs.parent.Get(key) // TODO overflow-safe math? gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(key)), types.GasReadPerByteDesc) - gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(len(value)), types.GasReadPerByteDesc) + gs.gasMeter.ConsumeGas(gs.gasConfig.ReadCostPerByte*types.Gas(gs.valueLen(value)), types.GasReadPerByteDesc) return value } +func (gs *GStore[V]) assertValidValue(value V) { + if gs.isZero(value) { + panic("value is nil") + } + types.AssertValidValueLength(gs.valueLen(value)) +} + // Implements KVStore. -func (gs *Store) Set(key, value []byte) { +func (gs *GStore[V]) Set(key []byte, value V) { types.AssertValidKey(key) - types.AssertValidValue(value) + gs.assertValidValue(value) gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostFlat, types.GasWriteCostFlatDesc) // TODO overflow-safe math? gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(key)), types.GasWritePerByteDesc) - gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(value)), types.GasWritePerByteDesc) + gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(gs.valueLen(value)), types.GasWritePerByteDesc) gs.parent.Set(key, value) } // Implements KVStore. -func (gs *Store) Has(key []byte) bool { +func (gs *GStore[V]) Has(key []byte) bool { gs.gasMeter.ConsumeGas(gs.gasConfig.HasCost, types.GasHasDesc) return gs.parent.Has(key) } // Implements KVStore. -func (gs *Store) Delete(key []byte) { +func (gs *GStore[V]) Delete(key []byte) { // charge gas to prevent certain attack vectors even though space is being freed gs.gasMeter.ConsumeGas(gs.gasConfig.DeleteCost, types.GasDeleteDesc) gs.parent.Delete(key) @@ -68,7 +105,7 @@ func (gs *Store) Delete(key []byte) { // Iterator implements the KVStore interface. It returns an iterator which // incurs a flat gas cost for seeking to the first key/value pair and a variable // gas cost based on the current value's length if the iterator is valid. -func (gs *Store) Iterator(start, end []byte) types.Iterator { +func (gs *GStore[V]) Iterator(start, end []byte) types.GIterator[V] { return gs.iterator(start, end, true) } @@ -76,94 +113,95 @@ func (gs *Store) Iterator(start, end []byte) types.Iterator { // iterator which incurs a flat gas cost for seeking to the first key/value pair // and a variable gas cost based on the current value's length if the iterator // is valid. -func (gs *Store) ReverseIterator(start, end []byte) types.Iterator { +func (gs *GStore[V]) ReverseIterator(start, end []byte) types.GIterator[V] { return gs.iterator(start, end, false) } // Implements KVStore. -func (gs *Store) CacheWrap() types.CacheWrap { +func (gs *GStore[V]) CacheWrap() types.CacheWrap { panic("cannot CacheWrap a GasKVStore") } -func (gs *Store) iterator(start, end []byte, ascending bool) types.Iterator { - var parent types.Iterator +func (gs *GStore[V]) iterator(start, end []byte, ascending bool) types.GIterator[V] { + var parent types.GIterator[V] if ascending { parent = gs.parent.Iterator(start, end) } else { parent = gs.parent.ReverseIterator(start, end) } - gi := newGasIterator(gs.gasMeter, gs.gasConfig, parent) - gi.(*gasIterator).consumeSeekGas() + gi := newGasIterator(gs.gasMeter, gs.gasConfig, parent, gs.valueLen) + gi.consumeSeekGas() return gi } -type gasIterator struct { +type gasIterator[V any] struct { gasMeter types.GasMeter gasConfig types.GasConfig - parent types.Iterator + parent types.GIterator[V] + valueLen func(V) int } -func newGasIterator(gasMeter types.GasMeter, gasConfig types.GasConfig, parent types.Iterator) types.Iterator { - return &gasIterator{ +func newGasIterator[V any](gasMeter types.GasMeter, gasConfig types.GasConfig, parent types.GIterator[V], valueLen func(V) int) *gasIterator[V] { + return &gasIterator[V]{ gasMeter: gasMeter, gasConfig: gasConfig, parent: parent, + valueLen: valueLen, } } // Implements Iterator. -func (gi *gasIterator) Domain() (start, end []byte) { +func (gi *gasIterator[V]) Domain() (start, end []byte) { return gi.parent.Domain() } // Implements Iterator. -func (gi *gasIterator) Valid() bool { +func (gi *gasIterator[V]) Valid() bool { return gi.parent.Valid() } // Next implements the Iterator interface. It seeks to the next key/value pair // in the iterator. It incurs a flat gas cost for seeking and a variable gas // cost based on the current value's length if the iterator is valid. -func (gi *gasIterator) Next() { +func (gi *gasIterator[V]) Next() { gi.consumeSeekGas() gi.parent.Next() } // Key implements the Iterator interface. It returns the current key and it does // not incur any gas cost. -func (gi *gasIterator) Key() (key []byte) { +func (gi *gasIterator[V]) Key() (key []byte) { key = gi.parent.Key() return key } // Value implements the Iterator interface. It returns the current value and it // does not incur any gas cost. -func (gi *gasIterator) Value() (value []byte) { - value = gi.parent.Value() - return value +func (gi *gasIterator[V]) Value() (value V) { + return gi.parent.Value() } // Implements Iterator. -func (gi *gasIterator) Close() error { +func (gi *gasIterator[V]) Close() error { return gi.parent.Close() } // Error delegates the Error call to the parent iterator. -func (gi *gasIterator) Error() error { +func (gi *gasIterator[V]) Error() error { return gi.parent.Error() } // consumeSeekGas consumes on each iteration step a flat gas cost and a variable gas cost // based on the current value's length. -func (gi *gasIterator) consumeSeekGas() { +func (gi *gasIterator[V]) consumeSeekGas() { if gi.Valid() { key := gi.Key() value := gi.Value() gi.gasMeter.ConsumeGas(gi.gasConfig.ReadCostPerByte*types.Gas(len(key)), types.GasValuePerByteDesc) - gi.gasMeter.ConsumeGas(gi.gasConfig.ReadCostPerByte*types.Gas(len(value)), types.GasValuePerByteDesc) + gi.gasMeter.ConsumeGas(gi.gasConfig.ReadCostPerByte*types.Gas(gi.valueLen(value)), types.GasValuePerByteDesc) } gi.gasMeter.ConsumeGas(gi.gasConfig.IterNextCostFlat, types.GasIterNextCostFlatDesc) } diff --git a/store/iavl/tree.go b/store/iavl/tree.go index 889fc1d5a07f..6819029f2642 100644 --- a/store/iavl/tree.go +++ b/store/iavl/tree.go @@ -3,8 +3,8 @@ package iavl import ( "fmt" + dbm "github.com/cosmos/cosmos-db" "github.com/cosmos/iavl" - idb "github.com/cosmos/iavl/db" ) var ( @@ -31,7 +31,7 @@ type ( GetVersioned(key []byte, version int64) ([]byte, error) GetImmutable(version int64) (*iavl.ImmutableTree, error) SetInitialVersion(version uint64) - Iterator(start, end []byte, ascending bool) (idb.Iterator, error) + Iterator(start, end []byte, ascending bool) (dbm.Iterator, error) AvailableVersions() []int LoadVersionForOverwriting(targetVersion int64) error TraverseStateChanges(startVersion, endVersion int64, fn func(version int64, changeSet *iavl.ChangeSet) error) error diff --git a/store/cachekv/internal/btree.go b/store/internal/btree/btree.go similarity index 63% rename from store/cachekv/internal/btree.go rename to store/internal/btree/btree.go index 209f7e58c4dd..0fe28def41ee 100644 --- a/store/cachekv/internal/btree.go +++ b/store/internal/btree/btree.go @@ -1,4 +1,4 @@ -package internal +package btree import ( "bytes" @@ -22,44 +22,46 @@ var errKeyEmpty = errors.New("key cannot be empty") // we need it to be as fast as possible, while `MemDB` is mainly used as a mocking db in unit tests. // // We choose tidwall/btree over google/btree here because it provides API to implement step iterator directly. -type BTree struct { - tree *btree.BTreeG[item] +type BTree[V any] struct { + tree *btree.BTreeG[item[V]] } // NewBTree creates a wrapper around `btree.BTreeG`. -func NewBTree() BTree { - return BTree{ - tree: btree.NewBTreeGOptions(byKeys, btree.Options{ +func NewBTree[V any]() BTree[V] { + return BTree[V]{ + tree: btree.NewBTreeGOptions(byKeys[V], btree.Options{ Degree: bTreeDegree, NoLocks: false, }), } } -func (bt BTree) Set(key, value []byte) { +func (bt BTree[V]) Set(key []byte, value V) { bt.tree.Set(newItem(key, value)) } -func (bt BTree) Get(key []byte) []byte { - i, found := bt.tree.Get(newItem(key, nil)) +func (bt BTree[V]) Get(key []byte) V { + var empty V + i, found := bt.tree.Get(newItem(key, empty)) if !found { - return nil + return empty } return i.value } -func (bt BTree) Delete(key []byte) { - bt.tree.Delete(newItem(key, nil)) +func (bt BTree[V]) Delete(key []byte) { + var empty V + bt.tree.Delete(newItem(key, empty)) } -func (bt BTree) Iterator(start, end []byte) (types.Iterator, error) { +func (bt BTree[V]) Iterator(start, end []byte) (types.GIterator[V], error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errKeyEmpty } return newMemIterator(start, end, bt, true), nil } -func (bt BTree) ReverseIterator(start, end []byte) (types.Iterator, error) { +func (bt BTree[V]) ReverseIterator(start, end []byte) (types.GIterator[V], error) { if (start != nil && len(start) == 0) || (end != nil && len(end) == 0) { return nil, errKeyEmpty } @@ -68,24 +70,28 @@ func (bt BTree) ReverseIterator(start, end []byte) (types.Iterator, error) { // Copy the tree. This is a copy-on-write operation and is very fast because // it only performs a shadowed copy. -func (bt BTree) Copy() BTree { - return BTree{ +func (bt BTree[V]) Copy() BTree[V] { + return BTree[V]{ tree: bt.tree.Copy(), } } +func (bt BTree[V]) Clear() { + bt.tree.Clear() +} + // item is a btree item with byte slices as keys and values -type item struct { +type item[V any] struct { key []byte - value []byte + value V } // byKeys compares the items by key -func byKeys(a, b item) bool { +func byKeys[V any](a, b item[V]) bool { return bytes.Compare(a.key, b.key) == -1 } // newItem creates a new pair item. -func newItem(key, value []byte) item { - return item{key: key, value: value} +func newItem[V any](key []byte, value V) item[V] { + return item[V]{key: key, value: value} } diff --git a/store/cachekv/internal/btree_test.go b/store/internal/btree/btree_test.go similarity index 98% rename from store/cachekv/internal/btree_test.go rename to store/internal/btree/btree_test.go index 06437997f636..89b5827e6156 100644 --- a/store/cachekv/internal/btree_test.go +++ b/store/internal/btree/btree_test.go @@ -1,4 +1,4 @@ -package internal +package btree import ( "testing" @@ -9,7 +9,7 @@ import ( ) func TestGetSetDelete(t *testing.T) { - db := NewBTree() + db := NewBTree[[]byte]() // A nonexistent key should return nil. value := db.Get([]byte("a")) @@ -40,7 +40,7 @@ func TestGetSetDelete(t *testing.T) { } func TestDBIterator(t *testing.T) { - db := NewBTree() + db := NewBTree[[]byte]() for i := 0; i < 10; i++ { if i != 6 { // but skip 6. @@ -171,7 +171,7 @@ func TestDBIterator(t *testing.T) { []int64(nil), "reverse iterator from 2 (ex) to 4") // Ensure that the iterators don't panic with an empty database. - db2 := NewBTree() + db2 := NewBTree[[]byte]() itr, err = db2.Iterator(nil, nil) require.NoError(t, err) diff --git a/store/cachekv/internal/memiterator.go b/store/internal/btree/memiterator.go similarity index 64% rename from store/cachekv/internal/memiterator.go rename to store/internal/btree/memiterator.go index 9dbba7587071..e98ae9cd834e 100644 --- a/store/cachekv/internal/memiterator.go +++ b/store/internal/btree/memiterator.go @@ -1,4 +1,4 @@ -package internal +package btree import ( "bytes" @@ -9,13 +9,13 @@ import ( "cosmossdk.io/store/types" ) -var _ types.Iterator = (*memIterator)(nil) +var _ types.Iterator = (*memIterator[[]byte])(nil) // memIterator iterates over iterKVCache items. // if value is nil, means it was deleted. // Implements Iterator. -type memIterator struct { - iter btree.IterG[item] +type memIterator[V any] struct { + iter btree.IterG[item[V]] start []byte end []byte @@ -23,18 +23,21 @@ type memIterator struct { valid bool } -func newMemIterator(start, end []byte, items BTree, ascending bool) *memIterator { +func newMemIterator[V any](start, end []byte, items BTree[V], ascending bool) *memIterator[V] { + var ( + valid bool + empty V + ) iter := items.tree.Iter() - var valid bool if ascending { if start != nil { - valid = iter.Seek(newItem(start, nil)) + valid = iter.Seek(newItem(start, empty)) } else { valid = iter.First() } } else { if end != nil { - valid = iter.Seek(newItem(end, nil)) + valid = iter.Seek(newItem(end, empty)) if !valid { valid = iter.Last() } else { @@ -46,7 +49,7 @@ func newMemIterator(start, end []byte, items BTree, ascending bool) *memIterator } } - mi := &memIterator{ + mi := &memIterator[V]{ iter: iter, start: start, end: end, @@ -61,27 +64,27 @@ func newMemIterator(start, end []byte, items BTree, ascending bool) *memIterator return mi } -func (mi *memIterator) Domain() (start, end []byte) { +func (mi *memIterator[V]) Domain() (start, end []byte) { return mi.start, mi.end } -func (mi *memIterator) Close() error { +func (mi *memIterator[V]) Close() error { mi.iter.Release() return nil } -func (mi *memIterator) Error() error { +func (mi *memIterator[V]) Error() error { if !mi.Valid() { return errors.New("invalid memIterator") } return nil } -func (mi *memIterator) Valid() bool { +func (mi *memIterator[V]) Valid() bool { return mi.valid } -func (mi *memIterator) Next() { +func (mi *memIterator[V]) Next() { mi.assertValid() if mi.ascending { @@ -95,7 +98,7 @@ func (mi *memIterator) Next() { } } -func (mi *memIterator) keyInRange(key []byte) bool { +func (mi *memIterator[V]) keyInRange(key []byte) bool { if mi.ascending && mi.end != nil && bytes.Compare(key, mi.end) >= 0 { return false } @@ -105,15 +108,15 @@ func (mi *memIterator) keyInRange(key []byte) bool { return true } -func (mi *memIterator) Key() []byte { +func (mi *memIterator[V]) Key() []byte { return mi.iter.Item().key } -func (mi *memIterator) Value() []byte { +func (mi *memIterator[V]) Value() V { return mi.iter.Item().value } -func (mi *memIterator) assertValid() { +func (mi *memIterator[V]) assertValid() { if err := mi.Error(); err != nil { panic(err) } diff --git a/store/internal/btreeadaptor.go b/store/internal/btreeadaptor.go new file mode 100644 index 000000000000..322e0f4152fe --- /dev/null +++ b/store/internal/btreeadaptor.go @@ -0,0 +1,52 @@ +package internal + +import ( + "cosmossdk.io/store/cachekv" + "cosmossdk.io/store/internal/btree" + "cosmossdk.io/store/types" +) + +var _ types.KVStore = (*BTreeStore[[]byte])(nil) + +// BTreeStore is a wrapper for a BTree with GKVStore[V] implementation +type BTreeStore[V any] struct { + btree.BTree[V] + isZero func(V) bool + valueLen func(V) int +} + +// NewBTreeStore constructs new BTree adapter +func NewBTreeStore[V any](btree btree.BTree[V], isZero func(V) bool, valueLen func(V) int) *BTreeStore[V] { + return &BTreeStore[V]{btree, isZero, valueLen} +} + +// Hash Implements GKVStore. +func (ts *BTreeStore[V]) Has(key []byte) bool { + return !ts.isZero(ts.Get(key)) +} + +func (ts *BTreeStore[V]) Iterator(start, end []byte) types.GIterator[V] { + it, err := ts.BTree.Iterator(start, end) + if err != nil { + panic(err) + } + return it +} + +func (ts *BTreeStore[V]) ReverseIterator(start, end []byte) types.GIterator[V] { + it, err := ts.BTree.ReverseIterator(start, end) + if err != nil { + panic(err) + } + return it +} + +// GetStoreType returns the type of the store. +func (ts *BTreeStore[V]) GetStoreType() types.StoreType { + return types.StoreTypeDB +} + +// CacheWrap branches the underlying store. +func (ts *BTreeStore[V]) CacheWrap() types.CacheWrap { + return cachekv.NewGStore(ts, ts.isZero, ts.valueLen) +} diff --git a/store/listenkv/store.go b/store/listenkv/store.go index 343be611b9f0..43d69fa663fc 100644 --- a/store/listenkv/store.go +++ b/store/listenkv/store.go @@ -1,6 +1,7 @@ package listenkv import ( + "cosmossdk.io/store/cachekv" "cosmossdk.io/store/types" ) @@ -130,5 +131,5 @@ func (s *Store) GetStoreType() types.StoreType { // CacheWrap implements the KVStore interface. It panics as a Store // cannot be cache wrapped. func (s *Store) CacheWrap() types.CacheWrap { - panic("cannot CacheWrap a ListenKVStore") + return cachekv.NewStore(s) } diff --git a/store/listenkv/store_test.go b/store/listenkv/store_test.go index 1f9ca4b2942e..f688f5c4c0d9 100644 --- a/store/listenkv/store_test.go +++ b/store/listenkv/store_test.go @@ -272,5 +272,5 @@ func TestListenKVStoreGetStoreType(t *testing.T) { func TestListenKVStoreCacheWrap(t *testing.T) { store := newEmptyListenKVStore(nil) - require.Panics(t, func() { store.CacheWrap() }) + store.CacheWrap() } diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index 043d8c2b8bc2..ceb1a4fcd0aa 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -66,9 +66,10 @@ type Store struct { // iavlSyncPruning should rarely be set to true. // The Prune command will automatically set this to true. // This allows the prune command to wait for the pruning to finish before returning. - iavlSyncPruning bool - storesParams map[types.StoreKey]storeParams - stores map[types.StoreKey]types.CommitKVStore + iavlSyncPruning bool + storesParams map[types.StoreKey]storeParams + // CommitStore is a common interface to unify generic CommitKVStore of different value types + stores map[types.StoreKey]types.CommitStore keysByName map[string]types.StoreKey initialVersion int64 removalMap map[types.StoreKey]bool @@ -97,7 +98,7 @@ func NewStore(db dbm.DB, logger log.Logger, metricGatherer metrics.StoreMetrics) iavlCacheSize: iavl.DefaultIAVLCacheSize, iavlDisableFastNode: iavlDisablefastNodeDefault, storesParams: make(map[types.StoreKey]storeParams), - stores: make(map[types.StoreKey]types.CommitKVStore), + stores: make(map[types.StoreKey]types.CommitStore), keysByName: make(map[string]types.StoreKey), listeners: make(map[types.StoreKey]*types.MemoryListener), removalMap: make(map[types.StoreKey]bool), @@ -164,12 +165,6 @@ func (rs *Store) MountStoreWithDB(key types.StoreKey, typ types.StoreType, db db // GetCommitStore returns a mounted CommitStore for a given StoreKey. If the // store is wrapped in an inter-block cache, it will be unwrapped before returning. func (rs *Store) GetCommitStore(key types.StoreKey) types.CommitStore { - return rs.GetCommitKVStore(key) -} - -// GetCommitKVStore returns a mounted CommitKVStore for a given StoreKey. If the -// store is wrapped in an inter-block cache, it will be unwrapped before returning. -func (rs *Store) GetCommitKVStore(key types.StoreKey) types.CommitKVStore { // If the Store has an inter-block cache, first attempt to lookup and unwrap // the underlying CommitKVStore by StoreKey. If it does not exist, fallback to // the main mapping of CommitKVStores. @@ -182,6 +177,17 @@ func (rs *Store) GetCommitKVStore(key types.StoreKey) types.CommitKVStore { return rs.stores[key] } +// GetCommitKVStore returns a mounted CommitKVStore for a given StoreKey. If the +// store is wrapped in an inter-block cache, it will be unwrapped before returning. +func (rs *Store) GetCommitKVStore(key types.StoreKey) types.CommitKVStore { + store, ok := rs.GetCommitStore(key).(types.CommitKVStore) + if !ok { + panic(fmt.Sprintf("store with key %v is not CommitKVStore", key)) + } + + return store +} + // StoreKeysByName returns mapping storeNames -> StoreKeys func (rs *Store) StoreKeysByName() map[string]types.StoreKey { return rs.keysByName @@ -230,7 +236,7 @@ func (rs *Store) loadVersion(ver int64, upgrades *types.StoreUpgrades) error { } // load each Store (note this doesn't panic on unmounted keys now) - newStores := make(map[types.StoreKey]types.CommitKVStore) + newStores := make(map[types.StoreKey]types.CommitStore) storesKeys := make([]types.StoreKey, 0, len(rs.storesParams)) @@ -556,11 +562,13 @@ func (rs *Store) CacheWrap() types.CacheWrap { func (rs *Store) CacheMultiStore() types.CacheMultiStore { stores := make(map[types.StoreKey]types.CacheWrapper) for k, v := range rs.stores { - store := types.KVStore(v) - // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, - // set same listeners on cache store will observe duplicated writes. - if rs.ListeningEnabled(k) { - store = listenkv.NewStore(store, k, rs.listeners[k]) + store := types.CacheWrapper(v) + if kv, ok := store.(types.KVStore); ok { + // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, + // set same listeners on cache store will observe duplicated writes. + if rs.ListeningEnabled(k) { + store = listenkv.NewStore(kv, k, rs.listeners[k]) + } } stores[k] = store } @@ -576,7 +584,7 @@ func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStor var commitInfo *types.CommitInfo storeInfos := map[string]bool{} for key, store := range rs.stores { - var cacheStore types.KVStore + var cacheStore types.CacheWrapper switch store.GetStoreType() { case types.StoreTypeIAVL: // If the store is wrapped with an inter-block cache, we must first unwrap @@ -615,10 +623,12 @@ func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStor cacheStore = store } - // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, - // set same listeners on cache store will observe duplicated writes. - if rs.ListeningEnabled(key) { - cacheStore = listenkv.NewStore(cacheStore, key, rs.listeners[key]) + if kv, ok := cacheStore.(types.KVStore); ok { + // Wire the listenkv.Store to allow listeners to observe the writes from the cache store, + // set same listeners on cache store will observe duplicated writes. + if rs.ListeningEnabled(key) { + cacheStore = listenkv.NewStore(kv, key, rs.listeners[key]) + } } cachedStores[key] = cacheStore @@ -634,7 +644,7 @@ func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStor // TODO: This isn't used directly upstream. Consider returning the Store as-is // instead of unwrapping. func (rs *Store) GetStore(key types.StoreKey) types.Store { - store := rs.GetCommitKVStore(key) + store := rs.GetCommitStore(key) if store == nil { panic(fmt.Sprintf("store does not exist for key: %s", key.Name())) } @@ -653,7 +663,10 @@ func (rs *Store) GetKVStore(key types.StoreKey) types.KVStore { if s == nil { panic(fmt.Sprintf("store does not exist for key: %s", key.Name())) } - store := types.KVStore(s) + store, ok := s.(types.KVStore) + if !ok { + panic(fmt.Sprintf("store with key %v is not KVStore", key)) + } if rs.TracingEnabled() { store = tracekv.NewStore(store, rs.traceWriter, rs.getTracingContext()) @@ -665,6 +678,20 @@ func (rs *Store) GetKVStore(key types.StoreKey) types.KVStore { return store } +// GetObjKVStore returns a mounted ObjKVStore for a given StoreKey. +func (rs *Store) GetObjKVStore(key types.StoreKey) types.ObjKVStore { + s := rs.stores[key] + if s == nil { + panic(fmt.Sprintf("store does not exist for key: %s", key.Name())) + } + store, ok := s.(types.ObjKVStore) + if !ok { + panic(fmt.Sprintf("store with key %v is not ObjKVStore", key)) + } + + return store +} + func (rs *Store) handlePruning(version int64) error { pruneHeight := rs.pruningManager.GetPruningHeight(version) rs.logger.Debug("prune start", "height", version) @@ -716,7 +743,7 @@ func (rs *Store) GetStoreByName(name string) types.Store { return nil } - return rs.GetCommitKVStore(key) + return rs.GetCommitStore(key) } // Query calls substore.Query with the same `req` where `req.Path` is @@ -829,7 +856,7 @@ func (rs *Store) Snapshot(height uint64, protoWriter protoio.Writer) error { stores := []namedStore{} keys := keysFromStoreKeyMap(rs.stores) for _, key := range keys { - switch store := rs.GetCommitKVStore(key).(type) { + switch store := rs.GetCommitStore(key).(type) { case *iavl.Store: stores = append(stores, namedStore{name: key.Name(), Store: store}) case *transient.Store, *mem.Store: @@ -992,7 +1019,7 @@ loop: return snapshotItem, rs.LoadLatestVersion() } -func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, id types.CommitID, params storeParams) (types.CommitKVStore, error) { +func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, id types.CommitID, params storeParams) (types.CommitStore, error) { var db dbm.DB if params.db != nil { @@ -1032,12 +1059,20 @@ func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, id types.CommitID return transient.NewStore(), nil case types.StoreTypeMemory: + _, ok := key.(*types.ObjectStoreKey) + if !ok { + return nil, fmt.Errorf("invalid StoreKey for StoreTypeTransient: %s", key.String()) + } + if _, ok := key.(*types.MemoryStoreKey); !ok { return nil, fmt.Errorf("unexpected key type for a MemoryStoreKey; got: %s", key.String()) } return mem.NewStore(), nil + case types.StoreTypeObject: + return transient.NewObjStore(), nil + default: panic(fmt.Sprintf("unrecognized store type %v", params.typ)) } @@ -1049,7 +1084,7 @@ func (rs *Store) buildCommitInfo(version int64) *types.CommitInfo { for _, key := range keys { store := rs.stores[key] storeType := store.GetStoreType() - if storeType == types.StoreTypeTransient || storeType == types.StoreTypeMemory { + if storeType == types.StoreTypeTransient || storeType == types.StoreTypeMemory || storeType == types.StoreTypeObject { continue } storeInfos = append(storeInfos, types.StoreInfo{ @@ -1167,7 +1202,7 @@ func GetLatestVersion(db dbm.DB) int64 { } // Commits each store and returns a new commitInfo. -func commitStores(version int64, storeMap map[types.StoreKey]types.CommitKVStore, removalMap map[types.StoreKey]bool) *types.CommitInfo { +func commitStores(version int64, storeMap map[types.StoreKey]types.CommitStore, removalMap map[types.StoreKey]bool) *types.CommitInfo { storeInfos := make([]types.StoreInfo, 0, len(storeMap)) storeKeys := keysFromStoreKeyMap(storeMap) @@ -1187,7 +1222,7 @@ func commitStores(version int64, storeMap map[types.StoreKey]types.CommitKVStore } storeType := store.GetStoreType() - if storeType == types.StoreTypeTransient || storeType == types.StoreTypeMemory { + if storeType == types.StoreTypeTransient || storeType == types.StoreTypeMemory || storeType == types.StoreTypeObject { continue } diff --git a/store/rootmulti/store_test.go b/store/rootmulti/store_test.go index 69a02281cd59..1cae379c22c5 100644 --- a/store/rootmulti/store_test.go +++ b/store/rootmulti/store_test.go @@ -798,6 +798,7 @@ var ( testStoreKey1 = types.NewKVStoreKey("store1") testStoreKey2 = types.NewKVStoreKey("store2") testStoreKey3 = types.NewKVStoreKey("store3") + testStoreKey4 = types.NewKVStoreKey("store4") ) func newMultiStoreWithMounts(db dbm.DB, pruningOpts pruningtypes.PruningOptions) *Store { @@ -870,7 +871,7 @@ func getExpectedCommitID(store *Store, ver int64) types.CommitID { } } -func hashStores(stores map[types.StoreKey]types.CommitKVStore) []byte { +func hashStores(stores map[types.StoreKey]types.CommitStore) []byte { m := make(map[string][]byte, len(stores)) for key, store := range stores { name := key.Name() @@ -925,35 +926,39 @@ func TestStateListeners(t *testing.T) { require.Empty(t, ms.PopStateCache()) } -type commitKVStoreStub struct { - types.CommitKVStore +type commitStoreStub struct { + types.CommitStore Committed int } -func (stub *commitKVStoreStub) Commit() types.CommitID { - commitID := stub.CommitKVStore.Commit() +func (stub *commitStoreStub) Commit() types.CommitID { + commitID := stub.CommitStore.Commit() stub.Committed++ return commitID } -func prepareStoreMap() (map[types.StoreKey]types.CommitKVStore, error) { +func prepareStoreMap() (map[types.StoreKey]types.CommitStore, error) { var db dbm.DB = dbm.NewMemDB() store := NewStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) store.MountStoreWithDB(types.NewKVStoreKey("iavl1"), types.StoreTypeIAVL, nil) store.MountStoreWithDB(types.NewKVStoreKey("iavl2"), types.StoreTypeIAVL, nil) store.MountStoreWithDB(types.NewTransientStoreKey("trans1"), types.StoreTypeTransient, nil) + store.MountStoreWithDB(types.NewObjectStoreKey("obj1"), types.StoreTypeObject, nil) if err := store.LoadLatestVersion(); err != nil { return nil, err } - return map[types.StoreKey]types.CommitKVStore{ - testStoreKey1: &commitKVStoreStub{ - CommitKVStore: store.GetStoreByName("iavl1").(types.CommitKVStore), + return map[types.StoreKey]types.CommitStore{ + testStoreKey1: &commitStoreStub{ + CommitStore: store.GetStoreByName("iavl1").(types.CommitStore), }, - testStoreKey2: &commitKVStoreStub{ - CommitKVStore: store.GetStoreByName("iavl2").(types.CommitKVStore), + testStoreKey2: &commitStoreStub{ + CommitStore: store.GetStoreByName("iavl2").(types.CommitStore), }, - testStoreKey3: &commitKVStoreStub{ - CommitKVStore: store.GetStoreByName("trans1").(types.CommitKVStore), + testStoreKey3: &commitStoreStub{ + CommitStore: store.GetStoreByName("trans1").(types.CommitStore), + }, + testStoreKey4: &commitStoreStub{ + CommitStore: store.GetStoreByName("obj1").(types.CommitStore), }, }, nil } @@ -984,7 +989,7 @@ func TestCommitStores(t *testing.T) { t.Run(tc.name, func(t *testing.T) { storeMap, err := prepareStoreMap() require.NoError(t, err) - store := storeMap[testStoreKey1].(*commitKVStoreStub) + store := storeMap[testStoreKey1].(*commitStoreStub) for i := tc.committed; i > 0; i-- { store.Commit() } diff --git a/store/tracekv/store.go b/store/tracekv/store.go index 7df8b0f97ba7..9fbc1bb4a3ef 100644 --- a/store/tracekv/store.go +++ b/store/tracekv/store.go @@ -6,6 +6,7 @@ import ( "io" "cosmossdk.io/errors" + "cosmossdk.io/store/cachekv" "cosmossdk.io/store/types" ) @@ -164,7 +165,7 @@ func (tkv *Store) GetStoreType() types.StoreType { // CacheWrap implements the KVStore interface. It panics because a Store // cannot be branched. func (tkv *Store) CacheWrap() types.CacheWrap { - panic("cannot CacheWrap a TraceKVStore") + return cachekv.NewStore(tkv) } // writeOperation writes a KVStore operation to the underlying io.Writer as diff --git a/store/tracekv/store_test.go b/store/tracekv/store_test.go index d276accd530a..00e4406c7574 100644 --- a/store/tracekv/store_test.go +++ b/store/tracekv/store_test.go @@ -283,5 +283,5 @@ func TestTraceKVStoreGetStoreType(t *testing.T) { func TestTraceKVStoreCacheWrap(t *testing.T) { store := newEmptyTraceKVStore(nil) - require.Panics(t, func() { store.CacheWrap() }) + store.CacheWrap() } diff --git a/store/transient/store.go b/store/transient/store.go index 6f393279f571..53332e9f33c1 100644 --- a/store/transient/store.go +++ b/store/transient/store.go @@ -1,9 +1,8 @@ package transient import ( - dbm "github.com/cosmos/cosmos-db" - - "cosmossdk.io/store/dbadapter" + "cosmossdk.io/store/internal" + "cosmossdk.io/store/internal/btree" pruningtypes "cosmossdk.io/store/pruning/types" "cosmossdk.io/store/types" ) @@ -11,43 +10,73 @@ import ( var ( _ types.Committer = (*Store)(nil) _ types.KVStore = (*Store)(nil) + + _ types.Committer = (*ObjStore)(nil) + _ types.ObjKVStore = (*ObjStore)(nil) ) // Store is a wrapper for a MemDB with Commiter implementation +type GStore[V any] struct { + internal.BTreeStore[V] +} + +// NewGStore constructs new generic transient store +func NewGStore[V any](isZero func(V) bool, valueLen func(V) int) *GStore[V] { + return &GStore[V]{*internal.NewBTreeStore(btree.NewBTree[V](), isZero, valueLen)} +} + +// Store specializes GStore for []byte type Store struct { - dbadapter.Store + GStore[[]byte] } -// Constructs new MemDB adapter func NewStore() *Store { - return &Store{Store: dbadapter.Store{DB: dbm.NewMemDB()}} + return &Store{*NewGStore( + func(v []byte) bool { return v == nil }, + func(v []byte) int { return len(v) }, + )} +} + +func (*Store) GetStoreType() types.StoreType { + return types.StoreTypeTransient +} + +// ObjStore specializes GStore for any +type ObjStore struct { + GStore[any] +} + +func NewObjStore() *ObjStore { + return &ObjStore{*NewGStore( + func(v any) bool { return v == nil }, + func(v any) int { return 1 }, // for value length validation + )} +} + +func (*ObjStore) GetStoreType() types.StoreType { + return types.StoreTypeObject } // Implements CommitStore // Commit cleans up Store. -func (ts *Store) Commit() (id types.CommitID) { - ts.Store = dbadapter.Store{DB: dbm.NewMemDB()} +func (ts *GStore[V]) Commit() (id types.CommitID) { + ts.Clear() return } -func (ts *Store) SetPruning(_ pruningtypes.PruningOptions) {} +func (ts *GStore[V]) SetPruning(_ pruningtypes.PruningOptions) {} // GetPruning is a no-op as pruning options cannot be directly set on this store. // They must be set on the root commit multi-store. -func (ts *Store) GetPruning() pruningtypes.PruningOptions { +func (ts *GStore[V]) GetPruning() pruningtypes.PruningOptions { return pruningtypes.NewPruningOptions(pruningtypes.PruningUndefined) } // Implements CommitStore -func (ts *Store) LastCommitID() types.CommitID { +func (ts *GStore[V]) LastCommitID() types.CommitID { return types.CommitID{} } -func (ts *Store) WorkingHash() []byte { +func (ts *GStore[V]) WorkingHash() []byte { return []byte{} } - -// Implements Store. -func (ts *Store) GetStoreType() types.StoreType { - return types.StoreTypeTransient -} diff --git a/store/types/store.go b/store/types/store.go index f74cfbc440ec..4f90d942a55c 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -128,6 +128,7 @@ type MultiStore interface { // If the store does not exist, panics. GetStore(StoreKey) Store GetKVStore(StoreKey) KVStore + GetObjKVStore(StoreKey) ObjKVStore // TracingEnabled returns if tracing is enabled for the MultiStore. TracingEnabled() bool @@ -227,25 +228,25 @@ type CommitMultiStore interface { //---------subsp------------------------------- // KVStore -// BasicKVStore is a simple interface to get/set data -type BasicKVStore interface { +// GBasicKVStore is a simple interface to get/set data +type GBasicKVStore[V any] interface { // Get returns nil if key doesn't exist. Panics on nil key. - Get(key []byte) []byte + Get(key []byte) V // Has checks if a key exists. Panics on nil key. Has(key []byte) bool // Set sets the key. Panics on nil key or value. - Set(key, value []byte) + Set(key []byte, value V) // Delete deletes the key. Panics on nil key. Delete(key []byte) } -// KVStore additionally provides iteration and deletion -type KVStore interface { +// GKVStore additionally provides iteration and deletion +type GKVStore[V any] interface { Store - BasicKVStore + GBasicKVStore[V] // Iterator over a domain of keys in ascending order. End is exclusive. // Start must be less than end, or the Iterator is invalid. @@ -253,18 +254,54 @@ type KVStore interface { // To iterate over entire domain, use store.Iterator(nil, nil) // CONTRACT: No writes may happen within a domain while an iterator exists over it. // Exceptionally allowed for cachekv.Store, safe to write in the modules. - Iterator(start, end []byte) Iterator + Iterator(start, end []byte) GIterator[V] // Iterator over a domain of keys in descending order. End is exclusive. // Start must be less than end, or the Iterator is invalid. // Iterator must be closed by caller. // CONTRACT: No writes may happen within a domain while an iterator exists over it. // Exceptionally allowed for cachekv.Store, safe to write in the modules. - ReverseIterator(start, end []byte) Iterator + ReverseIterator(start, end []byte) GIterator[V] } -// Iterator is an alias db's Iterator for convenience. -type Iterator = dbm.Iterator +// GIterator is the generic version of dbm's Iterator +type GIterator[V any] interface { + // Domain returns the start (inclusive) and end (exclusive) limits of the iterator. + // CONTRACT: start, end readonly []byte + Domain() (start, end []byte) + + // Valid returns whether the current iterator is valid. Once invalid, the Iterator remains + // invalid forever. + Valid() bool + + // Next moves the iterator to the next key in the database, as defined by order of iteration. + // If Valid returns false, this method will panic. + Next() + + // Key returns the key at the current position. Panics if the iterator is invalid. + // CONTRACT: key readonly []byte + Key() (key []byte) + + // Value returns the value at the current position. Panics if the iterator is invalid. + // CONTRACT: value readonly []byte + Value() (value V) + + // Error returns the last error encountered by the iterator, if any. + Error() error + + // Close closes the iterator, relasing any allocated resources. + Close() error +} + +type ( + Iterator = GIterator[[]byte] + BasicKVStore = GBasicKVStore[[]byte] + KVStore = GKVStore[[]byte] + + ObjIterator = GIterator[any] + ObjBasicKVStore = GBasicKVStore[any] + ObjKVStore = GKVStore[any] +) // CacheKVStore branches a KVStore and provides read cache functionality. // After calling .Write() on the CacheKVStore, all previously created @@ -290,11 +327,10 @@ type CommitKVStore interface { // a Committer, since Commit ephemeral store make no sense. It can return KVStore, // HeapStore, SpaceStore, etc. type CacheWrap interface { + CacheWrapper + // Write syncs with the underlying store. Write() - - // CacheWrap recursively wraps again. - CacheWrap() CacheWrap } type CacheWrapper interface { @@ -324,6 +360,7 @@ const ( StoreTypeMemory StoreTypeSMT StoreTypePersistent + StoreTypeObject ) func (st StoreType) String() string { @@ -348,6 +385,9 @@ func (st StoreType) String() string { case StoreTypePersistent: return "StoreTypePersistent" + + case StoreTypeObject: + return "StoreTypeObject" } return "unknown store type" @@ -427,6 +467,29 @@ func (key *TransientStoreKey) String() string { return fmt.Sprintf("TransientStoreKey{%p, %s}", key, key.name) } +// ObjectStoreKey is used for indexing transient stores in a MultiStore +type ObjectStoreKey struct { + name string +} + +// Constructs new ObjectStoreKey +// Must return a pointer according to the ocap principle +func NewObjectStoreKey(name string) *ObjectStoreKey { + return &ObjectStoreKey{ + name: name, + } +} + +// Implements StoreKey +func (key *ObjectStoreKey) Name() string { + return key.name +} + +// Implements StoreKey +func (key *ObjectStoreKey) String() string { + return fmt.Sprintf("ObjectStoreKey{%p, %s}", key, key.name) +} + // MemoryStoreKey defines a typed key to be used with an in-memory KVStore. type MemoryStoreKey struct { name string diff --git a/store/types/validity.go b/store/types/validity.go index a1fbaba999c7..a1363ff28b1d 100644 --- a/store/types/validity.go +++ b/store/types/validity.go @@ -1,5 +1,7 @@ package types +import "errors" + var ( // 128K - 1 MaxKeyLength = (1 << 17) - 1 @@ -22,7 +24,12 @@ func AssertValidValue(value []byte) { if value == nil { panic("value is nil") } - if len(value) > MaxValueLength { - panic("value is too large") + AssertValidValueLength(len(value)) +} + +// AssertValidValueLength checks if the value length is within length limit +func AssertValidValueLength(l int) { + if l > MaxValueLength { + panic(errors.New("value is too large")) } } diff --git a/tests/go.mod b/tests/go.mod index 48a382f0099c..7911e3cc24f5 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -226,6 +226,7 @@ require ( replace ( // We always want to test against the latest version of the simapp. cosmossdk.io/simapp => ../simapp + cosmossdk.io/store => ../store github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 // We always want to test against the latest version of the SDK. github.com/cosmos/cosmos-sdk => ../. diff --git a/types/context.go b/types/context.go index 5742ec41c85d..ade2906ba1d9 100644 --- a/types/context.go +++ b/types/context.go @@ -349,6 +349,11 @@ func (c Context) TransientStore(key storetypes.StoreKey) storetypes.KVStore { return gaskv.NewStore(c.ms.GetKVStore(key), c.gasMeter, c.transientKVGasConfig) } +// ObjectStore fetches an object store from the MultiStore, +func (c Context) OjectStore(key storetypes.StoreKey) storetypes.ObjKVStore { + return gaskv.NewObjStore(c.ms.GetObjKVStore(key), c.gasMeter, c.transientKVGasConfig) +} + // CacheContext returns a new Context with the multi-store cached and a new // EventManager. The cached context is written to the context when writeCache // is called. Note, events are automatically emitted on the parent context's diff --git a/x/group/internal/orm/testsupport.go b/x/group/internal/orm/testsupport.go index b4fe3d0354ee..b03fcec42ba9 100644 --- a/x/group/internal/orm/testsupport.go +++ b/x/group/internal/orm/testsupport.go @@ -26,8 +26,8 @@ func NewMockContext() *MockContext { } func (m MockContext) KVStore(key storetypes.StoreKey) storetypes.KVStore { - if s := m.store.GetCommitKVStore(key); s != nil { - return s + if s := m.store.GetCommitStore(key); s != nil { + return s.(storetypes.KVStore) } m.store.MountStoreWithDB(key, storetypes.StoreTypeIAVL, m.db) if err := m.store.LoadLatestVersion(); err != nil { From 37a7805f791a7a5436028a269deda2ef19fe211d Mon Sep 17 00:00:00 2001 From: "jay.tseng" Date: Tue, 6 May 2025 17:12:47 -0400 Subject: [PATCH 03/13] fix cherry-picking errors --- baseapp/baseapp.go | 3 +-- simapp/go.mod | 1 + store/iavl/tree.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 19bfdf853eb5..d1a6d9d3be16 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -359,8 +359,7 @@ func (app *BaseApp) MountMemoryStores(keys map[string]*storetypes.MemoryStoreKey // MountObjectStores mounts all transient object stores with the BaseApp's internal // commit multi-store. func (app *BaseApp) MountObjectStores(keys map[string]*storetypes.ObjectStoreKey) { - skeys := maps.Keys(keys) - sort.Strings(skeys) + skeys := slices.Sorted(maps.Keys(keys)) for _, key := range skeys { memKey := keys[key] app.MountStore(memKey, storetypes.StoreTypeObject) diff --git a/simapp/go.mod b/simapp/go.mod index beeb4ca9ea67..df5efaaf21e6 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -228,6 +228,7 @@ require ( // Below are the long-lived replace of the SimApp replace ( + cosmossdk.io/store => ../store // use cosmos fork of keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 // Simapp always use the latest version of the cosmos-sdk diff --git a/store/iavl/tree.go b/store/iavl/tree.go index 6819029f2642..1f6e63a7c547 100644 --- a/store/iavl/tree.go +++ b/store/iavl/tree.go @@ -3,8 +3,8 @@ package iavl import ( "fmt" - dbm "github.com/cosmos/cosmos-db" "github.com/cosmos/iavl" + "github.com/cosmos/iavl/db" ) var ( @@ -31,7 +31,7 @@ type ( GetVersioned(key []byte, version int64) ([]byte, error) GetImmutable(version int64) (*iavl.ImmutableTree, error) SetInitialVersion(version uint64) - Iterator(start, end []byte, ascending bool) (dbm.Iterator, error) + Iterator(start, end []byte, ascending bool) (db.Iterator, error) AvailableVersions() []int LoadVersionForOverwriting(targetVersion int64) error TraverseStateChanges(startVersion, endVersion int64, fn func(version int64, changeSet *iavl.ChangeSet) error) error From 01e3dbaf1b1730bfb35984135ad671efa9c4fdd2 Mon Sep 17 00:00:00 2001 From: yihuang Date: Wed, 27 Mar 2024 12:24:20 +0800 Subject: [PATCH 04/13] prefix store support object store (#236) --- store/cachekv/store.go | 9 +--- store/gaskv/store.go | 9 +--- store/prefix/store.go | 91 ++++++++++++++++++++++++++++------------- store/types/store.go | 14 +++++++ store/types/validity.go | 8 ++++ types/context.go | 2 +- 6 files changed, 87 insertions(+), 46 deletions(-) diff --git a/store/cachekv/store.go b/store/cachekv/store.go index 9f7ff680db60..7c7cf7513845 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -89,17 +89,10 @@ func (store *GStore[V]) Get(key []byte) (value V) { return value } -func (store *GStore[V]) assertValidValue(value V) { - if store.isZero(value) { - panic("value is nil") - } - types.AssertValidValueLength(store.valueLen(value)) -} - // Set implements types.KVStore. func (store *GStore[V]) Set(key []byte, value V) { types.AssertValidKey(key) - store.assertValidValue(value) + types.AssertValidValueGeneric(value, store.isZero, store.valueLen) store.mtx.Lock() defer store.mtx.Unlock() diff --git a/store/gaskv/store.go b/store/gaskv/store.go index 7aab63b0863a..7033d7360036 100644 --- a/store/gaskv/store.go +++ b/store/gaskv/store.go @@ -71,17 +71,10 @@ func (gs *GStore[V]) Get(key []byte) (value V) { return value } -func (gs *GStore[V]) assertValidValue(value V) { - if gs.isZero(value) { - panic("value is nil") - } - types.AssertValidValueLength(gs.valueLen(value)) -} - // Implements KVStore. func (gs *GStore[V]) Set(key []byte, value V) { types.AssertValidKey(key) - gs.assertValidValue(value) + types.AssertValidValueGeneric(value, gs.isZero, gs.valueLen) gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostFlat, types.GasWriteCostFlatDesc) // TODO overflow-safe math? gs.gasMeter.ConsumeGas(gs.gasConfig.WriteCostPerByte*types.Gas(len(key)), types.GasWritePerByteDesc) diff --git a/store/prefix/store.go b/store/prefix/store.go index 10f50b3ea795..908f70210b55 100644 --- a/store/prefix/store.go +++ b/store/prefix/store.go @@ -8,20 +8,53 @@ import ( "cosmossdk.io/store/types" ) -var _ types.KVStore = Store{} +type ( + Store = GStore[[]byte] + ObjStore = GStore[any] +) + +var ( + _ types.KVStore = Store{} + _ types.ObjKVStore = ObjStore{} +) + +func NewStore(parent types.KVStore, prefix []byte) Store { + return NewGStore( + parent, prefix, + func(v []byte) bool { return v == nil }, + func(v []byte) int { return len(v) }, + ) +} -// Store is similar with cometbft/cometbft/libs/db/prefix_db +func NewObjStore(parent types.ObjKVStore, prefix []byte) ObjStore { + return NewGStore( + parent, prefix, + func(v any) bool { return v == nil }, + func(v any) int { return 1 }, + ) +} + +// GStore is similar with cometbft/cometbft/libs/db/prefix_db // both gives access only to the limited subset of the store // for convinience or safety -type Store struct { - parent types.KVStore +type GStore[V any] struct { + parent types.GKVStore[V] prefix []byte + + isZero func(V) bool + valueLen func(V) int } -func NewStore(parent types.KVStore, prefix []byte) Store { - return Store{ +func NewGStore[V any]( + parent types.GKVStore[V], prefix []byte, + isZero func(V) bool, valueLen func(V) int, +) GStore[V] { + return GStore[V]{ parent: parent, prefix: prefix, + + isZero: isZero, + valueLen: valueLen, } } @@ -32,7 +65,7 @@ func cloneAppend(bz, tail []byte) (res []byte) { return } -func (s Store) key(key []byte) (res []byte) { +func (s GStore[V]) key(key []byte) (res []byte) { if key == nil { panic("nil key on Store") } @@ -41,41 +74,41 @@ func (s Store) key(key []byte) (res []byte) { } // Implements Store -func (s Store) GetStoreType() types.StoreType { +func (s GStore[V]) GetStoreType() types.StoreType { return s.parent.GetStoreType() } // Implements CacheWrap -func (s Store) CacheWrap() types.CacheWrap { - return cachekv.NewStore(s) +func (s GStore[V]) CacheWrap() types.CacheWrap { + return cachekv.NewGStore(s, s.isZero, s.valueLen) } // Implements KVStore -func (s Store) Get(key []byte) []byte { +func (s GStore[V]) Get(key []byte) V { res := s.parent.Get(s.key(key)) return res } // Implements KVStore -func (s Store) Has(key []byte) bool { +func (s GStore[V]) Has(key []byte) bool { return s.parent.Has(s.key(key)) } // Implements KVStore -func (s Store) Set(key, value []byte) { +func (s GStore[V]) Set(key []byte, value V) { types.AssertValidKey(key) - types.AssertValidValue(value) + types.AssertValidValueGeneric(value, s.isZero, s.valueLen) s.parent.Set(s.key(key), value) } // Implements KVStore -func (s Store) Delete(key []byte) { +func (s GStore[V]) Delete(key []byte) { s.parent.Delete(s.key(key)) } // Implements KVStore // Check https://github.com/cometbft/cometbft/blob/master/libs/db/prefix_db.go#L106 -func (s Store) Iterator(start, end []byte) types.Iterator { +func (s GStore[V]) Iterator(start, end []byte) types.GIterator[V] { newstart := cloneAppend(s.prefix, start) var newend []byte @@ -92,7 +125,7 @@ func (s Store) Iterator(start, end []byte) types.Iterator { // ReverseIterator implements KVStore // Check https://github.com/cometbft/cometbft/blob/master/libs/db/prefix_db.go#L129 -func (s Store) ReverseIterator(start, end []byte) types.Iterator { +func (s GStore[V]) ReverseIterator(start, end []byte) types.GIterator[V] { newstart := cloneAppend(s.prefix, start) var newend []byte @@ -107,18 +140,18 @@ func (s Store) ReverseIterator(start, end []byte) types.Iterator { return newPrefixIterator(s.prefix, start, end, iter) } -var _ types.Iterator = (*prefixIterator)(nil) +var _ types.Iterator = (*prefixIterator[[]byte])(nil) -type prefixIterator struct { +type prefixIterator[V any] struct { prefix []byte start []byte end []byte - iter types.Iterator + iter types.GIterator[V] valid bool } -func newPrefixIterator(prefix, start, end []byte, parent types.Iterator) *prefixIterator { - return &prefixIterator{ +func newPrefixIterator[V any](prefix, start, end []byte, parent types.GIterator[V]) *prefixIterator[V] { + return &prefixIterator[V]{ prefix: prefix, start: start, end: end, @@ -128,17 +161,17 @@ func newPrefixIterator(prefix, start, end []byte, parent types.Iterator) *prefix } // Implements Iterator -func (pi *prefixIterator) Domain() ([]byte, []byte) { +func (pi *prefixIterator[V]) Domain() ([]byte, []byte) { return pi.start, pi.end } // Implements Iterator -func (pi *prefixIterator) Valid() bool { +func (pi *prefixIterator[V]) Valid() bool { return pi.valid && pi.iter.Valid() } // Implements Iterator -func (pi *prefixIterator) Next() { +func (pi *prefixIterator[V]) Next() { if !pi.valid { panic("prefixIterator invalid, cannot call Next()") } @@ -150,7 +183,7 @@ func (pi *prefixIterator) Next() { } // Implements Iterator -func (pi *prefixIterator) Key() (key []byte) { +func (pi *prefixIterator[V]) Key() (key []byte) { if !pi.valid { panic("prefixIterator invalid, cannot call Key()") } @@ -162,7 +195,7 @@ func (pi *prefixIterator) Key() (key []byte) { } // Implements Iterator -func (pi *prefixIterator) Value() []byte { +func (pi *prefixIterator[V]) Value() V { if !pi.valid { panic("prefixIterator invalid, cannot call Value()") } @@ -171,13 +204,13 @@ func (pi *prefixIterator) Value() []byte { } // Implements Iterator -func (pi *prefixIterator) Close() error { +func (pi *prefixIterator[V]) Close() error { return pi.iter.Close() } // Error returns an error if the prefixIterator is invalid defined by the Valid // method. -func (pi *prefixIterator) Error() error { +func (pi *prefixIterator[V]) Error() error { if !pi.Valid() { return errors.New("invalid prefixIterator") } diff --git a/store/types/store.go b/store/types/store.go index 4f90d942a55c..59e8a140fece 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -583,3 +583,17 @@ func NewMemoryStoreKeys(names ...string) map[string]*MemoryStoreKey { return keys } + +// NewObjectStoreKeys constructs a new map matching store key names to their +// respective ObjectStoreKey references. +// The function will panic if there is a potential conflict in names (see `assertNoPrefix` +// function for more details). +func NewObjectStoreKeys(names ...string) map[string]*ObjectStoreKey { + assertNoCommonPrefix(names) + keys := make(map[string]*ObjectStoreKey) + for _, n := range names { + keys[n] = NewObjectStoreKey(n) + } + + return keys +} diff --git a/store/types/validity.go b/store/types/validity.go index a1363ff28b1d..cfb6088c8a32 100644 --- a/store/types/validity.go +++ b/store/types/validity.go @@ -27,6 +27,14 @@ func AssertValidValue(value []byte) { AssertValidValueLength(len(value)) } +// AssertValidValueGeneric checks if the value is valid(value is not nil and within length limit) +func AssertValidValueGeneric[V any](value V, isZero func(V) bool, valueLen func(V) int) { + if isZero(value) { + panic("value is nil") + } + AssertValidValueLength(valueLen(value)) +} + // AssertValidValueLength checks if the value length is within length limit func AssertValidValueLength(l int) { if l > MaxValueLength { diff --git a/types/context.go b/types/context.go index ade2906ba1d9..3c8015de9673 100644 --- a/types/context.go +++ b/types/context.go @@ -350,7 +350,7 @@ func (c Context) TransientStore(key storetypes.StoreKey) storetypes.KVStore { } // ObjectStore fetches an object store from the MultiStore, -func (c Context) OjectStore(key storetypes.StoreKey) storetypes.ObjKVStore { +func (c Context) ObjectStore(key storetypes.StoreKey) storetypes.ObjKVStore { return gaskv.NewObjStore(c.ms.GetObjKVStore(key), c.gasMeter, c.transientKVGasConfig) } From ae9ebd6b7d55bd87c393f7c44abf667aa0aba468 Mon Sep 17 00:00:00 2001 From: yihuang Date: Fri, 22 Mar 2024 09:14:32 +0800 Subject: [PATCH 05/13] Problem: parallel tx execution is not supported (#205) add basic support in sdk: - add a TxExecutor baseapp option - add TxIndex/TxCount/MsgIndex in context Update CHANGELOG.md Signed-off-by: yihuang fix misspell fix lint run gci fix lint gci seems not compatible with gofumpt --- CHANGELOG.md | 8 +++- baseapp/abci.go | 85 ++++++++++++++++++++++++++--------------- baseapp/baseapp.go | 28 +++++++++++--- baseapp/genesis.go | 2 +- baseapp/options.go | 10 +++++ baseapp/test_helpers.go | 4 +- baseapp/txexecutor.go | 16 ++++++++ types/context.go | 35 +++++++++++++++++ 8 files changed, 148 insertions(+), 40 deletions(-) create mode 100644 baseapp/txexecutor.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1752ce366647..1b7f7c28abbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,7 +174,13 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [v0.50.9](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.9) - 2024-08-07 -## Bug Fixes +### Features + +* (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Add `TxExecutor` baseapp option, add `TxIndex`/`TxCount`/`MsgIndex`/`BlockGasUsed` fields to `Context, to support tx parallel execution. + +## [Unreleased-Upstream] + +### Bug Fixes * (baseapp) [#21159](https://github.com/cosmos/cosmos-sdk/pull/21159) Return PreBlocker events in FinalizeBlockResponse. * [#20939](https://github.com/cosmos/cosmos-sdk/pull/20939) Fix collection reverse iterator to include `pagination.key` in the result. diff --git a/baseapp/abci.go b/baseapp/abci.go index 8d4d83befa5c..306f34bbf84d 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -791,48 +791,34 @@ func (app *BaseApp) internalFinalizeBlock(ctx context.Context, req *abci.Request // Reset the gas meter so that the AnteHandlers aren't required to gasMeter = app.getBlockGasMeter(app.finalizeBlockState.Context()) - app.finalizeBlockState.SetContext(app.finalizeBlockState.Context().WithBlockGasMeter(gasMeter)) + app.finalizeBlockState.SetContext( + app.finalizeBlockState.Context(). + WithBlockGasMeter(gasMeter). + WithTxCount(len(req.Txs)), + ) // Iterate over all raw transactions in the proposal and attempt to execute // them, gathering the execution results. // // NOTE: Not all raw transactions may adhere to the sdk.Tx interface, e.g. // vote extensions, so skip those. - txResults := make([]*abci.ExecTxResult, 0, len(req.Txs)) - for _, rawTx := range req.Txs { - var response *abci.ExecTxResult - - if _, err := app.txDecoder(rawTx); err == nil { - response = app.deliverTx(rawTx) - } else { - // In the case where a transaction included in a block proposal is malformed, - // we still want to return a default response to comet. This is because comet - // expects a response for each transaction included in a block proposal. - response = sdkerrors.ResponseExecTxResultWithEvents( - sdkerrors.ErrTxDecode, - 0, - 0, - nil, - false, - ) - } - - // check after every tx if we should abort - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - // continue - } - - txResults = append(txResults, response) + txResults, err := app.executeTxs(ctx, req.Txs) + if err != nil { + // usually due to canceled + return nil, err } if app.finalizeBlockState.ms.TracingEnabled() { app.finalizeBlockState.ms = app.finalizeBlockState.ms.SetTracingContext(nil).(storetypes.CacheMultiStore) } - endBlock, err := app.endBlock(app.finalizeBlockState.Context()) + var blockGasUsed uint64 + for _, res := range txResults { + blockGasUsed += uint64(res.GasUsed) + } + sdkCtx := app.finalizeBlockState.Context().WithBlockGasUsed(blockGasUsed) + + endBlock, err := app.endBlock(sdkCtx) if err != nil { return nil, err } @@ -856,6 +842,45 @@ func (app *BaseApp) internalFinalizeBlock(ctx context.Context, req *abci.Request }, nil } +func (app *BaseApp) executeTxs(ctx context.Context, txs [][]byte) ([]*abci.ExecTxResult, error) { + if app.txExecutor != nil { + return app.txExecutor(ctx, len(txs), app.finalizeBlockState.ms, func(i int, ms storetypes.MultiStore) *abci.ExecTxResult { + return app.deliverTxWithMultiStore(txs[i], i, ms) + }) + } + + txResults := make([]*abci.ExecTxResult, 0, len(txs)) + for i, rawTx := range txs { + var response *abci.ExecTxResult + + if _, err := app.txDecoder(rawTx); err == nil { + response = app.deliverTx(rawTx, i) + } else { + // In the case where a transaction included in a block proposal is malformed, + // we still want to return a default response to comet. This is because comet + // expects a response for each transaction included in a block proposal. + response = sdkerrors.ResponseExecTxResultWithEvents( + sdkerrors.ErrTxDecode, + 0, + 0, + nil, + false, + ) + } + + // check after every tx if we should abort + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + // continue + } + + txResults = append(txResults, response) + } + return txResults, nil +} + // FinalizeBlock will execute the block proposal provided by RequestFinalizeBlock. // Specifically, it will execute an application's BeginBlock (if defined), followed // by the transactions in the proposal, finally followed by the application's diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index d1a6d9d3be16..b93181e78aa1 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -198,6 +198,9 @@ type BaseApp struct { // // SAFETY: it's safe to do if validators validate the total gas wanted in the `ProcessProposal`, which is the case in the default handler. disableBlockGasMeter bool + + // Optional alternative tx executor, used for block-stm parallel transaction execution. + txExecutor TxExecutor } // NewBaseApp returns a reference to an initialized BaseApp. It accepts a @@ -684,17 +687,17 @@ func (app *BaseApp) getBlockGasMeter(ctx sdk.Context) storetypes.GasMeter { } // retrieve the context for the tx w/ txBytes and other memoized values. -func (app *BaseApp) getContextForTx(mode execMode, txBytes []byte) sdk.Context { +func (app *BaseApp) getContextForTx(mode execMode, txBytes []byte, txIndex int) sdk.Context { app.mu.Lock() defer app.mu.Unlock() - modeState := app.getState(mode) if modeState == nil { panic(fmt.Sprintf("state is nil for mode %v", mode)) } ctx := modeState.Context(). WithTxBytes(txBytes). - WithGasMeter(storetypes.NewInfiniteGasMeter()) + WithGasMeter(storetypes.NewInfiniteGasMeter()). + WithTxIndex(txIndex) // WithVoteInfos(app.voteInfos) // TODO: identify if this is needed ctx = ctx.WithIsSigverifyTx(app.sigverifyTx) @@ -779,7 +782,11 @@ func (app *BaseApp) beginBlock(_ *abci.RequestFinalizeBlock) (sdk.BeginBlock, er return resp, nil } -func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { +func (app *BaseApp) deliverTx(tx []byte, txIndex int) *abci.ExecTxResult { + return app.deliverTxWithMultiStore(tx, txIndex, nil) +} + +func (app *BaseApp) deliverTxWithMultiStore(tx []byte, txIndex int, txMultiStore storetypes.MultiStore) *abci.ExecTxResult { gInfo := sdk.GasInfo{} resultStr := "successful" @@ -792,7 +799,7 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted") }() - gInfo, result, anteEvents, err := app.runTx(execModeFinalize, tx, nil) + gInfo, result, anteEvents, err := app.runTxWithMultiStore(execModeFinalize, tx, nil, txIndex, txMultiStore) if err != nil { resultStr = "failed" resp = sdkerrors.ResponseExecTxResultWithEvents( @@ -852,12 +859,19 @@ func (app *BaseApp) endBlock(_ context.Context) (sdk.EndBlock, error) { // both txbytes and the decoded tx are passed to runTx to avoid the state machine encoding the tx and decoding the transaction twice // passing the decoded tx to runTX is optional, it will be decoded if the tx is nil func (app *BaseApp) runTx(mode execMode, txBytes []byte, tx sdk.Tx) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) { + return app.runTxWithMultiStore(mode, txBytes, tx, -1, nil) +} + +func (app *BaseApp) runTxWithMultiStore(mode execMode, txBytes []byte, tx sdk.Tx, txIndex int, txMultiStore storetypes.MultiStore) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) { // NOTE: GasWanted should be returned by the AnteHandler. GasUsed is // determined by the GasMeter. We need access to the context to get the gas // meter, so we initialize upfront. var gasWanted uint64 - ctx := app.getContextForTx(mode, txBytes) + ctx := app.getContextForTx(mode, txBytes, txIndex) + if txMultiStore != nil { + ctx = ctx.WithMultiStore(txMultiStore) + } ms := ctx.MultiStore() // only run the tx if there is block gas remaining @@ -1050,6 +1064,8 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, msgsV2 []protov2.Me break } + ctx = ctx.WithMsgIndex(i) + handler := app.msgServiceRouter.Handler(msg) if handler == nil { return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "no message handler found for %T", msg) diff --git a/baseapp/genesis.go b/baseapp/genesis.go index 4662d1187b4a..2e5455adc3f5 100644 --- a/baseapp/genesis.go +++ b/baseapp/genesis.go @@ -13,7 +13,7 @@ var _ genesis.TxHandler = (*BaseApp)(nil) // ExecuteGenesisTx implements genesis.GenesisState from // cosmossdk.io/core/genesis to set initial state in genesis func (ba *BaseApp) ExecuteGenesisTx(tx []byte) error { - res := ba.deliverTx(tx) + res := ba.deliverTx(tx, -1) if res.Code != types.CodeTypeOK { return errors.New(res.Log) diff --git a/baseapp/options.go b/baseapp/options.go index 1f809e498b9c..ec32a241401f 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -129,6 +129,11 @@ func DisableBlockGasMeter() func(*BaseApp) { return func(app *BaseApp) { app.SetDisableBlockGasMeter(true) } } +// SetTxExecutor sets a custom tx executor for the BaseApp, usually for parallel execution. +func SetTxExecutor(executor TxExecutor) func(*BaseApp) { + return func(app *BaseApp) { app.txExecutor = executor } +} + func (app *BaseApp) SetName(name string) { if app.sealed { panic("SetName() on sealed BaseApp") @@ -403,3 +408,8 @@ func (app *BaseApp) SetMsgServiceRouter(msgServiceRouter *MsgServiceRouter) { func (app *BaseApp) SetGRPCQueryRouter(grpcQueryRouter *GRPCQueryRouter) { app.grpcQueryRouter = grpcQueryRouter } + +// SetTxExecutor sets a custom tx executor for the BaseApp, usually for parallel execution. +func (app *BaseApp) SetTxExecutor(executor TxExecutor) { + app.txExecutor = executor +} diff --git a/baseapp/test_helpers.go b/baseapp/test_helpers.go index b3aa396a0250..91914f972136 100644 --- a/baseapp/test_helpers.go +++ b/baseapp/test_helpers.go @@ -77,9 +77,9 @@ func (app *BaseApp) NewUncachedContext(isCheckTx bool, header cmtproto.Header) s } func (app *BaseApp) GetContextForFinalizeBlock(txBytes []byte) sdk.Context { - return app.getContextForTx(execModeFinalize, txBytes) + return app.getContextForTx(execModeFinalize, txBytes, -1) } func (app *BaseApp) GetContextForCheckTx(txBytes []byte) sdk.Context { - return app.getContextForTx(execModeCheck, txBytes) + return app.getContextForTx(execModeCheck, txBytes, -1) } diff --git a/baseapp/txexecutor.go b/baseapp/txexecutor.go new file mode 100644 index 000000000000..230250ce9992 --- /dev/null +++ b/baseapp/txexecutor.go @@ -0,0 +1,16 @@ +package baseapp + +import ( + "context" + + abci "github.com/cometbft/cometbft/abci/types" + + "cosmossdk.io/store/types" +) + +type TxExecutor func( + ctx context.Context, + blockSize int, + cms types.MultiStore, + deliverTxWithMultiStore func(int, types.MultiStore) *abci.ExecTxResult, +) ([]*abci.ExecTxResult, error) diff --git a/types/context.go b/types/context.go index 3c8015de9673..824453621d96 100644 --- a/types/context.go +++ b/types/context.go @@ -64,6 +64,15 @@ type Context struct { streamingManager storetypes.StreamingManager cometInfo comet.BlockInfo headerInfo header.Info + + // the index of the current tx in the block, -1 means not in finalize block context + txIndex int + // the index of the current msg in the tx, -1 means not in finalize block context + msgIndex int + // the total number of transactions in current block + txCount int + // sum the gas used by all the transactions in the current block, only accessible by end blocker + blockGasUsed uint64 } // Proposed rename, not done to avoid API breakage @@ -92,6 +101,10 @@ func (c Context) TransientKVGasConfig() storetypes.GasConfig { return c.trans func (c Context) StreamingManager() storetypes.StreamingManager { return c.streamingManager } func (c Context) CometInfo() comet.BlockInfo { return c.cometInfo } func (c Context) HeaderInfo() header.Info { return c.headerInfo } +func (c Context) TxIndex() int { return c.txIndex } +func (c Context) MsgIndex() int { return c.msgIndex } +func (c Context) TxCount() int { return c.txCount } +func (c Context) BlockGasUsed() uint64 { return c.blockGasUsed } // BlockHeader returns the header by value. func (c Context) BlockHeader() cmtproto.Header { @@ -138,6 +151,8 @@ func NewContext(ms storetypes.MultiStore, header cmtproto.Header, isCheckTx bool eventManager: NewEventManager(), kvGasConfig: storetypes.KVGasConfig(), transientKVGasConfig: storetypes.TransientGasConfig(), + txIndex: -1, + msgIndex: -1, } } @@ -317,6 +332,26 @@ func (c Context) WithHeaderInfo(headerInfo header.Info) Context { return c } +func (c Context) WithTxIndex(txIndex int) Context { + c.txIndex = txIndex + return c +} + +func (c Context) WithTxCount(txCount int) Context { + c.txCount = txCount + return c +} + +func (c Context) WithMsgIndex(msgIndex int) Context { + c.msgIndex = msgIndex + return c +} + +func (c Context) WithBlockGasUsed(gasUsed uint64) Context { + c.blockGasUsed = gasUsed + return c +} + // TODO: remove??? func (c Context) IsZero() bool { return c.ms == nil From 5762123adaa0483bef85cf2f44aef425fcf06933 Mon Sep 17 00:00:00 2001 From: yihuang Date: Thu, 28 Mar 2024 12:08:04 +0800 Subject: [PATCH 06/13] Problem: fee collection not compatible with parallel execution (#237) * Problem: no efficient way to collect fee Solution: - support an idea of virtual account in bank module, where the incoming coins are accumulated in a per-tx object store first, then accumulate and credit to the real account at end blocker. it's nesserary to support parallel tx execution, where we try not to access shared states. more efficient sum support SendCoinsFromModuleToAccountVirtual fix test fix test * fix lint * fix test * fix test * fix test * fix test * fix test * fix mock keeper * try fix lint * try fix lint * reuse code * try fix linter * Update x/bank/keeper/send.go Signed-off-by: yihuang * algin panic call * fix error handling * try fix lint * nolintlint generate falst postiive --------- Signed-off-by: yihuang --- .golangci.yml | 1 - CHANGELOG.md | 3 +- baseapp/baseapp.go | 3 + runtime/module.go | 7 + simapp/app.go | 17 +- simapp/app_config.go | 1 + .../bank/keeper/deterministic_test.go | 4 +- .../distribution/keeper/msg_server_test.go | 6 +- .../evidence/keeper/infraction_test.go | 6 +- tests/integration/gov/keeper/keeper_test.go | 6 +- .../slashing/keeper/keeper_test.go | 4 +- .../integration/staking/keeper/common_test.go | 6 +- .../staking/keeper/determinstic_test.go | 4 +- testutil/context.go | 14 ++ testutil/integration/example_test.go | 4 +- testutil/integration/router.go | 6 +- x/bank/keeper/collections_test.go | 4 +- x/bank/keeper/keeper.go | 9 +- x/bank/keeper/keeper_test.go | 47 ++++- x/bank/keeper/send.go | 15 +- x/bank/keeper/virtual.go | 178 ++++++++++++++++++ x/bank/module.go | 10 +- x/bank/types/keys.go | 3 + x/gov/testutil/expected_keepers_mocks.go | 23 +++ 24 files changed, 360 insertions(+), 21 deletions(-) create mode 100644 x/bank/keeper/virtual.go diff --git a/.golangci.yml b/.golangci.yml index a80041c273e2..ea5aaf81db5e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,7 +21,6 @@ linters: - ineffassign - misspell - nakedret - - nolintlint - revive - staticcheck - thelper diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b7f7c28abbd..8511026f6a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Add `TxExecutor` baseapp option, add `TxIndex`/`TxCount`/`MsgIndex`/`BlockGasUsed` fields to `Context, to support tx parallel execution. -* (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Support mount object store in baseapp, add `ObjectStore` api in context.. +* (baseapp) [#206](https://github.com/crypto-org-chain/cosmos-sdk/pull/206) Support mount object store in baseapp, add `ObjectStore` api in context.. +* (bank) [#237](https://github.com/crypto-org-chain/cosmos-sdk/pull/237) Support virtual accounts in sending coins. ## [v0.53.0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.53.0) - 2025-04-29 diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index b93181e78aa1..1cab6c2dc509 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -321,6 +321,9 @@ func (app *BaseApp) MountStores(keys ...storetypes.StoreKey) { case *storetypes.MemoryStoreKey: app.MountStore(key, storetypes.StoreTypeMemory) + case *storetypes.ObjectStoreKey: + app.MountStore(key, storetypes.StoreTypeObject) + default: panic(fmt.Sprintf("Unrecognized store key type :%T", key)) } diff --git a/runtime/module.go b/runtime/module.go index d71c668ef49b..bd098b767e99 100644 --- a/runtime/module.go +++ b/runtime/module.go @@ -68,6 +68,7 @@ func init() { ProvideKVStoreKey, ProvideTransientStoreKey, ProvideMemoryStoreKey, + ProvideObjectStoreKey, ProvideGenesisTxHandler, ProvideKVStoreService, ProvideMemoryStoreService, @@ -238,6 +239,12 @@ func ProvideMemoryStoreKey(config *runtimev1alpha1.Module, key depinject.ModuleK return storeKey } +func ProvideObjectStoreKey(key depinject.ModuleKey, app *AppBuilder) *storetypes.ObjectStoreKey { + storeKey := storetypes.NewObjectStoreKey(fmt.Sprintf("object:%s", key.Name())) + registerStoreKey(app, storeKey) + return storeKey +} + func ProvideGenesisTxHandler(appBuilder *AppBuilder) genesis.TxHandler { return appBuilder.app } diff --git a/simapp/app.go b/simapp/app.go index 620e73b9fc90..1efcb10dff26 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -143,7 +143,9 @@ type SimApp struct { interfaceRegistry types.InterfaceRegistry // keys to access the substores - keys map[string]*storetypes.KVStoreKey + keys map[string]*storetypes.KVStoreKey + tkeys map[string]*storetypes.TransientStoreKey + okeys map[string]*storetypes.ObjectStoreKey // essential keepers AccountKeeper authkeeper.AccountKeeper @@ -280,6 +282,8 @@ func NewSimApp( panic(err) } + tkeys := storetypes.NewTransientStoreKeys(paramstypes.TStoreKey) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) app := &SimApp{ BaseApp: bApp, legacyAmino: legacyAmino, @@ -287,6 +291,8 @@ func NewSimApp( txConfig: txConfig, interfaceRegistry: interfaceRegistry, keys: keys, + tkeys: tkeys, + okeys: okeys, } // set the BaseApp's parameter store @@ -313,6 +319,7 @@ func NewSimApp( app.BankKeeper = bankkeeper.NewBaseKeeper( appCodec, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], app.AccountKeeper, BlockedAddresses(), authtypes.NewModuleAddress(govtypes.ModuleName).String(), @@ -469,7 +476,7 @@ func NewSimApp( app.GovKeeper = *govKeeper.SetHooks( govtypes.NewMultiGovHooks( - // register the governance hooks + // register the governance hooks ), ) @@ -499,7 +506,7 @@ func NewSimApp( app.EpochsKeeper.SetHooks( epochstypes.NewMultiEpochHooks( - // insert epoch hooks receivers here + // insert epoch hooks receivers here ), ) @@ -568,6 +575,8 @@ func NewSimApp( epochstypes.ModuleName, ) app.ModuleManager.SetOrderEndBlockers( + banktypes.ModuleName, + crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, genutiltypes.ModuleName, @@ -663,6 +672,8 @@ func NewSimApp( // initialize stores app.MountKVStores(keys) + app.MountTransientStores(tkeys) + app.MountObjectStores(okeys) // initialize BaseApp app.SetInitChainer(app.InitChainer) diff --git a/simapp/app_config.go b/simapp/app_config.go index f072296c3156..99dc0cc01fdc 100644 --- a/simapp/app_config.go +++ b/simapp/app_config.go @@ -124,6 +124,7 @@ var ( epochstypes.ModuleName, }, EndBlockers: []string{ + banktypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, feegrant.ModuleName, diff --git a/tests/integration/bank/keeper/deterministic_test.go b/tests/integration/bank/keeper/deterministic_test.go index d5fe027f5c61..450153416c90 100644 --- a/tests/integration/bank/keeper/deterministic_test.go +++ b/tests/integration/bank/keeper/deterministic_test.go @@ -67,10 +67,11 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { t.Helper() keys := storetypes.NewKVStoreKeys(authtypes.StoreKey, banktypes.StoreKey) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -96,6 +97,7 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { bankKeeper := keeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/distribution/keeper/msg_server_test.go b/tests/integration/distribution/keeper/msg_server_test.go index bc0434af8886..62503bf67e03 100644 --- a/tests/integration/distribution/keeper/msg_server_test.go +++ b/tests/integration/distribution/keeper/msg_server_test.go @@ -62,10 +62,13 @@ func initFixture(tb testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, distrtypes.StoreKey, stakingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, distribution.AppModuleBasic{}).Codec logger := log.NewTestLogger(tb) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, types.Header{}, true, logger) @@ -93,6 +96,7 @@ func initFixture(tb testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/evidence/keeper/infraction_test.go b/tests/integration/evidence/keeper/infraction_test.go index 18a507f96de8..55c15d39d784 100644 --- a/tests/integration/evidence/keeper/infraction_test.go +++ b/tests/integration/evidence/keeper/infraction_test.go @@ -83,10 +83,13 @@ func initFixture(tb testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, consensusparamtypes.StoreKey, evidencetypes.StoreKey, stakingtypes.StoreKey, slashingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, evidence.AppModuleBasic{}).Codec logger := log.NewTestLogger(tb) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -114,6 +117,7 @@ func initFixture(tb testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/gov/keeper/keeper_test.go b/tests/integration/gov/keeper/keeper_test.go index 388e0be915c1..d4825ca35c3b 100644 --- a/tests/integration/gov/keeper/keeper_test.go +++ b/tests/integration/gov/keeper/keeper_test.go @@ -53,10 +53,13 @@ func initFixture(tb testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, distrtypes.StoreKey, stakingtypes.StoreKey, types.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}, gov.AppModuleBasic{}).Codec logger := log.NewTestLogger(tb) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -86,6 +89,7 @@ func initFixture(tb testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/slashing/keeper/keeper_test.go b/tests/integration/slashing/keeper/keeper_test.go index fc296c86d370..a38df6f49049 100644 --- a/tests/integration/slashing/keeper/keeper_test.go +++ b/tests/integration/slashing/keeper/keeper_test.go @@ -55,10 +55,11 @@ func initFixture(tb testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, slashingtypes.StoreKey, stakingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}).Codec logger := log.NewTestLogger(tb) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -86,6 +87,7 @@ func initFixture(tb testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/staking/keeper/common_test.go b/tests/integration/staking/keeper/common_test.go index 7ed49482d348..100d613836d5 100644 --- a/tests/integration/staking/keeper/common_test.go +++ b/tests/integration/staking/keeper/common_test.go @@ -101,10 +101,13 @@ func initFixture(tb testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, types.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys( + banktypes.ObjectStoreKey, + ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, staking.AppModuleBasic{}).Codec logger := log.NewTestLogger(tb) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtprototypes.Header{}, true, logger) @@ -133,6 +136,7 @@ func initFixture(tb testing.TB) *fixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/tests/integration/staking/keeper/determinstic_test.go b/tests/integration/staking/keeper/determinstic_test.go index 170c51535d56..a0012b14a05b 100644 --- a/tests/integration/staking/keeper/determinstic_test.go +++ b/tests/integration/staking/keeper/determinstic_test.go @@ -70,10 +70,11 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, ) + okeys := storetypes.NewObjectStoreKeys(banktypes.ObjectStoreKey) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, distribution.AppModuleBasic{}).Codec logger := log.NewTestLogger(t) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, okeys, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) @@ -102,6 +103,7 @@ func initDeterministicFixture(t *testing.T) *deterministicFixture { bankKeeper := bankkeeper.NewBaseKeeper( cdc, runtime.NewKVStoreService(keys[banktypes.StoreKey]), + okeys[banktypes.ObjectStoreKey], accountKeeper, blockedAddresses, authority.String(), diff --git a/testutil/context.go b/testutil/context.go index 288f7fc55bcd..ffa2e426c9b6 100644 --- a/testutil/context.go +++ b/testutil/context.go @@ -80,3 +80,17 @@ func DefaultContextWithDB(tb testing.TB, key, tkey storetypes.StoreKey) TestCont return TestContext{ctx, db, cms} } + +func DefaultContextWithObjectStore(t testing.TB, key, tkey, okey storetypes.StoreKey) TestContext { + db := dbm.NewMemDB() + cms := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + cms.MountStoreWithDB(key, storetypes.StoreTypeIAVL, db) + cms.MountStoreWithDB(tkey, storetypes.StoreTypeTransient, nil) + cms.MountStoreWithDB(okey, storetypes.StoreTypeObject, nil) + err := cms.LoadLatestVersion() + assert.NoError(t, err) + + ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Now()}, false, log.NewNopLogger()) + + return TestContext{ctx, db, cms} +} diff --git a/testutil/integration/example_test.go b/testutil/integration/example_test.go index b395324c1917..698bcbd5d4c4 100644 --- a/testutil/integration/example_test.go +++ b/testutil/integration/example_test.go @@ -37,7 +37,7 @@ func Example() { // replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t)) logger := log.NewNopLogger() - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, nil, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) accountKeeper := authkeeper.NewAccountKeeper( @@ -126,7 +126,7 @@ func Example_oneModule() { // replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t)) logger := log.NewLogger(io.Discard) - cms := integration.CreateMultiStore(keys, logger) + cms := integration.CreateMultiStore(keys, nil, logger) newCtx := sdk.NewContext(cms, cmtproto.Header{}, true, logger) accountKeeper := authkeeper.NewAccountKeeper( diff --git a/testutil/integration/router.go b/testutil/integration/router.go index cae9a308e3ac..01cbd0dac844 100644 --- a/testutil/integration/router.go +++ b/testutil/integration/router.go @@ -181,7 +181,7 @@ func (app *App) QueryHelper() *baseapp.QueryServiceTestHelper { } // CreateMultiStore is a helper for setting up multiple stores for provided modules. -func CreateMultiStore(keys map[string]*storetypes.KVStoreKey, logger log.Logger) storetypes.CommitMultiStore { +func CreateMultiStore(keys map[string]*storetypes.KVStoreKey, okeys map[string]*storetypes.ObjectStoreKey, logger log.Logger) storetypes.CommitMultiStore { db := dbm.NewMemDB() cms := store.NewCommitMultiStore(db, logger, metrics.NewNoOpMetrics()) @@ -189,6 +189,10 @@ func CreateMultiStore(keys map[string]*storetypes.KVStoreKey, logger log.Logger) cms.MountStoreWithDB(keys[key], storetypes.StoreTypeIAVL, db) } + for key := range okeys { + cms.MountStoreWithDB(okeys[key], storetypes.StoreTypeObject, nil) + } + _ = cms.LoadLatestVersion() return cms } diff --git a/x/bank/keeper/collections_test.go b/x/bank/keeper/collections_test.go index 97b5e3e8bca0..7c037ff9fb24 100644 --- a/x/bank/keeper/collections_test.go +++ b/x/bank/keeper/collections_test.go @@ -26,7 +26,8 @@ import ( func TestBankStateCompatibility(t *testing.T) { key := storetypes.NewKVStoreKey(banktypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(t, key, storetypes.NewTransientStoreKey("transient_test")) + okey := storetypes.NewObjectStoreKey(banktypes.ObjectStoreKey) + testCtx := testutil.DefaultContextWithObjectStore(t, key, storetypes.NewTransientStoreKey("transient_test"), okey) ctx := testCtx.Ctx.WithBlockHeader(cmtproto.Header{Time: cmttime.Now()}) encCfg := moduletestutil.MakeTestEncodingConfig() @@ -40,6 +41,7 @@ func TestBankStateCompatibility(t *testing.T) { k := keeper.NewBaseKeeper( encCfg.Codec, storeService, + okey, authKeeper, map[string]bool{accAddrs[4].String(): true}, authtypes.NewModuleAddress("gov").String(), diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index 6883e60aa509..e9ca60c62d87 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -8,6 +8,7 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -46,9 +47,14 @@ type Keeper interface { MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromAccountToModuleVirtual(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToAccountVirtual(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + DelegateCoins(ctx context.Context, delegatorAddr, moduleAccAddr sdk.AccAddress, amt sdk.Coins) error UndelegateCoins(ctx context.Context, moduleAccAddr, delegatorAddr sdk.AccAddress, amt sdk.Coins) error + CreditVirtualAccounts(ctx context.Context) error + types.QueryServer } @@ -84,6 +90,7 @@ func (k BaseKeeper) GetPaginatedTotalSupply(ctx context.Context, pagination *que func NewBaseKeeper( cdc codec.BinaryCodec, storeService store.KVStoreService, + objStoreKey storetypes.StoreKey, ak types.AccountKeeper, blockedAddrs map[string]bool, authority string, @@ -97,7 +104,7 @@ func NewBaseKeeper( logger = logger.With(log.ModuleKey, "x/"+types.ModuleName) return BaseKeeper{ - BaseSendKeeper: NewBaseSendKeeper(cdc, storeService, ak, blockedAddrs, authority, logger), + BaseSendKeeper: NewBaseSendKeeper(cdc, storeService, objStoreKey, ak, blockedAddrs, authority, logger), ak: ak, cdc: cdc, storeService: storeService, diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go index 0952c34d8a06..0dde9dd3578d 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -130,7 +130,8 @@ func TestKeeperTestSuite(t *testing.T) { func (suite *KeeperTestSuite) SetupTest() { key := storetypes.NewKVStoreKey(banktypes.StoreKey) - testCtx := testutil.DefaultContextWithDB(suite.T(), key, storetypes.NewTransientStoreKey("transient_test")) + okey := storetypes.NewObjectStoreKey(banktypes.ObjectStoreKey) + testCtx := testutil.DefaultContextWithObjectStore(suite.T(), key, storetypes.NewTransientStoreKey("transient_test"), okey) ctx := testCtx.Ctx.WithBlockHeader(cmtproto.Header{Time: cmttime.Now()}) encCfg := moduletestutil.MakeTestEncodingConfig() @@ -145,6 +146,7 @@ func (suite *KeeperTestSuite) SetupTest() { suite.bankKeeper = keeper.NewBaseKeeper( encCfg.Codec, storeService, + okey, suite.authKeeper, map[string]bool{accAddrs[4].String(): true}, authtypes.NewModuleAddress(govtypes.ModuleName).String(), @@ -196,6 +198,16 @@ func (suite *KeeperTestSuite) mockSendCoinsFromAccountToModule(acc *authtypes.Ba suite.authKeeper.EXPECT().HasAccount(suite.ctx, moduleAcc.GetAddress()).Return(true) } +func (suite *KeeperTestSuite) mockSendCoinsFromAccountToModuleVirtual(acc *authtypes.BaseAccount, moduleAcc *authtypes.ModuleAccount) { + suite.authKeeper.EXPECT().GetModuleAccount(suite.ctx, moduleAcc.Name).Return(moduleAcc) + suite.authKeeper.EXPECT().GetAccount(suite.ctx, acc.GetAddress()).Return(acc) +} + +func (suite *KeeperTestSuite) mockSendCoinsFromModuleToAccountVirtual(moduleAcc *authtypes.ModuleAccount, accAddr sdk.AccAddress) { + suite.authKeeper.EXPECT().GetModuleAddress(moduleAcc.Name).Return(moduleAcc.GetAddress()) + suite.authKeeper.EXPECT().HasAccount(suite.ctx, accAddr).Return(true) +} + func (suite *KeeperTestSuite) mockSendCoins(ctx context.Context, sender sdk.AccountI, receiver sdk.AccAddress) { suite.authKeeper.EXPECT().GetAccount(ctx, sender.GetAddress()).Return(sender) suite.authKeeper.EXPECT().HasAccount(ctx, receiver).Return(true) @@ -316,6 +328,7 @@ func (suite *KeeperTestSuite) TestGetAuthority() { return keeper.NewBaseKeeper( moduletestutil.MakeTestEncodingConfig().Codec, storeService, + nil, suite.authKeeper, nil, authority, @@ -632,6 +645,38 @@ func (suite *KeeperTestSuite) TestSendCoinsNewAccount() { require.Equal(acc1Balances, updatedAcc1Bal) } +func (suite *KeeperTestSuite) TestSendCoinsVirtual() { + ctx := suite.ctx + require := suite.Require() + keeper := suite.bankKeeper + sdkCtx := sdk.UnwrapSDKContext(ctx) + acc0 := authtypes.NewBaseAccountWithAddress(accAddrs[0]) + feeDenom1 := "fee1" + feeDenom2 := "fee2" + + balances := sdk.NewCoins(sdk.NewInt64Coin(feeDenom1, 100), sdk.NewInt64Coin(feeDenom2, 100)) + suite.mockFundAccount(accAddrs[0]) + require.NoError(banktestutil.FundAccount(ctx, suite.bankKeeper, accAddrs[0], balances)) + + sendAmt := sdk.NewCoins(sdk.NewInt64Coin(feeDenom1, 50), sdk.NewInt64Coin(feeDenom2, 50)) + suite.mockSendCoinsFromAccountToModuleVirtual(acc0, burnerAcc) + require.NoError( + keeper.SendCoinsFromAccountToModuleVirtual(sdkCtx, accAddrs[0], authtypes.Burner, sendAmt), + ) + + refundAmt := sdk.NewCoins(sdk.NewInt64Coin(feeDenom1, 25), sdk.NewInt64Coin(feeDenom2, 25)) + suite.mockSendCoinsFromModuleToAccountVirtual(burnerAcc, accAddrs[0]) + require.NoError( + keeper.SendCoinsFromModuleToAccountVirtual(sdkCtx, authtypes.Burner, accAddrs[0], refundAmt), + ) + + suite.authKeeper.EXPECT().HasAccount(suite.ctx, burnerAcc.GetAddress()).Return(true) + require.NoError(keeper.CreditVirtualAccounts(ctx)) + + require.Equal(math.NewInt(25), keeper.GetBalance(suite.ctx, burnerAcc.GetAddress(), feeDenom1).Amount) + require.Equal(math.NewInt(25), keeper.GetBalance(suite.ctx, burnerAcc.GetAddress(), feeDenom2).Amount) +} + func (suite *KeeperTestSuite) TestInputOutputNewAccount() { ctx := suite.ctx require := suite.Require() diff --git a/x/bank/keeper/send.go b/x/bank/keeper/send.go index 96fa6f2a089b..e96e4a69538e 100644 --- a/x/bank/keeper/send.go +++ b/x/bank/keeper/send.go @@ -9,6 +9,7 @@ import ( errorsmod "cosmossdk.io/errors" "cosmossdk.io/log" "cosmossdk.io/math" + storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/telemetry" @@ -60,6 +61,7 @@ type BaseSendKeeper struct { ak types.AccountKeeper storeService store.KVStoreService logger log.Logger + objStoreKey storetypes.StoreKey // list of addresses that are restricted from receiving transactions blockedAddrs map[string]bool @@ -74,6 +76,7 @@ type BaseSendKeeper struct { func NewBaseSendKeeper( cdc codec.BinaryCodec, storeService store.KVStoreService, + objStoreKey storetypes.StoreKey, ak types.AccountKeeper, blockedAddrs map[string]bool, authority string, @@ -88,6 +91,7 @@ func NewBaseSendKeeper( cdc: cdc, ak: ak, storeService: storeService, + objStoreKey: objStoreKey, blockedAddrs: blockedAddrs, authority: authority, logger: logger, @@ -240,6 +244,12 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA return err } + k.ensureAccountCreated(ctx, toAddr) + k.emitSendCoinsEvents(ctx, fromAddr, toAddr, amt) + return nil +} + +func (k BaseSendKeeper) ensureAccountCreated(ctx context.Context, toAddr sdk.AccAddress) { // Create account if recipient does not exist. // // NOTE: This should ultimately be removed in favor a more flexible approach @@ -249,7 +259,10 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA defer telemetry.IncrCounter(1, "new", "account") k.ak.SetAccount(ctx, k.ak.NewAccountWithAddress(ctx, toAddr)) } +} +// emitSendCoinsEvents emit send coins events. +func (k BaseSendKeeper) emitSendCoinsEvents(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) { // bech32 encoding is expensive! Only do it once for fromAddr fromAddrString := fromAddr.String() sdkCtx := sdk.UnwrapSDKContext(ctx) @@ -265,8 +278,6 @@ func (k BaseSendKeeper) SendCoins(ctx context.Context, fromAddr, toAddr sdk.AccA sdk.NewAttribute(types.AttributeKeySender, fromAddrString), ), }) - - return nil } // subUnlockedCoins removes the unlocked amt coins of the given account. diff --git a/x/bank/keeper/virtual.go b/x/bank/keeper/virtual.go new file mode 100644 index 000000000000..f7288e7e0b52 --- /dev/null +++ b/x/bank/keeper/virtual.go @@ -0,0 +1,178 @@ +package keeper + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "fmt" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// SendCoinsFromAccountToModuleVirtual sends coins from account to a virtual module account. +func (k BaseSendKeeper) SendCoinsFromAccountToModuleVirtual( + ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins, +) error { + recipientAcc := k.ak.GetModuleAccount(ctx, recipientModule) + if recipientAcc == nil { + panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", recipientModule)) + } + + return k.SendCoinsToVirtual(ctx, senderAddr, recipientAcc.GetAddress(), amt) +} + +// SendCoinsFromModuleToAccountVirtual sends coins from account to a virtual module account. +func (k BaseSendKeeper) SendCoinsFromModuleToAccountVirtual( + ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, +) error { + senderAddr := k.ak.GetModuleAddress(senderModule) + if senderAddr == nil { + panic(errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", senderModule)) + } + + if k.BlockedAddr(recipientAddr) { + return errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", recipientAddr) + } + + return k.SendCoinsFromVirtual(ctx, senderAddr, recipientAddr, amt) +} + +// SendCoinsToVirtual accumulate the recipient's coins in a per-transaction transient state, +// which are sumed up and added to the real account at the end of block. +// Events are emiited the same as normal send. +func (k BaseSendKeeper) SendCoinsToVirtual(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + var err error + err = k.subUnlockedCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + toAddr, err = k.sendRestriction.apply(ctx, fromAddr, toAddr, amt) + if err != nil { + return err + } + + k.addVirtualCoins(ctx, toAddr, amt) + k.emitSendCoinsEvents(ctx, fromAddr, toAddr, amt) + return nil +} + +// SendCoinsFromVirtual deduct coins from virtual from account and send to recipient account. +func (k BaseSendKeeper) SendCoinsFromVirtual(ctx context.Context, fromAddr, toAddr sdk.AccAddress, amt sdk.Coins) error { + var err error + err = k.subVirtualCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + toAddr, err = k.sendRestriction.apply(ctx, fromAddr, toAddr, amt) + if err != nil { + return err + } + + err = k.addCoins(ctx, toAddr, amt) + if err != nil { + return err + } + + k.ensureAccountCreated(ctx, toAddr) + k.emitSendCoinsEvents(ctx, fromAddr, toAddr, amt) + return nil +} + +func (k BaseSendKeeper) addVirtualCoins(ctx context.Context, addr sdk.AccAddress, amt sdk.Coins) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + store := sdkCtx.ObjectStore(k.objStoreKey) + + key := make([]byte, len(addr)+8) + copy(key, addr) + binary.BigEndian.PutUint64(key[len(addr):], uint64(sdkCtx.TxIndex())) + + var coins sdk.Coins + value := store.Get(key) + if value != nil { + coins = value.(sdk.Coins) + } + coins = coins.Add(amt...) + store.Set(key, coins) +} + +func (k BaseSendKeeper) subVirtualCoins(ctx context.Context, addr sdk.AccAddress, amt sdk.Coins) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + store := sdkCtx.ObjectStore(k.objStoreKey) + + key := make([]byte, len(addr)+8) + copy(key, addr) + binary.BigEndian.PutUint64(key[len(addr):], uint64(sdkCtx.TxIndex())) + + value := store.Get(key) + if value == nil { + return errorsmod.Wrapf( + sdkerrors.ErrInsufficientFunds, + "spendable balance 0 is smaller than %s", + amt, + ) + } + spendable := value.(sdk.Coins) + balance, hasNeg := spendable.SafeSub(amt...) + if hasNeg { + return errorsmod.Wrapf( + sdkerrors.ErrInsufficientFunds, + "spendable balance %s is smaller than %s", + spendable, amt, + ) + } + if balance.IsZero() { + store.Delete(key) + } else { + store.Set(key, balance) + } + + return nil +} + +// CreditVirtualAccounts sum up the transient coins and add them to the real account, +// should be called at end blocker. +func (k BaseSendKeeper) CreditVirtualAccounts(ctx context.Context) error { + store := sdk.UnwrapSDKContext(ctx).ObjectStore(k.objStoreKey) + + var toAddr sdk.AccAddress + sum := sdk.NewMapCoins(nil) + flushCurrentAddr := func() error { + if len(sum) == 0 { + // nothing to flush + return nil + } + + if err := k.addCoins(ctx, toAddr, sum.ToCoins()); err != nil { + return err + } + clear(sum) + + k.ensureAccountCreated(ctx, toAddr) + return nil + } + + it := store.Iterator(nil, nil) + defer it.Close() + for ; it.Valid(); it.Next() { + if len(it.Key()) <= 8 { + return fmt.Errorf("unexpected key length: %s", hex.EncodeToString(it.Key())) + } + + addr := it.Key()[:len(it.Key())-8] + if !bytes.Equal(toAddr, addr) { + if err := flushCurrentAddr(); err != nil { + return err + } + toAddr = addr + } + + sum.Add(it.Value().(sdk.Coins)...) + } + + return flushCurrentAddr() +} diff --git a/x/bank/module.go b/x/bank/module.go index e7140af204a9..0d7dd5feba9d 100644 --- a/x/bank/module.go +++ b/x/bank/module.go @@ -17,6 +17,7 @@ import ( corestore "cosmossdk.io/core/store" "cosmossdk.io/depinject" "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" @@ -44,7 +45,8 @@ var ( _ module.HasGenesis = AppModule{} _ module.HasServices = AppModule{} - _ appmodule.AppModule = AppModule{} + _ appmodule.AppModule = AppModule{} + _ appmodule.HasEndBlocker = AppModule{} ) // AppModuleBasic defines the basic application module used by the bank module. @@ -202,6 +204,10 @@ func (am AppModule) WeightedOperationsX(weights simsx.WeightSource, reg simsx.Re reg.Add(weights.Get("msg_multisend", 10), simulation.MsgMultiSendFactory()) } +func (am AppModule) EndBlock(ctx context.Context) error { + return am.keeper.CreditVirtualAccounts(ctx) +} + // App Wiring Setup func init() { @@ -219,6 +225,7 @@ type ModuleInputs struct { Cdc codec.Codec StoreService corestore.KVStoreService Logger log.Logger + ObjStoreKey *storetypes.ObjectStoreKey AccountKeeper types.AccountKeeper @@ -258,6 +265,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs { bankKeeper := keeper.NewBaseKeeper( in.Cdc, in.StoreService, + in.ObjStoreKey, in.AccountKeeper, blockedAddresses, authority.String(), diff --git a/x/bank/types/keys.go b/x/bank/types/keys.go index b4ea683d4b69..485d1cdb9ef0 100644 --- a/x/bank/types/keys.go +++ b/x/bank/types/keys.go @@ -17,6 +17,9 @@ const ( // RouterKey defines the module's message routing key RouterKey = ModuleName + + // ObjectStoreKey defines the store name for the object store + ObjectStoreKey = "object:" + ModuleName ) // KVStore keys diff --git a/x/gov/testutil/expected_keepers_mocks.go b/x/gov/testutil/expected_keepers_mocks.go index 64a678b21741..416e85ba8530 100644 --- a/x/gov/testutil/expected_keepers_mocks.go +++ b/x/gov/testutil/expected_keepers_mocks.go @@ -791,6 +791,22 @@ func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx context.Context, sende return ret0 } +// SendCoinsFromAccountToModuleVirtual mocks base method. +func (m *MockBankKeeper) SendCoinsFromAccountToModuleVirtual(ctx context.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromAccountToModuleVirtual", ctx, senderAddr, recipientModule, amt) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendCoinsFromModuleToAccountVirtual mocks base method. +func (m *MockBankKeeper) SendCoinsFromModuleToAccountVirtual(ctx context.Context, senderModule string, recipientAddr types.AccAddress, amt types.Coins) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoinsFromModuleToAccountVirtual", ctx, senderModule, recipientAddr, amt) + ret0, _ := ret[0].(error) + return ret0 +} + // SendCoinsFromAccountToModule indicates an expected call of SendCoinsFromAccountToModule. func (mr *MockBankKeeperMockRecorder) SendCoinsFromAccountToModule(ctx, senderAddr, recipientModule, amt any) *gomock.Call { mr.mock.ctrl.T.Helper() @@ -819,6 +835,13 @@ func (m *MockBankKeeper) SendCoinsFromModuleToModule(ctx context.Context, sender return ret0 } +func (m *MockBankKeeper) CreditVirtualAccounts(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreditVirtualAccounts", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + // SendCoinsFromModuleToModule indicates an expected call of SendCoinsFromModuleToModule. func (mr *MockBankKeeperMockRecorder) SendCoinsFromModuleToModule(ctx, senderModule, recipientModule, amt any) *gomock.Call { mr.mock.ctrl.T.Helper() From db74a3d1c7f63728a5d025573fb0f4abe6e7af98 Mon Sep 17 00:00:00 2001 From: yihuang Date: Fri, 29 Mar 2024 15:59:45 +0800 Subject: [PATCH 07/13] Problem: MultiStore interface is bloated (#240) * Problem: MultiStore interface is bloated Solution: - Split out specialied methods from it, keeping the MultiStore generic * Update store/CHANGELOG.md Signed-off-by: yihuang --------- Signed-off-by: yihuang --- baseapp/abci.go | 2 +- baseapp/baseapp.go | 2 +- baseapp/options.go | 2 +- server/mock/store.go | 2 +- store/CHANGELOG.md | 1 + store/cachemulti/store.go | 14 -------------- store/types/store.go | 14 +++++++++----- 7 files changed, 14 insertions(+), 23 deletions(-) diff --git a/baseapp/abci.go b/baseapp/abci.go index 306f34bbf84d..9ed9bcd9e0b3 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -1238,7 +1238,7 @@ func (app *BaseApp) CreateQueryContextWithCheckHeader(height int64, prove, check // use custom query multi-store if provided qms := app.qms if qms == nil { - qms = app.cms.(storetypes.MultiStore) + qms = storetypes.RootMultiStore(app.cms) } lastBlockHeight := qms.LatestVersion() diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 1cab6c2dc509..2ab24219402d 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -68,7 +68,7 @@ type BaseApp struct { name string // application name from abci.BlockInfo db dbm.DB // common DB backend cms storetypes.CommitMultiStore // Main (uncached) state - qms storetypes.MultiStore // Optional alternative multistore for querying only. + qms storetypes.RootMultiStore // Optional alternative multistore for querying only. storeLoader StoreLoader // function to handle store loading, may be overridden with SetStoreLoader() grpcQueryRouter *GRPCQueryRouter // router for redirecting gRPC query calls msgServiceRouter *MsgServiceRouter // router for redirecting Msg service messages diff --git a/baseapp/options.go b/baseapp/options.go index ec32a241401f..096d860d8337 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -326,7 +326,7 @@ func (app *BaseApp) SetTxEncoder(txEncoder sdk.TxEncoder) { // SetQueryMultiStore set a alternative MultiStore implementation to support grpc query service. // // Ref: https://github.com/cosmos/cosmos-sdk/issues/13317 -func (app *BaseApp) SetQueryMultiStore(ms storetypes.MultiStore) { +func (app *BaseApp) SetQueryMultiStore(ms storetypes.RootMultiStore) { app.qms = ms } diff --git a/server/mock/store.go b/server/mock/store.go index 4bd897fc681a..83d059d84e58 100644 --- a/server/mock/store.go +++ b/server/mock/store.go @@ -12,7 +12,7 @@ import ( storetypes "cosmossdk.io/store/types" ) -var _ storetypes.MultiStore = multiStore{} +var _ storetypes.CommitMultiStore = multiStore{} type multiStore struct { kv map[storetypes.StoreKey]kvStore diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 8f7b99d786c4..1c1b855cf3ea 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -29,6 +29,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#207](https://github.com/crypto-org-chain/cosmos-sdk/pull/207) Remove api CacheWrapWithTrace. * [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Support object store. +* [#240](https://github.com/crypto-org-chain/cosmos-sdk/pull/240) Split methods from `MultiStore` into specialized `RootMultiStore`, keep `MultiStore` generic. ## v1.1.2 (March 31, 2025) diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index 67ccec653b24..42e3618b14c6 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -110,11 +110,6 @@ func (cms Store) TracingEnabled() bool { return cms.traceWriter != nil } -// LatestVersion returns the branch version of the store -func (cms Store) LatestVersion() int64 { - panic("cannot get latest version from branch cached multi-store") -} - // GetStoreType returns the type of the store. func (cms Store) GetStoreType() types.StoreType { return types.StoreTypeMulti @@ -138,15 +133,6 @@ func (cms Store) CacheMultiStore() types.CacheMultiStore { return newCacheMultiStoreFromCMS(cms) } -// CacheMultiStoreWithVersion implements the MultiStore interface. It will panic -// as an already cached multi-store cannot load previous versions. -// -// TODO: The store implementation can possibly be modified to support this as it -// seems safe to load previous versions (heights). -func (cms Store) CacheMultiStoreWithVersion(_ int64) (types.CacheMultiStore, error) { - panic("cannot branch cached multi-store with a version") -} - // GetStore returns an underlying Store by key. func (cms Store) GetStore(key types.StoreKey) types.Store { s := cms.stores[key] diff --git a/store/types/store.go b/store/types/store.go index 59e8a140fece..79d5776a870f 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -120,10 +120,6 @@ type MultiStore interface { // call CacheMultiStore.Write(). CacheMultiStore() CacheMultiStore - // CacheMultiStoreWithVersion branches the underlying MultiStore where - // each stored is loaded at a specific version (height). - CacheMultiStoreWithVersion(version int64) (CacheMultiStore, error) - // Convenience for fetching substores. // If the store does not exist, panics. GetStore(StoreKey) Store @@ -142,6 +138,14 @@ type MultiStore interface { // implied that the caller should update the context when necessary between // tracing operations. The modified MultiStore is returned. SetTracingContext(TraceContext) MultiStore +} + +type RootMultiStore interface { + MultiStore + + // CacheMultiStoreWithVersion branches the underlying MultiStore where + // each stored is loaded at a specific version (height). + CacheMultiStoreWithVersion(version int64) (CacheMultiStore, error) // LatestVersion returns the latest version in the store LatestVersion() int64 @@ -156,7 +160,7 @@ type CacheMultiStore interface { // CommitMultiStore is an interface for a MultiStore without cache capabilities. type CommitMultiStore interface { Committer - MultiStore + RootMultiStore snapshottypes.Snapshotter // Mount a store of type using the given db. From 0e327bb620dc85648d8a8f25acadabd00ddb1382 Mon Sep 17 00:00:00 2001 From: yihuang Date: Mon, 1 Apr 2024 11:07:18 +0800 Subject: [PATCH 08/13] Problem: many memory allocs in cache store wrapping (#242) Solution: - init cachestore on cachestore lazily. - cleanup some unused stuff. Update store/CHANGELOG.md Signed-off-by: yihuang --- store/CHANGELOG.md | 3 ++ store/cachemulti/store.go | 75 ++++++++++++++++++++------------------- store/rootmulti/store.go | 4 +-- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 1c1b855cf3ea..4d4f4bfd2e56 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -30,6 +30,9 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#207](https://github.com/crypto-org-chain/cosmos-sdk/pull/207) Remove api CacheWrapWithTrace. * [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Support object store. * [#240](https://github.com/crypto-org-chain/cosmos-sdk/pull/240) Split methods from `MultiStore` into specialized `RootMultiStore`, keep `MultiStore` generic. +* [#241](https://github.com/crypto-org-chain/cosmos-sdk/pull/241) Refactor the cache store to be btree backed, prepare to support copy-on-write atomic branching. +* [#242](https://github.com/crypto-org-chain/cosmos-sdk/pull/242) Init cache on cache lazily, save memory allocations. + ## v1.1.2 (March 31, 2025) diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index 42e3618b14c6..ac40519ad9ec 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -5,9 +5,6 @@ import ( "io" "maps" - dbm "github.com/cosmos/cosmos-db" - - "cosmossdk.io/store/dbadapter" "cosmossdk.io/store/tracekv" "cosmossdk.io/store/types" ) @@ -24,12 +21,11 @@ const storeNameCtxKey = "store_name" // NOTE: a Store (and MultiStores in general) should never expose the // keys for the substores. type Store struct { - db types.CacheWrap stores map[types.StoreKey]types.CacheWrap - keys map[string]types.StoreKey traceWriter io.Writer traceContext types.TraceContext + parentStore func(types.StoreKey) types.CacheWrap } var _ types.CacheMultiStore = Store{} @@ -38,29 +34,17 @@ var _ types.CacheMultiStore = Store{} // CacheWrapper objects and a KVStore as the database. Each CacheWrapper store // is a branched store. func NewFromKVStore( - store types.CacheWrapper, stores map[types.StoreKey]types.CacheWrapper, - keys map[string]types.StoreKey, traceWriter io.Writer, traceContext types.TraceContext, + stores map[types.StoreKey]types.CacheWrapper, + traceWriter io.Writer, traceContext types.TraceContext, ) Store { cms := Store{ - db: store.CacheWrap(), stores: make(map[types.StoreKey]types.CacheWrap, len(stores)), - keys: keys, traceWriter: traceWriter, traceContext: traceContext, } for key, store := range stores { - if cms.TracingEnabled() { - // only support tracing on KVStore. - if kvstore, ok := store.(types.KVStore); ok { - tctx := cms.traceContext.Clone().Merge(types.TraceContext{ - storeNameCtxKey: key.Name(), - }) - - store = tracekv.NewStore(kvstore, cms.traceWriter, tctx) - } - } - cms.stores[key] = store.CacheWrap() + cms.initStore(key, store) } return cms @@ -69,19 +53,35 @@ func NewFromKVStore( // NewStore creates a new Store object from a mapping of store keys to // CacheWrapper objects. Each CacheWrapper store is a branched store. func NewStore( - db dbm.DB, stores map[types.StoreKey]types.CacheWrapper, keys map[string]types.StoreKey, + stores map[types.StoreKey]types.CacheWrapper, traceWriter io.Writer, traceContext types.TraceContext, ) Store { - return NewFromKVStore(dbadapter.Store{DB: db}, stores, keys, traceWriter, traceContext) + return NewFromKVStore(stores, traceWriter, traceContext) } func newCacheMultiStoreFromCMS(cms Store) Store { - stores := make(map[types.StoreKey]types.CacheWrapper) - for k, v := range cms.stores { - stores[k] = v + return Store{ + stores: make(map[types.StoreKey]types.CacheWrap), + traceWriter: cms.traceWriter, + traceContext: cms.traceContext, + parentStore: cms.getCacheWrap, } +} + +func (cms Store) initStore(key types.StoreKey, store types.CacheWrapper) types.CacheWrap { + if cms.TracingEnabled() { + // only support tracing on KVStore. + if kvstore, ok := store.(types.KVStore); ok { + tctx := cms.traceContext.Clone().Merge(types.TraceContext{ + storeNameCtxKey: key.Name(), + }) - return NewFromKVStore(cms.db, stores, nil, cms.traceWriter, cms.traceContext) + store = tracekv.NewStore(kvstore, cms.traceWriter, tctx) + } + } + cache := store.CacheWrap() + cms.stores[key] = cache + return cache } // SetTracer sets the tracer for the MultiStore that the underlying @@ -117,7 +117,6 @@ func (cms Store) GetStoreType() types.StoreType { // Write calls Write on each underlying store. func (cms Store) Write() { - cms.db.Write() for _, store := range cms.stores { store.Write() } @@ -133,19 +132,23 @@ func (cms Store) CacheMultiStore() types.CacheMultiStore { return newCacheMultiStoreFromCMS(cms) } -// GetStore returns an underlying Store by key. -func (cms Store) GetStore(key types.StoreKey) types.Store { - s := cms.stores[key] - if key == nil || s == nil { +func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { + store, ok := cms.stores[key] + if !ok && cms.parentStore != nil { + // load on demand + store = cms.initStore(key, cms.parentStore(key)) + } + if key == nil || store == nil { panic(fmt.Sprintf("kv store with key %v has not been registered in stores", key)) } - return s.(types.Store) + return store } -func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { - store := cms.stores[key] - if key == nil || store == nil { - panic(fmt.Sprintf("kv store with key %v has not been registered in stores", key)) +// GetStore returns an underlying Store by key. +func (cms Store) GetStore(key types.StoreKey) types.Store { + store, ok := cms.getCacheWrap(key).(types.Store) + if !ok { + panic(fmt.Sprintf("store with key %v is not Store", key)) } return store } diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index ceb1a4fcd0aa..26461c714499 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -572,7 +572,7 @@ func (rs *Store) CacheMultiStore() types.CacheMultiStore { } stores[k] = store } - return cachemulti.NewStore(rs.db, stores, rs.keysByName, rs.traceWriter, rs.getTracingContext()) + return cachemulti.NewStore(stores, rs.traceWriter, rs.getTracingContext()) } // CacheMultiStoreWithVersion is analogous to CacheMultiStore except that it @@ -634,7 +634,7 @@ func (rs *Store) CacheMultiStoreWithVersion(version int64) (types.CacheMultiStor cachedStores[key] = cacheStore } - return cachemulti.NewStore(rs.db, cachedStores, rs.keysByName, rs.traceWriter, rs.getTracingContext()), nil + return cachemulti.NewStore(cachedStores, rs.traceWriter, rs.getTracingContext()), nil } // GetStore returns a mounted Store for a given StoreKey. If the StoreKey does From 7797ce57c7b072292c2e9e25707771577ce6b6f1 Mon Sep 17 00:00:00 2001 From: yihuang Date: Sat, 30 Mar 2024 00:32:58 +0800 Subject: [PATCH 09/13] Problem: nested cache store not efficient (#241) * Problem: nested cache store not efficient Solution: - introduce copy-on-write btree based cache store temp * changelog * rename * Update store/cachekv/store.go Signed-off-by: yihuang --------- Signed-off-by: yihuang --- store/cachekv/search_benchmark_test.go | 44 --- store/cachekv/search_test.go | 139 ---------- store/cachekv/store.go | 368 ++++--------------------- store/internal/btree/btree.go | 25 +- store/internal/btree/btree_test.go | 23 +- store/internal/btreeadaptor.go | 8 +- store/types/store.go | 6 + 7 files changed, 99 insertions(+), 514 deletions(-) delete mode 100644 store/cachekv/search_benchmark_test.go delete mode 100644 store/cachekv/search_test.go diff --git a/store/cachekv/search_benchmark_test.go b/store/cachekv/search_benchmark_test.go deleted file mode 100644 index f0e29bc4ec18..000000000000 --- a/store/cachekv/search_benchmark_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package cachekv - -import ( - "strconv" - "testing" - - "cosmossdk.io/store/internal/btree" -) - -func BenchmarkLargeUnsortedMisses(b *testing.B) { - for i := 0; i < b.N; i++ { - b.StopTimer() - store := generateStore() - b.StartTimer() - - for k := 0; k < 10000; k++ { - // cache has A + Z values - // these are within range, but match nothing - store.dirtyItems([]byte("B1"), []byte("B2")) - } - } -} - -func generateStore() *Store { - cache := map[string]*cValue[[]byte]{} - unsorted := map[string]struct{}{} - for i := 0; i < 5000; i++ { - key := "A" + strconv.Itoa(i) - unsorted[key] = struct{}{} - cache[key] = &cValue[[]byte]{} - } - - for i := 0; i < 5000; i++ { - key := "Z" + strconv.Itoa(i) - unsorted[key] = struct{}{} - cache[key] = &cValue[[]byte]{} - } - - return &GStore[[]byte]{ - cache: cache, - unsortedCache: unsorted, - sortedCache: btree.NewBTree[[]byte](), - } -} diff --git a/store/cachekv/search_test.go b/store/cachekv/search_test.go deleted file mode 100644 index aedc0669030a..000000000000 --- a/store/cachekv/search_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package cachekv - -import "testing" - -func TestFindStartIndex(t *testing.T) { - tests := []struct { - name string - sortedL []string - query string - want int - }{ - { - name: "non-existent value", - sortedL: []string{"a", "b", "c", "d", "e", "l", "m", "n", "u", "v", "w", "x", "y", "z"}, - query: "o", - want: 8, - }, - { - name: "dupes start at index 0", - sortedL: []string{"a", "a", "a", "b", "c", "d", "e", "l", "m", "n", "u", "v", "w", "x", "y", "z"}, - query: "a", - want: 0, - }, - { - name: "dupes start at non-index 0", - sortedL: []string{"a", "c", "c", "c", "c", "d", "e", "l", "m", "n", "u", "v", "w", "x", "y", "z"}, - query: "c", - want: 1, - }, - { - name: "at end", - sortedL: []string{"a", "e", "u", "v", "w", "x", "y", "z"}, - query: "z", - want: 7, - }, - { - name: "dupes at end", - sortedL: []string{"a", "e", "u", "v", "w", "x", "y", "z", "z", "z", "z"}, - query: "z", - want: 7, - }, - { - name: "entirely dupes", - sortedL: []string{"z", "z", "z", "z", "z"}, - query: "z", - want: 0, - }, - { - name: "non-existent but within >=start", - sortedL: []string{"z", "z", "z", "z", "z"}, - query: "p", - want: 0, - }, - { - name: "non-existent and out of range", - sortedL: []string{"d", "e", "f", "g", "h"}, - query: "z", - want: -1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - body := tt.sortedL - got := findStartIndex(body, tt.query) - if got != tt.want { - t.Fatalf("Got: %d, want: %d", got, tt.want) - } - }) - } -} - -func TestFindEndIndex(t *testing.T) { - tests := []struct { - name string - sortedL []string - query string - want int - }{ - { - name: "non-existent value", - sortedL: []string{"a", "b", "c", "d", "e", "l", "m", "n", "u", "v", "w", "x", "y", "z"}, - query: "o", - want: 7, - }, - { - name: "dupes start at index 0", - sortedL: []string{"a", "a", "a", "b", "c", "d", "e", "l", "m", "n", "u", "v", "w", "x", "y", "z"}, - query: "a", - want: 0, - }, - { - name: "dupes start at non-index 0", - sortedL: []string{"a", "c", "c", "c", "c", "d", "e", "l", "m", "n", "u", "v", "w", "x", "y", "z"}, - query: "c", - want: 1, - }, - { - name: "at end", - sortedL: []string{"a", "e", "u", "v", "w", "x", "y", "z"}, - query: "z", - want: 7, - }, - { - name: "dupes at end", - sortedL: []string{"a", "e", "u", "v", "w", "x", "y", "z", "z", "z", "z"}, - query: "z", - want: 7, - }, - { - name: "entirely dupes", - sortedL: []string{"z", "z", "z", "z", "z"}, - query: "z", - want: 0, - }, - { - name: "non-existent and out of range", - sortedL: []string{"z", "z", "z", "z", "z"}, - query: "p", - want: -1, - }, - { - name: "non-existent and out of range", - sortedL: []string{"d", "e", "f", "g", "h"}, - query: "z", - want: 4, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - body := tt.sortedL - got := findEndIndex(body, tt.query) - if got != tt.want { - t.Fatalf("Got: %d, want: %d", got, tt.want) - } - }) - } -} diff --git a/store/cachekv/store.go b/store/cachekv/store.go index 7c7cf7513845..df47dfc3e6ae 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -1,34 +1,18 @@ package cachekv import ( - "bytes" - "sort" - "sync" - - dbm "github.com/cosmos/cosmos-db" - - "cosmossdk.io/math" "cosmossdk.io/store/cachekv/internal" "cosmossdk.io/store/internal/btree" - "cosmossdk.io/store/internal/conv" "cosmossdk.io/store/types" ) -// cValue represents a cached value. -// If dirty is true, it indicates the cached value is different from the underlying value. -type cValue[V any] struct { - value V - dirty bool -} - -type kvPair[V any] struct { - Key []byte - Value V -} - type Store = GStore[[]byte] -var _ types.CacheKVStore = (*Store)(nil) +var ( + _ types.CacheKVStore = (*Store)(nil) + _ types.CacheWrap = (*Store)(nil) + _ types.BranchStore = (*Store)(nil) +) func NewStore(parent types.KVStore) *Store { return NewGStore( @@ -40,11 +24,8 @@ func NewStore(parent types.KVStore) *Store { // GStore wraps an in-memory cache around an underlying types.KVStore. type GStore[V any] struct { - mtx sync.Mutex - cache map[string]*cValue[V] - unsortedCache map[string]struct{} - sortedCache btree.BTree[V] // always ascending sorted - parent types.GKVStore[V] + writeSet btree.BTree[V] // always ascending sorted + parent types.GKVStore[V] // isZero is a function that returns true if the value is considered "zero", for []byte and pointers the zero value // is `nil`, zero value is not allowed to set to a key, and it's returned if the key is not found. @@ -57,12 +38,10 @@ type GStore[V any] struct { // NewStore creates a new Store object func NewGStore[V any](parent types.GKVStore[V], isZero func(V) bool, valueLen func(V) int) *GStore[V] { return &GStore[V]{ - cache: make(map[string]*cValue[V]), - unsortedCache: make(map[string]struct{}), - sortedCache: btree.NewBTree[V](), - parent: parent, - isZero: isZero, - valueLen: valueLen, + writeSet: btree.NewBTree[V](), + parent: parent, + isZero: isZero, + valueLen: valueLen, } } @@ -71,21 +50,35 @@ func (store *GStore[V]) GetStoreType() types.StoreType { return store.parent.GetStoreType() } -// Get implements types.KVStore. -func (store *GStore[V]) Get(key []byte) (value V) { - store.mtx.Lock() - defer store.mtx.Unlock() +// Clone creates a copy-on-write snapshot of the cache store, +// it only performs a shallow copy so is very fast. +func (store *GStore[V]) Clone() types.BranchStore { + return &GStore[V]{ + writeSet: store.writeSet.Copy(), + parent: store.parent, + } +} + +// swapCache swap out the internal cache store and leave the current store unusable. +func (store *GStore[V]) swapCache() btree.BTree[V] { + cache := store.writeSet + store.writeSet = btree.BTree[V]{} + return cache +} +// Restore restores the store cache to a given snapshot, leaving the snapshot unusable. +func (store *GStore[V]) Restore(s types.BranchStore) { + store.writeSet = s.(*GStore[V]).swapCache() +} + +// Get implements types.KVStore. +func (store *GStore[V]) Get(key []byte) V { types.AssertValidKey(key) - cacheValue, ok := store.cache[conv.UnsafeBytesToStr(key)] - if !ok { - value = store.parent.Get(key) - store.setCacheValue(key, value, false) - } else { - value = cacheValue.value + value, found := store.writeSet.Get(key) + if !found { + return store.parent.Get(key) } - return value } @@ -94,93 +87,37 @@ func (store *GStore[V]) Set(key []byte, value V) { types.AssertValidKey(key) types.AssertValidValueGeneric(value, store.isZero, store.valueLen) - store.mtx.Lock() - defer store.mtx.Unlock() - store.setCacheValue(key, value, true) + store.writeSet.Set(key, value) } // Has implements types.KVStore. func (store *GStore[V]) Has(key []byte) bool { - value := store.Get(key) + types.AssertValidKey(key) + + value, found := store.writeSet.Get(key) + if !found { + return store.parent.Has(key) + } return !store.isZero(value) } // Delete implements types.KVStore. func (store *GStore[V]) Delete(key []byte) { types.AssertValidKey(key) - - store.mtx.Lock() - defer store.mtx.Unlock() - - store.setCacheValue(key, store.zeroValue, true) -} - -func (store *GStore[V]) resetCaches() { - if len(store.cache) > 100_000 { - // Cache is too large. We likely did something linear time - // (e.g. Epoch block, Genesis block, etc). Free the old caches from memory, and let them get re-allocated. - // TODO: In a future CacheKV redesign, such linear workloads should get into a different cache instantiation. - // 100_000 is arbitrarily chosen as it solved Osmosis' InitGenesis RAM problem. - store.cache = make(map[string]*cValue[V]) - store.unsortedCache = make(map[string]struct{}) - } else { - // Clear the cache using the map clearing idiom - // and not allocating fresh objects. - // Please see https://bencher.orijtech.com/perfclinic/mapclearing/ - for key := range store.cache { - delete(store.cache, key) - } - for key := range store.unsortedCache { - delete(store.unsortedCache, key) - } - } - store.sortedCache = btree.NewBTree[V]() + store.writeSet.Set(key, store.zeroValue) } // Implements Cachetypes.KVStore. func (store *GStore[V]) Write() { - store.mtx.Lock() - defer store.mtx.Unlock() - - if len(store.cache) == 0 && len(store.unsortedCache) == 0 { - store.sortedCache = btree.NewBTree[V]() - return - } - - type cEntry struct { - key string - val *cValue[V] - } - - // We need a copy of all of the keys. - // Not the best. To reduce RAM pressure, we copy the values as well - // and clear out the old caches right after the copy. - sortedCache := make([]cEntry, 0, len(store.cache)) - - for key, dbValue := range store.cache { - if dbValue.dirty { - sortedCache = append(sortedCache, cEntry{key, dbValue}) - } - } - store.resetCaches() - sort.Slice(sortedCache, func(i, j int) bool { - return sortedCache[i].key < sortedCache[j].key - }) - - // TODO: Consider allowing usage of Batch, which would allow the write to - // at least happen atomically. - for _, obj := range sortedCache { - // We use []byte(key) instead of conv.UnsafeStrToBytes because we cannot - // be sure if the underlying store might do a save with the byteslice or - // not. Once we get confirmation that .Delete is guaranteed not to - // save the byteslice, then we can assume only a read-only copy is sufficient. - if !store.isZero(obj.val.value) { - // It already exists in the parent, hence update it. - store.parent.Set([]byte(obj.key), obj.val.value) + store.writeSet.Scan(func(key []byte, value V) bool { + if store.isZero(value) { + store.parent.Delete(key) } else { - store.parent.Delete([]byte(obj.key)) + store.parent.Set(key, value) } - } + return true + }) + store.writeSet.Clear() } // CacheWrap implements CacheWrapper. @@ -202,11 +139,7 @@ func (store *GStore[V]) ReverseIterator(start, end []byte) types.GIterator[V] { } func (store *GStore[V]) iterator(start, end []byte, ascending bool) types.GIterator[V] { - store.mtx.Lock() - defer store.mtx.Unlock() - - store.dirtyItems(start, end) - isoSortedCache := store.sortedCache.Copy() + isoSortedCache := store.writeSet.Copy() var ( err error @@ -226,200 +159,3 @@ func (store *GStore[V]) iterator(start, end []byte, ascending bool) types.GItera return internal.NewCacheMergeIterator(parent, cache, ascending, store.isZero) } - -func findStartIndex(strL []string, startQ string) int { - // Modified binary search to find the very first element in >=startQ. - if len(strL) == 0 { - return -1 - } - - var left, right, mid int - right = len(strL) - 1 - for left <= right { - mid = (left + right) >> 1 - midStr := strL[mid] - if midStr == startQ { - // Handle condition where there might be multiple values equal to startQ. - // We are looking for the very first value < midStL, that i+1 will be the first - // element >= midStr. - for i := mid - 1; i >= 0; i-- { - if strL[i] != midStr { - return i + 1 - } - } - return 0 - } - if midStr < startQ { - left = mid + 1 - } else { // midStrL > startQ - right = mid - 1 - } - } - if left >= 0 && left < len(strL) && strL[left] >= startQ { - return left - } - return -1 -} - -func findEndIndex(strL []string, endQ string) int { - if len(strL) == 0 { - return -1 - } - - // Modified binary search to find the very first element > 1 - midStr := strL[mid] - if midStr == endQ { - // Handle condition where there might be multiple values equal to startQ. - // We are looking for the very first value < midStL, that i+1 will be the first - // element >= midStr. - for i := mid - 1; i >= 0; i-- { - if strL[i] < midStr { - return i + 1 - } - } - return 0 - } - if midStr < endQ { - left = mid + 1 - } else { // midStrL > startQ - right = mid - 1 - } - } - - // Binary search failed, now let's find a value less than endQ. - for i := right; i >= 0; i-- { - if strL[i] < endQ { - return i - } - } - - return -1 -} - -type sortState int - -const ( - stateUnsorted sortState = iota - stateAlreadySorted -) - -const minSortSize = 1024 - -// Constructs a slice of dirty items, to use w/ memIterator. -func (store *GStore[V]) dirtyItems(start, end []byte) { - startStr, endStr := conv.UnsafeBytesToStr(start), conv.UnsafeBytesToStr(end) - if end != nil && startStr > endStr { - // Nothing to do here. - return - } - - n := len(store.unsortedCache) - unsorted := make([]*kvPair[V], 0) - // If the unsortedCache is too big, its costs too much to determine - // whats in the subset we are concerned about. - // If you are interleaving iterator calls with writes, this can easily become an - // O(N^2) overhead. - // Even without that, too many range checks eventually becomes more expensive - // than just not having the cache. - if n < minSortSize { - for key := range store.unsortedCache { - // dbm.IsKeyInDomain is nil safe and returns true iff key is greater than start - if dbm.IsKeyInDomain(conv.UnsafeStrToBytes(key), start, end) { - cacheValue := store.cache[key] - unsorted = append(unsorted, &kvPair[V]{Key: []byte(key), Value: cacheValue.value}) - } - } - store.clearUnsortedCacheSubset(unsorted, stateUnsorted) - return - } - - // Otherwise it is large so perform a modified binary search to find - // the target ranges for the keys that we should be looking for. - strL := make([]string, 0, n) - for key := range store.unsortedCache { - strL = append(strL, key) - } - sort.Strings(strL) - - // Now find the values within the domain - // [start, end) - startIndex := findStartIndex(strL, startStr) - if startIndex < 0 { - startIndex = 0 - } - - var endIndex int - if end == nil { - endIndex = len(strL) - 1 - } else { - endIndex = findEndIndex(strL, endStr) - } - if endIndex < 0 { - endIndex = len(strL) - 1 - } - - // Since we spent cycles to sort the values, we should process and remove a reasonable amount - // ensure start to end is at least minSortSize in size - // if below minSortSize, expand it to cover additional values - // this amortizes the cost of processing elements across multiple calls - if endIndex-startIndex < minSortSize { - endIndex = math.Min(startIndex+minSortSize, len(strL)-1) - if endIndex-startIndex < minSortSize { - startIndex = math.Max(endIndex-minSortSize, 0) - } - } - - kvL := make([]*kvPair[V], 0, 1+endIndex-startIndex) - for i := startIndex; i <= endIndex; i++ { - key := strL[i] - cacheValue := store.cache[key] - kvL = append(kvL, &kvPair[V]{Key: []byte(key), Value: cacheValue.value}) - } - - // kvL was already sorted so pass it in as is. - store.clearUnsortedCacheSubset(kvL, stateAlreadySorted) -} - -func (store *GStore[V]) clearUnsortedCacheSubset(unsorted []*kvPair[V], sortState sortState) { - n := len(store.unsortedCache) - if len(unsorted) == n { // This pattern allows the Go compiler to emit the map clearing idiom for the entire map. - for key := range store.unsortedCache { - delete(store.unsortedCache, key) - } - } else { // Otherwise, normally delete the unsorted keys from the map. - for _, kv := range unsorted { - delete(store.unsortedCache, conv.UnsafeBytesToStr(kv.Key)) - } - } - - if sortState == stateUnsorted { - sort.Slice(unsorted, func(i, j int) bool { - return bytes.Compare(unsorted[i].Key, unsorted[j].Key) < 0 - }) - } - - for _, item := range unsorted { - // sortedCache is able to store `nil` value to represent deleted items. - store.sortedCache.Set(item.Key, item.Value) - } -} - -//---------------------------------------- -// etc - -// Only entrypoint to mutate store.cache. -// A `nil` value means a deletion. -func (store *GStore[V]) setCacheValue(key []byte, value V, dirty bool) { - keyStr := conv.UnsafeBytesToStr(key) - store.cache[keyStr] = &cValue[V]{ - value: value, - dirty: dirty, - } - if dirty { - store.unsortedCache[keyStr] = struct{}{} - } -} diff --git a/store/internal/btree/btree.go b/store/internal/btree/btree.go index 0fe28def41ee..bef4309412ff 100644 --- a/store/internal/btree/btree.go +++ b/store/internal/btree/btree.go @@ -36,22 +36,18 @@ func NewBTree[V any]() BTree[V] { } } +// Set supports nil as value when used as overlay func (bt BTree[V]) Set(key []byte, value V) { bt.tree.Set(newItem(key, value)) } -func (bt BTree[V]) Get(key []byte) V { - var empty V - i, found := bt.tree.Get(newItem(key, empty)) - if !found { - return empty - } - return i.value +func (bt BTree[V]) Get(key []byte) (V, bool) { + i, found := bt.tree.Get(newItemWithKey[V](key)) + return i.value, found } func (bt BTree[V]) Delete(key []byte) { - var empty V - bt.tree.Delete(newItem(key, empty)) + bt.tree.Delete(newItemWithKey[V](key)) } func (bt BTree[V]) Iterator(start, end []byte) (types.GIterator[V], error) { @@ -80,6 +76,12 @@ func (bt BTree[V]) Clear() { bt.tree.Clear() } +func (bt BTree[V]) Scan(cb func(key []byte, value V) bool) { + bt.tree.Scan(func(i item[V]) bool { + return cb(i.key, i.value) + }) +} + // item is a btree item with byte slices as keys and values type item[V any] struct { key []byte @@ -95,3 +97,8 @@ func byKeys[V any](a, b item[V]) bool { func newItem[V any](key []byte, value V) item[V] { return item[V]{key: key, value: value} } + +// newItem creates a new pair item with empty value. +func newItemWithKey[V any](key []byte) item[V] { + return item[V]{key: key} +} diff --git a/store/internal/btree/btree_test.go b/store/internal/btree/btree_test.go index 89b5827e6156..75613ebbb69c 100644 --- a/store/internal/btree/btree_test.go +++ b/store/internal/btree/btree_test.go @@ -12,17 +12,20 @@ func TestGetSetDelete(t *testing.T) { db := NewBTree[[]byte]() // A nonexistent key should return nil. - value := db.Get([]byte("a")) + value, found := db.Get([]byte("a")) require.Nil(t, value) + require.False(t, found) // Set and get a value. db.Set([]byte("a"), []byte{0x01}) db.Set([]byte("b"), []byte{0x02}) - value = db.Get([]byte("a")) + value, found = db.Get([]byte("a")) require.Equal(t, []byte{0x01}, value) + require.True(t, found) - value = db.Get([]byte("b")) + value, found = db.Get([]byte("b")) require.Equal(t, []byte{0x02}, value) + require.True(t, found) // Deleting a non-existent value is fine. db.Delete([]byte("x")) @@ -30,13 +33,23 @@ func TestGetSetDelete(t *testing.T) { // Delete a value. db.Delete([]byte("a")) - value = db.Get([]byte("a")) + value, found = db.Get([]byte("a")) require.Nil(t, value) + require.False(t, found) db.Delete([]byte("b")) - value = db.Get([]byte("b")) + value, found = db.Get([]byte("b")) require.Nil(t, value) + require.False(t, found) +} + +func TestNilValue(t *testing.T) { + db := NewBTree[[]byte]() + db.Set([]byte("a"), nil) + value, found := db.Get([]byte("a")) + require.Nil(t, value) + require.True(t, found) } func TestDBIterator(t *testing.T) { diff --git a/store/internal/btreeadaptor.go b/store/internal/btreeadaptor.go index 322e0f4152fe..318c67bfda89 100644 --- a/store/internal/btreeadaptor.go +++ b/store/internal/btreeadaptor.go @@ -20,9 +20,15 @@ func NewBTreeStore[V any](btree btree.BTree[V], isZero func(V) bool, valueLen fu return &BTreeStore[V]{btree, isZero, valueLen} } +func (ts *BTreeStore[V]) Get(key []byte) (value V) { + value, _ = ts.BTree.Get(key) + return +} + // Hash Implements GKVStore. func (ts *BTreeStore[V]) Has(key []byte) bool { - return !ts.isZero(ts.Get(key)) + _, found := ts.BTree.Get(key) + return found } func (ts *BTreeStore[V]) Iterator(start, end []byte) types.GIterator[V] { diff --git a/store/types/store.go b/store/types/store.go index 79d5776a870f..16bfbb13c228 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -350,6 +350,12 @@ func (cid CommitID) String() string { return fmt.Sprintf("CommitID{%v:%X}", cid.Hash, cid.Version) } +// BranchStore +type BranchStore interface { + Clone() BranchStore + Restore(BranchStore) +} + //---------------------------------------- // Store types From bcdf190084988dc754c1e18498f2d2e7797fa092 Mon Sep 17 00:00:00 2001 From: yihuang Date: Mon, 1 Apr 2024 14:46:42 +0800 Subject: [PATCH 10/13] Problem: no API to use the new CoW branch store (#243) * Support RunAtomic API * add unit test --- CHANGELOG.md | 8 ++--- store/CHANGELOG.md | 1 + store/cachekv/store.go | 7 ++--- store/cachekv/store_test.go | 14 +++++++++ store/cachemulti/store.go | 53 ++++++++++++++++++++++++++++++---- store/cachemulti/store_test.go | 30 +++++++++++++++++++ store/internal/btreeadaptor.go | 5 +++- store/types/store.go | 2 ++ types/context.go | 21 ++++++++++++++ 9 files changed, 124 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8511026f6a75..a45cc2508851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Add `TxExecutor` baseapp option, add `TxIndex`/`TxCount`/`MsgIndex`/`BlockGasUsed` fields to `Context, to support tx parallel execution. * (baseapp) [#206](https://github.com/crypto-org-chain/cosmos-sdk/pull/206) Support mount object store in baseapp, add `ObjectStore` api in context.. * (bank) [#237](https://github.com/crypto-org-chain/cosmos-sdk/pull/237) Support virtual accounts in sending coins. +* (x/bank) [#239](https://github.com/crypto-org-chain/cosmos-sdk/pull/239) Add low level `AddBalance`,`SubBalance` APIs to bank keeper. +* [#243](https://github.com/crypto-org-chain/cosmos-sdk/pull/243) Support `RunAtomic` API in `Context` to use new CoW branched cache store. ## [v0.53.0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.53.0) - 2025-04-29 @@ -175,12 +177,6 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [v0.50.9](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.9) - 2024-08-07 -### Features - -* (baseapp) [#205](https://github.com/crypto-org-chain/cosmos-sdk/pull/205) Add `TxExecutor` baseapp option, add `TxIndex`/`TxCount`/`MsgIndex`/`BlockGasUsed` fields to `Context, to support tx parallel execution. - -## [Unreleased-Upstream] - ### Bug Fixes * (baseapp) [#21159](https://github.com/cosmos/cosmos-sdk/pull/21159) Return PreBlocker events in FinalizeBlockResponse. diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 4d4f4bfd2e56..58f94a61aa74 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -32,6 +32,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#240](https://github.com/crypto-org-chain/cosmos-sdk/pull/240) Split methods from `MultiStore` into specialized `RootMultiStore`, keep `MultiStore` generic. * [#241](https://github.com/crypto-org-chain/cosmos-sdk/pull/241) Refactor the cache store to be btree backed, prepare to support copy-on-write atomic branching. * [#242](https://github.com/crypto-org-chain/cosmos-sdk/pull/242) Init cache on cache lazily, save memory allocations. +* [#243](https://github.com/crypto-org-chain/cosmos-sdk/pull/243) Support `RunAtomic` API to use new CoW cache store. ## v1.1.2 (March 31, 2025) diff --git a/store/cachekv/store.go b/store/cachekv/store.go index df47dfc3e6ae..c11ddd9e2b21 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -53,10 +53,9 @@ func (store *GStore[V]) GetStoreType() types.StoreType { // Clone creates a copy-on-write snapshot of the cache store, // it only performs a shallow copy so is very fast. func (store *GStore[V]) Clone() types.BranchStore { - return &GStore[V]{ - writeSet: store.writeSet.Copy(), - parent: store.parent, - } + v := *store + v.writeSet = store.writeSet.Copy() + return &v } // swapCache swap out the internal cache store and leave the current store unusable. diff --git a/store/cachekv/store_test.go b/store/cachekv/store_test.go index 980fe194677b..f99514ecce4a 100644 --- a/store/cachekv/store_test.go +++ b/store/cachekv/store_test.go @@ -447,6 +447,20 @@ func TestIteratorDeadlock(t *testing.T) { defer it2.Close() } +func TestBranchStore(t *testing.T) { + mem := dbadapter.Store{DB: dbm.NewMemDB()} + store := cachekv.NewStore(mem) + + store.Set([]byte("key1"), []byte("value1")) + + branch := store.Clone().(types.CacheKVStore) + branch.Set([]byte("key1"), []byte("value2")) + + require.Equal(t, []byte("value1"), store.Get([]byte("key1"))) + store.Restore(branch.(types.BranchStore)) + require.Equal(t, []byte("value2"), store.Get([]byte("key1"))) +} + //------------------------------------------------------------------------------------------- // do some random ops diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index ac40519ad9ec..0a2f000e0df2 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -26,6 +26,8 @@ type Store struct { traceWriter io.Writer traceContext types.TraceContext parentStore func(types.StoreKey) types.CacheWrap + + branched bool } var _ types.CacheMultiStore = Store{} @@ -44,7 +46,7 @@ func NewFromKVStore( } for key, store := range stores { - cms.initStore(key, store) + cms.stores[key] = cms.initStore(key, store) } return cms @@ -79,9 +81,7 @@ func (cms Store) initStore(key types.StoreKey, store types.CacheWrapper) types.C store = tracekv.NewStore(kvstore, cms.traceWriter, tctx) } } - cache := store.CacheWrap() - cms.stores[key] = cache - return cache + return store.CacheWrap() } // SetTracer sets the tracer for the MultiStore that the underlying @@ -117,6 +117,9 @@ func (cms Store) GetStoreType() types.StoreType { // Write calls Write on each underlying store. func (cms Store) Write() { + if cms.branched { + panic("cannot Write on branched store") + } for _, store := range cms.stores { store.Write() } @@ -134,9 +137,14 @@ func (cms Store) CacheMultiStore() types.CacheMultiStore { func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { store, ok := cms.stores[key] - if !ok && cms.parentStore != nil { + if !ok { // load on demand - store = cms.initStore(key, cms.parentStore(key)) + if cms.branched { + store = cms.parentStore(key).(types.BranchStore).Clone().(types.CacheWrap) + } else if cms.parentStore != nil { + store = cms.initStore(key, cms.parentStore(key)) + } + cms.stores[key] = store } if key == nil || store == nil { panic(fmt.Sprintf("kv store with key %v has not been registered in stores", key)) @@ -170,3 +178,36 @@ func (cms Store) GetObjKVStore(key types.StoreKey) types.ObjKVStore { } return store } + +func (cms Store) Clone() Store { + return Store{ + stores: make(map[types.StoreKey]types.CacheWrap), + + traceWriter: cms.traceWriter, + traceContext: cms.traceContext, + parentStore: cms.getCacheWrap, + + branched: true, + } +} + +func (cms Store) Restore(other Store) { + if !other.branched { + panic("cannot restore from non-branched store") + } + + // restore the stores + for k, v := range other.stores { + cms.stores[k].(types.BranchStore).Restore(v.(types.BranchStore)) + } +} + +func (cms Store) RunAtomic(cb func(types.CacheMultiStore) error) error { + branch := cms.Clone() + if err := cb(branch); err != nil { + return err + } + + cms.Restore(branch) + return nil +} diff --git a/store/cachemulti/store_test.go b/store/cachemulti/store_test.go index 0ea7785bff93..8029282eafc4 100644 --- a/store/cachemulti/store_test.go +++ b/store/cachemulti/store_test.go @@ -4,8 +4,12 @@ import ( "fmt" "testing" + dbm "github.com/cosmos/cosmos-db" "github.com/stretchr/testify/require" + "cosmossdk.io/store/dbadapter" + "cosmossdk.io/store/internal" + "cosmossdk.io/store/internal/btree" "cosmossdk.io/store/types" ) @@ -22,3 +26,29 @@ func TestStoreGetKVStore(t *testing.T) { require.PanicsWithValue(errMsg, func() { s.GetKVStore(key) }) } + +func TestRunAtomic(t *testing.T) { + store := dbadapter.Store{DB: dbm.NewMemDB()} + objStore := internal.NewBTreeStore(btree.NewBTree[any](), + func(v any) bool { return v == nil }, + func(v any) int { return 1 }, + ) + keys := map[string]types.StoreKey{ + "abc": types.NewKVStoreKey("abc"), + "obj": types.NewObjectStoreKey("obj"), + "lazy": types.NewKVStoreKey("lazy"), + } + s := Store{stores: map[types.StoreKey]types.CacheWrap{ + keys["abc"]: store.CacheWrap(), + keys["obj"]: objStore.CacheWrap(), + keys["lazy"]: nil, + }} + + s.RunAtomic(func(ms types.CacheMultiStore) error { + ms.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value")) + ms.GetObjKVStore(keys["obj"]).Set([]byte("key"), "value") + return nil + }) + require.Equal(t, []byte("value"), s.GetKVStore(keys["abc"]).Get([]byte("key"))) + require.Equal(t, "value", s.GetObjKVStore(keys["obj"]).Get([]byte("key")).(string)) +} diff --git a/store/internal/btreeadaptor.go b/store/internal/btreeadaptor.go index 318c67bfda89..dbc212779177 100644 --- a/store/internal/btreeadaptor.go +++ b/store/internal/btreeadaptor.go @@ -6,7 +6,10 @@ import ( "cosmossdk.io/store/types" ) -var _ types.KVStore = (*BTreeStore[[]byte])(nil) +var ( + _ types.KVStore = (*BTreeStore[[]byte])(nil) + _ types.ObjKVStore = (*BTreeStore[any])(nil) +) // BTreeStore is a wrapper for a BTree with GKVStore[V] implementation type BTreeStore[V any] struct { diff --git a/store/types/store.go b/store/types/store.go index 16bfbb13c228..e63bba79f276 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -155,6 +155,8 @@ type RootMultiStore interface { type CacheMultiStore interface { MultiStore Write() // Writes operations to underlying KVStore + + RunAtomic(func(CacheMultiStore) error) error } // CommitMultiStore is an interface for a MultiStore without cache capabilities. diff --git a/types/context.go b/types/context.go index 824453621d96..4109c95ca153 100644 --- a/types/context.go +++ b/types/context.go @@ -2,6 +2,7 @@ package types import ( "context" + "errors" "time" abci "github.com/cometbft/cometbft/abci/types" @@ -405,6 +406,26 @@ func (c Context) CacheContext() (cc Context, writeCache func()) { return cc, writeCache } +// RunAtomic execute the callback function atomically, i.e. the state and event changes are +// only persisted if the callback returns no error, or discarded as a whole. +// It uses an efficient approach than CacheContext, without wrapping stores. +func (c Context) RunAtomic(cb func(Context) error) error { + evtManager := NewEventManager() + cacheMS, ok := c.ms.(storetypes.CacheMultiStore) + if !ok { + return errors.New("multistore is not a CacheMultiStore") + } + if err := cacheMS.RunAtomic(func(ms storetypes.CacheMultiStore) error { + ctx := c.WithMultiStore(ms).WithEventManager(evtManager) + return cb(ctx) + }); err != nil { + return err + } + + c.EventManager().EmitEvents(evtManager.Events()) + return nil +} + var ( _ context.Context = Context{} _ storetypes.Context = Context{} From 4e335cd75b8e78127e598e063f40e3c1c3f92f3f Mon Sep 17 00:00:00 2001 From: yihuang Date: Mon, 1 Apr 2024 16:07:07 +0800 Subject: [PATCH 11/13] Problem: store key type assertion is incorrect (#244) fix and add test --- store/cachemulti/store_test.go | 9 +++++++++ store/rootmulti/store.go | 16 +++++++--------- store/rootmulti/store_test.go | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/store/cachemulti/store_test.go b/store/cachemulti/store_test.go index 8029282eafc4..03053c4e5633 100644 --- a/store/cachemulti/store_test.go +++ b/store/cachemulti/store_test.go @@ -1,6 +1,7 @@ package cachemulti import ( + "errors" "fmt" "testing" @@ -51,4 +52,12 @@ func TestRunAtomic(t *testing.T) { }) require.Equal(t, []byte("value"), s.GetKVStore(keys["abc"]).Get([]byte("key"))) require.Equal(t, "value", s.GetObjKVStore(keys["obj"]).Get([]byte("key")).(string)) + + require.Error(t, s.RunAtomic(func(ms types.CacheMultiStore) error { + ms.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value2")) + ms.GetObjKVStore(keys["obj"]).Set([]byte("key"), "value2") + return errors.New("failure") + })) + require.Equal(t, []byte("value"), s.GetKVStore(keys["abc"]).Get([]byte("key"))) + require.Equal(t, "value", s.GetObjKVStore(keys["obj"]).Get([]byte("key")).(string)) } diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index 26461c714499..d210887f2a87 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -1051,26 +1051,24 @@ func (rs *Store) loadCommitStoreFromParams(key types.StoreKey, id types.CommitID return commitDBStoreAdapter{Store: dbadapter.Store{DB: db}}, nil case types.StoreTypeTransient: - _, ok := key.(*types.TransientStoreKey) - if !ok { - return nil, fmt.Errorf("invalid StoreKey for StoreTypeTransient: %s", key.String()) + if _, ok := key.(*types.TransientStoreKey); !ok { + return nil, fmt.Errorf("unexpected key type for a TransientStoreKey; got: %s, %T", key.String(), key) } return transient.NewStore(), nil case types.StoreTypeMemory: - _, ok := key.(*types.ObjectStoreKey) - if !ok { - return nil, fmt.Errorf("invalid StoreKey for StoreTypeTransient: %s", key.String()) - } - if _, ok := key.(*types.MemoryStoreKey); !ok { - return nil, fmt.Errorf("unexpected key type for a MemoryStoreKey; got: %s", key.String()) + return nil, fmt.Errorf("unexpected key type for a MemoryStoreKey; got: %s, %T", key.String(), key) } return mem.NewStore(), nil case types.StoreTypeObject: + if _, ok := key.(*types.ObjectStoreKey); !ok { + return nil, fmt.Errorf("unexpected key type for a ObjectStoreKey; got: %s, %T", key.String(), key) + } + return transient.NewObjStore(), nil default: diff --git a/store/rootmulti/store_test.go b/store/rootmulti/store_test.go index 1cae379c22c5..ff7d83264d0c 100644 --- a/store/rootmulti/store_test.go +++ b/store/rootmulti/store_test.go @@ -943,6 +943,7 @@ func prepareStoreMap() (map[types.StoreKey]types.CommitStore, error) { store.MountStoreWithDB(types.NewKVStoreKey("iavl1"), types.StoreTypeIAVL, nil) store.MountStoreWithDB(types.NewKVStoreKey("iavl2"), types.StoreTypeIAVL, nil) store.MountStoreWithDB(types.NewTransientStoreKey("trans1"), types.StoreTypeTransient, nil) + store.MountStoreWithDB(types.NewMemoryStoreKey("mem1"), types.StoreTypeMemory, nil) store.MountStoreWithDB(types.NewObjectStoreKey("obj1"), types.StoreTypeObject, nil) if err := store.LoadLatestVersion(); err != nil { return nil, err From 3fde12ec93ea193a0d7e83883425637bcb7c8ae5 Mon Sep 17 00:00:00 2001 From: yihuang Date: Tue, 2 Apr 2024 09:54:25 +0800 Subject: [PATCH 12/13] Problem: Restore don't work snapshot revert usage (#245) Solution: - fix and add test to support the usage pattern in ethermint add Discard method to CacheWrap better testing --- store/CHANGELOG.md | 1 + store/cachekv/store.go | 4 +++ store/cachemulti/store.go | 47 ++++++++++++++++++++++++---------- store/cachemulti/store_test.go | 39 +++++++++++++++++++++++----- store/types/store.go | 3 +++ 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 58f94a61aa74..2ac2aa5570dc 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -33,6 +33,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#241](https://github.com/crypto-org-chain/cosmos-sdk/pull/241) Refactor the cache store to be btree backed, prepare to support copy-on-write atomic branching. * [#242](https://github.com/crypto-org-chain/cosmos-sdk/pull/242) Init cache on cache lazily, save memory allocations. * [#243](https://github.com/crypto-org-chain/cosmos-sdk/pull/243) Support `RunAtomic` API to use new CoW cache store. +* [#244](https://github.com/crypto-org-chain/cosmos-sdk/pull/244) Add `Discard` method to CacheWrap to discard the write buffer. ## v1.1.2 (March 31, 2025) diff --git a/store/cachekv/store.go b/store/cachekv/store.go index c11ddd9e2b21..5d174acaaeb8 100644 --- a/store/cachekv/store.go +++ b/store/cachekv/store.go @@ -119,6 +119,10 @@ func (store *GStore[V]) Write() { store.writeSet.Clear() } +func (store *GStore[V]) Discard() { + store.writeSet.Clear() +} + // CacheWrap implements CacheWrapper. func (store *GStore[V]) CacheWrap() types.CacheWrap { return NewGStore(store, store.isZero, store.valueLen) diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index 0a2f000e0df2..3a78622a7e39 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -46,7 +46,7 @@ func NewFromKVStore( } for key, store := range stores { - cms.stores[key] = cms.initStore(key, store) + cms.initStore(key, store) } return cms @@ -81,7 +81,9 @@ func (cms Store) initStore(key types.StoreKey, store types.CacheWrapper) types.C store = tracekv.NewStore(kvstore, cms.traceWriter, tctx) } } - return store.CacheWrap() + cache := store.CacheWrap() + cms.stores[key] = cache + return cache } // SetTracer sets the tracer for the MultiStore that the underlying @@ -125,6 +127,12 @@ func (cms Store) Write() { } } +func (cms Store) Discard() { + for _, store := range cms.stores { + store.Discard() + } +} + // Implements CacheWrapper. func (cms Store) CacheWrap() types.CacheWrap { return cms.CacheMultiStore().(types.CacheWrap) @@ -137,14 +145,9 @@ func (cms Store) CacheMultiStore() types.CacheMultiStore { func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { store, ok := cms.stores[key] - if !ok { + if !ok && cms.parentStore != nil { // load on demand - if cms.branched { - store = cms.parentStore(key).(types.BranchStore).Clone().(types.CacheWrap) - } else if cms.parentStore != nil { - store = cms.initStore(key, cms.parentStore(key)) - } - cms.stores[key] = store + store = cms.initStore(key, cms.parentStore(key)) } if key == nil || store == nil { panic(fmt.Sprintf("kv store with key %v has not been registered in stores", key)) @@ -180,12 +183,15 @@ func (cms Store) GetObjKVStore(key types.StoreKey) types.ObjKVStore { } func (cms Store) Clone() Store { + stores := make(map[types.StoreKey]types.CacheWrap, len(cms.stores)) + for k, v := range cms.stores { + stores[k] = v.(types.BranchStore).Clone().(types.CacheWrap) + } return Store{ - stores: make(map[types.StoreKey]types.CacheWrap), - + stores: stores, traceWriter: cms.traceWriter, traceContext: cms.traceContext, - parentStore: cms.getCacheWrap, + parentStore: cms.parentStore, branched: true, } @@ -196,9 +202,22 @@ func (cms Store) Restore(other Store) { panic("cannot restore from non-branched store") } - // restore the stores + // discard the non-exists stores + for k, v := range cms.stores { + if _, ok := other.stores[k]; !ok { + // clear the cache store if it's not in the other + v.Discard() + } + } + + // restore the other stores for k, v := range other.stores { - cms.stores[k].(types.BranchStore).Restore(v.(types.BranchStore)) + store, ok := cms.stores[k] + if !ok { + store = cms.initStore(k, cms.parentStore(k)) + } + + store.(types.BranchStore).Restore(v.(types.BranchStore)) } } diff --git a/store/cachemulti/store_test.go b/store/cachemulti/store_test.go index 03053c4e5633..01c7a810bf91 100644 --- a/store/cachemulti/store_test.go +++ b/store/cachemulti/store_test.go @@ -35,22 +35,22 @@ func TestRunAtomic(t *testing.T) { func(v any) int { return 1 }, ) keys := map[string]types.StoreKey{ - "abc": types.NewKVStoreKey("abc"), - "obj": types.NewObjectStoreKey("obj"), - "lazy": types.NewKVStoreKey("lazy"), + "abc": types.NewKVStoreKey("abc"), + "obj": types.NewObjectStoreKey("obj"), } - s := Store{stores: map[types.StoreKey]types.CacheWrap{ - keys["abc"]: store.CacheWrap(), - keys["obj"]: objStore.CacheWrap(), - keys["lazy"]: nil, + parent := Store{stores: map[types.StoreKey]types.CacheWrap{ + keys["abc"]: store.CacheWrap(), + keys["obj"]: objStore.CacheWrap(), }} + s := Store{stores: map[types.StoreKey]types.CacheWrap{}, parentStore: parent.getCacheWrap} s.RunAtomic(func(ms types.CacheMultiStore) error { ms.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value")) ms.GetObjKVStore(keys["obj"]).Set([]byte("key"), "value") return nil }) require.Equal(t, []byte("value"), s.GetKVStore(keys["abc"]).Get([]byte("key"))) + require.Equal(t, []byte(nil), s.GetKVStore(keys["abc"]).Get([]byte("key-non-exist"))) require.Equal(t, "value", s.GetObjKVStore(keys["obj"]).Get([]byte("key")).(string)) require.Error(t, s.RunAtomic(func(ms types.CacheMultiStore) error { @@ -61,3 +61,28 @@ func TestRunAtomic(t *testing.T) { require.Equal(t, []byte("value"), s.GetKVStore(keys["abc"]).Get([]byte("key"))) require.Equal(t, "value", s.GetObjKVStore(keys["obj"]).Get([]byte("key")).(string)) } + +func TestBranchStore(t *testing.T) { + store := dbadapter.Store{DB: dbm.NewMemDB()} + objStore := internal.NewBTreeStore(btree.NewBTree[any](), + func(v any) bool { return v == nil }, + func(v any) int { return 1 }, + ) + keys := map[string]types.StoreKey{ + "abc": types.NewKVStoreKey("abc"), + "obj": types.NewObjectStoreKey("obj"), + } + parent := Store{stores: map[types.StoreKey]types.CacheWrap{ + keys["abc"]: store.CacheWrap(), + keys["obj"]: objStore.CacheWrap(), + }} + + s := Store{stores: map[types.StoreKey]types.CacheWrap{}, parentStore: parent.getCacheWrap} + s.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value")) + snapshot := s.Clone() + s.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value2")) + s.GetObjKVStore(keys["obj"]).Set([]byte("key"), "value") + s.Restore(snapshot) + require.Equal(t, []byte("value"), s.GetKVStore(keys["abc"]).Get([]byte("key"))) + require.Equal(t, nil, s.GetObjKVStore(keys["obj"]).Get([]byte("key"))) +} diff --git a/store/types/store.go b/store/types/store.go index e63bba79f276..9e1f3861c8d6 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -337,6 +337,9 @@ type CacheWrap interface { // Write syncs with the underlying store. Write() + + // Discard the write set + Discard() } type CacheWrapper interface { From 9ccebe17cad50c6c9c1c29b2610fad789663135f Mon Sep 17 00:00:00 2001 From: yihuang Date: Fri, 5 Apr 2024 11:19:53 +0800 Subject: [PATCH 13/13] Problem: no api to create cachemulti store from external cache store (#258) Solution: - add API NewFromParent to cache multistore. Update store/CHANGELOG.md Signed-off-by: yihuang fix test fix lint --- store/CHANGELOG.md | 1 + store/cachemulti/store.go | 25 +++++++++++++++---------- store/cachemulti/store_test.go | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/store/CHANGELOG.md b/store/CHANGELOG.md index 2ac2aa5570dc..69c6c75cfac6 100644 --- a/store/CHANGELOG.md +++ b/store/CHANGELOG.md @@ -34,6 +34,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#242](https://github.com/crypto-org-chain/cosmos-sdk/pull/242) Init cache on cache lazily, save memory allocations. * [#243](https://github.com/crypto-org-chain/cosmos-sdk/pull/243) Support `RunAtomic` API to use new CoW cache store. * [#244](https://github.com/crypto-org-chain/cosmos-sdk/pull/244) Add `Discard` method to CacheWrap to discard the write buffer. +* [#258](https://github.com/crypto-org-chain/cosmos-sdk/pull/258) Add `NewFromParent` API to cachemulti store to create a new store from block-stm multiversion data structure. ## v1.1.2 (March 31, 2025) diff --git a/store/cachemulti/store.go b/store/cachemulti/store.go index 3a78622a7e39..2a822831c317 100644 --- a/store/cachemulti/store.go +++ b/store/cachemulti/store.go @@ -25,7 +25,7 @@ type Store struct { traceWriter io.Writer traceContext types.TraceContext - parentStore func(types.StoreKey) types.CacheWrap + parentStore func(types.StoreKey) types.CacheWrapper branched bool } @@ -61,12 +61,17 @@ func NewStore( return NewFromKVStore(stores, traceWriter, traceContext) } -func newCacheMultiStoreFromCMS(cms Store) Store { +// NewFromParent constructs a cache multistore with a parent store lazily, +// the parent is usually another cache multistore or the block-stm multiversion store. +func NewFromParent( + parentStore func(types.StoreKey) types.CacheWrapper, + traceWriter io.Writer, traceContext types.TraceContext, +) Store { return Store{ stores: make(map[types.StoreKey]types.CacheWrap), - traceWriter: cms.traceWriter, - traceContext: cms.traceContext, - parentStore: cms.getCacheWrap, + traceWriter: traceWriter, + traceContext: traceContext, + parentStore: parentStore, } } @@ -140,10 +145,10 @@ func (cms Store) CacheWrap() types.CacheWrap { // Implements MultiStore. func (cms Store) CacheMultiStore() types.CacheMultiStore { - return newCacheMultiStoreFromCMS(cms) + return NewFromParent(cms.getCacheWrapper, cms.traceWriter, cms.traceContext) } -func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { +func (cms Store) getCacheWrapper(key types.StoreKey) types.CacheWrapper { store, ok := cms.stores[key] if !ok && cms.parentStore != nil { // load on demand @@ -157,7 +162,7 @@ func (cms Store) getCacheWrap(key types.StoreKey) types.CacheWrap { // GetStore returns an underlying Store by key. func (cms Store) GetStore(key types.StoreKey) types.Store { - store, ok := cms.getCacheWrap(key).(types.Store) + store, ok := cms.getCacheWrapper(key).(types.Store) if !ok { panic(fmt.Sprintf("store with key %v is not Store", key)) } @@ -166,7 +171,7 @@ func (cms Store) GetStore(key types.StoreKey) types.Store { // GetKVStore returns an underlying KVStore by key. func (cms Store) GetKVStore(key types.StoreKey) types.KVStore { - store, ok := cms.getCacheWrap(key).(types.KVStore) + store, ok := cms.getCacheWrapper(key).(types.KVStore) if !ok { panic(fmt.Sprintf("store with key %v is not KVStore", key)) } @@ -175,7 +180,7 @@ func (cms Store) GetKVStore(key types.StoreKey) types.KVStore { // GetObjKVStore returns an underlying KVStore by key. func (cms Store) GetObjKVStore(key types.StoreKey) types.ObjKVStore { - store, ok := cms.getCacheWrap(key).(types.ObjKVStore) + store, ok := cms.getCacheWrapper(key).(types.ObjKVStore) if !ok { panic(fmt.Sprintf("store with key %v is not ObjKVStore", key)) } diff --git a/store/cachemulti/store_test.go b/store/cachemulti/store_test.go index 01c7a810bf91..38d1182cdf7a 100644 --- a/store/cachemulti/store_test.go +++ b/store/cachemulti/store_test.go @@ -43,7 +43,7 @@ func TestRunAtomic(t *testing.T) { keys["obj"]: objStore.CacheWrap(), }} - s := Store{stores: map[types.StoreKey]types.CacheWrap{}, parentStore: parent.getCacheWrap} + s := Store{stores: map[types.StoreKey]types.CacheWrap{}, parentStore: parent.getCacheWrapper} s.RunAtomic(func(ms types.CacheMultiStore) error { ms.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value")) ms.GetObjKVStore(keys["obj"]).Set([]byte("key"), "value") @@ -77,7 +77,7 @@ func TestBranchStore(t *testing.T) { keys["obj"]: objStore.CacheWrap(), }} - s := Store{stores: map[types.StoreKey]types.CacheWrap{}, parentStore: parent.getCacheWrap} + s := Store{stores: map[types.StoreKey]types.CacheWrap{}, parentStore: parent.getCacheWrapper} s.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value")) snapshot := s.Clone() s.GetKVStore(keys["abc"]).Set([]byte("key"), []byte("value2"))