拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Substrate区块链开发框架解析:模块化设计、Wasm运行时与实战入门

一个词在不同行当里指的东西可以天差地别生物学家看到 substrate 想到的是酶作用的底物材料工程师看到它第一反应是镀膜或者印刷用的基材。但过去这几年如果你在开发者社区搜这个词十有八九指向的是同一个东西——区块链开发框架 Substrate。我第一次接触它是在一个项目里要快速验证一条链的可行性当时被从零写共识、写 P2P、写状态存储那一套搞怕了Substrate 让我第一次觉得搭一条链这件事可以像拼乐高一样有章法。这篇文章就围绕 Substrate 展开说说它到底解决了什么问题、核心架构怎么拆解、一条链怎么从零跑起来以及我实际踩过的一些坑。Substrate 是一个用于构建区块链的框架由 Parity Technologies 开发和维护也是 Polkadot 生态的底层技术基础。它把区块链开发里大量重复的底层工作——网络层、共识、存储、节点客户端——全部做成可复用组件开发者只需要聚焦业务逻辑也就是运行时逻辑。这篇文章适合三类人想了解区块链底层原理但不想啃源码的人准备用 Substrate 做毕业设计或企业原型验证的开发者以及在以太坊系开发里待了很久、想换一条技术路线的工程师。1. 内容整体设计与思路拆解1.1 区块链开发的旧痛为什么大家不想从零造链在没有 Substrate 之前自己搞一条链是什么体验你得先解决节点之间怎么发现对方、消息怎么广播这属于 P2P 网络层然后要决定区块怎么打包、谁有权利出块这是共识层接着是账户体系、交易格式、状态存储每一个模块都是需要投入大量时间的底层系统。以太坊给了大家一条路写 Solidity 智能合约部署上去直接用现成的链。但这条路有天花板合约跑在 EVM 里Gas 机制、执行模型、存储模型都是定死的你想要自定义共识、自定义费用规则、甚至修改交易格式合约层面根本做不到。而比特币那种单用途链就更不用说了脚本语言是刻意保持非图灵完备的。Substrate 的思路就是既要又要它提供一条链上所有底层组件的标准实现让你不必从零开始写 P2P 网络和共识算法同时它把运行时这块设计成完全可定制的状态你的业务逻辑跑在一个 Wasm 虚拟机上想怎么改就怎么改改完可以通过链上治理或者强制升级来更新不用硬分叉。这个设计思路本质上是在做一个区块链操作系统式的抽象。就像你写 Web 应用不会自己去实现 TCP/IP、HTTP 协议和数据库驱动而是依赖一个成熟框架Substrate 对区块链开发做的就是同样的事。它提供一个标准化的节点骨架业务层之外的一切都被剥离出来做成可插拔模块。1.2 为什么是 Wasm Runtime而不是直接跑原生代码Substrate 架构里最核心的决策是把 Runtime 编译成 WebAssembly然后在节点里通过 wasmi 或 Wasmtime 这样的执行引擎跑起来。很多人第一次听到这个设计会问多此一举直接跑原生编译的 Runtime 不是性能更高吗关键在于四个字链上升级。原生二进制文件是编译进节点的要改 Runtime 逻辑就必须替换节点二进制这就意味着所有节点运营者要手动升级软件否则整个网络就会分叉。但你如果把 Runtime 编译成 Wasm 存放在链上的状态里每个节点在出块时就会从链上读取最新的 Wasm 并执行Runtime 升级就变成了一个状态变更天然具备分叉安全性。这也是为什么 Substrate 节点在启动时会有本地原生代码优先但链上 Wasm 版本不相同则执行 Wasm的机制。这个设计为无分叉升级铺平了道路也就是后来 Polkadot 实现 forkless upgrade 的基础。实际开发中runtime 代码改了之后你只需要重新编译、重新部署不用所有的节点运营者都手动拉代码、跑 cargo build。性能方面当然有损耗Wasm 执行和原生执行之间的差异客观存在但 Substrate 框架里有个折中如果本地原生 Runtime 和链上 Wasm Runtime 的版本一致就执行原生代码只有当链上 Wasm 版本更旧或更新时才走 Wasm 执行路径。这样保持了性能上限同时保留了升级安全性。1.3 模块化程度决定了开发效率Substrate 的模块化体现在 FRAME 体系上。FRAME 是一套约定让你把业务逻辑封装成 Pallet模块每个 Pallet 负责一类功能。比如你要搞代币就引入 Balances Pallet要搞治理就引入 Democracy Pallet要搞账户就引入 System Pallet。一个 Pallet 在代码上就是一个 Rust crate包含了存储项、事件、错误、外部函数Extrinsics和运行时 API。这种设计带来的直接好处是你写业务逻辑不需要关心底层存储怎么实现、交易怎么进入区块、区块怎么验证你只需要告诉框架我的业务需要存什么、暴露哪些接口。每个 Pallet 之间通过 trait 解耦可以复用别人的实现也可以替换成自己的实现。我最开始写第一个 Pallet 的时候感觉有点像写一个带状态的后端服务存储相当于数据库表外部函数相当于 HTTP 接口事件相当于日志。一旦建立了这个映射关系理解 Substrate 开发的核心逻辑就不难了。1.4 和以太坊合约开发的实际对比很多从 Solidity 转过来的人最需要理解的差异是在链上谁说了算的问题。你在以太坊上写合约合约是一段部署在链上的代码用户调用它执行逻辑发生在 EVM 里状态保存在合约自己的存储槽里。合约之间调用关系、权限模型都由代码自己控制。但在 Substrate 中业务逻辑直接嵌入链本身。你写的 Pallet 不是部署在链上的合约而是链自身的组成部分。比如你用 Substrate 开发一条链链上的交易格式、区块头结构、账户体系都可以按自己的需求定义它天然知道什么是系统转账、什么是质押、什么是治理。链对自己的规则拥有完全的控制权。这也意味着开发时的调试方式不太一样。Solidity 里你写合约部署后通过 RPC 调用接口状态体现在合约地址上。Substrate 里你写 Pallet编译后直接跑一个节点状态体现在链上前端可以通过 polkadot.js apps 来查看。虽然工具链不同但模式上其实是互通的——RPC 调用对应外部函数调用事件日志对应 Substrate 的事件系统。2. 工具选型解析搭建 Substrate 开发环境2.1 为什么必须要用 RustSubstrate 框架本身是 Rust 写的用 Rust 做开发几乎是唯一选项。这不仅是语言层面的偏好而是有实际技术原因Rust 的所有权模型保证了内存安全避免了很多运行时层面的数据竞争问题零成本抽象让你在写高层业务逻辑时不用担心底层性能被拖垮Cargo 的模块和依赖管理体系对 FRAME 这种大规模的 crate 结构非常友好。如果你是 Rust 新手最开始确实会有点痛苦尤其是生命周期、trait 约束、泛型这些概念。但 Substrate 的 Pallet 开发其实用到的 Rust 特性相对集中主要是 struct、enum、trait、泛型约束以及一些宏。真正需要深入研究 Rust 高级特性的时候并不多。我的建议是先通过 Rust Book 补一下所有权、trait、枚举的基本概念然后直接上手 Substrate在实战中慢慢学。2.2 环境准备的具体步骤Substrate 开发环境最核心的依赖是 Rust 工具链。Substrate 官方推荐使用 Rust 的 nightly 版本因为很多依赖的编译特性需要 nightly 才能开启。搭建步骤大致如下首先安装 rustup这是 Rust 的版本管理器。然后通过 rustup 安装 nightly 工具链curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup update nightly rustup target add wasm32-unknown-unknown --toolchain nightlywasm32-unknown-unknown 这个 target 是编译 Runtime 到 Wasm 必须的忘了安装后面编译会报错。除此之外Ubuntu/Debian 系系统还需要安装编译链工具比如 build-essential、clang、libssl-dev 等。macOS 上则需要 Xcode Command Line Tools。还有一个无形但很实际的坑国内网络环境下Cargo 拉依赖包经常很慢尤其是 Substrate 这种依赖超多的项目首次编译可能需要拉取几百个 crate。建议预先配置一个 crates.io 镜像源比如使用国内镜像同时把 Rust 的 toolchain 网络超时时间调大。配置镜像的一个示例# ~/.cargo/config.toml [source.crates-io] replace-with mirror [source.mirror] registry sparsehttps://mirrors.xxx.com/crates.io-index/这步不涉及任何特殊协议只是国内公共镜像能显著提速。2.3 选模板node-template 还是 substrate-node-templateSubstrate 官方提供了一个最小可运行的链模板substrate-node-template。这是一个非常精简的项目包含了一条链最基本的组件一个可以出块的共识、一个账户系统、一个 balances Pallet。代码量少结构清晰适合用来理解一条链的组成。如果你要做正式的商业项目或复杂的业务链后续一般会基于这个模板扩展或者直接用 substrate 主仓库里的 node 以及 polkadot-sdk 下的框架来搭建。但对于学习和验证概念node-template 是起步的最佳选择。还有一个 front-end-template是一套 React 前端模板可以直接和链交互适合快速做原型演示。2.4 本地开发节点的启动与测试启动一条 Substrate 开发链非常简单。在 node-template 的目录里执行cargo build --release ./target/release/node-template --dev--dev 参数表示以开发模式启动节点会使用预设的开发账号出块速度极快通常一两秒一个块不需要挖矿或者验证人质押。这样本地开发体验和调试智能合约差不多改代码、编译、重跑节点、调接口、看状态。要注意的是--dev 模式默认是监听在 127.0.0.1 的端口是 9944WebSocket和 30333P2P。前端工具通过 WebSocket 连接节点。如果你改过 chain type 或者用了非 dev 模式这些端口和连接参数都要对应调整。3. 核心细节解析与实操要点3.1 一个 Pallet 的标准组成Pallet 是 Substrate 开发的基本单元。用 FRAME 写一个 Pallet代码结构上有几个关键组成部分。以最简单的计数 Pallet 为例它通常会包含以下几个部分Config trait 定义这个 Pallet 对外部系统的依赖比如它可能需要访问账户余额、可能需要触发事件。通过关联类型的方式把可变的部分都抽象出去保证 Pallet 之间的松耦合。Storage 定义链上状态。FRAME 提供了 storage_macro 宏来声明存储项语法类似于声明一组键值对。每个存储项都有固定的键值可以是一个数字、一个结构体、一个映射表。Event 表示链上发生的重要事件。FRAME 会自动生成一个事件枚举你只需要往里面添加字段。事件会在交易执行成功后触发前端可以监听。Error 定义交易失败的返回错误。有了错误枚举外部程序可以精确知道链上发生了什么问题而不是只收到一个通用的失败状态。Extrinsic 是用户能调用的函数。它们通常定义在 call 模块内每个函数都会经过权限检查、参数解析然后写入存储或触发事件。3.2 编写第一个 Pallet实现一个简单计数器为了理解上面说的这些概念实际写一个计数器的 Pallet 是最好的练习。假设我们要做一个任何人都可以增加计数、但只有管理员可以重置的 Pallet。#[frame_support::pallet] 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; type AdminOrigin: EnsureOriginSelf::Origin; } #[pallet::storage] pub type CountT: Config StorageValue_, u32, ValueQuery; #[pallet::event] #[pallet::generate_deposit] pub enum EventT: Config { CountIncremented { who: T::AccountId, count: u32 }, CountReset { who: T::AccountId }, } #[pallet::error] pub enum ErrorT { Overflow, } #[pallet::call] implT: Config PalletT { #[pallet::call_index(0)] #[pallet::weight(10_000)] pub fn increment(origin: OriginForT) - DispatchResult { let who ensure_signed(origin)?; let count Count::T::get().checked_add(1).ok_or(Error::T::Overflow)?; Count::T::put(count); Self::deposit_event(Event::CountIncremented { who, count }); Ok(()) } #[pallet::call_index(1)] #[pallet::weight(10_000)] pub fn reset(origin: OriginForT) - DispatchResult { T::AdminOrigin::ensure_origin(origin)?; Count::T::put(0); Self::deposit_event(Event::CountReset { who: T::AccountId::default() }); Ok(()) } } }这段代码包含了刚才提到的大部分组件。Count 存储项使用 StorageValue意思是保存一个单值。increment 函数先做权限检查ensure_signed 确保是已签名账户调用然后取值、加一、写回最后触发事件。reset 函数使用 Config 里定义的 AdminOrigin 检查调用者权限。从这段代码可以看到 Substrate 开发的核心模式模式化地写存储、事件、错误和调用函数框架帮你处理序列化、存储、区块验证等底层细节。这对新手上手非常友好因为没有太多暗黑魔法每一块的意图都很清晰。3.3 Storage 设计的经验存储是链上区块链成本的大头提到链上存储这是很多新手没有重视、但实际开发中非常有讲究的地方。链上存储不是普通的数据库链上数据的读写会直接影响交易权重手续费和区块执行时间。存储越频繁、数据越大交易执行成本就越高。所以我在实际开发中的经验是存储能少用就少用能精简的类型不要用复杂类型。比如用 u32 够的就不要用 u64能用 StorageValue 就别用 StorageMap用 StorageMap 的时候要仔细设计 key避免迭代整个 map 来查找数据这会非常昂贵。另外Substrate 还提供了 StorageDoubleMap 这种双键存储结构适合需要按两个维度索引的数据比如用户 A 在资产池 B 中的份额。这类高级存储结构能帮你减少一次额外索引但也增加了理解的复杂度。3.4 Weight 机制给每条交易定价Substrate 引入了 Weight 概念用来衡量一个交易执行时消耗的计算资源。Weight 实际上就是一个 u64 数值表示执行时间的相对度量。每个外部函数声明一个 weight 值区块生产者在打包交易时会累加它们的 weight确保一个区块的总 weight 不超过区块上限。这类似于以太坊的 Gas 模型但实现方式更直接——一条区块链的区块执行时间被硬性地控制在特定预算内超出则这个块无法被网络接受。在写 Pallet 时简单地使用固定 weight 快速地办是可行的比如写死 10_000。生产环境则需要认真地用 benchmark 来测量每个操作的耗时然后基于测出来的数据设置 weight。Substrate 提供了 frame-benchmarking 工具来做这件事它会自动生成一个 Pallet 的基准测试代码你可以跑一批交易样本统计出最大的执行时间然后转换成 weight 值。我在一个存证类项目的实际使用中一开始全部用固定 weight后来上线前做 benchmark发现有些操作的耗时差了三倍这时候才知道 weight 的重要性。提前做好 benchmark可以避免正式上线后因交易费用不合理导致的拥堵或滥用。4. 实操过程与核心环节实现4.1 从 node-template 起步创建项目并启动第一条链具体流程上我会推荐用官方的 node-template 作为起点。这一步其实是整个开发过程中最简单但最关键的一步因为环境一旦通了后面只是迭代。首先克隆模板仓库git clone https://github.com/substrate-developer-hub/substrate-node-template.git cd substrate-node-template cargo build --release首次编译会比较痛苦可能需要十几分钟甚至更久。原因是 Substrate 的依赖树非常庞大包含了大量编译优化配置。如果你用的是开发模式debug而不是 release 模式编译时间会短很多但节点运行时的性能会差。开发调试时用 debug 就够了正式需用 release。编译完之后启动./target/release/node-template --dev打开另一个终端窗口可以看到节点在持续出块。这时已经有一条活着的链在运行了。通过 polkadot.js apps 可以连接到本地节点直接在网页上查看余额、转移代币、查看区块甚至通过前端界面调用你的 Pallet 函数。这里有一个我个人的小心得刚开始调试时不建议一上来就写前端而是先用 polkadot.js apps 这个通用工具和链交互把链的能力摸清楚再去写前端界面。polkadot.js apps 是一个类似 Ethereum 的 RemixMetaMask 综合体的工具它可以根据链的元数据自动生成可视化的交易界面非常省事。4.2 核心配置修改 runtime 和 chain_spec要让做出来的链有自定义的名字或原生 Token 符号需要修改链配置。最常改的是 runtime/src/lib.rs 里的版本号和 chain_spec.rs 里的链参数。在 runtime/src/lib.rs 中你可以找到类似下面的代码pub const VERSION: RuntimeVersion RuntimeVersion { spec_name: create_runtime_str!(node-template), spec_version: 1, impl_version: 1, ... };其中 spec_name 是链的名字spec_version 是你链的版本号。每当你修改了 Runtime 逻辑都要递增 spec_version否则链上已有的状态可能认为你是同一个版本而拒绝升级。在 chain_spec.rs 里你可以设置链的 ID、Token 符号、账户初始余额等。比如把开发者预置的初始余额设置多或少直接影响测试体验。这里的修改是纯配置工作但需要理解 chain spec 的作用——它定义了链的初始状态包括哪些账户有初始余额、哪些验证人初始出块等。4.3 运行前端的交互方式Polkadot.js Apps 和自定义 UI我在本地开发中基本都是通过 https://polkadot.js.org/apps 访问节点在网页右上角把 endpoint 切到本地节点默认是 ws://127.0.0.1:9944就可以看到自己的链的链名、Token 符号和区块高度。如果你改了 chain_spec 里的 Token 符号这里就会显示你设置的名字。另外一个方式是直接用 WebSocket 接口对接节点。Substrate 节点暴露了 JSON-RPC 接口常用的有 state_getMetadata获取链的元数据用于自动生成接口、author_submitExtrinsic提交交易、state_getStorage读取链状态等。第三方库比如 polkadot-js/api已经把这些接口封装好了你不用自己去写 WS 消息格式。我的建议是先用 polkadot.js apps 验证链的功能然后通过 polkadot-js/api 开始设计自己的前端界面。前端架构最好是React/Vue 负责展示和交互polkadot-js/api 负责和链通信链数据通过 API 查询到本地再渲染。4.4 编译发布与链升级的无缝体验最让 Substrate 开发者上头的特性之一就是无分叉升级。我改完 Runtime 后不需要全体节点停机只需要提交一个 Runtime 升级交易链上的 Wasm 码就会被替换。当然本地开发时你不用真的走治理流程可以直接通过 sudo Pallet 调用 runtime upgrade。操作方式 先在 Ubuntu 里编译新的 Runtimecargo build --release编译产物是一个单独的 wasm 文件通常在 target/release/wbuild/node-template-runtime/node_template_runtime.compact.wasm。然后用 polkadot.js apps 开发者面板找到 Sudo - sudoUncheckedWeight调用 parachainSystem.setCode上传这个 Wasm 文件。交易成功后节点会立即接受新 Runtime链的 spec_version 随之更新而你不需要任何节点重启。这种体验在和传统区块链开发对比时第一次感受会非常深刻——不用硬分叉就完成了代码升级。5. 常见问题与排查技巧实录5.1 编译问题wasm32 target 缺失、Rust 版本不匹配最常碰到的编译错误是 wasm target 未安装。错误信息通常会明确提示 target might not be installed这时执行 rustup target add wasm32-unknown-unknown --toolchain nightly 即可。另一个常见问题是 Rust 版本太旧Substrate 依赖的很多 crate 需要较新的 nightly。如果你常年没更新工具链建议先执行 rustup update nightly rustup default nightly然后重新编译。如果还是编译失败检查一下 Cargo.lock 是否被改动过有时候用了旧的 lock 文件会导致版本不兼容。我的建议是在任何 substrate 项目里优先保持 nightly 工具链和 wasm32 target 的安装。这个组合用起来最顺利而且官方文档也是按照这个配置写的。5.2 存储读写的 panicValueQuery 与 OptionQuery 的坑在 Pallet 存储中你可以用 ValueQuery 或 OptionQuery 两种方式定义查询行为。ValueQuery 表示这个存储项一定会有值默认值为零值比如 0、空字符串、false。OptionQuery 表示存储项可能不存在查询时返回 Option 。新手容易踩的坑是在声明 StorageMap 时如果想要按键查值但又不确定这个键是否一定存在仍然用了 ValueQuery结果查询一个不存在的键时会返回零值而不是 None导致业务逻辑判断出错。我的经验是除非你确认这个键一定有值否则一律用 OptionQuery处理时显式检查 None 的情况。这个坑的效果在我做一个 NFT 项目时特别明显当时一个查询某个 Token ID 是否存在的操作因为 ValueQuery 特性导致所有不存在的 Token ID 都返回了一个默认的 Token 对象浪费了很多调试时间。5.3 出块停滞和交易无法打包本地开发时另一个常见问题是节点出块正常但交易提交后一直没有进入区块。这通常有几种可能交易没有通过签名校验比如签名用的 nonce 不对或者账户余额不足以支付手续费。这种错误一般会在提交接口返回具体信息。交易的价格设置过低低于区块生产者的最低可接受值。这个可以在前端把 tip 调高一点。内存池堆积了大量重复交易把区块空间耗尽。重启节点能清理掉内存池。如果你用 --dev 模式通常不会遇到这些问题。但如果是多节点本地网络比如你在一台机器上跑四个验证人组成的测试网就需要小心节点间的时间同步问题。我没少被所有节点都活着但就是不出块折磨后来发现是验证人 Key 没配置对导致没有任何一个节点有资格出块。5.4 Runtime 升级后状态不兼容的问题无分叉升级让人着迷但也有它的风险升级后的 Runtime 代码和链上已有的旧状态不兼容时节点会直接拒绝出块或者在新 Runtime 看来旧状态是非法的。这种情况一般出现在存储结构发生变化时比如原来某个 StorageMap 的 key 结构改了、某个枚举的存储编码变了。解决办法是做好迁移。Substrate 的运行时迁移工具允许你在 Runtime 升级时附带一段迁移逻辑对旧存储进行预定的转换。例如某个旧字段要从 u32 改成 u64迁移代码可以读取旧值并写入新的存储键。开发中如果只是本地测试链很多人直接重置链状态rm -rf data 重启 --dev但生产环境必须把迁移写好。我的经验是哪怕是在原型阶段也建议养成升级时带上 migration 的习惯。因为很多问题在链的数据量很小时看不出来一旦状态多起来再做迁移要复杂十倍。5.5 Benchmark 与费用异常如果你在正式网或者测试网上发现某些交易费用高得离谱很可能是因为 weight 设置没有经过 benchmark。Substrate 要求每个外部函数都有 weight如果你直接用了很小的固定值那这个交易会非常便宜可能被恶意刷量如果你用了很大的固定值那正常用户的费用会虚高体验很差。正确的做法是跑一遍 frame-benchmarking我简单说一说流程cargo build --release --features runtime-benchmarks ./target/release/node-template benchmark pallet \ --pallet pallet_name \ --extrinsic * \ --steps 50 \ --repeat 20它会生成一份关于每个调用的耗时统计然后你可以把这些数据填入 pallet 的 weight 数组里。不用 каждое操作都手动写Substrate 还提供了标准的 weight 模板代码来生成对应的 weight 文件。6. 写在最后给新入坑者的一些实在建议我在实际使用 Substrate 的过程中最大的感受是它的门槛不在于 Rust 本身而在于你对区块链里那些基础概念的理解深不深。如果你对区块、交易、状态、账本这些模型本来就有认识把 Substrate 的思路迁移过来并不难如果你一开始就硬啃宏和 trait容易掉进语法细节里出不来。我建议先跑通一条模板链再改几个数字体验一下升级和转账之后再慢慢研究 Pallet 的内部细节。还有一点很多人不会告诉你Substrate 的开发节奏是非常快的API 迭代频繁。你网上搜到的一份两年前的教程很可能因为框架版本差异已经跑不起来了。遇到这种情况不要急着怀疑自己的环境先看看官方文档和模板的版本尽量用官方最新模板起步。我自己每次开始新项目前都会重新拉一份最新的 node-template而不是复用之前的旧项目很大程度上就是为了避免版本错位。如果你打算认真做一条链从 node-template 起步只是热身。生产级别的链还需考虑共识参数调优、经济模型设计、存储性能规划以及后续的链上治理机制。把这些纳入考虑后Substrate 的真正优势才会开始体现出来它能把你从底层重复劳动里解放出来让你把精力放在业务和产品上。我个人体会是用 Substrate 做事前期的学习曲线换来的是后期极高的迭代自由度这笔买卖是值得的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门