From ba9d3548d460d79e2c795b062c2d7d7fd4c1c4fd Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Wed, 4 Feb 2026 08:21:06 +0100 Subject: [PATCH 1/6] core/txpool/blobpool: simplify evictionPriority code evictionPriority was used in only one place, where we cap it to 0. Cleaner to do it right in the function. Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/evictheap.go | 6 ------ core/txpool/blobpool/priority.go | 5 +---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/core/txpool/blobpool/evictheap.go b/core/txpool/blobpool/evictheap.go index 722a71bc9b..a519b0c5ee 100644 --- a/core/txpool/blobpool/evictheap.go +++ b/core/txpool/blobpool/evictheap.go @@ -95,13 +95,7 @@ func (h *evictHeap) Less(i, j int) bool { lastJ := txsJ[len(txsJ)-1] prioI := evictionPriority(h.basefeeJumps, lastI.evictionExecFeeJumps, h.blobfeeJumps, lastI.evictionBlobFeeJumps) - if prioI > 0 { - prioI = 0 - } prioJ := evictionPriority(h.basefeeJumps, lastJ.evictionExecFeeJumps, h.blobfeeJumps, lastJ.evictionBlobFeeJumps) - if prioJ > 0 { - prioJ = 0 - } if prioI == prioJ { return lastI.evictionExecTip.Lt(lastJ.evictionExecTip) } diff --git a/core/txpool/blobpool/priority.go b/core/txpool/blobpool/priority.go index 7ae7f92def..0fa9986c60 100644 --- a/core/txpool/blobpool/priority.go +++ b/core/txpool/blobpool/priority.go @@ -36,10 +36,7 @@ func evictionPriority(basefeeJumps float64, txBasefeeJumps, blobfeeJumps, txBlob basefeePriority = evictionPriority1D(basefeeJumps, txBasefeeJumps) blobfeePriority = evictionPriority1D(blobfeeJumps, txBlobfeeJumps) ) - if basefeePriority < blobfeePriority { - return basefeePriority - } - return blobfeePriority + return min(0, basefeePriority, blobfeePriority) } // evictionPriority1D calculates the eviction priority based on the algorithm From c376651c427d7fea09658150f31d47a6f66f1d08 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Wed, 4 Feb 2026 09:04:09 +0100 Subject: [PATCH 2/6] core/txpool/blobpool: update blobpool eviction policy Previously, blobpool eviction priority did not differentiate well transactions that are close to the basefee limit and transactions that are way under the limit. Here we improve this differentiation, giving more priority to transactions that are closer to the current base fee and/or blob fee, thus potentially includable in a shorter time. Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool.go | 47 ++++++++++++++------------- core/txpool/blobpool/priority.go | 7 ++-- core/txpool/blobpool/priority_test.go | 12 +++---- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 27441ac2e2..4a03a0d86c 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -281,47 +281,48 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac // solve after every block. // // - The first observation is that comparing 1559 base fees or 4844 blob fees -// needs to happen in the context of their dynamism. Since these fees jump -// up or down in ~1.125 multipliers (at max) across blocks, comparing fees -// in two transactions should be based on log1.125(fee) to eliminate noise. +// needs to happen in the context of their dynamism. Since base fees are +// adjusted continuously and fluctuate, and we want to optimize for effective +// miner fees, it is better to disregard small base fee cap differences. +// Instead of considering the exact fee cap values, we should group +// transactions into buckets based on fee cap values, allowing us to use +// the miner tip meaningfully as a splitter inside a bucket. // -// - The second observation is that the basefee and blobfee move independently, -// so there's no way to split mixed txs on their own (A has higher base fee, -// B has higher blob fee). Rather than look at the absolute fees, the useful -// metric is the max time it can take to exceed the transaction's fee caps. +// To create these buckets, rather than looking at the absolute fee +// differences, the useful metric is the max time it can take to exceed the +// transaction's fee caps. Base fee changes are multiplicative, so we use a +// logarithmic scale. Fees jumps up or down in ~1.125 multipliers at max +// across blocks, so we use log1.125(fee) and rounding to eliminate noise. // Specifically, we're interested in the number of jumps needed to go from // the current fee to the transaction's cap: // -// jumps = log1.125(txfee) - log1.125(basefee) +// jumps = floor(log1.125(txfee) - log1.125(basefee)) + +// - The second observation is that when ranking executable blob txs, it +// does not make sense to grant a later eviction priority to txs with high +// fee caps since it could enable pool wars. As such, any positive priority +// will be grouped together. // -// - The third observation is that the base fee tends to hover around rather -// than swing wildly. The number of jumps needed from the current fee starts -// to get less relevant the higher it is. To remove the noise here too, the -// pool will use log(jumps) as the delta for comparing transactions. +// priority = min(jumps, 0) + +// - The third observation is that the basefee and blobfee move independently, +// so there's no way to split mixed txs on their own (A has higher base fee, +// B has higher blob fee). // -// delta = sign(jumps) * log(abs(jumps)) -// -// - To establish a total order, we need to reduce the dimensionality of the +// To establish a total order, we need to reduce the dimensionality of the // two base fees (log jumps) to a single value. The interesting aspect from // the pool's perspective is how fast will a tx get executable (fees going // down, crossing the smaller negative jump counter) or non-executable (fees // going up, crossing the smaller positive jump counter). As such, the pool // cares only about the min of the two delta values for eviction priority. // -// priority = min(deltaBasefee, deltaBlobfee) +// priority = min(deltaBasefee, deltaBlobfee, 0) // // - The above very aggressive dimensionality and noise reduction should result // in transaction being grouped into a small number of buckets, the further // the fees the larger the buckets. This is good because it allows us to use // the miner tip meaningfully as a splitter. // -// - For the scenario where the pool does not contain non-executable blob txs -// anymore, it does not make sense to grant a later eviction priority to txs -// with high fee caps since it could enable pool wars. As such, any positive -// priority will be grouped together. -// -// priority = min(deltaBasefee, deltaBlobfee, 0) -// // Optimisation tradeoffs: // // - Eviction relies on 3 fee minimums per account (exec tip, exec cap and blob diff --git a/core/txpool/blobpool/priority.go b/core/txpool/blobpool/priority.go index 0fa9986c60..5c17b44d9c 100644 --- a/core/txpool/blobpool/priority.go +++ b/core/txpool/blobpool/priority.go @@ -43,11 +43,8 @@ func evictionPriority(basefeeJumps float64, txBasefeeJumps, blobfeeJumps, txBlob // described in the BlobPool docs for a single fee component. func evictionPriority1D(basefeeJumps float64, txfeeJumps float64) int { jumps := txfeeJumps - basefeeJumps - if int(jumps) == 0 { - return 0 // can't log2 0 - } - if jumps < 0 { - return -intLog2(uint(-math.Floor(jumps))) + if jumps <= 0 { + return int(math.Floor(jumps)) } return intLog2(uint(math.Ceil(jumps))) } diff --git a/core/txpool/blobpool/priority_test.go b/core/txpool/blobpool/priority_test.go index 1eaee6d7df..28b0b970d8 100644 --- a/core/txpool/blobpool/priority_test.go +++ b/core/txpool/blobpool/priority_test.go @@ -30,12 +30,12 @@ func TestPriorityCalculation(t *testing.T) { txfee uint64 result int }{ - {basefee: 7, txfee: 10, result: 2}, // 3.02 jumps, 4 ceil, 2 log2 - {basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 log2 - {basefee: 9_853_941_692, txfee: 11_085_092_510, result: 0}, // 0.99 jumps, 1 ceil, 0 log2 - {basefee: 11_544_106_391, txfee: 10_356_781_100, result: 0}, // -0.92 jumps, -1 floor, 0 log2 - {basefee: 17_200_000_000, txfee: 7, result: -7}, // -183.57 jumps, -184 floor, -7 log2 - {basefee: 7, txfee: 17_200_000_000, result: 7}, // 183.57 jumps, 184 ceil, 7 log2 + {basefee: 7, txfee: 10, result: 2}, // 3.02 jumps, 4 ceil, 2 log2 + {basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 log2 + {basefee: 9_853_941_692, txfee: 11_085_092_510, result: 0}, // 0.99 jumps, 1 ceil, 0 log2 + {basefee: 11_544_106_391, txfee: 10_356_781_100, result: -1}, // -0.92 jumps, -1 floor + {basefee: 17_200_000_000, txfee: 7, result: -184}, // -183.57 jumps, -184 floor + {basefee: 7, txfee: 17_200_000_000, result: 7}, // 183.57 jumps, 184 ceil, 7 log2 } for i, tt := range tests { var ( From 0e980c6ea292d314c5e5d110564feae04dfabe33 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Mon, 2 Feb 2026 21:22:07 +0100 Subject: [PATCH 3/6] core/txpool/blobpool: delay announcement of non-includable txs It is not the role of the blobpool to serve as a storage for limit orders below market: blob transactions with fee caps way below base fee or blob fee. Therefore, the propagation of blob transactions that are far from being includable is suppressed. The pool will only announce blob transactions that are close to being includable (based on the current fees and the transaction's fee caps), and will delay the announcement of blob transactions that are far from being includable until base fee and/or blob fee is reduced. Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool.go | 72 ++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 4a03a0d86c..491f87998c 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -104,6 +104,16 @@ const ( // maxGappedTxs is the maximum number of gapped transactions kept overall. // This is a safety limit to avoid DoS vectors. maxGapped = 128 + + // notifyThreshold is the eviction priority threshold above which a transaction + // is considered close enough to being includable to be announced to peers. + // Setting this to zero will disable announcements for anyting not immediately + // includable. Setting it to -1 allows transactions that are close to being + // includable, maybe already in the next block if fees go down, to be announced. + + // Note, this threshold is in the abstract eviction priority space, so its + // meaning depends on the current basefee/blobfee and the transaction's fees. + announceThreshold = -1 ) // blobTxMeta is the minimal subset of types.BlobTx necessary to validate and @@ -115,6 +125,8 @@ type blobTxMeta struct { vhashes []common.Hash // Blob versioned hashes to maintain the lookup table version byte // Blob transaction version to determine proof type + announced bool // Whether the tx has been announced to listeners + id uint64 // Storage ID in the pool's persistent store storageSize uint32 // Byte size in the pool's persistent store size uint64 // RLP-encoded size of transaction including the attached blob @@ -210,6 +222,14 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac // via a normal transaction. It should nonetheless be high enough to support // resurrecting reorged transactions. Perhaps 4-16. // +// - It is not the role of the blobpool to serve as a storage for limit orders +// below market: blob transactions with fee caps way below base fee or blob fee. +// Therefore, the propagation of blob transactions that are far from being +// includable is suppressed. The pool will only announce blob transactions that +// are close to being includable (based on the current fees and the transaction's +// fee caps), and will delay the announcement of blob transactions that are far +// from being includable until base fee and/or blob fee is reduced. +// // - Local txs are meaningless. Mining pools historically used local transactions // for payouts or for backdoor deals. With 1559 in place, the basefee usually // dominates the final price, so 0 or non-0 tip doesn't change much. Blob txs @@ -894,6 +914,37 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { } p.evict.reinit(basefee, blobfee, false) + // Announce transactions that became announcable due to fee changes + var announcable []*types.Transaction + for addr, txs := range p.index { + for i, meta := range txs { + if !meta.announced && (i == 0 || txs[i-1].announced) && p.isAnnouncable(meta) { + // Load the full transaction and strip the sidecar before announcing + // TODO: this is a bit ugly, as we have everything needed in meta already + data, err := p.store.Get(meta.id) + // Technically, we are supposed to set announced only if Get is successful. + // However, Get failing here indicates a more serious issue (data loss), + // so we set announced anyway to avoid repeated attempts. + meta.announced = true + if err != nil { + log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) + continue + } + var tx types.Transaction + if err = rlp.DecodeBytes(data, &tx); err != nil { + log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) + continue + } + announcable = append(announcable, tx.WithoutBlobTxSidecar()) + log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", tx.Hash()) + } + } + } + if len(announcable) > 0 { + p.discoverFeed.Send(core.NewTxsEvent{Txs: announcable}) + } + + // Update the basefee and blobfee metrics basefeeGauge.Update(int64(basefee.Uint64())) blobfeeGauge.Update(int64(blobfee.Uint64())) p.updateStorageMetrics() @@ -1679,9 +1730,15 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error addValidMeter.Mark(1) - // Notify all listeners of the new arrival - p.discoverFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) - p.insertFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) + // Transaction was addded successfully, but we only announce if it is (close to being) + // includable and the previous one was already announced. + if p.isAnnouncable(meta) && (meta.nonce == next || (len(txs) > 1 && txs[offset-1].announced)) { + meta.announced = true + p.discoverFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) + p.insertFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) + } else { + log.Trace("Blob transaction not announcable yet", "hash", tx.Hash(), "nonce", tx.Nonce()) + } //check the gapped queue for this account and try to promote if gtxs, ok := p.gapped[from]; checkGapped && ok && len(gtxs) > 0 { @@ -2005,6 +2062,15 @@ func (p *BlobPool) evictGapped() { } } +// isAnnouncable checks whether a transaction is announcable based on its +// fee parameters and announceThreshold. +func (p *BlobPool) isAnnouncable(meta *blobTxMeta) bool { + if evictionPriority(p.evict.basefeeJumps, meta.basefeeJumps, p.evict.blobfeeJumps, meta.blobfeeJumps) >= announceThreshold { + return true + } + return false +} + // Stats retrieves the current pool stats, namely the number of pending and the // number of queued (non-executable) transactions. func (p *BlobPool) Stats() (int, int) { From 9c4a1f900841cc35f7e0719670768ecea87acbf6 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Wed, 4 Feb 2026 10:31:31 +0100 Subject: [PATCH 4/6] core/txpool/blobpool: fix announced state initialization and tests Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool.go | 15 +++++++++++++++ core/txpool/blobpool/blobpool_test.go | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 491f87998c..eb42e63b87 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -491,6 +491,20 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser } p.evict = newPriceHeap(basefee, blobfee, p.index) + // Guess what was announced. This is needed because we don't want to + // participate in the diffusion of transactions where inclusion is blocked by + // a low base fee transaction. Since we don't persist that info, the best + // we can do is to assume that anything that could have been announced + // at current prices, actually was. + for addr := range p.index { + for _, tx := range p.index[addr] { + tx.announced = p.isAnnouncable(tx) + if !tx.announced { + break + } + } + } + // Pool initialized, attach the blob limbo to it to track blobs included // recently but not yet finalized p.limbo, err = newLimbo(p.chain.Config(), limbodir) @@ -538,6 +552,7 @@ func (p *BlobPool) Close() error { // parseTransaction is a callback method on pool creation that gets called for // each transaction on disk to create the in-memory metadata index. +// Announced state is not initialized here, it needs to be iniitalized seprately. func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error { tx := new(types.Transaction) if err := rlp.DecodeBytes(blob, tx); err != nil { diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 4bb3567b69..7fc63b96dc 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1751,8 +1751,8 @@ func TestAdd(t *testing.T) { // Create a blob pool out of the pre-seeded dats chain := &testBlockChain{ config: params.MainnetChainConfig, - basefee: uint256.NewInt(1050), - blobfee: uint256.NewInt(105), + basefee: uint256.NewInt(1), + blobfee: uint256.NewInt(1), statedb: statedb, } pool := New(Config{Datadir: storage}, chain, nil) From a19f1ff11b99d39611053114fb47693705b42d63 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Wed, 25 Feb 2026 09:36:22 +0100 Subject: [PATCH 5/6] core/txpool/blobpool: remove the log-log grouping for positive priorities The priority groups for positive fee difference were not used. In these cases we always used the tip as the basis for comparison. Thus, it is useless to do an extra log, just to then throw it away. Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/priority.go | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/core/txpool/blobpool/priority.go b/core/txpool/blobpool/priority.go index 5c17b44d9c..27e87a775a 100644 --- a/core/txpool/blobpool/priority.go +++ b/core/txpool/blobpool/priority.go @@ -18,7 +18,6 @@ package blobpool import ( "math" - "math/bits" "github.com/holiman/uint256" ) @@ -46,7 +45,10 @@ func evictionPriority1D(basefeeJumps float64, txfeeJumps float64) int { if jumps <= 0 { return int(math.Floor(jumps)) } - return intLog2(uint(math.Ceil(jumps))) + // We only use the negative part for ordering. The positive part is only used + // for threshold comparision (with a negative threshold), so the value is almost + // irrelevant, as long as it's positive. + return int((math.Ceil(jumps))) } // dynamicFeeJumps calculates the log1.125(fee), namely the number of fee jumps @@ -64,21 +66,5 @@ func dynamicFeeJumps(fee *uint256.Int) float64 { return math.Log(fee.Float64()) / log1_125 } -// intLog2 is a helper to calculate the integral part of a log2 of an unsigned -// integer. It is a very specific calculation that's not particularly useful in -// general, but it's what we need here (it's fast). -func intLog2(n uint) int { - switch { - case n == 0: - panic("log2(0) is undefined") - - case n < 2048: - return bits.UintSize - bits.LeadingZeros(n) - 1 - - default: - // The input is log1.125(uint256) = log2(uint256) / log2(1.125). At the - // most extreme, log2(uint256) will be a bit below 257, and the constant - // log2(1.125) ~= 0.17. The larges input thus is ~257 / ~0.17 ~= ~1511. - panic("dynamic fee jump diffs cannot reach this") } } From 27994a7c0951a08e25dfe21eef3b1ea5697d6869 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Thu, 26 Feb 2026 01:19:27 +0100 Subject: [PATCH 6/6] core/txpool/blobpool: adopt log calculation to new max decrease EIP-7892 (BPO) changed the maximum blobfee decrease in a slot from 1.125 to 1.17 . Since we want priorties to approximate time, we should change our log calculation. Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool.go | 12 +++++++++--- core/txpool/blobpool/blobpool_test.go | 4 ++-- core/txpool/blobpool/evictheap.go | 2 +- core/txpool/blobpool/evictheap_test.go | 24 ++++++++++++------------ core/txpool/blobpool/priority.go | 11 +++++++++++ core/txpool/blobpool/priority_test.go | 12 ++++++------ 6 files changed, 41 insertions(+), 24 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index eb42e63b87..d1c732ae0a 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -171,7 +171,7 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac blobGas: tx.BlobGas(), } meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap) - meta.blobfeeJumps = dynamicFeeJumps(meta.blobFeeCap) + meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap) return meta } @@ -317,14 +317,20 @@ func newBlobTxMeta(id uint64, size uint64, storageSize uint32, tx *types.Transac // the current fee to the transaction's cap: // // jumps = floor(log1.125(txfee) - log1.125(basefee)) - +// +// For blob fees, EIP-7892 changed the ratio of target to max blobs, and +// with that also the maximum blob fee decrease in a slot from 1.125 to +// approx 1.17. therefore, we use: +// +// blobfeeJumps = floor(log1.17(txBlobfee) - log1.17(blobfee)) +// // - The second observation is that when ranking executable blob txs, it // does not make sense to grant a later eviction priority to txs with high // fee caps since it could enable pool wars. As such, any positive priority // will be grouped together. // // priority = min(jumps, 0) - +// // - The third observation is that the basefee and blobfee move independently, // so there's no way to split mixed txs on their own (A has higher base fee, // B has higher blob fee). diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 7fc63b96dc..6580a339e3 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -830,8 +830,8 @@ func TestOpenIndex(t *testing.T) { //blobfeeJumps = []float64{34.023, 35.570, 36.879, 29.686, 26.243, 20.358} // log 1.125 (blob fee cap) evictExecTipCaps = []uint64{10, 10, 5, 5, 1, 1} - evictExecFeeJumps = []float64{39.098, 38.204, 38.204, 19.549, 19.549, 19.549} // min(log 1.125 (exec fee cap)) - evictBlobFeeJumps = []float64{34.023, 34.023, 34.023, 29.686, 26.243, 20.358} // min(log 1.125 (blob fee cap)) + evictExecFeeJumps = []float64{39.098, 38.204, 38.204, 19.549, 19.549, 19.549} // min(log 1.125 (exec fee cap)) + evictBlobFeeJumps = []float64{25.517256, 25.517256, 25.517256, 22.264502, 19.682646, 15.268934} // min(log 1.17 (blob fee cap)) totalSpent = uint256.NewInt(21000*(100+90+200+10+80+300) + blobSize*(55+66+77+33+22+11) + 100*6) // 21000 gas x price + 128KB x blobprice + value ) diff --git a/core/txpool/blobpool/evictheap.go b/core/txpool/blobpool/evictheap.go index a519b0c5ee..a46b8e9a6f 100644 --- a/core/txpool/blobpool/evictheap.go +++ b/core/txpool/blobpool/evictheap.go @@ -67,7 +67,7 @@ func newPriceHeap(basefee *uint256.Int, blobfee *uint256.Int, index map[common.A func (h *evictHeap) reinit(basefee *uint256.Int, blobfee *uint256.Int, force bool) { // If the update is mostly the same as the old, don't sort pointlessly basefeeJumps := dynamicFeeJumps(basefee) - blobfeeJumps := dynamicFeeJumps(blobfee) + blobfeeJumps := dynamicBlobFeeJumps(blobfee) if !force && math.Abs(h.basefeeJumps-basefeeJumps) < 0.01 && math.Abs(h.blobfeeJumps-blobfeeJumps) < 0.01 { // TODO(karalabe): 0.01 enough, maybe should be smaller? Maybe this optimization is moot? return diff --git a/core/txpool/blobpool/evictheap_test.go b/core/txpool/blobpool/evictheap_test.go index de4076e298..112fa77a01 100644 --- a/core/txpool/blobpool/evictheap_test.go +++ b/core/txpool/blobpool/evictheap_test.go @@ -109,22 +109,22 @@ func TestPriceHeapSorting(t *testing.T) { order: []int{3, 2, 1, 0, 4, 5, 6}, }, // If both basefee and blobfee is specified, sort by the larger distance - // of the two from the current network conditions, splitting same (loglog) + // of the two from the current network conditions, splitting same // ones via the tip. // - // Basefee: 1000 - // Blobfee: 100 + // Basefee: 1000 , jumps: 888, 790, 702, 624 + // Blobfee: 100 , jumps: 85, 73, 62, 53 // - // Tx #0: (800, 80) - 2 jumps below both => priority -1 - // Tx #1: (630, 63) - 4 jumps below both => priority -2 - // Tx #2: (800, 63) - 2 jumps below basefee, 4 jumps below blobfee => priority -2 (blob penalty dominates) - // Tx #3: (630, 80) - 4 jumps below basefee, 2 jumps below blobfee => priority -2 (base penalty dominates) + // Tx #0: (800, 80) - 2 jumps below both => priority -2 + // Tx #1: (630, 55) - 4 jumps below both => priority -4 + // Tx #2: (800, 55) - 2 jumps below basefee, 4 jumps below blobfee => priority -4 (blob penalty dominates) + // Tx #3: (630, 80) - 4 jumps below basefee, 2 jumps below blobfee => priority -4 (base penalty dominates) // // Txs 1, 2, 3 share the same priority, split via tip, prefer 0 as the best { execTips: []uint64{1, 2, 3, 4}, execFees: []uint64{800, 630, 800, 630}, - blobFees: []uint64{80, 63, 63, 80}, + blobFees: []uint64{80, 55, 55, 80}, basefee: 1000, blobfee: 100, order: []int{1, 2, 3, 0}, @@ -142,7 +142,7 @@ func TestPriceHeapSorting(t *testing.T) { blobFee = uint256.NewInt(tt.blobFees[j]) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) index[addr] = []*blobTxMeta{{ id: uint64(j), @@ -201,7 +201,7 @@ func benchmarkPriceHeapReinit(b *testing.B, datacap uint64) { blobFee = uint256.NewInt(rnd.Uint64()) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) index[addr] = []*blobTxMeta{{ id: uint64(i), @@ -277,7 +277,7 @@ func benchmarkPriceHeapOverflow(b *testing.B, datacap uint64) { blobFee = uint256.NewInt(rnd.Uint64()) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) index[addr] = []*blobTxMeta{{ id: uint64(i), @@ -308,7 +308,7 @@ func benchmarkPriceHeapOverflow(b *testing.B, datacap uint64) { blobFee = uint256.NewInt(rnd.Uint64()) basefeeJumps = dynamicFeeJumps(execFee) - blobfeeJumps = dynamicFeeJumps(blobFee) + blobfeeJumps = dynamicBlobFeeJumps(blobFee) ) metas[i] = &blobTxMeta{ id: uint64(int(blobs) + i), diff --git a/core/txpool/blobpool/priority.go b/core/txpool/blobpool/priority.go index 27e87a775a..c8365ab277 100644 --- a/core/txpool/blobpool/priority.go +++ b/core/txpool/blobpool/priority.go @@ -25,6 +25,13 @@ import ( // log1_125 is used in the eviction priority calculation. var log1_125 = math.Log(1.125) +// log1_17 is used in the eviction priority calculation for blob fees. +// EIP-7892 (BPO) changed the ratio of target to max blobs, and with that +// also the maximum blob fee decrease in a slot from 1.125 to approx 1.17 . +// Since we want priorities to approximate time, we should change our log +// calculation for blob fees. +var log1_17 = log1_125 * 4 / 3 + // evictionPriority calculates the eviction priority based on the algorithm // described in the BlobPool docs for both fee components. // @@ -66,5 +73,9 @@ func dynamicFeeJumps(fee *uint256.Int) float64 { return math.Log(fee.Float64()) / log1_125 } +func dynamicBlobFeeJumps(fee *uint256.Int) float64 { + if fee.IsZero() { + return 0 // can't log2 zero, should never happen outside tests, but don't choke } + return math.Log(fee.Float64()) / log1_17 } diff --git a/core/txpool/blobpool/priority_test.go b/core/txpool/blobpool/priority_test.go index 28b0b970d8..47b3a5375f 100644 --- a/core/txpool/blobpool/priority_test.go +++ b/core/txpool/blobpool/priority_test.go @@ -30,12 +30,12 @@ func TestPriorityCalculation(t *testing.T) { txfee uint64 result int }{ - {basefee: 7, txfee: 10, result: 2}, // 3.02 jumps, 4 ceil, 2 log2 - {basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 log2 - {basefee: 9_853_941_692, txfee: 11_085_092_510, result: 0}, // 0.99 jumps, 1 ceil, 0 log2 + {basefee: 7, txfee: 10, result: 4}, // 3.02 jumps, 4 ceil + {basefee: 17_200_000_000, txfee: 17_200_000_000, result: 0}, // 0 jumps, special case 0 + {basefee: 9_853_941_692, txfee: 11_085_092_510, result: 1}, // 0.99 jumps, 1 ceil {basefee: 11_544_106_391, txfee: 10_356_781_100, result: -1}, // -0.92 jumps, -1 floor {basefee: 17_200_000_000, txfee: 7, result: -184}, // -183.57 jumps, -184 floor - {basefee: 7, txfee: 17_200_000_000, result: 7}, // 183.57 jumps, 184 ceil, 7 log2 + {basefee: 7, txfee: 17_200_000_000, result: 184}, // 183.57 jumps, 184 ceil } for i, tt := range tests { var ( @@ -69,7 +69,7 @@ func BenchmarkPriorityCalculation(b *testing.B) { blobfee := uint256.NewInt(123_456_789_000) // Completely random, no idea what this will be basefeeJumps := dynamicFeeJumps(basefee) - blobfeeJumps := dynamicFeeJumps(blobfee) + blobfeeJumps := dynamicBlobFeeJumps(blobfee) // The transaction's fee cap and blob fee cap are constant across the life // of the transaction, so we can pre-calculate and cache them. @@ -77,7 +77,7 @@ func BenchmarkPriorityCalculation(b *testing.B) { txBlobfeeJumps := make([]float64, b.N) for i := 0; i < b.N; i++ { txBasefeeJumps[i] = dynamicFeeJumps(uint256.NewInt(rnd.Uint64())) - txBlobfeeJumps[i] = dynamicFeeJumps(uint256.NewInt(rnd.Uint64())) + txBlobfeeJumps[i] = dynamicBlobFeeJumps(uint256.NewInt(rnd.Uint64())) } b.ResetTimer() b.ReportAllocs()