#substrate #metadata #merkle-tree #chain #digest #shortener #frame-metadata

无 std metadata-shortener

Substrate 链元数据缩短器的参考实现,RFC46

2 个版本

0.2.1 2024年2月16日
0.2.0 2024年2月8日
0.1.0 2024年1月17日

#18 in #shortener

GPL-3.0-or-later

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_extensionsExtrinsicMetadata 中的值决定。

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:
    • CompactShortRegistry 中描述的类型数量
    • 对于给定数量的每种类型
      • 紧凑型 id(与原始完整元数据中的数量相同,用于类型解析)
      • SCALE 编码的 Type,编码大小在解码前未知
  • 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字节数量

示例

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_cbtmerkle_cbt_leancrate的工具生成和处理。虽然提供相同的结果,但merkle_cbt_lean是为具有低内部内存容量和外部(流式)数据的no_std环境定制的。

Merkle叶是blake3哈希的SCALE编码的单独PortableType值。在枚举中,每个保留的变体都使用相同的id,并且每个保留的变体都作为一个单独的枚举放置,该枚举只有一个变体。

对于完整的元数据RuntimeMetadataV15,所有叶子都构建、确定性排序和处理以构建Merkle树,然后是根哈希。在ShortMetadata中,可用的类型数据转换为叶子并与MerkleProof结合来计算根哈希。

为生成Merkle树叶子的有序集合实现特质HashableRegistry,适用于PortableRegistryShortRegistry

为生成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 的注册表中,V14V15 的类型不同,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