HyperLedger Fabric鏈碼開發及測試
1.鏈碼開發
先設計一個簡單的應用場景,假設有這樣的業務需求:
- 可以新增學校,資訊包括學校名稱、學校ID;
- 新增該學校的學生,資訊包括姓名,使用者ID,所在學校ID,所在班級名稱;
- 更新學生資訊;
- 根據學生ID查詢學生資訊;
- 根據學生ID刪除學生資訊;
- 根據學校ID刪除學校資訊,包括該學校下的所有學生。
接下來開發滿足這些需求的鏈碼。關鍵程式碼如下
1.1 定義School,Student結構體
type StudentChaincode struct {
}
type Student struct {
UserId int `json:"user_id"` //學生id
Name string `json:"name"` //姓名
SchoolId string `json:"school_id"` //學校id
Class string `jsong:"class"` //班級名稱
}
type School struct {
SchoolId string `json:"id"` //學校id
School string `json:"name"` //學校名稱
}
1.2 部分業務需求的實現
1.2.1 新增學校
這裡用到了shim.ChaincodeStubInterface的CreateCompositeKey
func (t *StudentChaincode) initSchool(stub shim.ChaincodeStubInterface, args []string) pd.Response {
if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 2(school_id, school_name)" )
}
schoolId := args[0]
schoolName := args[1]
school := &School{schoolId, schoolName}
//這裡利用聯合主鍵,使得查詢school時,可以通過主鍵的“school”字首找到所有school
schoolKey, err := stub.CreateCompositeKey("School", []string{"school", schoolId})
if err != nil {
return shim.Error(err.Error())
}
//結構體轉json字串
schoolJSONasBytes, err := json.Marshal(school)
if err != nil {
return shim.Error(err.Error())
}
//儲存
err = stub.PutState(schoolKey, schoolJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(schoolJSONasBytes)
}
1.2.2 新增學生,需要檢查所屬學校是否已經存在。
抽出來的工具類方法querySchoolIds 中用到的GetStateByPartialCompositeKey 是對聯合主鍵進行字首匹配的查詢
func (t *StudentChaincode) addStudent(stub shim.ChaincodeStubInterface, args []string) pd.Response {
st, err := studentByArgs(args)
if err != nil {
return shim.Error(err.Error())
}
useridAsString := strconv.Itoa(st.UserId)
//檢查學校是否存在,不存在則新增失敗
schools := querySchoolIds(stub)
if len(schools) > 0 {
for _, schoolId := range schools {
if schoolId == st.SchoolId {
goto SchoolExists;
}
}
fmt.Println("school " + st.SchoolId+ " does not exist")
return shim.Error("school " + st.SchoolId+ " does not exist")
} else {
fmt.Println("school " + st.SchoolId+ " does not exist")
return shim.Error("school " + st.SchoolId+ " does not exist")
}
SchoolExists:
//檢查學生是否存在
studentAsBytes, err := stub.GetState(useridAsString)
if err != nil {
return shim.Error(err.Error())
} else if studentAsBytes != nil {
fmt.Println("This student already exists: " + useridAsString)
return shim.Error("This student already exists: " + useridAsString)
}
//結構體轉json字串
studentJSONasBytes, err := json.Marshal(st)
if err != nil {
return shim.Error(err.Error())
}
//儲存
err = stub.PutState(useridAsString, studentJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(studentJSONasBytes)
}
//將引數構造成學生結構體
func studentByArgs(args []string) (*Student, error) {
if len(args) != 4 {
return nil, errors.New("Incorrect number of arguments. Expecting 4(name, userid, schoolid, classid)")
}
name := args[0]
userId, err := strconv.Atoi(args[1]) //字串轉換int
if err != nil {
return nil, errors.New("2rd argument must be a numeric string")
}
schoolId := args[2]
class := args[3]
st := &Student{userId, name, schoolId, class}
return st, nil
}
//獲取所有建立的學校id
func querySchoolIds(stub shim.ChaincodeStubInterface) []string {
resultsIterator, err := stub.GetStateByPartialCompositeKey("School", []string{"school"})
if err != nil {
return nil
}
defer resultsIterator.Close()
scIds := make([]string,0)
for i := 0; resultsIterator.HasNext(); i++ {
responseRange, err := resultsIterator.Next()
if err != nil {
return nil
}
_, compositeKeyParts, err := stub.SplitCompositeKey(responseRange.Key)
if err != nil {
return nil
}
returnedSchoolId := compositeKeyParts[1]
scIds = append(scIds, returnedSchoolId)
}
return scIds
}
1.2.3 刪除學校,包括刪除所有對應學生資訊
刪除學校下的所有學生時,需要根據根據學校ID找到匹配的所有學生。這裡用到了富查詢方法GetQueryResult ,可以查詢value中匹配的項。但如果是LevelDB,那麼是不支援,只有CouchDB時才能用這個方法。
func (t *StudentChaincode) deleteSchool(stub shim.ChaincodeStubInterface, args []string) pd.Response {
if len(args) < 1 {
return shim.Error("Incorrect number of arguments. Expecting 1(schoolid)")
}
schoolidAsString := args[0]
schoolKey, err := stub.CreateCompositeKey("School", []string{"school", schoolidAsString})
if err != nil {
return shim.Error(err.Error())
}
schoolAsBytes, err := stub.GetState(schoolKey)
if err != nil {
return shim.Error("Failed to get school:" + err.Error())
} else if schoolAsBytes == nil {
return shim.Error("School does not exist")
}
//刪除學校
err = stub.DelState(schoolKey)
if err != nil {
return shim.Error("Failed to delete school:" + schoolidAsString + err.Error())
}
//刪除學校下的所有學生
queryString := fmt.Sprintf("{\"selector\":{\"school_id\":\"%s\"}}", schoolidAsString)
resultsIterator, err := stub.GetQueryResult(queryString)//富查詢,必須是CouchDB才行
if err != nil {
return shim.Error("Rich query failed")
}
defer resultsIterator.Close()
for i := 0; resultsIterator.HasNext(); i++ {
responseRange, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
err = stub.DelState(responseRange.Key)
if err != nil {
return shim.Error("Failed to delete student:" + responseRange.Key + err.Error())
}
}
return shim.Success(nil)
}
1.2.4 刪除學生
func (t *StudentChaincode) deleteStudent(stub shim.ChaincodeStubInterface, args []string) pd.Response {
if len(args) < 1 {
return shim.Error("Incorrect number of arguments. Expecting 1(userid)")
}
useridAsString := args[0]
studentAsBytes, err := stub.GetState(useridAsString)
if err != nil {
return shim.Error("Failed to get student:" + err.Error())
} else if studentAsBytes == nil {
return shim.Error("Student does not exist")
}
err = stub.DelState(useridAsString)
if err != nil {
return shim.Error("Failed to delete student:" + useridAsString + err.Error())
}
return shim.Success(nil)
}
1.2.5 更新學生資訊
對於State DB來說,增加和修改資料是統一的操作,因為State DB是一個Key Value資料庫,如果我們指定的Key在資料庫中已經存在,那麼就是修改操作,如果Key不存在,那麼就是插入操作。
func (t *StudentChaincode) updateStudent(stub shim.ChaincodeStubInterface, args []string) pd.Response {
st, err := studentByArgs(args)
if err != nil {
return shim.Error(err.Error())
}
useridAsString := strconv.Itoa(st.UserId)
//檢查學校是否存在,不存在則新增失敗
schools := querySchoolIds(stub)
if len(schools) > 0 {
for _, schoolId := range schools {
if schoolId == st.SchoolId {
goto SchoolExists;
}
}
fmt.Println("school " + st.SchoolId+ " does not exist")
return shim.Error("school " + st.SchoolId+ " does not exist")
} else {
fmt.Println("school " + st.SchoolId+ " does not exist")
return shim.Error("school " + st.SchoolId+ " does not exist")
}
SchoolExists:
//因為State DB是一個Key Value資料庫,如果我們指定的Key在資料庫中已經存在,那麼就是修改操作,如果Key不存在,那麼就是插入操作。
studentJSONasBytes, err := json.Marshal(st)
if err != nil {
return shim.Error(err.Error())
}
//儲存
err = stub.PutState(useridAsString, studentJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(studentJSONasBytes)
}
1.2.6 鏈碼的Init、Invoke實現
func (t *StudentChaincode) Init(stub shim.ChaincodeStubInterface) pd.Response {
return shim.Success(nil)
}
func (t *StudentChaincode) Invoke(stub shim.ChaincodeStubInterface) pd.Response {
fn, args := stub.GetFunctionAndParameters()
fmt.Println("invoke is running " + fn)
if fn == "initSchool" {
return t.initSchool(stub, args)
} else if fn == "addStudent" {
return t.addStudent(stub, args)
} else if fn == "queryStudentByID" {
return t.queryStudentByID(stub, args)
} else if fn == "deleteSchool" {
return t.deleteSchool(stub, args)
} else if fn == "updateStudent" {
return t.updateStudent(stub, args)
}
fmt.Println("invoke did not find func: " + fn)
return shim.Error("Received unknown function invocation")
}
2.單元測試
在開發完鏈碼後,我們並不需要在區塊鏈環境中部署鏈碼才能進行除錯。可以利用shim.MockStub 來編寫單元測試程式碼,直接在無網路的環境中debug。
先新建一個編寫測試程式碼的go檔案,如student_test.go。假如我們想測試前面的建立學習功能,可以這樣碼程式碼:
func TestInitSchool(t *testing.T) {
//模擬鏈碼部署
scc := new(StudentChaincode)
stub := shim.NewMockStub("StudentChaincode", scc)
mockInit(t, stub, nil)
//呼叫鏈碼
initSchool(t, stub, []string{"schoolId_A", "學校1"})
initSchool(t, stub, []string{"schoolId_B", "學校2"})
}
func mockInit(t *testing.T, stub *shim.MockStub, args [][]byte) {
res := stub.MockInit("1", args)
if res.Status != shim.OK {
fmt.Println("Init failed", string(res.Message))
t.FailNow()
}
}
func initSchool(t *testing.T, stub *shim.MockStub, args []string) {
res := stub.MockInvoke("1", [][]byte{[]byte("initSchool"), []byte(args[0]), []byte(args[1])})
if res.Status != shim.OK {
fmt.Println("InitSchool failed:", args[0], string(res.Message))
t.FailNow()
}
}
3. 開發環境測試鏈碼
經過單元測試後,我們還需要在區塊鏈開發環境中跑鏈碼進行測試。
3.1 下載fabric-samples
3.2 下載或拷貝二進位制檔案到fabric-samples目錄下
3.3 將開發好的鏈碼檔案拷貝到fabric-samples/chaincode目錄下
3.4 進入鏈碼開發環境目錄
cd fabric-samples/chaincode-docker-devmode
3.5 開啟3個終端,每個都進入到chaincode-docker-devmode資料夾
終端 1 – 啟動網路
$ docker-compose -f docker-compose-simple.yaml up
終端 2 – 編譯和部署鏈碼
$ docker exec -it chaincode bash
$ cd Student
$ go build
$ CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./Student
終端3 – 使用鏈碼
啟動cli
$ docker exec -it cli bash
安裝、例項化鏈碼
$ cd ../
$ peer chaincode install -p chaincodedev/chaincode/Student -n mycc -v 0
$ peer chaincode instantiate -n mycc -v 0 -c '{"Args":[]}' -C myc
呼叫鏈碼
$ peer chaincode invoke -n mycc -c '{"Args":["initSchool", "schoolId_A", "學校A"]}' -C myc
$ peer chaincode invoke -n mycc -c '{"Args":["addStudent", "張三", "1", "schoolId_A", "classA"]}' -C myc