diff --git a/core/blockchain.go b/core/blockchain.go index 901a93315a..6667f64911 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "io" + "math" "math/big" "runtime" "slices" @@ -100,6 +101,10 @@ var ( errInvalidNewChain = errors.New("invalid new chain") ) +var ( + forkReadyInterval = 3 * time.Minute +) + const ( bodyCacheLimit = 256 blockCacheLimit = 256 @@ -275,6 +280,8 @@ type BlockChain struct { processor Processor // Block transaction processor interface vmConfig vm.Config logger *tracing.Hooks + + lastForkReadyAlert time.Time // Last time there was a fork readiness print out } // NewBlockChain returns a fully initialised block chain using information @@ -514,19 +521,33 @@ func (bc *BlockChain) loadLastState() error { log.Warn("Empty database, resetting chain") return bc.Reset() } - // Make sure the entire head block is available - headBlock := bc.GetBlockByHash(head) + headHeader := bc.GetHeaderByHash(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 { // Corrupt or empty database, init from scratch log.Warn("Head block missing, resetting chain", "hash", head) return bc.Reset() } // Everything seems to be fine, set as the head block - bc.currentBlock.Store(headBlock.Header()) + bc.currentBlock.Store(headHeader) headBlockGauge.Update(int64(headBlock.NumberU64())) // Restore the last known head header - headHeader := headBlock.Header() if head := rawdb.ReadHeadHeaderHash(bc.db); head != (common.Hash{}) { if header := bc.GetHeaderByHash(head); header != nil { headHeader = header @@ -620,7 +641,7 @@ func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { if predefinedPoint == nil { log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash()) 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) 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 header := bc.CurrentBlock() if block := bc.GetBlock(header.Hash(), header.Number.Uint64()); block == nil { - // This should never happen. In practice, previously currentBlock - // 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]) + // In a pruned node the genesis block will not exist in the freezer. + // It should not happen that we set head to any other pruned block. + if header.Number.Uint64() > 0 { + // This should never happen. In practice, previously currentBlock + // 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}) return nil @@ -663,11 +688,15 @@ func (bc *BlockChain) SetHeadWithTimestamp(timestamp uint64) error { // Send chain head event to update the transaction pool header := bc.CurrentBlock() if block := bc.GetBlock(header.Hash(), header.Number.Uint64()); block == nil { - // This should never happen. In practice, previously currentBlock - // 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]) + // In a pruned node the genesis block will not exist in the freezer. + // It should not happen that we set head to any other pruned block. + if header.Number.Uint64() > 0 { + // This should never happen. In practice, previously currentBlock + // 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}) return nil @@ -1862,6 +1891,9 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness trieDiffNodes, trieBufNodes, _ := bc.triedb.Size() 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 { // After merge we expect few side chains. Simply count // 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()) } } + stats.ignored += it.remaining() 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)) } +// 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 // relevant information. func summarizeBadBlock(block *types.Block, receipts []*types.Receipt, config *params.ChainConfig, err error) string { diff --git a/core/filtermaps/chain_view.go b/core/filtermaps/chain_view.go index a8cf53b1c0..d6f0a727bf 100644 --- a/core/filtermaps/chain_view.go +++ b/core/filtermaps/chain_view.go @@ -17,6 +17,8 @@ package filtermaps import ( + "sync" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -39,6 +41,7 @@ type blockchain interface { // of the underlying blockchain, it should only possess the block headers // and receipts up until the expected chain view head. type ChainView struct { + lock sync.Mutex chain blockchain headNumber uint64 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. func (cv *ChainView) blockHash(number uint64) common.Hash { + cv.lock.Lock() + defer cv.lock.Unlock() + if number+uint64(len(cv.hashes)) <= cv.headNumber { hash := cv.chain.GetCanonicalHash(number) if !cv.extendNonCanonical() { diff --git a/core/filtermaps/map_renderer.go b/core/filtermaps/map_renderer.go index cd960ad31c..23d84f0eca 100644 --- a/core/filtermaps/map_renderer.go +++ b/core/filtermaps/map_renderer.go @@ -143,6 +143,7 @@ func (f *FilterMaps) lastCanonicalSnapshotOfMap(mapIndex uint32) *renderedMap { var best *renderedMap for _, blockNumber := range f.renderSnapshots.Keys() { 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 && cp.mapIndex == mapIndex && (best == nil || blockNumber > best.lastBlock) { best = cp diff --git a/core/forkid/forkid_test.go b/core/forkid/forkid_test.go index 31e2b534be..413e4d77a8 100644 --- a/core/forkid/forkid_test.go +++ b/core/forkid/forkid_test.go @@ -76,8 +76,10 @@ func TestCreation(t *testing.T) { {20000000, 1681338454, ID{Hash: checksumToBytes(0xf0afd0e3), Next: 1681338455}}, // Last Gray Glacier block {20000000, 1681338455, ID{Hash: checksumToBytes(0xdce96c2d), Next: 1710338135}}, // First Shanghai block {30000000, 1710338134, ID{Hash: checksumToBytes(0xdce96c2d), Next: 1710338135}}, // Last Shanghai block - {40000000, 1710338135, ID{Hash: checksumToBytes(0x9f3d2254), Next: 0}}, // First Cancun block - {50000000, 2000000000, ID{Hash: checksumToBytes(0x9f3d2254), Next: 0}}, // Future Cancun block + {40000000, 1710338135, ID{Hash: checksumToBytes(0x9f3d2254), Next: 1746612311}}, // First 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 @@ -137,9 +139,11 @@ func TestCreation(t *testing.T) { // fork ID. func TestValidation(t *testing.T) { // Config that has not timestamp enabled + // TODO(lightclient): this always needs to be updated when a mainnet timestamp is set. legacyConfig := *params.MainnetChainConfig legacyConfig.ShanghaiTime = nil legacyConfig.CancunTime = nil + legacyConfig.PragueTime = nil tests := []struct { config *params.ChainConfig @@ -314,9 +318,7 @@ func TestValidation(t *testing.T) { // 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. - // - // 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}, + {params.MainnetChainConfig, 0, 0, ID{Hash: checksumToBytes(0x3edd5b10), Next: 1710338135}, nil}, // Local is mainnet Shanghai, remote announces Cancun. Local is out of sync, accept. {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 // 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(0x00000000), Next: 0}, nil}, + {params.MainnetChainConfig, 21000000, 1678000000, ID{Hash: checksumToBytes(0xc376cf8b), Next: 0}, nil}, // Local is mainnet Cancun. remote announces Shanghai but is not aware of further forks. // Remote needs software update. @@ -342,11 +343,11 @@ func TestValidation(t *testing.T) { // Local is mainnet Shanghai, remote is random Shanghai. {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. // // 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 // fork) at timestamp 1668000000, before Cancun. Local is incompatible. diff --git a/core/txindexer.go b/core/txindexer.go index 31f069995b..64a2e8c49f 100644 --- a/core/txindexer.go +++ b/core/txindexer.go @@ -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 // on the received chain event. 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. var ( - stop 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 + stop chan struct{} // Non-nil if background routine is active + done chan struct{} // Non-nil if background routine is active + head = indexer.resolveHead() // The latest announced chain head headCh = make(chan ChainHeadEvent) sub = chain.SubscribeChainHeadEvent(headCh) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 12a4133b40..e506da228d 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1391,6 +1391,8 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) { switch { case errors.Is(err, txpool.ErrUnderpriced): addUnderpricedMeter.Mark(1) + case errors.Is(err, txpool.ErrTxGasPriceTooLow): + addUnderpricedMeter.Mark(1) case errors.Is(err, core.ErrNonceTooLow): addStaleMeter.Mark(1) case errors.Is(err, core.ErrNonceTooHigh): diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 76d21a0c9e..0a323179a6 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -1484,7 +1484,7 @@ func TestAdd(t *testing.T) { { // New account, no previous txs, nonce 0, but blob fee cap too low from: "alice", tx: makeUnsignedTx(0, 1, 1, 0), - err: txpool.ErrUnderpriced, + err: txpool.ErrTxGasPriceTooLow, }, { // Same as above but blob fee cap equals minimum, should be accepted from: "alice", diff --git a/core/txpool/errors.go b/core/txpool/errors.go index 02f5703b6c..968c9d9542 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,14 +28,19 @@ var ( // ErrInvalidSender is returned if the transaction contains an invalid signature. ErrInvalidSender = errors.New("invalid sender") - // ErrUnderpriced is returned if a transaction's gas price is below the minimum - // configured for the transaction pool. + // ErrUnderpriced is returned if a transaction's gas price is too low to be + // included in the pool. If the gas price is lower than the minimum configured + // one for the transaction pool, use ErrTxGasPriceTooLow instead. ErrUnderpriced = errors.New("transaction underpriced") // ErrReplaceUnderpriced is returned if a transaction is attempted to be replaced // with a different one without the required price bump. ErrReplaceUnderpriced = errors.New("replacement transaction underpriced") + // ErrTxGasPriceTooLow is returned if a transaction's gas price is below the + // minimum configured for the transaction pool. + ErrTxGasPriceTooLow = errors.New("transaction gas price below minimum") + // ErrAccountLimitExceeded is returned if a transaction would exceed the number // allowed by a pool for a single account. ErrAccountLimitExceeded = errors.New("account limit exceeded") diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 04f1a2234c..7bf360ff65 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -1827,6 +1827,16 @@ func (t *lookup) Remove(hash common.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. func (t *lookup) TxsBelowTip(threshold *big.Int) types.Transactions { found := make(types.Transactions, 0, 128) @@ -1923,7 +1933,7 @@ func (pool *LegacyPool) Clear() { for addr := range pool.queue { pool.reserver.Release(addr) } - pool.all = newLookup() + pool.all.Clear() pool.priced = newPricedList(pool.all) pool.pending = make(map[common.Address]*list) pool.queue = make(map[common.Address]*list) diff --git a/core/txpool/legacypool/legacypool2_test.go b/core/txpool/legacypool/legacypool2_test.go index 3f210e3d1b..deb06aa617 100644 --- a/core/txpool/legacypool/legacypool2_test.go +++ b/core/txpool/legacypool/legacypool2_test.go @@ -82,12 +82,14 @@ func TestTransactionFutureAttack(t *testing.T) { // Create the pool to test the limit enforcement with statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) blockchain := newTestBlockChain(eip1559Config, 1000000, statedb, new(event.Feed)) + config := testTxPoolConfig config.GlobalQueue = 100 config.GlobalSlots = 100 pool := New(config, blockchain) pool.Init(config.PriceLimit, blockchain.CurrentBlock(), newReserver()) defer pool.Close() + fillPool(t, pool) pending, _ := pool.Stats() // Now, future transaction attack starts, let's add a bunch of expensive non-executables, and see if the pending-count drops @@ -180,7 +182,9 @@ func TestTransactionZAttack(t *testing.T) { ivPending := countInvalidPending() t.Logf("invalid pending: %d\n", ivPending) - // Now, DETER-Z attack starts, let's add a bunch of expensive non-executables (from N accounts) along with balance-overdraft txs (from one account), and see if the pending-count drops + // Now, DETER-Z attack starts, let's add a bunch of expensive non-executables + // (from N accounts) along with balance-overdraft txs (from one account), and + // see if the pending-count drops for j := 0; j < int(pool.config.GlobalQueue); j++ { futureTxs := types.Transactions{} key, _ := crypto.GenerateKey() diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index bb1323a7d1..2fdf890320 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -413,7 +413,7 @@ func TestInvalidTransactions(t *testing.T) { tx = transaction(1, 100000, key) pool.gasTip.Store(uint256.NewInt(1000)) - if err, want := pool.addRemote(tx), txpool.ErrUnderpriced; !errors.Is(err, want) { + if err, want := pool.addRemote(tx), txpool.ErrTxGasPriceTooLow; !errors.Is(err, want) { t.Errorf("want %v have %v", want, err) } } @@ -484,7 +484,7 @@ func TestNegativeValue(t *testing.T) { tx, _ := types.SignTx(types.NewTransaction(0, common.Address{}, big.NewInt(-1), 100, big.NewInt(1), nil), types.HomesteadSigner{}, key) from, _ := deriveSender(tx) testAddBalance(pool, from, big.NewInt(1)) - if err := pool.addRemote(tx); err != txpool.ErrNegativeValue { + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrNegativeValue) { t.Error("expected", txpool.ErrNegativeValue, "got", err) } } @@ -497,7 +497,7 @@ func TestTipAboveFeeCap(t *testing.T) { tx := dynamicFeeTx(0, 100, big.NewInt(1), big.NewInt(2), key) - if err := pool.addRemote(tx); err != core.ErrTipAboveFeeCap { + if err := pool.addRemote(tx); !errors.Is(err, core.ErrTipAboveFeeCap) { t.Error("expected", core.ErrTipAboveFeeCap, "got", err) } } @@ -512,12 +512,12 @@ func TestVeryHighValues(t *testing.T) { veryBigNumber.Lsh(veryBigNumber, 300) tx := dynamicFeeTx(0, 100, big.NewInt(1), veryBigNumber, key) - if err := pool.addRemote(tx); err != core.ErrTipVeryHigh { + if err := pool.addRemote(tx); !errors.Is(err, core.ErrTipVeryHigh) { t.Error("expected", core.ErrTipVeryHigh, "got", err) } tx2 := dynamicFeeTx(0, 100, veryBigNumber, big.NewInt(1), key) - if err := pool.addRemote(tx2); err != core.ErrFeeCapVeryHigh { + if err := pool.addRemote(tx2); !errors.Is(err, core.ErrFeeCapVeryHigh) { t.Error("expected", core.ErrFeeCapVeryHigh, "got", err) } } @@ -1424,14 +1424,14 @@ func TestRepricing(t *testing.T) { t.Fatalf("pool internal state corrupted: %v", err) } // Check that we can't add the old transactions back - if err := pool.addRemote(pricedTransaction(1, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrUnderpriced) { - t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced) + if err := pool.addRemote(pricedTransaction(1, 100000, big.NewInt(1), keys[0])); !errors.Is(err, txpool.ErrTxGasPriceTooLow) { + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow) } - if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); !errors.Is(err, txpool.ErrUnderpriced) { - t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced) + if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); !errors.Is(err, txpool.ErrTxGasPriceTooLow) { + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow) } - if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrUnderpriced) { - t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced) + if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), keys[2])); !errors.Is(err, txpool.ErrTxGasPriceTooLow) { + t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow) } if err := validateEvents(events, 0); err != nil { t.Fatalf("post-reprice event firing failed: %v", err) @@ -1476,14 +1476,14 @@ func TestMinGasPriceEnforced(t *testing.T) { tx := pricedTransaction(0, 100000, big.NewInt(2), key) pool.SetGasTip(big.NewInt(tx.GasPrice().Int64() + 1)) - if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrUnderpriced) { + if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrTxGasPriceTooLow) { t.Fatalf("Min tip not enforced") } tx = dynamicFeeTx(0, 100000, big.NewInt(3), big.NewInt(2), key) pool.SetGasTip(big.NewInt(tx.GasTipCap().Int64() + 1)) - if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrUnderpriced) { + if err := pool.Add([]*types.Transaction{tx}, true)[0]; !errors.Is(err, txpool.ErrTxGasPriceTooLow) { t.Fatalf("Min tip not enforced") } } @@ -1560,16 +1560,16 @@ func TestRepricingDynamicFee(t *testing.T) { } // Check that we can't add the old transactions back tx := pricedTransaction(1, 100000, big.NewInt(1), keys[0]) - if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrUnderpriced) { - t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced) + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrTxGasPriceTooLow) { + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow) } tx = dynamicFeeTx(0, 100000, big.NewInt(2), big.NewInt(1), keys[1]) - if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrUnderpriced) { - t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced) + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrTxGasPriceTooLow) { + t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow) } tx = dynamicFeeTx(2, 100000, big.NewInt(1), big.NewInt(1), keys[2]) - if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrUnderpriced) { - t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrUnderpriced) + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrTxGasPriceTooLow) { + t.Fatalf("adding underpriced queued transaction error mismatch: have %v, want %v", err, txpool.ErrTxGasPriceTooLow) } if err := validateEvents(events, 0); err != nil { t.Fatalf("post-reprice event firing failed: %v", err) @@ -1673,7 +1673,7 @@ func TestUnderpricing(t *testing.T) { t.Fatalf("failed to add well priced transaction: %v", err) } // Ensure that replacing a pending transaction with a future transaction fails - if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); err != ErrFutureReplacePending { + if err := pool.addRemoteSync(pricedTransaction(5, 100000, big.NewInt(6), keys[1])); !errors.Is(err, ErrFutureReplacePending) { t.Fatalf("adding future replace transaction error mismatch: have %v, want %v", err, ErrFutureReplacePending) } pending, queued = pool.Stats() @@ -1995,7 +1995,7 @@ func TestReplacement(t *testing.T) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(1), key)); err != nil { t.Fatalf("failed to add original cheap pending transaction: %v", err) } - if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(1), key)); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original cheap pending transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced) } if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(2), key)); err != nil { @@ -2008,7 +2008,7 @@ func TestReplacement(t *testing.T) { if err := pool.addRemoteSync(pricedTransaction(0, 100000, big.NewInt(price), key)); err != nil { t.Fatalf("failed to add original proper pending transaction: %v", err) } - if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(threshold-1), key)); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(pricedTransaction(0, 100001, big.NewInt(threshold-1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original proper pending transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced) } if err := pool.addRemote(pricedTransaction(0, 100000, big.NewInt(threshold), key)); err != nil { @@ -2022,7 +2022,7 @@ func TestReplacement(t *testing.T) { if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(1), key)); err != nil { t.Fatalf("failed to add original cheap queued transaction: %v", err) } - if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(1), key)); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original cheap queued transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced) } if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(2), key)); err != nil { @@ -2032,7 +2032,7 @@ func TestReplacement(t *testing.T) { if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(price), key)); err != nil { t.Fatalf("failed to add original proper queued transaction: %v", err) } - if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(threshold-1), key)); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(pricedTransaction(2, 100001, big.NewInt(threshold-1), key)); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original proper queued transaction replacement error mismatch: have %v, want %v", err, txpool.ErrReplaceUnderpriced) } if err := pool.addRemote(pricedTransaction(2, 100000, big.NewInt(threshold), key)); err != nil { @@ -2096,7 +2096,7 @@ func TestReplacementDynamicFee(t *testing.T) { } // 2. Don't bump tip or feecap => discard tx = dynamicFeeTx(nonce, 100001, big.NewInt(2), big.NewInt(1), key) - if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original cheap %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced) } // 3. Bump both more than min => accept @@ -2117,24 +2117,25 @@ func TestReplacementDynamicFee(t *testing.T) { if err := pool.addRemoteSync(tx); err != nil { t.Fatalf("failed to add original proper %s transaction: %v", stage, err) } + // 6. Bump tip max allowed so it's still underpriced => discard tx = dynamicFeeTx(nonce, 100000, big.NewInt(gasFeeCap), big.NewInt(tipThreshold-1), key) - if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced) } // 7. Bump fee cap max allowed so it's still underpriced => discard tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold-1), big.NewInt(gasTipCap), key) - if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced) } // 8. Bump tip min for acceptance => accept tx = dynamicFeeTx(nonce, 100000, big.NewInt(gasFeeCap), big.NewInt(tipThreshold), key) - if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced) } // 9. Bump fee cap min for acceptance => accept tx = dynamicFeeTx(nonce, 100000, big.NewInt(feeCapThreshold), big.NewInt(gasTipCap), key) - if err := pool.addRemote(tx); err != txpool.ErrReplaceUnderpriced { + if err := pool.addRemote(tx); !errors.Is(err, txpool.ErrReplaceUnderpriced) { t.Fatalf("original proper %s transaction replacement error mismatch: have %v, want %v", stage, err, txpool.ErrReplaceUnderpriced) } // 10. Check events match expected (3 new executable txs during pending, 0 during queue) diff --git a/core/txpool/locals/errors.go b/core/txpool/locals/errors.go new file mode 100644 index 0000000000..fda50bf218 --- /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/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 + } +} diff --git a/core/txpool/locals/tx_tracker.go b/core/txpool/locals/tx_tracker.go index eccdcf422a..e08384ce71 100644 --- a/core/txpool/locals/tx_tracker.go +++ b/core/txpool/locals/tx_tracker.go @@ -74,32 +74,22 @@ func New(journalPath string, journalTime time.Duration, chainConfig *params.Chai // Track adds a transaction to the tracked set. // Note: blob-type transactions are ignored. -func (tracker *TxTracker) Track(tx *types.Transaction) error { - return tracker.TrackAll([]*types.Transaction{tx})[0] +func (tracker *TxTracker) Track(tx *types.Transaction) { + tracker.TrackAll([]*types.Transaction{tx}) } // TrackAll adds a list of transactions to the tracked set. // Note: blob-type transactions are ignored. -func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error { +func (tracker *TxTracker) TrackAll(txs []*types.Transaction) { tracker.mu.Lock() defer tracker.mu.Unlock() - var errors []error for _, tx := range txs { if tx.Type() == types.BlobTxType { - errors = append(errors, nil) - continue - } - // Ignore the transactions which are failed for fundamental - // validation such as invalid parameters. - if err := tracker.pool.ValidateTxBasics(tx); err != nil { - log.Debug("Invalid transaction submitted", "hash", tx.Hash(), "err", err) - errors = append(errors, err) continue } // If we're already tracking it, it's a no-op if _, ok := tracker.all[tx.Hash()]; ok { - errors = append(errors, nil) continue } // Theoretically, checking the error here is unnecessary since sender recovery @@ -108,11 +98,8 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error { // Therefore, the error is still checked just in case. addr, err := types.Sender(tracker.signer, tx) if err != nil { - errors = append(errors, err) continue } - errors = append(errors, nil) - tracker.all[tx.Hash()] = tx if tracker.byAddr[addr] == nil { tracker.byAddr[addr] = legacypool.NewSortedMap() @@ -124,7 +111,6 @@ func (tracker *TxTracker) TrackAll(txs []*types.Transaction) []error { } } localGauge.Update(int64(len(tracker.all))) - return errors } // recheck checks and returns any transactions that needs to be resubmitted. diff --git a/core/txpool/locals/tx_tracker_test.go b/core/txpool/locals/tx_tracker_test.go index 5585589b6c..0668d243fc 100644 --- a/core/txpool/locals/tx_tracker_test.go +++ b/core/txpool/locals/tx_tracker_test.go @@ -17,7 +17,6 @@ package locals import ( - "errors" "math/big" "testing" "time" @@ -91,10 +90,12 @@ func (env *testEnv) close() { env.chain.Stop() } +// nolint:unused func (env *testEnv) setGasTip(gasTip uint64) { env.pool.SetGasTip(new(big.Int).SetUint64(gasTip)) } +// nolint:unused func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction { if nonce == 0 { head := env.chain.CurrentHeader() @@ -121,6 +122,7 @@ func (env *testEnv) makeTxs(n int) []*types.Transaction { return txs } +// nolint:unused func (env *testEnv) commit() { head := env.chain.CurrentBlock() block := env.chain.GetBlock(head.Hash(), head.Number.Uint64()) @@ -137,60 +139,6 @@ func (env *testEnv) commit() { } } -func TestRejectInvalids(t *testing.T) { - env := newTestEnv(t, 10, 0, "") - defer env.close() - - var cases = []struct { - gasTip uint64 - tx *types.Transaction - expErr error - commit bool - }{ - { - tx: env.makeTx(5, nil), // stale - expErr: core.ErrNonceTooLow, - }, - { - tx: env.makeTx(11, nil), // future transaction - expErr: nil, - }, - { - gasTip: params.GWei, - tx: env.makeTx(0, new(big.Int).SetUint64(params.GWei/2)), // low price - expErr: txpool.ErrUnderpriced, - }, - { - tx: types.NewTransaction(10, common.Address{0x00}, big.NewInt(1000), params.TxGas, big.NewInt(params.GWei), nil), // invalid signature - expErr: types.ErrInvalidSig, - }, - { - commit: true, - tx: env.makeTx(10, nil), // stale - expErr: core.ErrNonceTooLow, - }, - { - tx: env.makeTx(11, nil), - expErr: nil, - }, - } - for i, c := range cases { - if c.gasTip != 0 { - env.setGasTip(c.gasTip) - } - if c.commit { - env.commit() - } - gotErr := env.tracker.Track(c.tx) - if c.expErr == nil && gotErr != nil { - t.Fatalf("%d, unexpected error: %v", i, gotErr) - } - if c.expErr != nil && !errors.Is(gotErr, c.expErr) { - t.Fatalf("%d, unexpected error, want: %v, got: %v", i, c.expErr, gotErr) - } - } -} - func TestResubmit(t *testing.T) { env := newTestEnv(t, 10, 0, "") defer env.close() diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 2ed38772ce..cc8f74c1b8 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -186,13 +186,15 @@ func (p *TxPool) loop(head *types.Header) { // Try to inject a busy marker and start a reset if successful select { case resetBusy <- struct{}{}: - statedb, err := p.chain.StateAt(newHead.Root) - if err != nil { - log.Crit("Failed to reset txpool state", "err", err) + // Updates the statedb with the new chain head. The head state may be + // unavailable if the initial state sync has not yet completed. + 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 go func(oldHead, newHead *types.Header) { @@ -322,31 +324,6 @@ func (p *TxPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Pr return nil, nil } -// ValidateTxBasics checks whether a transaction is valid according to the consensus -// rules, but does not check state-dependent validation such as sufficient balance. -func (p *TxPool) ValidateTxBasics(tx *types.Transaction) error { - addr, err := types.Sender(p.signer, tx) - if err != nil { - return err - } - // Reject transactions with stale nonce. Gapped-nonce future transactions - // are considered valid and will be handled by the subpool according to its - // internal policy. - p.stateLock.RLock() - nonce := p.state.GetNonce(addr) - p.stateLock.RUnlock() - - if nonce > tx.Nonce() { - return core.ErrNonceTooLow - } - for _, subpool := range p.subpools { - if subpool.Filter(tx) { - return subpool.ValidateTxBasics(tx) - } - } - return fmt.Errorf("%w: received type %d", core.ErrTxTypeNotSupported, tx.Type()) -} - // Add enqueues a batch of transactions into the pool if they are valid. Due // to the large transaction churn, add may postpone fully integrating the tx // to a later point to batch multiple ones together. diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 8747724247..e370f2ce84 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -131,12 +131,12 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the gasprice is high enough to cover the requirement of the calling pool if tx.GasTipCapIntCmp(opts.MinTip) < 0 { - return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrUnderpriced, tx.GasTipCap(), opts.MinTip) + return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip) } if tx.Type() == types.BlobTxType { // Ensure the blob fee cap satisfies the minimum blob gas price if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 { - return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrUnderpriced, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice) + return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice) } sidecar := tx.BlobTxSidecar() if sidecar == nil { diff --git a/eth/api_backend.go b/eth/api_backend.go index 182c081d2b..64fb58a1fd 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/txpool/locals" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/gasprice" @@ -307,19 +308,24 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri } func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { - locals := b.eth.localTxTracker - if locals != nil { - if err := locals.Track(signedTx); err != nil { - return err - } - } - // No error will be returned to user if the transaction fails stateful - // validation (e.g., no available slot), as the locally submitted transactions - // may be resubmitted later via the local tracker. err := b.eth.txPool.Add([]*types.Transaction{signedTx}, false)[0] - if err != nil && locals == nil { + + // If the local transaction tracker is not configured, returns whatever + // returned from the txpool. + if b.eth.localTxTracker == nil { return err } + // If the transaction fails with an error indicating it is invalid, or if there is + // very little chance it will be accepted later (e.g., the gas price is below the + // configured minimum, or the sender has insufficient funds to cover the cost), + // propagate the error to the user. + if err != nil && !locals.IsTemporaryReject(err) { + return err + } + // No error will be returned to user if the transaction fails with a temporary + // error and might be accepted later (e.g., the transaction pool is full). + // Locally submitted transactions will be resubmitted later via the local tracker. + b.eth.localTxTracker.Track(signedTx) return nil } diff --git a/eth/api_backend_test.go b/eth/api_backend_test.go new file mode 100644 index 0000000000..049f68d827 --- /dev/null +++ b/eth/api_backend_test.go @@ -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 . + +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) + } + } +} diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index dd9d8f9062..b84df9a4d6 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "errors" "fmt" + "math" "sync" "time" @@ -124,9 +125,13 @@ func NewSimulatedBeacon(period uint64, feeRecipient common.Address, eth *eth.Eth 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{ eth: eth, - period: period, + period: min(period, maxPeriod), shutdownCh: make(chan struct{}), engineAPI: engineAPI, lastBlockTime: block.Time, diff --git a/eth/fetcher/tx_fetcher.go b/eth/fetcher/tx_fetcher.go index 1c192d4112..ff17ae4945 100644 --- a/eth/fetcher/tx_fetcher.go +++ b/eth/fetcher/tx_fetcher.go @@ -345,7 +345,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) // Track the transaction hash if the price is too low for us. // Avoid re-request this transaction when we receive another // announcement. - if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) { + if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow) { f.underpriced.Add(batch[j].Hash(), batch[j].Time()) } // Track a few interesting failure types @@ -355,7 +355,7 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) case errors.Is(err, txpool.ErrAlreadyKnown): duplicate++ - case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced): + case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow): underpriced++ default: diff --git a/eth/fetcher/tx_fetcher_test.go b/eth/fetcher/tx_fetcher_test.go index 7f3080f5f6..c4c8cac56e 100644 --- a/eth/fetcher/tx_fetcher_test.go +++ b/eth/fetcher/tx_fetcher_test.go @@ -1244,10 +1244,12 @@ func TestTransactionFetcherUnderpricedDedup(t *testing.T) { func(txs []*types.Transaction) []error { errs := make([]error, len(txs)) for i := 0; i < len(errs); i++ { - if i%2 == 0 { + if i%3 == 0 { errs[i] = txpool.ErrUnderpriced - } else { + } else if i%3 == 1 { errs[i] = txpool.ErrReplaceUnderpriced + } else { + errs[i] = txpool.ErrTxGasPriceTooLow } } return errs diff --git a/eth/handler.go b/eth/handler.go index b2ad6effdb..8283d7d02f 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -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 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() hash = make([]byte, 32) ) diff --git a/ethclient/simulated/backend_test.go b/ethclient/simulated/backend_test.go index fc78e84362..303e480a09 100644 --- a/ethclient/simulated/backend_test.go +++ b/ethclient/simulated/backend_test.go @@ -25,16 +25,14 @@ import ( "testing" "time" - "go.uber.org/goleak" - - "github.com/ethereum/go-ethereum/crypto/kzg4844" - "github.com/holiman/uint256" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "go.uber.org/goleak" ) var _ bind.ContractBackend = (Client)(nil) diff --git a/params/config.go b/params/config.go index 8f9e02583b..67aa6b2225 100644 --- a/params/config.go +++ b/params/config.go @@ -60,10 +60,12 @@ var ( TerminalTotalDifficulty: MainnetTerminalTotalDifficulty, // 58_750_000_000_000_000_000_000 ShanghaiTime: newUint64(1681338455), CancunTime: newUint64(1710338135), + PragueTime: newUint64(1746612311), DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"), Ethash: new(EthashConfig), BlobScheduleConfig: &BlobScheduleConfig{ Cancun: DefaultCancunBlobConfig, + Prague: DefaultPragueBlobConfig, }, } // 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 // rescheduled to block s2 because head is already past the fork. func isForkBlockIncompatible(s1, s2, head *big.Int) bool { diff --git a/params/forks/forks.go b/params/forks/forks.go index 02f6e5b612..5c9612a625 100644 --- a/params/forks/forks.go +++ b/params/forks/forks.go @@ -20,7 +20,7 @@ package forks type Fork int const ( - Frontier = iota + Frontier Fork = iota FrontierThawing Homestead DAO @@ -41,3 +41,35 @@ const ( Prague 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", +}