tendermint源码解析

  • Post author:
  • Post category:其他

一、最基础的默认配置
源码文件: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把消息转发给对应的模块