ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Substrate开发实战:15分钟写出并上链你的第一个Pallet

Substrate开发实战:15分钟写出并上链你的第一个Pallet 如果你和我一样第一次看到“链上模块”“runtime”“pallet”这些词的时候第一反应多半是这玩意的开发门槛到底有多高我是不是得先把密码学、共识算法、P2P网络全部啃一遍才敢动手说实话我自己刚打开 Substrate 的 node-template 源码时也是一脸懵。但真正把第一个 pallet 跑通之后我发现链上开发这件事被严重神化了。在 Polkadot / Substrate 这套体系里写一个链上模块pallet本质上就是一套有规可循的框架式编程。只要抓住最小闭环从新建文件到上链调用15 分钟真的够用。这篇内容不是泛泛讲概念我会用一个完整的“点赞计数”模块为例从环境准备、pallet 源码、runtime 接入到本地节点上链验证一步步带你走一遍。适合刚接触区块链开发、或者想在 Polkadot 生态里跑通第一个 demo 的开发者参考。你也可以把它当成一张地图先整体跑通一遍再回去补细节效率会高很多。1. 为什么说 pallet 开发是“搭积木”而不是“造轮子”先搞清楚一个前提Polkadot 本身不是一条传统意义上的单链而是一个异构多链网络。在这个网络里各条平行链的核心逻辑其实是用 Substrate 框架开发的。Substrate 提供了一套叫 FRAME 的模块化开发体系pallet 就是 FRAME 下的一个独立业务模块。可以把它类比成后端开发里的一个“服务”或者“微服务”它有自己的状态存储、自己的事件、自己的可调用函数然后被组装进整个 runtime 里成为链的一部分。很多人会把链上开发想象成“从零写一条链”这其实是一个思维误区。真正常用到的链上业务绝大多数都可以拆成一个个 pallet。你写业务逻辑时不需要关心区块是怎么打包的不需要关心共识怎么跑也不需要处理账本层的存储怎么落盘。Substrate 已经把链底层的东西全部抽象好了你只需要按照 pallet 的标准接口把业务状态和业务规则写进去。这个体验很像你在 Spring Boot 里写一个 Controller路由、序列化、依赖注入框架全给你处理了你只要写路由函数和业务代码。FRAME 框架本身已经内置了大量官方 pallet比如处理账户余额的pallet_balances、管理链上治理的pallet-democracy、配置共识参数的pallet-session等等。这些官方模块承担了链上最通用的基础能力。而我们自己写的业务 pallet更像是往这套体系里挂一个新的“乐高积木块”挂上去之后它就能和系统里的其他模块协同工作。为什么选 Polkadot / Substrate 来做这件事原因很直接Substrate 允许你用最少的代码把一个真正能跑的业务模块接入到一条链上。你不需要先维护一套复杂的 P2P 网络不需要设计创世区块甚至在本地开发模式下连 token 经济模型都不用管。用官方提供的 node-template 起一条私有开发链几分钟就能出块。这种“开箱即用”的开发体验在区块链领域里真的算非常友好的了。明白了这个背景接下来就可以直接上手。我们的目标是在现有 node-template 里新增一个pallet-likes模块让每个账户可以执行“点赞”和“取消点赞”操作链上保存每个账户的点赞总数。这个模块麻雀虽小但包含了 pallet 的全部标准组成部分弄懂它就弄懂了 pallet 的基础骨架。2. 环境准备先把模板跑起来这步最花时间我先把话说在前头标题里的“15 分钟”指的是你已经装好 Rust 工具链、并且把官方模板完整编译过一次之后的增量时间。如果你是一台全新的电脑从零装环境加首次全量编译可能要 30 到 60 分钟这取决于你的网速和机器性能。别指望第一次接触就能在 15 分钟内完成这不现实。但等你熟悉了整个流程第二次、第三次新建 pallet15 分钟确实足够。准备工作的第一步是安装 Rust。Substrate 开发对 Rust 的工具链版本有明确要求官方一直推荐用rustup来管理不要手动去装某个特定的 Rust 发行包。在终端执行curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh装完之后执行rustup show确认当前目录使用的工具链。node-template 仓库里通常会带一个rust-toolchain.toml文件它锁定了 Substrate 所需的 Rust 版本和组件。Substrate 较新的版本已经默认使用 stable 工具链所以一般直接rustup default stable就能满足要求。接下来把官方模板克隆到本地git clone https://github.com/substrate-developer-hub/substrate-node-template cd substrate-node-template这个模板就是一条完整的、能跑的开发链。里面包含了runtime目录链上逻辑、pallets目录模板自带一个templatepallet、node目录节点程序。如果你之前没接触过这个项目结构建议先别急着到处乱翻先执行编译cargo build --release首次编译会拉取大量依赖并且要构建 Wasm runtime所以时间较长。我的建议是给这个步骤留出足够时间中途不要随便 kill。如果你发现下载速度很慢可以配置 Rust 的国内镜像源来加速 crates 拉取或者适当增加并发下载的 onejob 数。编译成功之后启动本地开发链验证一下./target/release/node-template --dev看到终端滚动出区块生产日志说明环境已经通了。--dev模式会使用一个临时存储目录适合快速测试重启之后数据默认会重置。所以如果后面你想验证“数据真的存在链上”记得先确认启动参数别在重启后误以为代码丢了数据。环境这部分其实没什么技术含量但它决定了后面所有步骤能否顺利推进。很多新手卡在这里并不是因为不会写代码而是 Rust 工具链和 Substrate 版本不匹配导致各种莫名其妙的编译错误。所以我的经验是不要自己去追最新版 Rust以项目里的rust-toolchain.toml为准这是最省心的做法。3. 写一个真正能跑的点赞 pallet代码逐段拆解环境跑通之后开始写我们自己的模块。先规划一下文件结构在pallets/目录下新建likes/src目录对应完整的 pallet 源码结构。mkdir -p pallets/likes/src然后建一个Cargo.toml文件[package] name pallet-likes version 0.1.0 edition 2021 [dependencies] frame-support { version 4.0.0-dev, default-features false, git https://github.com/paritytech/substrate.git, branch polkadot-v1.0.0 } frame-system { version 4.0.0-dev, default-features false, git https://github.com/paritytech/substrate.git, branch polkadot-v1.0.0 } parity-scale-codec { version 3.0.0, default-features false, features [derive] } scale-info { version 2.0.0, default-features false, features [derive] } [features] default [std] std [ frame-support/std, frame-system/std, parity-scale-codec/std, scale-info/std, ]这里有几个点需要解释一下。frame-support和frame-system是写 pallet 最少需要依赖的两个 crate前者提供#[pallet]这个核心宏后者提供Origin、AccountId等系统级类型。parity-scale-codec负责存储数据的序列化scale-info负责向链外暴露类型元数据。features里的std开关也很关键因为 Substrate runtime 需要编译成两种形态带标准库的用于原生执行不带标准库的no_std用于 Wasm 环境。如果你漏配了stdfeature编译时会遇到莫名其妙的链接错误。接下来是核心文件src/lib.rs。为了保证代码可以完整编译我用的是 Substrate 比较通用的 polkadot-v1.0.0 风格写法#![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::pallet] pub struct PalletT(_); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: FromEventSelf IsTypeSelf as frame_system::Config::RuntimeEvent; } // 存储每个账户对应的点赞总数 #[pallet::storage] #[pallet::getter(fn likes_count)] pub type LikesT: Config StorageMap _, Blake2_128Concat, T::AccountId, u64, ValueQuery, ; // 事件链上执行成功后会触发的记录 #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum EventT: Config { Liked { who: T::AccountId, count: u64 }, Unliked { who: T::AccountId, count: u64 }, } // 错误业务校验失败时返回的错误类型 #[pallet::error] pub enum ErrorT { NoLikeToCancel, } // 可调用函数extrinsics #[pallet::call] implT: Config PalletT { #[pallet::call_index(0)] #[pallet::weight(10_000)] pub fn like(origin: OriginForT) - DispatchResult { let who ensure_signed(origin)?; let current Likes::T::get(who); let next current.saturating_add(1); Likes::T::insert(who, next); Self::deposit_event(Event::Liked { who, count: next }); Ok(()) } #[pallet::call_index(1)] #[pallet::weight(10_000)] pub fn unlike(origin: OriginForT) - DispatchResult { let who ensure_signed(origin)?; let current Likes::T::get(who); ensure!(current 0, Error::T::NoLikeToCancel); let next current - 1; Likes::T::insert(who, next); Self::deposit_event(Event::Unliked { who, count: next }); Ok(()) } } }这段代码虽然不长但是 pallet 的五个核心组成部分全部覆盖到了。我们逐个拆开看。Configtrait 是整个 pallet 和 runtime 之间的桥梁。每一个 pallet 都需要声明自己依赖哪些系统能力最基础的就是frame_system::Config。这里还定义了一个关联类型RuntimeEvent它的作用是让 pallet 在触发事件时能把事件统一转换成 runtime 层面的RuntimeEvent。新手最容易忽略的就是这个关联类型写自定义事件时如果不加这一行大概率会报类型不匹配的错误。Likes是链上的状态存储。这里的声明方式是声明一个StorageMap键是账户地址T::AccountId值是一个u64计数。ValueQuery表示当键不存在时直接返回该类型的默认值 0而不是返回None。这对点赞计数来说很合适一个从未点过赞的账户它的计数自然应该当成 0。如果想要“未赞过”和“赞过但计数为 0”有区分那就得改用OptionQuery两者的语义是有差别的后面我会再提。Event和Error是 pallet 对外暴露的结果反馈机制。事件会被记录进区块里链下可以通过查询事件来感知链上发生了什么错误则会在交易执行失败时被回滚掉调用方会拿到对应的错误码。在unlike函数里我们刻意用ensure!构造了一个错误路径当计数已经是 0 时再取消点赞就要拒绝执行。这样不仅演示了 Error 的用法也给你留了点自己扩展逻辑的空间。两个可调用函数like和unlike就是真正的链上交易入口也就是 Substrate 里的 extrinsic。ensure_signed(origin)用于确认调用者是一个真实签名账户然后拿到调用者身份who。之后就是常规的“读-改-写”流程读取当前计数加一或减一写入存储触发事件。#[pallet::weight(10_000)]表示这个函数消耗的权重在 demo 里给个常量即可真实项目需要通过 benchmark 来测定实际权重不然会引发链上的资源计价问题。这个 pallet 的完整骨架已经在手上了。你可以试着把它改造成其他业务比如把“点赞计数”换成“每日签到次数”“投票记录”甚至是“链上任务进度”。核心开发模式都是一样的定义存储、定义事件和错误、定义可调用函数剩下的交给 FRAME。4. 三步接入 runtimeCargo 配置、Config 实现、construct_runtimepallet 写完之后它本身还只是一个孤立的库不会出现在链上。要让这条链真正识别并运行pallet-likes必须在 runtime 里把它“挂载”进去。这个过程一共有三步每一步都有坑但都不难。第一步在runtime/Cargo.toml里注册这个 pallet。打开文件在[dependencies]区域添加pallet-likes { path ../pallets/likes, default-features false, version 0.1.0 }同时在[features]的std列表里加上pallet-likes/std这一步不能省。如果不加pallet-likes/std当 runtime 以原生模式编译时这个 pallet 仍然会以no_std的方式编译最终会引发一堆“某 trait 未被实现”之类的奇怪错误。这算是外部 pallet 接入 runtime 时最经典的遗漏点。第二步在runtime/src/lib.rs中实现这个 pallet 的Configtrait。一般建议写在新出的模块声明区域后面代码如下impl pallet_likes::Config for Runtime { type RuntimeEvent RuntimeEvent; }因为我们这个 pallet 只定义了一个关联类型所以这里的实现非常简短。如果你的 pallet 里有type WeightInfo、type Currency之类的关联类型也需要在这里一一指定。第三步在construct_runtime!宏里注册模块。在该宏的模块列表中找到比如TemplatePallet那一段在它下面加一行Likes: pallet_likes,Likes是这条 runtime 内部的模块名pallet_likes是 crate 名。这里要注意construct_runtime!宏实际上是生成了一堆对应的类型和枚举所以模块名必须是合法的 Rust 标识符并且不能和已有模块重名。有些教程里会写成PalletLikes: pallet_likes这样也是可以的只是后续在链上看到的模块名会不一样。完成这三步后重新编译cargo build --release因为只新增了一个模块增量编译通常只需要几分钟。等编译结束后再启动本地节点./target/release/node-template --dev如果编译过程没报错说明你的 pallet 已经被成功“焊”进 runtime 了。此时链的 metadata 里已经包含likes模块这是后面上链交互的基础。特别提醒一点如果修改了 pallet 代码必须重新编译并重启节点链上才会加载最新的逻辑光刷新浏览器界面是没有用的。5. 上链验证启动本地节点用 polkadot.js 提交第一次链上交易模块接入 runtime 之后最让人兴奋的部分来了真正把一笔交易发到链上亲眼看到自己的模块在跑。这里我不会选择用复杂的前端工程直接使用 polkadot.js apps 的在线界面连接本地节点最快也最直观。先确保本地开发链还在运行。然后在浏览器里打开https://polkadot.js.org/apps/#/?rpcws://127.0.0.1:9944这个地址的意思是让前端界面连接本地节点的 WebSocket RPC 端口9944。如果页面左上角显示已经连上了本地节点并且区块高度在持续增长就可以进行交互了。第一步进入“开发者 - 交易”页面Developer - Extrinsics。在“提交外部交易”的下拉菜单里你会看到刚才在construct_runtime!里注册的likes模块。选择它之后下方会出现两个可调用函数like和unlike。选择like点击“提交交易”然后签名并广播。这个过程会用到你的开发账户node-template 的--dev模式默认预置了一批带余额的测试账户选第一个即可不需要自己额外配置。交易打包进块之后注意看“事件”列表。里面会出现我们自定义的事件格式类似likes.Liked并且带上了who和count字段。看到这个事件说明like函数确实执行成功了而且事件确实被记录进了区块。这是对你刚才写的代码最直接的反馈。接下来到“链状态”页面Developer - Chain State。在模块下拉菜单里选择likes然后选择存储条目likesCount。此时会列出每个账户对应当前的点赞计数。按 F5 多刷新几次你应该能看到刚才操作过的账户数量变成了 1。如果切换到unlike再提交一次计数会减回到 0这就完成了一个完整的“读-改-写-再读”闭环。到这里你已经完成了人生中第一次真正意义上的链上模块交互。它不是模拟不是本地函数调用而是经过签名、打包、执行、落盘、出块完整流程的链上交易。说实话我第一次跑通这个流程的时候特意去翻了浏览器里那个区块的事件列表确认自己的事件真的写进去了那种成就感是写普通后端接口给不了的。把时间账算一下翻开 pallet 模板、改代码大概 4 分钟配置 runtime 大概 2 分钟增量编译 3 到 5 分钟UI 操作验证 2 分钟。如果已经准备好环境15 分钟完成一个“从零到链上验证”的模块这个说法是站得住脚的。6. 新手最容易踩的三个坑编译、Storage 和命名流程走完了我想把实际开发过程中踩过的坑也一并放出来。这些坑在文档里很难查得到遇到了才会知道有多疼。第一个坑是 Rust 工具链和 Substrate 版本不匹配。Substrate 对版本相当敏感你用 stable 能编译的代码切到 nightly 可能突然报出一堆陌生的错误。反过来也一样。最稳妥的做法是始终检查项目根目录的rust-toolchain.toml让rustup自动切换到指定版本。不要手痒去更新全局工具链特别是不要把项目目录里的rust-toolchain.toml随手删掉。我见过有人就是因为全局 nightly 版本太新导致frame_support宏展开时报错整整折腾了一个晚上才定位到是工具链的问题。第二个坑是编译时内存不足和 Wasm target 缺失。首次构建 Substrate 项目时需要编译一个 Wasm 版本的 runtime这一步对机器内存有一定要求。如果编译过程中直接报内存不足的错误可以适当减少并行任务数用下面这个命令试试CARGO_BUILD_JOBS4 cargo build --release如果报的是找不到wasm32-unknown-unknowntarget那就先手动装一下rustup target add wasm32-unknown-unknown另外有些 Rust 组件比如rust-src也是必要的开发环境装齐了之后绝大多数“莫名其妙”的编译错误都会消失。第三个坑是关于StorageMap的 Query 语义和 hasher 选择。我在前面代码里用的是ValueQuery这意味着任何账户即使没有写入过数据读取时也会拿到默认值 0。这在某些业务场景下会掩盖“到底插入过没有”这个信息。如果你希望区分“零值”和“无值”就必须换成OptionQuery。这个决定会影响后面所有业务逻辑的写法一开始就要想清楚不要写到后来再改因为牵一发而动全身。至于 hasher示例里用的是Blake2_128Concat这是 Substrate 里偏安全的默认选择。不要因为简单就换成Twox64Concat那会导致存储键的可预测性增强在部分敏感场景下有安全风险。最后一个细节也许不算坑但很多人会忽略你修改了 pallet 代码之后必须重新编译并重启节点链上的 metadata 才会更新。如果你在 UI 里找不到新加的函数或存储大概率不是前端的问题而是节点没重启或者编译时发生了静默失败。先cargo build --release确认成功再重启节点基本都能解决。以我自己的开发习惯为例现在每跑一个新人上手 Substrate我都建议先照着“模板 pallet - 改名字 - 改一个存储 - 改一个调用函数 - 重新编译 - 上链验证”这个循环走三遍。第一遍是熟悉第二遍是理解第三遍才能谈得上独立设计。这个流程看着简单但它建立的不是“能跑”的幻觉而是一条完整的、可以反复执行的开发链路。之后无论是接pallet_balances做积分转账还是接pallet_timestamp做时间窗口任务你会发现底层思路都是这套定义存储、定义事件、写调用函数、接入 runtime、上链验证。如果你第一次照着文章跑完发现自己的模块没出块事件不要急着怀疑人生先对照一下看看是不是已经在编译期就把错误跳过了。比如确认unlike的ensure!是不是被误写成了反逻辑确认Event里的事件是不是真的在函数末尾被deposit_event触发。调试这一类问题时我的土办法是在函数里临时加一个不触发任何存储变更的log调用配合节点日志看执行流走到哪一步了。等你处理完这些问题再回头看这 15 分钟你会发现自己已经掌握了一套完全可复现的链上开发骨架。
返回列表