本文作者:陈进坚
个人博客:https://jian1098.github.io
CSDN博客:https://blog.csdn.net/c_jian
简书:https://www.jianshu.com/u/8ba9ac5706b6
联系方式:[email protected]
环境安装
安装docker
必须是CE(社区)版,如果装企业版的只能卸载重装,否则会出错;如果已安装可跳过,下面是CentOS
安装步骤
- 设置仓库
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
yum install docker-ce
- 查看版本
docker -v
Ubuntu
安装docker
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
安装docker-compose
如果已安装可跳过
-
下载
去
https://github.com/docker/compose/releases
查看最新版本,替换下面的链接,后面不要带-rc
的
curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose
- 添加权限
chmod +x /usr/local/bin/docker-compose
- 查看版本
docker-compose --version
安装go语言环境
需要 go1.11
以上版本,如果已安装可跳过
- 安装程序
到官网https://golang.google.cn/dl/
复制最新的下载地址,然后下载压缩包
wget https://dl.google.com/go/go1.13.1.linux-amd64.tar.gz
解压
tar zxvf go1.13.1.linux-amd64.tar.gz -C /opt/
- 配置环境
mkdir go #创建项目目录
vi /etc/profile
将下面的GOPATH路径修改为你的项目路径,然后将3条命令添加到文件的最后,保存;第一个是工作目录,第二个是go程序目录
export GOROOT=/opt/go
export GOPATH=/home/jian/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
执行下面的命令使环境变量生效
source /etc/profile
查看配置好的go环境变量
go env
查看版本
go version
安装python
需要python 2.7.x
版本,一般系统已自带有,如果没有,可以按下面的步骤安装
到https://www.python.org/ftp/python/
选择合适的版本,以下例子基于python 2.7.9,其他版本同理
-
下载
wget https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz
-
安装
依次执行下面5行命令即可
tar -zxvf Python-2.7.9.tgz cd Python-2.7.9 ./configure --prefix=/usr/local/python-2.7.9 make make install
-
查看版本
python --version
安装node.js及npm
如果你将用Node.js的Hyperledger Fabric SDK开发Hyperledger Fabric的应用程序,则需安装Node.js的8.9.x版本
依次执行下面的命令即可,要装其他版本可以修改10.x
为你要的版本
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
查看版本
node -v
如果你想要其他版本的Node.js的话,那么执行命令可以用下面的命令将已安装的Node.js移除,然后重新安装即可
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’。
curl -sSL http://bit.ly/2ysbOFE | bash -s # 服务器需要科学上网
如果要指定版本需要加一个 Fabric、Fabric-ca 和第三方 Docker 镜像的版本号
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 +x
bootstrap.sh `执行脚本即可
chmod +x bootstrap.sh
bash ./bootstrap.sh
执行完后会得到fabric-samples
目录
生成网络构件
2.2以前的版本进入/fabric-samples/first-network
目录,2.2以后的版本的目录在fabric-samples/test-network
,执行下面的命令,然后输入Y继续,注意2.2
版本及之后的版本不需要执行
./byfn.sh generate
上面的命令为我们的各种网络实体生成证书和秘钥。创世区块 genesis block
用于引导排序服务,也包含了一组配置 Channel 所需要的配置交易集合
关闭网络
执行下面的命令,然后输入Y继续
2.2以前的版本的目录在fabric-samples/test-network
./byfn.sh down //2.2以前的版本
./network.sh down //2.2及以后的版本
上面的命令会结束掉你所有的容器,移除加密的材料和四个构件,并且从 Docker 仓库删除链码镜像
启动网络
执行下面的命令,然后输入Y继续
./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.js 和 Java 的链码,详情可以看官方文档https://hyperledger-fabric.readthedocs.io/zh_CN/release-1.4/build_network.html
;
这一步会启动所有的容器,然后启动一个完整的 end-to-end 应用场景,并且会打印下面的日志
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...
等两分钟后命令会自动结束然后打印下面的日志
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的
Init
和Invoke
方法,chaincode编写完需要通过peer
的chaincode install
命令安装 - fabric 的数据以键值对的形式存放在
peer
的levelDB
中,可以切换为couchDB
链码的生命周期
- install:将已编写完成的链码安装在网络节点中。
- instantiate:对已安装的链码进行实例化。
- upgrade:对已有链码进行升级。链代码可以在安装后根据具体需求的变化进行升级。
- package:对指定的链码进行打包的操作。
- singnpackage:签名。
链码结构
node.js
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编写链码
创建目录
mkdir /root/fish/chaincode/fishcc
安装依赖
cd /root/fish/chaincode/fishcc
npm install --save fabric-shim
编写合约代码
vi index.js
示例:江苏省农牧厅渔业管理系统
'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编写链码
示例:两个账户转账与查询
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://github.com/itheima1/hyperledger