Hyperledger Fabric 賬本結構解析
前言
現在很多人都在從事區塊鏈方面的研究,作者也一直在基於Hyperledger Fabric做一些開發工作。為了方便後來人更快的入門,本著“開源”的精神,在本文中向大家講解一下Hyperledger Fabric賬本的結構和原理。作者解析的Fabric的工程版本為v1.0.1,在新版本中可能會有些許偏差。
ps:作者預設各位讀者已經具備了一定的區塊鏈基本知識,不再做一些基礎知識的闡述。
Hyperledger Fabric賬本的結構
在作者最初瞭解bitcoin的時候有一個疑問:礦工如何校驗一筆交易中引入的Utxo是否合法呢?如果過是從創世塊開始遍歷工作量會隨著塊的增加而逐漸變大。當作者研究過Bitcoin的原始碼後發現,在Bitcoin中有一部分模組是用來儲存當前所有Utxo的,它採用的是levelDB資料庫,在校驗一筆交易中的Utxo是否合法時直接去DB中檢索即可。
Bitcoin的這種做法給作者總結為如下:“單純”的區塊鏈賬本儲存區塊資料即可,而為了方便支援各種功能往往會提取出一些資料(重要的、頻繁訪問的)獨立儲存。在Bitcoin的世界中礦工的共識實際上是對當前Utxo的共識,所以它將Utxo從區塊量賬本中提取出來獨立的儲存,那在Hyperledger Fabric中需要從區塊賬本中提取出的資料是什麼?
Fabric中是一個針對商業應用的分散式賬本技術,本身並不存在代幣,你可以在它的基礎上進行二次開發,利用Fabric的智慧合約實現自己需要的各種業務--包括髮放代幣。在它的智慧合約模組中有兩個最關鍵的介面中:shim.PutState(Key, Value) 和 shim.GetState(Key),這個是用來向節點本地讀寫資料的指令,它呈現給智慧合約的是一個key-value結構的非關係形資料庫。然後Client通過呼叫智慧合約執行業務邏輯,智慧合約來進行資料的讀寫操作的(在區塊鏈的世界中呼叫智慧合約的Request統稱為Transaction,這是約定俗稱的,雖然在Fabric中沒有代幣),這和傳統的中心化Web服務十分相似,只不過Tomcat換成了Fabric,而後臺的jar包換成了智慧合約,中心化的APP變成了分散式的DAPP。
現在參照作者從Bitcoin中總結的規律,在Fabric智慧合約中讀寫的業務資料符合重要的、頻繁訪問的特徵,應該獨立儲存,這個資料庫的名稱為StateDB 。除了StateDB以外我們還要儲存區塊資料,在Fabric裡面它有一個自己的FileSystem,用來儲存區塊資料,這個檔案系統是儲存在本地的檔案中的。區塊資料被連續的寫入到本地的BlockFile中,通過File.io來讀寫資料,Fabric工程是用go語言編寫的,工程中操作檔案IO的包是os ,包中常用操作檔案的函式如下:
type File func Create(name string) (*File, error) func NewFile(fd uintptr, name string) *File func Open(name string) (*File, error) func OpenFile(name string, flag int, perm FileMode) (*File, error) func Pipe() (r *File, w *File, err error) func (f *File) Chdir() error func (f *File) Chmod(mode FileMode) error func (f *File) Chown(uid, gid int) error func (f *File) Close() error func (f *File) Fd() uintptr func (f *File) Name() string func (f *File) Read(b []byte) (n int, err error) func (f *File) ReadAt(b []byte, off int64) (n int, err error) func (f *File) Readdir(n int) ([]FileInfo, error) func (f *File) Readdirnames(n int) (names []string, err error) func (f *File) Seek(offset int64, whence int) (ret int64, err error) func (f *File) SetDeadline(t time.Time) error func (f *File) SetReadDeadline(t time.Time) error func (f *File) SetWriteDeadline(t time.Time) error func (f *File) Stat() (FileInfo, error) func (f *File) Sync() error func (f *File) Truncate(size int64) error func (f *File) Write(b []byte) (n int, err error) func (f *File) WriteAt(b []byte, off int64) (n int, err error) func (f *File) WriteString(s string) (n int, err error) type FileInfo func Lstat(name string) (FileInfo, error) func Stat(name string) (FileInfo, error) type FileMode func (m FileMode) IsDir() bool func (m FileMode) IsRegular() bool func (m FileMode) Perm() FileMode func (m FileMode) String() string
Q1:以上所有的介面都不支援複雜的查詢功能,讀者們可以想象一下,當要去BlockFile中讀取一個塊的時候要什麼樣的條件才能快速的找到對應塊?
A1: 如果查詢者能知道一個Block資料在檔案中的便宜量則可以快速定位到Block,相反則只能通過遍歷的方式。為了解決這個問題Fabric 將Block在BlockFile檔案中的偏移量記載到了一個非關係形DB中,這個DB的名稱為indexDB。
Q2:區塊資料是不可篡改的,也就是說BlockFile的size是持續增長的,如果size過大超過了位置偏移量的最大範圍怎麼辦?
A2: 將區塊資料儲存在多個檔案塊中,以blockfile_ 為字首,以塊的生成次序為字尾。
解決了上面的兩個問題後,查詢一個塊資料的過程如下:
setup1: 從indexDB中讀取Block的位置資訊(blockfile的編號、位置偏移量);
setup2: 開啟對應的blockfile,位移到指定位置,讀取Block資料。
Fabric賬本的整體結構圖來如下圖所示:
其中ledgersData是整個賬本的根目錄,作者會逐個資料夾進行解析:
(1)chains:chains/chains下包含的mychannel是對應的channel的名稱,因為Fabric是有多channel的機制,而channel之間的賬本是隔離的,每個channel都有自己的賬本空間。chains/index下面包含的是levelDB資料庫檔案,在Fabric中預設所有的資料庫都是LevelDB,這個原因作者下面會講到,DB中儲存的就是我們上面說的區塊索引部分。chains/chains和chains/index就是上面所說的File System和indexDB;
(2)stateLeveldb: 同樣是levelDB資料庫,儲存的就是我們上面所說的智慧合約putstate寫入的資料;
(3)ledgerProvider:資料庫記憶體儲的是當前節點所包含channel的資訊(已經建立的channel id 和正在建立中的channel id),主要是為了Fabric的多channel機制服務的;
(4)historyLeveldb:資料庫記憶體儲的是智慧合約中寫入的key的歷史記錄的索引地址。
Hyperledger Fabric賬本詳解
這一部分作者會結合幾個問題對Hyperledger Fabric 區塊鏈賬本的內在原理進行深入的剖析,其中會涉及到原始碼部分的解析,如果讀者只是想對賬本機制做一些簡單瞭解,可以跳過原始碼分析的部分。
Q3: 智慧合約讀寫資料到stateLeveldb的流程?
A3:作者以GetState()介面為例進行說明:
首先,讀者們需要明確Hyperledger Fabric的智慧合約是在docker容器中執行的,而stateLeveldb是位於節點伺服器本地的。同時,Fabric沒有將stateLevelDB對映到docker容器中,所以智慧合約不能直接訪問stateLevelDB。
Fabric中是通過rpc服務呼叫的方式,來解決這個問題的。開發過Fabric智慧合約的讀者應該清楚,Fabric提供了一箇中間層Shim包,同時在Peer節點中預設會開啟一個grpc服務 ChainCodeService。智慧合約檔案在編譯的時候會自動引入shim包,而shim包中在GetState的過程中會向節點的ChainCodeService傳送請求到節點,然後節點再從本地的stateLevelDB中讀取請求的資料,返回給智慧合約。
流程圖:
以上只是讀資料的過程,寫資料遠比讀資料更為複雜。
//TODO 寫資料過程
Q4:fabric支援交易的查詢介面,那麼交易查詢是如何實現的?
A4: 交易查詢與Block查詢的實現原理一致,節點在寫入賬本到本地的時候,會將交易在FileSystem中的索引也寫入indexDB中。
Q5: indexDB是levelDB,它是如何實現複雜查詢的,比如交易的範圍查詢?