当前位置: 代码迷 >> 综合 >> hyperledger fabric 智能合约开发(三)------合约接口的数据操作方法
  详细解决方案

hyperledger fabric 智能合约开发(三)------合约接口的数据操作方法

热度:36   发布时间:2023-12-10 18:10:38.0

合约接口的数据操作方法

写入区块链数据如果是 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。

希望大家关注我的微信公众号,有疑问可以后台留言。

  相关解决方案