Soroban 中的成本、DoS 风险以及 Instance 与 Persistent 数据类型

文章分析了 Soroban 智能合约中存储不断增长数据的不同设计方案,重点比较了 Instance 与 Persistent 存储、Vector 与 Variable DataKey 的差异。结论是:使用带计数器的可变 DataKey 并配合 Persistent 存储,既能避免 64kb 导致的 DoS 风险,也能降低合约交互成本。

Soroswap.Finance 协议的开发历程中,关于智能合约数据类型的关键决策已经制定。存储的选择会影响拒绝服务 (DoS) 的风险,增加合约交互成本,或者同时引入这两种风险。

本文探讨了在 Soroban 区块链上存储不断增加的信息量的四种设计模式的优缺点。此外,它还深入研究了每种场景的相关成本。

背景: Soroswap.Finance 是 Stellar 区块链内 Soroban 智能合约平台中的一个自动化做市商,由 PaltaLabs 🥑 团队开发。

在这里关注代码:Instance-Persistent-Dos-Soroban

TLDR;

本文证明了:

  • Instance 数据类型共享一个公共的 Ledger Entry,如果存储的信息总量达到 64kb,会导致合约失败。
  • Instance 存储 Ledger Entry 与合约大小无关,无论合约尺寸如何,都预留了固定的 64kb。
  • Instance 数据类型在每次交互时都会被读取,使得任何合约交互的成本都更高。
  • 在 Vectors 或 Mappings 中存储无界数据存在达到 64kb 并容易受到 DoS 攻击的风险。
  • 可变的 DataKey 技术是存储无界数据的推荐方法。

Instance 和 Persistent 数据类型

从 Soroban 文档中,我们知道:

  • Instance 存储的数据限制由 Ledger Entry 大小决定(来源)。
  • 所有 Instance 存储都保存在一个大小为 64kb 的单一合约实例 LedgerEntry 中(来源)。
  • Ledger Entry 大小上限为 64kb(来源)。

挑战:存储无界信息

示例挑战涉及创建一个智能合约,该合约存储两个不断增加的 32 字节地址集合,分别代表买家和卖家。使用两个独立的集合,是因为数据类型的选择可能会在一个集合增长时影响另一个集合。

在外部,与此合约的交互旨在为每个集合遵循以下模式:

  • 调用一个函数来存储一个新地址(编号 n)。
  • 获取给定编号 n 的地址。

的确,每个设计的测试都是相同的,而且都通过了!所以开发者请注意,通过测试并不意味着你的代码是安全的!

在接下来的章节中,将解释四种设计模式,其中只有一种被证明是无 DoS 风险且不会增加合约交互成本的。

设计 1:在 Instance 数据类型中存储 Vector。轻量级智能合约的情况。

使用这种技术,Vector 被存储在一个 Instance 存储槽中。每次推入新元素时,都会读取当前的 Vector 值,添加新元素,并将更新后的 Vector 写回同一个 Instance 存储槽。代码如下所示:

let mut vector: Vec<Address> = env.storage().instance()
            .get(&VECTOR_A).unwrap_or(Vec::new(&env)); // 如果没有设置值,则假设为一个空的 vector。

// 将当前合约地址推入 vector
vector.push_back(env.current_contract_address().clone());

// 将更新后的 vector 保存到 instance 存储
env.storage().instance().set(&VECTOR_A, &vector);

DoS 攻击模拟显示,这种设计会导致合约在集合总和达到 64kb 后因 ResourceLimitExceeded 错误而失败,这相当于在每个集合上进行了 818 次地址推入。更多详情,包括代码、模拟和结果,可以在 代码库 中找到。此外,这种设计增加了每次调用智能合约的成本,即使是对于无关的函数也是如此。

设计 2:在 Instance 数据类型中存储 Vector。重量级智能合约的情况。

与设计 1 类似,这种方法证明了 Instance 存储是存储在与智能合约本身独立的 LedgerEntry 中的。在这种设计下,读取合约的成本随着每次推入操作而增加,即使是对无关函数也是如此。这种设计同样会导致 ResourceLimitExceeded 错误。

设计 3:在 Persistent 数据类型中存储 Vector。

这种设计涉及将 Vector 存储在 Persistent 数据类型中。

let mut vector: Vec<Address> = env.storage().persistent()
            .get(&VECTOR_A).unwrap_or(Vec::new(&env)); // 如果没有设置值,则假设为一个空的 vector。
vector.push_back(env.current_contract_address().clone());
env.storage().persistent().set(&VECTOR_A, &vector);

虽然它避免了与其他变量共享存储大小,但它仍然被限制在 64kb 的信息量内。攻击模拟 的细节显示,与之前的示例相比,它允许达到两倍的条目数。然而,对于无关函数,读取合约的成本不会增加,因为 Persistent 数据类型不会在每次调用合约时都被检索。

设计 4:使用带有 Persistent 数据类型的可变 DataKeys。

这种设计引入了 可变 DataKey 技术,其中存储槽的名称取决于一个参数。DataKey 定义如下:

#[contracttype]
pub enum DataKey {
    StoredAddressesA(u32),
}

这种技术通常用于存储不断增加的数据量,正如在 用于存储用户余额的 Token 合约 中所见。信息的存储方式如下:

let mut count: u32 = env.storage().instance().get(&COUNTER_A).unwrap_or(0);
env.storage().persistent().set(&DataKey::StoredAddressesA(count), &env.current_contract_address().clone());
count += 1;
env.storage().persistent().set(&COUNTER_A, &count);

这种设计,包括存储一个 COUNTER 来跟踪存储的地址数量,被认为是最佳选择。它成功地避免了 DoS 攻击,并最大限度地降低了读取合约的成本。

使用 u32 为 StoredAddressA 提供了 2^32 个不同的存储槽,这已经足够了。即使攻击者调用 push 函数 2^32 次,计算出的相关成本也约为 439,289 stroops (0.044 XLM)。这种成本限制使得攻击者实际上无法可行地添加超过 2,196,523,503 个条目 (2^21),即使拥有 XLM 的全部总供应量 (50,001,787,051)。

设计 5:使用带有 Instance 数据类型的可变 DataKeys。

最后,将可变 DataKeys 作为 Instance 数据类型的解决方案是一个错误。这种设计与使用 Vector 一样低效,因为存储在单个 Ledger Entry 中共享的不同插槽中几乎等同于将所有内容存储在一个插槽中(类似于 Vector)。这种情况会导致 DoS 攻击并产生 ResourceLimitExceeded 错误。

联系我们:

你喜欢这篇文章吗?在我们的 Discord https://discord.gg/HFkBquZNNg 联系我们,或者在 https://paltalabs.io 找到我们。

  • 原文链接: dev.to/soroswap/-costs-d...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
soroswap
soroswap
江湖只有他的大名,没有他的介绍。