借助 Hyperledger Fabric Client SDK for Node.js,您可以輕松地使用 API 來與基於 Hyperledger Fabric v0.6 的區塊鏈交互。本教程將介紹如何編寫客戶端應用程序中的一些最常見和必要的功能。教程中的所有代碼示例都包含在可重用的樣本客戶端中,您可以下載並自定義它來滿足您的需求。
本系列分三部分,本教程是最后一部分,您將學習如何開發一個 Node.js 客戶端應用程序,用它來與基於 Hyperledger Fabric v0.6 的區塊鏈網絡通信。您將了解注冊、登記和通過 TCert 執行訪問控制,並獲取設置區塊鏈網絡、基於 Cloudant 的鍵值存儲,以及用於調用和查詢區塊鏈的 API 層的代碼。按照本教程中的步驟,可以將第 1 部分中開發的鏈代碼部署到 IBM Bluemix® 上的 Blockchain 服務上,並從客戶端應用程序調用它。
前提條件
我們將繼續介紹本教程系列前面兩部分中介紹的住房貸款申請用例。
- 本教程將引用第 1 部分中編寫的鏈代碼,所以在繼續之前請復習該教程。
- 您應該熟悉 Node.js 和 Bluebird promise。
- 應該已經在機器上安裝了 Node v4.5.0。
- 應該已經在機器上安裝了 NPM v2.15.9。
- 應該已經在機器上安裝了 Git v2.7.4。
- 您需要一個 Bluemix 帳戶(獲取一個免費試用賬戶)。
- 您需要樣本代碼包(從本教程下載它)。
重要術語和概念
Hyperledger Fabric Client SDK 是 Hyperledger Fabric v0.6 的官方 SDK。它是用 TypeScript 編寫的,擁有豐富的 API,可通過 Node.js 應用程序與基於 Hyperledger Fabric 的區塊鏈網絡進行交互。該 SDK 使用 gRPC 與區塊鏈網絡通信。
- 鏈。鏈是讓程序運轉起來的頂級對象。鏈對象用於配置對等節點、成員服務、安全性、鍵值存儲和事件中心。鏈對象能訪問可用的成員對象,而成員對象用於在區塊鏈網絡上執行事務。
- 成員。成員對象表示能在區塊鏈網絡上執行事務的實體。成員對象用於向成員服務注冊和登記。完成注冊和登記后,成員對象將收到的證書存儲在以前配置的鍵值存儲中。然后可以使用成員對象與區塊鏈網絡上的對等節點進行通信,以便調用事務和查詢賬本。
- 鍵值存儲。鍵值存儲用於存儲成員元數據和登記證書。該 SDK 為鍵值存儲提供了一種基於文件的默認實現,可用在客戶端應用程序中。但是,建議您自行為鍵值存儲編寫更安全、更容易訪問的實現。我將帶領您在我們的客戶端中實現一個基於 Cloudant NoSQL 數據庫的鍵值存儲。
- 注冊、登記和訪問控制。要與基於 Hyperledger Fabric 的區塊鏈網絡交互,用戶需要向證書頒發機構 (CA) 注冊和登記。CA 生成事務證書,需要這些證書才能在區塊鏈網絡上執行事務,它們也可用於實現基於屬性的訪問控制。CA 提供了幾種不同的證書服務:
- 登記證書頒發機構 (ECA)。ECA 使新用戶能向區塊鏈網絡注冊,並在以后請求一個登記證書對。這個證書對包含兩個證書,一個用於數據簽名,另一個用於數據加密。
- 事務證書頒發機構 (TCA)。用戶在區塊鏈網絡中登記后,可以請求事務證書 (TCert)。要在區塊鏈網絡上部署鏈代碼和調用鏈代碼事務,需要使用 TCert。也可以在 TCert 中嵌入經過加密的用戶定義屬性。然后可以在鏈代碼中訪問這些屬性來執行訪問控制。一種確保隱私的最佳做法是為每個事務請求一個不同的 TCert。這是該 SDK 的默認行為。
- TLS 證書頒發機構。TLS 證書保護客戶端、對等節點和 CA 之間的通信渠道。TLS 證書可從 TLS 證書頒發機構 (TCA) 請求獲得。
- 屬性證書頒發機構 (ACA)。ACA 將每個用戶的屬性和從屬關系存儲在一個數據庫中。它證明某個特定用戶是否擁有這些屬性。對於經過認證的屬性,ACA 會返回一個 ACert,其中包含加密的屬性值。
此圖展示了用戶、登記證書頒發機構 (ECA)、事務證書頒發機構 (TCA) 和屬性證書頒發機構 (ACA) 之間的交互流。
在上圖中:
- 用戶向 ECA 發出注冊請求並傳入一個登記 ID、角色、從屬關系和屬性鍵值對等信息。
- 如果傳入的登記 ID 尚未注冊,那么 ECA 會向用戶回復一個一次性密碼。
- 用戶向 ECA 發出登記請求,並傳入一個簽名和加密密鑰,以及在上一步中獲得的一次性密碼令牌。
- 完成驗證后,ECA 會返回一個登記證書對。這個證書對包含兩個證書,一個用於簽名,另一個用於加密。然后,這個證書對連同其他用戶元數據一起被存儲在鏈對象中配置的鍵值存儲中。
- ECA 向 ACA 發出一個獲取屬性請求,並傳入第 4 步中生成的 ECert。
- ACA 將 ECert 連同其他細節一起存儲在它的數據庫中,這些細節包括從屬關系、屬性鍵/值對和它們的有效性等。
- 用戶向 TCA 請求 TCert,TCert 可用於調用鏈代碼。用戶傳入 ECert、要生成的 TCert 數量和一組屬性,這些屬性可以是第 1 步中傳入的一組屬性的子集。
- TCA 向 ACA 發出請求,以確保用戶擁有這些屬性。TCA 傳入 ECert 和這組屬性。
- ACA 在其數據庫中查找這些屬性。
- 如果未找到任何屬性,則返回一個錯誤。如果找到所有或部分屬性,則將一個包含已找到的屬性的 ACert 返回給 TCA。
- TCA 創建一批 TCert 並將它們返回給用戶。每個 TCert 都包含有效的屬性。
開始實踐
本教程中和可供下載的代碼樣本是使用以下軟件和版本來開發和測試的:
- Node v4.5.0
- NPM v2.15.9
- Git v2.7.4
- Hyperledger Fabric Client v0.6.5
- Bluemix 上的 Blockchain 服務
- Bluemix 上的 Cloudant NoSQL DB 服務
在 Bluemix 上創建 IBM Blockchain 服務
要開發本教程中的樣本客戶端,您需要一個 Bluemix 帳戶(獲取一個 Bluemix 免費試用賬戶)。
- 擁有 Bluemix 帳戶后,在 Bluemix 上創建 Blockchain 服務。一定要選擇 Starter Developer plan (beta),這會為您設置一個基於 Hyperledger Fabric v0.6 的區塊鏈網絡。
- 創建服務后,導航到您的 Blockchain 服務主頁,單擊導航欄中的 Service credentials:
- 單擊 ACTIONS 下的 View credentials:
您現在可以看到您的服務的憑證。復制文本框中的全部內容,將它們存儲在一個安全的位置。這些憑證包含客戶端應用程序與區塊鏈網絡通信所需的信息。教程后面的章節將介紹如何將這些服務憑證放在樣本客戶端的配置文件中。
在 Bluemix 上創建 Cloudant NoSQL DB 服務
樣本客戶端使用了一個基於 Cloudant 的鍵值存儲的自定義實現。Cloudant 服務是一個由 couch DB 提供助力的 NoSQL DB 服務。所以,與 Blockchain 服務一樣,您將使用您的 Bluemix 帳戶(獲取 Bluemix 免費試用帳戶)。
- 擁有 Bluemix 帳戶后,讓我們來創建 Cloudant 服務。選擇 Lite (free) plan。
- 創建服務后,導航到您的 Cloudant 服務主頁,單擊導航欄中的 Service credentials。
- 單擊 ACTIONS 下的 View credentials。
您現在可以看到您的服務的憑證。復制文本框中的全部內容,將它們保存到一個安全位置供以后使用。
下載樣本代碼包
下載並解壓樣本代碼包(從本教程下載它)。在終端中導航到解壓的 code_package 目錄的根路徑,運行 npm install
。此命令將下載運行樣本客戶端所需的所有模塊。完成上述操作后,您會在 code_package 根路徑中看到一個 node_modules 文件夾。
部署樣本鏈代碼
運行客戶端之前,您需要將 code_package/chaincode/ 下的 chaincode_sample.go 部署到早先創建的 IBM Blockchain 服務中。
- 導航到 IBM Blockchain 服務儀表板,單擊 APIs:
- 在 APIs 部分,單擊 Network’s Enroll IDs。這部分列出了已向區塊鏈網絡注冊的用戶,這些用戶只需要登記即可部署和調用鏈代碼。
選擇 user_type1_0 用戶。記下此用戶的 ID 和密碼。
備注:不要登記 admin 和 WebAppAdmin 用戶。
它們是有權注冊其他用戶的特殊用戶。我們的 Node.js 客戶端將負責登記和利用這些用戶。 - 導航到 /registrar 選項卡,在有效負載中填入登記 ID 和登記密碼:
1234
{
"enrollId": "user_type1_0",
"enrollSecret": "5c9db3deb6"
}
單擊 Try It Out 按鈕,以便對區塊鏈網絡中選定的對等節點執行 REST 調用。您將獲得一個響應,表明該用戶已成功登錄。現在可以使用 user_type1_0 部署鏈代碼了。
- 導航到 /chaincode 選項卡。
單擊 DeploySpec 部分旁邊的文本區域。將文本區域的內容替換為以下內容:123456789101112131415161718{
"jsonrpc": "2.0",
"method": "deploy",
"params": {
"type": 1,
"chaincodeID": {
"path": "https://github.com/varojha1/Hyperledger-Fabric-v6-tutorial/chaincode"
},
"ctorMsg": {
"function": "init",
"args": [
]
},
"secureContext": "user_type1_0"
},
"id": 0
}
chaincodeID
是 GitHub 存儲庫中的 chaincode_sample.go 文件的路徑。對於本教程,我已創建了存儲庫,並上傳了客戶端代碼和鏈代碼供您使用。 - 單擊 Try It Out 按鈕執行部署請求。您會獲得類似這樣的響應:
12345678
{
"jsonrpc": "2.0",
"result": {
"status": "OK",
"message": "36ebb862f73a0e18701338fe2b8de99e31202ff8649c8b9909fa0c0fada22b127a997e5f8bf074f35a36a60e765a2919a8a405ee29f"
},
"id": 1
}
復制message
鍵的內容並保存。它表示剛部署的鏈代碼的鏈代碼 ID。 - 在編輯器中打開 code_package/config/runtime.json 文件:
1234567891011121314151617181920212223242526272829303132333435363738394041
{
"env": "local",
"processname": "BlockchainSampleClient",
"request": {
"size": "16mb"
},
"chaincode": {
"id": "",
"name": "cp_chaincode"
},
"databases": {
"devWorksAppMaster": "devworks_app_master"
},
"VCAP_SERVICES": {
"cloudantNoSQLDB": [{
"name": "Cloudant NoSQL DB-vern",
"label": "cloudantNoSQLDB",
"plan": "Lite",
"credentials": {
}
}],
"ibm-blockchain-5-prod": [{
"name": "Blockchain-v6-devWorks",
"label": "ibm-blockchain-5-prod",
"plan": "ibm-blockchain-plan-5-prod",
"credentials": {
}
}]
},
"log4js": {
"replaceConsole": true,
"appenders": [{
"type": "console"
}, {
在
chaincode
對象下,將id
的值替換為您在第 4-5 步中保存的鏈代碼 ID。將
ibm-blockchain-5-prod
對象下的credentials
對象的內容替換為您在第 1-3 步中從 Bluemix 上的 Blockchain 服務獲取的服務憑證。這些服務憑證有一個證書鏈接,可下載用於客戶端與 Blockchain 服務之間的 TLS 通信的證書:
"cert": https://blockchain-certs.mybluemix.net/us.blockchain.ibm.com.cert盡管我已將該證書包含在 code_package 中,但建議您將該證書替換為上述鏈接中找到的證書。將該證書放在 code_package 的根文件夾中。
類似地,如果您想使用基於 Cloudant 的鍵值存儲實現來運行客戶端,可將
cloudantNoSQLDB
對象下的credentials
對象的內容替換為您在第 2-3 步中從 Bluemix 上的 Cloudant 服務獲取的服務憑證
。
代碼包結構
我們快速看看 code_package 的結構,然后再繼續介紹第 7 步。
- blockchain_sample_client.js 包含客戶端代碼,該代碼將注冊和登記用戶,並創建和從區塊鏈獲取貸款申請。
- chaincode 包含 chaincode_sample.go 文件,該文件將被部署到區塊鏈供以后調用。
- config 文件夾包含 runtime.json 文件,您之前已更新了該文件。它包含客戶端應用程序的所有配置信息。
- src 文件夾包含 blockchain_sample_client 依賴的所有源代碼。
- src/blockchain/blockchain_network.js 文件包含所有設置和引導代碼,用於設置和配置客戶端來與區塊鏈網絡通信。
- src/blockchain/blockchain_sdk.js 文件包含的 Hyperledger Fabric Client 代碼用於與區塊鏈網絡交互,以便執行注冊、登記、調用和查詢。
- src/database/datastore.js 包含與 Bluemix 上的 Cloudant 服務通信所需的代碼。
- src/database/model/kv_store_model.js 是一個基於 Cloudant 的鍵值存儲實現, Hyperledger Fabric Client 客戶端將使用它管理 ECert 和用戶元數據。
- 在終端中導航到 code_package/ 並運行:
node blockchain_sample_client.js
這是輸出的一個片段:
樣本客戶端能成功地在區塊鏈上創建一個抵押貸款申請,隨后從區塊鏈賬本獲得它。
區塊鏈樣本客戶端
現在詳細分析客戶端代碼,以了解它的工作原理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
function runClient(){
logHelper.initialize(config.processname, config.env, config.log4js);
logger = logHelper.getLogger('blockchain_sample_client.js');
var user = 'vojha25';
var affiliation = 'Bank_Home_Loan_Admin';
var args = process.argv.slice(2);
if(args.length >=1){
var input = JSON.parse(args);
if(validate.isValidString(input['user'])){
user = input['user'];
}
if(validate.isValidString(input['affiliation'])){
affiliation = input['affiliation'];
}
}
setup()
.then(function(resp){
return bcSdk.recursiveRegister({username: user, affiliation: affiliation})
})
.then(function(resp){
return bcSdk.recursiveLogin({username: user, password: resp['body']['password'] })
})
.then(function(resp){
var id = Math.floor(Math.random() * (100000 - 1)) + 1;
var maStr = '{"propertyId":"prop1","landId":"land1","permitId":"permit1","buyerId":"vojha24","personalInfo":{"firstname":"Varun","lastname":"Ojha","dob":"dob","email":"varun@gmail.com","mobile":"99999999"},"financialInfo":{"monthlySalary":162000,"otherExpenditure":0,"monthlyRent":41500,"monthlyLoanPayment":40000},"status":"Submitted","requestedAmount":4000000,"fairMarketValue":5800000,"approvedAmount":4000000,"reviewedBy":"bond","lastModifiedDate":"21/09/2016 2:30pm"}';
var ma = JSON.parse(maStr);
ma['id'] = 'la'+id;
return bcSdk.createMortgageApplication({user: user, mortgageApplication: ma})
})
.then(function(resp){
var ma = resp.body;
return bcSdk.getMortgageApplication({user: user, id: ma['id']})
})
.then(function(resp){
logHelper.logMessage(logger,"runClient","Fetched mortgage application",resp.body);
})
.catch(function(err){
logHelper.logError(logger,"runClient","Error Occurred",err);
})
}
|
上面的清單中顯示的 blockchain_sample_client.js 將執行以下操作:
- 讀取在調用客戶端時傳入的命令行參數。客戶端需要兩個值:用戶名和從屬關系。如果在客戶端調用期間未提供任何值,客戶端將使用默認值。
- 然后客戶端會執行區塊鏈網絡的設置。在設置期間,會初始化鏈對象,並為其配置客戶端通信的所有對等節點和成員服務。設置期間還會初始化 Cloudant 數據存儲。
- 然后客戶端會繼續注冊默認用戶,或者注冊作為命令行參數傳入的用戶。
- 隨后是登記步驟,這一步使用注冊期間獲取的一次性密碼來獲取 ECert 對。這個 ECert 對將存儲在基於 Cloudant 的鍵值存儲中。
- 然后客戶端會創建一個樣本抵押貸款申請 JSON 對象。它隨后會調用 blockchain_sdk.js 文件中的 createMortgageApplication API。這個 createMortgageApplication 方法調用區塊鏈中的合適的鏈代碼方法,並傳入貸款申請 JSON 內容。
- 成功創建抵押貸款申請后,客戶端會調用 getMortgageApplication 方法,該方法進而會調用合適的鏈代碼方法來查詢區塊鏈,以獲取上一步中創建的抵押貸款申請。
網絡設置功能
設置
我們現在來詳細分析 setup
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
function setup(){
return new Promise(function(resolve, reject){
try{
logHelper.logMethodEntry(logger,"setup");
//Fetch IBM Bluemix Cloudant and Blockchain service instance configuration
var cloudantConfig = config.VCAP_SERVICES[constants.VCAP_SERVICES_CLOUDANT][0];
var blockchainConfig = config.VCAP_SERVICES[constants.VCAP_SERVICES_BLOCKCHAIN][0];
//Setup datastore
var result = datastore.initSync(cloudantConfig);
if(result.statusCode != constants.SUCCESS){
logHelper.logError(logger,'Could not initialize datastore', result);
return reject({statusCode: 500, body: ''});
}
//Setup Cloudant based KeyValueStore
var cloudantSetupDone = false;
getCloudantKeyValStore(datastore, config.databases[constants.APP_MASTER_DB])
.then(function(resp){
cloudantKvStore = resp.body;
cloudantSetupDone = true;
return blockchainNetwork.setupBlockchain({blockchainConfig: blockchainConfig, ccName: constants['BLOCKCHAIN_CHAINCODE_NAME'] , kvStore: cloudantKvStore })
})
.then(function(resp){
return resolve({statusCode: 200, body: ''});
})
.catch(function(err){
if(cloudantSetupDone != true){
logHelper.logError(logger,'Could not initialize CloudantKeyValueStore', err);
}
else{
logHelper.logError(logger,'Blockchain setup failed. exiting...',err);
}
return reject(err);
});
}
catch(err){
logHelper.logError(logger,'Could not complete setup', err);
throw reject({statusCode: 500, body: err});
}
})
}
|
在上面的清單中:
- 第 7 和第 8 行會從 runtime.json 文件中獲取 Cloudant 和 Blockchain 服務憑證,我們之前已更新過這些信息。config 模塊會解析 runtime.json 文件,並將其內容作為一個 JSON 對象提供。
- 第 11 到第 15 行會初始化 Cloudant 數據存儲。datastore.js 文件用於獲取一個數據庫實例的句柄,該句柄可用於在我們的 Cloudant 數據庫上執行 CRUD 操作。
- 第 18 到第 24 行會使用之前獲得的數據庫實例,初始化基於 Cloudant 的自定義鍵值存儲實現。Hyperledger Fabric Client SDK 鏈對象將使用這個鍵值存儲來管理 ECert 和其他用戶元數據。隨后是區塊鏈網絡設置。
區塊鏈網絡設置
現在讓我們來看看 setupBlockchain
方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
function setupBlockchain(params){
return new Promise(function(resolve,reject){
try{
logHelper.logEntryAndInput(logger, 'setupBlockchain', params);
if(!validate.isValidJson(params)){
logHelper.logError(logger, 'setupBlockchain', 'Invalid params');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not setup blockchain. Invalid params' });
}
var goPath = __dirname+'/../../chaincode/';
process.env.GOPATH = goPath;
chainInit({ccName: params.ccName, kvStorePath: params.kvStorePath, kvStore: params.kvStore})
.then(function(resp){
return loadNetworkConfiguration(params.blockchainConfig);
})
.then(function(resp){
return configureNetwork();
})
.then(function(resp){
logHelper.logMessage(logger, 'setupBlockchain', 'blockchain setup complete');
isSetupComplete = true;
return resolve({statusCode: constants.SUCCESS, body: 'blockchain setup complete'});
})
.catch(function(err){
logHelper.logError(logger, 'setupBlockchain', 'Could not setup blockchain', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not setup blockchain', err});
});
}
catch(err){
logHelper.logError(logger, 'setupBlockchain', 'Could not setup blockchain', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not setup blockchain', err});
}
});
}
|
此設置包含 3 個函數。第 14 行 以 chainInit
開頭,隨后是 loadNetworkConfiguration
,最后是 configureNetwork
。
這是初始化這個鏈的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
|
var kvStore = params.kvStore;
if(!validate.isValid(kvStore)){
logHelper.logError(logger, 'chainInit', 'Invalid kvStore');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not initialize Chain. Invalid kvStore' })
}
chain = hfc.newChain(ccName);
// Configure the KeyValStore which is used to store sensitive keys
// as so it is important to secure this storage.
chain.setKeyValStore(kvStore);
chain.setECDSAModeForGRPC(true);
|
- 第 1 到第 6 行負責輸入驗證。
- 在第 8 行中,使用 Hyperledger Fabric Client SDK 模塊
hfc
創建了一個新的鏈對象。 - 在第 11 行中,為這個鏈對象配置了鍵值存儲實現,以便用它來管理用戶的 ECert。在本例中,該實現是 Cloudant 鍵值存儲實現。Hyperledger Fabric Client SDK 還提供了一個默認的基於文件的鍵值存儲實現可供使用。(但是,要將區塊鏈客戶端應用程序部署到雲上,不建議使用基於文件的實現,因為重新部署客戶端代碼會導致基於文件的鍵值存儲丟失。在這種情況下,所有以前注冊的用戶都將失去作用,因為它們沒有必要的證書來執行驗證。)
- 在第 12 行中,用於 TLS 通信的密碼組被設置為 ECDSA。
鏈對象的配置屬性
- IsSecurityEnabled:如果為鏈對象配置了有效的成員服務端點,則需要考慮啟用安全性。啟用安全性意味着,將使用 TCert 生成一個基於 ECDSA 的數字簽名,所有鏈代碼調用事務中都會發送該簽名。各個對等節點都將使用這個 TCert 來驗證調用方。
- IsPreFetchMode:在預取模式中,鏈對象在初始化時將向成員服務請求一批 TCert,而不是等待一次事務調用。這么做是為了提高性能。
- TCertBatchSize:獲取 TCert 批量請求中包含的要返回的 TCert 數量。默認值為 200。
加載區塊鏈網絡配置
讓我們來看看 loadNetworkConfiguration
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
function loadNetworkConfiguration(blockchainConfig){
return new Promise(function(resolve, reject){
try {
logHelper.logEntryAndInput(logger, 'loadNetworkConfiguration');
if(!validate.isValidJson(blockchainConfig)){
logHelper.logError(logger, 'loadNetworkConfiguration', 'Invalid blockchainConfig');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not loadNetworkConfiguration. Invalid blockchainConfig' });
}
var bcService = blockchainConfig;
var peers = bcService.credentials.peers;
for (var i in peers) {
peerURLs.push(constants['BLOCKCHAIN_NW_PROTOCOL'] + peers[i].discovery_host + ":" + peers[i].discovery_port);
peerHosts.push("" + peers[i].discovery_host);
}
var ca = bcService.credentials.ca;
for (var i in ca) {
caURL = constants['BLOCKCHAIN_NW_PROTOCOL'] + ca[i].url;
}
//users are only found if security is on
if (bcService.credentials.users) users = bcService.credentials.users;
for (var z in users) {
if (users[z].username == constants['BLOCKCHAIN_REGISTRAR_ID']) {
registrarPassword = users[z].secret;
}
}
logHelper.logMessage(logger, 'loadNetworkConfiguration', 'Successfully loaded network configuration');
return resolve({statusCode: constants.SUCCESS, body: 'Successfully loaded network configuration'});
}
catch (err) {
logHelper.logError(logger, 'loadNetworkConfiguration', 'Could not load Network Configuration', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not load Network Configuration'});
}
});
}
|
- 第 13 到第 17 行將會解析 Blockchain 服務配置,以獲取對等節點列表,並將這些對等節點的發現 URL 添加到 peerHosts 集合中。
- 第 18 到第 21 行會獲取 CA(成員服務)的 URL。constants['BLOCKCHAIN_NW_PROTOCOL'] 的值為
grpcs
。Bluemix 上的 IBM Blockchain 服務僅支持安全通信。 - 第 23 到第 29 行從 WebAppAdmin 的 Blockchain 服務憑證中包含的預注冊用戶列表中獲取密碼。之前已經提到過,WebAppAdmin 用戶擁有注冊員權利。這意味着可使用 WebAppAdmin 用戶向區塊鏈注冊其他新用戶。此密碼以后會用於登記 WebAppAdmin。
區塊鏈網絡配置
讓我們來看看 configureNetwork
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
function configureNetwork() {
return new Promise(function(resolve, reject){
try{
logHelper.logEntryAndInput(logger, 'configureNetwork');
var pem;
fs.readFile(constants['BLOCKCHAIN_NW_CERT_PATH'], function(err, data){
if(validate.isValid(err)){
logHelper.logError(logger,'configureNetwork', 'Could not read cert: '+constants['BLOCKCHAIN_NW_CERT_PATH']);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not read cert: '+constants['BLOCKCHAIN_NW_CERT_PATH'] });
}
else{
pem = data;
chain.setMemberServicesUrl(caURL, { pem: pem });
for (var i in peerURLs) {
chain.addPeer(peerURLs[i], { pem: pem });
}
recursiveLogin({username: constants['BLOCKCHAIN_REGISTRAR_ID'], password: registrarPassword, chain: chain })
.then(function(resp){
logHelper.logMessage(logger,'configureNetwork', 'Successfully enrolled registrar: '+constants['BLOCKCHAIN_REGISTRAR_ID']);
var registrarMember = resp.body;
chain.setRegistrar(registrarMember);
return resolve({statusCode: constants.SUCCESS, body: 'Network configuration complete'});
})
.catch(function(err){
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not enroll registrar: '+constants['BLOCKCHAIN_REGISTRAR_ID'] });
})
}
});
}
catch(err){
logHelper.logError(logger, 'configureNetwork', 'Could not configure network', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not configure network', err});
}
});
}
|
- 第 7 行會讀取我們從 Blockchain 服務憑證中提供的證書路徑下載的 pem 文件。已為 Bluemix Blockchain 網絡中的所有通信啟用了 TLS。這也包括客戶端應用程序與網絡實體(比如成員服務和對等節點)之間的通信。
- 第 14 到第 17 行將之前獲取的成員服務和對等節點 URL 添加到鏈對象和 pem 文件中。鏈對象在與成員服務和對等節點通信時會使用 pem 文件。
- 第 19 到第 28 行會在區塊鏈網絡中登記 WebAppAdmin。之前已經解釋過,登記后將生成 ECert 對,鏈對象會將該證書對存儲在 Cloudant 鍵值存儲中。如果通過 Cloudant 服務查看您的 Cloudant 數據庫中的文檔,現在將會看到一個針對 member.WebAppAdmin 的條目,如下面示例所示:
{"name":"WebAppAdmin","enrollment":{"key":"f4b19c7195d2da0ea
02a47fa8e2aabdc0b4ba610720a696e895b400fb81cb9be","cert":"d2
d2d2d454e44204543445341205055424c4943204b45592d2d2d2d2d
0a","queryStateKey":{"type":"Buffer","data":[91,181,140,162,159,21
8,158,144,230,192,52,99,100,155,235,23,72,242,97,158,82,29,54,22
2]}}}
- 第 23 行將鏈的注冊員設置為 WebAppAdmin 成員對象。鏈將使用這個注冊員向區塊鏈注冊新成員/用戶。
區塊鏈設置到此就完成了。現在看看其他重要方法:
使用功能
注冊新用戶
讓我們來看看 doRegister
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
var roles = params.roles;
if(!validate.isValidArray(roles)){
roles = ['client'];
}
var enrollsecret
var chain = bcNetwork.getChain();
var reg = chain.getRegistrar();
var chainAsync = Promise.promisifyAll(chain);
chainAsync.getMemberAsync(username)
.then(function(member){
var memberAsync = Promise.promisifyAll(member);
var registrationRequest = {
enrollmentID: username,
attributes: [
{name: 'role', value: affiliation},
{name: 'username', value: username}
],
affiliation: 'group1',
registrar: reg,
roles: roles
};
return memberAsync.registerAsync(registrationRequest);
})
.then(function(enrollsec){
logHelper.logMessage(logger, 'registerUser', 'Successfully registered user on blockchain: '+username);
enrollsecret = enrollsec;
return resolve({statusCode: constants.SUCCESS, body: {password: enrollsecret}});
})
|
要向基於 Hyperledger Fabric 的區塊鏈注冊新用戶,可在注冊請求中發送以下字段:
- enrollmentID(字符串):此成員/用戶的登記 ID。
- roles(字符串數組):表示與此成員有關聯的角色。有效的角色包括
client
(默認)、peer
、validator
和auditor
。 affiliation
(字符串):此成員的從屬關系。從屬關系可在 manifest.yaml 文件中找到。對於 Bluemix 上的 Blockchain 服務,父分組是group1
。- attributes(屬性數組):授予此成員的屬性名稱和值。這些屬性由用戶定義,存儲在 ACA 中。所有這些屬性或部分屬性都將嵌入在此成員請求的 TCert 中。鏈代碼可以使用相同的屬性,通過解析調用方 TCert 來實現訪問控制。
- registrar:用戶可以具有注冊員的角色,該角色使用戶能注冊其他成員。需要在請求中包含擁有注冊員權利的成員對象。這個注冊員成員將用於注冊所請求的成員。
- 第 7 和第 8 行將從 blockchain_network.js 文件獲取鏈對象,並從該鏈對象獲取注冊員對象。
- 第 11 行將從鏈中獲取成員對象。在基於 Hyperledger Fabric 與區塊鏈網絡進行任何交互時,我們都必須擁有一個成員對象的句柄,代表這個成員對象來執行所有鏈代碼部署和調用。getMember 方法從 Cloudant 鍵值存儲中獲取成員 ECert 和其他元數據信息。
- 第 15 到第 23 行將創建一個注冊請求。請注意
username
和role
的屬性鍵/值對。本教程系列的第 1 部分介紹了鏈代碼如何利用這些屬性來實現訪問控制。 - 第 27 行將會調用成員對象上的注冊方法,后續代碼會獲取從 ECA 返回的一次性密碼令牌,以便在登記流程中使用它。
登記用戶
這是 doLogin
方法的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
var chain = bcNetwork.getChain();
var chainAsync = Promise.promisifyAll(chain);
chainAsync.getMemberAsync(username)
.then(function(member){
var memberAsync = Promise.promisifyAll(member);
return memberAsync.enrollAsync(password);
})
.then(function(crypto){
logHelper.logMessage(logger, 'doLogin', 'Successfully logged in user on blockchain: '+username);
return resolve({statusCode: constants.SUCCESS, body: crypto});
})
.catch(function(err){
logHelper.logError(logger, 'doLogin', 'Could not login user on blockchain: '+username, err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not login user' });
});
|
- 和之前一樣,第 1 到 6 行將獲取鏈對象並從鏈中獲取成員對象。
- 第 7 行將調用成員對象上的登記方法,並傳入從注冊響應中獲得的密碼。
- 第 9 行成功地從 ECA 獲取 ECert 對。
創建抵押貸款申請
這是 createMortgageApplication
方法的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
function createMortgageApplication(params) {
return new Promise(function(resolve, reject){
var mortgageApplication;
try{
logHelper.logEntryAndInput(logger, 'createMortgageApplication', params);
if(!validate.isValidJson(params)){
logHelper.logError(logger, 'createMortgageApplication', 'Invalid params');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not create mortgage application. Invalid params' })
}
var user = params.user;
if(!validate.isValidString(user)){
logHelper.logError(logger, 'createMortgageApplication', 'Invalid user');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not create mortgage application. Invalid user' })
}
mortgageApplication = params.mortgageApplication;
if(!validate.isValidJson(mortgageApplication)){
logHelper.logError(logger, 'createMortgageApplication', 'Invalid mortgageApplication');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not create mortgage application. Invalid mortgageApplication' })
}
var id = mortgageApplication['id'];
var payload = JSON.stringify(mortgageApplication);
var reqSpec = getRequestSpec({functionName: 'CreateLoanApplication', args: [id, payload]});
recursiveInvoke({requestSpec: reqSpec, user: user})
.then(function(resp){
logHelper.logMessage(logger, 'createMortgageApplication', 'Successfully created mortgageApplication', resp.body);
return resolve({statusCode: constants.SUCCESS, body: mortgageApplication});
})
.catch(function(err){
logHelper.logError(logger, 'createMortgageApplication', 'Could not create mortgageApplication', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not create mortgageApplication' });
});
}
catch(err){
logHelper.logError(logger, 'createMortgageApplication', 'Could not create mortgage application on blockchain ledger: ', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not create mortgage application' });
}
});
}
|
- 第 7 到第 22 行執行輸入驗證。該方法需要兩個參數:
User
(將用於調用鏈代碼事務)和要持久保存在區塊鏈上的抵押貸款申請 JSON 內容。 - 第 28 行將創建調用鏈代碼需要使用的請求規范。
- 第 29 行將調用
recursiveInvoke
函數,該函數根據所提供的請求規范來實際調用鏈代碼。
用於鏈代碼調用的請求規范
這是 getRequestSpec
方法的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
function getRequestSpec(params){
if(!validate.isValidJson(params)){
logHelper.logError(logger, 'getRequestSpec', 'Invalid params');
throw new Error("Invalid params");
}
var chaincodeID = config['chaincode']['id']
if(!validate.isValidString(chaincodeID)){
logHelper.logError(logger, 'getRequestSpec', 'Invalid chaincodeID');
throw new Error("Invalid chaincodeID");
}
var functionName = params.functionName;
if(!validate.isValidString(functionName)){
logHelper.logError(logger, 'getRequestSpec', 'Invalid function name');
throw new Error("Invalid function name");
}
var args = []
if(validate.isValidArray(params.args)){
args = params.args;
}
var attributes = ['username', 'role']
if(validate.isValidArray(params.attributes)){
attributes = params.attributes;
}
var spec = {
chaincodeID: chaincodeID,
fcn: functionName,
args: args,
attrs: attributes
}
return spec;
}
|
- 第 8 行將從配置 (runtime.json) 中獲取部署的鏈代碼的鏈代碼 ID。這是我們想要調用的鏈代碼。
- 第 14 行將獲取要在鏈代碼中調用的函數的名稱。在本例中,它是 CreateLoanApplication 方法。
- 第 26 行將輸入屬性鍵。這些鍵已硬編碼到角色和用戶名中,但在理想情況下,應將它們作為參數傳遞給
getRequestSpec
方法。備注:如果未在請求規范中傳入屬性鍵,這些屬性不會在 TCert 獲取請求中發送給 TCA,因此得到的 TCert 將沒有任何嵌入式屬性。當鏈代碼嘗試解析此 TCert 來獲取特定屬性(例如角色和用戶名)實現訪問控制時,該操作會失敗。第 32 到 37 行包含實際的請求規范模式。
調用部署在區塊鏈上的鏈代碼
這是 doInvoke
方法的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
var chain = bcNetwork.getChain();
var chainAsync = Promise.promisifyAll(chain);
chainAsync.getMemberAsync(user)
.then(function(member){
var tx = member.invoke(requestSpec);
tx.on('submitted', function(data) {
logHelper.logMessage(logger, 'doInvoke', 'Transaction for invoke submitted ',requestSpec);
return resolve({statusCode: constants.SUCCESS, body: data});
});
tx.on('complete', function(data) {
//logHelper.logMessage(logger, 'doInvoke', 'Transaction for invoke complete ',data);
});
tx.on('error', function (err) {
logHelper.logError(logger, 'doInvoke', 'Could not perform invoke ',err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not perform invoke ' });
});
})
|
- 第 1 到第 7 行將重復獲取鏈對象並獲取成員對象的過程。
- 第 8 行將在成員對象上調用
invoke
方法,以便根據傳入的請求規范調用鏈代碼。這次調用會返回一個事務對象。
備注 1:您需要在事務對象上注冊 submitted
、complete
和 error
事件,才能獲知事務的狀態:
- Submitted:事務已成功提交到區塊鏈網絡並被該網絡執行。此時會從區塊鏈網絡返回一個事務 UUID。
- Error:事務無法提交到區塊鏈網絡,或者無法被區塊鏈網絡接受。
- Complete:事務已完成。這不代表成功完成。(complete 事件會連同 submitted 和 error 事件一起被調用。)
備注 2:調用請求僅返回鏈代碼調用的事務 UUID。它不會返回從鏈代碼函數本身返回的任何數據。
備注 3:即使實際的鏈代碼函數拋出了錯誤且沒有成功完成,也不會調用 error 事件。此時會調用 complete 事件。
我采用的方法是等待 submitted 事件。這至少表明鏈代碼函數將被調用。要確保成功執行鏈代碼,您可以:
- 訂閱一個將在成功執行鏈代碼函數后生成的事件。
- 查詢區塊鏈。
從區塊鏈獲取抵押貸款申請
這是 getMortgageApplication
方法的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
function getMortgageApplication(params) {
return new Promise(function(resolve, reject){
try{
logHelper.logEntryAndInput(logger, 'getMortgageApplication', params);
if(!validate.isValidJson(params)){
logHelper.logError(logger, 'getMortgageApplication', 'Invalid params');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not fetch mortgage application. Invalid params' })
}
var user = params.user;
if(!validate.isValidString(user)){
logHelper.logError(logger, 'getMortgageApplication', 'Invalid user');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not fetch mortgage application. Invalid user' })
}
var id = params.id;
if(!validate.isValidString(id)){
logHelper.logError(logger, 'getMortgageApplication', 'Invalid id');
return reject({statusCode: constants.INVALID_INPUT, body: 'Could not fetch mortgage application. Invalid id' })
}
var reqSpec = getRequestSpec({functionName: 'GetLoanApplication', args: [id]});
recursiveQuery({requestSpec: reqSpec, user: user})
.then(function(resp){
logHelper.logMessage(logger, 'GetMortgageApplication', 'Successfully fetched mortgage application', resp.body);
return resolve({statusCode: constants.SUCCESS, body: resp.body});
})
.catch(function(err){
logHelper.logError(logger, 'GetMortgageApplication', 'Could not fetch mortgage application', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not fetch mortgage applications' });
});
}
catch(err){
logHelper.logError(logger, 'getMortgageApplication', 'Could not fetch property ad ', err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not fetch mortgage application ' });
}
});
}
|
getMortgageApplication
方法在結構上類似於 createMortgageApplication
方法。它接受需要代表其調用鏈代碼的用戶,以及要獲取的抵押貸款申請的 ID。
隨后是 getRequestSpec
方法,該方法像之前一樣創建請求規范並調用 recursiveQuery
方法來查詢區塊鏈。
使用鏈代碼查詢區塊鏈
這是 doQuery
方法的代碼段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
var chain = bcNetwork.getChain();
var chainAsync = Promise.promisifyAll(chain);
chainAsync.getMemberAsync(user)
.then(function(member){
var tx = member.query(requestSpec);
tx.on('submitted', function() {
logHelper.logMessage(logger, 'doQuery','Transaction for query submitted');
});
tx.on('complete', function(data) {
try{
logHelper.logMessage(logger, 'doQuery', 'Transaction for query complete ',requestSpec);
var buffer = new Buffer(data.result);
var jsonResp = JSON.parse(buffer.toString());
return resolve({statusCode: constants.SUCCESS, body: jsonResp});
}
catch(err){
logHelper.logError(logger,'doQuery','Could not parse query response',err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not parse query response ' });
}
});
tx.on('error', function (err) {
logHelper.logError(logger, 'doQuery', 'Could not perform query ',err);
return reject({statusCode: constants.INTERNAL_SERVER_ERROR, body: 'Could not perform query ' });
});
})
|
- 第 7 行將對成員對象調用
query
方法,並傳入合適的請求規范。
在區塊鏈上成功執行查詢事務后,將調用 complete 事件。從區塊鏈讀取的數據需要讀入到一個緩沖區中。因為我們的鏈代碼實現將數據存儲為 JSON 字符串,所以需要將返回的內容解析為 JSON。
訂閱事件
基於 Hyperledger Fabric 的區塊鏈網絡擁有一個內置事件中心,我們的客戶端應用程序將連接到該事件中心並監聽事件。
這是連接到事件中心並訂閱事件的代碼段。此代碼段包含在 blockchain_network.js 文件中,但由於下面的已知問題部分提到的第二個問題,它已被注釋掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
chain.eventHubConnect(peerEventHosts[0],{pem:pem});
setupEvents();
/**
* Sample method to showcase how to subscribe and consume events emitted from blockchain
*/
function setupEvents(){
try{
var eh = chain.getEventHub();
var cid = config['chaincode']['id'];
var regid = eh.registerChaincodeEvent(cid, "^eventSender$", function(event) {
console.log(event);
var buffer = new Buffer(event.payload);
console.log(buffer.toString());
});
console.log("EVENT SETUP DONE");
}
catch(err){
console.log(err);
console.log("Could not setup events");
}
}
process.on('exit', function (){
console.log('exit called');
chain.eventHubDisconnect();
});
|
- 第 1 行將連接指定對等節點的事件中心。runtime.json 中的 Blockchain 服務的服務憑證擁有每個對等節點的
event_host
和event_port
值。這些值用於與事件中心建立連接。 - 第 2 行將調用
setupEvents()
方法,該方法可用於訂閱和處理來自區塊鏈的事件。 - 第 11 行將展示如何注冊鏈代碼事件,並定義該事件發布時的處理函數。我們部署的鏈代碼會在成功創建貸款申請時發出一個事件。我們在這里注冊了同一個事件。
如果您運行的客戶端啟用了事件(取消注釋 blockchain_network.js 中的事件代碼),客戶端將接收鏈代碼中定義的createLoanApplication
事件並打印到控制台。
Hyperledger Fabric Client SDK 的已知問題
- 在成員對象上調用 invoke、query、deploy 時,出現隨機/間歇性的安全握手失敗錯誤。
為通信啟用 TLS 時會出現這種錯誤。發生此故障的一個主要原因是,操作系統中沒有足夠的文件描述符。該錯誤在 Mac 機器(EL Capitan 版)上最常見,在 Windows 7 機器上較少發生,而將應用程序/客戶端部署在 Bluemix 上並運行時,幾乎可以忽略該錯誤。
為了在開發和測試期間解決此問題,我將對區塊鏈網絡的所有 Hyperledger Fabric Client SDK 調用都包裝在遞歸函數中。可通過修改 blockchain_sdk.js 文件中的
retryInterval
和retryLimit
變量,更改重試間隔和重試次數。 - 事件中心未連接或在連接后崩潰。
此錯誤已被歸因於所依賴的一些第三方庫(比如 grpc node 模塊)中未解決的 bug。
結束語
通過本文中的步驟和代碼,您可以開發您自己的 Node.js 客戶端來與基於 Hyperledger Fabric v0.6 的區塊鏈網絡通信,並將第 1 部分中開發的鏈代碼部署到 Bluemix 上的 IBM Blockchain 服務,以便可從您的客戶端應用程序調用它。下載與本教程相關的代碼並開始實踐吧。
這個基於 Hyperledger Fabric v0.6 的教程系列已介紹完畢。我正針對 Hyperledger Fabric v1.0 更新本系列,敬請期待!