拜占庭容錯
拜占庭將軍問題提出后,有很多的算法被提出用於解決這個問題。這類算法統稱拜占庭容錯算法(BFT: Byzantine Fault Tolerance)。簡略來說,拜占庭容錯(BFT)不是某一個具體算法,而是能夠抵抗拜占庭將軍問題導致的一系列失利的系統特點。 這意味着即使某些節點出現缺點或惡意行為,拜占庭容錯系統也能夠繼續運轉。本質上來說,拜占庭容錯方案就是少數服從多數。
拜占庭容錯系統需要達成如下兩個指標:
- 安全性:任何已經完成的請求都不會被更改,它可以在以后請求看到。在區塊鏈系統中,可以理解為,已經生成的賬本不可篡改,並且可以被節點隨時查看。
- 活性:可以接受並且執行非拜占庭客戶端的請求,不會被任何因素影響而導致非拜占庭客戶端的請求不能執行。在區塊鏈系統中,可以理解為,系統需要持續生成區塊,為用戶記賬,這主要靠挖礦的激勵機制來保證。
拜占庭系統目前普遍采用的假設條件包括:
- 拜占庭節點的行為可以是任意的,拜占庭節點之間可以共謀;
- 節點之間的錯誤是不相關的;
- 節點之間通過異步網絡連接,網絡中的消息可能丟失、亂序、延時到達;
- 服務器之間傳遞的信息,第三方可以知曉 ,但是不能竄改、偽造信息的內容和驗證信息的完整性;
在BFT共識機制中,網絡中節點的數量和身份必須是提前確定好的。且每一次節點的進出都需要對網絡進行初始化,故其無法像PoW共識機制那樣任何人都可以隨時加入/退出挖礦。另外,由於節點間基於消息傳遞達成共識,因此采用BFT算法的網絡無法承載大量的節點,業內普遍認為100個節點是BFT算法的上限。所以BFT算法無法直接用於公有鏈,而更多的應用於私有鏈和聯盟鏈。業內大名鼎鼎的聯盟鏈Hyperledger fabric v0.6采用的是PBFT,v1.0又推出PBFT的改進版本SBFT。后續又有相當多的人對其進行了改進,力求提高其擴展性。但往往都是基於對網絡環境的理想假設,以省去部分共識階段,實現更高的節點承載量。
在可信環境下共識算法一般使用傳統的分布式一致算法PAXOS或者RAFT。
BFT算法和公有鏈合適的結合點在於基於BFT的PoS共識算法(BFT based PoS)。基於BFT的PoS共識算法要點有:
- 網絡節點通過鎖定虛擬資產申請成為區塊鏈系統的驗證者(或礦工)。系統驗證者的數量是動態變化的。
- 系統從當前驗證者中隨機選擇一個人作為區塊提案人。
- 系統驗證者對區塊提案進行投票表決,投票可能要進行多輪才能達成共識。每個人的投票比重與鎖定的虛擬資產成比例
實用拜占庭容錯
PBFT(Practical Byzantine Fault Tolerance)算法由麻省理工學院的Miguel Castro 和Barbara Liskov於1999年提出,解決了原始拜占庭容錯算法效率不高的問題,將算法復雜度由指數級降低到多項式級,使得拜占庭容錯算法在實際系統應用中變得可行。
PBFT是聯盟幣的共識算法的基礎。實現了在有限個節點的情況下的拜占庭問題,有3f + 1的容錯性(拜占庭將軍問題也只在節點數N > 3f時可解),並同時保證一定的性能。其采用了密碼學相關技術(RSA 簽名算法、消息驗證編碼和摘要)確保消息傳遞過程無法被篡改和破壞。
拜占庭將軍問題在節點數N > 3f時有解的正確性證明比較復雜,此處僅舉一個簡單例子說明。
在惡意節點數f = 1,節點數N = 3f = 3時,可以看到,發令者和接令者任意角色出現叛徒都會導致其他節點無法作出決定。
為什么PBFT算法最大容錯節點數量f是(N-1)/3?
假定節點總數是N,作惡節點數為f,那么剩下的正確節點數為N - f,意味着只要收到N - f個消息且N - f > f就能做出決定,但是這N - f個消息有可能有f個是由作惡節點冒充的(或因網絡延遲導致f個惡意節點的消息先被收到),那么正確的消息就是N - f - f個,為了多數一致,正確消息必須占多數,也就是N - f - f > f ,所以N最少是3f + 1個。
為什么PBFT算法最大容錯節點數量f是(N-1)/3?
假定節點總數是N,作惡節點數為f,那么剩下的正確節點數為N - f,意味着只要收到N - f個消息且N - f > f就能做出決定,但是這N - f個消息有可能有f個是由作惡節點冒充的(或因網絡延遲導致f個惡意節點的消息先被收到),那么正確的消息就是N - f - f個,為了多數一致,正確消息必須占多數,也就是N - f - f > f ,所以N最少是3f + 1個。
PBFT算法流程
涉及角色:主節點、普通節點
在PBFT原始論文中,存在副本節點(replica)和備份節點(backup)兩種稱謂。其中,副本節點一般包括主節點在內,備份節點則不包括,而兩種稱謂又相當容易混淆,因此本文將備份節點稱為普通節點。
每個主節點的工作過程稱為一個視圖(view),用v表示視圖編號
主節點由普通節點輪流當選,具體計算過程為主節點p = v mod |R|(|R|為節點個數)
主節點的作用:
正常工作時,接收客戶端的事務請求,驗證request身份后,為該請求設置編號,廣播pre-prepare消息
新主節點當選時,根據自己收集的View-Change消息,發送View-New信息,讓其它節點同步數據
主節點與所有的其它節點維系心跳
如果主節點宕機,會因為心跳超時,而觸發重新選舉,保證系統運行穩定
如果主節點惡意發送錯誤編號的消息,那么會在后續的操作中,被副本節點察覺,因為 prepare和commit階段都是會進行廣播的,一旦不一致,觸發view-change
如果主節點不發送接收到的request,客戶端在超時未回復時,會重發request到所有的副本節點,並觸發view-change
如果主節點節點篡改消息,因為有Request里面有數據和客戶端的簽名,所以primary無法篡改消息,其它副本會先驗證消息的合法性,否則丟棄,並觸發view-change
綜上所述,限制了權限的主節點,如果宕機、或者不發生消息、或者發送錯誤編號的消息、或者篡改消息,都會被其它節點感知,並觸發view-change。
(1)Request
客戶端C向主節點p發送<REQUEST, o, t, c>
o:請求的具體操作
t:請求時客戶端追加的時間戳
c:客戶端標識。
REQUEST: 包含消息內容m,以及消息摘要d(m)。
客戶端對請求進行簽名。
(2)Pre-Prepare
主節點收到客戶端的請求,需要對客戶端請求消息簽名是否正確進行校驗。
非法請求則丟棄。正確請求則分配一個編號n,編號n主要用於對客戶端的請求進行排序。然后廣播一條<<PRE-PREPARE, v, n, d>, m>消息給其它普通節點。
v:視圖編號
d:客戶端消息摘要
m:消息內容
主節點對<PRE-PREPARE, v, n, d>進行簽名。
(3)Prepare
普通節點i收到主節點的Pre-Prepare消息,需要滿足以下條件方可接受消息:
A、請求和預准備消息的簽名正確,並且d與m的摘要一致。
B、當前視圖編號是v。
C、該普通節點從未在視圖v中接受過序號為n但是摘要d不同的消息m。
D、預准備消息的序號n在區間[h, H]內。
非法請求則丟棄。正確請求則普通節點i進入准備狀態並向所有其它節點(包括主節點)發送一條<PREPARE, v, n, d, i>消息, v, n, d, m與上述Pre-Prepare消息內容相同,i是當前副本節點編號。
普通節點i對<PREPARE, v, n, d, i>簽名。記錄Pre-Prepare和Prepare消息到日志中,用於視圖輪換過程中恢復未完成的請求操作。
Prepare階段如果發生視圖輪換會導致丟棄Prepare階段的請求。
(4)Commit
主節點和普通節點收到PREPARE消息,需要滿足以下條件方可接受消息:
A、普通節點對Prepare消息的簽名正確。
B、消息的視圖編號v與節點的當前視圖編號一致。
C、n是否在區間[h, H]內。
非法請求則丟棄。如果節點i收到了2f+1個(包括自身在內)驗證通過的Prepare消息,表明網絡中的大多數節點已經收到同意信息,則向其它節點包括主節點發送一條<COMMIT, v, n, d, i>消息,v, n, d, i與上述PREPARE消息內容相同。
節點i對<COMMIT, v, n, d, i>簽名。記錄Commit消息到日志中,用於視圖輪換過程中恢復未完成的請求操作。記錄其它副本節點發送的Prepare消息到日志中。Commit階段用來確保網絡中大多數節點都已經收到足夠多的信息來達成共識,如果Commit階段發生視圖輪換,會保存原來Commit階段的請求,不會達不成共識,也不會丟失請求編號。
(5)Reply
主節點和普通節點收到Commit消息,需要滿足以下條件方可接受消息:
A、節點對Commit消息的簽名正確。
B、消息的視圖編號v與節點的當前視圖編號一致。
C、n是否在區間[h, H]內。
非法請求則丟棄。如果副本節點i收到了2f+1個(包括自身在內)驗證通過的Commit消息,說明當前網絡中的大部分節點已經達成共識,運行客戶端的請求操作o,並返回<REPLY, v, t, c, i, r>給客戶端,
r:是請求操作結果,客戶端如果收到f+1個相同的REPLY消息,說明客戶端發起的請求已經達成全網共識,否則客戶端需要判斷是否重新發送請求給主節點。記錄其它副本節點發送的Commit消息到日志中。
圖為節點數為4,失效節點數為1情況下的共識過程,其中,C為客戶端,0為主節點,3為失效節點。
注意:可能有人會注意到圖中普通節點並未收到2f + 1個(包括自身在內)Prepare消息依然進入了Commit階段,這是由於主節點不廣播Prepare消息。查詢多方資料后未得到充分的解釋,姑且認為,默認已收到主節點的Prepare消息或將主節點的Pre-Prepare消息也算在內。
為什么節點需要收到2f+1個Prepare和Commit消息才確認?
具體的正確性證明比較麻煩,此處為便於理解只作簡單分析
2f+1作為達成共識的一個條件,需要考慮各種極端情況,例如:
若規定收到大於2f+1個消息才確認,則在總節點數為3f+1的系統中,當f個惡意節點都不發送消息時,系統將永遠無法達成共識。
若規定收到小於2f+1個消息才確認,則也無法保證達成共識或誠實節點的消息占大多數。
為什么客戶端收到f+1個確認時,交易就成功了?
因為默認問題節點為f,那么f+1個確認節點中,肯定有1個是誠實的節點,只要有1個誠實的確認消息,則交易成功,因為1個誠實的消息必須是2f+1個節點都commit操作成功了,才可能有這個1個最終確認消息的。所以為了提升交易處理的速度,只要有f+1個確認反饋,就可以表示交易成功。
垃圾回收
為了確保在視圖輪換的過程中,能夠恢復先前的請求,每一個副本節點都記錄一些消息到本地的日志中,當執行請求后副本節點需要把之前該請求的記錄消息清除掉。最簡單的做法是在Reply消息后,再執行一次當前狀態的共識同步,但成本比較高,因此可以在執行完多條請求K(例如:100條)后執行一次狀態同步。狀態同步消息就是CheckPoint消息。
節點i發送<CheckPoint, n, d, i>給其它節點,n是當前節點所保留的最后一個視圖請求編號,d是對當前狀態的一個摘要,該CheckPoint消息記錄到日志中。如果副本節點i收到了2f+1個驗證過的CheckPoint消息,則清除先前日志中的消息,並以n作為當前一個stable checkpoint(穩定檢查點)。
實際中,當節點i向其它節點發出CheckPoint消息后,其它節點還沒有完成K條請求,所以不會立即對i的請求作出響應,還會按照自己的節奏,向前行進,但此時發出的CheckPoint並未形成stable,為了防止i的處理請求過快,設置一個上文提到的高低水位區間[h, H]來解決問題。低水位h等於上一個stable checkpoint的編號,高水位H = h + L,其中L是指定的數值,等於checkpoint周期處理請求數K的整數倍,可以設置為L = 2K。當節點i處理請求超過高水位H時,此時就會停止腳步,等待stable checkpoint發生變化,再繼續前進。
視圖輪換
當普通節點感知到primary異常的時候,觸發view-change,重新選舉必須要有2f+1個節點都confirm(VIEW-CHANGE)了,發起重選才生效,一旦超過2f節點都發起VIEW-CHANGE消息,則選舉結束,p =v+1 mod |R|節點當選為new Primary。並且new primary會根據自己統計的VIEW-CHANGE的內容,生成並廣播NEW-VIEW消息,其它節點驗證之后,開始新的view
<VIEW-CHANGE, v+1, n, C, P, i>消息
v+1 :新的view編號
n是最新的stable checkpoint的編號
C是2f+1驗證過的CheckPoint消息集合
P是當前副本節點未完成的請求的PRE-PREPARE和PREPARE消息集合
新的主節點就是 newPrimary = v + 1 mod |R|。當newPrimary收到2f個有效的VIEW-CHANGE消息后,向其他節點廣播NEW-VIEW消息
<NEW-VIEW, v+1, V, O>
V是有效的VIEW-CHANGE消息集合
O是主節點重新發起的未經完成的PRE-PREPARE消息集合
未完成的PRE-PREPARE消息集合的生成邏輯:
選取V中最小的stable checkpoint編號min-s,選取V中prepare消息的最大編號max-s。
在min-s和max-s之間,如果存在P消息集合,則創建<<PRE-PREPARE, v+1, n, d>, m>消息。否則創建一個空的PRE-PREPARE消息,即:<<PRE-PREPARE, v+1, n, d(null)>, m(null)>, m(null)空消息,d(null)空消息摘要。
普通節點收到主節點的NEW-VIEW消息,驗證有效性(各個節點都統計view-change的個數),有效的話,進入v+1狀態,並且開始O中的PRE-PREPARE消息處理流程。
總結
優點:
- 節能。
- 吞吐量高。
- 分叉幾率很低。
- 節點數適當時交易延時極低。
- PBFT中的主節點並不具備很大權限,與普通節點地位相對平等,如果主節點出現問題,普通節點可以拒絕其請求並可以很容易地將其替換。
缺點:
- 節點需要選舉或許可,不像PoW可以隨意加入,去中心化程度相對較弱。
- 不能很好的存貯記錄交易信息,黑客能夠截取一些失效的副本,這可能會讓信息外漏。
- 系統節點是固定的,無法應對公有鏈的開放環境。因此只適用於節點數量少且網絡環境更可信的聯盟鏈或私有鏈。
- 安全邊界較Pow等算法相對較低。Pow對網絡內惡意算力容忍度為不超過1/2,PBFT對惡意節點數的容忍度則為1/3。
- 受節點數量限制,可擴展性差,由於每個副本節點都需要和其它節點進行P2P的共識同步,因此隨着節點的增多,性能會下降的很快。
PBFT在很多場景都有應用,在區塊鏈場景中,一般適合於對強一致性有要求的私有鏈和聯盟鏈場景,但如果能夠結合DPOS節點代表選舉規則,也可以應用於公有鏈。