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-types、aptos-consensus、aptos-crypto、aptos-types、move-core-types、bcs等核心 crate,意味着它反射的是整个链上数据面的关键类型。
模块架构:五个 Corpus 与三份核心快照
generate-format将待追踪的 Rust 类型划分为多个Corpus(语料),见 src/lib.rs 中的Corpus枚举:
| Corpus | 覆盖范围 | 输出文件(相对仓库根目录) |
|---|---|---|
API | Rest API 相关类型(aptos_api_types) | testsuite/generate-format/tests/staged/api.yaml |
Aptos | 交易、认证器、写集等核心链上类型 | testsuite/generate-format/tests/staged/aptos.yaml |
Consensus | 共识消息与区块类型(aptos_consensus_types) | testsuite/generate-format/tests/staged/consensus.yaml |
Network | 网络握手与消息类型(aptos_network) | testsuite/generate-format/tests/staged/network.yaml |
MoveABI | Move ABI 相关类型 | testsuite/generate-format/tests/staged/move_abi.yaml |
每个 Corpus 的get_registry()方法负责完成一次"格式采集",核心流程在三份最关键的采集器——src/api.rs、src/aptos.rs、src/consensus.rs——中是一致的,可归纳为两步:
- 记录采样值(samples):对具有自定义反序列化器或特殊格式语义的类型,先用真实可用的值调用
tracer.trace_value,让Tracer依据具体实例推断格式,例如:- 用
StdRng::from_seed([0; 32])生成确定性随机密钥,记录Ed25519PublicKey、secp256k1_ecdsa、bls12381、slh_dsa_sha2_128s等各套加密算法的公私钥与签名样本; event::EventKey::random()、write_set::WriteOp::legacy_deletion()等特殊类型的样本。
- 用
- 逐类型追踪(trace_type):以"主入口类型 + 每个枚举单独追踪"的方式批量反射类型图,例如
transaction::Transaction、TransactionAuthenticator、AnyPublicKey、AnySignature、AssertionSignature、ValidatorTransaction、BlockMetadataExt等,最终通过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.rs、testsuite/generate-format/src/aptos.rs、testsuite/generate-format/src/consensus.rs; - 格式快照:
testsuite/generate-format/tests/staged/api.yaml、testsuite/generate-format/tests/staged/aptos.yaml、testsuite/generate-format/tests/staged/consensus.yaml。
前者让新类型进入追踪范围,后者把追踪结果固化下来供测试比对。下面以文档中的真实案例——新增secp256r1_ecdsa加密库(含PublicKey、Signature、PrivateKey三个类型)以及支撑 WebAuthn 交易的PartialAuthenticatorAssertionResponse、AssertionSignature结构——分两步完整演练。
案例一:新增加密库类型的四步改动
第一步:在三个采集器中添加 tracer 追踪
在api.rs、aptos.rs、consensus.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.yaml、aptos.yaml、consensus.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: Ed25519、1: Secp256k1Ecdsa、2: Secp256r1Ecdsa、3: Keyless、4: FederatedKeyless、5: 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行 印证了这套结构:AnySignature的WebAuthn变体序号为2(排在0: Ed25519、1: Secp256k1Ecdsa之后),PartialAuthenticatorAssertionResponse由signature、authenticator_data、client_data_json三字段组成。
关于这三个文件的采集器,README 特别强调:由于api.rs/aptos.rs/consensus.rs已经追踪了AnyPublicKey与AnySignature结构,此处无需为新增变体添加额外 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会遍历格式树并执行如下规则:
- 禁止浮点与字符类型:
F32、F64、Char一律报错,因为 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()执行三重校验:
- 与磁盘快照比对:实时计算当前代码的
Registry,与 staged YAML 反序列化出的Registry逐类型比对。若已有类型被删除或格式不匹配、或有新类型未记录,断言失败并给出提示; - Linter 校验:对每个 corpus 中的每个格式定义运行
lint_bcs_format,确保符合上述 BCS 最佳实践; - 跨 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确实不相等,确保比对逻辑本身对格式差异敏感。
完整变更流程速查
将上述内容串联,一次"新增类型"的标准操作流程为:
- 在
testsuite/generate-format/src/{api,aptos,consensus}.rs中按需添加tracer.trace_value(针对有自定义反序列化器的类型,通常在trace_crypto_values中)或tracer.trace_type(针对需整体展开的枚举/入口类型); - 若 YAML 键名与 Rust 类型名不一致,使用
key_name宏指定; - 为参与序列化的
Vec<u8>字段添加#[serde(with = "serde_bytes")],并在必要时借助 BCS 格式 Linter 自查; - 运行
cargo run -p generate-format -- --corpus {API|Aptos|Consensus} --record重新生成三个 staged YAML; - 人工审查 YAML diff,重点确认枚举变体序号未被重排、字节字段呈现为
BYTES而非SEQ; - 提交时保证
src与tests/staged六处文件同步变更,由detect_format_change.rs在 CI 中完成最终校验。
对于文档中强调的"新类型改动必须落在api.rs、aptos.rs、consensus.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),仅供参考