Skip to content
🎉 Welcome to the new Aptos Docs! Click here to submit an issue.
BuildSmart Contracts (Move)Binary Canonical Serialization (BCS)

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.

ValueBytes
true0x01
false0x00
bool.move
#[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.

u8.move
#[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.

u16.move
#[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.

u32.move
#[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.

u64.move
#[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.

u128.move
#[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.

u256.move
#[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.

uleb128.move
// 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.

vector.move
#[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)?);

Reference