公司动态

UTXO快照工具utxo-dump:从LevelDB导出可校验链上状态

📅 2026/9/2 3:06:55
UTXO快照工具utxo-dump:从LevelDB导出可校验链上状态
简介用于快速导出比特币 UTXO 快照的 Python 实用工具适合区块链开发者、链上数据分析人员以及希望深入研究 UTXO 模型的进阶学习者。它通过调用 bitcoind 索引与重放区块数据可在指定区块高度生成完整的 UTXO 集合便于离线分析、钱包恢复或测试网仿真。资源共 13 个文件压缩包约 344KB以 Python 脚本为主配合依赖列表、配置示例、README、License 等辅助文件脚本内部按数据读取、B128 编解码、链状态解析等模块拆分结构清晰。已有 178 人下载学习。包内附带的 vmcp.csv 与多个示例参数可支持按指定高度快照、自定义 bitcoind 路径等运行方式具备直接上手的可操作性是理解比特币 UTXO 机制和搭建本地链上分析环境的实用参考。 UTXO是比特币这类UTXO模型公链最核心的底层概念简单说就是还没被花掉的交易输出。做链上数据分析、资产审计、余额快照甚至项目方做空投都绕不开一个动作在某个区块高度把UTXO集合完整倒出来。utxo-dump这个工具干的就是这件事——扫描本地全节点的chainstate把散落在LevelDB里的UTXO记录导成一个可读、可校验的快照文件方便后续离线分析、余额聚合和跨节点一致性比对。这篇文章我把整个工具的前因后果、实现思路、实操过程和踩坑经历都写清楚适合三类人看一是刚接触链上数据开发想搞明白UTXO到底怎么存、怎么导的二是项目方要做空投资格快照不想被中心化API卡脖子三是纯粹对Bitcoin Core底层数据结构好奇的技术玩家。只要你照着往下走就能自己造出一个顺手可用的UTXO快照管线。1. UTXO快照是什么为什么每个做链上分析的人都绕不开它1.1 先说透UTXO模型和“未花费输出”比特币不记录“张三余额是多少”它只记录一堆交易输出。每一个输出都是一笔可花费的“钱”花掉之后这个输出就变成已花费同时在新的交易里生成新的输出。UTXO就是Unspent Transaction Output的缩写翻译成大白话还没被当成输入花掉的交易输出。这里有个很反直觉的点整个比特币网络的状态其实不是交易历史而是当前所有尚未花费的输出集合。交易历史只是产生和消耗UTXO的流水账真正决定“某个地址现在有多少币”的是UTXO集合里那些output的加总。所以无论你要算总流通量、做地址余额排名还是给某个协议的用户空投都需要先把UTXO集合完整搞出来。那个“完整搞出来”的动作就是UTXO快照。快照不是备份交易流水而是备份一个确定区块高度上、全节点眼中“此刻还没花掉的钱”。它是链上状态的压缩表达也是很多上层业务的起点。1.2 utxo-dump解决的实际问题Bitcoin Core节点确实内置了gettxoutsetinfo能返回UTXO数量、总金额和一个哈希校验值但它不给明细。dumptxoutset能从节点直接导出一个二进制文件但那是为节点之间同步设计的格式可读性差拿来做分析还要自己解析。utxo-dump解决的正是这个缝隙它绕过RPC层直接在chainstate的LevelDB里遍历UTXO记录输出成CSV或者JSON Lines同时把防篡改校验也做了。好处有三点输出格式可控方便导入数据库导出速度比RPC循环快得多因为它走的是存储层迭代整个过程不依赖第三方索引服务数据自主可控。在实际项目中我一般拿它做三件事一是定期生成全量余额快照存到数据仓库用于报表二是在某个区块高度冻结状态给空投和治理提供依据三是做节点之间的数据一致性检查比对快照哈希确认两个节点的链上状态没有分叉。2. 快照方案对比为什么我最终选择了utxo-dump而不是直接抄RPC2.1 主流UTXO获取方式横向对比在决定自己造轮子之前我把现成的方案都过了一遍各有各的道理但也有各自的坑。方案能拿到什么优点缺点bitcoin-cli gettxoutsetinfo统计信息一条命令搞定带哈希没有UTXO明细没法做余额聚合bitcoin-cli dumptxoutset二进制文件Core原生支持速度快格式私有二次解析成本高反复调用gettxoutset/getrawtransaction单笔交易输出能追溯历史慢到怀疑人生几百万次RPC不现实直接遍历chainstate LevelDB完整UTXO集合可控、可定制、性能最好需要处理锁和数据结构兼容如果你只是想知道链上有多少个UTXO、总供应量多少那gettxoutsetinfo就够了没必要折腾。但你要是想计算“排名前1万的地址各有多少币”、要给某个协议的LP做快照、要把UTXO数据灌进ClickHouse里做OLAP分析就必须拿到逐条明细没有第二个选择。这里面最容易被忽略的是gettxoutset这个坑。它确实能查某个指定输出的状态但你得先知道txid和vout这等于“先有答案再问问题”。想靠它枚举全量UTXO是走不通的RPC接口的定位是点查不是全表扫描。2.2 utxo-dump的设计取舍我最终选择自研这个工具核心考虑是“快照文件必须可复现、可校验”。RPC方式导出的数据很难做一致性校验而chainstate本身就是确定性的只要链没有分叉同一高度上所有节点看到的UTXO集合必须完全一致。这个确定性能转换成快照文件的确定性——两次独立导出结果应该是逐字节相同或者至少哈希一致。所以在设计utxo-dump时我定了几个原则输出顺序要稳定。不能因为LevelDB内部迭代顺序变化导致文件哈希漂移必须对记录做排序或定义明确的序列化顺序。用整数表示金额。BTC的浮点数精度问题在币圈是个老坑所有金额字段统一用“聪”为单位1 BTC 100000000 sat。自带校验。导出的同时计算一个与节点内部序列化规则对齐的哈希方便和gettxoutsetinfo的hash_serialized做交叉验证。支持断点恢复。主网UTXO集合有几千万甚至上亿条记录导出时间可能长达数小时中途断电不能白跑。这套取舍在后面实现的时候帮了大忙尤其是校验哈希它几乎成了所有后续数据任务的“定海神针”。3. 核心实现从LevelDB到快照文件3.1 UTXO在磁盘上到底长什么样Bitcoin Core的数据目录下有多个子目录其中chainstate目录存的就是当前主链的UTXO集合底层是LevelDB。LevelDB是一个Key-Value存储UTXO记录以特定的key和value编码存放在里面。不同Bitcoin Core版本的编码细节有过调整但大方向是稳定的key通常由C前缀加32字节txid加4字节vout索引组成value则是压缩后的输出数据包含金额、脚本、所在区块高度、是否coinbase等信息。chainstate存储的是当前未花费集合也就是说它天然就是最新高度的UTXO快照。这里有个关键点chainstate不保存历史UTXO集合。如果你想做的是“第800000块时的快照”但节点已经同步到第810000块光靠chainstate是不够的需要先把节点回滚到目标高度或者在节点同步到目标高度时立刻暂停导出。这一点在整条链上做“历史快照”时非常容易踩坑。3.2 导出流程与核心代码逻辑utxo-dump的核心逻辑可以拆成四步打开LevelDB、遍历所有UTXO键值对、解析出业务字段、按固定格式落盘。以下是核心遍历逻辑的伪代码我用Go的语法写因为LevelDB在Go生态里用起来最顺手func dumpUTXO(db *leveldb.DB, w io.Writer) error { iter : db.NewIterator(nil, nil) defer iter.Release() for iter.Next() { key : iter.Key() if len(key) 37 || key[0] ! C { continue } txid : hex.EncodeToString(key[1:33]) vout : binary.LittleEndian.Uint32(key[33:37]) value, script, height, coinbase : decodeOutput(iter.Value()) if _, err : fmt.Fprintf( w, %s,%d,%d,%s,%d,%t\n, txid, vout, value, hex.EncodeToString(script), height, coinbase, ); err ! nil { return err } } return iter.Error() }decodeOutput是解析value的部分需要按照节点源码里的序列化格式把压缩字段还原出来。这里有几个容易翻车的点value字段存在变长整数编码里直接读4字节大端会得到错误结果。scriptPubKey是压缩存储的前缀字节会标识脚本类型得先解压成完整hex再输出否则后面做地址转换会失败。height和coinbase标志经常被压在一个字节的高低位里逻辑运算错一位都会导致快照数据整体偏移。所以这个函数一定不能凭感觉写最好拿着Bitcoin Core源代码里的CCoins和TxOut序列化定义逐字段核对。好在网上有现成的实现可以参考但建议自己先在一个几千区块的测试网上跑通再上主网。3.3 命令行用法与参数说明工具装好之后命令行形态大致是下面这个风格utxo-dump \ --db ~/.bitcoin/chainstate \ --network mainnet \ --output utxo_$(date %Y%m%d).csv \ --verify我用到的参数并不多但每个都有讲究参数作用备注--db指定chainstate目录路径必须指向节点数据目录下的chainstate不是整个bitcoin目录--network主网/测试网/回归测试网不同网络的地址前缀不一样影响后续地址转换--output输出文件路径建议写到独立数据盘主网快照文件能到十几GB甚至更大--height声明快照高度只做标记记录到文件头不改变chainstate内容--verify边导出边计算校验哈希强烈建议加上可以跟gettxoutsetinfo比对--workers并行解析线程数不是越多越好后面会说输出文件的第一行我习惯放一个头信息记录版本、网络、导出时间和目标高度便于后续排查。数据行则是txid,vout,value_sat,script_hex,height,is_coinbase简单直观几乎任何分析工具都能直接消费。4. 完整实操记录从准备节点到校验快照4.1 导出前最重要的三件事第一件事确认节点高度和链状态。如果你要做的是“当前高度快照”先运行bitcoin-cli getblockcount记下高度再看看bitcoin-cli getblockchaininfo里verificationprogress是否为1。同步没完成就去导出出来的UTXO集合是不完整的后面所有分析都是错的。第二件事停掉节点。chainstate的LevelDB被节点进程占用时直接用工具打开会碰到文件锁就算用只读模式硬读也可能读出写到一半的脏数据。我的做法是bitcoin-cli stop等进程完全退出后再导出。一次完整导出可能需要几个小时不想让节点停这么久的话也可以复制一份chainstate目录到别处在副本上做导出原节点继续同步。第三件事检查磁盘空间和内存。UTXO集合数量和区块高度直接相关主网现阶段大约有8000万到1亿个UTXO记录CSV格式导出后可能占用20GB以上的空间。内存方面LevelDB默认缓存会吃掉不少内存在2G内存的小机器上导出时很容易被系统杀掉建议先给工具加个--cache-size参数控制缓存大小必要时用swap兜底。4.2 执行导出和结果验证我平时会用一个两段式的流程# 第一阶段停节点、导出 bitcoin-cli stop sleep 10 utxo-dump \ --db ~/.bitcoin/chainstate \ --network mainnet \ --output /data/snapshot/utxo_mainnet.csv \ --verify # 第二阶段重启节点 bitcoind --daemon导出过程中工具会打印进度条每处理10万条记录刷新一次同时统计当前UTXO数量和总金额。导出完成后工具会输出一个snapshot_hash。这个哈希非常重要它可以和节点内置的gettxoutsetinfo返回的hash_serialized_3做比对。但有个细节gettxoutsetinfo只能在节点运行时查询而导出时节点是停着的所以正确的比对姿势是导出前先记录一份gettxoutsetinfo输出等导出完成后拿snapshot_hash和它比对。两个值一致说明快照文件完整描述了节点当时看到的UTXO状态。如果发现对不上优先怀疑两件事一是导出期间节点没停干净有新区块写入导致状态漂移二是utxo-dump的序列化顺序和节点内部计算哈希的顺序不一致。顺序问题不影响快照本身的正确性但会让哈希校验失败。解决方法是让工具内部先按节点定义的“coins order”排序再算哈希或者干脆去掉哈希校验改用文件行数、总金额等粗粒度指标做一致性检查。4.3 常见问题速查表我把实际操作中碰到过的问题整理成了一张速查表按概率排序现象可能原因解决办法Resource temporarily lockedbitcoind还在运行bitcoin-cli stop后重试或用副本目录导出过程中进程被kill内存不足LevelDB缓存过大调小--cache-size临时加swap快照文件行数和节点统计不一致导出前节点未停止产生新区块停止节点重新导出务必等进程退出解析出来的金额和实际UTXO对不上变长整数解析错误版本不兼容核对Core源码中的value编码格式更新工具脚本hex转地址后全部错误输出的是压缩脚本没做解压确认解码层优先解压scriptPubKey快照文件太大普通编辑器打不开CSV被当成Excel文件打开用wc -l、head、tail或导入数据库观察这里最想提醒的是导出过程中不要手贱去跑bitcoind查询命令。有一次我为了确认导出进度在另一个终端启动了bitcoin-cli getblockcount结果bitcoind进程被系统自动拉起LevelDB状态开始变化整个导出白跑。现在我的流程里停节点之后第一件事就是确认进程彻底消失再开始导出。4.4 性能调优笔记UTXO导出是典型的IO密集型任务优化空间主要在三块。第一块是读。LevelDB遍历本身顺序读为主对机械硬盘不友好建议放在SSD上跑。如果你的机器是4核8G可以把--workers调到2到4但不要盲目调高。当解析线程数量超过CPU核数时收益快速递减反而会因为上下文切换和内存竞争拖慢速度。第二块是写。CSV的输出如果一条一条fmt.Fprintf到标准输出再重定向到文件缓冲开销会很大。我实际测试下来用带缓冲的writer每处理1000条记录刷一次盘性能和稳定性最好。太小会导致频繁io太大则会导致掉电时丢失太多进度。第三块是校验。如果不开--verify导出速度能提升不少。但我不建议省这一步。快照文件的正确性比导出速度重要得多第一次做校验能帮你发现一堆实现细节的bug后面熟练了再考虑按需关闭。5. 快照拿完之后能做什么5.1 从UTXO快照算地址余额快照文件拿到手最直接的应用就是算地址余额。一个地址的所有UTXO金额加起来就是它在该快照高度下的余额。下面这个Python脚本可以快速做一次全量余额统计import csv from collections import defaultdict balances defaultdict(int) with open(utxo_mainnet.csv, newline) as f: reader csv.DictReader(f) for row in reader: addr row[address] value int(row[value_sat]) balances[addr] value # 输出余额最高的前10个地址 for addr, bal in sorted(balances.items(), keylambda x: x[1], reverseTrue)[:10]: print(f{addr}\t{bal / 100000000:.8f} BTC)这里有个前置条件CSV里得有address字段。如果工具只导出了script_hex你需要先把脚本转成地址取决于脚本类型是P2PKH、P2SH还是P2WSH转换规则不一样。我建议在工具里集成地址转换逻辑否则每次分析都要重复处理脚本hex很浪费时间。还有一点需要注意一地址一余额不是UTXO模型的原生概念。一个地址的余额分散在多个UTXO里聚合时要全部累加。而同一个人的币可能放在多个地址里所以“地址余额排名”和“真实用户资产排名”是两码事。5.2 动态决策快照与链上治理场景“快照”这个词在很多场景里被滥用但UTXO快照它其实是一种动态决策快照。它的含义是选择一个确定区块高度把当时的链上状态冻结成一个基线之后所有关于空投比例、治理权重、社区激励的分配都以这份基线为准而不是实时去扫描每一笔历史交易。为什么不用实时数据因为实时数据是流动的项目方今天做快照、明天发空投如果空投发放前有人转移资产资格判定就会产生争议。UTXO快照天然解决了这个问题它把“决策时点”和“执行时点”解耦了。做空投时只需要在快照文件里筛选符合条件的地址和金额后续无论链上怎么交易都不影响分配结果。这里的实操要点是快照文件的防篡改。毕竟是涉及利益分配的数据一旦被质疑必须能证明它和某个区块高度的链状态一致。这就是为什么我前面反复强调校验哈希和节点交叉验证。给项目方做快照服务时我每次都会附上一份gettxoutsetinfo的输出截图和快照哈希形成一个可审计的交付物。5.3 扩展玩法增量快照和入库分析全量快照的缺点是贵。主网动辄几十GB的数据量每小时跑一次全量既不现实也没必要。我的做法是每天凌晨跑一次全量快照同时监听最新区块把新产生的UTXO记录和维护消耗记录做成增量日志。这样既能回溯任意历史时刻的UTXO状态又不用每次都全量重跑。增量日志的格式其实和全量快照类似多了两个字段actionadd/spend和spend_txid。重放时先加载最近的全量快照到内存再按顺序应用增量记录就能在几秒内重建出目标区块高度的UTXO集合。这个方案在数据量增长之后比全量导出划算非常多。更进一步可以把快照文件导入ClickHouse或PostgreSQL。CSV格式的好处在这时候就体现出来了数据库原生支持直接导入。我习惯建一张宽表包含txid、vout、address、value_sat、height、coinbase等字段加上以address为前缀的索引之后做余额分布、大户变动、UTXO年龄分析都只是几行SQL的事。最后聊几句实在的我自己在实际跑这个工具时印象最深的一次是在一台2核4G的小机器上导主网快照刚开始内存直接被打满进程被系统OOM kill。后来把LevelDB的缓存调到200MB又加了4G swap才勉强跑完整个过程花了4个多小时。从那以后我就学乖了第一次在新环境跑一定先拿测试网或regtest试一遍确认工具行为和磁盘路径都对再上主网。主网快照的临时文件也千万别放系统盘至少留出一个大于快照文件2倍的空间。如果你也在做UTXO链的数据分析或者正被空投快照搞得焦头烂额我建议不要一上来就追求复杂方案先把全量导出和哈希校验这条路走通再考虑增量、并行、入库这些东西。跑通基本流程之后剩下的优化都只是锦上添花。链上数据这个东西自己手里有一份可验证的快照文件永远比到处接别人的API靠谱。本文还有配套的精品资源点击获取