Rust智能合约养成日记: 用 Rust 开发智能合约

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. 环境配置
    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,这里可以简单的看作一个字典类型。keyvalue 都是普通的字符串类型。
    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,返回值为 SelfSelf 在 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<String>?? {
    9?? ? ? ? ?return?self.records.get(&account_id);
    10?? ? }
    11?}
    ?

    第 2 行 impl 关键字表明,我们在对 StatusMessage 做具体的实现。
    第 3-6 行定义了方法 setstatus。该函数用来设置当前合约的状态。其中第三个声明了方法名和变量。该函数共有两个变量,分别为 &mut selfmessage: 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。不同于 setstatusgetstatus 会返回一个 None 或者是 String 类型的值,这里我们用 Option 表示。
    第 9 行则是通过查询用户给定的 account_id,得到对应的 message。
    本期总结和预告
    这是 BlockSec 针对 Rust 合约开发的第一期 blog,本期我们讲述了 Rust 合约的背景,以及如何基于 NEAR 链去创建一个简单的合约。下一期我们将进一步描述如何利用 Rust 对我们创建的合约编写单元测试用例,从而调试我们的合约。