ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Substrate框架从入门到实战:模块化架构、自定义托盘与运行时热升级

Substrate框架从入门到实战:模块化架构、自定义托盘与运行时热升级 1. 从一条链到一条链工厂substrate 到底在解决什么问题如果你最近两年在区块链底层开发圈子里混一定绕不开 substrate 这个词。我第一次接触它是在一个需要快速验证共识算法的项目里当时团队评估了三条路直接 fork 一条成熟公链改参数、用智能合约在现有链上写逻辑、以及从零搭一条链。前两条路要么受限于原有链的治理和性能天花板要么被虚拟机的执行模型框死最后我们选了第三条而 substrate 就是那条路上绕不开的脚手架。substrate 本质上是一套用 Rust 写的区块链开发框架。注意我的措辞——是框架不是一条链。它不像某些项目那样给你一条现成的链让你改改创世块就上线而是给你一整套模块化的零件共识、网络、存储、交易池、运行时执行环境、治理模块、甚至前端交互的 RPC 接口。你可以把它理解成汽车工业里的“底盘平台”同一个底盘既能造轿车也能造皮卡发动机、变速箱、悬挂都可以按需替换。substrate 提供的正是这样一个底盘开发者要做的是决定装什么发动机、拉什么货。它解决的问题非常具体。传统方式从零写一条链光是 P2P 网络同步、数据库抽象、共识引擎的工程实现就能吃掉一个团队半年以上的时间而且这些代码的稳定性和安全性极难保证。substrate 把这些“脏活累活”全部封装成库开发者只需要关注业务逻辑——也就是“这条链到底要干什么”。更关键的是它把“运行时”这个概念做成了可热更新的 Wasm 字节码这意味着链的逻辑可以在不停机、不分叉的情况下升级。这一点在传统链上几乎是不可想象的你要改共识参数或者加个新功能往往意味着硬分叉和社区撕裂。适合谁来学我认为有三类人最该认真看第一类是准备做应用链的团队尤其是那些业务逻辑复杂、对性能或隐私有特殊要求的场景第二类是想深入理解区块链底层原理的工程师因为 substrate 的代码结构非常清晰读一遍相当于把区块链的各个组件都过了一遍第三类是做跨链或平行链相关产品的开发者因为 substrate 生态里的 XCMP、GRANDPA、BABE 这些机制是理解跨链通信的基础。哪怕你最后不用它读懂它的设计思路也会让你对整个行业的底层架构有全新的认知。2. 核心架构拆解为什么是“框架”而不是“链”2.1 运行时与节点的分离设计substrate 最核心的设计决策是把“节点”和“运行时”彻底分开。节点负责网络通信、区块广播、数据库读写、交易池管理这些“外围”工作而运行时负责状态转换——也就是“一个区块来了账户余额怎么变、合约怎么执行、治理提案怎么投票”。这两者之间通过一个明确的接口通信运行时被编译成 Wasm 字节码节点通过 Wasm 执行器来调用它。为什么要这么设计因为 Wasm 字节码是可以存在链上的。当需要升级运行时逻辑时只需要发一笔特殊的交易把新的 Wasm 字节码写进链的状态里下一个区块开始所有节点就会用新代码执行。整个过程不需要重启节点不需要硬分叉甚至不需要所有节点同时升级。我实测过这个流程从提交升级提案到全网生效在测试网环境下大约几分钟就能完成而且旧节点会自动同步新代码。这种“链上治理 热升级”的能力是 substrate 区别于绝大多数区块链框架的杀手锏。但这里有个坑要注意Wasm 执行是有性能损耗的。虽然 substrate 用了 wasmi 和 wasmtime 两种执行器并且支持“原生执行”作为优化——也就是如果节点的原生代码版本和链上 Wasm 版本一致就直接跑原生代码否则回退到 Wasm 解释执行。这个机制叫“执行策略”默认是NativeElseWasm。但在实际生产环境里我建议把策略设成Wasm或者Both因为原生执行虽然快但一旦出现原生代码和 Wasm 行为不一致的情况就会导致共识失败。这个坑我在早期版本里踩过当时因为浮点数精度问题原生和 Wasm 算出了不同的结果直接导致节点卡死。2.2 模块化托盘像搭积木一样拼出一条链substrate 的另一个精髓是“托盘”机制。托盘就是功能模块每个托盘定义了一组存储项、可调用函数、事件和错误类型。比如pallet-balances管余额pallet-staking管质押pallet-governance管治理。你要做一条链就是把需要的托盘挑出来在运行时的construct_runtime!宏里组装起来。这个设计的好处是复用和隔离。复用不用多说标准托盘经过大量测试和审计比自己写的靠谱得多。隔离则体现在存储层面每个托盘有独立的存储前缀托盘之间的存储不会冲突。但这里有个经验之谈托盘之间的耦合要小心。比如pallet-staking依赖pallet-balances来锁定代币如果你把 balances 的某些参数改得太激进staking 可能会因为余额不足而无法正常工作。我在一个项目里把ExistentialDeposit设得太高结果导致很多小额账户无法参与质押排查了半天才发现是托盘间的隐式依赖。另外托盘的组合顺序在construct_runtime!里是有讲究的。虽然宏会帮你处理大部分依赖但某些托盘的事件和钩子执行顺序会影响最终结果。比如pallet-session和pallet-staking的on_initialize钩子如果顺序反了可能导致质押奖励计算错误。这个在官方文档里没有明确写是我在调试一个奖励分发异常时通过日志对比发现的。2.3 共识层的可插拔设计substrate 默认提供了几种共识组合Aura 做区块生产GRANDPA 做最终性确认或者 BABE 做区块生产GRANDPA 做最终性。Aura 是简单的轮询出块适合联盟链或测试网BABE 是基于 VRF 的随机出块更适合公链场景。GRANDPA 则是一个拜占庭容错最终性小工具它不负责出块只负责对已经产生的区块进行最终确认。为什么要把出块和最终性分开因为这两个问题的性质不同。出块需要快速、持续对延迟敏感最终性需要安全、不可逆对一致性要求极高。分开之后出块可以很快最终性可以稍慢但绝对可靠。GRANDPA 的最终性延迟通常在 2-3 个区块左右也就是说一个区块产生后大约十几秒就能获得最终确认。这个速度在 BFT 类共识里算是相当不错的。但如果你要做一条对最终性延迟极其敏感的链比如高频交易场景GRANDPA 可能就不够快。这时候可以考虑换成pallet-aura加一个自定义的最终性小工具或者直接用pallet-babe配合更激进的参数。不过我要提醒一句共识参数不是拍脑袋调的。出块时间、最终性阈值、验证人数量这三个变量是相互制约的。出块时间越短网络传播压力越大验证人越多最终性确认需要的通信轮次越多。我见过一个项目把出块时间设成 2 秒验证人设了 100 个结果网络常年处于拥堵状态最终性延迟反而比 6 秒出块、20 个验证人的配置还高。3. 从零搭建一条 substrate 链完整实操流程3.1 环境准备与依赖安装先说环境。substrate 对 Rust 版本有要求我写这篇文章时稳定版是 1.75 以上。安装 Rust 用 rustup 最省事但要注意把wasm32-unknown-unknown这个 target 加上因为运行时需要编译成 Wasm。命令很简单rustup target add wasm32-unknown-unknown然后装一些系统依赖。在 Ubuntu 上需要build-essential、clang、libssl-dev、protobuf-compiler这些。macOS 上需要 Xcode 命令行工具和openssl。Windows 用户我强烈建议用 WSL2原生 Windows 编译 substrate 的坑太多尤其是路径长度限制和文件锁问题我试过三次都卡在编译阶段换 WSL2 后一次通过。接下来是获取模板。substrate 官方提供了substrate-node-template这是一个最小可运行的链模板包含 Aura GRANDPA 共识、balances 托盘、sudo 托盘和一个简单的前端。克隆下来之后第一件事是编译cargo build --release这个编译过程非常漫长我第一次编译用了将近 40 分钟机器是 8 核 16G 内存。如果你内存小于 8G可能会在链接阶段被 OOM killer 干掉。解决办法是加 swap 或者用cargo build --release -j 4限制并行任务数。编译完成后用./target/release/node-template --dev启动一条开发链你会看到节点开始出块每 6 秒一个日志里会打印区块高度和交易数量。注意开发链用的是--dev模式数据存在临时目录每次重启都会清空。如果要保留数据用--base-path指定一个持久化目录并且去掉--dev换成--chain local。3.2 添加自定义托盘一个“留言板”功能光跑通模板没什么意思我们加一个自定义托盘来理解整个开发流程。假设我们要做一个链上留言板任何用户都可以支付一定押金后留言留言内容存在链上押金在删除留言时退还。首先在pallets目录下新建一个pallet-message-board结构如下pallets/message-board/ ├── Cargo.toml └── src/ └── lib.rs在lib.rs里定义托盘。核心是#[pallet::storage]定义存储、#[pallet::call]定义可调用函数、#[pallet::event]定义事件。存储我用一个StorageMap键是留言 ID值是留言结构体包含作者、内容哈希和押金金额。可调用函数有两个post_message和delete_message。post_message的逻辑是检查用户余额是否大于押金加 ED从用户账户扣掉押金转入托盘账户生成留言 ID存入存储触发事件。delete_message的逻辑是检查调用者是否是留言作者从托盘账户把押金转回作者删除存储项触发事件。这里有个细节要注意托盘账户的地址生成。substrate 里每个托盘都有一个“账户 ID”是通过托盘名称的哈希生成的。在Configtrait 里需要定义PalletId然后用T::PalletId::get().into_account_truncating()来获取账户。这个账户没有私钥只能由托盘逻辑控制所以是安全的。但如果你忘了在construct_runtime!里给托盘分配PalletId编译会报错。写完托盘后在运行时的lib.rs里注册。先加依赖然后在construct_runtime!宏里加一行MessageBoard: pallet_message_board。最后在Config实现里把RuntimeEvent、Currency、PalletId这些关联类型填上。编译通过后启动链用 Polkadot.js 前端连接就能在“开发者 - 交易”里找到messageBoard.postMessage这个 extrinsic填上内容哈希和押金就能发交易了。3.3 运行时升级不停机换逻辑刚才说的留言板托盘如果上线后发现 bug比如押金计算错了怎么办传统链只能硬分叉substrate 可以热升级。流程是这样的修改托盘代码重新编译 Wasm然后用sudo托盘开发链上或者治理提案生产链上提交system.setCode交易把新的 Wasm 字节码写进去。具体操作先编译出 Wasm 文件路径在target/release/wbuild/node-template-runtime/node_template_runtime.compact.compressed.wasm。然后用 Polkadot.js 的“开发者 - 链状态 - 提交交易”找到system.setCode把 Wasm 文件内容传进去。如果是 sudo 链直接 sudo 调用如果是治理链需要走提案、投票、执行流程。我实测下来从提交到生效大约需要两个区块的时间。第一个区块打包交易第二个区块执行升级。升级完成后链上存储不会丢失因为存储布局没变。但如果你改了存储结构比如给留言结构体加了一个字段那就需要做存储迁移。substrate 提供了on_runtime_upgrade钩子可以在升级时执行迁移逻辑。这个钩子非常关键我见过一个项目忘了写迁移升级后所有旧留言都读不出来因为新代码按新结构解析旧数据直接 panic。提示运行时升级前一定要在本地测试网完整跑一遍升级流程包括存储迁移。测试网和主网的存储状态可能不同本地测试能发现大部分问题。4. 踩坑实录那些文档里不会写的经验4.1 编译与依赖问题速查substrate 的编译问题能占新手 80% 的时间。我整理了一个速查表问题现象可能原因解决办法编译到wasm-builder卡住网络问题导致下载 Wasm 工具链失败设置WASM_BUILD_TOOLCHAIN环境变量或手动下载工具链放到缓存目录链接阶段 OOM内存不足加 swap或用-j 2限制并行编译任务数duplicate lang item错误Rust 版本不匹配用rustup override set锁定项目目录的 Rust 版本Wasm 编译报wasm32-unknown-unknown找不到target 未安装rustup target add wasm32-unknown-unknown运行时升级后节点 panic存储迁移缺失或错误检查on_runtime_upgrade钩子用try-runtime工具验证try-runtime这个工具我要特别提一下。它可以在不启动完整节点的情况下模拟运行时升级并检查存储迁移是否正确。命令是cargo run --release --features try-runtime try-runtime --runtime ./target/release/wbuild/... on-runtime-upgrade live --uri ws://localhost:9944。这个工具救过我两次一次是发现迁移逻辑漏了一个存储项另一次是发现迁移后的数据类型不匹配。强烈建议每次升级前都跑一遍。4.2 性能调优的实战参数substrate 的性能调优主要围绕几个参数出块时间、交易池大小、数据库缓存、Wasm 执行策略。出块时间默认 6 秒改成 3 秒能提升吞吐量但对网络要求更高。交易池默认最多 8192 笔交易如果链上交易量大可以调到 16384 或更高但内存占用会上升。数据库缓存默认 1024MB对于出块节点建议调到 2048MB 以上因为出块节点需要频繁读写状态。Wasm 执行策略我前面提过生产环境建议用Wasm或Both。Both会同时执行原生和 Wasm 并对比结果性能损耗大约 30%但能提前发现不一致问题。如果你的链对性能极其敏感可以用NativeElseWasm但一定要确保原生代码和 Wasm 代码完全同步每次升级都重新编译原生代码。还有一个容易被忽略的参数是--wasm-execution。默认是interpreted也就是解释执行 Wasm速度较慢。可以改成compiled用 wasmtime 的 JIT 编译速度能提升 5-10 倍。但 JIT 编译有预热时间节点刚启动时第一个区块的执行会慢一些。我实测下来对于出块节点compiled模式能把区块执行时间从 200ms 降到 30ms 左右效果非常明显。4.3 常见问题与排查思路问题一节点不同步日志显示“Peer disconnected”。这通常是网络问题。先检查防火墙是否放行了 30333 端口P2P 端口和 9944 端口RPC 端口。如果端口没问题可能是 bootnode 地址失效了。substrate 模板里默认的 bootnode 是官方测试网的如果你做的是自己的链需要在链规格文件里配置自己的 bootnode。问题二交易提交后一直 pending。检查交易池是否满了或者 nonce 是否冲突。substrate 的交易池是按 nonce 排序的如果前一个 nonce 的交易卡住后面的都会排队。可以用author_pendingExtrinsicsRPC 接口查看待处理交易。如果是 nonce 冲突等前一个交易打包或超时后会自动恢复。问题三运行时升级后前端报“元数据不匹配”。这是因为运行时的元数据变了前端需要重新获取。Polkadot.js 前端通常会自动刷新但如果没刷新手动断开重连即可。如果是自定义前端需要调用api.rpc.state.getMetadata重新获取元数据并重新生成类型。问题四GRANDPA 最终性停滞。这通常意味着验证人节点之间通信出了问题。检查验证人节点的时钟是否同步GRANDPA 对时间敏感时钟偏差超过阈值会导致投票无法达成。另外检查验证人是否在线如果超过 1/3 的验证人离线最终性就会停滞。可以用grandpa_roundStateRPC 接口查看当前轮次状态。5. 生态工具链与扩展方向5.1 开发调试工具推荐除了前面提到的try-runtime还有几个工具值得一用。substrate-api-client是一个 Rust 写的轻客户端库适合写自动化测试脚本。polkadot-js/api是 JavaScript 的 API 库前端开发必备。subxt是 parity 官方维护的 Rust 库用于与 substrate 链交互类型安全做得很好。调试方面RUST_LOG环境变量可以控制日志级别。比如RUST_LOGdebug会打印详细日志RUST_LOGruntimedebug只打印运行时的调试信息。我调试托盘逻辑时通常用RUST_LOGruntimetrace能看到每个 extrinsic 的执行细节。但要注意trace 级别日志量极大生产环境千万别开。还有一个神器是polkadot-launch可以一键启动多个节点组成本地测试网。配置一个 JSON 文件指定节点数量、出块模式、初始余额等然后polkadot-launch config.json就能拉起一个完整的测试网。我做跨链测试时经常用它省去了手动配置多个节点的麻烦。5.2 从单链到平行链的扩展路径substrate 的终极形态是平行链。平行链是连接到中继链上的独立链共享中继链的安全性同时保持自己的独立性和可定制性。要把一条 substrate 链变成平行链需要做几件事接入 Cumulus 库实现 collator 节点逻辑配置 XCMP 通道以及通过插槽拍卖或平行线程的方式获得中继链的插槽。Cumulus 是 substrate 生态里专门做平行链的库它把 collator 的逻辑封装好了开发者只需要关注自己的运行时。XCMP 是跨链消息传递协议允许平行链之间发送消息。这个协议目前还在演进中但基本的 HRMP 通道已经可用了。HRMP 是 XCMP 的简化版消息通过中继链转发虽然效率不如原生 XCMP但实现简单适合早期项目。如果你不打算做平行链substrate 也可以做独立链也就是 solo chain。独立链有自己的验证人集合和共识不依赖中继链。适合联盟链、企业链或者测试网。独立链的部署更简单不需要插槽拍卖但安全性需要自己保证。我的建议是如果目标是公链且需要共享安全性走平行链路线如果是企业内部使用或者测试验证独立链足够了。5.3 我个人的一些经验体会做了几个 substrate 项目之后我最大的体会是不要试图一次性把所有功能都做进去。substrate 的托盘机制很灵活但灵活也意味着容易过度设计。我见过一个项目在 MVP 阶段就集成了十几个托盘结果运行时编译一次要 20 分钟调试起来极其痛苦。正确的做法是先做最小可用版本只保留核心托盘等业务验证通过后再逐步添加。另一个体会是存储设计要提前规划。substrate 的存储是链上状态一旦上线就很难改结构。我在一个项目里把留言内容直接存成Vecu8后来想加个索引字段不得不做存储迁移折腾了一整天。如果一开始就用结构体加版本号迁移会简单很多。所以我的建议是任何存储项都加一个版本字段方便后续升级。最后说一个关于社区的经验。substrate 的官方文档虽然全面但更新速度跟不上代码迭代。遇到问题最好的去处是 substrate 的 StackExchange 和 GitHub issues。我很多坑都是在那上面找到答案的。另外parity 的开发者偶尔会在 Discord 上答疑但回复不一定及时。如果项目紧急建议直接读源码substrate 的代码注释写得相当清楚尤其是托盘部分每个函数都有文档注释。这个内容后续还可以这样扩展如果你对跨链感兴趣可以研究 XCMP 的消息格式和路由机制如果对治理感兴趣可以深入pallet-democracy和pallet-collective的实现如果对性能优化感兴趣可以研究 Wasm 执行器的基准测试和调优。substrate 的生态很大每个方向都值得深挖。
返回列表