HyperLedger Fabric 1.0的Transaction處理流程

深藍發表於2017-06-24

如果把區塊鏈比作一個只能讀寫,不能刪改的分散式資料庫的話,那麼事務和查詢就是對這個資料庫進行的最重要的操作。以比特幣來說,我們通過錢包或者Blockchain.info進行區塊鏈的查詢操作,而轉賬行為就是Transaction的處理。而HyperLedger Fabric在1.0對系統架構進行了升級,使得事務的處理更加複雜。

一、架構

讓我們來看看Fabric 0.6到1.0的架構圖:

image

這個圖來自IBM微課堂第三講,我們可以看到原來單一的peer節點在1.0中進行了拆分,分為peer(背書節點和提交節點)和orderer(排序節點)。membership也就是我們在1.0中說的CA節點,其中也涉及到很多密碼學和安全相關的知識,我們暫且按住不表,只說SDK、Peer和Orderer之間的關係。

二、賬本

要了解Fabric對事務的處理,首先我們需要了解Fabric中的賬本,也就是實際儲存和查詢資料的地方。這是IBM微講堂中對Fabric賬本的示意圖:

image

Fabric 1.0中的賬本分為3種:

  1. 區塊鏈資料,這是用檔案系統儲存在Committer節點上的。區塊鏈中儲存了Transaction的讀寫集。
  2. 為了檢索區塊鏈的方便,所以用LevelDB對其中的Transaction進行了索引。
  3. ChainCode操作的實際資料儲存在State Database中,這是一個Key Value的資料庫,預設採用的LevelDB,現在1.0也支援使用CouchDB作為State Database。

三、事務提交過程

瞭解了Fabric中的賬本,接下來我們來了解一下對這些賬本的操作涉及到的Transaction。

我們仍然以Example02為例,具體準備過程可參看我之前的部落格:http://www.cnblogs.com/studyzy/p/6973334.html

當執行a向b轉賬10元,我們在cli中執行的命令為:

peer chaincode invoke -o orderer.example.com:7050  --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem  -C mychannel -n devincc -c '{"Args":["invoke","a","b","10"]}'

 

當CLI中執行該命令時,發生了什麼呢?我們來看看IBM微講堂中PPT關於事務生命週期和相關賬本的示例圖:
image
其中peer chaincode invoke表明這是一個Transaction呼叫。-c '{"Args":["invoke","a","b","10"]}'中的”invoke”說明呼叫的是example02.go中的invoke函式,具體函式我們可以看看到底實現了什麼功能:
// Transaction makes payment of X units from A to B

func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response {

var A, B string // Entities

var Aval, Bval int // Asset holdings

var X int // Transaction value

var err error

if len(args) != 3 {

return shim.Error("Incorrect number of arguments. Expecting 3")

    }

A = args[0]

B = args[1]

// Get the state from the ledger

// TODO: will be nice to have a GetAllState call to ledger

Avalbytes, err := stub.GetState(A)

if err != nil {

return shim.Error("Failed to get state")

    }

if Avalbytes == nil {

return shim.Error("Entity not found")

    }

Aval, _ = strconv.Atoi(string(Avalbytes))

Bvalbytes, err := stub.GetState(B)

if err != nil {

return shim.Error("Failed to get state")

    }

if Bvalbytes == nil {

return shim.Error("Entity not found")

    }

Bval, _ = strconv.Atoi(string(Bvalbytes))

// Perform the execution

X, err = strconv.Atoi(args[2])

if err != nil {

return shim.Error("Invalid transaction amount, expecting a integer value")

    }

Aval = Aval - X

Bval = Bval + X

    fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)

// Write the state back to the ledger

err = stub.PutState(A, []byte(strconv.Itoa(Aval)))

if err != nil {

return shim.Error(err.Error())

    }

err = stub.PutState(B, []byte(strconv.Itoa(Bval)))

if err != nil {

return shim.Error(err.Error())

    }

return shim.Success(nil)

}
其中主要的4個關於StateDatabase呼叫是:
Avalbytes, err := stub.GetState(A)
Bvalbytes, err := stub.GetState(B)
err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
err = stub.PutState(B, []byte(strconv.Itoa(Bval)))

1.客戶端SDK把'{"Args":["invoke","a","b","10"]}'這些引數傳送到endorser peer節點,
2.endorser peer會與ChainCode的docker例項通訊,併為其提供模擬的State Database的讀寫集,也就是說ChainCode會執行完邏輯,但是並不會在stub.PutState的時候寫資料庫。
3.endorser把這些讀寫集連同簽名返回給Client SDK。
4.SDK再把讀寫集傳送給Orderer節點,Orderer節點是進行共識的排序節點,在測試的情況下,只啟動一個orderer節點,沒有容錯。在生產環境,要進行Crash容錯,需要啟用Zookeeper和Kafka。在1.0中移除了拜占庭容錯,沒有0.6的PBFT,也沒有傳說中的SBFT,不得不說是一個遺憾。
5.Orderer節點只是負責排序和打包工作,處理的結果是一個Batch的Transactions,也就是一個Block,這個Block的產生有兩種情況,一種情況是Transaction很多,Block的大小達到了設定的大小,而另一種情況是Transaction很少,沒有達到設定的大小,那麼Orderer就會等,等到大小足夠大或者超時時間。這些設定是在configtx.yaml中設定的。
6.打包好的一堆Transactions會傳送給Committer Peer提交節點,
7.提交節點收到Orderer節點的資料後,會先進行VSCC校驗,檢查Block的資料是否正確。接下來是對每個Transaction的驗證,主要是驗證Transaction中的讀寫資料集是否與State Database的資料版本一致。驗證完Block中的所有Transactions後,提交節點會把吧Block寫入區塊鏈。然後把所有驗證通過的Transaction的讀寫集中的寫的部分寫入State Database。另外對於區塊鏈,本身是檔案系統,不是資料庫,所有也會有把區塊中的資料在LevelDB中建立索引。

四、查詢

如果我們只是通過ChainCode查詢資料,而存在寫入資料,那麼會有什麼區別呢?在CLI中peer命令提供了query子命令,比如Example02中,查詢a賬戶的餘額是:

peer chaincode query -C mychannel -n devincc -c '{"Args":["query","a"]}'
這樣系統會呼叫ChainCode中的invoke函式,但是傳入的function name是query。也就是會執行如下程式碼:
} else if function == "query" {

// the old "Query" is now implemtned in invoke

return t.query(stub, args)

}

// query callback representing the query of a chaincode

func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {

var A string // Entities

var err error

if len(args) != 1 {

return shim.Error("Incorrect number of arguments. Expecting name of the person to query")

}

A = args[0]

// Get the state from the ledger

Avalbytes, err := stub.GetState(A)

if err != nil {

jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}"

return shim.Error(jsonResp)

}

if Avalbytes == nil {

jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}"

return shim.Error(jsonResp)

}

jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}"

fmt.Printf("Query Response:%s\n", jsonResp)

return shim.Success(Avalbytes)

}

我們可以看到,我們只是呼叫了stub.GetState(A),並沒有寫操作,那麼會像前面說的Transaction一樣那麼複雜嗎?答案是不會。

因為呼叫呼叫的是peer query,在程式碼中,只有invoke的時候才會執行Transaction步驟中的4、5、6、7.

但是如果我們使用peer invoke,那麼會怎麼樣呢?比如如下的命令:

peer chaincode invoke -o orderer.example.com:7050  --tls $CORE_PEER_TLS_ENABLED --cafile /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/ordererOrganizations/example.com/orderers/orderer.example.com/msp/cacerts/ca.example.com-cert.pem  -C mychannel -n mycc -c '{"Args":["query","a"]}'

那麼從程式碼上來看,雖然我們是一個查詢,卻會以Transaction的生命週期來處理。

五、小結

通過對這個Transaction過程的分析,我們可以得出以下結論:
  1. Fabric不支援對同一個資料的併發事務處理,也就是說,如果我們同時執行了a->b 10元,b->a 10元,那麼只會第一條Transaction成功,而第二條失敗。因為在Committer節點進行讀寫集版本驗證的時候,第二條Transaction會驗證失敗。這是我完全無法接受的一點!
  2. Fabric是非同步的系統,在Endorser的時候a->b 10元,b->a 10元都會返回給SDK成功,而第二條Transaction在Committer驗證失敗後不進行State Database的寫入,但是並不會通知Client SDK,所以必須使用EventHub通知Client或者Client重新查詢才能知道是否寫入成功。
  3. 不管在提交節點對事務的讀寫資料版本驗證是否通過,因為Block已經在Orderer節點生成了,所以Block是被整塊寫入區塊鏈的,而在State Database不會寫入,所以會在Transaction之外的地方標識該Transaction是無效的。
  4. query沒有獨立的函式出來,並不是根據只有讀集沒有寫集而判斷是query還是Transaction。

相關文章