本文是为以太坊虚拟机(EVM)开发者准备的Sui区块链开发心理模型系列文章的第二部分,重点介绍了在Sui上进行开发时需要考虑的安全因素、显式上下文、Move语言的严格性以及其他方面。文章通过对比EVM和Sui的架构,解释了Sui如何通过其对象和所有权模型在设计之初就避免了许多常见的安全问题,并强调了在Sui上开发时应关注的重点。
EVM 开发者在 Sui 上构建的心智模型 - 第二部分
29 23K 上周我们介绍了 EVM 开发者在 Sui 上构建的心智模型的第一部分。简单回顾一下,我们重点介绍了在 Sui 上开发如何围绕与传统 EVM 应用开发不同的对象和所有权模型展开。
今天,我们将通过涵盖安全考虑、显式上下文而非隐式全局变量、Move 的“严格”感觉的意义等,来完善这个在 Sui 生态系统中构建的先决条件。
作者:Eric Nordelo
到目前为止,我们还没有直接谈论太多“安全”,这是故意的。
在 Solidity 中,安全通常是你添加到设计之上的东西:修饰符、模式、审计、防御性编码。在 Sui 上,许多安全属性自然地从你已经看到的所有权和对象模型中产生。
让我们看看这在实践中意味着什么。
重入:为什么 EVM 威胁模型不适用
如果你编写 Solidity 有一段时间了,你几乎肯定担心过重入。通常,当我们谈论它时,问题来自于你最终会得到一个“夹在中间”的调用栈,其中:
合约执行一些检查,
然后进行外部调用或Hook,使这些检查无效,
之后恢复执行,假设这些检查仍然有效。
这种模式依赖于非常具体的东西:能够对你无法控制的代码进行外部调用,这些代码可以执行任意逻辑并在第一个执行帧完成之前回调到原始函数。这是经典 EVM 重入的核心。
在 Sui 上,这种执行形状无法表达。
Move 不支持像 Solidity 那样的动态调度。调用在编译时静态解析为已知的模块和函数。没有机制来调用用户提供的代码、函数指针或任意合约地址作为回调。
你可能会注意到,Move 还限制了哪些模块可以相互调用:一个模块只能调用自身或其显式声明的依赖项,并且包依赖图是无环的。虽然这增强了可预测性,但这不是避免重入的主要原因。即使具有无环调用图,在允许运行时选择回调的语言中,重入仍然是可能的。
真正的约束更简单也更强大:
没有通用的方法可以在执行过程中将控制权交给不受信任的代码。
这也排除了你最初可能担心的更微妙的形状,例如通过不同的包 ID 回调到同一包的旧版本。
所有调用目标都是显式的,静态已知的,并在执行之前解析。
更少的隐藏副作用
如果重入作为默认威胁消失了,那么下一个问题是:仍然可能出现什么样的错误?Solidity 中另一个常见的错误来源是隐式状态访问。
一个函数可能:
从你忘记的映射中读取
以意想不到的方式更新共享存储
影响你没有预料到的用户
在 Sui 上,这很难意外发生。
因为一个交易可能接触到的所有对象都必须显式传入:
没有隐藏的读取
没有意外的写入
依赖关系是预先可见的
这使得代码更容易推理、更容易审计和更容易审查。
批准、授权以及它们为什么不那么重要
在 Solidity 中,批准和授权的存在是因为用户不直接控制资产;合约控制。
在 Sui 上,用户直接拥有资产。要让其他人能够对资产采取行动,你通常会:
转移一种能力(capability)
转移对象的所有权
或者与设计上的共享对象互动
这并不意味着批准完全消失,但它们不再是大多数工作流程所需的核心原语。EVM 世界中许多依赖授权的模式被显式所有权转移或能力委派所取代。
仍然需要注意什么
这些都不意味着安全性得到了解决。在 Sui 上,改变的不是需要仔细推理,而是你所推理的内容。
一旦重入和隐藏的回调被排除在外,剩余的风险来自你可以在代码中看到和追踪的显式状态转换。
在 Sui 上,你调用的任何函数都可以改变一个对象,如果它作为可变引用传递。仅仅所有权并不能在这里保护你。如果一个值作为 &mut 传递,无论它是拥有的、共享的还是嵌套在另一个对象中,都可以被更改。
这意味着你仍然需要有意识地:
业务逻辑的正确性:确保状态转换与应用程序的预期规则相匹配。
跨调用的不变量管理:如果你检查一个条件,调用另一个函数,然后继续执行,你必须假设作为 &mut 传递的任何对象在该调用期间可能已更改。
排序和组合:涉及多个对象突变的复杂流程必须设计成中间状态要么有效,要么不可观察。
权限和升级边界:能力、管理对象和升级权限仍然是强大的工具,滥用仍然可能导致严重的失败。
通过竞争进行拒绝服务:虽然不是一个正确性问题,但在同一对象上集中太多的操作可能会降低性能或可用性。
重要的是,这些风险是显式的和局部的。
没有不可见的读取,没有意外的写入,也没有仅在运行时出现的执行路径。当某些东西可以改变时,它在函数签名中是可见的。当权限存在时,它被表示为一个对象。当排序很重要时,它直接编码在调用顺序中。
换句话说,剩余的挑战不是关于防御未知行为,而是关于正确地建模已知行为。
安全作为模型的一个属性
总结这种转变的一个有用的方法是:
在 Solidity 中,安全主要关于防御性 编码
在 Sui 中,安全主要关于选择正确的所有权和对象模型
一旦做出正确的选择,许多整个类别的错误将变得不可能。这并不是因为开发者是完美的,而是因为语言和运行时不允许你表达它们。
以这种方式构建安全之后,我们现在可以缩小范围,看看更大的图景:所有这些如何在系统层面影响执行、性能和并行性。
到现在,你可能已经注意到一个反复出现的主题:
在 Sui 上,许多在 EVM 中是隐式的属性都被显式化了。
在执行方面尤其如此。
EVM 世界:一切事物都与一切事物竞争
在 EVM 中,所有交易实际上都在争夺相同的全局执行通道。即使两个交易接触到完全不相关的合约,对不同的用户进行操作,并且没有任何逻辑交互,它们仍然需要完全排序并一个接一个地执行。
从验证者的角度来看,没有安全的方法可以提前假设独立性。
存储是全局的并且隐式共享的,因此依赖关系仅在执行期间发现。
这就是基于 EVM 的系统中的并行执行如此困难的原因。系统无法自信地并行执行交易,因为它不知道事先哪些状态将被影响。
Sui 的方法:使依赖关系显而易见
Sui 通过从一开始就显式化依赖关系来颠覆这个模型。状态存在于对象中,对象必须作为交易输入传递,并且所有权是明确的。因此,验证者可以预先看到一个交易将接触到哪些状态。
这使得在 EVM 中很大程度上遥不可及的事情成为可能。对不相交的已拥有或不可变对象集进行操作的交易不需要相对于彼此排序。它们可以被独立地验证、执行和最终确定,从而允许安全的并行执行,而不会牺牲正确性。
共识和条件排序
Sui 并不消除共识。相反,它缩小了共识的作用。
所有交易都在一个单一的共识系统下处理,但只有当交易实际在相同的状态上竞争时才需要排序(ordering)。因为交易必须预先声明它们读取或修改的对象,所以系统可以区分排序很重要的案例和排序不重要的案例。
当交易对不相交的已拥有或不可变对象集进行操作时,没有歧义。这些交易不需要相对于彼此排序,并且它们可以被独立地验证和执行。
只有当多个交易尝试改变相同的对象时,排序才变得必要。在这些情况下,共识建立一个单一的、商定的序列以确保正确性。
并行性不是一种优化,而是一种设计选择
在大多数区块链系统中,性能调优发生在设计完成之后。开发者会寻求 gas 优化、批量处理技巧或链下解决方法,以使系统在瓶颈开始出现时进行扩展。
在 Sui 上,性能从更早的时候开始,在数据模型本身的层面。
当你决定如何表示状态时,例如哪些对象是被拥有的,哪些是被共享的,哪些是不可变的,你同时也在决定系统如何执行。这些选择决定了有多少并行性是可能的,竞争会在哪里自然出现,以及当负载增加时应用程序如何表现。
这是一个微妙但强大的转变。你不是通过越来越复杂的优化来对抗执行模型,而是与它一起工作。并行性不再是你希望运行时在事后能够恢复的东西。它是你从一开始就设计到系统中的东西。
用竞争的术语思考
在 Sui 上设计时,一个有用的思维习惯是问一个简单的问题:
哪些对象是许多用户想要同时接触的?
这些对象自然地定义了你的系统的压力点。它们是竞争会首先出现的地方,是排序约束变得必要的地方,也是性能会在负载下降低的地方。尽早识别它们可以更容易地推理你的应用程序将如何扩展。
在许多情况下,正确的反应不是更努力地优化,而是改变所有权边界。将每个用户的状态移动到已拥有的对象中,保持共享对象的小型和专注,并将不可变的数据推出热路径通常比任何低级别的优化都更有影响力。
这种思考方式在全局存储模型中要困难得多,在这种模型中,一切都是隐式共享的,并且竞争仅在运行时变得可见。在 Sui 上,所有权使这些边界变得显式,从而将竞争从一个惊喜变成了一个设计选择。
执行可预测性作为一个特性
Sui 的执行模型的一个不太明显的好处是可预测性。
在 EVM 中,通常很难提前知道哪些交易会冲突,负载将如何影响延迟,或者性能悬崖可能出现在哪里。许多这些影响只有在系统处于实际使用中时才变得可见,即使这样,也很难仅从代码中进行推理。
在 Sui 上,这些问题在很大程度上由设计回答。对已拥有对象进行操作的交易自然地扩展,因为它们不与其他用户竞争。不可变对象总是廉价读取的,并且永远不会引入冲突。当竞争确实存在时,它是显式的和局部的,通常围绕着一小部分共享对象。
这使得不仅更容易推理正确性,而且更容易推理系统在现实世界条件下的行为。
现在,执行和并行性已经建立在对象和所有权模型的基础上,下一步是看看交易本身的结构,以及 Sui 如何在可以调用和不能调用的内容周围划定明确的界限。
到目前为止,我们已经谈了很多关于代码可以在 Sui 上做什么,就对象、所有权、资产和执行而言。
下一个主题是关于如何进入执行,以及 Sui 如何让你明确哪些函数旨在直接由交易触发。
入口函数:一个显式的交易边界
如果你来自 Solidity,你习惯于一个非常灵活的模型。任何公共或外部函数都可以被调用:
直接由用户调用,
由其他合约调用,
由包装器、代理或中介调用。
这种灵活性很强大,但它也模糊了一个重要的区别:一个函数是打算成为合约内部逻辑的一部分,还是打算成为面向用户的交易入口点?
在 Sui 上,这种区别可以被显式化。
Move 支持入口函数,这些函数被显式标记为可以直接从交易调用的函数。
entry fun claim_reward(
reward: Reward,
ctx: &mut TxContext
) {
// logic here
}
一个入口函数:
可以直接从交易(钱包、CLI、前端)调用,
不能被其他 Move 模块调用,
为交易执行开始的地方定义了一个清晰的边界。
重要的是要注意,入口不是强制性的。Move 模块仍然可以公开公共函数,并且这些函数的行为在可组合性和重用方面更接近 Solidity 的公共或外部函数。
关键的区别在于,当你想要一个更严格的模型时,入口允许你选择加入。
为什么这很重要
对于 Solidity 开发者来说,这起初可能会感到限制。毕竟,灵活性和可组合性是熟悉且通常是理想的。
入口的价值不在于它取代了公共函数,而在于它为你提供了一个工具来清楚地区分:
用户打算直接调用的面向交易的 API,与
旨在被其他模块重用的内部或可组合逻辑。
这使得更容易推理:
哪些操作可以直接由用户触发,
哪些流程是纯内部的,
交易执行从哪里开始和结束。
这对于你不希望函数可以被其他合约以编程方式调用的情况特别有用,例如随机数消耗、特权操作或假设单个交易边界的流程。
在 Solidity 中,强制执行这种分离通常需要额外的保护措施或仔细的约定。在 Sui 上,这种区别可以直接在函数签名中表达,并由语言强制执行。
结果不是更少的灵活性,而是更明确的意图。你可以选择一个函数何时是公共交易表面的一部分,以及它何时只是可重用的逻辑。
显式上下文代替隐式全局变量
你可能注意到的另一个区别是缺少像 msg.sender 这样的东西。
在 Move 中,没有隐式的全局变量可供你的代码使用。如果一个函数需要与交易相关的信息,它必须显式地接收它。
这就是 TxContext 的用途。
ctx: &mut TxContext
交易上下文包含诸如以下内容:
发送者的地址
创建新对象 ID 所需的信息
交易元数据
但至关重要的是,这不是你可以神奇访问的东西。你必须在函数签名中请求它。
这种设计强化了你在整个 Sui 中看到的相同原则:
所有输入和依赖关系都是显式的。
为什么显式上下文是一个特性
你可以查看一个函数签名并立即看到:
它是否创建对象
它是否依赖于发送者
它是否旨在成为一个入口点
没有任何东西是隐藏的。
一个更清晰的心智模型
一个有用的思考方式是:
入口函数定义了用户在一个交易中被允许做什么
非入口函数定义了模块内的可重用逻辑
TxContext 是一个明确的证据,证明一个函数依赖于交易级别的信息
这种清晰的分离减少了歧义,并消除了 Solidity 开发者已经学会解决的许多极端情况。
现在交易边界已经明确定义,拼图的下一块是合约如何初始化和引导。这尤其重要,因为 Sui 没有 Solidity 意义上的构造函数。
如果你来自 Solidity,你会习惯于:
运行构造函数
初始状态在部署时设置
Sui 没有那种意义上的构造函数。相反,Sui Move 支持一个名为 init 的模块初始化器。
什么是 init?
一个 init 函数是一个特殊的函数,它在包发布时自动运行一次。Sui 运行时在发布过程中为每个模块调用 init 函数。
它的作用不是“在合约中设置变量”,而是:
创建初始对象
铸造和分发能力对象
可选地共享或冻结对象作为设置的一部分
这是一个简单的例子:
module my_module;
/// Capability object granting admin rights.
public struct AdminCap has key {
id: UID
}
/// Runs once, right after the module is published.
fun init(ctx: &mut TxContext) {
let cap = AdminCap {
id: object::new(ctx),
};
// Transfer admin capability to the publisher.
transfer::transfer(cap, ctx.sender());
}
这表面上看起来很像 Solidity 构造函数,但语义却大不相同。
没有任意的构造函数参数
与 Solidity 的一个重要区别是 init 不接受任意的用户提供的参数。
你不能像使用 Solidity 构造函数那样传递自定义参数。初始化被有意地约束为确定性和自包含的。
有一些高级模式可以参数化初始化,最值得注意的是通过一次性见证人,但这些超出了本指南的范围。现在,知道 init 是为引导对象和能力而设计的,而不是用于一般配置就足够了。
一个有用的心智模型
一个有用的思考方式是:
Solidity 构造函数:用参数初始化合约存储
Sui init:创建第一个对象并移交权限
一旦初始化完成,其他一切都遵循你已经看到的相同规则:所有权是显式的,访问由能力强制执行,并且状态存在于对象中。
随着初始化完成,我们现在已经完成了 Sui 应用程序从发布到执行的生命周期。剩余的主题较少关于机制,而更多关于用 Move 构建的感受。
如果你来自 Solidity,你对 Move 的第一反应之一可能类似于:
为什么这种语言如此严格?为什么我必须对一切都如此明确?
这种反应是完全正常的。
Move 被有意地设计成使某些错误不可能表达,即使这意味着编写更多的代码或提前更努力地思考。
Solidity 感觉很灵活,直到它不是为止
Solidity 给你很大的自由:
你可以自由地复制值
你可以隐式地丢弃东西
你几乎可以从任何地方读取和写入存储
这种灵活性很强大,但这也意味着编译器对意图的理解非常少。从它的角度来看,余额、计数器和随机数都只是整数。
因此,许多重要的规则,如“不要重复花费”、“不要忘记更新双方”、“不要丢失资金”等,仅存在于开发者的自律和审查过程中。
Move 对稀缺性有自己的看法
Move 从一个不同的前提开始:
某些值代表稀缺资源,语言应该以不同的方式对待它们。
这就是为什么 Move 有像这样的概念:
能力(复制、丢弃、存储、键)
线性资源
显式的所有权和移动
Move 并没有假定一切都可以被复制和丢弃,而是强迫你声明一个类型被允许做什么。
例如:
一个币不能被复制
一个能力不能被静默地丢弃
除非你拥有一个对象,否则它不能被改变
这些规则由编译器强制执行,而不是由约定强制执行。
能力作为意图,而非语法
早些时候,你看到了像 key 和 store 这样的能力。虽然它们起初可能看起来像是语法噪音,但它们有一个重要的目的:
能力告诉编译器一个值应该如何使用。
它们将意图直接编码到类型系统中:
这个值是否打算成为一个链上对象?
它可以存储在其他对象中吗?
复制它是安全的吗?
允许它消失吗?
一旦你内化了这一点,Move 代码开始读起来不像“仪式”,而更像是编译器强制执行的文档。
显式性作为一个特性
同样的理念出现在每个地方:
交易上下文是显式的
对象所有权是显式的
状态依赖关系是显式的
入口点是显式的
这起初可能会感到冗长,特别是如果你习惯于 Solidity 的隐式全局变量。但这有一个回报,即当你阅读一个函数签名时,你通常可以准确地说出它做什么以及它依赖什么。
很少有事情会“意外”发生。
一个有用的思考 Move 的方式不是“一个更严格的 Solidity”,而是更接近于:
一种用于数字资产的系统语言。
就像 Rust 强迫你思考内存所有权以避免整个类别的错误一样,Move 强迫你思考资产所有权以避免整个类别的金融和安全错误。
学习曲线是真实的,但它是前期加载的。一旦心智模型点击,许多模式会变得更简单,而不是更难。
总的来说,我们已经涵盖了完整的概念弧线:从状态存在的地方,到对象和所有权,到资产、安全、执行、交易和初始化如何组合在一起,最后是语言哲学,它将一切联系在一起。
剩下要做的就是缩小范围,简要地谈谈开发者体验和工具,然后总结。
在这一点上,许多 Solidity 开发者自然会关注的不再是概念上的问题,而是实际问题:
工具就绪了吗?在 Sui 上构建的实际感受如何?
诚然,以太坊生态系统已经有多年发展,围绕 Solidity 的工具的广度很难与之匹敌。但 Sui 的工具是有意集中在其执行模型中最重要的事物上。
以 CLI 为中心的工作流程
Sui 非常强调其命令行工具。sui CLI 是大多数开发工作流程的主要入口点,它涵盖了一个非常广泛的领域:
创建和管理账户
运行本地网络
发布和升级包
检查对象和所有权
提交和模拟交易
因为状态存在于对象中,所以能够直接从 CLI 检查对象被证明是非常有价值的。你不仅仅是查询存储;你正在查看具体的状态,具有明确的所有权和历史记录。
本地开发感觉很熟悉
运行本地 Sui 网络并迭代 Move 代码感觉比你想象的更接近传统开发:
发布一个包
通过交易与之互动
检查结果对象
迭代和升级
不需要设置复杂的索引器或自定义脚本来了解发生了什么。对象模型使状态可见和有形,这在基于 EVM 的系统中通常更难实现。
更少的工具,更多有意的工具
Sui 生态系统还没有 Solidity 享有的第三方工具的数量,但它确实直接将许多基本功能纳入平台及其标准工具中。
与其严重依赖外部框架来掩盖语言限制,不如通过以下方式直接解决许多问题:
Move 类型系统
显式所有权和状态访问
内置的交易模拟
清晰的执行边界
对于许多开发者来说,这最终感觉像更少的总体工具,但也减少了粘合代码和更少的尖锐边缘。
一种不同的生产力
当你适应心智模型时,在 Sui 上构建通常一开始感觉更慢。但一旦发生这种调整,生产力往往会提高。这并不是因为你编写的代码更少,而是因为你花更少的时间来调试不可见的状态、访问控制错误或意外的互动。
从这个意义上说,Sui 的开发者体验更多的是关于信心的速度,而不是编写代码的速度。
随着工具的覆盖,我们现在可以通过回顾和总结从 Solidity 到 Sui 的整体转变来总结,不是从功能的角度,而是从心态的角度。
这就是我们将要完成的地方。
从 Solidity 迁移到 Sui 实际上不是关于学习新的语法或记住新的 API。它是关于采用一种不同的方式来思考区块链应用程序是如何构建的。
在 EVM 世界中,你习惯于:
合约拥有状态
全局存储是隐式可用的
通过模式和纪律来强制执行安全
性能是你围绕着优化的东西
在 Sui 上,这些假设被颠覆了:
状态存在于对象中
所有权是显式的,并由系统强制执行
资产是资源,而不是数字
共识和排序是由所有权和竞争决定的,而不是一个不变的税收
并行性是你为之设计的,而不是你所希望的
最初感觉有约束的东西:显式上下文、显式所有权、显式输入,结果是使系统更容易推理、更容易扩展和更难破坏的东西。
对于 Solidity 开发者来说,这可能是一个令人耳目一新的转变。你多年来内化的许多“最佳实践”并没有消失,它们变成了模型本身的属性。你不再需要防御整个类别的错误,你根本无法首先表达它们。
Sui 并没有试图成为“更快版本的以太坊”。它做了不同的权衡,这些权衡出现在各个地方:在语言、执行模型、安全保证和开发者体验中。
如果你对这条道路的走向感到好奇,那么最好的下一步是进行实验:
构建一个小模块
创建和移动一些对象
有意地建模所有权
一旦心智模型建立起来,Sui 就不再感到陌生,而是开始感到非常自然。
Sui 正作为速度、安全性和开发者体验的首选区块链而获得发展势头,这是所有团队都想要的。请务必关注 Sui 和 OpenZeppelin,了解未来几个月将发布的关于库和入门应用程序的更多开发,这将使开发者更容易为未来的金融构建链上应用程序。
如果你想阅读完整的指南,包括第一部分和第二部分,请查看我们研究博客上的完整版本。
- 原文链接: x.com/openzeppelin/statu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!