写在前面
hotstuff与raft有类似的结构和行为,因此可以借鉴etcd-raft的设计。笔者打算使用Rust+tokio异步栈来实现一个事件驱动hotstuff的demo,通过动手来进一步学习和思考。
目前最新的重构代码在:https://github.com/Tsumida/hotstuff-consensus/tree/dev/hotstuff-rs
要求:
- 在无故障状态下能连续、正确达成共识。
具体来说:
- 所有节点都是诚实节点
- 网络无故障,
- leader在每一个view进行一次propose,各个节点应当accept该proposal,leader随后计算出门限签名并且生成qc。
- view change时各个节点应该知悉上一轮共识达成的qc并且用其更新自己的qc-high。
参考:
代码:
交互
正常提交流程
下面是无故障情况下,client发起操作,到操作被提交的大概流程。(实际上需要累积三次proposal才能提交cmd)。
经验总结
起初笔者参考etcd-raft实现,将hotstuff分为 statemachine, pacemaker, network, eventbus 几个模块,模块之间通过channel通信,每个模块定义自己的输入输出结构体,由EventBus进行转换,模块之间基本完全解耦:

但是写着写着, 笔者发现问题比较大:
- 不同模块会对类似事件做重复定义,比如StateMachine的一个输出
Ready::Update, 要转化为ToDo::UpdateQCHigh来传递给Pacemaker。 - 不同的输入输出之间的转换存在障碍。比如
ToDo::UpdateQCHigh需要node和qc_high两个参数,而Ready::UpdateQCHigh只包含qc_high。 - StateMachine独享大部分状态,而Pacemaker无法获取一些必需状态,阻碍了模块间输入输出的转换。
- 一些Msg的定义不包含上下文,导致Network component拿到消息后不知道该向谁发rpc。
- 每个模块之间进行双向通信需要两个channel,eventbus作为枢纽,包含大量channel成员。
这些障碍使得作者花了很多时间进行修改。改着改着,代码逐渐变为shit mountain,继续写下去只会浪费更多实践。笔者只好推倒重来,重新规划。好在这坑也不是白踩,笔者还是进行了一些思考,总结了一点经验,如:
- 抽象出一个共享存储MemPool,Pacemaker和Statemachine都可以访问它,从而完成
QC <-> TreeNode这样的双向查找。 - 把常见的curd操作封装好,这样不用写大量重复代码。
- 定义一套Statemachine, Pacemaker,network component共用的消息、事件集合。
- TreeNode, GenericQC等大对象会在多个协程中流动,使用Box或者Arc智能指针来减少内存复制。
针对上面的问题,笔者了几天重写,结构如下:

另外,节点对外暴露restful api,用于peer之间的通信和client\server通信 :
/hotstuff/status用于客户端获取StateMachine的状态快照。/hotstuff/new-tx用于发起新的交易。- leader通过
http://node-a:port/hotstuff/new-proposal向node-a发送新proposal。
笔者考察了两个http库:
- warp, 基于tokio, 通过组合来实现http服务。缺点是封装不够完善,一些组合函数的返回值较繁琐而且意义不明,组合子写起来也很麻烦。
- actix-web, 基于tokio和actix,社区生态很好,活跃更新中。
actix-web写起来简单,但是它的task必须要跑在 actix-web::runtime 上,而笔者的hotstuff模块使用的tokio runtime, 最后解决办法是:
- main函数开两个线程,分别启动actix-web-runtime和tokio-runtime, hotstuff proxy和 Machine之间用
tokio::sync::mpsc::channel通信。实测可行。
实现细节
门限签名
笔者使用这个库:https://github.com/poanetwork/threshold_crypto
这个库的使用者较多,也通过了安全审计,没发现大问题。
GenericQC与TreeNode
GenericQC与TreeNode有紧密联系, 每一个node都有justify域,justify用于判断proposal是否可以接受
另外,可能出现不同节点的justify域相同的情况。如下图,绿色节点b’还没达成共识系统就进入了下一轮,因此它的下一个节点b’’还是沿用之前的justify。

下图是理想情况, 蓝色节点b与其后面三个连续的三个节点形成了3链,因此b及其前面三个节点都可以提交了。

因此,对于TreeNode和GenericQC,有如下注意事项:
- TreeNode通过justify域找到对应的GenericQC
- GenericQC能通过node域找到对应的TreeNode
- 两者不能形成无限递归结构
由于要通过网络传输,所以不能使用内存指针和引用。
<br />最后确定用Sha256来计算TreeNode和GenericQC的哈希值,分别存储在 qc.node和node.justify域。Node通过一个HashMap qc_map来寻找GenericQC。
下一步
笔者肝了一个星期终于让demo跑起来了。虽然设计上参考了etcd,但这终究是闭门造车,代码设计上很拉胯, 仅仅能跑起来。笔者下一步要做的,是进行大量调研和思考,从而进一步完善demo,使其称为一个可用的轮子。
要做的工作包括:
