本文介绍了 Aptos Move VM 从 Eager Loading 转向 Lazy Loading 的原因与效果。

TL;DR:
Move Virtual Machine (VM) 使用一个名为 loader 的组件来获取包含 Move bytecode 的 modules。Modules 可以依赖其他 modules,从而支持 smart contract 的可组合性。开发者可以通过在链上发布新的 bytecode 来升级 modules。
当用户执行 smart contract 中的某个函数时,VM 会加载定义该函数的 module,然后解释执行 bytecode。加载一个 module 不只是“读取字节”:loader 会从存储中获取 module code,进行反序列化,并验证它。
一直以来,VM 采用的是 Eager Loading 方法:通过加载某个模块及其依赖、依赖的依赖等,来验证该 module 是否能够正确链接到其声明的 dependencies。最坏情况下,可能会加载一个 module 的所有传递依赖。
为了限制模块加载所耗费的时间并防止拒绝服务攻击,VM 会对每次 module load 收取 gas,并限制任何 module 可拥有的传递依赖数量。使用 eager loading 时,最坏情况是加载一个 module 的所有传递依赖,因此 VM 无法在“事后”安全地对它们进行 metering。相反,VM 必须预先遍历所有传递依赖,以便:
不幸的是,这种设计损害了 smart contract 的可组合性,并限制了流行的 DeFi use cases。
链上的 DEX aggregators 是常见的 DeFi contracts,允许用户在同一条区块链内交换资产。下面是一个用 Move 编写的此类 aggregator module 的最小示例:它允许用户在某个 exchange 上将类型 X 的资产兑换为类型 Y 的资产。
module 0x99::dex_aggregator {
// 该模块的所有 dependencies。
use 0x1::fungible_asset;
use 0x123::dex_a;
use 0x456::dex_b;
// 支持的 DEX 列表。
const DEX_A: u8 = 0;
const DEX_B: u8 = 1;
// 在选定的 DEX 上将某些资产 X 兑换为资产 Y。
public entry fun swap<X, Y>(user: &signer, amount: u64, dex: u8) {
let asset_x = fungible_asset::withdraw<X>(user, amount);
let asset_y = if (dex == DEX_A) {
dex_a::swap<X, Y>(asset_x)
} else if (dex == DEX_B) {
dex_b::swap<X, Y>(asset_x)
} else {
// 这个 DEX 未知 - 中止。
abort 1;
};
fungible_asset::deposit<Y>(user, asset_y);
}
}
图 1:一个用 Move 编写的 DEX aggregator module 示例。这个 aggregator module 有 3 个 dependencies:0x123::dex_a 、0x456::dex_b 、0x1::fungible_asset 。这些 modules 都各自还有自己的 dependencies(此处未展示)。
代码完成后,开发者发布 dex_aggregator module。在发布期间,VM:
0x123::dex_a、0x123::dex_a 的所有传递依赖、0x456::dex_b、0x456::dex_b 的所有传递依赖、0x1::fungible_asset 及其传递依赖;代码部署后,用户与其交互,提交交易来执行 swap。对于每笔交易,在执行 swap 之前,VM 都会重复发布流程中的步骤 (1) 和 (2):遍历传递依赖、收取 gas,并强制执行依赖数量的硬性限制。只有完成这些之后,它才会执行指定的 swap 函数。
系统看起来运行良好,用户也很满意,直到……
使用 eager loading 时,随着 DEX aggregator module 或其 dependencies 的演进,问题会出人意料地容易出现。
问题 1:该 module 无法重新发布
开发者决定集成一个新的 exchange,发布为 dex_c。他们向 dex_aggregator module 添加 dex_c dependency,并尝试重新发布。结果发布失败,因为 transitive dependencies 的数量超过了硬性限制。这形成了可组合性的上限:无法再添加新的 exchange。
问题 2:该 module 无法加载
Exchange A 的开发者发布了一个新特性并升级了他们的 module dex_a。升级成功了;然而,dex_a 也增加了更多传递依赖,并因此破坏了 dex_aggregator:其传递依赖数量超过了限制。结果,任何对 aggregator 的调用都会失败,即使用户想通过未发生变化的 dex_b 来进行交换。
问题 3:过高的 gas 收费
即使 dex_aggregator 仍然可以调用,eager loading 也会增加执行成本。如果用户选择通过 exchange B 进行交换,则会调用 dex_b::swap,而不会触及 dex_a::swap。但 eager loading 仍然会对包含 dex_a(以及它新增的任何依赖)的所有传递依赖进行 metering。用户实际上是在为他们没有使用的集成支付 dependency tax。
问题 4:性能开销
Loader V2 和 module caching 使频繁使用的 modules 加载速度非常快。但 eager loading 仍然需要遍历传递依赖来收取 gas 费用。即使所有 modules 都已经加载并位于 cache 中,VM 仍然必须执行与可能依赖数量成比例的工作,而不是与实际执行到的 dependencies 数量成比例的工作,这在依赖数量增长时会成为不必要的开销。
这一解决方案作为 AIP-127 的一部分,于 2025 年 12 月 16 日在 Aptos mainnet 上启用,用 Lazy Loading 取代了 eager loading。Lazy Loading 将思维模型改为“只加载执行时实际用到的 modules”。
具体来说:
A 上进行一次 swap 时,只会对 0x99::dex_aggregator、0x123::dex_a 以及 0x123::dex_a 将要用到的任何 dependencies 收费。以上图 1 的示例为例,在发布包含 DEX aggregator 代码的 module 时,VM 只会对 0x99::dex_aggregator、0x123::dex_a 和 0x456::dex_b 进行 metering。
这种 lazy 方法保留了所有安全保证:在发布时,每个 module 都会被验证并检查,以确保它能够正确链接到 dependencies。与此同时,这种方法移除了加载所有传递依赖这一不必要的要求。关于 module metering 的详细规范,请参见 AIP-127。
使用 Lazy Loading 后,每笔交易的 gas 使用会降低**,因为 gas 只会对实际使用到的 modules 收费。同时,也更不容易触及 dependency limits。图 2 展示了在使用 Eager 或 Lazy Loading 回放 mainnet blocks 时的 block gas usage。该图显示了版本 [2719042368, 2719042916) 的 mainnet transactions 的 block gas usage,总计 61 个 blocks。\
\
\
\
图 2:在使用 eager 或 lazy loading 回放 61 个 mainnet blocks [2719042368, 2719042916) 时的 block gas usage。越小越好。\
\
\
lazy loading 的目标并不是提升性能。尽管如此,实验评估显示,lazy loading 的性能优于或不逊于 eager loading。图 3 展示了在相同版本 [2719042368, 2719042916) 下,启用和不启用 lazy loading 时 mainnet blocks 的平均执行时间。前几个 blocks 的执行之所以更快,是因为发生的 module accesses(cache misses)更少。一旦 cache 中包含了大部分用到的 contracts,lazy loading 就能提供同等性能。\
\
\
\
图 3:在使用 eager 或 lazy loading 回放 61 个 mainnet blocks [2719042368, 2719042916) 时的平均 block execution time(毫秒)。越小越好。\
\
\ eager loading 使 Move smart contract 的可组合性变得非常脆弱:contract upgrades 可能会因 dependency limits 而被阻止,或者在下游 dependency 发生变化后导致 transactions 失败。用户也会因为执行 transaction 时从未触及的 dependencies 而被收取 gas。\ \ Lazy loading(AIP-127)改变了这一模型:dependencies 会在首次使用时进行 metering 和加载。这对于 DeFi 尤其重要,因为 smart contracts 天生依赖很多其他 protocols:它们会针对许多其他 protocols 进行编译,但典型的 transaction 只会经过少数几条执行路径(例如某个特定 exchange)。使用 Lazy Loading 后,添加新的 dependency 不再会迫使每笔 transaction 为其所有传递依赖付费。下游 dependency upgrades 也不太可能破坏那些从未触及新代码路径的无关执行。\ \ *位于 0x1–0xf 等“特殊”地址上的 dependencies 不会被 metering。\ \ **在某些情况下,使用 lazy loading 可能会导致 gas usage 增加,仅仅因为此前从未对其进行 metering。例如,view functions 现在会对其加载的 modules 进行 metering。
- 原文链接: aptoslabs.medium.com/laz...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!