本文作者:jolestar.eth(Twitter:@jolestar)
原文標題及鏈接:為什麼是Move 之編程語言的生態構建
作為一個Move 的鼓吹者,每次給開發者推廣Move 的時候都會遇到這樣的問題。 Move 有什麼優勢嗎?為什麼是Move?就像你給好友介紹自己的新戀人,總會遇到類似的問題。但這種問題其實不易回答,如果一條一條列舉優缺點,總是會有人質疑,畢竟新語言的生態都不成熟,選擇只能基於它的潛力來判斷。我先說一個論斷:Move 是最有潛力構建出Solidity 這樣的生態系統,甚至超越的智能合約編程語言。
目標讀者:開發者以及對區塊鏈領域的技術感興趣的朋友。本文希望盡量以通俗的方式說明智能合約當前遇到的難題以及Move 的一些嘗試,盡量少用代碼,期望不懂編程語言的朋友也能大致理解,但這個很難,希望讀者給一點反饋。
智能合約的兩條路
如果把時間拖回到幾年前,新公鏈上支持圖靈完備智能合約的編程語言主要有兩種方式:
1. 一種是基於現有的編程語言進行裁剪,然後運行在WASM 等通用的虛擬機裡。這種方案的優勢是可以沿用當前編程語言以及WASM 虛擬機的生態。
2. 一種是新造一個專門的智能合約編程語言,以及虛擬機,從頭構造語言以及虛擬機生態。 Solidity 就是這條路線,Move 也是這條路線。
那時候大家普遍其實不太看好Solidity&Evm 生態,覺得Solidity 除了用來發Token,貌似也沒有什麼用,性能也不好,工具也孱弱,像是個玩具。很多鏈的目標是讓開發者用已有的語言來進行智能合約編程,覺得前一條路更被看好,很少有新公鏈直接複製Solidity&Evm。
但經過幾年的發展,尤其DeFi 崛起之後,大家突然發現Solidity 的生態不一樣了。而走前一條路的智能合約生態反倒沒有成長起來,為什麼呢?我總結有幾個原因。
1. 區塊鏈的程序運行環境和麵向操作系統的程序運行環境區別很大,如果拋棄掉操作系統的系統調用,文件IO,硬件,網絡,並發等相關的庫,再考慮鏈上的執行成本,已有編程語言能和智能合約共享的代碼庫非常少。
2. 第一種方案理論上能支持的語言很多,但實際上帶有Runtime 的編程語言編譯到WASM 等虛擬機後文件會非常大,不適合區塊鏈場景使用,能用的也主要是C , C++, Rust 等。而這幾種語言的學習門檻實際上並不比Solidity 這種專門的智能合約編程語言的成本更低,並且同時支持多個語言可能會導致早期生態的割裂。
3. 各鏈的狀態處理機制不一樣,即便都用都是WASM 虛擬機,各鏈的智能合約應用也不能直接遷移,無法共享一個共同的編程語言以及開發者生態。
對應用開發者來說,直接面對的就是智能合約編程語言,編程語言的基礎庫,有沒有可複用的開源庫。 DeFi 的安全性要求智能合約代碼要經過審計,而經過審計的代碼每一行都代表著錢,大家基於已有的代碼略做修改進行複制,就能降低審計成本。
現在看來Solidity 雖然走了一個看起來慢的路,但實際上更快的構建出了生態。現在很多人已經認為Solidity&EVM 就是智能合約的終點了,很多鏈都開始兼容或者移植Solidity&Evm。這時候,新的智能合約編程語言需要證明自己有更強的生態構建能力,才能說服大家關注與投入。
那新的問題就是,一個編程語言語言,如何衡量它的生態構建能力?
編程語言的生態構建能力
編程語言的生態構建能力,簡單的來說就是它的代碼的複用能力,主要體現在兩個方面:
1. 編程語言模塊之間的依賴方式。
2. 編程語言模塊之間的組合方式。 “可組合性”是智能合約標榜的一個特性,但實際上編程語言都有組合性,我們發明的Interface, Trait 等都是為了更方便的組合。
先說說依賴方式,編程語言實現實現依賴主要通過三個方式:
1. 通過靜態庫(Static-Libraries)的方式,在編譯期靜態鏈接,將依賴打包在同一個二進制中。
2. 通過動態庫(Dynamic-Libraries)的方式,運行時動態鏈接,依賴並不在二進制中,但要預先在目標平台上部署。
3. 通過遠程調用(RPC)在運行期依賴。這裡泛指各種可以遠程調用的API。
1,2 一般都用在基礎庫依賴的場景下。基礎庫一般是無狀態的,因為應用如何處理狀態,比如寫哪個文件裡,還是存哪個數據庫表裡,基礎庫是很難假設。這種調用是在同一個進程同一個方法調用的上下文裡,共享調用棧,共享內存空間,沒有安全隔離(或者說隔離很弱),需要可信環境。
3 實際上調用的是另外的進程或者另外的機器上的進程,互相通過消息通信,各進程負責自己的狀態,所以可以提供狀態的依賴,調用也有安全隔離。
這三種方法各有優劣。 1 在最終二進制中包含依賴的庫,優點是對目標平台的環境無依賴,但缺點是二進制比較大,2 的優勢是二進制比較小,但對運行環境有前置要求,3 可以構建跨語言的依賴關係,一般用在跨服務,跨機構合作的場景中,為了方便開發者調用,一般通過SDK 或者代碼生成模擬成方法調用。
技術歷史上,很多編程語言,操作系統平台都花費了很大的精力想彌合遠程調用和本地調用之間的差異,想實現無縫的遠程調用和組合。隨便舉一些著名的技術詞彙,COM(Component Object Model)/CORBA/SOAP/REST 等等,都是為了解決這些問題。雖然實現無縫調用組合的夢想破滅了,大家最後還是靠工程師人力拼接口,把整個Web2 的服務給拼接在一起,但夢想的火種還在。
而智能合約,給應用間的依賴方式帶來了新變化。
智能合約帶來的改變
傳統的企業應用之間的依賴方式可以用下圖表示
1. 系統之間通過各種RPC 協議把運行在不同的機器上的服務連接在一起。
2. 機器之間有各種技術的,人工的“牆”進行隔離,保證安全。
而智能合約的運行環境是鏈的節點給構造出的沙箱環境,多個合約程序是運行在同一個進程內的不同的虛擬機沙箱中,如下圖所示:
合約之間的調用是同一個進程內不同的智能合約虛擬機之間的調用。
安全依賴於智能合約虛擬機之間的隔離。
我們以Solidity為例子,Solidity 的合約(表明為contract的模塊)將自己的函數聲明為public ,然後其他合約就可以直接通過這個public方法調用該合約。以下圖的一個RPC 調用過程為例:
圖片來源https://docs.microsoft.com/en-us/windows/win32/rpc/how-rpc-works
鏈實際上接管了上圖中, Client和Server之間通信的所有過程,自動生成stub ,實現序列化和反序列化,真正讓開發者感覺到遠程調用就像本地方法調用一樣。
當然,技術並沒有銀彈,沒有一勞永逸的方案,新的方案總帶來新的難題需要解決。
智能合約的依賴難題
通過前面的分析,我們理解了智能合約之間的調用實際上是一種類似於遠程調用的方法。那如果像要通過庫的方式進行依賴調用呢?
在Solidity中,表明為library的模塊,就相當於靜態庫,它必須是無狀態的。對library的依賴會在編譯期打包到最終的合約二進制中。
這樣帶來的問題就是如果合約複雜,依賴過多,導致編譯後的合約過大,無法部署。但如果拆成多個合約,則又無法直接共享狀態,內部依賴變成遠程服務間的依賴,增加了調用成本。
那是不是可以走第二條動態庫加載的路呢?比如Ethereum上的大部分合約都依賴了SafeMath.sol這個庫,每個合約都包含了它的二進制,既然代碼都在鏈上了,為什麼不能直接共享呢?
於是Solidity中提供了delegatecall的方法,類似於動態鏈接庫的解決方案,把另外一個合約的代碼,嵌入到當前合約調用的上下文中執行,讓另外一個合約直接讀寫當前合約的狀態。但這就有兩個要求:
1. 調用和被調用方要是完全信任的關係。
2. 兩個合約的狀態要對齊。
非智能合約開發者可能不太理解這個問題,如果是Java開發者可以這樣理解: Solidity的每個合約都相當於一個Class ,它部署後運行起來是一個單例的Object ,如果想在運行時,加載另外一個Class的方法來修改當前Object裡的屬性,那這兩個Class裡定義的字段必須相同,並且新加載的方法相當於一個內部方法,Object 的內部屬性完全對它可見。
這樣就限制了動態鏈接的使用場景和復用程度,現在主要用來做內部的合約升級。
因為上面的原因, Solidity很難像其他編程語言一樣提供一個豐富的標準庫(stdlib) ,提前部署到鏈上由其他合約依賴,只能提供有限的幾個預編譯方法。
這也導致了EVM 字節碼的膨脹。很多本來可以通過Solidity 代碼從狀態中獲取的數據,被迫實現成了通過虛擬機指令從運行時上下文中獲取。比如區塊相關的信息本可以通過標準庫裡的系統合約從狀態中獲取,編程語言本身不需要知道區塊相關的信息。
這個問題是所有的鍊和智能合約編程語言都會遇到的問題。傳統編程語言並沒有考慮同一個方法調用棧內的安全問題(或者說考慮的比較少),搬到鏈上之後,也只能通過靜態依賴,和遠程依賴的方式解決依賴關係,一般連類似於Solidity中的delegatecall方案都很難提供。
那我們如何才能做到在智能合約之間實現類似動態庫鏈接的方式調用?合約之間的調用可以共享同一個方法調用棧,並且可以直接傳遞變量?
這樣做帶來兩個安全性方面的挑戰:
合約的狀態的安全性要通過編程語言內部的安全性進行隔離,而不能依賴虛擬機進行隔離。
跨合約的變量傳遞需要保證安全,保證不能隨意丟棄,尤其是表達資產類型的變量。
智能合約的狀態隔離
前面說到,智能合約實際上是把不同組織機構的代碼放在同一個進程中執行,那合約的狀態(簡單理解就是合約執行時生成的結果,需要保存起來供下次執行的時候使用)的隔離就是必要的了,如果直接允許一個合約讀寫另外一個合約的狀態,肯定帶來安全問題。
隔離方案理解起來其實也很簡單,就是給每個合約一個獨立的狀態空間。執行智能合約的時候,將當前智能合約的狀態空間和虛擬機綁定,這樣智能合約就只能讀取自己的狀態了。如果要讀取另外的合約,則需要前面提到的合約間的調用,實際上是在另外一個虛擬機裡執行。
但如果想要通過動態庫的方式進行依賴的時候,這樣的隔離就不夠了。因為實際上,另外一個合約是在當前合約的執行棧中運行的,我們需要基於語言層面的隔離,而不是虛擬機的隔離。
另外,基於合約的狀態空間的隔離同時帶來的一個問題是狀態所有權的問題。這種情況下,所有的狀態都屬於合約,並沒有區分合約的公共狀態和個人的狀態,給狀態計費帶來難題,長遠來看會有狀態爆炸的問題。
那如何在智能合約語言層面做狀態隔離呢?思路其實也很簡單,基於類型。
1. 利用編程語言對類型提供的可見性的約束,這個特性大多數編程語言都支持。
2. 利用編程語言對變量提供的可變性約束,許多編程語言區分引用的可變與不可變,比如Rust 。
3. 提供基於類型為Key 的外部存儲,限制當前模塊只能用自己定義的類型作為Key來讀取外部存儲。
4. 在編程語言層面對類型提供聲明copy, drop的能力, 保證資產類的變量不可以被隨意複製和丟棄。
Move 語言就是用了以上解決方案,其中第3,4點是Move 特有的。這個解決方案其實也比較容易理解,如果不能在虛擬機層面給每個智能合約程序一個單獨的狀態空間,在合約內部做狀態隔離,基於類型是比較容易理解的方式,因為類型有明確的歸屬和可見性。
這樣在Move 中,智能合約之間的調用變成如下圖所示:
不同組織和機構的程序,通過動態庫的方式,組合成同一個應用運行,共享同一個編程語言的內存世界。組織之間不僅可以傳遞消息,同時可以傳遞引用,和資源。組織之間的交互規則和協議,只受編程語言的規則約束。 (關於資源的定義後文中有描述)。
這個改變同時帶來幾個方面的變化:
1. 編程語言以及鏈可以提供一個功能豐富的基礎庫,提前部署到鏈上。應用直接依賴復用並不需要在自己的二進制中包含基礎庫部分。
2. 由於不同組織之間的代碼在同一個編程語言的內存世界狀態裡,可以提供更豐富和復雜的組合方式。這個話題在後面會詳述。
Move 的這種依賴方式雖然和動態庫的模式類似,但它同時利用了鏈的狀態託管的特性,給編程語言帶來了一種新的依賴模式。
這種模式下,鏈既是智能合約的運行環境,同時也是智能合約程序的二進制倉庫。開發者通過依賴將鏈上的智能合約自由組合起來提供一個新的智能合約程序,並且這種依賴關係是鏈上可追踪的。
當然Move 現在還很早期,這種依賴方式提供的能力尚未充分發揮出來,不過雛形以及出現。可以設想,未來肯定可以出現基於依賴關係的激勵機制,以及基於這種激勵模式構建出的新的開源生態。後面我們繼續談一談可“組合性”的問題。
智能合約的可組合性
編程語言模塊之間的可組合性是構建編程語言生態的另外一個重要特性。可以說,正因為模塊之間有可組合性,才產生依賴關係,而不同的依賴方式也提供了不同的組合能力。
根據前面對依賴方式的分析,在Solidity生態談論智能合約的可組合性的時候,實際上主要說的是contract之間的組合,而不是library之間的組合。而我們前面也說了, contract之間的依賴是一種類似與遠程調用的依賴,互相傳遞的實際上是消息,而不能是引用或者資源。
這裡用資源(resource)這個詞,主要是強調這種類型的變量在程序內不能隨意的複制(copy)或者丟棄(drop),這是線性類型帶來的特性,這個概念在編程語言中還不普及。
線性類型來自於線性邏輯,而線性邏輯本身是為了表達經典邏輯無法表達的資源消耗類的邏輯。比如有“牛奶”,邏輯上可以推導出“奶酪”,但這裡沒辦法表達資源消耗,沒辦法表達X 單位的“牛奶”可以得出Y 單位的“奶酪” 這樣的邏輯,所以才有了線性邏輯,編程語言裡也有了線性類型。
編程語言中首先要處理資源就是內存,所以線性類型的一個應用場景就是追踪內存的使用,保證內存資源被正確的回收,比如Rust 。但如果將這個特性普遍推廣,我們就可以在程序中模擬和表達任意類型的資源。
那為什麼組合時能進行資源 傳遞非常重要呢?我們先來理解一下當前的基於Interface的組合方式,大多數編程語言,包括Solidity都是這樣的組合方式。
我們要將多個模塊組合起來,最關鍵的是約定好調用的函數以及函數的參數和返回值類型,一般叫做函數的“簽名”。我們一般用Interface 來定義這種約束,但具體的實現由各方自己實現。
比如大家常說的ERC20 Token ,它就是一個Interface ,提供以下方法:
function balanceOf(address _owner) public view returns (uint256 balance)function transfer(address _to, uint256 _value) public returns (bool success)
這個接口的定義中,有給某個地址轉帳的方法,也有查詢餘額的方法,但沒有直接提款(withdraw)的方法。因為在Solidity 中,Token 是一個服務,而不是一種類型。下面是Move 中定義的類似的方法:
可以看出,Token 是一種類型,可以從賬號withdraw 出來一個Token 對象。有人要問,這樣做有什麼意義呢?
我們可以通過一種比較通俗的類比來比較二者的組合方式的區別。 Token 對像類似於生活中的現金,你想去一個商場購買東西,有兩種支付方式:
1. 商場和銀行對接好接口,接入電子支付系統,你支付的時候直接發起請求讓銀行劃賬給商場。
2. 你從銀行取出現金,直接在商場支付。這種情況,商場並不需要提前和銀行對接接口,只要接受這種現金類型就行。至於接收現金後,商場是將現金鎖在保險櫃裡,還是繼續存到銀行中,這個由商場自己解決。
而後一種組合類型,可以稱做基於資源類型的組合方式,我們可以把這種在不同組織的合約之間流動的資源叫做“自由狀態”。
基於自由狀態的組合方式,更像是物理世界中的組合方式。比如光碟和播放機,各種機器的配件。這種組合方式和基於接口的組合方式也並不衝突。比如多個交易所(swap)想對外提供統一的接口,方便第三方集成,則使用接口的組合方式更合適。
基於自由狀態的組合的關鍵優勢有兩個:
可以有效的降低基於接口組合的嵌套深度,對這個感興趣的朋友可以參看以前我一次分享中關於閃電貸的例子。考慮到有的讀者對閃電貸背景不清楚,這裡就不詳述裡。
可以明確的將資源的定義和基於資源的行為拆分開來,這裡有一個典型的例子是靈魂綁定的NFT。
靈魂綁定的NFT 這個概念是Vitalik 提出的,想用NFT 來表達一種身份關係,而這種關係不應該是可以轉讓的,比如畢業證,榮譽證書等。
而ETH 上的NFT 標準,都是一個接口,比如ERC721 的幾個方法:
function ownerOf(uint256 _tokenId) external view returns (address);function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
如果想擴展新的行為,比如綁定,就需要定義新的接口。還會影響舊的方法,比如轉讓NFT 的時候,如果NFT 已經靈魂綁定了,就無法轉讓,勢必帶來兼容性問題。更難的是開始允許轉讓流通,但綁定後就無法流通的場景,比如部分遊戲道具。
但如果我們把NFT 設想成一個物品,這個物品本身只決定了它如何展示,有哪些屬性,至於能否轉讓,這個應該是上層的封裝。
比如下面是用Move 定義的NFT,它是一種類型。
然後我們可以把上層封裝設想成不同容器,不同的容器有不同的行為。比如NFT 放在個人展覽館裡的時候,是可以拿出來的,但一旦放一些特殊容器中,想要拿出來則需要有其他規則限制,這就實現了“綁定”。
比如Starcoin 的NFT 標準實現了一種靈魂綁定NFT 的容器叫做IdentifierNFT :
/// IdentifierNFT 中包含了一個Option 的NFT,默認是空的,相當於一個可以容納NFT 的箱子struct IdentifierNFT has key {
nft: Option>,}/// 用戶通過Accept 方法初始化一個空的IdentifierNFT 在自己的賬號下public fun accept(sender: &signer);/// 開發者通過MintCapability 給receiver 授予該nft,將nft 嵌入到IdentifierNFT中public fun grant_to(_cap: &mut MintCapability, receiver: address, nft: NFT);/// 開發者也可以通過BurnCapability 將`owner` IdentifierNFT 中的NFT 取出來public fun revoke(_cap: &mut BurnCapability, owner: address ): NFT;
這個箱子裡的NFT,只有NFT 的發行方可以授予或者收回,用戶自己只能決定是否接受,比如畢業證書,學校可以頒發和收回。當然開發者也可以實現其他規則的容器,但NFT 標準是統一的。對這個具體實現感興趣的朋友,可以參看文末鏈接。
這段闡述了Move 基於線性類型帶來的一種新的組合方式。當然,只有語言的特性優勢並不能自然帶來編程語言的生態,還必須有應用場景。我們繼續來討論Move 語言的應用場景擴展。
智能合約的應用場景擴展
Move 最初作為Libra 鏈的智能合約編程語言,設計之處就考慮到了不同的應用場景。當時Starcoin 正好在設計中,考慮到它的特性正好符合Starcoin 追求的目標,就將其應用在公鏈場景裡。再後來Libra 項目擱淺,又孵化出幾個公鏈項目,在幾個不同的方向上探索:
- MystenLabs 的Sui 引入了不可變狀態,試圖在Move 中實現類似UTXO 的編程模型。
- Aptos 在探索Layer1 上的交易的並行執行,以及高性能。
- Pontem 試圖將Move 帶入Polkadot 生態。
- Starcoin 在探索Layer2 乃至Layer3 的分層擴展模式。
同時Meta(Facebook)的原Move 團隊在嘗試將Move 運行在Evm 之上,雖然會損失合約之間的傳遞資源的特性,但有助於Move 生態的擴展以及Move 生態和Solidity 生態的融合。
當前Move 項目已經獨立出來,作為一個完全社區化的編程語言。現在面臨幾個挑戰:
1. 如何在不同的鏈的需求之間尋找最大公約數?保證語言的通用性。
2. 如何讓不同的鏈實現自己的特殊語言擴展?
3. 如何在多個鏈之間共享基礎庫和應用生態?
這幾個挑戰同時也是機遇,它們之間是衝突的,需要有取捨,需要在發展中尋找一種平衡,還沒有一種語言做過這種嘗試。這種平衡可以保證Move 有可能探索更多的應用場景,而不僅僅是和區塊鏈綁定。
這點上,Solidity 通過指令和鏈交互帶來的一個問題是Solidity&EVM 生態完全和鏈的綁定了,運行就需要模擬一個鏈的環境。這限制了Solidity 拓展到其他場景。
關於智能合約編程語言的未來,有許多不同的看法,大體上有四種:
1. 不需要圖靈完備的智能合約語言,Bitcoin 的那種script 就夠用了。沒有圖靈完備的智能合約,就很難實現通用的仲裁能力,會局限住鏈的應用場景。這點可以看我以前的一篇文章《開啟比特幣智能合約的「三把鎖」》。
2. 不需要專門的智能合約語言,用已有的編程語言就夠了,這個觀點我們上面已經分析了。
3. 需要一種圖靈完備的智能合約語言,但應用場景也僅僅在鏈上,類似於數據庫中的存儲過程腳本。這是大多數當前智能合約開發者的觀點。
4. 智能合約編程語言會推廣到其他場景,最終變為一種通用的編程語言。
最後一種可以稱做智能合約語言最大化主義者,我個人持這種觀點。理由也很簡單,在Web3 世界裡,無論是遊戲還是其他應用,如果遇到爭議,需要有一種數字化的爭議仲裁方案。而區塊鍊和智能合約關鍵的技術點就是關於狀態和計算的證明,當前這個領域摸索出來的仲裁機制,完全可以使用到更通用的場景中去。當用戶安裝一個應用,擔心應用不安全,希望應用能提供狀態和計算證明的時候,也就是應用開發必須要選擇用智能合約實現應用核心邏輯的時候。
總結
這篇從鏈上智能合約的實現途徑上,以及當前智能合約在依賴和組合性上遇到的難題,用盡可能通俗的語言闡述了Move 在這個方向上做的嘗試,以及基於這些嘗試帶來的生態構建的可能性。
考慮到文章也比較長了,很多方面還沒表述到,我會基於這個題目寫一個系列,這裡做個預告:
為什麼是Move 之生態構建能力
這是本文。
為什麼是Move 之智能合約的安全
智能合約的安全是一個廣泛關注的問題,佈道Move 的文章也喜歡提“安全”這個特性。但如何比較不同的編程語言之間的安全性?有句俗話說,你不能阻止一個人向自己的腳開槍,編程語言是一個工具,開發者用這個工具向自己的腳開槍的時候,編程語言本身能做些什麼事情?智能合約讓不通組織的程序運行在同一個進程中,最大化了編程語言的作用,但也帶來了新的安全挑戰。這篇文章將從一個整體的視角去討論這個問題。
為什麼是Move 之狀態爆炸與分層
Move 在編程語言內部實現了狀態隔離,同時也給這個領域的解決方案提供了更多的可能性。合約可以更自由的處理狀態的存儲位置,比如將狀態存儲在用戶自己的狀態空間,這樣更利於實現狀態計費,激勵用戶釋放空間。比如是否可以真正實現狀態在不同的層之間的遷移,從而將Layer1 的狀態遷移到Layer2 ,從而在根本上解決狀態爆炸問題?這篇文章將探討一些這個方向的可能性。
相關鏈接
1. https://github.com/move-language/move Move 項目的新倉庫
2. awesome-move: Code and content from the Move community 一個Move 相關項目的資源集合,包括公鏈以及Move 實現的庫
3. Soulbound (vitalik.ca) Vitalik 關於NFT 靈魂綁定的文章
4. SIP22 NFT Starcoin 的NFT 標準,包括IdentifierNFT 的說明
5. 開啟比特幣智能合約的「三把鎖」 (jolestar.com)