Skip to content

Binary Canonical Serialization (BCS)

Binary Canonical Serialization (BCS) is the serialization format used on the Aptos blockchain. It is a binary canonical non-self-describing serialization format that is used to serialize data structures. BCS is used to serialize all data on-chain, provide binary responses on the REST API, and encode input arguments to transactions.

Because BCS is not a self describing format, the reader must know the format of the bytes ahead of time.

8-bit, 16-bit, 32-bit, 64-bit, 128-bit, and 256-bit unsigned integers are supported. They are serialized in little-endian byte order.

Booleans are serialized as a single byte. true is serialized as 0x01 and false is serialized as 0x00. All other values are invalid.

ValueBytes
true0x01
false0x00
#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_bool() {
// Serialize
let val: bool = true;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0x01]);
// Deserialize
let val_des = from_bcs::to_bool(bytes);
assert!(val_des == true);
}
}

Unsigned 8-bit integers are serialized as a single byte.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_u8() {
// Serialize
let val: u8 = 1;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0x01]);
// Deserialize
let val_des = from_bcs::to_u8(bytes);
assert!(val_des == 1);
}
}

Unsigned 16-bit integers are serialized as 2 bytes in little-endian byte order.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_u16() {
// Serialize
let val: u16 = 1000;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0xe8, 0x03]);
// Deserialize
let val_des = from_bcs::to_u16(bytes);
assert!(val_des == 1000);
}
}

Unsigned 32-bit integers are serialized as 4 bytes in little-endian byte order.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_u32() {
// Serialize
let val: u32 = 1000000000;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0x00, 0xca, 0x9a, 0x3b]);
// Deserialize
let val_des = from_bcs::to_u32(bytes);
assert!(val_des == 1000000000);
}
}

Unsigned 64-bit integers are serialized as 8 bytes in little-endian byte order.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_u64() {
// Serialize
let val: u64 = 10000000000000000;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]);
// Deserialize
let val_des = from_bcs::to_u64(bytes);
assert!(val_des == 10000000000000000);
}
}

Unsigned 128-bit integers are serialized as 16 bytes in little-endian byte order.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_u128() {
// Serialize
let val: u128 = 10000000000000000;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]);
// Deserialize
let val_des = from_bcs::to_u128(bytes);
assert!(val_des == 10000000000000000);
}
}

Unsigned 256-bit integers are serialized as 32 bytes in little-endian byte order.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_u256() {
// Serialize
let val: u256 = 10000000000000000;
let bytes: vector<u8> = bcs::to_bytes(&val);
assert!(bytes == vector[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]);
// Deserialize
let val_des = from_bcs::to_u256(bytes);
assert!(val_des == 10000000000000000);
}
}

Uleb128 (unsigned 128-bit variable length integer)

Section titled “Uleb128 (unsigned 128-bit variable length integer)”

Unsigned 128-bit variable length integers are serialized as a variable number of bytes. The most significant bit of each byte is used to indicate if there are more bytes to read. The remaining 7 bits are used to store the value.

This is common used for variable lengths of vectors, or for enums.

// Currently not supported by itself in Move

Sequences are serialized as a variable length vector of an item. The length of the vector is serialized as a Uleb128 followed by repeated items. FixedSequences are serialized without the leading size byte. The reader must know the number of bytes prior to deserialization.

#[test_only]
module 0x42::example {
use std::bcs;
use std::from_bcs;
#[test]
fun test_vector() {
// Serialize
let val = vector[1u8, 2u8, 3u8];
let bytes = bcs::to_bytes(&val);
assert!(bytes == vector[3, 1, 2, 3]);
// Deserialize, only supports bytes for now
let val_des = from_bcs::to_bytes(bytes);
assert!(val_des == vector[1, 2, 3]);
}
}

Strings are serialized as a vector of bytes, however the bytes are encoded as UTF-8.

// Note that this string has 10 characters but has a byte length of 24
let utf8_str = "çå∞≠¢õß∂ƒ∫";
let expecting = vec![
24, 0xc3, 0xa7, 0xc3, 0xa5, 0xe2, 0x88, 0x9e, 0xe2, 0x89, 0xa0, 0xc2,
0xa2, 0xc3, 0xb5, 0xc3, 0x9f, 0xe2, 0x88, 0x82, 0xc6, 0x92, 0xe2, 0x88, 0xab,
];
assert_eq!(to_bytes(&utf8_str)?, expecting);

AccountAddress is serialized as a fixed 32 byte vector of bytes.

@0x1 => [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01]

Structs are serialized as an ordered set of fields. The fields are serialized in the order they are defined in the struct.

struct Color {
r: u8 = 1,
g: u8 = 2,
b: u8 = 3,
} => [0x01, 0x02, 0x03]

Options are serialized as a single byte to determine whether it’s filled. If the option is None, the byte is 0x00. If the option is Some, the byte is 0x01 followed by the serialized value.

let some_data: Option<u8> = Some(8);
assert_eq!(to_bytes(&some_data)?, vec![1, 8]);
let no_data: Option<u8> = None;
assert_eq!(to_bytes(&no_data)?, vec![0]);

Enums are serialized as a uleb128 to determine which variant is being used. The size is followed by the serialized value of the variant.

#[derive(Serialize)]
enum E {
Variant0(u16),
Variant1(u8),
Variant2(String),
}
let v0 = E::Variant0(8000);
let v1 = E::Variant1(255);
let v2 = E::Variant2("e".to_owned());
assert_eq!(to_bytes(&v0)?, vec![0, 0x40, 0x1F]);
assert_eq!(to_bytes(&v1)?, vec![1, 0xFF]);
assert_eq!(to_bytes(&v2)?, vec![2, 1, b'e']);

Maps are stored as a sequence of tuples. The length of the map is serialized as a Uleb128 followed by repeated key-value pairs.

let mut map = HashMap::new();
map.insert(b'e', b'f');
map.insert(b'a', b'b');
map.insert(b'c', b'd');
let expecting = vec![(b'a', b'b'), (b'c', b'd'), (b'e', b'f')];
assert_eq!(to_bytes(&map)?, to_bytes(&expecting)?);