ARTICLE DETAIL

资讯详情

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

Substrate区块链开发入门:从零搭建一条可升级的链

Substrate区块链开发入门:从零搭建一条可升级的链 Substrate这个词在区块链圈子里出现的频率越来越高。如果你尝试过从零开始写一条链大概能体会那种绝望感P2P网络、共识算法、状态存储、交易池、RPC接口、账户模型每一块都得自己啃光是把节点跑起来就能耗掉几个月的业余时间更别提后续的链上治理、升级、平行链接入这些事了。Substrate的出现就是把这些区块链底层的脏活累活全部封装成可复用的模块让你把精力真正花在业务逻辑和链的差异化设计上。它不是一条链而是一个造链的框架Parity团队用它支撑起了Polkadot生态同时也向所有人开源。这篇文章我从一个普通开发者的视角把从零开始接触Substrate、搭环境、写pallet、踩坑排错的全过程捋一遍给想进这个领域的朋友做个参考。1. 为什么选择Substrate搭链不重造轮子的正确姿势1.1 从零搭链的痛点先聊聊大多数人最开始的想法既然区块链本质就是个分布式状态机那我用Go或者Rust写个节点广播交易跑个简单共识是不是就有了一条链理论上没错但真动手做你会发现几个绕不开的坎。第一是P2P网络层。节点之间怎么发现彼此怎么处理NAT穿透消息怎么序列化、广播、去重这些不是不能做但要做到生产级稳定投入的时间成本远超过你的预期。第二是状态存储。每条链都需要维护一份全局状态怎么存、怎么高效读写、怎么在共识分叉时回滚状态这些都有成熟方案但自己造轮子性能和安全都是问号。第三是共识算法。哪怕用一个最简单的实用拜占庭容错也要处理视图切换、消息超时、领导节点选举这些细节。做完一轮你会觉得自己不是在写业务应用而是在做操作系统。而Substrate的思路完全不一样它把这些通用能力做了抽象直接用现成的模块给你。P2P协议栈、数据库存储、共识引擎、交易队列、RPC服务全都可以开箱即用。你只需要专注一件事——定义状态如何变化也就是写runtime。1.2 Substrate替你解决了什么Substrate最核心的设计是把区块链节点分成了两层外层客户端和内层Runtime。外层客户端是用Rust写的一个完整节点程序它负责所有跟业务无关的事情网络同步、共识出块、交易广播、RPC处理、数据库存储。这些在Substrate里都有非常成熟的默认实现大部分情况下你甚至不用改任何代码。内层Runtime就是链的业务逻辑层。它定义了链上有什么资产、转账规则是什么、治理投票怎么算、Staking怎么结算这些全部通过一种统一的框架——FRAME——来组织。FRAME里有一堆现成的pallet模块比如Balances管理账户余额、System管理账户和Nonce、Session管理验证人节点、Treasury管理国库资金。你可以像拼积木一样把需要的pallet组合进你的runtime也可以写自己的pallet完全控制链的行为。这样分层带来的直接好处是你的链在运行过程中可以升级。因为Runtime会被编译成WebAssemblyWASM而链上的区块头里就带着这个WASM运行时。只要有治理机制投票通过新功能就能直接部署到链上节点自动同步新的WASM并切换节点不需要停机更不需要硬分叉。这个分叉升级能力是Substrate区别于绝大多数区块链框架的最大亮点。1.3 它跟其他框架的核心差异有人会问Cosmos SDK不也是模块化框架吗以太坊不也能做Layer 2吗区别还是蛮明显的。Cosmos SDK也是模块化开发但它的默认共识和应用层耦合较深工程上要做更多适配。以太坊是虚拟机智能合约范式开发者在Solidity里写业务但合约的部署、Gas机制、存储模型这些底层规则是锁死的你改不了。Substrate则完全放开存储结构、费用模型、共识类型、治理方式通通可以自定义。更关键的是Substrate的Runtime本质是一套WASM逻辑链的逻辑可以写死也可以动态换而Cosmos的模块升级往往依赖链级治理和协调。这就意味着Substrate是一条什么都能改的链不是在固定的框架里做选择题而是给你一堆乐高积木允许你拼出任意形状的体系也因此成为Polkadot生态里平行链开发的主流选择。2. 本地开发环境搭建那份最容易被忽略的依赖清单2.1 Rust工具链的正确配置顺序Substrate开发绕不开Rust。但不要直接装个Rust就开始编译工具链版本没配对应分分钟卡在奇怪的地方。我自己平时的配置流程是这样的# 1. 安装rustup curl https://sh.rustup.rs -sSf | sh # 2. 设置默认工具链 rustup default stable # 3. 安装nightly工具链Substrate runtime编译时需要 rustup toolchain install nightly # 4. 添加WASM编译目标 rustup target add wasm32-unknown-unknown --toolchain nightly强调一点只有stable工具链是不够的。Substrate的Runtime部分依赖一些只有nightly编译器才支持的feature比如alloc_error_handler、panic_info_message这些。所以编译的时候你会发现脚本会强制切到nightly环境。如果你用的是最新版Substrate建议查一下项目的rust-toolchain.toml文件里面有项目期望的工具链版本直接用里面的nightly版本比用最新的nightly稳妥得多——因为最新nightly偶尔会引入breaking change导致编译器报毫无头绪的内部错误。2.2 系统依赖与潜在雷区Substrate编译过程特别吃系统库。Linux环境下面下面这些基本是标配# Ubuntu/Debian apt install -y build-essential clang curl git libssl-dev llvm libudev-dev make protobuf-compilerlibssl-dev和protobuf-compiler是最容易缺失的。libssl缺失会导致依赖库编译失败protobuf缺失会导致网络协议相关的生成代码跑不过去。装完后可以用protoc --version检查一下。macOS这边则会用到brew install cmake protobuf clang这些。如果你用的是Apple Silicon芯片还要确认下依赖库是否有arm64版本的预编译产物有些旧版本依赖没有arm64 support只能用Rosetta转译来跑编译速度会明显变慢。还有个最容易被忽略的内存和磁盘空间。Substrate加上所有依赖release编译一次非常消耗资源特别是WASM runtime编译阶段内存不够会直接OOM。我自己在8G内存的机器上编译曾经被卡死过好几次。建议至少准备16G内存和20G以上的磁盘空闲否则你会反复怀疑人生。2.3 模板项目初始化与目录结构环境准备好之后可以直接用官方模板起步git clone https://github.com/substrate-developer-hub/substrate-node-template.git cd substrate-node-template cargo build --release这个node-template是官方维护的最小可运行节点。第一次build的时间很夸张基本要十几分钟到半小时取决于你的网络和CPU。它会把几百个crate拉下来编译这个过程需要耐心。build完成后看下目录结构几个核心目录要心里有数runtime/链的业务逻辑包含lib.rs、Cargo.toml以及pallet的组合配置。pallets/存放自定义pallet的地方模板里自带了一个template pallet。node/节点的外层客户端代码包括命令行入口、服务组装、链规格。scripts/一些脚手架脚本比如启动测试网的脚本。等你对这套结构足够熟悉再去看substrate-parachain-template做平行链开发会发现概念是完全共通的只是多了跨链消息处理的部分。3. 理解Substrate核心组件节点、FRAME与链的边界3.1 客户端层与Runtime层的分工整个Substrate架构里最值得花时间理解的就是这条边界线。节点客户端和Runtime之间通过一个叫作RuntimeInterface的抽象层通信。节点客户端负责的事连接其他节点、广播交易、执行共识、从别的节点同步区块、把外部传来的账户签名交易提交给runtime执行。它可以理解为计算机硬件操作系统但它不关心业务规则。Runtime负责的事处理交易、定义状态转换规则、管理余额、结算共识奖励。以Balances pallet为例如果A要给B转账节点客户端只负责接收这笔签名交易把它放进交易池然后作为区块生产者把交易打包进区块。真正执行检查A余额够不够、扣A加B、记录事件这套逻辑的是runtime里的Balances pallet。这个边界有多重要因为它决定了链的灵活边界。你想换共识算法只需要改node部分你想改链上业务只需要改runtime部分两者互不干扰。实际开发中你可以用纯Rust写runtime也可以把runtime的接口扩展成对WASM的调用交给其他语言实现当然这极不推荐实际项目中基本都用FRAME。3.2 FRAMEpallet拼装工厂FRAMEFramework for Runtime Aggregation of Modular Entities是Substrate提供的一套开发Runtime的框架核心思想很直白把runtime拆成很多独立单元每个单元叫pallet一个pallet负责一个领域。比如Pallet名称负责领域核心功能pallet_balances资产余额管理、转账、锁仓pallet_staking共识验证人竞选、抵押、奖励发放pallet_session会话验证人身份管理pallet_treasury资金国库资金分配、提案审批pallet_governance治理公投、理事会、技术委员会pallet_sudo管理超级管理员权限开发调试用每个pallet是一个Rust crate内部定义自己的存储项、事件、错误、可调用函数dispatchable。组合的方式是在runtime的construct_runtime!宏里把所有pallet列出来指定它们各自的类型。pallet之间还能互相调用。比如Staking pallet在发放奖励时会调用Balances pallet的转账逻辑Treasury的提案通过后也会调Balances来拨款。这种模块互相协作的设计使得整个runtime像一台精密的机器每个零件各司其职。新手最容易犯的错误是把太多逻辑塞进一个pallet里。正确的做法是按领域拆开一个pallet只做一件事。这样后续调试和升级都会轻松很多。3.3 WASM运行时分叉升级的底层逻辑这块内容值得单独展开因为它是Substrate最酷的设计也是很多人理解最模糊的点。传统区块链的链上逻辑是写死在节点客户端里的。要升级业务逻辑必须让所有节点停止旧版本运行新版本客户端在某个区块高度强制切换这就是硬分叉。协调成本非常高社区撕裂的风险也很大。Substrate的Runtime不这么做。开发时runtime代码会被编译成WASM字节码而且节点客户端在启动时会把WASM runtime写入链上状态。当链上执行区块时节点会加载WASM runtime来跑交易。那么节点怎么知道自己该用哪个runtime版本呢答案在区块头里。每个区块头都包含了spec_version和spec_name字段。当某个区块的runtime升级后其spec_version递增节点同步到这个区块时会发现版本变了于是自动从状态中读取新的WASM替代旧的runtime继续执行后续区块。这意味着只要链上有权限的人通过sudo或治理机制提交一个更新runtime WASM的交易全网节点无需任何人工干预在下一个区块就能使用新逻辑。这个能力叫forkless upgrade它是Substrate所谓可升级链的基石。实际项目里你不需要手动打包WASM。Substrate的构建流程会自动生成runtime的两份二进制一份原生Rust编译的一份WASM编译的。原生那份用于开发时提升性能链上同步时用WASM那份。4. 从模板启动到自定义链完整实操步骤4.1 预设的node-template启动与验证编译完node-template后在终端跑./target/release/node-template --dev--dev模式会使用开发专用的链规格单节点自动出块不用配置validator节点。启动后你会看到日志不断输出新的块产生说明链已经在你本机跑起来了。接下来打开浏览器访问 polkadot.js.org/apps 进入Settings把网络端点切到ws://localhost:9944就能连上你本地这条链。左侧能看到区块浏览器、账户、转账、链治理这些页面和Polkadot主网的操作体验几乎一模一样。选一个开发账户转一些余额签一笔交易你立刻能感受到链上操作到底怎么回事。这里有个值得注意的细节--dev模式下出块速度一般设置成几秒一块方便开发调试。如果你关了节点再重新启动直接--dev会继续之前的状态如果想完全重置需要先执行./target/release/node-template purge-chain --dev这会把本地数据库、区块链数据全清掉相当于把链恢复出厂设置。不purge的话区块台账和状态不一致时节点可能直接panic。4.2 写第一个pallet从hello world到业务逻辑模板自带的pallets/template是一个最小pallet它没有存储和事件只有一个do_something方法。真实业务里你大概率需要自己的存储和事件。下面我拆一个更接近实际的小例子。假设我要做一个留言板pallet允许用户发布一条公开消息。先定义存储项#[pallet::storage] pub type MessageT: Config StorageValue_, T::AccountId, ValueQuery;再定义事件#[pallet::event] pub enum EventT: Config { MessageUpdated(T::AccountId, Vecu8), }然后是调用逻辑#[pallet::call_index(0)] #[pallet::weight(10_000)] pub fn set_message( origin: OriginForT, message: Vecu8, ) - DispatchResult { let who ensure_signed(origin)?; Message::T::put(who); Self::deposit_event(Event::MessageUpdated(who, message)); Ok(()) }这里有几个概念需要解释一下ensure_signed验证调用者是一个有效账户如果调用者是智能合约或者其他无账户来源会直接返回错误。#[pallet::weight]这个交易的手续费权重开发阶段可以写一个固定值生产环境要根据实际计算复杂度来定。deposit_event把事件写入链的日志后续前端可以订阅事件通知。写好pallet后去runtime/Cargo.toml里加上依赖在runtime/src/lib.rs里通过construct_runtime!宏注册。如果漏了注册编译会报错说找不到对应的pallet新手经常在这种地方卡壳。4.3 自定义配置、接入前端与多节点组网等你的pallet能在本地跑通下一步要考虑的就是怎么让链看起来更像自己的链。一是改链规格Chain Spec。node/src/chain_spec.rs里可以定义链的名称、初始账户、初始余额、共识类型。注意chain_spec里的name和id会出现在RPC响应和浏览器里改掉它们能让别人知道连的是一条自定义链。生成chain spec的命令是./target/release/node-template build-spec --disable-default-bootnode my-chain.json二是前端接入。polkadot.js/apps能直接连但如果你要做一个面向用户的产品通常会自己写前端。Substrate生态里主流的方案是polkadot/api它是一个JavaScript库封装了与Substrate节点通信的所有接口。举个例子调用刚才的palletimport { ApiPromise, WsProvider } from polkadot/api; const provider new WsProvider(ws://127.0.0.1:9944); const api await ApiPromise.create({ provider }); await api.tx.messageModule .setMessage(hello substrate) .signAndSend(account);订阅事件也简单api.query.system.events((events) { events.forEach(({ event }) { if (api.events.messageModule.isMessageUpdated(event)) { console.log(新消息发布, event.toHuman()); } }); });整个打通后你就有一条一条从业务到前端完整闭环的链了。三是多节点组网。--dev模式只适合单机调试真正要测试网络效果需要起多个节点# 节点1 ./target/release/node-template --base-path /tmp/alice --chain my-chain.json --port 30333 --ws-port 9944 --rpc-port 9933 --validator --name alice # 节点2 ./target/release/node-template --base-path /tmp/bob --chain my-chain.json --port 30334 --ws-port 9945 --rpc-port 9934 --validator --name bob然后用--bootnodes参数把节点2连上节点1的P2P地址。只有它们都持有相同的chain spec并且都有验证人身份才能一起出块。真实生产环境要加--rpc-external和--ws-external同时记得设防火墙规则这里只做本地测试的简单演示。5. 常见踩坑记录与调试经验5.1 编译失败WASM优化未开启这个问题几乎是每个Substrate新手都会撞上的。现象是cargo build --release正常通过但编译WASM runtime时报错提示wasm-opt找不到或者类似rustc wasm32-unknown-unknown target is missing。我在自己的环境里排查的路径是这样先确认wasm target是否已添加。如果已经添加再看是否装了binaryen工具集的wasm-opt。命令行检查rustup target list --installed which wasm-opt没有binaryen的话Ubuntu直接apt install binaryenmacOS用brew install binaryen。如果你用的是Substrate官方给的cargo build --release流程它内部会自动执行一个脚本调wasm-opt做WASM优化。缺了这个工具构建会在最后一步报错。一定要注意能编出node的可执行文件不代表runtime的WASM编出来了它是两套独立的产物。5.2 端口冲突与数据库锁定跑过多个节点之后最头疼的报错就是Cannot create a runtime event connection. Database must be opened in read-write mode. Error: Service(KedOuter(Database ...))这种基本都是因为你试图用同一个base-path启动两个节点或者前一个节点进程没有正常关闭数据库文件处于锁定状态。解决办法很简单先确保旧进程已杀掉再用一个新的base-path启动# 查看残留进程 ps aux | grep node-template # 杀掉旧进程 kill -9 PID如果是端口被占用比如9944或30333被其他程序占了可以换端口启动或者使用lsof -i :9944查占用情况。开发时最省心的做法是每个节点用独立的base-path、独立端口别复用。5.3 运行时升级失败spec_version忘记bump在开发过程中改了runtime代码后没有重置链状态或者没有递增spec_version就去提交新runtime节点会直接拒绝你的runtime升级。报错通常是Runtime construction failed: native runtime requires newer spec_version这个问题很有代表性。因为节点会拿本地的原生runtime编译版本号和链上存储的WASM runtime版本号做比较。如果你链上已经部署了旧版本的WASM现在又部署一个版本号不变的WASM节点就会认为新版本并不“更新”拒绝加载。所以每次修改runtime并且打算部署到链上时记得在runtime/src/lib.rs里把spec_version递增。但注意开发阶段如果只是改了代码还没有上链直接purge-chain重新开发即可不需要递增版本号。否则版本号涨得飞快看着也乱。如果你是在线升级还会涉及authoring_version、transaction_version、api_version这些字段其中transaction_version尤其关键——一旦交易格式有破坏性变更不递增这个版本会导致旧客户端无法解析新交易。5.4 调试三板斧日志、事件订阅与状态查询Substrate的调试体验比很多从零手写的项目好不少。我平时主要用这几个手段第一是看日志。开发模式启动时会打印大量runtime日志但默认级别不够细可以用-lruntimedebug参数调高./target/release/node-template --dev -lruntimedebug这样你的pallet里那些用log::info!或log::debug!打印的内容就能直接看到非常适合检查某段代码是否被执行。第二是事件订阅。用polkadot.js/apps的Developer页面订阅system.events能实时看到每笔交易触发的事件。事件里会带详细信息包括是哪个模块发出的、参数是什么、有没有报错。如果有system.ExtrinsicFailed事件旁边还会跟着一个system.ExtrinsicSuccess的对照方便判断交易是否真的成功。第三是直接查状态。在polkadot.js/apps的Storage页面你可以读取任何pallet的存储项当前值。这个比print调试强得多因为它是链上状态的直接快照无需改代码重跑。有一点要记住Substrate里的存储项不是数据库而是基于StorageMap、StorageValue等抽象构建的key-value结构通过api.query.模块.存储项()可以在前端直接读取。熟练掌握这套查询API排查问题速度会快很多。写在最后的一点个人体会从第一次接触Substrate到跑通一个完整的多节点网络我花的时间不少但回头看这个框架给我最大的帮助不是省了写网络层的功夫而是它逼着我把链的运行逻辑和业务设计逻辑这两件事彻底分开来思考。很多所谓的链改项目实际只是在玩概念真正动手用Substrate搭过、跑过、升级过一条链之后你才会对区块链的运作方式有第一手的理解。如果你是刚入门别急着去追最新版的框架特性先把node-template跑熟把FRAME里几个核心pallet读一遍能把这个基础打牢后面的路会顺畅很多。
返回列表