合约接口的数据操作方法
写入区块链数据如果是 struct 结构体,需要序列化成二进制,通常使用 json,其他形式的序列化也可以,只要能反序列化即可(反序列化,是二进制数组变为格式化数据)。Hyperledger fabric levelDB提供基于key/value的数据存储,其中key是字符串,value则是二进制字节数组,Hyperledger的Go API提供了三个方法用于数据存取:PutState(key, value)用于向Hyperledger中存储数据, GetState(key)用于从Hyperledger中提取数据,而DelState(key)则从Hyperledger中删除数据。
1.PutState(key, value)写入区块,首先确定写入数据结构,其次从输入参数中获得string数据,可以选择检查数据是否存在,然后把输入参数写入数据结构,同时将数据结构(值)转化为json序列化数据,即二进制数组,将string类型key和序列化二进制数组写入数据库。
...
type marble struct {
ObjectType string `json:"docType"`
Name string `json:"name"`
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}
...
marbleName := args[0]
color := strings.ToLower(args[1])
owner := strings.ToLower(args[3])
size, err := strconv.Atoi(args[2])
if err != nil {
return shim.Error("3rd argument must be a numeric string")
}
...
//检查要输入的数据是否已经存在
marbleAsBytes, err := stub.GetState(marbleName)
if err != nil {
return shim.Error("Failed to get marble: " + err.Error())
} else if marbleAsBytes != nil {
fmt.Println("This marble already exists: " + marbleName)
return shim.Error("This marble already exists: " + marbleName)
}
...
objectType := "marble"
marble := &marble{objectType, marbleName, color, size, owner}
marbleJSONasBytes, err := json.Marshal(marble)
if err != nil {
return shim.Error(err.Error())
}
...
err = stub.PutState(marbleName, marbleJSONasBytes)
if err != nil {
return shim.Error(err.Error())
}
...
2.GetState(key) 读取区块,通过key获取区块信息,返回的信息是byte数组,我们需要Json反序列化,可以得到我们想要的对象具体信息。
dbStudentBytes,err:= stub.GetState(key)
var dbStudent Student;
err=json.Unmarshal(dbStudentBytes,&dbStudent)//反序列化
if err != nil {
return shim.Error("{\"Error\":\"Failed to decode JSON of: " + string(dbStudentBytes)+ "\" to Student}")
}
fmt.Println("Read Student from DB, name:"+dbStudent.Name)
【注意:不能在一个ChainCode函数中PutState后又马上GetState,这个时候GetState是没有最新值的,因为在这时Transaction并没有完成,还没有提交到StateDB里面】
3.传入参数stub shim.ChaincodeStubInterface,这个参数提供的接口为我们编写ChainCode的业务逻辑提供了大量实用的方法,如下:
GetArgs() [][]byte 以byte数组的数组的形式获得传入的参数列表
GetStringArgs() []string 以字符串数组的形式获得传入的参数列表
GetFunctionAndParameters() (string, []string) 将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter
GetArgsSlice() ([]byte, error) 以byte切片的形式获得参数列表
3.DelState(key) 删除区块,用来删除区块信息。根据Key删除State DB的数据。如果根据Key找不到对应的数据,删除失败。删除是删去stateDB数据库,而区块数据无法删除。
err= stub.DelState(key)
if err != nil {
return shim.Error("Failed to delete Student from DB, key is: "+key)
}
4.数据修改,State 数据库并没有提供修改功能,修改数据可以先读取,再修改,最后写入。
func (s *SmartContract) transferToken(stub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 3 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}
tokenAsBytes, _ := stub.GetState(args[0])
token := Token{}
json.Unmarshal(tokenAsBytes, &token)
token.transfer(args[1],args[2],args[3])
tokenAsBytes, _ = json.Marshal(token)
stub.PutState(args[0], tokenAsBytes)
return shim.Success(nil)
}
在区块链中,现在新增数据与原有的数据都存在,但在state DB只保存最新的数据,也就是最新的替代数据。
5.GetStateByRange(startKey, endKey) 范围查找,由于返回的是一个StateQueryIteratorInterface接口,我们在下例使用通用的接口而不是查询接口,我们需要通过这个接口再做一个for循环,才能读取返回的信息,所有我们可以独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。这是我们的转换方法:
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
startKey := "CAR0"
endKey := "CAR999"
//获取byte数组形式返回二进制数组
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()//关闭数据接受
//buffer是一组Json数组,需要转化为string格式数据
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\":")
//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())
}
6. 富查询GetQueryResult(query string) (StateQueryIteratorInterface, error),是对Value的内容进行查询,如果是LevelDB,那么是不支持,只有CouchDB时才能用这个方法。
关于传入的query这个字符串,其实是CouchDB所使用的Mango查询,我们可以在官方博客了解到一些信息:https://blog.couchdb.org/2016/08/03/feature-mango-query/ 其基本语法可以在https://github.com/cloudant/mango 这里看到。
func (t *SimpleChaincode) getQueryResult(stub shim.ChaincodeStubInterface, args []string) pb.Response{
name:="Neo Chen" //需要查询的名字
queryString := fmt.Sprintf("{\"selector\":{\"Name\":\"%s\"}}", name)
resultsIterator,err:= stub.GetQueryResult(queryString)//必须是CouchDB才行
if err!=nil{
return shim.Error("query failed")
}
person,err:=getListResult(resultsIterator)
if err!=nil{
return shim.Error("query failed")
}
return shim.Success(person)
}
7.历史数据查询GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error),比如之前的Student:1这个对象,我们更改和删除过数据,现在要查询这个对象的更改记录,直接返回区块链上数据,那么对应代码为:
func (t *SimpleChaincode) testHistoryQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{
student1:=Student{1,"Devin Zeng"}
key:="Student:"+strconv.Itoa(student1.Id)
it,err:= stub.GetHistoryForKey(key)
if err!=nil{
return shim.Error(err.Error())
}
var result,_= getHistoryListResult(it)
return shim.Success(result)
}
func getHistoryListResult(resultsIterator shim.HistoryQueryIteratorInterface) ([]byte,error){
defer resultsIterator.Close()
// buffer is a JSON array containing QueryRecords
var buffer bytes.Buffer
buffer.WriteString("[")
bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
// Add a comma before array members, suppress it for the first array member
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
item,_:= json.Marshal( queryResponse)
buffer.Write(item)
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("queryResult:\n%s\n", buffer.String())
return buffer.Bytes(), nil
}
8.生成复合键CreateCompositeKey(objectType string, attributes []string) (string, error),,ChainCode也为我们提供了生成Key的方法CreateCompositeKey,通过这个方法,我们可以将联合主键涉及到的属性都传进去,并声明了对象的类型即可。
以选课表为例,里面包含了以下属性:
type ChooseCourse struct {
CourseNumber string //开课编号
StudentId int //学生ID
Confirm bool //是否确认
}
其中CourseNumber+StudentId构成了这个对象的联合主键,我们要获得生成的复核主键,那么可写为:
cc:=ChooseCourse{"CS101",123,true}
var key1,_= stub.CreateCompositeKey("ChooseCourse",[]string{cc.CourseNumber,strconv.Itoa(cc.StudentId)})
fmt.Println(key1)
【注:其实Fabric就是用U+0000来把各个字段分割开的,因为这个字符太特殊,所以很适合做分割】
9.拆分复合键SplitCompositeKey(compositeKey string) (string, []string, error)
既然有组合那么就有拆分,当我们从数据库中获得了一个复合键的Key之后,怎么知道其具体是由哪些字段组成的呢。其实就是用U+0000把这个复合键再Split开,得到结果中第一个是objectType,剩下的就是复合键用到的列的值。
objType,attrArray,_:= stub.SplitCompositeKey(key1)
fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|"))
10. 部分复合键的查询GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
这里其实是一种对Key进行前缀匹配的查询,也就是说,我们虽然是部分复合键的查询,但是不允许拿后面部分的复合键进行匹配,必须是前面部分。
11.stub.SetEvent(key, value) 事件,Hyperledger Fabic 事件实现了发布/订阅消息队列。您可以自由地在链码中创建和发出自定义事件。例如,区块链的状态发生改变,就会生成一个事件。通过向区块链上的事件中心注册一个事件适配器,客户端应用程序可以订阅和使用这些事件。也就是说当ChainCode提交完毕,会通过Event的方式通知Client。而通知的内容可以通过SetEvent设置。事件设置完毕后,需要在客户端也做相应的修改。
func (t *SimpleChaincode) testEvent(stub shim.ChaincodeStubInterface, args []string) pb.Response{
message := "Event send data is here!"
err := stub.SetEvent("evtsender", []byte(message))
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(nil)
}
func (t *SimpleChaincode) testEvent(stub shim.ChaincodeStubInterface, args []string) pb.Response{
event := &Token{
Owner: "netkiller",
TotalSupply: 10000,
TokenName: "代币通正",
TokenSymbol: "COIN",
BalanceOf: map[string]uint{}}
eventBytes, err ;= json.Marshal(&event)
if err != nil {
return nil, err
}
err := stub.SetEvent("evtSender", eventBytes)
if err != nil {
fmt.Println("Could not set event for loan application creation", err)
}
}
12.stub.GetCreator() 获得证书资料,这个方法可以获得调用这个ChainCode的客户端的用户的证书,这里虽然返回的是byte数组,但是其实是一个字符串,内容格式如下:
-----BEGIN CERTIFICATE-----
MIICGjCCAcCgAwIBAgIRAMVe0+QZL+67Q+R2RmqsD90wCgYIKoZIzj0EAwIwczEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
cmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh
Lm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwODEyMTYyNTU1WhcNMjcwODEwMTYyNTU1
WjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN
U2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWVXNlcjFAb3JnMS5leGFtcGxlLmNvbTBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABN7WqfFwWWKynl9SI87byp0SZO6QU1hT
JRatYysXX5MJJRzvvVsSTsUzQh5jmgwkPbFcvk/x4W8lj5d2Tohff+WjTTBLMA4G
A1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIO2os1zK9BKe
Lb4P8lZOFU+3c0S5+jHnEILFWx2gNoLkMAoGCCqGSM49BAMCA0gAMEUCIQDAIDHK
gPZsgZjzNTkJgglZ7VgJLVFOuHgKWT9GbzhwBgIgE2YWoDpG0HuhB66UzlA+6QzJ
+jvM0tOVZuWyUIVmwBM=
-----END CERTIFICATE-----
我们常见的需求是在ChainCode中获得当前用户的信息,方便进行权限管理。那么我们怎么获得当前用户呢?我们可以把这个证书的字符串转换为Certificate对象。一旦转换成这个对象,我们就可以通过Subject获得当前用户的名字。
func (t *SimpleChaincode) testCertificate(stub shim.ChaincodeStubInterface, args []string) pb.Response{
creatorByte,_:= stub.GetCreator()
certStart := bytes.IndexAny(creatorByte, "-----BEGIN")
if certStart == -1 {
fmt.Errorf("No certificate found")
}
certText := creatorByte[certStart:]
bl, _ := pem.Decode(certText)
if bl == nil {
fmt.Errorf("Could not decode the PEM structure")
}
cert, err := x509.ParseCertificate(bl.Bytes)
if err != nil {
fmt.Errorf("ParseCertificate failed")
}
uname:=cert.Subject.CommonName
fmt.Println("Name:"+uname)
return shim.Success([]byte("Called testCertificate "+uname))
}
13.调用其他链码,我们的链上代码中调用别人已经部署好的链上代码。比如官方提供的example02,我们要在代码中去实现a->b的转账,这里需要注意,我们使用的是example02的链上代码的实例名mycc,而不是代码的名字example02,那么我们的代码应该如下:
func (t *SimpleChaincode) testInvokeChainCode(stub shim.ChaincodeStubInterface, args []string) pb.Response{
//全部转化byte数组,调用连码需要提供连码名和通道名 stub.InvokeChaincode("连码名",调用函数,"通道")
trans:=[][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("11")}
response:= stub.InvokeChaincode("mycc",trans,"mychannel")
fmt.Println(response.Message)
return shim.Success([]byte( response.Message))
}
例如以下程序
//将string参数转化为byte数组
func toChaincodeArgs(args ...string) [][]byte {
bargs := make([][]byte, len(args))
for i, arg := range args {
bargs[i] = []byte(arg)
}
return bargs
}
func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var sum, channelName string // Sum entity
var Aval int // value of sum entity - to be computed
chaincodeName := args[0]
sum = args[1]
f := "index"
queryArgs := toChaincodeArgs(f,sum) //进行byte[]转化
response := stub.InvokeChaincode(chaincodeName, queryArgs, channelName)
Aval, err = strconv.Atoi(string(response.Payload))
return shim.Success([]byte(strconv.Itoa(Aval)))
}
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
if function == "queryIndex" {
return t.query(stub, args)
}
return shim.Success([]byte("Invalid invoke function name. Expecting \"query\""))
}
14.获得提案对象Proposal属性, 获得签名的提案GetSignedProposal() (*pb.SignedProposal, error)
从客户端发现背书节点的Transaction或者Query都是一个提案,GetSignedProposal获得当前的提案对象包括客户端对这个提案的签名。提案的内容如果直接打印出来感觉就像是乱码,其内包含了提案Header,Payload和Extension,里面更包含了复杂的结构,这里不讲,以后可以写一篇博客专门研究提案对象。
15.获得Transient对象 GetTransient() (map[string][]byte, error),Transient是在提案中Payload对象中的一个属性,也就是ChaincodeProposalPayload.TransientMap
16.获得交易时间戳GetTxTimestamp() (*timestamp.Timestamp, error),交易时间戳也是在提案对象中获取的,提案对象的Header部分,也就是proposal.Header.ChannelHeader.Timestamp
17.获得Binding对象 GetBinding() ([]byte, error),这个Binding对象也是从提案对象中提取并组合出来的,其中包含proposal.Header中的SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch。
希望大家关注我的微信公众号,有疑问可以后台留言。