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 serde
import { 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)?);