-
Notifications
You must be signed in to change notification settings - Fork 20.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EIP-1559 tx pool support #22898
EIP-1559 tx pool support #22898
Changes from 32 commits
6b16cc8
4eac968
ba113ee
3c1803d
3502aa7
391db45
661731b
3596737
a3d9640
b7fcf62
667aea2
86411f3
8503b0b
f745568
0f30d65
b68ca25
3b3666e
b192e2b
17a7392
0139181
082de1f
3752760
eb38e01
047e149
88ad464
702b9af
a6cc15b
cf1f32c
e852c14
be64f2f
238da13
4ad2bed
dd5f811
4a66ab6
f35555a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import ( | |
"math" | ||
"math/big" | ||
"sort" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
|
@@ -279,15 +280,23 @@ func (l *txList) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Tran | |
// If there's an older better transaction, abort | ||
old := l.txs.Get(tx.Nonce()) | ||
if old != nil { | ||
// threshold = oldGP * (100 + priceBump) / 100 | ||
if old.FeeCapCmp(tx) >= 0 || old.TipCmp(tx) >= 0 { | ||
return false, nil | ||
} | ||
// thresholdFeeCap = oldFC * (100 + priceBump) / 100 | ||
a := big.NewInt(100 + int64(priceBump)) | ||
a = a.Mul(a, old.GasPrice()) | ||
aFeeCap := new(big.Int).Mul(a, old.FeeCap()) | ||
aTip := a.Mul(a, old.Tip()) | ||
|
||
// thresholdTip = oldTip * (100 + priceBump) / 100 | ||
b := big.NewInt(100) | ||
threshold := a.Div(a, b) | ||
// Have to ensure that the new gas price is higher than the old gas | ||
// price as well as checking the percentage threshold to ensure that | ||
thresholdFeeCap := aFeeCap.Div(aFeeCap, b) | ||
thresholdTip := aTip.Div(aTip, b) | ||
|
||
// Have to ensure that either the new fee cap or tip is higher than the | ||
// old ones as well as checking the percentage threshold to ensure that | ||
// this is accurate for low (Wei-level) gas price replacements | ||
if old.GasPriceCmp(tx) >= 0 || tx.GasPriceIntCmp(threshold) < 0 { | ||
if tx.FeeCapIntCmp(thresholdFeeCap) < 0 || tx.TipIntCmp(thresholdTip) < 0 { | ||
return false, nil | ||
} | ||
} | ||
|
@@ -406,52 +415,84 @@ func (l *txList) LastElement() *types.Transaction { | |
} | ||
|
||
// priceHeap is a heap.Interface implementation over transactions for retrieving | ||
// price-sorted transactions to discard when the pool fills up. | ||
type priceHeap []*types.Transaction | ||
// price-sorted transactions to discard when the pool fills up. If baseFee is set | ||
// then the heap is sorted based on the effective tip based on the given base fee. | ||
// If baseFee is nil then the sorting is based on feeCap. | ||
type priceHeap struct { | ||
baseFee *big.Int // heap should always be re-sorted after baseFee is changed | ||
list []*types.Transaction | ||
} | ||
|
||
func (h priceHeap) Len() int { return len(h) } | ||
func (h priceHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } | ||
func (h *priceHeap) Len() int { return len(h.list) } | ||
func (h *priceHeap) Swap(i, j int) { h.list[i], h.list[j] = h.list[j], h.list[i] } | ||
|
||
func (h priceHeap) Less(i, j int) bool { | ||
// Sort primarily by price, returning the cheaper one | ||
switch h[i].GasPriceCmp(h[j]) { | ||
func (h *priceHeap) Less(i, j int) bool { | ||
switch h.cmp(h.list[i], h.list[j]) { | ||
case -1: | ||
return true | ||
case 1: | ||
return false | ||
default: | ||
return h.list[i].Nonce() > h.list[j].Nonce() | ||
} | ||
// If the prices match, stabilize via nonces (high nonce is worse) | ||
return h[i].Nonce() > h[j].Nonce() | ||
} | ||
|
||
func (h *priceHeap) cmp(a, b *types.Transaction) int { | ||
if h.baseFee != nil { | ||
// Compare effective tips if baseFee is specified | ||
if c := a.EffectiveTipCmp(b, h.baseFee); c != 0 { | ||
return c | ||
} | ||
} | ||
// Compare fee caps if baseFee is not specified or effective tips are equal | ||
if c := a.FeeCapCmp(b); c != 0 { | ||
return c | ||
} | ||
// Compare tips if effective tips and fee caps are equal | ||
return a.TipCmp(b) | ||
} | ||
|
||
func (h *priceHeap) Push(x interface{}) { | ||
*h = append(*h, x.(*types.Transaction)) | ||
tx := x.(*types.Transaction) | ||
h.list = append(h.list, tx) | ||
} | ||
|
||
func (h *priceHeap) Pop() interface{} { | ||
old := *h | ||
old := h.list | ||
n := len(old) | ||
x := old[n-1] | ||
old[n-1] = nil | ||
*h = old[0 : n-1] | ||
h.list = old[0 : n-1] | ||
return x | ||
} | ||
|
||
// txPricedList is a price-sorted heap to allow operating on transactions pool | ||
// contents in a price-incrementing way. It's built opon the all transactions | ||
// in txpool but only interested in the remote part. It means only remote transactions | ||
// will be considered for tracking, sorting, eviction, etc. | ||
// | ||
// Two heaps are used for sorting: the urgent heap (based on effective tip in the next | ||
// block) and the floating heap (based on feeCap). Always the bigger heap is chosen for | ||
// eviction. Transactions evicted from the urgent heap are first demoted into the floating heap. | ||
// In some cases (during a congestion, when blocks are full) the urgent heap can provide | ||
// better candidates for inclusion while in other cases (at the top of the baseFee peak) | ||
// the floating heap is better. When baseFee is decreasing they behave similarly. | ||
type txPricedList struct { | ||
all *txLookup // Pointer to the map of all transactions | ||
remotes *priceHeap // Heap of prices of all the stored **remote** transactions | ||
stales int // Number of stale price points to (re-heap trigger) | ||
all *txLookup // Pointer to the map of all transactions | ||
urgent, floating priceHeap // Heaps of prices of all the stored **remote** transactions | ||
stales int // Number of stale price points to (re-heap trigger) | ||
} | ||
|
||
const ( | ||
// urgentRatio : floatingRatio is the capacity ratio of the two queues | ||
urgentRatio = 4 | ||
floatingRatio = 1 | ||
) | ||
|
||
// newTxPricedList creates a new price-sorted transaction heap. | ||
func newTxPricedList(all *txLookup) *txPricedList { | ||
return &txPricedList{ | ||
all: all, | ||
remotes: new(priceHeap), | ||
all: all, | ||
} | ||
} | ||
|
||
|
@@ -460,7 +501,8 @@ func (l *txPricedList) Put(tx *types.Transaction, local bool) { | |
if local { | ||
return | ||
} | ||
heap.Push(l.remotes, tx) | ||
// Insert every new transaction to the urgent heap first; Discard will balance the heaps | ||
heap.Push(&l.urgent, tx) | ||
} | ||
|
||
// Removed notifies the prices transaction list that an old transaction dropped | ||
|
@@ -469,58 +511,42 @@ func (l *txPricedList) Put(tx *types.Transaction, local bool) { | |
func (l *txPricedList) Removed(count int) { | ||
// Bump the stale counter, but exit if still too low (< 25%) | ||
l.stales += count | ||
if l.stales <= len(*l.remotes)/4 { | ||
if l.stales <= (len(l.urgent.list)+len(l.floating.list))/4 { | ||
return | ||
} | ||
// Seems we've reached a critical number of stale transactions, reheap | ||
l.Reheap() | ||
} | ||
|
||
// Cap finds all the transactions below the given price threshold, drops them | ||
// from the priced list and returns them for further removal from the entire pool. | ||
// | ||
// Note: only remote transactions will be considered for eviction. | ||
func (l *txPricedList) Cap(threshold *big.Int) types.Transactions { | ||
drop := make(types.Transactions, 0, 128) // Remote underpriced transactions to drop | ||
for len(*l.remotes) > 0 { | ||
// Discard stale transactions if found during cleanup | ||
cheapest := (*l.remotes)[0] | ||
if l.all.GetRemote(cheapest.Hash()) == nil { // Removed or migrated | ||
heap.Pop(l.remotes) | ||
l.stales-- | ||
continue | ||
} | ||
// Stop the discards if we've reached the threshold | ||
if cheapest.GasPriceIntCmp(threshold) >= 0 { | ||
break | ||
} | ||
heap.Pop(l.remotes) | ||
drop = append(drop, cheapest) | ||
} | ||
return drop | ||
} | ||
|
||
// Underpriced checks whether a transaction is cheaper than (or as cheap as) the | ||
// lowest priced (remote) transaction currently being tracked. | ||
func (l *txPricedList) Underpriced(tx *types.Transaction) bool { | ||
return l.underpricedFor(&l.urgent, tx) && l.underpricedFor(&l.floating, tx) | ||
} | ||
|
||
// underpricedFor checks whether a transaction is cheaper than (or as cheap as) the | ||
// lowest priced (remote) transaction in the given heap. | ||
func (l *txPricedList) underpricedFor(h *priceHeap, tx *types.Transaction) bool { | ||
// Discard stale price points if found at the heap start | ||
for len(*l.remotes) > 0 { | ||
head := []*types.Transaction(*l.remotes)[0] | ||
for len(h.list) > 0 { | ||
head := h.list[0] | ||
if l.all.GetRemote(head.Hash()) == nil { // Removed or migrated | ||
l.stales-- | ||
heap.Pop(l.remotes) | ||
heap.Pop(h) | ||
continue | ||
} | ||
break | ||
} | ||
// Check if the transaction is underpriced or not | ||
if len(*l.remotes) == 0 { | ||
return false // There is no remote transaction at all. | ||
if len(h.list) == 0 { | ||
// Note: since Underpriced is only called when the pool is full, this case is only | ||
// possible if one of the queues has a zero capacity. In this case we should report | ||
// that the transaction in question will not fit in this queue. | ||
return true // There is no remote transaction at all. | ||
} | ||
// If the remote transaction is even cheaper than the | ||
// cheapest one tracked locally, reject it. | ||
cheapest := []*types.Transaction(*l.remotes)[0] | ||
return cheapest.GasPriceCmp(tx) >= 0 | ||
return h.cmp(h.list[0], tx) >= 0 | ||
} | ||
|
||
// Discard finds a number of most underpriced transactions, removes them from the | ||
|
@@ -529,21 +555,36 @@ func (l *txPricedList) Underpriced(tx *types.Transaction) bool { | |
// Note local transaction won't be considered for eviction. | ||
func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool) { | ||
drop := make(types.Transactions, 0, slots) // Remote underpriced transactions to drop | ||
for len(*l.remotes) > 0 && slots > 0 { | ||
// Discard stale transactions if found during cleanup | ||
tx := heap.Pop(l.remotes).(*types.Transaction) | ||
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated | ||
l.stales-- | ||
continue | ||
for slots > 0 { | ||
if len(l.urgent.list)*floatingRatio > len(l.floating.list)*urgentRatio || floatingRatio == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's a good idea to rebalance things if they are not full. We should keep shoving txs into the urgent queue until it fills up and only then overflow into the floating one. Am I missing something perhaps? I don't see any upside, only downsides to moving txs into the floating queue while there's room in the urgent queue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In |
||
// Discard stale transactions if found during cleanup | ||
tx := heap.Pop(&l.urgent).(*types.Transaction) | ||
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated | ||
l.stales-- | ||
continue | ||
} | ||
// Non stale transaction found, move to floating heap | ||
heap.Push(&l.floating, tx) | ||
} else { | ||
if len(l.floating.list) == 0 { | ||
// Stop if both heaps are empty | ||
break | ||
} | ||
// Discard stale transactions if found during cleanup | ||
tx := heap.Pop(&l.floating).(*types.Transaction) | ||
if l.all.GetRemote(tx.Hash()) == nil { // Removed or migrated | ||
l.stales-- | ||
continue | ||
} | ||
// Non stale transaction found, discard it | ||
drop = append(drop, tx) | ||
slots -= numSlots(tx) | ||
} | ||
// Non stale transaction found, discard it | ||
drop = append(drop, tx) | ||
slots -= numSlots(tx) | ||
} | ||
// If we still can't make enough room for the new transaction | ||
if slots > 0 && !force { | ||
for _, tx := range drop { | ||
heap.Push(l.remotes, tx) | ||
heap.Push(&l.urgent, tx) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is incorrect. The transaction most probably originated from the floating heap, not the urgent heap. Furthermore, we have done certain urgent -> floating heap movements, which should also be reverted in case we can't discard the required number of slots. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With reverting urgent->floating movements you are theoretically right, I'll think about a clean and simple way to handle this.Putting everything in urgent is good though, we can always add everything to urgent first and balance the queues out before discarding anything. |
||
} | ||
return nil, false | ||
} | ||
|
@@ -552,12 +593,32 @@ func (l *txPricedList) Discard(slots int, force bool) (types.Transactions, bool) | |
|
||
// Reheap forcibly rebuilds the heap based on the current remote transaction set. | ||
func (l *txPricedList) Reheap() { | ||
reheap := make(priceHeap, 0, l.all.RemoteCount()) | ||
|
||
l.stales, l.remotes = 0, &reheap | ||
start := time.Now() | ||
l.stales = 0 | ||
l.urgent.list = make([]*types.Transaction, 0, l.all.RemoteCount()) | ||
l.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool { | ||
*l.remotes = append(*l.remotes, tx) | ||
l.urgent.list = append(l.urgent.list, tx) | ||
return true | ||
}, false, true) // Only iterate remotes | ||
heap.Init(l.remotes) | ||
heap.Init(&l.urgent) | ||
|
||
// balance out the two heaps by moving the worse half of transactions into the | ||
// floating heap | ||
// Note: Discard would also do this before the first eviction but Reheap can do | ||
// is more efficiently. Also, Underpriced would work suboptimally the first time | ||
// if the floating queue was empty. | ||
floatingCount := len(l.urgent.list) * floatingRatio / (urgentRatio + floatingRatio) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't floatingCount be only the excess above the urgent cap? E.g. if we have 4+1K txs configured, we should only overflow if above 4K. Currently if I have 10txs, I'll overflow 2, which imho doesn't make sense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See below |
||
l.floating.list = make([]*types.Transaction, floatingCount) | ||
for i := 0; i < floatingCount; i++ { | ||
l.floating.list[i] = heap.Pop(&l.urgent).(*types.Transaction) | ||
} | ||
heap.Init(&l.floating) | ||
reheapTimer.Update(time.Since(start)) | ||
} | ||
|
||
// SetBaseFee updates the base fee and triggers a re-heap. Note that Removed is not | ||
// necessary to call right before SetBaseFee when processing a new block. | ||
func (l *txPricedList) SetBaseFee(baseFee *big.Int) { | ||
l.urgent.baseFee = baseFee | ||
l.Reheap() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this strategy, we can only replace the transaction with 10% feeCap bump and 10% tip bump. It might hurt the UX to some extent.
For example, if the user already specify a high enough feeCap but a barely enough tip. And he wants to speed up the inclusion, he has to bump the feeCap as well. If the basefee raises up, then the maximum possible payment is increased.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been discussed at the 1559 dev channel, right now this replacement condition seems to be the best choice.