失败交易分析数据集(Failed Transactions Analytics)
来源:schema/clickhouse/derived/020_failed_txs.sql。 面向内部使用 SQL 分析链上交易失败原因、分类合约 revert 原因、以及监控合约日级故障率(Failure Rate)的工程与分析团队。
来源:schema/clickhouse/derived/020_failed_txs.sql。
面向内部使用 SQL 分析链上交易失败原因、分类合约 revert 原因、以及监控合约日级故障率(Failure Rate)的工程与分析团队。
1. 核心概念与数据来源
1.1 数据来源
{db}.transactions(回执合并表,见sql/schema.sql):status = 0表示 EVM 执行失败(reverted 或 out of gas)。- 提供发送者
from、目标合约to、方法选择器method_id = substring(input, 1, 4)、实际消耗 gasgas_used、以及有效 gas 价格effective_gas_price。 - 全历史覆盖:自创世块起所有交易均包含
status字段。
{db}.traces(调用追踪表,见sql/traces.sql):- 重要限制:
traces表仅在节点以--traces参数开启追踪跟随(起始时间为 2026-09-25)后入库的区块中存在。在此之前的历史补算(backfill)或未配置 traces 的区块中,无追踪数据。 - 提供顶层执行帧(
depth = 0/length(trace_address) = 0)的error(如execution reverted,out of gas)、revert_reason(追踪器解码的文字原因)和output(原始调用输出字节)。
- 重要限制:
2. Revert Reason 解码逻辑
在 EVM 标准中,交易 revert 时返回的数据结构通常有如下几类:
- ABI 标准
Error(string)(选择器0x08c379a0):- 4 字节 selector:
0x08c379a0 - 32 字节 offset:指向字符串位置(通常为
0x00...0020= 32) - 32 字节 length:动态字符串的实际字节长度
- 动态 UTF-8 字符串内容
- ClickHouse 解码:从偏移第 69 字节开始,截取大端整数指定的长度:
substring(output, 69, reinterpretAsUInt64(reverse(substring(output, 61, 8))))
- 4 字节 selector:
- ABI 标准
Panic(uint256)(选择器0x4e487b71):- 4 字节 selector:
0x4e487b71 - 32 字节 uint256 panic code
- 映射为标准 Solidity 错误:
0x00:generic panic0x01:assert evaluated to false0x11:arithmetic underflow or overflow0x12:division or modulo by zero0x21:enum out-of-bounds0x22:storage byte array out-of-bounds0x31:pop() on empty array0x32:array index out-of-bounds0x41:allocation of too much memory0x51:zero-initialized variable of internal function type- 其余 code 输出
Panic(0x...)
- 4 字节 selector:
- 节点 CallTracer 预解码:
- Geth/Nitro
callTracer会在部分帧中直接输出revertReason(如'SPL'、'arithmetic underflow or overflow'等)。解码器优先使用该非空字符。如果遇上嵌套 ABI 编码(如revert_reason本身以0x08c379a0开头),则自动递归解出内层消息。
- Geth/Nitro
- 其他自定义错误 / 无 revert message / Out of Gas:
- 自定义 Error(4 字节自定义选择器)或无 revert message 时,
revert_reason为NULL,error记录具体错误分类(如'execution reverted')。 - Out of Gas 时,
error = 'out of gas',revert_reason为NULL。
- 自定义 Error(4 字节自定义选择器)或无 revert message 时,
- 无 trace 历史区块:
- 对于 2026-09-25 前的历史区块,
{db}.failed_txs依然保留transactions的失败记录,error与revert_reason安全置为NULL。
- 对于 2026-09-25 前的历史区块,
3. 表结构与设计权衡
3.1 交易级失败明细视图:{db}.failed_txs
采用 ClickHouse VIEW(动态视图),在查询时将 {db}.transactions FINAL 与 {db}.traces FINAL(depth = 0)进行 LEFT JOIN:
| 列名 | 类型 | 说明 |
|---|---|---|
block | UInt64 | 区块高度(别名,与 block_number 相同) |
block_number | UInt64 | 区块高度 |
tx_index | UInt32 | 块内交易索引 |
tx_hash | FixedString(32) | 交易哈希(原始字节,展示用 hex()) |
block_timestamp | DateTime('UTC') | 出块时间(UTC) |
from | FixedString(20) | 交易发起方地址 |
to | Nullable(FixedString(20)) | 目标合约/接收地址(合约创建交易为 NULL) |
method_id | String | 调用方法 4 字节 selector(substring(input, 1, 4)) |
gas_used | UInt64 | 实际消耗的 gas |
fee | UInt256 | 消耗的手续费(wei):gas_used * effective_gas_price |
error | Nullable(String) | 顶层帧错误信息(如 execution reverted, out of gas;无 trace 为 NULL) |
revert_reason | Nullable(String) | 解码后的 revert 原因字符串(无文字原因或无 trace 为 NULL) |
为什么是 VIEW 而不是写入触发物化视图(Insert-Trigger MV)?
- 时序不一致与写入顺序依赖:在底层写入链路中(
src/sink_clickhouse.rs),区块提交顺序为先写入transactions,后写入traces。若在transactions上建立写入触发的 MV 并尝试 JOINtraces,在触发时刻对应交易的traces尚未落库,会导致 trace 关联信息全部丢失。 - 数据可用性覆盖:
traces仅存在于 2026-09-25 之后开启追踪的区块。VIEW 架构以transactions为基准 LEFT JOINtraces,在无 trace 的历史区块中优雅回退为 NULL,在有 trace 的新区块中精准展示解码详情,且完全享受FINAL WHERE is_deleted = 0带来的幂等与防重组保障。
3.2 合约日级故障率表:{db}.contract_daily_failure_rate
记录每个合约在各个已收盘自然日(UTC)的调用总数、失败数、成功数、故障率以及因失败浪费的手续费总额。
| 列名 | 类型 | 说明 |
|---|---|---|
date | Date | UTC 日期 |
contract | FixedString(20) | 目标合约地址(transactions.to) |
total_txs | UInt64 | 当天调用该合约的全部交易数 |
failed_txs | UInt64 | 失败交易数(status = 0) |
success_txs | UInt64 | 成功交易数(status = 1) |
failure_rate | Float64 | 故障率:failed_txs / total_txs(0.0 ~ 1.0) |
total_fee_lost | UInt256 | 当天该合约失败交易所消耗的手续费总和(wei) |
version | UInt64 | 写入版本号(ReplacingMergeTree 默认 1) |
is_deleted | UInt8 | 逻辑删除标记(ReplacingMergeTree 默认 0) |
- 排序键:
ORDER BY (date, contract) - 索引:
INDEX idx_contract contract TYPE bloom_filter GRANULARITY 1,加速单合约跨日时序点查。
为什么是可刷新物化视图(Refreshable MV,方案 (b))?
contract_daily_failure_rate 是对多行交易的聚合计算(count()、countIf()、sum())。若使用普通的写入触发 MV,在遇到断点续跑(backfill resume)造成的重复写入或链重组墓碑(is_deleted=1)时,累加计数器会被重复统计导致虚高,违背正确性原则。
因此采用方案 (b):
- 使用
REFRESH EVERY 1 DAY OFFSET 1 HOUR RANDOMIZE FOR 10 MINUTE。 - 仅重算已收盘的完整 UTC 日期(
toDate(block_timestamp) < today())。 - 每次刷新以原子替换方式更新数据,即便历史数据发生重组修正,也会在下一次周期刷新中自我修复。
4. 字段精度与资金安全(Money-Safety)
fee与total_fee_lost均为UInt256整数(wei 单位),禁止在手续费计算过程中引入任何浮点数。- 计算公式:
toUInt256(gas_used) * toUInt256(coalesce(effective_gas_price, gas_price, 0)) failure_rate仅为展示层比例指标,允许使用Float64(0.0 至 1.0)。
5. 真实数据观测与验证
5.1 生产环境真实交易解码验证
在生产环境 Robinhood Chain 主网(chain id 4663)上抽样核验:
- ABI
Error(string)解码:- 区块:
72077795,交易哈希:0x65A5C483CFE30A187B2AEF07BDBF0FA51ED1C63B0A3F895F97CD6D24281354FA output:08C379A000...0002000...0000353504C00...(长度 3,ASCII 码0x53504C)- 解码结果:
error = 'execution reverted',revert_reason = 'SPL' - 消耗手续费:
1738509696000wei。
- 区块:
- Solidity
Panic(uint256)解码:- 区块:
72083204,交易索引:5,交易哈希:0x5114EAA750D14672F52F5F0CFD74AA285B02E4DADE7019A7EF781A7CD47BA2A7 output:4E487B710000000000000000000000000000000000000000000000000000000000000011- 错误码:
0x11(17,算术溢出/下溢) - 解码结果:
error = 'execution reverted',revert_reason = 'arithmetic underflow or overflow' - 消耗手续费:
243210212520000wei。
- 区块:
- Out of Gas 交易:
- 区块:
72077012,交易索引:13,交易哈希:0xC886117D4A6122A208B69E9B9D00219AC381B80B6BF25C10582EEB0184088E2E - 解码结果:
error = 'out of gas',revert_reason = NULL。
- 区块:
- 历史未追踪区块(Traces 未开启):
- 区块:
72039003,交易哈希:0x43EA3399D254E7F3A689019EF5813B3E0AD6B00A3D371AB7B6BBA7F05E7FD640 - 结果:
error = NULL,revert_reason = NULL,交易基本信息与手续费正常计算。
- 区块:
- 实时节点 RPC
debug_traceTransaction双向确认:- 对节点近端(~13 秒状态保留窗口内)最新区块
72151835交易0x14f28ecede56c8f6a0d0c5deb1b78ac481534038899ea7e070d2ca5e906a1900调用debug_traceTransaction({'tracer':'callTracer'}): - 节点返回:
error = 'execution reverted',revertReason = 'SPL',output = '0x08c379a0...53504c...',与 ClickHouse 实时解码行为完全一致。
- 对节点近端(~13 秒状态保留窗口内)最新区块
5.2 独立 Python 离线聚合校验(0 Mismatches)
在收盘日 2026-08-19 的真实区块切片中,使用独立 Python 脚本对原始 transactions 数据重新做全量逐行统计,并与 ClickHouse contract_daily_failure_rate 聚合表结果比对:
- 核验合约总数:215 个合约
- 核验指标:
total_txs、failed_txs、success_txs、failure_rate、total_fee_lost - 比对结果:215 个合约全部完全吻合,0 mismatches,误差为 0。
6. 安装与补算
6.1 安装步骤
- 确认前置表已存在:
{db}.transactions与{db}.traces(执行过init-schema)。 - 执行 DDL 与初始化脚本:
该脚本会自动完成:sed 's/{db}/robinhood/g' schema/clickhouse/derived/020_failed_txs.sql | clickhouse-client --multiquery- 创建
failed_txs视图; - 创建
contract_daily_failure_rate基础表与可刷新物化视图mv_contract_daily_failure_rate; - 自动执行一次性
INSERT,补齐建表前所有的已收盘历史日(date < today())。
- 创建
6.2 刷新机制与手动触发
- 物化视图默认每自然日 01:00 UTC(带 10 分钟随机抖动)自动触发重算。
- 手动立即刷新视图(例如回填历史数据后):
SYSTEM REFRESH VIEW {db}.mv_contract_daily_failure_rate; SYSTEM WAIT VIEW {db}.mv_contract_daily_failure_rate; - 检查刷新状态:
SELECT view, status, last_refresh_time, next_refresh_time, exception FROM system.view_refreshes WHERE database = '{db}' AND view = 'mv_contract_daily_failure_rate';
7. 查询示例
7.1 查询指定合约最近的失败交易及原因
SELECT
block,
hex(tx_hash) AS tx_hash,
hex(`from`) AS sender,
hex(method_id) AS method_id,
gas_used,
fee,
error,
revert_reason
FROM {db}.failed_txs
WHERE to = unhex('9A7285C542E46C91DA2ACBF016A680B8C6D8CA90')
ORDER BY block DESC
LIMIT 20;7.2 查询某天故障率最高、且调用量达到一定阈值的合约 Top 10
SELECT
hex(contract) AS contract_addr,
total_txs,
failed_txs,
round(failure_rate * 100, 2) AS failure_rate_pct,
total_fee_lost
FROM {db}.contract_daily_failure_rate FINAL
WHERE is_deleted = 0
AND date = '2026-08-19'
AND total_txs >= 10
ORDER BY failure_rate DESC, failed_txs DESC
LIMIT 10;7.3 分析失败原因分布(按 Revert Reason 分组统计)
SELECT
coalesce(revert_reason, error, 'unknown') AS failure_category,
count() AS fail_count,
sum(fee) AS total_fee_wasted
FROM {db}.failed_txs
WHERE block_timestamp >= today() - 1
GROUP BY failure_category
ORDER BY fail_count DESC
LIMIT 15;吞吐数据集(throughput_minute / throughput_hour)
对应 SQL:schema/clickhouse/derived/021_throughput.sql。 给 dashboard/研究提供按分钟、按小时的链吞吐指标:出块数、交易数、TPS、gas 用量、 出块间隔(均值/分位数)、用户交易占比。只依赖 {db}.blocks / {db}.transact…
Fees 数据集
来源:schema/clickhouse/derived/013_fees.sql。给内部用 SQL 查询交易手续费的团队用:单笔交易费用(tx_fees 视图)、按天/按类型的费用统计、每日按地址汇总。