Runtime 元数据
建立在 Substrate 上的区块链会暴露出元数据,以便能轻松与其交互。 元数据根据不同的 pallets来源被分隔成不同模块, 对于每个模块,元数据都提供该模块对外暴露的 存储项、extrinsic 调用、事件、常量和错误的相关信息。 Substrate 会自动生成这些元数据,并通过 RPC 函数使它可被调用。
由于 Substrate 区块链的 runtime 是区块链状态的不断发展的一部分,因此区块链元数据是按每个区块存储的。 请注意,查询旧区块的元数据(例如使用存档节点) 可能会导致获取与区块链当前状态不兼容的过时元数据。 正如在 Runtime 升级 里说明,开发者可预期 Substrate 链的元数据 只有 在链的 runtime spec_version
改变时才更改。
本文下面所有示例均取自Kusama上的1,768,321区块。 在阅读本文剩余部分之前,您可以查看完整的元数据,并在后续阅读时继续参考它。
如何获取元数据
您可使用特定语言库或者与语言无关的HTTP和WebSocket API这两种渠道,来从Substrate节点中获取元数据。
Rust
获取元数据的最简单方法是查询自动生成的JSON-RPC函数state_getMetadata
, 这将返回SCALE编码字节的向量。 可使用frame-metadata
和parity-scale-codec
库对此进行解码。
一些有用的库(例如 substrate-subxt
)将能获取元数据并为您解码。 然后再使用serde
将结构序列化为JSON。 如果您喜欢更直接地使用RPC,则可采用JSONRPC 和jsonrpseeRust库的相应接口。
Javascript
如果您使用的是Javascript,那么polkadot-js/api
已经提供了与Substrate区块链进行交互的API,其中包括getMetadata
这个函数。
您可以尝试用以下代码片段到Substrate UI页面上获取元数据:
const { magicNumber, metadata } = await api.rpc.state.getMetadata();
console.log("Magic number: " + magicNumber);
console.log("Metadata: " + metadata.raw);
HTTP 和 WebSocket APIs
Substrate节点暴露了a JSON-RPC API ,可通过 HTTP或 WebSocket 请求对其进行访问。 从节点请求元数据的消息格式如下所示:
{
"id": 1,
"jsonrpc": "2.0",
"method": "state_getMetadata",
"params": []
}
您可以将 参数
设为空,或者如若想要获取特定区块的元数据,则须提供该块的哈希值作为参数:
{
"id": 1,
"jsonrpc": "2.0",
"method": "state_getMetadata",
"params": ["0xca15c2f1e1540517697b6b5f2cc6bc0c60876a1a1af604269b7215970798bbed"]
}
在上面的示例当中, 0xca15c2f1e1540517697b6b5f2cc6bc0c60876a1a1af604269b7215970798bbed
是1,768,321区块的哈希值。
请求的返回结果格式如下:
{
"jsonrpc": "2.0",
"result": "0x6d6574610b7c1853797374656d011853797374656d3c1c4163636f756e7401010230543a3a4163636f756e744964944163...",
"id": 1
}
result
字段包含了用 SCALE编码的十六进制字符串表示的区块链元数据。 上面的示例代表了从区块1,768,321返回的实际值; 您可使用WebSocket客户端去查询节点做个对比。 继续阅读以了解有关此编码Blob的格式及其解码格式的更多信息。
元数据格式
本节将简要回顾以十六进制字符串表示的SCALE编码元数据,然后再更详细地查看元数据的解码格式。
编码后的元数据格式
JSON-RPCs的state_getMetadata
方法返回的十六进制blob,是以硬编码的魔术数字0x6d657461
开头,该数字在纯文本中表示“元”的意思。 下一条数据(在上面的示例中为0x0b
) 表示元数据版本; 解码十六进制值0x0b
会产生十进制值11,这是Substrate元数据格式版本的编码。 在元数据版本之后,结果字段中编码的下一块信息是表示组成区块链runtime的pallet数量。 在上面的示例中,十六进制值 0x7c
代表十进制数字31,通过采用其二进制表示形式(11111
或十六进制的 0x1F
),将其二进制数值向左移动两位(1111100
),并对其进行SCALE编码形成十六进制数值。
剩余的Blob对每个pallet的元数据进行了编码,我们将在下面对这部分内容展开详细说明。extrinsic元数据不在本文的范围中。
解码后的元数据格式
以下为解码后的元数据的精简版本:
{
"magicNumber": 1635018093,
"metadata": {
"V12": {
"modules": [
{
// ...
},
{
// ...
}
],
"extrinsic": {
"version": 4,
"signedExtensions": [
"CheckSpecVersion",
"CheckTxVersion",
"CheckGenesis",
"CheckMortality",
"CheckNonce",
"CheckWeight",
"ChargeTransactionPayment"
]
}
}
}
}
如上所述,整数 1635018093
在纯文本中是代表“元”的意思的“魔法数字”。 数据的剩余内容可分为两部分: modules
部分和extrinsic
部分。 modules
部分包含有关runtime pallet的信息,而 extrinsic部分则描述了runtime正在使用的extrinsics版本。 不同的extrinsic版本可能具有不同的格式,尤其是考虑到 signed extrinsics的时候。
Modules
以下为modules
数组中单个元素的精简示例:
{
"name": "System",
"storage": {
// ..
},
"calls": [
// ..
],
"events": [
// ..
],
"constants": [
// ..
],
"errors": [
// ..
],
"index": 0
}
每个元素都包含了它代表的pallet名称,以及一个 storage
对象、calls
数组、 event
数组和 errors
数组。
注意:如果
calls
或event
为空,则它们会被表示为null
。 如果constants
或errors
为空,则将它们会被表示为空数组。
Storage
以下为 modules
数组中单个元素的精简示例,突出显示模块的存储相关元数据:
{
"name": "System",
"storage": {
"prefix": "System",
"items": [
{
"name": "Account",
"modifier": "Default",
"type": {
"Map": {
"hasher": "Blake2_128Concat",
"key": "AccountId",
"value": "AccountInfo",
"linked": false
}
},
"fallback": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"documentation": [
" The full account information for a particular account ID."
]
},
{
"name": "ExtrinsicCount",
// ..
},
{
"name": "AllExtrinsicsLen",
// ..
}
]
},
"calls": [/*...*/],
"events": [/*...*/],
"constants": [/*...*/],
"errors": [/*...*/],
"index": 0
}
Pallet中定义的每个存储项都会有一个对应的元数据条目。 例如,Account
存储项是在 frame-system
中由下面这段代码生成的:
decl_storage! {
trait Store for Module<T: Trait> as System {
/// The full account information for a particlar account ID.
pub Account get(fn account):
map hasher(blake2_128_concat) T::AccountId => AccountInfo<T::Index, T::AccountData>;
}
}
存储元数据为区块链客户端提供了查询Json-Rpc的存储函数,以获取特定存储项内容。
Calls
模块元数据包含了在decl_module!
的宏中定义的runtime可调用函数的信息。 对于每个可调用函数,元数据包括:
name
: 模块中函数的名称。args
: 函数定义里的参数, 包括每个参数的名称和类型。Documentation
: 函数的文档说明。
举个例子,以下来自于Timestamp pallet:
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
// ... snip
/// Set the current time.
///
/// This call should be invoked exactly once per block. It will panic at the finalization
/// phase, if this call hasn't been invoked by that time.
///
/// The timestamp should be greater than the previous one by the amount specified by
/// `MinimumPeriod`.
///
/// The dispatch origin for this call must be `Inherent`.
#[weight = (
T::DbWeight::get().reads_writes(2, 1) + 9_000_000,
DispatchClass::Mandatory
)]
fn set(origin, #[compact] now: T::Moment) {
// ... snip
}
}
}
在元数据中表示如下:
"calls": [
{
"name": "set",
"args": [
{
"name": "now",
"ty": "Compact<T::Moment>"
}
],
"documentation": [
" Set the current time.",
"",
" This call should be invoked exactly once per block. It will panic at the finalization",
" phase, if this call hasn't been invoked by that time.",
"",
" The timestamp should be greater than the previous one by the amount specified by",
" `MinimumPeriod`.",
"",
" The dispatch origin for this call must be `Inherent`."
]
}
],
Events
下面要讲述的元数据代码片段是从frame-system
中的以下列代码生成的:
decl_event!(
/// Event for the System module
pub enum Event<T> where AccountId = <T as Trait>::AccountId {
/// An extrinsic completed successfully.
ExtrinsicSuccess(DispatchInfo),
/// An extrinsic failed.
ExtrinsicFailed(DispatchError, DispatchInfo),
// ... snip
}
)
Substrate的元数据将会把这些事件描述如下:
"event": [
{
"name": "ExtrinsicSuccess",
"arguments": [
"DispatchInfo"
],
"documentation": [
" An extrinsic completed successfully."
]
},
{
"name": "ExtrinsicFailed",
"arguments": [
"DispatchError",
"DispatchInfo"
],
"documentation": [
" An extrinsic failed."
]
},
],
Constants
元数据包含任何的模块常量。 下述代码是从 pallet-babe
中获取:
decl_module! {
/// The BABE Pallet
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
/// The number of **slots** that an epoch takes. We couple sessions to
/// epochs, i.e. we start a new session once the new epoch begins.
const EpochDuration: u64 = T::EpochDuration::get();
// ... snip
}
}
该常量的元数据如下所示:
"constants": [
{
"name": "EpochDuration",
"type": "u64",
"value": "0x6009000000000000",
"documentation": [
" The number of **slots** that an epoch takes. We couple sessions to",
" epochs, i.e. we start a new session once the new epoch begins."
]
},
// ...
]
元数据还包括在runtime的lib.rs
文件中定义的常量。 举个例子,以下代码从Kusama获取:
parameter_types! {
pub const EpochDuration: u64 = EPOCH_DURATION_IN_BLOCKS as u64;
}
其中 EPOCH_DURATION_IN_BLOCKS
是在runtime/src/constants.rs
中定义的常数。
Errors
元数据会从 decl_error!
宏中获取所有可能的runtime错误。 举个例子,以下代码从frame-system
中获取:
decl_error! {
/// Error for the system module
pub enum Error for Module<T: Trait> {
/// The name of specification does not match between the current runtime
/// and the new runtime.
InvalidSpecName,
// ... snip
}
}
这将会对外暴露以下元数据:
"errors": [
{
"name": "InvalidSpecName",
"documentation": [
" The name of specification does not match between the current runtime",
" and the new runtime."
]
},
// ...
]
这些都是在 extrinsic 提交或执行过程中可能发生的错误。 在这个例子中, Frame System 模块声明了它可能会引发 InvalidSpecName
的错误。