Hyperledger Fabric 链码开发实战——弹珠资产管理(Marbles)

1、本篇背景

Marbles是IBM开源的区块链项目,基本网络是Hyperledger Fabric,GitHub地址如下:

https://github.com/IBM-Blockchain/marbles
Marbles项目演示

有对应的中文翻译版,这里就不再详细介绍了。Marbles是一个非常简单的资产转移演示,多个用户可以创建并相互转移弹珠。

这里不作项目介绍和安装内容,本篇内容主要是参考Marbles项目进行链码的开发和测试,所以需要对Marbles有基本的了解。

假设我们从零开始用链码开发简单的弹珠资产转移功能,实现如下几个业务需求:

  1. 创建一个弹珠,弹珠字段包括弹珠ID、颜色、尺寸和拥有者;
  2. 通过弹珠ID查询对应的弹珠信息;
  3. 通过弹珠ID删除对应的弹珠;
  4. 改变弹珠的拥有者;
  5. 查询指定ID范围的弹珠信息;
  6. 查询某个拥有者的所有弹珠信息;
  7. 通过弹珠ID查询所有的交易历史信息。

2、链码开发

了解了上面的业务需求后,我们直接进入链码的开发。

先创建一个用于保存本次实验的项目文件夹,这里命名为"marbles_chaincode",在该目录下创建一个与文件夹同名的后缀名为".go"链码文件。

2.1 创建结构体

先创建"弹珠"以及链码的结构体

// 定义"弹珠"链码的结构体
type MarblesChaincode struct {
}

/*
 * 定义"弹珠"结构体
 * 字段包括ID、颜色、尺寸和拥有者等
 */
type Marble struct {
  ObjectType string `json:"objectType"` // CouchDB的字段
  ID string `json:"id"` // ID(唯一字符串,将用作键)
  Color string `json:"color"` // 颜色(字符串,CSS颜色名称)
  Size int `json:"size"`  //  尺寸(整型,以毫米为单位)
  Owner string `json:"owner"` //  拥有者(字符串)
}
2.2 实现Init函数和main函数

这里先使用实现Init函数和main函数,Invoke函数和其他跟业务有关的功能后面一起实现。

// 在链码初始化过程中调用Init来初始化任何数据
func (t *MarblesChaincode) Init (stub shim.ChaincodeStubInterface) pb.Response {
  fmt.Println("Marbles Init Success")
  return shim.Success(nil)
}

// 后面实现Invoke函数和其他功能
func (t *MarblesChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
  fmt.Println("Marbles Invoke")
    // ...
    return shim.Error("没找到对应方法~")
}

// Go语言的入口是main函数
func main() {
  err := shim.Start(new(MarblesChaincode))
  if err != nil {
    fmt.Printf("Error starting Marbles Chaincode - %s", err)
  }
}
2.3 初始化弹珠

初始化创建一个弹珠时,需要根据弹珠ID判断是否已经存在账本中,不存在则将通过传入的参数来创建Marble对象,然后转换为Json格式的内容写入到账本中。

func (t *MarblesChaincode) initMarble (stub shim.ChaincodeStubInterface, args []string) pb.Response {
  if len(args) != 4 {
    return shim.Error("Incorrect number of arguments. Expecting 4")
  }

  marbleObjectType := "marble"
  marbleId := args[0]
  // 查询账本中是否已经存在该弹珠ID的信息
  marbleIdAsBytes, err := stub.GetState(marbleId)
  if err != nil {
    return shim.Error(err.Error())
  }

  if marbleIdAsBytes != nil {
    return shim.Error("该Marble已存在~")
  }

  marbleColor := args[1]
  // 字符串转换为整型
  marbleSize, err := strconv.Atoi(args[2])
  if err != nil {
    return shim.Error("size字段为整型~")
  }
  marbleOwner := args[3]
  // 创建Marble对象
  marble := &Marble{marbleObjectType, marbleId, marbleColor, marbleSize, marbleOwner}
  // 转换为Json格式的内容
  marbleJsonAsBytes, err := json.Marshal(marble)
  // 写入到账本中
  err = stub.PutState(marbleId, marbleJsonAsBytes)
  if err != nil {
    return shim.Error(err.Error())
  }

  return shim.Success(nil)
}
2.4 通过弹珠ID查询对应的弹珠信息

通过shim包的 GetState API方法来获取账本中的弹珠信息

// 通过弹珠ID查询对应的弹珠信息
func (t *MarblesChaincode) getMarbleInfoByID (stub shim.ChaincodeStubInterface, args []string) pb.Response {
  if len(args) != 1 {
    return shim.Error("Incorrect number of arguments. Expecting 1")
  }

  marbleId := args[0]
  // 查询账本中是否已经存在该弹珠ID的信息
  marbleIdAsBytes, err := stub.GetState(marbleId)

  if err != nil {
    return shim.Error(err.Error())
  } else if marbleIdAsBytes == nil {
    return shim.Error("该Marble不存在~")
  }

  return shim.Success(marbleIdAsBytes)
}
2.5 通过弹珠ID删除对应的弹珠

先通过 GetState 判断账本中是否存在对应弹珠ID的信息,存在则通过 DelState 删除账本记录。

// 通过弹珠ID删除对应的弹珠
func (t *MarblesChaincode) deleteMarbleByID (stub shim.ChaincodeStubInterface, args []string) pb.Response {
  if len(args) != 1 {
    return shim.Error("Incorrect number of arguments. Expecting 1")
  }

  marbleId := args[0]
  // 查询账本中是否已经存在该弹珠ID的信息
  marbleIdAsBytes, err := stub.GetState(marbleId)

  if err != nil {
    return shim.Error(err.Error())
  } else if marbleIdAsBytes == nil {
    return shim.Error("该Marble不存在~")
  }

  // 删除账本中的记录
  err = stub.DelState(marbleId)
  if err != nil {
    return shim.Error(err.Error())
  }

  return shim.Success(nil)
}
2.6 改变弹珠的拥有者

先通过 GetState 判断账本中是否存在对应弹珠ID的信息,存在则读取账本中Json格式的弹珠信息转换为 Marble 对象,修改 Marble 对象中的拥有者 Owner 字段,最后再转换为Json格式的内容通过 PutState 写入到账本中。

// 改变弹珠的拥有者
func (t *MarblesChaincode) changeMarbleOwner (stub shim.ChaincodeStubInterface, args []string) pb.Response {
  if len(args) != 2 {
        return shim.Error("Incorrect number of arguments. Expecting 2")
    }

  marbleId := args[0]
  newOwner := args[1]

  // 查询账本中是否已经存在该弹珠ID的信息
  marbleIdAsBytes, err := stub.GetState(marbleId)

  if err != nil {
    return shim.Error(err.Error())
  } else if marbleIdAsBytes == nil {
    return shim.Error("该Marble不存在~")
  }

  marble := Marble{}
  err = json.Unmarshal(marbleIdAsBytes, &marble)
  if err != nil {
    return shim.Error(err.Error())
  }
  marble.Owner = newOwner
  marbleJsonAsBytes, err := json.Marshal(marble)
  if err != nil {
    return shim.Error(err.Error())
  }

  // 写入到账本中
  err = stub.PutState(marbleId, marbleJsonAsBytes)
  if err != nil {
    return shim.Error(err.Error())
  }

  return shim.Success(nil)
}
2.7 查询指定ID范围的弹珠信息

通过 GetStateByRange 方法查询指定范围内的键值(即所有弹珠信息),然后遍历所有键值(即所有弹珠信息),转换为Json格式的字符串数组并返回。

// 查询指定ID范围的弹珠信息
func (t *MarblesChaincode) getMarbleByRange (stub shim.ChaincodeStubInterface, args []string) pb.Response {
  if len(args) != 2 {
        return shim.Error("Incorrect number of arguments. Expecting 2")
    }

  startKey := args[0]
  endKey := args[1]

  // 查询指定范围内的键值
  resultsIterator, err := stub.GetStateByRange(startKey, endKey)
  if err != nil {
    return shim.Error(err.Error())
  }
  defer resultsIterator.Close()

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

  bArrayMemberAlreadyWritten := false
  // 遍历弹珠信息,转换为Json格式的字符串数组并返回
  for resultsIterator.HasNext() {
    queryResult, err := resultsIterator.Next()
    if err != nil {
            return shim.Error(err.Error())
        }

    if bArrayMemberAlreadyWritten == true {
      buffer.WriteString(",")
    }

    buffer.WriteString("{\"Key\":")
        buffer.WriteString("\"")
        buffer.WriteString(queryResult.Key)
        buffer.WriteString("\"")

    buffer.WriteString(", \"Record\":")
        // Record is a JSON object, so we write as-is
        buffer.WriteString(string(queryResult.Value))
        buffer.WriteString("}")
        bArrayMemberAlreadyWritten = true
  }
  buffer.WriteString("]")
  fmt.Printf("- getMarblesByRange queryResult:\n%s\n", buffer.String())
  return shim.Success(buffer.Bytes())
}
2.8 查询某个拥有者的所有弹珠信息

通过 GetQueryResult 方法对(支持富查询功能的)状态数据库(State DB)进行富查询(rich query),目前仅有CouchDB类型的状态数据库支持富查询。

// 查询某个拥有者的所有弹珠信息
func (t *MarblesChaincode) getMarbleByOwner(stub shim.ChaincodeStubInterface, args []string) pb.Response {
  marbleOwner := args[0]
  queryStr := fmt.Sprintf("{\"selector\":{\"owner\":\"%s\"}}",marbleOwner)

  resultsIterator, err := stub.GetQueryResult(queryStr)
  if err != nil {
            return shim.Error(err.Error())
    }

  defer resultsIterator.Close()

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

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

    if bArrayMemberAlreadyWritten == true {
      buffer.WriteString(",")
    }

    buffer.WriteString("{\"Key\":")
        buffer.WriteString("\"")
        buffer.WriteString(queryResult.Key)
        buffer.WriteString("\"")

    buffer.WriteString(", \"Record\":")
        buffer.WriteString(string(queryResult.Value))
        buffer.WriteString("}")
        bArrayMemberAlreadyWritten = true
  }
  buffer.WriteString("]")

  fmt.Printf("- getMarbleByOwner queryResult:\n%s\n", buffer.String())
  return shim.Success(buffer.Bytes())
}
2.9 通过弹珠ID查询所有的交易历史信息

通过 GetHistoryForKey 方法查询某个键(即对应弹珠ID)的所有历史值。

// 通过弹珠ID查询所有的交易历史信息
func (t *MarblesChaincode) getMarbleHistoryForKey (stub shim.ChaincodeStubInterface, args []string) pb.Response {
  if len(args) != 1 {
        return shim.Error("Incorrect number of arguments. Expecting 1")
    }

  marbleId := args[0]

  // 返回某个键的所有历史值
  resultsIterator, err := stub.GetHistoryForKey(marbleId)
  if err != nil {
    return shim.Error(err.Error())
  }

  defer resultsIterator.Close()

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

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

    if bArrayMemberAlreadyWritten == true {
      buffer.WriteString(",")
    }

    buffer.WriteString("{\"TxId\":")
        buffer.WriteString("\"")
        buffer.WriteString(queryResult.TxId)
        buffer.WriteString("\"")

    buffer.WriteString(", \"Timestamp\":")
    buffer.WriteString("\"")
        buffer.WriteString(time.Unix(queryResult.Timestamp.Seconds, int64(queryResult.Timestamp.Nanos)).String())
        buffer.WriteString("\"")

    buffer.WriteString("{\"Value\":")
        buffer.WriteString("\"")
        buffer.WriteString(string(queryResult.Value))
        buffer.WriteString("\"")

    buffer.WriteString("{\"IsDelete\":")
        buffer.WriteString("\"")
        buffer.WriteString(strconv.FormatBool(queryResult.IsDelete))
        buffer.WriteString("\"")

        bArrayMemberAlreadyWritten = true
  }
  buffer.WriteString("]")
  fmt.Printf("- getMarblesByRange queryResult:\n%s\n", buffer.String())
  return shim.Success(buffer.Bytes())
}
2.10 实现Invoke函数
// 在链码每个交易中,Invoke函数会被调用。
func (t *MarblesChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
  fmt.Println("Marbles Invoke")
  function, args := stub.GetFunctionAndParameters()
  if function == "initMarble" {
    return t.initMarble(stub, args)
  } else if function == "getMarbleInfoByID" {
    return t.getMarbleInfoByID(stub, args)
  } else if function == "deleteMarbleByID" {
    return t.deleteMarbleByID(stub, args)
  } else if function == "changeMarbleOwner" {
    return t.changeMarbleOwner(stub, args)
  } else if function == "getMarbleByRange" {
    return t.getMarbleByRange(stub, args)
  } else if function == "getMarbleByOwner" {
    return t.getMarbleByOwner(stub, args)
  } else if function == "getMarbleHistoryForKey" {
    return t.getMarbleHistoryForKey(stub, args)
  }
  return shim.Error("没找到对应方法~")
}

3、富查询配置CouchDB

富查询需要配置CouchDB,进入到 fabric-samples/chaincode-docker-devmode 目录,找到 docker-compose-simple.yaml 配置文件进行修改,修改格式如下:

services:
    peer:
        environment:
            ...
            - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
            ###以下是添加的内容
            - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
            - CORE_LEDGER_STATE_STATEDATABASE=CouchDB
            - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984 
            - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=
            - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=
        
        depends_on:
            - orderer
            ###以下是添加的内容
            - couchdb
        
    couchdb:
        container_name: couchdb
        image: hyperledger/fabric-couchdb
        # Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password
        # for CouchDB. This will prevent CouchDB from operating in an "Admin Party" mode.
        environment:
            - COUCHDB_USER=
            - COUCHDB_PASSWORD=
        ports:
            - 5984:5984

最终效果如下图所示:


富查询配置CouchDB

4、编译和调用链码

确保您搭建并配置好了Hyperledger Fabric的开发环境,我们把上面创建的链码文件夹复制到 fabric-samples/chaincode 目录下。

链码保存路径

同时,开启三个终端,确保终端进入到"fabric-samples/chaincode-docker-devmode"目录下。

4.1 终端1 - 开启网络
###删除所有活跃的容器###
docker rm -f $(docker ps -aq)
###清理网络缓存###
docker network prune
###开启网络###
docker-compose -f docker-compose-simple.yaml up
4.1 终端2 - 编译和运行链码
###进入Docker容器cli###
docker exec -it chaincode bash
###进入到链码对应目录###
cd marbles_chaincode
###编译链码###
go build
###启动Peer节点运行链码###
CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./marbles_chaincode
###如果失败,把"7052"改为"7051"试试看
4.3 终端3 - 安装和调用链码

1、启动Docker CLI容器:

docker exec -it chaincode bash

2、安装和实例化链码:

peer chaincode install -p chaincodedev/chaincode/marbles_chaincode -n mycc -v 0

peer chaincode instantiate -n mycc -v 0 -c '{"Args":[]}' -C myc

3、初始化弹珠:

分别创建三个弹珠

peer chaincode invoke -n mycc -c '{"Args":["initMarble","1","red","20","Jack"]}' -C myc
peer chaincode invoke -n mycc -c '{"Args":["initMarble","2","green","66","Wenzil"]}' -C myc
peer chaincode invoke -n mycc -c '{"Args":["initMarble","3","blue","88","Li Lei"]}' -C myc

4、通过弹珠ID查询对应的弹珠信息:

peer chaincode invoke -n mycc -c '{"Args":["getMarbleInfoByID","1"]}' -C myc

5、改变弹珠的拥有者:

peer chaincode invoke -n mycc -c '{"Args":["changeMarbleOwner","1","Han Meimei"]}' -C myc

6、查询改变拥有者后的弹珠信息:

peer chaincode invoke -n mycc -c '{"Args":["getMarbleInfoByID","1"]}' -C myc

7、通过弹珠ID删除对应的弹珠:

peer chaincode invoke -n mycc -c '{"Args":["deleteMarbleByID","1"]}' -C myc

8、查询指定ID范围的弹珠信息:

peer chaincode invoke -n mycc -c '{"Args":["getMarbleByRange","2", "3"]}' -C myc

9、查询某个拥有者的所有弹珠信息:

peer chaincode invoke -n mycc -c '{"Args":["getMarbleByOwner","Wenzil"]}' -C myc

10、通过弹珠ID查询所有的交易历史信息:

peer chaincode invoke -n mycc -c '{"Args":["getMarbleHistoryForKey","1"]}' -C myc
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容

  • 【作者】滚滚红尘 停止了血腥的思考,艾晓雅的大脑开始慢慢冷静下来。 古人云:女子无才便是德。这话是很有道理的。女人...
    滚滚红尘623阅读 207评论 1 0
  • 与十九大安保相伴 与忠诚和担当相伴 与关怀和激励相伴 与决战和决胜相伴 与漳河流水声相伴 与太行山红叶相伴 与红旗...
    感恩_心阅读 116评论 0 0
  • 回家又吵架了,该说的不该说的都说了,心里想的或者一气之下说的也分不清了,其实你都不必放在心上,真的啊。 我今天在火...
    不识3阅读 276评论 0 1