關鍵字:BFT-SMaRt,共識算法,分布式網絡,Java,區塊鏈,狀態機復制,MOD-SMaRt,VP-Consensus
BFT-SMaRt 簡介
官方定義:基於拜占庭容錯的狀態機復制方案的性能改善。從github的發行歷史來看,BFT-SMaRt項目最早於2015年4月發布了beta版本。經歷了半年230個提交的迭代,當年12月BFT-SMaRt發布了1.1beta版本。然后,直到2018年10月才推出了v1.2正式版。隨后,2019年2月、3月開始較活躍的發行,5月歸於平靜,直到現在(筆者日期:2020-01-02)。通過這個時間線,可以看出,BFT-SMaRt與比特幣的發展波動還是有一定的關聯。但無論怎樣,這是一個優秀的項目值得我們學習,目前也有很多區塊鏈團隊在使用。
分布式計數器服務
下面通過一個簡單的官方實例展開對BFT-SMaRt的學習。
功能描述
這是一個分布式計數器,它有4個節點,每個節點均可接收來自不同終端的計數請求。一個計數請求包含三個參數:
- 節點id
- 步進
- 循環次數
當某一個節點接收到一個終端的計數請求以后,它會將本地的計數字段的值改為請求的值,同時,其他3個節點的計數字段也會同步該結果。
在這個4節點的網絡中,根據拜占庭容錯算法 3f+1 = n,最多允許出現1個問題節點。
組網配置
config/hosts.config文件是組網配置文件,用於配置網絡節點的訪問地址,包括節點id,ip及端口號。我們為便於學習,4個節點都部署在本機,只需要調整不同的端口號即可,4個節點共享同一份hosts.config文件。
如果節點分布在不同的機器,則每個節點均需要配置相同的hosts.config,用於描述同一個共識網絡。
#server id, address and port (the ids from 0 to n-1 are the service replicas)
0 127.0.0.1 11000 11001
1 127.0.0.1 11010 11011
2 127.0.0.1 11020 11021
3 127.0.0.1 11030 11031
默認情況下,不必修改。
啟動節點
節點的啟動是通過runscripts/smartrun.sh腳本。
java -Djava.security.properties="./config/java.security" -Dlogback.configurationFile="./config/logback.xml" -cp bin/*:lib/* $@
該腳本為java命令配置了bin/BFT-SMaRt.jar作為類執行文件,還有日志和安全的配置策略。BFT-SMaRt.jar是編譯整個BFT-SMaRt庫的jar包,所有的服務都集成在此,可在jvm虛機直接執行,熟悉Java的朋友應該很了解了,此處不再贅述。
回到項目根目錄,開啟4個連接會話,分別執行:
runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 0
runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 1
runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 2
runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 3
節點啟動成功,日志會打印出節點id、ip及端口,節點總數、容錯數,超時、SSL/TLS傳輸協議等信息。隨着節點的全部啟動,節點間會互相識別、同步,
lwb@lwbpc:~/work/bft-smart$ runscripts/smartrun.sh bftsmart.demo.counter.CounterServer
Use: java CounterServer <processId>
lwb@lwbpc:~/work/bft-smart$ runscripts/smartrun.sh bftsmart.demo.counter.CounterServer 3
-- Using view stored on disk
-- SSL/TLS handshake complete!, Id:0 ## CipherSuite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.
-- SSL/TLS handshake complete!, Id:1 ## CipherSuite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.
-- SSL/TLS handshake complete!, Id:2 ## CipherSuite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.
-- ID = 3
-- N = 4
-- F = 1
-- Port (client <-> server) = 11030
-- Port (server <-> server) = 11031
-- requestTimeout = 2000
-- maxBatch = 1024
-- Binded replica to IP address 127.0.0.1
-- SSL/TLS enabled, protocol version: TLSv1.2
-- In current view: ID:0; F:1; Processes:0(/127.0.0.1:11000),1(/127.0.0.1:11010),2(/127.0.0.1:11020),3(/127.0.0.1:11030),
-- Retrieving State
-- Replica state is up to date
--
###################################
Ready to process operations
###################################
當出現Ready to process operations時說明節點網絡組建成功,服務可用,等待終端請求。4個節點在啟動時,后啟動的節點會顯示與前面的節點的安全連接信息:SSL/TLS handshake complete!,這是建立連接,組網過程中的日志。
常見問題
在啟動id為1的節點時(也就是第二個啟動),有可能報錯:
javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
異常信息表示沒有合適的協議,cipher-suit(密碼套件,做SSL安全連接時使用)不可用。第二個節點啟動以后,根據hosts配置文件對網絡的描述,它會迅速與第一個節點建立安全連接,會使用到cipher-suit來建立SSL/TLS的加密連接。回到項目根目錄,進入config/system.config,這是BFT-SMaRt的全局配置文件。其中就包含了cipher-suit的啟用配置,可以找到:
system.ssltls.enabled_ciphers = TLS_ECDHE_ECDSA_WITH_NULL_SHA,
# With cipher (recommended).
#system.ssltls.enabled_ciphers = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
將默認配置改為推薦配置,
#system.ssltls.enabled_ciphers = TLS_ECDHE_ECDSA_WITH_NULL_SHA,
# With cipher (recommended).
system.ssltls.enabled_ciphers = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
重新啟動節點即可。
計數服務
打開第5個連接會話作為終端,進入項目根目錄,輸入:
runscripts/smartrun.sh bftsmart.demo.counter.CounterClient 1 1 2
含義為:標識自己作為客戶端的id為1向共識網絡發起請求,步進為1,執行兩次。得到的日志輸出為:
Invocation 0, returned value: 1
Invocation 1, returned value: 2
節點的日志輸出統一為:
-- Session Created, active clients=0
(1) Counter was incremented. Current value = 1
(2) Counter was incremented. Current value = 2
當前值為2,我們再次發起一個請求:
runscripts/smartrun.sh bftsmart.demo.counter.CounterClient 1 2 3
含義為:標識自己作為客戶端的id為1向共識網絡發起請求,步進為2,執行3次。可推測出從2加3個2,最終結果為8,實際上日志的顯示也是符合我們的推測的:
-- Session Created, active clients=0
(3) Counter was incremented. Current value = 4
(4) Counter was incremented. Current value = 6
(5) Counter was incremented. Current value = 8
BUG:clients有編號,第一個默認為0,當在本地啟動多個會話作為client發起請求時,會發生第二個client執行中止的情況,例如第一個client執行完請求以后並未退出線程,而此時第二個client發起一個執行次數為3的請求,那么該客戶端日志會卡在Invocation 0,而節點日志會成功執行1次,剩余2次未發生。此時退出一個client,仍舊可以正常服務。
解決措施:該BUG是由於理解出錯導致的,客戶端請求的第一個參數是用來標識自己的。因此當兩個客戶端均以相同的id發起請求時,對於共識網絡的節點,只能處理一個。而正確的方法應該是不同的客戶端采用不同的id作為自己的標識發起訪問。
容錯服務
我們來模擬一個節點斷線,然后再重新啟動。通過日志分析,該節點再次接入網絡以后,會主動請求其他節點,獲得之前的所有操作,然后本地依次全部執行,最終得到與其他節點相同的狀態值。
-- Retrieving State
-- Requesting state from other replicas
-- I just sent a request to the other replicas for the state up to CID 7
-- Received state. Will install it
-- Successfully installed proof for consensus 7
-- Last CID in state: 7
(1) Counter was incremented. Current value = 1
(2) Counter was incremented. Current value = 2
(3) Counter was incremented. Current value = 4
(4) Counter was incremented. Current value = 6
(5) Counter was incremented. Current value = 8
(6) Counter was incremented. Current value = 10
(7) Counter was incremented. Current value = 12
(8) Counter was incremented. Current value = 14
-- Setting last CID to 7
-- Current decided size: 0
-- All finished up to 7
--
###################
Ready to process operations
###################
-- I updated the state!
通過這個日志,可以非常清楚地看到狀態機復制的過程。
BFT-SMaRt 理論
BFT-SMaRt 是一個使用Java編程語言實現的分布式共識引擎,提升了模塊化程度,更加可靠,同時提供了靈活的編程接口。前綴BFT我們熟知,那么SMaRt是什么?可以看到大寫的部分SMR,其實是State Machine Replication,即狀態機復制的意思。那么BFT-SMaRt認為它是BFT-SMR的改善方案,因此湊上了小寫字母a和t組成了smart。所以BFT-SMaRt可以解釋為:通過BFT實現SMR的一套改善解決方案。
區塊鏈的分布式網絡需要解決的拜占庭問題,在此就不多介紹了。在此之前,PBFT、POW、POS以及DPOS,這些拜占庭容錯類的算法由於研究人員的學術或者性能需求,大多是由Go語言或C++寫成。這對於擁有廣大群眾基礎的Java從業者是不友好的,也是區塊鏈大規模商業化的阻力。因此,BFT-SMaRt最大的優勢或者特色就是使用了Java語言實現,同時可靠、模塊化、接口靈活。
本章依據論文 From Byzantine Consensus to BFT State Machine Replication: A Latency-Optimal Transformation
BFT-SMR 典型模式
在BFT-SMaRt出現之前,大部分論文所提出的BFT實現SMR的典型模式,往往需要更多的步驟。我們知道PBFT本身是需要5步的,除去客戶端發起請求REQUEST與節點回復客戶端REPLY,中間還有三階段共識。PBFT是用來在聯盟鏈環境中達成正確共識的,一般來講都會為交易排序,達成統一順序的共識,但並沒有狀態校驗。理論上來講,當各個節點的交易順序是正確一致且完整的,那么執行的結果即狀態值就應該是一致的。但是當系統脫離舒適區,來到了更加不可信的環境里,需要具備狀態的檢驗作為double-check。而狀態機復制的概念,是一個相較區塊鏈更久遠的概念。集群、分布式都會有負載以及同步的需求,多點承接請求,單點執行,多點再同步結果數據,這是一個耳熟能詳的過程。那么BFT-SMR所研究的內容恰恰是以上的所有,因此它會需要更多的步驟,如下圖所示。

基於可靠的廣播信道,首先在SMR過程發生了三個步驟:消息從客戶端發出並廣播到各個節點,節點間回應,節點同步完成。然后再執行與PBFT相同的3PC共識,最后再加上一個消息回復REPLY。總共七個步驟。為了優化通信的步驟,BFT-SMaRt提出了MOD-SMaRt模塊化的概念,而實現BFT的共識被稱作Validated and Provable Consensus (VP-Consensus),即已驗證可證明的共識。
VP-Consensus改善了現有的基於領導者驅動的共識算法。
SMR 狀態機復制
在這個模型中,任意數量的客戶端進程向一組副本進程(分布式集群)發出命令。這些副本實現一個有狀態的服務,例如一個賬戶的余額。該服務在處理客戶端命令后更改其狀態,並向發出這些命令的客戶端發送響應。該技術的目標是使每個副本上的狀態以保持一致的方式發展,從而使服務在每個副本上完整及准確地被復制。為了實現這種行為,需要滿足四個特性:
- 如果有兩個正確的副本r和r‘將操作o應用於狀態s,r和r’均獲得一個相同的狀態s‘。
- 任意兩個正確的副本r和r'均從狀態s0開始;
- 任意兩個正確的副本r和r’均執行相同的操作序列o0,…,oi;
- 來自正確客戶端的操作總是被執行。
前兩個要求可以在不使用任何分布式協議的情況下實現,但是接下來的兩個要求可以直接轉化為一個全順序廣播協議的實現——這相當於解決了共識問題。MOD-SMaRt滿足性質3和4,再加入一個VP-Consensus共識,被復制的服務將符合性質1和2。
VP-Consensus
VP-Consensus解釋為已驗證可證明共識。已驗證,指的是協議接收一個操作γ修改的一個值,那么任何其他節點的值必須滿足修改,經受住驗證。可證明,即零知識證明,意味着協議生成一個加密證據Γ,可證明一個已采納的值v是正確的。VP-Consensus實現提供以下接口:
- VP-Propose(我,l,γ,v):在共識實例i中,提出了一個值v,,以及初始的leader(領導者)l和操作γ;
- VP-Decide(我,v,Γ):當值v在共識實例i中被采納時觸發;
- VP-Timeout(i, l):用於在共識實例i中觸發超時,並指定一個新的leader進程l。
關於這個接口,有三件重要的事情需要注意:
- VP-Consensus采用leader驅動的協議,類似於任何拜占庭Paxos共識。
- 該接口假設VP-Consensus實現可以處理超時來更改領導者(就像Raft那樣),並且在超時之后本地選擇新的領導者。
- 我們隱式地假定所有正確的流程將為該共識實例i調用VP-Propose,執行相同的操作γ。
VPConsensus具備以下特性:
- 終止性:每一個正確的操作最終都會被有效執行;
- 完整性:正確的操作不會執行兩次;
- 一致性:執行兩次相同的操作,均會得到一致的結果。
此外,還需要另外兩個特性:
- 外部有效性:如果在系統內一個正確的操作y修改了v,那么外部執行γ(v)一定也是正確的;
- 外部可證性:如果一些正確的操作修改了v,同時加密證據Γ在共識實例i中,那么所有外部的正確操作均可以使用Γ來驗證:值v是我的正確修改。
MOD-SMaRt 模塊化
上面介紹了VP-Consensus基本上與PBFT差不多,而MOD-SMaRt則是BFT-SMaRt較為核心的部分。該協議又分為三個子算法:
- 客戶端操作,發起請求
- 正常階段(執行請求)
- 同步階段

MOD-SMaRt模塊化首先的前提是建立在可靠已認證的點對點信道,外加VP-Consensus共識實現。MOD-SMaRt使用VP-Consensus在共識實例i中(建立交付消息)執行一個正確的操作序列,這個操作序列也會在該共識實例中的其他副本中正確執行。MOD-SMaRt的一個副本架構如下圖。

在MOD-SMaRt第二個階段——正常階段中,一個正確的客戶端發送請求到所有副本,一個共識實例會建立整個執行順序,執行操作,然后回復給客戶端。如下圖所示:

接着,是第三個階段——同步階段。當第二次觸發消息的超時時,將啟動此階段,啟動時同步運行VP-Consensus,如下圖所示,虛線部分對應VP-Consensus協議的消息。

通過這個圖,可以清晰地觀察到MOD-SMaRt的每一個具體的步驟。
- 初始leader為節點R0,客戶端可以向任意一個節點定向發送消息,收到消息的節點會為消息排序,非leader一定會處理超時,那么它就會把原消息轉發給其他節點。其他節點第一次接受到該消息也會重復以上操作。可以確保leader節點收到原消息並排序。
- STOP:如果同一個請求有第二個超時,說明leader排序失敗,則更換leader。該節點會暫停本地所有倒計時器,然后啟動同步階段,leader默認更換為下一個節點,即R1,並廣播STOP消息。某節點統計本地收到STOP消息的節點數量達到確認數,即至少n/2+1(CFT),2/3n+1(BFT)則本地確定leader更換成功,並向新leader發送STOP-DATA消息。
- STOP-DATA:新leader接收該消息達到確認數,就向其他節點發送SYNC消息,包含原消息決策。
- SYNC:節點接收到SYNC消息后,會將原消息執行相同的計算,從而校驗leader的信息是否正確。然后向外廣播READ消息。
- READ:節點本地收集READ消息到確認數,則發送COLLECT消息給leader。
- COLLECT:leader接收COLLECT消息到確認數,則發送PROPOSE消息,代表該原消息狀態已同步。
- PROPOSE:節點接收leader發來的PROPOSE消息,完成本地對於該消息的處理。
n > 3f +1,設正確消息為t,則n > 3(n-t) +1,所以t>(2n+1)/3,由於節點必須為整數,因此最少為2/3n+1個正確消息。
如果原leader沒有執行異常,則不需要更換,只需要READ-COLLECT-PROPOSE三步即可完成對於消息的共識處理。
優化措施
對於MOD-SMaRt的實現,有兩種優化的措施。
日志
第一個重要的優化措施與操作日志的大小有關。在MOD-SMART中,日志是可以無限增長的,這使得它不適用於實際系統。為了避免這種情況,我們建議使用檢查點checkpoint和狀態轉移state transfer。
①檢查點
檢查點將在每個副本中定期執行:在交付了一定數量的決策之后,副本將從應用程序請求狀態,將其保存在內存或磁盤中,然后清除日志。
②狀態轉移
用於副本發現自身日志中最新決策與其他日志不一致時所使用(網絡不暢或系統異常)。首先主動向其他副本請求他們最新檢查點日志中保存的狀態。然后等待接收到來自不同副本的 f + 1 (正確的比錯誤的多一個即可)個狀態共識后,強制自己同步新狀態,並恢復運行。
計算開銷
第二個優化目標是避免在協議關鍵路徑中生成和驗證數字簽名的計算開銷。客戶端請求和VP-Consensus證明(以滿足外部可證明性)能夠使用MAC(消息認證碼)代替數字簽名,就像在PBFT中做的那樣。然而,這種情況下的客戶端請求會導致一個不太健壯的狀態機實現,容易受到某些性能下降攻擊(例如DDOS)。如果我們使用基於BFT的VP-Consensus,並用到這種優化,MOD-SMaRt將匹配PBFT的消息模式,同時有正確的領導者同步執行。因此需要相同數量的通信步驟和密碼操作。這也正是在BFT-SMaRt所做的:一種使用了BFT優化MOD-SMART的實現。
結論
盡管存在一些優化改善BFT-SMR的工作,但它們都沒有將協議封裝在一個共識算法中,只能是分片協作的。另一方面,所有已發布的BFT實現的整個操作順序的廣播,比實際的BFT-SMR需要更多的通信步驟。我們通過提供MOD-SMaRt(一種低延遲和彈性最優的BFT-SMR)來彌補這一缺陷,該算法使用定義良好的共識算法實現模塊化。為了實現這種最優性,我們引入了經過驗證和可證明的共識抽象,它可以通過對現有的共識協議進行簡單修改來實現。
后記
后續文章將升級難度,從源碼角度來分析分布式計數器服務的實現方法,以及BFT-SMaRt自身的包括可靠信道、共識達成、線程分配、系統架構等內容。
