news 2026/9/14 20:59:27

Solana 存储 Protobuf 定义与构建期代码生成机制解析(storage-proto 模块深度指南)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Solana 存储 Protobuf 定义与构建期代码生成机制解析(storage-proto 模块深度指南)

Solana 存储 Protobuf 定义与构建期代码生成机制解析(storage-proto 模块深度指南)

【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana

solana-storage-proto是 Solana 验证者仓库中负责链上数据持久化序列化契约的核心 crate:它以 protobuf 文件为唯一事实来源,在构建时自动生成 Rust 结构体,并通过convert.rs在「运行时交易类型」与「生成的 Protobuf 类型」之间建立双向转换。本文以 storage-proto/README.md 为骨架,结合proto/*.proto定义、src/convert.rs桥接实现与src/lib.rs中的存储结构体,完整讲解其工作原理、三个 Protobuf 数据模型、构建期代码生成配置,以及如何修改 proto 文件以演进链上数据格式。

一、模块定位:构建期自动生成的核心工作流

storage-proto的 README 用两句话界定了整个模块的核心范式:

Thesolana-storage-protostructs used insrc/convert.rsand elsewhere are auto-generated from protobuf definitions on build. To update these structs, simply make the desired edits toproto/*.protofiles.

翻译过来就是两条关键约定:

  1. structs 不是手写的convert.rs及仓库其他位置使用的solana-storage-proto结构体,全部是在build 阶段由 protobuf 定义自动生成的;
  2. 升级数据的正确姿势:要更新这些结构体,只需要编辑proto/*.proto文件,重新构建即可,无需手动同步 Rust 代码。

这种「单一事实来源(Single Source of Truth)」的设计意味着:链上区块、交易、账本条目等数据在写入 Bigtable 等持久化存储时的字节级布局,完全由proto/目录下的三个.proto文件决定。任何格式演进(新增字段、调整字段编号)都必须从这里开始,这与 protobuf 向后兼容的字段编号约束直接相关——已发布的字段编号不得复用或变更类型

模块的文件结构如下:

storage-proto/ ├── proto/ │ ├── confirmed_block.proto # 已确认区块/交易/状态元数据 │ ├── entries.proto # 账本 Entry(POH 条目)摘要 │ └── transaction_by_addr.proto # 按地址索引的交易记录 ├── src/ │ ├── lib.rs # 存储侧结构体(Stored*)与序列化兼容逻辑 │ └── convert.rs # 运行时类型 <-> 生成类型 的双向转换 ├── Cargo.toml # 构建依赖:tonic-build / protobuf-src └── README.md

二、构建期代码生成:tonic-build 与 OUT_DIR 注入

2.1 构建依赖配置

在 storage-proto/Cargo.toml 中,构建依赖(build-dependencies)决定了生成流程:

[build-dependencies] tonic-build = { workspace = true } # windows users should install the protobuf compiler manually and set the PROTOC # envar to point to the installed binary [target."cfg(not(windows))".build-dependencies] protobuf-src = { workspace = true }

两个要点:

  • tonic-build:在build.rs阶段读取proto/*.proto并调用protoc生成 Rust 代码。生成的代码依赖prost运行时(Cargo.toml 中prost = { workspace = true }即为运行时依赖)。
  • protobuf-src(仅非 Windows):为 Unix/Linux 平台编译内置的 protoc 二进制,免去手动安装 protobuf 编译器的麻烦。Windows 用户则需要自行安装 protobuf 编译器,并通过PROTOC环境变量指向安装的二进制,例如:
# Windows 环境变量示例 set PROTOC=C:\path\to\protoc.exe

2.2 生成代码如何进入编译单元

生成的代码并不出现在源码目录中,而是写入 cargo 的OUT_DIR,再通过include!宏在编译期嵌入。这在 storage-proto/src/convert.rs 中有明确的证据:

pub mod generated { include!(concat!( env!("OUT_DIR"), "/solana.storage.confirmed_block.rs" )); } pub mod tx_by_addr { include!(concat!( env!("OUT_DIR"), "/solana.storage.transaction_by_addr.rs" )); } pub mod entries { include!(concat!(env!("OUT_DIR"), "/solana.storage.entries.rs")); }

env!("OUT_DIR")是 cargo 在构建期注入的环境变量,指向生成文件所在目录。三个 proto 文件各自生成一个 Rust 模块文件,并分别挂载为convert::generatedconvert::tx_by_addrconvert::entries三个命名空间。模块名与 proto 文件中的package声明一一对应(详见下文各 proto 文件分析)。

可以推断的完整构建链路为:

proto/*.proto │ tonic-build(build.rs,调用 protoc / protobuf-src) ▼ OUT_DIR/solana.storage.*.rs(生成 Rust 结构体 + impl prost::Message) │ include!(concat!(env!("OUT_DIR"), ...)) ▼ src/convert.rs 中的 generated / tx_by_addr / entries 模块 │ From / TryFrom 桥接 ▼ src/lib.rs 的 Stored* 结构体 与 运行时交易类型

三、三个 Protobuf 数据模型详解

3.1confirmed_block.proto:已确认区块的完整画像

storage-proto/proto/confirmed_block.proto 定义了包solana.storage.ConfirmedBlock,是三个 proto 中信息量最大的一个,覆盖从区块头到交易执行细节的全部字段。

顶层ConfirmedBlock聚合了一个区块的核心要素:

字段类型编号说明
previous_blockhashstring1父区块哈希
blockhashstring2当前区块哈希
parent_slotuint643父区块所在的 Slot
transactionsrepeated ConfirmedTransaction4区块内全部交易(含状态元数据)
rewardsrepeated Reward5区块奖励(费用/租金/质押/投票)
block_timeUnixTimestamp6区块时间戳(int64 timestamp
block_heightBlockHeight7区块高度(uint64 block_height

Transaction/Message/MessageHeader完整刻画交易结构:Transactionrepeated bytes signaturesMessage组成;Message包含header(三个 uint32:必需签名数、只读签名账户数、只读非签名账户数)、account_keysrecent_blockhashinstructions,以及两个 Solana 特色字段:

bool versioned = 5; // 是否为版本化交易(v0) repeated MessageAddressTableLookup address_table_lookups = 6; // 地址查找表

versionedaddress_table_lookups直接对应 Solana 的 Versioned Transaction(v0 消息)与 Address Lookup Table(ALTs)特性,这也是convert.rs中区分VersionedMessage::LegacyVersionedMessage::V0的依据。

TransactionStatusMeta是交易执行结果的元数据集合,注意其中精心设计的*_none布尔标记:

TransactionError err = 1; uint64 fee = 2; repeated uint64 pre_balances = 3; repeated uint64 post_balances = 4; repeated InnerInstructions inner_instructions = 5; bool inner_instructions_none = 10; repeated string log_messages = 6; bool log_messages_none = 11; repeated TokenBalance pre_token_balances = 7; repeated TokenBalance post_token_balances = 8; repeated Reward rewards = 9; repeated bytes loaded_writable_addresses = 12; repeated bytes loaded_readonly_addresses = 13; ReturnData return_data = 14; bool return_data_none = 15; optional uint64 compute_units_consumed = 16; // 自 v1.10.35 / v1.11.6 起可用

*_none标记用于区分「字段为空」与「字段缺失」:protobuf3 的 repeated 字段无法直接表达Option,因此用inner_instructions_none = true表示「原本就是 None」而不是「空列表」,从而在反序列化时还原出正确的Option语义。同理,return_data_none对应Option<TransactionReturnData>compute_units_consumed使用optional关键字并在注释中注明其版本可用性(v1.10.35 / v1.11.6 之前执行的交易该字段为None)——这是 proto 文件对跨版本兼容性的显式管理。

RewardRewardType枚举

enum RewardType { Unspecified = 0; Fee = 1; Rent = 2; Staking = 3; Voting = 4; } message Reward { string pubkey = 1; int64 lamports = 2; uint64 post_balance = 3; RewardType reward_type = 4; string commission = 5; }

注意commission在 proto 中是string类型——因为奖励佣金在链上可能是可选数值,用字符串承载可以表达「无佣金」的语义;convert.rs中对应为commission.parse::<u8>().ok()(见 convert.rs),解析失败即得到None

其他辅助消息包括InnerInstructions/InnerInstruction(含自 v1.14.6 起可用的optional uint32 stack_height)、CompiledInstructionTokenBalance/UiTokenAmountui_amount为 double、amount为最小单位字符串)、ReturnDataprogram_id+data)、UnixTimestampBlockHeight包装器。

3.2entries.proto:PoH 条目摘要

storage-proto/proto/entries.proto 定义了包solana.storage.Entries,用于持久化PoH(Proof of History)账本条目的轻量摘要

message Entries { repeated Entry entries = 1; } message Entry { uint32 index = 1; // 条目序号 uint64 num_hashes = 2; // 该条目包含的哈希数 bytes hash = 3; // 条目哈希 uint64 num_transactions = 4; // 条目内交易数 uint32 starting_transaction_index = 5; // 起始交易索引 }

它与convert.rsFrom<(usize, EntrySummary)> for entries::Entry的转换一一对应(convert.rs):(index, EntrySummary)元组被映射为Entry,其中EntrySummary来自solana-transaction-statuscrate,携带num_hasheshashnum_transactionsstarting_transaction_index。反向转换From<entries::Entry> for EntrySummary则用Hash::new(&entry.hash)重建哈希对象。

3.3transaction_by_addr.proto:按地址查询交易的索引模型

storage-proto/proto/transaction_by_addr.proto 定义了包solana.storage.TransactionByAddr,是「按地址反查交易历史」场景的数据模型,也是三个 proto 中错误类型编码最精细的一个:

message TransactionByAddr { repeated TransactionByAddrInfo tx_by_addrs = 1; } message TransactionByAddrInfo { bytes signature = 1; TransactionError err = 2; uint32 index = 3; Memo memo = 4; UnixTimestamp block_time = 5; }

错误类型的三层结构是这里的重点:

message TransactionError { TransactionErrorType transaction_error = 1; // 顶层错误类别(37 个枚举值) InstructionError instruction_error = 2; // 指令级错误(含索引 + 错误码 + 自定义错误) TransactionDetails transaction_details = 3; // 携带账户索引的细节(如 DuplicateInstruction) } enum TransactionErrorType { ACCOUNT_IN_USE = 0; ACCOUNT_LOADED_TWICE = 1; ... WOULD_EXCEED_MAX_BLOCK_COST_LIMIT = 17; ADDRESS_LOOKUP_TABLE_NOT_FOUND = 23; ... UNBALANCED_TRANSACTION = 36; }

TransactionErrorType枚举完整镜像了solana_sdk::transaction::TransactionError的 37 个变体,从ACCOUNT_IN_USE(0)到UNBALANCED_TRANSACTION(36),涵盖账户、签名、费用、地址查找表、成本限制等各类失败原因。InstructionErrorType枚举则更细,包含 54 个值(GENERIC_ERROR0 到BUILTIN_PROGRAMS_MUST_CONSUME_COMPUTE_UNITS53),对应InstructionError的全部错误码。

convert.rs中这段转换非常值得研究(convert.rs):TryFrom<tx_by_addr::TransactionError> for TransactionError首先特判transaction_error == 8(即INSTRUCTION_ERROR),此时从instruction_error子消息中还原出TransactionError::InstructionError(index, InstructionError)——其中error字段的数值被逐一分派回对应的InstructionError变体,custom字段还原为InstructionError::Custom(custom);随后对transaction_details处理DuplicateInstruction(30)、InsufficientFundsForRent(31)、ProgramExecutionTemporarilyRestricted(35)这三个携带索引的变体;最后才是平铺的其余 34 个无载荷变体。反向From<TransactionError> for tx_by_addr::TransactionError则做完全对称的编码。

四、convert.rs:运行时类型与生成类型的双向桥

4.1 转换矩阵总览

convert.rs的全部职责是打通三层类型系统:

  1. 运行时类型solana-sdk/solana-transaction-status):如VersionedTransactionTransactionStatusMetaConfirmedBlockRewardTransactionError等;
  2. 生成类型(本 crate 的generated/tx_by_addr/entries模块):protobuf 编译产物;
  3. 存储类型lib.rsStored*):面向 bincode 序列化的中间表示。

关键转换实现及其行为:

转换方向说明
VersionedConfirmedBlock → generated::ConfirmedBlockFrom区块聚合转换,block_time/block_height包装为Option
generated::ConfirmedBlock → ConfirmedBlockTryFrom反向转换,错误类型为bincode::Error
TransactionWithStatusMeta → generated::ConfirmedTransactionFrom区分MissingMetadataComplete两种形态
VersionedMessage → generated::MessageFrom依据Legacy/V0分别转换,V0 携带address_table_lookups
generated::Message → VersionedMessageFrom依据versioned布尔标志还原为LegacyV0
TransactionStatusMeta ↔ generated::TransactionStatusMetaFrom/TryFrom*_none标记与loaded_addresses的拆装
Reward ↔ generated::RewardFromRewardType与 protobuf 枚举的数值映射
TransactionError ↔ tx_by_addr::TransactionErrorFrom/TryFrom三层错误结构的双向编解码(见 3.3)
(usize, EntrySummary) ↔ entries::EntryFrom账本条目摘要转换

4.2 版本化交易与地址查找表的转换细节

generated::Messageversioned布尔字段决定了还原路径(convert.rs):

impl From<generated::Message> for VersionedMessage { fn from(value: generated::Message) -> Self { // ...header / account_keys / recent_blockhash / instructions 重建 if !value.versioned { Self::Legacy(LegacyMessage { header, account_keys, recent_blockhash, instructions }) } else { Self::V0(v0::Message { header, account_keys, recent_blockhash, instructions, address_table_lookups }) } } }

同时MessageAddressTableLookup ↔ generated::MessageAddressTableLookup的转换中,account_key以 Pubkey 的原始字节形式存入bytes字段,writable_indexes/readonly_indexes则直接以字节切片存储。

4.3 Token 余额的转换细节

generated::TokenBalance → TransactionTokenBalance(convert.rs)中有一个值得注意的容错逻辑:ui_amount与 0.0 的差值在浮点精度(f64::EPSILON)范围内时被还原为Noneui_amount_string为空时,会用real_number_string_trimmed(amount, decimals)从最小单位金额重新计算——这保证了旧数据(可能缺少展示字符串)也能正确渲染。

五、lib.rs:存储侧结构体与向后兼容

5.1 为什么需要Stored*结构体

lib.rs定义了四个Stored*结构体(见 storage-proto/src/lib.rs):StoredExtendedRewardStoredTokenAmountStoredTransactionTokenBalanceStoredTransactionStatusMeta。它们的作用是作为介于运行时类型与生成类型之间的 bincode 友好中间表示,用于旧版 Bigtable 数据路径。

StoredTransactionStatusMeta是其中最重要的结构:

#[derive(Serialize, Deserialize)] pub struct StoredTransactionStatusMeta { pub status: Result<()>, pub fee: u64, pub pre_balances: Vec<u64>, pub post_balances: Vec<u64>, #[serde(deserialize_with = "default_on_eof")] pub inner_instructions: Option<Vec<InnerInstructions>>, // ... log_messages / pre_token_balances / post_token_balances / // rewards / return_data / compute_units_consumed 同构 }

5.2default_on_eof:字段级向后兼容的利器

Stored*结构体的每个可选字段都标注了#[serde(deserialize_with = "default_on_eof")]default_on_eof来自solana_sdk::deserialize_utils(lib.rs),其语义是:当 bincode 反序列化到达 EOF(即旧数据不包含该字段)时,返回None/默认值而不是报错

这是 Solana 升级存储格式的经典手法:新版本在Stored*中追加可选字段后,旧数据仍可被读取(缺失字段默认为None),实现平滑的格式演进,无需数据迁移。这与 proto 文件中*_none标记、optional字段形成互补——前者保护 bincode 旧路径,后者保护 protobuf 新路径。

5.3 bincode 的边界:loaded_addresses的弃用

TryFrom<TransactionStatusMeta> for StoredTransactionStatusMeta(lib.rs)中有一段显式的弃用守卫:

if !loaded_addresses.is_empty() { // Deprecated bincode serialized status metadata doesn't support // loaded addresses. return Err( bincode::ErrorKind::Custom("Bincode serialization is deprecated".into()).into(), ); }

即:旧版 bincode 序列化的状态元数据不支持 loaded addresses(v0 版本化交易的地址查找表加载结果)。一旦发现loaded_addresses非空,直接返回"Bincode serialization is deprecated"错误,引导上层走 protobuf 序列化路径。这清晰展示了新旧两条序列化通道的职责划分与迁移方向。

六、测试验证:往返一致性保障

convert.rs文件末尾内置了完整的单元测试模块(convert.rs,#[cfg(test)] mod test),核心测试模式是往返(round-trip)一致性

  • test_reward_type_encode:对None/Fee/Rent/Voting/Staking五种奖励类型逐一验证Reward → generated::Reward → Reward往返后完全相等;
  • test_transaction_by_addr_encode:构造带真实 base58 签名的TransactionByAddrInfoindex: 5memo: "string"block_time: 1610674861),验证经 protobuf 往返后相等;
  • test_transaction_error_encode:覆盖AccountBorrowOutstandingAccountInUseInstructionError(10, AccountAlreadyInitialized)等大量错误变体(包括三层结构中的InstructionErrorTransactionDetails路径),逐一验证TransactionError → tx_by_addr::TransactionError → TransactionError往返相等。

这些测试的工程意义在于:任何对 proto 文件的修改或转换逻辑的调整,都必须保持与运行时类型的双向转换不丢失信息,从而保证 Bigtable 写入的数据可被完整读回。

七、实际消费方:storage-bigtable

solana-storage-proto的主要消费方是 storage-bigtable 模块(storage-bigtable/Cargo.toml、storage-bigtable/src/bigtable.rs、storage-bigtable/src/lib.rs 中均引用了该 crate)。在bigtable.rs的测试代码中可以看到典型用法(bigtable.rs):ConfirmedBlock先转换为StoredConfirmedBlock(bincode 路径),再通过solana_storage_proto::convert::generated转换为generated::ConfirmedBlock(protobuf 路径),随后即可由 prost 编码为字节写入 Bigtable。

由此可以梳理出完整的链上数据持久化链路

区块/交易确认 │ ▼ solana-transaction-status 的 ConfirmedBlock / TransactionStatusMeta(运行时类型) │ convert.rs 的 From / TryFrom ▼ storage-proto 生成类型(generated::ConfirmedBlock 等) │ prost::Message::encode(protobuf 序列化) ▼ storage-bigtable 写入 Google Cloud Bigtable

八、修改 proto 的实践指南

根据 README 的指引与源码证据,扩展存储格式的标准流程为:

  1. 编辑 proto 文件:在 storage-proto/proto/ 下修改或新增消息字段。务必遵守 protobuf 兼容性铁律:只能新增字段并分配新编号,不得修改既有字段的类型或复用编号
  2. 补充转换逻辑:在 storage-proto/src/convert.rs 中为新字段增加From/TryFrom的映射(注意*_none标记、Option语义的还原);
  3. 同步存储结构:若涉及 bincode 旧路径,在 storage-proto/src/lib.rs 的Stored*结构体中追加可选字段,并用#[serde(deserialize_with = "default_on_eof")]保持向后兼容;
  4. 构建生成:重新cargo buildtonic-build会重新生成 Rust 结构体;非 Windows 平台由protobuf-src提供 protoc,Windows 需确保PROTOC环境变量已配置;
  5. 运行往返测试:执行cargo test -p solana-storage-proto,确保test_*_encode系列测试通过,验证新字段的序列化往返一致。

总结

solana-storage-proto通过「proto 定义 → 构建期自动生成 → 双向转换桥接 → bincode/protobuf 双通道兼容」的设计,将 Solana 链上数据的持久化格式从手写结构体中解放出来。理解这个模块的关键在于把握三条主线:proto/*.proto是数据契约的事实来源,convert.rs是运行时类型与生成类型之间的翻译层,lib.rsStored*结构体配合default_on_eof保障了存储格式的平滑演进。无论是为链上数据新增字段,还是排查 Bigtable 数据读写的格式问题,这三个文件都是需要优先审视的入口。

【免费下载链接】solanaWeb-Scale Blockchain for fast, secure, scalable, decentralized apps and marketplaces.项目地址: https://gitcode.com/GitHub_Trending/so/solana

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 20:58:18

MATLAB实现光纤布拉格光栅传输矩阵法仿真

1. 光纤布拉格光栅仿真概述光纤布拉格光栅&#xff08;FBG&#xff09;作为光纤通信和传感领域的核心器件&#xff0c;其光谱特性直接影响着系统性能。传统实验方法需要昂贵的制备设备和复杂的测试流程&#xff0c;而MATLAB仿真为我们提供了一种经济高效的研究手段。传输矩阵法…

作者头像 李华
网站建设 2026/9/14 20:57:58

接口芯片的四大物理契约:电压、时序、拓扑与鲁棒性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 20:57:40

交直流混合配电网潮流计算的统一求解法及Matlab实现

1. 交直流混合配电网潮流计算概述交直流混合配电网是未来智能电网发展的重要方向&#xff0c;它结合了交流电网的成熟技术和直流电网的高效传输优势。在这种混合系统中&#xff0c;潮流计算作为电网分析的基础工具&#xff0c;其重要性不言而喻。传统的交替迭代法在处理交直流混…

作者头像 李华
网站建设 2026/9/14 20:57:27

CRMEB开源多商户商城系统架构与实现解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华