Fabric進階(三)—— 使用SDK動態增加組織

fabric網路運行過程中動態追加新的組織是相當複雜的,網上的資料也十分匱乏,大多是基於first-network這樣的簡單示例,而且是使用啟動cli容器的方法來增加組織,幾乎沒有針對實際應用的解決方案。本文介紹了如何在應用程式中調用SDK來進行組織的動態增加。

前言

首先需要介紹一個配置區塊的概念,fabric中的配置資訊是作為區塊寫在鏈上的,每個配置區塊中只有一條配置交易,而且配置區塊是全量更新的,最新的配置區塊中應包含全部的配置資訊。

回憶一下在創建通道時,會從本地讀取通道配置交易(根據configtx.yaml生成),這個配置交易中指定了該通道中有哪些組織,以及設置了各組織的證書資訊。如果想要在後續進行添加,就必須要讓當前通道認可這個新組織,則需要提交一個包含新加組織的配置區塊來對當前配置進行更新。

大致思路是首先從節點中獲取到當前通道的最新配置區塊,利用configtxlator工具將配置資訊由protobuf格式轉化為可讀的json格式,手動在配置中添加上新組織的配置,然後再使用該工具計算修改前後的差值,將這個增量作為通道更新的請求發送出去。同時,這個通道更新的請求需要超過半數的當前組織簽名才算有效。

調用SDK增加組織

因為是在fabric實際應用中增加組織,所以通過在app中編寫程式碼調用SDK來完成所有操作是最優的方案。而且一旦實現,在之後的應用開發中可以很方便地復用,再配合上一些自動化腳本可以使繁雜的操作變得簡單化,做到輕鬆的增加或刪除網路內的組織。

值得一提的是,官方的node-sdk中提供了一段關於通道更新的例子configtxlator.js,不過裡面實現的是刪除某個組織,我們可以做一些改動來實現添加組織。

本文以balance-transfer v1.0為例,介紹如何通過調用Node SDK的方法,在已有兩個組織的基礎上增加新組織Org3,其中包含1個CA節點,2個Peer節點。

一、生成新組織證書目錄

因為進入fabric網路是需要身份的,所以不論是加入新節點還是加入新組織,都要為新增的成員生成MSP目錄。在artifacts/channel目錄下創建新組織的配置文件org3-crypto.yaml

PeerOrgs:
  - Name:Org3
    Domain: org3.example.com
    CA:
      Hostname: ca 
    Template:
      Count: 2
      SANS:
        - "localhost"
    Users:
      Count: 1

接著利用cryptogen工具生成Org3的msp目錄,並輸出到crypto-config目錄中:

./cryptogen generate --config=./crypto-org2.yaml --output ./crypto-config

二、編寫Nodejs程式碼調用SDK

我在app目錄下創建了一個單獨的文件add-org.js來完成添加組織,下面只提供程式的主要思路,細節可參考詳細程式碼

1.安裝所需Node模組
由於要在Nodejs程式中發送REST請求給configtxlator工具,所以需要事先安裝模組(類似於curl):superagentsuperagent-promiserequest,其中request建議使用v1.9.8版本。導入模組:

var requester = require('request');
var agent = require('superagent-promise')(require('superagent'), Promise);

2.獲取當前配置區塊
調用getChannelConfig介面獲取到最新的配置資訊,接收到的結果是ConfigEvelope類型的對象:

var config_envelope = await channel.getChannelConfig()

我們只需要用到其中的config部分,取出後將其轉化為二進位,注意original_config_proto是原始的配置資訊,會在後面計算差值時用到。

var original_config_proto = config_envelope.config.toBuffer();

3.利用工具轉化為json格式
使用configtxlator工具進行protobuf和json之間的轉換,利用superagent-promise發出請求:

var response = await 
  agent.post('//127.0.0.1:7059/protolator/decode/common.Config',original_config_proto).buffer();

對響應結果進行處理:

var original_config_json = response.text.toString()     // json string
var updated_config = JSON.parse(updated_config_json)    // json object

4.手動增加新組織的資訊
我們需要仿照已有的兩個組織的配置結構添加上新組織的資訊,首先複製Org1MSP部分的內容,注意這裡通過先stringify再parse的方式完成一次深拷貝。

var new_config = JSON.parse(JSON.stringify(updated_config.channel_group.groups.Application.groups["Org1MSP"]));

接下來就是在new_config中做相關修改,主要包括兩部分,一是所有跟組織名稱有關的地方,都需要將Org1替換為Org3;二是將相關證書的值替換成Org3的MSP目錄中的實際證書的內容(從文件中讀取後還需要進行base64編碼),三種證書的路徑如下(當前位於app目錄下,這裡使用相對路徑):

// 1.admins:組織管理員證書
'../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/msp/admincerts/[email protected]'
// 2.root_certs:根CA證書
'../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/msp/admincerts/ca.org3.example.com-cert.pem'
// 3.tls_root_certs:tls根證書
'../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/msp/admincerts/tlsca.org3.example.com-cert.pem'

需要修改的具體位置這裡就不方便一一展開了,細節還是參考下程式碼。在編寫js程式碼的時候可以將配置資訊的json對象列印出來,對比下已有組織的配置內容,就可以很直觀的找到那些需要替換的地方了。

完成編輯之後,將新組織的配置new_org當前到原有配置update_config上:

updated_config.channel_group.groups.Application.groups["Org3MSP"] = new_config;

並轉化為json字元串:

updated_config_json = JSON.stringify(updated_config);

5.利用工具將json格式轉為pb格式

response = await 
  agent.post('//127.0.0.1:7059/protolator/encode/common.Config', updated_config_json.toString()).buffer();
var updated_config_proto = response.body;     // 響應結果:pb格式

6.利用工具計算差值
通道更新請求需要的參數並不是新的配置資訊,而是新配置與原始配置的一個差值,需要再次利用configtxlator工具計算這個增量。
首先構造向工具發送的請求體的結構,需要附帶我們原始獲取的配置original_config_proto以及修改過後的配置updated_config_proto,兩者都是pb格式:

var formData = {
    channel: channel_name,
    original: {
        value: original_config_proto,
        options: {
            filename: 'original.proto',
            contentType: 'application/octet-stream'
        }
    },
    updated: {
        value: updated_config_proto,
        options: {
            filename: 'updated.proto',
            contentType: 'application/octet-stream'
        }
    }
};

通過request模組發送post請求:

requester.post({
        url:'//127.0.0.1:7059/configtxlator/compute/update-from-configs',
        encoding: null,
        headers: {
            accept: '/',
            expect: '100-continue'
        },
        formData: formData
    }

計算的結果轉化為二進位以後賦值給變數config_proto,這就是通道配置的更新增量,下面會作為通道更新請求的重要參數。

7.對配置更新增量進行簽名
更新通道的請求需要超過半數的已有組織的管理員身份簽名,現有兩個組織,則需要兩個簽名。調用help.js里的getOrgAdmin()方法可以給client對象分配管理員用戶對象,然後調用SDK中的signChannelConfig()對配置進行簽名:

var signatures = []
for (let org of cur_orgs) {
    let client = helper.getClientForOrg(org)
    await helper.getOrgAdmin(org)     // 給client分配管理員對象
    let signature = client.signChannelConfig(config_proto);
    signatures.push(signature)
}

其中cur_orgs參數是除Orderer外所有組織名的集合,這裡用了一個循環讓所有組織管理員對配置簽名。

8.發送更新通道的請求
首先構造請求體:

let tx_id = client.newTransactionID();
var request = {
    config: config_proto,        // 配置更新增量
    signatures: signatures,      // 組織管理員簽名
    name: channel_name,
    orderer: channel.getOrderers()[0],
    txId: tx_id
};

調用SDK的updateChannel()介面對通道進行更新,該方法在內部會將新的配置交易發送到orderer節點,打包成配置區塊後分發給當前所有peer節點,peer節點將新的配置區塊存入鏈中,此時該通道就接受認可了新加入的組織。

var result = await client.updateChannel(request);

三、執行程式碼加入新組織

Nodejs程式碼編寫完成後整個工作就成功了一大半,接下來需要執行該程式,將Org3加入到當前網路。

首先啟動configtxlator服務,默認監聽7059埠:

configtxlator start

然後運行我們的Nodejs程式:

node add_org.js

成功響應後說明新組織加入成功,此時鏈上會生成一個新的配置區塊。

四、更新配置文件

1.創建CA伺服器配置文件
新加的組織Org3也擁有一個屬於自己的CA節點,在之前的修改組織名的文章中已經介紹了如何設置CA伺服器配置文件fabric-ca-server-config.yaml(主要是affiliations部分需要修改),以及如何在docker-compose文件中將該文件映射到CA容器內部。我的Github中也保存了該配置文件的模板

2.編寫容器配置文件啟動新組織節點
現在啟動Org3中的節點,首先需要編寫docker-compose文件。這一步比較簡單,只要模仿已有組織的docker-compose.yaml文件即可。

Org3包含一個CA節點,兩個Peer節點。編寫該配置文件需要注意:如果所有組織都在一個機器上,則要保證容器的埠不會衝突。而且CA容器中的CA_KEYFILETLS_KEYFILE兩個參數要和實際新組織的msp目錄中的私鑰文件路徑一致。最後不要忘記添加CA伺服器配置文件的映射。

將已完成Org3的配置文件docker-compose-org3.yaml置於artifacts目錄下,執行以下命令啟動三個節點:

docker-compose -f docker-compose-org3.yaml up -d

3.修改網路配置文件network-config.json
該文件路徑為app/network-config.json,主要設置了網路各組織節點的ip和port資訊,用於應用程式與網路節點進行交互。

需要仿照已有的組織,添加上新加入組織的資訊,Org3部分大致如下:

"Org3": {
    "name": "peerOrg3",
    "mspid": "Org3MSP",
    "ca": "//localhost:7054",
    "peers": {
        "peer1": {
            "requests": "grpcs://localhost:9051",
            "events": "grpcs://localhost:9053",
            "server-hostname": "peer0.org3.example.com",
            "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/peers/peer0.org3.example.com/tls/ca.crt"
        },
        "peer2": {
            "requests": "grpcs://localhost:9056",
            "events": "grpcs://localhost:9058",
            "server-hostname": "peer1.org3.example.com",
            "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/peers/peer1.org3.example.com/tls/ca.crt"
        }
    },
    "admin": {
        "key": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/users/[email protected]/msp/keystore",
        "cert": "../artifacts/channel/crypto-config/peerOrganizations/org3.example.com/users/[email protected]/msp/signcerts"
    }
}

五、將新組織中的節點加入通道

新組織的節點容器已經啟動,首先需要在Org3註冊某個用戶,拿到Org3的TOKEN,這裡設為ORG3_TOKEN,然後發送請求把Org3中的兩個節點加入到通道中:

curl -s -X POST \
  //localhost:4000/channels/mychannel/peers \
  -H "authorization: Bearer $ORG3_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "peers": ["peer1","peer2"]
}'

六、升級鏈碼

目前新組織節點沒有安裝鏈碼,只能參與記賬,無法指定其完成查詢或交易操作。但是即使安裝了舊版本的鏈碼,會發現節點可以查詢,但是進行的交易確是無效的。

這是因為在chaincode實例化的時候會指定背書策略,默認是channel其中一個組織的某一個成員進行背書,但是該背書策略中沒有包含後續新加入的組織,所以在驗證階段會被標記成invalid,能一直產生區塊,卻不會寫入狀態資料庫。

所以如果需要新加組織的節點來執行交易,則需要對鏈碼進行升級,不改變鏈碼內容,只改變版本和背書策略,為的就是在背書策略中加入新組織。

利用SDK來upgrade chaincode也並非易事,需要自行編寫js程式碼來實現。升級鏈碼和實例化鏈碼很相似,都需要生成一個交易。SDK中提供了sendUpgradeProposal()方法來發送升級鏈碼的提案,我們可以參考balance-transfer中的instantiateChaincode.js(鏈碼實例化)程式碼來編寫升級鏈碼的程式碼,詳細介面程式碼可見github

首先需要設置新的背書策略,該背書策略表示只要3個組織中的其中一個組織的任意一個節點對某個交易背書,該交易就滿足策略。

var endorsement_policy = 
{
  identities: [
    { role: { name: "member", mspId: "Org1MSP" }},
    { role: { name: "member", mspId: "Org2MSP" }},
    { role: { name: "member", mspId: "Org3MSP" }}
  ],
  policy: {
    "1-of": [{ "signed-by": 0 }, { "signed-by": 1 }]
  }
}

接下來構造升級鏈碼的請求:

var request = {
  "chaincodeId": "mycc",
  "chaincodeVersion": "v1",
  "args": [''],
  "txId": client.newTransactionID(),
  "endorsement-policy": endorsement_policy
};

然後發送提案到背書節點:

var results = channel.sendUpgradeProposal(request);

最後和交易流程一樣,需要根據提案響應生成交易,發送到排序服務節點:

var sendPromise = channel.sendTransaction(txRequest);

成功後通道會接受新版本的鏈碼,在Org3安裝新鏈碼後可以指定其節點進行有效的查詢和交易操作。至此,添加新組織成功!

實際應用開發中的實現

應用開發中應該優先選擇上述利用js腳本增加組織的方法。當然也可以使用cli容器的方法,最好要寫一個腳本,自動啟動cli容器,完成上述所有操作以後再刪除cli容器,不過相比調用SDK還是有諸多不便。

我在實際開發中是將添加或刪除組織升級鏈碼這兩個功能加入了應用程式程式碼中,寫成了RESTful介面,客戶端可以通過http請求來完成這兩個操作。

並且還寫了一個shell腳本,來自動化執行一些操作,包括生成證書,啟動configtxlator工具,發送更改組織的請求,關閉工具等。如果進一步完善,甚至可以將後續修改配置文件等操作也加入腳本中,達到一鍵執行就能夠完成增加或者刪除組織的效果。

上述程式碼可以在我的Github中找到://github.com/zhayujie/fabric-tools