1. 程式人生 > >初鏈主網上線技術解讀之-混合共識

初鏈主網上線技術解讀之-混合共識

背景

從2017年11月啟動至今,經過歷時近一年的研究、開發與測試,初鏈主網Beta版於新加坡時間2018年09月28日08:00正式上線,在此之前,07:56分PBFT委員會第一次共識出塊和TrueChain fPOW創世區塊被挖出,為了讓更多人從技術上去理解初鏈,初鏈社群釋出了初鏈技術解讀的任務,我也借這次任務開始我第一篇部落格。

說明

初鏈本次上線體現了五大亮點,包括混合共識,FPow公鏈,TrueHash抗ASIC的挖礦演算法,PBFT委員會的隨機選舉機制,高TPS。本文主要針對初鏈的混合共識進行解讀。後續一一解讀其它亮點。

名稱解釋

共識: 共識顧名思義,共同的認識,在區塊鏈世界中早期的共識演算法代表要算比特幣的pow。pow簡單來講,就是前一個區塊有一個隨機數,大家都去猜,誰先猜出來誰就有記賬權,記賬好了後,就將區塊廣播給其它節點,如果要想從原始碼層解讀,請參考文件“PoW挖礦演算法原理及其在比特幣、以太坊中的實現”。 混合共識:

pow共識安全但不高效,轉賬效率低,為了解決這個問題,後面出現了pos dpos,雖然效率提高了但是已不再那麼去中心化。為了找到效率和安全的平衡點,混合共識出現,一種共識解決記賬,一種共識解決區中心化。本文主要解讀初鏈,讓我們來看看true鏈是怎麼找到這個平衡點的。

技術架構

初鏈採用雙鏈結構,如圖所示。一條快鏈,一條慢鏈。快鏈是交易塊,裡面記錄的是很多交易。慢鏈是水果塊,裡面記錄的是很多水果,水果一次遞增,每個水果對映一個交易塊。 在這裡插入圖片描述 混合共識又是怎麼一回事呢? 在初鏈中,採用改進拜贊庭(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個安全有待進一步提升。最後,初鏈做出的努力和結果可喜可賀,為投資者和社群交了一份滿意的答卷。

參考資料