搜索
NFT元宇宙Web3
近期热门

Move vs. Rust:深入了解Move编程语言(上)

RR

信息来源自medium,略有修改,作者Krešimir Klas

本文旨在让你深入了解用于智能合约开发的新颖编程语言Move,以及它与Solana上使用的基于rust的现有模型的比较情况。

1. 导语

最近几个月来,新兴的高性能L1 Aptos和Sui以及这些新链不可或缺的Move智能合约编程语言引起了人们的关注。一些开发人员已经积极地转向了Move,宣称这是智能合约开发的未来,而另一些开发人员则对此谨慎,认为Move只是另一种智能合约编程语言,从根本上来说,比起现有的编程模型上并没有提供太多东西。加密货币投资者也想知道这些新的L1有什么特别之处,以及它们如何与Solana相提并论。

但我们迄今为止看到的讨论并没有达到充分理解这项新技术给我们带来什么的必要深度。因此一直在关注这些讨论的外部旁观者、加密开发者和投资者无法有信心地形成自己的观点。

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

2. Solana编程模型

为了充分理解本文提出的观点,需要对Solana编程模型有一定的了解,我将在这里做一个简短的总结。如果你已经熟悉Solana的编程模型,那么可以跳过本章。

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

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

账户存在所有权的概念。每个帐户都属于一个(且仅属于一个)程序。当一个帐户被某个程序所有时,该程序被允许修改其数据。程序不可以修改它们不拥有的帐户(但允许从中读取)。这些检查是由运行时通过比较程序执行前后的账户状态动态完成的,如果发生了非法的突变,则会导致交易失败。

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

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

程序可以通过CPI call调用其他程序。这种调用的工作原理与来自客户端的调用基本相同——调用者程序需要指定被调用者程序将访问的帐户,而被调用者程序将执行所有与从客户端调用相同的输入检查(它不信任调用者程序)。

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

以上是Solana上安全智能合约编程的基本构建块。在某种程度上,你可以将Solana程序看作操作系统中的程序,而将帐户看作文件,任何人都可以在其中自由地执行任何程序,甚至部署自己的程序。当程序(智能合约)运行时,它们将对文件(帐户)进行读写。所有的文件都可供所有程序读取,但只有对文件具有所有权权限的程序才能写入。程序也可以执行其他程序,但它们彼此之间没有任何信任——无论谁执行程序,它都需要假设输入是潜在的敌对行为。由于这个操作系统是任何人都可以在全球范围内访问的,所以程序中添加了本地签名验证支持,以便为用户提供权限和所有权功能……这不是一个完美的类比,但它很有趣。

3. Move编程模型

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

在Solana的背景下,这就好像所有的智能合约都作为模块发布在一个单一的程序中。这意味着所有智能合约(模块)都包含在同一个类型系统中,可以直接相互调用,而不需要通过中间API或接口。这一点非常重要,本文将对此进行深入讨论。

3.1. 对象

在我们继续之前,需要注意的是,以下对象的概念是特定于Sui变体的Move,而在Move的其他集成中(如Aptos或Diem/core Move),情况可能略有不同。即便如此,在其他Move变体中,也有类似的解决方案实现相同的目标(状态持久性),它们并没有太大的不同。本文中讨论的Move的所有主要优点都适用于所有的Move集成(原生支持Move字节码),这也包括Aptos。

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

Sui有三种不同类型的对象:

  • 所有对象
  • 共享对象
  • 不可变对象

所有对象是属于用户的对象。只有拥有该对象的用户才能在交易中使用它。所有权元数据是完全透明的,由runtime处理。它是使用公钥加密实现的——每个所有对象都与一个公钥相关联(存储在runtime对象的元数据中),任何时候你想在交易中使用一个对象,你都需要提供相应的签名。

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

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

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

4. Move的安全性

我们已经看到,在Move中:

  • 你可以将自己拥有(或共享)的任何对象传递给任何模块中的任何函数
  • 任何人都可以发布模块
  • 不存在模块拥有结构的概念,也不存在像Solana的帐户那样,使所有模块拥有对其进行更改的唯一权限——结构可以流向其他模块,也可以嵌入到其他结构中

现在的问题是,这种做法为什么是安全的?是什么阻止了人们发布恶意模块,获取共享对象(比如AMM池),并将其发送到恶意模块中,然后继续耗尽其资金?

在Solana中有一个帐户所有权的概念,只有拥有帐户的程序才允许对其进行更改。但是在Move中,没有模块拥有对象的概念,你可以将对象发送到任意的模块中。而且runtime也没有做具体的检查来确保该对象在通过不受信任模块时没有被非法修改。那么是什么保证了这个对象的安全呢?如何保证这个对象不被不受信任的代码滥用?

这便是Move的新颖性所在。

4.1. 结构

定义一个结构类型和你想的差不多:

struct Foo {

x:u64,

y:bool

到目前为止还不错——这也是在Rust中定义结构的方法。但是Move中的结构有一个独特之处,那就是在Move中,模块对其类型的使用有比传统编程语言更多的控制。上述代码片段中定义的结构将具有以下限制:

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

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

现在,由于这些限制,我们似乎失去了很多灵活性。这是事实——在传统编程中处理这样的结构会非常麻烦,但实际上,这也正是我们在智能合约中想要的。智能合约开发说到底就是对数字资产(资源)进行编程。如果你看一下上面描述的结构,这正是它的本质——它是一种资源。它不可能被凭空创造,不能被复制,也不能被意外销毁。因此,我们确实在这方面失去了一些灵活性,但我们失去的灵活性正是我们希望失去的。这使得使用资源变得更加直观和安全。

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

struct Foo has key, store, copy, drop {

id:UID,

x:u64,

y:bool

}

下面是它们的作用:

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

本质上,Move中的每个结构在默认情况下都是资源。以上功能使我们能够细化放宽这些限制,使其表现得更像传统结构。

4.2. Coin

为了更好地说明这一点,让我们以Coin类型为例。Coin在Sui中实现了类似ERC20 / SPL代币功能,是Sui Move Library的一部分。以下是它的定义:

// coin.move

struct Coinhas key, store {

id:UID,

balance:Balance

}

// balance.move

struct Balancehas store {

value:u64

}

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

因为没有drop功能,所以Coin不会在函数中被意外销毁。这是一个非常好的功能-这意味着你不会意外丢失Coin。如果要实现一个接受Coin作为参数的函数,在函数结束时,你需要明确地对它做一些事情——要么将它转移给用户,要么将它嵌入到另一个对象中,要么通过调用将它发送到另一个函数中。当然,通过在Coin模块中调用Coin::burn函数来销毁一个Coin是可能的,但你需要有目的地这样做(你不会意外地这样做)。

没有克隆能力意味着没有人可以复制Coin,从而凭空创造新的供应。创造新的供应可以通过coin::mint函数来完成,并且只能由Coin的treasury功能对象的所有者调用。

另外,请注意,由于泛型的存在,每个不同的Coin都有自己独特的类型。由于两个Coin只能通过coin::join函数相加(而不是直接访问它们的字段),这意味着不可能把不同类型的Coin加在一起。类型系统能够保护我们免受坏账影响。

在Move中,资源的安全性是由其类型定义的。考虑到Move有一个全球类型系统,这使得编程模型更自然和更安全,在该模型中资源可以直接传入和传出不受信任的代码,同时保留其安全性。乍一看,这似乎没什么大不了的,但实际上,这为智能合约的可组合性、人机工程学和安全性带来了巨大的好处。

4.3. 字节码验证

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

那么,是什么保证这些规则会被任意模块遵守呢?有什么可以防止有人上传一个带有特殊字节码的模块,例如接收一个Coin对象,然后通过直接改变其内部字段绕过这些规则?通过这样做,你可以非法增加你的Coin数量。

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

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

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

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

5. Solana与Move

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

5.1. 闪电贷

闪电贷是DeFi中的一种贷款类型,借出的金额必须在借入的同一笔交易中偿还。这样做的主要好处是,由于交易是原子性的,贷款可以完全没有抵押。这可以用于在资产之间套利,而不需要有本金来执行。

实现这一点的主要难题是如何在闪电贷智能合约内保证贷款金额将在同一笔交易中得到偿还?为了使贷款能够不被抵押,交易需要是原子性的——也就是说,如果贷款金额没有在同一交易中偿还,整个交易就会失败。

EVM具有动态调度,因此可以使用重入性来实现这一点:

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

这很好地实现了所需的功能,但问题是它依赖于可重入性。而可重入性本质上非常危险,是包括臭名昭著的DAO黑客在内的许多漏洞的根源。

Solana在这方面做得更好,因为它不允许重入。但是,在没有可重入性和闪电贷款智能合约回调自定义智能合约的情况下,如何在Solana上实现闪电贷呢?多亏了指令内省,这是有可能的。在Solana上,每笔交易都由多个指令(智能合约调用)组成,你可以从任何指令中检查同一交易中存在的其他指令(它们的程序ID、指令数据和帐户)。这使得实施闪电贷成为了可能:

  • 闪电贷款智能合约实施借款和还款指令
  • 用户通过在同一交易中把借款和还款指令的调用堆叠在一起,创建一个闪电贷交易。当执行借款指令时,它将使用指令内省检查还款指令安排在同一交易的后期。如果还款指令的调用不存在或无效,交易将在此阶段失败
  • 在借款和还款的调用之间,借款资金可以被中间的任何其他指令任意使用
  • 在交易结束时,还款指令调用将把资金返还给闪电出借人智能合约(在借款指令中使用内省检查该指令是否存在)。

该解决方案运行良好,但仍不理想。指令内省是一种特殊情况,在Solana中并不常用,它的使用要求开发者掌握大量概念,其实现本身也有很高的技术要求。此外,还有一个技术限制——由于还款指令需要静态地存在于交易中,因此不可能在交易执行期间通过CPI动态地调用还款。这几乎不会破坏交易,但它在一定程度上限制了与其他智能合约集成时的代码灵活性,并将更多的复杂性推给了客户端。

Move还禁止动态调度和重入,但与Solana不同的是,它为闪电贷提供了非常简单和自然的解决方案。Move的线性类型系统允许创建保证在交易执行期间只使用一次的结构。这就是所谓的“烫手山芋”模式——一个没有drop、key、store或克隆功能的结构。实现这种模式的模块通常有一个实例化结构的函数和一个销毁结构的函数。

让我们看看如何利用这一点来实现闪电贷:

  • 闪电贷智能合约实施了一个“烫手山芋”收据结构
  • 当通过调用贷款函数进行贷款时,它将向调用者发送两个对象—请求的资金(一个Coin)和一个收据(需要偿还的贷款金额的记录)
  • 然后,借款人就可以使用收到的资金进行预期的操作(例如套利)。
  • 在借款人完成其预期的操作后,需要调用将接收借款资金和收据作为参数的还款函数。该函数保证将在同一个交易中被调用,因为调用者无法摆脱收据实例(它不允许被删除或嵌入到另一个对象中)
  • 还款函数通过读取嵌入在收据中的贷款信息来检查是否已归还了正确的金额。

Move的资源安全特性使Move中的闪电贷在不使用可重入性或内省的情况下成为了可能。这保证了收据不会被不受信任的代码所修改,并且需要在交易结束时将其返回给还款函数。这样一来,我们可以保证在同一个交易中归还了正确的资金数额。

该功能完全使用基本的语言原语实现,而且Move不会像Solana那样因需要特别精心设计交易而存在集成开销。此外,也没有任何的复杂性被推到客户端。

闪电贷很好展示Move的线性类型系统和资源安全保障如何让我们以其他编程语言无法实现的方式来表达功能。

5.2. 铸币权限锁(Mint Authority Lock)

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

“Mint Authority Lock”智能合约扩展了代币铸造的功能,允许多个白名单方铸币。该智能合约所需的功能如下(适用于Solana和Sui):

  • 原始的代币铸币机构创建一个“铸币锁”,这将使我们的智能合约能够监管代币的铸造。调用者成为该铸币锁的管理员。
  • 然后,管理员可以为锁创建额外的铸币权限,可以将其授予其他各方,并允许他们随时使用该锁铸币。
  • 每个铸币权限都有其每日可以铸造的代币数量的限制。
  • 管理员可以在任何时候禁止(和取消)任何铸币权限方。
  • 管理员的能力可以转让给其他方。

该智能合约可以被用来在原始铸币机构(管理员)仍保留铸币控制权的情况下,将代币的铸币功能提供给其他用户或智能合约。不然,我们将不得不把铸币的全部控制权交给另一方,而这是不理想的,因为我们必须信任它不会滥用该权力。而且向多方提供许可也是不可能的。

现在让我们看一下代码,看看有什么不同。

值得注意的是,对于相同的功能,Solana的执行规模是Sui的两倍多。这是一个大问题,因为更少的代码通常意味着更少的bug和更短的开发时间。

那么,Solana的这些额外行数是哪里来的呢?如果我们仔细观察Solana代码,我们可以将其分为两个部分——指令执行(智能合约逻辑)和帐户检查。指令执行与Sui上的情况比较接近(126:104)。额外的行数源于两个CPI调用的模板(每个约10 LOC)。最显著的区别在于帐户检查(上面截图中红色标记的部分),这在Solana上是必需的,但在Move中并不是。帐户检查占该智能合约的40%左右(91 LOC)。

Move不需要帐户检查。LOC的减少能够带来好处,而且也非常重要,因为事实证明,正确实施这些检查是非常棘手的,如果你在这方面犯了一个错误,通常就会导致严重的漏洞和用户资金的损失。事实上,一些最大的Solana智能合约漏洞(就丢失的用户资金而言)就是由不当的帐户检查引起的帐户替换攻击:

  • Wormhole(3.36亿美元)- https://rekt.news/wormhole-rekt/
  • Cashio(4800万美元)- https://rekt.news/cashio-rekt/
  • Crema Finance(880万美元)- https://rekt.news/crema-finance-rekt/

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

那么,Move是如何做到在没有这些检查的情况下同样安全的呢?我们将在下篇为大家继续分析。请大家持续关注我们的更新。

编辑于 2022-09-20 23:39
「 真诚赞赏,手留余香 」
赞赏

发表评论已发布0

手机APP 意见反馈 返回顶部 返回底部