From 70bff850651067d16163bc54c11c1687b9986432 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Tue, 14 Jun 2022 13:47:50 -0400 Subject: [PATCH] updates --- .../{bench_test.go => cache_bench_test.go} | 0 mempool/ids.go | 77 ++ mempool/ids_test.go | 23 + mempool/mempool.go | 132 ++- mempool/mock/mempool.go | 24 +- mempool/tx.go | 284 +----- mempool/v0/bench_test.go | 106 +++ mempool/v0/cache_test.go | 82 ++ mempool/v0/clist_mempool.go | 681 ++++++++++++++ mempool/v0/clist_mempool_test.go | 692 ++++++++++++++ mempool/v0/doc.go | 23 + mempool/v0/reactor.go | 304 ++++++ mempool/v0/reactor_test.go | 391 ++++++++ mempool/v1/mempool.go | 880 ++++++++++++++++++ mempool/v1/mempool_bench_test.go | 32 + mempool/v1/mempool_test.go | 529 +++++++++++ mempool/v1/priority_queue.go | 159 ++++ mempool/v1/priority_queue_test.go | 176 ++++ mempool/v1/reactor.go | 385 ++++++++ mempool/v1/reactor_test.go | 145 +++ mempool/v1/tx.go | 281 ++++++ mempool/v1/tx_test.go | 230 +++++ mempool_bak/bench_test.go | 41 + mempool_bak/cache.go | 107 +++ mempool_bak/cache_test.go | 35 + {mempool => mempool_bak}/clist_mempool.go | 0 .../clist_mempool_test.go | 0 {mempool => mempool_bak}/doc.go | 0 {mempool => mempool_bak}/errors.go | 0 mempool_bak/mempool.go | 127 +++ mempool_bak/metrics.go | 108 +++ mempool_bak/mock/mempool.go | 43 + {mempool => mempool_bak}/priority_queue.go | 0 {mempool => mempool_bak}/reactor.go | 0 {mempool => mempool_bak}/reactor_test.go | 0 mempool_bak/tx.go | 281 ++++++ 36 files changed, 6060 insertions(+), 318 deletions(-) rename mempool/{bench_test.go => cache_bench_test.go} (100%) create mode 100644 mempool/ids.go create mode 100644 mempool/ids_test.go create mode 100644 mempool/v0/bench_test.go create mode 100644 mempool/v0/cache_test.go create mode 100644 mempool/v0/clist_mempool.go create mode 100644 mempool/v0/clist_mempool_test.go create mode 100644 mempool/v0/doc.go create mode 100644 mempool/v0/reactor.go create mode 100644 mempool/v0/reactor_test.go create mode 100644 mempool/v1/mempool.go create mode 100644 mempool/v1/mempool_bench_test.go create mode 100644 mempool/v1/mempool_test.go create mode 100644 mempool/v1/priority_queue.go create mode 100644 mempool/v1/priority_queue_test.go create mode 100644 mempool/v1/reactor.go create mode 100644 mempool/v1/reactor_test.go create mode 100644 mempool/v1/tx.go create mode 100644 mempool/v1/tx_test.go create mode 100644 mempool_bak/bench_test.go create mode 100644 mempool_bak/cache.go create mode 100644 mempool_bak/cache_test.go rename {mempool => mempool_bak}/clist_mempool.go (100%) rename {mempool => mempool_bak}/clist_mempool_test.go (100%) rename {mempool => mempool_bak}/doc.go (100%) rename {mempool => mempool_bak}/errors.go (100%) create mode 100644 mempool_bak/mempool.go create mode 100644 mempool_bak/metrics.go create mode 100644 mempool_bak/mock/mempool.go rename {mempool => mempool_bak}/priority_queue.go (100%) rename {mempool => mempool_bak}/reactor.go (100%) rename {mempool => mempool_bak}/reactor_test.go (100%) create mode 100644 mempool_bak/tx.go diff --git a/mempool/bench_test.go b/mempool/cache_bench_test.go similarity index 100% rename from mempool/bench_test.go rename to mempool/cache_bench_test.go diff --git a/mempool/ids.go b/mempool/ids.go new file mode 100644 index 000000000..d50337710 --- /dev/null +++ b/mempool/ids.go @@ -0,0 +1,77 @@ +package mempool + +// import ( +// "fmt" + +// tmsync "github.com/tendermint/tendermint/internal/libs/sync" +// "github.com/tendermint/tendermint/types" +// ) + +// // nolint: revive +// // TODO: Rename type. +// type MempoolIDs struct { +// mtx tmsync.RWMutex +// peerMap map[types.NodeID]uint16 +// nextID uint16 // assumes that a node will never have over 65536 active peers +// activeIDs map[uint16]struct{} // used to check if a given peerID key is used +// } + +// func NewMempoolIDs() *MempoolIDs { +// return &MempoolIDs{ +// peerMap: make(map[types.NodeID]uint16), + +// // reserve UnknownPeerID for mempoolReactor.BroadcastTx +// activeIDs: map[uint16]struct{}{UnknownPeerID: {}}, +// nextID: 1, +// } +// } + +// // ReserveForPeer searches for the next unused ID and assigns it to the provided +// // peer. +// func (ids *MempoolIDs) ReserveForPeer(peerID types.NodeID) { +// ids.mtx.Lock() +// defer ids.mtx.Unlock() + +// curID := ids.nextPeerID() +// ids.peerMap[peerID] = curID +// ids.activeIDs[curID] = struct{}{} +// } + +// // Reclaim returns the ID reserved for the peer back to unused pool. +// func (ids *MempoolIDs) Reclaim(peerID types.NodeID) { +// ids.mtx.Lock() +// defer ids.mtx.Unlock() + +// removedID, ok := ids.peerMap[peerID] +// if ok { +// delete(ids.activeIDs, removedID) +// delete(ids.peerMap, peerID) +// } +// } + +// // GetForPeer returns an ID reserved for the peer. +// func (ids *MempoolIDs) GetForPeer(peerID types.NodeID) uint16 { +// ids.mtx.RLock() +// defer ids.mtx.RUnlock() + +// return ids.peerMap[peerID] +// } + +// // nextPeerID returns the next unused peer ID to use. We assume that the mutex +// // is already held. +// func (ids *MempoolIDs) nextPeerID() uint16 { +// if len(ids.activeIDs) == MaxActiveIDs { +// panic(fmt.Sprintf("node has maximum %d active IDs and wanted to get one more", MaxActiveIDs)) +// } + +// _, idExists := ids.activeIDs[ids.nextID] +// for idExists { +// ids.nextID++ +// _, idExists = ids.activeIDs[ids.nextID] +// } + +// curID := ids.nextID +// ids.nextID++ + +// return curID +// } diff --git a/mempool/ids_test.go b/mempool/ids_test.go new file mode 100644 index 000000000..2d72076e7 --- /dev/null +++ b/mempool/ids_test.go @@ -0,0 +1,23 @@ +package mempool + +// import ( +// "testing" + +// "github.com/stretchr/testify/require" +// "github.com/tendermint/tendermint/types" +// ) + +// func TestMempoolIDsBasic(t *testing.T) { +// ids := NewMempoolIDs() + +// peerID, err := types.NewNodeID("0011223344556677889900112233445566778899") +// require.NoError(t, err) + +// ids.ReserveForPeer(peerID) +// require.EqualValues(t, 1, ids.GetForPeer(peerID)) +// ids.Reclaim(peerID) + +// ids.ReserveForPeer(peerID) +// require.EqualValues(t, 2, ids.GetForPeer(peerID)) +// ids.Reclaim(peerID) +// } diff --git a/mempool/mempool.go b/mempool/mempool.go index eece0cf1b..08717fd95 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -1,43 +1,67 @@ package mempool import ( + "crypto/sha256" + "errors" "fmt" + "math" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" ) +const ( + MempoolChannel = byte(0x30) + + // PeerCatchupSleepIntervalMS defines how much time to sleep if a peer is behind + PeerCatchupSleepIntervalMS = 100 + + // UnknownPeerID is the peer ID to use when running CheckTx when there is + // no peer (e.g. RPC) + UnknownPeerID uint16 = 0 + + MaxActiveIDs = math.MaxUint16 +) + // Mempool defines the mempool interface. // // Updates to the mempool need to be synchronized with committing a block so -// apps can reset their transient state on Commit. +// applications can reset their transient state on Commit. type Mempool interface { // CheckTx executes a new transaction against the application to determine // its validity and whether it should be added to the mempool. CheckTx(tx types.Tx, callback func(*abci.Response), txInfo TxInfo) error + // RemoveTxByKey removes a transaction, identified by its key, + // from the mempool. + RemoveTxByKey(txKey types.TxKey) error + // ReapMaxBytesMaxGas reaps transactions from the mempool up to maxBytes // bytes total with the condition that the total gasWanted must be less than // maxGas. + // // If both maxes are negative, there is no cap on the size of all returned // transactions (~ all available transactions). ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs - // ReapMaxTxs reaps up to max transactions from the mempool. - // If max is negative, there is no cap on the size of all returned - // transactions (~ all available transactions). + // ReapMaxTxs reaps up to max transactions from the mempool. If max is + // negative, there is no cap on the size of all returned transactions + // (~ all available transactions). ReapMaxTxs(max int) types.Txs - // Lock locks the mempool. The consensus must be able to hold lock to safely update. + // Lock locks the mempool. The consensus must be able to hold lock to safely + // update. Lock() // Unlock unlocks the mempool. Unlock() - // Update informs the mempool that the given txs were committed and can be discarded. - // NOTE: this should be called *after* block is committed by consensus. - // NOTE: Lock/Unlock must be managed by caller + // Update informs the mempool that the given txs were committed and can be + // discarded. + // + // NOTE: + // 1. This should be called *after* block is committed by consensus. + // 2. Lock/Unlock must be managed by the caller. Update( blockHeight int64, blockTxs types.Txs, @@ -46,17 +70,21 @@ type Mempool interface { newPostFn PostCheckFunc, ) error - // FlushAppConn flushes the mempool connection to ensure async reqResCb calls are - // done. E.g. from CheckTx. - // NOTE: Lock/Unlock must be managed by caller + // FlushAppConn flushes the mempool connection to ensure async callback calls + // are done, e.g. from CheckTx. + // + // NOTE: + // 1. Lock/Unlock must be managed by caller. FlushAppConn() error - // Flush removes all transactions from the mempool and cache + // Flush removes all transactions from the mempool and caches. Flush() - // TxsAvailable returns a channel which fires once for every height, - // and only when transactions are available in the mempool. - // NOTE: the returned channel may be nil if EnableTxsAvailable was not called. + // TxsAvailable returns a channel which fires once for every height, and only + // when transactions are available in the mempool. + // + // NOTE: + // 1. The returned channel may be nil if EnableTxsAvailable was not called. TxsAvailable() <-chan struct{} // EnableTxsAvailable initializes the TxsAvailable channel, ensuring it will @@ -70,8 +98,6 @@ type Mempool interface { SizeBytes() int64 } -//-------------------------------------------------------------------------------- - // PreCheckFunc is an optional filter executed before CheckTx and rejects // transaction if false is returned. An example would be to ensure that a // transaction doesn't exceeded the block size. @@ -82,27 +108,16 @@ type PreCheckFunc func(types.Tx) error // transaction doesn't require more gas than available for the block. type PostCheckFunc func(types.Tx, *abci.ResponseCheckTx) error -// TxInfo are parameters that get passed when attempting to add a tx to the -// mempool. -type TxInfo struct { - // SenderID is the internal peer ID used in the mempool to identify the - // sender, storing 2 bytes with each tx instead of 20 bytes for the p2p.ID. - SenderID uint16 - // SenderP2PID is the actual p2p.ID of the sender, used e.g. for logging. - SenderP2PID p2p.ID -} - -//-------------------------------------------------------------------------------- - -// PreCheckMaxBytes checks that the size of the transaction is smaller or equal to the expected maxBytes. +// PreCheckMaxBytes checks that the size of the transaction is smaller or equal +// to the expected maxBytes. func PreCheckMaxBytes(maxBytes int64) PreCheckFunc { return func(tx types.Tx) error { txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}) if txSize > maxBytes { - return fmt.Errorf("tx size is too big: %d, max: %d", - txSize, maxBytes) + return fmt.Errorf("tx size is too big: %d, max: %d", txSize, maxBytes) } + return nil } } @@ -122,6 +137,57 @@ func PostCheckMaxGas(maxGas int64) PostCheckFunc { return fmt.Errorf("gas wanted %d is greater than max gas %d", res.GasWanted, maxGas) } + return nil } } + +// ErrTxInCache is returned to the client if we saw tx earlier +var ErrTxInCache = errors.New("tx already exists in cache") + +// TxKey is the fixed length array key used as an index. +type TxKey [sha256.Size]byte + +// ErrTxTooLarge defines an error when a transaction is too big to be sent in a +// message to other peers. +type ErrTxTooLarge struct { + Max int + Actual int +} + +func (e ErrTxTooLarge) Error() string { + return fmt.Sprintf("Tx too large. Max size is %d, but got %d", e.Max, e.Actual) +} + +// ErrMempoolIsFull defines an error where Tendermint and the application cannot +// handle that much load. +type ErrMempoolIsFull struct { + NumTxs int + MaxTxs int + TxsBytes int64 + MaxTxsBytes int64 +} + +func (e ErrMempoolIsFull) Error() string { + return fmt.Sprintf( + "mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", + e.NumTxs, + e.MaxTxs, + e.TxsBytes, + e.MaxTxsBytes, + ) +} + +// ErrPreCheck defines an error where a transaction fails a pre-check. +type ErrPreCheck struct { + Reason error +} + +func (e ErrPreCheck) Error() string { + return e.Reason.Error() +} + +// IsPreCheckError returns true if err is due to pre check failure. +func IsPreCheckError(err error) bool { + return errors.As(err, &ErrPreCheck{}) +} diff --git a/mempool/mock/mempool.go b/mempool/mock/mempool.go index cd8df2198..8e6f0c7bf 100644 --- a/mempool/mock/mempool.go +++ b/mempool/mock/mempool.go @@ -1,32 +1,34 @@ package mock import ( + "context" + abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/clist" - mempl "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/internal/libs/clist" + "github.com/tendermint/tendermint/internal/mempool" "github.com/tendermint/tendermint/types" ) // Mempool is an empty implementation of a Mempool, useful for testing. type Mempool struct{} -var _ mempl.Mempool = Mempool{} +var _ mempool.Mempool = Mempool{} -func (Mempool) Lock() {} -func (Mempool) Unlock() {} -func (Mempool) Size() int { return 0 } -func (Mempool) SizeBytes() int64 { return 0 } -func (Mempool) CheckTx(_ types.Tx, _ func(*abci.Response), _ mempl.TxInfo) error { +func (Mempool) Lock() {} +func (Mempool) Unlock() {} +func (Mempool) Size() int { return 0 } +func (Mempool) CheckTx(_ context.Context, _ types.Tx, _ func(*abci.Response), _ mempool.TxInfo) error { return nil } +func (Mempool) RemoveTxByKey(txKey types.TxKey) error { return nil } func (Mempool) ReapMaxBytesMaxGas(_, _ int64) types.Txs { return types.Txs{} } func (Mempool) ReapMaxTxs(n int) types.Txs { return types.Txs{} } func (Mempool) Update( _ int64, _ types.Txs, _ []*abci.ResponseDeliverTx, - _ mempl.PreCheckFunc, - _ mempl.PostCheckFunc, + _ mempool.PreCheckFunc, + _ mempool.PostCheckFunc, ) error { return nil } @@ -34,7 +36,7 @@ func (Mempool) Flush() {} func (Mempool) FlushAppConn() error { return nil } func (Mempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) } func (Mempool) EnableTxsAvailable() {} -func (Mempool) TxsBytes() int64 { return 0 } +func (Mempool) SizeBytes() int64 { return 0 } func (Mempool) TxsFront() *clist.CElement { return nil } func (Mempool) TxsWaitChan() <-chan struct{} { return nil } diff --git a/mempool/tx.go b/mempool/tx.go index f8d02f0e9..d13f3d6b8 100644 --- a/mempool/tx.go +++ b/mempool/tx.go @@ -1,281 +1,17 @@ package mempool import ( - "sort" - "time" - - "github.com/tendermint/tendermint/libs/clist" - tmsync "github.com/tendermint/tendermint/libs/sync" - "github.com/tendermint/tendermint/types" + "github.com/tendermint/tendermint/p2p" ) -// WrappedTx defines a wrapper around a raw transaction with additional metadata -// that is used for indexing. -type WrappedTx struct { - // tx represents the raw binary transaction data - tx types.Tx +// TxInfo are parameters that get passed when attempting to add a tx to the +// mempool. +type TxInfo struct { + // SenderID is the internal peer ID used in the mempool to identify the + // sender, storing two bytes with each transaction instead of 20 bytes for + // the types.NodeID. + SenderID uint16 - // hash defines the transaction hash and the primary key used in the mempool - hash types.TxKey - - // height defines the height at which the transaction was validated at - height int64 - - // gasWanted defines the amount of gas the transaction sender requires - gasWanted int64 - - // priority defines the transaction's priority as specified by the application - // in the ResponseCheckTx response. - priority int64 - - // sender defines the transaction's sender as specified by the application in - // the ResponseCheckTx response. - sender string - - // timestamp is the time at which the node first received the transaction from - // a peer. It is used as a second dimension is prioritizing transactions when - // two transactions have the same priority. - timestamp time.Time - - // peers records a mapping of all peers that sent a given transaction - peers map[uint16]struct{} - - // heapIndex defines the index of the item in the heap - heapIndex int - - // gossipEl references the linked-list element in the gossip index - gossipEl *clist.CElement - - // removed marks the transaction as removed from the mempool. This is set - // during RemoveTx and is needed due to the fact that a given existing - // transaction in the mempool can be evicted when it is simultaneously having - // a reCheckTx callback executed. - removed bool -} - -func (wtx *WrappedTx) Size() int { - return len(wtx.tx) -} - -// TxStore implements a thread-safe mapping of valid transaction(s). -// -// NOTE: -// - Concurrent read-only access to a *WrappedTx object is OK. However, mutative -// access is not allowed. Regardless, it is not expected for the mempool to -// need mutative access. -type TxStore struct { - mtx tmsync.RWMutex - hashTxs map[types.TxKey]*WrappedTx // primary index - senderTxs map[string]*WrappedTx // sender is defined by the ABCI application -} - -func NewTxStore() *TxStore { - return &TxStore{ - senderTxs: make(map[string]*WrappedTx), - hashTxs: make(map[types.TxKey]*WrappedTx), - } -} - -// Size returns the total number of transactions in the store. -func (txs *TxStore) Size() int { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - return len(txs.hashTxs) -} - -// GetAllTxs returns all the transactions currently in the store. -func (txs *TxStore) GetAllTxs() []*WrappedTx { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - wTxs := make([]*WrappedTx, len(txs.hashTxs)) - i := 0 - for _, wtx := range txs.hashTxs { - wTxs[i] = wtx - i++ - } - - return wTxs -} - -// GetTxBySender returns a *WrappedTx by the transaction's sender property -// defined by the ABCI application. -func (txs *TxStore) GetTxBySender(sender string) *WrappedTx { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - return txs.senderTxs[sender] -} - -// GetTxByHash returns a *WrappedTx by the transaction's hash. -func (txs *TxStore) GetTxByHash(hash types.TxKey) *WrappedTx { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - return txs.hashTxs[hash] -} - -// IsTxRemoved returns true if a transaction by hash is marked as removed and -// false otherwise. -func (txs *TxStore) IsTxRemoved(hash types.TxKey) bool { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - wtx, ok := txs.hashTxs[hash] - if ok { - return wtx.removed - } - - return false -} - -// SetTx stores a *WrappedTx by it's hash. If the transaction also contains a -// non-empty sender, we additionally store the transaction by the sender as -// defined by the ABCI application. -func (txs *TxStore) SetTx(wtx *WrappedTx) { - txs.mtx.Lock() - defer txs.mtx.Unlock() - - if len(wtx.sender) > 0 { - txs.senderTxs[wtx.sender] = wtx - } - - txs.hashTxs[wtx.tx.Key()] = wtx -} - -// RemoveTx removes a *WrappedTx from the transaction store. It deletes all -// indexes of the transaction. -func (txs *TxStore) RemoveTx(wtx *WrappedTx) { - txs.mtx.Lock() - defer txs.mtx.Unlock() - - if len(wtx.sender) > 0 { - delete(txs.senderTxs, wtx.sender) - } - - delete(txs.hashTxs, wtx.tx.Key()) - wtx.removed = true -} - -// TxHasPeer returns true if a transaction by hash has a given peer ID and false -// otherwise. If the transaction does not exist, false is returned. -func (txs *TxStore) TxHasPeer(hash types.TxKey, peerID uint16) bool { - txs.mtx.RLock() - defer txs.mtx.RUnlock() - - wtx := txs.hashTxs[hash] - if wtx == nil { - return false - } - - _, ok := wtx.peers[peerID] - return ok -} - -// GetOrSetPeerByTxHash looks up a WrappedTx by transaction hash and adds the -// given peerID to the WrappedTx's set of peers that sent us this transaction. -// We return true if we've already recorded the given peer for this transaction -// and false otherwise. If the transaction does not exist by hash, we return -// (nil, false). -func (txs *TxStore) GetOrSetPeerByTxHash(hash types.TxKey, peerID uint16) (*WrappedTx, bool) { - txs.mtx.Lock() - defer txs.mtx.Unlock() - - wtx := txs.hashTxs[hash] - if wtx == nil { - return nil, false - } - - if wtx.peers == nil { - wtx.peers = make(map[uint16]struct{}) - } - - if _, ok := wtx.peers[peerID]; ok { - return wtx, true - } - - wtx.peers[peerID] = struct{}{} - return wtx, false -} - -// WrappedTxList implements a thread-safe list of *WrappedTx objects that can be -// used to build generic transaction indexes in the mempool. It accepts a -// comparator function, less(a, b *WrappedTx) bool, that compares two WrappedTx -// references which is used during Insert in order to determine sorted order. If -// less returns true, a <= b. -type WrappedTxList struct { - mtx tmsync.RWMutex - txs []*WrappedTx - less func(*WrappedTx, *WrappedTx) bool -} - -func NewWrappedTxList(less func(*WrappedTx, *WrappedTx) bool) *WrappedTxList { - return &WrappedTxList{ - txs: make([]*WrappedTx, 0), - less: less, - } -} - -// Size returns the number of WrappedTx objects in the list. -func (wtl *WrappedTxList) Size() int { - wtl.mtx.RLock() - defer wtl.mtx.RUnlock() - - return len(wtl.txs) -} - -// Reset resets the list of transactions to an empty list. -func (wtl *WrappedTxList) Reset() { - wtl.mtx.Lock() - defer wtl.mtx.Unlock() - - wtl.txs = make([]*WrappedTx, 0) -} - -// Insert inserts a WrappedTx reference into the sorted list based on the list's -// comparator function. -func (wtl *WrappedTxList) Insert(wtx *WrappedTx) { - wtl.mtx.Lock() - defer wtl.mtx.Unlock() - - i := sort.Search(len(wtl.txs), func(i int) bool { - return wtl.less(wtl.txs[i], wtx) - }) - - if i == len(wtl.txs) { - // insert at the end - wtl.txs = append(wtl.txs, wtx) - return - } - - // Make space for the inserted element by shifting values at the insertion - // index up one index. - // - // NOTE: The call to append does not allocate memory when cap(wtl.txs) > len(wtl.txs). - wtl.txs = append(wtl.txs[:i+1], wtl.txs[i:]...) - wtl.txs[i] = wtx -} - -// Remove attempts to remove a WrappedTx from the sorted list. -func (wtl *WrappedTxList) Remove(wtx *WrappedTx) { - wtl.mtx.Lock() - defer wtl.mtx.Unlock() - - i := sort.Search(len(wtl.txs), func(i int) bool { - return wtl.less(wtl.txs[i], wtx) - }) - - // Since the list is sorted, we evaluate all elements starting at i. Note, if - // the element does not exist, we may potentially evaluate the entire remainder - // of the list. However, a caller should not be expected to call Remove with a - // non-existing element. - for i < len(wtl.txs) { - if wtl.txs[i] == wtx { - wtl.txs = append(wtl.txs[:i], wtl.txs[i+1:]...) - return - } - - i++ - } + // SenderP2PID is the actual p2p.ID of the sender, used e.g. for logging. + SenderP2PID p2p.ID } diff --git a/mempool/v0/bench_test.go b/mempool/v0/bench_test.go new file mode 100644 index 000000000..46dc64797 --- /dev/null +++ b/mempool/v0/bench_test.go @@ -0,0 +1,106 @@ +package v0 + +import ( + "encoding/binary" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/abci/example/kvstore" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/proxy" +) + +func BenchmarkReap(b *testing.B) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(b, err) + defer cleanup() + + mp.config.Size = 100000 + + size := 10000 + for i := 0; i < size; i++ { + tx := make([]byte, 8) + binary.BigEndian.PutUint64(tx, uint64(i)) + if err := mp.CheckTx(tx, nil, mempool.TxInfo{}); err != nil { + b.Fatal(err) + } + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + mp.ReapMaxBytesMaxGas(100000000, 10000000) + } +} + +func BenchmarkCheckTx(b *testing.B) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(b, err) + defer cleanup() + + mp.config.Size = 1000000 + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + b.StopTimer() + tx := make([]byte, 8) + binary.BigEndian.PutUint64(tx, uint64(i)) + b.StartTimer() + + if err := mp.CheckTx(tx, nil, mempool.TxInfo{}); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkParallelCheckTx(b *testing.B) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(b, err) + defer cleanup() + + mp.config.Size = 100000000 + + var txcnt uint64 + next := func() uint64 { + return atomic.AddUint64(&txcnt, 1) - 1 + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + tx := make([]byte, 8) + binary.BigEndian.PutUint64(tx, next()) + if err := mp.CheckTx(tx, nil, mempool.TxInfo{}); err != nil { + b.Fatal(err) + } + } + }) +} + +func BenchmarkCheckDuplicateTx(b *testing.B) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(b, err) + defer cleanup() + + mp.config.Size = 1000000 + + for i := 0; i < b.N; i++ { + tx := make([]byte, 8) + binary.BigEndian.PutUint64(tx, uint64(i)) + if err := mp.CheckTx(tx, nil, mempool.TxInfo{}); err != nil { + b.Fatal(err) + } + + if err := mp.CheckTx(tx, nil, mempool.TxInfo{}); err == nil { + b.Fatal("tx should be duplicate") + } + } +} diff --git a/mempool/v0/cache_test.go b/mempool/v0/cache_test.go new file mode 100644 index 000000000..4dc8e2720 --- /dev/null +++ b/mempool/v0/cache_test.go @@ -0,0 +1,82 @@ +package v0 + +import ( + "crypto/sha256" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/abci/example/kvstore" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/types" +) + +func TestCacheAfterUpdate(t *testing.T) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + // reAddIndices & txsInCache can have elements > numTxsToCreate + // also assumes max index is 255 for convenience + // txs in cache also checks order of elements + tests := []struct { + numTxsToCreate int + updateIndices []int + reAddIndices []int + txsInCache []int + }{ + {1, []int{}, []int{1}, []int{1, 0}}, // adding new txs works + {2, []int{1}, []int{}, []int{1, 0}}, // update doesn't remove tx from cache + {2, []int{2}, []int{}, []int{2, 1, 0}}, // update adds new tx to cache + {2, []int{1}, []int{1}, []int{1, 0}}, // re-adding after update doesn't make dupe + } + for tcIndex, tc := range tests { + for i := 0; i < tc.numTxsToCreate; i++ { + tx := types.Tx{byte(i)} + err := mp.CheckTx(tx, nil, mempool.TxInfo{}) + require.NoError(t, err) + } + + updateTxs := []types.Tx{} + for _, v := range tc.updateIndices { + tx := types.Tx{byte(v)} + updateTxs = append(updateTxs, tx) + } + err := mp.Update(int64(tcIndex), updateTxs, abciResponses(len(updateTxs), abci.CodeTypeOK), nil, nil) + require.NoError(t, err) + + for _, v := range tc.reAddIndices { + tx := types.Tx{byte(v)} + _ = mp.CheckTx(tx, nil, mempool.TxInfo{}) + } + + cache := mp.cache.(*mempool.LRUTxCache) + node := cache.GetList().Front() + counter := 0 + for node != nil { + require.NotEqual(t, len(tc.txsInCache), counter, + "cache larger than expected on testcase %d", tcIndex) + + nodeVal := node.Value.(types.TxKey) + expectedBz := sha256.Sum256([]byte{byte(tc.txsInCache[len(tc.txsInCache)-counter-1])}) + // Reference for reading the errors: + // >>> sha256('\x00').hexdigest() + // '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d' + // >>> sha256('\x01').hexdigest() + // '4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a' + // >>> sha256('\x02').hexdigest() + // 'dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986' + + require.EqualValues(t, expectedBz, nodeVal, "Equality failed on index %d, tc %d", counter, tcIndex) + counter++ + node = node.Next() + } + require.Equal(t, len(tc.txsInCache), counter, + "cache smaller than expected on testcase %d", tcIndex) + mp.Flush() + } +} diff --git a/mempool/v0/clist_mempool.go b/mempool/v0/clist_mempool.go new file mode 100644 index 000000000..5bdf96187 --- /dev/null +++ b/mempool/v0/clist_mempool.go @@ -0,0 +1,681 @@ +package v0 + +import ( + "bytes" + "errors" + "sync" + "sync/atomic" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/clist" + "github.com/tendermint/tendermint/libs/log" + tmmath "github.com/tendermint/tendermint/libs/math" + tmsync "github.com/tendermint/tendermint/libs/sync" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/types" +) + +// CListMempool is an ordered in-memory pool for transactions before they are +// proposed in a consensus round. Transaction validity is checked using the +// CheckTx abci message before the transaction is added to the pool. The +// mempool uses a concurrent list structure for storing transactions that can +// be efficiently accessed by multiple concurrent readers. +type CListMempool struct { + // Atomic integers + height int64 // the last block Update()'d to + txsBytes int64 // total size of mempool, in bytes + + // notify listeners (ie. consensus) when txs are available + notifiedTxsAvailable bool + txsAvailable chan struct{} // fires once for each height, when the mempool is not empty + + config *config.MempoolConfig + + // Exclusive mutex for Update method to prevent concurrent execution of + // CheckTx or ReapMaxBytesMaxGas(ReapMaxTxs) methods. + updateMtx tmsync.RWMutex + preCheck mempool.PreCheckFunc + postCheck mempool.PostCheckFunc + + txs *clist.CList // concurrent linked-list of good txs + proxyAppConn proxy.AppConnMempool + + // Track whether we're rechecking txs. + // These are not protected by a mutex and are expected to be mutated in + // serial (ie. by abci responses which are called in serial). + recheckCursor *clist.CElement // next expected response + recheckEnd *clist.CElement // re-checking stops here + + // Map for quick access to txs to record sender in CheckTx. + // txsMap: txKey -> CElement + txsMap sync.Map + + // Keep a cache of already-seen txs. + // This reduces the pressure on the proxyApp. + cache mempool.TxCache + + logger log.Logger + metrics *mempool.Metrics +} + +var _ mempool.Mempool = &CListMempool{} + +// CListMempoolOption sets an optional parameter on the mempool. +type CListMempoolOption func(*CListMempool) + +// NewCListMempool returns a new mempool with the given configuration and +// connection to an application. +func NewCListMempool( + cfg *config.MempoolConfig, + proxyAppConn proxy.AppConnMempool, + height int64, + options ...CListMempoolOption, +) *CListMempool { + + mp := &CListMempool{ + config: cfg, + proxyAppConn: proxyAppConn, + txs: clist.New(), + height: height, + recheckCursor: nil, + recheckEnd: nil, + logger: log.NewNopLogger(), + metrics: mempool.NopMetrics(), + } + + if cfg.CacheSize > 0 { + mp.cache = mempool.NewLRUTxCache(cfg.CacheSize) + } else { + mp.cache = mempool.NopTxCache{} + } + + proxyAppConn.SetResponseCallback(mp.globalCb) + + for _, option := range options { + option(mp) + } + + return mp +} + +// NOTE: not thread safe - should only be called once, on startup +func (mem *CListMempool) EnableTxsAvailable() { + mem.txsAvailable = make(chan struct{}, 1) +} + +// SetLogger sets the Logger. +func (mem *CListMempool) SetLogger(l log.Logger) { + mem.logger = l +} + +// WithPreCheck sets a filter for the mempool to reject a tx if f(tx) returns +// false. This is ran before CheckTx. Only applies to the first created block. +// After that, Update overwrites the existing value. +func WithPreCheck(f mempool.PreCheckFunc) CListMempoolOption { + return func(mem *CListMempool) { mem.preCheck = f } +} + +// WithPostCheck sets a filter for the mempool to reject a tx if f(tx) returns +// false. This is ran after CheckTx. Only applies to the first created block. +// After that, Update overwrites the existing value. +func WithPostCheck(f mempool.PostCheckFunc) CListMempoolOption { + return func(mem *CListMempool) { mem.postCheck = f } +} + +// WithMetrics sets the metrics. +func WithMetrics(metrics *mempool.Metrics) CListMempoolOption { + return func(mem *CListMempool) { mem.metrics = metrics } +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) Lock() { + mem.updateMtx.Lock() +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) Unlock() { + mem.updateMtx.Unlock() +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) Size() int { + return mem.txs.Len() +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) SizeBytes() int64 { + return atomic.LoadInt64(&mem.txsBytes) +} + +// Lock() must be help by the caller during execution. +func (mem *CListMempool) FlushAppConn() error { + return mem.proxyAppConn.FlushSync() +} + +// XXX: Unsafe! Calling Flush may leave mempool in inconsistent state. +func (mem *CListMempool) Flush() { + mem.updateMtx.RLock() + defer mem.updateMtx.RUnlock() + + _ = atomic.SwapInt64(&mem.txsBytes, 0) + mem.cache.Reset() + + for e := mem.txs.Front(); e != nil; e = e.Next() { + mem.txs.Remove(e) + e.DetachPrev() + } + + mem.txsMap.Range(func(key, _ interface{}) bool { + mem.txsMap.Delete(key) + return true + }) +} + +// TxsFront returns the first transaction in the ordered list for peer +// goroutines to call .NextWait() on. +// FIXME: leaking implementation details! +// +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) TxsFront() *clist.CElement { + return mem.txs.Front() +} + +// TxsWaitChan returns a channel to wait on transactions. It will be closed +// once the mempool is not empty (ie. the internal `mem.txs` has at least one +// element) +// +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) TxsWaitChan() <-chan struct{} { + return mem.txs.WaitChan() +} + +// It blocks if we're waiting on Update() or Reap(). +// cb: A callback from the CheckTx command. +// It gets called from another goroutine. +// CONTRACT: Either cb will get called, or err returned. +// +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) CheckTx( + tx types.Tx, + cb func(*abci.Response), + txInfo mempool.TxInfo, +) error { + + mem.updateMtx.RLock() + // use defer to unlock mutex because application (*local client*) might panic + defer mem.updateMtx.RUnlock() + + txSize := len(tx) + + if err := mem.isFull(txSize); err != nil { + return err + } + + if txSize > mem.config.MaxTxBytes { + return mempool.ErrTxTooLarge{ + Max: mem.config.MaxTxBytes, + Actual: txSize, + } + } + + if mem.preCheck != nil { + if err := mem.preCheck(tx); err != nil { + return mempool.ErrPreCheck{ + Reason: err, + } + } + } + + // NOTE: proxyAppConn may error if tx buffer is full + if err := mem.proxyAppConn.Error(); err != nil { + return err + } + + if !mem.cache.Push(tx) { // if the transaction already exists in the cache + // Record a new sender for a tx we've already seen. + // Note it's possible a tx is still in the cache but no longer in the mempool + // (eg. after committing a block, txs are removed from mempool but not cache), + // so we only record the sender for txs still in the mempool. + if e, ok := mem.txsMap.Load(tx.Key()); ok { + memTx := e.(*clist.CElement).Value.(*mempoolTx) + _, loaded := memTx.senders.LoadOrStore(txInfo.SenderID, true) + // TODO: consider punishing peer for dups, + // its non-trivial since invalid txs can become valid, + // but they can spam the same tx with little cost to them atm. + if loaded { + return mempool.ErrTxInCache + } + } + + mem.logger.Debug("tx exists already in cache", "tx_hash", tx.Hash()) + return nil + } + + reqRes := mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{Tx: tx}) + reqRes.SetCallback(mem.reqResCb(tx, txInfo.SenderID, txInfo.SenderP2PID, cb)) + + return nil +} + +// Global callback that will be called after every ABCI response. +// Having a single global callback avoids needing to set a callback for each request. +// However, processing the checkTx response requires the peerID (so we can track which txs we heard from who), +// and peerID is not included in the ABCI request, so we have to set request-specific callbacks that +// include this information. If we're not in the midst of a recheck, this function will just return, +// so the request specific callback can do the work. +// +// When rechecking, we don't need the peerID, so the recheck callback happens +// here. +func (mem *CListMempool) globalCb(req *abci.Request, res *abci.Response) { + if mem.recheckCursor == nil { + return + } + + mem.metrics.RecheckTimes.Add(1) + mem.resCbRecheck(req, res) + + // update metrics + mem.metrics.Size.Set(float64(mem.Size())) +} + +// Request specific callback that should be set on individual reqRes objects +// to incorporate local information when processing the response. +// This allows us to track the peer that sent us this tx, so we can avoid sending it back to them. +// NOTE: alternatively, we could include this information in the ABCI request itself. +// +// External callers of CheckTx, like the RPC, can also pass an externalCb through here that is called +// when all other response processing is complete. +// +// Used in CheckTx to record PeerID who sent us the tx. +func (mem *CListMempool) reqResCb( + tx []byte, + peerID uint16, + peerP2PID p2p.ID, + externalCb func(*abci.Response), +) func(res *abci.Response) { + return func(res *abci.Response) { + if mem.recheckCursor != nil { + // this should never happen + panic("recheck cursor is not nil in reqResCb") + } + + mem.resCbFirstTime(tx, peerID, peerP2PID, res) + + // update metrics + mem.metrics.Size.Set(float64(mem.Size())) + + // passed in by the caller of CheckTx, eg. the RPC + if externalCb != nil { + externalCb(res) + } + } +} + +// Called from: +// - resCbFirstTime (lock not held) if tx is valid +func (mem *CListMempool) addTx(memTx *mempoolTx) { + e := mem.txs.PushBack(memTx) + mem.txsMap.Store(memTx.tx.Key(), e) + atomic.AddInt64(&mem.txsBytes, int64(len(memTx.tx))) + mem.metrics.TxSizeBytes.Observe(float64(len(memTx.tx))) +} + +// Called from: +// - Update (lock held) if tx was committed +// - resCbRecheck (lock not held) if tx was invalidated +func (mem *CListMempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromCache bool) { + mem.txs.Remove(elem) + elem.DetachPrev() + mem.txsMap.Delete(tx.Key()) + atomic.AddInt64(&mem.txsBytes, int64(-len(tx))) + + if removeFromCache { + mem.cache.Remove(tx) + } +} + +// RemoveTxByKey removes a transaction from the mempool by its TxKey index. +func (mem *CListMempool) RemoveTxByKey(txKey types.TxKey) error { + if e, ok := mem.txsMap.Load(txKey); ok { + memTx := e.(*clist.CElement).Value.(*mempoolTx) + if memTx != nil { + mem.removeTx(memTx.tx, e.(*clist.CElement), false) + return nil + } + return errors.New("transaction not found") + } + return errors.New("invalid transaction found") +} + +func (mem *CListMempool) isFull(txSize int) error { + var ( + memSize = mem.Size() + txsBytes = mem.SizeBytes() + ) + + if memSize >= mem.config.Size || int64(txSize)+txsBytes > mem.config.MaxTxsBytes { + return mempool.ErrMempoolIsFull{ + NumTxs: memSize, + MaxTxs: mem.config.Size, + TxsBytes: txsBytes, + MaxTxsBytes: mem.config.MaxTxsBytes, + } + } + + return nil +} + +// callback, which is called after the app checked the tx for the first time. +// +// The case where the app checks the tx for the second and subsequent times is +// handled by the resCbRecheck callback. +func (mem *CListMempool) resCbFirstTime( + tx []byte, + peerID uint16, + peerP2PID p2p.ID, + res *abci.Response, +) { + switch r := res.Value.(type) { + case *abci.Response_CheckTx: + var postCheckErr error + if mem.postCheck != nil { + postCheckErr = mem.postCheck(tx, r.CheckTx) + } + if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { + // Check mempool isn't full again to reduce the chance of exceeding the + // limits. + if err := mem.isFull(len(tx)); err != nil { + // remove from cache (mempool might have a space later) + mem.cache.Remove(tx) + mem.logger.Error(err.Error()) + return + } + + memTx := &mempoolTx{ + height: mem.height, + gasWanted: r.CheckTx.GasWanted, + tx: tx, + } + memTx.senders.Store(peerID, true) + mem.addTx(memTx) + mem.logger.Debug( + "added good transaction", + "tx", types.Tx(tx).Hash(), + "res", r, + "height", memTx.height, + "total", mem.Size(), + ) + mem.notifyTxsAvailable() + } else { + // ignore bad transaction + mem.logger.Debug( + "rejected bad transaction", + "tx", types.Tx(tx).Hash(), + "peerID", peerP2PID, + "res", r, + "err", postCheckErr, + ) + mem.metrics.FailedTxs.Add(1) + + if !mem.config.KeepInvalidTxsInCache { + // remove from cache (it might be good later) + mem.cache.Remove(tx) + } + } + + default: + // ignore other messages + } +} + +// callback, which is called after the app rechecked the tx. +// +// The case where the app checks the tx for the first time is handled by the +// resCbFirstTime callback. +func (mem *CListMempool) resCbRecheck(req *abci.Request, res *abci.Response) { + switch r := res.Value.(type) { + case *abci.Response_CheckTx: + tx := req.GetCheckTx().Tx + memTx := mem.recheckCursor.Value.(*mempoolTx) + + // Search through the remaining list of tx to recheck for a transaction that matches + // the one we received from the ABCI application. + for { + if bytes.Equal(tx, memTx.tx) { + // We've found a tx in the recheck list that matches the tx that we + // received from the ABCI application. + // Break, and use this transaction for further checks. + break + } + + mem.logger.Error( + "re-CheckTx transaction mismatch", + "got", types.Tx(tx), + "expected", memTx.tx, + ) + + if mem.recheckCursor == mem.recheckEnd { + // we reached the end of the recheckTx list without finding a tx + // matching the one we received from the ABCI application. + // Return without processing any tx. + mem.recheckCursor = nil + return + } + + mem.recheckCursor = mem.recheckCursor.Next() + memTx = mem.recheckCursor.Value.(*mempoolTx) + } + + var postCheckErr error + if mem.postCheck != nil { + postCheckErr = mem.postCheck(tx, r.CheckTx) + } + + if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { + // Good, nothing to do. + } else { + // Tx became invalidated due to newly committed block. + mem.logger.Debug("tx is no longer valid", "tx", types.Tx(tx).Hash(), "res", r, "err", postCheckErr) + // NOTE: we remove tx from the cache because it might be good later + mem.removeTx(tx, mem.recheckCursor, !mem.config.KeepInvalidTxsInCache) + } + if mem.recheckCursor == mem.recheckEnd { + mem.recheckCursor = nil + } else { + mem.recheckCursor = mem.recheckCursor.Next() + } + if mem.recheckCursor == nil { + // Done! + mem.logger.Debug("done rechecking txs") + + // incase the recheck removed all txs + if mem.Size() > 0 { + mem.notifyTxsAvailable() + } + } + default: + // ignore other messages + } +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) TxsAvailable() <-chan struct{} { + return mem.txsAvailable +} + +func (mem *CListMempool) notifyTxsAvailable() { + if mem.Size() == 0 { + panic("notified txs available but mempool is empty!") + } + if mem.txsAvailable != nil && !mem.notifiedTxsAvailable { + // channel cap is 1, so this will send once + mem.notifiedTxsAvailable = true + select { + case mem.txsAvailable <- struct{}{}: + default: + } + } +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { + mem.updateMtx.RLock() + defer mem.updateMtx.RUnlock() + + var ( + totalGas int64 + runningSize int64 + ) + + // TODO: we will get a performance boost if we have a good estimate of avg + // size per tx, and set the initial capacity based off of that. + // txs := make([]types.Tx, 0, tmmath.MinInt(mem.txs.Len(), max/mem.avgTxSize)) + txs := make([]types.Tx, 0, mem.txs.Len()) + for e := mem.txs.Front(); e != nil; e = e.Next() { + memTx := e.Value.(*mempoolTx) + + txs = append(txs, memTx.tx) + + dataSize := types.ComputeProtoSizeForTxs([]types.Tx{memTx.tx}) + + // Check total size requirement + if maxBytes > -1 && runningSize+dataSize > maxBytes { + return txs[:len(txs)-1] + } + + runningSize += dataSize + + // Check total gas requirement. + // If maxGas is negative, skip this check. + // Since newTotalGas < masGas, which + // must be non-negative, it follows that this won't overflow. + newTotalGas := totalGas + memTx.gasWanted + if maxGas > -1 && newTotalGas > maxGas { + return txs[:len(txs)-1] + } + totalGas = newTotalGas + } + return txs +} + +// Safe for concurrent use by multiple goroutines. +func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { + mem.updateMtx.RLock() + defer mem.updateMtx.RUnlock() + + if max < 0 { + max = mem.txs.Len() + } + + txs := make([]types.Tx, 0, tmmath.MinInt(mem.txs.Len(), max)) + for e := mem.txs.Front(); e != nil && len(txs) <= max; e = e.Next() { + memTx := e.Value.(*mempoolTx) + txs = append(txs, memTx.tx) + } + return txs +} + +// Lock() must be help by the caller during execution. +func (mem *CListMempool) Update( + height int64, + txs types.Txs, + deliverTxResponses []*abci.ResponseDeliverTx, + preCheck mempool.PreCheckFunc, + postCheck mempool.PostCheckFunc, +) error { + // Set height + mem.height = height + mem.notifiedTxsAvailable = false + + if preCheck != nil { + mem.preCheck = preCheck + } + if postCheck != nil { + mem.postCheck = postCheck + } + + for i, tx := range txs { + if deliverTxResponses[i].Code == abci.CodeTypeOK { + // Add valid committed tx to the cache (if missing). + _ = mem.cache.Push(tx) + } else if !mem.config.KeepInvalidTxsInCache { + // Allow invalid transactions to be resubmitted. + mem.cache.Remove(tx) + } + + // Remove committed tx from the mempool. + // + // Note an evil proposer can drop valid txs! + // Mempool before: + // 100 -> 101 -> 102 + // Block, proposed by an evil proposer: + // 101 -> 102 + // Mempool after: + // 100 + // https://github.com/tendermint/tendermint/issues/3322. + if e, ok := mem.txsMap.Load(tx.Key()); ok { + mem.removeTx(tx, e.(*clist.CElement), false) + } + } + + // Either recheck non-committed txs to see if they became invalid + // or just notify there're some txs left. + if mem.Size() > 0 { + if mem.config.Recheck { + mem.logger.Debug("recheck txs", "numtxs", mem.Size(), "height", height) + mem.recheckTxs() + // At this point, mem.txs are being rechecked. + // mem.recheckCursor re-scans mem.txs and possibly removes some txs. + // Before mem.Reap(), we should wait for mem.recheckCursor to be nil. + } else { + mem.notifyTxsAvailable() + } + } + + // Update metrics + mem.metrics.Size.Set(float64(mem.Size())) + + return nil +} + +func (mem *CListMempool) recheckTxs() { + if mem.Size() == 0 { + panic("recheckTxs is called, but the mempool is empty") + } + + mem.recheckCursor = mem.txs.Front() + mem.recheckEnd = mem.txs.Back() + + // Push txs to proxyAppConn + // NOTE: globalCb may be called concurrently. + for e := mem.txs.Front(); e != nil; e = e.Next() { + memTx := e.Value.(*mempoolTx) + mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{ + Tx: memTx.tx, + Type: abci.CheckTxType_Recheck, + }) + } + + mem.proxyAppConn.FlushAsync() +} + +//-------------------------------------------------------------------------------- + +// mempoolTx is a transaction that successfully ran +type mempoolTx struct { + height int64 // height that this tx had been validated in + gasWanted int64 // amount of gas this tx states it will require + tx types.Tx // + + // ids of peers who've sent us this tx (as a map for quick lookups). + // senders: PeerID -> bool + senders sync.Map +} + +// Height returns the height for this transaction +func (memTx *mempoolTx) Height() int64 { + return atomic.LoadInt64(&memTx.height) +} diff --git a/mempool/v0/clist_mempool_test.go b/mempool/v0/clist_mempool_test.go new file mode 100644 index 000000000..93ce4c0cb --- /dev/null +++ b/mempool/v0/clist_mempool_test.go @@ -0,0 +1,692 @@ +package v0 + +import ( + "context" + "crypto/rand" + "encoding/binary" + "fmt" + mrand "math/rand" + "os" + "testing" + "time" + + "github.com/gogo/protobuf/proto" + gogotypes "github.com/gogo/protobuf/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + abciclient "github.com/tendermint/tendermint/abci/client" + abciclimocks "github.com/tendermint/tendermint/abci/client/mocks" + "github.com/tendermint/tendermint/abci/example/kvstore" + abciserver "github.com/tendermint/tendermint/abci/server" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/log" + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/libs/service" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/types" +) + +// A cleanupFunc cleans up any config / test files created for a particular +// test. +type cleanupFunc func() + +func newMempoolWithApp(cc abciclient.Creator) (*CListMempool, cleanupFunc, error) { + conf, err := config.ResetTestRoot("mempool_test") + if err != nil { + return nil, func() {}, err + } + + mp, cu := newMempoolWithAppAndConfig(cc, conf) + return mp, cu, nil +} + +func newMempoolWithAppAndConfig(cc abciclient.Creator, cfg *config.Config) (*CListMempool, cleanupFunc) { + appConnMem, _ := cc() + appConnMem.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "mempool")) + err := appConnMem.Start() + if err != nil { + panic(err) + } + + mp := NewCListMempool(cfg.Mempool, appConnMem, 0) + mp.SetLogger(log.TestingLogger()) + + return mp, func() { os.RemoveAll(cfg.RootDir) } +} + +func ensureNoFire(t *testing.T, ch <-chan struct{}, timeoutMS int) { + timer := time.NewTimer(time.Duration(timeoutMS) * time.Millisecond) + select { + case <-ch: + t.Fatal("Expected not to fire") + case <-timer.C: + } +} + +func ensureFire(t *testing.T, ch <-chan struct{}, timeoutMS int) { + timer := time.NewTimer(time.Duration(timeoutMS) * time.Millisecond) + select { + case <-ch: + case <-timer.C: + t.Fatal("Expected to fire") + } +} + +func checkTxs(t *testing.T, mp mempool.Mempool, count int, peerID uint16) types.Txs { + txs := make(types.Txs, count) + txInfo := mempool.TxInfo{SenderID: peerID} + for i := 0; i < count; i++ { + txBytes := make([]byte, 20) + txs[i] = txBytes + _, err := rand.Read(txBytes) + if err != nil { + t.Error(err) + } + if err := mp.CheckTx(context.Background(), txBytes, nil, txInfo); err != nil { + // Skip invalid txs. + // TestMempoolFilters will fail otherwise. It asserts a number of txs + // returned. + if types.IsPreCheckError(err) { + continue + } + t.Fatalf("CheckTx failed: %v while checking #%d tx", err, i) + } + } + return txs +} + +func TestReapMaxBytesMaxGas(t *testing.T) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + // Ensure gas calculation behaves as expected + checkTxs(t, mp, 1, mempool.UnknownPeerID) + tx0 := mp.TxsFront().Value.(*mempoolTx) + // assert that kv store has gas wanted = 1. + require.Equal(t, app.CheckTx(abci.RequestCheckTx{Tx: tx0.tx}).GasWanted, int64(1), "KVStore had a gas value neq to 1") + require.Equal(t, tx0.gasWanted, int64(1), "transactions gas was set incorrectly") + // ensure each tx is 20 bytes long + require.Equal(t, len(tx0.tx), 20, "Tx is longer than 20 bytes") + mp.Flush() + + // each table driven test creates numTxsToCreate txs with checkTx, and at the end clears all remaining txs. + // each tx has 20 bytes + tests := []struct { + numTxsToCreate int + maxBytes int64 + maxGas int64 + expectedNumTxs int + }{ + {20, -1, -1, 20}, + {20, -1, 0, 0}, + {20, -1, 10, 10}, + {20, -1, 30, 20}, + {20, 0, -1, 0}, + {20, 0, 10, 0}, + {20, 10, 10, 0}, + {20, 24, 10, 1}, + {20, 240, 5, 5}, + {20, 240, -1, 10}, + {20, 240, 10, 10}, + {20, 240, 15, 10}, + {20, 20000, -1, 20}, + {20, 20000, 5, 5}, + {20, 20000, 30, 20}, + } + for tcIndex, tt := range tests { + checkTxs(t, mp, tt.numTxsToCreate, mempool.UnknownPeerID) + got := mp.ReapMaxBytesMaxGas(tt.maxBytes, tt.maxGas) + assert.Equal(t, tt.expectedNumTxs, len(got), "Got %d txs, expected %d, tc #%d", + len(got), tt.expectedNumTxs, tcIndex) + mp.Flush() + } +} + +func TestMempoolFilters(t *testing.T) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + emptyTxArr := []types.Tx{[]byte{}} + + nopPreFilter := func(tx types.Tx) error { return nil } + nopPostFilter := func(tx types.Tx, res *abci.ResponseCheckTx) error { return nil } + + // each table driven test creates numTxsToCreate txs with checkTx, and at the end clears all remaining txs. + // each tx has 20 bytes + tests := []struct { + numTxsToCreate int + preFilter mempool.PreCheckFunc + postFilter mempool.PostCheckFunc + expectedNumTxs int + }{ + {10, nopPreFilter, nopPostFilter, 10}, + {10, mempool.PreCheckMaxBytes(10), nopPostFilter, 0}, + {10, mempool.PreCheckMaxBytes(22), nopPostFilter, 10}, + {10, nopPreFilter, mempool.PostCheckMaxGas(-1), 10}, + {10, nopPreFilter, mempool.PostCheckMaxGas(0), 0}, + {10, nopPreFilter, mempool.PostCheckMaxGas(1), 10}, + {10, nopPreFilter, mempool.PostCheckMaxGas(3000), 10}, + {10, mempool.PreCheckMaxBytes(10), mempool.PostCheckMaxGas(20), 0}, + {10, mempool.PreCheckMaxBytes(30), mempool.PostCheckMaxGas(20), 10}, + {10, mempool.PreCheckMaxBytes(22), mempool.PostCheckMaxGas(1), 10}, + {10, mempool.PreCheckMaxBytes(22), mempool.PostCheckMaxGas(0), 0}, + } + for tcIndex, tt := range tests { + err := mp.Update(1, emptyTxArr, abciResponses(len(emptyTxArr), abci.CodeTypeOK), tt.preFilter, tt.postFilter) + require.NoError(t, err) + checkTxs(t, mp, tt.numTxsToCreate, mempool.UnknownPeerID) + require.Equal(t, tt.expectedNumTxs, mp.Size(), "mempool had the incorrect size, on test case %d", tcIndex) + mp.Flush() + } +} + +func TestMempoolUpdate(t *testing.T) { + app := kvstore.NewApplication() + cc := proxy.NewLocalClientCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + // 1. Adds valid txs to the cache + { + err := mp.Update(1, []types.Tx{[]byte{0x01}}, abciResponses(1, abci.CodeTypeOK), nil, nil) + require.NoError(t, err) + err = mp.CheckTx(context.Background(), []byte{0x01}, nil, mempool.TxInfo{}) + require.NoError(t, err) + } + + // 2. Removes valid txs from the mempool + { + err := mp.CheckTx(context.Background(), []byte{0x02}, nil, mempool.TxInfo{}) + require.NoError(t, err) + err = mp.Update(1, []types.Tx{[]byte{0x02}}, abciResponses(1, abci.CodeTypeOK), nil, nil) + require.NoError(t, err) + assert.Zero(t, mp.Size()) + } + + // 3. Removes invalid transactions from the cache and the mempool (if present) + { + err := mp.CheckTx(context.Background(), []byte{0x03}, nil, mempool.TxInfo{}) + require.NoError(t, err) + err = mp.Update(1, []types.Tx{[]byte{0x03}}, abciResponses(1, 1), nil, nil) + require.NoError(t, err) + assert.Zero(t, mp.Size()) + + err = mp.CheckTx(context.Background(), []byte{0x03}, nil, mempool.TxInfo{}) + require.NoError(t, err) + } +} + +func TestMempoolUpdateDoesNotPanicWhenApplicationMissedTx(t *testing.T) { + var callback abciclient.Callback + mockClient := new(abciclimocks.Client) + mockClient.On("Start").Return(nil) + mockClient.On("SetLogger", mock.Anything) + + mockClient.On("Error").Return(nil).Times(4) + mockClient.On("FlushAsync", mock.Anything).Return(abciclient.NewReqRes(abci.ToRequestFlush()), nil) + mockClient.On("SetResponseCallback", mock.MatchedBy(func(cb abciclient.Callback) bool { callback = cb; return true })) + + cc := func() (abciclient.Client, error) { + return mockClient, nil + } + + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + // Add 4 transactions to the mempool by calling the mempool's `CheckTx` on each of them. + txs := []types.Tx{[]byte{0x01}, []byte{0x02}, []byte{0x03}, []byte{0x04}} + for _, tx := range txs { + reqRes := abciclient.NewReqRes(abci.ToRequestCheckTx(abci.RequestCheckTx{Tx: tx})) + reqRes.Response = abci.ToResponseCheckTx(abci.ResponseCheckTx{Code: abci.CodeTypeOK}) + + mockClient.On("CheckTxAsync", mock.Anything, mock.Anything).Return(reqRes, nil) + err := mp.CheckTx(context.Background(), tx, nil, mempool.TxInfo{}) + require.NoError(t, err) + + // ensure that the callback that the mempool sets on the ReqRes is run. + reqRes.InvokeCallback() + } + + // Calling update to remove the first transaction from the mempool. + // This call also triggers the mempool to recheck its remaining transactions. + err = mp.Update(0, []types.Tx{txs[0]}, abciResponses(1, abci.CodeTypeOK), nil, nil) + require.Nil(t, err) + + // The mempool has now sent its requests off to the client to be rechecked + // and is waiting for the corresponding callbacks to be called. + // We now call the mempool-supplied callback on the first and third transaction. + // This simulates the client dropping the second request. + // Previous versions of this code panicked when the ABCI application missed + // a recheck-tx request. + resp := abci.ResponseCheckTx{Code: abci.CodeTypeOK} + req := abci.RequestCheckTx{Tx: txs[1]} + callback(abci.ToRequestCheckTx(req), abci.ToResponseCheckTx(resp)) + + req = abci.RequestCheckTx{Tx: txs[3]} + callback(abci.ToRequestCheckTx(req), abci.ToResponseCheckTx(resp)) + mockClient.AssertExpectations(t) +} + +func TestMempool_KeepInvalidTxsInCache(t *testing.T) { + app := kvstore.NewApplication() + cc := abciclient.NewLocalCreator(app) + wcfg := config.DefaultConfig() + wcfg.Mempool.KeepInvalidTxsInCache = true + mp, cleanup := newMempoolWithAppAndConfig(cc, wcfg) + defer cleanup() + + // 1. An invalid transaction must remain in the cache after Update + { + a := make([]byte, 8) + binary.BigEndian.PutUint64(a, 0) + + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, 1) + + err := mp.CheckTx(context.Background(), b, nil, mempool.TxInfo{}) + require.NoError(t, err) + + // simulate new block + _ = app.DeliverTx(abci.RequestDeliverTx{Tx: a}) + _ = app.DeliverTx(abci.RequestDeliverTx{Tx: b}) + err = mp.Update(1, []types.Tx{a, b}, + []*abci.ResponseDeliverTx{{Code: abci.CodeTypeOK}, {Code: 2}}, nil, nil) + require.NoError(t, err) + + // a must be added to the cache + err = mp.CheckTx(context.Background(), a, nil, mempool.TxInfo{}) + require.NoError(t, err) + + // b must remain in the cache + err = mp.CheckTx(context.Background(), b, nil, mempool.TxInfo{}) + require.NoError(t, err) + } + + // 2. An invalid transaction must remain in the cache + { + a := make([]byte, 8) + binary.BigEndian.PutUint64(a, 0) + + // remove a from the cache to test (2) + mp.cache.Remove(a) + + err := mp.CheckTx(context.Background(), a, nil, mempool.TxInfo{}) + require.NoError(t, err) + } +} + +func TestTxsAvailable(t *testing.T) { + app := kvstore.NewApplication() + cc := abciclient.NewLocalCreator(app) + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + mp.EnableTxsAvailable() + + timeoutMS := 500 + + // with no txs, it shouldnt fire + ensureNoFire(t, mp.TxsAvailable(), timeoutMS) + + // send a bunch of txs, it should only fire once + txs := checkTxs(t, mp, 100, mempool.UnknownPeerID) + ensureFire(t, mp.TxsAvailable(), timeoutMS) + ensureNoFire(t, mp.TxsAvailable(), timeoutMS) + + // call update with half the txs. + // it should fire once now for the new height + // since there are still txs left + committedTxs, txs := txs[:50], txs[50:] + if err := mp.Update(1, committedTxs, abciResponses(len(committedTxs), abci.CodeTypeOK), nil, nil); err != nil { + t.Error(err) + } + ensureFire(t, mp.TxsAvailable(), timeoutMS) + ensureNoFire(t, mp.TxsAvailable(), timeoutMS) + + // send a bunch more txs. we already fired for this height so it shouldnt fire again + moreTxs := checkTxs(t, mp, 50, mempool.UnknownPeerID) + ensureNoFire(t, mp.TxsAvailable(), timeoutMS) + + // now call update with all the txs. it should not fire as there are no txs left + committedTxs = append(txs, moreTxs...) + if err := mp.Update(2, committedTxs, abciResponses(len(committedTxs), abci.CodeTypeOK), nil, nil); err != nil { + t.Error(err) + } + ensureNoFire(t, mp.TxsAvailable(), timeoutMS) + + // send a bunch more txs, it should only fire once + checkTxs(t, mp, 100, mempool.UnknownPeerID) + ensureFire(t, mp.TxsAvailable(), timeoutMS) + ensureNoFire(t, mp.TxsAvailable(), timeoutMS) +} + +func TestSerialReap(t *testing.T) { + app := kvstore.NewApplication() + cc := abciclient.NewLocalCreator(app) + + mp, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + appConnCon, _ := cc() + appConnCon.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "consensus")) + err = appConnCon.Start() + require.Nil(t, err) + + cacheMap := make(map[string]struct{}) + deliverTxsRange := func(start, end int) { + // Deliver some txs. + for i := start; i < end; i++ { + + // This will succeed + txBytes := make([]byte, 8) + binary.BigEndian.PutUint64(txBytes, uint64(i)) + err := mp.CheckTx(context.Background(), txBytes, nil, mempool.TxInfo{}) + _, cached := cacheMap[string(txBytes)] + if cached { + require.NotNil(t, err, "expected error for cached tx") + } else { + require.Nil(t, err, "expected no err for uncached tx") + } + cacheMap[string(txBytes)] = struct{}{} + + // Duplicates are cached and should return error + err = mp.CheckTx(context.Background(), txBytes, nil, mempool.TxInfo{}) + require.NotNil(t, err, "Expected error after CheckTx on duplicated tx") + } + } + + reapCheck := func(exp int) { + txs := mp.ReapMaxBytesMaxGas(-1, -1) + require.Equal(t, len(txs), exp, fmt.Sprintf("Expected to reap %v txs but got %v", exp, len(txs))) + } + + updateRange := func(start, end int) { + txs := make([]types.Tx, 0) + for i := start; i < end; i++ { + txBytes := make([]byte, 8) + binary.BigEndian.PutUint64(txBytes, uint64(i)) + txs = append(txs, txBytes) + } + if err := mp.Update(0, txs, abciResponses(len(txs), abci.CodeTypeOK), nil, nil); err != nil { + t.Error(err) + } + } + + commitRange := func(start, end int) { + ctx := context.Background() + // Deliver some txs. + for i := start; i < end; i++ { + txBytes := make([]byte, 8) + binary.BigEndian.PutUint64(txBytes, uint64(i)) + res, err := appConnCon.DeliverTxSync(ctx, abci.RequestDeliverTx{Tx: txBytes}) + if err != nil { + t.Errorf("client error committing tx: %v", err) + } + if res.IsErr() { + t.Errorf("error committing tx. Code:%v result:%X log:%v", + res.Code, res.Data, res.Log) + } + } + res, err := appConnCon.CommitSync(ctx) + if err != nil { + t.Errorf("client error committing: %v", err) + } + if len(res.Data) != 8 { + t.Errorf("error committing. Hash:%X", res.Data) + } + } + + //---------------------------------------- + + // Deliver some txs. + deliverTxsRange(0, 100) + + // Reap the txs. + reapCheck(100) + + // Reap again. We should get the same amount + reapCheck(100) + + // Deliver 0 to 999, we should reap 900 new txs + // because 100 were already counted. + deliverTxsRange(0, 1000) + + // Reap the txs. + reapCheck(1000) + + // Reap again. We should get the same amount + reapCheck(1000) + + // Commit from the conensus AppConn + commitRange(0, 500) + updateRange(0, 500) + + // We should have 500 left. + reapCheck(500) + + // Deliver 100 invalid txs and 100 valid txs + deliverTxsRange(900, 1100) + + // We should have 600 now. + reapCheck(600) +} + +func TestMempool_CheckTxChecksTxSize(t *testing.T) { + app := kvstore.NewApplication() + cc := abciclient.NewLocalCreator(app) + mempl, cleanup, err := newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + maxTxSize := mempl.config.MaxTxBytes + + testCases := []struct { + len int + err bool + }{ + // check small txs. no error + 0: {10, false}, + 1: {1000, false}, + 2: {1000000, false}, + + // check around maxTxSize + 3: {maxTxSize - 1, false}, + 4: {maxTxSize, false}, + 5: {maxTxSize + 1, true}, + } + + for i, testCase := range testCases { + caseString := fmt.Sprintf("case %d, len %d", i, testCase.len) + + tx := tmrand.Bytes(testCase.len) + + err := mempl.CheckTx(context.Background(), tx, nil, mempool.TxInfo{}) + bv := gogotypes.BytesValue{Value: tx} + bz, err2 := bv.Marshal() + require.NoError(t, err2) + require.Equal(t, len(bz), proto.Size(&bv), caseString) + + if !testCase.err { + require.NoError(t, err, caseString) + } else { + require.Equal(t, err, types.ErrTxTooLarge{ + Max: maxTxSize, + Actual: testCase.len, + }, caseString) + } + } +} + +func TestMempoolTxsBytes(t *testing.T) { + app := kvstore.NewApplication() + cc := abciclient.NewLocalCreator(app) + cfg, err := config.ResetTestRoot("mempool_test") + require.NoError(t, err) + + cfg.Mempool.MaxTxsBytes = 10 + mp, cleanup := newMempoolWithAppAndConfig(cc, cfg) + defer cleanup() + + // 1. zero by default + assert.EqualValues(t, 0, mp.SizeBytes()) + + // 2. len(tx) after CheckTx + err = mp.CheckTx(context.Background(), []byte{0x01}, nil, mempool.TxInfo{}) + require.NoError(t, err) + assert.EqualValues(t, 1, mp.SizeBytes()) + + // 3. zero again after tx is removed by Update + err = mp.Update(1, []types.Tx{[]byte{0x01}}, abciResponses(1, abci.CodeTypeOK), nil, nil) + require.NoError(t, err) + assert.EqualValues(t, 0, mp.SizeBytes()) + + // 4. zero after Flush + err = mp.CheckTx(context.Background(), []byte{0x02, 0x03}, nil, mempool.TxInfo{}) + require.NoError(t, err) + assert.EqualValues(t, 2, mp.SizeBytes()) + + mp.Flush() + assert.EqualValues(t, 0, mp.SizeBytes()) + + // 5. ErrMempoolIsFull is returned when/if MaxTxsBytes limit is reached. + err = mp.CheckTx( + context.Background(), + []byte{0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}, + nil, + mempool.TxInfo{}, + ) + require.NoError(t, err) + + err = mp.CheckTx(context.Background(), []byte{0x05}, nil, mempool.TxInfo{}) + if assert.Error(t, err) { + assert.IsType(t, types.ErrMempoolIsFull{}, err) + } + + // 6. zero after tx is rechecked and removed due to not being valid anymore + app2 := kvstore.NewApplication() + cc = abciclient.NewLocalCreator(app2) + mp, cleanup, err = newMempoolWithApp(cc) + require.NoError(t, err) + defer cleanup() + + txBytes := make([]byte, 8) + binary.BigEndian.PutUint64(txBytes, uint64(0)) + + err = mp.CheckTx(context.Background(), txBytes, nil, mempool.TxInfo{}) + require.NoError(t, err) + assert.EqualValues(t, 8, mp.SizeBytes()) + + appConnCon, _ := cc() + appConnCon.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "consensus")) + err = appConnCon.Start() + require.Nil(t, err) + t.Cleanup(func() { + if err := appConnCon.Stop(); err != nil { + t.Error(err) + } + }) + ctx := context.Background() + res, err := appConnCon.DeliverTxSync(ctx, abci.RequestDeliverTx{Tx: txBytes}) + require.NoError(t, err) + require.EqualValues(t, 0, res.Code) + res2, err := appConnCon.CommitSync(ctx) + require.NoError(t, err) + require.NotEmpty(t, res2.Data) + + // Pretend like we committed nothing so txBytes gets rechecked and removed. + err = mp.Update(1, []types.Tx{}, abciResponses(0, abci.CodeTypeOK), nil, nil) + require.NoError(t, err) + assert.EqualValues(t, 8, mp.SizeBytes()) + + // 7. Test RemoveTxByKey function + err = mp.CheckTx(context.Background(), []byte{0x06}, nil, mempool.TxInfo{}) + require.NoError(t, err) + assert.EqualValues(t, 9, mp.SizeBytes()) + assert.Error(t, mp.RemoveTxByKey(types.Tx([]byte{0x07}).Key())) + assert.EqualValues(t, 9, mp.SizeBytes()) + assert.NoError(t, mp.RemoveTxByKey(types.Tx([]byte{0x06}).Key())) + assert.EqualValues(t, 8, mp.SizeBytes()) + +} + +// This will non-deterministically catch some concurrency failures like +// https://github.com/tendermint/tendermint/issues/3509 +// TODO: all of the tests should probably also run using the remote proxy app +// since otherwise we're not actually testing the concurrency of the mempool here! +func TestMempoolRemoteAppConcurrency(t *testing.T) { + sockPath := fmt.Sprintf("unix:///tmp/echo_%v.sock", tmrand.Str(6)) + app := kvstore.NewApplication() + cc, server := newRemoteApp(t, sockPath, app) + t.Cleanup(func() { + if err := server.Stop(); err != nil { + t.Error(err) + } + }) + cfg, err := config.ResetTestRoot("mempool_test") + require.NoError(t, err) + + mp, cleanup := newMempoolWithAppAndConfig(cc, cfg) + defer cleanup() + + // generate small number of txs + nTxs := 10 + txLen := 200 + txs := make([]types.Tx, nTxs) + for i := 0; i < nTxs; i++ { + txs[i] = tmrand.Bytes(txLen) + } + + // simulate a group of peers sending them over and over + N := cfg.Mempool.Size + maxPeers := 5 + for i := 0; i < N; i++ { + peerID := mrand.Intn(maxPeers) + txNum := mrand.Intn(nTxs) + tx := txs[txNum] + + // this will err with ErrTxInCache many times ... + mp.CheckTx(tx, nil, mempool.TxInfo{SenderID: uint16(peerID)}) //nolint: errcheck // will error + } + err = mp.FlushAppConn() + require.NoError(t, err) +} + +// caller must close server +func newRemoteApp( + t *testing.T, + addr string, + app abci.Application, +) ( + clientCreator abciclient.Creator, + server service.Service, +) { + clientCreator = abciclient.NewRemoteCreator(addr, "socket", true) + + // Start server + server = abciserver.NewSocketServer(addr, app) + server.SetLogger(log.TestingLogger().With("module", "abci-server")) + if err := server.Start(); err != nil { + t.Fatalf("Error starting socket server: %v", err.Error()) + } + return clientCreator, server +} + +func abciResponses(n int, code uint32) []*abci.ResponseDeliverTx { + responses := make([]*abci.ResponseDeliverTx, 0, n) + for i := 0; i < n; i++ { + responses = append(responses, &abci.ResponseDeliverTx{Code: code}) + } + return responses +} diff --git a/mempool/v0/doc.go b/mempool/v0/doc.go new file mode 100644 index 000000000..3b5d0d20d --- /dev/null +++ b/mempool/v0/doc.go @@ -0,0 +1,23 @@ +// The mempool pushes new txs onto the proxyAppConn. +// It gets a stream of (req, res) tuples from the proxy. +// The mempool stores good txs in a concurrent linked-list. + +// Multiple concurrent go-routines can traverse this linked-list +// safely by calling .NextWait() on each element. + +// So we have several go-routines: +// 1. Consensus calling Update() and ReapMaxBytesMaxGas() synchronously +// 2. Many mempool reactor's peer routines calling CheckTx() +// 3. Many mempool reactor's peer routines traversing the txs linked list + +// To manage these goroutines, there are three methods of locking. +// 1. Mutations to the linked-list is protected by an internal mtx (CList is goroutine-safe) +// 2. Mutations to the linked-list elements are atomic +// 3. CheckTx() and/or ReapMaxBytesMaxGas() calls can be paused upon Update(), protected by .updateMtx + +// Garbage collection of old elements from mempool.txs is handlde via the +// DetachPrev() call, which makes old elements not reachable by peer +// broadcastTxRoutine(). + +// TODO: Better handle abci client errors. (make it automatically handle connection errors) +package v0 diff --git a/mempool/v0/reactor.go b/mempool/v0/reactor.go new file mode 100644 index 000000000..3fc850641 --- /dev/null +++ b/mempool/v0/reactor.go @@ -0,0 +1,304 @@ +package v0 + +import ( + "errors" + "fmt" + "time" + + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/clist" + "github.com/tendermint/tendermint/libs/log" + tmsync "github.com/tendermint/tendermint/libs/sync" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/p2p" + protomem "github.com/tendermint/tendermint/proto/tendermint/mempool" + "github.com/tendermint/tendermint/types" +) + +// Reactor handles mempool tx broadcasting amongst peers. +// It maintains a map from peer ID to counter, to prevent gossiping txs to the +// peers you received it from. +type Reactor struct { + p2p.BaseReactor + config *cfg.MempoolConfig + mempool *CListMempool + ids *mempoolIDs +} + +type mempoolIDs struct { + mtx tmsync.RWMutex + peerMap map[p2p.ID]uint16 + nextID uint16 // assumes that a node will never have over 65536 active peers + activeIDs map[uint16]struct{} // used to check if a given peerID key is used, the value doesn't matter +} + +// Reserve searches for the next unused ID and assigns it to the +// peer. +func (ids *mempoolIDs) ReserveForPeer(peer p2p.Peer) { + ids.mtx.Lock() + defer ids.mtx.Unlock() + + curID := ids.nextPeerID() + ids.peerMap[peer.ID()] = curID + ids.activeIDs[curID] = struct{}{} +} + +// nextPeerID returns the next unused peer ID to use. +// This assumes that ids's mutex is already locked. +func (ids *mempoolIDs) nextPeerID() uint16 { + if len(ids.activeIDs) == mempool.MaxActiveIDs { + panic(fmt.Sprintf("node has maximum %d active IDs and wanted to get one more", mempool.MaxActiveIDs)) + } + + _, idExists := ids.activeIDs[ids.nextID] + for idExists { + ids.nextID++ + _, idExists = ids.activeIDs[ids.nextID] + } + curID := ids.nextID + ids.nextID++ + return curID +} + +// Reclaim returns the ID reserved for the peer back to unused pool. +func (ids *mempoolIDs) Reclaim(peer p2p.Peer) { + ids.mtx.Lock() + defer ids.mtx.Unlock() + + removedID, ok := ids.peerMap[peer.ID()] + if ok { + delete(ids.activeIDs, removedID) + delete(ids.peerMap, peer.ID()) + } +} + +// GetForPeer returns an ID reserved for the peer. +func (ids *mempoolIDs) GetForPeer(peer p2p.Peer) uint16 { + ids.mtx.RLock() + defer ids.mtx.RUnlock() + + return ids.peerMap[peer.ID()] +} + +func newMempoolIDs() *mempoolIDs { + return &mempoolIDs{ + peerMap: make(map[p2p.ID]uint16), + activeIDs: map[uint16]struct{}{0: {}}, + nextID: 1, // reserve unknownPeerID(0) for mempoolReactor.BroadcastTx + } +} + +// NewReactor returns a new Reactor with the given config and mempool. +func NewReactor(config *cfg.MempoolConfig, mempool *CListMempool) *Reactor { + memR := &Reactor{ + config: config, + mempool: mempool, + ids: newMempoolIDs(), + } + memR.BaseReactor = *p2p.NewBaseReactor("Mempool", memR) + return memR +} + +// InitPeer implements Reactor by creating a state for the peer. +func (memR *Reactor) InitPeer(peer p2p.Peer) p2p.Peer { + memR.ids.ReserveForPeer(peer) + return peer +} + +// SetLogger sets the Logger on the reactor and the underlying mempool. +func (memR *Reactor) SetLogger(l log.Logger) { + memR.Logger = l + memR.mempool.SetLogger(l) +} + +// OnStart implements p2p.BaseReactor. +func (memR *Reactor) OnStart() error { + if !memR.config.Broadcast { + memR.Logger.Info("Tx broadcasting is disabled") + } + return nil +} + +// GetChannels implements Reactor by returning the list of channels for this +// reactor. +func (memR *Reactor) GetChannels() []*p2p.ChannelDescriptor { + largestTx := make([]byte, memR.config.MaxTxBytes) + batchMsg := protomem.Message{ + Sum: &protomem.Message_Txs{ + Txs: &protomem.Txs{Txs: [][]byte{largestTx}}, + }, + } + + return []*p2p.ChannelDescriptor{ + { + ID: mempool.MempoolChannel, + Priority: 5, + RecvMessageCapacity: batchMsg.Size(), + }, + } +} + +// AddPeer implements Reactor. +// It starts a broadcast routine ensuring all txs are forwarded to the given peer. +func (memR *Reactor) AddPeer(peer p2p.Peer) { + if memR.config.Broadcast { + go memR.broadcastTxRoutine(peer) + } +} + +// RemovePeer implements Reactor. +func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { + memR.ids.Reclaim(peer) + // broadcast routine checks if peer is gone and returns +} + +// Receive implements Reactor. +// It adds any received transactions to the mempool. +func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := memR.decodeMsg(msgBytes) + if err != nil { + memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "err", err) + memR.Switch.StopPeerForError(src, err) + return + } + memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) + + txInfo := mempool.TxInfo{SenderID: memR.ids.GetForPeer(src)} + if src != nil { + txInfo.SenderP2PID = src.ID() + } + + for _, tx := range msg.Txs { + err = memR.mempool.CheckTx(tx, nil, txInfo) + if errors.Is(err, mempool.ErrTxInCache) { + memR.Logger.Debug("Tx already exists in cache", "tx", tx.String()) + } else if err != nil { + memR.Logger.Info("Could not check tx", "tx", tx.String(), "err", err) + } + } + + // broadcasting happens from go routines per peer +} + +// PeerState describes the state of a peer. +type PeerState interface { + GetHeight() int64 +} + +// Send new mempool txs to peer. +func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) { + peerID := memR.ids.GetForPeer(peer) + var next *clist.CElement + + for { + // In case of both next.NextWaitChan() and peer.Quit() are variable at the same time + if !memR.IsRunning() || !peer.IsRunning() { + return + } + // This happens because the CElement we were looking at got garbage + // collected (removed). That is, .NextWait() returned nil. Go ahead and + // start from the beginning. + if next == nil { + select { + case <-memR.mempool.TxsWaitChan(): // Wait until a tx is available + if next = memR.mempool.TxsFront(); next == nil { + continue + } + case <-peer.Quit(): + return + case <-memR.Quit(): + return + } + } + + // Make sure the peer is up to date. + peerState, ok := peer.Get(types.PeerStateKey).(PeerState) + if !ok { + // Peer does not have a state yet. We set it in the consensus reactor, but + // when we add peer in Switch, the order we call reactors#AddPeer is + // different every time due to us using a map. Sometimes other reactors + // will be initialized before the consensus reactor. We should wait a few + // milliseconds and retry. + time.Sleep(mempool.PeerCatchupSleepIntervalMS * time.Millisecond) + continue + } + + // Allow for a lag of 1 block. + memTx := next.Value.(*mempoolTx) + if peerState.GetHeight() < memTx.Height()-1 { + time.Sleep(mempool.PeerCatchupSleepIntervalMS * time.Millisecond) + continue + } + + // NOTE: Transaction batching was disabled due to + // https://github.com/tendermint/tendermint/issues/5796 + + if _, ok := memTx.senders.Load(peerID); !ok { + msg := protomem.Message{ + Sum: &protomem.Message_Txs{ + Txs: &protomem.Txs{Txs: [][]byte{memTx.tx}}, + }, + } + + bz, err := msg.Marshal() + if err != nil { + panic(err) + } + + success := peer.Send(mempool.MempoolChannel, bz) + if !success { + time.Sleep(mempool.PeerCatchupSleepIntervalMS * time.Millisecond) + continue + } + } + + select { + case <-next.NextWaitChan(): + // see the start of the for loop for nil check + next = next.Next() + case <-peer.Quit(): + return + case <-memR.Quit(): + return + } + } +} + +func (memR *Reactor) decodeMsg(bz []byte) (TxsMessage, error) { + msg := protomem.Message{} + err := msg.Unmarshal(bz) + if err != nil { + return TxsMessage{}, err + } + + var message TxsMessage + + if i, ok := msg.Sum.(*protomem.Message_Txs); ok { + txs := i.Txs.GetTxs() + + if len(txs) == 0 { + return message, errors.New("empty TxsMessage") + } + + decoded := make([]types.Tx, len(txs)) + for j, tx := range txs { + decoded[j] = types.Tx(tx) + } + + message = TxsMessage{ + Txs: decoded, + } + return message, nil + } + return message, fmt.Errorf("msg type: %T is not supported", msg) +} + +// TxsMessage is a Message containing transactions. +type TxsMessage struct { + Txs []types.Tx +} + +// String returns a string representation of the TxsMessage. +func (m *TxsMessage) String() string { + return fmt.Sprintf("[TxsMessage %v]", m.Txs) +} diff --git a/mempool/v0/reactor_test.go b/mempool/v0/reactor_test.go new file mode 100644 index 000000000..0c7aab1b9 --- /dev/null +++ b/mempool/v0/reactor_test.go @@ -0,0 +1,391 @@ +package v0 + +// import ( +// "context" +// "sync" +// "testing" +// "time" + +// "github.com/stretchr/testify/require" + +// abciclient "github.com/tendermint/tendermint/abci/client" +// "github.com/tendermint/tendermint/abci/example/kvstore" +// abci "github.com/tendermint/tendermint/abci/types" +// "github.com/tendermint/tendermint/config" +// "github.com/tendermint/tendermint/internal/mempool" +// "github.com/tendermint/tendermint/internal/p2p" +// "github.com/tendermint/tendermint/internal/p2p/p2ptest" +// "github.com/tendermint/tendermint/libs/log" +// tmrand "github.com/tendermint/tendermint/libs/rand" +// protomem "github.com/tendermint/tendermint/proto/tendermint/mempool" +// "github.com/tendermint/tendermint/types" +// ) + +// type reactorTestSuite struct { +// network *p2ptest.Network +// logger log.Logger + +// reactors map[types.NodeID]*Reactor +// mempoolChnnels map[types.NodeID]*p2p.Channel +// mempools map[types.NodeID]*CListMempool +// kvstores map[types.NodeID]*kvstore.Application + +// peerChans map[types.NodeID]chan p2p.PeerUpdate +// peerUpdates map[types.NodeID]*p2p.PeerUpdates + +// nodes []types.NodeID +// } + +// func setup(t *testing.T, config *config.MempoolConfig, numNodes int, chBuf uint) *reactorTestSuite { +// t.Helper() + +// rts := &reactorTestSuite{ +// logger: log.TestingLogger().With("testCase", t.Name()), +// network: p2ptest.MakeNetwork(t, p2ptest.NetworkOptions{NumNodes: numNodes}), +// reactors: make(map[types.NodeID]*Reactor, numNodes), +// mempoolChnnels: make(map[types.NodeID]*p2p.Channel, numNodes), +// mempools: make(map[types.NodeID]*CListMempool, numNodes), +// kvstores: make(map[types.NodeID]*kvstore.Application, numNodes), +// peerChans: make(map[types.NodeID]chan p2p.PeerUpdate, numNodes), +// peerUpdates: make(map[types.NodeID]*p2p.PeerUpdates, numNodes), +// } + +// chDesc := p2p.ChannelDescriptor{ID: byte(mempool.MempoolChannel)} +// rts.mempoolChnnels = rts.network.MakeChannelsNoCleanup(t, chDesc, new(protomem.Message), int(chBuf)) + +// for nodeID := range rts.network.Nodes { +// rts.kvstores[nodeID] = kvstore.NewApplication() +// cc := abciclient.NewLocalCreator(rts.kvstores[nodeID]) + +// mempool, memCleanup, err := newMempoolWithApp(cc) +// require.NoError(t, err) +// t.Cleanup(memCleanup) +// mempool.SetLogger(rts.logger) +// rts.mempools[nodeID] = mempool + +// rts.peerChans[nodeID] = make(chan p2p.PeerUpdate) +// rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID], 1) +// rts.network.Nodes[nodeID].PeerManager.Register(rts.peerUpdates[nodeID]) + +// rts.reactors[nodeID] = NewReactor( +// rts.logger.With("nodeID", nodeID), +// config, +// mempool, +// rts.mempoolChnnels[nodeID], +// rts.peerUpdates[nodeID], +// ) + +// rts.nodes = append(rts.nodes, nodeID) + +// require.NoError(t, rts.reactors[nodeID].Start()) +// require.True(t, rts.reactors[nodeID].IsRunning()) +// } + +// require.Len(t, rts.reactors, numNodes) + +// t.Cleanup(func() { +// for nodeID := range rts.reactors { +// if rts.reactors[nodeID].IsRunning() { +// require.NoError(t, rts.reactors[nodeID].Stop()) +// require.False(t, rts.reactors[nodeID].IsRunning()) +// } +// } +// }) + +// return rts +// } + +// func (rts *reactorTestSuite) start(t *testing.T) { +// t.Helper() +// rts.network.Start(t) +// require.Len(t, +// rts.network.RandomNode().PeerManager.Peers(), +// len(rts.nodes)-1, +// "network does not have expected number of nodes") +// } + +// func (rts *reactorTestSuite) assertMempoolChannelsDrained(t *testing.T) { +// t.Helper() + +// for id, r := range rts.reactors { +// require.NoError(t, r.Stop(), "stopping reactor %s", id) +// r.Wait() +// require.False(t, r.IsRunning(), "reactor %s did not stop", id) +// } + +// for _, mch := range rts.mempoolChnnels { +// require.Empty(t, mch.Out, "checking channel %q (len=%d)", mch.ID, len(mch.Out)) +// } +// } + +// func (rts *reactorTestSuite) waitForTxns(t *testing.T, txs types.Txs, ids ...types.NodeID) { +// t.Helper() + +// fn := func(pool *CListMempool) { +// for pool.Size() < len(txs) { +// time.Sleep(50 * time.Millisecond) +// } + +// reapedTxs := pool.ReapMaxTxs(len(txs)) +// require.Equal(t, len(txs), len(reapedTxs)) +// for i, tx := range txs { +// require.Equalf(t, +// tx, +// reapedTxs[i], +// "txs at index %d in reactor mempool mismatch; got: %v, expected: %v", i, tx, reapedTxs[i], +// ) +// } +// } + +// if len(ids) == 1 { +// fn(rts.reactors[ids[0]].mempool) +// return +// } + +// wg := &sync.WaitGroup{} +// for id := range rts.mempools { +// if len(ids) > 0 && !p2ptest.NodeInSlice(id, ids) { +// continue +// } + +// wg.Add(1) +// func(nid types.NodeID) { defer wg.Done(); fn(rts.reactors[nid].mempool) }(id) +// } + +// wg.Wait() +// } + +// func TestReactorBroadcastTxs(t *testing.T) { +// numTxs := 1000 +// numNodes := 10 +// cfg := config.TestConfig() + +// rts := setup(t, cfg.Mempool, numNodes, 0) + +// primary := rts.nodes[0] +// secondaries := rts.nodes[1:] + +// txs := checkTxs(t, rts.reactors[primary].mempool, numTxs, mempool.UnknownPeerID) + +// // run the router +// rts.start(t) + +// // Wait till all secondary suites (reactor) received all mempool txs from the +// // primary suite (node). +// rts.waitForTxns(t, txs, secondaries...) + +// for _, pool := range rts.mempools { +// require.Equal(t, len(txs), pool.Size()) +// } + +// rts.assertMempoolChannelsDrained(t) +// } + +// // regression test for https://github.com/tendermint/tendermint/issues/5408 +// func TestReactorConcurrency(t *testing.T) { +// numTxs := 5 +// numNodes := 2 +// cfg := config.TestConfig() + +// rts := setup(t, cfg.Mempool, numNodes, 0) + +// primary := rts.nodes[0] +// secondary := rts.nodes[1] + +// rts.start(t) + +// var wg sync.WaitGroup + +// for i := 0; i < 1000; i++ { +// wg.Add(2) + +// // 1. submit a bunch of txs +// // 2. update the whole mempool + +// txs := checkTxs(t, rts.reactors[primary].mempool, numTxs, mempool.UnknownPeerID) +// go func() { +// defer wg.Done() + +// mempool := rts.mempools[primary] + +// mempool.Lock() +// defer mempool.Unlock() + +// deliverTxResponses := make([]*abci.ResponseDeliverTx, len(txs)) +// for i := range txs { +// deliverTxResponses[i] = &abci.ResponseDeliverTx{Code: 0} +// } + +// require.NoError(t, mempool.Update(1, txs, deliverTxResponses, nil, nil)) +// }() + +// // 1. submit a bunch of txs +// // 2. update none +// _ = checkTxs(t, rts.reactors[secondary].mempool, numTxs, mempool.UnknownPeerID) +// go func() { +// defer wg.Done() + +// mempool := rts.mempools[secondary] + +// mempool.Lock() +// defer mempool.Unlock() + +// err := mempool.Update(1, []types.Tx{}, make([]*abci.ResponseDeliverTx, 0), nil, nil) +// require.NoError(t, err) +// }() + +// // flush the mempool +// rts.mempools[secondary].Flush() +// } + +// wg.Wait() +// } + +// func TestReactorNoBroadcastToSender(t *testing.T) { +// numTxs := 1000 +// numNodes := 2 +// cfg := config.TestConfig() + +// rts := setup(t, cfg.Mempool, numNodes, uint(numTxs)) + +// primary := rts.nodes[0] +// secondary := rts.nodes[1] + +// peerID := uint16(1) +// _ = checkTxs(t, rts.mempools[primary], numTxs, peerID) + +// rts.start(t) + +// time.Sleep(100 * time.Millisecond) + +// require.Eventually(t, func() bool { +// return rts.mempools[secondary].Size() == 0 +// }, time.Minute, 100*time.Millisecond) + +// rts.assertMempoolChannelsDrained(t) +// } + +// func TestReactor_MaxTxBytes(t *testing.T) { +// numNodes := 2 +// cfg := config.TestConfig() + +// rts := setup(t, cfg.Mempool, numNodes, 0) + +// primary := rts.nodes[0] +// secondary := rts.nodes[1] + +// // Broadcast a tx, which has the max size and ensure it's received by the +// // second reactor. +// tx1 := tmrand.Bytes(cfg.Mempool.MaxTxBytes) +// err := rts.reactors[primary].mempool.CheckTx( +// context.Background(), +// tx1, +// nil, +// mempool.TxInfo{ +// SenderID: mempool.UnknownPeerID, +// }, +// ) +// require.NoError(t, err) + +// rts.start(t) + +// // Wait till all secondary suites (reactor) received all mempool txs from the +// // primary suite (node). +// rts.waitForTxns(t, []types.Tx{tx1}, secondary) + +// rts.reactors[primary].mempool.Flush() +// rts.reactors[secondary].mempool.Flush() + +// // broadcast a tx, which is beyond the max size and ensure it's not sent +// tx2 := tmrand.Bytes(cfg.Mempool.MaxTxBytes + 1) +// err = rts.mempools[primary].CheckTx(context.Background(), tx2, nil, mempool.TxInfo{SenderID: mempool.UnknownPeerID}) +// require.Error(t, err) + +// rts.assertMempoolChannelsDrained(t) +// } + +// func TestDontExhaustMaxActiveIDs(t *testing.T) { +// cfg := config.TestConfig() + +// // we're creating a single node network, but not starting the +// // network. +// rts := setup(t, cfg.Mempool, 1, mempool.MaxActiveIDs+1) + +// nodeID := rts.nodes[0] + +// peerID, err := types.NewNodeID("0011223344556677889900112233445566778899") +// require.NoError(t, err) + +// // ensure the reactor does not panic (i.e. exhaust active IDs) +// for i := 0; i < mempool.MaxActiveIDs+1; i++ { +// rts.peerChans[nodeID] <- p2p.PeerUpdate{ +// Status: p2p.PeerStatusUp, +// NodeID: peerID, +// } + +// rts.mempoolChnnels[nodeID].Out <- p2p.Envelope{ +// To: peerID, +// Message: &protomem.Txs{ +// Txs: [][]byte{}, +// }, +// } +// } + +// require.Eventually( +// t, +// func() bool { +// for _, mch := range rts.mempoolChnnels { +// if len(mch.Out) > 0 { +// return false +// } +// } + +// return true +// }, +// time.Minute, +// 10*time.Millisecond, +// ) + +// rts.assertMempoolChannelsDrained(t) +// } + +// func TestMempoolIDsPanicsIfNodeRequestsOvermaxActiveIDs(t *testing.T) { +// if testing.Short() { +// t.Skip("skipping test in short mode") +// } + +// // 0 is already reserved for UnknownPeerID +// ids := mempool.NewMempoolIDs() + +// peerID, err := types.NewNodeID("0011223344556677889900112233445566778899") +// require.NoError(t, err) + +// for i := 0; i < mempool.MaxActiveIDs-1; i++ { +// ids.ReserveForPeer(peerID) +// } + +// require.Panics(t, func() { +// ids.ReserveForPeer(peerID) +// }) +// } + +// func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { +// if testing.Short() { +// t.Skip("skipping test in short mode") +// } + +// cfg := config.TestConfig() + +// rts := setup(t, cfg.Mempool, 2, 0) + +// primary := rts.nodes[0] +// secondary := rts.nodes[1] + +// rts.start(t) + +// // disconnect peer +// rts.peerChans[primary] <- p2p.PeerUpdate{ +// Status: p2p.PeerStatusDown, +// NodeID: secondary, +// } +// } diff --git a/mempool/v1/mempool.go b/mempool/v1/mempool.go new file mode 100644 index 000000000..82107cb22 --- /dev/null +++ b/mempool/v1/mempool.go @@ -0,0 +1,880 @@ +package v1 + +import ( + "bytes" + "context" + "errors" + "fmt" + "reflect" + "sync/atomic" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/internal/libs/clist" + tmsync "github.com/tendermint/tendermint/internal/libs/sync" + "github.com/tendermint/tendermint/internal/mempool" + "github.com/tendermint/tendermint/internal/proxy" + "github.com/tendermint/tendermint/libs/log" + tmmath "github.com/tendermint/tendermint/libs/math" + "github.com/tendermint/tendermint/types" +) + +var _ mempool.Mempool = (*TxMempool)(nil) + +// TxMempoolOption sets an optional parameter on the TxMempool. +type TxMempoolOption func(*TxMempool) + +// TxMempool defines a prioritized mempool data structure used by the v1 mempool +// reactor. It keeps a thread-safe priority queue of transactions that is used +// when a block proposer constructs a block and a thread-safe linked-list that +// is used to gossip transactions to peers in a FIFO manner. +type TxMempool struct { + logger log.Logger + metrics *mempool.Metrics + config *config.MempoolConfig + proxyAppConn proxy.AppConnMempool + + // txsAvailable fires once for each height when the mempool is not empty + txsAvailable chan struct{} + notifiedTxsAvailable bool + + // height defines the last block height process during Update() + height int64 + + // sizeBytes defines the total size of the mempool (sum of all tx bytes) + sizeBytes int64 + + // cache defines a fixed-size cache of already seen transactions as this + // reduces pressure on the proxyApp. + cache mempool.TxCache + + // txStore defines the main storage of valid transactions. Indexes are built + // on top of this store. + txStore *TxStore + + // gossipIndex defines the gossiping index of valid transactions via a + // thread-safe linked-list. We also use the gossip index as a cursor for + // rechecking transactions already in the mempool. + gossipIndex *clist.CList + + // recheckCursor and recheckEnd are used as cursors based on the gossip index + // to recheck transactions that are already in the mempool. Iteration is not + // thread-safe and transaction may be mutated in serial order. + // + // XXX/TODO: It might be somewhat of a codesmell to use the gossip index for + // iterator and cursor management when rechecking transactions. If the gossip + // index changes or is removed in a future refactor, this will have to be + // refactored. Instead, we should consider just keeping a slice of a snapshot + // of the mempool's current transactions during Update and an integer cursor + // into that slice. This, however, requires additional O(n) space complexity. + recheckCursor *clist.CElement // next expected response + recheckEnd *clist.CElement // re-checking stops here + + // priorityIndex defines the priority index of valid transactions via a + // thread-safe priority queue. + priorityIndex *TxPriorityQueue + + // heightIndex defines a height-based, in ascending order, transaction index. + // i.e. older transactions are first. + heightIndex *WrappedTxList + + // timestampIndex defines a timestamp-based, in ascending order, transaction + // index. i.e. older transactions are first. + timestampIndex *WrappedTxList + + // A read/write lock is used to safe guard updates, insertions and deletions + // from the mempool. A read-lock is implicitly acquired when executing CheckTx, + // however, a caller must explicitly grab a write-lock via Lock when updating + // the mempool via Update(). + mtx tmsync.RWMutex + preCheck mempool.PreCheckFunc + postCheck mempool.PostCheckFunc +} + +func NewTxMempool( + logger log.Logger, + cfg *config.MempoolConfig, + proxyAppConn proxy.AppConnMempool, + height int64, + options ...TxMempoolOption, +) *TxMempool { + + txmp := &TxMempool{ + logger: logger, + config: cfg, + proxyAppConn: proxyAppConn, + height: height, + cache: mempool.NopTxCache{}, + metrics: mempool.NopMetrics(), + txStore: NewTxStore(), + gossipIndex: clist.New(), + priorityIndex: NewTxPriorityQueue(), + heightIndex: NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { + return wtx1.height >= wtx2.height + }), + timestampIndex: NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { + return wtx1.timestamp.After(wtx2.timestamp) || wtx1.timestamp.Equal(wtx2.timestamp) + }), + } + + if cfg.CacheSize > 0 { + txmp.cache = mempool.NewLRUTxCache(cfg.CacheSize) + } + + proxyAppConn.SetResponseCallback(txmp.defaultTxCallback) + + for _, opt := range options { + opt(txmp) + } + + return txmp +} + +// WithPreCheck sets a filter for the mempool to reject a transaction if f(tx) +// returns an error. This is executed before CheckTx. It only applies to the +// first created block. After that, Update() overwrites the existing value. +func WithPreCheck(f mempool.PreCheckFunc) TxMempoolOption { + return func(txmp *TxMempool) { txmp.preCheck = f } +} + +// WithPostCheck sets a filter for the mempool to reject a transaction if +// f(tx, resp) returns an error. This is executed after CheckTx. It only applies +// to the first created block. After that, Update overwrites the existing value. +func WithPostCheck(f mempool.PostCheckFunc) TxMempoolOption { + return func(txmp *TxMempool) { txmp.postCheck = f } +} + +// WithMetrics sets the mempool's metrics collector. +func WithMetrics(metrics *mempool.Metrics) TxMempoolOption { + return func(txmp *TxMempool) { txmp.metrics = metrics } +} + +// Lock obtains a write-lock on the mempool. A caller must be sure to explicitly +// release the lock when finished. +func (txmp *TxMempool) Lock() { + txmp.mtx.Lock() +} + +// Unlock releases a write-lock on the mempool. +func (txmp *TxMempool) Unlock() { + txmp.mtx.Unlock() +} + +// Size returns the number of valid transactions in the mempool. It is +// thread-safe. +func (txmp *TxMempool) Size() int { + return txmp.txStore.Size() +} + +// SizeBytes return the total sum in bytes of all the valid transactions in the +// mempool. It is thread-safe. +func (txmp *TxMempool) SizeBytes() int64 { + return atomic.LoadInt64(&txmp.sizeBytes) +} + +// FlushAppConn executes FlushSync on the mempool's proxyAppConn. +// +// NOTE: The caller must obtain a write-lock via Lock() prior to execution. +func (txmp *TxMempool) FlushAppConn() error { + return txmp.proxyAppConn.FlushSync(context.Background()) +} + +// WaitForNextTx returns a blocking channel that will be closed when the next +// valid transaction is available to gossip. It is thread-safe. +func (txmp *TxMempool) WaitForNextTx() <-chan struct{} { + return txmp.gossipIndex.WaitChan() +} + +// NextGossipTx returns the next valid transaction to gossip. A caller must wait +// for WaitForNextTx to signal a transaction is available to gossip first. It is +// thread-safe. +func (txmp *TxMempool) NextGossipTx() *clist.CElement { + return txmp.gossipIndex.Front() +} + +// EnableTxsAvailable enables the mempool to trigger events when transactions +// are available on a block by block basis. +func (txmp *TxMempool) EnableTxsAvailable() { + txmp.mtx.Lock() + defer txmp.mtx.Unlock() + + txmp.txsAvailable = make(chan struct{}, 1) +} + +// TxsAvailable returns a channel which fires once for every height, and only +// when transactions are available in the mempool. It is thread-safe. +func (txmp *TxMempool) TxsAvailable() <-chan struct{} { + return txmp.txsAvailable +} + +// CheckTx executes the ABCI CheckTx method for a given transaction. It acquires +// a read-lock attempts to execute the application's CheckTx ABCI method via +// CheckTxAsync. We return an error if any of the following happen: +// +// - The CheckTxAsync execution fails. +// - The transaction already exists in the cache and we've already received the +// transaction from the peer. Otherwise, if it solely exists in the cache, we +// return nil. +// - The transaction size exceeds the maximum transaction size as defined by the +// configuration provided to the mempool. +// - The transaction fails Pre-Check (if it is defined). +// - The proxyAppConn fails, e.g. the buffer is full. +// +// If the mempool is full, we still execute CheckTx and attempt to find a lower +// priority transaction to evict. If such a transaction exists, we remove the +// lower priority transaction and add the new one with higher priority. +// +// NOTE: +// - The applications' CheckTx implementation may panic. +// - The caller is not to explicitly require any locks for executing CheckTx. +func (txmp *TxMempool) CheckTx( + ctx context.Context, + tx types.Tx, + cb func(*abci.Response), + txInfo mempool.TxInfo, +) error { + + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() + + txSize := len(tx) + if txSize > txmp.config.MaxTxBytes { + return types.ErrTxTooLarge{ + Max: txmp.config.MaxTxBytes, + Actual: txSize, + } + } + + if txmp.preCheck != nil { + if err := txmp.preCheck(tx); err != nil { + return types.ErrPreCheck{ + Reason: err, + } + } + } + + if err := txmp.proxyAppConn.Error(); err != nil { + return err + } + + txHash := tx.Key() + + // We add the transaction to the mempool's cache and if the + // transaction is already present in the cache, i.e. false is returned, then we + // check if we've seen this transaction and error if we have. + if !txmp.cache.Push(tx) { + txmp.txStore.GetOrSetPeerByTxHash(txHash, txInfo.SenderID) + return types.ErrTxInCache + } + + if ctx == nil { + ctx = context.Background() + } + + reqRes, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{Tx: tx}) + if err != nil { + txmp.cache.Remove(tx) + return err + } + + reqRes.SetCallback(func(res *abci.Response) { + if txmp.recheckCursor != nil { + panic("recheck cursor is non-nil in CheckTx callback") + } + + wtx := &WrappedTx{ + tx: tx, + hash: txHash, + timestamp: time.Now().UTC(), + height: txmp.height, + } + txmp.initTxCallback(wtx, res, txInfo) + + if cb != nil { + cb(res) + } + }) + + return nil +} + +func (txmp *TxMempool) RemoveTxByKey(txKey types.TxKey) error { + txmp.Lock() + defer txmp.Unlock() + + // remove the committed transaction from the transaction store and indexes + if wtx := txmp.txStore.GetTxByHash(txKey); wtx != nil { + txmp.removeTx(wtx, false) + return nil + } + + return errors.New("transaction not found") +} + +// Flush flushes out the mempool. It acquires a read-lock, fetches all the +// transactions currently in the transaction store and removes each transaction +// from the store and all indexes and finally resets the cache. +// +// NOTE: +// - Flushing the mempool may leave the mempool in an inconsistent state. +func (txmp *TxMempool) Flush() { + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() + + txmp.heightIndex.Reset() + txmp.timestampIndex.Reset() + + for _, wtx := range txmp.txStore.GetAllTxs() { + txmp.removeTx(wtx, false) + } + + atomic.SwapInt64(&txmp.sizeBytes, 0) + txmp.cache.Reset() +} + +// ReapMaxBytesMaxGas returns a list of transactions within the provided size +// and gas constraints. Transaction are retrieved in priority order. +// +// NOTE: +// - A read-lock is acquired. +// - Transactions returned are not actually removed from the mempool transaction +// store or indexes. +func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() + + var ( + totalGas int64 + totalSize int64 + ) + + // wTxs contains a list of *WrappedTx retrieved from the priority queue that + // need to be re-enqueued prior to returning. + wTxs := make([]*WrappedTx, 0, txmp.priorityIndex.NumTxs()) + defer func() { + for _, wtx := range wTxs { + txmp.priorityIndex.PushTx(wtx) + } + }() + + txs := make([]types.Tx, 0, txmp.priorityIndex.NumTxs()) + for txmp.priorityIndex.NumTxs() > 0 { + wtx := txmp.priorityIndex.PopTx() + txs = append(txs, wtx.tx) + wTxs = append(wTxs, wtx) + size := types.ComputeProtoSizeForTxs([]types.Tx{wtx.tx}) + + // Ensure we have capacity for the transaction with respect to the + // transaction size. + if maxBytes > -1 && totalSize+size > maxBytes { + return txs[:len(txs)-1] + } + + totalSize += size + + // ensure we have capacity for the transaction with respect to total gas + gas := totalGas + wtx.gasWanted + if maxGas > -1 && gas > maxGas { + return txs[:len(txs)-1] + } + + totalGas = gas + } + + return txs +} + +// ReapMaxTxs returns a list of transactions within the provided number of +// transactions bound. Transaction are retrieved in priority order. +// +// NOTE: +// - A read-lock is acquired. +// - Transactions returned are not actually removed from the mempool transaction +// store or indexes. +func (txmp *TxMempool) ReapMaxTxs(max int) types.Txs { + txmp.mtx.RLock() + defer txmp.mtx.RUnlock() + + numTxs := txmp.priorityIndex.NumTxs() + if max < 0 { + max = numTxs + } + + cap := tmmath.MinInt(numTxs, max) + + // wTxs contains a list of *WrappedTx retrieved from the priority queue that + // need to be re-enqueued prior to returning. + wTxs := make([]*WrappedTx, 0, cap) + defer func() { + for _, wtx := range wTxs { + txmp.priorityIndex.PushTx(wtx) + } + }() + + txs := make([]types.Tx, 0, cap) + for txmp.priorityIndex.NumTxs() > 0 && len(txs) < max { + wtx := txmp.priorityIndex.PopTx() + txs = append(txs, wtx.tx) + wTxs = append(wTxs, wtx) + } + + return txs +} + +// Update iterates over all the transactions provided by the caller, i.e. the +// block producer, and removes them from the cache (if applicable) and removes +// the transactions from the main transaction store and associated indexes. +// Finally, if there are trainsactions remaining in the mempool, we initiate a +// re-CheckTx for them (if applicable), otherwise, we notify the caller more +// transactions are available. +// +// NOTE: +// - The caller must explicitly acquire a write-lock via Lock(). +func (txmp *TxMempool) Update( + blockHeight int64, + blockTxs types.Txs, + deliverTxResponses []*abci.ResponseDeliverTx, + newPreFn mempool.PreCheckFunc, + newPostFn mempool.PostCheckFunc, +) error { + + txmp.height = blockHeight + txmp.notifiedTxsAvailable = false + + if newPreFn != nil { + txmp.preCheck = newPreFn + } + if newPostFn != nil { + txmp.postCheck = newPostFn + } + + for i, tx := range blockTxs { + if deliverTxResponses[i].Code == abci.CodeTypeOK { + // add the valid committed transaction to the cache (if missing) + _ = txmp.cache.Push(tx) + } else if !txmp.config.KeepInvalidTxsInCache { + // allow invalid transactions to be re-submitted + txmp.cache.Remove(tx) + } + + // remove the committed transaction from the transaction store and indexes + if wtx := txmp.txStore.GetTxByHash(tx.Key()); wtx != nil { + txmp.removeTx(wtx, false) + } + } + + txmp.purgeExpiredTxs(blockHeight) + + // If there any uncommitted transactions left in the mempool, we either + // initiate re-CheckTx per remaining transaction or notify that remaining + // transactions are left. + if txmp.Size() > 0 { + if txmp.config.Recheck { + txmp.logger.Debug( + "executing re-CheckTx for all remaining transactions", + "num_txs", txmp.Size(), + "height", blockHeight, + ) + txmp.updateReCheckTxs() + } else { + txmp.notifyTxsAvailable() + } + } + + txmp.metrics.Size.Set(float64(txmp.Size())) + return nil +} + +// initTxCallback performs the initial, i.e. the first, callback after CheckTx +// has been executed by the ABCI application. In other words, initTxCallback is +// called after executing CheckTx when we see a unique transaction for the first +// time. CheckTx can be called again for the same transaction at a later point +// in time when re-checking, however, this callback will not be called. +// +// After the ABCI application executes CheckTx, initTxCallback is called with +// the ABCI *Response object and TxInfo. If postCheck is defined on the mempool, +// we execute that first. If there is no error from postCheck (if defined) and +// the ABCI CheckTx response code is OK, we attempt to insert the transaction. +// +// When attempting to insert the transaction, we first check if there is +// sufficient capacity. If there is sufficient capacity, the transaction is +// inserted into the txStore and indexed across all indexes. Otherwise, if the +// mempool is full, we attempt to find a lower priority transaction to evict in +// place of the new incoming transaction. If no such transaction exists, the +// new incoming transaction is rejected. +// +// If the new incoming transaction fails CheckTx or postCheck fails, we reject +// the new incoming transaction. +// +// NOTE: +// - An explicit lock is NOT required. +func (txmp *TxMempool) initTxCallback(wtx *WrappedTx, res *abci.Response, txInfo mempool.TxInfo) { + checkTxRes, ok := res.Value.(*abci.Response_CheckTx) + if !ok { + return + } + + var err error + if txmp.postCheck != nil { + err = txmp.postCheck(wtx.tx, checkTxRes.CheckTx) + } + + if err != nil || checkTxRes.CheckTx.Code != abci.CodeTypeOK { + // ignore bad transactions + txmp.logger.Info( + "rejected bad transaction", + "priority", wtx.priority, + "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "peer_id", txInfo.SenderNodeID, + "code", checkTxRes.CheckTx.Code, + "post_check_err", err, + ) + + txmp.metrics.FailedTxs.Add(1) + + if !txmp.config.KeepInvalidTxsInCache { + txmp.cache.Remove(wtx.tx) + } + if err != nil { + checkTxRes.CheckTx.MempoolError = err.Error() + } + return + } + + sender := checkTxRes.CheckTx.Sender + priority := checkTxRes.CheckTx.Priority + + if len(sender) > 0 { + if wtx := txmp.txStore.GetTxBySender(sender); wtx != nil { + txmp.logger.Error( + "rejected incoming good transaction; tx already exists for sender", + "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "sender", sender, + ) + txmp.metrics.RejectedTxs.Add(1) + return + } + } + + if err := txmp.canAddTx(wtx); err != nil { + evictTxs := txmp.priorityIndex.GetEvictableTxs( + priority, + int64(wtx.Size()), + txmp.SizeBytes(), + txmp.config.MaxTxsBytes, + ) + if len(evictTxs) == 0 { + // No room for the new incoming transaction so we just remove it from + // the cache. + txmp.cache.Remove(wtx.tx) + txmp.logger.Error( + "rejected incoming good transaction; mempool full", + "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "err", err.Error(), + ) + txmp.metrics.RejectedTxs.Add(1) + return + } + + // evict an existing transaction(s) + // + // NOTE: + // - The transaction, toEvict, can be removed while a concurrent + // reCheckTx callback is being executed for the same transaction. + for _, toEvict := range evictTxs { + txmp.removeTx(toEvict, true) + txmp.logger.Debug( + "evicted existing good transaction; mempool full", + "old_tx", fmt.Sprintf("%X", toEvict.tx.Hash()), + "old_priority", toEvict.priority, + "new_tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "new_priority", wtx.priority, + ) + txmp.metrics.EvictedTxs.Add(1) + } + } + + wtx.gasWanted = checkTxRes.CheckTx.GasWanted + wtx.priority = priority + wtx.sender = sender + wtx.peers = map[uint16]struct{}{ + txInfo.SenderID: {}, + } + + txmp.metrics.TxSizeBytes.Observe(float64(wtx.Size())) + txmp.metrics.Size.Set(float64(txmp.Size())) + + txmp.insertTx(wtx) + txmp.logger.Debug( + "inserted good transaction", + "priority", wtx.priority, + "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "height", txmp.height, + "num_txs", txmp.Size(), + ) + txmp.notifyTxsAvailable() + +} + +// defaultTxCallback performs the default CheckTx application callback. This is +// NOT executed when a transaction is first seen/received. Instead, this callback +// is executed during re-checking transactions (if enabled). A caller, i.e a +// block proposer, acquires a mempool write-lock via Lock() and when executing +// Update(), if the mempool is non-empty and Recheck is enabled, then all +// remaining transactions will be rechecked via CheckTxAsync. The order in which +// they are rechecked must be the same order in which this callback is called +// per transaction. +func (txmp *TxMempool) defaultTxCallback(req *abci.Request, res *abci.Response) { + if txmp.recheckCursor == nil { + return + } + + txmp.metrics.RecheckTimes.Add(1) + + checkTxRes, ok := res.Value.(*abci.Response_CheckTx) + if !ok { + txmp.logger.Error("received incorrect type in mempool callback", + "expected", reflect.TypeOf(&abci.Response_CheckTx{}).Name(), + "got", reflect.TypeOf(res.Value).Name(), + ) + return + } + tx := req.GetCheckTx().Tx + wtx := txmp.recheckCursor.Value.(*WrappedTx) + + // Search through the remaining list of tx to recheck for a transaction that matches + // the one we received from the ABCI application. + for { + if bytes.Equal(tx, wtx.tx) { + // We've found a tx in the recheck list that matches the tx that we + // received from the ABCI application. + // Break, and use this transaction for further checks. + break + } + + txmp.logger.Error( + "re-CheckTx transaction mismatch", + "got", wtx.tx.Hash(), + "expected", types.Tx(tx).Key(), + ) + + if txmp.recheckCursor == txmp.recheckEnd { + // we reached the end of the recheckTx list without finding a tx + // matching the one we received from the ABCI application. + // Return without processing any tx. + txmp.recheckCursor = nil + return + } + + txmp.recheckCursor = txmp.recheckCursor.Next() + wtx = txmp.recheckCursor.Value.(*WrappedTx) + } + + // Only evaluate transactions that have not been removed. This can happen + // if an existing transaction is evicted during CheckTx and while this + // callback is being executed for the same evicted transaction. + if !txmp.txStore.IsTxRemoved(wtx.hash) { + var err error + if txmp.postCheck != nil { + err = txmp.postCheck(tx, checkTxRes.CheckTx) + } + + if checkTxRes.CheckTx.Code == abci.CodeTypeOK && err == nil { + wtx.priority = checkTxRes.CheckTx.Priority + } else { + txmp.logger.Debug( + "existing transaction no longer valid; failed re-CheckTx callback", + "priority", wtx.priority, + "tx", fmt.Sprintf("%X", wtx.tx.Hash()), + "err", err, + "code", checkTxRes.CheckTx.Code, + ) + + if wtx.gossipEl != txmp.recheckCursor { + panic("corrupted reCheckTx cursor") + } + + txmp.removeTx(wtx, !txmp.config.KeepInvalidTxsInCache) + } + } + + // move reCheckTx cursor to next element + if txmp.recheckCursor == txmp.recheckEnd { + txmp.recheckCursor = nil + } else { + txmp.recheckCursor = txmp.recheckCursor.Next() + } + + if txmp.recheckCursor == nil { + txmp.logger.Debug("finished rechecking transactions") + + if txmp.Size() > 0 { + txmp.notifyTxsAvailable() + } + } + + txmp.metrics.Size.Set(float64(txmp.Size())) +} + +// updateReCheckTxs updates the recheck cursors by using the gossipIndex. For +// each transaction, it executes CheckTxAsync. The global callback defined on +// the proxyAppConn will be executed for each transaction after CheckTx is +// executed. +// +// NOTE: +// - The caller must have a write-lock when executing updateReCheckTxs. +func (txmp *TxMempool) updateReCheckTxs() { + if txmp.Size() == 0 { + panic("attempted to update re-CheckTx txs when mempool is empty") + } + + txmp.recheckCursor = txmp.gossipIndex.Front() + txmp.recheckEnd = txmp.gossipIndex.Back() + ctx := context.Background() + + for e := txmp.gossipIndex.Front(); e != nil; e = e.Next() { + wtx := e.Value.(*WrappedTx) + + // Only execute CheckTx if the transaction is not marked as removed which + // could happen if the transaction was evicted. + if !txmp.txStore.IsTxRemoved(wtx.hash) { + _, err := txmp.proxyAppConn.CheckTxAsync(ctx, abci.RequestCheckTx{ + Tx: wtx.tx, + Type: abci.CheckTxType_Recheck, + }) + if err != nil { + // no need in retrying since the tx will be rechecked after the next block + txmp.logger.Error("failed to execute CheckTx during rechecking", "err", err) + } + } + } + + if _, err := txmp.proxyAppConn.FlushAsync(ctx); err != nil { + txmp.logger.Error("failed to flush transactions during rechecking", "err", err) + } +} + +// canAddTx returns an error if we cannot insert the provided *WrappedTx into +// the mempool due to mempool configured constraints. Otherwise, nil is returned +// and the transaction can be inserted into the mempool. +func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { + var ( + numTxs = txmp.Size() + sizeBytes = txmp.SizeBytes() + ) + + if numTxs >= txmp.config.Size || int64(wtx.Size())+sizeBytes > txmp.config.MaxTxsBytes { + return types.ErrMempoolIsFull{ + NumTxs: numTxs, + MaxTxs: txmp.config.Size, + TxsBytes: sizeBytes, + MaxTxsBytes: txmp.config.MaxTxsBytes, + } + } + + return nil +} + +func (txmp *TxMempool) insertTx(wtx *WrappedTx) { + txmp.txStore.SetTx(wtx) + txmp.priorityIndex.PushTx(wtx) + txmp.heightIndex.Insert(wtx) + txmp.timestampIndex.Insert(wtx) + + // Insert the transaction into the gossip index and mark the reference to the + // linked-list element, which will be needed at a later point when the + // transaction is removed. + gossipEl := txmp.gossipIndex.PushBack(wtx) + wtx.gossipEl = gossipEl + + atomic.AddInt64(&txmp.sizeBytes, int64(wtx.Size())) +} + +func (txmp *TxMempool) removeTx(wtx *WrappedTx, removeFromCache bool) { + if txmp.txStore.IsTxRemoved(wtx.hash) { + return + } + + txmp.txStore.RemoveTx(wtx) + txmp.priorityIndex.RemoveTx(wtx) + txmp.heightIndex.Remove(wtx) + txmp.timestampIndex.Remove(wtx) + + // Remove the transaction from the gossip index and cleanup the linked-list + // element so it can be garbage collected. + txmp.gossipIndex.Remove(wtx.gossipEl) + wtx.gossipEl.DetachPrev() + + atomic.AddInt64(&txmp.sizeBytes, int64(-wtx.Size())) + + if removeFromCache { + txmp.cache.Remove(wtx.tx) + } +} + +// purgeExpiredTxs removes all transactions that have exceeded their respective +// height and/or time based TTLs from their respective indexes. Every expired +// transaction will be removed from the mempool entirely, except for the cache. +// +// NOTE: purgeExpiredTxs must only be called during TxMempool#Update in which +// the caller has a write-lock on the mempool and so we can safely iterate over +// the height and time based indexes. +func (txmp *TxMempool) purgeExpiredTxs(blockHeight int64) { + now := time.Now() + expiredTxs := make(map[types.TxKey]*WrappedTx) + + if txmp.config.TTLNumBlocks > 0 { + purgeIdx := -1 + for i, wtx := range txmp.heightIndex.txs { + if (blockHeight - wtx.height) > txmp.config.TTLNumBlocks { + expiredTxs[wtx.tx.Key()] = wtx + purgeIdx = i + } else { + // since the index is sorted, we know no other txs can be be purged + break + } + } + + if purgeIdx >= 0 { + txmp.heightIndex.txs = txmp.heightIndex.txs[purgeIdx+1:] + } + } + + if txmp.config.TTLDuration > 0 { + purgeIdx := -1 + for i, wtx := range txmp.timestampIndex.txs { + if now.Sub(wtx.timestamp) > txmp.config.TTLDuration { + expiredTxs[wtx.tx.Key()] = wtx + purgeIdx = i + } else { + // since the index is sorted, we know no other txs can be be purged + break + } + } + + if purgeIdx >= 0 { + txmp.timestampIndex.txs = txmp.timestampIndex.txs[purgeIdx+1:] + } + } + + for _, wtx := range expiredTxs { + txmp.removeTx(wtx, false) + } +} + +func (txmp *TxMempool) notifyTxsAvailable() { + if txmp.Size() == 0 { + panic("attempt to notify txs available but mempool is empty!") + } + + if txmp.txsAvailable != nil && !txmp.notifiedTxsAvailable { + // channel cap is 1, so this will send once + txmp.notifiedTxsAvailable = true + + select { + case txmp.txsAvailable <- struct{}{}: + default: + } + } +} diff --git a/mempool/v1/mempool_bench_test.go b/mempool/v1/mempool_bench_test.go new file mode 100644 index 000000000..ca23f1479 --- /dev/null +++ b/mempool/v1/mempool_bench_test.go @@ -0,0 +1,32 @@ +package v1 + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/internal/mempool" +) + +func BenchmarkTxMempool_CheckTx(b *testing.B) { + txmp := setup(b, 10000) + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + b.StopTimer() + prefix := make([]byte, 20) + _, err := rng.Read(prefix) + require.NoError(b, err) + + priority := int64(rng.Intn(9999-1000) + 1000) + tx := []byte(fmt.Sprintf("%X=%d", prefix, priority)) + b.StartTimer() + + require.NoError(b, txmp.CheckTx(context.Background(), tx, nil, mempool.TxInfo{})) + } +} diff --git a/mempool/v1/mempool_test.go b/mempool/v1/mempool_test.go new file mode 100644 index 000000000..72a72861c --- /dev/null +++ b/mempool/v1/mempool_test.go @@ -0,0 +1,529 @@ +package v1 + +import ( + "bytes" + "context" + "errors" + "fmt" + "math/rand" + "os" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + abciclient "github.com/tendermint/tendermint/abci/client" + "github.com/tendermint/tendermint/abci/example/code" + "github.com/tendermint/tendermint/abci/example/kvstore" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/internal/mempool" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/types" +) + +// application extends the KV store application by overriding CheckTx to provide +// transaction priority based on the value in the key/value pair. +type application struct { + *kvstore.Application +} + +type testTx struct { + tx types.Tx + priority int64 +} + +func (app *application) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + var ( + priority int64 + sender string + ) + + // infer the priority from the raw transaction value (sender=key=value) + parts := bytes.Split(req.Tx, []byte("=")) + if len(parts) == 3 { + v, err := strconv.ParseInt(string(parts[2]), 10, 64) + if err != nil { + return abci.ResponseCheckTx{ + Priority: priority, + Code: 100, + GasWanted: 1, + } + } + + priority = v + sender = string(parts[0]) + } else { + return abci.ResponseCheckTx{ + Priority: priority, + Code: 101, + GasWanted: 1, + } + } + + return abci.ResponseCheckTx{ + Priority: priority, + Sender: sender, + Code: code.CodeTypeOK, + GasWanted: 1, + } +} + +func setup(t testing.TB, cacheSize int, options ...TxMempoolOption) *TxMempool { + t.Helper() + + app := &application{kvstore.NewApplication()} + cc := abciclient.NewLocalCreator(app) + + cfg, err := config.ResetTestRoot(strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + cfg.Mempool.CacheSize = cacheSize + + appConnMem, err := cc() + require.NoError(t, err) + require.NoError(t, appConnMem.Start()) + + t.Cleanup(func() { + os.RemoveAll(cfg.RootDir) + require.NoError(t, appConnMem.Stop()) + }) + + return NewTxMempool(log.TestingLogger().With("test", t.Name()), cfg.Mempool, appConnMem, 0, options...) +} + +func checkTxs(t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { + txs := make([]testTx, numTxs) + txInfo := mempool.TxInfo{SenderID: peerID} + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + for i := 0; i < numTxs; i++ { + prefix := make([]byte, 20) + _, err := rng.Read(prefix) + require.NoError(t, err) + + priority := int64(rng.Intn(9999-1000) + 1000) + + txs[i] = testTx{ + tx: []byte(fmt.Sprintf("sender-%d-%d=%X=%d", i, peerID, prefix, priority)), + priority: priority, + } + require.NoError(t, txmp.CheckTx(context.Background(), txs[i].tx, nil, txInfo)) + } + + return txs +} + +func TestTxMempool_TxsAvailable(t *testing.T) { + txmp := setup(t, 0) + txmp.EnableTxsAvailable() + + ensureNoTxFire := func() { + timer := time.NewTimer(500 * time.Millisecond) + select { + case <-txmp.TxsAvailable(): + require.Fail(t, "unexpected transactions event") + case <-timer.C: + } + } + + ensureTxFire := func() { + timer := time.NewTimer(500 * time.Millisecond) + select { + case <-txmp.TxsAvailable(): + case <-timer.C: + require.Fail(t, "expected transactions event") + } + } + + // ensure no event as we have not executed any transactions yet + ensureNoTxFire() + + // Execute CheckTx for some transactions and ensure TxsAvailable only fires + // once. + txs := checkTxs(t, txmp, 100, 0) + ensureTxFire() + ensureNoTxFire() + + rawTxs := make([]types.Tx, len(txs)) + for i, tx := range txs { + rawTxs[i] = tx.tx + } + + responses := make([]*abci.ResponseDeliverTx, len(rawTxs[:50])) + for i := 0; i < len(responses); i++ { + responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK} + } + + // commit half the transactions and ensure we fire an event + txmp.Lock() + require.NoError(t, txmp.Update(1, rawTxs[:50], responses, nil, nil)) + txmp.Unlock() + ensureTxFire() + ensureNoTxFire() + + // Execute CheckTx for more transactions and ensure we do not fire another + // event as we're still on the same height (1). + _ = checkTxs(t, txmp, 100, 0) + ensureNoTxFire() +} + +func TestTxMempool_Size(t *testing.T) { + txmp := setup(t, 0) + txs := checkTxs(t, txmp, 100, 0) + require.Equal(t, len(txs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + + rawTxs := make([]types.Tx, len(txs)) + for i, tx := range txs { + rawTxs[i] = tx.tx + } + + responses := make([]*abci.ResponseDeliverTx, len(rawTxs[:50])) + for i := 0; i < len(responses); i++ { + responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK} + } + + txmp.Lock() + require.NoError(t, txmp.Update(1, rawTxs[:50], responses, nil, nil)) + txmp.Unlock() + + require.Equal(t, len(rawTxs)/2, txmp.Size()) + require.Equal(t, int64(2850), txmp.SizeBytes()) +} + +func TestTxMempool_Flush(t *testing.T) { + txmp := setup(t, 0) + txs := checkTxs(t, txmp, 100, 0) + require.Equal(t, len(txs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + + rawTxs := make([]types.Tx, len(txs)) + for i, tx := range txs { + rawTxs[i] = tx.tx + } + + responses := make([]*abci.ResponseDeliverTx, len(rawTxs[:50])) + for i := 0; i < len(responses); i++ { + responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK} + } + + txmp.Lock() + require.NoError(t, txmp.Update(1, rawTxs[:50], responses, nil, nil)) + txmp.Unlock() + + txmp.Flush() + require.Zero(t, txmp.Size()) + require.Equal(t, int64(0), txmp.SizeBytes()) +} + +func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { + txmp := setup(t, 0) + tTxs := checkTxs(t, txmp, 100, 0) // all txs request 1 gas unit + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + + txMap := make(map[types.TxKey]testTx) + priorities := make([]int64, len(tTxs)) + for i, tTx := range tTxs { + txMap[tTx.tx.Key()] = tTx + priorities[i] = tTx.priority + } + + sort.Slice(priorities, func(i, j int) bool { + // sort by priority, i.e. decreasing order + return priorities[i] > priorities[j] + }) + + ensurePrioritized := func(reapedTxs types.Txs) { + reapedPriorities := make([]int64, len(reapedTxs)) + for i, rTx := range reapedTxs { + reapedPriorities[i] = txMap[rTx.Key()].priority + } + + require.Equal(t, priorities[:len(reapedPriorities)], reapedPriorities) + } + + // reap by gas capacity only + reapedTxs := txmp.ReapMaxBytesMaxGas(-1, 50) + ensurePrioritized(reapedTxs) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Len(t, reapedTxs, 50) + + // reap by transaction bytes only + reapedTxs = txmp.ReapMaxBytesMaxGas(1000, -1) + ensurePrioritized(reapedTxs) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + require.GreaterOrEqual(t, len(reapedTxs), 16) + + // Reap by both transaction bytes and gas, where the size yields 31 reaped + // transactions and the gas limit reaps 25 transactions. + reapedTxs = txmp.ReapMaxBytesMaxGas(1500, 30) + ensurePrioritized(reapedTxs) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Len(t, reapedTxs, 25) +} + +func TestTxMempool_ReapMaxTxs(t *testing.T) { + txmp := setup(t, 0) + tTxs := checkTxs(t, txmp, 100, 0) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + + txMap := make(map[types.TxKey]testTx) + priorities := make([]int64, len(tTxs)) + for i, tTx := range tTxs { + txMap[tTx.tx.Key()] = tTx + priorities[i] = tTx.priority + } + + sort.Slice(priorities, func(i, j int) bool { + // sort by priority, i.e. decreasing order + return priorities[i] > priorities[j] + }) + + ensurePrioritized := func(reapedTxs types.Txs) { + reapedPriorities := make([]int64, len(reapedTxs)) + for i, rTx := range reapedTxs { + reapedPriorities[i] = txMap[rTx.Key()].priority + } + + require.Equal(t, priorities[:len(reapedPriorities)], reapedPriorities) + } + + // reap all transactions + reapedTxs := txmp.ReapMaxTxs(-1) + ensurePrioritized(reapedTxs) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Len(t, reapedTxs, len(tTxs)) + + // reap a single transaction + reapedTxs = txmp.ReapMaxTxs(1) + ensurePrioritized(reapedTxs) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Len(t, reapedTxs, 1) + + // reap half of the transactions + reapedTxs = txmp.ReapMaxTxs(len(tTxs) / 2) + ensurePrioritized(reapedTxs) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Len(t, reapedTxs, len(tTxs)/2) +} + +func TestTxMempool_CheckTxExceedsMaxSize(t *testing.T) { + txmp := setup(t, 0) + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + tx := make([]byte, txmp.config.MaxTxBytes+1) + _, err := rng.Read(tx) + require.NoError(t, err) + + require.Error(t, txmp.CheckTx(context.Background(), tx, nil, mempool.TxInfo{SenderID: 0})) + + tx = make([]byte, txmp.config.MaxTxBytes-1) + _, err = rng.Read(tx) + require.NoError(t, err) + + require.NoError(t, txmp.CheckTx(context.Background(), tx, nil, mempool.TxInfo{SenderID: 0})) +} + +func TestTxMempool_CheckTxSamePeer(t *testing.T) { + txmp := setup(t, 100) + peerID := uint16(1) + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + prefix := make([]byte, 20) + _, err := rng.Read(prefix) + require.NoError(t, err) + + tx := []byte(fmt.Sprintf("sender-0=%X=%d", prefix, 50)) + + require.NoError(t, txmp.CheckTx(context.Background(), tx, nil, mempool.TxInfo{SenderID: peerID})) + require.Error(t, txmp.CheckTx(context.Background(), tx, nil, mempool.TxInfo{SenderID: peerID})) +} + +func TestTxMempool_CheckTxSameSender(t *testing.T) { + txmp := setup(t, 100) + peerID := uint16(1) + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + prefix1 := make([]byte, 20) + _, err := rng.Read(prefix1) + require.NoError(t, err) + + prefix2 := make([]byte, 20) + _, err = rng.Read(prefix2) + require.NoError(t, err) + + tx1 := []byte(fmt.Sprintf("sender-0=%X=%d", prefix1, 50)) + tx2 := []byte(fmt.Sprintf("sender-0=%X=%d", prefix2, 50)) + + require.NoError(t, txmp.CheckTx(context.Background(), tx1, nil, mempool.TxInfo{SenderID: peerID})) + require.Equal(t, 1, txmp.Size()) + require.NoError(t, txmp.CheckTx(context.Background(), tx2, nil, mempool.TxInfo{SenderID: peerID})) + require.Equal(t, 1, txmp.Size()) +} + +func TestTxMempool_ConcurrentTxs(t *testing.T) { + txmp := setup(t, 100) + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + checkTxDone := make(chan struct{}) + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + for i := 0; i < 20; i++ { + _ = checkTxs(t, txmp, 100, 0) + dur := rng.Intn(1000-500) + 500 + time.Sleep(time.Duration(dur) * time.Millisecond) + } + + wg.Done() + close(checkTxDone) + }() + + wg.Add(1) + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + defer wg.Done() + + var height int64 = 1 + + for range ticker.C { + reapedTxs := txmp.ReapMaxTxs(200) + if len(reapedTxs) > 0 { + responses := make([]*abci.ResponseDeliverTx, len(reapedTxs)) + for i := 0; i < len(responses); i++ { + var code uint32 + + if i%10 == 0 { + code = 100 + } else { + code = abci.CodeTypeOK + } + + responses[i] = &abci.ResponseDeliverTx{Code: code} + } + + txmp.Lock() + require.NoError(t, txmp.Update(height, reapedTxs, responses, nil, nil)) + txmp.Unlock() + + height++ + } else { + // only return once we know we finished the CheckTx loop + select { + case <-checkTxDone: + return + default: + } + } + } + }() + + wg.Wait() + require.Zero(t, txmp.Size()) + require.Zero(t, txmp.SizeBytes()) +} + +func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { + txmp := setup(t, 500) + txmp.height = 100 + txmp.config.TTLNumBlocks = 10 + + tTxs := checkTxs(t, txmp, 100, 0) + require.Equal(t, len(tTxs), txmp.Size()) + require.Equal(t, 100, txmp.heightIndex.Size()) + + // reap 5 txs at the next height -- no txs should expire + reapedTxs := txmp.ReapMaxTxs(5) + responses := make([]*abci.ResponseDeliverTx, len(reapedTxs)) + for i := 0; i < len(responses); i++ { + responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK} + } + + txmp.Lock() + require.NoError(t, txmp.Update(txmp.height+1, reapedTxs, responses, nil, nil)) + txmp.Unlock() + + require.Equal(t, 95, txmp.Size()) + require.Equal(t, 95, txmp.heightIndex.Size()) + + // check more txs at height 101 + _ = checkTxs(t, txmp, 50, 1) + require.Equal(t, 145, txmp.Size()) + require.Equal(t, 145, txmp.heightIndex.Size()) + + // Reap 5 txs at a height that would expire all the transactions from before + // the previous Update (height 100). + // + // NOTE: When we reap txs below, we do not know if we're picking txs from the + // initial CheckTx calls or from the second round of CheckTx calls. Thus, we + // cannot guarantee that all 95 txs are remaining that should be expired and + // removed. However, we do know that that at most 95 txs can be expired and + // removed. + reapedTxs = txmp.ReapMaxTxs(5) + responses = make([]*abci.ResponseDeliverTx, len(reapedTxs)) + for i := 0; i < len(responses); i++ { + responses[i] = &abci.ResponseDeliverTx{Code: abci.CodeTypeOK} + } + + txmp.Lock() + require.NoError(t, txmp.Update(txmp.height+10, reapedTxs, responses, nil, nil)) + txmp.Unlock() + + require.GreaterOrEqual(t, txmp.Size(), 45) + require.GreaterOrEqual(t, txmp.heightIndex.Size(), 45) +} + +func TestTxMempool_CheckTxPostCheckError(t *testing.T) { + cases := []struct { + name string + err error + }{ + { + name: "error", + err: errors.New("test error"), + }, + { + name: "no error", + err: nil, + }, + } + for _, tc := range cases { + testCase := tc + t.Run(testCase.name, func(t *testing.T) { + postCheckFn := func(_ types.Tx, _ *abci.ResponseCheckTx) error { + return testCase.err + } + txmp := setup(t, 0, WithPostCheck(postCheckFn)) + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + tx := make([]byte, txmp.config.MaxTxBytes-1) + _, err := rng.Read(tx) + require.NoError(t, err) + + callback := func(res *abci.Response) { + checkTxRes, ok := res.Value.(*abci.Response_CheckTx) + require.True(t, ok) + expectedErrString := "" + if testCase.err != nil { + expectedErrString = testCase.err.Error() + } + require.Equal(t, expectedErrString, checkTxRes.CheckTx.MempoolError) + } + require.NoError(t, txmp.CheckTx(context.Background(), tx, callback, mempool.TxInfo{SenderID: 0})) + }) + } +} diff --git a/mempool/v1/priority_queue.go b/mempool/v1/priority_queue.go new file mode 100644 index 000000000..df74a92d3 --- /dev/null +++ b/mempool/v1/priority_queue.go @@ -0,0 +1,159 @@ +package v1 + +import ( + "container/heap" + "sort" + + tmsync "github.com/tendermint/tendermint/internal/libs/sync" +) + +var _ heap.Interface = (*TxPriorityQueue)(nil) + +// TxPriorityQueue defines a thread-safe priority queue for valid transactions. +type TxPriorityQueue struct { + mtx tmsync.RWMutex + txs []*WrappedTx +} + +func NewTxPriorityQueue() *TxPriorityQueue { + pq := &TxPriorityQueue{ + txs: make([]*WrappedTx, 0), + } + + heap.Init(pq) + + return pq +} + +// GetEvictableTxs attempts to find and return a list of *WrappedTx than can be +// evicted to make room for another *WrappedTx with higher priority. If no such +// list of *WrappedTx exists, nil will be returned. The returned list of *WrappedTx +// indicate that these transactions can be removed due to them being of lower +// priority and that their total sum in size allows room for the incoming +// transaction according to the mempool's configured limits. +func (pq *TxPriorityQueue) GetEvictableTxs(priority, txSize, totalSize, cap int64) []*WrappedTx { + pq.mtx.RLock() + defer pq.mtx.RUnlock() + + txs := make([]*WrappedTx, len(pq.txs)) + copy(txs, pq.txs) + + sort.Slice(txs, func(i, j int) bool { + return txs[i].priority < txs[j].priority + }) + + var ( + toEvict []*WrappedTx + i int + ) + + currSize := totalSize + + // Loop over all transactions in ascending priority order evaluating those + // that are only of less priority than the provided argument. We continue + // evaluating transactions until there is sufficient capacity for the new + // transaction (size) as defined by txSize. + for i < len(txs) && txs[i].priority < priority { + toEvict = append(toEvict, txs[i]) + currSize -= int64(txs[i].Size()) + + if currSize+txSize <= cap { + return toEvict + } + + i++ + } + + return nil +} + +// NumTxs returns the number of transactions in the priority queue. It is +// thread safe. +func (pq *TxPriorityQueue) NumTxs() int { + pq.mtx.RLock() + defer pq.mtx.RUnlock() + + return len(pq.txs) +} + +// RemoveTx removes a specific transaction from the priority queue. +func (pq *TxPriorityQueue) RemoveTx(tx *WrappedTx) { + pq.mtx.Lock() + defer pq.mtx.Unlock() + + if tx.heapIndex < len(pq.txs) { + heap.Remove(pq, tx.heapIndex) + } +} + +// PushTx adds a valid transaction to the priority queue. It is thread safe. +func (pq *TxPriorityQueue) PushTx(tx *WrappedTx) { + pq.mtx.Lock() + defer pq.mtx.Unlock() + + heap.Push(pq, tx) +} + +// PopTx removes the top priority transaction from the queue. It is thread safe. +func (pq *TxPriorityQueue) PopTx() *WrappedTx { + pq.mtx.Lock() + defer pq.mtx.Unlock() + + x := heap.Pop(pq) + if x != nil { + return x.(*WrappedTx) + } + + return nil +} + +// Push implements the Heap interface. +// +// NOTE: A caller should never call Push. Use PushTx instead. +func (pq *TxPriorityQueue) Push(x interface{}) { + n := len(pq.txs) + item := x.(*WrappedTx) + item.heapIndex = n + pq.txs = append(pq.txs, item) +} + +// Pop implements the Heap interface. +// +// NOTE: A caller should never call Pop. Use PopTx instead. +func (pq *TxPriorityQueue) Pop() interface{} { + old := pq.txs + n := len(old) + item := old[n-1] + old[n-1] = nil // avoid memory leak + item.heapIndex = -1 // for safety + pq.txs = old[0 : n-1] + return item +} + +// Len implements the Heap interface. +// +// NOTE: A caller should never call Len. Use NumTxs instead. +func (pq *TxPriorityQueue) Len() int { + return len(pq.txs) +} + +// Less implements the Heap interface. It returns true if the transaction at +// position i in the queue is of less priority than the transaction at position j. +func (pq *TxPriorityQueue) Less(i, j int) bool { + // If there exists two transactions with the same priority, consider the one + // that we saw the earliest as the higher priority transaction. + if pq.txs[i].priority == pq.txs[j].priority { + return pq.txs[i].timestamp.Before(pq.txs[j].timestamp) + } + + // We want Pop to give us the highest, not lowest, priority so we use greater + // than here. + return pq.txs[i].priority > pq.txs[j].priority +} + +// Swap implements the Heap interface. It swaps two transactions in the queue. +func (pq *TxPriorityQueue) Swap(i, j int) { + pq.txs[i], pq.txs[j] = pq.txs[j], pq.txs[i] + pq.txs[i].heapIndex = i + pq.txs[j].heapIndex = j +} diff --git a/mempool/v1/priority_queue_test.go b/mempool/v1/priority_queue_test.go new file mode 100644 index 000000000..c0048f388 --- /dev/null +++ b/mempool/v1/priority_queue_test.go @@ -0,0 +1,176 @@ +package v1 + +import ( + "math/rand" + "sort" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTxPriorityQueue(t *testing.T) { + pq := NewTxPriorityQueue() + numTxs := 1000 + + priorities := make([]int, numTxs) + + var wg sync.WaitGroup + for i := 1; i <= numTxs; i++ { + priorities[i-1] = i + wg.Add(1) + + go func(i int) { + pq.PushTx(&WrappedTx{ + priority: int64(i), + timestamp: time.Now(), + }) + + wg.Done() + }(i) + } + + sort.Sort(sort.Reverse(sort.IntSlice(priorities))) + + wg.Wait() + require.Equal(t, numTxs, pq.NumTxs()) + + // Wait a second and push a tx with a duplicate priority + time.Sleep(time.Second) + now := time.Now() + pq.PushTx(&WrappedTx{ + priority: 1000, + timestamp: now, + }) + require.Equal(t, 1001, pq.NumTxs()) + + tx := pq.PopTx() + require.Equal(t, 1000, pq.NumTxs()) + require.Equal(t, int64(1000), tx.priority) + require.NotEqual(t, now, tx.timestamp) + + gotPriorities := make([]int, 0) + for pq.NumTxs() > 0 { + gotPriorities = append(gotPriorities, int(pq.PopTx().priority)) + } + + require.Equal(t, priorities, gotPriorities) +} + +func TestTxPriorityQueue_GetEvictableTxs(t *testing.T) { + pq := NewTxPriorityQueue() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + values := make([]int, 1000) + + for i := 0; i < 1000; i++ { + tx := make([]byte, 5) // each tx is 5 bytes + _, err := rng.Read(tx) + require.NoError(t, err) + + x := rng.Intn(100000) + pq.PushTx(&WrappedTx{ + tx: tx, + priority: int64(x), + }) + + values[i] = x + } + + sort.Ints(values) + + max := values[len(values)-1] + min := values[0] + totalSize := int64(len(values) * 5) + + testCases := []struct { + name string + priority, txSize, totalSize, cap int64 + expectedLen int + }{ + { + name: "larest priority; single tx", + priority: int64(max + 1), + txSize: 5, + totalSize: totalSize, + cap: totalSize, + expectedLen: 1, + }, + { + name: "larest priority; multi tx", + priority: int64(max + 1), + txSize: 17, + totalSize: totalSize, + cap: totalSize, + expectedLen: 4, + }, + { + name: "larest priority; out of capacity", + priority: int64(max + 1), + txSize: totalSize + 1, + totalSize: totalSize, + cap: totalSize, + expectedLen: 0, + }, + { + name: "smallest priority; no tx", + priority: int64(min - 1), + txSize: 5, + totalSize: totalSize, + cap: totalSize, + expectedLen: 0, + }, + { + name: "small priority; no tx", + priority: int64(min), + txSize: 5, + totalSize: totalSize, + cap: totalSize, + expectedLen: 0, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + evictTxs := pq.GetEvictableTxs(tc.priority, tc.txSize, tc.totalSize, tc.cap) + require.Len(t, evictTxs, tc.expectedLen) + }) + } +} + +func TestTxPriorityQueue_RemoveTx(t *testing.T) { + pq := NewTxPriorityQueue() + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + numTxs := 1000 + + values := make([]int, numTxs) + + for i := 0; i < numTxs; i++ { + x := rng.Intn(100000) + pq.PushTx(&WrappedTx{ + priority: int64(x), + }) + + values[i] = x + } + + require.Equal(t, numTxs, pq.NumTxs()) + + sort.Ints(values) + max := values[len(values)-1] + + wtx := pq.txs[pq.NumTxs()/2] + pq.RemoveTx(wtx) + require.Equal(t, numTxs-1, pq.NumTxs()) + require.Equal(t, int64(max), pq.PopTx().priority) + require.Equal(t, numTxs-2, pq.NumTxs()) + + require.NotPanics(t, func() { + pq.RemoveTx(&WrappedTx{heapIndex: numTxs}) + pq.RemoveTx(&WrappedTx{heapIndex: numTxs + 1}) + }) + require.Equal(t, numTxs-2, pq.NumTxs()) +} diff --git a/mempool/v1/reactor.go b/mempool/v1/reactor.go new file mode 100644 index 000000000..94d0580e9 --- /dev/null +++ b/mempool/v1/reactor.go @@ -0,0 +1,385 @@ +package v1 + +import ( + "context" + "errors" + "fmt" + "runtime/debug" + "sync" + + "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/internal/libs/clist" + tmsync "github.com/tendermint/tendermint/internal/libs/sync" + "github.com/tendermint/tendermint/internal/mempool" + "github.com/tendermint/tendermint/internal/p2p" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/service" + protomem "github.com/tendermint/tendermint/proto/tendermint/mempool" + "github.com/tendermint/tendermint/types" +) + +var ( + _ service.Service = (*Reactor)(nil) + _ p2p.Wrapper = (*protomem.Message)(nil) +) + +// Reactor implements a service that contains mempool of txs that are broadcasted +// amongst peers. It maintains a map from peer ID to counter, to prevent gossiping +// txs to the peers you received it from. +type Reactor struct { + service.BaseService + + cfg *config.MempoolConfig + mempool *TxMempool + ids *mempool.MempoolIDs + + mempoolCh *p2p.Channel + peerUpdates *p2p.PeerUpdates + closeCh chan struct{} + + // peerWG is used to coordinate graceful termination of all peer broadcasting + // goroutines. + peerWG sync.WaitGroup + + // observePanic is a function for observing panics that were recovered in methods on + // Reactor. observePanic is called with the recovered value. + observePanic func(interface{}) + + mtx tmsync.Mutex + peerRoutines map[types.NodeID]*tmsync.Closer +} + +// NewReactor returns a reference to a new reactor. +func NewReactor( + logger log.Logger, + cfg *config.MempoolConfig, + txmp *TxMempool, + mempoolCh *p2p.Channel, + peerUpdates *p2p.PeerUpdates, +) *Reactor { + + r := &Reactor{ + cfg: cfg, + mempool: txmp, + ids: mempool.NewMempoolIDs(), + mempoolCh: mempoolCh, + peerUpdates: peerUpdates, + closeCh: make(chan struct{}), + peerRoutines: make(map[types.NodeID]*tmsync.Closer), + observePanic: defaultObservePanic, + } + + r.BaseService = *service.NewBaseService(logger, "Mempool", r) + return r +} + +func defaultObservePanic(r interface{}) {} + +// GetChannelShims returns a map of ChannelDescriptorShim objects, where each +// object wraps a reference to a legacy p2p ChannelDescriptor and the corresponding +// p2p proto.Message the new p2p Channel is responsible for handling. +// +// +// TODO: Remove once p2p refactor is complete. +// ref: https://github.com/tendermint/tendermint/issues/5670 +func GetChannelShims(cfg *config.MempoolConfig) map[p2p.ChannelID]*p2p.ChannelDescriptorShim { + largestTx := make([]byte, cfg.MaxTxBytes) + batchMsg := protomem.Message{ + Sum: &protomem.Message_Txs{ + Txs: &protomem.Txs{Txs: [][]byte{largestTx}}, + }, + } + + return map[p2p.ChannelID]*p2p.ChannelDescriptorShim{ + mempool.MempoolChannel: { + MsgType: new(protomem.Message), + Descriptor: &p2p.ChannelDescriptor{ + ID: byte(mempool.MempoolChannel), + Priority: 5, + RecvMessageCapacity: batchMsg.Size(), + RecvBufferCapacity: 128, + MaxSendBytes: 5000, + }, + }, + } +} + +// OnStart starts separate go routines for each p2p Channel and listens for +// envelopes on each. In addition, it also listens for peer updates and handles +// messages on that p2p channel accordingly. The caller must be sure to execute +// OnStop to ensure the outbound p2p Channels are closed. +func (r *Reactor) OnStart() error { + if !r.cfg.Broadcast { + r.Logger.Info("tx broadcasting is disabled") + } + + go r.processMempoolCh() + go r.processPeerUpdates() + + return nil +} + +// OnStop stops the reactor by signaling to all spawned goroutines to exit and +// blocking until they all exit. +func (r *Reactor) OnStop() { + r.mtx.Lock() + for _, c := range r.peerRoutines { + c.Close() + } + r.mtx.Unlock() + + // wait for all spawned peer tx broadcasting goroutines to gracefully exit + r.peerWG.Wait() + + // Close closeCh to signal to all spawned goroutines to gracefully exit. All + // p2p Channels should execute Close(). + close(r.closeCh) + + // Wait for all p2p Channels to be closed before returning. This ensures we + // can easily reason about synchronization of all p2p Channels and ensure no + // panics will occur. + <-r.mempoolCh.Done() + <-r.peerUpdates.Done() +} + +// handleMempoolMessage handles envelopes sent from peers on the MempoolChannel. +// For every tx in the message, we execute CheckTx. It returns an error if an +// empty set of txs are sent in an envelope or if we receive an unexpected +// message type. +func (r *Reactor) handleMempoolMessage(envelope p2p.Envelope) error { + logger := r.Logger.With("peer", envelope.From) + + switch msg := envelope.Message.(type) { + case *protomem.Txs: + protoTxs := msg.GetTxs() + if len(protoTxs) == 0 { + return errors.New("empty txs received from peer") + } + + txInfo := mempool.TxInfo{SenderID: r.ids.GetForPeer(envelope.From)} + if len(envelope.From) != 0 { + txInfo.SenderNodeID = envelope.From + } + + for _, tx := range protoTxs { + if err := r.mempool.CheckTx(context.Background(), types.Tx(tx), nil, txInfo); err != nil { + logger.Error("checktx failed for tx", "tx", fmt.Sprintf("%X", types.Tx(tx).Hash()), "err", err) + } + } + + default: + return fmt.Errorf("received unknown message: %T", msg) + } + + return nil +} + +// handleMessage handles an Envelope sent from a peer on a specific p2p Channel. +// It will handle errors and any possible panics gracefully. A caller can handle +// any error returned by sending a PeerError on the respective channel. +func (r *Reactor) handleMessage(chID p2p.ChannelID, envelope p2p.Envelope) (err error) { + defer func() { + if e := recover(); e != nil { + r.observePanic(e) + err = fmt.Errorf("panic in processing message: %v", e) + r.Logger.Error( + "recovering from processing message panic", + "err", err, + "stack", string(debug.Stack()), + ) + } + }() + + r.Logger.Debug("received message", "peer", envelope.From) + + switch chID { + case mempool.MempoolChannel: + err = r.handleMempoolMessage(envelope) + + default: + err = fmt.Errorf("unknown channel ID (%d) for envelope (%T)", chID, envelope.Message) + } + + return err +} + +// processMempoolCh implements a blocking event loop where we listen for p2p +// Envelope messages from the mempoolCh. +func (r *Reactor) processMempoolCh() { + defer r.mempoolCh.Close() + + for { + select { + case envelope := <-r.mempoolCh.In: + if err := r.handleMessage(r.mempoolCh.ID, envelope); err != nil { + r.Logger.Error("failed to process message", "ch_id", r.mempoolCh.ID, "envelope", envelope, "err", err) + r.mempoolCh.Error <- p2p.PeerError{ + NodeID: envelope.From, + Err: err, + } + } + + case <-r.closeCh: + r.Logger.Debug("stopped listening on mempool channel; closing...") + return + } + } +} + +// processPeerUpdate processes a PeerUpdate. For added peers, PeerStatusUp, we +// check if the reactor is running and if we've already started a tx broadcasting +// goroutine or not. If not, we start one for the newly added peer. For down or +// removed peers, we remove the peer from the mempool peer ID set and signal to +// stop the tx broadcasting goroutine. +func (r *Reactor) processPeerUpdate(peerUpdate p2p.PeerUpdate) { + r.Logger.Debug("received peer update", "peer", peerUpdate.NodeID, "status", peerUpdate.Status) + + r.mtx.Lock() + defer r.mtx.Unlock() + + switch peerUpdate.Status { + case p2p.PeerStatusUp: + // Do not allow starting new tx broadcast loops after reactor shutdown + // has been initiated. This can happen after we've manually closed all + // peer broadcast loops and closed r.closeCh, but the router still sends + // in-flight peer updates. + if !r.IsRunning() { + return + } + + if r.cfg.Broadcast { + // Check if we've already started a goroutine for this peer, if not we create + // a new done channel so we can explicitly close the goroutine if the peer + // is later removed, we increment the waitgroup so the reactor can stop + // safely, and finally start the goroutine to broadcast txs to that peer. + _, ok := r.peerRoutines[peerUpdate.NodeID] + if !ok { + closer := tmsync.NewCloser() + + r.peerRoutines[peerUpdate.NodeID] = closer + r.peerWG.Add(1) + + r.ids.ReserveForPeer(peerUpdate.NodeID) + + // start a broadcast routine ensuring all txs are forwarded to the peer + go r.broadcastTxRoutine(peerUpdate.NodeID, closer) + } + } + + case p2p.PeerStatusDown: + r.ids.Reclaim(peerUpdate.NodeID) + + // Check if we've started a tx broadcasting goroutine for this peer. + // If we have, we signal to terminate the goroutine via the channel's closure. + // This will internally decrement the peer waitgroup and remove the peer + // from the map of peer tx broadcasting goroutines. + closer, ok := r.peerRoutines[peerUpdate.NodeID] + if ok { + closer.Close() + } + } +} + +// processPeerUpdates initiates a blocking process where we listen for and handle +// PeerUpdate messages. When the reactor is stopped, we will catch the signal and +// close the p2p PeerUpdatesCh gracefully. +func (r *Reactor) processPeerUpdates() { + defer r.peerUpdates.Close() + + for { + select { + case peerUpdate := <-r.peerUpdates.Updates(): + r.processPeerUpdate(peerUpdate) + + case <-r.closeCh: + r.Logger.Debug("stopped listening on peer updates channel; closing...") + return + } + } +} + +func (r *Reactor) broadcastTxRoutine(peerID types.NodeID, closer *tmsync.Closer) { + peerMempoolID := r.ids.GetForPeer(peerID) + var nextGossipTx *clist.CElement + + // remove the peer ID from the map of routines and mark the waitgroup as done + defer func() { + r.mtx.Lock() + delete(r.peerRoutines, peerID) + r.mtx.Unlock() + + r.peerWG.Done() + + if e := recover(); e != nil { + r.observePanic(e) + r.Logger.Error( + "recovering from broadcasting mempool loop", + "err", e, + "stack", string(debug.Stack()), + ) + } + }() + + for { + if !r.IsRunning() { + return + } + + // This happens because the CElement we were looking at got garbage + // collected (removed). That is, .NextWait() returned nil. Go ahead and + // start from the beginning. + if nextGossipTx == nil { + select { + case <-r.mempool.WaitForNextTx(): // wait until a tx is available + if nextGossipTx = r.mempool.NextGossipTx(); nextGossipTx == nil { + continue + } + + case <-closer.Done(): + // The peer is marked for removal via a PeerUpdate as the doneCh was + // explicitly closed to signal we should exit. + return + + case <-r.closeCh: + // The reactor has signaled that we are stopped and thus we should + // implicitly exit this peer's goroutine. + return + } + } + + memTx := nextGossipTx.Value.(*WrappedTx) + + // NOTE: Transaction batching was disabled due to: + // https://github.com/tendermint/tendermint/issues/5796 + if ok := r.mempool.txStore.TxHasPeer(memTx.hash, peerMempoolID); !ok { + // Send the mempool tx to the corresponding peer. Note, the peer may be + // behind and thus would not be able to process the mempool tx correctly. + r.mempoolCh.Out <- p2p.Envelope{ + To: peerID, + Message: &protomem.Txs{ + Txs: [][]byte{memTx.tx}, + }, + } + r.Logger.Debug( + "gossiped tx to peer", + "tx", fmt.Sprintf("%X", memTx.tx.Hash()), + "peer", peerID, + ) + } + + select { + case <-nextGossipTx.NextWaitChan(): + nextGossipTx = nextGossipTx.Next() + + case <-closer.Done(): + // The peer is marked for removal via a PeerUpdate as the doneCh was + // explicitly closed to signal we should exit. + return + + case <-r.closeCh: + // The reactor has signaled that we are stopped and thus we should + // implicitly exit this peer's goroutine. + return + } + } +} diff --git a/mempool/v1/reactor_test.go b/mempool/v1/reactor_test.go new file mode 100644 index 000000000..0454ad9c5 --- /dev/null +++ b/mempool/v1/reactor_test.go @@ -0,0 +1,145 @@ +package v1 + +import ( + "os" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/abci/example/kvstore" + "github.com/tendermint/tendermint/config" + tmsync "github.com/tendermint/tendermint/internal/libs/sync" + "github.com/tendermint/tendermint/internal/mempool" + "github.com/tendermint/tendermint/internal/p2p" + "github.com/tendermint/tendermint/internal/p2p/p2ptest" + "github.com/tendermint/tendermint/libs/log" + protomem "github.com/tendermint/tendermint/proto/tendermint/mempool" + "github.com/tendermint/tendermint/types" +) + +type reactorTestSuite struct { + network *p2ptest.Network + logger log.Logger + + reactors map[types.NodeID]*Reactor + mempoolChannels map[types.NodeID]*p2p.Channel + mempools map[types.NodeID]*TxMempool + kvstores map[types.NodeID]*kvstore.Application + + peerChans map[types.NodeID]chan p2p.PeerUpdate + peerUpdates map[types.NodeID]*p2p.PeerUpdates + + nodes []types.NodeID +} + +func setupReactors(t *testing.T, numNodes int, chBuf uint) *reactorTestSuite { + t.Helper() + + cfg, err := config.ResetTestRoot(strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + + rts := &reactorTestSuite{ + logger: log.TestingLogger().With("testCase", t.Name()), + network: p2ptest.MakeNetwork(t, p2ptest.NetworkOptions{NumNodes: numNodes}), + reactors: make(map[types.NodeID]*Reactor, numNodes), + mempoolChannels: make(map[types.NodeID]*p2p.Channel, numNodes), + mempools: make(map[types.NodeID]*TxMempool, numNodes), + kvstores: make(map[types.NodeID]*kvstore.Application, numNodes), + peerChans: make(map[types.NodeID]chan p2p.PeerUpdate, numNodes), + peerUpdates: make(map[types.NodeID]*p2p.PeerUpdates, numNodes), + } + + chDesc := p2p.ChannelDescriptor{ID: byte(mempool.MempoolChannel)} + rts.mempoolChannels = rts.network.MakeChannelsNoCleanup(t, chDesc, new(protomem.Message), int(chBuf)) + + for nodeID := range rts.network.Nodes { + rts.kvstores[nodeID] = kvstore.NewApplication() + + mempool := setup(t, 0) + rts.mempools[nodeID] = mempool + + rts.peerChans[nodeID] = make(chan p2p.PeerUpdate) + rts.peerUpdates[nodeID] = p2p.NewPeerUpdates(rts.peerChans[nodeID], 1) + rts.network.Nodes[nodeID].PeerManager.Register(rts.peerUpdates[nodeID]) + + rts.reactors[nodeID] = NewReactor( + rts.logger.With("nodeID", nodeID), + cfg.Mempool, + mempool, + rts.mempoolChannels[nodeID], + rts.peerUpdates[nodeID], + ) + + rts.nodes = append(rts.nodes, nodeID) + + require.NoError(t, rts.reactors[nodeID].Start()) + require.True(t, rts.reactors[nodeID].IsRunning()) + } + + require.Len(t, rts.reactors, numNodes) + + t.Cleanup(func() { + for nodeID := range rts.reactors { + if rts.reactors[nodeID].IsRunning() { + require.NoError(t, rts.reactors[nodeID].Stop()) + require.False(t, rts.reactors[nodeID].IsRunning()) + } + } + }) + + return rts +} + +func (rts *reactorTestSuite) start(t *testing.T) { + t.Helper() + rts.network.Start(t) + require.Len(t, + rts.network.RandomNode().PeerManager.Peers(), + len(rts.nodes)-1, + "network does not have expected number of nodes") +} + +func TestReactorBroadcastDoesNotPanic(t *testing.T) { + numNodes := 2 + rts := setupReactors(t, numNodes, 0) + + observePanic := func(r interface{}) { + t.Fatal("panic detected in reactor") + } + + primary := rts.nodes[0] + secondary := rts.nodes[1] + primaryReactor := rts.reactors[primary] + primaryMempool := primaryReactor.mempool + secondaryReactor := rts.reactors[secondary] + + primaryReactor.observePanic = observePanic + secondaryReactor.observePanic = observePanic + + firstTx := &WrappedTx{} + primaryMempool.insertTx(firstTx) + + // run the router + rts.start(t) + + closer := tmsync.NewCloser() + primaryReactor.peerWG.Add(1) + go primaryReactor.broadcastTxRoutine(secondary, closer) + + wg := &sync.WaitGroup{} + for i := 0; i < 50; i++ { + next := &WrappedTx{} + wg.Add(1) + go func() { + defer wg.Done() + primaryMempool.insertTx(next) + }() + } + + err := primaryReactor.Stop() + require.NoError(t, err) + primaryReactor.peerWG.Wait() + wg.Wait() +} diff --git a/mempool/v1/tx.go b/mempool/v1/tx.go new file mode 100644 index 000000000..c5b7ca82f --- /dev/null +++ b/mempool/v1/tx.go @@ -0,0 +1,281 @@ +package v1 + +import ( + "sort" + "time" + + "github.com/tendermint/tendermint/internal/libs/clist" + tmsync "github.com/tendermint/tendermint/internal/libs/sync" + "github.com/tendermint/tendermint/types" +) + +// WrappedTx defines a wrapper around a raw transaction with additional metadata +// that is used for indexing. +type WrappedTx struct { + // tx represents the raw binary transaction data + tx types.Tx + + // hash defines the transaction hash and the primary key used in the mempool + hash types.TxKey + + // height defines the height at which the transaction was validated at + height int64 + + // gasWanted defines the amount of gas the transaction sender requires + gasWanted int64 + + // priority defines the transaction's priority as specified by the application + // in the ResponseCheckTx response. + priority int64 + + // sender defines the transaction's sender as specified by the application in + // the ResponseCheckTx response. + sender string + + // timestamp is the time at which the node first received the transaction from + // a peer. It is used as a second dimension is prioritizing transactions when + // two transactions have the same priority. + timestamp time.Time + + // peers records a mapping of all peers that sent a given transaction + peers map[uint16]struct{} + + // heapIndex defines the index of the item in the heap + heapIndex int + + // gossipEl references the linked-list element in the gossip index + gossipEl *clist.CElement + + // removed marks the transaction as removed from the mempool. This is set + // during RemoveTx and is needed due to the fact that a given existing + // transaction in the mempool can be evicted when it is simultaneously having + // a reCheckTx callback executed. + removed bool +} + +func (wtx *WrappedTx) Size() int { + return len(wtx.tx) +} + +// TxStore implements a thread-safe mapping of valid transaction(s). +// +// NOTE: +// - Concurrent read-only access to a *WrappedTx object is OK. However, mutative +// access is not allowed. Regardless, it is not expected for the mempool to +// need mutative access. +type TxStore struct { + mtx tmsync.RWMutex + hashTxs map[types.TxKey]*WrappedTx // primary index + senderTxs map[string]*WrappedTx // sender is defined by the ABCI application +} + +func NewTxStore() *TxStore { + return &TxStore{ + senderTxs: make(map[string]*WrappedTx), + hashTxs: make(map[types.TxKey]*WrappedTx), + } +} + +// Size returns the total number of transactions in the store. +func (txs *TxStore) Size() int { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + return len(txs.hashTxs) +} + +// GetAllTxs returns all the transactions currently in the store. +func (txs *TxStore) GetAllTxs() []*WrappedTx { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + wTxs := make([]*WrappedTx, len(txs.hashTxs)) + i := 0 + for _, wtx := range txs.hashTxs { + wTxs[i] = wtx + i++ + } + + return wTxs +} + +// GetTxBySender returns a *WrappedTx by the transaction's sender property +// defined by the ABCI application. +func (txs *TxStore) GetTxBySender(sender string) *WrappedTx { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + return txs.senderTxs[sender] +} + +// GetTxByHash returns a *WrappedTx by the transaction's hash. +func (txs *TxStore) GetTxByHash(hash types.TxKey) *WrappedTx { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + return txs.hashTxs[hash] +} + +// IsTxRemoved returns true if a transaction by hash is marked as removed and +// false otherwise. +func (txs *TxStore) IsTxRemoved(hash types.TxKey) bool { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + wtx, ok := txs.hashTxs[hash] + if ok { + return wtx.removed + } + + return false +} + +// SetTx stores a *WrappedTx by it's hash. If the transaction also contains a +// non-empty sender, we additionally store the transaction by the sender as +// defined by the ABCI application. +func (txs *TxStore) SetTx(wtx *WrappedTx) { + txs.mtx.Lock() + defer txs.mtx.Unlock() + + if len(wtx.sender) > 0 { + txs.senderTxs[wtx.sender] = wtx + } + + txs.hashTxs[wtx.tx.Key()] = wtx +} + +// RemoveTx removes a *WrappedTx from the transaction store. It deletes all +// indexes of the transaction. +func (txs *TxStore) RemoveTx(wtx *WrappedTx) { + txs.mtx.Lock() + defer txs.mtx.Unlock() + + if len(wtx.sender) > 0 { + delete(txs.senderTxs, wtx.sender) + } + + delete(txs.hashTxs, wtx.tx.Key()) + wtx.removed = true +} + +// TxHasPeer returns true if a transaction by hash has a given peer ID and false +// otherwise. If the transaction does not exist, false is returned. +func (txs *TxStore) TxHasPeer(hash types.TxKey, peerID uint16) bool { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + wtx := txs.hashTxs[hash] + if wtx == nil { + return false + } + + _, ok := wtx.peers[peerID] + return ok +} + +// GetOrSetPeerByTxHash looks up a WrappedTx by transaction hash and adds the +// given peerID to the WrappedTx's set of peers that sent us this transaction. +// We return true if we've already recorded the given peer for this transaction +// and false otherwise. If the transaction does not exist by hash, we return +// (nil, false). +func (txs *TxStore) GetOrSetPeerByTxHash(hash types.TxKey, peerID uint16) (*WrappedTx, bool) { + txs.mtx.Lock() + defer txs.mtx.Unlock() + + wtx := txs.hashTxs[hash] + if wtx == nil { + return nil, false + } + + if wtx.peers == nil { + wtx.peers = make(map[uint16]struct{}) + } + + if _, ok := wtx.peers[peerID]; ok { + return wtx, true + } + + wtx.peers[peerID] = struct{}{} + return wtx, false +} + +// WrappedTxList implements a thread-safe list of *WrappedTx objects that can be +// used to build generic transaction indexes in the mempool. It accepts a +// comparator function, less(a, b *WrappedTx) bool, that compares two WrappedTx +// references which is used during Insert in order to determine sorted order. If +// less returns true, a <= b. +type WrappedTxList struct { + mtx tmsync.RWMutex + txs []*WrappedTx + less func(*WrappedTx, *WrappedTx) bool +} + +func NewWrappedTxList(less func(*WrappedTx, *WrappedTx) bool) *WrappedTxList { + return &WrappedTxList{ + txs: make([]*WrappedTx, 0), + less: less, + } +} + +// Size returns the number of WrappedTx objects in the list. +func (wtl *WrappedTxList) Size() int { + wtl.mtx.RLock() + defer wtl.mtx.RUnlock() + + return len(wtl.txs) +} + +// Reset resets the list of transactions to an empty list. +func (wtl *WrappedTxList) Reset() { + wtl.mtx.Lock() + defer wtl.mtx.Unlock() + + wtl.txs = make([]*WrappedTx, 0) +} + +// Insert inserts a WrappedTx reference into the sorted list based on the list's +// comparator function. +func (wtl *WrappedTxList) Insert(wtx *WrappedTx) { + wtl.mtx.Lock() + defer wtl.mtx.Unlock() + + i := sort.Search(len(wtl.txs), func(i int) bool { + return wtl.less(wtl.txs[i], wtx) + }) + + if i == len(wtl.txs) { + // insert at the end + wtl.txs = append(wtl.txs, wtx) + return + } + + // Make space for the inserted element by shifting values at the insertion + // index up one index. + // + // NOTE: The call to append does not allocate memory when cap(wtl.txs) > len(wtl.txs). + wtl.txs = append(wtl.txs[:i+1], wtl.txs[i:]...) + wtl.txs[i] = wtx +} + +// Remove attempts to remove a WrappedTx from the sorted list. +func (wtl *WrappedTxList) Remove(wtx *WrappedTx) { + wtl.mtx.Lock() + defer wtl.mtx.Unlock() + + i := sort.Search(len(wtl.txs), func(i int) bool { + return wtl.less(wtl.txs[i], wtx) + }) + + // Since the list is sorted, we evaluate all elements starting at i. Note, if + // the element does not exist, we may potentially evaluate the entire remainder + // of the list. However, a caller should not be expected to call Remove with a + // non-existing element. + for i < len(wtl.txs) { + if wtl.txs[i] == wtx { + wtl.txs = append(wtl.txs[:i], wtl.txs[i+1:]...) + return + } + + i++ + } +} diff --git a/mempool/v1/tx_test.go b/mempool/v1/tx_test.go new file mode 100644 index 000000000..fb4beafab --- /dev/null +++ b/mempool/v1/tx_test.go @@ -0,0 +1,230 @@ +package v1 + +import ( + "fmt" + "math/rand" + "sort" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/types" +) + +func TestTxStore_GetTxBySender(t *testing.T) { + txs := NewTxStore() + wtx := &WrappedTx{ + tx: []byte("test_tx"), + sender: "foo", + priority: 1, + timestamp: time.Now(), + } + + res := txs.GetTxBySender(wtx.sender) + require.Nil(t, res) + + txs.SetTx(wtx) + + res = txs.GetTxBySender(wtx.sender) + require.NotNil(t, res) + require.Equal(t, wtx, res) +} + +func TestTxStore_GetTxByHash(t *testing.T) { + txs := NewTxStore() + wtx := &WrappedTx{ + tx: []byte("test_tx"), + sender: "foo", + priority: 1, + timestamp: time.Now(), + } + + key := wtx.tx.Key() + res := txs.GetTxByHash(key) + require.Nil(t, res) + + txs.SetTx(wtx) + + res = txs.GetTxByHash(key) + require.NotNil(t, res) + require.Equal(t, wtx, res) +} + +func TestTxStore_SetTx(t *testing.T) { + txs := NewTxStore() + wtx := &WrappedTx{ + tx: []byte("test_tx"), + priority: 1, + timestamp: time.Now(), + } + + key := wtx.tx.Key() + txs.SetTx(wtx) + + res := txs.GetTxByHash(key) + require.NotNil(t, res) + require.Equal(t, wtx, res) + + wtx.sender = "foo" + txs.SetTx(wtx) + + res = txs.GetTxByHash(key) + require.NotNil(t, res) + require.Equal(t, wtx, res) +} + +func TestTxStore_GetOrSetPeerByTxHash(t *testing.T) { + txs := NewTxStore() + wtx := &WrappedTx{ + tx: []byte("test_tx"), + priority: 1, + timestamp: time.Now(), + } + + key := wtx.tx.Key() + txs.SetTx(wtx) + + res, ok := txs.GetOrSetPeerByTxHash(types.Tx([]byte("test_tx_2")).Key(), 15) + require.Nil(t, res) + require.False(t, ok) + + res, ok = txs.GetOrSetPeerByTxHash(key, 15) + require.NotNil(t, res) + require.False(t, ok) + + res, ok = txs.GetOrSetPeerByTxHash(key, 15) + require.NotNil(t, res) + require.True(t, ok) + + require.True(t, txs.TxHasPeer(key, 15)) + require.False(t, txs.TxHasPeer(key, 16)) +} + +func TestTxStore_RemoveTx(t *testing.T) { + txs := NewTxStore() + wtx := &WrappedTx{ + tx: []byte("test_tx"), + priority: 1, + timestamp: time.Now(), + } + + txs.SetTx(wtx) + + key := wtx.tx.Key() + res := txs.GetTxByHash(key) + require.NotNil(t, res) + + txs.RemoveTx(res) + + res = txs.GetTxByHash(key) + require.Nil(t, res) +} + +func TestTxStore_Size(t *testing.T) { + txStore := NewTxStore() + numTxs := 1000 + + for i := 0; i < numTxs; i++ { + txStore.SetTx(&WrappedTx{ + tx: []byte(fmt.Sprintf("test_tx_%d", i)), + priority: int64(i), + timestamp: time.Now(), + }) + } + + require.Equal(t, numTxs, txStore.Size()) +} + +func TestWrappedTxList_Reset(t *testing.T) { + list := NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { + return wtx1.height >= wtx2.height + }) + + require.Zero(t, list.Size()) + + for i := 0; i < 100; i++ { + list.Insert(&WrappedTx{height: int64(i)}) + } + + require.Equal(t, 100, list.Size()) + + list.Reset() + require.Zero(t, list.Size()) +} + +func TestWrappedTxList_Insert(t *testing.T) { + list := NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { + return wtx1.height >= wtx2.height + }) + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + var expected []int + for i := 0; i < 100; i++ { + height := rng.Int63n(10000) + expected = append(expected, int(height)) + list.Insert(&WrappedTx{height: height}) + + if i%10 == 0 { + list.Insert(&WrappedTx{height: height}) + expected = append(expected, int(height)) + } + } + + got := make([]int, list.Size()) + for i, wtx := range list.txs { + got[i] = int(wtx.height) + } + + sort.Ints(expected) + require.Equal(t, expected, got) +} + +func TestWrappedTxList_Remove(t *testing.T) { + list := NewWrappedTxList(func(wtx1, wtx2 *WrappedTx) bool { + return wtx1.height >= wtx2.height + }) + + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + var txs []*WrappedTx + for i := 0; i < 100; i++ { + height := rng.Int63n(10000) + tx := &WrappedTx{height: height} + + txs = append(txs, tx) + list.Insert(tx) + + if i%10 == 0 { + tx = &WrappedTx{height: height} + list.Insert(tx) + txs = append(txs, tx) + } + } + + // remove a tx that does not exist + list.Remove(&WrappedTx{height: 20000}) + + // remove a tx that exists (by height) but not referenced + list.Remove(&WrappedTx{height: txs[0].height}) + + // remove a few existing txs + for i := 0; i < 25; i++ { + j := rng.Intn(len(txs)) + list.Remove(txs[j]) + txs = append(txs[:j], txs[j+1:]...) + } + + expected := make([]int, len(txs)) + for i, tx := range txs { + expected[i] = int(tx.height) + } + + got := make([]int, list.Size()) + for i, wtx := range list.txs { + got[i] = int(wtx.height) + } + + sort.Ints(expected) + require.Equal(t, expected, got) +} diff --git a/mempool_bak/bench_test.go b/mempool_bak/bench_test.go new file mode 100644 index 000000000..1c26999d1 --- /dev/null +++ b/mempool_bak/bench_test.go @@ -0,0 +1,41 @@ +package mempool + +import ( + "encoding/binary" + "testing" +) + +func BenchmarkCacheInsertTime(b *testing.B) { + cache := NewLRUTxCache(b.N) + + txs := make([][]byte, b.N) + for i := 0; i < b.N; i++ { + txs[i] = make([]byte, 8) + binary.BigEndian.PutUint64(txs[i], uint64(i)) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + cache.Push(txs[i]) + } +} + +// This benchmark is probably skewed, since we actually will be removing +// txs in parallel, which may cause some overhead due to mutex locking. +func BenchmarkCacheRemoveTime(b *testing.B) { + cache := NewLRUTxCache(b.N) + + txs := make([][]byte, b.N) + for i := 0; i < b.N; i++ { + txs[i] = make([]byte, 8) + binary.BigEndian.PutUint64(txs[i], uint64(i)) + cache.Push(txs[i]) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + cache.Remove(txs[i]) + } +} diff --git a/mempool_bak/cache.go b/mempool_bak/cache.go new file mode 100644 index 000000000..cdaefe762 --- /dev/null +++ b/mempool_bak/cache.go @@ -0,0 +1,107 @@ +package mempool + +import ( + "container/list" + + tmsync "github.com/tendermint/tendermint/libs/sync" + "github.com/tendermint/tendermint/types" +) + +// TxCache defines an interface for raw transaction caching in a mempool. +// Currently, a TxCache does not allow direct reading or getting of transaction +// values. A TxCache is used primarily to push transactions and removing +// transactions. Pushing via Push returns a boolean telling the caller if the +// transaction already exists in the cache or not. +type TxCache interface { + // Reset resets the cache to an empty state. + Reset() + + // Push adds the given raw transaction to the cache and returns true if it was + // newly added. Otherwise, it returns false. + Push(tx types.Tx) bool + + // Remove removes the given raw transaction from the cache. + Remove(tx types.Tx) +} + +var _ TxCache = (*LRUTxCache)(nil) + +// LRUTxCache maintains a thread-safe LRU cache of raw transactions. The cache +// only stores the hash of the raw transaction. +type LRUTxCache struct { + mtx tmsync.Mutex + size int + cacheMap map[types.TxKey]*list.Element + list *list.List +} + +func NewLRUTxCache(cacheSize int) *LRUTxCache { + return &LRUTxCache{ + size: cacheSize, + cacheMap: make(map[types.TxKey]*list.Element, cacheSize), + list: list.New(), + } +} + +// GetList returns the underlying linked-list that backs the LRU cache. Note, +// this should be used for testing purposes only! +func (c *LRUTxCache) GetList() *list.List { + return c.list +} + +func (c *LRUTxCache) Reset() { + c.mtx.Lock() + defer c.mtx.Unlock() + + c.cacheMap = make(map[types.TxKey]*list.Element, c.size) + c.list.Init() +} + +func (c *LRUTxCache) Push(tx types.Tx) bool { + c.mtx.Lock() + defer c.mtx.Unlock() + + key := tx.Key() + + moved, ok := c.cacheMap[key] + if ok { + c.list.MoveToBack(moved) + return false + } + + if c.list.Len() >= c.size { + front := c.list.Front() + if front != nil { + frontKey := front.Value.(types.TxKey) + delete(c.cacheMap, frontKey) + c.list.Remove(front) + } + } + + e := c.list.PushBack(key) + c.cacheMap[key] = e + + return true +} + +func (c *LRUTxCache) Remove(tx types.Tx) { + c.mtx.Lock() + defer c.mtx.Unlock() + + key := tx.Key() + e := c.cacheMap[key] + delete(c.cacheMap, key) + + if e != nil { + c.list.Remove(e) + } +} + +// NopTxCache defines a no-op raw transaction cache. +type NopTxCache struct{} + +var _ TxCache = (*NopTxCache)(nil) + +func (NopTxCache) Reset() {} +func (NopTxCache) Push(types.Tx) bool { return true } +func (NopTxCache) Remove(types.Tx) {} diff --git a/mempool_bak/cache_test.go b/mempool_bak/cache_test.go new file mode 100644 index 000000000..44b2beb01 --- /dev/null +++ b/mempool_bak/cache_test.go @@ -0,0 +1,35 @@ +package mempool + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCacheRemove(t *testing.T) { + cache := NewLRUTxCache(100) + numTxs := 10 + + txs := make([][]byte, numTxs) + for i := 0; i < numTxs; i++ { + // probability of collision is 2**-256 + txBytes := make([]byte, 32) + _, err := rand.Read(txBytes) + require.NoError(t, err) + + txs[i] = txBytes + cache.Push(txBytes) + + // make sure its added to both the linked list and the map + require.Equal(t, i+1, len(cache.cacheMap)) + require.Equal(t, i+1, cache.list.Len()) + } + + for i := 0; i < numTxs; i++ { + cache.Remove(txs[i]) + // make sure its removed from both the map and the linked list + require.Equal(t, numTxs-(i+1), len(cache.cacheMap)) + require.Equal(t, numTxs-(i+1), cache.list.Len()) + } +} diff --git a/mempool/clist_mempool.go b/mempool_bak/clist_mempool.go similarity index 100% rename from mempool/clist_mempool.go rename to mempool_bak/clist_mempool.go diff --git a/mempool/clist_mempool_test.go b/mempool_bak/clist_mempool_test.go similarity index 100% rename from mempool/clist_mempool_test.go rename to mempool_bak/clist_mempool_test.go diff --git a/mempool/doc.go b/mempool_bak/doc.go similarity index 100% rename from mempool/doc.go rename to mempool_bak/doc.go diff --git a/mempool/errors.go b/mempool_bak/errors.go similarity index 100% rename from mempool/errors.go rename to mempool_bak/errors.go diff --git a/mempool_bak/mempool.go b/mempool_bak/mempool.go new file mode 100644 index 000000000..eece0cf1b --- /dev/null +++ b/mempool_bak/mempool.go @@ -0,0 +1,127 @@ +package mempool + +import ( + "fmt" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +// Mempool defines the mempool interface. +// +// Updates to the mempool need to be synchronized with committing a block so +// apps can reset their transient state on Commit. +type Mempool interface { + // CheckTx executes a new transaction against the application to determine + // its validity and whether it should be added to the mempool. + CheckTx(tx types.Tx, callback func(*abci.Response), txInfo TxInfo) error + + // ReapMaxBytesMaxGas reaps transactions from the mempool up to maxBytes + // bytes total with the condition that the total gasWanted must be less than + // maxGas. + // If both maxes are negative, there is no cap on the size of all returned + // transactions (~ all available transactions). + ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs + + // ReapMaxTxs reaps up to max transactions from the mempool. + // If max is negative, there is no cap on the size of all returned + // transactions (~ all available transactions). + ReapMaxTxs(max int) types.Txs + + // Lock locks the mempool. The consensus must be able to hold lock to safely update. + Lock() + + // Unlock unlocks the mempool. + Unlock() + + // Update informs the mempool that the given txs were committed and can be discarded. + // NOTE: this should be called *after* block is committed by consensus. + // NOTE: Lock/Unlock must be managed by caller + Update( + blockHeight int64, + blockTxs types.Txs, + deliverTxResponses []*abci.ResponseDeliverTx, + newPreFn PreCheckFunc, + newPostFn PostCheckFunc, + ) error + + // FlushAppConn flushes the mempool connection to ensure async reqResCb calls are + // done. E.g. from CheckTx. + // NOTE: Lock/Unlock must be managed by caller + FlushAppConn() error + + // Flush removes all transactions from the mempool and cache + Flush() + + // TxsAvailable returns a channel which fires once for every height, + // and only when transactions are available in the mempool. + // NOTE: the returned channel may be nil if EnableTxsAvailable was not called. + TxsAvailable() <-chan struct{} + + // EnableTxsAvailable initializes the TxsAvailable channel, ensuring it will + // trigger once every height when transactions are available. + EnableTxsAvailable() + + // Size returns the number of transactions in the mempool. + Size() int + + // SizeBytes returns the total size of all txs in the mempool. + SizeBytes() int64 +} + +//-------------------------------------------------------------------------------- + +// PreCheckFunc is an optional filter executed before CheckTx and rejects +// transaction if false is returned. An example would be to ensure that a +// transaction doesn't exceeded the block size. +type PreCheckFunc func(types.Tx) error + +// PostCheckFunc is an optional filter executed after CheckTx and rejects +// transaction if false is returned. An example would be to ensure a +// transaction doesn't require more gas than available for the block. +type PostCheckFunc func(types.Tx, *abci.ResponseCheckTx) error + +// TxInfo are parameters that get passed when attempting to add a tx to the +// mempool. +type TxInfo struct { + // SenderID is the internal peer ID used in the mempool to identify the + // sender, storing 2 bytes with each tx instead of 20 bytes for the p2p.ID. + SenderID uint16 + // SenderP2PID is the actual p2p.ID of the sender, used e.g. for logging. + SenderP2PID p2p.ID +} + +//-------------------------------------------------------------------------------- + +// PreCheckMaxBytes checks that the size of the transaction is smaller or equal to the expected maxBytes. +func PreCheckMaxBytes(maxBytes int64) PreCheckFunc { + return func(tx types.Tx) error { + txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}) + + if txSize > maxBytes { + return fmt.Errorf("tx size is too big: %d, max: %d", + txSize, maxBytes) + } + return nil + } +} + +// PostCheckMaxGas checks that the wanted gas is smaller or equal to the passed +// maxGas. Returns nil if maxGas is -1. +func PostCheckMaxGas(maxGas int64) PostCheckFunc { + return func(tx types.Tx, res *abci.ResponseCheckTx) error { + if maxGas == -1 { + return nil + } + if res.GasWanted < 0 { + return fmt.Errorf("gas wanted %d is negative", + res.GasWanted) + } + if res.GasWanted > maxGas { + return fmt.Errorf("gas wanted %d is greater than max gas %d", + res.GasWanted, maxGas) + } + return nil + } +} diff --git a/mempool_bak/metrics.go b/mempool_bak/metrics.go new file mode 100644 index 000000000..5d3022e80 --- /dev/null +++ b/mempool_bak/metrics.go @@ -0,0 +1,108 @@ +package mempool + +import ( + "github.com/go-kit/kit/metrics" + "github.com/go-kit/kit/metrics/discard" + "github.com/go-kit/kit/metrics/prometheus" + stdprometheus "github.com/prometheus/client_golang/prometheus" +) + +const ( + // MetricsSubsystem is a subsystem shared by all metrics exposed by this + // package. + MetricsSubsystem = "mempool" +) + +// Metrics contains metrics exposed by this package. +// see MetricsProvider for descriptions. +type Metrics struct { + // Size of the mempool. + Size metrics.Gauge + + // Histogram of transaction sizes, in bytes. + TxSizeBytes metrics.Histogram + + // Number of failed transactions. + FailedTxs metrics.Counter + + // RejectedTxs defines the number of rejected transactions. These are + // transactions that passed CheckTx but failed to make it into the mempool + // due to resource limits, e.g. mempool is full and no lower priority + // transactions exist in the mempool. + RejectedTxs metrics.Counter + + // EvictedTxs defines the number of evicted transactions. These are valid + // transactions that passed CheckTx and existed in the mempool but were later + // evicted to make room for higher priority valid transactions that passed + // CheckTx. + EvictedTxs metrics.Counter + + // Number of times transactions are rechecked in the mempool. + RecheckTimes metrics.Counter +} + +// PrometheusMetrics returns Metrics build using Prometheus client library. +// Optionally, labels can be provided along with their values ("foo", +// "fooValue"). +func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { + labels := []string{} + for i := 0; i < len(labelsAndValues); i += 2 { + labels = append(labels, labelsAndValues[i]) + } + return &Metrics{ + Size: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "size", + Help: "Size of the mempool (number of uncommitted transactions).", + }, labels).With(labelsAndValues...), + + TxSizeBytes: prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "tx_size_bytes", + Help: "Transaction sizes in bytes.", + Buckets: stdprometheus.ExponentialBuckets(1, 3, 17), + }, labels).With(labelsAndValues...), + + FailedTxs: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "failed_txs", + Help: "Number of failed transactions.", + }, labels).With(labelsAndValues...), + + RejectedTxs: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "rejected_txs", + Help: "Number of rejected transactions.", + }, labels).With(labelsAndValues...), + + EvictedTxs: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "evicted_txs", + Help: "Number of evicted transactions.", + }, labels).With(labelsAndValues...), + + RecheckTimes: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "recheck_times", + Help: "Number of times transactions are rechecked in the mempool.", + }, labels).With(labelsAndValues...), + } +} + +// NopMetrics returns no-op Metrics. +func NopMetrics() *Metrics { + return &Metrics{ + Size: discard.NewGauge(), + TxSizeBytes: discard.NewHistogram(), + FailedTxs: discard.NewCounter(), + RejectedTxs: discard.NewCounter(), + EvictedTxs: discard.NewCounter(), + RecheckTimes: discard.NewCounter(), + } +} diff --git a/mempool_bak/mock/mempool.go b/mempool_bak/mock/mempool.go new file mode 100644 index 000000000..cd8df2198 --- /dev/null +++ b/mempool_bak/mock/mempool.go @@ -0,0 +1,43 @@ +package mock + +import ( + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/clist" + mempl "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/types" +) + +// Mempool is an empty implementation of a Mempool, useful for testing. +type Mempool struct{} + +var _ mempl.Mempool = Mempool{} + +func (Mempool) Lock() {} +func (Mempool) Unlock() {} +func (Mempool) Size() int { return 0 } +func (Mempool) SizeBytes() int64 { return 0 } +func (Mempool) CheckTx(_ types.Tx, _ func(*abci.Response), _ mempl.TxInfo) error { + return nil +} +func (Mempool) ReapMaxBytesMaxGas(_, _ int64) types.Txs { return types.Txs{} } +func (Mempool) ReapMaxTxs(n int) types.Txs { return types.Txs{} } +func (Mempool) Update( + _ int64, + _ types.Txs, + _ []*abci.ResponseDeliverTx, + _ mempl.PreCheckFunc, + _ mempl.PostCheckFunc, +) error { + return nil +} +func (Mempool) Flush() {} +func (Mempool) FlushAppConn() error { return nil } +func (Mempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) } +func (Mempool) EnableTxsAvailable() {} +func (Mempool) TxsBytes() int64 { return 0 } + +func (Mempool) TxsFront() *clist.CElement { return nil } +func (Mempool) TxsWaitChan() <-chan struct{} { return nil } + +func (Mempool) InitWAL() error { return nil } +func (Mempool) CloseWAL() {} diff --git a/mempool/priority_queue.go b/mempool_bak/priority_queue.go similarity index 100% rename from mempool/priority_queue.go rename to mempool_bak/priority_queue.go diff --git a/mempool/reactor.go b/mempool_bak/reactor.go similarity index 100% rename from mempool/reactor.go rename to mempool_bak/reactor.go diff --git a/mempool/reactor_test.go b/mempool_bak/reactor_test.go similarity index 100% rename from mempool/reactor_test.go rename to mempool_bak/reactor_test.go diff --git a/mempool_bak/tx.go b/mempool_bak/tx.go new file mode 100644 index 000000000..f8d02f0e9 --- /dev/null +++ b/mempool_bak/tx.go @@ -0,0 +1,281 @@ +package mempool + +import ( + "sort" + "time" + + "github.com/tendermint/tendermint/libs/clist" + tmsync "github.com/tendermint/tendermint/libs/sync" + "github.com/tendermint/tendermint/types" +) + +// WrappedTx defines a wrapper around a raw transaction with additional metadata +// that is used for indexing. +type WrappedTx struct { + // tx represents the raw binary transaction data + tx types.Tx + + // hash defines the transaction hash and the primary key used in the mempool + hash types.TxKey + + // height defines the height at which the transaction was validated at + height int64 + + // gasWanted defines the amount of gas the transaction sender requires + gasWanted int64 + + // priority defines the transaction's priority as specified by the application + // in the ResponseCheckTx response. + priority int64 + + // sender defines the transaction's sender as specified by the application in + // the ResponseCheckTx response. + sender string + + // timestamp is the time at which the node first received the transaction from + // a peer. It is used as a second dimension is prioritizing transactions when + // two transactions have the same priority. + timestamp time.Time + + // peers records a mapping of all peers that sent a given transaction + peers map[uint16]struct{} + + // heapIndex defines the index of the item in the heap + heapIndex int + + // gossipEl references the linked-list element in the gossip index + gossipEl *clist.CElement + + // removed marks the transaction as removed from the mempool. This is set + // during RemoveTx and is needed due to the fact that a given existing + // transaction in the mempool can be evicted when it is simultaneously having + // a reCheckTx callback executed. + removed bool +} + +func (wtx *WrappedTx) Size() int { + return len(wtx.tx) +} + +// TxStore implements a thread-safe mapping of valid transaction(s). +// +// NOTE: +// - Concurrent read-only access to a *WrappedTx object is OK. However, mutative +// access is not allowed. Regardless, it is not expected for the mempool to +// need mutative access. +type TxStore struct { + mtx tmsync.RWMutex + hashTxs map[types.TxKey]*WrappedTx // primary index + senderTxs map[string]*WrappedTx // sender is defined by the ABCI application +} + +func NewTxStore() *TxStore { + return &TxStore{ + senderTxs: make(map[string]*WrappedTx), + hashTxs: make(map[types.TxKey]*WrappedTx), + } +} + +// Size returns the total number of transactions in the store. +func (txs *TxStore) Size() int { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + return len(txs.hashTxs) +} + +// GetAllTxs returns all the transactions currently in the store. +func (txs *TxStore) GetAllTxs() []*WrappedTx { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + wTxs := make([]*WrappedTx, len(txs.hashTxs)) + i := 0 + for _, wtx := range txs.hashTxs { + wTxs[i] = wtx + i++ + } + + return wTxs +} + +// GetTxBySender returns a *WrappedTx by the transaction's sender property +// defined by the ABCI application. +func (txs *TxStore) GetTxBySender(sender string) *WrappedTx { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + return txs.senderTxs[sender] +} + +// GetTxByHash returns a *WrappedTx by the transaction's hash. +func (txs *TxStore) GetTxByHash(hash types.TxKey) *WrappedTx { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + return txs.hashTxs[hash] +} + +// IsTxRemoved returns true if a transaction by hash is marked as removed and +// false otherwise. +func (txs *TxStore) IsTxRemoved(hash types.TxKey) bool { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + wtx, ok := txs.hashTxs[hash] + if ok { + return wtx.removed + } + + return false +} + +// SetTx stores a *WrappedTx by it's hash. If the transaction also contains a +// non-empty sender, we additionally store the transaction by the sender as +// defined by the ABCI application. +func (txs *TxStore) SetTx(wtx *WrappedTx) { + txs.mtx.Lock() + defer txs.mtx.Unlock() + + if len(wtx.sender) > 0 { + txs.senderTxs[wtx.sender] = wtx + } + + txs.hashTxs[wtx.tx.Key()] = wtx +} + +// RemoveTx removes a *WrappedTx from the transaction store. It deletes all +// indexes of the transaction. +func (txs *TxStore) RemoveTx(wtx *WrappedTx) { + txs.mtx.Lock() + defer txs.mtx.Unlock() + + if len(wtx.sender) > 0 { + delete(txs.senderTxs, wtx.sender) + } + + delete(txs.hashTxs, wtx.tx.Key()) + wtx.removed = true +} + +// TxHasPeer returns true if a transaction by hash has a given peer ID and false +// otherwise. If the transaction does not exist, false is returned. +func (txs *TxStore) TxHasPeer(hash types.TxKey, peerID uint16) bool { + txs.mtx.RLock() + defer txs.mtx.RUnlock() + + wtx := txs.hashTxs[hash] + if wtx == nil { + return false + } + + _, ok := wtx.peers[peerID] + return ok +} + +// GetOrSetPeerByTxHash looks up a WrappedTx by transaction hash and adds the +// given peerID to the WrappedTx's set of peers that sent us this transaction. +// We return true if we've already recorded the given peer for this transaction +// and false otherwise. If the transaction does not exist by hash, we return +// (nil, false). +func (txs *TxStore) GetOrSetPeerByTxHash(hash types.TxKey, peerID uint16) (*WrappedTx, bool) { + txs.mtx.Lock() + defer txs.mtx.Unlock() + + wtx := txs.hashTxs[hash] + if wtx == nil { + return nil, false + } + + if wtx.peers == nil { + wtx.peers = make(map[uint16]struct{}) + } + + if _, ok := wtx.peers[peerID]; ok { + return wtx, true + } + + wtx.peers[peerID] = struct{}{} + return wtx, false +} + +// WrappedTxList implements a thread-safe list of *WrappedTx objects that can be +// used to build generic transaction indexes in the mempool. It accepts a +// comparator function, less(a, b *WrappedTx) bool, that compares two WrappedTx +// references which is used during Insert in order to determine sorted order. If +// less returns true, a <= b. +type WrappedTxList struct { + mtx tmsync.RWMutex + txs []*WrappedTx + less func(*WrappedTx, *WrappedTx) bool +} + +func NewWrappedTxList(less func(*WrappedTx, *WrappedTx) bool) *WrappedTxList { + return &WrappedTxList{ + txs: make([]*WrappedTx, 0), + less: less, + } +} + +// Size returns the number of WrappedTx objects in the list. +func (wtl *WrappedTxList) Size() int { + wtl.mtx.RLock() + defer wtl.mtx.RUnlock() + + return len(wtl.txs) +} + +// Reset resets the list of transactions to an empty list. +func (wtl *WrappedTxList) Reset() { + wtl.mtx.Lock() + defer wtl.mtx.Unlock() + + wtl.txs = make([]*WrappedTx, 0) +} + +// Insert inserts a WrappedTx reference into the sorted list based on the list's +// comparator function. +func (wtl *WrappedTxList) Insert(wtx *WrappedTx) { + wtl.mtx.Lock() + defer wtl.mtx.Unlock() + + i := sort.Search(len(wtl.txs), func(i int) bool { + return wtl.less(wtl.txs[i], wtx) + }) + + if i == len(wtl.txs) { + // insert at the end + wtl.txs = append(wtl.txs, wtx) + return + } + + // Make space for the inserted element by shifting values at the insertion + // index up one index. + // + // NOTE: The call to append does not allocate memory when cap(wtl.txs) > len(wtl.txs). + wtl.txs = append(wtl.txs[:i+1], wtl.txs[i:]...) + wtl.txs[i] = wtx +} + +// Remove attempts to remove a WrappedTx from the sorted list. +func (wtl *WrappedTxList) Remove(wtx *WrappedTx) { + wtl.mtx.Lock() + defer wtl.mtx.Unlock() + + i := sort.Search(len(wtl.txs), func(i int) bool { + return wtl.less(wtl.txs[i], wtx) + }) + + // Since the list is sorted, we evaluate all elements starting at i. Note, if + // the element does not exist, we may potentially evaluate the entire remainder + // of the list. However, a caller should not be expected to call Remove with a + // non-existing element. + for i < len(wtl.txs) { + if wtl.txs[i] == wtx { + wtl.txs = append(wtl.txs[:i], wtl.txs[i+1:]...) + return + } + + i++ + } +}