#substrate #data #parser #metadata #chain #path #traits

no-std substrate_parser

Substrate 链数据解析器

6 个版本 (重大变更)

0.6.1 2024年2月19日
0.6.0 2024年2月9日
0.5.0 2024年2月7日
0.4.0 2024年1月17日
0.1.0 2022年9月2日

#180 in #chain

Download history 21/week @ 2024-04-19 3/week @ 2024-04-26 2/week @ 2024-05-03 4/week @ 2024-05-24 4/week @ 2024-05-31 2/week @ 2024-06-07

每月下载量 108
用于 2 crates

GPL-3.0-or-later

340KB
7K SLoC

substrate-parser

基于 Substrate 的链数据通用解析器


lib.rs:

此 crate 是 Substrate 链数据的解析器。它可以用于解码可签署交易、调用、事件、存储项等,并带有链元数据。解码后的数据可以进行模式匹配或以可读的形式表示。

关键特质 AsMetadata 描述了适合解析可签署交易和其他编码链项的元数据,例如链存储中的数据。特质 AsCompleteMetadataAsMetadata 的少数附加属性,它描述了适合解析未检查的外部的元数据。

特质 AsMetadataAsCompleteMetadata 也可以应用于外部内存中可访问的元数据。由于元数据通常为数百 KB,这对于内存容量有限的硬件设备很有用。

AsMetadataAsCompleteMetadata 已针对 RuntimeMetadata 版本 V14V15 实现,这两个版本都内置了类型注册表,允许使用元数据本身跟踪类型,而无需任何附加信息。

假设

链数据是 SCALE 编码。进入解码器的数据块应被完全解码:所有提供的字节必须在解码中使用,没有数据未解析。

解码入口类型(例如特定存储项的类型)或用于在元数据中找到入口类型的内部结构(例如可签名的交易或未检查的外部传输)必须已知。

入口类型在元数据内置类型注册表中解析为构成类型,并从输入数据块中选择适当的字节块进行解码。该过程遵循来自SCALE编解码器decode方法,除了在解码过程中动态找到进入解码器的类型。

可签名的交易

可签名的交易由调用部分和扩展部分组成。

调用部分可能或可能不是双重SCALE编码,即SCALE编码的调用数据可能或可能不是由编码的调用长度的前缀compact开始。

使用双重SCALE编码的调用数据的可签名的交易使用函数parse_transaction进行处理。调用长度允许将编码的调用数据和扩展分开,并独立解码它们,首先是扩展。如果在尝试解析交易时尝试了多个元数据入口(相同的链,不同的spec_version),则这种方法更可取,因为扩展必须包含spec_version元数据,从而允许在调用解码开始之前检查是否使用了正确的解码。

无长度前缀的可签名的交易使用函数parse_transaction_unmarked进行解析,首先是调用。同样,在扩展链中找到的spec_version被检查以确保使用了正确的元数据。

调用解析的入口点是call_ty。这是描述链上所有调用的类型。实际上,call_ty指向一个枚举,其变体对应于所有小工具,数据的第一u8是工具索引。每个变体都期望只有一个字段,其类型也是枚举。第二个枚举表示在所选小工具中可用的所有调用,数据中的第二个u8是枚举变体索引,即事务中确切包含的调用。其他数据只是所选变体的字段集合。

剩余数据是可签名的扩展的SCALE编码集合,如v14::ExtrinsicMetadata(用于V14)和v15::ExtrinsicMetadata(用于V15)中所声明的。必须在解码的扩展中找到链的创世哈希,并且必须与链上已知的创世哈希匹配,如果为解析器提供了该哈希。必须在解码的扩展中找到spec_version,并且必须与从提供的元数据中推导出的spec_version匹配。

存储项

可以通过rpc调用从链中查询存储项,检索到的SCALE编码数据在相应的链元数据StorageEntryType中声明。

存储项(键和值的组合)使用decode_as_storage_entry函数进行解析。

未检查的外部事务

可以使用实现AsCompleteMetadata特质的元数据,通过函数decode_as_unchecked_extrinsic对未检查的外部事务进行解码。

其他项目

如果已知对应类型,则可以使用函数decode_as_type_at_position对字节块中的任何部分进行解码。

对于整个块对应单一已知类型的情况,建议使用函数decode_all_as_type

解析数据和卡片

使用给定类型解析数据会产生ExtendedData。将数据解析为调用结果会产生Call。这两种类型都很复杂,通常包含多层解析数据。在解析过程中,尽可能地保留内部数据结构、标识符和文档,以便更容易在解析项中查找信息或进行模式匹配。

所有解析结果都可以制成卡片。卡片是带有类型相关信息的扁平格式元素,可以打印或以其他方式显示给用户。每个CallExtendedData都会制成Vec<ExtendedCard>

特殊类型

存储在元数据类型注册表中的类型具有相关的Path信息。用于检测特殊类型的Pathident段。

一些Path标识符未经进一步检查即可使用,例如知名的基于数组的类型(AccountId32、散列、公钥、签名等)或具有已知或容易确定的编码大小的其他类型,如EraPerThing项等。

其他Path标识符首先进行检查,并且仅在发现的类型信息与预期匹配时才使用,这对于CallEvent是这种情况。如果不匹配,则数据按原样解析,即不适用于特定项目格式。

枚举和结构体包含一组Field。字段nametype_name也可能提供有关类型特殊信息的线索,尽管不如Path可靠。这样的提示不会在解析流程中出现错误,并且会被忽略。

字段可能包含与货币相关的数据。在制成卡片并显示时,只有在出现在显示余额的或扩展中时,货币才会以链上小数和单位显示。

某些类型(如 AccountId32Era、公钥类型、签名类型)在本软件包(模块 additional_types)中进行了重新定义,与在 sp_coresp_runtime 软件包中的原始类型类似。这样做是为了确保与 no_std 的兼容性并简化依赖关系树。如果需要,这些类型的内部内容可以无缝转移到原始类型中。

特性

default-features = false 模式下,软件包支持 no_std

示例

 # #[cfg(feature = "std")]
 # {
 use frame_metadata::v14::RuntimeMetadataV14;
 use parity_scale_codec::Decode;
 use primitive_types::H256;
 use scale_info::{IntoPortable, Path, Registry};
 use std::str::FromStr;
 use substrate_parser::{
     parse_transaction,
     AddressableBuffer,
     AsMetadata,
     additional_types::{AccountId32, Era},
     cards::{
         Call, ExtendedData, FieldData, Info,
         PalletSpecificData, ParsedData, VariantData,
     },
     special_indicators::SpecialtyUnsignedInteger,
 };

 // A simple signable transaction: Alice sends some cash by `transfer_keep_alive` method
 let signable_data = hex::decode("9c0403008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a480284d717d5031504025a62029723000007000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e98a8ee9e389043cd8a9954b254d822d34138b9ae97d3b7f50dc6781b13df8d84").unwrap();

 // Hexadecimal metadata, such as one fetched through rpc query
 let metadata_westend9111_hex = std::fs::read_to_string("for_tests/westend9111").unwrap();

 // SCALE-encoded `V14` metadata, first 5 elements cut off here are `META` prefix and
 // `V14` enum index
 let metadata_westend9111_vec = hex::decode(&metadata_westend9111_hex.trim()).unwrap()[5..].to_vec();

 // `RuntimeMetadataV14` decoded and ready to use.
 let metadata_westend9111 = RuntimeMetadataV14::decode(&mut &metadata_westend9111_vec[..]).unwrap();

 // Chain genesis hash, typically well-known. Could be fetched through a separate rpc query.
 let westend_genesis_hash = H256::from_str("e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e").unwrap();

 let parsed = parse_transaction(
     &signable_data.as_ref(),
     &mut (),
     &metadata_westend9111,
     Some(westend_genesis_hash),
 ).unwrap();

 let call_data = parsed.call_result.unwrap();

 // Pallet name.
 assert_eq!(call_data.0.pallet_name, "Balances");

 // Call name within the pallet.
 assert_eq!(call_data.0.variant_name, "transfer_keep_alive");

 // Call contents are the associated `Field` data.
 let expected_field_data = vec![
     FieldData {
         field_name: Some(String::from("dest")),
         type_name: Some(String::from("<T::Lookup as StaticLookup>::Source")),
         field_docs: String::new(),
         data: ExtendedData {
             data: ParsedData::Variant(VariantData {
                 variant_name: String::from("Id"),
                 variant_docs: String::new(),
                 fields: vec![
                     FieldData {
                         field_name: None,
                         type_name: Some(String::from("AccountId")),
                         field_docs: String::new(),
                         data: ExtendedData {
                             data: ParsedData::Id(AccountId32(hex::decode("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48").unwrap().try_into().unwrap())),
                             info: vec![
                                 Info {
                                     docs: String::new(),
                                     path: Path::from_segments(vec![
                                         "sp_core",
                                         "crypto",
                                         "AccountId32"
                                     ])
                                        .unwrap()
                                        .into_portable(&mut Registry::new()),
                                 }
                             ]
                         }
                     }
                 ]
             }),
             info: vec![
                 Info {
                     docs: String::new(),
                     path: Path::from_segments(vec![
                         "sp_runtime",
                         "multiaddress",
                         "MultiAddress"
                     ])
                        .unwrap()
                        .into_portable(&mut Registry::new()),
                 }
             ],
         }
     },
     FieldData {
         field_name: Some(String::from("value")),
         type_name: Some(String::from("T::Balance")),
         field_docs: String::new(),
         data: ExtendedData {
             data: ParsedData::PrimitiveU128{
                 value: 100000000,
                 specialty: SpecialtyUnsignedInteger::Balance,
             },
             info: Vec::new()
         }
     }
 ];
 assert_eq!(call_data.0.fields, expected_field_data);

 // Parsed extensions. Note that many extensions are empty.
 let expected_extensions_data = vec![
     ExtendedData {
         data: ParsedData::Composite(Vec::new()),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "frame_system",
                     "extensions",
                     "check_spec_version",
                     "CheckSpecVersion",
                 ])
                     .unwrap()
                     .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Composite(Vec::new()),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "frame_system",
                     "extensions",
                     "check_tx_version",
                     "CheckTxVersion",
                 ])
                     .unwrap()
                     .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Composite(Vec::new()),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "frame_system",
                     "extensions",
                     "check_genesis",
                     "CheckGenesis",
                 ])
                     .unwrap()
                     .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Composite(vec![
             FieldData {
                 field_name: None,
                 type_name: Some(String::from("Era")),
                 field_docs: String::new(),
                 data: ExtendedData {
                     info: vec![
                         Info {
                             docs: String::new(),
                             path: Path::from_segments(vec![
                                 "sp_runtime",
                                 "generic",
                                 "era",
                                 "Era",
                             ])
                             .unwrap()
                             .into_portable(&mut Registry::new()),
                         }
                     ],
                     data: ParsedData::Era(Era::Mortal(64, 61)),
                 }
             }
         ]),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "frame_system",
                     "extensions",
                     "check_mortality",
                     "CheckMortality",
                 ])
                 .unwrap()
                 .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Composite(vec![
             FieldData {
                 field_name: None,
                 type_name: Some(String::from("T::Index")),
                 field_docs: String::new(),
                 data: ExtendedData {
                     data: ParsedData::PrimitiveU32 {
                         value: 261,
                         specialty: SpecialtyUnsignedInteger::Nonce,
                     },
                     info: Vec::new()
                 }
             }
         ]),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "frame_system",
                     "extensions",
                     "check_nonce",
                     "CheckNonce",
                 ])
                     .unwrap()
                     .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Composite(Vec::new()),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "frame_system",
                     "extensions",
                     "check_weight",
                     "CheckWeight",
                 ])
                     .unwrap()
                     .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Composite(vec![
             FieldData {
                 field_name: None,
                 type_name: Some(String::from("BalanceOf<T>")),
                 field_docs: String::new(),
                 data: ExtendedData {
                     data: ParsedData::PrimitiveU128 {
                         value: 10000000,
                         specialty: SpecialtyUnsignedInteger::Tip
                     },
                     info: Vec::new()
                 }
             }
         ]),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                     "pallet_transaction_payment",
                     "ChargeTransactionPayment",
                 ])
                     .unwrap()
                     .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::PrimitiveU32 {
             value: 9111,
             specialty: SpecialtyUnsignedInteger::SpecVersion
         },
         info: Vec::new()
     },
     ExtendedData {
         data: ParsedData::PrimitiveU32 {
             value: 7,
             specialty: SpecialtyUnsignedInteger::TxVersion
         },
         info: Vec::new()
     },
     ExtendedData {
         data: ParsedData::GenesisHash(H256::from_str("e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e").unwrap()),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                    "primitive_types",
                    "H256",
                ])
                    .unwrap()
                    .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::BlockHash(H256::from_str("98a8ee9e389043cd8a9954b254d822d34138b9ae97d3b7f50dc6781b13df8d84").unwrap()),
         info: vec![
             Info {
                 docs: String::new(),
                 path: Path::from_segments(vec![
                    "primitive_types",
                    "H256",
                ])
                    .unwrap()
                    .into_portable(&mut Registry::new()),
             }
         ]
     },
     ExtendedData {
         data: ParsedData::Tuple(Vec::new()),
         info: Vec::new()
     },
     ExtendedData {
         data: ParsedData::Tuple(Vec::new()),
         info: Vec::new()
     },
     ExtendedData {
         data: ParsedData::Tuple(Vec::new()),
         info: Vec::new()
     }
 ];

  assert_eq!(parsed.extensions, expected_extensions_data);
 # }

可以使用 card 方法将解析的数据转换为一系列平坦和格式化的 ExtendedCard 卡。

可以使用 showshow_with_docs 方法将卡打印成可读的字符串。

依赖

~8MB
~150K SLoC