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 }