mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-04 04:01:13 +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
|
fees *uint256.Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTxWithMinerFee creates a wrapped transaction, calculating the effective
|
// maxLookahead is the maximum number of queued transactions to consider when
|
||||||
// miner gasTipCap if a base fee is provided.
|
// computing a sender's look-ahead score. This bounds the CPU cost of the
|
||||||
// Returns error in case of a negative effective miner gasTipCap.
|
// optimal-prefix search during heap construction and on every Shift.
|
||||||
func newTxWithMinerFee(tx *txpool.LazyTransaction, from common.Address, baseFee *uint256.Int) (*txWithMinerFee, error) {
|
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)
|
tip := new(uint256.Int).Set(tx.GasTipCap)
|
||||||
if baseFee != nil {
|
if baseFee != nil {
|
||||||
if tx.GasFeeCap.Cmp(baseFee) < 0 {
|
if tx.GasFeeCap.Cmp(baseFee) < 0 {
|
||||||
return nil, types.ErrGasFeeCapTooLow
|
return nil // cannot pay base fee
|
||||||
}
|
}
|
||||||
tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee)
|
tip = new(uint256.Int).Sub(tx.GasFeeCap, baseFee)
|
||||||
if tip.Gt(tx.GasTipCap) {
|
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{
|
return &txWithMinerFee{
|
||||||
tx: tx,
|
tx: tx,
|
||||||
from: from,
|
from: from,
|
||||||
fees: tip,
|
fees: score,
|
||||||
}, nil
|
}, 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
|
// Initialize a price and received time based heap with the head transactions
|
||||||
heads := make(txByPriceAndTime, 0, len(txs))
|
heads := make(txByPriceAndTime, 0, len(txs))
|
||||||
for from, accTxs := range 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 {
|
if err != nil {
|
||||||
delete(txs, from)
|
delete(txs, from)
|
||||||
continue
|
continue
|
||||||
|
|
@ -138,7 +194,7 @@ func (t *transactionsByPriceAndNonce) Peek() (*txpool.LazyTransaction, *uint256.
|
||||||
func (t *transactionsByPriceAndNonce) Shift() {
|
func (t *transactionsByPriceAndNonce) Shift() {
|
||||||
acc := t.heads[0].from
|
acc := t.heads[0].from
|
||||||
if txs, ok := t.txs[acc]; ok && len(txs) > 0 {
|
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:]
|
t.heads[0], t.txs[acc] = wrapped, txs[1:]
|
||||||
heap.Fix(&t.heads, 0)
|
heap.Fix(&t.heads, 0)
|
||||||
return
|
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
|
// 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.
|
// are prioritized to avoid network spam attacks aiming for a specific ordering.
|
||||||
func TestTransactionTimeSort(t *testing.T) {
|
func TestTransactionTimeSort(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue