mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-02-26 07:37:20 +00:00
eth: add tx to locals only if it has a chance of acceptance (#31618)
This pull request improves error handling for local transaction submissions. Specifically, if a transaction fails with a temporary error but might be accepted later, the error will not be returned to the user; instead, the transaction will be tracked locally for resubmission. However, if the transaction fails with a permanent error (e.g., invalid transaction or insufficient balance), the error will be propagated to the user. These errors returned in the legacyPool are regarded as temporary failure: - `ErrOutOfOrderTxFromDelegated` - `txpool.ErrInflightTxLimitReached` - `ErrAuthorityReserved` - `txpool.ErrUnderpriced` - `ErrTxPoolOverflow` - `ErrFutureReplacePending` Notably, InsufficientBalance is also treated as a permanent error, as it’s highly unlikely that users will transfer funds into the sender account after submitting the transaction. Otherwise, users may be confused—seeing their transaction submitted but unaware that the sender lacks sufficient funds—and continue waiting for it to be included. --------- Co-authored-by: lightclient <lightclient@protonmail.com>
This commit is contained in:
parent
074da25f66
commit
9089f9461c
15 changed files with 284 additions and 152 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
46
core/txpool/locals/errors.go
Normal file
46
core/txpool/locals/errors.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -324,31 +324,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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,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/gasprice"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
157
eth/api_backend_test.go
Normal file
157
eth/api_backend_test.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue