搭建基于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)
	}
}

  

  

 

posted @ 2017-10-30 15:46  preminem  阅读(7403)  评论(0编辑  收藏  举报