小坚的技术博客

hyperledger-fabric测试环境搭建

本文作者:陈进坚
个人博客:https://jian1098.github.io
CSDN博客:https://blog.csdn.net/c_jian
简书:https://www.jianshu.com/u/8ba9ac5706b6
联系方式:jian1098@qq.com

环境安装


安装docker

必须是CE(社区)版,如果装企业版的只能卸载重装,否则会出错;如果已安装可跳过,下面是CentOS安装步骤

  • 设置仓库
1
2
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  • 安装docker-ce
1
yum install docker-ce
  • 查看版本
1
docker -v

Ubuntu安装docker

1
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

安装docker-compose

如果已安装可跳过

  • 下载

    https://github.com/docker/compose/releases查看最新版本,替换下面的链接,后面不要带-rc

1
curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose
  • 添加权限
1
chmod +x /usr/local/bin/docker-compose
  • 查看版本
1
docker-compose --version

安装go语言环境

需要 go1.11以上版本,如果已安装可跳过

  • 安装程序

到官网https://golang.google.cn/dl/复制最新的下载地址,然后下载压缩包

1
wget https://dl.google.com/go/go1.13.1.linux-amd64.tar.gz

解压

1
tar zxvf go1.13.1.linux-amd64.tar.gz -C /opt/
  • 配置环境
1
2
mkdir go		#创建项目目录
vi /etc/profile

将下面的GOPATH路径修改为你的项目路径,然后将3条命令添加到文件的最后,保存;第一个是工作目录,第二个是go程序目录

1
2
3
export GOROOT=/opt/go
export GOPATH=/home/jian/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

执行下面的命令使环境变量生效

1
source /etc/profile

查看配置好的go环境变量

1
go env

查看版本

1
go version

安装python

需要python 2.7.x版本,一般系统已自带有,如果没有,可以按下面的步骤安装

https://www.python.org/ftp/python/选择合适的版本,以下例子基于python 2.7.9,其他版本同理

  • 下载

    1
    wget https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz
  • 安装

    依次执行下面5行命令即可

    1
    2
    3
    4
    5
    tar -zxvf Python-2.7.9.tgz
    cd Python-2.7.9
    ./configure --prefix=/usr/local/python-2.7.9
    make
    make install
  • 查看版本

    1
    python --version

安装node.js及npm

如果你将用Node.js的Hyperledger Fabric SDK开发Hyperledger Fabric的应用程序,则需安装Node.js的8.9.x版本

依次执行下面的命令即可,要装其他版本可以修改10.x为你要的版本

1
2
3
4
curl -sL https://rpm.nodesource.com/setup_10.x | bash -
sudo yum clean all && sudo yum makecache fast
sudo yum install -y gcc-c++ make
sudo yum install -y nodejs

查看版本

1
node -v

如果你想要其他版本的Node.js的话,那么执行命令可以用下面的命令将已安装的Node.js移除,然后重新安装即可

1
yum remove nodejs

搭建fabric环境


参照官方文档https://hyperledger-fabric.readthedocs.io/en/latest/install.html,2.2及以后版本参考https://hyperledger-fabric.readthedocs.io/en/release-2.2/test_network.html,步骤如下

安装示例、二进制文件和 Docker 镜像

下面的命令下载并执行一个 bash 脚本,该脚本将下载并提取设置网络所需的所有特定于平台的二进制文件,并将它们放入fabric-samples文件夹中;然后,该脚本会将从 Docker Hub 上下载 Hyperledger Fabric docker 镜像到本地 Docker 注册表中,并将其标记为 ‘latest’。

1
curl -sSL http://bit.ly/2ysbOFE | bash -s		# 服务器需要科学上网

如果要指定版本需要加一个 Fabric、Fabric-ca 和第三方 Docker 镜像的版本号

1
curl -sSL http://bit.ly/2ysbOFE | bash -s -- 1.4.2 1.4.2 0.4.15

如果你的服务器无法科学上网,可以到http://note.youdao.com/noteshare?id=4fb074480d296adf1e931c734e18d3bd&sub=2C7210BDD6D04349B332CD66131C58ED获取脚本,然后保存到bootstrap.sh文件中,然后添加权限chmod +xbootstrap.sh `执行脚本即可

1
2
chmod +x bootstrap.sh
bash ./bootstrap.sh

执行完后会得到fabric-samples目录

生成网络构件

2.2以前的版本进入/fabric-samples/first-network目录,2.2以后的版本的目录在fabric-samples/test-network,执行下面的命令,然后输入Y继续,注意2.2版本及之后的版本不需要执行

1
./byfn.sh generate

上面的命令为我们的各种网络实体生成证书和秘钥。创世区块 genesis block 用于引导排序服务,也包含了一组配置 Channel 所需要的配置交易集合

关闭网络

执行下面的命令,然后输入Y继续

2.2以后的版本的目录在fabric-samples/test-network,2.2以前的版本进入fabric-samples/first-network

1
2
./byfn.sh down			//2.2以前的版本
./network.sh down //2.2及以后的版本

上面的命令会结束掉你所有的容器,移除加密的材料和四个构件,并且从 Docker 仓库删除链码镜像

启动网络

执行下面的命令,然后输入Y继续

1
2
./byfn.sh up				# 默认golang启动
./byfn.sh up -l javascript #启动node.js版本,旧版本的命令是./byfn.sh up -l node

2.2及以后的版本将./byfn.sh换成./network.sh即可

上面的命令会编译 Golang 智能合约的镜像并且启动相应的容器。Go 语言是默认的链码语言,但是它也支持 Node.jsJava 的链码,详情可以看官方文档https://hyperledger-fabric.readthedocs.io/zh_CN/release-1.4/build_network.html;

这一步会启动所有的容器,然后启动一个完整的 end-to-end 应用场景,并且会打印下面的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Continue? [Y/n]
proceeding ...
Creating network "net_byfn" with the default driver
Creating peer0.org1.example.com
Creating peer1.org1.example.com
Creating peer0.org2.example.com
Creating orderer.example.com
Creating peer1.org2.example.com
Creating cli


____ _____ _ ____ _____
/ ___| |_ _| / \ | _ \ |_ _|
\___ \ | | / _ \ | |_) | | |
___) | | | / ___ \ | _ < | |
|____/ |_| /_/ \_\ |_| \_\ |_|

Channel name : mychannel
Creating channel...

等两分钟后命令会自动结束然后打印下面的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Querying chaincode on peer1.org2...
===================== Querying on peer1.org2 on channel 'mychannel'... =====================
+ peer chaincode query -C mychannel -n mycc -c '{"Args":["query","a"]}'
Attempting to Query peer1.org2 ...3 secs
+ res=0
+ set +x

90
===================== Query successful on peer1.org2 on channel 'mychannel' =====================

========= All GOOD, BYFN execution completed ===========


_____ _ _ ____
| ____| | \ | | | _ \
| _| | \| | | | | |
| |___ | |\ | | |_| |
|_____| |_| \_| |____/

如果启动报错,建议先执行一次关闭网络操作清空数据

至此,fabric的环境搭建完成

名词解释


fabric ca

数字签名授权,任何一个操作都需要数字签名证书

fabric peer

节点,区块存储好的位置

ordering服务

创建区块,验证和排序服务

channel

每个channel都是独立的fabric实例,数据不互通

一个peer可以有多个channel,一个channel可能有多个peer

chaincode

智能合约,也称链码,chaincode属于某个channel

生命周期

  • 安装install
  • 实例化init
  • 调用invoke

MSP

membership service provider,会员服务提供者,管理peer的身份和访问许可,每个peer都有自己的MSP证书

工作流程


提案

通过sdk向各个peer发起更新数据提案

背书

endorsing,足够多的peer发回响应给sdk

更新申请

sdk将更新申请发送给orderer

调用更新

orderer节点验证更新操作(消息队列)和数字证书没问题后各个peer执行更新数据

Chaincode链码

  • 开发者必须同时实现chaincode的InitInvoke方法,chaincode编写完需要通过peerchaincode install命令安装
  • fabric 的数据以键值对的形式存放在peerlevelDB中,可以切换为couchDB

链码的生命周期

  • install:将已编写完成的链码安装在网络节点中。
  • instantiate:对已安装的链码进行实例化。
  • upgrade:对已有链码进行升级。链代码可以在安装后根据具体需求的变化进行升级。
  • package:对指定的链码进行打包的操作。
  • singnpackage:签名。

链码结构

node.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const shim = require('fabric-shim');

const Chaincode = class {
async Init(stub) { //初始化参数
await stub.putState(key, Buffer.from(aStringValue));
return shim.success(Buffer.from('Initialized Successfully!'));
}

async Invoke(stub) { //调用方法,主要写逻辑业务
// 读取数据
let oldValue = await stub.getState(key);

// 写入数据,如果key存在则为更新
let newValue = oldValue + delta; //定义数据,只能是键值对,存数组可以转json
await stub.putState(key, Buffer.from(newValue));

//删除数据
await stub.deleteState(key);

return shim.success(Buffer.from(newValue.toString()));
}
};

shim.start(new Chaincode());

用NodeJs编写链码

创建目录

1
mkdir /root/fish/chaincode/fishcc

安装依赖

1
2
cd /root/fish/chaincode/fishcc
npm install --save fabric-shim

编写合约代码

1
vi index.js

示例:江苏省农牧厅渔业管理系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
'use strict'
const shim = require('fabric-shim');
const util = require('util');

let Chaincode = class {
//初始化方法,不用写什么东西
async Init(stub) {
console.info('初始化成功');
return shim.success();
}

//调用方法,主要写逻辑业务
async Invoke(stub) {
let ret = stub.getFunctionAndParameters(); //获取函数和参数

let method = this[ret.fcn];
if (!method) {
console.error('找不到要调用的函数,函数名:' + ret.fcn);
throw new Error('找不到要调用的函数,函数名:' + ret.fcn);
}

try {
let payload = await method(stub,ret.params); //直接调用函数,获取返回值
return shim.success(payload);
} catch (err) {
console.log(err);
return shim.error(err);
}
}

//查询fish信息
async queryFish(stub,args) {
if(args.length != !){
throw new Error('错误的调用函数,实例:FISH01');
}
let fishNumber = args[0];
let fishAsBytes = await stub.getState(fishNumber); //从账本中获取fish的信息,账本是二进制存储的
if (!fishAsBytes || fishAsBytes.toString().length <= 0) {
throw new Error(fishAsBytes + '不存在');
}
console.log(fishAsBytes.toString());
return fishAsBytes;
}

//初始化账本方法,官方建议单独写,不用最上面点Init方法
async initLedger(stub,args){
console.info('开始:初始化账本');
let fishes = [];
fishes.push({
vessel:"奋进号38A",
location:"12,34",
timestamp:"1598509989",
holder:"wang"
});
fishes.push({
vessel:"奋进号39A",
location:"123,346",
timestamp:"1598509989",
holder:"gao"
});
fishes.push({
vessel:"奋进号40A",
location:"1234,3467",
timestamp:"1598509989",
holder:"liu"
});

for (let i = 0; i < fishes.length; i++) {
await stub.putState('FISH' + i,Buffer.from(JSON.stringify(fishes[i])));
console.info('Add <--> ',fishes[i]);
}
console.info('结束:初始化账本');
}

//记录fish信息
async recoredFish(stub,args){
console.info('开始:记录fish信息');
if (args.length != 5) {
throw new Error('需要5个参数');
}

var fish = {
vessel:args[1],
location:args[2],
timestamp:args[3],
holder:args[4]
}
await stub.putState(args[0],Buffer.from(JSON.stringify(fishes)));
console.info('结束:记录fish信息');
}

//查询所有fish
async queryAllFish(stub,args){
let startKey = 'FISH0';
let endKey = 'FISH999';
let iterator = await stub.getStateByRange(startKey,endKey);

let allResults = [];
while (true) {
let res = await iterator.next();

if (res.value && res.value.value.toString()) {
let jsonRes = {};
console.log(res.value.value.toString('utf8'));

jsonRes.Key = res.value.key;
try {
jsonRes.Record = JSON.parse(res.value.value.toString('utf8'));
} catch (err) {
console.log(err);
jsonRes.Record = res.value.value.toString('utf8');
}
allResults.push(jsonRes);
}

if (res.done) {
console.log('end of data');
await iterator.close();
console.info(allResults);
return Buffer.from(JSON.stringify(allResults));
}
}
}

//更改归属人
async changeFishHolder(stub,args){
console.info('开始:更改归属人');
if (args.length != 2) {
throw new Error('需要2个参数');
}

let fishAsBytes = await stub.getState(args[0]);
let fish = JSON.parse(fishAsBytes);
fishAsBytes.holder = args[1];

await stub.putState(args[0],Buffer.from(JSON.stringify(fish)));
console.info('结束:更改归属人');
}
}

shim.start(new Chaincode());

用Golang编写链码

示例:两个账户转账与查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package main

import (
"fmt"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/peer"
"strconv"
)

type ChainCode struct{}

func main() {
err := shim.Start(new(ChainCode))

if( err!= nil){
fmt.Printf("Error starting Simple Chaincode is %s \n",err)
}
}

//链码初始化
func (cc *ChainCode) Init(stub shim.ChaincodeStubInterface) peer.Response {
fmt.Println("链码实例例化")
_, args := stub.GetFunctionAndParameters()
var accountA, accountB string // 定义账号
var balanceA, balanceB int // 定义余额
var err error

if len(args) != 4 {
return shim.Error("参数数量错误")
}

// 初始化余额
accountA = args[0]
balanceA, err = strconv.Atoi(args[1])
if err != nil {
return shim.Error("请输入整数余额")
}
accountB = args[2]
balanceB, err = strconv.Atoi(args[3])
if err != nil {
return shim.Error("请输入整数余额")
}
fmt.Printf("A余额 = %d, B余额 = %d\n", balanceA, balanceB)

// 数据上链
err = stub.PutState(accountA, []byte(strconv.Itoa(balanceA)))
if err != nil {
return shim.Error(err.Error())
}

err = stub.PutState(accountB, []byte(strconv.Itoa(balanceB)))
if err != nil {
return shim.Error(err.Error())
}

return shim.Success(nil)
}

//调用链码
func (cc *ChainCode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
fmt.Println("链码调用")
function, args := stub.GetFunctionAndParameters()
if function == "transfer" {
// 转账
return cc.transfer(stub, args)
} else if function == "delete" {
// 删除账户
return cc.delete(stub, args)
} else if function == "query" {
//查询余额
return cc.query(stub, args)
} else if function == "create" {
//创建账户
return cc.create(stub, args)
}

return shim.Error("请输入正确的方法名. 方法名只能是 \"invoke\" \"delete\" \"query\" \"create\"")
}

// 从A账户转移资产给B账户
func (cc *ChainCode) transfer(stub shim.ChaincodeStubInterface, args []string) peer.Response {
var accountA, accountB string // 定义账号
var balanceA, balanceB int // 定义余额
var X int // 交易数量
var err error

if len(args) != 3 {
return shim.Error("参数数量错误")
}

accountA = args[0]
accountB = args[1]

// 读取余额
balanceAbytes, err := stub.GetState(accountA)
if err != nil {
return shim.Error("获取数据失败")
}
if balanceAbytes == nil {
return shim.Error("找不到账号信息")
}
balanceA, _ = strconv.Atoi(string(balanceAbytes))

balanceBbytes, err := stub.GetState(accountB)
if err != nil {
return shim.Error("获取数据失败")
}
if balanceBbytes == nil {
return shim.Error("找不到账号信息")
}
balanceB, _ = strconv.Atoi(string(balanceBbytes))

// 执行转账
X, err = strconv.Atoi(args[2])
if err != nil {
return shim.Error("转账数量必须是整数")
}

if balanceA < X {
return shim.Error("余额不足")
}

balanceA = balanceA - X
balanceB = balanceB + X
fmt.Printf("转账后A余额 = %d, B余额 = %d\n", balanceA, balanceB)

// 数据写入账本
err = stub.PutState(accountA, []byte(strconv.Itoa(balanceA)))
if err != nil {
return shim.Error(err.Error())
}

err = stub.PutState(accountB, []byte(strconv.Itoa(balanceB)))
if err != nil {
return shim.Error(err.Error())
}

return shim.Success(nil)
}

// 删除某个账户实体
func (cc *ChainCode) delete(stub shim.ChaincodeStubInterface, args []string) peer.Response {
if len(args) != 1 {
return shim.Error("参数数量错误")
}

A := args[0]

// 删除数据
err := stub.DelState(A)
if err != nil {
return shim.Error("删除数据失败")
}

return shim.Success(nil)
}

// 查询账户的资产,对应peer chaincode query
func (cc *ChainCode) query(stub shim.ChaincodeStubInterface, args []string) peer.Response {
var account string
var err error

if len(args) != 1 {
return shim.Error("参数数量错误")
}

account = args[0]

// 从账本中获取状态
balanceAbytes, err := stub.GetState(account)
if err != nil {
jsonResp := "获取" + account + "数据失败"
return shim.Error(jsonResp)
}

if balanceAbytes == nil {
jsonResp := "找不到账号信息"
return shim.Error(jsonResp)
}

jsonResp := "{\"Name\":\"" + account + "\",\"Amount\":\"" + string(balanceAbytes) + "\"}"
fmt.Printf("查询结果:%s\n", jsonResp)
return shim.Success(balanceAbytes)
}

//创建账户
func (cc *ChainCode) create(stub shim.ChaincodeStubInterface, args []string) peer.Response {
var account string
var balanceA int
var err error

if len(args) != 2 {
return shim.Error("参数数量错误")
}

// 初始化账号信息
account = args[0]
balanceA, err = strconv.Atoi(args[1])
if err != nil {
return shim.Error("余额输入错误")
}

fmt.Printf("balanceA余额 = %d\n", balanceA)

// 写入状态到账本
err = stub.PutState(account, []byte(strconv.Itoa(balanceA)))
if err != nil {
return shim.Error(err.Error())
}

return shim.Success(nil)
}

参考文档

视频:

https://www.bilibili.com/video/BV1554y1q7iE

https://www.bilibili.com/video/[BV1zt411H7qX](https://www.bilibili.com/video/BV1zt411H7qX)/

文档

https://github.com/itheima1/hyperledger

-------------本文结束感谢您的阅读-------------
🐶 您的支持将鼓励我继续创作 🐶