方法选择器字典 + 解码交易视图(method_selectors / transactions_named)
对应文件: - schema/clickhouse/derived/016_method_selectors.sql - scripts/derived/gen_method_selectors.py
对应文件:
schema/clickhouse/derived/016_method_selectors.sqlscripts/derived/gen_method_selectors.py
是什么
{db}.method_selectors:一张手工维护的静态参考表(不是从链上数据派生出来
的),把 4 字节 ABI 函数选择器({db}.transactions.input 的前 4 字节,
selector = keccak256(ascii(canonical_signature))[:4])映射到人类可读的
signature / name。种子数据来自公开、知名的 ABI:ERC20 / ERC721 / ERC1155、
WETH9、Uniswap V2 Router / V3 SwapRouter / Universal Router、
Multicall / Multicall3、Permit2、Gnosis Safe、EIP-1967/897 代理、
OpenZeppelin AccessControl / Ownable / Pausable,以及 Arbitrum 的
ArbSys(0x64)/ ArbRetryableTx(0x6E)预编译合约。目前共 118 行,
选择器互不冲突(生成脚本会在冲突时直接报错拒绝生成)。
{db}.transactions_named:在 {db}.transactions 的每一行基础上,附加解码出的
method_id(4 字节选择器)、以及命中 method_selectors 时的 method_name /
method_signature。
表结构
CREATE TABLE {db}.method_selectors
(
selector FixedString(4),
signature String,
name String,
source LowCardinality(String), -- 分组标签,如 erc20 / uniswap-v3 / arbsys-precompile
version UInt64,
is_deleted UInt8
)
ENGINE = ReplacingMergeTree(version, is_deleted)
ORDER BY selector;method_id 为 NULL 的两种情形(不适用"函数选择器"这个概念):
- 合约创建交易(
to IS NULL)——这时input是初始化字节码,不是对已有合约的 调用数据; - 空/过短 calldata(
length(input) < 4)——纯转账或无参数调用。
method_name / method_signature 为 NULL 表示 method_id 存在但不在当前
目录里(未收录的选择器),不是函数没有名字的空字符串——见下面「LEFT JOIN
的一个坑」。
LEFT JOIN 的一个坑:ClickHouse 默认不用 NULL 补未命中
{db}.transactions_named 的 JOIN 部分在开发过程中踩到一个真实的坑:
ClickHouse 的 LEFT JOIN 默认(join_use_nulls = 0)用右表列的默认值
(String 类型是空字符串 '')填未命中的行,而不是 NULL。本地验证时用
method_name IS NOT NULL 统计覆盖率,第一版返回了 decoded = total
(100% "覆盖",包括没打上名字的行),一看就不对。
修复:在视图定义末尾加 SETTINGS join_use_nulls = 1,并确认过这个设置会
固化进视图定义本身(用另一个不带该 setting 的查询去 SELECT * FROM view
也生效),不是只在建视图那一次会话里生效。修完后未命中选择器的
method_name 才是真正的 NULL,覆盖率统计才有意义(见下面「本地验证」)。
CORRECTNESS RULE 适用性说明
AGENTS.md 的 CORRECTNESS RULE 针对的是"从原始表 SUM/COUNT 出聚合结果"的派生表
——ReplacingMergeTree 原始表可能有重复插入(backfill 续跑)和 reorg 墓碑
(is_deleted=1 的高 version 行),insert-trigger 的 MV 如果直接 SUM/COUNT
插入行会重复计数。
method_selectors / transactions_named 都不属于这一类:
method_selectors不对任何链上表做 SUM/COUNT,是我们自己拥有、可幂等重新 播种的参考数据——ReplacingMergeTree(version, is_deleted)+ORDER BY selector让"追加/修正一行选择器"变成"提高 version 重新 INSERT",... FINAL WHERE is_deleted = 0折叠成每个 selector 一行,不需要 MV。transactions_named是一个非物化的普通 VIEW,每次查询都现算, 底层FROM {db}.transactions AS t FINAL WHERE t.is_deleted = 0已经写进视图定义(调用方不需要自己记得加),继承的正是原始表自身的去重/ reorg 语义,view 本身不引入任何新的重复计数风险。
安装与补算
sed 's/{db}/robinhood/g' schema/clickhouse/derived/016_method_selectors.sql \
| clickhouse-client --multiquery用 HTTP 接口的话该文件包含 3 条语句(CREATE TABLE / INSERT /
CREATE OR REPLACE VIEW),HTTP 接口不支持一次发多条语句,需要按
CREATE TABLE ...;、INSERT INTO ... VALUES (...);、
CREATE OR REPLACE VIEW ...; 切成 3 次独立请求发送(同 003_backfill.sql 的
HTTP 用法说明)。
没有"补算历史数据"这一步——这不是从 {db}.logs/{db}.transactions 派生出来
的表,method_selectors 的种子数据一次性 INSERT 即可覆盖所有历史和未来交易,
transactions_named 是非物化视图,建好即对全表(历史+新增)生效,不需要
003_backfill.sql 那种"MV 建立前的数据要单独补"的步骤。
新增/修改选择器:编辑 scripts/derived/gen_method_selectors.py 里的
SELECTORS 列表,重新生成并替换 016 文件里的 INSERT ... VALUES 数据段,
提交 diff:
python3 -m venv /tmp/venv && /tmp/venv/bin/pip install pycryptodome
/tmp/venv/bin/python scripts/derived/gen_method_selectors.py > /tmp/method_selectors.inserts.sql(脚本自带 transfer(address,uint256) == 0xa9059cbb 的 keccak self-test,
以及选择器冲突检测——冲突会直接 sys.exit,不会静默生成错误数据。)
本地验证(精确匹配)
拓扑:本地 Docker ClickHouse(http://127.0.0.1:8123),真实节点 RPC 回填,
scratch db msel_wt40,区块区间 [72,040,000, 72,042,000)(2000 个块,
回填当天链头约 72,044,420)。
chain-indexer backfill --sink clickhouse --from 72040000 --to 72042000
→ blocks=2000 txs=12073 logs=55722| 验证项 | 结果 |
|---|---|
method_selectors FINAL 行数 / 去重后行数 | 118 / 118(无重复 selector) |
transfer(address,uint256) 选择器 | hex(selector) = 'A9059CBB',name = 'transfer',与题面给定值精确匹配 |
transactions_named 行数 vs transactions FINAL WHERE is_deleted=0 行数 | 12073 = 12073(精确相等,视图未产生任何行膨胀/丢失) |
独立 RPC 交叉验证(eth_getTransactionByHash,非 ClickHouse 路径) | 3 笔命中 method_name='execute' 的交易,直接对节点查询 input 字段前 4 字节,均为 0x3593564c,与 ClickHouse 侧解码结果精确一致:0x4223ce2a2c55082365c83702e2192a08f1901cf771ef0a5a069910a575835cef0x102eb59d7050f22c5ecdafae9bfb9a627a76adda7f0c2f7aa2a27ff577cb283d0x9939bd8d5d6f97242ec669d6508388b3c6b217e789bf7db73e2277c2cd3ce2d3 |
本地样本覆盖率(method_id IS NOT NULL 中能解出名字的占比) | 2814 / 11426 = 24.63% |
修复 join_use_nulls 问题之前,同一条覆盖率查询错误地返回 100%(见上文
「LEFT JOIN 的一个坑」)——记录在这里是为了说明这项验证真的跑出来会失败的
中间状态,不是凑数。
生产环境研究:按交易数排名前 50 的 method id
只读查询,SETTINGS max_threads=2, max_execution_time=90,通过
ssh [email protected] 读生产 ClickHouse(未做任何写入)。范围:
{db}.transactions WHERE is_deleted=0 AND to IS NOT NULL AND length(input)>=4
(全部历史,不分区间),共 124,890,974 笔"可解码"交易。
总体覆盖率:27,836,932 / 124,890,974 = 22.29%(用当前 118 个种子选择器 匹配,全表口径,不只是 Top 50)。
Top 50(selector / 目录里的 name,未收录留空 / 交易数):
| # | selector | name(本目录) | 交易数 |
|---|---|---|---|
| 1 | 6bf6a42d | (未收录) | 15,946,248 |
| 2 | 4d819a2a | (未收录) | 11,710,362 |
| 3 | 095ea7b3 | approve | 10,352,248 |
| 4 | 3593564c | execute | 7,343,743 |
| 5 | 00000000 | (未收录) | 5,402,242 |
| 6 | 04e45aaf | (未收录) | 4,332,060 |
| 7 | b6621842 | (未收录) | 4,067,707 |
| 8 | cce7ec13 | (未收录) | 3,604,439 |
| 9 | 39ecce49 | (未收录) | 3,105,851 |
| 10 | b344ecec | (未收录) | 2,930,043 |
| 11 | 5ae401dc | multicall | 2,390,989 |
| 12 | 3e0f9c3c | (未收录) | 2,030,433 |
| 13 | 765e827f | (未收录) | 1,679,944 |
| 14 | c1120e3d | (未收录) | 1,542,552 |
| 15 | ac9650d8 | multicall | 1,511,937 |
| 16 | 5965a4ff | (未收录) | 1,459,814 |
| 17 | 0a2b8f36 | (未收录) | 1,426,547 |
| 18 | b6f9de95 | swapExactETHForTokensSupportingFeeOnTransferTokens | 1,328,297 |
| 19 | 24856bc3 | execute | 1,304,851 |
| 20 | a9059cbb | transfer | 1,230,161 |
| 21 | e6cb474f | (未收录) | 1,192,050 |
| 22 | 18a54a74 | (未收录) | 960,324 |
| 23 | a84b47b4 | (未收录) | 946,473 |
| 24 | 791ac947 | swapExactTokensForETHSupportingFeeOnTransferTokens | 864,195 |
| 25 | 161ac21f | (未收录) | 786,752 |
| 26 | f2c42696 | (未收录) | 769,184 |
| 27 | 00000008 | (未收录) | 742,942 |
| 28 | 5fc3c09f | (未收录) | 736,948 |
| 29 | 87517c45 | (未收录) | 658,349 |
| 30 | 83701d69 | (未收录) | 544,083 |
| 31 | f3f77a0e | (未收录) | 531,916 |
| 32 | 01000bd7 | (未收录) | 512,077 |
| 33 | 0000003a | (未收录) | 483,162 |
| 34 | c7d693bf | (未收录) | 479,395 |
| 35 | 19409a60 | (未收录) | 467,585 |
| 36 | b3938887 | (未收录) | 447,600 |
| 37 | 95c3abcf | (未收录) | 434,839 |
| 38 | 00000005 | (未收录) | 411,473 |
| 39 | aededc1a | (未收录) | 392,263 |
| 40 | 6ab3d6ed | (未收录) | 367,649 |
| 41 | 2e1a7d4d | withdraw | 339,582 |
| 42 | a59ac6dd | (未收录) | 308,261 |
| 43 | b1521cf6 | (未收录) | 303,489 |
| 44 | a0328eb2 | (未收录) | 301,499 |
| 45 | 036f80a5 | (未收录) | 296,232 |
| 46 | 0273a58c | (未收录) | 288,378 |
| 47 | 08c1284c | (未收录) | 283,187 |
| 48 | c5d78cea | (未收录) | 265,526 |
| 49 | 7299095e | (未收录) | 264,629 |
| 50 | 2213bc0b | (未收录) | 258,846 |
Top 50 里目前收录了 9 个(approve / execute ×2 / multicall ×2 /
transfer / withdraw / 两个 SupportingFeeOnTransferTokens 变体),
Top 50 范围内的加权覆盖率 26,666,003 / 100,339,356 = 26.58%。
已知缺口(未收录、留作后续)
排名第 1、2 的选择器(6bf6a42d、4d819a2a)合计接近 2800 万笔交易,
显著高于任何一个已收录方法,值得优先补齐。查过公开的 4byte.directory,
分别返回过 startBlock(uint256,uint64,uint64,uint64) 和一个 DEX 聚合器风格的
swap((...)[],address,uint256,uint256,uint256) 元组签名候选——但
4byte.directory 是众包数据库,条目未经这条链验证(没有拿对应 to 地址的已验证
合约源码/ABI 交叉核对过参数形状),本 PR 不把它们当"知名 ABI"塞进种子表,
避免在参考表里放不确定的名字。这两个、以及 00000000 / 00000008 /
0000003a / 00000005 这类高位全零的选择器(形状上更像是某个自定义合约的
低数值函数选择器,也可能是 Robinhood 相关合约的自有方法),都建议作为独立的
后续任务:拿到对应 to 地址在生产库里的合约字节码/已验证源码后再收录,
不在本 PR 范围内。
事件签名库 + 解码日志视图(`event_signatures` / `logs_named`)
面向直接写 SQL 的内部团队:给定 logs.topic0,查出这是哪个事件、参数长什么样, 并对生产环境里最常见的一批事件提供开箱即用的解码视图。
Traces 数据集(traces / contract_creations / native_transfers / _trace_gaps)
对应 DDL:sql/traces.sql(表与两个派生 MV)+ sql/traces_block_hash.sql(block_hash 迁移, 2026-09-25 加入)。写入方是 chain-indexer follow --traces(src/trace.rs 解码、 src/sink_cli…