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) + } +}