Aptos Core 序列化格式守护者:generate-format 与 serde-reflection 的类型兼容性追踪实战指南
2026/9/17 14:02:12 网站建设 项目流程

Aptos Core 序列化格式守护者:generate-format 与 serde-reflection 的类型兼容性追踪实战指南

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

本指南围绕 Aptos 仓库中的testsuite/generate-format模块展开,讲解 Aptos 如何基于serde-reflection构建"核心类型检查器",持续追踪 BCS 序列化格式的演进。通过本文,你将掌握在引入新的 struct、enum 变体或加密库类型时,如何同步修改api.rs/aptos.rs/consensus.rs及其对应的 staged YAML 文件,并学会用cargo run -p generate-format -- --corpus <X> --record重新生成格式快照、用测试守护格式不被悄然破坏的完整工作流。

为什么 Aptos 需要一套"类型格式快照"机制

区块链网络中,节点之间、客户端与节点之间需要通过 BCS(Binary Canonical Serialization)交换交易、区块、共识消息等核心数据结构。任何序列化格式的意外变化——比如新增字段、调整枚举变体顺序、把Vec<u8>换成定长数组——都会导致新旧节点无法互相解析数据,轻则升级不兼容,重则引发共识分叉。

generate-format正是为此而生的守护机制。根据 testsuite/generate-format/README.md 的定义,它托管着Aptos 核心类型检查器,利用serde-reflection在每次构建与测试时"反射"出所有关键 Rust 类型的 Serde 序列化格式,并以 YAML 文件的形式固化在仓库中。一旦代码中的类型格式与 YAML 快照不一致,CI 测试便会失败,从而在合并前暴露问题。

从源码结构看,该模块的定位非常清晰:Cargo.toml 中的包描述即为 "Aptos core type checker to ensure compatibility",其依赖覆盖了aptos-api-typesaptos-consensusaptos-cryptoaptos-typesmove-core-typesbcs等核心 crate,意味着它反射的是整个链上数据面的关键类型。

模块架构:五个 Corpus 与三份核心快照

generate-format将待追踪的 Rust 类型划分为多个Corpus(语料),见 src/lib.rs 中的Corpus枚举:

Corpus覆盖范围输出文件(相对仓库根目录)
APIRest API 相关类型(aptos_api_typestestsuite/generate-format/tests/staged/api.yaml
Aptos交易、认证器、写集等核心链上类型testsuite/generate-format/tests/staged/aptos.yaml
Consensus共识消息与区块类型(aptos_consensus_typestestsuite/generate-format/tests/staged/consensus.yaml
Network网络握手与消息类型(aptos_networktestsuite/generate-format/tests/staged/network.yaml
MoveABIMove ABI 相关类型testsuite/generate-format/tests/staged/move_abi.yaml

每个 Corpus 的get_registry()方法负责完成一次"格式采集",核心流程在三份最关键的采集器——src/api.rs、src/aptos.rs、src/consensus.rs——中是一致的,可归纳为两步:

  1. 记录采样值(samples):对具有自定义反序列化器或特殊格式语义的类型,先用真实可用的值调用tracer.trace_value,让Tracer依据具体实例推断格式,例如:
    • StdRng::from_seed([0; 32])生成确定性随机密钥,记录Ed25519PublicKeysecp256k1_ecdsabls12381slh_dsa_sha2_128s等各套加密算法的公私钥与签名样本;
    • event::EventKey::random()write_set::WriteOp::legacy_deletion()等特殊类型的样本。
  2. 逐类型追踪(trace_type):以"主入口类型 + 每个枚举单独追踪"的方式批量反射类型图,例如transaction::TransactionTransactionAuthenticatorAnyPublicKeyAnySignatureAssertionSignatureValidatorTransactionBlockMetadataExt等,最终通过tracer.registry()产出Registry

采集完成后,由命令行工具 src/compute.rs 将Registry序列化为 YAML。该工具支持两个参数:

# 仅打印指定 corpus 的格式(默认 Aptos,不写盘) cargo run -p generate-format -- --corpus API # 重新生成并写回 staged YAML 文件 cargo run -p generate-format -- --corpus API --record

--record模式会调用对应output_file()返回的相对路径(如tests/staged/aptos.yaml),在仓库根目录拼接成testsuite/generate-format/tests/staged/aptos.yaml后落盘;不带--record时则打印到标准输出,便于快速预览格式变化。

变更清单:新类型必须同步修改六处文件

当你在代码中引入新的 struct、enum、variant 或任何参与序列化的类型时,testsuite/generate-format/README.md 明确要求同步修改以下两组文件,缺一不可:

  • 采集器源码testsuite/generate-format/src/api.rstestsuite/generate-format/src/aptos.rstestsuite/generate-format/src/consensus.rs
  • 格式快照testsuite/generate-format/tests/staged/api.yamltestsuite/generate-format/tests/staged/aptos.yamltestsuite/generate-format/tests/staged/consensus.yaml

前者让新类型进入追踪范围,后者把追踪结果固化下来供测试比对。下面以文档中的真实案例——新增secp256r1_ecdsa加密库(含PublicKeySignaturePrivateKey三个类型)以及支撑 WebAuthn 交易的PartialAuthenticatorAssertionResponseAssertionSignature结构——分两步完整演练。

案例一:新增加密库类型的四步改动

第一步:在三个采集器中添加 tracer 追踪

api.rsaptos.rsconsensus.rs各自的trace_crypto_values函数中,添加对secp256r1_ecdsa公私钥与签名的追踪调用:

fn trace_crypto_values(tracer: &mut Tracer, samples: &mut Samples) -> Result<()> { ... // Add tracing for secp256r1_ecdsa keys and sigs tracer.trace_value(samples, &secp256r1_ecdsa_private_key)?; tracer.trace_value(samples, &secp256r1_ecdsa_public_key)?; tracer.trace_value(samples, &secp256r1_ecdsa_signature)?; Ok(()) }

这三个文件的实际实现中,样本均通过StdRng::from_seed([0; 32])确定性生成,例如 src/aptos.rs 中的secp256r1_ecdsa::PrivateKey::generate(&mut rng)后取公钥并签名。确定性种子保证了每次运行生成的样本一致,YAML 快照可稳定比对。

第二步:按需使用 key_name 宏

如果你希望 YAML 中记录的类型名与 Rust struct 名不同,可以使用key_name宏显式指定:

#[key_name("Secp256r1EcdsaPrivateKey")] pub struct PublicKey {...}

该机制让 YAML 中的键名(如Secp256r1EcdsaPrivateKey)与代码中的类型名(PublicKey)解耦,便于跨语言 SDK 或规范文档使用统一命名。

第三步:在三个 YAML 快照中添加对应条目

api.yamlaptos.yamlconsensus.yaml中,为三个新类型声明格式。由于这三个类型本质上就是字节串的 newtype 包装,其格式为:

... Secp256r1EcdsaPrivateKey: NEWTYPESTRUCT: BYTES Secp256r1EcdsaPublicKey: NEWTYPESTRUCT: BYTES Secp256r1EcdsaSignature: NEWTYPESTRUCT: BYTES ...

对照仓库中的实际快照 testsuite/generate-format/tests/staged/aptos.yaml,这三个条目已真实存在,且与文档示例完全一致——这也是后文"测试守护"能够成立的根基。

第四步:在枚举类型中挂接新变体

Secp256r1Ecdsa还被添加为AnyPublicKey枚举的新变体,因此必须更新AnyPublicKey的格式描述。枚举的每个变体都有数字序号,序号决定了 BCS 编码中的判别值,必须保持稳定且连续:

AnyPublicKey: ENUM: ... 2: Secp256r1Ecdsa: STRUCT: - public_key: TYPENAME: Secp256r1EcdsaPublicKey

实际快照 testsuite/generate-format/tests/staged/aptos.yaml 显示,AnyPublicKey的完整变体序列为:0: Ed255191: Secp256k1Ecdsa2: Secp256r1Ecdsa3: Keyless4: FederatedKeyless5: SlhDsa_Sha2_128s。注意:新增变体只能追加在已有序号之后,绝不能修改或重排已有序号,否则会破坏 BCS 向后兼容。

案例二:WebAuthn 交易类型的 struct/enum 变更

为支持 WebAuthn 签名,AnySignature枚举新增了WebAuthn变体,它承载一个PartialAuthenticatorAssertionResponse结构体;该结构体内部又引用AssertionSignature枚举。对应的 YAML 格式为:

AnySignature: ENUM: ... 2: WebAuthn: STRUCT: - signature: TYPENAME: PartialAuthenticatorAssertionResponse PartialAuthenticatorAssertionResponse: STRUCT: - signature: TYPENAME: AssertionSignature - authenticator_data: BYTES - client_data_json: BYTES AssertionSignature: ENUM: 0: Secp256r1Ecdsa: STRUCT: - signature: TYPENAME: Secp256r1EcdsaSignature

仓库快照 testsuite/generate-format/tests/staged/aptos.yaml 与 第643-648行 印证了这套结构:AnySignatureWebAuthn变体序号为2(排在0: Ed255191: Secp256k1Ecdsa之后),PartialAuthenticatorAssertionResponsesignatureauthenticator_dataclient_data_json三字段组成。

关于这三个文件的采集器,README 特别强调:由于api.rs/aptos.rs/consensus.rs已经追踪了AnyPublicKeyAnySignature结构,此处无需为新增变体添加额外 tracer——trace_type会递归展开枚举的每个变体。真正需要补的是独立的枚举类型AssertionSignature,它在三个采集器的get_registry()中通过以下方式单独追踪:

fn get_registry(){ ... tracer.trace_type::<transaction::webauthn::AssertionSignature>(&samples)?; ... }

这一点同样有源码佐证:src/api.rs、src/aptos.rs、src/consensus.rs 中都存在这行调用。同时,为了让Vec<u8>字段在格式中呈现为BYTES而非变长SEQ,结构体需要加serde_bytes宏:

/// `PartialAuthenticatorAssertionResponse` includes a subset of the fields returned from /// an `AuthenticatorAssertionResponse` /// /// See <https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse> #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct PartialAuthenticatorAssertionResponse { /// This attribute contains the raw signature returned from the authenticator. /// NOTE: Many signatures returned from WebAuthn assertions are not raw signatures. /// As an example, secp256r1_ecdsa signatures are encoded as an [ASN.1 DER Ecdsa-Sig_value](https://www.w3.org/TR/webauthn-3/#sctn-signature-attestation-types) /// If the signature is encoded, the client is expected to convert the encoded signature /// into a raw signature before including it in the transaction signature: AssertionSignature, /// This attribute contains the authenticator data returned by the authenticator. /// See `AuthenticatorData`. #[serde(with = "serde_bytes")] authenticator_data: Vec<u8>, /// This attribute contains the JSON byte serialization of `CollectedClientData` passed to the /// authenticator by the client in order to generate this credential. The exact JSON serialization /// MUST be preserved, as the hash of the serialized client data has been computed over it. #[serde(with = "serde_bytes")] client_data_json: Vec<u8>, }

从源码注释可以看到两个关键设计细节:其一,signature字段虽由 WebAuthn 认证器返回,但诸如 secp256r1_ecdsa 的签名常以 ASN.1 DER 编码形式出现,客户端需在纳入交易前转换为原始签名;其二,client_data_json的 JSON 字节序列化必须原样保留,因为客户端数据的哈希正是基于该序列化计算的。

为什么要用 serde_bytes:BCS 格式 Linter 的硬性约束

Vec<u8>必须加serde_bytes并非风格偏好,而是 BCS 兼容性的强制要求。模块内置的格式检查器 src/linter.rs 中的lint_bcs_format会遍历格式树并执行如下规则:

  • 禁止浮点与字符类型F32F64Char一律报错,因为 BCS 不支持这些类型;
  • 禁止裸Vec<u8>:若检测到Seq(U8)(即未包装的Vec<u8>),会直接提示 "Please use#[serde(with = "serde_bytes")onVec<u8>objects."——这正是文档案例中给两个字节数组字段添加宏的原因;
  • 禁止空容器:零大小的容器(UnitStruct、空TupleStruct、空Struct)、空Seq、空键值Map均被拒绝,避免序列化语义歧义。

测试守护:格式变更如何被 CI 拦截

真正的"守护"落在集成测试 tests/detect_format_change.rs 中,其核心测试analyze_serde_formats对所有Corpus::value_variants()执行三重校验:

  1. 与磁盘快照比对:实时计算当前代码的Registry,与 staged YAML 反序列化出的Registry逐类型比对。若已有类型被删除或格式不匹配、或有新类型未记录,断言失败并给出提示;
  2. Linter 校验:对每个 corpus 中的每个格式定义运行lint_bcs_format,确保符合上述 BCS 最佳实践;
  3. 跨 corpus 一致性:同一类型若出现在多个 corpus 中(如AnyPublicKey同时出现在 API、Aptos、Consensus 三个快照里),其格式定义必须完全一致,防止三个采集器各自漂移。

失败时的报错信息会直接给出修复指引——运行以下命令刷新记录:

cargo run -p generate-format -- --corpus <CORPUS> --record

并提示开发者"仔细核查记录文件的改动,并考虑将 PR 标记为breaking"。同时,测试文件顶部注释也重申了同样的操作流程(tests/detect_format_change.rs)。测试还自带一个自检用例test_we_can_detect_changes_in_yaml:通过给一个Person枚举追加变体,验证serde_yaml解析出的两个Registry确实不相等,确保比对逻辑本身对格式差异敏感。

完整变更流程速查

将上述内容串联,一次"新增类型"的标准操作流程为:

  1. testsuite/generate-format/src/{api,aptos,consensus}.rs中按需添加tracer.trace_value(针对有自定义反序列化器的类型,通常在trace_crypto_values中)或tracer.trace_type(针对需整体展开的枚举/入口类型);
  2. 若 YAML 键名与 Rust 类型名不一致,使用key_name宏指定;
  3. 为参与序列化的Vec<u8>字段添加#[serde(with = "serde_bytes")],并在必要时借助 BCS 格式 Linter 自查;
  4. 运行cargo run -p generate-format -- --corpus {API|Aptos|Consensus} --record重新生成三个 staged YAML;
  5. 人工审查 YAML diff,重点确认枚举变体序号未被重排、字节字段呈现为BYTES而非SEQ
  6. 提交时保证srctests/staged六处文件同步变更,由detect_format_change.rs在 CI 中完成最终校验。

对于文档中强调的"新类型改动必须落在api.rsaptos.rsconsensus.rs及对应 YAML 上"这一原则,其根本原因在于:api.yaml保障外部 API 与 SDK 的序列化契约,aptos.yaml保障交易与账本数据的链上格式,consensus.yaml保障节点间共识消息的互通性——三者共同构成 Aptos 网络"数据格式不变量"的完整闭环。任何一条链路被破坏,都可能在升级或跨版本通信时引发严重后果,这正是 generate-format 作为"核心类型检查器"存在的意义。

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询