我们知道,在启动一个节点前,需要执行tendermint init完成相关系统文件的生成,具体如何处理的我们查看下InitFilesCmd

查看源代码,发现本部分功能比较长

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
privValKeyFile := config.PrivValidatorKeyFile()
privValStateFile := config.PrivValidatorStateFile()
var pv *privval.FilePV
if tmos.FileExists(privValKeyFile) {
	pv = privval.LoadFilePV(privValKeyFile, privValStateFile)
	logger.Info("Found private validator", "keyFile", privValKeyFile,
		"stateFile", privValStateFile)
} else {
	pv = privval.GenFilePV(privValKeyFile, privValStateFile)
	pv.Save()
	logger.Info("Generated private validator", "keyFile", privValKeyFile,
		"stateFile", privValStateFile)
}

nodeKeyFile := config.NodeKeyFile()
if tmos.FileExists(nodeKeyFile) {
	logger.Info("Found node key", "path", nodeKeyFile)
} else {
	if _, err := p2p.LoadOrGenNodeKey(nodeKeyFile); err != nil {
		return err
	}
	logger.Info("Generated node key", "path", nodeKeyFile)
}

// genesis file
genFile := config.GenesisFile()
if tmos.FileExists(genFile) {
	logger.Info("Found genesis file", "path", genFile)
} else {
	genDoc := types.GenesisDoc{
		ChainID:         fmt.Sprintf("test-chain-%v", tmrand.Str(6)),
		GenesisTime:     tmtime.Now(),
		ConsensusParams: types.DefaultConsensusParams(),
	}
	key := pv.GetPubKey()
	genDoc.Validators = []types.GenesisValidator{{
		Address: key.Address(),
		PubKey:  key,
		Power:   10,
	}}

	if err := genDoc.SaveAs(genFile); err != nil {
		return err
	}
	logger.Info("Generated genesis file", "path", genFile)
}

return nil

我们一步一步进行分析。

初始化分析

配置文件

初始化首先默认获取验证节点的秘钥配置文件和状态文件。

查看默认的配置文件,可以发现文件的默认位置按照如下规则生成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
defaultConfigDir     = "config"
...
defaultPrivValKeyName   = "priv_validator_key.json"
defaultPrivValStateName = "priv_validator_state.json"
...
defaultPrivValKeyPath   = filepath.Join(defaultConfigDir, defaultPrivValKeyName)
defaultPrivValStatePath = filepath.Join(defaultDataDir, defaultPrivValStateName)
...
rootify(cfg.PrivValidatorKey, cfg.RootDir)
...
func rootify(path, root string) string {
	if filepath.IsAbs(path) {
		return path
	}
	return filepath.Join(root, path)
}

上文已经分析,绝对路径root在系统启动前已经配置了,是读取的HOME中的设置,如果我们没有设置,则路径就是.tendermint,所以获取的文件路径是.tendermint/config/priv_validator_key.json.tendermint/config/priv_validator_state.json。如果文件存在,则读取文件内容,否则生成。

FilePV

一个默认验证者的信息、状态是通过FilePV进行持久化存储的,具体的定义格式如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type FilePV struct {
	Key           FilePVKey
	LastSignState FilePVLastSignState
}
type FilePVKey struct {
	Address types.Address  `json:"address"`
	PubKey  crypto.PubKey  `json:"pub_key"`
	PrivKey crypto.PrivKey `json:"priv_key"`

	filePath string
}
type FilePVLastSignState struct {
	Height    int64            `json:"height"`
	Round     int              `json:"round"`
	Step      int8             `json:"step"`
	Signature []byte           `json:"signature,omitempty"`
	SignBytes tmbytes.HexBytes `json:"signbytes,omitempty"`

	filePath string
}

验证者信息包括验证者地址、公私钥,验证状态包括当前验证的高度、进行PBFT的轮次和所处的位置、当前进行的签名。

读取过程如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func loadFilePV(keyFilePath, stateFilePath string, loadState bool) *FilePV {
	keyJSONBytes, err := ioutil.ReadFile(keyFilePath)
	if err != nil {
		tmos.Exit(err.Error())
	}
	pvKey := FilePVKey{}
	err = cdc.UnmarshalJSON(keyJSONBytes, &pvKey)
	if err != nil {
		tmos.Exit(fmt.Sprintf("Error reading PrivValidator key from %v: %v\n", keyFilePath, err))
	}

	// overwrite pubkey and address for convenience
	pvKey.PubKey = pvKey.PrivKey.PubKey()
	pvKey.Address = pvKey.PubKey.Address()
	pvKey.filePath = keyFilePath

	pvState := FilePVLastSignState{}
	if loadState {
		stateJSONBytes, err := ioutil.ReadFile(stateFilePath)
		if err != nil {
			tmos.Exit(err.Error())
		}
		err = cdc.UnmarshalJSON(stateJSONBytes, &pvState)
		if err != nil {
			tmos.Exit(fmt.Sprintf("Error reading PrivValidator state from %v: %v\n", stateFilePath, err))
		}
	}

	pvState.filePath = stateFilePath

	return &FilePV{
		Key:           pvKey,
		LastSignState: pvState,
	}
}

本流程实际比较简单,直接读取文件,反序列化到结构体中即可。我们在读取FilePV时有一个假设,即节点的公私钥信息存在,则默认节点的状态信息也存在。而当启动时节点信息不存在的情况下,需要生成节点信息和相应的状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func GenFilePV(keyFilePath, stateFilePath string) *FilePV {
	privKey := ed25519.GenPrivKey()

	return &FilePV{
		Key: FilePVKey{
			Address:  privKey.PubKey().Address(),
			PubKey:   privKey.PubKey(),
			PrivKey:  privKey,
			filePath: keyFilePath,
		},
		LastSignState: FilePVLastSignState{
			Step:     stepNone,
			filePath: stateFilePath,
		},
	}
}

在此处我们发现生成时并不对直接生成文件,而是在上层显示调用Save函数保存的。

由于保存文件对于启动过程影响并不大,而且保存的代码比较长,我们暂时忽略。

到目前为止,验证者的信息和状态已经获取完毕。

NodeKey

读取完验证者的相关信息后,接下来是节点的信息

1
2
3
type NodeKey struct {
	PrivKey crypto.PrivKey `json:"priv_key"` // our priv key
}

其读取方式以及生成很简单,和验证者的处理方式一致,此处不再赘述。

但是,此处有一点问题,在上层调用时显示判断了一次文件是否存在,当文件不存在时,程序调用的方式为LoadOrGenNodeKey,即又判断了一次,私认为此处有一些重复代码。

此外,还有一个需要注意的,在这个地方,node的私钥信息是和privval的私钥信息是不一致的。在次数是为什么呢?

通常一个节点,可以对应一个验证者身份,节点用私钥可以标识自己的唯一身份,所以用验证者的私钥生成地址即可,为何要再单独生成呢?

我们带着问题往后面看吧。

Genesis

生成了验证者身份和节点身份后,初始化还需要创建创世区块。

创世区块的格式如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type GenesisDoc struct {
	GenesisTime     time.Time          `json:"genesis_time"`
	ChainID         string             `json:"chain_id"`
	ConsensusParams *ConsensusParams   `json:"consensus_params,omitempty"`
	Validators      []GenesisValidator `json:"validators,omitempty"`
	AppHash         tmbytes.HexBytes   `json:"app_hash"`
	AppState        json.RawMessage    `json:"app_state,omitempty"`
}
type ConsensusParams struct {
	Block     BlockParams     `json:"block"` //定义生成区块的参数,包括区块最大大小,区块最大Gas以及区块最小生成间隔
	Evidence  EvidenceParams  `json:"evidence"` // tendermint支持对错误交易的举证,此处指允许证据距离如今最长的时间
	Validator ValidatorParams `json:"validator"` //指的是验证者的地址类型,主要包括ed25519,sr25519,secp256k1三种类型
}
type GenesisValidator struct {
	Address Address       `json:"address"`
	PubKey  crypto.PubKey `json:"pub_key"`
	Power   int64         `json:"power"`
	Name    string        `json:"name"`
}

可以得知,生成的创世区块内容比较简单,包括整个链的配置和验证者,此外没有其他的信息了。然而,创世区块中的AppHashAppState指的是什么呢?

我们后续继续探索。

结束

到此为止,一个节点的初始化就算结束了,而且创世过程也比较简单,就是尝试读取已有的文件,没有则创建新的,生成完毕创立区块链结束。