以太坊作为全球领先的智能合约平台,其核心功能之一便是处理和执行交易,每一笔转账、每一个智能合约的调用,都对应着一笔特定的以太坊交易,这些交易不仅仅是屏幕上的一串数字,它们是经过精心设计、遵循严格协议的“数字契约”,要真正理解以太坊的运作机制,深入其交易底层源码是必不可少的一步,本文将带您一同探索以太坊交易从创建到被打包进区块的底层源码实现逻辑,揭示其背后的技术奥秘。
以太坊的客户端实现有多种,其中最广为人知的是使用Go语言编写的go-ethereum(geth),我们将主要以geth的源码为例,进行剖析。
交易的本质:RLP编码的数据结构
在以太坊网络中,一切数据都以RLP(Recursive Length Prefix)编码进行序列化后传输和存储,交易也不例外,一个以太坊交易,在底层是一个结构化的数据对象,在Go语言中,它主要由core/types包中的Transaction结构体定义。
让我们先来看一下core/types/transaction.go中Transaction结构体的核心定义(简化版):
type Transaction struct {
data txdata
// caches
hash atomic.Value
size atomic.Value
from atomic.Value
}
type txdata struct {
AccountNonce uint64 // 账户nonce值,防止重放攻击
Price *big.Int // 每gas价格(在EIP-1559之前是Gwei,之后是maxFeePerGas/maxPriorityFeePerGas)
GasLimit uint64 // gas限制
Recipient *common.Address // 接收方地址,nil表示合约创建
Amount *big.Int // 转账金额
Payload []byte // 交易数据,对于合约调用是函数调用和数据,对于合约创建是合约代码
V *big.Int // 回归值,用于恢复发送者地址
R *big.Int // 签名分量r
S *big.Int // 签名分量s
// EIP-1559 fields
Type uint8 // 交易类型,如0x00 (Legacy), 0x01 (EIP-2930), 0x02 (EIP-1559)
AccessList AccessList // EIP-2930访问列表
ChainID *big.Int // 链ID,防止重放攻击
GasFeeCap *big.Int // EIP-1559: 每gas最高费用
GasTipCap *big.Int // EIP-1559: 小费(优先费)上限
}
txdata结构体包含了交易的所有核心信息,而Transaction结构体则在其基础上封装了一些缓存字段(如hash, size, from)以提高性能,当我们说“一笔交易”时,实际上就是指这样一个包含了特定字段值的Transaction实例,并经过RLP编码后的字节序列。
交易的创建与签名
交易的创建通常由外部账户(通过以太坊钱包如MetaMask,或直接调用API)发起,用户指定接收方、金额、gas价格、gas限制以及可选的交易数据(Payload),对于智能合约交互,Payload就是合约函数选择器和参数的编码。
在geth中,创建一个未签名的交易对象,通常会使用NewTransaction函数(或针对EIP-1559的NewTx系列函数):
// core/types/transaction.go func NewTransaction(nonce uint64, to common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction { return newTransaction(nonce, &to, amount, gasLimit, gasPrice, nil, data, 0, 0) } func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte, accessList AccessList, chainID *big.Int) *Transaction { // ... 根据参数创建对应类型的txdata ... // 对于Legacy交易: if gasFeeCap != nil || gasTipCap != nil || chainID != nil || len(accessList) > 0 { // 创建EIP-1559或EIP-2930交易 } else { // 创建Legacy交易 return &Transaction{ data: txdata{ AccountNonce: nonce, Recipient: to, Payload: data, Amount: amount, GasLimit: gasLimit, Price: gasPrice, V: new(big.Int), R: new(big.Int), S: new(big.Int), }, } } }
创建好未签名的交易对象后,最关键的一步是签名,签名目的是证明交易的发起者拥有该账户的私钥,并确保交易在传输过程中未被篡改,签名过程使用的是椭圆曲线数字签名算法(ECDSA)。
在geth中,签名操作通常通过Signer接口来完成。Signer定义了如何从交易中获取发送者地址、如何验证签名以及如何对交易进行签名等,常见的Signer实现有:
HomesteadSigner:用于Homestead硬分叉后的Legacy交易。EIP155Signer:实现了EIP-155,引入了链ID,防止跨链重放攻击。LondonSigner:用于EIP-1559(伦敦升级)交易。
签名过程大致如下:
- 从交易数据(
txdata)中提取特定的哈希预镜像(hash prefix),对于Legacy交易,通常是rlp([nonce, price, gaslimit, to, value, data, v, r, s])的哈希;对于EIP-1559,则有不同的编码方式。 - 使用发送者的私钥对这个哈希预镜像进行ECDSA签名,得到两个分量
r和s,以及一个恢复IDv。 - 将
v,r,s设置到交易对象的data字段中。
签名后的交易对象就包含了完整的、可验证的信息,使用EIP155Signer签名:
// accounts/abi/bind/signer.go (简化示例)
func (s *EIP155Signer) SignTx(tx *types.Transaction, privateKey *ecdsa.PrivateKey) (*types.Transaction, error) {
// 1. 获取交易哈希
hashed := s.Hash(tx)
// 2. 使用私钥签名
sig, err := crypto.Sign(hashed[:], privateKey)
if err != nil {
return nil, err
}
// 3. 设置R, S, V
// ... 处理sig字节,提取r, s, 并根据EIP-155规则计算v ...
// 4. 返回已签名的交易
return tx.WithSignature(s, sig)
}
交易的广播与验证
签名后的交易会被序列化为RLP编码的字节串,然后通过以太坊的P2P网络广播给附近的节点,接收到交易的节点会进行一系列验证:
- RLP解码:将接收到的RLP字节流解码回
Transaction对象。 - 基本格式验证:检查交易字段是否齐全,数值是否合法(如gasLimit是否合理,金额是否为正等)。
- 签名验证:
- 从交易中提取
R,S,V。 - 根据交易类型(Legacy/EIP-1559等)和
V值,恢复出发送者的公钥和地址。 - 使用恢复的公钥对交易哈希进行签名验证,确保
R,S,V的有效性。
- 从交易中提取
- Nonce检查:检查发送者账户的nonce值是否与交易中的
AccountNonce匹配。 - Gas检查:检查发送者账户是否有足够的ETH支付交易费用(
gasLimit * gasPrice或更复杂的EIP-1559费用计算)。 - EIP-2718类型检查:对于新型交易,检查其类型和格式是否符合规范。
如果所有验证都通过,节点会将交易加入到自己的本地交易池(mempool)中,并继续广播给其他节点,如果验证失败,交易会被丢弃。
交易的选择与打包
矿工(或验证者)节点会从自己的交易池中选择交易,打包进候选区块,选择交易的策略通常基于:
- Gas Price/Gas Tip:矿工优先选择Gas Price或Gas Tip(优先费)高的交易,以获得更高的收益。
- Nonce顺序:对于同一个账户,必须按照nonce顺序处理交易,不能跳过。
- Gas Limit:确保打包
