BlockVectra

失败交易分析数据集(Failed Transactions Analytics)

来源:schema/clickhouse/derived/020_failed_txs.sql。 面向内部使用 SQL 分析链上交易失败原因、分类合约 revert 原因、以及监控合约日级故障率(Failure Rate)的工程与分析团队。

This content is sourced from upstream and is currently available in Chinese only.

来源:schema/clickhouse/derived/020_failed_txs.sql。 面向内部使用 SQL 分析链上交易失败原因、分类合约 revert 原因、以及监控合约日级故障率(Failure Rate)的工程与分析团队。


1. 核心概念与数据来源

1.1 数据来源

  1. {db}.transactions(回执合并表,见 sql/schema.sql):
    • status = 0 表示 EVM 执行失败(reverted 或 out of gas)。
    • 提供发送者 from、目标合约 to、方法选择器 method_id = substring(input, 1, 4)、实际消耗 gas gas_used、以及有效 gas 价格 effective_gas_price。
    • 全历史覆盖:自创世块起所有交易均包含 status 字段。
  2. {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 时返回的数据结构通常有如下几类:

  1. 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))))
  2. ABI 标准 Panic(uint256)(选择器 0x4e487b71):
    • 4 字节 selector:0x4e487b71
    • 32 字节 uint256 panic code
    • 映射为标准 Solidity 错误:
      • 0x00: generic panic
      • 0x01: assert evaluated to false
      • 0x11: arithmetic underflow or overflow
      • 0x12: division or modulo by zero
      • 0x21: enum out-of-bounds
      • 0x22: storage byte array out-of-bounds
      • 0x31: pop() on empty array
      • 0x32: array index out-of-bounds
      • 0x41: allocation of too much memory
      • 0x51: zero-initialized variable of internal function type
      • 其余 code 输出 Panic(0x...)
  3. 节点 CallTracer 预解码:
    • Geth/Nitro callTracer 会在部分帧中直接输出 revertReason(如 'SPL'、'arithmetic underflow or overflow' 等)。解码器优先使用该非空字符。如果遇上嵌套 ABI 编码(如 revert_reason 本身以 0x08c379a0 开头),则自动递归解出内层消息。
  4. 其他自定义错误 / 无 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。
  5. 无 trace 历史区块:
    • 对于 2026-09-25 前的历史区块,{db}.failed_txs 依然保留 transactions 的失败记录,error 与 revert_reason 安全置为 NULL。

3. 表结构与设计权衡

3.1 交易级失败明细视图:{db}.failed_txs

采用 ClickHouse VIEW(动态视图),在查询时将 {db}.transactions FINAL 与 {db}.traces FINAL(depth = 0)进行 LEFT JOIN:

列名类型说明
blockUInt64区块高度(别名,与 block_number 相同)
block_numberUInt64区块高度
tx_indexUInt32块内交易索引
tx_hashFixedString(32)交易哈希(原始字节,展示用 hex())
block_timestampDateTime('UTC')出块时间(UTC)
fromFixedString(20)交易发起方地址
toNullable(FixedString(20))目标合约/接收地址(合约创建交易为 NULL)
method_idString调用方法 4 字节 selector(substring(input, 1, 4))
gas_usedUInt64实际消耗的 gas
feeUInt256消耗的手续费(wei):gas_used * effective_gas_price
errorNullable(String)顶层帧错误信息(如 execution reverted, out of gas;无 trace 为 NULL)
revert_reasonNullable(String)解码后的 revert 原因字符串(无文字原因或无 trace 为 NULL)

为什么是 VIEW 而不是写入触发物化视图(Insert-Trigger MV)?

  1. 时序不一致与写入顺序依赖:在底层写入链路中(src/sink_clickhouse.rs),区块提交顺序为先写入 transactions,后写入 traces。若在 transactions 上建立写入触发的 MV 并尝试 JOIN traces,在触发时刻对应交易的 traces 尚未落库,会导致 trace 关联信息全部丢失。
  2. 数据可用性覆盖:traces 仅存在于 2026-09-25 之后开启追踪的区块。VIEW 架构以 transactions 为基准 LEFT JOIN traces,在无 trace 的历史区块中优雅回退为 NULL,在有 trace 的新区块中精准展示解码详情,且完全享受 FINAL WHERE is_deleted = 0 带来的幂等与防重组保障。

3.2 合约日级故障率表:{db}.contract_daily_failure_rate

记录每个合约在各个已收盘自然日(UTC)的调用总数、失败数、成功数、故障率以及因失败浪费的手续费总额。

列名类型说明
dateDateUTC 日期
contractFixedString(20)目标合约地址(transactions.to)
total_txsUInt64当天调用该合约的全部交易数
failed_txsUInt64失败交易数(status = 0)
success_txsUInt64成功交易数(status = 1)
failure_rateFloat64故障率:failed_txs / total_txs(0.0 ~ 1.0)
total_fee_lostUInt256当天该合约失败交易所消耗的手续费总和(wei)
versionUInt64写入版本号(ReplacingMergeTree 默认 1)
is_deletedUInt8逻辑删除标记(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)上抽样核验:

  1. ABI Error(string) 解码:
    • 区块:72077795,交易哈希:0x65A5C483CFE30A187B2AEF07BDBF0FA51ED1C63B0A3F895F97CD6D24281354FA
    • output: 08C379A000...0002000...0000353504C00...(长度 3,ASCII 码 0x53504C)
    • 解码结果:error = 'execution reverted', revert_reason = 'SPL'
    • 消耗手续费:1738509696000 wei。
  2. Solidity Panic(uint256) 解码:
    • 区块:72083204,交易索引:5,交易哈希:0x5114EAA750D14672F52F5F0CFD74AA285B02E4DADE7019A7EF781A7CD47BA2A7
    • output: 4E487B710000000000000000000000000000000000000000000000000000000000000011
    • 错误码:0x11(17,算术溢出/下溢)
    • 解码结果:error = 'execution reverted', revert_reason = 'arithmetic underflow or overflow'
    • 消耗手续费:243210212520000 wei。
  3. Out of Gas 交易:
    • 区块:72077012,交易索引:13,交易哈希:0xC886117D4A6122A208B69E9B9D00219AC381B80B6BF25C10582EEB0184088E2E
    • 解码结果:error = 'out of gas', revert_reason = NULL。
  4. 历史未追踪区块(Traces 未开启):
    • 区块:72039003,交易哈希:0x43EA3399D254E7F3A689019EF5813B3E0AD6B00A3D371AB7B6BBA7F05E7FD640
    • 结果:error = NULL, revert_reason = NULL,交易基本信息与手续费正常计算。
  5. 实时节点 RPC debug_traceTransaction 双向确认:
    • 对节点近端(~13 秒状态保留窗口内)最新区块 72151835 交易 0x14f28ecede56c8f6a0d0c5deb1b78ac481534038899ea7e070d2ca5e906a1900 调用 debug_traceTransaction({'tracer':'callTracer'}):
    • 节点返回:error = 'execution reverted', revertReason = 'SPL', output = '0x08c379a0...53504c...',与 ClickHouse 实时解码行为完全一致。

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 安装步骤

  1. 确认前置表已存在:{db}.transactions 与 {db}.traces(执行过 init-schema)。
  2. 执行 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;

On this page