news 2026/9/26 18:46:27

Substrate实战:从核心架构到自定义Pallet开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Substrate实战:从核心架构到自定义Pallet开发

1. substrate是干嘛的——先把这词说透了

很多人第一次看到“substrate”这词,第一反应是“酶反应的底物”,或者“PCB板子上的基材”。在区块链开发圈里,Substrate是Parity团队开源的一套用来构建自定义区块链的框架,而且是用Rust写的。你可以把它理解成一套乐高式的链开发工具包:共识、P2P网络、状态存储、交易池、数据库这些链底层该有的东西,Substrate全给你封装好了,你只需要把你的业务逻辑写进去,编译一下,一条链就跑起来了。

这背后解决的问题非常实在。早几年想发一条链,要么从零撸一个Bitcoin或Ethereum的分叉,改共识改挖矿逻辑,改得痛不欲生;要么基于以太坊EVM写智能合约,虽然不用碰底层,但受限于Solidity的沙盒和执行模型的限制,想做自定义的签名方案、自定义的共识、自定义的费用模型,基本是想都别想。Substrate就是在这两者之间给你开了一条中间路:底层基础设施全给你,上层业务自由发挥,想改哪一层都能改,而且改动成本比分叉一条链低得多。

这篇文章的目标读者是两类人:一类是想搞懂“Substrate到底是什么、链是怎么跑起来的”的技术爱好者,另一类是正准备上手接一个链项目的开发者。我会把Substrate的核心架构拆开讲,然后带你把一条链跑起来,再写一个自定义模块(pallet),最后把我在实际开发和排错中踩过的坑直接列出来。看完你应该能对Substrate有一条清晰的认知,并且能动手搭出自己的第一条链。

2. 核心架构:一条Substrate链到底由什么组成

2.1 外部节点和运行时:谁在跑共识,谁在执行逻辑

Substrate最核心的设计,是把一条链分成了两层:外部节点(Client)和运行时(Runtime)。

外部节点是一台机器上跑的那个二进制程序,它负责跟其他节点通信、同步区块头、维护数据库、验证交易格式、跟对等节点交换 gossip 消息。说白了,它是区块链网络的"身体"——保证节点之间能连接起来,拿到相同的链上数据。

运行时则是那条链的"大脑"——决定一条交易提交上来之后,状态会发生什么变化,手续费扣多少,新增的块是否合法,质押了多少、出块奖励发多少,这些都是运行时里定义的逻辑。在Substrate里,运行时是一份会被编译成WebAssembly(Wasm)的代码,并且这段Wasm的哈希会直接存到链上状态里。

为什么要把运行时的Wasm存到链上?这是Substrate特别聪明的一个点。因为每一个全节点在验证别人的区块时,必须使用同一份业务逻辑去重放交易、检查状态转移的结果,才能保证"同一个世界,同一个状态"。如果把逻辑写死在一个旧版本的二进制里,那每次升级都要硬分叉,所有节点被迫停掉换新程序。但把Wasm存上链之后,当新块发布时节点读到新Wasm,就会用新逻辑去验证后续的块——这就是所谓的无分叉运行时升级。

这段Wasm的存储还可以再细讲一点。运行时在链上有一份"当前运行时"和一个"待升级的运行时"。验证交易时,全节点会从自己的存储里把那份Wasm代码加载出来,跑在一个轻量的Wasm虚拟机上。而整个状态转移过程本身又是确定性的:同样一组输入,在任何一台机器上执行,产出的状态根哈希完全一致。确定性这一点是区块链验证的基础,也是为什么智能合约平台最终都要走到Wasm这个方向上的原因——Wasm既够快,又能在多个环境中得到一致结果,而且生态工具链成熟。

这个Client和Runtime的分离,对开发者来说简直是个解放。你写链业务时,几乎不用关心网络层是怎么建块的、交易怎么广播的——框架全给做了。你只需要盯着自己定义的调度逻辑,把它编译成Wasm,就能完成一次链的升级。

2.2 Pallet:把业务逻辑拆成一个一个小积木

Substrate里可复用的业务模块叫Pallet(就是"栈板、货盘"那个pallet,一下装很多东西的那个意思)。如果不喜欢Pallet这个名字,你可以叫它"模块"或"积木",概念上就是以太坊里一个个"合约"的升级版——区别在于Pallet不是跑在一个受限的虚拟机沙盒里,而是直接编译进运行时,跑起来就是原生级别的执行速度。

Pallet之间用宏(derive宏)组合起来,这些宏来自一个叫FRAME(Framework for Runtime Aggregation of Modular Entities)的框架。FRAME负责处理模块的声明、类型注入、事件定义、错误定义、存储集成等等,最终生成一个完整的运行时结构体。这有点像写一个大型后端项目时用依赖注入框架去装配各个服务,Pallet之间可以互相调用彼此的接口,比如一个"众筹"Pallet可以调用"余额"Pallet来锁定用户的代币。

官方自带了很多常用的Pallet,这里列几个最常见的:

  • pallet_system:所有运行时必需的内置基础模块,处理区块头、交易权重、存储清理等底层逻辑。每个运行时都有它,且必排第一位。
  • pallet_balances:管理账户余额,处理转账、锁仓、交易费扣除,相当于链上的"中央银行"记账模块。
  • pallet_sudo:超级管理员权限——允许某个指定账户(sudo账户)执行运行时里的任意调用。开发调试时你用它可以随时调余额、改参数,特别方便,但主网上一般会去掉这个模块。
  • pallet_utility:提供批量调用、作为其他账户代理执行等能力,方便应用侧做复杂合约交互。

你也可以写自己的Pallet,这是Substrate开发里最常规的扩展点。Pallet由几部分构成:存储项(Storage)、事件(Event)、错误(Error)、可调用函数(Callable)、状态钩子(Hooks,可选)。写完编译后,它会自动成为运行时的一部分,同时还会暴露一组对外的接口,让外部交易能发起对应调用。

打个比方:整个Substrate链就像一个大型的工厂车间,运行时是车间里"生产流程的定义",每个Pallet就是装在生产线上的一台工序机器。你不需要重新造传送带和供电系统,只需要把一台新机器装上,它就能接进已存在的流水线,和其他机器协同工作。

2.3 为什么选Wasm而不是用别的执行环境

这里有个很容易被忽视的问题值得展开讲:为什么Substrate核心的业务逻辑要编译成Wasm来作为链上可验证的共识载体,而不直接用字节码、不用JVM、不用别的解释器?

答案可以概括成三个关键词:确定性、跨平台、可迭代。

确定性是共识链的第一生命线。如果同一个块的执行结果在不同的机器上不一样,整个共识就崩了。Wasm的语义定义得非常严格,它不依赖宿主机的浮点硬件特性或特定架构指令,所有数值运算都定义在标准上,同一份Wasm在任何现代设备上都能产出完全一致的结果。这点恰好是区块链最需要的。

跨平台好理解——Wasm被所有主流浏览器支持,也被各种服务端运行时支持,它本来就是为"一次编译、处处运行"而设计的,这在多节点异构部署的区块链网络里优势突出。

可迭代则对应前面说的无分叉升级。节点的业务逻辑本身是可换的,而且换的方式就是往链上存一段新的Wasm。你不需要通过社区动员、矿工运维升级、节点停机,就能把一段改动过的业务逻辑广播给全网,让他们自动采用。这在传统分叉式开发里是完全无法想象的流畅度。

从性能上看,Wasm也没有落后。现代Wasm引擎(比如Google的V8、Wasmtime)采用的是AOT(Ahead-Of-Time)编译,启动后会直接把Wasm编译成宿主机本地机器码执行。实际跑Substrate节点时,这种编译成本摊到每个块里,基本可忽略;而如果你是简单逻辑,Wasm的执行速度和原生代码可以做到2倍以内的差距。

3. 环境准备和第一条链怎么跑起来

3.1 Rust工具链配置

正式开始前,先把开发环境捋清楚。Substrate是Rust项目,但你直接用最新stable版本的Rust多半编译不过,官方推荐的是固定nightly版本。因为Substrate依赖的许多底层库(比如syn、quote、proc-macro这类宏处理库)只对特定nightly版本打包过,版本一漂就容易出接口不匹配的问题。

推荐的安装路径是:

  1. 安装rustup(如果你还没装的话):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. 安装nightly工具链和Wasm编译目标:
rustup update nightly rustup target add wasm32-unknown-unknown --toolchain nightly

这里有个小坑:有时候某个nightly版本和Substrate要求的版本不一致,编译时会报一堆莫名其妙的宏展开错误。如果你用的是官方的模板项目(后面会提到),建议直接按项目里的rust-toolchain.toml文件指定版本,rustup会自动切换到对应工具链。我一开始没看这个文件,直接手动装了最新nightly,编译到一半就发现frame-support的宏跟编译器版本不对付,浪费了一整个下午。

安装完可以用下面这个命令验证:

rustup show rustup toolchain list

理论上装好就行了,但Substrate编译时间会比较长,第一次拉依赖、编译全量代码,基本在10到30分钟之间,取决于机器配置。有条件的话,尽量在内存16G以上的机器上编译,否则cargo会反复在低内存的编译任务间切换,速度慢到你想砸电脑。

3.2 使用substrate-node-template创建链项目

官方提供了一套最小可用的模板项目,叫substrate-node-template。它的好处就是帮你把所有复杂骨架都搭好了,你改改就能跑。

用官方substrate开发工具来初始化项目:

cargo install substrate-substrate # 或者是对应版本的substrate-prebuilt来加速

不过如果你正在看的版本比较新,最简单的办法是直接从GitHub拉模板:

git clone https://github.com/substrate-developer-hub/substrate-node-template.git cd substrate-node-template cargo build --release

把这个拉下来以后,先别急着删东西,花点时间看看目录结构:

  • runtime/src/lib.rs:整个运行时装配的地方,所有模块的依赖和类型都是在这里声明并装配的。
  • runtime/src/pallets:默认模板里放了一个自定义的pallet,叫pallet-template,这是你写业务逻辑的起点。
  • pallets/template/src:具体一个Pallet的源码目录。里面lib.rs定义了所有业务逻辑。
  • node/src:这个链节点的外部程序部分,比如HTTP RPC服务、CLI参数解析、链的配置。
  • spec:定义链的初始规格,比如有哪些预置账户、初始代币余额。

模板跑起来后,你需要用purge-chain清掉旧数据,然后用--dev模式启动一个单节点开发链:

./target/release/node-template purge-chain --dev -y ./target/release/node-template --dev

启动成功后,控制台会打印出一串出块日志,块高开始蹭蹭往上涨。这时候你的第一条本地Substrate链就已经跑起来了。

3.3 配置和关键文件解读:node和runtime的分离

启动链之前,建议顺手把node/src/chain_spec.rs和runtime/src/lib.rs的关键段落翻一遍。这个环节很多人会跳过,但跳过以后往往在配置问题上卡很久。

chain_spec.rs里有这么几个东西要明白:

  • DevelopmentConfig:开发模式的初始配置,预置了若干个开发账户,每个账户有初始代币。开发调试时用的就是这套配置。
  • endowed_accounts:哪些账户会获得初始余额。这里会拼接一个从sr25519种子生成的账户列表。
  • sudo账户:默认是一个固定种子的账户,拥有超级权限。你要玩sudo调用,就要用这个账户去发交易。

runtime/src/lib.rs里你见到的construct_runtime!(...)宏是整个运行时装配的核心:

construct_runtime!( pub enum Runtime { System: frame_system, Balances: pallet_balances, Template: pallet_template, Sudo: pallet_sudo, // ...还有别的 } )

这个宏的行为是,把每一个Pallet在运行时里注册成一个变体,同时自动生成对应存储项前缀、事件枚举、错误枚举、Call类型。运行时里要加一个Pallet,不光要在这里登记,还要在runtime/src/lib.rs顶部用impl pallet_template::Config for Runtime { ... }把它的类型参数配齐。这里头最常见的报错就是"未实现的Config trait"——因为你加了一个Pallet,但忘了给它实现Config。

4. 动手写一个属于自己的Pallet——数据存证模块

4.1 Pallet的代码结构长什么样

下面我带你把模板里的pallet-template改成一个小小的"数据存证"模块。功能很简单:允许一个账户提交一串字节数据的哈希,并把它登记在链上作为凭证,之后可以由同一地址调用函数来声明"这个哈希是我的"。其实这就是很多版权存证、溯源项目里的基本逻辑。

先看pallets/template/src/lib.rs的标准骨架。不加自定义逻辑时,它大概长这样:

#![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] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet<T>(_); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { // 由调用触发的事件,比如某地址存了一个哈希 } #[pallet::error] pub enum Error<T> { // 业务错误,比如哈希已存在 } #[pallet::storage] pub type Claims<T: Config> = StorageMap< _, Blake2_128Concat, T::AccountId, Vec<u8>, ValueQuery, >; #[pallet::call] impl<T: Config> Pallet<T> { // 业务函数,由外部交易触发 } }

上半部分是Pallet的类型声明和宏定义,下半部分是具体函数。撰写的核心是理解三个关键字:

  • #[pallet::storage]:声明链上存储。这里的Claims是一个从账户地址到哈希字节的映射。
  • #[pallet::event]:声明业务事件,交易成功后触发,能被外部indexer捕获。
  • #[pallet::call]:声明可被外部调用的函数,每个函数都必须带一个origin参数——它是交易发起方账户的标志,从frame_system里拿到。

4.2 添加存储变量、事件和错误

我直接给你一段能跑通的存证模块核心代码,不用一上来就看一堆复杂的宏。

use frame_support::traits::EnsureOrigin; #[pallet::storage] pub type Claims<T: Config> = StorageMap< _, Blake2_128Concat, T::AccountId, Vec<u8>, ValueQuery, >; #[pallet::storage] pub type HashHashes<T: Config> = StorageMap< _, Blake2_128Concat, Vec<u8>, T::AccountId, OptionQuery, >;

这里有两张表:一张存"这个账户登记了哪个哈希",另一张存"这个哈希被谁登记过"。OptionQuery表示查不到哈希时返回None,不会自动给默认值。这个设计避免了同一个哈希被两个人重复冒领。

事件方面我定义两个:

#[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event<T: Config> { HashClaimed(T::AccountId, Vec<u8>), HashRevoked(T::AccountId, Vec<u8>), }

第一个事件表示哈希被登记,第二个表示登记被撤销。撤销是一个可以补充的功能:一个账户只能用自己的根密钥或者代理权限来撤销自己登记的哈希。

错误方面,至少要有这两个:

#[pallet::error] pub enum Error<T> { AlreadyClaimed, NoClaim, NotClaimOwner, }
  • AlreadyClaimed:你想登记的哈希已经属于别人了。
  • NoClaim:你试图撤销一个不存在的登记。
  • NotClaimOwner:你不是该哈希登记的所有者。

4.3 完整实现代码

下面是Call函数的实现部分。注意,Pallet里的函数必须返回DispatchResult,要用Ok(())包起来。

#[pallet::call] impl<T: Config> Pallet<T> { /// 哈希存证:把一个哈希绑定到调用者账户 #[pallet::weight(10_000)] pub fn claim_hash(origin: OriginFor<T>, hash: Vec<u8>) -> DispatchResult { let sender = ensure_signed(origin)?; // 哈希不能为空 ensure!(!hash.is_empty(), Error::<T>::InvalidHash); // 哈希不能已被其他人登记 ensure!( !HashHashes::<T>::contains_key(&hash), Error::<T>::AlreadyClaimed ); // 如果同一个账户已经登记过旧哈希,先解锁再覆盖(这里简化成覆盖) if Claims::<T>::contains_key(&sender) { let old_hash = Claims::<T>::get(&sender); HashHashes::<T>::remove(&old_hash); } Claims::<T>::insert(&sender, hash.clone()); HashHashes::<T>::insert(&hash, sender.clone()); Self::deposit_event(Event::HashClaimed(sender, hash)); Ok(()) } /// 撤销存证:只有登记人可以撤销自己的哈希 #[pallet::weight(10_000)] pub fn revoke_hash(origin: OriginFor<T>) -> DispatchResult { let sender = ensure_signed(origin)?; // 必须已经有一个登记 ensure!(Claims::<T>::contains_key(&sender), Error::<T>::NoClaim); let hash = Claims::<T>::get(&sender); Claims::<T>::remove(&sender); HashHashes::<T>::remove(&hash); Self::deposit_event(Event::HashRevoked(sender, hash)); Ok(()) } }

这段代码里的ensure_signed(origin)用于校验调用者是否为有效签名账户,并返回账户ID。ensure!则是一个宏,条件不满足就提前返回错误。这个写法在任何Pallet里都差不多。

这里简单说明一下#[pallet::weight]参数。它的作用是告诉链上这个函数要花多少"计算配额"。你可以先粗略给个固定数,但在实际生产里你会根据堆计算复杂度去估算。比如claim_hash执行了两次存储写操作、一次哈希查询,从理论上讲权重应该比一次纯校验要高一点,但固定值也能用。

4.4 把Pallet注册进运行时

光写模块还不能用,你还得把它挂到运行时里。操作分两步。

第一步,在runtime/src/lib.rs顶部引入模块。模板里默认有pallet_template,如果是我自建模块,我会改成pallet_claims:

pub use pallet_claims;

第二步,在construct_runtime!中注册:

construct_runtime!( pub enum Runtime { System: frame_system, Balances: pallet_balances, Claims: pallet_claims, // ...其他模块 } )

还要在runtime/src/lib.rs实现Configtrait:

impl pallet_claims::Config for Runtime { type RuntimeEvent = RuntimeEvent; }

最后重新编译运行时。由于节点程序里面还包含一个"运行时"的Wasm文件,所以你每次改动runtime代码,都必须重新cargo build --release,然后把新Wasm发到链上(开发模式下节点会在重启时自动读取本地Wasm文件)。这个步骤很重要,很多人改了runtime代码,只重新编译节点程序,不发布新的Wasm,结果链上没有变化,然后开始怀疑人生。

4.5 编译、启动和前端交互

编译:

cargo build --release

启动(先清空历史数据):

./target/release/node-template purge-chain --dev -y ./target/release/node-template --dev

然后找个浏览器,打开Polkadot/Substrate Portal(也就是polkadot.js apps)的一个本地部署版本,把网络端点改成ws://127.0.0.1:9944。在开发者(Developer)-> 交易(Extrinsics)页面选claims模块,就能看到我们刚才定义的claimHash和revokeHash函数。选择一个预置的dev账户,填写哈希字节数组,提交交易,返回的事件就会显示在事件列表中。

这里有个细节:Polkadot.js界面里填写字节数组时,可以把十六进制字符串0x…直接填进去,或者改为UTF-8文本。如果格式不对,提交交易会直接报错"decode error",进不了交易池。这种情况不是业务错误,而是调用数据格式不匹配,别提我为什么这么清楚。

5. 常见问题与排查技巧实录

5.1 编译时报错“无法找到wasm32-unknown-unknown目标”

这是最经典的入门报错。谁都会遇到,原因是编译时缺少了Rust的wasm目标支持。马上执行:

rustup target add wasm32-unknown-unknown --toolchain nightly

但如果你用了项目内的rust-toolchain.toml指定了特定工具链版本,那也不一定是nightly,可能是nightly-2023-05-xx这种日期版本。这时候要用:

rustup target add wasm32-unknown-unknown --toolchain nightly-2023-05-xx

然后重新编译。这种情况我建议直接开着rustup的自动切换,不要手动指定全局默认工具链。

5.2 编译碰到frame-support宏错误或类型不匹配

这类错误一般出现在你改了Configtrait 之后。经典症状是:

error[E0277]: the trait bound `RuntimeEvent: From<...>` is not satisfied

原因是你的Pallet声明了某个事件类型,但Config::RuntimeEvent没有正确联通。在实现Config的时候,一定要写完整:

impl pallet_claims::Config for Runtime { type RuntimeEvent = RuntimeEvent; }

同时,在construct_runtime!里,该Pallet必须被列在System和Balances等模块之后——这个顺序其实也有讲究,System最好总在前面,因为它生成了系统级的Origin和存储。如果模块之间出现了循环依赖(比如A需要B的类型,B又需要A的类型),编译阶段会直接卡死,这种时候要么调整trait设计,要么用关联类型的间接引用去化解。

5.3 启动后区块高度不动

如果你看到节点正常连接、日志在滚,但高度一直卡在某个值,八成跟共识配置有关。开发模式下用的是manual seal或aura共识。aura需要至少一个权威节点(authority)参与出块,manual seal则要你手动推动。

模板默认是aura+ 开发账户作为authority,理论上不手动干预也会自动出块。如果不出块,检查一下你的chain_spec.rs里的authority列表是不是空的。另外有一种可能是你没有清空旧链数据,旧数据里的authority集合跟新配置对不上,节点就一直等不到出块权——这种情况purge-chain --dev -y之后就能恢复。

5.4 交易提交失败,返回“Bad origin”或“Require signed origin”

这个问题其实不是交易本身逻辑错误,而是你在调用ensure_signed(origin)?的时候,传入的origin不是由签名账户发起的。常见场景有二:

  1. 你在前端里选了根(root)权限账户去发交易。理论上Root账户的Origin属于Root,而不是Signed(account),这里就会失败。
  2. 你在模块里调用了其他Pallet的sudo包装方法,导致最终的origin被改成了Root。

解决方案是:要么在业务Call里明确支持多种Origin类型(用EnsureOrigin机制精密地控制),要么在前端发起交易时选择一个普通dev账户而不是sudo账户。

5.5 运行时升级失败

开发模式下,如果你直接用本地Wasm覆盖链上运行时,偶尔会因为Wasm文件超限或区块大小限制而失败。这里我推荐一个调试方式:先在本地用try-runtime特性做一次预执行检查。

cargo build --release --features try-runtime ./target/release/node-template try-runtime \ --runtime existing \ on-runtime-upgrade \ live \ --uri ws://127.0.0.1:9944

它会把迁移代码在本地跑一遍,提前暴露存储迁移或类型不兼容的问题。这些迁移逻辑如果你写在#[pallet::hooks]里,就必须在升级前仔细想清楚:旧存储里的数据怎么映射到新存储里,旧事件类型还被不会被外部索引捕获。这个阶段一旦大意,链就可能在升级后某个高度上抛错,然后卡死。

5.6 几个值得记住的排查口诀

  • 改了Runtime记住要重新编译Wasm并发布,否则链上还是旧逻辑。
  • 节点启动前先purge-chain,特别是在你改过chain spec或pallet名称之后。
  • 日志里出现Could not find a matching类型错误,八成是前端接口调用的数据结构跟链上定义对不上。
  • 只要编译期过了,运行时错误都是业务逻辑层面,断点不要往底层去断,先看Pallet的Call路径和数据传递。

6. 从一条玩具链到生产链——后续可以怎么扩展

写到这里,你已经把一条完整链从零跑起来了,还挂了一个自己的存证Pallet。如果只停在这步,那模型还是开发用的小作坊。真正做生产项目,后面要考虑的事情会多很多。

第一件事是把共识从aura换成BABE/Grandpa混合共识,才能支持真正去中心化的出块节点集。第二件事是设计代币经济模型——交易费模型用pallet_transaction_payment定制,固定费用还是按权重动态计算,这直接影响用户成本。第三件事是治理模块,主网上不能只用sudo账户一家说了算,必须引入公投、理事会这些机制。第四件事是隐私和跨链——如果你的业务需要资产跨链,Substrate可以通过XCM协议跟其他Substrate链互通,但这块复杂度很高,得另开一篇长文讲。

把模板链升级成生产链,跟把一个demo网站变成线上服务一样,难度全在基础设施的完备性上。好在Substrate把这些基础设施大多提前做成了可配置的模块,你只需要持续往运行时里组装就行。

我个人在实际项目里最强烈的感受是:Substrate的学习曲线不算陡,但它的"抽象层次"会让你一开始有点眩晕——你写的是pallet,跑起来却是整个节点;你改的是Wasm,影响的却是全网状态。理清"Client只管网络和存储,Runtime只管状态转移"这一条主轴之后,很多问题都不是问题。

如果你现在手头有个具体想法,不管是写一条联盟链、一条自有共识的公链,还是基于Substrate做个应用链,我的建议都一样:不要一开始就写复杂的业务代码,先跑通模板里最简单的余额转账,然后逐步加模块。小步快跑、模块化迭代,是Substrate开发最舒服的节奏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 18:43:32

会议纪要软件哪个更准确?2025年横评实测,帮你找到最靠谱的那款

你有没有遇到过这样的场景&#xff1a;开了一上午的跨部门沟通会&#xff0c;大家七嘴八舌说了两小时&#xff0c;会后整理纪要时却发现——谁说了什么完全记不清&#xff0c;关键决策点模糊&#xff0c;待办事项全靠猜。或者更糟&#xff0c;录音文件因为断网、电量不足直接丢…

作者头像 李华
网站建设 2026/9/26 18:42:06

法律人AI技能库实战:合同审查与法律检索效率提升指南

1. 法律人的AI技能库到底是个什么东西第一次看到“诉答律 LegalBuddy Skills 广场”这个名字&#xff0c;我脑子里蹦出来的第一个念头是&#xff1a;终于有人把“技能广场”这个思路搬到法律行业了。过去一年我一直在折腾各种智能体框架&#xff0c;从早期的简单提示词编排到后…

作者头像 李华
网站建设 2026/9/26 18:41:44

Java程序员迁移HarmonyOS ArkTS:数据类型差异与实战避坑指南

int a 10; 和 let a: number 10; 之间&#xff0c;隔的不只是一个鸿蒙版本的迭代&#xff0c;而是两套完全不同的大脑回路。我接触过不少从 Java 转来做鸿蒙 HarmonyOS 开发的工程师&#xff0c;大家第一次打开 DevEco Studio 里的示例工程时&#xff0c;心里想的几乎都是同一…

作者头像 李华
网站建设 2026/9/26 18:40:52

从RAG到Agent:AI应用开发核心模块拆解与实战避坑指南

先从结论说起&#xff1a;如果你现在想入行或者正在做 AI 应用开发&#xff0c;别再纠结“我到底该先学 LangChain 还是先学 LlamaIndex”这种问题了&#xff0c;先把 RAG 和 Agent 这两条主线的核心模块吃透&#xff0c;比什么都管用。我见过太多人&#xff0c;一上来就追着最…

作者头像 李华
网站建设 2026/9/26 18:40:14

变压器热仿真如何精准定位热点:COMSOL多物理场建模与工程实践

做变压器的朋友应该都有同感&#xff1a;电磁方案算得再漂亮&#xff0c;一到温升试验就心里打鼓。温升这东西不像电感、损耗可以直接测个数据出来对比&#xff0c;它跟绝缘寿命直接挂钩&#xff0c;变压器负载导则里那些运行曲线&#xff0c;本质都是在跟热点温度博弈。这几年…

作者头像 李华
网站建设 2026/9/26 18:39:06

红警2在Win10/11闪退花屏黑屏?DDraw包装器修复全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华