搭建基于hyperledger fabric的联盟社区(四) --chaincode开发
前几章已经分别把三台虚拟机环境和配置文件准备好了,在启动fabric网络之前我们要准备好写好的chaincode。chaincode的开发一般是使用GO或者JAVA,而我选择的是GO语言。先分析一下官方最典型的一个chaincode--fabcar,然后着重介绍一下shim.ChaincodeSubInterface,最后在贴上我自己的chaincode。
一.chaincode主要框架结构
1.1 引入了4个程序库,用于格式化、处理字节、读取和写入JSON,以及字符串操作。2个Hyperledger Fabric 特定的智能合同库。shim包提供了一些 API,以便chaincode与底层区块链网络交互来访问状态变量、交易上下文、调用方证书和属性,并调用其他chaincode和执行其他操作。
import ( "bytes" "encoding/json" "fmt" "strconv" "github.com/hyperledger/fabric/core/chaincode/shim" sc "github.com/hyperledger/fabric/protos/peer" )
1.2 定义SmartContract结构体,然后在该struct上定义Init和Invoke两个函数
type SmartContract struct { }
1.3 定义具有四个变量的汽车结构体,结构体标签被编码/json库使用。
type Car struct { Make string `json:"make"` Model string `json:"model"` Colour string `json:"colour"` Owner string `json:"owner"` }
1.4 当智能合同“fabcar”由区块链网络实例化时,Init方法被调用。在Go中通过给函数标明所属类型,来给该类型定义方法,下面的 s *SmartContract即表示给SmartContract声明了一个方法。在Init和Invoke的时候,都会传入参数stub shim.ChaincodeStubInterface,这个参数提供的接口为我们编写ChainCode的业务逻辑提供了大量的实用方法。假设一切顺利,将返回一个表示初始化已经成功的sc.Response对象。
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response { return shim.Success(nil) }
1.5 应用程序请求运行智能合约fabcar后,Invoke方法被调用。在Invoke的时候,由传入的参数来决定我们具体调用了哪个方法,所以需要用GetFunctionAndParameters解析调用的时候传入的参数。
func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response { // 检索请求的智能合约的函数和参数,GetFunctionAndParameters() (string, []string)将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter function, args := APIstub.GetFunctionAndParameters() // 找到合适的处理函数以便与账本进行交互 if function == "queryCar" { return s.queryCar(APIstub, args) } else if function == "initLedger" { return s.initLedger(APIstub) } else if function == "createCar" { return s.createCar(APIstub, args) } else if function == "queryAllCars" { return s.queryAllCars(APIstub) } else if function == "changeCarOwner" { return s.changeCarOwner(APIstub, args) } return shim.Error("Invalid Smart Contract function name.") }
1.6 主函数仅与单元测试模式相关,任何GO程序的起点都是main函数,在这里只是为了完整性。
func main() { // 创建了新的智能合同,并向对等节点注册它 err := shim.Start(new(SmartContract)) if err != nil { fmt.Printf("Error creating new Smart Contract: %s", err) } }
二.对State DB (状态数据库)的增删改查
2.1 查询某辆汽车
通过GetState(key string) ([]byte, error)查询数据。因为我们是Key Value数据库,所以根据Key来对数据库进行查询,是一件很常见,很高效的操作,返回的数据是byte数组。
func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 1 { return shim.Error("Incorrect number of arguments. Expecting 1") } carAsBytes, _ := APIstub.GetState(args[0]) return shim.Success(carAsBytes) }
2.2 初始化账本
通过PutState(key string, value []byte) error 增改数据,对于State DB来说,增加和修改数据是统一的操作,因为State DB是一个Key Value数据库,如果我们指定的Key在数据库中已经存在,那么就是修改操作,如果Key不存在,那么就是插入操作。对于实际的系统来说,我们的Key可能是单据编号,或者系统分配的自增ID+实体类型作为前缀,而Value则是一个对象经过JSON序列号后的字符串。
func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response { cars := []Car{ Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"}, Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"}, Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"}, Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"}, Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"}, Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"}, Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"}, Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"}, Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"}, Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"}, } i := 0 for i < len(cars) { fmt.Println("i is ", i) carAsBytes, _ := json.Marshal(cars[i]) APIstub.PutState("CAR"+strconv.Itoa(i), carAsBytes) fmt.Println("Added", cars[i]) i = i + 1 } return shim.Success(nil) }
2.3 创建汽车
把对象转换为JSON的方法(函数)为 json.Marshal(),也就是说,这个函数接收任意类型的数据 v,并转换为字节数组类型,返回值就是我们想要的JSON数据和一个错误代码。当转换成功的时候,这个错误代码为nil。
func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 5 { return shim.Error("Incorrect number of arguments. Expecting 5") } var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]} carAsBytes, _ := json.Marshal(car) APIstub.PutState(args[0], carAsBytes) return shim.Success(nil) }
2.4 查询所有汽车
Key区间查询GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) 提供了对某个区间的Key进行查询的接口,适用于任何State DB。由于返回的是一个StateQueryIteratorInterface(迭代器)接口,我们需要通过这个接口再做一个for循环,才能读取返回的信息,所有我们可以独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response { startKey := "CAR0" endKey := "CAR999" resultsIterator, err := APIstub.GetStateByRange(startKey, endKey) if err != nil { return shim.Error(err.Error()) } //defer关键字用来标记最后执行的Go语句,一般用在资源释放、关闭连接等操作,会在函数关闭前调用。多个defer的定义与执行类似于栈的操作:先进后出,最先定义的最后执行。 defer resultsIterator.Close() // buffer 是一个包含查询结果的JSON数组,bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte,这样直接定义一个 Buffer 变量,而不用初始化。 var buffer bytes.Buffer buffer.WriteString("["] bArrayMemberAlreadyWritten := false //迭代器的两个方法,hasNext:没有指针下移操作,只是判断是否存在下一个元素。next:指针下移,返回该指针所指向的元素。 for resultsIterator.HasNext() { queryResponse, err := resultsIterator.Next() if err != nil { return shim.Error(err.Error()) } // 在数组成员前加一个逗号 if bArrayMemberAlreadyWritten == true { buffer.WriteString(",") } buffer.WriteString("{\"Key\":") buffer.WriteString("\"") buffer.WriteString(queryResponse.Key) buffer.WriteString("\"") buffer.WriteString(", \"Record\":") // Record 是一个JSON对象,所以我们按原样写 buffer.WriteString(string(queryResponse.Value)) buffer.WriteString("}") bArrayMemberAlreadyWritten = true } buffer.WriteString("]") fmt.Printf("- queryAllCars:\n%s\n", buffer.String()) return shim.Success(buffer.Bytes()) }
2.5 改变汽车拥有人
Unmarshal是用于反序列化json的函数根据data将数据反序列化到传入的对象中。
func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 2 { return shim.Error("Incorrect number of arguments. Expecting 2") } carAsBytes, _ := APIstub.GetState(args[0]) car := Car{} json.Unmarshal(carAsBytes, &car) car.Owner = args[1] carAsBytes, _ = json.Marshal(car) APIstub.PutState(args[0], carAsBytes) return shim.Success(nil) }
以上就是官方链代码fabcar的分析
三.链码开发API
作为chaincode中的利器shim.ChaincodeSubInterface提供了一系列API供开发者在编写链码时灵活选择使用。
这些API可分为四类:账本状态交互API、交易信息相关API、参数读取API、其他API,下面分别介绍。
3.1账本状态交互API
chaincode需要将一些数据记录在分布式账本中。需要记录的数据称为状态state,以键值对(key-value)的形式存储。账本状态API可以对账本状态进行操作,十分重要。方法的调用会更新交易提案的读写集和,在committer进行验证时会在此执行,跟账本状态进行比对,这类API的大致功能如下:
3.2交易信息相关API
交易信息相关API可以获取到与交易信息自身相关的数据。用户对链码的调用(初始化和升级时调用Init()方法,运行时调用Invoke()方法)过程中会产生交易提案。这些API支持查询当前交易提案的一些属性,具体信息如下:
3.3 参数读取API
调用链码时支持传入若干参数,参数可通过API读取。具体信息如下:
3.4其他API
除了上面一些API以外还有一些辅助API,如下:
四. 社区联盟chaincode
4.1 业务功能需求
4.1.1 AddPost(Post)
本函数功能为存入一个帖子。
参数:
Post 为JSON 格式数据,包含帖子的的基本信息。须将字段名作为Key,字段的值作为 Value。
返回值:
-1:操作不成功
某个正整数:操作成功,返回值表示新帖的ID
4.1.2 UpdatePost(Post)
本函数功能为更改一个已经存在的帖子。
参数:
Post 为JSON 格式数据,包含帖子的的基本信息,其中帖子ID不可为空。须将字段名作为Key,字段的值作为 Value。
返回值:
-1:操作不成功
1:操作成功
4.1.3 RichQueryPost (Attribute, Operator, Value)
本函数功能为查找帖子。
参数:
Attribute 表示帖子的属性名称,须符合附件一中的字段名。
Operator 表示比较运算符的代码。如下所示
0: =
1: >
2: >=
3: <
4: <=
5: between
6: like
Value 为属性的值。若Operator 为0(等于),则本函数将返回对应属性中等于指定Value的所有帖子;若Operator为1~4,则Value 视为单个值(单个数值或单个字符串);若Operator为5,则Value须为两个值,中间以逗号分开;若Operator为6,则本函数将返回对应属性中包含指定Value的所有帖子。
返回值:
-1:操作不成功
JSON格式字符串:操作成功,所有符合条件的帖子将组织为JSON格式的数组,数组中每个元素为一个帖子。
注:目前仅支持单属性的查找。
4.1.4 GetPostNum(Attribute, Operator, Value)
本函数功能为查找返回某个条件的帖子的数量。
参数的含义与函数QueryPost相同。
返回值:
-1:操作不成功
某个正整数:操作成功,返回值符合查询条件的帖子的数量
4.2 编写chaincode遇到的问题
在实现有transaction功能的函数时,在函数里面写一个返回值,并不能像查询类型的函数一样在用fabric SDK调用时得到对应的返回值。addpost功能需要返回新帖子的ID,但是我的帖子id正好就是键值对的数量,我通过fabric node SDK 返回了键值对的数量,也就是返回了新帖子的ID。
RichQueryPost和GetPostNum两个函数都会用到富查询。富查询的语法可以参考couchdb官方文档关于Selector语法部分的介绍:
http://docs.couchdb.org/en/2.0.0/api/database/find.html#find-selectors
https://github.com/cloudant/mango
但是模糊查询功能暂时没有实现。(下个版本会实现)
4.3 chaincode代码
我们将自己编写符合业务逻辑的chaincode放在peer0.org1和peer0.org2的/go/src/github.com/hyperledger/fabric/examples/chaincode/go/community
目录下,之后启动docker容器的时候会自动挂载chaincode
cd ~/go/src/github.com/hyperledger/fabric/examples/chaincode/go/
mkdir community
完整chaincode代码如下
package main import ( "bytes" "encoding/json" "fmt" "strconv" "strings" "github.com/hyperledger/fabric/core/chaincode/shim" sc "github.com/hyperledger/fabric/protos/peer" ) type SmartContract struct { } type Post struct { Id string `json:"id"` OriginalWebsite string `json:"originalwebsite"` OriginalID string `json:"originalid"` Title string `json:"title"` Content string `json:"content"` AuthorId string `json:"authorid"` PublishTime string `json:"publishtime"` UpdateTime string `json:"updatetime"` Category string `json:"category"` SourceId string `json:"sourceid"` Labels string `json:"labels"` Follower_num int `json:"follower_num"` Browse_num int `json:"browse_num"` Star_num int `json:"star_num"` } type PostLength struct { Length int `json:"length"` } func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response { return shim.Success(nil) } func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response { function, args := APIstub.GetFunctionAndParameters() if function == "queryPost" { return s.queryPost(APIstub, args) } else if function == "initLedger" { return s.initLedger(APIstub) } else if function == "addPost" { return s.addPost(APIstub, args) } else if function == "updatePost" { return s.updatePost(APIstub, args) } else if function == "richQueryPosts" { return s.richQueryPosts(APIstub, args) } else if function == "getPostNum" { return s.getPostNum(APIstub, args) } return shim.Error("Invalid Smart Contract function name.") } func (s *SmartContract) queryPost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 1 { return shim.Error("Incorrect number of arguments. Expecting 1") } postAsBytes, _ := APIstub.GetState(args[0]) return shim.Success(postAsBytes) } func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response { posts := []Post{ Post{Id: "1", OriginalWebsite: "b", OriginalID: "c", Title: "d",Content:"e",AuthorId:"f",PublishTime:"g",UpdateTime:"h",Category:"i",SourceId:"j",Labels:"k",Follower_num:100,Browse_num:200,Star_num:300}, Post{Id: "2", OriginalWebsite: "bb", OriginalID: "bb", Title: "dd",Content:"ee",AuthorId:"ff",PublishTime:"gg",UpdateTime:"hh",Category:"ii",SourceId:"jj",Labels:"kk",Follower_num:400,Browse_num:500,Star_num:600}, } length := PostLength{Length:len(posts)} lengthAsBytes,_ := json.Marshal(length) APIstub.PutState("POSTLENGTH",lengthAsBytes) i := 0 for i < len(posts) { fmt.Println("i is ", i) postAsBytes, _ := json.Marshal(posts[i]) APIstub.PutState("POST"+strconv.Itoa(i), postAsBytes) fmt.Println("Added", posts[i]) i = i + 1 } return shim.Success(nil) } func (s *SmartContract) addPost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 13 { return shim.Error("Incorrect number of arguments. Expecting 13") } args10,error := strconv.Atoi(args[10]) args11,error := strconv.Atoi(args[11]) args12,error := strconv.Atoi(args[12]) if error != nil{ fmt.Println("String conversion integer failed!") } lengthAsBytes, _ := APIstub.GetState("POSTLENGTH") length := PostLength{} json.Unmarshal(lengthAsBytes,&length) newlength := length.Length+1 var post = Post{Id: strconv.Itoa(newlength), OriginalWebsite: args[0], OriginalID: args[1], Title: args[2],Content:args[3],AuthorId:args[4],PublishTime:args[5],UpdateTime:args[6],Category:args[7],SourceId:args[8],Labels:args[9],Follower_num:args10,Browse_num:args11,Star_num:args12} postAsBytes, _ := json.Marshal(post) APIstub.PutState("POST"+strconv.Itoa(newlength), postAsBytes) length.Length = newlength lengthAsBytes,_ = json.Marshal(length) APIstub.PutState("POSTLENGTH",lengthAsBytes) return shim.Success(lengthAsBytes) } func (s *SmartContract) updatePost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 14 { return shim.Error("Incorrect number of arguments. Expecting 14") } args11,error := strconv.Atoi(args[11]) args12,error := strconv.Atoi(args[12]) args13,error := strconv.Atoi(args[13]) if error != nil{ fmt.Println("String conversion integer failed!") } var post = Post{Id: args[0], OriginalWebsite: args[1], OriginalID: args[2], Title: args[3],Content:args[4],AuthorId:args[5],PublishTime:args[6],UpdateTime:args[7],Category:args[8],SourceId:args[9],Labels:args[10],Follower_num:args11,Browse_num:args12,Star_num:args13} postAsBytes, _ := json.Marshal(post) APIstub.PutState("POST"+args[0], postAsBytes) return shim.Success(nil) } func (s *SmartContract) richQueryPosts(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 3 { return shim.Error("Incorrect number of arguments. Expecting 3") } var queryString string if args[1] == "0" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":\"%s\"}}", args[0],args[2]) } else if args[1] == "1" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gt\":%s}}}", args[0],args[2]) } else if args[1] == "2" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gte\":%s}}}", args[0],args[2]) } else if args[1] == "3" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lt\":%s}}}", args[0],args[2]) } else if args[1] == "4" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lte\":%s}}}", args[0],args[2]) } else if args[1] == "5" { between := strings.Split(args[2], ",") queryString = fmt.Sprintf("{\"selector\":{\"$and\":[{\"%s\":{\"$gte\":%s}},{\"%s\":{\"$lte\":%s}}]}}", args[0],between[0],args[0],between[1]) } else { return shim.Error("Incorrect number of arguments. Expecting 0~5") } resultsIterator, err := APIstub.GetQueryResult(queryString) if err != nil { return shim.Error(err.Error()) } defer resultsIterator.Close() var buffer bytes.Buffer buffer.WriteString("[") bArrayMemberAlreadyWritten := false for resultsIterator.HasNext() { queryResponse, err := resultsIterator.Next() if err != nil { return shim.Error(err.Error()) } if bArrayMemberAlreadyWritten == true { buffer.WriteString(",") } buffer.WriteString("{\"Key\":") buffer.WriteString("\"") buffer.WriteString(queryResponse.Key) buffer.WriteString("\"") buffer.WriteString(", \"Record\":") buffer.WriteString(string(queryResponse.Value)) buffer.WriteString("}") bArrayMemberAlreadyWritten = true } buffer.WriteString("]") fmt.Printf("- richQueryPosts:\n%s\n", buffer.String()) return shim.Success(buffer.Bytes()) } func (s *SmartContract) getPostNum(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { if len(args) != 3 { return shim.Error("Incorrect number of arguments. Expecting 3") } var queryString string if args[1] == "0" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":\"%s\"}}", args[0],args[2]) } else if args[1] == "1" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gt\":%s}}}", args[0],args[2]) } else if args[1] == "2" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gte\":%s}}}", args[0],args[2]) } else if args[1] == "3" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lt\":%s}}}", args[0],args[2]) } else if args[1] == "4" { queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lte\":%s}}}", args[0],args[2]) } else if args[1] == "5" { between := strings.Split(args[2], ",") queryString = fmt.Sprintf("{\"selector\":{\"$and\":[{\"%s\":{\"$gte\":%s}},{\"%s\":{\"$lte\":%s}}]}}", args[0],between[0],args[0],between[1]) } else { return shim.Error("Incorrect number of arguments. Expecting 0~5") } resultsIterator, err := APIstub.GetQueryResult(queryString) if err != nil { return shim.Error(err.Error()) } defer resultsIterator.Close() i := 0 for resultsIterator.HasNext() { resultsIterator.Next() i = i + 1 } fmt.Printf("- getPostNum:\n%s\n", strconv.Itoa(i)) return shim.Success([]byte(strconv.Itoa(i))) } func main() { err := shim.Start(new(SmartContract)) if err != nil { fmt.Printf("Error creating new Smart Contract: %s", err) } }