BCS 编码

Binary Canonical Serialization, BCS, 是在 Diem 区块链项目中开发出来的序列化格式,现在也被广泛应用于大部分基于 Move 的区块链,比如Sui, Starcoin, Aptos, 0L. 除了在 Move VM 虚拟机中使用,BCS也被用在交易 transaction 和事件 event 编码中,比如在签署交易之前做序列化处理,解析事件数据。

如果你想深入了解Move的工作原理并成为Move专家,了解BCS的工作原理是至关重要的。让我们开启深入探讨。

BCS 特性说明

在我们继续学习的过程中,有一些关于BCS编码的高级属性是值得记住的:

  • BCS是一种数据序列化格式,其生成的输出字节不包含任何类型信息。因此,接收编码字节的一方需要知道如何反序列化数据

  • BCS 中没有数据类型,当然也没有结构体 structs; struct 只是定义了内部字段 fields 被序列化的顺序

  • Wrapper 类型会被忽略掉,因此 OuterTypeUnnestedType 会有同样的BCS表示:

    #![allow(unused)] fn main() { public struct OuterType { owner: InnerType } public struct InnerType { address: address } public struct UnnestedType { address: address } }
  • 包含泛型类型字段的类型可以被解析到第一个泛型类型字段。因此,如果泛型类型字段是自定义类型,并且需要进行序列化和反序列化操作,将泛型类型字段放在最后是一个好的实践方式。

    #![allow(unused)] fn main() { public struct BCSObject<T> has drop, copy { id: ID, owner: address, meta: Metadata, generic: T } }

    在这个例子中,我们可以将所有数据反序列化直到meta字段。

  • 原始类型 primitive types(如无符号整数)以小端格式进行编码

  • 不定长向量 Vector 被序列化成一个表明包含向量 vector 长度的数字(最大取值是 u32), 后面跟着向量内的元素。参考样例都是采用小端编码

完整的BCS特性说明可以在 BCS repository 里找到。

使用 @mysten/bcs NPM 库

运行

查看库文档@mysten/bcs library. 运行环境是deno, 无手动安装操作,直接导入即可。

import { BCS, getSuiMoveConfig } from "npm:@mysten/bcs";

基础用例

使用bcs库对一些简单数据做序列化和反序列化操作:

import { BCS, getSuiMoveConfig } from "npm:@mysten/bcs"; // initialize the serializer with default Sui Move configurations const bcs = new BCS(getSuiMoveConfig()); // Define some test data types const integer = 10; const array = [1, 2, 3, 4]; const string = "test string" // use bcs.ser() to serialize data const ser_integer = bcs.ser(BCS.U16, integer); const ser_array = bcs.ser("vector<u8>", array); const ser_string = bcs.ser(BCS.STRING, string); // use bcs.de() to deserialize data const de_integer = bcs.de(BCS.U16, ser_integer.toBytes()); const de_array = bcs.de("vector<u8>", ser_array.toBytes()); const de_string = bcs.de(BCS.STRING, ser_string.toBytes());

我们可以像上面的语法那样,用内置的默认设置new BCS(getSuiMoveConfig())来初始化Sui Move的序列化器实例。

BCS中有内置的枚举类型,如BCS.U16, BCS.STRING等,可以直接被当作 Sui Move 类型使用。对于泛型类型,可以使用与Sui Move相同的语法进行定义,例如上面的示例中的vector<u8>

现在来仔细观察序列化和反序列化字段:

# ints are little endian hexadecimals 0a00 10 # the first element of a vector indicates the total length, # then it's just whatever elements are in the vector 0401020304 1,2,3,4 # strings are just vectors of u8's, with the first element equal to the length of the string 0b7465737420737472696e67 test string

类型注册

可以使用以下语法,来注册我们将要使用的自定义类型:

import { BCS, getSuiMoveConfig } from "npm:@mysten/bcs"; const bcs = new BCS(getSuiMoveConfig()); // Register the Metadata Type bcs.registerStructType("Metadata", { name: BCS.STRING, }); // Same for the main object that we intend to read bcs.registerStructType("BCSObject", { // BCS.ADDRESS is used for ID types as well as address types id: BCS.ADDRESS, owner: BCS.ADDRESS, meta: "Metadata", });

在 Sui 智能合约中使用 bcs

继续使用上面 structs 的例子来进行演示。

定义 Struct

我们首先在 Sui Move 合约中定义与之前对应的 struct.

#![allow(unused)] fn main() { { //.. struct Metadata has drop, copy { name: std::ascii::String } struct BCSObject has drop, copy { id: ID, owner: address, meta: Metadata } //.. } }

反序列化

现在,在 Sui 合约中写一个函数将一个 object 反序列化操作。

#![allow(unused)] fn main() { public fun object_from_bytes(bcs_bytes: vector<u8>): BCSObject { // Initializes the bcs bytes instance let bcs = bcs::new(bcs_bytes); // Use `peel_*` functions to peel values from the serialized bytes. // Order has to be the same as we used in serialization! let (id, owner, meta) = ( bcs::peel_address(&mut bcs), bcs::peel_address(&mut bcs), bcs::peel_vec_u8(&mut bcs) ); // Pack a BCSObject struct with the results of serialization BCSObject { id: object::id_from_address(id), owner, meta: Metadata {name: std::ascii::string(meta)} } } }

在 Sui bcs 模块中,各种peel_*方法用于从BCS序列化的字节中"peel"出每个单独的字段。请注意,我们"peel"字段的顺序必须与结构定义中字段的顺序完全相同。

测验: 为什么在对同一个bcs对象调用的前两个peel_address的结果不相同?

还要注意我们如何使用辅助函数将类型从address转换为id,以及从vector<8>转换为std::ascii::string.

测验: 如果BSCObject拥有的类型是UID而不是ID,会发生什么?

补全 序列化/反序列化 示例

完整的 TypeScript 和 Sui Move 示例代码可以在example_projects文件夹中找到。

首先,我们使用TypeScript程序序列化一个测试object:

// We construct a test object to serialize, note that we can specify the format of the output to hex let _bytes = bcs .ser("BCSObject", { id: "0x0000000000000000000000000000000000000000000000000000000000000005", owner: "0x000000000000000000000000000000000000000000000000000000000000000a", meta: {name: "aaa"} }) .toString("hex");

这次我们希望BCS writer的输出是十六进制格式,可以像上面那样指定。

将序列化结果的十六进制字符串添加前缀 0x, 并导出到一个环境变量中:

export OBJECT_HEXSTRING=0x0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000a03616161

现在我们可以运行相关的Move单元测试来检查正确性:

sui move test

你应该会在控制台中看到这个:

BUILDING bcs_move Running Move unit tests [ PASS ] 0x0::bcs_object::test_deserialization Test result: OK. Total tests: 1; passed: 1; failed: 0

或者我们可以发布该模块(并导出PACKAGE_ID), 然后使用上述BCS序列化的十六进制字符串调用 emit_object 方法:

sui client call --function emit_object --module bcs_object --package $PACKAGE_ID --args $OBJECT_HEXSTRING --gas-budget 100000000

我们可以检查Sui Explorer上交易事务的 Events 选项卡,以查看emit输出的反序列化BCSObject是否正确:

Event