Derick
2521 words
13 minutes
解剖交易 —— 从调用到事件日志

“交易,是以太坊的语言。

它让世界状态从一个快照跃迁到另一个快照。”


一、从区块到交易:事件的起点#

还记得上一讲我们获取到的区块结构吗?

每个区块其实就是一个“交易集合”。

这些交易可能是:

  • 转账(账户对账户)
  • 部署智能合约(合约创建)
  • 调用智能合约函数(执行逻辑)
  • 内部合约间调用(子交易)

换句话说,交易是状态变化的唯一通道

在传统后端系统中,我们可能有“HTTP 请求”触发数据库操作;

而在以太坊上,交易就是那次请求


二、交易的生命周期:从签名到上链#

我们先来理解“交易”这件事到底经历了什么。

阶段描述类比
1️⃣ 创建 (Create)用户或合约发起一笔交易构造 HTTP 请求
2️⃣ 签名 (Sign)使用私钥签名,确保来源可信用 Token 认证请求
3️⃣ 广播 (Broadcast)交易被节点传播到全网将请求提交给网关
4️⃣ 打包 (Include)矿工/验证者将交易收入区块请求被后端服务处理
5️⃣ 执行 (Execute)EVM 执行代码、修改状态执行数据库操作
6️⃣ 生成收据 (Receipt)节点记录执行结果、日志返回 API 响应

这其中最关键的两个对象是:

  • Transaction:交易的原始请求体。
  • Receipt:执行结束后的“回执”,包含结果与日志。

三、目标:通过 Go 获取一笔交易及其收据#

我们将使用 Go 语言完成以下操作:

  1. 获取一笔交易(通过交易哈希)
  2. 查看其发送方、接收方、Gas 信息等
  3. 获取交易收据,判断是否执行成功
  4. 读取交易产生的事件日志(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] 分别是 fromto 地址,

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 事件:

参数存放位置
fromtopics[1]
totopics[2]
valuedata

🧩 实践目标:#

  1. 在测试网(如 Sepolia)选择一个 ERC20 代币(例如 DAI 或 USDC)。
  2. 使用上一讲的 TransactionReceipt 方法,获取收据的 Logs 字段。
  3. 使用 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 依然存在——因为即便失败,也要支付执行成本。


🧩 实践建议:#

  1. 选择一笔失败的交易(可在区块浏览器中找到标红的交易)。
  2. client.TransactionReceipt(context, hash) 获取其收据。
  3. 打印出以下关键信息:
fmt.Printf("Status: %d\nGasUsed: %d\nLogsCount: %d\n",
	receipt.Status,
	receipt.GasUsed,
	len(receipt.Logs))
  1. 尝试理解为什么会失败(可以在 Etherscan 的 “Input Data” 区块中查看合约调用参数)。

💡 小提示:

在失败的交易中,EVM 会抛出 REVERT 异常,

但返回的数据仍然可以通过 debug trace 或模拟执行获取。


练习 3:批量扫描区块交易(Block Scan)#

到目前为止,我们都是通过交易哈希直接获取收据。

但如果我们想要“扫整条链”,该怎么办?

最直接的办法,就是批量遍历区块。


🧩 实践目标:#

编写一段程序,从最新区块开始向前遍历:

  1. 使用 client.BlockByNumber() 获取区块;
  2. 读取 block.Transactions()
  3. 对每个交易调用 TransactionReceipt()
  4. 输出交易状态、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 执行阶段#

当一笔交易进入区块后,节点会执行以下步骤:

  1. 加载账户与合约状态(从状态树中读取)
  2. 执行字节码(EVM 逐条运行 opcode)
  3. 记录执行日志(Log、Gas 消耗、中间状态)
  4. 生成 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 不只是交易的收据,

它是以太坊‘世界共识’的证明。

每一份收据,都是一段可验证的历史。”


解剖交易 —— 从调用到事件日志
https://blog.ithuo.net/posts/blockchain-tutorial-evm-4/
Author
Derick
Published at
2024-12-08