经常有朋友问我,Polkadot SDK(也就是以前的Substrate)到底怎么上手,尤其是想把一条平行链跑起来,却不知道从哪一步开始。网上教程一堆,但要么太老,要么是照着文档念一遍,真正自己动手的时候全是坑。这篇文章就是我反复搭环境、编译、跑通测试网之后,整理出来的一份可以照着抄的完整流程,希望能帮你少走点弯路。
先说清楚这篇文章要解决什么问题。Polkadot SDK本身是一个区块链开发框架,平行链模板则是官方准备的一个“差不多能用的链”,它已经接好了共识、P2P网络、账户系统、余额模块、权限治理模块等一堆基础功能。你要做的不是从零写一条链,而是把这个模板拿过来,编译成一条真正能跑的平行链节点,然后和Rococo测试网(或者本地中继链)连接上。整个过程涉及工具链配置、代码编译、链配置、节点启动、平行链注册等环节,每个环节都有不少容易踩的坑。
这篇文章适合两类人:一类是完全没碰过Rust和Substrate,但想用Polkadot SDK做开发的人;另一类是已经跑过独立Substrate节点,但对“平行链”这层概念还比较模糊,想把连接中继链这步彻底搞明白的人。我会尽量把每一步背后的原因也讲清楚,而不只是告诉你“敲这个命令”。
1. 项目设计与核心思路拆解
1.1 为什么用模板而不是从零写链
刚接触Polkadot生态时,我也有过“从零手写一条链”的想法,试过之后才发现完全没必要。平行链本质上是一条“插进中继链”的区块链,它的区块头要提交给中继链验证,所以除了普通区块链的共识、存储、交易池之外,还得实现平行链特定的接口,比如验证人怎么校验你的区块、出块人怎么从中继链拿到可用性数据。这些东西如果自己写,光是把接口定义理解清楚就得花不少时间。
官方提供的平行链模板(polkadot-sdk仓库里的substrate-parachain-template,现在叫parachain-template)已经把这些问题都处理好了。它默认实现了CollatorSelection(收集人选择)和Session(会话管理)两个关键pallet,也就是说你拿到的模板已经具备“跑起来然后注册成平行链”的基础条件。直接基于它改,能够把所有精力放在业务逻辑上。
这里也顺带提一个重要的选择:用polkadot-sdk而不是老版本的substrate。Polkadot SDK是2024年以后的统一版本号发布方式,以前分的substrate、polkadot、cumulus三个仓库合并成了一个仓库。查资料时如果看到2023年之前的文档,很多路径和依赖名称都对不上,这点必须有心理准备。
1.2 模板整体架构速览
拿到的模板代码,大致分为三层:
- 最底层是
runtime,封装了链上的业务逻辑,比如余额怎么转账、质押怎么结算、Staking怎么验证,全部以pallet的形式挂载在runtime里。 - 中间层是
node(节点),负责把runtime跑起来,处理网络、同步、交易池之类的底层工作,最终生成一个可执行文件。 - 最上层是
pallets(自定义业务模块),默认会有一个template示例pallet,方便你往里面添加自己的逻辑。
这个概念怎么理解呢?你可以把runtime想象成一台手表的机芯,决定“时间怎么计算”,而节点是表壳和指针,负责把机芯展示出来。平时你开发业务就是在机芯里面加零件,节点层基本不用动。
2. 环境准备与工具链安装
2.1 Rust工具链的具体配置方法
Polkadot SDK是Rust写的,所以你首先得装Rust。我建议用rustup来管理,版本选择上别用最新的stable,而是用官方推荐的nightly-2024-XX-XX某个固定版本。理由很实在:Polkadot SDK的编译依赖许多只有nightly才有的特性,但最新nightly又经常引入不兼容的改动,所以官方模板里会锁定一个测试过的nightly版本。
打开模板目录(或下载好代码后),里面有个文件叫rust-toolchain.toml,内容大概是这样:
[toolchain] channel = "nightly-2024-03-01" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] profile = "minimal"这个文件告诉rustup,进入当前目录时自动切换到你锁定的nightly版本。所以推荐的做法是先把官方模板克隆下来,再执行rustup安装,不要自己手动选版本。如果已经装了其他版本也没关系,rustup会按需下载。
执行安装:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup show执行rustup show会按照rust-toolchain.toml自动下载对应工具链。这一步可能需要几分钟,取决于网络状况。另外提醒一句:编译wasm需要wasm32-unknown-unknown目标,这个目标不能少,否则后面编译runtime会直接报错找不到目标。
2.2 系统依赖与资源预估
编译Polkadot SDK内存吃得很凶,代码编译期的峰值内存可以达到8GB到16GB。建议虚拟机或云服务器至少4核8GB起步,16GB会更稳。磁盘方面,一个release编译目标目录大概会占10到15GB,预留30GB以上比较稳妥。
系统依赖是另一个容易卡住的点。Ubuntu/Debian上执行:
apt update apt install -y git clang curl libssl-dev protobuf-compiler build-essentialmacOS上需要保证Xcode Command Line Tools已经安装,同时用Homebrew安装protobuf和clang:
xcode-select --install brew install protobuf clang这里重点说一下protobuf-compiler。Polkadot SDK在编译过程中会用到Prost生成Rust的protobuf代码,如果系统里没有protoc,编译到某个依赖时就会莫名其妙地报protoc: not found或者failed to execute protoc。这个错误很常见,但排错方向却很少被人提及,先把protobuf装好能免掉一个大麻烦。
2.3 源码获取与依赖加速
获取模板代码最直接的方式是直接克隆官方仓库:
git clone https://github.com/paritytech/polkadot-sdk.git但polkadot-sdk整个仓库体积非常大,如果没有特殊需求,建议只拉取模板子目录。官方也提供了模板的独立模板仓库(substrate-parachain-template的历史版本),但要注意版本匹配。我更推荐的做法是克隆polkadot-sdk然后切到模板目录下工作,因为这样能保证使用的依赖代码版本和自己仓库的同步更新一致,后续升级依赖时也不容易出错。
依赖下载靠cargo从crates.io拉,在国内网络下慢得让人绝望。强烈建议配置~/.cargo/config.toml使用国内镜像源(比如字节跳动的镜像):
[source.crates-io] replace-with = "rsproxy-sparse" [source.rsproxy-sparse] registry = "sparse+https://rsproxy.cn/index/"另外polkadot-sdk里有一些依赖是GitHub依赖,不走crates.io,所以如果拉取时卡住,还得结合代理或者多试几次。这里提醒一下,不要在公网上讨论代理工具,但本地开发时自己做好网络加速是完全合理的工程实践。
3. 模板结构解析与核心文件说明
3.1 顶层目录分布
我把负责人目录列出来,这样进入仓库之后你能快速找到方向。
parachain-template/ ├── node/ │ ├── src/ │ │ ├── command.rs # CLI入口和参数解析 │ │ ├── rpc.rs # RPC接口 │ │ ├── chain_spec.rs # 链配置(初始账户、初始token、初始收集人) │ │ ├── service.rs # 节点服务组装,最重要文件之一 │ │ └── main.rs ├── pallets/ │ └── template/ ├── runtime/ │ └── src/ │ ├── lib.rs # runtime组装文件,所有pallet都要在这挂载 │ ├── config.rs # 各pallet的Config实现 │ ├── genesis_config.rs # 创世配置 │ └── weights.rs # 权重计算 ├── specs/ # 链的规格文件 ├── rust-toolchain.toml └── Cargo.toml一眼看过去,最需要关心的是runtime/src/lib.rs、runtime/src/config.rs、node/src/chain_spec.rs这三个文件。模板自带的所有能力、初始配置,基本都在这几个文件里。
3.2 关键文件的作用
很多人刚拿到代码时会问:为什么平行链需要这么多配置文件?普通的一条链不是编译完就直接跑吗?
这里说一下根本原因。平行链需要向中继链注册自己的“身份”,中继链要验证它、要给它分配平行链编号、要让它接入共享安全。所以在启动和连接的整个过程中,需要一份描述自己是谁、初始状态是什么的ChainSpec文件。同时,因为平行链的出块逻辑依赖中继链的验证人集合,所以节点的服务逻辑(service.rs)会比普通节点复杂得多。
chain_spec.rs里需要重点关注这几个部分:
parachain_id:你的平行链在中继链上的编号,比如2000。collator账号:负责出块的收集人,至少要有几个初始账号。sudo账号:拥有超级权限,用来执行注册、升级、转账等治理操作。endowed_accounts:初始拥有token的账户列表。
这些值不只是一堆配置,它们会直接影响你能不能成功出块、能不能进行后续的升级操作。后面实操部分我会具体演示怎么改。
3.3 为什么默认是“收集人”而不是“验证人”
这是初学平行链时最常混淆的概念。普通链有验证人,负责打包和验证区块;但平行链自己不维护一条“完整验证人链”,它的出块节点叫“收集人”(Collator)。收集人的职责是从交易池拿交易,聚合出区块,再把区块头和有效性证明提交给中继链的验证人。中继链上的验证人负责做最终校验。
所以模板的service.rs里运行的是收集人节点,而不是验证人节点。启动命令也是polkadot-parachain这样的节点(其实模板的二进制就叫parachain-template-node),它与中继链节点连接时,是通过“平行链-中继链”专用的共识协议的。这一点如果没理解透,很可能在配置节点时下意识去找“验证人密钥”,但找半天也找不到正确配置。
4. 编译全流程与常见报错处理
4.1 第一次编译前的编译选项
在项目根目录执行:
cargo build --release第一次编译WebAssembly和native代码大概需要40分钟到1小时,时间长短主要取决于CPU性能,也有一部分磁盘IO影响。官方文档建议使用--release,这会让二进制小很多、运行效率高很多。如果只是想快速检查代码是否编译通过,可以用cargo check --release,只需验证类型和依赖,不产生最终二进制,会快很多。
我在第一次编译时,遇到最多的是“内存不足”。如果当前机器只有8GB内存,建议临时增加swap(交换分区),否则编译到一半会被Linux的OOM Killer杀掉。增加swap的方法:
fallocate -l 8G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile注意这只是开发环境下的临时方案,生产环境还是得保证足够内存。
编译过程中还有一点容易被忽视,就是CPU核心数过高反而可能出问题。Cargo默认会占用所有可用核心,但在一些云服务器上,多核心编译反而因为内存不够而频繁触发交换。这里可以用一个参数限制并发:
cargo build --release -j 8这样能限制同时编译的任务数量,减少内存峰值。我自己实测下来,16GB内存的机器用默认并行度没问题,8GB内存建议限制到-j4或-j8。
4.2 被频繁踩坑的编译报错及定位方法
我整理了一份高频报错表,帮你在卡住时快速定位:
| 报错信息 | 出现原因 | 解决办法 |
|---|---|---|
failed to run custom build command for ... libp2p | 缺少protoc | apt install protobuf-compiler或brew install protobuf |
failed to select a version for ... | 依赖版本冲突 | 检查Cargo.toml依赖版本和Cargo.lock是否匹配,删除Cargo.lock后重新生成 |
thewasm32-unknown-unknowntarget is not installed | 缺wasm目标 | rustup target add wasm32-unknown-unknown |
toolchain 'nightly-...' is not installed | rust-toolchain没生效 | 执行rustup show确认自动安装,手动rustup toolchain install nightly-xxxx |
cannot find attribute macroconstruct_runtimein this scope | runtime版本不匹配或宏导入错误 | 检查Cargo.toml中substrate-wasm-builder和frame的版本,清理后重编 |
recursion limit reached while expanding the macro | 依赖宏递归过深 | 在对应lib.rs或主入口加#![recursion_limit = "256"] |
报错信息只是个线索,真正排错时要多看日志前几百行上下文。比如failed to run custom build command之后往往有一大堆上一级依赖信息,排查时从最底层的第一个error开始解决,不要从最上面的warning开始。
4.3 二进制产物的验证
编译完成后,在target/release下会有一个parachain-template-node可执行文件(或者polkadot-parachain,取决于模板名称)。执行:
./target/release/parachain-template-node --version如果输出版本号,比如parachain-template-node 0.1.0,说明本地代码和工具链基本正常,接下来可以生成链规格和启动节点。
5. 平行链的启动与连接中继链
5.1 生成链规格(ChainSpec)
平行链启动前,要生成一份描述创世状态的JSON文件,它包含初始账户、初始余额、初始收集人列表等信息。生成命令是:
./target/release/parachain-template-node build-spec --disable-default-bootnode > plain-parachain.json--disable-default-bootnode非常重要,如果不加,生成的规格里会包含一堆模板自带的默认节点(官方测试节点),这些节点在国内网络环境下根本连不通,会拖慢启动过程。生成完之后,可以打开JSON看一眼,搜索parachainId,确认编号是否符合预期。默认可能不是2000,可以根据后续中继链要求改。
建议在生成时直接指定链名或编号,比如:
./target/release/parachain-template-node build-spec --disable-default-bootnode --chain mychain > mychain-spec.json但这一步只能改部分配置,要在创世就注入自己的账户,最可靠的方式还是直接改chain_spec.rs源码后重新编译,或者用build-spec生成后手动修改JSON里genesis下的字段。
5.2 本地中继链还是公共测试网
连接中继链有两条路,一条是连官方公共测试网Rococo,另一条是本地起一条中继链(用polkadot二进制配合rococo-custom或dev链)。对初学者,我更推荐先本地起一条中继链来练兵。
本地中继链的好处太多了:
- 出块速度可控,一般设为2秒或6秒一个块,出问题好排查。
- 拥有sudo权限,可以自己执行平行链注册、转交中继链token等操作。
- 不会因为公共测试网上的排队、资源不足影响你的研发节奏。
本地方案的步骤如下:
- 构建或下载一个
polkadot的release二进制(中继链节点)。 - 生成中继链规格,指定一个或两个验证人。
- 启动中继链节点。
- 构建平行链节点并启动它。
- 在中继链上注册平行链(通过sudo调用
registrar.registerParachain)。
这里中继链的二进制可以直接从官方Release页面下载预编译版本,也可以自己用cargo build --release -p polkadot编译,但耗时比平行链本身还长,所以首次就别自己折腾了。
5.3 启动平行链节点并连接本地中继链
先启动中继链。用预生成的rococo-custom-2.json(两个验证人)或dev模式。如果只用单验证人,可以执行:
polkadot --chain rococo-custom-2.json --alice --tmp --port 30333 --rpc-port 9944这里的--alice会为Alice预置验证人密钥,--tmp指定临时数据目录(每次重启会清空状态)。重新开一个终端,启动平行链节点:
./target/release/parachain-template-node \ --alice \ --collator \ --force-authoring \ --tmp \ --port 40333 \ --rpc-port 8844 \ --ws-port 9945 \ -- --execution wasm \ --chain /path/to/relay-chain-spec.json \ --port 30344 \ --rpc-port 9946 \ --tmp注意这条命令中有个非常容易忽略的细节:在--后面指定的是中继链相关参数,--前面的参数是平行链自身参数。平行链节点实际上是一个“二元节点”,既要跑自己的平行链逻辑,又要作为全节点连接中继链,所以有两组参数。很多新手在日志里看到大量中继链同步信息,却看不到自己的区块生产,多半就是这两组参数混在一起导致的。
启动之后,如果一切正常,日志里会出现💤 Idle(空闲中,等待交易)或Prepared block for proposing,说明平行链运行起来了,但还没有真正开始出块,因为中继链上还没注册它。
5.4 通过Polkadot JS注册平行链并转交充值
注册这一步,核心操作是通过中继链的registrar模块的registerParachain调用完成。操作时要持有中继链上的sudo账号(通常是Alice)。流程是:
- 打开Polkadot JS Apps,连接到中继链RPC(本地
ws://127.0.0.1:9944)。 - 在
Developer->Extrinsics中找到registrar的registerParachain。 - 填入平行链ID(与前面ChainSpec里的ID一致,比如2000)。
- 填入平行链的创世状态(
genesis state)和验证人代码(validation code,也就是runtime编译出来的wasm)。
这两个数据怎么拿?需要用到平行链节点提供的RPC,对应两个调用:
# 获取创世状态 curl http://127.0.0.1:8844 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"chain_getBlock","params":[],"id":1}'不过更直接的方法是平行链节点自己提供了一种便捷命令来导出创世状态和wasm:
# 导出创世状态 ./target/release/parachain-template-node export-genesis-state > genesis-state # 导出wasm验证代码 ./target/release/parachain-template-node export-genesis-wasm > genesis-wasm把两个文件内容填到Polkadot JS的对应字段里,就完成了注册。这里有个细节,如果文件内容是二进制格式,Polkadot JS有时不接受,需要转成hex格式。可以用xxd -p genesis-wasm | tr -d '\n'转换。
完成注册后,等待几个区块,平行链就会开始出块。再回到平行链节点的日志,可以看到🥩开头的出块日志,说明平行链已经从“准备中”变成了“运行中”。
6. 自定义平行链模板:加一个属于自己的pallet
6.1 为什么必须先跑通模板再加业务
很多朋友一上来就想在模板里加自己的业务逻辑,结果连基础链都没跑通,出了问题根本分不清是模板问题还是自己代码问题。我个人强烈建议的路径是:先原封不动编译、跑通、注册,再去添加业务。
等到模板跑通之后,业务添加就相对自由了。最简单的方式是在pallets/template里改,这个目录已经写好了Extrinsic的示例,你可以顺着它的写法,定义自己的Call、Storage、Event。
6.2 修改chain_spec设置初始账户
如果想让某个账户在创世时就有余额,需要修改node/src/chain_spec.rs里的endowed_accounts列表。找到这行类似:
vec![ get_account_id_from_seed::<sr25519::Public>("Alice"), get_account_id_from_seed::<sr25519::Public>("Bob"), ]添加你自己的地址,只需要在列表里加一个SS58格式地址:
vec![ get_account_id_from_seed::<sr25519::Public>("Alice"), "YOUR_SS58_ADDRESS".into(), ]这里要注意地址格式。Polkadot生态的SS58地址在不同链上前缀不一样,平行链的默认前缀是42,所以添加自己的地址时,保持和模板里Alice、Bob的地址前缀一样即可(5开头的通用地址)。如果你用了其他工具导入了地址,注意检查SS58前缀。
改完重新编译:
cargo build --release再用build-spec重新生成链规格,后面注册的创世状态就会带上这些初始余额。
6.3 在runtime中挂载自定义pallet的流程
如果你想完全新建一个pallet,而不是改示例,步骤如下:
第一步,在pallets/下创建新目录,比如pallets/my_pallet,仿照pallets/template写好Cargo.toml、src/lib.rs。
第二步,在自己的pallet的Cargo.toml里,保持依赖版本和workspace一致。这一步最容易出错,因为polkadot-sdk统一使用workspace = true的方式引用依赖,新建的pallet也会自动继承workspace。如果版本不一致(比如frame-support使用了default-features = false却没同时开启std),编译时就会遇到一堆trait未实现错误。
第三步,在runtime/Cargo.toml加上对应的依赖,格式参照现有的pallet-template。
第四步,在runtime/src/lib.rs里,用construct_runtime!宏注册。格式:
construct_runtime!( pub enum Runtime { ... MyPallet: pallet_my_pallet, } );最后在runtime/src/config.rs(如果你的模板版本是在单独的config.rs文件里实现Config)中实现pallet_my_pallet::Config,通常写一个空实现就够了,比如:
impl pallet_my_pallet::Config for Runtime { type RuntimeEvent = RuntimeEvent; }重新编译,检查是否通过。如果通过,说明你的pallet已经成功挂载,可以开始在里面写业务了。
7. 常见问题排查与调试技巧
7.1 区块高度不增长
这是最常见的现象。平行链注册完成,日志却一直是Idle,不出块。排查思路按顺序来:
- 先确认中继链是否正常出块。如果中继链卡住了,平行链也不可能出块。
- 然后看平行链的区块头是否已提交给中继链。打开Polkadot JS,进入
Parachains->Overview,如果显示你的平行链“是”出块状态,说明正常;如果显示“准备中”,说明注册还没生效或验证人没被分配到。 - 再看平行链节点日志中是否有
Could not create proposal,如果有,多半是交易池有无效交易,或构建区块时补了无效的Runtime调用。 - 检查钱包余额。平行链需要在中继链上有一定的余额来支付注册和出块手续费,如果你的平行链账户被清空了,它可能无法继续提交区块头。
大部分情况下,问题出在中继链验证人的抵押数量不够,或者平行链账户没有足够的token来支付“抵押存款”。解决方法是在中继链上给平行链账户转账,或者以sudo身份修改平行链相关的存款参数。
7.2 验证人收不到平行链状态
有时中继链节点日志显示一切正常,但平行链就是不被认可。这时可以先看RPC:
curl http://127.0.0.1:9944 -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"chain_getHeader","params":[],"id":1}'如果中继链返回正常的head信息,但平行链节点返回的head始终停留在一个高度,那说明平行链出块后没有把区块头广播给中继链。重点检查平行链启动命令中--后面的中继链规格文件是否使用了--chain指定了正确的中继链。如果平行链并不知道自己连接的是哪条中继链,它无法正确上传块头。
另外,如果用了多个--tmp参数,容易造成平行链和中继链的数据目录混乱,出现奇怪的连接问题。建议每个节点都显式指定数据目录:
--base-path /tmp/parachain-alice中继链节点同样指定--base-path /tmp/relay-alice,避免多个进程共用一个临时目录报冲突。
7.3 同步卡住或者大量“Import failed”
同步卡住通常表现为区块高度停滞或日志刷屏。优先看是否遇到“bad justification”或“dispute”。这往往是因为平行链和中继链的runtime版本不匹配。举个具体的例子:中继链升级了新版本,而平行链的验证人代码(wasm)还是老版本,导致验证人校验不一致。
解决方法是重新生成平行链的wasm并更新到中继链,注册时直接使用新的wasm,而不是沿用旧文件。注意平行链的wasm文件大小是否合理,如果生成出来很小(几百字节),很可能没有正确导出wasm,需要重新查编译路径。
7.4 日志级别调整
排查问题时默认日志不够详细,可以把日志级别调高:
RUST_LOG=parachain_template=debug,pallet_collator_selection=debug,aura=debug,consensus=debug ./target/release/parachain-template-node ...但这些模块名在不同版本里有细微差异,建议先执行一次RUST_LOG=debug看输出,再针对性缩小范围。日志调优能省下大量猜测时间,这条经验真的重要。
8. 模板的二开经验分享
走到这里,你已经拥有了一条能出块的平行链,无论是连本地中继链还是Rococo,都能跑通。下面是我在二开过程中的一点体会。
模板默认带的业务非常少,只有余额、权限、资产、示例pallet这些基础功能。真正做应用时,你会想往里加更多模块,比如NFT、DeFi、游戏道具等。这时候建议维护两个分支:一个upstream分支,只跟踪官方SDK的更新;另一个develop分支,存放自己的业务代码。每次官方更新了bug修复或新版本,把upstream合并到develop,解决完冲突再继续开发。别在模板主分支直接写业务,否则后续升级依赖时会很痛苦。
依赖升级的时间点也值得讲究。不要一看到有新版本就立刻升,而是要认真看changelog。如果只是修复了某个pallet的bug或优化了性能,可以先不升;如果涉及安全补丁或新的共识机制(比如异步支持、新的弹性扩容),那就值得花时间升。实测中最稳妥的方式是每三四个月统一升一次,每次留出1到2天的兼容性调试时间。
最后再分享一个小技巧。如果你打算长期做Polkadot SDK开发,本地建议预先构建一份编译缓存,就是第一次完整编译后,不要轻易清掉target目录,也不要频繁切换nightly版本。Rust的增量编译在这里效果虽然一般,但依赖缓存总比重头拉取强太多。每次切版本或清target,少则半小时,多则一整天,这笔时间成本实在不划算。
整个过程走完,差不多就能理解平行链在Polkadot生态里的定位了。它和中继链的关系,就像一台台业务服务器接入了一个统一的共享共识网络。模板给了你骨架,剩下的血肉需要你自己去长。编译、连接、注册、二开,每一步都有坑,但每跨过一个坑,你对这套系统的理解就深一层。遇到问题时,多留心排查路径的先后顺序,大多数问题其实都能靠日志定位到根因。