萬字長文,教你用 go 開發區塊鏈應用

大概 2 年前,碰巧學習區塊鏈(Hyperledger Fabric),便寫了一個入門級的項目放在 GitHub 上,公衆號有不少讀者是通過這個項目關注到我的,也經常問我,有沒有區塊鏈這方面的學習資料,有沒有這個項目的詳細講解,如何搭建一個區塊鏈網絡,林林總總。

對於這些問題,我每次的回覆都一樣,學習資料我倒是沒有,但是 官方文檔 [1] 就是最好的資料了。

不過今天,我想還是通過這篇文章來記錄一下我對之前區塊鏈學習的一次總結吧。

對了,這個項目的地址是:https://github.com/togettoyou/fabric-realty[2] ,有幫助的話點個 star

預警:爲了照顧到更多讀者,本篇儘量從新手的視角出發,可能會有很多特別基礎的內容,對於已經懂的部分,選擇跳過即可。

再次預警:文章內容有點長,請耐心看,最好跟着一起動手實踐,如果中途發現了錯誤之處,歡迎告知我。

技術棧

首先,以下這些我提到的技術要求你事先稍微學習掌握一下:

1、yaml 文件的編寫

需要注意一下幾個規則:

2、Docker 和 Docker Compose

後續區塊鏈的節點以及應用程序的部署我們都會使用 Docker Compose 來管理。

3、 go 語言

我的項目包括本篇文章的示例都是使用 go 語言開發的,雖然 fabric 也提供了 Java,nodejs,python 等語言的 SDK ,但個人還是比較推薦 go 語言,畢竟 fabric 自身也是 go 實現的。

題外話:以上這些技能除了在 fabric 區塊鏈體系中需掌握,在如今火熱的雲原生技術下也一樣是基礎。

區塊鏈基礎知識

1、什麼是區塊

Block ,每個區塊記錄着上一個區塊的 hash 值、本區塊中的交易集合、本區塊的 hash 等基礎數據。由於每個區塊都有上一區塊的 hash 值,區塊間由這個值兩兩串聯,形成了區塊鏈。

2、什麼是區塊鏈

Blockchain ,最早起源於比特幣的底層技術,並在其後不斷演進發展。

區塊鏈本質上就是一個多方共享的分佈式賬本技術,用來記錄網絡上發生的所有交易。

而其中去中心化的概念,是因爲賬本信息會被複制到許多網絡參與者中,每個參與者都在協作維護賬本,不像傳統應用的數據被中心管理着。

另外信息只能以附加的方式記錄到賬本上,並使用加密技術保證一旦將交易添加到賬本就無法修改。這種不可修改的屬性簡化了信息的溯源,因爲參與者可以確定信息在記錄後沒有改變過。所以區塊鏈有時也被稱爲證明系統

3、什麼是公鏈、聯盟鏈和私鏈

區塊鏈分爲公有鏈、聯盟鏈、私有鏈三種基本類型。其中:

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 網絡的賬本的副本。

總結:Hyperledger Fabric 是一種賬本技術,其賬本包括世界狀態數據庫和交易日誌歷史記錄。

2、什麼是聯盟

聯盟指參與一個基於區塊鏈的業務協作或業務交易網絡的所有組織的集合,一個聯盟一般包含多個組織。

一般由聯盟發起方或運營方創建 Orderer 排序節點,並負責交易排序、區塊產生和達成共識。聯盟發起方或運營方邀請各個組織實例加入聯盟,進而創建通道。

3、什麼是組織

組織代表的是參與區塊鏈網絡的企業、政府機構、團體等實體。

一個組織實例主要包含如下節點:

4、什麼是節點

節點(Peers)是區塊鏈的通信實體。它只是一個邏輯功能,只要能在 “信任域” 中分組並與控制它們的邏輯實體相關聯,就可以將不同類型的多個節點運行在同一個物理服務器上,比如用 Docker 部署。

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]

自行根據你的系統環境下載對應的包。

其中幾個主要的工具說明:

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 來創建聯盟鏈的基礎設施, 而 TaobaoJD 則是作爲組織成員加入到鏈中,各自提供 2 個 Peer 節點 peer0.xx.compeer1.xx.com 參與工作,以及還各自創建了 2 個組織用戶 AdminUser1 。然後我們使用 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 來創建聯盟鏈的基礎設施, 而 TaobaoJD 則是作爲組織成員加入到鏈中,各自提供 2 個 Peer 節點 peer0.xx.compeer1.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 配置項中,我們將 configcrypto-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 命令,所以要爲其先配置一下環境變量,使用四個不同的變量 TaobaoPeer0CliTaobaoPeer1CliJDPeer0CliJDPeer1Cli ,代表 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"

這樣,TaobaoJD 組織間的節點就都可以互相發現了。

到這裏,我們的區塊鏈網絡基本已經搭建好了,但是還差最關鍵的智能合約。一個沒有智能合約的通道是沒有靈魂的,啥事都做不了。

編寫智能合約

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)) 就完成了鏈碼的啓動,沒錯,就是這麼簡單。

我們知道鏈碼其實就是用來處理區塊鏈網絡中的成員一致同意的業務邏輯。比如 TaobaoJD 規定了一個規則,將其編寫成鏈碼,後面雙方就只能遵循這個規則了,因爲鏈碼到時候即部署在你的節點,也會部署在我的節點上,你偷偷改了邏輯,我的節點不會認可你的,這也正是區塊鏈的作用之一。

鏈碼的功能定義在 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
}

在這段鏈碼中,初始化的時候我們假設有用戶 AB ,並且都各自有 1000 元餘額,我們在 Invoke 方法中爲其定義了兩個功能函數 querytransfer 。 其中 query 函數可以查詢 AB 或指定用戶的餘額信息, transfer 函數可以通過傳入轉賬人,被轉賬人,金額,三個參數來實現轉賬功能。例如 {"Args":["transfer","A","B","100.0"]} 代表 AB 轉賬 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') 代表鏈碼的寫入操作需要同時得到 TaobaoJD 組織成員的背書才允許通過。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.compeer0.taobao.com 節點進行背書,這是因爲我們在實例化鏈碼的時候指定了背書策略爲 AND ('TaobaoMSP.member','JDMSP.member') ,代表交易需要同時得到 TaobaoJD 組織成員的背書才允許通過。每次寫入賬本時,會驗證這兩個節點的數據一致性,只有當這兩個節點的數據一致時,交易纔算最終成功。

繼續編寫 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 /queryPOST /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