一个小时带你用 Hyperledger Fabric开发一个Demo

先懂是什么,再懂怎么做,再懂为什么

本文作为系列开篇,力求通过一个简单fabric应用开发过程,让大家对fabric有个直观概括的了解。

前言

先放三张图:



第一张是Gartner 2017的技术成熟曲线图。对这张图的理解,大家仁者见仁智者见智,作为区块链从业者,既不想它被过度吹捧也不想被无脑黑。区块链不是万能的,但是在某些方向,尤其是对敏感信息加密共享有旺盛需求的领域,确实有其优势。


第二张是当前主要区块链项目的发展和应用状况。公共链领域,比特币依然一骑绝尘,以太坊紧跟其后,企业级商用方案百花齐放,但是IMB的Hyperledger正在逐渐成为联盟链方案的首选。



第三张是Hyperledger Fabric 1.0之后的总体功能图。从中可以看到,Fabric相比其他联盟链方案最大的优势有几点,一是原生自带的MSP(Member Service Provider),即人员组织权限管理,二是网络节点拓扑图原生符合联盟链的组织关系需求,三是各个模块的可插拔,对开发人员比较友好。

什么是Hyperledger Fabric

简而言之,Hyperledger Fabric 是IBM开源的一套区块链的解决方案。

维基百科上的介绍:


官网介绍:

几个关键词:distributed,confidentiality,flexibility,scalability,pluggable

即分布式(也有翻译成”去中心化“)、机密(PKI权限认证+信息加密传输)、灵活可扩展(节点类型多样、节点可复用、不同channel业务权限隔离)、支持组件插拔(CA、排序组件、加密组件即插即用)

环境搭建

因本文主旨在Fabric开发,所以基本环境搭建步骤略去,默认大家一分钟搭建完毕(不能破坏本博主说的一小时开发demo的宏伟承诺 😆)

1、centos(或者ubuntu)尽量用内核比较新的版本


linux.jpg

2、docker



docker安装完毕后最好添加阿里的docker hub镜像,步骤参见官网account.aliyun.com

3、docker-compose



4、go


Fabric环境部署&运行示例

下载fabric源代码

mk dir -p $GOPATH/src/github.com/hyperledger
cd $GOPATH/src/github.com/hyperledger
git clone https://github.com/hyperledger/fabric.git

下载结束后

cd $GOPATH/src/github.com/hyperledger/fabric
git checkout release-1.1(1.1是最新稳定版本)

批量下载fabric组件的docker镜像(也可以把所有组件下载本地编译运行,但太繁琐,不推荐)

cd $GOPATH/src/github.com/hyperledger/fabric/examples/e2e_cli/
source download-dockerimages.sh -c x86_64-1.1.0 -f x86_64-1.1.0

开始下载镜像,hold on a moment...

docker images 查看本地镜像


这只是几个主要的镜像,包括CA,排序,节点,java&go链码相关。真正开发中还会需要kafka,zookeeper,couchdb等支持,这些也是通过docker镜像的方式引入。
下面给出一张fabric主要镜像的依赖图,大家可以先体会下。

ok,环境部署完毕,进入例子目录
cd $GOPATH/src/github.com/hyperledger/fabric/examples/e2e_cli
先看下例子文件


惊不惊喜? 一个例子包含了这么多配置文件,不要怕,里面docker.yaml 都是不同网络拓扑的docker-compose配置文件,crypto-config.yaml是用来给网络节点、CA、用户初始化PKI环境(生成文件存储在cryto-config目录下,大家可以进该目录 tree命令感受下 手动斜眼),configtx.yaml是用来生成channel和order初始化信息(生成文件存储在channel-artifacts目录下),examples目录下存储的是智能合约代码(chaincode),其余的.sh文件就是这个例子执行命令。

跑之前先清一下docker网络环境

docker rm $(docker ps -qa)
docker network prune

开跑
./network_setup.sh up
经过一段超长的打印,执行结束时会出现


中途如果报错,可能是fabric版本跟images版本不一致原因,可重新下载fabric源码或镜像。

先docker ps看看当前容器



是不是有点晕,take it easy ,其实网络结构很简单,四个peer节点是用来提出交易(或者用来背书,或者用来commit),orderer节点用来接收交易并校验,kafka和zookeeper集群是用来做具体的排序共识,cli用来给开发者提供命令行操作入口。其中四个peer节点分别从属两个不同组织(org1,org2),清晰吧,其实这就是个比较简单的联盟链拓扑关系。

刚跑例子看见一大片一大片的命令刷屏是不是觉得wtf? 什么鬼?
好吧,这个官方例子是把网络建立,合约初始化,查询&交易,一次性全部执行一遍,所以略显繁杂。其实笔者是打算在后继章节详细解析一遍列子的配置文件和执行流程。这里先简单介绍下

整个例子执行分三大块

第一块:
根据crypto-config.yaml生成不同组织下的节点(CA,peer)用户(admin)的公私钥、数字证书
根据configtx.yaml生成创世块、channel初始化配置、锚节点配置(可省略)


第二块:
根据docker-compose文件生成网络。例子默认是使用docker-compose-cli.yaml。



第三块:
正如下面代码注释,创建channel、按配置文件把peer加入channel、安装和初始化chaincode、Query&Invode操作


怎么样,是不是很清晰。
这里额外说一句,Fabric 开发配置一直是大头,但是不要一开始就陷入配置的”汪洋大海“中。先大概了解是什么,再学会做一两个简单例子,再去深入了解为什么。循环往复,认知螺旋上升。

交易流程

晕了没,没晕再给大家放一张从fabric官网截下来的一次交易的时序图


不要关注细节,刚开始只要对fabric网络中的节点类型和交易流程有个直观印象就行。如上所示,一次交易过程必须包含的节点:
client : 可以把它理解为C端用户,通过命令行或者sdk发起查询(Query)或执行(Invoke)命令
peer:fabric网络中的节点,注意fabric中的节点也会根据需要划分几种类型(普通peer, endorsing peer, committer peer,anchor peer,且能根据需求切换),为什么要划分这么多类型,根本原因是为了减轻peer的负担,也是为了同一peer能在不同channel中复用。
consenters: 共识服务,fabric 0.6之前支持PBFT(拜占庭容错算法),1.0大的改版之后官方宣传支持kafka、solo、PBFT,不过当前版本仅支持前两种(期待后期添加)

开发一个小demo

好了,准备知识已完毕,进入最激动人心的模块,带大家从无到有建立一个基于fabric网络的demo,并封装sdk做简单的调用。

场景:账户管理
需求: 增删改查
网络拓扑:1CA+1Org(2peer)+1couchdb+1Orderer+2Chaincode

例子虽小,但基本上能模拟一个简单的联盟链网络。

mkdir -p $GOPATH/src/github.com/hyperledger/fabric-demo
cd $GOPATH/src/github.com/hyperledger/fabric-demo

在fabric-demo下新建三个目录


分别存放网络初始化文件,网络配置文件,合约代码

照例,初始化之前清理docker容器和网络:

docker rm $(docker ps -qa)
docker network prune

然后生产节点PKI文件和创世块等基础配置:

cd basic-network/
./generate.sh

注意:因为在我demo文件里已存在这些配置,所有这步可略过,此步骤主要针对初始文件为空,或者网络拓扑发生改变后需重新生成配置文件情况。且docker-compose.yml文件中CA的路径需要做同步更新。


启动

cd account
./startFabric.sh

最后打印这个表示执行成功


初始化流程可参看startFabric.sh源码
首先执行basic-network下的start.sh文件,初始化所有网络节点,其中节点的配置基于basic-network下的docker-compose.yml文件



网络初始化成功后会创建channel,并将两个peer加入到指定channel中



成功后再将chaincode分别装载进两个peer,并执行账本初始化

命令行最后打印的一大段信息就是初始化账本成功后的信息展示。

ok网络搭建成功,此时docker ps查看容器信息



简单明了,一个CA中心,用于负责org1下所有节点和用户的公私钥生产、证书颁布、权限校验,oderder节点,容器化的chaincode,两个peer节点,还有一个couchdb

couchdb

此处介绍下couchdb,fabric网络实际上有三种类型的数据存储,一种是账本本身,也就是区块链数据,是以文件形式存储;第二种是区块数据和历史数据的索引数据库;第三种是状态数据库,即存储我们在chaincode中执行的业务数据。其中第一和第二种是不能更换,第三个状态数据库默认是采用的levelDB,也支持couchdb。这些内容会在后继章节详细介绍。

怎么访问couchdb呢,上面暴露端口5984,那就直接访问 http://fabric所在主机ip:5984/_utils


其中就能直观看到我初始化的账号数据 ,是不是很方便,couchdb还能结合elasticsearch实现对状态数据的更为丰富的检索。此处注意,通过coudb能对状态数据做增删改操作,但是并不违背区块链不可篡改的特性,因为你仍然无法修改区块链本身的数据。

编写chainCode

chaincode(智能合约),其实就是一个接口,通过实现接口实现具体的业务操作。
Fabric的chaincode接口也很简单,只需实现Init和Invoke方法,具体的业务实现则通过在Invoke中对输入方法名做判断分发。
废话不说,show me the code

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "strconv"

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

type SmartContract struct {
}

type Account struct {
    Name   string `json:"name"`
    Gender string `json:"gender"`
    Age    string `json:"age"`
    Mail   string `json:"mail"`
}

func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
    return shim.Success(nil)
}


func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {

    function, args := APIstub.GetFunctionAndParameters()
    if function == "query" {
        return s.query(APIstub, args)
    } else if function == "init" {
        return s.initAccont(APIstub)
    } else if function == "create" {
        return s.create(APIstub, args)
    } else if function == "list" {
        return s.list(APIstub)
    } else if function == "update" {
        return s.update(APIstub, args)
    }

    return shim.Error("Invalid Smart Contract function name.")
}

func (s *SmartContract) query(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

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

    carAsBytes, _ := APIstub.GetState(args[0])
    return shim.Success(carAsBytes)
}

func (s *SmartContract) initAccont(APIstub shim.ChaincodeStubInterface) sc.Response {
    Accounts := []Account{
        Account{Name: "wesker", Gender: "male", Age: "26", Mail: "wesker@gmail.com"},
        Account{Name: "jill", Gender: "female", Age: "21", Mail: "jill@gmail.com"},
        Account{Name: "leon", Gender: "male", Age: "22", Mail: "leon@gmail.com"},
        Account{Name: "chris", Gender: "male", Age: "25", Mail: "chris@gmail.com"},
    }

    i := 0
    for i < len(Accounts) {
        fmt.Println("i is ", i)
        accountAsBytes, _ := json.Marshal(Accounts[i])
        APIstub.PutState("ACCOUNT"+strconv.Itoa(i), accountAsBytes)
        fmt.Println("Added", Accounts[i])
        i = i + 1
    }

    return shim.Success(nil)
}

func (s *SmartContract) create(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {

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

    var account = Account{Name: args[1], Gender: args[2], Age: args[3], Mail: args[4]}
        fmt.Println("New Added:", account)
    accountAsBytes, _ := json.Marshal(account)
        fmt.Println("New args[0]:", args[0])
        fmt.Println("New accountAsBytes:", accountAsBytes)
    APIstub.PutState(args[0], accountAsBytes)

    return shim.Success(nil)
}

func (s *SmartContract) list(APIstub shim.ChaincodeStubInterface) sc.Response {

    startKey := "ACCOUNT0"
    endKey := "ACCOUNT999"

    resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
    if err != nil {
        return shim.Error(err.Error())
    }
    defer resultsIterator.Close()

    // buffer is a JSON array containing QueryResults
    var buffer bytes.Buffer
    buffer.WriteString("[")

    bArrayMemberAlreadyWritten := false
    for resultsIterator.HasNext() {
        queryResponse, err := resultsIterator.Next()
        if err != nil {
            return shim.Error(err.Error())
        }
        // Add a comma before array members, suppress it for the first array member
        if bArrayMemberAlreadyWritten == true {
            buffer.WriteString(",")
        }
        buffer.WriteString("{\"Key\":")
        buffer.WriteString("\"")
        buffer.WriteString(queryResponse.Key)
        buffer.WriteString("\"")

        buffer.WriteString(", \"Record\":")
        // Record is a JSON object, so we write as-is
        buffer.WriteString(string(queryResponse.Value))
        buffer.WriteString("}")
        bArrayMemberAlreadyWritten = true
    }
    buffer.WriteString("]")

    fmt.Printf("- listAllAcount:\n%s\n", buffer.String())

    return shim.Success(buffer.Bytes())
}

func (s *SmartContract) update(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
        fmt.Println("Account update start")
    if len(args) != 2 {
        return shim.Error("Incorrect number of arguments. Expecting 2")
    }

    accountAsBytes, _ := APIstub.GetState(args[0])
    account := Account{}

    json.Unmarshal(accountAsBytes, &account)
    account.Name = args[1]
        fmt.Println("Account update name:",args[1])
    accountAsBytes, _ = json.Marshal(account)
    APIstub.PutState(args[0], accountAsBytes)

        fmt.Println("Account update end")
    return shim.Success(nil)
}

func main() {

    err := shim.Start(new(SmartContract))
    if err != nil {
        fmt.Printf("Error creating new Smart Contract: %s", err)
    }
}

主要实现自定义的SmartContract接口的两个方法,Init,Invoke即可。Invoke中通过shim.ChaincodeStubInterface实现具体的对账户的增删改操作。具体的chaincode实现会在后继章节详细介绍。此处主要是给大家一个直观印象,神乎其神的“智能合约”其实so easy。

sdk简单封装调用

网络ok了,合约安装完毕,只差应用层调用。
其实Fabric提供了两种方式给C端用户,一种是通过cli容器入口,一种是sdk。Fabric官网支持多种sdk,官方release的是node,java,除此之外还有Go,Python等sdk,活跃度都很高。笔者试验过Java和Go的sdk,表示功能和性能上都没太大差别,可能还是考虑到受众问题,官方还是以Node和Java为主。鉴于网络上大部分博客都是以node为实例,本博文打算以Java sdk为基础(不走寻常路,一小时时间快到了,加把劲~ )

在该demo中博主对java sdk的几个主要方法做了简单封装,实现admin账号注册登录,channel初始化,普通用户注册登录,普通用户对peer的Query和Invoke操作。
下面是demo的项目结构图。


对关键代码做一下简单说明
一个是pom.xml中导入fabric-sdk

<dependency>
            <groupId>org.hyperledger.fabric-sdk-java</groupId>
            <artifactId>fabric-sdk-java</artifactId>
            <version>1.1.0-alpha</version>
</dependency>

二个要实现Fabric 的User接口


/**
 * @program: HFUser
 * @description: 实现User接口
 * @author: Zhun.Xiao
 * @create: 2018-05-13 11:11
 **/
public class HFUser implements User, Serializable {


    private String name;
    private String account;
    private String affiliation;
    private String mspId;
    private Set<String> roles;
    private Enrollment enrollment;

    public HFUser(String name, String affiliation, String mspId, Enrollment enrollment) {
        this.name = name;
        this.affiliation = affiliation;
        this.mspId = mspId;
        this.enrollment = enrollment;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    @Override
    public String getAffiliation() {
        return affiliation;
    }

    public void setAffiliation(String affiliation) {
        this.affiliation = affiliation;
    }

    @Override
    public String getMspId() {
        return mspId;
    }

    public void setMspId(String mspId) {
        this.mspId = mspId;
    }

    @Override
    public Set<String> getRoles() {
        return roles;
    }

    public void setRoles(Set<String> roles) {
        this.roles = roles;
    }

    @Override
    public Enrollment getEnrollment() {
        return enrollment;
    }

    public void setEnrollment(Enrollment enrollment) {
        this.enrollment = enrollment;
    }
}

三个是对admin和普通用户证书的存放,因为是实例程序,证书简单存储在当前目录,以tail结尾。

HFJavaExample的main函数记录了一个正常的用户调用链网络执行查询和操作的过程。

        HFCAClient caClient = getHfCaClient("http://10.211.55.23:7054", null);
        // enroll or load admin
        HFUser admin = getAdmin(caClient);
        log.info(admin.toString());
        // register and enroll new user
        HFUser hfUser = getUser(caClient, admin, "wesker");
        log.info(hfUser.toString());
        // get HFC client instance
        HFClient client = getHfClient();
        // set user context
        client.setUserContext(admin);
        // get HFC channel using the client
        Channel channel = getChannel(client);
        log.info("Channel: " + channel.getName());
        // query alll account list
        queryBlockChain(client, "list");
        //update
        invokeBlockChain(client, "update", "ACCOUNT1", "jill_1");
        // query by condition
        queryBlockChain(client, "query", "ACCOUNT1");

右键点击run,可以根据idea打印的内容跟踪运行过程


也可以在fabric所在服务器

docker logs -f orderer.example.com 

跟踪orderer的docker log,查看怎么提交块信息的



也可以通过

docker logs -f peer0.org1.example.com

或者

docker logs -f peer1.org1.example.com

查看数据写入和同步的情况


也可以通过couchdb浏览器查看状态数据。

怎么样,开不开心?就是这么简单。

总结

本博文通过一个简单demo的实现,让大家对Hyperledger Fabric的特性、交易、开发有个直观的印象,很多地方只是点到即止,估计很多同学在看完后一脸懵B,不过不要紧,只要能把环境部署成功,官方示例运行成功,然后把博主的demo运行起来,个人觉得你已经很牛叉了,毕竟你才花了一个小时就掌握了博主n多天才弄明白的东西😆

附:
demo 配置文件地址:https://github.com/wesker8088/fabric-account.git
demo sdk例子地址: https://github.com/wesker8088/fabric-java-demo.git

推荐阅读更多精彩内容