- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
Substrate 的pallet-contracts(智能合约模块)在frame/contracts/benchmarks/目录中存放了两组用真实编程语言(ink! 与 Solidity/solang)编写并编译的 ERC-20 Wasm 合约。本文以该目录及其 README 为核心,详细说明这些真实世界合约在 Substrate 合约基准测试体系中的定位——它们不参与权重(weight)标定,而是专门服务于#[extra]宏基准测试,用于在大体积 Wasm 模块场景下横向比较不同合约语言与不同执行引擎的差异。读完本文,你将掌握benchmarks/目录每个文件的用途、对应基准测试的源码实现与调用链,以及如何运行这些宏基准。
一、目录概览:真实世界合约在基准测试中的角色
frame/contracts/benchmarks/目录下共包含 5 个文件:
| 文件 | 说明 |
|---|---|
ink_erc20.wasm | 用 ink! 3.0.0-rc4 编写的 ERC-20 合约,编译产物,基准测试实际消费的文件 |
ink_erc20_test.wasm | 同合约的测试专用变体,仅在基准测试以测试模式(cfg(test))运行时被加载 |
ink_erc20.json | 对应的 ink! 合约元数据(metadata),仅作信息参考 |
solang_erc20.wasm | 用 Solidity 编写、由 Hyperledger solang 0.1.7 编译器产出的 ERC-20 合约,基准测试实际消费的文件 |
solang_erc20.json | 对应的 solang 合约元数据,仅作信息参考 |
根据 frame/contracts/benchmarks/README.md 的明确说明,本目录的核心定位有两点:
- 真实世界(real world)合约:这些不是为基准测试而手工构造的玩具 Wasm,而是分别由 ink! 和 solang 两个生态产出的真实合约代码。
- 用于宏基准(macro benchmarks)而非权重标定:它们的用途不是像
frame/contracts/src/benchmarking/mod.rs中其余基准那样计算 extrinsic 权重,而是用来在更大的 Wasm 模块前提下比较不同的合约语言(Rust 系 ink! vs Solidity 系 solang)和执行引擎的表现差异。
同时 README 特别强调:目录中的 JSON 文件仅供信息参考,并不参与基准测试的实际执行;真正被消费的是.wasm二进制文件。
二、这些合约如何被基准测试消费:#[extra]与load_benchmark!
从源码结构看,本目录文件被src/benchmarking中标记为#[extra]的基准测试使用(见 frame/contracts/src/benchmarking/mod.rs)。Substrate 的frame-benchmarking宏体系中,#[extra]基准不会进入权重生成流程,而是作为额外补充基准存在,这与 README 中"不用于确定权重"的描述完全吻合。
加载 Wasm 文件的核心机制是load_benchmark!宏(位于 frame/contracts/src/benchmarking/mod.rs):
macro_rules! load_benchmark { ($name:expr) => {{ #[cfg(not(test))] { include_bytes!(concat!("../../benchmarks/", $name, ".wasm")) } #[cfg(test)] { include_bytes!(concat!("../../benchmarks/", $name, "_test.wasm")) } }}; }该宏通过include_bytes!在编译期把 Wasm 二进制直接嵌入 runtime,并针对"基准测试运行"与"测试运行"两种模式选择不同文件:
- 非测试构建:加载
ink_erc20.wasm; - 测试构建(
cfg(test)):加载ink_erc20_test.wasm。
其注释解释了原因:ink! 合约依赖的类型大小在测试环境下定义不同,因此需要一份专门针对测试环境编译的变体;而 solang 合约在这一方面约束更宽松,不需要单独的测试版本(这也解释了目录中为何没有solang_erc20_test.wasm)。
三、源码级实现:ink_erc20_transfer基准
ink_erc20_transfer是消费ink_erc20.wasm的#[extra]基准,完整实现位于 frame/contracts/src/benchmarking/mod.rs:
// Execute one erc20 transfer using the ink! erc20 example contract. #[extra] #[pov_mode = Measured] ink_erc20_transfer { let code = load_benchmark!("ink_erc20"); let data = { let new: ([u8; 4], BalanceOf<T>) = ([0x9b, 0xae, 0x9d, 0x5e], 1000u32.into()); new.encode() }; let instance = Contract::<T>::new( WasmModule::from_code(code), data, )?; let data = { let transfer: ([u8; 4], AccountIdOf<T>, BalanceOf<T>) = ( [0x84, 0xa1, 0x5d, 0xa1], account::<T::AccountId>("receiver", 0, 0), 1u32.into(), ); transfer.encode() }; }: { <Contracts<T>>::bare_call( instance.caller, instance.account_id, 0u32.into(), Weight::MAX, None, data, DebugInfo::Skip, CollectEvents::Skip, Determinism::Enforced, ) .result?; }这段代码揭示了完整的调用链,可以与ink_erc20.json中的元数据一一对应印证:
- 构造函数调用:
data中首 4 字节0x9b ae 9d 5e正是ink_erc20.json中new构造器的 selector,参数为初始供给1000(Balance类型,即u128)。 - 部署:
Contract::<T>::new(...)与WasmModule::from_code(code)结合,把真实 Wasm 模块部署为链上合约。from_code的实现见 frame/contracts/src/benchmarking/code.rs,它会解析 Wasm 模块的导入段,提取线性内存的初始页数与最大页数,并计算代码哈希。 - 转账调用:
data中0x84 a1 5d a1正是ink_erc20.json中transfer消息的 selector(0x84a15da1),携带接收方账户与转账额1。 - 执行:通过
bare_call(不经过 extrinsics 层、绕过签名的底层调用接口)执行一次 ERC-20 转账,使用Weight::MAX的无限 Gas,并以Determinism::Enforced强制确定性执行。
从元数据(ink_erc20.json)可知,该合约还包含total_supply、balance_of、allowance、approve、transfer_from等标准 ERC-20 消息,存储布局由total_supply、balances(ink 的 stash/hashmap 结构)与allowances组成,错误类型为InsufficientBalance/InsufficientAllowance。基准选择transfer这一可变消息,正好覆盖了余额读取、存储写入与事件(Transfer)派发的完整路径。
四、源码级实现:solang_erc20_transfer基准
solang_erc20_transfer消费solang_erc20.wasm,实现位于 frame/contracts/src/benchmarking/mod.rs:
// Execute one erc20 transfer using the open zeppelin erc20 contract compiled with solang. #[extra] #[pov_mode = Measured] solang_erc20_transfer { let code = include_bytes!("../../benchmarks/solang_erc20.wasm"); let caller = account::<T::AccountId>("instantiator", 0, 0); let mut balance = [0u8; 32]; balance[0] = 100; let data = { let new: ([u8; 4], &str, &str, [u8; 32], AccountIdOf<T>) = ( [0xa6, 0xf1, 0xf5, 0xe1], "KSM", "K", balance, caller.clone(), ); new.encode() }; let instance = Contract::<T>::with_caller( caller, WasmModule::from_code(code), data, )?; balance[0] = 1; let data = { let transfer: ([u8; 4], AccountIdOf<T>, [u8; 32]) = ( [0x6a, 0x46, 0x73, 0x94], account::<T::AccountId>("receiver", 0, 0), balance, ); transfer.encode() }; }: { <Contracts<T>>::bare_call( instance.caller, instance.account_id, 0u32.into(), Weight::MAX, None, data, DebugInfo::Skip, CollectEvents::Skip, Determinism::Enforced, ) .result?; }与 ink! 版本的关键差异点:
- 直接
include_bytes!:由于 solang 合约不需要测试变体,这里直接内嵌solang_erc20.wasm,未走load_benchmark!宏。 - 构造参数不同:selector
0xa6 f1 f5 e1对应 solang_erc20.json 中new构造器(合约名ERC20PresetFixedSupply),参数为代币名"KSM"、符号"K"、initialSupply(以 32 字节表示的 u256,这里首字节为 100)以及 owner 账户。 - u256 语义:Solidity 的
initialSupply类型为u256,对应元数据中的 32 字节编码,与 ink! 的u128形成鲜明对比。 - transfer 参数形态:
transfer的 selector 为0x6a 46 73 94,第三个参数同样以 32 字节([u8; 32])编码数量(首字节为 1),体现了两种语言 ABI 编码方式的差异。 - 显式指定部署者:使用
with_caller明确指定instantiator账户,而非默认账户。
两个基准放在一起恰好构成"同一业务(ERC-20 转账)、两种语言、两套 ABI 编码"的对照实验,这正是 README 所述"比较不同合约语言与执行引擎"的直接落地。
五、运行这些宏基准测试
#[extra]基准的运行方式与普通权重基准相同,只是需要加上--extra标志。源码注释中给出了可参考的调试命令(见 frame/contracts/src/benchmarking/mod.rs,原命令用于输出当前Schedule,同样适用于运行本目录相关基准):
cargo run \ --features runtime-benchmarks \ -- benchmark pallet --extra --dev --execution=native \ -p pallet_contracts -e print_schedule --no-median-slopes --no-min-squares如需运行具体的宏基准,可将-e后的参数替换为目标基准名(如ink_erc20_transfer、solang_erc20_transfer),并保留--extra。需要注意:
- 必须以
--features runtime-benchmarks特性编译,因为src/benchmarking/mod.rs整体以#![cfg(feature = "runtime-benchmarks")]守卫; --execution=native确保以原生 runtime 执行(print_schedule这类基准在非 std 环境下会直接返回错误);- 这些基准依赖
pallet_balances提供账户资金(基准中通过caller_funding为调用者铸币),因此运行 runtime 需要同时配置pallet-contracts与pallet-balances。
此外,src/benchmarking末尾的impl_benchmark_test_suite!宏(frame/contracts/src/benchmarking/mod.rs)会把上述基准(含ink_erc20_transfer)挂接到测试套件,在测试模式下自动改用ink_erc20_test.wasm执行。
六、指令级基准与沙箱:宏基准之外的对照
值得补充的是,本目录的"真实合约"基准与src/benchmarking中的指令级基准(instruction benchmarks)形成互补。指令级基准(如instr_i64const)并不实例化完整合约,而是通过 frame/contracts/src/benchmarking/sandbox.rs 中的Sandbox仅实例化 Wasm 执行环境——它使用EmptyEnv(不导入任何 seal 函数)执行合约的call导出函数,并把 wasmi 引擎的燃料(fuel)设为u64::MAX,以纯粹测量单个 Wasm 指令的执行成本。
两者的分工可以概括为:
| 基准类别 | 输入 | 目的 | 是否参与权重 |
|---|---|---|---|
| 权重基准(weight benchmarks) | 程序化生成的 Wasm 模块(WasmModule::dummy/sized) | 标定各 extrinsic 与 seal API 的权重 | 是 |
| 指令级基准(instruction benchmarks) | 程序化生成的单指令模块 | 生成每条 Wasm 指令的权重 | 是 |
| 宏基准(macro benchmarks,本目录) | 真实世界合约(ink!/solang ERC-20) | 对比合约语言与执行引擎、验证大模块场景 | 否(#[extra]) |
七、结论与进一步探索
frame/contracts/benchmarks/是 Substrate 合约基准测试体系中"真实世界验证"一环:它用 ink! 与 solang 两条技术栈编译出的同业务合约,通过#[extra]宏基准在完整的沙箱执行路径(部署 → 实例化 →bare_call)中运行,与程序化生成的合成基准互为印证。JSON 元数据文件则完整记录了合约 ABI、selector、存储布局与编译器信息,是理解两个基准中硬编码字节序列(如0x9bae9d5e、0x84a15da1、0xa6f1f5e1)的权威参考。
感兴趣的读者可以继续深入以下路径:
- frame/contracts/benchmarks/ink_erc20.json:ink! ERC-20 完整 ABI 与存储布局;
- frame/contracts/benchmarks/solang_erc20.json:solang ERC-20(
ERC20PresetFixedSupply)完整 ABI; - frame/contracts/src/benchmarking/mod.rs:全部基准实现、
load_benchmark!宏与运行命令注释; - frame/contracts/src/benchmarking/code.rs:
WasmModule构造逻辑(from_code、dummy、sized); - frame/contracts/src/benchmarking/sandbox.rs:指令级基准的无导入沙箱环境。
- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
相关推荐
Presto 宏基准测试完全指南:基于 Benchto 的 presto-benchto-benchmarks 实战详解
Presto 宏基准测试完全指南:基于 Benchto 的 presto benchto benchmarks 实战详解 本指南以 Presto 仓库中的 pr
大数据数据库后端给 Windows Terminal 装上会看时间的皮肤:主题自动切换实战
给 Windows Terminal 装上会看时间的皮肤:主题自动切换实战 周五下午,你盯着发白的终端背景皱了皱眉,顺手打开「设置 个性化」,把应用模式从浅色切
区块链开发框架后端SciPy 性能基准测试完全指南:基于 Airspeed Velocity 的 benchmarks 框架剖析
SciPy 性能基准测试完全指南:基于 Airspeed Velocity 的 benchmarks 框架剖析 导读 本文以 benchmarks/README
科学计算数据科学高性能计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考