智能合约开发语言 — Move 与 Rust 的对比 (#2)

  • MoveMoon
  • 更新于 2022-09-09 17:05
  • 阅读 5249

精彩而深入的Solana编程及Move编程对比,了解Move如何实现安全性及开发高效性。

接着上一篇, 我们介绍了 Solana 编程模型及Move 编程模型,以及 Move 如何确保安全性, 这一篇,我们继续更深入的对比 Solana 上编程与Move 编程,这一篇我们会介绍 Solana 上一写可组合性的缺陷,以及相同的功能使用在 Solana 下因为需要进行账号检查,导致代码冗长,并讨论了在 Solana 上使用 Move 开发的一些可能性, 文章非常深入且精彩。

5. Solana 与 Move

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

5.1. 闪电贷

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

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

EVM 闪电贷

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

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

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

Solana 闪电贷

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

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

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

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

还有另一种方法来实现没有指令内省的闪电借贷--你可以让借贷智能合约中的闪电借贷指令带着资金做一个CPI调用到你的任意智能合约中,然后在CPI调用返回后会检查资金是否已经正确返回。这种方法是由SPL借贷程序实现的(链接)。但这有一个不同的问题--没有通用的方法可以在一次调用中聚合(合并)多个贷款。

Move 闪电贷

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

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

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

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

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

闪电贷功能是完全使用基本语言原语实现的,Move 实现不会像 Solana 实现需要特殊处理的交易那样增添额外的集成开销。 此外,也没有把复杂性被推给客户端。

5.2. 铸币权限锁

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

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

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

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

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

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

1_2MuC9-26MwUr4YlyWlEALg

你会立刻注意到,对于相同的功能,Solana 实现的大小是 Sui 的两倍多(230 对 104行)。 这很重要,因为更少的代码通常意味着更少的错误和更短的开发时间。

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

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

显然,移除这些检测将是一件大事。

那么Move是如何做到没有这些检查而又同样安全的呢?让我们仔细看看这些检查的实际作用。以下是 "mint_to "指令所需的账户检查(授权者通过锁控制来铸造代币)。

img

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

  1. 检查所提供的锁账户是否为该智能合约所拥有,并且是MintLock类型。有必要传入锁,因为它被用于CPI调用代币程序进行铸币(它存储了权限)。
  2. 检查所提供的铸币授权账户是否属于所提供的锁。铸币厂授权账户持有授权状态(其公钥,是否被禁止,等等)。
  3. 检查指令调用者是否拥有该权限的所需密钥(所需权限签署了该交易)。
  4. 需要传入代币目标账户,因为代币程序将在CPI调用中修改它(增加余额)。铸币厂检查在这里并不是严格必要的,因为如果传入了错误的账户,CPI调用就会失败,但还是要做检查,这是很好的做法。
  5. 与4类似。
  6. 检查代币项目账户是否正确传入。

我们可以看到,账户检查(在这个例子中)分为这五类:

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

注意。这并没有涵盖所有类型的账户检查,但足以说明问题。

在Move中,虽然没有账户检查或任何类似的要求,只是有函数签名:

img

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

那么,在Solana中,我们需要声明6个账户,还需要手动实现对它们的各种检查,而在Move中,我们只需要传入2个对象,而且不需要明确的检查,这怎么可能呢?

好吧,在Move中,有些检查是由运行时透明地完成的,有些是由验证器在编译时静态地完成的,而有些则是在结构上根本不需要的

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

本质上,Move利用字节码验证,以使数字资产的编程模型更加自然。Solana的模型围绕着账户所有权、签名、CPI调用、PDA等。但是,如果我们退一步想一想,我们就会发现,我们并不真的想要处理这些问题。它们与数字资产本身没有任何关系--相反,我们必须使用它们,因为这使我们能够在Solana的编程模型中实现所需功能。

在Solana上,由于没有字节码验证来保证更细粒度的类型或资源安全,你不能允许任何程序修改任何账户,所以引入账户所有权的概念是必要的。由于类似的原因(没有跨程序调用的类型/资源安全),也没有可以进出程序的用户拥有的对象的概念,相反,我们用账户签名来证明权限。由于有时程序也需要能够提供账户签名,所以我们有PDA......

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

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智能合约将如何共同组合。这纯粹是一个假设性的例子,可能在现实生活中没有任何用处,但可以用来说明问题 -- 现实生活中组合智能合约的例子与此没有太大区别。

做到这一点的指令调用的账户检查可能看起来像这样:

img

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

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

img

这只是3个对象而已。

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

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

下面是一个Solana账户和Sui对象之间的比较,各自持有一个AMM池的状态,看起来像这样:

img

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

但是,传递大量账户的主要问题甚至不是开发者的效率问题。在组成多个智能合约时,必须传递(从而检查)许多账户,这就造成了相当大的实现复杂性,并具有很大安全隐患。这些账户之间的关系可能相当错综复杂,在某一点上,跟踪所有必要的账户检查以及它们是否已经正确完成变得相当困难。

事实上,这就是我认为在Cashio漏洞中发生的情况(4800万美元)。这里是该漏洞的主要账户检查的细目(不充分)。正如你所看到的,这些账户检查可以变得有些错综复杂。开发者可能有最好的意图来做正确的检查,但在某一点上,精神上的开销变得太大,变得非常容易失误。账户越多,出现错误的机会就越大。

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

作为一个附带说明,在Move与Rust/Anchor相比的安全性方面,还有一件事值得考虑,可能不是很明显。这就是Move的TCB(trusted computing base可信计算基础)比Rust/Anchor小得多。较小的TCB意味着被信任的、需要进入智能合约编译和执行的组件更少。这就减少了可能影响智能合约的漏洞的表面积--TCB之外的bug不会影响智能合约的安全或保障。

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

6. Solana上的Move

很明显,Move是很了不起的。我们也知道,Solana的设计允许其他编程语言可为其开发智能合约。现在的问题是--我们能否在Solana上实现Move,以及如何实现?

6.1. Anchor有全局类型安全?

在我们开始研究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的账户判别器(或类似的方式),并能够检查传入的账户是否符合所需的判别器(Anchor账户的前8个字节)。

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

Solana程序编译为SBF(Solana Bytecode Format,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(eBPF后端支持的LLVM),甚至支持Rust编译器以eBPF为目标,你仍然无法使Solana智能合约编译到eBPF。这就是为什么Solana团队不得不对Rust代码库和eBPF LLVM后端(例如支持通过堆栈传递参数)进行多次修改。

由于其中的一些修改本身就不能上游(upstreaming)(无论是对Rust还是LLVM),Solana团队目前维护着Rust和LLVM的分叉,并对这些修改进行了维护。当你执行cargo build-bpf(构建Solana智能合约的典型命令)时,Cargo会拉动这个Solana特定版本的rustc来进行智能合约的编译(原来的rustc无法工作)。

这就是SBF的诞生过程 -- Solana需要的一些要求与eBPF不兼容。Solana团队目前正在努力将SBF作为一个独立的LLVM后端上游,并将其加入作为一个Rust目标,以避免维护单独的分叉。

如果你对这个话题有更多的兴趣,这里有一些相关的GitHub讨论:

因此,虽然eBPF可以作为智能合约的一种格式,但它并不像纸面上看起来那么理想。它需要进行一些修补,而且原来的验证器也没有很大的用处。

在关于Move和Solana/SBF的讨论中,我还看到一个误解,我觉得这个误解非常重要。有人评论说,Move的主要思想应该适用于SBF,因为它是基于eBPF的,它的验证器有可能被利用,使账户修改检查在静态编译时进行,而不是在运行时动态进行的。

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

Move的主要思想是使一个以资源为中心的编程模型能够以一种自然的方式与不受信任的代码进行交互。

在实践中,这意味着:

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

第五节闪电贷中说明了这在智能合约的安全性、可组合性和人机工程学方面是多么重要。它的重要性远远超出了跳过一些运行时安全检查来提高跨程序调用的性能

现在,如果你想知道将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。

似乎有三种方案可以让Move在Solana上运行:

  1. 添加Move虚拟机作为一个本地加载器(与SBF虚拟机一起)。
  2. 将Move VM作为一个程序运行(如Neon)。
  3. 将Move编译为SBF(如Solang)。

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

但从智能合约方面来看,这并没有什么好处。虽然有真正的理由为Move建立一个LLVM前端,例如为了性能或可移植性,但在我看来,使用SBF运行时来执行Move智能合约并没有带来多大的好处。事实上,我认为以这种方式开发智能合约会比直接使用Anchor更糟。

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

Move不是很适合与其他编程模型一起使用。这是因为它被特别设计为能够编译成Move字节码并通过验证器验证。所有围绕能力和借贷检查器的自定义规则,都需要它,字节码验证是如此具体,以至于其他语言几乎没有机会编译成Move字节码并通过验证器。因为Move是围绕这个非常特殊的字节码验证而设计的,所以它不像Rust那样灵活。

剥离字节码就放弃了Move的所有主要优势。正如"资源:货币的安全语言抽象 "论文中所说的那样。

Move的显著特征是一个可执行的字节码表示,对所有程序都有资源安全保证。考虑到合约的开放部署模式,这一点至关重要--回顾一下,任何合约都必须容忍与不受信任的代码进行任意的交互。如果源码级的线性可以被可执行级的不可信任的代码所违反,那么源码级的线性的价值是有限的(例如,可信任的代码复制源码级线性类型)。

虽然Move的类型、资源和内存安全特性可以在程序级别上会被保留下来,但它们不会被全局保留下来。而程序级的安全并没有带来多少新的东西--我们在很大程度上已经有了Rust。

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

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

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

有了这种方法,就有可能保持Move智能合约之间相互作用的所有好处,但这里的一个大困难是让Move智能合约能够与SBF智能合约进行交互,反之亦然。实现这将是一个挑战 -- 你需要一个对Move和Solana有深刻理解的人。验证器也将不得不被调整。

还有一个缺点是需要在运行时维护两个不同的加载器。这有安全方面的影响,因为它意味着攻击面是两倍的大小--任何一个加载器的错误都可能意味着整个链条被利用。顺便提一下,实际上早在2019年,Solana就加入了对Move VM的早期支持(#5150),但后来由于安全问题而被删除(#11184)(见本主题)。

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

我不熟悉Move VM的技术细节,所以我不能对这种做法的可行性以及它的局限性做太多的评论。我想到的第一件事是,验证器也必须作为一个程序运行,这意味着在计算预算内。这种方法也会受到SBF和Move智能合约之间相同的互操作性问题的影响,如同方案(1)。

没有直接的方法可以将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化以提高性能。这就是为Move构建一个LLVM前端的好处所在。另外,由于Move本身对静态分析的适应性,Move特有的进一步优化也有可能。

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

7. 其他Move特性

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

7.1. Prover

Move 有一个用于智能合约的形式化验证工具,叫做Move Prover。通过这个工具,你能够断言不同的不变量对你的智能合约是否成立。在幕后,验证条件被翻译成SMT公式,然后使用SMT解算器进行检查。这与模糊测试有很大的不同,例如,模糊测试是通过遍历输入空间来试错。例如,如果模糊测试和单元/集成测试未能测试特定的输入或输入的组合,从而表明程序有一个错误,那么它们仍然可以提供一个假阳性。另一方面,验证器本质上提供了形式上的证明,即指定的不变量对所提供的程序是成立的。这就像针对所有可能的输入检查程序一样,但不需要这样做。

Move 验证器的速度非常快,所以它可以像类型检查器或linter那样被整合到常规的开发工作流程中。

下面是一个关于验证器规范的例子(摘自"用Move Prover对智能合约进行快速可靠的形式化验证 "白皮书)。

1_EjWE1sSANYyeKAHpwGkTzA.png

这增加了transfer函数的规范,一个用于规范的辅助函数bal,以及两个全局内存不变性(invariant)。第一个不变量指出,余额永远不能低于某个最小值。第二个不变式(invariant)指的是全局内存的更新,有前和后的状态:一个账户的余额永远不能在一个步骤中减少超过一定数量。

关于验证器的更多细节,我推荐以下白皮书:

7.2. 钱包安全

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

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

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

我们可以从函数签名中得知,这个交易将访问用户的3个资产(资产类型)。不仅如此,根据&&mut关键字(或没有关键字),我们还可以知道asset1可以被读取,asset2可以被修改(但不能转移或销毁),而asset3有可能被修改、转移或销毁。

钱包可以向用户显示这些信息,用户可以更有意义地了解交易可能对资产做什么。如果有什么不对劲的地方,例如,来自web3应用程序的交易调用正在接触一些不应该接触的资产或代币,用户可以观察到这一点并决定不继续进行交易。有一篇出色的文章来自 Ethos Wallet 涵盖了这个话题(链接)。

钱包也可以额外模拟交易,这将给用户提供更多关于其效果的信息。Sui 以对象为中心的编程模型,以及类型信息是运行时的原生信息这一事实,意味着有可能解释对象的变化,而无需对智能合约有任何具体的应用层面的知识。

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

7.3 简单和复杂交易

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

这是由于Sui对拥有者对象和共享对象进行了区分(见3.1节)。只涉及拥有者对象的交易(被称为简单交易)不需要在Sui上达成完全的共识。由于拥有者对象在交易中不能被其他人使用,只能由发送者使用,而且发送者一次只能发送一个交易,这本质上意味着这些交易不需要参照其他交易进行排序(总排序与因果排序)-- 我们知道一个事实,即交易中引用的对象不能被其他交易影响,同时这个交易也不能影响其他对象。因此,我们并不关心这个交易相对于链上平行发生的其他交易的排序 -- 这实际上是不相关的。Sui能够利用这一事实,大大优化简单交易的处理,在几百毫秒内实现最终确定性。另一方面,涉及任何数量的共享对象的交易(被称为复杂交易),总是需要完全的共识。

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

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

还有一些应用可以混合使用简单和复杂的交易。这些应用需要复杂的交易才能实现他们想要的功能,但在某些操作上可以利用简单的交易来获得更好的效率。例如,一个价格预言机可以被设计成这样。我们可以让多个发布者使用简单交易为一个市场提交价格数据,然后由一个权威机构使用复杂交易对价格进行汇总(例如,股权加权中值)。在某些时候不依靠复杂交易是不可能实现价格预言机的(根本原因是在其他交易中使用发布的价格需要就排序达成一致,从而达成完全的共识),但至少我们可以用简单交易优化发布者的写入。

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

8. 结束语

本文深入探讨了Solana和Sui的编程模型,它们如何进行比较,以及Move编程语言。

第二节是对Solana编程模型的总结,而第三节则介绍了Sui Move及其编程模型。第4节接着解释了Move中的类型和资源安全是如何工作的。Move的功能对智能合约开发的意义并不是立竿见影的,所以在第5节中,我们用现实生活中的例子对Solana和Sui Move进行了更彻底的比较。第6节讨论了eBPF/SBF,并表明让Move功能或Move本身在Solana上工作并不是一件容易的事。第7节涉及到Sui的一些Move相关功能。

智能合约编程是关于数字资产的编程。而且可以说这是一种新的编程类型,与我们目前看到的其他类型的编程(如系统、后台......)截然不同。正因为如此,现有的编程语言和编程模型不能很好地适应这种使用场景也就不奇怪了。

问题的关键在于,我们希望有一个编程模型,能够自然地与资源打交道,但同时我们又要与不受信任的代码交互。Solana在这里做了一个妥协,它确实能够让智能合约在不受信任的环境中具有必要的可编程性,但它的编程模型对与资源的编程来说不是很自然。字节码验证是使其有可能同时拥有这两者。在某种程度上,它把不可信任的代码变成了可信任的。

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

这对智能合约开发的影响乍一看并不明显,但正如第5节所说明的,它们在智能合约的人机工程学、可组合性和安全性方面确实非常重要。与Solana的基于Rust的编程模型相比,Move提出了一个重大的提升。

以至于我认为Move对智能合约开发的作用就像React对前端开发的作用一样,说 "你能用Move做的事你能用Rust做"就像说 "你能用React做的事你能用jQuery做 "一样。当然,有可能实现一个基于jQuery的应用,相当于一个React应用,但这并不实际。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本身就是一种复杂的语言,再加上Solana的概念,如PDA,给初学者带来很多困惑)或Solidity(你需要熟悉语言中非常微妙的细节,如重入,以便能够开发安全的智能合约)容易得多。即使现有的Solana和Solidity开发者不转向Move,尚未进入该领域的开发者市场也比该领域现有的开发者数量多出好几个数量级。由于Move的进入门槛较低,并允许更快的开发,它比Rust或Solidity有更好的产品市场契合度,并可以从这块蛋糕上分得更大的一杯羹。如果新的开发者开始大量涌入,我希望他们从Move开始,而不是Rust或Solidity。这也类似于React在web行业的情况。

正因为如此,我完全期待在中长期内,Solana会加入对Move作为一等公民的支持。但这并不是一件容易的事。为了获得Move的主要好处,Move字节码需要得到本地支持。这意味着简单地将Move编译成eBPF/SBF是不可能的(见第6.3节)。为了保持现有的生态系统,两种运行时都需要被支持。主要的技术挑战是如何在运行时之间实现适当的互操作性。这需要对Move和Solana的深入了解,所以我希望这需要Solana团队在Move团队的支持下直接推动。

Move起源于Meta(前 Facebook)的Diem项目。由Sam Blackshear领导的Move团队的任务是弄清楚如何处理智能合约的问题。在仔细研究了这个问题之后,他们发现智能合约的编程都是关于数字资产(资源)的编程,但现有的语言都不支持这个使用场景。于是决定从头开始建立一种新的编程语言。

我想强调的是,创建一个新的语言的决定根本不是一个明显的决定,因为它需要多年的工程努力才能落地,在大多数情况下,使用现有的解决方案会更好。Move团队正确地预见到,一种安全的、对资源有一流支持的、同时又足够灵活的智能合约语言是可以建立的,仅此一点就显示出高度的专业性。这是团队和支持该项目的Novi/Meta的工程领导层的一个大胆举动(这样的决定会有一些董事和副总裁参与)。Meta后来关闭了他们的Diem努力,并在最后没有能够收获其在Move的投资成果,但尽管如此,它对更广泛的加密货币社区是一个出色的贡献。

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

点赞 3
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
MoveMoon
MoveMoon
Move to Moon