Bitcoin Core 29.4 维护版深度解析:chainstate 数据库定期压缩与一批稳定性修复
本文基于本仓库
doc/release-notes/release-notes-29.4.md发布说明,结合src/下实现源码,逐项拆解 Bitcoin Core 29.4 这一维护版本的核心变更:链状态(chainstate)数据库的“反复重写大量数据”缺陷如何被修复、LevelDB 的 seek compaction 为何被禁用、以及钱包、构建、CI 与文档层面的多项修正。读完你既能掌握 29.4 的升级路径与兼容性边界,也能从源码层面理解 UTXO 集落盘与压缩的真实工作机制。
版本总览:这是一个“修内功”的维护版
Bitcoin Core 29.4 属于 29.x 系列的功能性维护版本,官方发布说明(即仓库中的 doc/release-notes/release-notes-29.4.md)明确指出:本次发布包含各种缺陷修复与性能改进,以及更新的翻译。本仓库根目录的 README.md 表明其定位为 Bitcoin Core 的 integration/staging tree,29.4 的改动正对应同步进主线的这些合并请求。
从补丁分布看,29.4 的变更集中在:
- Validation(验证):2 个补丁,其中就包含本次最大的看点——chainstate 定期压缩;
- Leveldb:禁用 seek compaction(1 个上游仓库补丁);
- Net / Wallet / Build / Test / Doc / CI / Misc:各 1~4 个小补丁。
整个版本没有引入新的共识规则或 RPC 行为变化,属于典型的“稳定性 + 工程质量”发布。以下按“能直接影响你节点磁盘 I/O 的核心变更 → 其余分项修复”顺序展开。
如何升级到 29.4
发布说明给出了标准的三平台升级流程,保持与既有维护版一致:
- 先彻底关停旧版本节点,并等待进程完全退出。由于需要把内存中的脏缓存刷盘、可能进行数据目录迁移,某些情况下关闭过程可能持续几分钟,切勿在中途强行结束进程。
- Windows:直接运行安装程序(installer)覆盖安装。
- macOS:将新的
/Applications/Bitcoin-Qt覆盖到旧程序。 - Linux:覆盖
bitcoind/bitcoin-qt可执行文件即可。
两点补充说明(与发布说明一致):
- 支持从已到达 EOL(生命周期结束)的旧版本直接升级,但若数据目录需要迁移,首次启动可能耗时较长;
- 旧钱包版本在一般情况下仍然受支持,即老钱包数据文件无需特殊转换即可被 29.4 加载。
兼容性:受支持与测试的操作系统
发布说明声明的支持边界为:
| 系统 | 版本要求 |
|---|---|
| Linux | Kernel 3.17+ |
| macOS | macOS 13+ |
| Windows | Windows 10+ |
绝大多数其他类 Unix 系统理论上可运行,但官方测试频率较低;官方明确不建议在不支持的系统上运行 Bitcoin Core。对于运行节点的生产环境,建议优先匹配上表。构建细节可参考本仓库 INSTALL.md 及各平台独立指南(如 doc/build-freebsd.md)。
核心看点:修复 chainstate 数据库“反复重写自身大块数据”
发布说明在 Notable changes 中给出了一句关键描述:
本版本修复了一个问题:chainstate 数据库在正常运行时反复重写自身的大块数据,导致过量的磁盘读写。
这句话直接点出了 29.4 最重要的性能修复。它由三个 PR 协同完成,下面从源码层面还原其来龙去脉。
为什么会出现“反复重写”?——LevelDB 的 seek compaction 机制
chainstate(UTXO 集)与区块、交易索引一样,底层由嵌入的 LevelDB 存储。LevelDB 的 compaction(压缩)分为两类:
- size compaction(大小压缩):当某层文件总大小超过阈值时触发,把低层小文件合并成大文件,属于写入驱动的常规维护;
- seek compaction(寻道压缩):当某层文件因随机读(seek)次数累积到一定阈值时,主动重写该文件,把热点数据提升到更易读的位置。
本仓库内嵌 LevelDB 的 src/leveldb/db/version_set.cc 保留了相关注释与逻辑:
// conservative and allow approximately one seek for every 16KB
// of data before triggering a compaction.
//
// Note: seek compactions are disabled. See Version::UpdateStats.
f->allowed_seeks = static_cast<int>((f->file_size / 16384U));
if (f->allowed_seeks < 100) f->allowed_seeks = 100;
注释中 "seek compactions are disabled. See Version::UpdateStats" 即对应发布说明中的 #61 (bitcoin-core/leveldb): Disable seek compaction。其风险在于:UTXO 集是典型的“大量随机读取 + 少量写入”负载,如果高频的随机查询反复命中同一批 SSTable 文件,seek compaction 会不断把这些文件重写到新的层级布局,造成发布说明所描述的“normal operation 期间过量磁盘读写”。
与之配套的两个回归测试也写明了行为契约:
- src/leveldb/db/db_test.cc 中的
GetDoesNotTriggerSeekCompaction:seek compaction 在此 fork 中禁用,因此重复读取不得改变层级布局,而手动压缩仍须正常工作; - src/leveldb/db/autocompact_test.cc:连续读取 100 次后,被读区间的文件大小不得缩小——即读操作永远不会调度压缩。
也就是说:29.4 起,LevelDB 层的“读导致写”这一自我放大行为被彻底掐断。
配套的正向维护:chainstate 按概率定期压缩
仅仅关掉 seek compaction 还不够——UTXO 集中长期失效的数据仍需要一种受控的回收机制,这正是 PR #35465 coins: compact chainstate regularly 的职责:把“压缩时机”从 LevelDB 内部的隐式触发,改为 Bitcoin Core 验证层显式、有节制地调度。
在 src/validation.cpp 中可以看到决定是否压缩的判定函数:
// Return whether the completed full flush should compact chainstate
static bool ShouldCompactChainstate(bool in_ibd)
{
static constexpr uint32_t flush_ratio{320}; // Roughly every 2 weeks with hourly flushes
return !in_ibd && FastRandomContext().randrange(flush_ratio) == 0;
}
关键实现事实:
- 压缩只在一次完整的全量落盘(full flush)结束后才可能发生,避免与常规写入争抢;
- 采用 1/320 的随机概率(
randrange(flush_ratio) == 0),若按约每小时一次的 flush 频率估算,平均大约每两周触发一次整库压缩; - 同步阶段(Initial Block Download,IBD)中不触发——刚同步完的 chainstate 本身布局较新,无需压缩,也避免拖慢同步。
调度点在 FlushStateToDisk 的全量 flush 分支中(src/validation.cpp):
if (full_flush_completed) {
...
if (!m_chainman.m_interrupt && ShouldCompactChainstate(m_chainman.IsInitialBlockDownload())) {
try {
CoinsDB().CompactFullAsync();
} catch (const std::exception& e) {
LogWarning("Failed to start chainstate compaction (%s)", e.what());
}
}
}
后台异步压缩的实现:CompactFullAsync
真正执行压缩的是 CCoinsViewDB::CompactFullAsync()(src/txdb.cpp)。它被要求在持有 cs_main 时调用,内部通过 std::async 在一次性后台线程中执行,避免阻塞主验证线程:
m_compaction = std::async(std::launch::async, [this] {
try {
util::ThreadRename("utxocompact");
LOCK(m_db_mutex);
LogDebug(BCLog::COINDB, "Starting chainstate compaction of %s", ...);
m_db->CompactFull();
...
} catch (const std::exception& e) {
LogWarning("Failed chainstate compaction (%s)", e.what());
}
}).share();
几个值得注意的工程细节:
- 线程被命名为
utxocompact,方便运维通过线程名定位; - 内部加
m_db_mutex与 src/txdb.h 声明的缓存重分配互斥,防止压缩期间ResizeCache()替换底层CDBWrapper造成竞态; - 返回
std::shared_future<void>,多个调用方可共享等待。若上一次压缩尚未结束,直接复用同一个 future(m_compaction),不会叠加多个压缩任务; - 析构函数中若检测到后台压缩未完成,会打印
Waiting for background chainstate compaction of ...并阻塞等待其结束,保证正常退出时数据一致(src/txdb.cpp)。
最底层调用 CompactFull()(src/dbwrapper.cpp)等价于对整个键空间的 CompactRange(nullptr, nullptr):
void CDBWrapper::CompactFull() { DBContext().pdb->CompactRange(nullptr, nullptr); }
用测试“看见”压缩效果
仓库的单元测试 src/test/coins_tests.cpp(coins_db_leveldb_layout)直观地验证了压缩的行为:它通过 LevelDB 属性 leveldb.num-files-at-level2 统计第二层文件数,先断言初始为 0,然后执行
WITH_LOCK(::cs_main, return base.CompactFullAsync()).wait();
BOOST_CHECK_EQUAL(level2_files(base), 1);
——一次完整的后台压缩后,第二层恰好收敛为 1 个文件,并且其中的 coin 与 best block 数据完好可读。这从测试侧印证了“定期整库压缩能把散落数据合并为紧凑布局”的效果。
如果你希望对节点施加一次即时压缩,可以关注 src/dbwrapper.cpp 中由 DBParams.options.force_compact 控制的路径——打开数据库时即执行一次全量压缩(对应相关启动选项/配置),属于运维侧的可选手段。
小结:29.4 对磁盘 I/O 的实际影响
把以上三点拼起来,29.4 修复 chainstate 异常磁盘写放大的完整链路是:
- LevelDB 层面禁用 seek compaction(
#61),杜绝“读热点 → 重写文件”的自我放大循环,这是消除“反复重写大块数据”的关键; - 验证层引入 1/320 概率、非 IBD 期间触发的显式定期全量压缩(
#35465),在 flush 完成后的后台线程中执行,保证 UTXO 存储长期保持紧凑; - 整套行为被测试锁定:读不改变层级(src/leveldb/db/autocompact_test.cc)、压缩收敛层级布局(src/test/coins_tests.cpp)。
从源码结构可以推断,受益面最大的是长期运行、持续处理随机 UTXO 查询的主网全节点:其运行期磁盘写入应明显下降,而存储布局仍会由周期压缩保持健康。
Validation:#35209 修正预计算交易数据的生命周期
第二个 Validation 补丁 #35209 validation: correct lifetime of precomputed tx data 属于内存安全/正确性修复,与本仓库 29.4 代码中 src/validation.cpp 的实现直接对应:
CBlockUndo blockundo;
// Precomputed transaction data pointers must not be invalidated
// until after `control` has run the script checks (potentially
// in multiple threads). Preallocate the vector size so a new allocation
// doesn't invalidate pointers into the vector, and keep txsdata in scope
// for as long as `control`.
std::vector<PrecomputedTransactionData> txsdata(block.vtx.size());
std::optional<CCheckQueueControl<CScriptCheck>> control;
if (auto& queue = m_chainman.GetCheckQueue(); queue.HasThreads() && fScriptChecks) control.emplace(queue);
理解这个补丁需要先了解 PrecomputedTransactionData:它是签名/脚本校验的“预计算数据”(BIP341 taproot 所需的 hashPrevouts/hashAmounts 等单次或双重 SHA256 中间量,定义见 src/script/interpreter.h),在校验同一笔交易的多个输入时可复用,避免重复计算哈希。
修复的实质是生命周期(lifetime)问题:
ConnectBlock会把各笔交易的预计算数据指针交给CScriptCheck(定义于 src/validation.h,持有PrecomputedTransactionData* txdata指针),脚本检查可能在CCheckQueueControl管理下的多线程工作队列中并发执行;- 若保存这些对象的容器在脚本检查尚未结束时就因扩容(reallocation)而移动了元素,指向旧位置的指针就会悬空——这是典型的使用已失效指针(use-after-move/use-after-free 类)隐患;
- 修复方式是:按区块交易数预先分配好容量(
std::vector<PrecomputedTransactionData> txsdata(block.vtx.size());),确保后续写入元素时 vector 不再触发重新分配,同时让txsdata的生命周期覆盖control运行脚本检查的整个过程。
从代码层面看,这一改动消除了并发脚本校验路径上对容器地址稳定性的依赖,属于保障长稳运行的正确性加固。相关的脚本缓存校验行为可进一步参考 src/test/txvalidationcache_tests.cpp,其中同样以栈上 PrecomputedTransactionData 贯穿多轮 CheckInputScripts 调用。
Net:#34093 修复默认网关查询的编译告警
#34093 netif: fix compilation warning in QueryDefaultGatewayImpl() 对应 src/common/netif.cpp 中的默认网关探测实现。该文件按平台分别实现:
- Linux(含 IPv4/IPv6,src/common/netif.cpp 起):通过
AF_NETLINK的NETLINK_ROUTEsocket 发起RTM_GETROUTE查询,注意代码中对 Linux 与 FreeBSD 的NLM_F_DUMP、RTM_F_PREFIX标志做了条件编译区分; - Windows(src/common/netif.cpp 起):使用
GetBestRoute2查询到默认网关的最佳路由; - macOS(src/common/netif.cpp 起):通过
sysctl读取PF_ROUTE路由表并遍历rt_msghdr消息寻找RTAX_GATEWAY; - 其他平台则落到返回
std::nullopt的 dummy 实现(src/common/netif.cpp)。
该函数服务于出站连接的默认出口探测(用于确定是否使用代理等场景),本补丁仅消除特定平台编译时的告警,不影响行为语义。
Wallet:#35228 估算输入体积时使用真实 outpoint
#35228 wallet: use outpoint when estimating input size 改善了钱包在构造/预估交易时的输入体积(进而影响手续费)估算精度。其代码落点在 src/wallet/spend.cpp:
int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoint,
const SigningProvider* provider, bool can_grind_r,
const CCoinControl* coin_control)
{
if (!provider) return -1;
if (const auto desc = InferDescriptor(txout.scriptPubKey, *provider)) {
if (const auto weight = MaxInputWeight(*desc, CTxIn{outpoint}, coin_control, true, can_grind_r)) {
return static_cast<int>(GetVirtualTransactionSize(*weight, 0, 0));
}
}
return -1;
}
核心变化是:把具体花费的 COutPoint 传入 MaxInputWeight,使“最大输入体积”的推导能结合该输入真实的 outpoint 信息(例如基于描述符推断出的输入构造),而不是仅凭脚本模板做泛化估计。旧路径在估算时未能利用 outpoint 细节。
在预选输入的处理路径上(src/wallet/spend.cpp),外部输入也会携带 outpoint 走带完整参数的估算分支:
if (input_bytes == -1) {
input_bytes = CalculateMaximumSignedInputSize(txout, outpoint,
&coin_control.m_external_provider, can_grind_r, &coin_control);
}
从代码结构可以推断,这一改动提升了 fee bump / 找零计算前“预估输入字节数”的准确性,从而让手续费估算更贴近最终签名后交易的实际情况。若某些输入因无法求解而估算失败(返回 -1),上层会报出 Not solvable pre-selected input ... 之类的错误(src/wallet/spend.cpp),属于既有行为。
Build:两项构建系统修正
#34228:gen_id 中取消继承 SOURCE_DATE_EPOCH
依赖构建脚本 depends/gen_id 中,在输出工具链 ID 前显式清除了环境变量:
# Unset SOURCE_DATE_EPOCH to prevent it from leaking into tool
# outputs and to maximize reuse of the built package cache.
unset SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH 是 GNU 可复现构建标准中用于固定时间戳的环境变量;若其值渗入某些工具(例如 gcc 的 __DATE__/__TIME__ 或压缩工具时间戳)的输出,会导致同一工具链在不同时间生成的内容 ID 漂移。此改动通过 unset 它,一方面避免 ID 被无关的时间因素污染,另一方面最大化 depends 预构建包缓存的复用命中率,进而加速构建与 CI。
#34848:CMake 迁移废弃的 SQLite3 target
29.x 系列已全面转向 CMake 构建体系(见根目录 CMakeLists.txt 与 CMakePresets.json)。本补丁在 CMake 侧迁离了新版 CMake 中已废弃的 SQLite3 target 用法,改为使用新的引用方式,以适配更新的 CMake 版本并消除 deprecation 告警,避免未来 CMake 升级导致构建失败。该改动不影响 -DWITH_SQLITE 之类的既有构建开关语义。
Test:#34918 清理 fuzz 中未使用的全局指针
#34918 fuzz: [refactor] Remove unused g_setup pointers 是一次纯重构:删除 fuzz 目标中不再使用的 g_setup 全局指针变量。fuzz 构建与运行入口可参考 ci/lint/requirements.txt 与 test/fuzz/coins_view.cpp(该文件里 CCoinsViewDB::CompactFullAsync 也出现在 fuzz 操作的候选动作中),此类清理旨在减少全局状态与未使用变量,提升 fuzz 代码的可维护性与覆盖率工具的准确性,不产生行为变化。
Doc:四处文档修正
本版本打包了四个文档层面的修正,均不影响运行行为,但值得部署与开发人员留意:
| PR | 内容 | 影响 |
|---|---|---|
| #34510 | 修复 bpftrace 安装文档链接失效 | 涉及 contrib/tracing/README.md 一带的跟踪工具指引可读性 |
| #34561 | 钱包 RPC 手册页中补全缺失的 fee_rate 参数示例 |
修正 send*/bumpfee 等 RPC 文档示例的可执行性 |
| #34671 | 更新 Debian/Ubuntu 下的 Guix 安装指引 | 对应 contrib/guix/INSTALL.md,涉及 contrib/guix 目录下的构建流程 |
| #35283 | 在 BSD 构建指南中提示需 -DWITH_ZMQ=ON |
已体现于 doc/build-freebsd.md:要启用 ZeroMQ 通知,需安装 libzmq4 pkgconf 并在配置时传入 -DWITH_ZMQ=ON |
其中 #35283 对 BSD 用户有实操价值:ZeroMQ 通知不是默认编译进构建的,必须显式 -DWITH_ZMQ=ON。ZMQ 消息协议的整体用法可参见 doc/zmq.md 与 contrib/zmq/zmq_sub.py。
CI:三处流水线调整
#35202 ci: restore sockets in i686, no IPC job:在 i686 且关闭 IPC 的 CI job 中恢复 socket 相关测试覆盖;#35378 ci: switch runners from cirrus to warpbuild:把 CI runner 从 Cirrus 迁移到 WarpBuild(基础设施调整);#35408 ci: 35378 followups:上一迁移的后续收尾补丁。
这些 CI 脚本改动分布在 ci/test/(如 ci/test/00_setup_env_i686_no_ipc.sh)下,属于工程维护范畴,与运行时行为无关。
Misc:#35175 兼容 Boost ≥ 1.91 的编译修复
#35175 multi_index: fix compilation failure with boost >= 1.91 修复了 Boost 1.91 及以上版本中 boost::multi_index 相关 API 变动引起的编译失败。Bitcoin Core 大量依赖 Boost 容器(如交易池、addrman 等模块中的 multi_index 结构),当发行版升级 Boost 后此补丁保证 29.4 仍可正常编译。
Credits:贡献者
29.4 由以下直接贡献者合入(依发布说明):andrewtoth、Cory Fields、Daniel Pfeifer、darosior、fanquake、Hennadii Stepanov、jayvaliya、junbyjun1238、Lőrinc、MarcoFalke、SomberNight、ToRyVand、willcl-ark,同时感谢在 Transifex 上协助翻译的所有社区成员。
升级建议与总结
综合来看,Bitcoin Core 29.4 是一个“低成本、高确定性收益”的维护版:
- 建议升级:修复了 chainstate 在正常运行期的异常磁盘写放大,对长期运行的主网/测试网节点尤其有意义;无任何共识或 RPC 破坏性变化,升级风险低;
- 升级方式:按上文三平台流程,先完全关停旧进程再替换二进制,首次启动如遇数据迁移请耐心等待;
- 验证要点:升级后可在日志中观察
utxocompact线程相关的Starting chainstate compaction ... / Finished chainstate compaction ...(对应 src/txdb.cpp),确认周期压缩按约两周一次的节奏正常工作。
如需从源码深入后续版本差异,可在本仓库 doc/release-notes/ 目录下对照 29.0、30.0、31.0 等相邻版本发布说明,观察该项目在链状态存储与验证性能上的持续演进脉络。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00