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
Section titled “Overview”Because BCS is not a self describing format, the reader must know the format of the bytes ahead of time.
Primitive Types
Section titled “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)
Section titled “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); }}// Serializelet val: bool = true;let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![0x01]);
// Deserializelet val_des = bcs::from_bytes::<bool>(&bytes).unwrap();assert_eq!(val_des, true);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeBool(true);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([1]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeBool();console.log(val == true);import ( "github.com/aptos-labs/aptos-go-sdk")
func main() { // Serialize ser := bcs.Serializer{} ser.Bool(true) trueBytes := ser.ToBytes() trueBytes == []byte{0x01}
// Deserialize des := bcs.NewDeserializer(trueBytes) val := des.Bool() val == true}U8 (unsigned 8-bit integer)
Section titled “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); }}// Serializelet val: u8 = 1;let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![0x01]);
// Deserializelet val_des = bcs::from_bytes::<u8>(&bytes).unwrap();assert_eq!(val_des, 1);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU8(1);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([1]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeU8();console.log(val == 1);import ( "github.com/aptos-labs/aptos-go-sdk")
func main() { // Serialize ser := bcs.Serializer{} ser.U8(1) trueBytes := ser.ToBytes() trueBytes == []byte{0x01}
// Deserialize des := bcs.NewDeserializer(trueBytes) val := des.U8() val == 1}U16 (unsigned 16-bit integer)
Section titled “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); }}// Serializelet val: u16 = 1000;let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![0xe8, 0x03]);
// Deserializelet val_des = bcs::from_bytes::<u16>(&bytes).unwrap();assert_eq!(val_des, 1000);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU16(1000);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([0xe8, 0x03]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeU16();console.log(val == 1000);import ( "github.com/aptos-labs/aptos-go-sdk")
func main() { // Serialize ser := bcs.Serializer{} ser.U16(1000) bytes := ser.ToBytes() bytes == []byte{0xe8, 0x03}
// Deserialize des := bcs.NewDeserializer(bytes) val := des.U16() val == 1000}U32 (unsigned 32-bit integer)
Section titled “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); }}// Serializelet val: u32 = 1000000000;let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![0x00, 0xca, 0x9a, 0x3b]);
// Deserializelet val_des = bcs::from_bytes::<u32>(&bytes).unwrap();assert_eq!(val_des, 1000000000);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU32(1000000000);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([0x00, 0xca, 0x9a, 0x3b]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeU32();console.log(val == 1000000000);import ( "github.com/aptos-labs/aptos-go-sdk")
func main() { // Serialize ser := bcs.Serializer{} ser.U32(1000000000) bytes := ser.ToBytes() bytes == []byte{0x00, 0xca, 0x9a, 0x3b}
// Deserialize des := bcs.NewDeserializer(bytes) val := des.U32() val == 1000000000}U64 (unsigned 64-bit integer)
Section titled “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); }}// Serializelet val: u64 = 10000000000000000;let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]);
// Deserializelet val_des = bcs::from_bytes::<u64>(&bytes).unwrap();assert_eq!(val_des, 10000000000000000);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU64(10000000000000000n);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeU64();console.log(val == 10000000000000000n);import ( "github.com/aptos-labs/aptos-go-sdk")
func main() { // Serialize ser := bcs.Serializer{} ser.U64(10000000000000000) bytes := ser.ToBytes() bytes == []byte{0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00}
// Deserialize des := bcs.NewDeserializer(bytes) val := des.U64() val == 10000000000000000}U128 (unsigned 128-bit integer)
Section titled “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); }}// Serializelet val: u128 = 10000000000000000;let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]);
// Deserializelet val_des = bcs::from_bytes::<u128>(&bytes).unwrap();assert_eq!(val_des, 10000000000000000);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU128(10000000000000000n);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeU128();console.log(val == 10000000000000000n);import ( "github.com/aptos-labs/aptos-go-sdk" "math/big")
func main() { // Serialize ser := bcs.Serializer{} val := new(big.Int) val.SetString("10000000000000000", 10) ser.U128(val) bytes := ser.ToBytes() bytes == []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x9c, 0x4f, 0x2c, 0x68, 0x00, 0x00}
// Deserialize des := bcs.NewDeserializer(bytes) val_des := des.U128() val_des.String() == "10000000000000000"}U256 (unsigned 256-bit integer)
Section titled “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); }}// Serializelet val: U256 = U256::from(10000000000000000u64);let bytes: Vec<u8> = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![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]);
// Deserializelet val_des = bcs::from_bytes::<U256>(&bytes).unwrap();assert_eq!(val_des, U256::from(10000000000000000u64));import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU256(10000000000000000n);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([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]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeU256();console.log(val == 10000000000000000n);import ( "github.com/aptos-labs/aptos-go-sdk" "math/big")
func main() { // Serialize ser := bcs.Serializer{} val := new(big.Int) val.SetString("10000000000000000", 10) ser.U256(val) bytes := ser.ToBytes() bytes == []byte{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 des := bcs.NewDeserializer(bytes) val_des := des.U256() val_des.String() == "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// Currently not supported by itself in Rust with serdeimport { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeU32AsUleb128(127);const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([0x7f]));
const ser = new Serializer();ser.serializeU32AsUleb128(128);const bytes2 = ser.toUint8Array();console.log(bytes2 == Uint8Array.from([0x80, 0x01]));
// Deserializeconst des = new Deserializer(bytes2);const val = des.deserializeUleb128AsU32();console.log(val == 128);import ( "github.com/aptos-labs/aptos-go-sdk" "math/big")
func main() { // Serialize ser := bcs.Serializer{} val := new(big.Int) val.SetInt64(127) ser.Uleb128(val) bytes := ser.ToBytes() bytes == []byte{0x7f}
// Deserialize des := bcs.NewDeserializer(bytes) val_des := des.Uleb128() val_des.Int64() == 127}Sequence and FixedSequence
Section titled “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]); }}// Serializelet val = vec![1u8, 2u8, 3u8];let bytes = bcs::to_bytes(&val).unwrap();assert_eq!(bytes, vec![3, 1, 2, 3]);
// Deserializelet val_des = bcs::from_bytes::<Vec<u8>>(&bytes).unwrap();assert_eq!(val_des, vec![1, 2, 3]);import { Serializer, Deserializer } from "@aptos-labs/ts-sdk";
// Serializeconst ser = new Serializer();ser.serializeVector([1, 2, 3], (s, item) => s.serializeU8(item));const bytes = ser.toUint8Array();console.log(bytes == Uint8Array.from([3, 1, 2, 3]));
// Deserializeconst des = new Deserializer(bytes);const val = des.deserializeVector((d) => d.deserializeU8());console.log(val == [1, 2, 3]);import ( "github.com/aptos-labs/aptos-go-sdk")
func main() { // Serialize ser := bcs.Serializer{} ser.SerializeVector([]uint8{1, 2, 3}, func(s *bcs.Serializer, item uint8) { s.U8(item) }) bytes := ser.ToBytes() bytes == []byte{3, 1, 2, 3}
// Deserialize des := bcs.NewDeserializer(bytes) val := des.DeserializeVector(func(d *bcs.Deserializer) uint8 { return d.U8() }) val == []uint8{1, 2, 3}}Complex types
Section titled “Complex types”String
Section titled “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 24let 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
Section titled “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
Section titled “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
Section titled “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]);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)?);