PBFT演算法實現過程
轉載:https://www.jianshu.com/p/2383c7841d41
PBFT演算法
三個階段:預準備(pre-prepare)、準備(prepare)、和確認(commit)
步驟:
- 從全網節點選舉出一個主節點(Leader),新區塊由主節點負責生成
- Pre-Prepare:每個節點把客戶端發來的交易向全網廣播,主節點0將從網路收集到需放在新區塊內的多個交易排序後存入列表,並將該列表向全網廣播,擴散至123
- Prepare:每個節點接收到交易列表後,根據排序模擬執行這些交易。所有交易執行完後,基於交易結果計算新區塊的雜湊摘要,並向全網廣播,1->023,2->013,3因為宕機無法廣播
- Commit:如果一個節點收到的2f(f為可容忍的拜占庭節點數)個其它節點發來的摘要都和自己相等,就向全網廣播一條commit訊息
- Reply:如果一個節點收到2f+1條commit訊息,即可提交新區塊及其交易到本地的區塊鏈和狀態資料庫。
拜占庭容錯能夠容納將近1/3的錯誤節點誤差,IBM建立的Hyperledger 0.6版本就是使用了該演算法作為共識演算法(1.0版本已棄用,使用kafka)。
這個機制下有一個叫檢視view的概念,在一個視圖裡,一個是主節點,其餘的都叫備份節點。主節點負責將來自客戶端的請求給排好序,然後按序傳送給備份節點們。但是主節點可能會是拜占庭的:它可能會給不同的請求編上相同的序號,或者不去分配序號,或者讓相鄰的序號不連續。備份節點應當有職責來主動檢查這些序號的合法性,並能通過timeout機制檢測到主節點是否已經宕掉。當出現這些異常情況時,這些備份節點就會觸發檢視更換view change協議來選舉出新的主節點。
檢視是連續編號的整數。主節點由公式p = v mod |R|計算得到,這裡v是檢視編號,p是副本編號,|R|是副本集合的個數。當主節點失效的時候就需要啟動檢視更換(view change)過程。
預準備階段
在預準備階段,主節點分配一個序列號n給收到的請求,然後向所有備份節點群發預準備訊息,預準備訊息的格式為<<PRE-PREPARE,v,n,d>,m>
,這裡v是檢視編號,m是客戶端傳送的請求訊息,d是請求訊息m的摘要。
請求本身是不包含在預準備的訊息裡面的,這樣就能使預準備訊息足夠小,因為預準備訊息的目的是作為一種證明,確定該請求是在檢視v中被賦予了序號n,從而在檢視變更的過程中可以追索。另外一個層面,將“請求排序協議”和“請求傳輸協議”進行解耦,有利於對訊息傳輸的效率進行深度優化。
備份節點對預準備訊息的態度:
只有滿足以下條件,各個備份節點才會接受一個預準備訊息:
- 請求和預準備訊息的簽名正確,並且d與m的摘要一致。
- 當前檢視編號是v。
- 該備份節點從未在檢視v中接受過序號為n但是摘要d不同的訊息m。
- 預準備訊息的序號n必須在水線上下限h和H之間。
水線存在的意義在於防止一個失效節點使用一個很大的序號消耗序號空間。
進入準備階段:
如果備份節點i接受了預準備訊息<<PRE-PREPARE,v,n,d>,m>
,則進入準備階段。在準備階段的同時,該節點向所有副本節點發送準備訊息<PREPARE,v,n,d,i>
,並且將預準備訊息和準備訊息寫入自己的訊息日誌。如果看預準備訊息不順眼,就什麼都不做。
接受準備訊息需要滿足的條件:
包括主節點在內的所有副本節點在收到準備訊息之後,對訊息的簽名是否正確,檢視編號是否一致,以及訊息序號是否滿足水線限制這三個條件進行驗證,如果驗證通過則把這個準備訊息寫入訊息日誌中。
準備階段完成的標誌:
我們定義準備階段完成的標誌為副本節點i將(m,v,n,i)記入其訊息日誌,其中m是請求內容,預準備訊息m在檢視v中的編號n,以及2f個從不同副本節點收到的與預準備訊息一致的準備訊息。每個副本節點驗證預準備和準備訊息的一致性主要檢查:檢視編號v、訊息序號n和摘要d。
預準備階段和準備階段確保所有正常節點對同一個檢視中的請求序號達成一致。。
進入確認階段
當(m,v,n,i)
條件為真的時候,副本i將<COMMIT,v,n,D(m),i>
向其他副本節點廣播,於是就進入了確認階段。每個副本接受確認訊息的條件是:
- 簽名正確;
- 訊息的檢視編號與節點的當前檢視編號一致;
- 訊息的序號n滿足水線條件,在h和H之間。
一旦確認訊息的接受條件滿足了,則該副本節點將確認訊息寫入訊息日誌中。(補充:需要將針對某個請求的所有接受的訊息寫入日誌,這個日誌可以是在記憶體中的)。
接受確認訊息需要滿足的條件
我們定義確認完成committed(m,v,n)
為真得條件為:任意f+1個正常副本節點集合中的所有副本i其prepared(m,v,n,i)
為真;本地確認完成committed-local(m,v,n,i)
為真的條件為:prepared(m,v,n,i)
為真,並且i已經接受了2f+1個確認(包括自身在內)與預準備訊息一致。確認與預準備訊息一致的條件是具有相同的檢視編號、訊息序號和訊息摘要。
確認被接受的形式化描述
確認階段保證了以下這個不變式:對某個正常節點i來說,如果committed-local(m,v,n,i)
為真則committed(m,v,n)
也為真。這個不變式和檢視變更協議保證了所有正常節點對本地確認的請求的序號達成一致,即使這些請求在每個節點的確認處於不同的檢視。更進一步地講,這個不變式保證了任何正常節點的本地確認最終會確認f+1個更多的正常副本。
故事的終結
每個副本節點i在committed-local(m,v,n,i)
為真之後執行m的請求,並且i的狀態反映了所有編號小於n的請求依次順序執行。這就確保了所有正常節點以同樣的順序執行所有請求,這樣就保證了演算法的正確性。在完成請求的操作之後,每個副本節點都向客戶端傳送回覆。副本節點會把時間戳比已回覆時間戳更小的請求丟棄,以保證請求只會被執行一次。
使用計時器的超時機制觸發檢視變更事件
檢視變更將在主節點失效的時候仍然保證系統的活性。檢視變更可以由超時觸發,以防止備份節點無期限地等待請求的執行。備份節點在接收到一個有效請求,但是還沒有執行它時,會檢視計時器是否在執行,如果沒有,那麼它將啟動計時器;當請求被執行時就把計時器停止。如果計時器超時,將會把檢視變更的訊息向全網廣播。
各個節點會收集檢視變更資訊,併發送確認給 view v+1 中的主節點。新的主節點收集了檢視變更和檢視變更確認訊息(包含自己的資訊),然後選出一個checkpoint作為新view處理請求的起始狀態。它會從checkpoint的集合中選出編號最大(假設編號為h)的checkpoint。接下來,主節點會從h開始依次選取h到h+L(L是高低水位之差)之間的編號n對應的請求在新的view中進行pre-prepare,如果一條請求在上一個view中到達了committed狀態,主節點就選取這個請求開始在新的view中進行第三階段。但是如果選取的請求在上一view中並沒有被prepare,那它的編號n有可能是不被同意的,我們選擇在新的view中作廢這樣的請求。