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. 环境配置
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 合约
终于,我们到了正题。在这里,我们将通过深入剖析一个个智能合约的项目,带大家了解并且掌握如何利用 Rust 编写智能合约。如果大家对 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<String, String>,
5 }
接下来,我们将仔细的分析上述的五行代码。第 1,2 行以 # 开头,类似注解。事实上,这是 Rust 中的一种宏的表现形式。它会接收第 3-5 行作为输入,根据宏的定义,产生输出。例如,第一行中的 #[nearbindgen] 事实上是在 near-sdk-macros-version 包中通过 nearbindgen 函数定义,这是利用宏自动生成注入代码的地方 (Macros-Auto-Generated Injected Code,简称 M.A.G.I.C. )。
如果不理解,没关系。我们只需要知道第 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(br.to_vec()),
5 }
6 }
7 }
其中,第 1 行声明了 这是对于 StatusMessage 默认值的一个实现。第 2 行声明该方法名称为 default,返回值为 Self。Self 在 Rust 中即表示当前的模块作用域,具体来说,即代表一个 StatusMessage 实例。而第 3-5 行即为该实例的定义。由于该实例仅包含 records 一个类型为 LookupMap 的变量。通过传入一个二进制数组 br.tovec(), 即可将 LookupMap 初始化。其中 LookupMap 的 new 方法由 NEAR 自己定义,br.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<String> {
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 表示。
第 9 行则是通过查询用户给定的 account_id,得到对应的 message。
本期总结和预告
这是 BlockSec 针对 Rust 合约开发的第一期 blog,本期我们讲述了 Rust 合约的背景,以及如何基于 NEAR 链去创建一个简单的合约。下一期我们将进一步描述如何利用 Rust 对我们创建的合约编写单元测试用例,从而调试我们的合约。