通过代码分析Ethereum 交易打包排序
在Ethereum 中, 一笔交易的流程大概如下
- 客户端生成交易信息
- 私钥签名
- 通过rpc 发送给节点
- 节点收到后在local tx pool 中处理, 然后在广播到其他节点
- 打包交易出块
所有的交易在未被打包前,都进入交易池中也就是tx pool
然后所有交易都会在miner包下面的worker.go中被打包
// fillTransactions retrieves the pending transactions from the txpool and fills them
// into the given sealing block. The transaction selection and ordering strategy can
// be customized with the plugin in the future.
func (w *worker) fillTransactions(interrupt *int32, env *environment) {
// Split the pending transactions into locals and remotes
// Fill the block with all available pending transactions.
pending := w.eth.TxPool().Pending(true)
localTxs, remoteTxs := make(map[common.Address]types.Transactions), pending
for _, account := range w.eth.TxPool().Locals() {
if txs := remoteTxs[account]; len(txs) > 0 {
delete(remoteTxs, account)
localTxs[account] = txs
}
}
if len(localTxs) > 0 {
txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, env.header.BaseFee)
if w.commitTransactions(env, txs, interrupt) {
return
}
}
if len(remoteTxs) > 0 {
txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, env.header.BaseFee)
if w.commitTransactions(env, txs, interrupt) {
return
}
}
}
每个tx pool中的Transaction都是由 NewTransactionsByPriceAndNonce 构造而 来,所以可以看到交易的打包逻辑
// NewTransactionsByPriceAndNonce creates a transaction set that can retrieve
// price sorted transactions in a nonce-honouring way.
//
// Note, the input map is reowned so the caller should not interact any more with
// if after providing it to the constructor.
func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, baseFee *big.Int) *TransactionsByPriceAndNonce {
// Initialize a price and received time based heap with the head transactions
heads := make(TxByPriceAndTime, 0, len(txs))
for from, accTxs := range txs {
acc, _ := Sender(signer, accTxs[0])
wrapped, err := NewTxWithMinerFee(accTxs[0], baseFee)
// Remove transaction if sender doesn't match from, or if wrapping fails.
if acc != from || err != nil {
delete(txs, from)
continue
}
heads = append(heads, wrapped)
txs[from] = accTxs[1:]
}
heap.Init(&heads)
// Assemble and return the transaction set
return &TransactionsByPriceAndNonce{
txs: txs,
heads: heads,
signer: signer,
baseFee: baseFee,
}
}
所有的交易都是插入到堆中, 然后通过堆排序 heap.Init(&heads), 堆中的排序规则是通过 TxByPriceAndTime 来实现的
// TxByPriceAndTime implements both the sort and the heap interface, making it useful
// for all at once sorting as well as individually adding and removing elements.
type TxByPriceAndTime []*TxWithMinerFee
func (s TxByPriceAndTime) Len() int { return len(s) }
func (s TxByPriceAndTime) Less(i, j int) bool {
// If the prices are equal, use the time the transaction was first seen for
// deterministic sorting
cmp := s[i].minerFee.Cmp(s[j].minerFee)
if cmp == 0 {
return s[i].tx.time.Before(s[j].tx.time)
}
return cmp > 0
}
func (s TxByPriceAndTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s *TxByPriceAndTime) Push(x interface{}) {
*s = append(*s, x.(*TxWithMinerFee))
}
func (s *TxByPriceAndTime) Pop() interface{} {
old := *s
n := len(old)
x := old[n-1]
*s = old[0 : n-1]
return x
}
通过 func (s TxByPriceAndTime) Less(i, j int) 方法可以看到排序优先根据 minerFee, 然后gas 费一样的情况下根据time 来判断
以上是go-ethereum的逻辑, 理论上所有fork geth的节点都是类似的实现, 例如
需要特别注意的是以上只是项目方节点的打包逻辑, 具体矿工们出块可能有自己的规则比如用了flashbot MEV