diff --git a/runtime/app.go b/runtime/app.go index 55b44aaa0d34..1a845c72763e 100644 --- a/runtime/app.go +++ b/runtime/app.go @@ -8,7 +8,6 @@ import ( "golang.org/x/exp/slices" runtimev1alpha1 "cosmossdk.io/api/cosmos/app/runtime/v1alpha1" - appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" "cosmossdk.io/core/appmodule" "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" @@ -51,7 +50,6 @@ type App struct { baseAppOptions []BaseAppOption msgServiceRouter *baseapp.MsgServiceRouter grpcQueryRouter *baseapp.GRPCQueryRouter - appConfig *appv1alpha1.Config logger log.Logger // initChainer is the init chainer function defined by the app config. // this is only required if the chain wants to add special InitChainer logic. diff --git a/runtime/module.go b/runtime/module.go index 9ab1bbf40816..b326206b368e 100644 --- a/runtime/module.go +++ b/runtime/module.go @@ -9,10 +9,16 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" runtimev1alpha1 "cosmossdk.io/api/cosmos/app/runtime/v1alpha1" +<<<<<<< HEAD appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" authmodulev1 "cosmossdk.io/api/cosmos/auth/module/v1" stakingmodulev1 "cosmossdk.io/api/cosmos/staking/module/v1" "cosmossdk.io/core/address" +======= + autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" + reflectionv1 "cosmossdk.io/api/cosmos/reflection/v1" + "cosmossdk.io/core/app" +>>>>>>> 502450cd1 (fix(runtime): remove `appv1alpha1.Config` from runtime (#21042)) "cosmossdk.io/core/appmodule" "cosmossdk.io/core/comet" "cosmossdk.io/core/event" @@ -127,6 +133,7 @@ func ProvideApp(interfaceRegistry codectypes.InterfaceRegistry) ( type AppInputs struct { depinject.In +<<<<<<< HEAD AppConfig *appv1alpha1.Config Config *runtimev1alpha1.Module AppBuilder *AppBuilder @@ -136,13 +143,21 @@ type AppInputs struct { InterfaceRegistry codectypes.InterfaceRegistry LegacyAmino *codec.LegacyAmino Logger log.Logger +======= + Logger log.Logger + Config *runtimev1alpha1.Module + AppBuilder *AppBuilder + ModuleManager *module.Manager + BaseAppOptions []BaseAppOption + InterfaceRegistry codectypes.InterfaceRegistry + LegacyAmino legacy.Amino +>>>>>>> 502450cd1 (fix(runtime): remove `appv1alpha1.Config` from runtime (#21042)) } func SetupAppBuilder(inputs AppInputs) { app := inputs.AppBuilder.app app.baseAppOptions = inputs.BaseAppOptions app.config = inputs.Config - app.appConfig = inputs.AppConfig app.logger = inputs.Logger app.ModuleManager = module.NewManagerFromMap(inputs.Modules) diff --git a/runtime/v2/app.go b/runtime/v2/app.go new file mode 100644 index 000000000000..56632590b4f9 --- /dev/null +++ b/runtime/v2/app.go @@ -0,0 +1,123 @@ +package runtime + +import ( + "encoding/json" + "errors" + + gogoproto "github.com/cosmos/gogoproto/proto" + "golang.org/x/exp/slices" + + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" + "cosmossdk.io/core/legacy" + "cosmossdk.io/core/log" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/transaction" + "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 legacy.Amino + moduleManager *MM[T] + + // GRPCQueryDecoders maps gRPC method name to a function that decodes the request + // bytes into a gogoproto.Message, which then can be passed to appmanager. + GRPCQueryDecoders map[string]func(requestBytes []byte) (gogoproto.Message, error) +} + +// 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() +} + +// LoadLatest loads the latest version. +func (a *App[T]) LoadLatest() error { + return a.db.LoadLatestVersion() +} + +// LoadHeight loads a particular height +func (a *App[T]) LoadHeight(height uint64) error { + return a.db.LoadVersion(height) +} + +// 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 +} + +// GetLogger returns the app logger. +func (a *App[T]) GetLogger() log.Logger { + return a.logger +} + +func (a *App[T]) ExecuteGenesisTx(_ []byte) error { + panic("App.ExecuteGenesisTx not supported in runtime/v2") +} + +func (a *App[T]) GetAppManager() *appmanager.AppManager[T] { + return a.AppManager +} + +func (a *App[T]) GetGRPCQueryDecoders() map[string]func(requestBytes []byte) (gogoproto.Message, error) { + return a.GRPCQueryDecoders +} diff --git a/runtime/v2/module.go b/runtime/v2/module.go new file mode 100644 index 000000000000..588584d06500 --- /dev/null +++ b/runtime/v2/module.go @@ -0,0 +1,248 @@ +package runtime + +import ( + "fmt" + "os" + "slices" + + "github.com/cosmos/gogoproto/proto" + "github.com/spf13/viper" + "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" + "cosmossdk.io/core/app" + "cosmossdk.io/core/appmodule" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/comet" + "cosmossdk.io/core/genesis" + "cosmossdk.io/core/legacy" + "cosmossdk.io/core/log" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + "cosmossdk.io/depinject/appconfig" + "cosmossdk.io/runtime/v2/services" + "cosmossdk.io/server/v2/stf" +) + +var ( + _ appmodulev2.AppModule = appModule[transaction.Tx]{} + _ appmodule.HasServices = 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], + ProvideGenesisTxHandler[transaction.Tx], + ProvideCometService, + ProvideAppVersionModifier[transaction.Tx], + ), + appconfig.Invoke(SetupAppBuilder), + ) +} + +func ProvideAppBuilder[T transaction.Tx]( + interfaceRegistrar registry.InterfaceRegistrar, + amino legacy.Amino, +) ( + *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 + } + 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 legacy.Amino + Logger log.Logger + Viper *viper.Viper `optional:"true"` +} + +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.Viper != nil { + inputs.AppBuilder.viper = inputs.Viper + } +} + +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 ProvideGenesisTxHandler[T transaction.Tx](appBuilder *AppBuilder[T]) genesis.TxHandler { + return appBuilder.app +} + +func ProvideCometService() comet.Service { + return &services.ContextAwareCometInfoService{} +} + +// ProvideAppVersionModifier returns nil, `app.VersionModifier` is a feature of BaseApp and neither used nor required for runtim/v2. +// nil is acceptable, see: https://github.com/cosmos/cosmos-sdk/blob/0a6ee406a02477ae8ccbfcbe1b51fc3930087f4c/x/upgrade/keeper/keeper.go#L438 +func ProvideAppVersionModifier[T transaction.Tx](app *AppBuilder[T]) app.VersionModifier { + return nil +}