github.com/cosmos/cosmos-sdk@v0.50.10/server/util.go (about) 1 package server 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net" 9 "os" 10 "os/signal" 11 "path" 12 "path/filepath" 13 "strings" 14 "syscall" 15 "time" 16 17 cmtcmd "github.com/cometbft/cometbft/cmd/cometbft/commands" 18 cmtcfg "github.com/cometbft/cometbft/config" 19 dbm "github.com/cosmos/cosmos-db" 20 "github.com/rs/zerolog" 21 "github.com/spf13/cast" 22 "github.com/spf13/cobra" 23 "github.com/spf13/pflag" 24 "github.com/spf13/viper" 25 "golang.org/x/sync/errgroup" 26 27 "cosmossdk.io/log" 28 "cosmossdk.io/store" 29 "cosmossdk.io/store/snapshots" 30 snapshottypes "cosmossdk.io/store/snapshots/types" 31 storetypes "cosmossdk.io/store/types" 32 33 "github.com/cosmos/cosmos-sdk/baseapp" 34 "github.com/cosmos/cosmos-sdk/client/flags" 35 "github.com/cosmos/cosmos-sdk/server/config" 36 "github.com/cosmos/cosmos-sdk/server/types" 37 sdk "github.com/cosmos/cosmos-sdk/types" 38 "github.com/cosmos/cosmos-sdk/types/mempool" 39 "github.com/cosmos/cosmos-sdk/version" 40 genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" 41 ) 42 43 // ServerContextKey defines the context key used to retrieve a server.Context from 44 // a command's Context. 45 const ServerContextKey = sdk.ContextKey("server.context") 46 47 // server context 48 type Context struct { 49 Viper *viper.Viper 50 Config *cmtcfg.Config 51 Logger log.Logger 52 } 53 54 func NewDefaultContext() *Context { 55 return NewContext( 56 viper.New(), 57 cmtcfg.DefaultConfig(), 58 log.NewLogger(os.Stdout), 59 ) 60 } 61 62 func NewContext(v *viper.Viper, config *cmtcfg.Config, logger log.Logger) *Context { 63 return &Context{v, config, logger} 64 } 65 66 func bindFlags(basename string, cmd *cobra.Command, v *viper.Viper) (err error) { 67 defer func() { 68 if r := recover(); r != nil { 69 err = fmt.Errorf("bindFlags failed: %v", r) 70 } 71 }() 72 73 cmd.Flags().VisitAll(func(f *pflag.Flag) { 74 // Environment variables can't have dashes in them, so bind them to their equivalent 75 // keys with underscores, e.g. --favorite-color to STING_FAVORITE_COLOR 76 err = v.BindEnv(f.Name, fmt.Sprintf("%s_%s", basename, strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_")))) 77 if err != nil { 78 panic(err) 79 } 80 81 err = v.BindPFlag(f.Name, f) 82 if err != nil { 83 panic(err) 84 } 85 86 // Apply the viper config value to the flag when the flag is not set and 87 // viper has a value. 88 if !f.Changed && v.IsSet(f.Name) { 89 val := v.Get(f.Name) 90 err = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) 91 if err != nil { 92 panic(err) 93 } 94 } 95 }) 96 97 return err 98 } 99 100 // InterceptConfigsPreRunHandler is identical to InterceptConfigsAndCreateContext 101 // except it also sets the server context on the command and the server logger. 102 func InterceptConfigsPreRunHandler(cmd *cobra.Command, customAppConfigTemplate string, customAppConfig interface{}, cmtConfig *cmtcfg.Config) error { 103 serverCtx, err := InterceptConfigsAndCreateContext(cmd, customAppConfigTemplate, customAppConfig, cmtConfig) 104 if err != nil { 105 return err 106 } 107 108 // overwrite default server logger 109 logger, err := CreateSDKLogger(serverCtx, cmd.OutOrStdout()) 110 if err != nil { 111 return err 112 } 113 serverCtx.Logger = logger.With(log.ModuleKey, "server") 114 115 // set server context 116 return SetCmdServerContext(cmd, serverCtx) 117 } 118 119 // InterceptConfigsAndCreateContext performs a pre-run function for the root daemon 120 // application command. It will create a Viper literal and a default server 121 // Context. The server CometBFT configuration will either be read and parsed 122 // or created and saved to disk, where the server Context is updated to reflect 123 // the CometBFT configuration. It takes custom app config template and config 124 // settings to create a custom CometBFT configuration. If the custom template 125 // is empty, it uses default-template provided by the server. The Viper literal 126 // is used to read and parse the application configuration. Command handlers can 127 // fetch the server Context to get the CometBFT configuration or to get access 128 // to Viper. 129 func InterceptConfigsAndCreateContext(cmd *cobra.Command, customAppConfigTemplate string, customAppConfig interface{}, cmtConfig *cmtcfg.Config) (*Context, error) { 130 serverCtx := NewDefaultContext() 131 132 // Get the executable name and configure the viper instance so that environmental 133 // variables are checked based off that name. The underscore character is used 134 // as a separator. 135 executableName, err := os.Executable() 136 if err != nil { 137 return nil, err 138 } 139 140 basename := path.Base(executableName) 141 142 // configure the viper instance 143 if err := serverCtx.Viper.BindPFlags(cmd.Flags()); err != nil { 144 return nil, err 145 } 146 if err := serverCtx.Viper.BindPFlags(cmd.PersistentFlags()); err != nil { 147 return nil, err 148 } 149 150 serverCtx.Viper.SetEnvPrefix(basename) 151 serverCtx.Viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 152 serverCtx.Viper.AutomaticEnv() 153 154 // intercept configuration files, using both Viper instances separately 155 config, err := interceptConfigs(serverCtx.Viper, customAppConfigTemplate, customAppConfig, cmtConfig) 156 if err != nil { 157 return nil, err 158 } 159 160 // return value is a CometBFT configuration object 161 serverCtx.Config = config 162 if err = bindFlags(basename, cmd, serverCtx.Viper); err != nil { 163 return nil, err 164 } 165 166 return serverCtx, nil 167 } 168 169 // CreateSDKLogger creates a the default SDK logger. 170 // It reads the log level and format from the server context. 171 func CreateSDKLogger(ctx *Context, out io.Writer) (log.Logger, error) { 172 var opts []log.Option 173 if ctx.Viper.GetString(flags.FlagLogFormat) == flags.OutputFormatJSON { 174 opts = append(opts, log.OutputJSONOption()) 175 } 176 opts = append(opts, 177 log.ColorOption(!ctx.Viper.GetBool(flags.FlagLogNoColor)), 178 // We use CometBFT flag (cmtcli.TraceFlag) for trace logging. 179 log.TraceOption(ctx.Viper.GetBool(FlagTrace))) 180 181 // check and set filter level or keys for the logger if any 182 logLvlStr := ctx.Viper.GetString(flags.FlagLogLevel) 183 if logLvlStr == "" { 184 return log.NewLogger(out, opts...), nil 185 } 186 187 logLvl, err := zerolog.ParseLevel(logLvlStr) 188 switch { 189 case err != nil: 190 // If the log level is not a valid zerolog level, then we try to parse it as a key filter. 191 filterFunc, err := log.ParseLogLevel(logLvlStr) 192 if err != nil { 193 return nil, err 194 } 195 196 opts = append(opts, log.FilterOption(filterFunc)) 197 default: 198 opts = append(opts, log.LevelOption(logLvl)) 199 } 200 201 return log.NewLogger(out, opts...), nil 202 } 203 204 // GetServerContextFromCmd returns a Context from a command or an empty Context 205 // if it has not been set. 206 func GetServerContextFromCmd(cmd *cobra.Command) *Context { 207 if v := cmd.Context().Value(ServerContextKey); v != nil { 208 serverCtxPtr := v.(*Context) 209 return serverCtxPtr 210 } 211 212 return NewDefaultContext() 213 } 214 215 // SetCmdServerContext sets a command's Context value to the provided argument. 216 // If the context has not been set, set the given context as the default. 217 func SetCmdServerContext(cmd *cobra.Command, serverCtx *Context) error { 218 v := cmd.Context().Value(ServerContextKey) 219 if v == nil { 220 v = serverCtx 221 } 222 223 serverCtxPtr := v.(*Context) 224 *serverCtxPtr = *serverCtx 225 226 return nil 227 } 228 229 // interceptConfigs parses and updates a CometBFT configuration file or 230 // creates a new one and saves it. It also parses and saves the application 231 // configuration file. The CometBFT configuration file is parsed given a root 232 // Viper object, whereas the application is parsed with the private package-aware 233 // viperCfg object. 234 func interceptConfigs(rootViper *viper.Viper, customAppTemplate string, customConfig interface{}, cmtConfig *cmtcfg.Config) (*cmtcfg.Config, error) { 235 rootDir := rootViper.GetString(flags.FlagHome) 236 configPath := filepath.Join(rootDir, "config") 237 cmtCfgFile := filepath.Join(configPath, "config.toml") 238 239 conf := cmtConfig 240 241 switch _, err := os.Stat(cmtCfgFile); { 242 case os.IsNotExist(err): 243 cmtcfg.EnsureRoot(rootDir) 244 245 if err = conf.ValidateBasic(); err != nil { 246 return nil, fmt.Errorf("error in config file: %w", err) 247 } 248 249 defaultCometCfg := cmtcfg.DefaultConfig() 250 // The SDK is opinionated about those comet values, so we set them here. 251 // We verify first that the user has not changed them for not overriding them. 252 if conf.Consensus.TimeoutCommit == defaultCometCfg.Consensus.TimeoutCommit { 253 conf.Consensus.TimeoutCommit = 5 * time.Second 254 } 255 if conf.RPC.PprofListenAddress == defaultCometCfg.RPC.PprofListenAddress { 256 conf.RPC.PprofListenAddress = "localhost:6060" 257 } 258 259 cmtcfg.WriteConfigFile(cmtCfgFile, conf) 260 261 case err != nil: 262 return nil, err 263 264 default: 265 rootViper.SetConfigType("toml") 266 rootViper.SetConfigName("config") 267 rootViper.AddConfigPath(configPath) 268 269 if err := rootViper.ReadInConfig(); err != nil { 270 return nil, fmt.Errorf("failed to read in %s: %w", cmtCfgFile, err) 271 } 272 } 273 274 // Read into the configuration whatever data the viper instance has for it. 275 // This may come from the configuration file above but also any of the other 276 // sources viper uses. 277 if err := rootViper.Unmarshal(conf); err != nil { 278 return nil, err 279 } 280 281 conf.SetRoot(rootDir) 282 283 appCfgFilePath := filepath.Join(configPath, "app.toml") 284 if _, err := os.Stat(appCfgFilePath); os.IsNotExist(err) { 285 if customAppTemplate != "" { 286 config.SetConfigTemplate(customAppTemplate) 287 288 if err = rootViper.Unmarshal(&customConfig); err != nil { 289 return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err) 290 } 291 292 config.WriteConfigFile(appCfgFilePath, customConfig) 293 } else { 294 appConf, err := config.ParseConfig(rootViper) 295 if err != nil { 296 return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err) 297 } 298 299 config.WriteConfigFile(appCfgFilePath, appConf) 300 } 301 } 302 303 rootViper.SetConfigType("toml") 304 rootViper.SetConfigName("app") 305 rootViper.AddConfigPath(configPath) 306 307 if err := rootViper.MergeInConfig(); err != nil { 308 return nil, fmt.Errorf("failed to merge configuration: %w", err) 309 } 310 311 return conf, nil 312 } 313 314 // add server commands 315 func AddCommands(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, appExport types.AppExporter, addStartFlags types.ModuleInitFlags) { 316 cometCmd := &cobra.Command{ 317 Use: "comet", 318 Aliases: []string{"cometbft", "tendermint"}, 319 Short: "CometBFT subcommands", 320 } 321 322 cometCmd.AddCommand( 323 ShowNodeIDCmd(), 324 ShowValidatorCmd(), 325 ShowAddressCmd(), 326 VersionCmd(), 327 cmtcmd.ResetAllCmd, 328 cmtcmd.ResetStateCmd, 329 BootstrapStateCmd(appCreator), 330 ) 331 332 startCmd := StartCmd(appCreator, defaultNodeHome) 333 addStartFlags(startCmd) 334 335 rootCmd.AddCommand( 336 startCmd, 337 cometCmd, 338 ExportCmd(appExport, defaultNodeHome), 339 version.NewVersionCommand(), 340 NewRollbackCmd(appCreator, defaultNodeHome), 341 ModuleHashByHeightQuery(appCreator), 342 ) 343 } 344 345 // AddCommandsWithStartCmdOptions adds server commands with the provided StartCmdOptions. 346 func AddCommandsWithStartCmdOptions(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, appExport types.AppExporter, opts StartCmdOptions) { 347 cometCmd := &cobra.Command{ 348 Use: "comet", 349 Aliases: []string{"cometbft", "tendermint"}, 350 Short: "CometBFT subcommands", 351 } 352 353 cometCmd.AddCommand( 354 ShowNodeIDCmd(), 355 ShowValidatorCmd(), 356 ShowAddressCmd(), 357 VersionCmd(), 358 cmtcmd.ResetAllCmd, 359 cmtcmd.ResetStateCmd, 360 BootstrapStateCmd(appCreator), 361 ) 362 363 startCmd := StartCmdWithOptions(appCreator, defaultNodeHome, opts) 364 365 rootCmd.AddCommand( 366 startCmd, 367 cometCmd, 368 ExportCmd(appExport, defaultNodeHome), 369 version.NewVersionCommand(), 370 NewRollbackCmd(appCreator, defaultNodeHome), 371 ) 372 } 373 374 // AddTestnetCreatorCommand allows chains to create a testnet from the state existing in their node's data directory. 375 func AddTestnetCreatorCommand(rootCmd *cobra.Command, appCreator types.AppCreator, addStartFlags types.ModuleInitFlags) { 376 testnetCreateCmd := InPlaceTestnetCreator(appCreator) 377 addStartFlags(testnetCreateCmd) 378 rootCmd.AddCommand(testnetCreateCmd) 379 } 380 381 // https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go 382 // TODO there must be a better way to get external IP 383 func ExternalIP() (string, error) { 384 ifaces, err := net.Interfaces() 385 if err != nil { 386 return "", err 387 } 388 389 for _, iface := range ifaces { 390 if skipInterface(iface) { 391 continue 392 } 393 addrs, err := iface.Addrs() 394 if err != nil { 395 return "", err 396 } 397 398 for _, addr := range addrs { 399 ip := addrToIP(addr) 400 if ip == nil || ip.IsLoopback() { 401 continue 402 } 403 ip = ip.To4() 404 if ip == nil { 405 continue // not an ipv4 address 406 } 407 return ip.String(), nil 408 } 409 } 410 return "", errors.New("are you connected to the network?") 411 } 412 413 // ListenForQuitSignals listens for SIGINT and SIGTERM. When a signal is received, 414 // the cleanup function is called, indicating the caller can gracefully exit or 415 // return. 416 // 417 // Note, the blocking behavior of this depends on the block argument. 418 // The caller must ensure the corresponding context derived from the cancelFn is used correctly. 419 func ListenForQuitSignals(g *errgroup.Group, block bool, cancelFn context.CancelFunc, logger log.Logger) { 420 sigCh := make(chan os.Signal, 1) 421 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 422 423 f := func() { 424 sig := <-sigCh 425 cancelFn() 426 427 logger.Info("caught signal", "signal", sig.String()) 428 } 429 430 if block { 431 g.Go(func() error { 432 f() 433 return nil 434 }) 435 } else { 436 go f() 437 } 438 } 439 440 // GetAppDBBackend gets the backend type to use for the application DBs. 441 func GetAppDBBackend(opts types.AppOptions) dbm.BackendType { 442 rv := cast.ToString(opts.Get("app-db-backend")) 443 if len(rv) == 0 { 444 rv = cast.ToString(opts.Get("db_backend")) 445 } 446 447 // Cosmos SDK has migrated to cosmos-db which does not support all the backends which tm-db supported 448 if rv == "cleveldb" || rv == "badgerdb" || rv == "boltdb" { 449 panic(fmt.Sprintf("invalid app-db-backend %q, use %q, %q, %q instead", rv, dbm.GoLevelDBBackend, dbm.PebbleDBBackend, dbm.RocksDBBackend)) 450 } 451 452 if len(rv) != 0 { 453 return dbm.BackendType(rv) 454 } 455 456 return dbm.GoLevelDBBackend 457 } 458 459 func skipInterface(iface net.Interface) bool { 460 if iface.Flags&net.FlagUp == 0 { 461 return true // interface down 462 } 463 464 if iface.Flags&net.FlagLoopback != 0 { 465 return true // loopback interface 466 } 467 468 return false 469 } 470 471 func addrToIP(addr net.Addr) net.IP { 472 var ip net.IP 473 474 switch v := addr.(type) { 475 case *net.IPNet: 476 ip = v.IP 477 case *net.IPAddr: 478 ip = v.IP 479 } 480 return ip 481 } 482 483 func openDB(rootDir string, backendType dbm.BackendType) (dbm.DB, error) { 484 dataDir := filepath.Join(rootDir, "data") 485 return dbm.NewDB("application", backendType, dataDir) 486 } 487 488 func openTraceWriter(traceWriterFile string) (w io.WriteCloser, err error) { 489 if traceWriterFile == "" { 490 return 491 } 492 return os.OpenFile( 493 traceWriterFile, 494 os.O_WRONLY|os.O_APPEND|os.O_CREATE, 495 0o666, 496 ) 497 } 498 499 // DefaultBaseappOptions returns the default baseapp options provided by the Cosmos SDK 500 func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) { 501 var cache storetypes.MultiStorePersistentCache 502 503 if cast.ToBool(appOpts.Get(FlagInterBlockCache)) { 504 cache = store.NewCommitKVStoreCacheManager() 505 } 506 507 pruningOpts, err := GetPruningOptionsFromFlags(appOpts) 508 if err != nil { 509 panic(err) 510 } 511 512 homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) 513 chainID := cast.ToString(appOpts.Get(flags.FlagChainID)) 514 if chainID == "" { 515 // fallback to genesis chain-id 516 reader, err := os.Open(filepath.Join(homeDir, "config", "genesis.json")) 517 if err != nil { 518 panic(err) 519 } 520 defer reader.Close() 521 522 chainID, err = genutiltypes.ParseChainIDFromGenesis(reader) 523 if err != nil { 524 panic(fmt.Errorf("failed to parse chain-id from genesis file: %w", err)) 525 } 526 } 527 528 snapshotStore, err := GetSnapshotStore(appOpts) 529 if err != nil { 530 panic(err) 531 } 532 533 snapshotOptions := snapshottypes.NewSnapshotOptions( 534 cast.ToUint64(appOpts.Get(FlagStateSyncSnapshotInterval)), 535 cast.ToUint32(appOpts.Get(FlagStateSyncSnapshotKeepRecent)), 536 ) 537 538 defaultMempool := baseapp.SetMempool(mempool.NoOpMempool{}) 539 if maxTxs := cast.ToInt(appOpts.Get(FlagMempoolMaxTxs)); maxTxs >= 0 { 540 defaultMempool = baseapp.SetMempool( 541 mempool.NewSenderNonceMempool( 542 mempool.SenderNonceMaxTxOpt(maxTxs), 543 ), 544 ) 545 } 546 547 return []func(*baseapp.BaseApp){ 548 baseapp.SetPruning(pruningOpts), 549 baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(FlagMinGasPrices))), 550 baseapp.SetHaltHeight(cast.ToUint64(appOpts.Get(FlagHaltHeight))), 551 baseapp.SetHaltTime(cast.ToUint64(appOpts.Get(FlagHaltTime))), 552 baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(FlagMinRetainBlocks))), 553 baseapp.SetInterBlockCache(cache), 554 baseapp.SetTrace(cast.ToBool(appOpts.Get(FlagTrace))), 555 baseapp.SetIndexEvents(cast.ToStringSlice(appOpts.Get(FlagIndexEvents))), 556 baseapp.SetSnapshot(snapshotStore, snapshotOptions), 557 baseapp.SetIAVLCacheSize(cast.ToInt(appOpts.Get(FlagIAVLCacheSize))), 558 baseapp.SetIAVLDisableFastNode(cast.ToBool(appOpts.Get(FlagDisableIAVLFastNode))), 559 defaultMempool, 560 baseapp.SetChainID(chainID), 561 baseapp.SetQueryGasLimit(cast.ToUint64(appOpts.Get(FlagQueryGasLimit))), 562 } 563 } 564 565 func GetSnapshotStore(appOpts types.AppOptions) (*snapshots.Store, error) { 566 homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) 567 snapshotDir := filepath.Join(homeDir, "data", "snapshots") 568 if err := os.MkdirAll(snapshotDir, 0o744); err != nil { 569 return nil, fmt.Errorf("failed to create snapshots directory: %w", err) 570 } 571 572 snapshotDB, err := dbm.NewDB("metadata", GetAppDBBackend(appOpts), snapshotDir) 573 if err != nil { 574 return nil, err 575 } 576 snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir) 577 if err != nil { 578 return nil, err 579 } 580 581 return snapshotStore, nil 582 }