1. 程式人生 > >對PBFT演算法的理解

對PBFT演算法的理解

PBFT論文斷斷續續讀了幾遍,每次讀或多或少都會有新的理解,結合最近的專案程式碼,對於共識的原理有了更清晰的認識。雖然之前寫過一篇整理PBFT論文的部落格,但是當時只是知道了怎麼做,卻不理解為什麼。現在整理下思路,寫一篇關於PBFT的理解。

1. 前提假定

1.1 同步模型

在分散式系統中談論共識,首先需要明確系統同步模型是synchrony,asynchrony還是partial synchrony?

  • synchrony: 節點所發出的訊息,在一個確定的時間內,肯定會到達目標節點;
  • asynchrony: 節點所發出的訊息,不能確定一定會到達目標節點;
  • partial synchrony
    : 節點發出的訊息,雖然會有延遲,但是最終會到達目標節點。

synchrony是十分理想的情況,如果假設分散式系統是一個同步系統,那麼共識演算法的設計可以簡化很多,在同步系統中只要超時沒收到訊息就可以認為節點除了問題。asynchrony是更為貼近實際的模型,但是根據FLP Impossibility原理,在asynchrony假定下,共識演算法不可能同時滿足safetyliveness。為了設計能夠符合實際場景的共識演算法,目前的BFT類共識演算法多是基於partial synchrony假定,這在PBFT論文中被稱為"weak synchrony"。

PBFT假設系統是非同步的,節點通過網路連線,訊息會被延遲,但是不會被無限延遲。

1.2 容錯型別

PBFT假定錯誤可以是拜占庭型別的,也就是說可以使任意型別的錯誤,比如節點作惡、說謊等。這有別於crash-down型別的錯誤,raft、paxos這類共識演算法只能允許crash-down型別錯誤,節點只能crash而不能產生假訊息。

錯誤型別 總節點數
Byzantine fault \(3f+1\)
Crash-down fault \(2f+1\)

對於拜占庭類錯誤,總節點數為n,假設系統可能存在f個拜占庭節點,假如需要根據節點發送過來的訊息做判斷。為了共識正常進行,在收到n-f個訊息時,就應該進行處理,因為可能有f個節點根本不傳送訊息。現在我們根據收到的n-f個訊息做判斷,判斷的原則至少f+1個相同結果。但是,在收到的n-f個訊息中,不能確定其中沒有錯誤節點過來的訊息,其中也可能存在f個假訊息。應該保證n-f-f > f,即n>3f。

系統模型

一組節點構成狀態機複製系統,一個節點作為主節點(privary),其他節點作為備份節點(back-ups)。某個節點作為主節點時,這稱為系統的一個view。當節點出了問題,就進行view更新,切換到下一個節點擔任主節點。主節點更替不需要選舉過程,而是採用round-robin方式。

\[ privary = view % N \]

在系統的主節點接收client發來的請求,併產生pre-prepare訊息,進入共識流程。

我們需要系統滿足如下兩個條件
deterministic: 在一個給定狀態上的操作,產生一樣的執行結果
+ 每個節點都有一樣的起始狀態

要保證non-fault節點對於執行請求的全域性順序達成一致。

1.3 safety & liveness

  • safety: 壞的事情不會發生,即共識系統不能產生錯誤的結果,比如一部分節點說yes,另一部分說no。在區塊鏈的語義下,指的是不會分叉。
  • liveness: 好的事情一定會發生,即系統一直有迴應,在區塊鏈的語義下,指的是共識會持續進行,不會卡住。假如一個區塊鏈系統的共識卡在了某個高度,那麼新的交易是沒有迴應的,也就是不滿足liveness。

2. Normal process

正常狀態下的共識流程可以用論文中的配圖清晰表示,如下所示。

共識過程由三個階段構成,pre-prepare階段和prepare階段確保了在同一個view下,正常節點對於訊息m達成了全域性一致的順序,用\(Order<v, m, n>\)表示,在view = v下,正常節點都會對訊息m,確認一個序號n。接下來的commit投票,再配合上viewchange的設計,實現了即使view切換,也可以保證對於m的全域性一致順序,即\(Order<v+1, m, n>\),檢視切換到v+1, 依然會對訊息m,確認序號n。

pre-prepare

privary節點收到請求m時,會做兩件事,首先需要講這個請求m廣播給其他節點;然後是給請求m分配一個序號n,並廣播給其他節點。廣播之後會將訊息儲存在本地log中。

pre-prepare階段的訊息格式\(<<PRE-PREPARE, v, n, d>_p, m>\),其中v表示當前view編號,n表示給m分配的序號,d為m的雜湊,以及m的原文。

其他節點收到pre-prepare訊息時,會依次做如下幾步操作:

  1. 簽名驗證
  2. 訊息是本本節點所在view的訊息
  3. 本節點在v檢視下,還沒有收到序號n的其他訊息
  4. 收到的訊息序號n,在當前接收視窗內(h, H)
  5. 以上幾部都通過,則接受該訊息,並廣播prepare訊息進入prepare階段

一旦節點接受\(\langle \langle PRE-PREPARE, v, n, d \rangle_p, m \rangle\),則該節點進入到prepare階段,然後節點廣播prepare訊息\(\langle PREPARE, v, n, d, i \rangle_i\)。之後,節點將訊息加入到本地的log中。

prepare

節點收到prepare訊息時,會驗籤並檢查是否是當前view的訊息,同時檢查訊息序號n在當前的接收視窗內,驗證通過則接受該訊息,儲存到本地log中。

當節點達成以下3點時,則表明節點達成了prepared狀態,記為prepared(m,v,n,i)

  1. 在log中存在訊息m
  2. 在log中存在m的pre-prepare訊息,pre-prepare(m,v,n)
  3. 在log中存在2f個來自其他節點的prepare訊息,prepare(m,v,n,i)

至此,可以確保在view不發生切換的情況下,對於訊息m有全域性一致的順序。

也就是說,在view不變的情況的下:

  • (1) 一個正常節點i,不能對兩個及以上的不同訊息,達成相同序號n的prepared狀態。即不能同時存在prepared(m,v,n,i)和prepared(m',v,n,i)
  • (2) 兩個正常節點i、j,必須對相同的訊息m,達成相同序號n的prepared狀態。prepared(m,v,n,i) && prepared(m,v,n,j)
簡要的證明:
(1) 假如正常節點i, 對於訊息m達成了prepared(m,v,n,i),同時存在一個m',也達成了prepared(m',v,n,i)。

首先對於prepared(m,v,n,i),肯定有2m+1個節點發出了<prepare,m,v,n>訊息。
對於prepared(m',v,n,i),肯定也有2m+1個節點發出了<prepare,m',v,n>。

2*(2f+1) - (3f+1) = f+1

所以至少有f+1個節點,既發出了<prepare,m,v,n>,又發出了<prepare,m',v,n>,這明顯是拜占庭行為。也就是說,至少有f+1個拜占庭節點,而這與容錯條件相矛盾。

(2) 假如兩個正常節點i、j,分別對不同的訊息m、m',達成序號n的prepared狀態,prepared(m,v,n,i)和prepared(m',v,n,j)

首先對於prepared(m,v,n,i),肯定有2m+1個節點發出了<prepare,m,v,n>訊息。
對於prepared(m',v,n,j),肯定也有2m+1個節點發出了<prepare,m',v,n>。
2*(2f+1) - (3f+1) = f+1

所以至少有f+1個節點,既發出了<prepare,m,v,n>,又發出了<prepare,m',v,n>,這明顯是拜占庭行為。也就是說,至少有f+1個拜占庭節點,而這與容錯條件相矛盾。

prepared狀態是十分重要的,當涉及到view轉換時,為了保證view切換前後的safety特性,需要將上一輪view的資訊傳遞到新的view,而在pbft中就是將prepared狀態資訊傳遞到新的view。可以這麼理解,新的view中需要在上一輪view的prepared資訊基礎上,繼續進行共識。

在tendermin共識演算法中,同樣是採用與pbft類似的三個階段(兩輪投票),但是在round切換時,並沒有傳遞prepared狀態資訊。為了保證safety特性,tendermint中新的輪次中,根據本地節點是否有鎖定的資訊來進行,而鎖定的資訊就是prepared狀態。所以,tendermint也是在本節點上一輪prepared資訊的基礎上繼續進行共識。

達成prepared狀態以後,節點會廣播commit訊息\(\langle COMMIT, v, n, d, i \rangle_i\).

commit

節點接收commit訊息後,會像收到prepare訊息一樣進行幾步驗證已確定是否接受該訊息。

當節點i,達成了prepared(m,v,n,i)狀態,並且收到了\(2f+1\)個commit(v,n,d,i)訊息,則該節點達成了commit-local(m,v,n,i)狀態。

達成commit-local之後,節點對於訊息m就有了一個全域性一致的順序,可以執行該訊息並 reply to 客戶端了。

commit-local狀態說明有2f+1個節點達成了prepared狀態.

3. garbage collect

由於實際的訊息log不可能無限大,因此需要設定checkpoint,以實現過時訊息的清除。

直觀的做法就是,每隔一段時間,在序號(n%100 == 0)時,確認每個節點都已經執行完第n個訊息了。這樣就可以清除掉比n還要早的訊息了。

在pbft論文中,這也是通過投票實現的,當一個節點執行完第n個訊息後,就廣播\(\langle CHECKPOINT, n, d, i \rangle\) 訊息。節點收集到\(2f+1\) checkpoint訊息後,就產生一個本地的checkpoint,然後清除掉比n小的訊息。然後將接收訊息的視窗調整為(n, n+100).

4. viewchange

個人認為,viewchange是pbft中最為關鍵的設計,viewchange的設計保證了共識系統的safety和liveness特性。

當節點檢測到超時時,會發送viewchange訊息,進入viewchange流程,viewchange訊息包含如下內容:

  • <VIEWCHANGE, n, C, P, i>
    • n: 訊息序號,本節點最近的一個check-point所確定的序號
    • C: 對應於n的check-point 2f+1個CHECKPOINT訊息集合
    • P: 一個\(P_m\)組成的集合,m表示訊息,m的序號是大於n的,\(P_m\)表示序號為m的達成prepared狀態的訊息集合。\(P_m\)內容包含關於m的\(1\)個pre-prepare訊息和\(2f\)條prepare訊息集合。
    • i: 節點ID

由訊息結構可以看出,當節點發出viewchange訊息時,節點將本地的prepared狀態資訊打包到了訊息中,傳遞給後續的view。

當view+1所對應的privary收到了2f個有效的view-change訊息,它就會廣播<NEW-VIEW, v+1, \(V\), \(O\)>訊息;
+ \(V\): 是view-change訊息集合
+ \(O\): pre-prepare訊息的集合, \(O\)按照如下的過程計算:
- privary根據收到的view-change訊息判斷,最低的check-point min-s和最高的check-point max-s
- 對介於 min-s和max-s之間的每個序號n建立pre-prepare訊息。這分兩種情況:(1) 在P集合存在一個\(P_m\)其中序號為n; (2) 沒有這樣的集合\(P_m\). 對於第一種情況,建立一個pre-prepare訊息,<PRE-PREPARE, v+1, n, d>。對於第二種情況,建立新的<PRE-PREPARE, v+1, n, d_null>。

可以這樣理解,在新的view中,節點是在上一輪view中各個節點的prepared狀態基礎上進行共識流程的。

發生view轉換時,需要的保證的是:如果檢視轉換之前的訊息m被分配了序號n, 並且達到了prepared狀態,那麼在檢視轉換之後,該訊息也必須被分配序號n(safety特性)。因為達到prepared狀態以後,就有可能存在某個節點commit-local。要保證對於m的commit-local,在檢視轉換之後,其他節點的commit-local依然是一樣的序號。

5. 思考

  • 經過兩輪投票的BFT共識協議,比如PBFT、tendermint等,輪次切換時,都是在previous輪次中的第一輪投票結果基礎上繼續共識流程。
  • BFT類共識需要保證safty和liveness,safety可以在asynchrony假設下達成,liveness需要弱同步假設
  • pbft的核心設計是viewchange,巧妙的在viewchange訊息新增prepared資訊,實現將previous檢視資訊傳遞到下一輪。但是,這樣存在的問題是,訊息太大了,有些冗餘。

6. Reference

[1] Castro, Miguel, and Barbara Liskov. "Practical Byzantine fault tolerance." OSDI. Vol. 99. 1999.

[2] Kwon, Jae. "Tendermint: Consensus without mining." Draft v. 0.6, fall (2014).