一个词在不同行当里指的东西可以天差地别,生物学家看到 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 为什么必须要用 Rust
Substrate 框架本身是 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 = "sparse+https://mirrors.xxx.com/crates.io-index/"这步不涉及任何特殊协议,只是国内公共镜像,能显著提速。
2.3 选模板:node-template 还是 substrate-node-template
Substrate 官方提供了一个最小可运行的链模板: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 的,端口是 9944(WebSocket)和 30333(P2P)。前端工具通过 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: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; type AdminOrigin: EnsureOrigin<Self::Origin>; } #[pallet::storage] pub type Count<T: Config> = StorageValue<_, u32, ValueQuery>; #[pallet::event] #[pallet::generate_deposit] pub enum Event<T: Config> { CountIncremented { who: T::AccountId, count: u32 }, CountReset { who: T::AccountId }, } #[pallet::error] pub enum Error<T> { Overflow, } #[pallet::call] impl<T: Config> Pallet<T> { #[pallet::call_index(0)] #[pallet::weight(10_000)] pub fn increment(origin: OriginFor<T>) -> 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: OriginFor<T>) -> 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 的 Remix+MetaMask 综合体的工具,它可以根据链的元数据自动生成可视化的交易界面,非常省事。
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 里编译新的 Runtime:
cargo 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 存储读写的 panic:ValueQuery 与 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 做事,前期的学习曲线换来的是后期极高的迭代自由度,这笔买卖是值得的。