NOTE: 如果你現(xiàn)在還不理解Fabric網(wǎng)絡(luò)的基本架構(gòu),你可能需要先去了解一下怎么構(gòu)建你的第一個(gè)網(wǎng)絡(luò)那篇教程。
這一節(jié)中我們會(huì)有好多個(gè)示例程序來看看Fabric的應(yīng)用是怎么工作的。這些apps(以及他們使用的智能合約)- 共同稱為fabcar - 提供了一個(gè)Fabric功能的全面的展示
尤其是,我們會(huì)演示和證書認(rèn)證(Certificate Authority)之間交互的流程,以及生成登記證書(enrollment certificates)的過程,在這之后會(huì)利用這些生成的標(biāo)識(shí)(用戶對(duì)象)來查詢和更新賬本。
我們會(huì)經(jīng)過如下三個(gè)理論上的步驟:
-
建立起一個(gè)開發(fā)環(huán)境。我們的應(yīng)用程序需要一個(gè)網(wǎng)絡(luò)來運(yùn)行,所以我們會(huì)下載一個(gè),并且剝離出中間我們需要的那些用于注冊(cè)/登記、查詢和更新的模塊:
學(xué)習(xí)我們app中要用到的示例智能合約的參數(shù)。我們的智能合約包含很多功能,這些功能允許我們和賬本以不同的方式進(jìn)行交互。我們會(huì)去研究這些智能合約,然和學(xué)習(xí)我們application要使用的那些功能。
開發(fā)一個(gè)可以查詢和更新賬本上的資產(chǎn)的應(yīng)用程序。我們會(huì)去深入學(xué)習(xí)一下app的代碼(這里的app都是用javascript寫的),并且手動(dòng)去控制變量來運(yùn)行不同的查詢和更新。
完成這個(gè)教程后,你就會(huì)對(duì)Fabric網(wǎng)絡(luò)里面如何編程來通過智能合約和賬本交互有個(gè)基本的認(rèn)識(shí)。
1 構(gòu)建你的開發(fā)環(huán)境
第一件事情,下載用于構(gòu)建網(wǎng)絡(luò)和應(yīng)用程序的Fabric鏡像,以及相關(guān)的組件。
接下來,下載fabric-samples工程,接著下載最新的穩(wěn)定版本的Fabric鏡像和可用的工具。
現(xiàn)在就可以看看我們fabric-samples目錄下都有些什么
cd fabric-samples/fabcar && ls
你會(huì)看到這些:
enrollAdmin.js invoke.js package.json query.js registerUser.js startFabric.sh
在開始之前,我們還要做一些清理工作,刪除所有的容器
docker rm -f $(docker ps -aq)
清除cached網(wǎng)絡(luò):
# Press 'y' when prompted by the command
docker network prune
最后的,如果你以及運(yùn)行過本教程了,你還需要?jiǎng)h除隱藏的chaincode鏡像。
docker rmi dev-peer0.org1.example.com-fabcar-1.0-5c906e402ed29f20260ae42283216aa75549c571e2e380f3615826365d8269ba
1.1. 安裝clients并且啟動(dòng)網(wǎng)絡(luò)
NOTE: 后面的這些命令都要求你在fabcar這個(gè)子目錄下進(jìn)行。
運(yùn)行下面的命令來安裝應(yīng)用程序的Fabric依賴,
- fabric-ca-client是用來允許我們的app和CA服務(wù)器通信并且獲取識(shí)別材料;
- fabric-client允許我們來加載識(shí)別材料到節(jié)點(diǎn)和ordering服務(wù)上
npm install
使用腳本startFabric.sh來啟動(dòng)你的網(wǎng)絡(luò),這個(gè)命令會(huì)讓我們的多個(gè)Fabric實(shí)體運(yùn)轉(zhuǎn)起來(spin up),并且為golang編寫的chaincode啟動(dòng)一個(gè)智能合約容器:
./startFabric.sh
現(xiàn)在我們就有一個(gè)示例網(wǎng)絡(luò)以及一些代碼了,接下來就是看看這些是怎么組合到一起工作的。
2. Application怎么和網(wǎng)絡(luò)交互?
為了更深入一步去看看我們的fabcar網(wǎng)絡(luò)中的組件(還有如何部署),以及應(yīng)用程序怎么和這些組件進(jìn)行交互,請(qǐng)參看understand_fabcar_network.
開發(fā)者更加感興趣的是要看看應(yīng)用程序在做什么 - 以及研究代碼的本身以了解應(yīng)用程序是怎么構(gòu)成的?,F(xiàn)在最重要的事情是了解我們的應(yīng)用程序是通過一個(gè)SDK來訪問APIs,這些APIs允許我們查詢和更新賬本。
3. 登記管理(Admin)用戶
NOTE: 接下來的兩節(jié)涉及到和Certificate Authority之間的通信。你會(huì)發(fā)現(xiàn)運(yùn)行后面這些程序的時(shí)候,輸出CA的日志很有幫助
開啟一個(gè)新的shell,使用如下命令來輸出你的CA日志:
docker logs -f ca.example.com
然后回到原來的終端,
當(dāng)我們啟動(dòng)這個(gè)網(wǎng)絡(luò)的時(shí)候,一個(gè)管理用戶 - admin - 就注冊(cè)了?,F(xiàn)在我們需要發(fā)送一個(gè)enroll的調(diào)用到CA服務(wù),來獲取這個(gè)用戶的登記證書(enrollment certificate, eCert)。我們這里不會(huì)深入了解enrollment的細(xì)節(jié),簡單來說就是SDK以及我們的應(yīng)用程序需要這個(gè)證書來為admin生成一個(gè)用戶對(duì)象。之后我們才能使用這個(gè)管理對(duì)象(admin object)來注冊(cè)和登記新的用戶。
發(fā)送admin enroll調(diào)用給CA服務(wù):
node enrollAdmin.js
這個(gè)程序會(huì)調(diào)用一個(gè)證書簽字請(qǐng)求(certificate signing request,CSR),并且最終輸出一個(gè)eCert證書和密鑰材料到一個(gè)新創(chuàng)建的文件夾 - hfc-key-store - 這個(gè)文件夾在這個(gè)工程的根目錄下。我們的apps會(huì)在這個(gè)位置尋找所需的材料用來創(chuàng)建或者加載不同用戶的識(shí)別對(duì)象。
4. 注冊(cè)和登記user1
我們現(xiàn)在就可以使用我們新生成的admin eCert證書來和CA服務(wù)通信,以注冊(cè)和登記新的用戶了。用戶user1就是我們用來查詢和更新賬本的識(shí)別號(hào)。這里需要注意的一點(diǎn),只有管理者標(biāo)記(admin identity)才能發(fā)起新用戶的注冊(cè)和登記調(diào)用。為user1發(fā)送注冊(cè)和登記調(diào)用,
node registerUser.js
類似于管理者的登記,這個(gè)函數(shù)調(diào)用一個(gè)CSR并且輸出密鑰和eCert到子目錄hfc-key-store中。所以我們現(xiàn)在就有兩個(gè)獨(dú)立用戶(admin和user1)的識(shí)別材料了。
是時(shí)候開始和賬本進(jìn)行交互了...
5. 查詢賬本
查詢就是從賬本讀取數(shù)據(jù)。這個(gè)數(shù)據(jù)保存成了一系列的鍵值對(duì),你可以查詢單個(gè)key或者多個(gè)keys對(duì)于的值;或者,如果賬本作為一個(gè)像JSON這樣的富數(shù)據(jù)存儲(chǔ)格式寫入到DB中的,那么我們還可以進(jìn)行更加復(fù)雜的查詢操作。
下圖演示的是查詢操作工作的過程,

首先,我們運(yùn)行一個(gè)query.js的程序來返回賬本上所有汽車的列表。我們這里使用我們的第二個(gè)標(biāo)識(shí) - user1 - 作為這個(gè)應(yīng)用程序的簽名實(shí)體。我們程序里的這幾行指定了user1作為簽名者
fabric_client.getUserContext('user1', true);
回想一下我們之前放到目錄hfc-key-store的登記材料,我們的應(yīng)用程序需要獲取這些識(shí)別材料。我們這個(gè)時(shí)候可以使用這些定義的用戶對(duì)象來執(zhí)行從賬本讀取的操作。queryAllCars函數(shù)會(huì)查詢所有的汽車,已經(jīng)預(yù)加載到app中了,我們這里可以簡單的如下運(yùn)行程序:
node query.js
會(huì)返回如下的內(nèi)容,
Query result count = 1
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倆車,這個(gè)賬本是一個(gè)基于鍵值對(duì),在我們的實(shí)現(xiàn)中鍵值是CAR0到CAR9。
我們現(xiàn)在打開query.js文件,來仔細(xì)看看這個(gè)程序。
應(yīng)用程序的初始化部分定義了一些變量,比如說通道名稱,證書保存的位置,以及網(wǎng)絡(luò)節(jié)點(diǎn)。在我們的示例app里,這些變量都以及打包進(jìn)去了,但是在真是app中,這些變量需要通過app開發(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)造請(qǐng)求的代碼
// 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īng)用程序運(yùn)行的時(shí)候,它會(huì)調(diào)用節(jié)點(diǎn)上的fabcar chaincode,并且運(yùn)行里面的queryAllCars函數(shù),沒有入?yún)ⅰ?br> 再看一眼我們的智能合約里面可用的函數(shù),找到fabric-samples/chaincode/fabcar/go子目錄下的fabcar.go文件并打開,
NOTE: 相同的函數(shù)也定義在Node.js版本的fabcar chaincode中。
我們可以看到有以下可用的調(diào)用:initLedger, queryCar, queryAllCars, createCar, 以及changeCarOwner。
現(xiàn)在讓我們仔細(xì)看一下queryAllCars函數(shù)來看看它是怎么和賬本交互的。
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response{
startKey := "CAR0"
endKey := "CAR999"
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
這個(gè)函數(shù)定義了queryAllCars的范圍。介于CAR0到CAR999之間的所有的汽車 - 查詢總共會(huì)返回1000輛車(假設(shè)每個(gè)鍵值都標(biāo)記了)。
下面這個(gè)圖演示的是,一個(gè)應(yīng)用程序怎么來調(diào)用chaincode上不同的函數(shù)的。每個(gè)函數(shù)必須基于chaincode shim接口上可用的API來進(jìn)行編碼,這些接口最終會(huì)映射到智能合約容器到相應(yīng)的peer賬本之間的接口。

我們來看一下queryAllCars這個(gè)函數(shù),這個(gè)函數(shù)就和createCar這個(gè)函數(shù)類似,都允許我們來更新賬本,并且最終添加一個(gè)新的區(qū)塊到當(dāng)前的鏈上。
首先,我們回到query.js程序,并修改指令來請(qǐng)求CAR4。我們把queryAllCars修改成queryCar并傳遞參數(shù)CAR4給這個(gè)函數(shù)
程序修改成這樣:
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR4']
};
保存函數(shù),重新運(yùn)行程序:
node query.js
你可以看到如下結(jié)果:
{"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
使用queryCar函數(shù)你可以查詢?nèi)魏我粋€(gè)鍵值來獲取這些汽車的信息。
Great. At this point you should be comfortable with the basic query functions in the smart contract and the handful of
parameters in the query program. Time to update the ledger...
好的接下來我們來聊一聊怎么更新ledger
6. 更新賬本
我們可以對(duì)賬本進(jìn)行各種各樣的更新,但是我們從創(chuàng)建一個(gè)汽車開始。
接下來我們可以看到這個(gè)過程是怎樣進(jìn)行的。首先請(qǐng)求一個(gè)更新,背書,接著返回給應(yīng)用程序,然后再發(fā)出去進(jìn)行排序,最終寫到每個(gè)節(jié)點(diǎn)的賬本中。

這里有一個(gè)單獨(dú)的javascript函數(shù) - invoke.js - 我們要使用這個(gè)函數(shù)進(jìn)行更新操作。同樣的,打開程序,找到調(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
};
You’ll see that we can call one of two functions - createCar or changeCarOwner . First, let’s create a red Chevy
Volt and give it to an owner named Nick. We’re up to CAR9 on our ledger, so we’ll use CAR10 as the identifying key
here. Edit this code block to look like this:
我們看到這里我們可以調(diào)用兩個(gè)函數(shù) - createCar或者changeCarOwner。首先我們創(chuàng)建一個(gè)red Chevy Volt,并命名為Nick。然后使用鍵值CAR10來進(jìn)行標(biāo)識(shí),代碼修改成這樣,
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)行
node invoke.js
終端會(huì)有一些log的打印,但是這里我們只關(guān)心這一條,
The transaction has been committed on peer localhost:7053
看到這條打印后,我們?cè)倩厝ミ\(yùn)行查詢程序,首先修改query.js程序
const request = {
//targets : --- letting this default to the peers assigned to the channel
chaincodeId: 'fabcar',
fcn: 'queryCar',
args: ['CAR10']
};
先保存然后運(yùn)行,
node query.js
返回結(jié)果是
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Nick"}
那么如果我們現(xiàn)在Nick要把他的這輛車轉(zhuǎn)贈(zèng)給Dave該怎么操作?
回到invoke.js程序,然后把函數(shù)createCar修改成changeCarOwner如下
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è)入?yún)?,CAR10代表要交易的汽車,Dave代表新的主人。
執(zhí)行一下更新,
node invoke.js
接著再查詢一下結(jié)果
node query.js
返回結(jié)果如下:
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Dave"}
NOTE: 在真實(shí)場景中的應(yīng)用程序,chaincode里面會(huì)有一些訪問控制邏輯。比如說只有某些授權(quán)的用戶可以創(chuàng)建一個(gè)新車,只有汽車的擁有著才能轉(zhuǎn)移這個(gè)車給別人。
7. 總結(jié)
現(xiàn)在其實(shí)你應(yīng)該對(duì)應(yīng)用程序怎么和網(wǎng)絡(luò)之間交互有了一些基本的理解了,我們可以看到智能合約,APIs,以及SDK分別扮演怎樣的角色。
接下來我們會(huì)學(xué)習(xí)怎么去真正的寫一個(gè)智能合約,以及這些更底層的應(yīng)用程序函數(shù)是怎么被驅(qū)動(dòng)的(尤其是涉及身份認(rèn)知和成員服務(wù)的。)
