从2017年11月启动至今,经过历时近一年的研究、开发与测试,初链主网Beta版于新加坡时间2018年09月28日08:00正式上线,在此之前,07:56分PBFT委员会第一次共识出块和TrueChain fPOW创世区块被挖出。在阅读了一些大神对初链技术的解读之后有一定的了解。本文将对之前白皮书解读和黄皮书解读中的一些疑问进行代码的学习。这里主要记录PBFT选举。
白皮书和黄皮书中选举的相关内容及问题
初链白皮书中对PBFT的选举描述如下:
以 PoW 为基础,选举产生 PBFT 节点的混合共识机制设计可以保证 PBFT 节点出现问题时及时进行重新选举,并对PBFT节点进行实时监督。
因此,PBFT节点是从慢链的挖矿节点中选举的。一定程度上讲,PBFT选举也算是双链交互的一个方面。
初链黄皮书中对PBFT的换届描述如下:
PoW协议选择BFT委员会成员的依据是csize(挖出的区块数量)和节点权益的结合。这就提供了一种必要的准入系统,以处理动态的成员以及在许可的环境下切换委员会。
[问题] 这里的csize与节点权益有什么区别?
[解答] 从代码中看到委员会的选举对节点设置了水果数量的门槛,依靠难度值来评估节点选中的概率。这里设置水果的门槛是滤除那些挖出水果少的、性能较差的节点。
另一方面,我们结合了来自Thunderella 的经过认证的投诉的观点,其中慢链可以作为BFT委员会成员不当行为的证据。也就是说,每当委员会的不当行为从慢链中被发现时,第二天的起始点(不一定是第k天)就会触发委员会强制换届。
因此,换届有两种方式:一种固定时间的强制换届(即这里的选举),一种由不当行为触发的特定的换届。
[问题] 不当行为触发的换届是否有在代码中实现?如何进行识别行为不当?
[解答] 在委员会的选举过程的代码中没有体现,可能在其他位置进行设定,还需要学习和追踪。
代码解读
这里在代码中加入一些注释,包含变量的作用、自己的思考与问题,以便更好的理解函数。注释中包含一些关于GO语言语法的学习。
查看了相关代码,对如下一些函数进行细致分析:
getCommittee:获取委员会成员
electCommittee:选举委员会成员
getCandinates:获取候选人
elect:对候选人进行选举
先从选举委员会成员开始。
electCommittee:选举委员会成员
// electCommittee elect committee members from snail block.
// go语法:实现接口的方法;(e *Election)为结构体指针;函数作用:选举委员会成员
func (e *Election) electCommittee(snailBeginNumber *big.Int, snailEndNumber *big.Int) []*types.CommitteeMember {
// 打印LOG:选举范围:慢链开始的区块数、慢链结束的区块数、参与选举慢链节点的水果数量门槛、最大的委员会委员数量
log.Info("elect new committee..", "begin", snailBeginNumber, "end", snailEndNumber, "threshold", params.ElectionFruitsThreshold, "max", params.MaximumCommitteeNumber)
// go语法:变量名为committee的CommitteeMember类型的指针切片
var committee []*types.CommitteeMember
// singleNode为布尔值,表示是否为单节点;
if e.singleNode {
// 如果只有一个节点,则直接把创世委员会节点的第一个放入到committee中并返回committee。
// [问题] 通过genesisCommittee是一个数组(或切片)的形式推断创世委员会成员也不止一个,那么singleNode会在什么时候使用?
committee = append(committee, e.genesisCommittee[0])
return committee
}
// 将默认的members放到committee里面
for _, member := range e.defaultMembers {
committee = append(committee, member)
}
// 调用getCandinates获取候选人和随机数种子
seed, candidates := e.getCandinates(snailBeginNumber, snailEndNumber)
if candidates == nil {
// 如果没有候选人,则仍然使用目前的委员会成员
log.Info("can't get new committee, retain current committee")
} else {
members := e.elect(candidates, seed) // 通过seed来选举候选人
for _, member := range members {
committee = append(committee, member) // 将选举之后的members放入到committee中。
}
}
return committee // 返回committee:选举完成,返回新的委员会成员(可能没有候选人,仍然是之前的委员会成员)
}
该函数使用了如下两个函数:获取候选人和对候选人进行选举。这中间通过随机数种子seed进行随机地选举委员会成员。这里随机数种子seed是与选举中选用的慢链区块区间中的区块相关,如果挖矿节点的难度区间越大,其获得选举的可能性越高。且看后面的elect的分析。
getCandinates:获取候选人
// getCandinates get candinate miners and seed from given snail blocks
// go语法:实现接口的方法;函数作用:获取候选矿工以及从慢链区块中获取种子。
func (e *Election) getCandinates(snailBeginNumber *big.Int, snailEndNumber *big.Int) (common.Hash, []*candidateMember) {
// fruitsCount作用:标记每个地址挖出了多少个水果
var fruitsCount map[common.Address]uint64 = make(map[common.Address]uint64)
// 候选人的指针切片
var members []*candidateMember
var seed []byte
// get all fruits want to be elected and their pubic key is valid
// 获取公钥有效且水果表示愿意被选举的节点
for blockNumber := snailBeginNumber; blockNumber.Cmp(snailEndNumber) <= 0; {
// 通过区块number来获取区块
block := e.snailchain.GetBlockByNumber(blockNumber.Uint64())
if block == nil {
return common.Hash{
}, nil
}
// 把区块的哈希按照字节放入到seed切片中
seed = append(seed, block.Hash().Bytes()...)
// 获取慢链的水果
fruits := block.Fruits()
for _, f := range fruits {
if f.ToElect() {
// 如果这个水果表示是愿意被选举的
// 获取public Key
pubkey, err := f.GetPubKey()
if err != nil {
// 异常处理
continue
}
// 通过Public Key 转换成Address
addr := crypto.PubkeyToAddress(*pubkey)
// 获取难度值
act, diff := e.engine.GetDifficulty(f