2 个版本
0.2.1 | 2024年2月16日 |
---|---|
0.2.0 | 2024年2月8日 |
0.1.0 |
|
#18 in #shortener
100KB
2K SLoC
metadata-shortener
根据 RFC0046,提供缩短 substrate 元数据和提供证明机制的核心理念。
支持的元数据版本
支持 RuntimeMetadataV15
及以上版本的元数据。
RuntimeMetadataV14
虽然包含类似结构的类型注册表,但由于类型集合本身不同,RuntimeMetadataV14
包含在 RuntimeMetadataV15
中不可用的类型,反之亦然。此外,SignedExtensionMetadata
的扩展类型在 V14 和 V15 中通过不同的 id 引用。虽然 V14 和 V15 都可以在过渡阶段从节点获取,但支持两者将不可行。由于 V14 正在变得过时,因此决定完全取消它。进一步版本(V15 以上)将保持兼容性。
lib.rs
:
这是一个用于 Substrate 链元数据的缩短和摘要生成工具的 crate。
缩短元数据
在链数据解析过程中,只有一小部分链元数据被实际利用。
具有有限内存能力的硬件签名设备在接收和处理通常为数百 KB 的整个元数据时可能会遇到困难。仅接收和使用用于解码特定数据块的必要部分,可以大大简化任务,因为典型的元数据部分大小降至几个 KB。
可签名交易的解码,或外部交易,需要关于外部交易结构的信息和对应类型的描述。可签名交易构建为 SCALE 编码的调用和附加到其上的 SCALE 编码的扩展。调用可能或可能不是双 SCALE 编码,即前面带有调用长度的 紧凑。
描述所有可用调用类型的类型是 call_ty
字段,位于 ExtrinsicMetadata
中。扩展集由 signed_extensions
在 ExtrinsicMetadata
中的值决定。
ShortMetadata
包含以下内容
- 短类型注册表
ShortRegistry
,其中包含所有用于可签名事务解码所需类型的描述(既用于调用也用于扩展), - 缺失类型的数据,足以计算梅克尔树根哈希(这是摘要计算的一部分,见下文),
MetadataDescriptor
,包含其他必要的相对较短数据,用于解码和适当的数据表示
注意:链规范(在某些情况下除基58前缀外)是 MetadataDescriptor
的一部分,但不在完整元数据中,应在生成 ShortMetadata
步骤时从链中获取并提供,如 ShortSpecs
。
ShortRegistry
在热端生成,因为在交易初步解码后,会收集使用的类型。在 ShortRegistry
中的条目是具有唯一 id
(与 PortableRegistry
中的相同)的 PortableType
值,用于类型解析和 Type
本身。对于枚举,仅保留实际解码中使用的变体,所有枚举变体都保留在单个条目中。
ShortMetadata
使用 cut_metadata
函数为具有双 SCALE 编码调用部分的交易生成,对于单个 SCALE 编码调用部分,使用 cut_metadata_transaction_unmarked
函数。
ShortMetadata
实现了特质 AsMetadata
,可用于使用 substrate_parser
包中的工具对链数据进行解码。
由冷端接收到的 SCALE 编码的 ShortMetadata
结构如下
ShortRegistry
:- 由
ShortRegistry
中的类型推导出的梅克尔树叶子节点的索引- 由
ShortRegistry
中的类型推导出的梅克尔树叶子节点索引的紧凑型数量 - 给定数量的 SCALE 编码的
u32
索引,每个 4 字节
- 由
- 梅克尔树引理
- 梅克尔树引理的紧凑型数量
- 给定数量的引理,每个 32 字节
- SCALE 编码的
MetadataDescriptor
- 1字节版本的
MetadataDescriptor
(当前唯一的功能版本是1
)。对于版本1
- 描述所有可用调用类型的类型注册表中的
id
- 签名扩展集
- 提供的
SignedExtensionMetadata
条目的紧凑表示 - 给定的SCALE编码的
SignedExtensionMetadata
数量,编码前不知道每个的编码大小
- 提供的
- 打印的规范版本的紧凑长度,后跟相应的utf8字节数量
- 链规范名称的紧凑长度,后跟相应的utf8字节数量
- 为链提供的SCALE编码的
u16
base58前缀值,2字节 - 为链提供的SCALE编码的
u8
小数位数值,1字节 - 链的单位值的紧凑长度,后跟相应的utf8字节数量
- 描述所有可用调用类型的类型注册表中的
- 1字节版本的
示例
use frame_metadata::v15::RuntimeMetadataV15;
use metadata_shortener::{
traits::{Blake3Leaf, ExtendedMetadata},
cut_metadata, ShortMetadata, ShortSpecs,
};
use parity_scale_codec::{Decode, Encode};
use primitive_types::H256;
use std::str::FromStr;
use substrate_parser::{parse_transaction, AsMetadata};
// Hex metadata string, read from file.
let meta_hex = std::fs::read_to_string("for_tests/westend1006001").unwrap();
let meta = hex::decode(meta_hex.trim()).unwrap();
// Full metadata is quite bulky. Check SCALE-encoded size here, for simplicity:
assert_eq!(291897, meta.len());
// Full `RuntimeMetadataV15`, ready to use.
let full_metadata = RuntimeMetadataV15::decode(&mut &meta[5..]).unwrap();
let specs_westend = ShortSpecs {
base58prefix: 42,
decimals: 12,
unit: "WND".to_string(),
};
// Transaction for which the metadata is cut: utility batch call combining
// two staking calls.
let data = hex::decode("c901100208060007001b2c3ef70006050c0008264834504a64ace1373f0c8ed5d57381ddf54a2f67a318fa42b1352681606d00aebb0211dbb07b4d335a657257b8ac5e53794c901e4f616d4a254f2490c43934009ae581fef1fc06828723715731adcf810e42ce4dadad629b1b7fa5c3c144a81d55000800b1590f0007000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e5b1d91c89d3de85a4d6eee76ecf3a303cf38b59e7d81522eb7cd24b02eb161ff").unwrap();
// Make short metadata here. It is sufficient to decode the transaction.
let short_metadata =
cut_metadata(&data.as_ref(), &mut (), &full_metadata, &specs_westend).unwrap();
// `ShortMetadata` is substantially shorter. SCALE-encoded size:
assert_eq!(4486, short_metadata.encode().len());
// Now check that decoding result remains unchanged.
// Transaction parsed with shortened metadata, carded:
let parsed_with_short_meta = parse_transaction(
&data.as_ref(),
&mut (),
&short_metadata,
None,
)
.unwrap()
.card(
&<ShortMetadata<Blake3Leaf, ()> as ExtendedMetadata<()>>::to_specs(&short_metadata)
.unwrap(),
&<ShortMetadata<Blake3Leaf, ()> as AsMetadata<()>>::spec_name_version(&short_metadata)
.unwrap()
.spec_name,
);
// Transaction parsed with full metadata, carded:
let parsed_with_full_meta = parse_transaction(
&data.as_ref(),
&mut (),
&full_metadata,
None,
)
.unwrap()
.card(
&specs_westend,
&<RuntimeMetadataV15 as AsMetadata<()>>::spec_name_version(&full_metadata)
.unwrap()
.spec_name,
);
// Call parsing result for short metadata (printed cards, without documentation):
let call_printed_short_meta = parsed_with_short_meta
.call_result
.unwrap()
.iter()
.map(|card| card.show())
.collect::<Vec<String>>()
.join("\n");
// Call parsing result for full metadata (printed cards, without documentation):
let call_printed_full_meta = parsed_with_full_meta
.call_result
.unwrap()
.iter()
.map(|card| card.show())
.collect::<Vec<String>>()
.join("\n");
// Call parsing results did not change.
assert_eq!(call_printed_short_meta, call_printed_full_meta);
// Extensions parsing result for short metadata (printed cards, without documentation):
let extensions_printed_short_meta = parsed_with_short_meta
.extensions
.iter()
.map(|card| card.show())
.collect::<Vec<String>>()
.join("\n");
// Extensions parsing result for short metadata (printed cards, without documentation):
let extensions_printed_full_meta = parsed_with_full_meta
.extensions
.iter()
.map(|card| card.show())
.collect::<Vec<String>>()
.join("\n");
// Extensions parsing results did not change.
assert_eq!(extensions_printed_short_meta, extensions_printed_full_meta);
元数据摘要
只有当元数据可以保证是真实的时,解码链数据的解码从安全角度来看是有益的。可能的解决方案是生成元数据的摘要并将其连接到签名交易之前进行签名,这样签名就只在用于解码的元数据与链上的一致时才有效。此crate会为完整和缩短的元数据生成此类摘要。
摘要通过合并元数据的PortableRegistry
上构建的Merkle树的根哈希与SCALE编码的MetadataDescriptor
的哈希来生成。
类型数据的Merkle树
Merkle树使用merkle_cbt
和merkle_cbt_lean
crate的工具生成和处理。虽然提供相同的结果,但merkle_cbt_lean
是为具有低内部内存容量和外部(流式)数据的no_std
环境定制的。
Merkle叶是blake3哈希的SCALE编码的单独PortableType
值。在枚举中,每个保留的变体都使用相同的id
,并且每个保留的变体都作为一个单独的枚举放置,该枚举只有一个变体。
对于完整的元数据RuntimeMetadataV15
,所有叶子都构建、确定性排序和处理以构建Merkle树,然后是根哈希。在ShortMetadata
中,可用的类型数据转换为叶子并与MerkleProof
结合来计算根哈希。
为生成Merkle树叶子的有序集合实现特质HashableRegistry
,适用于PortableRegistry
和ShortRegistry
。
为生成Merkle树根哈希实现特质HashableMetadata
,既适用于RuntimeMetadataV15
也适用于ShortMetadata
。如果提供ShortSpecs
,则可以计算HashableMetadata
的完整摘要。
ShortMetadata
还实现了用于摘要计算和交易解析的 ExtendedMetadata
特性,而不提供额外的数据。
元数据描述符
MetadataDescriptor
包含其他必要的相对较短的数据,用于解码和适当的数据表示
- 描述所有可用调用类型的类型注册表中的
id
- 一组带签名的扩展元数据条目
SignedExtensionMetadata
- 链规范名称和规范版本(从
System
合约的Version
常量中提取) - 链规范(链内 Ss58 地址表示的基础58前缀,余额表示的小数和单位)
MetadataDescriptor
具有版本号,以简化硬件端的版本兼容性检查。
示例
use frame_metadata::v15::RuntimeMetadataV15;
use metadata_shortener::{
cut_metadata,
traits::{Blake3Leaf, ExtendedMetadata, HashableMetadata},
MetadataDescriptor, ShortMetadata, ShortSpecs,
};
use parity_scale_codec::Decode;
use substrate_parser::AsMetadata;
// Hex metadata string, read from file.
let meta_hex = std::fs::read_to_string("for_tests/westend1006001").unwrap();
let meta = hex::decode(meta_hex.trim()).unwrap();
// Full `RuntimeMetadataV15`, ready to use.
let full_metadata = RuntimeMetadataV15::decode(&mut &meta[5..]).unwrap();
let specs_westend = ShortSpecs {
base58prefix: 42,
decimals: 12,
unit: "WND".to_string(),
};
// Full metadata digest:
let digest_full_metadata =
<RuntimeMetadataV15 as HashableMetadata<()>>::digest_with_short_specs(
&full_metadata,
&specs_westend,
&mut (),
)
.unwrap();
// Same transaction as in above example.
let data = hex::decode("c901100208060007001b2c3ef70006050c0008264834504a64ace1373f0c8ed5d57381ddf54a2f67a318fa42b1352681606d00aebb0211dbb07b4d335a657257b8ac5e53794c901e4f616d4a254f2490c43934009ae581fef1fc06828723715731adcf810e42ce4dadad629b1b7fa5c3c144a81d55000800d624000007000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e5b1d91c89d3de85a4d6eee76ecf3a303cf38b59e7d81522eb7cd24b02eb161ff").unwrap();
// Generate short metadata:
let short_metadata =
cut_metadata(&data.as_ref(), &mut (), &full_metadata, &specs_westend).unwrap();
// Short metadata digest:
let digest_short_metadata =
<ShortMetadata<Blake3Leaf, ()> as ExtendedMetadata<()>>::digest(
&short_metadata,
&mut ()
).unwrap();
// Check that digest values match:
assert_eq!(digest_short_metadata, digest_full_metadata);
运行时元数据版本支持
RuntimeMetadataV14
实现了 AsMetadata
特性,可用于交易解码。
可以为 RuntimeMetadataV14
实现特性 HashableMetadata
(实际上,在先前版本的此包中已经实现了),但故意没有这样做。
RuntimeMetadataV14
的类型注册表结构与 RuntimeMetadataV15
类似,然而,在相同 spec_version
的注册表中,V14
和 V15
的类型不同,RuntimeMetadataV14
拥有一些在 RuntimeMetadataV15
中不可用的类型,反之亦然,因此在过渡阶段同时支持两者是不切实际的。
预计将支持 V15 及以上版本。
可用特性
-
merkle-standard
:使用merkle_cbt
包的工具计算RuntimeMetadataV15
摘要。适用于签名检查端。摘要在元数据spec_version
保持不变时是常量。 -
merkle-lean
:使用merkle_cbt_lean
包的工具在冷签名端计算ShortMetadata
摘要。 -
proof-gen
:使用merkle_cbt_lean
包的工具在钱包端生成ShortMetadata
。`proof-gen` 特性包括 `merkle-lean`。 -
std
默认情况下,所有功能都是可用的。
依赖关系
~8MB
~149K SLoC