- 区块链
- 金融科技
【免费下载链接】monero
Monero: the secure, private, untraceable cryptocurrency
导读
本文以 tests/fuzz/fuzz_rpc/README.md 为骨架,结合 Monero 仓库中tests/fuzz/fuzz_rpc/目录的完整源码,深入解析 Monero 官方 RPC 模糊测试(fuzzing)框架的设计思路、构建方式与运行流程。读完本文,你将掌握:Monero 如何在不启动真实服务器的情况下直接对core_rpc_server的各 RPC 端点处理函数进行模糊测试、Safe/Risky/Priority 三类端点如何划分与编排、FAKECHAIN回归测试模式如何构造虚拟区块链,以及 ZMQ 发布端如何被纳入同一套模糊测试体系。
为什么模糊测试要跳过传输层?
Monero 的 RPC 服务(core_rpc_server)对外通过 HTTP/JSON-RPC 与 ZMQ 两种途径暴露大量端点。传统做法是启动一个真实服务器、通过网络协议灌入随机请求,再观察崩溃与内存错误——但这样做有一个显著缺陷:
- 传输层噪声大:HTTP 解析、JSON 编解码、连接管理会消耗大量模糊测试输入(fuzz input),真正触达业务处理逻辑的路径占比很低;
- 端点覆盖不完整:网络层入口难以稳定覆盖到每个处理函数的所有分支;
- 启动开销高:每轮迭代都走完整的网络栈,吞吐量受限。
Monero 的做法是:跳过传输层,直接实例化core_rpc_server对象,跳过 fake server 的搭建,直接调用各 RPC 端点的处理函数。这样模糊测试的随机数据全部作用于 handler 内部的请求构造与业务逻辑,效率与覆盖率都更高。该思想在 tests/fuzz/fuzz_rpc/README.md 开头即有明确说明。
文件结构总览
tests/fuzz/fuzz_rpc/目录下共 9 个文件,职责划分清晰:
| 文件 | 作用 |
|---|---|
| fuzz_rpc.cpp | RPC 模糊测试主入口,包含LLVMFuzzerTestOneInput,管理每一轮迭代 |
| fuzz_zmq.cpp | ZMQ 模糊测试主入口,同样包含LLVMFuzzerTestOneInput |
| initialisation.cpp | 初始化辅助函数:配置 dummy 协议、P2P、core 对象;生成随机区块、矿工、交易 |
| initialisation.h | 初始化相关类型声明与导出 |
| rpc_endpoints.cpp | RPC 端点请求/响应对象构造与端点函数调用封装,以及三类端点映射表 |
| rpc_endpoints.h | RPC 端点 fuzz 函数声明与映射表导出 |
| zmq_endpoints.cpp | ZMQ 发布端点的请求构造与调用封装 |
| zmq_endpoints.h | ZMQ 端点声明 |
| README.md | 本文所基于的框架说明文档 |
RPC 端点的三类划分:Safe / Risky / Priority
rpc_endpoints.cpp将所有被模糊测试的 RPC 端点函数划分为三个映射表(std::map<int, std::function<...>>),分别对应三类行为特征:
- Priority(优先级):最关键、每轮迭代至少执行一次的端点。包括
get_blocks、get_blocks_by_height、get_hashes、get_outs_bin、get_transactions、is_key_image_spent、send_raw_tx、get_output_distribution_bin、pop_blocks、getblocktemplate、submitblock、generateblocks、relay_tx、get_output_distribution(映射键 0~13)。这些端点大多直接读写区块链数据或涉及交易提交,属于风险与价值双高的核心路径。 - Safe(安全):被视为稳定、不易失败的端点,共 41 个(映射键 14~54),覆盖
get_height、get_info、get_transaction_pool*系列、区块头查询系列、peer 列表、日志设置、set_bans/get_bans、hard_fork_info等只读或轻量写操作。 - Risky(高风险):更容易失败或提前返回(尤其是没有有效区块链生成时),包括
start_mining、stop_mining、mining_status、save_bc、stop_daemon、update、add_aux_pow、flush_txpool、flush_cache、get_txids_loose(映射键 55~65,其中 59 空缺、72 即prune_blockchain被注释禁用)。这些端点或会修改全局状态,或在空链/脏数据下容易触发异常提前退出。
从源码结构看,
safe_fuzz_targets中的键与priority_fuzz_targets的键连续衔接(14 起),而risky_fuzz_targets键位存在跳跃(55~65 与 72),说明映射键只是内部编号,与端点实际 ID 无直接对应关系。完整定义见 rpc_endpoints.cpp。
构建方式:一个源文件编译出三个 fuzzer 变体
同一份 fuzz_rpc.cpp 被编译成两个版本,区别在于是否定义SAFE宏;fuzz_zmq.cpp 则只编译单一版本。对应构建规则在 tests/fuzz/CMakeLists.txt,并且整体受OSSFUZZ开关控制:
| 目标名 | 宏定义 | 行为 |
|---|---|---|
fuzz_rpc | SAFE | 只模糊测试 Safe 类端点 |
fuzz_rpc_full | 无 | 同时包含 Safe 与 Risky 端点 |
fuzz_rpc_full_no_exceptions | CATCH_ALL_EXCEPTIONS | 无SAFE且吞掉全部异常(仅捕获runtime_error与DB_ERROR之外的兜底分支也被静默) |
fuzz_zmq | 无 | 单版本 ZMQ 模糊测试,链接rpc_pub库 |
构建时还需要LIB_FUZZING_ENGINE环境变量指定 libFuzzer 或 AFL 等引擎,<fuzzer/FuzzedDataProvider.h>头文件由 tests/fuzz/include 提供(这是 LLVM 项目的单头文件工具,用于把模糊输入字节流按需拆分为整数、字符串、布尔等类型)。此外,CMake 会为fuzz_unsafe_macro目标单独重编译 src/common/perf_timer.cpp 并定义FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION,确保性能计时器在模糊测试构建下不会干扰(并避免生产断言被误触发)。
运行流程逐层拆解
第一步:随机选择端点函数
在 fuzz_rpc.cpp 中,LLVMFuzzerTestOneInput先做输入门槛检查:单轮迭代至少需要 512 字节输入,不足则直接返回 0 跳过。随后:
- 根据是否定义
SAFE决定is_safe_mode,调用get_fuzz_targets(is_safe_mode)取回端点函数映射。非安全模式下会合并 priority + safe + risky 三张表;安全模式则只取 safe 表并对键重新编号(kv.first - 14,见 rpc_endpoints.cpp)。 - 通过
provider.ConsumeIntegralInRange<unsigned>(1, 16)随机决定本轮要发送 1~16 条 RPC 消息。 - 非安全模式下,priority 端点保证至少出现一次:先把 priority 表全部索引压入 selectors,再随机洗牌;随后再随机追加若干普通端点索引。
- 调用
el::Loggers::addFlag(...DisableApplicationAbortOnFatalLog)关闭致命日志导致的进程退出。
第二步:初始化 core 与 core_rpc_server
每次迭代都会重新创建一个core_rpc_server对象,并配齐 dummy 协议与 P2P 组件(见 fuzz_rpc.cpp):
initialise_rpc_core():创建DummyProtocol与cryptonote::core,解析命令行参数--regtest --offline --data-dir <临时目录>(临时目录为系统 temp 下的monero-fuzz-rpc),然后调用core->init(vm);initialise_rpc_server(*dummy_core, provider.ConsumeBool()):用随机布尔值决定本次迭代运行在restricted(受限)还是 unrestricted(非受限)RPC 模式——这直接对应真实节点中--restricted-rpc的语义,见 src/rpc/core_rpc_server.cpp 中m_restricted的初始化。受限模式下,get_info等端点会隐藏 alt 块数、连接数、对等节点列表等敏感信息(core_rpc_server.cpp)。
初始化函数内部(initialisation.cpp)还通过模板实例化nodetool::node_server<cryptonote::t_cryptonote_protocol_handler<cryptonote::core>>作为 dummy P2P 节点,core_rpc_server的构造函数正是接收(core&, node_server&, bool restricted)三个参数。
fuzz_zmq不需要这一步——它不依赖 core/区块链,直接创建 libzmq context(zmq_ctx_new)并实例化zmq_pub发布器。
第三步:生成随机区块与交易(FAKECHAIN)
generate_random_blocks(initialisation.cpp)是流程中最复杂的一环,其核心是FAKECHAIN(回归测试模式):--regtest标志让区块链跳过对作者签名和交易 ID 的校验逻辑,同时保留其余校验规则,从而允许伪造的区块/交易被顺利接纳。具体流程:
- 区块缓存:先用
generate_genesis_block生成创世块;再随机生成 1~4 个矿工账户(account_base::generate)、2~15 个区块(输入不足时提前终止)。每个区块按median_weight = 300000、block_weight = 100000计算奖励(get_block_reward),用construct_miner_tx构造带随机 nonce 的矿工交易,累积coins余额。区块与交易会缓存在static变量中,并通过provider.ConsumeBool()决定本轮是复用缓存还是重新生成——这保证了模糊测试过程中"链状态"的连续性。 - 交易缓存:随机生成 1~4 笔交易。每笔交易创建发送/接收账户、随机转账金额(10000 ~ 1000000000)、构造 10 个环签名输出条目(其中 1 个为真实输出、其余为假输出,
view_tag取随机值),最终通过construct_tx_and_get_tx_key构造带RangeProofPaddedBulletproof范围证明的 v2 交易,序列化后缓存。 - 上链:先
batch_start()开启数据库批量写,然后调用add_new_block逐块加入,再用handle_incoming_tx把交易送入交易池;被接纳的交易哈希存入全局cached_tx_hashes,供fuzz_get_transactions等端点使用"真实存在的交易哈希"做深度校验。若没有任何区块成功加入,则batch_stop()后跳过本轮迭代。
第四步:真正的模糊测试
selectors 全部确定后,进入执行循环(fuzz_rpc.cpp):
for (unsigned selector : selectors) { try { fuzz_targetsselector; } catch (const std::runtime_error&) { // 已知的 monero runtime_error } catch (const cryptonote::DB_ERROR& e) { // 随机值触发内部区块链 DB 检查时的已知错误 } }每个端点函数(见 rpc_endpoints.cpp)的模式高度统一:声明对应的COMMAND_RPC_*::request与response结构体(定义于 src/rpc/core_rpc_server_commands_defs.h),用FuzzedDataProvider生成随机字段填充请求,然后直接调用core_rpc_server上的on_*处理函数。以fuzz_get_blocks为例:随机生成 0~16 个区块哈希、随机start_height、max_block_count、prune等标志位,再调用rpc.on_get_blocks(req, res, &ctx)。rpc_endpoints.cpp顶部定义了共享的connection_context ctx与epee::json_rpc::error error_resp,模拟真实 HTTP 连接上下文与 JSON-RPC 错误通道。
每轮迭代结束时调用get_db().batch_stop()关闭批量写入,然后返回 0 进入下一轮。
异常处理策略:让 fuzzer 只对真正的崩溃负责
模糊测试的输入完全随机,必然触发大量业务层"预期中的"失败路径。Monero 在三个层面做了隔离:
- 已知异常静默:
std::runtime_error与cryptonote::DB_ERROR(定义于 src/lmdb/error.h)被显式捕获并忽略,因为随机值触发内部 DB 检查报错是正常现象; - 全量兜底:
fuzz_rpc_full_no_exceptions变体通过CATCH_ALL_EXCEPTIONS宏启用catch (...)兜底,把一切异常都静默掉,用于区分"异常路径"与"真正未处理错误"; - 致命日志抑制:
DisableApplicationAbortOnFatalLog避免 easylogging++ 的致命日志直接 abort 进程。
这样一来,只有未被捕获的崩溃(segfault、断言失败、越界等)才会被 libFuzzer/AFL 报告为 bug,信噪比大幅提升。
ZMQ 模糊测试:fuzz_zmq 的独立路径
zmq_endpoints.cpp 定义了 4 个 ZMQ 发布端点 fuzz 函数:
| 键 | 函数 | 作用 |
|---|---|---|
| 0 | fuzz_sub_request | 随机订阅字符串调用pub.sub_request |
| 1 | fuzz_send_chain_main | 随机高度 + 0~4 个随机区块调用pub.send_chain_main |
| 2 | fuzz_send_miner_data | 随机主版本、高度、难度、median weight、coins 与 0~3 条 backlog 条目调用pub.send_miner_data |
| 3 | fuzz_send_txpool_add | 构造 0~3 个txpool_event(随机 blob 尝试解析为交易)调用pub.send_txpool_add |
fuzz_zmq.cpp的最小输入门槛为 64 字节;每轮随机发送 1~8 条 ZMQ 消息,使用独立于 RPC 的zmq_targets映射表,不经过 core/区块链初始化。其被 fuzz 的对象是 src/rpc/zmq_pub.h 中的zmq_pub发布器——模糊测试目标从"请求处理"扩展到了"发布消息的构造与序列化"。
如何运行
在配置了OSSFUZZ的构建环境中(CMake 开关开启,且提供LIB_FUZZING_ENGINE),编译后在build/tests下得到fuzz_rpc、fuzz_rpc_full、fuzz_rpc_full_no_exceptions、fuzz_zmq四个可执行文件,配合 libFuzzer 即可运行,例如:
# 安全模式(仅 Safe 端点) ./fuzz_rpc -runs=10000 corpus_rpc # 完整模式(Safe + Risky,priority 每轮必跑) ./fuzz_rpc_full -runs=10000 corpus_rpc_full # ZMQ 发布器模糊测试 ./fuzz_zmq -runs=10000 corpus_zmq-runs之外的选项(如-max_len、-jobs)均遵循 libFuzzer 通用约定;也可用 AFL 等引擎替换。仓库中 contrib/fuzz_testing/fuzz.sh 提供了通用模糊测试辅助脚本,可作参考。
小结
Monero 的 RPC 模糊测试框架通过"跳过传输层、直连 handler、FAKECHAIN 伪造链、三类端点分级编排"四板斧,把有限模糊测试输入的能量集中释放在 RPC 业务逻辑上,并以fuzz_rpc/fuzz_rpc_full/fuzz_rpc_full_no_exceptions/fuzz_zmq四种变体覆盖不同风险偏好与异常策略组合。其设计思路与源码实现(fuzz_rpc.cpp、initialisation.cpp、rpc_endpoints.cpp、zmq_endpoints.cpp)对其他大型 C++ 项目做 RPC/API 层模糊测试具有直接借鉴价值——尤其是"用宏开关控制端点集合""静态缓存链上数据提升迭代效率""显式区分已知异常与真崩溃"这三个工程细节。
- 区块链
- 金融科技
【免费下载链接】monero
Monero: the secure, private, untraceable cryptocurrency
相关推荐
浏览器模糊测试完全指南:Awesome-Fuzzing中的BFuzz框架详解
浏览器模糊测试是现代安全研究中的重要技术,能够有效发现浏览器中的各种安全问题。在Awesome Fuzzing项目中的BFuzz框架是一个专门针对浏览器设计的输
文档教程网络安全测试告别磁盘空间危机:Czkawka/Krokiet开源清理工具全攻略
告别磁盘空间危机:Czkawka/Krokiet开源清理工具全攻略 你是否经常遇到磁盘空间不足的烦恼?照片、视频、文档重复堆积,临时文件占用宝贵空间,相似图片占
桌面应用.NET Runtime 模糊测试实战:深入解析基于 SharpFuzz 与 libFuzzer 的库级 Fuzzing 框架
.NET Runtime 模糊测试实战:深入解析基于 SharpFuzz 与 libFuzzer 的库级 Fuzzing 框架 导读 本文以 dotnet/ru
语言运行时标准库JIT编译编译器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考