diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go
index 12a4133b40c..e506da228d9 100644
--- a/core/txpool/blobpool/blobpool.go
+++ b/core/txpool/blobpool/blobpool.go
@@ -1391,6 +1391,8 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) {
switch {
case errors.Is(err, txpool.ErrUnderpriced):
addUnderpricedMeter.Mark(1)
+ case errors.Is(err, txpool.ErrTxGasPriceTooLow):
+ addUnderpricedMeter.Mark(1)
case errors.Is(err, core.ErrNonceTooLow):
addStaleMeter.Mark(1)
case errors.Is(err, core.ErrNonceTooHigh):
diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go
index 76d21a0c9e0..0a323179a6a 100644
--- a/core/txpool/blobpool/blobpool_test.go
+++ b/core/txpool/blobpool/blobpool_test.go
@@ -1484,7 +1484,7 @@ func TestAdd(t *testing.T) {
{ // New account, no previous txs, nonce 0, but blob fee cap too low
from: "alice",
tx: makeUnsignedTx(0, 1, 1, 0),
- err: txpool.ErrUnderpriced,
+ err: txpool.ErrTxGasPriceTooLow,
},
{ // Same as above but blob fee cap equals minimum, should be accepted
from: "alice",
diff --git a/core/txpool/errors.go b/core/txpool/errors.go
index 02f5703b6ca..968c9d95423 100644
--- a/core/txpool/errors.go
+++ b/core/txpool/errors.go
@@ -16,7 +16,9 @@
package txpool
-import "errors"
+import (
+ "errors"
+)
var (
// ErrAlreadyKnown is returned if the transactions is already contained
@@ -26,14 +28,19 @@ var (
// ErrInvalidSender is returned if the transaction contains an invalid signature.
ErrInvalidSender = errors.New("invalid sender")
- // ErrUnderpriced is returned if a transaction's gas price is below the minimum
- // configured for the transaction pool.
+ // ErrUnderpriced is returned if a transaction's gas price is too low to be
+ // included in the pool. If the gas price is lower than the minimum configured
+ // one for the transaction pool, use ErrTxGasPriceTooLow instead.
ErrUnderpriced = errors.New("transaction underpriced")
// ErrReplaceUnderpriced is returned if a transaction is attempted to be replaced
// with a different one without the required price bump.
ErrReplaceUnderpriced = errors.New("replacement transaction underpriced")
+ // ErrTxGasPriceTooLow is returned if a transaction's gas price is below the
+ // minimum configured for the transaction pool.
+ ErrTxGasPriceTooLow = errors.New("transaction gas price below minimum")
+
// ErrAccountLimitExceeded is returned if a transaction would exceed the number
// allowed by a pool for a single account.
ErrAccountLimitExceeded = errors.New("account limit exceeded")
diff --git a/core/txpool/legacypool/legacypool2_test.go b/core/txpool/legacypool/legacypool2_test.go
index 3f210e3d1b9..deb06aa6178 100644
--- a/core/txpool/legacypool/legacypool2_test.go
+++ b/core/txpool/legacypool/legacypool2_test.go
@@ -82,12 +82,14 @@ func TestTransactionFutureAttack(t *testing.T) {
// Create the pool to test the limit enforcement with
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
blockchain := newTestBlockChain(eip1559Config, 1000000, statedb, new(event.Feed))
+
config := testTxPoolConfig
config.GlobalQueue = 100
config.GlobalSlots = 100
pool := New(config, blockchain)
pool.Init(config.PriceLimit, blockchain.CurrentBlock(), newReserver())
defer pool.Close()
+
fillPool(t, pool)
pending, _ := pool.Stats()
// Now, future transaction attack starts, let's add a bunch of expensive non-executables, and see if the pending-count drops
@@ -180,7 +182,9 @@ func TestTransactionZAttack(t *testing.T) {
ivPending := countInvalidPending()
t.Logf("invalid pending: %d\n", ivPending)
- // Now, DETER-Z attack starts, let's add a bunch of expensive non-executables (from N accounts) along with balance-overdraft txs (from one account), and see if the pending-count drops
+ // Now, DETER-Z attack starts, let's add a bunch of expensive non-executables
+ // (from N accounts) along with balance-overdraft txs (from one account), and
+ // see if the pending-count drops
for j := 0; j < int(pool.config.GlobalQueue); j++ {
futureTxs := types.Transactions{}
key, _ := crypto.GenerateKey()
diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go
index bb1323a7d1f..2fdf8903203 100644
--- a/core/txpool/legacypool/legacypool_test.go
+++ b/core/txpool/legacypool/legacypool_test.go
@@ -413,7 +413,7 @@ func TestInvalidTransactions(t *testing.T) {
tx = transaction(1, 100000, key)
pool.gasTip.Store(uint256.NewInt(1000))
- if err, want := pool.addRemote(tx), txpool.ErrUnderpriced; !errors.Is(err, want) {
+ if err, want := pool.addRemote(tx), txpool.ErrTxGasPriceTooLow; !errors.Is(err, want) {
t.Errorf("want %v have %v", want, err)
}
}
@@ -484,7 +484,7 @@ func TestNegativeValue(t *testing.T) {
tx, _ := types.SignTx(types.NewTransaction(0, common.Address{}, big.NewInt(-1), 100, big.NewInt(1), nil), types.HomesteadSigner{}, key)
from, _ := deriveSender(tx)
testAddBalance(pool, from, big.NewInt(1))
- if err := pool.addRemote(tx); err != txpool.ErrNegativeValue {
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrNegativeValue) {
t.Error("expected", txpool.ErrNegativeValue, "got", err)
}
}
@@ -497,7 +497,7 @@ func TestTipAboveFeeCap(t *testing.T) {
tx := dynamicFeeTx(0, 100, big.NewInt(1), big.NewInt(2), key)
- if err := pool.addRemote(tx); err != core.ErrTipAboveFeeCap {
+ if err := pool.addRemote(tx); !errors.Is(err, core.ErrTipAboveFeeCap) {
t.Error("expected", core.ErrTipAboveFeeCap, "got", err)
}
}
@@ -512,12 +512,12 @@ func TestVeryHighValues(t *testing.T) {
veryBigNumber.Lsh(veryBigNumber, 300)
tx := dynamicFeeTx(0, 100, big.NewInt(1), veryBigNumber, key)
- if err := pool.addRemote(tx); err != core.ErrTipVeryHigh {
+ if err := pool.addRemote(tx); !errors.Is(err, core.ErrTipVeryHigh) {
t.Error("expected", core.ErrTipVeryHigh, "got", err)
}
tx2 := dynamicFeeTx(0, 100, veryBigNumber, big.NewInt(1), key)
- if err := pool.addRemote(tx2); err != core.ErrFeeCapVeryHigh {
+ if err := pool.addRemote(tx2); !errors.Is(err, core.ErrFeeCapVeryHigh) {
t.Error("expected", core.ErrFeeCapVeryHigh, "got", err)
}
}
@@ -1424,14 +1424,14 @@ func TestRepricing(t *testing.T) {
t.Fatalf("pool internal state corrupted: %v", err)
}
// Check that we can't add the old transactions back
- if err := pool.addRemote(pricedTransaction(1, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrUnderpriced) {
- t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced)
+ if err := pool.addRemote(pricedTransaction(1, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
}
- if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); !errors.Is(err, txpool.ErrUnderpriced) {
- t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced)
+ if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
}
- if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrUnderpriced) {
- t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced)
+ if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
}
if err := validateEvents(events, 0); err != nil {
t.Fatalf("post-reprice event firing failed: %v", err)
@@ -1476,14 +1476,14 @@ func TestMinGasPriceEnforced(t *testing.T) {
tx := pricedTransaction(0, 100000, big.NewInt(2), key)
pool.SetGasTip(big.NewInt(tx.GasPrice().Int64() + 1))
- if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrUnderpriced) {
+ if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
t.Fatalf("Min tip not enforced")
}
tx = dynamicFeeTx(0, 100000, big.NewInt(3), big.NewInt(2), key)
pool.SetGasTip(big.NewInt(tx.GasTipCap().Int64() + 1))
- if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrUnderpriced) {
+ if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
t.Fatalf("Min tip not enforced")
}
}
@@ -1560,16 +1560,16 @@ func TestRepricingDynamicFee(t *testing.T) {
}
// Check that we can't add the old transactions back
tx := pricedTransaction(1, 100000, big.NewInt(1), keys[0])
- if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrUnderpriced) {
- t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced)
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
}
tx = dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[1])
- if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrUnderpriced) {
- t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced)
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
}
tx = dynamicFeeTx(2, 100000, big.NewInt(1), big.NewInt(1), keys[2])
- if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrUnderpriced) {
- t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced)
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
}
if err := validateEvents(events, 0); err != nil {
t.Fatalf("post-reprice event firing failed: %v", err)
@@ -1673,7 +1673,7 @@ func TestUnderpricing(t *testing.T) {
t.Fatalf("failed to add well priced transaction: %v", err)
}
// Ensure that replacing a pending transaction with a future transaction fails
- if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); err != ErrFutureReplacePending {
+ if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); !errors.Is(err, ErrFutureReplacePending) {
t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, ErrFutureReplacePending)
}
pending, queued = pool.Stats()
@@ -1995,7 +1995,7 @@ func TestReplacement(t *testing.T) {
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), key)); err != nil {
t.Fatalf("failed to add original cheap pending transaction: %v", err)
}
- if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(1), key)); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original cheap pending transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced)
}
if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(2), key)); err != nil {
@@ -2008,7 +2008,7 @@ func TestReplacement(t *testing.T) {
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(price), key)); err != nil {
t.Fatalf("failed to add original proper pending transaction: %v", err)
}
- if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(threshold-1), key)); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(threshold-1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original proper pending transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced)
}
if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(threshold), key)); err != nil {
@@ -2022,7 +2022,7 @@ func TestReplacement(t *testing.T) {
if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), key)); err != nil {
t.Fatalf("failed to add original cheap queued transaction: %v", err)
}
- if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(1), key)); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original cheap queued transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced)
}
if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(2), key)); err != nil {
@@ -2032,7 +2032,7 @@ func TestReplacement(t *testing.T) {
if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(price), key)); err != nil {
t.Fatalf("failed to add original proper queued transaction: %v", err)
}
- if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(threshold-1), key)); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(threshold-1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original proper queued transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced)
}
if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(threshold), key)); err != nil {
@@ -2096,7 +2096,7 @@ func TestReplacementDynamicFee(t *testing.T) {
}
// 2. Don't bump tip or feecap => discard
tx = dynamicFeeTx(nonce, 100001, big.NewInt(2), big.NewInt(1), key)
- if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original cheap %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
}
// 3. Bump both more than min => accept
@@ -2117,24 +2117,25 @@ func TestReplacementDynamicFee(t *testing.T) {
if err := pool.addRemoteSync(tx); err != nil {
t.Fatalf("failed to add original proper %s transaction: %v", stage, err)
}
+
// 6. Bump tip max allowed so it's still underpriced => discard
tx = dynamicFeeTx(nonce, 100000, big.NewInt(gasFeeCap), big.NewInt(tipThreshold-1), key)
- if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
}
// 7. Bump fee cap max allowed so it's still underpriced => discard
tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold-1), big.NewInt(gasTipCap), key)
- if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
}
// 8. Bump tip min for acceptance => accept
tx = dynamicFeeTx(nonce, 100000, big.NewInt(gasFeeCap), big.NewInt(tipThreshold), key)
- if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
}
// 9. Bump fee cap min for acceptance => accept
tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold), big.NewInt(gasTipCap), key)
- if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) {
t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
}
// 10. Check events match expected (3 new executable txs during pending, 0 during queue)
diff --git a/core/txpool/locals/errors.go b/core/txpool/locals/errors.go
new file mode 100644
index 00000000000..fda50bf2181
--- /dev/null
+++ b/core/txpool/locals/errors.go
@@ -0,0 +1,46 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package locals
+
+import (
+ "errors"
+
+ "github.com/ethereum/go-ethereum/core/txpool"
+ "github.com/ethereum/go-ethereum/core/txpool/legacypool"
+)
+
+// IsTemporaryReject determines whether the given error indicates a temporary
+// reason to reject a transaction from being included in the txpool. The result
+// may change if the txpool's state changes later.
+func IsTemporaryReject(err error) bool {
+ switch {
+ case errors.Is(err, legacypool.ErrOutOfOrderTxFromDelegated):
+ return true
+ case errors.Is(err, txpool.ErrInflightTxLimitReached):
+ return true
+ case errors.Is(err, legacypool.ErrAuthorityReserved):
+ return true
+ case errors.Is(err, txpool.ErrUnderpriced):
+ return true
+ case errors.Is(err, legacypool.ErrTxPoolOverflow):
+ return true
+ case errors.Is(err, legacypool.ErrFutureReplacePending):
+ return true
+ default:
+ return false
+ }
+}
diff --git a/core/txpool/locals/tx_tracker.go b/core/txpool/locals/tx_tracker.go
index eccdcf422ad..e08384ce711 100644
--- a/core/txpool/locals/tx_tracker.go
+++ b/core/txpool/locals/tx_tracker.go
@@ -74,32 +74,22 @@ func New(journalPath string, journalTime time.Duration, chainConfig *params.Chai
// Track adds a transaction to the tracked set.
// Note: blob-type transactions are ignored.
-func (tracker *TxTracker) Track(tx *types.Transaction) error {
- return tracker.TrackAll([]*types.Transaction{tx})[0]
+func (tracker *TxTracker) Track(tx *types.Transaction) {
+ tracker.TrackAll([]*types.Transaction{tx})
}
// TrackAll adds a list of transactions to the tracked set.
// Note: blob-type transactions are ignored.
-func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error {
+func (tracker *TxTracker) TrackAll(txs []*types.Transaction) {
tracker.mu.Lock()
defer tracker.mu.Unlock()
- var errors []error
for _, tx := range txs {
if tx.Type() == types.BlobTxType {
- errors = append(errors, nil)
- continue
- }
- // Ignore the transactions which are failed for fundamental
- // validation such as invalid parameters.
- if err := tracker.pool.ValidateTxBasics(tx); err != nil {
- log.Debug("Invalid transaction submitted", "hash", tx.Hash(), "err", err)
- errors = append(errors, err)
continue
}
// If we're already tracking it, it's a no-op
if _, ok := tracker.all[tx.Hash()]; ok {
- errors = append(errors, nil)
continue
}
// Theoretically, checking the error here is unnecessary since sender recovery
@@ -108,11 +98,8 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error {
// Therefore, the error is still checked just in case.
addr, err := types.Sender(tracker.signer, tx)
if err != nil {
- errors = append(errors, err)
continue
}
- errors = append(errors, nil)
-
tracker.all[tx.Hash()] = tx
if tracker.byAddr[addr] == nil {
tracker.byAddr[addr] = legacypool.NewSortedMap()
@@ -124,7 +111,6 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error {
}
}
localGauge.Update(int64(len(tracker.all)))
- return errors
}
// recheck checks and returns any transactions that needs to be resubmitted.
diff --git a/core/txpool/locals/tx_tracker_test.go b/core/txpool/locals/tx_tracker_test.go
index 5585589b6cd..0668d243fcd 100644
--- a/core/txpool/locals/tx_tracker_test.go
+++ b/core/txpool/locals/tx_tracker_test.go
@@ -17,7 +17,6 @@
package locals
import (
- "errors"
"math/big"
"testing"
"time"
@@ -91,10 +90,12 @@ func (env *testEnv) close() {
env.chain.Stop()
}
+// nolint:unused
func (env *testEnv) setGasTip(gasTip uint64) {
env.pool.SetGasTip(new(big.Int).SetUint64(gasTip))
}
+// nolint:unused
func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction {
if nonce == 0 {
head := env.chain.CurrentHeader()
@@ -121,6 +122,7 @@ func (env *testEnv) makeTxs(n int) []*types.Transaction {
return txs
}
+// nolint:unused
func (env *testEnv) commit() {
head := env.chain.CurrentBlock()
block := env.chain.GetBlock(head.Hash(), head.Number.Uint64())
@@ -137,60 +139,6 @@ func (env *testEnv) commit() {
}
}
-func TestRejectInvalids(t *testing.T) {
- env := newTestEnv(t, 10, 0, "")
- defer env.close()
-
- var cases = []struct {
- gasTip uint64
- tx *types.Transaction
- expErr error
- commit bool
- }{
- {
- tx: env.makeTx(5, nil), // stale
- expErr: core.ErrNonceTooLow,
- },
- {
- tx: env.makeTx(11, nil), // future transaction
- expErr: nil,
- },
- {
- gasTip: params.GWei,
- tx: env.makeTx(0, new(big.Int).SetUint64(params.GWei/2)), // low price
- expErr: txpool.ErrUnderpriced,
- },
- {
- tx: types.NewTransaction(10, common.Address{0x00}, big.NewInt(1000), params.TxGas, big.NewInt(params.GWei), nil), // invalid signature
- expErr: types.ErrInvalidSig,
- },
- {
- commit: true,
- tx: env.makeTx(10, nil), // stale
- expErr: core.ErrNonceTooLow,
- },
- {
- tx: env.makeTx(11, nil),
- expErr: nil,
- },
- }
- for i, c := range cases {
- if c.gasTip != 0 {
- env.setGasTip(c.gasTip)
- }
- if c.commit {
- env.commit()
- }
- gotErr := env.tracker.Track(c.tx)
- if c.expErr == nil && gotErr != nil {
- t.Fatalf("%d, unexpected error: %v", i, gotErr)
- }
- if c.expErr != nil && !errors.Is(gotErr, c.expErr) {
- t.Fatalf("%d, unexpected error, want: %v, got: %v", i, c.expErr, gotErr)
- }
- }
-}
-
func TestResubmit(t *testing.T) {
env := newTestEnv(t, 10, 0, "")
defer env.close()
diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go
index 2ed38772ce8..6608024952c 100644
--- a/core/txpool/txpool.go
+++ b/core/txpool/txpool.go
@@ -322,31 +322,6 @@ func (p *TxPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Pr
return nil, nil
}
-// ValidateTxBasics checks whether a transaction is valid according to the consensus
-// rules, but does not check state-dependent validation such as sufficient balance.
-func (p *TxPool) ValidateTxBasics(tx *types.Transaction) error {
- addr, err := types.Sender(p.signer, tx)
- if err != nil {
- return err
- }
- // Reject transactions with stale nonce. Gapped-nonce future transactions
- // are considered valid and will be handled by the subpool according to its
- // internal policy.
- p.stateLock.RLock()
- nonce := p.state.GetNonce(addr)
- p.stateLock.RUnlock()
-
- if nonce > tx.Nonce() {
- return core.ErrNonceTooLow
- }
- for _, subpool := range p.subpools {
- if subpool.Filter(tx) {
- return subpool.ValidateTxBasics(tx)
- }
- }
- return fmt.Errorf("%w: received type %d", core.ErrTxTypeNotSupported, tx.Type())
-}
-
// Add enqueues a batch of transactions into the pool if they are valid. Due
// to the large transaction churn, add may postpone fully integrating the tx
// to a later point to batch multiple ones together.
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index 8747724247f..e370f2ce84f 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -131,12 +131,12 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the gasprice is high enough to cover the requirement of the calling pool
if tx.GasTipCapIntCmp(opts.MinTip) < 0 {
- return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrUnderpriced, tx.GasTipCap(), opts.MinTip)
+ return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip)
}
if tx.Type() == types.BlobTxType {
// Ensure the blob fee cap satisfies the minimum blob gas price
if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 {
- return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrUnderpriced, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice)
+ return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice)
}
sidecar := tx.BlobTxSidecar()
if sidecar == nil {
diff --git a/eth/api_backend.go b/eth/api_backend.go
index b39dd4cbdb2..ab0c9e9788c 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/txpool"
+ "github.com/ethereum/go-ethereum/core/txpool/locals"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/ethconfig"
@@ -307,19 +308,24 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri
}
func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
- locals := b.eth.localTxTracker
- if locals != nil {
- if err := locals.Track(signedTx); err != nil {
- return err
- }
- }
- // No error will be returned to user if the transaction fails stateful
- // validation (e.g., no available slot), as the locally submitted transactions
- // may be resubmitted later via the local tracker.
err := b.eth.txPool.Add([]*types.Transaction{signedTx}, false)[0]
- if err != nil && locals == nil {
+
+ // If the local transaction tracker is not configured, returns whatever
+ // returned from the txpool.
+ if b.eth.localTxTracker == nil {
+ return err
+ }
+ // If the transaction fails with an error indicating it is invalid, or if there is
+ // very little chance it will be accepted later (e.g., the gas price is below the
+ // configured minimum, or the sender has insufficient funds to cover the cost),
+ // propagate the error to the user.
+ if err != nil && !locals.IsTemporaryReject(err) {
return err
}
+ // No error will be returned to user if the transaction fails with a temporary
+ // error and might be accepted later (e.g., the transaction pool is full).
+ // Locally submitted transactions will be resubmitted later via the local tracker.
+ b.eth.localTxTracker.Track(signedTx)
return nil
}
diff --git a/eth/api_backend_test.go b/eth/api_backend_test.go
new file mode 100644
index 00000000000..049f68d8273
--- /dev/null
+++ b/eth/api_backend_test.go
@@ -0,0 +1,157 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package eth
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "errors"
+ "math/big"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/txpool"
+ "github.com/ethereum/go-ethereum/core/txpool/blobpool"
+ "github.com/ethereum/go-ethereum/core/txpool/legacypool"
+ "github.com/ethereum/go-ethereum/core/txpool/locals"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+var (
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ address = crypto.PubkeyToAddress(key.PublicKey)
+ funds = big.NewInt(1000_000_000_000_000)
+ gspec = &core.Genesis{
+ Config: params.MergedTestChainConfig,
+ Alloc: types.GenesisAlloc{
+ address: {Balance: funds},
+ },
+ Difficulty: common.Big0,
+ BaseFee: big.NewInt(params.InitialBaseFee),
+ }
+ signer = types.LatestSignerForChainID(gspec.Config.ChainID)
+)
+
+func initBackend(withLocal bool) *EthAPIBackend {
+ var (
+ // Create a database pre-initialize with a genesis block
+ db = rawdb.NewMemoryDatabase()
+ engine = beacon.New(ethash.NewFaker())
+ )
+ chain, _ := core.NewBlockChain(db, nil, gspec, nil, engine, vm.Config{}, nil)
+
+ txconfig := legacypool.DefaultConfig
+ txconfig.Journal = "" // Don't litter the disk with test journals
+
+ blobPool := blobpool.New(blobpool.Config{Datadir: ""}, chain, nil)
+ legacyPool := legacypool.New(txconfig, chain)
+ txpool, _ := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{legacyPool, blobPool})
+
+ eth := &Ethereum{
+ blockchain: chain,
+ txPool: txpool,
+ }
+ if withLocal {
+ eth.localTxTracker = locals.New("", time.Minute, gspec.Config, txpool)
+ }
+ return &EthAPIBackend{
+ eth: eth,
+ }
+}
+
+func makeTx(nonce uint64, gasPrice *big.Int, amount *big.Int, key *ecdsa.PrivateKey) *types.Transaction {
+ if gasPrice == nil {
+ gasPrice = big.NewInt(params.GWei)
+ }
+ if amount == nil {
+ amount = big.NewInt(1000)
+ }
+ tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x00}, amount, params.TxGas, gasPrice, nil), signer, key)
+ return tx
+}
+
+type unsignedAuth struct {
+ nonce uint64
+ key *ecdsa.PrivateKey
+}
+
+func pricedSetCodeTx(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, key *ecdsa.PrivateKey, unsigned []unsignedAuth) *types.Transaction {
+ var authList []types.SetCodeAuthorization
+ for _, u := range unsigned {
+ auth, _ := types.SignSetCode(u.key, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(gspec.Config.ChainID),
+ Address: common.Address{0x42},
+ Nonce: u.nonce,
+ })
+ authList = append(authList, auth)
+ }
+ return pricedSetCodeTxWithAuth(nonce, gaslimit, gasFee, tip, key, authList)
+}
+
+func pricedSetCodeTxWithAuth(nonce uint64, gaslimit uint64, gasFee, tip *uint256.Int, key *ecdsa.PrivateKey, authList []types.SetCodeAuthorization) *types.Transaction {
+ return types.MustSignNewTx(key, signer, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(gspec.Config.ChainID),
+ Nonce: nonce,
+ GasTipCap: tip,
+ GasFeeCap: gasFee,
+ Gas: gaslimit,
+ To: common.Address{},
+ Value: uint256.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ AuthList: authList,
+ })
+}
+
+func TestSendTx(t *testing.T) {
+ testSendTx(t, false)
+ testSendTx(t, true)
+}
+
+func testSendTx(t *testing.T, withLocal bool) {
+ b := initBackend(withLocal)
+
+ txA := pricedSetCodeTx(0, 250000, uint256.NewInt(params.GWei), uint256.NewInt(params.GWei), key, []unsignedAuth{
+ {
+ nonce: 0,
+ key: key,
+ },
+ })
+ b.SendTx(context.Background(), txA)
+
+ txB := makeTx(1, nil, nil, key)
+ err := b.SendTx(context.Background(), txB)
+
+ if withLocal {
+ if err != nil {
+ t.Fatalf("Unexpected error sending tx: %v", err)
+ }
+ } else {
+ if !errors.Is(err, txpool.ErrInflightTxLimitReached) {
+ t.Fatalf("Unexpected error, want: %v, got: %v", txpool.ErrInflightTxLimitReached, err)
+ }
+ }
+}
diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go
index 1c192d41122..ff17ae4945a 100644
--- a/eth/fetcher/tx_fetcher.go
+++ b/eth/fetcher/tx_fetcher.go
@@ -345,7 +345,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
// Track the transaction hash if the price is too low for us.
// Avoid re-request this transaction when we receive another
// announcement.
- if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) {
+ if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow) {
f.underpriced.Add(batch[j].Hash(), batch[j].Time())
}
// Track a few interesting failure types
@@ -355,7 +355,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
case errors.Is(err, txpool.ErrAlreadyKnown):
duplicate++
- case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced):
+ case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow):
underpriced++
default:
diff --git a/eth/fetcher/tx_fetcher_test.go b/eth/fetcher/tx_fetcher_test.go
index 7f3080f5f6f..c4c8cac56e0 100644
--- a/eth/fetcher/tx_fetcher_test.go
+++ b/eth/fetcher/tx_fetcher_test.go
@@ -1244,10 +1244,12 @@ func TestTransactionFetcherUnderpricedDedup(t *testing.T) {
func(txs []*types.Transaction) []error {
errs := make([]error, len(txs))
for i := 0; i < len(errs); i++ {
- if i%2 == 0 {
+ if i%3 == 0 {
errs[i] = txpool.ErrUnderpriced
- } else {
+ } else if i%3 == 1 {
errs[i] = txpool.ErrReplaceUnderpriced
+ } else {
+ errs[i] = txpool.ErrTxGasPriceTooLow
}
}
return errs
diff --git a/ethclient/simulated/backend_test.go b/ethclient/simulated/backend_test.go
index fc78e843627..303e480a098 100644
--- a/ethclient/simulated/backend_test.go
+++ b/ethclient/simulated/backend_test.go
@@ -25,16 +25,14 @@ import (
"testing"
"time"
- "go.uber.org/goleak"
-
- "github.com/ethereum/go-ethereum/crypto/kzg4844"
- "github.com/holiman/uint256"
-
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+ "go.uber.org/goleak"
)
var _ bind.ContractBackend = (Client)(nil)