本文深入探讨了区块链交易费⽤模型的重要性及其在确保网络安全和有效运行中的关键作用。通过对以太坊和 Solana 区块链网络的交易费⽤模型进行比较分析,揭示了不安全的交易计费可能引发的网络安全风险。

作者:Certik

封面:Photo by Markus Spiske on Unsplash

导读:本文深入探讨了区块链交易费⽤模型的重要性及其在确保网络安全和有效运行中的关键作用。通过对以太坊和 Solana 区块链网络的交易费⽤模型进行比较分析,揭示了不安全的交易计费可能引发的网络安全风险。特别关注了 CertiK 团队发现并协助修复的 Solana 网络中大整数模幂运算的 CU 计算错误,这一错误可能导致潜在的 DOS 攻击。文章详细分析了 Solana 的智能合约计价模型、POH 共识机制、并行事务处理方式,并通过在 Solana 私有集群上的实验,复现了远程 DOS 攻击的过程和成本。

1.  摘要与背景

区块链的计费模式是确保网络安全和有效运行的关键机制,通过收取用户执行操作所需的费用(如 Gas 费用),防止恶意行为和资源滥用,保护用户利益,并推动整个区块链生态系统的发展和创新。一个有效的计费系统不仅是财务基础,也是促进技术进步和社区信任的重要因素。

本文将深入分析以太坊(ETH)和 Solana 区块链网络的交易费用模型,以及不安全的交易计费可能引发的网络安全风险。重点讨论 CertiK 团队发现并修复的 Solana 网络中大整数模幂运算 CU(计算单元)计算错误所引发的潜在远程 DOS 攻击漏洞,并通过此案例探讨区块链计费模型中的安全隐患。

2.  交易费用模型的重要性

在 Web3.0 领域中,底层基础设施运行在去中心化的区块链网络上。这些网络由全球验证者共同维护和运营。用户通过交易和智能合约进行互动,所有的交易都被记录在分布式账本上,并且这些记录是永久不可篡改的。

验证者们凭借有限的资源共同维护着这庞大的区块链网络,而交易费用在确保网络稳定性和安全性方面起到关键作用。这些费用不仅激励着网络参与者,还是推动区块链成功发展的动力。有效的交易费用模型能够确保网络资源的合理分配,防止恶意行为和资源滥用,同时保护用户利益,促进技术的持续创新和社区的健康发展。

交易费用模型的安全性对于区块链的长期健康和稳定至关重要。这种模型不仅仅是对网络资源的有效管理,还直接影响用户的信任和参与度。一个健全的交易费用模型能够有效地防止网络遭受恶意行为,如拒绝服务攻击(DDoS),通过设定适当的费用门槛使攻击者难以滥用网络资源。

合理的费用结构能够激励验证者和矿工投入足够的资源来维护区块链的安全性和稳定性,因为他们通过收取交易费用来获取奖励和补偿。此外,透明和公平的计费机制还可以保护用户免受不当收费和资源耗费,增强他们对区块链生态系统的信任感。因此,一个经过良好设计的交易费用模型不仅是经济上的基础,更是确保区块链网络安全和用户权益的重要保障。

3. ETH 与 Solana 网络的交易费用模式设计

在 BTC 网络中,所有交易的复杂度相对一致,并采用单一的交易计费模型。相比之下,ETH 和 Solana 网络采用图灵完备的脚本语言,其交易计费模型设计更为复杂,涵盖带宽消耗、存储消耗和计算消耗等多个方面。智能合约可以消耗任意数量的带宽、存储和计算资源,而 Gas 费用则是衡量合约执行所需计算工作量的单位。通过限制 Gas 费用的消耗,可以有效控制智能合约对资源的过度利用。

3.1:ETH 网络的交易计费模型设计

在以太坊(ETH)网络中,交易计费模型设计如下:

3.1.1:单位和概念解释

Wei 和 Gwei 单位:Wei 是以太币(ether)的最小单位,1 ether = 1 x 10^18 wei。Gwei 是 Gas 的计量单位,1 gwei = 1 x 10^9 wei。

3.1.2:Gas 费用计算系统

Gas Limit:用户愿意为确认交易或执行操作支付的最大 Gas 量。在伦敦升级 [1]后,Gas Limit 可以根据网络需求在 15M 至 30M 之间动态调整。

Gas Used:实际消耗的 Gas 数量,不超过 Gas Limit。对于未使用的 Gas 部分,将会自动退回到用户的钱包余额。

Gas Price:用户愿意为每单位 Gas 支付的价格。Gas Price 随着以太坊网络上交易拥堵情况而变化,通常是动态调整的,当前查询 [2]Gas Price 约为 10gwei 左右。伦敦升级引入了改进的 EIP-1559[3]  新增两个参数(基础费用 BaseFee 和优先级费用 PriorityFee),改进后的 Gas Price 计算为 BaseFee + PriorityFee。

3.1.3:ETH 网络交易费用计算示例

例如,如果 Gas Price 为 35 gwei/Gas,交易手续费计算如下:交易手续费 = Gas Price (10 gwei) * Gas Limit (21000) / 10^9 = 0.00021 ETH 如果以每 ETH 单价 3500 美元计算,这笔交易手续费大约为 0.735 美元。在网络极度拥堵时,Gas Price 可能会上升至 100 gwei/Gas 以上,导致单笔交易费用超过 10 美元。

3.1.4:ETH 网络的 TPS

在区块链领域中,每秒事务数(TPS)是指网络每秒处理或执行的事务数量。当前查询 [4]显示以太坊网络的 TPS 约为 12.7。这些特性和参数使得以太坊网络能够根据需求动态调整交易费用,并有效管理网络资源,从而支持其广泛的智能合约和分布式应用生态系统的运行和发展。

3.2:Solana 网络的交易计费模型设计

3.2.1:Gas 费计算系统

本地代币 SOL 和 Lamport:Lamports 是 Solana 网络中的本地代币,相当于 SOL 的最小单位。一个 SOL 等于 1,000,000,000 Lamports。MicroLamport 是 Lamports 的最小单位,等于 0.000001 Lamports,用于计算优先级费用。

签名费用:每笔交易必须包含一个签名,每个签名的基本费用固定为 5000 Lamports(0.000005 SOL)。

计算单元(CU):用于衡量 Solana 区块链智能合约执行资源消耗的最小单位。在 Solana 1.9.2 中引入了类似 ETH 的 Gas Limit 的功能,每笔交易默认具有 200,000 CU 预算,并且可以设置最大的 1,400,000 CU。

3.2.2:Solana 经济学设计

基础费用:在 2020 年推出后的前两年,Solana 的交易基础费用固定为 0.000005 SOL 每笔(每笔交易一个签名)。

优先费用:  自 2022 年起,Solana 引入了额外的优先费用机制,允许在交易时支付额外费用以优先处理。计算优先费用的方法是将请求的最大计算单元 (CU) 乘以每个计算单元 0.000001 Lamports 的价格,四舍五入到最接近的 Lamports。

租金费用:每个 Solana 账户在区块链上存储数据的费用称为 “租金”。验证者根据账户在内存中维护的数据收取基于时间和空间的租金费用。账户需要足够的 Lamports 余额以免除租金并保留在 Solana 区块链上。账户若无法支付租金,则可能会通过垃圾收集过程从网络中删除。

例如:如果一个账户持有至少 2 年的租金,则该账户被视为免租。每次账户余额减少时都会检查此项,将余额 减少到最低金额以下的交易将会失败,目前的免租费用为每 MB 6.96 SOL。创建新账户时,费用会分配给该账户;删除账户时,可重新收取免租费用,假如一个大小为 15,000 字节的可执行程序需要 105,290,880 个 lamports (=~ 0.105 SOL) 的余额才能免租。

3.2.3:Solana 交易费用计算示例

交易总费用计算:  假设一笔交易请求 1,000,000 CU,并设置每个 CU 的优先费用为 10,000 MicroLamports。则交易总费用为 5000 Lamports(签名费用)+ 1,000,000 CU * 10,000 MicroLamports * 0.000001 Lamports = 0.000015 SOL。当前 Solana 平均费用查询 [5]显示,平均费用约为 0.000021 SOL,额外费用约为 0.000025 SOL。以每 SOL 100 美元的价格计算,单笔交易费用大约为 0.0021 美元。

计算单元时间限制:  计算单元 CU 也可以取决于在 Solana SVM 中执行时间的定价,每 33 ns 的计算时间等价于 1 CU。假设默认上限为 200,000 CU,那么在 Solana SVM 中执行 200,000 CU 的智能合约时间约为 6.6 毫秒。最大 1,400,000 CU 的执行时间约为 46.2 毫秒。

交易费用分配: Solana 网络将所有交易费用的 50% 烧毁,其余 50% 分配给处理交易的验证者。

3.2.4:Solana 网络的 TPS

根据 Solana 的白皮书,理论上 Solana 每秒可以处理多达 710,000 笔交易。在实际场景中,Solana 已经表现出超过 5,000 TPS 的能力,并且在测试期间达到了 65,000 TPS。当前查询 [6]显示 Solana 的 TPS 约为 3300。

这些设计和参数使得 Solana 网络能够高效处理大量交易,并提供灵活的费用和资源管理机制,以支持其快速发展和广泛应用的智能合约和分布式应用生态系统。

4. ETH 和 Solana 网络交易费用对比

4.1:Gas 费用对比:

根据平均费用查询,Solana 的单笔交易费用为 0.0021 美元,远低于 ETH 的 0.7 美元交易费。ETH 的交易费用随着网络拥堵而上升,而 Solana 相对于 ETH 网络,其单笔交易费用非常低,大约 500 笔交易的总费用才相当于 1 美元。

4.2:TPS 对比:

目前,Solana 的 TPS 约为 ETH 网络的 250 倍至 500 倍(ETH 的 tps 为 3000 至 5000 的 12 分之一)。

4.3:总结:

总体而言,Solana 相对于 ETH 网络在极低的 Gas 费用下能够处理大量交易。然而,Solana 网络面临垃圾交易的风险高于 ETH 网络,这对其稳定性和冗余性构成了相当大的考验。

5. ETH 和 Solana 网络交易计费设计缺陷带来的风险

前面了解并对比了 ETH 网络和 Solana 网络计费系统设计的细节和差异性,计费系统设计有缺陷会带来严重的网络安全风险,拒绝服务攻击是图灵完备的区块链网络备受困扰的网络安全风险之一,后续将会详细讨论 ETH 网络和 Solana 网络面临计费系统缺陷带来的网络安全风险。

5.1:ETH 网络历史上的计费系统风险

ETH 网络在历史上遭遇了两次拒绝服务攻击,并导致网络性能下降或者崩溃,解决方案分别是 2016 年 10 月 18 日的橘子哨(Tangerine Whistle)分叉 [7]和 2016 年 11 月 22 日的伪龙(Spurious Dragon)分叉 [8]

5.1.1:EXTCODESIZE 操作码攻击的原因: 

造成这次拒绝服务攻击的罪魁祸首是 EXTCODESIZE 操作码,由于 EXTCODESIZE 操作码的 Gas Price 相当低,并且需要节点从磁盘读取状态信息,只要这些操作的 Gas 加起来不超过区块的 Gas Limit,每个区块的攻击交易可调用此操作码大约 50,000 次。这一个区块内的交易所占用的计算时间就被大大延长,从而导致了整个以太坊网络的瘫痪。

修复方案 EIP-150[9]:通过增加 EXTCODESIZE 操作码的 Gas 成本,从 20 增加到 700,以此防止类似的攻击再次发生。

5.1.2:SELFDESTRUCT 操作码攻击的原因:

攻击者利用 SELFDESTRUCT 操作码生成了大量空账户。每次 SELFDESTRUCT 操作会花费 90 Gas 生成一个空账户,这些账户需要存储在以太坊的状态树中。攻击者总共创建了约 1900 万个空账户,导致状态树的存储空间大幅膨胀,超过了实际创建账户所需的存储空间,最终导致节点存储压力爆炸。

修复方案 EIP-161[10]:EIP-161 通过清除空账户和优化状态树的存储机制,减少了存储空间的浪费。现在创建新账户时需要额外支付 25,000 Gas,空账户功能上等同于不存在的账户,可以通过交易与空账户交互来删除它们。

5.1.3:总结:

ETH 网络这两次拒绝服务攻击都利用了某些操作码的低 Gas 成本,从而放大了攻击的效果。攻击者可以用很少的资源对网络造成极大的负担,暴露了区块链网络在低 Gas 机制下的潜在脆弱性。

5.2:Solana 网络面临过的计费系统风险

ETH 网络在 2016 年经历了重大的拒绝服务攻击,成为当时的重要挑战之一。而彼时 Solana 还处于创始阶段,被视为潜在的 “以太坊杀手”。Solana 在 2022 年经历了严峻的网络中断考验(总计中断 5 次),但在 2023 年仅有一次中断。作为超高性能的区块链网络,Solana 引入了 8 项关键创新 [11],虽然带来了技术进步,也引发了许多未知的风险。

在数次中断事件中,2022 年 1 月和 2022 年 4 月,Solana 网络分别因大量垃圾交易遭遇拒绝服务攻击。

5.2.1: 2022 年 1 月 – 严重的网络拥塞 [12]

由于重复交易过多导致缓存耗尽,网络性能严重下降,部分节点的处理能力受限,持续了较长时间。

5.2.2:2022 年 4 月和 5 月 – 短暂的中断 [13]

2022 年 4 月,Solana 网络中断了 2 小时 42 分钟,主要原因是 NFT 机器人大量发送垃圾邮件,导致网络过载。同年 5 月,Solana 再度因垃圾邮件攻击中断了 5 小时 31 分钟,每秒 600 万次的垃圾邮件交易使验证器内存不足,网络流量超出 100 Gbps,最终导致主网 Beta 集群的共识停滞。投票不足以清理旧区块,进一步加剧了内存使用问题,导致验证器崩溃。

与 2016 年 ETH 网络拒绝服务攻击类似,Solana 在几次中断后也通过提高 Gas 费用来缓解攻击问题。特别是引入了优先级费用,允许用户为交易支付额外费用,以确保其交易优先处理。这种机制有效增加了攻击成本,降低了垃圾邮件攻击的可行性。

5.2.3:总结

无论是 ETH 还是 Solana 网络,正确设计计费系统对于防范拒绝服务攻击至关重要。攻击者往往会利用 Gas 费用机制中的漏洞发起攻击。虽然 Solana 引入了优先级费用来提高攻击成本,但其交易成本仍然非常低,这可能意味着未来仍存在未知的攻击面。

6. Solana 智能合约的计价模型

6.1:Solana 合约精确计算模型

Solana 虚拟机(SVM)在 Solana 区块链上运行合约至关重要,因其高效的计算单元(CU)模型和快速执行速度。SVM 通过精确的计费结构和优化的执行引擎,支持高吞吐量和低延迟的应用程序需求,同时保障了合约的安全性和稳定性。

Solana 的合约计费模式采用精确计算单元(CU)模型。每 33 纳秒的计算时间对应消耗 1 CU,CU 是衡量 Solana 虚拟机中资源消耗的标准单位。合约的执行成本是基于 SVM 中运行的字节码所消耗的 CU 来计算的。

SVM 的 CU 计算模型对于 Solana 生态系统至关重要。根据 CertiK 团队于 5 月 24 日发布的一篇关于 SVM 的 CU 计算漏洞的研究文章,SVM 的 CU 计算指令在某些情况下可能会出现错误翻译,导致智能合约在 SVM 中出现无限计算消耗,严重时可能会导致整个 Solana 区块链网络的崩溃。

6.2:Solana 引入 syscall 功能采用预估计算模型

随着 Solana 的发展,可能会引入新的功能或补丁来改变集群的行为和程序的运行方式。这些更改可以通过增加或减少 syscall 功能来实现。

Solana 支持一种称为运行时功能的机制,类似于热补丁,用于在集群发生行为更改时调整程序的行为。这些更改通过 syscall 功能门实现,默认情况下这些功能是禁用的,除非开启了相应的 syscall 功能。

syscall 的计费通过为每个特定的系统调用功能分配固定的 CU 值来实现,这些值被定义在计算预算中。

在 Solana 的 CU 计费研究中,syscall 功能的执行并不完全依赖于 SVM 标准模型,而是采用预估计算模型,某些情况下可能涉及第三方库的支持。syscall 的这一特性使其在功能引入时更灵活,但同时也带来了一定的复杂性。

7. syscall 功能预估模型的 CU 计算错误

Solana 的 Gas 成本限制通过 CU 资源限制来实现的,CU 计算的错误会导致合约在 Solana 链上执行消耗的资源 (cpu、time) 严重超出了限制,进一步导致潜在的 Solana 区块链上远程 DOS 攻击。

在对 Solana 的 syscall 功能研究中,结合 ETH 网络和 Solana 历史漏洞同时,CertiK 团队针对 syscall 功能的 Gas 成本做了深入研究,研究发现 big_mod_exp syscall 功能在 CU 成本计算存在漏洞,bits 和 bytes 的混用,导致 CU 计算出问题,严重偏移了资源限制消耗,最终会导致 Solana 区块链远程 DOS 攻击。

7.1:syscall 功能 big_mod_exp 模块的作用与其 CU 计算模型

7.1.1:引入的 pull 提案

Solana 的 syscall 中引入了大整数模幂运算功能,详细见提案:add big_mod_exp syscall #28503[14]。该功能类似于 EIP-198[15]中的实现。big_mod_exp 的 CU 计算模型如下:

[1u8; len] for the base and exponent Use a prime or an RSA modulus for each bit-sizes.

7.1.2:CU 计算与消耗时间预期测试结果

由于该系统调用适用于使用 RSA 算法的程序,输入数据最大支持 2048 位(支持的位数包括 32、64、128、256、512、1024、2048)。CU 的计算公式如下:

计算时间(ns)= bits^2CU = 计算时间 / 33 ns

以 2048 位为例,预期输入的执行时间为 4,194,304 纳秒,其 CU 计算结果为:

CU = 4,194,304 / 33 ≈ 127,100

注:Solana 这里最高引入了 4096 位的计算,对于 4096 位的输入,计算时间为 52 毫秒,对应的 CU 为:

CU = 52,000,000 / 33 ≈ 1,575,757

由此可以看出,big_mod_exp 的 CU 消耗基于输入的位长度进行计算,位数越高,CU 的消耗也越大。

7.2:syscall 功能 big_mod_exp 代码深入分析

Solana 引入 #28503 时,big_mod_exp 功能 4096 bits 的 CU 预算是 1,575,757,而测试发现 CU 预算只有 8043,big_mod_exp 功能的 CU 计算出现了问题。big_mod_exp 的关键代码位于 SyscallBigModExp 中,三个主要输入参数为 base、exponent 和 modulus,分别对应的长度为 params.base_lenparams.exponent_lenparams.modulus_len。需要注意的是,这些参数的单位是 bytes:

let params = &translate_slice::<BigModExpParams>(            memory_mapping,            params,            1,            invoke_context.get_check_aligned(),            invoke_context.get_check_size(),        )?        .get(0)        .ok_or(SyscallError::InvalidLength)?;        if params.base_len > 512 || params.exponent_len > 512 ||params.modulus_len > 512 {            return Err(Box::new(SyscallError::InvalidLength));        }let input_len: u64 = std::cmp::max(params.base_len, params.exponent_len);let input_len: u64 = std::cmp::max(input_len, params.modulus_len);

big_mod_exp 功能关键 CU 计算:

let budget = invoke_context.get_compute_budget();        consume_compute_meter(            invoke_context,            budget.syscall_base_cost.saturating_add(                input_len                    .saturating_mul(input_len)                    .checked_div(budget.big_modular_exponentiation_cost)                    .unwrap_or(u64::MAX),            ),        )?;

从代码中可以推导出 CU 的计算公式为:

CU = bytes^2 / big_modular_exponentiation_cost (33)

对比 Solana 在引入 #28503[16]时的原始计算公式:

CU = bits^2 / 33

这揭示了漏洞的核心问题:输入的单位为 bytes,但应转换为 bits。正确的 CU 计算方式应为:

CU = (bytes * 8) ^2 / big_modular_exponentiation_cost

以 4096 位输入为例,正确的 CU 计算为:

CU = 4096 ^ 2 / 33 ~= 508,400

在当前存在漏洞的情况下,CU 预算为 200,000 时,调用 4096 位的 big_mod_exp 功能合约的执行时间为:

200_000 / 8043 ≈ 2424 * 37 ms ≈ 890 ms

而在最大 CU 上限为 1,400,000 的情况下,单个合约的执行时间约为 6.23 秒。Solana 的平均出块时间约为 400ms~600ms,具体出块时间可在 Solana Explorer[17]中查看,目前约为 400ms。考虑到 Solana 通过 Proof of History(PoH)实现的共识机制及其并行事务处理能力,单个最高 6.23s 的合约能造成 Solana 集群的远程 DOS 攻击吗?或者并行执行多个合约呢? 后续将会对 Solana 共识机制和事务处理展开讨论,结合有问题的 big_mod_exp 合约实现对本地 Solana 集群的远程 DOS 攻击。

8.  深入分析 Solana 区块链与智能合约交互

8.1:POH 共识

Slot 和 Epoch 是 Solana 网络中的时间单位。Slot 是最小的单位,432,000 个 Slot 组成一个 Epoch。PoH(Proof of History)通过可验证延迟函数(VDF)生成一系列不可预测的时间序列,用于给每个 Slot 中生成的区块打上时间戳。

Solana 每笔交易都有时间作为交易生命周期 [18]:每笔交易都包含一个 “最近的区块哈希”,用作 PoH 时钟时间戳,并在该区块哈希不再足够 “最新” 时过期,在 150 个 Slot 周期内。

Solana 验证器会查找他们希望在区块中处理的每笔交易的区块链相应时隙编号。如果验证器找不到区块哈希的插槽号,或者查找到的插槽号比正在处理的区块的插槽号高 150 个以上,交易就会被拒绝:

Solana 区块链的工作原理涉及以下 6 个基本步骤:

(1)领导者选举与时隙分配

Solana 网络通过基于 PoS 的选举机制轮流选出一个领导者(Leader)。每个领导者被分配四个连续的时间槽(Slot)来处理数据,总共持续约 1.6 秒(4 个区块,每个区块 400 毫秒)。领导者的选举是随机的,概率与验证者的权益权重成正比,选举周期约为 2-3 天(即 432,000 个时隙)。

(2)领导者消息排序与数据流最大化 

领导者使用 Proof of History(PoH)生成一条可验证的时间序列,确保了全网的读一致性和时间的可验证性。领导者对用户提交的交易进行排序,确保验证者能够以一致的顺序处理交易,从而最大化数据流的效率并保持网络高效运行。

(3)领导者执行交易 

领导者将用户提交的交易存储在 RAM 中,并在当前状态上执行这些交易,处理诸如代币转移、智能合约执行等操作。交易数据在 RAM 中的临时存储提高了处理速度和吞吐量。

(4)领导者发布交易结果 

领导者在执行完交易并更新状态后,对交易集的哈希和最终状态进行签名,然后将这些信息发布给验证者(也称为复制节点),确保交易结果的不可篡改性和可验证性。

(5)验证者验证交易与最终状态 

验证者接收到领导者发布的交易和状态签名后,在其本地状态副本上重复执行相同的交易,以确认最终状态的正确性。验证者通过应用分叉选择规则(Fork Choice Rule)评估领导者提出的区块,并确保其与网络的整体一致性。

(6)共识确认 

验证者通过 Gossip 网络发布其状态签名,作为共识算法的一部分,确保网络达成一致。这些签名作为投票,用于确认区块的有效性,最终形成全网共识。

8.2:并行事务处理

Solana 验证器中通过多线程来执行多队列的,单个线程执行单笔队列,其中参数 NUM_THREADS 和 MIN_TOTAL_THREADS 用于控制线程数量:

 pub const NUM_THREADS: u32 = 6;    const NUM_VOTE_PROCESSING_THREADS: u32 = 2;    const MIN_THREADS_BANKING: u32 = 1;    const MIN_TOTAL_THREADS: u32 = NUM_VOTE_PROCESSING_THREADS +MIN_THREADS_BANKING;    ...    pub fn num_threads() -> u32 {            cmp::max(                env::var("Solana_BANKING_THREADS")                    .map(|x| x.parse().unwrap_or(NUM_THREADS))                    .unwrap_or(NUM_THREADS),                MIN_TOTAL_THREADS,            )         }

8.3:深入事务处理代码

Solana 事务处理是在一个多线程中进行的,通过调用 process_loop[19]函数来循环处理交易事务。

 Builder::new()            .name(format!("solBanknStgTx{id:02}"))            .spawn(move || {                Self::process_loop(                    &mut packet_receiver,                    &decision_maker,                    &mut forwarder,                    &consumer,                    id,                    unprocessed_transaction_storage,                )            })            .unwrap()

在 process_loop 里面 [20]通过 unprocessed_transaction_storage: UnprocessedTransactionStorage 循环获取待处理的交易 hash,函数 process_buffered_packets 将带出来的交易 hash 传递到下层函数,最终到 Solana 的 SVM 虚拟机进行处理,receive_and_buffer_packets 则循环接收待处理交易 hash。

 loop {            if !unprocessed_transaction_storage.is_empty()                || last_metrics_update.elapsed() >= SLOT_BOUNDARY_CHECK_PERIOD            {                let (_, process_buffered_packets_time) = measure!(                    Self::process_buffered_packets(                        decision_maker,                        forwarder,                        consumer,                        &mut unprocessed_transaction_storage,                        &banking_stage_stats,                        &mut slot_metrics_tracker,                        &mut tracer_packet_stats,                    ),                    "process_buffered_packets",                );                slot_metrics_tracker                    .increment_process_buffered_packets_us(process_buffered_packets_time.as_us());                last_metrics_update = Instant::now();            }
           tracer_packet_stats.report(1000);
           match packet_receiver.receive_and_buffer_packets(                &mut unprocessed_transaction_storage,                &mut banking_stage_stats,                &mut tracer_packet_stats,                &mut slot_metrics_tracker,            ) {                Ok(()) | Err(RecvTimeoutError::Timeout) => (),                Err(RecvTimeoutError::Disconnected) => break,            }            banking_stage_stats.report(1000);        }

Solana 处理交易的关键在 execute_and_commit_transactions_locked[21],通过在 SVM 虚拟机执行指令,并检查错误后,将完整的交易 commit 提交,返回被正确记录的交易到上层 process,调用了 record_transactions 记录交易,committer.commit_transactions 提交完整的事务:

let (load_and_execute_transactions_output, load_execute_us) = measure_us!(bank            .load_and_execute_transactions(                batch,                MAX_PROCESSING_AGE,                &mut execute_and_commit_timings.execute_timings,                TransactionProcessingConfig {                    account_overrides: None,                    log_messages_bytes_limit: self.log_messages_bytes_limit,                    limit_to_load_programs: true,                    recording_config: ExecutionRecordingConfig::new_single_setting(                        transaction_status_sender_enabled                    ),                }            ));        execute_and_commit_timings.load_execute_us = load_execute_us;        .......        .......        let (record_transactions_summary, record_us) = measure_us!(self            .transaction_recorder            .record_transactions(bank.slot(), executed_transactions));        execute_and_commit_timings.record_us = record_us;        .......        ......        let (commit_time_us, commit_transaction_statuses) = if executed_transactions_count != 0 {            self.committer.commit_transactions(                batch,                &mut loaded_transactions,                execution_results,                last_blockhash,                lamports_per_signature,                starting_transaction_index,                bank,                &mut pre_balance_info,                &mut execute_and_commit_timings,                signature_count,                executed_transactions_count,                executed_non_vote_transactions_count,                executed_with_successful_result_count,            )        } else {            (                0,
               vec![CommitTransactionDetails::NotCommitted; execution_results.len()],            )        };

正常合约完整执行过程为(SVM 虚拟机执行-> 记录交易-> 提交交易):

8.4:存在 CU 漏洞的合约会跨 Slot 导致事务处理混乱

分析完 Solana 的 POH 共识机制和并行事务处理方式,这里引入带有 CU 计算漏洞的 big_mod_exp 合约,结合漏洞探究其对 Solana 链上执行合约造成的影响和原理。当执行一次带有 4096 位 big_mod_exp 功能的合约,在默认 200,000CU 上限中,执行时间 890ms 的可以跨越 1-2 个 Slot,当跨越 Slot 将会导致 record[22]失败,是因为交易后传入的原始 Slot 比当前 slot 慢 1-2 个 Slot,返回 PohRecorderError::MaxHeightReached 错误:

if bank_slot != working_bank.bank.slot() {                return Err(PohRecorderError::MaxHeightReached);            }

上文我们分析了 Solana 的交易是存在生命周期 [23]的,而其生命周期的控制是通过参数 MAX_PROCESSING_AGE,MAX_PROCESSING_AGE 的值为 150 也就是对比是否小于 150 Slot,带有 big_mod_exp 漏洞的合约因为跨了 Slot 导致事务失败,失败后 Solana 会比较当前 Slot 和 MAX_PROCESSING_AGE,只要小于 150 Slot,Solana 会设置 retryable_transaction_indexes 为 0,并且返 回到 process_packets 填充 retryable_packets,重新 retry 当前交易:

fn process_packets<F>(        &mut self,        bank: &Bank,        banking_stage_stats: &BankingStageStats,        slot_metrics_tracker: &mut LeaderSlotMetricsTracker,        mut processing_function: F,    ) -> bool    where        F: FnMut(            &Vec<Arc<ImmutableDeserializedPacket>>,            &mut ConsumeScannerPayload,        ) -> Option<Vec<usize>>,    {        let mut retryable_packets = self.take_priority_queue();        let original_capacity = retryable_packets.capacity();        let mut new_retryable_packets = MinMaxHeap::with_capacity(original_capacity);        let all_packets_to_process = retryable_packets.drain_desc().collect_vec();        ..............                ..........                while let Some((packets_to_process, payload)) = scanner.iterate() {            let packets_to_process = packets_to_process                .iter()                .map(|p| (*p).clone())                .collect_vec();            let retryable_packets = if let Some(retryable_transaction_indexes) =                processing_function(&packets_to_process, payload)            {                Self::collect_retained_packets(                    payload.message_hash_to_transaction,                    &packets_to_process,                    &retryable_transaction_indexes,                )            } else {                packets_to_process            };
           new_retryable_packets.extend(retryable_packets);        }        .......      }

所以最后带有 CU 计算漏洞的 bid_mod_exp 合约将会重复执行 150 次后,最后导致交易过期失败,而 MAX_PROCESSING_AGE 的检查在 chek_transaction_age[24]调用 4096 位的 big_mod_exp 功能合约:200_000 / 8043 ~= 24, time : 24 * 37 ms ~= 890 ms,而 retry 150 次后,整个交易将会在 133,500ms,约 130s 后结束,长期占用资源将会导致队列堵塞。将会是以下过程:

9.  在搭建的 Solana 私有集群上复现远程 DOS 攻击

本地测试准备了 10-20 个独立账户,每个独立账户调用一次带有 4096 位的 big_mod_exp 功能的合约,都是默认 200,000CU 上限,同时运行多个独立账户的正常合约进行对比:

私有集群测试

私有集群总计 4 个节点,包括 Leader Node、User RPC Node、Attacker RPC Node、Node4 , User 界面通过 10 个独立账户调用正常合约执行,正常合约调用从 rpc 客户端请求到返回结束平均花费 740ms 左右,Attacker 是模拟攻击者的界面,通过 20 个独立账户,每个账户调用 20 次带有 4096 位的 big_mod_exp 功能合约,以下截图是模拟攻击攻击发送了 20 次恶意合约的场景,可以观察到 Leader Node 的 cpu 已经是满负荷运行, 并且 User 界面已没有正常合约调用的 rpc 返回:

测试结果显示私有集群和本地集群在一次性处理多个 (20) 独立账户带有 4096 位的 big_mod_exp 功能的合约时,总计调用 20 次,出现了长时间高达 (133s) 的堵塞,反复攻击将会造成严重的 DOS 攻击!

10.  远程 DOS 攻击成本

一次攻击成本预估:带有 4096 位的 big_mod_exp 功能的合约在本地集群运行时,由于交易过期被 drop 掉,Gas 费后续都为 0,Solana 的 CU 计算成本类似 ETH 的 Gas 计算,通过限制合约消耗的网络资源来防止远程 DOS 攻击,Solana 引入的预估费用计算模型存在的缺陷带来了 CU 计算漏洞,导致了严重的资源消耗偏移,最终导致潜在的远程 DOS 攻击。幸运的是 Solana 在引入 syscall 新功能前会经过大量测试和验证并且包括了和外部安全研究员漏洞赏金合作的模式,确保了上线后的稳定性,这次提前发现了 Solana 的 syscall 的 big_mod_exp 功能存在严重的漏洞,维护了 Solana 网络的稳定性和安全性!

11.  漏洞确认与修复

Solana 确认了 CertiK 团队提交的大整数模幂运算 (big_mod_exp) 中 CU 计算错误将会导致潜在的远程 DOS 攻击,并分类为 DOS 攻击漏洞,Solana 开发者修复方案 [25]重新计算了 big_mod_exp 的 CU 成本,通过重新基准测试模幂运算的性能,调整计算单位(CU)为 N^2/2 + 190, 虽然修复不是把 bytes 置换成 bits,但是重新计算了大整数模幂运算的 CU 成本最终修复了安全漏洞,根据安全公告未来可能还会优化算法以提高性能。

参考:

[1]  伦敦升级: https://ethereum.org/zh/history/#london

[2]  当前查询: https://cn.etherscan.com/Gastracker

[3] EIP-1559: https://eips.ethereum.org/EIPS/eip-1559

[4]  当前查询: https://etherscan.io/

[5]  查询: https://beta-analysis.solscan.io/public/dashboard/06d689e1-dcd7-4175-a16a-efc074ad5ce2

[6]  查询: https://solscan.io/analytics

[7]  橘子哨(Tangerine Whistle)分叉: https://ethereum.org/zh/history/#tangerine-whistle

[8]  伪龙(Spurious Dragon)分叉: https://ethereum.org/zh/history/#spurious-dragon

[9] EIP-150: https://eips.ethereum.org/EIPS/eip-150

[10] EIP-161: https://eips.ethereum.org/EIPS/eip-161

[11] 8 项关键创新: https://medium.com/Solana-labs/proof-of-history-a-clock-for-blockchain-cf47a61a9274

[12] 2022 年 1 月 – 严重的网络拥塞: https://twitter.com/SolanaStatus/status/1484947431796219906?s=20&t=x6Itu5Yn_8-HtapAyLBrfA

[13] 2022 年 4 月和 5 月 – 短暂的中断: https://solana.com/news/04-30-22-Solana-mainnet-beta-outage-report-mitigation

[14] add big_mod_exp syscall #28503: https://github.com/Solana-labs/Solana/pull/28503

[15] EIP-198: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-198.md

[16] #28503: https://github.com/Solana-labs/Solana/pull/28503

[17] Solana Explorer: https://explorer.solana.com/

[18]  交易生命周期: https://docs.solana.com/developing/transaction_confirmation

[19] process_loop: https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/core/src/banking_stage.rs#L644C8-L656C22

[20] process_loop 里面: https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/core/src/banking_stage.rs#L742

[21] execute_and_commit_transactions_locked: https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/core/src/banking_stage/consumer.rs#L569

[22] record: https://github.com/anza-xyz/agave/blob/master/poh/src/poh_recorder.rs#L942

[23]  生命周期: https://solana.com/docs/advanced/confirmation

[24] chek_transaction_age: https://github.com/anza-xyz/agave/blob/5263c9d61f3af060ac995956120bef11c1bbf182/runtime/src/bank.rs#L3513

[25]  修复方案: https://github.com/anza-xyz/agave/commit/eb37b21d4d5ed29d1bf40c9ca7c64509681a2a09

免责声明:作为区块链信息平台,本站所发布文章仅代表作者及嘉宾个人观点,与 Web3Caff 立场无关。文章内的信息仅供参考,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。