测试如何提升安全性
测试是区块链开发流程中的关键环节:它能展示单个功能和用户流程是否正确实现,验证访问控制的健壮性,检验合约在对抗性场景下的表现,并防止合约变更导致功能回退。
以下是区块链项目推荐的三种测试方法:
单元测试:这是项目最基础的测试设置,测试代码的最小功能单元。单元测试套件包含对单个函数行为的测试用例,检查特定输入值或可能触发边界情况的值。功能完善且健壮的单元测试套件能使代码重构更轻松,并为集成测试奠定坚实基础。
集成测试:集成测试套件包含函数与合约间交互的测试用例,以及用户交互、管理操作和其他类型操作流程的端到端测试。这些用例模拟合约部署后的实际行为,有助于检测与数据验证、访问控制和合约交互相关的问题。
模糊测试:这些测试生成与合约或函数交互的随机序列,每次调用使用随机数据,并在交易执行后评估最终系统状态。最终状态必须符合测试套件中定义的特定不变条件,测试才算成功。模糊测试适用于单个函数或操作流程的端到端测试,能检测数学函数中的定义域和值域错误、数据编码解码错误以及数据持久化不正确等问题。
如何衡量测试套件有效性
如果你在2025年开发区块链协议,最低测试水平应包含所有三种方法。但仅仅使用这三种方法,并不代表你能以真正捕获漏洞的有效方式运用它们。
测试套件有效性最常用的指标是"覆盖率"。覆盖率衡量测试套件"触及"了多少代码。常识表明,一个好的测试套件应该覆盖100%的代码——即测试覆盖100%的所有行/分支。
通常,实现100%代码覆盖率既困难又耗费资源。大多数软件工程项目认为80%的覆盖率就"足够好",但考虑到区块链的固有风险和财务激励,这对合约来说绝对不够好。
即便如此,假设你的测试套件覆盖了所有代码,你就能高枕无忧地认为系统安全吗?你可能已经知道答案——“不能”。使用覆盖率评估测试套件的最大缺点之一是,100%的覆盖率并不意味着所有合法和恶意的用例都得到了测试。
让我们通过一个简单的示例来说明覆盖率指标如何具有欺骗性。下面有一个verifyMinimumDeposit()函数,如果存款金额至少为1 ether则返回true,否则返回false:
|
|
开发者为该函数创建了两个单元测试来测试true和false返回值:
|
|
verifyMinimumDeposit()函数的测试覆盖率为100%,因为所有行和分支都被覆盖。开发者对这个指标很满意,认为工作完成。然而,测试存在缺陷:没有检查边界值的测试用例。例如,如果代码重构错误地将条件改为deposit >= 2 ether,测试仍将通过,但基本协议功能将被破坏。测试套件未能检测到不正确的值,根据其他因素,新代码甚至可能构成安全风险。
由此可见,覆盖率不是评估测试套件有效性的最佳指标。更好的方法是使用变异测试,这是一种发现与实际行或分支覆盖率无关的测试套件覆盖空白的技术。
变异测试
高层面上,变异测试活动对代码库进行微小的系统性更改,并对修改后的代码运行现有测试套件。每个修改后的代码库版本称为"变异体"。
对变异体运行测试套件后,可能出现两种结果:如果测试套件失败,变异体被"捕获"或"杀死",意味着测试套件对该特定更改有检查。但如果测试套件正常完成,变异体未被捕获(“存活”),揭示了测试套件中的覆盖空白。
变异测试活动的目标是生成尽可能多的变异体,并验证测试套件能否捕获所有变异体。评估测试套件有效性的有用指标是捕获变异体占所有生成变异体的百分比。理想情况下,这个值应为100%,意味着测试套件能杀死所有生成的变异体。
以下是对代码库可执行的一些常见变异:
- 替换一元或二元运算符;例如,将加法替换为减法
- 替换赋值运算符;例如,将+=替换为=
- 替换常量字面值;例如,将任何非零常量替换为0
- 取反或替换if语句或循环中的条件
- 注释掉整行代码
- 用revert指令替换行
- 替换数据类型;例如,将int128替换为int64
变异测试的最大缺点是活动可能耗时很长:每个新生成的变异体都必须运行完整的编译和测试过程。减少执行时间的一种策略是将变异分为优先级组,如果高优先级变异体存活,则跳过低优先级变异体。例如,如果注释掉的一行代码未被捕获,更改该行中的加法运算符很可能也会导致变异体存活。
活动运行后,必须分析结果。存活的变异体表明测试覆盖空白,可能意味着隐藏的安全风险。发现根本原因对于确定问题的影响和推荐解决方案很重要。
自动化变异测试
自0.10.2版本起,Slither通过slither-mutate原生支持Solidity代码库的变异测试,这是一个自动化生成变异体、评估变异体并生成存活变异报告的命令行工具。
要启动自己的变异测试活动,只需下载最新版本的Slither并执行以下命令:
|
|
此命令专门用于使用Foundry框架进行测试的代码库。如果未使用Foundry,请将–test-cmd内容替换为运行测试套件所需的指令。
还有其他几个命令行选项可用。要了解这些选项,请运行:
|
|
活动完成后,你将获得包含所有未捕获变异体和活动相关指标的报告。这些变异体的副本将保存在输出目录中,默认为./mutation_campaign。
输出将以下列格式呈现:
|
|
这显示了合约ContractName在FileLine行的未捕获变异体示例。如果将原始行替换为变异行,测试套件执行时不会检测到任何测试失败。有多个变异器可用,每个都有唯一别名。例如,如果变异体被"注释替换"变异器捕获,Mutator将为"CR",该变异器会注释掉整行。slither-mutate –list-mutators显示可用变异器及其别名的完整列表。
如前所述,执行变异测试活动可能需要数小时或数天,具体取决于代码库大小、选择变异的合约数量、启用的变异器以及测试套件运行时间。
案例研究
为了展示变异测试的有效性,让我们看看Trail of Bits对Arkis协议的审计。在审计期间,我们的工程师对范围内的文件运行了变异测试活动,发现了几个未捕获的变异体,这导致了TOB-ARK-10的发现,这是一个高危问题,可能允许攻击者耗尽协议资金。
该问题源于对用户提供参数缺乏验证。函数没有验证转移的代币数量,而是盲目信任_cmd参数,该参数可被攻击者操纵。
报告附录C中的图C.2显示了slither-mutate的部分输出:
|
|
这些结果表明受影响文件的测试套件覆盖不足:注释掉第33行对测试没有影响。分析根本原因后,我们的工程师发现并报告了该问题。
此类问题通常由对结果状态缺少检查、使用不反映真实情况的模拟对象,或 simply 对给定功能缺乏测试用例引起。提高测试套件质量不仅关乎实现更高覆盖率,还关乎使测试用例健壮且有意义。
在项目中使用变异测试
如果你是区块链开发者,请运行变异测试活动并改进测试套件以杀死所有变异体。作为回报,你将拥有一个全面的测试套件,帮助你在开发过程早期发现问题,并帮助安全工程师更有效地审计你的代码库。如果你是审计员,请将变异测试加入工具箱,找出存活变异体的根本原因;通常情况下,它们能揭示代码库中隐藏的错误。
你的测试套件是否足够强大,能杀死所有变异体?我们在此帮助保护你的项目安全。联系我们,我们很乐意交流。