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.
Overview
Because BCS is not a self describing format, the reader must know the format of the bytes ahead of time.
Primitive Types
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.
Bool (boolean)
Booleans are serialized as a single byte. true
is serialized as 0x01
and
false
is serialized as 0x00
. All other values are invalid.
Value | Bytes |
---|---|
true | 0x01 |
false | 0x00 |
#[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);
}
}
U8 (unsigned 8-bit integer)
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);
}
}
U16 (unsigned 16-bit integer)
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);
}
}
U32 (unsigned 32-bit integer)
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);
}
}
U64 (unsigned 64-bit integer)
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);
}
}
U128 (unsigned 128-bit integer)
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);
}
}
U256 (unsigned 256-bit integer)
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)
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
Sequence and FixedSequence
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]);
}
}
Complex types
String
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
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]
Struct
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]
Option
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]);
Enum
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
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)?);