萬字長文,教你用 go 開發區塊鏈應用
大概 2 年前,碰巧學習區塊鏈(Hyperledger Fabric),便寫了一個入門級的項目放在 GitHub 上,公衆號有不少讀者是通過這個項目關注到我的,也經常問我,有沒有區塊鏈這方面的學習資料,有沒有這個項目的詳細講解,如何搭建一個區塊鏈網絡,林林總總。
對於這些問題,我每次的回覆都一樣,學習資料我倒是沒有,但是 官方文檔 [1] 就是最好的資料了。
不過今天,我想還是通過這篇文章來記錄一下我對之前區塊鏈學習的一次總結吧。
對了,這個項目的地址是:https://github.com/togettoyou/fabric-realty[2] ,有幫助的話點個 star
預警:爲了照顧到更多讀者,本篇儘量從新手的視角出發,可能會有很多特別基礎的內容,對於已經懂的部分,選擇跳過即可。
再次預警:文章內容有點長,請耐心看,最好跟着一起動手實踐,如果中途發現了錯誤之處,歡迎告知我。
技術棧
首先,以下這些我提到的技術要求你事先稍微學習掌握一下:
1、yaml 文件的編寫
需要注意一下幾個規則:
-
大小寫敏感
-
使用縮進表示層級關係
-
縮進不允許使用 tab,只允許空格
-
縮進的空格數不重要,只要相同層級的元素左對齊即可
-
#
表示註釋 -
&
用來建立錨點,<<
表示合併到當前數據,*
用來引用錨點
2、Docker 和 Docker Compose
-
Docker 是一個開源的應用容器引擎,可以將應用以及所需要的環境一起打包到一個輕量級、可移植的容器中,從而可以快速交付軟件。
-
Docker Compose 是用來定義和運行多容器的工具。可以通過 yaml 文件來配置應用程序需要的所有服務。說白了,就是批量管理 Docker 容器。
後續區塊鏈的節點以及應用程序的部署我們都會使用 Docker Compose 來管理。
3、 go 語言
我的項目包括本篇文章的示例都是使用 go 語言開發的,雖然 fabric 也提供了 Java,nodejs,python 等語言的 SDK ,但個人還是比較推薦 go 語言,畢竟 fabric 自身也是 go 實現的。
題外話:以上這些技能除了在 fabric 區塊鏈體系中需掌握,在如今火熱的雲原生技術下也一樣是基礎。
區塊鏈基礎知識
1、什麼是區塊
Block
,每個區塊記錄着上一個區塊的 hash
值、本區塊中的交易集合、本區塊的 hash
等基礎數據。由於每個區塊都有上一區塊的 hash
值,區塊間由這個值兩兩串聯,形成了區塊鏈。
2、什麼是區塊鏈
Blockchain
,最早起源於比特幣的底層技術,並在其後不斷演進發展。
區塊鏈本質上就是一個多方共享的分佈式賬本技術,用來記錄網絡上發生的所有交易。
而其中去中心化的概念,是因爲賬本信息會被複制到許多網絡參與者中,每個參與者都在協作維護賬本,不像傳統應用的數據被中心管理着。
另外信息只能以附加的方式記錄到賬本上,並使用加密技術保證一旦將交易添加到賬本就無法修改。這種不可修改的屬性簡化了信息的溯源,因爲參與者可以確定信息在記錄後沒有改變過。所以區塊鏈有時也被稱爲證明系統。
3、什麼是公鏈、聯盟鏈和私鏈
區塊鏈分爲公有鏈、聯盟鏈、私有鏈三種基本類型。其中:
-
完全去中心化:公鏈,人人都可以參與,就像比特幣(挖礦相當於在記賬)。主要採取工作量證明機制 (POW)、權益證明機制(POS)、股份授權證明機制(DPOS) 等方式。
-
部分去中心化:聯盟鏈,參與者是指定的。聯盟鏈可以是幾家公司共同擁有的鏈,也可能是幾個國家共同承認的鏈。這是後續發展的趨勢。
-
中心化:私鏈,寫入權限僅在一個組織手裏的區塊鏈,僅對特定的團隊、組織或者個人開放。
4、什麼是交易
Transaction
,區塊鏈接收的數據稱之爲交易。
5、什麼是智能合約
Smart contract
,爲了支持以同樣的方式更新信息,並實現一整套賬本功能(交易,查詢等),區塊鏈使用智能合約來提供對賬本的受控訪問。
智能合約不僅是在網絡中封裝和簡化信息的關鍵機制,它還可以被編寫成自動執行參與者的特定交易的合約。
例如,可以編寫智能合約以規定運輸物品的成本,其中運費根據物品到達的速度而變化。根據雙方同意並寫入賬本的條款,當收到物品時,相應的資金會自動轉手。
通俗易懂點,智能合約就是按照大家約定好的規則編寫的業務邏輯代碼實現,然後只能通過這些合約來操作區塊鏈網絡這個賬本。
6、什麼是共識
保持賬本在整個網絡中同步的過程稱爲共識。該過程確保賬本僅在交易被相應參與者批准時纔會更新,並且當賬本更新時,它們以相同的順序更新相同的交易。
Hyperledger Fabric 基礎知識
1、什麼是 Hyperledger Fabric
Linux 基金會於 2015 年創建了 Hyperledger(超級賬本)項目,而 Hyperledger Fabric 是其中一個用 Go 語言實現的版本。
Hyperledger Fabric 網絡的成員只能從可信賴的成員服務提供者(MSP) 註冊,也就是說 Hyperledger Fabric 搭建的區塊鏈是一種聯盟鏈。
Hyperledger Fabric 的賬本包括兩個組件: 世界狀態和交易日誌。並且每個參與者都擁有他們所屬的每個 Hyperledger Fabric 網絡的賬本的副本。
-
世界狀態:描述了在給定時間點的賬本的狀態。它是賬本的數據庫。默認情況下,使用 LevelDB 鍵值存儲數據庫,可插拔,可替換爲 CouchDB 。
-
交易日誌:記錄產生世界狀態中當前值的所有交易。這是世界狀態的更新歷史。它只記錄區塊鏈網絡使用賬本數據庫前後的值。
總結:Hyperledger Fabric 是一種賬本技術,其賬本包括世界狀態數據庫和交易日誌歷史記錄。
2、什麼是聯盟
聯盟指參與一個基於區塊鏈的業務協作或業務交易網絡的所有組織的集合,一個聯盟一般包含多個組織。
一般由聯盟發起方或運營方創建 Orderer
排序節點,並負責交易排序、區塊產生和達成共識。聯盟發起方或運營方邀請各個組織實例加入聯盟,進而創建通道。
3、什麼是組織
組織代表的是參與區塊鏈網絡的企業、政府機構、團體等實體。
一個組織實例主要包含如下節點:
-
CA
:區塊鏈節點類型之一,全稱 Certificate Authority ,數字證書頒發機構,負責組織內部成員的register
和enroll
等,爲該組織的區塊鏈用戶生成和頒發數字證書。 -
Peer
:區塊鏈節點類型之一,負責保存和記錄賬本數據、對交易背書、運行智能合約等。
4、什麼是節點
節點(Peers)是區塊鏈的通信實體。它只是一個邏輯功能,只要能在 “信任域” 中分組並與控制它們的邏輯實體相關聯,就可以將不同類型的多個節點運行在同一個物理服務器上,比如用 Docker 部署。
-
Orderer
排序服務節點 或 排序節點:Orderer 是一個運行實現交付擔保的通信服務節點,例如原子性或總順序廣播。排序節點負責接受交易並排序(排序算法有: SOLO,KAFKA,RAFT,PBFT),最後將排序好的交易按照配置中的約定整理爲區塊之後提交給記賬節點進行處理。 -
Peer
節點:Peer 是業務參與方組織在區塊鏈網絡中所擁有的參與共識和賬本記錄的節點。可以有多種角色。作爲Committing Peer
記賬節點時,無需安裝鏈碼,只負責驗證從 Orderer 發出的區塊和交易的合法性、並存儲賬本區塊信息。作爲Endorsing Peer
背書節點時,必須安裝鏈碼,在交易時需進行簽名背書。 -
Anchor
錨節點:爲了實現高可用,每個參與方組織一般包含兩個或多個Peer
節點,可以設置其中的一個爲Anchor
,與區塊鏈網絡中的其他組織進行信息同步。 -
客戶端節點:客戶端扮演了代表最終用戶的實體,可以同時與
Peer
和Orderer
通信,創建並調用交易。這裏客戶端可以指應用程序、SDK、命令行等。
5、什麼是通道
Hyperledger Fabric 中的通道(Channel
)是兩個或兩個以上特定網絡成員之間通信的專用 “子網”,用於進行私有和機密的交易。
可以理解爲組織間拉了個羣聊,這個羣聊就是通道,在裏面聊天交易,一個聯盟鏈中可以有多個羣聊(通道),一個組織可以加入多個羣聊,每個羣聊可以代表一項具體的業務,有自身對應的一套賬本,羣聊間互不干擾,互相隔離。
6、什麼是鏈碼
Hyperledger Fabric 的智能合約用鏈碼(Chaincode
)編寫。在大多數情況下,鏈碼只與賬本的數據庫即世界狀態交互,而不與交易日誌交互。
鏈碼可以用多種編程語言實現。有 Go、Node.js 和 Java 鏈碼等。
搭建區塊鏈網絡
基礎知識過完,接下來就到了本篇核心的項目實戰環節。首先是搭建一個區塊鏈網絡,只需按照下面幾個順序,一步步來就行(推薦在 Linux 或 MacOS 下操作):
1、下載 fabric 二進制工具
以 v1.4.12
版本爲例, fabric 二進制工具的下載地址在:https://github.com/hyperledger/fabric/releases/tag/v1.4.12[3]
自行根據你的系統環境下載對應的包。
其中幾個主要的工具說明:
-
cryptogen
:用來生成 Hyperledger Fabric 密鑰材料的工具,這個過程是靜態的。cryptogen
工具通過一個包含網絡拓撲的crypto-config.yaml
文件,爲所有組織和屬於這些組織的組件生成一組證書和祕鑰。cryptogen
適合用於測試開發環境,在生產環境建議使用動態的 CA 服務。 -
configtxgen
:用於創建和查看排序節點的創世區塊、通道配置交易等相關的工具。configtxgen
使用configtx.yaml
文件來定義網絡配置。 -
configtxlator
:fabric 中Protobuf
和JSON
格式轉換的工具,fabric 中任何的使用Protobuf
定義的類型,都可使用該工具進行轉換。 -
peer
:peer 命令有 5 個不同的子命令,每個命令都可以讓指定的 peer 節點執行特定的一組任務。比如,可以使用子命令peer channel
讓一個 peer 節點加入通道,或者使用peer chaincode
命令把智能合約鏈碼部署到 peer 節點上。
2、將 fabric 二進制工具添加到環境變量
爲了後續方便使用命令,可以將第 1 步下載的工具添加到系統環境變量中:
$ export PATH=${PWD}/hyperledger-fabric-linux-amd64-1.4.12/bin:$PATH
3、生成證書和祕鑰
我們將使用 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
接着執行 cryptogen generate
命令,生成結果將默認保存在 crypto-config
文件夾中:
$ cryptogen generate --config=./crypto-config.yaml
taobao.com
jd.com
我們可以看看在 crypto-config
文件夾裏生成了什麼:
$ tree crypto-config
crypto-config
├── ordererOrganizations
│ └── qq.com
│ ├── ca
│ │ ├── 3e41f960bb5a3002a1e436e9079311d79cf8846c2ad2a09080ea8575e16bb5b7_sk
│ │ └── ca.qq.com-cert.pem
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@qq.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.qq.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.qq.com-cert.pem
│ ├── orderers
│ │ └── orderer.qq.com
│ │ ├── msp
│ │ │ ├── admincerts
│ │ │ │ └── Admin@qq.com-cert.pem
│ │ │ ├── cacerts
│ │ │ │ └── ca.qq.com-cert.pem
│ │ │ ├── keystore
│ │ │ │ └── 6bd45f78877b96cfbcd040262ee4c808bd6d894cabfed44552fb7c22d6d427d1_sk
│ │ │ ├── signcerts
│ │ │ │ └── orderer.qq.com-cert.pem
│ │ │ └── tlscacerts
│ │ │ └── tlsca.qq.com-cert.pem
│ │ └── tls
│ │ ├── ca.crt
│ │ ├── server.crt
│ │ └── server.key
│ ├── tlsca
│ │ ├── bd48b5360c82ce5beeb31dea1b7e8e7918a5e7246d3f8892889fe1b2efadc1aa_sk
│ │ └── tlsca.qq.com-cert.pem
│ └── users
│ └── Admin@qq.com
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@qq.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.qq.com-cert.pem
│ │ ├── keystore
│ │ │ └── f28c1ed4c67fd438a891e420a2e53b20352bdf40907a0a8ee39095505475c99f_sk
│ │ ├── signcerts
│ │ │ └── Admin@qq.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.qq.com-cert.pem
│ └── tls
│ ├── ca.crt
│ ├── client.crt
│ └── client.key
└── peerOrganizations
├── jd.com
│ ├── ca
│ │ ├── 5672a9717fd943d0dcd2269ea1700c10309ad49d16b849e9c6e24225deafceb5_sk
│ │ └── ca.jd.com-cert.pem
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@jd.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.jd.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.jd.com-cert.pem
│ ├── peers
│ │ ├── peer0.jd.com
│ │ │ ├── msp
│ │ │ │ ├── admincerts
│ │ │ │ │ └── Admin@jd.com-cert.pem
│ │ │ │ ├── cacerts
│ │ │ │ │ └── ca.jd.com-cert.pem
│ │ │ │ ├── keystore
│ │ │ │ │ └── 012700eb44d6e19becb63c944e685a18d69ea9f1120aaa45fe549236c6a90fb6_sk
│ │ │ │ ├── signcerts
│ │ │ │ │ └── peer0.jd.com-cert.pem
│ │ │ │ └── tlscacerts
│ │ │ │ └── tlsca.jd.com-cert.pem
│ │ │ └── tls
│ │ │ ├── ca.crt
│ │ │ ├── server.crt
│ │ │ └── server.key
│ │ └── peer1.jd.com
│ │ ├── msp
│ │ │ ├── admincerts
│ │ │ │ └── Admin@jd.com-cert.pem
│ │ │ ├── cacerts
│ │ │ │ └── ca.jd.com-cert.pem
│ │ │ ├── keystore
│ │ │ │ └── b1e81b66080705595f5e56cc8d78575b0e935b79c8f674001e46cae452a71f32_sk
│ │ │ ├── signcerts
│ │ │ │ └── peer1.jd.com-cert.pem
│ │ │ └── tlscacerts
│ │ │ └── tlsca.jd.com-cert.pem
│ │ └── tls
│ │ ├── ca.crt
│ │ ├── server.crt
│ │ └── server.key
│ ├── tlsca
│ │ ├── f4c7d0b660575f383d189696480bf559f312d798eb0352c9102f8be6ecde52d6_sk
│ │ └── tlsca.jd.com-cert.pem
│ └── users
│ ├── Admin@jd.com
│ │ ├── msp
│ │ │ ├── admincerts
│ │ │ │ └── Admin@jd.com-cert.pem
│ │ │ ├── cacerts
│ │ │ │ └── ca.jd.com-cert.pem
│ │ │ ├── keystore
│ │ │ │ └── d7f476884ff36a19aa7100c63aa30f8f378cc5ec826ca58977539e1c9c6b22df_sk
│ │ │ ├── signcerts
│ │ │ │ └── Admin@jd.com-cert.pem
│ │ │ └── tlscacerts
│ │ │ └── tlsca.jd.com-cert.pem
│ │ └── tls
│ │ ├── ca.crt
│ │ ├── client.crt
│ │ └── client.key
│ └── User1@jd.com
│ ├── msp
│ │ ├── admincerts
│ │ │ └── User1@jd.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.jd.com-cert.pem
│ │ ├── keystore
│ │ │ └── e83862c8e78509f2a4362d3282214421179fa47f3d655f75cb3539d5534f7494_sk
│ │ ├── signcerts
│ │ │ └── User1@jd.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.jd.com-cert.pem
│ └── tls
│ ├── ca.crt
│ ├── client.crt
│ └── client.key
└── taobao.com
├── ca
│ ├── 4a31791b9fade54ab70496f03169707f6b9643c04d1bc734da15b0c625628865_sk
│ └── ca.taobao.com-cert.pem
├── msp
│ ├── admincerts
│ │ └── Admin@taobao.com-cert.pem
│ ├── cacerts
│ │ └── ca.taobao.com-cert.pem
│ └── tlscacerts
│ └── tlsca.taobao.com-cert.pem
├── peers
│ ├── peer0.taobao.com
│ │ ├── msp
│ │ │ ├── admincerts
│ │ │ │ └── Admin@taobao.com-cert.pem
│ │ │ ├── cacerts
│ │ │ │ └── ca.taobao.com-cert.pem
│ │ │ ├── keystore
│ │ │ │ └── 914648b8c4dc4783b0505a22b5c7630e424c3cf8dd54e2fe05b47dc321a4e61b_sk
│ │ │ ├── signcerts
│ │ │ │ └── peer0.taobao.com-cert.pem
│ │ │ └── tlscacerts
│ │ │ └── tlsca.taobao.com-cert.pem
│ │ └── tls
│ │ ├── ca.crt
│ │ ├── server.crt
│ │ └── server.key
│ └── peer1.taobao.com
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@taobao.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.taobao.com-cert.pem
│ │ ├── keystore
│ │ │ └── 3eef8defc07afb547e94f08702a5b30807d2e2a672e3d437bfb54dd1590b0fa7_sk
│ │ ├── signcerts
│ │ │ └── peer1.taobao.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.taobao.com-cert.pem
│ └── tls
│ ├── ca.crt
│ ├── server.crt
│ └── server.key
├── tlsca
│ ├── 296a941f625974153aa5ab6cf57b0933023aaa13b0e4363a7378e5c527de26a1_sk
│ └── tlsca.taobao.com-cert.pem
└── users
├── Admin@taobao.com
│ ├── msp
│ │ ├── admincerts
│ │ │ └── Admin@taobao.com-cert.pem
│ │ ├── cacerts
│ │ │ └── ca.taobao.com-cert.pem
│ │ ├── keystore
│ │ │ └── a2af975d659f77182b2aca318321797d281036f085dda9799ab79b6400e5e970_sk
│ │ ├── signcerts
│ │ │ └── Admin@taobao.com-cert.pem
│ │ └── tlscacerts
│ │ └── tlsca.taobao.com-cert.pem
│ └── tls
│ ├── ca.crt
│ ├── client.crt
│ └── client.key
└── User1@taobao.com
├── msp
│ ├── admincerts
│ │ └── User1@taobao.com-cert.pem
│ ├── cacerts
│ │ └── ca.taobao.com-cert.pem
│ ├── keystore
│ │ └── c65d45e1c7e1070e3f1b00bd8ac41e91d2bfaea10a769d75b9599590791ccc02_sk
│ ├── signcerts
│ │ └── User1@taobao.com-cert.pem
│ └── tlscacerts
│ └── tlsca.taobao.com-cert.pem
└── tls
├── ca.crt
├── client.crt
└── client.key
109 directories, 101 files
總結:在這個環節中,我們假設 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:
# 定義Peer組織如何與應用程序通道交互的策略
# 默認策略:所有Peer組織都將能夠讀取數據並將數據寫入賬本
Application: &ApplicationDefaults
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
執行 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
:
$ configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./config/JDAnchor.tx -channelID appchannel -asOrg JD
當然,這一步也是爲後續使用做準備的。不過至此,需要準備的配置都齊了。
來看看現在 config
文件夾都有什麼:
$ tree config
config
├── JDAnchor.tx
├── TaobaoAnchor.tx
├── appchannel.tx
└── genesis.block
0 directories, 4 files
6、創建並啓動各組織的節點
我們說過:我們假設 QQ
作爲一個運營方,提供了 1 個 Orderer 節點 orderer.qq.com
來創建聯盟鏈的基礎設施, 而 Taobao
和 JD
則是作爲組織成員加入到鏈中,各自提供 2 個 Peer 節點 peer0.xx.com
和 peer1.xx.com
參與工作。
現在這些組織及其節點所需要的配置已經準備好了。我們接下來就可以使用 Docker Compose 來模擬啓動這些節點服務。
由於這些節點之間需要互相通信,所以我們需要將這些節點都放入到一個 Docker 網絡中,以 fabric_network
爲例。
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
爲了方便,這裏我還定義了一個 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
注意觀察,在 volumes
配置項中,我們將 config
和 crypto-config
內的配置文件都掛載到相對應的節點中了。並且在 peer 的公共服務中,我們還掛載了 /var/run/docker.sock
文件,有了該文件,在容器內就可以向其發送 http 請求和 Docker Daemon 通信,通俗理解,就是有了它,就可以在容器內操作宿主機的 Docker 了,比如在容器內控制 Docker 再啓動一個容器出來。而這,就是爲了後面可以部署智能合約(節點部署鏈碼其實就是啓動一個鏈碼容器)。
現在繼續將這些節點服務啓動起來:
$ docker-compose up -d
Creating network "fabric_network" with the default driver
Creating volume "network_orderer.qq.com" with default driver
Creating volume "network_peer0.taobao.com" with default driver
Creating volume "network_peer1.taobao.com" with default driver
Creating volume "network_peer0.jd.com" with default driver
Creating volume "network_peer1.jd.com" with default driver
Creating orderer.qq.com ... done
Creating peer1.taobao.com ... done
Creating peer0.jd.com ... done
Creating peer1.jd.com ... done
Creating peer0.taobao.com ... done
Creating cli ... done
哦對了,除了必須的節點服務,我還啓動了一個 cli
服務,來自 hyperledger/fabric-tools
鏡像,這個其實就是集成了前面第 1 步提到的 fabric 工具的容器,我們接下來的命令執行就使用這個容器內的工具來完成了,你也可以繼續使用自己下載的二進制工具,只是個人覺得環境配置起來會比較麻煩。
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/Admin@taobao.com/msp"
$ TaobaoPeer1Cli="CORE_PEER_ADDRESS=peer1.taobao.com:7051 CORE_PEER_LOCALMSPID=TaobaoMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/taobao.com/users/Admin@taobao.com/msp"
$ JDPeer0Cli="CORE_PEER_ADDRESS=peer0.jd.com:7051 CORE_PEER_LOCALMSPID=JDMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/jd.com/users/Admin@jd.com/msp"
$ JDPeer1Cli="CORE_PEER_ADDRESS=peer1.jd.com:7051 CORE_PEER_LOCALMSPID=JDMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/jd.com/users/Admin@jd.com/msp"
8、開始創建通道
通道主要用於實現區塊鏈網絡中業務的隔離。一個聯盟中可以有多個通道,每個通道可代表一項業務,並且對應一套賬本。通道內的成員爲業務參與方(即聯盟內的組織),一個組織可以加入多個通道。
我們現在有請 Taobao
組織的 peer0
節點來創建一個通道 appchannel
:
$ docker exec cli bash -c "$TaobaoPeer0Cli peer channel create -o orderer.qq.com:7050 -c appchannel -f /etc/hyperledger/config/appchannel.tx"
通道就相當於 “羣聊”, 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"
這時相當於大家都加入到了 appchannel
“羣聊”中,之後大家都可以在裏面 “聊天” 了。
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"
這樣,Taobao
和 JD
組織間的節點就都可以互相發現了。
到這裏,我們的區塊鏈網絡基本已經搭建好了,但是還差最關鍵的智能合約。一個沒有智能合約的通道是沒有靈魂的,啥事都做不了。
編寫智能合約
fabric 的智能合約稱爲鏈碼,編寫智能合約也就是編寫鏈碼。
鏈碼其實很簡單,可以由 Go 、 node.js 、或者 Java 編寫,其實只是實現一些預定義的接口。
以 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
接口:
// Chaincode interface must be implemented by all chaincodes. The fabric runs
// the transactions by calling these functions as specified.
type Chaincode interface {
// Init is called during Instantiate transaction after the chaincode container
// has been established for the first time, allowing the chaincode to
// initialize its internal data
Init(stub ChaincodeStubInterface) pb.Response
// Invoke is called to update or query the ledger in a proposal transaction.
// Updated state variables are not committed to the ledger until the
// transaction is committed.
Invoke(stub ChaincodeStubInterface) pb.Response
}
然後在啓動入口 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
元。
部署鏈碼
我們將剛剛編寫的智能合約也就是鏈碼安裝到區塊鏈網絡中,同樣是藉助 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}'
dev-peer0.taobao.com-fabric-realty-1.0.0-4f127a0415dd835529133a69b480ce24581dd5ddcaf18426ecc1d3dfb02b4670
因爲我們使用 Taobao
組織的 peer0
節點實例化鏈碼,所以此時還只有這個節點的鏈碼容器啓動起來了。
我們可以試着使用 cli
服務去調用鏈碼:
$ docker exec cli bash -c "$TaobaoPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{\"Args\":[\"query\"]}'"
2022-03-22 21:13:40.152 UTC [chaincodeCmd] InitCmdFactory -> INFO 001 Retrieved channel (appchannel) orderer endpoint: orderer.qq.com:7050
2022-03-22 21:13:40.157 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 002 Chaincode invoke successful. result: status:200 payload:"[{\
"balance\":1000,\"name\":\"A\"},{\"balance\":1000,\"name\":\"B\"}]"
當然,使用JD
組織的節點也是可以的:
$ docker exec cli bash -c "$JDPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{\"Args\":[\"query\"]}'"
2022-03-22 21:14:45.397 UTC [chaincodeCmd] InitCmdFactory -> INFO 001 Retrieved channel (appchannel) orderer endpoint: orderer.qq.com:7050
2022-03-22 21:14:45.402 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 002 Chaincode invoke successful. result: status:200 payload:"[{\
"balance\":1000,\"name\":\"A\"},{\"balance\":1000,\"name\":\"B\"}]"
此時,因爲我們查詢了 JD
組織的 peer0
節點上的鏈碼,所以對應的鏈碼容器也會啓動起來了,再次查看啓動的鏈碼容器:
$ docker ps -a | awk '($2 ~ /dev-peer.*fabric-realty.*/) {print $2}'
dev-peer0.jd.com-fabric-realty-1.0.0-5c5e915cdcd47324151383f9619a0ff9a33283d969555e6029aa256cc389ebc9
dev-peer0.taobao.com-fabric-realty-1.0.0-4f127a0415dd835529133a69b480ce24581dd5ddcaf18426ecc1d3dfb02b4670
現在,我們的智能合約就成功部署到區塊鏈網絡的通道中了。
編寫應用程序
在部署鏈碼之後,我們是使用 cli
服務去調用的,但這種方式一般只是作爲驗證使用,更多情況下,應該是我們自己編寫應用程序集成 fabric 提供的 SDK
去調用。
Go 語言可以使用官方的 github.com/hyperledger/fabric-sdk-go
庫。
這個 SDK 使用起來也很簡單。
第一步調用其 New
方法創建一個 FabricSDK
實例,後續使用這個實例就可以調用操作合約的方法了。
// New 根據提供的一組選項初始化 SDK
// ConfigOptions 提供應用程序配置
func New(configProvider core.ConfigProvider, opts ...Option) (*FabricSDK, error) {
pkgSuite := defPkgSuite{}
return fromPkgSuite(configProvider, &pkgSuite, opts...)
}
其中 configProvider
可以從 Reader
(實現了io.Reader
接口的實例) 、 File
(文件) 或 Raw
([]byte
) 獲取。我們選擇最簡單的文件方式。
創建一個 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
:
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 服務:
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")
}
在 main
函數中,我們創建了兩個接口 GET /query
和 POST /transfer
,其中 /query
接口調用鏈碼的 query
函數功能實現查詢用戶餘額,/transfer
接口調用鏈碼的 transfer
函數功能實現轉賬功能。
我們將繼續使用 Docker 部署該應用程序,這樣的好處是可以和區塊鏈網絡處於同一網絡下,方便調用節點,當然你也可以更改 config.yaml
文件去調用暴露在宿主機的節點端口也是可以的,首先編寫 Dockerfile
文件:
FROM golang:1.14 AS app
ENV GO111MODULE=on
ENV GOPROXY https://goproxy.cn,direct
WORKDIR /root/togettoyou
COPY . .
RUN CGO_ENABLED=0 go build -v -o "app" .
FROM scratch
WORKDIR /root/togettoyou/
COPY --from=app /root/togettoyou/app ./
COPY --from=app /root/togettoyou/config.yaml ./
ENTRYPOINT ["./app"]
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
$ docker-compose up
調用應用程序的接口:
$ curl "http://localhost:8000/query"
[{"balance":1000,"name":"A"},{"balance":1000,"name":"B"}]
$ curl "http://localhost:8000/query?user=A"
[{"balance":1000,"name":"A"}]
$ curl "http://localhost:8000/query?user=B"
[{"balance":1000,"name":"B"}]
$ curl -X POST "http://localhost:8000/transfer?from=A&to=B&money=500"
{"msg":"ok"}
$ curl "http://localhost:8000/query"
[{"balance":500,"name":"A"},{"balance":1500,"name":"B"}]
到這裏,我們就已經完整地實現了一個區塊鏈應用了。你也可以繼續爲這個區塊鏈應用實現前端頁面。流程呢,和傳統前後端分離架構也沒什麼區別。
最後
關於對 fabric 的瞭解程度,我已經儘可能地毫無保留了,但是對於真正想要進入區塊鏈這一領域的讀者來講,fabric 技術只是區塊鏈中的冰山一角,更多的還需要你們自己去探索。
而爲什麼我沒有選擇繼續往區塊鏈這一領域發展,理由很簡單,因爲個人比較喜歡雲原生方向。
本篇完。關注我,下次見。
參考資料
[1]
官方文檔: https://hyperledger-fabric.readthedocs.io/zh_CN/release-2.2/
[2]
項目地址: https://github.com/togettoyou/fabric-realty
[3]
fabric v1.4.12 二進制工具: https://github.com/hyperledger/fabric/releases/tag/v1.4.12
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yDmGwfRjXxDJfgv1d0p3Ig