github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/mempool/v0/clist_mempool.go (about) 1 package v0 2 3 import ( 4 "bytes" 5 "errors" 6 "sync" 7 "sync/atomic" 8 9 abci "github.com/badrootd/nibiru-cometbft/abci/types" 10 "github.com/badrootd/nibiru-cometbft/config" 11 "github.com/badrootd/nibiru-cometbft/libs/clist" 12 "github.com/badrootd/nibiru-cometbft/libs/log" 13 cmtmath "github.com/badrootd/nibiru-cometbft/libs/math" 14 cmtsync "github.com/badrootd/nibiru-cometbft/libs/sync" 15 "github.com/badrootd/nibiru-cometbft/mempool" 16 "github.com/badrootd/nibiru-cometbft/p2p" 17 "github.com/badrootd/nibiru-cometbft/proxy" 18 "github.com/badrootd/nibiru-cometbft/types" 19 ) 20 21 // CListMempool is an ordered in-memory pool for transactions before they are 22 // proposed in a consensus round. Transaction validity is checked using the 23 // CheckTx abci message before the transaction is added to the pool. The 24 // mempool uses a concurrent list structure for storing transactions that can 25 // be efficiently accessed by multiple concurrent readers. 26 type CListMempool struct { 27 // Atomic integers 28 height int64 // the last block Update()'d to 29 txsBytes int64 // total size of mempool, in bytes 30 31 // notify listeners (ie. consensus) when txs are available 32 notifiedTxsAvailable bool 33 txsAvailable chan struct{} // fires once for each height, when the mempool is not empty 34 35 config *config.MempoolConfig 36 37 // Exclusive mutex for Update method to prevent concurrent execution of 38 // CheckTx or ReapMaxBytesMaxGas(ReapMaxTxs) methods. 39 updateMtx cmtsync.RWMutex 40 preCheck mempool.PreCheckFunc 41 postCheck mempool.PostCheckFunc 42 43 txs *clist.CList // concurrent linked-list of good txs 44 proxyAppConn proxy.AppConnMempool 45 46 // Track whether we're rechecking txs. 47 // These are not protected by a mutex and are expected to be mutated in 48 // serial (ie. by abci responses which are called in serial). 49 recheckCursor *clist.CElement // next expected response 50 recheckEnd *clist.CElement // re-checking stops here 51 52 // Map for quick access to txs to record sender in CheckTx. 53 // txsMap: txKey -> CElement 54 txsMap sync.Map 55 56 // Keep a cache of already-seen txs. 57 // This reduces the pressure on the proxyApp. 58 cache mempool.TxCache 59 60 logger log.Logger 61 metrics *mempool.Metrics 62 } 63 64 var _ mempool.Mempool = &CListMempool{} 65 66 // CListMempoolOption sets an optional parameter on the mempool. 67 type CListMempoolOption func(*CListMempool) 68 69 // NewCListMempool returns a new mempool with the given configuration and 70 // connection to an application. 71 func NewCListMempool( 72 cfg *config.MempoolConfig, 73 proxyAppConn proxy.AppConnMempool, 74 height int64, 75 options ...CListMempoolOption, 76 ) *CListMempool { 77 78 mp := &CListMempool{ 79 config: cfg, 80 proxyAppConn: proxyAppConn, 81 txs: clist.New(), 82 height: height, 83 recheckCursor: nil, 84 recheckEnd: nil, 85 logger: log.NewNopLogger(), 86 metrics: mempool.NopMetrics(), 87 } 88 89 if cfg.CacheSize > 0 { 90 mp.cache = mempool.NewLRUTxCache(cfg.CacheSize) 91 } else { 92 mp.cache = mempool.NopTxCache{} 93 } 94 95 proxyAppConn.SetResponseCallback(mp.globalCb) 96 97 for _, option := range options { 98 option(mp) 99 } 100 101 return mp 102 } 103 104 // NOTE: not thread safe - should only be called once, on startup 105 func (mem *CListMempool) EnableTxsAvailable() { 106 mem.txsAvailable = make(chan struct{}, 1) 107 } 108 109 // SetLogger sets the Logger. 110 func (mem *CListMempool) SetLogger(l log.Logger) { 111 mem.logger = l 112 } 113 114 // WithPreCheck sets a filter for the mempool to reject a tx if f(tx) returns 115 // false. This is ran before CheckTx. Only applies to the first created block. 116 // After that, Update overwrites the existing value. 117 func WithPreCheck(f mempool.PreCheckFunc) CListMempoolOption { 118 return func(mem *CListMempool) { mem.preCheck = f } 119 } 120 121 // WithPostCheck sets a filter for the mempool to reject a tx if f(tx) returns 122 // false. This is ran after CheckTx. Only applies to the first created block. 123 // After that, Update overwrites the existing value. 124 func WithPostCheck(f mempool.PostCheckFunc) CListMempoolOption { 125 return func(mem *CListMempool) { mem.postCheck = f } 126 } 127 128 // WithMetrics sets the metrics. 129 func WithMetrics(metrics *mempool.Metrics) CListMempoolOption { 130 return func(mem *CListMempool) { mem.metrics = metrics } 131 } 132 133 // Safe for concurrent use by multiple goroutines. 134 func (mem *CListMempool) Lock() { 135 mem.updateMtx.Lock() 136 } 137 138 // Safe for concurrent use by multiple goroutines. 139 func (mem *CListMempool) Unlock() { 140 mem.updateMtx.Unlock() 141 } 142 143 // Safe for concurrent use by multiple goroutines. 144 func (mem *CListMempool) Size() int { 145 return mem.txs.Len() 146 } 147 148 // Safe for concurrent use by multiple goroutines. 149 func (mem *CListMempool) SizeBytes() int64 { 150 return atomic.LoadInt64(&mem.txsBytes) 151 } 152 153 // Lock() must be help by the caller during execution. 154 func (mem *CListMempool) FlushAppConn() error { 155 return mem.proxyAppConn.FlushSync() 156 } 157 158 // XXX: Unsafe! Calling Flush may leave mempool in inconsistent state. 159 func (mem *CListMempool) Flush() { 160 mem.updateMtx.RLock() 161 defer mem.updateMtx.RUnlock() 162 163 _ = atomic.SwapInt64(&mem.txsBytes, 0) 164 mem.cache.Reset() 165 166 for e := mem.txs.Front(); e != nil; e = e.Next() { 167 mem.txs.Remove(e) 168 e.DetachPrev() 169 } 170 171 mem.txsMap.Range(func(key, _ interface{}) bool { 172 mem.txsMap.Delete(key) 173 return true 174 }) 175 } 176 177 // TxsFront returns the first transaction in the ordered list for peer 178 // goroutines to call .NextWait() on. 179 // FIXME: leaking implementation details! 180 // 181 // Safe for concurrent use by multiple goroutines. 182 func (mem *CListMempool) TxsFront() *clist.CElement { 183 return mem.txs.Front() 184 } 185 186 // TxsWaitChan returns a channel to wait on transactions. It will be closed 187 // once the mempool is not empty (ie. the internal `mem.txs` has at least one 188 // element) 189 // 190 // Safe for concurrent use by multiple goroutines. 191 func (mem *CListMempool) TxsWaitChan() <-chan struct{} { 192 return mem.txs.WaitChan() 193 } 194 195 // It blocks if we're waiting on Update() or Reap(). 196 // cb: A callback from the CheckTx command. 197 // 198 // It gets called from another goroutine. 199 // 200 // CONTRACT: Either cb will get called, or err returned. 201 // 202 // Safe for concurrent use by multiple goroutines. 203 func (mem *CListMempool) CheckTx( 204 tx types.Tx, 205 cb func(*abci.Response), 206 txInfo mempool.TxInfo, 207 ) error { 208 209 mem.updateMtx.RLock() 210 // use defer to unlock mutex because application (*local client*) might panic 211 defer mem.updateMtx.RUnlock() 212 213 txSize := len(tx) 214 215 if err := mem.isFull(txSize); err != nil { 216 return err 217 } 218 219 if txSize > mem.config.MaxTxBytes { 220 return mempool.ErrTxTooLarge{ 221 Max: mem.config.MaxTxBytes, 222 Actual: txSize, 223 } 224 } 225 226 if mem.preCheck != nil { 227 if err := mem.preCheck(tx); err != nil { 228 return mempool.ErrPreCheck{ 229 Reason: err, 230 } 231 } 232 } 233 234 // NOTE: proxyAppConn may error if tx buffer is full 235 if err := mem.proxyAppConn.Error(); err != nil { 236 return err 237 } 238 239 if !mem.cache.Push(tx) { // if the transaction already exists in the cache 240 // Record a new sender for a tx we've already seen. 241 // Note it's possible a tx is still in the cache but no longer in the mempool 242 // (eg. after committing a block, txs are removed from mempool but not cache), 243 // so we only record the sender for txs still in the mempool. 244 if e, ok := mem.txsMap.Load(tx.Key()); ok { 245 memTx := e.(*clist.CElement).Value.(*mempoolTx) 246 memTx.senders.LoadOrStore(txInfo.SenderID, true) 247 // TODO: consider punishing peer for dups, 248 // its non-trivial since invalid txs can become valid, 249 // but they can spam the same tx with little cost to them atm. 250 } 251 return mempool.ErrTxInCache 252 } 253 254 reqRes := mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{Tx: tx}) 255 reqRes.SetCallback(mem.reqResCb(tx, txInfo.SenderID, txInfo.SenderP2PID, cb)) 256 257 return nil 258 } 259 260 // Global callback that will be called after every ABCI response. 261 // Having a single global callback avoids needing to set a callback for each request. 262 // However, processing the checkTx response requires the peerID (so we can track which txs we heard from who), 263 // and peerID is not included in the ABCI request, so we have to set request-specific callbacks that 264 // include this information. If we're not in the midst of a recheck, this function will just return, 265 // so the request specific callback can do the work. 266 // 267 // When rechecking, we don't need the peerID, so the recheck callback happens 268 // here. 269 func (mem *CListMempool) globalCb(req *abci.Request, res *abci.Response) { 270 if mem.recheckCursor == nil { 271 return 272 } 273 274 mem.metrics.RecheckTimes.Add(1) 275 mem.resCbRecheck(req, res) 276 277 // update metrics 278 mem.metrics.Size.Set(float64(mem.Size())) 279 } 280 281 // Request specific callback that should be set on individual reqRes objects 282 // to incorporate local information when processing the response. 283 // This allows us to track the peer that sent us this tx, so we can avoid sending it back to them. 284 // NOTE: alternatively, we could include this information in the ABCI request itself. 285 // 286 // External callers of CheckTx, like the RPC, can also pass an externalCb through here that is called 287 // when all other response processing is complete. 288 // 289 // Used in CheckTx to record PeerID who sent us the tx. 290 func (mem *CListMempool) reqResCb( 291 tx []byte, 292 peerID uint16, 293 peerP2PID p2p.ID, 294 externalCb func(*abci.Response), 295 ) func(res *abci.Response) { 296 return func(res *abci.Response) { 297 if mem.recheckCursor != nil { 298 // this should never happen 299 panic("recheck cursor is not nil in reqResCb") 300 } 301 302 mem.resCbFirstTime(tx, peerID, peerP2PID, res) 303 304 // update metrics 305 mem.metrics.Size.Set(float64(mem.Size())) 306 mem.metrics.SizeBytes.Set(float64(mem.SizeBytes())) 307 308 // passed in by the caller of CheckTx, eg. the RPC 309 if externalCb != nil { 310 externalCb(res) 311 } 312 } 313 } 314 315 // Called from: 316 // - resCbFirstTime (lock not held) if tx is valid 317 func (mem *CListMempool) addTx(memTx *mempoolTx) { 318 e := mem.txs.PushBack(memTx) 319 mem.txsMap.Store(memTx.tx.Key(), e) 320 atomic.AddInt64(&mem.txsBytes, int64(len(memTx.tx))) 321 mem.metrics.TxSizeBytes.Observe(float64(len(memTx.tx))) 322 } 323 324 // Called from: 325 // - Update (lock held) if tx was committed 326 // - resCbRecheck (lock not held) if tx was invalidated 327 func (mem *CListMempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromCache bool) { 328 mem.txs.Remove(elem) 329 elem.DetachPrev() 330 mem.txsMap.Delete(tx.Key()) 331 atomic.AddInt64(&mem.txsBytes, int64(-len(tx))) 332 333 if removeFromCache { 334 mem.cache.Remove(tx) 335 } 336 } 337 338 // RemoveTxByKey removes a transaction from the mempool by its TxKey index. 339 func (mem *CListMempool) RemoveTxByKey(txKey types.TxKey) error { 340 if e, ok := mem.txsMap.Load(txKey); ok { 341 memTx := e.(*clist.CElement).Value.(*mempoolTx) 342 if memTx != nil { 343 mem.removeTx(memTx.tx, e.(*clist.CElement), false) 344 return nil 345 } 346 return errors.New("transaction not found") 347 } 348 return errors.New("invalid transaction found") 349 } 350 351 func (mem *CListMempool) isFull(txSize int) error { 352 var ( 353 memSize = mem.Size() 354 txsBytes = mem.SizeBytes() 355 ) 356 357 if memSize >= mem.config.Size || int64(txSize)+txsBytes > mem.config.MaxTxsBytes { 358 return mempool.ErrMempoolIsFull{ 359 NumTxs: memSize, 360 MaxTxs: mem.config.Size, 361 TxsBytes: txsBytes, 362 MaxTxsBytes: mem.config.MaxTxsBytes, 363 } 364 } 365 366 return nil 367 } 368 369 // callback, which is called after the app checked the tx for the first time. 370 // 371 // The case where the app checks the tx for the second and subsequent times is 372 // handled by the resCbRecheck callback. 373 func (mem *CListMempool) resCbFirstTime( 374 tx []byte, 375 peerID uint16, 376 peerP2PID p2p.ID, 377 res *abci.Response, 378 ) { 379 switch r := res.Value.(type) { 380 case *abci.Response_CheckTx: 381 var postCheckErr error 382 if mem.postCheck != nil { 383 postCheckErr = mem.postCheck(tx, r.CheckTx) 384 } 385 if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { 386 // Check mempool isn't full again to reduce the chance of exceeding the 387 // limits. 388 if err := mem.isFull(len(tx)); err != nil { 389 // remove from cache (mempool might have a space later) 390 mem.cache.Remove(tx) 391 mem.logger.Error(err.Error()) 392 return 393 } 394 395 // Check transaction not already in the mempool 396 if e, ok := mem.txsMap.Load(types.Tx(tx).Key()); ok { 397 memTx := e.(*clist.CElement).Value.(*mempoolTx) 398 memTx.senders.LoadOrStore(peerID, true) 399 mem.logger.Debug( 400 "transaction already there, not adding it again", 401 "tx", types.Tx(tx).Hash(), 402 "res", r, 403 "height", mem.height, 404 "total", mem.Size(), 405 ) 406 return 407 } 408 409 memTx := &mempoolTx{ 410 height: mem.height, 411 gasWanted: r.CheckTx.GasWanted, 412 tx: tx, 413 } 414 memTx.senders.Store(peerID, true) 415 mem.addTx(memTx) 416 mem.logger.Debug( 417 "added good transaction", 418 "tx", types.Tx(tx).Hash(), 419 "res", r, 420 "height", memTx.height, 421 "total", mem.Size(), 422 ) 423 mem.notifyTxsAvailable() 424 } else { 425 // ignore bad transaction 426 mem.logger.Debug( 427 "rejected bad transaction", 428 "tx", types.Tx(tx).Hash(), 429 "peerID", peerP2PID, 430 "res", r, 431 "err", postCheckErr, 432 ) 433 mem.metrics.FailedTxs.Add(1) 434 435 if !mem.config.KeepInvalidTxsInCache { 436 // remove from cache (it might be good later) 437 mem.cache.Remove(tx) 438 } 439 } 440 441 default: 442 // ignore other messages 443 } 444 } 445 446 // callback, which is called after the app rechecked the tx. 447 // 448 // The case where the app checks the tx for the first time is handled by the 449 // resCbFirstTime callback. 450 func (mem *CListMempool) resCbRecheck(req *abci.Request, res *abci.Response) { 451 switch r := res.Value.(type) { 452 case *abci.Response_CheckTx: 453 tx := req.GetCheckTx().Tx 454 memTx := mem.recheckCursor.Value.(*mempoolTx) 455 456 // Search through the remaining list of tx to recheck for a transaction that matches 457 // the one we received from the ABCI application. 458 for { 459 if bytes.Equal(tx, memTx.tx) { 460 // We've found a tx in the recheck list that matches the tx that we 461 // received from the ABCI application. 462 // Break, and use this transaction for further checks. 463 break 464 } 465 466 mem.logger.Error( 467 "re-CheckTx transaction mismatch", 468 "got", types.Tx(tx), 469 "expected", memTx.tx, 470 ) 471 472 if mem.recheckCursor == mem.recheckEnd { 473 // we reached the end of the recheckTx list without finding a tx 474 // matching the one we received from the ABCI application. 475 // Return without processing any tx. 476 mem.recheckCursor = nil 477 return 478 } 479 480 mem.recheckCursor = mem.recheckCursor.Next() 481 memTx = mem.recheckCursor.Value.(*mempoolTx) 482 } 483 484 var postCheckErr error 485 if mem.postCheck != nil { 486 postCheckErr = mem.postCheck(tx, r.CheckTx) 487 } 488 489 if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { 490 // Good, nothing to do. 491 } else { 492 // Tx became invalidated due to newly committed block. 493 mem.logger.Debug("tx is no longer valid", "tx", types.Tx(tx).Hash(), "res", r, "err", postCheckErr) 494 // NOTE: we remove tx from the cache because it might be good later 495 mem.removeTx(tx, mem.recheckCursor, !mem.config.KeepInvalidTxsInCache) 496 } 497 if mem.recheckCursor == mem.recheckEnd { 498 mem.recheckCursor = nil 499 } else { 500 mem.recheckCursor = mem.recheckCursor.Next() 501 } 502 if mem.recheckCursor == nil { 503 // Done! 504 mem.logger.Debug("done rechecking txs") 505 506 // incase the recheck removed all txs 507 if mem.Size() > 0 { 508 mem.notifyTxsAvailable() 509 } 510 } 511 default: 512 // ignore other messages 513 } 514 } 515 516 // Safe for concurrent use by multiple goroutines. 517 func (mem *CListMempool) TxsAvailable() <-chan struct{} { 518 return mem.txsAvailable 519 } 520 521 func (mem *CListMempool) notifyTxsAvailable() { 522 if mem.Size() == 0 { 523 panic("notified txs available but mempool is empty!") 524 } 525 if mem.txsAvailable != nil && !mem.notifiedTxsAvailable { 526 // channel cap is 1, so this will send once 527 mem.notifiedTxsAvailable = true 528 select { 529 case mem.txsAvailable <- struct{}{}: 530 default: 531 } 532 } 533 } 534 535 // Safe for concurrent use by multiple goroutines. 536 func (mem *CListMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { 537 mem.updateMtx.RLock() 538 defer mem.updateMtx.RUnlock() 539 540 var ( 541 totalGas int64 542 runningSize int64 543 ) 544 545 // TODO: we will get a performance boost if we have a good estimate of avg 546 // size per tx, and set the initial capacity based off of that. 547 // txs := make([]types.Tx, 0, cmtmath.MinInt(mem.txs.Len(), max/mem.avgTxSize)) 548 txs := make([]types.Tx, 0, mem.txs.Len()) 549 for e := mem.txs.Front(); e != nil; e = e.Next() { 550 memTx := e.Value.(*mempoolTx) 551 552 txs = append(txs, memTx.tx) 553 554 dataSize := types.ComputeProtoSizeForTxs([]types.Tx{memTx.tx}) 555 556 // Check total size requirement 557 if maxBytes > -1 && runningSize+dataSize > maxBytes { 558 return txs[:len(txs)-1] 559 } 560 561 runningSize += dataSize 562 563 // Check total gas requirement. 564 // If maxGas is negative, skip this check. 565 // Since newTotalGas < masGas, which 566 // must be non-negative, it follows that this won't overflow. 567 newTotalGas := totalGas + memTx.gasWanted 568 if maxGas > -1 && newTotalGas > maxGas { 569 return txs[:len(txs)-1] 570 } 571 totalGas = newTotalGas 572 } 573 return txs 574 } 575 576 // Safe for concurrent use by multiple goroutines. 577 func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { 578 mem.updateMtx.RLock() 579 defer mem.updateMtx.RUnlock() 580 581 if max < 0 { 582 max = mem.txs.Len() 583 } 584 585 txs := make([]types.Tx, 0, cmtmath.MinInt(mem.txs.Len(), max)) 586 for e := mem.txs.Front(); e != nil && len(txs) <= max; e = e.Next() { 587 memTx := e.Value.(*mempoolTx) 588 txs = append(txs, memTx.tx) 589 } 590 return txs 591 } 592 593 // Lock() must be help by the caller during execution. 594 func (mem *CListMempool) Update( 595 height int64, 596 txs types.Txs, 597 deliverTxResponses []*abci.ResponseDeliverTx, 598 preCheck mempool.PreCheckFunc, 599 postCheck mempool.PostCheckFunc, 600 ) error { 601 // Set height 602 mem.height = height 603 mem.notifiedTxsAvailable = false 604 605 if preCheck != nil { 606 mem.preCheck = preCheck 607 } 608 if postCheck != nil { 609 mem.postCheck = postCheck 610 } 611 612 for i, tx := range txs { 613 if deliverTxResponses[i].Code == abci.CodeTypeOK { 614 // Add valid committed tx to the cache (if missing). 615 _ = mem.cache.Push(tx) 616 } else if !mem.config.KeepInvalidTxsInCache { 617 // Allow invalid transactions to be resubmitted. 618 mem.cache.Remove(tx) 619 } 620 621 // Remove committed tx from the mempool. 622 // 623 // Note an evil proposer can drop valid txs! 624 // Mempool before: 625 // 100 -> 101 -> 102 626 // Block, proposed by an evil proposer: 627 // 101 -> 102 628 // Mempool after: 629 // 100 630 // https://github.com/tendermint/tendermint/issues/3322. 631 if e, ok := mem.txsMap.Load(tx.Key()); ok { 632 mem.removeTx(tx, e.(*clist.CElement), false) 633 } 634 } 635 636 // Either recheck non-committed txs to see if they became invalid 637 // or just notify there're some txs left. 638 if mem.Size() > 0 { 639 if mem.config.Recheck { 640 mem.logger.Debug("recheck txs", "numtxs", mem.Size(), "height", height) 641 mem.recheckTxs() 642 // At this point, mem.txs are being rechecked. 643 // mem.recheckCursor re-scans mem.txs and possibly removes some txs. 644 // Before mem.Reap(), we should wait for mem.recheckCursor to be nil. 645 } else { 646 mem.notifyTxsAvailable() 647 } 648 } 649 650 // Update metrics 651 mem.metrics.Size.Set(float64(mem.Size())) 652 mem.metrics.SizeBytes.Set(float64(mem.SizeBytes())) 653 654 return nil 655 } 656 657 func (mem *CListMempool) recheckTxs() { 658 if mem.Size() == 0 { 659 panic("recheckTxs is called, but the mempool is empty") 660 } 661 662 mem.recheckCursor = mem.txs.Front() 663 mem.recheckEnd = mem.txs.Back() 664 665 // Push txs to proxyAppConn 666 // NOTE: globalCb may be called concurrently. 667 for e := mem.txs.Front(); e != nil; e = e.Next() { 668 memTx := e.Value.(*mempoolTx) 669 mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{ 670 Tx: memTx.tx, 671 Type: abci.CheckTxType_Recheck, 672 }) 673 } 674 675 mem.proxyAppConn.FlushAsync() 676 } 677 678 //-------------------------------------------------------------------------------- 679 680 // mempoolTx is a transaction that successfully ran 681 type mempoolTx struct { 682 height int64 // height that this tx had been validated in 683 gasWanted int64 // amount of gas this tx states it will require 684 tx types.Tx // 685 686 // ids of peers who've sent us this tx (as a map for quick lookups). 687 // senders: PeerID -> bool 688 senders sync.Map 689 } 690 691 // Height returns the height for this transaction 692 func (memTx *mempoolTx) Height() int64 { 693 return atomic.LoadInt64(&memTx.height) 694 }