拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Substrate区块链开发框架:从核心原理到自定义Pallet实战

Substrate区块链开发框架:从核心原理到自定义Pallet实战 1. 从零认识 Substrate它到底是什么能解决什么问题第一次接触 Substrate 的人大概率是被一个词带进来的——造链。在区块链开发这个圈子里Substrate 的名气这几年一直往上走但很多人对它的理解停留在波卡用的框架这个层面再往下问就说不清楚了。我自己最开始也是这样直到真正用它跑起来一条链才慢慢摸清楚它的定位和价值。Substrate 是一个开源的区块链开发框架用 Rust 语言编写。它的核心目标是让开发者能够快速构建一条专属的区块链而不需要从网络层、共识层、存储层一层一层手写。你可以把它理解成一套区块链乐高——底层的基础设施它已经搭好了你只需要挑选和组装自己需要的功能模块就能拼出一条能跑的链。这件事为什么重要因为在 Substrate 出现之前如果你想做一条自己的链摆在面前的路基本只有两条。一条是直接改比特币或以太坊的源码但这两套代码的耦合度很高改起来牵一发动全身而且性能受限于原有架构。另一条是从头写网络通信、共识算法、状态存储、交易池、RPC 接口全部自己实现这个工作量对于小团队来说几乎是不可承受的。Substrate 把这两条路之间的空白填上了它提供了一套模块化的运行时框架让你把精力集中在业务逻辑上而不是重复造底层轮子。那么 Substrate 适合谁我梳理了一下大概有三类人值得花时间研究它。第一类是想做应用链的团队比如你需要一条专门跑游戏资产或者供应链存证的链对通用性要求不高但对性能和定制化要求很高。第二类是区块链底层技术的学习者Substrate 的代码结构清晰模块划分合理是理解区块链运行原理的绝佳教材。第三类是已经在做波卡生态开发的工程师Substrate 是波卡平行链的开发基础绕不开。这篇文章我会从整体设计思路讲起然后拆解核心模块和实操要点接着走一遍完整的搭建流程最后把我在实际开发中踩过的坑和排查经验整理出来。不管你是刚听说 Substrate 的新手还是已经上手但遇到瓶颈的开发者应该都能从中找到对自己有用的部分。2. Substrate 的整体设计与核心思路拆解2.1 为什么选择框架而不是直接改源码要理解 Substrate 的设计先要理解它为什么选择做框架这条路。直接改现有链的源码最大的问题是继承了一堆你不需要的东西。比如你只是想做一条记录设备数据的链但比特币的 UTXO 模型、以太坊的账户模型和 EVM 虚拟机这些对你来说都是负担。它们不仅增加了代码复杂度还限制了你的性能上限。Substrate 的思路是反过来的它把区块链的通用能力抽象成一个个独立的组件每个组件都可以替换。共识算法可以换网络协议可以换甚至运行时的状态转换逻辑也可以完全自定义。这种设计带来的直接好处是你不需要为不需要的功能付出代价。一条基于 Substrate 构建的链最终二进制文件可以控制在几十兆以内启动速度和运行效率都比改造通用链要高得多。另一个关键考量是升级能力。传统区块链的升级非常麻烦因为共识规则写在代码里一旦要改就得硬分叉。Substrate 把运行时的逻辑编译成 Wasm 字节码存在链上状态里。升级的时候只需要提交一个交易把新的 Wasm 字节码写进去链就完成了升级不需要停链也不需要所有节点同时更新客户端。这个设计在工程上的价值非常大它让链的迭代速度从月级别缩短到了天级别。2.2 运行时的模块化设计FRAME 到底解决了什么Substrate 最核心的设计之一就是 FRAME全称是 Framework for Runtime Aggregation of Modularized Entities。这个名字很长但拆开看就清楚了它是一个用来把模块化实体聚合成运行时的框架。在 FRAME 里每个功能模块叫做一个 Pallet。一个 Pallet 可以包含存储项、可调用函数、事件、错误类型和钩子函数。比如 Balances Pallet 负责管理账户余额它定义了余额的存储结构、转账的调用函数、转账成功或失败的事件以及区块开始时需要执行的逻辑。你想给自己的链加一个投票功能就写一个 Voting Pallet定义投票的存储、发起投票的函数、投票结果的事件然后把它注册到运行时里。这种设计的精妙之处在于关注点分离。每个 Pallet 只关心自己的逻辑Pallet 之间的交互通过明确定义的接口进行。这带来的好处是代码可复用性极高。Substrate 官方和社区已经积累了大量的 Pallet涵盖资产、治理、身份、合约等常见场景。你构建一条新链的时候大部分功能可以直接复用现成的 Pallet只需要写少量业务特有的逻辑。我个人的体会是FRAME 的学习曲线主要集中在前一两个 Pallet 的编写上。一旦你理解了 Pallet 的结构和宏的用法后面再写新的 Pallet 就是重复套用模式。真正需要花心思的是 Pallet 之间的依赖关系和权重计算这部分后面会详细讲。2.3 无分叉升级的实现原理无分叉升级是 Substrate 区别于很多区块链框架的重要特性值得单独拿出来说。它的实现依赖于一个关键设计链的运行时逻辑不是硬编码在节点客户端里的而是编译成 WebAssembly 字节码作为链上状态的一部分存储。节点客户端本身只负责网络通信、共识、交易池这些外围工作。当需要执行状态转换时客户端会调用存储在链上的 Wasm 运行时。这意味着只要通过治理流程把新的 Wasm 字节码写入链上状态所有节点在下一个区块就会自动使用新的逻辑不需要更新客户端软件。这个机制在工程上带来几个实际好处。第一升级不需要协调所有节点运营者降低了社区治理的协调成本。第二升级过程可以平滑进行不需要停链。第三如果新版本出现问题可以通过同样的机制快速回滚。当然这个能力也带来了治理上的挑战——升级权限的管理必须非常谨慎否则可能被恶意利用。Substrate 提供了多种治理 Pallet 来管理升级权限从单点控制到社区投票都可以配置。2.4 存储与状态管理的基本逻辑Substrate 使用了一种叫做 Trie 的树形数据结构来存储链上状态。每个存储项在 Trie 中有一个对应的键值就是存储的内容。Trie 的特点是任何状态变化都会导致根哈希变化而这个根哈希会被写入区块头。这样轻节点只需要同步区块头就能验证某个状态是否存在不需要下载全部状态数据。对于开发者来说日常接触更多的是 FRAME 提供的存储抽象。你可以在 Pallet 里用#[pallet::storage]宏声明存储项FRAME 会自动帮你处理键的生成和值的序列化。存储项有几种类型单值存储StorageValue、映射存储StorageMap、双映射存储StorageDoubleMap等。选择哪种类型取决于你的数据访问模式。如果需要按某个键快速查找就用 StorageMap如果需要按两个键联合查找就用 StorageDoubleMap。这里有一个容易被忽视的点存储读写的成本差异很大。读操作相对便宜写操作昂贵得多因为写操作会改变状态根需要在整个网络中传播和验证。所以在设计 Pallet 时要尽量减少不必要的写操作。比如如果一个值在一笔交易中会被多次修改应该先在内存中累积变化最后一次性写入而不是每次修改都写一次存储。3. 核心模块与实操要点深度解析3.1 Pallet 的结构与编写要点一个标准的 Pallet 由几个固定部分组成理解每个部分的作用是编写正确代码的前提。最外层是#[frame_support::pallet]宏它负责把下面的代码转换成 Substrate 运行时能识别的结构。宏内部通常包含配置 trait、存储定义、事件定义、错误定义、可调用函数和钩子函数。配置 trait 是 Pallet 与外部交互的接口。它定义了 Pallet 运行所需的类型和参数比如关联类型RuntimeEvent用来指定事件类型Currency用来指定资产操作接口。通过配置 trait同一个 Pallet 可以在不同的运行时中复用只需要提供不同的具体类型。这是 FRAME 实现模块化的关键机制。存储定义用#[pallet::storage]宏声明。每个存储项需要指定类型和访问方式。比如#[pallet::storage] #[pallet::getter(fn something)] pub type SomethingT StorageValue_, u32;定义了一个 u32 类型的单值存储并自动生成一个名为something的 getter 函数。这里要注意getter 函数默认是只读的如果需要修改存储要用mutate方法。可调用函数用#[pallet::call]宏声明每个函数对应一个可以被外部调用的交易。函数需要标注权重weight权重代表这个函数执行所需的计算资源。权重的计算是 Substrate 开发中的一个难点后面会专门讲。函数内部通过ensure!宏做前置条件检查通过Ok(())返回成功。如果检查失败用Err(Error::T::SomeError.into())返回错误。事件用#[pallet::event]宏声明用来记录链上发生的重要动作。事件会被包含在区块中可以被外部工具索引和查询。错误用#[pallet::error]宏声明用来定义 Pallet 可能返回的错误类型。钩子函数包括on_initialize、on_finalize、on_runtime_upgrade等分别在区块开始、区块结束和运行时升级时执行。3.2 权重计算为什么它比你想的重要权重是 Substrate 里一个容易被低估的概念。简单说权重衡量的是一个操作消耗的计算资源包括执行时间和存储读写。每个可调用函数都必须声明自己的权重这个权重会被用来计算交易手续费也会被用来限制单个区块能容纳的交易数量。为什么权重这么重要因为区块链的计算资源是有限的一个区块能执行的操作数量有上限。如果没有权重机制恶意用户就可以提交大量计算密集型的交易把区块塞满导致正常交易无法被打包。权重机制通过给每个操作标价让资源分配市场化同时保护网络不被滥用。权重的计算通常分两步。第一步是基准测试用 Substrate 提供的 benchmarking 工具在标准硬件上运行函数测量实际消耗。第二步是根据测量结果生成权重公式通常是base_weight component_weight * n的形式其中 n 是某个变量的数量比如循环次数或存储项数量。Substrate 的 CLI 工具可以自动生成基准测试代码和权重文件但你需要自己定义测试场景和参数范围。我踩过的一个坑是早期为了省事直接给所有函数写了一个固定的权重值。测试网跑的时候没问题因为交易量小。但到了压力测试阶段区块经常被打满因为固定权重没有反映实际操作的成本差异。后来老老实实做了基准测试给每个函数生成了独立的权重公式问题才解决。所以我的建议是从第一天就认真对待权重不要等到出问题再补。3.3 存储设计中的取舍与优化存储设计直接影响链的性能和成本是 Pallet 开发中最需要动脑筋的部分之一。Substrate 提供了几种存储类型每种都有适用的场景。StorageValue 适合存储全局唯一的配置或状态比如总发行量、管理员账户。它的读写成本最低因为键是固定的。StorageMap 适合按键查找的数据比如账户余额、资产所有权。它的键是动态生成的读写成本比 StorageValue 略高。StorageDoubleMap 适合需要按两个维度查找的数据比如某个账户在某个资产下的余额。它的查询效率比嵌套 StorageMap 高因为只需要一次 Trie 查找。除了选择存储类型还要考虑数据的组织方式。一个常见的优化技巧是把相关的数据打包成一个结构体用一次存储读写完成操作而不是分成多个存储项分别读写。比如如果你需要存储用户的昵称、头像和签名可以定义一个 Profile 结构体用一个 StorageMap 存储而不是用三个独立的 StorageMap。这样读取用户资料只需要一次 Trie 查找而不是三次。另一个技巧是使用OptionQuery和ValueQuery来控制存储查询的返回类型。OptionQuery在键不存在时返回 None适合需要区分不存在和存在但值为默认值的场景。ValueQuery在键不存在时返回类型的默认值适合不需要区分这两种情况的场景。选择哪种取决于你的业务逻辑选错了可能导致难以排查的 bug。3.4 事件与错误的规范使用事件和错误是 Pallet 与外部世界沟通的桥梁但很多新手会忽视它们的规范使用。事件应该记录所有对链上状态有实质性影响的操作这样外部索引器才能重建链上发生的一切。比如转账操作应该触发一个 Transfer 事件包含发送方、接收方和金额。如果漏了事件链下工具就无法追踪这笔转账。错误的定义要尽量具体不要用一个笼统的 OperationFailed 覆盖所有失败情况。具体的错误类型能帮助调用者快速定位问题也能让链下工具做出更准确的响应。比如余额不足应该返回 InsufficientBalance而不是 TransferFailed。Substrate 的错误类型有数量限制所以要在具体性和数量之间做平衡。事件和错误的命名也有讲究。事件通常用过去式表示已经发生的事情比如 Transfer、VoteCast。错误通常用描述性的名词短语比如 InsufficientBalance、NotAuthorized。保持命名一致能让代码更易读也能让外部工具更容易处理。4. 从零搭建一条 Substrate 链的完整实操4.1 环境准备与工具链安装搭建 Substrate 开发环境的第一步是安装 Rust 工具链。Substrate 对 Rust 版本有要求建议使用官方推荐的版本管理方式。安装完成后需要添加 Wasm 编译目标因为运行时代码需要编译成 Wasm 字节码。# 安装 Rust 版本管理工具 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh # 添加 Wasm 编译目标 rustup target add wasm32-unknown-unknown # 安装 Substrate 相关的 CLI 工具 cargo install --locked substrate-contracts-node安装完成后用rustc --version和cargo --version验证安装是否成功。如果遇到编译错误大概率是 Rust 版本不匹配或者缺少系统依赖。在 Ubuntu 上需要安装build-essential、clang、libssl-dev等包。在 macOS 上需要安装 Xcode 命令行工具。接下来是获取模板。Substrate 官方提供了几种模板最常用的是substrate-node-template它包含了一个最小可运行的链包括基本的 Balances 和 Sudo Pallet。用git clone把模板拉到本地然后进入目录执行cargo build --release。第一次编译会比较慢因为需要编译大量依赖耐心等待即可。注意编译过程中如果出现内存不足的错误可以尝试减少并行编译的任务数用cargo build --release -j 2限制并行度。Substrate 的编译对内存要求较高建议至少 8GB 内存。4.2 运行本地开发链并观察出块编译完成后用./target/release/node-template --dev启动一条开发链。--dev参数会启动一个单节点的开发网络使用预置的 Alice 账户作为出块节点并且会自动清理之前的状态。启动成功后你会在终端看到出块日志每隔几秒产生一个新区块。开发链启动后可以通过 Polkadot.js Apps 这个网页工具连接到本地节点。在设置里把端点改为ws://127.0.0.1:9944就能看到链的实时状态。你可以查看账户余额、提交交易、查询事件这些操作都不需要写代码通过界面就能完成。这个阶段我建议多花点时间熟悉工具。试着用 Alice 账户给 Bob 转账观察余额变化和事件记录。试着查看区块详情理解区块头包含哪些字段。试着查询存储项看看 Pallet 的数据是怎么组织的。这些直观的体验能帮你建立对 Substrate 运行机制的感性认识后面写代码的时候会更有方向感。4.3 编写第一个自定义 Pallet理解了模板链的运行方式后就可以开始写自己的 Pallet 了。我建议从一个简单的功能入手比如一个留言板 Pallet允许用户提交留言并查询留言列表。这个功能足够简单但涵盖了 Pallet 开发的完整流程。首先在pallets目录下创建一个新的目录比如pallets/guestbook。在里面创建Cargo.toml和src/lib.rs。Cargo.toml需要声明依赖包括frame-support、frame-system等。src/lib.rs是 Pallet 的主体代码。Pallet 的代码结构大致如下先声明配置 trait定义 Pallet 需要的类型和参数。然后定义存储项用来保存留言数据。接着定义事件和错误。最后定义可调用函数实现提交留言的逻辑。提交留言的函数需要检查留言内容不为空然后把留言写入存储并触发一个事件。#[pallet::storage] #[pallet::getter(fn messages)] pub type MessagesT: Config StorageMap_, Blake2_128Concat, u32, Vecu8; #[pallet::call] implT: Config PalletT { #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn post_message(origin: OriginForT, content: Vecu8) - DispatchResult { let who ensure_signed(origin)?; ensure!(!content.is_empty(), Error::T::EmptyMessage); let message_id Self::next_message_id(); Messages::T::insert(message_id, content.clone()); Self::deposit_event(Event::MessagePosted { who, message_id, content }); Ok(()) } }写完 Pallet 后需要在运行时的lib.rs里注册它。注册包括实现配置 trait、把 Pallet 加入construct_runtime!宏。注册完成后重新编译启动链就能通过 Polkadot.js Apps 调用post_message函数了。4.4 添加 Pallet 到运行时的完整流程把 Pallet 添加到运行时是连接开发代码和实际链的关键步骤这里容易出各种配置错误。完整流程包括几个环节。第一步是在运行时的Cargo.toml里添加对新 Pallet 的依赖。路径要写对通常是../pallets/guestbook这样的相对路径。第二步是在运行时的lib.rs里实现 Pallet 的配置 trait。配置 trait 的实现需要指定关联类型比如RuntimeEvent要指向运行时的事件类型Currency要指向 Balances Pallet。第三步是把 Pallet 加入construct_runtime!宏。这个宏定义了运行时的组成部分每个 Pallet 需要指定一个索引和名称。索引决定了 Pallet 在运行时中的位置名称用于在代码中引用。第四步是处理事件转换。如果 Pallet 定义了自己的事件类型需要在运行时的事件类型里包含它通常用#[derive(RuntimeEvent)]自动处理。配置完成后执行cargo build --release重新编译。如果编译通过启动链后就能在 Polkadot.js Apps 的开发者-交易页面看到新 Pallet 的可调用函数。如果编译失败仔细看错误信息通常是关联类型没实现或者事件类型不匹配。这类错误信息一般比较明确按提示补全即可。4.5 基准测试与权重生成实操前面提到权重的重要性这里具体讲怎么生成权重。Substrate 提供了 benchmarking 工具可以自动测量函数的执行成本。使用流程分几步。首先在 Pallet 的Cargo.toml里启用runtime-benchmarks特性并添加frame-benchmarking依赖。然后在 Pallet 里创建一个benchmarking.rs文件为每个可调用函数定义基准测试。基准测试需要指定测试场景和参数范围比如留言长度从 1 到 1000 字节测量不同长度下的执行成本。benchmarks! { post_message { let content vec![0u8; 100]; }: _(RawOrigin::Signed(whitelisted_caller()), content) verify { assert!(Messages::T::get(0).is_some()); } }定义好基准测试后在运行时的Cargo.toml里配置runtime-benchmarks特性然后执行cargo build --release --features runtime-benchmarks。编译完成后用./target/release/node-template benchmark pallet命令运行基准测试生成权重文件。权重文件会被放在指定的输出目录然后在 Pallet 里用#[pallet::weight(T::WeightInfo::post_message())]引用。这个过程听起来繁琐但它是保证链安全运行的必要步骤。我见过不少项目为了赶进度跳过基准测试结果上线后要么区块被打满要么手续费计算错误。花半天时间做好基准测试能省掉后面很多麻烦。5. 常见问题与排查技巧实录5.1 编译错误排查速查表Substrate 的编译错误信息有时候比较晦涩尤其是涉及宏展开的错误。我整理了一个常见错误速查表覆盖了大部分新手会遇到的问题。错误信息关键词可能原因解决方法the trait bound ... is not satisfied配置 trait 的关联类型没实现检查运行时里是否实现了所有必需的关联类型cannot find type ... in this scope缺少导入或依赖没添加检查Cargo.toml和use语句expected ... found ...类型不匹配检查函数签名和调用处的类型是否一致the method ... exists but trait ... is not in scope缺少 trait 导入添加对应的use语句wasm32-unknown-unknown target not found缺少 Wasm 编译目标执行rustup target add wasm32-unknown-unknownlinker ... not found缺少系统链接器安装clang和build-essential除了表格里的问题还有一个常见情况是宏展开后的错误信息指向了宏内部而不是你的代码。这时候要看错误信息里的行号找到对应的宏调用然后检查宏参数是否正确。如果实在看不懂可以把宏展开后的代码打印出来看用cargo expand命令可以做到。5.2 运行时升级中的典型问题运行时升级是 Substrate 的强项但操作不当也会出问题。我遇到过几次升级相关的事故总结下来有几个典型场景。第一种是存储迁移没做。如果你在新版本里修改了存储结构比如给一个 StorageMap 的值类型加了字段旧数据在新代码下会解析失败。这时候需要写存储迁移逻辑在on_runtime_upgrade钩子里把旧数据转换成新格式。存储迁移要小心处理最好先在测试网验证确认无误再上主网。第二种是权重文件没更新。如果你修改了可调用函数的逻辑但没有重新生成权重新函数的实际成本可能超过声明的权重导致区块执行超时。每次修改函数逻辑后都要重新跑基准测试生成权重。第三种是版本号没改。Substrate 的运行时有一个spec_version每次升级都要递增。如果忘了改节点可能无法正确识别新版本导致升级失败。这个错误很低级但确实容易犯建议把版本号更新加入升级检查清单。5.3 性能调优的实战经验链跑起来之后性能调优是绕不开的话题。我在这方面积累了一些经验分享几个最有效的优化方向。第一个方向是减少存储写入。前面提过写操作比读操作昂贵得多。我做过一个测试把一笔交易中的多次存储写入合并成一次区块执行时间减少了将近 40%。具体做法是在函数内部用局部变量累积变化最后一次性写入存储。第二个方向是优化数据结构。比如如果你需要频繁查询某个键是否存在用 StorageMap 比用 StorageValue 存储一个列表要高效得多。StorageMap 的查询是 O(1) 的 Trie 查找而遍历列表是 O(n) 的。数据量大的时候这个差异非常明显。第三个方向是合理使用缓存。Substrate 的存储层有缓存机制但缓存的大小和策略可以调整。在节点启动参数里可以配置缓存大小根据你的硬件资源适当调大缓存能减少磁盘 IO提升执行速度。不过缓存不是越大越好要平衡内存占用和性能收益。5.4 调试技巧与工具推荐调试 Substrate 代码有一些专门的技巧和工具掌握它们能大幅提升效率。第一个技巧是用日志。Substrate 集成了日志框架你可以在代码里用log::info!、log::debug!等宏输出日志。启动节点时用-ldebug参数可以开启调试日志。日志会输出到终端也可以配置输出到文件。我习惯在关键逻辑处加日志排查问题时能快速定位。第二个技巧是用单元测试。Substrate 的 Pallet 可以写单元测试用new_test_ext()创建一个模拟的运行时环境然后调用函数并断言结果。单元测试跑得快适合在开发过程中频繁运行。我建议每个可调用函数都至少写一个测试覆盖正常情况和异常情况。第三个技巧是用 Polkadot.js Apps 的链状态查询功能。你可以直接查询任意存储项的值不需要写代码。这在排查存储相关问题时特别有用能直观地看到链上数据的状态。配合事件查询可以重建一笔交易的完整执行过程。6. 我在实际开发中积累的一些体会写到这里Substrate 的核心概念、实操流程和排查技巧基本都覆盖了。最后分享几点我在实际开发中积累的体会这些是文档里不会写、但真正做项目时很有用的经验。第一点是关于学习路径。Substrate 的官方文档很全但信息量大新手容易迷失。我的建议是先跑通模板链然后写一个最简单的 Pallet再逐步增加复杂度。不要一上来就啃 FRAME 的源码那样容易受挫。先会用再理解原理这个顺序对 Substrate 特别适用。第二点是关于社区资源。Substrate 的社区很活跃遇到问题可以在论坛或聊天群里提问。但提问之前先自己搜索大部分常见问题都有现成的答案。提问的时候把错误信息、代码片段和你已经尝试过的解决方法都贴出来这样别人才能高效地帮你。第三点是关于版本管理。Substrate 的版本更新比较频繁不同版本之间的 API 可能有变化。建议锁定一个稳定版本不要频繁升级。如果必须升级先在小项目上验证确认没问题再迁移到主项目。升级前一定要备份代码和链数据以防万一。第四点是关于测试。Substrate 项目一定要重视测试包括单元测试、集成测试和压力测试。单元测试保证逻辑正确集成测试保证 Pallet 之间协作正常压力测试保证链在高负载下稳定运行。我见过太多项目因为测试不足上线后才发现问题修复成本比提前测试高得多。第五点是关于文档。写 Pallet 的时候顺手写文档包括配置 trait 的说明、存储项的含义、可调用函数的参数和返回值。这些文档不仅方便别人理解你的代码也方便你自己几个月后回顾。Substrate 的代码注释可以用///和//!生成的文档可以用cargo doc查看。这个内容后续还可以这样扩展研究跨链消息传递的实现把自定义 Pallet 和 XCM 集成起来探索智能合约 Pallet在 Substrate 链上运行兼容 EVM 的合约研究共识算法的替换把默认的 Aura 换成更复杂的共识机制。每一个方向都够写一篇独立的文章等我在实际项目中实践之后再找机会分享。
返回列表