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: u8 | 42 | 2A |
| is_active: bool | true | 01 |
| 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_bool、peel_u8 到 peel_u256,以及 peel_address;針對常見容器則有 peel_vec_* 系列與 peel_option_* 系列;其餘情況則有巨集可用。若解碼器的 bytes 用盡 — 或這些 bytes 無法構成有效的值,例如 boolean byte 不是 0 或 1 — 該次呼叫就會中止(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 代表 none,1 代表 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)
- BCS specification - 完整的格式說明。
- std::bcs 與 sui::bcs 模組 文件。