11. TBFT 开源引擎

11.1. 概述

Tendermint是一个开源的完整的区块链实现,可以用于公链或联盟链,其官方定位是面向开发者的区块链共识引擎。Tendermint引以为傲的是其共识算法 —— 世界上第一个可以应用于公链的拜占庭容错算法。由于避免了POW机制,Tendermint可以实现很高的交易吞吐量。 Tendermint同时是拜占庭容错的(Byzantine Fault Tolerance),因此对于3f+1个 验证节点组成的区块链,即使有f个节点出现拜占庭错误,也可以保证全局正确共识的达成。同时在极端环境下,Tendermint在交易安全与停机风险之间选择了安全,因此当超过f个验证节点发生故障时,系统将停止工作。

Tendermint共识算法和PBFT最大的不同点就是Tendermint没有PBFT的View Change阶段。Tendermint很巧妙的把超时的情况跟普通情况融合成了统一的形式,都是 propose->prevote->precommit 三阶段,只是超时的时候通过投空票从而进入新的轮次来切换主节点。而PBFT是有一个单独的View Change过程来触发primary轮换。因此相比PBFT,Tendermint共识算法更加高效和灵活。

TBFT引擎采用的共识算法,即TBFP共识算法就是基于Tendermint算法,为了更好的适应区块链环境,我们在其基础上进行了很多优化和改进,实现了名为TBFT的共识算法,它生于Tendermint,优于Tendermint。

11.2. TBFT 开源引擎

站在区块链定制化角度来看,tendermint其实已经非常优秀。tendermint作最大的特点是其差异化的定位: 尽管包含了区块链的完整实现,但它却是以SDK的形式将这些核心功能提供出来,供开发者 方便地定制自己的专有区块链。tendermint的SDK中包含了构造一个区块链节点旳绝大部分组件,例如加密算法、共识算法、 区块链存储、RPC接口、P2P通信等等,开发人员只需要根据其应用开发接口 (Application Blockchain Communication Interface)的要求实现自己 的应用即可。ABCI是开发语言无关的,开发人员可以使用自己喜欢的任何语言来开发基于tendermint的 专用区块链。

虽然tendermint已经非常优秀,但是它仍然有很多局限性以及不足之处。tendermint的抽象性不足够,用户需要做非常多的操作才能适配到自己的环境,很多开发者想自己定义的模块,由于tendermint没有抽象出来,开发者不能够自定义,比如说通信网络、区块链存储、加密算法等等。因此为了弥补tendermint的这些不足,我们设计开发了TBFT开源引擎。

TBFT开源引擎就是使用TBFT共识算法作为核心的开源引擎架构,它的目标是允许开发者轻松创建自己定义的基于TBFT共识算法的区块链。TBFT引擎只专注于共识本身,不关心共识之外的其他实现,如网络通信、验签、共识内容、持久化等。开发者可以实现引擎对外提供的接口,启动自定义的节点,从而能够快速搭建一条基于TBFT共识算法实现的区块链。

TBFT共识算法已经是一个成熟且完善的共识算法,在长安链项目中,TBFT共识算法作为其核心的共识算法之一,已经稳定运行了很久,结合开源社区使用情况,解决了很多实际遇到的问题并且会继续优化和更新,这也是tendermint不具备的优势,因此TBFT开源引擎也是长安链社区对外贡献的一部分。

TBFT开源引擎包括两个主要的内容:共识引擎和通用应用接口。 共识引擎就是TBFT共识算法,保证所有的机器达成相同的共识操作从而确保安全性和活性。 应用接口,则是对外暴露一系列需要开发者实现的接口,从而达到自定义区块链的目的。在后面的章节,我们会对TBFT引擎的这两个内容进行详细的介绍。

11.3. TBFT VS Tendermint

TBFT与原生Tendermint的区别主要包括两个方面,第一是共识算法本身的优化,第二则是从开源角度来讲,TBFT具有更优越的开源性。 共识算法上来看,TBFT在Tendermint共识算法上改进点:
1)增加同步节点状态时的传票机制,在弱网络下的稳定性更强;
2)优化proposal提案广播机制,采用斐波那契数列发送方式,降低消息广播量;
3)节点重启时采用round快速同步模式,使得该节点可尽快与其他节点对齐;
4)将共识核心流程与辅助流程分离,提供一致性引擎辅助消息广播;
5)优化WAL写入模型,降低写入次数和内容,提供整体处理性能;
6)二阶段协议增强,支持随机函数类交易剔除操作;
7)优化超时处理机制,支持节点间动态调整超时时间,提高共识效率;
8)异步并行的消息处理模型,提高消息处理并行度,降低共识时间;
9)增加超前票的缓存机制,当节点状态达到时,可直接使用,无需等待。

在独立开源角度来看,TBFT具有如下的优势:
1)TBFT更加纯粹,它面向的对象是一个批次,这个批次可以是交易集合,也可以是其他的信息,TBFT实现本身是不关心共识内容的;
2)TBFT接口更清晰、独立。用户可以根据自己的业务场景,自定义签名、验证规则,网络消息处理、WAL存储等多种信息,TBFT提供的是一个更加独立的框架;
3)TBFT依赖库小,并且提供了基本的Demo模型,用户可根据Demo模型实现自己的区块链,而不需要太复杂的编码操作。

11.4. TBFT 共识算法

11.4.1. TBFT 算法三阶段提交

TBFT引擎,既TBFT共识算法是一种拜占庭容错共识算法,也是最多容忍不超过1/3的恶意节点。协议遵循一个简单的状态机,通过消息事件推动状态的改变。TBFT共识主要有一下几个阶段:NewHeight、NewRound、Propose、Prevote、Precommit、Commit。作为一个BFT类的共识算法。TBFT对应的三阶段分别是Propose,Prevote,Precommit三个阶段:


在上面这个三阶段图例中,第一阶段是主节点创建proposal并且广播给其他从节点的过程,第一阶段所有的节点会开启一个定时器,如果在这个超时时间内没有收到提案(主节点来看就是没有在超时时间内创建提案,从节点来看就是在超时时间内没有收到主节点的提案),那么节点就会投一个空票。节点在一阶段收到了proposal或者一阶段超时,就会基于这个proposal生成prevote预投票,或者投空prevote票从而进入第二阶段。

第二阶段就是节点收集一阶段生成的prevote预投票,在这个阶段中,每个节点需要收集到2/3+1张针对同一个proposal的prevote预投票(prevote可能是空投票,一阶段超时生成的是空prevote投票),在这个收集过程中,如果收集到了2/3+1个任意的投票(任意的意思是可能存在针对不同proposal的投票或者空投票)就会开启第二阶段定时器。第二阶段收集到了足够的prevote投票或者第二阶段超时,会生成precommit预提交投票进入第三阶段。

第三阶段就是收集第二阶段生成的预提交投票,和二阶段一样,第三阶段也是需要收集2/3+1张针对同一个proposal的precommit预提交投票,如果收集到了2/3+1个任意的预提交投票,会开启第三阶段定时器。第三阶段收集到了足够的precommit预提交投票,或者第三阶段超时则会进入到提交阶段。以上就是TBFT共识算法的三阶段核心流程,除了经典的三阶段,在三阶段前后TBFT还有其他的步骤和阶段进行对三阶段提交的补充。

11.4.2. TBFT 算法流程

11.4.2.1. NewHeight阶段

NewHeight阶段属于特殊阶段,是一个新高度共识的开始阶段,这个阶段表示上一个高度的batchMsg已经被commit了,开启下一个高度的共识。

11.4.2.2. NewRound阶段

NewHeight阶段之后,会进入到NewRound阶段,这时候是从round 0开始进行共识流程。TBFT达成一个batchMsg的共识可能需要多个round,因为在上文我们描述三阶段的时候提及到了如果是超时就有可能投空票,因此在一个round的共识过程中,TBFT可能在commit提交阶段的时候,没有收到2/3+1个基于同一个proposal的precommit预提交投票,因此在这个round我们没有达成基于这个proposal的共识。如果在一个round没有达成有效batchMsg投票一致的话,共识不会提交batchMsg,而是会round+1进入继续到NewRound阶段。NewRound阶段之后我们就会进入到Propose阶段,这个阶段就开始熟悉的BFT三阶段了。

11.4.2.3. Propose阶段

到了这个阶段,主节点就开始根据batchMsg生成提案了,主节点的选取规则是根据高度和round以及共识节点列表来计算出主节点。生成proposal之后,直接点会把proposal发送给其他从节点,然后共识进入到prevote阶段。从节点收到proposal后,验证proposal和batchMsg之后,也会进入到prevote阶段。Propose阶段所有的节点还会开启一个一阶段定时器,如果在这个时间之内,如果没有收到proposal(主节点可能是没有收到batchMsg,从节点可能是没有收到proposal),则会超时进入到prevote阶段。

11.4.2.4. Prevote阶段

在Prevote开始阶段,节点会根据收到的proposal进行构造prevote投票,并将prevote投票广播给其他节点。如果由于某些原因当前节点并没有收到任何proposal,那么会签名并广播一个空的prevote投票。prevote阶段会不停的收取来自其他节点的prevote投票,如果收到了2/3+1的任何prevote投票(包括自己的prevote投票),prevote阶段还会开启二阶段定时器,如果在这个时间内,没有收到2/3+1的同一个proposal的prevote投票,则会超时进入precommit阶段。(注:必须是收到2/3+1的任意prevote投票才开启定时器。)如果收到了2/3+1的基于同一个proposal的prevote投票(2/3+1的空prevote投票也是一样)就会进入到precommit阶段。

11.4.2.5. PreCommit阶段

Prevote阶段超时或者收到2/3+1的prevote投票的时候,就进入到Precommit阶段,如果此时节点收到+2/3的Prevote的投票,则会生成并且广播一条Precommit投票。和Prevote阶段一样,如果收到了2/3+1的任何precommit投票(包括自己的precommit投票),precommit阶段还会开启第三阶段定时器(注:必须是收到2/3+1的任意precommit投票才开启定时器)。如果在这个时间内,没有收到2/3+1的同一个proposal的precommit投票,则会超时进入Commit阶段。如果收集到了2/3+1的同一个proposal的precommit投票,则会立即进入到Commit阶段。

11.4.2.6. Commit阶段

Commit阶段是共识流程的最后阶段了,如果收到了针对本轮次的2/3+1个precommit投票,并且之前也收到了对应这个precommit集的proposal,则会commit这个proposal中的batchMsg,然后进入NewHeight 阶段, 开启新的height;而如果没有收集到这个2/3+1的针对这个proposal的precommit投票集或者没有收到proposal,则进入NewRound 阶段, 开启新的一轮共识。

11.4.3. TBFT 引擎工作流程

11.4.3.1. TBFT 引擎流程图

在第上一章节,我们详细分析了TBFT共识算法的原理。TBFT引擎包括共识算法和应用接口。通过应用层实现的应用接口,从而让TBFT引擎运作起来。下面我们的流程图展示了TBFT引擎的工作流程,包括应用层和底层引擎的交互。

11.4.3.2. TBFT 引擎运行流程

在上面的TBFT引擎整体流程图中能够看到,TBFT引擎通过抽象接口给上层实现,从而达到了自定义区块链的目标。在后面的章节,我们会详细描述TBFT开源引擎的另外一个重要内容:TBFT引擎应用接口。本章节我们先描述TBFT引擎的运行流程。

enterNewHeight阶段是共识流程的开始阶段,表示我们进入了一个新的区块高度。 NewHeight阶段之后,会进入到NewRound阶段,每个高度都是从round 0开始进行共识流程。 TBFT达成一个可以commit的共识可能需要多个round。接着共识进入到enterPropose阶段,这时候节点会开启第一阶段的定时器。 在这个阶段需要注意的是,主、从节点接下来的流程有一些区别:
1)主节点视角:通过sendProposeState方法通知应用层,通过应用层创建batchMsg(关于batchMsg我们在第三章节已经提及,可以把他理解为需要共识的内容)。 共识引擎通过BatchMsgInterpreter接口监听应用层产生的batchMsg。 主节点收到batchMsg,调用Verifier接口给batchMsg签名,接着创建proposal,再调用Verifier接口给proposal签名,再调用Coder接口序列化Proposal,然后再通过NetHandler接口将序列化后的proposal发送给从节点。 同时主节点进入enterPrevote阶段,如果主节点在第一阶段超时时间内没有收到batchMsg,那么它也会enterPrevote阶段,只是在prevote阶段会投一个空的prevote投票;
2)从节点视角:在enterPropose阶段,从节点开启第一阶段定时器后,会一直等待主节点发送来的proposal。接收到proposal之后,从节点会调用Coder接反序列化得到proposal对象,再调用Verifier接口验证proposa是否合法,再调用Verifier接口验证batchMsg,接着通过handleVerifyResult方法获取batchMsg的验证结果。 最后从节点也进入到enterPrevote阶段。如果从节点在第一阶段超时时间内没有收到有效的proposal,也会进入到enterPrevote,当然和主节点一样,在这个阶段它也是投prevote空票。

在进入到enterPrevote阶段后,节点会生成prevote投票,同时使用Verifier接口签名,最后将prevote投票通过procProvote方法进行处理。在prevote阶段会通过procProvote不停的接收处理本节点和其他节点生成的prevote投票,procProvote方法收到prevote投票后,首先会通过Verifier接口验证prevote投票是否合法,然后将prevote投票添加进投票集合,如果是本节点的投票,会调用walHandler接口将proposal保存(保存proposal的目的是为了防止节点异常停止,重启恢复时能够恢复共识状态),同时如果是本节点的投票,还会将本节点的prevote投票发送给其他节点。当收到2/3+1张任意投票时,就会开启第二阶段的定时器,如果接收到2/3+1张针对同一个proposal的投票时,就会进入到precommit阶段,在precommit阶段会生成precommit投票。在第二阶段定时器超时时,节点也会进入到precommit阶段,当然在这个阶段它会投precommit空票。

如果第二阶段超时或者收集到了足够的prevote投票,共识会进入到precommit阶段。在这个阶段,节点会生成precommit投票,同时使用Verifier接口签名,最后将precommit投票通过procPrecommit方法进行处理。在precommit阶段会通过procPrecommit方法接收处理本节点和其他节点生成的precommit投票,procPrecommit方法收到precommit投票后,首先会通过Verifier接口验证precommit投票是否合法,然后将precommit投票添加进投票集合,同时如果是本节点的投票,还会将本节点的precommit投票发送给其他节点。当收到2/3+1张任意投票时,就会开启第三阶段的定时器,如果接收到2/3+1张针对同一个proposal的投票时,就会进入到commit阶段。在第三阶段定时器超时时,节点也会进入到commit阶段。

在commit阶段,代表共识进入到最后的提交阶段了。如果收集到了足够的针对一个proposal的非空precommit投票,在commit阶段就可以将这个proposal中的batchMsg进行提交了,这时候调用Committer接口应用层就会提交这个batchMsg。共识引擎还监听着Committer接口,应用层完成这个batchMsg的提交之后还应该通过Committer接口通过共识引擎,告诉引擎可以开始新一个高度的共识了,这时候共识又进入enterNewHeight开始新的共识流程。如果共识进入commit阶段的时候,没有收集到足够的有效precommit投票,那么他不会提交,而是进入enterNewRound,开始在这个高度的新一个round的共识。

通过上面的描述,TBFT共识引擎的完整流程已经阐述清楚,在讲述过程中提及了很多的接口,就是这些接口我们才实现了所谓的区块链自定义,下面的章节我们详细描述这些应用接口的作用和意义。

11.4.4. TBFT 引擎应用接口

在TBFT引擎中,我们定义了一系列的接口,开发者通过实现这些接口,共识引擎和应用层的配合从而实现自定义基于TBFT共识算法的区块链。这些接口主要是:实现网络处理的NetHandler、创建batchMsg的BatchMsgInterpreter、实现proposal序列化和反序列化的Coder、提交batchMsg的committer、日志功能的Logger、获取共识参数的ParamHandler、实现签名验签的Verifier、存储共识状态的Walhandler、自定义共识内容的batchMsg。

11.4.4.1. NetHandler

type NetHandler interface {
   BroadCastNetMsg(data []byte, to string) error
   Listen() <-chan interface{}
}

NetHandler是网络处理器,作用是用于共识消息的收发处理,NetHandler只管消息的接收和发送,消息的具体处理包括序列化等都不关心,这都是TBFT引擎自己处理和实现。NetHandler有两个方法:
1)BroadCastNetMsg方法的作用是将消息发送给其他节点,第一个参数是序列化后的共识消息,第二个参数to是指接受者,如果to为””则表示需要广播消息给所有的节点;
2)Listen方法的意义是:TBFT引擎通过监听这个方法返回的chan,共识引擎可以获取到接收到其他节点发来的共识消息,然后引擎内部进行处理。
应用层可以使用自己的方式去实现NetHandler接口,实现对proposal、vote等消息的发送和接收,应用层可以使用HTTPS、P2P、RPC等通信方式去实现自己网络处理器,如果是用于正式的生产环境,NetHandler的实现需要考虑通信安全,采用安全性高的通信方式。

11.4.4.2. BatchMsgInterprete

type BatchMsgInterpreter interface {
   PrepareBatchMsg() <-chan *ProposalBatchMsg
}

BatchMsgInterpreter的作用是产生batchMsg,TBFT共识引擎监听这个接口获取应用层产生的batchMsg来进行共识。
PrepareBatchMsg()方法是返回一个chan,TBFT引擎需要监听这个chan,从而获取到应用层产生的batchMsg。
在之前介绍TBFT共识算法的时候我们提及了BatchMsg,TBFT共识算法需要batchMsg来生成proposal进行共识,应用层需要实现BatchMsgInterpreter接口,如果业务所需要的batchMsg较为复杂的话,应用层还需要实现自己的交易池、交易执行、调度等,以便应用层能够高效的创建batchMsg来提供给TBFT共识算法。

11.4.4.3. Coder

type Coder interface {
   MarshalProposal(p *Proposal) ([]byte, error)
   UnmarshalProposal(data []byte) (*Proposal, error)
}

Coder是共识Proposal解码器,实现TBFT引擎proposal的序列化和反序列化。
1)MarshalProposal实现将共识的proposal序列化;
2)UnmarshalProposal实现将一个bytes反序列为共识proposal。
在TBFT共识算法中,TBFT根据从应用层获取的batchMsg生成了proposal。由于batchMsg是应用层定义的,在TBFT算法内部不清楚应用层batchMsg的实现细节,在TBFT引擎往其他节点发送proposal以及接收其他节点发来的proposal的时候,需要调用应用层实现的Coder进行序,因此需要应用层实现对proposal的序列化和反序列化。开发者可以使用protobuf、amino等序列化方式。

11.4.4.4. Committer

type Committer interface {
   Commit(b BatchMsg, vs *tbftpb.VoteSet) error
   CommitDone() <-chan uint64
}

Committer是batchMsg提交器,作用是将达成共识的batchMsg持久化提交。
1)Commit方法实现将一个达成共识的batchMsg和达成共识的投票集合,vs就是达成共识的投票集合,它的内容是2/3+1个节点,针对这个batchMsg的precommit投票集合;
2)commitDone方法是应用层完成了这个batchMsg的提交,通知共识引擎可以开始下一次共识操作了,TBFT引擎监听此chan获取这个通知。
在TBFT算法共识的最后,即对一个batchMsg达成共识后,我们需要将batchMsg提交,也就是区块链中落块的过程,这时候需要应用层实现对batchMsg的commit。开发者可以采用区块链项目比较主流的leveldb、rocksdb等数据库进行batchMsg的持久化。

11.4.4.5. Logger

type Logger interface {
   Debug(args ...interface{})
   Debugf(format string, args ...interface{})
   Debugw(msg string, keysAndValues ...interface{})
   Error(args ...interface{})
   Errorf(format string, args ...interface{})
   Errorw(msg string, keysAndValues ...interface{})
   Warn(args ...interface{})
   Warnf(format string, args ...interface{})
   Warnw(msg string, keysAndValues ...interface{})
   Info(args ...interface{})
   Infof(format string, args ...interface{})
   Infow(msg string, keysAndValues ...interface{})
   Fatal(args ...interface{})
   Fatalf(format string, args ...interface{})
   Fatalw(msg string, keysAndValues ...interface{})
}

Logger接口的作用是打印日志,TBFT引擎内部运行过程中输出的日志,应用层也可以自定义处理方式,Logger接口的方法都是常见的日志处理方法。

11.4.4.6. ParamsHandler

type ParamsHandler interface {
   GetNewParams() (validators []string, timeoutPropose time.Duration,timeoutProposeDelta time.Duration, tbftBlocksPerProposer uint64, err error)
}

ParamsHandler是共识参数处理器,TBFT引擎通过这个接口获取一次新的共识需要的参数。
GetNewParams方法是TBFT引擎从应用层获取一次新的共识开始所需的参数,包括应用层维护的共识节点列表、超时时间、超时递增时间、连续生成batchMsg次数等。
在项目执行过程中,应用层可能涉及到一些参数的调整,比如说应用层业务逻辑实现了共识节点的新增和删除等功能、需要调整三个阶段的超时时间。因此TBFT引擎在新的高度开始共识时,都需要获取应用层这些最新的参数。

11.4.4.7. Verifier

type Verifier interface {
   SignProposal(p *Proposal) error
   SignBatchMsg(b BatchMsg) error
   SignVote(v *tbftpb.Vote) error
   VerifyProposal(p *Proposal) error
   VerifyBatchMsg(b BatchMsg) (*VerifyResult, error)
   VerifyVote(v *tbftpb.Vote) error
}

Verifier是共识验证器,它实现应用层对Proposal、Vote和batchMsg的签名和验签,引擎内部不关系这些签名验签细节,只关心对这些消息的签名和验证结果。
1)SignProposal是实现对proposal的签名;
2)SignBatchMsg是实现对batchMsg的签名;
3)SignVote是实现对Vote的签名,包括prevote和precommit;
4)VerifyProposal是实现对proposal的验签操作;
5)VerifyBatchMsg是实现对batchMsg的验签操作;
6)VerifyVote是实现对prevote和precommit的验签操作。
TBFT共识需要应用层实现对共识消息的签名和验签,因为TBFT发送的消息接收节点需要确认是发送者发的,因此需要签名和验签操作,应用层需要自己实现签名和验签细节,签名后需要将签名写入到消息体内,验签时需要将消息体内的签名进行验签操作。开发者可以使用任意的签名和验签方式,如果不考虑安全因素的话,甚至可以不进行签名和验签。但是在实际项目中,还是需要使用非对称签名等安全性高并且方便的加密方式。

11.4.4.8. WalHandler

type WalHandler interface {
   Write(entry *WalEntry) error
   ReadLast() (*WalEntry, error)
}

WalHandler处理器作用是共识状态的保存和恢复,实现读写共识消息的wal操作。
1)Write是实现对一个共识状态的写;
2)ReadLast是实现对最新的一个共识状态的读操作,TBFT引擎重启时候只需要读取最新写的一个共识状态。
TBFT在第二阶段的时候需要保存共识状态,以便在节点重启恢复共识的时候,恢复关闭前的共识状态,因此需要应用层实现对共识状态的保存和恢复。TBFT共识算法只需要最后保存最后一个高度的第二阶段的状态即可保证共识的安全性,因此应用层的时候只需要实现对最新的一个状态的恢复。

11.4.4.9. BatchMsg

type BatchMsg interface {
   Sequence() uint64
   Key() []byte
}

BatchMsg接口是共识内容的定义,TBFT引擎的目的是实现自定义共识内容,因此任何内容都可以进行共识。在某种程度上,为了方便理解,可以把bacthMsg当作是区块链的block。
1)Sequence方法是获取共识batchMsg内容的索引,可以理解为区块链广义上的高度这个概念;
2)Key方法是获取共识batchMsg内容的Key,可以理解为区块链广义上的blockHash。
TBFT在第一阶段需要从应用层获取到batchMsg来生成proposal,batchMsg就是共识内容最重要的核心,TBFT引擎是不关系共识内容的,任何结构或者类型都可以交给TBFT引擎进行共识,只要是实现了BatchMsg接口即可。