主网上线
- 初链主网Beta版与新加坡时间2018年09月28日08:00正式上线,那么初链为何物呢?我们今天来粗浅的聊聊。
何为初链
- 初链是一种全新的混合共识公链,他创新性的采用PBFT+PoW的混合共识机制,用快、慢链的双链结构在效率和性能之间取得了较好的平衡点。其中,使用PBFT为快链,主要用来存储账本和达成交易;使用PoW做为慢链,主要用来挖矿和拜占庭委员会的选举。那么PBFT和PoW又分别为何物呢?
- PBFT共识机制全称叫实用拜占庭容错,是降低了拜占庭协议的运行复杂度,从指数级别降低到多项式级别(Polynomial),使拜占庭协议在分布式系统中应用成为可能的一种改进算法。
- PoW共识机制随着中本聪:一种点对点的电子现金系统的发布而闻名于世,PoW在共识安全性能方面较为出色,理论上来说,可以抵御小于50%的自私节点的攻击,但是它也有几个明显的缺点:
- PoW的前提是假设节点和算力是均匀发布的,因此通过CPU算力进行竞选创建的区块的权利,拥有节点数和算力值应该大致匹配,然而随着人们将CPU挖矿逐渐升级到GPU、FPGA,直至ASIC矿机挖矿,节点数和算力值也渐渐失配。
- 另一方面就是PoW对资源的浪费了,为了竞争到创建区块的权利,需要做大量的无用哈希运算。这既是对资源的一种浪费,也不符合现阶段可持续发展的路线。
选举过程分析
-
为了优化PoW共识机制,初链引入了PBFT来组建混合共识机制,但是我们知道,PBFT虽好,但是只能用在联盟链(许可链)和私有链中,因为PBFT算法在完成一次共识时会在共识的节点之间进行多次数据交互,如果参与共识的节点过多则网络的带宽会成为明显的瓶颈,所以对参与PBFT共识的节点数量是有要求的,但效率较公链共识算法而言要高得多。既然如此,那就你应该马上能够想到初链需要选举出一个拜占庭委员会来负责全网共识的快速推进,下面我就粗浅的分析下拜占庭委员会的选举过程:
双链结构——初链TrueChain采用PBFT+PoW混合共识的双链结构, 快链用于存储账本和达成交易,慢链用于挖矿和委员会选举,从而实现了公平和效率的有效融合。
-
老规矩,先看总体流程图,而后再抠细节。
-
看了上面的流程图,相信有一个简单的概念了,下面看看一些实现细节,因为涉及到的代码繁琐且量大,所以有些地方就省略了部分代码,只呈现关键部分,大家可以去代码详情细览。
-
下面是选举的外部接口,主要是一些预处理以及防止重复选举的逻辑。在调用
getCommittee
之前,先从Cache内获取到当前的委员会名单,如果当前存在一个委员会名单,则不再选举,这里是为了避免重复选举,在Cache内有一个点需要注意:committeeNumber := new(big.Int).Div(snailNumber, params.ElectionPeriodNumber)
这说明拜占庭委员会成员数是根据慢链的数量变化的,这里的ElectionPeriodNumber
是144,可配置。
func (e *Election) GetCommittee(fastNumber *big.Int) []*types.CommitteeMember { ... // @Zuo 从缓存中获取到委员会名单,防止重复选举 committee := e.getCommitteeFromCache(fastNumber, snailNumber) if committee != nil { return committee.Members() } // @Zuo 选举 committee = e.getCommittee(fastNumber, snailNumber) if committee == nil { return nil } ... }
-
选举的内部逻辑入口,包括创建创世拜占庭委员会逻辑,以及选举委员会成员逻辑。
func (e *Election) getCommittee(fastNumber *big.Int, snailNumber *big.Int) *committee { ... // @Zuo 创建创世拜占庭委员会成员 if committeeNumber.Cmp(common.Big0) == 0 { // genesis committee log.Debug("get genesis committee") return &committee{ id: new(big.Int).Set(common.Big0), beginFastNumber: new(big.Int).Set(common.Big1), endFastNumber: new(big.Int).Set(common.Big0), firstElectionNumber: new(big.Int).Set(common.Big0), lastElectionNumber: new(big.Int).Set(common.Big0), switchCheckNumber: params.ElectionPeriodNumber, members: e.genesisCommittee, } } ... // @Zuo 选举拜占庭委员会 members := e.electCommittee(preBeginElectionNumber, preEndElectionNumber) return &committee{ id: new(big.Int).Sub(committeeNumber, common.Big1), beginFastNumber: new(big.Int).Add(preEndFast, common.Big1), endFastNumber: lastFastNumber, firstElectionNumber: preBeginElectionNumber, lastElectionNumber: preEndElectionNumber, switchCheckNumber: lastSnailNumber, members: members, } ... }
-
进入上文的
electCommittee()
函数,可以看到有两个主要逻辑——获取候选人名单和正式选举,下面分开来讲:- 获取选举人,可以看到是从慢链中选取合适的fruits。计算其贡献的水果的难度值,然后根据其难度值划分其在256位空间中的分布情况,以便后面进行选举。
// 从慢链中获取符合规则的候选人 func (e *Election) getCandinates(snailBeginNumber *big.Int, snailEndNumber *big.Int) (common.Hash, []*candidateMember) { ... // @Zuo 从所有慢链里面刷选所有合法的且愿意参选的fruits for blockNumber := snailBeginNumber; blockNumber.Cmp(snailEndNumber) <= 0; { block := e.snailchain.GetBlockByNumber(blockNumber.Uint64()) if block == nil { return common.Hash{}, nil } seed = append(seed, block.Hash().Bytes()...) // @Zuo 刷选愿意参选的fruit fruits := block.Fruits() for _, f := range fruits { if f.ToElect() { pubkey, err := f.GetPubKey() if err != nil { continue } addr := crypto.PubkeyToAddress(*pubkey) // @Zuo 获取其水果难度值 act, diff := e.engine.GetDifficulty(f.Header()) ... } } blockNumber = new(big.Int).Add(blockNumber, big.NewInt(1)) } // @Zuo 需要参选Fruit数目大于ElectionFruitsThreshold(100)个 // 存储在ElectionFruitsThreshold在/params/protocol_params.go内,可设置 var candidates []*candidateMember td := big.NewInt(0) for _, member := range members { if cnt, ok := fruitsCount[member.address]; ok { log.Trace("get committee candidate", "keyAddr", member.address, "count", cnt, "diff", member.difficulty) if cnt >= params.ElectionFruitsThreshold { td.Add(td, member.difficulty) candidates = append(candidates, member) } } } ... // @Zuo 根据其水果难度值计算它们在256位数值空间中的分布 dd := big.NewInt(0) rate := new(big.Int).Div(maxUint256, td) for i, member := range candidates { member.lower = new(big.Int).Mul(rate, dd) dd = new(big.Int).Add(dd, member.difficulty) if i == len(candidates)-1 { member.upper = new(big.Int).Set(maxUint256) } else { member.upper = new(big.Int).Mul(rate, dd) } log.Trace("get power", "member", member.address, "lower", member.lower, "upper", member.upper) } return crypto.Keccak256Hash(seed), candidates }
- 在上面的
getCandinates
中分配好的256位数值空间中随机选取候选人成为正式的拜占庭委员会成员。// elect is a lottery function that select committee members from candidates miners func (e *Election) elect(candidates []*candidateMember, seed common.Hash) []*types.CommitteeMember { ... for { seedNumber := new(big.Int).Add(seed.Big(), round) hash := crypto.Keccak256Hash(seedNumber.Bytes()) prop := hash.Big() // @Zuo 哈希值转大数 for _, cm := range candidates { // @Zuo 选择在某个候选区间内的候选人当选 if prop.Cmp(cm.lower) < 0 { continue } if prop.Cmp(cm.upper) >= 0 { continue } log.Trace("get member", "seed", hash, "member", cm.address, "prop", prop) if _, ok := addrs[cm.address]; ok { break } addrs[cm.address] = 1 member := &types.CommitteeMember{ Coinbase: cm.coinbase, Publickey: cm.publickey, } members = append(members, member) break } round = new(big.Int).Add(round, common.Big1) // @Zuo MaximumCommitteeNumber = 30 可配置 // MaximumCommitteeNumber在/params/protocol_params.go内 if round.Cmp(params.MaximumCommitteeNumber) > 0 { break } } ... return members }
关于白皮书和黄皮书说到的如果老委员会表现良好,为了减少选举带来的开销,初链将不会强迫必须换届,又或者加大新节点当选的难度.如果是前者,那文中未有明确说明如何判断腐败节点;如果是后者,那文中也并未明确说明如果如何加大难度,且也未能够在代码中发现,如果读者有新的发现,请留言交流.
- 获取选举人,可以看到是从慢链中选取合适的fruits。计算其贡献的水果的难度值,然后根据其难度值划分其在256位空间中的分布情况,以便后面进行选举。
-
以上就是我对初链选举逻辑的简单分析,如有不足,烦请指正。其实,通过初链代码我们可以看到,初链是一个非常有活力的项目团队,GitHub的更新速度也越来越快,如果有兴趣,大家要时常关注,更可以加入到社团中来一起互动。
总结
- 简单的总结一下初链的选举逻辑,以及内部机理。一开始从所有矿工节点中选取挖取过水果且愿意参选的节点,把他们都当做本次拜占庭委员会的候选人。(当然在这之前需要判断没有重复选举)将候选人按照各自挖取水果难度分配在256空间内,然后通过VRF可验证随机算法在256空间内随机选择新一届的委员会成员。
- 由于有了挖取水果的证明,也就是有了一定程度上的PoW逻辑,这可以防范一定程度的女巫攻击;再加上用随机算法选取的委员会,挖取水果的较低算力门槛,因此也能够在一定程度上稀释强大算力(也就是矿池)的威胁。相信再加上初链的反ASIC芯片的挖矿机制,矿池的中心化问题能够得到较好的解决。