返回

Hyperledger Fabric基础

Fabric环境搭建

准备工作

  1. 官方帮助文档
  2. 安装cURLcurd-version查i间版本
  3. 安装docker,docker-version查i间版本
  4. 安装docker-compose,docker-compose --version查询版本
  5. 安装go语言环境,go version查询版本
  6. 安装node.js,node -v查间版本
  7. 安装python,python-version查询版本

docker镜像

镜像文件名称 用途
hyperledger/fabric-peer peer模块镜像文件
hyperledger/fabric-orderer orderer节点库镜像文件
hyperledger/fabric-ccenv Go语言chaincode运行环境库镜像文件
hyperledger/fabric-tools 相关工具镜像文件包含了cryptogenconfigtxgen等工具
hyperledger/fabric-ca CA模块镜像文件
hyperledger/fabric-couchdb couchdb数据库镜像文件
hyperledger/fabric-kafka kafka库镜像文件
hyperledger/fabric-zookeeper zookeeper库镜像文件

Fabric基本概念

逻辑架构

  • 身份管理

    • 会员注册
      • 注册成功一个账号得到的不是用户名密码
      • 使用证书作为身份认证的标志(非对称加密密钥对:私钥签名,公钥验证签名)
    • 身份保护
      • 用户保护好私钥就行
    • 交易审计
      • 保证实名制
    • 内容保密
      • 不同于公链只有一条主链
      • Fabric中有可以有多条主链,各个链用通道来区分
      • 一个节点可以加入多个通道,通道可以看作一个群,
      • 一个节点在各个通道中传输中的内容是保密的
  • 账本管理

    • 区块链
      • 在分布式网络中,记录会同步存储在所有节点中
      • 区块链保存所有的交易记录
    • 世界状态
      • 数据的最新状态
      • 数据存储在当前节点的数据库中
        • 自带的默认数据库:levelDB,也可以使用couchdb
        • 以键值对的方式进行存储
  • 交易管理

    • 部署交易

      • 部署的是链码,就是给节点安装链码chaincode,也就是智能合约
    • 调用交易

      • invoke
  • 智能合约

    • 一段代码,处理网络成员所同意的业务逻辑
    • 真正实现了链码和账本的分离(逻辑和数据分离)

基础概念

组织

  • 有用户
  • 有进行数据处理的节点peer
    • put:写数据到区块链中
    • get:从区块链查询数据

节点

  • client:进行交易管理,方式有clinode.jsjava sdk
    • cli:通过linux命令进行操作,使用的是shell命令对数据进行提交和查询(测试环境)
    • node.js:通过node.js实现一个客户端,在浏览器访问(生产环境)
    • java:通过java api实现一个客户端
    • go:用go写一个服务端,然后用浏览器访问服务端实现操作
  • peer:存储和同步账本数据
    • 用户通过客户端工具对数据进行put操作,数据写入到一个节点里边
    • 数据同步是fabric框架自动实现的
  • orderer:排序和分发交易
    • 排序是为了解决双花问题:同一个人在同一时刻发起多笔交易
    • 没发起的一般交易都会在order节点进行排序
    • 交易数据需要先进行打包,然后写入到区块中

通道channel

  • 通道是有共识服务(ordering)提供的一种通讯机制,将peerorder连接在一起,形成一个个具有保密性的通讯链路(虚拟),实现了业务隔离的要求:通道也与账本(ledger)-状态(worldstate)紧密相关

  • consensus service:orderer节点
  • 三条不同颜色的线代表三个通道
  • 一个peer节点可以同时加入到不同的通道中
  • peer节点每加入到一个新的通道,存储数据的区块链就需要添加一条,只要加入到通道中就可以拥有这个通道中的数据,每个通道对应一个区块链

交易流程

要完成交易,这笔交易必须要有背书策略,假设背书策略为:

  • 组织A中的成员必须同意
  • 组织B中的成员也必须同意
  1. Application/SDK:充当客户端client的角色
    • 写数据,查询数据
  2. 客户端发起一个提案,给到peer节点
    • 会发送给组织A和组织B中的节点
  3. peer节点将交易进行预演,会得到一个结果
  4. peer节点将交易结果发送给客户端client
    • 若模拟交易失败,整个交易流程终止
    • 成功,继续
  5. 客户端client将交易提交给排序节点orderer
  6. 排序节点orderer对交易打包
  7. orderer节点将打包数据发送给peer节点,peer节点将数据写入区块中
    • 打包数据的发送不是实时的
    • 有设定条件,在配置文件中
    • 达到设定条件时才会给peer发送打包好的区块数据

背书策略:

  • 要完成一笔交易,这笔交易的完成过程叫背书

账本

  • 每个peer节点中都有一个账本ledger
  • 账本由两部分组成:文件系统、数据库
  • 同一个通道中的peer节点的账本里存储的内容是一样的

Fabric核心模块

Fabric是一个由五个核心模块组成的程序组,在fabric成功编译完成之后,一共会有五个核心模块,如下:

模块名称 功能
peer 主节点模块
orderer 交易打包,排序模块
cryptogen 组织和证书生成模块
configtxgen 区块和交易生成模块
configtxlator 区块和交易解析模块
  • 五个模块中peerorderer属于系统模块,cryptogen,configtxgen,configtlator属于工具模块,工具模块负责证书文件、区块链创始块、通道创始块等相关文件和证书的生成工作,但是工具模块不参与系统的运行。peer模块和orderer模块作为系统模块是Fabric的核心模块,启动之后会以守护进程的方式在系统后台长期运行
  • Fabric的5个核心模块都是基于命令行的方式运行的,目前Fabric没有为这些模块提供相关的图形界面,因此想要熟练使用Fabric的这些核心模块,必须熟悉这些模块的命令选项

cryptogen

cryptogen模块主要用来生成组织结构和账号相关的文件,任何Fabric系统的开发通常都是从cryptogen模块开始的。在Fabric项目中,当系统设计完成之后第一项工作就是根据系统的设计来编写cryptogen的配置文件,然后通过这些配置文件生成相关的证书文件

cryptogen模块所使用的配置文件是整个Fabric项目的基石。下面我们将介绍cryptogen模块命令行选项及其使用方式

cryptogen模块命令

cryptogen模块是通过命令行的方式运行的,一个cryptogen命令由命令行参数配置文件两部分组成,通过执行命令cryptogen --help可以显示cryptogen模块的命令行选项

手动搭建Fabric网络

  1. 生成fabric证书:fabric框架中的所有角色间通信加密需要证书
  2. 生成创始块文件和通道文件:配置fabrci框架中有哪些组织,组织中有哪些成员
  3. 编写docker-compose文件:对各个组织中的成员做对应的镜像启动
  4. 管理channel:在对应的节点上创建通道,将对应的peer节点添加到通道中
  5. chaincode的安装和实例化:给peer节点安装链码并初始化后即可进行交易

我们自己搭建一个fabric网络,网络结构如下:

  • 排序节点1个
  • 组织个数2个,分别为go和cpp,每个组织分别由两个节点,三个用户组成
组织名 组织标识符 组织ID
Go学科 org_go OrgGoMSP
CPP org_cpp OrgCppMSP

一些理论基础:

  • msp
    • Membership service provider (MSP)是一个提供虚拟成员操作的管理框架的组件
    • 理解为账号
      • 每个节点都有一个msp账号
  • 锚节点:负责组织间通信的节点
  • peer节点的种类:

生成fabric证书

命令介绍

1
$ cryptogen --help

编写配置文件crypto-config.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
OrdererOrgs: # 排序节点组织的信息
	-  Name: Orderer # 组织名
	   Domain: example.com # 根域名
	   Specs: # 几个orderer节点下面写几个子域名
	   	- Hostname: orderer # orderer.example.com
	   	- Hostname: order2 # order2.example.com

PeerOrgs: # peer节点组织的信息
	- Name: OrgGo # 组织名
	  Domain: orggo.example.com # 根域名
	  EnableNodeOUs: true # 启用cryptogen
	  Template: # 模板,根据默认规则生成2个peer存储数据的节点
	  	Count: 2 # 1. peer0.org1.example.com 2. peer1.org1.example.com
	  Users: # 创建的普通用户的个数
	  	Count: 3

	- Name: OrgCpp
	  Domain: orgcpp.example.com
	  EnableNodeOUs: true
	  Template:
	  	Count: 2
	  Users:
	  	Count: 3
  • 上面使用的域名,在生产环境下需要注册备案,测试环境下随便写

通过命令生成证书文件

1
$ cryptogen generate --config=yaml配置文件名

生成创始块文件和通道文件

命令介绍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ configtxgen --help
	# 输出创始块区块文件的路径和名字
	`-outputBlock string`
	# 指定创建的channel的名字,如果没指定系统会提供一个默认的名字.
	`-channelID string`
	# 表示输出通道文件路径和名字
	`-outputCreateChannelTx string`
	# 指定配置文件中的节点
	`-profile string`
	# 更新channel的配置信息
	`-outputAnchorPeersUpdate string`
	# 指定所属的组织名称
	`-asOrg string`
	# 要想执行这个命令,需要一个配置文件 configtx.yaml

编写配置文件configtx.yaml

 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
# configtx.yaml
Organization: # 固定的不能改
	- &OrderOrg # 排序节点组织,自己起个名字
		Name: OrdererOrg # 排序节点的组织名
		ID: OrdererMSP # 排序节点组织的ID
		MSPDir: crypto-config/ordererOrganizations/example.com/msp
		
	- &Org1 # 第一个组织,自己起个名字
		Name: Org1MSP # 排序节点的组织名
		ID: Org1MSP # 排序节点组织的ID
		MSPDir: crypto-config/peerOrganizations/org1.example.com/msp
		AnchorPeers: # 锚节点
			- Host: peer0.org1.example.com # 指定一个peer节点的域名
			- Port: 7051  # 端口不要改
			
	- &Org2
		Name: Org2MSP
		ID: Org2MSP
		MSPDir: crypto-config/peerOrganizations/org2.example.com/msp
		AnchorPeers:
			- Host: peer0.org2.example.com
			- Port: 7051 
			
Capabilities: # 能力
	Global: &ChannelCapabilities
		V1_1: true
	Orderer: &ChannelCapabilities
		V1_1: true
	Application: &ApplicationCapabilities
		V1_2: true
		
Application: &ApplicationDefaults # 默认不用改
	Organization: 
Orderer: &OrderDefaults
	# Available types are "solo" and "kafka"
	# 共识机制 == 排序算法
	OrdererType: solo
	Addresses: # orderer节点的地址
		- orderer.example.com:7050 # 要看上一个yaml文件配置的orderer的域名 端口不要改
	
	# BatchTimeout,BatchSize,AbsoluteMaxBytes只要一个满足,区块就会产生
	BatchTimeout: 2s # 经过这么长时间,会产生一个区块
	BatchSize:
		MaxMessageCount: 10 # 交易的最大数量达到这个值,会产生一个区块,建议100左右
		AbsoluteMaxBytes: 99MB # 数据量达到这个值,会产生一个区块,32M/64M
		PreferredMaxBytes: 512KB
	Kafka:
		Brokers:
			- 127.0.0.1:9902
	Organization:
	
Profiles: # 不能改
	TwoOrgsOrdererGenessis: # 区块名字,可以改
		Capabilities:
			<<: *ChannelCapabilities
		Orderer:
			<<: *OrdererDefaults
			Organizations:
				- *OrdererOrg
			Capabilities:
				<<: *OrdererCapabilities
		Consortiums: # 联盟
			SampleConsortium: # 可以改
				Organizations:
					- *OrgGo
					- *OrgCpp
     TwoOrgsChannel: # 通道名字,可以改
         Consortium: SampleConsortium # 对应63行
         Application:
             <<: *ApplicationDefaults
             Organizations:
                 - *OrgGo
                 - *OrgCpp
             Capabilities:
                 <<: *ApplicationCapabilities
  • &后面的名称代表下面三行整体后面用*来引用

生成创始块文件genesis.block

1
$ configtxgen -profile 第53行的区块名字 -outputBlock ./genesis.block

生成通道文件channel.tx

1
2
$ configtxgen -profile 第67行的通道名字 -outputCreateChannelTx channel.tx -channelID ichannel
# 若未指定channelID,则默认是mychannel

生成锚节点更新文件CppMSPanchors.txGoMSPanchors.tx

这个操作是可选的

1
2
3
4
 # 组织的锚节点文件
configtxgen -profile 第67行的通道名字 -outputAnchorPeersUpdate CppMSPanchors.tx -channelID ichannel -asOrg OrgCppMSP
# go组织锚节点文件
configtxgen -profile 第67行的通道名字 -outputAnchorPeersUpdate GoMSPanchors.tx -channelID ichannel -asOrg OrgGoMSP

docker-compose文件的编写

docker-compose用来管理各个docker容器,fabric网络中每个节点都要用一个docker容器来启动

客户端角色需要使用的环境变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 客户端docker容器启动之后, go的工作目录
- GOPATH=/opt/gopath    # 不需要修改
# docker容器启动之后, 对应的守护进程的本地套接字, 不需要修改
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_LOGGING_LEVEL=INFO    # 日志级别
- CORE_PEER_ID=cli            # 当前客户端节点的ID, 自己指定
- CORE_PEER_ADDRESS=peer0.org1.example.com:7051 # 客户端连接的peer节点
- CORE_PEER_LOCALMSPID=     # 组织ID
- CORE_PEER_TLS_ENABLED=true    # 通信是否使用tls加密
- CORE_PEER_TLS_CERT_FILE=        # 证书文件
 /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt
- CORE_PEER_TLS_KEY_FILE=        # 私钥文件
 /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key
-CORE_PEER_TLS_ROOTCERT_FILE=    # 根证书文件
 /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
# 指定当前客户端的身份
- CORE_PEER_MSPCONFIGPATH=      /opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp

orderer节点需要使用的环境变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- ORDERER_GENERAL_LOGLEVEL=INFO    # 日志级别
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0    # orderer节点监听的地址
- ORDERER_GENERAL_GENESISMETHOD=file    # 创始块的来源, 指定file来源就是文件中
# 创始块对应的文件, 这个不需要改
- ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block
- ORDERER_GENERAL_LOCALMSPID=OrdererMSP    # orderer节点所属的组的ID
- ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp    # 当前节点的msp账号路径
# enabled TLS
- ORDERER_GENERAL_TLS_ENABLED=true    # 是否使用tls加密
- ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key    # 私钥
- ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt    # 证书
- ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]            # 根证书

peer节点需要使用的环境变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
- CORE_PEER_ID=peer0.orggo.test.com    # 当前peer节点的名字, 自己起
# 当前peer节点的地址信息
- CORE_PEER_ADDRESS=peer0.orggo.test.com:7051
# 启动的时候, 指定连接谁, 一般写自己就行
- CORE_PEER_GOSSIP_BOOTSTRAP=peer0.orggo.test.com:7051
# 为了被其他节点感知到, 如果不设置别的节点不知有该节点的存在
- CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.orggo.test.com:7051
- CORE_PEER_LOCALMSPID=OrgGoMSP
# docker的本地套接字地址, 不需要改
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
# 当前节点属于哪个网络
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=network_default
- CORE_LOGGING_LEVEL=INFO
- CORE_PEER_TLS_ENABLED=true
- CORE_PEER_GOSSIP_USELEADERELECTION=true    # 自动选举leader节点
- CORE_PEER_GOSSIP_ORGLEADER=false            # 当前不是leader
- CORE_PEER_PROFILE_ENABLED=true    # 在peer节点中有一个profile服务
- CORE_PEER_TLS_CERT_FILE=/etc/hyperledger/fabric/tls/server.crt
- CORE_PEER_TLS_KEY_FILE=/etc/hyperledger/fabric/tls/server.key
- CORE_PEER_TLS_ROOTCERT_FILE=/etc/hyperledger/fabric/tls/ca.crt

启动docker-compose

启动命令:

1
2
# 在docker-compose.yaml 文件目录下执行下边命令
docker-compose up -d

创建的网络前面的前缀名是docker-compose.yaml所在的目录

检测容器是否正常启动:

1
docker-compose ps

管理channel

这个动作需要在cli容器中运行

创建通道,通过客户端来完成

进入cli容器:

1
docker exec -it cli /bin/bash

创建命令,参数详情如下:

1
2
3
4
5
6
7
8
9
$ peer channel create [flags], 常用参数为:
	`-o, --orderer: orderer节点的地址
	`-c, --channelID: 要创建的通道的ID, 必须小写, 在250个字符以内
	`-f, --file: 由configtxgen 生成的通道文件, 用于提交给orderer
	-t, --timeout: 创建通道的超时时长, 默认为5s
	`- -tls: 通信时是否使用tls加密
	`- -cafile: 当前orderer节点pem格式的tls证书文件, 要使用绝对路径.
# example
peer channel create -o orderer节点地址:端口 -c 通道名 -f 通道文件 --tls true --cafile orderer节点pem格式的证书文件

执行命令后,会在当前工作目录下生成一个文件: 通道名.block

加入通道

默认节点,加入管道:

1
2
3
$ peer channel join[flags], 常用参数为:
    `-b, --blockpath: 通过 peer channel create 命令生成的通道文件 
$ peer channel join -b 生成的通道block文件

其他的节点,加入通道:

我们只需修改cli容器的配置文件中的环境变量,让节点连接到其他节点中,再执行peer channel join命令即可将节点加入到通道中。

比如:第二个节点(Go组织的 peer1)加入通道,我们只需赋值第二个节点的export内容,在cli容器中粘贴,然后执行加入通道的命令。

更新锚节点

该步可选

将每个组织的锚节点(anchor peer)更新到orderer

1
2
3
4
5
6
7
8
9
$ peer channel update -o ubuntu.itcast.com:7050 -c $CHANNEL_NAME -f ./channelartifacts/${CORE_PEER_LOCALMSPID}anchors.tx --tls $CORE_PEER_TLS_ENABLED --cafile
$ORDERER_CA
# 参数说明:
- ubuntu.itcast.com: 连接的orderer的地址,hostname:port
- ${CORE_PEER_LOCALMSPID}anchors.tx: 组织的锚节点文件, 是通过configtxgen指令生成的
- $CORE_PEER_TLS_ENABLED: 和orderer通信时是否启用tls
- $ORDERER_CA: 使用tls时,所使用的orderer的证书
# example
$ peer channel update -o orderer节点地址:端口 -c 通道名 -f 锚节点更新文件 --tls true --cafile orderer节点pem格式的证书文件

上述操作需要对多个节点进行操作,因此我们可以写一个脚本文件, 对这些操作进行统一处理,方便使用者进行操作。为了对编写的脚本统一管理,可以将其放入一个目录中。 我们可以在 ~/Demo 目录下创建新的子目录 scripts , 将所有的脚本文件放到里边。

chaincode的安装和实例化

安装链码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ peer chaincode install [flags], 常用参数为:
    -c, --ctor: JSON格式的构造参数, 默认是"{}"
    `-l, --lang: 编写chaincode的编程语言, 默认值是 golang
    `-n, --name: chaincode的名字
    `-p, --path: chaincode源代码的目录, 从 $GOPATH/src 路径后开始写
    `-v, --version: 当前操作的chaincode的版本, 适用这些命令install/instantiate/upgrade
# example
$ peer chaincode install -n 链码的名字 -v 链码的版本 -l 链码的语言 -p 链码的位置
    - 链码名字自己起
    - 链码的版本, 自己根据实际情况指定

链码初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ peer chaincode instantiate [flags], 常用参数为:
    `-C,--channelID:当前命令运行的通道,默认值是“testchainid"。
    `-c, --ctor:JSON格式的构造参数,默认值是“{}"
    `-l,--lang:编写Chaincode的编程语言,默认值是golang
    `-n,--name:Chaincode的名字。
    `-P,--policy:当前Chaincode的背书策略。
    `-v,--version:当前操作的Chaincode的版本,适用于install/instantiate/upgrade等命令
    `--tls: 通信时是否使用tls加密
    `--cafile: 当前orderer节点pem格式的tls证书文件, 要使用绝对路径.
# example
$ peer chaincode instantiate -o orderer节点地址:端口 --tls true --cafile orderer节点pem格式的证书文件 -C 通道名称 -n 链码名称 -l 链码语言 -v 链码版本 -c 链码Init函数调用 -P 背书策略

总结:假如要对peer0.OrgGo操作

  1. 要保证客户端操作的是peer0.OrgGo
    • 可以查看:echo $CORE_PEER_ADDRESS
  2. 将当前节点加入到通道中
    • peer channel join -b xxx.block
  3. 安装链代码
    • peer chaincode install [flags]
  4. 链代码的初始化 -> 只需要做一次
    • peer chaincode instantiate [flags]
  5. 查询/调用

智能合约

智能合约是区块链中的一个非常重要的概念和组成部分

在Fabric中将智能合约称之为ChainCode - 链码

ChainCode介绍

Fabric中的Chaincode包含了代码管理命令这两部分的内容。

其中:

  • 代码是业务的承载体,负责具体的业务逻辑;
  • 管理命令负责Chaincdoe的部署、安装、维护等工作。
  1. 代码
    • Fabric的Chaincode是一段运行在容器中的程序,这些程序可以是Go、Java、Node-js等语言开发的
    • Chaincode 是客户端程序和Fabric之间的桥梁。通过客户端程序可以发起交易、查询交易
    • Chaincode是运行在 Docker容器中的,因此Chaincode相对比较安全
  2. 管理命令
    • Chaincode管理命令主要用来对Chaincode进行安装、实例化、调用、打包、签名等操作
    • Chaincode命令包含在peer模块中,是peer模块的一个子命令,该子命令的名称为chaincode,该子命令的格式:peer chaincode

ChainCode的代码结构

Golang版本

  • 包名
1
2
// 一个chaincode通常是一个Golang源代码文件, 在这份源代码中, 包名必须是main
package main
  • 引入包
1
2
3
4
5
6
7
8
9
// ChainCode需要引入Fabric提供的一些系统包, 这些系统提供了ChainCode和Fabric进行通信的接口
// 必须要引入包的如下:
import ( 
    "github.com/hyperledger/fabric/core/chaincode/shim"
    pb "github.com/hyperledger/fabric/protos/peer"
)
/*
在引入包中 "github.com/hyperledger/fabric/core/chaincode/shim" 是Fabric提供的上下文环境,包含了Fabric和ChainCode交互的接口。在ChainCode中,执行赋值、查询等功能都需要通过shim。
*/
  • 定义结构体并实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/*
每个ChainCode都需要定义一个结构体,结构体的名字可以是任意符合Golang命名规范的字符串。
*/
// 自定义结构体名为: chainCodeStudy
type TestStudy struct {
    //空着
}
/*
Chaincode结构体是ChainCode的主体结构。ChainCode结构体需要实现Fabric提供的接口:
"github.com/hyperledger/fabric/protos/peer",其中必须实现下面两个方法:
*/
// 系统初始化
func (t *TestStudy) Init(stub shim.ChaincodeStubInterface) pb.Response {};
// 数据写入
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response{}
  • Init方法

Init方法是系统初始化方法。当执行命令peer chaincode instantiate实例化chaincode时候会调用该方法,同时命令中-c选项后面内容会作为参数传入Init方法中。以下面的chaincode实例化命令为例:

1
$ peer chaincode instantiate -o orderer.test.com:7050 -C mychanne -n mytestcc -v 1.0 -c '{"Args": ["init","a", "100","b","200"]}'

上面命令给Chaincode传入4个参数“a”“100”“b”“200”。注意命令中Args后面一共有5个参数,其中第一个参数init是固定值,后面的才是参数。传参数的个数是没有限制的,但是实际应用的时候不要太多。如果有很多参数需要传递给ChainCode,可以采用一些数据格式(比如Json),把数据格式化之后传递给ChainCode。在Init方法中可以通过下列方法获取传入参数

1
2
3
4
5
func (t *TestStudy) Init(stub shim.ChaincodeStubInterface) pb.Response {
    // 获取客户端传入的参数, args是一个字符串, 存储传入的字符串参数
    _, args := stub.GetFunctionAndParameters()
    return shim.Success([]byte("sucess init!!!"))
};
  • Invoke方法

Invoke方法的主要作用是写入数据,比如发起交易等。在执行命令peer chaincode invoke的时候系统会调用该方法,同时会把命令中-c后面的参数传入Invoke方法中,以下面的Invoke命令为例:

1
2
$ peer chaincode invoke -o 192.168.1.100:7050 -C mychanne -n mytestcc -c '{"Args":
["invoke","a","b","10"]}'

上面的命令调用Chaincode的Invoke方法并且传入三个参数“a”“b”“10”。注意Args后面数组中的第一个值 “invoke”是默认的固定参数

1
2
3
4
5
6
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    // 进行交易操作的源代码, 调用ChaincodeStubInterface接口中的方法
    // stub.xxx()
    // stub.yyy()
    return shim.Success([]byte("sucess invoke!!!"))
};
  • shim包的核心方法

在Fabric的Golang语言的Chaincode源代码中如需要引入系统包 “github.com/hyperledger/fabric/core/chaincode/shim”

shim包主要负责和客户端进行通信。shim提供了一组核心方法和客户端进行交互,这些方法如下所示:

shim中API查看地址: https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim

  • Success
1
2
3
4
5
6
7
8
9
/*
Sucess 方法负责将正确的消息返回给调用ChainCode的客户端, Sucess方法的定义和调用如下:
*/
// 方法定义
func Success(payload []byte) pb.Response;
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	return shim.Success([]byte("sucess invoke!!!"))
};
  • Error
1
2
3
4
5
6
7
// Error方法负责将错误信息返回给调用ChainCode的客户端, Error方法的定义和调用如下
// 方法定义
func Error(msg string) pb.Response;
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	return shim.Error("operation fail!!!")
};
  • LogLevel
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// LogLevel方法负责修改ChainCode中运行日志的级别, LogLevel方法的定义和调用如下
// 将日志级别描述字符串转为 LoggingLevel 类型
func LogLevel(levelString string) (LoggingLevel, error);
    - levelString可用参数:
        - CRITICAL, 级别最高, 写日志最少
        - ERROR
        - WARNING
        - NOTICE
        - INFO
        - DEBUG, 级别最低, 写日志最多
// 设置日志级别
func SetLoggingLevel(level LoggingLevel);

// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    loglevel, _ := shim.LogLevel("debug")
    shim.setLoggingLevel(loglevel)
    return shim.Success([]byte("operation fail!!!"))
};
  • ChaincodeStubInterface接口中的核心方法

在shim包中有一个接口ChaincodeStubInterface,该接口提供了一组方法,通过这组方法可以非常方便的操作Fabric中账本数据。ChaincodeStubInterface接口的核心方法大概可以分为四大类:系统管理、存储管理、交易管理、调动外部chaincode。

  • 系统管理相关的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 赋值接收调用chaincode的客户端传递过来的参数
func GetFunctionAndParameters() (function string, params []string);
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    // 获取客户端传入的参数, args是一个字符串, 存储传入的字符串参数
    _, args := stub.GetFunctionAndParameters()
    var a_param = args[0]
    var b_param = args[1]
    var c_param = args[2]
    return shim.Success([]byte("sucess init!!!"))
};
  • 存储管理相关的方法
    • PutState
1
2
3
4
5
6
7
8
// 把客户端传递过来的数据保存到Fabric中, 数据格式为键值对
func PutState(key string, value []byte) error;
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    // 数据写入
    stub.PutState("user1", []byte("putvalue"))
    return shim.Success([]byte("sucess invoke user1"))
};
  • GetState
1
2
3
4
5
6
7
8
// 从Fabric中取出数据, 然后把这些数据交给chaincode处理.
func GetState(key string) ([]byte, error);
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    // 读数据
    keyvalue, err := stub.GetState("user1")
    return shim.Success(keyvalue)
};
  • GetStateByRange
 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
// 根据key的访问查询相关数据
func GetStateByRange(startKey,endKey string)(StateQueryIteratorInterface,error);
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    startKey := "startkey"
    endKey := "endkey"
    // 根据范围查询, 得到StateQueryIteratorInterface迭代器接口
    keysIter, err := stub.getStateByRange(startKey, endKey)
    // 最后关闭迭代器接口
    defer keysIter.Close()
    var keys []string
    for keysIter.HasNext() { // 如果有下一个节点
        // 得到下一个键值对
        response, iterErr := keysIter.Next()
        if iterErr != nil {
        	return shim.Error(fmt.Sprintf("find an error %s", iterErr))
        }
        keys = append(keys, response.Key) // 存储键值到数组中
    }
    // 遍历keys数组
    for key, value := range keys {
    	fmt.Printf("key %d contains %s\n", key, value)
    }
    // 编码keys数组成json格式
    jsonKeys, err := json.Marshal(keys)
    if err := nil {
    	return shim.Error(fmt.Sprintf("data Marshal json error: %s", err))
    }
    // 将编码之后的json字符串传递给客户端
    return shim.Success(jsonKeys)
};
  • GetHistoryForKey
 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
// 查询某个键的历史记录
func GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error);
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    keysIter, err := stub.GetHistoryForKey("user1") // user1是一个假设的键值
    if err := nil {
		return shim.Error(fmt.Sprintf("GetHistoryForKey error: %s", err))
}
    defer keysIter.Close()
    var keys []string
    for keysIter.HasNext() { // 遍历集合,如果有下一个节点
        // 得到下一个键值对
        response, iterErr := keysIter.Next()
        if iterErr != nil {
        return shim.Error(fmt.Sprintf("find an error %s", iterErr))
        }
        // 交易编号
        txid := response.TxId
        // 交易的值
        txvalue := response.Value
        // 当前交易的状态
        txStatus := response.IsDelete
        // 交易发生的时间戳
        txtimestamp := response.Timestamp
        // 计算从1970.1.1到时间戳的秒数
        tm := time.Unix(txtimestamp.Seconds, 0)
        // 根据指定的格式将日期格式化
        datestr := tm.Format("2018-11-11 11:11:11 AM")
        fmt.Printf("info - txid:%s, value:%s, isDel:%t, dateTime:%s\n", txid,
		string(txvalue), txStatus, datestr)
		keys = append(keys, txid)
	}
    // 将数组中历史信息的key编码为json格式
    jsonKeys, err := json.Marshal(keys)
    if err := nil {
		return shim.Error(fmt.Sprintf("data Marshal json error: %s", err))
}
    // 将编码之后的json字符串传递给客户端
    return shim.Success(jsonKeys)
}	
  • DelState
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 删除一个key
func DelState(key string) error;
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    err := stub.DelState("delKey")
    if err != nil {
    	return shim.Error("delete key error !!!")
    }
    return shim.Success("delete key Success !!!")
}
  • CreateCompositeKey
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 给定一组属性,将这些属性组合起来构造一个复合键
func CreateCompositeKey(objectType string, attributes []string) (string,
error);
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    parms := []string("go1", "go2", "go3", "go4", "go5", "go6")
    ckey, _ := stub.CreateCompositeKey("testkey", parms)
    // 复合键存储到账本中
    err := stub.putState(ckey, []byte("hello, go"))
    if err != nil {
    	fmt.Println("find errors %s", err)
    }
    // print value: testkeygo1go2go3go4go5go6
    fmt.Println(ckey)
    return shim.Success([]byte(ckey))
}
  • GetStateByPartialCompositeKey / SplitCompositeKey
 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
// 根据局部的复合键返回所有的匹配的键值
func GetStateByPartialCompositeKey(objectType string, keys []string)
(StateQueryIteratorInterface, error);
// 给定一个复合键,将其拆分为复合键所有的属性
func SplitCompositeKey(compositeKey string) (string, []string, error)
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    searchparm := []string{"go1"}
    rs, err := stub.GetStateByPartialCompositeKey("testkey", searchparm)
    if err != nil {
        error_str := fmt.Sprintf("find error %s", err)
        return shim.Error(error_str)
    }
    defer rs.Close()
    var tlist []string
    for rs.HasNext() {
		responseRange, err := rs.Next()
        if err != nil {
            error_str := fmt.Sprintf("find error %s", err)
            fmt.Println(error_str)
            return shim.Error(error_str)
        }
        value1,compositeKeyParts,_ := stub.SplitCompositeKey(responseRange,
        key)
        value2 := compositeKeyParts[0]
        value3 := compositeKeyParts[1]
        // print: find value v1:testkey, v2:go1, v3go2
        fmt.Printf("find value v1:%s, v2:%s, V3:%s\n", value1, value2,
        value3)
    }
    return shim.Success("success")
}
  • 交易管理相关的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 获取当前客户端发送的交易时间戳
func GetTxTimestamp() (*timestamp.Timestamp, error);
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
	txtime, err := stub.GetTxTimestamp()
    if err != nil {
        fmt.printf("Error getting transaction timestamp: %s", error)
        return shim.Error(fmt.Sprintf("get transaction timestamp error: %s",
    error))
    }
    tm := time.Unix(txtime.Second, 0)
    return shim.Success([]byte(fmt.Sprint("time is: %s", tm.Format("2018-11-11
    23:23:32"))))
}
  • 调用其他chaincode的方法
 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
// 调用另一个链码中的Invoke方法
func InvokeChaincode(chaincodeName string,args [][]byte,channel string)
pb.Response
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    // 设置参数, a向b转转11
    trans:=[][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("11")}
    // 调用chaincode
    response := stub.InvokeChaincode("mycc", trans, "mychannel")
	// 判断是否操作成功了
    // 课查询:
    https://godoc.org/github.com/hyperledger/fabric/protos/peer#Response
    if response.Status != shim.OK {
        errStr := fmt.Sprintf("Invoke failed, error: %s", response.Payload)
        return shim.Error(errStr)
    }
    return shim.Success([]byte("转账成功..."))
}
// ==================================================
// 获取客户端发送的交易编号
func GetTxID() string
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    txid := stub.GetTxID()
    return shim.Success([]byte(txid))
}

ChainCode交易的背书(endorse)

区块链是一个去中心的,所有参与方集体维护的公共账本。Fabric作为一个典型的区块链的技平台当然也具备这样的特点。Fabric中对数据参与方对数据的确认是通过Chaincode来进行的

在Fabric中有一个非常重要的概念称为Endorsement,中文名为背书。背书的过程是一笔交易被确认的过程。而背书策略被用来指示对相关的参与方如何对交易讲行确认。当一个节点接收到一个交易请求的时候,会调用 VSCC(系统Chaincode,专门负责处理背书相关的操作)与交易的chaincode共同来验证交易的合法性。在VSCC 和交易的Chaincode共同对交易的确认中, 通常会做以下的校验:

  • 所有的背书是否有效(参与的背书的签名是否有效)
  • 参与背书的数量是否满足要求
  • 所有背书参与方是否满足要求

背书政策是指定第二和第三点的一种方式。这些概念看起来还是比较难懂的,理解它们最好的办法是通过一个 具体的实例。背书策略的设置是通过部署时instantiate命令中-P-参数来设置的。背书策略在链码初始化的时候就需要指定,命令样式如下:

1
2
3
4
$ peer chaincode instantiate -o oraderer.test.com:7050 -C mychannel -n mycc —v 1.0 -c
'{"Args":["init", "a", "100", "b", "200"]}' -P "AND ('Org1MSP.member', 'Org2MSP.member')"
# 上述命令是对Chaincode进行实例化的操作,我们提取-P后面的参数:
"AND ('Org1MSP.member', 'Org2MSP.member')"

这个参数包说明的是当前Chaincode发起的交易,需要组织编号为Org1MSP和组织编号为Org2MSP的组织中的任何一个用户共同参与交易的确认并且同意,这样交易才能生效并被记录到区块链中。通过上述背书策略的实例我们可以知道背书策略是通过一定的关键字和系统的属性组成的。根据Fabric的系统定义,可以将上面的背书策略拆解如下:

AND参与背书者之间的关系,AND表示所有参与方共同对交易进行确认。除了AND之外还可以使用关键字OR, 如果使用关键字OR表示参与方的任何一方参与背书即完成交易的确认

Org1MSP.member表示参与背书的组织和组织中参与背书的用户。Org1MSP表示组织的编号,这个值是怎么来的呢?之前我们介绍过cryptogen模块,该模块根据配置文件生成系统的配置和账号信息。在cryptogen的配置文件中有一个节点Organizations->ID,该节点的值就是该组织的编号,也是在配置背书策略时需要用到的组织的编号。member泛指组织内的任何一个用户,当然也可以是组织某个具体的用户。

通过上面的描述,基本上可以了解背书策略的编写规则,下面通过几个实例进一步了解背书策略的编写规则

  • 背书规则示例1
1
2
# 按照该背书规则进行交易, 必须通过组织Org1MSP,Org2MSP,Org3MSP中的用户共同验证交易才能生效
"AND ('Org1MSP.member', 'Org2MSP.member', 'Org3MSP.member')"
  • 背书规则示例2
1
2
# 按照该背书规则进行交易,只需要通过组织 Org1MSP 或 Org2MSP 或 Org3MSP 中的任何一个成员验证,即可生效
"OR ('Org1MSP.member', 'Org2MSP.member', 'Org3MSP.member')"
  • 背书规则示例3
1
2
3
4
# 按照该背书规则进行交易,有两种办法让交易生效
# 1. 组织Org1MSP中的某个成员对交易进行验证。
# 2. 组织Org2MSP和组织Org3MSP中的成员共同就交易进行验证。
"OR ('Org1MSP.member', AND('Org2MSP.member', 'Org3MSP.member'))"

以上介绍了背书的规则,有一点需要注意:背书规则只针对chaincode中写入数据的操作进行校验,对于查询类操作不背书。以golang版本的chaincode为例, 需要利用背书规则对操作进行校验的方法如下:

1
2
PutState(key string, value []byte) error;
DelState(key string) error;

Fabric中的背书是发生在客户端的,需要进行相关的代码的编写才能完成整个背书的操作

链码

不同的链码名称对应这不同的Go文件

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//chaincode/go/test1/test.go  -》 对应链码名testcc
package main
import {}
type Test struct{}
func (t *Test)Init();
func (t *Test)Invoke();	//业务逻辑1
func main(){}

//chaincode/go/test2/test1.go -》 对应链码名testcc1
package main
import {}
type Test struct{}
func (t *Test)Init();
func (t *Test)Invoke();	//业务逻辑2
func main(){}

示例链码解析

调用的json:

  • 初始化json:{“Args”:[“init”,“a”,“100”,“b”,“200”]}
  • 调用的json:{“Args”:[“invoke”,“a”,“b”,“10”]}

官方示例fabric-samples/chaincode/chaincode_example02/go/chaincode_example02.go文件:

  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
package main

//WARNING - this chaincode's ID is hard-coded in chaincode_example04 to illustrate one way of
//calling chaincode from a chaincode. If this example is modified, chaincode_example04.go has
//to be modified as well with the new ID of chaincode_example02.
//chaincode_example05 show's how chaincode ID can be passed in as a parameter instead of
//hard-coding.

import (
        "fmt"
        "strconv"

        "github.com/hyperledger/fabric/core/chaincode/shim"
        pb "github.com/hyperledger/fabric/protos/peer"
)

// SimpleChaincode example simple Chaincode implementation
type SimpleChaincode struct {
}

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
        fmt.Println("ex02 Init")
        _, args := stub.GetFunctionAndParameters()   // 获取调用的函数参数, 返回值_就是Init,args是后接的参数
        var A, B string    // Entities
        var Aval, Bval int // Asset holdings
        var err error

        if len(args) != 4 {			//判断参数个数
                return shim.Error("Incorrect number of arguments. Expecting 4")
        }

        // Initialize the chaincode
        A = args[0]
        Aval, err = strconv.Atoi(args[1])
        if err != nil {
                return shim.Error("Expecting integer value for asset holding")
        }
        B = args[2]
        Bval, err = strconv.Atoi(args[3])
        if err != nil {
                return shim.Error("Expecting integer value for asset holding")
        }
        fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)

        // Write the state to the ledger  把数据写入账本中
        err = stub.PutState(A, []byte(strconv.Itoa(Aval)))  
        if err != nil {
                return shim.Error(err.Error())
        }

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

        return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {  //交易函数
        fmt.Println("ex02 Invoke")
        function, args := stub.GetFunctionAndParameters()  //获取参数,function是第一个参数代表保存的函数名,这里是invoke
    	//“路由设置”
        if function == "invoke" {
                // Make payment of X units from A to B
                return t.invoke(stub, args)
        } else if function == "delete" {
                // Deletes an entity from its state
                return t.delete(stub, args)
        } else if function == "query" {
                // the old "Query" is now implemtned in invoke
                return t.query(stub, args)
        }

        return shim.Error("Invalid invoke function name. Expecting \"invoke\" \"delete\" \"query\"")
}
// Transaction makes payment of X units from A to B
func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response {
        var A, B string    // Entities
        var Aval, Bval int // Asset holdings
        var X int          // Transaction value
        var err error

        if len(args) != 3 {
                return shim.Error("Incorrect number of arguments. Expecting 3")
        }

        A = args[0]
        B = args[1]

        // Get the state from the ledger
        // TODO: will be nice to have a GetAllState call to ledger
        Avalbytes, err := stub.GetState(A)  	//获取A的资产
        if err != nil {
                return shim.Error("Failed to get state")
        }
        if Avalbytes == nil {
                return shim.Error("Entity not found")
        }
        Aval, _ = strconv.Atoi(string(Avalbytes))

        Bvalbytes, err := stub.GetState(B) 		//获取B的资产
        if err != nil {
                return shim.Error("Failed to get state")
        }
        if Bvalbytes == nil {
                return shim.Error("Entity not found")
        }
        Bval, _ = strconv.Atoi(string(Bvalbytes))

        // Perform the execution
        X, err = strconv.Atoi(args[2])  	//获取转账金额
        if err != nil {
                return shim.Error("Invalid transaction amount, expecting a integer value")
        }
        Aval = Aval - X  		// 转账
        Bval = Bval + X	
        fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)

        // Write the state back to the ledger
        err = stub.PutState(A, []byte(strconv.Itoa(Aval)))		//重新写入
        if err != nil {
                return shim.Error(err.Error())
        }

        err = stub.PutState(B, []byte(strconv.Itoa(Bval)))		//重新写入
        if err != nil {
                return shim.Error(err.Error())
        }

        return shim.Success(nil)
}

// Deletes an entity from state
func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string) pb.Response {
        if len(args) != 1 {
                return shim.Error("Incorrect number of arguments. Expecting 1")
        }

        A := args[0]

        // Delete the key from the state in ledger
        err := stub.DelState(A)			//删除账本中的key,假删除,这个操作是被记录的
        if err != nil {
                return shim.Error("Failed to delete state")
        }

        return shim.Success(nil)
}

// query callback representing the query of a chaincode
func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {		//查询函数
        var A string // Entities
        var err error

        if len(args) != 1 {
                return shim.Error("Incorrect number of arguments. Expecting name of the person to query")
        }

        A = args[0]

        // Get the state from the ledger
        Avalbytes, err := stub.GetState(A)	//取出值
        if err != nil {
                jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}"
                return shim.Error(jsonResp)
        }

        if Avalbytes == nil {
                jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}"
                return shim.Error(jsonResp)
        }

        jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}"		//拼接成字符串给客户端
        fmt.Printf("Query Response:%s\n", jsonResp)
        return shim.Success(Avalbytes)
}

func main() {
        err := shim.Start(new(SimpleChaincode))  //写法固定  SimpleChaincode就是定义的空结构体
        if err != nil {
                fmt.Printf("Error starting Simple chaincode: %s", err)
        }
}

如果要自定义函数,函数的格式:(自定义函数一般都是在Invoke(大写)函数中被调用的)

1
2
3
4
func (t  *自定义结构体) 
functionName (stub shim.ChaincodeStubInterface, args []string) pb.Response {
	xxxxx
}

fabric账号

账号:Fabric中的账号实际上是根据PKI规范生成的一组证书和密钥文件

账号的作用:

  • 保证记录在区块链中的数据不可篡改、不可逆
  • Fabric中每条交易都会加上发起者的标签(签名证书),同时用发起人的私钥进行加密
  • 如果交易需要其他组织的节点提供背书,那么背书节点也会在交易中加入自己的签名

需要找哪个节点/组织的证书,就去该目录下找其对应MSP文件夹即可

msp文件夹中内容中主要存放签名用的证书文件和加密用的私钥文件。

  • admincerts:管理员证书
  • cacerts:根CA服务器的证书
  • keystore:节点或者账号的私钥
  • signcerts:符合x.509的节点或者用户证书文件
  • tlscacerts:TLS根CA的证书

tls文件夹中存放加密通信相关的证书文件

账号的使用场景

  • 启动orderer

启动orderer的时候我们需要通过环境变量或者配置文件给当前启动的orderer设定相应的账号

1
2
3
# 环境变量账号:->该路径为宿主机上的路径,非docker启动的orderer节点内部挂载路径
ORDERER_GENERAL_LOCALMSPDIR=
# 账号目录信息
  • 启动peer

启动peer的时候我们需要通过环境变量或者配置文件给当前启动的peer设定相应的账号

1
2
3
# 环境变量账号:->该路径为宿主机上的路径,非docker启动的orderer节点内部挂载路径
CORE_PEER_MSPCONFIGPATH=
# 账号目录信息
  • 创建channel

channel是fabric中的重要组成部分,创建channelt也是需要账号的

1
2
3
4
# 环境变量账号:->该路径为宿主机上的路径,非docker启动的orderer节点内部挂载路径

CORE_PEER_MSPCONFIGPATH=
# 账号目录信息

创建通道是在客户端完成的,并且必须是Admin创建

1
2
3
4
5
6
# Orderer启动路径
crypto-config/ordererOrganizations/itcast.com/orderers/orderer.itcast.com/msp
# Peer启动的账号路径
crypto-config/peerOrganizations/orggo.itcast.com/peers/peere0.orggo.itcast.com/msp
# 创建channel的账号路径
crypto-config/peerOrganizations/orggo.itcast.com/users/Admin@orggo.itcast.com/msp

我们可以发现:

  • Peer和Orderer都有属于自己的账号,创建Channel使用的是用户账号
  • Peer和创建Channel的用户账号属于某个组织,而ordrer的启动账号只属于他自己
  • 这里特别注意,用户账号在很多操作中都会用到,而且很多操作的错误都是用户账号的路径设置不当而引起的

Fabric-ca

  • fabric-ca项目是专门为了解决Fabric账号问题而发起的一个开源项目,它非常完美的解决了fabric账号生成的问题
  • fabric-ca项目由fabric-server和fabric-client这两个模块组成,其中fabric-server在fabric中占有非常重要的作用
  • 我们可以只使用cryptogen命令同配置文件就生成一些账号信息,但是如果有动态添加账号的需求,就无法满足,所以这个时候我们就应该在项目中引入fabric-ca
  • 当需要动态的添加用户的时候,使用cryptogen就过于繁琐了,Fabric提供了fabric-ca机制

可以通过fabric-ca-client连接fabric-ca-server注册账号

官方提供了已经写好的二进制可执行文件供我们访问:fabric-ca-client,但是弊端是使用命令行的方式,这对用户来说是难以接受的,所以一般我们会通过一些sdk调用

Fabric CA提供了两种访问方式调用Server服务:

  • 通过Fabric-Clienti调用
  • 通过SDK调用(node.js、java、go)

通常情况下,一个组织会对应一个fabric-server服务器,下面介绍如何将fabric-server加入到网络中

通过sdk编写客户端实现:

  • 连接Fabric-ca-server,创建账户
  • 访问peer节点查询数据

以此来代替之前配置的cli容器

在一个fabric网络中有多个组织,那么fabric-ca该如何部署?

  • 在每个组织中部署一个Fabric-ca,这样创建出来的用户可以访问整个组织中的所有peer节点

fabric-ca服务器还可以设置代理,使用一些关系型的数据库

将fabric-ca加入到网络:

  • 进入fabric-samples/basic-network文件夹中,分别给两个组织加上fabric-ca服务器,启动成功后重新创建网络,进入客户端…..等一系列操作

solo共识下多机多节点部署

上面的是solo共识下单机多节点部署,下面介绍solo共识下多机多节点部署

所有的节点分离部署,每台主机上有一个节点

名称 IP HostName 组织机构
orderer …. orderer.example.com Orderer
peer0 …. peer0.orggo.com OrgGo
peer1 peer1.orggo.com OrgGo
peer0 …. peer0.orgcpp.com OrgCpp
peer1 peer1.orgcpp.com OrgCpp

准备工作

  • n台主机需要创建一个名字相同的工作目录,为的是能够连接上同一个网络
  • 编写配置文件
    • 生成证书的:crypto-config.yaml,名字可以改
  • 生成通道文件和创始块文件
    • 名字不能变:configtx.yaml

部署orderer排序节点

  • 编写orderer节点启动的docker-compose.yaml配置文件

部署peer0.orggo节点

  • 切换到peer0.orggo主机
  • 进入到工作目录
  • 拷贝文件
  • 编写docker-compose.yaml配置文件
  • 在工作目录下创建chaincode目录,并将链代码放入其中
  • 运行docker-compose文件
  • 进入到Cli容器中, 创建通道
  • 将当前节点加入到通道中
  • 安装链码

部署peer0.orgcpp节点

  • 前几步同上
  • 初始化链码
  • 通过查询/修改数据验证部署是否成功

我们在进行多机多节点部署的时候, 所有的peer节点都需要安装链码, 有时候会出现链码安装失败的问题, 提示链码的指纹(哈希)不匹配,我们可以通过以下方法解决

  1. 通过客户端在第1个peer节点中安装好链码之后, 将链码打包
  2. 将打包之后的链码从容器中拷贝出来
  3. 将得到的打包之后的链码文件拷贝到其他的peer节点上
  4. 通过客户端在其他peer节点上安装链码

参考资料1

参考资料2

Fabric梳理

  1. 生成节点证书
1
2
3
4
# 1. 编写组织信息的配置文件, 该文件中声明每个组织有多少个节点, 多少用户
# 在这个配置文件中声明了每个节点访问的地址(域名)
# 一般命名为crypto-config.yaml
$ cryptogen generate --config=xxx.yaml
  1. 生成创始块文件和通道文件
  • 编写配置文件 - configtx.yaml
    • 配置组织信息
      • name
      • ID
      • msp
      • anchor peer
    • 排序节点设置
      • 排序算法( 共识机制)
      • orderer节点服务器的地址
      • 区块如何生成
    • 对组织关系的概述
    • 当前组织中所有的信息 -> 生成创始块文件
    • 通道信息 -> 生成通道文件 或者 生成锚节点更新文件
  • 通过命令生成文件
1
$ configtxgen -profile [从configtx.yaml->profiles->下属字段名] -outputxxxx
  • 创始块文件: 给排序节点使用了
1
2
ORDERER_GENERAL_GENESISMETHOD=file
ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/orderer.genesis.block
  • 通道文件: 被一个可以操作peer节点的客户端使用该文件创建了通道, 得到一个通道名.block
  1. 编写orderer节点对应的配置文件
  • 编写配置文件
1
# docker-compose.yaml
  • 启动docker容器
1
$ docker-compose up -d
  • 检测
1
$ docker-compose ps
  1. 编写peer节点对应的配置文件
1
2
3
4
# docker-compose.yaml
 - 两个服务器
 	- peer
 	- cli
  • 启动容器
1
$ docker-compose up -d
  • 检测
1
$ docker-compose ps
  • 进入到客户端容器中
1
$ docker exec -it cli bash
  • 创建通道
  • 当前节点加入到通道
  • 安装链码
  • 初始化 - > 一次就行

  • 客户端
    • 连接peer需要用户身份的账号信息, 可以连接到同组的peer节点上
    • 客户端发起一笔交易
      1. 会发送到参与背书的各个节点上
      2. 参加背书的节点进行模拟交易
      3. 背书节点将处理结果发送给客户端
      4. 如果提案的结果都没有问题, 客户端将交易提交给orderer节点
      5. orderer节点将交易打包
      6. leader节点将打包数据同步到当前组织
      7. 当前组织的提交节点将打包数据写入到区块中
  • Fabric-ca-sever
    • 可以通过它动态创建用户
    • 网络中可以没有这个角色
  • 组织
    • peer节点 -> 存储账本
    • 用户
  • 排序节点
    • 对交易进行排序
      • 解决双花问题
    • 对交易打包
      • configtx.yaml
  • peer节点
    • 背书节点
      • 进行交易的模拟, 将节点返回给客户端
      • 客户端选择的, 客户端指定谁去进行模拟交易谁就是背书节点
    • 提交节点
      • 将orderer节点打包的数据, 加入到区块链中
      • 只要是peer节点, 就具有提交数据的能力
    • 主节点
      • 和orderer排序节点直接通信的节点
        • 从orderer节点处获取到打包数据
        • 将数据同步到当前组织的各个节点中
      • 只能有一个
        • 可以自己指定
        • 也可以通过fabric框架选择 -> 推荐
    • 锚节点
      • 代表当前组织和其他组织通信的节点
      • 只能有一个

Fabric中的共识机制

交易必须按照发生的顺序写入分类帐,尽管它们可能位于网络中不同的参与者组之间。为了实现这一点,必须建立交易的顺序,并且必须建立一种拒绝错误(或恶意)插入分类帐的坏交易的方法。

在分布式分类帐技术中,共识渐渐已成为单一功能中特定算法的代名词。然而,共识不仅仅是简单地同意交易顺序,而是通过在整个交易流程中的基本作用,从提案和认可到订购,验证和承诺,在Hyperledger Fabric中强调了这种差异化。简而言之,共识被定义为对包含块的一组交易的正确性的全面验证

Hyperledger Fabric共识机制,目前包括SOLO,Kafka,以及未来可能要使用的PBFT(实践拜占庭容错)、SBFT(简化拜占庭容错)

Solo

SOLO机制是一个非常容易部署的非生产环境的共识排序节点。它由一个为所有客户服务的单一节点组成,所以不需要“共识”,因为只有一个中央权威机构。相应地没有高可用性或可扩展性。这使得独立开发和测试很理想,但不适合生产环境部署。orderer-solo模式作为单节点通信模式,所有从peer收到的消息都在本节点进行排序与生成数据块。

客户端通过GRPC发起通信,与Orderer连接成功之后,便可以向Orderer发送消息。Orderer通过Recv接口接收Peer发送过来的消息,Orderer将接收到的消息生成数据块,并将数据块存入ledger,peer通过deliver接口从orderer中的ledger获取数据块。

Kafka

Katka是一个分布式消息系统,由LinkedIn使用scala编写,用作LinkedIn的活动流(Activitystream)和运营数据处理管道(Pipeline)的基础。具有高水平扩展和高吞吐量。

在Fabric网络中,数据是由Peer节点提交到Orderer排序服务,而Orderer相对于Kafka来说相当于上游模块,且Orderer还兼具提供了对数据进行排序及生成符合配置规范及要求的区块。而使用上游模块的数据计算、统计、分析,这个时候就可以使用类似于Kafka这样的分布式消息系统来协助业务流程。

有人说Kafka是一种共识模式,也就是说平等信任,所有的HyperLedger Fabric网络加盟方都是可信方,因为消息总是均匀地分布在各处。但具体生产使用的时候是依赖于背书来做到确权,相对而言,Kafka应该只能是一种启动Fabric网络的模式或类型。

Zookeeper一种在分布式系统中被广泛用来作为分布式状态管理、分布式协调管理、分布式配置管理和分布式锁服务的集群。Kafka增加和减少服务器都会在Zookeeper节点上触发相应的事件,Kafka系统会捕获这些事件,进行新一轮的负载均衡,客户端也会捕获这些事件来进行新一轮的处理。

Orderer排序服务是Fablic网络事务流中的最重要的环节,也是所有请求的点,它并不会立刻对请求给予回馈,一是因为生成区块的条件所限,二是因为依托下游集群的消息处理需要等待结果

Kafka多机多节点部署

未完待续。。。

Built with Hugo
Theme Stack designed by Jimmy