Skip to main content

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

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

雖然序列化聽起來可能是進階主題,但 BCS 在 Sui 中無所不在:交易的引數會以 BCS 編碼、物件和事件會以 BCS 位元組形式 儲存,並可在鏈下讀取;而在智慧合約中簽署及驗證的訊息通常是經 BCS 序列化的結構。大多數時候編碼會由系統代為處理, 但應用程式遲早需要手動進行:解碼已簽署的承載資料、剖析作為 vector<u8> 引數傳入的原始位元組,或產生與鏈下用戶端 建立結果相符的位元組。

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

格式 (Format)​

BCS 是一種二進位格式,支援最大 256 位元的無號整數、選項、布林值、單位 (空值)、固定與可變長度序列,以及映射。此格式設計為具決定性,表示相同資料 將一律序列化為相同位元組。

「BCS 並非自我描述格式。因此,若要反序列化訊息,必須預先知道 訊息的型別及配置。」摘自 README

核心規則如下:

  • 整數以小端序位元組順序儲存;
  • 序列(例如向量)會加上長度前綴,其使用 ULEB128 編碼——一種精簡的可變長度整數編碼;
  • 列舉會儲存為變體的索引,後接該變體的 欄位;
  • 映射會儲存為依序排列的鍵值配對序列;
  • 結構會被視為欄位序列:欄位會依其在結構中的定義順序逐一序列化,中間不含名稱、 型別或分隔符號。

為使此概念更具體,以下是 User 值逐位元組的配置方式:

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

使用 BCS (Using BCS)​

有兩個模組在 Move 中實作 BCS:標準函式庫 提供含有單一原生編碼函式 to_bytes 的 std::bcs,而 Sui 框架則以此為基礎建立 sui::bcs 模組, 其重新匯出 to_bytes,並新增以 Move 實作的解碼函式。在 Sui 原始碼中,只要匯入 sui::bcs 即可同時進行編碼與解碼。

編碼 (Encoding)​

若要編碼資料,請使用 bcs::to_bytes 函式;此函式會將資料參考轉換為位元組 向量。此函式支援編碼任何型別,包括結構與列舉。

module std::bcs;

/// 以 BCS(二進位標準
/// 序列化)格式回傳 `v` 的二進位表示。
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");

結構編碼 (Encoding a Struct)​

結構的編碼僅由其欄位逐一組成。以下範例會編碼格式章節中的 User 值、檢查表格中的確切位元組,接著直接示範「欄位序列」規則——串接各自編碼的 欄位會得到相同結果:

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 並非自我描述格式,解碼時必須預先知道資料型別。這不只是形式上的要求—— 相同位元組在不同解讀下都可能完全有效,且解碼器無法偵測不相符的情況。上述已編碼 User 的 6 個位元組同樣可以讀取為一個 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_* 函式家族,其他所有情況則使用巨集。若解碼器用盡位元組——或位元組 未構成有效值,例如布林位元組不是 0 或 1——呼叫就會中止。

包裝器 API (Wrapper API)​

解碼器是位元組的包裝器:bcs::new 函式會依值接收位元組,然後呼叫端透過呼叫 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);

向量解碼 (Decoding Vectors)​

雖然大多數基本型別都有專用的解碼函式,向量需要特殊處理,且取決於元素的型別。 其底層結構始終相同:先解碼向量長度,再於迴圈中解碼每個元素。

// 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! 巨集,它會在內部執行迴圈,並針對每個元素呼叫 給定函式一次;此外,也為基本型別的向量提供現成的 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]);

選項解碼 (Decoding Option)​

Option 會編碼為單一位元組——0 表示 none、1 表示 some——若存在值,後面會接續該值。peel_option! 巨集會讀取此位元組,且只有在值存在時 才評估給定函式;基本型別也有現成的 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));

結構解碼 (Decoding Structs)​

沒有方法可以自動將位元組解碼為 Move 結構——結構 只能由其模組封裝,而且位元組不帶有其所代表內容的資訊。若要將位元組剖析為結構, 請剝離每個欄位並封裝該型別。以下範例完成完整往返:它編碼一個 User 值,從位元組 解碼回來,並檢查結果與原始值完全相同。

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);

位元組不含欄位名稱或型別標籤,因此能使解碼正確的唯一條件,就是以完全相同的順序 剝離與編碼時完全相同的型別。順序錯誤不一定會中止——它可能會悄悄產生錯誤值,如 上述範例所示。

列舉解碼 (Decoding Enums)​

列舉值會編碼為其變體的索引,後接該變體的 欄位。解碼與此對應: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 的標準二進位序列化格式:具決定性——相同值一律產生相同位元組。
  • 此格式並非自我描述:位元組不帶有名稱或型別,讀取端必須預先知道配置。
  • 結構與列舉會依宣告順序將其欄位編碼;解碼時必須以相同順序剝離相同型別。
  • 編碼使用 bcs::to_bytes;解碼使用 bcs::new 包裝器,以及 peel_* 函式與巨集 家族;它們會在輸入格式不正確或遭截斷時中止。

延伸閱讀 (Further Reading)​