Sui.

帖子

分享您的知识。

赏金+10

Peera Admin.
May 29, 2025
专家问答

当 Move 结构有命名字段时,为什么 BCS 需要精确的字段顺序才能进行反序列化?

当 Move 结构有命名字段时,为什么 BCS 需要精确的字段顺序才能进行反序列化? 我一直在深入研究 Move 中的 BCS 编码/解码,特别是跨链通信和链下数据处理. 在浏览 Sui Move 文档中的示例时,我遇到了一些似乎违反直觉的行为,我正在尝试理解底层的设计决策.

根据BCS规范,“BCS中没有结构(因为没有类型);该结构只是定义了字段序列化的顺序. ”这意味着在反序列化时,我们必须按照与peel_*结构字段定义完全相同的顺序使用函数.

我的具体问题:

  1. 设计理由:当 Move 结构具有命名字段时,为什么 BCS 需要精确的字段顺序匹配?像 JSON 或其他自描述格式一样,将字段名称与值一起序列化不是更强大吗?
  2. 泛型类型交互:文档提到 “包含泛型类型字段的类型最多可以解析到第一个泛型类型字段. ”考虑一下这个结构:

struct ComplexObject<T, U> has drop, copy {
    id: ID,
    owner: address,
    metadata: Metadata,
    generic_data: T,
    more_metadata: String,
    another_generic: U
}

部分反序列化在这里到底是如何工作的?我可以反序列化到more_metadata并忽略两个泛型字段,还是第一个泛型字段(generic_data)完全阻止了进一步的反序列化? 4. 跨语言一致性:使用 @mysten /bcs JavaScript 库序列化将由 Move 合约使用的数据时,在以下情况下会发生什么:

-我不小心重新排序了 JavaScript 对象中的字段? -Move 结构定义会在合约升级中更改字段顺序? -我有带有自己的泛型参数的嵌套结构吗?

  1. 实际启示:在生产系统中,团队如何处理 BCS 架构演变?您是否对BCS架构进行了版本控制,还是期望结构字段顺序在部署后不可变?
  • Sui
  • Move
5
2
分享
评论
.

答案

2
0xduckmove.
May 31 2025, 15:14

Move 中的二进制规范序列化 (BCS)

以下是我对你问题的答案

为什么字段顺序必须完全匹配

假设你有一个这样定义的 Move 结构:

module example::ordering {
    struct MyStruct has copy, drop, store {
        a: u64,
        b: bool,
    }

    public fun to_bytes_example(): vector<u8> {
        let instance = MyStruct { a: 42, b: true };
        bcs::to_bytes(&instance)
    }

    public fun from_bytes_example(bytes: vector<u8>): MyStruct {
        bcs::from_bytes(&bytes)
    }
}

你们都知道,序列化 myStruct {a: 42,b: true} 会为 u64 42(以小端序列表示)生成八个字节,然后为 true(例如 0x01)生成一个字节. 现在,如果你将结构更改为:

public struct MyStruct has copy, drop, store {
    b: bool,
    a: u64,
}

序列化的字节将以布尔值的一个字节开头,然后是整数的八字节. 然而,Move 中的反序列化器仍会尝试将前八个字节读取为 u64,将下一个字节读取为 bool.

由于 BCS 从未记录过 “首先是布尔值”,因此 Move 最终会将剩余的数据解释为错误的数字,或者完全失败了. 简而言之,交换字段会破坏一切.


泛型如何影响部分反序列化

如果我只关心后面的字段,我可以跳过它们吗

答案是:除非你确切知道如何解析这些泛型. 想象一下这个 Move 结构:

module example::generics {
    use std::string::String;

    public struct ComplexObject<T, U> has copy, drop, store {
        id: ID,
        owner: address,
        metadata: vector<u8>,
        generic_data: T,
        more_metadata: String,
        another_generic: U,
    }

    public fun partial_deserialize(bytes: vector<u8>): String {
        let obj: ComplexObject<Signer, Signer> = bcs::from_bytes(&bytes);
        obj.more_metadata
    }
}


当你调用时bcs::from_bytes::<ComplexObject<Signer, Signer>>(&bytes),解析器知道那T是 Signer,它在 Sui 中是一个固定大小的 32 字节地址. U它读取 32 字节,计算出这是你的通用数据,然后它继续读取 “more_metadata:字符串”(以长度为前缀),最后读取 another_generic_generic:.

但是,如果你没有指定 T,或者 T 是像矢量一样的东西,其长度是事先未知的,那么就无法找出 more_metadata 从哪里开始.

BCS 没有针对任意字段的内置长度标记;您必须完全解码 generic_data,然后才能考虑 more_metadata. 换句话说,除非你已经知道它的确切类型���否则部分反序列化会在第一个泛型字段处停止.


保持 JavaScript 和 Move 与 @mysten /bcs 同步

使用在 JavaScript 中进行序列化@mysten/bcs时,必须使用与 Move 完全相同的字段顺序注册结构. 否则,Move 的 bcs:: from_bytes 会误解字节. 看起来像这样:

import { Bcs, u64, bool } from "@mysten/bcs";

const bcs = new Bcs();

// Match Move’s struct: struct MyStruct { a: u64, b: bool }
bcs.registerStructType("MyStruct", {
  a: u64(),
  b: bool(),
});

// If you accidentally do this instead:
// bcs.registerStructType("MyStruct", {
//   b: bool(),
//   a: u64(),
// });

const instance = { a: 42n, b: true };
const bytes = bcs.ser("MyStruct", instance);

由于 Move 需要 [8 字节 u64] [1 字节的布尔值],因此,如果 JavaScript 发出 [1 字节的布尔值] [8 字节的 u64],Move 的解析器会尝试读取 u64 的八字节,但会看到垃圾信息. 这种不匹配会导致解码错误或数据损坏.

现在,假设你稍后尝试升级那个 Move 结构:

module upgrade::v1 {
    struct MyStruct has copy, drop, store {
        a: u64,
        b: bool,
    }
}

module upgrade::v2 {
    struct MyStruct has copy, drop, store {
        b: bool,
        a: u64,
    }
}

部署升级:: v1 后,JS 前端开始序列化为 [u64] [bool]. 如果你推送交换这些字段的升级,Move 中的新调用将需要 [bool] [u64]. 突然之间,旧字节在 v2 下简直是胡说八道,任何新的序列化都会破坏旧客户端.

这正是 Move 的升级政策禁止在 “兼容” 升级中更改结构字段顺序的原因. 如果您绝对必须更改布局,则可以发布新结构(例如 myStructv2)并逐步迁移数据.

嵌套结构或泛型结构也是如此:

module nest::demo {
    struct Inner has copy, drop, store {
        x: u64,
        y: vector<u8>,
    }

    struct Outer<T> has copy, drop, store {
        id: u64,
        payload: T,
        note: vector<u8>,
    }
}

在 JavaScript 方面,你会做这样的事情:

import { Bcs, u64, vector, Struct } from "@mysten/bcs";

const bcs = new Bcs();

// Register Inner exactly as Move defined it
bcs.registerStructType("Inner", {
  x: u64(),
  y: vector(u8()),
});

// Register Outer<Inner>, matching Move’s field order
bcs.registerStructType("Outer<Inner>", {
  id: u64(),
  payload: new Struct("Inner"),
  note: vector(u8()),
});

当 JS 序列化 Outer 时,它会写入 8 字节的 id;然后它递归到内部,为 x 写入 8 个字节和一个以长度为前缀的 y;然后它写入注释. Move 的 bcs:: from_Bytes 执行相同的步骤.

如果该嵌套架构的任何部分不匹配,则使用 vector () 而不是 vector (),所有内容都会中断,因为长度前缀和元素大小会有所不同.

关于最后一个问题

在基于 Move 的生产系统中,一旦你在链上发布了一个结构(尤其是在客户开始读取和写入 BCS 数据之后),该结构的布局实际上是不可改变的. Aptos 和 Sui 都要求兼容的升级必须保持现有的结构字段布局不变. 如果您尝试重新排序、插入或删除已发布结构中的字段,则升级会被视为不兼容,通常会受到监管或发布工具的阻止.

当你确实需要更改架构时,大多数团队会选择以下路径之一:

首先,你可以在结构的前面嵌入一个小版本:u8 字段,这样读者就可以看到版本号并相应地分支:

public struct DataV1 has copy, drop, store {
    version: u8,       // 1
    field_a: u64,
    field_b: bool,
}

public struct DataV2 has copy, drop, store {
    version: u8,       // 2
    field_a: u64,
    field_b: bool,
    field_c: vector<u8>, // new field in version 2
}

Here, any consumer can read the first byte; if it’s 1, they parse with DataV1. If it’s 2, they parse with DataV2. The downside is old clients that only know about DataV1 may choke on version 2 unless you write special fallback logic.

Another approach is to publish a new struct entirely, such as MyStructV2, leaving MyStructV1 unchanged. You migrate on-chain data via a transaction that reads V1 and writes the new V2. All old code that still understands only MyStructV1 continues to work until you decide to deprecate it.

On Sui, you also have dynamic fields for more flexibility. Instead of tacking on a new field to an existing struct, you store additional data as a separate child object keyed by the original object’s ID. Because the base struct’s BCS layout never changes, clients reading it still see the original fields. Then, whenever they need to read or write the extra data, they know to query the dynamic child. Dynamic fields are a powerful way to extend object schemas without ever touching the original struct layout.

Some teams reserve padding fields:


struct MyStruct has copy, drop, store {
    field_a: u64,
    reserved_1: u64, // unused for now
    field_b: bool,
}

``

















2
评论
.
Owen.
May 31 2025, 04:13

1. 为什么 BCS 需要精确的现场顺序?

**因为 BCS 仅序列化原始值,而不是字段名称或类型元数据. **

移动结构有命名字段,但是BCS 将它们视为有序元组. 字段名称是编译时信息,不包含在序列化输出中.

示例:

struct MyStruct {
    a: u64,
    b: bool,
}

-序列化为:[u64 bytes] + [bool bytes] -反序列化器必须读取 u64``bool顺序很重要

这使得 BCS: -快速而紧凑 -不能自我描述或架构不灵活


2. 部分反序列化如何处理泛型类型?

鉴于:

struct ComplexObject<T, U> {
    id: ID,
    owner: address,
    metadata: Metadata,
    generic_data: T,      // <-- generic
    more_metadata: String,
    another_generic: U,   // <-- generic
}

规则:

generic_data**您最多可以反序列化第一个通用字段 (). T之后,除非你知道如何解码,否则解析会停止. **

所以: T``more_metadata-如果你不知道如何解析another_generic,你无法安全地到达或. -peel_*如果要部分访问,则必须谨慎使用低级函数.

在没有完整类型知识的情况下,只能安全地解析非泛型前缀.


3. @mysten/bcs与 JavaScript () 的跨语言一致性

-现场订单必须完全匹配,就像在 Move 中一样. -重新排序 JS 对象中的字段 →反序列化错误或误解数据 -部署后更改移动结构字段顺序 →破坏兼容性 -带有泛型的嵌套结构 → 仅在所有嵌套架构都正确注册后才有效


4. 团队如何处理生产中的 BCS 架构演变?

常见策略: -架构版本控制:将版本号附加到序列化数据 -不可变结构:部署后,切勿重新排序或删除字段 -避免在部署后重新排序、重命名或删除字段 -使用保留/填充字段以允许将来扩展 -引入新结构而不是修改旧结构 -首选跨语言结构中的具体类型

0
评论
.

你知道答案吗?

请登录并分享。

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

306帖子450答案
Sui.X.Peera.

赚取你的 1000 Sui 份额

获取声誉积分,并因帮助 Sui 社区成长而获得奖励。

奖励活动六月
我们使用 cookie 确保您在我们的网站上获得最佳体验。
更多信息