Bitcoin Core 交易索引(txindex)新存储格式:磁盘占用减半的升级原理与重建指南
2026/9/7 8:05:29 网站建设 项目流程

Bitcoin Core 交易索引(txindex)新存储格式:磁盘占用减半的升级原理与重建指南

【免费下载链接】bitcoinBitcoin Core integration/staging tree项目地址: https://gitcode.com/GitHub_Trending/bi/bitcoin

本文基于 Bitcoin Core 发布说明 doc/release-notes-35531.md(对应 PR #35531)展开,讲清-txindex交易索引在磁盘存储格式上的新优化:为什么重建后的索引占用空间不到原来的一半、新旧格式如何共存与回退,以及存量节点如何通过删除indexes/txindex目录安全重建以回收磁盘空间,并借助getindexinfoRPC 监控重建进度。

一、背景:-txindex的作用、默认值与运行约束

-txindex用于让节点维护一份“全交易索引”,使任意历史交易都能按 txid 被快速检索。该参数在 src/init.cpp 中注册:

argsman.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ...);

从源码结构看,有三个值得注意的事实:

  1. 默认关闭。src/index/txindex.h 中inline constexpr bool DEFAULT_TXINDEX{false};,即不开启时不会建立交易索引,getrawtransaction等依赖索引的 RPC 也不可用。
  2. 与剪枝互斥。src/init.cpp 中,-prune-txindex同时启用会直接报InitError("Prune mode is incompatible with -txindex."),因为按 txid 查历史交易必须随时能读回对应区块文件。
  3. 索引数据库路径固定。src/index/txindex.cpp 中:
    static fs::path TxIndexDBPath() { return gArgs.GetDataDirNet() / "indexes" / "txindex"; }

    即索引存放在<datadir>/indexes/txindex(LevelDB 数据库)。这正是发布说明中要求手动删除的目录。

  4. 缓存分配。索引的 LevelDB 缓存占总数据库缓存的 10%,上限 1 GiB,见 src/node/caches.cpp:
    index_sizes.tx_index = std::min(total_cache * 10 / 100, args.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? MAX_TX_INDEX_CACHE : 0);

    注释解释了理由:txindex 主要服务getrawtransaction这类“全链范围、低重复”的点查,因此分配比例高于 filter 索引。

二、#35531 变更内容:重建后索引占用不足一半

发布说明的核心结论是:

交易索引(-txindex)现在在磁盘上存储的数据更少;一个完全重建的索引占用空间不到原来的一半。索引保持向后兼容,因此现有用户除非重建索引,否则不会看到空间节省。

操作层面的完整说明如下(这是存量节点回收空间的标准流程):

  1. 停止节点
  2. 删除<datadir>/indexes/txindex目录(注意:只删这个目录,不要动blockschainstate等);
  3. 重新启动节点,txindex 会从已有区块数据中重建。根据硬件不同,重建可能需要数小时;
  4. getindexinfoRPC 监控进度。该 RPC 定义于 src/rpc/node.cpp,可查询全部索引或按名称过滤,例如:
    bitcoin-cli getindexinfo bitcoin-cli getindexinfo txindex

    返回结果中包含索引的当前同步高度与best_block_hash,据此可以判断重建是否追平。

兼容性要点同样重要:

  • 新格式索引向后兼容:旧版本仍可运行并继续写入旧格式条目;
  • 重建后的索引无法被更早版本读取,回退(downgrade)会迫使旧版本按老格式再重建一遍;
  • 因此若打算永久回退到旧版本,请先删除<datadir>/indexes/txindex目录,因为旧版本不会回收新格式条目占用的空间。

节点层面也提供了自动提示:当检测到数据库内混有旧格式(legacy)条目时,启动日志会输出(见 src/index/txindex.cpp):

txindex contains entries in the legacy format, which uses excessive disk space. To reclaim disk space, stop the node, delete <datadir>/indexes/txindex and restart to rebuild the index.

这为运维人员提供了明确信号:看到这条日志,即可按上面流程重建。

三、旧格式为什么大:完整 txid 作为键 + CDiskTxPos 值

旧(legacy)格式下,每一笔交易在 LevelDB 中占一条记录:

  • 't'前缀(0x74)+ 完整 32 字节 txid,共 33 字节,定义见 src/index/txindex_key.h 的LegacyTxKey
  • CDiskTxPos,结构为区块文件号nFile(varint)+ 区块在文件中的位置nPos(varint)+ 交易在区块内的偏移nTxOffset(varint),定义见 src/index/disktxpos.h:
struct CDiskTxPos : public FlatFilePos { uint32_t nTxOffset{0}; // after header SERIALIZE_METHODS(CDiskTxPos, obj) { READWRITE(AsBase<FlatFilePos>(obj), VARINT(obj.nTxOffset)); } ... };

也就是说,每条交易记录 = 33 字节键 + 约 3~5 字节以上(三个 varint 各占 1~5 字节)的值。全链约 8 亿笔交易时,仅 txid 键就贡献约 26 GB 的键空间,这是旧索引体积大的主因。

四、新格式:SipHash 5 字节前缀键 + 位置编码进键内

#35531 引入的新数据库布局完整定义在 src/index/txindex_key.h 的注释中:

Database layout: ['x', hash prefix, block seq, tx offset] -> (empty) ['s', block seq] -> block hash ['h', block hash] -> block seq ["next_block_seq"] -> next block seq to assign ["txid_hash_salt"] -> txid hasher salt ["best_block_v2"] -> current sync locator ['t', txid] -> legacy CDiskTxPos ['B'] -> legacy sync locator

各条目的含义:

用途
'x' + 5 字节前缀 + 区块序号 + 3 字节偏移每笔交易一条,位置直接编码在键内
's' + block_seq区块哈希由序号反查区块哈希
'h' + 区块哈希block_seq由哈希查序号(去重用)
next_block_seq下一个可分配的序号单调分配,区块重连后保持不变
txid_hash_salt128 位随机盐初始化时随机生成,用于 SipHash1-3 派生前缀
best_block_v2同步定位点(locator)记录索引已同步到的位置
't' + txid(legacy)CDiskTxPos兼容旧数据

新交易键如何变小。前缀由对 txid 做 SipHash1-3 再取高 5 字节得到(盐持久化在txid_hash_salt,见 src/index/txindex.cpp 的ReadOrCreateTxidHasher):

constexpr int HASH_PREFIX_SIZE{5}; using TxHashKeyPrefix = uint64_t; inline TxHashKeyPrefix CreateKeyPrefix(const SipHasher13UJ& hasher, const Txid& txid) { return hasher.Hash(txid.ToUint256()) >> (8 * (sizeof(TxHashKeyPrefix) - HASH_PREFIX_SIZE)); }

(见 src/index/txindex_key.h)

位置部分BlockTxPosition由变长整数block_seq+3 字节大端tx_offset_in_block组成(见 src/index/txindex_key.h):

//! tx_offset is encoded in 3-byte big-endian integer. //! This can hold up to 16,777,216, which is >4x the maximum 4 million block weight position static constexpr uint32_t TX_OFFSET_SIZE{3}; static_assert(MAX_BLOCK_SERIALIZED_SIZE <= BigEndianFormatter<TX_OFFSET_SIZE>::MAX);

固定 3 字节编码偏移的原因很直接:区块最大序列化大小受 400 万权重上限约束,3 字节能表示 16,777,216,留有 4 倍以上余量;相比旧格式三个 varint,空间更稳定且省去变长开销。

体积对比:旧条目 = 33 字节键 + 约 3~5 字节值;新条目 = 1 + 5 + 1~5 + 3 ≈ 10~14 字节键 +0 字节值(空值,EMPTY_VALUE)。每笔交易省下一半以上的字节,乘以全链交易数量,即得到“重建后不足一半空间”的效果。代价是前缀冲突:不同 txid 可能共享同一个 5 字节前缀(每 2^25 个前缀桶约 3 万笔交易),因此读路径必须做全量交易校验(见下节)。

五、写入路径:WriteTxs 如何落库

区块进入索引时调用CustomAppend,创世块因输出不可花费而被排除(src/index/txindex.cpp):

bool TxIndex::CustomAppend(const interfaces::BlockInfo& block) { // Exclude genesis block transaction because outputs are not spendable. if (block.height == 0) return true; assert(block.data); m_db->WriteTxs(block); return true; }

WriteTxs的完整逻辑(src/index/txindex.cpp):

  1. 去重:若该区块哈希已有'h'条目则直接返回。注释解释了动机——区块在重组重连或非干净关闭后重放时会再次提交,必须保持它原有的block_seq,避免产生重复条目;
  2. 分配序号:从next_block_seq读出当前序号,随后在同一个CDBBatch中原子写入'h''s'映射并递增next_block_seq
  3. 逐笔交易写键:交易偏移从区块头(80 字节)加交易计数 compact size 之后开始累加,每笔交易写一条DBKey{前缀, {block_seq, tx_offset_in_block}} -> 空值,然后tx_offset_in_block += tx->ComputeTotalSize()
uint32_t tx_offset_in_block{txindex::BLOCK_HEADER_SIZE + GetSizeOfCompactSize(block.data->vtx.size())}; for (const auto& tx : block.data->vtx) { const txindex::DBKey key{txindex::CreateKeyPrefix(m_hasher, tx->GetHash()), txindex::BlockTxPosition{block_seq, tx_offset_in_block}}; batch.Write(key, txindex::EMPTY_VALUE); tx_offset_in_block += tx->ComputeTotalSize(); } WriteBatch(batch);

六、读取路径:前缀定位 + 区块文件回放 + 前缀冲突校验

FindTxgetrawtransaction等 RPC 的底层查询入口(src/index/txindex.cpp),流程为:

  1. 用同一 salt 计算查询 txid 的 5 字节前缀,构造DBKey{prefix, {}}
  2. 迭代器Seek到该前缀起点,遍历所有同前缀条目;对每条候选,用's'条目把block_seq解析为区块哈希,再确认该区块存在于区块索引且BLOCK_HAVE_DATA(有本地区块文件);
  3. 打开区块文件,按nFile+nDataPos + tx_offset_in_block定位,反序列化候选交易并比较完整 txid,以此过滤前缀冲突的“邻居”交易;
  4. 候选优先级:活动链上的区块优先,其次按block_seq大的优先(即后连接的区块优先)。注释明确说明:“active chain candidates are attempted first, so duplicate entries in both active and stale blocks will always return the active block hash”——这保证了重组期间陈旧区块与活动区块各有一条记录时,RPC 返回的一定是活动链的区块哈希;
  5. legacy 回退:若新格式没查到且库里存在旧格式条目,则回退到FindLegacyTx(src/index/txindex.cpp),按完整 txid 键读取CDiskTxPos再定位。注释坦承这是“miss 多付一次查找的代价,以保证升级后旧条目仍可读”。

库内是否含 legacy 条目在打开数据库时就探测一次:src/index/txindex.cpp 通过CDBWrapper::HasKeyStartingWith(TxIndexDBPath(), txindex::DB_TXINDEX)扫描't'前缀,并只在存在旧条目时才启用 LevelDB 布隆过滤器——因为新格式的点查都走迭代器Seek(绕过过滤器),过滤器只对 legacy 的逐交易点查有益,注释对这一点有完整说明。

七、重建、降级与兼容性的工程细节

把发布说明的操作流程落到行为层面:

  • 为什么必须删目录而不是-reindex:重建索引依赖的是<datadir>/indexes/txindex本身被清空后按新格式重写。旧格式条目是“增量共存”的——升级后新交易写新格式,旧交易仍是't'键,磁盘占用并不下降。只有整体删除后从零重建,才能得到“不足一半”的空间收益。
  • 重建耗时:需重放全部历史区块,取决于硬件,文档给出“up to a few hours”的预期;期间节点正常同步,建议用getindexinfo txindex观察索引synced字段与best_block_hash是否追平链尖。
  • 前向兼容:重建后新格式索引对当前版本完全可用;但对更早版本而言该库不可读。若回退到旧版本运行,旧代码会按自己的逻辑重建旧格式索引(再次膨胀)。
  • 永久降级的注意事项:旧版本不会主动回收新格式条目占用的空间,所以若确定不再升级回去,应先在旧版本运行前删除indexes/txindex,让旧版本干净地重建,否则磁盘上会长期保留一份无用的新格式数据。
  • 混合期提示:升级后未重建时,新旧条目并存,启动日志会提示“legacy format, which uses excessive disk space”并给出删除路径,作为人工介入的明确信号(src/index/txindex.cpp)。

八、单元测试对编码的“钉死”

新格式的二进制编码由单元测试中的向量显式固化(src/test/txindex_tests.cpp),任何序列化改动都会立即被这些测试捕获:

constexpr struct { txindex::BlockTxPosition position; std::string_view encoded; } test_vectors[]{ {{0, 0}, "00000000"}, {{1, 2}, "01000002"}, {{10'000'000, 123}, "83e1ac0000007b"}, {{456, 3'999'999}, "82483d08ff"}, };

BlockTxPosition{1,2}编码为01 000002(1 字节 varint 序号 + 3 字节大端偏移)正是上节编码的直观印证;同一测试文件还固定了各类型前缀键的编码(如BlockSeqKey{1}编码为7301,即前缀's'=0x73),并覆盖重组、legacy 迁移与前缀冲突等场景。这说明磁盘格式是有意保持稳定的:它决定了索引能否跨版本兼容,因此用向量测试把每一字节都“钉死”。

小结

  • -txindex默认关闭、与-prune互斥,索引位于<datadir>/indexes/txindex,LevelDB 缓存占总 db 缓存的 10%(上限 1 GiB)。
  • #35531 把每笔交易的 LevelDB 记录从“32 字节 txid 键 + CDiskTxPos 值”压缩为“SipHash 5 字节前缀键 + 位置编码进键 + 空值”,重建后全索引占用不足一半。
  • 新格式向后兼容但旧版本读不了重建后的库:想回收空间就删目录重建并用getindexinfo跟踪进度;想永久降级就先删目录再回退。
  • 读路径通过“前缀 Seek + 区块文件回放 + 全 txid 校验”处理前缀冲突,并按活动链、后连接优先的次序选择候选,重组期间的正确性由该排序保证。
  • 关键实现见 src/index/txindex.cpp、src/index/txindex_key.h,编码约束由 src/test/txindex_tests.cpp 固化。

【免费下载链接】bitcoinBitcoin Core integration/staging tree项目地址: https://gitcode.com/GitHub_Trending/bi/bitcoin

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询