初探Move語言:Part 2-Move如何描述虛擬資產

James Shieh
技術保鮮盒
Published in
15 min readJun 24, 2023

這一系列的文章,我想談談Move相較於Solidity有什麼特別之處,不過許多人可能對於這兩種語言都很陌生,甚至不太了解區塊鏈以及去中心化的概念,為了能與大家分享我研究新技術時的喜悅 — — 特別是它們能帶來什麼樣的可能性,我想盡可能地用簡短的文字幫大家建構區塊鏈的世界觀,這個世界觀不免有我主觀的想像,但我認為在這樣的脈絡下,比較容易講述程式語言對區塊鏈世界的影響。

他們說,「來吧,我們要建造一座城和一座塔,塔頂通天,為了揚我們的名,免得我們被分散到世界各地。」。但是耶和華降臨看到了世人所建造的城和塔。耶和華說,「看哪,他們都是一樣的人,說著同一種語言,如今他們既然能做起這事,以後他們想要做的事就沒有不成功的了。」讓我們下去,在那裡打亂他們的語言,讓他們不能知曉別人的意思。於是耶和華使他們分散到了世界各地,他們也就停止建造那座城。因為耶和華在那裡打亂了天下人的言語,使眾人分散到了世界各地,所以那座城名叫巴別。 — — 創世記11:4–9

上一篇:

基於智能合約實現的代幣特性

我們若細究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做了些權衡:

  1. 沒有指標(pointer)之類的動態指派運算。這使得驗證工具可以簡單且精確的對程式進行安全性分析,避免執行複雜的調用分析。
  2. 有限的修改:Move會確保在同個時間下,一個數值只有一個可修改的參考(mutable reference)。
  3. 模組化:類似物件導向的封裝概念,其邏輯對外不透明,對內透明。可強化資料的抽象,並保障模組內對資源的操作安全性。這樣的設計,可以使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 變數在調用函數後仍然有效且值不變。
  • copymove 的使用取決於數據的類型。對於資源類型(例如,這裡的 Coin),只能使用 move,因為資源必須保證唯一性,不能被複製。對於非資源類型(例如,這裡的 u64 整數),可以選擇使用 copymove

資源無法被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_valueto_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 資源。

以下是該函數的具體步驟:

  1. let transaction_sender_address: address = GetTxnSenderAddress();:獲取交易的發送者的地址。
  2. let coin_ref: &mut Coin = BorrowGlobal<Coin>(move(transaction_sender_address));:創建一個指向發送者帳戶中 Coin 資源的可變引用。
  3. let coin_value_ref: &mut u64 = &mut move(coin_ref).value;:創建一個指向 Coin 資源中 value 字段的可變引用。
  4. let coin_value: u64 = *move(coin_value_ref);:獲取 Coin 資源的值。
  5. RejectUnless(copy(coin_value) >= copy(amount));:檢查發送者帳戶中的 Coin 資源的值是否大於或等於要提取的數量。如果不是,則拒絕交易。
  6. *move(coin_value_ref) = move(coin_value) - copy(amount);:從發送者帳戶中的 Coin 資源的值中扣除要提取的數量。
  7. let new_coin: Coin = Pack<Coin>(move(amount));:創建一個新的 Coin 資源,其值等於要提取的數量。
  8. 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等明確的方法,讓開發者在撰寫程式時,能夠直接意識到這些資源的所有權變化。

下一篇:

--

--

James Shieh
技術保鮮盒

Find something more important than you are and dedicate your life to it.