13.5 应用开发案例二:资产权属管理

通过智能合约进行资产权属管理是许多区块链应用场景的基础。

本节将以大理石的权属管理为例,介绍如何在链码中定义一种资产,并围绕这种资产提供创建、查询、转移所有权等操作。

与案例一相比,该案例的链码使用了shim.ChaincodeSubInterface中更为丰富的API。链码代码可参考examples/chaincode/go/marbles02/marbles_chaincode.go。

13.5.1 链码结构

与案例一类似,该链码的主要结构如下所示:


package main

// 引入必要的包
import (
    "bytes"
    "encoding/json"
    "fmt"
    "strconv"
    "strings"
    "time"

    "github.com/hyperledger/fabric/core/chaincode/shim"
    pb "github.com/hyperledger/fabric/protos/peer"
)

// 声明名为SimpleChaincode的结构体
type SimpleChaincode struct {
}

// 声明大理石(marble)结构体
type marble struct {
    ObjectType string `json:"docType"`
    Name       string `json:"name"`
    Color      string `json:"color"`
    Size       int    `json:"size"`
    Owner      string `json:"owner"`
}

// 主函数,需要调用shim.Start()方法
func main() {
    err := shim.Start(new(SimpleChaincode))
    if err != nil {
        fmt.Printf("Error starting Simple chaincode: %s", err)
    }
}

// 为SimpleChaincode添加Init方法
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
    // 不做具体处理
    return shim.Success(nil)
}

// 为SimpleChaincode添加Invoke方法
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    function, args := stub.GetFunctionAndParameters()
    fmt.Println("invoke is running " + function)

    // 定位到不同的分支处理逻辑
    if function == "initMarble" {
        return t.initMarble(stub, args)
    } else if function == "transferMarble" {
        return t.transferMarble(stub, args)
    } else if function == "transferMarblesBasedOnColor" {
        return t.transferMarblesBasedOnColor(stub, args)
    } else if function == "delete" {
        return t.delete(stub, args)
    } else if function == "readMarble" {
        return t.readMarble(stub, args)
    } else if function == "queryMarblesByOwner" {
        return t.queryMarblesByOwner(stub, args)
    } else if function == "queryMarbles" {
        return t.queryMarbles(stub, args)
    } else if function == "getHistoryForMarble" {
        return t.getHistoryForMarble(stub, args)
    } else if function == "getMarblesByRange" {
        return t.getMarblesByRange(stub, args)
    }

    fmt.Println("invoke did not find func: " + function) // error
    return shim.Error("Received unknown function invocation")
}

在链码中,可以自定义结构体类型来表示一种资产,并设定资产的各种属性。本例中定义了大理石(marble)资产,其属性包括类型、名称、颜色、尺寸、拥有者。具体映射到代码中,对marble类型的声明如下:


type marble struct {
    ObjectType string `json:"docType"`
    Name       string `json:"name"`
    Color      string `json:"color"`
    Size       int    `json:"size"`
    Owner      string `json:"owner"`
}

可以看到,marble包含5个成员,分别对应各个属性。注意,这里为每一个成员变量设定了标签(如json:"docType"),用于指定将结构体序列化成特定格式(如JSON)时该字段的键的名称。

13.5.2 Invoke方法

链码的Init方法中未进行任何处理,Invoke方法中则包含了9个分支方法。各分支方法的功能和参数示例如表13-6所示。

表13-6 Invoke方法中的9个分支方法总结

13.5 应用开发案例二:资产权属管理

13.5 应用开发案例二:资产权属管理

下面对分支方法逐一进行介绍。

1.initMarble方法

initMarble方法根据输入参数创建一个大理石,并写入账本。

方法接受4个参数,依次表示大理石名称、颜色、尺寸、拥有者名称。例如,如果调用链码时指定参数{"Args": ["initMarble","marble1","blue","35","tom"]},则功能为创建并记录一个名称为marble1、蓝色、尺寸为 35的大理石,拥有者为tom。

读取参数后,首先使用stub.GetState()进行查重。如果同样名称的大理石在账本中已经存在,则返回error的Response:


// 检查大理石是否已经存在
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)
}

创建相应的marble类型变量,并用json.Marshal()方法将其序列化到JSON对象中。自定义类型的变量序列化之后才可以写入账本,同理,对于从账本中读取出的信息需要反序列化后才便于进行操作:


// 创建marble,并序列化为JSON对象
objectType := "marble"
marble := &marble{objectType, marbleName, color, size, owner}
marbleJSONasBytes, err := json.Marshal(marble)
if err != nil {
    return shim.Error(err.Error())
}

之后,用stub.PutState()将序列化后的内容写入账本,以大理石名称marbleName为键:


// 将marbleJSONasBytes存入状态
err = stub.PutState(marbleName, marbleJSONasBytes)
if err != nil {
    return shim.Error(err.Error())
}

代码中同时加入了与复合键(composite key)、范围查找相关的功能,读者可以继续观察并学习代码中如何调用相应的stub方法。

在initMarble中,为了支持之后针对某一特定颜色的大理石进行范围查找,需要将该大理石的颜色与名称这两个属性 组合起来创建一个复合键,并记录在账本中。这里,复合键的意义是将一部分属性也构造为了索引的一部分,使得针对这部分属性做查询时,可以直接根据索引返回 查询结果,而不需要具体提取完整信息来作比对:


indexName := "color~name"
colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string
    {marble.Color, marble.Name})
if err != nil {
    return shim.Error(err.Error())
}

这里调用了stub的CreateCompositeKey方法来创建复合键。该方法格式为 CreateComposite-Key(objectType string,attributes[]string)(string,error),实际上会将objectType和attributes中的每个 string串联起来,中间用U+0000分割;同时在开头加上\x00,标明该键为复合键。

最后,以复合键为键,以0x00为值,将复合键记录入账本中:


value := []byte{0x00}
stub.PutState(colorNameIndexKey, value)

2.readMarble方法

根据大理石名称,readMarble方法会在账本中查询并返回大理石信息。

方法接受1个参数,即大理石名称。例如,如果调用链码时指定参数{"Args":["readMarble","marble1"]},则功能为查找名称为marble1的大理石,如果找到,返回其信息:


valAsbytes, err := stub.GetState(name)
if err != nil {
    jsonResp = "{\"Error\":\"Failed to get state for " + name + "\"}"
    return shim.Error(jsonResp)
} else if valAsbytes == nil {
    jsonResp = "{\"Error\":\"Marble does not exist: " + name + "\"}"
    return shim.Error(jsonResp)
}

return shim.Success(valAsbytes)

3.delete方法

根据大理石名称,delete方法会在账本中删除大理石信息。

方法接受1个参数,即大理石名称。例如,如果调用链码时指定参数{"Args":["delete","marble1"]},则功能为删除名称为marble1的大理石的信息。

除了删除以大理石名称为键的状态,还需删除该大理石的颜色与名称复合键。所以方法中第一步需要读取该大理石的颜色:


var marbleJSON marble

valAsbytes, err := stub.GetState(marbleName)
if err != nil {
    jsonResp = "{\"Error\":\"Failed to get state for " + marbleName + "\"}"
    return shim.Error(jsonResp)
} else if valAsbytes == nil {
    jsonResp = "{\"Error\":\"Marble does not exist: " + marbleName + "\"}"
    return shim.Error(jsonResp)
}

err = json.Unmarshal([]byte(valAsbytes), &marbleJSON)
if err != nil {
    jsonResp = "{\"Error\":\"Failed to decode JSON of: " + marbleName + "\"}"
    return shim.Error(jsonResp)
}

其中用json.Unmarshal方法将从账本中读取到的值反序列化为marble类型变量marbleJSON。则大理石颜色为marbleJSON.Color。

删除以大理石名称为键的状态:


err = stub.DelState(marbleName)
if err != nil {
    return shim.Error("Failed to delete state:" + err.Error())
}

删除以大理石的颜色与名称复合键为键的状态:


indexName := "color~name"
colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marbleJSON.
    Color, marbleJSON.Name})
if err != nil {
    return shim.Error(err.Error())
}

err = stub.DelState(colorNameIndexKey)
if err != nil {
    return shim.Error("Failed to delete state:" + err.Error())
}

4.transferMarble方法

transferMarble方法用于更改一个大理石的拥有者。

方法接受两个参数,依次为大理石名称和新拥有者名称。例如,如果调用链码时指定参数{"Args":["transferMarble","marble2","jerry"]},则功能是将名称为marble2的大理石的拥有者改为jerry。

首先用stub.GetState()方法从账本中取得信息,再用json.Unmarshal()方法将其反序列化为marble类型:


marbleAsBytes, err := stub.GetState(marbleName)
if err != nil {
    return shim.Error("Failed to get marble:" + err.Error())
} else if marbleAsBytes == nil {
    return shim.Error("Marble does not exist")
}

marbleToTransfer := marble{}
err = json.Unmarshal(marbleAsBytes, &marbleToTransfer)
if err != nil {
    return shim.Error(err.Error())
}

更改大理石的拥有者:


marbleToTransfer.Owner = newOwner

最后将更改后的状态写入账本:


marbleJSONasBytes, _ := json.Marshal(marbleToTransfer)
err = stub.PutState(marbleName, marbleJSONasBytes)
if err != nil {
    return shim.Error(err.Error())
}

5.getMarblesByRange方法

给定大理石名称的起始和终止,getMarblesByRange可以进行范围查询,返回所有名称在指定范围内的大理石信息。

方法接受两个参数,依次为字典序范围的起始(包括)和终止(不包括)。例如,调用链码时可以指定参数{"Args":["getMarblesByRange","marble1","marble3"]}进行范围查询,返回查找到的结果的键值。

方法中调用了stub.GetStateByRange(startKey,endKey)进行范围查询,其返回结果是一个迭代器StateQueryIteratorInterface结构,可以按照字典序迭代每个键值对,最后需调用Close()方法关闭:


resultsIterator, err := stub.GetStateByRange(startKey, endKey)
if err != nil {
    return shim.Error(err.Error())
}
defer resultsIterator.Close()

通过迭代器的迭代构造出查询结果的JSON数组,最后通过shim.Success()方法来返回结果:


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\":")
    // 记录本身就是一个 JSON 对象
    buffer.WriteString(string(queryResponse.Value))
    buffer.WriteString("}")
    bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

fmt.Printf("- getMarblesByRange queryResult:\n%s\n", buffer.String())

return shim.Success(buffer.Bytes())

6.transferMarblesBasedOnColor方法

transferMarblesBasedOnColor用于将指定颜色大理石的所有权全部更新为指定用户。

方法接受两个参数,依次为大理石颜色、目标拥有者名称。例如,调用链码时可以指定参数{"Args":["transferMarblesBasedOnColor","blue","jerry"]},将所有蓝色大理石的拥有者都改为jerry。

该方法的重点在于查找到所有蓝色大理石,这里便用到了之前创建的复合键。给定复合键的前 缀,stub.GetStateByPartialCompositeKey方法可以返回所有满足条件的键值对。其返回结果也是一个迭代器 StateQueryIteratorInterface结构,可以按照字典序迭代每个键值对:


coloredMarbleResultsIterator, err := stub.GetStateByPartialCompositeKey("
    color~name", []string{color})
if err != nil {
    return shim.Error(err.Error())
}
defer coloredMarbleResultsIterator.Close()

可以观察GetStateByPartialCompositeKey的参数,回忆之前创建复合键的过程,这里指定了前 缀为当时设定的objectType("color~name")加上attributes的第一个string(color的值)。事实 上,GetStateByPartialCompositeKey在实现上是以复合键前缀为起始,前缀加utf8.MaxRune为终止,通过调用范围查 找返回的匹配结果。

接下来迭代所有匹配的大理石,并更新拥有者:


var i int
for i = 0; coloredMarbleResultsIterator.HasNext(); i++ {
    responseRange, err := coloredMarbleResultsIterator.Next()
    if err != nil {
        return shim.Error(err.Error())
    }

    // 得到color~name复合键中的颜色和名称的值
    objectType, compositeKeyParts, err := stub.SplitCompositeKey(response-Range.Key)
    if err != nil {
        return shim.Error(err.Error())
    }
    returnedColor := compositeKeyParts[0]
    returnedMarbleName := compositeKeyParts[1]
    fmt.Printf("- found a marble from index:%s color:%s name:%s\n", object-Type,
        returnedColor, returnedMarbleName)

    response := t.transferMarble(stub, []string{returnedMarbleName, newOwner})
    if response.Status != shim.OK {
        return shim.Error("Transfer failed: " + response.Message)
    }
}

responsePayload := fmt.Sprintf("Transferred %d %s marbles to %s", i, color, newOwner)
fmt.Println("- end transferMarblesBasedOnColor: " + responsePayload)
return shim.Success([]byte(responsePayload))

注意,对于每一次迭代,这里使用stub.SplitCompositeKey()方法拆分了复合键,得到构造复合键时所用的各个attributes,即大理石颜色和名称。得到名称后,通过内部调用transfer-Marble()方法更新拥有者。

7.queryMarbles方法

如果使用支持富查询的数据库(如CouchDB)作为状态数据库,则可以进行规则更为复杂的富查询(richquery)。

这里需要使用的stub方法是stub.GetQueryResult,格式为 GetQueryResult(query string)(StateQueryIteratorInterface,error)。传入的参数为富查询指令字符串,具体语法和使用的数据库有关; 返回结果为迭代器结构StateQueryIteratorInterface。

以目前支持的CouchDB为例,富查询的语法可以参考CouchDB官方文档关于Selector语法部分的介绍:http://docs.couchdb.org/en/2.0.0/api/database/find.html#find-selectors

举例来讲,对于GetQueryResult方法,如果传入参数为"{"selector": {"owner":"tom"}}",则表示查询拥有者为tom的所有大理石;如果传入参数为"{"selector":{"$and": [{"size":{"$gte":2}},{"size":{"$lte":10}},{"$nor":[{"size":3}, {"size":5},{"size":7}]}]}}",则表示查询所有满足size>=2且size<=10且size不等于3、5、7 的大理石。

注意,stub.GetQueryResult方法不会被Committer重新执行进行验证,因此,不应该被用于更新账本状态的交易中,建议只用于查询状态。

本例的queryMarbles方法对stub.GetQueryResult做了封装,将所有通过富查询查询到的结果组合为一个JSON数组,最后返回。核心代码如下:


resultsIterator, err := stub.GetQueryResult(queryString)
if err != nil {
    return nil, err
}
defer resultsIterator.Close()

var buffer bytes.Buffer
buffer.WriteString("[")

bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
    queryResponse, err := resultsIterator.Next()
    if err != nil {
        return nil, err
    }
    if bArrayMemberAlreadyWritten == true {
        buffer.WriteString(",")
    }
    buffer.WriteString("{\"Key\":")
    buffer.WriteString("\"")
    buffer.WriteString(queryResponse.Key)
    buffer.WriteString("\"")

    buffer.WriteString(", \"Record\":")
    // 记录本身就是一个JSON对象
    buffer.WriteString(string(queryResponse.Value))
    buffer.WriteString("}")
    bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

fmt.Printf("- getQueryResultForQueryString queryResult:\n%s\n", buffer.String())

return buffer.Bytes(), nil

8.queryMarblesByOwner方法

queryMarblesByOwner方法使用富查询,返回所有属于指定用户的大理石信息。

具体,根据传入的拥有者参数owner,构造富查询指令字符串如下:


queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"marble\",\"owner\":\
    "%s\"}}", owner)

之后进行富查询,返回值的构造格式同上。

9.getHistoryForMarble方法

getHistoryForMarble用于对一个大理石的历史信息进行查询。

这里使用了stub的stub.GetHistoryForKey方法,能够返回某个键的所有历史值。格式为 GetHistoryForKey(key string)(HistoryQueryIteratorInterface,error),输入参数为键,返回结果是一个迭代器 HistoryQueryIteratorInterface结构,可以迭代该状态的每个历史值,还包括每个历史值的交易ID和时间戳信息:


resultsIterator, err := stub.GetHistoryForKey(marbleName)
if err != nil {
    return shim.Error(err.Error())
}
defer resultsIterator.Close()

之后通过历史值的迭代,构造出包含完整历史值、更新时对应的交易ID和时间戳的JSON数组,最后返回结果:


var buffer bytes.Buffer
buffer.WriteString("[")

bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
    response, err := resultsIterator.Next()
    if err != nil {
        return shim.Error(err.Error())
    }

    if bArrayMemberAlreadyWritten == true {
        buffer.WriteString(",")
    }
    buffer.WriteString("{\"TxId\":")
    buffer.WriteString("\"")
    buffer.WriteString(response.TxId)
    buffer.WriteString("\"")

    buffer.WriteString(", \"Value\":")
    // 如果是删除操作,则将对应的历史值记为null
    if response.IsDelete {
        buffer.WriteString("null")
    } else {
        buffer.WriteString(string(response.Value))
    }
    buffer.WriteString(", \"Timestamp\":")
    buffer.WriteString("\"")
    buffer.WriteString(time.Unix(response.Timestamp.Seconds, int64(response.Timestamp.
        Nanos)).String())
    buffer.WriteString("\"")

    buffer.WriteString(", \"IsDelete\":")
    buffer.WriteString("\"")
    buffer.WriteString(strconv.FormatBool(response.IsDelete))
    buffer.WriteString("\"")

    buffer.WriteString("}")
    bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

fmt.Printf("- getHistoryForMarble returning:\n%s\n", buffer.String())

return shim.Success(buffer.Bytes())


来源:我是码农,转载请保留出处和链接!

本文链接:http://www.54manong.com/?id=893

'); (window.slotbydup = window.slotbydup || []).push({ id: "u3646208", container: s }); })();
'); (window.slotbydup = window.slotbydup || []).push({ id: "u3646147", container: s }); })();