由于新型肺炎,不得不宅在家里面,闲着也是闲着,不如再认真地过一遍Tendermint源码吧。之前看的比较粗略,理解的也不够深。正好毕设使用了Tendermint平台作为底层支持,能够深入了解下也挺好的。

系统的启动

我们可以知道,tendermint最基本的启动流程通常是

1
2
tendermint init
tendermint node

在根目录,通过linux命令查找main函数可以得以下结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# grep -r "func main()" .
./abci/cmd/abci-cli/main.go:func main() {
./abci/tests/benchmarks/parallel/parallel.go:func main() {
./abci/tests/benchmarks/simple/simple.go:func main() {
./abci/tests/test_app/main.go:func main() {
./abci/types/protoreplace/protoreplace.go:func main() {
./cmd/contract_tests/main.go:func main() {
./cmd/priv_val_server/main.go:func main() {
./cmd/tendermint/main.go:func main() {
./docs/guides/go-built-in.md:func main() {
./docs/guides/go-built-in.md:func main() {
./docs/guides/go.md:func main() {
./docs/guides/go.md:func main() {
./libs/autofile/cmd/logjack.go:func main() {
./rpc/lib/test/main.go:func main() {
./scripts/json2wal/main.go:func main() {
./scripts/privValUpgrade.go:func main() {
./scripts/wal2json/main.go:func main() {
./test/app/grpc_client.go:func main() {
./tools/tm-signer-harness/main.go:func main() {

我们容易得知系统的启动入口函数在cmd/tendermint/main.go

main函数比较简单,首先指定了根指令rootCmd,然后给根指令添加了一系列的二级指令,包括

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cmd.GenValidatorCmd, //对于验证者生成所需要的密钥对,指令为tendermint gen_validator
cmd.InitFilesCmd, // 生成一个全新的tendermint实例所需要的文件,指令为tendermint init
cmd.ProbeUpnpCmd, 
cmd.LiteCmd, 
cmd.ReplayCmd,
cmd.ReplayConsoleCmd,
cmd.ResetAllCmd,
cmd.ResetPrivValidatorCmd,
cmd.ShowValidatorCmd,
cmd.TestnetFilesCmd,
cmd.ShowNodeIDCmd,
cmd.GenNodeKeyCmd,
cmd.VersionCmd,
debug.DebugCmd,

还有一个

1
2
nodeFunc := nm.DefaultNewNode
rootCmd.AddCommand(cmd.NewRunNodeCmd(nodeFunc)) // 启动一个节点指令为tendermint node

最后通过tendermint自己封装的cli库解析环境指令,然后处理命令行,执行对应的执行。

根指令

系统main入口很简单,在执行我们的启动指令前,tendermint处理了什么呢?

rootCmd定义中我们可以看到,定义了一个PersistentPreRunE函数,具体内容为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func(cmd *cobra.Command, args []string) (err error) {
		if cmd.Name() == VersionCmd.Name() {
			return nil
		}
		config, err = ParseConfig()
		if err != nil {
			return err
		}
		if config.LogFormat == cfg.LogFormatJSON {
			logger = log.NewTMJSONLogger(log.NewSyncWriter(os.Stdout))
		}
		logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
		if err != nil {
			return err
		}
		if viper.GetBool(cli.TraceFlag) {
			logger = log.NewTracingLogger(logger)
		}
		logger = logger.With("module", "main")
		return nil
	}

可以得知,如果调用的指令是版本查询,则无需处理配置文件,直接进行后续的版本查询操作即可,预执行指令也可以结束了。

如果不是版本查询,需要首先解析相关配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func ParseConfig() (*cfg.Config, error) {
	conf := cfg.DefaultConfig()
	err := viper.Unmarshal(conf)
	if err != nil {
		return nil, err
	}
	conf.SetRoot(conf.RootDir)
	cfg.EnsureRoot(conf.RootDir)
	if err = conf.ValidateBasic(); err != nil {
		return nil, fmt.Errorf("error in config file: %v", err)
	}
	return conf, err
}

本部分比较简单,只是从系统中读取了默认的配置,然后将配置写入到viper中方便后续的全局调用。

处理完配置后,接下来rootCmd进行日志的配置,我们可以得知日志处理文件在libs中,我们后续进行深入的探索,目前只知道是对不同的日志级别进行配置。

环境解析

我们现在要看最后这一句话,完成了系统启动前的最后一步配置

1
cmd := cli.PrepareBaseCmd(rootCmd, "TM", os.ExpandEnv(filepath.Join("$HOME", cfg.DefaultTendermintDir)))

其中默认的Tendermint路径为.tendermint,然后系统具体做了什么呢?

1
2
3
4
5
6
7
func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defaultHome string) Executor {
	cobra.OnInitialize(func() { initEnv(envPrefix) })
	cmd.PersistentFlags().StringP(HomeFlag, "", defaultHome, "directory for config and data")
	cmd.PersistentFlags().Bool(TraceFlag, false, "print out full stack trace on errors")
	cmd.PersistentPreRunE = concatCobraCmdFuncs(bindFlagsLoadViper, cmd.PersistentPreRunE)
	return Executor{cmd, os.Exit}
}

似乎有一些乱,可以一句一句的分析。

  1. 初始化系统环境变量,把系统中的关于Tendermint的环境变量如TMROOT换成TM_ROOT,同时配置到viper中。
  2. 获取系统中配置的home的值,指的是存储系统中config和data的路径
  3. 获取系统中配置的trace的值,指的是对于错误是否输出全部信息
  4. 这段分两个逻辑,一个是组合所有的预处理程序,一个是解析配置文件。其中解析配置文件是读取config文件写入到viper中,组合是直接遍历程序写入。

通过完成以上最基本的配置和日志的处理,节点启动前的准备就结束了。