From e4d258378aba1c10e0802fb109fa322108a58244 Mon Sep 17 00:00:00 2001
From: Daniel Liu <139250065@qq.com>
Date: Fri, 13 Mar 2026 15:21:06 +0800
Subject: [PATCH] feat(core/txpool,eth): align local tx tracking with geth
#31202 #31618
Implements ethereum/go-ethereum PR #31202 and #31618.
When local tracking is enabled:
- EthAPIBackend.SendTx tracks transactions after pool submission and keeps tracking temporary rejects so they can be retried by the local tracker.
- TxPool.AddLocal tracks accepted submissions and temporary rejects for local re-journal/re-submit flows, while preserving the original txpool error return to the caller.
This avoids persisting permanently invalid transactions while preserving retry signals for transient failures without masking submission outcomes in caller workflows.
Also included:
- classify temporary rejection reasons in core/txpool/locals
- expose SubPool.ValidateTxBasics and align LegacyPool implementation
- split low-tip rejection into ErrTxGasPriceTooLow
- simplify local tracker integration in txpool
- update txpool and eth tests for accepted vs retryable local tracking behavior
Refs: ethereum/go-ethereum#31202
Refs: ethereum/go-ethereum#31618
---
core/txpool/errors.go | 17 +-
core/txpool/legacypool/legacypool.go | 12 +-
core/txpool/legacypool/legacypool2_test.go | 6 +-
core/txpool/legacypool/legacypool_test.go | 59 +++---
core/txpool/locals/errors.go | 46 +++++
core/txpool/locals/tx_tracker.go | 5 +
core/txpool/subpool.go | 6 +
core/txpool/txpool.go | 22 +--
core/txpool/txpool_local_test.go | 169 ++++++++++++++---
core/txpool/validation.go | 4 +-
eth/api_backend.go | 24 ++-
eth/api_backend_test.go | 203 +++++++++++++++++++++
12 files changed, 489 insertions(+), 84 deletions(-)
create mode 100644 core/txpool/locals/errors.go
create mode 100644 eth/api_backend_test.go
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)
+ }
+}