☰
用Substrate搭建你的第一条应用链:从环境配置到自定义Pallet
2026/9/28 16:13:27 网站建设 项目流程

1. Substrate到底是什么:一个被多个行业共用、却被开发者盯上的关键词

1.1 同一个词,在不同语境里代表完全不同的东西

substrate这个词,在不同圈子里指代的东西完全不一样。学生物的人看到它,想到的是酶促反应里的“底物”,也就是被酶催化转化的那个物质;做硬件和材料的人看到它,会联想到“基板”或“基底”,比如PCB板上用来承载电路的那层介质。但在区块链开发者眼里,substrate通常指Parity开源的区块链开发框架,也是时下应用链项目选型时被反复提到底层工具。

如果你搜索这个词,大概率会看到一堆生物化学或者材料科学的解释。很多人做链开发时第一次接触它,其实是看到别人说“用Substrate搭一条应用链”,结果点进去发现是一套Rust工程。本文说的就是后者,尤其面向想把新链搭起来、又不想从零实现网络层、共识层和状态存储的那批开发者。

1.2 选Substrate而不是直接Fork现成链,图的是什么

不少项目方一开始的想法很朴素:找一条现成公链源码,删掉不需要的模块,改改共识参数,直接Fork一份。这个方案不是不能跑,问题在于Fork之后,你就被动继承了原链的全部设计约束。账户模型怎么定、交易格式长什么样、链上治理走什么流程、Runtime升级用什么机制,这些全都耦合在旧代码里。你想改其中任何一项,都得先搞清楚别人的设计意图,再承担改不动的风险。

Substrate的思路正好反过来。它把链上最常见的能力拆成一个个独立的模块,比如账户抽象、余额管理、交易池、共识、存储、治理等。你不需要的模块可以不编译进运行时的,需要的模块能直接通过FRAME框架安装进来,甚至可以在不影响节点主体结构的前提下替换共识实现。拿工程话来形容,就是“做可组合的积木,而不是做一次性改版”。对团队来说,这意味着新链不是从某条老链的“边界”出发,而是从自己的业务需求出发。

这篇文章适合两类人:一类是想搭概念验证链,但此前只用过以太坊系开发工具,对Rust和Substrate生态不熟;另一类是已经跑通过节点,但还没动手写自己的自定义逻辑,想搞清楚pallet和Runtime之间连接方式。下面内容会按“环境准备—跑通默认链—改自定义模块—排坑”这条顺序讲,跟着做基本不会走偏。

2. 准备工程的那些细节:Rust工具链、模板与首次编译

2.1 Rust工具链不是装上就能直接用的

Substrate是一个Rust工程,但它的构建要求跟普通Rust项目不太一样。你不仅要装Rust工具链,通常还要准备nightly版本,以及wasm32-unknown-unknown这个编译目标。原因不复杂:Substrate的节点程序是Rust编译出来的原生可执行文件,但链上Runtime需要被编译成WebAssembly格式,这样节点才能在不同环境下加载和运行同一个Runtime。缺少wasm编译目标,构建会在中间环节报错。

我的建议是,先确认基本工具链再动手。按下面顺序来:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env rustup default stable rustup update rustup toolchain install nightly rustup target add wasm32-unknown-unknown --toolchain nightly

装完之后,进入Substrate工程目录,优先看有没有rust-toolchain.toml文件。如果模板里指定了某个nightly版本,就让它生效,别手动去切版本。Substrate升级频率很高,不同nightly之间的API变动会直接导致编译失败,而这种失败往往跟你写的代码没有关系。把工具链锁在工程级,是省时间的核心操作。

2.2 拿到官方模板,先别急着改代码

官方维护了一个叫substrate-node-template的工程模板,它是最小可运行的起点。相比直接拿完整节点源码来看,模板砍掉了大量不相关功能,保留了一条链最基础的骨架:节点进程、一个最小Runtime、几个内置pallet,以及一堆示例脚本。

克隆和构建的命令是这样的:

git clone https://github.com/substrate-developer-hub/substrate-node-template cd substrate-node-template cargo build --release

第一次构建会把几百个依赖crate拉下来并编译,这是一个比较漫长的过程,以主流配置的电脑来说,第一次构建通常会持续几分钟到十几分钟左右。如果你是8GB内存以下的机器,还可能遇到内存不足导致编译被系统杀掉的问题。遇到这种情况,关掉浏览器和其他大内存程序再试,不要一边开着视频一边硬编。

模板的目录结构值得先花十分钟看懂。node目录里是节点进程相关代码,runtime目录里是链上逻辑,pallets目录里放自定义模块,scripts目录里提供一些常用辅助命令。绝大多数开发时间会花在runtime和pallets上,node目录反而很少动。

2.3 首次编译时的几个心理预期

很多新手第一次编译失败,问题不出在代码,而是出在环境变量或者平台依赖上。比如Linux环境缺了clang、cmake这类构建工具,就会在某个依赖阶段报一堆看不懂的原生错误;Windows上还可能出现WASM构建相关工具链问题。最稳妥的路线是先用Linux服务器或WSL2跑,省掉不少环境折腾。

编译过程中如果看到Compiling substrate-wasm-builder这类字样,说明正在生成WASM的Runtime包,这一步耗时最长。不要因为进度条卡住就判定死机,它确实需要慢慢编完。构建完成之后,target/release目录下会出现node-template可执行文件,这就是后续所有操作的主角。

注意:有的人为了节省编译时间,会设置SKIP_WASM_BUILD=1跳过WASM构建。这个变量偶尔调试原生代码时能用,但如果你要真正跑链,跳过之后得到的节点是没法正常出块的,别长期依赖这个临时手段。

3. 第一条链跑通之后,先学会“观察”它

3.1 启动节点时的两个小开关:--dev和--tmp

构建完成后,启动一条测试链其实只需要一行命令:

./target/release/node-template --dev --tmp

--dev表示进入开发模式。开发模式下节点会自动配置好一个单节点网络,并且预置Alice、Bob等测试账户,这些账户里有余额,方便你直接做转账、调用等操作。如果不开--dev,节点会按正式网络的逻辑去找其他节点组网,单节点跑起来会很麻烦,还得自己配置验证人密钥。所以做本地开发,--dev几乎必加。

--tmp的意思是使用临时数据目录。节点会自己生成一个临时的链下数据库,退出之后数据就清空。这对反复修改Runtime、重启节点的开发场景特别友好,不用担心上一次测试的脏数据影响判断。

启动之后,日志会不断刷新。会看到类似Importing blocks、Proposing block这样的输出,后面的区块高度持续递增。对于模板节点来说,只要出块高度在稳定增长,就说明共识和交易池已经正常工作。你也可以观察到best和finalized两个高度,两者之间相差很小,属于正常状态;如果差距拉得很大,通常说明共识或者网络同步出了问题。

3.2 三个端口分别管什么

Substrate节点启动后会监听多个端口。默认情况下,9944是WebSocket端口,前端页面向节点提交交易、查询状态都走它;30333是点对点端口,节点之间互相同步区块数据时使用;还有RPC相关的端口,比如9933,给HTTP RPC调用用。开发模式下,你一般只关心WebSocket端口,因为前端模板默认连接的就是ws://127.0.0.1:9944。

搞清楚端口分配很有用。当你遇到“前端连接不上节点”时,第一反应不是去检查前端代码,而是先确认这个端口有没有被其他进程占用。后面我会专门讲这个坑。

3.3 用前端模板直接看链上状态

Substrate官方还提供了一个前端模板,叫substrate-front-end-template。它在浏览器里运行,能帮你查看账户、余额、系统事件,甚至调用链上的交易。开发和调试自定义模块时,这个前端几乎是必备工具。

启动方式不复杂:

git clone https://github.com/substrate-developer-hub/substrate-front-end-template cd substrate-front-end-template yarn install && yarn start

默认情况下,前端页面会打开一个本地端口,通常是http://localhost:8000。页面加载后会尝试连接ws://127.0.0.1:9944。连接成功之后,你在页面上能看到当前链上出块情况、Alice的账户余额,以及最近的系统事件。这个阶段不用写一行代码,就能直观理解Substrate里的“状态”到底是什么形态。

4. 写第一个自己的pallet:让链上多个可调用的“计数器”

4.1 pallet是FRAME里面真正装业务逻辑的地方

Substrate的Runtime建设并不是把所有代码塞进一个文件里,而是通过FRAME这套模块化框架,把不同功能拆成不同pallet。可以简单理解为,每个pallet都是一块业务积木,它拥有自己的存储、事件、可调用函数,以及跟其他模块互动的接口。最后把所有pallet组装进Runtime,编译成WASM,才成为真正的链上逻辑。

这种设计的关键价值在于隔离。业务A的逻辑放在pallet_a里,业务B放在pallet_b里,它们之间互不干扰。想要添加功能,不需要大规模改现有运行时,只要新增一个pallet并接入Runtime配置即可。比把几万行代码堆在一个进程里好维护得多。

4.2 一个极简pallet的代码骨架

下面这个例子,是在pallets/counter目录下创建一个最简单的计数器pallet。它只做一件事:允许一个签名账户把一个u32值写入链上存储。代码骨架如下:

#![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 {} #[pallet::pallet] pub struct Pallet<T>(_); #[pallet::storage] pub type CounterValue<T> = StorageValue<_, u32, ValueQuery>; #[pallet::call] impl<T: Config> Pallet<T> { #[pallet::weight(10_000)] pub fn set_value(origin: OriginFor<T>, value: u32) -> DispatchResult { ensure_signed(origin)?; CounterValue::<T>::put(value); Ok(()) } } }

这段代码有三个关键部分。#[pallet::storage]定义了一项链上存储,名叫CounterValue,类型是u32,默认值为0。#[pallet::call]定义了一个可以被外部调用的函数set_value,它接收一个origin参数,用来表示这次交易是谁发起的,然后通过ensure_signed校验调用者是签名账户,最后把传入的value写入存储。

#[pallet::weight(10_000)]是手续费权重的估算值。真实项目需要更精细地设计权重,但示例阶段10_000足够。这个值实际上会影响交易的计价和资源上限,先不深究。

4.3 把pallet接入Runtime,只需要三步

写完pallet代码,它还不是链上的一部分。你需要把模块在Runtime里“注册”进去,这样节点和前端才能感知到它的存在。

第一步,在runtime/Cargo.toml中添加依赖:

[dependencies] pallet-counter = { version = "4.0.0-dev", default-features = false, path = "../pallets/counter" } [features] std = [ ... "pallet-counter/std", ]

这里的default-features = false和std特性配置是为了同时支持原生构建和WASM构建。漏掉std特性那一行,后面编译时经常出现无法找到标准库相关类型的错误。

第二步,在runtime/src/lib.rs里引入并实现Config。在文件顶部加一行:

pub use pallet_counter;

然后在已有的impl ... Config for Runtime区块附近,加上:

impl pallet_counter::Config for Runtime {}

第三步,在construct_runtime!宏里注册Pallet。找到类似下面的代码:

construct_runtime!( pub enum Runtime { System: frame_system, Balances: pallet_balances, // ...原有内容 CounterPallet: pallet_counter, } );

注意这里的CounterPallet名字可以自己起,但必须保证它在整个construct_runtime!里唯一。注册完成之后,重新执行cargo build --release。

4.4 在浏览器里验证链上的新功能

重新编译并启动节点,前端模板重新连接后,你就能在页面上找到新模块。具体操作是:

  • 在“Extrinsics”栏,选择CounterPallet,再选择setValue函数,用小写值比如42,提交账户选Alice。
  • 等待区块被打包,交易状态变成success。
  • 切到“Chain State”栏,选择CounterPallet、counterValue存储项,查询一次,返回结果应该就是42。

这一步会给你一个很直观的体感:自定义业务逻辑确实已经写进了链的层级,而不是像智能合约那样跑在链上的一层虚拟机环境里。它就是Runtime的一部分。这也正是很多人选择Substrate做应用链的核心理由:业务逻辑能和链本身一样获得原生执行性能和更直接的状态访问能力。

5. 从能跑到能“稳定跑”:我踩过的几个坑

5.1 Rust工具链不一致带来的编译迷雾

我在搭建过程中遇到最多的问题,就是本地工具的默认版本跟模板要求不一致。Substrate官方在升级过程中会锁定一个特定nightly版本,但你本地rustup default可能是stable,或者是一个日期更早的nightly。两者同时存在时,cargo build会根据rust-toolchain文件自动选择,但有时候你手动执行rustup component add却加到了错误的工具链上,导致目标缺失。

这个时候不要急着怀疑代码,先用rustup show看当前生效的工具链和目标。再确认wasm32-unknown-unknown是否已经安装到对应工具链:

rustup target list --installed --toolchain nightly

如果列表里没有wasm32-unknown-unknown,补上之后再重新编译。这类环境问题占掉的时间,通常比写业务逻辑本身还多。

5.2 端口被占用,节点起来却连不上

开发模式下节点默认占用9944端口。如果你反复重启节点,或者以前启动的实例没有被完全杀掉,端口会被旧进程占住。新节点启动时大概率报错Unable to bind,前端自然也就连不上。

解决办法很简单,先找到占用端口的进程,杀掉,再启动。Linux和macOS上可以用:

lsof -i :9944

Windows上则可以用netstat -ano | findstr :9944查PID再结束进程。如果实在不想处理,也可以给节点指定新的WebSocket端口,比如--ws-port 9945,但前端模板连接地址也要同步改成ws://127.0.0.1:9945。做本地开发时,习惯每次重启前检查一下进程残留,能省掉不少“为什么连不上”的迷惑时间。

5.3 Runtime里没有Event,前端却硬要去查事件

Substrate的前端模板会把链上事件展示出来,很多新手在测试时就希望看到自己pallet里的事件。但如果你在pallet中定义了事件,却在Runtime的construct_runtime!里没有正确注册,或者Config里的RuntimeEvent类型没有关联对,编译阶段会直接报错。

同样的,如果你根本没定义事件,前端自然查不到任何新事件。这里要理解一个关键区分:Extrinsics是“发出去的交易”,Events是“交易执行后产生的结果记录”。交易成功不代表一定产生自定义事件,只有代码里显式.emit()的事件才会出现在前端。排查这类问题,思路是先从pallet源码确认有没有#[pallet::event]定义,再检查Runtime里是否注册了对应类型,最后才去前端看事件列表。

5.4 Substrate版本更新太快,旧代码不能盲抄

这一点是Substrate生态和其他成熟开源框架最大的不同。它的核心API演进速度很快,很多三年前的教程代码放到现在的模板上根本无法编译。decl_storage!这类老的声明式存储宏已经被基于属性的新宏替代,一些pallet的辅助函数名也频繁变化。如果你在网上看到一段代码,用的是decl_storage!风格,最好先确认它的发布时间是否和当前模板版本匹配,而不是直接复制粘贴。

我个人建议,把官方模板里的pallets/template目录当成“活的参考”,每次写pallet都从它复制一份,再改业务逻辑。你想用的宏、导入路径,模板里都有现成的可运行例子。结合当前模板做开发,比抱着旧文章逐字对照要可靠得多。

还有一个实用技巧:在跑通默认链之后,先用版本管理工具把最初的编译环境记录好,比如把Cargo.lock和rust-toolchain.toml提交进仓库。这样你的开发环境至少是自洽的,出了问题还能对照“第一次能编译时的状态”来回滚排查。

Substrate这套东西,上手门槛主要不在概念,而在工具链和版本管理。只要第一次能在你的机器上成功跑通一个模板节点,后面写自己的pallet、调整Runtime,都会顺畅许多。多花点时间把框架的模块边界和运行方式摸清,比一开始就堆业务代码更能避免后期返工。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询