Monero epee 可移植存储(Portable Storage)二进制格式完全解析

发布时间:2026/9/24 4:15:35
Monero epee 可移植存储(Portable Storage)二进制格式完全解析 区块链金融科技【免费下载链接】moneroMonero: the secure, private, untraceable cryptocurrency项目地址https://gitcode.com/gh_mirrors/mo/monero点击查看免费下载导读本文基于 Monero 仓库中的 docs/PORTABLE_STORAGE.md 系统讲解 epee 库的可移植存储Portable Storage二进制序列化格式。该格式是 Monero 全节点 P2P 通信Levin 协议与 RPC 接口传输数据的基础载体理解它有助于分析 Monero 网络报文、调试钱包与节点交互、以及实现兼容的第三方节点或工具。读完本文你将掌握该格式的整型与 varint 编码、字符串与 section key 的编码规则、完整的二进制布局规范、13 种条目类型及数组标志的用法并能独立手工解析一段真实的 epee 序列化字节流。背景epee 与 Portable Storage 的定位Monero 使用了一个小型辅助库 contrib/epee 中的一系列工具类。其中一部分实现了一个名为 Levin 的网络协议其核心定义位于 contrib/epee/include/net/levin_base.hLevin 报文头中的签名常量LEVIN_SIGNATURE 0x0101010101012101LL与可移植存储头部的签名呼应代码注释中戏称为 Benders nightmare。Levin 协议内部依赖一种存储格式即本文主角 Portable Storage实现位于 contrib/epee/include/storages 目录。在过去这一格式连同 epee 库的其余部分几乎没有独立文档代码本身就是唯一文档。epee 库的其他部分相对容易从代码读懂而 Portable Storage 则不然——这正是本指南存在的意义。当前仓库中与它配套的完整实现文件包括portable_storage_base.h格式常量、类型码、section数据结构定义portable_storage.hportable_storage类声明与值读写模板portable_storage_to_bin.h序列化打包实现portable_storage_from_bin.h反序列化解析实现含递归深度与资源限额保护portable_storage_bin_utils.h大小端字节序转换CONVERT_PODportable_storage.cppstore_to_binary/load_from_binary等核心函数。下文所有规则均可在上述源码中逐一印证。字符串与整型编码整型除少数例外epee 可移植存储格式中序列化的整数一律采用小端序little-endian。例如 4 字节的int32值 20140418 在字节流中写作82 51 33 01见下文完整示例。这一行为在 portable_storage_bin_utils.h 中有直接体现CONVERT_POD(x)在大端机器上会通过SWAP16LE/SWAP32LE/SWAP64LE将值转换为小端在小端机器上则原样直出从而保证线上字节序恒定。Varint变长整型Varint 用于以节省空间的方式打包整数。编码规则Varint 以 4 字节或 8 字节小端整型存储其最低 2 个比特bit0bit1记录整个值实际占用的字节数因此 1 个字节内最多只能存放 6 个有效数据位单字节可表示的最大值为 63。具体对应关系如下表最低 2 位 → 占用字节数 → 可表示范围最低 2 位占用字节值范围b001 字节0 到 63b012 字节64 到 16383b104 字节16384 到 1073741823b118 字节1073741824 到 4611686018427387903编码时实际值先左移 2 位v pv 2再与占用字节标记按位或v | type_or最后以小端写出。这在 portable_storage_to_bin.h 的pack_varint/pack_varint_t中实现if(val 63) // 一个字节就够了 return pack_varint_tuint8_t(strm, PORTABLE_RAW_SIZE_MARK_BYTE, val); else if(val 16383) return pack_varint_tuint16_t(strm, PORTABLE_RAW_SIZE_MARK_WORD, val); else if(val 1073741823) return pack_varint_tuint32_t(strm, PORTABLE_RAW_SIZE_MARK_DWORD, val); else return pack_varint_tuint64_t(strm, PORTABLE_RAW_SIZE_MARK_INT64, val);对应的PORTABLE_RAW_SIZE_MARK_*掩码常量定义在 portable_storage_base.hPORTABLE_RAW_SIZE_MARK_MASK 0x03用于取出最低 2 位BYTE0、WORD1、DWORD2、INT643。解析端在 portable_storage_from_bin.h 的read_varint()中读取首字节最低 2 位决定按 1/2/4/8 字节读取再v 2还原真实值。示例值的字节表示值字节表示hex00071c10195 0117,000A2 09 01 007,942,319,74403 BA 98 65 07 00 00 00以 7 为例验证编码过程7 2 28 0x1C最低 2 位为00单字节故输出1c。101 则需 2 字节101 2 404 0x194或上标记0x01得0x195小端写出即95 01。字符串String字符串编码十分简单先是一个 varint 表示长度随后紧跟原始字符数据没有结尾的\0终止符当然如果实现者愿意可以自行追加一个。格式不强制任何字符编码事实上很多场景下二进制数据块也直接以这种字符串形式存储下文Monero 特有约定一节会再次提到。需要特别强调本节描述的字符串与 section 中的键key不是一回事——section 键长度上限为 255 字节且不用 varint 编码长度见下一节。Howdy 14 48 6F 77 64 79即varint 长度 5 →0x145 2 20 0x14随后是 ASCII 字符48 6F 77 64 79。Section 键Section KeysSection 键与字符串类似但有两个不同点长度被限制在255 字节以内长度信息使用单个前置字节描述而非 varint。因此Howdy编码为05 48 6F 77 64 79——05就是键长 5。在实现中写端代码 portable_storage_to_bin.h 的pack_entry_to_buff(strm, const section sec)对每个键做了严格校验键长必须小于std::numeric_limitsuint8_t::max()255且不允许为空读端 portable_storage_from_bin.h 的read_sec_name()读取 1 字节长度后按此大小读取键名并校验name_len 0。二进制格式规范头部Header任何可移植存储数据必须以如下头部开始字段类型值签名 ASignature Part AUInt320x01011101签名 BSignature Part BUInt320x01020101版本VersionUInt80x01合计 9 字节的头部在 hex 中表现为01 11 01 01 01 01 02 01 01。代码中对应的常量定义于 portable_storage_base.h#define PORTABLE_STORAGE_SIGNATUREA 0x01011101 #define PORTABLE_STORAGE_SIGNATUREB 0x01020101 // benders nightmare #define PORTABLE_STORAGE_FORMAT_VER 1序列化时portable_storage.cpp 的store_to_binary通过#pragma pack(1)定义的storage_block_header结构两个uint32_t加一个uint8_t将三个字段依次写入反序列化时load_from_binary会逐一校验包长不小于头部大小、两个签名SWAP32LE转换后比对与版本号任一不匹配都会记录LOG_ERROR并返回false。Section区块/对象头部之后是一个根对象库中称为 section。它本质上是名-值对entries的映射表源码中section即std::mapstd::string, storage_entry见 portable_storage_base.h。其序列化以条目计数开始Section类型条目计数Entry countvarint随后按顺序排列该 section 的名-值对条目Entry。Entry条目每个条目由四部分组成字段类型名称Namesection key类型Type1 字节计数Count1varint值Value(s)取决于类型的可变数据1注意仅当条目类型带有数组标志见下文时才存在此字段。条目类型Entry Types类型码定义如下完整源码见 portable_storage_base.h#define SERIALIZE_TYPE_INT64 1 #define SERIALIZE_TYPE_INT32 2 #define SERIALIZE_TYPE_INT16 3 #define SERIALIZE_TYPE_INT8 4 #define SERIALIZE_TYPE_UINT64 5 #define SERIALIZE_TYPE_UINT32 6 #define SERIALIZE_TYPE_UINT16 7 #define SERIALIZE_TYPE_UINT8 8 #define SERIALIZE_TYPE_DOUBLE 9 #define SERIALIZE_TYPE_STRING 10 #define SERIALIZE_TYPE_BOOL 11 #define SERIALIZE_TYPE_OBJECT 12 #define SERIALIZE_TYPE_ARRAY 13条目类型字节可以按位或上一个标志#define SERIALIZE_FLAG_ARRAY 0x80该标志表示该条目含有多个值数组。由于只预留了一个比特位表示数组格式无法直接表达嵌套数组。不过有一个标准变通方案把每个内部数组放进各自的 section 中再将外层数组声明为SERIALIZE_TYPE_OBJECT | SERIALIZE_FLAG_ARRAY。类型码字节之后紧跟一个 varint 指定数组长度随后所有元素按顺序连续序列化无任何填充padding、不带任何类型信息type, count, value1, value2, ..., value_n序列化端对应 portable_storage_to_bin.h 中array_entry_store_visitor的pack_pod_array_typePOD 数组以及针对std::string数组、section数组、array_entry数组的重载实现解析端则是 portable_storage_from_bin.h 的load_storage_array_entry先type ~SERIALIZE_FLAG_ARRAY剥离标志再分发与read_aeT()先读 varint 长度并做资源配额与ps_min_bytes最小字节数健全性检查再逐个读取元素。条目值Entry Values需要强调条目值本身允许实现自行选择编码方式。例如整数既可以小端也可以大端存储——虽然 Monero/epee 的当前实现统一采用小端见上文的CONVERT_POD机制但格式规范层面并不强制。值为对象即SERIALIZE_TYPE_OBJECT的条目其内容按Section的规则递归存储先 varint 计数再逐条目。因此整个格式本质上是对象图的递归结构。解析端通过EPEE_PORTABLE_STORAGE_RECURSION_LIMIT默认 100限制递归深度防止恶意构造的深层嵌套数据导致栈溢出见 portable_storage_from_bin.h。另外原文档作者指出尚未在实际代码中见到SERIALIZE_TYPE_ARRAY13这一类型的使用推测它是为无类型数组预留的——即数组内后续条目可以是任意类型。当前仓库的序列化端确实保留了array_entry_tarray_entry的处理分支SERIALIZE_TYPE_ARRAY|SERIALIZE_FLAG_ARRAY说明这一能力在底层是存在的只是 Monero 自身的消息结构中尚未用到。完整示例把以上规则组合起来看一个完整对象序列化后的样子。为了便于理解先定义一个 JSON 对象大多数读者都熟悉 JSON 语法{ short_quote: Give me liberty or give me death, long_quote: Monero is more than just a technology. Its also what the technology stands for., signed_32bit_int: 20140418, array_of_bools: [true, false, true, true], nested_section: { double: -6.9, unsigned_64bit_int: 11111111111111111111 } }该对象序列化为 epee 可移植存储格式后的完整字节流如下hex 表示注释与空白为便于阅读所加01 11 01 01 01 01 02 01 // Signature 01 // Version 14 // Varint number of section entries (5) 0b // Length of next section key (11) 73 68 6f 72 74 5f 71 75 6f 74 65 // Section key (short_quote) 0a // Type code (STRING) 80 // Varint length of string (32) 47 69 76 65 20 6d 65 20 6c 69 62 65 72 74 79 20 // STRING value (Give me liberty ) 6f 72 20 67 69 76 65 20 6d 65 20 64 65 61 74 68 // STRING value cont. (or give me death) 0a // Length of next section key (10) 6c 6f 6e 67 5f 71 75 6f 74 65 // Section key (long_quote) 0a // Type code (STRING) 41 01 // Varint length of string (80). Note its 2 bytes 4d 6f 6e 65 72 6f 20 69 73 20 6d 6f 72 65 20 74 // STRING value (Monero is more t) 68 61 6e 20 6a 75 73 74 20 61 20 74 65 63 68 6e // STRING value cont. (han just a techn) 6f 6c 6f 67 79 2e 20 49 74 27 73 20 61 6c 73 6f // STRING value cont. (ology. Its also) 20 77 68 61 74 20 74 68 65 20 74 65 63 68 6e 6f // STRING value cont. ( what the techno) 6c 6f 67 79 20 73 74 61 6e 64 73 20 66 6f 72 2e // STRING value cont. (logy stands for.) 10 // Length of next section key (16) 73 69 67 6e 65 64 5f 33 32 62 69 74 5f 69 6e 74 // Section key (signed_32bit_int) 02 // type code (INT32) 82 51 33 01 // INT32 value (20140418) 0e // Length of next section key (14) 61 72 72 61 79 5f 6f 66 5f 62 6f 6f 6c 73 // Section key (array_of_bools) 8b // Type code (BOOL | FLAG_ARRAY) 10 // Varint number of array elements (4) 01 00 01 01 // Array BOOL values [true, false, true, true] 0e // Length of next section key (14) 6e 65 73 74 65 64 5f 73 65 63 74 69 6f 6e // Section key (nested_section) 0c // Type code (OBJECT) 08 // Varint number of inner section entries (2) 06 // Length of first inner section key (6) 64 6f 75 62 6c 65 // Section key (double) 09 // Type code (DOUBLE) 9a 99 99 99 99 99 1b c0 // DOUBLE value (-6.9) 12 // Length of second inner section key (18) 75 6e 73 69 67 6e 65 64 5f 36 34 62 69 74 5f 69 // Section key (unsigned_64bit_i) 6e 74 // Section key cont (nt) 05 // Type code (UINT64) c7 71 ac b5 af 98 32 9a // UINT64 value (11111111111111111111)逐段解读这份字节流可以验证前面所有的规则头部前 8 字节为两个签名0x01011101、0x01020101各占 4 字节注意文件头中的字节序是小端01 11 01 01第 9 字节01是版本号。根 section14是 varint0x14 5 2最低 2 位为00单字节表示根下有 5 个条目。第一个条目键长0b11键名short_quote类型码0a10STRING字符串长度 varint800x80 32 2最低 2 位00随后 32 字节字符串内容。第二个条目键long_quote长度 10STRING 类型字符串长度 varint 为41 01——注意这是 2 字节 varint最低 2 位01表示 WORD2 字节值0x0141 2 80即 80 字节的字符串。第三个条目键signed_32bit_int长度 16类型码02INT32值82 51 33 01小端存储即 0x01335182 20140418。第四个条目键array_of_bools长度 14类型码8b0x80 | 0x0bBOOL | FLAG_ARRAYvarint10表示 4 个元素随后 4 个单字节布尔值01 00 01 01true, false, true, true——布尔值在 epee 中严格限制为 0 或 1解析端会校验t 1否则抛异常。第五个条目键nested_section长度 14类型码0cOBJECT随后递归进入一个内层 sectionvarint082 个条目第一个内层条目键double长度 6类型码09DOUBLE8 字节 IEEE 754 双精度小端9a 99 99 99 99 99 1b c0即 -6.9第二个内层条目键unsigned_64bit_int长度 18类型码05UINT648 字节小端c7 71 ac b5 af 98 32 9a 11111111111111111111。Monero 特有约定Monero specifics条目值Entry Values在 Monero 的实际消息定义中各类型数据的映射约定如下哈希Hashes、密钥Keys、二进制块Blobs统一以字符串类型SERIALIZE_TYPE_STRING存储。这印证了前文字符串可用于存放二进制数据块的说法——Monero 的 32 字节哈希、公钥、密钥等均作为无 NUL 终止的二进制字符串装入条目。STL 容器vector、list可序列化为标准整型数组、字符串数组或用于结构体的SERIALIZE_TYPE_OBJECT对象数组即带SERIALIZE_FLAG_ARRAY标志的对象条目。这与解析端read_aesection()对对象数组的支持一一对应。Monero 结构定义速查Monero 中大量网络与 RPC 消息结构正是通过这套 KV 序列化宏BEGIN_KV_SERIALIZE_MAP等声明字段再由 epee 自动完成结构体 ⇄ 可移植存储转换的。可以参考以下两个核心定义文件来观察实际用法Core RPC 定义rpc_request_base、rpc_response_base以及各 RPC 命令的request_t/response_t结构全部以BEGIN_KV_SERIALIZE_MAP()声明字段名与类型CryptoNote 协议定义P2P 层节点间同步区块、交易等消息的结构定义。此外epee 还在 portable_storage_template_helper.h 中提供了load_t_from_json、store_t_to_json、load_t_from_binarybinary_to_struct系等模板工具函数实现了任意实现了load(portable_storage)/store(portable_storage)接口的结构体与 JSON / 二进制格式之间的双向互转。这意味着同一份结构体定义既可以用于 Levin 二进制协议也可以用于 JSON 化的调试与互操作场景。结语与进一步阅读epee 可移植存储格式是一套设计紧凑、无填充、自描述的递归名-值对二进制格式小端整型 双比特位标记的 varint 负责空间效率单字节类型码与0x80数组标志负责自描述Section 递归结构则承载任意嵌套的复杂对象。理解它之后你既可以手工解读 Monero 的 P2P 报文与 RPC 载荷也可以为自己的协议设计借鉴这套长度前缀 类型码的编码思路。如需深入推荐按以下顺序阅读仓库源码先读 portable_storage_base.h 掌握常量与数据结构再对照 portable_storage_to_bin.h 与 portable_storage_from_bin.h 的打包/解析逻辑最后结合 portable_storage.cpp 的store_to_binary/load_from_binary与 portable_storage_template_helper.h 的模板工具函数理解端到端用法。Levin 协议层的报文封装可继续阅读 contrib/epee/include/net/levin_base.h 与 docs/LEVIN_PROTOCOL.md。赞分享区块链金融科技【免费下载链接】moneroMonero: the secure, private, untraceable cryptocurrency项目地址https://gitcode.com/gh_mirrors/mo/monero点击查看免费下载相关推荐终极指南DuckDB备份格式——可移植的数据存储方案全解析终极指南DuckDB备份格式——可移植的数据存储方案全解析 DuckDB是一款高性能的嵌入式SQL OLAP数据库管理系统其备份格式为用户提供了简单可靠的数数据库OLAP嵌入式数据库数据分析MV2 单文件 AI 记忆格式规范深度解析Memvid 的二进制存储格式完全指南MV2 单文件 AI 记忆格式规范深度解析Memvid 的二进制存储格式完全指南 MV2Memory Vault v2是 Memvid 项目定义的一种 单人工智能Agent 记忆RAG向量数据库Shiori 存储目录完全指南数据存放位置、--storage-dir / --portable 与 SHIORI_DIR 配置详解Shiori 存储目录完全指南数据存放位置、 storage dir / portable 与 SHIORI_DIR 配置详解 Shiori用 Go 编写的后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询