这篇文章来看看以太坊的交易流程及交易池TXpool。
这篇文章来看看以太坊的交易流程及交易池TXpool。
用户通过 Json RPC
向以太坊网络发送的交易请求最后都会被 go-ethereum/internal/ethapi/api.go
的SendTransaction
函数所接收。 从接收用户传入的参数,到把交易放入交易池等待广播的流程如下图所示:
整个流程从SendTransaction
接收到SendTxArgs
开始, SendTxArgs
的结构如下;
type SendTxArgs struct {
From common.Address `json:"from"`
To *common.Address `json:"to"`
Gas *hexutil.Uint64 `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"`
Value *hexutil.Big `json:"value"`
Nonce *hexutil.Uint64 `json:"nonce"`
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input"`
}
SendTransaction首先需要根据From字段来找到当前的账户,为签名交易做准备。
接着开始对交易进行预处理,为SendTxArgs
的一些空字段设置默认值,比如分配Nonce
,根据To
字段是否为空,来判断交易是部署合约还是发送交易等。
进行预处理之后,需要对交易进行RLP编码,再根据之前获得的账户私钥进行签名。
最后在把交易提交到TXpool。
交易的序列化是通过 toTransaction
这个函数来完成的。
序列化的时候根据To字段是否为nil
来判断是将交易序列化成交易,还是创建合约。
调用SendTranstion
接口的Data
和Input
字段,最终都会被赋值给Input
,再被序列化成Payload
放入交易池(TXpool)中,现在保留Data
字主要是为了向前兼容,目前推荐用Input
字段。
当部署合约的时候
Input
是合约的代码,当发送交易的时候Input
是交易的内容
func (args *SendTxArgs) toTransaction() *types.Transaction {
var input []byte
if args.Data != nil {
input = *args.Data
} else if args.Input != nil {
input = *args.Input
}
if args.To == nil {
return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), uint64(*args.Gas), (*big.Int)(args.GasPrice), input)
}
return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), uint64(*args.Gas), (*big.Int)(args.GasPrice), input)
}
最终序列化后的交易包含以下字段,需要注意的是不包含From字段,把交易和发送者解耦以后可以支持域名地址,提供了更多的可能性。
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *big.Int `json:"gasPrice" gencodec:"required"`
GasLimit uint64 `json:"gas" gencodec:"required"`
Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation
Amount *big.Int `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
r,s,v是交易签名后的值,它们可以被用来生成签名者的公钥;R,S是ECDSA椭圆加密算法的输出值,V是用于恢复结果的ID
用户私钥签名序列化的交易以后就会被放入到交易池中。
无论是本节点创建的交易(local)还是其他节点广播过来的交易(remote),都会缓存在TXpool中,当需要生成区块时,就从TXpool中选择合适的交易打包成块,经由共识最终确认。
TXpool的核心功能
TXpool的核心结构如下图;
TXpool最为核心的结构是两个Map: queued
和pending
,用来存未验证的交易和验证过的交易。
添加交易到TXpool的过程比较简单,总体流程是这样的;
price
是否大于缓存中最小的,如果小于就拒收,如果大于就删除最小的交易,把本次交易插入pending
nonce
已经存在,依然是按照price的大小进行替换pending
里面的任何交易,则添加到queued
中TXpool
存在内存中,不可能无限大,等超过一定阈值就需要对交易池里面的交易进行清理。
pending的缓冲区容量默认是 4096,queued的缓冲区容量默认是1024
清理交易分为清理queued
和清理pending
,清理顺序queued
->pending
->queued
当满足以下条件的时候就会清理queued
nonce
小于当前账号发送noce
的最小值,也就是说之前的交易已经全部上链nonce
符合条件可以移动到pending
队列中,先从queued
清除,然后移动(send)到pending
中清理
queued
会影响pending
的大小,所以queued
清理优先级高
清理pending时,首先把超过每个账户可执行交易数量(AccountSlots)的数量,按照从大到小记录下来,接着按照从多到少删除。 举个例子来说明剔除的规则;
假如AccountSlots为4 有四个超出的账户,它们的数量分别是10, 9, 7,5 第一次剔除 [10], 剔除结束 [10] 第二次剔除 [10, 9] 剔除结束 [9,9] 第三次剔除 [9, 9, 7] 剔除结束 [7, 7, 7] 第四次剔除 [7, 7 , 7 ,5] 剔除结束 [5,5,5,5] 这个时候如果还是超出限制,则继续剔除 第五次剔除 [5, 5 , 5 ,5] 剔除结束 [4,4,4,4]
接着清理ququed
,规则也很简单,越先进入queued
的越后删除,直到清理到满足最大队列长度(GlobalQueue
)为止。
到这一步还有一个问题没有解决,以太坊是分布式系统,当本地节点已经挑选出最优的交易,准备广播给整个网络,这个时候矿工已经打包了一个区块,本地节点的区块头就是旧的了,本地筛选的交易有可能已经被打包,如果已经被打包生成了新区块,再将这个交易广播已经没有任何的意义,甚至我们费尽心思准备好的pending
缓冲区里的交易都是无效的。
为了避免上面的情况发生,我们就需要监听链是否有新区块产生,也就是ChainHeadEvent
事件。
当监听到ChainHeadEvent
事件时候,我们又该如何调整queued
和pending
呢?
首先需要将已经分叉的链回退到同一个区块号上(blockNumebr),有可能是本地节点领先,有可能是网络上其他节点领先,但无论怎样,都回退到同一个区块号。
本地节点回退时,撤销的交易保存到discarded
切片中,网络上其他节点的撤销交易保存在included
切片中。
当区块号一致的时候,还需要进一步的比较区块的Hash
来进一步确认区块里面的交易是否一致,如果不一致一致回退到区块Hash为止,回退撤销的交易依旧保存在discarded
和included
切片中。
等完全确认本地和网络的链没有分叉的时候,就需要比较discarded和included里面的交易,因为网络上区块的生成优先级高于本地,所以需要剔除discarded
中inclueded
的交易,生成reinject
切片,剔除完以后还需要对TXpool
按照网络新生成区块的信息设置世界状态等信息,设置完以后,重新将reinject
加入TXpool
,加入以后在进行验证清理等流程。
本文作者是深入浅出区块链共建者清源,欢迎关注清源的博客,不定期分享一些区块链底层技术文章。
备注:编者在原文上略有修改。
深入浅出区块链 - 打造高质量区块链技术博客,学区块链都来这里,关注知乎、微博。
区块链技术问答 - 专家坐镇,有问必答。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!