1. 程式人生 > >區塊鏈基礎知識:智慧合約和微服務

區塊鏈基礎知識:智慧合約和微服務

微服務和區塊鏈

微服務與區塊鏈的智慧合約有很多共同之處。二者都獨立執行(on-chain),並通過基於訊息的通道與外界通訊(off-chain)。二者的體積都很小,開發者希望他們都自主地、獨立地執行,而且當佈署在去中心化的網路上時,表現更好。

本文主要闡述使用微服務架構構建區塊鏈應用的設計原則,及程式碼示例。涉及到:

  • 微服務架構設計
  • 去中心化的領域驅動設計
  • 事件源與CQRS
  • 分散式交易
  • 非同步訊息

微服務完美地體現了 Unix 哲學的精神:只做一件事,並且把它做好。微服務是一個獨立的,可佈署的有邊界的元件,這個元件支援基於訊息的互動通訊。基於這個前提,微服務架構是一種工程模式,這種模式用於構建高度自動化,可進化的單一功能的軟體系統。
這裡寫圖片描述

區塊鏈應用與微服務的共同之處是什麼?什麼樣的設計原則可以從微服務架構用於去中心化的世界?上面的圖表比較了微服務和智慧合約的特定設計屬性。

把區塊鏈應用設計成微服務可以帶來這些福利:

  • 可以讓多個軟體工程主動的並行執行
  • 減少軟體開發團隊與測試團隊之間的依賴
  • 支援多種技術,語言和框架
  • 可以隨手丟棄的程式碼能提升創新的容易性

微服務通常使用一種通用的語言API與外界通訊,一般是JSON或SOAP。r提供一種基於訊息的共通,來跨越不同技術(.NET,Java,Node.js等)和不同平臺(Windows,Linux)。

從微服務到到去中心化應用

如果你知道DevOps的話,它把伺服器當成畜牲而不是寵物來對待。你也許會用同樣的方式對待你的程式碼,簡單地可丟棄的程式碼可以減少技術債。通過優化基礎架構來提升工程流程的現代化水平,減少運營成本(比如放進容器或者完全使用無服務配置)。

用微服務架構原則設計區塊鏈應用還可以有業務福利。通過減少基礎設施的成本及擴能相關的風險可以改進軟體系統的效率。這些方面對於私有的區塊鏈有特殊的價值,對於業務來說成本和服務的持續性是關鍵需求。

微服務架構原則還支援使用可替換的元件,以減少技術債。Solidity,Ethereum 中用於智慧合約的程式語言,有一種機制可以用於為每一個執行的合約指定準確的執行時版本。隨著時間的推移,智慧合約的版本號可以用於識別過時的區塊鏈,之前的程式碼可以被替換了。要注意在區塊鏈中,已經被處理的智慧合約(也就是說,這個智慧合約是被挖到的礦的區塊的一部分)可以被刪除掉,必須釋出一個新版本的智慧合約用於將來的交易。

另一個好處是可以更好的擴充套件執行時,讓軟體系統可以按需調整。以微服務實現的智慧合約可以讓區塊鏈在私有的業務中以更靈活的方式分發用於挖礦交易的負載。

在最基礎的層面,微服務架構把應用打散成更小的部分並獲得釋出式安裝的福利。同樣地,運行於區塊鏈的智慧合約從分散式的P2P網路中獲利。使用面向微服務架構設計的智慧合約可以有效的、可擴充套件的、可管理的、交付。

去中心化的領域驅動設計

寫一個用於去中心的區塊鏈數字賬本應用,就需要一個典型的分散式系統,比如獨立儲存,非同步訊息及分散式交易。區塊鏈應用還需要使用者和裝置的驗證,以及驗證執行智慧合約中特定的動作。擴充套件到流行的DDD(領域驅動設計)方式,我指的是在區塊鏈上實踐,即 DDDD(去中心化的領域驅動設計)。

我們來設計這個領域,或者說一個數字賬本中執行的智慧合約的上下文。智慧合約把區塊鏈應用的業務邏輯表示成工作流,由一個訊息傳送者(使用者或執行合約函式的裝置)和一個狀態(合約的引數)區分工作流的每一個環節,其他實體(使用者或裝置)可能會被執行智慧合約中的函式所影響。

這裡寫圖片描述
每個合約包含:

  • 角色
  • 狀態
  • 要執行的函式
  • 業務流程的執行

在本文中,第一個建立合約的應用角色叫做 發起者(initiator)。合約的內部狀態發生變化時,會觸發一個事件,發訊號給智慧合約的其他部分,或者執行應用。這是一個典型的模式,用於丟擲 off-chain 資料,使用服務匯流排處理智慧合約的事件,然後分佈訊息給接收者。
上圖區分了一個智慧合約的工作流中引用的實體。

Ethereum 使用 Solidity 作為程式語言,編寫自制的用於智慧合約的業務邏輯,智慧合約在 Solidity 中類似面嚮物件語言中的類。每個合約包含角色,狀態,函式,環節以及業務流程的執行動作。

pragma solidity ^0.4.20;

contract Betting
{
    //Roles
    address public Gamler;
    address public Bookmaker;

    //State
    enum BetType { Placed, Won, Lost }
    BetType public State;

    //Properties
    uint public BetAmount;
}

上同的程式碼段展示了 Solidity 中宣告的不同型別的變數,可以用於賭博應用。角色(遊戲者,發牌者)被定義為地址,是Ethereum中的使用者或合約的唯一識別符號。狀態 是一個列舉標記,用於區分當前的賭局狀態。函式,後面會介紹,定義狀態的變化。賭資額用一個uint數字表示(目前Solidity不支援decimal值)。

要注意Solidity的版本宣告,是0.4.20,這是為了避免與以後版本的Solidity編譯器產生相容問題。還可以區分智慧合約中的老程式碼,這些老程式碼可能需要更新。從區塊鏈中移除現存智慧合約的過程叫做“自銷燬”。
一個智慧合約應該是單一職責並且儘可能只包含最少的業務邏輯。在這個賭博應用中,一個智慧合約可以給賭徒暴露一些函式用於放置一個賭局,然後發牌者來判定輸或贏。貨幣可以在兩個角色之間交換,這也是賭博工作流的一部分。需要用合約驗證的貨幣交易的常用模式一般可以看作兩個階段。如下圖:
這裡寫圖片描述
發牌者(合約的發起者)下一定量的注,然後被儲存進智慧合約,如果這一注贏了,由發牌者標記為贏,然後賭資轉移給賭徒,否則,發牌者就收回賭資。

constructor() public{
    Gambler = msg.sender;
}

function Bet(uint amount) public payable{
    require(msg.sender == Gambler, "Only a gambler can place a bet.");
    require(amout > 0, "Amount should be greater than zero.");

    BetAmount = amount;
    address(this).transfer(amount);

    State = BetType.Placed;
}

function Bet(uint amount) public payable{
    require(msg.sender != Gambler, "Only the bookmaker can mark a bet as won.");
    require(amout > 0, "Amount should be greater than zero.");

    Gambler.transfer(amount);
    Close(BetType.Won);
}

function Lost() public payable{
    require(msg.sender != Gambler, "Only the bookmaker can mark a bet as won.");

    Bookmaker = msg.sender;
    Bookmaker.transfer(BetAmount);
    Close(BetType.Lost);
}

function Close(BetType state) internal{
    Gambler = 0x0;
    BetAmount = 0;
    State = state;
}

如上面的程式碼所示,這個用Solidity實現的智慧合約需要為工作流中的每個動作定義幾個函式:
- 建構函式把訊息傳送者儲存為發牌者,這是智慧合約的初創者。
- Bet 函式,接收賭資作為輸入,執行一些驗證(這個函式只能被賭徒呼叫,且賭資必須大於0)。然後把賭資轉移給合約。因為on-chain貨幣轉移的要求,函式需要標記為 payable。
- Won 函式,驗證了呼叫者不是發牌者之後,把贏得的賭資轉移給賭徒,然後關閉賭局,標記為“Won”。
- Lost 函式,只能被髮牌者呼叫,轉移最初的賭徒輸掉的賭資給發牌者,然後關閉賭局,標記為“Lost”。
- 關閉賭局後,賭徒被移除(address被設定成0x0),賭資也設定成0,準備下一局。

雖然實現比較簡單,但這個場景實現了一個典型的區塊鏈應用中管理錢的消費的模式。其他場景中,可能需要使用證明檔案,比如文件,表格,證書,或者圖片。由於諸多原因,主要是儲存的限制,把檔案放入區塊鏈是不合適的。通用的作法是執行一個加密演算法的雜湊計算(比如SHA-256),然後把這個雜湊值分享到分散式賬本中,然後外部系統來儲存這個檔案。

後面什麼時候執行這個雜湊演算法都會得到同樣的結果,除非這個檔案被改了,哪怕是一個畫素。這個流程授予了一個存證(proof of existence),可以是郵件,檔案,文件,通話記錄或者視訊。也授予了身份證明(proof of authenticity),你知道一個數字資產沒有改變過,因為數字賬本是不可變的,獨立的,所有交易的可驗證記錄。

事件來源與CQRS

之前說過,建議讓智慧合約只有單一職能,所以對於智慧合約來說,面向職能的設計是一項重要的技能。多個智慧合約可能作業系統領域中的同一個資料模型,而執行是獨立的。比如,在一個應用中,可能有一個智慧合約用於管理賭局,另一個管理體育事件,賭局的合約可能引用體育事件,建立一個二者之間的依賴是不可能的。有辦法生成一個模型來幫助避免在多個智慧合約之間共享資料嗎?

不管我們用什麼格式的資料(SQL,NoSQL,JSON),我們都會用CRUD(增刪改查)操作來實現資料模型。這裡不儲存領域狀態的資料結構,我們可以儲存那些導致當前狀態的事件。這種模式方法叫做“事件源”(Event Sourceing)https://bit.ly/2068nrt

Event Sourceing 全都是關於儲存fact的,fact是一個事件發生的代表值。就像生活中,我們無法回到過去也不能改變過去,我們只能做在當下來補償過去。資料是不可變的:所以我們永遠會發布新的命令或事件來補償,而不是更新一個實體的狀態。這個方式也叫做 CRAB(Create,Retrieve,Append,Burn)(https://bit.ly/2MbpUOb),這也是一個區塊鏈允許的操作:沒有資料的更新的刪除,只能附加到鏈上。從一個區塊鏈中刪除東西與它的不變性有衝突,但是你可以通過“burning”接收者的地址來停止轉移資產。

這種方式有一個疑慮:效能。如果任何狀態值是事件的函式,你可以假設每次訪問這個值都需要重新計算當前源事件的狀態。明顯,會很慢。在事件源中,你可以避免這樣高成本的操作,可以使用一個叫做 回滾快照(實體狀態在某個時間點的對映)的方式。比如,銀行會在月末預先計算你的銀行賬戶餘額,這樣你就不需要計算從開戶時的所有貸款和信用記錄。
這裡寫圖片描述

上圖說明了用於賭局應用的結構化資料模型,這有時也叫:雪花模型,因為每個實體(一個數據表)都與其他的不同。

這個結構化資料只儲存當前系統的狀態,而事件源方式儲存單獨的fact。事件源中的狀態,是所有發生的相關fact的函式。

為了更遠的推動職責單一,CQRS(Command Query Responsibility Segregation)補充事件源作一種設計模式用於資料儲存。CQRS鼓勵有效的單一職責以及微服務的可佈署性。意思是你可以(應該)分離資料的更新和查詢到單獨的模型中。

當使用CQRS時,需要跨上下文訪問資料的形式會被淘汰。智慧合約可以擁有和封裝任何對模型狀態的更新和狀態變化的上報事件。通過訂閱這些事件的通知,一個單獨的智慧合約可以構建一個完整的、獨立的、優化了查詢的模型,不需要與其他合約或外部服務共享。有關CQRS的介紹,可以看 Martin Fowler的部落格 bit.ly/2Awoz33
這裡寫圖片描述

上圖描述了我設計的用於賭局應用的基於事件源的資料模型。這個簡單的模型使用一個不考慮事件處理的相似結構。不需要知道賭局的當前狀態,來讀取事件的序列。這個事件的資料結構依賴於事件自身。雖然狀態序列在工作流中存在,但是從資料模型的角度來說是不相干的。

分散式交易

在多個智慧合約之間共享資料模型並不是唯一導致緊耦合的使用場景,另一個威脅是工作流。很多真實生活中的流程不可能用單一的原子的操作來表示。在RDBMS中,事務中的一個步驟失敗了,整個事務就失敗了。

對於分散式工作流和無狀態的微服務,傳統的事務是用資料鎖和原子性、一致性、獨立性、永續性(ACID)的約束來實現是不切實際的。

Sages(bit.ly/2AzdKNR)是一個長期存在的分散式交易系統。這個工作流中的每一步執行一部分工作,用一個叫做 “routing slip” 的訊息給“修正事務”註冊一個備用,然後傳遞並更新訊息到活動鏈上。如果後面的步驟失敗了,這一步驟檢視“routing slip”,然後呼叫最近的“修正事務”,回傳給“routing slip”。上一步也做同樣的事情,呼叫其前任的“修正事務”,就這樣下去,直到所有的事務都被修正。這種模式最終會導致分散式交易中資料的一致性。Sagas是非常適合微服務架構的,當然也適用於區塊鏈智慧合約。

你可以用Solidity通過備用函式實現一種“routing slip”。備用函式是一個無名函式,無入參,也無返回值。當呼叫這個合約的函式時,如果沒有其他函式符合給定的函式定義,或者當合約收到“以太坊”時,就呼叫這個函式。為了能收到“以太坊”,這個函式必須宣告為payable,否則就不能通過正規的(address 到 address)的交易方式收到以太坊。

值得注意的是,不帶 payable 的備用函式可以收到“以太坊” 作為挖礦交易的回執,比如挖礦的區塊獎勵。合約不能拒絕這種轉移,這是“以太坊”的設計,而Solidity不能克服。一個合約可以有一個無名函式,如下:

// Fallback function
function() public payable{
    emit AmountTransfered(msg.sender);
}

event AmountTransfered(address sender);

在“以太坊”中,備用函式是智慧合約需要的,以實現賬戶-賬戶的直接轉移。