PBFT概念與Go語言入門(Tendermint基礎)
Tendermint作為當前最知名且實用的PBFT框架,網上資料並不很多,而實現Tendermint的Go語言,由於相對小眾,也存在資料匱乏和模糊錯漏的問題。本文簡單介紹PBFT概念和Go語言[&開發環境]關鍵知識點,其中大部分都可單獨成篇,限於篇幅,文中提供諸多連結供大家深入。日後可能會基於Tendermint出系列博文,此篇純當基礎。
概念
下述一部分在前篇區塊鏈初探中亦有涉及,可結合著看。
分散式系統中的非同步和共識
非同步:這裡的非同步不同於通常技術術語中的非同步呼叫的非同步,而是指在一個分散式系統中,對訊息的處理速度或者訊息送達時間不做任何假設。
共識:互相獨立的多個系統參與方(程序或者節點)間對某個問題達成一致的結果。
FLP Impossibility:在非同步的場景下,即便沒有拜占庭故障,即便訊息系統足夠可靠(所有的訊息都可以被正確的送達剛好一次),只要有至少一個程序失效(比如掉線等無法響應的情形),也沒有任何演算法能保證非失效程序達到一致性(共識)。
個人認為理解FLP的重點是:一個程序無法探測未響應程序的當前狀態(失效or網路延遲導致),導致該程序無法確認自己是否應該[不管未響應程序]作出決策或者[不管未響應程序]作出的決策是否正確,從而導致整個系統狀態的不確定性。
PBFT:實用拜占庭容錯演算法。現實生活中,完全非同步的場景較少,或者我們可以設定超時等規則,繞過FLP的限制,稱之為實用的一致性演算法。比如PBFT,它的核心三階段協議,依靠的是弱同步的網路環境(如因特網,雖有延遲但總體可控),具體流程可看
基於PBFT的區塊鏈共識框架,最知名的當屬Tendermint。網上資料常拿他與以太坊的Casper協議比較優劣,Casper是基於鏈的共識演算法,且內部實現了較為嚴厲的懲罰措施制約惡意節點。達成共識的手段上,Tendermint是基於輪次的投票機制,而Casper是基於賭博的投注機制。目前我對Casper節點具體的投注規則和流程仍然一頭霧水,有興趣的朋友可參看 乾貨 | 理解 Serenity,Part-2:Casper。
Tendermint的資料採用Amino編碼,Amino繼承自Protobuf3,Protobuf3主要是使用變長編碼Varint編碼數值,以減少儲存空間大小和傳輸量,可參看
CAP理論:這玩意兒貌似很有名,然而我剛看到就有點糊塗,總感覺CP兩個貌似就湊不到一起,後來發現果然有些質疑之聲,參看 CAP理論 。說到底,CAP理論需要基於具體場景討論,幾個字母的意思在不同場景中未必一樣,不可泛泛而談。
一秒入門Go語言
Go或者Go語言,而不是Golang,Golang是網站的名字。
與其它大多數OO語言不同,Go的變數間賦值與傳參幾乎都是值拷貝。以陣列為例:
func main() { var array1 = [3]int{0, 1, 2} var array2 = array1 fmt.Println(array1, array2) fmt.Printf("%p, %p, %p, %p\n", &array1, &array2, &array1[0], &array2[0]) } /*輸出: [0 1 2] [0 1 2] 0xc04204c0a0, 0xc04204c0c0, 0xc04204c0a0, 0xc04204c0c0 */
可知兩個變數指向不同的記憶體塊。
再看slice
func main() { var s1 = []int{0, 1, 2} //[]表示切片型別,若陣列要忽略元素個數,使用[...] var s2 = s1 fmt.Printf("%p, %p, %p, %p\n", &s1, &s2, &s1[0], &s2[0]) } /*輸出: 0xc0420483a0, 0xc0420483c0, 0xc04204c0a0, 0xc04204c0a0 */
可得兩個slice變數指向不同地址(值拷貝),但是其中的元素指向的是同一個地址;假如設定s2[1]=5,那麼s1[1]也會變為5。這就涉及到slice的實現原理了,簡單的說,一個slice對應一個數組,可以認為slice指向該陣列的一個片段(所謂切片的由來)。上述程式碼中,s1、s2對應同一個陣列。我們知道,陣列是固定大小的,而在程式設計中,常常需要給slice追加資料,當資料項個數超過對應的陣列長度時,系統會new一個新陣列,原來的資料複製到新陣列,這個新的array則為slice新的底層依賴。具體可參看 Golang 切片與函式引數“陷阱”, 其它型別可參看 golang傳值和傳引用
注意:fmt.Printf("%p", s1)和fmt.Printf("%p", &s1[0])輸出結果相同。
所以為了避免記憶體浪費和資料不一致,我們常使用指標(雖然指標拷貝了,但是它們指向的資料還是同一個)。
interface:Go的OO特性主要依託的就是interface,語法同主流OO語言完全不同,其中有許多值得注意的特性和用法,當然我們亦要考慮前述的值傳遞or引用傳遞的問題(我們會看到有很多地方是指標實現介面方式,就是緣於此)。一些概念的運用可參看 Go語言基礎:Interface。一個型別只有實現了interface的所有方法,才能認為是實現了這個interface,若只實現了部分方法,專案中又有它們之間的型別轉換程式碼時,編譯時將報“does not implement IXXX(missing X method)”錯。若interfaceA包含了interfaceB,那麼A就自動擁有了B中定義的所有方法,效果同C#中IA:IB的繼承模式。
若型別XXX的指標*XXX實現了interfaceA,那麼XXX的物件xxx和指標*XXX的物件都可以點出interfaceA中的方法,但只有*XXX才可以與interfaceA做型別轉換。
struct:有個tag的概念,參看 GoLang structTag說明 。在tag中可以寫第三方庫的型別而不需要Import,如:RootDir string `mapstructure:"home"`,mapstructure就是第三方庫,定義struct欄位時不需引入,在實際呼叫的檔案中再引入。struct也有類似上述interface的“繼承”語法。更多可參看 Golang struct結構。
型別斷言(Comma-ok斷言):即判斷物件的執行時型別,起到的作用類似於Java的instanceof、C#的is/as。它的語法有點奇怪:value, ok := em.(T)——em為某個interface變數,T是期望的執行時型別,ok表示em的執行時型別是否就是T,value是轉換後的物件。如:
b,ok:=a.([]int) if ok{ //... }
等價於
if b,ok:=a.([]int);ok{ //... }
若我們知道b就是[]int,那麼也可以省略ok的賦值 b,_:=a.([]int) 或者b:=a.([]int)
或者b:=[]int(a),(a為interface時貌似必須要用斷言語法進行型別轉換)。另外還有switch value := e.(type){ case int:...}的寫法。
若interfaceA包含interfaceB,某個類XXX實現了interfaceA,那麼XXX物件xxx轉成interfaceA/interfaceB.(type),case interfaceA/interfaceB/XXX 都成立。
Go沒有工程檔案的概念,是通過目錄結構來體現工程的結構關係。也有諸多環境變數,接觸較多的是GOPATH,在其中可以設定多個目錄,每個目錄就是每個專案的根目錄。具體可參看 Go專案的目錄結構。同屬於某個包的程式碼檔案要有單獨目錄(目錄名即包名),不同包的檔案不能放置於同一個目錄下。GOPATH有個bin目錄,用於存放可執行檔案,為了方便,我們常把%GOPATH%/bin加入到PATH中,不過假如GOPATH有多個目錄,那麼在windows下只能分別加入了(Linux可以用${GOPATH//://bin:}/bin新增所有的bin目錄,windows不知道有沒有類似的語法)。
go build、go install的區別:前者用於編譯檢查,除可執行檔案外,不會有其它檔案生成,它的作用就是為了檢查是否有編譯時錯誤;後者當然也包括前者環節,同時會生成依賴包檔案。
Go語言沒有像其它語言一樣有public、protected、private等訪問控制修飾符,它是通過字母大小寫來控制可見性的,如果定義的常量、變數、型別、介面、結構、函式等的名稱是大寫字母開頭表示能被其它包訪問或呼叫(相當於public),非大寫開頭就只能在包內使用(相當於private,當然同個包的不同型別也可以使用。變數或常量也可以下劃線開頭)。
tips:與C#不同的是,我們可以定義一個private struct/others,實現public interface,該特性提供了一個對外隱藏物件細節,但又不妨礙呼叫其邏輯的途徑。
Go的function types:可以給包含相同引數和返回型別的函式集合抽象出一個函式型別,如此我們也可以給函式增加新方法(因為其是一個型別嘛)。參看 理解go的function types。本人現在尚不知為何要搞出這一個概念,因為如果是管理方法和多型特性的話,還是使用傳統的型別比如interface/struct比較合理;如果只是為了函式能作為引數傳遞,更沒有必要,因為函式本身就支援將自己傳給另外的函式。其實可以直接使用類似 var delegate func(int) 宣告一個符合特定規則的函式變數。
讓人困惑的一段程式碼,有知道的同學請不吝賜教:
1 func main() { 2 fmt.Println("anything") //加上這句,且與第7行都為fmt.Println 3 4 func() { println("順序測試") }() //若該行也改為fmt.Println,則順序正常 5 6 x := 10000 7 fmt.Println(byte(x)) 8 } 9 10 /*輸出 11 anything 12 16 13 順序測試 14 */
命令列互動:很多程式特別是在linux系統下,都以命令列互動為主。Go語言有命令列庫cobra,能非常快捷地讓程式支援命令形式。首先我們要安裝cobra:
go get -v github.com/spf13/cobra/cobra
cobra庫較大,同時依賴其它的一些庫,因此最好開個vpnFQ,免得中途斷線(雖然github能在國內訪問,但本人在下載原始碼過程中仍然幾乎每次都出現斷連情況)。開個vpn,在下載 https://golang.org/x/text/transform?go-get=1 時仍不成功,原因不知為何,在網上找了一份單獨下載放到GOPATH\src下即可(參看 【Golang筆記】Golang工具包Cobra安裝記錄)。關於cobra的使用可參看 golang命令列庫cobra的使用/Golang: Cobra命令列引數庫的使用。
上面用到的go get是一種依賴管理方式,類似於Java的maven、Js的npm、python的pip、C#的nuget,使用它來下載或更新當前專案依賴的第三方庫。所不同的是,go get 用來動態獲取遠端程式碼包後再install成pkg,要是非開源的庫應該就不能用這種方法了。go get是通過解析程式碼中的import語句來檢視依賴包(而非需要我們人工提供一個依賴庫的列表檔案),當下載了依賴庫之後,它會繼續分析該依賴庫依賴的其它庫,直到所需要的庫全部下載完畢。
go get的缺陷是沒有庫版本資訊,第三方庫管理工具godep、govender、glide和官方1.9版本推出的dep倒是可以了。它們同go get方式一樣分析所需的依賴庫,並將依賴庫及其版本資訊記錄在生成的檔案裡,下載的依賴會放到一個叫vendor的目錄下——Go1.5開始引入了另一種包的發現方法,如果專案中包含一個叫vendor的目錄,go將會從這個目錄搜尋依賴的包,這些包會在標準庫之前被找到。vendor目錄是放在當前工程目錄下,避免了go get方式的將所有依賴庫存放於GOPATH的第一個目錄導致的遷移問題和不同專案引用的庫版本衝突問題。需要注意,vendor本身只是一個目錄,不承擔庫版本控制的職責,這方面工作還是得由dep等去完成。
等到dep正式整合到Go環境中時候,也許是Go 1.10 ,廣大吃瓜群眾就可以直接使用go dep命令,現在還是需要自己安裝。dep的用法網上資料很多,需要注意的是Gopkg.toml中override特性的作用,用於解決多個關聯專案引用不同版本的依賴庫的版本衝突問題,可參看 使用 override 解決 dep 中的依賴衝突。
由於牆的緣故,很多官方(golang.org)的庫無法下載,雖然基本上的庫都可以在github上找到相應的原始碼,但是若要手動下載install啥的,就回到原點,失去依賴管理工具帶來的便捷了。網上也有同仁遇到這種煩惱:dep ensure無法拉取golang.cn以及google.golang.org的依賴問題,按照回答裡的資訊,我打算買臺境外伺服器裝ss5作為代理。於是我去買了阿里雲位於香港的ECS,登入ECS訪問大陸被牆的網站妥妥的沒問題,我滿心歡喜;接著照著 CentOS7 配置SOCKS5代理服務 中的步驟安裝了ss5,然後在伺服器和阿里雲控制檯都打開了1080埠(ss5預設埠),並在火狐中進行了代理設定,結果是IP顯示的確是香港的,然而原本被牆的[幾大]網站(google、facebook、youtube)還是上不了,本來可訪問的站點變得很慢甚至連線不上,和 centos 搭建了 ss5,為什麼不能訪問 google 中的問題幾乎一樣,我估計是阿里雲後臺做了限制。
換成AWS也一樣(EC2選在美國東部),但是訪問非被牆的國外網站確實快了不少,訪問國內站點亦變得較慢,所以ss5應該還是起作用了,至於為何翻不了牆,估計是政府要求這些雲廠商都做了處理;後來偶然發現,一些原本無法訪問的國外站點(筆者暫試了sex類),代理之後就能訪問了(推測依舊無法訪問的只有少數的政府重點關注的網站)。並且不但設定代理的火狐可以訪問,未設定的chrome偶爾也可以(此時chrome訪問國內站點速度並未減慢,對外IP仍為本地),取消代理後一段時間內仍能訪問,種種狀況,不知何故;有次發現在無法訪問時ping得的IP和可訪問時ping得的IP不一樣,估計是連上ss5之後,代理端的dns伺服器返回了可訪問的IP(域名解析過程可參看 DNS解釋),且該IP和域名的對映被同時快取到了本地全域性域裡,這倒是可以解釋前述情景;不過我將hosts顯式配置了可訪問的IP,然後取消代理,該網站又無法訪問了,真的是難以捉摸;大部分情況兩次ping得的IP是一樣的,且是否設定代理確實直接影響到網站是否能訪問,即[代理後]能否ping通和[代理後]能否訪問並無關聯。(另:Amazon自己的Linux版本安裝不了ss5,會報“undefined reference to S5ChildClose”,貌似是ss5不相容gcc5之類的原因)
不過幸好golang.org是可以訪問了,為了使得命令列也能用上ss5代理,去下個Proxifier,然後再dep ensure,妥妥的沒問題。(目前AWS的t2.micro型別例項有一年的免費期,1C1G1M,作為自己的獨家代理夠用了)
goroutine:網上資料很多,引出的是協程的概念,這個概念在我之前的博文中有所涉及,可參看。常使用channel為協程間協調和通訊,channel有隻讀只寫的用法,主要用在一段邏輯中,表明在這段邏輯裡,該channel只能讀或只能寫,否則編譯報錯,它並不表示真的有隻讀只寫的channel,可參看 Go 只讀/只寫channel。
其它語言不常見的select控制結構:Go 語言 select 語句
其它
vpn:我們常用vpnFQ,可以看作一種代理模式,其實vpn的初衷是為了方便非本地區域網的合法使用者訪問本地區域網資源,通俗例子可看vpn的實現原理。外部網路預設無法訪問區域網資源,就如同我們無法訪問牆的那一邊[被牆的資源],可能因為這暗合了vpn的用途,所以當前市場各色FQ軟體以vpn為主。貌似vpn目前正在遭受有關部門的封殺清洗,另有其它技術如socks可實現代理功能。
pv操作:P和V是來源於兩個荷蘭語詞彙,P—— passeren,中文譯為"通過";V—— vrijgeven,中文譯為"釋放"。P操作和V操作是執行時不被打斷的兩個作業系統原語(在執行這兩個語句時不允許系統發生中斷,從而保證語句的原子性執行),它們操作的是訊號量S。執行緒/程序要執行時,先P一下看是否通過(S是否>=0,即是否可以執行),若否則等待;執行完畢V一下將S+1,表示資源被釋放,其它執行緒可以開始執行。
乙太網智慧合約:
智慧合約就是一段程式,一段邏輯(這段程式碼可以有狀態變數),我們將它編譯後的位元組碼部署到區塊鏈上(需要發起一個交易),合約部署後會建立一個合約賬戶,合約賬戶裡儲存著智慧合約的可執行位元組碼,並且有儲存空間用於存變數值(storage)。有個abi的概念,abi是一個介面結構,利用abiDefinition可以建立呼叫該合約的結構,abi應該由合約所有方自己儲存和提供。
要執行智慧合約時,呼叫方從區塊鏈上[通過地址]獲取這段程式碼並呼叫(一般也會發起一個交易),呼叫時可能會改變狀態變數的值,這些狀態量的更改反映到storage中。storage的物理儲存結構時怎樣的,根據Solidity首席工程師Chriseth的說法,“你可以把storage想像成一個大陣列”,就跟 瞭解以太坊智慧合約儲存 寫的一樣,深入瞭解以太坊虛擬機器第2部分——固定長度資料型別的表示方法 中也有無限量的記憶體的說法,如此即可將值存入hash(key)後的最大為2256記憶體地址中,且幾乎肯定的不會產生衝突(即無需使用類似hashmap的衝突處理)。然而實際的儲存空間肯定遠小於此,網上搜了一圈沒看到實際的結構介紹,先將此疑問記錄於此,日後檢視。
另外,我們不要被網上智慧合約的概念欺騙,目前,智慧合約遠未到預期的設想,主要的障礙有兩點:
- 智慧合約的應用依賴於基於區塊鏈資產的數字化,但是目前來講,這種數字化程度還遠遠不夠。即智慧合約只有在數字版本與實體之間存在某種明確的聯絡時才能有效代替普通合約,且實體關係需要隨著數字資產變化而自覺變化,反之亦然,案例設想可參看 概念炒作的背後,“智慧合約”的真相是什麼?
- 智慧合約只能被動響應外部訪問請求,根本無法做到內部合同條款的自動執行。而外部請求一般都是中心化的,這進一步會極大降低智慧合約作為一個去中心化系統的有效性。
其它參考資料: