王淵命:從智慧合約的演進看 Move 的架構設計

買賣虛擬貨幣
講師:王淵命(@jolestar) westar實驗室 首席架構師

智慧合約是區塊鏈領域大家一直比較關注的主題,本文試圖從智慧合約的演進角度分析 libra 的 move 合約要解決的問題,以及其架構設計,也就是回答『 libra 為什麼要重新設計一種程式語言?』這個問題。

智慧合約是什麼

開始這個題目之前,我們可以先討論一個比較大一點的話題,到底什麼是『智慧合約』?其實『智慧合約』這個詞在業界爭議也很大,每個人有每個人的看法。我們為了方便討論,在當前主題背景下給它做一個限定。

  • 執行在鏈上的,由使用者自定義的程式
  • 透過鏈節點的重複校驗以及共識機制,使其具有不依賴於權威方的獨立約束力

首先它是執行在鏈上,由使用者自定義的程式。如果是在鏈上直接實現的,哪怕是透過外掛的機制,我們這裡也不把它叫做智慧合約。其次,因為有了鏈的重複校驗以及共識能力,讓這種程式具有了約束力。這個約束力不來自於你對某一方的信任,即便是合約的開發者,也要受這套約束機制的約束。

至於關於『智慧合約』的爭議點這裡不深入討論,感興趣的朋友可以看我以前的一篇從『法律合約』角度討論智慧合約的文章 -《智慧合約到底是什麼?》。

回顧智慧合約的演進

為了理解 libra 的 move,我們簡單回顧一下智慧合約的演進過程。畢竟發明一種新的程式語言需要有足夠的動機,世界上的程式語言這麼多了,為什麼又要發明一種新的語言?這種新的語言要解決什麼問題?所以我們得從演進的角度看這個問題。對 bitcoin 和 ethereum 都非常熟悉的朋友可以直接跳到後面部分。

bitcoin 的智慧合約

(圖片來源:mastering bitcoin 2nd)

bitcoin 的智慧合約就是給資產上鎖和解鎖的指令碼(locking & unlocking script)。簡單的理解就是 bitcoin 提供了一種智慧鎖,使用者可以根據自己的需求,排列組合,形成自己的鎖,用來鎖定或者解鎖資產。

那它為什麼不直接確定一種鎖定和解鎖方式,而是弄很指令碼讓使用者自定義呢?主要目的是給鏈提供一種邏輯擴充套件性,這樣使用者可以在不改變鏈的基礎程式碼的情況下,增加一些功能,做一些實驗。同時它是無狀態的(stateless),一把鎖只管一份資產,鎖與鎖之間也不能共享資料。也被設計成圖靈不完備的(turing incompleteness),避免使用者寫出太複雜的鎖增加鏈的解鎖校驗成本。

因為 bitcoin 自己定義的應用場景很明確,無論把它理解成密碼貨幣或者密碼資產,主要功能就是儲存資產,轉讓資產。這樣的設計也滿足它的場景需求了。只要資產轉讓合約可以透過鎖和解鎖來表達,那就可以用 bitcoin 的智慧合約實現。 但是不是所有的合約都可以透過鎖和解鎖表達出來?這個還有待證明。設計一套鎖和解鎖的協議是非常有挑戰性的,比如閃電網路的協議。 關於 bitcoin 上的不同的鎖的機制可以參看李畫和我的一個訪談文章 -《開啟比特幣智慧合約的「三把鎖」》。

但既然 bitcoin 提供了這樣一種去中心化記賬的能力,使用者會想我是否可以把它用在其他地方?比如把一份資料的 hash 作為一個地址,給這個地址轉很小一筆資產,這個 hash 地址就被公示在鏈上了,就可以提供一種資料的存在證明。但這筆錢是沒辦法被花費的,因為沒有人知道這個 hash 地址對應的私鑰是什麼。如果這樣的交易多了後,會給 bitcoin 的鏈帶來很大的壓力,因為鏈要維護所有的未花費交易(utxo)。

於是 bitcoin 開發者就想了一個辦法,增加了一個指令 op_return。使用者不需要把自己的資料偽裝成地址,直接在指令碼中嵌入自己的資料,然後再加上 op_return,這樣鏈就知道這筆交易在未來不會被花費,僅僅是記錄資料在區塊中。而有了這套機制後,越來越多的第三方開發者就嘗試用 bitcoin 的網路來發行另外一種資產,這就是大家常說的染色幣(colored coins)。染色幣利用了 bitcoin 網路的廣播交易,在區塊中記錄資料的能力,發行方只需要執行一些節點接入 bitcoin 網路,對 op_return 中的資料進行校驗即可,成本比自己獨立執行一條鏈的成本低許多。那再進一步想下,如果指令碼中能讀取和生成狀態,是不是這種校驗節點也可以不需要了,可以直接委託給鏈來校驗了?這就誕生了 ethereum。

ethereum 的智慧合約

ethereum 的智慧合約是有狀態的,圖靈完備的。比如看一下社羣官網給的一個 coin 的例子(有簡化):

這個例子中,coin 合約中的 balances 是一個 map,儲存使用者的地址和餘額的對映,轉賬時減少傳送方的餘額,增加接收方的餘額就行。看起來很簡單吧?像是一個單機程式一樣,會一點程式語言的人大約就能看明白。但要提供這樣的能力,有一些難題需需要解決,而 ethereum 的這些解決方案都是區塊鏈智慧合約領域很重要的創新。

圖靈完備與停機問題

既然是圖靈完備的語言,那就需要解決停機問題。如果有人寫一個死迴圈合約放到鏈上,那所有的節點都進入了死迴圈。並且根據停機問題的證明,沒辦法直接透過分析程式來判斷是否會進入死迴圈,也就是說沒辦法提前阻止使用者部署這樣的合約到鏈上,只有真正執行的時候才知道是否會死迴圈。於是 ethereum 設計了一種 gas 機制,執行每個指令的時候都需要消耗一部分 gas,當 gas 消耗完了後合約會執行失敗退出。這是一個非常經典的創新。

合約的狀態儲存與節點狀態的一致性校驗

bitcoin 中的指令碼是無狀態的,它的狀態只是 utxo,每個節點維護一份自己的 utxo 列表就行。但 ethereum 的合約是有狀態的(就是合約中儲存的資料,比如前面例子中每個人的餘額),並且可以透過交易變更,那如果這些狀態出現了不一致的情況(比如有 bug 或者儲存裝置錯誤),節點如何校驗?

為了解決這個問題,ethereum 設計了一種狀態樹:

(圖片來源:ethereum stackexchange ethereum-block-architecture)

它整體的思路是每個合約中的每個外部儲存變數,在狀態樹上都表現為一個節點。一個合約的所有變數生成一個狀態樹,數的根節點就是 storage root,而這個 storage root 又透過合約地址對映到 global storage root 上。只要任何一個合約中的任何一個變數有變化,global storage root 就會變化,節點之間就可以透過比較 global storage root 來快速校驗資料的一致性。同時它也提供了一種狀態證明能力,可以讓節點之間信任對方的狀態資料,快速同步節點狀態,而不是重新透過區塊計算一遍。

關於這個狀態樹(merkle patricia tree),這裡也不細說,有興趣的可以看看 ethereum 相關的書或者文章。

合約的抽象與跨合約呼叫

既然是 ethereum 提供的 solidity 是一種完備的程式語言,就有抽象與互相呼叫的問題。solidity 中設計了 interface,這個和其他程式語言中的 interface 類似。開發者可以先協商定義一種 interface 作為標準,然後各自實現。合約之間,以及合約和客戶端(比如錢包)之間都可以透過 interface 來呼叫。

例如上面的 erc20 的 interface,定義了轉賬,查詢餘額等標準方法。而 approve 的目的是給第三方授權一個額度,可以從使用者賬號上扣錢,類似於信用卡的預授權機制。社羣開發者如果有新的想法了就提出一個 ercxxx,其他人可以基於這個 ercxxx 進行進一步的組合創新。這套機制靈活性很強,ethereum 上繁榮的 defi 生態主要就是依賴這套機制演進出來的。

ethereum 的問題

當然,世上沒有完美的技術,ethereum 提供了一些新的能力,同時也帶來一些新的問題。

鏈上原生資產(ether)和透過合約定義的資產(erc 20 token)之間的抽象和行為不一致

這點如果寫過 ethereum 合約的人就會很有體會。如果你要寫一個合約,同時處理 ether 和其他 token,你會發現二者的處理邏輯完全不一樣,很難用統一的方式去處理。並且二者的安全性也不一樣,ether 的安全性由鏈保證,而 token 的安全依賴於該 token 的開發者。比如 ethereum 中的 layer2 方案裡,如果同時要支援 ether 和 token 也會比較複雜。

安全問題

ethereum 上爆出了很多安全性事故,雖然很多問題的直接原因是合約開發者的實現不夠嚴謹,但本質上的原因來自於兩點:

  • 可擴充套件性與確定性之間的矛盾

interface 的機制提供了很強大的擴充套件性,但 interface 只定義了介面,並無法保證實現遵循介面的要求。比如下面這個例子,是我曾經開發的一個惡搞的 token:

這個 token 的轉賬方法裡面包含一種隨機機制,有一定的概率可以轉讓成功,但也有一定的概率反倒增加自己的 token。而這種行為被 interface 的 transfer 掩蓋,呼叫方根本不清楚它的具體實現,從而可能帶來安全性問題。

當然有一種辦法是直接實現一種確定邏輯的 token,不允許使用者自定義邏輯,只提供有限的配置項,比如總額,名稱等。有的鏈為了解決 ethereum 的安全性問題,就是這樣做的。但這樣使用者就無法根據自己的場景進行擴充套件,比如要實現一個只對某個群體的使用者可用的 token。同時這種擴充套件性需求是沒法窮舉的,只能讓使用者不斷嘗試演化。這就是可擴充套件性與確定性之間的矛盾難題。

  • 合約間的呼叫問題

ethereum 上的合約間的呼叫,是一種動態呼叫。它實際上構造了一種內部的交易,然後啟動了一個新的虛擬機器去執行呼叫。機制上有點像伺服器之間的遠端呼叫,而這種呼叫有可能形成迴圈呼叫,從而出現類似併發的情況,即便虛擬機器是單執行緒執行的。比如 a 合約呼叫 b 合約,而 b 合約又調回 a 合約。於是 a 合約的前一次執行尚未完成,又進行了下一次執行,而第二次的執行無法讀取到第一次執行的中間狀態。這也就是那次 dao attack 利用的漏洞。關於這個問題的分析可以參看論文 a concurrent perspective on smart contracts。

  • 合約狀態爆炸

合約狀態爆炸的主要原因是 ethereum 雖然設計了 gas 費用機制,避免使用者濫用網路。但這個 gas 只針對計算,是一次性收取的,一旦資料寫入到合約狀態,就永久保留了,使用者無需為自己的資料未來的儲存承擔成本。這樣導致使用者和開發者都沒有動力去清理無用的狀態。同時,ethereum 的同一個合約的所有使用者狀態都在該合約賬號下,熱門合約下的資料會膨脹的更厲害。

ethereum 開發者嘗試實現狀態租賃的機制,讓使用者為自己的狀態付 state fees,但所有使用者的狀態都是儲存在合約賬號下的,鏈很難明確狀態和使用者的關係,更無法區分那些是合約的公共狀態,哪些是具體使用者的狀態,方案設計起來非常複雜,最後還是放棄了,將這個目標放在了 ethereum 2.0 中。感興趣的讀者可以看看 ethereum state fees 的一版方案設計。

思考一下,上面 ethereum 遺留的幾個問題,如果讓你來設計方案,你會如何解決?後面我們來具體分析 libra 的 move 是如何解決這些問題的。

libra move

這是我總結的 move 的幾個特點:

  • first-class resources
  • abstract by data not behavior,no interface,no dynamic dispatch。
  • use data visibility & limited mutability to protected resource,no inheritance.

first-class resources 包含兩層的含義。一層就是大家常說的一等公民資產。libra 上,所有的資產都是透過合約實現的,包括 libracoin,都共享一套抽象邏輯以及安全機制,地位平等。第二層意思是 move 中的資產都是透過 resource 定義的一種型別,可以直接在資料結構中引用。

第二條簡單的理解就是它拋棄了 interface 的機制。但如果沒有了 interface,我們怎麼定義協議和標準呢?具體它是怎麼透過資料來抽象的,這個後面細說。

第三條是說,既然資產是一種資料型別,並不能隱藏到合約內部,透過什麼方式保護它?比如阻止使用者直接 new 出某種資產?

在瞭解 move 如何解決上面的問題之前,我們先了解一下 move 中的基本概念。

move 中的基本概念

module,resource|struct,function

module 和其他語言中的模組,比如 rust 中的 mod,ethereum solidity 中的 contract 類似,封裝了一系列資料結構以及方法。resource|struct 和其他語言中的 struct 一樣,都是用來定義新的資料結構型別。resourc 和 |struct 的區別是 resource 要儲存到外部狀態中,並且是不可複製的。function 和其他語言中的 function 沒有太大區別。

copy,move

這是 move 借鑑 rust 的生命週期機制引入的概念,move 中的所有變數使用的時候都需要確定是要 move 還是 copy,一旦被 move,該變數就不可再被使用。而 resource 只能被 move 不能被 copy,當然 resource 的引用(reference)是可以 copy 的。這樣編譯器就可以向追蹤記憶體的使用一樣,追蹤資產的轉移和變化,避免資產憑空消失或者產生。

builtin 方法

move 提供了一些內建的方法來和鏈的狀態互動。solidity 中,開發者幾乎不需要關心合約的狀態是怎麼儲存和持久化的,幾乎對開發者透明。但 libra 中,開發者要顯式的呼叫方法去外部獲取狀態。這樣強迫開發者在寫合約的時候,把狀態明確拆分到了具體的賬號下,才能實現狀態租賃以及淘汰機制。

  • borrow_global(address)/borrow_global_mut(address) 從 address 賬號下獲取型別為 t 的 resource 引用。
  • move_from(address) 將 resource t 從 address 賬號下 move 出來。
  • move_to_sender() 將 resource t 存到交易的傳送方賬號下。

上面例子中的 t 都必須是當前 module 定義的型別,一個 module 中不能直接獲取其他 module 定義的型別。下面我們透過一些具體的例子來理解 move 的機制。

一個的例子

這個例子是 libracoin 的定義:

比如上圖的 libracoin module,定義了一種 resource 型別 t,代表一種 coin,內部只包含一個數字,代表它的值。而在 libraaccount 以及 hashtimelock 中,直接引用 libracoin.t 作為自己型別的一個欄位。從這裡就可以看出 move 和 solidity 之間的差異。solidity 中,一種 coin 就是一個合約,並不存在某種 coin 的資料型別。那這樣定義後,coin 如何定義自己的行為呢?使用者怎麼用呢?繼續看下面的例子。

libracoin module 中定義了 libracoin.t 的最基本的方法,比如獲取它的 value,切分,合併等。這些方法都是和 coin 型別相關的最基本的操作,並不包含更高階的,比如給某個賬號轉賬。libracoin 中的 deposit 和 withdraw 方法,並不是針對賬號,而是正對 libracoin.t 型別的引用。在 move 中,某個 resource 的內部結構只有定義該 resource 的 module 內可見,外部 module 只能將某個 resource 當一個整體對待,無法直接行 split,join 這樣的操作。

那合約中定義的 coin 如何發行呢?既然它是一種型別,如何控制 coin 發行的許可權?

上面的例子中有一個 mint 方法,進行鑄幣,方法最後實際上就是直接 new 了一個 libracoin.t,並填入一個 數字。而這個方法有一個引數,capability,它代表一種鑄幣的許可權。而 mintcapability 如何產生呢?可以看 initialize 方法,這個是在創世塊初始化的時候,由一個特殊賬號建立,並持有。只要賬號下擁有 mintcapability ,即可透過 mint_with_default_capability 方法來鑄幣。

繼續來看看如何 libracoin 如何實現高階的轉賬:

libraaccount 的 t 中的 balance 欄位是 libracoin.t 型別的,所以針對賬號的轉賬,支付都是在 libraaccount 中定義的,libraaccount 呼叫 libracoin 的方法操作自己的 balance 欄位,從而實現轉賬。libracoin 本身不關心高階的轉賬邏輯。這樣一層一層組合起來,就構造出高階的功能了。

繼續看一下 hashtimelock 的一種實現:

例子中的 lock 方法,將資產封裝到 hashtimelock.t 中,並和 hash ,time 繫結在一起。unlock 的時候校驗 hash 以及 time,如果正確則將 hashtimelock.t 解開,返回其中封裝的資產。而 lock 的時候的資產從哪裡來,解鎖後的資產又存到哪裡去,這個 hashtimelock 合約不關心,可以由其他合約定義,或者直接在交易的 main 指令碼中編寫,如:

這個指令碼中,資產是從 libraaccount 中取出,之後又充到 libraaccount 中。這個可以由每個交易自己定義。

不支援 interface 如何定義標準

前面的例子可以看出 move 如何透過組裝以及可見性機制來組合出高階功能,在不提供動態分發的機制下,也保證了足夠的擴充套件。但如果沒有 interface 這樣的機制,如何定義像 ethereum 中的 erc20 這樣的標準?不同的 token 的實現都不一樣,上層的 defi 協議如何定義?

有一次我和 move 的開發者交流這個問題,他說了一句話給我印象很深刻。

when code is law, interfaces are a crime. – tnowacki

在程式碼即法律的世界,介面就是犯罪。因為介面只定義了行為,卻不關心實現。而對資產進行編碼的時候,使用者更希望資產本身相關的操作是確定的。

他舉了一個 token 的例子:

這個例子透過泛型定義了一種帶標籤的 coin,任何人都可以基於這個 coin 來定義一種新的 coin。coin 的基本操作都是確定的,但 coin 的發行者可以在上層繼續封裝,衍生出不一樣的特性。這樣的機制下,既保證了行為的確定性,也具有足夠的擴充套件性。

打一個比喻,solidity 中的 token 實現類似於記錄了一個賬本,而 move 則類似於把資產封裝起來。大家去租東西交押金的時候,會遇到兩種營業員。第一種把營業員把錢彙總到一起,然後在賬本上記錄一下。第二種營業員則把錢用一個夾子夾起來,記個名字。第一種退錢的時候,營業員需要修改賬本,然後把押金從彙總的錢中分出來,給客戶。而後一種則直接找到對應的押金夾子直接給客戶。前一種像 solidity ,後一種像 move。箱子的好處是可以箱子套箱子,很容易組合出更復雜的箱子,賬本要組合就只能透過賬本之間互相引用記錄來實現。

move 的狀態儲存

前面介紹了一下 move 語言本身的特性,而合約的程式語言和它的狀態儲存機制密不可分,下面我們再探討一下 libra 在狀態儲存機制上的改進。

上面這個公式說明了哪些資料屬於 libra 的 globalstate。所有賬號和賬號狀態之間的對映。而賬號的狀態包括使用者部署的合約(module),以及使用者透過合約生成的 resource。這點上和 ethereum 的關鍵區別是每個使用者的所有狀態都在其的賬號路徑下,而不是散落在各個合約裡。

上圖中的, sparse merkle tree 相當於 ethereum 中的 merkle patricia tree,二者作用一樣,只是演算法實現上有差異。而 merkle tree accumulator 則是 libra 新增加的。ethereum 中,每個區塊生成一個全域性狀態樹的根,打包在區塊頭裡。而 libra 中的做法是每個交易都生成一個狀態樹的根,然後把這些根和交易資訊關聯,再用累加器累加起來,生成一個累加器的根,區塊的頭中包含累加器的根就可以了。這個累加器是全域性的,並不是某個區塊的。

可以先看看它的累加器(accumulator)的實現:

累加器顧名思義其實就是把資料累加起來形成一個結果,但還要能提供這個結果包含某個資料的證明。累加器也有純密碼學的實現(如 rsa accumulator),但計算效率還很難達到應用的要求,安全證明也比較難,所以透過 merkle tree 來實現累加器是一種更現實的做法。

merkle tree 來實現累加器其實就是要提供一種給 merkle tree 動態增加節點的演算法。merkle tree 本身是一種二叉樹,如果葉子節點是確定的,樹的高度也就是確定的,計算根節點的值比較容易。但累加器的葉子節點要動態增長的,樹的高度也是動態增長的。如上圖所示,左邊的兩顆子樹是已經 frozen 的,它的葉節點的值對後面的計算已經沒用了,只需要 frozen 的根節點參與計算即可。整個樹逐漸增長,逐漸從左到右 frozen。這種演算法業界討論的也挺早了,比如 bitcion 社羣討論的 merkle mountain ranges 演算法。由於篇幅所限,這裡不仔細分析它的演算法。這種全域性累加器的最大作用是提供全域性的交易執行存在證明。ethereum 和 bitcion 上要證明一個交易發生過,只能先證明改交易被打包在某個區塊了,再證明該區塊確實在鏈上,而有了全域性累加器可以提供直接的證明。

再來看看它的 sparse merkle tree 實現。

(圖片來源 libra 白皮書)

ethereum 用 merkle tree 來存狀態,提供狀態證明,遇到的第一個問題就是 merkle tree 的高度太高,計算和儲存成本高,生成的證明也很大。於是它做了一個最佳化,相當於把二叉樹給變成十六叉樹了。而 libra 的思路類似,也是給路徑做壓縮。比如上圖中的 1 是一棵完整的二叉樹,2 最佳化掉了空的子樹,3 在不產生分歧的情況下,最佳化掉了中間的節點,縮短了路徑。可以說和 ethereum 的 merkle patricia tree 殊途同歸,但 sparse merkle tree 的一個獨特優點是可以提供不存在證明。更詳細的演算法實現分析可以看 @lerencao 的文章 jellyfish merkle tree in libra blockchain 。

move 的現狀

最後簡單說一下 move 的現狀。

  • 上面的部分例子是 move ir(中間語言),部分是 move source lang。move source lang 是對最終開發者的程式語言,但現在還沒正式用在 libra 中,libra 中用的還是 ir。
  • 泛型的支援尚未最終完成,上面的部分例子還不能直接執行。
  • account 狀態現在整個是打包成一個大的二進位制,尚未像 ethereum 那樣拆分成兩層的 tree。
  • 集合型別支援上不完備,比如 map 的支援等。
  • 空間租賃機制只是理論上設計,尚未實現。

總結

總結一下 move 和 libra 的主要改進點以及意義:

  • firstclass resource,讓資產不僅可程式設計,並且可以對映成程式中的型別,提供了一種新的程式設計的模型。
  • 同一個使用者的所有狀態都在使用者路徑下。這個讓對狀態空間租賃以及使用者狀態淘汰提供了技術上的可能。(注:使用者狀態被淘汰後,可以透過付費再重新存回來)。
  • 狀態儲存機制上的改進與最佳化,給一個交易都關聯一個全域性狀態,可以提供交易的全域性證明。

libra 的這些設計在二層(layer2)機制也很有潛力,主要來源於 1 它統一的資產程式設計模型,比較容易設計通用的 layer2 方案。2 使用者狀態的拆分,方便狀態在鏈上鍊下之間遷移。3. 全域性的證明機制,方便二層仲裁。具體如何利用這些特性設計 layer2 方案,我會在未來的文章中和大家探討。

最後回答一下開篇的問題,上面這些創新點確實足夠支撐 move 作為一種新的程式語言立足。技術的發展就是在不斷引入透過創新來解決遺留問題,但同時又帶來新的問題,然後再觸發新的創新這樣一波一波推進的。


免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;