(轉)對PBFT算法的理解

轉自: https://www.cnblogs.com/gexin/p/10242161.html

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。

系統模型

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

 

primary=view

 

在系統的主節點接收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

primary節點收到請求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階段

一旦節點接受〈〈PRE−PREPARE,v,n,d〉p,m〉

,則該節點進入到prepare階段,然後節點廣播prepare消息〈PREPARE,v,n,d,i〉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消息〈COMMIT,v,n,d,i〉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個消息後,就廣播〈CHECKPOINT,n,d,i〉

消息。節點收集到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: 一個Pm

組成的集合,m表示消息,m的序號是大於n的,Pm表示序號爲m的達成prepared狀態的消息集合。Pm內容包含關於m的1個pre-prepare消息和2f

    • 條prepare消息集合。
    • i: 節點ID

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

當view+1所對應的primary收到了2f個有效的view-change消息,它就會廣播<NEW-VIEW, v+1, V

, O>消息;
+ V: 是view-change消息集合
+ O: pre-prepare消息的集合, O按照如下的過程計算:
- primary根據收到的view-change消息判斷,最低的check-point min-s和最高的check-point max-s
- 對介於 min-s和max-s之間的每個序號n創建pre-prepare消息。這分兩種情況:(1) 在P集合存在一個Pm其中序號爲n; (2) 沒有這樣的集合Pm

. 對於第一種情況,創建一個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).

發佈了43 篇原創文章 · 獲贊 11 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章