智能合约的函数调用是确定性的,但现实世界的交互是异步的。事件(Event / Log)是以太坊的"消息队列",它是链上合约与链下世界的唯一桥梁。
12.2.1 事件的内部结构
solidity
pragma solidity ^0.8.0;
contract EventDemo {
event Transfer(address indexed from, address indexed to, uint256 value, string memo);
event Approval(address indexed owner, address indexed spender, uint256 value);
// indexed 参数存储在 "topic" 中,允许直接过滤
// 非 indexed 参数在 "data" 字段,ABI 编码,不用于索引
function transferWithMemo(address to, uint256 value, string calldata memo) external {
// ... 检查逻辑 ...
emit Transfer(msg.sender, to, value, memo);
}
}事件在 EVM 层面被记录为日志(LOGn 操作码):
| 字段 | 大小 | 说明 |
|---|---|---|
| Topics[0] | 32 bytes | Event selector = keccak256("Transfer(address,address,uint256,string)") |
| Topics[1] | 32 bytes | from(因为 indexed) |
| Topics[2] | 32 bytes | to(因为 indexed) |
| Data | 变长 | value, memo 的 ABI 编码,不用于索引 |
最多 4 个 indexed 参数。Indexed 参数允许直接使用 eth_getLogs 进行日志过滤。非 indexed 参数只能获取后解码才能筛选。
12.2.2 链下监听架构
graph LR
subgraph Chain[以太坊节点/WebSocket/RPC]
C[合约 emit Event]
L[日志存储<br/>Receipt Logs]
end
subgraph Indexing[索引层]
S[Subgraph / TheGraph]
E[自定义 ETL<br/>LevelDB 遍历]
end
subgraph 消费[应用层]
A[前端轮询/subscribe]
N[通知系统]
end
C --> L --> |"eth_getLogs"| S
S --> |"GraphQL"| A
S --> N
三种数据获取策略
| 策略 | 延迟 | 成本 | 去中心化程度 | 适用场景 |
|---|---|---|---|---|
| 直接 RPC 查询 | 12s-1h(视轮询间隔) | 高(每次请求计费) | 低 | 小规模 DApp |
| WebSocket 订阅 | 接近实时 | 高(需要持久连接) | 低 | 高频交易/监控 |
| TheGraph 索引 | 12s(通常) | 中(按查询量计费) | 中 | 90% 生产级 DApp |
| 自建 ETL + RDB | 12s-5min | 低(存储主导) | 高 | 全量分析/监控 |
12.2.3 Topics 过滤的精确检索
typescript
/**
* 基于 keccak 事件签名和 topics 的日志过滤逻辑
* 模拟 EIP-20 Transfer 事件的链下监听
*/
function keccak256(input: string): string {
// 简化:用简单哈希模拟,真实实现需要 sha3-256
let h = 0;
for (let i = 0; i < input.length; i++) {
h = ((h << 5) - h + input.charCodeAt(i)) | 0;
}
return "0x" + (h >>> 0).toString(16).padStart(64, '0');
}
// 真实 Event 签名哈希: keccak256("Transfer(address,address,uint256)")
// = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
const TRANSFER_EVENT_SIGNATURE = "Transfer(address,address,uint256)";
const TRANSFER_TOPIC0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
interface LogFilter {
fromBlock: number;
toBlock: number | "latest";
address: string; // 合约地址
topics: (string | null)[]; // topic[0]=事件签名, [1]=from, [2]=to
}
function buildTransferFilter(
contractAddress: string,
fromBlock: number,
from?: string,
to?: string,
): LogFilter {
return {
fromBlock,
toBlock: "latest",
address: contractAddress,
topics: [
TRANSFER_TOPIC0,
from ? from.slice(2).padStart(64, '0').toLowerCase() : null,
to ? to.slice(2).padStart(64, '0').toLowerCase() : null,
],
};
}
// 示例:监听从 Vitalik 地址发出的所有 USDC 转账
const vitalikAddress = "0xd8dA6BF26964aF9D7aEd9e03E53415D3aD4803d1";
const usdcContract = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const filter = buildTransferFilter(usdcContract, 18000000, vitalikAddress, undefined);
console.log("Filter:", JSON.stringify(filter, null, 2));
// 解析日志数据(非 topics)
interface DecodedTransfer {
from: string;
to: string;
value: bigint;
blockNumber: number;
transactionHash: string;
}
function decodeTransferLog(data: string, topics: string[]): DecodedTransfer {
// 简化:假设 topics[0]=signature, [1]=from, [2]=to
const padToAddr = (hex: string): string => "0x" + hex.slice(-40);
const from = padToAddr(topics[1]);
const to = padToAddr(topics[2]);
// data 中只包含 value (uint256),ABI 编码为 32 字节
const valueHex = data.slice(2, 66);
const value = BigInt("0x" + valueHex);
return { from, to, value, blockNumber: 0, transactionHash: "0x" }; // 占位
}12.2.4 事件驱动的智能合约设计模式
推送 vs 拉取:
- 拉取模式:用户主动调用合约读取状态(如查询余额)。好处:无延迟;坏处:用户需主动交互。
- 推送模式:合约通过事件触发,链下系统监听并采取行动(如发送通知、更新数据库、触发自动交易)。
flowchart LR
subgraph Pull[拉取模式]
U1[用户] --> |"balanceOf()"| S1[合约查询]
S1 --> U1
end
subgraph Push[推送模式]
C[合约] --> |"emit Update"| L[日志]
L --> |"监听"| I[链下系统]
I --> |"调用"| S2[外部 API]
end
style Pull fill:#ffebee
style Push fill:#e8f5e9
> ← 上一节:12.1 Solidity进阶 | 前往 → 12.3 典型漏洞与攻击 |*
评论
0评论加载中…