HyperLedger Fabric链码开发及测试

xiaoxiao2021-02-28  7

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,来创建联合主键。其实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") }

完整代码:https://github.com/mh4u/chaincode_demo

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() } }

完整代码:https://github.com/mh4u/chaincode_demo

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
转载请注明原文地址: https://www.6miu.com/read-1900343.html

最新回复(0)