Crypto OS
Technical Crypto OS第一阶段 · Blockchain Developer Mental Model

T4 · RPC 是什么

你的 DApp 其实在跟谁说话?

练习的能力
Builder
动手
对同一个查询分别用两家 RPC 服务,比较延迟、限速和历史数据的可用范围。
AI Lab
让 AI 写一个带重试、超时与多节点回退的 Provider 封装,自己补上它漏掉的幂等处理。

一个现实问题

本地跑得好好的 DApp,上线第三天开始出怪事。

  • 有用户说打开页面余额是 0,刷新几次又对了。
  • 有用户说交易记录少了几条,第二天又全了。
  • 你自己怎么试都正常。

你去翻日志,看到一堆 429。再往下翻,看到几条超时。再往下,看到一次返回的区块高度比五分钟前那次还要小

这时候你才意识到一件事:**你的代码从头到尾没有碰过任何一条链。**它碰的是一个 HTTP 端点——一个你没写过、没部署过、没监控过、出了问题也进不去看日志的服务。

那个端点后面是谁?它凭什么给你数据?它什么时候会不给?它给错了你怎么知道?

思想实验

假设你在做一个报表系统,数据来自一个只读的数据库副本。有人告诉你这个副本有五个特点:

  1. **它会限速。**每秒超过若干次查询就拒绝你,而且不同查询的「重量」不一样——查一行和扫一千万行算的额度差几百倍。
  2. **它可能落后。**主库写入之后,副本什么时候同步完没有保证。你连续查两次,第二次可能比第一次更旧,因为负载均衡把你分到了另一台机器。
  3. 它可能没有历史。为了省磁盘,它只保留最近一段时间的数据。查更早的,它不是返回空,是报一个你看不懂的错
  4. **它可能返回假数据。**没有任何机制阻止它这么做。你收到的是一个 JSON,你无法从这个 JSON 本身判断它对不对。
  5. **它不归你管。**它挂了,你只能等。

现在问自己一个问题:在这五个前提下,你还能做出一个可靠的报表系统吗?

能,但你的架构必须显式地处理这五件事:限速要有退避和排队;落后要有单调性校验;历史缺失要提前知道边界在哪;假数据要有交叉验证;不归你管要有备份来源。

这五件事,就是这一章的全部内容。

把「只读副本」换成「RPC 端点」,上面每一条都原样成立。差别只有一个:那个数据库至少是你们公司的,而这个端点不是。

你来决定

你要给一个即将上线的产品选 RPC 方案。

观察结果

四个选项的差异,可以收成五个维度:

维度公开免费端点托管服务自建节点多源加自有封装
成本按用量,随规模线性增长固定,前期高最高,但可控
限速严格且不透明明确额度,超额计费只受硬件限制由你自己分配
历史深度通常很浅看套餐,归档要加钱你自己决定按方法路由
信任完全信任对方完全信任对方只信任自己可交叉验证
故障时只能等只能等你自己修自动回退

五个维度里,只有「信任」这一条是无法用钱解决的。

其余四条都是取舍:多花钱买额度、多花钱买归档、多花人力自建。只有「我怎么知道这个 JSON 是真的」这件事,不管你付多少钱,答案都一样——你不知道,除非你自己验证。

这是理解 RPC 最重要的一句话:**用第三方 RPC,意味着你信任它返回的数据。**你的产品的去中心化程度,在接入那一刻就被这个端点封了顶。

建立模型

一次调用真正走过的路:

  1. 你的代码
  2. Provider 封装
  3. HTTPS 到服务商入口
  4. 负载均衡挑一个实例
  5. 某个节点进程
  6. 它本地的数据库
最后两步是关键:你每次请求碰到的可能是不同的实例,它们的同步进度并不一致。

决定你产品上限的三个维度:

一、限速。不要按「每秒多少次请求」来理解,要按「计算量额度」来理解。一次读余额和一次扫描一百万个区块的日志,消耗的额度可能差几百倍。所以你的用量曲线不取决于用户数,取决于你调用了哪些方法

优化的第一步永远是:把日志范围缩小、把批量查询合并、把不变的数据缓存住。T15 会讲缓存层。

**二、数据深度。**节点分三种:只保留最近状态的(大多数),保留全部区块但状态被裁剪过的,以及保留每一个历史高度完整状态的归档节点。

查一个很老高度的余额或存储,前两种会报错而不是返回空。这个错误信息通常长得完全不像「没有历史数据」,第一次见到的人一般会以为是自己参数写错了。

**三、一致性。**同一时刻,两个实例的最新高度可能不同;链末端还会重组。这意味着两件事:

  • 同一个逻辑请求连着发两次,第二次可能读到更旧的状态。
  • 你按「最新」读到的数据,可能在几秒后就不存在了。

处理方法是用明确的区块标签,而不是每次都用「最新」:

标签含义什么时候用
latest当前这个实例认为的最新区块展示用,可以接受抖动
safe / finalized已被协议标记为不易回滚 / 不可回滚涉及资金的判定
具体高度指定那一个区块批量索引、对账、任何需要可重放的场景
pending包含内存池里待处理的内容只用于查 nonce,别用于读余额

**一条实践规则:任何需要「跑两次结果一样」的逻辑,都必须把区块高度固定下来,而不是用 latest。**索引器尤其如此——用 latest 写出来的索引器是不可重放的,出了问题你连复现都做不到。

自有封装层该有的能力:

能力解决什么
超时上游挂起时不拖垮你自己的连接池
重试加指数退避瞬时抖动与 429
熔断一家持续失败时不再浪费时间打它
按方法路由归档查询走贵的源,普通查询走便宜的源
高度单调性保护回退到落后的源时,不让业务看到高度倒退
交叉验证对关键数据向两个源各问一次,不一致就告警

最后两条是把「多个后端」和「高可用」区分开的地方。只有前四条,你做的是负载均衡;加上后两条,你才是在处理不信任

它叫什么

Node节点

一台运行着链客户端、保存着链数据并执行验证的机器。

按保留的数据量分成几档:只保留近期状态的、保留全部区块但状态被裁剪的、以及保留每个高度完整状态的归档节点。档次决定了你能查多久以前的数据,也决定了成本。

JSON-RPC远程过程调用

链对外暴露的接口协议。请求体是一个 JSON,里面有方法名和参数数组,返回体里要么有 result 要么有 error。

它的方法名是跨客户端标准化的,这是它比任何 SDK 都稳定的原因:SDK 的 API 半年一变,eth_getBalance 十年没变。这也是为什么这门课的示例都直接用它。

Provider提供者 / 连接器

你代码里那一层负责发起 RPC 调用的抽象。

它是你唯一能插入重试、超时、缓存、路由、校验的地方。如果你的业务代码到处直接发 HTTP 请求,这一章讲的所有能力你都没有地方放。

Rate Limit限速

服务商对调用量的限制。通常不是按请求数计,而是按每个方法的计算量加权计。

它的失败表现有两种:明确的 429,以及降级成错误对象返回。后者最危险,因为不检查错误字段的代码会把它当成正常的空结果。

Archive Node归档节点

保存每一个历史高度完整状态的节点。查任意历史时刻的余额、存储槽、合约调用结果都需要它。

它的存储成本比普通节点高一个数量级以上,服务商也按更贵的档次收费。很多数据产品的成本结构就是在这里被决定的。

Block Tag区块标签

查询时指定「按哪个区块的状态来算」的参数。latest、finalized、具体高度各有各的用途。

**这是一个被严重低估的参数。**用错它,你的代码在 99% 的时间里都是对的,剩下 1% 的时间给出无法复现的错误结果。

动手

动手对同一个查询用两家 RPC,比较延迟、限速和历史深度两个不同来源的 RPC 端点 + curl0 元,用免费档即可

**全程只读查询,不发任何交易,不需要私钥,不涉及任何真实资产。**建议一个用公开免费端点,一个用托管服务的免费档,差异会更明显。

比延迟。

for i in $(seq 1 10); do
  curl -s -o /dev/null -w '%{time_total}\n' -X POST <端点 A> \
    -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'
done

两个端点各跑一次,对比中位数和最大值。看最大值,不要只看平均值——用户感知到的卡顿来自长尾。

比高度是否一致。

同一时刻分别问两家 eth_blockNumber,把十六进制转成十进制,看差几个块。

再连着问同一家十次,看有没有出现后一次比前一次小的情况。出现了,说明你被分到了不同的实例上,这就是你必须做高度单调性保护的原因。

比历史深度。

curl -s -X POST <端点 A> \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance",
       "params":["<任意活跃地址>","0x<一个很老的区块高度>"]}'

把高度从最近往回推,二分查找出每家开始报错的那个高度。这就是它的历史边界。

仔细读那条错误信息。它多半不会写「没有归档数据」,而是一个关于状态查找失败的底层错误。记住它长什么样,以后在生产日志里再见到就能一眼认出来。

比日志查询的范围上限。

curl -s -X POST <端点 A> \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
        "fromBlock":"0x...","toBlock":"0x...",
        "address":"<一个活跃的代币合约地址>"}]}'

从 100 个区块开始,每次翻倍,直到被拒绝。记下两家各自的上限,以及它们是按区块跨度限制还是按返回条数限制——这两种限制会让你的索引器写成完全不同的形状。

比限速。

连续发 100 次 eth_blockNumber,统计第几次开始返回 429 或错误对象。

然后换一个重方法(比如一个大范围的 eth_getLogs)再试,看多少次就被限住。对比两个数字,你就直观理解了「按计算量计费」是什么意思。

留意被限时返回的 HTTP 状态码和响应体。**有没有 Retry-After 头?错误是在 HTTP 层还是在 JSON 的 error 字段里?**你的重试逻辑必须同时覆盖这两种。

做一次交叉验证。

对同一个地址、同一个具体区块高度(不要用 latest),向两家各查一次 eth_getBalance,比对结果。

再换一个合约的 eth_call 做同样的事。

结果应该完全一致。不一致,说明至少有一家的数据有问题——而这正是你在生产环境里唯一能自己发现这件事的办法。

把结论写成一张表。

两家各自的:延迟中位数与 P99、历史边界高度、日志范围上限、被限速的阈值、以及价格。

这张表就是你选型的依据,也是你以后写 Provider 路由规则的依据。三个月后重新测一遍,这些数字会变。

AI Lab

AI Lab让 AI 写一个带重试、超时与多源回退的 Provider 封装,你补上它漏掉的那部分Level 2 · AI Copilot

分两步,第二步是重点。

第一步:
写一个 RPC Provider 封装,要求:
支持多个后端端点、超时、失败重试加指数退避、
按方法路由(归档类方法走指定端点)、以及熔断。
用我指定的语言,不要用任何我没提到的第三方库。

第二步:
现在假设这个封装被用在一个需要重放的索引器里。
逐条指出你上面的实现会在哪些情况下返回「不一致但看起来正常」的结果。
对每一条给出修复方案。

第二步要逼出来的是这三件事,模型第一次写基本都会漏:

  • 幂等性。eth_sendRawTransaction 超时后重试是相对安全的(重复广播同一份签名字节会被节点当作已知交易),但如果重试时重新构造并签名,钱就会出去两次(T3 讲过)。模型经常会把这两件事混为一谈。
  • **高度回退。**从源 A 切到源 B,B 落后几个区块,你的索引器会读到一个比自己已处理位置更旧的状态,然后开始写入错误数据。
  • **超时不等于没执行。**超时只说明你没等到响应,上游可能已经处理完了。对读方法无所谓,对写方法是致命的。

拿到它的修复方案后,自己挑一条在本地构造出来验证:把一个源换成一个故意返回旧高度的假服务,看它的封装会不会让业务读到倒退的数据。

**能让模型写出封装的人很多,知道该拿什么去质问这份封装的人很少。**这才是这个 Lab 训练的东西。

AI 说完之后,你必须自己验证

  • 它用到的库名、类名和方法签名真实存在,你能在官方文档里查到同名的方法
  • 它有没有把 HTTP 层的 429 和 JSON 响应体里的 error 字段都当作失败处理
  • 它重试时有没有区分幂等方法与写方法,对广播交易的重试是否安全
  • 它回退到另一个源时,有没有处理新源的区块高度更低这件事
  • 它有没有把超时当成失败,而实际上超时的请求可能已经在上游执行了
  • 它写的十六进制与十进制转换没有搞错,特别是区块高度和余额
  • 它有没有把「调用成功返回」当成「数据一定正确」

真实案例

一家托管服务故障,一批 DApp 同时白屏

某次大型托管 RPC 服务出现区域性故障,大量前端在同一时间无法读取任何链上数据。

讽刺的地方在于:链本身一直在正常出块。链没停,产品停了。

这件事暴露的是一个结构性问题:一批号称去中心化的应用,实际上共享同一个中心化的单点。事后不少团队才开始做多源回退,而这件事本来在架构评审时就该被提出来。

免费端点返回落后数据,下单价格算错

一个交易界面用免费公开端点读取池子状态来计算报价。高峰期该端点落后了若干个区块。

用户看到的是几十秒前的价格,按这个价格提交的交易到链上必然滑点超限而失败,或者更糟——在一个明显不利的价格上成交。

根因不是「免费端点不好」,是用 latest 读取价格敏感数据,却没有校验数据的新鲜度。正确做法是把返回里的区块高度一起读出来,和你认为的最新高度比对,差得太多就拒绝报价。

日志范围上限逼出的架构

一个数据产品要回扫某个合约从部署至今的全部事件。直接用一个大范围的日志查询,端点直接拒绝。

于是团队改成按固定跨度分片、并发拉取、每片记录检查点、失败重试。这套东西写完之后,他们才发现自己写了一个索引器的雏形。

**几乎所有链上数据基础设施,都是从「日志查询范围被限制」这一刻开始的。**T17 会把这个雏形补完,包括重组处理和断点续传。

被诱导添加的恶意 RPC

一类诈骗手法:诱导用户在钱包里添加一个「更快的」自定义 RPC 端点。

这个端点可以对余额查询返回任意数字。受害者在钱包里看到一大笔「到账」的资产,以为交易完成,于是把自己的东西交了出去。链上从头到尾没有发生任何转账。

这是「用第三方 RPC 意味着你信任它返回的数据」这句话最直白的一次演示。你的界面上显示的每一个数字,都只是某个端点声称的数字。

改一个变量

如果你完全不信任任何 RPC 返回的数据

你需要自己验证。路径有三条:自建节点(最彻底,也最贵);向多个独立来源交叉验证(便宜,能发现分歧但不能判断谁对);或者用轻客户端的思路——只信任区块头,其余数据要求对方给出可验证的证明。

第三条正是 T2 里 Merkle 结构的用武之地:给你一个状态值,同时给你一条到状态根的路径,你自己算一遍。

大多数产品选第二条,因为它的性价比最高。但你至少要知道自己选的是哪一条。

如果你的调用量涨了一百倍

按用量计费的账单会变成一个真实的成本项,而且它的增长曲线和你的收入曲线未必匹配。

这时候真正的优化不是换更便宜的服务商,是减少调用:把读多写少的数据缓存住、把高频轮询改成事件订阅、把每用户一次的查询改成批量聚合、把能落库的东西落库。T15 和 T16 讲的就是这件事。

自建节点的临界点通常出现在这个阶段——不是因为想去中心化,是因为算下来更便宜。

如果你改用 WebSocket 订阅代替轮询

延迟和调用量都会大幅下降,但你会换来一类新问题:断线期间的数据是丢的。

重连之后必须从上次处理到的高度回补,否则会静默漏掉一段区块。而且不同服务商对订阅的重组通知处理方式不同,有的会推送回滚事件,有的不会。

订阅是优化,不是替代。一个可靠的系统底下永远要有一条能按高度重放的补偿路径。

如果换成 Solana

方法名完全不同(读账户用 getAccountInfo,读余额用 getBalance),但这一章的五个维度一条不少地成立:限速、历史深度、一致性、信任、单点故障。

有一处更严格:Solana 的历史数据默认只保留一段时间,更早的要走专门的历史服务。一个「查半年前的交易」的需求,在这里的成本结构和 EVM 上完全不同。T6 会讲它的账户模型,你会发现连查询的形状都变了。

带走的问题

2
为什么需要 Blockchain?

如果你的产品所有数据都来自一个第三方端点,而用户也没有任何办法验证,那它和一个中心化 API 在信任结构上没有区别。链的价值在于「任何人都能独立验证」,而这个价值是否传递到你的用户手上,取决于你这一层怎么设计。

5
谁在支付?

RPC 账单由你付,不由用户付。这是一个容易被忽略的单位经济学问题:每个活跃用户每天消耗多少计算量额度,这个数字决定了你的产品能不能规模化。在还只有一百个用户的时候就算一遍,比在有十万用户时再算要便宜得多。

9
谁承担风险?

端点返回了错误数据导致用户损失,谁承担?合同上多半是你。所以「我们用的是知名服务商」不是一个风险控制手段,交叉验证和数据新鲜度校验才是。涉及资金判定的地方尤其如此。

本章自测

一句话带走

RPC 是你和链之间唯一的门,它的限速与数据完整性决定了产品的上限。

做完这一章的动手环节了?勾上它查看全部进度

本页目录