區塊鏈在中國(1):IBM HyperLedger fabric
在我看來,比特幣就是現實中的V字仇殺隊,當然現實是更殘酷的世界政府,這場博弈關乎著人類文明、政治、社會屬性、經濟和人權。
IBM HyperLeger 又叫 fabric,你可以把它想象成一個由全社會來共同維護的一個超級賬本,沒有中心機構擁攬權力,你的每一筆交易都是全網公開且安全的,信用由全社會共同見證。它與Bitcoin的關係就是,你可以利用fabric構建出一個叫Bitcoin的應用來幫助你change the world。
願景是那麼的牛X,貌似正合我們想改變世界的胃口,但是在殘酷的現實和世介面前我們永遠是天真幼稚的,blockchain需要一步一步腳印來構建它的巨集偉藍圖,起碼目前是沒有將它用於工業生產和國家經濟的案例的。
fabric源於IBM,初衷為了服務於工業生產,IBM將44,000行程式碼開源,是了不起的貢獻,讓我們可以有機會如此近的去探究區塊鏈的原理,但畢竟IBM是從自身利益和客戶利益出發的,並不是毫無目的的去做這項公益事業,我們在看fabric的同時要有一種審慎的思維:區塊鏈不一定非得這樣,它跟比特幣最本質的非技術區別在哪裡。我們先來大致瞭解一下fabric的關鍵術語(因為一些詞彙用英文更準確,我就不硬翻譯了)。
1. Terminology
- Transaction
它一條request,用來在ledger上執行一個function,這個function是用chaincode來實現的
- Transactor
發出transaction的實體,比如它可能是一個客戶端應用
- Ledger
Legder可以理解為一串經過加密的block鏈條,每一個block包含著transactions和當前world state等資訊
- World State
world state是一組變數的集合,包含著transactions的執行結果
- Chaincode
這是一段應用層面的程式碼(又叫smart contract,智慧合約),它儲存在ledger上,作為transaction的一部分。也就是說chaincode來執行transaction,然後執行結果可能會修改world state
- Validating Peer
參與者之一,它是一種在網路裡負責執行一致性協議、確認交易和維護賬本的計算機節點
- Nonvalidating Peer
它相當於一個代理節點,用來連線transactor和鄰近的VP(Validating Peer)節點。一個NVP節點不會去執行transactions但是回去驗證它們。同時它也會承擔起事件流server和提供REST services的角色
- Permissioned Ledger
這是一個要求每一個實體和節點都要成為網路成員的blockchain網路,所有匿名節點都不被允許連線
- Privacy
用來保護和隱蔽chain transactors的身份,當網路成員要檢查交易時,如果沒有特權的話,是無法追蹤到交易的transactor
- Confidentiality
這個特性使得交易內容不是對所有人可見,只開放給利益相關者
- Auditability
將blockchain用於商業用途需要遵守規則,方便監管者調查交易記錄
2. Architecture
架構核心邏輯有三條:Membership、Blockchain和Chaincode。
2.1 Membership Services
這項服務用來管理節點身份、隱私、confidentiality 和 auditability。在一個 non-permissioned的區塊鏈網路裡,參與者不要求授權,所有的節點被視作一樣,都可以去submit一個transaction,去把這些交易存到區塊(blocks)中。那Membership Service是要將一個 non-permissioned的區塊鏈網路變成一個permissioned的區塊鏈網路,憑藉著Public Key Infrastructure (PKI)、去中心和一致性。
2.2 Blockchain Services
Blockchain services使用建立在HTTP/2上的P2P協議來管理分散式賬本。提供最有效的雜湊演算法來維護world state的副本。採取可插拔的方式來根據具體需求來設定共識協議,比如PBFT,Raft,PoW和PoS等等。
2.3 Chaincode Services
Chaincode services 會提供一種安全且輕量級的沙盒執行模式,來在VP節點上執行chaincode邏輯。這裡使用container環境,裡面的base映象都是經過簽名驗證的安全映象,包括OS層和開發chaincode的語言、runtime和SDK層,目前支援Go、Jave和Nodejs開發語言。
2.4 Events
在blockchain網路裡,VP節點和chaincode會發送events來觸發一些監聽動作。比如chaincode是使用者程式碼,它可以產生使用者事件。
2.5 API 和 CLI
提供REST API,允許註冊使用者、查詢blockchain和傳送transactions。一些針對chaincode的API,可以用來執行transactions和查詢交易結果。對於開發者,可以通過CLI快速去測試chaincode,或者去查詢交易狀態。
3. Topology
分散式網路的拓撲結構是非常值得研究的。在這個世界裡散佈著眾多參與者,不同角色,不同利益體,各種各樣的情況處理象徵著分散式網路裡的規則和法律,無規則不成方圓。在區塊鏈網路裡,有Membership service,有VP節點,NVP節點,一個或多個應用,它們形成一個chain,然後會有多個chain,每一個chain都有各自的安全要求和操作需求。
3.1 單個VP節點網路
最簡單的網路就是隻包含一個VP節點,因此就省去了共識部分。
3.2 多個VP節點網路
多個VP和NVP參與的網路才是有價值和實際意義的。NVP節點分擔VP節點的工作壓力,承擔處理API請求和events的工作。
而對於VP節點,VP節點間會組成一個網狀網路來傳播資訊。一個NVP節點如果被允許的話可以與鄰近的一個VP節點相連。NVP節點是可以省略的,如果Application可以直接和VP節點通訊。
3.3 Multichain
還會存在一個網路裡多條chain的情況,各個chain的意圖不一樣。
4. Protocol
fabric是用gRPC來做P2P通訊的,是一個雙向流訊息傳遞。使用 Protocol Buffer來序列化要傳遞的資料結構。
4.1 Message
message分四種:Discovery,Transaction,Synchronization 和 Consensus。每一種資訊下還會包含更多的子資訊,由payload指出。
payload是一個不透明的位元組陣列,它包含著一些物件,比如 Transaction 或者 Response。例如,如果 type 是 CHAIN_TRANSACTION,那麼 payload 就是一個 Transaction的物件。
message Message {
enum Type {
UNDEFINED = 0;
DISC_HELLO = 1;
DISC_DISCONNECT = 2;
DISC_GET_PEERS = 3;
DISC_PEERS = 4;
DISC_NEWMSG = 5;
CHAIN_STATUS = 6;
CHAIN_TRANSACTION = 7;
CHAIN_GET_TRANSACTIONS = 8;
CHAIN_QUERY = 9;
SYNC_GET_BLOCKS = 11;
SYNC_BLOCKS = 12;
SYNC_BLOCK_ADDED = 13;
SYNC_STATE_GET_SNAPSHOT = 14;
SYNC_STATE_SNAPSHOT = 15;
SYNC_STATE_GET_DELTAS = 16;
SYNC_STATE_DELTAS = 17;
RESPONSE = 20;
CONSENSUS = 21;
}
Type type = 1;
bytes payload = 2;
google.protobuf.Timestamp timestamp = 3;
}
4.1.1 Discovery Messages
一個新啟動的節點,如果CORE_PEER_DISCOVERY_ROOTNODE(ROOTNODE是指網路中其它任意一個節點的IP)被指定了,它就會開始執行discovery協議。而ROOTNODE就作為最一開始的發現節點,然後通過ROOTNODE節點進而發現全網中所有的節點。discovery協議資訊是DISC_HELLO,它的payload是一個HelloMessage物件,同時包含資訊傳送節點的資訊:
message HelloMessage {
PeerEndpoint peerEndpoint = 1;
uint64 blockNumber = 2;
}
message PeerEndpoint {
PeerID ID = 1;
string address = 2;
enum Type {
UNDEFINED = 0;
VALIDATOR = 1;
NON_VALIDATOR = 2;
}
Type type = 3;
bytes pkiID = 4;
}
message PeerID {
string name = 1;
}
屬性 | 含義 |
---|---|
PeerID | 在啟動之初定義的或者在配置檔案中定義的該節點的名字 |
PeerEndpoint | 描述該節點,並判斷是否是NVP和VP節點 |
pkiID | 該節點的加密ID |
address | ip:port |
blockNumber | 該節點目前擁有的blockchain的高度 |
如果一個節點接收到DISC_HELLO資訊,發現裡面的block height高於自己目前的block height,它會立即傳送一個同步協議來與全網同步自己的狀態(mark:但是在原始碼層面似乎並沒有實現同步這個邏輯)。
在這個剛加入節點完成DISC_HELLO這輪訊息傳遞後,接下來回週期性的傳送DISC_GET_PEERS來發現其它加入網路中的節點。為了回覆DISC_GET_PEERS,一個節點會發送DISC_PEERS。
4.1.2 Synchronization Messages
Synchronization 協議是接著上面所說的discovery協議開始的,當一個節點發現它的block的狀態跟其它節點不一致時,就會觸發同步。該節點會廣播(broadcast)三種資訊:SYNC_GET_BLOCKS , SYNC_STATE_GET_SNAPSHOT 或者
SYNC_STATE_GET_DELTAS,同時對應接收到三種資訊:SYNC_BLOCKS , SYNC_STATE_SNAPSHOT 或者 SYNC_STATE_DELTAS。
目前fabric嵌入的共識演算法是pbft。
SYNC_GET_BLOCKS 會請求一系列連續的block,傳送的資料結構中payload將是一個SyncBlockRange物件。
message SyncBlockRange {
uint64 correlationId = 1;
uint64 start = 2;
uint64 end = 3;
}
接收的節點會回覆SYNC_BLOCKS,它的payload是一個SyncBlocks物件:
message SyncBlocks {
SyncBlockRange range = 1;
repeated Block blocks = 2;
}
start和end表示起始和結束的block。例如start=3, end=5,代表了block 3,4,5;start=5, end=3,代表了block 5,4,3。
SYNC_STATE_GET_SNAPSHOT 會請求當前world state的一個snapshot,該資訊的payload是一個SyncStateSnapshotRequest物件:
message SyncStateSnapshotRequest {
uint64 correlationId = 1;
}
correlationId是發出請求的peer用來追蹤對應的該資訊的回覆。收到該訊息的peer會回覆SYNC_STATE_SNAPSHOT,它的payload是一個SyncStateSnapshot物件:
message SyncStateSnapshot {
bytes delta = 1;
uint64 sequence = 2;
uint64 blockNumber = 3;
SyncStateSnapshotRequest request = 4;
}
SYNC_STATE_GET_DELTAS 預設Ledger會包含500個transition deltas。delta(j)表示block(i)和block(j)之間的狀態轉變(i = j -1)。
4.1.3 Consensus Messages
Consensus framework會將接收到的CHAIN_TRANSACTION轉變成CONSENSUS,然後廣播給所有的VP節點。
4.1.4 Transaction Messages
在fabric中的交易有三種:Deploy, Invoke 和 Query。Deploy將指定的chaincode安裝到chain上,Invoke和Query會呼叫已經部署好的chaincode的函式。
4.2 Ledger
Ledger主要包含兩塊:blockchain和world state。blockchain就是一系列連在一起的block,用來記錄歷史交易。world state是一個key-value資料庫,當交易執行後,chaincode會將state存在裡面。
4.2.1 Blockchain
blockchain是指由一些block連成的list,每一個block都包含上一個block的hash。一個block還會包含一些交易列表以及執行所有這些交易後world state的一個hash。
message Block {
version = 1;
google.protobuf.Timestamp timestamp = 2;
bytes transactionsHash = 3;
bytes stateHash = 4;
bytes previousBlockHash = 5;
bytes consensusMetadata = 6;
NonHashData nonHashData = 7;
}
message BlockTransactions {
repeated Transaction transactions = 1;
}
那上一個block的hash是如何計算的呢:
- 用 protocol buffer 序列化block的資訊
- 用 SHA3 SHAKE256 演算法將序列化後的block資訊雜湊成一個512位元組的輸出
上面的資料結構中有一個 transactionHash, 它是transaction merkle tree的根節點(用默克爾樹來描述這些交易)。
4.2.2 World State
一個peer的world state是所有部署的chaincodes的狀態(state)的集合。一個chaincode的狀態由鍵值對(key-value)的集合來描述。我們期望網路裡的節點擁有一致的world state,所以會通過計算world state的 crypto-hash 來進行比較,但是將會消耗比較昂貴的算力,為此我們需要設計一個高效率的計算方法。比如引入Bucket-tree來實現world state的組織。
world state中的key的表示為{chaincodeID, ckey},我們可以這樣來描述key, key = chaincodeID+nil+cKey。
world state的key-value會存到一個hash表中,這個hash表有預先定義好數量(numBuckets)的buckets組成。一個 hash function 會來定義哪個桶包含哪個key。這些buckets都將作為merkle-tree的葉子節點,編號最小的bucket作為這個merkle-tree最左面的葉子節點。倒數第二層的構建,從左開始每maxGroupingAtEachLevel(預先定義好數量)這麼多的葉子節點為一組聚在一起,形成N組,每一組都會插入一個節點作為所包含葉子節點的父節點,這樣就形成了倒數第二層。要注意的是,最末層的父節點(就是剛剛描述的插入的節點)可能會有少於maxGroupingAtEachLevel的孩子節點。按照這樣的方法不斷構建更高一層,直到根節點被構建出來。
舉一個例子,{numBuckets=10009 and maxGroupingAtEachLevel=10},它形成的tree的每一次包含的節點數目如下:
Level | Number of nodes |
---|---|
0 | 1 |
1 | 2 |
2 | 11 |
3 | 101 |
4 | 1001 |
5 | 10009 |
4.3 Consensus Framework
consensus framework包含了三個package:consensus、controller和helper。
- consensus.Communicator用來發送訊息給其他的VP節點
- consensus.Executor用於交易的啟動、執行和回滾,還有preview、commit
- controller指定被VP節點使用的consensus plugin
- helper用來幫助consensus plugin與stack互動,例如維護message handler
目前有兩個consensus plugin:pbft和noops。
pbft是 微軟論文PBFT共識演算法的一個實現。
noops 用於開發和測試,它沒有共識機制,但是會處理所有consensus message,所以如果要開發自己的consensus plugin,從它開始吧。
4.3.1 Executor 介面
在原始碼中我們會經常看 executor 相關的程式碼,這個藉口下的方法可以做到:
開始批量交易、執行交易、提交與回滾交易
4.3.2 Ledger 介面
type Ledger interface {
ReadOnlyLedger
UtilLedger
WritableLedger
}
ReadOnlyLedger介面用來查詢 ledger 的本地備份,不做修改,函式有:
- GetBlockchainSize() (uint64, error),這個函式在原始碼裡常見,返回了ledger的長度
- GetBlock(id uint64) (block *pb.Block, err error)
- GetCurrentStateHash() (stateHash []byte, err error),返回 ledger 當前狀態的hash
4.3.3 helper 包
helper包可以幫助VP節點建立與其他peer之間的通訊和訊息處理,helper.HandleMessage,這個函式會處理四種訊息型別,
pb.Message_CONSENSUS
pb.Message_CHAIN_TRANSACTION
pb.Message_CHAIN_QUERY
others
4.4 Chaincode
chaincode是一段應用級的程式碼,交易邏輯就在裡面,fabric是用Docker容器來執行chaincode的。一旦chaincode容器被啟動,它就會通過gRPC與啟動這個chaincode的VP(Validating Peer)節點連線。
上面4.1提到的四種訊息中有一種叫transaction message,包含Deploy, Invoke 和 Query。指的就是與chaincode相關的交易資訊。chaincode需要實現三個函式,Init,Invoke 和 Query。Init是建構函式,它只在部署交易時被執行,Query函式用來讀取狀態。Invoke來進行交易的發生。
chaincode容器被部署時,會向對應的peer進行註冊,註冊之後,VP節點就會通知chaincode容器呼叫Init函式。其實peer跟chaincode容器之間是隔著一個shim層的,chaincode容器的shim層會接收來自peer的資訊,根據資訊呼叫chaincode相應的函式,如Invoke。
5. What we can do
5.1 Asset Management 資產管理
這是一個在fabric上實現的一個chaincode demo,用來模擬數字資產的管理。chaincode一共有四個函式:init(user), assign(asset, user), transfer(asset, user), query(asset)。
在chaincode被部署時,init(user)就會被自動呼叫。設想一下,
1. Alice是這個chaincode的部署者;
2. Alice想要將管理員這個角色分配給Bob;
3. 之後Alice會獲得Bob的一個TCert,我們叫這個證書BobCert;
4. Alice構建一條deploy交易,並將交易的元資料設定到BobCert;
5. Alice將這個交易提交到fabric網路中。
這樣Bob就會被賦予管理員角色,這就是init函式要做的。接下來看一下assign:
- Bob成為了chaincode的管理員
- Bob想要將資產‘Picasso’分配給Charlie
- Bob會獲得Charlie的一個TCert,我們叫這個證書CharlieCert
- Bob構建一個invoke交易,來呼叫assign這個函式,引數是 (‘Picasso’, Base64(DER(CharlieCert)))
- Bob提交這個交易到fabric網路中
transfer函式:
1. Charlie成為了資產‘Picasso’的擁有者了
2. Charlie想要將‘Picasso’的所有權轉交給Dave
3. Charlie獲得Dave的一個TCert,我們叫這個證書為DaveCert
4. Charlie構建一個invoke交易,來呼叫transfer函式,引數為(‘Picasso’, Base64(DER(DaveCert)))
5. Charlie提交交易到fabric網路中
query函式用來查詢資產的擁有者。
完成整套邏輯,需要我們寫的chaincode的程式碼只有三百行。像在transfer的實現中,我們需要首先判斷這個發起人的身份,確保只有資產所有者才能轉移自己的資產,然後全網公證資產的轉移,任何一方都無法篡改和抵賴。
6. Defect
其實fabric還存在著諸多的缺陷,畢竟目前還是一個襁褓中的嬰兒。
例如memberserice與現有CA系統的整合,資料庫部分也欠缺。
其實這裡有一個開放性的命題,大家不妨一起想想,可以在部落格下面的評論中留言,或許會碰撞出一些火花:VP(validating peer)節點是網路的實質性參與者,可以提出交易,並就交易達成一致,然後執行交易,但在fabric中有一個節點叫NVP(not-validating peer)節點,它只與某一個VP節點相連,不能參與交易執行和一致性達成,只能為它所連的VP節點分擔API處理部分和事件部分的壓力,但可以去查詢網路產生的ledger,有人說這樣的設計可以有助於監管者加入進來,監管者只需查詢生成的ledger,而不需參與交易,也有人說NVP節點引入是為了降低VP節點的計算壓力,將一些外圍的操作讓NVP節點來做。
7. Contribution
Implement SYNC_BLOCK_ADDED handler
我的一個同事實現了SYNC_BLOCK_ADDED訊息的handler,這樣在noops共識模式下,當一個block被加到(mined/added)ledger時,NVP節點就可以處理這條訊息了,並將最新加入的block存在它自己的ledger中。
SYNC_BLOCK_ADDED message 對應的callback是beforeBlockAdded(core/peer/handler.go),官方程式碼如下:
func (d *Handler) beforeBlockAdded(e *fsm.Event) {
peerLogger.Debugf("Received message: %s", e.Event)
msg, ok := e.Args[0].(*pb.Message)
if !ok {
e.Cancel(fmt.Errorf("Received unexpected message type"))
return
}
// Add the block and any delta state to the ledger
_ = msg
}
這裡並沒有去獲取和處理block的資訊,我們需要加入如下:
+ if ValidatorEnabled() {
+ e.Cancel(fmt.Errorf("VP shouldn't receive SYNC_BLOCK_ADDED"))
+ return
+ }
// Add the block and any delta state to the ledger
- _ = msg
+ blockState := &pb.BlockState{}
+ err := proto.Unmarshal(msg.Payload, blockState)
+ if err != nil {
+ e.Cancel(fmt.Errorf("Error unmarshalling BlockState: %s", err))
+ return
+ }
+ coord := d.Coordinator
+ blockHeight := coord.GetBlockchainSize()
+ if blockHeight <= 0 {
+ e.Cancel(fmt.Errorf("No genesis block is made"))
+ return
+ }
+ curBlock, err := coord.GetBlockByNumber(blockHeight -1)
+ if err != nil {
+ e.Cancel(fmt.Errorf("Error fetching block #%d, %s", blockHeight -1, err))
+ return
+ }
+ hash, err := curBlock.GetHash()
+ if err != nil {
+ e.Cancel(fmt.Errorf("Error hashing latest block"))
+ return
+ }
+ if bytes.Compare(hash, blockState.Block.PreviousBlockHash) != 0 {
+ e.Cancel(fmt.Errorf("PreviousBlockHash of received block doesnot match hash of current block"))
+ return
+ }
+ coord.PutBlock(blockHeight, blockState.Block)
+ delta := &statemgmt.StateDelta{}
+ if err := delta.Unmarshal(blockState.StateDelta); nil != err {
+ e.Cancel(fmt.Errorf("Received a corrupt state delta"))
+ return
+ }
+ coord.ApplyStateDelta(msg, delta)
+ if coord.CommitStateDelta(msg) != nil {
+ e.Cancel(fmt.Errorf("Played state forward, hashes matched, but failed to commit, invalidated state"))
+ return
+ }
+ peerLogger.Infof("Blockchain height grows into %d", coord.GetBlockchainSize())
Enable statetransfer for HELLO message
我們還發現當一個NVP節點剛加入網路時,它會發送一個DISC_HELLO message,隨後從其他節點接收一個包含那個節點的blockchain資訊的DISC_HELLO message,不過官方程式碼並沒有給出NVP依據這些返回資訊同步自己狀態的實現。NVP正在網路中實施自己的狀態同步時,一個新的block被mine,NVP卻不能把這個新的block加入到自己的chain中。所以目前就出現了一個棘手的情況:當新的NVP節點剛加入網路時,通過HELLO message獲取其他節點的blockchain資訊開始同步自己的狀態,這肯定需要一定的時間來完成,但與此同時,網路裡的交易還在繼續,新的block會被不斷的mined,雖然NVP能接收到SYNC_BLOCK_ADDED,並擁有處理它的handler,但是這時候卻不能將新的block資訊加入到自己的chain中,因為hash不匹配,畢竟NVP節點並沒有完成一開始的同步。