ARTICLE DETAIL

资讯详情

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

Substrate 区块链开发框架实战:从零跑通第一条链

Substrate 区块链开发框架实战:从零跑通第一条链 第一次看到 substrate 这个词我脑子里先冒出来的其实是三个完全不同的画面生物化学实验室里酶反应试管里的底物芯片厂里的硅衬底还有Parity那套区块链开发框架。这三样东西相隔十万八千里但如果你把substrate翻译成托住上层的那层基石它们的精神内核其实完全一致。在区块链开发这个圈子里Substrate 已经从一个生僻的技术名词变成高频关键词了。Polkadot、Kusama 这些老牌项目长在它上面无数新项目也拿它做底子。这篇文章我想从一个实际做过链开发的人的角度把 Substrate 到底能干什么、为什么值得选、怎么跑通第一条链、怎么写自己的业务模块、以及哪些坑我替你踩过了一次性讲透。无论你是刚听说这个概念、正在纠结技术选型还是已经 Rust 写了一段时间想切进链开发这篇文章的路径应该都能帮你快速建立实操意义上的认知。1. 底物、衬底、还是链框架Substrate 的多重身份与它真正火的地方1.1 一个词在不同领域的三张面孔如果你去查生物学词典substrate 是酶催化反应里被转化的那个分子。葡萄糖在己糖激酶手里被磷酸化葡萄糖就是那个底物。没有它酶就没有用武之地整个代谢通路就转不起来。如果你去翻材料科学的书substrate 是生长薄膜、芯片电路的那块基底。晶圆就是最典型的衬底上面可以沉积各种功能层。衬底的平整度、纯度、热稳定性直接决定了上层电路能做多细、跑多稳。到了区块链领域Substrate 的含义和前面两个一脉相承它是以太坊生态之外一套用来托住上层业务逻辑的开发框架。你写的链上业务跑在 Substrate 提供的执行环境里节点之间的网络通信、区块存储、账本共识这些很重的基础设施全部被这套框架接走了。这么一理解三张面孔归根到底其实是同一个词根逻辑下面那层决定了上面能搭什么、能搭多高。1.2 技术圈的 Substrate区块链开发框架的定位技术圈提到 Substrate通常特指 Parity Technologies 开源的区块链开发框架。Polkadot 是基于它搭起来的这已经是公开的事实。更准确地说Substrate 提供了一整套区块链公共部件点对点网络层、交易池、共识引擎、数据库存储、RPC 接口连客户端和 runtime 的分层骨架都给你铺好了。开发者拿到的是一个大毛坯房水电管线已经走好墙体结构已经成型你要做的是把房间隔断改一改、刷上自己的漆而不是从挖地基、浇混凝土开始。写业务逻辑的时候你面对的核心单位叫作 pallet中文圈常叫模块一个 pallet 就是一组状态、事件、外部函数、错误的封装体。你的链究竟支持哪些功能本质上就是往 runtime 里装配了哪些 pallet。这里要特别提一句很多人把 Substrate 和区块链操作系统画等号我其实不太认同这个类比。操作系统管的是整台电脑的进程、内存、设备而 Substrate 更像给了你一副已经拼接好的底盘和动力总成驾驶舱的仪表盘、车机交互、甚至方向盘手感都需要你自己设计。它给你省掉了最繁琐也最不容易做对的底层工程但业务判断力一点都不会帮你省。1.3 它能做什么三个能打动开发者的典型场景从实际项目看用 Substrate 的动机一般落在三种典型的场景里。第一种快速搭一条独立的应用链。项目方想要自己的链、自己的代币经济、自己的治理规则但不想把网络层和共识层重造一遍。Substrate 的 node-template 是一套开箱即跑的节点骨架你改一下链名、配好创世状态加上自己的 pallet一条具备完整出块、转账、浏览器数据接口的链就能在本地跑起来。这件事情在一开始可能需要一两天熟练之后半天就能完成。第二种做 Polkadot 生态里的平行链。Polkadot 的架构本身就是由 Substrate 构建的所以用 Substrate 写的链天然具备接入平行链的可能。共享安全性、跨链消息传递这些能力对于想借势大型生态的项目来说非常关键。第三种在传统业务系统里引入可验证的链上机制。举例说供应链场景里做溯源存证审计场景里做操作日志固化存证系统用传统数据库也能做但链条上的关键证据如果由一个自己完全掌控的链来背书在多方协作里的可信度完全不一样。很多企业级项目并不发币不上交易所只是要一个可控、可审计、可长期运维的链式账本Substrate 恰好合适。2. 选型逻辑为什么 Substrate 比从零造链和分叉改造更适合多数项目2.1 三方对比从零开发、分叉、和 Substrate 的取舍我见过不少团队在立项第一周就陷进一个经典争论链到底怎么做选项通常有三个从零写一个区块链分叉一个现成的链或者用 Substrate 做。从零写区块链是一条极具诱惑力的路。什么都是自己的逻辑完全可控但如果团队不是长期做分布式系统的人很快就会撞到天花板。共识算法要考虑网络分区、消息延迟、恶意节点行为p2p 层要考虑连接管理、地址发现、NAT 穿透存储层要用 Merkle 树把状态根组织起来。这些工作每一项都足够一支团队啃好几个月而且做出来的东西大概率没有经过真实攻击场景的检验。分叉现成链是另一个常见选择。比特币、以太坊的代码质量经过了长期考验拉下来编译就能跑。但凡是分叉过以太坊的人都知道链的历史状态、Gas 模型、账户体系、智能合约执行环境全都刻在基因里。你想改共识、改出块时间、把手续费模型换成自己的逻辑都是在跟一堆历史包袱对抗很多地方牵一发动全身。Substrate 走的是中间路线。它把公链、联盟链、应用链共同需要的部分作为框架沉淀下来同时把业务层留白给你。以下这张表是我自己选型时常用的对比方式维度从零开发分叉改造Substrate开发周期数月到数年数周起但改造受限于原有架构数天到数周可以出原型共识灵活性自己实现风险高几乎继承原有共识可插拔可自定义可升级性自己设计硬分叉或类似硬分叉的机制支持 forkless runtime 升级生态资产无继承被分叉链的生态但也继承负担可直接复用 Substrate/Polkadot 生态工具链维护成本非常高高且要持续跟进上游安全补丁框架层面的更新由社区维护这张表不是要证明 Substrate 在所有场景都赢但可以看出它确实覆盖了最多项目的核心诉求快速验证想法、保留足够的自由定制空间、后期还能平滑进化。2.2 核心抽象Client、Runtime、共识三者如何分工想用好 Substrate必须先理解它的三层抽象。我把这三层理解成身体大脑和仲裁者。Client 是身体。它负责节点怎么在网络中找到别人、怎么广播交易、怎么把区块数据落盘、怎么对外提供 RPC 接口。这部分主要是基础设施多数情况下你不用动它但它决定了链的性能上限和节点运行的稳定性。Runtime 是大脑。你的全部业务逻辑都在这里账户余额怎么变、存证数据怎么写入、投票结果怎么计算。Substrate 的一个关键设计是 runtime 被编译成 Wasm 字节码它在链上以 Wasm 的形式存在和运行。这个机制的解释很关键节点可以通过链上治理提交一个新的 runtime Wasm全体节点同步执行后链就完成了升级。整个过程不需要停服也不需要所有节点手动换二进制这就是所谓的 forkless runtime 升级。共识是仲裁者。它负责让分布在不同机器上的节点对同一份历史达成一致。Substrate 支持可插拔共识比如开发模式用的ManualSeal、单节点出块的Aura、多节点环境下带最终性确认的Grandpa。你不需要从零写共识但需要理解不同共识适合的场景网络里有哪些节点、信任边界在哪里这些决定了你选哪个方案。看到这里你可能会想把 runtime 编译成 Wasm 这种设计有必要吗有必要。正因为业务逻辑以统一格式的字节码存在且随链存储升级才不需要全网替换客户端。这是相比传统公链那种升级等于硬分叉模式最划时代的一点。2.3 一个关键认知Substrate 不是盒子而是积木 规范很多人刚接触 Substrate 时容易产生一种错觉它已经把所有东西都做好了我只要填几个配置文件就能得到一条生产级链。这个误解在玩 demo 的时候无伤大雅但做真实项目会出大问题。Substrate 确实给你默认实现了一套电池节点之间能出块、能转账、能查询状态。但要让它真正满足业务需求通常必须动三处最核心的 runtime加入自己的 pallet、创世配置决定起始余额、初始委员会、以及共识与网络参数决定链的治理模式、TPS 预期、节点发现方式。这三处都有大量细节需要自己根据业务去填。更重要的是开发范式上的转变。传统应用开发是改代码然后部署新的服务。Substrate 开发则是写 pallet编译成 runtime Wasm然后把版本提交到链上。这要求团队把升级当成链上资产来治理而不是简单的发版。我见过不少项目方把链做出来后升级流程完全没设计一改链上逻辑就要停机重启等于把 forkless 升级的能力白白浪费了。3. 从依赖安装到第一个链跑通开发环境的每一步与常见翻车点3.1 环境准备Rust 工具链与 node-template 获取Substrate 开发跑不掉 Rust。就算你的团队以前写 JavaScript 或 Go这一步也躲不开。这里说的不是要精通 Rust而是至少要能读能编译因为 runtime 的编写语言就是 Rust。建议的安装路径是这样第一步安装 rustup把 stable 工具链装好。第二步安装 nightly 工具链并添加 wasm32-unknown-unknown 目标。这一步经常有新人漏掉因为直接用 stable 也能编译节点二进制但编译 runtime 的 Wasm 时就会报错。rustup update nightly rustup target add wasm32-unknown-unknown --toolchain nightly环境变量建议统一设置一条方便后续构建export RUST_BACKTRACE1然后获取官方节点模板。项目模板的仓库在 Substrate developer hub 里名字是 substrate-node-template。你可以直接 clone也可以借助 cargo-generate 这种脚手架工具cargo install cargo-generate cargo generate --git https://github.com/substrate-developer-hub/substrate-node-template.git --name my-chain拿到模板之后先别急着改代码直接试构建一次。另外Linux 系统需要注意几个系统级依赖缺了它们编译会在奇怪的地方失败。通常把这些装齐sudo apt update sudo apt install -y build-essential clang pkg-config libssl-dev protobuf-compiler如果你用的是 WSL 或者云服务器这步基本是避免编译中途报错的保险。macOS 上一般不需要这么折腾但 Xcode Command Line Tools 一定要装齐。3.2 编译第一条链那些容易误导新人的编译输出第一次跑cargo build --release你要有心理准备这不是常规 Web 项目那种三五十秒能搞定的事。Substrate 依赖的 Rust crate 数量非常多第一次全量编译哪怕是配置不错的机器也往往要 20 分钟到 1 小时不等。中间很多输出看起来像是在卡住或者报错其实只是大型工程编译的正常节奏。最常见的真翻车点有三个。第一个是缺系统库。如果你在编译时看到类似于failed to run custom build command for libp2p或者clang: error: linker command failed先回过去检查上文那几条 apt 依赖是否完整。特别要注意libssl-dev和protobuf-compiler前者是加密与传输层的底层依赖后者影响节点内部使用 protobuf 的构件编译。第二个是 Wasm target 缺失。报错信息通常会出现unknown target: wasm32-unknown-unknown或者在你构建时提示找不到 wasm toolchain。这时候执行上文的rustup target add即可记得确认是加到了 nightly 工具链下面不是 stable。第三个是内存不足。编译链路长链接阶段吃内存尤其凶。有人刚开一台 2G 内存的小机器想编译链节点结果链接阶段直接 OOM。建议开发机至少 8G 内存或者配置一些 swap 空间作为缓冲。等你做完业务原型再考虑精简编译链路开发阶段没必要跟自己过不去。给你一个区分正常与卡死的经验看编译日志最后一行是否仍在推进。如果输出停在一个下载或解析阶段且长时间不动多半是网络拉包慢可以等一等如果停止在 clang/linker 阶段则多半是系统库或内存问题。3.3 启动节点与交互验证日志、RPC 和前端模板编译成功后进入模板目录my-chain或者你改的自定义名直接用 release 二进制启动开发链cd my-chain ./target/release/node-template --dev --tmp--dev表示以开发模式运行--tmp表示使用临时数据目录每次重启都会清空历史数据。这对本地测试非常友好因为不需要担心上次实验搞脏了链状态。启动后你会看到节点持续打日志里面能看到出块信息、正在监听的端口默认是 9944以及共识相关的输出。如果你看到区块高度持续增加Top 出块正常推进说明链已经能跑了。这时候可以进一步验证 RPC 接口是否工作。另开一个终端执行curl -H Content-Type: application/json -d {id:1,jsonrpc:2.0,method:system_name,params:[]} http://localhost:9944正常情况会返回节点名称类似于result:my-chain。这就说明链不只自己跑得动对外界查询也完全开放。想更直观一点可以再拉一个前端模板substrate-front-end-template它是一个 React 应用连接本地节点之后能在网页里看到当前出块高度、余额变化、以及你提交的 extrinsic 记录。对新手来说前端模板是理解链到底在干什么的最好辅助工具建议别跳过这一步。4. 第一次写 Pallet业务逻辑如何变成链上逻辑4.1 Pallet 的本质状态、事件、可调用函数、错误如果此前只做过传统后端开发第一次面对 pallet 时最容易蒙的点是我的业务代码到底以什么形式存在。在传统后端里业务逻辑体现为服务、类、函数处理完数据落进数据库。在 Substrate 里这些对应关系变成了状态就是我定义的存储项函数就是链上可调用的 extrinsic事件就是对外广播的通知错误就是调用失败时返回的异常信息。以一个最简单的积分系统为例。你要让用户拥有积分余额于是你定义一个存储项从用户账号映射到积分余额你要让用户能消费积分于是写一个被#[pallet::call]标记的外部函数输入账号和积分数量在里面校验余额是否足够、扣减、发出一个消费事件。这就成了一个完整链上逻辑闭环。为什么状态和函数要被这样组合核心原因是可验证性。链上的每一笔调用、每一个状态变更全部记录在区块历史里可以被任何节点重放和验证。这在积分系统里也许只是方便对账但在治理投票、权益分配、资产交易这些场景里可验证性就是业务可信度的根基。4.2 一个最小自定义 Pallet 的代码结构拆解node-template 自带一个示例 pallet位于pallets/template/src/lib.rs。它的结构非常值得逐行读一遍。我挑核心部分讲解。最外层是宏标记#[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*;这部分告诉编译器下面这个模块是一个 Substrate palletframe_support提供的宏会展开成大量的样板代码完成存储、事件、调用的底层接线。接着是 Config trait#[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: FromEventSelf IsTypeSelf as frame_system::Config::RuntimeEvent; }这是整个 pallet 与 runtime 之间的接口约定关键是定义一个关联类型RuntimeEvent用户的调用才会统一汇入 runtime 的事件系统。相当于是说我这个模块要发事件runtime 你得能接收。存储项是最直观的业务状态#[pallet::storage] #[pallet::getter(fn something)] pub type SomethingT StorageValue_, u32, ValueQuery;这个定义了一个简单的全局计数器当前值是一个 u32。ValueQuery的作用是读取不存在的值时返回零值简化后续逻辑。Event 和 Error 分别定义模块的事件枚举和错误枚举#[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum EventT: Config { SomethingStored(u32, T::AccountId), } #[pallet::error] pub enum ErrorT { NoneValue, StorageOverflow, }最后是外部调用函数本体#[pallet::call] implT: Config PalletT { #[pallet::weight(10_000)] pub fn do_something(origin: OriginForT, something: u32) - DispatchResult { let who ensure_signed(origin)?; Something::T::set(something); Self::deposit_event(Event::SomethingStored(something, who)); Ok(()) } }ensure_signed(origin)负责确认调用者身份deposit_event发出存储事件整段逻辑非常平铺直叙但背后框架已经替你做完了签名校验、存储序列化、事件上链等一系列工作。对于刚上手的人来说先把模板跑通、再改参数、再逐步增加存储项和调用函数是最平滑的学习曲线不建议一上来就写一个巨大的 pallet。4.3 我在写第一个 Pallet 时踩的三个坑第一个坑写了 pallet 却忘记在 runtime 里注册。Substrate 的 runtime 有一个construct_runtime!宏所有 pallet 都要在这个宏里显式列出否则编译时模块根本不会出现在链上。那种报错往往很诡异看起来像 trait 无法满足但根因就是你少写了一个模块注册条目。每次新建 pallet我都建议先检查runtime/src/lib.rs里construct_runtime!是否加上了TemplateModule或你自己的模块名同时确认runtime/src/pallets.rs里把模块的依赖声明了。这个检查动作十次有八次能救你于水深火热。第二个坑Event 类型的约束写错。这个问题典型表现是编译报错提示类似于the trait FromEventSelf is not implemented。原因是 pallet 内 Config 里RuntimeEvent类型定义和 runtime 端泛型参数不一致。你在 runtime 里实现Configtrait 的时候必须把RuntimeEvent指定为 runtime 级别的RuntimeEvent否则事件无法桥接。这一步几乎所有人都会遇到解决办法本来也不复杂复制模板写法别自己发挥。第三个坑权重随便填。开发期#[pallet::weight(10_000)]写一个常量没有任何问题但如果你要做真实可运营的链这个值直接决定了区块执行费用和防滥用能力。用恒定值意味着链上所有调用消耗的资源被同等收费有的函数轻、有的函数重这个露馅的地方早晚会被攻击者盯上。生产链需要用 benchmarking 机制为每个 extrinsic 生成合理的权重模型。开发阶段可以先到处占位但心里一定要有一笔账这个坑如果留到上线治理升级会把痛苦放大数倍。5. 测试与升级开发之外真正决定交付质量的几件事5.1 单元测试与 Mock Runtime 的搭建很多刚接触 Substrate 的人会忽略测试其实它恰恰是链开发里性价比最高的环节。pallet 的测试通常在模块内部的 tests 模块里核心是用一个 mock runtime 替代真实 runtime然后用外部性构造器模拟链的状态。模板里通常已经给了一部分测试基础。一个典型的测试骨架长这样#[cfg(test)] mod tests { use super::*; use frame_support::{derive_impl, traits::ConstU32}; use sp_runtime::BuildStorage; #[derive_impl(frame_system::Config)] impl frame_system::Config for TestRuntime { type Block frame_system::mocking::MockBlockTestRuntime; type AccountId u64; ... } frame_support::construct_runtime!( pub enum TestRuntime { System: frame_system, TemplateModule: crate::pallet, } ); #[test] fn do_something_works() { new_test_ext().execute_with(|| { let alice 1u64; assert_ok!(TemplateModule::do_something(RuntimeOrigin::signed(alice), 42)); assert_eq!(Something::TestRuntime::get(), 42); }); } pub fn new_test_ext() - sp_io::TestExternalities { let storage frame_system::GenesisConfig::TestRuntime::default() .build_storage() .unwrap(); storage.into() } }这个结构之所以有意义是因为它让你脱离整个链的运行单独测一个 pallet 的行为。每次修改业务逻辑后跑一遍cargo test -p pallet-template模块名按你自己的 crate 调整就能快速确证状态变更、事件投递、权限校验是否符合预期不用每次都用前端手动触发一遍。5.2 Forkless Runtime 升级为什么它不是停机维护这是 Substrate 最值得被拿出来讲的能力也是很多人第一次意识到这个框架和普通联盟链不一样的时刻。传统链一旦上线想修复 bug、调整参数通常要分叉或发动整体替换牵连一连串节点和生态工具。Substrate 的 runtime 以 Wasm 形式存储在链上通过治理机制提交一个新的 Wasm 版本并执行节点会像执行普通区块一样执行这个升级整个过程不需要停链。这件事对操作者最大的启示是你设计链时要把未来可能的升级当作第一类公民来对待。存储结构要不要预留版本号事件版本变不变数据从旧结构迁移到新结构需要写 migration而 migration 是 chain 升级时要特别小心的部分。没有 migration 的升级只改代码不搬数据状态与代码一旦不一致链可能出现逻辑灾难。所以只要上线过任何升级计划的第一步都应该是检查旧的存储项与新逻辑是否兼容而不是直接把新 runtime 传上去。5.3 上线前的实战检查清单链开发到尾声真正决定交付质量的往往不是功能代码而是那些不起眼但上线必炸的工程细节。我把自己带过的项目里最实用的检查项整理成一张清单照着过一遍能省很多事检查项具体内容如果不做会怎样存储设计确认 storage schema 是否有版本字段未来的迁移路径是否清晰上线后发现存储结构要改升级计划变成灾难权重基准为每个 extrinsic 跑 benchmark而不是使用固定权重链上费用失衡复杂调用挤占区块空间权限控制确认某些敏感操作是否有 Root 或治理门槛任意账号可以调用管理函数链被破坏创世配置检查初始余额、初始成员、创世存储是否正确链一启动状态就是错的只能推倒重来监控与日志确认 RPC 端点、日志级别、指标上报已经接入线上问题出现时无从下手定位升级预案写清楚多签升级流程、回滚方案、迁移脚本升级出问题只能干瞪眼这一整套检查之所以重要是因为链的特性决定了上线之后不能随便改。在所有软件开发里链是少有的需要把运行期安全印在设计期的类型。花在做设计、做迁移、做测试上的时间会在线上稳定运行的日子里一一还给你。我的个人体会是Substrate 的入门门槛确实存在但它的所有复杂度几乎都集中在最初那条学习曲线上。一旦你能编译通过模板、写通一个 pallet、理解了 runtime 与 client 的界限后面的一切都会变得顺理成章。如果你现在正卡在某个编译错误里出不来给你一个最小路径先跑通 template再去抄一个现成 pallet改几个字段试试然后尝试往 runtime 里加一个自定义模块。不要一开始就啃 p2p 或共识源码那是走得足够远之后的事。这条路径我验证过很多遍也带过不少人走通剩下的就是动手了。
返回列表