本文深入探讨了NEAR协议的Nightshade分片技术及其跨合约调用的实现机制,重点分析了异步跨合约调用带来的潜在安全风险。文章通过具体案例展示了如何利用时间窗口进行攻击,并提出了通过状态锁定、回调验证和失败回滚等关键安全措施来防范这些风险,从而保障智能合约的安全性。
这是分为三个部分系列文章的第一部分。随便看看其他的:
NEAR 协议实现了一种创新的分片解决方案,称为“Nightshade”,旨在解决高可扩展性问题,同时保持安全性和可用性。本文探讨了 NEAR 分片实现的技术细节,以及跨合约调用如何在具有安全含义的这种架构中工作。
像区块链这样的分布式系统中的分片是一种通过将网络(这里是区块链)划分为多个“分片”来提高网络速度和容量的方法。这种划分将允许每个分片独立于其他分片处理交易——理论上增加了交易吞吐量。
NEAR 实现了他们称为“Nightshade”的分片方法。每个分片并行处理网络交易的子集。NEAR 分片在每个区块中创建分片(“块”),而不是分片链。因此,它只维护一个链,并允许异步(“跨分片”)交易。
以下是 Nightshade 与 Beacon 链相比的简化图示:
图 1. Nightshade 分片图示。来源:LiNEAR 协议博客
NEAR 的分片允许一种称为“动态重新分片”的功能。 它允许 NEAR 根据网络需求自动调整分片数量。 NEAR 验证者也只需要跟踪其指定分片的状态。
由于 NEAR 分片技术的性质,其跨合约调用是异步且独立的。 这对应该如何处理回调有重要的影响。
NEAR 中的跨合约调用通过 promise 系统进行操作。 每个跨合约交互都涉及两个关键组件:发起调用和处理结果。 这是在底层发生的事情:
当合约 A 需要与合约 B 交互时,它使用 NEAR 的 SDK 创建一个 promise。 这个 promise 代表了对合约 B 调用的未来结果。 该过程是异步的,这意味着合约 A 不会闲置地等待合约 B 响应 - 它在等待回调时继续进行其他操作。
以下是如何在代码中实现此目的的示例:
##[near_bindgen]
impl MyContract {
pub fn initiate_cross_contract_call(&mut self) -> Promise {
Promise::new("contract-b.near".parse().unwrap())
.function_call(
"process_request".to_string(),
json!({
"data": "example"
}).to_string().into_bytes(),
0, // attached deposit
Gas(5_000_000_000_000) // attached gas
)
.then(
Promise::new(env::current_account_id())
.function_call(
"callback_function".to_string(),
Vec::new(),
0,
Gas(2_000_000_000_000)
)
)
}
#[private]
pub fn callback_function(&mut self, #[callback_result] call_result: Result<String, PromiseError>) {
match call_result {
Ok(result) => {
// Handle successful result
}
Err(_) => {
// Handle error case
}
}
}
}
这些跨合约调用的异步特性引入了潜在的漏洞,必须仔细考虑。
竞争条件是最重要的安全挑战之一。 在跨合约调用及其回调执行之间的这段时间内(通常为 1-2 个区块),你的合约保持活动状态且可调用。 这意味着恶意用户可能会利用此窗口来操纵合约状态。
让我们检查两个具体的场景,说明正确的实现和潜在的漏洞。
在此图中,我们看到一个合法的用户“Alice”与智能合约交互:
stake()
以接收 200 个代币最终状态显示 Alice 拥有 0N 余额和收到的 200 个代币。 这是预期的安全行为,其中合约在整个异步操作中保持适当的状态管理。
但是,如果没有适当的保护,恶意用户可能会利用跨合约调用的异步特性:
在此图中,我们看到攻击者如何可能利用该系统:
stake()
(在本例中为 2 次)此漏洞的存在是因为原始实现未阻止在处理期间进行多次 stake()
调用。
为防止此类漏洞,请实施以下最基本的安全措施:
#[private]
装饰器。 这确保只有合约可以调用回调函数,而外部用户不能。以下是如何在代码中实现此目的的示例:
##[near_bindgen]
impl StakingContract {
pub fn stake(&mut self) -> Promise {
// Add state lock to prevent multiple calls
assert!(!self.processing, "Already processing a stake operation");
self.processing = true;
// Store initial state for potential rollback
self.last_stake_amount = self.stake_balance;
Promise::new(self.staking_target.clone())
.function_call(
"stake_tokens".to_string(),
// ... stake parameters ...
Gas(5_000_000_000_000)
)
.then(Self::ext(env::current_account_id())
.with_static_gas(Gas(2_000_000_000_000))
.stake_callback())
}
#[private]
pub fn stake_callback(&mut self, #[callback_result] call_result: Result<(), PromiseError>) {
// Always reset processing flag in callback
let processing = std::mem::replace(&mut self.processing, false);
assert!(processing, "Callback called without active processing");
match call_result {
Ok(_) => {
// Verify state changes are valid
assert!(
self.stake_balance >= self.last_stake_amount,
"Invalid state change detected"
);
}
Err(_) => {
// Rollback any state changes
self.stake_balance = self.last_stake_amount;
env::log_str("Stake operation failed, state rolled back");
}
}
}
}
这篇文章介绍了 NEAR 区块链中分片所包含的本质,并触及了跨合约调用及其安全含义的一些复杂性。
请记住,这种类型的漏洞并非我们的质押操作示例所独有。 如果没有得到适当的保护,任何修改合约状态的跨合约调用都可能容易受到类似的竞争条件的影响。 最好假设恶意用户会尝试利用调用和回调执行之间的时间窗口。
测试和理解这些安全措施对于开发人员和安全审核员来说非常重要。 它可以提高协议的安全性,保护用户的资产,并保持整个生态系统的安全和可信。 我们鼓励所有开发人员和安全审核员随时了解情况,并积极主动地查找和缓解这些类型的漏洞。
在 Sigma Prime,我们致力于保护和加强各种区块链网络和协议。 如果你正在构建解决方案并希望利用我们在该领域的最先进的安全专业知识,请联系我们!
- 原文链接: blog.sigmaprime.io/near-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!