mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 13:21:37 +00:00
miner: consider additional sender nonces when ordering txs
This commit is contained in:
parent
189f9d0b17
commit
5283e97b02
2 changed files with 157 additions and 9 deletions
|
|
@ -33,24 +33,80 @@ type txWithMinerFee struct {
|
|||
fees *uint256.Int
|
||||
}
|
||||
|
||||
// newTxWithMinerFee creates a wrapped transaction, calculating the effective
|
||||
// miner gasTipCap if a base fee is provided.
|
||||
// Returns error in case of a negative effective miner gasTipCap.
|
||||
func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee *uint256.Int) (*txWithMinerFee, error) {
|
||||
// maxLookahead is the maximum number of queued transactions to consider when
|
||||
// computing a sender's look-ahead score. This bounds the CPU cost of the
|
||||
// optimal-prefix search during heap construction and on every Shift.
|
||||
const maxLookahead = 16
|
||||
|
||||
// effectiveTip computes the effective miner tip for a single transaction.
|
||||
func effectiveTip(tx *txpool.LazyTransaction, baseFee *uint256.Int) *uint256.Int {
|
||||
tip := new(uint256.Int).Set(tx.GasTipCap)
|
||||
if baseFee != nil {
|
||||
if tx.GasFeeCap.Cmp(baseFee) < 0 {
|
||||
return nil, types.ErrGasFeeCapTooLow
|
||||
return nil // cannot pay base fee
|
||||
}
|
||||
tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee)
|
||||
if tip.Gt(tx.GasTipCap) {
|
||||
tip = tx.GasTipCap
|
||||
tip = new(uint256.Int).Set(tx.GasTipCap)
|
||||
}
|
||||
}
|
||||
return tip
|
||||
}
|
||||
|
||||
// queueScore computes the optimal look-ahead score for a sender's pending
|
||||
// transaction queue. It finds the prefix of the nonce-ordered sequence that
|
||||
// maximizes the weighted-average effective tip per gas. This allows the miner
|
||||
// to "see through" a low-tip head transaction to high-value ones behind it.
|
||||
//
|
||||
// For example, if a sender has [0.01 gwei tip @ 21k gas, 100 gwei tip @ 21k gas],
|
||||
// the head-only score would be 0.01 gwei, but the optimal prefix score is
|
||||
// ~50 gwei — correctly reflecting that committing both yields high revenue.
|
||||
func queueScore(headTip *uint256.Int, headGas uint64, pending []*txpool.LazyTransaction, baseFee *uint256.Int) *uint256.Int {
|
||||
best := new(uint256.Int).Set(headTip)
|
||||
if len(pending) == 0 {
|
||||
return best
|
||||
}
|
||||
// Running weighted sum: sum(tip_i * gas_i) and sum(gas_i)
|
||||
sumTipGas := new(uint256.Int).Mul(headTip, new(uint256.Int).SetUint64(headGas))
|
||||
sumGas := new(uint256.Int).SetUint64(headGas)
|
||||
|
||||
lookahead := len(pending)
|
||||
if lookahead > maxLookahead {
|
||||
lookahead = maxLookahead
|
||||
}
|
||||
for i := 0; i < lookahead; i++ {
|
||||
tip := effectiveTip(pending[i], baseFee)
|
||||
if tip == nil {
|
||||
break // tx can't pay base fee, stop looking ahead
|
||||
}
|
||||
gas := new(uint256.Int).SetUint64(pending[i].Gas)
|
||||
sumTipGas.Add(sumTipGas, new(uint256.Int).Mul(tip, gas))
|
||||
sumGas.Add(sumGas, gas)
|
||||
|
||||
avg := new(uint256.Int).Div(sumTipGas, sumGas)
|
||||
if avg.Gt(best) {
|
||||
best.Set(avg)
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// newTxWithMinerFee creates a wrapped transaction, calculating the effective
|
||||
// miner gasTipCap if a base fee is provided. The pending slice contains the
|
||||
// sender's subsequent queued transactions (nonce-ordered); when non-empty, a
|
||||
// look-ahead score is computed so that a low-tip head transaction does not hide
|
||||
// high-value transactions behind it.
|
||||
// Returns error in case of a negative effective miner gasTipCap.
|
||||
func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee *uint256.Int, pending []*txpool.LazyTransaction) (*txWithMinerFee, error) {
|
||||
tip := effectiveTip(tx, baseFee)
|
||||
if tip == nil {
|
||||
return nil, types.ErrGasFeeCapTooLow
|
||||
}
|
||||
score := queueScore(tip, tx.Gas, pending, baseFee)
|
||||
return &txWithMinerFee{
|
||||
tx: tx,
|
||||
from: from,
|
||||
fees: tip,
|
||||
fees: score,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +163,7 @@ func newTransactionsByPriceAndNonce(signer types.Signer, txs map[common.Address]
|
|||
// Initialize a price and received time based heap with the head transactions
|
||||
heads := make(txByPriceAndTime, 0, len(txs))
|
||||
for from, accTxs := range txs {
|
||||
wrapped, err := newTxWithMinerFee(accTxs[0], from, baseFeeUint)
|
||||
wrapped, err := newTxWithMinerFee(accTxs[0], from, baseFeeUint, accTxs[1:])
|
||||
if err != nil {
|
||||
delete(txs, from)
|
||||
continue
|
||||
|
|
@ -138,7 +194,7 @@ func (t *transactionsByPriceAndNonce) Peek() (*txpool.LazyTransaction, *uint256.
|
|||
func (t *transactionsByPriceAndNonce) Shift() {
|
||||
acc := t.heads[0].from
|
||||
if txs, ok := t.txs[acc]; ok && len(txs) > 0 {
|
||||
if wrapped, err := newTxWithMinerFee(txs[0], acc, t.baseFee); err == nil {
|
||||
if wrapped, err := newTxWithMinerFee(txs[0], acc, t.baseFee, txs[1:]); err == nil {
|
||||
t.heads[0], t.txs[acc] = wrapped, txs[1:]
|
||||
heap.Fix(&t.heads, 0)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -138,6 +138,98 @@ func testTransactionPriceNonceSort(t *testing.T, baseFee *big.Int) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestTransactionLookahead verifies that a sender with a low-tip head transaction
|
||||
// followed by a high-tip transaction is promoted above a sender whose single
|
||||
// transaction has a tip between the two. Without look-ahead scoring the low-tip
|
||||
// head would bury the high-value transaction.
|
||||
func TestTransactionLookahead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
signer := types.LatestSignerForChainID(common.Big1)
|
||||
baseFee := big.NewInt(10)
|
||||
|
||||
keyA, _ := crypto.GenerateKey()
|
||||
keyB, _ := crypto.GenerateKey()
|
||||
addrA := crypto.PubkeyToAddress(keyA.PublicKey)
|
||||
addrB := crypto.PubkeyToAddress(keyB.PublicKey)
|
||||
|
||||
// Sender A: nonce 0 has tip 1, nonce 1 has tip 100.
|
||||
// Head-only score = 1, but look-ahead average ≈ 50.
|
||||
txA0, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
|
||||
Nonce: 0,
|
||||
To: &common.Address{},
|
||||
Gas: 100,
|
||||
GasFeeCap: big.NewInt(11), // baseFee + 1
|
||||
GasTipCap: big.NewInt(1),
|
||||
}), signer, keyA)
|
||||
txA1, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
|
||||
Nonce: 1,
|
||||
To: &common.Address{},
|
||||
Gas: 100,
|
||||
GasFeeCap: big.NewInt(110), // baseFee + 100
|
||||
GasTipCap: big.NewInt(100),
|
||||
}), signer, keyA)
|
||||
|
||||
// Sender B: single tx with tip 20.
|
||||
// Without look-ahead, B (tip 20) would rank above A (tip 1).
|
||||
// With look-ahead, A's score (~50) should rank above B (20).
|
||||
txB0, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{
|
||||
Nonce: 0,
|
||||
To: &common.Address{},
|
||||
Gas: 100,
|
||||
GasFeeCap: big.NewInt(30), // baseFee + 20
|
||||
GasTipCap: big.NewInt(20),
|
||||
}), signer, keyB)
|
||||
|
||||
now := time.Now()
|
||||
groups := map[common.Address][]*txpool.LazyTransaction{
|
||||
addrA: {
|
||||
{Hash: txA0.Hash(), Tx: txA0, Time: now, GasFeeCap: uint256.NewInt(11), GasTipCap: uint256.NewInt(1), Gas: 100},
|
||||
{Hash: txA1.Hash(), Tx: txA1, Time: now, GasFeeCap: uint256.NewInt(110), GasTipCap: uint256.NewInt(100), Gas: 100},
|
||||
},
|
||||
addrB: {
|
||||
{Hash: txB0.Hash(), Tx: txB0, Time: now, GasFeeCap: uint256.NewInt(30), GasTipCap: uint256.NewInt(20), Gas: 100},
|
||||
},
|
||||
}
|
||||
|
||||
txset := newTransactionsByPriceAndNonce(signer, groups, baseFee)
|
||||
|
||||
// First tx out should be A's nonce 0 (sender A ranked higher due to look-ahead).
|
||||
first, _ := txset.Peek()
|
||||
if first == nil {
|
||||
t.Fatal("expected a transaction")
|
||||
}
|
||||
if first.Hash != txA0.Hash() {
|
||||
t.Errorf("expected sender A's tx first (look-ahead should promote it), got sender B")
|
||||
}
|
||||
txset.Shift()
|
||||
|
||||
// Second should be A's nonce 1 (tip 100 > B's tip 20).
|
||||
second, _ := txset.Peek()
|
||||
if second == nil {
|
||||
t.Fatal("expected a transaction")
|
||||
}
|
||||
if second.Hash != txA1.Hash() {
|
||||
t.Errorf("expected sender A's nonce 1 second, got %s", second.Hash)
|
||||
}
|
||||
txset.Shift()
|
||||
|
||||
// Third should be B's tx.
|
||||
third, _ := txset.Peek()
|
||||
if third == nil {
|
||||
t.Fatal("expected a transaction")
|
||||
}
|
||||
if third.Hash != txB0.Hash() {
|
||||
t.Errorf("expected sender B's tx third, got %s", third.Hash)
|
||||
}
|
||||
txset.Shift()
|
||||
|
||||
// Should be empty now.
|
||||
if last, _ := txset.Peek(); last != nil {
|
||||
t.Error("expected no more transactions")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that if multiple transactions have the same price, the ones seen earlier
|
||||
// are prioritized to avoid network spam attacks aiming for a specific ordering.
|
||||
func TestTransactionTimeSort(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue