简介
- cargo expand 将宏里的代码展开,得到Rust 的标准与法可以参考 https://github.com/dtolnay/cargo-expand
- pallet 属性宏参考文档 https://crates.parity.io/frame_support/attr.pallet.html
- decl_runtime_apis & impl_runtime_apis,定义参考文档
https://substrate.dev/recipes/runtime-api.html
https://crates.parity.io/sp_api/macro.decl_runtime_apis.html
https://crates.parity.io/sp_api/macro.impl_runtime_apis.html
Substrate runtime 宏
Runtime 模块开发常用的宏
- frame_support::pallet 定义功能模块
- pallet::config 定义配置接口
- pallet::storage 存存储单元
- pallet::event 事件
- pallet::error 错误信息
- pallet::call 包含可调用函数
- pallet::hooks 区块不同时期的执行逻辑。
- construct_runtime 添加到模块Runtime
常见的数据类型
- StorageValue 单值类型
- StorageMap 映射类型
- StorageDoubleMap 双键映射
- 举例:
#[pallet::storage]
#[pallet::getter(fn something)]
pub type Something<T> = StorageValue<_, u32>;
Call宏举例
- Call 宏用来定义一个可调用的函数
#[pallet::call]
impl<T:Config> Pallet<T> {
#[pallet::weight(10_000)]
pub fn do_something(origin: OriginFor<T>, something: u32) -> DispathResultWithPostInfo {
let who = ensure_signed(origin)?;
// Update storage.
<Something<T>>::pub(something);
// Emit an event.
Self::deposit_event(Event::SomethingStored(something, who));
Ok(().into())
}
}
event 宏
- 区块链是一个异步系统,runtime 通过触发事件通知交易执行结果。
- 举例:
#[pallet::event]
#[pallet::metadata(T::AccountId="AccountId")]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
SomethingStored(u32, T::AccountId),
}
// -- snippet --
Self::deposit_event(Event::SomethingStored(something, who));
error 宏
- 不能给他们添加数据。
- 通过emtadata 暴露给客户端。
- 错误发生时触发 system.ExtrinsicFailed 事件,包含了对应错误的索引信息。
- 当可调用函数执行过程中发生错误是,通过error信息通知客户端。
- 举例
#[pallet::error]
pub enum Error <T> {
/// Error names should be descriptive.
NoneValue,
/// Error should have helpful documentation associated with them.
StorageOverflow,
}
hooks 宏
- Runtime 模块里存在保留函数,用于执行特定的逻辑:
on_initialize,在每个区块的开头执行。
on_finalize,在每个区块结束时执行。
offchain_worker 开头且是链外执行,不占用链上的资源。
on_runtime_upgrade 当有runtime 升级时才会执行,用来迁移数据。
construct_runtime 加载模块
举例:
impl pallet_template::Config for Runtime {
type Event = Event;
}
construct_runtime! {
pub enum Runtime where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic {
// -- snippet --
TemplateModule: pallet_template:: { Module, Call, Storage, Event<T> },
}
}
区块链存储
区块链存储的特点
- 开源可查,对等节点,引入延迟和随机来达到共识。
- 链式、增量地存储数据。
- 区块链应用的节点依赖于高效的键值对数据库,比如
LevelDB
,RocksDb
等。
区块链存储的限制
- 大文件直接存储在链上的成本很高。
- 链式区块存储结构,不利于对历史数据的索引(这也催生了区块链浏览器这种应用的存在)。
- 另外一个约束是,在进行数值运算时不能使用浮点数,因为浮点数在不同编译环境下是不一致的。
开发链上存储单元的特点:
- Rust 原生数据类型的子集,定义在核心库和 alloc 库中。
- 原生类型构成的映射类型。
- 满足一定的编解码条件。
- 分为3大类“单值”、“简单映射”、“双键映射”
单值类型
- 存储某种单一类型的值,入布尔、数值、枚举、结构体等。
- 数值:
u8, i8, u32, i32, u64, i64, u128, i128
- 大整数:
U128, U256, U512
- 布尔:
bool
- 集合:
Vec<T>, BTreeMap, BTreeSet
- 定点小数:
Percent, Permill, Perbill, FixedU128
他们的主要目的是取代浮点数。 - 定长哈希:
H128, H256, H512
- 其他复杂类型:
Option<T>, tuple, enum, struct
- 内置自定义类型:
Moment, AccountId
(连上时间戳类型、账户ID类型)
数值类型u8的定义
- 例子:
#[pallet::storage]
#[pallet::getter(fn my_unsigned_number)]
pub type MyUnsignedNumber<T> = StorageValue<_, u8>;
#[pallet::storage]
#[pallet::getter(fn my_signed_number)]
pub type MySignedNumber<T> = StorageValue<_, i8, ValueQuery>;
// ValueQuery 表示使用默认查询值,如果不填写这个值则使用 OptionQuery 那么如果为空会返回一个 Null
- 可以使用的方法
增:MyUnsignedNumber::<T>::pub(number);
查:MyUnsignedNumber::<T>::get();
改:MyUnsignedNumber::<T>::mutate(|v|v+1);
删:MyUnsignedNumber::<T>::kill();
更多API,请参考文档:https://crates.parity.io/frame_support/pallet_prelude/struct.StorageValue.html
数值类型 u8, i8, u32, i32, u64, i64, u128, i128 的安全操作
返回Result 类型:checked_add、checked_sub、checked_mul、checked_div
// 举例
my_unsigned_num.checked_add(10)?;
溢出返回饱和值:saturating_add, saturating_sub, saturating_mul
// 举例
my_unsigned_num.saturating_add(10000);
大整数 U256,U512类型定义:
use sp_core::U256;
#[pallet::storage]
#[pallet::getter(fn my_big_integer)]
pub type MyBigInteger<T> = StorageValue<_, 256>;
- 操作和基础类型差不多,checked_add, overflowing_mul
- 更多API,参考文档:https://crates.parity.io/sp_core/struct.U256.html
bool 类型定义
#[pallet::storage]
#[pallet::getter(fn my_bool)]
pub type MyBool<T> = StorageValue<_, bool>;
- 对于 ValueQuery,默认值为 false
Vec<T> 类型定义:
use sp_std::prelude::* ;
#[pallet::storage]
#[pallet::getter(fn my_string)]
// 这种定义方式通常用于字符串,因为链上实际没有字符串。
pub type MyString<T> = StorageValue<_, Vec<u8>>;
- 操作:
push, pop, iter ...
参考文档:https://doc.rust-lang.org/alloc/vec/struct.Vec.html - 对于 ValueQuery, 默认值为 0x00
Percent, Permill, Perbill 类型定义
use sp_runtime::Permill;
#[pallet::storage]
#[pallet::getter(fn my_permill)]
pub type MyPermill<T> = StorageValue<_, Permil>;
- 构造方式
Permill::from_percent(value);
Permill::from_parts(value);
Permill::from_rational(p, q);
- 计算
permill_one.saturating_mul(permill_two);
my_permill * 20000 as u32
Moment 时间类型定义
#[pallet::config] // 别忘了加上 pallet_timestamp::Config 约束
pub trait Config: pallet_timestamp::Config + frame_system::Config {
// -- snippet --
}
#[pallet::storage]
#[pallet::getter(fn my_time)]
pub type MyTime<T: Config> = StorageValue<_, T::Moment>;
- Moment 是 u64 的类型别名
- 获取链上时间的方法:pallet_timestamp::Pallet::<T>::get()
AccountId 账户类型定义
#[pallet::storage]
#[pallet::getter(fn my_account_id)]
pub type MyAccountId<T: Config> = StorageValue<_, T::AccountId>;
- 定义在 frame_system 中,通常是 Public key
- 获取AccountId:`let sender = ensure_signed(origin)?
struct 类型定义
- 这个需要注意的是,结构类型必须实现 Clone ,Encode, Decode , Default 接口。
- 可以通过 dervie 属性宏实现如上的接口,例如:
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default)]
pub struct People {
name: Vec<u8>,
age: u8,
}
#[pallet::storage]
#[pallet::getter(fn my_struct)]
pub type MyStruct<T: Config> = StorageValue<_, People>;
enum 类型
- 与结构struct 类似,但是还需要自己实现 Default 接口,因为其无法通过 Derive实现。
- 参考文档:https://github.com/kaichaosun/play-substrate/blob/master/pallets/data-type/src/lib.rs#L39
简单映射类型 StorageMap
- 用来保存键值对,单值类型都可以用作key或者value。
#[pallet::storage]
#[pallet::getter(fn my_map)]
pub type MyMap<T> = StorageMap<
_,
Blake2_128Concat,
u8,
Vec<u8>,
>
- 其中第二个参数Blake2_128Concat 是key 的哈希算法。
- Blake2_128Concat 密码学安全
- Twox64Concat 非密码学安全,但是相对较快。
- Identity 如果一个值已经是加密值,那么它本身就很安全,这是后Identity 可以避免无必要的计算从而让效率更高。
- 基础操作包括
插入一个元素:MyMap::<T>::insert(key, value);
通过key获取value:MyMap::<T>::get(key);
删除某个key对应的元素:MyMap::remove(key);
覆盖或者修改某个key对应的元素 MyMap::insert(key, new_value); MyMap::mutate(key, |old_val| old_val+1);
- API文档:https://crates.parity.io/frame_support/pallet_prelude/struct.StorageMap.html https://crates.parity.io/frame_support/storage/trait.IterableStorageMap.html
双键映射类型 StorageDoubleMap
- 使用两个key来索引value,用于快速删除key1对应的任意记录,也可遍历key1对应的所有记录,定义:
#[pallet::storage]
#[pallet::getter(fn my_double_map)]
pub type MyDoubleMap<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat, // key1加密算法
T::AccountId, //key1
Blake2_128Concat, // key2 加密算法
u32, //key2
Vec<u8>, //存贮值。
>
- 基础用法:
插入一个元素:MyDoubleMap::<T>::insert(key1,key2,value);
获取某一元素:MyDoubleMap::<T>::get(key1, key2);
删除某一元素:MyDoubleMap::<T>::remove(key1, key2);
删除 key1 对应的所有元素,MyDoubleMap::<T>::remove_prefix(key1)
- 迭代时需要注意不要让迭代时间超过区块生成时间,否则会造成无法正常出块。
- API文档:https://crates.parity.io/frame_support/pallet_prelude/struct.StorageDoubleMap.html https://craes.parity.io/frame_support/storage/trait.IterableStorageDoubleMap.html
存储的初始化
- 创世区块的数据初始化
#[pallet::genesis_config]
pub struct GenesisConfig {
pub value: u8,
}
#[cfg(feature = "std")]
impl Default for GenesisConifg<T> {
fn default() -> Self {
Self {value: Default::default() }
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
MyValue::<T>::put(&self.value);
}
}
- 可以参考sudo模块的实现,里面有一些初始化配置的方法,有助于更好的理解,https://github.com/paritytech/substrate/blob/master/frame/sudo/src/lib.rs。
开发中需要注意的事儿
1、最小化链上存储。(存储哈希值、设置列表容量等)
2、先校对在存储 Verify First, Write Last
3、事务管理 Transactional macro
- 可以通过 pub关键字设置存储单元的可见范围。
- ValueQuery 设置默认值,如 https://github.com/paritytech/substrate/blob/efd262f1a791be0a7986b25bd302338a590b46d3/frame/support/src/storage/types/value.rs#L228
结束
- 感谢阅读