前言
現在很多人都在從事區塊鏈方面的研究,作者也一直在基於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,它是如何實現復雜查詢的,比如交易的范圍查詢?
未完待續... ...