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

T1 · Blockchain 数据究竟是什么

一条链上真正存了什么?

练习的能力
BuilderOnchain Literacy
动手
写一个脚本,拉取最近 10 个区块,打印每个区块的交易数、Gas 用量与第一笔交易的接收方。
AI Lab
让 AI 解释 State 与 Event 的区别,再让它举一个只看 Event 会得出错误结论的例子。

一个现实问题

产品给你一个需求:某个地址一收到 USDC,就给用户推送通知。

看起来五分钟就能做完。于是你打开文档,开始纠结第一个问题:这个「收到」,我该去哪里读?

  • 每隔几秒查一次这个地址的余额,变多了就推送?
  • 监听 USDC 合约的 Transfer 事件?
  • 把每个区块的交易都拉下来,逐笔解析?
  • 直接用第三方 API,省事?

四种做法都能做出一个 demo,但它们在真实世界里的表现差异巨大:有的会漏消息,有的会重复推送,有的会在链重组时推送一条不存在的转账。

选错的代价不是性能,是正确性。要选对,先得搞清楚:链上到底存了什么。

思想实验

把一条链想成两样东西,而不是一样。

第一样:一本只能追加的流水账。

每隔一段时间产生一个区块,区块里按顺序装着若干笔交易。写下去就不能改,也不能插队。这本账只记录动作:谁调用了什么、带了什么参数、付了多少手续费。

第二样:一张由流水账推导出来的当前状态表。

每个账户现在有多少余额、每个合约的每个存储槽现在是什么值。这张表不是被「写」进链里的,它是从创世块开始把所有交易依次执行一遍得到的结果

现在关键的问题来了:一笔交易执行时,改了哪些状态?

流水账里没有直接写。它只记了「调用了合约 A 的 transfer 函数,参数是地址 B 和数量 100」。至于这次调用最终改了多少个存储槽、有没有顺带触发别的合约,都要真的执行一遍才知道。

这就是为什么会有第三样东西:日志。合约在执行时主动喊一声「我刚刚做了一次转账,从 B 到 C,数量 100」。这一声被记录在交易回执里,专门给外部世界听。

  1. Transaction 我要做什么
  2. State 世界变成了什么样
  3. Event 我主动告诉你我做了什么
三者不是同一份数据。Event 是合约自愿发出的说明,不是状态变化的完整记录。

你来决定

回到那个推送需求。你选哪一种?

观察结果

四个选项暴露出同一件事:你以为在读「链上数据」,其实在读它的某一层投影。

你读的东西它真正是什么会漏掉什么
账户余额当前状态的一个切片中间过程,同区块内的多次变化
交易列表外部发起的动作合约内部的调用与转账
Event 日志合约自愿发出的说明不发 Event 或不按标准发的情况
执行追踪完整的调用树基本不漏,但又慢又贵

选哪一层,取决于你的业务能容忍什么。做一个余额展示,读状态就够;做资金对账,必须用追踪;做通知和索引,Event 是性价比最高的选择。

没有「最准确的那一层」,只有「和你的需求匹配的那一层」。

建立模型

把一条链的数据结构按层排开:

Block                       区块:一批交易的容器
 ├─ header                  头:父哈希、高度、时间戳、状态根
 └─ transactions[]          体:按顺序排列的交易
      └─ Transaction        一笔交易:from、to、value、data、nonce、gas
           └─ Receipt       回执:执行之后才产生
                ├─ status   成功还是失败
                ├─ gasUsed  实际消耗
                └─ logs[]   合约发出的事件
                     └─ Log  topics(可索引,便于过滤)+ data(内容)

State                       状态:由所有交易依次执行推导出来
 ├─ account balance         账户余额
 ├─ contract code           合约代码
 └─ contract storage        合约存储槽

四个要点,记住它们能让你少走很多弯路:

  1. Receipt 在执行之后才存在。 交易被打包不等于成功,status 要单独看。前端把「已广播」当成「成功」是最常见的 bug(T14 会展开)。
  2. Log 的 topics 是可索引的,data 不是。 这决定了你能按什么条件高效过滤:合约地址和 topics 可以,事件里的普通字段不行。
  3. State 只有「现在」。 想查历史某个高度的状态,需要归档节点(T4 会讲)。这是一个常见的成本陷阱。
  4. Event 不是状态。 合约可以发一个和实际状态完全不符的事件——这在攻击中被用过。真正要确认状态,得去读状态。

它叫什么

Block区块

一批交易的容器。区块头里的 parentHash 把它们串成一条链,改动任何一个历史区块都会让后面全部失效。

Transaction交易

一次由外部账户发起的状态变更请求。它只记录「要做什么」,不记录「结果是什么」。

State状态

当前所有账户余额、合约代码与存储的总和。它是所有历史交易执行后的结果,不是被单独存储的一份数据。

Receipt回执

交易执行后的结果记录:成功与否、消耗了多少 Gas、产生了哪些日志。

查一笔交易成没成功,看 Receipt 的 status,而不是看它在不在区块里。

Event / Log事件 / 日志

合约主动写入回执的结构化记录,供链下系统读取。合约本身读不到它。

topics 是可索引字段(第一个通常是事件签名的哈希),data 是其余内容。

Internal Transaction内部交易

合约在执行过程中发起的调用或转账。

不是一笔独立的交易,不会出现在交易列表里,只能通过执行追踪还原。很多「为什么浏览器上有这笔钱、我的程序却没读到」的问题,根源都在这里。

动手

动手用 JSON-RPC 直接读十个区块任意 RPC 端点 + curl 或 Node0 元,用公共端点即可

不用任何 SDK,直接调 JSON-RPC。目的是让你看清数据的原始形状。

拿到最新高度。

curl -s -X POST <你的-RPC-地> \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'

返回的是十六进制。这一点会坑你很多次:链上返回的数值几乎都是十六进制字符串。

取一个区块的完整内容。

curl -s -X POST <你的-RPC-地> \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",true]}'

第二个参数是 true 时返回完整交易,false 时只返回交易哈希。看一眼两者的体积差异,你就明白为什么索引器要控制这个参数。

取一笔交易的回执,找到 status 和 logs。

curl -s -X POST <你的-RPC-地> \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<交易哈希>"]}'

对照区块浏览器上的同一笔交易,确认你看到的字段和它显示的一致。

按事件过滤一段区块范围。

curl -s -X POST <你的-RPC-地> \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
        "fromBlock":"0x...","toBlock":"0x...",
        "address":"<代币合约地址>",
        "topics":["<Transfer 事件签名的哈希>"]}]}'

这一步是所有索引器的核心。试着把区块范围调大,看 RPC 从什么跨度开始拒绝你——这个限制会直接决定你的索引器怎么分片。

写成脚本,输出一张表。

拉最近 10 个区块,打印:高度、时间戳、交易数、总 Gas 用量、第一笔交易的接收方。

这就是你的阶段作品的第一版。

全程使用测试网或公共只读端点,不需要任何私钥。 这一章不发交易。

AI Lab

AI Lab让 AI 解释 State 与 Event 的区别,并给出一个会出错的例子Level 2 · AI Copilot

分两步问,第二步比第一步重要得多:

第一步:
解释 Blockchain 上 State 和 Event 的区别,说明为什么两者可能不一致。

第二步:
给我三个具体场景,在这些场景下,只监听 Transfer 事件的索引器会得出错误结论。
每个场景说明:会漏掉什么、后果是什么、正确做法是什么。

第二步是这个 Lab 的价值所在。让模型去找自己方案的反例,比让它给方案有用得多。

拿到三个场景后,自己去链上找一个真实例子来验证。找得到,说明这个坑是真的;找不到,就去问它「请给出一个真实的合约地址或交易哈希」——然后你多半会发现,它给的那个哈希并不存在。

这正是你需要亲身经历一次的事情。

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

  • 它给的 JSON-RPC 方法名和参数顺序,与官方规范一致
  • 它说的「可索引字段」确实只包括 topics,没有把 data 里的字段说成可过滤
  • 它举的「只看 Event 会漏掉」的例子,你能在真实链上找到一个对应的案例
  • 它写的示例代码里,十六进制与十进制的转换没有搞错
  • 它有没有把「交易在区块里」当成「交易成功」

真实案例

原生币转账没有 Transfer 事件

ETH、SOL 这类原生币的转账,不经过任何合约,因此不会产生 Transfer 事件

只监听 Transfer 的索引器会完全看不到这类转账。这是新手最常踩的第一个坑:代币转账收得到通知,主币转账一条都没有。

正确做法:原生币走交易本身的 value 字段,或者走执行追踪来覆盖内部转账。

转账时扣费的代币

有些代币在转账时会自动扣掉一部分作为税费或销毁。

这类代币的 Transfer 事件里写的是「转出 100」,但接收方实际只收到 97。按事件记账的系统会算出一个永远对不上的余额。

正确做法:涉及资金对账时,以状态为准,事件只作为索引线索。

通过合约的内部转账

用户通过一个聚合器合约做转账,最外层交易的 to 是聚合器,不是最终接收方。

只看交易列表的系统会把这笔钱归错人。区块浏览器上能看到「内部交易」,是因为它跑了完整的执行追踪。

正确做法:需要完整资金流时,用追踪;只做通知时,用事件。

区块重组

链的末端会发生重组:你已经处理过的区块被回滚,交易可能进入另一个区块,也可能永远不进。

如果你在一个区块被确认的瞬间就推送了通知,重组之后就会出现「通知了一笔不存在的转账」。

正确做法:按业务风险设置确认数,并且让索引器能够回滚。T17 会完整讲这件事。

改一个变量

如果合约作者忘了发 Event

链上的状态变化仍然正确发生,但链下系统完全看不见。

这说明一件重要的事:可观测性是合约设计的一部分,不是链自带的功能。 T7 写合约时,什么时候发事件、发哪些字段,是要专门设计的。

如果你需要查一年前某个地址的余额

普通节点只保留最近一段时间的历史状态,查不到就会报错。你需要归档节点,而它的存储成本高出一个数量级。

很多数据产品的成本结构,就是在这里被决定的。T4 会讲怎样在成本和能力之间做取舍。

如果换成 Solana

模型的形状完全不同:没有全局的状态树,数据散落在一个个账户里;交易必须提前声明它会读写哪些账户,因此可以并行执行。

日志的处理方式也不一样。T6 会做完整对比——对比是理解任何一种模型的最快方式

带走的问题

1
它解决什么问题?

这一章解决的是「怎样正确地读到链上发生了什么」。听起来基础,但它是后面所有章节的前提:读错了数据,上面的一切都是错的。

2
为什么需要 Blockchain?

为什么需要 Blockchain?在这个问题上,它给出的独特答案是:任何人都能独立验证同一份历史。你自己跑一个节点,就能得到和所有人一致的结果,不需要相信任何 API 提供方。

9
谁承担风险?

如果你的索引器漏了一笔转账,谁承担损失?通常是用户,而你事后甚至可能不知道漏了。这就是为什么索引的正确性要用「可重放」来保证,而不是靠「跑起来看着对」。

本章自测

一句话带走

链存的是状态和让状态改变的交易,Event 只是给外部世界看的投影。

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

本页目录