面向 RGB++ 的 BTC Indexer 选型调研

背景

RGB++ 目前没有协议级的索引器。应用侧可用的是 btc-assets-api,它的接口提供了部分索引能力,但实际使用中它的问题比较明显:

  • 查询维度只有 BTC 地址和 outpoint 两种,没有资产维度的能力。给定一种 UDT,查不到持有地址数、持有地址列表、资产维度的流转记录这类数据。它的实现是从地址的 BTC UTXO 正向映射到 CKB Cell,没有维护反向的资产索引。
  • 聚合能力弱、性能一般。
  • BTC 数据源是自部署的 mempool/electrsmempool.space API,两者的按地址查询 UTXO 均受到 --utxos-limit(默认 500)限制,若地址持有的 UTXO 超限会直接报错。

开发一个 RGB++ 协议的索引器,一个方向是像 Ordinals、BRC-20、Runes 这类 Bitcoin meta-protocol 一样,从头开始构建。

但由于 RGB++ 协议本身的特性,我认为以通用的 BTC 索引器为数据源进行开发,会是更好的选择。

RGB++ 的同构绑定把资产状态放在 CKB Cell 上:Bitcoin UTXO 一一映射到 CKB Cell,所有权由 RGB++ lock 同步,状态由 cell 的 data 和 type 维护,状态转移由 CKB 共识层的脚本验证。BTC 交易只在 OP_RETURN 里携带一个 commitment,BTC 链上不含任何资产语义数据;BTC UTXO 扮演一次性密封(single-use seal)的角色,比特币共识只保证它只能被花费一次,即防双花,并不验证资产状态。

因此 RGB++ 服务在 BTC 侧需要的查询是通用能力:地址的 UTXO 列表、余额、交易历史,outpoint 的花费状态,交易广播和费率估算。资产语义在 CKB 侧解析——已知 btc_txid + vout 就能直接构造出 RGB++ lock args,用 CKB indexer 按 lock 检索 cell,token 类型由 type script 给出、数量由 cell data 给出,正确性由 CKB 共识背书,不需要任何链下状态推导。

这和上面提到的 meta-protocol 有本质区别,它们的资产状态不受比特币共识验证,必须由链下索引器实现协议自己的状态机并全链重放:

  • Ordinals:铭文内容在 reveal 交易输入的 Taproot script-path witness 里上链,绑定到特定的聪,归属要按 Ordinal Theory 追踪这个聪在 UTXO 间的流转。Inscription ID 可由链上交易确定,顺序编号 Inscription Number 则只是 ord 的索引规则。通用 UTXO 索引回答不了一个 UTXO 上有哪些铭文。
  • BRC-20:余额由索引器按链上顺序解释 deploy、mint、transfer 铭文得出:第一个有效 deploy 确立 ticker,mint 记给铭文的初始所有者,transfer 首次转移才生效,每条规则都要索引器自己实现。
  • Runes:Runestone 在 OP_RETURN OP_13 输出里,索引器要汇总输入携带的 Rune 余额,再按 etching、mint、edicts、默认分配、燃烧、cenotaph 等规则算出每个输出的余额。

基于以上分析,以及 mempool/electrs 已知的局限,我对市面上已有的 BTC 索引器进行了调研,以便筛选出适合作为支撑 RGB++ 索引器的通用 BTC 索引器。

调研需求

  • 核心接口能力:
    1. 地址维度的聚合查询:余额、UTXO 列表、交易历史
    2. 推荐手续费率
  • 可自部署
  • 支持 BTC 测试网

候选项目

Electrum 系

Electrum 是一款 Bitcoin 轻钱包,依赖实现了 Electrum Protocol 的服务端提供索引服务。Electrum Protocol 是客户端-服务端架构的 JSON-RPC 协议:

┌──────────┐      Electrum       ┌──────────────────┐       RPC        ┌────────────────────┐
│  Wallet  │ ──── Protocol ────▶ │  Electrum Server │ ──────────────▶  │ Bitcoin Full Node  │
└──────────┘                     └──────────────────┘                  └────────────────────┘

主流 Electrum Server 对比:

项目 定位 开发语言 底层数据库 数据源 支持链 BTC 网络支持 支持机构 社区活跃度
electrum-server Electrum 协议的初代实现 Python LevelDB Bitcoin Core 仅 BTC mainnet / testnet3,signet、testnet4、regtest 不支持 Electrum 团队 已归档
ElectrumX 公共 Electrum Server 网络的主流实现 Python (≥3.10) LevelDB / RocksDB Bitcoin Core BTC mainnet / testnet3 / testnet4 / signet / regtest Electrum 钱包官方 维护中,节奏偏慢
romanz/electrs 为个人使用场景开发的轻量实现,对硬件资源要求与运行 Bitcoin 全节点相近 Rust RocksDB Bitcoin Core 仅 BTC mainnet / testnet3 / testnet4 / signet / regtest romanz 个人维护 活跃
Blockstream/electrs Blockstream Explorer 的后端,在 romanz/electrs 基础上新增 HTTP REST API,扩充索引以提升高负载性能,支持 Liquid 等基于 Elements 框架的区块链 Rust RocksDB Bitcoin Core / Elements 节点 BTC + Liquid mainnet / testnet3 / testnet4 / signet / regtest Blockstream 维护中,节奏偏慢
mempool/electrs mempool.space 的后端,基于 Blockstream/electrs 二次开发 Rust RocksDB Bitcoin Core BTC + Liquid mainnet / testnet3 / testnet4 / signet / regtest Mempool 非常活跃
Fulcrum 可直接替换 ElectrumX 的高性能实现,兼容 Electrum Cash 1.6 Protocol(Electrum Protocol 的超集) C++20 (Qt5/Qt6) RocksDB 各币种对应全节点 BTC / BCH / LTC mainnet / testnet3 / testnet4 / signet / regtest Calin Culianu 个人维护 活跃

其它

项目 定位 开发语言 底层数据库 数据源 支持链 BTC 网络支持 支持机构 社区活跃度
Blockbook Trezor Suite 的官方后端 Go RocksDB 各币种对应全节点 BTC、BCH、LTC、DOGE、ETH 等 30+ mainnet / testnet3 / testnet4 / signet / regtest Trezor 活跃
bitcore-node BitPay 钱包与支付产品的后端 TypeScript(Node.js) MongoDB Bitcoin Core BTC / BCH / DOGE / LTC mainnet / testnet3 / regtest,signet、testnet4 不支持 BitPay 维护中,节奏偏慢
BRK 自托管版 Glassnode + mempool.space Rust Sparse file 自研存储 直接解析 Bitcoin Core 区块文件 仅 BTC mainnet 独立开源项目 活跃,当前版本 v0.3.6
rust-bitcoin-indexer 实验性 SQL 数仓型索引器 Rust PostgreSQL Bitcoin Core 仅 BTC mainnet dpc 个人维护 多年不活跃

API 能力对比

对比范围限定在维护活跃、支持测试网、适合生产部署的项目。

能力维度 Blockstream/electrs & mempool/electrs REST Fulcrum / ElectrumX(Electrum Protocol) Blockbook bitcore-node
地址余额 GET /address/:address
返回 confirmed 和 mempool 两个维度的统计:tx_countfunded_txo_count/sumspent_txo_count/sum
blockchain.scripthash.get_balance
返回 confirmed 和 unconfirmed 余额
GET /api/v2/address/{address}
返回余额
GET /api/BTC/mainnet/address/:address/balance
返回 confirmedunconfirmedbalance
地址交易历史 GET /address/:address/txs
mempool 中最多 50 笔未确认交易 + 25 笔已确认交易,更多已确认交易经 GET /address/:address/txs/chain[/:last_seen_txid] 游标查询
blockchain.scripthash.get_history
返回 (tx_hash, height) 列表;按块高过滤(from_height/to_height)是 Fulcrum 扩展,ElectrumX 不支持
GET /api/v2/address/{address}
details 参数 + page/pageSize 标准分页 + from/to 块高过滤
GET /api/BTC/mainnet/address/:address/txs
返回含 mintTxidmintHeightspentTxidspentHeightvaluescript 的数组
地址 UTXO 列表 GET /address/:address/utxo
返回 txidvoutvaluestatus总数受 --utxos-limit 限制
blockchain.scripthash.listunspent
返回 heighttx_postx_hashvalue;协议本身无数量上限(若由 electrs 提供 Electrum 接口则同样受 --utxos-limit 限制)
GET /api/v2/utxo/{descriptor}
接受地址、xpub 或 descriptor;默认含未确认交易,支持 confirmed=true 过滤;无数量限制
GET /api/BTC/mainnet/address/:address/?unspent=true
可加 excludeConflicting=true 排除冲突 UTXO
交易详情 GET /tx/:txid blockchain.transaction.get(tx_hash, verbose=true)
返回解析后的 JSON 结构
GET /api/v2/tx/{txid} 返回详细交易信息
GET /api/v2/tx-specific/{txid} 返回链原生 JSON 格式
GET /api/BTC/mainnet/tx/:txid
Raw Transaction GET /tx/:txid/hex 返回 hex
GET /tx/:txid/raw 返回二进制
blockchain.transaction.get(tx_hash, verbose=false)
直接返回 hex
GET /api/v2/tx/{txid}
交易输出的花费状态 GET /tx/:txid/outspend/:vout
GET /tx/:txid/outspends 批量查询,返回 spenttxidvinstatus
不支持 GET /api/v2/tx/{txid}
vout 数组中已花费的输出带 spent: truespentTxId
GET /api/BTC/mainnet/tx/:txid/coins
每个输出带 spentTxid/spentHeight:未花费时 spentTxid 为空、spentHeight 为负
区块查询 GET /block/:hash 查详情
GET /block-height/:height 按高度反查 hash
blockchain.block.headers
仅返回区块头
GET /api/v2/block/{blockId} GET /api/BTC/mainnet/block/:blockId
GET /api/BTC/mainnet/block/tip
区块内交易列表 GET /block/:hash/txs/:start_index 每页 25 笔游标翻页
GET /block/:hash/txids 只查哈希列表
无列表方法,仅 blockchain.transaction.id_from_pos 按块高 + 块内位置逐笔取 txid GET /api/v2/block/{blockId}
支持 page 分页
GET /api/BTC/mainnet/tx?blockHeight=N
网络费率估算 GET /fee-estimates
返回 1–25、144、504、1008 等确认目标的 sat/vB
blockchain.estimatefee(number) 返回指定区块数内确认的费率
mempool.get_fee_histogram 返回内存池费率分布
GET /api/v2/estimatefee/{blocks} GET /api/BTC/mainnet/fee/:target
全网内存池概览 GET /mempool/txids 返回全部交易哈希
GET /mempool 返回 countvsizetotal_feefee_histogram
不支持
只能按单地址查 blockchain.scripthash.get_mempool
不支持 不支持
交易广播 POST /tx 单笔广播
POST /txs/package CPFP 包广播(需 Bitcoin Core 28.0+)
blockchain.transaction.broadcast
blockchain.transaction.broadcast_package(Fulcrum 扩展,ElectrumX 不支持)
POST /api/v2/sendtx/{hex}
POST /api/v2/sendtx/
POST /api/BTC/mainnet/tx/send
请求体 {"rawTx":"02000000…"},返回 txid
xpub 支持 不支持 不支持 原生支持
GET /api/v2/xpub/{xpub},服务端完成 BIP44/49/84 子地址派生、Gap Limit 扫描和汇总
POST /wallet 注册 pubKey + 派生路径 → 导入地址 → 按钱包查交易、余额、UTXO
事件推送 原生 REST 无推送能力 blockchain.scripthash.subscribe / blockchain.headers.subscribe 订阅后经 TCP/SSL 长连接推送 原生 WebSocket
支持 subscribeNewBlocksubscribeAddresses 等事件订阅
socket.io 协议订阅事件主题

--utxos-limit 看 electrs 与 Blockbook 的索引设计对比

electrs:查询时重放历史

mempool/electrs 的索引分三个 RocksDB 库:txstorehistorycache(见 doc/schema.md)。与地址查询相关的是 history 库,每个 scripthash 的资金流水以独立行追加存储:

  • 每笔资金流入(funding output)一行:H{scripthash}{funding-height}F{funding-txid:vout}{value}
  • 每笔资金流出(spending input)一行:H{scripthash}{spending-height}S{spending-txid:vin}{funding-txid:vout}{value}

也就是说,索引里不存在“某地址当前 UTXO 集”这样一条可以直接读出的记录,它的查询逻辑是按前缀扫描该 scripthash 的全部 H 行,在内存中逐条重放:funding 行把 outpoint 插入集合,spending 行把对应 outpoint 移除,扫完全部历史,剩下的集合才是当前 UTXO 集。

这个设计有两个直接后果:

  1. 单次查询的开销正比于地址的全部历史事件数,而不是当前 UTXO 数量。 一个发生过百万笔交易、当前只剩 3 个 UTXO 的地址,也要重放百万行才能得到这 3 个 UTXO。
  2. 无法分页。

electrs 用两个机制控制这里的开销:

  • cache 库中的 U{scripthash} → {utxo-set}{blockhash} 行缓存上次计算出的整个集合,下次查询从缓存的 blockhash 之后增量重放;遇到重组则丢弃缓存全量重算。
  • --utxos-limit(默认 500):重放过程中集合大小一旦超过限制,立即以 ErrorKind::TooManyUtxos 中断。检查发生在重放的任意时刻:即使地址当前的 UTXO 数量在限制以内,只要重放区间内任何一个时点同时持有超过限制数量的 UTXO,查询同样失败。

Blockbook:写入时物化 UTXO 集

Blockbook 采用另一种实现(见 docs/rocksdb.md)。Bitcoin 类币种有一个专门的 RocksDB column family addressBalance,以地址描述符为键,value 里依次编码了该地址的交易计数、累计支出、当前余额,以及完整的 UTXO 数组,一个地址的全部状态就是一条 key-value 记录。

这条记录在区块 connect/disconnect 时以读-改-写方式维护:整行读出,新增输出调 addUtxo,花费时调 markUtxoAsSpent 标记,块处理结束后 storeBalances 把每个被触及地址的整行重新打包写回,维护成本在索引写入时产生。

于是读路径变得非常轻:GET /api/v2/utxo/{descriptor} 的实现就是拿地址作 key 从 addressBalance 里取出这一条记录、顺序解码,不做任何扫描,开销正比于该地址当前的 UTXO 条数,与历史长度无关,结果数组原样返回、无截断。

部署实测

Testnet4:Blockbook(bitcoind 需开启 txindex

同步至高度 143966,Blockbook 全量索引耗时不到 1 小时:

$ du -h -d 1 ~/.bitcoin/testnet4
704M    /root/.bitcoin/testnet4/indexes
13G     /root/.bitcoin/testnet4/blocks
993M    /root/.bitcoin/testnet4/chainstate
15G     /root/.bitcoin/testnet4

$ du -h -d 1 ~/blockbook/data
12G     /root/blockbook/data

Blockbook 索引体积(12 G)与区块数据(13 G)接近 1:1。

Mainnet:mempool/electrs(bitcoind 未开启 txindex

$ du -h -d 1 ~/.bitcoin/
969M    /home/admin/.bitcoin/indexes
11G     /home/admin/.bitcoin/chainstate
12G     /home/admin/.bitcoin/

$ du -h -d 1 mainnet
992G    mainnet/blocks

$ du -h -d 1 electrs_data
1.9T    electrs_data/mainnet

electrs 索引体积(1.9 T)约为区块数据(992 G)的 2 倍。这是因为 electrs 的 txstore 库自带全量交易存储,所以它不要求 bitcoind 开 txindex;再叠加 historycache 等索引,总体积超过链本身。

选型结论

首选 Blockbook,次选 mempool/electrs。

关键的判断标准在于,RGB++ 服务最核心、调用最频繁的查询路径是“一个地址的全部 UTXO”,索引器需要准确且高效地提供这一能力。

选 Blockbook 的理由:

  • 任何地址都能完整枚举 UTXO。
  • 以它为基础构建服务,二次封装工作量小:交易历史是候选中唯一提供标准分页;原生 WebSocket 可订阅地址和新块事件;xpub/descriptor 原生支持,账户级查询一次调用。
  • 部署和运维成本可接受。实测 testnet4 从零建完索引不到 1 小时,索引体积和链数据基本 1:1;支持 mainnet、testnet3、testnet4、signet 和 regtest。
  • 它是 Trezor Suite 的生产后端,Trezor 官方维护,活跃度高。

次选 mempool/electrs。它的 REST 接口覆盖面是候选中最全的,有 mempool.space 的生产验证,社区也最活跃。但 --utxos-limit 限制的恰恰是 UTXO 查询这条关键路径,因此退为次选。

不选 Fulcrum:只提供 Electrum Protocol,没有 HTTP REST,接入要自建一层协议适配;而它通过 Electrum Protocol 提供的能力,electrs 已经覆盖,同时没有搜索到充分证据表明它的性能优于 electrs。

不选 bitcore-node:不支持 signet 和 testnet4,不满足硬性需求,维护节奏也慢。

electrum-server 已归档,BRK 与 rust-bitcoin-indexer 仅支持 mainnet,因此不在本次考虑范围内。

本次调研主要基于文档、代码实现和有限的部署验证,缺少实测的性能对比数据。如有遗漏的项目、不同的实践经验或事实性错误,欢迎补充和指正。

4 Likes