Rust智能合約養成日記(1)
BlockSec
2022-03-29 10:25
本文约3914字,阅读全文需要约16分钟
BlockSec除了提供審計服務之外,也希望可以從安全開發的角度給予社區更多的支持。

1. EVM or WASM?

隨著Ethereum 的普及,我們在談論智能合約時,往往默認都是利用Solidity 語言開發,基於EVM 的智能合約。然而,由於Ethereum 本身出塊時間慢,交易所需手續費高的一些缺點,越來越多的優化技術和新的公鏈得以推出。而WASM 則是其中的一個代表性技術。作為一種全新的二進制語法,WASM 有著諸多的優點,如指令體積小,運行速度快,並且內存安全。因此,運行在WASM 上的智能合約可以大大減少佔用的區塊鏈資源,明顯的提升出塊速度和效率,並且運行時更加穩定,使得用戶獲得更好的使用體驗。 WASM 支持多種不同的前端開發語言,包括Rust、C、C++、TypeScript、AssemblyScript 等。考慮到適配以及工具鏈,並且語言本身的安全性,Rust 是非常好的選擇之一。

2. BlockSec 的選擇

BlockSec 的使命是讓整個Defi 生態更加的安全。因此,我們除了提供審計服務之外,也希望可以從安全開發的角度給予社區更多的支持。基於Rust 和WASM 的諸多優點,我們決定專門針對這一技術棧給大家帶來一系列的分享,也希望大家可以持續的關注我們。我們調研瞭如今一些比較流行的公鏈項目,其中NEAR 公鏈也採用了同樣的技術棧。 NEAR 原生支持WASM 合約,並且支持Rust 語言和AssemblyScript 開發智能合約。因此,我們將以NEAR 公鍊為基礎,展開我們的分享與討論。

3. 用Rust 開發智能合約

Rust 語言由Mozilla 主導開發,程序編譯後的運行速度驚人,且有相當高的內存利用率,並且支持函數式和麵向對象的編程風格。也許很多同學還對Rust 這門語言比較陌生。不過不用擔心,從本期博客開始,BlockSec 會跟大家一起撥開Rust 的迷霧,讓每個人都能利用Rust 開發出高效,安全的智能合約。

4.1 IDE 使用

4.1 IDE 使用

當我們在學習利用一門新的語言去開發時,選擇一個優秀的IDE 一定是有必要的。在此,BlockSec 推薦大家使用Visual Studio Code 配合Rust 的插件(例如Rust-analyzer),幾乎可以滿足大家的日常所需。如果大家有條件,也可以嘗試一下Jetbrains Clion + Rust 插件, 學生可以免費使用哦。

4.2 安裝Rust 工具鏈

當有了一個優秀的IDE 後,我們自然還需要下載安裝Rust。 Rust 提供了非常簡單便捷的安裝方法。在Linux 系統中, 我們只需要運行如下一行代碼,即可自動下載安裝Rust。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安裝完畢後,我們可以通過執行$ rustup --version 來檢查安裝是否成功。 rustup 作為Rust 工具鏈的管理器,提供了安裝、刪除、更新、選擇和管理這些工具鍊及其相關部件的方法。再此我們需要通過執行如下命令,將WASM (WebAssembly) 目標添加到工具鏈 :

$ rustup target add wasm32-unknown-unknown

5. 第一個Rust 合約

5.1 Rust 的包管理器

5.1 Rust 的包管理器

隨著整個開源社區對Rust 的支持,各種各樣的第三方庫層出不窮。為了更好的管理這些庫,Cargo 應運而生。上述的安裝命令,也會同時幫大家安裝Cargo。 Cargo 可協助開發者處理諸多任務,例如創建新的Rust 項目,下載並編譯Rust 項目所依賴的庫,以及完整地構建整個項目等。

5.2 創建第一個Rust 合約項目

當我們準備好開發環境後,首先利用Cargo 新建一個合約項目,並命名為StatusMessage。

$ cargo init --lib StatusMessage

該項目的目錄樹如下:

StatusMessage/
├── Cargo.toml
└── src
          └── lib.rs

5.3 聲明一個合約

一個智能合約(Smart Contract) 往往需要維護一組合約狀態數據。如下一段編寫於src/lib.rs 的代碼聲明了一個簡單的合約,叫做StatusMessage。

1  #[near_bindgen]
2  #[derive(BorshDeserialize, BorshSerialize)]
3  pub struct StatusMessage {
4      records: LookupMap,
5 }

接下來,我們將仔細的分析上述的五行代碼。第1,2 行以# 開頭,類似註解。事實上,這是Rust 中的一種宏的表現形式。它會接收第3-5 行作為輸入,根據宏的定義,產生輸出。例如,第一行中的#[nearbindgen] 事實上是在near-sdk-macros-version 包中通過nearbindgen 函數定義,這是利用宏自動生成注入代碼的地方(Macros-Auto-Generated Injected Code,簡稱MAGIC )。

如果不理解,沒關係。我們只需要知道第1,2 行的作用即可。具體的來說,被#[nearbindgen] 註解的struct 將會成為NEAR 上的一個智能合約。而其他的struct 只是普通的struct。因此[nearbindgen] 是由NEAR 開發並且提供給開發者使用的包。而第2 行中的#[derive(BorshDeserialize, BorshSerialize)] 則是用來做序列化和反序列化,從而將合約的狀態可以在鏈上以二進制格式傳輸。第3-5 行即為一個名為StatusMessage 的結構體,其維護了一個智能合約的狀態。而狀態的內容在第4 行中被描述。這一結構體中只含有一個成員變量,名為records。其類型為LookupMap,這裡可以簡單的看作一個字典類型。 key 和value 都是普通的字符串類型。

5.4 設定合約默認值

當我們聲明了一個合約後,我們往往需要定義其默認值。如下代碼設定了合約StatusMessage 的默認值。

1  impl Default for StatusMessage {
2      fn default() -> Self {
3          Self {
4              records: LookupMap::new(b"r".to_vec()),
5         }
6     }
7 }

其中,第1 行聲明了這是對於StatusMessage 默認值的一個實現。第2 行聲明該方法名稱為default,返回值為Self。 Self 在Rust 中即表示當前的模塊作用域,具體來說,即代表一個StatusMessage 實例。而第3-5 行即為該實例的定義。由於該實例僅包含records 一個類型為LookupMap 的變量。通過傳入一個二進制數組 b"r".tovec(),即可將LookupMap 初始化。其中LookupMap 的new 方法由NEAR 自己定義,b"r".tovec() 表明存儲於該LookupMap 中鍵的前綴。

5.5 定義合約方法

當我們用一個結構體定義了合約的狀態後,我們還需要定義一系列方法,從而可以通過外部交易,去調用這些暴露出來的方法。如下是兩個定義的方法,分別可以修改和獲得當前合約中的records 值。注意,定義合約的方法時,也需要我們加上#[near_bindgen],如第1 行所示 :

1  #[near_bindgen]
2  impl StatusMessage {
3      pub fn set_status(&mut self, message: String) {
4          let account_id = env::signer_account_id();
5          self.records.insert(&account_id, &message);
6     }
7
8      pub fn get_status(&self, account_id: String) -> Option   {
9          return self.records.get(&account_id);
10     }
11 }

第2 行impl 關鍵字表明,我們在對StatusMessage 做具體的實現。

第3-6 行定義了方法setstatus。該函數用來設置當前合約的狀態。其中第三個聲明了方法名和變量。該函數共有兩個變量,分別為&mut self 和message: String。 &mut 表示對self 的引用,並且可能修改self 的內容。而message: String 表明了message 的類型為String。同時該函數用關鍵字pub 修飾,注意,只有被pub fn 修飾的函數才可以被外部的交易調用,表明其是public。

第4 行會定義一個局部變量accountid, 其值通過env::signeraccountid() 中獲取,表明發起這筆交易簽名的用戶id。

第5 行將accountid 做為鍵,message 做為值插入到records 中。注意,message 是一個String 類型的變量,由用戶傳入。而&message 則表示對message 的引用。

第8-10 行則聲明了另外一個函數名為getstatus。不同於setstatus,getstatus 會返回一個None 或者是String 類型的值,這裡我們用Option 表示。

本期總結和預告

本期總結和預告

這是BlockSec 針對Rust 合約開發的第一期blog,本期我們講述了Rust 合約的背景,以及如何基於NEAR 鏈去創建一個簡單的合約。下一期我們將進一步描述如何利用Rust 對我們創建的合約編寫單元測試用例,從而調試我們的合約。

BlockSec
作者文库