說明指南本指南適用於剛開始使用Tendermint Core應用程式的初學者。不具有Tendermint Core的任何開發經驗。Tendermint Core是一個用Go語言開發的支援拜占庭容錯/BFT的區塊鏈中介軟體, 用於在一組節點之間安全地複製狀態機/FSM。透過遵循本指南,您將建立一個名為kvstore的Tendermint核心專案,這是一個(非常簡單的)分散式BFT鍵值儲存。
在Go中建立內建應用程式在與Tendermint Core相同的過程中執行應用程式將為您提供最佳效能。對於其他語言,您的應用程式必須透過TCP,Unix域套接字或gRPC與Tendermint Core進行通訊。1.1安裝Go請參考官方指南以安裝Go。確認您已安裝最新版本的Go:
$goversiongoversiongo1.12.7darwin/amd64確保已設定$GOPATH環境變數:
$echo$GOPATH/Users/melekes/go1.2建立一個新的Go專案我們將從建立一個新的Go專案開始。
$mkdir-p$GOPATH/src/github.com/me/kvstore$cd$GOPATH/src/github.com/me/kvstore在示例目錄中,建立一個main.go檔案,其內容如下:
packagemainimport("fmt")funcmain(){fmt.Println("Hello,TendermintCore")}行時,應該輸出“Hello,Tendermint Core”。
$gorunmain.goHello,TendermintCore1.3編寫Tendermint Core應用程式Tendermint Core透過應用程式區塊連結口(ABCI)與應用程式進行通訊。 所有訊息型別均在protobuf檔案中定義。 這使Tendermint Core可以執行以任何程式語言編寫的應用程式。建立一個名為app.go的檔案,其內容如下:
packagemainimport(abcitypes"github.com/tendermint/tendermint/abci/types")typeKVStoreApplicationstruct{}var_abcitypes.Application=(*KVStoreApplication)(nil)funcNewKVStoreApplication()*KVStoreApplication{return&KVStoreApplication{}}func(KVStoreApplication)
Info(reqabcitypes.RequestInfo)abcitypes.ResponseInfo{returnabcitypes.ResponseInfo{}}func(KVStoreApplication)
SetOption(reqabcitypes.RequestSetOption)abcitypes.ResponseSetOption{returnabcitypes.ResponseSetOption{}}func(KVStoreApplication)
DeliverTx(reqabcitypes.RequestDeliverTx)abcitypes.ResponseDeliverTx{returnabcitypes.ResponseDeliverTx{Code:0}}func(KVStoreApplication)
CheckTx(reqabcitypes.RequestCheckTx)abcitypes.ResponseCheckTx{returnabcitypes.ResponseCheckTx{Code:0}}func(KVStoreApplication)
Commit()abcitypes.ResponseCommit{returnabcitypes.ResponseCommit{}}func(KVStoreApplication)
Query(reqabcitypes.RequestQuery)abcitypes.ResponseQuery{returnabcitypes.ResponseQuery{Code:0}}func(KVStoreApplication)
InitChain(reqabcitypes.RequestInitChain)abcitypes.ResponseInitChain{returnabcitypes.ResponseInitChain{}}func(KVStoreApplication)BeginBlock(reqabcitypes.RequestBeginBlock)
abcitypes.ResponseBeginBlock{
returnabcitypes.ResponseBeginBlock{}}func(KVStoreApplication)
EndBlock(reqabcitypes.RequestEndBlock)abcitypes.ResponseEndBlock{returnabcitypes.ResponseEndBlock{}}現在我將詳細介紹每個方法,解釋何時呼叫它並新增所需的業務邏輯。1.3.1 CheckTx將新交易新增到Tendermint Core時,它將要求應用程式對其進行檢查(驗證格式,簽名等)。
func(app*KVStoreApplication)isValid(tx[]byte)(codeuint32){//checkformatparts:=bytes.Split(tx,[]byte("="))iflen(parts)!=2{return1}key,value:=parts[0],parts[1]//checkifthesamekey=valuealreadyexistserr:=app.db.View(func(txn*badger.Txn)error{item,err:=txn.Get(key)iferr!=nil&&err!=badger.ErrKeyNotFound{returnerr}iferr==nil{returnitem.Value(func(val[]byte)error{ifbytes.Equal(val,value){code=2}returnnil})}returnnil})iferr!=nil{panic(err)}returncode}func(app*KVStoreApplication)
CheckTx(reqabcitypes.RequestCheckTx)abcitypes.ResponseCheckTx{code:=app.isValid(req.Tx)returnabcitypes.ResponseCheckTx{Code:code,GasWanted:1}}如果還沒有編譯,不要擔心。如果事務的格式不為{bytes} = {bytes},我們將返回1程式碼。當相同的key=value已經存在(相同的鍵和值)時,我們返回2程式碼。 對於其他,我們返回0程式碼,表明它們是有效的。請注意,Tendermint Core會將具有非零程式碼的任何內容視為無效(-1、100等)。只要交易量不大且有足夠的天然氣,最終將進行有效交易。對於基礎鍵值儲存,我們將使用Bader,它是一個可嵌入,持久且快速的鍵值(KV)資料庫。
import"github.com/dgraph-io/badger"typeKVStoreApplicationstruct{db*badger.DBcurrentBatch*badger.Txn}funcNewKVStoreApplication(db*badger.DB)*KVStoreApplication{return&KVStoreApplication{db:db,}}1.3.2 BeginBlock-> DeliverTx-> EndBlock->Commit當Tendermint Core確定了區塊後,它會分為3部分轉移到應用程式中:BeginBlock,每個事務一個DeliverTx和最後的EndBlock。DeliverTx正在非同步傳輸,但預期響應會按順序進行。
func(app*KVStoreApplication)
BeginBlock(reqabcitypes.RequestBeginBlock)abcitypes.ResponseBeginBlock{app.currentBatch=app.db.NewTransaction(true)returnabcitypes.ResponseBeginBlock{}}在這裡我們建立一個批處理,它將儲存block的事務。
func(app*KVStoreApplication)DeliverTx(reqabcitypes.RequestDeliverTx)abcitypes.ResponseDeliverTx{code:=app.isValid(req.Tx)ifcode!=0{returnabcitypes.ResponseDeliverTx{Code:code}}parts:=bytes.Split(req.Tx,[]byte("="))key,value:=parts[0],parts[1]err:=app.currentBatch.Set(key,value)iferr!=nil{panic(err)}returnabcitypes.ResponseDeliverTx{Code:0}}如果交易格式錯誤或已經存在相同的key = value,我們將再次返回非零程式碼。 否則,我們將其新增到當前批處理中。在當前設計中,一個區塊可能包含不正確的事務(那些透過CheckTx,但DeliverTx失敗的事務或提議者直接包含的事務)。 這樣做是出於效能原因。請注意,我們無法在DeliverTx內提交事務,因為在這種情況下,可以並行呼叫的查詢將返回不一致的資料(即使尚未提交實際區塊,它也會報告某些值已經存在)。Commit指示應用程式保持新狀態。
func(app*KVStoreApplication)Commit()abcitypes.ResponseCommit{app.currentBatch.Commit()returnabcitypes.ResponseCommit{Data:[]byte{}}}1.3.3查詢現在,當客戶端想知道何時存在特定的Key/Value時,它將呼叫Tendermint Core RPC/abci_query端點,該端點又將呼叫應用程式的Query方法。應用程式可以免費提供自己的API。但是透過使用Tendermint Core作為代理,客戶端(包括輕客戶端軟體包)可以在不同應用程式之間利用統一的API。 另外他們無需呼叫另外單獨的Tendermint Core API即可獲得其他證明。請注意,我們此處不包含證明。
func(app*KVStoreApplication)
Query(reqQueryabcitypes.RequestQuery)(resQueryabcitypes.ResponseQuery){resQuery.Key=reqQuery.Dataerr:=app.db.View(func(txn*badger.Txn)error{item,err:=txn.Get(reqQuery.Data)iferr!=nil&&err!=badger.ErrKeyNotFound{returnerr}iferr==badger.ErrKeyNotFound{resQuery.Log="doesnotexist"}else{returnitem.Value(func(val[]byte)error{resQuery.Log="exists"resQuery.Value=valreturnnil})}returnnil})iferr!=nil{panic(err)}return}1.4在同一過程中啟動應用程式和Tendermint Core例項將以下程式碼放入“ main.go”檔案中:
packagemainimport("flag""fmt""os""os/signal""path/filepath""syscall""github.com/dgraph-io/badger""github.com/pkg/errors""github.com/spf13/viper"abci"github.com/tendermint/tendermint/abci/types"cfg"github.com/tendermint/tendermint/config"tmflags"github.com/tendermint/tendermint/libs/cli/flags""github.com/tendermint/tendermint/libs/log"nm"github.com/tendermint/tendermint/node""github.com/tendermint/tendermint/p2p""github.com/tendermint/tendermint/privval""github.com/tendermint/tendermint/proxy")varconfigFilestringfuncinit(){flag.StringVar(&configFile,"config","$HOME/.tendermint/config/config.toml","Pathtoconfig.toml")}funcmain(){db,err:=badger.Open(badger.DefaultOptions("/tmp/badger"))iferr!=nil{fmt.Fprintf(os.Stderr,"failedtoopenbadgerdb:%v",err)os.Exit(1)}deferdb.Close()app:=NewKVStoreApplication(db)flag.Parse()node,err:=newTendermint(app,configFile)iferr!=nil{fmt.Fprintf(os.Stderr,"%v",err)os.Exit(2)}node.Start()deferfunc(){node.Stop()node.Wait()}()c:=make(chanos.Signal,1)signal.Notify(c,os.Interrupt,syscall.SIGTERM)<-cos.Exit(0)}funcnewTendermint(appabci.Application,configFilestring)(*nm.Node,error){//readconfigconfig:=cfg.DefaultConfig()config.RootDir=filepath.Dir(filepath.Dir(configFile))viper.SetConfigFile(configFile)iferr:=viper.ReadInConfig();err!=nil{returnnil,errors.Wrap(err,"viperfailedtoreadconfigfile")}iferr:=viper.Unmarshal(config);err!=nil{returnnil,errors.Wrap(err,"viperfailedtounmarshalconfig")}iferr:=config.ValidateBasic();err!=nil{returnnil,errors.Wrap(err,"configisinvalid")}//createloggerlogger:=log.NewTMLogger(log.NewSyncWriter(os.Stdout))varerrerrorlogger,err=tmflags.ParseLogLevel(config.LogLevel,logger,cfg.DefaultLogLevel())iferr!=nil{returnnil,errors.Wrap(err,"failedtoparseloglevel")}//readprivatevalidatorpv:=privval.LoadFilePV(config.PrivValidatorKeyFile(),config.PrivValidatorStateFile(),)//readnodekeynodeKey,err:=p2p.LoadNodeKey(config.NodeKeyFile())iferr!=nil{returnnil,errors.Wrap(err,"failedtoloadnode'skey")}//createnodenode,err:=nm.NewNode(config,pv,nodeKey,proxy.NewLocalClientCreator(app),nm.DefaultGenesisDocProviderFunc(config),nm.DefaultDBProvider,nm.DefaultMetricsProvider(config.Instrumentation),logger)iferr!=nil{returnnil,errors.Wrap(err,"failedtocreatenewTendermintnode")}returnnode,nil}
這是一個巨大的程式碼塊,所以讓我們將其分解為幾部分。首先,我們初始化Badger資料庫並建立一個應用程式例項:
db,err:=badger.Open(badger.DefaultOptions("/tmp/badger"))iferr!=nil{fmt.Fprintf(os.Stderr,"failedtoopenbadgerdb:%v",err)os.Exit(1)}deferdb.Close()app:=NewKVStoreApplication(db)然後我們使用它來建立一個Tendermint Core Node例項:
flag.Parse()node,err:=newTendermint(app,configFile)iferr!=nil{fmt.Fprintf(os.Stderr,"%v",err)os.Exit(2)}...//createnodenode,err:=nm.NewNode(config,pv,nodeKey,proxy.NewLocalClientCreator(app),nm.DefaultGenesisDocProviderFunc(config),nm.DefaultDBProvider,nm.DefaultMetricsProvider(config.Instrumentation),logger)iferr!=nil{returnnil,errors.Wrap(err,"failedtocreatenewTendermintnode")}NewNode需要一些東西,包括配置檔案,私有驗證節點,節點金鑰和其他一些東西,才能構建完整的節點。請注意,此處我們使用proxy.NewLocalClientCreator建立本地客戶端,而不是透過套接字或gRPC進行通訊的客戶端。viper被用於讀取配置,我們稍後將使用tendermint init命令生成該配置。
config:=cfg.DefaultConfig()config.RootDir=filepath.Dir(filepath.Dir(configFile))viper.SetConfigFile(configFile)iferr:=viper.ReadInConfig();err!=nil{returnnil,errors.Wrap(err,"viperfailedtoreadconfigfile")}iferr:=viper.Unmarshal(config);err!=nil{returnnil,errors.Wrap(err,"viperfailedtounmarshalconfig")}iferr:=config.ValidateBasic();err!=nil{returnnil,errors.Wrap(err,"configisinvalid")}我們使用FilePV,它是一個私有驗證器(即簽署共識訊息的東西)。通常您會使用SignerRemote連線到外部HSM。
pv:=privval.LoadFilePV(config.PrivValidatorKeyFile(),config.PrivValidatorStateFile(),)需要nodeKey來識別p2p網路中的節點。
nodeKey,err:=p2p.LoadNodeKey(config.NodeKeyFile())iferr!=nil{returnnil,errors.Wrap(err,"failedtoloadnode'skey")}至於logger,我們使用內建庫,它提供了一個很好的抽象over-go-kit的logger。
logger:=log.NewTMLogger(log.NewSyncWriter(os.Stdout))varerrerrorlogger,err=tmflags.ParseLogLevel(config.LogLevel,logger,cfg.DefaultLogLevel())iferr!=nil{returnnil,errors.Wrap(err,"failedtoparseloglevel")}最後,我們啟動節點並新增一些訊號處理,以在收到SIGTERM或Ctrl-C後正常停止它。
node.Start()deferfunc(){node.Stop()node.Wait()}()c:=make(chanos.Signal,1)signal.Notify(c,os.Interrupt,syscall.SIGTERM)<-cos.Exit(0)1.5啟動並執行我們將使用Go模組進行依賴項管理。
$exportGO111MODULE=on$gomodinitgithub.com/me/example$gobuild這應該構建二進位制檔案。要建立預設配置,nodeKey和專用驗證器檔案,讓我們執行bidmint init。 但是在執行此操作之前,我們將需要安裝Tendermint Core。
$rm-rf/tmp/example$cd$GOPATH/src/github.com/tendermint/tendermint$makeinstall$TMHOME="/tmp/example"tendermintinitI[2019-07-16|18:40:36.480]Generatedprivatevalidatormodule=mainkeyFile=/tmp/example/config/priv_validator_key.jsonstateFile=/tmp/example2/data/priv_validator_state.jsonI[2019-07-16|18:40:36.481]Generatednodekeymodule=mainpath=/tmp/example/config/node_key.jsonI[2019-07-16|18:40:36.482]Generatedgenesisfilemodule=mainpath=/tmp/example/config/genesis.json我們準備開始我們的應用程式:
$./example-config"/tmp/example/config/config.toml"badger2019/07/1618:42:25INFO:All0tablesopenedin0sbadger2019/07/1618:42:25INFO:Replayingfileid:0atoffset:0badger2019/07/1618:42:25INFO:Replaytook:695.227sE[2019-07-16|18:42:25.818]Couldn'tconnecttoanyseedsmodule=p2pI[2019-07-16|18:42:26.853]Executedblockmodule=stateheight=1validTxs=0invalidTxs=0I[2019-07-16|18:42:26.865]Committedstatemodule=stateheight=1txs=0appHash=現在在終端中開啟另一個標籤,然後嘗試傳送交易:
$curl-s'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"'{"jsonrpc":"2.0","id":"","result":{"check_tx":{"gasWanted":"1"},"deliver_tx":{},"hash":"1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6","height":"128"}}響應中應包含提交此事務的高度。現在檢查給定Key是否存在及其Value:
$curl-s'localhost:26657/abci_query?data="tendermint"'{"jsonrpc":"2.0","id":"","result":{"response":{"log":"exists","key":"dGVuZGVybWludA==","value":"cm9ja3M="}}}“ dGVuZGVybWludA ==”和“ cm9ja3M =”分別是“ tendermint”和“ rocks”的ASCII的base64編碼。我希望一切順利並且您的第一個(但不是最後一個)Tendermint Core應用程式已啟動並正在執行。