初鏈主網上線技術解讀之-混合共識
背景
從2017年11月啟動至今,經過歷時近一年的研究、開發與測試,初鏈主網Beta版於新加坡時間2018年09月28日08:00正式上線,在此之前,07:56分PBFT委員會第一次共識出塊和TrueChain fPOW創世區塊被挖出,為了讓更多人從技術上去理解初鏈,初鏈社群釋出了初鏈技術解讀的任務,我也借這次任務開始我第一篇部落格。
說明
初鏈本次上線體現了五大亮點,包括混合共識,FPow公鏈,TrueHash抗ASIC的挖礦演算法,PBFT委員會的隨機選舉機制,高TPS。本文主要針對初鏈的混合共識進行解讀。後續一一解讀其它亮點。
名稱解釋
共識: 共識顧名思義,共同的認識,在區塊鏈世界中早期的共識演算法代表要算比特幣的pow。pow簡單來講,就是前一個區塊有一個隨機數,大家都去猜,誰先猜出來誰就有記賬權,記賬好了後,就將區塊廣播給其它節點,如果要想從原始碼層解讀,請參考文件“PoW挖礦演算法原理及其在比特幣、以太坊中的實現”。 混合共識:
技術架構
初鏈採用雙鏈結構,如圖所示。一條快鏈,一條慢鏈。快鏈是交易塊,裡面記錄的是很多交易。慢鏈是水果塊,裡面記錄的是很多水果,水果一次遞增,每個水果對映一個交易塊。 混合共識又是怎麼一回事呢? 在初鏈中,採用改進拜贊庭(fbft)和工作量證明(pow)兩種共識。fbft主要解決交易效率問題,如圖所示,委員會由41個節點組成,相對於位元比節點而已,已經少之又少,當交易網路的交易提交到委員會網路以後,交易能夠得到快速的確認,自然整個網路的交易效率也就提高了額。pow主要解決去中心化問題,每個水果和每個快區塊一一對應,而每個水果又會被pow再次打包出塊,如果想要篡改交易,首先要篡改pow裡面的水果,就必須控制51%的算力。 這有點虛吧!沒問題,下面就來點實際的。
從啟動到執行bft有很深的呼叫鏈,如下可以找到node的NewNode方法 main-->>cmd:StartNode() cmd-->>node:Start() node-->>service:start() service-->>backend-Truechain:Start() truechain-->>pbft_agent:start()loop() pbft_agent-->>commitee:PutNodes() commitee-->>pbftserver:PutNodes() pbftserver-->>proxy_server:NewServer() proxy_server-->>node:NewNode()
有關pbft演算法過程詳情可以參考【連結5】,大概有5個過程1. Request 2. Pre-Prepare 3. Prepare 4. Commit 5.Reply。true鏈進行了改造。整個邏輯如圖,重點是node的resolveMsg,該方法是true鏈委員會和fbft_impl起到核心關聯作用。 相關程式碼如下
type PBFT interface {
StartConsensus(request *RequestMsg) (*PrePrepareMsg, error)
PrePrepare(prePrepareMsg *PrePrepareMsg) (*VoteMsg, error)
Prepare(prepareMsg *VoteMsg) (*VoteMsg, error)
Commit(commitMsg *VoteMsg) (*ReplyMsg, *RequestMsg, error)
}
實現類在pbft_impl.go中,這裡只展示一段,其它方法感興趣的同學可以自行檢視
func (state *State) PrePrepare(prePrepareMsg *PrePrepareMsg) (*VoteMsg, error) {
// Get ReqMsgs and save it to its logs like the primary.
state.MsgLogs.ReqMsg = prePrepareMsg.RequestMsg
// Verify if v, n(a.k.a. sequenceID), d are correct.
if !state.verifyMsg(prePrepareMsg.ViewID, prePrepareMsg.SequenceID, prePrepareMsg.Digest) {
return nil, errors.New("pre-prepare message is corrupted")
}
// Change the stage to pre-prepared.
state.CurrentStage = PrePrepared
return &VoteMsg{
ViewID: state.ViewID,
SequenceID: prePrepareMsg.SequenceID,
Digest: prePrepareMsg.Digest,
MsgType: PrepareMsg,
Height: prePrepareMsg.Height,
}, nil
}
而上文提到的node.NewNode中有這麼一段,其中resolveMsg就是處理各種fbft過程傳統msg,對msg校驗,然後發起下一階段的請求。
// Start message dispatcher
go node.dispatchMsg()
// Start alarm trigger
go node.alarmToDispatcher()
// Start message resolver
go node.resolveMsg()
//start backward message dispatcher
go node.dispatchMsgBackward()
//start Process message commit wait
go node.processCommitWaitMessage()
resolveMsg程式碼如下
func (node *Node) resolveMsg() {
for {
// Get buffered messages from the dispatcher.
msgs := <-node.MsgDelivery
switch msgs.(type) {
case []*consensus.RequestMsg:
errs := node.resolveRequestMsg(msgs.([]*consensus.RequestMsg))
if len(errs) != 0 {
for _, err := range errs {
fmt.Println(err)
}
// TODO: send err to ErrorChannel
}
case []*consensus.PrePrepareMsg:
errs := node.resolvePrePrepareMsg(msgs.([]*consensus.PrePrepareMsg))
if len(errs) != 0 {
for _, err := range errs {
fmt.Println(err)
}
// TODO: send err to ErrorChannel
}
case []*consensus.VoteMsg:
voteMsgs := msgs.([]*consensus.VoteMsg)
if len(voteMsgs) == 0 {
break
}
if voteMsgs[0].MsgType == consensus.PrepareMsg {
errs := node.resolvePrepareMsg(voteMsgs)
if len(errs) != 0 {
for _, err := range errs {
fmt.Println(err)
}
// TODO: send err to ErrorChannel
}
} else if voteMsgs[0].MsgType == consensus.CommitMsg {
errs := node.resolveCommitMsg(voteMsgs)
if len(errs) != 0 {
for _, err := range errs {
fmt.Println(err)
}
// TODO: send err to ErrorChannel
}
}
}
}
}
還有一個重要的問題必須要回答,因為現在fbft呼叫鏈找到了額,fbft過程訊息處理機制也找到了額,但是共識的是什麼?這個問題一直沒回答。其實fbft共識的是leader和fastblock。程式碼如下,中間省去部分細節。
app啟動是會開啟維護員leader選舉
pbft_agent.go
case types.CommitteeStart:
log.Info("CommitteeStart...")
self.committeeMu.Lock()
self.setCommitteeInfo(self.NextCommitteeInfo, CurrentCommittee)
self.committeeMu.Unlock()
if self.IsCommitteeMember(self.CommitteeInfo) {
go self.server.Notify(self.CommitteeInfo.Id, int(ch.Option))//傳送選舉通知 Notify為呼叫pbftserver.work
}
pbftserver.go work程式碼如下
func (ss *PbftServerMgr) work(cid *big.Int, acChan <-chan *consensus.ActionIn) {
for {
select {
case ac := <-acChan:
if ac.AC == consensus.ActionFecth {
req, err := ss.GetRequest(cid)
if err == nil && req != nil {
if server, ok := ss.servers[cid.Uint64()]; ok {
server.Height = big.NewInt(req.Height)
server.server.PutRequest(req) //發起共識
} else {
fmt.Println(err.Error())
}
} else {
lock.PSLog(err.Error())
}
} else if ac.AC == consensus.ActionBroadcast {
ss.Broadcast(ac.Height)
} else if ac.AC == consensus.ActionFinish {
return
}
}
}
選舉完成之後將自己設定為leader
func (ss *PbftServerMgr) PutCommittee(committeeInfo *types.CommitteeInfo) error {
lock.PSLog("PutCommittee", committeeInfo.Id, committeeInfo.Members)
id := committeeInfo.Id
members := committeeInfo.Members
if id == nil || len(members) <= 0 {
return errors.New("wrong params...")
}
if _, ok := ss.servers[id.Uint64()]; ok {
return errors.New("repeat ID:" + id.String())
}
leader := members[0].Publickey
infos := make([]*types.CommitteeNode, 0)
server := serverInfo{ //第一次共識完成,選出leader
leader: leader,
nodeid: common.ToHex(crypto.FromECDSAPub(ss.pk)),
info: infos,
Height: new(big.Int).Set(common.Big0),
clear: false,
}
for _, v := range members {
server.insertMember(v)
}
ss.servers[id.Uint64()] = &server
return nil
}
在fb出塊的時候有一個leader判斷動作
func (ss *PbftServerMgr) GetRequest(id *big.Int) (*consensus.RequestMsg, error) {
// get new fastblock 產出一個fastblock
server, ok := ss.servers[id.Uint64()]
if !ok {
return nil, errors.New("wrong conmmitt ID:" + id.String())
}
// the node must be leader
if !bytes.Equal(crypto.FromECDSAPub(server.leader), crypto.FromECDSAPub(ss.pk)) { //leader判斷
return nil, errors.New("local node must be leader...")
}
lock.PSLog("AGENT", "FetchFastBlock", "start")
fb, err := ss.Agent.FetchFastBlock()
lock.PSLog("AGENT", "FetchFastBlock", err == nil, "end")
if err != nil {
return nil, err
}
if fb := ss.getBlock(fb.NumberU64()); fb != nil {
return nil, errors.New("same height:" + fb.Number().String())
}
fmt.Println(len(ss.blocks))
sum := ss.getBlockLen()
if sum > 0 {
last := ss.getLastBlock()
if last != nil {
cur := last.Number()
cur.Add(cur, common.Big1)
if cur.Cmp(fb.Number()) != 0 {
return nil, errors.New("wrong fastblock,lastheight:" + cur.String() + " cur:" + fb.Number().String())
}
}
}
ss.putBlock(fb.NumberU64(), fb)
data, err := rlp.EncodeToBytes(fb)
if err != nil {
return nil, err
}
msg := hex.EncodeToString(data)
val := &consensus.RequestMsg{ //返回一個fastblock共識訊息
ClientID: server.nodeid,
Timestamp: time.Now().Unix(),
Operation: msg,
Height: fb.Number().Int64(),
}
return val, nil
}
本來想對pow在解析一下,發現有童鞋寫的非常好了額,感興趣的可以移步連結【6】
執行狀況
再來看看true鏈執行情況。 總體執行情況如下圖:圖中可以看出目前委員會為6個,FB69758個,SB982個。委員會6據瞭解是前期為了主網穩定,但6個節點那麼允許作惡容忍6*0.25=1.2個,作惡風險還是比較高,如果出現2臺機器作惡,將會產生混亂,還是希望官網引起重視。 再來看混合共識的結果 快鏈:初略看了一下大部分block的共識委員會都是6個,說明目前沒有分岔,但有一個疑問,為什麼第一個的block高度不是1? 慢鏈:初略看了一下大部分挖礦地址比較分散,說明pow效果還是很明顯真正做到了去中心化。但是大部分割槽塊沒有交易數,很多空塊會佔用大量的儲存空間,而且毫無意義,因為沒有交易。我想一種採用一種壓縮技術,對連續空塊進行壓縮;一種方式降低出塊速率讓速率和交易量掛鉤。這兩種思路都可以解決空塊暫用磁碟問題,我的一點拙見。 Snail Blocks:進入SnailBlocks發現沒有水果,不知是我理解的問題,還是瀏覽器的bug,沒有發現水果。按照邏輯應該水果能被查詢出來才算正常,水果作為Fast Block再次打包的憑證,如果沒有水果很難說pow發揮作用,希望是瀏覽器的bug。
結論
節點啟動以後會先發起一輪fbft的共識選舉,選舉leader有記賬權,記賬的表現形式為fastblock。如果想研究pow機制請參考連結【6】。這兩種共識一種保障效率,一種保障安全,在去中心化和效率選進行了一個折中的選擇。從上線的執行效果來看,兩種共識能夠完美配合,但是仍存在一些不足,比如空塊暫用磁碟可以進行優化,水果在區塊鏈瀏覽器中無法檢視,委員會只有6個安全有待進一步提升。最後,初鏈做出的努力和結果可喜可賀,為投資者和社群交了一份滿意的答卷。