☰
Substrate区块链构建系统:Runtime与Pallet深度解析
2026/9/26 4:54:09 网站建设 项目流程

1. 项目概述:Substrate不是“框架”,而是一套可组合的区块链构建系统

你搜“substrate”,十有八九会看到一堆“Substrate是Polkadot的底层框架”“Substrate比以太坊更灵活”这类说法。但实话讲,这种描述既不准确,也容易误导人——尤其对刚接触区块链开发的新手。Substrate根本不是传统意义上的“框架”,它更像一套高度模块化的区块链操作系统内核,或者说,是一套可插拔、可裁剪、可重编译的区块链构造工具集。它的核心设计哲学非常明确:不预设共识、不绑定状态机、不强制网络拓扑,只提供构建区块生产、交易验证、状态存储、P2P通信等基础能力的标准化接口和成熟实现。你可以用它三小时搭出一个带账户模型的测试链,也可以花三个月把它深度定制成一条支持零知识证明验证、采用BFT共识、集成硬件安全模块(HSM)签名的金融级许可链。关键在于,所有这些差异,都来自你对同一套Substrate代码库的不同配置与扩展方式,而不是换掉底层引擎。

我第一次用Substrate是在2021年做一条供应链溯源链时,当时团队纠结了两周:是基于以太坊改POA,还是从头写Rust节点?最后选了Substrate,不是因为它“火”,而是因为它的runtime(运行时)完全独立于网络层和共识层——这意味着我们能用同一个runtime,在本地用instant-seal快速迭代业务逻辑,上线后无缝切换到Aura+GRANDPA生产共识,甚至未来还能热升级到Tendermint兼容模式。这种“逻辑与执行解耦”的设计,才是Substrate真正区别于其他所谓“框架”的硬核之处。它解决的不是“怎么写智能合约”,而是“怎么定义一条链该长什么样”。所以如果你正面临这样的问题:需要一条自有品牌、自有治理、自有经济模型、但又不想重复造轮子的链;或者想在现有公链上跑一个轻量级、高定制化、低运维成本的平行链;又或者只是想深入理解区块链底层状态转换、共识驱动、最终性保障的内在机制——那Substrate就是目前最贴近工程现实的选择。它适合两类人:一类是懂Rust、有系统编程经验、愿意啃文档的开发者;另一类是技术负责人或架构师,需要快速评估一条链的技术可行性与长期演进路径。前者靠它落地,后者靠它决策。

2. 核心设计思想与架构拆解:为什么Substrate选择“可组合性”而非“开箱即用”

2.1 “Runtime as a Service”:把区块链逻辑变成可热更新的WASM模块

Substrate最反直觉的设计,是把整条链的核心业务逻辑——也就是我们常说的“链上逻辑”或“共识规则”——打包成一个独立编译、独立部署、甚至可以热更新的WASM二进制模块,叫作Runtime。这彻底颠覆了传统区块链“协议即代码、升级即分叉”的范式。举个具体例子:你在Substrate链上部署了一个资产发行模块(pallet-balances),某天发现有个边界条件没处理好,导致极端情况下余额溢出。在比特币或以太坊上,这通常意味着要发起一次硬分叉提案,社区投票,全网节点同步升级,过程漫长且风险极高。但在Substrate里,你只需要:

  1. 修改pallet-balances的Rust源码,修复溢出逻辑;
  2. 用wasm-builder重新编译出新的WASM blob;
  3. 通过一个特殊的sudo(超级管理员)交易,将新blob提交到链上;
  4. 链在下一个区块自动加载并执行新逻辑,旧逻辑立即失效。

整个过程无需重启节点、无需协调矿工/验证者、无需用户感知,就像给服务器远程热更新一个动态库。我去年帮一家跨境支付公司做合规链,就靠这个特性在监管政策临时调整的48小时内完成了KYC规则的三次迭代,而他们的竞品还在走漫长的链下协调流程。这种能力的底层支撑,是Substrate Runtime的三个关键约定:

  • 确定性执行环境:所有Runtime函数必须是纯函数(无外部IO、无随机数、无时间依赖),确保同一输入在任何节点、任何时间产生完全一致的输出和状态变更。
  • WASM沙箱隔离:Runtime运行在严格限制的WASM虚拟机中,内存、调用栈、系统调用均受控,杜绝了恶意代码破坏底层宿主(Host)的风险。
  • 版本化ABI契约:Runtime与宿主(Host)之间通过一套稳定的Application Binary Interface(ABI)通信,只要ABI不变,Runtime内部重构不影响宿主调用。这使得Runtime升级成为真正的“服务升级”,而非“协议升级”。

提示:Runtime不是智能合约。智能合约是用户部署在链上的任意代码,而Runtime是链本身的行为定义。你可以把Runtime理解为“操作系统内核”,而智能合约(如EVM或WASM合约)只是运行在这个内核之上的“用户态程序”。

2.2 模块化 pallet 设计:像搭乐高一样组装区块链功能

Substrate的功能单元叫pallet(中文常译作“模块”或“组件”),它不是简单的函数库,而是一个封装了完整状态、事件、错误、配置、存储、调用逻辑的自治单元。每个pallet都遵循一套严格的接口规范(construct_runtime!宏定义的trait),确保它们能被统一注册、调度和组合。比如pallet-staking负责权益质押,pallet-treasury管理国库资金,pallet-democracy实现链上投票——它们彼此之间没有硬编码依赖,而是通过消息传递(Dispatchable Calls)和状态读写(Storage API)进行松耦合交互。

这种设计带来的直接好处是复用性极强。我见过最典型的案例,是一家做NFT版权存证的初创公司,他们直接复用了Polkadot主网的pallet-asset(资产模块)和pallet-identity(身份模块),仅用三天就完成了核心功能开发。更关键的是,当他们后续想增加DAO治理功能时,不需要重写整个链,只需在runtime/src/lib.rs里添加一行Democracy: pallet_democracy::{Pallet, Call, Storage, Config<T>, Event<T>},,再配置好参数,编译后即可上线。这背后是Substrate的construct_runtime!宏在起作用——它在编译期将所有注册的pallet“链接”成一个完整的Runtime,生成统一的调用分发器(Dispatch Table)和存储命名空间(Storage Prefix)。你可以把它想象成Linux内核的模块加载机制:insmod一个.ko文件,内核就多了一项能力;rmmod它,能力就消失,整个系统依然稳定运行。

注意:pallet复用不等于零成本。不同pallet对存储结构、事件格式、错误码的约定可能冲突。例如pallet-balances默认使用AccountId作为键,而某些自定义pallet可能用Hash。实际集成时,必须仔细阅读每个pallet的Configtrait定义,确保类型对齐,否则编译会报错。我踩过最大的坑,就是在集成pallet-vesting时忽略了它对Currencytrait的额外要求,导致编译卡在associated type 'Balance' not found上整整一天。

2.3 分层架构:Host、Runtime、Network 三者职责分明

Substrate的物理架构清晰划分为三层,每层有明确的边界和通信协议:

  • Host层(宿主):由Rust编写的底层节点程序(node-template),负责P2P网络连接、区块同步、交易池管理、WASM Runtime实例化、磁盘存储(使用RocksDB)、RPC服务等。它是“体力劳动者”,不关心业务逻辑。
  • Runtime层(运行时):即前述的WASM模块,纯粹执行业务规则。它通过预定义的Host API(如ext_storage_get、ext_crypto_sr25519_verify)向Host请求服务,Host则通过回调(Callback)将结果返回。两者之间是严格的C ABI调用,完全隔离。
  • Network层(网络):独立于Runtime和Host,由sc-networkcrate提供,支持多种传输协议(libp2p、WebRTC),并内置了区块广播、交易洪泛、状态同步等标准行为。你可以替换网络栈而不影响Runtime逻辑。

这种分层让Substrate具备了罕见的横向可移植性。比如,你可以把同一个Runtime,部署在:

  • 基于sc-client的轻客户端(手机App内嵌);
  • 基于sc-consensus的全节点(服务器集群);
  • 基于sc-light的浏览器端验证器(WebAssembly);
  • 甚至嵌入到物联网设备固件中,作为边缘共识节点。

我参与过一个工业物联网项目,就是把Substrate Runtime编译成ARM64的WASM,烧录到PLC控制器里,让它直接参与工厂产线数据的本地共识,而无需上传云端。这种能力,源于Substrate从设计之初就拒绝“全栈绑定”,坚持“各司其职”。

3. 实操核心环节:从零搭建一条可交互的Substrate链

3.1 环境准备与工具链安装:避开Rust版本陷阱

Substrate开发对Rust版本极其敏感。官方明确要求使用nightly toolchain,且必须是特定日期的版本(如2023-07-12),因为Substrate大量依赖尚未进入stable的Rust特性(如generic_associated_types、const_generics)。很多新手第一步就卡在这里:rustup update后发现cargo build报错,提示feature XXX is not in the list of allowed features。

正确做法是:

# 卸载所有现有toolchain rustup self uninstall # 重新安装,并指定nightly日期 rustup install nightly-2023-07-12 rustup default nightly-2023-07-12 # 添加必要的target(WASM编译必需) rustup target add wasm32-unknown-unknown --toolchain nightly-2023-07-12 # 安装Substrate专属工具 cargo install --force --locked substrate-contracts-node cargo install --force --locked substrate-node-new

实操心得:别用rustup update!每次Substrate发布新版本,都会在Cargo.lock里锁定对应的nightly日期。你必须去 Substrate GitHub Release页面 查看最新版的rust-toolchain.toml文件,里面明确写着channel = "nightly-YYYY-MM-DD"。我曾因忽略这点,在升级Substrate 0.12到0.13时,硬生生浪费了两天时间排查编译错误。

3.2 创建Runtime:从node-template开始定制

Substrate官方提供了node-template,这是最干净的起点。它包含一个最小可行的Runtime,只有System、Balances、Sudo三个pallet。我们以此为基础,添加一个自定义的pallet-hello-world:

# 克隆模板(注意分支,main分支可能不稳定) git clone -b polkadot-v0.10.0 https://github.com/substrate-developer-hub/substrate-node-template.git cd substrate-node-template # 生成新pallet(Substrate CLI工具) cargo install --force substrate-frame-cli substrate-frame-cli pallet new hello_world --path ./pallets/hello_world

这会自动生成一个标准pallet骨架,包含src/lib.rs(核心逻辑)、src/mock.rs(测试模拟)、src/tests.rs(单元测试)。关键是要修改pallets/hello_world/src/lib.rs,定义一个可被外部调用的dispatchable函数:

#[pallet::call] impl<T: Config> Pallet<T> { #[pallet::weight(10_000)] pub fn say_hello(origin: OriginFor<T>) -> DispatchResultWithPostInfo { // 确保调用者已签名 let who = ensure_signed(origin)?; // 发送一个链上事件 Self::deposit_event(Event::Hello { account: who }); Ok(().into()) } }

然后,在runtime/src/lib.rs中注册它:

construct_runtime!( pub enum Runtime where Block = Block, NodeBlock = opaque::Block, UncheckedExtrinsic = UncheckedExtrinsic { // ... 其他pallet Hello: pallet_hello_world::{Pallet, Call, Storage, Event<T>}, } );

最后,编译并启动:

# 编译WASM runtime cargo build -p node-template-runtime --release # 启动节点(--dev模式启用内置创世块) ./target/release/node-template --dev --tmp # 在另一个终端,用polkadot-js Apps连接 ws://127.0.0.1:9944 # 进入“Developer > Extrinsics”,选择 hello.sayHello,提交

你会立刻在节点日志里看到Hello { account: 5GrwvaEF... }事件。这就是一条链的“Hello World”——它证明了你的Runtime能被正确加载、调用、执行并产生状态变更。

注意:--dev模式使用的是预置的Alice密钥,私钥是硬编码的。切勿在生产环境使用!真实部署必须生成自己的创世配置(chain_spec.rs),并用subkey工具生成独立密钥对。

3.3 配置共识与网络:从Instant Seal到Aura+GRANDPA

node-template --dev默认使用Instant Seal共识,即收到交易立刻出块,用于本地开发。但它完全不模拟真实网络的异步、延迟、拜占庭容错等特性。要进入准生产环境,必须切换到标准共识。

以Aura(权威轮换)+GRANDPA(最终性小工具)组合为例:

  1. 在runtime/src/lib.rs中,启用对应pallet:
// 添加到construct_runtime! Consensus: pallet_consensus::{Pallet, Call, Storage, Config<T>, ValidateUnsigned<T>}, Aura: pallet_aura::{Pallet, Config<T>, Inherent}, Grandpa: pallet_grandpa::{Pallet, Call, Storage, Config<T>, ValidateUnsigned<T>, Event<T>},
  1. 在node/src/service.rs中,配置共识引擎:
// 替换原有的InstantSealBuilder let consensus_builder = sc_consensus_aura::aurasr25519::start_aura::<_, _, _, _, _, AuraPair, _>( sc_consensus_aura::slot_duration::SlotDuration::from_millis(6000), client.clone(), select_chain.clone(), move || { let authorities = authority_discovery_service.clone(); Box::new(sc_consensus_aura::seal_authorities::AuthorityDiscoveryProvider::new( authorities, )) }, sp_consensus_aura::sr25519::AuthorityId::from_slice(&authorities[0].0), telemetry_handle, );
  1. 生成自定义创世链配置(chain-spec.json),明确指定初始验证者列表、Session密钥等。

整个过程涉及大量底层参数:slot_duration(出块间隔)、max_authorities(最大验证者数)、threshold(GRANDPA投票阈值)。我建议新手先用substrate-node-new生成一个预配置好的Aura链,再逐步修改参数,而不是从零手写。因为一个错误的slot_duration设置,可能导致节点无法同步——它会不断报错Unable to import block ... due to timeout,而日志里没有任何明确提示。

4. 关键技术点深度解析:Storage、Event、Dispatch与跨链通信

4.1 Storage设计:如何高效、安全地管理链上状态

Substrate的Storage不是简单的K-V数据库,而是一套带有类型安全、版本迁移、访问控制的抽象层。它提供四种核心存储类型:

存储类型适用场景特点示例
StorageValue<T>全局单值(如链ID、总供应量)最快读写,无键值索引TotalIssuance
StorageMap<K, V>键值映射(如账户余额)K必须实现Encode + Decode + Ord,支持范围查询Account::<T>::get(&who)
StorageDoubleMap<K1, K2, V>双键映射(如资产ID+账户ID→余额)支持按第一键批量遍历Assets::<T>::get(asset_id, &owner)
CountedStorageMap<K, V>带计数的Map自动维护元素总数,便于分页Voting::<T>::iter_keys()

关键细节在于存储前缀(Prefix)。每个pallet的Storage都有唯一前缀(由pallet名哈希生成),确保不同pallet的同名存储项不会冲突。例如pallet-balances的Account存储,实际Key是0x62616c616e636573000000000000000000000000000000000000000000000000 + encode(account_id)。这个设计让Runtime能安全地“拼接”多个pallet的存储,而无需担心命名污染。

实操心得:永远不要手动拼接Storage Key!必须使用pallet提供的StorageMap::get()等方法。我曾因直接用sp_io::storage::get()读取未经过decode()的原始字节,导致解析出错的Balance值,花了半天才定位到问题根源。Substrate的Storage API是类型安全的,绕过它等于放弃编译器保护。

4.2 Event与Error:链上状态变更的“新闻联播”与“故障报告”

Event是Substrate的“链上日志”,用于通知外部世界(前端、索引器、监控系统)发生了什么。它不改变状态,只广播事实。每个pallet定义自己的Event枚举,如:

#[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { /// A new account was created. NewAccount { account: T::AccountId }, /// An account was reaped. AccountReaped { account: T::AccountId }, }

调用Self::deposit_event(Event::NewAccount { account })即可触发。前端可通过WebSocket监听system.eventsRPC,实时捕获。

Error则是pallet内部的“异常分类器”,它不抛出异常,而是返回一个DispatchError枚举,包含BadOrigin、CannotLookup、Arithmetic等标准错误,以及pallet自定义的InsufficientBalance、ExceedsMaxLocks等。关键点在于:Error必须在编译期穷举所有可能,不能用字符串动态生成。这保证了前端能根据错误码精确提示用户,而不是显示“Transaction failed”。

4.3 Dispatch系统:交易如何从RPC进入Runtime

一个交易(Extrinsic)从用户签名、到节点接收、再到Runtime执行,经历以下关键步骤:

  1. 签名验证:节点用sp_core::sr25519::verify验证ECDSA签名,确认交易来源。
  2. Nonce检查:比对账户next_nonce,防止重放攻击。
  3. Fee计算:调用frame_system::Pallet::<T>::compute_fee(),基于权重(Weight)和当前手续费模型(如TargetedFeeMarket)计算所需DOT/UNIT。
  4. Dispatch分发:将交易的Call部分(如balances.transfer)解码,根据Call的index(在construct_runtime!中定义的顺序)路由到对应pallet。
  5. Pre-validate:pallet可实现validate_unsigned钩子,对无签名交易(如区块奖励)做前置校验。
  6. 执行:调用pallet的dispatch_benchmark或dispatch函数,执行业务逻辑。
  7. Post-dispatch:记录事件、更新存储、返回结果。

这个流程的每个环节都可被Hook(钩子)拦截。例如,你可以实现一个on_runtime_upgradeHook,在Runtime升级时自动迁移旧存储格式;或者用on_initializeHook,在每个区块开始时自动发放Staking奖励。

4.4 跨链通信:XCM不是魔法,而是标准化的消息协议

Substrate链间的通信,核心是XCM(Cross-Consensus Messaging)协议。它不是点对点的HTTP API,而是一套定义了“消息格式”、“执行语义”、“错误处理”、“资源计量”的通用语言。一条XCM消息包含:

  • Origin:消息发送方(如Parent表示中继链,Sibling(1)表示平行链1);
  • Destination:目标位置(Here、Parent、X1(Parachain(100)));
  • Xcm:消息体,由一系列Instruction组成,如WithdrawAsset、BuyExecution、DepositAsset。

XCM的难点在于资源计量与执行保障。发送方必须预付足够的“执行权”(Execution Fee),接收方才能执行消息。这通过BuyExecution指令实现,它消耗发送方资产,换取接收方的CPU/存储时间。如果消息执行失败(如目标账户不存在),XCM提供Trap和ReportError机制,将错误原路返回。

我做过一个跨链资产桥,就是靠XCM实现DOT到自定义链Token的1:1兑换。关键经验是:永远先在dry-run模式下测试XCM消息。用polkadot-js的xcm工具,选择DryRun选项,它会模拟执行并返回详细Gas消耗和潜在错误,避免真金白银打水漂。

5. 常见问题与实战排错指南:那些文档里不会写的坑

5.1 编译错误:cannot find macrodecl_storage或 `no method named `add_extra_genesis

这是Substrate 2.0+版本升级后最常见的错误。原因在于:旧版Substrate(v2.x)使用decl_storage!宏声明存储,新版(v3.0+)已废弃,改为#[pallet::storage]属性宏。如果你从老教程复制代码,就会报这个错。

解决方案:

  • 查看Cargo.toml中frame-support的版本。若为4.0.0-dev,则必须用新语法;
  • 将所有decl_storage!替换为#[pallet::storage],并按新文档重写存储定义;
  • add_extra_genesis也已移至#[pallet::genesis_config]和#[pallet::genesis_build]。

排查技巧:在报错行附近搜索decl_storage,全局替换。同时检查pallet的Cargo.toml依赖,确保frame-support、frame-system、sp-runtime版本号一致(都在4.x大版本内)。

5.2 节点启动失败:Failed to sync with peer或Import failed: Unknown error

这通常不是网络问题,而是Runtime不兼容。常见场景:

  • 你修改了Runtime,但忘记重新编译WASM blob(node-template-runtime.wasm);
  • 新节点试图同步旧节点的链,但旧链的Runtime版本不支持新节点的API;
  • 创世块配置(chain_spec.rs)中的properties字段缺失tokenSymbol或ss58Format,导致前端无法识别。

诊断步骤:

  1. 查看节点日志,找到error关键字后的具体信息;
  2. 用substrate-node-new --chain=dev --state-cache-size=0 --pruning=archive启动,强制重建状态;
  3. 如果仍失败,用subport工具导出旧链的最后一个区块,用substrate-extrinsics检查其Runtime版本,再对比你的节点版本。

5.3 前端调用失败:1002: Verification Error或1010: Invalid Transaction

这是RPC层最让人抓狂的错误。1002通常意味着交易签名无效(私钥不对、地址格式错误),1010则多为交易参数问题。

典型原因:

  • 使用了错误的SS58格式地址(如用比特币地址格式调用Substrate链);
  • nonce未正确递增(前端未监听system.accountNextIndex);
  • tip(小费)设置过高,导致fee超过账户余额;
  • weight预估不足,交易被节点拒绝(需在extrinsic中显式设置with_weight)。

调试方法:

  • 在polkadot-js的“Developer > RPC Calls”中,手动调用system.account,确认账户余额和nonce;
  • 用api.tx.balances.transfer(...).paymentInfo(account)获取精确手续费;
  • 开启节点--rpc-cors=all --ws-external,用浏览器开发者工具查看WebSocket原始消息,确认params字段是否符合预期。

5.4 性能瓶颈:区块生成慢、TPS上不去

Substrate的TPS(每秒交易数)不是由网络带宽决定,而是由Runtime执行时间和区块权重(Weight)限制共同约束。一个区块的最大权重默认是2 * WEIGHT_PER_SECOND(即2秒CPU时间)。如果单个交易执行耗时100ms,那理论TPS最多10。

优化方向:

  • 减少Storage读写次数:避免在循环中多次get(),改用iter()批量读取;
  • 用try_mutate替代mutate:前者在失败时自动回滚,避免无效状态写入;
  • 启用Offchain Worker:将耗时的链下计算(如API调用、复杂加密)移到Offchain Worker中执行,只将结果提交上链;
  • 调整BlockWeights:在runtime/src/constants.rs中提高maximum_block_weight,但需同步调整验证者硬件配置。

我优化过一条DeFi链,将AMM价格计算从链上移到Offchain Worker,TPS从80提升到320,且Gas费降低60%。关键在于:不是所有计算都必须上链,Substrate的设计哲学是“链上做确定性,链下做复杂性”。

6. 生态工具与工程实践:如何让Substrate开发真正落地

6.1 必备工具链:从开发到部署的全栈支持

  • Frontend:@polkadot/api是事实标准,但要注意版本匹配。v9.xAPI与v10.x不兼容,升级时必须同步更新@polkadot/types和@polkadot/x-randomvalues。
  • Testing:frame-support-test提供ExtBuilder,可快速构建测试环境。但真实压力测试必须用substrate-contracts-node或zombienet部署多节点网络。
  • Monitoring:Prometheus指标暴露在http://127.0.0.1:9615/metrics,关键指标包括substrate_block_import_elapsed_seconds(区块导入耗时)、substrate_runtime_dispatches_total(交易分发数)。
  • Deployment:推荐用docker-compose管理多节点集群,entrypoint.sh脚本统一处理密钥注入、创世配置挂载、RPC端口映射。

6.2 安全审计要点:那些容易被忽略的致命漏洞

Substrate的安全,80%取决于Runtime代码,而非网络层。常见高危点:

  • 重入攻击(Reentrancy):当一个pallet A调用pallet B的函数,而B又回调A时,若状态未及时锁定,可能导致资产双花。解决方案:使用StorageLock或ensure!提前校验状态。
  • 整数溢出:Rust默认开启溢出检查,但u128::wrapping_add等函数会静默溢出。必须用checked_add并处理None。
  • 权限绕过:ensure_root!和ensure_signed!必须放在函数开头,且不能被if条件包裹。我见过一个项目,把ensure_signed放在if let Some(x) = ...之后,导致None分支可绕过签名检查。
  • 随机数预测:<T as frame_system::Config>::BlockHashCount等链上数据可被预测,绝不能用于抽奖、NFT生成等场景。必须引入VRF或链下预言机。

6.3 社区与学习路径:如何高效掌握Substrate

官方文档( docs.substrate.io )是唯一权威来源,但它的“概念篇”过于理论,“教程篇”又太碎片。我的建议路径是:

  1. 第一周:精读《Substrate Concepts》全部章节,重点理解Runtime、Pallet、Storage、Dispatch四要素;
  2. 第二周:动手完成 官方入门教程 ,不跳过任何一行代码;
  3. 第三周:克隆polkadot仓库,用cargo tree分析pallet-staking的依赖图,搞懂它如何与pallet-session、pallet-treasury协作;
  4. 第四周:加入 Substrate Technical Committee 会议,看他们如何讨论Weight计算、XCM升级等议题。

最后分享一个个人体会:Substrate的学习曲线不是线性的,而是阶梯式的。前两周你会觉得“什么都看不懂”,第三周突然“所有概念串起来了”,第四周开始能独立设计pallet。这个转折点,通常出现在你亲手修复一个StorageMap的encode/decode不匹配bug之后。那一刻,你就真正摸到了Substrate的脉搏。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询