mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-19 14:34:17 +00:00
updates
This commit is contained in:
@@ -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
|
||||
// }
|
||||
@@ -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)
|
||||
// }
|
||||
+99
-33
@@ -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{})
|
||||
}
|
||||
|
||||
+13
-11
@@ -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 }
|
||||
|
||||
+10
-274
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
// }
|
||||
// }
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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{}))
|
||||
}
|
||||
}
|
||||
@@ -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}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user