ARTICLE DETAIL

资讯详情

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

Substrate区块链开发框架:架构、Runtime与实操指南

Substrate区块链开发框架:架构、Runtime与实操指南 第一次接触 Substrate 这个词我以为是某种底层材质后来才意识到在区块链开发这个圈子里它已经是一个绕不过去的名字。Substrate 是一个用 Rust 编写的区块链开发框架由 Parity 团队推出波卡Polkadot网络本身就是基于这个框架搭出来的。它解决的是一个让人头皮发麻的经典问题以前团队要做一条链网络层、共识层、存储、交易池、虚拟机全都得从零造而现在这些基础组件全部变成了可替换的模块你只需要把精力放在业务逻辑上。如果你正在考虑做应用链、联盟链或者想真正搞懂区块链是怎么运转的这篇文章值得花十分钟看完。1. Substrate 到底解决了什么为什么大厂和独立开发者都在用1.1 一条链的“硬核研发”曾经有多贵先说一个反常识的事实做一个区块链项目大多数工作根本不是在“做链”而是在跟基础设施搏斗。早年很多团队选型时要么 fork 一份比特币代码要么 fork 一份以太坊代码然后开始漫长的改造。改动一个共识参数就得重新编译全节点逻辑写错了全网一起分叉升级一次还得挨个节点去催运维更新客户端。更别提网络层、P2P 协议、数据库状态树、RPC 接口这些东西每一个都是无底洞。我见过不少项目光是把一条链的“骨架”跑起来就花了大半年真正的业务还没开始写。Substrate 的逻辑是把这些基础设施全部“产品化”。它把区块链底层拆成一个个标准组件比如数据库存储使用 RocksDB网络层使用 libp2p交易池、共识引擎、RPC 节点这些都给出了默认实现。Chain 开发者不需要再纠结“我要自己写一个 Merkle 树吗”“P2P 连接断了怎么做回退”这类问题而是直接面对更关键的问题我的状态转换逻辑是什么我的业务要什么数据结构这种抽象方式和“从 CD 上的刻录机软件升级成扫码即用的云盘”是同一个道理底层技术还是那些底层技术但使用门槛降下来了。1.2 Substrate 和波卡的关系以及和 Cosmos SDK 的定位差异很多人第一次听到 Substrate 是因为波卡。Polkadot 本身是一条基于 Substrate 构建的网络Substrate 是这个网络的下层框架而波卡的“平行链”生态里有大量项目也是直接用 Substrate 搭出来的。这里容易混淆的点是Substrate 并不等于波卡。你可以用 Substrate 独立搭建一条私链、联盟链完全不需要接波卡Polkadot 只相当于 Substrate 的其中一个“示例使用者”。换句话说Substrate 是一个更底层的材料波卡只是它造出来的一栋楼。另一个经常被对比的是 Cosmos SDK。两者定位确实很像都是“模块化多链框架”而且都是各自生态的底座。我个人的体感是如果你想要开箱即用、快速跑起来Substrate 的 FRAME 开发体验更“一体化”如果你是做异构跨链的Cosmos 的 IBC 协议更成熟。技术上没有绝对优劣关键看你要什么。如果只是做一条独立链Substrate 可以让你在几天内就有一个带浏览器前端和完整钱包交互的测试网这个“从零到一”的速度真的很能打。2. 架构里最该搞懂的四个字Runtime2.1 一块数据两种状态客户端与 Runtime 的边界区块链本质上是一台分布式状态机每出一个区块所有节点的状态都按照统一的规则发生一次转移。Substrate 把“状态转移函数”这一整块独立出来命名为 Runtime。和它对应的另一块是“链头”Client也就是那些处理网络同步、共识验证、存储读写、RPC 请求的代码。这个边界的价值在于Client 是“怎么跑”的问题Runtime 是“跑什么”的问题。我最开始看 Substrate 文档时最容易绕晕的就是“Runtime 既是一段存储的 Wasm 代码又是一台能执行的原生二进制”。实际上 Substrate 允许同一个 Runtime 被编译成两种形态一种是在链上存放的 Wasm 字节码另一种是集成在节点程序里的原生可执行代码。两者必须通过一组 runtime version 来保证一致性如果不一致节点会优先使用链上的 Wasm 来执行确保网络状态永远不会因为节点软件版本不统一而分叉。这个机制看着抽象实际上就是“代码在链上全网自动同步执行规则”。2.2 FRAME 积木体系System、Balances、Sudo 是怎么协同的Substrate 生态里提到 FRAME指的是“一种高度模块化的 Runtime 构建工具”核心是 Pallet。Pallet 是 Substrate 最小的功能单元一个 Pallet 负责一类业务比如转账、投票、资产管理。你写 Runtime 时本质上就是挑选一堆 Pallet 并拼接它们。框架自带的几个常用 Pallet我实际测试下来就是整个框架的心脏System Pallet最基础的那个定义区块头、账户 ID、Nonce、执行外部交易等没有它其他 Pallet 全跑不起来。Balances Pallet负责余额、转账、锁仓和手续费扣除是所有“经济系统”的底座。Sudo Pallet提供超级管理员权限开发阶段用来强制执行关键操作比如紧急升级配置。Timestamp Pallet把链上时间和真实时间对齐很多依赖时间的逻辑都建立在它之上。这些 Pallet 协同工作的方式有点像乐高积木。每个 Pallet 有自己独立的存储和事件同时通过 trait 对外暴露依赖。比如 Balances 需要知道“什么类型是账户 ID”这个 ID 正是 System 定义的。你不需要自己维护这些依赖关系Sharded FRAME 的 composability 机制会在编译期告诉你哪里有缺口。新手第一次接触时看到T::AccountId、T::Balance这种泛型可能不适应但用熟了之后会发现这是保证代码在不同 Runtime 之间可复用的关键。2.3 Runtime Version升级安全性的底层保障无分叉升级是 Substrate 最被低估的设计之一。传统区块链一旦要改状态转换规则只能硬分叉所有节点运营商必须手动升级至新软件否则这些旧节点会永远停留在旧规则上。而 Substrate 把升级动作变成了“链上的一次普通交易”——把新的 Wasm Runtime 代码作为参数提交链上代码替换后所有诚实的节点会在下一个区块自动切换到新逻辑不需要停机不需要换二进制。这里的关键保护机制就是 Runtime Version。每个 Runtime 上线时会带一组版本号包括spec_version、impl_version、transaction_version。假如新代码把某个交易的编码方式改了而旧客户端还在用旧格式解析网络就会立刻拒绝运行旧 native 代码强制回退到 Wasm 执行。我第一次给一条长跑测试网做链上升级时其实很紧张生怕改坏存储后来发现只要版本号正确重启节点后同步一切正常才真正体会到这个机制省下来多少运维成本。3. 快速实操把 substrate-node-template 跑起来3.1 环境准备与首次编译想要用最有体感的方式理解 Substrate最快路径是跑通官方模板。我是在 Ubuntu 22.04 上配的这里把关键步骤写清楚避免你和我一样踩到环境坑。首先准备 Rust 工具链。Substrate 推荐使用 nightly 工具链但不要手动去rustup default nightly因为不同模板版本对 Rust nightly 的日期要求非常敏感。模板仓库里通常放着rust-toolchain.toml文件它会自动帮你锁定正确的 nightly 版本直接运行以下命令即可curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup show接着安装编译依赖。在 Ubuntu 上我建议一次性装齐sudo apt-get install build-essential clang curl git libssl-dev protobuf-compiler忘记了protobuf-compiler的话后续编译libp2p组件时会报一堆自定义构建错误非常劝退。然后 clone 官方模板并切到稳定 taggit clone https://github.com/substrate-developer-hub/substrate-node-template cd substrate-node-template git checkout tags/v0.9.37 cargo build --release首次编译差不多要 30 到 60 分钟取决于机器配置它会把 600 多个依赖 crate 全部编一遍。建议留出至少 10GB 磁盘空间内存最好在 8GB 以上8GB 会慢到怀疑人生。编译完成后运行这条命令就能启动一条开发链./target/release/node-template --dev --tmp--dev表示使用开发模式预设--tmp表示每次启动都用一个临时目录链上的数据不会保留。这种模式非常适合反复改代码验证重启一下就是一条“出厂状态”的新链。3.2 修改链配置发一个自己的原生代币跑通模板后你应该立刻做的一件事改出你自己的币。很多人一直停留在“能跑起来”的阶段却不知道在哪里改钱和给谁发钱。答案在node/src/chain_spec.rs文件里。模板里有一个testnet_genesis函数它是所有 Pallet 初始状态的集合地。其中balances这一节定义了所有创世账户的初始余额代码大致长这样不同版本字段名略有差异balances: BalancesConfig { balances: endowed_accounts .iter() .cloned() .map(|k| (k, 1 60)) .collect(), },这段代码的意思是endowed_accounts列表里的每个账户都发1 60个最小单位。理解最小单位很重要Substrate 默认的币种精度是 12 位小数写入存储的数值是按最小单位计算的所以1 60就是很大很大的余额。你可以改这个数值也可以改endowed_accounts列表往里面多塞几个自己的测试地址。我这里有一个建议动手改之前先跑一次polkadot-js/apps把区块浏览器拉起来看清模板当前的代币叫UNIT、精度 12再回去改代码反馈会更加直观。重新编译并启动后用浏览器打开https://polkadot.js.org/apps点击左上角选择网络填入ws://127.0.0.1:9944就能连上本地开发链。默认的“Alice”等预置账号会出现在账户列表里余额就是你在chain_spec.rs里分配的数字。3.3 给自己加一个自定义 Pallet存储、事件与 Call模板自带了一个pallet_template这个示例 Pallet 的作用是给开发者一个最低可运行骨架。我第一次真正上手是把模板文件改成一个“记事本”功能允许用户在链上存一句话只有提交者自己能更新。改动并不复杂需要关注三个核心部分存储。在 Pallet 里用宏声明一块数据例如#[pallet::storage] pub type StoredNoteT StorageValue_, T::AccountId, ValueQuery;事件。状态变更时发出通知前端和浏览器才能捕捉到活动例如#[pallet::event] pub enum EventT: Config { NoteStored { who: T::AccountId }, }Call。这是用户能调用的函数入口例如#[pallet::call] implT: Config PalletT { #[pallet::weight(10_000)] pub fn store_note( origin: OriginForT, note: Vecu8, ) - DispatchResult { let who ensure_signed(origin)?; // 存储逻辑 Self::deposit_event(Event::NoteStored { who }); Ok(()) } }写完 Pallet 本身之后还有三步装配工作在runtime/Cargo.toml中声明新依赖在runtime/src/lib.rs中实现Configtrait最后把 Pallet 挂进construct_runtime!宏。这三步漏掉任何一环编译都会直接报错报错信息一般会明确告诉你是“找不到这个 crate”还是“没有在 runtime 中注册”。这个过程我第一次跑会有点手忙脚乱但走完一次之后后面对 Substrate 的掌控感会完全不一样。4. 三条“杀手级”特性值得深入理解的设计逻辑4.1 可插拔共识Aura、GRANDPA、BABE 怎么选很多初学者以为区块链的共识是铁板一块Substrate 最大的理念之一却是共识也可以随意拼接。开发模式下最常用 Aura它逻辑简单预定的验证节点轮流生产区块每个 slot 只允许一个节点出块。这种模式在联盟链和测试网里非常舒服因为出块节奏稳定区块时间基本恒定调试起来很省心。但如果做面向公网的链你就需要考虑对抗随机性。Substrate 里的 BABE 共识使用可验证随机函数VRF决定每个 slot 由谁出块可以出现同一 slot 多个区块的情况安全性更强。而无论是 Aura 还是 BABE它们只解决“谁来出块”的问题“哪个区块最终被确认”则由 GRANDPA 共识负责。GRANDPA 在网络部落中扮演的是裁判角色即使前面出了分叉它也会在若干轮之后对其中一个区块完成最终性确认之后那个区块的所有祖先区块都被视为不可回滚。我在实际项目里的选择经验是只想快速验证业务逻辑直接--dev或 Aura要发测试网给别人体验AuraGRANDPA 组合最稳妥真正面向社区开放前才值得花精力切换到 BABE。这个决定顺序千万不要反过来否则会浪费大量时间在无关紧要的共识调参上。4.2 无分叉升级把新逻辑“种”进链上前面提到 Runtime 可以链上升级现在补一个实际案例。假设我已经上线了一条链币种精度想从 12 位改成 15 位或者想给转账逻辑增加一个小额最低限额。传统做法是发公告、敲定时间点、让全员换节点软件Substrate 的做法是构造一条sudo交易或治理提案触发set_code或“调度升级”逻辑把新的 Wasm Runtime 实例提交上去。升级后所有节点会在区块高度上自动转换执行逻辑。判断这次升级是否正常主要看两点第一新 Runtime 里spec_version必须大于旧版本第二如果存储结构有变化必须有对应的存储迁移代码它会在区块执行的入口统一跑一次。这里有一个我当初特别注意的细节升级并不要求所有节点在同一时间完成软件部署运行旧节点的网络会先用链上 Wasm 执行新代码。所以说“无分叉升级”是把节点软件的版本差异彻底消解掉了这可是原来区块链项目里最让人焦虑的运维噩梦。4.3 民主治理从“开发者说了算”到“社区说了算”Substrate 的治理模块组合起来简直就是一个完整的“网络版人民代表大会”。开发阶段用 Sudo Pallet团队做紧急决策当链逐步成熟可以引入 Democracy、Collective、Treasury 等模块。Democracy 负责公投所有人都可以提交提案持币者投票决定是否执行Collective 提供委员会机制也可以配置快速通道Treasury 管理社区资金池提案人可以从资金池申请开发预算。这些模块最妙的地方在于它们是 Pallet你可以根据需要决定“我的链第一步用几个治理模块”。比如我先只开 Democracy把公投周期设为 3 天等社区队伍扩大再新增 Committee。整个过程不用改链底层代码都是链上操作。我见过不少项目把治理仓促开得很早导致社区投票率太低、决策瘫痪。我自己会建议测试阶段保留 Sudo预生产阶段用“技术委员会公投”混合模式真正进入主网后再把 Sudo 权限移除或交给多重签名委员会。渐进式去中心化远比“一刀切”要稳得多。5. 实操中会遇到的坑和我的解决方式5.1 编译环境里的四个高频报错我前后折腾过不少基于 Substrate 的模板项目最令人崩溃的永远是环境问题不是逻辑问题。下面四类报错出现频率最高我把关键现象和解决命令一起列出来报错现场根本原因解决办法cant find crate for wasm32-unknown-unknownRust 没有安装 Wasm 编译目标rustup target add wasm32-unknown-unknown --toolchain nightly-xxxerror: failed to run custom build command for librocksdb-sys缺少 Clang 和系统依赖安装libclang-devlibssl-devprotobuf-compiler编译时内存被吃掉进程被系统杀掉联动编译资源过多加 swap 或cargo build --release -j 4限制并行度版本不一致导致编译失败nightly 日期与模板要求不匹配删除rust-toolchain.toml外的自定义 override执行rustup show锁定模板指定版本这些坑并不是 Substrate 设计得不好而是 Rust 生态本身的依赖链太长环境惰于排查的话很容易把大量精力烧在这里。我的习惯是在新机器上第一步先跑rustup show然后是系统依赖安装再正式开始编译。顺序对了后面就顺了。5.2 存储变更必须做 Migration这个坑能让你丢数据Runtime 升级不等于“随便替换代码”。如果新 Runtime 里某个 Pallet 的存储结构变了而你没有处理旧的链上数据那么节点在升级后尝试读取旧格式数据时会直接报错严重情况下整条链都无法出块。这个问题的本质是区块链的核心价值就是历史数据不可篡改你升级的是执行逻辑但已有的存储数据还躺在磁盘上双方必须进行“兼容握手”。Substrate 提供了OnRuntimeUpgradetrait允许你在升级代码被执行时手动对旧数据做迁移。举个例子原来StorageValueT, u32现在要变成StorageValueT, u64你需要在 Pallet 里实现一个on_runtime_upgrade函数遍历旧存储并移除旧值同时写入新值。考虑到链上数据量迁移函数必须返回权重告诉系统这次升级消耗了多少计算资源。我一直建议任何涉及存储 schema 变更的升级先在一个长期运行的测试网上演练一遍把旧数据灌进去再升级确认没有 panic 再考虑上生产。等到主网出事的那天再现场 debug那就是彻头彻尾的灾难。5.3 关于学习路线我最后说几句实在话很多刚接触 Substrate 的朋友一上来就想把整个框架源码全部读完这是最容易劝退自己的做法。框架的层次非常多有 FRAME 的宏体系、有 consensus 层、有 network 协议、有轻客户端设施每个方向都能让人钻研几个月。我更推荐一条有反馈感的路线先跑通模版然后照着模板抄一个“记事本”点功能接着把 Balances Pallet 的源码打印出来通读一遍看看一条转账交易底层需要哪些存储、事件和权重处理最后再去碰共识与网络层的源码。等你真正独立实现过一个 Pallet并且做过一次链上升级后对 Substrate 的理解就已经超过了大多数看文档的人。我说的实在话就是框架再好也得靠一个个最小改动的项目真正“喂”出来。我第一次给测试网加自定义 Pallet 时光是construct_runtime!宏里少挂了一个Pallet编译就报了一个完全不知所云的错误后来一行行对着官方示例排查才发现只是顺序写错了。那次之后我再没犯过类似的低级错误。踩坑本身就是学习路径的一部分关键是别慌着绕开多 debug 几次你对框架的心智模型就建立起来了。
返回列表