BCS 编码
Binary Canonical Serialization - Sui 的数据序列化标准
本节重点
- 什么是 BCS?为什么需要它?
- 如何编码和解码基础类型?
- 如何处理复杂数据结构?
- 如何在交易中使用 BCS?
- 常见错误和解决方案
什么是 BCS?
BCS (Binary Canonical Serialization) 是一种确定性的二进制序列化格式,专为区块链设计。Sui 使用 BCS 来:
- 📦 序列化交易数据: 将交易转换为字节
- 🔐 签名验证: 确保数据完整性
- 💾 存储优化: 高效的二进制格式
- 🔄 跨语言兼容: 统一的序列化标准
BCS 特点
- ✅ 确定性: 相同输入总是产生相同输出
- ✅ 紧凑: 二进制格式,空间效率高
- ✅ 类型安全: 强类型系统
- ✅ 可扩展: 支持复杂数据结构
安装
bash
npm install @mysten/bcsBCS 也包含在 @mysten/sui 中:
typescript
import { bcs } from '@mysten/sui/bcs';基础类型编码
整数类型
typescript
import { bcs } from '@mysten/bcs';
// u8 (0-255)
const u8Bytes = bcs.u8().serialize(42).toBytes();
console.log('u8:', u8Bytes); // Uint8Array [42]
const u8Value = bcs.u8().parse(new Uint8Array([42]));
console.log('解码 u8:', u8Value); // 42
// u16, u32, u64, u128, u256
const u64Bytes = bcs.u64().serialize(12345n).toBytes();
const u64Value = bcs.u64().parse(u64Bytes);
console.log('u64:', u64Value); // 12345n
// 有符号整数 (i8, i16, i32, i64, i128)
const i64Bytes = bcs.i64().serialize(-12345n).toBytes();
const i64Value = bcs.i64().parse(i64Bytes);
console.log('i64:', i64Value); // -12345n布尔类型
typescript
// bool
const trueByte = bcs.bool().serialize(true).toBytes();
console.log('true:', trueByte); // Uint8Array [1]
const falseByte = bcs.bool().serialize(false).toBytes();
console.log('false:', falseByte); // Uint8Array [0]字符串
typescript
// string (UTF-8 编码)
const strBytes = bcs.string().serialize('Hello, Sui!').toBytes();
const strValue = bcs.string().parse(strBytes);
console.log('字符串:', strValue); // "Hello, Sui!"
// 中文支持
const chineseBytes = bcs.string().serialize('你好,Sui!').toBytes();
const chineseValue = bcs.string().parse(chineseBytes);
console.log('中文:', chineseValue); // "你好,Sui!"地址类型
typescript
// address (32 字节)
const address = '0x0000000000000000000000000000000000000000000000000000000000000001';
const addrBytes = bcs.Address.serialize(address).toBytes();
const addrValue = bcs.Address.parse(addrBytes);
console.log('地址:', addrValue);向量/数组
typescript
// vector<u64>
const vecBytes = bcs
.vector(bcs.u64())
.serialize([1n, 2n, 3n, 4n, 5n])
.toBytes();
const vecValue = bcs.vector(bcs.u64()).parse(vecBytes);
console.log('向量:', vecValue); // [1n, 2n, 3n, 4n, 5n]
// vector<string>
const strVecBytes = bcs
.vector(bcs.string())
.serialize(['hello', 'world'])
.toBytes();
// 嵌套向量 vector<vector<u8>>
const nestedVec = bcs
.vector(bcs.vector(bcs.u8()))
.serialize([
[1, 2, 3],
[4, 5, 6],
])
.toBytes();Option 类型
typescript
// option<u64>
const someBytes = bcs.option(bcs.u64()).serialize(100n).toBytes();
const someValue = bcs.option(bcs.u64()).parse(someBytes);
console.log('Some:', someValue); // 100n
const noneBytes = bcs.option(bcs.u64()).serialize(null).toBytes();
const noneValue = bcs.option(bcs.u64()).parse(noneBytes);
console.log('None:', noneValue); // null复杂数据结构
结构体(Struct)
typescript
// 定义结构体
const Person = bcs.struct('Person', {
name: bcs.string(),
age: bcs.u8(),
address: bcs.Address,
is_active: bcs.bool(),
});
// 编码
const personData = {
name: 'Alice',
age: 30,
address: '0x1',
is_active: true,
};
const personBytes = Person.serialize(personData).toBytes();
console.log('Person 字节:', personBytes);
// 解码
const personValue = Person.parse(personBytes);
console.log('Person:', personValue);嵌套结构体
typescript
// 定义嵌套结构体
const Address = bcs.struct('Address', {
street: bcs.string(),
city: bcs.string(),
zip: bcs.u32(),
});
const User = bcs.struct('User', {
id: bcs.u64(),
name: bcs.string(),
address: Address,
tags: bcs.vector(bcs.string()),
});
// 编码
const userData = {
id: 1n,
name: 'Bob',
address: {
street: '123 Main St',
city: 'New York',
zip: 10001,
},
tags: ['developer', 'blockchain'],
};
const userBytes = User.serialize(userData).toBytes();
const userValue = User.parse(userBytes);
console.log('User:', userValue);枚举(Enum)
typescript
// 定义枚举
const Color = bcs.enum('Color', {
Red: null,
Green: null,
Blue: null,
RGB: bcs.struct('RGB', {
r: bcs.u8(),
g: bcs.u8(),
b: bcs.u8(),
}),
});
// 简单枚举值
const redBytes = Color.serialize({ Red: null }).toBytes();
const redValue = Color.parse(redBytes);
console.log('颜色:', redValue); // { Red: null }
// 带数据的枚举值
const rgbBytes = Color.serialize({
RGB: { r: 255, g: 128, b: 0 },
}).toBytes();
const rgbValue = Color.parse(rgbBytes);
console.log('RGB:', rgbValue); // { RGB: { r: 255, g: 128, b: 0 } }泛型类型
typescript
// 定义泛型容器
function Box<T>(innerType: any) {
return bcs.struct('Box', {
value: innerType,
});
}
// 使用泛型
const NumberBox = Box(bcs.u64());
const numberBoxBytes = NumberBox.serialize({ value: 42n }).toBytes();
const StringBox = Box(bcs.string());
const stringBoxBytes = StringBox.serialize({ value: 'hello' }).toBytes();
// 泛型向量
function Wrapper<T>(innerType: any) {
return bcs.struct('Wrapper', {
items: bcs.vector(innerType),
count: bcs.u32(),
});
}
const NumberWrapper = Wrapper(bcs.u64());
const wrapperData = {
items: [1n, 2n, 3n],
count: 3,
};
const wrapperBytes = NumberWrapper.serialize(wrapperData).toBytes();在交易中使用 BCS
编码交易参数
typescript
import { Transaction } from '@mysten/sui/transactions';
import { bcs } from '@mysten/sui/bcs';
// 定义复杂参数类型
const NFTMetadata = bcs.struct('NFTMetadata', {
name: bcs.string(),
description: bcs.string(),
image_url: bcs.string(),
attributes: bcs.vector(
bcs.struct('Attribute', {
key: bcs.string(),
value: bcs.string(),
})
),
});
// 准备数据
const metadata = {
name: 'My NFT',
description: 'A unique digital asset',
image_url: 'https://example.com/nft.png',
attributes: [
{ key: 'rarity', value: 'legendary' },
{ key: 'power', value: '9000' },
],
};
// 编码为 BCS
const metadataBytes = NFTMetadata.serialize(metadata).toBytes();
// 在交易中使用
const tx = new Transaction();
tx.moveCall({
target: '0xpackage::nft::mint_with_metadata',
arguments: [
tx.pure(metadataBytes), // 直接传递 BCS 编码的数据
],
});解码交易返回值
typescript
// 假设合约返回 BCS 编码的数据
async function decodeTransactionResult(txBytes: Uint8Array) {
const Result = bcs.struct('MintResult', {
nft_id: bcs.Address,
owner: bcs.Address,
timestamp: bcs.u64(),
});
const result = Result.parse(txBytes);
console.log('NFT ID:', result.nft_id);
console.log('所有者:', result.owner);
console.log('时间戳:', result.timestamp);
return result;
}编码动态字段
typescript
// 动态字段的键值对
const DynamicFieldKey = bcs.struct('Key', {
id: bcs.u64(),
category: bcs.string(),
});
const DynamicFieldValue = bcs.struct('Value', {
data: bcs.string(),
timestamp: bcs.u64(),
});
// 编码键
const keyBytes = DynamicFieldKey.serialize({
id: 1n,
category: 'metadata',
}).toBytes();
// 编码值
const valueBytes = DynamicFieldValue.serialize({
data: 'some data',
timestamp: BigInt(Date.now()),
}).toBytes();
// 在交易中添加动态字段
tx.moveCall({
target: '0x2::dynamic_field::add',
typeArguments: ['KeyType', 'ValueType'],
arguments: [
tx.object(parentObjectId),
tx.pure(keyBytes),
tx.pure(valueBytes),
],
});链上数据解析
解析对象内容
typescript
import { SuiClient } from '@mysten/sui/client';
// 定义对象结构
const GameAsset = bcs.struct('GameAsset', {
id: bcs.Address,
owner: bcs.Address,
level: bcs.u16(),
experience: bcs.u64(),
attributes: bcs.vector(bcs.u32()),
});
async function parseGameAsset(client: SuiClient, objectId: string) {
// 获取对象
const object = await client.getObject({
id: objectId,
options: {
showBcs: true,
showContent: true,
},
});
if (object.data?.bcs) {
// 解析 BCS 数据
const bcsBytes = Buffer.from(object.data.bcs.bcsBytes, 'base64');
const asset = GameAsset.parse(new Uint8Array(bcsBytes));
console.log('游戏资产:', asset);
return asset;
}
}解析事件数据
typescript
// 定义事件结构
const NFTMintedEvent = bcs.struct('NFTMintedEvent', {
nft_id: bcs.Address,
minter: bcs.Address,
name: bcs.string(),
timestamp: bcs.u64(),
});
async function parseNFTMintedEvents(
client: SuiClient,
packageId: string
) {
const events = await client.queryEvents({
query: {
MoveEventType: `${packageId}::nft::NFTMinted`,
},
});
for (const event of events.data) {
// 解析 BCS 编码的事件数据
if (event.bcs) {
const bcsBytes = Buffer.from(event.bcs, 'base64');
const eventData = NFTMintedEvent.parse(new Uint8Array(bcsBytes));
console.log('NFT 铸造事件:', eventData);
}
}
}自定义序列化器
创建自定义类型
typescript
// 自定义序列化器
const CustomDate = {
name: 'CustomDate',
serialize(value: Date, writer: any) {
// 将 Date 序列化为 u64 时间戳
return bcs.u64().serialize(BigInt(value.getTime()), writer);
},
parse(reader: any): Date {
const timestamp = bcs.u64().parse(reader);
return new Date(Number(timestamp));
},
};
// 使用自定义类型
const Event = bcs.struct('Event', {
name: bcs.string(),
date: CustomDate,
participants: bcs.vector(bcs.string()),
});
const eventData = {
name: 'Sui Hackathon',
date: new Date(),
participants: ['Alice', 'Bob', 'Charlie'],
};
const eventBytes = Event.serialize(eventData).toBytes();
const parsedEvent = Event.parse(eventBytes);
console.log('事件:', parsedEvent);UUID 类型
typescript
// 自定义 UUID 类型(16 字节)
const UUID = {
name: 'UUID',
serialize(value: string, writer: any) {
// 将 UUID 字符串转换为 16 字节
const bytes = new Uint8Array(16);
const hex = value.replace(/-/g, '');
for (let i = 0; i < 16; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
writer.writeBytes(bytes);
return writer;
},
parse(reader: any): string {
const bytes = reader.readBytes(16);
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return `${hex.substr(0, 8)}-${hex.substr(8, 4)}-${hex.substr(
12,
4
)}-${hex.substr(16, 4)}-${hex.substr(20, 12)}`;
},
};工具函数
Base64 转换
typescript
// BCS 字节 <-> Base64
function bcsToBase64(bytes: Uint8Array): string {
return Buffer.from(bytes).toString('base64');
}
function base64ToBcs(base64: string): Uint8Array {
return new Uint8Array(Buffer.from(base64, 'base64'));
}
// 使用
const data = { value: 42n };
const bytes = bcs.u64().serialize(data.value).toBytes();
const base64 = bcsToBase64(bytes);
console.log('Base64:', base64);
const decoded = bcs.u64().parse(base64ToBcs(base64));
console.log('解码:', decoded);Hex 转换
typescript
// BCS 字节 <-> Hex
function bcsToHex(bytes: Uint8Array): string {
return '0x' + Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
function hexToBcs(hex: string): Uint8Array {
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
const bytes = new Uint8Array(cleanHex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(cleanHex.substr(i * 2, 2), 16);
}
return bytes;
}大小计算
typescript
function calculateBcsSize<T>(schema: any, value: T): number {
const bytes = schema.serialize(value).toBytes();
return bytes.length;
}
// 使用
const person = {
name: 'Alice',
age: 30,
address: '0x1',
};
const size = calculateBcsSize(Person, person);
console.log(`Person 占用 ${size} 字节`);调试和验证
比较序列化结果
typescript
function compareBcsSerialization<T>(
schema: any,
value1: T,
value2: T
): boolean {
const bytes1 = schema.serialize(value1).toBytes();
const bytes2 = schema.serialize(value2).toBytes();
if (bytes1.length !== bytes2.length) return false;
for (let i = 0; i < bytes1.length; i++) {
if (bytes1[i] !== bytes2[i]) return false;
}
return true;
}
// 使用
const data1 = { value: 42n };
const data2 = { value: 42n };
const data3 = { value: 43n };
console.log('相同数据:', compareBcsSerialization(bcs.u64(), data1.value, data2.value)); // true
console.log('不同数据:', compareBcsSerialization(bcs.u64(), data1.value, data3.value)); // false可视化 BCS 数据
typescript
function visualizeBcs(bytes: Uint8Array): string {
const hex = Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join(' ');
const ascii = Array.from(bytes)
.map((b) => (b >= 32 && b <= 126 ? String.fromCharCode(b) : '.'))
.join('');
return `Hex: ${hex}\nASCII: ${ascii}`;
}
// 使用
const strBytes = bcs.string().serialize('Hello').toBytes();
console.log(visualizeBcs(strBytes));最佳实践
1. 版本控制
typescript
// 为数据结构添加版本号
const VersionedData = bcs.struct('VersionedData', {
version: bcs.u8(),
data: bcs.vector(bcs.u8()), // 原始数据
});
// V1 结构
const DataV1 = bcs.struct('DataV1', {
name: bcs.string(),
value: bcs.u64(),
});
// V2 结构(新增字段)
const DataV2 = bcs.struct('DataV2', {
name: bcs.string(),
value: bcs.u64(),
metadata: bcs.option(bcs.string()), // 新字段
});
// 解析时根据版本选择
function parseVersionedData(bytes: Uint8Array) {
const versionedData = VersionedData.parse(bytes);
switch (versionedData.version) {
case 1:
return DataV1.parse(new Uint8Array(versionedData.data));
case 2:
return DataV2.parse(new Uint8Array(versionedData.data));
default:
throw new Error('未知版本');
}
}2. 错误处理
typescript
function safeParse<T>(schema: any, bytes: Uint8Array): T | null {
try {
return schema.parse(bytes);
} catch (error) {
console.error('BCS 解析失败:', error);
return null;
}
}
// 使用
const result = safeParse(Person, personBytes);
if (result) {
console.log('解析成功:', result);
} else {
console.log('解析失败');
}3. 类型验证
typescript
function validateBcsData<T>(
schema: any,
value: T,
validator: (v: T) => boolean
): boolean {
if (!validator(value)) {
return false;
}
try {
const bytes = schema.serialize(value).toBytes();
const parsed = schema.parse(bytes);
return validator(parsed);
} catch {
return false;
}
}
// 使用
const isValidPerson = validateBcsData(
Person,
personData,
(p) => p.age >= 0 && p.age <= 150 && p.name.length > 0
);常见错误
错误 1: 类型不匹配
typescript
// ❌ 错误:使用 number 而不是 bigint
try {
bcs.u64().serialize(12345); // 错误!
} catch (error) {
console.error('类型错误');
}
// ✅ 正确:使用 bigint
bcs.u64().serialize(12345n);错误 2: 向量长度编码
typescript
// BCS 会自动处理向量长度
// ❌ 不需要手动添加长度
const wrongBytes = new Uint8Array([
3, // 长度
1,
2,
3, // 元素
]);
// ✅ 正确:让 BCS 处理
const correctBytes = bcs.vector(bcs.u8()).serialize([1, 2, 3]).toBytes();错误 3: 字节序
typescript
// BCS 使用小端序(Little Endian)
const value = 0x12345678;
// ❌ 错误:大端序
const wrongBytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]);
// ✅ 正确:使用 BCS
const correctBytes = bcs.u32().serialize(value).toBytes();
// 结果: [0x78, 0x56, 0x34, 0x12]