From c1836f60a1cbc58982d78914c09637e84ac3162d Mon Sep 17 00:00:00 2001 From: Randy Grok <98407738+randygrok@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:15:45 +0200 Subject: [PATCH] feat: export genesis in simapp v2 (#21199) Co-authored-by: marbar3778 (cherry picked from commit aeeaca64da2c1d579196557d63f8c053bcb6bb33) # Conflicts: # runtime/v2/app.go # runtime/v2/builder.go # runtime/v2/manager.go # server/v2/appmanager/appmanager.go # server/v2/cometbft/go.mod # simapp/app.go # simapp/v2/go.mod --- runtime/v2/app.go | 124 +++++ runtime/v2/builder.go | 232 ++++++++++ runtime/v2/manager.go | 704 +++++++++++++++++++++++++++++ server/v2/appmanager/appmanager.go | 188 ++++++++ server/v2/cometbft/go.mod | 6 + simapp/app.go | 10 +- simapp/v2/app_di.go | 2 - simapp/v2/app_test.go | 155 +++++++ simapp/v2/export.go | 27 +- simapp/v2/go.mod | 17 +- simapp/v2/simdv2/cmd/commands.go | 37 +- x/genutil/v2/cli/commands.go | 47 ++ x/genutil/v2/cli/export.go | 106 +++++ x/genutil/v2/types.go | 27 ++ x/staking/genesis.go | 3 +- 15 files changed, 1649 insertions(+), 36 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 server/v2/appmanager/appmanager.go create mode 100644 simapp/v2/app_test.go create mode 100644 x/genutil/v2/cli/commands.go create mode 100644 x/genutil/v2/cli/export.go create mode 100644 x/genutil/v2/types.go diff --git a/runtime/v2/app.go b/runtime/v2/app.go new file mode 100644 index 000000000000..08f2498255c5 --- /dev/null +++ b/runtime/v2/app.go @@ -0,0 +1,124 @@ +package runtime + +import ( + "encoding/json" + "errors" + + gogoproto "github.com/cosmos/gogoproto/proto" + "golang.org/x/exp/slices" + + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" + "cosmossdk.io/core/legacy" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/server/v2/stf" +) + +// App is a wrapper around AppManager and ModuleManager that can be used in hybrid +// app.go/app config scenarios or directly as a servertypes.Application instance. +// To get an instance of *App, *AppBuilder must be requested as a dependency +// in a container which declares the runtime module and the AppBuilder.Build() +// method must be called. +// +// App can be used to create a hybrid app.go setup where some configuration is +// done declaratively with an app config and the rest of it is done the old way. +// See simapp/app_v2.go for an example of this setup. +type App[T transaction.Tx] struct { + *appmanager.AppManager[T] + + // app manager dependencies + stf *stf.STF[T] + msgRouterBuilder *stf.MsgRouterBuilder + queryRouterBuilder *stf.MsgRouterBuilder + db Store + + // app configuration + logger log.Logger + config *runtimev2.Module + + // modules configuration + storeKeys []string + interfaceRegistrar registry.InterfaceRegistrar + amino legacy.Amino + moduleManager *MM[T] + + // GRPCMethodsToMessageMap maps gRPC method name to a function that decodes the request + // bytes into a gogoproto.Message, which then can be passed to appmanager. + GRPCMethodsToMessageMap map[string]func() gogoproto.Message +} + +// Name returns the app name. +func (a *App[T]) Name() string { + return a.config.AppName +} + +// Logger returns the app logger. +func (a *App[T]) Logger() log.Logger { + return a.logger +} + +// ModuleManager returns the module manager. +func (a *App[T]) ModuleManager() *MM[T] { + return a.moduleManager +} + +// DefaultGenesis returns a default genesis from the registered modules. +func (a *App[T]) DefaultGenesis() map[string]json.RawMessage { + return a.moduleManager.DefaultGenesis() +} + +// LoadLatest loads the latest version. +func (a *App[T]) LoadLatest() error { + return a.db.LoadLatestVersion() +} + +// LoadHeight loads a particular height +func (a *App[T]) LoadHeight(height uint64) error { + return a.db.LoadVersion(height) +} + +// LoadLatestHeight loads the latest height. +func (a *App[T]) LoadLatestHeight() (uint64, error) { + return a.db.GetLatestVersion() +} + +// Close is called in start cmd to gracefully cleanup resources. +func (a *App[T]) Close() error { + return nil +} + +// GetStoreKeys returns all the app store keys. +func (a *App[T]) GetStoreKeys() []string { + return a.storeKeys +} + +// UnsafeFindStoreKey fetches a registered StoreKey from the App in linear time. +// NOTE: This should only be used in testing. +func (a *App[T]) UnsafeFindStoreKey(storeKey string) (string, error) { + i := slices.IndexFunc(a.storeKeys, func(s string) bool { return s == storeKey }) + if i == -1 { + return "", errors.New("store key not found") + } + + return a.storeKeys[i], nil +} + +// GetStore returns the app store. +func (a *App[T]) GetStore() Store { + return a.db +} + +// GetLogger returns the app logger. +func (a *App[T]) GetLogger() log.Logger { + return a.logger +} + +func (a *App[T]) GetAppManager() *appmanager.AppManager[T] { + return a.AppManager +} + +func (a *App[T]) GetGPRCMethodsToMessageMap() map[string]func() gogoproto.Message { + return a.GRPCMethodsToMessageMap +} diff --git a/runtime/v2/builder.go b/runtime/v2/builder.go new file mode 100644 index 000000000000..578cd9934a6e --- /dev/null +++ b/runtime/v2/builder.go @@ -0,0 +1,232 @@ +package runtime + +import ( + "context" + "encoding/json" + "fmt" + "io" + "path/filepath" + + "github.com/spf13/viper" + + "cosmossdk.io/core/appmodule" + appmodulev2 "cosmossdk.io/core/appmodule/v2" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/server/v2/appmanager" + "cosmossdk.io/server/v2/stf" + "cosmossdk.io/server/v2/stf/branch" + "cosmossdk.io/store/v2/db" + rootstore "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] + storeOptions *rootstore.FactoryOptions + viper *viper.Viper + + // 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 +} + +// DefaultGenesis returns a default genesis from the registered AppModule's. +func (a *AppBuilder[T]) DefaultGenesis() map[string]json.RawMessage { + return a.app.moduleManager.DefaultGenesis() +} + +// 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 +} + +// RegisterStores registers the provided store keys. +// This method should only be used for registering extra stores +// which is necessary for modules that not registered using the app config. +// To be used in combination of RegisterModules. +func (a *AppBuilder[T]) RegisterStores(keys ...string) { + a.app.storeKeys = append(a.app.storeKeys, keys...) + if a.storeOptions != nil { + a.storeOptions.StoreKeys = append(a.storeOptions.StoreKeys, keys...) + } +} + +// 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 + } + } + + if err := a.app.moduleManager.RegisterServices(a.app); err != nil { + return nil, err + } + + endBlocker, valUpdate := a.app.moduleManager.EndBlock() + + stf, err := stf.NewSTF[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 + + v := a.viper + home := v.GetString(FlagHome) + + storeOpts := rootstore.DefaultStoreOptions() + if s := v.Sub("store.options"); s != nil { + if err := s.Unmarshal(&storeOpts); err != nil { + return nil, fmt.Errorf("failed to store options: %w", err) + } + } + + scRawDb, err := db.NewDB(db.DBType(v.GetString("store.app-db-backend")), "application", filepath.Join(home, "data"), nil) + if err != nil { + panic(err) + } + + storeOptions := &rootstore.FactoryOptions{ + Logger: a.app.logger, + RootDir: home, + Options: storeOpts, + StoreKeys: append(a.app.storeKeys, "stf"), + SCRawDB: scRawDb, + } + a.storeOptions = storeOptions + + rs, err := rootstore.CreateRootStore(a.storeOptions) + if err != nil { + return nil, fmt.Errorf("failed to create root store: %w", err) + } + a.app.db = rs + + appManagerBuilder := appmanager.Builder[T]{ + STF: a.app.stf, + DB: a.app.db, + ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit, + QueryGasLimit: a.app.config.GasConfig.QueryGasLimit, + SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit, + InitGenesis: func(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) error { + // this implementation assumes that the state is a JSON object + bz, err := io.ReadAll(src) + if err != nil { + return fmt.Errorf("failed to read import state: %w", err) + } + var genesisState map[string]json.RawMessage + if err = json.Unmarshal(bz, &genesisState); err != nil { + return err + } + if err = a.app.moduleManager.InitGenesisJSON(ctx, genesisState, txHandler); err != nil { + return fmt.Errorf("failed to init genesis: %w", err) + } + return nil + }, + ExportGenesis: func(ctx context.Context, version uint64) ([]byte, error) { + genesisJson, err := a.app.moduleManager.ExportGenesisForModules(ctx) + 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 + }, + } + + appManager, err := appManagerBuilder.Build() + if err != nil { + return nil, fmt.Errorf("failed to build app manager: %w", err) + } + a.app.AppManager = appManager + + return a.app, 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..093140bcd370 --- /dev/null +++ b/runtime/v2/manager.go @@ -0,0 +1,704 @@ +package runtime + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + "sort" + + gogoproto "github.com/cosmos/gogoproto/proto" + "golang.org/x/exp/maps" + "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/legacy" + "cosmossdk.io/core/registry" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + "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 := 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(cdc legacy.Amino) { + for _, b := range m.modules { + if mod, ok := b.(appmodule.HasAminoCodec); ok { + mod.RegisterLegacyAminoCodec(cdc) + } + } +} + +// 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, + 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 ModuleI interface { + ExportGenesis(ctx context.Context) (json.RawMessage, error) + } + + genesisData := make(map[string]json.RawMessage) + + // TODO: make async export genesis https://github.com/cosmos/cosmos-sdk/issues/21303 + 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.HasGenesis); hasABCIGenesis { + moduleI = module.(ModuleI) + } else { + continue + } + + res, err := moduleI.ExportGenesis(ctx) + if err != nil { + return nil, err + } + + genesisData[moduleName] = res + } + + 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 + } +} + +// TODO write as descriptive godoc as module manager v1. +// TODO include feedback from https://github.com/cosmos/cosmos-sdk/issues/15120 +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 services, ok := module.(hasServicesV1); ok { + if err := registerServices(services, 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 + } + } + + // TODO: register pre and post msg + } + + 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] + _, hasBlock := module.(appmodulev2.HasPreBlocker) + return !hasBlock + }); 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 { + m := m + 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 hasServicesV1, app *App[T], registry *protoregistry.Files) error { + c := &configurator{ + grpcQueryDecoders: map[string]func() gogoproto.Message{}, + stfQueryRouter: app.queryRouterBuilder, + stfMsgRouter: app.msgRouterBuilder, + registry: registry, + err: nil, + } + + err := s.RegisterServices(c) + if err != nil { + return fmt.Errorf("unable to register services: %w", err) + } + // merge maps + for path, decoder := range c.grpcQueryDecoders { + app.GRPCMethodsToMessageMap[path] = decoder + } + return nil +} + +var _ grpc.ServiceRegistrar = (*configurator)(nil) + +type configurator struct { + // grpcQueryDecoders is required because module expose queries through gRPC + // this provides a way to route to modules using gRPC. + grpcQueryDecoders map[string]func() gogoproto.Message + + 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? + requestFullName, err := registerMethod(c.stfQueryRouter, sd, md, ss) + if err != nil { + return fmt.Errorf("unable to register query handler %s: %w", md.MethodName, err) + } + + // register gRPC query method. + typ := gogoproto.MessageType(requestFullName) + if typ == nil { + return fmt.Errorf("unable to find message in gogotype registry: %w", err) + } + decoderFunc := func() gogoproto.Message { + return reflect.New(typ.Elem()).Interface().(gogoproto.Message) + } + methodName := fmt.Sprintf("/%s/%s", sd.ServiceName, md.MethodName) + c.grpcQueryDecoders[methodName] = decoderFunc + } + return nil +} + +func (c *configurator) registerMsgHandlers(sd *grpc.ServiceDesc, ss interface{}) error { + for _, md := range sd.Methods { + _, err := registerMethod(c.stfMsgRouter, sd, md, ss) + if err != nil { + return fmt.Errorf("unable to register msg handler %s: %w", md.MethodName, err) + } + } + return nil +} + +// requestFullNameFromMethodDesc returns the fully-qualified name of the request message of the provided service's method. +func requestFullNameFromMethodDesc(sd *grpc.ServiceDesc, method grpc.MethodDesc) (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(), nil +} + +func registerMethod( + stfRouter *stf.MsgRouterBuilder, + sd *grpc.ServiceDesc, + md grpc.MethodDesc, + ss interface{}, +) (string, error) { + requestName, err := requestFullNameFromMethodDesc(sd, md) + if err != nil { + return "", err + } + + return string(requestName), stfRouter.RegisterHandler(string(requestName), 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 + }) +} + +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) + } +} + +// 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 +} diff --git a/server/v2/appmanager/appmanager.go b/server/v2/appmanager/appmanager.go new file mode 100644 index 000000000000..284b1dcb0965 --- /dev/null +++ b/server/v2/appmanager/appmanager.go @@ -0,0 +1,188 @@ +package appmanager + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + + appmanager "cosmossdk.io/core/app" + corestore "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" +) + +// 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 { + config Config + + db Store + + initGenesis InitGenesis + exportGenesis ExportGenesis + + stf StateTransitionFunction[T] +} + +// InitGenesis initializes the genesis state of the application. +func (a AppManager[T]) InitGenesis( + ctx context.Context, + blockRequest *appmanager.BlockRequest[T], + initGenesisJSON []byte, + txDecoder transaction.Codec[T], +) (*appmanager.BlockResponse, corestore.WriterMap, error) { + v, zeroState, err := a.db.StateLatest() + if err != nil { + return nil, 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, nil, errors.New("cannot init genesis on non-zero state") + } + + var genTxs []T + genesisState, err := a.stf.RunWithCtx(ctx, zeroState, func(ctx context.Context) error { + return 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) { + zeroState, err := a.db.StateAt(version) + if err != nil { + return nil, fmt.Errorf("unable to get latest state: %w", err) + } + + bz := make([]byte, 0) + _, err = a.stf.RunWithCtx(ctx, zeroState, func(ctx context.Context) error { + if a.exportGenesis == nil { + return errors.New("export genesis function not set") + } + + bz, err = a.exportGenesis(ctx, version) + if err != nil { + return fmt.Errorf("failed to export genesis state: %w", err) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to export genesis state: %w", err) + } + + return bz, nil +} + +func (a AppManager[T]) DeliverBlock( + ctx context.Context, + block *appmanager.BlockRequest[T], +) (*appmanager.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) (appmanager.TxResult, error) { + _, latestState, err := a.db.StateLatest() + if err != nil { + return appmanager.TxResult{}, err + } + return a.stf.ValidateTx(ctx, latestState, a.config.ValidateTxGasLimit, tx), nil +} + +// Simulate runs validation and execution flow of a Tx. +func (a AppManager[T]) Simulate(ctx context.Context, tx T) (appmanager.TxResult, corestore.WriterMap, error) { + _, state, err := a.db.StateLatest() + if err != nil { + return appmanager.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/cometbft/go.mod b/server/v2/cometbft/go.mod index acaa2d7bcc2b..065147dcb151 100644 --- a/server/v2/cometbft/go.mod +++ b/server/v2/cometbft/go.mod @@ -23,9 +23,15 @@ require ( cosmossdk.io/core v1.0.0 // main cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.4.0 +<<<<<<< HEAD cosmossdk.io/server/v2 v2.0.0-20240802142126-a26970e547ab // main cosmossdk.io/server/v2/appmanager v0.0.0-20240731205446-aee9803a0af6 // main cosmossdk.io/store/v2 v2.0.0-20240731205446-aee9803a0af6 // main +======= + cosmossdk.io/server/v2 v2.0.0-00010101000000-000000000000 + cosmossdk.io/server/v2/appmanager v0.0.0-20240802110823-cffeedff643d + cosmossdk.io/store/v2 v2.0.0-00010101000000-000000000000 +>>>>>>> aeeaca64d (feat: export genesis in simapp v2 (#21199)) cosmossdk.io/x/consensus v0.0.0-00010101000000-000000000000 github.com/cometbft/cometbft v1.0.0-rc1 github.com/cometbft/cometbft/api v1.0.0-rc.1 diff --git a/simapp/app.go b/simapp/app.go index 77cd0773eff3..b1daf9aab6cd 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -50,7 +50,11 @@ import ( circuittypes "cosmossdk.io/x/circuit/types" "cosmossdk.io/x/consensus" consensusparamkeeper "cosmossdk.io/x/consensus/keeper" +<<<<<<< HEAD consensusparamtypes "cosmossdk.io/x/consensus/types" +======= + consensustypes "cosmossdk.io/x/consensus/types" +>>>>>>> aeeaca64d (feat: export genesis in simapp v2 (#21199)) distr "cosmossdk.io/x/distribution" distrkeeper "cosmossdk.io/x/distribution/keeper" distrtypes "cosmossdk.io/x/distribution/types" @@ -262,7 +266,7 @@ func NewSimApp( keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, - govtypes.StoreKey, consensusparamtypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey, + govtypes.StoreKey, consensustypes.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey, evidencetypes.StoreKey, circuittypes.StoreKey, authzkeeper.StoreKey, nftkeeper.StoreKey, group.StoreKey, pooltypes.StoreKey, accounts.StoreKey, epochstypes.StoreKey, @@ -285,7 +289,7 @@ func NewSimApp( cometService := runtime.NewContextAwareCometInfoService() // set the BaseApp's parameter store - app.ConsensusParamsKeeper = consensusparamkeeper.NewKeeper(appCodec, runtime.NewEnvironment(runtime.NewKVStoreService(keys[consensusparamtypes.StoreKey]), logger.With(log.ModuleKey, "x/consensus")), authtypes.NewModuleAddress(govtypes.ModuleName).String()) + app.ConsensusParamsKeeper = consensusparamkeeper.NewKeeper(appCodec, runtime.NewEnvironment(runtime.NewKVStoreService(keys[consensustypes.StoreKey]), logger.With(log.ModuleKey, "x/consensus")), authtypes.NewModuleAddress(govtypes.ModuleName).String()) bApp.SetParamStore(app.ConsensusParamsKeeper.ParamsStore) // add keepers @@ -516,7 +520,7 @@ func NewSimApp( group.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, - consensusparamtypes.ModuleName, + consensustypes.ModuleName, circuittypes.ModuleName, pooltypes.ModuleName, epochstypes.ModuleName, diff --git a/simapp/v2/app_di.go b/simapp/v2/app_di.go index 7cde4042f3c8..4e87bcc305df 100644 --- a/simapp/v2/app_di.go +++ b/simapp/v2/app_di.go @@ -12,7 +12,6 @@ import ( "cosmossdk.io/depinject" "cosmossdk.io/log" "cosmossdk.io/runtime/v2" - serverv2 "cosmossdk.io/server/v2" "cosmossdk.io/x/accounts" authkeeper "cosmossdk.io/x/auth/keeper" authzkeeper "cosmossdk.io/x/authz/keeper" @@ -92,7 +91,6 @@ func NewSimApp[T transaction.Tx]( logger log.Logger, viper *viper.Viper, ) *SimApp[T] { - viper.Set(serverv2.FlagHome, DefaultNodeHome) // TODO possibly set earlier when viper is created var ( app = &SimApp[T]{} appBuilder *runtime.AppBuilder[T] diff --git a/simapp/v2/app_test.go b/simapp/v2/app_test.go new file mode 100644 index 000000000000..a2ffdcee4ef5 --- /dev/null +++ b/simapp/v2/app_test.go @@ -0,0 +1,155 @@ +package simapp + +import ( + "context" + "crypto/sha256" + "encoding/json" + "testing" + "time" + + "github.com/cometbft/cometbft/types" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + app2 "cosmossdk.io/core/app" + "cosmossdk.io/core/comet" + context2 "cosmossdk.io/core/context" + "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/log" + sdkmath "cosmossdk.io/math" + serverv2 "cosmossdk.io/server/v2" + comettypes "cosmossdk.io/server/v2/cometbft/types" + "cosmossdk.io/store/v2/db" + authtypes "cosmossdk.io/x/auth/types" + banktypes "cosmossdk.io/x/bank/types" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/testutil/mock" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewTestApp(t *testing.T) (*SimApp[transaction.Tx], context.Context) { + t.Helper() + + logger := log.NewTestLogger(t) + + vp := viper.New() + vp.Set("store.app-db-backend", string(db.DBTypeGoLevelDB)) + vp.Set(serverv2.FlagHome, t.TempDir()) + + app := NewSimApp[transaction.Tx](logger, vp) + genesis := app.ModuleManager().DefaultGenesis() + + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + // create validator set with single validator + validator := types.NewValidator(pubKey, 1) + valSet := types.NewValidatorSet([]*types.Validator{validator}) + + // generate genesis account + senderPrivKey := secp256k1.GenPrivKey() + acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0) + balance := banktypes.Balance{ + Address: acc.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000))), + } + + genesis, err = simtestutil.GenesisStateWithValSet( + app.AppCodec(), + genesis, + valSet, + []authtypes.GenesisAccount{acc}, + balance, + ) + require.NoError(t, err) + + genesisBytes, err := json.Marshal(genesis) + require.NoError(t, err) + + st := app.GetStore().(comettypes.Store) + ci, err := st.LastCommitID() + require.NoError(t, err) + + bz := sha256.Sum256([]byte{}) + + ctx := context.Background() + + _, newState, err := app.InitGenesis( + ctx, + &app2.BlockRequest[transaction.Tx]{ + Time: time.Now(), + Hash: bz[:], + ChainId: "theChain", + AppHash: ci.Hash, + IsGenesis: true, + }, + genesisBytes, + nil, + ) + require.NoError(t, err) + + changes, err := newState.GetStateChanges() + require.NoError(t, err) + + _, err = st.Commit(&store.Changeset{Changes: changes}) + require.NoError(t, err) + + return app, ctx +} + +func MoveNextBlock(t *testing.T, app *SimApp[transaction.Tx], ctx context.Context) { + t.Helper() + + bz := sha256.Sum256([]byte{}) + + st := app.GetStore().(comettypes.Store) + ci, err := st.LastCommitID() + require.NoError(t, err) + + height, err := app.LoadLatestHeight() + require.NoError(t, err) + + // TODO: this is a hack to set the comet info in the context for distribution module dependency. + ctx = context.WithValue(ctx, context2.CometInfoKey, comet.Info{ + Evidence: nil, + ValidatorsHash: nil, + ProposerAddress: nil, + LastCommit: comet.CommitInfo{}, + }) + + _, newState, err := app.DeliverBlock( + ctx, + &app2.BlockRequest[transaction.Tx]{ + Height: height + 1, + Time: time.Now(), + Hash: bz[:], + AppHash: ci.Hash, + }) + require.NoError(t, err) + + changes, err := newState.GetStateChanges() + require.NoError(t, err) + + _, err = st.Commit(&store.Changeset{Changes: changes}) + require.NoError(t, err) +} + +func TestSimAppExportAndBlockedAddrs_WithOneBlockProduced(t *testing.T) { + app, ctx := NewTestApp(t) + + MoveNextBlock(t, app, ctx) + + _, err := app.ExportAppStateAndValidators(nil) + require.NoError(t, err) +} + +func TestSimAppExportAndBlockedAddrs_NoBlocksProduced(t *testing.T) { + app, _ := NewTestApp(t) + + _, err := app.ExportAppStateAndValidators(nil) + require.NoError(t, err) +} diff --git a/simapp/v2/export.go b/simapp/v2/export.go index 41b8d94e9e7a..5a1757b16535 100644 --- a/simapp/v2/export.go +++ b/simapp/v2/export.go @@ -1,10 +1,29 @@ package simapp import ( - servertypes "github.com/cosmos/cosmos-sdk/server/types" + "context" + + v2 "github.com/cosmos/cosmos-sdk/x/genutil/v2" ) -// ExportAppStateAndValidators exports the state of the application for a genesis file. -func (app *SimApp[T]) ExportAppStateAndValidators(forZeroHeight bool, jailAllowedAddrs, modulesToExport []string) (servertypes.ExportedApp, error) { - panic("not implemented") +// ExportAppStateAndValidators exports the state of the application for a genesis +// file. +func (app *SimApp[T]) ExportAppStateAndValidators(jailAllowedAddrs []string) (v2.ExportedApp, error) { + // as if they could withdraw from the start of the next block + ctx := context.Background() + + latestHeight, err := app.LoadLatestHeight() + if err != nil { + return v2.ExportedApp{}, err + } + + genesis, err := app.ExportGenesis(ctx, latestHeight) + if err != nil { + return v2.ExportedApp{}, err + } + + return v2.ExportedApp{ + AppState: genesis, + Height: int64(latestHeight), + }, nil } diff --git a/simapp/v2/go.mod b/simapp/v2/go.mod index 3fc21e00a28c..4b9794cc56ce 100644 --- a/simapp/v2/go.mod +++ b/simapp/v2/go.mod @@ -13,7 +13,11 @@ require ( cosmossdk.io/runtime/v2 v2.0.0-20240815194237-858ec2fcb897 // main cosmossdk.io/server/v2 v2.0.0-20240815194237-858ec2fcb897 // main cosmossdk.io/server/v2/cometbft v0.0.0-00010101000000-000000000000 +<<<<<<< HEAD cosmossdk.io/store/v2 v2.0.0-20240815194237-858ec2fcb897 // indirect; main +======= + cosmossdk.io/store/v2 v2.0.0 +>>>>>>> aeeaca64d (feat: export genesis in simapp v2 (#21199)) cosmossdk.io/tools/confix v0.0.0-00010101000000-000000000000 cosmossdk.io/x/accounts v0.0.0-20240226161501-23359a0b6d91 cosmossdk.io/x/auth v0.0.0-00010101000000-000000000000 @@ -33,7 +37,7 @@ require ( cosmossdk.io/x/staking v0.0.0-00010101000000-000000000000 cosmossdk.io/x/upgrade v0.0.0-20230613133644-0a778132a60f github.com/cometbft/cometbft v1.0.0-rc1 - github.com/cosmos/cosmos-db v1.0.2 + github.com/cosmos/cosmos-db v1.0.2 // indirect // this version is not used as it is always replaced by the latest Cosmos SDK version github.com/cosmos/cosmos-sdk v0.53.0 github.com/spf13/cobra v1.8.1 @@ -52,6 +56,7 @@ require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.8 // indirect cloud.google.com/go/storage v1.42.0 // indirect +<<<<<<< HEAD cosmossdk.io/collections v0.4.1-0.20240802064046-23fac2f1b8ab // indirect; main cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect @@ -59,6 +64,16 @@ require ( cosmossdk.io/server/v2/appmanager v0.0.0-20240815194237-858ec2fcb897 // indirect; main cosmossdk.io/server/v2/stf v0.0.0-20240815194237-858ec2fcb897 // indirect; main cosmossdk.io/store v1.1.1-0.20240815194237-858ec2fcb897 // indirect; main +======= + cosmossdk.io/collections v0.4.0 // indirect + cosmossdk.io/core/testing v0.0.0-00010101000000-000000000000 // indirect + cosmossdk.io/errors v1.0.1 // indirect + cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect + cosmossdk.io/schema v0.1.1 // indirect + cosmossdk.io/server/v2/appmanager v0.0.0-20240802110823-cffeedff643d // indirect + cosmossdk.io/server/v2/stf v0.0.0-00010101000000-000000000000 // indirect + cosmossdk.io/store v1.1.1-0.20240418092142-896cdf1971bc // indirect +>>>>>>> aeeaca64d (feat: export genesis in simapp v2 (#21199)) cosmossdk.io/x/accounts/defaults/lockup v0.0.0-20240417181816-5e7aae0db1f5 // indirect cosmossdk.io/x/accounts/defaults/multisig v0.0.0-00010101000000-000000000000 // indirect cosmossdk.io/x/epochs v0.0.0-20240522060652-a1ae4c3e0337 // indirect diff --git a/simapp/v2/simdv2/cmd/commands.go b/simapp/v2/simdv2/cmd/commands.go index ec9998f5c8f2..b4d13aa7a78c 100644 --- a/simapp/v2/simdv2/cmd/commands.go +++ b/simapp/v2/simdv2/cmd/commands.go @@ -3,9 +3,7 @@ package cmd import ( "errors" "fmt" - "io" - dbm "github.com/cosmos/cosmos-db" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -26,17 +24,21 @@ import ( "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/client/rpc" "github.com/cosmos/cosmos-sdk/server" - servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + genutilv2 "github.com/cosmos/cosmos-sdk/x/genutil/v2" + v2 "github.com/cosmos/cosmos-sdk/x/genutil/v2/cli" ) func newApp[T transaction.Tx]( logger log.Logger, viper *viper.Viper, ) serverv2.AppI[T] { - return serverv2.AppI[T](simapp.NewSimApp[T](logger, viper)) + viper.Set(serverv2.FlagHome, simapp.DefaultNodeHome) + + return serverv2.AppI[T]( + simapp.NewSimApp[T](logger, viper)) } func initRootCmd[T transaction.Tx]( @@ -84,25 +86,11 @@ func initRootCmd[T transaction.Tx]( // genesisCommand builds genesis-related `simd genesis` command. Users may provide application specific commands as a parameter func genesisCommand[T transaction.Tx]( moduleManager *runtimev2.MM[T], - appExport func(logger log.Logger, - height int64, - forZeroHeight bool, - jailAllowedAddrs []string, - viper *viper.Viper, - modulesToExport []string, - ) (servertypes.ExportedApp, error), + appExport genutilv2.AppExporter, cmds ...*cobra.Command, ) *cobra.Command { - compatAppExporter := func(logger log.Logger, db dbm.DB, traceWriter io.Writer, height int64, forZeroHeight bool, jailAllowedAddrs []string, appOpts servertypes.AppOptions, modulesToExport []string) (servertypes.ExportedApp, error) { - viperAppOpts, ok := appOpts.(*viper.Viper) - if !ok { - return servertypes.ExportedApp{}, errors.New("appOpts is not viper.Viper") - } - - return appExport(logger, height, forZeroHeight, jailAllowedAddrs, viperAppOpts, modulesToExport) - } + cmd := v2.Commands(moduleManager.Modules()[genutiltypes.ModuleName].(genutil.AppModule), moduleManager, appExport) - cmd := genutilcli.Commands(moduleManager.Modules()[genutiltypes.ModuleName].(genutil.AppModule), moduleManager, compatAppExporter) for _, subCmd := range cmds { cmd.AddCommand(subCmd) } @@ -159,26 +147,25 @@ func txCommand() *cobra.Command { func appExport[T transaction.Tx]( logger log.Logger, height int64, - forZeroHeight bool, jailAllowedAddrs []string, viper *viper.Viper, - modulesToExport []string, -) (servertypes.ExportedApp, error) { +) (genutilv2.ExportedApp, error) { // overwrite the FlagInvCheckPeriod viper.Set(server.FlagInvCheckPeriod, 1) + viper.Set(serverv2.FlagHome, simapp.DefaultNodeHome) var simApp *simapp.SimApp[T] if height != -1 { simApp = simapp.NewSimApp[T](logger, viper) if err := simApp.LoadHeight(uint64(height)); err != nil { - return servertypes.ExportedApp{}, err + return genutilv2.ExportedApp{}, err } } else { simApp = simapp.NewSimApp[T](logger, viper) } - return simApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) + return simApp.ExportAppStateAndValidators(jailAllowedAddrs) } var _ transaction.Codec[transaction.Tx] = &genericTxDecoder[transaction.Tx]{} diff --git a/x/genutil/v2/cli/commands.go b/x/genutil/v2/cli/commands.go new file mode 100644 index 000000000000..93052d1e4907 --- /dev/null +++ b/x/genutil/v2/cli/commands.go @@ -0,0 +1,47 @@ +package cli + +import ( + "encoding/json" + + "github.com/spf13/cobra" + + banktypes "cosmossdk.io/x/bank/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/x/genutil" + "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + v2 "github.com/cosmos/cosmos-sdk/x/genutil/v2" +) + +type genesisMM interface { + DefaultGenesis() map[string]json.RawMessage + ValidateGenesis(genesisData map[string]json.RawMessage) error +} + +// Commands adds core sdk's sub-commands into genesis command. +func Commands(genutilModule genutil.AppModule, genMM genesisMM, appExport v2.AppExporter) *cobra.Command { + return CommandsWithCustomMigrationMap(genutilModule, genMM, appExport, genutiltypes.MigrationMap{}) +} + +// CommandsWithCustomMigrationMap adds core sdk's sub-commands into genesis command with custom migration map. +// This custom migration map can be used by the application to add its own migration map. +func CommandsWithCustomMigrationMap(genutilModule genutil.AppModule, genMM genesisMM, appExport v2.AppExporter, migrationMap genutiltypes.MigrationMap) *cobra.Command { + cmd := &cobra.Command{ + Use: "genesis", + Short: "Application's genesis-related subcommands", + DisableFlagParsing: false, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand( + cli.GenTxCmd(genMM, banktypes.GenesisBalancesIterator{}), + cli.MigrateGenesisCmd(migrationMap), + cli.CollectGenTxsCmd(genutilModule.GenTxValidator()), + cli.ValidateGenesisCmd(genMM), + cli.AddGenesisAccountCmd(), + ExportCmd(appExport), + ) + + return cmd +} diff --git a/x/genutil/v2/cli/export.go b/x/genutil/v2/cli/export.go new file mode 100644 index 000000000000..c19a02f870e3 --- /dev/null +++ b/x/genutil/v2/cli/export.go @@ -0,0 +1,106 @@ +package cli + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/version" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + v2 "github.com/cosmos/cosmos-sdk/x/genutil/v2" +) + +const ( + flagHeight = "height" + flagJailAllowedAddrs = "jail-allowed-addrs" +) + +// ExportCmd dumps app state to JSON. +func ExportCmd(appExporter v2.AppExporter) *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "Export state to JSON", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + config := client.GetConfigFromCmd(cmd) + viper := client.GetViperFromCmd(cmd) + logger := client.GetLoggerFromCmd(cmd) + + if _, err := os.Stat(config.GenesisFile()); os.IsNotExist(err) { + return err + } + + if appExporter == nil { + if _, err := fmt.Fprintln(cmd.ErrOrStderr(), "WARNING: App exporter not defined. Returning genesis file."); err != nil { + return err + } + + // Open file in read-only mode so we can copy it to stdout. + // It is possible that the genesis file is large, + // so we don't need to read it all into memory + // before we stream it out. + f, err := os.OpenFile(config.GenesisFile(), os.O_RDONLY, 0) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(cmd.OutOrStdout(), f); err != nil { + return err + } + + return nil + } + + height, _ := cmd.Flags().GetInt64(flagHeight) + jailAllowedAddrs, _ := cmd.Flags().GetStringSlice(flagJailAllowedAddrs) + outputDocument, _ := cmd.Flags().GetString(flags.FlagOutputDocument) + + exported, err := appExporter(logger, height, jailAllowedAddrs, viper) + if err != nil { + return fmt.Errorf("error exporting state: %w", err) + } + + appGenesis, err := genutiltypes.AppGenesisFromFile(config.GenesisFile()) + if err != nil { + return err + } + + // set current binary version + appGenesis.AppName = version.AppName + appGenesis.AppVersion = version.Version + + appGenesis.AppState = exported.AppState + appGenesis.InitialHeight = exported.Height + + out, err := json.Marshal(appGenesis) + if err != nil { + return err + } + + if outputDocument == "" { + // Copy the entire genesis file to stdout. + _, err := io.Copy(cmd.OutOrStdout(), bytes.NewReader(out)) + return err + } + + if err = appGenesis.SaveAs(outputDocument); err != nil { + return err + } + + return nil + }, + } + + cmd.Flags().Int64(flagHeight, -1, "Export state from a particular height (-1 means latest height)") + cmd.Flags().StringSlice(flagJailAllowedAddrs, []string{}, "Comma-separated list of operator addresses of jailed validators to unjail") + cmd.Flags().String(flags.FlagOutputDocument, "", "Exported state is written to the given file instead of STDOUT") + + return cmd +} diff --git a/x/genutil/v2/types.go b/x/genutil/v2/types.go new file mode 100644 index 000000000000..1b94c8bbc9be --- /dev/null +++ b/x/genutil/v2/types.go @@ -0,0 +1,27 @@ +package v2 + +import ( + "encoding/json" + + "github.com/spf13/viper" + + "cosmossdk.io/log" +) + +// AppExporter is a function that dumps all app state to +// JSON-serializable structure and returns the current validator set. +type AppExporter func( + logger log.Logger, + height int64, + jailAllowedAddrs []string, + viper *viper.Viper, +) (ExportedApp, error) + +// ExportedApp represents an exported app state, along with +// validators, consensus params and latest app height. +type ExportedApp struct { + // AppState is the application state as JSON. + AppState json.RawMessage + // Height is the app's latest block height. + Height int64 +} diff --git a/x/staking/genesis.go b/x/staking/genesis.go index edb7548fc7fd..fcd758952e0d 100644 --- a/x/staking/genesis.go +++ b/x/staking/genesis.go @@ -1,6 +1,7 @@ package staking import ( + "context" "fmt" cmttypes "github.com/cometbft/cometbft/types" @@ -14,7 +15,7 @@ import ( ) // WriteValidators returns a slice of bonded genesis validators. -func WriteValidators(ctx sdk.Context, keeper *keeper.Keeper) (vals []cmttypes.GenesisValidator, returnErr error) { +func WriteValidators(ctx context.Context, keeper *keeper.Keeper) (vals []cmttypes.GenesisValidator, returnErr error) { err := keeper.LastValidatorPower.Walk(ctx, nil, func(key []byte, _ gogotypes.Int64Value) (bool, error) { validator, err := keeper.GetValidator(ctx, key) if err != nil {