From 0d13437991d529a79c6c229029a4d0b962ee67d0 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Mon, 12 Jan 2026 19:51:43 +0100 Subject: [PATCH 1/6] core/txpool/blobpool: add test for drop at capacity Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool_test.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 4bb3567b69..dcb14efbc7 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1359,9 +1359,10 @@ func TestAdd(t *testing.T) { } tests := []struct { - seeds map[string]seed - adds []addtx - block []addtx + seeds map[string]seed + adds []addtx + block []addtx + datacap uint64 }{ // Transactions from new accounts should be accepted if their initial // nonce matches the expected one from the statedb. Higher or lower must @@ -1715,6 +1716,24 @@ func TestAdd(t *testing.T) { }, }, }, + // Transactions above the Datacap should be rejected + { + seeds: map[string]seed{ + "alice": {balance: 10000000}, + }, + datacap: 1 * (txAvgSize + blobSize + uint64(txBlobOverhead)), // only allow 1 blob + adds: []addtx{ + { // Fits in capacity + from: "alice", + tx: makeUnsignedTx(0, 1, 1, 1), + }, + { // Beyond capacity + from: "alice", + tx: makeUnsignedTx(1, 1, 1, 1), + err: txpool.ErrUnderpriced, + }, + }, + }, } for i, tt := range tests { // Create a temporary folder for the persistent backend @@ -1755,7 +1774,7 @@ func TestAdd(t *testing.T) { blobfee: uint256.NewInt(105), statedb: statedb, } - pool := New(Config{Datadir: storage}, chain, nil) + pool := New(Config{Datadir: storage, Datacap: tt.datacap}, chain, nil) if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil { t.Fatalf("test %d: failed to create blob pool: %v", i, err) } From e9d185f15b15329c3dc268d5e1e71ed8225efbdd Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Mon, 12 Jan 2026 15:50:56 +0100 Subject: [PATCH 2/6] core/txpool/blobpool: return error on underpriced This also solves a forwarding issue, where underpriced transactions were announced. Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 27441ac2e2..a1d9d9b820 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1676,6 +1676,14 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error } p.updateStorageMetrics() + // If we've just dropped the added transaction, it was clearly underpriced. + // We could also try to check for this earlier, but it is compex because + // of the rolling fee caculations. + if !p.lookup.exists(tx.Hash()) { + addUnderpricedMeter.Mark(1) + return txpool.ErrUnderpriced + } + addValidMeter.Mark(1) // Notify all listeners of the new arrival From 5c1272effc2ccd9ccf75e35f02d8e8f35518ddb7 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Mon, 12 Jan 2026 20:02:52 +0100 Subject: [PATCH 3/6] core/txpool/blobpool: verify that events are emittted correctly Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index dcb14efbc7..d50dfee2cb 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1780,6 +1780,11 @@ func TestAdd(t *testing.T) { } verifyPoolInternals(t, pool) + // subscibe to pool events to verify they are emitted correctly + txsCh := make(chan core.NewTxsEvent, 1) + txsSub := pool.SubscribeTransactions(txsCh, false) + defer txsSub.Unsubscribe() + // Add each transaction one by one, verifying the pool internals in between for j, add := range tt.adds { signed, _ := types.SignNewTx(keys[add.from], types.LatestSigner(params.MainnetChainConfig), add.tx) @@ -1810,6 +1815,26 @@ func TestAdd(t *testing.T) { } // Verify the pool internals after each addition verifyPoolInternals(t, pool) + // verify that if the tx was added, an event was emitted + if add.err == nil { + select { + case ev := <-txsCh: + if len(ev.Txs) != 1 { + t.Errorf("test %d, tx %d: event txs length mismatch: have %d, want 1", i, j, len(ev.Txs)) + } + if ev.Txs[0].Hash() != signed.Hash() { + t.Errorf("test %d, tx %d: event tx mismatch: have %v, want %v", i, j, ev.Txs[0].Hash(), signed.Hash()) + } + default: + t.Errorf("test %d, tx %d: expected new tx event, none received", i, j) + } + } else { + select { + case ev := <-txsCh: + t.Errorf("test %d, tx %d: unexpected new tx event with %d txs", i, j, len(ev.Txs)) + default: + } + } } verifyPoolInternals(t, pool) From daa495f7759b03e64b84356997995f5b57292cec Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Fri, 16 Jan 2026 11:35:27 +0100 Subject: [PATCH 4/6] core/txpool: fix check for gapped messages Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool_test.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index d50dfee2cb..97bbe9a5ce 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1816,23 +1816,30 @@ func TestAdd(t *testing.T) { // Verify the pool internals after each addition verifyPoolInternals(t, pool) // verify that if the tx was added, an event was emitted - if add.err == nil { - select { - case ev := <-txsCh: + txStatus := pool.Status(signed.Hash()) + select { + case ev := <-txsCh: + switch { + case add.err == nil && txStatus == txpool.TxStatusPending: if len(ev.Txs) != 1 { t.Errorf("test %d, tx %d: event txs length mismatch: have %d, want 1", i, j, len(ev.Txs)) } if ev.Txs[0].Hash() != signed.Hash() { t.Errorf("test %d, tx %d: event tx mismatch: have %v, want %v", i, j, ev.Txs[0].Hash(), signed.Hash()) } + case add.err == nil && txStatus == txpool.TxStatusQueued: + t.Errorf("test %d, tx %d: unexpected new tx event for queued tx", i, j) + case add.err != nil: + t.Errorf("test %d, tx %d: unexpected new tx event for failed tx", i, j) default: - t.Errorf("test %d, tx %d: expected new tx event, none received", i, j) + t.Errorf("test %d, tx %d: unexpected test result", i, j) } - } else { - select { - case ev := <-txsCh: - t.Errorf("test %d, tx %d: unexpected new tx event with %d txs", i, j, len(ev.Txs)) + default: + switch { + case add.err == nil && txStatus == txpool.TxStatusPending: + t.Errorf("test %d, tx %d: expected new tx event, none received", i, j) default: + // expected no event } } } From 9372a14868637c342d49a46cc36464a6bd553767 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Tue, 24 Feb 2026 12:00:00 +0100 Subject: [PATCH 5/6] core/txpool/blobpool: add extra underpriced test Signed-off-by: Csaba Kiraly --- core/txpool/blobpool/blobpool_test.go | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 97bbe9a5ce..642fbb5102 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1734,6 +1734,48 @@ func TestAdd(t *testing.T) { }, }, }, + // Transactions above the Datacap should be rejected, use eviction values + { + seeds: map[string]seed{ + "alice": { + balance: 2000000, + //nonce: 1, + txs: []*types.BlobTx{ + makeUnsignedTxWithTestBlob(0, 2, 2, 2, 0), + makeUnsignedTxWithTestBlob(1, 2, 2, 2, 1), + }, + }, + "bob": { + balance: 1000000, + //nonce: 1, + txs: []*types.BlobTx{ + makeUnsignedTxWithTestBlob(0, 3, 3, 3, 2), + }, + }, + }, + datacap: 3 * (txAvgSize + blobSize + uint64(txBlobOverhead)), // only allow 2 blobs + adds: []addtx{ + { // Beyond capacity, but kicking out one from Alice should make it fit + from: "bob", + tx: makeUnsignedTxWithTestBlob(1, 3, 3, 3, 3), + }, + { // We've just kicked our nonce 1, so this is nonce too high + from: "alice", + tx: makeUnsignedTxWithTestBlob(2, 1, 2, 2, 4), + err: core.ErrNonceTooHigh, + }, + { // This should not succeed, fees are low + from: "alice", + tx: makeUnsignedTxWithTestBlob(1, 1, 1, 1, 4), + err: txpool.ErrUnderpriced, + }, + { // This should also not succeed, because of rolling fee calculation + from: "alice", + tx: makeUnsignedTxWithTestBlob(1, 1, 1, 1, 4), + err: txpool.ErrUnderpriced, + }, + }, + }, } for i, tt := range tests { // Create a temporary folder for the persistent backend From 173e211d668dd06f081bed6f08efd5d22817b9d8 Mon Sep 17 00:00:00 2001 From: Csaba Kiraly Date: Thu, 5 Mar 2026 09:43:21 +0100 Subject: [PATCH 6/6] core/txpool/blobpool: fix reserver warning In the previous version the Reserver was released twice for underpriced transactions - once in the drop loop - once in the deferred code path Here we factorize the code to scope the deferred release better. --- core/txpool/blobpool/blobpool.go | 55 +++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index a1d9d9b820..ea949e31b2 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1509,10 +1509,10 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) { return p.addLocked(tx, true) } -// addLocked inserts a new blob transaction into the pool if it passes validation (both +// addLockedInternal inserts a new blob transaction into the pool if it passes validation (both // consensus validity and pool restrictions). It must be called with the pool lock held. // Only for internal use. -func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error) { +func (p *BlobPool) addLockedInternal(tx *types.Transaction, checkGapped bool) (err error) { // Ensure the transaction is valid from all perspectives if err := p.validateTx(tx); err != nil { log.Trace("Transaction validation failed", "hash", tx.Hash(), "err", err) @@ -1525,21 +1525,6 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error addStaleMeter.Mark(1) case errors.Is(err, core.ErrNonceTooHigh): addGappedMeter.Mark(1) - // Store the tx in memory, and revalidate later - from, _ := types.Sender(p.signer, tx) - allowance := p.gappedAllowance(from) - if allowance >= 1 && len(p.gapped) < maxGapped { - p.gapped[from] = append(p.gapped[from], tx) - p.gappedSource[tx.Hash()] = from - log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) - return nil - } else { - // if maxGapped is reached, it is better to give time to gapped - // transactions by keeping the old and dropping this one. - // Thus replacing a gapped transaction with another gapped transaction - // is discouraged. - log.Trace("no gapped blob queue allowance", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) - } case errors.Is(err, core.ErrInsufficientFunds): addOverdraftedMeter.Mark(1) case errors.Is(err, txpool.ErrAccountLimitExceeded): @@ -1669,6 +1654,37 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error heap.Fix(p.evict, p.evict.index[from]) } } + return nil +} + +// addLocked inserts a new blob transaction into the pool if it passes validation (both +// consensus validity and pool restrictions). It handles gapped transactions, underpriced +// transactions, pool limits, and notifications. +// It must be called with the pool lock held. Only for internal use. +func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error) { + // check for nonce gaps and add to gapped buffer if needed. + if err := p.addLockedInternal(tx, checkGapped); err != nil { + if errors.Is(err, core.ErrNonceTooHigh) { + addGappedMeter.Mark(1) + // Store the tx in memory, and revalidate later + from, _ := types.Sender(p.signer, tx) // already validated in addLockedInternal/ValidateTransactionWithState + allowance := p.gappedAllowance(from) + if allowance >= 1 && len(p.gapped) < maxGapped { + p.gapped[from] = append(p.gapped[from], tx) + p.gappedSource[tx.Hash()] = from + log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) + return nil + } else { + // if maxGapped is reached, it is better to give time to gapped + // transactions by keeping the old and dropping this one. + // Thus replacing a gapped transaction with another gapped transaction + // is discouraged. + log.Trace("no gapped blob queue allowance", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) + } + } + return err + } + // If the pool went over the allowed data limit, evict transactions until // we're again below the threshold for p.stored > p.config.Datacap { @@ -1677,8 +1693,8 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error p.updateStorageMetrics() // If we've just dropped the added transaction, it was clearly underpriced. - // We could also try to check for this earlier, but it is compex because - // of the rolling fee caculations. + // We could also try to check for this before adding to the tx pool, but it is + // complex because of database internals (RLP encoding, billy shelves). if !p.lookup.exists(tx.Hash()) { addUnderpricedMeter.Mark(1) return txpool.ErrUnderpriced @@ -1691,6 +1707,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error p.insertFeed.Send(core.NewTxsEvent{Txs: []*types.Transaction{tx.WithoutBlobTxSidecar()}}) //check the gapped queue for this account and try to promote + from, _ := types.Sender(p.signer, tx) // already validated in addLockedInternal/ValidateTransactionWithState if gtxs, ok := p.gapped[from]; checkGapped && ok && len(gtxs) > 0 { // We have to add in nonce order, but we want to stable sort to cater for situations // where transactions are replaced, keeping the original receive order for same nonce