- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
导读
pallet-glutton(Glutton Pallet)是 Substrate 生态中一个专为测试场景设计的有趣 FRAME 模块:它的名字源于其"贪婪吞噬资源"的特性——可以在每个区块的on_idle阶段,按比例消耗链上剩余的计算权重(ref_time)与存储证明权重(proof_size),从而在实战环境中把平行链(parachain)及其中继链(relay chain)推至理论极限。阅读本文后,你将掌握 Glutton 的初始化、set_compute/set_storage两个核心控制接口、on_idle的权重吞噬原理、Genesis 配置方式,以及如何结合源码与测试用例进行实操验证。注意:该 Pallet 仅用于测试,严禁部署在任何承载真实价值的链上。
一、Glutton 是什么:为什么需要"资源吞金兽"
在区块链开发中,有一个非常现实的问题:如何在实际运行环境中验证一条链的理论吞吐上限?普通单元测试往往无法模拟真实网络负载、区块生产节奏与资源争抢。Substrate 在 frame/glutton 中提供的pallet-glutton正是为此而生。
从其 README 的第一行就能看到官方明确的警告:
DO NOT USE ON VALUE-BEARING CHAINS. THIS PALLET IS ONLY INTENDED FOR TESTING USAGE.
也就是说,这个 Pallet 唯一的目的就是把平行链与中继链压到极限,用实践检验理论极限(theoretical limits)。它的实现思路非常精巧:
- 它本身不执行任何有业务意义的逻辑,只负责"浪费"区块权重;
- 它通过 FRAME 的
on_idlehook 工作,在区块执行完正常交易后,按设定的比例吞噬"剩余可用权重"; - 消耗的对象有两个维度:
ref_time(计算时间)与proof_size(存储证明大小),对应 Substrate 2D 权重模型中的两个分量。
从 源码模块文档 中可以确认其定位:Pallet that consumes ref_time and proof_size of a block. Based on the Compute and Storage parameters the pallet consumes the adequate amount of weight.
二、核心机制:on_idle与权重吞噬原理
2.1 FRAME 的on_idlehook
on_idle是 FRAME 提供给每个 Pallet 的区块生命周期 hook 之一。在 frame/support/src/traits/hooks.rs 中定义如下:
pub trait OnIdle<BlockNumber> { /// See [`Hooks::on_idle`]. fn on_idle(_n: BlockNumber, _remaining_weight: Weight) -> Weight { Weight::zero() } }它的语义是:区块在执行完所有交易(extrinsics)之后、还有剩余权重(remaining_weight)时被调用,返回值表示该 hook 实际消耗了多少权重。多个 Pallet 的on_idle会按照元组顺序被依次调用,后一个 Pallet 只能看到前一个消耗后剩下的权重(见 hooks.rs 中的元组实现)。Glutton 正是利用了这一机制——它专门"吃掉"别人用不完的权重。
2.2 Glutton 的on_idle实现
Glutton 在 frame/glutton/src/lib.rs#L177-L210 中实现了on_idle:
fn on_idle(_: BlockNumberFor<T>, remaining_weight: Weight) -> Weight { let mut meter = WeightMeter::from_limit(remaining_weight); if meter.try_consume(T::WeightInfo::empty_on_idle()).is_err() { return T::WeightInfo::empty_on_idle() } let proof_size_limit = Storage::<T>::get().saturating_mul_int(meter.remaining().proof_size()); let computation_weight_limit = Compute::<T>::get().saturating_mul_int(meter.remaining().ref_time()); let mut meter = WeightMeter::from_limit(Weight::from_parts( computation_weight_limit, proof_size_limit, )); Self::waste_at_most_proof_size(&mut meter); Self::waste_at_most_ref_time(&mut meter); meter.consumed() }工作流程分为四步:
- 扣除基础开销:先用
WeightMeter包装剩余权重,尝试消耗empty_on_idle()这一基础成本(用于读取Compute/Storage两个存储项的权重)。如果剩余权重连这一步都不够,就直接返回empty_on_idle(),保证on_idle本身的开销被如实上报。 - 按比例计算消耗上限:从存储中读取
Compute与Storage两个比例值,分别乘以meter.remaining()的ref_time与proof_size,得到本次允许消耗的"计算权重上限"与"证明大小上限"。这意味着 Glutton 消耗的是基于当前剩余权重的一定比例,而非固定值。 - 先浪费 proof size,再浪费 ref time:分别调用
waste_at_most_proof_size与waste_at_most_ref_time,尽量逼近(但不超过)各自的上限。 - 返回实际消耗:最终返回
meter.consumed(),让区块生产者在结算时知道 Glutton 用掉了多少权重。
2.3 两个"浪费"函数:贴近上限的精细控制
证明大小消耗(waste_at_most_proof_size)在 lib.rs#L283-L312:
pub(crate) fn waste_at_most_proof_size(meter: &mut WeightMeter) { let Ok(n) = Self::calculate_proof_size_iters(&meter) else { return }; meter.consume(T::WeightInfo::waste_proof_size_some(n)); (0..n).for_each(|i| { TrashData::<T>::get(i); }); }它先通过权重曲线waste_proof_size_some(n)估算出"执行多少次读取操作能填满剩余权重",然后实际执行n次TrashData读取。每次读取会访问一个 1 KiB 的存储值,从而产生真实的 PoV(Proof of Validity)证明开销。
计算时间消耗(waste_at_most_ref_time)在 lib.rs#L314-L364:
pub(crate) fn waste_at_most_ref_time(meter: &mut WeightMeter) { let Ok(n) = Self::calculate_ref_time_iters(&meter) else { return }; meter.consume(T::WeightInfo::waste_ref_time_iter(n)); let clobber = Self::waste_ref_time_iter(vec![0u8; 64], n); // By casting it into a vec we can hopefully prevent the compiler from optimizing it debug_assert!(clobber.len() == 64); if clobber.len() == 65 { TrashData::<T>::insert(0, [clobber[0] as u8; VALUE_SIZE]); } }实际的计算"浪费"由waste_ref_time_iter完成(lib.rs#L332-L345):用 Blake2b-512 哈希器对一段数据反复执行i次哈希更新,最后finalize一次。注释中特别说明了两点设计意图:
- Blake2 哈希速度非常快,所以单次迭代会做多次
hasher.update,以便一次性消耗更多ref_time; - 通过把哈希结果放入
Vec并加一个"不可能成立"的len() == 65判断分支,尽可能阻止编译器把这段纯计算优化掉——否则就浪费不成权重了。
从 benchmarking.rs#L52-L56 可看到,waste_ref_time_iter的基准测试范围是i in 0..100_000,每次迭代的ref_time开销在 1–10 ms 量级(见 lib.rs#L332-L335 的注释)。
三、三个核心外部接口
Glutton 对外只暴露三个可调用函数,全部仅允许 Root 或AdminOrigin调用,权限由 Config trait 中的AdminOrigin控制:
| 接口 | 参数 | 作用 | 相关事件 |
|---|---|---|---|
initialize_pallet | new_count: u32,witness_count: Option<u32> | 初始化/调整TrashData垃圾数据量 | PalletInitialized { reinit } |
set_compute | compute: FixedU64 | 设置on_idle消耗剩余ref_time的比例 | ComputationLimitSet { compute } |
set_storage | storage: FixedU64 | 设置on_idle消耗剩余proof_size的比例 | StorageLimitSet { storage } |
3.1initialize_pallet:先初始化,才能开吃
在 lib.rs#L212-L248 中,initialize_pallet负责管理TrashData——一张用于"浪费证明大小"的存储 Map。关键点:
pub fn initialize_pallet( origin: OriginFor<T>, new_count: u32, witness_count: Option<u32>, ) -> DispatchResult { T::AdminOrigin::ensure_origin_or_root(origin)?; let current_count = TrashDataCount::<T>::get(); ensure!( current_count == witness_count.unwrap_or_default(), Error::<T>::AlreadyInitialized ); // ... 按 new_count 与 current_count 的大小关系插入或删除条目 Self::deposit_event(Event::PalletInitialized { reinit: witness_count.is_some() }); TrashDataCount::<T>::set(new_count); Ok(()) }new_count:目标条目数量。源码注释给出的推荐默认值是5_000(一个合理的垃圾数据量)。witness_count:当前TrashData的实际条目数(见证值)。首次初始化时传None;如果 Pallet 已经初始化过,必须传Some(current_count)才能绕过AlreadyInitialized错误,实现"重新初始化"(扩容或收缩)。new_count > current_count时插入新条目,否则删除多余条目。
TrashData的每个值大小为VALUE_SIZE = 1024字节(1 KiB),Map 的MaxValues被限制为MAX_TRASH_DATA_ENTRIES = 65_000(见 lib.rs#L49-L54)。为什么是 65k?源码注释解释得很清楚(lib.rs#L120-L135):65k 刚好比16^4 = 65536小一点,避免触发证明大小的基准测试跳变(overestimate);实践中 65k × 1 KiB ≈ 65 MiB 的证明浪费上限已经足够。
3.2set_compute与set_storage:设定吞噬比例
两个函数逻辑完全对称(lib.rs#L250-L280):
pub fn set_compute(origin: OriginFor<T>, compute: FixedU64) -> DispatchResult { T::AdminOrigin::ensure_origin_or_root(origin)?; ensure!(compute <= RESOURCE_HARD_LIMIT, Error::<T>::InsaneLimit); Compute::<T>::set(compute); Self::deposit_event(Event::ComputationLimitSet { compute }); Ok(()) } pub fn set_storage(origin: OriginFor<T>, storage: FixedU64) -> DispatchResult { T::AdminOrigin::ensure_origin_or_root(origin)?; ensure!(storage <= RESOURCE_HARD_LIMIT, Error::<T>::InsaneLimit); Storage::<T>::set(storage); Self::deposit_event(Event::StorageLimitSet { storage }); Ok(()) }比例值的语义非常直白(lib.rs#L106-L118):
1.0对应 100%,即吞掉全部剩余ref_time(或proof_size);- 最大允许值为
RESOURCE_HARD_LIMIT = 10(1000%),超过会返回InsaneLimit错误; - 超过 1.0 可能导致链停滞(stall the chain)——因为你让
on_idle去消耗超过区块剩余权重的资源。
两个存储项Compute与Storage都是StorageValue<_, FixedU64, ValueQuery>,默认值为 0(即默认不消耗任何资源),这也是测试在 tests.rs#L92 中断言Compute::<T>::get()初始为零的原因。
四、Genesis 配置:预置初始状态
如果不希望在部署后手动调用initialize_pallet,可以在 Genesis 中直接配置。Glutton 的 GenesisConfig 包含三个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
compute | FixedU64 | 初始计算消耗比例 |
storage | FixedU64 | 初始存储消耗比例 |
trash_data_count | u32 | 初始垃圾数据条目数(用于浪费 proof size) |
genesis_build中有三条硬性断言:
trash_data_count <= MAX_TRASH_DATA_ENTRIES,超出直接 panic;compute <= RESOURCE_HARD_LIMIT,否则报 "Compute limit is insane";storage <= RESOURCE_HARD_LIMIT,否则报 "Storage limit is insane"。
示例配置(以 JSON chain spec 形式):
{ "glutton": { "compute": "500000000", "storage": "500000000", "trash_data_count": 5000 } }注意:
FixedU64在 JSON 中通常以整数形式表示定点数(1.0 对应其定点表示值),实际数值需按运行时使用的FixedU64编码换算;trash_data_count建议使用文档推荐的5_000作为初始值。
五、Runtime 集成方式
在 Substrate 节点运行时中,Glutton 的集成方式与普通 FRAME Pallet 一致。以bin/node/runtime为例:
1. 依赖声明(bin/node/runtime/Cargo.toml):
pallet-glutton = { version = "4.0.0-dev", default-features = false, path = "../../../frame/glutton" }并在std、runtime-benchmarks、try-runtime三个 feature 中分别引入对应开关(见 Cargo.toml#L178、#L274、#L346)。
2. Config 实现(bin/node/runtime/src/lib.rs#L443-L447):
impl pallet_glutton::Config for Runtime { type RuntimeEvent = RuntimeEvent; type AdminOrigin = EnsureRoot<AccountId>; type WeightInfo = pallet_glutton::weights::SubstrateWeight<Runtime>; }AdminOrigin使用EnsureRoot,即只有 Root(治理)能调用三个外部接口;WeightInfo使用仓库自动生成的 weights.rs 中的SubstrateWeight。
3. 注册到 Runtime(bin/node/runtime/src/lib.rs#L2050):
Glutton: pallet_glutton,并在 benchmarking 白名单中加入[pallet_glutton, Glutton](bin/node/runtime/src/lib.rs#L2200),使其可以参与基准测试。
六、基准测试与权重校准
Glutton 的"浪费"行为高度依赖准确的权重曲线,因此它的 benchmark 设计非常讲究。benchmarking.rs 顶部注释特别提醒:
Has to be compiled and run twice to calibrate on new hardware.
也就是说,在新硬件上必须把 benchmark 编译并运行两次来完成校准。它定义了以下基准:
| Benchmark | 变量范围 | 用途 |
|---|---|---|
initialize_pallet_grow | n in 0..1_000 | 初始化/扩容TrashData的权重 |
initialize_pallet_shrink | n in 0..1_000 | 收缩TrashData的权重 |
waste_ref_time_iter | i in 0..100_000 | 单次哈希迭代消耗的ref_time曲线 |
waste_proof_size_some | i in 0..5_000 | 单次TrashData读取消耗的 proof size 曲线 |
on_idle_high_proof_waste/on_idle_low_proof_waste | — | 手动验证on_idle在高低两种 proof 负载下的行为 |
empty_on_idle | — | on_idle自身的基础开销 |
set_compute/set_storage | — | 两个设置函数的基础权重 |
生成的权重文件 frame/glutton/src/weights.rs 是 2023-06-16 在 2.60 GHz Intel Xeon 上、以--steps=50 --repeat=20参数自动生成的,其中记录了完整的 benchmark CLI 命令(weights.rs#L26-L43),可供复现:
./target/production/substrate benchmark pallet \ --chain=dev --steps=50 --repeat=20 \ --pallet=pallet_glutton \ --no-storage-info --no-median-slopes --no-min-squares \ --extrinsic='*' --execution=wasm --wasm-execution=compiled \ --heap-pages=4096 \ --output=./frame/glutton/src/weights.rs \ --header=./HEADER-APACHE2integrity_test:防死循环的最后防线
Glutton 在 lib.rs#L179-L188 还实现了一个integrity_test:
fn integrity_test() { assert!( !T::WeightInfo::waste_ref_time_iter(1).ref_time().is_zero(), "Weight zero; would get stuck in an infinite loop" ); assert!( !T::WeightInfo::waste_proof_size_some(1).proof_size().is_zero(), "Weight zero; would get stuck in an infinite loop" ); }它的作用很关键:如果某个平台上这两条权重曲线是零,on_idle里的迭代计算就会陷入死循环(永远填不满 meter)。这个 hook 会在运行时完整性检查阶段(如try-runtime或集成测试)提前拦截此类错误配置。
七、测试用例:行为验证与精度保证
frame/glutton/src/tests.rs 提供了完整的单元测试,可以从四个角度验证 Pallet 行为:
7.1 初始化与扩容/收缩
initialize_pallet_works:验证只有 Root 能调用、首次初始化后重复调用(不带见证值)会返回AlreadyInitialized、事件正确发出,且TrashData中确实写入了确定性生成的值(tests.rs#L28-L60)。expand_and_shrink_trash_data_works:验证5000 → 8000 → 6000 → 0的扩容/收缩流程,TrashDataCount与iter_keys().count()始终一致(tests.rs#L62-L87)。
7.2 比例设置与硬上限
setting_compute_respects_limit/setting_storage_respects_limit:验证9.99、10.0合法,10.01返回InsaneLimit(tests.rs#L111-L161)。setting_compute_works/setting_storage_works:验证非 Root 签名(signed(1))和无签名(none())调用都会返回BadOrigin(tests.rs#L89-L109)。
7.3on_idle的"接近上限"精度
Glutton 的测试最核心的断言是:实际消耗必须尽量接近目标,且绝不能超支(over-spend)。以on_idle_weight_high_proof_is_close_enough_works为例(tests.rs#L172-L194):
let should = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, WEIGHT_PROOF_SIZE_PER_MB * 5); let got = Glutton::on_idle(1, should); assert!(got.all_lte(should), "Consumed too much weight"); let ratio = Perbill::from_rational(got.proof_size(), should.proof_size()); assert!(ratio >= Perbill::from_percent(99), "Too few proof size consumed, ..."); let ratio = Perbill::from_rational(got.ref_time(), should.ref_time()); assert!(ratio >= Perbill::from_percent(99), "Too few ref time consumed, ...");在compute = storage = 1.0(100%)的情况下,on_idle消耗的权重必须同时满足:
all_lte(should):绝不允许超过剩余权重;- 消耗比例 ≥ 99%:也不能浪费太多,保证压测效果接近真实上限。
on_idle_weight_over_unity_is_close_enough_works(tests.rs#L220-L249)进一步验证了比例超过 1.0 的场景:当设置compute = 1.75, storage = 1.5时,on_idle会尝试消耗超过区块限制的权重(consumed.all_gt(max_block)),此时消耗量应介于区块上限与"请求量"之间——这正是测试链是否会被"撑爆"的极端场景。
waste_at_most_ref_time_weight_close_enough与waste_at_most_proof_size_weight_close_enough(tests.rs#L251-L283)则直接对两个内部"浪费"函数做精度校验:在ref_time与proof_size分别接近无限的极端 meter 上,要求consumed_ratio() >= 99%,否则报错Weight calibration failed. Please re-run the benchmarks on the same hardware.——再次印证了"权重曲线必须与硬件匹配"这一前提。
7.4 数据生成器的确定性
gen_value_works(tests.rs#L285-L294)验证gen_value:生成的 1 KiB 值长度正确、不同 seed 值互不相同、非全零、且对相同 seed 具有确定性。这是TrashData数据可复现、可基准化的基础。
八、使用场景与实战建议
8.1 典型压测流程
结合 README 与源码,一个完整的 Glutton 压测流程如下:
- 部署:在测试平行链/中继链的 runtime 中注册
pallet-glutton(参考第五节),并在 Genesis 中预置trash_data_count(如 5000)。 - 初始化:若未配置 Genesis,先以 Root 调用
initialize_pallet(5000, None)初始化垃圾数据。 - 设置比例:以 Root 调用
set_compute/set_storage,例如各设0.5(消耗 50% 剩余权重);逐步调大观察链的负载与出块表现。 - 压测:观察区块利用率、出块时间、PoV 大小等指标,逐步逼近 100%(
1.0)甚至超过(> 1.0),找到链的真实瓶颈。
8.2 注意事项(红线)
- 严禁用于有价值链:README 与源码文档首行都给出了
WARNING,这条红线没有例外; - 比例不要轻易设超过 1.0:源码与注释多次警告
Setting this to over 1.0 could stall the chain,只有在你确实想测试"链何时会停滞"时才使用; - 换硬件必须重新校准权重:
benchmarking.rs明确要求在新硬件上编译运行两次 benchmark,否则on_idle的实际消耗可能与预期偏差超过 1%; TrashData上限 65k:虽然源码注释说明该上限"未强制实施"(超出也能工作),但超过后基准权重可能被低估,导致 PoV 相关权重不准确。
九、源码结构速查
| 文件 | 内容 |
|---|---|
| frame/glutton/README.md | 官方警告与功能简介 |
| frame/glutton/src/lib.rs | Pallet 全部实现:存储项、外部接口、on_idle与内部浪费逻辑 |
| frame/glutton/src/benchmarking.rs | 9 个 benchmark 与校准说明 |
| frame/glutton/src/weights.rs | 自动生成的权重曲线(SubstrateWeight) |
| frame/glutton/src/tests.rs | 行为、权限、精度与确定性测试 |
| frame/glutton/src/mock.rs | 测试用 mock runtime(AdminOrigin = EnsureRoot) |
| frame/glutton/Cargo.toml | 依赖与 feature 声明(std/runtime-benchmarks/try-runtime) |
| bin/node/runtime/src/lib.rs | 参考运行时中的集成示例(Config、注册、benchmark 白名单) |
结语
Glutton Pallet 是 Substrate 测试工具箱中一个"反常规"却极其实用的组件:它用最朴素的方式(反复哈希、反复读存储)把区块链的权重预算精确"烧掉",让开发者能在真实环境中验证链的理论极限。理解它的on_idle消耗模型、FixedU64比例语义与基准测试校准逻辑,不仅能帮助你正确地用它做压测,也能加深对 Substrate 权重系统(ref_time/proof_size二维模型)本身的理解。记住那句开篇警告:它只属于测试环境,永远不要让它出现在承载真实价值的链上。
- 区块链
- 开发框架
- 后端
【免费下载链接】substrate
Substrate: The platform for blockchain innovators
相关推荐
Substrate pallet-elections-phragmen 深度解析:基于顺序 Phragmén 的链上选举模块
Substrate pallet elections phragmen 深度解析:基于顺序 Phragmén 的链上选举模块 本文围绕 Substrate FR
区块链开发框架后端Substrate区块链框架深度解析:下一代区块链开发利器
Substrate区块链框架深度解析:下一代区块链开发利器 什么是Substrate? Substrate是一个革命性的区块链开发框架,它代表了区块链技术的下一
区块链开发框架后端WeKan 卡片清单怎么配置自动重置间隔?
WeKan 卡片清单怎么配置自动重置间隔? 在 WeKan 里把卡片清单(checklist)当作日常例行事项使用时,常见的麻烦是:勾完一遍后还要手动把每一项取
区块链开发框架后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考