cosmos-sdk/runtime/v2/manager.go

719 lines
22 KiB
Go

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 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.HasGenesis); hasABCIGenesis {
moduleI = module.(ModuleI)
}
channels[moduleName] = make(chan genesisResult)
go func(moduleI ModuleI, ch chan genesisResult) {
jm, err := moduleI.ExportGenesis(ctx)
if err != nil {
ch <- genesisResult{nil, err}
return
}
ch <- genesisResult{jm, nil}
}(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
}
}
// 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 appmodulev2.Message,
) (resp appmodulev2.Message, err error) {
res, err := md.Handler(ss, ctx, noopDecoder, messagePassingInterceptor(msg))
if err != nil {
return nil, err
}
return res.(appmodulev2.Message), nil
})
}
func noopDecoder(_ interface{}) error { return nil }
func messagePassingInterceptor(msg appmodulev2.Message) 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
}