跳到主要內容

在本地 Kubernetes 上快速部署 Hyperledger Fabric 網路

Paper
ArcBlock

作者 Paper

ArcBlock 一直致力於打造一個更好的區塊鏈開發平台,希望為開發者提供更多高效的開發工具,所以我們需要研究各種區塊鏈技術,首先了解開發者們迫切需要的是什麼,我們之前也分享了不少 BTC 和 ETH 的見解,怎麼能放過 Hyperledger,況且 ArcBlock 作為堂堂的 Hyperledge

什麼是 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/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 :) 最後把 chaincode_instantiate 和 chaincode_install 中 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,我們為區塊鏈開發者提供各種便利的工具,研究各種區塊鏈技術,而且我們還會把我們學到的都分享出來,讓大家一起進步。如果你熱愛區塊鏈或是你很喜歡我們的分享文化,趕快來加入我們这里。