一、概述
官方原文地址 Writing Your First Application
如果對fabric網絡的基本運行機制不熟悉的話,請看這里。
注意:本教程是對fabric應用以及如何使用智能合約的簡單介紹,對fabric應用及智能合約的詳細介紹請看應用開發部分和商業票據教程。
本教程將介紹一些示例程序以助於理解fabric應用是如何工作的。這些應用和所使用的智能合約被稱為FabCar。它們是理解Hyperledger Fabric blockchain的很好的起點。你將會學習如何編寫一個應用和智能合約來查詢或更新賬本,以及如何使用CA生成區塊鏈應用程序交互所需要的X.509證書。
我們會使用SDK(詳細介紹在這里)來調用智能合約,該合約使用智能合約SDK查詢和更新賬本(詳細介紹在這里)。
二、開發fabric應用包括三個主要步驟
1. 設置開發環境:應用程序需要一個網絡來進行交互,因此我們將得到一個智能合約和應用程序所需要的基本網絡。
2. 學習智能合約示例——FabCar:此合約是用JavaScript編寫的。我們會查看該合約,理解其中的交易,以及它是如何被應用程序用來查看和更新賬本的。
3. 使用FabCar開發一個示例程序:該程序會使用FabCar智能合約來查詢和更新賬本中的汽車資產(car assets)。我們將深入了解應用程序代碼及其創建的交易,包括查詢汽車、查詢一系列汽車以及創建一輛新車。
三、示例下載學習
3.1、代碼下載
cd $GOPATH/src/github.com/hyperledger/ git clone https://github.com/hyperledger/fabric-samples.git cd fabric-samples/fabcar
此代碼放到與fabric並行目錄下,沒特殊要求
3.2、在fabcar下會有如下文件
javascript javascript-low-level startFabric.sh typescript
3.3、關閉曾經創建過的網絡
注意:此部分需要你進入first-network子目錄
如果你已經完成了 Building Your First Network ,說明你已經下載了fabric-samples並且啟動了網絡。在開始本教程之前,必須先關閉此網絡:
./byfn.sh down
如果你之前運行過本教程,請使用以下命令刪除所有容器(不管與fabric相關與否,都會刪除):
docker rm -f $(docker ps -aq)
docker rmi -f $(docker images | grep fabcar | awk '{print $3}')
注意如果這里自己有其他的應用使用,推薦一個一個刪除
如果你的環境和相關依賴沒有配置好,請根據 Prerequisites 以及 Install Samples, Binaries and Docker Images 進行配置。
3.4、啟動網絡
注意:此部分需要你進入fabcar子目錄
在fabcar目錄下運行./startFabric.sh javascript
實際測試增加了javascript會啟動失敗,
如遇到:
ERROR: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
到上面這個問題,可以參考這里解決
再次運行,得到下面的結果:dsafdsfsfsdfsdfdsfdsfsd
這表明已經成功啟動了示例網絡,並且fabcar智能合約也被成功的安裝和實例化了。接下來進行應用程序的安裝。
3.5、安裝應用程序
注意:此部分需要你進入fabcar/javascript子目錄
運行下面的命令安裝應用程序所需的fabric依賴(在此之前要保證已經安裝了npm):
npm install
3.6、注冊admin用戶
注意:以下兩個部分涉及與證書頒發機構的通信。當運行程序時,通過打開新的終端shell並運行docker logs -f ca.example.com,您可能會發現流式傳輸CA日志非常有用。
當我們創建網絡時,一個名為admin的管理用戶被創建為證書頒發機構(CA)的注冊器。我們的第一步是使用enroll.js程序為管理員生成私鑰、公鑰和X.509證書。此過程使用證書簽名請求(CSR)(私鑰和公鑰首先在本地生成,然后將公鑰發送到CA,CA返回編碼的證書供應用程序使用。然后,這三個憑證存儲在錢包中,允許我們充當CA的管理員。)
接下來注冊admin用戶(此命令將會把CA管理員的憑證存儲在wallet目錄中):
node enrollAdmin.js
3.7、注冊user用戶
上一步已經注冊了管理員用戶,錢包中有了管理員的憑證,現在可以注冊一個新用戶(user1)用於查詢和更新分類賬:
node registerUser.js
現在我們就有了兩個獨立用戶admin和user1的憑證了,后面的應用程序會用到這些憑證。
3.8、查詢賬本
區塊鏈網絡中的每一個節點都有一個賬本的副本,應用程序可以通過調用智能合約查詢賬本,該智能合約查詢賬本的最新值(世界狀態)並將其返回給應用程序。世界狀態是一組鍵值對,應用程序可以查詢單個鍵或多個鍵。
現在首先運行query.js程序獲取賬本上所有車輛的信息,此程序使用第二個身份(user1)來獲取賬本:
node query.js
查看query.js
/* * SPDX-License-Identifier: Apache-2.0 */ 'use strict'; const { FileSystemWallet, Gateway } = require('fabric-network'); const fs = require('fs'); const path = require('path'); const ccpPath = path.resolve(__dirname, '..', '..', 'basic-network', 'connection.json'); const ccpJSON = fs.readFileSync(ccpPath, 'utf8'); const ccp = JSON.parse(ccpJSON); async function main() { try { // Create a new file system based wallet for managing identities. const walletPath = path.join(process.cwd(), 'wallet'); const wallet = new FileSystemWallet(walletPath); console.log(`Wallet path: ${walletPath}`); // Check to see if we've already enrolled the user. const userExists = await wallet.exists('user1'); if (!userExists) { console.log('An identity for the user "user1" does not exist in the wallet'); console.log('Run the registerUser.js application before retrying'); return; } // Create a new gateway for connecting to our peer node. const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'user1', discovery: { enabled: false } }); // Get the network (channel) our contract is deployed to. const network = await gateway.getNetwork('mychannel'); // Get the contract from the network. const contract = network.getContract('fabcar'); // Evaluate the specified transaction. // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4') // queryAllCars transaction - requires no arguments, ex: ('queryAllCars') const result = await contract.evaluateTransaction('queryAllCars'); console.log(`Transaction has been evaluated, result is: ${result.toString()}`); } catch (error) { console.error(`Failed to evaluate transaction: ${error}`); process.exit(1); } } main();
程序開始部分首先引用了fabric-network模塊的FileSystemWallet
和Gateway兩個關鍵類。這兩個類用來定位user1的錢包身份,以及連接網絡:
const { FileSystemWallet, Gateway } = require('fabric-network');
應用程序使用一個網關來連接網絡:
const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'user1', discovery: { enabled: false } });
接下來嘗試修改query.js,使其只查詢CAR4:
//const result = await contract.evaluateTransaction('queryAllCars'); const result = await contract.evaluateTransaction('queryCar', 'CAR4');
具體鏈碼可以查看:fabric-samples/chaincode查找,核心代碼

/* * SPDX-License-Identifier: Apache-2.0 */ 'use strict'; const { Contract } = require('fabric-contract-api'); class FabCar extends Contract { async initLedger(ctx) { console.info('============= START : Initialize Ledger ==========='); const cars = [ { color: 'blue', make: 'Toyota', model: 'Prius', owner: 'Tomoko', }, { color: 'red', make: 'Ford', model: 'Mustang', owner: 'Brad', }, { color: 'green', make: 'Hyundai', model: 'Tucson', owner: 'Jin Soo', }, { color: 'yellow', make: 'Volkswagen', model: 'Passat', owner: 'Max', }, { color: 'black', make: 'Tesla', model: 'S', owner: 'Adriana', }, { color: 'purple', make: 'Peugeot', model: '205', owner: 'Michel', }, { color: 'white', make: 'Chery', model: 'S22L', owner: 'Aarav', }, { color: 'violet', make: 'Fiat', model: 'Punto', owner: 'Pari', }, { color: 'indigo', make: 'Tata', model: 'Nano', owner: 'Valeria', }, { color: 'brown', make: 'Holden', model: 'Barina', owner: 'Shotaro', }, ]; for (let i = 0; i < cars.length; i++) { cars[i].docType = 'car'; await ctx.stub.putState('CAR' + i, Buffer.from(JSON.stringify(cars[i]))); console.info('Added <--> ', cars[i]); } console.info('============= END : Initialize Ledger ==========='); } async queryCar(ctx, carNumber) { const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state if (!carAsBytes || carAsBytes.length === 0) { throw new Error(`${carNumber} does not exist`); } console.log(carAsBytes.toString()); return carAsBytes.toString(); } async createCar(ctx, carNumber, make, model, color, owner) { console.info('============= START : Create Car ==========='); const car = { color, docType: 'car', make, model, owner, }; await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); console.info('============= END : Create Car ==========='); } async queryAllCars(ctx) { const startKey = 'CAR0'; const endKey = 'CAR999'; const iterator = await ctx.stub.getStateByRange(startKey, endKey); const allResults = []; while (true) { const res = await iterator.next(); if (res.value && res.value.value.toString()) { console.log(res.value.value.toString('utf8')); const Key = res.value.key; let Record; try { Record = JSON.parse(res.value.value.toString('utf8')); } catch (err) { console.log(err); Record = res.value.value.toString('utf8'); } allResults.push({ Key, Record }); } if (res.done) { console.log('end of data'); await iterator.close(); console.info(allResults); return JSON.stringify(allResults); } } } async changeCarOwner(ctx, carNumber, newOwner) { console.info('============= START : changeCarOwner ==========='); const carAsBytes = await ctx.stub.getState(carNumber); // get the car from chaincode state if (!carAsBytes || carAsBytes.length === 0) { throw new Error(`${carNumber} does not exist`); } const car = JSON.parse(carAsBytes.toString()); car.owner = newOwner; await ctx.stub.putState(carNumber, Buffer.from(JSON.stringify(car))); console.info('============= END : changeCarOwner ==========='); } } module.exports = FabCar;
再次運行query.js:
$ node query-car4.js
Wallet path: /Users/lihongxu6/work/mygo/src/github.com/hyperledger/fabric-samples/fabcar/javascript/wallet
Transaction has been evaluated, result is: {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
現在,大概已經對查詢交易有了大概的了解,接下來看看如何更新賬本。
3.9、更新賬本
3.9.1、創建
我們可以做很多方面的更新操作,但先從創建一輛新車開始吧。
從應用程序的角度來看,更新賬本是很簡單的。應用先提交一個交易到區塊鏈網絡,然后當這個交易被證明有效后,應用會收到一個交易成功的通知。這個過程涉及到共識機制:
上圖顯示了更新賬本所涉及到的主要組件。區塊鏈網絡包含多個peer,每個peer都維護一份賬本副本,並且選擇性的維護一個智能合約副本,除此之外,網絡還包括一個排序服務。排序服務能夠創建一個包含連接到此網絡的不同應用所提交的經過排序的交易的區塊。
我們使用invoke.js程序來實現創建一輛新車的更新操作:以下是在invoke中的一段代碼:
await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom');
運行
$ node invoke.js Wallet path: /Users/lihongxu6/work/mygo/src/github.com/hyperledger/fabric-samples/fabcar/javascript/wallet 2019-04-03T10:26:48.661Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "5a9563b1397339de4de191aa3c6e52371033811b9aa3d3435bc429b2afabe54f" Transaction has been submitted
注意一下,invoke.js程序與區塊鏈交互使用的是submitTransaction API而不是evaluateTransaction。
submitTransaction比evaluateTransaction復雜的多。SDK會將submitTransaction提案發送給每一個需要的組織中的peer,而不是只和單個peer交互。所有這些接收提案的peer將會執行提案要求執行的智能合約生成交易響應,並對交易響應進行簽名后發回給SDK。SDK將收集到的所有經過簽名的交易響應合並到一個新的交易中,然后將其發送給orderer。orderer將來自各個應用的交易進行收集和排序后,將這些交易放到區塊中。然后將這個新區塊(每個交易都是經過證明合法的)分發給網絡中的所有peer節點。最后,通知SDK將控制權交回給應用程序。
上一段很重要,涉及到很多工作,而這些工作都是由submitTransaction完成的。應用程序、智能合約、peers、ordering service協同工作來保證賬本的一致性的過程叫做共識過程,共識機制的詳細介紹看這里。
通過已下查看即可【注意鏈碼調用方法】
node query.js
查看結果中會有新加入的記錄
3.9.2、修改
現在,假設Tom非常慷慨,想把Honda Accord車送給Dave,為了實現這個過程,需要修改invoke.js程序,
// await contract.submitTransaction('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom'); await contract.submitTransaction('changeCarOwner', 'CAR12', 'Dave');
再次執行invoke.js程序(由於網絡原因,可能會由於連接超時而報錯,多運行幾次即可):
$ node invoke.js Wallet path: /Users/lihongxu6/work/mygo/src/github.com/hyperledger/fabric-samples/fabcar/javascript/wallet 2019-04-03T10:35:21.125Z - info: [TransactionEventHandler]: _strategySuccess: strategy success for transaction "d64761e0880c70b4578b53e467e05cfa8696cfb2791bce5d3062bc1196894359" Transaction has been submitted
再次執行query.js查看owner值是否為Dave:
可以看到,所有者確實變為了Dave。
至此,就成功完成了fabric1.4官方文檔中的writing your first application部分。這一部分比較簡單,更深入的請看下面:
3.10、更高級的應用【Additional resources】
As we said in the introduction, we have a whole section on Developing Applications that includes in-depth information on smart contracts, process and data design, a tutorial using a more in-depth Commercial Paper tutorial and a large amount of other material relating to the development of applications.
四、舊版本說明
以下是1.1版本的示例,chaincode在根目錄下
3、開啟網絡配置[注意docker版本17.06,在1.12.6版本沒有-e命令]
./startFabric.sh
4、查詢一個賬本
安裝node
yum install gcc-c++
npm install
查詢[注意配置query.js中的ip地址]
node query.js
展示如下數據
Create a client and set the wallet location Set wallet path, and associate user PeerAdmin with application Check user is enrolled, and set a query URL in the network Make query Assigning transaction_id: f2f45cb045d6290d199e1b2d4eb3b60b1e9cafeff8d09e2b7683dd8578492be7 returned from query 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"}}]
5、分析query.js
1》初始化參數,包含了用戶ID,信道,鏈碼,網絡連接入口
var options = { wallet_path: path.join(__dirname, './creds'), user_id: 'PeerAdmin', channel_id: 'mychannel', chaincode_id: 'fabcar', network_url: 'grpc://localhost:7051', };
2》查詢代碼
var transaction_id = client.newTransactionID(); // queryCar - requires 1 argument, ex: args: ['CAR4'], // queryAllCars - requires no arguments , ex: args: [''], const request = { chaincodeId: options.chaincode_id, txId: transaction_id, fcn: 'queryAllCars', args: [''] }; return channel.queryByChaincode(request);
這里設置了鏈碼ID,交易ID,以及調用的鏈碼的方法fcn,方法參數args等
3》鏈碼:/chaincode/fabcar/目錄下fabcar.go
此文件匹配上文的鏈碼ID,包含了如下方法:initLedger
, queryCar
,queryAllCars
, createCar
and changeCarOwner
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response { startKey := "CAR0" endKey := "CAR999" resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
此處就是查詢范圍內的數據。
查看所有方法
6、測試
cp query.js query1.js
vim query1.js
修改內部訪問鏈碼方法
const request = { chaincodeId: options.chaincode_id, txId: transaction_id, fcn: 'queryCar', args: ['CAR4'] };
執行:node query1.js
Create a client and set the wallet location Set wallet path, and associate user PeerAdmin with application Check user is enrolled, and set a query URL in the network Make query Assigning transaction_id: ca88dc3b60f4df009a709f2f5ee5ad3b54f43d03a7e0b931042e2797f70c795d returned from query Query result count = 1 Response is {"colour":"black","make":"Tesla","model":"S","owner":"Adriana"}
7、更新賬本數據
使用fabcar目錄下的invoke.js
修改,中的 fcn,以及args等參數
var request = { targets: targets, chaincodeId: options.chaincode_id, fcn: '', args: [''], chainId: options.channel_id, txId: tx_id
如下
var request = { targets: targets, chaincodeId: options.chaincode_id, fcn: 'createCar', args: ['CAR10', 'Chevy', 'Volt', 'Red', 'Nick'], chainId: options.channel_id, txId: tx_id
執行命令
node invoke.js
成功后會有
The transaction has been committed on peer localhost:7053
執行
cp query1.js query2.js vim query2.js
將query2.js中查詢條件參數,變為CAR10即可
Response is {"colour":"Red","make":"Chevy","model":"Volt","owner":"Nick"}
ok,可以繼續調試其他方法。