From 0795530bb7884cfacee59035a227460cdbd97014 Mon Sep 17 00:00:00 2001 From: Randy Grok <98407738+randygrok@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:22:14 +0200 Subject: [PATCH] feat(runtime/v2): store loader on simappv2 (#21704) Co-authored-by: marbar3778 (cherry picked from commit 7d6ff0df56421ea5affce7d5254d0f15b3af1b23) # Conflicts: # CHANGELOG.md # runtime/v2/app.go # runtime/v2/module.go # runtime/v2/store.go --- CHANGELOG.md | 8 ++ runtime/store.go | 21 +++ runtime/v2/app.go | 125 +++++++++++++++++ runtime/v2/module.go | 239 +++++++++++++++++++++++++++++++++ runtime/v2/store.go | 90 +++++++++++++ simapp/v2/upgrades.go | 5 +- x/upgrade/README.md | 3 + x/upgrade/types/storeloader.go | 28 +--- 8 files changed, 491 insertions(+), 28 deletions(-) create mode 100644 runtime/v2/app.go create mode 100644 runtime/v2/module.go create mode 100644 runtime/v2/store.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 423b364e7419..99529f57e299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ Every module contains its own CHANGELOG.md. Please refer to the module you are interested in. +<<<<<<< HEAD +======= +### Features + +* (baseapp) [#20291](https://github.com/cosmos/cosmos-sdk/pull/20291) Simulate nested messages. +* (runtime) [#21704](https://github.com/cosmos/cosmos-sdk/pull/21704) Add StoreLoader in simappv2. + +>>>>>>> 7d6ff0df5 (feat(runtime/v2): store loader on simappv2 (#21704)) ### Improvements * (genutil) [#21701](https://github.com/cosmos/cosmos-sdk/pull/21701) Improved error messages for genesis validation. diff --git a/runtime/store.go b/runtime/store.go index 32097fb41bbe..1585bc54af71 100644 --- a/runtime/store.go +++ b/runtime/store.go @@ -9,6 +9,7 @@ import ( "cosmossdk.io/core/store" storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -181,3 +182,23 @@ func (s kvStoreAdapter) ReverseIterator(start, end []byte) dbm.Iterator { func KVStoreAdapter(store store.KVStore) storetypes.KVStore { return &kvStoreAdapter{store} } + +// UpgradeStoreLoader is used to prepare baseapp with a fixed StoreLoader +// pattern. This is useful for custom upgrade loading logic. +func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *store.StoreUpgrades) baseapp.StoreLoader { + return func(ms storetypes.CommitMultiStore) error { + if upgradeHeight == ms.LastCommitID().Version+1 { + // Check if the current commit version and upgrade height matches + if len(storeUpgrades.Deleted) > 0 || len(storeUpgrades.Added) > 0 { + stup := &storetypes.StoreUpgrades{ + Added: storeUpgrades.Added, + Deleted: storeUpgrades.Deleted, + } + return ms.LoadLatestVersionAndUpgrade(stup) + } + } + + // Otherwise load default store loader + return baseapp.DefaultStoreLoader(ms) + } +} diff --git a/runtime/v2/app.go b/runtime/v2/app.go new file mode 100644 index 000000000000..b7104f9e4774 --- /dev/null +++ b/runtime/v2/app.go @@ -0,0 +1,125 @@ +package runtime + +import ( + "encoding/json" + "errors" + "slices" + + gogoproto "github.com/cosmos/gogoproto/proto" + + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/server/v2/stf" +) + +// App is a wrapper around AppManager and ModuleManager that can be used in hybrid +// app.go/app config scenarios or directly as a servertypes.Application instance. +// To get an instance of *App, *AppBuilder must be requested as a dependency +// in a container which declares the runtime module and the AppBuilder.Build() +// method must be called. +// +// App can be used to create a hybrid app.go setup where some configuration is +// done declaratively with an app config and the rest of it is done the old way. +// See simapp/app_v2.go for an example of this setup. +type App[T transaction.Tx] struct { + *appmanager.AppManager[T] + + // app manager dependencies + stf *stf.STF[T] + msgRouterBuilder *stf.MsgRouterBuilder + queryRouterBuilder *stf.MsgRouterBuilder + db Store + + // app configuration + logger log.Logger + config *runtimev2.Module + + // modules configuration + storeKeys []string + interfaceRegistrar registry.InterfaceRegistrar + amino registry.AminoRegistrar + moduleManager *MM[T] + + // GRPCMethodsToMessageMap maps gRPC method name to a function that decodes the request + // bytes into a gogoproto.Message, which then can be passed to appmanager. + GRPCMethodsToMessageMap map[string]func() gogoproto.Message + + storeLoader StoreLoader +} + +// Name returns the app name. +func (a *App[T]) Name() string { + return a.config.AppName +} + +// Logger returns the app logger. +func (a *App[T]) Logger() log.Logger { + return a.logger +} + +// ModuleManager returns the module manager. +func (a *App[T]) ModuleManager() *MM[T] { + return a.moduleManager +} + +// DefaultGenesis returns a default genesis from the registered modules. +func (a *App[T]) DefaultGenesis() map[string]json.RawMessage { + return a.moduleManager.DefaultGenesis() +} + +// SetStoreLoader sets the store loader. +func (a *App[T]) SetStoreLoader(loader StoreLoader) { + a.storeLoader = loader +} + +// LoadLatest loads the latest version. +func (a *App[T]) LoadLatest() error { + return a.storeLoader(a.db) +} + +// LoadHeight loads a particular height +func (a *App[T]) LoadHeight(height uint64) error { + return a.db.LoadVersion(height) +} + +// LoadLatestHeight loads the latest height. +func (a *App[T]) LoadLatestHeight() (uint64, error) { + return a.db.GetLatestVersion() +} + +// Close is called in start cmd to gracefully cleanup resources. +func (a *App[T]) Close() error { + return nil +} + +// GetStoreKeys returns all the app store keys. +func (a *App[T]) GetStoreKeys() []string { + return a.storeKeys +} + +// UnsafeFindStoreKey fetches a registered StoreKey from the App in linear time. +// NOTE: This should only be used in testing. +func (a *App[T]) UnsafeFindStoreKey(storeKey string) (string, error) { + i := slices.IndexFunc(a.storeKeys, func(s string) bool { return s == storeKey }) + if i == -1 { + return "", errors.New("store key not found") + } + + return a.storeKeys[i], nil +} + +// GetStore returns the app store. +func (a *App[T]) GetStore() Store { + return a.db +} + +func (a *App[T]) GetAppManager() *appmanager.AppManager[T] { + return a.AppManager +} + +func (a *App[T]) GetGPRCMethodsToMessageMap() map[string]func() gogoproto.Message { + return a.GRPCMethodsToMessageMap +} diff --git a/runtime/v2/module.go b/runtime/v2/module.go new file mode 100644 index 000000000000..8db0f557a57b --- /dev/null +++ b/runtime/v2/module.go @@ -0,0 +1,239 @@ +package runtime + +import ( + "fmt" + "os" + "slices" + + "github.com/cosmos/gogoproto/proto" + "google.golang.org/grpc" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoregistry" + + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" + appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" + reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/comet" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + "cosmossdk.io/depinject/appconfig" + "cosmossdk.io/log" + "cosmossdk.io/runtime/v2/services" + "cosmossdk.io/server/v2/stf" +) + +var ( + _ appmodulev2.AppModule = appModule[transaction.Tx]{} + _ hasServicesV1 = appModule[transaction.Tx]{} +) + +type appModule[T transaction.Tx] struct { + app *App[T] +} + +func (m appModule[T]) IsOnePerModuleType() {} +func (m appModule[T]) IsAppModule() {} + +func (m appModule[T]) RegisterServices(registar grpc.ServiceRegistrar) error { + autoCliQueryService, err := services.NewAutoCLIQueryService(m.app.moduleManager.modules) + if err != nil { + return err + } + + autocliv1.RegisterQueryServer(registar, autoCliQueryService) + + reflectionSvc, err := services.NewReflectionService() + if err != nil { + return err + } + reflectionv1.RegisterReflectionServiceServer(registar, reflectionSvc) + + return nil +} + +func (m appModule[T]) AutoCLIOptions() *autocliv1.ModuleOptions { + return &autocliv1.ModuleOptions{ + Query: &autocliv1.ServiceCommandDescriptor{ + Service: appv1alpha1.Query_ServiceDesc.ServiceName, + RpcCommandOptions: []*autocliv1.RpcCommandOptions{ + { + RpcMethod: "Config", + Short: "Query the current app config", + }, + }, + SubCommands: map[string]*autocliv1.ServiceCommandDescriptor{ + "autocli": { + Service: autocliv1.Query_ServiceDesc.ServiceName, + RpcCommandOptions: []*autocliv1.RpcCommandOptions{ + { + RpcMethod: "AppOptions", + Short: "Query the custom autocli options", + }, + }, + }, + "reflection": { + Service: reflectionv1.ReflectionService_ServiceDesc.ServiceName, + RpcCommandOptions: []*autocliv1.RpcCommandOptions{ + { + RpcMethod: "FileDescriptors", + Short: "Query the app's protobuf file descriptors", + }, + }, + }, + }, + }, + } +} + +func init() { + appconfig.Register(&runtimev2.Module{}, + appconfig.Provide( + ProvideAppBuilder[transaction.Tx], + ProvideEnvironment[transaction.Tx], + ProvideModuleManager[transaction.Tx], + ProvideCometService, + ), + appconfig.Invoke(SetupAppBuilder), + ) +} + +func ProvideAppBuilder[T transaction.Tx]( + interfaceRegistrar registry.InterfaceRegistrar, + amino registry.AminoRegistrar, +) ( + *AppBuilder[T], + *stf.MsgRouterBuilder, + appmodulev2.AppModule, + protodesc.Resolver, + protoregistry.MessageTypeResolver, +) { + protoFiles := proto.HybridResolver + protoTypes := protoregistry.GlobalTypes + + // At startup, check that all proto annotations are correct. + if err := validateProtoAnnotations(protoFiles); err != nil { + // Once we switch to using protoreflect-based ante handlers, we might + // want to panic here instead of logging a warning. + _, _ = fmt.Fprintln(os.Stderr, err.Error()) + } + + msgRouterBuilder := stf.NewMsgRouterBuilder() + app := &App[T]{ + storeKeys: nil, + interfaceRegistrar: interfaceRegistrar, + amino: amino, + msgRouterBuilder: msgRouterBuilder, + queryRouterBuilder: stf.NewMsgRouterBuilder(), // TODO dedicated query router + GRPCMethodsToMessageMap: map[string]func() proto.Message{}, + storeLoader: DefaultStoreLoader, + } + appBuilder := &AppBuilder[T]{app: app} + + return appBuilder, msgRouterBuilder, appModule[T]{app}, protoFiles, protoTypes +} + +type AppInputs struct { + depinject.In + + Config *runtimev2.Module + AppBuilder *AppBuilder[transaction.Tx] + ModuleManager *MM[transaction.Tx] + InterfaceRegistrar registry.InterfaceRegistrar + LegacyAmino registry.AminoRegistrar + Logger log.Logger + DynamicConfig server.DynamicConfig `optional:"true"` // can be nil in client wiring +} + +func SetupAppBuilder(inputs AppInputs) { + app := inputs.AppBuilder.app + app.config = inputs.Config + app.logger = inputs.Logger + app.moduleManager = inputs.ModuleManager + app.moduleManager.RegisterInterfaces(inputs.InterfaceRegistrar) + app.moduleManager.RegisterLegacyAminoCodec(inputs.LegacyAmino) + + if inputs.DynamicConfig != nil { + inputs.AppBuilder.config = inputs.DynamicConfig + } +} + +func ProvideModuleManager[T transaction.Tx]( + logger log.Logger, + config *runtimev2.Module, + modules map[string]appmodulev2.AppModule, +) *MM[T] { + return NewModuleManager[T](logger, config, modules) +} + +// ProvideEnvironment provides the environment for keeper modules, while maintaining backward compatibility and provide services directly as well. +func ProvideEnvironment[T transaction.Tx]( + logger log.Logger, + config *runtimev2.Module, + key depinject.ModuleKey, + appBuilder *AppBuilder[T], +) ( + appmodulev2.Environment, + store.KVStoreService, + store.MemoryStoreService, +) { + var ( + kvService store.KVStoreService = failingStoreService{} + memKvService store.MemoryStoreService = failingStoreService{} + ) + + // skips modules that have no store + if !slices.Contains(config.SkipStoreKeys, key.Name()) { + var kvStoreKey string + storeKeyOverride := storeKeyOverride(config, key.Name()) + if storeKeyOverride != nil { + kvStoreKey = storeKeyOverride.KvStoreKey + } else { + kvStoreKey = key.Name() + } + + registerStoreKey(appBuilder, kvStoreKey) + kvService = stf.NewKVStoreService([]byte(kvStoreKey)) + + memStoreKey := fmt.Sprintf("memory:%s", key.Name()) + registerStoreKey(appBuilder, memStoreKey) + memKvService = stf.NewMemoryStoreService([]byte(memStoreKey)) + } + + env := appmodulev2.Environment{ + Logger: logger, + BranchService: stf.BranchService{}, + EventService: stf.NewEventService(), + GasService: stf.NewGasMeterService(), + HeaderService: stf.HeaderService{}, + QueryRouterService: stf.NewQueryRouterService(), + MsgRouterService: stf.NewMsgRouterService([]byte(key.Name())), + TransactionService: services.NewContextAwareTransactionService(), + KVStoreService: kvService, + MemStoreService: memKvService, + } + + return env, kvService, memKvService +} + +func registerStoreKey[T transaction.Tx](wrapper *AppBuilder[T], key string) { + wrapper.app.storeKeys = append(wrapper.app.storeKeys, key) +} + +func storeKeyOverride(config *runtimev2.Module, moduleName string) *runtimev2.StoreKeyConfig { + for _, cfg := range config.OverrideStoreKeys { + if cfg.ModuleName == moduleName { + return cfg + } + } + + return nil +} + +func ProvideCometService() comet.Service { + return &services.ContextAwareCometInfoService{} +} diff --git a/runtime/v2/store.go b/runtime/v2/store.go new file mode 100644 index 000000000000..5268033ad323 --- /dev/null +++ b/runtime/v2/store.go @@ -0,0 +1,90 @@ +package runtime + +import ( + "fmt" + + "cosmossdk.io/core/store" + "cosmossdk.io/server/v2/stf" + storev2 "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/proof" +) + +// NewKVStoreService creates a new KVStoreService. +// This wrapper is kept for backwards compatibility. +// When migrating from runtime to runtime/v2, use runtimev2.NewKVStoreService(storeKey.Name()) instead of runtime.NewKVStoreService(storeKey). +func NewKVStoreService(storeKey string) store.KVStoreService { + return stf.NewKVStoreService([]byte(storeKey)) +} + +type Store interface { + // GetLatestVersion returns the latest version that consensus has been made on + GetLatestVersion() (uint64, error) + // StateLatest returns a readonly view over the latest + // committed state of the store. Alongside the version + // associated with it. + StateLatest() (uint64, store.ReaderMap, error) + + // StateAt returns a readonly view over the provided + // version. Must error when the version does not exist. + StateAt(version uint64) (store.ReaderMap, error) + + // SetInitialVersion sets the initial version of the store. + SetInitialVersion(uint64) error + + // WorkingHash writes the provided changeset to the state and returns + // the working hash of the state. + WorkingHash(changeset *store.Changeset) (store.Hash, error) + + // Commit commits the provided changeset and returns the new state root of the state. + Commit(changeset *store.Changeset) (store.Hash, error) + + // Query is a key/value query directly to the underlying database. This skips the appmanager + Query(storeKey []byte, version uint64, key []byte, prove bool) (storev2.QueryResult, error) + + // GetStateStorage returns the SS backend. + GetStateStorage() storev2.VersionedDatabase + + // GetStateCommitment returns the SC backend. + GetStateCommitment() storev2.Committer + + // LoadVersion loads the RootStore to the given version. + LoadVersion(version uint64) error + + // LoadLatestVersion behaves identically to LoadVersion except it loads the + // latest version implicitly. + LoadLatestVersion() error + + // LastCommitID returns the latest commit ID + LastCommitID() (proof.CommitID, error) +} + +// StoreLoader allows for custom loading of the store, this is useful when upgrading the store from a previous version +type StoreLoader func(store Store) error + +// DefaultStoreLoader just calls LoadLatestVersion on the store +func DefaultStoreLoader(store Store) error { + return store.LoadLatestVersion() +} + +// UpgradeStoreLoader upgrades the store if the upgrade height matches the current version, it is used as a replacement +// for the DefaultStoreLoader when there are store upgrades +func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *store.StoreUpgrades) StoreLoader { + return func(store Store) error { + latestVersion, err := store.GetLatestVersion() + if err != nil { + return err + } + + if uint64(upgradeHeight) == latestVersion+1 { + if len(storeUpgrades.Deleted) > 0 || len(storeUpgrades.Added) > 0 { + if upgrader, ok := store.(storev2.UpgradeableStore); ok { + return upgrader.LoadVersionAndUpgrade(latestVersion, storeUpgrades) + } + + return fmt.Errorf("store does not support upgrades") + } + } + + return DefaultStoreLoader(store) + } +} diff --git a/simapp/v2/upgrades.go b/simapp/v2/upgrades.go index aa8ff003bb93..35355b28c367 100644 --- a/simapp/v2/upgrades.go +++ b/simapp/v2/upgrades.go @@ -5,6 +5,7 @@ import ( "cosmossdk.io/core/appmodule" "cosmossdk.io/core/store" + "cosmossdk.io/runtime/v2" "cosmossdk.io/x/accounts" protocolpooltypes "cosmossdk.io/x/protocolpool/types" upgradetypes "cosmossdk.io/x/upgrade/types" @@ -40,8 +41,6 @@ func (app *SimApp[T]) RegisterUpgradeHandlers() { Deleted: []string{"crisis"}, // The SDK discontinued the crisis module in v0.52.0 } - // configure store loader that checks if version == upgradeHeight and applies store upgrades - _ = storeUpgrades - // app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + app.SetStoreLoader(runtime.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) } } diff --git a/x/upgrade/README.md b/x/upgrade/README.md index be6e3476a156..81534d422694 100644 --- a/x/upgrade/README.md +++ b/x/upgrade/README.md @@ -94,6 +94,9 @@ expected upgrade. It eliminiates the chances for the new binary to execute `Stor times every time on restart. Also if there are multiple upgrades planned on same height, the `Name` will ensure these `StoreUpgrades` takes place only in planned upgrade handler. +**Note:** The `StoreLoader` helper function for StoreUpgrades in v2 is not part of the `x/upgrade` module; +instead, you can find it in the runtime v2 module. + ### Proposal Typically, a `Plan` is proposed and submitted through governance via a proposal diff --git a/x/upgrade/types/storeloader.go b/x/upgrade/types/storeloader.go index 881c8df29eab..7099c9395dee 100644 --- a/x/upgrade/types/storeloader.go +++ b/x/upgrade/types/storeloader.go @@ -1,28 +1,6 @@ package types -import ( - corestore "cosmossdk.io/core/store" - storetypes "cosmossdk.io/store/types" +import "github.com/cosmos/cosmos-sdk/runtime" - "github.com/cosmos/cosmos-sdk/baseapp" -) - -// UpgradeStoreLoader is used to prepare baseapp with a fixed StoreLoader -// pattern. This is useful for custom upgrade loading logic. -func UpgradeStoreLoader(upgradeHeight int64, storeUpgrades *corestore.StoreUpgrades) baseapp.StoreLoader { - return func(ms storetypes.CommitMultiStore) error { - if upgradeHeight == ms.LastCommitID().Version+1 { - // Check if the current commit version and upgrade height matches - if len(storeUpgrades.Deleted) > 0 || len(storeUpgrades.Added) > 0 { - stup := &storetypes.StoreUpgrades{ - Added: storeUpgrades.Added, - Deleted: storeUpgrades.Deleted, - } - return ms.LoadLatestVersionAndUpgrade(stup) - } - } - - // Otherwise load default store loader - return baseapp.DefaultStoreLoader(ms) - } -} +// UpgradeStoreLoader moved to runtime package, keeping this for backwards compatibility +var UpgradeStoreLoader = runtime.UpgradeStoreLoader