初探Move語言:Part 2-Move如何描述虛擬資產
這一系列的文章,我想談談Move相較於Solidity有什麼特別之處,不過許多人可能對於這兩種語言都很陌生,甚至不太了解區塊鏈以及去中心化的概念,為了能與大家分享我研究新技術時的喜悅 — — 特別是它們能帶來什麼樣的可能性,我想盡可能地用簡短的文字幫大家建構區塊鏈的世界觀,這個世界觀不免有我主觀的想像,但我認為在這樣的脈絡下,比較容易講述程式語言對區塊鏈世界的影響。
上一篇:
基於智能合約實現的代幣特性
我們若細究Solidity或其他區塊鏈程式語言的特性,會發現其在描述資產上,有一些根本性的問題。
這裡做個簡單的知識點補充:
1. 原生代幣(native token):每一種區塊鏈都會有一種原生代幣,例如在Ethereum這個鏈上的原生代幣是ETH(Ether)、在Solana這個鏈上的原生代幣是SOL。每種鏈就像是一個國家,原生代幣就是這個國家的通用貨幣,你必須使用它來支付交易手續費。
2. 智能合約(smart contract):寫入區塊鏈的程式碼,因為區塊鏈具有不可逆性與公開性,將程式碼寫入區塊鏈後,就有合約的作用——利用不可修改的程序來擬定各種規則,可保障參與合約的利害關係人的權益。
3. ERC-20:以太坊區塊鏈上的一種智能合約代幣的協議標準。這個標準描述了如何創造一種代幣,並使其具備存款、轉帳等交易行為。
稀缺性是無法擴展的
許多公鏈的原生代幣本身具有稀缺性,但這樣的稀缺性是無法擴展的。所以在這些鏈上發行的代幣(例如狗狗幣),必須自己實現各種交易邏輯,這也是為什麼在Ethereum上會有ERC-20的代幣標準,其描述代幣的總發行量以及如何使用這個代幣進行交易等邏輯。換言之,除了原生代幣外,那些基於智能合約所實現的代幣都是次等代幣,必須依賴開發者去維護其稀缺性以及交易安全,例如the DAO事件是駭客發現智能合約的漏洞,可以做重入攻擊不斷提款,這是透過智能合約創建代幣時,因交易邏輯的漏洞破壞了代幣本身的稀缺性。
顯然地,這些遵循ERC-20標準的代幣無法繼承原生代幣本身的稀缺性與安全性。同理,無論是BEP-20等在其他鏈上的代幣標準,也有一樣的問題。
無法確切地表達虛擬資產
BTC, ETH都以整數形態進行編碼,但資產其實更像是一種有各種屬性與操作方法的結構。當我們撰寫程式去操作用整數進行編碼的資產,特別容易出錯或必須用一些奇怪的方式來撰寫程式。
存取控制不夠靈活
存取控制是指"誰能執行某個操作",例如,只有你可以將自己的資產轉移到別人的帳戶下。既有的區塊鏈系統是用基於公鑰的簽章來實現存取控制的,至於要如何定義客製化的存取控制規則?這些鏈沒有一個明顯的方法作為指引。
Move語言的特性
Move設計了一些特性,讓開發者可以更確切地在鏈上表達虛擬資產,同時又能有效地表達稀缺性(Scarcity),並且對於資產的所有權(Onwership)有更靈活的存取控制。
資源(Resource)
資源是一種具有特定屬性的結構,我們可以根據需要客製化其屬性,資源可類比成物件導向的類別(Class),就如同前述地,虛擬資產有其特殊的屬性特徵,無論是NFT、代幣,我們都需要適當地表達他們。因此透過資源這種可以客製化屬性的結構來描述虛擬資產,會更加適合。
安全性
Move拒絕不滿足資源安全性、型別安全性、記憶體安全性等關鍵特性的程式碼,藉以保障其安全性。
可驗證性
為了保障前述的安全性,Move需要對程式碼進行驗證,然而不可能對鏈上資料進行實時的檢查,這樣太耗費運算資源了。因此Move做了些權衡:
- 沒有指標(pointer)之類的動態指派運算。這使得驗證工具可以簡單且精確的對程式進行安全性分析,避免執行複雜的調用分析。
- 有限的修改:Move會確保在同個時間下,一個數值只有一個可修改的參考(mutable reference)。
- 模組化:類似物件導向的封裝概念,其邏輯對外不透明,對內透明。可強化資料的抽象,並保障模組內對資源的操作安全性。這樣的設計,可以使Move的靜態驗證工具單獨的驗證模組,確保模組內的邏輯是符合安全性的,可不考慮模組外的調用方是如何與模組互動的。
Move的靜態驗證工具利用上述這些特性,以便有效且精確地檢查執行階段的錯誤(例如整數溢位)與一些程式功能的正確性。
實作案例
public main(payee: address, amount: u64) {
let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));
0x0.Currency.deposit(copy(payee), move(coin));
}
這段程式碼是一個 Move 程序的主函數,它從發送者的帳戶中提取一定數量的貨幣,然後將這些貨幣存入收款人的帳戶。以下是每行程式碼的解釋:
public main(payee: address, amount: u64) {
:這是主函數的定義,它接受兩個參數:payee
是收款人的地址,amount
是要轉移的貨幣數量。let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));
:這行程式碼從發送者的帳戶中提取amount
數量的貨幣。這裡使用了0x0.Currency.withdraw_from_sender
函數(在Move中稱為Procedure),該函數從發送者的帳戶中提取指定數量的貨幣並返回。返回的貨幣被存儲在coin
變數中。其中:- 0x0是帳戶地址,Move的任何Module、Procedure、Resource都被定義在某個帳戶下
- 0x0.Currency是0x0這個帳戶下的一個模組(module),模組名稱為Currency
- 0x0.Currency.Coin是在0x0.Currency這個模組下定義的一個資源(Resource),資源名稱為Coin
0x0.Currency.deposit(copy(payee), move(coin));
:這行程式碼將coin
變數中的貨幣存入payee
地址的帳戶。這裡使用了0x0.Currency.deposit
函數,該函數接受一個地址和一個貨幣值,並將貨幣值存入該地址的帳戶。- 在 Move 程序語言中,
copy
是一個關鍵字,用於從一個變數創建一個副本,而不會改變原始變數的值。這在處理不可變數據或需要保留原始數據的情況下非常有用。 - 在這個特定的例子中,
copy(amount)
會創建amount
變數的一個副本,並將該副本傳遞給0x0.Currency.withdraw_from_sender
函數。這樣做的原因是,Move 語言中的move
語義會將原始數據 "移動" 到新的位置,並使原始數據無效。使用copy
可以確保amount
變數在調用函數後仍然有效且值不變。 copy
和move
的使用取決於數據的類型。對於資源類型(例如,這裡的Coin
),只能使用move
,因為資源必須保證唯一性,不能被複製。對於非資源類型(例如,這裡的u64
整數),可以選擇使用copy
或move
。
資源無法被copy,只能被move,同時,Move也不允許你引用了資源卻沒有明確的move它(因為這可能會造成資源丟失),這些是Move用來保證資源安全性的機制
public deposit(payee: address, to_deposit: Coin) {
let to_deposit_value: u64 = Unpack<Coin>(move(to_deposit));
let coin_ref: &mut Coin = BorrowGlobal<Coin>(move(payee));
let coin_value_ref: &mut u64 = &mut move(coin_ref).value;
let coin_value: u64 = *move(coin_value_ref);
*move(coin_value_ref) = move(coin_value) + move(to_deposit_value);
}
這段程式碼是一個名為 deposit
的 Move 函數,它將一定數量的 Coin
存入指定的 payee
帳戶。以下是每行程式碼的解釋:
public deposit(payee: address, to_deposit: Coin) {
:這是函數的定義,它接受兩個參數:payee
是收款人的地址,to_deposit
是要存入的Coin
。let to_deposit_value: u64 = Unpack<Coin>(move(to_deposit));
:這行程式碼正在解包傳入函數的Coin
資源,提取其值(類型為u64
),並將其存儲在to_deposit_value
變數中。- 在 Move 語言中,
Unpack
是一種操作,用於解包一個結構體並獲取其內部的值。在你給出的代碼中,Unpack<Coin>(move(to_deposit))
這行代碼正在解包to_deposit
變數中的Coin
結構體,並獲取其內部的值。 Unpack
是 Move 的內建操作,它可以將一個結構體解包為一組值。這是一種反向操作,與Pack
操作相對,Pack
操作可以將一組值打包成一個結構體。- 需要注意的是,
Unpack
操作會消耗掉原來的結構體,也就是說,一旦一個結構體被Unpack
,那麼這個結構體就不能再被使用了。這是因為 Move 語言的設計原則之一是資源不能被copy,只能被move,所以當一個結構體被Unpack
之後,它就被移動出了原來的變數,不能再被使用了。 let coin_ref: &mut Coin = BorrowGlobal<Coin>(move(payee));
:這行程式碼從全局狀態中借用payee
帳戶的Coin
資源,並將其存儲在coin_ref
變數中。- 這裡可以發現Move和Solidity的一個明顯的差異 — — Solidity是把用戶資產存在合約之中,用戶本身不擁有資產,但Move則會在用戶底下建立一個資產對應的資源實體,需要透過BorrowGlobal來引用用戶底下相應的資產(本例為Coin)
let coin_value_ref: &mut u64 = &mut move(coin_ref).value;
:這行程式碼正在獲取coin_ref
所指向的Coin
資源的值的可變引用,並將其存儲在coin_value_ref
變數中。- 這裡我們可以看到Move的其中一個特性 — — 有限的修改。Move會確保在同個時間下,一個數值只有一個可修改的參考(mutable reference)。在 Move 語言中,
&mut
是一種用於創建可變引用的操作符。當你看到&mut
,你可以理解為它正在創建一個可以被用來修改其所指向的值的引用。 - 需要注意的是,
&mut
創建的可變引用有一些重要的限制: - 你不能同時擁有一個值的多個可變引用,這是為了防止數據競爭(data race)。
- 你不能同時擁有一個值的可變引用和不可變引用,這也是為了防止數據競爭。
- 當你擁有一個值的可變引用時,你不能再對這個值本身進行操作,直到你放棄這個可變引用。
let coin_value: u64 = *move(coin_value_ref);
:這行程式碼正在讀取coin_value_ref
所指向的值,並將其存儲在coin_value
變數中。*move(coin_value_ref) = move(coin_value) + move(to_deposit_value);
:這行程式碼正在將coin_value
和to_deposit_value
相加,並將結果存儲在coin_value_ref
所指向的位置。}
:這是函數的結束括號。
public withdraw_from_sender(amount: u64): Coin {
let transaction_sender_address: address = GetTxnSenderAddress();
let coin_ref: &mut Coin = BorrowGlobal<Coin>(move(transaction_sender_address));
let coin_value_ref: &mut u64 = &mut move(coin_ref).value;
let coin_value: u64 = *move(coin_value_ref);
RejectUnless(copy(coin_value) >= copy(amount));
*move(coin_value_ref) = move(coin_value) - copy(amount);
let new_coin: Coin = Pack<Coin>(move(amount));
return move(new_coin);
}
這段程式碼定義了一個名為 withdraw_from_sender
的公開函數,該函數從交易發送者的帳戶中提取一定數量的 Coin
資源。
以下是該函數的具體步驟:
let transaction_sender_address: address = GetTxnSenderAddress();
:獲取交易的發送者的地址。let coin_ref: &mut Coin = BorrowGlobal<Coin>(move(transaction_sender_address));
:創建一個指向發送者帳戶中Coin
資源的可變引用。let coin_value_ref: &mut u64 = &mut move(coin_ref).value;
:創建一個指向Coin
資源中value
字段的可變引用。let coin_value: u64 = *move(coin_value_ref);
:獲取Coin
資源的值。RejectUnless(copy(coin_value) >= copy(amount));
:檢查發送者帳戶中的Coin
資源的值是否大於或等於要提取的數量。如果不是,則拒絕交易。*move(coin_value_ref) = move(coin_value) - copy(amount);
:從發送者帳戶中的Coin
資源的值中扣除要提取的數量。let new_coin: Coin = Pack<Coin>(move(amount));
:創建一個新的Coin
資源,其值等於要提取的數量。return move(new_coin);
:返回新創建的Coin
資源。
這個函數的主要用途是在發送者帳戶中提取一定數量的 Coin
資源,並將其作為一個新的 Coin
資源返回。這可以用於實現轉賬等功能。
Move的Procedure
Move在調用Procedure時,需要明確的Procedure ID做為參數,因此所有的Procedure call都是靜態決定的(不需要等到執行期間再做驗證) — — 因此沒有函數指標之類的特性,除此之外,在模組間的依賴關係是被建構成非循環的(acyclic), 非循環模組的組合和動態指派的限制:所有屬於模組程序的stack frames必須是連續的。先天上可以避免Ethereum的重入攻擊。
靜態檢查
所有的Move程式都必須通過靜態檢查才能發佈到鏈上。靜態檢查包刮Structural checks、Semantic checks,是否有非法的引入內部procedure等等。
總結
Move用了類似物件導向設計的概念來表達虛擬資產,Module/Resource/Procedure的關係類似於Class/Object/Method。並且限制了一些程式語言常用的動態指派特性,讓大部分的問題都可以透過靜態檢查的方式處理,因此可以避免重入攻擊,同時降低鏈上計算的成本。同時針對資源的存取控制,有move, copy, pack, unpack等明確的方法,讓開發者在撰寫程式時,能夠直接意識到這些資源的所有權變化。
下一篇: