Move 语言中文白皮书(四) ----- Move 概览

4. Move概览

我们通过简单的点对点支付所涉及的交易脚本和模块来介绍 Move 的基础知识。该模块是真实的Libra 代币实现的简化版本。 示例交易脚本演示了模块外的恶意或粗心程序员不能违反模块resources的关键安全不变量。 示例模块展示了如何实现利用强数据抽象来建立和维护这些不变量的资源。

本节中的代码片段是用 Move 中间代码(IR) 的变体编写的。Move IR 足够高级,可以编写人类可读的代码,但又足够低级,可以直接转换为 Move 字节码。我们在 IR 中展示代码是因为基于堆栈的 Move 字节码更难阅读,我们目前正在设计 Move 源语言(参见第 7 节)。我们注意到,在执行代码之前,Move 类型系统提供的所有安全保证都在字节码级别进行了检查。

4.1. 点对点支付脚本

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));
}

该脚本有两个输入:付款接收者的帐户地址和一个无符号整数,表示要转移给接收者的代币数量。 执行此脚本的效果很简单:代币金额将从交易发送方转移到收款方。 这发生在两个步骤中。
第一步,发送者从存储在 0x0.Currency 的模块中调用一个名为withdraw_from_sender 的过程。 正如我们将在 4.2 节中解释的那样,0x0 是存储模块的帐户地址,Currency是模块的名称。 该过程返回的价值硬币是类型为0x0.Currency.Coin的资源值。
第二步,发送方通过resours类型的代币的值转移到 0x0.Currency 模块的存款程序中,将资金转移给收款人。

这个例子很有趣,因为它非常巧妙。Move 的类型系统将拒绝可能导致不良行为的相同代码的小变体。特别是,类型系统确保resources永远不会被复制、重用或丢失。例如,脚本的以下三个更改将被类型系统拒绝:

  • 通过将 move(coin) 更改为 copy(coin) 来复制代币。请注意,示例中变量的每次使用都包含在 copy() 或 move() 中。 Move 遵循 Rust 和 C++,实现了 move 语义。 每次读取Move变量 x 都必须指定用法是将 x 的值移出变量(使 x 不可用)还是复制该值(使 x 可继续使用)。 u64 和地址等不受限制的值都可以复制和移动。 但是resources值只能移动。 尝试复制resources值(例如,在上面的示例中使用 copy(coin))将导致字节码验证时出错。
  • 通过两次写入 move(coin) 来重用代币。 在上面的示例中添加行 0x0.Currency.deposit(copy(some_other_payee), move(coin)) 将使发送者“花费”两次代币——第一次使用 payee,第二次使用 some_other_payee。 这种不良行为对于实物资产是不可能的。 幸运的是,Move 会拒绝这个程序。 变量coin在第一次移动后变得不可用,第二次移动将触发字节码验证错误。
  • 因忽视移动(代币)而损失代币金额。 Move 语言实现了必须移动一次的线性 [3][23] resources。 未能移动resources(例如,通过删除上面示例中包含 move(coin) 的行)将触发字节码验证错误。 这可以保护 Move 程序员免于意外或故意丢失resources的跟踪。 这些保证超出了纸币等实物资产的可能范围。

我们使用术语resources安全来描述 Move resources永远不会被复制、重用或丢失的保证。 这些保证非常强大,因为 Move 程序员可以实现也享有这些保护的自定义resources。 正如我们在 3.1 节中提到的,即使是 Libra 代币也是作为自定义resources实现的,在 Move 语言中没有特殊状态。

4.2. Currency 模块

在本节中,我们将展示上述示例中使用的 Currency 模块的实现如何利用resources安全性来实现安全的可替代资产。 我们将首先解释一下运行 Move 代码的区块链环境。

入门:Move执行模型。 正如我们在第 3.2 节中解释的那样,Move 有两种不同的程序:

  • 交易脚本,如第 4.1 节中概述的示例
  • 模块,如我们将很快介绍的Currency模块。

像示例这样的交易脚本包含在每个用户提交的交易中,并调用模块的过程来更新全局状态。 执行交易脚本是个原子性的执行逻辑,只有全部代码执行成功才会交易脚本执行完成,否则交易脚本内的代码全部不执行。并且脚本执行的所有写入都提交到全局存储,要么执行因错误而终止(例如,由于断言失败或超出 gas错误),并且没有任何事情发生。 交易脚本是一段一次性的代码——在它执行之后,它不能被其他交易脚本或模块再次调用。

相比之下,模块是在全局状态下发布的一段长期存在的代码。 上面示例中使用的模块名称 0x0.Currency 包含发布模块代码的帐户地址 0x0。 全局状态被构造为从帐户地址到帐户的映射。

每个帐户可以包含零个或多个模块和一个或多个resources值。 例如,地址为 0x0 的账户包含模块 0x0.Currency 和类型为 0x0.Currency.Coin 的resources值。 地址 0x1 的账户有两个resources和一个模块; 地址 0x2 的帐户有两个模块和一个资源值。
相比之下,模块是在全局状态下发布的一段长期存在的代码。 上面示例中使用的模块名称 0x0.Currency 包含发布模块代码的帐户地址 0x0。 全局状态被构造为从帐户地址到帐户的映射。
figure1

帐户最多可以包含一个给定类型的resources值和最多一个具有给定名称的模块。 不允许地址 0x0 的帐户包含额外的 0x0.Currency.Coin resources或另一个名为 Currency 的模块。 但是,地址 0x1 的帐户可以添加一个名为 Currency 的模块。 在这种情况下,0x0 也可以拥有 0x1.Currency.Coin 类型的resources。 0x0.Currency.Coin 和 0x1.Currency.Coin 是不同的类型,不能互换使用; 声明模块的地址是类型的一部分。

请注意,帐户中最多允许给定类型的单个resource不是限制性的。 此设计为顶级帐户值提供了可预测的存储架构。 程序员仍然可以通过定义自定义包装resource(例如,resource TwoCoins { c1: 0x0.Currency.Coin, c2: 0x0.Currency.Coin })在帐户中保存给定resource类型的多个实例。

声明 Coin 资源。 在解释了模块如何适应 Move 执行模型之后,我们可以开始看 Currency 模块的内部:

module Currency {
  resource Coin { value: u64 }
  // ...
}

以上代码声明了一个Currency 的模块,里面包含了一个 名为Coin的 resource 类型。Coin 是一种结构类型,具有 u64 类型的单个字段值(64 位无符号整数)。 Coin 的结构在 Currency 模块之外是不透明的。 其他模块和交易脚本只能通过模块公开的公共过程写入或引用值字段。 同样,只有 Currency 模块的程序可以创建或销毁 Coin 类型的值。 该方案支持强大的数据抽象——模块作者可以完全控制其声明resource的访问、创建和销毁。 在 Currency 模块公开的 API 之外,另一个模块可以对 Coin 执行的唯一操作是移动。 resource安全禁止其他模块复制、破坏或双重移动resource。

deposit过程的实现。 让我们研究上一节中交易脚本调用的 Currency.deposit 过程是如何工作的:

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);
}

面向代码阅读者者的可读性代码逻辑。此过程将 Coin resource作为输入,并将其与存储在收款人帐户中的 Coin resource相结合。 它通过以下方式实现:

  1. 销毁输入的 Coin 并记录它的值。
  2. 获取收款人下面的唯一 resource类型的coin 引用。
  3. 加上传递给程序的 Coin 的值来更新到收款人的 Coin 的值。

此过程的面向机器语言的某些方面值得解释。 绑定到 to_deposit 的 Coin 资源归存款程序所有。 要调用该过程,调用者需要将绑定到 to_deposit 的 Coin 移动到被调用者(这将阻止调用者重用它)。

在第一行调用的 Unpack 过程是用于操作模块声明的类型的几个内置模块之一。 Unpack 是删除 T 类型resource的唯一方法。它将 T 类型的resource作为输入,销毁它,并返回绑定到resource字段的值。 像 Unpack 这样的内置模块只能用于当前模块中声明的resource。 在 Unpack 的情况下,此约束防止其他代码销毁 Coin,这反过来又允许 Currency 模块为销毁 Coin resource设置自定义的前提条件(例如,它可以选择只允许销毁0值的Currency)。

第三行调用的 BorrowGlobal 过程也是一个内置模块。 BorrowGlobal 将地址作为输入并返回对在该地址下发布的 T 的唯一实例的引用。 这意味着上面代码中的 coin_ref 类型是 &mut Coin,表面这是对 Coin resource的可变引用,而不是拥有Coin resource的 本身。 下一行移动绑定到 coin_ref 的参考值,以获取 Coin 值字段的参考 coin_value_ref。 该过程的最后几行读取收款人 Coin resource的先前值,并改变 coin_value_ref 以反映存款金额。

我们注意到 Move 类型系统无法捕获模块内的所有实现错误。 例如,类型系统不会确保所有存在的coin的总量通过存款调用来保存。 如果程序员在最后一行写错了 *move(coin_value_ref) = 1 + move(coin_value) + move(to_deposit_value),类型系统将毫无疑问地接受该代码。 这表明了明确的职责分工:在模块范围内为 Coin 建立适当的安全不变量是程序员的工作,而类型系统的工作是确保模块外的 Coin 客户端不会违反这些不变量。

实现withdraw_from_sender过程。 在上面的实现中,通过deposit程序存入资金不需要任何授权——任何人都可以调用存款。 相比之下,从帐户中取款必须受到授予货币resource所有者独占权限的访问控制策略的保护。 让我们看看我们的点对点支付交易脚本调用的withdraw_from_sender 过程是如何实现这个授权的:

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);
}

这个过程几乎是deposit的逆过程,但也不全是。 如下:

  1. 获取对发送者帐户下发布的 Coin 类型的唯一resource的引用。
  2. 将引用的 Coin 的值减少输入数量。
  3. 创建并返回一个具有对应发送金额的新Coin。

此过程执行的访问控制检查有些微妙。 withdraw过程允许调用者指定传递给 BorrowGlobal 的地址,但withdraw_from_sender 只能传递 GetTxnSenderAddress 返回的地址。 此过程是允许Move代码从当前正在执行的交易中读取数据的几个交易内置程序之一。 Move 虚拟机在交易执行之前验证发送者地址。 以这种方式使用 BorrowGlobal 内置确保交易的发送者只能从她自己的 Coin resource中提取资金。

与所有内置模块一样,BorrowGlobal 只能在声明 Coin 的模块内调用。 如果 Currency 模块没有公开返回 BorrowGlobal 结果的过程,则 Currency 模块之外的代码无法获取对全局存储中发布的 Coin resource的引用。

在减少交易发送者的 Coin resource的值之前,该过程使用 RejectUnless 指令断言代币的价值大于或等于输入的数额。 这确保了发件者不能提取超过她所拥有的金额。 如果此检查失败,当前交易脚本的执行将停止,并且它执行的任何操作都不会更新于全局状态。

最后,该过程将发送者的 Coin 的值按数量减少,并使用 Unpack 的逆操作(内置的 Pack 模块)创建一个新的 Coin resource。 Pack 创建一个类型为 T 的新resource。与 Unpack 一样,Pack 只能在resource T 的声明模块内部调用。这里,Pack 用于创建一个类型为 Coin 的resource new_coin 并移动它 给 调用者。 调用者现在拥有此 Coin resource,并且可以将其移动到任何她喜欢的地方。 在我们第 4.1 节的示例交易脚本中,调用者选择将 Coin 存入收款人的账户。