github.com/ethereum/go-ethereum@v1.16.1/core/txpool/locals/tx_tracker.go (about)

     1  // Copyright 2023 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // Package locals implements tracking for "local" transactions
    18  package locals
    19  
    20  import (
    21  	"slices"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/ethereum/go-ethereum/common"
    26  	"github.com/ethereum/go-ethereum/core/txpool"
    27  	"github.com/ethereum/go-ethereum/core/txpool/legacypool"
    28  	"github.com/ethereum/go-ethereum/core/types"
    29  	"github.com/ethereum/go-ethereum/log"
    30  	"github.com/ethereum/go-ethereum/metrics"
    31  	"github.com/ethereum/go-ethereum/params"
    32  )
    33  
    34  var (
    35  	recheckInterval = time.Minute
    36  	localGauge      = metrics.GetOrRegisterGauge("txpool/local", nil)
    37  )
    38  
    39  // TxTracker is a struct used to track priority transactions; it will check from
    40  // time to time if the main pool has forgotten about any of the transaction
    41  // it is tracking, and if so, submit it again.
    42  // This is used to track 'locals'.
    43  // This struct does not care about transaction validity, price-bumps or account limits,
    44  // but optimistically accepts transactions.
    45  type TxTracker struct {
    46  	all    map[common.Hash]*types.Transaction       // All tracked transactions
    47  	byAddr map[common.Address]*legacypool.SortedMap // Transactions by address
    48  
    49  	journal   *journal       // Journal of local transaction to back up to disk
    50  	rejournal time.Duration  // How often to rotate journal
    51  	pool      *txpool.TxPool // The tx pool to interact with
    52  	signer    types.Signer
    53  
    54  	shutdownCh chan struct{}
    55  	mu         sync.Mutex
    56  	wg         sync.WaitGroup
    57  }
    58  
    59  // New creates a new TxTracker
    60  func New(journalPath string, journalTime time.Duration, chainConfig *params.ChainConfig, next *txpool.TxPool) *TxTracker {
    61  	pool := &TxTracker{
    62  		all:        make(map[common.Hash]*types.Transaction),
    63  		byAddr:     make(map[common.Address]*legacypool.SortedMap),
    64  		signer:     types.LatestSigner(chainConfig),
    65  		shutdownCh: make(chan struct{}),
    66  		pool:       next,
    67  	}
    68  	if journalPath != "" {
    69  		pool.journal = newTxJournal(journalPath)
    70  		pool.rejournal = journalTime
    71  	}
    72  	return pool
    73  }
    74  
    75  // Track adds a transaction to the tracked set.
    76  // Note: blob-type transactions are ignored.
    77  func (tracker *TxTracker) Track(tx *types.Transaction) {
    78  	tracker.TrackAll([]*types.Transaction{tx})
    79  }
    80  
    81  // TrackAll adds a list of transactions to the tracked set.
    82  // Note: blob-type transactions are ignored.
    83  func (tracker *TxTracker) TrackAll(txs []*types.Transaction) {
    84  	tracker.mu.Lock()
    85  	defer tracker.mu.Unlock()
    86  
    87  	for _, tx := range txs {
    88  		if tx.Type() == types.BlobTxType {
    89  			continue
    90  		}
    91  		// If we're already tracking it, it's a no-op
    92  		if _, ok := tracker.all[tx.Hash()]; ok {
    93  			continue
    94  		}
    95  		// Theoretically, checking the error here is unnecessary since sender recovery
    96  		// is already part of basic validation. However, retrieving the sender address
    97  		// from the transaction cache is effectively a no-op if it was previously verified.
    98  		// Therefore, the error is still checked just in case.
    99  		addr, err := types.Sender(tracker.signer, tx)
   100  		if err != nil {
   101  			continue
   102  		}
   103  		tracker.all[tx.Hash()] = tx
   104  		if tracker.byAddr[addr] == nil {
   105  			tracker.byAddr[addr] = legacypool.NewSortedMap()
   106  		}
   107  		tracker.byAddr[addr].Put(tx)
   108  
   109  		if tracker.journal != nil {
   110  			_ = tracker.journal.insert(tx)
   111  		}
   112  	}
   113  	localGauge.Update(int64(len(tracker.all)))
   114  }
   115  
   116  // recheck checks and returns any transactions that needs to be resubmitted.
   117  func (tracker *TxTracker) recheck(journalCheck bool) (resubmits []*types.Transaction, rejournal map[common.Address]types.Transactions) {
   118  	tracker.mu.Lock()
   119  	defer tracker.mu.Unlock()
   120  
   121  	var (
   122  		numStales = 0
   123  		numOk     = 0
   124  	)
   125  	for sender, txs := range tracker.byAddr {
   126  		// Wipe the stales
   127  		stales := txs.Forward(tracker.pool.Nonce(sender))
   128  		for _, tx := range stales {
   129  			delete(tracker.all, tx.Hash())
   130  		}
   131  		numStales += len(stales)
   132  
   133  		// Check the non-stale
   134  		for _, tx := range txs.Flatten() {
   135  			if tracker.pool.Has(tx.Hash()) {
   136  				numOk++
   137  				continue
   138  			}
   139  			resubmits = append(resubmits, tx)
   140  		}
   141  	}
   142  
   143  	if journalCheck { // rejournal
   144  		rejournal = make(map[common.Address]types.Transactions)
   145  		for _, tx := range tracker.all {
   146  			addr, _ := types.Sender(tracker.signer, tx)
   147  			rejournal[addr] = append(rejournal[addr], tx)
   148  		}
   149  		// Sort them
   150  		for _, list := range rejournal {
   151  			// cmp(a, b) should return a negative number when a < b,
   152  			slices.SortFunc(list, func(a, b *types.Transaction) int {
   153  				return int(a.Nonce() - b.Nonce())
   154  			})
   155  		}
   156  	}
   157  	localGauge.Update(int64(len(tracker.all)))
   158  	log.Debug("Tx tracker status", "need-resubmit", len(resubmits), "stale", numStales, "ok", numOk)
   159  	return resubmits, rejournal
   160  }
   161  
   162  // Start implements node.Lifecycle interface
   163  // Start is called after all services have been constructed and the networking
   164  // layer was also initialized to spawn any goroutines required by the service.
   165  func (tracker *TxTracker) Start() error {
   166  	tracker.wg.Add(1)
   167  	go tracker.loop()
   168  	return nil
   169  }
   170  
   171  // Stop implements node.Lifecycle interface
   172  // Stop terminates all goroutines belonging to the service, blocking until they
   173  // are all terminated.
   174  func (tracker *TxTracker) Stop() error {
   175  	close(tracker.shutdownCh)
   176  	tracker.wg.Wait()
   177  	return nil
   178  }
   179  
   180  func (tracker *TxTracker) loop() {
   181  	defer tracker.wg.Done()
   182  
   183  	if tracker.journal != nil {
   184  		tracker.journal.load(func(transactions []*types.Transaction) []error {
   185  			tracker.TrackAll(transactions)
   186  			return nil
   187  		})
   188  		defer tracker.journal.close()
   189  	}
   190  	var (
   191  		lastJournal = time.Now()
   192  		timer       = time.NewTimer(10 * time.Second) // Do initial check after 10 seconds, do rechecks more seldom.
   193  	)
   194  	for {
   195  		select {
   196  		case <-tracker.shutdownCh:
   197  			return
   198  		case <-timer.C:
   199  			checkJournal := tracker.journal != nil && time.Since(lastJournal) > tracker.rejournal
   200  			resubmits, rejournal := tracker.recheck(checkJournal)
   201  			if len(resubmits) > 0 {
   202  				tracker.pool.Add(resubmits, false)
   203  			}
   204  			if checkJournal {
   205  				// Lock to prevent journal.rotate <-> journal.insert (via TrackAll) conflicts
   206  				tracker.mu.Lock()
   207  				lastJournal = time.Now()
   208  				if err := tracker.journal.rotate(rejournal); err != nil {
   209  					log.Warn("Transaction journal rotation failed", "err", err)
   210  				}
   211  				tracker.mu.Unlock()
   212  			}
   213  			timer.Reset(recheckInterval)
   214  		}
   215  	}
   216  }