基于区块链的房地产交易系统

好久不见,看了下,距离上次更文 1 个多月过去了,卷不动。

大概 2 年前,碰巧学习区块链(Hyperledger Fabric),便写了一个入门级的项目放在 GitHub 上,公众号有不少读者是通过这个项目关注到我的,也经常问我,有没有区块链这方面的学习资料,有没有这个项目的详细讲解,如何搭建一个区块链网络。

对于这些问题,我每次的回复都一样,学习资料我倒是没有,但是 官方文档[1] 就是最好的资料了。

不过今天,我想还是通过这篇文章来记录一下我对之前区块链学习的一次总结吧。

对了,这个项目的地址是:https://github.com/china-li-shuo/fabric-realty ,有帮助的话点个 star 图片

预警:为了照顾到更多读者,本篇尽量从新手的视角出发,可能会有很多特别基础的内容,对于已经懂的部分,选择跳过即可。

再次预警:文章内容有点长,请耐心看,最好跟着一起动手实践,如果中途发现了错误之处,欢迎告知我。

技术栈

首先,以下这些我提到的技术要求你事先稍微学习掌握一下:

1、yaml 文件的编写

需要注意一下几个规则:

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用 tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • # 表示注释
  • & 用来建立锚点,<< 表示合并到当前数据,* 用来引用锚点

2、Docker 和 Docker Compose

  • Docker 是一个开源的应用容器引擎,可以将应用以及所需要的环境一起打包到一个轻量级、可移植的容器中,从而可以快速交付软件。
  • Docker Compose 是用来定义和运行多容器的工具。可以通过 yaml 文件来配置应用程序需要的所有服务。说白了,就是批量管理 Docker 容器。

后续区块链的节点以及应用程序的部署我们都会使用 Docker Compose 来管理。

3、 go 语言

我的项目包括本篇文章的示例都是使用 go 语言开发的,虽然 fabric 也提供了 Java,nodejs,python 等语言的 SDK ,但个人还是比较推荐 go 语言,毕竟 fabric 自身也是 go 实现的。

题外话:以上这些技能除了在 fabric 区块链体系中需掌握,在如今火热的云原生技术下也一样是基础。

区块链基础知识

1、什么是区块

Block ,每个区块记录着上一个区块的 hash 值、本区块中的交易集合、本区块的 hash 等基础数据。由于每个区块都有上一区块的 hash 值,区块间由这个值两两串联,形成了区块链。

2、什么是区块链

Blockchain ,最早起源于比特币的底层技术,并在其后不断演进发展。

区块链本质上就是一个多方共享的分布式账本技术,用来记录网络上发生的所有交易。

而其中去中心化的概念,是因为账本信息会被复制到许多网络参与者中,每个参与者都在协作维护账本,不像传统应用的数据被中心管理着。

另外信息只能以附加的方式记录到账本上,并使用加密技术保证一旦将交易添加到账本就无法修改。这种不可修改的属性简化了信息的溯源,因为参与者可以确定信息在记录后没有改变过。所以区块链有时也被称为证明系统

3、什么是公链、联盟链和私链

区块链分为公有链、联盟链、私有链三种基本类型。其中:

  • 完全去中心化:公链,人人都可以参与,就像比特币(挖矿相当于在记账)。主要采取工作量证明机制(POW)、权益证明机制(POS)、股份授权证明机制(DPOS)等方式。
  • 部分去中心化:联盟链,参与者是指定的。联盟链可以是几家公司共同拥有的链,也可能是几个国家共同承认的链。这是后续发展的趋势。
  • 中心化:私链,写入权限仅在一个组织手里的区块链,仅对特定的团队、组织或者个人开放。

4、什么是交易

Transaction ,区块链接收的数据称之为交易。

5、什么是智能合约

Smart contract,为了支持以同样的方式更新信息,并实现一整套账本功能(交易,查询等),区块链使用智能合约来提供对账本的受控访问。

智能合约不仅是在网络中封装和简化信息的关键机制,它还可以被编写成自动执行参与者的特定交易的合约。

例如,可以编写智能合约以规定运输物品的成本,其中运费根据物品到达的速度而变化。根据双方同意并写入账本的条款,当收到物品时,相应的资金会自动转手。

通俗易懂点,智能合约就是按照大家约定好的规则编写的业务逻辑代码实现,然后只能通过这些合约来操作区块链网络这个账本。

6、什么是共识

保持账本在整个网络中同步的过程称为共识。该过程确保账本仅在交易被相应参与者批准时才会更新,并且当账本更新时,它们以相同的顺序更新相同的交易。

Hyperledger Fabric 基础知识

1、什么是 Hyperledger Fabric

Linux 基金会于 2015 年创建了 Hyperledger(超级账本)项目,而 Hyperledger Fabric 是其中一个用 Go 语言实现的版本。

Hyperledger Fabric 网络的成员只能从可信赖的成员服务提供者(MSP) 注册,也就是说 Hyperledger Fabric 搭建的区块链是一种联盟链。

Hyperledger Fabric 的账本包括两个组件: 世界状态和交易日志。并且每个参与者都拥有他们所属的每个 Hyperledger Fabric 网络的账本的副本。

  • 世界状态:描述了在给定时间点的账本的状态。它是账本的数据库。默认情况下,使用 LevelDB 键值存储数据库,可插拔,可替换为 CouchDB 。
  • 交易日志:记录产生世界状态中当前值的所有交易。这是世界状态的更新历史。它只记录区块链网络使用账本数据库前后的值。

总结:Hyperledger Fabric 是一种账本技术,其账本包括世界状态数据库和交易日志历史记录。

2、什么是联盟

联盟指参与一个基于区块链的业务协作或业务交易网络的所有组织的集合,一个联盟一般包含多个组织。

一般由联盟发起方或运营方创建 Orderer 排序节点,并负责交易排序、区块产生和达成共识。联盟发起方或运营方邀请各个组织实例加入联盟,进而创建通道。

3、什么是组织

组织代表的是参与区块链网络的企业、政府机构、团体等实体。

一个组织实例主要包含如下节点:

  • CA :区块链节点类型之一,全称 Certificate Authority ,数字证书颁发机构,负责组织内部成员的 registerenroll 等,为该组织的区块链用户生成和颁发数字证书。
  • Peer :区块链节点类型之一,负责保存和记录账本数据、对交易背书、运行智能合约等。

4、什么是节点

节点(Peers)是区块链的通信实体。它只是一个逻辑功能,只要能在“信任域”中分组并与控制它们的逻辑实体相关联,就可以将不同类型的多个节点运行在同一个物理服务器上,比如用 Docker 部署。

  • Orderer 排序服务节点 或 排序节点:Orderer 是一个运行实现交付担保的通信服务节点,例如原子性或总顺序广播。排序节点负责接受交易并排序(排序算法有: SOLO,KAFKA,RAFT,PBFT),最后将排序好的交易按照配置中的约定整理为区块之后提交给记账节点进行处理。
  • Peer 节点:Peer 是业务参与方组织在区块链网络中所拥有的参与共识和账本记录的节点。可以有多种角色。作为 Committing Peer 记账节点时,无需安装链码,只负责验证从 Orderer 发出的区块和交易的合法性、并存储账本区块信息。作为 Endorsing Peer 背书节点时,必须安装链码,在交易时需进行签名背书。
  • Anchor 锚节点:为了实现高可用,每个参与方组织一般包含两个或多个 Peer 节点,可以设置其中的一个为 Anchor ,与区块链网络中的其他组织进行信息同步。
  • 客户端节点:客户端扮演了代表最终用户的实体,可以同时与 PeerOrderer 通信,创建并调用交易。这里客户端可以指应用程序、SDK、命令行等。

5、什么是通道

Hyperledger Fabric 中的通道(Channel)是两个或两个以上特定网络成员之间通信的专用“子网”,用于进行私有和机密的交易。

可以理解为组织间拉了个群聊,这个群聊就是通道,在里面聊天交易,一个联盟链中可以有多个群聊(通道),一个组织可以加入多个群聊,每个群聊可以代表一项具体的业务,有自身对应的一套账本,群聊间互不干扰,互相隔离。

6、什么是链码

Hyperledger Fabric 的智能合约用链码(Chaincode)编写。在大多数情况下,链码只与账本的数据库即世界状态交互,而不与交易日志交互。

链码可以用多种编程语言实现。有 Go、Node.js 和 Java 链码等。

搭建区块链网络

基础知识过完,接下来就到了本篇核心的项目实战环节。首先是搭建一个区块链网络,只需按照下面几个顺序,一步步来就行(推荐在 Linux 或 MacOS 下操作):

1、下载 fabric 二进制工具

v1.4.12 版本为例, fabric 二进制工具的下载地址在:https://github.com/hyperledger/fabric/releases/tag/v1.4.12

自行根据你的系统环境下载对应的包。

其中几个主要的工具说明:

  • cryptogen :用来生成 Hyperledger Fabric 密钥材料的工具,这个过程是静态的。cryptogen 工具通过一个包含网络拓扑的 crypto-config.yaml 文件,为所有组织和属于这些组织的组件生成一组证书和秘钥。cryptogen 适合用于测试开发环境,在生产环境建议使用动态的 CA 服务。
  • configtxgen :用于创建和查看排序节点的创世区块、通道配置交易等相关的工具。configtxgen 使用 configtx.yaml 文件来定义网络配置。
  • configtxlator:fabric 中 ProtobufJSON 格式转换的工具,fabric 中任何的使用 Protobuf 定义的类型,都可使用该工具进行转换。
  • peer:peer 命令有 5 个不同的子命令,每个命令都可以让指定的 peer 节点执行特定的一组任务。比如,可以使用子命令 peer channel 让一个 peer 节点加入通道,或者使用 peer chaincode 命令把智能合约链码部署到 peer 节点上。

2、将 fabric 二进制工具添加到环境变量

为了后续方便使用命令,可以将第 1 步下载的工具添加到系统环境变量中:

  1. $ export PATH=${PWD}/hyperledger-fabric-linux-amd64-1.4.12/bin:$PATH

3、生成证书和秘钥

我们将使用 cryptogen 工具生成各种加密材料( x509 证书和签名秘钥)。这些证书是身份的代表,在实体相互通信和交易的时候,可以对其身份进行签名和验证。

首先创建 crypto-config.yaml 文件,定义网络拓扑,为所有组织和属于这些组织的组件(也就是节点)生成一组证书和秘钥,内容如下:

  1. # 排序节点的组织定义
  2. OrdererOrgs:
  3. - Name: QQ # 名称
  4. Domain: qq.com # 域名
  5. Specs: # 节点域名:orderer.qq.com
  6. - Hostname: orderer # 主机名
  7. # peer节点的组织定义
  8. PeerOrgs:
  9. # Taobao-组织
  10. - Name: Taobao # 名称
  11. Domain: taobao.com # 域名
  12. Template: # 使用模板定义。Count 指的是该组织下组织节点的个数
  13. Count: 2 # 节点域名:peer0.taobao.com 和 peer1.taobao.com
  14. Users: # 组织的用户信息。Count 指该组织中除了 Admin 之外的用户的个数
  15. Count: 1 # 用户:Admin 和 User1
  16. # JD-组织
  17. - Name: JD
  18. Domain: jd.com
  19. Template:
  20. Count: 2 # 节点域名:peer0.jd.com 和 peer1.jd.com
  21. Users:
  22. Count: 1 # 用户:Admin 和 User1

接着执行 cryptogen generate 命令,生成结果将默认保存在 crypto-config 文件夹中:

  1. $ cryptogen generate --config=./crypto-config.yaml
  2. taobao.com
  3. jd.com

我们可以看看在 crypto-config 文件夹里生成了什么:

  1. $ tree crypto-config
  2. crypto-config
  3. ├── ordererOrganizations
  4. └── qq.com
  5. ├── ca
  6. ├── 3e41f960bb5a3002a1e436e9079311d79cf8846c2ad2a09080ea8575e16bb5b7_sk
  7. └── ca.qq.com-cert.pem
  8. ├── msp
  9. ├── admincerts
  10. └── Admin@qq.com-cert.pem
  11. ├── cacerts
  12. └── ca.qq.com-cert.pem
  13. └── tlscacerts
  14. └── tlsca.qq.com-cert.pem
  15. ├── orderers
  16. └── orderer.qq.com
  17. ├── msp
  18. ├── admincerts
  19. └── Admin@qq.com-cert.pem
  20. ├── cacerts
  21. └── ca.qq.com-cert.pem
  22. ├── keystore
  23. └── 6bd45f78877b96cfbcd040262ee4c808bd6d894cabfed44552fb7c22d6d427d1_sk
  24. ├── signcerts
  25. └── orderer.qq.com-cert.pem
  26. └── tlscacerts
  27. └── tlsca.qq.com-cert.pem
  28. └── tls
  29. ├── ca.crt
  30. ├── server.crt
  31. └── server.key
  32. ├── tlsca
  33. ├── bd48b5360c82ce5beeb31dea1b7e8e7918a5e7246d3f8892889fe1b2efadc1aa_sk
  34. └── tlsca.qq.com-cert.pem
  35. └── users
  36. └── Admin@qq.com
  37. ├── msp
  38. ├── admincerts
  39. └── Admin@qq.com-cert.pem
  40. ├── cacerts
  41. └── ca.qq.com-cert.pem
  42. ├── keystore
  43. └── f28c1ed4c67fd438a891e420a2e53b20352bdf40907a0a8ee39095505475c99f_sk
  44. ├── signcerts
  45. └── Admin@qq.com-cert.pem
  46. └── tlscacerts
  47. └── tlsca.qq.com-cert.pem
  48. └── tls
  49. ├── ca.crt
  50. ├── client.crt
  51. └── client.key
  52. └── peerOrganizations
  53. ├── jd.com
  54. ├── ca
  55. ├── 5672a9717fd943d0dcd2269ea1700c10309ad49d16b849e9c6e24225deafceb5_sk
  56. └── ca.jd.com-cert.pem
  57. ├── msp
  58. ├── admincerts
  59. └── Admin@jd.com-cert.pem
  60. ├── cacerts
  61. └── ca.jd.com-cert.pem
  62. └── tlscacerts
  63. └── tlsca.jd.com-cert.pem
  64. ├── peers
  65. ├── peer0.jd.com
  66. ├── msp
  67. ├── admincerts
  68. └── Admin@jd.com-cert.pem
  69. ├── cacerts
  70. └── ca.jd.com-cert.pem
  71. ├── keystore
  72. └── 012700eb44d6e19becb63c944e685a18d69ea9f1120aaa45fe549236c6a90fb6_sk
  73. ├── signcerts
  74. └── peer0.jd.com-cert.pem
  75. └── tlscacerts
  76. └── tlsca.jd.com-cert.pem
  77. └── tls
  78. ├── ca.crt
  79. ├── server.crt
  80. └── server.key
  81. └── peer1.jd.com
  82. ├── msp
  83. ├── admincerts
  84. └── Admin@jd.com-cert.pem
  85. ├── cacerts
  86. └── ca.jd.com-cert.pem
  87. ├── keystore
  88. └── b1e81b66080705595f5e56cc8d78575b0e935b79c8f674001e46cae452a71f32_sk
  89. ├── signcerts
  90. └── peer1.jd.com-cert.pem
  91. └── tlscacerts
  92. └── tlsca.jd.com-cert.pem
  93. └── tls
  94. ├── ca.crt
  95. ├── server.crt
  96. └── server.key
  97. ├── tlsca
  98. ├── f4c7d0b660575f383d189696480bf559f312d798eb0352c9102f8be6ecde52d6_sk
  99. └── tlsca.jd.com-cert.pem
  100. └── users
  101. ├── Admin@jd.com
  102. ├── msp
  103. ├── admincerts
  104. └── Admin@jd.com-cert.pem
  105. ├── cacerts
  106. └── ca.jd.com-cert.pem
  107. ├── keystore
  108. └── d7f476884ff36a19aa7100c63aa30f8f378cc5ec826ca58977539e1c9c6b22df_sk
  109. ├── signcerts
  110. └── Admin@jd.com-cert.pem
  111. └── tlscacerts
  112. └── tlsca.jd.com-cert.pem
  113. └── tls
  114. ├── ca.crt
  115. ├── client.crt
  116. └── client.key
  117. └── User1@jd.com
  118. ├── msp
  119. ├── admincerts
  120. └── User1@jd.com-cert.pem
  121. ├── cacerts
  122. └── ca.jd.com-cert.pem
  123. ├── keystore
  124. └── e83862c8e78509f2a4362d3282214421179fa47f3d655f75cb3539d5534f7494_sk
  125. ├── signcerts
  126. └── User1@jd.com-cert.pem
  127. └── tlscacerts
  128. └── tlsca.jd.com-cert.pem
  129. └── tls
  130. ├── ca.crt
  131. ├── client.crt
  132. └── client.key
  133. └── taobao.com
  134. ├── ca
  135. ├── 4a31791b9fade54ab70496f03169707f6b9643c04d1bc734da15b0c625628865_sk
  136. └── ca.taobao.com-cert.pem
  137. ├── msp
  138. ├── admincerts
  139. └── Admin@taobao.com-cert.pem
  140. ├── cacerts
  141. └── ca.taobao.com-cert.pem
  142. └── tlscacerts
  143. └── tlsca.taobao.com-cert.pem
  144. ├── peers
  145. ├── peer0.taobao.com
  146. ├── msp
  147. ├── admincerts
  148. └── Admin@taobao.com-cert.pem
  149. ├── cacerts
  150. └── ca.taobao.com-cert.pem
  151. ├── keystore
  152. └── 914648b8c4dc4783b0505a22b5c7630e424c3cf8dd54e2fe05b47dc321a4e61b_sk
  153. ├── signcerts
  154. └── peer0.taobao.com-cert.pem
  155. └── tlscacerts
  156. └── tlsca.taobao.com-cert.pem
  157. └── tls
  158. ├── ca.crt
  159. ├── server.crt
  160. └── server.key
  161. └── peer1.taobao.com
  162. ├── msp
  163. ├── admincerts
  164. └── Admin@taobao.com-cert.pem
  165. ├── cacerts
  166. └── ca.taobao.com-cert.pem
  167. ├── keystore
  168. └── 3eef8defc07afb547e94f08702a5b30807d2e2a672e3d437bfb54dd1590b0fa7_sk
  169. ├── signcerts
  170. └── peer1.taobao.com-cert.pem
  171. └── tlscacerts
  172. └── tlsca.taobao.com-cert.pem
  173. └── tls
  174. ├── ca.crt
  175. ├── server.crt
  176. └── server.key
  177. ├── tlsca
  178. ├── 296a941f625974153aa5ab6cf57b0933023aaa13b0e4363a7378e5c527de26a1_sk
  179. └── tlsca.taobao.com-cert.pem
  180. └── users
  181. ├── Admin@taobao.com
  182. ├── msp
  183. ├── admincerts
  184. └── Admin@taobao.com-cert.pem
  185. ├── cacerts
  186. └── ca.taobao.com-cert.pem
  187. ├── keystore
  188. └── a2af975d659f77182b2aca318321797d281036f085dda9799ab79b6400e5e970_sk
  189. ├── signcerts
  190. └── Admin@taobao.com-cert.pem
  191. └── tlscacerts
  192. └── tlsca.taobao.com-cert.pem
  193. └── tls
  194. ├── ca.crt
  195. ├── client.crt
  196. └── client.key
  197. └── User1@taobao.com
  198. ├── msp
  199. ├── admincerts
  200. └── User1@taobao.com-cert.pem
  201. ├── cacerts
  202. └── ca.taobao.com-cert.pem
  203. ├── keystore
  204. └── c65d45e1c7e1070e3f1b00bd8ac41e91d2bfaea10a769d75b9599590791ccc02_sk
  205. ├── signcerts
  206. └── User1@taobao.com-cert.pem
  207. └── tlscacerts
  208. └── tlsca.taobao.com-cert.pem
  209. └── tls
  210. ├── ca.crt
  211. ├── client.crt
  212. └── client.key
  213. 109 directories, 101 files

总结:在这个环节中,我们假设 QQ 作为一个运营方,提供了 1 个 Orderer 节点 orderer.qq.com 来创建联盟链的基础设施, 而 TaobaoJD 则是作为组织成员加入到链中,各自提供 2 个 Peer 节点 peer0.xx.compeer1.xx.com 参与工作,以及还各自创建了 2 个组织用户 AdminUser1 。然后我们使用 crypto-config.yaml 文件和 cryptogen 工具为其定义所需要的证书文件以供后续使用。

4、创建排序通道创世区块

我们可以使用 configtx.yaml 文件和 configtxgen 工具轻松地创建通道的配置。configtx.yaml 文件可以以易于理解和编辑的 yaml 格式来构建通道配置所需的信息。configtxgen 工具通过读取 configtx.yaml 文件中的信息,将其转成 Fabric 可以读取的 protobuf 格式。

先来创建 configtx.yaml 文件,内容如下:

  1. # 定义组织机构实体
  2. Organizations:
  3. - &QQ
  4. Name: QQ # 组织的名称
  5. ID: QQMSP # 组织的 MSPID
  6. MSPDir: crypto-config/ordererOrganizations/qq.com/msp #组织的证书相对位置(生成的crypto-config目录)
  7. - &Taobao
  8. Name: Taobao
  9. ID: TaobaoMSP
  10. MSPDir: crypto-config/peerOrganizations/taobao.com/msp
  11. AnchorPeers: # 组织锚节点的配置
  12. - Host: peer0.taobao.com
  13. Port: 7051
  14. - &JD
  15. Name: JD
  16. ID: JDMSP
  17. MSPDir: crypto-config/peerOrganizations/jd.com/msp
  18. AnchorPeers: # 组织锚节点的配置
  19. - Host: peer0.jd.com
  20. Port: 7051
  21. # 定义了排序服务的相关参数,这些参数将用于创建创世区块
  22. Orderer: &OrdererDefaults
  23. # 排序节点类型用来指定要启用的排序节点实现,不同的实现对应不同的共识算法
  24. OrdererType: solo # 共识机制
  25. Addresses: # Orderer 的域名(用于连接)
  26. - orderer.qq.com:7050
  27. BatchTimeout: 2s # 出块时间间隔
  28. BatchSize: # 用于控制每个block的信息量
  29. MaxMessageCount: 10 #每个区块的消息个数
  30. AbsoluteMaxBytes: 99 MB #每个区块最大的信息大小
  31. PreferredMaxBytes: 512 KB #每个区块包含的一条信息最大长度
  32. Organizations:
  33. # 定义Peer组织如何与应用程序通道交互的策略
  34. # 默认策略:所有Peer组织都将能够读取数据并将数据写入账本
  35. Application: &ApplicationDefaults
  36. Organizations:
  37. # 用来定义用于 configtxgen 工具的配置入口
  38. # 将 Profile 参数( TwoOrgsOrdererGenesis 或 TwoOrgsChannel )指定为 configtxgen 工具的参数
  39. Profiles:
  40. # TwoOrgsOrdererGenesis配置文件用于创建系统通道创世块
  41. # 该配置文件创建一个名为SampleConsortium的联盟
  42. # 该联盟在configtx.yaml文件中包含两个Peer组织Taobao和JD
  43. TwoOrgsOrdererGenesis:
  44. Orderer:
  45. <<: *OrdererDefaults
  46. Organizations:
  47. - *QQ
  48. Consortiums:
  49. SampleConsortium:
  50. Organizations:
  51. - *Taobao
  52. - *JD
  53. # 使用TwoOrgsChannel配置文件创建应用程序通道
  54. TwoOrgsChannel:
  55. Consortium: SampleConsortium
  56. Application:
  57. <<: *ApplicationDefaults
  58. Organizations:
  59. - *Taobao
  60. - *JD

执行 configtxgen 命令,并指定 Profile 为 TwoOrgsOrdererGenesis 参数:

  1. $ configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./config/genesis.block -channelID firstchannel

排序区块是排序服务的创世区块,通过以上命令就可以预先生成创世区块的 protobuf 格式的配置文件 ./config/genesis.block 了。这一步也是为后续做准备用的。

5、创建通道配置交易

接下来,我们需要继续使用 configtxgen 根据去创建通道的交易配置,和第 4 步不同的是,这次需要指定 Profile 为 TwoOrgsChannel 参数。

生成通道配置事务 ./config/appchannel.tx

  1. $ configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./config/appchannel.tx -channelID appchannel

Taobao 组织定义锚节点,生成 ./config/TaobaoAnchor.tx

  1. $ configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./config/TaobaoAnchor.tx -channelID appchannel -asOrg Taobao

JD 组织定义锚节点,生成 ./config/JDAnchor.tx

  1. $ configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./config/JDAnchor.tx -channelID appchannel -asOrg JD

当然,这一步也是为后续使用做准备的。不过至此,需要准备的配置都齐了。

来看看现在 config 文件夹都有什么:

  1. $ tree config
  2. config
  3. ├── JDAnchor.tx
  4. ├── TaobaoAnchor.tx
  5. ├── appchannel.tx
  6. └── genesis.block
  7. 0 directories, 4 files

6、创建并启动各组织的节点

我们说过:我们假设 QQ 作为一个运营方,提供了 1 个 Orderer 节点 orderer.qq.com 来创建联盟链的基础设施, 而 TaobaoJD 则是作为组织成员加入到链中,各自提供 2 个 Peer 节点 peer0.xx.compeer1.xx.com 参与工作。

现在这些组织及其节点所需要的配置已经准备好了。我们接下来就可以使用 Docker Compose 来模拟启动这些节点服务。

由于这些节点之间需要互相通信,所以我们需要将这些节点都放入到一个 Docker 网络中,以 fabric_network 为例。

docker-compose.yaml 的内容如下:

  1. version: '2.1'
  2. volumes:
  3. orderer.qq.com:
  4. peer0.taobao.com:
  5. peer1.taobao.com:
  6. peer0.jd.com:
  7. peer1.jd.com:
  8. networks:
  9. fabric_network:
  10. name: fabric_network
  11. services:
  12. # 排序服务节点
  13. orderer.qq.com:
  14. container_name: orderer.qq.com
  15. image: hyperledger/fabric-orderer:1.4.12
  16. environment:
  17. - GODEBUG=netdns=go
  18. - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
  19. - ORDERER_GENERAL_GENESISMETHOD=file
  20. - ORDERER_GENERAL_GENESISFILE=/etc/hyperledger/config/genesis.block # 注入创世区块
  21. - ORDERER_GENERAL_LOCALMSPID=QQMSP
  22. - ORDERER_GENERAL_LOCALMSPDIR=/etc/hyperledger/orderer/msp # 证书相关
  23. command: orderer
  24. ports:
  25. - "7050:7050"
  26. volumes: # 挂载由cryptogen和configtxgen生成的证书文件以及创世区块
  27. - ./config/genesis.block:/etc/hyperledger/config/genesis.block
  28. - ./crypto-config/ordererOrganizations/qq.com/orderers/orderer.qq.com/:/etc/hyperledger/orderer
  29. - orderer.qq.com:/var/hyperledger/production/orderer
  30. networks:
  31. - fabric_network
  32. # Taobao 组织 peer0 节点
  33. peer0.taobao.com:
  34. extends:
  35. file: docker-compose-base.yaml
  36. service: peer-base
  37. container_name: peer0.taobao.com
  38. environment:
  39. - CORE_PEER_ID=peer0.taobao.com
  40. - CORE_PEER_LOCALMSPID=TaobaoMSP
  41. - CORE_PEER_ADDRESS=peer0.taobao.com:7051
  42. ports:
  43. - "7051:7051" # grpc服务端口
  44. - "7053:7053" # eventhub端口
  45. volumes:
  46. - ./crypto-config/peerOrganizations/taobao.com/peers/peer0.taobao.com:/etc/hyperledger/peer
  47. - peer0.taobao.com:/var/hyperledger/production
  48. depends_on:
  49. - orderer.qq.com
  50. # Taobao 组织 peer1 节点
  51. peer1.taobao.com:
  52. extends:
  53. file: docker-compose-base.yaml
  54. service: peer-base
  55. container_name: peer1.taobao.com
  56. environment:
  57. - CORE_PEER_ID=peer1.taobao.com
  58. - CORE_PEER_LOCALMSPID=TaobaoMSP
  59. - CORE_PEER_ADDRESS=peer1.taobao.com:7051
  60. ports:
  61. - "17051:7051"
  62. - "17053:7053"
  63. volumes:
  64. - ./crypto-config/peerOrganizations/taobao.com/peers/peer1.taobao.com:/etc/hyperledger/peer
  65. - peer1.taobao.com:/var/hyperledger/production
  66. depends_on:
  67. - orderer.qq.com
  68. # JD 组织 peer0 节点
  69. peer0.jd.com:
  70. extends:
  71. file: docker-compose-base.yaml
  72. service: peer-base
  73. container_name: peer0.jd.com
  74. environment:
  75. - CORE_PEER_ID=peer0.jd.com
  76. - CORE_PEER_LOCALMSPID=JDMSP
  77. - CORE_PEER_ADDRESS=peer0.jd.com:7051
  78. ports:
  79. - "27051:7051"
  80. - "27053:7053"
  81. volumes:
  82. - ./crypto-config/peerOrganizations/jd.com/peers/peer0.jd.com:/etc/hyperledger/peer
  83. - peer0.jd.com:/var/hyperledger/production
  84. depends_on:
  85. - orderer.qq.com
  86. # JD 组织 peer1 节点
  87. peer1.jd.com:
  88. extends:
  89. file: docker-compose-base.yaml
  90. service: peer-base
  91. container_name: peer1.jd.com
  92. environment:
  93. - CORE_PEER_ID=peer1.jd.com
  94. - CORE_PEER_LOCALMSPID=JDMSP
  95. - CORE_PEER_ADDRESS=peer1.jd.com:7051
  96. ports:
  97. - "37051:7051"
  98. - "37053:7053"
  99. volumes:
  100. - ./crypto-config/peerOrganizations/jd.com/peers/peer1.jd.com:/etc/hyperledger/peer
  101. - peer1.jd.com:/var/hyperledger/production
  102. depends_on:
  103. - orderer.qq.com
  104. # 客户端节点
  105. cli:
  106. container_name: cli
  107. image: hyperledger/fabric-tools:1.4.12
  108. tty: true
  109. environment:
  110. # go 环境设置
  111. - GO111MODULE=auto
  112. - GOPROXY=https://goproxy.cn
  113. - CORE_PEER_ID=cli
  114. command: /bin/bash
  115. volumes:
  116. - ./config:/etc/hyperledger/config
  117. - ./crypto-config/peerOrganizations/taobao.com/:/etc/hyperledger/peer/taobao.com
  118. - ./crypto-config/peerOrganizations/jd.com/:/etc/hyperledger/peer/jd.com
  119. - ./../chaincode:/opt/gopath/src/chaincode # 链码路径注入
  120. networks:
  121. - fabric_network
  122. depends_on:
  123. - orderer.qq.com
  124. - peer0.taobao.com
  125. - peer1.taobao.com
  126. - peer0.jd.com
  127. - peer1.jd.com

为了方便,这里我还定义了一个 docker-compose-base.yaml 作为 Peer 节点的公共模板,内容如下:

  1. version: '2.1'
  2. services:
  3. peer-base: # peer的公共服务
  4. image: hyperledger/fabric-peer:1.4.12
  5. environment:
  6. - GODEBUG=netdns=go
  7. - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
  8. - CORE_LOGGING_PEER=info
  9. - CORE_CHAINCODE_LOGGING_LEVEL=INFO
  10. - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/msp # msp证书(节点证书)
  11. - CORE_LEDGER_STATE_STATEDATABASE=goleveldb # 状态数据库的存储引擎(or CouchDB)
  12. - CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fabric_network # docker 网络
  13. volumes:
  14. - /var/run/docker.sock:/host/var/run/docker.sock
  15. working_dir: /opt/gopath/src/github.com/hyperledger/fabric
  16. command: peer node start
  17. networks:
  18. - fabric_network

注意观察,在 volumes 配置项中,我们将 configcrypto-config 内的配置文件都挂载到相对应的节点中了。并且在 peer 的公共服务中,我们还挂载了 /var/run/docker.sock 文件,有了该文件,在容器内就可以向其发送 http 请求和 Docker Daemon 通信,通俗理解,就是有了它,就可以在容器内操作宿主机的 Docker 了,比如在容器内控制 Docker 再启动一个容器出来。而这,就是为了后面可以部署智能合约(节点部署链码其实就是启动一个链码容器)。

现在继续将这些节点服务启动起来:

  1. $ docker-compose up -d
  2. Creating network "fabric_network" with the default driver
  3. Creating volume "network_orderer.qq.com" with default driver
  4. Creating volume "network_peer0.taobao.com" with default driver
  5. Creating volume "network_peer1.taobao.com" with default driver
  6. Creating volume "network_peer0.jd.com" with default driver
  7. Creating volume "network_peer1.jd.com" with default driver
  8. Creating orderer.qq.com ... done
  9. Creating peer1.taobao.com ... done
  10. Creating peer0.jd.com ... done
  11. Creating peer1.jd.com ... done
  12. Creating peer0.taobao.com ... done
  13. Creating cli ... done

哦对了,除了必须的节点服务,我还启动了一个 cli 服务,来自 hyperledger/fabric-tools 镜像,这个其实就是集成了前面第 1 步提到的 fabric 工具的容器,我们接下来的命令执行就使用这个容器内的工具来完成了,你也可以继续使用自己下载的二进制工具,只是个人觉得环境配置起来会比较麻烦。

7、为 cli 服务配置环境

接下来我们要使用 cli 服务来执行 peer 命令,所以要为其先配置一下环境变量,使用四个不同的变量 TaobaoPeer0CliTaobaoPeer1CliJDPeer0CliJDPeer1Cli ,代表 cli 服务代表着不同的节点:

  1. $ TaobaoPeer0Cli="CORE_PEER_ADDRESS=peer0.taobao.com:7051 CORE_PEER_LOCALMSPID=TaobaoMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/taobao.com/users/Admin@taobao.com/msp"
  2. $ TaobaoPeer1Cli="CORE_PEER_ADDRESS=peer1.taobao.com:7051 CORE_PEER_LOCALMSPID=TaobaoMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/taobao.com/users/Admin@taobao.com/msp"
  3. $ JDPeer0Cli="CORE_PEER_ADDRESS=peer0.jd.com:7051 CORE_PEER_LOCALMSPID=JDMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/jd.com/users/Admin@jd.com/msp"
  4. $ JDPeer1Cli="CORE_PEER_ADDRESS=peer1.jd.com:7051 CORE_PEER_LOCALMSPID=JDMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/jd.com/users/Admin@jd.com/msp"

8、开始创建通道

通道主要用于实现区块链网络中业务的隔离。一个联盟中可以有多个通道,每个通道可代表一项业务,并且对应一套账本。通道内的成员为业务参与方(即联盟内的组织),一个组织可以加入多个通道。

我们现在有请 Taobao 组织的 peer0 节点来创建一个通道 appchannel

  1. $ docker exec cli bash -c "$TaobaoPeer0Cli peer channel create -o orderer.qq.com:7050 -c appchannel -f /etc/hyperledger/config/appchannel.tx"

通道就相当于“群聊”, Taobao 组织的 peer0 节点创建了一个名称为 appchannel 的“群聊”。

9、将所有节点加入通道

将所有的节点都加入到通道 appchannel 中(正常是按需加入):

  1. $ docker exec cli bash -c "$TaobaoPeer0Cli peer channel join -b appchannel.block"
  2. $ docker exec cli bash -c "$TaobaoPeer1Cli peer channel join -b appchannel.block"
  3. $ docker exec cli bash -c "$JDPeer0Cli peer channel join -b appchannel.block"
  4. $ docker exec cli bash -c "$JDPeer1Cli peer channel join -b appchannel.block"

这时相当于大家都加入到了 appchannel “群聊”中,之后大家都可以在里面“聊天”了。

10、更新锚节点

锚节点是必需的。普通节点只能发现本组织下的其它节点,而锚节点可以跨组织服务发现到其它组织下的节点,建议每个组织都选择至少一个锚节点。

利用之前准备好的配置文件,向通道更新锚节点:

  1. $ docker exec cli bash -c "$TaobaoPeer0Cli peer channel update -o orderer.qq.com:7050 -c appchannel -f /etc/hyperledger/config/TaobaoAnchor.tx"
  2. $ docker exec cli bash -c "$JDPeer0Cli peer channel update -o orderer.qq.com:7050 -c appchannel -f /etc/hyperledger/config/JDAnchor.tx"

这样,TaobaoJD 组织间的节点就都可以互相发现了。

到这里,我们的区块链网络基本已经搭建好了,但是还差最关键的智能合约。一个没有智能合约的通道是没有灵魂的,啥事都做不了。

编写智能合约

fabric 的智能合约称为链码,编写智能合约也就是编写链码。

链码其实很简单,可以由 Go 、 node.js 、或者 Java 编写,其实只是实现一些预定义的接口。

以 Go 为例,创建一个 main.go 文件:

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/hyperledger/fabric/core/chaincode/shim"
  5. pb "github.com/hyperledger/fabric/protos/peer"
  6. )
  7. type MyChaincode struct {
  8. }
  9. // Init 初始化时会执行该方法
  10. func (c *MyChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
  11. fmt.Println("链码初始化")
  12. return shim.Success(nil)
  13. }
  14. // Invoke 智能合约的功能函数定义
  15. func (c *MyChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
  16. funcName, args := stub.GetFunctionAndParameters()
  17. switch funcName {
  18. default:
  19. return shim.Error(fmt.Sprintf("没有该功能: %s", funcName))
  20. }
  21. }
  22. func main() {
  23. err := shim.Start(new(MyChaincode))
  24. if err != nil {
  25. panic(err)
  26. }
  27. }

我们定义的 MyChaincode 结构体实现了 shim.Chaincode 接口:

  1. // Chaincode interface must be implemented by all chaincodes. The fabric runs
  2. // the transactions by calling these functions as specified.
  3. type Chaincode interface {
  4. // Init is called during Instantiate transaction after the chaincode container
  5. // has been established for the first time, allowing the chaincode to
  6. // initialize its internal data
  7. Init(stub ChaincodeStubInterface) pb.Response
  8. // Invoke is called to update or query the ledger in a proposal transaction.
  9. // Updated state variables are not committed to the ledger until the
  10. // transaction is committed.
  11. Invoke(stub ChaincodeStubInterface) pb.Response
  12. }

然后在启动入口 main 函数中调用 shim.Start(new(MyChaincode)) 就完成了链码的启动,没错,就是这么简单。

我们知道链码其实就是用来处理区块链网络中的成员一致同意的业务逻辑。比如 TaobaoJD 规定了一个规则,将其编写成链码,后面双方就只能遵循这个规则了,因为链码到时候即部署在你的节点,也会部署在我的节点上,你偷偷改了逻辑,我的节点不会认可你的,这也正是区块链的作用之一。

链码的功能定义在 Invoke 方法中。

一个简易的示例如下:

  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "github.com/hyperledger/fabric/core/chaincode/shim"
  8. pb "github.com/hyperledger/fabric/protos/peer"
  9. )
  10. type MyChaincode struct {
  11. }
  12. // Init 初始化时会执行该方法
  13. func (c *MyChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
  14. fmt.Println("链码初始化")
  15. // 假设A有1000元,以复合主键 userA 的形式写入账本
  16. err := WriteLedger(stub, map[string]interface{}{"name": "A", "balance": 1000}, "user", []string{"A"})
  17. if err != nil {
  18. return shim.Error(err.Error())
  19. }
  20. // 假设B有1000元,以复合主键 userB 的形式写入账本
  21. err = WriteLedger(stub, map[string]interface{}{"name": "B", "balance": 1000}, "user", []string{"B"})
  22. if err != nil {
  23. return shim.Error(err.Error())
  24. }
  25. return shim.Success(nil)
  26. }
  27. // Invoke 智能合约的功能函数定义
  28. func (c *MyChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
  29. funcName, args := stub.GetFunctionAndParameters()
  30. switch funcName {
  31. case "query":
  32. return query(stub, args)
  33. case "transfer":
  34. return transfer(stub, args)
  35. default:
  36. return shim.Error(fmt.Sprintf("没有该功能: %s", funcName))
  37. }
  38. }
  39. func query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
  40. // 如果 args 为空,则表示查询所有 user
  41. results, err := ReadLedger(stub, "user", args)
  42. if err != nil {
  43. return shim.Error(err.Error())
  44. }
  45. var users []map[string]interface{}
  46. for _, result := range results {
  47. var user map[string]interface{}
  48. if err = json.Unmarshal(result, &user); err != nil {
  49. return shim.Error(err.Error())
  50. }
  51. users = append(users, user)
  52. }
  53. usersByte, err := json.Marshal(&users)
  54. if err != nil {
  55. return shim.Error(err.Error())
  56. }
  57. return shim.Success(usersByte)
  58. }
  59. func transfer(stub shim.ChaincodeStubInterface, args []string) pb.Response {
  60. // 验证参数
  61. if len(args) != 3 {
  62. return shim.Error("参数个数不满足")
  63. }
  64. from := args[0]
  65. to := args[1]
  66. money, err := strconv.ParseFloat(args[2], 64)
  67. if err != nil {
  68. return shim.Error(err.Error())
  69. }
  70. // 从账本查询 from 用户
  71. fromResults, err := ReadLedger(stub, "user", []string{from})
  72. if err != nil {
  73. return shim.Error(err.Error())
  74. }
  75. if len(fromResults) != 1 {
  76. return shim.Error("没有该用户 " + from)
  77. }
  78. var fromUser map[string]interface{}
  79. if err = json.Unmarshal(fromResults[0], &fromUser); err != nil {
  80. return shim.Error(err.Error())
  81. }
  82. // 从账本查询 to 用户
  83. toResults, err := ReadLedger(stub, "user", []string{to})
  84. if err != nil {
  85. return shim.Error(err.Error())
  86. }
  87. if len(toResults) != 1 {
  88. return shim.Error("没有该用户 " + to)
  89. }
  90. var toUser map[string]interface{}
  91. if err = json.Unmarshal(toResults[0], &toUser); err != nil {
  92. return shim.Error(err.Error())
  93. }
  94. // from 用户扣除余额
  95. if money > fromUser["balance"].(float64) {
  96. return shim.Error("余额不足")
  97. }
  98. fromUser["balance"] = fromUser["balance"].(float64) - money
  99. // to 用户增加余额
  100. toUser["balance"] = toUser["balance"].(float64) + money
  101. // 写回账本
  102. err = WriteLedger(stub, fromUser, "user", []string{from})
  103. if err != nil {
  104. return shim.Error(err.Error())
  105. }
  106. err = WriteLedger(stub, toUser, "user", []string{to})
  107. if err != nil {
  108. return shim.Error(err.Error())
  109. }
  110. return shim.Success([]byte("ok"))
  111. }
  112. func main() {
  113. err := shim.Start(new(MyChaincode))
  114. if err != nil {
  115. panic(err)
  116. }
  117. }
  118. // WriteLedger 写入账本
  119. // obj 为要写入的数据
  120. // objectType和keys 共同组成复合主键
  121. func WriteLedger(stub shim.ChaincodeStubInterface, obj interface{}, objectType string, keys []string) error {
  122. //创建复合主键
  123. var key string
  124. if val, err := stub.CreateCompositeKey(objectType, keys); err != nil {
  125. return errors.New(fmt.Sprintf("%s-创建复合主键出错 %s", objectType, err.Error()))
  126. } else {
  127. key = val
  128. }
  129. bytes, err := json.Marshal(obj)
  130. if err != nil {
  131. return err
  132. }
  133. //写入区块链账本
  134. if err := stub.PutState(key, bytes); err != nil {
  135. return errors.New(fmt.Sprintf("%s-写入区块链账本出错: %s", objectType, err.Error()))
  136. }
  137. return nil
  138. }
  139. // ReadLedger 根据复合主键查询账本数据(适合获取全部或指定的数据)
  140. // objectType和keys 共同组成复合主键
  141. func ReadLedger(stub shim.ChaincodeStubInterface, objectType string, keys []string) (results [][]byte, err error) {
  142. // 通过主键从区块链查找相关的数据,相当于对主键的模糊查询
  143. resultIterator, err := stub.GetStateByPartialCompositeKey(objectType, keys)
  144. if err != nil {
  145. return nil, errors.New(fmt.Sprintf("%s-获取全部数据出错: %s", objectType, err))
  146. }
  147. defer resultIterator.Close()
  148. //检查返回的数据是否为空,不为空则遍历数据,否则返回空数组
  149. for resultIterator.HasNext() {
  150. val, err := resultIterator.Next()
  151. if err != nil {
  152. return nil, errors.New(fmt.Sprintf("%s-返回的数据出错: %s", objectType, err))
  153. }
  154. results = append(results, val.GetValue())
  155. }
  156. return results, nil
  157. }

在这段链码中,初始化的时候我们假设有用户 AB ,并且都各自有 1000 元余额,我们在 Invoke 方法中为其定义了两个功能函数 querytransfer 。 其中 query 函数可以查询 AB 或指定用户的余额信息, transfer 函数可以通过传入转账人,被转账人,金额,三个参数来实现转账功能。例如 {"Args":["transfer","A","B","100.0"]} 代表 AB 转账 100 元。

部署链码

我们将刚刚编写的智能合约也就是链码安装到区块链网络中,同样是借助 cli 服务,我们在 Taobao 组织的 peer0 节点和 JD 组织的 peer0 节点上都安装上链码:

  1. $ docker exec cli bash -c "$TaobaoPeer0Cli peer chaincode install -n fabric-realty -v 1.0.0 -l golang -p chaincode"
  2. $ docker exec cli bash -c "$JDPeer0Cli peer chaincode install -n fabric-realty -v 1.0.0 -l golang -p chaincode"

其中 -n 参数是链码名称,可以自己随便设置,-v 是链码版本号,-p 是链码的目录(我们已经将链码挂载到 cli 容器中了,在 /opt/gopath/src/ 目录下)

链码安装后,还需要实例化后才可以使用,只需要在任意一个节点实例化就可以了,以 Taobao 组织的 peer0 节点为例:

  1. $ docker exec cli bash -c "$TaobaoPeer0Cli peer chaincode instantiate -o orderer.qq.com:7050 -C appchannel -n fabric-realty -l golang -v 1.0.0 -c '{"Args":["init"]}' -P "AND ('TaobaoMSP.member','JDMSP.member')""

实例化链码主要就是传入 {"Args":["init"]} 参数,此时会调用我们编写的 func (c *MyChaincode) Init 方法,进行链码的初始化。其中 -P 参数用于指定链码的背书策略,AND ('TaobaoMSP.member','JDMSP.member') 代表链码的写入操作需要同时得到 TaobaoJD 组织成员的背书才允许通过。AND 也可以替换成 OR,代表任意一组织成员背书即可,更多具体用法,可以去看官方文档。

链码实例化成功之后就会启动链码容器,而启动的方法,就是我们之前提过的 peer 节点服务挂载了 /var/run/docker.sock 文件。

查看启动的链码容器:

  1. $ docker ps -a | awk '($2 ~ /dev-peer.*fabric-realty.*/) {print $2}'
  2. dev-peer0.taobao.com-fabric-realty-1.0.0-4f127a0415dd835529133a69b480ce24581dd5ddcaf18426ecc1d3dfb02b4670

因为我们使用 Taobao 组织的 peer0 节点实例化链码,所以此时还只有这个节点的链码容器启动起来了。

我们可以试着使用 cli 服务去调用链码:

  1. $ docker exec cli bash -c "$TaobaoPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{"Args":["query"]}'"
  2. 2022-03-22 21:13:40.152 UTC [chaincodeCmd] InitCmdFactory -> INFO 001 Retrieved channel (appchannel) orderer endpoint: orderer.qq.com:7050
  3. 2022-03-22 21:13:40.157 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 002 Chaincode invoke successful. result: status:200 payload:"[{\
  4. "balance":1000,"name":"A"},{"balance":1000,"name":"B"}]"

当然,使用JD组织的节点也是可以的:

  1. $ docker exec cli bash -c "$JDPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{"Args":["query"]}'"
  2. 2022-03-22 21:14:45.397 UTC [chaincodeCmd] InitCmdFactory -> INFO 001 Retrieved channel (appchannel) orderer endpoint: orderer.qq.com:7050
  3. 2022-03-22 21:14:45.402 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 002 Chaincode invoke successful. result: status:200 payload:"[{\
  4. "balance":1000,"name":"A"},{"balance":1000,"name":"B"}]"

此时,因为我们查询了 JD 组织的 peer0 节点上的链码,所以对应的链码容器也会启动起来了,再次查看启动的链码容器:

  1. $ docker ps -a | awk '($2 ~ /dev-peer.*fabric-realty.*/) {print $2}'
  2. dev-peer0.jd.com-fabric-realty-1.0.0-5c5e915cdcd47324151383f9619a0ff9a33283d969555e6029aa256cc389ebc9
  3. dev-peer0.taobao.com-fabric-realty-1.0.0-4f127a0415dd835529133a69b480ce24581dd5ddcaf18426ecc1d3dfb02b4670

现在,我们的智能合约就成功部署到区块链网络的通道中了。

编写应用程序

在部署链码之后,我们是使用 cli 服务去调用的,但这种方式一般只是作为验证使用,更多情况下,应该是我们自己编写应用程序集成 fabric 提供的 SDK 去调用。

Go 语言可以使用官方的 github.com/hyperledger/fabric-sdk-go 库。

这个 SDK 使用起来也很简单。

第一步调用其 New 方法创建一个 FabricSDK 实例,后续使用这个实例就可以调用操作合约的方法了。

  1. // New 根据提供的一组选项初始化 SDK
  2. // ConfigOptions 提供应用程序配置
  3. func New(configProvider core.ConfigProvider, opts ...Option) (*FabricSDK, error) {
  4. pkgSuite := defPkgSuite{}
  5. return fromPkgSuite(configProvider, &pkgSuite, opts...)
  6. }

其中 configProvider 可以从 Reader(实现了io.Reader接口的实例) 、 File(文件) 或 Raw([]byte) 获取。我们选择最简单的文件方式。

创建一个 config.yaml ,配置如下:

  1. version: 1.0.0
  2. # GO SDK 客户端配置
  3. client:
  4. # 客户端所属的组织,必须是organizations定义的组织
  5. organization: JD
  6. # 日志级别
  7. logging:
  8. level: info
  9. # MSP证书的根路径
  10. cryptoconfig:
  11. path: /network/crypto-config
  12. # 通道定义
  13. channels:
  14. appchannel:
  15. orderers:
  16. - orderer.qq.com
  17. peers:
  18. peer0.jd.com:
  19. endorsingPeer: true
  20. chaincodeQuery: true
  21. ledgerQuery: true
  22. eventSource: true
  23. peer1.jd.com:
  24. endorsingPeer: true
  25. chaincodeQuery: true
  26. ledgerQuery: true
  27. eventSource: true
  28. # 组织配置
  29. organizations:
  30. JD:
  31. mspid: "JDMSP"
  32. cryptoPath: peerOrganizations/jd.com/users/{username}@jd.com/msp
  33. peers:
  34. - peer0.jd.com
  35. - peer1.jd.com
  36. # orderer节点列表
  37. orderers:
  38. orderer.qq.com:
  39. url: orderer.qq.com:7050
  40. # 传递给gRPC客户端构造函数
  41. grpcOptions:
  42. ssl-target-name-override: orderer.qq.com
  43. keep-alive-time: 0s
  44. keep-alive-timeout: 20s
  45. keep-alive-permit: false
  46. fail-fast: false
  47. allow-insecure: true
  48. # peers节点列表
  49. peers:
  50. # peer节点定义,可以定义多个
  51. peer0.jd.com:
  52. # URL用于发送背书和查询请求
  53. url: peer0.jd.com:7051
  54. # 传递给gRPC客户端构造函数
  55. grpcOptions:
  56. ssl-target-name-override: peer0.jd.com
  57. keep-alive-time: 0s
  58. keep-alive-timeout: 20s
  59. keep-alive-permit: false
  60. fail-fast: false
  61. allow-insecure: true
  62. peer1.jd.com:
  63. url: peer1.jd.com:7051
  64. grpcOptions:
  65. ssl-target-name-override: peer1.jd.com
  66. keep-alive-time: 0s
  67. keep-alive-timeout: 20s
  68. keep-alive-permit: false
  69. fail-fast: false
  70. allow-insecure: true
  71. peer0.taobao.com:
  72. url: peer0.taobao.com:7051
  73. grpcOptions:
  74. ssl-target-name-override: peer0.taobao.com
  75. keep-alive-time: 0s
  76. keep-alive-timeout: 20s
  77. keep-alive-permit: false
  78. fail-fast: false
  79. allow-insecure: true
  80. peer1.taobao.com:
  81. url: peer1.taobao.com:7051
  82. grpcOptions:
  83. ssl-target-name-override: peer1.taobao.com
  84. keep-alive-time: 0s
  85. keep-alive-timeout: 20s
  86. keep-alive-permit: false
  87. fail-fast: false
  88. allow-insecure: true

我们假定是 JD 组织来编写这个应用程序,该配置主要就是用于验证 JD 组织及其节点的身份。

其中组织配置中 {username} 为动态传递, MSP 证书的根路径我们后续会挂载进去。

现在开始编写代码,我们先来实例化 SDK ,创建 sdk.go

  1. package main
  2. import (
  3. "github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
  4. "github.com/hyperledger/fabric-sdk-go/pkg/core/config"
  5. "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
  6. )
  7. // 配置信息
  8. var (
  9. sdk *fabsdk.FabricSDK // Fabric SDK
  10. channelName = "appchannel" // 通道名称
  11. username = "Admin" // 用户
  12. chainCodeName = "fabric-realty" // 链码名称
  13. endpoints = []string{"peer0.jd.com", "peer0.taobao.com"} // 要发送交易的节点
  14. )
  15. // init 初始化
  16. func init() {
  17. var err error
  18. // 通过配置文件初始化SDK
  19. sdk, err = fabsdk.New(config.FromFile("config.yaml"))
  20. if err != nil {
  21. panic(err)
  22. }
  23. }
  24. // ChannelExecute 区块链交互
  25. func ChannelExecute(fcn string, args [][]byte) (channel.Response, error) {
  26. // 创建客户端,表明在通道的身份
  27. ctx := sdk.ChannelContext(channelName, fabsdk.WithUser(username))
  28. cli, err := channel.New(ctx)
  29. if err != nil {
  30. return channel.Response{}, err
  31. }
  32. // 对区块链账本的写操作(调用了链码的invoke)
  33. resp, err := cli.Execute(channel.Request{
  34. ChaincodeID: chainCodeName,
  35. Fcn: fcn,
  36. Args: args,
  37. }, channel.WithTargetEndpoints(endpoints...))
  38. if err != nil {
  39. return channel.Response{}, err
  40. }
  41. //返回链码执行后的结果
  42. return resp, nil
  43. }
  44. // ChannelQuery 区块链查询
  45. func ChannelQuery(fcn string, args [][]byte) (channel.Response, error) {
  46. // 创建客户端,表明在通道的身份
  47. ctx := sdk.ChannelContext(channelName, fabsdk.WithUser(username))
  48. cli, err := channel.New(ctx)
  49. if err != nil {
  50. return channel.Response{}, err
  51. }
  52. // 对区块链账本查询的操作(调用了链码的invoke),只返回结果
  53. resp, err := cli.Query(channel.Request{
  54. ChaincodeID: chainCodeName,
  55. Fcn: fcn,
  56. Args: args,
  57. }, channel.WithTargetEndpoints(endpoints...))
  58. if err != nil {
  59. return channel.Response{}, err
  60. }
  61. //返回链码执行后的结果
  62. return resp, nil
  63. }

在这段代码中,我们将使用 Admin 的身份去调用合约,并将每次的交易同时发送给 peer0.jd.compeer0.taobao.com 节点进行背书,这是因为我们在实例化链码的时候指定了背书策略为 AND ('TaobaoMSP.member','JDMSP.member') ,代表交易需要同时得到 TaobaoJD 组织成员的背书才允许通过。每次写入账本时,会验证这两个节点的数据一致性,只有当这两个节点的数据一致时,交易才算最终成功。

继续编写 main.go ,我们使用 gin 来创建一个 http 服务:

  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "github.com/gin-gonic/gin"
  6. )
  7. func main() {
  8. g := gin.Default()
  9. g.GET("/query", func(c *gin.Context) {
  10. args := make([][]byte, 0)
  11. user := c.Query("user")
  12. if user != "" {
  13. args = append(args, []byte(user))
  14. }
  15. // 调用链码的query函数
  16. resp, err := ChannelQuery("query", args)
  17. if err != nil {
  18. c.AbortWithStatusJSON(500, gin.H{"err": err.Error()})
  19. return
  20. }
  21. var data []map[string]interface{}
  22. if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
  23. c.AbortWithStatusJSON(500, gin.H{"err": err.Error()})
  24. return
  25. }
  26. c.JSON(200, data)
  27. })
  28. g.POST("/transfer", func(c *gin.Context) {
  29. from := c.Query("from")
  30. to := c.Query("to")
  31. money := c.Query("money")
  32. if from == "" || to == "" || money == "" {
  33. c.AbortWithStatusJSON(400, gin.H{"err": "参数不能为空"})
  34. return
  35. }
  36. args := make([][]byte, 0)
  37. args = append(args, []byte(from), []byte(to), []byte(money))
  38. // 调用链码的transfer函数
  39. resp, err := ChannelExecute("transfer", args)
  40. if err != nil {
  41. c.AbortWithStatusJSON(500, gin.H{"err": err.Error()})
  42. return
  43. }
  44. c.JSON(200, gin.H{"msg": string(resp.Payload)})
  45. })
  46. g.Run("0.0.0.0:8000")
  47. }

main 函数中,我们创建了两个接口 GET /queryPOST /transfer ,其中 /query 接口调用链码的 query 函数功能实现查询用户余额,/transfer 接口调用链码的 transfer 函数功能实现转账功能。

我们将继续使用 Docker 部署该应用程序,这样的好处是可以和区块链网络处于同一网络下,方便调用节点,当然你也可以更改 config.yaml 文件去调用暴露在宿主机的节点端口也是可以的,首先编写 Dockerfile 文件:

  1. FROM golang:1.14 AS app
  2. ENV GO111MODULE=on
  3. ENV GOPROXY https://goproxy.cn,direct
  4. WORKDIR /root/togettoyou
  5. COPY . .
  6. RUN CGO_ENABLED=0 go build -v -o "app" .
  7. FROM scratch
  8. WORKDIR /root/togettoyou/
  9. COPY --from=app /root/togettoyou/app ./
  10. COPY --from=app /root/togettoyou/config.yaml ./
  11. ENTRYPOINT ["./app"]

docker-compose.yml 文件:

  1. version: '2.1'
  2. networks:
  3. fabric_network:
  4. external:
  5. name: fabric_network
  6. services:
  7. app:
  8. build: .
  9. image: app:latest
  10. ports:
  11. - "8000:8000"
  12. volumes:
  13. - ./../network/crypto-config:/network/crypto-config # 挂载搭建区块链网络时生成的crypto-config文件夹
  14. networks:
  15. - fabric_network

其中挂载的 crypto-config 文件夹就是之前搭建区块链网络时生成的。

编译部署应用程序:

  1. $ docker-compose build
  2. $ docker-compose up

调用应用程序的接口:

  1. $ curl "http://localhost:8000/query"
  2. [{"balance":1000,"name":"A"},{"balance":1000,"name":"B"}]
  3. $ curl "http://localhost:8000/query?user=A"
  4. [{"balance":1000,"name":"A"}]
  5. $ curl "http://localhost:8000/query?user=B"
  6. [{"balance":1000,"name":"B"}]
  7. $ curl -X POST "http://localhost:8000/transfer?from=A&to=B&money=500"
  8. {"msg":"ok"}
  9. $ curl "http://localhost:8000/query"
  10. [{"balance":500,"name":"A"},{"balance":1500,"name":"B"}]

到这里,我们就已经完整地实现了一个区块链应用了。你也可以继续为这个区块链应用实现前端页面。流程呢,和传统前后端分离架构也没什么区别。