
1. 什么是 Substrate它不是“基板”而是区块链的“乐高底盘”如果你最近在技术社区、开发者群或者项目路演里频繁听到“Substrate”别急着去查半导体材料手册——这里说的不是芯片制造里的硅基板也不是印刷电路板上的环氧树脂层。Substrate 是一个开源的区块链开发框架由 Parity Technologies也就是以太坊早期核心团队孵化出的那批人主导设计目标非常明确让构建一条功能完整、可定制、可升级的区块链像搭积木一样简单可靠。我第一次接触它是在2021年帮一家供应链金融公司做底层链选型当时他们提了三个硬性要求要能自己定义资产类型、要支持链上治理投票、还要能和现有ERP系统做细粒度权限对接。我们试过直接改 Cosmos SDK也评估过 Ethereum 的 Layer-2 方案最后发现 Substrate 是唯一能把这三件事“原生集成”而不是“打补丁拼凑”的方案。它的核心价值不在于“多快”或“多去中心化”而在于把区块链里最耗时、最易出错的底层模块——共识引擎、状态存储、网络传输、运行时逻辑、升级机制——全部封装成可插拔的组件。你不需要从零写 PoS 共识算法也不用重造 Merkle Patricia Trie 存储树你只需要在 Rust 里声明“我要用 Aura GRANDPA 组合共识”“我的账户模型沿用 FRAME 中的pallet-balances”“升级策略采用 Wasm 运行时热更新”。这些不是配置项而是编译期就确定的类型安全组合。换句话说Substrate 不是让你“写一条链”而是让你“声明一条链”——就像用 TypeScript 声明接口一样编译器会替你检查逻辑是否自洽、模块是否兼容、升级路径是否安全。对开发者来说这意味着什么意味着你花三天时间就能跑通一条带代币发行、链上投票、区块浏览器接入的测试链意味着你不用再为“如何安全地升级智能合约逻辑”这种问题开三次架构评审会意味着你的业务团队可以看懂 runtime 的 pallet 配置表而不是对着 Solidity 字节码发呆。它解决的不是“能不能做”而是“要不要重复造轮子”和“敢不敢上线后改逻辑”这两个长期困扰联盟链和行业链的真实痛点。尤其适合那些需要快速验证商业模式、但又不能接受公链级妥协比如 Gas 费波动、交易确认不可控、治理话语权不在自己手里的中大型企业技术团队。2. Substrate 的整体设计思路为什么它不像 Ethereum 或 Cosmos2.1 核心哲学运行时即代码而非虚拟机即规则绝大多数区块链框架都遵循“虚拟机中心主义”Ethereum 用 EVM 执行字节码Solana 用 BPFCosmos SDK 则依赖 Tendermint 的 ABCI 接口调用 Go 模块。Substrate 走了一条更激进的路——它把整个区块链的状态转换逻辑State Transition Function直接编译成 WebAssemblyWasm模块这个模块就是所谓的“Runtime”。注意这不是“链上智能合约”而是整条链的共识规则本身。你可以把它理解成Linux 内核的 syscall 表不是硬编码在 C 里而是用 Rust 写好、编译成 Wasm、然后在启动时动态加载进去。这个设计带来三个关键优势第一升级无需分叉。传统链升级要全网节点同步停机、下载新二进制、重启——哪怕只改一行手续费计算逻辑也要经历数小时的协调窗口。Substrate 的 Runtime 升级只要提案通过节点在下一个区块就自动拉取新的 Wasm blob 并校验执行整个过程对用户交易完全透明。我实测过某政务链的证书签发费用调整从提案到生效只用了 2.7 个区块约 4 分钟而旧方案需要提前 72 小时公告停机维护。第二跨链兼容性天然友好。因为所有链的 Runtime 都跑在同一个 Wasm 环境里XCMCross-Consensus Messaging协议才能实现“消息体结构一致、校验逻辑统一、错误回滚机制相同”。这不像 Cosmos 的 IBC 那样需要每个链单独实现轻客户端验证而是靠统一的 Wasm 执行环境兜底。我们曾用同一套 XCM 配置让一条能源链和一条医疗链互通资产中间没改过一行桥接代码。第三调试与测试极度可控。你写的 Runtime pallet 在本地用cargo test就能跑满覆盖率不需要起一整套测试网节点。因为 Wasm 模块是纯函数式执行输入状态哈希 输入交易 → 输出新状态哈希 事件日志全程无副作用。这让我们在开发阶段就把 93% 的逻辑错误卡死在 CI 流水线里而不是等上测试网才发现某个ensure!()宏漏写了括号。2.2 架构分层从底层到应用每一层都可替换Substrate 的架构不是“单体堆叠”而是清晰的四层洋葱模型底层Core Layer提供最基础的抽象包括sp-core密码学原语、哈希函数、sp-runtimeRuntime API、Wasm 执行环境、sp-consensus共识算法接口。这一层几乎不碰业务逻辑全是类型定义和 trait 声明。中间件层FRAME Layer这才是开发者天天打交道的部分。它由一系列标准化的 pallet模块组成比如pallet-balances余额管理、pallet-staking质押、pallet-democracy链上治理。每个 pallet 都遵循统一的宏语法decl_storage!,decl_event!,decl_module!内部状态存取、事件触发、错误定义全部模板化。你不是“继承类”而是“声明 pallet”编译器会自动生成存储映射、事件序列化、RPC 接口绑定。运行时层Runtime Layer把选好的 pallet 组装起来定义它们之间的依赖关系和初始化顺序。比如pallet-staking必须在pallet-balances之后初始化因为质押操作要扣减余额。这个组装过程用 Rust 的construct_runtime!宏完成本质是生成一个类型安全的状态机描述。网络与 CLI 层Node Layer负责 P2P 网络、RPC 接口、区块同步、CLI 工具。Substrate 自带sc-service服务抽象和sc-cli命令行你可以用substrate --dev一键起本地链用substrate --chainpolkadot直接连波卡主网。更重要的是这一层完全解耦——你可以把 FRAME Runtime 换成自己的 WASM 解释器只要实现sp-runtime定义的 trait网络层照样工作。这种分层带来的直接好处是你可以只换 Runtime不动网络层也可以只换共识不动业务 pallet。我们给某银行做的跨境支付链初期用 Aura权威证明快速上线半年后按监管要求切换成 PoAProof of Authority整个过程只改了两行consensus配置Runtime 和 pallet 代码零修改。2.3 与主流框架的关键差异对比维度SubstrateEthereum (Solidity)Cosmos SDK (Go)Polkadot Relay Chain开发语言Rust强类型、内存安全Solidity弱类型、需审计Go简洁但泛型支持晚同 SubstrateRust升级方式Wasm Runtime 热更新合约部署新地址状态迁移复杂链重启需协调所有验证者由中继链统一调度状态存储原生支持 Trie RocksDB / SQLiteEVM 存储SLOAD/SSTORE 开销大KV 存储LevelDB/Badger同 Substrate跨链通信XCM统一消息格式内置校验Layer-2 桥接安全性依赖第三方IBC需各链实现轻客户端XCM 原生支持学习曲线高Rust 类型系统 FRAME 宏中Solidity 语法简单但生态复杂中Go 易上手但 ABCI 抽象难懂极高需理解中继链调度逻辑适用场景需自主控制、高频升级、强合规要求的行业链DApp 快速迭代、DeFi 实验、NFT 发行模块化公链、主权链、IBC 生态链作为中继链承载平行链这张表不是为了说谁“更好”而是帮你判断如果你的项目需要“明年 Q3 上线后年 Q1 就得根据新出台的数据安全法调整 KYC 流程”Substrate 的 Runtime 可升级性就是刚需如果你要做一个 NFT 交易平台追求的是生态工具链成熟度和开发者数量那 Ethereum 的 VercelHardhatAlchemy 组合可能更省力。3. Substrate 的核心细节与实操要点从零搭建一条可用链3.1 环境准备Rust 是唯一入口别想绕开Substrate 官方明确声明不支持任何非 Rust 的 Runtime 开发。这不是技术傲慢而是由其设计目标决定的——内存安全、零成本抽象、Wasm 编译质量Rust 是目前唯一能同时满足这三点的语言。所以第一步必须装好 Rust 工具链# 安装 rustupRust 版本管理器 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh # 添加 wasm32-unknown-unknown 目标编译 Wasm 必需 rustup target add wasm32-unknown-unknown # 安装 cargo-contract可选用于 ink! 智能合约 cargo install cargo-contract提示别用apt install rustc或brew install rust这些包管理器提供的 Rust 版本往往滞后且缺少wasm32-unknown-unknowntarget。rustup是唯一官方推荐方式。接着用 Substrate 官方脚手架创建项目# 安装 substrate-node-template最简模板 cargo install --force --git https://github.com/paritytech/polkadot.git --branch master substrate-node-template # 创建新链比如叫 acme-chain substrate-node-template --name acme-chain --author Your Name --organization Acme Corp这个命令会生成一个标准目录结构acme-chain/ ├── node/ # CLI 和网络层可执行文件 ├── runtime/ # Runtime 层核心业务逻辑 ├── pallets/ # 自定义 pallet可选模板里为空 ├── scripts/ # 部署脚本 └── Cargo.toml # 项目依赖总入口关键点在于runtime/Cargo.toml它定义了这条链用哪些 pallet。默认模板已包含pallet-balances代币、pallet-sudo超级管理员、pallet-timestamp时间戳等基础模块。你要做的不是写新代码而是删掉不用的、加上需要的、调整参数值。3.2 Runtime 配置用宏“声明”你的链而不是“编写”它打开runtime/src/lib.rs你会看到类似这样的代码construct_runtime!( pub enum Runtime where Block Block, NodeBlock opaque::Block, UncheckedExtrinsic UncheckedExtrinsic { System: frame_system::{Pallet, Call, Config, Storage, EventT}, Balances: pallet_balances::{Pallet, Call, ConfigT, Storage, EventT}, Sudo: pallet_sudo::{Pallet, Call, ConfigT, Storage, EventT, Inherent}, } );这就是 Substrate 的魔法所在construct_runtime!是一个过程宏它读取你列出的 pallet 列表自动生成所有 pallet 的存储键前缀避免冲突RPC 方法注册表如balances_getBalance事件订阅通道如Balances.Reward跨 pallet 调用的 dispatch 函数如Balances::transfer内部调用System::inc_consumers你真正要写的是每个 pallet 的配置参数。比如pallet-balances的初始配置impl pallet_balances::Config for Runtime { type Balance Balance; type DustRemoval (); type Event Event; type ExistentialDeposit ConstU128500; type AccountStore System; type WeightInfo weights::pallet_balances::WeightInfoRuntime; type MaxLocks ConstU3250; type MaxReserves ConstU3250; type ReserveIdentifier [u8; 8]; }这里ExistentialDeposit ConstU128500就是“生存保证金”——账户余额低于 500 个代币就会被自动清理。这个值不是随便定的它必须大于单笔转账手续费否则用户转完账就被清空又要小于典型用户日常余额否则频繁触发清理。我们给物流链定的值是ConstU12810_000_000因为他们的运费单位是“分”1000 万“分”≈10 万元足够覆盖一个月运营成本。注意所有ConstU128xxx都是编译期常量不是运行时变量。这意味着一旦链上线这个值就固化在 Wasm blob 里只能通过 Runtime 升级来改。所以首次配置务必反复测算——我们曾因低估手续费在测试网跑了两周才发现ExistentialDeposit设得太低导致大量测试账户被误删重放交易时状态不一致。3.3 自定义 Pallet从“复制粘贴”到“精准手术”Substrate 的强大在于它鼓励复用但绝不强制照搬。pallets/目录就是为你准备的“手术室”。假设你要加一个“电子合同存证”功能标准 pallet 里没有但pallet-contract太重它带完整 EVM 兼容pallet-timestamp又太轻只提供时间不存证。这时你应该先找相似 palletpallet-utility批量操作、pallet-transaction-payment手续费管理、pallet-treasury资金池都是很好的参考。用grep -r deposit_event查它们怎么发事件用grep -r decl_storage看存储结构怎么定义。用 macro 快速生成骨架# 在 pallets/ 目录下 cargo new --lib pallet-contract-proof在pallet-contract-proof/src/lib.rs里严格遵循 FRAME 模式#[frame_support::pallet] pub mod pallet { use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*}; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config { type Event: FromEventSelf IsTypeSelf as frame_system::Config::Event; } #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct PalletT(_); #[pallet::storage] #[pallet::getter(fn proofs)] pub type ProofsT: Config StorageMap _, Blake2_128Concat, Vecu8, // 合同哈希 (T::AccountId, T::BlockNumber), // 存证人 区块号 ValueQuery ; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum EventT: Config { ProofStored { who: T::AccountId, hash: Vecu8 }, } #[pallet::call] implT: Config PalletT { #[pallet::weight(100_000)] // 权重预估单位是 weight pub fn store_proof( origin: OriginForT, hash: Vecu8, ) - DispatchResultWithPostInfo { let who ensure_signed(origin)?; // 检查 hash 长度防 DOS 攻击 ensure!(hash.len() 32, Error::T::InvalidHashLength); // 存储 ProofsT::insert(hash, (who, frame_system::PalletT::block_number())); Self::deposit_event(Event::ProofStored { who, hash }); Ok(().into()) } } }这段代码里藏着几个实操关键点#[pallet::weight]不是随便写的数字。Substrate 的权重系统会根据实际执行时间反推但首次必须预估。我们用cargo run --release --featuresruntime-benchmarks -- benchmark ...跑基准测试得到真实值再填进去。StorageMap的 key 类型Vecu8要谨慎。如果存的是 SHA256 哈希固定 32 字节应该用[u8; 32]而不是Vec因为后者在存储中会额外存长度字段浪费空间且影响性能。ensure!宏里的错误类型Error::T::InvalidHashLength必须在#[pallet::error]里提前声明否则编译失败。3.4 启动与调试--dev不是玩具而是生产级调试器很多人以为substrate --dev只是本地玩具其实它是 Substrate 最强大的调试工具# 启动开发链带详细日志 substrate --dev --logruntimedebug,runtime::pallet_contract_prooftrace # 查看实时区块和交易 curl -H Content-Type: application/json -d {jsonrpc:2.0,method:chain_getBlock,params:[0x...],id:1} http://localhost:9933 # 用 polkadot-js/apps 直接连接 http://localhost:9944--dev模式下Substrate 会自动创建 2 个预充值账户Alice/Bob私钥明文打印在终端使用Aura共识单节点出块跳过复杂的 PoS 投票流程开启rpc-corsall允许前端直接调用启用telemetry遥测所有事件、存储变更、错误堆栈实时上报我习惯在启动时加--ws-max-connections1000WebSocket 连接上限因为前端调试时经常开十几个 tab。另外--tmp参数很重要它让链数据存在内存里关掉进程就自动清理避免每次都要手动删~/.local/share/substrate/chains/dev/db。实操心得别信文档里说的“--dev仅供测试”。我们正式上线前的 UAT 环境就是用--dev加上--validator参数跑的——它和生产链唯一的区别只是少了多节点网络拓扑。真正的压力测试是在--dev里用cargo run --release --featuresruntime-benchmarks跑出每秒 3200 笔转账的吞吐量再据此反推生产环境需要多少验证者节点。4. Substrate 的实操全流程从模板到上线的七步法4.1 第一步明确你的链“长什么样”——需求反推 pallet 清单别急着敲代码。先用一张白纸画出你的链必须有的能力账户模型需要普通账户还是企业级多签pallet-multisig必选。资产发行是同质化代币Fungible还是 NFTpallet-assets或pallet-nfts。权限控制谁可以发币谁可以暂停转账pallet-sudo临时pallet-collective长期。数据存证哈希上链就够了还是需要 IPFS 链接pallet-timestamp 自定义 pallet。升级机制是否允许链上投票升级pallet-democracypallet-scheduler。我们给某碳交易所做的链需求清单是✅ 企业账户需营业执照哈希上链→pallet-identity 自定义验证✅ 碳配额代币ERC-20 类似→pallet-assets✅ 配额拍卖定时触发→pallet-schedulerpallet-auction❌ 智能合约业务逻辑全在 pallet 里→ 不装pallet-contract✅ 链上治理交易所会员投票→pallet-democracy这个清单直接决定了runtime/Cargo.toml里要引入哪些依赖以及construct_runtime!里要声明哪些 pallet。少一个功能缺失多一个二进制体积增大、启动变慢、审计成本上升。4.2 第二步定制 Runtime —— 修改配置比写代码更重要拿到模板后首要任务不是加功能而是调参。重点修改三个文件runtime/src/constants.rs定义链的核心常量MAXIMUM_BLOCK_WEIGHT单区块最大计算权重直接影响 TPS。默认2_000_000_00020 亿我们调到5_000_000_00050 亿因为碳交易需要处理大量小额度转账。VERSION语义化版本号1.0.0每次 Runtime 升级必须改否则节点拒绝同步。EXISTENTIAL_DEPOSIT前面讲过必须结合手续费精确计算。runtime/src/lib.rsconstruct_runtime!里的 pallet 列表按依赖顺序排列。比如pallet-staking必须在pallet-balances之后因为质押要扣余额。node/src/service.rs网络层配置pruning设为PruningMode::Archive归档模式否则历史状态查不到审计无法进行。keystore_path指定密钥存储路径生产环境必须指向硬件 HSM 设备挂载点。注意所有const值都参与 Wasm 编译改完必须cargo build --release。我们曾因忘记改VERSION导致测试网节点全部掉线——新 Runtime 的版本号和旧节点不匹配P2P 握手直接失败。4.3 第三步编写第一个自定义 pallet —— 用 ink! 还是原生 pallet这是新手最容易踩坑的决策点。Substrate 提供两条路径原生 palletRust性能最高、权限最大、可直接访问链状态。适合核心业务逻辑如资产发行、治理投票。ink! 智能合约Rust 子集沙盒执行、独立存储、升级灵活。适合实验性功能、用户自定义逻辑如 DAO 规则。选择标准很简单如果这个功能要被其他 pallet 调用或者要影响全局状态如增发代币必须用原生 pallet如果只是用户上传的业务规则用 ink!。我们给医院链做的“处方流转”功能最初用 ink! 实现结果发现药房 pallet 要实时查询处方状态每次都要跨合约调用Gas 消耗翻倍。后来重构成原生 pallet把处方状态存进StorageMapHash, Prescription药房 pallet 直接get()性能提升 8 倍。ink! 的实操要点cargo contract new prescription-contract创建项目合约代码里不能用std只能用core和scale序列化库部署前必须cargo contract build生成.contract文件用polkadot-js/apps的 “Contracts” 标签页上传不是 RPC 调用4.4 第四步集成前端 —— polkadot-js 是事实标准Substrate 没有官方前端框架但polkadot/api库已成为事实标准。安装和使用极其简单npm install polkadot/api polkadot/rpc-providerimport { ApiPromise, WsProvider } from polkadot/api; // 连接本地链 const provider new WsProvider(ws://127.0.0.1:9944); const api await ApiPromise.create({ provider }); // 查询余额 const balance await api.query.system.account(5GrwvaEF...); console.log(balance.data.free.toHuman()); // 10,000 // 发送转账 const tx api.tx.balances.transfer(5Gh...,1000000000000); await tx.signAndSend(alicePair, ({ status }) { if (status.isInBlock) { console.log(成功打包到区块, status.asInBlock.toHex()); } });关键技巧toHuman()方法把 u128 数字转成带逗号的字符串toBigInt()转成大整数别直接.toString()会丢失精度。signAndSend的回调里status.isInBlock表示已上链status.isFinalized表示已被最终确认通常 64 个区块后金融场景必须等isFinalized。前端不要自己算签名用polkadot/keyring生成keyring.addFromUri(//Alice)私钥永远不离开浏览器内存。4.5 第五步压力测试与性能调优 —— 别信理论值要测真实负载Substrate 的 TPS 不是固定值它取决于单区块权重上限MAXIMUM_BLOCK_WEIGHT交易平均权重weight注解网络延迟影响区块传播速度硬件性能CPU 主频比核心数更重要我们的标准测试流程用subport工具生成 10 万个测试账户和密钥写 Python 脚本并发发送balances.transfer交易每秒 500 笔监控substrate --dev --logruntimedebug的日志看Imported #12345是否稳定用polkadot-js/apps的 “Network Stats” 查看实际 TPS 和区块大小实测数据默认配置20 亿权重稳定 TPS 1200峰值 1800调高权重50 亿 SSD 存储稳定 TPS 3200峰值 4100加入pallet-transaction-payment的ChargeTransactionPaymentTPS 下降 15%但手续费计算更准踩过的坑测试时发现 TPS 上不去排查发现是--ws-max-connections默认只有 100Python 脚本开了 500 个连接大量请求排队。改成--ws-max-connections2000后TPS 立刻翻倍。4.6 第六步上线部署 —— Docker 是生产环境唯一推荐方式别用cargo run --release直接跑生产节点。必须容器化FROM parity/substrate:v3.0.0 # 复制编译好的二进制 COPY ./target/release/acme-chain /usr/local/bin/acme-chain # 复制链规范chain-spec.json COPY ./chainspec.json /chainspec.json # 启动命令 CMD [acme-chain, --chain/chainspec.json, --validator, --rpc-corsall, --ws-max-connections2000]关键配置项--chain/chainspec.json链规范文件包含创世区块、初始验证者、Runtime 代码 Hash必须和编译时一致。--validator启用验证者模式否则只是普通全节点。--rpc-corsall生产环境要限制为前端域名如--rpc-corshttps://acme-app.com。--prometheus-port9615暴露 Prometheus 指标监控 CPU、内存、区块高度、未确认交易数。我们用 Kubernetes 部署每个验证者节点配 8 核 CPU 32GB 内存 NVMe SSDStatefulSet 管理PersistentVolume 存储链数据。滚动升级时先kubectl cordon节点再kubectl delete pod新 Pod 启动后自动同步零停机。4.7 第七步Runtime 升级 —— 一次成功的升级胜过十次新链开发这才是 Substrate 的终极价值。升级流程分三步开发新 Runtime在runtime/src/lib.rs里改代码cargo build --release --featuresruntime-benchmarks重新生成runtime.wasm。发起链上提案用sudo账户或democracy投票调用system.set_code传入新 Wasm blob 的 hex 字符串。等待生效提案通过后下一个区块开始所有节点自动下载新 Runtime 并校验。旧 Runtime 的状态无缝迁移到新版本。我们做过最复杂的升级把pallet-balances的ExistentialDeposit从 1000 改成 5000同时新增pallet-asset-manager管理多资产。整个过程从提案到生效耗时 12 分钟期间交易持续处理无一笔失败。重要提醒升级前必须做三件事用cargo test跑通所有单元测试用substrate --dev启动新 Runtime手动测试所有关键交易在测试网如 Rococo上预演观察节点日志是否有Runtime error或Storage migration failed5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表高频故障与定位方法现象可能原因排查命令解决方案节点启动报错Failed to initialize the Substrate clientchain-spec.json里的code字段和编译出的runtime.wasmHash 不匹配sha256sum ./target/release/wbuild/acme-chain/acme_chain_runtime.compact.wasm对比chainspec.json里的code重新生成 chainspec./target/release/acme-chain build-spec --disable-default-bootnode chainspec.json交易一直InBlock但不Finalized网络中验证者节点数不足Aura 需至少 1 个GRANDPA 需至少 2 个curl -s http://localhost:9933 -H Content-Type: application/json -d {jsonrpc:2.0,method:author_pendingExtrinsics,params:[],id:1} | jq .result | length查待处理交易数加--validator启动第二个节点或改用--dev模式pallet-contract部署失败报CodeTooLarge合约 Wasm 体积超过默认限制1MBcargo contract build --release查target/ink/xxx.contract大小在runtime/src/lib.rs里增加pallet-contract::Config的MaxCodeSize ConstU322_000_000前端调用api.query.system.account返回null账户从未有过交易状态未初始化curl -s http://localhost:9933 -H Content-Type: application/json -d {jsonrpc:2.0,method:state_getStorage,params:[0x...,0x...],id:1}手动查存储给账户发 1 个代币触发system::on_runtime_upgrade初始化cargo build报错proc-macro derive panickedRust 版本不匹配或frame-support版本和 Substrate 主干不一致rustc --version和cargo tree | grep frame-support用rustup update升级 Rustcargo update -p frame-support同步版本5.2 独家避坑技巧来自三年实战的血泪经验技巧一永远用--dev模式做第一次集成测试别一上来就搞多节点测试网。substrate --dev --tmp启动的链所有日志打到终端错误堆栈完整Wasm 编译失败会直接告诉你哪一行宏错了。我们曾为一个decl_event!里少写了个逗号折腾了两天最后在--dev日志里一眼看到macro expansion错误位置。技巧二存储键命名必须全局唯一哪怕不同 palletSubstrate 的存储是扁平化的 Key-Valuepallet-balances的Account和pallet-staking的Ledger如果都用bAccount作前缀就会冲突。官方 pallet 用bBalances和