Skip to main content

Binary Canonical Serialization 二進位標準序列化 (Binary Canonical Serialization)

Binary Canonical Serialization(BCS)是一種用於結構化資料的二進位編碼格式。它最初是在 Diem 中設計的,後來成為 Move 的標準序列化格式。BCS 簡單、高效、具確定性,且容易用任何程式語言實作。

雖然序列化聽起來像是進階主題,但 BCS 在 Sui 上無所不在:交易的參數是以 BCS 編碼的,物件與事件是以 BCS bytes 的形式儲存 — 也是以此形式在鏈下讀取的,而智慧合約中簽署與驗證的訊息通常也是以 BCS 序列化的 struct。大多數時候編碼是自動幫你處理好的,但遲早會有應用需要手動處理:解碼已簽署的 payload、解析以 vector<u8> 參數傳入的原始 bytes,或產生與鏈下客戶端建構結果相符的 bytes。

完整的格式規範可在 BCS repository 中取得。

格式 (Format)

BCS 是一種二進位格式,支援最多 256 位元的無號整數、option、boolean、unit(空值)、固定與變動長度的序列,以及 map。此格式的設計具有確定性,意即相同的資料永遠會序列化為相同的 bytes。

「BCS 並非自我描述格式。因此,要反序列化一則訊息,必須事先知道該訊息的型別與版面配置」——引自 README

核心規則如下:

  • 整數以小端序(little-endian)位元組順序儲存;
  • 序列(如 vector)會以其長度作為前綴,並以 ULEB128 編碼 — 一種緊湊、可變長度的整數編碼方式;
  • enum 會以變體(variant)的索引儲存,後面接著該變體的欄位;
  • map 會以有序的鍵值對序列儲存;
  • struct 會被視為欄位的序列:各欄位依照在 struct 中定義的順序依序序列化,中間不含名稱、型別或分隔符。

以下具體展示 User 值是如何逐位元組排列的:

/// A struct we will encode and decode in the examples below.
public struct User has drop {
age: u8,
is_active: bool,
name: String,
}
欄位編碼後的 bytes
age: u8422A
is_active: booltrue01
name: String"Bob"03 42 6F 62(長度 + bytes)
User(以上全部)2A 01 03 42 6F 62

使用 BCS (Using BCS)

Move 中有兩個模組實作了 BCS:Standard Library 提供 std::bcs,其中只有單一原生編碼函式 to_bytes;而 Sui Framework 在此基礎上建立了 sui::bcs 模組,該模組重新匯出了 to_bytes,並新增了以 Move 實作的解碼函式。在 Sui 程式碼中,只要匯入 sui::bcs 即可同時進行編碼與解碼。

編碼 (Encoding)

要編碼資料,可使用 bcs::to_bytes 函式,它會將資料參考轉換為 byte vector。此函式支援對任何型別進行編碼,包括 struct 與 enum。

module std::bcs;

/// 回傳 `v` 以 BCS(Binary Canonical
/// Serialization)格式的二進位表示。
public native fun to_bytes<MoveValue>(v: &MoveValue): vector<u8>;

以下範例展示了基本型別的編碼:

use sui::bcs;

// 0x01 - a single byte with value 1 (or 0 for false)
let bool_bytes = bcs::to_bytes(&true);
assert_eq!(bool_bytes, x"01");

// 0x2a - just a single byte
let u8_bytes = bcs::to_bytes(&42u8);
assert_eq!(u8_bytes, x"2A");

// 0x2a00000000000000 - 8 bytes, little-endian
let u64_bytes = bcs::to_bytes(&42u64);
assert_eq!(u64_bytes, x"2A00000000000000");

// address is a fixed sequence of 32 bytes
// 0x0000000000000000000000000000000000000000000000000000000000000002
let addr = bcs::to_bytes(&@sui);
assert_eq!(addr, x"0000000000000000000000000000000000000000000000000000000000000002");

編碼 Struct (Encoding a Struct)

struct 的編碼不過就是其欄位依序排列而已。以下範例編碼了 格式 (Format) 小節中的 User 值,核對了表格中的確切 bytes,接著直接展示「欄位序列」規則 — 將各別編碼的欄位串接起來,會得到相同的結果:

let user = User {
age: 42,
is_active: true,
name: "Bob",
};

// A struct is encoded as its fields, one after another, in the
// order they are declared: no names, no types, no separators.
//
// age | is_active | name
// 2A | 01 | 03 42 6F 62 (length + "Bob")
let user_bytes = bcs::to_bytes(&user);
assert_eq!(user_bytes, x"2A0103426F62");

// Concatenating individually encoded fields gives the same bytes!
let name: String = "Bob";
let mut field_bytes = vector[];
field_bytes.append(bcs::to_bytes(&42u8));
field_bytes.append(bcs::to_bytes(&true));
field_bytes.append(bcs::to_bytes(&name));

assert_eq!(user_bytes, field_bytes);

解碼 (Decoding)

由於 BCS 並非自我描述格式,解碼需要事先知道資料型別。這不只是形式上的要求 — 同一組 bytes 在不同解讀方式下都完全有效,而解碼器無法偵測不匹配的情況。上面編碼後的 User 的 6 個 bytes,同樣可以被解讀為一個 u16 後面接一個 vector<u8>

// The exact same 6 bytes that encoded the `User` above...
let mut bcs = bcs::new(x"2A0103426F62");

// ...can be read as completely different types. The bytes carry
// no type information - the reader decides what they mean.
let num = bcs.peel_u16(); // 0x012A = 298
let vec = bcs.peel_vec_u8(); // [0x42, 0x6F, 0x62]

assert_eq!(num, 298);
assert_eq!(vec, vector[66, 111, 98]);

sui::bcs 模組提供了輔助解碼的函式:針對基本型別有 peel_boolpeel_u8peel_u256,以及 peel_address;針對常見容器則有 peel_vec_* 系列與 peel_option_* 系列;其餘情況則有巨集可用。若解碼器的 bytes 用盡 — 或這些 bytes 無法構成有效的值,例如 boolean byte 不是 01 — 該次呼叫就會中止(abort)。

包裝器 API (Wrapper API)

解碼器是包裝這些 bytes 的一個包裝器:bcs::new 函式以傳值方式接收 bytes,接著呼叫端透過呼叫 peel_* 函式,由前到後逐一「剝離」出各個值。尚未被解碼的部分會留在包裝器中,並可透過 into_remainder_bytes 函式取出。

use sui::bcs;

// The decoder wraps the bytes; it must be declared as mutable,
// because every `peel_*` call consumes a part of the input.
let mut bcs = bcs::new(x"012A2823000000000000");

let bool_value = bcs.peel_bool();
assert_eq!(bool_value, true);

let u8_value = bcs.peel_u8();
assert_eq!(u8_value, 42);

// Whatever was not decoded can be taken back out of the wrapper.
let remainder = bcs.into_remainder_bytes();
assert_eq!(remainder.length(), 8);

在解碼過程中,有個常見做法是在單一 let 陳述式中使用多個變數。這讓程式碼稍微更易讀,也有助於避免不必要的資料複製。

let mut bcs = bcs::new(x"012A2823000000000000");

// mind the order!!!
// handy way to peel multiple values
let (bool_value, u8_value, u64_value) = (
bcs.peel_bool(),
bcs.peel_u8(),
bcs.peel_u64(),
);

assert_eq!(u64_value, 9000);

解碼 Vector (Decoding Vectors)

雖然大多數基本型別都有專屬的解碼函式,但 vector 需要特殊處理,處理方式取決於元素的型別。其底層結構永遠相同:先解碼出 vector 的長度,接著在迴圈中逐一解碼每個元素。

// vector[1u64, 2u64]: length prefix `02`, then the two elements
let mut bcs = bcs::new(x"0201000000000000000200000000000000");

// first, peel the length of the vector...
let mut len = bcs.peel_vec_length();
let mut vec = vector[];

// ...then peel each element in a loop
while (len > 0) {
vec.push_back(bcs.peel_u64()); // or any other type
len = len - 1;
};

assert_eq!(vec, vector[1, 2]);

在日常使用中,函式庫提供了 peel_vec! 巨集,它會在內部執行該迴圈,並針對每個元素呼叫一次給定的函式;此外也針對基本型別的 vector 提供了現成的 peel_vec_* 函式:

let mut bcs = bcs::new(x"0201000000000000000200000000000000");

// The `peel_vec!` macro does the same in a single call.
let vec = bcs.peel_vec!(|bcs| bcs.peel_u64());
assert_eq!(vec, vector[1, 2]);

// For vectors of primitive types, there are ready-made functions.
let mut bcs = bcs::new(x"0201000000000000000200000000000000");
let vec = bcs.peel_vec_u64();
assert_eq!(vec, vector[1, 2]);

解碼 Option (Decoding Option)

Option 是以單一 byte 編碼的 — 0 代表 none1 代表 some — 後面接著該值(若存在的話)。peel_option! 巨集會讀取該 byte,只有在值存在時才會執行給定的函式;基本型別也有現成的 peel_option_* 函式可用。

// `option::none<u8>()` is a single `00` byte...
let mut bcs = bcs::new(x"00");
let none = bcs.peel_option!(|bcs| bcs.peel_u8());
assert!(none.is_none());

// ...and `option::some(42u8)` is `01` followed by the value.
let mut bcs = bcs::new(x"012A");
let some = bcs.peel_option!(|bcs| bcs.peel_u8());
assert_eq!(some, option::some(42));

// For primitive types, there are ready-made `peel_option_*` functions.
let mut bcs = bcs::new(x"012A");
let some = bcs.peel_option_u8();
assert_eq!(some, option::some(42));

解碼 Struct (Decoding Structs)

沒有辦法自動將 bytes 解碼為 Move 的 struct — struct 只能由其所屬模組打包(pack),而這些 bytes 本身不帶有任何關於它們所代表內容的資訊。要將 bytes 解析為 struct,必須逐一剝離每個欄位,再打包成該型別。以下範例走完整趟流程:編碼一個 User 值、從 bytes 將其解碼回來,並確認結果與原始值完全相同。

let user = User {
age: 42,
is_active: true,
name: "Bob",
};

// Encode the value...
let mut bcs = bcs::new(bcs::to_bytes(&user));

// ...and decode it back, peeling the fields in exactly the order
// they are declared in the struct definition.
let decoded = User {
age: bcs.peel_u8(),
is_active: bcs.peel_bool(),
name: bcs.peel_vec_u8().to_string(),
};

assert_eq!(user, decoded);

這些 bytes 不含任何欄位名稱或型別標籤,因此讓解碼正確的唯一關鍵,就是以與編碼時完全相同的順序、剝離出完全相同的型別。順序錯誤未必會導致中止(abort) — 它可能會悄悄產生錯誤的值,就如同上面的範例所示。

解碼 Enum (Decoding Enums)

enum 值的編碼方式是以其變體(variant)的索引,後面接著該變體的欄位。解碼的方式與此對應:peel_enum_tag 函式會讀取變體索引,接著針對該索引使用 match 運算式來解碼對應的欄位:

let status = Status::Shipped { tracking: 12345 };

// An enum value is encoded as the variant index, followed by the
// fields of that variant.
let mut bcs = bcs::new(bcs::to_bytes(&status));

let decoded = match (bcs.peel_enum_tag()) {
0 => Status::Pending,
1 => Status::Shipped { tracking: bcs.peel_u64() },
_ => abort,
};

assert_eq!(status, decoded);

總結 (Summary)

  • BCS 是 Move 的標準二進位序列化格式:具確定性 — 相同的值永遠會產生相同的 bytes。
  • 此格式並非自我描述的:這些 bytes 不帶有名稱或型別,讀取端必須事先知道版面配置。
  • struct 與 enum 是依照宣告順序將其欄位編碼;解碼時也必須以相同順序剝離相同的型別。
  • 編碼使用 bcs::to_bytes 完成;解碼則使用 bcs::new 包裝器與 peel_* 系列的函式與巨集,遇到格式錯誤或截斷的輸入時會中止(abort)。

延伸閱讀 (Further Reading)