2. 区块链数据基础:区块、交易、地址、Token标准回顾

好,咱们正式开始第二讲。在动手抓链上数据之前,有些基础概念得先捋清楚。我见过不少新手,上来就写代码调 RPC,结果连区块里装的是啥都没搞明白——嗯,那后面肯定踩坑。

这一节,咱们把区块、交易、地址、还有那几个 Token 标准过一遍。说白了,就是给后续的巨鲸追踪打地基。

2.1 区块:链上数据的“集装箱”

区块是什么?你可以把它想象成一个集装箱。每个集装箱里装了一批交易,盖上盖子(也就是区块头),然后挂到链条上。

我个人习惯把区块理解成“时间切片”。每出一个新区块,就代表链上状态更新了一次。以太坊平均 12 秒出一个块,比特币慢一些,10 分钟。

区块结构其实不复杂,核心就两部分:

  • 区块头:存元数据。比如父区块哈希、时间戳、难度、Nonce 值。嗯,这里有个关键字段——stateRoot,它记录了整个链上状态的 Merkle 根。我在做状态校验时经常用到它。
  • 区块体:就是交易列表。以太坊的区块体里还有叔块引用,不过咱们做数据分析时,主要关注交易列表就够了。

避坑指南:我曾经以为区块高度是连续的,后来发现以太坊偶尔会出“空块”——就是区块里没有交易。这种情况在 Gas 费低的时候很常见。你统计交易量时,记得过滤掉空块。

来看一个实际抓取区块数据的代码片段,我用的是 Web3.py:

from web3 import Web3

w3 = Web3(Web3.HTTPProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'))

# 获取最新区块
latest_block = w3.eth.get_block('latest')
print(f"区块高度: {latest_block['number']}")
print(f"交易数量: {len(latest_block['transactions'])}")
print(f"时间戳: {latest_block['timestamp']}")

# 获取指定区块的完整信息
block = w3.eth.get_block(18000000, full_transactions=True)
for tx in block['transactions'][:3]:  # 只看前3笔
    print(f"交易哈希: {tx['hash'].hex()}")

你想想看,每次调用 get_block 其实就是在拉取一个集装箱的全部内容。如果 full_transactions=True,连箱子里的每件货物(交易详情)都给你。

2.2 交易:链上活动的“原子单位”

交易是区块链上最小的操作单元。说白了,任何状态变化——转账、部署合约、调用合约——都得通过交易来触发。

以太坊的交易结构,我列个表给你看:

字段 说明 我常用的场景
from 发送方地址 识别巨鲸地址
to 接收方地址(合约或EOA) 判断是转账还是合约调用
value 转账金额(单位:Wei) 计算大额转账阈值
input 调用数据(合约交互时) 解析具体调用了哪个函数
gasPrice Gas 单价 判断交易紧急程度
nonce 发送方交易序号 检测交易是否被替换

这里有个细节——input 字段。很多新手以为交易只有转账,其实大部分链上活动都是合约调用。比如你调用 Uniswap 的 swap 函数,input 里就编码了函数签名和参数。

我的经验:抓巨鲸交易时,别只看 value 字段。很多大额操作是通过合约转账完成的,value 可能是 0,但 input 里藏着几百万 USDT 的转移。我吃过这个亏,后来养成了解析 input 的习惯。

解析交易 input 的代码示例:

# 解析 ERC-20 transfer 函数的 input
# transfer 函数签名: 0xa9059cbb
# 参数: to (地址), value (uint256)

tx_input = "0xa9059cbb000000000000000000000000abc...0000000000000000000000000000000000000000000000000000000000000001"

# 提取接收方地址(从第10个字符开始,取40个字符)
to_address = "0x" + tx_input[34:74]
# 提取金额(最后64个字符)
value_hex = tx_input[74:]
value = int(value_hex, 16)
print(f"接收方: {to_address}")
print(f"金额: {value}")

为什么会这样?因为以太坊虚拟机(EVM)就是这么设计的——所有数据都塞进 input 里,按 ABI 编码规则排列。你只要知道合约的 ABI,就能反解出具体操作。

2.3 地址:链上的“身份标识”

地址,说白了就是你在区块链上的账户。以太坊地址是 40 位十六进制数,前面加个 0x

但地址分两种:

  • EOA(外部账户):由私钥控制,可以发起交易。巨鲸的钱包基本都是 EOA。
  • 合约地址:由代码控制,不能主动发起交易,只能被动响应。

我记得刚开始做链上分析时,经常分不清一个地址是 EOA 还是合约。后来学了一招——查它的 code 字段。如果 code 是空的,就是 EOA;否则是合约。

def is_contract(address):
    code = w3.eth.get_code(address)
    return code != b''

# 测试
addr = "0x...一些巨鲸地址"
if is_contract(addr):
    print("这是合约地址")
else:
    print("这是个人钱包")

注意:有些合约地址的 code 虽然不为空,但可能已经自毁了(selfdestruct)。这种情况下,地址虽然存在,但已经无法交互。我在追踪一个老牌 DeFi 项目时就遇到过,折腾了半天才发现合约已经废了。

2.4 Token 标准:ERC-20 / ERC-721 / ERC-1155

Token 标准,就是一套约定好的接口规范。说白了,大家按同一个规则写合约,这样钱包、交易所、分析工具都能统一识别。

ERC-20:同质化代币

这是最常见的标准。USDT、UNI、LINK 都是 ERC-20。每个代币都一样,可以互换。

核心接口就几个:totalSupply()balanceOf()transfer()approve()transferFrom()

我做巨鲸分析时,最常查的就是 balanceOf。比如查某个地址持有多少 USDT:

usdt_contract = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
usdt = w3.eth.contract(address=usdt_contract, abi=erc20_abi)

whale_address = "0x...某巨鲸"
balance = usdt.functions.balanceOf(whale_address).call()
print(f"持有 USDT: {balance / 10**6}")  # USDT 是6位小数

ERC-721:非同质化代币(NFT)

每个 NFT 都是独一无二的。核心区别在于多了个 tokenId 字段。你查某个地址的 NFT 持仓,得遍历它的所有 tokenId。

我遇到过最坑的事:有些 NFT 合约的 totalSupply() 不准确,因为部分 NFT 被销毁了。后来我改用 ownerOf() 逐个 tokenId 查,虽然慢,但准确。

ERC-1155:多代币标准

这个标准比较新,一个合约可以同时管理同质化和非同质化代币。说白了,就是 ERC-20 和 ERC-721 的合体。

它的 balanceOfBatch() 函数可以一次查多个地址、多个 tokenId 的余额,效率很高。我在分析游戏项目时经常用到——一个合约里既有金币(同质化),又有武器(非同质化)。

总结一下我的经验

  • 抓 ERC-20 转账,监听 Transfer 事件就行
  • 抓 NFT 交易,得同时监听 TransferApproval 事件
  • ERC-1155 的 TransferSingleTransferBatch 事件要分开处理

2.5 知识体系总览

下面这张图,我把这一节的核心逻辑串起来了。你保存下来,以后做数据分析时对照着看:

区块链数据基础结构 区块 (Block) 区块头 交易列表 交易 (Transaction) from/to value input gasPrice 地址 (Address) EOA 合约 Token 标准 ERC-20 (同质化) ERC-721 (NFT) ERC-1155 (混合) 关联 调用

这张图把区块、交易、地址、Token 标准的关系画清楚了。区块包含交易,交易由地址发起,交易可以调用 Token 合约。你顺着这个逻辑去抓数据,思路会清晰很多。

最后说一句:这些基础概念,我建议你动手跑一遍代码。光看文档记不牢。我自己当年就是一边抓区块数据一边学的,踩过的坑现在都变成了经验。


专注资料整理