本研究报告介绍了 Big Vector 的概念,这是一种我们用于 Typus V2 的新数据结构,以缓解 Sui 上数组和动态字段 (dynamic field) 的局限性。Big Vector 的主要目标是探索其在单一交易内需要超扩展性 (hyper-scalability) 的应用中的潜力,同时保持成本在最低水平。
动态字段首次亮相于 2022 年 10 月的 Sui 0.13.0 版本。这些“字段”独特之处在于它们与一个对象 (object) 相关联,而不是在一個数组内將其序列化。在使用动态字段时,通过 Move 函数在执行过程中进行访问,而不是在对象最初加载时就提前加载。
尽管动态字段没有绝对的容量限制,但每个交易访问的对象数量有一个最大限制,即 1,000 个对象。如果用户数超过这个限制,单一交易无法完全容纳他们。例如,对于 10,000 名用户,至少需要将操作分成 10 个批次。此外,访问动态字段内的对象的气费遵循固定的收费机制,这意味着随着更多的读取和访问被执行,相关的燃气费用会增加。
为了更好地理解这一点,让我们通过 Typus V1 框架的一个例子来进行说明。为了缓解 1,000 个对象的限制,我们通过将原本是一个单一步骤的代码拆分成 17 个步骤来进行重写。
(以下内容需要对 Typus 的架构有基础的了解,请参考 https://docs.typus.finance/architecture 以获取更多信息。)
(0) settle (0) settle_performance_fee_balance (1n) settle_bidder_balance (2n) settle_adjust_active_user_share_ratio (3n) settle_adjust_deactivating_user_share_ratio (4n) settle_reset_bidder_user_share (5n) settle_remained_performance_fee_combination (6n) settle_rock_n_roll_deactivating (7n) settle_rock_n_roll_active(8n) activate_share(9) activate_vault(10) new_auction(11) delivery_premium(12n) refund_active(13n) refund_deactivating(14n) delivery_active(15n) delivery_deactivating(16n) delivery_bidder
将过程拆分成 17 个交易明显影响了执行速度,导致了明显的减速,同时也增加了气费。其根本问题在于访问动态字段所带来的高成本。
https://github.com/MystenLabs/sui/blob/main/crates/sui-protocol-config/src/lib.rs#L1164
执行一个包含 20,000 用户的期权库的单轮通常会产生大约 30 到 70 SUI 的气费。考虑到目前有 14 个期权库,每周 7 个,每天 7 个,我们的月度结算气费将在 7,140 到 16,660 SUI 的范围内。
为了扩大规模并容纳更多的底层资产、行权价格和到期日,我们不可避免地需要扩展超出现有的 14 个期权库。然而,这样的扩张将会由于不断上升的气费而大幅提高我们的运营成本。
即使撇开气费问题,拆分交易也会给合约带来额外的复杂性。某些交易需要在下一个交易开始之前完成,这种方法涉及添加多个临时字段以实现新的函数来执行原本在每一轮中执行的任务。
这引入了额外的依赖关系,并降低了代码的整体稳健性 (robustness)。由此产生的问题是,如果在访问这些临时字段时出现逻辑错误,导致计算错误,这不仅需要大量的时间来在复杂的代码中找出问题,还需要额外的人力来纠正合约。
让我们来看一个我们过去遇到的场景。这个例子发生在所有“交易失败”事件发生之前。在之前的内部调查中,我们注意到尽管最近的几轮都没有导致实现盈利,但我们的存款用户份额仍有所下降。理论上,用户份额应该与前几天的数值保持一致。
在更仔细地检查后,我们发现问题源于在步骤结算 step settle 过程(步骤 0 到 7)中没有完全限制用户操作。这里的 n 的存在表明它涉及到解决遇到 1,000 个动态字段访问的挑战,并需要多次运行来解决。
(0) settle (0) settle_performance_fee_balance (1n) settle_bidder_balance (2n) settle_adjust_active_user_share_ratio (3n) settle_adjust_deactivating_user_share_ratio (4n) settle_reset_bidder_user_share (5n) settle_remained_performance_fee_combination (6n) settle_rock_n_roll_deactivating (7n) settle_rock_n_roll_active(8n) activate_share(9) activate_vault(10) new_auction(11) delivery_premium(12n) refund_active(13n) refund_deactivating(14n) delivery_active(15n) delivery_deactivating(16n) delivery_bidder
这个问题出现的场景是,在一个特定的实现盈利的结算阶段,用户在步骤 2step 2n 中执行取消订阅 unsubscribe。步骤 2step 2n 的目的是将用户的份额和余额以 1:1 的比例对齐,以避免后续的浮点计算可能导致的微小余额差异。
然而,如果一个用户在这个过程中执行了取消订阅 unsubscribe,特别是当一部分用户已经达到了 1:1 的比例,而其他人还没有调整时,那么还没有完成调整的用户(相对于他们实际价值持有更高的份额和余额)将会把他们的份额转移到“停用中的子期权库”deactivating sub vault 中。因此,这个用户实际上会承受部分损失,将这个损失按比例分配给仍然在“活跃子期权库”active sub vault 中的人。
余额 (balance) 和份额供应 (share_supply) 不是 1:1 的比例
在这一事件之后的后续轮次将需要重新调整回 1:1 的比例,因为在活跃子期权库中的用户份额和余额将不再平衡。如果后来有任何用户在步骤 2n 中取消订阅,它会让情况更复杂,使追踪原始存款金额这件事变得更具挑战性。
由于在活跃子期权库中的用户份额和余额不再是 1:1 的比例,每一轮的步骤 8nstep 8n 都会将活跃子期权库中现有的损失或收益重新分配给从“预热子期权库”warmup sub vault 转移过来的用户。此外,每一个在步骤 12nstep 12n 中退款 refund_active 时都会将未填充的用户份额 (unfilled user shares) 从活跃子期权库转回到预热子期权库。这种复杂性会在每一轮中累积,这就是为什么,在计算错误的情况下,尚未经历实现盈利阶段的用户的新存款可能会导致意外的损失。
这个操作的问题源于拆分交易需要以原子操作来执行(译注:原子操作 (atomic operation) 指的是不可被中断的一个或一系列操作,简单来说就是这些交易最終需要全部被执行,但是必须依序一个一个来执行)。为了解决这个问题,我们实施了一个类似于链上互斥锁 (mutex)的概念,在上述步骤的操作期间暂时停止用户交易。这样做的后果是,操作需要相当多的时间,导致我们的拍卖时间延迟,并需要更多的交易执行者 (crankers)。
我们的产品并不是完全依赖动态字段的理想选择。尽管数组可能看似是解决这个问题的一个潜在解决方案,但 Sui 对数组的容量设置了上限,限制为 256,000 字节 (bytes)。为了更直观地说明这一点,让我们从 Typus 出发,它为每个元素分配 100 字节的用户数据。在这种情况下,单个数组的最大容量仅限于容纳 2,560 名用户。任何试图超过这个阈值的尝试都将导致交易失败。这让我们难以直接使用数组这个解决方案。
总结一下,我们从当前的 Sui 框架中面临的两个限制是:
让我们引入 big vector 的概念,这是一个旨在同时解决这两个限制的解决方案。big vector 背后的基本思想是使用动态字段来存储多个数组,类似于将一个大型数组分解成多个较小的切片。如果我们可以在一个单独的数组切片中存储 2,000 个用户的数据,并且一个单独的动态字段一次可以访问多达 1,000 个对象,那么最大容量就扩展到在单个交易中容纳 2,000,000 个用户。
从本质上讲,Big Vector 通过合并动态字段和数组,打破了数组所施加的上限,同时放宽了动态字段内 1,000 个对象的访问限制。这导致了单个交易内可以访问的元素数量的增加,有效地提高了可扩展性。
我们希望将 Big Vector 集成到 Sui 框架中的理由是,我们认为在访问相同的动态字段时,气费不应随每次访问而激增,而且应该有一种更高效的内存分配方法。
我们使用我们的 Big Vector 框架在测试网上复制了期权库结算气费的实验。当执行一个包含 45,000 名用户的期权库的单一轮次时,相关的气费总计为 7.1 SUI。考虑到我们当前的期权库,这大大降低了我们每月的运营结算气费支出,大约为 1,690 SUI。这与 V1 中的 7,140 到 16,660 SUI 的范围相比有着显著的下降,同时还能同时容纳更多的用户。
V1 mainnet gas fee 20k users in the vault -> 30 — 70 SUI / round
V2 testnet gas fee 45k users in the vault -> 7.1 SUI / round
在我们的 V2 中,费用减少的一个重要因素是数据访问的变化。与过去每个用户都有自己的动态字段不同,我们现在将数组放在动态字段内。这导致了动态字段访问频率的大幅下降。因此,剩下的只是常规的、标准的数据访问气费成本。
V2 testnet gas fee txns
update_price + activate: 3.80566692 SUI https://suiexplorer.com/txblock/8mp6Jug43CQd8Y4Xk5K5gmApxVt2cz91a8qv8MEbZc8Q?network=testnet
new_auction: 0.019801108 SUI
https://suiexplorer.com/txblock/Du2Dvbnc3nN3h51t16moEGnjBDBvRcWnQbpGDnS6hJ2s?network=testnet
delivery: 1.8085723 SUI
https://suiexplorer.com/txblock/7SHDY4JLBJqpGkvqP78F83JJqPUfh4L8ugG6rzrUth6c?network=testnet
recoup: 2.39032962 SUI
https://suiexplorer.com/txblock/BmLXPeoL5U1TeaVyhMXDjeUnUiL53vb1oAMocG5tR7vV?network=testnet
settle: 0.001376352 SUI
https://suiexplorer.com/txblock/CUiKwKbMpXUfh3vonQL1zJ9nxGiYSokGE2G4dp2r8Ts3?network=testnet
通过大幅降低运营成本,我们现在能够让我们的产品多样化。Typus V2 可以涵盖更广泛的基础资产、执行价格和到期日。将操作合并到单一交易的优点是,它将执行多个交易可能产生的计算差异的风险降至最低。因此,我们可以推出短期期权库,如 1 小时、5 分钟和 1 分钟的期权库,同时保持结算的准确性。
我们认识到动态字段作为数据存储访问的作用,但我们想强调它们对可扩展性和复杂代码相关风险的限制。有人可能会争辩说,通过实施限制和优化数据结构可以缓解这些问题,但缺点是执行速度更慢和成本更高。鉴于此,它提出了一个问题:是否有更好的解决方案?
Big Vector 作为一个解决方案,适用于两种应用。第一种是需要单一交易操作,对时间敏感的这种应用类型;第二种是因为其数据访问密集,而无法完全倚赖动态字段的这种应用类型(译注:因为有动态字段限制,且成本可能很高)。这种方法不仅让开发者不用因为 1,000 个对象限制而在代码上妥协,而且还大幅降低了气费。
我们的目标是开启一个关于将 Big Vector 集成到 Sui 框架中的讨论,提供一种更高效的内存分配方法。我们认为,当用户访问相同的动态字段时,气费不应随每次访问而增加。
随着链上流动性持续积累和 Sui 的 TVL 经历指数级增长,我们预计会有大量新用户和应用涌入。对于一个能够在单一交易内提供超高可扩展性同时保持最低成本的框架,有其技术上的必要性。我们认为,应该让在 Sui 上的所有开发者尝试实施这种新方法。
由 Typus 团队的 six0hfour 撰写,由 kyrie.move 翻译。
参考资料:
Typus 开发者
https://github.com/MystenLabs/sui/blob/main/crates/sui-protocol-config/src/lib.rs#L1164
https://docs.sui.io/
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。