Repository: ethereum/go-ethereum
Pull request #31752: core: reduce load on txindexer from API
Author: login: s1na
Created: 2025-05-01T12:59:28Z | Merged: 2025-05-05T02:07:55Z

### Context

Fixes https://github.com/ethereum/go-ethereum/issues/31732.

This logic was removed in the recent refactoring in the txindexer to handle history cutoff (#31393). It was first introduced in this PR: https://github.com/ethereum/go-ethereum/pull/28908.

I have tested it and it works as an alternative to #31745.

### Content

This PR packs 3 changes to the flow of fetching txes from the API:

- It caches the indexer tail after each run is over to avoid hitting the db all the time as was done originally in #28908.
- It propagates the API context to the indexer and handles cancellation. This is to allow goroutines to be cleaned up when a request times out.
- Changes `backend.GetTransaction`. It doesn't return an error anymore when tx indexer is in progress. It shifts the responsibility to the caller to check the progress. The reason is that in most cases we anyway check the txpool for the tx. If it was indeed a pending tx we can avoid the indexer progress check.


Diff:
diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go
index b4ba5d9fd88b..fefeb3754245 100644
--- a/core/blockchain_reader.go
+++ b/core/blockchain_reader.go
@@ -270,42 +270,20 @@ func (bc *BlockChain) GetAncestor(hash common.Hash, number, ancestor uint64, max
 // GetTransactionLookup retrieves the lookup along with the transaction
 // itself associate with the given transaction hash.
 //
-// An error will be returned if the transaction is not found, and background
-// indexing for transactions is still in progress. The transaction might be
-// reachable shortly once it's indexed.
-//
-// A null will be returned in the transaction is not found and background
-// transaction indexing is already finished. The transaction is not existent
-// from the node's perspective.
-func (bc *BlockChain) GetTransactionLookup(hash common.Hash) (*rawdb.LegacyTxLookupEntry, *types.Transaction, error) {
+// A null will be returned if the transaction is not found. This can be due to
+// the transaction indexer not being finished. The caller must explicitly check
+// the indexer progress.
+func (bc *BlockChain) GetTransactionLookup(hash common.Hash) (*rawdb.LegacyTxLookupEntry, *types.Transaction) {
 	bc.txLookupLock.RLock()
 	defer bc.txLookupLock.RUnlock()
 
 	// Short circuit if the txlookup already in the cache, retrieve otherwise
 	if item, exist := bc.txLookupCache.Get(hash); exist {
-		return item.lookup, item.transaction, nil
+		return item.lookup, item.transaction
 	}
 	tx, blockHash, blockNumber, txIndex := rawdb.ReadTransaction(bc.db, hash)
 	if tx == nil {
-		progress, err := bc.TxIndexProgress()
-		if err != nil {
-			// No error is returned if the transaction indexing progress is unreachable
-			// due to unexpected internal errors. In such cases, it is impossible to
-			// determine whether the transaction does not exist or has simply not been
-			// indexed yet without a progress marker.
-			//
-			// In such scenarios, the transaction is treated as unreachable, though
-			// this is clearly an unintended and unexpected situation.
-			return nil, nil, nil
-		}
-		// The transaction indexing is not finished yet, returning an
-		// error to explicitly indicate it.
-		if !progress.Done() {
-			return nil, nil, errors.New("transaction indexing still in progress")
-		}
-		// The transaction is already indexed, the transaction is either
-		// not existent or not in the range of index, returning null.
-		return nil, nil, nil
+		return nil, nil
 	}
 	lookup := &rawdb.LegacyTxLookupEntry{
 		BlockHash:  blockHash,
@@ -316,7 +294,23 @@ func (bc *BlockChain) GetTransactionLookup(hash common.Hash) (*rawdb.LegacyTxLoo
 		lookup:      lookup,
 		transaction: tx,
 	})
-	return lookup, tx, nil
+	return lookup, tx
+}
+
+// TxIndexDone returns true if the transaction indexer has finished indexing.
+func (bc *BlockChain) TxIndexDone() bool {
+	progress, err := bc.TxIndexProgress()
+	if err != nil {
+		// No error is returned if the transaction indexing progress is unreachable
+		// due to unexpected internal errors. In such cases, it is impossible to
+		// determine whether the transaction does not exist or has simply not been
+		// indexed yet without a progress marker.
+		//
+		// In such scenarios, the transaction is treated as unreachable, though
+		// this is clearly an unintended and unexpected situation.
+		return true
+	}
+	return progress.Done()
 }
 
 // HasState checks if state trie is fully present in the database or not.
@@ -412,7 +406,7 @@ func (bc *BlockChain) TxIndexProgress() (TxIndexProgress, error) {
 	if bc.txIndexer == nil {
 		return TxIndexProgress{}, errors.New("tx indexer is not enabled")
 	}
-	return bc.txIndexer.txIndexProgress()
+	return bc.txIndexer.txIndexProgress(), nil
 }
 
 // HistoryPruningCutoff returns the configured history pruning point.
diff --git a/core/txindexer.go b/core/txindexer.go
index 64a2e8c49f55..587118ed7f3d 100644
--- a/core/txindexer.go
+++ b/core/txindexer.go
@@ -17,8 +17,8 @@
 package core
 
 import (
-	"errors"
 	"fmt"
+	"sync/atomic"
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/rawdb"
@@ -47,26 +47,38 @@ type txIndexer struct {
 	//       and all others shouldn't.
 	limit uint64
 
+	// The current head of blockchain for transaction indexing. This field
+	// is accessed by both the indexer and the indexing progress queries.
+	head atomic.Uint64
+
+	// The current tail of the indexed transactions, null indicates
+	// that no transactions have been indexed yet.
+	//
+	// This field is accessed by both the indexer and the indexing
+	// progress queries.
+	tail atomic.Pointer[uint64]
+
 	// cutoff denotes the block number before which the chain segment should
 	// be pruned and not available locally.
-	cutoff   uint64
-	db       ethdb.Database
-	progress chan chan TxIndexProgress
-	term     chan chan struct{}
-	closed   chan struct{}
+	cutoff uint64
+	db     ethdb.Database
+	term   chan chan struct{}
+	closed chan struct{}
 }
 
 // newTxIndexer initializes the transaction indexer.
 func newTxIndexer(limit uint64, chain *BlockChain) *txIndexer {
 	cutoff, _ := chain.HistoryPruningCutoff()
 	indexer := &txIndexer{
-		limit:    limit,
-		cutoff:   cutoff,
-		db:       chain.db,
-		progress: make(chan chan TxIndexProgress),
-		term:     make(chan chan struct{}),
-		closed:   make(chan struct{}),
+		limit:  limit,
+		cutoff: cutoff,
+		db:     chain.db,
+		term:   make(chan chan struct{}),
+		closed: make(chan struct{}),
 	}
+	indexer.head.Store(indexer.resolveHead())
+	indexer.tail.Store(rawdb.ReadTxIndexTail(chain.db))
+
 	go indexer.loop(chain)
 
 	var msg string
@@ -154,6 +166,7 @@ func (indexer *txIndexer) repair(head uint64) {
 		// A crash may occur between the two delete operations,
 		// potentially leaving dangling indexes in the database.
 		// However, this is considered acceptable.
+		indexer.tail.Store(nil)
 		rawdb.DeleteTxIndexTail(indexer.db)
 		rawdb.DeleteAllTxLookupEntries(indexer.db, nil)
 		log.Warn("Purge transaction indexes", "head", head, "tail", *tail)
@@ -174,6 +187,7 @@ func (indexer *txIndexer) repair(head uint64) {
 		// Traversing the database directly within the transaction
 		// index namespace might be slow and expensive, but we
 		// have no choice.
+		indexer.tail.Store(nil)
 		rawdb.DeleteTxIndexTail(indexer.db)
 		rawdb.DeleteAllTxLookupEntries(indexer.db, nil)
 		log.Warn("Purge transaction indexes", "head", head, "cutoff", indexer.cutoff)
@@ -187,6 +201,7 @@ func (indexer *txIndexer) repair(head uint64) {
 		// A crash may occur between the two delete operations,
 		// potentially leaving dangling indexes in the database.
 		// However, this is considered acceptable.
+		indexer.tail.Store(&indexer.cutoff)
 		rawdb.WriteTxIndexTail(indexer.db, indexer.cutoff)
 		rawdb.DeleteAllTxLookupEntries(indexer.db, func(txhash common.Hash, blob []byte) bool {
 			n := rawdb.DecodeTxLookupEntry(blob, indexer.db)
@@ -216,16 +231,15 @@ 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 = indexer.resolveHead() // 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
 		headCh = make(chan ChainHeadEvent)
 		sub    = chain.SubscribeChainHeadEvent(headCh)
 	)
 	defer sub.Unsubscribe()
 
 	// Validate the transaction indexes and repair if necessary
+	head := indexer.head.Load()
 	indexer.repair(head)
 
 	// Launch the initial processing if chain is not empty (head != genesis).
@@ -238,17 +252,18 @@ func (indexer *txIndexer) loop(chain *BlockChain) {
 	for {
 		select {
 		case h := <-headCh:
+			indexer.head.Store(h.Header.Number.Uint64())
 			if done == nil {
 				stop = make(chan struct{})
 				done = make(chan struct{})
 				go indexer.run(h.Header.Number.Uint64(), stop, done)
 			}
-			head = h.Header.Number.Uint64()
+
 		case <-done:
 			stop = nil
 			done = nil
-		case ch := <-indexer.progress:
-			ch <- indexer.report(head)
+			indexer.tail.Store(rawdb.ReadTxIndexTail(indexer.db))
+
 		case ch := <-indexer.term:
 			if stop != nil {
 				close(stop)
@@ -264,7 +279,7 @@ func (indexer *txIndexer) loop(chain *BlockChain) {
 }
 
 // report returns the tx indexing progress.
-func (indexer *txIndexer) report(head uint64) TxIndexProgress {
+func (indexer *txIndexer) report(head uint64, tail *uint64) TxIndexProgress {
 	// Special case if the head is even below the cutoff,
 	// nothing to index.
 	if head < indexer.cutoff {
@@ -284,7 +299,6 @@ func (indexer *txIndexer) report(head uint64) TxIndexProgress {
 	}
 	// Compute how many blocks have been indexed
 	var indexed uint64
-	tail := rawdb.ReadTxIndexTail(indexer.db)
 	if tail != nil {
 		indexed = head - *tail + 1
 	}
@@ -300,16 +314,12 @@ func (indexer *txIndexer) report(head uint64) TxIndexProgress {
 	}
 }
 
-// txIndexProgress retrieves the tx indexing progress, or an error if the
-// background tx indexer is already stopped.
-func (indexer *txIndexer) txIndexProgress() (TxIndexProgress, error) {
-	ch := make(chan TxIndexProgress, 1)
-	select {
-	case indexer.progress <- ch:
-		return <-ch, nil
-	case <-indexer.closed:
-		return TxIndexProgress{}, errors.New("indexer is closed")
-	}
+// txIndexProgress retrieves the transaction indexing progress. The reported
+// progress may slightly lag behind the actual indexing state, as the tail is
+// only updated at the end of each indexing operation. However, this delay is
+// considered acceptable.
+func (indexer *txIndexer) txIndexProgress() TxIndexProgress {
+	return indexer.report(indexer.head.Load(), indexer.tail.Load())
 }
 
 // close shutdown the indexer. Safe to be called for multiple times.
diff --git a/core/txindexer_test.go b/core/txindexer_test.go
index 7a5688241f06..6543ff429dc4 100644
--- a/core/txindexer_test.go
+++ b/core/txindexer_test.go
@@ -121,9 +121,8 @@ func TestTxIndexer(t *testing.T) {
 
 		// Index the initial blocks from ancient store
 		indexer := &txIndexer{
-			limit:    0,
-			db:       db,
-			progress: make(chan chan TxIndexProgress),
+			limit: 0,
+			db:    db,
 		}
 		for i, limit := range c.limits {
 			indexer.limit = limit
@@ -241,9 +240,8 @@ func TestTxIndexerRepair(t *testing.T) {
 
 		// Index the initial blocks from ancient store
 		indexer := &txIndexer{
-			limit:    c.limit,
-			db:       db,
-			progress: make(chan chan TxIndexProgress),
+			limit: c.limit,
+			db:    db,
 		}
 		indexer.run(chainHead, make(chan struct{}), make(chan struct{}))
 
@@ -432,15 +430,11 @@ func TestTxIndexerReport(t *testing.T) {
 
 		// Index the initial blocks from ancient store
 		indexer := &txIndexer{
-			limit:    c.limit,
-			cutoff:   c.cutoff,
-			db:       db,
-			progress: make(chan chan TxIndexProgress),
+			limit:  c.limit,
+			cutoff: c.cutoff,
+			db:     db,
 		}
-		if c.tail != nil {
-			rawdb.WriteTxIndexTail(db, *c.tail)
-		}
-		p := indexer.report(c.head)
+		p := indexer.report(c.head, c.tail)
 		if p.Indexed != c.expIndexed {
 			t.Fatalf("Unexpected indexed: %d, expected: %d", p.Indexed, c.expIndexed)
 		}
diff --git a/eth/api_backend.go b/eth/api_backend.go
index 10f7ffcbce7d..57f5a5083744 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -349,22 +349,20 @@ func (b *EthAPIBackend) GetPoolTransaction(hash common.Hash) *types.Transaction
 // GetTransaction retrieves the lookup along with the transaction itself associate
 // with the given transaction hash.
 //
-// An error will be returned if the transaction is not found, and background
-// indexing for transactions is still in progress. The error is used to indicate the
-// scenario explicitly that the transaction might be reachable shortly.
-//
-// A null will be returned in the transaction is not found and background transaction
-// indexing is already finished. The transaction is not existent from the perspective
-// of node.
-func (b *EthAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) {
-	lookup, tx, err := b.eth.blockchain.GetTransactionLookup(txHash)
-	if err != nil {
-		return false, nil, common.Hash{}, 0, 0, err
-	}
+// A null will be returned if the transaction is not found. The transaction is not
+// existent from the node's perspective. This can be due to the transaction indexer
+// not being finished. The caller must explicitly check the indexer progress.
+func (b *EthAPIBackend) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) {
+	lookup, tx := b.eth.blockchain.GetTransactionLookup(txHash)
 	if lookup == nil || tx == nil {
-		return false, nil, common.Hash{}, 0, 0, nil
+		return false, nil, common.Hash{}, 0, 0
 	}
-	return true, tx, lookup.BlockHash, lookup.BlockIndex, lookup.Index, nil
+	return true, tx, lookup.BlockHash, lookup.BlockIndex, lookup.Index
+}
+
+// TxIndexDone returns true if the transaction indexer has finished indexing.
+func (b *EthAPIBackend) TxIndexDone() bool {
+	return b.eth.blockchain.TxIndexDone()
 }
 
 func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) {
@@ -391,7 +389,7 @@ func (b *EthAPIBackend) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.S
 	return b.eth.txPool.SubscribeTransactions(ch, true)
 }
 
-func (b *EthAPIBackend) SyncProgress() ethereum.SyncProgress {
+func (b *EthAPIBackend) SyncProgress(ctx context.Context) ethereum.SyncProgress {
 	prog := b.eth.Downloader().Progress()
 	if txProg, err := b.eth.blockchain.TxIndexProgress(); err == nil {
 		prog.TxIndexFinishedBlocks = txProg.Indexed
diff --git a/eth/tracers/api.go b/eth/tracers/api.go
index 3cb86c80e263..17a0ad687a89 100644
--- a/eth/tracers/api.go
+++ b/eth/tracers/api.go
@@ -82,7 +82,8 @@ type Backend interface {
 	HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
 	BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
 	BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
-	GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error)
+	GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64)
+	TxIndexDone() bool
 	RPCGasCap() uint64
 	ChainConfig() *params.ChainConfig
 	Engine() consensus.Engine
@@ -858,12 +859,13 @@ func containsTx(block *types.Block, hash common.Hash) bool {
 // TraceTransaction returns the structured logs created during the execution of EVM
 // and returns them as a JSON object.
 func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *TraceConfig) (interface{}, error) {
-	found, _, blockHash, blockNumber, index, err := api.backend.GetTransaction(ctx, hash)
-	if err != nil {
-		return nil, ethapi.NewTxIndexingError()
-	}
-	// Only mined txes are supported
+	found, _, blockHash, blockNumber, index := api.backend.GetTransaction(hash)
 	if !found {
+		// Warn in case tx indexer is not done.
+		if !api.backend.TxIndexDone() {
+			return nil, ethapi.NewTxIndexingError()
+		}
+		// Only mined txes are supported
 		return nil, errTxNotFound
 	}
 	// It shouldn't happen in practice.
diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go
index 529448e39727..fa3918769422 100644
--- a/eth/tracers/api_test.go
+++ b/eth/tracers/api_test.go
@@ -116,9 +116,13 @@ func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber)
 	return b.chain.GetBlockByNumber(uint64(number)), nil
 }
 
-func (b *testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) {
+func (b *testBackend) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) {
 	tx, hash, blockNumber, index := rawdb.ReadTransaction(b.chaindb, txHash)
-	return tx != nil, tx, hash, blockNumber, index, nil
+	return tx != nil, tx, hash, blockNumber, index
+}
+
+func (b *testBackend) TxIndexDone() bool {
+	return true
 }
 
 func (b *testBackend) RPCGasCap() uint64 {
diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go
index 0090a7d4c1d1..b6191baa12b9 100644
--- a/ethstats/ethstats.go
+++ b/ethstats/ethstats.go
@@ -66,7 +66,7 @@ type backend interface {
 	CurrentHeader() *types.Header
 	HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
 	Stats() (pending int, queued int)
-	SyncProgress() ethereum.SyncProgress
+	SyncProgress(ctx context.Context) ethereum.SyncProgress
 }
 
 // fullNodeBackend encompasses the functionality necessary for a full node
@@ -766,7 +766,7 @@ func (s *Service) reportStats(conn *connWrapper) error {
 	)
 	// check if backend is a full node
 	if fullBackend, ok := s.backend.(fullNodeBackend); ok {
-		sync := fullBackend.SyncProgress()
+		sync := fullBackend.SyncProgress(context.Background())
 		syncing = !sync.Done()
 
 		price, _ := fullBackend.SuggestGasTipCap(context.Background())
@@ -775,7 +775,7 @@ func (s *Service) reportStats(conn *connWrapper) error {
 			gasprice += int(basefee.Uint64())
 		}
 	} else {
-		sync := s.backend.SyncProgress()
+		sync := s.backend.SyncProgress(context.Background())
 		syncing = !sync.Done()
 	}
 	// Assemble the node stats and send it to the server
diff --git a/graphql/graphql.go b/graphql/graphql.go
index 7af1adbb4a8e..e23e6fcb0e6e 100644
--- a/graphql/graphql.go
+++ b/graphql/graphql.go
@@ -229,7 +229,7 @@ func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, *Block)
 		return t.tx, t.block
 	}
 	// Try to return an already finalized transaction
-	found, tx, blockHash, _, index, _ := t.r.backend.GetTransaction(ctx, t.hash)
+	found, tx, blockHash, _, index := t.r.backend.GetTransaction(t.hash)
 	if found {
 		t.tx = tx
 		blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, false)
@@ -1530,8 +1530,8 @@ func (s *SyncState) TxIndexRemainingBlocks() hexutil.Uint64 {
 // - healingBytecode:     number of bytecodes pending
 // - txIndexFinishedBlocks:  number of blocks whose transactions are indexed
 // - txIndexRemainingBlocks: number of blocks whose transactions are not indexed yet
-func (r *Resolver) Syncing() (*SyncState, error) {
-	progress := r.backend.SyncProgress()
+func (r *Resolver) Syncing(ctx context.Context) (*SyncState, error) {
+	progress := r.backend.SyncProgress(ctx)
 
 	// Return not syncing if the synchronisation already completed
 	if progress.Done() {
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 3b699748b898..8f736226c71b 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -144,8 +144,8 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big {
 // - highestBlock:  block number of the highest block header this node has received from peers
 // - pulledStates:  number of state entries processed until now
 // - knownStates:   number of known state entries that still need to be pulled
-func (api *EthereumAPI) Syncing() (interface{}, error) {
-	progress := api.b.SyncProgress()
+func (api *EthereumAPI) Syncing(ctx context.Context) (interface{}, error) {
+	progress := api.b.SyncProgress(ctx)
 
 	// Return not syncing if the synchronisation already completed
 	if progress.Done() {
@@ -1333,16 +1333,18 @@ func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address comm
 // GetTransactionByHash returns the transaction for the given hash
 func (api *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*RPCTransaction, error) {
 	// Try to return an already finalized transaction
-	found, tx, blockHash, blockNumber, index, err := api.b.GetTransaction(ctx, hash)
+	found, tx, blockHash, blockNumber, index := api.b.GetTransaction(hash)
 	if !found {
 		// No finalized transaction, try to retrieve it from the pool
 		if tx := api.b.GetPoolTransaction(hash); tx != nil {
 			return NewRPCPendingTransaction(tx, api.b.CurrentHeader(), api.b.ChainConfig()), nil
 		}
-		if err == nil {
-			return nil, nil
+		// If also not in the pool there is a chance the tx indexer is still in progress.
+		if !api.b.TxIndexDone() {
+			return nil, NewTxIndexingError()
 		}
-		return nil, NewTxIndexingError()
+		// If the transaction is not found in the pool and the indexer is done, return nil
+		return nil, nil
 	}
 	header, err := api.b.HeaderByHash(ctx, blockHash)
 	if err != nil {
@@ -1354,27 +1356,31 @@ func (api *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common
 // GetRawTransactionByHash returns the bytes of the transaction for the given hash.
 func (api *TransactionAPI) GetRawTransactionByHash(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) {
 	// Retrieve a finalized transaction, or a pooled otherwise
-	found, tx, _, _, _, err := api.b.GetTransaction(ctx, hash)
+	found, tx, _, _, _ := api.b.GetTransaction(hash)
 	if !found {
 		if tx = api.b.GetPoolTransaction(hash); tx != nil {
 			return tx.MarshalBinary()
 		}
-		if err == nil {
-			return nil, nil
+		// If also not in the pool there is a chance the tx indexer is still in progress.
+		if !api.b.TxIndexDone() {
+			return nil, NewTxIndexingError()
 		}
-		return nil, NewTxIndexingError()
+		// If the transaction is not found in the pool and the indexer is done, return nil
+		return nil, nil
 	}
 	return tx.MarshalBinary()
 }
 
 // GetTransactionReceipt returns the transaction receipt for the given transaction hash.
 func (api *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (map[string]interface{}, error) {
-	found, tx, blockHash, blockNumber, index, err := api.b.GetTransaction(ctx, hash)
-	if err != nil {
-		return nil, NewTxIndexingError() // transaction is not fully indexed
-	}
+	found, tx, blockHash, blockNumber, index := api.b.GetTransaction(hash)
 	if !found {
-		return nil, nil // transaction is not existent or reachable
+		// Make sure indexer is done.
+		if !api.b.TxIndexDone() {
+			return nil, NewTxIndexingError()
+		}
+		// No such tx.
+		return nil, nil
 	}
 	header, err := api.b.HeaderByHash(ctx, blockHash)
 	if err != nil {
@@ -1774,15 +1780,17 @@ func (api *DebugAPI) GetRawReceipts(ctx context.Context, blockNrOrHash rpc.Block
 // GetRawTransaction returns the bytes of the transaction for the given hash.
 func (api *DebugAPI) GetRawTransaction(ctx context.Context, hash common.Hash) (hexutil.Bytes, error) {
 	// Retrieve a finalized transaction, or a pooled otherwise
-	found, tx, _, _, _, err := api.b.GetTransaction(ctx, hash)
+	found, tx, _, _, _ := api.b.GetTransaction(hash)
 	if !found {
 		if tx = api.b.GetPoolTransaction(hash); tx != nil {
 			return tx.MarshalBinary()
 		}
-		if err == nil {
-			return nil, nil
+		// If also not in the pool there is a chance the tx indexer is still in progress.
+		if !api.b.TxIndexDone() {
+			return nil, NewTxIndexingError()
 		}
-		return nil, NewTxIndexingError()
+		// Transaction is not found in the pool and the indexer is done.
+		return nil, nil
 	}
 	return tx.MarshalBinary()
 }
diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go
index 5071e2412f0d..ef799d999452 100644
--- a/internal/ethapi/api_test.go
+++ b/internal/ethapi/api_test.go
@@ -472,7 +472,9 @@ func (b *testBackend) setPendingBlock(block *types.Block) {
 	b.pending = block
 }
 
-func (b testBackend) SyncProgress() ethereum.SyncProgress { return ethereum.SyncProgress{} }
+func (b testBackend) SyncProgress(ctx context.Context) ethereum.SyncProgress {
+	return ethereum.SyncProgress{}
+}
 func (b testBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
 	return big.NewInt(0), nil
 }
@@ -589,9 +591,12 @@ func (b testBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) even
 func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
 	panic("implement me")
 }
-func (b testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) {
+func (b testBackend) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) {
 	tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.db, txHash)
-	return true, tx, blockHash, blockNumber, index, nil
+	return true, tx, blockHash, blockNumber, index
+}
+func (b testBackend) TxIndexDone() bool {
+	return true
 }
 func (b testBackend) GetPoolTransactions() (types.Transactions, error)         { panic("implement me") }
 func (b testBackend) GetPoolTransaction(txHash common.Hash) *types.Transaction { panic("implement me") }
diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go
index e28cb9329626..49c3a37560ff 100644
--- a/internal/ethapi/backend.go
+++ b/internal/ethapi/backend.go
@@ -41,7 +41,7 @@ import (
 // both full and light clients) with access to necessary functions.
 type Backend interface {
 	// General Ethereum API
-	SyncProgress() ethereum.SyncProgress
+	SyncProgress(ctx context.Context) ethereum.SyncProgress
 
 	SuggestGasTipCap(ctx context.Context) (*big.Int, error)
 	FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error)
@@ -74,7 +74,8 @@ type Backend interface {
 
 	// Transaction pool API
 	SendTx(ctx context.Context, signedTx *types.Transaction) error
-	GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error)
+	GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64)
+	TxIndexDone() bool
 	GetPoolTransactions() (types.Transactions, error)
 	GetPoolTransaction(txHash common.Hash) *types.Transaction
 	GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error)
diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go
index 9dd6a547290e..9b86e452a5be 100644
--- a/internal/ethapi/transaction_args_test.go
+++ b/internal/ethapi/transaction_args_test.go
@@ -323,7 +323,9 @@ func (b *backendMock) CurrentHeader() *types.Header     { return b.current }
 func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config }
 
 // Other methods needed to implement Backend interface.
-func (b *backendMock) SyncProgress() ethereum.SyncProgress { return ethereum.SyncProgress{} }
+func (b *backendMock) SyncProgress(ctx context.Context) ethereum.SyncProgress {
+	return ethereum.SyncProgress{}
+}
 func (b *backendMock) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) {
 	return nil, nil, nil, nil, nil, nil, nil
 }
@@ -378,9 +380,10 @@ func (b *backendMock) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) eve
 	return nil
 }
 func (b *backendMock) SendTx(ctx context.Context, signedTx *types.Transaction) error { return nil }
-func (b *backendMock) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) {
-	return false, nil, [32]byte{}, 0, 0, nil
+func (b *backendMock) GetTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) {
+	return false, nil, [32]byte{}, 0, 0
 }
+func (b *backendMock) TxIndexDone() bool                                        { return true }
 func (b *backendMock) GetPoolTransactions() (types.Transactions, error)         { return nil, nil }
 func (b *backendMock) GetPoolTransaction(txHash common.Hash) *types.Transaction { return nil }
 func (b *backendMock) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) {


Reviews:
user: login: rjl493456442
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-02T13:33:16Z
user: login: rjl493456442
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-04T07:46:31Z
user: login: s1na
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T12:23:16Z
user: login: rjl493456442
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T14:49:31Z
user: login: rjl493456442
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T14:59:00Z
user: login: rjl493456442
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T02:06:54Z

Review comments:
diff_hunk: @@ -300,16 +309,12 @@ func (indexer *txIndexer) report(head uint64) TxIndexProgress {
 	}
 }
 
-// txIndexProgress retrieves the tx indexing progress, or an error if the
-// background tx indexer is already stopped.
-func (indexer *txIndexer) txIndexProgress() (TxIndexProgress, error) {
-	ch := make(chan TxIndexProgress, 1)
-	select {
-	case indexer.progress <- ch:
-		return <-ch, nil
-	case <-indexer.closed:
-		return TxIndexProgress{}, errors.New("indexer is closed")
-	}
+// txIndexProgress retrieves the transaction indexing progress. The reported
+// progress may slightly lag behind the actual indexing state, as the tail is
+// only updated at the end of each indexing operation. However, this delay is
+// considered acceptable.
+func (indexer *txIndexer) txIndexProgress() TxIndexProgress {
+	return indexer.report(indexer.resolveHead(), indexer.tail.Load())
path: core/txindexer.go
user: login: s1na
body: This will translate to 2 db accesses because the head is not cached. I will try this on my node to see what happens.
created_at: 2025-05-04T12:23:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,16 +309,12 @@ func (indexer *txIndexer) report(head uint64) TxIndexProgress {
 	}
 }
 
-// txIndexProgress retrieves the tx indexing progress, or an error if the
-// background tx indexer is already stopped.
-func (indexer *txIndexer) txIndexProgress() (TxIndexProgress, error) {
-	ch := make(chan TxIndexProgress, 1)
-	select {
-	case indexer.progress <- ch:
-		return <-ch, nil
-	case <-indexer.closed:
-		return TxIndexProgress{}, errors.New("indexer is closed")
-	}
+// txIndexProgress retrieves the transaction indexing progress. The reported
+// progress may slightly lag behind the actual indexing state, as the tail is
+// only updated at the end of each indexing operation. However, this delay is
+// considered acceptable.
+func (indexer *txIndexer) txIndexProgress() TxIndexProgress {
+	return indexer.report(indexer.resolveHead(), indexer.tail.Load())
path: core/txindexer.go
user: login: rjl493456442
body: oh no.. you are right.

I think we need to avoid the db accesses, let me try to fix it.
created_at: 2025-05-04T14:49:31Z
author_association: MEMBER
diff_hunk: @@ -300,16 +309,12 @@ func (indexer *txIndexer) report(head uint64) TxIndexProgress {
 	}
 }
 
-// txIndexProgress retrieves the tx indexing progress, or an error if the
-// background tx indexer is already stopped.
-func (indexer *txIndexer) txIndexProgress() (TxIndexProgress, error) {
-	ch := make(chan TxIndexProgress, 1)
-	select {
-	case indexer.progress <- ch:
-		return <-ch, nil
-	case <-indexer.closed:
-		return TxIndexProgress{}, errors.New("indexer is closed")
-	}
+// txIndexProgress retrieves the transaction indexing progress. The reported
+// progress may slightly lag behind the actual indexing state, as the tail is
+// only updated at the end of each indexing operation. However, this delay is
+// considered acceptable.
+func (indexer *txIndexer) txIndexProgress() TxIndexProgress {
+	return indexer.report(indexer.resolveHead(), indexer.tail.Load())
path: core/txindexer.go
user: login: rjl493456442
body: fixed
created_at: 2025-05-04T14:58:59Z
author_association: MEMBER

PR comments:
user: login: rjl493456442
created_at: 2025-05-02T13:34:03Z
body: The error handling for whether the transaction indexer is running is much improved compared to before.

SGTM!

author_association: MEMBER
user: login: s1na
created_at: 2025-05-02T16:01:36Z
body: Tested the PR on my node. It's fast!
author_association: CONTRIBUTOR
user: login: s1na
created_at: 2025-05-04T13:09:47Z
body: With the latest changes. Still apart from some spikes goroutine under control. So IMO it's good to go.

![Screenshot 2025-05-04 at 15 06 39](https://github.com/user-attachments/assets/3068cf6e-3a33-4643-9f2b-520196f4bffa)

I did notice some blocks taking very long in the logs when the script from https://github.com/ethereum/go-ethereum/issues/31732#issuecomment-2848623632 is running as the node is being hammered with requests. So I can imagine why engine API requests might time out if the OP's node is not as powerful.

```
INFO [05-04|13:01:35.218] Imported new potential chain segment     number=22,410,559 hash=d134fa..c732a1 blocks=1 txs=187 mgas=35.981 elapsed=8.266s      mgasps=4.353   snapdiffs=7.08MiB triediffs=228.70MiB triedirty=157.61MiB

INFO [05-04|13:06:30.010] Imported new potential chain segment     number=22,410,584 hash=68e4d8..aa6140 blocks=1 txs=180 mgas=34.197 elapsed=4.928s       mgasps=6.938   snapdiffs=8.21MiB triediffs=229.50MiB triedirty=170.21MiB
```
author_association: CONTRIBUTOR