价格预言机数据集(oracle_feeds / oracle_prices)
Robinhood Chain(chain id 4663)上确实存在链上价格预言机,用于给约 60 多个代币化股票 / 加密资产 / 稳定币提供参考价。经独立验证,其形态是经典 Chainlink FluxAggregator (AggregatorV3Interface + AnswerUpdated/…
结论
Robinhood Chain(chain id 4663)上确实存在链上价格预言机,用于给约 60 多个代币化股票 /
加密资产 / 稳定币提供参考价。经独立验证,其形态是经典 Chainlink FluxAggregator
(AggregatorV3Interface + AnswerUpdated/NewRound 事件),不是 OCR2/OCR3(未发现
NewTransmission),也没有发现 Pyth / Redstone / Chronicle 的痕迹。
本次交付:{db}.oracle_feeds(feed 注册表,脚本填充)+ {db}.oracle_prices
(从 {db}.logs 派生的 MV)。
研究方法与证据
1. 事件签名先用节点自身计算,不凭记忆
用生产节点的 web3_sha3 RPC(对 Transfer(address,address,uint256) 先做了一次已知值
校验,结果与 001_erc20_transfers.sql 里硬编码的 topic0 完全一致,确认方法可信),
现算出以下候选事件的 topic0:
| 候选协议 | 事件签名 | topic0(web3_sha3 实算) | 链上是否出现 |
|---|---|---|---|
| Chainlink Flux/OCR1 | AnswerUpdated(int256,uint256,uint256) | 0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f | 是,62 个地址 |
| Chainlink Flux/OCR1 | NewRound(uint256,address,uint256) | 0x0109fc6f55cf40689f02fbaad7af7fe7bbac8a3d2186600afc7d3e10cac60271 | 是(与上表同一批地址伴生) |
| Chainlink OCR2/OCR3 | NewTransmission(uint32,int192,address,int192[],bytes,bytes32) | 0xf6a97944f31ea060dfde0566e4167c1a1082551e64b60ecb14d599a9d023d451 | 否 |
| Pyth | PriceFeedUpdate(bytes32,uint64,int64,uint64) | 0xd06a6b7f4918494b3719217d1802786c1f5112a6c1d88fe2cfec00b4584f6aec | 否 |
| Chronicle | LogPushed(bytes32,bytes) | 0x9d041a8cc415c772acd95a12cd9f5be2c46c6172974094f821ecdd24a37975c3 | 否 |
“否”是在对生产 {db}.logs(417,492,832 行,区块 1..72,044,367,17,891 个不同
topic0)按 topic0 IN (...) 的全表扫描下得到的零匹配,不是抽样结果。Redstone
按其设计本来就不产生持久化链上事件(价格数据在 calldata 里现付现验),因此这种“拉取式”
预言机原则上不会留下可供本方法发现的日志痕迹,本次未见其特征,但也不能仅凭日志排除其
存在——如后续需要,需改为扫描交易 calldata 特征。
2. 用真实合约交叉验证,不只信文档 / 搜索结果
公开文档(docs.chain.link/data-feeds/tokenized-equity-feeds/robinhood)声称 Robinhood
Chain 采用 Chainlink 标准 AggregatorV3Interface(latestRoundData()),但页面本身没有
给出可核实的合约地址;一次网络搜索摘要给出了 2 个候选地址(AAPL/NVDA),在写入任何
结论前先用节点 eth_call 独立验证,而不是直接采信搜索结果(该搜索结果本身还提示
"Robinhood Chain" 生态存在钓鱼站点,对不熟悉的区块浏览器域名一律未访问):
| 资产 | 代理合约(consumer 用的地址) | decimals() | description() | latestRoundData().updatedAt |
|---|---|---|---|---|
| AAPL | 0x6B22A786bAa607d76728168703a39Ea9C99f2cD0 | 8 | Robinhood AAPL / USD | 2026-09-24T19:55:25Z(新鲜) |
| NVDA | 0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F15 | 8 | RHNVDA / USD | 2026-09-25T06:22:16Z(新鲜) |
两个合约都有真实字节码(19,142 hex 字符 / ~9,571 字节),且都实现了标准
AggregatorV3Interface(decimals()/description()/latestRoundData() 均返回合理值),
证实预言机确实存在且在正常工作,而非文档臆造。
代理合约的 aggregator()(Chainlink EACAggregatorProxy 标准 getter)进一步给出了真正
产生数据的底层聚合器地址:AAPL -> 0xbb11a21267cfdb63d4935d99a499133dd1744acb,NVDA ->
0xc9d16e4f2569b9e3ea0468fd85844953713dc2a2——这两个地址正是上面 topic0 扫描发现的 62
个地址中的两个。
3. 从"发现两个"到"发现全部":直接对全链日志做 topic0+shape 扫描
不依赖搜索引擎能给出多少个例子,而是直接对 {db}.logs 按
topic0 = AnswerUpdated 且 topic1/topic2 非空、topic3 为空、data 恰好 32 字节 做去重
地址统计,一次拿到全部 62 个 feed 聚合器地址(这就是
scripts/oracle/discover_feeds.py discover 子命令做的事)。再对这 62 个地址逐一
eth_call decimals()/description(),61 个成功解析,1 个(0xa0a3e2149b4a7125ad521a20f2531962433959b7,
历史上只出现过 1 次 AnswerUpdated,区块 902149)在 description()/latestRoundData()
上都 execution reverted,判定为已废弃/自毁的旧 feed,按 fail-closed 原则不写入
oracle_feeds(宁可缺记录,不猜一个描述/decimals)。
61 个已确认的 feed 覆盖:
- 代币化股票(约 40 个,
description()多为Robinhood <TICKER> / USD或RH<TICKER> / USD):AAPL、NVDA、TSLA(RHTSLA)、MSFT(RHMSFT)、GOOGL、AMZN、META、 COIN、PLTR、BABA、TSM、ORCL、MSTR、GME、DELL、QQQ、SPY(RHSPY)、AMD(RHAMD)、 INTC(RHINTC)、CRCL、CRWV、IONQ、RGTI、RKLB、SPCX、CLSK、NBIS、ASML、EWY、SLV、 USO(RHUSO)、SGOV 等。 - 加密资产:ETH、BTC、WBTC、CBBTC、LINK、LBTC、weETH、wstETH、EURC。
- 稳定币 / 汇率型 feed(
decimals=18,语义是汇率而非美元价): weETH/eETH、wstETH/stETH、Syrup* 系列(USDC/USDT/USDG exchange rate)、USDT/USD、 USDC/USD、USDG/USD、USDE/USD、USDS/USD。 - 另有 3 个
... Managed ETH / USD(BOWD ×2、NEXORA、RHOD)——最后更新时间在 2026 年 3 月左右,明显已停更。
完整、权威的清单以 {db}.oracle_feeds 表内容为准(见下方安装步骤),本文档不重复维护
一份会过期的地址表。
数据质量说明(重要,务必读完再用这个数据集)
对 62 个 feed 按"最后一条 AnswerUpdated 所在区块是否落在链头附近(tip − 500,000
区块内,约最近 14 小时)"分类:
- 31 个 feed 的事件流持续产生到链头附近(含 NVDA)。
- 31 个 feed 的事件流在区块 ~15,636,111 ~ 17,262,918(对应 2026 年 7 月中下旬)
之间永久停止,此后再也没有任何日志——但通过
eth_call latestRoundData()直接读取 同一个地址(含 AAPL),返回的却是当天(2026-09-24)的新鲜数据,roundId远大于历史事件条数(AAPL:日志里只有 272 条AnswerUpdated,但当前roundId已到 668)。也确认了这 31 个地址在停止发日志之后完全没有任何其它 topic0 的日志(不是换了 个新事件签名,是彻底不再LOG)。
结论:这批 feed 在某个时间点之后的更新方式变了(大概率是底层聚合器实现升级/替换为
更省 gas、不写事件的版本,地址本身没变),导致 oracle_prices 对这 31 个 feed 的
"最新一行" 明显滞后于链上真实状态,不能当作"当前价格"使用。这个数据集对:
- 历史回放 / 某个时间点当时的价格:所有 62 个 feed 在"仍在发日志"的区间内都可信 (见下方验证)。
- "现在的价格":只有那 31 个仍在持续发日志的 feed 可以直接用最新一行;另外 31 个
必须对代理合约
eth_call latestRoundData()(不是本表)。这一点会在{db}.oracle_feeds之外单独提示,不在本次交付范围内自动修复(需要另开工作项调查 这 31 个聚合器为何停止发日志,是否有替代事件、或是否需要改用状态快照而非事件流)。
表结构
{db}.oracle_feeds(注册表,脚本填充,非 MV)
address FixedString(20) -- 聚合器合约地址(不是代理地址)
description String -- eth_call description()
decimals UInt8 -- eth_call decimals()
discovered_at DateTime('UTC')
version UInt64 -- ReplacingMergeTree 版本,重跑发现脚本会推进由 scripts/oracle/discover_feeds.py 两步填充:discover 子命令从
{db}.logs 里找出全部候选地址,resolve 子命令对每个地址 eth_call
decimals()/description() 并 upsert。解析失败(如已废弃合约)的地址不写入
(fail-closed,不猜默认值)。
{db}.oracle_prices(MV,来自 {db}.logs)
feed FixedString(20)
description Nullable(String) -- 来自 oracle_feeds JOIN,未注册的 feed 为 NULL
decimals Nullable(UInt8) -- 同上,NULL 而不是猜一个 8
round_id UInt256
answer Int256 -- 原始整数,未按 decimals 换算,禁止转 float
updated_at DateTime('UTC')
block_number / block_timestamp / tx_hash / tx_index / log_index
version / is_deleted -- 从源 log 行原样带过正确性方案:选 (a) insert-trigger MV,version/is_deleted 从源 log 行原样带过
(与 erc20_transfers 一致),理由:每一条 AnswerUpdated 本身就是一个天然带唯一键
(feed, round_id) 的离散事件,不需要按"已收盘周期"做聚合刷新;oracle_feeds 的 JOIN
只增加描述性列,不改变每条源日志产出的行数,因此 reorg/去重语义与 {db}.logs FINAL
完全一致。
安装与补算
# 0) 替换 {db} 为目标库,例如 robinhood
DB=robinhood
# 1) 建表 + 物化视图(此时 oracle_feeds 还是空的也没关系,先建表结构)
sed "s/{db}/$DB/g" schema/clickhouse/derived/017_oracle_prices.sql | clickhouse-client --multiquery
# 2) 发现 + 解析 feed 注册表(node RPC 走共享节点,默认限速 0.15s/请求,60 个地址约 20 秒)
python3 scripts/oracle/discover_feeds.py discover --db "$DB" \
--ch-url "$CH_URL" --ch-user "$CH_USER" --ch-password "$CH_PASSWORD" \
> /tmp/oracle_candidates.txt
python3 scripts/oracle/discover_feeds.py resolve --db "$DB" \
--rpc-url "$NODE_RPC_URL" \
--addresses-file /tmp/oracle_candidates.txt \
--ch-url "$CH_URL" --ch-user "$CH_USER" --ch-password "$CH_PASSWORD"
# 3) 回填历史日志(MV 只处理建视图之后的新数据);oracle_feeds 更新后也可重跑本步骤,
# 历史行的 description/decimals 会从 NULL 变成正确值(ReplacingMergeTree 去重安全)
sed "s/{db}/$DB/g" schema/clickhouse/derived/017_oracle_prices_backfill.sql | clickhouse-client --multiquery重新发现(例如怀疑有新 feed 上线,或某个 feed 描述变了):重跑第 2、3 步即可,全程幂等。
验证结果(独立计算,精确匹配)
样本:NVDA 聚合器 0xc9d16e4f2569b9e3ea0468fd85844953713dc2a2,区块
72,009,385,round_id = 1100(十六进制 0x44c)。
| 数据来源 | round_id | answer(原始整数) | updated_at(unix) |
|---|---|---|---|
{db}.logs 原始行,Python 直接对 topic1/topic2/data 大端解码(独立于 SQL 实现) | 1100 | 22557472086 | 1790317336 |
017_oracle_prices.sql 里的 SQL 转换逻辑(reinterpretAsInt256(reverse(...)) 等) | 1100 | 22557472086 | 1790317336 |
对同一地址 eth_call latestRoundData()(生产节点,实时) | 1100(低 32 位) | 22557472086 | 1790317336 |
三者逐位精确一致(价格换算:22557472086 / 10^8 = 225.57472086 USD)。
补充:{db}.logs 里符合 AnswerUpdated 形状(topic1/topic2 非空、topic3 为空、
data 长度 32)且能匹配 topic0 的历史行总数为 46,994 条,覆盖 62 个 feed 地址——
这是回填后 SELECT count() FROM {db}.oracle_prices FINAL WHERE is_deleted=0
(在未来新增行之前)应该精确等于的基线数字,用于验收本表是否完整回填、有无漏行。
生产环境访问记录(供复核)
- SSH 只读查询:
[email protected],clickhouse-client/curl走/opt/chain-indexer/clickhouse.env里的凭据,SETTINGS max_threads=2, max_execution_time=90,全程未写入生产库。 - 节点 RPC:仅用于
web3_sha3(算 topic0)、eth_getCode、eth_call(decimals()/description()/latestRoundData()/aggregator()/owner()), 单请求间隔 ≥0.15s,未做批量/并发请求。
DEX 代币日度价格数据集(dex_price_daily / v_dex_price_daily)
对应 SQL:schema/clickhouse/derived/031_dex_prices.sql。
ERC1155 转账与 NFT 持有数据集(erc1155_transfers / erc721_current_owner / erc1155_balances)
对应 SQL:schema/clickhouse/derived/009_erc1155_nft_owners.sql。 依赖已安装的 {db}.logs(sql/schema.sql)和 {db}.erc721_transfers (schema/clickhouse/derived/002_erc721…