基于Tendermint的區(qū)塊鏈漂流瓶簡單實現(xiàn)
本文主要借demo介紹基于Tendermint的區(qū)塊鏈應(yīng)用開發(fā),這個demo很簡單,主要包含以下功能:
- 扔漂流瓶
- 撈漂流瓶
- 之后投放者和打撈者可以相互傳遞[加密]信息
代碼已上傳至github。
Tendermint
Tendermint幫我們實現(xiàn)了PBFT,相當(dāng)于搭了一個共識框架,包含兩部分:
- Tendermint-core:PBFT共識算法實現(xiàn);
- Tendermint-abci:定義了應(yīng)用須實現(xiàn)的接口和調(diào)用規(guī)則,還實現(xiàn)了與外部通信的socket-server。官方的這部分源碼可以看做是Go-abci,我們也可以根據(jù)需要編寫其它語言的xxx-abci。
可以將其類比為傳統(tǒng)應(yīng)用的開發(fā)框架(如MVC),而我們要做的就是基于abci編寫具體的區(qū)塊鏈邏輯(為方便和清晰起見,本文用Go編寫具體邏輯,自然abci就用官方的了),這就實現(xiàn)了服務(wù)端;而用戶也需要一個客戶端用來與區(qū)塊鏈交互。

以上,Tendermint、服務(wù)端邏輯、客戶端,三者組成了一個完整的區(qū)塊鏈應(yīng)用。
數(shù)據(jù)庫
在動手編碼之前,要考慮數(shù)據(jù)存儲的問題,選擇文本文件還是Oracle呢?區(qū)塊鏈網(wǎng)絡(luò)里大部分是普通電子設(shè)備,使用者亦是普通人,讓他們事先安裝大型數(shù)據(jù)庫顯然不現(xiàn)實,更不用說區(qū)塊鏈本身不會出現(xiàn)復(fù)雜操作數(shù)據(jù)的業(yè)務(wù)。另外由于全節(jié)點數(shù)據(jù)的完備性,用不著通過網(wǎng)絡(luò)去其它設(shè)備上查詢數(shù)據(jù),很多數(shù)據(jù)庫自帶的網(wǎng)絡(luò)服務(wù)也不需要(SPV這種,業(yè)務(wù)單一,完全可以單獨開放一個遠程接口)。而文本文件、excel之類的,只適合人類使用,根本不能算作數(shù)據(jù)引擎。我們需要的是一個滿足基本CUID的高效的本地數(shù)據(jù)庫,目前大多區(qū)塊鏈?zhǔn)褂肔evelDB作為存儲引擎,這是C/C++編寫的本地kv數(shù)據(jù)庫,原作者也寫了Go實現(xiàn)的版本,其原理可參看 半小時學(xué)會LevelDB原理及應(yīng)用 ,godoc地址:https://godoc.org/github.com/syndtr/goleveldb/leveldb。LevelDB總體上采用了LSM-Tree的設(shè)計思想(LSM-Tree的雖說是數(shù)據(jù)結(jié)構(gòu),但更偏重于設(shè)計思路)。
LevelDB同時只能被一個進程使用。另,以太坊的數(shù)據(jù)存儲于/chaindata目錄下,運行后其下會生成一坨.ldb文件,而非網(wǎng)上常說的sst文件,這可能是跟13年的一次版本更新有關(guān),Release LevelDB 1.14。另:LevelDB的k-v模式(順序讀效率不高)不適合relationship,即不適合有一定數(shù)據(jù)關(guān)聯(lián)度的業(yè)務(wù)場景。
為方便使用,可以封裝一些常用的數(shù)據(jù)庫操作。順便嘗試下提供新操作的幾種思路。
- 直接給leveldb.DB增加新方法:
然而,給一個類型新增方法只能在該類型同個package中,否則編譯時會報“Cannot define new methods on non-local type XXXX”的錯誤。此時,可以懷念下C#的擴展方法。// 給leveldb.DB增加Set方法 func (db *leveldb.DB) Set(key []byte, value []byte) { //... err := db.Put(key, value, nil) //... }
- 既然無法在外部修改leveldb.DB的方法集,那么就在當(dāng)前package建一個繼承l(wèi)eveldb.DB的struct,即內(nèi)嵌一個leveldb.DB類型字段, type GoLevelDB struct { *leveldb.DB } ,然后將上述代碼的指針類型改為*GoLevelDB即可,很完美。不過,在封裝Get方法的時候出問題了:
func (db *GoLevelDB) Get(key []byte) []byte { //... //Go不支持重載,或者說Go只把方法名作為唯一簽名。 //這里原意是調(diào)用的父類的Get方法,但該方法被當(dāng)前類的Get方法覆蓋了,參數(shù)不一致導(dǎo)致編譯失敗 res, err := db.Get(key, nil) //... return res }
不支持重載,只能修改子類的方法名,蛋疼;或者改成如下方式。
-
type GoLevelDB struct { db *leveldb.DB }
和第2種的區(qū)別就是把is-a改為has-a,也不用擔(dān)心方法重名的問題。不過我私以為若Go支持重載,第2種方式會好一點,至少不會嵌套太多層。
服務(wù)端
abci定義了如下接口:
type Application interface { // Info/Query Connection Info(RequestInfo) ResponseInfo // Return application info SetOption(RequestSetOption) ResponseSetOption // Set application option Query(RequestQuery) ResponseQuery // Query for state // Mempool Connection CheckTx(tx []byte) ResponseCheckTx // Validate a tx for the mempool // Consensus Connection InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set Commit() ResponseCommit // Commit the state and return the application Merkle root hash }
很明顯,后面幾個方法參與了區(qū)塊鏈狀態(tài)的更迭,我們就來捋捋交易從客戶端提交到最終上鏈的過程(不精確):
- 節(jié)點a的客戶端發(fā)起一筆交易tx;
- 節(jié)點a服務(wù)端調(diào)用CheckTx方法校驗tx是否合法,若非法則丟棄,當(dāng)做什么事都沒發(fā)生過;
- 若合法,則將tx加入到本地mempool中,并向其它節(jié)點廣播tx;
- 其它節(jié)點接收到tx,同樣執(zhí)行2-3步驟;
- 某輪決議開始,提議者搜集mempool中的txs,并發(fā)起投票,達成共識后,各節(jié)點調(diào)用BeginBlock開始將它們打包;
- 調(diào)用DeliverTx執(zhí)行每筆交易并將其記錄到區(qū)塊中(一筆交易執(zhí)行一次DeliverTx);
- 調(diào)用EndBlock表示打包完成;
- 發(fā)起共識決議,提議者將新區(qū)塊廣播給其它驗證者;(共識決議在第5步完成)
- 其它驗證者接收到區(qū)塊后,調(diào)用DeliverTx執(zhí)行每筆交易并校驗結(jié)果,若沒問題則廣播commit請求(預(yù)提交)和新區(qū)塊;
- 若節(jié)點收到超過2/3驗證者的commit請求,調(diào)用Commit方法,更新整個應(yīng)用狀態(tài)。
假如將要打包的tx緩存起來,我們就可以在DeliverTx、EndBlock、Commit三個方法中選擇其一實際執(zhí)行tx,但是一般來說,交易執(zhí)行都是放在DeliverTx,比較符合語義。EndBlock用于更新共識參數(shù)和Val集合,Commit用于更新整個應(yīng)用狀態(tài)(apphash),需要注意的是,本次提交的apphash若與上次提交的不同,則會繼續(xù)產(chǎn)生新的區(qū)塊(不管有沒有新交易,就算設(shè)置consensus.create_empty_blocks=false,tendermint也會產(chǎn)生空區(qū)塊,可參看 Enable no empty blocks #308 。),這似乎是tendermint的有意設(shè)計,但不知為何。
另Query方法接收的RequestQuery類型參數(shù)有Path和Data兩個字段,Path是string類型,Data是[]byte,應(yīng)該是對應(yīng)于Http的get、post。示例代碼中我是通過正則表達式解析Path查詢各類數(shù)據(jù),其實若是復(fù)雜查詢/結(jié)構(gòu)化查詢,還是Data字段比較實用。
正則表達式的所謂零寬斷言:只匹配位置,而不消費字符。下面舉個例子。如 \b\w*q[^u]\w*\b,它能匹配“Iraq,Benq”。因為[^u]總是匹配一個字符,所以如果q是單詞的最后一個字符的話,后面的[^u]將會匹配q后面的單詞分隔符(可能是空格,或者是句號或其它的什么),接著后面的\w+\b將會匹配下一個單詞,于是\b\w*q[^u]\w*\b就能匹配整個Iraq fighting。如果在這個例子中,我們只想匹配到Iraq,那么可以采用零寬負向先行斷言(?!exp)的方式,\b\w*q(?!u)\w*\b,它將不會消費Iraq后面的空格或逗號等字符,因此\w*也不會匹配到下一個單詞。參看 【詳細】正則表達式30分鐘入門教程 之位置指定和后向位置指定部分。
客戶端
demo采用命令行終端,基于cobra庫。
1 var rootCmd = &cobra.Command{ 2 Use: "dbcli", 3 //throw:丟;salvage:撈;reply:回應(yīng)。 ValidArgs要有定義Run[E],并與Args: cobra.OnlyValidArgs結(jié)合才起作用,表示參數(shù)值只能是預(yù)設(shè)值 4 //ValidArgs: []string{"throw", "salvage", "reply", "bbalj"}, 5 //Args主要是用來校驗參數(shù)的 6 //Args: cobra.OnlyValidArgs, //cobra.ExactArgs(0), 7 // RunE: func(cmd *cobra.Command, args []string) error { //args并不包含flag;os.Args是包含flag的 8 // }, 9 } 10 11 func main() { 12 if err := rootCmd.Execute(); err != nil { 13 fmt.Println(err) 14 os.Exit(-1) 15 } 16 }
原本我想實現(xiàn)交互模式(類似mysql>),但cobra似乎沒有提供相關(guān)方法,我們只好自己想辦法,需要注意的是需要自解析用戶輸入,比如用戶輸入有空格,該空格是分隔參數(shù)還是參數(shù)內(nèi)部的,要做區(qū)分。原本打算參考cobra解析命令行的源碼,發(fā)現(xiàn)實際解析使用的是spf13/pflag庫,而pflag只是加強了go標(biāo)準(zhǔn)庫flag,而flag庫也并沒有涉及到參數(shù)值本身的具體解析,這部分工作依靠的是oa庫,主要是oa.Args屬性,它依賴更底層的代碼。
// 摘自go/src/os/proc.go // Args hold the command-line arguments, starting with the program name. var Args []string func init() { if runtime.GOOS == "windows" { // Initialized in exec_windows.go. return } Args = runtime_args() } func runtime_args() []string // in package runtime
如注釋所示,windows下是在exec_windows.go中實現(xiàn),其它操作系統(tǒng)的實現(xiàn)沒找到,應(yīng)該是使用其它語言編寫或直接調(diào)用的系統(tǒng)api。進exec_windows.go中,發(fā)現(xiàn)關(guān)鍵函數(shù)readNextArg:
1 // readNextArg splits command line string cmd into next 2 // argument and command line remainder. 3 func readNextArg(cmd string) (arg []byte, rest string) { 4 var b []byte 5 var inquote bool 6 var nslash int 7 for ; len(cmd) > 0; cmd = cmd[1:] { 8 c := cmd[0] 9 switch c { 10 case ' ', '\t': 11 if !inquote { 12 return appendBSBytes(b, nslash), cmd[1:] 13 } 14 case '"': 15 b = appendBSBytes(b, nslash/2) 16 if nslash%2 == 0 { 17 // use "Prior to 2008" rule from 18 // http://daviddeley.com/autohotkey/parameters/parameters.htm 19 // section 5.2 to deal with double double quotes 20 if inquote && len(cmd) > 1 && cmd[1] == '"' { 21 b = append(b, c) 22 cmd = cmd[1:] 23 } 24 inquote = !inquote 25 } else { 26 b = append(b, c) 27 } 28 nslash = 0 29 continue 30 case '\\': 31 nslash++ 32 continue 33 } 34 b = appendBSBytes(b, nslash) 35 nslash = 0 36 b = append(b, c) 37 } 38 return appendBSBytes(b, nslash), "" 39 }
其中對雙引號做了處理,注釋中還提供了一個網(wǎng)址How Command Line Parameters Are Parsed,應(yīng)該是關(guān)于這方面的算法說明,日后再看。
序列化
當(dāng)我們在說序列化的時候,我們在說什么。序列化說白了就是數(shù)據(jù)轉(zhuǎn)化,或者說一一對應(yīng)的映射關(guān)系。就內(nèi)存場景來說,一個對象序列化為另一個對象,本質(zhì)上它們都一樣,都是存儲在內(nèi)存中的0、1序列,只是同一個東西不同的數(shù)據(jù)表達。比如將一個數(shù)值序列化(或者說轉(zhuǎn)化)成字符串類型,或者將數(shù)值int32轉(zhuǎn)為數(shù)值int8,那么內(nèi)存中的存儲空間和存儲數(shù)據(jù)都不會一樣,字符串還要看用的什么編碼。再如我們將一個對象序列化為byte[],不同的方案會產(chǎn)生不同的結(jié)果。比如使用C指針將物理數(shù)據(jù)直接映射出來,或者以json方式序列化,或者protobuf序列化,會產(chǎn)生不同的byte[];反之亦然。
不管是json編碼還是二進制編碼,物理上存儲的都是二進制,json編碼包含于二進制編碼,我們可以根據(jù)需要自定義二進制編碼,一般是為了減少存儲占用的空間。比如json編碼,對1、2等數(shù)值類型是按字符串格式編碼(如utf8格式,1編碼的就是0x31,12占兩個字節(jié)0x310x32),而我們自定義二進制,完全可以把12存儲在一個字節(jié)里面,該字節(jié)值就是數(shù)值本身;就算不是數(shù)值,而是字符串本身編碼,我們也可以在utf8編碼后再壓縮,類似gzip。
go中的序列化方式,可參看 Golang 序列化方式及對比,但是文中g(shù)ob的測試代碼其實可以改良下,將enc/dec兩個變量移到循環(huán)外,如此可在循環(huán)內(nèi)復(fù)用,這將發(fā)揮gob上下文的優(yōu)勢。
protobuf的變長編碼針對的是數(shù)值類型,so應(yīng)該只對數(shù)值字段多的類型有壓縮的意義。其實protobuf應(yīng)該是專門針對HTTP 2.0設(shè)計,相對于HTTP 1.X的純文本傳輸來,HTTP 2.0傳輸?shù)氖嵌M制數(shù)據(jù),與Protocol Buffers相輔相成,使得傳輸數(shù)據(jù)體積小、負載低,保持更加緊湊和高效。
go對字符串是utf8編碼,基本不用擔(dān)心中文亂碼問題。
vscode-go開發(fā)環(huán)境
在國內(nèi),搭建Go開發(fā)環(huán)境都不會太順利,下面我就說說在vscode中搭建環(huán)境可能會遇到的問題和解決方法。
Go開發(fā)環(huán)境需要vscode安裝一些插件,而項目中也有引用的類庫,這兩者都可能涉及到相關(guān)站點在墻外的情況,而我們也要分別設(shè)置代理。首先,給vscode本身設(shè)置代理,使得安裝插件沒有問題;其次,在命令行窗口設(shè)置http_proxy,使得dep順利進行。也可以在vscode終端窗口設(shè)置http_proxy(vscode的終端就是個命令行交互環(huán)境,使用的還是操作系統(tǒng)的shell,本質(zhì)上獨立于vscode),但博主發(fā)現(xiàn)似乎并不起作用。
在代理什么都設(shè)置好后,vscode安裝插件時仍可能遇到問題,比如文件中已經(jīng)存在的golang.org\x\tools目錄關(guān)聯(lián)的git源碼網(wǎng)址不是插件要求的源碼網(wǎng)址,原因可能是之前手動到github里下載的tools源碼,將tools目錄移除重新跑一遍安裝插件的步驟即可。
安裝goimports時可能會timeout等錯誤,參考 安裝goimports 解決。
項目方面,具體到我們這個demo,遵照tendermint官方文檔,make get_tools。我是windows10系統(tǒng),使用bash命令進入到自帶的Ubuntu子系統(tǒng),就可以使用內(nèi)置的make了。需要注意的是,若設(shè)置了系統(tǒng)變量GOPATH,且是以分號分隔的多個文件夾,那么切換到Ubuntu后,由于linux系統(tǒng)是按冒號分隔的,所以它會把分號當(dāng)做文件夾名的一部分,導(dǎo)致自動創(chuàng)建一些奇怪目錄。如果是其它windows系統(tǒng),可以安裝mingw,定位到安裝目錄的bin目錄下,就可以使用mingw-make操作了(可以將mingw-make重命名為make),可能會報錯:
process_begin: CreateProcess(NULL, env bash F:\Document\code\tendermint\tendermint\scripts\get_tools.sh, ...) failed. make (e=2): 系統(tǒng)找不到指定的文件。
如果不是get_tools.sh的路徑問題,那就應(yīng)該是bash沖突了(比如系統(tǒng)中安裝了git,同時把git目錄也配置到PATH下,實際定位的可能就是git的bash了)。
注意tendermint所需的最低Go版本。
我們要嚴格遵循Go的目錄規(guī)范,若將代碼直接置于src\目錄下,則執(zhí)行dep相關(guān)操作時,會拋出“root project import: dep does not currently support using GOPATH/src as the project root”錯誤。需要在src\下再建一個目錄,把代碼拷進這個子目錄再執(zhí)行dep。Go遵循約定大于配置的原則,它在項目中引入所有依賴類庫的代碼,而這些類庫也是放置于src目錄下,所以需要按子目錄分開。另關(guān)于依賴項搜尋Support vendor directory as $GOPATH/src/vendor #313 應(yīng)該有參考價值,另可參看 dep init fails if in not in $GOPATH[...]/src/{somedir..} #148。
dep似乎會將GOPATH\src下的依賴也復(fù)制到vendor下,感覺是不是沒這必要。
經(jīng)驗:最好在項目剛開始搭建就 dep init,否則在代碼敲了一個階段后,已經(jīng)import了多個外部依賴,當(dāng)這時候再 dep init,如果出現(xiàn)錯誤,將不會生成Gopkg.toml,如果是因為版本問題導(dǎo)致的錯誤,你都沒辦法通過編輯Gopkg.toml的方式解決。比如我就遇到這種情況,dep init -gopath, -gopath表示先去本地GOPATH目錄找依賴庫,找不到再去網(wǎng)上拉取,結(jié)果我的本地庫版本不是master分支,而貌似dep默認的就是master,導(dǎo)致“v0.30.2: Could not introduce github.com/tendermint/tendermint@v0.30.2, as it is not allowed by constraint master from project tuoxie/driftbottle.”這樣的錯誤提示(dep也是一根筋,它會把這個庫的所有release版本都比對一遍看滿不滿足constraint)。此時也不是沒辦法,我們可以把入口函數(shù)main所在文件整個注釋掉,這樣dep就不會遍歷代碼文件,但仍然會生成Gopkg.toml,這個時候就可以手動編輯約束版本號了。
go install 不會把vendor目錄下的所有包無腦打包進exe文件,而是會根據(jù)實際依賴打包,這樣也使得我們可以多個[子]項目使用同一個vendor,減小磁盤占用和復(fù)用已下載的依賴包,而不必擔(dān)心exe文件過大的問題。
目前vscode調(diào)試go尚不能支持交互模式的命令行調(diào)試,沒有如python那樣可以在launch.json設(shè)置console屬性[為externalTerminal]。
其它
作為區(qū)塊鏈最廣泛應(yīng)用的數(shù)字貨幣已經(jīng)不再像不久以前一樣能夠隨意撩撥投機者的神經(jīng),但這項技術(shù)在其它更實用的領(lǐng)域或許仍值得期待。比如區(qū)塊鏈的共識機制、區(qū)塊時間戳、防篡改特性,似乎天生是為知識產(chǎn)權(quán)保護打造的,然而迄今為止市面上尚未出現(xiàn)讓人眼前一亮的產(chǎn)品。前段時間看到一則新聞,說百度上線了一個保護圖片版權(quán)的區(qū)塊鏈項目“圖騰”,有興趣的同學(xué)可以去了解下。如果我要實現(xiàn)類似的知識產(chǎn)權(quán)鏈,會考慮文件相似度判別、[使用代幣]支付版權(quán)費及支付策略(買斷or按次付款等)等等,交易媒介和交易標(biāo)的都在鏈上,形成閉環(huán)。鏈上閉環(huán)可不受外部實體困擾,以區(qū)塊鏈二代的明星特性“智能合約”為例,一旦與外部有所關(guān)聯(lián),就無法保證合約的事務(wù)完整性,可參看我之前的觀點。
Tendermint里有很多ethereum的影子,比如gas、db的封裝等,部分思路和代碼應(yīng)該是參考了ethereum的實現(xiàn)。
ethereum(以太坊)相關(guān)概念:
MPT:即Merkle Patricia Tree,是Merkle Tree 和Patricia Tree結(jié)合的產(chǎn)物。Patricia Tree又是Trie Tree的一種變化。參考資料:Trie原理以及應(yīng)用于搜索提示,以太坊MPT原理,你最值得看的一篇。這兩篇偏向于原理,若要了解具體細節(jié),可看 干貨 | Merkle Patricia Tree 詳解。
叔區(qū)塊
gas:一直很好奇以太坊是怎么做到計算實際使用gas量的,特別是有控制跳轉(zhuǎn)語句的時候,最可靠的方式是實際運行時實時計算gas,那這個應(yīng)該是由EVM實現(xiàn)的。具體可看 以太坊虛擬機及交易的執(zhí)行,以太坊智能合約虛擬機(EVM)原理與實現(xiàn)。
數(shù)據(jù)結(jié)構(gòu)與存儲方式:以太坊源碼情景分析之?dāng)?shù)據(jù)結(jié)構(gòu),[以太坊源代碼分析] II. 數(shù)據(jù)的呈現(xiàn)和組織,緩存和更新
個人認為區(qū)塊鏈目前普遍存在的問題:
- 升級困難(分叉?)
- 維護困難(當(dāng)單節(jié)點故障時,只能依靠該節(jié)點自身能力處理,對于普通用戶來說,無疑是棘手的)
- 隨著時間的推移,數(shù)據(jù)量會變得越來越大,全節(jié)點將相應(yīng)變少,最終形成某種意義上的中心化網(wǎng)絡(luò)
更多資料:
以太坊源碼深入分析(7)-- 以太坊Downloader源碼分析
轉(zhuǎn)載請注明本文出處:http://www.rzrgm.cn/newton/p/9611340.html

浙公網(wǎng)安備 33010602011771號