Move Vs. Solana Rust

guanghua
发布于 阅读 2203

智能合约开发——Move vs. Rust

深入研究 Move,一种用于智能合约开发的新颖编程语言,以及它与 Solana 上使用的现有基于 Rust 的模型的比较。

1. 介绍

最近几周和几个月,围绕 Aptos 和 Sui、新兴的高性能 L1 以及这些新链不可或缺的 Move 智能合约编程语言引起了很多关注。 一些开发人员已经开始积极转向 Move,他们发誓这是智能合约开发的未来,而另一些开发人员则有点谨慎,他们认为 Move 只是另一种智能合约编程语言,从根本上说,它并没有提供比现有编程模型更多的功能。 加密货币投资者还想知道这些新的 L1 有什么特别之处,以及它们如何与 Solana 相抗衡,后者目前是高性能 L1 类别的主要参与者,尤其是使用 Rust 作为智能合约编程的基础。

但是到目前为止,我们所看到的讨论并没有深入到充分理解这项新技术给我们带来的必要深度。 这在讨论的两端都是正确的——Move 怀疑论者淡化了 Move 而没有充分理解它的一些更微妙(但非常重要)的方面,而 Move 粉丝则称赞 Move 没有尽头,但未能充分阐明究竟是什么让它如此 伟大的。 这留下了一个很大的中间立场和很多模棱两可的地方,因此一直在关注这个讨论的外部旁观者、加密货币开发人员和投资者无法自信地形成他们的意见。

我最近对 Move 进行了深入研究,并在 Solana 上拥有智能合约开发经验 在这个讨论之外。

在本文中,我将深入探讨 Move、其新颖的编程模型、Sui 区块链以及它如何利用 Move 的功能,以及它与 Solana 及其编程模型的比较。

为了突出 Move 的特性,我将 Solana/Rust 与 Sui/Move 进行比较。我这样做的主要原因是,当您将某件事与您已经熟悉的另一件事进行比较而不是尝试自己理解它时,更容易理解它。值得注意的是,Move 还有其他版本,例如 Aptos Move,它们在某些事情上略有不同。本文的重点不是讨论 Move 不同版本之间的细微差别,而是展示 Move 的一般优势以及它与 Solana 编程模型的比较,因此为简单起见,我决定只使用一种版本( Sui Move) 通篇。因此,我在本文中介绍的某些 Move 概念(即对象和相关功能)仅适用于 Move 的 Sui 版本,而不适用于其他版本。虽然 Move 的其他版本不一定具有这些概念,但它们使用不同的机制(例如全局存储)实现相同的功能。但即便如此,本文中讨论的所有主要 Move 优势都适用于所有 Move 集成(原生支持 Move 字节码),包括 Aptos。我特别选择 Sui 的原因很简单,因为我对它更熟悉,而且我觉得它更直观,更容易以文章的形式呈现。

再一次,尽管我在本文中对 Solana 和 Sui 进行了比较,但我的意图并不是要给任何人或任何事情蒙上阴影,而只是强调这些技术的不同方面以及它们的各种好处和权衡,以便于理解。

2. solana 编程模型

为了充分理解本文中的要点,需要对 Solana 编程模型有一定的了解。 如果您不熟悉 Solana 的编程模型,我建议您阅读我关于 Solana 智能合约编程的文章,其中涵盖了遵循此模型所需的所有概念。

为了快速复习,我也将在这里做一个简短的总结。 如果您已经熟悉 Solana 的编程模型,则可以跳过本章。

在 Solana 上,程序(智能合约)是无状态的,因为它们不能自行访问(读取或写入)在整个交易过程中持续存在的任何状态。 要访问或保持状态,程序需要使用帐户。 每个账户都有一个唯一的地址(Ed25519 密钥对的公钥),可以存储任意数据。

我们可以将 Solana 的帐户空间视为全局键值存储,其中键是帐户地址(公钥),值是帐户数据。 然后程序通过读取和修改其值在此键值存储之上运行。

账户有所有权的概念。 每个帐户由一个(并且只有一个)程序拥有。 当帐户归程序所有时,程序可以更改其数据。 程序不允许改变他们不拥有的帐户(但允许从中读取)。 这些检查是由运行时动态完成的,通过比较程序执行前后的帐户状态,如果发生非法突变,则交易失败。

每个账户还有一个与之关联的私钥(对应的公钥是它的地址),有权访问该私钥的用户可以用它签署交易。 使用这种机制,我们在 Solana 智能合约中实现了权限和所有权功能——例如 为了访问某些资金,智能合约可以要求用户提供必要的签名。

在其他进行程序调用时,客户端需要指定该程序在调用期间将访问哪些帐户。 这样交易处理运行时可以调度非重叠交易并行执行,同时保证数据一致性。 这是 Solana 实现高吞吐量的设计特点之一。

程序可以通过 CPI 调用来调用其他程序。 这些调用的工作方式与来自客户端的调用几乎相同——调用程序需要指定被调用程序将访问的帐户,并且被调用程序将执行所有相同的输入检查,就好像它是从客户端调用的一样( 它不信任调用程序)。

PDA 帐户是一种特殊的帐户,它使程序能够在不拥有或存储私钥的情况下提供帐户签名。 PDA 保证只有为其生成 PDA 的程序才能为其创建签名(而不是其他用户和程序)。 当一个程序需要通过 CPI 调用与另一个程序交互并提供权限(例如,实现一个保管库)时,这很有用。 PDA 保证除了程序之外没有人可以直接访问程序的资源。 PDA 也可用于在确定的地址创建帐户。

这些是 Solana 上安全智能合约编程的基本构建块。 同样,如果您觉得这些概念中的任何一个不清楚,我强烈建议您阅读我关于 Solana 智能合约编程的文章,该文章更深入地介绍了这些(和其他内容)中的每一个。

在某种程度上,您可以将 Solana 程序视为操作系统中的程序,将帐户视为文件,任何人都可以在其中自由执行任何程序,甚至部署自己的程序。 当程序(智能合约)运行时,它们将读取和写入文件(帐户)。 所有文件都可供所有程序读取,但只有对文件具有所有权权限的程序才能写入。 程序也可以执行其他程序,但它们不以任何方式相互信任——无论谁执行程序,它都需要假设输入是潜在的对抗性的。 由于任何人都可以在全球范围内访问此操作系统,因此将本机签名验证支持添加到程序中,以便为用户启用权限和所有权功能……这不是一个完美的类比,但它是一个有趣的类比。

3. Move 编程模型

在 Move 中,智能合约作为模块发布。 模块由函数和自定义类型(结构)组成。 结构由可以是原始类型(u8、u64、bool…)或其他结构的字段组成。 函数可以调用其他函数——可以在同一个模块中,也可以在其他模块中(如果它们是公开的)。

将其放在 Solana 的上下文中,就好像所有智能合约都作为模块发布在单个程序中。 这意味着所有智能合约(模块)都包含在同一类型系统中,并且可以直接相互调用,而无需通过中间 API 或接口。 这是非常重要的,其含义将在整篇文章中详细讨论。

3.1 对象

在我们继续之前,重要的是要注意以下对象概念特定于 Move 的 Sui 变体,并且在 Move 的其他集成中可能会略有不同(例如 Aptos 或 Diem/core Move)。 即便如此,在其他 Move 版本中,也有类似的解决方案可以实现相同的目标(状态持久性),但并不太相似。 我在这里介绍 Sui 对象的主要原因是因为本文后面的代码示例基于 Move 的 Sui 版本,还因为对象比例如对象更易于理解。 核心 Move 中的全局存储机制。 但重要的是,本文讨论的 Move 的所有主要优点都适用于所有 Move 集成(原生支持 Move 字节码),包括 Aptos。 要了解 Sui 和 Aptos 版本的不同之处,我们为什么创建 Sui Move文章和 Move on Aptos 文档页面是一个好的开始。

对象是由运行时存储的结构实例,并跨交易保持状态。

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

  • 拥有者对象
  • 共享对象
  • 不可更改对象

拥有者对象是属于用户的对象。 只有拥有该对象的用户才能在交易中使用它。 所有权元数据是完全透明的并由运行时处理。 它是使用公钥加密实现的——每个拥有的对象都与一个公钥相关联(存储在运行时对象的元数据中),并且任何时候你想在交易中使用一个对象,你都需要提供相应的签名(现在支持 Ed25519 ECDSA 和 K-of-N 多重签名支持即将推出)。

共享对象类似于拥有对象,但它们没有与之关联的所有者。 因此,您不必拥有任何私钥就可以在交易中使用它们(任何人都可以使用它们)。 任何拥有者对象都可以(由其所有者)共享,并且一旦共享了一个对象,它将永远保持共享状态——它永远不会被转移或再次成为拥有的对象。

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

Move 编程模型非常直观和简单。 每个智能合约都是一个模块,由函数和结构定义组成。 结构在函数中实例化,可以通过函数调用传递给其他模块。 为了使结构在交易中持久化,我们将其转换为可以拥有者、共享或不更改的对象(特定于 Sui,这在其他 Move 变体中略有不同)。 而已!

4. 安全的Move

所以我们已经在 Move 中看到了:

  • 您可以将您拥有(或共享)的任何对象传递给任何模块中的任何函数
  • 任何人都可以发布(可能是对抗性的)模块
  • 没有模块拥有结构的概念,这将使所有者模块拥有对其进行更改的唯一权限,就像 Solana 的帐户一样——结构可以流入其他模块并嵌入到其他结构中

现在的问题是,是什么让它安全? 是什么阻止了某人发布对抗模块、获取共享对象(如 AMM 池)并将其发送到对抗模块,然后该模块将继续耗尽资金?

在 Solana 中,有一个帐户所有权的概念,其中只有拥有帐户的程序才被允许对其进行更改。 但是在 Move 中,没有模块拥有对象的概念,您可以将对象发送到任意模块中——不仅是对对象的引用,而且是整个对象本身,按值的传递方式。 并且运行时没有进行特定检查以确保该对象在通过不受信任的模块时没有被非法修改。 那么是什么保证了这个对象的安全呢? 来自哪里的不可信代码不会滥用该对象的保证?

好吧,这就是 Move 的新颖性……让我们谈谈资源。

4.1. Structs

定义一个结构类型几乎是你所期望的:

struct Foo {
  x: u64,
  y: bool
}

到目前为止一切顺利——这也是你在 Rust 中定义结构的方式。 但是 Move 中的结构有一些独特之处,那就是在 Move 中,与传统编程语言相比,模块对如何使用和不能使用它们的类型有更多的控制。 如上面代码片段中定义的结构将具有以下限制:

  • 它只能在定义结构的模块内实例化(“打包”)和销毁(“解包”) - 即您不能从任何其他模块的任何函数内部实例化或销毁结构实例
  • 结构实例的字段只能从其模块访问(并因此发生更改)
  • 您不能在其模块之外克隆或复制结构实例
  • 您不能将结构实例存储在其他结构实例的字段中

这意味着如果您在另一个模块的函数中处理此结构的实例,您将无法改变其字段、克隆它、将其存储在另一个结构的字段中或删除它(您将 必须通过函数调用将其传递给其他地方)。 可能结构体的模块实现了一些函数,这些函数可以从我们的模块中调用,但除此之外,我们不能直接为外部类型做任何这些事情。 这使模块可以完全控制它们的类型如何使用和不能使用。

现在,似乎有了这些限制,我们失去了很多灵活性。 这是真的——在传统编程中处理这样的结构会非常麻烦,但事实上,这正是我们在智能合约中想要的。 毕竟,智能合约开发是关于对数字资产(资源)进行编程。 如果你看一下上面描述的结构,这正是它的本质——它是一种资源。 不能凭空随意创造,不能复制,不能随意破坏。 所以我们确实在这里失去了一些灵活性,但我们失去的灵活性正是我们想要失去的那种灵活性。 这使得使用资源变得直观且安全。

此外,Move 允许我们通过向结构添加功能来放松其中的一些限制。 有四种功能:key、store、copy 和 drop。 您可以将这些功能的任意组合添加到结构中:

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

他们是这样做的:

  • key -- 允许结构成为对象(特定于 Sui,核心 Move 略有不同)。 如前所述,对象是持久化的,并且在拥有对象的情况下,需要在智能合约调用中使用用户签名。 使用 key 能力时,struct 的第一个字段必须是 UID 类型的对象的 ID。 这将为其提供一个全局唯一 ID,可用于引用它。
  • store -- 允许将结构作为字段嵌入到另一个结构中
  • copy -- 允许从任何地方任意复制/克隆结构
  • drop -- 允许从任何地方任意销毁结构

本质上,Move 中的每个结构都是默认的资源。 功能使我们能够细化地放松这些限制,并使它们的行为更像传统的结构。

4.2 Coin

为了更好地说明这一点,让我们以 Coin 类型为例。 Coin 在 Sui 中实现了类似 ERC20 / SPL Token 的功能,并且是 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
}

您可以在 Sui 代码库中找到完整的模块实现。

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

由于没有丢弃功能,因此 Coin 不会在函数中意外丢弃(销毁)。 这是一个非常好的功能——这意味着你不会意外丢失Coin。 如果你正在实现一个接收 Coin 作为参数的函数,那么在函数结束时,你需要明确地对它做一些事情——要么将其传输给用户,将其嵌入到另一个对象中,要么将其发送到另一个函数中 一个电话(这又需要对它做些什么)。 当然可以通过调用 coin 模块中的 coin::burn 函数来销毁 Coin,但是你需要有目的地这样做(你不会意外地这样做)。

没有克隆能力意味着没有人可以复制代币,从而凭空创造新的供应。 创建新供应可以通过 coin::mint 函数完成,并且只能由该币的国库能力对象的所有者调用(该对象最初转移给货币创建者)。

另外,请注意,多亏了泛型,每个不同的硬币都会有自己独特的类型。 而且由于两个硬币只能通过 coin::join 函数相加(而不是直接访问它们的字段),这意味着根本不可能添加不同类型硬币的值(硬币 A + 硬币 B) - 没有 该签名的功能。 类型系统在这里保护我们免于做坏账。

在 Move 中,资源的资源安全性由其类型定义。 考虑到 Move 具有全局类型系统,这可以实现更自然和更安全的编程模型,其中资源可以直接传入和传出不受信任的代码,同时保持其安全性。 乍一看,这似乎没什么大不了的,但实际上,这对智能合约的可组合性、人体工程学和安全性有很大的好处。 这将在第 5 章中更深入地讨论。

4.3. 字节码验证

如前所述,Move 智能合约是作为模块发布的。 并且任何人都可以创建任意模块并将其上传到区块链以供任何人执行。 我们还看到 Move 对如何使用结构有一定的规则。

那么是什么保证了这些规则被任意模块所遵守呢? 是什么阻止某人上传带有特制字节码的模块,例如 接收一个 Coin 对象,然后通过直接改变其内部字段来绕过这些规则? 通过这样做,您可以非法夸大您拥有的硬币数量。 仅字节码语法就可以做到这一点。

字节码验证可以防止这种滥用。 Move 验证器是一个静态分析工具,它分析 Move 字节码并确定它是否遵守所需的类型、内存和资源安全规则。 所有上传到链上的代码都需要通过验证者。 当您尝试将 Move 模块上传到链上时,节点和验证器将首先通过验证器运行它,然后才能被允许提交。 如果任何模块试图绕过 Move 的安全规则,它将被验证者拒绝并且不会被发布。 这就是为什么用特制的字节码不可能破坏类型或资源安全规则的原因——验证会阻止你将这样的模块上传到链上!

Move 字节码和验证器是 Move 的核心新颖性。 这使得以资源为中心的直观编程模型成为可能,这是不可能的。 关键是它允许结构化类型跨越信任边界传递而不会失去其完整性。

在 Solana 上,智能合约是程序,而在 Move 中,它们是模块。 这似乎只是语义上的差异,但事实并非如此,它具有巨大的意义。 不同之处在于,在 Solana 上没有跨程序边界的类型安全——每个程序通过从原始帐户数据手动解码来加载实例,这涉及手动进行关键安全检查。 也没有本地资源安全。 相反,资源安全必须由每个智能合约单独实施。 这确实允许足够的可编程性,但与 Move 的模型相比,它极大地阻碍了可组合性和人机工程学,Move 的模型对资源有原生支持,并且它们可以安全地流入和流出不受信任的代码。

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

5. Solana vs. Move

现在我们已经了解了 Move 编程的工作原理以及它从根本上安全的原因,让我们从可组合性、人机工程学和安全性的角度更深入地了解它对智能合约编程的影响。 在这里,我将比较 Move/Sui 开发与 EVM 和 Rust/Solana/Anchor,以帮助了解 Move 的编程模型带来的好处。

5.1. 闪电贷

让我们从闪贷示例开始。 闪电贷是 DeFi 中的一种贷款,借出的金额必须在借入的同一笔交易中偿还。 这样做的主要好处是,由于交易是原子的,贷款可以完全无抵押。 这可以用于例如 无需本金即可在资产之间进行套利。

实现这一点的主要困难是——你如何从闪贷智能合约中保证借出的金额将在同一笔交易中得到偿还? 为了使贷款能够无抵押,交易需要是原子的——即如果借出的金额没有在同一笔交易中归还,整个交易需要失败。

EVM 具有动态调度,因此可以使用可重入实现,如下所示:

  • 闪贷用户创建并上传自定义智能合约,调用时将通过调用将控制权传递给闪贷智能合约
  • 闪贷智能合约然后将请求的贷款金额发送到自定义智能合约并调用自定义智能合约中的executeOperation()回调函数
  • 然后,自定义智能合约将使用收到的借出金额来执行其所需的操作(例如套利)
  • 自定义智能合约完成操作后,需要将借出的金额返还给闪贷智能合约
  • 至此,自定义智能合约的 executionOperation() 将完成,控制权将返回到闪贷智能合约,该合约将检查借出金额是否已正确归还
  • 如果自定义智能合约没有正确返回借出的金额,整个交易将失败

这很好地实现了所需的功能,但问题是它依赖于可重入性,我们非常希望禁止它在智能合约编程中成为可能。 这是因为重入本质上是非常危险的,并且是许多漏洞的根本原因,包括臭名昭著的 DAO hack

Solana 在这里做得更好,因为它不允许重入。 但是如果没有可重入性和闪贷智能合约回调自定义智能合约的可能性,您如何在 Solana 上实施闪贷? 好吧,这要归功于指令内省。 在 Solana 上,每笔交易都包含多条指令(智能合约调用),您可以从任何指令中检查同一交易中存在的其他指令(它们的程序 ID、指令数据和账户)。 这使得实施闪电贷款成为可能,如下所示:

  • 闪贷智能合约实现借款和还款指令
  • 用户通过在同一笔交易中将借入和还款指令调用堆叠在一起来创建闪贷交易。 借入指令在执行时将使用指令自省检查是否在同一交易中稍后安排还款指令。 如果还款指令调用不存在或无效,则此阶段交易将失败
  • 在借入和还款调用之间,借入的资金可以被介于两者之间的任何其他指令任意使用
  • 在交易结束时,repay 指令调用会将资金返还给 flash Lender 智能合约(在借入指令中使用自省检查该指令的存在)

出于好奇,这里有一个原型实现(链接

该解决方案运行良好,但仍不理想。 指令自省在某种程度上是一种特殊情况,而不是在 Solana 中常用的东西,因此它的使用在开发人员需要掌握的概念数量和实现本身的技术方面都有开销,因为有一些 需要适当考虑的细微差别。 还有一个技术限制——因为repay指令需要在交易中静态存在,所以不可能在交易执行期间通过CPI调用动态调用repay。 这几乎不会破坏交易,但在将其与其他智能合约集成时,它在一定程度上限制了代码的灵活性,并且还将更多的复杂性推到了客户端。

Move 还禁止动态调度和重入,但与 Solana 不同,它有一个非常简单和自然的闪贷解决方案。 Move 的线性类型系统允许创建保证在交易执行期间仅使用一次的结构。这就是所谓的“烫手山芋”模式——一种没有键、存储、删除或克隆功能的结构。实现这种模式的模块通常具有一个实例化结构的函数和另一个破坏它的函数。由于“烫手山芋”结构没有删除、键或存储功能,因此保证将调用其“销毁”函数以使用它。即使我们可以将它传递给任何模块中的任意数量的其他函数,最终它还是需要在“销毁”函数中结束。这仅仅是因为没有其他方法可以摆脱它,并且验证者要求在交易结束时对其进行处理(它不能随意丢弃,因为没有丢弃能力)。

让我们看看如何利用它来实施闪电贷:

  • 闪贷智能合约实现“烫手山芋” Receipt 结构
  • 当通过调用贷款函数进行贷款时,它将向调用者发送两个对象——请求的资金(一个硬币)和一个收据,它是要偿还的贷款金额的记录
  • 然后借款人可以将收到的资金用于其所需的操作(例如套利)
  • 借款人完成其预期操作后,需要调用 repay 函数,该函数将接收借入资金和收据作为参数。 这个函数保证在同一个交易中被调用,因为调用者没有其他方法可以摆脱 Receipt 实例(它不允许被丢弃或嵌入到另一个对象中,这是由验证者断言的)
  • 还款函数通过读取收据中嵌入的贷款信息来检查是否退回了正确的金额。

可以在这里找到一个示例实现。

Move 的资源安全功能使 Move 中的闪电贷成为可能,而无需使用重入或内省。 他们保证收据不能被不受信任的代码修改,并且需要在交易结束时返回到还款函数。 有了这个,我们可以保证在同一笔交易中返回正确数量的资金。

该功能是使用基本语言原语完全实现的,Move 实现不会像 Solana 实现需要特殊处理的交易那样受到集成开销的影响。 此外,没有把复杂性被推到客户端。

5.2. 铸币 权限锁

为了进一步突出 Move 编程模型的优势,我在 Solana (Anchor) 和 Sui Move 中都实现了“Mint Authority Lock”智能合约进行比较。

“铸币权限锁”智能合约所做的是将代币铸币的功能扩展为允许多个白名单方(机构)铸币,而不仅仅是一个铸币厂。 智能合约所需的功能如下(适用于 Solana 和 Sui 实现):

  • 原始代币铸币机构创建了一个“铸币锁”,这将使我们的智能合约能够规范代币的铸币。 调用者成为”铸币锁“的管理员。
  • 然后,管理员可以为可以提供给其他方的锁创建额外的铸币权限,并允许他们在需要时使用锁铸币。
  • 每个铸币权限都有每日可铸币数量的限制。
  • 管理员可以随时禁止(和取消禁止)任何铸币权限。
  • 管理员权限可以转移给另一方。

该智能合约可用于例如 在原始铸币权限(管理员)仍保留铸币厂控制权的情况下,将代币的铸币功能提供给其他用户或智能合约。 如果没有这个,我们必须将铸币的完全控制权交给另一方,这并不理想,因为我们必须相信它不会滥用它。 并且不可能向多方授予许可

可以在此处 (Solana) 和此处 (Sui) 找到这些智能合约的完整实施。

现在让我们看一下代码,看看实现有何不同。 以下是此智能合约的完整 Solana 和 Sui 实现的并排代码屏幕截图:

立即引人注目的是,对于相同的功能,Solana 实现的大小是 Sui 的两倍多(230 LOC 对 104)。 这已经很重要了,因为更少的代码通常意味着更少的错误和更短的开发时间。

那么这些额外的线路是从哪里来的 Solana 呢? 如果我们仔细查看 Solana 代码,我们可以将其分为两部分——指令实现(智能合约逻辑)和账户检查。 指令实现与我们在 Sui 上的实现有点接近——136 LOC 与 Sui 上的 104。 额外的行可归因于两个 CPI 调用的样板文件(每个约 10 LOC)。 最显着的区别是由于帐户检查(上面屏幕截图中标记为红色的部分)在 Solana 上是必需的(实际上很关键),但在 Move 中不需要。 账户支票占该智能合约(91 LOC)的约 40%。

Move 不需要帐户检查。 这不仅是有益的,因为 LOC 减少了。 消除进行帐户检查的必要性非常重要,因为事实证明正确实施这些检查非常棘手,而且如果您在那里犯了一个错误,通常会导致严重的漏洞和用户资金的损失。 事实上,一些最大的(就用户资金损失而言)Solana 智能合约漏洞是由不正确的账户检查引起的账户替换攻击:

显然,摆脱这些检查将是一件大事。

那么,Move 如何在没有这些检查的情况下保持安全呢? 让我们仔细看看检查的实际作用。 以下是 mint_to 指令所需的账户检查(授权持有者通过锁调用它来铸造代币)

有 6 个检查(以红色突出显示):

    1. 检查提供的锁定帐户是否属于此智能合约并且属于 MintLock 类型。有必要传递锁,因为它用于 CPI 调用 Token Program 进行铸币(存储着权限)。
    1. 检查提供的铸币权限帐户是否属于提供的锁。铸币授权账户持有授权状态(其公钥、是否被禁止等)
    1. 检查指令调用者是否拥有该授权所需的密钥(所需授权签署了交易)。
    1. 需要传入token目标帐户,因为token程序将在 CPI 调用中对其进行修改(添加余额)。mint检查在这里并不是绝对必要的,因为如果在 CPI 调用中传递了错误的帐户,则会失败,但仍然进行检查是一种好习惯。
    1. 类似于第4点。
  • 检查token程序帐户是否正确传参。

我们可以看到账户检查(在本例中)分为以下五类:

  • 帐户所有权检查(1、2、4、5)
  • 账户类型检查(1, 2, 4, 5))
  • 账户实例化检查(是否传入了某个账户类型的正确实例)(2, 5)
  • 账户签名检查(3)
  • 程序账户地址检查(6)

不过,在 Move 中,不需要进行帐户检查或任何等效操作。我们只有函数签名:

mint_balance 函数只需要四个参数。 在这四个中,只有 lock 和 cap 代表对象(有点类似于帐户)。

那么怎么可能在 Solana 中我们需要声明 6 个帐户并手动为它们执行各种检查,而在 Move 中我们只需要传入 2 个对象并且不需要显式检查?

好吧,在 Move 中,其中一些检查是由运行时透明地完成的,其中一些是在编译时由验证器静态完成的,而其中一些根本不是构造所必需的。 让我们来看看:

  • 帐户所有权检查——由于 Move 类型系统的设计,没有必要。 Move 结构只能通过其模块中定义的函数进行修改,而不能直接进行。字节码验证保证结构实例可以自由流入不受信任的代码(其他模块)而不会被非法修改。
  • 账户类型检查——没有必要,因为Move类型存在于智能合约中。 类型定义嵌入在模块二进制文件中(这是在区块链上发布并由 VM 执行的内容)。 在编译/发布期间调用我们的函数时,验证器将检查是否传递了正确的类型。
  • 帐户实例检查——在 Move 中(有时也在 Solana 上)你会在函数体中执行此操作。在这个特定的示例中,这不是必需的,因为 lock 和 cap 参数类型的泛型类型参数 T 强制传入的 cap(铸币能力/权限)对象正确匹配其锁(每个 Coin 类型 T 只能有一个锁)。
  • 帐户签名检查——我们不直接处理 Sui 中的签名。 对象可以归用户所有。 铸币权由铸币能力对象(由管理员创建)的所有权授予。 将对该对象的引用传递给 mint_balance 函数将允许我们进行铸币。 拥有的对象只能由其所有者在交易中使用。 换句话说,对象签名检查是由运行时透明地完成的。

从本质上讲,Move 利用字节码验证来使编程模型更自然地用于编程数字资产。 Solana 的模型围绕帐户所有权、签名、CPI 调用、PDA 等展开。但如果我们退后一步考虑一下,我们会发现我们真的不想处理这些问题。 它们本身与数字资产无关——而是我们必须使用它们,因为它允许我们在 Solana 的编程模型中实现所需的功能。

在 Solana 上,由于没有字节码验证来保证更细粒度的类型或资源安全,因此您不能允许任何程序更改任何帐户,因此有必要引入帐户所有权的概念。 出于类似的原因(程序调用之间没有类型/资源安全),没有用户拥有的对象可以流入和流出程序的概念,而是我们处理帐户签名以证明权限。 而且由于有时程序还需要能够提供帐户签名,所以我们有 PDA……

虽然您可以在 Solana 上拥有与在 Move 中相同的跨程序类型和资源安全性,但您必须使用低级构建块(帐户签名、PDA ......)手动实现它。 最终,我们正在做的是使用低级原语来建模可编程资源(线性类型)。 这就是帐户检查的内容——它们是手动实现类型安全和建模资源的开销。

Move 具有对资源的原生抽象,允许我们直接使用资源,而无需引入任何低级构建块,例如 PDA。 跨智能合约边界的类型和资源安全保证由验证者断言,不需要手动实施。

5.3. Solana 可组合性的局限性

我想再举一个例子来突出 Solana 上智能合约可组合性的一些痛点。

我们在 Mint Authority Lock 示例中看到,与 Sui 相比,我们需要在 Solana 上声明更多的输入(在 Solana 上有 6 个帐户,而在 Sui 中有 2 个对象用于 mint_to 调用)。 显然,必须处理 6 个帐户比处理 2 个对象更麻烦,特别是如果您认为我们还需要对帐户实施帐户检查。 可以说这仍然是可以管理的,但是当我们开始在一个调用中组合多个不同的智能合约时会发生什么?

假设我们要创建一个执行以下操作的智能合约:

  • 它拥有来自 Token Mint Lock 程序(如上一节所述)的某个代币的铸币权。
  • 当它被调用时,它将使用其权限来铸造用户指定数量的代币,使用 AMM 将它们换成不同的代币,并在同一条指令中将它们发送给用户

这个例子的重点是说明 Mint Authority Lock 智能合约和 AMM 智能合约如何组合在一起。这是一个纯粹的假设示例,可能在现实生活中没有任何用处,但可以说明这一点——编写智能合约的现实生活示例与此并无太大不同。

帐户检查执行此操作的指令调用可能如下所示:

那是17个帐户。 每个 CPI 调用(铸币和兑换)加上程序帐户大约有 5-6 个。

在 Sui 上,等效函数的签名是这样的:

那只是3个对象。

那么,我们在 Sui 上传递的对象与在 Solana 上的帐户(3 比 17)相比,怎么会少得多呢?

从根本上说,原因是因为在 Move 中我们能够嵌入(包装)它们。 类型系统的安全保证允许我们这样做。

下面是一个 Solana 账户和一个持有 AMM 池状态的 Sui 对象的比较:

我们可以看到,在 Solana 上,我们存储了其他帐户的地址(公钥),它们就像指针一样,不存储实际数据。 为了访问这些帐户,它们需要单独传入,我们还需要手动检查是否传入了正确的帐户。在 Move 中,我们能够将结构相互嵌入并直接访问它们的值。 我们可以混合和匹配来自任何模块的类型,同时保留它们的资源和类型安全保证。 由于 Move 的全局类型系统和字节码验证支持的资源安全性,这再次成为可能。

但是,传递大量帐户的主要问题甚至不是开发人员的人体工程学。 在编写多个智能合约时必须传递(并因此检查)许多帐户会产生相当大的实施复杂性并具有安全隐患。 这些账户之间的关系可能非常复杂,并且在某个时候很难跟踪所有必要的账户检查以及它们是否已正确完成。

事实上,这就是我认为在 Cashio 漏洞利用(4800 万美元)中发生的情况。 这是启用该漏洞的(不足)帐户检查的细分。 如您所见,这些帐户检查可能会有些复杂。 开发人员可能有最好的意图正确地进行检查,但在某个时候,精神开销变得太大,很容易出错。 帐户越多,出现错误的机会就越大。

Move 的全局类型系统和更自然的编程模型意味着我们可以在达到心理开销的极限之前更安全地推进智能合约组合。

作为旁注,与 Rust/Anchor 相比,Move 的安全性方面还有一点值得考虑,这可能不是立即显而易见的。 这就是 Move 的 TCB(可信计算库)比 Rust/Anchor 小得多。 更小的 TCB 意味着更少的进入智能合约编译和执行的组件必须被信任。 这减少了可能影响智能合约的漏洞的表面积——TCB 之外的错误不会影响智能合约的安全性。

Move 的设计考虑到了减少 TCB - 做出了许多决定以尽可能减少 TCB。 字节码验证器将 Move 编译器执行的许多检查从 TCB 中取出,而在 Rust/Anchor 中,必须信任更多组件,并且安全关键错误的表面积要大得多。

6. solana 上的Move

显然,Move 很棒。 我们还知道 Solana 的设计允许使用其他编程语言为其开发智能合约。 现在的问题是——我们如何在Solana上使用Move语言

6.1 锚定全局类型安全?

在我们开始研究 Move on Solana 之前,让我们简要了解一下 Anchor 并进行一些思考实验。 也许我们可以以某种方式升级 Anchor 以提供我们从 Move 中获得的一些好处? 也许我们可以在程序调用中获得对类型安全的原生支持? 毕竟,Anchor 指令已经类似于 Move 入口函数:

// Function signatures of the "mint to" function from the
// "Mint Authority Lock" smart contract from the previous chapter.
// Sui Move
public entry fun mint_balance<T>(
  lock: &mut TreasuryLock<T>,
  cap: &mut MintCap<T>,
  amount: u64,
  ctx: &mut TxContext
) {
// Anchor
pub fn mint_to(ctx: Context<MintTo>, amount: u64) -> Result<()> {

也许如果我们以某种方式扩展 Anchor 以允许直接在指令参数中传递帐户:

// Anchor modified
pub fn mint_to(
  lock: &mut MintLock,
  authority: &mut MintAuthority,
  amount: u64
) -> Result<()> {

我们可以避免进行帐户检查吗?

在这种情况下,我们希望由运行时而不是程序来完成类型检查——运行时将读取 Anchor 的帐户鉴别符(或等价物)并能够检查传入的帐户是否匹配所需的鉴别符(前 8 个字节) 一个锚帐户)。

但请记住,Solana 不会区分对同一程序的不同指令调用,这是由程序手动实现的(在这种情况下,繁重的工作由 Anchor 完成)。 因此,为了做到这一点,运行时必须以某种方式了解不同的指令、它们的签名,并且还要了解类型的信息。

Solana 程序编译为 SBF(Solana 字节码格式,eBPF 的一种修改),并照此上传到链(并执行)。 而且 SBF 本身并没有嵌入任何可以帮助我们的类型或功能信息。 但也许我们可以修改 SBF 以允许指令和类型信息嵌入到二进制文件中? 然后,运行时可以从二进制文件中读取有关指令和签名的所需信息。

我们确实可以这样做。 实现这一点将是一项相当大的工程任务,特别是如果您认为我们需要保持与旧程序的向后兼容性,但以下是我们将获得的好处:

  • 帐户所有权和类型检查由运行时而不是在程序中完成。
  • 对于在编译时地址已知的帐户(例如程序帐户),我们可以避免从客户端传递它们,现在可以由运行时注入这些帐户。
  • 如果我们还设法将帐户约束嵌入到二进制文件中,我们可以通过在运行时动态地递归加载它们(基于嵌入的约束信息)来进一步减少客户端必须传入的帐户数量。

我们仍然没有得到:

  • 嵌入式帐户。 我们仍然必须使用 Pubkeys 来引用其他帐户,而不是直接嵌入它们。 这意味着我们不会摆脱 5.3 节中描述的帐户膨胀。
  • 在进行跨程序调用时,帐户类型检查仍然需要在运行时动态完成,而不是像 Move 那样在编译时静态完成

虽然这些好处确实不错,但从智能合约开发的角度来看,它们并没有从根本上改变太多。 在运行时而不是程序中进行类型检查可能具有一些性能优势,并且不必从客户端手动传递具有编译时已知地址的帐户,这在一定程度上改善了交互(这也可以通过工具来缓解)。 但是,尽管这些人体工程学和性能改进确实有所帮助,但我们最终仍在处理 Solana 的编程模型,它本身并不能帮助我们处理数字资产——我们仍然没有原生资源安全,我们可以 '不要嵌入帐户,所以仍然存在帐户膨胀,我们仍在处理帐户签名和 PDA……

理想情况下,我们希望所有智能合约都存在于单一类型系统中,并且能够像在 Move 中那样自由地传入和传出对象。 但是因为其他智能合约不能被信任,我们不能直接这样做。 为了解决这个问题,Solana 具有程序分离和帐户所有权——每个程序管理自己的帐户,它们通过 CPI 调用进行交互。 这是安全的,并且允许足够的可编程性,但生成的编程模型并不理想——没有全局类型系统,没有它也没有有意义的资源安全。

我们想要一个自然的编程模型,但与此同时,我们正在处理不受信任的代码。 虽然在 Solana 上我们可以安全地使用不受信任的代码,但它会在编程模型上妥协。 字节码验证使得同时拥有两者成为可能。 因此,如果没有它,我们似乎无法对编程模型进行太多改进……

6.2. Solana 字节码格式

如前所述,SBF(Solana 字节码格式),Solana 智能合约编译并存储在链上的,基于 eBPF。 在 Solana 上使用 eBPF 作为基础而不是任何其他字节码格式(如 WASM)的主要动机是,Solana 对安全和高性能智能合约执行的要求与 eBPF 设计用于内核中沙盒程序执行的要求相匹配 (它还需要安全且高效)。 您可以在 Anatoly Yakovenko 的这篇文章中阅读更多关于 Solana 智能合约环境背后的初始设计决策的信息(请注意,这篇文章来自 2018 年,其中讨论的一些内容已经过时)。

在纸面上,eBPF 似乎确实是一个可靠的选择。 它是高性能的,它是围绕安全设计的,程序大小和指令数量是有限的,它有一个字节码验证器......看起来很有前途!

但让我们看看这在实践中意味着什么。 也许我们可以以某种方式利用 eBPF 验证器来提高我们智能合约的安全性? 以下是 eBPF 验证器所做的一些事情:

  • 不允许无界循环
  • 检查程序是否为 DAG
  • 不允许出界跳跃
  • 在进行各种辅助函数调用时检查参数类型(辅助函数在内核中定义,例如用于修改网络数据包)

好吧,禁止越界跳跃似乎很有用,但其他的东西就没那么多了。 事实上,强制程序必须是 DAG 并且没有无限循环是有问题的,因为它极大地限制了可编程性(我们没有图灵完备性)。 在 eBPF 程序中需要这样做的原因是,验证程序需要确定程序在一定数量的指令内终止(这样程序就不能停止内核;这是著名的停机问题),并且gas计量不是一个选项,因为 它会过多地阻碍性能。

虽然这种权衡对于实现高性能防火墙非常有用,但对于智能合约开发来说并不是那么好,并且大部分 eBPF 验证器不能用于 Solana 程序。 事实上,Solana 甚至根本没有使用原始的 eBPF 验证器。 它使用一个(更基本的)自定义验证器,主要只是检查指令是否正确以及是否有越界跳转。

除了验证者之外,还有一些其他的 eBPF 细节对于编译智能合约来说有点问题。 就像 eBPF 在设计上允许最多 5 个参数传递给函数调用的事实一样。 这实际上意味着 Rust 标准库不能直接编译为 eBPF。 或者堆栈大小限制为 512 字节,这减少了我们可以在不分配堆的情况下传递给函数的参数大小。

因此,即使 Rust 编译为 LLVM,LLVM 有一个 eBPF 后端,甚至还支持 Rust 编译器以 eBPF 为目标,你仍然无法让 Solana 智能合约按原样编译为 eBPF。 这就是为什么 Solana 团队必须对 Rust 代码库和 eBPF LLVM 后端进行多项更改(例如,支持通过堆栈传递参数)。

由于其中一些更改本质上不是上游可用的(Rust 和 LLVM 都不是),因此 Solana 团队目前通过这些更改维护 Rust 和 LLVM 的分支。 当您执行 cargo build-bpf(构建 Solana 智能合约的规范命令)时,Cargo 将拉取这个 Solana 特定版本的 rustc 来进行智能合约编译(原来的 rustc 不起作用)。

这就是 SBF 诞生的原因——Solana 需要的一些要求与 eBPF 不兼容。 Solana 团队目前正在努力将 SBF 作为单独的 LLVM 后端进行上游化,并将其添加为 Rust 目标,以避免必须维护单独的分叉。

出于好奇,这里有一些相关的 GitHub 讨论:
为我们的 Rust 和 LLVM 分叉上游定义策略
SBFv2
BPF VM stack frame size limit is too limiting for programs
Rename toolchain target triple to solana specific name

因此,虽然 eBPF 可以作为智能合约的一种格式,但它并不像纸面上看起来那么理想。 它需要稍微修补一下,原来的验证器用处不大。

在关于 Move 和 Solana/SBF 的讨论中,我还看到了一个误解,我认为解决这个误解非常重要。 一些评论认为,主要的 Move 想法应该适用于 SBF,因为它基于 eBPF,并且它的验证器可能被用来使帐户突变检查在运行时静态而不是动态地完成。

在我看来,这是一个可疑的说法。 即使有可能证明程序不会改变他们在 eBPF 中不拥有的账户,而且 Move 确实做到了这种事情,但它肯定不是主要 Move 的想法。

Move 的主要思想是启用以资源为中心的编程模型,该模型可以自然地与不受信任的代码交互。

在实践中,这意味着:

  • 全局类型安全
  • 资源安全(key、clone、store、drop)
  • 可嵌入资源
  • 资源安全地流入和流出不受信任的代码

第 5 章中的部分(闪贷、铸币局锁定、Solana 可组合性的局限性)说明了这在智能合约安全性、可组合性和人员交互方面的重要性。 这样做的影响远远超出了跳过一些运行时安全检查以提高跨程序调用的性能。 这一点不容小觑。

现在,如果你想知道将 Move 的主要想法带到 eBPF/SBF 有多么困难,答案是——非常困难。 如果不对 eBPF 进行重大修改,就不可能强制执行诸如“此不受信任的代码不应删除 T”之类的属性。 事实上,它需要大量的修改,以至于你最终会得到一个看起来更像 Move 而不是 eBPF 的新字节码。 这肯定是一项重大的研究工作。

事实上,类似的思路首先导致了 Move 的创建。 Move 团队(当时在 Diem)最初考虑从 WASM、JVM 或 CLR 等其他格式开始,但事后添加它太难了——线性/能力是非常非传统的。 因此,Move 是从头开始设计的,其理念是通过轻量级验证程序有效地执行这些检查。

如果你仔细想想,这实际上并不令人惊讶。 归根结底,智能合约编程不是系统编程、后端编程或任何其他常规编程。 这是一种完全不同的编程类型,因此现有字节码和指令格式的特性不能在这里重用也就不足为奇了,因为它们的设计考虑了完全不同的用例。

明确地说,我并不是在批评 Solana 使用 eBPF。 事实上,我认为这是一个非常可靠的选择和团队考虑到上下文的良好判断。 可以说,事后看来,团队可能已经走了,例如 使用 WASM 而不是 eBPF,这可以避免前面提到的将智能合约编译为 eBPF 的问题,因为 WASM 在 Rust 中具有一流的支持(尽管 WASM 可能存在不同的问题),但我可以看到团队如何感觉 考虑到对性能的重视,eBPF 是一个更安全的选择。 此外,在做出这些设计选择时,甚至都没有宣布 Move,从头开始创建一种新语言对于初创公司来说肯定不是一个合理的选择。 归根结底,Solana 成功交付了高性能的 L1,这才是最重要的。

6.3. 在 Solana 上运行 Move

扩展 eBPF/SBF 以支持 Move 特性似乎很困难,而且我们可能最终会得到类似于 Move 字节码的东西。 与其尝试改进 SBF,也许我们应该以某种方式让 Move 直接在 Solana 上运行? 毕竟,Solana 非常愿意支持多种编程语言进行智能合约开发,甚至 Anatoly 也鼓励将 Move 集成到他的一些推文中。

似乎有三种方法可以在 Solana 上支持Move:

  • 将 Move VM 添加为本机加载程序(与 SBF VM 一起)
  • 将 Move VM 作为程序运行(如 Neon)
  • 编译Move到 SBF(如 Solang)

我们先讨论(3)。 这里的想法是为 Move 构建一个 LLVM 前端,以便将其编译为 SBF。 编译为 SBF 的 Move 智能合约可以像在 Rust(或任何其他编译为 SBF 的语言)中构建的智能合约一样透明地执行,并且运行时不必对 Move 有任何区别或知识。 从运行时的角度来看,这将是一个非常优雅的解决方案,因为它不需要对其或其安全假设进行任何更改。

但从智能合约方面来看,这并没有太大的影响。 虽然有真正的理由为 Move 构建 LLVM 前端,例如 对于性能或可移植性,在我看来,使用 SBF 运行时执行 Move 智能合约并没有带来太多好处。 事实上,我认为以这种方式开发智能合约比仅使用 Anchor 更糟糕。

您通过 (3) 得到的是 Solana 编程模型中的 Move 语法。 这意味着第 5 章中讨论的 Move 的所有显着优势(全局类型安全、全局资源安全、可嵌入对象……)将不存在。 相反,我们仍然必须像在 Rust 中一样处理帐户检查、CPI 调用、PDA 等。 而且由于 Move 不支持宏,因此无法使用 eDSL 实现像 Anchor 这样的框架来简化其中的一些工作,因此代码将类似于原始 Rust(但可能更糟)。 Rust 标准库和生态系统也不可用,因此必须在 Move 中重新实现帐户序列化和反序列化等功能。

Move 不太适合与其他编程模型一起使用。 这是因为它经过专门设计,能够编译为 Move 字节码并通过验证程序。 由于围绕能力和借用检查器的所有自定义规则,这是必要的。 完成的字节码验证非常具体,以至于其他语言几乎没有机会编译为 Move 字节码并通过验证器。 因为 Move 是围绕这种非常具体的字节码验证设计的,所以它不像 Rust 那样灵活。

剥离字节码放弃了 Move 的所有主要优点。 正如“资源:货币的安全语言抽象”论文所述:
Move 的显着特点是可执行字节码表示,为所有程序提供资源安全保证。 鉴于合约的开放部署模型,这一点至关重要——回想一下,任何合约都必须容忍与不受信任的代码的任意交互。 如果在可执行级别上不受信任的代码(例如,复制源级线性类型的不受信任的代码)可能违反源级线性,那么它的价值是有限的。

虽然 Move 的类型、资源和内存安全特性将在程序级别保留,但它们不会在全局范围内保留。 并且程序级安全并没有带来太多新的东西——我们在很大程度上已经在 Rust 中拥有了这一点。

Move 的智能合约生态系统也无法在 Solana 上使用——编程模型如此不同,以至于智能合约的重要部分必须重写。

考虑到所有这些,我预计使用 (3) 实现的 Move 的使用不会流行——与 Anchor 相比,它使用起来太麻烦了。 在某些方面,它可能比原始 Rust 更麻烦。

至于 (1),这里的想法是(与 SBF 加载器一起)在运行时添加对移动加载器的支持。 Move 智能合约将作为 Move 字节码存储在链上并由 Move VM 执行(就像在 Sui 中一样)。 这意味着我们将拥有一个 SBF 智能合约生态系统和一个 Move 智能合约生态系统,其中前者将在当前的 Solana 编程模型上运行,而后者则在(可以说是更好的)Move 模型上运行。

使用这种方法,可以保持 Move for the Move 智能合约相互交互的所有好处,但这里的一个大困难是让 Move 智能合约能够与 SBF 智能合约交互,反之亦然 . 实施这将具有挑战性——您需要一个深入了解 Move 和 Solana 的人。 验证器也必须进行调整。

还有一个缺点是需要在运行时维护两个不同的加载器。 这具有安全隐患,因为这意味着攻击面的大小是两倍——任何加载程序中的错误都可能意味着整个链被利用。 作为旁注,对 Move VM 的早期支持实际上早在 2019 年(#5150)就添加到了 Solana,但后来由于安全问题被删除(#11184)(请参阅此线程)。

至于(2),想法是将整个 Move VM 作为 Solana 程序(智能合约)运行。 Move VM 是在 Rust 中实现的,因此可以将其编译为 SBF(除非它使用线程或其他一些不受支持的 API)。 听起来很疯狂,Neon 已经实施了一种类似的方法来将 EVM 作为 Solana 程序运行。 这种方法的好处是不需要对运行时进行任何更改,并且可以保持相同的安全假设。

我不熟悉 Move VM 的技术细节,所以我不能对它的可行性和它有什么限制发表太多评论。 首先想到的是验证程序还必须作为程序运行,这意味着在计算预算范围内。 这种方法也会遇到与 (1) 相同的 SBF 和 Move 智能合约之间的互操作性问题。

没有直接的方法可以将 Move 的主要功能带到 Solana。 虽然可以构建 LLVM 前端并将 Move 编译为 SBF,但这并不会起到太大作用,因为编程模型将保持不变。 如第 6.1 节中的思想实验所示,如果没有某种字节码验证,您将无法改进编程模型。 更改 eBPF/SBF 以支持字节码验证将非常困难。 似乎唯一合理的选择是以某种方式让 Move VM 运行。 但这意味着将有两个生态系统在不同的编程模型上运行,让它们正确地互操作是非常具有挑战性的。

6.4. Move 的性能

Move 字节码不是通用的字节码语言。 它有一个非常固执己见的类型系统,为了允许所有必要的验证,它是相当高级的。 与更接近原生代码的其他字节码格式(例如 eBPF/SBF)相比,这可能意味着性能较低,有人可能会认为这将是高性能 L1 中使用的一个问题。

但到目前为止,智能合约的执行在 Solana(在撰写本文时平均为 3k TPS)和 Sui(基于团队完成的初始 e2e 基准测试)都不是瓶颈。 提高交易处理性能的主要驱动力是并行执行。 Solana 和 Sui 都通过要求先声明依赖关系并并行调度依赖于不同对象/帐户集的交易的执行来实现这一点。 由于这一点,并且由于其他地方存在瓶颈,即网络层,交易执行就 TPS 而言,距离出现在关键路径上还有几个数量级。

此外,一旦 TX 执行确实出现在关键路径上,没有什么可以阻止 Move 被 AOT 编译或 JIT-ed 以提高性能。 这就是为 Move 构建 LLVM 前端的好处所在。 此外,由于 Move 对静态分析的固有适应性,可能会获得 Move 独有的进一步优化。

考虑到所有这些,我预计在可预见的未来,Move 的表现不会成为一个重要的障碍。

7. Move 的其他特性

在本章中,我将描述 Move 的一些其他功能,这些功能可能不是本文讨论的核心,但仍然相关。

7.1. prover

Move 有一个用于智能合约的形式化验证工具,称为 Move Prover。 使用此工具,您可以断言不同的不变量是否适用于您的智能合约。 在幕后,验证条件被转换为 SMT 公式,然后使用 SMT 求解器进行检查。 这是非常不同的,例如 来自模糊测试,它通过遍历输入空间进行反复试验。 例如,如果模糊测试和单元/集成测试未能测试特定输入或显示程序存在错误的输入组合,它们仍然会提供误报。 另一方面,Prover 本质上提供了指定不变量适用于所提供程序的正式证明。 这就像根据所有可能的输入检查程序,但不必这样做。

Move Prover 速度非常快,因此它可以像类型检查器或 linter 一样集成到常规开发工作流程中。

这是prover规范的示例(摘自“使用Move prover对智能合约进行快速可靠的形式化验证”白皮书):

[…] 这增加了传递函数的规范、一个用于规范的辅助函数 bal 和两个全局内存不变量。 第一个不变量表明余额永远不会低于某个最小值。 第二个不变量是指全局内存的更新前后状态:账户余额不能一步减少超过一定数量。

有关 Prover 的更多详细信息,我推荐以下白皮书:

7.2. 钱包安全

由于 Sui 要求交易将访问的所有对象都在函数参数中传递(没有从全局状态动态加载),并且 Move 函数签名和类型信息存储在字节码本身中,我们可以让钱包提供更有意义的信息 在用户签名之前向用户告知交易将做什么。

例如,如果我们有一个具有以下签名的函数:

public entry fun foo(
  asset1: &Asset,
  asset2: &mut Asset,
  asset3: Asset
)

我们可以从函数签名中看出,该交易将访问用户的 3 个资产(资产类型)。 不仅如此,根据 & 和 &mut 关键字(以及缺少关键字),我们还可以判断asset1 可以被读取,asset2 可以被改变(但不能被转移或销毁),而asset3 可能被改变、转移 ,或销毁。

钱包可以向用户显示此信息,然后用户可以更有意义地了解交易对资产的影响。 如果某事看起来不正确,例如 来自 web3 应用程序的交易调用正在触及一些不应该触及的资产或硬币,用户可以观察到这一点并决定不继续进行交易。 有一篇很棒的文章来自
Ethos 钱包
涵盖这个主题(链接)。

钱包也可以额外模拟交易,这将为用户提供更多关于其影响的信息。 Sui 的以对象为中心的编程模型以及类型信息是运行时原生的这一事实意味着,无需了解智能合约的任何特定应用程序级知识,就可以解释对象更改。

例如,这在 Solana 上是不可能的,因为从运行时的角度来看,帐户包含任意数据。 您需要帐户的外部描述(特定于应用程序)才能解释它们,智能合约发布者可能会或可能不会提供这些描述。 此外,在 Solana 运行时中不存在资产所有权的概念,每个智能合约都需要手动实现这种语义(通常使用账户签名和 PDA),这意味着没有通用的方法来跟踪它。

7.3. 简单和复杂的交易

具体到 Sui,共识级别有一个有趣的优化,允许某些类型的交易放弃完全共识,而是使用基于拜占庭一致广播的更简单算法来提交。 优点是这些交易可以在共识级别上并行化,消除了线性阻塞并以近乎即时的确定性提交——基本上实现了 web2 的可扩展性。

这是可能的,因为 Sui 区分了拥有对象和共享对象(参见第 3.1 节)。仅涉及拥有对象的交易(称为简单交易)不需要对 Sui 完全共识。由于拥有的对象不能在交易中被其他任何人使用,但发送者和发送者一次只能发送一个交易,这本质上意味着这些交易不必参考其他交易进行排序(总计 vs因果顺序)——我们知道交易中引用的对象不会受到其他交易的影响,并且该交易也不会影响其他对象。因此,我们不关心这些交易相对于链上并行发生的其他交易的排序——它实际上是不相关的。 Sui 能够利用这一事实极大地优化简单交易的处理,在几百毫秒内实现最终性。缺点是发件人一次只能发送一笔交易。另一方面,涉及任意数量共享对象的交易(称为复杂交易)总是需要完全共识。

考虑到拥有对象的创建、转移和修改可以完全通过简单的交易来完成,某些类型的应用程序可以很好地利用简单的交易。 很好的例子是 NFT(包括大规模铸造)和 web3 游戏。 这些用例受益于低延迟最终确定性和消除线性阻塞,从而实现更好的用户体验和可扩展性。 可以在此处找到更全面的单作者友好型应用程序列表。

不过,其他一些类型的应用程序必须依赖复杂的交易。 这包括大多数 DeFi 应用程序。 例如,AMM 流动性池需要是一个共享对象,因为任何类型的交易所订单执行都需要完全共识和总排序。 这是因为,从根本上说,如果多个订单同时来自不同的用户,我们需要就先执行谁的订单达成一致,这决定了每个用户将获得的执行价格。

还有一些应用程序可以混合使用简单和复杂的交易。 这些应用程序需要复杂的交易才能实现其所需的功能,但可以利用简单的交易进行某些操作以提高效率。 例如,价格预言机可以这样设计。 我们可以让多个发布者使用简单的交易提交市场的价格数据,然后授权使用复杂的交易来汇总价格(例如权益加权中位数)。 在某些时候不依赖复杂的交易是不可能实现价格预言的(基本上是因为在其他交易中使用公布的价格需要就订单达成一致,因此需要完全共识),但至少我们可以通过简单的交易优化发布者的写入。

Sui 文档有更多关于简单和复杂交易的详细信息:

8. 总结

本文深入探讨了 Solana 和 Sui 的编程模型、它们的比较方式以及 Move 编程语言。

第 2 章是对 Solana 编程模型的总结,第 3 章介绍了 Sui Move 及其编程模型。 第 4 章接着解释类型和资源安全如何在 Move 中工作。 Move 的特性对智能合约开发的意义并不是很明显,所以在第 5 章中,使用现实生活中的示例对 Solana 和 Sui Move 进行了更彻底的比较。 第 6 章讨论了 eBPF/SBF,并说明让 Move 功能或 Move 本身在 Solana 上工作并非易事。 第 7 章涉及 Sui 的一些与 Move 相关的功能。

智能合约编程是关于对数字资产进行编程。 可以肯定地说,这是一种新的编程类型,不同于我们迄今为止看到的其他类型的编程(例如系统、后端……)。 因此,现有的编程语言和编程模型不能很好地适应这个用例也就不足为奇了。

问题的症结在于,我们想要一个自然地用于处理资源的编程模型,但同时我们正在与不受信任的代码进行交互。 Solana 在这里做了一个妥协,它确实在不受信任的环境中启用了具有必要可编程性的智能合约,但它的编程模型对于使用资源进行编程并不是很自然。 字节码验证使两者都成为可能。 在某种程度上,它将不受信任的代码变成了受信任的代码。

Move 是一种用于智能合约开发的新颖编程语言。 它的核心新颖之处在于它的字节码,它被有意设计为易于验证。 虽然字节码验证本身并不是一个新概念,但 Move 所做的验证却是。 通过其字节码和验证,Move 实现了一种智能合约编程模型,该模型对资源具有一流的支持,并且可以在不受信任的环境中保证安全的可编程性。

乍一看,这对智能合约开发的影响并不明显,但如第 5 章所述,它们在智能合约的人体工程学、可组合性和安全性方面确实非常重要。 Move 比 Solana 的基于 Rust 的编程模型有了显着的进步。

如此之多,以至于我认为 Move 对智能合约开发的作用就像 React 对前端开发所做的那样,并且说“你可以用 Move 做什么,你可以用 Rust 做什么”类似于说“你可以用 React 做什么” 可以用 jQuery 做”。 当然,可以实现一个与 React 应用程序等效的基于 jQuery 的应用程序,但这并不实用。 React 引入了虚拟 DOM 的概念,它对开发人员完全透明,但允许更快、可扩展和简单的前端开发。 以类似的方式,Move 的字节码验证是一种底层技术,对开发人员也是完全透明的,但它提供了更符合人体工程学、可组合和更安全的智能合约开发。 由于其安全性和更直观的编程模型,Move 还大大降低了智能合约开发人员的入门门槛。

如果 Move 设法获得牵引力(有早期迹象表明它会),它可能会对 Solana 构成相当大的威胁。 这是因为两个原因。

首先是 Move 智能合约的开发时间要快得多。 看来,在 Move 中从头开始开发智能合约的速度可能比在 Rust 中快 2-5 倍。 在编写在 Move 中微不足道但在 Solana 上可能很复杂的智能合约时尤其如此。 因此,Move 生态系统的发展可能会超过 Solana。 由于区块链的开放性和无许可性,没有强大的锁定效应,流动性可以轻松流动。 Solana 开发人员可能会因为经济原因而被迫采用 Move——您可以切换到 Move 或被更快地开发更安全智能合约的 Move 开发人员赶超。 如果您正在雇用智能合约开发人员,您可以雇用将构建一个智能合约的 Rust 开发人员或将在相同时间内构建两个更安全的 Move 开发人员。 这类似于 React 对前端开发的影响。

第二个原因是 Move 的门槛远低于 Rust 或 Solidity。因为 Move 语法更简单,编程模型更直观,所以有一整类开发人员无法在 Rust 或 Solidity 中进行智能合约开发,但可能能够在 Move 中进行。而且由于要学习的概念更少,让非智能合约开发人员加入 Move 比让他们加入 Rust 容易得多(Rust 本身就是一种复杂的语言,加上像 PDA 这样的 Solana 概念会给初学者带来很多困惑)或 Solidity(您需要熟悉语言的非常微妙的细节,例如可重入性,以便能够开发安全的智能合约)。即使现有的 Solana 和 Solidity 开发人员不切换到 Move,尚未进入该领域的开发人员市场也比该领域现有开发人员的数量要大几个数量级。由于 Move 的进入门槛较低并允许更快的开发,因此与 Rust 或 Solidity 相比,它具有更好的产品市场契合度,并且可以占据更大的份额。如果新开发人员开始大量涌入,我希望他们从 Move 而不是 Rust 或 Solidity 开始。这再次类似于 Web 行业中 React 所发生的情况。

因此,我完全期望在中长期内将一流的 Move 支持添加到 Solana。 但这不是一件容易的事。 为了获得 Move 的主要好处,Move 字节码需要得到原生支持。 这意味着简单地编译 Move 到 eBPF/SBF 并不会削减它(参见第 6.3 节)。 为了保持现有的生态系统,两个运行时都需要得到支持。 主要的技术挑战是在运行时之间实现适当的互操作性。 这需要对 Move 和 Solana 有深入的了解,因此我希望 Solana 团队在 Move 团队的某个人的支持下直接推动这一点。

Move 起源于 Diem 项目的 Meta(née Facebook)。 由 Sam Blackshear 领导的 Move 团队的任务是弄清楚如何处理智能合约。 在更仔细地研究了这个问题后,他们发现智能合约编程就是对数字资产(资源)进行编程,但现有的语言都没有原生支持该用例。 决定从头开始构建一种新的编程语言。

我想强调的是,创建一门新语言的决定并不是一个显而易见的决定,因为它需要多年的工程努力才能让它起步,而且在大多数情况下,最好使用现有的解决方案。 Move 团队正确地预见到了一种安全、对资源具有一流支持、同时又足够灵活的智能合约语言可以构建,仅此一项就显示出高度的专业性。 这是团队和支持该项目的 Novi/Meta 工程领导层的一个大胆举措(将有许多董事和副总裁参与这样的决定)。 Meta 自从关闭了他们的 Diem 工作后,最终未能获得其对 Move 的投资的结果,但它仍然对更广泛的加密社区做出了巨大贡献。

总而言之,Move 是一项了不起的技术,我相信它将对我们开发智能合约的方式产生巨大影响。 干得好Move团队!

原文

1条评论
Fund
Fund
持有SUI,学习Move编程!将来再回头看,这个选择是否正确!
点赞
评论