Hyperledger Fabric 筆記(三)
Hyperledger Fabric 應用程式的開發
本系列文章是以 Blockchain with Hyperledger Fabric, Second Edition 書中的內容為基礎,配合一些實務上遇到的問題所做的筆記。
Hyperledger fabric 開發的應用程式可以完成以下任務:
- 查詢帳本
- 提交新交易
- 接收帳本通知訊息
而任何交易都離不開身份驗證的機制,這裡主要著重於交易流程與身份驗證的實作。
應用程式開發
目前最容易的就是直接使用官方開發好的套件來開發應用程式,其中fabric-network 這個 package 提供了許多強大的功能,相較於舊版的 fabric-client 更加方便使用。因此,在開發應用程式時,建議選擇使用 fabric-network 套件。此外,應用程式不需要關注智能合約的背書政策,因為 SDK 的 Discovery Service 會自動處理這些事項。
這裡得說,hyperledger fabric 的官方文件並沒有寫得很清楚,在開發時很常看到一些範例其實是 fabric-client 的舊功能,如果不仔細看很容易遇到開發成品不如預期的挫折。
查詢帳本
由於分散式帳本的特性,應用程式可以在網路中的任何帳本上查詢相對應的結果。然而,應用程式會優先查詢自己組織節點的帳本,從而提高查詢效率。
提交交易
整個交易提交流程就是產生共識的過程。也可以直接將整個流程稱呼為「共識」。透過SDK可以很簡單的完成整個流程,流程可以分為三個階段:交易簽章(Transaction Signing)、分配(Distribution)以及驗證(Validation)。
交易簽章
在這個階段,應用程式會生成交易提案,並將其發送到網路中的相關節點進行簽章。Discovery服務會幫助SDK識別出合約安裝在哪些節點上,以及背書政策的內容。這使得SDK能夠自動避開未運行或錯誤的節點,並收集所有必要的簽章。
在上圖的範例中,app1想要紀錄一筆車輛銷售的交易(id:1234)。整個交易起始於包含交易定義以及簽章的交易提案。該提案會被發送到網路中,並用於構建多方交易。 SDK可以判斷合約A須符合背書政策A,因此會將交易提案發送給組織A與組織B的節點(2a, 3a),而兩個組織都安裝了合約A,因此可以產生經過簽章的交易回覆。 雖然節點4沒有安裝相對應的合約,因此無法產生交易回覆,但他知道背書政策A,因此可以驗證交易的有效性。 而在這個階段,Discovery扮演重要的角色,Discovery幫助SDK認知到合約安裝在哪些節點上,且知道背書政策A的內容。 在步驟2b,3b,SDK可以將兩個已簽章的交易回傳值打包進原本的交易提案中(帳本狀態尚未被修改)。SDK提交以及收集回傳值都是並行的,並且會自動避開未運行或錯誤的節點。
分配
交易完成簽章後,SDK會將其傳送給排序節點,排序節點則會將交易打包成區塊,並分發給所有節點。
延續上面的流程,接著必須要將交易發送給所有的節點,而這件事是由排序節點來處理。SDK會將已經過簽章的多方交易傳送給排序節點(5a)。在交易送到排序節點後,就是共識流程開始之時,而SDK要一直等到交易寫入帳本後才會回傳給App。排序節點會將交易打包成區塊,並發送給各個節點(6),且發送是併發的。
驗證
每個節點收到區塊後,會將其加到自己的區塊鏈上,並開始驗證交易的有效性。經過驗證的交易會寫入狀態資料庫,並通知相關應用程式。
在目前的範例中,交易1234必須經過兩步驟驗證,第一步需要驗證被書政策,確認交易是經過組織A與B簽章的。第二步要驗證交易的before-image與狀態資料庫的物件狀態符合,在符合的情況下,after-image的內容才會改寫狀態資料庫。
當節點完成整個上鏈的程序後,他會通知所有要求通知的應用程式。在8a中,事件通知是由peer2產生,8b是SDK收到通知。應用程式不必設定特定節點接收通知,因為SDK會預設向所有組織中的節點註冊通知。當然,通知策略也是可以客製化的。當SDK收到通知後,他會將控制權交還給應用程式,並指示交易內容是有效還是無效。
接收帳本通知訊息
應用程式可以隨時向節點發送通知請求,以接收區塊或事件通知。當帳本有更動時,節點會通知註冊請求的應用程式,從而確保應用程式能夠即時獲取帳本的最新狀態。 與查詢相反,帳本通知本身是個非同步的過程。通知本身是個很簡單的流程,應用程式只需向節點發送通知請求即可。當帳本有更動時,節點會通知註冊請求的應用程式。而通知有兩種形式,事件通知(event notification)以及區塊通知(block notification)。
錢包(Wallet)與身份(Identity)
在Hyperledger Fabric中,身份和錢包是應用程式與區塊鏈網路互動的核心。
使用身份
身份在區塊鏈網路中扮演著重要的角色,每個網路元件(如節點、排序節點、CA、組織)都有自己的唯一身份,這些身份通常以X509證書形式存在。理論上,其他證書類型也是可能的,但不常見。每個參與者的 X.509 證書均由其組織的證書頒發機構(CA)頒發。 所有的元件透過網路通道結合在一起,且相關資訊被配置在一組MSP定義(MSP definitions)中。該定義描述了每個組織以及其關鍵角色,可以想像為一間公司的公司登記。透過MSP,每個在通道中的人都可以很快地知道對方身份所代表的組織。
使用錢包
錢包用於存儲身份資訊,並通過SDK的Gateway與區塊鏈網路進行交互。錢包的存儲形式可以多種多樣,如本地儲存(當應用程式運作在網頁上時很有用)、資料庫或硬體安全模組(Hardware Security Module 簡稱 HSM)。 錢包是透過SDK提供的gateway來與通道交互。以下是一個範例
const userName = 'pedroId1@orgA.example.com'
const wallet = new FileSystemWallet('../identity/user/pedro/wallet')
const connectionOptions = {
identity: userName,
wallet: wallet,
eventHandlerOptions: {
commitTimeout: 100,
strategy: EventStrategies.MSPID_SCOPE_ANYFORTX
}
}
await gateway.connect(connectionProfile, connectionOptions)
Gateways 與 Discovery
Gateway 將所有元件結合在一起,讓應用程式專注於主要功能(查找、提交、通知)的開發。Discovery 服務則利用 gossip 協定幫助 SDK 自動發現網路中的其他元件,簡化了網路的配置。
Gateways
可以將 Gateway 視為使用者與網路互動的連接點。我們只需要定義以下兩點。
- connectionProfile:定義組織的連線資訊。
- connectionOptions:定義應用程式與網路互動的設定,例如身份或錢包。
接著即可透過APIgateway.connect()
將所有的一切串接再一起。
下面是一個connectionProfile的範例:
name: vehicle-networks
version: 1.0.0
organizations:
OrgA:
mspid: OrgA.MSP
peers:
- peer1.orgA.example.com
certificateAuthorities:
- ca1.example.com
peers:
peer1.orgA.example.com:
url: grpc://peer1.orgA.example.com:7051
certificateAuthorities:
ca1.example.com:
url: http://ca1.example.com:7054
caName: ca1.orgA.example.com
我們只需要定義簡單的資訊,當SDK連上區塊鏈網路時,discovery 會自動拓墣(topology),將剩下的資訊補足並取代原本的 connectionProfile。
Discovery
因為 Discovery,我們可以使用很簡單的 connectionProfile 便可以連上區塊鏈網路。順帶一提,在舊版的 fabric 中,connectionProfile 需要完整的記錄所有在通道中的組織集結點資訊,相較之下新版的連線就方便許多。 Discovery 本身是個利用 gossip 的通訊協定去搜尋其他網路元件行為的稱呼。我們只需要定義網路中的一個節點,discovery 便可以為我們拿回該節點連接的所有通道相關資訊。SDK會找到其他節點並可以知道他們是否安裝著合約,預設上,SDK會假定有安裝合約的節點為背書節點。 Discovery不僅止於節點。他也可以透過定義在通道配置中的 錨節點(anchor peer) 找到其他的組織。
evaluateTransaction 與 submitTransaction 的不同
在看官方文件時,會發現這兩個方法看起來都是類似的用途,但主要的差異如下。 evaluateTransaction 用於在特定節點上查詢資料,而不會經過共識流程,也不會更改帳本內容;而submitTransaction 則會執行交易,經過共識流程並將結果寫入帳本。因此,如果單一節點被篡改時,雖然不影響整個鏈上的資料正確性,但該組織的使用者可能會查詢到被篡改的資料。
而為什麼 evaluateTransaction 不會經過共識流程,是因為每個分散式帳本的內容理論上都會是相同的,所以在特定節點的帳本查詢理論上來說就會是同樣的資料。
事件(Events)與通知(Notifications)
通知是補足整個交易流程的最後一塊拼圖,與查詢或提交不同,通知是一個非同步過程。應用程式可以註冊監聽特定的事件,當這些事件發生時,節點會透過SDK通知應用程式。
事件是由合約所創造,包含在 output 當中,事件本身沒有 before-image 或是 after-image,他僅僅傳達事實。 下面是一個包含事件的交易:
Registration updateRegistration transaction:
identifier: 1234567890
proposal:
input: {CAR1, Bob, {51.06,-1.32}}
signature: input signed by Pedro
response:
output:
{CAR1.currentOwner = Sara Seller,
CAR1.currentOwner = Bob Buyer,
event:{"regEvent",{car: CAR1, location: 51.06,-1.32}}}
signatures:
output signed by Sara Seller
output signed by Bob Buyer
同樣的,在合約中新增事件也是一件簡單的事。
import { Context, Contract, Info, Returns, Transaction } from 'fabriccontract-api';
import { Car } from './car';
import { Registration } from './registration';
import { Location } from './location';
@Info({title: 'RegistrationContract', description: 'The registration smart contract' })
export class RegistrationContract extends Contract {
//...
@Transaction()
public async updateRegistration(ctx: Context, CarId: string,
newReg: Registration,
coord: GpsLocation)
: Promise<Registration> {
const exists = await this.registrationExists(ctx, carId);
if (!exists) {
throw new Error(`Cannot find registration for car ${carId}.`);
}
const updatedReg = new Registration();
updatedReg.value = newReg;
const buffer = Buffer.from(JSON.stringify(updatedReg));
await ctx.stub.putState(CarId, buffer);
const eventPayload: Buffer = Buffer.from(
{'CarId': CarId, 'location': coord});
await ctx.stub.setEvent('regEvent', eventPayload);
return updatedReg;
}
//...
上面的合約在交易時會產生一個叫做 RegEvent 的事件,且事件是獨立於交易寫入的。此事件也會包含在回傳的簽章中。 再來看看應用程式如何接收到事件。
const userName = 'yogendraId@orgB.example.com';
const wallet = new FileSystemWallet('../identity/user/yogendra/wallet');
connectionProfilePath = path.resolve(__dirname, 'registration-network.json');
connectionProfile = JSON.parse(fs.readFileSync(connectionProfilePath,
'utf8'));
connectionOptions = {
identity: userName,
wallet: wallet,
eventHandlerOptions: {
commitTimeout: 100,
strategy: EventStrategies.MSPID_SCOPE_ANYFORTX
}
};
await gateway.connect(connectionProfile, connectionOptions);
regNetwork = await gateway.getNetwork('vehicle-registration');
contract = await network.getContract('RegistrationPackage',
'Registration');
// Listen for regEvent notifications
console.log(`Listening for registration event.`);
const listener =
await contract.addContractListener('reg-listener',
'regEvent',
(error: Error, event: any) => {
if (error) {
console.log(`Error from event: ${error.toString()}`);
return;
}
const eventString: string = 'chaincode_id: ${event.chaincode_id}',
tx_id: ${event.tx_id}, event_name: "${event.event_name}",
payload: `${event.payload.toString()}`;
console.log('Event caught: ${eventString}');
});
上面的addContractListener()
傳入三個參數,監聽器的名字、監聽的事件以及事件處理函式。當addContractListener()
完成後,節點就會記得我們發出的通知要求,當有事件產生時,就會透過SDK回傳兩個參數(error, event)回來。而 event 的結構如下:
- chaincodeId 是智能合約組合的名字。事件是包含在交易中,而交易則是透過合約生成的。
- tx_id 是交易 ID
- event_name 是產生事件的名字。
- event_payload 則是事件的內容。 當應用程式停止,監聽器會自然被移除,但事件依然會生成。兩者是解耦合的。
每個節點都可以產生事件通知,但還是可以透過設定擋設定負責產生通知的節點(event hub),他節點即使收到事件,也不會產生通知。
應用程式的設計策略
根據應用程式的需求,可以採用不同的設計策略:
- 應用程式同步:應用程式等待交易成功寫入帳本後再繼續操作。
- 應用程式非同步:應用程式不等待交易回應,並且可以執行其他不相關的交易。
- 交易非同步:應用程式可以同時處理交易與事件監聽,將兩者視為分開的應用程式。
小結
這篇簡單紀錄了使用 Hyperledger Fabric 開發一個應用程式的要點,涵蓋了查詢帳本、提交交易以及接收通知等核心功能。通過使用fabric-network package、Gateway 與 Discovery 服務,可以更簡單地與區塊鏈網路進行互動,實現應用程式的各項需求。