From 4755da2533012d1792eb3e8f311619d867c1e151 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Mon, 21 Oct 2024 15:45:28 +0200 Subject: [PATCH 1/4] refactor(runtime/v2): simplify app manager (#22300) (cherry picked from commit 681366e3469ccc1b28e2ef7e2d26ab50fe4418a6) # Conflicts: # runtime/v2/app.go # runtime/v2/builder.go # runtime/v2/manager.go # runtime/v2/module.go # server/v2/api/grpc/server.go # server/v2/api/rest/handler.go # server/v2/api/rest/server.go # server/v2/appmanager/appmanager.go # server/v2/appmanager/config.go # server/v2/appmanager/genesis.go # server/v2/appmanager/stf.go # server/v2/cometbft/server.go # server/v2/server_test.go # server/v2/stf/stf.go # server/v2/store/server.go # server/v2/types.go --- runtime/v2/app.go | 103 ++++ runtime/v2/builder.go | 216 ++++++++ runtime/v2/manager.go | 861 +++++++++++++++++++++++++++++ runtime/v2/module.go | 257 +++++++++ server/v2/api/grpc/server.go | 221 ++++++++ server/v2/api/rest/handler.go | 99 ++++ server/v2/api/rest/server.go | 96 ++++ server/v2/appmanager/appmanager.go | 221 ++++++++ server/v2/appmanager/config.go | 8 + server/v2/appmanager/genesis.go | 28 + server/v2/appmanager/stf.go | 43 ++ server/v2/cometbft/abci.go | 4 +- server/v2/cometbft/abci_test.go | 17 +- server/v2/cometbft/server.go | 22 +- server/v2/server_test.go | 110 ++++ server/v2/stf/stf.go | 635 +++++++++++++++++++++ server/v2/store/server.go | 91 +++ server/v2/types.go | 26 + simapp/v2/app_di.go | 4 +- simapp/v2/app_test.go | 4 +- simapp/v2/export.go | 2 +- 21 files changed, 3049 insertions(+), 19 deletions(-) create mode 100644 runtime/v2/app.go create mode 100644 runtime/v2/builder.go create mode 100644 runtime/v2/manager.go create mode 100644 runtime/v2/module.go create mode 100644 server/v2/api/grpc/server.go create mode 100644 server/v2/api/rest/handler.go create mode 100644 server/v2/api/rest/server.go create mode 100644 server/v2/appmanager/appmanager.go create mode 100644 server/v2/appmanager/config.go create mode 100644 server/v2/appmanager/genesis.go create mode 100644 server/v2/appmanager/stf.go create mode 100644 server/v2/server_test.go create mode 100644 server/v2/stf/stf.go create mode 100644 server/v2/store/server.go create mode 100644 server/v2/types.go diff --git a/runtime/v2/app.go b/runtime/v2/app.go new file mode 100644 index 000000000000..b7887ab77f54 --- /dev/null +++ b/runtime/v2/app.go @@ -0,0 +1,103 @@ +package runtime + +import ( + "encoding/json" + + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "cosmossdk.io/schema/decoding" + "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 configuration + logger log.Logger + config *runtimev2.Module + + // state + stf *stf.STF[T] + msgRouterBuilder *stf.MsgRouterBuilder + queryRouterBuilder *stf.MsgRouterBuilder + db Store + storeLoader StoreLoader + + // modules + interfaceRegistrar registry.InterfaceRegistrar + amino registry.AminoRegistrar + moduleManager *MM[T] + queryHandlers map[string]appmodulev2.Handler // queryHandlers defines the query handlers +} + +// 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() +} + +// GetQueryHandlers returns the query handlers. +func (a *App[T]) QueryHandlers() map[string]appmodulev2.Handler { + return a.queryHandlers +} + +// SchemaDecoderResolver returns the module schema resolver. +func (a *App[T]) SchemaDecoderResolver() decoding.DecoderResolver { + moduleSet := map[string]any{} + for moduleName, module := range a.moduleManager.Modules() { + moduleSet[moduleName] = module + } + return decoding.ModuleSetDecoderResolver(moduleSet) +} + +// Close is called in start cmd to gracefully cleanup resources. +func (a *App[T]) Close() error { + return nil +} diff --git a/runtime/v2/builder.go b/runtime/v2/builder.go new file mode 100644 index 000000000000..8556e35745a8 --- /dev/null +++ b/runtime/v2/builder.go @@ -0,0 +1,216 @@ +package runtime + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + + "cosmossdk.io/core/appmodule" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/runtime/v2/services" + "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/server/v2/stf" + "cosmossdk.io/server/v2/stf/branch" + "cosmossdk.io/store/v2/root" +) + +// AppBuilder is a type that is injected into a container by the runtime/v2 module +// (as *AppBuilder) which can be used to create an app which is compatible with +// the existing app.go initialization conventions. +type AppBuilder[T transaction.Tx] struct { + app *App[T] + storeBuilder root.Builder + + // the following fields are used to overwrite the default + branch func(state store.ReaderMap) store.WriterMap + txValidator func(ctx context.Context, tx T) error + postTxExec func(ctx context.Context, tx T, success bool) error +} + +// RegisterModules registers the provided modules with the module manager. +// This is the primary hook for integrating with modules which are not registered using the app config. +func (a *AppBuilder[T]) RegisterModules(modules map[string]appmodulev2.AppModule) error { + for name, appModule := range modules { + // if a (legacy) module implements the HasName interface, check that the name matches + if mod, ok := appModule.(interface{ Name() string }); ok { + if name != mod.Name() { + a.app.logger.Warn(fmt.Sprintf("module name %q does not match name returned by HasName: %q", name, mod.Name())) + } + } + + if _, ok := a.app.moduleManager.modules[name]; ok { + return fmt.Errorf("module named %q already exists", name) + } + a.app.moduleManager.modules[name] = appModule + + if mod, ok := appModule.(appmodulev2.HasRegisterInterfaces); ok { + mod.RegisterInterfaces(a.app.interfaceRegistrar) + } + + if mod, ok := appModule.(appmodule.HasAminoCodec); ok { + mod.RegisterLegacyAminoCodec(a.app.amino) + } + } + + return nil +} + +// Build builds an *App instance. +func (a *AppBuilder[T]) Build(opts ...AppBuilderOption[T]) (*App[T], error) { + for _, opt := range opts { + opt(a) + } + + // default branch + if a.branch == nil { + a.branch = branch.DefaultNewWriterMap + } + + // default tx validator + if a.txValidator == nil { + a.txValidator = a.app.moduleManager.TxValidators() + } + + // default post tx exec + if a.postTxExec == nil { + a.postTxExec = func(ctx context.Context, tx T, success bool) error { + return nil + } + } + + a.app.db = a.storeBuilder.Get() + if a.app.db == nil { + return nil, fmt.Errorf("storeBuilder did not return a db") + } + + if err := a.app.moduleManager.RegisterServices(a.app); err != nil { + return nil, err + } + + endBlocker, valUpdate := a.app.moduleManager.EndBlock() + + stf, err := stf.New[T]( + a.app.logger.With("module", "stf"), + a.app.msgRouterBuilder, + a.app.queryRouterBuilder, + a.app.moduleManager.PreBlocker(), + a.app.moduleManager.BeginBlock(), + endBlocker, + a.txValidator, + valUpdate, + a.postTxExec, + a.branch, + ) + if err != nil { + return nil, fmt.Errorf("failed to create STF: %w", err) + } + a.app.stf = stf + + a.app.AppManager = appmanager.New[T]( + appmanager.Config{ + ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit, + QueryGasLimit: a.app.config.GasConfig.QueryGasLimit, + SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit, + }, + a.app.db, + a.app.stf, + a.initGenesis, + a.exportGenesis, + ) + + return a.app, nil +} + +// initGenesis returns the app initialization genesis for modules +func (a *AppBuilder[T]) initGenesis(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, error) { + // this implementation assumes that the state is a JSON object + bz, err := io.ReadAll(src) + if err != nil { + return nil, fmt.Errorf("failed to read import state: %w", err) + } + var genesisJSON map[string]json.RawMessage + if err = json.Unmarshal(bz, &genesisJSON); err != nil { + return nil, err + } + + v, zeroState, err := a.app.db.StateLatest() + if err != nil { + return nil, fmt.Errorf("unable to get latest state: %w", err) + } + if v != 0 { // TODO: genesis state may be > 0, we need to set version on store + return nil, errors.New("cannot init genesis on non-zero state") + } + genesisCtx := services.NewGenesisContext(a.branch(zeroState)) + genesisState, err := genesisCtx.Mutate(ctx, func(ctx context.Context) error { + err = a.app.moduleManager.InitGenesisJSON(ctx, genesisJSON, txHandler) + if err != nil { + return fmt.Errorf("failed to init genesis: %w", err) + } + return nil + }) + + return genesisState, err +} + +// exportGenesis returns the app export genesis logic for modules +func (a *AppBuilder[T]) exportGenesis(ctx context.Context, version uint64) ([]byte, error) { + state, err := a.app.db.StateAt(version) + if err != nil { + return nil, fmt.Errorf("unable to get state at given version: %w", err) + } + + genesisJson, err := a.app.moduleManager.ExportGenesisForModules( + ctx, + func() store.WriterMap { + return a.branch(state) + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to export genesis: %w", err) + } + + bz, err := json.Marshal(genesisJson) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis: %w", err) + } + + return bz, nil +} + +// AppBuilderOption is a function that can be passed to AppBuilder.Build to customize the resulting app. +type AppBuilderOption[T transaction.Tx] func(*AppBuilder[T]) + +// AppBuilderWithBranch sets a custom branch implementation for the app. +func AppBuilderWithBranch[T transaction.Tx](branch func(state store.ReaderMap) store.WriterMap) AppBuilderOption[T] { + return func(a *AppBuilder[T]) { + a.branch = branch + } +} + +// AppBuilderWithTxValidator sets the tx validator for the app. +// It overrides all default tx validators defined by modules. +func AppBuilderWithTxValidator[T transaction.Tx]( + txValidators func( + ctx context.Context, tx T, + ) error, +) AppBuilderOption[T] { + return func(a *AppBuilder[T]) { + a.txValidator = txValidators + } +} + +// AppBuilderWithPostTxExec sets logic that will be executed after each transaction. +// When not provided, a no-op function will be used. +func AppBuilderWithPostTxExec[T transaction.Tx]( + postTxExec func( + ctx context.Context, tx T, success bool, + ) error, +) AppBuilderOption[T] { + return func(a *AppBuilder[T]) { + a.postTxExec = postTxExec + } +} diff --git a/runtime/v2/manager.go b/runtime/v2/manager.go new file mode 100644 index 000000000000..e2e90c27f808 --- /dev/null +++ b/runtime/v2/manager.go @@ -0,0 +1,861 @@ +package runtime + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "reflect" + "slices" + "sort" + + gogoproto "github.com/cosmos/gogoproto/proto" + "google.golang.org/grpc" + proto "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" + cosmosmsg "cosmossdk.io/api/cosmos/msg/v1" + "cosmossdk.io/core/appmodule" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "cosmossdk.io/runtime/v2/services" + "cosmossdk.io/server/v2/stf" +) + +type MM[T transaction.Tx] struct { + logger log.Logger + config *runtimev2.Module + modules map[string]appmodulev2.AppModule + migrationRegistrar *migrationRegistrar +} + +// NewModuleManager is the constructor for the module manager +// It handles all the interactions between the modules and the application +func NewModuleManager[T transaction.Tx]( + logger log.Logger, + config *runtimev2.Module, + modules map[string]appmodulev2.AppModule, +) *MM[T] { + // good defaults for the module manager order + modulesName := slices.Sorted(maps.Keys(modules)) + if len(config.PreBlockers) == 0 { + config.PreBlockers = modulesName + } + if len(config.BeginBlockers) == 0 { + config.BeginBlockers = modulesName + } + if len(config.EndBlockers) == 0 { + config.EndBlockers = modulesName + } + if len(config.TxValidators) == 0 { + config.TxValidators = modulesName + } + if len(config.InitGenesis) == 0 { + config.InitGenesis = modulesName + } + if len(config.ExportGenesis) == 0 { + config.ExportGenesis = modulesName + } + if len(config.OrderMigrations) == 0 { + config.OrderMigrations = defaultMigrationsOrder(modulesName) + } + + mm := &MM[T]{ + logger: logger, + config: config, + modules: modules, + migrationRegistrar: newMigrationRegistrar(), + } + + if err := mm.validateConfig(); err != nil { + panic(err) + } + + return mm +} + +// Modules returns the modules registered in the module manager +func (m *MM[T]) Modules() map[string]appmodulev2.AppModule { + return m.modules +} + +// RegisterLegacyAminoCodec registers all module codecs +func (m *MM[T]) RegisterLegacyAminoCodec(registrar registry.AminoRegistrar) { + for _, b := range m.modules { + if mod, ok := b.(appmodule.HasAminoCodec); ok { + mod.RegisterLegacyAminoCodec(registrar) + } + } +} + +// RegisterInterfaces registers all module interface types +func (m *MM[T]) RegisterInterfaces(registry registry.InterfaceRegistrar) { + for _, b := range m.modules { + if mod, ok := b.(appmodulev2.HasRegisterInterfaces); ok { + mod.RegisterInterfaces(registry) + } + } +} + +// DefaultGenesis provides default genesis information for all modules +func (m *MM[T]) DefaultGenesis() map[string]json.RawMessage { + genesisData := make(map[string]json.RawMessage) + for name, b := range m.modules { + if mod, ok := b.(appmodule.HasGenesisBasics); ok { + genesisData[name] = mod.DefaultGenesis() + } else if mod, ok := b.(appmodulev2.HasGenesis); ok { + genesisData[name] = mod.DefaultGenesis() + } else { + genesisData[name] = []byte("{}") + } + } + + return genesisData +} + +// ValidateGenesis performs genesis state validation for all modules +func (m *MM[T]) ValidateGenesis(genesisData map[string]json.RawMessage) error { + for name, b := range m.modules { + if mod, ok := b.(appmodule.HasGenesisBasics); ok { + if err := mod.ValidateGenesis(genesisData[name]); err != nil { + return err + } + } else if mod, ok := b.(appmodulev2.HasGenesis); ok { + if err := mod.ValidateGenesis(genesisData[name]); err != nil { + return err + } + } + } + + return nil +} + +// InitGenesisJSON performs init genesis functionality for modules from genesis data in JSON format +func (m *MM[T]) InitGenesisJSON( + ctx context.Context, + genesisData map[string]json.RawMessage, + txHandler func(json.RawMessage) error, +) error { + m.logger.Info("initializing blockchain state from genesis.json", "order", m.config.InitGenesis) + var seenValUpdates bool + for _, moduleName := range m.config.InitGenesis { + if genesisData[moduleName] == nil { + continue + } + + mod := m.modules[moduleName] + + // we might get an adapted module, a native core API module or a legacy module + switch module := mod.(type) { + case appmodule.HasGenesisAuto: + panic(fmt.Sprintf("module %s isn't server/v2 compatible", moduleName)) + case appmodulev2.GenesisDecoder: // GenesisDecoder needs to supersede HasGenesis and HasABCIGenesis. + genTxs, err := module.DecodeGenesisJSON(genesisData[moduleName]) + if err != nil { + return err + } + for _, jsonTx := range genTxs { + if err := txHandler(jsonTx); err != nil { + return fmt.Errorf("failed to handle genesis transaction: %w", err) + } + } + case appmodulev2.HasGenesis: + m.logger.Debug("running initialization for module", "module", moduleName) + if err := module.InitGenesis(ctx, genesisData[moduleName]); err != nil { + return fmt.Errorf("init module %s: %w", moduleName, err) + } + case appmodulev2.HasABCIGenesis: + m.logger.Debug("running initialization for module", "module", moduleName) + moduleValUpdates, err := module.InitGenesis(ctx, genesisData[moduleName]) + if err != nil { + return err + } + + // use these validator updates if provided, the module manager assumes + // only one module will update the validator set + if len(moduleValUpdates) > 0 { + if seenValUpdates { + return fmt.Errorf("validator InitGenesis updates already set by a previous module: current module %s", moduleName) + } else { + seenValUpdates = true + } + } + } + + } + return nil +} + +// ExportGenesisForModules performs export genesis functionality for modules +func (m *MM[T]) ExportGenesisForModules( + ctx context.Context, + stateFactory func() store.WriterMap, + modulesToExport ...string, +) (map[string]json.RawMessage, error) { + if len(modulesToExport) == 0 { + modulesToExport = m.config.ExportGenesis + } + // verify modules exists in app, so that we don't panic in the middle of an export + if err := m.checkModulesExists(modulesToExport); err != nil { + return nil, err + } + + type genesisResult struct { + bz json.RawMessage + err error + } + + type ModuleI interface { + ExportGenesis(ctx context.Context) (json.RawMessage, error) + } + + channels := make(map[string]chan genesisResult) + for _, moduleName := range modulesToExport { + mod := m.modules[moduleName] + var moduleI ModuleI + if module, hasGenesis := mod.(appmodulev2.HasGenesis); hasGenesis { + moduleI = module.(ModuleI) + } else if module, hasABCIGenesis := mod.(appmodulev2.HasABCIGenesis); hasABCIGenesis { + moduleI = module.(ModuleI) + } else { + continue + } + + channels[moduleName] = make(chan genesisResult) + go func(moduleI ModuleI, ch chan genesisResult) { + genesisCtx := services.NewGenesisContext(stateFactory()) + err := genesisCtx.Read(ctx, func(ctx context.Context) error { + jm, err := moduleI.ExportGenesis(ctx) + if err != nil { + return err + } + ch <- genesisResult{jm, nil} + return nil + }) + if err != nil { + ch <- genesisResult{nil, err} + } + }(moduleI, channels[moduleName]) + } + + genesisData := make(map[string]json.RawMessage) + for moduleName := range channels { + res := <-channels[moduleName] + if res.err != nil { + return nil, fmt.Errorf("genesis export error in %s: %w", moduleName, res.err) + } + + genesisData[moduleName] = res.bz + } + + return genesisData, nil +} + +// checkModulesExists verifies that all modules in the list exist in the app +func (m *MM[T]) checkModulesExists(moduleName []string) error { + for _, name := range moduleName { + if _, ok := m.modules[name]; !ok { + return fmt.Errorf("module %s does not exist", name) + } + } + + return nil +} + +// BeginBlock runs the begin-block logic of all modules +func (m *MM[T]) BeginBlock() func(ctx context.Context) error { + return func(ctx context.Context) error { + for _, moduleName := range m.config.BeginBlockers { + if module, ok := m.modules[moduleName].(appmodulev2.HasBeginBlocker); ok { + if err := module.BeginBlock(ctx); err != nil { + return fmt.Errorf("failed to run beginblocker for %s: %w", moduleName, err) + } + } + } + + return nil + } +} + +// hasABCIEndBlock is the legacy EndBlocker implemented by x/staking in the CosmosSDK +type hasABCIEndBlock interface { + EndBlock(context.Context) ([]appmodulev2.ValidatorUpdate, error) +} + +// EndBlock runs the end-block logic of all modules and tx validator updates +func (m *MM[T]) EndBlock() ( + endBlockFunc func(ctx context.Context) error, + valUpdateFunc func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error), +) { + var validatorUpdates []appmodulev2.ValidatorUpdate + endBlockFunc = func(ctx context.Context) error { + for _, moduleName := range m.config.EndBlockers { + if module, ok := m.modules[moduleName].(appmodulev2.HasEndBlocker); ok { + err := module.EndBlock(ctx) + if err != nil { + return fmt.Errorf("failed to run endblock for %s: %w", moduleName, err) + } + } else if module, ok := m.modules[moduleName].(hasABCIEndBlock); ok { // we need to keep this for our module compatibility promise + moduleValUpdates, err := module.EndBlock(ctx) + if err != nil { + return fmt.Errorf("failed to run enblock for %s: %w", moduleName, err) + } + // use these validator updates if provided, the module manager assumes + // only one module will update the validator set + if len(moduleValUpdates) > 0 { + if len(validatorUpdates) > 0 { + return errors.New("validator end block updates already set by a previous module") + } + + validatorUpdates = append(validatorUpdates, moduleValUpdates...) + } + } + } + + return nil + } + + valUpdateFunc = func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) { + // get validator updates of modules implementing directly the new HasUpdateValidators interface + for _, v := range m.modules { + if module, ok := v.(appmodulev2.HasUpdateValidators); ok { + moduleValUpdates, err := module.UpdateValidators(ctx) + if err != nil { + return nil, err + } + + if len(moduleValUpdates) > 0 { + if len(validatorUpdates) > 0 { + return nil, errors.New("validator end block updates already set by a previous module") + } + + validatorUpdates = append(validatorUpdates, moduleValUpdates...) + } + } + } + + // Reset validatorUpdates + res := validatorUpdates + validatorUpdates = []appmodulev2.ValidatorUpdate{} + + return res, nil + } + + return endBlockFunc, valUpdateFunc +} + +// PreBlocker runs the pre-block logic of all modules +func (m *MM[T]) PreBlocker() func(ctx context.Context, txs []T) error { + return func(ctx context.Context, txs []T) error { + for _, moduleName := range m.config.PreBlockers { + if module, ok := m.modules[moduleName].(appmodulev2.HasPreBlocker); ok { + if err := module.PreBlock(ctx); err != nil { + return fmt.Errorf("failed to run preblock for %s: %w", moduleName, err) + } + } + } + + return nil + } +} + +// TxValidators validates incoming transactions +func (m *MM[T]) TxValidators() func(ctx context.Context, tx T) error { + return func(ctx context.Context, tx T) error { + for _, moduleName := range m.config.TxValidators { + if module, ok := m.modules[moduleName].(appmodulev2.HasTxValidator[T]); ok { + if err := module.TxValidator(ctx, tx); err != nil { + return fmt.Errorf("failed to run tx validator for %s: %w", moduleName, err) + } + } + } + + return nil + } +} + +// RunMigrations performs in-place store migrations for all modules. This +// function MUST be called inside an x/upgrade UpgradeHandler. +// +// Recall that in an upgrade handler, the `fromVM` VersionMap is retrieved from +// x/upgrade's store, and the function needs to return the target VersionMap +// that will in turn be persisted to the x/upgrade's store. In general, +// returning RunMigrations should be enough: +// +// Example: +// +// app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM appmodule.VersionMap) (appmodule.VersionMap, error) { +// return app.ModuleManager().RunMigrations(ctx, fromVM) +// }) +// +// Internally, RunMigrations will perform the following steps: +// - create an `updatedVM` VersionMap of module with their latest ConsensusVersion +// - if module implements `HasConsensusVersion` interface get the consensus version as `toVersion`, +// if not `toVersion` is set to 0. +// - get `fromVersion` from `fromVM` with module's name. +// - if the module's name exists in `fromVM` map, then run in-place store migrations +// for that module between `fromVersion` and `toVersion`. +// - if the module does not exist in the `fromVM` (which means that it's a new module, +// because it was not in the previous x/upgrade's store), then run +// `InitGenesis` on that module. +// +// - return the `updatedVM` to be persisted in the x/upgrade's store. +// +// Migrations are run in an order defined by `mm.config.OrderMigrations`. +// +// As an app developer, if you wish to skip running InitGenesis for your new +// module "foo", you need to manually pass a `fromVM` argument to this function +// foo's module version set to its latest ConsensusVersion. That way, the diff +// between the function's `fromVM` and `udpatedVM` will be empty, hence not +// running anything for foo. +// +// Example: +// +// app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { +// // Assume "foo" is a new module. +// // `fromVM` is fetched from existing x/upgrade store. Since foo didn't exist +// // before this upgrade, `v, exists := fromVM["foo"]; exists == false`, and RunMigration will by default +// // run InitGenesis on foo. +// // To skip running foo's InitGenesis, you need set `fromVM`'s foo to its latest +// // consensus version: +// fromVM["foo"] = foo.AppModule{}.ConsensusVersion() +// +// return app.ModuleManager().RunMigrations(ctx, fromVM) +// }) +// +// Please also refer to https://docs.cosmos.network/main/core/upgrade for more information. +func (m *MM[T]) RunMigrations(ctx context.Context, fromVM appmodulev2.VersionMap) (appmodulev2.VersionMap, error) { + updatedVM := appmodulev2.VersionMap{} + for _, moduleName := range m.config.OrderMigrations { + module := m.modules[moduleName] + fromVersion, exists := fromVM[moduleName] + toVersion := uint64(0) + if module, ok := module.(appmodulev2.HasConsensusVersion); ok { + toVersion = module.ConsensusVersion() + } + + // We run migration if the module is specified in `fromVM`. + // Otherwise we run InitGenesis. + // + // The module won't exist in the fromVM in two cases: + // 1. A new module is added. In this case we run InitGenesis with an + // empty genesis state. + // 2. An existing chain is upgrading from version < 0.43 to v0.43+ for the first time. + // In this case, all modules have yet to be added to x/upgrade's VersionMap store. + if exists { + m.logger.Info(fmt.Sprintf("migrating module %s from version %d to version %d", moduleName, fromVersion, toVersion)) + if err := m.migrationRegistrar.RunModuleMigrations(ctx, moduleName, fromVersion, toVersion); err != nil { + return nil, err + } + } else { + m.logger.Info(fmt.Sprintf("adding a new module: %s", moduleName)) + if mod, ok := m.modules[moduleName].(appmodule.HasGenesis); ok { + if err := mod.InitGenesis(ctx, mod.DefaultGenesis()); err != nil { + return nil, fmt.Errorf("failed to run InitGenesis for %s: %w", moduleName, err) + } + } + if mod, ok := m.modules[moduleName].(appmodulev2.HasABCIGenesis); ok { + moduleValUpdates, err := mod.InitGenesis(ctx, mod.DefaultGenesis()) + if err != nil { + return nil, err + } + + // The module manager assumes only one module will update the validator set, and it can't be a new module. + if len(moduleValUpdates) > 0 { + return nil, errors.New("validator InitGenesis update is already set by another module") + } + } + } + + updatedVM[moduleName] = toVersion + } + + return updatedVM, nil +} + +// RegisterServices registers all module services. +func (m *MM[T]) RegisterServices(app *App[T]) error { + for _, module := range m.modules { + // register msg + query + if err := registerServices(module, app, protoregistry.GlobalFiles); err != nil { + return err + } + + // register migrations + if module, ok := module.(appmodulev2.HasMigrations); ok { + if err := module.RegisterMigrations(m.migrationRegistrar); err != nil { + return err + } + } + + // register pre and post msg + if module, ok := module.(appmodulev2.HasPreMsgHandlers); ok { + module.RegisterPreMsgHandlers(app.msgRouterBuilder) + } + + if module, ok := module.(appmodulev2.HasPostMsgHandlers); ok { + module.RegisterPostMsgHandlers(app.msgRouterBuilder) + } + } + + return nil +} + +// validateConfig validates the module manager configuration +// it asserts that all modules are defined in the configuration and that no modules are forgotten +func (m *MM[T]) validateConfig() error { + if err := m.assertNoForgottenModules("PreBlockers", m.config.PreBlockers, func(moduleName string) bool { + module := m.modules[moduleName] + _, hasPreBlock := module.(appmodulev2.HasPreBlocker) + return !hasPreBlock + }); err != nil { + return err + } + + if err := m.assertNoForgottenModules("BeginBlockers", m.config.BeginBlockers, func(moduleName string) bool { + module := m.modules[moduleName] + _, hasBeginBlock := module.(appmodulev2.HasBeginBlocker) + return !hasBeginBlock + }); err != nil { + return err + } + + if err := m.assertNoForgottenModules("EndBlockers", m.config.EndBlockers, func(moduleName string) bool { + module := m.modules[moduleName] + if _, hasEndBlock := module.(appmodulev2.HasEndBlocker); hasEndBlock { + return !hasEndBlock + } + + _, hasABCIEndBlock := module.(hasABCIEndBlock) + return !hasABCIEndBlock + }); err != nil { + return err + } + + if err := m.assertNoForgottenModules("TxValidators", m.config.TxValidators, func(moduleName string) bool { + module := m.modules[moduleName] + _, hasTxValidator := module.(appmodulev2.HasTxValidator[T]) + return !hasTxValidator + }); err != nil { + return err + } + + if err := m.assertNoForgottenModules("InitGenesis", m.config.InitGenesis, func(moduleName string) bool { + module := m.modules[moduleName] + if _, hasGenesis := module.(appmodule.HasGenesisAuto); hasGenesis { + panic(fmt.Sprintf("module %s isn't server/v2 compatible", moduleName)) + } + + if _, hasGenesis := module.(appmodulev2.HasGenesis); hasGenesis { + return !hasGenesis + } + + _, hasABCIGenesis := module.(appmodulev2.HasABCIGenesis) + return !hasABCIGenesis + }); err != nil { + return err + } + + if err := m.assertNoForgottenModules("ExportGenesis", m.config.ExportGenesis, func(moduleName string) bool { + module := m.modules[moduleName] + if _, hasGenesis := module.(appmodule.HasGenesisAuto); hasGenesis { + panic(fmt.Sprintf("module %s isn't server/v2 compatible", moduleName)) + } + + if _, hasGenesis := module.(appmodulev2.HasGenesis); hasGenesis { + return !hasGenesis + } + + _, hasABCIGenesis := module.(appmodulev2.HasABCIGenesis) + return !hasABCIGenesis + }); err != nil { + return err + } + + if err := m.assertNoForgottenModules("OrderMigrations", m.config.OrderMigrations, nil); err != nil { + return err + } + + return nil +} + +// assertNoForgottenModules checks that we didn't forget any modules in the *runtimev2.Module config. +// `pass` is a closure which allows one to omit modules from `moduleNames`. +// If you provide non-nil `pass` and it returns true, the module would not be subject of the assertion. +func (m *MM[T]) assertNoForgottenModules( + setOrderFnName string, + moduleNames []string, + pass func(moduleName string) bool, +) error { + ms := make(map[string]bool) + for _, m := range moduleNames { + ms[m] = true + } + var missing []string + for m := range m.modules { + if pass != nil && pass(m) { + continue + } + + if !ms[m] { + missing = append(missing, m) + } + } + + if len(missing) != 0 { + sort.Strings(missing) + return fmt.Errorf("all modules must be defined when setting %s, missing: %v", setOrderFnName, missing) + } + + return nil +} + +func registerServices[T transaction.Tx](s appmodulev2.AppModule, app *App[T], registry *protoregistry.Files) error { + // case module with services + if services, ok := s.(hasServicesV1); ok { + c := &configurator{ + queryHandlers: map[string]appmodulev2.Handler{}, + stfQueryRouter: app.queryRouterBuilder, + stfMsgRouter: app.msgRouterBuilder, + registry: registry, + err: nil, + } + if err := services.RegisterServices(c); err != nil { + return fmt.Errorf("unable to register services: %w", err) + } + + if c.err != nil { + app.logger.Warn("error registering services", "error", c.err) + } + + // merge maps + for path, decoder := range c.queryHandlers { + app.queryHandlers[path] = decoder + } + } + + // if module implements register msg handlers + if module, ok := s.(appmodulev2.HasMsgHandlers); ok { + wrapper := stfRouterWrapper{stfRouter: app.msgRouterBuilder} + module.RegisterMsgHandlers(&wrapper) + if wrapper.error != nil { + return fmt.Errorf("unable to register handlers: %w", wrapper.error) + } + } + + // if module implements register query handlers + if module, ok := s.(appmodulev2.HasQueryHandlers); ok { + wrapper := stfRouterWrapper{stfRouter: app.queryRouterBuilder} + module.RegisterQueryHandlers(&wrapper) + + for path, handler := range wrapper.handlers { + app.queryHandlers[path] = handler + } + } + + return nil +} + +var _ grpc.ServiceRegistrar = (*configurator)(nil) + +type configurator struct { + queryHandlers map[string]appmodulev2.Handler + + stfQueryRouter *stf.MsgRouterBuilder + stfMsgRouter *stf.MsgRouterBuilder + registry *protoregistry.Files + err error +} + +func (c *configurator) RegisterService(sd *grpc.ServiceDesc, ss interface{}) { + // first we check if it's a msg server + prefSd, err := c.registry.FindDescriptorByName(protoreflect.FullName(sd.ServiceName)) + if err != nil { + c.err = fmt.Errorf("register service: unable to find protov2 service descriptor: please make sure protov2 API counterparty is imported: %s", sd.ServiceName) + return + } + + if !proto.HasExtension(prefSd.(protoreflect.ServiceDescriptor).Options(), cosmosmsg.E_Service) { + err = c.registerQueryHandlers(sd, ss) + if err != nil { + c.err = err + } + } else { + err = c.registerMsgHandlers(sd, ss) + if err != nil { + c.err = err + } + } +} + +func (c *configurator) registerQueryHandlers(sd *grpc.ServiceDesc, ss interface{}) error { + for _, md := range sd.Methods { + // TODO(tip): what if a query is not deterministic? + + handler, err := grpcHandlerToAppModuleHandler(sd, md, ss) + if err != nil { + return fmt.Errorf("unable to make a appmodulev2.HandlerFunc from gRPC handler (%s, %s): %w", sd.ServiceName, md.MethodName, err) + } + + // register to stf query router. + err = c.stfQueryRouter.RegisterHandler(gogoproto.MessageName(handler.MakeMsg()), handler.Func) + if err != nil { + return fmt.Errorf("unable to register handler to stf router (%s, %s): %w", sd.ServiceName, md.MethodName, err) + } + + // register query handler using the same mapping used in stf + c.queryHandlers[gogoproto.MessageName(handler.MakeMsg())] = handler + } + return nil +} + +func (c *configurator) registerMsgHandlers(sd *grpc.ServiceDesc, ss interface{}) error { + for _, md := range sd.Methods { + handler, err := grpcHandlerToAppModuleHandler(sd, md, ss) + if err != nil { + return err + } + err = c.stfMsgRouter.RegisterHandler(gogoproto.MessageName(handler.MakeMsg()), handler.Func) + if err != nil { + return fmt.Errorf("unable to register msg handler %s.%s: %w", sd.ServiceName, md.MethodName, err) + } + } + return nil +} + +// grpcHandlerToAppModuleHandler converts a gRPC handler into an appmodulev2.HandlerFunc. +func grpcHandlerToAppModuleHandler( + sd *grpc.ServiceDesc, + md grpc.MethodDesc, + ss interface{}, +) (appmodulev2.Handler, error) { + requestName, responseName, err := requestFullNameFromMethodDesc(sd, md) + if err != nil { + return appmodulev2.Handler{}, err + } + + requestTyp := gogoproto.MessageType(string(requestName)) + if requestTyp == nil { + return appmodulev2.Handler{}, fmt.Errorf("no proto message found for %s", requestName) + } + responseTyp := gogoproto.MessageType(string(responseName)) + if responseTyp == nil { + return appmodulev2.Handler{}, fmt.Errorf("no proto message found for %s", responseName) + } + + handlerFunc := func( + ctx context.Context, + msg transaction.Msg, + ) (resp transaction.Msg, err error) { + res, err := md.Handler(ss, ctx, noopDecoder, messagePassingInterceptor(msg)) + if err != nil { + return nil, err + } + return res.(transaction.Msg), nil + } + + return appmodulev2.Handler{ + Func: handlerFunc, + MakeMsg: func() transaction.Msg { + return reflect.New(requestTyp.Elem()).Interface().(transaction.Msg) + }, + MakeMsgResp: func() transaction.Msg { + return reflect.New(responseTyp.Elem()).Interface().(transaction.Msg) + }, + }, nil +} + +func noopDecoder(_ interface{}) error { return nil } + +func messagePassingInterceptor(msg transaction.Msg) grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (interface{}, error) { + return handler(ctx, msg) + } +} + +// requestFullNameFromMethodDesc returns the fully-qualified name of the request message and response of the provided service's method. +func requestFullNameFromMethodDesc(sd *grpc.ServiceDesc, method grpc.MethodDesc) ( + protoreflect.FullName, protoreflect.FullName, error, +) { + methodFullName := protoreflect.FullName(fmt.Sprintf("%s.%s", sd.ServiceName, method.MethodName)) + desc, err := gogoproto.HybridResolver.FindDescriptorByName(methodFullName) + if err != nil { + return "", "", fmt.Errorf("cannot find method descriptor %s", methodFullName) + } + methodDesc, ok := desc.(protoreflect.MethodDescriptor) + if !ok { + return "", "", fmt.Errorf("invalid method descriptor %s", methodFullName) + } + return methodDesc.Input().FullName(), methodDesc.Output().FullName(), nil +} + +// defaultMigrationsOrder returns a default migrations order: ascending alphabetical by module name, +// except x/auth which will run last, see: +// https://github.com/cosmos/cosmos-sdk/issues/10591 +func defaultMigrationsOrder(modules []string) []string { + const authName = "auth" + out := make([]string, 0, len(modules)) + hasAuth := false + for _, m := range modules { + if m == authName { + hasAuth = true + } else { + out = append(out, m) + } + } + sort.Strings(out) + if hasAuth { + out = append(out, authName) + } + return out +} + +// hasServicesV1 is the interface for registering service in baseapp Cosmos SDK. +// This API is part of core/appmodule but commented out for dependencies. +type hasServicesV1 interface { + RegisterServices(grpc.ServiceRegistrar) error +} + +var _ appmodulev2.MsgRouter = (*stfRouterWrapper)(nil) + +// stfRouterWrapper wraps the stf router and implements the core appmodulev2.MsgRouter +// interface. +// The difference between this type and stf router is that the stf router expects +// us to provide it the msg name, but the core router interface does not have +// such requirement. +type stfRouterWrapper struct { + stfRouter *stf.MsgRouterBuilder + + error error + + handlers map[string]appmodulev2.Handler +} + +func (s *stfRouterWrapper) RegisterHandler(handler appmodulev2.Handler) { + req := handler.MakeMsg() + requestName := gogoproto.MessageName(req) + if requestName == "" { + s.error = errors.Join(s.error, fmt.Errorf("unable to extract request name for type: %T", req)) + } + + // register handler to stf router + err := s.stfRouter.RegisterHandler(requestName, handler.Func) + s.error = errors.Join(s.error, err) + + // also make the decoder + if s.error == nil { + s.handlers = map[string]appmodulev2.Handler{} + } + s.handlers[requestName] = handler +} diff --git a/runtime/v2/module.go b/runtime/v2/module.go new file mode 100644 index 000000000000..54d77dc2742f --- /dev/null +++ b/runtime/v2/module.go @@ -0,0 +1,257 @@ +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/event" + "cosmossdk.io/core/header" + "cosmossdk.io/core/registry" + "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" + "cosmossdk.io/store/v2/root" +) + +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(registrar grpc.ServiceRegistrar) error { + autoCliQueryService, err := services.NewAutoCLIQueryService(m.app.moduleManager.modules) + if err != nil { + return err + } + + autocliv1.RegisterQueryServer(registrar, autoCliQueryService) + + reflectionSvc, err := services.NewReflectionService() + if err != nil { + return err + } + reflectionv1.RegisterReflectionServiceServer(registrar, 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], + ProvideModuleManager[transaction.Tx], + ProvideEnvironment, + ProvideKVService, + ), + appconfig.Invoke(SetupAppBuilder), + ) +} + +func ProvideAppBuilder[T transaction.Tx]( + interfaceRegistrar registry.InterfaceRegistrar, + amino registry.AminoRegistrar, + storeBuilder root.Builder, +) ( + *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]{ + interfaceRegistrar: interfaceRegistrar, + amino: amino, + msgRouterBuilder: msgRouterBuilder, + queryRouterBuilder: stf.NewMsgRouterBuilder(), // TODO dedicated query router + queryHandlers: map[string]appmodulev2.Handler{}, + storeLoader: DefaultStoreLoader, + } + appBuilder := &AppBuilder[T]{app: app, storeBuilder: storeBuilder} + + 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 + StoreBuilder root.Builder +} + +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) + // STF requires some state to run + inputs.StoreBuilder.RegisterKey("stf") +} + +func ProvideModuleManager[T transaction.Tx]( + logger log.Logger, + config *runtimev2.Module, + modules map[string]appmodulev2.AppModule, +) *MM[T] { + return NewModuleManager[T](logger, config, modules) +} + +func ProvideKVService( + config *runtimev2.Module, + key depinject.ModuleKey, + kvFactory store.KVStoreServiceFactory, + storeBuilder root.Builder, +) (store.KVStoreService, store.MemoryStoreService) { + // skips modules that have no store + if slices.Contains(config.SkipStoreKeys, key.Name()) { + return &failingStoreService{}, &failingStoreService{} + } + var kvStoreKey string + override := storeKeyOverride(config, key.Name()) + if override != nil { + kvStoreKey = override.KvStoreKey + } else { + kvStoreKey = key.Name() + } + + storeBuilder.RegisterKey(kvStoreKey) + return kvFactory([]byte(kvStoreKey)), stf.NewMemoryStoreService([]byte(fmt.Sprintf("memory:%s", kvStoreKey))) +} + +func storeKeyOverride(config *runtimev2.Module, moduleName string) *runtimev2.StoreKeyConfig { + for _, cfg := range config.OverrideStoreKeys { + if cfg.ModuleName == moduleName { + return cfg + } + } + return nil +} + +// ProvideEnvironment provides the environment for keeper modules, while maintaining backward compatibility and provide services directly as well. +func ProvideEnvironment( + logger log.Logger, + key depinject.ModuleKey, + kvService store.KVStoreService, + memKvService store.MemoryStoreService, + headerService header.Service, + eventService event.Service, +) appmodulev2.Environment { + return appmodulev2.Environment{ + Logger: logger, + BranchService: stf.BranchService{}, + EventService: eventService, + GasService: stf.NewGasMeterService(), + HeaderService: headerService, + QueryRouterService: stf.NewQueryRouterService(), + MsgRouterService: stf.NewMsgRouterService([]byte(key.Name())), + TransactionService: services.NewContextAwareTransactionService(), + KVStoreService: kvService, + MemStoreService: memKvService, + } +} + +// DefaultServiceBindings provides default services for the following service interfaces: +// - store.KVStoreServiceFactory +// - header.Service +// - comet.Service +// - event.Service +// - store/v2/root.Builder +// +// They are all required. For most use cases these default services bindings should be sufficient. +// Power users (or tests) may wish to provide their own services bindings, in which case they must +// supply implementations for each of the above interfaces. +func DefaultServiceBindings() depinject.Config { + var ( + kvServiceFactory store.KVStoreServiceFactory = func(actor []byte) store.KVStoreService { + return services.NewGenesisKVService( + actor, + stf.NewKVStoreService(actor), + ) + } + cometService comet.Service = &services.ContextAwareCometInfoService{} + headerService = services.NewGenesisHeaderService(stf.HeaderService{}) + eventService = services.NewGenesisEventService(stf.NewEventService()) + storeBuilder = root.NewBuilder() + ) + return depinject.Supply( + kvServiceFactory, + headerService, + cometService, + eventService, + storeBuilder, + ) +} diff --git a/server/v2/api/grpc/server.go b/server/v2/api/grpc/server.go new file mode 100644 index 000000000000..10e22a514a1e --- /dev/null +++ b/server/v2/api/grpc/server.go @@ -0,0 +1,221 @@ +package grpc + +import ( + "context" + "errors" + "fmt" + "io" + "maps" + "net" + "slices" + "strconv" + "strings" + "sync" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/spf13/pflag" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/reflect/protoreflect" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + serverv2 "cosmossdk.io/server/v2" + "cosmossdk.io/server/v2/api/grpc/gogoreflection" +) + +const ( + ServerName = "grpc" + + BlockHeightHeader = "x-cosmos-block-height" +) + +type Server[T transaction.Tx] struct { + logger log.Logger + config *Config + cfgOptions []CfgOption + + grpcSrv *grpc.Server +} + +// New creates a new grpc server. +func New[T transaction.Tx](cfgOptions ...CfgOption) *Server[T] { + return &Server[T]{ + cfgOptions: cfgOptions, + } +} + +// Init returns a correctly configured and initialized gRPC server. +// Note, the caller is responsible for starting the server. +func (s *Server[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logger log.Logger) error { + serverCfg := s.Config().(*Config) + if len(cfg) > 0 { + if err := serverv2.UnmarshalSubConfig(cfg, s.Name(), &serverCfg); err != nil { + return fmt.Errorf("failed to unmarshal config: %w", err) + } + } + methodsMap := appI.QueryHandlers() + + grpcSrv := grpc.NewServer( + grpc.ForceServerCodec(newProtoCodec(appI.InterfaceRegistry()).GRPCCodec()), + grpc.MaxSendMsgSize(serverCfg.MaxSendMsgSize), + grpc.MaxRecvMsgSize(serverCfg.MaxRecvMsgSize), + grpc.UnknownServiceHandler( + makeUnknownServiceHandler(methodsMap, appI), + ), + ) + + // Reflection allows external clients to see what services and methods the gRPC server exposes. + gogoreflection.Register(grpcSrv, slices.Collect(maps.Keys(methodsMap)), logger.With("sub-module", "grpc-reflection")) + + s.grpcSrv = grpcSrv + s.config = serverCfg + s.logger = logger.With(log.ModuleKey, s.Name()) + + return nil +} + +func (s *Server[T]) StartCmdFlags() *pflag.FlagSet { + flags := pflag.NewFlagSet(s.Name(), pflag.ExitOnError) + flags.String(FlagAddress, "localhost:9090", "Listen address") + return flags +} + +func makeUnknownServiceHandler(handlers map[string]appmodulev2.Handler, querier interface { + Query(ctx context.Context, version uint64, msg transaction.Msg) (transaction.Msg, error) +}, +) grpc.StreamHandler { + getRegistry := sync.OnceValues(gogoproto.MergedRegistry) + + return func(srv any, stream grpc.ServerStream) error { + method, ok := grpc.MethodFromServerStream(stream) + if !ok { + return status.Error(codes.InvalidArgument, "unable to get method") + } + // if this fails we cannot serve queries anymore... + registry, err := getRegistry() + if err != nil { + return fmt.Errorf("failed to get registry: %w", err) + } + + method = strings.TrimPrefix(method, "/") + fullName := protoreflect.FullName(strings.ReplaceAll(method, "/", ".")) + // get descriptor from the invoke method + desc, err := registry.FindDescriptorByName(fullName) + if err != nil { + return fmt.Errorf("failed to find descriptor %s: %w", method, err) + } + md, ok := desc.(protoreflect.MethodDescriptor) + if !ok { + return fmt.Errorf("%s is not a method", method) + } + // find handler + handler, exists := handlers[string(md.Input().FullName())] + if !exists { + return status.Errorf(codes.Unimplemented, "gRPC method %s is not handled", method) + } + + for { + req := handler.MakeMsg() + err := stream.RecvMsg(req) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + + // extract height header + ctx := stream.Context() + height, err := getHeightFromCtx(ctx) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid get height from context: %v", err) + } + resp, err := querier.Query(ctx, height, req) + if err != nil { + return err + } + err = stream.SendMsg(resp) + if err != nil { + return err + } + } + } +} + +func getHeightFromCtx(ctx context.Context) (uint64, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return 0, nil + } + values := md.Get(BlockHeightHeader) + if len(values) == 0 { + return 0, nil + } + if len(values) != 1 { + return 0, fmt.Errorf("gRPC height metadata must be of length 1, got: %d", len(values)) + } + + heightStr := values[0] + height, err := strconv.ParseUint(heightStr, 10, 64) + if err != nil { + return 0, fmt.Errorf("unable to parse height string from gRPC metadata %s: %w", heightStr, err) + } + + return height, nil +} + +func (s *Server[T]) Name() string { + return ServerName +} + +func (s *Server[T]) Config() any { + if s.config == nil || s.config.Address == "" { + cfg := DefaultConfig() + // overwrite the default config with the provided options + for _, opt := range s.cfgOptions { + opt(cfg) + } + + return cfg + } + + return s.config +} + +func (s *Server[T]) Start(ctx context.Context) error { + if !s.config.Enable { + s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name())) + return nil + } + + listener, err := net.Listen("tcp", s.config.Address) + if err != nil { + return fmt.Errorf("failed to listen on address %s: %w", s.config.Address, err) + } + + s.logger.Info("starting gRPC server...", "address", s.config.Address) + if err := s.grpcSrv.Serve(listener); err != nil { + return fmt.Errorf("failed to start gRPC server: %w", err) + } + + return nil +} + +func (s *Server[T]) Stop(ctx context.Context) error { + if !s.config.Enable { + return nil + } + + s.logger.Info("stopping gRPC server...", "address", s.config.Address) + s.grpcSrv.GracefulStop() + return nil +} + +// GetGRPCServer returns the underlying gRPC server. +func (s *Server[T]) GetGRPCServer() *grpc.Server { + return s.grpcSrv +} diff --git a/server/v2/api/rest/handler.go b/server/v2/api/rest/handler.go new file mode 100644 index 000000000000..8159e23ba3bb --- /dev/null +++ b/server/v2/api/rest/handler.go @@ -0,0 +1,99 @@ +package rest + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strings" + + "github.com/cosmos/gogoproto/jsonpb" + gogoproto "github.com/cosmos/gogoproto/proto" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/server/v2/appmanager" +) + +const ( + ContentTypeJSON = "application/json" + MaxBodySize = 1 << 20 // 1 MB +) + +func NewDefaultHandler[T transaction.Tx](appManager appmanager.AppManager[T]) http.Handler { + return &DefaultHandler[T]{appManager: appManager} +} + +type DefaultHandler[T transaction.Tx] struct { + appManager appmanager.AppManager[T] +} + +func (h *DefaultHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := h.validateMethodIsPOST(r); err != nil { + http.Error(w, err.Error(), http.StatusMethodNotAllowed) + return + } + + if err := h.validateContentTypeIsJSON(r); err != nil { + http.Error(w, err.Error(), http.StatusUnsupportedMediaType) + return + } + + msg, err := h.createMessage(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + query, err := h.appManager.Query(r.Context(), 0, msg) + if err != nil { + http.Error(w, "Error querying", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", ContentTypeJSON) + if err := json.NewEncoder(w).Encode(query); err != nil { + http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) + } +} + +// validateMethodIsPOST validates that the request method is POST. +func (h *DefaultHandler[T]) validateMethodIsPOST(r *http.Request) error { + if r.Method != http.MethodPost { + return fmt.Errorf("method not allowed") + } + return nil +} + +// validateContentTypeIsJSON validates that the request content type is JSON. +func (h *DefaultHandler[T]) validateContentTypeIsJSON(r *http.Request) error { + contentType := r.Header.Get("Content-Type") + if contentType != ContentTypeJSON { + return fmt.Errorf("unsupported content type, expected %s", ContentTypeJSON) + } + + return nil +} + +// createMessage creates the message by unmarshalling the request body. +func (h *DefaultHandler[T]) createMessage(r *http.Request) (gogoproto.Message, error) { + path := strings.TrimPrefix(r.URL.Path, "/") + requestType := gogoproto.MessageType(path) + if requestType == nil { + return nil, fmt.Errorf("unknown request type") + } + + msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) + if !ok { + return nil, fmt.Errorf("failed to create message instance") + } + + defer r.Body.Close() + limitedReader := io.LimitReader(r.Body, MaxBodySize) + err := jsonpb.Unmarshal(limitedReader, msg) + if err != nil { + return nil, fmt.Errorf("error parsing body: %w", err) + } + + return msg, nil +} diff --git a/server/v2/api/rest/server.go b/server/v2/api/rest/server.go new file mode 100644 index 000000000000..0f2b1777973a --- /dev/null +++ b/server/v2/api/rest/server.go @@ -0,0 +1,96 @@ +package rest + +import ( + "context" + "errors" + "fmt" + "net/http" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + serverv2 "cosmossdk.io/server/v2" +) + +const ( + ServerName = "rest" +) + +type Server[T transaction.Tx] struct { + logger log.Logger + router *http.ServeMux + + httpServer *http.Server + config *Config + cfgOptions []CfgOption +} + +func New[T transaction.Tx](cfgOptions ...CfgOption) *Server[T] { + return &Server[T]{ + cfgOptions: cfgOptions, + } +} + +func (s *Server[T]) Name() string { + return ServerName +} + +func (s *Server[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logger log.Logger) error { + s.logger = logger.With(log.ModuleKey, s.Name()) + + serverCfg := s.Config().(*Config) + if len(cfg) > 0 { + if err := serverv2.UnmarshalSubConfig(cfg, s.Name(), &serverCfg); err != nil { + return fmt.Errorf("failed to unmarshal config: %w", err) + } + } + + s.router = http.NewServeMux() + s.router.Handle("/", NewDefaultHandler(appI)) + s.config = serverCfg + + return nil +} + +func (s *Server[T]) Start(ctx context.Context) error { + if !s.config.Enable { + s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name())) + return nil + } + + s.httpServer = &http.Server{ + Addr: s.config.Address, + Handler: s.router, + } + + s.logger.Info("starting HTTP server", "address", s.config.Address) + if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("failed to start HTTP server", "error", err) + return err + } + + return nil +} + +func (s *Server[T]) Stop(ctx context.Context) error { + if !s.config.Enable { + return nil + } + + s.logger.Info("stopping HTTP server") + + return s.httpServer.Shutdown(ctx) +} + +func (s *Server[T]) Config() any { + if s.config == nil || s.config.Address == "" { + cfg := DefaultConfig() + + for _, opt := range s.cfgOptions { + opt(cfg) + } + + return cfg + } + + return s.config +} diff --git a/server/v2/appmanager/appmanager.go b/server/v2/appmanager/appmanager.go new file mode 100644 index 000000000000..af54936ebf03 --- /dev/null +++ b/server/v2/appmanager/appmanager.go @@ -0,0 +1,221 @@ +package appmanager + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + + "cosmossdk.io/core/server" + corestore "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" +) + +// AppManager is a coordinator for all things related to an application +// It is responsible for interacting with stf and store. +// Runtime/v2 is an extension of this interface. +type AppManager[T transaction.Tx] interface { + // InitGenesis initializes the genesis state of the application. + InitGenesis( + ctx context.Context, + blockRequest *server.BlockRequest[T], + initGenesisJSON []byte, + txDecoder transaction.Codec[T], + ) (*server.BlockResponse, corestore.WriterMap, error) + + // ExportGenesis exports the genesis state of the application. + ExportGenesis(ctx context.Context, version uint64) ([]byte, error) + + // DeliverBlock executes a block of transactions. + DeliverBlock( + ctx context.Context, + block *server.BlockRequest[T], + ) (*server.BlockResponse, corestore.WriterMap, error) + + // ValidateTx will validate the tx against the latest storage state. This means that + // only the stateful validation will be run, not the execution portion of the tx. + // If full execution is needed, Simulate must be used. + ValidateTx(ctx context.Context, tx T) (server.TxResult, error) + + // Simulate runs validation and execution flow of a Tx. + Simulate(ctx context.Context, tx T) (server.TxResult, corestore.WriterMap, error) + + // Query queries the application at the provided version. + // CONTRACT: Version must always be provided, if 0, get latest + Query(ctx context.Context, version uint64, request transaction.Msg) (transaction.Msg, error) + + // QueryWithState executes a query with the provided state. This allows to process a query + // independently of the db state. For example, it can be used to process a query with temporary + // and uncommitted state + QueryWithState(ctx context.Context, state corestore.ReaderMap, request transaction.Msg) (transaction.Msg, error) +} + +// Store defines the underlying storage behavior needed by AppManager. +type Store interface { + // StateLatest returns a readonly view over the latest + // committed state of the store. Alongside the version + // associated with it. + StateLatest() (uint64, corestore.ReaderMap, error) + + // StateAt returns a readonly view over the provided + // state. Must error when the version does not exist. + StateAt(version uint64) (corestore.ReaderMap, error) +} + +// appManager is a coordinator for all things related to an application +type appManager[T transaction.Tx] struct { + // Gas limits for validating, querying, and simulating transactions. + config Config + // InitGenesis is a function that initializes the application state from a genesis file. + // It takes a context, a source reader for the genesis file, and a transaction handler function. + initGenesis InitGenesis + // ExportGenesis is a function that exports the application state to a genesis file. + // It takes a context and a version number for the genesis file. + exportGenesis ExportGenesis + // The database for storing application data. + db Store + // The state transition function for processing transactions. + stf StateTransitionFunction[T] +} + +func New[T transaction.Tx]( + config Config, + db Store, + stf StateTransitionFunction[T], + initGenesisImpl InitGenesis, + exportGenesisImpl ExportGenesis, +) AppManager[T] { + return &appManager[T]{ + config: config, + db: db, + stf: stf, + initGenesis: initGenesisImpl, + exportGenesis: exportGenesisImpl, + } +} + +// InitGenesis initializes the genesis state of the application. +func (a appManager[T]) InitGenesis( + ctx context.Context, + blockRequest *server.BlockRequest[T], + initGenesisJSON []byte, + txDecoder transaction.Codec[T], +) (*server.BlockResponse, corestore.WriterMap, error) { + var genTxs []T + genesisState, err := a.initGenesis( + ctx, + bytes.NewBuffer(initGenesisJSON), + func(jsonTx json.RawMessage) error { + genTx, err := txDecoder.DecodeJSON(jsonTx) + if err != nil { + return fmt.Errorf("failed to decode genesis transaction: %w", err) + } + genTxs = append(genTxs, genTx) + return nil + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to import genesis state: %w", err) + } + // run block + blockRequest.Txs = genTxs + + blockResponse, blockZeroState, err := a.stf.DeliverBlock(ctx, blockRequest, genesisState) + if err != nil { + return blockResponse, nil, fmt.Errorf("failed to deliver block %d: %w", blockRequest.Height, err) + } + + // after executing block 0, we extract the changes and apply them to the genesis state. + stateChanges, err := blockZeroState.GetStateChanges() + if err != nil { + return nil, nil, fmt.Errorf("failed to get block zero state changes: %w", err) + } + + err = genesisState.ApplyStateChanges(stateChanges) + if err != nil { + return nil, nil, fmt.Errorf("failed to apply block zero state changes to genesis state: %w", err) + } + + return blockResponse, genesisState, err +} + +// ExportGenesis exports the genesis state of the application. +func (a appManager[T]) ExportGenesis(ctx context.Context, version uint64) ([]byte, error) { + if a.exportGenesis == nil { + return nil, errors.New("export genesis function not set") + } + + return a.exportGenesis(ctx, version) +} + +// DeliverBlock executes a block of transactions. +func (a appManager[T]) DeliverBlock( + ctx context.Context, + block *server.BlockRequest[T], +) (*server.BlockResponse, corestore.WriterMap, error) { + latestVersion, currentState, err := a.db.StateLatest() + if err != nil { + return nil, nil, fmt.Errorf("unable to create new state for height %d: %w", block.Height, err) + } + + if latestVersion+1 != block.Height { + return nil, nil, fmt.Errorf("invalid DeliverBlock height wanted %d, got %d", latestVersion+1, block.Height) + } + + blockResponse, newState, err := a.stf.DeliverBlock(ctx, block, currentState) + if err != nil { + return nil, nil, fmt.Errorf("block delivery failed: %w", err) + } + + return blockResponse, newState, nil +} + +// ValidateTx will validate the tx against the latest storage state. This means that +// only the stateful validation will be run, not the execution portion of the tx. +// If full execution is needed, Simulate must be used. +func (a appManager[T]) ValidateTx(ctx context.Context, tx T) (server.TxResult, error) { + _, latestState, err := a.db.StateLatest() + if err != nil { + return server.TxResult{}, err + } + res := a.stf.ValidateTx(ctx, latestState, a.config.ValidateTxGasLimit, tx) + return res, res.Error +} + +// Simulate runs validation and execution flow of a Tx. +func (a appManager[T]) Simulate(ctx context.Context, tx T) (server.TxResult, corestore.WriterMap, error) { + _, state, err := a.db.StateLatest() + if err != nil { + return server.TxResult{}, nil, err + } + result, cs := a.stf.Simulate(ctx, state, a.config.SimulationGasLimit, tx) // TODO: check if this is done in the antehandler + return result, cs, nil +} + +// Query queries the application at the provided version. +// CONTRACT: Version must always be provided, if 0, get latest +func (a appManager[T]) Query(ctx context.Context, version uint64, request transaction.Msg) (transaction.Msg, error) { + // if version is provided attempt to do a height query. + if version != 0 { + queryState, err := a.db.StateAt(version) + if err != nil { + return nil, err + } + return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request) + } + + // otherwise rely on latest available state. + _, queryState, err := a.db.StateLatest() + if err != nil { + return nil, err + } + return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request) +} + +// QueryWithState executes a query with the provided state. This allows to process a query +// independently of the db state. For example, it can be used to process a query with temporary +// and uncommitted state +func (a appManager[T]) QueryWithState(ctx context.Context, state corestore.ReaderMap, request transaction.Msg) (transaction.Msg, error) { + return a.stf.Query(ctx, state, a.config.QueryGasLimit, request) +} diff --git a/server/v2/appmanager/config.go b/server/v2/appmanager/config.go new file mode 100644 index 000000000000..fb3c20341f35 --- /dev/null +++ b/server/v2/appmanager/config.go @@ -0,0 +1,8 @@ +package appmanager + +// Config represents the configuration options for the app manager. +type Config struct { + ValidateTxGasLimit uint64 `mapstructure:"validate-tx-gas-limit"` // TODO: check how this works on app mempool + QueryGasLimit uint64 `mapstructure:"query-gas-limit"` + SimulationGasLimit uint64 `mapstructure:"simulation-gas-limit"` +} diff --git a/server/v2/appmanager/genesis.go b/server/v2/appmanager/genesis.go new file mode 100644 index 000000000000..347d0f30e07b --- /dev/null +++ b/server/v2/appmanager/genesis.go @@ -0,0 +1,28 @@ +package appmanager + +import ( + "context" + "encoding/json" + "io" + + "cosmossdk.io/core/store" +) + +type ( + // InitGenesis is a function that will run at application genesis, it will be called with + // the following arguments: + // - ctx: the context of the genesis operation + // - src: the source containing the raw genesis state + // - txHandler: a function capable of decoding a json tx, will be run for each genesis + // transaction + // + // It must return a map of the dirty state after the genesis operation. + InitGenesis func( + ctx context.Context, + src io.Reader, + txHandler func(json.RawMessage) error, + ) (store.WriterMap, error) + + // ExportGenesis is a function type that represents the export of the genesis state. + ExportGenesis func(ctx context.Context, version uint64) ([]byte, error) +) diff --git a/server/v2/appmanager/stf.go b/server/v2/appmanager/stf.go new file mode 100644 index 000000000000..1e769c13ff9c --- /dev/null +++ b/server/v2/appmanager/stf.go @@ -0,0 +1,43 @@ +package appmanager + +import ( + "context" + + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" +) + +// StateTransitionFunction is an interface for processing transactions and blocks. +type StateTransitionFunction[T transaction.Tx] interface { + // DeliverBlock executes a block of transactions. + DeliverBlock( + ctx context.Context, + block *server.BlockRequest[T], + state store.ReaderMap, + ) (blockResult *server.BlockResponse, newState store.WriterMap, err error) + + // ValidateTx validates a transaction. + ValidateTx( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + tx T, + ) server.TxResult + + // Simulate executes a transaction in simulation mode. + Simulate( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + tx T, + ) (server.TxResult, store.WriterMap) + + // Query executes a query on the application. + Query( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + req transaction.Msg, + ) (transaction.Msg, error) +} diff --git a/server/v2/cometbft/abci.go b/server/v2/cometbft/abci.go index 901ec64b5e99..295c17638a71 100644 --- a/server/v2/cometbft/abci.go +++ b/server/v2/cometbft/abci.go @@ -42,7 +42,7 @@ var _ abci.Application = (*Consensus[transaction.Tx])(nil) type Consensus[T transaction.Tx] struct { logger log.Logger appName, version string - app *appmanager.AppManager[T] + app appmanager.AppManager[T] appCloser func() error txCodec transaction.Codec[T] store types.Store @@ -77,7 +77,7 @@ type Consensus[T transaction.Tx] struct { func NewConsensus[T transaction.Tx]( logger log.Logger, appName string, - app *appmanager.AppManager[T], + app appmanager.AppManager[T], appCloser func() error, mp mempool.Mempool[T], indexedEvents map[string]struct{}, diff --git a/server/v2/cometbft/abci_test.go b/server/v2/cometbft/abci_test.go index 9a42948c6c61..d58e87aa9ef9 100644 --- a/server/v2/cometbft/abci_test.go +++ b/server/v2/cometbft/abci_test.go @@ -646,7 +646,7 @@ func setUpConsensus(t *testing.T, gasLimit uint64, mempool mempool.Mempool[mock. }, nil }) - s, err := stf.NewSTF( + s, err := stf.New( log.NewNopLogger().With("module", "stf"), msgRouterBuilder, queryRouterBuilder, @@ -672,21 +672,20 @@ func setUpConsensus(t *testing.T, gasLimit uint64, mempool mempool.Mempool[mock. sc := cometmock.NewMockCommiter(log.NewNopLogger(), string(actorName), "stf") mockStore := cometmock.NewMockStore(ss, sc) - b := appmanager.Builder[mock.Tx]{ - STF: s, - DB: mockStore, + am := appmanager.New(appmanager.Config{ ValidateTxGasLimit: gasLimit, QueryGasLimit: gasLimit, SimulationGasLimit: gasLimit, - InitGenesis: func(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, error) { + }, + mockStore, + s, + func(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, error) { _, st, err := mockStore.StateLatest() require.NoError(t, err) return branch.DefaultNewWriterMap(st), nil }, - } - - am, err := b.Build() - require.NoError(t, err) + nil, + ) return NewConsensus[mock.Tx](log.NewNopLogger(), "testing-app", am, func() error { return nil }, mempool, map[string]struct{}{}, nil, mockStore, Config{AppTomlConfig: DefaultAppTomlConfig()}, mock.TxCodec{}, "test") } diff --git a/server/v2/cometbft/server.go b/server/v2/cometbft/server.go index 2cfc04502fbc..0e5439194b9f 100644 --- a/server/v2/cometbft/server.go +++ b/server/v2/cometbft/server.go @@ -101,15 +101,15 @@ func (s *CometBFTServer[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logg } s.logger = logger.With(log.ModuleKey, s.Name()) - rs := appI.GetStore() + rs := appI.Store() consensus := NewConsensus( s.logger, appI.Name(), - appI.GetAppManager(), + appI, appI.Close, s.serverOptions.Mempool(cfg), indexEvents, - appI.GetQueryHandlers(), + appI.QueryHandlers(), rs, s.config, s.initTxCodec, @@ -132,6 +132,22 @@ func (s *CometBFTServer[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logg } consensus.snapshotManager = snapshots.NewManager(snapshotStore, s.serverOptions.SnapshotOptions(cfg), sc, ss, nil, s.logger) +<<<<<<< HEAD +======= + // initialize the indexer + if indexerCfg := s.config.AppTomlConfig.Indexer; len(indexerCfg.Target) > 0 { + listener, err := indexer.StartIndexing(indexer.IndexingOptions{ + Config: indexerCfg, + Resolver: appI.SchemaDecoderResolver(), + Logger: s.logger.With(log.ModuleKey, "indexer"), + }) + if err != nil { + return fmt.Errorf("failed to start indexing: %w", err) + } + consensus.listener = &listener.Listener + } + +>>>>>>> 681366e34 (refactor(runtime/v2): simplify app manager (#22300)) s.Consensus = consensus return nil diff --git a/server/v2/server_test.go b/server/v2/server_test.go new file mode 100644 index 000000000000..a53b71fc35b9 --- /dev/null +++ b/server/v2/server_test.go @@ -0,0 +1,110 @@ +package serverv2_test + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + coreserver "cosmossdk.io/core/server" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + serverv2 "cosmossdk.io/server/v2" + grpc "cosmossdk.io/server/v2/api/grpc" + "cosmossdk.io/server/v2/store" + storev2 "cosmossdk.io/store/v2" +) + +type mockInterfaceRegistry struct{} + +func (*mockInterfaceRegistry) Resolve(typeUrl string) (gogoproto.Message, error) { + panic("not implemented") +} + +func (*mockInterfaceRegistry) ListImplementations(ifaceTypeURL string) []string { + panic("not implemented") +} +func (*mockInterfaceRegistry) ListAllInterfaces() []string { panic("not implemented") } + +type mockApp[T transaction.Tx] struct { + serverv2.AppI[T] +} + +func (*mockApp[T]) QueryHandlers() map[string]appmodulev2.Handler { + return map[string]appmodulev2.Handler{} +} + +func (*mockApp[T]) InterfaceRegistry() coreserver.InterfaceRegistry { + return &mockInterfaceRegistry{} +} + +func (*mockApp[T]) Store() storev2.RootStore { + return nil +} + +func TestServer(t *testing.T) { + currentDir, err := os.Getwd() + require.NoError(t, err) + configPath := filepath.Join(currentDir, "testdata") + + v, err := serverv2.ReadConfig(configPath) + if err != nil { + v = viper.New() + } + cfg := v.AllSettings() + + logger := log.NewLogger(os.Stdout) + + ctx, err := serverv2.SetServerContext(context.Background(), v, logger) + require.NoError(t, err) + + grpcServer := grpc.New[transaction.Tx]() + err = grpcServer.Init(&mockApp[transaction.Tx]{}, cfg, logger) + require.NoError(t, err) + + storeServer := store.New[transaction.Tx]() + err = storeServer.Init(&mockApp[transaction.Tx]{}, cfg, logger) + require.NoError(t, err) + + mockServer := &mockServer{name: "mock-server-1", ch: make(chan string, 100)} + + server := serverv2.NewServer( + serverv2.DefaultServerConfig(), + grpcServer, + storeServer, + mockServer, + ) + + serverCfgs := server.Configs() + require.Equal(t, serverCfgs[grpcServer.Name()].(*grpc.Config).Address, grpc.DefaultConfig().Address) + require.Equal(t, serverCfgs[mockServer.Name()].(*mockServerConfig).MockFieldOne, MockServerDefaultConfig().MockFieldOne) + + // write config + err = server.WriteConfig(configPath) + require.NoError(t, err) + + v, err = serverv2.ReadConfig(configPath) + require.NoError(t, err) + + require.Equal(t, v.GetString(grpcServer.Name()+".address"), grpc.DefaultConfig().Address) + + // start empty + ctx, cancelFn := context.WithCancel(ctx) + go func() { + // wait 5sec and cancel context + <-time.After(5 * time.Second) + cancelFn() + + err = server.Stop(ctx) + require.NoError(t, err) + }() + + err = server.Start(ctx) + require.NoError(t, err) +} diff --git a/server/v2/stf/stf.go b/server/v2/stf/stf.go new file mode 100644 index 000000000000..6b6f6b2c53da --- /dev/null +++ b/server/v2/stf/stf.go @@ -0,0 +1,635 @@ +package stf + +import ( + "context" + "errors" + "fmt" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + corecontext "cosmossdk.io/core/context" + "cosmossdk.io/core/event" + "cosmossdk.io/core/gas" + "cosmossdk.io/core/header" + "cosmossdk.io/core/log" + "cosmossdk.io/core/router" + "cosmossdk.io/core/server" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/schema/appdata" + stfgas "cosmossdk.io/server/v2/stf/gas" + "cosmossdk.io/server/v2/stf/internal" +) + +type eContextKey struct{} + +var executionContextKey = eContextKey{} + +// STF is a struct that manages the state transition component of the app. +type STF[T transaction.Tx] struct { + logger log.Logger + + msgRouter coreRouterImpl + queryRouter coreRouterImpl + + doPreBlock func(ctx context.Context, txs []T) error + doBeginBlock func(ctx context.Context) error + doEndBlock func(ctx context.Context) error + doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) + + doTxValidation func(ctx context.Context, tx T) error + postTxExec func(ctx context.Context, tx T, success bool) error + + branchFn branchFn // branchFn is a function that given a readonly state it returns a writable version of it. + makeGasMeter makeGasMeterFn + makeGasMeteredState makeGasMeteredStateFn +} + +// New returns a new STF instance. +func New[T transaction.Tx]( + logger log.Logger, + msgRouterBuilder *MsgRouterBuilder, + queryRouterBuilder *MsgRouterBuilder, + doPreBlock func(ctx context.Context, txs []T) error, + doBeginBlock func(ctx context.Context) error, + doEndBlock func(ctx context.Context) error, + doTxValidation func(ctx context.Context, tx T) error, + doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error), + postTxExec func(ctx context.Context, tx T, success bool) error, + branch func(store store.ReaderMap) store.WriterMap, +) (*STF[T], error) { + msgRouter, err := msgRouterBuilder.build() + if err != nil { + return nil, fmt.Errorf("build msg router: %w", err) + } + queryRouter, err := queryRouterBuilder.build() + if err != nil { + return nil, fmt.Errorf("build query router: %w", err) + } + + return &STF[T]{ + logger: logger, + msgRouter: msgRouter, + queryRouter: queryRouter, + doPreBlock: doPreBlock, + doBeginBlock: doBeginBlock, + doEndBlock: doEndBlock, + doValidatorUpdate: doValidatorUpdate, + doTxValidation: doTxValidation, + postTxExec: postTxExec, // TODO + branchFn: branch, + makeGasMeter: stfgas.DefaultGasMeter, + makeGasMeteredState: stfgas.DefaultWrapWithGasMeter, + }, nil +} + +// DeliverBlock is our state transition function. +// It takes a read only view of the state to apply the block to, +// executes the block and returns the block results and the new state. +func (s STF[T]) DeliverBlock( + ctx context.Context, + block *server.BlockRequest[T], + state store.ReaderMap, +) (blockResult *server.BlockResponse, newState store.WriterMap, err error) { + // creates a new branchFn state, from the readonly view of the state + // that can be written to. + newState = s.branchFn(state) + hi := header.Info{ + Hash: block.Hash, + AppHash: block.AppHash, + ChainID: block.ChainId, + Time: block.Time, + Height: int64(block.Height), + } + // set header info + err = s.setHeaderInfo(newState, hi) + if err != nil { + return nil, nil, fmt.Errorf("unable to set initial header info, %w", err) + } + + exCtx := s.makeContext(ctx, ConsensusIdentity, newState, internal.ExecModeFinalize) + exCtx.setHeaderInfo(hi) + + // reset events + exCtx.events = make([]event.Event, 0) + // pre block is called separate from begin block in order to prepopulate state + preBlockEvents, err := s.preBlock(exCtx, block.Txs) + if err != nil { + return nil, nil, err + } + + if err = isCtxCancelled(ctx); err != nil { + return nil, nil, err + } + + // reset events + exCtx.events = make([]event.Event, 0) + // begin block + var beginBlockEvents []event.Event + if !block.IsGenesis { + // begin block + beginBlockEvents, err = s.beginBlock(exCtx) + if err != nil { + return nil, nil, err + } + } + + // check if we need to return early + if err = isCtxCancelled(ctx); err != nil { + return nil, nil, err + } + + // execute txs + txResults := make([]server.TxResult, len(block.Txs)) + // TODO: skip first tx if vote extensions are enabled (marko) + for i, txBytes := range block.Txs { + // check if we need to return early or continue delivering txs + if err = isCtxCancelled(ctx); err != nil { + return nil, nil, err + } + txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi, int32(i+1)) + } + // reset events + exCtx.events = make([]event.Event, 0) + // end block + endBlockEvents, valset, err := s.endBlock(exCtx) + if err != nil { + return nil, nil, err + } + + return &server.BlockResponse{ + ValidatorUpdates: valset, + PreBlockEvents: preBlockEvents, + BeginBlockEvents: beginBlockEvents, + TxResults: txResults, + EndBlockEvents: endBlockEvents, + }, newState, nil +} + +// deliverTx executes a TX and returns the result. +func (s STF[T]) deliverTx( + ctx context.Context, + state store.WriterMap, + tx T, + execMode transaction.ExecMode, + hi header.Info, + txIndex int32, +) server.TxResult { + // recover in the case of a panic + var recoveryError error + defer func() { + if r := recover(); r != nil { + recoveryError = fmt.Errorf("panic during transaction execution: %s", r) + s.logger.Error("panic during transaction execution", "error", recoveryError) + } + }() + // handle error from GetGasLimit + gasLimit, gasLimitErr := tx.GetGasLimit() + if gasLimitErr != nil { + return server.TxResult{ + Error: gasLimitErr, + } + } + + if recoveryError != nil { + return server.TxResult{ + Error: recoveryError, + } + } + validateGas, validationEvents, err := s.validateTx(ctx, state, gasLimit, tx, execMode) + if err != nil { + return server.TxResult{ + Error: err, + } + } + events := make([]event.Event, 0) + // set the event indexes, set MsgIndex to 0 in validation events + for i, e := range validationEvents { + e.BlockStage = appdata.TxProcessingStage + e.TxIndex = txIndex + e.MsgIndex = 0 + e.EventIndex = int32(i + 1) + events = append(events, e) + } + + execResp, execGas, execEvents, err := s.execTx(ctx, state, gasLimit-validateGas, tx, execMode, hi) + // set the TxIndex in the exec events + for _, e := range execEvents { + e.BlockStage = appdata.TxProcessingStage + e.TxIndex = txIndex + events = append(events, e) + } + + return server.TxResult{ + Events: events, + GasUsed: execGas + validateGas, + GasWanted: gasLimit, + Resp: execResp, + Error: err, + } +} + +// validateTx validates a transaction given the provided WritableState and gas limit. +// If the validation is successful, state is committed +func (s STF[T]) validateTx( + ctx context.Context, + state store.WriterMap, + gasLimit uint64, + tx T, + execMode transaction.ExecMode, +) (gasUsed uint64, events []event.Event, err error) { + validateState := s.branchFn(state) + hi, err := s.getHeaderInfo(validateState) + if err != nil { + return 0, nil, err + } + validateCtx := s.makeContext(ctx, RuntimeIdentity, validateState, execMode) + validateCtx.setHeaderInfo(hi) + validateCtx.setGasLimit(gasLimit) + err = s.doTxValidation(validateCtx, tx) + if err != nil { + return 0, nil, err + } + + consumed := validateCtx.meter.Limit() - validateCtx.meter.Remaining() + + return consumed, validateCtx.events, applyStateChanges(state, validateState) +} + +// execTx executes the tx messages on the provided state. If the tx fails then the state is discarded. +func (s STF[T]) execTx( + ctx context.Context, + state store.WriterMap, + gasLimit uint64, + tx T, + execMode transaction.ExecMode, + hi header.Info, +) ([]transaction.Msg, uint64, []event.Event, error) { + execState := s.branchFn(state) + + msgsResp, gasUsed, runTxMsgsEvents, txErr := s.runTxMsgs(ctx, execState, gasLimit, tx, execMode, hi) + if txErr != nil { + // in case of error during message execution, we do not apply the exec state. + // instead we run the post exec handler in a new branchFn from the initial state. + postTxState := s.branchFn(state) + postTxCtx := s.makeContext(ctx, RuntimeIdentity, postTxState, execMode) + postTxCtx.setHeaderInfo(hi) + + postTxErr := s.postTxExec(postTxCtx, tx, false) + if postTxErr != nil { + // if the post tx handler fails, then we do not apply any state change to the initial state. + // we just return the exec gas used and a joined error from TX error and post TX error. + return nil, gasUsed, nil, errors.Join(txErr, postTxErr) + } + // in case post tx is successful, then we commit the post tx state to the initial state, + // and we return post tx events alongside exec gas used and the error of the tx. + applyErr := applyStateChanges(state, postTxState) + if applyErr != nil { + return nil, 0, nil, applyErr + } + // set the event indexes, set MsgIndex to -1 in post tx events + for i := range postTxCtx.events { + postTxCtx.events[i].EventIndex = int32(i + 1) + postTxCtx.events[i].MsgIndex = -1 + } + + return nil, gasUsed, postTxCtx.events, txErr + } + // tx execution went fine, now we use the same state to run the post tx exec handler, + // in case the execution of the post tx fails, then no state change is applied and the + // whole execution step is rolled back. + postTxCtx := s.makeContext(ctx, RuntimeIdentity, execState, execMode) // NO gas limit. + postTxCtx.setHeaderInfo(hi) + postTxErr := s.postTxExec(postTxCtx, tx, true) + if postTxErr != nil { + // if post tx fails, then we do not apply any state change, we return the post tx error, + // alongside the gas used. + return nil, gasUsed, nil, postTxErr + } + // both the execution and post tx execution step were successful, so we apply the state changes + // to the provided state, and we return responses, and events from exec tx and post tx exec. + applyErr := applyStateChanges(state, execState) + if applyErr != nil { + return nil, 0, nil, applyErr + } + // set the event indexes, set MsgIndex to -1 in post tx events + for i := range postTxCtx.events { + postTxCtx.events[i].EventIndex = int32(i + 1) + postTxCtx.events[i].MsgIndex = -1 + } + + return msgsResp, gasUsed, append(runTxMsgsEvents, postTxCtx.events...), nil +} + +// runTxMsgs will execute the messages contained in the TX with the provided state. +func (s STF[T]) runTxMsgs( + ctx context.Context, + state store.WriterMap, + gasLimit uint64, + tx T, + execMode transaction.ExecMode, + hi header.Info, +) ([]transaction.Msg, uint64, []event.Event, error) { + txSenders, err := tx.GetSenders() + if err != nil { + return nil, 0, nil, err + } + msgs, err := tx.GetMessages() + if err != nil { + return nil, 0, nil, err + } + msgResps := make([]transaction.Msg, len(msgs)) + + execCtx := s.makeContext(ctx, RuntimeIdentity, state, execMode) + execCtx.setHeaderInfo(hi) + execCtx.setGasLimit(gasLimit) + events := make([]event.Event, 0) + for i, msg := range msgs { + execCtx.sender = txSenders[i] + execCtx.events = make([]event.Event, 0) // reset events + resp, err := s.msgRouter.Invoke(execCtx, msg) + if err != nil { + return nil, 0, nil, err // do not wrap the error or we lose the original error type + } + msgResps[i] = resp + for j, e := range execCtx.events { + e.MsgIndex = int32(i + 1) + e.EventIndex = int32(j + 1) + events = append(events, e) + } + } + + consumed := execCtx.meter.Limit() - execCtx.meter.Remaining() + return msgResps, consumed, events, nil +} + +// preBlock executes the pre block logic. +func (s STF[T]) preBlock( + ctx *executionContext, + txs []T, +) ([]event.Event, error) { + err := s.doPreBlock(ctx, txs) + if err != nil { + return nil, err + } + + for i := range ctx.events { + ctx.events[i].BlockStage = appdata.PreBlockStage + ctx.events[i].EventIndex = int32(i + 1) + } + + return ctx.events, nil +} + +// beginBlock executes the begin block logic. +func (s STF[T]) beginBlock( + ctx *executionContext, +) (beginBlockEvents []event.Event, err error) { + err = s.doBeginBlock(ctx) + if err != nil { + return nil, err + } + + for i := range ctx.events { + ctx.events[i].BlockStage = appdata.BeginBlockStage + ctx.events[i].EventIndex = int32(i + 1) + } + + return ctx.events, nil +} + +// endBlock executes the end block logic. +func (s STF[T]) endBlock( + ctx *executionContext, +) ([]event.Event, []appmodulev2.ValidatorUpdate, error) { + err := s.doEndBlock(ctx) + if err != nil { + return nil, nil, err + } + events := ctx.events + ctx.events = make([]event.Event, 0) // reset events + valsetUpdates, err := s.validatorUpdates(ctx) + if err != nil { + return nil, nil, err + } + events = append(events, ctx.events...) + for i := range events { + events[i].BlockStage = appdata.EndBlockStage + events[i].EventIndex = int32(i + 1) + } + + return events, valsetUpdates, nil +} + +// validatorUpdates returns the validator updates for the current block. It is called by endBlock after the endblock execution has concluded +func (s STF[T]) validatorUpdates( + ctx *executionContext, +) ([]appmodulev2.ValidatorUpdate, error) { + valSetUpdates, err := s.doValidatorUpdate(ctx) + if err != nil { + return nil, err + } + return valSetUpdates, nil +} + +// Simulate simulates the execution of a tx on the provided state. +func (s STF[T]) Simulate( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + tx T, +) (server.TxResult, store.WriterMap) { + simulationState := s.branchFn(state) + hi, err := s.getHeaderInfo(simulationState) + if err != nil { + return server.TxResult{}, nil + } + txr := s.deliverTx(ctx, simulationState, tx, internal.ExecModeSimulate, hi, 0) + + return txr, simulationState +} + +// ValidateTx will run only the validation steps required for a transaction. +// Validations are run over the provided state, with the provided gas limit. +func (s STF[T]) ValidateTx( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + tx T, +) server.TxResult { + validationState := s.branchFn(state) + gasUsed, events, err := s.validateTx(ctx, validationState, gasLimit, tx, transaction.ExecModeCheck) + return server.TxResult{ + Events: events, + GasUsed: gasUsed, + Error: err, + } +} + +// Query executes the query on the provided state with the provided gas limits. +func (s STF[T]) Query( + ctx context.Context, + state store.ReaderMap, + gasLimit uint64, + req transaction.Msg, +) (transaction.Msg, error) { + queryState := s.branchFn(state) + hi, err := s.getHeaderInfo(queryState) + if err != nil { + return nil, err + } + queryCtx := s.makeContext(ctx, nil, queryState, internal.ExecModeSimulate) + queryCtx.setHeaderInfo(hi) + queryCtx.setGasLimit(gasLimit) + return s.queryRouter.Invoke(queryCtx, req) +} + +// clone clones STF. +func (s STF[T]) clone() STF[T] { + return STF[T]{ + logger: s.logger, + msgRouter: s.msgRouter, + queryRouter: s.queryRouter, + doPreBlock: s.doPreBlock, + doBeginBlock: s.doBeginBlock, + doEndBlock: s.doEndBlock, + doValidatorUpdate: s.doValidatorUpdate, + doTxValidation: s.doTxValidation, + postTxExec: s.postTxExec, + branchFn: s.branchFn, + makeGasMeter: s.makeGasMeter, + makeGasMeteredState: s.makeGasMeteredState, + } +} + +// executionContext is a struct that holds the context for the execution of a tx. +type executionContext struct { + context.Context + + // unmeteredState is storage without metering. Changes here are propagated to state which is the metered + // version. + unmeteredState store.WriterMap + // state is the gas metered state. + state store.WriterMap + // meter is the gas meter. + meter gas.Meter + // events are the current events. + events []event.Event + // sender is the causer of the state transition. + sender transaction.Identity + // headerInfo contains the block info. + headerInfo header.Info + // execMode retains information about the exec mode. + execMode transaction.ExecMode + + branchFn branchFn + makeGasMeter makeGasMeterFn + makeGasMeteredStore makeGasMeteredStateFn + + msgRouter router.Service + queryRouter router.Service +} + +// setHeaderInfo sets the header info in the state to be used by queries in the future. +func (e *executionContext) setHeaderInfo(hi header.Info) { + e.headerInfo = hi +} + +// setGasLimit will update the gas limit of the *executionContext +func (e *executionContext) setGasLimit(limit uint64) { + meter := e.makeGasMeter(limit) + meteredState := e.makeGasMeteredStore(meter, e.unmeteredState) + + e.meter = meter + e.state = meteredState +} + +func (e *executionContext) Value(key any) any { + if key == executionContextKey { + return e + } + + return e.Context.Value(key) +} + +// TODO: too many calls to makeContext can be expensive +// makeContext creates and returns a new execution context for the STF[T] type. +// It takes in the following parameters: +// - ctx: The context.Context object for the execution. +// - sender: The transaction.Identity object representing the sender of the transaction. +// - state: The store.WriterMap object for accessing and modifying the state. +// - gasLimit: The maximum amount of gas allowed for the execution. +// - execMode: The corecontext.ExecMode object representing the execution mode. +// +// It returns a pointer to the executionContext struct +func (s STF[T]) makeContext( + ctx context.Context, + sender transaction.Identity, + store store.WriterMap, + execMode transaction.ExecMode, +) *executionContext { + valuedCtx := context.WithValue(ctx, corecontext.ExecModeKey, execMode) + return newExecutionContext( + valuedCtx, + s.makeGasMeter, + s.makeGasMeteredState, + s.branchFn, + sender, + store, + execMode, + s.msgRouter, + s.queryRouter, + ) +} + +func newExecutionContext( + ctx context.Context, + makeGasMeterFn makeGasMeterFn, + makeGasMeteredStoreFn makeGasMeteredStateFn, + branchFn branchFn, + sender transaction.Identity, + state store.WriterMap, + execMode transaction.ExecMode, + msgRouter coreRouterImpl, + queryRouter coreRouterImpl, +) *executionContext { + meter := makeGasMeterFn(gas.NoGasLimit) + meteredState := makeGasMeteredStoreFn(meter, state) + + return &executionContext{ + Context: ctx, + unmeteredState: state, + state: meteredState, + meter: meter, + events: make([]event.Event, 0), + headerInfo: header.Info{}, + execMode: execMode, + sender: sender, + branchFn: branchFn, + makeGasMeter: makeGasMeterFn, + makeGasMeteredStore: makeGasMeteredStoreFn, + msgRouter: msgRouter, + queryRouter: queryRouter, + } +} + +// applyStateChanges applies the state changes from the source store to the destination store. +// It retrieves the state changes from the source store using GetStateChanges method, +// and then applies those changes to the destination store using ApplyStateChanges method. +// If an error occurs during the retrieval or application of state changes, it is returned. +func applyStateChanges(dst, src store.WriterMap) error { + changes, err := src.GetStateChanges() + if err != nil { + return err + } + return dst.ApplyStateChanges(changes) +} + +// isCtxCancelled reports if the context was canceled. +func isCtxCancelled(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + return nil + } +} diff --git a/server/v2/store/server.go b/server/v2/store/server.go new file mode 100644 index 000000000000..1fafe4e25d53 --- /dev/null +++ b/server/v2/store/server.go @@ -0,0 +1,91 @@ +package store + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + serverv2 "cosmossdk.io/server/v2" + storev2 "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/root" +) + +var ( + _ serverv2.ServerComponent[transaction.Tx] = (*Server[transaction.Tx])(nil) + _ serverv2.HasConfig = (*Server[transaction.Tx])(nil) + _ serverv2.HasCLICommands = (*Server[transaction.Tx])(nil) +) + +const ServerName = "store" + +// Server manages store config and contains prune & snapshot commands +type Server[T transaction.Tx] struct { + config *root.Config + backend storev2.Backend +} + +func New[T transaction.Tx]() *Server[T] { + return &Server[T]{} +} + +func (s *Server[T]) Init(app serverv2.AppI[T], v map[string]any, _ log.Logger) (err error) { + s.backend = app.Store() + s.config, err = UnmarshalConfig(v) + return err +} + +func (s *Server[T]) Name() string { + return ServerName +} + +func (s *Server[T]) Start(context.Context) error { + return nil +} + +func (s *Server[T]) Stop(context.Context) error { + return nil +} + +func (s *Server[T]) CLICommands() serverv2.CLIConfig { + return serverv2.CLIConfig{ + Commands: []*cobra.Command{ + s.PrunesCmd(), + s.ExportSnapshotCmd(), + s.DeleteSnapshotCmd(), + s.ListSnapshotsCmd(), + s.DumpArchiveCmd(), + s.LoadArchiveCmd(), + s.RestoreSnapshotCmd(s.backend), + }, + } +} + +func (s *Server[T]) Config() any { + if s.config == nil || s.config.AppDBBackend == "" { + return root.DefaultConfig() + } + + return s.config +} + +// UnmarshalConfig unmarshals the store config from the given map. +// If the config is not found in the map, the default config is returned. +// If the home directory is found in the map, it sets the home directory in the config. +// An empty home directory *is* permitted at this stage, but attempting to build +// the store with an empty home directory will fail. +func UnmarshalConfig(cfg map[string]any) (*root.Config, error) { + config := &root.Config{ + Options: root.DefaultStoreOptions(), + } + if err := serverv2.UnmarshalSubConfig(cfg, ServerName, config); err != nil { + return nil, fmt.Errorf("failed to unmarshal store config: %w", err) + } + home := cfg[serverv2.FlagHome] + if home != nil { + config.Home = home.(string) + } + return config, nil +} diff --git a/server/v2/types.go b/server/v2/types.go new file mode 100644 index 000000000000..40d51e42375c --- /dev/null +++ b/server/v2/types.go @@ -0,0 +1,26 @@ +package serverv2 + +import ( + "github.com/spf13/viper" + + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/server" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "cosmossdk.io/schema/decoding" + "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/store/v2" +) + +type AppCreator[T transaction.Tx] func(log.Logger, *viper.Viper) AppI[T] + +type AppI[T transaction.Tx] interface { + appmanager.AppManager[T] + + Name() string + InterfaceRegistry() server.InterfaceRegistry + QueryHandlers() map[string]appmodulev2.Handler + Store() store.RootStore + SchemaDecoderResolver() decoding.DecoderResolver + Close() error +} diff --git a/simapp/v2/app_di.go b/simapp/v2/app_di.go index 3c9933721f12..593c8d0c275f 100644 --- a/simapp/v2/app_di.go +++ b/simapp/v2/app_di.go @@ -218,8 +218,8 @@ func (app *SimApp[T]) TxConfig() client.TxConfig { return app.txConfig } -// GetStore returns the root store. -func (app *SimApp[T]) GetStore() store.RootStore { +// Store returns the root store. +func (app *SimApp[T]) Store() store.RootStore { return app.store } diff --git a/simapp/v2/app_test.go b/simapp/v2/app_test.go index 9d71c072141a..3795c7b9dff1 100644 --- a/simapp/v2/app_test.go +++ b/simapp/v2/app_test.go @@ -71,7 +71,7 @@ func NewTestApp(t *testing.T) (*SimApp[transaction.Tx], context.Context) { genesisBytes, err := json.Marshal(genesis) require.NoError(t, err) - st := app.GetStore() + st := app.Store() ci, err := st.LastCommitID() require.NoError(t, err) @@ -107,7 +107,7 @@ func MoveNextBlock(t *testing.T, app *SimApp[transaction.Tx], ctx context.Contex bz := sha256.Sum256([]byte{}) - st := app.GetStore() + st := app.Store() ci, err := st.LastCommitID() require.NoError(t, err) diff --git a/simapp/v2/export.go b/simapp/v2/export.go index 50f4a898bb37..61175f41607f 100644 --- a/simapp/v2/export.go +++ b/simapp/v2/export.go @@ -29,7 +29,7 @@ func (app *SimApp[T]) ExportAppStateAndValidators( return exportedApp, err } - readerMap, err := app.GetStore().StateAt(latestHeight) + readerMap, err := app.Store().StateAt(latestHeight) if err != nil { return exportedApp, err } From af0b28d2c8bb92458ef12bf33d668d9c85363b91 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Mon, 21 Oct 2024 15:54:18 +0200 Subject: [PATCH 2/4] removal --- runtime/v2/app.go | 103 ---- runtime/v2/builder.go | 216 -------- runtime/v2/manager.go | 861 ----------------------------- runtime/v2/module.go | 257 --------- server/v2/api/grpc/server.go | 221 -------- server/v2/api/rest/handler.go | 99 ---- server/v2/api/rest/server.go | 96 ---- server/v2/appmanager/appmanager.go | 221 -------- server/v2/appmanager/config.go | 8 - server/v2/appmanager/genesis.go | 28 - server/v2/appmanager/stf.go | 43 -- server/v2/server_test.go | 110 ---- server/v2/stf/stf.go | 635 --------------------- server/v2/store/server.go | 91 --- server/v2/types.go | 26 - 15 files changed, 3015 deletions(-) delete mode 100644 runtime/v2/app.go delete mode 100644 runtime/v2/builder.go delete mode 100644 runtime/v2/manager.go delete mode 100644 runtime/v2/module.go delete mode 100644 server/v2/api/grpc/server.go delete mode 100644 server/v2/api/rest/handler.go delete mode 100644 server/v2/api/rest/server.go delete mode 100644 server/v2/appmanager/appmanager.go delete mode 100644 server/v2/appmanager/config.go delete mode 100644 server/v2/appmanager/genesis.go delete mode 100644 server/v2/appmanager/stf.go delete mode 100644 server/v2/server_test.go delete mode 100644 server/v2/stf/stf.go delete mode 100644 server/v2/store/server.go delete mode 100644 server/v2/types.go diff --git a/runtime/v2/app.go b/runtime/v2/app.go deleted file mode 100644 index b7887ab77f54..000000000000 --- a/runtime/v2/app.go +++ /dev/null @@ -1,103 +0,0 @@ -package runtime - -import ( - "encoding/json" - - runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" - appmodulev2 "cosmossdk.io/core/appmodule/v2" - "cosmossdk.io/core/registry" - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - "cosmossdk.io/schema/decoding" - "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 configuration - logger log.Logger - config *runtimev2.Module - - // state - stf *stf.STF[T] - msgRouterBuilder *stf.MsgRouterBuilder - queryRouterBuilder *stf.MsgRouterBuilder - db Store - storeLoader StoreLoader - - // modules - interfaceRegistrar registry.InterfaceRegistrar - amino registry.AminoRegistrar - moduleManager *MM[T] - queryHandlers map[string]appmodulev2.Handler // queryHandlers defines the query handlers -} - -// 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() -} - -// GetQueryHandlers returns the query handlers. -func (a *App[T]) QueryHandlers() map[string]appmodulev2.Handler { - return a.queryHandlers -} - -// SchemaDecoderResolver returns the module schema resolver. -func (a *App[T]) SchemaDecoderResolver() decoding.DecoderResolver { - moduleSet := map[string]any{} - for moduleName, module := range a.moduleManager.Modules() { - moduleSet[moduleName] = module - } - return decoding.ModuleSetDecoderResolver(moduleSet) -} - -// Close is called in start cmd to gracefully cleanup resources. -func (a *App[T]) Close() error { - return nil -} diff --git a/runtime/v2/builder.go b/runtime/v2/builder.go deleted file mode 100644 index 8556e35745a8..000000000000 --- a/runtime/v2/builder.go +++ /dev/null @@ -1,216 +0,0 @@ -package runtime - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - - "cosmossdk.io/core/appmodule" - appmodulev2 "cosmossdk.io/core/appmodule/v2" - "cosmossdk.io/core/store" - "cosmossdk.io/core/transaction" - "cosmossdk.io/runtime/v2/services" - "cosmossdk.io/server/v2/appmanager" - "cosmossdk.io/server/v2/stf" - "cosmossdk.io/server/v2/stf/branch" - "cosmossdk.io/store/v2/root" -) - -// AppBuilder is a type that is injected into a container by the runtime/v2 module -// (as *AppBuilder) which can be used to create an app which is compatible with -// the existing app.go initialization conventions. -type AppBuilder[T transaction.Tx] struct { - app *App[T] - storeBuilder root.Builder - - // the following fields are used to overwrite the default - branch func(state store.ReaderMap) store.WriterMap - txValidator func(ctx context.Context, tx T) error - postTxExec func(ctx context.Context, tx T, success bool) error -} - -// RegisterModules registers the provided modules with the module manager. -// This is the primary hook for integrating with modules which are not registered using the app config. -func (a *AppBuilder[T]) RegisterModules(modules map[string]appmodulev2.AppModule) error { - for name, appModule := range modules { - // if a (legacy) module implements the HasName interface, check that the name matches - if mod, ok := appModule.(interface{ Name() string }); ok { - if name != mod.Name() { - a.app.logger.Warn(fmt.Sprintf("module name %q does not match name returned by HasName: %q", name, mod.Name())) - } - } - - if _, ok := a.app.moduleManager.modules[name]; ok { - return fmt.Errorf("module named %q already exists", name) - } - a.app.moduleManager.modules[name] = appModule - - if mod, ok := appModule.(appmodulev2.HasRegisterInterfaces); ok { - mod.RegisterInterfaces(a.app.interfaceRegistrar) - } - - if mod, ok := appModule.(appmodule.HasAminoCodec); ok { - mod.RegisterLegacyAminoCodec(a.app.amino) - } - } - - return nil -} - -// Build builds an *App instance. -func (a *AppBuilder[T]) Build(opts ...AppBuilderOption[T]) (*App[T], error) { - for _, opt := range opts { - opt(a) - } - - // default branch - if a.branch == nil { - a.branch = branch.DefaultNewWriterMap - } - - // default tx validator - if a.txValidator == nil { - a.txValidator = a.app.moduleManager.TxValidators() - } - - // default post tx exec - if a.postTxExec == nil { - a.postTxExec = func(ctx context.Context, tx T, success bool) error { - return nil - } - } - - a.app.db = a.storeBuilder.Get() - if a.app.db == nil { - return nil, fmt.Errorf("storeBuilder did not return a db") - } - - if err := a.app.moduleManager.RegisterServices(a.app); err != nil { - return nil, err - } - - endBlocker, valUpdate := a.app.moduleManager.EndBlock() - - stf, err := stf.New[T]( - a.app.logger.With("module", "stf"), - a.app.msgRouterBuilder, - a.app.queryRouterBuilder, - a.app.moduleManager.PreBlocker(), - a.app.moduleManager.BeginBlock(), - endBlocker, - a.txValidator, - valUpdate, - a.postTxExec, - a.branch, - ) - if err != nil { - return nil, fmt.Errorf("failed to create STF: %w", err) - } - a.app.stf = stf - - a.app.AppManager = appmanager.New[T]( - appmanager.Config{ - ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit, - QueryGasLimit: a.app.config.GasConfig.QueryGasLimit, - SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit, - }, - a.app.db, - a.app.stf, - a.initGenesis, - a.exportGenesis, - ) - - return a.app, nil -} - -// initGenesis returns the app initialization genesis for modules -func (a *AppBuilder[T]) initGenesis(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, error) { - // this implementation assumes that the state is a JSON object - bz, err := io.ReadAll(src) - if err != nil { - return nil, fmt.Errorf("failed to read import state: %w", err) - } - var genesisJSON map[string]json.RawMessage - if err = json.Unmarshal(bz, &genesisJSON); err != nil { - return nil, err - } - - v, zeroState, err := a.app.db.StateLatest() - if err != nil { - return nil, fmt.Errorf("unable to get latest state: %w", err) - } - if v != 0 { // TODO: genesis state may be > 0, we need to set version on store - return nil, errors.New("cannot init genesis on non-zero state") - } - genesisCtx := services.NewGenesisContext(a.branch(zeroState)) - genesisState, err := genesisCtx.Mutate(ctx, func(ctx context.Context) error { - err = a.app.moduleManager.InitGenesisJSON(ctx, genesisJSON, txHandler) - if err != nil { - return fmt.Errorf("failed to init genesis: %w", err) - } - return nil - }) - - return genesisState, err -} - -// exportGenesis returns the app export genesis logic for modules -func (a *AppBuilder[T]) exportGenesis(ctx context.Context, version uint64) ([]byte, error) { - state, err := a.app.db.StateAt(version) - if err != nil { - return nil, fmt.Errorf("unable to get state at given version: %w", err) - } - - genesisJson, err := a.app.moduleManager.ExportGenesisForModules( - ctx, - func() store.WriterMap { - return a.branch(state) - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to export genesis: %w", err) - } - - bz, err := json.Marshal(genesisJson) - if err != nil { - return nil, fmt.Errorf("failed to marshal genesis: %w", err) - } - - return bz, nil -} - -// AppBuilderOption is a function that can be passed to AppBuilder.Build to customize the resulting app. -type AppBuilderOption[T transaction.Tx] func(*AppBuilder[T]) - -// AppBuilderWithBranch sets a custom branch implementation for the app. -func AppBuilderWithBranch[T transaction.Tx](branch func(state store.ReaderMap) store.WriterMap) AppBuilderOption[T] { - return func(a *AppBuilder[T]) { - a.branch = branch - } -} - -// AppBuilderWithTxValidator sets the tx validator for the app. -// It overrides all default tx validators defined by modules. -func AppBuilderWithTxValidator[T transaction.Tx]( - txValidators func( - ctx context.Context, tx T, - ) error, -) AppBuilderOption[T] { - return func(a *AppBuilder[T]) { - a.txValidator = txValidators - } -} - -// AppBuilderWithPostTxExec sets logic that will be executed after each transaction. -// When not provided, a no-op function will be used. -func AppBuilderWithPostTxExec[T transaction.Tx]( - postTxExec func( - ctx context.Context, tx T, success bool, - ) error, -) AppBuilderOption[T] { - return func(a *AppBuilder[T]) { - a.postTxExec = postTxExec - } -} diff --git a/runtime/v2/manager.go b/runtime/v2/manager.go deleted file mode 100644 index e2e90c27f808..000000000000 --- a/runtime/v2/manager.go +++ /dev/null @@ -1,861 +0,0 @@ -package runtime - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "maps" - "reflect" - "slices" - "sort" - - gogoproto "github.com/cosmos/gogoproto/proto" - "google.golang.org/grpc" - proto "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/reflect/protoregistry" - - runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" - cosmosmsg "cosmossdk.io/api/cosmos/msg/v1" - "cosmossdk.io/core/appmodule" - appmodulev2 "cosmossdk.io/core/appmodule/v2" - "cosmossdk.io/core/registry" - "cosmossdk.io/core/store" - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - "cosmossdk.io/runtime/v2/services" - "cosmossdk.io/server/v2/stf" -) - -type MM[T transaction.Tx] struct { - logger log.Logger - config *runtimev2.Module - modules map[string]appmodulev2.AppModule - migrationRegistrar *migrationRegistrar -} - -// NewModuleManager is the constructor for the module manager -// It handles all the interactions between the modules and the application -func NewModuleManager[T transaction.Tx]( - logger log.Logger, - config *runtimev2.Module, - modules map[string]appmodulev2.AppModule, -) *MM[T] { - // good defaults for the module manager order - modulesName := slices.Sorted(maps.Keys(modules)) - if len(config.PreBlockers) == 0 { - config.PreBlockers = modulesName - } - if len(config.BeginBlockers) == 0 { - config.BeginBlockers = modulesName - } - if len(config.EndBlockers) == 0 { - config.EndBlockers = modulesName - } - if len(config.TxValidators) == 0 { - config.TxValidators = modulesName - } - if len(config.InitGenesis) == 0 { - config.InitGenesis = modulesName - } - if len(config.ExportGenesis) == 0 { - config.ExportGenesis = modulesName - } - if len(config.OrderMigrations) == 0 { - config.OrderMigrations = defaultMigrationsOrder(modulesName) - } - - mm := &MM[T]{ - logger: logger, - config: config, - modules: modules, - migrationRegistrar: newMigrationRegistrar(), - } - - if err := mm.validateConfig(); err != nil { - panic(err) - } - - return mm -} - -// Modules returns the modules registered in the module manager -func (m *MM[T]) Modules() map[string]appmodulev2.AppModule { - return m.modules -} - -// RegisterLegacyAminoCodec registers all module codecs -func (m *MM[T]) RegisterLegacyAminoCodec(registrar registry.AminoRegistrar) { - for _, b := range m.modules { - if mod, ok := b.(appmodule.HasAminoCodec); ok { - mod.RegisterLegacyAminoCodec(registrar) - } - } -} - -// RegisterInterfaces registers all module interface types -func (m *MM[T]) RegisterInterfaces(registry registry.InterfaceRegistrar) { - for _, b := range m.modules { - if mod, ok := b.(appmodulev2.HasRegisterInterfaces); ok { - mod.RegisterInterfaces(registry) - } - } -} - -// DefaultGenesis provides default genesis information for all modules -func (m *MM[T]) DefaultGenesis() map[string]json.RawMessage { - genesisData := make(map[string]json.RawMessage) - for name, b := range m.modules { - if mod, ok := b.(appmodule.HasGenesisBasics); ok { - genesisData[name] = mod.DefaultGenesis() - } else if mod, ok := b.(appmodulev2.HasGenesis); ok { - genesisData[name] = mod.DefaultGenesis() - } else { - genesisData[name] = []byte("{}") - } - } - - return genesisData -} - -// ValidateGenesis performs genesis state validation for all modules -func (m *MM[T]) ValidateGenesis(genesisData map[string]json.RawMessage) error { - for name, b := range m.modules { - if mod, ok := b.(appmodule.HasGenesisBasics); ok { - if err := mod.ValidateGenesis(genesisData[name]); err != nil { - return err - } - } else if mod, ok := b.(appmodulev2.HasGenesis); ok { - if err := mod.ValidateGenesis(genesisData[name]); err != nil { - return err - } - } - } - - return nil -} - -// InitGenesisJSON performs init genesis functionality for modules from genesis data in JSON format -func (m *MM[T]) InitGenesisJSON( - ctx context.Context, - genesisData map[string]json.RawMessage, - txHandler func(json.RawMessage) error, -) error { - m.logger.Info("initializing blockchain state from genesis.json", "order", m.config.InitGenesis) - var seenValUpdates bool - for _, moduleName := range m.config.InitGenesis { - if genesisData[moduleName] == nil { - continue - } - - mod := m.modules[moduleName] - - // we might get an adapted module, a native core API module or a legacy module - switch module := mod.(type) { - case appmodule.HasGenesisAuto: - panic(fmt.Sprintf("module %s isn't server/v2 compatible", moduleName)) - case appmodulev2.GenesisDecoder: // GenesisDecoder needs to supersede HasGenesis and HasABCIGenesis. - genTxs, err := module.DecodeGenesisJSON(genesisData[moduleName]) - if err != nil { - return err - } - for _, jsonTx := range genTxs { - if err := txHandler(jsonTx); err != nil { - return fmt.Errorf("failed to handle genesis transaction: %w", err) - } - } - case appmodulev2.HasGenesis: - m.logger.Debug("running initialization for module", "module", moduleName) - if err := module.InitGenesis(ctx, genesisData[moduleName]); err != nil { - return fmt.Errorf("init module %s: %w", moduleName, err) - } - case appmodulev2.HasABCIGenesis: - m.logger.Debug("running initialization for module", "module", moduleName) - moduleValUpdates, err := module.InitGenesis(ctx, genesisData[moduleName]) - if err != nil { - return err - } - - // use these validator updates if provided, the module manager assumes - // only one module will update the validator set - if len(moduleValUpdates) > 0 { - if seenValUpdates { - return fmt.Errorf("validator InitGenesis updates already set by a previous module: current module %s", moduleName) - } else { - seenValUpdates = true - } - } - } - - } - return nil -} - -// ExportGenesisForModules performs export genesis functionality for modules -func (m *MM[T]) ExportGenesisForModules( - ctx context.Context, - stateFactory func() store.WriterMap, - modulesToExport ...string, -) (map[string]json.RawMessage, error) { - if len(modulesToExport) == 0 { - modulesToExport = m.config.ExportGenesis - } - // verify modules exists in app, so that we don't panic in the middle of an export - if err := m.checkModulesExists(modulesToExport); err != nil { - return nil, err - } - - type genesisResult struct { - bz json.RawMessage - err error - } - - type ModuleI interface { - ExportGenesis(ctx context.Context) (json.RawMessage, error) - } - - channels := make(map[string]chan genesisResult) - for _, moduleName := range modulesToExport { - mod := m.modules[moduleName] - var moduleI ModuleI - if module, hasGenesis := mod.(appmodulev2.HasGenesis); hasGenesis { - moduleI = module.(ModuleI) - } else if module, hasABCIGenesis := mod.(appmodulev2.HasABCIGenesis); hasABCIGenesis { - moduleI = module.(ModuleI) - } else { - continue - } - - channels[moduleName] = make(chan genesisResult) - go func(moduleI ModuleI, ch chan genesisResult) { - genesisCtx := services.NewGenesisContext(stateFactory()) - err := genesisCtx.Read(ctx, func(ctx context.Context) error { - jm, err := moduleI.ExportGenesis(ctx) - if err != nil { - return err - } - ch <- genesisResult{jm, nil} - return nil - }) - if err != nil { - ch <- genesisResult{nil, err} - } - }(moduleI, channels[moduleName]) - } - - genesisData := make(map[string]json.RawMessage) - for moduleName := range channels { - res := <-channels[moduleName] - if res.err != nil { - return nil, fmt.Errorf("genesis export error in %s: %w", moduleName, res.err) - } - - genesisData[moduleName] = res.bz - } - - return genesisData, nil -} - -// checkModulesExists verifies that all modules in the list exist in the app -func (m *MM[T]) checkModulesExists(moduleName []string) error { - for _, name := range moduleName { - if _, ok := m.modules[name]; !ok { - return fmt.Errorf("module %s does not exist", name) - } - } - - return nil -} - -// BeginBlock runs the begin-block logic of all modules -func (m *MM[T]) BeginBlock() func(ctx context.Context) error { - return func(ctx context.Context) error { - for _, moduleName := range m.config.BeginBlockers { - if module, ok := m.modules[moduleName].(appmodulev2.HasBeginBlocker); ok { - if err := module.BeginBlock(ctx); err != nil { - return fmt.Errorf("failed to run beginblocker for %s: %w", moduleName, err) - } - } - } - - return nil - } -} - -// hasABCIEndBlock is the legacy EndBlocker implemented by x/staking in the CosmosSDK -type hasABCIEndBlock interface { - EndBlock(context.Context) ([]appmodulev2.ValidatorUpdate, error) -} - -// EndBlock runs the end-block logic of all modules and tx validator updates -func (m *MM[T]) EndBlock() ( - endBlockFunc func(ctx context.Context) error, - valUpdateFunc func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error), -) { - var validatorUpdates []appmodulev2.ValidatorUpdate - endBlockFunc = func(ctx context.Context) error { - for _, moduleName := range m.config.EndBlockers { - if module, ok := m.modules[moduleName].(appmodulev2.HasEndBlocker); ok { - err := module.EndBlock(ctx) - if err != nil { - return fmt.Errorf("failed to run endblock for %s: %w", moduleName, err) - } - } else if module, ok := m.modules[moduleName].(hasABCIEndBlock); ok { // we need to keep this for our module compatibility promise - moduleValUpdates, err := module.EndBlock(ctx) - if err != nil { - return fmt.Errorf("failed to run enblock for %s: %w", moduleName, err) - } - // use these validator updates if provided, the module manager assumes - // only one module will update the validator set - if len(moduleValUpdates) > 0 { - if len(validatorUpdates) > 0 { - return errors.New("validator end block updates already set by a previous module") - } - - validatorUpdates = append(validatorUpdates, moduleValUpdates...) - } - } - } - - return nil - } - - valUpdateFunc = func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) { - // get validator updates of modules implementing directly the new HasUpdateValidators interface - for _, v := range m.modules { - if module, ok := v.(appmodulev2.HasUpdateValidators); ok { - moduleValUpdates, err := module.UpdateValidators(ctx) - if err != nil { - return nil, err - } - - if len(moduleValUpdates) > 0 { - if len(validatorUpdates) > 0 { - return nil, errors.New("validator end block updates already set by a previous module") - } - - validatorUpdates = append(validatorUpdates, moduleValUpdates...) - } - } - } - - // Reset validatorUpdates - res := validatorUpdates - validatorUpdates = []appmodulev2.ValidatorUpdate{} - - return res, nil - } - - return endBlockFunc, valUpdateFunc -} - -// PreBlocker runs the pre-block logic of all modules -func (m *MM[T]) PreBlocker() func(ctx context.Context, txs []T) error { - return func(ctx context.Context, txs []T) error { - for _, moduleName := range m.config.PreBlockers { - if module, ok := m.modules[moduleName].(appmodulev2.HasPreBlocker); ok { - if err := module.PreBlock(ctx); err != nil { - return fmt.Errorf("failed to run preblock for %s: %w", moduleName, err) - } - } - } - - return nil - } -} - -// TxValidators validates incoming transactions -func (m *MM[T]) TxValidators() func(ctx context.Context, tx T) error { - return func(ctx context.Context, tx T) error { - for _, moduleName := range m.config.TxValidators { - if module, ok := m.modules[moduleName].(appmodulev2.HasTxValidator[T]); ok { - if err := module.TxValidator(ctx, tx); err != nil { - return fmt.Errorf("failed to run tx validator for %s: %w", moduleName, err) - } - } - } - - return nil - } -} - -// RunMigrations performs in-place store migrations for all modules. This -// function MUST be called inside an x/upgrade UpgradeHandler. -// -// Recall that in an upgrade handler, the `fromVM` VersionMap is retrieved from -// x/upgrade's store, and the function needs to return the target VersionMap -// that will in turn be persisted to the x/upgrade's store. In general, -// returning RunMigrations should be enough: -// -// Example: -// -// app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM appmodule.VersionMap) (appmodule.VersionMap, error) { -// return app.ModuleManager().RunMigrations(ctx, fromVM) -// }) -// -// Internally, RunMigrations will perform the following steps: -// - create an `updatedVM` VersionMap of module with their latest ConsensusVersion -// - if module implements `HasConsensusVersion` interface get the consensus version as `toVersion`, -// if not `toVersion` is set to 0. -// - get `fromVersion` from `fromVM` with module's name. -// - if the module's name exists in `fromVM` map, then run in-place store migrations -// for that module between `fromVersion` and `toVersion`. -// - if the module does not exist in the `fromVM` (which means that it's a new module, -// because it was not in the previous x/upgrade's store), then run -// `InitGenesis` on that module. -// -// - return the `updatedVM` to be persisted in the x/upgrade's store. -// -// Migrations are run in an order defined by `mm.config.OrderMigrations`. -// -// As an app developer, if you wish to skip running InitGenesis for your new -// module "foo", you need to manually pass a `fromVM` argument to this function -// foo's module version set to its latest ConsensusVersion. That way, the diff -// between the function's `fromVM` and `udpatedVM` will be empty, hence not -// running anything for foo. -// -// Example: -// -// app.UpgradeKeeper.SetUpgradeHandler("my-plan", func(ctx context.Context, plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { -// // Assume "foo" is a new module. -// // `fromVM` is fetched from existing x/upgrade store. Since foo didn't exist -// // before this upgrade, `v, exists := fromVM["foo"]; exists == false`, and RunMigration will by default -// // run InitGenesis on foo. -// // To skip running foo's InitGenesis, you need set `fromVM`'s foo to its latest -// // consensus version: -// fromVM["foo"] = foo.AppModule{}.ConsensusVersion() -// -// return app.ModuleManager().RunMigrations(ctx, fromVM) -// }) -// -// Please also refer to https://docs.cosmos.network/main/core/upgrade for more information. -func (m *MM[T]) RunMigrations(ctx context.Context, fromVM appmodulev2.VersionMap) (appmodulev2.VersionMap, error) { - updatedVM := appmodulev2.VersionMap{} - for _, moduleName := range m.config.OrderMigrations { - module := m.modules[moduleName] - fromVersion, exists := fromVM[moduleName] - toVersion := uint64(0) - if module, ok := module.(appmodulev2.HasConsensusVersion); ok { - toVersion = module.ConsensusVersion() - } - - // We run migration if the module is specified in `fromVM`. - // Otherwise we run InitGenesis. - // - // The module won't exist in the fromVM in two cases: - // 1. A new module is added. In this case we run InitGenesis with an - // empty genesis state. - // 2. An existing chain is upgrading from version < 0.43 to v0.43+ for the first time. - // In this case, all modules have yet to be added to x/upgrade's VersionMap store. - if exists { - m.logger.Info(fmt.Sprintf("migrating module %s from version %d to version %d", moduleName, fromVersion, toVersion)) - if err := m.migrationRegistrar.RunModuleMigrations(ctx, moduleName, fromVersion, toVersion); err != nil { - return nil, err - } - } else { - m.logger.Info(fmt.Sprintf("adding a new module: %s", moduleName)) - if mod, ok := m.modules[moduleName].(appmodule.HasGenesis); ok { - if err := mod.InitGenesis(ctx, mod.DefaultGenesis()); err != nil { - return nil, fmt.Errorf("failed to run InitGenesis for %s: %w", moduleName, err) - } - } - if mod, ok := m.modules[moduleName].(appmodulev2.HasABCIGenesis); ok { - moduleValUpdates, err := mod.InitGenesis(ctx, mod.DefaultGenesis()) - if err != nil { - return nil, err - } - - // The module manager assumes only one module will update the validator set, and it can't be a new module. - if len(moduleValUpdates) > 0 { - return nil, errors.New("validator InitGenesis update is already set by another module") - } - } - } - - updatedVM[moduleName] = toVersion - } - - return updatedVM, nil -} - -// RegisterServices registers all module services. -func (m *MM[T]) RegisterServices(app *App[T]) error { - for _, module := range m.modules { - // register msg + query - if err := registerServices(module, app, protoregistry.GlobalFiles); err != nil { - return err - } - - // register migrations - if module, ok := module.(appmodulev2.HasMigrations); ok { - if err := module.RegisterMigrations(m.migrationRegistrar); err != nil { - return err - } - } - - // register pre and post msg - if module, ok := module.(appmodulev2.HasPreMsgHandlers); ok { - module.RegisterPreMsgHandlers(app.msgRouterBuilder) - } - - if module, ok := module.(appmodulev2.HasPostMsgHandlers); ok { - module.RegisterPostMsgHandlers(app.msgRouterBuilder) - } - } - - return nil -} - -// validateConfig validates the module manager configuration -// it asserts that all modules are defined in the configuration and that no modules are forgotten -func (m *MM[T]) validateConfig() error { - if err := m.assertNoForgottenModules("PreBlockers", m.config.PreBlockers, func(moduleName string) bool { - module := m.modules[moduleName] - _, hasPreBlock := module.(appmodulev2.HasPreBlocker) - return !hasPreBlock - }); err != nil { - return err - } - - if err := m.assertNoForgottenModules("BeginBlockers", m.config.BeginBlockers, func(moduleName string) bool { - module := m.modules[moduleName] - _, hasBeginBlock := module.(appmodulev2.HasBeginBlocker) - return !hasBeginBlock - }); err != nil { - return err - } - - if err := m.assertNoForgottenModules("EndBlockers", m.config.EndBlockers, func(moduleName string) bool { - module := m.modules[moduleName] - if _, hasEndBlock := module.(appmodulev2.HasEndBlocker); hasEndBlock { - return !hasEndBlock - } - - _, hasABCIEndBlock := module.(hasABCIEndBlock) - return !hasABCIEndBlock - }); err != nil { - return err - } - - if err := m.assertNoForgottenModules("TxValidators", m.config.TxValidators, func(moduleName string) bool { - module := m.modules[moduleName] - _, hasTxValidator := module.(appmodulev2.HasTxValidator[T]) - return !hasTxValidator - }); err != nil { - return err - } - - if err := m.assertNoForgottenModules("InitGenesis", m.config.InitGenesis, func(moduleName string) bool { - module := m.modules[moduleName] - if _, hasGenesis := module.(appmodule.HasGenesisAuto); hasGenesis { - panic(fmt.Sprintf("module %s isn't server/v2 compatible", moduleName)) - } - - if _, hasGenesis := module.(appmodulev2.HasGenesis); hasGenesis { - return !hasGenesis - } - - _, hasABCIGenesis := module.(appmodulev2.HasABCIGenesis) - return !hasABCIGenesis - }); err != nil { - return err - } - - if err := m.assertNoForgottenModules("ExportGenesis", m.config.ExportGenesis, func(moduleName string) bool { - module := m.modules[moduleName] - if _, hasGenesis := module.(appmodule.HasGenesisAuto); hasGenesis { - panic(fmt.Sprintf("module %s isn't server/v2 compatible", moduleName)) - } - - if _, hasGenesis := module.(appmodulev2.HasGenesis); hasGenesis { - return !hasGenesis - } - - _, hasABCIGenesis := module.(appmodulev2.HasABCIGenesis) - return !hasABCIGenesis - }); err != nil { - return err - } - - if err := m.assertNoForgottenModules("OrderMigrations", m.config.OrderMigrations, nil); err != nil { - return err - } - - return nil -} - -// assertNoForgottenModules checks that we didn't forget any modules in the *runtimev2.Module config. -// `pass` is a closure which allows one to omit modules from `moduleNames`. -// If you provide non-nil `pass` and it returns true, the module would not be subject of the assertion. -func (m *MM[T]) assertNoForgottenModules( - setOrderFnName string, - moduleNames []string, - pass func(moduleName string) bool, -) error { - ms := make(map[string]bool) - for _, m := range moduleNames { - ms[m] = true - } - var missing []string - for m := range m.modules { - if pass != nil && pass(m) { - continue - } - - if !ms[m] { - missing = append(missing, m) - } - } - - if len(missing) != 0 { - sort.Strings(missing) - return fmt.Errorf("all modules must be defined when setting %s, missing: %v", setOrderFnName, missing) - } - - return nil -} - -func registerServices[T transaction.Tx](s appmodulev2.AppModule, app *App[T], registry *protoregistry.Files) error { - // case module with services - if services, ok := s.(hasServicesV1); ok { - c := &configurator{ - queryHandlers: map[string]appmodulev2.Handler{}, - stfQueryRouter: app.queryRouterBuilder, - stfMsgRouter: app.msgRouterBuilder, - registry: registry, - err: nil, - } - if err := services.RegisterServices(c); err != nil { - return fmt.Errorf("unable to register services: %w", err) - } - - if c.err != nil { - app.logger.Warn("error registering services", "error", c.err) - } - - // merge maps - for path, decoder := range c.queryHandlers { - app.queryHandlers[path] = decoder - } - } - - // if module implements register msg handlers - if module, ok := s.(appmodulev2.HasMsgHandlers); ok { - wrapper := stfRouterWrapper{stfRouter: app.msgRouterBuilder} - module.RegisterMsgHandlers(&wrapper) - if wrapper.error != nil { - return fmt.Errorf("unable to register handlers: %w", wrapper.error) - } - } - - // if module implements register query handlers - if module, ok := s.(appmodulev2.HasQueryHandlers); ok { - wrapper := stfRouterWrapper{stfRouter: app.queryRouterBuilder} - module.RegisterQueryHandlers(&wrapper) - - for path, handler := range wrapper.handlers { - app.queryHandlers[path] = handler - } - } - - return nil -} - -var _ grpc.ServiceRegistrar = (*configurator)(nil) - -type configurator struct { - queryHandlers map[string]appmodulev2.Handler - - stfQueryRouter *stf.MsgRouterBuilder - stfMsgRouter *stf.MsgRouterBuilder - registry *protoregistry.Files - err error -} - -func (c *configurator) RegisterService(sd *grpc.ServiceDesc, ss interface{}) { - // first we check if it's a msg server - prefSd, err := c.registry.FindDescriptorByName(protoreflect.FullName(sd.ServiceName)) - if err != nil { - c.err = fmt.Errorf("register service: unable to find protov2 service descriptor: please make sure protov2 API counterparty is imported: %s", sd.ServiceName) - return - } - - if !proto.HasExtension(prefSd.(protoreflect.ServiceDescriptor).Options(), cosmosmsg.E_Service) { - err = c.registerQueryHandlers(sd, ss) - if err != nil { - c.err = err - } - } else { - err = c.registerMsgHandlers(sd, ss) - if err != nil { - c.err = err - } - } -} - -func (c *configurator) registerQueryHandlers(sd *grpc.ServiceDesc, ss interface{}) error { - for _, md := range sd.Methods { - // TODO(tip): what if a query is not deterministic? - - handler, err := grpcHandlerToAppModuleHandler(sd, md, ss) - if err != nil { - return fmt.Errorf("unable to make a appmodulev2.HandlerFunc from gRPC handler (%s, %s): %w", sd.ServiceName, md.MethodName, err) - } - - // register to stf query router. - err = c.stfQueryRouter.RegisterHandler(gogoproto.MessageName(handler.MakeMsg()), handler.Func) - if err != nil { - return fmt.Errorf("unable to register handler to stf router (%s, %s): %w", sd.ServiceName, md.MethodName, err) - } - - // register query handler using the same mapping used in stf - c.queryHandlers[gogoproto.MessageName(handler.MakeMsg())] = handler - } - return nil -} - -func (c *configurator) registerMsgHandlers(sd *grpc.ServiceDesc, ss interface{}) error { - for _, md := range sd.Methods { - handler, err := grpcHandlerToAppModuleHandler(sd, md, ss) - if err != nil { - return err - } - err = c.stfMsgRouter.RegisterHandler(gogoproto.MessageName(handler.MakeMsg()), handler.Func) - if err != nil { - return fmt.Errorf("unable to register msg handler %s.%s: %w", sd.ServiceName, md.MethodName, err) - } - } - return nil -} - -// grpcHandlerToAppModuleHandler converts a gRPC handler into an appmodulev2.HandlerFunc. -func grpcHandlerToAppModuleHandler( - sd *grpc.ServiceDesc, - md grpc.MethodDesc, - ss interface{}, -) (appmodulev2.Handler, error) { - requestName, responseName, err := requestFullNameFromMethodDesc(sd, md) - if err != nil { - return appmodulev2.Handler{}, err - } - - requestTyp := gogoproto.MessageType(string(requestName)) - if requestTyp == nil { - return appmodulev2.Handler{}, fmt.Errorf("no proto message found for %s", requestName) - } - responseTyp := gogoproto.MessageType(string(responseName)) - if responseTyp == nil { - return appmodulev2.Handler{}, fmt.Errorf("no proto message found for %s", responseName) - } - - handlerFunc := func( - ctx context.Context, - msg transaction.Msg, - ) (resp transaction.Msg, err error) { - res, err := md.Handler(ss, ctx, noopDecoder, messagePassingInterceptor(msg)) - if err != nil { - return nil, err - } - return res.(transaction.Msg), nil - } - - return appmodulev2.Handler{ - Func: handlerFunc, - MakeMsg: func() transaction.Msg { - return reflect.New(requestTyp.Elem()).Interface().(transaction.Msg) - }, - MakeMsgResp: func() transaction.Msg { - return reflect.New(responseTyp.Elem()).Interface().(transaction.Msg) - }, - }, nil -} - -func noopDecoder(_ interface{}) error { return nil } - -func messagePassingInterceptor(msg transaction.Msg) grpc.UnaryServerInterceptor { - return func( - ctx context.Context, - req interface{}, - _ *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, - ) (interface{}, error) { - return handler(ctx, msg) - } -} - -// requestFullNameFromMethodDesc returns the fully-qualified name of the request message and response of the provided service's method. -func requestFullNameFromMethodDesc(sd *grpc.ServiceDesc, method grpc.MethodDesc) ( - protoreflect.FullName, protoreflect.FullName, error, -) { - methodFullName := protoreflect.FullName(fmt.Sprintf("%s.%s", sd.ServiceName, method.MethodName)) - desc, err := gogoproto.HybridResolver.FindDescriptorByName(methodFullName) - if err != nil { - return "", "", fmt.Errorf("cannot find method descriptor %s", methodFullName) - } - methodDesc, ok := desc.(protoreflect.MethodDescriptor) - if !ok { - return "", "", fmt.Errorf("invalid method descriptor %s", methodFullName) - } - return methodDesc.Input().FullName(), methodDesc.Output().FullName(), nil -} - -// defaultMigrationsOrder returns a default migrations order: ascending alphabetical by module name, -// except x/auth which will run last, see: -// https://github.com/cosmos/cosmos-sdk/issues/10591 -func defaultMigrationsOrder(modules []string) []string { - const authName = "auth" - out := make([]string, 0, len(modules)) - hasAuth := false - for _, m := range modules { - if m == authName { - hasAuth = true - } else { - out = append(out, m) - } - } - sort.Strings(out) - if hasAuth { - out = append(out, authName) - } - return out -} - -// hasServicesV1 is the interface for registering service in baseapp Cosmos SDK. -// This API is part of core/appmodule but commented out for dependencies. -type hasServicesV1 interface { - RegisterServices(grpc.ServiceRegistrar) error -} - -var _ appmodulev2.MsgRouter = (*stfRouterWrapper)(nil) - -// stfRouterWrapper wraps the stf router and implements the core appmodulev2.MsgRouter -// interface. -// The difference between this type and stf router is that the stf router expects -// us to provide it the msg name, but the core router interface does not have -// such requirement. -type stfRouterWrapper struct { - stfRouter *stf.MsgRouterBuilder - - error error - - handlers map[string]appmodulev2.Handler -} - -func (s *stfRouterWrapper) RegisterHandler(handler appmodulev2.Handler) { - req := handler.MakeMsg() - requestName := gogoproto.MessageName(req) - if requestName == "" { - s.error = errors.Join(s.error, fmt.Errorf("unable to extract request name for type: %T", req)) - } - - // register handler to stf router - err := s.stfRouter.RegisterHandler(requestName, handler.Func) - s.error = errors.Join(s.error, err) - - // also make the decoder - if s.error == nil { - s.handlers = map[string]appmodulev2.Handler{} - } - s.handlers[requestName] = handler -} diff --git a/runtime/v2/module.go b/runtime/v2/module.go deleted file mode 100644 index 54d77dc2742f..000000000000 --- a/runtime/v2/module.go +++ /dev/null @@ -1,257 +0,0 @@ -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/event" - "cosmossdk.io/core/header" - "cosmossdk.io/core/registry" - "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" - "cosmossdk.io/store/v2/root" -) - -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(registrar grpc.ServiceRegistrar) error { - autoCliQueryService, err := services.NewAutoCLIQueryService(m.app.moduleManager.modules) - if err != nil { - return err - } - - autocliv1.RegisterQueryServer(registrar, autoCliQueryService) - - reflectionSvc, err := services.NewReflectionService() - if err != nil { - return err - } - reflectionv1.RegisterReflectionServiceServer(registrar, 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], - ProvideModuleManager[transaction.Tx], - ProvideEnvironment, - ProvideKVService, - ), - appconfig.Invoke(SetupAppBuilder), - ) -} - -func ProvideAppBuilder[T transaction.Tx]( - interfaceRegistrar registry.InterfaceRegistrar, - amino registry.AminoRegistrar, - storeBuilder root.Builder, -) ( - *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]{ - interfaceRegistrar: interfaceRegistrar, - amino: amino, - msgRouterBuilder: msgRouterBuilder, - queryRouterBuilder: stf.NewMsgRouterBuilder(), // TODO dedicated query router - queryHandlers: map[string]appmodulev2.Handler{}, - storeLoader: DefaultStoreLoader, - } - appBuilder := &AppBuilder[T]{app: app, storeBuilder: storeBuilder} - - 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 - StoreBuilder root.Builder -} - -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) - // STF requires some state to run - inputs.StoreBuilder.RegisterKey("stf") -} - -func ProvideModuleManager[T transaction.Tx]( - logger log.Logger, - config *runtimev2.Module, - modules map[string]appmodulev2.AppModule, -) *MM[T] { - return NewModuleManager[T](logger, config, modules) -} - -func ProvideKVService( - config *runtimev2.Module, - key depinject.ModuleKey, - kvFactory store.KVStoreServiceFactory, - storeBuilder root.Builder, -) (store.KVStoreService, store.MemoryStoreService) { - // skips modules that have no store - if slices.Contains(config.SkipStoreKeys, key.Name()) { - return &failingStoreService{}, &failingStoreService{} - } - var kvStoreKey string - override := storeKeyOverride(config, key.Name()) - if override != nil { - kvStoreKey = override.KvStoreKey - } else { - kvStoreKey = key.Name() - } - - storeBuilder.RegisterKey(kvStoreKey) - return kvFactory([]byte(kvStoreKey)), stf.NewMemoryStoreService([]byte(fmt.Sprintf("memory:%s", kvStoreKey))) -} - -func storeKeyOverride(config *runtimev2.Module, moduleName string) *runtimev2.StoreKeyConfig { - for _, cfg := range config.OverrideStoreKeys { - if cfg.ModuleName == moduleName { - return cfg - } - } - return nil -} - -// ProvideEnvironment provides the environment for keeper modules, while maintaining backward compatibility and provide services directly as well. -func ProvideEnvironment( - logger log.Logger, - key depinject.ModuleKey, - kvService store.KVStoreService, - memKvService store.MemoryStoreService, - headerService header.Service, - eventService event.Service, -) appmodulev2.Environment { - return appmodulev2.Environment{ - Logger: logger, - BranchService: stf.BranchService{}, - EventService: eventService, - GasService: stf.NewGasMeterService(), - HeaderService: headerService, - QueryRouterService: stf.NewQueryRouterService(), - MsgRouterService: stf.NewMsgRouterService([]byte(key.Name())), - TransactionService: services.NewContextAwareTransactionService(), - KVStoreService: kvService, - MemStoreService: memKvService, - } -} - -// DefaultServiceBindings provides default services for the following service interfaces: -// - store.KVStoreServiceFactory -// - header.Service -// - comet.Service -// - event.Service -// - store/v2/root.Builder -// -// They are all required. For most use cases these default services bindings should be sufficient. -// Power users (or tests) may wish to provide their own services bindings, in which case they must -// supply implementations for each of the above interfaces. -func DefaultServiceBindings() depinject.Config { - var ( - kvServiceFactory store.KVStoreServiceFactory = func(actor []byte) store.KVStoreService { - return services.NewGenesisKVService( - actor, - stf.NewKVStoreService(actor), - ) - } - cometService comet.Service = &services.ContextAwareCometInfoService{} - headerService = services.NewGenesisHeaderService(stf.HeaderService{}) - eventService = services.NewGenesisEventService(stf.NewEventService()) - storeBuilder = root.NewBuilder() - ) - return depinject.Supply( - kvServiceFactory, - headerService, - cometService, - eventService, - storeBuilder, - ) -} diff --git a/server/v2/api/grpc/server.go b/server/v2/api/grpc/server.go deleted file mode 100644 index 10e22a514a1e..000000000000 --- a/server/v2/api/grpc/server.go +++ /dev/null @@ -1,221 +0,0 @@ -package grpc - -import ( - "context" - "errors" - "fmt" - "io" - "maps" - "net" - "slices" - "strconv" - "strings" - "sync" - - gogoproto "github.com/cosmos/gogoproto/proto" - "github.com/spf13/pflag" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/reflect/protoreflect" - - appmodulev2 "cosmossdk.io/core/appmodule/v2" - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - serverv2 "cosmossdk.io/server/v2" - "cosmossdk.io/server/v2/api/grpc/gogoreflection" -) - -const ( - ServerName = "grpc" - - BlockHeightHeader = "x-cosmos-block-height" -) - -type Server[T transaction.Tx] struct { - logger log.Logger - config *Config - cfgOptions []CfgOption - - grpcSrv *grpc.Server -} - -// New creates a new grpc server. -func New[T transaction.Tx](cfgOptions ...CfgOption) *Server[T] { - return &Server[T]{ - cfgOptions: cfgOptions, - } -} - -// Init returns a correctly configured and initialized gRPC server. -// Note, the caller is responsible for starting the server. -func (s *Server[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logger log.Logger) error { - serverCfg := s.Config().(*Config) - if len(cfg) > 0 { - if err := serverv2.UnmarshalSubConfig(cfg, s.Name(), &serverCfg); err != nil { - return fmt.Errorf("failed to unmarshal config: %w", err) - } - } - methodsMap := appI.QueryHandlers() - - grpcSrv := grpc.NewServer( - grpc.ForceServerCodec(newProtoCodec(appI.InterfaceRegistry()).GRPCCodec()), - grpc.MaxSendMsgSize(serverCfg.MaxSendMsgSize), - grpc.MaxRecvMsgSize(serverCfg.MaxRecvMsgSize), - grpc.UnknownServiceHandler( - makeUnknownServiceHandler(methodsMap, appI), - ), - ) - - // Reflection allows external clients to see what services and methods the gRPC server exposes. - gogoreflection.Register(grpcSrv, slices.Collect(maps.Keys(methodsMap)), logger.With("sub-module", "grpc-reflection")) - - s.grpcSrv = grpcSrv - s.config = serverCfg - s.logger = logger.With(log.ModuleKey, s.Name()) - - return nil -} - -func (s *Server[T]) StartCmdFlags() *pflag.FlagSet { - flags := pflag.NewFlagSet(s.Name(), pflag.ExitOnError) - flags.String(FlagAddress, "localhost:9090", "Listen address") - return flags -} - -func makeUnknownServiceHandler(handlers map[string]appmodulev2.Handler, querier interface { - Query(ctx context.Context, version uint64, msg transaction.Msg) (transaction.Msg, error) -}, -) grpc.StreamHandler { - getRegistry := sync.OnceValues(gogoproto.MergedRegistry) - - return func(srv any, stream grpc.ServerStream) error { - method, ok := grpc.MethodFromServerStream(stream) - if !ok { - return status.Error(codes.InvalidArgument, "unable to get method") - } - // if this fails we cannot serve queries anymore... - registry, err := getRegistry() - if err != nil { - return fmt.Errorf("failed to get registry: %w", err) - } - - method = strings.TrimPrefix(method, "/") - fullName := protoreflect.FullName(strings.ReplaceAll(method, "/", ".")) - // get descriptor from the invoke method - desc, err := registry.FindDescriptorByName(fullName) - if err != nil { - return fmt.Errorf("failed to find descriptor %s: %w", method, err) - } - md, ok := desc.(protoreflect.MethodDescriptor) - if !ok { - return fmt.Errorf("%s is not a method", method) - } - // find handler - handler, exists := handlers[string(md.Input().FullName())] - if !exists { - return status.Errorf(codes.Unimplemented, "gRPC method %s is not handled", method) - } - - for { - req := handler.MakeMsg() - err := stream.RecvMsg(req) - if err != nil { - if errors.Is(err, io.EOF) { - return nil - } - return err - } - - // extract height header - ctx := stream.Context() - height, err := getHeightFromCtx(ctx) - if err != nil { - return status.Errorf(codes.InvalidArgument, "invalid get height from context: %v", err) - } - resp, err := querier.Query(ctx, height, req) - if err != nil { - return err - } - err = stream.SendMsg(resp) - if err != nil { - return err - } - } - } -} - -func getHeightFromCtx(ctx context.Context) (uint64, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return 0, nil - } - values := md.Get(BlockHeightHeader) - if len(values) == 0 { - return 0, nil - } - if len(values) != 1 { - return 0, fmt.Errorf("gRPC height metadata must be of length 1, got: %d", len(values)) - } - - heightStr := values[0] - height, err := strconv.ParseUint(heightStr, 10, 64) - if err != nil { - return 0, fmt.Errorf("unable to parse height string from gRPC metadata %s: %w", heightStr, err) - } - - return height, nil -} - -func (s *Server[T]) Name() string { - return ServerName -} - -func (s *Server[T]) Config() any { - if s.config == nil || s.config.Address == "" { - cfg := DefaultConfig() - // overwrite the default config with the provided options - for _, opt := range s.cfgOptions { - opt(cfg) - } - - return cfg - } - - return s.config -} - -func (s *Server[T]) Start(ctx context.Context) error { - if !s.config.Enable { - s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name())) - return nil - } - - listener, err := net.Listen("tcp", s.config.Address) - if err != nil { - return fmt.Errorf("failed to listen on address %s: %w", s.config.Address, err) - } - - s.logger.Info("starting gRPC server...", "address", s.config.Address) - if err := s.grpcSrv.Serve(listener); err != nil { - return fmt.Errorf("failed to start gRPC server: %w", err) - } - - return nil -} - -func (s *Server[T]) Stop(ctx context.Context) error { - if !s.config.Enable { - return nil - } - - s.logger.Info("stopping gRPC server...", "address", s.config.Address) - s.grpcSrv.GracefulStop() - return nil -} - -// GetGRPCServer returns the underlying gRPC server. -func (s *Server[T]) GetGRPCServer() *grpc.Server { - return s.grpcSrv -} diff --git a/server/v2/api/rest/handler.go b/server/v2/api/rest/handler.go deleted file mode 100644 index 8159e23ba3bb..000000000000 --- a/server/v2/api/rest/handler.go +++ /dev/null @@ -1,99 +0,0 @@ -package rest - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "reflect" - "strings" - - "github.com/cosmos/gogoproto/jsonpb" - gogoproto "github.com/cosmos/gogoproto/proto" - - "cosmossdk.io/core/transaction" - "cosmossdk.io/server/v2/appmanager" -) - -const ( - ContentTypeJSON = "application/json" - MaxBodySize = 1 << 20 // 1 MB -) - -func NewDefaultHandler[T transaction.Tx](appManager appmanager.AppManager[T]) http.Handler { - return &DefaultHandler[T]{appManager: appManager} -} - -type DefaultHandler[T transaction.Tx] struct { - appManager appmanager.AppManager[T] -} - -func (h *DefaultHandler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if err := h.validateMethodIsPOST(r); err != nil { - http.Error(w, err.Error(), http.StatusMethodNotAllowed) - return - } - - if err := h.validateContentTypeIsJSON(r); err != nil { - http.Error(w, err.Error(), http.StatusUnsupportedMediaType) - return - } - - msg, err := h.createMessage(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - query, err := h.appManager.Query(r.Context(), 0, msg) - if err != nil { - http.Error(w, "Error querying", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", ContentTypeJSON) - if err := json.NewEncoder(w).Encode(query); err != nil { - http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) - } -} - -// validateMethodIsPOST validates that the request method is POST. -func (h *DefaultHandler[T]) validateMethodIsPOST(r *http.Request) error { - if r.Method != http.MethodPost { - return fmt.Errorf("method not allowed") - } - return nil -} - -// validateContentTypeIsJSON validates that the request content type is JSON. -func (h *DefaultHandler[T]) validateContentTypeIsJSON(r *http.Request) error { - contentType := r.Header.Get("Content-Type") - if contentType != ContentTypeJSON { - return fmt.Errorf("unsupported content type, expected %s", ContentTypeJSON) - } - - return nil -} - -// createMessage creates the message by unmarshalling the request body. -func (h *DefaultHandler[T]) createMessage(r *http.Request) (gogoproto.Message, error) { - path := strings.TrimPrefix(r.URL.Path, "/") - requestType := gogoproto.MessageType(path) - if requestType == nil { - return nil, fmt.Errorf("unknown request type") - } - - msg, ok := reflect.New(requestType.Elem()).Interface().(gogoproto.Message) - if !ok { - return nil, fmt.Errorf("failed to create message instance") - } - - defer r.Body.Close() - limitedReader := io.LimitReader(r.Body, MaxBodySize) - err := jsonpb.Unmarshal(limitedReader, msg) - if err != nil { - return nil, fmt.Errorf("error parsing body: %w", err) - } - - return msg, nil -} diff --git a/server/v2/api/rest/server.go b/server/v2/api/rest/server.go deleted file mode 100644 index 0f2b1777973a..000000000000 --- a/server/v2/api/rest/server.go +++ /dev/null @@ -1,96 +0,0 @@ -package rest - -import ( - "context" - "errors" - "fmt" - "net/http" - - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - serverv2 "cosmossdk.io/server/v2" -) - -const ( - ServerName = "rest" -) - -type Server[T transaction.Tx] struct { - logger log.Logger - router *http.ServeMux - - httpServer *http.Server - config *Config - cfgOptions []CfgOption -} - -func New[T transaction.Tx](cfgOptions ...CfgOption) *Server[T] { - return &Server[T]{ - cfgOptions: cfgOptions, - } -} - -func (s *Server[T]) Name() string { - return ServerName -} - -func (s *Server[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logger log.Logger) error { - s.logger = logger.With(log.ModuleKey, s.Name()) - - serverCfg := s.Config().(*Config) - if len(cfg) > 0 { - if err := serverv2.UnmarshalSubConfig(cfg, s.Name(), &serverCfg); err != nil { - return fmt.Errorf("failed to unmarshal config: %w", err) - } - } - - s.router = http.NewServeMux() - s.router.Handle("/", NewDefaultHandler(appI)) - s.config = serverCfg - - return nil -} - -func (s *Server[T]) Start(ctx context.Context) error { - if !s.config.Enable { - s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name())) - return nil - } - - s.httpServer = &http.Server{ - Addr: s.config.Address, - Handler: s.router, - } - - s.logger.Info("starting HTTP server", "address", s.config.Address) - if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - s.logger.Error("failed to start HTTP server", "error", err) - return err - } - - return nil -} - -func (s *Server[T]) Stop(ctx context.Context) error { - if !s.config.Enable { - return nil - } - - s.logger.Info("stopping HTTP server") - - return s.httpServer.Shutdown(ctx) -} - -func (s *Server[T]) Config() any { - if s.config == nil || s.config.Address == "" { - cfg := DefaultConfig() - - for _, opt := range s.cfgOptions { - opt(cfg) - } - - return cfg - } - - return s.config -} diff --git a/server/v2/appmanager/appmanager.go b/server/v2/appmanager/appmanager.go deleted file mode 100644 index af54936ebf03..000000000000 --- a/server/v2/appmanager/appmanager.go +++ /dev/null @@ -1,221 +0,0 @@ -package appmanager - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - - "cosmossdk.io/core/server" - corestore "cosmossdk.io/core/store" - "cosmossdk.io/core/transaction" -) - -// AppManager is a coordinator for all things related to an application -// It is responsible for interacting with stf and store. -// Runtime/v2 is an extension of this interface. -type AppManager[T transaction.Tx] interface { - // InitGenesis initializes the genesis state of the application. - InitGenesis( - ctx context.Context, - blockRequest *server.BlockRequest[T], - initGenesisJSON []byte, - txDecoder transaction.Codec[T], - ) (*server.BlockResponse, corestore.WriterMap, error) - - // ExportGenesis exports the genesis state of the application. - ExportGenesis(ctx context.Context, version uint64) ([]byte, error) - - // DeliverBlock executes a block of transactions. - DeliverBlock( - ctx context.Context, - block *server.BlockRequest[T], - ) (*server.BlockResponse, corestore.WriterMap, error) - - // ValidateTx will validate the tx against the latest storage state. This means that - // only the stateful validation will be run, not the execution portion of the tx. - // If full execution is needed, Simulate must be used. - ValidateTx(ctx context.Context, tx T) (server.TxResult, error) - - // Simulate runs validation and execution flow of a Tx. - Simulate(ctx context.Context, tx T) (server.TxResult, corestore.WriterMap, error) - - // Query queries the application at the provided version. - // CONTRACT: Version must always be provided, if 0, get latest - Query(ctx context.Context, version uint64, request transaction.Msg) (transaction.Msg, error) - - // QueryWithState executes a query with the provided state. This allows to process a query - // independently of the db state. For example, it can be used to process a query with temporary - // and uncommitted state - QueryWithState(ctx context.Context, state corestore.ReaderMap, request transaction.Msg) (transaction.Msg, error) -} - -// Store defines the underlying storage behavior needed by AppManager. -type Store interface { - // StateLatest returns a readonly view over the latest - // committed state of the store. Alongside the version - // associated with it. - StateLatest() (uint64, corestore.ReaderMap, error) - - // StateAt returns a readonly view over the provided - // state. Must error when the version does not exist. - StateAt(version uint64) (corestore.ReaderMap, error) -} - -// appManager is a coordinator for all things related to an application -type appManager[T transaction.Tx] struct { - // Gas limits for validating, querying, and simulating transactions. - config Config - // InitGenesis is a function that initializes the application state from a genesis file. - // It takes a context, a source reader for the genesis file, and a transaction handler function. - initGenesis InitGenesis - // ExportGenesis is a function that exports the application state to a genesis file. - // It takes a context and a version number for the genesis file. - exportGenesis ExportGenesis - // The database for storing application data. - db Store - // The state transition function for processing transactions. - stf StateTransitionFunction[T] -} - -func New[T transaction.Tx]( - config Config, - db Store, - stf StateTransitionFunction[T], - initGenesisImpl InitGenesis, - exportGenesisImpl ExportGenesis, -) AppManager[T] { - return &appManager[T]{ - config: config, - db: db, - stf: stf, - initGenesis: initGenesisImpl, - exportGenesis: exportGenesisImpl, - } -} - -// InitGenesis initializes the genesis state of the application. -func (a appManager[T]) InitGenesis( - ctx context.Context, - blockRequest *server.BlockRequest[T], - initGenesisJSON []byte, - txDecoder transaction.Codec[T], -) (*server.BlockResponse, corestore.WriterMap, error) { - var genTxs []T - genesisState, err := a.initGenesis( - ctx, - bytes.NewBuffer(initGenesisJSON), - func(jsonTx json.RawMessage) error { - genTx, err := txDecoder.DecodeJSON(jsonTx) - if err != nil { - return fmt.Errorf("failed to decode genesis transaction: %w", err) - } - genTxs = append(genTxs, genTx) - return nil - }, - ) - if err != nil { - return nil, nil, fmt.Errorf("failed to import genesis state: %w", err) - } - // run block - blockRequest.Txs = genTxs - - blockResponse, blockZeroState, err := a.stf.DeliverBlock(ctx, blockRequest, genesisState) - if err != nil { - return blockResponse, nil, fmt.Errorf("failed to deliver block %d: %w", blockRequest.Height, err) - } - - // after executing block 0, we extract the changes and apply them to the genesis state. - stateChanges, err := blockZeroState.GetStateChanges() - if err != nil { - return nil, nil, fmt.Errorf("failed to get block zero state changes: %w", err) - } - - err = genesisState.ApplyStateChanges(stateChanges) - if err != nil { - return nil, nil, fmt.Errorf("failed to apply block zero state changes to genesis state: %w", err) - } - - return blockResponse, genesisState, err -} - -// ExportGenesis exports the genesis state of the application. -func (a appManager[T]) ExportGenesis(ctx context.Context, version uint64) ([]byte, error) { - if a.exportGenesis == nil { - return nil, errors.New("export genesis function not set") - } - - return a.exportGenesis(ctx, version) -} - -// DeliverBlock executes a block of transactions. -func (a appManager[T]) DeliverBlock( - ctx context.Context, - block *server.BlockRequest[T], -) (*server.BlockResponse, corestore.WriterMap, error) { - latestVersion, currentState, err := a.db.StateLatest() - if err != nil { - return nil, nil, fmt.Errorf("unable to create new state for height %d: %w", block.Height, err) - } - - if latestVersion+1 != block.Height { - return nil, nil, fmt.Errorf("invalid DeliverBlock height wanted %d, got %d", latestVersion+1, block.Height) - } - - blockResponse, newState, err := a.stf.DeliverBlock(ctx, block, currentState) - if err != nil { - return nil, nil, fmt.Errorf("block delivery failed: %w", err) - } - - return blockResponse, newState, nil -} - -// ValidateTx will validate the tx against the latest storage state. This means that -// only the stateful validation will be run, not the execution portion of the tx. -// If full execution is needed, Simulate must be used. -func (a appManager[T]) ValidateTx(ctx context.Context, tx T) (server.TxResult, error) { - _, latestState, err := a.db.StateLatest() - if err != nil { - return server.TxResult{}, err - } - res := a.stf.ValidateTx(ctx, latestState, a.config.ValidateTxGasLimit, tx) - return res, res.Error -} - -// Simulate runs validation and execution flow of a Tx. -func (a appManager[T]) Simulate(ctx context.Context, tx T) (server.TxResult, corestore.WriterMap, error) { - _, state, err := a.db.StateLatest() - if err != nil { - return server.TxResult{}, nil, err - } - result, cs := a.stf.Simulate(ctx, state, a.config.SimulationGasLimit, tx) // TODO: check if this is done in the antehandler - return result, cs, nil -} - -// Query queries the application at the provided version. -// CONTRACT: Version must always be provided, if 0, get latest -func (a appManager[T]) Query(ctx context.Context, version uint64, request transaction.Msg) (transaction.Msg, error) { - // if version is provided attempt to do a height query. - if version != 0 { - queryState, err := a.db.StateAt(version) - if err != nil { - return nil, err - } - return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request) - } - - // otherwise rely on latest available state. - _, queryState, err := a.db.StateLatest() - if err != nil { - return nil, err - } - return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request) -} - -// QueryWithState executes a query with the provided state. This allows to process a query -// independently of the db state. For example, it can be used to process a query with temporary -// and uncommitted state -func (a appManager[T]) QueryWithState(ctx context.Context, state corestore.ReaderMap, request transaction.Msg) (transaction.Msg, error) { - return a.stf.Query(ctx, state, a.config.QueryGasLimit, request) -} diff --git a/server/v2/appmanager/config.go b/server/v2/appmanager/config.go deleted file mode 100644 index fb3c20341f35..000000000000 --- a/server/v2/appmanager/config.go +++ /dev/null @@ -1,8 +0,0 @@ -package appmanager - -// Config represents the configuration options for the app manager. -type Config struct { - ValidateTxGasLimit uint64 `mapstructure:"validate-tx-gas-limit"` // TODO: check how this works on app mempool - QueryGasLimit uint64 `mapstructure:"query-gas-limit"` - SimulationGasLimit uint64 `mapstructure:"simulation-gas-limit"` -} diff --git a/server/v2/appmanager/genesis.go b/server/v2/appmanager/genesis.go deleted file mode 100644 index 347d0f30e07b..000000000000 --- a/server/v2/appmanager/genesis.go +++ /dev/null @@ -1,28 +0,0 @@ -package appmanager - -import ( - "context" - "encoding/json" - "io" - - "cosmossdk.io/core/store" -) - -type ( - // InitGenesis is a function that will run at application genesis, it will be called with - // the following arguments: - // - ctx: the context of the genesis operation - // - src: the source containing the raw genesis state - // - txHandler: a function capable of decoding a json tx, will be run for each genesis - // transaction - // - // It must return a map of the dirty state after the genesis operation. - InitGenesis func( - ctx context.Context, - src io.Reader, - txHandler func(json.RawMessage) error, - ) (store.WriterMap, error) - - // ExportGenesis is a function type that represents the export of the genesis state. - ExportGenesis func(ctx context.Context, version uint64) ([]byte, error) -) diff --git a/server/v2/appmanager/stf.go b/server/v2/appmanager/stf.go deleted file mode 100644 index 1e769c13ff9c..000000000000 --- a/server/v2/appmanager/stf.go +++ /dev/null @@ -1,43 +0,0 @@ -package appmanager - -import ( - "context" - - "cosmossdk.io/core/server" - "cosmossdk.io/core/store" - "cosmossdk.io/core/transaction" -) - -// StateTransitionFunction is an interface for processing transactions and blocks. -type StateTransitionFunction[T transaction.Tx] interface { - // DeliverBlock executes a block of transactions. - DeliverBlock( - ctx context.Context, - block *server.BlockRequest[T], - state store.ReaderMap, - ) (blockResult *server.BlockResponse, newState store.WriterMap, err error) - - // ValidateTx validates a transaction. - ValidateTx( - ctx context.Context, - state store.ReaderMap, - gasLimit uint64, - tx T, - ) server.TxResult - - // Simulate executes a transaction in simulation mode. - Simulate( - ctx context.Context, - state store.ReaderMap, - gasLimit uint64, - tx T, - ) (server.TxResult, store.WriterMap) - - // Query executes a query on the application. - Query( - ctx context.Context, - state store.ReaderMap, - gasLimit uint64, - req transaction.Msg, - ) (transaction.Msg, error) -} diff --git a/server/v2/server_test.go b/server/v2/server_test.go deleted file mode 100644 index a53b71fc35b9..000000000000 --- a/server/v2/server_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package serverv2_test - -import ( - "context" - "os" - "path/filepath" - "testing" - "time" - - gogoproto "github.com/cosmos/gogoproto/proto" - "github.com/spf13/viper" - "github.com/stretchr/testify/require" - - appmodulev2 "cosmossdk.io/core/appmodule/v2" - coreserver "cosmossdk.io/core/server" - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - serverv2 "cosmossdk.io/server/v2" - grpc "cosmossdk.io/server/v2/api/grpc" - "cosmossdk.io/server/v2/store" - storev2 "cosmossdk.io/store/v2" -) - -type mockInterfaceRegistry struct{} - -func (*mockInterfaceRegistry) Resolve(typeUrl string) (gogoproto.Message, error) { - panic("not implemented") -} - -func (*mockInterfaceRegistry) ListImplementations(ifaceTypeURL string) []string { - panic("not implemented") -} -func (*mockInterfaceRegistry) ListAllInterfaces() []string { panic("not implemented") } - -type mockApp[T transaction.Tx] struct { - serverv2.AppI[T] -} - -func (*mockApp[T]) QueryHandlers() map[string]appmodulev2.Handler { - return map[string]appmodulev2.Handler{} -} - -func (*mockApp[T]) InterfaceRegistry() coreserver.InterfaceRegistry { - return &mockInterfaceRegistry{} -} - -func (*mockApp[T]) Store() storev2.RootStore { - return nil -} - -func TestServer(t *testing.T) { - currentDir, err := os.Getwd() - require.NoError(t, err) - configPath := filepath.Join(currentDir, "testdata") - - v, err := serverv2.ReadConfig(configPath) - if err != nil { - v = viper.New() - } - cfg := v.AllSettings() - - logger := log.NewLogger(os.Stdout) - - ctx, err := serverv2.SetServerContext(context.Background(), v, logger) - require.NoError(t, err) - - grpcServer := grpc.New[transaction.Tx]() - err = grpcServer.Init(&mockApp[transaction.Tx]{}, cfg, logger) - require.NoError(t, err) - - storeServer := store.New[transaction.Tx]() - err = storeServer.Init(&mockApp[transaction.Tx]{}, cfg, logger) - require.NoError(t, err) - - mockServer := &mockServer{name: "mock-server-1", ch: make(chan string, 100)} - - server := serverv2.NewServer( - serverv2.DefaultServerConfig(), - grpcServer, - storeServer, - mockServer, - ) - - serverCfgs := server.Configs() - require.Equal(t, serverCfgs[grpcServer.Name()].(*grpc.Config).Address, grpc.DefaultConfig().Address) - require.Equal(t, serverCfgs[mockServer.Name()].(*mockServerConfig).MockFieldOne, MockServerDefaultConfig().MockFieldOne) - - // write config - err = server.WriteConfig(configPath) - require.NoError(t, err) - - v, err = serverv2.ReadConfig(configPath) - require.NoError(t, err) - - require.Equal(t, v.GetString(grpcServer.Name()+".address"), grpc.DefaultConfig().Address) - - // start empty - ctx, cancelFn := context.WithCancel(ctx) - go func() { - // wait 5sec and cancel context - <-time.After(5 * time.Second) - cancelFn() - - err = server.Stop(ctx) - require.NoError(t, err) - }() - - err = server.Start(ctx) - require.NoError(t, err) -} diff --git a/server/v2/stf/stf.go b/server/v2/stf/stf.go deleted file mode 100644 index 6b6f6b2c53da..000000000000 --- a/server/v2/stf/stf.go +++ /dev/null @@ -1,635 +0,0 @@ -package stf - -import ( - "context" - "errors" - "fmt" - - appmodulev2 "cosmossdk.io/core/appmodule/v2" - corecontext "cosmossdk.io/core/context" - "cosmossdk.io/core/event" - "cosmossdk.io/core/gas" - "cosmossdk.io/core/header" - "cosmossdk.io/core/log" - "cosmossdk.io/core/router" - "cosmossdk.io/core/server" - "cosmossdk.io/core/store" - "cosmossdk.io/core/transaction" - "cosmossdk.io/schema/appdata" - stfgas "cosmossdk.io/server/v2/stf/gas" - "cosmossdk.io/server/v2/stf/internal" -) - -type eContextKey struct{} - -var executionContextKey = eContextKey{} - -// STF is a struct that manages the state transition component of the app. -type STF[T transaction.Tx] struct { - logger log.Logger - - msgRouter coreRouterImpl - queryRouter coreRouterImpl - - doPreBlock func(ctx context.Context, txs []T) error - doBeginBlock func(ctx context.Context) error - doEndBlock func(ctx context.Context) error - doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) - - doTxValidation func(ctx context.Context, tx T) error - postTxExec func(ctx context.Context, tx T, success bool) error - - branchFn branchFn // branchFn is a function that given a readonly state it returns a writable version of it. - makeGasMeter makeGasMeterFn - makeGasMeteredState makeGasMeteredStateFn -} - -// New returns a new STF instance. -func New[T transaction.Tx]( - logger log.Logger, - msgRouterBuilder *MsgRouterBuilder, - queryRouterBuilder *MsgRouterBuilder, - doPreBlock func(ctx context.Context, txs []T) error, - doBeginBlock func(ctx context.Context) error, - doEndBlock func(ctx context.Context) error, - doTxValidation func(ctx context.Context, tx T) error, - doValidatorUpdate func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error), - postTxExec func(ctx context.Context, tx T, success bool) error, - branch func(store store.ReaderMap) store.WriterMap, -) (*STF[T], error) { - msgRouter, err := msgRouterBuilder.build() - if err != nil { - return nil, fmt.Errorf("build msg router: %w", err) - } - queryRouter, err := queryRouterBuilder.build() - if err != nil { - return nil, fmt.Errorf("build query router: %w", err) - } - - return &STF[T]{ - logger: logger, - msgRouter: msgRouter, - queryRouter: queryRouter, - doPreBlock: doPreBlock, - doBeginBlock: doBeginBlock, - doEndBlock: doEndBlock, - doValidatorUpdate: doValidatorUpdate, - doTxValidation: doTxValidation, - postTxExec: postTxExec, // TODO - branchFn: branch, - makeGasMeter: stfgas.DefaultGasMeter, - makeGasMeteredState: stfgas.DefaultWrapWithGasMeter, - }, nil -} - -// DeliverBlock is our state transition function. -// It takes a read only view of the state to apply the block to, -// executes the block and returns the block results and the new state. -func (s STF[T]) DeliverBlock( - ctx context.Context, - block *server.BlockRequest[T], - state store.ReaderMap, -) (blockResult *server.BlockResponse, newState store.WriterMap, err error) { - // creates a new branchFn state, from the readonly view of the state - // that can be written to. - newState = s.branchFn(state) - hi := header.Info{ - Hash: block.Hash, - AppHash: block.AppHash, - ChainID: block.ChainId, - Time: block.Time, - Height: int64(block.Height), - } - // set header info - err = s.setHeaderInfo(newState, hi) - if err != nil { - return nil, nil, fmt.Errorf("unable to set initial header info, %w", err) - } - - exCtx := s.makeContext(ctx, ConsensusIdentity, newState, internal.ExecModeFinalize) - exCtx.setHeaderInfo(hi) - - // reset events - exCtx.events = make([]event.Event, 0) - // pre block is called separate from begin block in order to prepopulate state - preBlockEvents, err := s.preBlock(exCtx, block.Txs) - if err != nil { - return nil, nil, err - } - - if err = isCtxCancelled(ctx); err != nil { - return nil, nil, err - } - - // reset events - exCtx.events = make([]event.Event, 0) - // begin block - var beginBlockEvents []event.Event - if !block.IsGenesis { - // begin block - beginBlockEvents, err = s.beginBlock(exCtx) - if err != nil { - return nil, nil, err - } - } - - // check if we need to return early - if err = isCtxCancelled(ctx); err != nil { - return nil, nil, err - } - - // execute txs - txResults := make([]server.TxResult, len(block.Txs)) - // TODO: skip first tx if vote extensions are enabled (marko) - for i, txBytes := range block.Txs { - // check if we need to return early or continue delivering txs - if err = isCtxCancelled(ctx); err != nil { - return nil, nil, err - } - txResults[i] = s.deliverTx(exCtx, newState, txBytes, transaction.ExecModeFinalize, hi, int32(i+1)) - } - // reset events - exCtx.events = make([]event.Event, 0) - // end block - endBlockEvents, valset, err := s.endBlock(exCtx) - if err != nil { - return nil, nil, err - } - - return &server.BlockResponse{ - ValidatorUpdates: valset, - PreBlockEvents: preBlockEvents, - BeginBlockEvents: beginBlockEvents, - TxResults: txResults, - EndBlockEvents: endBlockEvents, - }, newState, nil -} - -// deliverTx executes a TX and returns the result. -func (s STF[T]) deliverTx( - ctx context.Context, - state store.WriterMap, - tx T, - execMode transaction.ExecMode, - hi header.Info, - txIndex int32, -) server.TxResult { - // recover in the case of a panic - var recoveryError error - defer func() { - if r := recover(); r != nil { - recoveryError = fmt.Errorf("panic during transaction execution: %s", r) - s.logger.Error("panic during transaction execution", "error", recoveryError) - } - }() - // handle error from GetGasLimit - gasLimit, gasLimitErr := tx.GetGasLimit() - if gasLimitErr != nil { - return server.TxResult{ - Error: gasLimitErr, - } - } - - if recoveryError != nil { - return server.TxResult{ - Error: recoveryError, - } - } - validateGas, validationEvents, err := s.validateTx(ctx, state, gasLimit, tx, execMode) - if err != nil { - return server.TxResult{ - Error: err, - } - } - events := make([]event.Event, 0) - // set the event indexes, set MsgIndex to 0 in validation events - for i, e := range validationEvents { - e.BlockStage = appdata.TxProcessingStage - e.TxIndex = txIndex - e.MsgIndex = 0 - e.EventIndex = int32(i + 1) - events = append(events, e) - } - - execResp, execGas, execEvents, err := s.execTx(ctx, state, gasLimit-validateGas, tx, execMode, hi) - // set the TxIndex in the exec events - for _, e := range execEvents { - e.BlockStage = appdata.TxProcessingStage - e.TxIndex = txIndex - events = append(events, e) - } - - return server.TxResult{ - Events: events, - GasUsed: execGas + validateGas, - GasWanted: gasLimit, - Resp: execResp, - Error: err, - } -} - -// validateTx validates a transaction given the provided WritableState and gas limit. -// If the validation is successful, state is committed -func (s STF[T]) validateTx( - ctx context.Context, - state store.WriterMap, - gasLimit uint64, - tx T, - execMode transaction.ExecMode, -) (gasUsed uint64, events []event.Event, err error) { - validateState := s.branchFn(state) - hi, err := s.getHeaderInfo(validateState) - if err != nil { - return 0, nil, err - } - validateCtx := s.makeContext(ctx, RuntimeIdentity, validateState, execMode) - validateCtx.setHeaderInfo(hi) - validateCtx.setGasLimit(gasLimit) - err = s.doTxValidation(validateCtx, tx) - if err != nil { - return 0, nil, err - } - - consumed := validateCtx.meter.Limit() - validateCtx.meter.Remaining() - - return consumed, validateCtx.events, applyStateChanges(state, validateState) -} - -// execTx executes the tx messages on the provided state. If the tx fails then the state is discarded. -func (s STF[T]) execTx( - ctx context.Context, - state store.WriterMap, - gasLimit uint64, - tx T, - execMode transaction.ExecMode, - hi header.Info, -) ([]transaction.Msg, uint64, []event.Event, error) { - execState := s.branchFn(state) - - msgsResp, gasUsed, runTxMsgsEvents, txErr := s.runTxMsgs(ctx, execState, gasLimit, tx, execMode, hi) - if txErr != nil { - // in case of error during message execution, we do not apply the exec state. - // instead we run the post exec handler in a new branchFn from the initial state. - postTxState := s.branchFn(state) - postTxCtx := s.makeContext(ctx, RuntimeIdentity, postTxState, execMode) - postTxCtx.setHeaderInfo(hi) - - postTxErr := s.postTxExec(postTxCtx, tx, false) - if postTxErr != nil { - // if the post tx handler fails, then we do not apply any state change to the initial state. - // we just return the exec gas used and a joined error from TX error and post TX error. - return nil, gasUsed, nil, errors.Join(txErr, postTxErr) - } - // in case post tx is successful, then we commit the post tx state to the initial state, - // and we return post tx events alongside exec gas used and the error of the tx. - applyErr := applyStateChanges(state, postTxState) - if applyErr != nil { - return nil, 0, nil, applyErr - } - // set the event indexes, set MsgIndex to -1 in post tx events - for i := range postTxCtx.events { - postTxCtx.events[i].EventIndex = int32(i + 1) - postTxCtx.events[i].MsgIndex = -1 - } - - return nil, gasUsed, postTxCtx.events, txErr - } - // tx execution went fine, now we use the same state to run the post tx exec handler, - // in case the execution of the post tx fails, then no state change is applied and the - // whole execution step is rolled back. - postTxCtx := s.makeContext(ctx, RuntimeIdentity, execState, execMode) // NO gas limit. - postTxCtx.setHeaderInfo(hi) - postTxErr := s.postTxExec(postTxCtx, tx, true) - if postTxErr != nil { - // if post tx fails, then we do not apply any state change, we return the post tx error, - // alongside the gas used. - return nil, gasUsed, nil, postTxErr - } - // both the execution and post tx execution step were successful, so we apply the state changes - // to the provided state, and we return responses, and events from exec tx and post tx exec. - applyErr := applyStateChanges(state, execState) - if applyErr != nil { - return nil, 0, nil, applyErr - } - // set the event indexes, set MsgIndex to -1 in post tx events - for i := range postTxCtx.events { - postTxCtx.events[i].EventIndex = int32(i + 1) - postTxCtx.events[i].MsgIndex = -1 - } - - return msgsResp, gasUsed, append(runTxMsgsEvents, postTxCtx.events...), nil -} - -// runTxMsgs will execute the messages contained in the TX with the provided state. -func (s STF[T]) runTxMsgs( - ctx context.Context, - state store.WriterMap, - gasLimit uint64, - tx T, - execMode transaction.ExecMode, - hi header.Info, -) ([]transaction.Msg, uint64, []event.Event, error) { - txSenders, err := tx.GetSenders() - if err != nil { - return nil, 0, nil, err - } - msgs, err := tx.GetMessages() - if err != nil { - return nil, 0, nil, err - } - msgResps := make([]transaction.Msg, len(msgs)) - - execCtx := s.makeContext(ctx, RuntimeIdentity, state, execMode) - execCtx.setHeaderInfo(hi) - execCtx.setGasLimit(gasLimit) - events := make([]event.Event, 0) - for i, msg := range msgs { - execCtx.sender = txSenders[i] - execCtx.events = make([]event.Event, 0) // reset events - resp, err := s.msgRouter.Invoke(execCtx, msg) - if err != nil { - return nil, 0, nil, err // do not wrap the error or we lose the original error type - } - msgResps[i] = resp - for j, e := range execCtx.events { - e.MsgIndex = int32(i + 1) - e.EventIndex = int32(j + 1) - events = append(events, e) - } - } - - consumed := execCtx.meter.Limit() - execCtx.meter.Remaining() - return msgResps, consumed, events, nil -} - -// preBlock executes the pre block logic. -func (s STF[T]) preBlock( - ctx *executionContext, - txs []T, -) ([]event.Event, error) { - err := s.doPreBlock(ctx, txs) - if err != nil { - return nil, err - } - - for i := range ctx.events { - ctx.events[i].BlockStage = appdata.PreBlockStage - ctx.events[i].EventIndex = int32(i + 1) - } - - return ctx.events, nil -} - -// beginBlock executes the begin block logic. -func (s STF[T]) beginBlock( - ctx *executionContext, -) (beginBlockEvents []event.Event, err error) { - err = s.doBeginBlock(ctx) - if err != nil { - return nil, err - } - - for i := range ctx.events { - ctx.events[i].BlockStage = appdata.BeginBlockStage - ctx.events[i].EventIndex = int32(i + 1) - } - - return ctx.events, nil -} - -// endBlock executes the end block logic. -func (s STF[T]) endBlock( - ctx *executionContext, -) ([]event.Event, []appmodulev2.ValidatorUpdate, error) { - err := s.doEndBlock(ctx) - if err != nil { - return nil, nil, err - } - events := ctx.events - ctx.events = make([]event.Event, 0) // reset events - valsetUpdates, err := s.validatorUpdates(ctx) - if err != nil { - return nil, nil, err - } - events = append(events, ctx.events...) - for i := range events { - events[i].BlockStage = appdata.EndBlockStage - events[i].EventIndex = int32(i + 1) - } - - return events, valsetUpdates, nil -} - -// validatorUpdates returns the validator updates for the current block. It is called by endBlock after the endblock execution has concluded -func (s STF[T]) validatorUpdates( - ctx *executionContext, -) ([]appmodulev2.ValidatorUpdate, error) { - valSetUpdates, err := s.doValidatorUpdate(ctx) - if err != nil { - return nil, err - } - return valSetUpdates, nil -} - -// Simulate simulates the execution of a tx on the provided state. -func (s STF[T]) Simulate( - ctx context.Context, - state store.ReaderMap, - gasLimit uint64, - tx T, -) (server.TxResult, store.WriterMap) { - simulationState := s.branchFn(state) - hi, err := s.getHeaderInfo(simulationState) - if err != nil { - return server.TxResult{}, nil - } - txr := s.deliverTx(ctx, simulationState, tx, internal.ExecModeSimulate, hi, 0) - - return txr, simulationState -} - -// ValidateTx will run only the validation steps required for a transaction. -// Validations are run over the provided state, with the provided gas limit. -func (s STF[T]) ValidateTx( - ctx context.Context, - state store.ReaderMap, - gasLimit uint64, - tx T, -) server.TxResult { - validationState := s.branchFn(state) - gasUsed, events, err := s.validateTx(ctx, validationState, gasLimit, tx, transaction.ExecModeCheck) - return server.TxResult{ - Events: events, - GasUsed: gasUsed, - Error: err, - } -} - -// Query executes the query on the provided state with the provided gas limits. -func (s STF[T]) Query( - ctx context.Context, - state store.ReaderMap, - gasLimit uint64, - req transaction.Msg, -) (transaction.Msg, error) { - queryState := s.branchFn(state) - hi, err := s.getHeaderInfo(queryState) - if err != nil { - return nil, err - } - queryCtx := s.makeContext(ctx, nil, queryState, internal.ExecModeSimulate) - queryCtx.setHeaderInfo(hi) - queryCtx.setGasLimit(gasLimit) - return s.queryRouter.Invoke(queryCtx, req) -} - -// clone clones STF. -func (s STF[T]) clone() STF[T] { - return STF[T]{ - logger: s.logger, - msgRouter: s.msgRouter, - queryRouter: s.queryRouter, - doPreBlock: s.doPreBlock, - doBeginBlock: s.doBeginBlock, - doEndBlock: s.doEndBlock, - doValidatorUpdate: s.doValidatorUpdate, - doTxValidation: s.doTxValidation, - postTxExec: s.postTxExec, - branchFn: s.branchFn, - makeGasMeter: s.makeGasMeter, - makeGasMeteredState: s.makeGasMeteredState, - } -} - -// executionContext is a struct that holds the context for the execution of a tx. -type executionContext struct { - context.Context - - // unmeteredState is storage without metering. Changes here are propagated to state which is the metered - // version. - unmeteredState store.WriterMap - // state is the gas metered state. - state store.WriterMap - // meter is the gas meter. - meter gas.Meter - // events are the current events. - events []event.Event - // sender is the causer of the state transition. - sender transaction.Identity - // headerInfo contains the block info. - headerInfo header.Info - // execMode retains information about the exec mode. - execMode transaction.ExecMode - - branchFn branchFn - makeGasMeter makeGasMeterFn - makeGasMeteredStore makeGasMeteredStateFn - - msgRouter router.Service - queryRouter router.Service -} - -// setHeaderInfo sets the header info in the state to be used by queries in the future. -func (e *executionContext) setHeaderInfo(hi header.Info) { - e.headerInfo = hi -} - -// setGasLimit will update the gas limit of the *executionContext -func (e *executionContext) setGasLimit(limit uint64) { - meter := e.makeGasMeter(limit) - meteredState := e.makeGasMeteredStore(meter, e.unmeteredState) - - e.meter = meter - e.state = meteredState -} - -func (e *executionContext) Value(key any) any { - if key == executionContextKey { - return e - } - - return e.Context.Value(key) -} - -// TODO: too many calls to makeContext can be expensive -// makeContext creates and returns a new execution context for the STF[T] type. -// It takes in the following parameters: -// - ctx: The context.Context object for the execution. -// - sender: The transaction.Identity object representing the sender of the transaction. -// - state: The store.WriterMap object for accessing and modifying the state. -// - gasLimit: The maximum amount of gas allowed for the execution. -// - execMode: The corecontext.ExecMode object representing the execution mode. -// -// It returns a pointer to the executionContext struct -func (s STF[T]) makeContext( - ctx context.Context, - sender transaction.Identity, - store store.WriterMap, - execMode transaction.ExecMode, -) *executionContext { - valuedCtx := context.WithValue(ctx, corecontext.ExecModeKey, execMode) - return newExecutionContext( - valuedCtx, - s.makeGasMeter, - s.makeGasMeteredState, - s.branchFn, - sender, - store, - execMode, - s.msgRouter, - s.queryRouter, - ) -} - -func newExecutionContext( - ctx context.Context, - makeGasMeterFn makeGasMeterFn, - makeGasMeteredStoreFn makeGasMeteredStateFn, - branchFn branchFn, - sender transaction.Identity, - state store.WriterMap, - execMode transaction.ExecMode, - msgRouter coreRouterImpl, - queryRouter coreRouterImpl, -) *executionContext { - meter := makeGasMeterFn(gas.NoGasLimit) - meteredState := makeGasMeteredStoreFn(meter, state) - - return &executionContext{ - Context: ctx, - unmeteredState: state, - state: meteredState, - meter: meter, - events: make([]event.Event, 0), - headerInfo: header.Info{}, - execMode: execMode, - sender: sender, - branchFn: branchFn, - makeGasMeter: makeGasMeterFn, - makeGasMeteredStore: makeGasMeteredStoreFn, - msgRouter: msgRouter, - queryRouter: queryRouter, - } -} - -// applyStateChanges applies the state changes from the source store to the destination store. -// It retrieves the state changes from the source store using GetStateChanges method, -// and then applies those changes to the destination store using ApplyStateChanges method. -// If an error occurs during the retrieval or application of state changes, it is returned. -func applyStateChanges(dst, src store.WriterMap) error { - changes, err := src.GetStateChanges() - if err != nil { - return err - } - return dst.ApplyStateChanges(changes) -} - -// isCtxCancelled reports if the context was canceled. -func isCtxCancelled(ctx context.Context) error { - select { - case <-ctx.Done(): - return ctx.Err() - default: - return nil - } -} diff --git a/server/v2/store/server.go b/server/v2/store/server.go deleted file mode 100644 index 1fafe4e25d53..000000000000 --- a/server/v2/store/server.go +++ /dev/null @@ -1,91 +0,0 @@ -package store - -import ( - "context" - "fmt" - - "github.com/spf13/cobra" - - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - serverv2 "cosmossdk.io/server/v2" - storev2 "cosmossdk.io/store/v2" - "cosmossdk.io/store/v2/root" -) - -var ( - _ serverv2.ServerComponent[transaction.Tx] = (*Server[transaction.Tx])(nil) - _ serverv2.HasConfig = (*Server[transaction.Tx])(nil) - _ serverv2.HasCLICommands = (*Server[transaction.Tx])(nil) -) - -const ServerName = "store" - -// Server manages store config and contains prune & snapshot commands -type Server[T transaction.Tx] struct { - config *root.Config - backend storev2.Backend -} - -func New[T transaction.Tx]() *Server[T] { - return &Server[T]{} -} - -func (s *Server[T]) Init(app serverv2.AppI[T], v map[string]any, _ log.Logger) (err error) { - s.backend = app.Store() - s.config, err = UnmarshalConfig(v) - return err -} - -func (s *Server[T]) Name() string { - return ServerName -} - -func (s *Server[T]) Start(context.Context) error { - return nil -} - -func (s *Server[T]) Stop(context.Context) error { - return nil -} - -func (s *Server[T]) CLICommands() serverv2.CLIConfig { - return serverv2.CLIConfig{ - Commands: []*cobra.Command{ - s.PrunesCmd(), - s.ExportSnapshotCmd(), - s.DeleteSnapshotCmd(), - s.ListSnapshotsCmd(), - s.DumpArchiveCmd(), - s.LoadArchiveCmd(), - s.RestoreSnapshotCmd(s.backend), - }, - } -} - -func (s *Server[T]) Config() any { - if s.config == nil || s.config.AppDBBackend == "" { - return root.DefaultConfig() - } - - return s.config -} - -// UnmarshalConfig unmarshals the store config from the given map. -// If the config is not found in the map, the default config is returned. -// If the home directory is found in the map, it sets the home directory in the config. -// An empty home directory *is* permitted at this stage, but attempting to build -// the store with an empty home directory will fail. -func UnmarshalConfig(cfg map[string]any) (*root.Config, error) { - config := &root.Config{ - Options: root.DefaultStoreOptions(), - } - if err := serverv2.UnmarshalSubConfig(cfg, ServerName, config); err != nil { - return nil, fmt.Errorf("failed to unmarshal store config: %w", err) - } - home := cfg[serverv2.FlagHome] - if home != nil { - config.Home = home.(string) - } - return config, nil -} diff --git a/server/v2/types.go b/server/v2/types.go deleted file mode 100644 index 40d51e42375c..000000000000 --- a/server/v2/types.go +++ /dev/null @@ -1,26 +0,0 @@ -package serverv2 - -import ( - "github.com/spf13/viper" - - appmodulev2 "cosmossdk.io/core/appmodule/v2" - "cosmossdk.io/core/server" - "cosmossdk.io/core/transaction" - "cosmossdk.io/log" - "cosmossdk.io/schema/decoding" - "cosmossdk.io/server/v2/appmanager" - "cosmossdk.io/store/v2" -) - -type AppCreator[T transaction.Tx] func(log.Logger, *viper.Viper) AppI[T] - -type AppI[T transaction.Tx] interface { - appmanager.AppManager[T] - - Name() string - InterfaceRegistry() server.InterfaceRegistry - QueryHandlers() map[string]appmodulev2.Handler - Store() store.RootStore - SchemaDecoderResolver() decoding.DecoderResolver - Close() error -} From 860ec891e5e1b5685a5a3ee23c44a48404476aab Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Mon, 21 Oct 2024 15:56:08 +0200 Subject: [PATCH 3/4] bump --- server/v2/cometbft/go.mod | 6 +++--- server/v2/cometbft/go.sum | 12 ++++++------ simapp/v2/go.mod | 8 ++++---- simapp/v2/go.sum | 16 ++++++++-------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/server/v2/cometbft/go.mod b/server/v2/cometbft/go.mod index 798c43f4ff04..1f32529d28eb 100644 --- a/server/v2/cometbft/go.mod +++ b/server/v2/cometbft/go.mod @@ -21,9 +21,9 @@ require ( cosmossdk.io/core v1.0.0-alpha.4 cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 cosmossdk.io/log v1.4.1 - cosmossdk.io/server/v2 v2.0.0-20241014060734-0b43fcc2164c // main - cosmossdk.io/server/v2/appmanager v0.0.0-20241008175849-325728a9fd6c // main - cosmossdk.io/server/v2/stf v0.0.0-20241008175849-325728a9fd6c // main + cosmossdk.io/server/v2 v2.0.0-20241021134528-681366e3469c // main + cosmossdk.io/server/v2/appmanager v0.0.0-20241021134528-681366e3469c // main + cosmossdk.io/server/v2/stf v0.0.0-20241021134528-681366e3469c // main cosmossdk.io/store/v2 v2.0.0-20241017091405-f01baf302e2b // main cosmossdk.io/x/consensus v0.0.0-00010101000000-000000000000 github.com/cometbft/cometbft v1.0.0-rc1.0.20240908111210-ab0be101882f diff --git a/server/v2/cometbft/go.sum b/server/v2/cometbft/go.sum index a0318ff165a1..fc398299e102 100644 --- a/server/v2/cometbft/go.sum +++ b/server/v2/cometbft/go.sum @@ -24,12 +24,12 @@ cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac h1:3joNZZWZ3k7fMsrBDL1ktuQ2xQwYLZOaDhkruadDFmc= cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= -cosmossdk.io/server/v2 v2.0.0-20241014060734-0b43fcc2164c h1:P6Em4H2U4nSem6VzgCDZgu0tzqDGmhHe6FN48vnd8TQ= -cosmossdk.io/server/v2 v2.0.0-20241014060734-0b43fcc2164c/go.mod h1:mqEGRyHXCcB1ozs177VfstarUVYWKogXdqGET4vUT24= -cosmossdk.io/server/v2/appmanager v0.0.0-20241008175849-325728a9fd6c h1:MJCOTFyL7lPlMDUFvylEu/2zyFe7NcYe4eMaZowvzk4= -cosmossdk.io/server/v2/appmanager v0.0.0-20241008175849-325728a9fd6c/go.mod h1:/xDfniqVtn5nraiHkNJ4e6rYU0e83YAGsSjwmUA6H8k= -cosmossdk.io/server/v2/stf v0.0.0-20241008175849-325728a9fd6c h1:thOij3diZWxwfKaSJNS6S1SFX+fnOW93emnuu+WSHJY= -cosmossdk.io/server/v2/stf v0.0.0-20241008175849-325728a9fd6c/go.mod h1:MjuTMonZ319tZQX6CV2O5E/+F85KrkNUj5U5ObrrkWs= +cosmossdk.io/server/v2 v2.0.0-20241021134528-681366e3469c h1:LK1ycnBLeAE/anYr8lGSg36GB5iDaBnmv0qcS/jbsCs= +cosmossdk.io/server/v2 v2.0.0-20241021134528-681366e3469c/go.mod h1:FMGQ99V9+I3gpQV7nidWAtxMSRAwRizt2wqbAD7Z81I= +cosmossdk.io/server/v2/appmanager v0.0.0-20241021134528-681366e3469c h1:yKf19uDz3nJNEcud3G2KAzIf/dxkurGD0gSef5sZVfs= +cosmossdk.io/server/v2/appmanager v0.0.0-20241021134528-681366e3469c/go.mod h1:o20qgxElItJvBh+k9DJedXE2tp6udkNrWaf5Fn1mSb4= +cosmossdk.io/server/v2/stf v0.0.0-20241021134528-681366e3469c h1:MbjFJxzzr1vuXBI9ih/agdTl9fX4nvzhOBbsi38I7Yk= +cosmossdk.io/server/v2/stf v0.0.0-20241021134528-681366e3469c/go.mod h1:MjuTMonZ319tZQX6CV2O5E/+F85KrkNUj5U5ObrrkWs= cosmossdk.io/store v1.0.0-rc.0.0.20241009154331-597e0fac1173 h1:MlvTcx2h4zmZZtIDg35B6bovbb5iUAExPmvaPE1Zci4= cosmossdk.io/store v1.0.0-rc.0.0.20241009154331-597e0fac1173/go.mod h1:lrhcXu/hRXrLJP4L8syVbs68GJU1WSRBFO3mmjn5oGc= cosmossdk.io/store/v2 v2.0.0-20241017091405-f01baf302e2b h1:epPBD1ebUwat6Ruw40rsEU4N9CHJ2ZN0ev8EGoNYBvE= diff --git a/simapp/v2/go.mod b/simapp/v2/go.mod index 287e7439d270..6791000cceb3 100644 --- a/simapp/v2/go.mod +++ b/simapp/v2/go.mod @@ -10,8 +10,8 @@ require ( cosmossdk.io/depinject v1.0.0 cosmossdk.io/log v1.4.1 cosmossdk.io/math v1.3.0 - cosmossdk.io/runtime/v2 v2.0.0-20241017091405-f01baf302e2b // main - cosmossdk.io/server/v2 v2.0.0-20241014060734-0b43fcc2164c // main + cosmossdk.io/runtime/v2 v2.0.0-20241021134528-681366e3469c // main + cosmossdk.io/server/v2 v2.0.0-20241021134528-681366e3469c // main cosmossdk.io/server/v2/cometbft v0.0.0-00010101000000-000000000000 cosmossdk.io/store/v2 v2.0.0-20241017091405-f01baf302e2b // main cosmossdk.io/tools/confix v0.0.0-00010101000000-000000000000 @@ -62,8 +62,8 @@ require ( cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac // indirect - cosmossdk.io/server/v2/appmanager v0.0.0-20241008175849-325728a9fd6c // indirect; main - cosmossdk.io/server/v2/stf v0.0.0-20241008175849-325728a9fd6c // indirect; main + cosmossdk.io/server/v2/appmanager v0.0.0-20241021134528-681366e3469c // indirect; main + cosmossdk.io/server/v2/stf v0.0.0-20241021134528-681366e3469c // indirect; main cosmossdk.io/store v1.1.1-0.20240909133312-50288938d1b6 // indirect; main cosmossdk.io/x/tx v0.13.4-0.20241003111526-30003f667944 // indirect; main filippo.io/edwards25519 v1.1.0 // indirect diff --git a/simapp/v2/go.sum b/simapp/v2/go.sum index c703f95e8963..ee331e702305 100644 --- a/simapp/v2/go.sum +++ b/simapp/v2/go.sum @@ -210,16 +210,16 @@ cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= cosmossdk.io/math v1.3.0/go.mod h1:vnRTxewy+M7BtXBNFybkuhSH4WfedVAAnERHgVFhp3k= -cosmossdk.io/runtime/v2 v2.0.0-20241017091405-f01baf302e2b h1:h/jxffgtZTMa1qvqz9+5c4uOCxpcEWxYuwHONjd16IM= -cosmossdk.io/runtime/v2 v2.0.0-20241017091405-f01baf302e2b/go.mod h1:KROAOJt1EDL7nELDT7LPpFqWubhTJFD4Uwa1gy2/yxo= +cosmossdk.io/runtime/v2 v2.0.0-20241021134528-681366e3469c h1:f9qU7HpJ1CuUDj4OkHca4BFbSW4X3rAYgwzwFNXiBhg= +cosmossdk.io/runtime/v2 v2.0.0-20241021134528-681366e3469c/go.mod h1:KROAOJt1EDL7nELDT7LPpFqWubhTJFD4Uwa1gy2/yxo= cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac h1:3joNZZWZ3k7fMsrBDL1ktuQ2xQwYLZOaDhkruadDFmc= cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= -cosmossdk.io/server/v2 v2.0.0-20241014060734-0b43fcc2164c h1:P6Em4H2U4nSem6VzgCDZgu0tzqDGmhHe6FN48vnd8TQ= -cosmossdk.io/server/v2 v2.0.0-20241014060734-0b43fcc2164c/go.mod h1:mqEGRyHXCcB1ozs177VfstarUVYWKogXdqGET4vUT24= -cosmossdk.io/server/v2/appmanager v0.0.0-20241008175849-325728a9fd6c h1:MJCOTFyL7lPlMDUFvylEu/2zyFe7NcYe4eMaZowvzk4= -cosmossdk.io/server/v2/appmanager v0.0.0-20241008175849-325728a9fd6c/go.mod h1:/xDfniqVtn5nraiHkNJ4e6rYU0e83YAGsSjwmUA6H8k= -cosmossdk.io/server/v2/stf v0.0.0-20241008175849-325728a9fd6c h1:thOij3diZWxwfKaSJNS6S1SFX+fnOW93emnuu+WSHJY= -cosmossdk.io/server/v2/stf v0.0.0-20241008175849-325728a9fd6c/go.mod h1:MjuTMonZ319tZQX6CV2O5E/+F85KrkNUj5U5ObrrkWs= +cosmossdk.io/server/v2 v2.0.0-20241021134528-681366e3469c h1:LK1ycnBLeAE/anYr8lGSg36GB5iDaBnmv0qcS/jbsCs= +cosmossdk.io/server/v2 v2.0.0-20241021134528-681366e3469c/go.mod h1:FMGQ99V9+I3gpQV7nidWAtxMSRAwRizt2wqbAD7Z81I= +cosmossdk.io/server/v2/appmanager v0.0.0-20241021134528-681366e3469c h1:yKf19uDz3nJNEcud3G2KAzIf/dxkurGD0gSef5sZVfs= +cosmossdk.io/server/v2/appmanager v0.0.0-20241021134528-681366e3469c/go.mod h1:o20qgxElItJvBh+k9DJedXE2tp6udkNrWaf5Fn1mSb4= +cosmossdk.io/server/v2/stf v0.0.0-20241021134528-681366e3469c h1:MbjFJxzzr1vuXBI9ih/agdTl9fX4nvzhOBbsi38I7Yk= +cosmossdk.io/server/v2/stf v0.0.0-20241021134528-681366e3469c/go.mod h1:MjuTMonZ319tZQX6CV2O5E/+F85KrkNUj5U5ObrrkWs= cosmossdk.io/store v1.0.0-rc.0.0.20240913190136-3bc707a5a214 h1:UUW0+2UgbDwQ452o2aw4DrVSWmowcad7DB7Vln+N94I= cosmossdk.io/store v1.0.0-rc.0.0.20240913190136-3bc707a5a214/go.mod h1:ct8HATr+s48YYTRXEyP3HF33v9qEVWHMxwOL8P/v4iQ= cosmossdk.io/store/v2 v2.0.0-20241017091405-f01baf302e2b h1:epPBD1ebUwat6Ruw40rsEU4N9CHJ2ZN0ev8EGoNYBvE= From 2451f6484982727f7bc22000dc5460edbe945a6b Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Mon, 21 Oct 2024 15:57:44 +0200 Subject: [PATCH 4/4] conflict --- server/v2/cometbft/server.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/server/v2/cometbft/server.go b/server/v2/cometbft/server.go index 0e5439194b9f..02c53b014bc9 100644 --- a/server/v2/cometbft/server.go +++ b/server/v2/cometbft/server.go @@ -131,23 +131,6 @@ func (s *CometBFTServer[T]) Init(appI serverv2.AppI[T], cfg map[string]any, logg return err } consensus.snapshotManager = snapshots.NewManager(snapshotStore, s.serverOptions.SnapshotOptions(cfg), sc, ss, nil, s.logger) - -<<<<<<< HEAD -======= - // initialize the indexer - if indexerCfg := s.config.AppTomlConfig.Indexer; len(indexerCfg.Target) > 0 { - listener, err := indexer.StartIndexing(indexer.IndexingOptions{ - Config: indexerCfg, - Resolver: appI.SchemaDecoderResolver(), - Logger: s.logger.With(log.ModuleKey, "indexer"), - }) - if err != nil { - return fmt.Errorf("failed to start indexing: %w", err) - } - consensus.listener = &listener.Listener - } - ->>>>>>> 681366e34 (refactor(runtime/v2): simplify app manager (#22300)) s.Consensus = consensus return nil