ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Substrate区块链开发实战:从架构到无分叉升级

Substrate区块链开发实战:从架构到无分叉升级 1. 为什么我现在搭链只推 Substrate而不是从零造轮子1.1 从从零造链到模块化组装的转变我最早做区块链项目时第一拨同事的想法都是从零写一个链自己实现 P2P 网络、自己设计共识、自己写状态树、自己处理交易池。结果项目迭代到第三个月就卡住了因为你会发现真正要花时间的根本不是链本身而是链上的业务逻辑能不能快速验证、能不能安全升级。后来我开始用 Substrate 搭链坦白讲第一次编译通过、一条能出块的链跑起来时整个团队的效率根本不是同一个量级。Substrate 不是一个现成的公链也不是一个智能合约平台。它是一个用来组装区块链的框架出自 Parity 团队之手核心目标就是把区块链节点的所有通用能力抽出来让你聚焦在这条链要做什么上。如果你听过 Polkadot可以把它理解成 Polkadot 的底层技术底座但 Substrate 本身是通用的任何人都能用它做出一条独立链不需要依附任何生态。1.2 Substrate 到底把哪些事提前做完了如果你脑子里对一条链的概念还停留在网络层 共识 状态机的抽象层面下面的清单会更直观。Substrate 已经帮你处理好的通用模块包括点对点网络基于 libp2p多节点发现、广播、同步协议都内置了交易池交易验证、排序、打包的机制已经实现状态数据库底层用 RocksDB 等存储引擎配合带默克尔证明的状态树共识框架接不同的共识引擎就像换插头Aura、GRANDPA、BABE 都支持RPC 接口JSON-RPC 和订阅机制已经暴露好前端直接能连运行时执行器可以把链的逻辑编译成 Wasm在链上完成更新与执行。换句话说Substrate 把一条链的骨架做完了你要做的是往骨架里填业务器官。这种设计最大的好处是你不需要先成为一个网络协议专家也不需要了解每一个数据库坑只要你理解链的状态转换逻辑就能在几周内搭出一条具备生产雏形的链。1.3 它适合谁不适合谁我经常被问到一个问题我们到底要不要用 Substrate我的回答是先想清楚你自己要什么。适合用的场景包括项目方需要在短时间内发布一条有自定义业务逻辑的链团队打算做跨链互操作相关的基础设施或者你只是需要一条可以随时改共识、随时加模块的试验链。Substrate 的模块化特性让这些场景非常自然。不太适合的场景也有你只想发一个简单的同质化代币那直接用现有链的合约就行没必要自己跑节点你的团队完全没有 Rust 基础却要在一个月内交付那学习成本可能比预期高不少或者你需要的只是一个私有化数据库式的账本那么 Substrate 的节点运维复杂度对一个小团队来说确实偏重。2. 核心架构拆解Client 与 Runtime 的边界2.1 外层节点与 Runtime 的职责划分刚开始接触 Substrate 的人最容易犯的一个认知错误是把节点和链的逻辑当成一回事。实际上 Substrate 的设计里有一条非常清晰的边界外层节点Client和运行时Runtime。外层节点负责那些相对稳定、和具体业务无关的事情。节点之间的通信、WebSocket RPC、交易池、数据库、共识引擎的外壳都跑在这一层。这一层的代码即使升级也大多不改变链的规则。Runtime 则是真正的状态转换函数。你写了一个 Pallet定义了链上的存储、调用函数、事件它会被编译成两种东西一种是本机运行的原生代码一种是 Wasm 字节码。这个 Wasm 字节码会存在链上状态里。每当节点需要执行区块中的交易时它会把 Wasm 加载起来执行而不是直接信任本地代码。这么设计就是为了实现后面会讲到的无分叉升级。你可以把外层节点想象成一家餐厅的厨房硬件灶台、冰箱、管线都是固定的。而 Runtime 就是菜单和做菜流程今天可以炒川菜明天可以把菜单从链上换掉换成粤菜。硬件不用重装整个餐厅照样营业。2.2 状态存储与默克尔化的意义Substrate 的链上状态本质上是一个巨大的键值数据库。每个存储项都有一个确定的键值被组织进一棵默克尔树里区块头只保存这棵树的根哈希。为什么要费劲做树化存储核心目的是轻量级验证。一个不运行全节点的客户端只要拿到区块头再用默克尔证明去查询某个存储项的值就能确认这个值确实被这条链认可。这让轻客户端成为可能也让跨链消息验证有了基础。实际写 Pallet 的时候你不需要手动维护这棵树。你只需要声明类似StorageValue、StorageMap这样的存储类型框架会自动把数据挂到树的对应位置。但理解这个底层的每个存储变更都会改变根哈希的机制对排查为什么我的链状态不一致这类问题非常有帮助。2.3 FRAME 与 Pallets积木式开发FRAME 是 Substrate 提供的一套构建 Runtime 的框架它把链上逻辑组织成一个个 Pallet。你可以把 Pallet 理解成积木块每个积木块只做一件职责清晰的事。官方和生态里已经有一批现成的积木处理余额转账的pallet_balances处理权限和超级管理员的pallet_sudo处理链上议会的pallet_collective处理管理密钥的pallet_session等。你自己要做的往往只是写一个或几个承载业务逻辑的 Pallet然后把它们像拼图一样组装进construct_runtime!宏里。FRAME 这套思路最赞的一点是它让开发边界变得非常干净。每个 Pallet 可以单独被测试、单独被审计甚至可以在不同的链之间复用。我做过的几个项目里经常有一个团队内部沉淀的 Pallet 库新项目只需要选配其中的一部分再加少量新逻辑就能快速起链。3. 从零跑通一条开发链环境准备与常用操作3.1 准备 Rust 工具链Substrate 的代码是 Rust 写的所以第一件事是装好 Rust。我建议直接用rustup管理工具链因为它可以随时切换 stable 和 nightly 版本。curl https://sh.rustup.rs -sSf | sh rustup update stable rustup target add wasm32-unknown-unknown这里有一个新手经常漏掉的步骤Substrate 编译时会把 Runtime 编译成 Wasm所以必须添加wasm32-unknown-unknown这个目标。如果你跳过了这一步编译到后半段会因为找不到 Wasm 目标而报错。接下来直接从官方仓库拿模板git clone https://github.com/substrate-developer-hub/substrate-node-template cd substrate-node-template cargo build --release第一次编译的时间会超出你的预期尤其是frame系列依赖比较多。机器配置好一点能省不少心。如果网络状况不太理想构建期间断断续续可以设置好镜像源或者直接把整个依赖目录缓存好。这一步不要急后面构建一次就会越来越快。3.2 本地启动开发节点命令与调试编译完成后启动一个开发链很简单./target/release/node-template --dev --tmp--dev表示用开发模式运行生成一个单验证人的网络出块速度很快非常适合跑业务逻辑。--tmp则表示所有数据只用临时目录节点一停数据就没适合做实验。如果你不想每次重启都清空数据就别加--tmp换成显式指定--base-path例如./target/release/node-template --dev --base-path /tmp/my-chain-data启动后看到的日志里最关键的信息是Local node identity is后面那串 Peer ID以及Listening for new connections on port。真正和前端交互靠的是 RPC 服务默认监听127.0.0.1:9944。3.3 和链交互的工具选择Substrate 生态里最常见的交互工具是 Polkadot.js Apps。打开浏览器在设置里把端点改成ws://127.0.0.1:9944就能连上本地链。你可以在里面转账、发送交易、查看存储也可以调用自定义 Pallet 里的函数。Polkadot.js 这个工具虽然界面看起来有些工程风但它确实能让你把所有链上状态看得清清楚楚。比如你刚才通过 Pallet 写进链的一段数据在 Developer 页面的 Storage 选项卡里输入对应的存储项就能查出来。对于我来说这比一遍遍打 RPC 命令要高效得多。如果你喜欢纯命令行也可以直接调用 JSON-RPC。Substrate 节点暴露的state_getStorage、author_submitExtrinsic这类接口用curl就能测。但日常开发调试我还是推荐先熟悉 Polkadot.js因为它能直观展示区块、事件和交易详情。4. 亲手写一个业务 Pallet从事件到存储4.1 Pallet 的基本骨架写一个 Pallet 其实没有想象中复杂。Substrate 近几个版本已经全面使用#[frame_support::pallet]宏来声明模块整个结构非常固定。下面是我常用的一个最小骨架看起来像这样#![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::pallet] pub struct PalletT(_); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: FromEventSelf IsTypeSelf as frame_system::Config::RuntimeEvent; } #[pallet::storage] pub type NoticeT StorageValue_, Vecu8, OptionQuery; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum EventT: Config { NoticeUpdated(T::AccountId, Vecu8), } #[pallet::error] pub enum ErrorT { EmptyNotice, } #[pallet::call] implT: Config PalletT { #[pallet::weight(10_000 T::DbWeight::get().writes(1))] pub fn set_notice(origin: OriginForT, notice: Vecu8) - DispatchResult { let who ensure_signed(origin)?; ensure!(!notice.is_empty(), Error::T::EmptyNotice); Notice::T::put(notice); Self::deposit_event(Event::NoticeUpdated(who, notice)); Ok(()) } } }这个例子虽然业务简单但把所有核心组件都过了一遍配置项、存储、事件、错误、调用函数。4.2 把 Pallet 组装进 Runtime写完上面这个 Pallet 文件后还需要把它注册进 Runtime。这步通常是新手最容易漏的环节。你需要做三件事第一在runtime/Cargo.toml里添加依赖[dependencies] pallet-notice { version 4.0.0-dev, path ../pallets/notice }第二在runtime/src/lib.rs里实现这个 Pallet 的Configimpl pallet_notice::Config for Runtime { type RuntimeEvent RuntimeEvent; }第三把它加进construct_runtime!宏。比如construct_runtime!( pub enum Runtime where Block Block, NodeBlock opaque::Block, UncheckedExtrinsic UncheckedExtrinsic { System: frame_system, Notice: pallet_notice, } );重新执行cargo build --release然后启动节点在 Polkadot.js 的交易页面里找到notice模块调用setNotice就能看到链上的状态变化和事件输出。4.3 为什么用 Pallet 这种设计很多人会问我直接用智能合约不一样也能实现类似逻辑吗确实可以但 Pallet 和智能合约的区别在于Pallet 的代码是链本身的一部分它可以直接调用系统级的存储和函数不需要经过解释器或虚拟机性能更可控逻辑表达能力也更强。与此同时Pallet 之间的组合是有边界的。每个 Pallet 的存储通过一个独立的命名空间隔离Pallet A 不会随便覆盖 Pallet B 的数据。这样的模块化设计让大团队的并行开发成为可能——你写你的积分模块我写我的身份模块最后在construct_runtime!里汇合即可。5. 共识、治理与无分叉升级一条链的成人礼5.1 共识引擎怎么选Substrate 的共识框架是抽象的没有强行绑死某一种共识。它把问题拆成两个部分区块生产Block Production和区块确定性Finality。我查过模板的默认配置substrate-node-template使用的组合是 Aura 负责出块、GRANDPA 负责最终性。这是很经典的搭配也是开发模式下的靠谱选择。Aura 的逻辑是已知验证人轮流分配出块 slot谁在这个 slot 里出块就由谁说了算GRANDPA 则让验证人们对某个历史区块达成最终性共识一旦最终确认便不会再回滚。共识组件典型使用场景特点Aura开发链、PoA 网络简单确定适合验证人固定的小网络BABE面向 PoS 的公链支持随机 slot 分配适合出块验证人不固定GRANDPA确定性层异步最终性不阻塞出块速度Manual Seal本地测试手动控制出块适合调试如果你的链打算做 PoS可以考虑 BABE 加 GRANDPA如果只是联盟链Aura 加 GRANDPA 就能满足。真正理解这套区分之后你会发现 Substrate 的共识选型就是为了让你在出块速度和安全性之间找到自己的平衡点。5.2 链上治理与升级流程链上的规则不是铁板一块。Substrate 允许你把治理也做成 Pallet。不管是简单的 Sudo 模式还是加入议会、公投的完整版治理都是在链上代码里定义好的流程。开发阶段我习惯先用 Sudo。所谓 Sudo 就是一个超级管理员账号它可以执行任何 Runtime 的权限函数。测试完毕、准备上生产之前再切换到pallet_collective加pallet_democracy的治理组合不然单点权限会变成整个链的安全瓶颈。5.3 无分叉升级的原理与实操意义这是 Substrate 最值得讲的能力也是我当初决定深入使用的关键原因。传统区块链如果业务逻辑出现问题通常需要节点运营商停止节点、拉新代码、重新编译然后重启整个网络这个过程可能引发分叉。但 Substrate 的 Runtime 已经在链上以 Wasm 形式存在升级的行为本质上是一次链上交易它会把新的 Runtime Wasm 代码写入某个存储项。接下来发生的事很有意思那些仍然运行旧客户端代码的节点收到这个包含新 Wasm 的区块后会在执行后续交易时自动切换去执行新的 Wasm。节点运营商不需要更新二进制链的逻辑就已经换了。无分叉升级不仅提升了迭代速度也改变了运营方式。团队可以把一个新的业务函数、一个新的 Pallet 通过治理投票上线而不需要向全网节点下达统一指令。当然这也意味着你要把迁移逻辑写得更谨慎因为你的改动会立刻在所有节点上生效。6. 我踩过的坑版本漂移、编译耗时与调试姿势6.1 版本对齐是最容易翻车的点Substrate 迭代速度很快不同版本之间的宏和接口变化非常频繁。我见过太多人把网上找的一个旧 Pallet 代码直接粘到最新模板里然后面对几十个编译错误完全不知道从哪里下手。我的经验是不要相信看起来差不多的接口一切以模板仓库的Cargo.lock锁定的版本为准。模板仓库对应的 Substrate 依赖版本一般都写在Cargo.toml里。如果你要引入生态里的第三方 Pallet务必检查那个 Pallet 声明的 Substrate 版本是否和你的一致。否则光是frame_support版本不一致引发的 trait 冲突就足够折腾一个下午。比较好的做法是固定依赖。比如把frame-support、frame-system的 git 仓库地址和rev字段锁定确保团队每次构建都用同一套代码。6.2 编译太慢的应对思路Substrate 第一次编译很慢尤其是全量构建十几分钟到几十分钟都很正常。我在一台八核的开发机上首次构建模板大概要五到十分钟如果是更大的项目半小时也不奇怪。针对这个问题我有几个实用的建议。第一尽量用 Linux 或者 macOS 原生环境WSL 的 I/O 性能在大型 Rust 项目里有些吃亏。第二可以考虑装sccache它会把 Rust 编译产物缓存起来多次构建之间能省不少时间。第三日常迭代时尽量只改 Runtime 或本地 Pallet别动不动就改节点代码这样依赖树大部分不会变化。6.3 调试时的实用日志与 RPC 排查Substrate 节点启动时是可以开详细日志的。比如./target/release/node-template --dev --tmp -l runtimedebug-l runtimedebug会把 Runtime 打印的日志显示出来这对确认 Pallet 里的执行路径很有用。你的代码里可以直接用log::info!、log::debug!输出信息节点终端里就能看到不需要额外打断点。另一个我经常用的招数是查询原生 RPC。当交易提交后没有看到预期事件我会优先去 Polkadot.js 的 Explorer 页面看这一笔交易的执行结果再结合state_getStorage查相关存储项。大部分问题都集中在两个方向要么是签名账号没有足够的余额支付手续费要么是ensure!校验没有通过。6.4 团队协作时的一点体会最后分享一个和代码关系不大但很实用的经验。用 Substrate 做项目团队内部最好把平台版本当成和数据库 schema 一样重要的东西看待。每次升级 Substrate 版本都应该当做一个专门任务来做而不是在写业务功能时顺手升级。否则业务代码和新框架接口混在一起排查会让你分不清哪些错误是自己引入的、哪些是升级带来的。还要养成小步提交的习惯。每个 Pallet 独立提交每个 Runtime 变更尽量可验证。跟着模板走先跑通第一个自定义 Pallet再逐步叠加治理、共识定制、多节点部署。这个路径我走了很多次每一步都稳后续踩坑的机会自然就少。
返回列表