基於Tendermint的區塊鏈漂流瓶簡單實現

本文主要借demo介紹基於Tendermint的區塊鏈應用開發,這個demo很簡單,主要包含如下功能:html

  1. 扔漂流瓶
  2. 撈漂流瓶
  3. 以後投放者和打撈者能夠相互傳遞[加密]信息

代碼已上傳至githubnode


Tendermintpython

Tendermint幫咱們實現了PBFT,至關於搭了一個共識框架,包含兩部分:mysql

  • Tendermint-core:PBFT共識算法實現;
  • Tendermint-abci:定義了應用須實現的接口和調用規則,還實現了與外部通訊的socket-server。官方的這部分源碼能夠看作是Go-abci,咱們也能夠根據須要編寫其它語言的xxx-abci。

能夠將其類比爲傳統應用的開發框架(如MVC),而咱們要作的就是基於abci編寫具體的區塊鏈邏輯(爲方便和清晰起見,本文用Go編寫具體邏輯,天然abci就用官方的了),這就實現了服務端;而用戶也須要一個客戶端用來與區塊鏈交互。linux

以上,Tendermint、服務端邏輯、客戶端,三者組成了一個完整的區塊鏈應用。git


數據庫github

在動手編碼以前,要考慮數據存儲的問題,選擇文本文件仍是Oracle呢?區塊鏈網絡裏大部分是普通電子設備,使用者亦是普通人,讓他們事先安裝大型數據庫顯然不現實,更不用說區塊鏈自己不會出現複雜操做數據的業務。另外因爲全節點數據的完備性,用不着經過網絡去其它設備上查詢數據,不少數據庫自帶的網絡服務也不須要(SPV這種,業務單一,徹底能夠單獨開放一個遠程接口)。而文本文件、excel之類的,只適合人類使用,根本不能算做數據引擎。咱們須要的是一個知足基本CUID的高效的本地數據庫,目前大多區塊鏈使用LevelDB做爲存儲引擎,這是C/C++編寫的本地kv數據庫,原做者也寫了Go實現的版本,其原理可參看 半小時學會LevelDB原理及應用 ,godoc地址:https://godoc.org/github.com/syndtr/goleveldb/leveldb。LevelDB整體上採用了LSM-Tree的設計思想(LSM-Tree的雖然說是數據結構,但更偏重於設計思路)。golang

LevelDB同時只能被一個進程使用。另,以太坊的數據存儲於/chaindata目錄下,運行後其下會生成一坨.ldb文件,而非網上常說的sst文件,這多是跟13年的一次版本更新有關,Release LevelDB 1.14。另:LevelDB的k-v模式(順序讀效率不高)不適合relationship,即不適合有必定數據關聯度的業務場景。正則表達式

爲方便使用,能夠封裝一些經常使用的數據庫操做。順便嘗試下提供新操做的幾種思路。算法

  1. 直接給leveldb.DB增長新方法:
    // 給leveldb.DB增長Set方法
    func (db *leveldb.DB) Set(key []byte, value []byte) {
        //...
        err := db.Put(key, value, nil)
        //...
    }
    然而,給一個類型新增方法只能在該類型同個package中,不然編譯時會報「Cannot define new methods on non-local type XXXX」的錯誤。此時,能夠懷念下C#的擴展方法。
  2. 既然沒法在外部修改leveldb.DB的方法集,那麼就在當前package建一個繼承leveldb.DB的struct,即內嵌一個leveldb.DB類型字段, type GoLevelDB struct { *leveldb.DB } ,而後將上述代碼的指針類型改成*GoLevelDB便可,很完美。不過,在封裝Get方法的時候出問題了:
    func (db *GoLevelDB) Get(key []byte) []byte {
        //...
        //Go不支持重載,或者說Go只把方法名做爲惟一簽名。
        //這裏原意是調用的父類的Get方法,但該方法被當前類的Get方法覆蓋了,參數不一致致使編譯失敗
        res, err := db.Get(key, nil)
        //...
        return res
    }

    不支持重載,只能修改子類的方法名,蛋疼;或者改爲以下方式。

  3. type GoLevelDB struct {
        db *leveldb.DB
    }

    和第2種的區別就是把is-a改成has-a,也不用擔憂方法重名的問題。不過我私覺得若Go支持重載,第2種方式會好一點,至少不會嵌套太多層。


服務端

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
}

很明顯,後面幾個方法參與了區塊鏈狀態的更迭,咱們就來捋捋交易從客戶端提交到最終上鍊的過程(不精確):

  1. 節點a的客戶端發起一筆交易tx;
  2. 節點a服務端調用CheckTx方法校驗tx是否合法,若非法則丟棄,當作什麼事都沒發生過;
  3. 若合法,則將tx加入到本地mempool中,並向其它節點廣播tx;
  4. 其它節點接收到tx,一樣執行2-3步驟;
  5. 某輪決議開始,提議者蒐集mempool中的txs,併發起投票,達成共識後,各節點調用BeginBlock開始將它們打包;
  6. 調用DeliverTx執行每筆交易並將其記錄到區塊中(一筆交易執行一次DeliverTx);
  7. 調用EndBlock表示打包完成;
  8. 發起共識決議,提議者將新區塊廣播給其它驗證者;(共識決議在第5步完成)
  9. 其它驗證者接收到區塊後,調用DeliverTx執行每筆交易並校驗結果,若沒問題則廣播commit請求(預提交)和新區塊;
  10. 若節點收到超過2/3驗證者的commit請求,調用Commit方法,更新整個應用狀態。

假如將要打包的tx緩存起來,咱們就能夠在DeliverTx、EndBlock、Commit三個方法中選擇其一實際執行tx,可是通常來講,交易執行都是放在DeliverTx,比較符合語義。EndBlock用於更新共識參數和Val集合,Commit用於更新整個應用狀態(apphash),須要注意的是,本次提交的apphash若與上次提交的不一樣,則會繼續產生新的區塊(無論有沒有新交易,就算設置consensus.create_empty_blocks=false,tendermint也會產生空區塊,可參看 Enable no empty blocks #308 。),這彷佛是tendermint的有意設計,但不知爲什麼。

另Query方法接收的RequestQuery類型參數有Path和Data兩個字段,Path是string類型,Data是[]byte,應該是對應於Http的get、post。示例代碼中我是經過正則表達式解析Path查詢各種數據,其實如果複雜查詢/結構化查詢,仍是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:迴應。 ValidArgs要有定義Run[E],並與Args: cobra.OnlyValidArgs結合才起做用,表示參數值只能是預設值
 4     //ValidArgs: []string{"throw", "salvage", "reply", "bbalj"},
 5     //Args主要是用來校驗參數的
 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 }

本來我想實現交互模式(相似mysql>),但cobra彷佛沒有提供相關方法,咱們只好本身想辦法,須要注意的是須要自解析用戶輸入,好比用戶輸入有空格,該空格是分隔參數仍是參數內部的,要作區分。本來打算參考cobra解析命令行的源碼,發現實際解析使用的是spf13/pflag庫,而pflag只是增強了go標準庫flag,而flag庫也並無涉及到參數值自己的具體解析,這部分工做依靠的是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
View Code

如註釋所示,windows下是在exec_windows.go中實現,其它操做系統的實現沒找到,應該是使用其它語言編寫或直接調用的系統api。進exec_windows.go中,發現關鍵函數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 }
View Code

其中對雙引號作了處理,註釋中還提供了一個網址How Command Line Parameters Are Parsed,應該是關於這方面的算法說明,往後再看。


序列化

當咱們在說序列化的時候,咱們在說什麼。序列化說白了就是數據轉化,或者說一一對應的映射關係。就內存場景來講,一個對象序列化爲另外一個對象,本質上它們都同樣,都是存儲在內存中的0、1序列,只是同一個東西不一樣的數據表達。好比將一個數值序列化(或者說轉化)成字符串類型,或者將數值int32轉爲數值int8,那麼內存中的存儲空間和存儲數據都不會同樣,字符串還要看用的什麼編碼。再如咱們將一個對象序列化爲byte[],不一樣的方案會產生不一樣的結果。好比使用C指針將物理數據直接映射出來,或者以json方式序列化,或者protobuf序列化,會產生不一樣的byte[];反之亦然。

無論是json編碼仍是二進制編碼,物理上存儲的都是二進制,json編碼包含於二進制編碼,咱們能夠根據須要自定義二進制編碼,通常是爲了減小存儲佔用的空間。好比json編碼,對一、2等數值類型是按字符串格式編碼(如utf8格式,1編碼的就是0x31,12佔兩個字節0x310x32),而咱們自定義二進制,徹底能夠把12存儲在一個字節裏面,該字節值就是數值自己;就算不是數值,而是字符串自己編碼,咱們也能夠在utf8編碼後再壓縮,相似gzip。

go中的序列化方式,可參看 Golang 序列化方式及對比,可是文中gob的測試代碼其實能夠改良下,將enc/dec兩個變量移到循環外,如此可在循環內複用,這將發揮gob上下文的優點。

protobuf的變長編碼針對的是數值類型,so應該只對數值字段多的類型有壓縮的意義。

go對字符串是utf8編碼,基本不用擔憂中文亂碼問題。


vscode-go開發環境

在國內,搭建Go開發環境都不會太順利,下面我就說說在vscode中搭建環境可能會遇到的問題和解決方法。

Go開發環境須要vscode安裝一些插件,而項目中也有引用的類庫,這二者均可能涉及到相關站點在牆外的狀況,而咱們也要分別設置代理。首先,給vscode自己設置代理,使得安裝插件沒有問題;其次,在命令行窗口設置http_proxy,使得dep順利進行。也能夠在vscode終端窗口設置http_proxy(vscode的終端就是個命令行交互環境,使用的仍是操做系統的shell,本質上獨立於vscode),但博主發現彷佛並不起做用。

在代理什麼都設置好後,vscode安裝插件時仍可能遇到問題,好比文件中已經存在的golang.org\x\tools目錄關聯的git源碼網址不是插件要求的源碼網址,緣由多是以前手動到github裏下載的tools源碼,將tools目錄移除從新跑一遍安裝插件的步驟便可。

安裝goimports時可能會timeout等錯誤,參考 安裝goimports 解決。

項目方面,具體到咱們這個demo,遵守tendermint官方文檔,make get_tools。我是windows10系統,使用bash命令進入到自帶的Ubuntu子系統,就可使用內置的make了。須要注意的是,若設置了系統變量GOPATH,且是以分號分隔的多個文件夾,那麼切換到Ubuntu後,因爲linux系統是按冒號分隔的,因此它會把分號當作文件夾名的一部分,致使自動建立一些奇怪目錄。若是是其它windows系統,能夠安裝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): 系統找不到指定的文件。

若是不是get_tools.sh的路徑問題,那就應該是bash衝突了(好比系統中安裝了git,同時把git目錄也配置到PATH下,實際定位的可能就是git的bash了)。

注意tendermint所需的最低Go版本。

咱們要嚴格遵循Go的目錄規範,若將代碼直接置於src\目錄下,則執行dep相關操做時,會拋出「root project import: dep does not currently support using GOPATH/src as the project root」錯誤。須要在src\下再建一個目錄,把代碼拷進這個子目錄再執行dep。Go遵循約定大於配置的原則,它在項目中引入全部依賴類庫的代碼,而這些類庫也是放置於src目錄下,因此須要按子目錄分開。另關於依賴項搜尋Support vendor directory as $GOPATH/src/vendor #313 應該有參考價值,另可參看 dep init fails if in not in $GOPATH[...]/src/{somedir..} #148

dep彷佛會將GOPATH\src下的依賴也複製到vendor下,感受是否是沒這必要。

經驗:最好在項目剛開始搭建就 dep init,不然在代碼敲了一個階段後,已經import了多個外部依賴,當這時候再 dep init,若是出現錯誤,將不會生成Gopkg.toml,若是是由於版本問題致使的錯誤,你都沒辦法經過編輯Gopkg.toml的方式解決。好比我就遇到這種狀況,dep init -gopath, -gopath表示先去本地GOPATH目錄找依賴庫,找不到再去網上拉取,結果個人本地庫版本不是master分支,而貌似dep默認的就是master,致使「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)。此時也不是沒辦法,咱們能夠把入口函數main所在文件整個註釋掉,這樣dep就不會遍歷代碼文件,但仍然會生成Gopkg.toml,這個時候就能夠手動編輯約束版本號了。

go install 不會把vendor目錄下的全部包無腦打包進exe文件,而是會根據實際依賴打包,這樣也使得咱們能夠多個[子]項目使用同一個vendor,減少磁盤佔用和複用已下載的依賴包,而沒必要擔憂exe文件過大的問題。

目前vscode調試go尚不能支持交互模式的命令行調試,沒有如python那樣能夠在launch.json設置console屬性[爲externalTerminal]。


其它

做爲區塊鏈最普遍應用的數字貨幣已經再也不像不久之前同樣可以隨意撩撥投機者的神經,但這項技術在其它更實用的領域或許仍值得期待。好比區塊鏈的共識機制、區塊時間戳、防篡改特性,彷佛天生是爲知識產權保護打造的,然而迄今爲止市面上還沒有出現讓人眼前一亮的產品。前段時間看到一則新聞,說百度上線了一個保護圖片版權的區塊鏈項目「圖騰」,有興趣的同窗能夠去了解下。若是我要實現相似的知識產權鏈,會考慮文件類似度判別、[使用代幣]支付版權費及支付策略(買斷or按次付款等)等等,交易媒介和交易標的都在鏈上,造成閉環。鏈上閉環可不受外部實體困擾,以區塊鏈二代的明星特性「智能合約」爲例,一旦與外部有所關聯,就沒法保證合約的事務完整性,可參看我以前的觀點

Tendermint裏有不少ethereum的影子,好比gas、db的封裝等,部分思路和代碼應該是參考了ethereum的實現。

ethereum(以太坊)相關概念:

MPT:即Merkle Patricia Tree,是Merkle Tree 和Patricia Tree結合的產物。Patricia Tree又是Trie Tree的一種變化。參考資料:Trie原理以及應用於搜索提示以太坊MPT原理,你最值得看的一篇。這兩篇偏向於原理,若要了解具體細節,可看 乾貨 | Merkle Patricia Tree 詳解

叔區塊

gas:一直很好奇以太坊是怎麼作到計算實際使用gas量的,特別是有控制跳轉語句的時候,最可靠的方式是實際運行時實時計算gas,那這個應該是由EVM實現的。具體可看 以太坊虛擬機及交易的執行以太坊智能合約虛擬機(EVM)原理與實現

數據結構與存儲方式:以太坊源碼情景分析之數據結構[以太坊源代碼分析] II. 數據的呈現和組織,緩存和更新

我的認爲區塊鏈目前廣泛存在的問題:

  • 升級困難(側鏈?)
  • 維護困難(當單節點故障時,只能依靠該節點自身能力處理,對於普通用戶來講,無疑是棘手的)
  • 隨着時間的推移,數據量會變得愈來愈大,全節點將相應變少,最終造成某種意義上的中心化網絡

 

更多資料:

以太坊源碼深刻分析(7)-- 以太坊Downloader源碼分析

 

轉載請註明本文出處:http://www.javashuo.com/article/p-egfhuhoh-co.html

相關文章
相關標籤/搜索