前言
官方英文:Writing Your First Application
仍然感謝中文翻譯!不過官方的教程有更新。我已經(jīng)按官方英文進(jìn)行了補(bǔ)充和修正,但是不保存翻譯的準(zhǔn)確性。如果希望看完整及原汁原味的文檔,推薦看官方英文文檔。
本文基于Mac OS以及Hyperledger Fabric 1.1.0。
Writing Your First Application - 編寫第一個(gè)應(yīng)用
注意:如果你對(duì)Fabric的原理及架構(gòu)還不熟悉,你可能需要先學(xué)習(xí)Introduction 和 Building Your First Network。
這篇文章中,我們會(huì)學(xué)習(xí)Fabric app是如何工作的。這些app(以及它們使用的智能合約),我們把它稱為fabcar,提供了許多Fabric函數(shù)。尤其是Certificate Authority對(duì)進(jìn)程的影響,以及生成證書登記。然后我們會(huì)使用這些生成的身份(用戶對(duì)象)來查詢和更新賬本。
通過以下三個(gè)步驟:
- 設(shè)置一個(gè)開發(fā)環(huán)境。我們的應(yīng)用需要一個(gè)網(wǎng)絡(luò)環(huán)境,所以我們下載一些組件,以便進(jìn)行注冊(cè)/登記,查詢和更新。

學(xué)習(xí)應(yīng)用程序中所用到的智能合約例子的參數(shù)。智能合約包含的各種功能讓我們可以用多種方式和賬本進(jìn)行交互。
開發(fā)能夠查詢以及更新資產(chǎn)的應(yīng)用程序。我們會(huì)進(jìn)入到app的代碼內(nèi)部(我們的app使用Javascript編寫),手動(dòng)修改變量來進(jìn)行不同方式的查詢和更新。
完成本教程之后,你應(yīng)該會(huì)基本了解一個(gè)帶有智能合約的應(yīng)用程序如何編碼,以及如何與Fabric網(wǎng)絡(luò)中的賬本(或者節(jié)點(diǎn))進(jìn)行交互的。
設(shè)置開發(fā)環(huán)境
首先配置開發(fā)環(huán)境,下載fabric-samples代碼以及鏡像文件。這些內(nèi)容請(qǐng)參考Hyperledger Fabric 1.1.0環(huán)境搭建。
現(xiàn)在進(jìn)入到文件夾,然后看看里面都有些什么:
yuyangdeMacBook-Pro:~ yuyang$ cd /Users/yuyang/fabric-sample/fabric-samples/fabcar
yuyangdeMacBook-Pro:fabcar yuyang$ ls
enrollAdmin.js package.json registerUser.js
invoke.js query.js startFabric.sh
在開始之前,我們先做些清理工作。執(zhí)行以下命令以關(guān)閉舊的或者啟動(dòng)著的容器:
yuyangdeMacBook-Pro:fabcar yuyang$ docker rm -f $(docker ps -aq)
清除網(wǎng)絡(luò)中的緩存:
# Press 'y' when prompted by the command
yuyangdeMacBook-Pro:fabcar yuyang$ docker network prune
WARNING! This will remove all networks not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Networks:
net_byfn
最后如果你準(zhǔn)備好運(yùn)行,你可能需要?jiǎng)h除潛在的fabcar智能合約鏡像。如果是第一次運(yùn)行,你將不需要這些在你系統(tǒng)中的鏈碼鏡像。
yuyangdeMacBook-Pro:fabcar yuyang$ docker rmi dev-peer0.org1.example.com-fabcar-1.0-5c906e402ed29f20260ae42283216aa75549c571e2e380f3615826365d8269ba
安裝客戶端&啟動(dòng)網(wǎng)絡(luò)
注意:接下來的命令都是運(yùn)行在
fabcar文件夾中的。
執(zhí)行以下命令為應(yīng)用安裝Fabric依賴。fabric-ca-client允許我們的app連接CA server并且檢索身份材料。fabric-client允許我們獲取身份材料,并且與節(jié)點(diǎn)和排序服務(wù)對(duì)話。
yuyangdeMacBook-Pro:fabcar yuyang$ npm install
使用startFabric.sh腳本啟動(dòng)網(wǎng)絡(luò)。這個(gè)命令會(huì)初始化各種Fabric實(shí)體,還會(huì)啟動(dòng)一個(gè)使用Golang編寫的智能合約容器。
./startFabric.sh
你也可以使用Node.js編寫的鏈碼,將剛才的指令修改為以下命令啟動(dòng):
./startFabric.sh node
好了,現(xiàn)在我們有了簡(jiǎn)單的網(wǎng)絡(luò)以及一些代碼,現(xiàn)在看看他們是怎么一起工作的。
應(yīng)用程序如何與網(wǎng)絡(luò)進(jìn)行交互(How Applications Interact with the Network)
如果你想深入了解fabcar網(wǎng)絡(luò)中的組件(以及怎樣部署的),更新詳細(xì)的了解應(yīng)用和組件是如何交互的,可以閱讀Understanding the Fabcar Network。
登記管理員用戶
接下來的兩部分內(nèi)容都與CA的通訊有關(guān),你可以通過查看CA的日志來獲得更多信息。
新開一個(gè)命令行窗口,輸入以下指令查看CA日志:
yuyangdeMacBook-Pro:fabcar yuyang$ docker logs -f ca.example.com
現(xiàn)在回到fabcar上來... ...
當(dāng)我們啟動(dòng)網(wǎng)絡(luò)時(shí),我們通過Certificate Authority注冊(cè)了一個(gè)管理員用戶admin?,F(xiàn)在我們需要向CA服務(wù)器發(fā)送一個(gè)登記請(qǐng)求,然后為其取回一個(gè)登記證書。這里我們不深入登記的細(xì)節(jié),只要知道這個(gè)證書是構(gòu)成管理員用戶的必要條件。我們隨后會(huì)使用這個(gè)管理員來注冊(cè)和登記新的用戶。現(xiàn)在向CA服務(wù)器發(fā)送管理員登記請(qǐng)求:
yuyangdeMacBook-Pro:fabcar yuyang$ node enrollAdmin.js
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully enrolled admin user "admin"
Assigned the admin user to the fabric client ::{"name":"admin","mspid":"Org1MSP","roles":null,"affiliation":"","enrollmentSecret":"","enrollment":{"signingIdentity":"c5e8730cb530c1e2db68c233cb8684023032451560db835a2404c2db6d9ff757","identity":{"certificate":"-----BEGIN CERTIFICATE-----\nMIICAjCCAaigAwIBAgIUCbtDEKsU4rC1FqRcX2F/Ilwv6WAwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMTgwMzIxMDQzNTAwWhcNMTkwMzIxMDQ0\nMDAwWjAhMQ8wDQYDVQQLEwZjbGllbnQxDjAMBgNVBAMTBWFkbWluMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEEc/eyDHDU5aPW5x1+5FojpsFWc4kgKDjflx30uZl\nXRZXmonEgYuVQYyVMHSaDrRjiTc1aewion01/4CSwCHodKNsMGowDgYDVR0PAQH/\nBAQDAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFP/+uwBM8hgooR+DggGmMmKR\nXiijMCsGA1UdIwQkMCKAIEI5qg3NdtruuLoM2nAYUdFFBNMarRst3dusalc2Xkl8\nMAoGCCqGSM49BAMCA0gAMEUCIQDwpO9f41gNPnvG5CitVUIe/Df5/2elTL2uMKcq\n/jUi4gIgCRtU78R7rJIjSo1UPo61I5O60zgQRkUJfl6o0xHEVKg=\n-----END CERTIFICATE-----\n"}}}
這行代碼會(huì)調(diào)用一個(gè)證書簽名請(qǐng)求(CSR),最后會(huì)在項(xiàng)目根目錄生成一個(gè)新的文件夾hfc-key-store,里面包含了證書和密鑰材料。當(dāng)我們的app需要?jiǎng)?chuàng)建和讀取不同身份用戶時(shí),需要定位到此文件夾。
User1的注冊(cè)和登記(Register and Enroll user1)
使用剛剛生成的管理員證書,我們?cè)僖淮温?lián)通CA服務(wù)器來注冊(cè)和登記一個(gè)新用戶。user1是我們用來查詢和更新賬本的用戶。這里著重說明的是,admin發(fā)起了新用戶的注冊(cè)和登記工作(就好像admin扮演了登記員的角色)。現(xiàn)在為admin發(fā)起登記和注冊(cè)請(qǐng)求:
yuyangdeMacBook-Pro:fabcar yuyang$ node registerUser.js
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded admin from persistence
Successfully registered user1 - secret:ErdiJNAOBHAN
Successfully enrolled member user "user1"
User1 was successfully registered and enrolled and is ready to intreact with the fabric network
和管理員登記一樣,上面的指令會(huì)調(diào)用CSR然后將證書和密鑰放入hfc-key-store文件夾中?,F(xiàn)在我們有了兩個(gè)用戶的身份材料。是時(shí)候與賬本交互了。
查詢賬本(Querying the Ledger)
查詢是指如何從賬本中讀取數(shù)據(jù)。您可以查詢單個(gè)或者多個(gè)鍵的值,如果賬本是以類似于JSON這樣的數(shù)據(jù)存儲(chǔ)格式寫入的,則可以執(zhí)行更復(fù)雜的搜索(如查找包含某些關(guān)鍵字的所有資產(chǎn))。
下圖是一個(gè)查詢流程的示意圖:

首先,運(yùn)行query.js 程序,返回賬本上所有汽車列表。我們使用user1作為簽名實(shí)體。我們的程序中已經(jīng)指定了user1為簽名實(shí)體:
fabric_client.getUserContext('user1', true);
user1的登記材料已經(jīng)放在了hfc-key-store文件夾中,我們只需要簡(jiǎn)單的告訴程序去獲取它就行了。在定義了用戶對(duì)象后,我們繼續(xù)讀取賬本的流程。queryAllCars這個(gè)方法已經(jīng)被提前定義在了app中,它可以查詢所有的cars。執(zhí)行以下指令:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
返回應(yīng)如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is [{"Key":"CAR0", "Record":{"colour":"blue","make":"Toyota","model":"Prius","owner":"Tomoko"}},{"Key":"CAR1", "Record":{"colour":"red","make":"Ford","model":"Mustang","owner":"Brad"}},{"Key":"CAR2", "Record":{"colour":"green","make":"Hyundai","model":"Tucson","owner":"Jin Soo"}},{"Key":"CAR3", "Record":{"colour":"yellow","make":"Volkswagen","model":"Passat","owner":"Max"}},{"Key":"CAR4", "Record":{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}},{"Key":"CAR5", "Record":{"colour":"purple","make":"Peugeot","model":"205","owner":"Michel"}},{"Key":"CAR6", "Record":{"colour":"white","make":"Chery","model":"S22L","owner":"Aarav"}},{"Key":"CAR7", "Record":{"colour":"violet","make":"Fiat","model":"Punto","owner":"Pari"}},{"Key":"CAR8", "Record":{"colour":"indigo","make":"Tata","model":"Nano","owner":"Valeria"}},{"Key":"CAR9", "Record":{"colour":"brown","make":"Holden","model":"Barina","owner":"Shotaro"}}]
這里有10輛車,一輛屬于Adriana的黑色Tesla Model S、一輛屬于Brad的紅色Ford Mustang、一輛屬于Pari的紫羅蘭色Fiat Punto等等。賬本是基于Key/Value 的,在這里,關(guān)鍵字是從CAR0到CAR9。這一點(diǎn)特別重要。
現(xiàn)在讓我們來看看代碼內(nèi)容。使用編輯器(例如atom或visual studio)打開query.js程序。
應(yīng)用程序的初始部分定義了變量,如鏈碼,通道名稱和網(wǎng)絡(luò)端點(diǎn)。在我們的app中,這些變量已經(jīng)定義好了,但是在真實(shí)的開發(fā)中,這些變量應(yīng)該又開發(fā)者指定。
var channel = fabric_client.newChannel('mychannel');
var peer = fabric_client.newPeer('grpc://localhost:7051');
channel.addPeer(peer);
var member_user = null;
var store_path = path.join(__dirname, 'hfc-key-store');
console.log('Store path:'+store_path);
var tx_id = null;
這是構(gòu)建查詢的代碼塊:
// queryCar chaincode function - requires 1 argument, ex: args: ['CAR4'],
// queryAllCars chaincode function - requires no arguments , ex: args: [''],
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryAllCars',
args: ['']
};
當(dāng)程序運(yùn)行時(shí),它會(huì)調(diào)用節(jié)點(diǎn)上的fabcar鏈碼,執(zhí)行queryAllCars函數(shù),不傳任何參數(shù)。
要查看鏈碼提供的其他函數(shù),轉(zhuǎn)至到fabric-samples子目錄chaincode/fabcar/go,并在編輯器中打開fabcar.go。
里面也有使用Node.js版本的鏈碼。
你會(huì)看到,我們可以調(diào)用下面的函數(shù)- initLedger、queryCar、queryAllCars、createCar和changeCarOwner。
讓我們仔細(xì)看看queryAllCars函數(shù)是如何與賬本進(jìn)行交互的。
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
startKey := "CAR0"
endKey := "CAR999"
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
這里定義了queryAllCars的范圍。在CAR0和CAR999的每輛車。因此,我們理論上可以創(chuàng)建1,000輛汽車,queryAllCars函數(shù)將會(huì)顯示出每一輛汽車的信息。
下面的圖演示了app如何調(diào)用鏈碼上的不同方法。每一個(gè)方法都必須在chaincode shim interface中可見的API中編碼。這可以使智能合約容器與節(jié)點(diǎn)賬本有序連接。

我們可以看到我們用過的queryAllCars函數(shù),還有一個(gè)叫做createCar,這個(gè)函數(shù)可以讓我們更新賬本,并最終在鏈上增加一個(gè)新區(qū)塊。
現(xiàn)在我們返回query.js程序并編輯請(qǐng)求構(gòu)造函數(shù)以查詢特定的車輛。為達(dá)此目的,我們將函數(shù)queryAllCars更改為queryCar并將特定的“Key” 傳遞給args參數(shù)。在這里,我們使用CAR4。 所以我們編輯后的query.js程序現(xiàn)在應(yīng)該包含以下內(nèi)容:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR4']
};
保存程序并返回fabcar目錄?,F(xiàn)在再次運(yùn)行程序:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
您應(yīng)該看到以下內(nèi)容:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
這樣,我們就從查詢所有車變成了只查詢一輛車:Adriana的黑色Tesla Model S。使用queryCar函數(shù),我們可以查詢?nèi)我怅P(guān)鍵字(例如CAR0),并獲得與該車相對(duì)應(yīng)的制造廠商、型號(hào)、顏色和所有者。
很好,現(xiàn)在您應(yīng)該比較熟悉該鏈碼的基本查詢功能以及帶參數(shù)的查詢功能了?,F(xiàn)在是時(shí)候更新賬本了…
更新賬本(Updating the Ledger)
現(xiàn)在我們已經(jīng)完成了一些賬本查詢,并且增加了一些代碼,我們準(zhǔn)備好更新賬本了。有許多種更新賬本的方法,不過我們先從創(chuàng)造一輛車開始。
下面的示意圖演示了這個(gè)流程。

賬本更新是從生成交易提案的應(yīng)用程序開始的。就像查詢一樣,我們將會(huì)構(gòu)造一個(gè)請(qǐng)求,用來識(shí)別要進(jìn)行交易的通道ID、函數(shù)以及智能合約。該程序然后調(diào)用channel.SendTransactionProposalAPI將交易建議發(fā)送給peer(s)進(jìn)行認(rèn)證。
網(wǎng)絡(luò)(即endorsing peer)返回一個(gè)提案答復(fù),應(yīng)用程序以此來創(chuàng)建和簽署交易請(qǐng)求。該請(qǐng)求通過調(diào)用channel.sendTransaction API發(fā)送到排序服務(wù)器。排序服務(wù)器將把交易打包進(jìn)區(qū)塊,然后將區(qū)塊“發(fā)送”到通道上的所有peers進(jìn)行認(rèn)證。(在我們的例子中,我們只有一個(gè)endorsing peer。)
最后,應(yīng)用程序使用eh.setPeerAddr API連接到peer的事務(wù)監(jiān)聽端口,并調(diào)用eh.registerTxEvent注冊(cè)與特定交易ID相關(guān)聯(lián)的事務(wù)。該API使得應(yīng)用程序獲得事務(wù)的結(jié)果(即成功提交或不成功)。把它當(dāng)作一個(gè)通知機(jī)制。
我們初始調(diào)用的目標(biāo)是簡(jiǎn)單地創(chuàng)建一個(gè)新的汽車。我們有一個(gè)獨(dú)立的用于這些交易的JavaScript程序 - invoke.js。就像查詢一樣,使用編輯器打開程序并轉(zhuǎn)到構(gòu)建調(diào)用的代碼塊:
// createCar chaincode function - requires 5 args, ex: args: ['CAR12', 'Honda', 'Accord', 'Black', 'Tom'],
// changeCarOwner chaincode function - requires 2 args , ex: args: ['CAR10', 'Barry'],
// must send the proposal to endorsing peers
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: '',
args: [''],
chainId: 'mychannel',
txId: tx_id
};
我們可以調(diào)用函數(shù)createCar或者changeCarOwner。首先我們創(chuàng)建一個(gè)紅色的Chevy Volt,并把它歸屬于Nick。在賬本中我們的Key值已經(jīng)用到了CAR9 ,所以這里我們將使用CAR10。更新代碼塊如下:
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: 'createCar',
args: ['CAR10', 'Chevy', 'Volt', 'Red', 'Nick'],
chainId: 'mychannel',
txId: tx_id
};
保存并運(yùn)行程序:
yuyangdeMacBook-Pro:fabcar yuyang$ node invoke.js
輸入如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Assigning transaction_id: ea460cb160e0d5c15737b35aade81bbaaa7b29ccbd3b34f43929809290c213fa
Transaction proposal was good
Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK"
The transaction has been committed on peer localhost:7053
Send transaction promise and event listener promise have completed
Successfully sent transaction to the orderer.
Successfully committed the change to the ledger by the peer
我們關(guān)心的是這個(gè):
The transaction has been committed on peer localhost:7053
可以看到交易已經(jīng)被確認(rèn)。現(xiàn)在回到query.js,然后修改參數(shù)CAR4為CAR10。
修改前:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR4']
};
修改后:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR10']
};
保存后,然后查詢:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
結(jié)果如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Nick"}
恭喜!你已經(jīng)創(chuàng)造了一輛車!
最后,我們來調(diào)用最后一個(gè)函數(shù)changeCarOwner。Nick很慷慨,他想把他的Chevy Volt送給Dave。所以,我們簡(jiǎn)單編輯invoke.js 如下:
var request = {
//targets: let default to the peer assigned to the client
chaincodeId: 'fabcar',
fcn: 'changeCarOwner',
args: ['CAR10', 'Dave'],
chainId: 'mychannel',
txId: tx_id
};
第一個(gè)參數(shù)定義了哪輛車被變更主人。第二個(gè)參數(shù)定義了新主人姓名。
保存并執(zhí)行:
yuyangdeMacBook-Pro:fabcar yuyang$ node invoke.js
結(jié)果如下:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Assigning transaction_id: c5f23e3da0812761b47755c3487ad9cf8291a2b756061473ce0b85aa0fce1411
Transaction proposal was good
Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK"
The transaction has been committed on peer localhost:7053
Send transaction promise and event listener promise have completed
Successfully sent transaction to the orderer.
Successfully committed the change to the ledger by the peer
現(xiàn)在我們?nèi)ゲ樵冑~本,看看CAR10是不是已經(jīng)在Dave名下:
yuyangdeMacBook-Pro:fabcar yuyang$ node query.js
結(jié)果如下,Dave擁有了CAR10:
Store path:/Users/yuyang/fabric-sample/fabric-samples/fabcar/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Dave"}
真實(shí)情況下,鏈碼需要權(quán)限控制。例如只有某些具有權(quán)限的人才能創(chuàng)造新車,也應(yīng)該只有車主才能轉(zhuǎn)讓汽車所有權(quán)。