diff --git a/core/txpool/errors.go b/core/txpool/errors.go
index 7b03d80e88fa..3f4c1161adc8 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,8 +28,9 @@ 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")
// ErrTxPoolOverflow is returned if the transaction pool is full and can't accept
@@ -38,6 +41,10 @@ var (
// 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")
@@ -73,5 +80,9 @@ var (
ErrSpecialTxCostOverflow = errors.New("special transaction cost overflow")
+ // ErrSpecialTxNotFromSigner is returned if a special transaction is submitted
+ // by an account that is not an authorized signer.
+ ErrSpecialTxNotFromSigner = errors.New("special transaction sender is not a signer")
+
ErrMinDeploySMC = errors.New("smart contract creation cost is under allowance")
)
diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go
index 99055a24cc35..5f3cdd700e42 100644
--- a/core/txpool/legacypool/legacypool.go
+++ b/core/txpool/legacypool/legacypool.go
@@ -15,6 +15,7 @@
// along with the go-ethereum library. If not, see .
// Package legacypool implements the normal EVM execution transaction pool.
+
package legacypool
import (
@@ -582,11 +583,11 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address]
return pending
}
-// validateTxBasics checks whether a transaction is valid according to the consensus
+// ValidateTxBasics checks whether a transaction is valid according to the consensus
// rules, but does not check state-dependent validation such as sufficient balance.
// This check is meant as an early check which only needs to be performed once,
// and does not require the pool mutex to be held.
-func (pool *LegacyPool) validateTxBasics(tx *types.Transaction) error {
+func (pool *LegacyPool) ValidateTxBasics(tx *types.Transaction) error {
opts := &txpool.ValidationOptions{
Config: pool.chainconfig,
Accept: 0 |
@@ -600,10 +601,7 @@ func (pool *LegacyPool) validateTxBasics(tx *types.Transaction) error {
return pool.isSigner != nil && !pool.isSigner(from)
},
}
- if err := txpool.ValidateTransaction(tx, pool.currentHead.Load(), pool.signer, opts); err != nil {
- return err
- }
- return nil
+ return txpool.ValidateTransaction(tx, pool.currentHead.Load(), pool.signer, opts)
}
// validateTx checks whether a transaction is valid according to the consensus
@@ -1070,7 +1068,7 @@ func (pool *LegacyPool) Add(txs []*types.Transaction, sync bool) []error {
// Exclude transactions with basic errors, e.g invalid signatures and
// insufficient intrinsic gas as soon as possible and cache senders
// in transactions before obtaining lock
- if err := pool.validateTxBasics(tx); err != nil {
+ if err := pool.ValidateTxBasics(tx); err != nil {
errs[i] = err
invalidTxMeter.Mark(1)
continue
diff --git a/core/txpool/legacypool/legacypool2_test.go b/core/txpool/legacypool/legacypool2_test.go
index 2f601fd3a735..c0f8b442cbee 100644
--- a/core/txpool/legacypool/legacypool2_test.go
+++ b/core/txpool/legacypool/legacypool2_test.go
@@ -81,12 +81,14 @@ func TestTransactionFutureAttack(t *testing.T) {
// Create the pool to test the limit enforcement with
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()))
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
@@ -179,7 +181,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 f06812331606..e4f35eaa9318 100644
--- a/core/txpool/legacypool/legacypool_test.go
+++ b/core/txpool/legacypool/legacypool_test.go
@@ -554,7 +554,7 @@ func TestInvalidTransactions(t *testing.T) {
// which is higher than MinGasPrice but lower than pool's gasTip (1 Gwei)
pool.gasTip.Store(uint256.NewInt(1000000000)) // Set pool gasTip to 1 Gwei (1000000000)
tx = pricedTransaction(1, 100000, big.NewInt(300000000), key)
- 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)
}
}
@@ -626,7 +626,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)
}
}
@@ -688,7 +688,7 @@ func TestValidateTransactionEIP2681(t *testing.T) {
GasPrice: tt.gasPrice,
})
signedTx, _ := types.SignTx(tx, types.HomesteadSigner{}, key)
- err := pool.validateTxBasics(signedTx)
+ err := pool.ValidateTxBasics(signedTx)
if tt.wantErr == nil && err != nil {
t.Errorf("expected nil, got %v", err)
@@ -707,7 +707,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)
}
}
@@ -722,12 +722,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)
}
}
@@ -1655,14 +1655,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(250000000), 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(250000000), 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(250000000), 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(250000000), 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(250000000), 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(250000000), 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)
@@ -1712,14 +1712,14 @@ func TestMinGasPriceEnforced(t *testing.T) {
tx := pricedTransaction(0, 100000, legacyPrice, key)
pool.SetGasTip(new(big.Int).Add(legacyPrice, big.NewInt(1)))
- if err := pool.Add([]*types.Transaction{tx}, false)[0]; !errors.Is(err, txpool.ErrUnderpriced) {
+ if err := pool.Add([]*types.Transaction{tx}, false)[0]; !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
t.Fatalf("Min tip not enforced")
}
tx = dynamicFeeTx(0, 100000, dynamicFeeCap, dynamicTip, key)
pool.SetGasTip(new(big.Int).Add(dynamicTip, big.NewInt(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")
}
}
@@ -1796,16 +1796,16 @@ func TestRepricingDynamicFee(t *testing.T) {
}
// Check that we can't add the old transactions back
tx := pricedTransaction(1, 100000, big.NewInt(300000000), 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(350000000), big.NewInt(300000000), 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(300000000), big.NewInt(300000000), 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)
@@ -2233,7 +2233,7 @@ func TestReplacement(t *testing.T) {
if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(250000000), key)); err != nil {
t.Fatalf("failed to add original cheap pending transaction: %v", err)
}
- if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(250000000), key)); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(250000000), 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(300000000), key)); err != nil {
@@ -2246,7 +2246,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 {
@@ -2260,7 +2260,7 @@ func TestReplacement(t *testing.T) {
if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(250000000), key)); err != nil {
t.Fatalf("failed to add original cheap queued transaction: %v", err)
}
- if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(250000000), key)); err != txpool.ErrReplaceUnderpriced {
+ if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(250000000), 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(300000000), key)); err != nil {
@@ -2270,7 +2270,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 {
@@ -2334,7 +2334,7 @@ func TestReplacementDynamicFee(t *testing.T) {
}
// 2. Don't bump tip or feecap => discard
tx = dynamicFeeTx(nonce, 100001, big.NewInt(300000000), big.NewInt(250000000), 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
@@ -2355,24 +2355,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 000000000000..de4d199490a2
--- /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/XinFinOrg/XDPoSChain/core/txpool"
+ "github.com/XinFinOrg/XDPoSChain/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 8cd33948bfc6..7a24ff2137ad 100644
--- a/core/txpool/locals/tx_tracker.go
+++ b/core/txpool/locals/tx_tracker.go
@@ -78,6 +78,11 @@ func (tracker *TxTracker) Track(tx *types.Transaction) {
tracker.TrackAll([]*types.Transaction{tx})
}
+// IsRetryableReject determines whether an add error is temporary and retryable.
+func (tracker *TxTracker) IsRetryableReject(err error) bool {
+ return IsTemporaryReject(err)
+}
+
// TrackAll adds a list of transactions to the tracked set.
func (tracker *TxTracker) TrackAll(txs []*types.Transaction) {
tracker.mu.Lock()
diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go
index a0c7ac169cbb..66e1c7c00405 100644
--- a/core/txpool/subpool.go
+++ b/core/txpool/subpool.go
@@ -114,6 +114,12 @@ type SubPool interface {
// Get returns a transaction if it is contained in the pool, or nil otherwise.
Get(hash common.Hash) *types.Transaction
+ // ValidateTxBasics checks whether a transaction is valid according to the consensus
+ // rules, but does not check state-dependent validation such as sufficient balance.
+ // This check is meant as a static check which can be performed without holding the
+ // pool mutex.
+ ValidateTxBasics(tx *types.Transaction) error
+
// 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/txpool.go b/core/txpool/txpool.go
index bfd44e0452d3..5f2cae60443b 100644
--- a/core/txpool/txpool.go
+++ b/core/txpool/txpool.go
@@ -69,7 +69,7 @@ type TxPool struct {
// TxPool local submission helpers.
type LocalTracker interface {
Track(tx *types.Transaction)
- TrackAll(txs []*types.Transaction)
+ IsRetryableReject(err error) bool
}
// New creates a new transaction pool to gather, sort and filter inbound
@@ -327,18 +327,18 @@ func (p *TxPool) SetLocalTracker(tracker LocalTracker) {
p.localTracker = tracker
}
-// AddLocals enqueues a batch of local transactions into the pool if they are
-// valid and tracks them for re-journal/re-submit flows.
-func (p *TxPool) AddLocals(txs []*types.Transaction, sync bool) []error {
+// AddLocal enqueues a single local transaction into the pool and return the
+// original error. The transaction will be tracked if it was accepted or
+// rejected for a temporary reason, allowing the local tracker to implement
+// re-journal and re-submit flows.
+func (p *TxPool) AddLocal(tx *types.Transaction, sync bool) error {
+ err := p.Add([]*types.Transaction{tx}, sync)[0]
if p.localTracker != nil {
- p.localTracker.TrackAll(txs)
+ if err == nil || p.localTracker.IsRetryableReject(err) {
+ p.localTracker.Track(tx)
+ }
}
- return p.Add(txs, sync)
-}
-
-// AddLocal enqueues a single local transaction into the pool if it is valid.
-func (p *TxPool) AddLocal(tx *types.Transaction, sync bool) error {
- return p.AddLocals([]*types.Transaction{tx}, sync)[0]
+ return err
}
// Pending retrieves all currently processable transactions, grouped by origin
diff --git a/core/txpool/txpool_local_test.go b/core/txpool/txpool_local_test.go
index bceec4b05fae..1613ccbf8afa 100644
--- a/core/txpool/txpool_local_test.go
+++ b/core/txpool/txpool_local_test.go
@@ -1,6 +1,7 @@
package txpool
import (
+ "errors"
"math/big"
"reflect"
"sync"
@@ -30,16 +31,14 @@ type testLocalTracker struct {
}
func (t *testLocalTracker) Track(tx *types.Transaction) {
- t.TrackAll([]*types.Transaction{tx})
-}
-
-func (t *testLocalTracker) TrackAll(txs []*types.Transaction) {
t.mu.Lock()
defer t.mu.Unlock()
*t.events = append(*t.events, "track")
- for _, tx := range txs {
- t.tracked = append(t.tracked, tx.Hash())
- }
+ t.tracked = append(t.tracked, tx.Hash())
+}
+
+func (t *testLocalTracker) IsRetryableReject(err error) bool {
+ return errors.Is(err, ErrUnderpriced)
}
type testSubPool struct {
@@ -47,6 +46,7 @@ type testSubPool struct {
lastAdd []*types.Transaction
lastSync bool
+ addErrs []error
}
func (s *testSubPool) Filter(tx *types.Transaction) bool { return true }
@@ -63,10 +63,17 @@ func (s *testSubPool) Has(hash common.Hash) bool { return false }
func (s *testSubPool) Get(hash common.Hash) *types.Transaction { return nil }
+func (s *testSubPool) ValidateTxBasics(tx *types.Transaction) error { return nil }
+
func (s *testSubPool) Add(txs []*types.Transaction, sync bool) []error {
*s.events = append(*s.events, "add")
s.lastAdd = txs
s.lastSync = sync
+ if len(s.addErrs) > 0 {
+ errs := make([]error, len(txs))
+ copy(errs, s.addErrs)
+ return errs
+ }
return make([]error, len(txs))
}
@@ -99,7 +106,7 @@ func (s *testSubPool) SetSigner(f func(address common.Address) bool) {}
func (s *testSubPool) IsSigner(addr common.Address) bool { return false }
-func TestAddLocalTracksBeforeAdd(t *testing.T) {
+func TestAddLocalTracksAfterAdd(t *testing.T) {
events := []string{}
tracker := &testLocalTracker{events: &events}
subpool := &testSubPool{events: &events}
@@ -126,12 +133,12 @@ func TestAddLocalTracksBeforeAdd(t *testing.T) {
if !subpool.lastSync {
t.Fatalf("sync flag not propagated to subpool Add")
}
- if !reflect.DeepEqual(events, []string{"track", "add"}) {
+ if !reflect.DeepEqual(events, []string{"add", "track"}) {
t.Fatalf("unexpected call order: have %v", events)
}
}
-func TestAddLocalsTracksBeforeAdd(t *testing.T) {
+func TestAddLocalMultipleTracksAfterAdd(t *testing.T) {
events := []string{}
tracker := &testLocalTracker{events: &events}
subpool := &testSubPool{events: &events}
@@ -146,16 +153,11 @@ func TestAddLocalsTracksBeforeAdd(t *testing.T) {
tx0 := types.NewTransaction(0, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
tx1 := types.NewTransaction(1, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
- txs := []*types.Transaction{tx0, tx1}
-
- errs := pool.AddLocals(txs, true)
- if len(errs) != len(txs) {
- t.Fatalf("unexpected error result length: have %d, want %d", len(errs), len(txs))
+ if err := pool.AddLocal(tx0, true); err != nil {
+ t.Fatalf("AddLocal tx0 failed: %v", err)
}
- for i, err := range errs {
- if err != nil {
- t.Fatalf("AddLocals error at index %d: %v", i, err)
- }
+ if err := pool.AddLocal(tx1, true); err != nil {
+ t.Fatalf("AddLocal tx1 failed: %v", err)
}
hashes := []common.Hash{tx0.Hash(), tx1.Hash()}
@@ -166,18 +168,131 @@ func TestAddLocalsTracksBeforeAdd(t *testing.T) {
t.Fatalf("tracker hashes mismatch: have %v, want %v", tracker.tracked, hashes)
}
- if len(subpool.lastAdd) != len(hashes) {
- t.Fatalf("subpool Add tx count mismatch: have %d, want %d", len(subpool.lastAdd), len(hashes))
- }
- for i, tx := range subpool.lastAdd {
- if tx.Hash() != hashes[i] {
- t.Fatalf("subpool Add hash mismatch at index %d", i)
- }
+ if len(subpool.lastAdd) != 1 || subpool.lastAdd[0].Hash() != tx1.Hash() {
+ t.Fatalf("subpool Add did not receive second local tx")
}
if !subpool.lastSync {
t.Fatalf("sync flag not propagated to subpool Add")
}
- if !reflect.DeepEqual(events, []string{"track", "add"}) {
+ if !reflect.DeepEqual(events, []string{"add", "track", "add", "track"}) {
+ t.Fatalf("unexpected call order: have %v", events)
+ }
+}
+
+func TestAddLocalMultipleTracksOnlyAcceptedTransactions(t *testing.T) {
+ events := []string{}
+ tracker := &testLocalTracker{events: &events}
+ subpool := &testSubPool{
+ events: &events,
+ addErrs: []error{ErrInvalidSender},
+ }
+
+ pool, err := New(0, testChain{}, []SubPool{subpool})
+ if err != nil {
+ t.Fatalf("failed to create txpool: %v", err)
+ }
+ defer pool.Close()
+
+ pool.SetLocalTracker(tracker)
+
+ tx0 := types.NewTransaction(0, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
+ tx1 := types.NewTransaction(1, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
+ if err := pool.AddLocal(tx0, true); !errors.Is(err, ErrInvalidSender) {
+ t.Fatalf("unexpected first error: have %v, want %v", err, ErrInvalidSender)
+ }
+ subpool.addErrs = nil
+ if err := pool.AddLocal(tx1, true); err != nil {
+ t.Fatalf("unexpected second error: %v", err)
+ }
+
+ hashes := []common.Hash{tx1.Hash()}
+ if !reflect.DeepEqual(tracker.tracked, hashes) {
+ t.Fatalf("tracker hashes mismatch: have %v, want %v", tracker.tracked, hashes)
+ }
+ if !reflect.DeepEqual(events, []string{"add", "add", "track"}) {
+ t.Fatalf("unexpected call order: have %v", events)
+ }
+}
+
+func TestAddLocalTracksOnlyAcceptedTransaction(t *testing.T) {
+ events := []string{}
+ tracker := &testLocalTracker{events: &events}
+ subpool := &testSubPool{
+ events: &events,
+ addErrs: []error{ErrInvalidSender},
+ }
+
+ pool, err := New(0, testChain{}, []SubPool{subpool})
+ if err != nil {
+ t.Fatalf("failed to create txpool: %v", err)
+ }
+ defer pool.Close()
+
+ pool.SetLocalTracker(tracker)
+
+ tx := types.NewTransaction(0, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
+ err = pool.AddLocal(tx, true)
+ if !errors.Is(err, ErrInvalidSender) {
+ t.Fatalf("unexpected error: have %v, want %v", err, ErrInvalidSender)
+ }
+
+ if len(tracker.tracked) != 0 {
+ t.Fatalf("tracker should not receive failed local tx, have %d tracked", len(tracker.tracked))
+ }
+ if !reflect.DeepEqual(events, []string{"add"}) {
+ t.Fatalf("unexpected call order: have %v", events)
+ }
+}
+
+func TestAddLocalTracksTemporaryRejectedTransaction(t *testing.T) {
+ events := []string{}
+ tracker := &testLocalTracker{events: &events}
+ subpool := &testSubPool{
+ events: &events,
+ addErrs: []error{ErrUnderpriced},
+ }
+
+ pool, err := New(0, testChain{}, []SubPool{subpool})
+ if err != nil {
+ t.Fatalf("failed to create txpool: %v", err)
+ }
+ defer pool.Close()
+
+ pool.SetLocalTracker(tracker)
+
+ tx := types.NewTransaction(0, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
+ err = pool.AddLocal(tx, true)
+ if !errors.Is(err, ErrUnderpriced) {
+ t.Fatalf("unexpected error: have %v, want %v", err, ErrUnderpriced)
+ }
+
+ if !reflect.DeepEqual(tracker.tracked, []common.Hash{tx.Hash()}) {
+ t.Fatalf("tracker should receive temporary rejected local tx")
+ }
+ if !reflect.DeepEqual(events, []string{"add", "track"}) {
+ t.Fatalf("unexpected call order: have %v", events)
+ }
+}
+
+func TestAddLocalTemporaryRejectWithoutTrackerReturnsError(t *testing.T) {
+ events := []string{}
+ subpool := &testSubPool{
+ events: &events,
+ addErrs: []error{ErrUnderpriced},
+ }
+
+ pool, err := New(0, testChain{}, []SubPool{subpool})
+ if err != nil {
+ t.Fatalf("failed to create txpool: %v", err)
+ }
+ defer pool.Close()
+
+ tx := types.NewTransaction(0, common.Address{0x1}, big.NewInt(1), 21000, big.NewInt(1), nil)
+ err = pool.AddLocal(tx, true)
+ if !errors.Is(err, ErrUnderpriced) {
+ t.Fatalf("unexpected error: have %v, want %v", err, ErrUnderpriced)
+ }
+ if !reflect.DeepEqual(events, []string{"add"}) {
t.Fatalf("unexpected call order: have %v", events)
}
}
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index ae10835e4340..4fb7b598e734 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -100,7 +100,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
// Skip further validation for special transactions
if tx.IsSpecialTransaction() {
if opts.NotSigner(from) {
- return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrUnderpriced, tx.GasTipCap(), opts.MinTip)
+ return fmt.Errorf("%w: %s", ErrSpecialTxNotFromSigner, from.Hex())
}
return nil
}
@@ -129,7 +129,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
}
// Ensure the gas price 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.SetCodeTxType {
if len(tx.SetCodeAuthorizations()) == 0 {
diff --git a/eth/api_backend.go b/eth/api_backend.go
index e703f9f64b8e..748e6c6542f6 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -40,6 +40,7 @@ import (
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
"github.com/XinFinOrg/XDPoSChain/core/state"
"github.com/XinFinOrg/XDPoSChain/core/txpool"
+ "github.com/XinFinOrg/XDPoSChain/core/txpool/locals"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/core/vm"
"github.com/XinFinOrg/XDPoSChain/eth/downloader"
@@ -301,10 +302,25 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri
}
func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
- if locals := b.eth.localTxTracker; locals != nil {
- locals.Track(signedTx)
- }
- return b.eth.txPool.Add([]*types.Transaction{signedTx}, false)[0]
+ err := b.eth.txPool.Add([]*types.Transaction{signedTx}, false)[0]
+
+ // 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
}
func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) {
diff --git a/eth/api_backend_test.go b/eth/api_backend_test.go
new file mode 100644
index 000000000000..3c0b32932ea1
--- /dev/null
+++ b/eth/api_backend_test.go
@@ -0,0 +1,203 @@
+// 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"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/XinFinOrg/XDPoSChain/common"
+ "github.com/XinFinOrg/XDPoSChain/consensus/ethash"
+ "github.com/XinFinOrg/XDPoSChain/core"
+ "github.com/XinFinOrg/XDPoSChain/core/rawdb"
+ "github.com/XinFinOrg/XDPoSChain/core/txpool"
+ "github.com/XinFinOrg/XDPoSChain/core/txpool/legacypool"
+ "github.com/XinFinOrg/XDPoSChain/core/txpool/locals"
+ "github.com/XinFinOrg/XDPoSChain/core/types"
+ "github.com/XinFinOrg/XDPoSChain/core/vm"
+ "github.com/XinFinOrg/XDPoSChain/crypto"
+ "github.com/XinFinOrg/XDPoSChain/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(t *testing.T, withLocal bool) *EthAPIBackend {
+ t.Helper()
+
+ var (
+ // Create a database pre-initialize with a genesis block
+ db = rawdb.NewMemoryDatabase()
+ engine = ethash.NewFaker()
+ )
+ chain, err := core.NewBlockChain(db, nil, gspec, engine, vm.Config{})
+ if err != nil {
+ t.Fatalf("failed to create blockchain: %v", err)
+ }
+
+ txconfig := legacypool.DefaultConfig
+ txconfig.Journal = "" // Don't litter the disk with test journals
+
+ legacyPool := legacypool.New(txconfig, chain)
+ txpool, err := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{legacyPool})
+ if err != nil {
+ // Ensure we don't leak the blockchain goroutines if txpool creation fails.
+ chain.Stop()
+ t.Fatalf("failed to create txpool: %v", err)
+ }
+
+ eth := &Ethereum{
+ blockchain: chain,
+ txPool: txpool,
+ }
+ if withLocal {
+ eth.localTxTracker = locals.New("", time.Minute, gspec.Config, txpool)
+ }
+ t.Cleanup(func() {
+ if eth.localTxTracker != nil {
+ if err := eth.localTxTracker.Stop(); err != nil {
+ t.Errorf("failed to stop local tx tracker: %v", err)
+ }
+ }
+ if err := txpool.Close(); err != nil {
+ t.Errorf("failed to close txpool: %v", err)
+ }
+ chain.Stop()
+ })
+
+ 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(t, withLocal)
+
+ txA := pricedSetCodeTx(0, 250000, uint256.NewInt(params.GWei), uint256.NewInt(params.GWei), key, []unsignedAuth{{nonce: 0, key: key}})
+ if err := b.SendTx(context.Background(), txA); err != nil {
+ t.Fatalf("Failed to submit tx: %v", err)
+ }
+ for {
+ pending, _ := b.TxPool().ContentFrom(address)
+ if len(pending) == 1 {
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ 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)
+ }
+ }
+}
+
+func TestSendTxWithLocalPermanentErrorNotTracked(t *testing.T) {
+ b := initBackend(t, true)
+ if b.eth.localTxTracker == nil {
+ t.Fatal("expected local tx tracker to be configured")
+ }
+ // Force txpool min tip above tx gas price so submission fails permanently.
+ if err := b.TxPool().SetGasTip(big.NewInt(params.GWei + 1)); err != nil {
+ t.Fatalf("failed to set gas tip: %v", err)
+ }
+
+ tx := makeTx(0, big.NewInt(params.GWei), nil, key)
+ err := b.SendTx(context.Background(), tx)
+ if !errors.Is(err, txpool.ErrTxGasPriceTooLow) {
+ t.Fatalf("unexpected error, want: %v, got: %v", txpool.ErrTxGasPriceTooLow, err)
+ }
+
+ tracked := reflect.ValueOf(b.eth.localTxTracker).Elem().FieldByName("all").Len()
+ if tracked != 0 {
+ t.Fatalf("unexpected tracked tx count: have %d, want 0", tracked)
+ }
+}