HyperLedger Fabric链码开发及测试

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
3
4
5
6
7
8
9
10
11
12
13
14
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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")
    }

扫描二维码关注公众号,回复: 8927445 查看本文章

    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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//将参数构造成学生结构体
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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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")

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
完整代码: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()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
完整代码: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
1
3.5 打开3个终端,每个都进入到chaincode-docker-devmode文件夹
终端 1 – 启动网络

$ docker-compose -f docker-compose-simple.yaml up
1
终端 2 – 编译和部署链码

$ docker exec -it chaincode bash 
$ cd Student
$ go build
$ CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./Student
1
2
3
4
终端3 – 使用链码
启动cli

$ docker exec -it cli bash
1
安装、实例化链码

$ cd ../
$ peer chaincode install -p chaincodedev/chaincode/Student -n mycc -v 0
$ peer chaincode instantiate -n mycc -v 0 -c '{"Args":[]}' -C myc
1
2
3
调用链码

$ 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
 

发布了290 篇原创文章 · 获赞 467 · 访问量 187万+

猜你喜欢

转载自blog.csdn.net/starzhou/article/details/104108227