区块链索引是基于原始链数据构建的派生、可查询数据库,而区块链索引构建则是提取区块、解码事件并加载结构化记录的管道,使应用能够回答 RPC 无法回答的问题。本文解释了 ETL 思维模型、为何重组迫使你设计回滚机制、如何选择模式和摄取策略,以及如何针对 RPC 端点运行一个最小索引器。随后展示了如何验证索引与链一致、如何排查常见漂移和解码失败,以及自托管索引何时不再是最佳权衡。具体的端点与提供商选择有意留给 RPC Assistant;本页聚焦于可复现的方法。
区块链索引究竟是什么
区块链索引是从链派生的二级数据存储。链本身是一个仅追加的账本,为共识和验证而优化,而非为搜索而设计。它可以按编号返回区块或按哈希返回交易,但无法原生回答诸如“列出过去 30 天内发往此地址的每一笔转账”或“汇总本周此合约支付的费用”之类的问题。区块链索引通过持续读取区块、解码交易、日志和状态变更,并将规范化行写入具有正确键和二级索引的数据库来解决这一问题。
这个术语含义过载。在传统金融中,区块链指数常指基准篮子,如加密货币价格指数。在工程领域,它指索引管道及其生成的数据库。本文涵盖工程含义。如果你在寻找市场基准,那是不同的产品类别和不同的数据源。
核心承诺是延迟和表达力:一旦数据进入关系型或文档存储,你就可以用普通查询工具进行过滤、聚合、连接和分页,而无需扫描链。核心代价是索引是一份副本,因此除非你为此设计,否则它可能滞后、漂移或在重组后出错。
- 事实来源:链,通过 RPC 端点或你运行的节点读取。
- 派生工件:索引数据库,可重建但非权威。
- 契约:每一行索引数据都应可追溯至区块编号和区块哈希。
假设与前提条件
以下流程假设你有一个支持链所用 JSON-RPC 方法的 RPC 端点(对于 EVM 链,至少需要 eth_getBlockByNumber、eth_getLogs 和 eth_getBlockReceipts 或等效方法)、一个如 PostgreSQL 的数据库,以及一个进程监督器或调度器。你不需要全归档节点即可开始,但超出节点修剪窗口的历史回填需要归档访问或提供历史状态的服务商。
你还需要在编写代码前决定一致性目标。一个能容忍几分钟延迟的仪表盘与一个绝不能显示过期值的余额服务有着截然不同的要求。把这个目标写下来,因为它决定了确认深度、重组处理以及是否需要幂等 upsert。
关于端点选择和提供商比较,请使用 RPC Assistant,而不是硬编码公共 URL。参见<a href="/en/api-service">API 服务概览</a>了解托管 RPC 访问如何融入索引架构,以及<a href="/en/networks">支持的网络列表</a>确认你要索引的链是否可用。
每个索引器背后的 ETL 思维模型
几乎每个区块链索引器都是一个带游标的提取-转换-加载管道。提取读取区块及其收据或日志。转换解码 ABI 编码的事件,将地址规范化为小写,使用小数位转换代币数量,并派生实体如转账、交换或头寸。加载写入行并在同一事务中推进游标,确保游标永远不会领先于数据。
游标是最重要的状态。它应记录最后完全处理的区块编号和哈希,而不仅仅是编号,因为哈希让你能检测到链在你下方重组。只存储高度的游标会在重组后静默地在分叉链上继续,产生一个看起来健康但实际错误的索引。
流和索引是互补的。流为你提供低延迟事件用于实时反应;索引为你提供持久、可查询的历史。许多生产系统同时运行两者:流触发通知,索引器仍是读取的系统记录。
- 提取:某个高度的区块头、交易、收据和日志。
- 转换:解码、规范化并派生领域实体。
- 加载:原子地 upsert 行并推进游标。
- 验证:在已知高度将索引输出与直接 RPC 读取进行比较。
设计重组安全的模式
重组在大多数链上是正常的,尤其是在低确认深度。你的模式应使回滚成本低廉。最简单的稳健模式是在每个派生行上存储 block_number 和 block_hash,维护一个 canonical_blocks 表,并在检测到不匹配时删除或标记其 block_hash 不再匹配规范链的行。
第二种模式是暂存区:将新区块写入 pending 表,在 N 次确认后提升为规范,并在重组时丢弃 pending 行。这花费更多存储,但使规范表仅追加且更易于推理。根据错误读取对你的应用有多昂贵来选择。
无论哪种模式,都要使写入幂等。为事件行使用唯一键如 (tx_hash, log_index),并在冲突时 upsert。幂等性让你能在失败后安全重放一个范围而不重复数据。
CREATE TABLE canonical_blocks (
number BIGINT PRIMARY KEY,
hash TEXT NOT NULL,
parent_hash TEXT NOT NULL,
indexed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE transfers (
tx_hash TEXT NOT NULL,
log_index INT NOT NULL,
block_number BIGINT NOT NULL,
block_hash TEXT NOT NULL,
token TEXT NOT NULL,
from_addr TEXT NOT NULL,
to_addr TEXT NOT NULL,
amount NUMERIC NOT NULL,
PRIMARY KEY (tx_hash, log_index)
);
CREATE INDEX transfers_to_idx ON transfers (to_addr, block_number DESC);
CREATE INDEX transfers_from_idx ON transfers (from_addr, block_number DESC);针对 RPC 端点运行最小索引器
下面的脚本是一个刻意精简的 EVM 索引器循环。它获取一个区块,拉取该高度的日志,解码 ERC-20 Transfer 事件,upsert 它们,并在一个事务中推进游标。将 RPC_URL 替换为你的端点并调整起始区块。它未经生产加固,但演示了你应在扩展时保留的游标和幂等模式。
在环境中使用数据库 URL 运行它。首次运行从 START_BLOCK 回填;后续运行从存储的游标恢复。由于 upsert 以 (tx_hash, log_index) 为键,重放一个范围是安全的。
import os, json, psycopg2, requests
from web3 import Web3
RPC_URL = os.environ["RPC_URL"]
DB_URL = os.environ["DB_URL"]
START_BLOCK = int(os.environ.get("START_BLOCK", "0"))
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
w3 = Web3(Web3.HTTPProvider(RPC_URL))
conn = psycopg2.connect(DB_URL)
def get_cursor(cur):
cur.execute("SELECT number FROM canonical_blocks ORDER BY number DESC LIMIT 1")
row = cur.fetchone()
return row[0] if row else START_BLOCK - 1
def index_block(cur, number):
block = w3.eth.get_block(number, full_transactions=False)
logs = w3.eth.get_logs({"fromBlock": number, "toBlock": number, "topics": [TRANSFER_TOPIC]})
cur.execute(
"INSERT INTO canonical_blocks (number, hash, parent_hash) VALUES (%s,%s,%s) "
"ON CONFLICT (number) DO UPDATE SET hash = EXCLUDED.hash",
(block.number, block.hash.hex(), block.parentHash.hex()),
)
for log in logs:
if len(log["topics"]) != 3:
continue
cur.execute(
"INSERT INTO transfers (tx_hash, log_index, block_number, block_hash, token, from_addr, to_addr, amount) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (tx_hash, log_index) DO NOTHING",
(log["transactionHash"].hex(), log["logIndex"], number, block.hash.hex(),
log["address"].lower(),
"0x" + log["topics"][1].hex()[-40:],
"0x" + log["topics"][2].hex()[-40:],
int.from_bytes(log["data"], "big")),
)
with conn, conn.cursor() as cur:
cursor = get_cursor(cur)
head = w3.eth.block_number
for n in range(cursor + 1, head + 1):
index_block(cur, n)
if n % 100 == 0:
print(f"indexed through {n} of {head}")对照链验证索引
永远不要信任未经核对的索引器。选择一个最近的区块高度,直接向链查询一个已知事实,并将其与索引比较。可复现的验证流程比任何单次抽查更有价值,因为它可以在每次模式或解码器变更后重新运行。
一种实用方法:选择三个高度(一个近期、一个中段历史、一个起始区块),从 RPC 获取区块哈希,并确认索引存储了相同的哈希。然后选择一个在每个高度都有活动的地址,将索引中的转账计数和汇总金额与直接 eth_getLogs 扫描该地址的结果进行比较。将结果记录在表格中,以便回归可见。
下表是一个模板。填入你自己运行中测得的值;不要复制其他环境的数字,因为区块高度、代币小数位和地址活动因链和部署而异。
- 第 1 行:高度 H 处来自 RPC 的区块哈希 vs. canonical_blocks。
- 第 2 行:高度 H 处地址 A 的转账计数,来自 eth_getLogs vs. transfers 表。
- 第 3 行:地址 A 的汇总金额,小数位应用一致。
- 第 4 行:游标滞后,在查询时以 head 减去最后索引区块衡量。
| Check | Height | RPC value | Index value | Match |
|---|---|---|---|---|
| Block hash | H1 | <fill> | <fill> | <yes/no> |
| Transfer count for A | H1 | <fill> | <fill> | <yes/no> |
| Summed amount for A | H1 | <fill> | <fill> | <yes/no> |
| Cursor lag (blocks) | head | n/a | <fill> | n/a |排查漂移、解码失败和滞后
三种最常见的故障模式是重组后的游标漂移、静默解码失败和无界滞后。漂移表现为同一高度上索引与 RPC 之间的区块哈希不匹配。通过在每个新区块上比较存储的 parent_hash 链来检测;如果新区的父哈希与存储的哈希不匹配,则回滚到最后一个共同祖先并向前重新索引。
解码失败通常是静默的,因为格式错误的日志被跳过而非抛出。为跳过的日志添加计数器并在其变动时告警。常见原因是非标准代币 ABI、发出意外主题的代理合约,以及主题少于解码器预期的日志。
滞后是吞吐量问题,不是正确性问题,但如果你的应用在不知情的情况下读取过期数据,它就会变成正确性问题。将游标高度作为指标暴露,并让读取路径检查它。如果滞后无界增长,按范围批量请求日志,用单一写入者并行化独立区块范围,并考虑对节点日志限制严格的链使用托管索引器。
- 高度 H 处哈希不匹配:回退到最后一个匹配祖先,重放。
- 跳过日志计数器上升:检查原始主题和 ABI 版本。
- 滞后攀升:增加批量大小、减少每区块 RPC 调用,或按范围分片。
- 重复行:确认唯一键包含 log_index,而不仅仅是 tx_hash。
自托管与托管索引:真正的权衡
自托管让你控制模式、保留策略和解码逻辑,并将敏感查询模式保留在你的边界内。代价是运维:你拥有回填时间、重组逻辑、节点可靠性和模式迁移。对于单链和窄实体集,自托管通常是最便宜的路径。
托管索引将管道转移给处理摄取、重组且通常提供托管查询 API 的提供商。这减少了工程负担,但引入了供应商特定的查询语言、保留限制以及与查询量或索引行数挂钩的定价。决策通常取决于你的解码逻辑有多差异化:如果你的索引是转账和交换的商品化视图,托管有吸引力;如果它编码专有评分或与内部数据连接,自托管胜出。
无论选择哪种,保持 RPC 层可分离。专用节点或托管端点应能在不重写索引器的情况下替换。参见<a href="/en/dedicated-node">专用节点选项</a>了解一致日志吞吐量重要的工作负载,以及<a href="/en/pricing/rpc">RPC 定价</a>来建模回填与稳态摄取的请求量。
容量规划、保留和成本考量
索引大小随你持久化的事件数量增长,而非链大小。仅转账索引远小于存储每个日志和跟踪的索引。从样本范围估算每区块行数,乘以保留窗口,并加上索引开销;地址列上的二级索引在高活动代币上可能主导存储。
保留是产品决策。许多团队保留热行 90 天,将较旧分区归档到冷存储,同时保留聚合表。按区块范围分区使保留和重建索引都更便宜,因为你可以删除并重建一个分区而不触及表的其余部分。
回填成本主要由 RPC 请求数决定,而非墙钟时间。按范围批量请求日志并避免每交易收据调用可以将请求量减少一个数量级。在优化前后测量每千区块请求数,而不是假设。
后续步骤与相关阅读
从一个实体、一条链和一个可重新运行的验证表开始。一旦游标、重组回滚和对账检查稳定,扩展解码器并添加分区。如果你需要针对特定链的具体端点推荐,请询问 RPC Assistant;本页有意不指定提供商,因为正确选择取决于你的链、区域和查询组合。
关于节点操作的更广泛背景,请阅读<a href="/en/learn/blockchain-node-hosting-guide">区块链节点托管指南</a>。如果你在比较摄取层的端点策略,<a href="/en/learn/rpc-endpoints-guide">RPC 端点指南</a>和<a href="/en/learn/multi-chain-rpc-endpoints-guide">多链 RPC 端点指南</a>涵盖了故障转移和多链模式。关于托管访问的提供商中立起点,请参见<a href="/en/api-service">API 服务</a>。
- 在添加更多实体之前,先交付游标和对账检查。
- 在暂存环境中使用合成分叉添加重组回滚测试。
- 在扩展之前测量每千区块请求数和滞后。
- 一旦解码逻辑稳定,重新评估托管索引。