本文探讨了在Solana多重签名环境中,当部分签名者被攻击时,如何安全地进行交易签名。文章分析了Solana的两种签名方式:基于最近区块哈希的签名和基于持久nonce的签名,并提出了一种在允许签名者观察交易手续费支付者的前提下,通过等待区块哈希过期并观察链上交易来确保安全签名的流程。总结了在签名者无法完全信任的环境下,确保交易安全性的关键在于可观察性。
如果他们的多签签署者被入侵,团队该怎么办? 我们将探索 Solana 的交易签名模型,并提出在 Solana 上存在恶意签署者的情况下安全签名的程序。
Bybit 黑客事件 提出了一个有趣的问题:如果他们的签名者受到威胁,团队该怎么办?
我们首先需要了解 Solana 签名是如何工作的。 有两种方法可以签署 Solana 交易。
最直接的方法是使用“最近的区块哈希”。 来自文档:
在交易处理期间,Solana 验证器将检查每个交易的最近区块哈希是否记录在最近存储的 151 个哈希(即“最大处理期限”)中。 如果交易的最近区块哈希早于此最大处理期限,则该交易将不被处理。
实际的常量在此处定义。
// 领导者将接受的区块哈希的最大期限
pub const MAX_PROCESSING_AGE: usize = MAX_RECENT_BLOCKHASHES / 2;
对于那些好奇的人,该逻辑从这里开始,并且很容易理解,最终在 is_hash_index_valid
检查中结束。
fn is_hash_index_valid(last_hash_index: u64, max_age: usize, hash_index: u64) -> bool {
last_hash_index - hash_index <= max_age as u64
}
一个重要的结果是,任何签名的交易都有大约几分钟的自然过期时间。
由于插槽(即验证者可以生成区块的时间段)配置为持续约 400 毫秒,但可能在 400 毫秒到 600 毫秒之间波动,因此给定的区块哈希只能被交易使用约 60 到 90 秒,之后将被运行时视为已过期。
这意味着攻击者必须在很短的时间内使用恶意的签名交易。
第二种类型的签名是持久 nonce。 这些是为了解决上面提到的功能(或问题)而创建的:短过期时间。
持久交易 nonce 提供了一个机会来创建和签署可以在未来任何时间提交的交易,以及更多。 这开辟了范围广泛的用例,否则这些用例是不可能或太难实现的
如果我们检查一下最近的区块哈希验证代码,我们还可以看到对持久 nonce 的处理。
let recent_blockhash = tx.message().recent_blockhash();
if let Some(hash_info) = hash_queue.get_hash_info_if_valid(recent_blockhash, max_age) {
Ok(CheckedTransactionDetails {
nonce: None,
lamports_per_signature: hash_info.lamports_per_signature(),
})
} else if let Some((nonce, previous_lamports_per_signature)) = self
.check_load_and_advance_message_nonce_account(
tx.message(),
next_durable_nonce,
next_lamports_per_signature,
)
{
Ok(CheckedTransactionDetails {
nonce: Some(nonce),
lamports_per_signature: previous_lamports_per_signature,
})
} else {
error_counters.blockhash_not_found += 1;
Err(TransactionError::BlockhashNotFound)
}
该文档很好地解释了它们的工作原理。
持久交易 Nonce 的长度为 32 字节(通常表示为 base58 编码的字符串),用于代替最近的区块哈希,以使每笔交易都是唯一的(以避免双重支付),同时消除了未执行交易的死亡率。
持久 nonce 由系统程序创建和管理。 它们没有固定的 PDA,因此每个帐户可以有多个关联的 nonce。
使用持久 nonce 后,它将被“推进”以防止重放攻击。 新的 nonce 是基于当前的区块哈希计算的,并且无法提前预测。
let hash_queue = self.blockhash_queue.read().unwrap();
let last_blockhash = hash_queue.last_hash();
let next_durable_nonce = DurableNonce::from_blockhash(&last_blockhash);
这对我们的威胁模型产生了一个重要的影响。 与最近的区块哈希交易不同,持久 nonce 交易可以被保存和重复使用。
让我们考虑一下原始问题的简化形式。
我们可以安全地签署交易吗?
一个观察结果是,这个问题很难用持久 nonce 解决。 通过签署持久 nonce 交易,攻击者可以收集签名并在未来某个不确定的时间点重放它们。
持久 nonce 需要一个链上账户,并且可以使用 getProgramAccounts
调用来验证你的签名者是否具有关联的持久 nonce。
const connection = new Connection(clusterApiUrl('testnet'));
const nonceAccounts = await connection.getProgramAccounts(
// 系统程序拥有所有 nonce 账户。
SYSTEM_PROGRAM_ADDRESS,
{
filters: [\
{\
// Nonce 账户正好是 80 字节长\
dataSize: 80,\
},\
{\
// 授权者的 32 字节公钥被写入\
// 到 nonce 账户数据的第 8-40 个字节中。\
memcmp: {\
bytes: AUTHORITY_PUBLIC_KEY.toBase58(),\
offset: 8,\
},\
},\
],
}
);
不幸的是,这还不够1。 一笔交易可能有多个签名者,攻击者可以使用他们自己的持久 nonce 支付者。 这意味着我们上面定义的问题很遗憾是无法解决的。
let instruction = system_instruction::transfer(&from, &ledger_base_pubkey, 42);
let message =
Message::new_with_nonce(vec![instruction], Some(&evil_nonce_authority), &nonce_account, &evil_nonce_authority)
.serialize();
幸运的是,通过一个小修改,这个问题是可以解决的。 如果允许签名者观察交易的费用支付者呢? 例如,Ledger 在此处记录费用支付者。
bool print_config_show_authority(const PrintConfig* print_config, const Pubkey* authority) {
return print_config->expert_mode || !pubkeys_equal(print_config->signer_pubkey, authority);
}
假设我们已经确定我们的签名者没有关联的 nonce 账户。 如果我们的公钥是新提议交易的费用支付者,我们可以肯定该交易不使用持久 nonce!
如果没有持久 nonce,问题就变得容易解决得多。 等待足够的时间后,所有先前签名的交易都将过期。 如果我们没有看到任何意外的交易,那就意味着我们是安全的。
然后,我们可以使用以下过程。
Solana 的签名模型是独一无二的。 如果协议部署在没有这些独特属性的区块链上,该怎么办? 最重要的约束是可观察性。 必须有一种方法可以看到你正在签署的内容,无论是在签署时还是在事后隐式地看到。
例如,pcaversaccio 编写了一个工具来验证 Safe 交易哈希。 随着这个领域的成熟,我们希望更多的开源工具能够出现。
- 原文链接: osec.io/blog/2025-02-22-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!