代码及说明参考网址:https://hyperledger-fabric.readthedocs.io/en/latest/chaincode4ade.html
每个链码调用函数都会传递进去一个交易上下文对象"ctx",该对象通过GetStub()获取stub对象可以进一步去访问账本。
下列的案例实现一个资产管理链码,包含对资产账本的创建、初始化、读、更新、删除,检查一个资产是否存在,把资产从一个拥有者转给另一个拥有者。
1.创建代码
首先进入我们的工作根目录,这个根目录最好不要有其他文件,因为稍候我们会在这个根目录下去创建新的go文件,如/home/xxx/xxx,然后运行如下命令创建链码:
mkdir atcc
然后用如下命令来初创建模块和源文件。
go mod init main
touch atcc/atcc.go
2.编写代码
进入atcc文件夹,修改atcc.go,全部代码如下,各部分在注释说明功能和注意事项。
package atcc
// 导入必要的依赖
import (
"fmt"
"encoding/json"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
type SmartContract struct {
contractapi.Contract
}
// 定义资产的数据结构,并使用注解的方式来辅助序列化,marshal函数会使用字母序对key进行排序,这样可以
// 保证其序列化之后具有唯一性,即不会出现导出的json字符串中ID字段在Color字段前面这种情况,
// 这样做的主要原因是为了保证输入输出的唯一性,防止背书验证的时候失败。
type Asset struct {
AppraisedValue int `json:"AppraisedValue"`
Color string `json:"Color"`
ID string `json:"ID"`
Owner string `json:"Owner"`
Size int `json:"Size"`
}
// 使用数据对链码进行初始化。
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
assets := []Asset{
{ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
{ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
{ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
{ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
{ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
{ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
}
for _, asset := range assets {
// 序列化资产
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
// 按照id存储序列化后的资产
err = ctx.GetStub().PutState(asset.ID, assetJSON)
if err != nil {
return fmt.Errorf("failed to put to world state. %v", err)
}
}
return nil
}
// 通过传入参数来创建一个账本上不存在的资产,这里有在后面实现的方法AssetExists来检查是否存在某个key为id的资产。
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("the asset %s already exists", id)
}
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// 从账本中读取资产,调用GetState来实现
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read from world state: %v", err)
}
if assetJSON == nil {
return nil, fmt.Errorf("the asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// 更新资产,这里实现逻辑是根据传入参数创建一个新的资产并序列化,然后覆盖原来的资产。
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}
// overwriting original asset with new asset
asset := Asset{
ID: id,
Color: color,
Size: size,
Owner: owner,
AppraisedValue: appraisedValue,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// 删除资产,直接调用DelState函数来实现删除。
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("the asset %s does not exist", id)
}
return ctx.GetStub().DelState(id)
}
// 检查id对应的资产是否存在,判断能不能读取出value即可。
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read from world state: %v", err)
}
return assetJSON != nil, nil
}
// 资产转移,实质是修改资产结构体的owner字段。
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// 读取全部资产,调用GetStateByRange函数来获取账本上的全部记录。
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// range query with empty string for startKey and endKey does an
// open-ended query of all assets in the chaincode namespace.
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
以上是全部的功能了,但是在实际开发的时候可能不会将全部链码都放到同一个文件,而是拆分,这样也会方便单元测试,目前对于测试我还没有仔细研究过,但其实在fabric-samples/asset-transfer-basic/chaincode-go/chaincode下有一个mock类,大概扫了一眼感觉像是把很多fabricapi的方法写成了桩来让链码调用,有点DIY的意思,之后也会再去看看有没有类似Junit这样的测试库。
接下来需要写一个主类来调用我们写的智能合约实例化链码,在工作根目录,即atcc文件夹的上层新建assetsManager.go,写入如下内容:
package main
import (
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"main/atcc"
)
func main() {
assetChaincode, err := contractapi.NewChaincode(&atcc.SmartContract{})
if err != nil {
log.Panicf("Error creating atcc chaincode: %v", err)
}
if err := assetChaincode.Start(); err != nil {
log.Panicf("Error starting atcc chaincode: %v", err)
}
}
3.构建链码
用如下命令将非标准库依赖下载到当前路径中。
go mod tidy
go mod vendor
vendor文件夹中存储的就是外部依赖。
到这里链码的编写工作就完成了,接下来可以使用peer chaincode package命令和install命令进行打包和安装了。
4.打包链码
首先需要把core.yml文件放在当前工作目录下,否则无法完成打包,会报找不到core文件的错误,这里由于使用的是fabric-samples的test-network,所以可以直接把fabric-samples/config下的core.yaml复制到当前的工作目录下。
然后进行打包。
export CC_NAME=atcc
export CC_SRC_PATH=.
export CC_SRC_LANGUAGE=go
export CC_RUNTIME_LANGUAGE=golang
export CC_VERSION=1.0
然后打包:
peer lifecycle chaincode package ${CC_NAME}.tar.gz --path ${CC_SRC_PATH} --lang ${CC_RUNTIME_LANGUAGE} --label ${CC_NAME}_${CC_VERSION}
完成之后在当前目录下回出现一个atcc.tar.gz,说明我们的链码打包成功了。
之后就是安装和批准提交操作了,这部分的详情可以看我上一篇博客:
Fabric 2.0,不使用脚本的情况下启动test-network
5.使用链码
5.1.初始化账本
这里实质上是调用了我们在代码中写的InitLedger方法。
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" -C $CHANNEL_NAME -n ${CC_NAME} --peerAddresses localhost:7051 --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt --isInit -c '{"function":"InitLedger","Args":[]}'
我们也可以看出规律,调用的时候,只需要在最后-c后面写对应的方法名和参数即可。
5.2.获取当前所有资产
这里我们只需要修改一下上一条命令中最后的function对应的value,此外还需要去掉–isInit参数。
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" -C $CHANNEL_NAME -n ${CC_NAME} --peerAddresses localhost:7051 --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"GetAllAssets","Args":[]}'
可以得到输出:
2021-09-20 16:35:52.427 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 payload:"[{\"ID\":\"asset1\",\"color\":\"blue\",\"size\":5,\"owner\":\"Tomoko\",\"appraisedValue\":300},{\"ID\":\"asset2\",\"color\":\"red\",\"size\":5,\"owner\":\"Brad\",\"appraisedValue\":400},{\"ID\":\"asset3\",\"color\":\"green\",\"size\":10,\"owner\":\"Jin Soo\",\"appraisedValue\":500},{\"ID\":\"asset4\",\"color\":\"yellow\",\"size\":10,\"owner\":\"Max\",\"appraisedValue\":600},{\"ID\":\"asset5\",\"color\":\"black\",\"size\":15,\"owner\":\"Adriana\",\"appraisedValue\":700},{\"ID\":\"asset6\",\"color\":\"white\",\"size\":15,\"owner\":\"Michel\",\"appraisedValue\":800}]"
5.3.创建新的资产
这里我们测试一下调用函数时传入参数,创建一个ID为assets7、color为black,size为20,owner为zekdot,appraisedValue为1000的资产:
peer chaincode invoke -o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile "$ORDERER_CA" -C $CHANNEL_NAME -n ${CC_NAME} --peerAddresses localhost:7051 --tlsRootCertFiles organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt -c '{"function":"CreateAsset","Args":["assets7", "black", "20", "zekdot", "1000"]}'
然后再查看全部资产:
2021-09-20 16:40:03.852 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 payload:"[{\"ID\":\"asset1\",\"color\":\"blue\",\"size\":5,\"owner\":\"Tomoko\",\"appraisedValue\":300},{\"ID\":\"asset2\",\"color\":\"red\",\"size\":5,\"owner\":\"Brad\",\"appraisedValue\":400},{\"ID\":\"asset3\",\"color\":\"green\",\"size\":10,\"owner\":\"Jin Soo\",\"appraisedValue\":500},{\"ID\":\"asset4\",\"color\":\"yellow\",\"size\":10,\"owner\":\"Max\",\"appraisedValue\":600},{\"ID\":\"asset5\",\"color\":\"black\",\"size\":15,\"owner\":\"Adriana\",\"appraisedValue\":700},{\"ID\":\"asset6\",\"color\":\"white\",\"size\":15,\"owner\":\"Michel\",\"appraisedValue\":800},{\"ID\":\"assets7\",\"color\":\"black\",\"size\":20,\"owner\":\"zekdot\",\"appraisedValue\":1000}]"
可以看到我们的资产已经加入进去了,另一方面也可以看出,调用fabric链码传参时不管Go代码中是什么类型,传入时一律看作字符串。