ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

Bitcoin Core 交易索引(txindex)新存储格式:磁盘占用减半的升级原理与重建指南 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), ...);从源码结构看有三个值得注意的事实默认关闭。src/index/txindex.h 中inline constexpr bool DEFAULT_TXINDEX{false};即不开启时不会建立交易索引getrawtransaction等依赖索引的 RPC 也不可用。与剪枝互斥。src/init.cpp 中-prune与-txindex同时启用会直接报InitError(Prune mode is incompatible with -txindex.)因为按 txid 查历史交易必须随时能读回对应区块文件。索引数据库路径固定。src/index/txindex.cpp 中static fs::path TxIndexDBPath() { return gArgs.GetDataDirNet() / indexes / txindex; }即索引存放在datadir/indexes/txindexLevelDB 数据库。这正是发布说明中要求手动删除的目录。缓存分配。索引的 LevelDB 缓存占总数据库缓存的 10%上限 1 GiB见 src/node/caches.cppindex_sizes.tx_index std::min(total_cache * 10 / 100, args.GetBoolArg(-txindex, DEFAULT_TXINDEX) ? MAX_TX_INDEX_CACHE : 0);注释解释了理由txindex 主要服务getrawtransaction这类“全链范围、低重复”的点查因此分配比例高于 filter 索引。二、#35531 变更内容重建后索引占用不足一半发布说明的核心结论是交易索引-txindex现在在磁盘上存储的数据更少一个完全重建的索引占用空间不到原来的一半。索引保持向后兼容因此现有用户除非重建索引否则不会看到空间节省。操作层面的完整说明如下这是存量节点回收空间的标准流程停止节点删除datadir/indexes/txindex目录注意只删这个目录不要动blocks、chainstate等重新启动节点txindex 会从已有区块数据中重建。根据硬件不同重建可能需要数小时用getindexinfoRPC 监控进度。该 RPC 定义于 src/rpc/node.cpp可查询全部索引或按名称过滤例如bitcoin-cli getindexinfo bitcoin-cli getindexinfo txindex返回结果中包含索引的当前同步高度与best_block_hash据此可以判断重建是否追平。兼容性要点同样重要新格式索引向后兼容旧版本仍可运行并继续写入旧格式条目但重建后的索引无法被更早版本读取回退downgrade会迫使旧版本按老格式再重建一遍因此若打算永久回退到旧版本请先删除datadir/indexes/txindex目录因为旧版本不会回收新格式条目占用的空间。节点层面也提供了自动提示当检测到数据库内混有旧格式legacy条目时启动日志会输出见 src/index/txindex.cpptxindex 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结构为区块文件号nFilevarint 区块在文件中的位置nPosvarint 交易在区块内的偏移nTxOffsetvarint定义见 src/index/disktxpos.hstruct CDiskTxPos : public FlatFilePos { uint32_t nTxOffset{0}; // after header SERIALIZE_METHODS(CDiskTxPos, obj) { READWRITE(AsBaseFlatFilePos(obj), VARINT(obj.nTxOffset)); } ... };也就是说每条交易记录 33 字节键 约 35 字节以上三个 varint 各占 15 字节的值。全链约 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 txidlegacyCDiskTxPos兼容旧数据新交易键如何变小。前缀由对 txid 做 SipHash1-3 再取高 5 字节得到盐持久化在txid_hash_salt见 src/index/txindex.cpp 的ReadOrCreateTxidHasherconstexpr 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_seq3 字节大端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 BigEndianFormatterTX_OFFSET_SIZE::MAX);固定 3 字节编码偏移的原因很直接区块最大序列化大小受 400 万权重上限约束3 字节能表示 16,777,216留有 4 倍以上余量相比旧格式三个 varint空间更稳定且省去变长开销。体积对比旧条目 33 字节键 约 35 字节值新条目 1 5 15 3 ≈ 1014 字节键 0 字节值空值EMPTY_VALUE。每笔交易省下一半以上的字节乘以全链交易数量即得到“重建后不足一半空间”的效果。代价是前缀冲突不同 txid 可能共享同一个 5 字节前缀每 2^25 个前缀桶约 3 万笔交易因此读路径必须做全量交易校验见下节。五、写入路径WriteTxs 如何落库区块进入索引时调用CustomAppend创世块因输出不可花费而被排除src/index/txindex.cppbool 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去重若该区块哈希已有h条目则直接返回。注释解释了动机——区块在重组重连或非干净关闭后重放时会再次提交必须保持它原有的block_seq避免产生重复条目分配序号从next_block_seq读出当前序号随后在同一个CDBBatch中原子写入h、s映射并递增next_block_seq逐笔交易写键交易偏移从区块头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);六、读取路径前缀定位 区块文件回放 前缀冲突校验FindTx是getrawtransaction等 RPC 的底层查询入口src/index/txindex.cpp流程为用同一 salt 计算查询 txid 的 5 字节前缀构造DBKey{prefix, {}}迭代器Seek到该前缀起点遍历所有同前缀条目对每条候选用s条目把block_seq解析为区块哈希再确认该区块存在于区块索引且BLOCK_HAVE_DATA有本地区块文件打开区块文件按nFilenDataPos tx_offset_in_block定位反序列化候选交易并比较完整 txid以此过滤前缀冲突的“邻居”交易候选优先级活动链上的区块优先其次按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 返回的一定是活动链的区块哈希legacy 回退若新格式没查到且库里存在旧格式条目则回退到FindLegacyTxsrc/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}, {{10000000, 123}, 83e1ac0000007b}, {{456, 3999999}, 82483d08ff}, };BlockTxPosition{1,2}编码为01 0000021 字节 varint 序号 3 字节大端偏移正是上节编码的直观印证同一测试文件还固定了各类型前缀键的编码如BlockSeqKey{1}编码为7301即前缀s0x73并覆盖重组、legacy 迁移与前缀冲突等场景。这说明磁盘格式是有意保持稳定的它决定了索引能否跨版本兼容因此用向量测试把每一字节都“钉死”。小结-txindex默认关闭、与-prune互斥索引位于datadir/indexes/txindexLevelDB 缓存占总 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),仅供参考
返回列表