在没有区块链的相关知识之前建议先学习一下基本知识,如果有一定理论基础便可通过这篇文章对区块链应用有一个更深层次的理解。
闲话少说,直接开始!
PART1:Go语言安装
这里我以Linux服务器为主从零开始搭建,有条件的可以选择一个好的配置
具体服务器配置教程如下:
从0开始部署阿里云服务器(萌新必看)_阿里云搭建服务器部署-CSDN博客
配置完后可以进行下面的内容,首先安装Go语言
控制台输入
wget https://golang.google.cn/dl/go1.20.6.linux-amd64.tar.gz
然后解压缩
sudo tar -C /usr/local -xzf go1.20.6.linux-amd64.tar.gz
接着配置环境
vim ~/.bash_profile
加入

export GOPATH=$HOME/gopath
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
返回然后
source ~/.bash_profile
接着输入go version检查
即可完成安装
PART2:搭建区块链网络
1、下载 fabric 二进制工具
以 v1.4.12 版本为例, fabric 二进制工具的下载地址在https://github.com/hyperledger/fabric/releases/tag/v1.4.12
下载linux系统的,其他系统的根据需求下载
下载完后把它丢到服务器中并解压
2、将 fabric 二进制工具添加到环境变量
为了后续方便使用命令,可以将第 1 步下载的工具添加到系统环境变量中:
export PATH=/usr/local/fabric/hyperledger-fabric-linux-amd64-1.4.12/bin:$PATH
3、生成证书和秘钥
我在相同目录上新建了个0202demo,接下来的所有操作都将在这个文件夹里进行
首先我们新建一个network文件夹,在0202demo中
进入到network文件夹
这里将使用 cryptogen
工具生成各种加密材料(x509 证书和签名秘钥)。这些证书是身份的代表,在实体相互通信和交易的时候,可以对其身份进行签名和验证。
首先创建 crypto-config.yaml
文件,定义网络拓扑,为所有组织和属于这些组织的组件(也就是节点)生成一组证书和秘钥,内容如下:
# 排序节点的组织定义
OrdererOrgs:
- Name: QQ # 名称
Domain: qq.com # 域名
Specs: # 节点域名:orderer.qq.com
- Hostname: orderer # 主机名
# peer节点的组织定义
PeerOrgs:
# Taobao-组织
- Name: Taobao # 名称
Domain: taobao.com # 域名
Template: # 使用模板定义。Count 指的是该组织下组织节点的个数
Count: 2 # 节点域名:peer0.taobao.com 和 peer1.taobao.com
Users: # 组织的用户信息。Count 指该组织中除了 Admin 之外的用户的个数
Count: 1 # 用户:Admin 和 User1
# JD-组织
- Name: JD
Domain: jd.com
Template:
Count: 2 # 节点域名:peer0.jd.com 和 peer1.jd.com
Users:
Count: 1 # 用户:Admin 和 User1
不会生成的,直接vim crypto-config.yaml进行编辑即可。
接着执行 cryptogen generate
命令,生成结果将默认保存在 crypto-config
文件夹中
进入这个文件夹是可以看到有很多文件生成
总结:在这个环节中,我们假设 QQ
作为一个运营方,提供了 1 个 Orderer 节点 orderer.qq.com
来创建联盟链的基础设施,而 Taobao
和 JD
则是作为组织成员加入到链中,各自提供 2 个 Peer 节点 peer0.xx.com
和 peer1.xx.com
参与工作,以及还各自创建了 2 个组织用户 Admin
和 User1
。然后我们使用 crypto-config.yaml
文件和 cryptogen
工具为其定义所需要的证书文件以供后续使用。
4、创建排序通道创世区块
我们可以使用 configtx.yaml 文件和 configtxgen 工具轻松地创建通道的配置。configtx.yaml 文件可以以易于理解和编辑的 yaml 格式来构建通道配置所需的信息。configtxgen 工具通过读取 configtx.yaml 文件中的信息,将其转成 Fabric 可以读取的 protobuf 格式。
先来创建 configtx.yaml 文件,内容如下:
# 定义组织机构实体
Organizations:
- &QQ
Name: QQ # 组织的名称
ID: QQMSP # 组织的 MSPID
MSPDir: crypto-config/ordererOrganizations/qq.com/msp #组织的证书相对位置(生成的crypto-config目录)
- &Taobao
Name: Taobao
ID: TaobaoMSP
MSPDir: crypto-config/peerOrganizations/taobao.com/msp
AnchorPeers: # 组织锚节点的配置
- Host: peer0.taobao.com
Port: 7051
- &JD
Name: JD
ID: JDMSP
MSPDir: crypto-config/peerOrganizations/jd.com/msp
AnchorPeers: # 组织锚节点的配置
- Host: peer0.jd.com
Port: 7051
# 定义了排序服务的相关参数,这些参数将用于创建创世区块
Orderer: &OrdererDefaults
# 排序节点类型用来指定要启用的排序节点实现,不同的实现对应不同的共识算法
OrdererType: solo # 共识机制
Addresses: # Orderer 的域名(用于连接)
- orderer.qq.com:7050
BatchTimeout: 2s # 出块时间间隔
BatchSize: # 用于控制每个block的信息量
MaxMessageCount: 10 #每个区块的消息个数
AbsoluteMaxBytes: 99 MB #每个区块最大的信息大小
PreferredMaxBytes: 512 KB #每个区块包含的一条信息最大长度
Organizations:
# 用来定义用于 configtxgen 工具的配置入口
# 将 Profile 参数( TwoOrgsOrdererGenesis 或 TwoOrgsChannel )指定为 configtxgen 工具的参数
Profiles:
# TwoOrgsOrdererGenesis配置文件用于创建系统通道创世块
# 该配置文件创建一个名为SampleConsortium的联盟
# 该联盟在configtx.yaml文件中包含两个Peer组织Taobao和JD
TwoOrgsOrdererGenesis:
Orderer:
<<: *OrdererDefaults
Organizations:
- *QQ
Consortiums:
SampleConsortium:
Organizations:
- *Taobao
- *JD
# 使用TwoOrgsChannel配置文件创建应用程序通道
TwoOrgsChannel:
Consortium: SampleConsortium
Application:
<<: *ApplicationDefaults
Organizations:
- *Taobao
- *JD
在network中新建一个config文件夹
执行 configtxgen 命令,并指定 Profile 为 TwoOrgsOrdererGenesis 参数:
configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./config/genesis.block -channelID firstchannel
排序区块是排序服务的创世区块,通过以上命令就可以预先生成创世区块的 protobuf
格式的配置文件 ./config/genesis.block
了。
5、创建通道配置交易
接下来,我们需要继续使用 configtxgen 根据去创建通道的交易配置,和第 4 步不同的是,这次需要指定Profile 为 TwoOrgsChannel 参数。
生成通道配置事务 ./config/appchannel.tx :
configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./config/appchannel.tx -channelID appchannel
为 Taobao
组织定义锚节点,生成 ./config/TaobaoAnchor.tx
:
configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./config/TaobaoAnchor.tx -channelID appchannel -asOrg Taobao
为 JD
组织定义锚节点,生成 ./config/JDAnchor.tx
:
然后我们可以看到,config里有如下四个文件
6、创建并启动各组织的节点
我们说过:我们假设 QQ 作为一个运营方,提供了 1 个 Orderer 节点 orderer.qq.com 来创建联盟链的基础设施, 而 Taobao 和 JD 则是作为组织成员加入到链中,各自提供 2 个 Peer 节点 peer0.xx.com 和 peer1.xx.com 参与工作。
现在这些组织及其节点所需要的配置已经准备好了。我们接下来就可以使用 Docker Compose 来模拟启动这些节点服务。
由于这些节点之间需要互相通信,所以我们需要将这些节点都放入到一个 Docker 网络中,以 fabric_network 为例。
这里的话我们就在network文件夹下新建docker-compose.yaml
docker-compose.yaml 的内容如下:
version: '2.1'
volumes:
orderer.qq.com:
peer0.taobao.com:
peer1.taobao.com:
peer0.jd.com:
peer1.jd.com:
networks:
fabric_network:
name: fabric_network
services:
# 排序服务节点
orderer.qq.com:
container_name: orderer.qq.com
image: hyperledger/fabric-orderer:1.4.12
environment:
- GODEBUG=netdns=go
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_GENESISMETHOD=file
- ORDERER_GENERAL_GENESISFILE=/etc/hyperledger/config/genesis.block # 注入创世区块
- ORDERER_GENERAL_LOCALMSPID=QQMSP
- ORDERER_GENERAL_LOCALMSPDIR=/etc/hyperledger/orderer/msp # 证书相关
command: orderer
ports:
- "7050:7050"
volumes: # 挂载由cryptogen和configtxgen生成的证书文件以及创世区块
- ./config/genesis.block:/etc/hyperledger/config/genesis.block
- ./crypto-config/ordererOrganizations/qq.com/orderers/orderer.qq.com/:/etc/hyperledger/orderer
- orderer.qq.com:/var/hyperledger/production/orderer
networks:
- fabric_network
# Taobao 组织 peer0 节点
peer0.taobao.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer0.taobao.com
environment:
- CORE_PEER_ID=peer0.taobao.com
- CORE_PEER_LOCALMSPID=TaobaoMSP
- CORE_PEER_ADDRESS=peer0.taobao.com:7051
ports:
- "7051:7051" # grpc服务端口
- "7053:7053" # eventhub端口
volumes:
- ./crypto-config/peerOrganizations/taobao.com/peers/peer0.taobao.com:/etc/hyperledger/peer
- peer0.taobao.com:/var/hyperledger/production
depends_on:
- orderer.qq.com
# Taobao 组织 peer1 节点
peer1.taobao.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer1.taobao.com
environment:
- CORE_PEER_ID=peer1.taobao.com
- CORE_PEER_LOCALMSPID=TaobaoMSP
- CORE_PEER_ADDRESS=peer1.taobao.com:7051
ports:
- "17051:7051"
- "17053:7053"
volumes:
- ./crypto-config/peerOrganizations/taobao.com/peers/peer1.taobao.com:/etc/hyperledger/peer
- peer1.taobao.com:/var/hyperledger/production
depends_on:
- orderer.qq.com
# JD 组织 peer0 节点
peer0.jd.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer0.jd.com
environment:
- CORE_PEER_ID=peer0.jd.com
- CORE_PEER_LOCALMSPID=JDMSP
- CORE_PEER_ADDRESS=peer0.jd.com:7051
ports:
- "27051:7051"
- "27053:7053"
volumes:
- ./crypto-config/peerOrganizations/jd.com/peers/peer0.jd.com:/etc/hyperledger/peer
- peer0.jd.com:/var/hyperledger/production
depends_on:
- orderer.qq.com
# JD 组织 peer1 节点
peer1.jd.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer1.jd.com
environment:
- CORE_PEER_ID=peer1.jd.com
- CORE_PEER_LOCALMSPID=JDMSP
- CORE_PEER_ADDRESS=peer1.jd.com:7051
ports:
- "37051:7051"
- "37053:7053"
volumes:
- ./crypto-config/peerOrganizations/jd.com/peers/peer1.jd.com:/etc/hyperledger/peer
- peer1.jd.com:/var/hyperledger/production
depends_on:
- orderer.qq.com
# 客户端节点
cli:
container_name: cli
image: hyperledger/fabric-tools:1.4.12
tty: true
environment:
# go 环境设置
- GO111MODULE=auto
- GOPROXY=https://goproxy.cn
- CORE_PEER_ID=cli
command: /bin/bash
volumes:
- ./config:/etc/hyperledger/config
- ./crypto-config/peerOrganizations/taobao.com/:/etc/hyperledger/peer/taobao.com
- ./crypto-config/peerOrganizations/jd.com/:/etc/hyperledger/peer/jd.com
- ./../chaincode:/opt/gopath/src/chaincode # 链码路径注入
networks:
- fabric_network
depends_on:
- orderer.qq.com
- peer0.taobao.com
- peer1.taobao.com
- peer0.jd.com
- peer1.jd.com
如果是mac电脑使用M1,M2芯片,则需要添加这一行
为了方便,这里我还定义了一个 docker-compose-base.yaml
作为 Peer 节点的公共模板,内容如下:
version: '2.1'
services:
peer-base: # peer的公共服务
image: hyperledger/fabric-peer:1.4.12
environment:
- GODEBUG=netdns=go
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_LOGGING_PEER=info
- CORE_CHAINCODE_LOGGING_LEVEL=INFO
- CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/msp # msp证书(节点证书)
- CORE_LEDGER_STATE_STATEDATABASE=goleveldb # 状态数据库的存储引擎(or CouchDB)
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fabric_network # docker 网络
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
command: peer node start
networks:
- fabric_network
如果是mac电脑使用M1,M2芯片,则需要添加这一行
platform: linux/x86_64
在上面这个位置
注意观察,在 volumes 配置项中,我们将 config 和 crypto-config 内的配置文件都挂载到相对应的节点中了。并且在 peer 的公共服务中,我们还挂载了 /var/run/docker.sock 文件,有了该文件,在容器内就可以向其发送 http 请求和 Docker Daemon 通信,通俗理解,就是有了它,就可以在容器内操作宿主机的 Docker 了,比如在容器内控制 Docker 再启动一个容器出来。而这,就是为了后面可以部署智能合约(节点部署链码其实就是启动一个链码容器)。
接下来将节点启动
docker-compose up -d
出现以上界面即为启动成功
7、为 cli 服务配置环境
接下来我们要使用 cli
服务来执行 peer
命令,所以要为其先配置一下环境变量,使用四个不同的变量 TaobaoPeer0Cli
、TaobaoPeer1Cli
、JDPeer0Cli
、JDPeer1Cli
,代表 cli
服务代表着不同的节点:
分别输入
TaobaoPeer0Cli="CORE_PEER_ADDRESS=peer0.taobao.com:7051 CORE_PEER_LOCALMSPID=TaobaoMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/taobao.com/users/[email protected]/msp"
TaobaoPeer1Cli="CORE_PEER_ADDRESS=peer1.taobao.com:7051 CORE_PEER_LOCALMSPID=TaobaoMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/taobao.com/users/[email protected]/msp"
JDPeer0Cli="CORE_PEER_ADDRESS=peer0.jd.com:7051 CORE_PEER_LOCALMSPID=JDMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/jd.com/users/[email protected]/msp"
JDPeer1Cli="CORE_PEER_ADDRESS=peer1.jd.com:7051 CORE_PEER_LOCALMSPID=JDMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/jd.com/users/[email protected]/msp"
8、开始创建通道
通道主要用于实现区块链网络中业务的隔离。一个联盟中可以有多个通道,每个通道可代表一项业务,并且对应一套账本。通道内的成员为业务参与方(即联盟内的组织),一个组织可以加入多个通道。
我们现在有请 Taobao
组织的 peer0
节点来创建一个通道 appchannel
9、将所有节点加入通道
将所有的节点都加入到通道 appchannel
中(正常是按需加入):
docker exec cli bash -c "$TaobaoPeer0Cli peer channel join -b appchannel.block"
docker exec cli bash -c "$TaobaoPeer1Cli peer channel join -b appchannel.block"
docker exec cli bash -c "$JDPeer0Cli peer channel join -b appchannel.block"
docker exec cli bash -c "$JDPeer1Cli peer channel join -b appchannel.block"
10、更新锚节点
锚节点是必需的。普通节点只能发现本组织下的其它节点,而锚节点可以跨组织服务发现到其它组织下的节点,建议每个组织都选择至少一个锚节点。
利用之前准备好的配置文件,向通道更新锚节点:
docker exec cli bash -c "$TaobaoPeer0Cli peer channel update -o orderer.qq.com:7050 -c appchannel -f /etc/hyperledger/config/TaobaoAnchor.tx"
docker exec cli bash -c "$JDPeer0Cli peer channel update -o orderer.qq.com:7050 -c appchannel -f /etc/hyperledger/config/JDAnchor.tx"
PART3:编写智能合约
fabric 的智能合约称为链码,编写智能合约也就是编写链码。
在0202demo文件夹下新建一个chaincode文件夹
在里面以 Go 为例,创建一个 main.go 文件:
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)
type MyChaincode struct {
}
// Init 初始化时会执行该方法
func (c *MyChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("链码初始化")
return shim.Success(nil)
}
// Invoke 智能合约的功能函数定义
func (c *MyChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
funcName, args := stub.GetFunctionAndParameters()
switch funcName {
default:
return shim.Error(fmt.Sprintf("没有该功能: %s", funcName))
}
}
func main() {
err := shim.Start(new(MyChaincode))
if err != nil {
panic(err)
}
}
这里我们定义的 MyChaincode 结构体实现了 shim.Chaincode 接口:
然后在启动入口 main 函数中调用 shim.Start(new(MyChaincode)) 就完成了链码的启动,例子如下。
我们知道链码其实就是用来处理区块链网络中的成员一致同意的业务逻辑。比如 Taobao 和 JD 规定了一个规则,将其编写成链码,后面双方就只能遵循这个规则了,因为链码到时候即部署在你的节点,也会部署在我的节点上,你偷偷改了逻辑,我的节点不会认可你的,这也正是区块链的作用之一。
链码的功能定义在 Invoke 方法中。
一个简易的示例如下:
package main
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)
type MyChaincode struct {
}
// Init 初始化时会执行该方法
func (c *MyChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("链码初始化")
// 假设A有1000元,以复合主键 userA 的形式写入账本
err := WriteLedger(stub, map[string]interface{}{"name": "A", "balance": 1000}, "user", []string{"A"})
if err != nil {
return shim.Error(err.Error())
}
// 假设B有1000元,以复合主键 userB 的形式写入账本
err = WriteLedger(stub, map[string]interface{}{"name": "B", "balance": 1000}, "user", []string{"B"})
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(nil)
}
// Invoke 智能合约的功能函数定义
func (c *MyChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
funcName, args := stub.GetFunctionAndParameters()
switch funcName {
case "query":
return query(stub, args)
case "transfer":
return transfer(stub, args)
default:
return shim.Error(fmt.Sprintf("没有该功能: %s", funcName))
}
}
func query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 如果 args 为空,则表示查询所有 user
results, err := ReadLedger(stub, "user", args)
if err != nil {
return shim.Error(err.Error())
}
var users []map[string]interface{}
for _, result := range results {
var user map[string]interface{}
if err = json.Unmarshal(result, &user); err != nil {
return shim.Error(err.Error())
}
users = append(users, user)
}
usersByte, err := json.Marshal(&users)
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(usersByte)
}
func transfer(stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 验证参数
if len(args) != 3 {
return shim.Error("参数个数不满足")
}
from := args[0]
to := args[1]
money, err := strconv.ParseFloat(args[2], 64)
if err != nil {
return shim.Error(err.Error())
}
// 从账本查询 from 用户
fromResults, err := ReadLedger(stub, "user", []string{from})
if err != nil {
return shim.Error(err.Error())
}
if len(fromResults) != 1 {
return shim.Error("没有该用户 " + from)
}
var fromUser map[string]interface{}
if err = json.Unmarshal(fromResults[0], &fromUser); err != nil {
return shim.Error(err.Error())
}
// 从账本查询 to 用户
toResults, err := ReadLedger(stub, "user", []string{to})
if err != nil {
return shim.Error(err.Error())
}
if len(toResults) != 1 {
return shim.Error("没有该用户 " + to)
}
var toUser map[string]interface{}
if err = json.Unmarshal(toResults[0], &toUser); err != nil {
return shim.Error(err.Error())
}
// from 用户扣除余额
if money > fromUser["balance"].(float64) {
return shim.Error("余额不足")
}
fromUser["balance"] = fromUser["balance"].(float64) - money
// to 用户增加余额
toUser["balance"] = toUser["balance"].(float64) + money
// 写回账本
err = WriteLedger(stub, fromUser, "user", []string{from})
if err != nil {
return shim.Error(err.Error())
}
err = WriteLedger(stub, toUser, "user", []string{to})
if err != nil {
return shim.Error(err.Error())
}
return shim.Success([]byte("ok"))
}
func main() {
err := shim.Start(new(MyChaincode))
if err != nil {
panic(err)
}
}
// WriteLedger 写入账本
// obj 为要写入的数据
// objectType和keys 共同组成复合主键
func WriteLedger(stub shim.ChaincodeStubInterface, obj interface{}, objectType string, keys []string) error {
//创建复合主键
var key string
if val, err := stub.CreateCompositeKey(objectType, keys); err != nil {
return errors.New(fmt.Sprintf("%s-创建复合主键出错 %s", objectType, err.Error()))
} else {
key = val
}
bytes, err := json.Marshal(obj)
if err != nil {
return err
}
//写入区块链账本
if err := stub.PutState(key, bytes); err != nil {
return errors.New(fmt.Sprintf("%s-写入区块链账本出错: %s", objectType, err.Error()))
}
return nil
}
// ReadLedger 根据复合主键查询账本数据(适合获取全部或指定的数据)
// objectType和keys 共同组成复合主键
func ReadLedger(stub shim.ChaincodeStubInterface, objectType string, keys []string) (results [][]byte, err error) {
// 通过主键从区块链查找相关的数据,相当于对主键的模糊查询
resultIterator, err := stub.GetStateByPartialCompositeKey(objectType, keys)
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-获取全部数据出错: %s", objectType, err))
}
defer resultIterator.Close()
//检查返回的数据是否为空,不为空则遍历数据,否则返回空数组
for resultIterator.HasNext() {
val, err := resultIterator.Next()
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-返回的数据出错: %s", objectType, err))
}
results = append(results, val.GetValue())
}
return results, nil
}
在这段链码中,初始化的时候我们假设有用户 A 和 B ,并且都各自有 1000 元余额,我们在 Invoke 方法中为其定义了两个功能函数 query 和 transfer 。 其中 query 函数可以查询 A 和 B 或指定用户的余额信息, transfer 函数可以通过传入转账人,被转账人,金额,三个参数来实现转账功能。例如 {"Args":["transfer","A","B","100.0"]}代表 A 向 B 转账 100 元。
PART4:部署链码
我们将刚刚编写的智能合约也就是链码安装到区块链网络中,同样是借助 cli
服务,我们在 Taobao
组织的 peer0
节点和 JD
组织的 peer0
节点上都安装上链码:
docker exec cli bash -c "$TaobaoPeer0Cli peer chaincode install -n fabric-realty -v 1.0.0 -l golang -p chaincode"
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 节点为例:
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') 代表链码的写入操作需要同时得到 Taobao和 JD 组织成员的背书才允许通过。AND 也可以替换成 OR,代表任意一组织成员背书即可,更多具体用法,可以去看官方文档。
链码实例化成功之后就会启动链码容器,而启动的方法,就是我们之前提过的 peer 节点服务挂载了/var/run/docker.sock 文件。
查看启动的链码容器:
docker ps -a | awk '($2 ~ /dev-peer.*fabric-realty.*/) {print $2}'
因为我们使用 Taobao 组织的 peer0 节点实例化链码,所以此时还只有这个节点的链码容器启动起来了。
我们可以试着使用 cli 服务去调用链码:
docker exec cli bash -c "$TaobaoPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{\"Args\":[\"query\"]}'"
使用JD组织的节点也是可以的:
docker exec cli bash -c "$JDPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{\"Args\":[\"query\"]}'"
此时,因为我们查询了 JD 组织的 peer0 节点上的链码,所以对应的链码容器也会启动起来了,再次查看启动的链码容器:
现在,我们的智能合约就成功部署到区块链网络的通道中了。
PART5:编写应用程序
在0202demo里新建一个chaincode文件夹,在里面新建一个config.yaml,配置如下:
version: 1.0.0
# GO SDK 客户端配置
client:
# 客户端所属的组织,必须是organizations定义的组织
organization: JD
# 日志级别
logging:
level: info
# MSP证书的根路径
cryptoconfig:
path: /network/crypto-config
# 通道定义
channels:
appchannel:
orderers:
- orderer.qq.com
peers:
peer0.jd.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
peer1.jd.com:
endorsingPeer: true
chaincodeQuery: true
ledgerQuery: true
eventSource: true
# 组织配置
organizations:
JD:
mspid: "JDMSP"
cryptoPath: peerOrganizations/jd.com/users/{username}@jd.com/msp
peers:
- peer0.jd.com
- peer1.jd.com
# orderer节点列表
orderers:
orderer.qq.com:
url: orderer.qq.com:7050
# 传递给gRPC客户端构造函数
grpcOptions:
ssl-target-name-override: orderer.qq.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: true
# peers节点列表
peers:
# peer节点定义,可以定义多个
peer0.jd.com:
# URL用于发送背书和查询请求
url: peer0.jd.com:7051
# 传递给gRPC客户端构造函数
grpcOptions:
ssl-target-name-override: peer0.jd.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: true
peer1.jd.com:
url: peer1.jd.com:7051
grpcOptions:
ssl-target-name-override: peer1.jd.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: true
peer0.taobao.com:
url: peer0.taobao.com:7051
grpcOptions:
ssl-target-name-override: peer0.taobao.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: true
peer1.taobao.com:
url: peer1.taobao.com:7051
grpcOptions:
ssl-target-name-override: peer1.taobao.com
keep-alive-time: 0s
keep-alive-timeout: 20s
keep-alive-permit: false
fail-fast: false
allow-insecure: true
我们假定是 JD 组织来编写这个应用程序,该配置主要就是用于验证 JD 组织及其节点的身份。其中组织配置中 {username} 为动态传递, MSP 证书的根路径我们后续会挂载进去。
现在开始编写代码,我们先来实例化 SDK ,创建 sdk.go:
这里其实可以偷懒一些直接在电脑上先用pycharm写好,同样项目名为chaincode
sdk.go代码如下:
package main
import (
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
"github.com/hyperledger/fabric-sdk-go/pkg/core/config"
"github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
)
// 配置信息
var (
sdk *fabsdk.FabricSDK // Fabric SDK
channelName = "appchannel" // 通道名称
username = "Admin" // 用户
chainCodeName = "fabric-realty" // 链码名称
endpoints = []string{"peer0.jd.com", "peer0.taobao.com"} // 要发送交易的节点
)
// init 初始化
func init() {
var err error
// 通过配置文件初始化SDK
sdk, err = fabsdk.New(config.FromFile("config.yaml"))
if err != nil {
panic(err)
}
}
// ChannelExecute 区块链交互
func ChannelExecute(fcn string, args [][]byte) (channel.Response, error) {
// 创建客户端,表明在通道的身份
ctx := sdk.ChannelContext(channelName, fabsdk.WithUser(username))
cli, err := channel.New(ctx)
if err != nil {
return channel.Response{}, err
}
// 对区块链账本的写操作(调用了链码的invoke)
resp, err := cli.Execute(channel.Request{
ChaincodeID: chainCodeName,
Fcn: fcn,
Args: args,
}, channel.WithTargetEndpoints(endpoints...))
if err != nil {
return channel.Response{}, err
}
//返回链码执行后的结果
return resp, nil
}
// ChannelQuery 区块链查询
func ChannelQuery(fcn string, args [][]byte) (channel.Response, error) {
// 创建客户端,表明在通道的身份
ctx := sdk.ChannelContext(channelName, fabsdk.WithUser(username))
cli, err := channel.New(ctx)
if err != nil {
return channel.Response{}, err
}
// 对区块链账本查询的操作(调用了链码的invoke),只返回结果
resp, err := cli.Query(channel.Request{
ChaincodeID: chainCodeName,
Fcn: fcn,
Args: args,
}, channel.WithTargetEndpoints(endpoints...))
if err != nil {
return channel.Response{}, err
}
//返回链码执行后的结果
return resp, nil
}
在这段代码中,我们将使用 Admin 的身份去调用合约,并将每次的交易同时发送给 peer0.jd.com 和peer0.taobao.com 节点进行背书,这是因为我们在实例化链码的时候指定了背书策略为 AND ('TaobaoMSP.member','JDMSP.member') ,代表交易需要同时得到 Taobao和 JD 组织成员的背书才允许通过。每次写入账本时,会验证这两个节点的数据一致性,只有当这两个节点的数据一致时,交易才算最终成功。
继续编写 main.go ,我们使用 gin 来创建一个 http 服务:
先运行go get -u github.com/gin-gonic/gin下载gin包
main.go代码如下:
package main
import (
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/query", func(c *gin.Context) {
args := make([][]byte, 0)
user := c.Query("user")
if user != "" {
args = append(args, []byte(user))
}
// 调用链码的query函数
resp, err := ChannelQuery("query", args)
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"err": err.Error()})
return
}
var data []map[string]interface{}
if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
c.AbortWithStatusJSON(500, gin.H{"err": err.Error()})
return
}
c.JSON(200, data)
})
g.POST("/transfer", func(c *gin.Context) {
from := c.Query("from")
to := c.Query("to")
money := c.Query("money")
if from == "" || to == "" || money == "" {
c.AbortWithStatusJSON(400, gin.H{"err": "参数不能为空"})
return
}
args := make([][]byte, 0)
args = append(args, []byte(from), []byte(to), []byte(money))
// 调用链码的transfer函数
resp, err := ChannelExecute("transfer", args)
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"err": err.Error()})
return
}
c.JSON(200, gin.H{"msg": string(resp.Payload)})
})
g.Run("0.0.0.0:8000")
}
然后编写dockerfile文件
FROM golang:1.20 AS server
ENV GO111MODULE=on
ENV GOPROXY https://goproxy.cn,direct
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -v -o "server" .
RUN chmod +x server
FROM scratch
WORKDIR /app
COPY --from=server /app/server ./
COPY config.yaml ./
ENTRYPOINT ["./server"]
docker-compose.yml 文件:
version: '2.1'
networks:
fabric_network:
external:
name: fabric_network
services:
app:
build: .
image: app:latest
ports:
- "8000:8000"
volumes:
- ./../network/crypto-config:/network/crypto-config # 挂载搭建区块链网络时生成的crypto-config文件夹
networks:
- fabric_network
其中挂载的 crypto-config 文件夹就是之前搭建区块链网络时生成的。
接下来运行docker-compose build
如果出现问题如下:
可能是go没有配置镜像的问题,在环境中添加:
export GOPROXY=https://goproxy.cn
然后执行
go mod tidy
go mod download
即可正常
运行docker-compose up,效果如下:
接着可以通过CURL测试:
curl "http://localhost:8000/query"
curl http://localhost:8000/query?user=A
curl http://localhost:8000/query?user=B
curl -X POST http://localhost:8000/transfer?from=A&to=B&money=500
curl http://localhost:8000/query
如果想要通过ip在浏览器上查看,不要忘记打开8000的端口和防火墙。
测试完成,完结撒花~
拓展可以添加前端页面。流程呢,和传统前后端分离架构也没什么区别。
最后感谢原文博主的支持,特此奉上原文。
本人也是根据以上的学习再加以总结写的这篇文章。