文章分析了 Soroban 智能合约中存储不断增长数据的不同设计方案,重点比较了 Instance 与 Persistent 存储、Vector 与 Variable DataKey 的差异。结论是:使用带计数器的可变 DataKey 并配合 Persistent 存储,既能避免 64kb 导致的 DoS 风险,也能降低合约交互成本。
在 Soroswap.Finance 协议的开发历程中,关于智能合约数据类型的关键决策已经制定。存储的选择会影响拒绝服务 (DoS) 的风险,增加合约交互成本,或者同时引入这两种风险。
本文探讨了在 Soroban 区块链上存储不断增加的信息量的四种设计模式的优缺点。此外,它还深入研究了每种场景的相关成本。
背景: Soroswap.Finance 是 Stellar 区块链内 Soroban 智能合约平台中的一个自动化做市商,由 PaltaLabs 🥑 团队开发。
在这里关注代码:Instance-Persistent-Dos-Soroban
本文证明了:
从 Soroban 文档中,我们知道:
示例挑战涉及创建一个智能合约,该合约存储两个不断增加的 32 字节地址集合,分别代表买家和卖家。使用两个独立的集合,是因为数据类型的选择可能会在一个集合增长时影响另一个集合。
在外部,与此合约的交互旨在为每个集合遵循以下模式:
的确,每个设计的测试都是相同的,而且都通过了!所以开发者请注意,通过测试并不意味着你的代码是安全的!
在接下来的章节中,将解释四种设计模式,其中只有一种被证明是无 DoS 风险且不会增加合约交互成本的。
使用这种技术,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 次地址推入。更多详情,包括代码、模拟和结果,可以在 代码库 中找到。此外,这种设计增加了每次调用智能合约的成本,即使是对于无关的函数也是如此。
与设计 1 类似,这种方法证明了 Instance 存储是存储在与智能合约本身独立的 LedgerEntry 中的。在这种设计下,读取合约的成本随着每次推入操作而增加,即使是对无关函数也是如此。这种设计同样会导致 ResourceLimitExceeded 错误。
这种设计涉及将 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 数据类型不会在每次调用合约时都被检索。
这种设计引入了 可变 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)。
最后,将可变 DataKeys 作为 Instance 数据类型的解决方案是一个错误。这种设计与使用 Vector 一样低效,因为存储在单个 Ledger Entry 中共享的不同插槽中几乎等同于将所有内容存储在一个插槽中(类似于 Vector)。这种情况会导致 DoS 攻击并产生 ResourceLimitExceeded 错误。
你喜欢这篇文章吗?在我们的 Discord https://discord.gg/HFkBquZNNg 联系我们,或者在 https://paltalabs.io 找到我们。
- 原文链接: dev.to/soroswap/-costs-d...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!