本文介绍了如何利用 Pinocchio 框架和 Mollusk 测试框架构建高性能、轻量级的 Solana 程序。作者强调了传统测试仅关注交易成败的局限性,并提出通过 sbpf-coverage 工具实现源码级的代码覆盖率分析,以确保每一行逻辑和执行路径都经过严谨验证,从而达到“航空级”的工程可靠性。
Vladislav Kotsev
6 分钟
当机械师在飞行前检查商业客机时,他们不仅仅是启动发动机,听着它的轰鸣声说:“我觉得听起来没错。” 因为这关乎人类的生命,那种表面层级的检查是危险且不充分的。相反,他们使用专门的 X 射线设备深入观察机器内部,检查每个独立涡轮叶片内部是否存在微观裂纹。
作为一名在 Solana 上构建的区块链架构师,我通过 类似的视角 来看待我们的工作。
我们编写和部署的程序处理现实世界的业务运营,且财务风险很高。然而,从历史上看,区块链领域的许多标准测试相当于只是听听发动机的运转声。你编写一个测试,模拟一个交易,它返回一个 Ok(()),然后你就发布了。
但那些没有被触发的执行路径呢?那些隐藏在指令逻辑深处的模糊错误处理块或死代码呢?为了构建真正具有韧性的基础设施,我们需要我们自己版本的 X 射线机。我们需要行级可视化。
在我的日常工作中,我非常关注如何优化 Solana 程序的编写和验证方式。在过去的一年里,我逐渐从 Anchor 较重的传统框架转向更精简、更精确的技术栈。
对于 CU-敏感的应用和程序架构,我一直在使用 Pinocchio 库进行构建。它是一个极简的 Solana 程序框架,剥离了开销,让我们能够直接、无过滤地访问 Solana 的执行环境,以节省计算单元。这是有代价的:你需要对安全性负责。但回报也是显著的:程序更小、更快且执行成本更低。
为了演示这一点,让我们看一个经典的原子交换模式:Token 托管 (Escrow)。我们的托管程序支持两条指令:
当你剥离了宏(macros),你的项目结构会变得极其专注:
escrow/
├── Cargo.toml
├── Makefile.toml
├── src/
│ ├── lib.rs # 入口点 + 指令分发
│ ├── constants.rs # Seed 常量
│ ├── state/
│ │ ├── mod.rs
│ │ └── escrow.rs # 托管账户状态
│ └── instructions/
│ ├── mod.rs
│ ├── helpers.rs # 可重用的账户验证助手
│ ├── make.rs # Make 指令
│ └── take.rs # Take 指令
└── tests/
├── make_test.rs
└── take_test.rs
托管账户存储了谁创建了它、涉及哪些 Mint、预期多少 Token B 以及 PDA 派生参数。使用 Pinocchio,我们不拥有数据;我们借用它来实现零拷贝反序列化:
use pinocchio::Address;
use wincode::{SchemaRead, SchemaWrite};
#[repr(C)]
#[derive(SchemaRead, SchemaWrite)]
pub struct Escrow<'a> {
maker: &'a [u8; 32],
mint_a: &'a [u8; 32],
mint_b: &'a [u8; 32],
receive: &'a u64,
seed: &'a u64,
bump: &'a u8,
}
impl<'a> Escrow<'a> {
// 手动计算大小: 3*(32) + 2*(8) + 1 = 113 字节
pub const LEN: usize = 3 * size_of::<[u8; 32]>() + 2 * size_of::<u64>() + size_of::<u8>();
}
#[repr(C)] 确保了可预测的内存布局。结合 wincode 的派生,该结构体创建了指向现有字节切片的指针,而不是对其进行昂贵的复制。
入口点完全没有任何魔术代码。单个字节决定了分发:
use pinocchio::{AccountView, Address, ProgramResult, entrypoint, error::ProgramError};
use pinocchio_pubkey::declare_id;
declare_id!("4AtAjt1xrf6SwnwSh8GjnTJFrkMVZBscPSKKaFG8mQam");
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Address,
accounts: &[AccountView],
instruction_data: &[u8],
) -> ProgramResult {
let [disc, data @ ..] = instruction_data else {
return Err(ProgramError::InvalidInstructionData);
};
match disc {
Make::DISCRIMINATOR => Make::try_from((accounts, data))?.process(),
Take::DISCRIMINATOR => Take::try_from((accounts, data))?.process(),
_ => Err(ProgramError::InvalidInstructionData),
}
}
在处理指令时,例如 Make,我们通过跨程序调用 (CPIs) 显式处理 PDA 创建、状态初始化和 Token 转账:
pub fn process(&self) -> ProgramResult {
// 1. 创建托管 PDA 账户
let seeds = &[ /* ... */ ];
ProgramAccount::init::<Escrow>(self.accounts.maker, self.accounts.escrow, seeds, space)?;
// 2. 创建由托管账户拥有的金库 ATA
AssociatedToken::init(self.accounts.vault, self.accounts.mint_a, /* ... */)?;
// 3. 写入托管状态
let mut escrow = deserialize_mut::<EscrowMut>(escrow_account_data)?;
escrow.set_inner(maker, mint_a, mint_b, &receive, &seed, &bump);
// 4. 将 Token 从创建者的 ATA 转账到金库
Transfer { from: self.accounts.maker_ata, authority: self.accounts.maker,
to: self.accounts.vault, amount: self.data.amount }.invoke()
}
然而,像这样精简的架构 必须 配合全面的测试,以确保其在生产环境中的可靠性。
为了测试这些程序,我们利用 Mollusk 测试框架。Mollusk 在本地直接调用 Solana 虚拟机 (SVM) 指令处理器。它能让我们快速高效地启动引擎。
设置非常简单:
// 我们使用可调试的构造函数初始化 mollusk,稍后会用到它。
let mut mollusk = Mollusk::new_debuggable(&PROGRAM_ADDRESS, "target/deploy/escrow", true);
Mollusk 要求你手动提供所有账户。这迫使你深入理解程序交互的确切状态:
// 为测试状态手动创建一个 Token 账户
let maker_a_data = TokenAccount {
mint: mint_a_address,
owner: maker_address,
amount: 1_000_000,
delegate: COption::None,
state: AccountState::Initialized,
is_native: COption::None,
delegated_amount: 0,
close_authority: COption::None,
};
let (maker_a_ata_address, maker_a_ata_account) =
create_account_for_associated_token_account(maker_a_data);
但同样,仅运行引擎是不够的。我们需要 X 射线。
这正是 sbpf-coverage 所提供的。当你运行结合了 sbpf-coverage 的 Mollusk 测试时,你不再需要猜测。该工具将执行的 SBPF (Solana Berkeley Packet Filter) 指令直接映射回你的 Rust 源代码。
它提供了行级可视化。它能准确告诉你测试触达了哪些代码行,更重要的是,错过了哪些。如果存在隐藏的 panic、未经测试的算术溢出检查,或者你的测试根本无法触及的逻辑分支,sbpf-coverage 会直接在上面点亮明灯。
与其费力地编辑你的 Cargo.toml 来更改发布配置 (release profiles)(这可能很痛苦且容易误提交),你不如干脆跳过编辑文件。你可以手动构建 Solana 程序,通过 RUSTFLAGS 直接传递你需要的确切编译器参数。
只需运行以下构建命令:
RUSTFLAGS="-Copt-level=0 -C strip=none -C debuginfo=2" cargo build-sbf --tools-version v1.54 --arch v1 --debug
注意:[profile.release] 部分的设置是强制性的。如果没有它们,你的 X 射线机将会变得模糊且不准确。
使用这套工具栈可以确保在程序系统上线之前,其最深处不存在隐藏错误或未经测试的路径。

覆盖率报告示例
为了演示这种架构和测试方法论是如何结合在一起的,我准备了一个全面的概念验证。
我最近准备了一个完全使用这套新工具栈构建的完整托管示例程序。这是一个经典的 Solana 托管实现,但其价值在于底层的结构化和测试方式。它使用 Pinocchio 编写程序逻辑,使用 Mollusk 作为测试环境,并利用 sbpf-coverage 来确保行级的审查。
我相信,如果我们真的想让 Solana 成为全球金融基础设施的基准,我们就必须采用航空级的工程标准。我们不能再仅仅满足于听听发动机的声音了。
我邀请你深入研究该代码库,检查代码,并查看实际运行中的测试框架。你可以在这里查看该框架和完整实现:https://github.com/vlady-kotsev/escrow-pinocchio-0.10\ \
欢迎随时联系交流。我总是很乐意与对高性能架构充满热情的同行开发者们一起切磋。Peace. ✌️
- 原文链接: limechain.tech/blog/insp...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!