Merge branch 'master' into upgrade_filter_system

This commit is contained in:
maskpp 2025-04-18 21:42:58 +08:00
commit 4ffc292bd4
25 changed files with 460 additions and 189 deletions

View file

@ -21,6 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"math/big" "math/big"
"runtime" "runtime"
"slices" "slices"
@ -100,6 +101,10 @@ var (
errInvalidNewChain = errors.New("invalid new chain") errInvalidNewChain = errors.New("invalid new chain")
) )
var (
forkReadyInterval = 3 * time.Minute
)
const ( const (
bodyCacheLimit = 256 bodyCacheLimit = 256
blockCacheLimit = 256 blockCacheLimit = 256
@ -275,6 +280,8 @@ type BlockChain struct {
processor Processor // Block transaction processor interface processor Processor // Block transaction processor interface
vmConfig vm.Config vmConfig vm.Config
logger *tracing.Hooks logger *tracing.Hooks
lastForkReadyAlert time.Time // Last time there was a fork readiness print out
} }
// NewBlockChain returns a fully initialised block chain using information // NewBlockChain returns a fully initialised block chain using information
@ -514,19 +521,33 @@ func (bc *BlockChain) loadLastState() error {
log.Warn("Empty database, resetting chain") log.Warn("Empty database, resetting chain")
return bc.Reset() return bc.Reset()
} }
// Make sure the entire head block is available headHeader := bc.GetHeaderByHash(head)
headBlock := bc.GetBlockByHash(head) if headHeader == nil {
// Corrupt or empty database, init from scratch
log.Warn("Head header missing, resetting chain", "hash", head)
return bc.Reset()
}
var headBlock *types.Block
if cmp := headHeader.Number.Cmp(new(big.Int)); cmp == 1 {
// Make sure the entire head block is available.
headBlock = bc.GetBlockByHash(head)
} else if cmp == 0 {
// On a pruned node the block body might not be available. But a pruned
// block should never be the head block. The only exception is when, as
// a last resort, chain is reset to genesis.
headBlock = bc.genesisBlock
}
if headBlock == nil { if headBlock == nil {
// Corrupt or empty database, init from scratch // Corrupt or empty database, init from scratch
log.Warn("Head block missing, resetting chain", "hash", head) log.Warn("Head block missing, resetting chain", "hash", head)
return bc.Reset() return bc.Reset()
} }
// Everything seems to be fine, set as the head block // Everything seems to be fine, set as the head block
bc.currentBlock.Store(headBlock.Header()) bc.currentBlock.Store(headHeader)
headBlockGauge.Update(int64(headBlock.NumberU64())) headBlockGauge.Update(int64(headBlock.NumberU64()))
// Restore the last known head header // Restore the last known head header
headHeader := headBlock.Header()
if head := rawdb.ReadHeadHeaderHash(bc.db); head != (common.Hash{}) { if head := rawdb.ReadHeadHeaderHash(bc.db); head != (common.Hash{}) {
if header := bc.GetHeaderByHash(head); header != nil { if header := bc.GetHeaderByHash(head); header != nil {
headHeader = header headHeader = header
@ -620,7 +641,7 @@ func (bc *BlockChain) initializeHistoryPruning(latest uint64) error {
if predefinedPoint == nil { if predefinedPoint == nil {
log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash()) log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash())
return fmt.Errorf("history pruning requested for unknown network") return fmt.Errorf("history pruning requested for unknown network")
} else if freezerTail != predefinedPoint.BlockNumber { } else if freezerTail > 0 && freezerTail != predefinedPoint.BlockNumber {
log.Error("Chain history database is pruned to unknown block", "tail", freezerTail) log.Error("Chain history database is pruned to unknown block", "tail", freezerTail)
return fmt.Errorf("unexpected database tail") return fmt.Errorf("unexpected database tail")
} }
@ -642,11 +663,15 @@ func (bc *BlockChain) SetHead(head uint64) error {
// Send chain head event to update the transaction pool // Send chain head event to update the transaction pool
header := bc.CurrentBlock() header := bc.CurrentBlock()
if block := bc.GetBlock(header.Hash(), header.Number.Uint64()); block == nil { if block := bc.GetBlock(header.Hash(), header.Number.Uint64()); block == nil {
// This should never happen. In practice, previously currentBlock // In a pruned node the genesis block will not exist in the freezer.
// contained the entire block whereas now only a "marker", so there // It should not happen that we set head to any other pruned block.
// is an ever so slight chance for a race we should handle. if header.Number.Uint64() > 0 {
log.Error("Current block not found in database", "block", header.Number, "hash", header.Hash()) // This should never happen. In practice, previously currentBlock
return fmt.Errorf("current block missing: #%d [%x..]", header.Number, header.Hash().Bytes()[:4]) // contained the entire block whereas now only a "marker", so there
// is an ever so slight chance for a race we should handle.
log.Error("Current block not found in database", "block", header.Number, "hash", header.Hash())
return fmt.Errorf("current block missing: #%d [%x..]", header.Number, header.Hash().Bytes()[:4])
}
} }
bc.chainHeadFeed.Send(ChainHeadEvent{Header: header}) bc.chainHeadFeed.Send(ChainHeadEvent{Header: header})
return nil return nil
@ -663,11 +688,15 @@ func (bc *BlockChain) SetHeadWithTimestamp(timestamp uint64) error {
// Send chain head event to update the transaction pool // Send chain head event to update the transaction pool
header := bc.CurrentBlock() header := bc.CurrentBlock()
if block := bc.GetBlock(header.Hash(), header.Number.Uint64()); block == nil { if block := bc.GetBlock(header.Hash(), header.Number.Uint64()); block == nil {
// This should never happen. In practice, previously currentBlock // In a pruned node the genesis block will not exist in the freezer.
// contained the entire block whereas now only a "marker", so there // It should not happen that we set head to any other pruned block.
// is an ever so slight chance for a race we should handle. if header.Number.Uint64() > 0 {
log.Error("Current block not found in database", "block", header.Number, "hash", header.Hash()) // This should never happen. In practice, previously currentBlock
return fmt.Errorf("current block missing: #%d [%x..]", header.Number, header.Hash().Bytes()[:4]) // contained the entire block whereas now only a "marker", so there
// is an ever so slight chance for a race we should handle.
log.Error("Current block not found in database", "block", header.Number, "hash", header.Hash())
return fmt.Errorf("current block missing: #%d [%x..]", header.Number, header.Hash().Bytes()[:4])
}
} }
bc.chainHeadFeed.Send(ChainHeadEvent{Header: header}) bc.chainHeadFeed.Send(ChainHeadEvent{Header: header})
return nil return nil
@ -1862,6 +1891,9 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness
trieDiffNodes, trieBufNodes, _ := bc.triedb.Size() trieDiffNodes, trieBufNodes, _ := bc.triedb.Size()
stats.report(chain, it.index, snapDiffItems, snapBufItems, trieDiffNodes, trieBufNodes, setHead) stats.report(chain, it.index, snapDiffItems, snapBufItems, trieDiffNodes, trieBufNodes, setHead)
// Print confirmation that a future fork is scheduled, but not yet active.
bc.logForkReadiness(block)
if !setHead { if !setHead {
// After merge we expect few side chains. Simply count // After merge we expect few side chains. Simply count
// all blocks the CL gives us for GC processing time // all blocks the CL gives us for GC processing time
@ -1895,6 +1927,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness
"root", block.Root()) "root", block.Root())
} }
} }
stats.ignored += it.remaining() stats.ignored += it.remaining()
return witness, it.index, err return witness, it.index, err
} }
@ -2492,6 +2525,23 @@ func (bc *BlockChain) reportBlock(block *types.Block, res *ProcessResult, err er
log.Error(summarizeBadBlock(block, receipts, bc.Config(), err)) log.Error(summarizeBadBlock(block, receipts, bc.Config(), err))
} }
// logForkReadiness will write a log when a future fork is scheduled, but not
// active. This is useful so operators know their client is ready for the fork.
func (bc *BlockChain) logForkReadiness(block *types.Block) {
c := bc.Config()
current, last := c.LatestFork(block.Time()), c.LatestFork(math.MaxUint64)
t := c.Timestamp(last)
if t == nil {
return
}
at := time.Unix(int64(*t), 0)
if current < last && time.Now().After(bc.lastForkReadyAlert.Add(forkReadyInterval)) {
log.Info("Ready for fork activation", "fork", last, "date", at.Format(time.RFC822),
"remaining", time.Until(at).Round(time.Second), "timestamp", at.Unix())
bc.lastForkReadyAlert = time.Now()
}
}
// summarizeBadBlock returns a string summarizing the bad block and other // summarizeBadBlock returns a string summarizing the bad block and other
// relevant information. // relevant information.
func summarizeBadBlock(block *types.Block, receipts []*types.Receipt, config *params.ChainConfig, err error) string { func summarizeBadBlock(block *types.Block, receipts []*types.Receipt, config *params.ChainConfig, err error) string {

View file

@ -17,6 +17,8 @@
package filtermaps package filtermaps
import ( import (
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -39,6 +41,7 @@ type blockchain interface {
// of the underlying blockchain, it should only possess the block headers // of the underlying blockchain, it should only possess the block headers
// and receipts up until the expected chain view head. // and receipts up until the expected chain view head.
type ChainView struct { type ChainView struct {
lock sync.Mutex
chain blockchain chain blockchain
headNumber uint64 headNumber uint64
hashes []common.Hash // block hashes starting backwards from headNumber until first canonical hash hashes []common.Hash // block hashes starting backwards from headNumber until first canonical hash
@ -147,6 +150,9 @@ func (cv *ChainView) extendNonCanonical() bool {
// blockHash returns the given block hash without doing the head number check. // blockHash returns the given block hash without doing the head number check.
func (cv *ChainView) blockHash(number uint64) common.Hash { func (cv *ChainView) blockHash(number uint64) common.Hash {
cv.lock.Lock()
defer cv.lock.Unlock()
if number+uint64(len(cv.hashes)) <= cv.headNumber { if number+uint64(len(cv.hashes)) <= cv.headNumber {
hash := cv.chain.GetCanonicalHash(number) hash := cv.chain.GetCanonicalHash(number)
if !cv.extendNonCanonical() { if !cv.extendNonCanonical() {

View file

@ -143,6 +143,7 @@ func (f *FilterMaps) lastCanonicalSnapshotOfMap(mapIndex uint32) *renderedMap {
var best *renderedMap var best *renderedMap
for _, blockNumber := range f.renderSnapshots.Keys() { for _, blockNumber := range f.renderSnapshots.Keys() {
if cp, _ := f.renderSnapshots.Get(blockNumber); cp != nil && blockNumber < f.indexedRange.blocks.AfterLast() && if cp, _ := f.renderSnapshots.Get(blockNumber); cp != nil && blockNumber < f.indexedRange.blocks.AfterLast() &&
blockNumber <= f.indexedView.headNumber && f.indexedView.getBlockId(blockNumber) == cp.lastBlockId &&
blockNumber <= f.targetView.headNumber && f.targetView.getBlockId(blockNumber) == cp.lastBlockId && blockNumber <= f.targetView.headNumber && f.targetView.getBlockId(blockNumber) == cp.lastBlockId &&
cp.mapIndex == mapIndex && (best == nil || blockNumber > best.lastBlock) { cp.mapIndex == mapIndex && (best == nil || blockNumber > best.lastBlock) {
best = cp best = cp

View file

@ -76,8 +76,10 @@ func TestCreation(t *testing.T) {
{20000000, 1681338454, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}}, // Last Gray Glacier block {20000000, 1681338454, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}}, // Last Gray Glacier block
{20000000, 1681338455, ID{Hash: checksumToBytes(0xdce96c2d), Next: 1710338135}}, // First Shanghai block {20000000, 1681338455, ID{Hash: checksumToBytes(0xdce96c2d), Next: 1710338135}}, // First Shanghai block
{30000000, 1710338134, ID{Hash: checksumToBytes(0xdce96c2d), Next: 1710338135}}, // Last Shanghai block {30000000, 1710338134, ID{Hash: checksumToBytes(0xdce96c2d), Next: 1710338135}}, // Last Shanghai block
{40000000, 1710338135, ID{Hash: checksumToBytes(0x9f3d2254), Next: 0}}, // First Cancun block {40000000, 1710338135, ID{Hash: checksumToBytes(0x9f3d2254), Next: 1746612311}}, // First Cancun block
{50000000, 2000000000, ID{Hash: checksumToBytes(0x9f3d2254), Next: 0}}, // Future Cancun block {30000000, 1746022486, ID{Hash: checksumToBytes(0x9f3d2254), Next: 1746612311}}, // Last Cancun block
{30000000, 1746612311, ID{Hash: checksumToBytes(0xc376cf8b), Next: 0}}, // First Prague block
{50000000, 2000000000, ID{Hash: checksumToBytes(0xc376cf8b), Next: 0}}, // Future Prague block
}, },
}, },
// Sepolia test cases // Sepolia test cases
@ -137,9 +139,11 @@ func TestCreation(t *testing.T) {
// fork ID. // fork ID.
func TestValidation(t *testing.T) { func TestValidation(t *testing.T) {
// Config that has not timestamp enabled // Config that has not timestamp enabled
// TODO(lightclient): this always needs to be updated when a mainnet timestamp is set.
legacyConfig := *params.MainnetChainConfig legacyConfig := *params.MainnetChainConfig
legacyConfig.ShanghaiTime = nil legacyConfig.ShanghaiTime = nil
legacyConfig.CancunTime = nil legacyConfig.CancunTime = nil
legacyConfig.PragueTime = nil
tests := []struct { tests := []struct {
config *params.ChainConfig config *params.ChainConfig
@ -314,9 +318,7 @@ func TestValidation(t *testing.T) {
// Local is mainnet Prague, remote announces Shanghai + knowledge about Cancun. Remote // Local is mainnet Prague, remote announces Shanghai + knowledge about Cancun. Remote
// is definitely out of sync. It may or may not need the Prague update, we don't know yet. // is definitely out of sync. It may or may not need the Prague update, we don't know yet.
// {params.MainnetChainConfig, 0, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 1710338135}, nil},
// TODO(karalabe): Enable this when Cancun **and** Prague is specced, update all the numbers
//{params.MainnetChainConfig, 0, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 4370000}, nil},
// Local is mainnet Shanghai, remote announces Cancun. Local is out of sync, accept. // Local is mainnet Shanghai, remote announces Cancun. Local is out of sync, accept.
{params.MainnetChainConfig, 21000000, 1700000000, ID{Hash: checksumToBytes(0x9f3d2254), Next: 0}, nil}, {params.MainnetChainConfig, 21000000, 1700000000, ID{Hash: checksumToBytes(0x9f3d2254), Next: 0}, nil},
@ -324,8 +326,7 @@ func TestValidation(t *testing.T) {
// Local is mainnet Shanghai, remote announces Cancun, but is not aware of Prague. Local // Local is mainnet Shanghai, remote announces Cancun, but is not aware of Prague. Local
// out of sync. Local also knows about a future fork, but that is uncertain yet. // out of sync. Local also knows about a future fork, but that is uncertain yet.
// //
// TODO(karalabe): Enable this when Cancun **and** Prague is specced, update remote checksum {params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0xc376cf8b), Next: 0}, nil},
//{params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0x00000000), Next: 0}, nil},
// Local is mainnet Cancun. remote announces Shanghai but is not aware of further forks. // Local is mainnet Cancun. remote announces Shanghai but is not aware of further forks.
// Remote needs software update. // Remote needs software update.
@ -342,11 +343,11 @@ func TestValidation(t *testing.T) {
// Local is mainnet Shanghai, remote is random Shanghai. // Local is mainnet Shanghai, remote is random Shanghai.
{params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0x12345678), Next: 0}, ErrLocalIncompatibleOrStale}, {params.MainnetChainConfig, 20000000, 1681338455, ID{Hash: checksumToBytes(0x12345678), Next: 0}, ErrLocalIncompatibleOrStale},
// Local is mainnet Cancun, far in the future. Remote announces Gopherium (non existing fork) // Local is mainnet Prague, far in the future. Remote announces Gopherium (non existing fork)
// at some future timestamp 8888888888, for itself, but past block for local. Local is incompatible. // at some future timestamp 8888888888, for itself, but past block for local. Local is incompatible.
// //
// This case detects non-upgraded nodes with majority hash power (typical Ropsten mess). // This case detects non-upgraded nodes with majority hash power (typical Ropsten mess).
{params.MainnetChainConfig, 88888888, 8888888888, ID{Hash: checksumToBytes(0x9f3d2254), Next: 8888888888}, ErrLocalIncompatibleOrStale}, {params.MainnetChainConfig, 88888888, 8888888888, ID{Hash: checksumToBytes(0xc376cf8b), Next: 8888888888}, ErrLocalIncompatibleOrStale},
// Local is mainnet Shanghai. Remote is also in Shanghai, but announces Gopherium (non existing // Local is mainnet Shanghai. Remote is also in Shanghai, but announces Gopherium (non existing
// fork) at timestamp 1668000000, before Cancun. Local is incompatible. // fork) at timestamp 1668000000, before Cancun. Local is incompatible.

View file

@ -196,6 +196,19 @@ func (indexer *txIndexer) repair(head uint64) {
} }
} }
// resolveHead resolves the block number of the current chain head.
func (indexer *txIndexer) resolveHead() uint64 {
headBlockHash := rawdb.ReadHeadBlockHash(indexer.db)
if headBlockHash == (common.Hash{}) {
return 0
}
headBlockNumber := rawdb.ReadHeaderNumber(indexer.db, headBlockHash)
if headBlockNumber == nil {
return 0
}
return *headBlockNumber
}
// loop is the scheduler of the indexer, assigning indexing/unindexing tasks depending // loop is the scheduler of the indexer, assigning indexing/unindexing tasks depending
// on the received chain event. // on the received chain event.
func (indexer *txIndexer) loop(chain *BlockChain) { func (indexer *txIndexer) loop(chain *BlockChain) {
@ -203,9 +216,9 @@ func (indexer *txIndexer) loop(chain *BlockChain) {
// Listening to chain events and manipulate the transaction indexes. // Listening to chain events and manipulate the transaction indexes.
var ( var (
stop chan struct{} // Non-nil if background routine is active stop chan struct{} // Non-nil if background routine is active
done chan struct{} // Non-nil if background routine is active done chan struct{} // Non-nil if background routine is active
head = rawdb.ReadHeadBlock(indexer.db).NumberU64() // The latest announced chain head head = indexer.resolveHead() // The latest announced chain head
headCh = make(chan ChainHeadEvent) headCh = make(chan ChainHeadEvent)
sub = chain.SubscribeChainHeadEvent(headCh) sub = chain.SubscribeChainHeadEvent(headCh)

View file

@ -1391,6 +1391,8 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) {
switch { switch {
case errors.Is(err, txpool.ErrUnderpriced): case errors.Is(err, txpool.ErrUnderpriced):
addUnderpricedMeter.Mark(1) addUnderpricedMeter.Mark(1)
case errors.Is(err, txpool.ErrTxGasPriceTooLow):
addUnderpricedMeter.Mark(1)
case errors.Is(err, core.ErrNonceTooLow): case errors.Is(err, core.ErrNonceTooLow):
addStaleMeter.Mark(1) addStaleMeter.Mark(1)
case errors.Is(err, core.ErrNonceTooHigh): case errors.Is(err, core.ErrNonceTooHigh):

View file

@ -1484,7 +1484,7 @@ func TestAdd(t *testing.T) {
{ // New account, no previous txs, nonce 0, but blob fee cap too low { // New account, no previous txs, nonce 0, but blob fee cap too low
from: "alice", from: "alice",
tx: makeUnsignedTx(0, 1, 1, 0), tx: makeUnsignedTx(0, 1, 1, 0),
err: txpool.ErrUnderpriced, err: txpool.ErrTxGasPriceTooLow,
}, },
{ // Same as above but blob fee cap equals minimum, should be accepted { // Same as above but blob fee cap equals minimum, should be accepted
from: "alice", from: "alice",

View file

@ -16,7 +16,9 @@
package txpool package txpool
import "errors" import (
"errors"
)
var ( var (
// ErrAlreadyKnown is returned if the transactions is already contained // 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 is returned if the transaction contains an invalid signature.
ErrInvalidSender = errors.New("invalid sender") ErrInvalidSender = errors.New("invalid sender")
// ErrUnderpriced is returned if a transaction's gas price is below the minimum // ErrUnderpriced is returned if a transaction's gas price is too low to be
// configured for the transaction pool. // 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") ErrUnderpriced = errors.New("transaction underpriced")
// ErrReplaceUnderpriced is returned if a transaction is attempted to be replaced // ErrReplaceUnderpriced is returned if a transaction is attempted to be replaced
// with a different one without the required price bump. // with a different one without the required price bump.
ErrReplaceUnderpriced = errors.New("replacement transaction underpriced") 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 // ErrAccountLimitExceeded is returned if a transaction would exceed the number
// allowed by a pool for a single account. // allowed by a pool for a single account.
ErrAccountLimitExceeded = errors.New("account limit exceeded") ErrAccountLimitExceeded = errors.New("account limit exceeded")

View file

@ -1827,6 +1827,16 @@ func (t *lookup) Remove(hash common.Hash) {
delete(t.txs, hash) delete(t.txs, hash)
} }
// Clear resets the lookup structure, removing all stored entries.
func (t *lookup) Clear() {
t.lock.Lock()
defer t.lock.Unlock()
t.slots = 0
t.txs = make(map[common.Hash]*types.Transaction)
t.auths = make(map[common.Address][]common.Hash)
}
// TxsBelowTip finds all remote transactions below the given tip threshold. // TxsBelowTip finds all remote transactions below the given tip threshold.
func (t *lookup) TxsBelowTip(threshold *big.Int) types.Transactions { func (t *lookup) TxsBelowTip(threshold *big.Int) types.Transactions {
found := make(types.Transactions, 0, 128) found := make(types.Transactions, 0, 128)
@ -1923,7 +1933,7 @@ func (pool *LegacyPool) Clear() {
for addr := range pool.queue { for addr := range pool.queue {
pool.reserver.Release(addr) pool.reserver.Release(addr)
} }
pool.all = newLookup() pool.all.Clear()
pool.priced = newPricedList(pool.all) pool.priced = newPricedList(pool.all)
pool.pending = make(map[common.Address]*list) pool.pending = make(map[common.Address]*list)
pool.queue = make(map[common.Address]*list) pool.queue = make(map[common.Address]*list)

View file

@ -82,12 +82,14 @@ func TestTransactionFutureAttack(t *testing.T) {
// Create the pool to test the limit enforcement with // Create the pool to test the limit enforcement with
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
blockchain := newTestBlockChain(eip1559Config, 1000000, statedb, new(event.Feed)) blockchain := newTestBlockChain(eip1559Config, 1000000, statedb, new(event.Feed))
config := testTxPoolConfig config := testTxPoolConfig
config.GlobalQueue = 100 config.GlobalQueue = 100
config.GlobalSlots = 100 config.GlobalSlots = 100
pool := New(config, blockchain) pool := New(config, blockchain)
pool.Init(config.PriceLimit, blockchain.CurrentBlock(), newReserver()) pool.Init(config.PriceLimit, blockchain.CurrentBlock(), newReserver())
defer pool.Close() defer pool.Close()
fillPool(t, pool) fillPool(t, pool)
pending, _ := pool.Stats() pending, _ := pool.Stats()
// Now, future transaction attack starts, let's add a bunch of expensive non-executables, and see if the pending-count drops // 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() ivPending := countInvalidPending()
t.Logf("invalid pending: %d\n", ivPending) 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++ { for j := 0; j < int(pool.config.GlobalQueue); j++ {
futureTxs := types.Transactions{} futureTxs := types.Transactions{}
key, _ := crypto.GenerateKey() key, _ := crypto.GenerateKey()

View file

@ -413,7 +413,7 @@ func TestInvalidTransactions(t *testing.T) {
tx = transaction(1, 100000, key) tx = transaction(1, 100000, key)
pool.gasTip.Store(uint256.NewInt(1000)) 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) 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) tx, _ := types.SignTx(types.NewTransaction(0, common.Address{}, big.NewInt(-1), 100, big.NewInt(1), nil), types.HomesteadSigner{}, key)
from, _ := deriveSender(tx) from, _ := deriveSender(tx)
testAddBalance(pool, from, big.NewInt(1)) 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) 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) 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) t.Error("expected", core.ErrTipAboveFeeCap, "got", err)
} }
} }
@ -512,12 +512,12 @@ func TestVeryHighValues(t *testing.T) {
veryBigNumber.Lsh(veryBigNumber, 300) veryBigNumber.Lsh(veryBigNumber, 300)
tx := dynamicFeeTx(0, 100, big.NewInt(1), veryBigNumber, key) 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) t.Error("expected", core.ErrTipVeryHigh, "got", err)
} }
tx2 := dynamicFeeTx(0, 100, veryBigNumber, big.NewInt(1), key) 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) t.Error("expected", core.ErrFeeCapVeryHigh, "got", err)
} }
} }
@ -1424,14 +1424,14 @@ func TestRepricing(t *testing.T) {
t.Fatalf("pool internal state corrupted: %v", err) t.Fatalf("pool internal state corrupted: %v", err)
} }
// Check that we can't add the old transactions back // 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) { 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.ErrUnderpriced) 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) { 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.ErrUnderpriced) 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) { 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.ErrUnderpriced) t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
} }
if err := validateEvents(events, 0); err != nil { if err := validateEvents(events, 0); err != nil {
t.Fatalf("post-reprice event firing failed: %v", err) 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) tx := pricedTransaction(0, 100000, big.NewInt(2), key)
pool.SetGasTip(big.NewInt(tx.GasPrice().Int64() + 1)) 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") t.Fatalf("Min tip not enforced")
} }
tx = dynamicFeeTx(0, 100000, big.NewInt(3), big.NewInt(2), key) tx = dynamicFeeTx(0, 100000, big.NewInt(3), big.NewInt(2), key)
pool.SetGasTip(big.NewInt(tx.GasTipCap().Int64() + 1)) 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") 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 // Check that we can't add the old transactions back
tx := pricedTransaction(1, 100000, big.NewInt(1), keys[0]) tx := pricedTransaction(1, 100000, big.NewInt(1), keys[0])
if err := pool.addRemote(tx); !errors.Is(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.ErrUnderpriced) 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]) tx = dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[1])
if err := pool.addRemote(tx); !errors.Is(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.ErrUnderpriced) 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]) tx = dynamicFeeTx(2, 100000, big.NewInt(1), big.NewInt(1), keys[2])
if err := pool.addRemote(tx); !errors.Is(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.ErrUnderpriced) t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow)
} }
if err := validateEvents(events, 0); err != nil { if err := validateEvents(events, 0); err != nil {
t.Fatalf("post-reprice event firing failed: %v", err) 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) t.Fatalf("failed to add well priced transaction: %v", err)
} }
// Ensure that replacing a pending transaction with a future transaction fails // 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) t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, ErrFutureReplacePending)
} }
pending, queued = pool.Stats() 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 { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), key)); err != nil {
t.Fatalf("failed to add original cheap pending transaction: %v", err) 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) 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 { 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 { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(price), key)); err != nil {
t.Fatalf("failed to add original proper pending transaction: %v", err) 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) 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 { 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 { if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), key)); err != nil {
t.Fatalf("failed to add original cheap queued transaction: %v", err) 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) 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 { 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 { if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(price), key)); err != nil {
t.Fatalf("failed to add original proper queued transaction: %v", err) 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) 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 { 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 // 2. Don't bump tip or feecap => discard
tx = dynamicFeeTx(nonce, 100001, big.NewInt(2), big.NewInt(1), key) 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) t.Fatalf("original cheap %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
} }
// 3. Bump both more than min => accept // 3. Bump both more than min => accept
@ -2117,24 +2117,25 @@ func TestReplacementDynamicFee(t *testing.T) {
if err := pool.addRemoteSync(tx); err != nil { if err := pool.addRemoteSync(tx); err != nil {
t.Fatalf("failed to add original proper %s transaction: %v", stage, err) t.Fatalf("failed to add original proper %s transaction: %v", stage, err)
} }
// 6. Bump tip max allowed so it's still underpriced => discard // 6. Bump tip max allowed so it's still underpriced => discard
tx = dynamicFeeTx(nonce, 100000, big.NewInt(gasFeeCap), big.NewInt(tipThreshold-1), key) 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) 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 // 7. Bump fee cap max allowed so it's still underpriced => discard
tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold-1), big.NewInt(gasTipCap), key) 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) t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced)
} }
// 8. Bump tip min for acceptance => accept // 8. Bump tip min for acceptance => accept
tx = dynamicFeeTx(nonce, 100000, big.NewInt(gasFeeCap), big.NewInt(tipThreshold), key) 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) 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 // 9. Bump fee cap min for acceptance => accept
tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold), big.NewInt(gasTipCap), key) 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) 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) // 10. Check events match expected (3 new executable txs during pending, 0 during queue)

View 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
}
}

View file

@ -74,32 +74,22 @@ func New(journalPath string, journalTime time.Duration, chainConfig *params.Chai
// Track adds a transaction to the tracked set. // Track adds a transaction to the tracked set.
// Note: blob-type transactions are ignored. // Note: blob-type transactions are ignored.
func (tracker *TxTracker) Track(tx *types.Transaction) error { func (tracker *TxTracker) Track(tx *types.Transaction) {
return tracker.TrackAll([]*types.Transaction{tx})[0] tracker.TrackAll([]*types.Transaction{tx})
} }
// TrackAll adds a list of transactions to the tracked set. // TrackAll adds a list of transactions to the tracked set.
// Note: blob-type transactions are ignored. // Note: blob-type transactions are ignored.
func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error { func (tracker *TxTracker) TrackAll(txs []*types.Transaction) {
tracker.mu.Lock() tracker.mu.Lock()
defer tracker.mu.Unlock() defer tracker.mu.Unlock()
var errors []error
for _, tx := range txs { for _, tx := range txs {
if tx.Type() == types.BlobTxType { 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 continue
} }
// If we're already tracking it, it's a no-op // If we're already tracking it, it's a no-op
if _, ok := tracker.all[tx.Hash()]; ok { if _, ok := tracker.all[tx.Hash()]; ok {
errors = append(errors, nil)
continue continue
} }
// Theoretically, checking the error here is unnecessary since sender recovery // 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. // Therefore, the error is still checked just in case.
addr, err := types.Sender(tracker.signer, tx) addr, err := types.Sender(tracker.signer, tx)
if err != nil { if err != nil {
errors = append(errors, err)
continue continue
} }
errors = append(errors, nil)
tracker.all[tx.Hash()] = tx tracker.all[tx.Hash()] = tx
if tracker.byAddr[addr] == nil { if tracker.byAddr[addr] == nil {
tracker.byAddr[addr] = legacypool.NewSortedMap() tracker.byAddr[addr] = legacypool.NewSortedMap()
@ -124,7 +111,6 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error {
} }
} }
localGauge.Update(int64(len(tracker.all))) localGauge.Update(int64(len(tracker.all)))
return errors
} }
// recheck checks and returns any transactions that needs to be resubmitted. // recheck checks and returns any transactions that needs to be resubmitted.

View file

@ -17,7 +17,6 @@
package locals package locals
import ( import (
"errors"
"math/big" "math/big"
"testing" "testing"
"time" "time"
@ -91,10 +90,12 @@ func (env *testEnv) close() {
env.chain.Stop() env.chain.Stop()
} }
// nolint:unused
func (env *testEnv) setGasTip(gasTip uint64) { func (env *testEnv) setGasTip(gasTip uint64) {
env.pool.SetGasTip(new(big.Int).SetUint64(gasTip)) env.pool.SetGasTip(new(big.Int).SetUint64(gasTip))
} }
// nolint:unused
func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction { func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction {
if nonce == 0 { if nonce == 0 {
head := env.chain.CurrentHeader() head := env.chain.CurrentHeader()
@ -121,6 +122,7 @@ func (env *testEnv) makeTxs(n int) []*types.Transaction {
return txs return txs
} }
// nolint:unused
func (env *testEnv) commit() { func (env *testEnv) commit() {
head := env.chain.CurrentBlock() head := env.chain.CurrentBlock()
block := env.chain.GetBlock(head.Hash(), head.Number.Uint64()) 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) { func TestResubmit(t *testing.T) {
env := newTestEnv(t, 10, 0, "") env := newTestEnv(t, 10, 0, "")
defer env.close() defer env.close()

View file

@ -186,13 +186,15 @@ func (p *TxPool) loop(head *types.Header) {
// Try to inject a busy marker and start a reset if successful // Try to inject a busy marker and start a reset if successful
select { select {
case resetBusy <- struct{}{}: case resetBusy <- struct{}{}:
statedb, err := p.chain.StateAt(newHead.Root) // Updates the statedb with the new chain head. The head state may be
if err != nil { // unavailable if the initial state sync has not yet completed.
log.Crit("Failed to reset txpool state", "err", err) if statedb, err := p.chain.StateAt(newHead.Root); err != nil {
log.Error("Failed to reset txpool state", "err", err)
} else {
p.stateLock.Lock()
p.state = statedb
p.stateLock.Unlock()
} }
p.stateLock.Lock()
p.state = statedb
p.stateLock.Unlock()
// Busy marker injected, start a new subpool reset // Busy marker injected, start a new subpool reset
go func(oldHead, newHead *types.Header) { go func(oldHead, newHead *types.Header) {
@ -322,31 +324,6 @@ func (p *TxPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Pr
return nil, nil 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 // 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 the large transaction churn, add may postpone fully integrating the tx
// to a later point to batch multiple ones together. // to a later point to batch multiple ones together.

View file

@ -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 // Ensure the gasprice is high enough to cover the requirement of the calling pool
if tx.GasTipCapIntCmp(opts.MinTip) < 0 { 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 { if tx.Type() == types.BlobTxType {
// Ensure the blob fee cap satisfies the minimum blob gas price // Ensure the blob fee cap satisfies the minimum blob gas price
if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 { 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() sidecar := tx.BlobTxSidecar()
if sidecar == nil { if sidecar == nil {

View file

@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/txpool" "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/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/gasprice" "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 { 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] 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 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 return nil
} }

157
eth/api_backend_test.go Normal file
View 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)
}
}
}

View file

@ -21,6 +21,7 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"math"
"sync" "sync"
"time" "time"
@ -124,9 +125,13 @@ func NewSimulatedBeacon(period uint64, feeRecipient common.Address, eth *eth.Eth
return nil, err return nil, err
} }
} }
// cap the dev mode period to a reasonable maximum value to avoid
// overflowing the time.Duration (int64) that it will occupy
const maxPeriod = uint64(math.MaxInt64 / time.Second)
return &SimulatedBeacon{ return &SimulatedBeacon{
eth: eth, eth: eth,
period: period, period: min(period, maxPeriod),
shutdownCh: make(chan struct{}), shutdownCh: make(chan struct{}),
engineAPI: engineAPI, engineAPI: engineAPI,
lastBlockTime: block.Time, lastBlockTime: block.Time,

View file

@ -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. // Track the transaction hash if the price is too low for us.
// Avoid re-request this transaction when we receive another // Avoid re-request this transaction when we receive another
// announcement. // 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()) f.underpriced.Add(batch[j].Hash(), batch[j].Time())
} }
// Track a few interesting failure types // 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): case errors.Is(err, txpool.ErrAlreadyKnown):
duplicate++ 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++ underpriced++
default: default:

View file

@ -1244,10 +1244,12 @@ func TestTransactionFetcherUnderpricedDedup(t *testing.T) {
func(txs []*types.Transaction) []error { func(txs []*types.Transaction) []error {
errs := make([]error, len(txs)) errs := make([]error, len(txs))
for i := 0; i < len(errs); i++ { for i := 0; i < len(errs); i++ {
if i%2 == 0 { if i%3 == 0 {
errs[i] = txpool.ErrUnderpriced errs[i] = txpool.ErrUnderpriced
} else { } else if i%3 == 1 {
errs[i] = txpool.ErrReplaceUnderpriced errs[i] = txpool.ErrReplaceUnderpriced
} else {
errs[i] = txpool.ErrTxGasPriceTooLow
} }
} }
return errs return errs

View file

@ -484,7 +484,7 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) {
total := new(big.Int).Exp(direct, big.NewInt(2), nil) // Stabilise total peer count a bit based on sqrt peers total := new(big.Int).Exp(direct, big.NewInt(2), nil) // Stabilise total peer count a bit based on sqrt peers
var ( var (
signer = types.LatestSignerForChainID(h.chain.Config().ChainID) // Don't care about chain status, we just need *a* sender signer = types.LatestSigner(h.chain.Config()) // Don't care about chain status, we just need *a* sender
hasher = crypto.NewKeccakState() hasher = crypto.NewKeccakState()
hash = make([]byte, 32) hash = make([]byte, 32)
) )

View file

@ -25,16 +25,14 @@ import (
"testing" "testing"
"time" "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/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
"go.uber.org/goleak"
) )
var _ bind.ContractBackend = (Client)(nil) var _ bind.ContractBackend = (Client)(nil)

View file

@ -60,10 +60,12 @@ var (
TerminalTotalDifficulty: MainnetTerminalTotalDifficulty, // 58_750_000_000_000_000_000_000 TerminalTotalDifficulty: MainnetTerminalTotalDifficulty, // 58_750_000_000_000_000_000_000
ShanghaiTime: newUint64(1681338455), ShanghaiTime: newUint64(1681338455),
CancunTime: newUint64(1710338135), CancunTime: newUint64(1710338135),
PragueTime: newUint64(1746612311),
DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"), DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"),
Ethash: new(EthashConfig), Ethash: new(EthashConfig),
BlobScheduleConfig: &BlobScheduleConfig{ BlobScheduleConfig: &BlobScheduleConfig{
Cancun: DefaultCancunBlobConfig, Cancun: DefaultCancunBlobConfig,
Prague: DefaultPragueBlobConfig,
}, },
} }
// HoleskyChainConfig contains the chain parameters to run a node on the Holesky test network. // HoleskyChainConfig contains the chain parameters to run a node on the Holesky test network.
@ -907,6 +909,23 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork {
} }
} }
// Timestamp returns the timestamp associated with the fork or returns nil if
// the fork isn't defined or isn't a time-based fork.
func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 {
switch {
case fork == forks.Osaka:
return c.OsakaTime
case fork == forks.Prague:
return c.PragueTime
case fork == forks.Cancun:
return c.CancunTime
case fork == forks.Shanghai:
return c.ShanghaiTime
default:
return nil
}
}
// isForkBlockIncompatible returns true if a fork scheduled at block s1 cannot be // isForkBlockIncompatible returns true if a fork scheduled at block s1 cannot be
// rescheduled to block s2 because head is already past the fork. // rescheduled to block s2 because head is already past the fork.
func isForkBlockIncompatible(s1, s2, head *big.Int) bool { func isForkBlockIncompatible(s1, s2, head *big.Int) bool {

View file

@ -20,7 +20,7 @@ package forks
type Fork int type Fork int
const ( const (
Frontier = iota Frontier Fork = iota
FrontierThawing FrontierThawing
Homestead Homestead
DAO DAO
@ -41,3 +41,35 @@ const (
Prague Prague
Osaka Osaka
) )
// String implements fmt.Stringer.
func (f Fork) String() string {
s, ok := forkToString[f]
if !ok {
return "Unknown fork"
}
return s
}
var forkToString = map[Fork]string{
Frontier: "Frontier",
FrontierThawing: "Frontier Thawing",
Homestead: "Homestead",
DAO: "DAO",
TangerineWhistle: "Tangerine Whistle",
SpuriousDragon: "Spurious Dragon",
Byzantium: "Byzantium",
Constantinople: "Constantinople",
Petersburg: "Petersburg",
Istanbul: "Istanbul",
MuirGlacier: "Muir Glacier",
Berlin: "Berlin",
London: "London",
ArrowGlacier: "Arrow Glacier",
GrayGlacier: "Gray Glacier",
Paris: "Paris",
Shanghai: "Shanghai",
Cancun: "Cancun",
Prague: "Prague",
Osaka: "Osaka",
}