这篇文章上次修改于 620 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

Move 还有其它变种,如 Aptos Move。本文中介绍的某些 Move 概念(即对象和相关功能)只适用于 Move 的 Sui 变体,而不适用于其他变体。

1 Move 编程模型

所有的智能合约(模块)都包含在同一类系统中,可以直接相互调用,而不需要通过中间的 API 或接口。

1.1 对象

有三种不同类型的对象(在 Sui 中):

  • 自有对象(owned objects)

    只有拥有该对象的用户才能在交易中使用它。每个自有对象都与一个公钥关联(运行时存储在对象元数据中),任何时候你想在交易中使用对象,你都需要提供相应签名(现在支持 Ed25519,即将支持 ECDSA 和 K-of-N 多签名)。

  • 共享对象(shared objects)

    不需要拥有任何私钥就可以在交易中使用它们(任何人都可以使用它们)。任何自有对象都可以被共享(由其所有者),一旦一个对象被共享,它将永远保持共享——永远不能被转移或再次成为自有对象。

  • 不可变对象(immutable objects)

    一旦一个对象被标记为不可变,它的字段就不能再被修改。与共享对象类似,这些对象没有所有者,可以被任何人使用。

2 Move 的安全性

2.1 结构

定义一个结构(struct)类型和你所期望的差不多:

struct Foo {
  x: u64,
  y: bool
}

在上面的代码片断中定义的结构将受以下限制:

  • 它只能在定义该结构的模块中被实例化(“打包”)和销毁(“解包”)——也就是说,你不能从任何其他模块的任何函数中实例化或销毁一个结构实例。
  • 结构实例的字段只能在其模块中被访问(因此也可以被改动)
  • 不能在其模块之外克隆或复制结构实例
  • 不能将一个结构实例存储在其他结构实例的字段中

这意味着,如果在其他模块的函数中处理这个结构的实例,我们将无法改动其字段、克隆它、将其存储在其他结构的字段中,或将其丢弃(必须通过函数调用将其传递到其他地方)。

结构是一种资源。它不能随意被凭空创造,不能被复制,也不能被意外地销毁。因此,我们确实在这里失去了一些灵活性,但我们失去的灵活性正是我们所希望的,因为这使对资源的操作变得直观而安全。

此外,Move 允许我们通过向结构添加能力(capability)来放宽其中一些限制。有四种能力:键、存储、复制和删除。你可以将这些能力的任何组合添加到一个结构中。

struct Foo has key, store, copy, drop {
  id: UID,
  x: u64,
  y: bool
}

下面是它们的作用:

  • key 允许一个结构成为一个对象(Sui,core Move 情况略有不同)。如前所述,对象是持久化的,如果是自有对象,需要用户签名才能在智能合约调用中使用。当使用键能力时,结构的第一个字段必须是具有 UID 类型的对象 ID。这将给它一个全局唯一的 ID,能够用它进行引用。
  • store 允许将该结构作为一个字段嵌入另一个结构中
  • clone 允许从任何地方任意复制/克隆该结构
  • drop 允许从任何地方任意销毁该结构

2.2 币

币在 Sui 中实现了类似 ERC20/SPL 代币的功能,是 Sui Move Library 的一部分。它的定义是这样的:

// coin.move
struct Coin<phantom T> has key, store {
    id: UID,
    balance: Balance<T>
}
// balance.move
struct Balance<phantom T> has store {
    value: u64
}

币类型具有 key 和 store 的功能。键意味着它可以作为一个对象使用。这允许用户直接拥有币(作为一个顶层对象)。当你拥有一个币时,除你之外,其他人甚至不能在交易中引用它(更不用说使用它)。存储意味着,币可以作为一个字段嵌入到另一个结构中,这对于可组合性很有用。

由于没有 drop 功能,币不能在函数中被意外丢弃(销毁)。当然,通过调用币模块中的 coin::burn 函数来销毁一个币是可能的,但你需要有目的地这样做(你不会这样意外操作的)。

没有 clone 能力意味着没有人可以复制币,从而凭空创造新的供应。创造新的供应可以通过 coin::mint 函数来完成,而且只能由该币的国库能力对象(treasury capability)的所有者调用。

另外,由于泛型(generics)的存在,每个不同的硬币都是独特类型。由于两个币只能通过 coin::join 函数加在一起(而不是直接访问它们的字段),这意味着不可能把不同类型的币值加在一起(币A+币B)——因为没有这种签名的函数。类型系统能够保护我们免受坏账影响。

2.3 字节码验证

Move 验证器是一个静态分析工具,它分析 Move 字节码并确定它是否遵守所需的类型、内存和资源安全规则。所有上传到链上的代码都需要通过验证器。当你试图上传一个 Move 模块到链上时,节点和验证器将首先通过验证器运行,然后才允许提交。如果任何模块试图绕过 Move 的安全规则,它将被验证器拒绝,并且不会被发布。

Move 字节码和验证器是 Move 的核心创新之处。它实现了一个以资源为中心的直观编程模型,在其他处是无法实现的。最关键的是,它允许结构化类型跨越信任边界而不失去其完整性。

在 Solana 上,跨程序边界是没有类型安全的——每个程序通过手动从原始账户数据解码来加载实例,这需要手动进行关键的安全检查,也没有本地资源安全。相反,资源安全必须由每个智能合约单独实现。这确实能够实现足够的可编程性,但与 Move 的模式相比,它在很大程度上阻碍了可组合性和人机工程学,因为 Move 的模式对资源有原生支持,它们可以安全地流入和流出不信的代码。

在 Move 中,类型确实存在于各个模块中——类型系统是全局的。这意味着不需要 CPI 调用,账户编码/解码,账户所有权检查等——你只需直接调用另一个模块中的函数与参数。整个智能合约的类型和资源安全由编译/发布时的字节码验证来保证,不需要像 Solana 那样在智能合约层面上实现,然后在运行时检查。

参考

Smart Contract Development — Move vs. Rust