资讯动态

Substrate 应用链开发实战:从 Runtime 到 Pallet 的完整复盘

发布时间:2026/9/28 17:36:36 来源:尧图企业网站定制
我第一次被 substrate 这个词狂轰滥炸是在一次技术选型的深夜会议。有人问如果我们想发一条自己的链三个月内能不能跑起来另一个同事头也不抬直接用 substrate 拼一条就行。那句话让我开始重新审视这套框架。后来我花了两周时间把 substrate 从编译到跑通、再到自己写一个 pallet 的整个过程走了一遍期间遇到的问题比想象中多但收获也远超预期。下面这份记录是我的实践复盘不是官方文档的翻译。我会先讲清楚为什么我会选 substrate再把核心概念拆开揉碎最后记录我从编译、运行、写业务模块到排错的全过程。如果你想做自定义链但还没确定技术路线或者已经在官网看过概念、想知道真实使用体验这篇记录应该能帮你少走很多弯路。1. 选型定调为什么我放弃了从零写链最终投向 Substrate1.1 三种搭链方案的现实对比很多人在选型时会想到三种路径从零写一条链fork 一条现成的链或者用 substrate。从零写链看起来自由度最高。P2P 网络、交易池、状态树、共识、RPC、序列化每一个组件都能按需定制。但真走到那一步你会发现这些都是独立的重型课题。比如状态存储不只要弄懂 MPT还得考虑数据库读写性能和跨实现的兼容性比如网络层光是同步协议和 gossiping 的各种边界情况就能让人崩溃。我见过一些小团队从零写链半年后还在和内存泄漏搏斗业务逻辑一行没写。fork 现有链听起来更快。比特币、以太坊的源码很成熟拿过来改一改就能跑。但分叉的最大问题不是能不能跑而是后续的维护成本。你 fork 的链别人更新了共识层漏洞修复你要手动 cherry-pick你如果要改状态转换函数牵一发动全身社区给你的支持很少因为你已经不是一个标准的链。更致命的是如果链本身不支持无分叉升级你发的每一条链未来任何一次业务规则调整都可能需要硬分叉节点运营方和用户要被反复折腾。substrate 选择了第三条路它把区块链里最难的部分——网络层、数据库存储、RPC、共识、序列化——做成了一套基础设施同时把业务逻辑抽象成 runtime。开发者只写 runtime不用关心节点怎么连、区块怎么广播、数据库怎么落盘。更妙的是因为 runtime 会编译成 Wasm 存在链上它天然支持无分叉升级。这是很多人在选型时愿意压上它的核心原因。1.2 Substrate 让我觉得值的地方等到我自己把 substrate 的代码翻过一遍我发现它比封装好更进一步——它是可以解耦替换的。共识算法可以换网络层可以换甚至存储也可以根据需求调整。默认的模板用的是 BABE/GRANDPA 共识、libp2p 网络、RocksDB 存储但这些都不是写死的。你要做一条给企业内部系统用的联盟链可以把共识换成 Aura要做对实时性要求较高的链可以调整出块参数。这种框架级的灵活性和 fork 链那种改源码改到头皮发麻完全是两个体验。同时其周边生态也为选型提供了不少安全感。FRAME 是 substrate 提供的一组模块化开发库里面已经包含了 Accounts、Balances、Assets、Treasury 等现成的 pallet。如果是做一条链你完全可以像拼乐高一样把这些模块装上去。比如要做一条简单的转账链Balances 这个 pallet 一装账户余额、转账、锁仓这些逻辑都有了不需要重复造轮子。这里放一张我后来的主观对比表维度不追求全面但对选型帮助很大维度从零写链fork 现有链使用 Substrate开发周期很长网络和共识就要大半年前期快后期维护难模板起步几天可跑通 dev 链业务逻辑隔离需要自己设计受限于原有代码结构runtime 独立pallet 模块清晰升级方式通常硬分叉极难无分叉基于 Wasm 的可升级 runtime跨链互操作全自研困难Cumulus XCM 生态支持社区与文档靠自己分叉后官方不背锅文档、论坛、Workshop 较多所以要问我的态度我会说如果只是想快速做链、把精力放在业务上substrate 是当前平衡得最好的方案。你不需要一开始理解所有底层原理模板照样能跑起来等你深入进去了每一层又都有源码可读不会被框架框死。2. 搞清楚 Runtime 和 FRAME才算摸到 Substrate 的门2.1 Client 与 Runtime一条链的硬件和软件我第一次看 substrate 的项目结构时第一反应是代码量怎么这么大后来我意识到看 substrate 的源码不能从上往下扫要先分清楚两个层面——client 和 runtime。client 是链的硬件层负责出块、导入、网络同步、RPC 这些底层机制。它相当于一个节点程序的外壳用户安装一个节点运行起来的就是 client。runtime 是链的业务规则定义了每一条消息进来之后状态会怎么变化。它相当于操作系统里面的业务逻辑模块。client 与 runtime 之间通过确定的接口通信client 把一个区块里的外部消息递给 runtimeruntime 执行完之后把结果返回给 client再由 client 写入数据库。为什么这个设计关键因为 runtime 会被编译成两种形式native 代码只为了性能加速和 Wasm 字节码。Wasm 字节码会直接保存在链上作为当前状态的最终解释版本。当你想升级业务规则时不需要换 client只需要经由链上治理提案把一个新的 Wasm runtime 发布到链上下一个区块开始整条链就会自动切换成新逻辑。这个过程没有硬分叉这就是无分叉升级的机制。为了让我自己彻底理解我当时做了一个很土的类比client 像一部 iPhone 的硬件系统和 iOSruntime 像是你手机上装的业务 App。App 更新时不需要把手机砸了重买线上升级即可。当然这个类比不完美因为 runtime 是所有节点必须统一执行的 App但它对理解为什么能升级很有帮助。2.2 pallet所有业务逻辑都长在这套壳里在 substrate 里runtime 通常不是一个巨大的单体代码而是通过 FRAME 组装出来的。FRAME 提供了一整套宏和模块化约定让开发者把业务逻辑拆成一个一个的 pallet。pallet 可以理解为功能模块。模板默认帮我们装了几个核心 pallet比如 system系统模块、balances余额模块等。你自己写的业务逻辑同样会成为一个 pallet然后被放进 runtime 的construct_runtime!宏里。这样 runtime 的代码就变成了一堆 pallet 的组装清单。看起来像这样construct_runtime!( pub enum Runtime { System: frame_system, Balances: pallet_balances, TodoModule: pallet_todo, } );每个 pallet 都有几个固定组成部分Config定义模块需要配置的参数类型Storage定义链上状态Event定义状态变化时对外通知的信号Error定义可能发生的失败原因Call定义对外可调用的函数。这套结构不是随便设计的而是要保证逻辑可以被编译器检查、被链上存储正确映射、被前端工具自动识别。新手最容易翻车的地方是先写函数。比如我一开始想在 pallet 里写一个保存数据的函数就直接用了普通的fn。但真正能被链上调用、被签名、被验证的函数必须放在#[pallet::call]的 impl 里同时用宏标注。因为只有经过这些宏处理函数才会生成对应的 extrinsic 编码器和链上 dispatch 逻辑。这不是形式主义而是保证客户端发来的字节流和链上逻辑能一一对应。2.3 存储、Event、Error 跟调用之间的关系调用一个 extrinsic从用户视角来说就是发起一笔交易从链的角度看就是 runtime 执行了一段确定性的状态转换。这个过程包括几个环节先通过ensure_signed取得发起人然后校验签名接着读取存储、执行业务计算最后写入存储发出 Event。如果中间任何一步校验不通过就会丢出 Error。存储是很关键的一块。Substrate 的链上存储本质上是一个巨大的键值数据库但它在 pallet 里被封装成了有语义的类型。比如StorageMap就会把 pallet 前缀、模块名、键名合在一起生成一个唯一的 key再通过 hash 存进底层数据库。所以你在写业务时不用关心键值对具体怎么拼只要声明好存储类型就好。但反过来说如果乱用存储会导致存储膨胀、查询变慢。记住一个经验存储里的数据不是免费的每一条都会占用链上空间越少越好能用事件记录历史意义的就不要长期存在链上。Event 和 Error 同样重要。Event 不参与状态存储它是交易执行完的信号订阅它可以方便前端感知链上变化。Error 则是失败的信号。我在早期测试时不太重视这两个东西后来真正接前端的时候才发现没有 Event前端根本不知道什么时候刷新数据Error 定义不明确出了问题只能对着十六进制数据发呆。所以从第一个 pallet 开始就养成每个可调用函数必须同时考虑 Event 和 Error的习惯能省不少排查时间。3. 从模板到首条自定义链的完整实操记录3.1 环境准备和首次编译我用的环境是 Ubuntu 22.04普通台式机 16G 内存。substrate 官方文档推荐用 nightly Rust因为一些运行时宏需要 nightly 才支持。第一步是准备工具链rustup update nightly rustup target add wasm32-unknown-unknown --toolchain nightly这里有一个容易忽略的点cargo 会同时编译 host 工具和 wasm target。如果你没有添加wasm32-unknown-unknown编译到 runtime 时会报target not found或 cannot find wasm-builder。如果你和我一样用 nightly建议把 nightly 设为全局默认否则部分子命令会混乱rustup default nightly接着克隆官方模板。我的建议是直接拉一个 release 分支而不是 master。master 可能正在向新版本演进依赖变动大遇到问题很难查。我使用polkadot-v1.0.0这个 tag 当时是稳定的。git clone -b polkadot-v1.0.0 https://github.com/substrate-developer-hub/substrate-node-template cd substrate-node-template cargo build --release第一次编译会非常久我实测在 16G 内存的机器上大约花了 40 分钟到 1 小时。不要慌这是正常现象。因为 substrate 的依赖树非常大光 Cargo 依赖就有几百个 crate同时wasm-builder会启动一个子构建任务编译 Wasm runtime。如果中途被系统 kill大概率是内存不够具体排查我放在后一章节。编译成功后直接启动开发模式./target/release/node-template --dev --tmp--dev表示使用开发配置单节点自动出块--tmp表示数据存到临时目录退出即销毁测试非常方便。启动后你会看到类似 Imported #123 的日志说明节点已经在出块了。再用浏览器打开 Polkadot JS Apps连接ws://127.0.0.1:9944就能看到链上区块持续增长。这里提一句不要一上来就连接公网测试网先用本地 dev 链把基本流程跑通心态会稳很多。3.2 写一个最小的 TODO 存证 pallet模板跑通后就可以开始写自己的 pallet。我以一个非常简单的TODO 存证为例说明最小闭环是怎么搭起来的。先创建 pallet 目录mkdir -p pallets/pallet-todo/src在pallets/pallet-todo/Cargo.toml里声明依赖然后写src/lib.rs。核心代码大概长这样#![cfg_attr(not(feature std), no_std)] pub use pallet::*; #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: FromEventSelf IsTypeSelf as frame_system::Config::RuntimeEvent; } #[pallet::pallet] pub struct PalletT(_); #[pallet::storage] #[pallet::getter(fn todos)] pub type TodosT: Config StorageMap_, _, T::AccountId, Vecu8, ValueQuery; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum EventT: Config { TodoSet(T::AccountId, Vecu8), } #[pallet::error] pub enum ErrorT { DataTooLong, } #[pallet::call] implT: Config PalletT { #[pallet::weight(10_000)] pub fn set_todo(origin: OriginForT, data: Vecu8) - DispatchResult { let who ensure_signed(origin)?; if data.len() 128 { return Err(Error::T::DataTooLong.into()); } TodosT::insert(who, data.clone()); Self::deposit_event(Event::TodoSet(who, data)); Ok(()) } } }这里的重点是几个宏标签#[pallet::storage]声明存储#[pallet::event]声明事件#[pallet::call]声明可调用函数。外人看这套代码可能会觉得为什么这么多宏满天飞但正是这些宏帮你生成了权重、编码解码、存储接口等一整套东西。你先照着写后面再慢慢理解宏展开后的代码会轻松很多。3.3 注册 pallet 并用前端模板验证写完 pallet不注册进 runtime 是不会生效的。注册步骤一共有三处在runtime/Cargo.toml的 dependencies 里添加pallet-todo { path ../pallets/pallet-todo, default-features false }。在runtime/src/lib.rs里为 Config trait 实现impl pallet_todo::Config for Runtime { type RuntimeEvent RuntimeEvent; }在construct_runtime!中注册TodoModule: pallet_todo,注册完成后再编译一次。编译成功后启动节点然后我建议直接用官方前端模板来验证。克隆substrate-front-end-template安装依赖启动git clone https://github.com/substrate-developer-hub/substrate-front-end-template cd substrate-front-end-template yarn yarn dev打开http://localhost:8000用内置的账本账户连接 Polkadot JS extension你就能在页面上看到一条自定义 extrinsictodoModule.setTodo。填一点数据、签名提交上方事件列表里会冒出todoModule.TodoSet存储查询里也能看到todoModule.todos变长。这个时候一条链的最小闭环才算真正完成节点在跑、业务逻辑在链上可调用、事件和存储可观测。4. 我的真实排错笔记编译、连接与升级中的那些坑4.1 编译时系统卡死的排查链路我第一次编译 substrate-node-template 时编译到一半终端突然报错signal: 9, SIGKILL然后系统像被冻住一样。第一次遇到这种事第一反应是代码有什么问题。后来检查发现是内存不够构建进程被 OOM killer 杀了。我当时用的机器 16G 内存默认没有 swap。substrate 构建时Rust 的cargo会起多个并行编译线程每个线程都要吃内存再加上 Wasm 构建又起一个子进程内存峰值很容易突破 16G。我的排查顺序是用free -m看内存和 swap发现 swap 为 0。用htop看内存消耗进程发现 rustc 有多个并发实例。临时加了 8G swap 文件再降低并行度问题解决。增加 swap 的命令如下sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile注意这只是临时调试用生产服务器上的内存资源配置应该按照实际需要设计。除了加 swap你也可以在构建时限流cargo build --release -j 2-j 2表示同时只编译 2 个 crate内存压力会小很多代价是时间更长。如果你在本地只是跑 dev 链也可以先不用 release用cargo build构建 debug 版本快一些但性能也会差一些。另外一个经验不要看到编译时间长就不停重启。Cargo 有增量编译缓存正常中断后继续构建比全部重编快得多。如果你因为内存不足中断了优先加资源或降并发而不是删掉target目录重来。删target会让我们回到全量编译的漫长节奏里。4.2 RPC 连不上时我做了什么有一次我启动了节点却怎么都连不上 Polkadot JS Apps。页面一直转圈控制台没有报错。我按这个链路排查先确认端口。Substrate 默认 WebSocket 端口是 9944HTTP RPC 是 9933。如果机器之前跑过别的节点端口可能被占用lsof -i:9944查一下。再看 RPC 是否真的在监听。用 curl 发一个简单请求curl -H Content-Type: application/json -d {id:1,jsonrpc:2.0,method:system_health,params:[]} http://localhost:9933如果返回jsonrpc结果说明 RPC 正常如果提示连接被拒就可能是节点没起来或者端口监听有问题。检查 CORS。--dev模式下默认允许所有来源但如果你手动指定了--rpc-cors只填了白名单浏览器跨域就会被拦。开发和测试阶段我一般直接--dev不加额外 CORS 配置。最后再看节点日志输出。如果日志一直停在waiting for the head block说明节点可能卡在状态同步。本地 dev 链不会同步但如果你连了公共测试网就会遇到这个问题。这次排查后我养成了一个习惯每次连接不上先分三层——端口通不通、RPC 返回不返回、链有没有在出块。按这个链路大多数连接问题十分钟内都能定位。4.3 Runtime 升级与存储迁移的警惕点Substrate 引入了无分叉升级这听起来很美但也带来了一个隐藏责任升级之前的存储数据结构如果变了必须写迁移逻辑。我举一个自己差点犯的错。早期 pallet 里我用StorageMap_, _, T::AccountId, Vecu8保存数据后来想扩展成结构体struct Todo { content: Vecu8, timestamp: u64 }。如果把存储定义直接改成新类型旧链上已经存在的存储 key 对应的是旧编码格式的数据新 runtime 读取时会得到错误的数据。最轻的问题是一笔旧数据读不出来严重的可能导致整个 runtime 在某个区块 panic。正确的做法是在升级的 runtime 里实现on_runtime_upgrade遍历旧存储并迁移到新存储结构。这里有几个注意点迁移必须幂等。如果节点重复执行迁移逻辑不能造成双重转换。迁移过程中要控制权重不能因为遍历存储太多导致区块执行超时。在开发链上测试迁移时最好先备份旧链的数据库目录或者用--tmp模式验证确认没有问题再部署到真实环境。实际开发中最让我受用的经验是在开发初期不要过度设计存储结构先用简单的类型快速验证业务闭环。等业务逻辑稳定了再通过严谨的迁移升级到复杂结构。这样能在早期把迭代成本降到最低。4.4 权重为什么要认真对待Substrate 里每个 extrinsic 都需要一个 weight。如果不给编译会报错如果乱给一个固定值链上也是能跑的但这是个隐患。权重可以理解为这条交易预计消耗多少计算资源。它决定两个关键事情交易费的价格和单区块能容纳的 extrinsics 总量。如果你给某个循环操作设置了一个过小的固定权重它有可能在一个区块里超时执行拖慢整个链如果权重设得过大又会卡掉大量正常交易。我刚入门时图省事所有函数都写#[pallet::weight(10_000)]。这在小范围测试中没大问题但要上生产就必须用 Benchmark。Substrate 有frame-benchmarking可以按实际执行路径测出不同输入的耗时生成权重文件。以 TODO 存证为例data.len()不同执行耗时差异会很明显用固定权重就没法表达这种差异。正确的思路是测试阶段可以用固定权重视作占位接近生产环境前用 Benchmark 把每个可调用函数的权重跑出来。这不是可选项而是保护链资源的必要步骤。你会看到仓库里大量src/weights.rs文件都是通过这种方法生成的。5. 从单链到应用链Substrate 生态里的下一步5.1 平行链、共享安全与 XCM 到底是怎么回事当一条 substrate 链跑通之后你自然会问一句我这条链怎么跟外面的链交互这就引出了平行链和 XCM。平行链是插在共享生态里的链。它本质上还是一条独立的 substrate 链但通过 Cumulus 库与中继链连接把最终确定性交给共享的验证节点。这样做的好处是你不需要自己维护一套庞大的验证人网络而可以借用中继链的安全性让链的开销和维护难度大幅下降。当然要成为平行链通常需要通过插槽竞拍或在测试网上获取对应核心资源。XCM 是跨链消息格式。它能让你这条链上的代码发出消息给另一条链也能接收对方发来的资产和指令。我第一次跑通 XCM 的时候最大的感受是原来跨链没有想象中的神秘它也是一套可编程的消息协议。不过 XCM 的复杂性不在格式而在信任模型不同的链之间如何验证消息来源、如何处理资产冻结、怎么回滚失败这些问题都需要明确的配置。对大多数开发团队来说我建议先把单链业务做扎实再考虑去接生态。跨链是放大器不是起点。如果单链上的业务模型还没跑通去接 XCM 只会把问题复杂化。5.2 我给新人的学习路径和个人体会如果让我给一个完全没接触过 substrate 的开发者画一条学习路径大概是这样的先跑通 node-template把 dev 链和前端模板跑起来别着急读源码。手动改模板里的 balances 初始值、出块间隔这些参数观察链上变化建立配置驱动的感觉。自己写一个极简 pallet比如存证、计数器、投票箱把它注册进 runtime。等业务逻辑熟练了再理解 client 和 runtime 的分层、Wasm 升级机制、存储迁移、权重原理。最后可以去看 frame 里内置的 balances、treasury 等 pallet 源码这些是质量很高的参考实现。我个人在实际操作中最大的体会是substrate 最开始最折磨人的不是概念而是编译和宏。一旦你接受框架已经帮你做了大量设计决策这个前提很多代码就好理解了。它不适合那种什么都想从零手搓的人但非常适合想让链快速上线、同时保留后续深度定制空间的团队。最后分享一个小技巧在开发阶段不要一上来就接公共测试网也不要一上来就调网络层和共识层。先在本地 dev 模式把 runtime 逻辑玩到滚瓜烂熟再一步步往生产环境走。这样踩坑的范围会被压缩得很小你也能更快体会到 substrate 真正擅长的部分——把业务逻辑从区块链底层里解放出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑