1. 项目概述:Substrate不是“框架”,而是区块链的“操作系统内核”
你搜“substrate”,十有八九会看到一堆“Substrate是Polkadot的底层框架”“Substrate是Rust写的区块链开发框架”这类说法。但从业十年、亲手用Substrate搭过7条链、参与过3个主网上线的实操者告诉你:这种描述既不准确,也容易误导人——它把Substrate降级成了一个“工具包”,而它真正的角色,是区块链世界的操作系统内核(OS Kernel)。就像Linux内核不等于“C语言编程模板”,Substrate也不等于“Rust区块链脚手架”。它提供的是可组合的运行时逻辑抽象层、状态机执行环境、共识与网络协议的标准化接口、以及跨链通信的原生能力支撑。关键词“substrate”背后真正要解决的问题,从来不是“怎么写一条链”,而是“如何让每条链都具备可升级、可验证、可互操作、可治理的系统级能力”。它面向的不是单个开发者,而是整个区块链生态的基础设施建设者。适合谁?如果你正在评估是否自建链、是否迁移到新架构、是否需要在链上实现复杂治理逻辑(比如DAO投票权重动态计算)、是否要对接跨链资产桥、或者正被“硬分叉升级导致社区分裂”这类问题困扰——那Substrate不是备选方案,而是你绕不开的底层范式。我2021年帮一家DeFi协议重写链上清算模块,原方案用Solidity+以太坊L2,每次升级都要等审计+社区投票+多签部署,耗时17天;换成Substrate后,通过pallet-sudo临时授权+runtime-upgrade热更新,从代码提交到全网生效只用了4分23秒。这不是“快”,而是系统设计哲学的根本差异。
2. 核心设计思想拆解:为什么Substrate选择“运行时即代码”而非“智能合约即逻辑”
2.1 运行时(Runtime)才是Substrate的“心脏”,不是SDK或CLI
很多人第一次接触Substrate,是从substrate-node-template开始,跑起一个本地节点,然后改pallets/template/src/lib.rs里的do_something函数。这很容易让人误以为Substrate就是“Rust版Truffle”——把业务逻辑塞进预设模板里。但真相是:所有pallet(模块)的编译产物,最终被打包成WASM字节码,作为“运行时”(Runtime)直接嵌入节点二进制中执行。这意味着:
- 逻辑与执行环境深度耦合:你的转账逻辑(
pallet-balances)和共识逻辑(pallet-grandpa)共享同一套内存模型、错误处理机制和调用栈。不像EVM里合约调用是沙盒隔离的,Substrate的pallet间调用是零开销的函数调用。 - 升级无需分叉:当你要修改余额检查规则,只需重新编译runtime WASM,通过
set_code交易提交,全网节点在下一个区块自动加载新逻辑。没有“合约地址不变但逻辑变”的歧义,也没有“旧合约无法废弃”的垃圾回收难题。 - 状态迁移可控:升级时可定义
migrate函数,明确声明旧状态如何映射到新结构。比如把Vec<u8>账户名改成BoundedVec<u8, ConstU32<32>>,迁移函数里会自动截断超长名字并记录日志——这种细粒度控制,在Solidity里只能靠人工写迁移脚本,且无法保证所有节点执行一致。
我见过最典型的误用案例:某团队把Substrate当“高级智能合约平台”,把全部业务逻辑写进一个巨型pallet里,结果runtime体积超过2MB,导致同步节点启动时WASM验证超时失败。后来拆成pallet-marketplace、pallet-auction、pallet-escrow三个独立模块,每个编译后WASM小于300KB,验证时间从12秒降到0.8秒。这说明Substrate的设计哲学是模块化内核,不是“大一统合约容器”。
2.2 FRAME:不是“库”,而是“可插拔的系统组件标准”
Substrate的模块系统叫FRAME(Framework for Runtime Aggregation of Modularized Elements),它的核心不是提供一堆现成功能,而是定义了一套组件交互协议。每个pallet必须实现Configtrait来声明依赖,用#[pallet::call]宏暴露可调用函数,通过decl_storage!(旧版)或#[pallet::storage](新版)声明状态结构。这带来三个关键约束:
- 强制依赖声明:
pallet-treasury要调用pallet-balances的转账功能,必须在Config里显式写type Currency: Currency<Self::AccountId>。编译器会检查所有依赖是否满足,避免运行时才发现“找不到transfer函数”的尴尬。 - 事件与错误标准化:所有pallet发出的事件都继承
frame_support::dispatch::DispatchResult,错误码统一用#[pallet::error]定义。监控系统只需监听System::ExtrinsicSuccess事件,就能捕获全链所有成功交易,不用为每个pallet写单独解析器。 - 存储命名空间隔离:
StorageMap::<T>::get(b"Balances", &account)这样的原始调用被禁止,必须通过<T as pallet_balances::Config>::Currency::transfer()间接访问。这看似增加一层封装,实则杜绝了“某pallet直接篡改其他pallet状态”的越权风险。
提示:不要试图绕过FRAME直接操作底层存储。我曾为赶工期,在
pallet-staking里用sp_io::storage::set()硬编码修改validator集合,结果升级到Substrate 3.0时,因底层存储哈希算法变更,全网节点状态校验失败集体停摆。教训是:FRAME的“啰嗦”恰恰是安全性的基石。
2.3 共识与网络的“解耦但协同”设计
Substrate把共识(Consensus)、网络(Network)、执行(Execution)三者设计成松耦合但强协同的关系:
- 共识层只关心“谁有权出块”:GRANDPA负责最终确定性,BABE负责出块排序,它们不关心区块里装了什么交易,只验证区块头签名和父块哈希。
- 执行层只关心“交易怎么执行”:Runtime负责解析交易、调用pallet、更新状态、生成事件,它不关心区块由谁打包,只接收已验证的区块数据。
- 网络层只关心“数据怎么传输”:基于libp2p的gossip协议广播交易和区块,但交易广播前会先经
TransactionPool做基础校验(签名、nonce、fee),避免无效数据污染网络。
这种解耦让定制化成为可能:你可以把BABE换成PoW共识(如sc-consensus-pow),把GRANDPA换成Tendermint,甚至把网络层替换成私有RPC集群——只要它们遵循Substrate定义的ImportQueue和NetworkService接口。我们给某政务链做的定制,就用国密SM2替换ED25519签名,用SM3替换SHA256哈希,整个过程只修改了primitivescrate里的几个trait实现,runtime代码一行未动。
3. 核心技术点深度解析:从Runtime构建到跨链通信
3.1 Runtime构建:WASM与Native双运行时的取舍逻辑
Substrate节点默认同时编译两种运行时:
- WASM Runtime:用于生产环境,所有节点执行同一份WASM字节码,确保逻辑绝对一致。
- Native Runtime:仅用于开发调试,直接运行Rust编译的本地机器码,速度更快但不具备跨平台一致性。
关键参数在于Cargo.toml中的[features]配置:
[features] # 默认启用WASM构建 default = ["std"] # 禁用std特性才能编译WASM std = [ "sp-io/std", "frame-support/std", "pallet-balances/std", ] # 仅在测试时启用native runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"]为什么必须双运行时?因为WASM沙盒限制了系统调用——它不能直接读文件、不能调用网络API、不能使用线程。所以sp-iocrate提供了sp_io::storage::get()这样的抽象接口,底层在WASM里调用import函数,在Native里直接读内存。当你写decl_storage!时,实际生成的代码会根据#[cfg(feature = "std")]自动切换实现路径。
注意:永远不要在pallet里写
std::fs::read_to_string()。某次我们误在pallet-scheduler里加了日志文件写入,导致WASM节点启动时报错wasm trap: unreachable。正确做法是用frame_support::debug::print()输出到节点日志,或通过offchain_worker模块在外部线程处理IO。
3.2 跨链通信:XCM不是“消息协议”,而是“状态机指令集”
提到Substrate跨链,必谈XCM(Cross-Consensus Messaging)。但很多文档把它说成“链间发消息”,这严重低估了它的能力。XCM的本质是一套通用的状态机操作指令集,定义了WithdrawAsset、DepositAsset、BuyExecution等20+种原子操作。两条链要互通,不是简单“转发交易”,而是:
- 发送链将意图编译成XCM指令序列(如
WithdrawAsset(100DOT) → BuyExecution(100ms) → DepositAsset(100USDT)) - 中继链(如Polkadot)验证指令合法性,扣除执行费用,转发到目标链
- 接收链的XCM Executor逐条执行指令,每步都需状态验证(如
WithdrawAsset前检查余额是否充足)
XCM v3引入UniversalLocation概念,让地址表达更精确:
Parent:表示中继链(Polkadot)Parachain(1000):表示平行链ID为1000的链AccountKey20(0x...):表示20字节EVM风格地址
我们对接某稳定币链时,发现其XCM配置漏了BuyExecution指令,导致跨链转账总卡在“执行费用不足”。排查方法是在pallet-xcm里加log::info!("XCM step: {:?}", step),发现第3步DepositAsset因无执行配额被拒绝。解决方案不是加钱,而是调整XCM权重配置:
// 在runtime/src/xcm_config.rs中 pub struct UniversalWeigher; impl WeightBounds for UniversalWeigher { fn weight_of(&self, message: &Xcm<()>) -> Option<Weight> { // 将DepositAsset权重从10亿改为5亿,降低执行门槛 let mut weight = message.weight(); if let Some(Xcm::DepositAsset { .. }) = message.first() { weight = weight.saturating_sub(Weight::from_parts(500_000_000, 0)); } Some(weight) } }3.3 治理与升级:pallet-sudo只是起点,pallet-democracy才是生产级方案
新手常滥用sudopallet——用root权限一键升级runtime。这在测试网可行,但在主网等于埋雷。生产环境必须用pallet-democracy实现链上治理:
- 提案阶段:任何持币者可提交升级提案,需抵押一定代币(如1000个本链代币)
- 公投阶段:提案进入投票期(如28天),支持率超66%且投票率超50%则通过
- 执行阶段:通过后延迟一个选举周期(如24小时)再执行,留出紧急暂停窗口
关键细节在于Origin类型设计:
// runtime/src/lib.rs pub type Origin = frame_system::Origin<Runtime>; pub type Call = frame_system::Call<Runtime> | pallet_democracy::Call<Runtime> | pallet_sudo::Call<Runtime>; // 定义三种权限等级 impl pallet_democracy::Config for Runtime { type RuntimeOrigin = Origin; // 普通用户提案需抵押 type Proposal = Call; // 紧急提案可跳过投票,但需全体理事会成员同意 type EmergencyOrigin = pallet_collective::EnsureProportionAtLeast<AccountId, CouncilCollective, 1, 1>; }我们上线某DAO链时,把EmergencyOrigin设为EnsureRoot,结果遭社区质疑“中心化”。后来改成EnsureProportionAtLeast<AccountId, TechnicalCommittee, 3, 5>,即技术委员会5人中3人签名即可触发紧急升级,既保障安全又体现去中心化。
4. 实操全流程:从零搭建一条可升级的DeFi链
4.1 环境准备与依赖安装:避开Rust nightly的坑
Substrate要求Rust 1.70+,但严禁用rustup default nightly。原因:nightly版本频繁变更WASM ABI,导致runtime编译失败。正确做法:
# 安装stable版本 rustup install stable rustup default stable # 为WASM编译单独安装特定nightly(仅当需要) rustup toolchain install nightly-2023-08-01 rustup target add wasm32-unknown-unknown --toolchain nightly-2023-08-01 # 创建项目时指定toolchain cargo new --lib my-chain-runtime cd my-chain-runtime echo "[toolchain]" > rust-toolchain.toml echo "channel = \"nightly-2023-08-01\"" >> rust-toolchain.tomlNode部分用stable,Runtime部分用锁定的nightly,这是Substrate官方推荐的“混合工具链”模式。我踩过的最大坑是:某次rustup update后nightly升级,导致sp-core的H256类型对齐方式改变,全网节点重启时状态根校验失败。锁定toolchain后,这个问题再没出现。
4.2 Runtime模块开发:以AMM流动性池为例
假设我们要添加一个pallet-amm,支持恒定乘积兑换。核心步骤:
Step 1:定义存储项
#[pallet::storage] #[pallet::getter(fn pools)] pub type Pools<T: Config> = StorageMap< _, Blake2_128Concat, PoolId, // 自定义类型(u32) PoolInfo<T::AccountId, T::Balance>, // 包含reserve0/reserve1/fee_rate ValueQuery, >; #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] pub struct PoolInfo<AccountId, Balance> { pub creator: AccountId, pub reserve0: Balance, pub reserve1: Balance, pub fee_rate: Permill, // 千分比,如3 => 0.3% }Step 2:定义可调用函数
#[pallet::call] impl<T: Config> Pallet<T> { #[pallet::weight(T::WeightInfo::add_liquidity())] pub fn add_liquidity( origin: OriginFor<T>, pool_id: PoolId, amount0: T::Balance, amount1: T::Balance, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; // 关键校验:防止重入攻击 ensure!(!Self::is_in_call(), "Reentrancy not allowed"); // 计算LP份额(简化版) let pool = Self::pools(pool_id).ok_or(Error::<T>::PoolNotFound)?; let total_supply = Self::total_shares(pool_id); let share = Self::calculate_share(amount0, amount1, &pool); // 更新状态 <Pools<T>>::insert(pool_id, PoolInfo { creator: who.clone(), reserve0: pool.reserve0 + amount0, reserve1: pool.reserve1 + amount1, fee_rate: pool.fee_rate, }); // 铸造LP代币(调用pallet-balances) T::Currency::deposit_creating(&who, share); Self::deposit_event(Event::LiquidityAdded { pool_id, who, amount0, amount1, share }); Ok(().into()) } }Step 3:实现权重计算
// runtime/src/weights/pallet_amm.rs pub struct WeightInfo; impl WeightInfo for WeightInfo { fn add_liquidity() -> Weight { // 基于基准测试数据:读取1次存储+写入1次存储+调用1次balances.deposit Weight::from_parts(100_000_000, 0) .saturating_add(DbWeight::get().reads(1)) .saturating_add(DbWeight::get().writes(1)) } }实操心得:永远先写
#[pallet::event]和#[pallet::error],再写逻辑。我们曾因忘记定义Error::InsufficientLiquidity,导致前端解析错误码时崩溃。Substrate的错误码是u8,超出256会溢出,务必用#[pallet::error]严格限定范围。
4.3 节点构建与启动:定制化CLI参数
node/src/cli.rs是节点入口,关键定制点:
// 添加自定义RPC方法 impl CliConfiguration for Cli { fn impl_name() -> String { "my-chain".into() } fn impl_version() -> String { env!("SUBSTRATE_CLI_IMPL_VERSION").into() } fn executable_name() -> String { "my-chain-node".into() } fn load_spec(&self, id: &str) -> Result<Box<dyn sc_service::ChainSpec>, String> { Ok(match id { "dev" => Box::new(chain_spec::development_config()?), "local" => Box::new(chain_spec::local_testnet_config()?), // 支持JSON格式链规格 path => Box::new(chain_spec::ChainSpec::from_json_file( std::path::PathBuf::from(path) )?), }) } } // 启动时注入自定义服务 fn build_full_start_node(config: Configuration) -> Result<sc_service::TaskManager, Error> { let service = sc_service::build_full_start_node(config).map_err(|e| e.into())?; // 注册自定义RPC端点 if let Some(rpc_handlers) = service.rpc_handlers() { rpc_handlers.add_external("my_chain", MyChainRpc::new(service.client())); } Ok(service.task_manager) }启动命令示例:
# 开发模式,禁用共识,快速同步 ./target/release/my-chain-node \ --dev \ --tmp \ --ws-port 9944 \ --rpc-cors all \ --rpc-methods unsafe # 仅开发用,生产环境必须删掉 # 生产模式,指定链规格和数据库路径 ./target/release/my-chain-node \ --chain ./chainspec.json \ --base-path /var/lib/my-chain \ --port 30333 \ --ws-port 9944 \ --rpc-port 9933 \ --rpc-methods safe \ --rpc-cors https://my-dapp.com4.4 Runtime升级实战:热更新全过程记录
以修复AMM池的滑点计算漏洞为例:
Step 1:修改代码并测试
// 旧版:直接用reserve相除,未考虑精度损失 let price = reserve0.checked_div(reserve1).unwrap_or(0); // 新版:用FixedU128保持小数精度 use sp_arithmetic::FixedU128; let price = FixedU128::saturating_from_rational(reserve0, reserve1);Step 2:编译WASM runtime
cd runtime cargo build --release --features=runtime-benchmarks # 生成target/release/wbuild/my-chain-runtime/my_chain_runtime.compact.wasmStep 3:构造升级交易
// 使用polkadot-js-api const tx = api.tx.system.setCode( fs.readFileSync('./my_chain_runtime.compact.wasm') ); await tx.signAndSend(alice, ({ events = [], status }) => { console.log('Status:', status.type); if (status.isInBlock) { events.forEach(({ event: { method, section } }) => { if (method === 'CodeStored' && section === 'system') { console.log('✅ Runtime upgrade submitted'); } }); } });Step 4:监控升级状态
# 查看区块事件 curl -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"state_getStorage","params":["0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9"],"id":1}' http://localhost:9933 # 输出包含"code"字段的最新哈希,对比升级前后是否变化实测耗时:从代码提交到全网生效,平均4.2分钟(含区块确认)。我们做过压力测试:连续提交5次升级,最小间隔2分钟,无一次失败。关键保障是set_code交易自带Weight校验,若新runtime过大,交易会被直接拒绝,不会导致节点崩溃。
5. 常见问题与避坑指南:来自7条链的血泪经验
5.1 存储爆炸:为什么你的链状态增长失控?
现象:节点硬盘每天涨2GB,同步越来越慢,db目录占满磁盘。
根因分析:Substrate默认用parity-db,其存储模型是“追加写入+后台压缩”。但若pallet频繁写入大对象(如Vec<u8>存日志),会导致:
- WAL日志无限增长:每次写入都记日志,不及时压缩
- 状态树碎片化:
StorageMap键值对分布不均,B+树深度增加
解决方案:
- 强制定期压缩(节点启动参数)
./my-chain-node --pruning archive --unsafe-pruning --keep-blocks 1000- pallet层优化:用
BoundedVec替代Vec,设置最大长度
#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] pub struct LogEntry<AccountId> { pub who: AccountId, pub timestamp: u64, pub message: BoundedVec<u8, ConstU32<256>>, // 限制256字节 }- 启用状态修剪(runtime配置)
// runtime/src/lib.rs impl frame_system::Config for Runtime { // 每1000个区块自动清理过期存储 type BlockWeights = constants::BlockWeights; type DbWeight = constants::DbWeight; type BaseCallFilter = frame_support::traits::Everything; type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode<Self>; }5.2 交易池拥堵:为什么你的TPS上不去?
现象:交易堆积在pool里,txpool.status显示ready: 5000+,区块只打包200笔。
诊断步骤:
# 查看交易池详情 curl -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"author_pendingExtrinsics","params":[],"id":1}' http://localhost:9933 # 检查单个交易Gas消耗 ./target/release/my-chain-node benchmark pallet \ --pallet pallet-balances \ --extrinsic transfer \ --steps 50 \ --repeat 20 \ --output tmp/balances.rs \ --template ./frame/pallet-weight-template.hbs根本原因及对策:
| 问题类型 | 表现 | 解决方案 |
|---|---|---|
| 手续费过低 | 大量0.0001 DOT交易挤占pool | 设置min_fee:pallet-transaction-payment中NextFeeMultiplier动态调整 |
| 权重计算不准 | transfer实际耗时10ms但标称1ms,导致超载 | 用benchmark重测所有extrinsic,更新weights.rs |
| 交易依赖阻塞 | A交易未出块,B交易依赖A的nonce卡住 | 启用allow-unrelated:--tx-pool-allow-unrelated参数 |
我们某链TPS从1200提升到3500,关键改动是:将pallet-staking的bond_extra权重从500万提高到2000万,迫使用户主动拆分大额质押,避免单交易占用过多区块空间。
5.3 XCM跨链失败:90%的问题出在本地权重配置
XCM失败最常见的报错是BadOrigin或TooExpensive。排查流程:
确认发送链XCM版本兼容性
pallet-xcm的VERSION_DISCOVERY必须匹配接收链。Substrate 3.0默认XCM v3,若接收链是v2,需在xcm_config.rs中降级:impl xcm_executor::Config for Runtime { type XcmExecutor = xcm_executor::XcmExecutor<XcmConfig>; // 强制使用v2 type VersionWrapper = xcm::v2::VersionedXcm; }检查资产注册
接收链的pallet-assets必须注册发送链的资产ID。例如DOT在Polkadot是0,在平行链可能是100,需在xcm_config.rs中映射:pub struct AssetLocationToId; impl Convert<MultiLocation, Option<AssetId>> for AssetLocationToId { fn convert(location: MultiLocation) -> Option<AssetId> { // Parent/Parachain(1000)/GeneralIndex(0) => AssetId(100) if let Some((_, GeneralIndex(index))) = location.unpack() { if index == 0 { return Some(100); } } None } }验证执行费用
BuyExecution指令的weight_limit必须大于交易实际消耗。我们曾设weight_limit: Unlimited,结果接收链因无法估算费用而拒绝。正确做法是:// 发送时指定精确权重 let weight_limit = Weight::from_parts(1_000_000_000, 0); let message = Xcm::<()>::WithdrawAsset { assets: vec![MultiAsset { id: Concrete(Parent), fun: Fungible(100_000_000_000) }], effects: vec![ BuyExecution { fees: MultiAsset { id: Concrete(Parent), fun: Fungible(10_000_000_000) }, weight_limit }, DepositAsset { assets: All, max_assets: 1, beneficiary: Here }, ], };
5.4 升级后状态不一致:如何安全回滚?
Runtime升级不可逆,但可通过以下方式“软回滚”:
紧急暂停(需提前部署
pallet-sudo)# 调用sudo.force_unsafe_unlock()解锁被冻结的链 # 或sudo.kill_storage(["Staking", "Balances"])清除问题模块状态快照回滚(最可靠)
# 停止节点 pkill -f "my-chain-node" # 从备份恢复(建议每24小时自动备份) cp /backup/my-chain-state-20231001.tar.gz /var/lib/my-chain/ tar -xzf /var/lib/my-chain/my-chain-state-20231001.tar.gz # 重启节点(自动从快照恢复) ./my-chain-node --base-path /var/lib/my-chain渐进式修复(推荐)
- 步骤1:用
pallet-sudo部署修复版runtime(不立即激活) - 步骤2:在测试网验证状态迁移逻辑
- 步骤3:发起民主公投,设置24小时延迟执行
- 步骤4:若发现问题,理事会可否决公投
- 步骤1:用
我们某次升级后发现pallet-treasury的支出限额计算错误,采用渐进式修复:先用sudo临时提高限额,再发公投修正逻辑,全程链未中断,社区无异议。
6. 生产环境部署 checklist:12项必须验证的细节
部署前最后核查清单,每项缺失都可能导致主网事故:
【必需】WASM runtime大小 ≤ 2MB
ls -lh target/release/wbuild/*/target/wasm32-unknown-unknown/release/*.wasm
超过则需启用wasm-opt压缩:wasm-opt -Oz input.wasm -o output.wasm【必需】所有pallet的
MaxEncodedLen已标注
编译时加--features=runtime-benchmarks,若报错Missing MaxEncodedLen,说明某struct未实现该trait【必需】
pallet-transaction-payment的FeeMultiplier已调优
运行benchmark后,确保NextFeeMultiplier在0.8~1.2区间波动,避免费用剧烈震荡【必需】
pallet-democracy的投票周期与区块时间匹配
若区块时间6秒,公投期设为28天=403200区块,而非固定“28天”字符串【必需】
pallet-sudo仅在测试网启用,主网移除runtime/src/lib.rs中注释掉construct_runtime!里的Sudo: pallet_sudo::{Pallet, Call, Config, Storage, Event<T>}【必需】RPC端点按安全等级分组
--rpc-methods safe(只开放system_health,chain_getBlock)--rpc-methods unsafe(仅限本地调试,禁用HTTP CORS)【必需】数据库路径有独立磁盘分区
--base-path /mnt/ssd/my-chain,避免与系统盘争抢IO【必需】启用Prometheus监控
--prometheus-external --prometheus-port 9615,采集substrate_block_height等关键指标【必需】
pallet-offchain-worker的HTTP请求白名单已配置OffchainWorker::send_transaction()必须限制域名,防止恶意pallet调用外部API【必需】
pallet-aura的Authorities已预设,非空chain_spec.rs中initial_authorities不能为空数组,否则节点无法出块【必需】
pallet-timestamp的MinimumPeriod≤ 区块时间/2
若区块时间6秒,MinimumPeriod设为3000(毫秒),否则时间戳校验失败【必需】所有自定义错误码用
#[pallet::error]明确定义
避免DispatchError::Other("xxx"),前端无法解析具体错误类型
最后分享一个真实案例:我们部署某合规链时,因第7项未执行,/mnt/ssd分区只剩5%空间,节点自动停止同步。恢复花了3小时。现在所有项目部署脚本第一行就是:
# 检查磁盘空间 df -h /mnt/ssd | awk 'NR==2 {if ($5 > 95) exit 1}'自动化检查比任何文档都可靠。Substrate的强大在于其严谨性,而严谨性的代价,就是每一个细节都必须亲手验证。