PBFT概念與Go語言入門(Tendermint基礎)
Tendermint作為當前最知名且實用的PBFT框架,網上資料并不很多,而實現Tendermint和以太坊的Go語言,由于相對小眾,也存在資料匱乏和模糊錯漏的問題。本文簡單介紹PBFT概念和Go語言[&開發環境]關鍵知識點,其中大部分都可單獨成篇,限于篇幅,文中提供諸多鏈接供大家深入。日后可能會基于Tendermint出系列博文,此篇純當基礎。
概念
下述一部分在前篇區塊鏈初探中亦有涉及,可結合著看。
分布式系統中的異步和共識
異步:這里的異步不同于通常技術術語中的異步調用的異步,而是指在一個分布式系統中,對消息的處理速度或者消息送達時間不做任何假設。
共識:互相獨立的多個系統參與方(進程或者節點)間對某個問題達成一致的結果。
FLP Impossibility:在異步的場景下,即便沒有拜占庭故障,即便消息系統足夠可靠(所有的消息都可以被正確的送達剛好一次),只要有至少一個進程失效(比如掉線等無法響應的情形),也沒有任何算法能保證非失效進程達到一致性(共識)。
個人認為理解FLP的重點是:一個進程無法探測未響應進程的當前狀態(失效or網絡延遲導致),導致該進程無法確認自己是否應該[不管未響應進程]作出決策或者[不管未響應進程]作出的決策是否正確,從而導致整個系統狀態的不確定性。
PBFT:實用拜占庭容錯算法。現實生活中,完全異步的場景較少,或者我們可以設置超時等規則,繞過FLP的限制,稱之為實用的一致性算法。比如PBFT,它的核心三階段協議,依靠的是弱同步的網絡環境(如因特網,雖有延遲但總體可控),具體流程可看 PBFT實用拜占庭容錯算法深入詳解。
基于PBFT的區塊鏈共識框架,最知名的當屬Tendermint。網上資料常拿他與以太坊的Casper協議比較優劣,Casper是基于鏈的共識算法,且內部實現了較為嚴厲的懲罰措施制約惡意節點。達成共識的手段上,Tendermint是基于輪次的投票機制,而Casper是基于賭博的投注機制。目前我對Casper節點具體的投注規則和流程仍然一頭霧水,有興趣的朋友可參看 干貨 | 理解 Serenity,Part-2:Casper。
Tendermint的數據采用Amino編碼,Amino繼承自Protobuf3,Protobuf3主要是使用變長編碼Varint編碼數值,以減少存儲空間大小和傳輸量,可參看 Varint與ZigZag編碼、Protobuf3語言指南。SPV實現,同比特幣一樣依靠的是Merkel Tree,原理參看梅克爾樹和簡單支付驗證
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支持多返回值,簡直完美。
變量聲明,若不賦值,則為默認值,注意并非nil,以struct為例:
type State struct { Height int64 AppHash string } func main() { var state State println(state.Height) println(state.AppHash) } /*輸出 0 "" */
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命令,現在還是需要自己安裝。使用要點可參看Go語言入門——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 語句
測試:用到testing包,參看Golang 語言的單元測試和性能測試(也叫壓力測試)。
其它
vpn:我們常用vpnFQ,可以看作一種代理模式,其實vpn的初衷是為了方便非本地局域網的合法用戶訪問本地局域網資源,通俗例子可看vpn的實現原理。外部網絡默認無法訪問局域網資源,就如同我們無法訪問墻的那一邊[被墻的資源],可能因為這暗合了vpn的用途,所以當前市場各色FQ軟件以vpn為主。目前工信部嚴管VPN提速,另有其它技術如socks可實現代理功能。
除了上面提到的ss5+Proxifier外,還有ss-fly+Shadowsocks。使用起來感覺后者比較穩定,不存在部分能上部分不能上的問題,特別是提供侵入性小的PAC模式(Proxifier似乎也有提供),如果軟件支持代理設置(如瀏覽器),則設置為使用系統代理或者設置代理地址為http://127.0.0.1:port即可,其中port是Shadowsocks暴露給本機的代理端口;不過如果軟件沒有地方設置代理,那么可以用前者,在Proxifier中設置哪些軟件使用代理;當然也可以ss-fly+Shadowsocks+Proxifier結合使用()設置Proxifier的代理為Shadowsocks提供的代理地址http://127.0.0.1:port)。
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的沖突處理)。然而實際的存儲空間肯定遠小于此,網上搜了一圈沒看到實際的結構介紹,先將此疑問記錄于此,日后查看。
另外,我們不要被網上智能合約的概念欺騙,目前,智能合約遠未到預期的設想,主要的障礙有兩點:
- 智能合約的應用依賴于基于區塊鏈資產的數字化,但是目前來講,這種數字化程度還遠遠不夠。即智能合約只有在數字版本與實體之間存在某種明確的聯系時才能有效代替普通合約,且實體關系需要隨著數字資產變化而自覺變化,反之亦然,案例設想可參看 概念炒作的背后,“智能合約”的真相是什么?
- 智能合約只能被動響應外部訪問請求,根本無法做到內部合同條款的自動執行。而外部請求一般都是中心化的,這進一步會極大降低智能合約作為一個去中心化系統的有效性。
其它參考資料:

浙公網安備 33010602011771號