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