在本地 kubernetes 上快速部署 Hyperledger Fabric 网络

作者 Paper

ArcBlock 一直致力于打造一个更好的区块链开发平台,希望给开发者提供更多高效的开发工具,所以我们需要研究各种区块链技术,首先了解开发者们迫切需要的是什么,我们之前也分享了不少 BTC 和 ETH 的见解,怎么能放过 Hyperledger,况且 ArcBlock 作为堂堂 Hyperledger Foundation 的会员,分享 Hyperledger 的知识是必须的。

什么是 Hyperledger?

Hyperledger (or the Hyperledger project) is an umbrella project of open source blockchains and related tools, started in December 2015 by the Linux Foundation, to support the collaborative development of blockchain-based distributed ledgers.

这是 wiki 百科的描述。它是一个区块链的 umbrella project,所以在它旗下有着诸多优秀项目,如 以太坊虚拟机 Burrow,支持分布账本上独立身份的 Indy,以及今天的主角 Fabric。

hyperledger 项目

Fabric 是一个提供分布式账本解决方案的平台。Hyperledger Fabric 由模块化架构支撑,并具备极佳的保密性、可伸缩性、灵活性和可扩展性。Hyperledger Fabric 被设计成支持不同的模块组件直接拔插启用,并能适应在经济生态系统中错综复杂的各种场景。Fabric 提供的可伸缩,可扩展的架构分享适合需要完备审查机制且需求各不相同的企业级区块链搭建。

废话少说,开始吧

打开 Fabric 的 get started, Ooops... 我还需要拉取 fabric 源码,并且编译各种工具。我只是想跑一个 HelloWorld!

为了可伸缩性和灵活性真是得付出不少代价啊,说到可伸缩和灵活,这不正是 Kubernets 的特点吗?thanks god!no,thanks open sources!

https://github.com/IBM/blockchain-network-on-kubernetes

这有一个非常棒的项目可以支持 Fabric 部署到 kuberntes 上,那么我们动手吧。

Hello Kubernetes

brew cask install docker

先安装 Docker 客户端,打开设置,到 kubernetes 标签页下勾选 enable kubernetes,然后让子弹飞一会儿~ enable k8s

kubectl create -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml

安装 Dashboard,当然,你也可以不用安装。

#获取token,复制一条即可
kubectl -n kube-system describe secret `kubectl -n kube-system get secret|grep admin-token|cut -d " " -f1`|grep "token:"|tr -s " "|cut -d " " -f2
#启动一个代理
kubectl proxy

打开http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/discovery?namespace=default,粘贴,即可看到 dashboard 你可以从 dashboard 可视化的看到你部署的容器以及容器的日志,当然你完全可以用 termial 查看。

部署区块链网络

git clone git@github.com:IBM/blockchain-network-on-kubernetes.git
chmod +x setup_blockchainNetwork.sh
chmod +x deleteNetwork.sh
./setup_blockchainNetwork.sh

好了,完成了 :),剩下的事儿只有等待。

TIPS:由于这个过程中需要从 docker hub 拉取镜像,所以,你可能需要一点众所周知的网络能力

当一切都完成之后:

$ kubectl get pods
NAME                                    READY     STATUS      RESTARTS   AGE
blockchain-ca-77459f9b84-zd8qt          1/1       Running     0          2m
blockchain-orderer-5c88f8cf95-25862     1/1       Running     0          2m
blockchain-org1peer1-7d95cbfd64-wcvz6   1/1       Running     0          2m
blockchain-org2peer1-d85dfcfc7-4kpmz    1/1       Running     0          2m
blockchain-org3peer1-6cffb6cbc7-stnj5   1/1       Running     0          2m
blockchain-org4peer1-84486f557c-pmvq8   1/1       Running     0          2m
chaincodeinstall-skd26                  0/4       Completed   0          1m
chaincodeinstantiate-jm85m              0/1       Completed   0          1m
copyartifacts-kmcp9                     0/1       Completed   0          3m
createchannel-p6ggq                     0/2       Completed   0          2m
joinchannel-lhqnx                       0/4       Completed   0          2m
utils-vr8fr                             0/2       Completed   0          2m

恭喜,你已经完成了 Fabric 网络的部署,是不是很简单。

来试试这个网络

我们从上面的输出可以看出来,这个区块链网络一共部署了一个 CA 节点,一个 orderer 节点 和 4 个组织节点 org1peer~org4peer 以及一些已经完成的事务操作。

CA 服务主要作用是身份注册,管理登录和交易证书,这里我们先不展开。

ORDERER 会提供排序服务,为客户端和 peer 节点提供共享的通信信道,为包含交易的广播提供服务。简单的说,这个服务保障了账本的正确记录和各 peer 本地账本的一致性。

Peer 节点以块的形式从排序服务接收有序状态更新,维护状态和账本,这里就是我们存放账本的地方,我们先进这个容器看一看:

kubectl exec -it blockchain-org1peer1-7d95cbfd64-wcvz6 bash #进入org1节点的bash
root@blockchain-org1peer1-7d95cbfd64-wcvz6:/# peer chaincode query -C channel1 -n cc -c '{"Args":["query","a"]}'

你会看到 get a value 我们先不管其中的过程,我们可以看到最后一行,我们得到的结果,值为 100.用同样的方法查询 b 的值,会得到 200。 然后我们再试试用 b 转账给 a 51 块钱。

peer chaincode invoke -o blockchain-orderer:31010 -C channel1 -n cc -c '{"Args":["invoke","b","a","51"]}'

get a value 同样先忽略校验过程,我们可以看到 invoke success 这样的字样。 然后你再登录到其他节点,看看 a 和 b 的值呢?

发生了什么?

其实以上部分,你已经完成了 Kubernetes 的本地部署和 Fabirc 网络在 Kubernetes 上的安装,你甚至在不知不觉中在网络中安装并实例化了一个 chaincode(可以理解为智能合约)。如果你不想跟深入的话,你已经可以关掉浏览器了,是不是很简单呢? 如果你想在这个网络上部署你自己写的 chaincode,那我们就得好好看看了。 首先打开 setup_blockchainNetwork.sh 这个脚本,看看里面究竟有些什么呢? 通读下来发现里面无非以下几个步骤。

  1. Creating Persistant Volume(创建一个持久卷)
  2. Copy artifacts job(把配置文件复制到持久卷中)
  3. Generate artifacts job (利用配置文件生成必要的文件)
  4. Creating Services for blockchain network (创建服务)
  5. Create peers, CA, Orderer using Kubernetes Deployments (部署服务)
  6. Creating channel transaction artifact and a channel (创建通道)
  7. Join all peers on a channel (让所有的节点加入到通道)
  8. Install chaincode on each peer (为每个节点安装 chaincode)
  9. Instantiate chaincode on channel (在通道上实例化 chaincode)

其实我们也不用关注其中所有的步骤,你只是想部署自己的 chaincode 而已。 1 和 2 是由于我们在 Kubernetes 上部署,他们需要一个磁盘空间,并把文件放上去,可以不用管(如果你要部署到云端的 kubernets 服务上,可以需要根据各云服务平台自行调整)。 3,4,5,6,7 都是在建立整个区块链环境,包含四个组织节点。 从 8 开始,我们才真正关注 chaincode。 我们来看看到这两步究竟做了什么? 打开 configFiles 中的 chaincode_install.xml,发现里面有四个任务,大同小异,重点是:

command: ["sh", "-c", "echo $GOPATH; cp -r /shared/artifacts/chaincode $GOPATH/src/; ls $GOPATH/src; peer chaincode install -n ${CHAINCODE_NAME} -v ${CHAINCODE_VERSION} -p chaincode_example02/"]

其实只有两步,把 artifacts 下的 chaincode 拷贝到 gopath 下,然后执行 install 命令。那我们来看看 chaincode 下有什么。

万圣节快乐

其实只有一个 go 文件,没错,这个就是你刚刚不小心已经安装好的 chaincode 了。

同样,我们再看看 chaincode_instantiate.yaml 这个文件,重点仍然是一句 commond:

peer chaincode instantiate -o blockchain-orderer:31010 -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -v ${CHAINCODE_VERSION} -c '{\"Args\":[\"init\",\"a\",\"100\",\"b\",\"200\"]}'

我们总算知道我们 a 和 b 的值从哪里来了。

什么是 Chaincode

Chaincode 是一段由 Go 语言编写(支持其他编程语言,如 Java),并能实现预定义接口的程序。chaincode 运行在一个受保护的 Docker 容器当中,与背书节点的运行互相隔离。chaincode 可通过应用提交的交易对账本状态初始化并进行管理。 一段 chaincode 通常处理由网络中的成员一致认可的业务逻辑,故我们很可能用“智能合约”来代指 chaincode。一段 chaincode 创建的(账本)状态是与其他 chaincode 互相隔离的,故而不能被其他 chaincode 直接访问。不过,如果是在相同的网络中,一段 chiancode 在获取相应许可后则可以调用其他 chiancode 来访问它的账本。 打开刚刚那个 go 文件,关注以下几个函数。

func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response;
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response;
func main();

init 函数会在 chaincode 实例化和升级的时候调用。 invoke 函数则可以处理各个节点的查询和改动账本的请求。 main 函数是入口函数,主要作用是启动 chaincode。

我们看看这几个函数里又有什么,不出意外,这两个函数都是在读取参数,格式转换。 有几句话引起了我们的注意: 3 从注释很容易理解,写入账本,把鼠标挪到它的类型处,可以看到更多方法。 我们这里暂时使用 getState 和 putState 就足够了。

开始写一个 chaincode

这一节可能需要一点点 golang 的基础知识,不要紧,我们为了尽量简洁,我们直接修改原来的 chaincode,不过在此之前,我们先把网络删除(这里可以不删除网络,直接更新 chaincode,但是需要编写新的配置文件,为了 demo 简洁,我们先删除网络)。

./deleteNetwork.sh

然后我们来编写一个博弈游戏,规则很简单:

  1. 设置一个初始值作为奖池
  2. user 来竞猜奖池金额,竞猜失败金额会累加到奖池中
  3. 竞猜金额与奖池金额差值在奖池总金额 1/4~3/8 区间会给用户反馈一个奖励
  4. 猜中金额会结束游戏,获得所有奖励

Talk is cheap, show you my code!

复制一下代码,覆盖原有的 chaincode。

package main

import (
    "errors"
    "fmt"
    "strconv"

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

type GameChainCode struct {
}

func (cc *GameChainCode) Init(stub shim.ChaincodeStubInterface) pb.Response {

    var sum string // Sum of asset holdings across accounts. Initially 0
    var sumVal int // Sum of holdings
    var err error
    _, args := stub.GetFunctionAndParameters()
    if len(args) != 1 {
        return shim.Error("Incorrect number of arguments. Expecting 1")
    }
    // Initialize the chaincode
    sum = args[0]
    sumVal, err = strconv.Atoi(sum)
    if err != nil {
        return shim.Error("Expecting integer value for sum")
    }
    fmt.Printf("sumVal = %d\n", sumVal)
    if sumVal < 100 {
        return shim.Error("init value must bigger than 100")
    }
    // Write the state to the ledger
    err = stub.PutState("total", []byte(strconv.Itoa(sumVal)))
    if err != nil {
        return shim.Error(err.Error())
    }
    return shim.Success(nil)
}

func (cc *GameChainCode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    function, args := stub.GetFunctionAndParameters()
    if function == "query" {
        return cc.query(stub, args)
    } else if function == "lottery" {
        return cc.lottery(stub, args)
    } else if function == "create_user" {
        return cc.create_user(stub, args)
    }
    return shim.Error("Invalid invoke function name. Expecting query uid,lottery uid token,create_user uid")
}

func (cc *GameChainCode) create_user(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    if len(args) != 1 {
        return shim.Error("params error")
    }
    uId := args[0]
    balance, err := cc.getBalance(stub, uId)
    if balance < 0 || err != nil {
        stub.PutState(uId, []byte("1000"))
        return shim.Success([]byte("1000"))
    } else {
        return shim.Error("uid has been registed")
    }
}

func (cc *GameChainCode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    if len(args) == 0 {
        t, err := cc.getTotal(stub)
        if err != nil {
            return shim.Error("get total error!")
        }
        if t < 100 {
            return shim.Success([]byte("false"))
        } else {
            return shim.Success([]byte("true"))
        }
    } else {
        t, err := cc.getBalance(stub, args[0])
        if err != nil {
            return shim.Error("get balance error!")
        } else {
            return shim.Success([]byte(strconv.Itoa(t)))
        }
    }

}

func (cc *GameChainCode) lottery(stub shim.ChaincodeStubInterface, args []string) pb.Response {
    if len(args) != 2 {
        return shim.Error("params error")
    }
    t, err := cc.getTotal(stub)
    if err != nil {
        return shim.Error("get total error!")
    }
    uId := args[0]
    uWager, err := strconv.Atoi(args[1])
    if err != nil {
        return shim.Error("wager error!")
    }
    err = cc.plus(stub, uId, -uWager)
    if err != nil {
        return shim.Error("balance not enough")
    }

    if (uWager - t) == 0 {
        //hit
        err = cc.plus(stub, uId, uWager+t)
        return shim.Success([]byte("you win! game over!"))
    } else if ((t - uWager) > t>>2) && ((t - uWager) < t>>2+t>>3) {
        err = cc.plus(stub, uId, uWager>>1)
        return shim.Success([]byte("congratulation!!!!"))
    } else {
        err = cc.plus(stub, "total", uWager)
        return shim.Success([]byte("Good Luck Next Time!"))
    }
    return shim.Error("bet error")

}

//plus or minus value of key
func (cc *GameChainCode) plus(stub shim.ChaincodeStubInterface, k string, v int) error {
    Avalbytes, err := stub.GetState(k)
    if err != nil {
        return err
    }
    TotalV, err := strconv.Atoi(string(Avalbytes))
    if err != nil {
        return err
    }
    TotalV += v
    if TotalV < 0 {
        return errors.New("balance not enough")
    }
    stub.PutState(k, []byte(strconv.Itoa(TotalV)))
    return nil
}

func (cc *GameChainCode) getBalance(stub shim.ChaincodeStubInterface, k string) (int, error) {
    Avalbytes, err := stub.GetState(k)
    if err != nil {
        return -1, err
    }
    TotalV, err := strconv.Atoi(string(Avalbytes))
    if err != nil {
        return -1, err
    }
    return TotalV, nil
}

func (cc *GameChainCode) getTotal(stub shim.ChaincodeStubInterface) (int, error) {
    return cc.getBalance(stub, "total")
}

func main() {
    err := shim.Start(new(GameChainCode))
    if err != nil {
        fmt.Printf("Error starting game chaincode: %s", err)
    }
}

然后修改 chaincode_instantiate 文件中 chaincode 实例化中的参数。

peer chaincode instantiate -o blockchain-orderer:31010 -C ${CHANNEL_NAME} -n ${CHAINCODE_NAME} -v ${CHAINCODE_VERSION} -c '{\"Args\":[\"init\",\"1024\"]}'

我们把初始奖池金额设置成 1024 :) 最后把 chaincodeinstantiate 和 chaincodeinstall 中 CHAINCODE_VERSION 值改为 1.1

ok,再次启动./setup_blockchainNetwork.sh

先创建一个用户 paper,系统会分配 1000 给他。

kubectl exec -it $(kubectl get pods | grep org1peer | awk '{ print $1 }')  -- peer chaincode invoke -C channel1 -n cc -c '{"Args":["create_user","paper"]}'
#查询余额,看看是不是1000块
kubectl exec -it $(kubectl get pods | grep org1peer | awk '{ print $1 }')  -- peer chaincode invoke -C channel1 -n cc -c '{"Args":["query","paper"]}'
#投资50,猜一猜
kubectl exec -it $(kubectl get pods | grep org1peer | awk '{ print $1 }')  -- peer chaincode invoke -C channel1 -n cc -c '{"Args":["lottery","paper","50"]}'
#获得结果 result: status:200 payload:"Good Luck Next Time!"

#再来一次,赌大一点800
kubectl exec -it $(kubectl get pods | grep org1peer | awk '{ print $1 }')  -- peer chaincode invoke -C channel1 -n cc -c '{"Args":["lottery","paper","800"]}'
#result: status:200 payload:"congratulation!!!!"
#哈哈,中奖了

你可以在同的 peer 节点多创建几个用户来模拟游戏。 这是个典型的多方零和博弈游戏,如果部署在传统中信化的网络中,难免要怀疑主办方作弊从而增加监察机构,如果再怀疑监察机构,那么还得再引入一方。

而在这个 demo 的区块链网络中,会有 4 个 org peer 保存账本,任何一方无法作弊,另外,作为一个零和游戏,也没有必要所有节点都作弊,因为这样谁也没好处,最后,当一轮游戏结束后,我们可以查看账本里所有的记录来确保没有人作弊。所以,参与这个游戏的用户可以非常放心,这里就展现了区块链技术的一个非常大的优势,低成本的信任。当然以上代码只是非常简单的一个 demo,逻辑和书写上都不够完善,有兴趣的同学可以自己再去改进, 本demo中的代码可以在 https://github.com/ArcBlock/blockchain-network-on-kubernetes 这里找到。

经过以上的内容,你已经成功部署了一个 Fabric 网络,以及部署了自己的 chaincode,但是这其中还涉及到验证过程,排序过程,背书策略等等都不是一下子能讲清楚的,以后的 blog 会一一说明,欢迎大家继续关注 ArcBlock Enginneer Blog。

在 ArcBlock,我们为区块链开发者提供各种便捷的工具,研究各种区块链技术,而且我们还会把我们学到的都分享出来,让大家一起进步。如果你热爱区块链或者你很喜欢我们的分享文化,赶快来加入我们这里