这篇文章详细介绍了使用 Trident 进行模糊测试的过程,包括设置 Trident 工作空间、编写模糊测试、以及运行测试与获取结果等步骤。文章涉及了 Rust 代码示例来演示如何初始化程序、处理账户和执行模糊测试,同时指出了潜在的函数缺陷和运行中的错误处理。
[!IMPORTANT] 有关 Trident 的更多详细信息,请查看 Trident 文档 Trident 文档
本节中指定的程序具有以下结构
程序在其初始化指令中创建一个资产账户,该账户具有以下字段
authority
:负责资产的创建和对资产的管理权限。mint
:与资产账户相关联,也在初始化指令中初始化。counter
:在初始化过程中计算得出。##[account]
pub struct Asset {
pub authority: Pubkey,
pub mint: Pubkey,
pub counter: u64,
}
impl Asset {
pub const LEN: usize = 32 + 32 + 8;
}
初始化指令期望以下账户
signer
:新的资产管理权限者。asset
:将要初始化的新资产账户。mint
:与资产相关联的mint账户,也将被初始化。metadata_account
:存储与mint账户相关的附加数据,将通过CPI初始化到Metaplex Metadata程序。[!TIP] 有关 Metaplex Metadata 程序的更多详细信息,请查看 Token Metadata 文档 Token Metadata
##[derive(Accounts)]
pub struct InitializeContext<'info> {
// 1.
#[account(mut)]
pub signer: Signer<'info>,
// 2.
#[account(
init,
payer = signer,
space = 8 + Asset::LEN,
seeds = [b"asset",signer.key().as_ref(),mint.key().as_ref()],
bump
)]
pub asset: Account<'info, Asset>,
// 3.
#[account(
init,
payer = signer,
mint::decimals = 9,
mint::authority = signer,
)]
pub mint: Account<'info, Mint>,
// 4.
/// CHECK: 将被初始化
#[account(mut)]
pub metadata_account: UncheckedAccount<'info>,
// 5.
pub mpl_token_metadata: Program<'info, Metadata>,
pub system_program: Program<'info, System>,
pub token_program: Interface<'info, TokenInterface>,
}
buggy_math_function
,它的结果将被分配到 counter 字段。pub fn _initialize_ix(
ctx: Context<InitializeContext>,
input1: u8,
input2: u8,
name: String,
symbol: String,
uri: String,
) -> Result<()> {
// 1.
ctx.accounts.create_metadata(name, symbol, uri)?;
let asset = &mut ctx.accounts.asset;
asset.authority = ctx.accounts.signer.key();
asset.mint = ctx.accounts.mint.key();
// 2.
asset.counter = buggy_math_function(input1, input2).into();
Ok(())
}
[!CAUTION]
buggy_math_function
是不正确的,包含以下问题。pub fn buggy_math_function(input1: u8, input2: u8) -> u8 { let divisor = 254 - input2; input1 / divisor }
- 如果
input2 == 254
-> 会发生除以零的恐慌。- 如果
input2 > 254
-> 会发生减法溢出的恐慌。
在 Anchor 工作区中,调用
trident init
这将初始化新的 Trident 工作区并准备所有所需的文件和模糊模板。
[!tip] 如果你想添加新的模糊测试模板,请调用
trident fuzz add
在 trident-tests/fuzz_tests/Cargo.toml
中,添加编写模糊测试所需的依赖项。
[!tip] 在大多数情况下,这些与你在程序 Cargo.toml 中指定的依赖项相同。
在我们提供的示例中,我们需要添加以下依赖项
[[bin]]
name = "fuzz_0"
path = "fuzz_0/test_fuzz.rs"
[package]
name = "fuzz_tests"
version = "0.1.0"
description = "Created with Trident"
edition = "2021"
[dependencies]
honggfuzz = "0.5.56"
arbitrary = "1.3.0"
assert_matches = "1.4.0"
## --- 已添加 ---
anchor-spl = { version = "0.30.1", features = ["metadata"] }
[dependencies.trident-client]
version = "0.7.0"
[dependencies.trident-lesson-part-i]
path = "../../programs/trident-lesson-part-i"
在 trident-tests/fuzz_tests/fuzz_0/accounts_snapshots.rs
中添加所需的使用声明。
[!tip] 在我们的例子中,我们处理的是
Metadata
、Mint
和TokenInterface
,因此我们需要添加use anchor_spl::{metadata::Metadata, token::Mint, token_interface::TokenInterface};
在 trident-tests/fuzz_tests/fuzz_0/test_fuzz.rs
中,指定所有应包含在测试环境中的程序。
[!tip] 在我们的例子中,我们使用CPI到Metaplex Token Metadata,因此我们还需要在创世中包含此程序
[!note] 注意
Metaplex Token Metadata
的入口函数指定为 None。这是因为我们正在包含 SBF 二进制文件,它会自动从trident-genesis
文件夹中读取。[!important] 不要忘记指定你希望模糊测试器生成的指令类型,在这种情况下我们希望生成
FuzzInstruction_trident_lesson_part_i
。如果你在 Anchor 环境中有多个程序,你可以决定要让模糊测试器生成哪些程序的指令。
// ...
pub type FuzzInstruction = FuzzInstruction_trident_lesson_part_i;
// ...
fn main() {
loop {
fuzz_trident!(fuzz_ix: FuzzInstruction, |fuzz_data: MyFuzzData| {
// 1.
let fuzzing_program1 = FuzzingProgram::new(
PROGRAM_NAME_TRIDENT_LESSON_PART_I,
&PROGRAM_ID_TRIDENT_LESSON_PART_I,
processor!(convert_entry!(entry_trident_lesson_part_i))
);
// 2.
let fuzzing_program2 = FuzzingProgram::new(
"metaplex-token-metadata",
&anchor_spl::metadata::ID,
None
);
// 3.
let mut client =
ProgramTestClientBlocking::new(&[fuzzing_program1,fuzzing_program2])
.unwrap();
let _ = fuzz_data.run_with_runtime(PROGRAM_ID_TRIDENT_LESSON_PART_I, &mut client);
});
}
}
[!important] 编写模糊测试分为 3 个基本步骤
- 指定
FuzzAccounts
作为账户存储。- 指定
get_data()
函数。- 指定
get_accounts()
函数。- 可选地,你可以指定不变性检查。
- 不变性检查非常适合在指令之前和之后比较账户。如果指令通过,你可以比较账户是否按预期更新。
FuzzAccounts
存储[!tip] 并不需要为所有账户指定类型。例如,对于众所周知的程序,如系统程序、token程序或Metaplex程序,我们不需要存储。这意味着在
get_accounts()
函数中我们不需要从存储中获取程序。[!note] 对于metadata_account,可以存储在类型为 PdaStore 的存储中。然而在我们的示例中,我们只有一个指令,因此我们不需要存储和重复使用metadata_account,此外,代替手动指定种子,我们可以使用Metaplex的
find_pda
函数。
#[doc = r" 使用 AccountsStorage<T>,其中 T 可以是以下之一:"]
#[doc = r" Keypair, PdaStore, TokenStore, MintStore, ProgramStore"]
#[derive(Default)]
pub struct FuzzAccounts {
asset: AccountsStorage<PdaStore>,
// metadata_account: AccountsStorage<PdaStore>,
mint: AccountsStorage<Keypair>,
// mpl_token_metadata: AccountsStorage<todo!()>,
signer: AccountsStorage<Keypair>,
// system_program: AccountsStorage<todo!()>,
// token_program: AccountsStorage<todo!()>,
}
get_data()
函数[!note] 对于指令输入数据,我们无需做任何事情,除了在 InitializeIx 结构中分配正确的数据字段。这是因为我们可以自动生成随机数据并将其发送到指令。
impl<'info> IxOps<'info> for InitializeIx {
// ...
fn get_data(
&self,
_client: &mut impl FuzzClient,
_fuzz_accounts: &mut FuzzAccounts,
) -> Result<Self::IxData, FuzzingError> {
let data = trident_lesson_part_i::instruction::InitializeIx {
input1: self.data.input1,
input2: self.data.input2,
name: self.data.name.clone(),
symbol: self.data.symbol.clone(),
uri: self.data.uri.clone(),
};
Ok(data)
}
// ...
}
get_accounts()
函数[!note] 在
get_accounts()
中,我们指定从账户存储中创建或重复使用哪些账户。当你的程序包含多个指令时,这特别有帮助。在这种情况下,你可以混合发送到指令的账户,看看是否存在未经授权的访问。
impl<'info> IxOps<'info> for InitializeIx {
// ...
fn get_accounts(
&self,
client: &mut impl FuzzClient,
fuzz_accounts: &mut FuzzAccounts,
) -> Result<(Vec<Keypair>, Vec<AccountMeta>), FuzzingError> {
let signer = fuzz_accounts.signer.get_or_create_account(
self.accounts.signer,
client,
10 * LAMPORTS_PER_SOL,
);
let mint = fuzz_accounts.mint.get_or_create_account(
self.accounts.mint,
client,
10 * LAMPORTS_PER_SOL,
);
let metadata_account =
anchor_spl::metadata::mpl_token_metadata::accounts::Metadata::find_pda(
&mint.pubkey(),
);
let asset = fuzz_accounts
.asset
.get_or_create_account(
self.accounts.asset,
&[b"asset", signer.pubkey().as_ref(), mint.pubkey().as_ref()],
&trident_lesson_part_i::ID,
)
.unwrap();
let signers = vec![signer.clone(), mint.clone()];
let acc_meta = trident_lesson_part_i::accounts::InitializeContext {
signer: signer.pubkey(),
asset: asset.pubkey,
mint: mint.pubkey(),
metadata_account: metadata_account.0,
mpl_token_metadata: anchor_spl::metadata::ID,
system_program: solana_sdk::system_program::ID,
token_program: anchor_spl::token::ID,
}.to_account_metas(None);
Ok((signers, acc_meta))
// ...
}
}
要运行特定的模糊测试:
## trident fuzz run fuzz_0
trident fuzz run <FUZZ_TARGET>
[!important] Honggfuzz提供的输出如下
- 模糊测试迭代次数。
- 反馈驱动模式 = Honggfuzz根据反馈生成数据(即基于覆盖率进展的反馈)。
- 每秒平均迭代次数
- 找到的崩溃次数(恐慌 或 不变性检查失败)
------------------------[ 0 days 00 hrs 00 mins 01 secs ]----------------------
Iterations : 688 (out of: 1000 [68%]) # -- 1. --
Mode [3/3] : Feedback Driven Mode # -- 2. --
Target : trident-tests/fuzz_tests/fuzzing.....wn-linux-gnu/release/fuzz_0
Threads : 16, CPUs: 32, CPU%: 1262% [39%/CPU]
Speed : 680/sec [avg: 688] # -- 3. --
Crashes : 1 [unique: 1, blocklist: 0, verified: 0] # -- 4. --
Timeouts : 0 [10 sec]
Corpus Size : 98, max: 1048576 bytes, init: 0 files
Cov Update : 0 days 00 hrs 00 mins 00 secs ago
Coverage : edge: 10345/882951 [1%] pc: 163 cmp: 622547
---------------------------------- [ LOGS ] ------------------/ honggfuzz 2.6 /-
要查看发现的 Crashfile 的结果。
[!tip] CrashFiles 默认存储在
trident-tests/fuzz_tests/fuzzing/hfuzz_workspace/<FUZZ_TARGET>
中。
trident fuzz run-debug <FUZZ_TARGET> <PATH_TO_CRASHFILE>
[!important] 如果在调试期间出现以下错误消息:
:personality set failed: Operation not permitted
运行以下命令:
echo 'settings set target.disable-aslr false' >~/.lldbinit
[!important] 如果出现以下错误消息:
ImportError: cannot import name 'SBData' from 'lldb' (unknown location)
在此查看解决方案 llvm-project-issue
[!important] 当前开发阶段的调试输出非常冗长,并包含 lldb 部分。我们正在努力改善这一体验。在下图中,你可以看到所提供调试输出的示例。
- 交易日志系列
- 在指令中发送的数据结构
- 恐慌 或 崩溃,基于模糊测试是否在 Solana 程序中出现恐慌或不变性检查失败。
- 原文链接: github.com/Ackee-Blockch...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!