资讯动态

用Substrate搭建你的第一条应用链:从环境配置到自定义Pallet

发布时间:2026/9/28 16:13:33 来源:尧图企业网站定制
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_BUILD1跳过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 PalletT(_); #[pallet::storage] pub type CounterValueT StorageValue_, u32, ValueQuery; #[pallet::call] implT: Config PalletT { #[pallet::weight(10_000)] pub fn set_value(origin: OriginForT, 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 :9944Windows上则可以用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都会顺畅许多。多花点时间把框架的模块边界和运行方式摸清比一开始就堆业务代码更能避免后期返工。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑