1. 程式人生 > >HyperLedger Fabric鏈碼開發及測試

HyperLedger Fabric鏈碼開發及測試

1.鏈碼開發

先設計一個簡單的應用場景,假設有這樣的業務需求:

  1. 可以新增學校,資訊包括學校名稱、學校ID;
  2. 新增該學校的學生,資訊包括姓名,使用者ID,所在學校ID,所在班級名稱;
  3. 更新學生資訊;
  4. 根據學生ID查詢學生資訊;
  5. 根據學生ID刪除學生資訊;
  6. 根據學校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

,來建立聯合主鍵。其實Fabric就是用U+0000來把各個聯合主鍵的欄位拼接起來,因為這個字元太特殊,所以很適合,對應的拆分聯合主鍵的欄位的方法是SplitCompositeKey

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