天天看點

淺談以太坊智能合約的設計模式與更新方法淺談以太坊智能合約的設計模式與更新方法

淺談以太坊智能合約的設計模式與更新方法

作者:fisco-dev  https://github.com/FISCO-BCOS/Wiki

    • 1. 最佳實踐
    • 2. 實用設計案例
      • 2.1 控制器合約與資料合約: 1->1
      • 2.2 控制器合約與資料合約: 1->N
      • 2.3 控制器合約與資料合約: N->1
      • 2.4 控制器合約與資料合約: N->N
      • 2.5 總結
    • 3. 更新
      • 3.1 控制器合約更新,資料合約不更新
      • 3.2 控制器合約不更新,資料合約更新
      • 3.3 控制器合約更新,資料合約更新
    • 4. 資料遷移
      • 4.1 寫死遷移法
      • 4.2 硬拷貝遷移法
      • 4.3 默克爾樹遷移法

以太坊EVM是目前區塊鍊行業應用最為廣泛的虛拟機。其所支援的智能合約語言是圖靈完備的。該語言支援各種基礎類型(Booleans,Integers,Address,String,Enum,Address等)、複雜類型(Struct,Mapping,Array等)、複雜的表達式和控制結構及接口繼承等面向對象的特性。

正是由于強大的智能合約語言,原本在真實世界中的複雜商業邏輯和應用都能在區塊鍊上輕松實作。然而需要注意的是,盡管公有鍊可以實作合理的GAS機制自我保護,聯盟鍊可以用其他機制替代GAS的計算及代币化來保障EVM沙盒安全,但由于區塊鍊運作機制的原因,智能合約的運作即使是異常運作都會在所有區塊鍊節點上獨立重複運作。是以,無論是在公有鍊還是聯盟鍊運作智能合約都是非常昂貴(運算資源、存儲資源)的操作。

另外,智能合約與傳統應用程式有一個不同的地方在于智能合約一經釋出于區塊鍊上就無法篡改,即使智能合約中有Bug需要修複或者業務邏輯變更,它也不能直接在原有的合約上直接修改再重新釋出。是以在設計之初就需要結合業務場景考慮合理的更新機制。

總而言之,智能合約實作上要達到的目标是:完備的業務功能、精悍的代碼邏輯、良好的子產品抽象、清晰的合約結構、合理的安全檢查、完備的更新方案。

智能合約的生命周期主要有設計、開發、部署、運作、更新、銷毀。在下文中主要是基于目标在設計階段、更新階段的一些梳理總結。

從業務視角來看,智能合約隻需要做兩件事,其一是如何定義資料的結構和讀寫方式,其二是如何處理資料并對外提供服務接口。

為了更好的做好子產品抽象和合約結構分層,将這兩件事分開,既是将業務控制邏輯和資料從合約代碼層面就做好分離,這樣的處理在複雜業務邏輯場景中經過實踐是目前被認為最佳的模式。

這個模式簡稱為CD(Controller-Data)模式。将合約分為兩類:控制器合約(Controller Contract)與資料合約(Data Contract)。

控制器合約通過通路資料合約獲得資料,并對資料做邏輯處理,然後寫回資料合約。�它專注于對資料的邏輯處理和對外提供服務。根據處理邏輯的不同,常見的有命名空間控制器合約、代理控制器合約、業務控制器合約、工廠控制器合約等。一般情況下,控制器合約不需要存儲任何資料,它完全依賴外部的輸入來決定對資料合約的通路。特殊情況下,控制器合約可以存儲某個固定的資料合約的位址或者命名空間(通過命名空間在運作時獲得合約位址)。

資料合約專注于資料結構定義與所存儲資料的讀寫裸接口。為了達到資料統一通路管理和資料通路權限控制的目的,最好是将資料讀寫接口隻暴露給對應的控制器合約。禁止其他方式的讀寫通路。

基于這個模式,遵循從上至下的分析方式,從對外提供的服務接口開始設計各類控制器合約,再逐漸過渡到服務接口所需要的資料模型和存儲方式,進而設計各類資料合約,可以較為快速的完成合約架構的設計。

在CD模式下,根據控制器合約與資料合約之間的操作關系,從邏輯上歸結為四類:

  1. 控制器合約與資料合約 1->1
  2. 控制器合約與資料合約 1->N
  3. 控制器合約與資料合約 N->1
  4. 控制器合約與資料合約 N->N

假設一個業務場景:将全國所有銀行的業務和資訊上鍊。

假設全國隻有兩家銀行,A銀行和B銀行。A銀行隻有存款業務,B銀行隻有取款業務。一種可能的設計是這樣的:

代理控制器合約:面向Dapp,是所有業務合約的入口,提供命名空間服務,提供了命名空間到合約位址的映射。使得Dapp對鍊上合約更新導緻的位址變更無感覺。例如,Dapp對A銀行的存款請求隻需要(“BankA",deposit,args) 即可。對B銀行的取款請求隻需要(”BankB",withdraw,args)即可。代理器控制合約實作上應該是區塊鍊底層内置的、固化的,或者是業務上極少變更的。Dapp在業務運作之前已經明确知道代理控制器合約的位址。

命名控制器合約:面向鍊上合約,提供命名空間服務,提供了命名空間到合約位址的映射。使得鍊上合約可以在運作時根據命名獲得實際的合約位址。例如,A銀行控制器合約向命名控制器合約請求(“BankA-Data"),可以獲得A銀行資料合約位址,使得A銀行控制器合約可以在運作時通路A銀行資料合約。它與代理控制器合約的主要不同在于服務對象的不同,代理控制器合約面向Dapp,命名控制器合約面向鍊上合約。另外,命名控制器合約包含有版本控制的設計(下文第3.2節介紹),可以根據需要配合灰階政策的實施。

A銀行控制器合約:提供了存款服務接口deposit。部署初始化時已經明确知道自己的身份”BankA"。并且可以在運作時通過命名控制合約獲得”BankA“的資料合約“BankA-Data"的位址。

A銀行資料合約:儲存了A銀行的目前餘額。提供add和sub接口給A銀行控制器合約來更新餘額資訊。

B銀行控制器合約:提供了存款服務接口withdraw。部署初始化時已經明确知道自己的身份”BankB"。并且可以在運作時通過命名控制合約獲得”BankB“的資料合約"BankB-Data"的位址。

B銀行資料合約:儲存了B銀行的目前餘額。提供add和sub接口給B銀行控制器合約來更新餘額資訊。

對A銀行的存款請求的流程是這樣的:

  1. Dapp指定代理控制器合約位址,請求存款交易(“BankA",deposit,money)
  2. 代理控制器合約,運作時得到”BankA"對應的A銀行控制器合約位址,并向A銀行控制器合約請求存款交易(deposit,money)
  3. A銀行控制器合約的deposit接口向命名控制器合約請求A銀行的資料合約“BankA-Data"的位址,并通路到A銀行資料合約的資料,然後執行一些存款業務邏輯。傳回結果。
  4. 依次傳回結果到Dapp。

假設全國有N家銀行,所有銀行都有存款業務和取款業務,并且業務流程都是一樣的。一種可能的設計是這樣的:

這個設計與上面的2.1不一樣的地方在于,将存款服務接口和取款接口都集中歸結到銀行業務控制器合約裡面了。這意味着任何銀行的存款和取款業務都由銀行業務控制器合約來統一處理,處理邏輯上不再區分是A銀行還是B銀行,隻是在資料通路的時候需要根據入參的不同來決定通路不同的銀行資料合約。

還有,于2.1相比,對于Dapp而言,它送出請求的時候隻需要将請求發往固定的”Bank"就可以了,不用具體關心某個銀行。

另外,由于銀行有很多個,并且它們的存儲結構都是一樣的,是以可以設計一個銀行資料合約的工廠控制器合約,來負責對新的資料合約的生成實作模闆化。

  1. Dapp指定代理控制器合約位址,請求存款交易(“Bank",deposit,”BankA“,money)
  2. 代理控制器合約,運作時得到”Bank"對應的銀行業務控制器合約位址,向銀行業務控制器合約請求存款交易(deposit,”BankA“,money)
  3. 銀行業務控制器合約的deposit接口向命名控制器合約請求A銀行的資料合約“BankA-Data"的位址,并通路到A銀行資料合約的資料,然後執行一些存款業務邏輯。傳回結果。

假設全國有N家銀行,所有銀行都有存款業務和取款業務,并且業務流程都是一樣的,但是由于業務邏輯較為複雜,出于子產品化維護的需要,需要将存款業務和取款業務做分拆。一種可能的設計是這樣的:

這個設計與上面的2.2不一樣的地方在于,将存款服務接口和取款接口拆分到了不同的業務控制器合約裡面了。這意味着不同的業務邏輯從子產品上做了清晰的切分。對于Dapp而言,它送出請求的時候需要明确指向所對應的業務接口。

  1. Dapp指定代理控制器合約位址,請求存款交易(“deposit",”BankA“,money)
  2. 代理控制器合約,運作時得到”deposit"對應的存款業務控制器合約位址,向存款業務控制器合約請求存款交易(”BankA“,money)
  3. 存款業務控制器合約的deposit接口向命名控制器合約請求A銀行的資料合約“BankA-Data"的位址,并通路到A銀行資料合約的資料,然後執行一些存款業務邏輯。傳回結果。

此類情況可以拆解為上面三種情況的組合。不再贅述。

從Dapp視角考慮,可以總結如下:

CD模式 特點
1->1 面向業務對象
1->N 面向業務流程
N->1 面向業務接口
N->N /

在CD模式下,在業務邏輯變更需要更新合約的情況下,根據控制器合約與資料合約的更新關系來劃分,可以歸納為以下三種情況:

控制器合約 資料合約
更新 不更新

在更新過程中,還需要考慮是全量更新還是灰階更新?如果是灰階更新,灰階政策是怎麼樣的?另外,在多鍊場景和單鍊場景、跨鍊場景,更新過程是否有不同?多鍊場景的灰階政策如何考慮?新舊版本資料能否共存?如果需要資料遷移,如何做到無縫遷移?

下面以最為常見的1->N 場景來介紹不同的更新情況。

如上圖所示,銀行業務控制器合約從V1更新到V2,而其他的合約和接口都是不需要更新的,假設V2版本相對V1版本隻是更新withdraw這個接口。

此時,V2版本的銀行業務控制器合約需要做的事情是:

  1. 繼承V1版本的銀行業務控制器合約。
  2. 增加一個指向V1版本的鍊上合約位址的成員變量。
  3. 增加一個withdraw開關接口,允許外部賬戶通過普通交易來操作V2版本合約的啟停灰階政策。
  4. 重載withdraw接口。更新對應的接口邏輯。并且在業務邏輯真正開始執行之前,自定義實作灰階政策(譬如灰階特定使用者,或者一定比例使用者或者其他政策)。并且需要注意的是在打開灰階開關的情況下,如果請求沒有命中灰階政策,則直接透傳參數調用V1版本的合約接口,V2版本的withdraw接口不做任何額外工作。

完成V2版本的合約工作之後,即可釋出一個普通交易,交易中的邏輯是,先部署V2版本的銀行業務控制器合約,再将其位址更新到代理控制器合約中,使得将“Bank”映射到V2版本的合約位址上。這樣控制器合約即更新完成。

如果需要回退版本,隻需要釋出一個普通交易,将代理控制器合約的“Bank”映射到V1版本的合約位址上即可。

以上是單鍊場景的更新方法。如果是多鍊場景,隻需根據業務的需要來判斷鍊與鍊之間的灰階政策,重複單鍊場景的更新即可。如果是跨鍊場景,需要根據跨鍊兩端的具體情況來制定更新方法。

而對于業務發起端的Dapp而言,它是無任何感覺的。它對A銀行的存款請求與2.2中完全一樣。依舊是以(“Bank",deposit,”BankA“,money)來送出請求。

總結而言,灰階政策定義在新版本的控制器合約中,資料無需遷移,業務無感覺,無需停止服務。無縫更新。

如上圖所示,A銀行資料合約從V1更新到V2。而其他的合約和接口都是不需要更新,假設V2版本相對V1版本隻是增加新的資料字段loan,并假設銀行業務控制器合約原本就能支援到V2版本的A銀行資料合約(如果是銀行業務控制器合約也需要更新則是3.3節的場景,這裡不做描述)。

此時,V2版本的A銀行資料合約需要做的事情是:

  1. 繼承V1版本的銀行資料合約。
  2. 增加一個新字段loan。并實作loan相關的資料接口。

需要注意的是,命名控制器合約有如下重要的設計:

  1. 命名控制器合約是通過通路命名資料合約來存儲和通路資料的(為了友善描述,圖中并沒有畫出來),是以指令控制器合約是可以參考3.1節的方法來更新的。
  2. 命名資料合約儲存了name=>mapping(version=>address)的映射表。
  3. 命名資料合約儲存了name=>目前有效的version的映射表。
  4. 命名控制器合約提供了對命名資料合約的name進行周遊的接口。
  5. 命名控制器合約提供了對命名資料合約的映射表的變更接口。

是以,完成V2版本的資料合約之後,即可釋出一個普通交易,交易中的邏輯是,先部署V2版本的A銀行資料合約,并完成V1版本資料合約到V2版本資料合約的資料遷移(資料遷移方法第4節會描述),接着将V2版本資料合約位址注冊到命名控制器合約,并更新BankA-Data所映射的目前有效verison=V2。此時已完成了A銀行資料合約的V2版本更新。

如果需要回退版本,隻需要釋出一個普通交易,将命名控制器合約的BankA-Data所映射的目前有效verison=V1即可。

對于B銀行而言,因為B銀行資料合約并沒有執行更新,是以與它相關的業務請求依然是通路的B銀行資料合約的V1版本。是以,對于曆史舊版本的資料合約,可以根據業務的需要來判斷是否需要對曆史舊版本執行更新。有些特殊場景下,需要對所有的曆史舊版本資料合約進行更新,這時可以利用命名控制器合約的周遊功能,對所有資料合約進行類似的更新。而對于新加入的C銀行,它可以直接使用最新版本V2的資料合約,按照正常流程完成部署與注冊,無任何額外操作。

正是由于有了命名控制器合約的版本控制邏輯,可以使得即使存在新老版本資料合約并存的情況下,業務控制器類合約依然能正常運作。而對于由于業務的發展和不斷的版本更新,會帶來命名資料合約的存儲量膨脹,導緻可能出現的性能下降的情況,依然可以套用本節所述的資料遷移與更新的方法來解決。

總結而言,得益于命名控制器合約的版本控制設計,灰階政策可以交給業務方非常自由地選擇,業務無感覺,無需停止服務。無縫更新。

此種情況下,實質是3.1與3.2 兩種情況的混搭。

是以根據具體情況,拆解成參考3.1和3.2場景方法來執行即可。

如3.2節所描述,在資料合約更新的場景,某些情況需要處理曆史資料在新舊合約之間的遷移。遷移的方法有如下三種,各有特點。

寫死遷移法指的是,新版本的資料合約中儲存一個指向舊版本資料合約的合約位址,新版本資料合約儲存的是增量的資料内容。

這樣相當于新版本合約保留了一份舊版本資料的指針,當新版本需要使用舊資料的時候,直接調用舊資料合約位址對應資料接口即可。這樣,新舊版本資料合約可以并存,即使是在異常情況下,資料被誤寫到了舊版本合約上,它依然可以被新版本所通路到。

這個方法的優點是:新舊合約可以同時并存,不增加區塊鍊存儲壓力,簡單靈活,較強的更新容錯能力。缺點:持續不斷的版本更新會導緻形成較長的鍊式邏輯關系,維護成本較高。

硬拷貝遷移法指的是,新版本和舊版本之間切斷邏輯關系,利用外部遷移工具,将舊版本資料逐漸拷貝到鍊下,再從鍊下重新存儲到新版本合約的過程。

這個方法的優點是:無曆史包袱。缺點是:大幅度增加區塊鍊存儲壓力;資料遷移工具需要适配不同的資料合約,開發成本較高;遷移過程需要停止服務,否則容易出現髒資料;資料量大時,耗時長,操作複雜,容易出錯,基本無法實操。

默克爾數遷移法要點如下:

  1. 利用智能合約語言的面向對象的繼承特性,使得新版本合約存儲結構完全相容舊版本合約存儲結構。
  2. 利用智能合約在區塊鍊上的storage樹原理,使得新版本合約的storeage樹直接從舊版本合約上衍生。無需顯式的遷移過程。
  3. 利用區塊鍊交易的原子性,使得新版本合約的部署、資料遷移、更新,原子完成。

這個方法擁有前面兩個方法的所有優點,且簡單高效,安全,實操性強。缺點:需要區塊鍊底層功能特性的支援。

上面的幹貨這麼多,請允許我夾帶點私貨,分享兩個區塊鍊、以太坊開發DApp的實戰教程:

1. 适合區塊鍊新手的以太坊DApp開發:

http://xc.hubwiz.com/course/5a952991adb3847553d205d1

2. 用區塊鍊、星際檔案系統(IPFS)、Node.js和MongoDB來建構電商平台:

http://xc.hubwiz.com/course/5abbb7acc02e6b6a59171dd6

繼續閱讀