github.com/dim4egster/coreth@v0.10.2/plugin/evm/mempool.go (about)

     1  // (c) 2019-2020, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package evm
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"sync"
    10  
    11  	"github.com/dim4egster/qmallgo/cache"
    12  	"github.com/dim4egster/qmallgo/ids"
    13  	"github.com/dim4egster/coreth/metrics"
    14  	"github.com/ethereum/go-ethereum/log"
    15  )
    16  
    17  const (
    18  	discardedTxsCacheSize = 50
    19  )
    20  
    21  var errNoGasUsed = errors.New("no gas used")
    22  
    23  // mempoolMetrics defines the metrics for the atomic mempool
    24  type mempoolMetrics struct {
    25  	pendingTxs metrics.Gauge // Gauge of currently pending transactions in the txHeap
    26  	currentTxs metrics.Gauge // Gauge of current transactions to be issued into a block
    27  	issuedTxs  metrics.Gauge // Gauge of transactions that have been issued into a block
    28  
    29  	addedTxs     metrics.Counter // Count of all transactions added to the mempool
    30  	discardedTxs metrics.Counter // Count of all discarded transactions
    31  
    32  	newTxsReturned metrics.Counter // Count of transactions returned from GetNewTxs
    33  }
    34  
    35  // newMempoolMetrics constructs metrics for the atomic mempool
    36  func newMempoolMetrics() *mempoolMetrics {
    37  	return &mempoolMetrics{
    38  		pendingTxs:     metrics.GetOrRegisterGauge("atomic_mempool_pending_txs", nil),
    39  		currentTxs:     metrics.GetOrRegisterGauge("atomic_mempool_current_txs", nil),
    40  		issuedTxs:      metrics.GetOrRegisterGauge("atomic_mempool_issued_txs", nil),
    41  		addedTxs:       metrics.GetOrRegisterCounter("atomic_mempool_added_txs", nil),
    42  		discardedTxs:   metrics.GetOrRegisterCounter("atomic_mempool_discarded_txs", nil),
    43  		newTxsReturned: metrics.GetOrRegisterCounter("atomic_mempool_new_txs_returned", nil),
    44  	}
    45  }
    46  
    47  // Mempool is a simple mempool for atomic transactions
    48  type Mempool struct {
    49  	lock sync.RWMutex
    50  
    51  	// AVAXAssetID is the fee paying currency of any atomic transaction
    52  	AVAXAssetID ids.ID
    53  	// maxSize is the maximum number of transactions allowed to be kept in mempool
    54  	maxSize int
    55  	// currentTxs is the set of transactions about to be added to a block.
    56  	currentTxs map[ids.ID]*Tx
    57  	// issuedTxs is the set of transactions that have been issued into a new block
    58  	issuedTxs map[ids.ID]*Tx
    59  	// discardedTxs is an LRU Cache of transactions that have been discarded after failing
    60  	// verification.
    61  	discardedTxs *cache.LRU
    62  	// Pending is a channel of length one, which the mempool ensures has an item on
    63  	// it as long as there is an unissued transaction remaining in [txs]
    64  	Pending chan struct{}
    65  	// newTxs is an array of [Tx] that are ready to be gossiped.
    66  	newTxs []*Tx
    67  	// txHeap is a sorted record of all txs in the mempool by [gasPrice]
    68  	// NOTE: [txHeap] ONLY contains pending txs
    69  	txHeap *txHeap
    70  	// utxoSpenders maps utxoIDs to the transaction consuming them in the mempool
    71  	utxoSpenders map[ids.ID]*Tx
    72  
    73  	metrics *mempoolMetrics
    74  }
    75  
    76  // NewMempool returns a Mempool with [maxSize]
    77  func NewMempool(AVAXAssetID ids.ID, maxSize int) *Mempool {
    78  	return &Mempool{
    79  		AVAXAssetID:  AVAXAssetID,
    80  		issuedTxs:    make(map[ids.ID]*Tx),
    81  		discardedTxs: &cache.LRU{Size: discardedTxsCacheSize},
    82  		currentTxs:   make(map[ids.ID]*Tx),
    83  		Pending:      make(chan struct{}, 1),
    84  		txHeap:       newTxHeap(maxSize),
    85  		maxSize:      maxSize,
    86  		utxoSpenders: make(map[ids.ID]*Tx),
    87  		metrics:      newMempoolMetrics(),
    88  	}
    89  }
    90  
    91  // Len returns the number of transactions in the mempool
    92  func (m *Mempool) Len() int {
    93  	m.lock.RLock()
    94  	defer m.lock.RUnlock()
    95  
    96  	return m.length()
    97  }
    98  
    99  // assumes the lock is held
   100  func (m *Mempool) length() int {
   101  	return m.txHeap.Len() + len(m.issuedTxs)
   102  }
   103  
   104  // has indicates if a given [txID] is in the mempool and has not been
   105  // discarded.
   106  func (m *Mempool) has(txID ids.ID) bool {
   107  	_, dropped, found := m.GetTx(txID)
   108  	return found && !dropped
   109  }
   110  
   111  // atomicTxGasPrice is the [gasPrice] paid by a transaction to burn a given
   112  // amount of [AVAXAssetID] given the value of [gasUsed].
   113  func (m *Mempool) atomicTxGasPrice(tx *Tx) (uint64, error) {
   114  	gasUsed, err := tx.GasUsed(true)
   115  	if err != nil {
   116  		return 0, err
   117  	}
   118  	if gasUsed == 0 {
   119  		return 0, errNoGasUsed
   120  	}
   121  	burned, err := tx.Burned(m.AVAXAssetID)
   122  	if err != nil {
   123  		return 0, err
   124  	}
   125  	return burned / gasUsed, nil
   126  }
   127  
   128  // Add attempts to add [tx] to the mempool and returns an error if
   129  // it could not be addeed to the mempool.
   130  func (m *Mempool) AddTx(tx *Tx) error {
   131  	m.lock.Lock()
   132  	defer m.lock.Unlock()
   133  
   134  	return m.addTx(tx, false)
   135  }
   136  
   137  // forceAddTx forcibly adds a *Tx to the mempool and bypasses all verification.
   138  func (m *Mempool) ForceAddTx(tx *Tx) error {
   139  	m.lock.Lock()
   140  	defer m.lock.Unlock()
   141  
   142  	return m.addTx(tx, true)
   143  }
   144  
   145  // checkConflictTx checks for any transactions in the mempool that spend the same input UTXOs as [tx].
   146  // If any conflicts are present, it returns the highest gas price of any conflicting transaction, the
   147  // txID of the corresponding tx and the full list of transactions that conflict with [tx].
   148  func (m *Mempool) checkConflictTx(tx *Tx) (uint64, ids.ID, []*Tx, error) {
   149  	utxoSet := tx.InputUTXOs()
   150  
   151  	var (
   152  		highestGasPrice             uint64 = 0
   153  		conflictingTxs              []*Tx  = make([]*Tx, 0)
   154  		highestGasPriceConflictTxID ids.ID = ids.ID{}
   155  	)
   156  	for utxoID := range utxoSet {
   157  		// Get current gas price of the existing tx in the mempool
   158  		conflictTx, ok := m.utxoSpenders[utxoID]
   159  		if !ok {
   160  			continue
   161  		}
   162  		conflictTxID := conflictTx.ID()
   163  		conflictTxGasPrice, err := m.atomicTxGasPrice(conflictTx)
   164  		// Should never error to calculate the gas price of a transaction already in the mempool
   165  		if err != nil {
   166  			return 0, ids.ID{}, conflictingTxs, fmt.Errorf("failed to re-calculate gas price for conflict tx due to: %w", err)
   167  		}
   168  		if highestGasPrice < conflictTxGasPrice {
   169  			highestGasPrice = conflictTxGasPrice
   170  			highestGasPriceConflictTxID = conflictTxID
   171  		}
   172  		conflictingTxs = append(conflictingTxs, conflictTx)
   173  	}
   174  	return highestGasPrice, highestGasPriceConflictTxID, conflictingTxs, nil
   175  }
   176  
   177  // addTx adds [tx] to the mempool. Assumes [m.lock] is held.
   178  // If [force], skips conflict checks within the mempool.
   179  func (m *Mempool) addTx(tx *Tx, force bool) error {
   180  	txID := tx.ID()
   181  	// If [txID] has already been issued or is in the currentTxs map
   182  	// there's no need to add it.
   183  	if _, exists := m.issuedTxs[txID]; exists {
   184  		return nil
   185  	}
   186  	if _, exists := m.currentTxs[txID]; exists {
   187  		return nil
   188  	}
   189  	if _, exists := m.txHeap.Get(txID); exists {
   190  		return nil
   191  	}
   192  
   193  	utxoSet := tx.InputUTXOs()
   194  	gasPrice, _ := m.atomicTxGasPrice(tx)
   195  	highestGasPrice, highestGasPriceConflictTxID, conflictingTxs, err := m.checkConflictTx(tx)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	if len(conflictingTxs) != 0 && !force {
   200  		// If [tx] does not have a higher fee than all of its conflicts,
   201  		// we refuse to issue it to the mempool.
   202  		if highestGasPrice >= gasPrice {
   203  			return fmt.Errorf(
   204  				"%w: issued tx (%s) gas price %d <= conflict tx (%s) gas price %d (%d total conflicts in mempool)",
   205  				errConflictingAtomicTx,
   206  				txID,
   207  				gasPrice,
   208  				highestGasPriceConflictTxID,
   209  				highestGasPrice,
   210  				len(conflictingTxs),
   211  			)
   212  		}
   213  		// Remove any conflicting transactions from the mempool
   214  		for _, conflictTx := range conflictingTxs {
   215  			m.removeTx(conflictTx, true)
   216  		}
   217  	}
   218  	// If adding this transaction would exceed the mempool's size, check if there is a lower priced
   219  	// transaction that can be evicted from the mempool
   220  	if m.length() >= m.maxSize {
   221  		if m.txHeap.Len() > 0 {
   222  			// Get the lowest price item from [txHeap]
   223  			minTx, minGasPrice := m.txHeap.PeekMin()
   224  			// If the [gasPrice] of the lowest item is >= the [gasPrice] of the
   225  			// submitted item, discard the submitted item (we prefer items
   226  			// already in the mempool).
   227  			if minGasPrice >= gasPrice {
   228  				return fmt.Errorf(
   229  					"%w currentMin=%d provided=%d",
   230  					errInsufficientAtomicTxFee,
   231  					minGasPrice,
   232  					gasPrice,
   233  				)
   234  			}
   235  
   236  			m.removeTx(minTx, true)
   237  		} else {
   238  			// This could occur if we have used our entire size allowance on
   239  			// transactions that are currently processing.
   240  			return errTooManyAtomicTx
   241  		}
   242  	}
   243  
   244  	// If the transaction was recently discarded, log the event and evict from
   245  	// discarded transactions so it's not in two places within the mempool.
   246  	// We allow the transaction to be re-issued since it may have been invalid
   247  	// due to an atomic UTXO not being present yet.
   248  	if _, has := m.discardedTxs.Get(txID); has {
   249  		log.Debug("Adding recently discarded transaction %s back to the mempool", txID)
   250  		m.discardedTxs.Evict(txID)
   251  	}
   252  
   253  	// Add the transaction to the [txHeap] so we can evaluate new entries based
   254  	// on how their [gasPrice] compares and add to [utxoSet] to make sure we can
   255  	// reject conflicting transactions.
   256  	m.txHeap.Push(tx, gasPrice)
   257  	m.metrics.addedTxs.Inc(1)
   258  	m.metrics.pendingTxs.Update(int64(m.txHeap.Len()))
   259  	for utxoID := range utxoSet {
   260  		m.utxoSpenders[utxoID] = tx
   261  	}
   262  	// When adding [tx] to the mempool make sure that there is an item in Pending
   263  	// to signal the VM to produce a block. Note: if the VM's buildStatus has already
   264  	// been set to something other than [dontBuild], this will be ignored and won't be
   265  	// reset until the engine calls BuildBlock. This case is handled in IssueCurrentTx
   266  	// and CancelCurrentTx.
   267  	m.newTxs = append(m.newTxs, tx)
   268  	m.addPending()
   269  	return nil
   270  }
   271  
   272  // NextTx returns a transaction to be issued from the mempool.
   273  func (m *Mempool) NextTx() (*Tx, bool) {
   274  	m.lock.Lock()
   275  	defer m.lock.Unlock()
   276  
   277  	// We include atomic transactions in blocks sorted by the [gasPrice] they
   278  	// pay.
   279  	if m.txHeap.Len() > 0 {
   280  		tx := m.txHeap.PopMax()
   281  		m.currentTxs[tx.ID()] = tx
   282  		m.metrics.pendingTxs.Update(int64(m.txHeap.Len()))
   283  		m.metrics.currentTxs.Update(int64(len(m.currentTxs)))
   284  		return tx, true
   285  	}
   286  
   287  	return nil, false
   288  }
   289  
   290  // GetPendingTx returns the transaction [txID] and true if it is
   291  // currently in the [txHeap] waiting to be issued into a block.
   292  // Returns nil, false otherwise.
   293  func (m *Mempool) GetPendingTx(txID ids.ID) (*Tx, bool) {
   294  	m.lock.RLock()
   295  	defer m.lock.RUnlock()
   296  
   297  	return m.txHeap.Get(txID)
   298  }
   299  
   300  // GetTx returns the transaction [txID] if it was issued
   301  // by this node and returns whether it was dropped and whether
   302  // it exists.
   303  func (m *Mempool) GetTx(txID ids.ID) (*Tx, bool, bool) {
   304  	m.lock.RLock()
   305  	defer m.lock.RUnlock()
   306  
   307  	if tx, ok := m.txHeap.Get(txID); ok {
   308  		return tx, false, true
   309  	}
   310  	if tx, ok := m.issuedTxs[txID]; ok {
   311  		return tx, false, true
   312  	}
   313  	if tx, ok := m.currentTxs[txID]; ok {
   314  		return tx, false, true
   315  	}
   316  	if tx, exists := m.discardedTxs.Get(txID); exists {
   317  		return tx.(*Tx), true, true
   318  	}
   319  
   320  	return nil, false, false
   321  }
   322  
   323  // IssueCurrentTx marks [currentTx] as issued if there is one
   324  func (m *Mempool) IssueCurrentTxs() {
   325  	m.lock.Lock()
   326  	defer m.lock.Unlock()
   327  
   328  	for txID := range m.currentTxs {
   329  		m.issuedTxs[txID] = m.currentTxs[txID]
   330  		delete(m.currentTxs, txID)
   331  	}
   332  	m.metrics.issuedTxs.Update(int64(len(m.issuedTxs)))
   333  	m.metrics.currentTxs.Update(int64(len(m.currentTxs)))
   334  
   335  	// If there are more transactions to be issued, add an item
   336  	// to Pending.
   337  	if m.txHeap.Len() > 0 {
   338  		m.addPending()
   339  	}
   340  }
   341  
   342  // CancelCurrentTx marks the attempt to issue [txID]
   343  // as being aborted. This should be called after NextTx returns [txID]
   344  // and the transaction [txID] cannot be included in the block, but should
   345  // not be discarded. For example, CancelCurrentTx should be called if including
   346  // the transaction will put the block above the atomic tx gas limit.
   347  func (m *Mempool) CancelCurrentTx(txID ids.ID) {
   348  	m.lock.Lock()
   349  	defer m.lock.Unlock()
   350  
   351  	if tx, ok := m.currentTxs[txID]; ok {
   352  		m.cancelTx(tx)
   353  	}
   354  }
   355  
   356  // [CancelCurrentTxs] marks the attempt to issue [currentTxs]
   357  // as being aborted. If this is called after a buildBlock error
   358  // caused by the atomic transaction, then DiscardCurrentTx should have been called
   359  // such that this call will have no effect and should not re-issue the invalid tx.
   360  func (m *Mempool) CancelCurrentTxs() {
   361  	m.lock.Lock()
   362  	defer m.lock.Unlock()
   363  
   364  	// If building a block failed, put the currentTx back in [txs]
   365  	// if it exists.
   366  	for _, tx := range m.currentTxs {
   367  		m.cancelTx(tx)
   368  	}
   369  
   370  	// If there are more transactions to be issued, add an item
   371  	// to Pending.
   372  	if m.txHeap.Len() > 0 {
   373  		m.addPending()
   374  	}
   375  }
   376  
   377  // cancelTx removes [tx] from current transactions and moves it back into the
   378  // tx heap.
   379  // assumes the lock is held.
   380  func (m *Mempool) cancelTx(tx *Tx) {
   381  	// Add tx to heap sorted by gasPrice
   382  	gasPrice, err := m.atomicTxGasPrice(tx)
   383  	if err == nil {
   384  		m.txHeap.Push(tx, gasPrice)
   385  		m.metrics.pendingTxs.Update(int64(m.txHeap.Len()))
   386  	} else {
   387  		// If the err is not nil, we simply discard the transaction because it is
   388  		// invalid. This should never happen but we guard against the case it does.
   389  		log.Error("failed to calculate atomic tx gas price while canceling current tx", "err", err)
   390  		m.removeSpenders(tx)
   391  		m.discardedTxs.Put(tx.ID(), tx)
   392  		m.metrics.discardedTxs.Inc(1)
   393  	}
   394  
   395  	delete(m.currentTxs, tx.ID())
   396  	m.metrics.currentTxs.Update(int64(len(m.currentTxs)))
   397  }
   398  
   399  // DiscardCurrentTx marks a [tx] in the [currentTxs] map as invalid and aborts the attempt
   400  // to issue it since it failed verification.
   401  func (m *Mempool) DiscardCurrentTx(txID ids.ID) {
   402  	m.lock.Lock()
   403  	defer m.lock.Unlock()
   404  
   405  	if tx, ok := m.currentTxs[txID]; ok {
   406  		m.discardCurrentTx(tx)
   407  	}
   408  }
   409  
   410  // DiscardCurrentTxs marks all txs in [currentTxs] as discarded.
   411  func (m *Mempool) DiscardCurrentTxs() {
   412  	m.lock.Lock()
   413  	defer m.lock.Unlock()
   414  
   415  	for _, tx := range m.currentTxs {
   416  		m.discardCurrentTx(tx)
   417  	}
   418  }
   419  
   420  // discardCurrentTx discards [tx] from the set of current transactions.
   421  // Assumes the lock is held.
   422  func (m *Mempool) discardCurrentTx(tx *Tx) {
   423  	m.removeSpenders(tx)
   424  	m.discardedTxs.Put(tx.ID(), tx)
   425  	delete(m.currentTxs, tx.ID())
   426  	m.metrics.currentTxs.Update(int64(len(m.currentTxs)))
   427  	m.metrics.discardedTxs.Inc(1)
   428  }
   429  
   430  // removeTx removes [txID] from the mempool.
   431  // Note: removeTx will delete all entries from [utxoSpenders] corresponding
   432  // to input UTXOs of [txID]. This means that when replacing a conflicting tx,
   433  // removeTx must be called for all conflicts before overwriting the utxoSpenders
   434  // map.
   435  // Assumes lock is held.
   436  func (m *Mempool) removeTx(tx *Tx, discard bool) {
   437  	txID := tx.ID()
   438  
   439  	// Remove from [currentTxs], [txHeap], and [issuedTxs].
   440  	delete(m.currentTxs, txID)
   441  	m.txHeap.Remove(txID)
   442  	delete(m.issuedTxs, txID)
   443  
   444  	if discard {
   445  		m.discardedTxs.Put(txID, tx)
   446  		m.metrics.discardedTxs.Inc(1)
   447  	} else {
   448  		m.discardedTxs.Evict(txID)
   449  	}
   450  	m.metrics.pendingTxs.Update(int64(m.txHeap.Len()))
   451  	m.metrics.currentTxs.Update(int64(len(m.currentTxs)))
   452  	m.metrics.issuedTxs.Update(int64(len(m.issuedTxs)))
   453  
   454  	// Remove all entries from [utxoSpenders].
   455  	m.removeSpenders(tx)
   456  }
   457  
   458  // removeSpenders deletes the entries for all input UTXOs of [tx] from the
   459  // [utxoSpenders] map.
   460  // Assumes the lock is held.
   461  func (m *Mempool) removeSpenders(tx *Tx) {
   462  	for utxoID := range tx.InputUTXOs() {
   463  		delete(m.utxoSpenders, utxoID)
   464  	}
   465  }
   466  
   467  // RemoveTx removes [txID] from the mempool completely.
   468  // Evicts [tx] from the discarded cache if present.
   469  func (m *Mempool) RemoveTx(tx *Tx) {
   470  	m.lock.Lock()
   471  	defer m.lock.Unlock()
   472  
   473  	m.removeTx(tx, false)
   474  }
   475  
   476  // addPending makes sure that an item is in the Pending channel.
   477  func (m *Mempool) addPending() {
   478  	select {
   479  	case m.Pending <- struct{}{}:
   480  	default:
   481  	}
   482  }
   483  
   484  // GetNewTxs returns the array of [newTxs] and replaces it with an empty array.
   485  func (m *Mempool) GetNewTxs() []*Tx {
   486  	m.lock.Lock()
   487  	defer m.lock.Unlock()
   488  
   489  	cpy := m.newTxs
   490  	m.newTxs = nil
   491  	m.metrics.newTxsReturned.Inc(int64(len(cpy))) // Increment the number of newTxs
   492  	return cpy
   493  }