一、最基础的默认配置
源码文件:tendermint/tendermint/config/config.go
// NOTE: Most of the structs & relevant comments + the
// default configuration options were used to manually
// generate the config.toml. Please reflect any changes
// made here in the defaultConfigTemplate constant in
// config/toml.go
// NOTE: tmlibs/cli must know to look in the config dir!
var (
DefaultTendermintDir = ".tendermint"
defaultConfigDir = "config"
defaultDataDir = "data"
defaultConfigFileName = "config.toml"
defaultGenesisJSONName = "genesis.json"
defaultPrivValName = "priv_validator.json"
defaultNodeKeyName = "node_key.json"
defaultAddrBookName = "addrbook.json"
defaultConfigFilePath = filepath.Join(defaultConfigDir, defaultConfigFileName)
defaultGenesisJSONPath = filepath.Join(defaultConfigDir, defaultGenesisJSONName)
defaultPrivValPath = filepath.Join(defaultConfigDir, defaultPrivValName)
defaultNodeKeyPath = filepath.Join(defaultConfigDir, defaultNodeKeyName)
defaultAddrBookPath = filepath.Join(defaultConfigDir, defaultAddrBookName)
)
这里定义了tendermint的默认配置存放的目录和文件名,在执行tendermint的init命令时会调用。
二、生成默认配置文件
命令:
tendermint init
源码文件:tendermint/cmd/tendermint/commands/init.go
触发的函数调用关系
initFiles -> initFilesWithConfig
结果生成两个配置文件:
private validator
genesis file
node_key.json
三、核心配置文件
config.toml由tendermint node命令生成
源码文件:
tendermint/cmd/tendermint/main.go
tendermint/cmd/tendermint/commands/root.go
tendermint/config/toml.go
调用关系:
main() -> RootCmd -> ParseConfig() -> EnsureRoot() -> writeDefaultConfigFile() -> DefaultConfig()
// Write default config file if missing.
if !cmn.FileExists(configFilePath) {
writeDefaultConfigFile(configFilePath)
}
tendermint每次运行时,都会检查config.toml,找不到就会自动生成一个。
四、编译文件
var _ types.Application = (*PersistentKVStoreApplication)(nil)
这句代码的作用强制转换一个空指针到一个unused的变量。
官方注释(看不懂)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
GOLANG注释
This post is about a little-known way to make compile-time assertions in Go. You probably shouldn’t use it, but it is interesting to know about.
As a warm-up, here’s a fairly well-known form of compile-time assertions in Go: Interface satisfaction checks.
In this code (playground), the var _ = line ensures that type W is a stringWriter, as checked for by io.WriteString.
五、RPC测试
1.RPC配置
源码文件:tendermint/config/config.go
type RPCConfig struct {
RootDir string
Unsafe bool
ListenAddress string
GRPCListenAddress string
MaxOpenConnections int
GRPCMaxOpenConnections int
}
Unsafe是针对ListenAddress和GRPCListenAddress都起作用的配置项。ListenAddress和GRPCListenAddress的关系一看就是完全对等的。既然这两种连接是对等的,那么MaxOpenConnections和GRPCMaxOpenConnections也应该是独立计数的。
从源码看出,ListenAddress在社区中称为RPC,支持两种协议,Http和Websocket,这两种协议可以共用一个端口,源于websockt与 HTTP 协议有着良好的兼容性,tendermint中使用一个特殊的路径来做区分,下面是部分源码。
mux := http.NewServeMux()
wm := rpcserver.NewWebsocketManager(rpccore.Routes, coreCodec, rpcserver.EventSubscriber(n.eventBus))
wm.SetLogger(rpcLogger.With("protocol", "websocket"))
mux.HandleFunc("/websocket", wm.WebsocketHandler)
2.RPC测试
tendermint在连接ABCI服务端的时候,扮演的是一个客户端;当JSON-RPC连接tendermint的时候扮演的是一个服务端的角色。
Tendermint 支持下面三种RPC协议:
- URI over HTTP
- JSONRPC over HTTP
- JSONRPC over websockets
同时,tendermint也提供了GRPC监听接口,下面是相关代码
func (n *Node) startRPC() ([]net.Listener, error) {
n.ConfigureRPC()
listenAddrs := splitAndTrimEmpty(n.config.RPC.ListenAddress, ",", " ")
coreCodec := amino.NewCodec()
ctypes.RegisterAmino(coreCodec)
if n.config.RPC.Unsafe {
rpccore.AddUnsafeRoutes()
}
// we may expose the rpc over both a unix and tcp socket
listeners := make([]net.Listener, len(listenAddrs))
for i, listenAddr := range listenAddrs {
mux := http.NewServeMux()
rpcLogger := n.Logger.With("module", "rpc-server")
wm := rpcserver.NewWebsocketManager(rpccore.Routes, coreCodec, rpcserver.EventSubscriber(n.eventBus))
wm.SetLogger(rpcLogger.With("protocol", "websocket"))
mux.HandleFunc("/websocket", wm.WebsocketHandler)
rpcserver.RegisterRPCFuncs(mux, rpccore.Routes, coreCodec, rpcLogger)
listener, err := rpcserver.StartHTTPServer(
listenAddr,
mux,
rpcLogger,
rpcserver.Config{MaxOpenConnections: n.config.RPC.MaxOpenConnections},
)
if err != nil {
return nil, err
}
listeners[i] = listener
}
// we expose a simplified api over grpc for convenience to app devs
grpcListenAddr := n.config.RPC.GRPCListenAddress
if grpcListenAddr != "" {
listener, err := grpccore.StartGRPCServer(
grpcListenAddr,
grpccore.Config{
MaxOpenConnections: n.config.RPC.GRPCMaxOpenConnections,
},
)
if err != nil {
return nil, err
}
listeners = append(listeners, listener)
}
return listeners, nil
}
从代码注释可以看出,目前gRPC接口只是用来方便开发者,并不带算实际生产中使用。
交易流程
在我们调用 broadcast_tx_commit 的时候,会先调用 CheckTx,验证通过后会把 TX 加入到 mempool 里。在 kvstore 示例中没有对 transaction 做检查,直接通过:
func (app *KVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx {
return types.ResponseCheckTx{Code: code.CodeTypeOK}
}
放到 mempool 里的 TX 会被定期广播到所有节点。当 Tendermint 选出了 Proposal 节点后,它便会从 mempool 里选出一系列的 TXs,将它们组成一个 Block,广播给所有的节点。节点在收到 Block 后,会对 Block 里的所有 TX 执行 DeliverTX 操作,同时对 Block 执行 Commit 操作。
我们调用 broadcast_tx_commit 返回的结果其实就是 DeliverTX 返回的结果
func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx {
var key, value []byte
parts := bytes.Split(tx, []byte("="))
if len(parts) == 2 {
key, value = parts[0], parts[1]
} else {
key, value = tx, tx
}
app.state.db.Set(prefixKey(key), value)
app.state.Size += 1
tags := []cmn.KVPair{
{[]byte("app.creator"), []byte("jae")},
{[]byte("app.key"), key},
}
return types.ResponseDeliverTx{Code: code.CodeTypeOK, Tags: tags}
}
可以看出它会从输入参数中解析出 key 和 value,最后保存在应用的 State 中。
当所有的 TX 被处理完之后需要调用 Commit 来更新整个区块的状态,包括高度加 1 等:
tendermint的proposer节点选举
概述
endermint的proposer节点选举过程不需要网络通信,而是根据config目录下的genesis.json而决定的。genesis.json文件中有一个配置项是“validators”,这个key对应的是一个validator的列表,包含validator的pub_key和power。pub_key确定是哪个tendermint节点,power决定这个节点被选举为proposer节点的频率。
选举算法详解
1)加载并解析genesis.json,获取每个节点的pub_key和power,power解析后保存在VotingPower中。
2)对每个validator的Accum进行赋值,Accum的值为验证节点的权重值。
validatorsHeap := cmn.NewHeap()
for _, val := range vals.Validators {
// Check for overflow both multiplication and sum.
val.Accum = safeAddClip(val.Accum, safeMulClip(val.VotingPower, int64(times)))
validatorsHeap.PushComparable(val, accumComparable{val})
}
3)选择当次Accum值最大的为提议节点,同时对提议节点的的Accum值减去TotalVotinPower(所有验证节点power的总和)
for i := 0; i < times; i++ {
mostest := validatorsHeap.Peek().(*Validator)
// mind underflow
mostest.Accum = safeSubClip(mostest.Accum, vals.TotalVotingPower())
if i == times-1 {
vals.Proposer = mostest
} else {
validatorsHeap.Update(mostest, accumComparable{mostest})
}
}
注:上面的代码中都有一个times变量,这个变量在正常运行中都是1。如果当前节点中途崩溃重启过,则times是它落后于集群的选举次数。
Tendermint Networks
Tendermint 网络中节点有两种类型:validator node 和 non-validator node。
If we want to add more nodes to the network, we have two choices: we can add a new validator node, who will also participate in the consensus by proposing blocks and voting on them, or we can add a new non-validator node, who will not participate directly, but will verify and keep up with the consensus protocol.
当 tendermint core 收到一个 rpc 交易请求,并完成共识之后,会给 app server 发送请求。
消息通讯
P2P模块初始化的时候,也会初始化mempool reactor、blockchain reactor、consensus reactor、evidence reactor(还有上面讲述的pex reactor),然后add到p2p模块,不同的reactor带不同的channel id
当mempool、blockchain、consensus、evidence模块发送消息的时候,调用p2p模块的send,参数有channel id
对端节点p2p模块收到消息后,会根据channel id把消息转发给对应的模块