“交易,是以太坊的语言。
它让世界状态从一个快照跃迁到另一个快照。”
一、从区块到交易:事件的起点
还记得上一讲我们获取到的区块结构吗?
每个区块其实就是一个“交易集合”。
这些交易可能是:
- 转账(账户对账户)
- 部署智能合约(合约创建)
- 调用智能合约函数(执行逻辑)
- 内部合约间调用(子交易)
换句话说,交易是状态变化的唯一通道。
在传统后端系统中,我们可能有“HTTP 请求”触发数据库操作;
而在以太坊上,交易就是那次请求。
二、交易的生命周期:从签名到上链
我们先来理解“交易”这件事到底经历了什么。
| 阶段 | 描述 | 类比 |
|---|---|---|
| 1️⃣ 创建 (Create) | 用户或合约发起一笔交易 | 构造 HTTP 请求 |
| 2️⃣ 签名 (Sign) | 使用私钥签名,确保来源可信 | 用 Token 认证请求 |
| 3️⃣ 广播 (Broadcast) | 交易被节点传播到全网 | 将请求提交给网关 |
| 4️⃣ 打包 (Include) | 矿工/验证者将交易收入区块 | 请求被后端服务处理 |
| 5️⃣ 执行 (Execute) | EVM 执行代码、修改状态 | 执行数据库操作 |
| 6️⃣ 生成收据 (Receipt) | 节点记录执行结果、日志 | 返回 API 响应 |
这其中最关键的两个对象是:
- Transaction:交易的原始请求体。
- Receipt:执行结束后的“回执”,包含结果与日志。
三、目标:通过 Go 获取一笔交易及其收据
我们将使用 Go 语言完成以下操作:
- 获取一笔交易(通过交易哈希)
- 查看其发送方、接收方、Gas 信息等
- 获取交易收据,判断是否执行成功
- 读取交易产生的事件日志(Logs)
四、实战:解剖一笔交易
创建文件 tx_inspect.go:
package main
import (
"context"
"fmt"
"log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
// 1. 连接以太坊节点
client, err := ethclient.Dial("https://sepolia.infura.io/v3/<YOUR_PROJECT_ID>")
if err != nil {
log.Fatalf("连接节点失败: %v", err)
}
defer client.Close()
// 2. 准备要查询的交易哈希
txHash := common.HexToHash("0xYOUR_TX_HASH_HERE") // 替换为真实哈希
// 3. 查询交易详情
tx, isPending, err := client.TransactionByHash(context.Background(), txHash)
if err != nil {
log.Fatalf("无法获取交易信息: %v", err)
}
fmt.Println("———————— 交易详情 ————————")
fmt.Printf("交易哈希: %v\n", tx.Hash().Hex())
fmt.Printf("是否未确认(Pending): %v\n", isPending)
fmt.Printf("Nonce: %v\n", tx.Nonce())
fmt.Printf("GasLimit: %v\n", tx.Gas())
fmt.Printf("Value (ETH): %v\n", tx.Value())
// 4. 获取交易收据(执行结果)
receipt, err := client.TransactionReceipt(context.Background(), txHash)
if err != nil {
log.Fatalf("无法获取交易收据: %v", err)
}
fmt.Println("———————— 收据信息 ————————")
fmt.Printf("区块号: %v\n", receipt.BlockNumber)
fmt.Printf("交易状态: %v\n", receipt.Status)
fmt.Printf("Gas 消耗: %v\n", receipt.GasUsed)
fmt.Printf("事件日志数量: %d\n", len(receipt.Logs))
// 5. 打印日志详情
if len(receipt.Logs) > 0 {
fmt.Println("———————— 第一条事件日志 ————————")
log := receipt.Logs[0]
fmt.Printf("合约地址: %v\n", log.Address.Hex())
fmt.Printf("主题数量: %d\n", len(log.Topics))
fmt.Printf("原始数据: %x\n", log.Data)
}
}
运行:
go run tx_inspect.go
如果一切正常,你将看到:
———————— 交易详情 ————————
交易哈希: 0x8b2f...
是否未确认(Pending): false
Nonce: 12
GasLimit: 21000
Value (ETH): 1000000000000000000
———————— 收据信息 ————————
区块号: 5689012
交易状态: 1
Gas 消耗: 21000
事件日志数量: 0
💡 状态码说明:
1表示执行成功0表示执行失败(可能是合约异常或 gas 不足)
五、事件日志(Logs):智能合约的“回声”
如果交易是“调用”,那么事件日志就是它的“回声”。
每当智能合约执行 emit 操作(如 emit Transfer(...)),
EVM 都会在交易收据中生成一条 Log。
| 字段 | 含义 |
|---|---|
address | 产生事件的合约地址 |
topics | 事件主题(Indexed 参数) |
data | 非索引参数的原始数据 |
一个典型的 ERC20 转账事件如下:
event Transfer(address indexed from, address indexed to, uint256 value);
执行时会生成一条日志,
其 topics[0] 存放事件签名哈希,
topics[1] 与 topics[2] 分别是 from、to 地址,
data 部分存放转账金额。
“当你学会看懂一笔交易的执行过程,
你就不再是观察区块链的人,
而是理解它的那个人。”
六、延伸练习:从日志到真相
在上面的章节中,我们学会了获取交易收据(Receipt),知道它是合约执行后的“回执”。
但如果只停留在打印状态,我们还离真正的理解差一步。
接下来这三组练习,会让你从不同角度看透链上执行的全貌。
练习 1:解析 ERC20 转账事件(Transfer)
几乎所有的代币转账,都会触发一个标准事件:
event Transfer(address indexed from, address indexed to, uint256 value);
它是 ERC20 标准中最重要的信号之一——代表“从哪转到哪,转了多少”。
🧠 背后的机制:
每当合约执行 emit Transfer(...),EVM 就会将这条事件日志写入收据中的 logs 字段。
这个日志并不是可执行代码,而是一段结构化的“广播消息”。
每条日志由以下部分组成:
address:事件来源(合约地址)topics[]:索引字段(indexed 参数)data:非索引字段(普通参数)
例如,对于上面的 Transfer 事件:
| 参数 | 存放位置 |
|---|---|
from | topics[1] |
to | topics[2] |
value | data |
🧩 实践目标:
- 在测试网(如 Sepolia)选择一个 ERC20 代币(例如 DAI 或 USDC)。
- 使用上一讲的
TransactionReceipt方法,获取收据的Logs字段。 - 使用
go-ethereum/accounts/abi模块解析日志。
import "github.com/ethereum/go-ethereum/accounts/abi"
erc20Abi, _ := abi.JSON(strings.NewReader(`[{"anonymous":false,"inputs":[
{"indexed":true,"name":"from","type":"address"},
{"indexed":true,"name":"to","type":"address"},
{"indexed":false,"name":"value","type":"uint256"}
],"name":"Transfer","type":"event"}]`))
通过 UnpackIntoInterface() 解析 vLog.Data,即可获取转账金额。
同时,vLog.Topics[1]、vLog.Topics[2] 分别对应发送方与接收方地址。
✅ 当你能打印出 “From → To : Value” 的信息时,
你就完成了第一次事件解析任务。
练习 2:追踪失败交易(Transaction Failure)
区块链的交易,并不是每次都会成功。
智能合约可能因为条件不满足、余额不足、或显式调用 revert() 而失败。
这类失败不会被节点“撤销”或“回滚”,
它仍然会被打包进区块,只是结果标记为失败。
🧠 观察要点:
在交易收据中,有一个非常关键的字段:
receipt.Status // 1 表示成功,0 表示失败
当 Status == 0 时,说明该交易执行失败。
这时,Logs 通常为空,因为合约内部的事件也被回滚。
而 GasUsed 依然存在——因为即便失败,也要支付执行成本。
🧩 实践建议:
- 选择一笔失败的交易(可在区块浏览器中找到标红的交易)。
- 用
client.TransactionReceipt(context, hash)获取其收据。 - 打印出以下关键信息:
fmt.Printf("Status: %d\nGasUsed: %d\nLogsCount: %d\n",
receipt.Status,
receipt.GasUsed,
len(receipt.Logs))
- 尝试理解为什么会失败(可以在 Etherscan 的 “Input Data” 区块中查看合约调用参数)。
💡 小提示:
在失败的交易中,EVM 会抛出
REVERT异常,但返回的数据仍然可以通过 debug trace 或模拟执行获取。
练习 3:批量扫描区块交易(Block Scan)
到目前为止,我们都是通过交易哈希直接获取收据。
但如果我们想要“扫整条链”,该怎么办?
最直接的办法,就是批量遍历区块。
🧩 实践目标:
编写一段程序,从最新区块开始向前遍历:
- 使用
client.BlockByNumber()获取区块; - 读取
block.Transactions(); - 对每个交易调用
TransactionReceipt(); - 输出交易状态、Gas 消耗等信息。
💻 伪代码示例:
for i := startBlock; i > startBlock-10; i-- {
block, _ := client.BlockByNumber(ctx, big.NewInt(int64(i)))
fmt.Printf("区块 #%d 包含 %d 笔交易\n", i, len(block.Transactions()))
for _, tx := range block.Transactions() {
receipt, _ := client.TransactionReceipt(ctx, tx.Hash())
fmt.Printf("- Tx %s | Status: %d | GasUsed: %d\n",
tx.Hash().Hex(), receipt.Status, receipt.GasUsed)
}
}
🚀 当你能批量遍历并打印交易结果时,
你就完成了“扫链”的最基础形态。
七、幕后机制:Receipt 的诞生之路
我们现在来揭开 Receipt(交易回执)背后的运行原理。
1️⃣ EVM 执行阶段
当一笔交易进入区块后,节点会执行以下步骤:
- 加载账户与合约状态(从状态树中读取)
- 执行字节码(EVM 逐条运行 opcode)
- 记录执行日志(Log、Gas 消耗、中间状态)
- 生成 Receipt(包含执行结果)
Receipt 的结构体大致如下:
type Receipt struct {
Status uint64
CumulativeGasUsed uint64
Logs []*Log
TxHash common.Hash
BlockHash common.Hash
BlockNumber *big.Int
}
2️⃣ 写入 Merkle Trie
每个区块执行完所有交易后,节点会:
- 把所有
Receipt的哈希插入一个 Merkle Trie; - 将这个 Trie 的根哈希(
receiptsRoot)写入区块头。
这就意味着:
如果任何一个交易的收据被篡改,
整个 Merkle 根都会变化,
因此所有节点都会立即检测到不一致。
3️⃣ 强一致性的保证
这正是以太坊“世界计算机”的奥义所在:
每个节点独立执行相同交易 → 得到相同的状态树根与收据树根。
区块头包含这两个根 → 保证链上结果不可伪造。
这就是所谓的:
Deterministic Execution + Merkle Proof = Trustless Finality
没有人能篡改结果,也没人能偷偷修改日志。
任何状态的正确性,都能被其他节点独立验证。
4️⃣ 一个简单的类比
你可以把 Receipt 想象成:
- 交易 是数据库的事务;
- Receipt 是事务的提交日志;
- Merkle Trie 是写入分布式账本的“哈希索引”。
所以,当你在浏览器上看到“交易成功”这几个字时,
其实背后是:
EVM 执行完 → 状态树更新 → 收据写入 Trie → 区块头签名 → 广播全网确认。
这一连串动作,全世界的节点都在同步执行。
而你的本地客户端,只是去读取那份共识结果。
🧩 总结
| 知识点 | 关键理解 |
|---|---|
| ERC20 Transfer 日志 | 合约通过事件广播状态变化 |
| 交易失败状态 | Status=0 仍会上链,Gas 不退还 |
| 批量区块扫描 | 是扫链与索引系统的基础 |
| Receipt 结构 | 包含执行状态、Gas、事件等信息 |
| Merkle Trie | 保证收据与状态的一致性与不可篡改性 |
“Receipt 不只是交易的收据,
它是以太坊‘世界共识’的证明。
每一份收据,都是一段可验证的历史。”
