1. 程式人生 > >區塊鏈在中國(1):IBM HyperLedger fabric

區塊鏈在中國(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 的本地備份,不做修改,函式有:

  1. GetBlockchainSize() (uint64, error),這個函式在原始碼裡常見,返回了ledger的長度
  2. GetBlock(id uint64) (block *pb.Block, err error)
  3. 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:

  1. Bob成為了chaincode的管理員
  2. Bob想要將資產‘Picasso’分配給Charlie
  3. Bob會獲得Charlie的一個TCert,我們叫這個證書CharlieCert
  4. Bob構建一個invoke交易,來呼叫assign這個函式,引數是 (‘Picasso’, Base64(DER(CharlieCert)))
  5. 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節點並沒有完成一開始的同步。