diff --git a/sei-cosmos/server/start.go b/sei-cosmos/server/start.go index 1b8e6c8e53..514c922cc7 100644 --- a/sei-cosmos/server/start.go +++ b/sei-cosmos/server/start.go @@ -360,7 +360,7 @@ func startInProcess( } defer func() { if tmNode.IsRunning() { - tmNode.Wait() + tmNode.Stop() } }() // Add the tx service to the gRPC router. We only need to register this @@ -464,8 +464,6 @@ func startInProcess( } } - // Defer cancelling as the last so that it is called first during unwinding. - defer cancel() // wait for signal capture and gracefully return return WaitForQuitSignals(goCtx, restartCh) } diff --git a/sei-tendermint/config/config.go b/sei-tendermint/config/config.go index 99b3b72667..49b3dfcd85 100644 --- a/sei-tendermint/config/config.go +++ b/sei-tendermint/config/config.go @@ -13,6 +13,7 @@ import ( mempoolcfg "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" tmos "github.com/sei-protocol/sei-chain/sei-tendermint/libs/os" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -860,15 +861,13 @@ type MempoolConfig struct { } func (cfg *MempoolConfig) ToMempoolConfig() *mempoolcfg.Config { - return &mempoolcfg.Config{ + mcfg := &mempoolcfg.Config{ Size: cfg.Size, MaxTxsBytes: cfg.MaxTxsBytes, CacheSize: cfg.CacheSize, DuplicateTxsCacheSize: cfg.DuplicateTxsCacheSize, KeepInvalidTxsInCache: cfg.KeepInvalidTxsInCache, MaxTxBytes: cfg.MaxTxBytes, - TTLDuration: cfg.TTLDuration, - TTLNumBlocks: cfg.TTLNumBlocks, TxNotifyThreshold: cfg.TxNotifyThreshold, PendingSize: cfg.PendingSize, MaxPendingTxsBytes: cfg.MaxPendingTxsBytes, @@ -877,6 +876,13 @@ func (cfg *MempoolConfig) ToMempoolConfig() *mempoolcfg.Config { DropUtilisationThreshold: cfg.DropUtilisationThreshold, DropPriorityReservoirSize: cfg.DropPriorityReservoirSize, } + if cfg.TTLDuration != 0 { + mcfg.TTLDuration = utils.Some(cfg.TTLDuration) + } + if cfg.TTLNumBlocks != 0 { + mcfg.TTLNumBlocks = utils.Some(cfg.TTLNumBlocks) + } + return mcfg } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool. @@ -891,8 +897,8 @@ func DefaultMempoolConfig() *MempoolConfig { KeepInvalidTxsInCache: cfg.KeepInvalidTxsInCache, MaxTxBytes: cfg.MaxTxBytes, MaxBatchBytes: 0, - TTLDuration: cfg.TTLDuration, - TTLNumBlocks: cfg.TTLNumBlocks, + TTLDuration: cfg.TTLDuration.Or(0), + TTLNumBlocks: cfg.TTLNumBlocks.Or(0), TxNotifyThreshold: cfg.TxNotifyThreshold, CheckTxErrorBlacklistEnabled: true, CheckTxErrorThreshold: 50, diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index f5c7ee088b..07fff9e134 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -66,30 +66,27 @@ func (s *State) makePayload(ctx context.Context) (*types.Payload, error) { // Wait for transactions. We give up and produce an empty block if mempool is empty for // cfg.BlockInterval. _ = utils.WithTimeout(ctx, s.cfg.BlockInterval, func(ctx context.Context) error { - return s.txMempool.TxStore().WaitForTxs(ctx) + return s.txMempool.WaitForTxs(ctx) }) // If the context has been cancelled though, we just fail. if err := ctx.Err(); err != nil { return nil, err } - txs, gasEstimated := s.txMempool.PopTxs(mempool.ReapLimits{ + txs, gasEstimated := s.txMempool.ReapTxs(mempool.ReapLimits{ MaxTxs: utils.Some(min(types.MaxTxsPerBlock, s.cfg.maxTxsPerBlock())), MaxBytes: utils.Some(utils.Clamp[int64](types.MaxTxsBytesPerBlock)), MaxGasWanted: utils.Some(s.cfg.MaxGasPerBlockI64()), MaxGasEstimated: utils.Some(s.cfg.MaxGasPerBlockI64()), - }) + }, true) payloadTxs := make([][]byte, 0, len(txs)) for _, tx := range txs { payloadTxs = append(payloadTxs, tx) } payload, err := types.PayloadBuilder{ CreatedAt: time.Now(), - // TODO: ReapMaxTxsBytesMaxGas does not handle corner cases correctly rn, which actually - // can produce negative total gas. Fixing it right away might be backward incompatible afaict, - // so we leave it as is for now. - TotalGas: uint64(gasEstimated), // nolint:gosec - Txs: payloadTxs, + TotalGas: uint64(gasEstimated), // nolint:gosec // always non-negative + Txs: payloadTxs, }.Build() // This should never happen: we construct the payload from correctly sized data. if err != nil { diff --git a/sei-tendermint/internal/consensus/mempool_test.go b/sei-tendermint/internal/consensus/mempool_test.go index b97b9e793d..e08ebef9c5 100644 --- a/sei-tendermint/internal/consensus/mempool_test.go +++ b/sei-tendermint/internal/consensus/mempool_test.go @@ -148,7 +148,7 @@ func checkTxsRange(ctx context.Context, t *testing.T, cs *testState, start, end for i := start; i < end; i++ { txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(i)) - res, err := cs.txMempool.CheckTx(ctx, txBytes, mempool.TxInfo{}) + res, err := cs.txMempool.CheckTx(ctx, txBytes) require.NoError(t, err, "error after checkTx") require.Equal(t, code.CodeTypeOK, res.Code, "checkTx code is error, txBytes %X, index=%d", txBytes, i) } @@ -182,7 +182,7 @@ func TestMempoolTxConcurrentWithCommit(t *testing.T) { for i := range int(numTxs) { txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(i)) - res, err := cs.txMempool.CheckTx(ctx, txBytes, mempool.TxInfo{}) + res, err := cs.txMempool.CheckTx(ctx, txBytes) require.NoError(t, err, "error after checkTx") require.Equal(t, code.CodeTypeOK, res.Code, "checkTx code is error, txBytes %X, index=%d", txBytes, i) } @@ -234,7 +234,7 @@ func TestMempoolRmBadTx(t *testing.T) { // Try to send the tx through the mempool. // CheckTx should not err, but the app should return a bad abci code // and the tx should get removed from the pool - res, err := cs.txMempool.CheckTx(ctx, txBytes, mempool.TxInfo{}) + res, err := cs.txMempool.CheckTx(ctx, txBytes) if err != nil { t.Errorf("error after CheckTx: %v", err) return @@ -247,7 +247,10 @@ func TestMempoolRmBadTx(t *testing.T) { // check for the tx for { - txs := cs.txMempool.ReapMaxBytesMaxGas(int64(len(txBytes)), utils.Max[int64](), utils.Max[int64]()) + txs, _ := cs.txMempool.ReapTxs( + mempool.ReapLimits{MaxBytes: utils.Some(int64(len(txBytes)))}, + false, + ) if len(txs) == 0 { emptyMempoolCh <- struct{}{} return diff --git a/sei-tendermint/internal/consensus/reactor_test.go b/sei-tendermint/internal/consensus/reactor_test.go index 1d56c57132..21c9306700 100644 --- a/sei-tendermint/internal/consensus/reactor_test.go +++ b/sei-tendermint/internal/consensus/reactor_test.go @@ -152,7 +152,7 @@ func finalizeTx( return scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { for i, sub := range blocksSubs { s.Spawn(func() error { - if _, err := states[i].txMempool.CheckTx(ctx, tx, mempool.TxInfo{}); err != nil { + if _, err := states[i].txMempool.CheckTx(ctx, tx); err != nil { return fmt.Errorf("CheckTx(): %w", err) } for { @@ -367,7 +367,6 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { _, err := states[1].txMempool.CheckTx( ctx, []byte{1, 2, 3}, - mempool.TxInfo{}, ) require.NoError(t, err) diff --git a/sei-tendermint/internal/consensus/replay_test.go b/sei-tendermint/internal/consensus/replay_test.go index c835b8757c..c4ff2f4804 100644 --- a/sei-tendermint/internal/consensus/replay_test.go +++ b/sei-tendermint/internal/consensus/replay_test.go @@ -154,7 +154,7 @@ func sendTxs(ctx context.Context, cs *testState) error { return nil } tx := []byte{byte(i)} - if _, err := cs.txMempool.CheckTx(ctx, tx, mempool.TxInfo{}); err != nil { + if _, err := cs.txMempool.CheckTx(ctx, tx); err != nil { return fmt.Errorf("cs.mempool.CheckTx(): %w", err) } } @@ -271,8 +271,7 @@ type simulatorTestSuite struct { Commits []*types.Commit CleanupFunc cleanupFunc - Mempool *mempool.TxMempool - Evpool sm.EvidencePool + Evpool sm.EvidencePool } const ( @@ -292,11 +291,8 @@ var modes = []uint{0, 1, 2, 3} func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { t.Helper() cfg := configSetup(t) - proxyApp := kvstore.NewProxy() - sim := &simulatorTestSuite{ - Mempool: newReplayTxMempool(proxyApp), - Evpool: sm.EmptyEvidencePool{}, + Evpool: sm.EmptyEvidencePool{}, } nPeers := 7 @@ -389,7 +385,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { require.NoError(t, err) valPubKey1ABCI := crypto.PubKeyToProto(newValidatorPubKey1) newValidatorTx1 := kvstore.MakeValSetChangeTx(valPubKey1ABCI, testMinPower) - _, err = css[0].txMempool.CheckTx(ctx, newValidatorTx1, mempool.TxInfo{}) + _, err = css[0].txMempool.CheckTx(ctx, newValidatorTx1) assert.NoError(t, err) css[0].signAddVotes(ctx, t, tmproto.PrecommitType, sim.Config.ChainID(), @@ -408,7 +404,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { require.NoError(t, err) updatePubKey1ABCI := crypto.PubKeyToProto(updateValidatorPubKey1) updateValidatorTx1 := kvstore.MakeValSetChangeTx(updatePubKey1ABCI, 25) - _, err = css[0].txMempool.CheckTx(ctx, updateValidatorTx1, mempool.TxInfo{}) + _, err = css[0].txMempool.CheckTx(ctx, updateValidatorTx1) assert.NoError(t, err) css[0].signAddVotes(ctx, t, tmproto.PrecommitType, sim.Config.ChainID(), types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()}, @@ -426,14 +422,14 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { require.NoError(t, err) newVal2ABCI := crypto.PubKeyToProto(newValidatorPubKey2) newValidatorTx2 := kvstore.MakeValSetChangeTx(newVal2ABCI, testMinPower) - _, err = css[0].txMempool.CheckTx(ctx, newValidatorTx2, mempool.TxInfo{}) + _, err = css[0].txMempool.CheckTx(ctx, newValidatorTx2) assert.NoError(t, err) pv, _ = css[nVals+2].privValidator.Get() newValidatorPubKey3, err := pv.GetPubKey(ctx) require.NoError(t, err) newVal3ABCI := crypto.PubKeyToProto(newValidatorPubKey3) newValidatorTx3 := kvstore.MakeValSetChangeTx(newVal3ABCI, testMinPower) - _, err = css[0].txMempool.CheckTx(ctx, newValidatorTx3, mempool.TxInfo{}) + _, err = css[0].txMempool.CheckTx(ctx, newValidatorTx3) assert.NoError(t, err) css[0].signAddVotes(ctx, t, tmproto.PrecommitType, sim.Config.ChainID(), types.BlockID{Hash: rs.ProposalBlock.Hash(), PartSetHeader: rs.ProposalBlockParts.Header()}, @@ -469,7 +465,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { ensureProposalFromCurrentLeader(height, round) rs = css[0].GetRoundState() removeValidatorTx2 := kvstore.MakeValSetChangeTx(newVal2ABCI, 0) - _, err = css[0].txMempool.CheckTx(ctx, removeValidatorTx2, mempool.TxInfo{}) + _, err = css[0].txMempool.CheckTx(ctx, removeValidatorTx2) assert.NoError(t, err) for i := 0; i < nVals+1; i++ { @@ -498,7 +494,7 @@ func setupSimulator(ctx context.Context, t *testing.T) *simulatorTestSuite { rs = css[0].GetRoundState() removeValidatorTx3 := kvstore.MakeValSetChangeTx(newVal3ABCI, 0) - _, err = css[0].txMempool.CheckTx(ctx, removeValidatorTx3, mempool.TxInfo{}) + _, err = css[0].txMempool.CheckTx(ctx, removeValidatorTx3) assert.NoError(t, err) for i := 0; i < nVals+1; i++ { if i == selfIndex { @@ -655,11 +651,12 @@ func testHandshakeReplay( store.commits = commits state := genesisState.Copy() + replayMempool := newReplayTxMempool(kvstore.NewProxy()) // run the chain through state.ApplyBlock to build up the tendermint state state = buildTMStateFromChain( ctx, t, - sim.Mempool, + replayMempool, sim.Evpool, stateStore, state, @@ -681,7 +678,7 @@ func testHandshakeReplay( stateStore := sm.NewStore(stateDB1) err := stateStore.Save(genesisState) require.NoError(t, err) - buildAppStateFromChain(ctx, t, app, stateStore, sim.Mempool, sim.Evpool, genesisState, chain, eventBus, nBlocks, mode, store) + buildAppStateFromChain(ctx, t, app, stateStore, sim.Evpool, genesisState, chain, eventBus, nBlocks, mode, store) } // Prune block store if requested @@ -759,7 +756,6 @@ func buildAppStateFromChain( t *testing.T, appClient *kvstore.Application, stateStore sm.Store, - mempool *mempool.TxMempool, evpool sm.EvidencePool, state sm.State, chain []*types.Block, @@ -771,6 +767,7 @@ func buildAppStateFromChain( t.Helper() // start a new app without handshake, play nBlocks blocks proxyApp := proxy.New(appClient, proxy.NopMetrics()) + mempool := newReplayTxMempool(proxyApp) state.Version.Consensus.App = kvstore.ProtocolVersion // simulate handshake, receive app version _, err := appClient.InitChain(ctx, &abci.RequestInitChain{}) require.NoError(t, err) diff --git a/sei-tendermint/internal/consensus/state.go b/sei-tendermint/internal/consensus/state.go index c385549c62..c7e47ee066 100644 --- a/sei-tendermint/internal/consensus/state.go +++ b/sei-tendermint/internal/consensus/state.go @@ -2279,7 +2279,7 @@ func (cs *State) buildProposalBlock(proposal *types.Proposal) *types.Block { txs, missingTxs := cs.blockExec.SafeGetTxsByHashes(proposal.TxHashes) if len(missingTxs) > 0 { cs.metrics.ProposalMissingTxs.Set(float64(len(missingTxs))) - logger.Debug("Missing txs when trying to build block", "missing_txs", cs.blockExec.GetMissingTxs(proposal.TxHashes)) + logger.Debug("Missing txs when trying to build block", "missing_txs", missingTxs) return nil } block := cs.state.MakeBlock(proposal.Height, txs, proposal.LastCommit, proposal.Evidence, proposal.ProposerAddress) diff --git a/sei-tendermint/internal/consensus/state_test.go b/sei-tendermint/internal/consensus/state_test.go index f3ec67aa1b..6a2531631a 100644 --- a/sei-tendermint/internal/consensus/state_test.go +++ b/sei-tendermint/internal/consensus/state_test.go @@ -18,7 +18,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" cstypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/consensus/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" tmpubsub "github.com/sei-protocol/sei-chain/sei-tendermint/internal/pubsub" tmquery "github.com/sei-protocol/sei-chain/sei-tendermint/internal/pubsub/query" @@ -2208,7 +2207,7 @@ func TestStartNextHeightCorrectlyAfterTimeout(t *testing.T) { ensureNewBlockHeader(t, newBlockHeader, height, blockID.Hash) - _, err := cs1.txMempool.CheckTx(ctx, types.Tx("test-key=test-value"), mempool.TxInfo{}) + _, err := cs1.txMempool.CheckTx(ctx, types.Tx("test-key=test-value")) require.NoError(t, err, "failed to seed the mempool with a transaction") ensureNewTimeout(t, timeoutProposeCh, height+1, 0) @@ -2592,7 +2591,7 @@ func TestTryCreateProposalBlock_PartsMismatch(t *testing.T) { incrementRound(vss[1:]...) cs.startTestRound(ctx, height, round) - _, err := cs.txMempool.CheckTx(ctx, types.Tx("test-key=test-value"), mempool.TxInfo{}) + _, err := cs.txMempool.CheckTx(ctx, types.Tx("test-key=test-value")) require.NoError(t, err, "failed to seed the mempool with a transaction") proposal, block := cs.decideProposal(ctx, t, vss[1], height, round) diff --git a/sei-tendermint/internal/evidence/pool.go b/sei-tendermint/internal/evidence/pool.go index 550b0a420f..fcf98d185d 100644 --- a/sei-tendermint/internal/evidence/pool.go +++ b/sei-tendermint/internal/evidence/pool.go @@ -534,7 +534,6 @@ func (evpool *Pool) removeEvidenceFromList( ev := e.Value() if _, ok := blockEvidenceMap[evMapKey(ev)]; ok { evpool.evidenceList.Remove(e) - e.DetachPrev() } } } diff --git a/sei-tendermint/internal/libs/clist/bench_test.go b/sei-tendermint/internal/libs/clist/bench_test.go index 8a5ab3699f..54f8ac64f1 100644 --- a/sei-tendermint/internal/libs/clist/bench_test.go +++ b/sei-tendermint/internal/libs/clist/bench_test.go @@ -2,24 +2,6 @@ package clist import "testing" -func BenchmarkDetaching(b *testing.B) { - lst := New[int]() - for i := 0; i < b.N+1; i++ { - lst.PushBack(i) - } - start := lst.Front() - nxt := start.Next() - b.ResetTimer() - for i := 0; i < b.N; i++ { - start.removed = true - start.detachNext() - start.DetachPrev() - tmp := nxt - nxt = nxt.Next() - start = tmp - } -} - // This is used to benchmark the time of RMutex. func BenchmarkRemoved(b *testing.B) { lst := New[int]() diff --git a/sei-tendermint/internal/libs/clist/clist.go b/sei-tendermint/internal/libs/clist/clist.go index 7c026077e4..80417f618f 100644 --- a/sei-tendermint/internal/libs/clist/clist.go +++ b/sei-tendermint/internal/libs/clist/clist.go @@ -6,7 +6,6 @@ The purpose of CList is to provide a goroutine-safe linked-list. This list can be traversed concurrently by any number of goroutines. However, removed CElements cannot be added back. NOTE: Not all methods of container/list are (yet) implemented. -NOTE: Removed elements need to DetachPrev or DetachNext consistently to ensure garbage collection of removed elements. */ @@ -77,63 +76,34 @@ func (e *CElement[T]) NextWait(ctx context.Context) (*CElement[T], error) { // Nonblocking, may return nil if at the end. func (e *CElement[T]) Next() *CElement[T] { e.mtx.RLock() - val := e.next - e.mtx.RUnlock() - return val + defer e.mtx.RUnlock() + return e.next } // Nonblocking, may return nil if at the end. func (e *CElement[T]) Prev() *CElement[T] { e.mtx.RLock() - prev := e.prev - e.mtx.RUnlock() - return prev + defer e.mtx.RUnlock() + return e.prev } func (e *CElement[T]) Removed() bool { e.mtx.RLock() - isRemoved := e.removed - e.mtx.RUnlock() - return isRemoved + defer e.mtx.RUnlock() + return e.removed } func (e *CElement[T]) Value() T { return e.value } -func (e *CElement[T]) detachNext() { - e.mtx.Lock() - if !e.removed { - e.mtx.Unlock() - panic("DetachNext() must be called after Remove(e)") - } - e.next = nil - e.mtx.Unlock() -} - -func (e *CElement[T]) DetachPrev() { - e.mtx.Lock() - if !e.removed { - e.mtx.Unlock() - panic("DetachPrev() must be called after Remove(e)") - } - e.prev = nil - e.mtx.Unlock() -} - // NOTE: This function needs to be safe for // concurrent goroutines waiting on nextWg. func (e *CElement[T]) setNext(newNext *CElement[T]) { e.mtx.Lock() - oldNext := e.next e.next = newNext if oldNext != nil && newNext == nil { - // See https://golang.org/pkg/sync/: - // - // If a WaitGroup is reused to wait for several independent sets of - // events, new Add calls must happen after all previous Wait calls have - // returned. e.nextWaitCh = make(chan struct{}) } if oldNext == nil && newNext != nil { @@ -154,13 +124,12 @@ func (e *CElement[T]) setPrev(newPrev *CElement[T]) { func (e *CElement[T]) setRemoved() { e.mtx.Lock() defer e.mtx.Unlock() - - e.removed = true - // This wakes up anyone waiting. if e.next == nil { close(e.nextWaitCh) } + e.prev = nil + e.removed = true } //-------------------------------------------------------------------------------- @@ -224,6 +193,21 @@ func (l *CList[T]) Back() *CElement[T] { return back } +func (l *CList[T]) Clear() { + l.mtx.Lock() + defer l.mtx.Unlock() + + for el := l.head; el != nil; { + next := el.Next() + el.setRemoved() + el = next + } + l.waitCh = make(chan struct{}) + l.head = nil + l.tail = nil + l.len = 0 +} + // Panics if list grows beyond its max length. func (l *CList[T]) PushBack(v T) *CElement[T] { l.mtx.Lock() @@ -256,7 +240,6 @@ func (l *CList[T]) PushBack(v T) *CElement[T] { return e } -// CONTRACT: Caller must call e.DetachPrev() and/or e.DetachNext() to avoid memory leaks. // NOTE: As per the contract of CList, removed elements cannot be added back. func (l *CList[T]) Remove(e *CElement[T]) T { l.mtx.Lock() diff --git a/sei-tendermint/internal/libs/clist/clist_test.go b/sei-tendermint/internal/libs/clist/clist_test.go index 3573cdb7dd..9a8fdb8723 100644 --- a/sei-tendermint/internal/libs/clist/clist_test.go +++ b/sei-tendermint/internal/libs/clist/clist_test.go @@ -75,13 +75,7 @@ func TestGCFifo(t *testing.T) { }) } - for el := l.Front(); el != nil; { - l.Remove(el) - // oldEl := el - el = el.Next() - // oldEl.DetachPrev() - // oldEl.DetachNext() - } + l.Clear() tickerQuitCh := make(chan struct{}) tickerDoneCh := make(chan struct{}) @@ -242,9 +236,7 @@ func TestScanRightDeleteRandom(t *testing.T) { close(stop) // And remove all the elements. - for el := l.Front(); el != nil; el = el.Next() { - l.Remove(el) - } + l.Clear() if l.Len() != 0 { t.Fatal("Failed to remove all elements from CList") } diff --git a/sei-tendermint/internal/mempool/cache.go b/sei-tendermint/internal/mempool/cache.go index 43ef786454..c959ad2bd8 100644 --- a/sei-tendermint/internal/mempool/cache.go +++ b/sei-tendermint/internal/mempool/cache.go @@ -3,7 +3,6 @@ package mempool import ( "container/list" "context" - "sync" "time" "github.com/patrickmn/go-cache" @@ -11,32 +10,9 @@ import ( "github.com/sei-protocol/sei-chain/sei-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 transaction key to the cache and returns true if it was - // newly added. Otherwise, it returns false. - Push(tx types.TxHash) bool - - // Remove removes the given transaction key from the cache. - Remove(tx types.TxHash) - - // Size returns the current size of the cache - Size() int -} - -var _ TxCache = (*LRUTxCache)(nil) - -// LRUTxCache maintains a thread-safe LRU cache of raw transactions. The cache +// lruTxCache maintains a NON-threadsafe lru cache of raw transactions. The cache // only stores the hash of the raw transaction. -type LRUTxCache struct { - mtx sync.Mutex +type lruTxCache struct { size int cacheMap map[cacheKey]*list.Element list *list.List @@ -45,7 +21,7 @@ type LRUTxCache struct { type cacheKey = string -// NewLRUTxCache creates an LRU (Least Recently Used) cache that stores +// newLRUTxCache creates an LRU (Least Recently Used) cache that stores // transactions by key. Keys are derived from the transaction key and trimmed to // at most maxKeyLen bytes for predictable and efficient storage. If maxKeyLen is // zero or negative, keys are not trimmed. When the cache exceeds cacheSize, the @@ -56,8 +32,8 @@ type cacheKey = string // positives in cache lookups. A larger value reduces collision risk but uses // more memory. A common choice is to use the full length of a cryptographic hash // (e.g., 32 bytes for SHA-256) to balance memory usage and collision risk. -func NewLRUTxCache(cacheSize int, maxKeyLen int) *LRUTxCache { - return &LRUTxCache{ +func newLRUTxCache(cacheSize int, maxKeyLen int) *lruTxCache { + return &lruTxCache{ size: cacheSize, cacheMap: make(map[cacheKey]*list.Element, cacheSize), list: list.New(), @@ -65,17 +41,20 @@ func NewLRUTxCache(cacheSize int, maxKeyLen int) *LRUTxCache { } } -func (c *LRUTxCache) Reset() { - c.mtx.Lock() - defer c.mtx.Unlock() +func (c *lruTxCache) Has(txHash types.TxHash) bool { + _, ok := c.cacheMap[c.toCacheKey(txHash)] + return ok +} +func (c *lruTxCache) Reset() { c.cacheMap = make(map[cacheKey]*list.Element, c.size) c.list.Init() } -func (c *LRUTxCache) Push(txHash types.TxHash) bool { - c.mtx.Lock() - defer c.mtx.Unlock() +func (c *lruTxCache) Push(txHash types.TxHash) bool { + if c.size <= 0 { + return true + } key := c.toCacheKey(txHash) moved, ok := c.cacheMap[key] @@ -99,9 +78,7 @@ func (c *LRUTxCache) Push(txHash types.TxHash) bool { return true } -func (c *LRUTxCache) Remove(txHash types.TxHash) { - c.mtx.Lock() - defer c.mtx.Unlock() +func (c *lruTxCache) Remove(txHash types.TxHash) { key := c.toCacheKey(txHash) e := c.cacheMap[key] @@ -112,26 +89,14 @@ func (c *LRUTxCache) Remove(txHash types.TxHash) { } } -func (c *LRUTxCache) Size() int { - c.mtx.Lock() - defer c.mtx.Unlock() +func (c *lruTxCache) Size() int { return c.list.Len() } -func (c *LRUTxCache) toCacheKey(key types.TxHash) cacheKey { +func (c *lruTxCache) toCacheKey(key types.TxHash) cacheKey { return cacheKey(trimToSize(key, c.maxKeyLen)) } -// NopTxCache defines a no-op raw transaction cache. -type NopTxCache struct{} - -var _ TxCache = (*NopTxCache)(nil) - -func (NopTxCache) Reset() {} -func (NopTxCache) Push(types.TxHash) bool { return true } -func (NopTxCache) Remove(types.TxHash) {} -func (NopTxCache) Size() int { return 0 } - // DuplicateTxCache implements TxCacheWithTTL using go-cache type DuplicateTxCache struct { maxSize int diff --git a/sei-tendermint/internal/mempool/cache_bench_test.go b/sei-tendermint/internal/mempool/cache_bench_test.go index 519d8bb2e9..d3be978c44 100644 --- a/sei-tendermint/internal/mempool/cache_bench_test.go +++ b/sei-tendermint/internal/mempool/cache_bench_test.go @@ -8,7 +8,7 @@ import ( ) func BenchmarkCacheInsertTime(b *testing.B) { - cache := NewLRUTxCache(b.N, 0) + cache := newLRUTxCache(b.N, 0) txs := make([]types.TxHash, b.N) for i := 0; i < b.N; i++ { @@ -27,7 +27,7 @@ func BenchmarkCacheInsertTime(b *testing.B) { // 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, 0) + cache := newLRUTxCache(b.N, 0) txs := make([]types.TxHash, b.N) for i := 0; i < b.N; i++ { diff --git a/sei-tendermint/internal/mempool/cache_test.go b/sei-tendermint/internal/mempool/cache_test.go index 6b2475f37e..9e8be7c9df 100644 --- a/sei-tendermint/internal/mempool/cache_test.go +++ b/sei-tendermint/internal/mempool/cache_test.go @@ -11,8 +11,8 @@ import ( ) func TestLRUTxCache(t *testing.T) { - t.Run("NewLRUTxCache", func(t *testing.T) { - cache := NewLRUTxCache(10, 0) + t.Run("newLRUTxCache", func(t *testing.T) { + cache := newLRUTxCache(10, 0) assert.NotNil(t, cache) assert.Equal(t, 10, cache.size) assert.NotNil(t, cache.cacheMap) @@ -20,7 +20,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Push_NewTransaction", func(t *testing.T) { - cache := NewLRUTxCache(3, 0) + cache := newLRUTxCache(3, 0) tx := types.Tx("test1").Hash() // First push should return true (newly added) @@ -30,7 +30,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Push_DuplicateTransaction", func(t *testing.T) { - cache := NewLRUTxCache(3, 0) + cache := newLRUTxCache(3, 0) tx := types.Tx("test1").Hash() // First push @@ -44,7 +44,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Push_CacheFull", func(t *testing.T) { - cache := NewLRUTxCache(2, 0) + cache := newLRUTxCache(2, 0) // Add two transactions tx1 := types.Tx("test1").Hash() @@ -54,7 +54,7 @@ func TestLRUTxCache(t *testing.T) { cache.Push(tx2) assert.Equal(t, 2, cache.Size()) - // Add third transaction, should evict the first one (LRU) + // Add third transaction, should evict the first one (lru) tx3 := types.Tx("test3").Hash() cache.Push(tx3) assert.Equal(t, 2, cache.Size()) @@ -64,7 +64,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Remove_ExistingTransaction", func(t *testing.T) { - cache := NewLRUTxCache(3, 0) + cache := newLRUTxCache(3, 0) tx := types.Tx("test1").Hash() cache.Push(tx) @@ -75,7 +75,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Remove_NonExistentTransaction", func(t *testing.T) { - cache := NewLRUTxCache(3, 0) + cache := newLRUTxCache(3, 0) tx := types.Tx("test1").Hash() // Remove non-existent transaction should not panic @@ -84,7 +84,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Reset", func(t *testing.T) { - cache := NewLRUTxCache(3, 0) + cache := newLRUTxCache(3, 0) // Add some transactions cache.Push(types.Tx("test1").Hash()) @@ -97,7 +97,7 @@ func TestLRUTxCache(t *testing.T) { }) t.Run("Size", func(t *testing.T) { - cache := NewLRUTxCache(3, 0) + cache := newLRUTxCache(3, 0) assert.Equal(t, 0, cache.Size()) cache.Push(types.Tx("test1").Hash()) @@ -108,32 +108,6 @@ func TestLRUTxCache(t *testing.T) { }) } -func TestNopTxCache(t *testing.T) { - cache := NopTxCache{} - - t.Run("Reset", func(t *testing.T) { - // Should not panic - cache.Reset() - }) - - t.Run("Push", func(t *testing.T) { - tx := types.Tx("test").Hash() - result := cache.Push(tx) - assert.True(t, result) - }) - - t.Run("Remove", func(t *testing.T) { - tx := types.Tx("test").Hash() - // Should not panic - cache.Remove(tx) - }) - - t.Run("Size", func(t *testing.T) { - size := cache.Size() - assert.Equal(t, 0, size) - }) -} - func TestDuplicateTxCache(t *testing.T) { t.Run("NewDuplicateTxCache_WithExpiration", func(t *testing.T) { cache := NewDuplicateTxCache(100, 100*time.Millisecond, 0) @@ -351,39 +325,6 @@ func TestDuplicateTxCache(t *testing.T) { }) } -func TestLRUTxCache_ConcurrentAccess(t *testing.T) { - cache := NewLRUTxCache(100, 0) - - // Test concurrent access - const numGoroutines = 10 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - tx := types.Tx(fmt.Sprintf("goroutine_%d_tx_%d", id, j)).Hash() - cache.Push(tx) - - if j%10 == 0 { - cache.Size() // Read operation - } - } - }(i) - } - - wg.Wait() - - // Verify final state is reasonable - size := cache.Size() - assert.True(t, size > 0) - assert.True(t, size <= 100) // Should not exceed cache size -} - func TestDuplicateTxCache_ConcurrentAccess(t *testing.T) { cache := NewDuplicateTxCache(100, 100*time.Millisecond, 0) @@ -426,27 +367,29 @@ func TestDuplicateTxCache_ConcurrentAccess(t *testing.T) { func TestLRUTxCache_EdgeCases(t *testing.T) { t.Run("ZeroSizeCache", func(t *testing.T) { - cache := NewLRUTxCache(0, 0) + cache := newLRUTxCache(0, 0) tx := types.Tx("test").Hash() - // Should handle zero size gracefully + // Zero-sized cache is effectively disabled. result := cache.Push(tx) assert.True(t, result) - assert.Equal(t, 1, cache.Size()) + assert.Equal(t, 0, cache.Size()) + assert.False(t, cache.Has(tx)) }) t.Run("NegativeSizeCache", func(t *testing.T) { - cache := NewLRUTxCache(-1, 0) + cache := newLRUTxCache(-1, 0) tx := types.Tx("test").Hash() - // Should handle negative size gracefully + // Negative-sized cache is effectively disabled. result := cache.Push(tx) assert.True(t, result) - assert.Equal(t, 1, cache.Size()) + assert.Equal(t, 0, cache.Size()) + assert.False(t, cache.Has(tx)) }) t.Run("NilTransaction", func(t *testing.T) { - cache := NewLRUTxCache(10, 0) + cache := newLRUTxCache(10, 0) var tx types.TxHash // Should handle nil transaction gracefully @@ -491,18 +434,6 @@ func TestDuplicateTxCache_EdgeCases(t *testing.T) { }) } -func TestCache_InterfaceCompliance(t *testing.T) { - // Test that all implementations properly implement their interfaces - - t.Run("LRUTxCache_Implements_TxCache", func(t *testing.T) { - var _ TxCache = (*LRUTxCache)(nil) - }) - - t.Run("NopTxCache_Implements_TxCache", func(t *testing.T) { - var _ TxCache = (*NopTxCache)(nil) - }) -} - // createTestTxHash creates a test TxHash from a string by hashing it func createTestTxHash(input string) types.TxHash { // Create a simple hash-like key for testing diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index c038a05b90..2a46b0ee5a 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -1,12 +1,10 @@ package mempool import ( - "bytes" "context" "crypto/sha256" "errors" "fmt" - "math/big" "sync" "sync/atomic" "time" @@ -14,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/common" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/reservoir" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" @@ -71,21 +68,11 @@ type Config struct { // NOTE: the max size of a tx transmitted over the network is {max-tx-bytes}. MaxTxBytes int - // TTLDuration, if non-zero, defines the maximum amount of time a transaction - // can exist for in the mempool. - // - // Note, if TTLNumBlocks is also defined, a transaction will be removed if it - // has existed in the mempool at least TTLNumBlocks number of blocks or if it's - // insertion time into the mempool is beyond TTLDuration. - TTLDuration time.Duration + // time after which transaction is removed from mempool. + TTLDuration utils.Option[time.Duration] - // TTLNumBlocks, if non-zero, defines the maximum number of blocks a transaction - // can exist for in the mempool. - // - // Note, if TTLDuration is also defined, a transaction will be removed if it - // has existed in the mempool at least TTLNumBlocks number of blocks or if - // it's insertion time into the mempool is beyond TTLDuration. - TTLNumBlocks int64 + // number of blocks after which a transaction is removed from mempool. + TTLNumBlocks utils.Option[int64] // TxNotifyThreshold, if non-zero, defines the minimum number of transactions // needed to trigger a notification in mempool's Tx notifier @@ -97,6 +84,7 @@ type Config struct { // Limit the total size of all txs in the pending set. MaxPendingTxsBytes int64 + // Whether expired READY transactions should be pruned from mempool (PENDING expired are always prunned) RemoveExpiredTxsFromQueue bool // DropPriorityThreshold defines the percentage of transactions with the lowest @@ -148,9 +136,9 @@ func DefaultConfig() *Config { MaxTxsBytes: 1024 * 1024 * 1024, // 1GB CacheSize: 10000, DuplicateTxsCacheSize: 100000, - MaxTxBytes: 1024 * 1024, // 1MB - TTLDuration: 5 * time.Second, // prevent stale txs from filling mempool - TTLNumBlocks: 10, // remove txs after 10 blocks + MaxTxBytes: 1024 * 1024, // 1MB + TTLDuration: utils.Some(5 * time.Second), // prevent stale txs from filling mempool + TTLNumBlocks: utils.Some(int64(10)), // remove txs after 10 blocks TxNotifyThreshold: 0, PendingSize: 5000, MaxPendingTxsBytes: 1024 * 1024 * 1024, // 1GB @@ -161,9 +149,26 @@ func DefaultConfig() *Config { } } -type evmAddrNonce struct { - Address common.Address - Nonce uint64 +type lockMap[K comparable] struct{ inner utils.Mutex[map[K]struct{}] } + +func newLockMap[K comparable]() *lockMap[K] { + return &lockMap[K]{inner: utils.NewMutex(map[K]struct{}{})} +} + +func (m *lockMap[K]) Lock(k K) bool { + for inner := range m.inner.Lock() { + if _, ok := inner[k]; ok { + return false + } + inner[k] = struct{}{} + } + return true +} + +func (m *lockMap[K]) Unlock(k K) { + for inner := range m.inner.Lock() { + delete(inner, k) + } } // TxMempool defines a prioritized mempool data structure used by the v1 mempool @@ -174,6 +179,7 @@ type TxMempool struct { metrics *Metrics config *Config app *proxy.Proxy + txLocks *lockMap[types.TxHash] // txsAvailable fires once for each height when the mempool is not empty txsAvailable chan struct{} @@ -182,50 +188,13 @@ type TxMempool struct { // height defines the last block height process during Update() height int64 - // cache defines a fixed-size cache of already seen transactions as this - // reduces pressure on the proxyApp. - cache TxCache - - // blockFailedTxs tracks tx hashes that have previously failed during - // block execution. Used to prevent infinite re-entry of txs that - // consistently fail before fee charging in DeliverTx. - blockFailedTxs TxCache - // A TTL cache which keeps all txs that we have seen before over the TTL window. // Currently, this can be used for tracking whether checkTx is always serving the same tx or not. duplicateTxsCache utils.Option[*DuplicateTxCache] // 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[*WrappedTx] - - // 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[*WrappedTx] // next expected response - recheckEnd *clist.CElement[*WrappedTx] // re-checking stops here - - // priorityIndex defines the priority index of valid transactions via a - // thread-safe priority queue. - priorityIndex *TxPriorityQueue - - // pendingTxs stores transactions that are not valid yet but might become valid - // once nonce ordering or sender balance catches up. - pendingTxs *PendingTxs - - byAddrNonce utils.Mutex[map[evmAddrNonce]*WrappedTx] + txStore *txStore // 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, @@ -233,37 +202,31 @@ type TxMempool struct { // the mempool via Update(). mtx sync.RWMutex txConstraintsFetcher TxConstraintsFetcher - - priorityReservoir *reservoir.Sampler[int64] } +func (txmp *TxMempool) Size() int { return txmp.txStore.State().total.count } +func (txmp *TxMempool) SizeBytes() uint64 { return txmp.txStore.State().ready.bytes } +func (txmp *TxMempool) NumTxsNotPending() int { return txmp.txStore.State().ready.count } +func (txmp *TxMempool) BytesNotPending() uint64 { return txmp.txStore.State().ready.bytes } +func (txmp *TxMempool) TotalTxsBytesSize() uint64 { return txmp.txStore.State().total.bytes } +func (txmp *TxMempool) PendingSize() int { return txmp.txStore.State().PendingCount() } +func (txmp *TxMempool) PendingSizeBytes() uint64 { return txmp.txStore.State().PendingBytes() } + func NewTxMempool( cfg *Config, app *proxy.Proxy, metrics *Metrics, txConstraintsFetcher TxConstraintsFetcher, ) *TxMempool { - txmp := &TxMempool{ config: cfg, app: app, txsAvailable: make(chan struct{}, 1), + txLocks: newLockMap[types.TxHash](), height: -1, - cache: NopTxCache{}, - blockFailedTxs: NopTxCache{}, metrics: metrics, - txStore: NewTxStore(), - gossipIndex: clist.New[*WrappedTx](), - priorityIndex: NewTxPriorityQueue(), - pendingTxs: NewPendingTxs(cfg), - byAddrNonce: utils.NewMutex(map[evmAddrNonce]*WrappedTx{}), + txStore: NewTxStore(cfg, app, metrics), txConstraintsFetcher: txConstraintsFetcher, - priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG - } - - if cfg.CacheSize > 0 { - txmp.cache = NewLRUTxCache(cfg.CacheSize, maxCacheKeySize) - txmp.blockFailedTxs = NewLRUTxCache(cfg.CacheSize, maxCacheKeySize) } if cfg.DuplicateTxsCacheSize > 0 { @@ -273,52 +236,16 @@ func NewTxMempool( return txmp } -func (txmp *TxMempool) Config() *Config { return txmp.config } - +func (txmp *TxMempool) Config() *Config { return txmp.config } func (txmp *TxMempool) App() *proxy.Proxy { return txmp.app } - func (txmp *TxMempool) EvmNextPendingNonce(addr common.Address) uint64 { - an := evmAddrNonce{addr, txmp.app.EvmNonce(addr)} - for byAddrNonce := range txmp.byAddrNonce.Lock() { - for { - if _, ok := byAddrNonce[an]; !ok { - break - } - an.Nonce += 1 - } - } - return an.Nonce + return txmp.txStore.NextNonce(addr) } -func (txmp *TxMempool) addNonce(wtx *WrappedTx) { - evm, ok := wtx.evm.Get() - if !ok { - return - } - an := evmAddrNonce{evm.address, evm.nonce} - for byAddrNonce := range txmp.byAddrNonce.Lock() { - if old, ok := byAddrNonce[an]; ok && old.priority >= wtx.priority { - return - } - byAddrNonce[an] = wtx - } +func (txmp *TxMempool) WaitForTxs(ctx context.Context) error { + return txmp.txStore.WaitForTxs(ctx) } -func (txmp *TxMempool) removeNonce(wtx *WrappedTx) { - evm, ok := wtx.evm.Get() - if !ok { - return - } - an := evmAddrNonce{evm.address, evm.nonce} - for byAddrNonce := range txmp.byAddrNonce.Lock() { - if byAddrNonce[an] == wtx { - delete(byAddrNonce, an) - } - } -} - -func (txmp *TxMempool) TxStore() *TxStore { return txmp.txStore } - // 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() } @@ -326,66 +253,19 @@ 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.NumTxsNotPending() + txmp.PendingSize() -} - func (txmp *TxMempool) utilisation() float64 { - return float64(txmp.NumTxsNotPending()) / float64(txmp.config.Size) -} - -func (txmp *TxMempool) NumTxsNotPending() int { - return txmp.txStore.Size() -} - -func (txmp *TxMempool) BytesNotPending() int64 { - return txmp.txStore.AllTxsBytes() + return float64(txmp.Size()) / float64(txmp.config.Size+txmp.config.PendingSize) } -func (txmp *TxMempool) TotalTxsBytesSize() int64 { - return txmp.BytesNotPending() + txmp.pendingTxs.SizeBytes() -} - -// PendingSize returns the number of pending transactions in the mempool. -func (txmp *TxMempool) PendingSize() int { return txmp.pendingTxs.Size() } -func (txmp *TxMempool) PendingSizeBytes() int64 { return txmp.pendingTxs.SizeBytes() } - -// 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 txmp.txStore.AllTxsBytes() } - // WaitForNextTx waits until the next transaction is available for gossip. // Returns the next valid transaction to gossip. -func (txmp *TxMempool) WaitForNextTx(ctx context.Context) (*clist.CElement[*WrappedTx], error) { - return txmp.gossipIndex.WaitFront(ctx) +func (txmp *TxMempool) WaitForReadyTx(ctx context.Context) (*clist.CElement[types.Tx], error) { + return txmp.txStore.readyTxs.WaitFront(ctx) } // 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 -} - -func (txmp *TxMempool) checkResponseState(wtx *WrappedTx) error { - constraints, err := txmp.txConstraintsFetcher() - if err != nil { - return err - } - - if constraints.MaxGas == -1 { - return nil - } - if wtx.gasWanted < 0 { - return fmt.Errorf("negative gas wanted: %d", wtx.gasWanted) - } - if wtx.gasWanted > constraints.MaxGas { - return fmt.Errorf("gas wanted exceeds max gas: gas wanted %d is greater than max gas %d", wtx.gasWanted, constraints.MaxGas) - } - - return nil -} +func (txmp *TxMempool) TxsAvailable() <-chan struct{} { return txmp.txsAvailable } // CheckTx executes the ABCI CheckTx method for a given transaction. // It acquires a read-lock and attempts to execute the application's @@ -408,19 +288,29 @@ func (txmp *TxMempool) checkResponseState(wtx *WrappedTx) error { // 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, txInfo TxInfo) (*abci.ResponseCheckTx, error) { +func (txmp *TxMempool) CheckTx(ctx context.Context, tx types.Tx) (*abci.ResponseCheckTx, error) { txmp.mtx.RLock() defer txmp.mtx.RUnlock() + // Early exit if tx is too large. if txSize := len(tx); txSize > txmp.config.MaxTxBytes { return nil, fmt.Errorf("%w: max size is %d, but got %d", ErrTxTooLarge, txmp.config.MaxTxBytes, txSize) } + hTx := newHashedTx(tx) + + // Avoid processing same transaction in parallel. + if !txmp.txLocks.Lock(hTx.Hash()) { + // ErrTxInCache is returned for backward compatibility. + return nil, ErrTxInCache + } + defer txmp.txLocks.Unlock(hTx.Hash()) + constraints, err := txmp.txConstraintsFetcher() if err != nil { return nil, fmt.Errorf("txmp.txConstraintsFetcher(): %w", err) } - if txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}); txSize > constraints.MaxDataBytes { - return nil, fmt.Errorf("%w: tx size is too big: %d, max: %d", ErrTxTooLarge, txSize, constraints.MaxDataBytes) + if hTx.protoSize > constraints.MaxDataBytes { + return nil, fmt.Errorf("%w: tx size is too big: %d, max: %d", ErrTxTooLarge, hTx.protoSize, constraints.MaxDataBytes) } // Reject low priority transactions when the mempool is more than @@ -430,43 +320,33 @@ func (txmp *TxMempool) CheckTx(ctx context.Context, tx types.Tx, txInfo TxInfo) hint, err := txmp.app.GetTxPriorityHint(ctx, &abci.RequestGetTxPriorityHintV2{Tx: tx}) if err != nil { - txmp.metrics.observeCheckTxPriorityDistribution(0, true, txInfo.SenderNodeID, true) + txmp.metrics.observeCheckTxPriorityDistribution(0, true, "", true) logger.Error("failed to get tx priority hint", "err", err) return nil, err } - txmp.metrics.observeCheckTxPriorityDistribution(hint.Priority, true, txInfo.SenderNodeID, false) + txmp.metrics.observeCheckTxPriorityDistribution(hint.Priority, true, "", false) - cutoff, found := txmp.priorityReservoir.Percentile() + cutoff, found := txmp.txStore.priorityReservoir.Percentile() if found && hint.Priority <= cutoff { txmp.metrics.CheckTxDroppedByPriorityHint.Add(1) return nil, errors.New("priority not high enough for mempool") } } - txHash := tx.Hash() // 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(txHash) { - txmp.txStore.GetOrSetPeerByTxHash(txHash, txInfo.SenderID) + if txmp.txStore.CacheHas(hTx.Hash()) { return nil, ErrTxInCache } - txmp.metrics.CacheSize.Set(float64(txmp.cache.Size())) - // Check TTL cache to see if we've recently processed this transaction - // Only execute TTL cache logic if we're using a real TTL cache (not NOP) if c, ok := txmp.duplicateTxsCache.Get(); ok { - c.Increment(txHash) - } - - if len(txInfo.SenderNodeID) == 0 { - txmp.metrics.NumberOfLocalCheckTx.Add(1) + c.Increment(hTx.Hash()) } res, err := txmp.app.CheckTxSafe(ctx, &abci.RequestCheckTxV2{Tx: tx}) if err != nil || !res.IsOK() { txmp.metrics.NumberOfFailedCheckTxs.Add(1) - txmp.metrics.observeCheckTxPriorityDistribution(0, false, txInfo.SenderNodeID, true) - txmp.cache.Remove(txHash) + txmp.metrics.observeCheckTxPriorityDistribution(0, false, "", true) } if err != nil { return nil, err @@ -475,16 +355,20 @@ func (txmp *TxMempool) CheckTx(ctx context.Context, tx types.Tx, txInfo TxInfo) return res.ResponseCheckTx, nil } txmp.metrics.NumberOfSuccessfulCheckTxs.Add(1) - txmp.metrics.observeCheckTxPriorityDistribution(res.Priority, false, txInfo.SenderNodeID, false) + txmp.metrics.observeCheckTxPriorityDistribution(res.Priority, false, "", false) + // Normalize the estimate. + estimatedGas := res.GasEstimated + if estimatedGas < MinGasEVMTx || estimatedGas > res.GasWanted { + estimatedGas = res.GasWanted + } wtx := &WrappedTx{ - hashedTx: newHashedTx(tx), + hashedTx: hTx, timestamp: time.Now().UTC(), height: txmp.height, priority: res.Priority, - estimatedGas: res.GasEstimated, + estimatedGas: estimatedGas, gasWanted: res.GasWanted, - peers: map[uint16]struct{}{txInfo.SenderID: {}}, } if res.IsEVM { wtx.evm = utils.Some(evmTx{ @@ -495,83 +379,40 @@ func (txmp *TxMempool) CheckTx(ctx context.Context, tx types.Tx, txInfo TxInfo) }) } - // only add new transaction if checkTx passes and is not pending - if !txmp.isPending(wtx) { - if err := txmp.addNewTransaction(wtx); err != nil { - return nil, err - } - } else { - // otherwise add to pending txs store - if err := txmp.canAddPendingTx(wtx); err != nil { - // TODO: eviction strategy for pending transactions - txmp.cache.Remove(txHash) - return nil, err - } - if err := txmp.pendingTxs.Insert(wtx); err != nil { - txmp.cache.Remove(txHash) - return nil, err - } + if err := wtx.check(constraints); err != nil { + // ignore bad transactions + logger.Info("rejected bad transaction", "priority", wtx.priority, "tx", wtx.Hash(), "post_check_err", err) + txmp.txStore.CachePush(hTx.Hash()) + txmp.metrics.FailedTxs.Add(1) + return nil, err } - txmp.addNonce(wtx) - return res.ResponseCheckTx, nil -} - -func (txmp *TxMempool) isInMempool(txHash types.TxHash) bool { - return !txmp.txStore.IsTxRemovedByHash(txHash) -} -func (txmp *TxMempool) HasTx(txHash types.TxHash) bool { - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() - return txmp.txStore.GetTxByHash(txHash) != nil -} + if err := txmp.txStore.Insert(wtx); err != nil { + txmp.metrics.RejectedTxs.Add(1) + return nil, err + } -func (txmp *TxMempool) GetTxsForHashes(txHashes []types.TxHash) types.Txs { - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() + txmp.metrics.InsertedTxs.Add(1) + txmp.metrics.TxSizeBytes.Add(float64(wtx.Size())) + txmp.metrics.Size.Set(float64(txmp.NumTxsNotPending())) + txmp.metrics.PendingSize.Set(float64(txmp.PendingSize())) + txmp.metrics.TotalTxsSizeBytes.Set(float64(txmp.TotalTxsBytesSize())) - txs := make([]types.Tx, 0, len(txHashes)) - for _, txHash := range txHashes { - wtx := txmp.txStore.GetTxByHash(txHash) - txs = append(txs, wtx.Tx()) - } - return txs + txmp.notifyTxsAvailable() + return res.ResponseCheckTx, nil } func (txmp *TxMempool) SafeGetTxsForHashes(txHashes []types.TxHash) (types.Txs, []types.TxHash) { - txmp.mtx.RLock() - defer txmp.mtx.RUnlock() - - txs := make([]types.Tx, 0, len(txHashes)) - missing := []types.TxHash{} - for _, txHash := range txHashes { - wtx := txmp.txStore.GetTxByHash(txHash) - if wtx == nil { - missing = append(missing, txHash) - continue - } - txs = append(txs, wtx.Tx()) - } - return txs, missing + return txmp.txStore.SafeGetTxsForHashes(txHashes) } -// Flush empties 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. +// Flush empties the mempool. func (txmp *TxMempool) Flush() { - txmp.mtx.Lock() - defer txmp.mtx.Unlock() - for _, wtx := range txmp.txStore.GetAllTxs() { - txmp.removeTx(wtx, false, false, true) - } - txmp.cache.Reset() + txmp.txStore.Clear() } -// ReapMaxBytesMaxGas returns a list of transactions within the provided size -// and gas constraints. The returned list starts with EVM transactions (in priority order), +// ReapTxs returns a list of transactions within the provided constraints and their total gas estimate. +// The returned list starts with EVM transactions (in priority order), // followed by non-EVM transactions (in priority order). // There are 4 types of constraints. // 1. maxBytes - stops pulling txs from mempool once maxBytes is hit. @@ -580,205 +421,16 @@ func (txmp *TxMempool) Flush() { // 3. maxGasEstimated - similar to maxGasWanted but will use the estimated gas used for EVM txs // while still using gas wanted for cosmos txs. Can be set to -1 to be ignored. // -// NOTE: -// - Transactions returned are not removed from the mempool transaction -// store or indexes. -func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimated int64) types.Txs { - txmp.mtx.Lock() - defer txmp.mtx.Unlock() - txs, _ := txmp.reapTxs(ReapLimits{ - MaxBytes: utils.Some(maxBytes), - MaxGasWanted: utils.Some(maxGasWanted), - MaxGasEstimated: utils.Some(maxGasEstimated), - }) - return txs -} - -type ReapLimits struct { - MaxTxs utils.Option[uint64] - MaxBytes utils.Option[int64] - MaxGasWanted utils.Option[int64] - MaxGasEstimated utils.Option[int64] -} - -// ReapMaxTxsBytesMaxGas returns a list of transactions within the provided tx, -// byte, and gas constraints together with the total estimated gas for the -// returned transactions. -// -// NOTE: Gas limits are enforced using int64 running totals. If those totals -// overflow, gas limit enforcement no longer works correctly. This preserves the -// historical behavior for backward compatibility. -func (txmp *TxMempool) reapTxs(l ReapLimits) (types.Txs, int64) { - maxTxs := l.MaxTxs.Or(utils.Max[uint64]()) - maxBytes := l.MaxBytes.Or(utils.Max[int64]()) - maxGasWanted := l.MaxGasWanted.Or(utils.Max[int64]()) - maxGasEstimated := l.MaxGasEstimated.Or(utils.Max[int64]()) - if maxBytes < 0 { - maxBytes = utils.Max[int64]() - } - if maxGasWanted < 0 { - maxGasWanted = utils.Max[int64]() - } - if maxGasEstimated < 0 { - maxGasEstimated = utils.Max[int64]() - } - var ( - totalGasWanted int64 - totalGasEstimated int64 - totalSize int64 - ) - - numTxs := uint64(0) - encounteredGasUnfit := false - if uint64(txmp.NumTxsNotPending()) < txmp.config.TxNotifyThreshold { //nolint:gosec // NumTxsNotPending returns non-negative value - // do not reap anything if threshold is not met - return []types.Tx{}, 0 - } - totalTxs := txmp.priorityIndex.NumTxs() - evmTxs := make([]types.Tx, 0, totalTxs) - nonEvmTxs := make([]types.Tx, 0, totalTxs) - txmp.priorityIndex.ForEachTx(func(wtx *WrappedTx) bool { - size := types.ComputeProtoSizeForTxs([]types.Tx{wtx.Tx()}) - - // bytes limit is a hard stop - if totalSize+size > maxBytes || numTxs+1 > maxTxs { - return false - } - - // if the tx doesn't have a gas estimate, fallback to gas wanted - var txGasEstimate int64 - if wtx.estimatedGas >= MinGasEVMTx && wtx.estimatedGas <= wtx.gasWanted { - txGasEstimate = wtx.estimatedGas - } else { - wtx.estimatedGas = wtx.gasWanted - txGasEstimate = wtx.gasWanted - } - - // prospective totals - prospectiveGasWanted := totalGasWanted + wtx.gasWanted - prospectiveGasEstimated := totalGasEstimated + txGasEstimate - - maxGasWantedExceeded := prospectiveGasWanted > maxGasWanted - maxGasEstimatedExceeded := prospectiveGasEstimated > maxGasEstimated - - if maxGasWantedExceeded || maxGasEstimatedExceeded { - // skip this unfit-by-gas tx once and attempt to pull up to 10 smaller ones - if !encounteredGasUnfit && numTxs < MinTxsToPeek { - encounteredGasUnfit = true - return true - } - return false - } - - // include tx and update totals - numTxs += 1 - totalSize += size - totalGasWanted = prospectiveGasWanted - totalGasEstimated = prospectiveGasEstimated - - if wtx.evm.IsPresent() { - evmTxs = append(evmTxs, wtx.Tx()) - } else { - nonEvmTxs = append(nonEvmTxs, wtx.Tx()) - } - if encounteredGasUnfit && numTxs >= MinTxsToPeek { - return false - } - return true - }) - - return append(evmTxs, nonEvmTxs...), totalGasEstimated -} - -// RemoveTxs removes the provided transactions from the mempool if present. -func (txmp *TxMempool) PopTxs(l ReapLimits) (types.Txs, int64) { - txmp.Lock() - defer txmp.Unlock() - txs, gasEstimated := txmp.reapTxs(l) - for _, tx := range txs { - if wtx := txmp.txStore.GetTxByHash(tx.Hash()); wtx != nil { - txmp.removeTx(wtx, false, false, true) - } - } - return txs, gasEstimated -} - -// ReapMaxTxs returns a list of transactions within the provided number of -// transactions bound. Transaction are retrieved in priority order. -// -// NOTE: -// - Transactions returned are not removed from the mempool transaction -// store or indexes. -func (txmp *TxMempool) ReapMaxTxs(max int) types.Txs { - txmp.mtx.Lock() - defer txmp.mtx.Unlock() - - wTxs := txmp.priorityIndex.PeekTxs(max) - txs := make([]types.Tx, 0, len(wTxs)) - for _, wtx := range wTxs { - txs = append(txs, wtx.Tx()) - } - if len(txs) < max { - // retrieve more from pending txs - pending := txmp.pendingTxs.Peek(max - len(txs)) - for _, ptx := range pending { - txs = append(txs, ptx.Tx()) - } - } - return txs +// NOTE: Transactions are removed from the mempool iff remove == true. +// Either way, the transactions stay in the LRU cache. +func (txmp *TxMempool) ReapTxs(limits ReapLimits, remove bool) (types.Txs, int64) { + return txmp.txStore.Reap(limits, remove) } // Update iterates over all the transactions provided by the block producer, // removes them from the cache (if applicable), and removes // the transactions from the main transaction store and associated indexes. -// If there are transactions remaining in the mempool, we initiate a -// re-CheckTx for them (if applicable), otherwise, we notify the caller more -// transactions are available. -// -// WARNING: callers should almost always pass recheck=false. recheck=true -// re-runs CheckTx on every tx still in the mempool after each block, and -// handleRecheckResult treats a "now pending" response as terminal: it -// evicts the tx and async-re-CheckTx-es it, which lands it back in -// pendingTxs. For chains whose antehandler returns pending for any -// ahead-of-nonce EVM tx (Sei), this evicts perfectly-valid queued txs. -// -// Example. txA (nonce 3), txB (nonce 2), txC (nonce 1) on the same sender. -// -// 1. txA, txB, txC are submitted in this order. -// 2. txA and txB enter pendingTxs (their nonce is ahead of the sender's -// expected nonce at CheckTx time so the EVM antehandler marks them -// pending). txC enters the priority index (its nonce matches expected). -// 3. Block 1 reaps and mines txC. The sender's expected nonce becomes 2. -// 4. handlePendingTransactions promotes txA and txB into the priority -// index. The per-sender evmQueue is now [txB (head), txA (tail)]. -// -// From step 5 onwards the recheck flag matters: -// -// recheck=false (correct): -// -// 5. updateReCheckTxs is skipped. The priority index keeps txB and txA. -// 6. Block 2 reaps the whole evmQueue. Both txB and txA mine. -// -// All 3 txs mine in 2 blocks, regardless of how out-of-order they arrived. -// -// recheck=true (broken): -// -// 5. updateReCheckTxs re-runs CheckTx on each tx in the priority index: -// - txB: nonce 2 == expected 2 → not pending → stays. -// - txA: nonce 3 > expected 2 → pending again. handleRecheckResult -// evicts it and async-re-CheckTx-es it, which lands it back in -// pendingTxs. -// 6. Block 2 reaps txB only (txA is no longer in the priority index). -// handlePendingTransactions re-promotes txA. txA's nonce now matches -// expected, so it survives the recheck this time. -// 7. Block 3 mines txA. -// -// All 3 txs take 3 blocks. With many out-of-order sequential nonces from -// one sender, this stalls the chain to 1-tx-per-block-per-sender throughput. -// -// CometBFT's default for ConsensusParams.ABCI.RecheckTx is false. Recheck -// primarily defended against state-dependent invalidation that modern -// chains catch in ProcessProposal/DeliverTx anyway. +// If recheck = true, CheckTx is called on all remaining transactions. // // NOTE: // - The caller must explicitly acquire a write-lock. @@ -787,481 +439,55 @@ func (txmp *TxMempool) Update( blockHeight int64, blockTxs types.Txs, execTxResult []*abci.ExecTxResult, - txConstraintsFetcher TxConstraintsFetcher, + txConstraints TxConstraints, recheck bool, ) error { + if blockHeight <= txmp.height { + return fmt.Errorf("blockHeight = %v, want > %v", blockHeight, txmp.height) + } txmp.height = blockHeight txmp.notifiedTxsAvailable.Store(false) - txmp.txConstraintsFetcher = txConstraintsFetcher - - for i, tx := range blockTxs { - txHash := tx.Hash() - if execTxResult[i].Code == abci.CodeTypeOK { - // add the valid committed transaction to the cache (if missing) - _ = txmp.cache.Push(txHash) - txmp.blockFailedTxs.Remove(txHash) - } else if !txmp.config.KeepInvalidTxsInCache { - if txmp.blockFailedTxs.Push(txHash) { - // First block failure: allow one retry - txmp.cache.Remove(txHash) - } - // Subsequent failures: leave in cache to prevent infinite re-entry - } - - // remove the committed transaction from the transaction store and indexes - if wtx := txmp.txStore.GetTxByHash(txHash); wtx != nil { - txmp.removeTx(wtx, false, false, true) - } - if execTxResult[i].EvmTxInfo != nil { - // remove any tx that has the same nonce (because the committed tx - // may be from block proposal and is never in the local mempool) - if wtx, _ := txmp.priorityIndex.TxByAddrNonce( - common.HexToAddress(execTxResult[i].EvmTxInfo.SenderAddress), - execTxResult[i].EvmTxInfo.Nonce, - ); wtx != nil { - txmp.removeTx(wtx, false, false, true) - } - } - } - - txmp.purgeExpiredTxs(blockHeight) - txmp.handlePendingTransactions() - - // 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 recheck { - logger.Debug( - "executing re-CheckTx for all remaining transactions", - "num_txs", txmp.Size(), - "height", blockHeight, - ) - txmp.updateReCheckTxs(ctx) - } else { - txmp.notifyTxsAvailable() - } - } - - txmp.metrics.Size.Set(float64(txmp.NumTxsNotPending())) - txmp.metrics.TotalTxsSizeBytes.Set(float64(txmp.TotalTxsBytesSize())) - txmp.metrics.PendingSize.Set(float64(txmp.PendingSize())) - return nil -} - -// addNewTransaction is invoked for a new unique transaction after CheckTx -// has been executed by the ABCI application for the first time on that transaction. -// CheckTx can be called again for the same transaction later when re-checking; -// however, this function will not be called. A recheck after a block is committed -// goes to handleRecheckResult. -// -// addNewTransaction runs after the ABCI application executes CheckTx. -// It runs the consensus-derived post-check for the current state snapshot. -// If the post-check reports an error, the transaction is rejected. Otherwise, -// we attempt to insert the transaction into the mempool. CheckTx response codes -// are filtered earlier in CheckTx. -// -// When inserting a transaction, we first check if there is sufficient capacity. -// If there is, the transaction is added to the txStore and 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. -// -// NOTE: -// - An explicit lock is NOT required. -func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx) error { - // Update transaction priority reservoir with the true Tx priority - // as determined by the application. - // - // NOTE: This is done before potentially rejecting the transaction due to - // mempool being full. This is to ensure that the reservoir contains a - // representative sample of all transactions that have been processed by - // CheckTx. - // - // However, this is NOT done if the tx is pending, since a spammer could - // throw off the correct priority percentiles otherwise. - // - // We do not use the priority hint here as it may be misleading and - // inaccurate. The true priority as determined by the application is the - // most accurate. - txmp.priorityReservoir.Add(wtx.priority) - err := txmp.checkResponseState(wtx) - if err != nil { - // ignore bad transactions - logger.Info( - "rejected bad transaction", - "priority", wtx.priority, - "tx", wtx.Hash(), - "post_check_err", err, - ) - txmp.metrics.FailedTxs.Add(1) - if !txmp.config.KeepInvalidTxsInCache { - txmp.cache.Remove(wtx.Hash()) - } - return err - } - if err := txmp.canAddTx(wtx); err != nil { - evictTxs := txmp.priorityIndex.GetEvictableTxs( - wtx.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.Hash()) - logger.Error( - "rejected incoming good transaction; mempool full", - "tx", wtx.Hash(), - "err", err, - ) - txmp.metrics.RejectedTxs.Add(1) - return nil - } - - // 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, true, true) - logger.Debug( - "evicted existing good transaction; mempool full", - "old_tx", fmt.Sprintf("%X", toEvict.Hash()), - "old_priority", toEvict.priority, - "new_tx", wtx.Hash(), - "new_priority", wtx.priority, - ) - txmp.metrics.EvictedTxs.Add(1) - } - } - - if txmp.isInMempool(wtx.Hash()) { - return nil + txmp.txConstraintsFetcher = func() (TxConstraints, error) { + return txConstraints, nil } - if txmp.insertTx(wtx) { - logger.Debug( - "inserted good transaction", - "priority", wtx.priority, - "tx", wtx.Hash(), - "height", txmp.height, - "num_txs", txmp.NumTxsNotPending(), - ) - txmp.notifyTxsAvailable() - } - - return nil -} - -// handleRecheckResult handles the responses from ABCI CheckTx calls issued -// during the recheck phase of a block Update. It removes any transactions -// invalidated by the application. -// -// The caller must hold 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 -// CheckTx. The order transactions are rechecked must be the same as -// the order in which this callback is called. -// -// This method is NOT executed for the initial CheckTx on a new transaction; -// that case is handled by addNewTransaction instead. -func (txmp *TxMempool) handleRecheckResult(tx types.Tx, res *abci.ResponseCheckTxV2) { - if txmp.recheckCursor == nil { - return - } - - txmp.metrics.RecheckTimes.Add(1) - - wtx := txmp.recheckCursor.Value() - - // Search through the remaining list of tx to recheck for a transaction that matches - // the one we received from the ABCI application. - for !bytes.Equal(tx, wtx.Tx()) { - - logger.Debug( - "re-CheckTx transaction mismatch", - "got", wtx.Hash(), - "expected", tx.Hash(), - ) - - 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() + txResults := map[types.TxHash]bool{} + for i, tx := range blockTxs { + txResults[tx.Hash()] = execTxResult[i].Code == abci.CodeTypeOK } - - // 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) { - err := txmp.checkResponseState(wtx) - if evm, ok := wtx.evm.Get(); ok { - evm.requiredBalance = new(big.Int).Set(res.EVMRequiredBalance) - wtx.evm = utils.Some(evm) - } - - // we will treat a transaction that turns pending in a recheck as invalid and evict it - if res.Code == abci.CodeTypeOK && err == nil && !txmp.isPending(wtx) { - wtx.priority = res.Priority - } else { - logger.Debug( - "existing transaction no longer valid; failed re-CheckTx callback", - "priority", wtx.priority, - "tx", wtx.Hash(), - "err", err, - "code", res.Code, - ) - - if wtx.gossipEl != txmp.recheckCursor { - panic("corrupted reCheckTx cursor") + newPriorities := map[types.TxHash]int64{} + invalidTxs := map[types.TxHash]bool{} + if recheck { + for _, wtx := range txmp.txStore.ReadyTxs() { + if _, ok := txResults[wtx.Hash()]; ok { + continue } - - txmp.removeTx(wtx, !txmp.config.KeepInvalidTxsInCache, true, true) - } - } - - // move reCheckTx cursor to next element - if txmp.recheckCursor == txmp.recheckEnd { - txmp.recheckCursor = nil - } else { - txmp.recheckCursor = txmp.recheckCursor.Next() - } - - if txmp.recheckCursor == nil { - logger.Debug("finished rechecking transactions") - - if txmp.NumTxsNotPending() > 0 { - txmp.notifyTxsAvailable() - } - } - - txmp.metrics.Size.Set(float64(txmp.NumTxsNotPending())) - txmp.metrics.PendingSize.Set(float64(txmp.PendingSize())) - txmp.metrics.TotalTxsSizeBytes.Set(float64(txmp.TotalTxsBytesSize())) -} - -// updateReCheckTxs updates the recheck cursors using the gossipIndex. For -// each transaction, it executes CheckTx. The global callback defined on -// the app 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(ctx context.Context) { - if txmp.Size() == 0 { - panic("attempted to update re-CheckTx txs when mempool is empty") - } - logger.Debug( - "executing re-CheckTx for all remaining transactions", - "num_txs", txmp.Size(), - "height", txmp.height, - ) - - txmp.recheckCursor = txmp.gossipIndex.Front() - txmp.recheckEnd = txmp.gossipIndex.Back() - - for e := txmp.gossipIndex.Front(); e != nil; e = e.Next() { - wtx := e.Value() - - // Only execute CheckTx if the transaction is not marked as removed which - // could happen if the transaction was evicted. - if !txmp.txStore.IsTxRemoved(wtx) { + txmp.metrics.RecheckTimes.Add(1) res, err := txmp.app.CheckTxSafe(ctx, &abci.RequestCheckTxV2{ Tx: wtx.Tx(), Type: abci.CheckTxTypeV2Recheck, }) - if err == nil { - err = res.Err() - } - if err != nil { - // no need in retrying since the tx will be rechecked after the next block - - logger.Debug("failed to execute CheckTx during recheck", "err", err, "hash", wtx.Hash()) - continue + if err != nil || !res.IsOK() { + invalidTxs[wtx.Hash()] = true + } else { + // If succeeds, we just care about the new priority. + newPriorities[wtx.Hash()] = res.Priority } - txmp.handleRecheckResult(wtx.Tx(), res) } } -} - -// canAddTx returns an error if we cannot insert the provided *WrappedTx into -// the mempool due to mempool configured constraints. If it returns nil, -// the transaction can be inserted into the mempool. -func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { - var ( - numTxs = txmp.NumTxsNotPending() - sizeBytes = txmp.SizeBytes() - ) - - if numTxs >= txmp.config.Size || int64(wtx.Size())+sizeBytes > txmp.config.MaxTxsBytes { - return fmt.Errorf("mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", - numTxs, - txmp.config.Size, - sizeBytes, - txmp.config.MaxTxsBytes, - ) - } - - return nil -} - -func (txmp *TxMempool) canAddPendingTx(wtx *WrappedTx) error { - var ( - numTxs = txmp.PendingSize() - sizeBytes = txmp.PendingSizeBytes() - ) - - if numTxs >= txmp.config.PendingSize || int64(wtx.Size())+sizeBytes > txmp.config.MaxPendingTxsBytes { - return fmt.Errorf("mempool pending set is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", - numTxs, - txmp.config.PendingSize, - sizeBytes, - txmp.config.MaxPendingTxsBytes, - ) - } - - return nil -} - -func (txmp *TxMempool) insertTx(wtx *WrappedTx) bool { - replacedTx, inserted := txmp.priorityIndex.PushTx(wtx) - if !inserted { - return false - } - txmp.metrics.TxSizeBytes.Add(float64(wtx.Size())) + txmp.txStore.Update(updateSpec{ + Now: time.Now(), + Height: blockHeight, + TxResults: txResults, + NewPriorities: newPriorities, + InvalidTxs: invalidTxs, + Constraints: txConstraints, + }) + txmp.notifyTxsAvailable() txmp.metrics.Size.Set(float64(txmp.NumTxsNotPending())) - txmp.metrics.PendingSize.Set(float64(txmp.PendingSize())) txmp.metrics.TotalTxsSizeBytes.Set(float64(txmp.TotalTxsBytesSize())) - - if replacedTx != nil { - txmp.removeTx(replacedTx, true, false, false) - } - - txmp.txStore.SetTx(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 - - txmp.metrics.InsertedTxs.Add(1) - return true -} - -func (txmp *TxMempool) removeTx(wtx *WrappedTx, removeFromCache bool, shouldReenqueue bool, updatePriorityIndex bool) { - if txmp.txStore.IsTxRemoved(wtx) { - return - } - - txmp.removeNonce(wtx) - txmp.txStore.RemoveTx(wtx) - toBeReenqueued := []*WrappedTx{} - if updatePriorityIndex { - toBeReenqueued = txmp.priorityIndex.RemoveTx(wtx, shouldReenqueue) - } - - // 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() - - txmp.metrics.RemovedTxs.Add(1) - if removeFromCache { - txmp.cache.Remove(wtx.Hash()) - } - - if shouldReenqueue { - for _, reenqueue := range toBeReenqueued { - txmp.removeTx(reenqueue, removeFromCache, false, true) - } - for _, reenqueue := range toBeReenqueued { - rtx := reenqueue.Tx() - go func() { - if _, err := txmp.CheckTx(context.Background(), rtx, TxInfo{}); err != nil { - logger.Error("failed to reenqueue transaction", "tx-hash", rtx.Hash(), "err", err) - } - }() - } - } -} - -func (txmp *TxMempool) expire(blockHeight int64, wtx *WrappedTx) { - txmp.metrics.ExpiredTxs.Add(1) - txmp.logExpiredTx(blockHeight, wtx) - if !txmp.config.KeepInvalidTxsInCache { - txmp.cache.Remove(wtx.Hash()) - } -} - -func (txmp *TxMempool) logExpiredTx(blockHeight int64, wtx *WrappedTx) { - // defensive check - if wtx == nil { - return - } - - logger.Info( - "transaction expired", - "priority", wtx.priority, - "tx", wtx.Hash(), - "address", func() string { - evm, ok := wtx.evm.Get() - if !ok { - return "" - } - return evm.address.Hex() - }(), - "evm", wtx.evm.IsPresent(), - "nonce", wtx.EVMNonce(), - "height", blockHeight, - "tx_height", wtx.height, - "tx_timestamp", wtx.timestamp, - "age", time.Since(wtx.timestamp), - ) -} - -// 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, but preserved in the cache (except for pending txs). -// -// 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() - - minHeight := utils.None[int64]() - if n := txmp.config.TTLNumBlocks; n > 0 && blockHeight > n { - minHeight = utils.Some(blockHeight - n) - } - minTime := utils.None[time.Time]() - if d := txmp.config.TTLDuration; d > 0 { - minTime = utils.Some(time.Now().Add(-d)) - } - expiredTxs := txmp.txStore.GetOlderThan(minTime, minHeight) - - for _, wtx := range expiredTxs { - if txmp.config.RemoveExpiredTxsFromQueue { - txmp.removeTx(wtx, !txmp.config.KeepInvalidTxsInCache, false, true) - } else { - txmp.expire(blockHeight, wtx) - } - } - - // remove pending txs that have expired - txmp.pendingTxs.PurgeExpired(blockHeight, now, func(wtx *WrappedTx) { - txmp.removeNonce(wtx) - txmp.expire(blockHeight, wtx) - }) + txmp.metrics.PendingSize.Set(float64(txmp.PendingSize())) + return nil } func (txmp *TxMempool) notifyTxsAvailable() { @@ -1275,46 +501,6 @@ func (txmp *TxMempool) notifyTxsAvailable() { } } -func (txmp *TxMempool) isPending(wtx *WrappedTx) bool { - evm, ok := wtx.evm.Get() - if !ok { - return false - } - if evm.nonce > txmp.EvmNextPendingNonce(evm.address) { - return true - } - balance := txmp.app.EvmBalance(evm.address, evm.seiAddress) - return balance.Cmp(evm.requiredBalance) < 0 -} - -func (txmp *TxMempool) handlePendingTransactions() { - accepted, rejected := txmp.pendingTxs.EvaluatePendingTransactions(func(wtx *WrappedTx) abci.PendingTxCheckerResponse { - evm, ok := wtx.evm.Get() - if !ok { - return abci.Accepted - } - if evm.nonce < txmp.app.EvmNonce(evm.address) { - return abci.Rejected - } - if txmp.isPending(wtx) { - return abci.Pending - } - return abci.Accepted - }) - for _, tx := range accepted { - if err := txmp.addNewTransaction(tx); err != nil { - txmp.removeNonce(tx) - logger.Error("error adding pending transaction", "err", err) - } - } - for _, tx := range rejected { - txmp.removeNonce(tx) - if !txmp.config.KeepInvalidTxsInCache { - txmp.cache.Remove(tx.Hash()) - } - } -} - // Run executes mempool background tasks. func (txmp *TxMempool) Run(ctx context.Context) error { c, ok := txmp.duplicateTxsCache.Get() diff --git a/sei-tendermint/internal/mempool/mempool_bench_test.go b/sei-tendermint/internal/mempool/mempool_bench_test.go index 67b1f35a7a..b0f1f302c0 100644 --- a/sei-tendermint/internal/mempool/mempool_bench_test.go +++ b/sei-tendermint/internal/mempool/mempool_bench_test.go @@ -6,10 +6,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" ) func BenchmarkTxMempool_CheckTx(b *testing.B) { @@ -20,8 +19,10 @@ func BenchmarkTxMempool_CheckTx(b *testing.B) { // setup the cache and the mempool number for hitting GetEvictableTxs during the // benchmark. 5000 is the current default mempool size in the TM config. - txmp := setup(b, proxyClient, 10000, NopTxConstraintsFetcher) - txmp.config.Size = 5000 + cfg := TestConfig() + cfg.CacheSize = 10000 + cfg.Size = 5000 + txmp := setup(cfg, proxyClient, NopTxConstraintsFetcher) rng := rand.New(rand.NewSource(time.Now().UnixNano())) const peerID = 1 @@ -36,11 +37,10 @@ func BenchmarkTxMempool_CheckTx(b *testing.B) { priority := int64(rng.Intn(9999-1000) + 1000) tx := []byte(fmt.Sprintf("sender-%d-%d=%X=%d", n, peerID, prefix, priority)) - txInfo := TxInfo{SenderID: uint16(peerID)} b.StartTimer() - _, err = txmp.CheckTx(ctx, tx, txInfo) + _, err = txmp.CheckTx(ctx, tx) require.NoError(b, err) } } diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 0b7c419daa..eddf66c679 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -14,7 +14,6 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/code" @@ -22,6 +21,7 @@ import ( abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -42,6 +42,14 @@ type testTx struct { var DefaultGasEstimated = int64(1) var DefaultGasWanted = int64(1) +func (app *application) EvmNonce(common.Address) uint64 { + return 0 +} + +func (app *application) EvmBalance(common.Address, []byte) *big.Int { + return big.NewInt(0) +} + func (app *application) CheckTx(_ context.Context, req *abci.RequestCheckTxV2) *abci.ResponseCheckTxV2 { var priority int64 @@ -134,23 +142,18 @@ func (app *application) GetTxPriorityHint(context.Context, *abci.RequestGetTxPri }, nil } -func setup(t testing.TB, app *proxy.Proxy, cacheSize int, txConstraintsFetcher TxConstraintsFetcher) *TxMempool { - t.Helper() - - cfg := TestConfig() - cfg.CacheSize = cacheSize +func setup(cfg *Config, app *proxy.Proxy, txConstraintsFetcher TxConstraintsFetcher) *TxMempool { return NewTxMempool(cfg, app, NopMetrics(), txConstraintsFetcher) } -func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { +func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int) []testTx { t.Helper() txs := make([]testTx, numTxs) - txInfo := TxInfo{SenderID: peerID} rng := rand.New(rand.NewSource(time.Now().UnixNano())) - for i := 0; i < numTxs; i++ { + for i := range numTxs { prefix := make([]byte, 20) _, err := rng.Read(prefix) require.NoError(t, err) @@ -158,32 +161,53 @@ func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, pe priority := int64(rng.Intn(9999-1000) + 1000) txs[i] = testTx{ - tx: []byte(fmt.Sprintf("sender-%d-%d=%X=%d", i, peerID, prefix, priority)), + tx: []byte(fmt.Sprintf("sender-%d=%X=%d", i, prefix, priority)), priority: priority, } - _, err = txmp.CheckTx(ctx, txs[i].tx, txInfo) + _, err = txmp.CheckTx(ctx, txs[i].tx) require.NoError(t, err) } return txs } -func convertTex(in []testTx) types.Txs { - out := make([]types.Tx, len(in)) +func totalTxSizeBytes(txs []testTx) uint64 { + var total uint64 + for _, tx := range txs { + total += uint64(len(tx.tx)) + } + return total +} - for idx := range in { - out[idx] = in[idx].tx +func totalRawTxSizeBytes(txs []types.Tx) uint64 { + var total uint64 + for _, tx := range txs { + total += uint64(len(tx)) } + return total +} - return out +func expectedReapCountByBytes(txs []testTx, maxBytes int64) int { + var total int64 + count := 0 + for _, tx := range txs { + txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx.tx}) + if maxBytes-total < txSize { + break + } + total += txSize + count++ + } + return count } func TestTxMempool_TxsAvailable(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) ensureNoTxFire := func() { timer := time.NewTimer(500 * time.Millisecond) @@ -208,7 +232,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) { // Execute CheckTx for some transactions and ensure TxsAvailable only fires // once. - txs := checkTxs(ctx, t, txmp, 100, 0) + txs := checkTxs(ctx, t, txmp, 100) ensureTxFire() ensureNoTxFire() @@ -218,20 +242,20 @@ func TestTxMempool_TxsAvailable(t *testing.T) { } responses := make([]*abci.ExecTxResult, len(rawTxs[:50])) - for i := 0; i < len(responses); i++ { + for i := range responses { responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK} } // commit half the transactions and ensure we fire an event txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, txmp.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) 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(ctx, t, txmp, 100, 0) + _ = checkTxs(ctx, t, txmp, 100) ensureNoTxFire() } @@ -239,12 +263,13 @@ func TestTxMempool_Size(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - txs := checkTxs(ctx, t, txmp, 100, 0) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) + txs := checkTxs(ctx, t, txmp, 100) require.Equal(t, len(txs), txmp.Size()) require.Equal(t, 0, txmp.PendingSize()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(txs), txmp.SizeBytes()) rawTxs := make([]types.Tx, len(txs)) for i, tx := range txs { @@ -252,27 +277,28 @@ func TestTxMempool_Size(t *testing.T) { } responses := make([]*abci.ExecTxResult, len(rawTxs[:50])) - for i := 0; i < len(responses); i++ { + for i := range responses { responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK} } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, txmp.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() require.Equal(t, len(rawTxs)/2, txmp.Size()) - require.Equal(t, int64(2850), txmp.SizeBytes()) + require.Equal(t, totalRawTxSizeBytes(rawTxs[50:]), txmp.SizeBytes()) } func TestTxMempool_Flush(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - txs := checkTxs(ctx, t, txmp, 100, 0) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) + txs := checkTxs(ctx, t, txmp, 100) require.Equal(t, len(txs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(txs), txmp.SizeBytes()) rawTxs := make([]types.Tx, len(txs)) for i, tx := range txs { @@ -280,17 +306,17 @@ func TestTxMempool_Flush(t *testing.T) { } responses := make([]*abci.ExecTxResult, len(rawTxs[:50])) - for i := 0; i < len(responses); i++ { + for i := range responses { responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK} } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, txmp.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() txmp.Flush() require.Zero(t, txmp.Size()) - require.Equal(t, int64(0), txmp.SizeBytes()) + require.Zero(t, txmp.SizeBytes()) } func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { @@ -298,11 +324,12 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { gasEstimated := int64(1) // gas estimated set to 1 client := &application{Application: kvstore.NewApplication(), gasEstimated: &gasEstimated} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - tTxs := checkTxs(ctx, t, txmp, 100, 0) // all txs request 1 gas unit + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) + tTxs := checkTxs(ctx, t, txmp, 100) // all txs request 1 gas unit require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) txMap := make(map[types.TxHash]testTx) priorities := make([]int64, len(tTxs)) @@ -316,6 +343,11 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { return priorities[i] > priorities[j] }) + sortedTxs := append([]testTx(nil), tTxs...) + sort.Slice(sortedTxs, func(i, j int) bool { + return sortedTxs[i].priority > sortedTxs[j].priority + }) + ensurePrioritized := func(reapedTxs types.Txs) { reapedPriorities := make([]int64, len(reapedTxs)) for i, rTx := range reapedTxs { @@ -328,58 +360,51 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { var wg sync.WaitGroup // reap by gas capacity only - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), 50, utils.Max[int64]()) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxGasWanted: utils.Some(int64(50))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) require.Len(t, reapedTxs, 50) - }() + }) // reap by transaction bytes only - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(1000, utils.Max[int64](), utils.Max[int64]()) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxBytes: utils.Some(int64(1000))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) - require.GreaterOrEqual(t, len(reapedTxs), 16) - }() + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) + require.Len(t, reapedTxs, expectedReapCountByBytes(sortedTxs, 1000)) + }) // Reap by both transaction bytes and gas, where the size yields 31 reaped // transactions and the gas limit reaps 25 transactions. - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(1500, 30, utils.Max[int64]()) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{ + MaxBytes: utils.Some(int64(1500)), + MaxGasWanted: utils.Some(int64(30)), + }, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) - require.Len(t, reapedTxs, 25) - }() + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) + require.Len(t, reapedTxs, min(expectedReapCountByBytes(sortedTxs, 1500), 30)) + }) // Reap by min transactions in block regardless of gas limit. - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), 2, utils.Max[int64]()) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxGasWanted: utils.Some(int64(2))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Len(t, reapedTxs, 2) - }() + }) // Reap by max gas estimated - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 50) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxGasEstimated: utils.Some(int64(50))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Len(t, reapedTxs, 50) - }() + }) wg.Wait() } @@ -389,9 +414,10 @@ func TestTxMempool_ReapMaxBytesMaxGas_FallbackToGasWanted(t *testing.T) { gasEstimated := int64(0) // gas estimated not set so fallback to gas wanted client := &application{Application: kvstore.NewApplication(), gasEstimated: &gasEstimated} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - tTxs := checkTxs(ctx, t, txmp, 100, 0) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) + tTxs := checkTxs(ctx, t, txmp, 100) txMap := make(map[types.TxHash]testTx) priorities := make([]int64, len(tTxs)) @@ -416,14 +442,12 @@ func TestTxMempool_ReapMaxBytesMaxGas_FallbackToGasWanted(t *testing.T) { var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 50) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxGasEstimated: utils.Some(int64(50))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Len(t, reapedTxs, 50) - }() + }) wg.Wait() } @@ -432,11 +456,12 @@ func TestTxMempool_ReapMaxTxs(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - tTxs := checkTxs(ctx, t, txmp, 100, 0) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) + tTxs := checkTxs(ctx, t, txmp, 100) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) txMap := make(map[types.TxHash]testTx) priorities := make([]int64, len(tTxs)) @@ -462,37 +487,31 @@ func TestTxMempool_ReapMaxTxs(t *testing.T) { var wg sync.WaitGroup // reap all transactions - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxTxs(utils.Max[int]()) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) require.Len(t, reapedTxs, len(tTxs)) - }() + }) // reap a single transaction - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxTxs(1) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxTxs: utils.Some(uint64(1))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) require.Len(t, reapedTxs, 1) - }() + }) // reap half of the transactions - wg.Add(1) - go func() { - defer wg.Done() - reapedTxs := txmp.ReapMaxTxs(len(tTxs) / 2) + wg.Go(func() { + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxTxs: utils.Some(uint64(len(tTxs) / 2))}, false) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) - require.Equal(t, int64(5690), txmp.SizeBytes()) + require.Equal(t, totalTxSizeBytes(tTxs), txmp.SizeBytes()) require.Len(t, reapedTxs, len(tTxs)/2) - }() + }) wg.Wait() } @@ -504,19 +523,19 @@ func TestTxMempool_ReapMaxBytesMaxGas_MinGasEVMTxThreshold(t *testing.T) { gasEstimated := int64(10000) gasWanted := int64(50000) client := &application{Application: kvstore.NewApplication(), gasEstimated: &gasEstimated, gasWanted: &gasWanted} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - peerID := uint16(1) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) address := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" // Insert a single EVM tx (format: evm-sender=account=priority=nonce) - _, err := txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address, 100, 0)), TxInfo{SenderID: peerID}) + _, err := txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address, 100, 0))) require.NoError(t, err) require.Equal(t, 1, txmp.Size()) // With MinGasEVMTx=21000, estimatedGas (10000) is ignored and we fallback to gasWanted (50000). // Setting maxGasEstimated below gasWanted should therefore result in 0 reaped txs. - reaped := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 40000) + reaped, _ := txmp.ReapTxs(ReapLimits{MaxGasEstimated: utils.Some(int64(40000))}, false) require.Len(t, reaped, 0) // Note: If MinGasEVMTx is changed to 0, the same scenario would use estimatedGas (10000) @@ -527,104 +546,33 @@ func TestTxMempool_CheckTxExceedsMaxSize(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) rng := rand.New(rand.NewSource(time.Now().UnixNano())) tx := make([]byte, txmp.config.MaxTxBytes+1) _, err := rng.Read(tx) require.NoError(t, err) - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.Error(t, err) tx = make([]byte, txmp.config.MaxTxBytes-1) _, err = rng.Read(tx) require.NoError(t, err) - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.NoError(t, err) } -func TestTxMempool_Reap_SkipGasUnfitAndCollectMinTxs(t *testing.T) { - ctx := t.Context() - - app := &application{Application: kvstore.NewApplication()} - client := app - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - peerID := uint16(1) - - // Insert one high-priority tx that is unfit by gas (exceeds maxGasEstimated) - gwBig := int64(100) - geBig := int64(100) - app.gasWanted = &gwBig - app.gasEstimated = &geBig - bigTx := []byte(fmt.Sprintf("sender-big=key=%d", 1000000)) - _, err := txmp.CheckTx(ctx, bigTx, TxInfo{SenderID: peerID}) - require.NoError(t, err) - - // Now insert many small, lower-priority txs that fit well under the gas limit - gwSmall := int64(1) - geSmall := int64(1) - app.gasWanted = &gwSmall - app.gasEstimated = &geSmall - for i := 0; i < 50; i++ { - tx := []byte(fmt.Sprintf("sender-%d=key=%d", i, 1000-i)) - _, err := txmp.CheckTx(ctx, tx, TxInfo{SenderID: peerID}) - require.NoError(t, err) - } - - // Reap with a maxGasEstimated that makes the first tx unfit but allows many small txs - reaped := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 50) - require.Len(t, reaped, MinTxsToPeek) - - // Ensure all reaped small txs are under gas constraint - for _, rtx := range reaped { - _ = rtx // gas constraints are enforced by ReapMaxBytesMaxGas; count assertion suffices here - } -} - -func TestTxMempool_Reap_SkipGasUnfitStopsAtMinEvenWithCapacity(t *testing.T) { - ctx := t.Context() - - app := &application{Application: kvstore.NewApplication()} - client := app - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - peerID := uint16(1) - - // First tx: unfit by gas (bigger than limit), highest priority - gwBig := int64(100) - geBig := int64(100) - app.gasWanted = &gwBig - app.gasEstimated = &geBig - bigTx := []byte(fmt.Sprintf("sender-big=key=%d", 1000000)) - _, err := txmp.CheckTx(ctx, bigTx, TxInfo{SenderID: peerID}) - require.NoError(t, err) - - // Insert many small txs that fit; plenty of capacity for more than 10 - gwSmall := int64(1) - geSmall := int64(1) - app.gasWanted = &gwSmall - app.gasEstimated = &geSmall - for i := 0; i < 100; i++ { - tx := []byte(fmt.Sprintf("sender-sm-%d=key=%d", i, 2000-i)) - _, err := txmp.CheckTx(ctx, tx, TxInfo{SenderID: peerID}) - require.NoError(t, err) - } - - // Make the gas limit very small so the first (big) tx is unfit and we only collect MinTxsPerBlock - reaped := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 10) - require.Len(t, reaped, MinTxsToPeek) -} - func TestTxMempool_Prioritization(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 100, NopTxConstraintsFetcher) - peerID := uint16(1) + cfg := TestConfig() + cfg.CacheSize = 100 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) address1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" address2 := "0xfD23B3A9DE15e92B9ef9540E587B3661E15A12fA" @@ -652,53 +600,25 @@ func TestTxMempool_Prioritization(t *testing.T) { rng.Shuffle(len(txsCopy), func(i, j int) { txsCopy[i], txsCopy[j] = txsCopy[j], txsCopy[i] }) - txs = [][]byte{ - []byte(fmt.Sprintf("sender-0-1=peer=%d", 9)), - []byte(fmt.Sprintf("sender-1-1=peer=%d", 8)), - []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 7, 0)), - []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 9, 1)), - []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address2, 6, 0)), - []byte(fmt.Sprintf("sender-2-1=peer=%d", 5)), - []byte(fmt.Sprintf("sender-3-1=peer=%d", 4)), - } txsCopy = append(txsCopy, evmTxs...) for i := range txsCopy { - _, err := txmp.CheckTx(ctx, txsCopy[i], TxInfo{SenderID: peerID}) + _, err := txmp.CheckTx(ctx, txsCopy[i]) require.NoError(t, err) } - // Reap the transactions - reapedTxs := txmp.ReapMaxTxs(len(txs)) - // Check if the reaped transactions are in the correct order of their priorities - for _, tx := range txs { - fmt.Printf("expected: %s\n", string(tx)) - } - fmt.Println("**************") - for _, reapedTx := range reapedTxs { - fmt.Printf("received: %s\n", string(reapedTx)) - } - for i, reapedTx := range reapedTxs { - require.Equal(t, txs[i], []byte(reapedTx)) + expectedReapedTxs := types.Txs{ + []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 7, 0)), + []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 9, 1)), + []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address2, 6, 0)), + []byte(fmt.Sprintf("sender-0-1=peer=%d", 9)), + []byte(fmt.Sprintf("sender-1-1=peer=%d", 8)), + []byte(fmt.Sprintf("sender-2-1=peer=%d", 5)), + []byte(fmt.Sprintf("sender-3-1=peer=%d", 4)), } -} -func TestTxMempool_PendingStoreSize(t *testing.T) { - ctx := t.Context() - - client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 100, NopTxConstraintsFetcher) - txmp.config.PendingSize = 1 - peerID := uint16(1) - - address1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" - - _, err := txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 1, 1)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - _, err = txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 1, 2)), TxInfo{SenderID: peerID}) - require.Error(t, err) - require.Contains(t, err.Error(), "mempool pending set is full") + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxTxs: utils.Some(uint64(len(expectedReapedTxs)))}, false) + require.Equal(t, expectedReapedTxs, reapedTxs) } func TestTxMempool_RemoveCacheWhenPendingTxIsFull(t *testing.T) { @@ -706,84 +626,46 @@ func TestTxMempool_RemoveCacheWhenPendingTxIsFull(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 10, NopTxConstraintsFetcher) - txmp.config.PendingSize = 1 - peerID := uint16(1) - address1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" - _, err := txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 1, 1)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - _, err = txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 1, 2)), TxInfo{SenderID: peerID}) - require.Error(t, err) - txCache := txmp.cache.(*LRUTxCache) - // Make sure the second tx is removed from cache - require.Equal(t, 1, len(txCache.cacheMap)) -} - -func TestTxMempool_EVMEviction(t *testing.T) { - ctx := t.Context() - - client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 100, NopTxConstraintsFetcher) - txmp.config.Size = 1 - peerID := uint16(1) - - address1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" - address2 := "0xfD23B3A9DE15e92B9ef9540E587B3661E15A12fA" - - // Add first transaction with priority 1 - _, err := txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 1, 0)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - - // This should evict the previous tx (priority 1 < priority 2) - _, err = txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 2, 0)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - require.Equal(t, 1, txmp.priorityIndex.NumTxs()) - require.Equal(t, int64(2), txmp.priorityIndex.txs[0].priority) - - // Increase mempool size to 2 - txmp.config.Size = 2 - _, err = txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address1, 3, 1)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - require.Equal(t, 0, txmp.pendingTxs.Size()) - require.Equal(t, 2, txmp.priorityIndex.NumTxs()) - - // This would evict the tx with priority 2 and cause the tx with priority 3 to go pending - _, err = txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address2, 4, 0)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - - require.Eventually(t, func() bool { - return txmp.priorityIndex.NumTxs() == 1 && txmp.pendingTxs.Size() == 1 - }, 5*time.Second, 100*time.Millisecond, "Expected mempool state not reached") - - // Verify final state - require.Equal(t, 1, txmp.priorityIndex.NumTxs()) - require.Equal(t, 1, txmp.pendingTxs.Size()) - - tx := txmp.priorityIndex.txs[0] - require.Equal(t, int64(4), tx.priority) // Should be the highest priority transaction - - _, err = txmp.CheckTx(ctx, []byte(fmt.Sprintf("evm-sender=%s=%d=%d", address2, 5, 1)), TxInfo{SenderID: peerID}) - require.NoError(t, err) - require.Equal(t, 2, txmp.priorityIndex.NumTxs()) + cfg := TestConfig() + cfg.CacheSize = 100 + cfg.Size = 5 + cfg.PendingSize = 0 + txmp := NewTxMempool(cfg, proxy.New(client, proxy.NopMetrics()), NopMetrics(), NopTxConstraintsFetcher) + + insertedTxs := make([]types.Tx, 0, 2*cfg.Size+1) + pruned := false + for i := range 100 { + tx := types.Tx(fmt.Appendf(nil, "sender-%d=peer=%d", i, i)) + insertedTxs = append(insertedTxs, tx) + expectedSize := len(insertedTxs) + _, err := txmp.CheckTx(ctx, tx) + if err != nil { + require.ErrorIs(t, err, errMempoolFull) + } + if txmp.Size() < expectedSize { + pruned = true + break + } + } - txmp.removeTx(tx, true, false, true) - // Should not reenqueue - require.Equal(t, 1, txmp.priorityIndex.NumTxs()) + require.True(t, pruned) + require.LessOrEqual(t, txmp.Size(), cfg.Size) + require.Positive(t, txmp.Size()) - require.Eventually(t, func() bool { - return txmp.pendingTxs.Size() == 1 - }, 5*time.Second, 100*time.Millisecond, "Expected pendingTxs size not reached") - require.Equal(t, 1, txmp.pendingTxs.Size()) + for _, tx := range insertedTxs { + _, inMempool := txmp.txStore.ByHash(tx.Hash()) + inCache := txmp.txStore.CacheHas(tx.Hash()) + require.Equal(t, inMempool, inCache) + } } -func TestTxMempool_CheckTxSamePeer(t *testing.T) { +func TestTxMempool_CheckTxDuplicateRejected(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 100, NopTxConstraintsFetcher) - peerID := uint16(1) + cfg := TestConfig() + cfg.CacheSize = 100 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) rng := rand.New(rand.NewSource(time.Now().UnixNano())) prefix := make([]byte, 20) @@ -792,9 +674,9 @@ func TestTxMempool_CheckTxSamePeer(t *testing.T) { tx := []byte(fmt.Sprintf("sender-0=%X=%d", prefix, 50)) - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: peerID}) + _, err = txmp.CheckTx(ctx, tx) require.NoError(t, err) - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: peerID}) + _, err = txmp.CheckTx(ctx, tx) require.Error(t, err) } @@ -802,8 +684,9 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 100, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 100 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) rng := rand.New(rand.NewSource(time.Now().UnixNano())) checkTxDone := make(chan struct{}) @@ -812,7 +695,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { wg.Add(1) go func() { for i := 0; i < 20; i++ { - _ = checkTxs(ctx, t, txmp, 100, 0) + _ = checkTxs(ctx, t, txmp, 100) dur := rng.Intn(1000-500) + 500 time.Sleep(time.Duration(dur) * time.Millisecond) } @@ -830,7 +713,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { var height int64 = 1 for range ticker.C { - reapedTxs := txmp.ReapMaxTxs(200) + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxTxs: utils.Some(uint64(200))}, false) if len(reapedTxs) > 0 { responses := make([]*abci.ExecTxResult, len(reapedTxs)) for i := 0; i < len(responses); i++ { @@ -846,7 +729,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, txmp.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() height++ @@ -871,28 +754,30 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 500, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 500 + cfg.TTLNumBlocks = utils.Some(int64(10)) + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) txmp.height = 100 - txmp.config.TTLNumBlocks = 10 - tTxs := checkTxs(ctx, t, txmp, 100, 0) + tTxs := checkTxs(ctx, t, txmp, 100) require.Equal(t, len(tTxs), txmp.Size()) // reap 5 txs at the next height -- no txs should expire - reapedTxs := txmp.ReapMaxTxs(5) + reapedTxs, _ := txmp.ReapTxs(ReapLimits{MaxTxs: utils.Some(uint64(5))}, false) responses := make([]*abci.ExecTxResult, len(reapedTxs)) for i := 0; i < len(responses); i++ { responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK} } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, txmp.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() require.Equal(t, 95, txmp.Size()) // check more txs at height 101 - _ = checkTxs(ctx, t, txmp, 50, 1) + _ = checkTxs(ctx, t, txmp, 50) require.Equal(t, 145, txmp.Size()) // Reap 5 txs at a height that would expire all the transactions from before @@ -903,14 +788,14 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { // 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) + reapedTxs, _ = txmp.ReapTxs(ReapLimits{MaxTxs: utils.Some(uint64(5))}, false) responses = make([]*abci.ExecTxResult, len(reapedTxs)) for i := 0; i < len(responses); i++ { responses[i] = &abci.ExecTxResult{Code: abci.CodeTypeOK} } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, txmp.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() require.GreaterOrEqual(t, txmp.Size(), 45) @@ -921,27 +806,32 @@ func TestMempoolExpiration(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - txmp.config.TTLDuration = time.Nanosecond // we want tx to expire immediately - txmp.config.RemoveExpiredTxsFromQueue = true - txs := checkTxs(ctx, t, txmp, 100, 0) - require.Equal(t, len(txs), txmp.priorityIndex.Len()) - require.Equal(t, len(txs), txmp.txStore.Size()) + cfg := TestConfig() + cfg.CacheSize = 0 + cfg.TTLDuration = utils.Some(time.Nanosecond) + cfg.RemoveExpiredTxsFromQueue = true + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) + txs := checkTxs(ctx, t, txmp, 100) + require.Equal(t, len(txs), txmp.Size()) + time.Sleep(time.Millisecond) - txmp.purgeExpiredTxs(txmp.height) - require.Equal(t, 0, txmp.priorityIndex.Len()) - require.Equal(t, 0, txmp.txStore.Size()) + + txmp.Lock() + require.NoError(t, txmp.Update(ctx, 1, nil, nil, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) + txmp.Unlock() + + require.Equal(t, 0, txmp.Size()) } -// TestReapMaxBytesMaxGas_EVMFirst verifies that ReapMaxBytesMaxGas returns +// TestTxMempool_ReapTxs_EVMFirst verifies that ReapTxs returns // EVM transactions first, followed by non-EVM transactions. -func TestReapMaxBytesMaxGas_EVMFirst(t *testing.T) { +func TestTxMempool_ReapTxs_EVMFirst(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - - txmp := setup(t, proxy.New(client, proxy.NopMetrics()), 0, NopTxConstraintsFetcher) - peerID := uint16(1) + cfg := TestConfig() + cfg.CacheSize = 0 + txmp := setup(cfg, proxy.New(client, proxy.NopMetrics()), NopTxConstraintsFetcher) evmAddress1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" evmAddress2 := "0xfD23B3A9DE15e92B9ef9540E587B3661E15A12fA" @@ -958,14 +848,14 @@ func TestReapMaxBytesMaxGas_EVMFirst(t *testing.T) { } for _, tx := range txsToAdd { - _, err := txmp.CheckTx(ctx, tx, TxInfo{SenderID: peerID}) + _, err := txmp.CheckTx(ctx, tx) require.NoError(t, err) } require.Equal(t, 5, txmp.Size()) // Reap all transactions - reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), utils.Max[int64]()) + reapedTxs, _ := txmp.ReapTxs(ReapLimits{}, false) require.Len(t, reapedTxs, 5) // Verify EVM transactions come first, then non-EVM @@ -1000,16 +890,17 @@ func TestReapMaxBytesMaxGas_EVMFirst(t *testing.T) { } func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() app := &application{Application: kvstore.NewApplication()} - txmp := setup(t, proxy.New(app, proxy.NopMetrics()), 500, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 500 + txmp := setup(cfg, proxy.New(app, proxy.NopMetrics()), NopTxConstraintsFetcher) tx := types.Tx("sender-0-0=key=1000") // Submit the tx — should enter the mempool - _, err := txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err := txmp.CheckTx(ctx, tx) require.NoError(t, err) require.Equal(t, 1, txmp.Size()) @@ -1017,14 +908,14 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, // out of gas - }, txmp.txConstraintsFetcher, true)) + }, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() // Tx should be removed from the mempool require.Equal(t, 0, txmp.Size()) // First failure: tx should have been removed from cache, allowing re-entry - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.NoError(t, err) require.Equal(t, 1, txmp.Size()) @@ -1032,70 +923,71 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, // out of gas again - }, txmp.txConstraintsFetcher, true)) + }, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() require.Equal(t, 0, txmp.Size()) // Second failure: tx should remain in cache — CheckTx should reject it - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.Equal(t, ErrTxInCache, err) require.Equal(t, 0, txmp.Size()) // A different tx (different hash) should still be admitted differentTx := types.Tx("sender-0-0=key=2000") - _, err = txmp.CheckTx(ctx, differentTx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, differentTx) require.NoError(t, err) require.Equal(t, 1, txmp.Size()) } func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() app := &application{Application: kvstore.NewApplication()} - txmp := setup(t, proxy.New(app, proxy.NopMetrics()), 500, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 500 + txmp := setup(cfg, proxy.New(app, proxy.NopMetrics()), NopTxConstraintsFetcher) tx := types.Tx("sender-0-0=key=1000") txHash := tx.Hash() // Submit and fail once in a block - _, err := txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err := txmp.CheckTx(ctx, tx) require.NoError(t, err) txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, txmp.txConstraintsFetcher, true)) + }, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() // Re-enter the mempool (first failure allows retry) - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.NoError(t, err) // This time the tx succeeds in the block txmp.Lock() require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{ {Code: abci.CodeTypeOK}, - }, txmp.txConstraintsFetcher, true)) + }, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() // Success clears the failure tracker. Simulate LRU eviction of the // main cache entry so we can verify the tracker was actually reset. - txmp.cache.Remove(txHash) + txmp.txStore.CacheRemove(txHash) // Tx should now be re-admittable - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.NoError(t, err) // Fail again in a block — this should be treated as a fresh first failure txmp.Lock() require.NoError(t, txmp.Update(ctx, 3, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, txmp.txConstraintsFetcher, true)) + }, utils.OrPanic1(txmp.txConstraintsFetcher()), true)) txmp.Unlock() // First-failure grace should be restored: tx allowed to re-enter - _, err = txmp.CheckTx(ctx, tx, TxInfo{SenderID: 0}) + _, err = txmp.CheckTx(ctx, tx) require.NoError(t, err) require.Equal(t, 1, txmp.Size()) } diff --git a/sei-tendermint/internal/mempool/metrics.go b/sei-tendermint/internal/mempool/metrics.go index db055894fe..a94293e851 100644 --- a/sei-tendermint/internal/mempool/metrics.go +++ b/sei-tendermint/internal/mempool/metrics.go @@ -60,7 +60,7 @@ type Metrics struct { // 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 + // due to other constraints, e.g. mempool is full and no lower priority // transactions exist in the mempool. //metrics:Number of rejected transactions. RejectedTxs metrics.Counter diff --git a/sei-tendermint/internal/mempool/priority_queue.go b/sei-tendermint/internal/mempool/priority_queue.go deleted file mode 100644 index 48913afa30..0000000000 --- a/sei-tendermint/internal/mempool/priority_queue.go +++ /dev/null @@ -1,423 +0,0 @@ -package mempool - -import ( - "cmp" - "container/heap" - "slices" - "sort" - "sync" - - "github.com/ethereum/go-ethereum/common" - tmmath "github.com/sei-protocol/sei-chain/sei-tendermint/libs/math" -) - -var _ heap.Interface = (*TxPriorityQueue)(nil) - -// TxPriorityQueue defines a thread-safe priority queue for valid transactions. -type TxPriorityQueue struct { - mtx sync.RWMutex - txs []*WrappedTx // priority heap - // invariant 1: no duplicate nonce in the same queue - // invariant 2: no nonce gap in the same queue - // invariant 3: head of the queue must be in heap - evmQueue map[common.Address][]*WrappedTx // indexed by sender address, sorted by nonce -} - -func insertToEVMQueue(queue []*WrappedTx, tx *WrappedTx, i int) []*WrappedTx { - // Make room for new value and add it - queue = append(queue, nil) - copy(queue[i+1:], queue[i:]) - queue[i] = tx - return queue -} - -// binarySearch finds the index at which nonce should be inserted in queue and -// whether an exact nonce match already exists. -func binarySearch(queue []*WrappedTx, nonce uint64) (int, bool) { - return slices.BinarySearchFunc(queue, nonce, func(tx *WrappedTx, target uint64) int { - return cmp.Compare(tx.EVMNonce(), target) - }) -} - -func NewTxPriorityQueue() *TxPriorityQueue { - pq := &TxPriorityQueue{ - txs: nil, - evmQueue: map[common.Address][]*WrappedTx{}, - } - heap.Init(pq) - return pq -} - -func (pq *TxPriorityQueue) TxByAddrNonce(addr common.Address, nonce uint64) (*WrappedTx, int) { - pq.mtx.RLock() - defer pq.mtx.RUnlock() - return pq.txByAddrNonceUnsafe(addr, nonce) -} - -func (pq *TxPriorityQueue) txByAddrNonceUnsafe(addr common.Address, nonce uint64) (*WrappedTx, int) { - queue := pq.evmQueue[addr] - if idx, found := binarySearch(queue, nonce); found { - return queue[idx], idx - } - return nil, -1 -} - -func (pq *TxPriorityQueue) tryReplacementUnsafe(tx *WrappedTx) (replaced *WrappedTx, shouldDrop bool) { - evm, ok := tx.evm.Get() - if !ok { - return nil, false - } - queue := pq.evmQueue[evm.address] - if len(queue) == 0 { - return nil, false - } - existing, idx := pq.txByAddrNonceUnsafe(evm.address, evm.nonce) - if existing == nil { - return nil, false - } - if tx.priority <= existing.priority { - // tx should be dropped since it's dominated by an existing tx - return nil, true - } - // should replace - // replace heap if applicable - if hi, ok := pq.findTxIndexUnsafe(existing); ok { - heap.Remove(pq, hi) - heap.Push(pq, tx) // need to be in the heap since it has the same nonce - } - pq.evmQueue[evm.address][idx] = tx // replace queue item in-place - return existing, false -} - -// 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 := append([]*WrappedTx{}, pq.txs...) - for _, queue := range pq.evmQueue { - txs = append(txs, queue[1:]...) - } - - 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 -} - -// requires read lock -func (pq *TxPriorityQueue) numQueuedUnsafe() int { - var result int - for _, queue := range pq.evmQueue { - result += len(queue) - } - // first items in queue are also in heap, subtract one - return result - len(pq.evmQueue) -} - -// 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) + pq.numQueuedUnsafe() -} - -func (pq *TxPriorityQueue) removeQueuedEvmTxUnsafe(tx *WrappedTx) (removedIdx int) { - evm, ok := tx.evm.Get() - if !ok { - return -1 - } - if queue, ok := pq.evmQueue[evm.address]; ok { - for i, t := range queue { - if t.Hash() == tx.Hash() { - pq.evmQueue[evm.address] = append(queue[:i], queue[i+1:]...) - if len(pq.evmQueue[evm.address]) == 0 { - delete(pq.evmQueue, evm.address) - } - return i - } - } - } - return -1 -} - -func (pq *TxPriorityQueue) findTxIndexUnsafe(tx *WrappedTx) (int, bool) { - // safety check for race situation where heapIndex is out of range of txs - if tx.heapIndex >= 0 && tx.heapIndex < len(pq.txs) && pq.txs[tx.heapIndex].Hash() == tx.Hash() { - return tx.heapIndex, true - } - - // heap index isn't trustable here, so attempt to find it - for i, t := range pq.txs { - if t.Hash() == tx.Hash() { - return i, true - } - } - return 0, false -} - -// RemoveTx removes a specific transaction from the priority queue. -func (pq *TxPriorityQueue) RemoveTx(tx *WrappedTx, shouldReenqueue bool) (toBeReenqueued []*WrappedTx) { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - var removedIdx int - - if idx, ok := pq.findTxIndexUnsafe(tx); ok { - heap.Remove(pq, idx) - if evm, ok := tx.evm.Get(); ok { - removedIdx = pq.removeQueuedEvmTxUnsafe(tx) - if !shouldReenqueue && len(pq.evmQueue[evm.address]) > 0 { - heap.Push(pq, pq.evmQueue[evm.address][0]) - } - } - } else if tx.evm.IsPresent() { - removedIdx = pq.removeQueuedEvmTxUnsafe(tx) - } - if evm, ok := tx.evm.Get(); ok && shouldReenqueue && len(pq.evmQueue[evm.address]) > 0 && removedIdx >= 0 { - toBeReenqueued = pq.evmQueue[evm.address][removedIdx:] - } - return -} - -func (pq *TxPriorityQueue) pushTxUnsafe(tx *WrappedTx) { - evm, ok := tx.evm.Get() - if !ok { - heap.Push(pq, tx) - return - } - - // if there aren't other waiting txs, init and return - queue, exists := pq.evmQueue[evm.address] - if !exists { - pq.evmQueue[evm.address] = []*WrappedTx{tx} - heap.Push(pq, tx) - return - } - - // this item is on the heap at the moment - first := queue[0] - - // the queue's first item (and ONLY the first item) must be on the heap - // if this tx is before the first item, then we need to remove the first - // item from the heap - if evm.nonce < first.EVMNonce() { - if idx, ok := pq.findTxIndexUnsafe(first); ok { - heap.Remove(pq, idx) - } - heap.Push(pq, tx) - } - idx, _ := binarySearch(queue, evm.nonce) - pq.evmQueue[evm.address] = insertToEVMQueue(queue, tx, idx) -} - -// PushTx adds a valid transaction to the priority queue. It is thread safe. -func (pq *TxPriorityQueue) PushTx(tx *WrappedTx) (*WrappedTx, bool) { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - replacedTx, shouldDrop := pq.tryReplacementUnsafe(tx) - - // tx was not inserted, and nothing was replaced - if shouldDrop { - return nil, false - } - - // tx replaced an existing transaction - if replacedTx != nil { - return replacedTx, true - } - - // tx was not inserted yet, so insert it - pq.pushTxUnsafe(tx) - return nil, true -} - -func (pq *TxPriorityQueue) popTxUnsafe() *WrappedTx { - if len(pq.txs) == 0 { - return nil - } - - // remove the first item from the heap - x := heap.Pop(pq) - if x == nil { - return nil - } - tx := x.(*WrappedTx) - - // this situation is primarily for a test case that inserts nils - if tx == nil { - return nil - } - - // non-evm transactions do not have txs waiting on a nonce - evm, ok := tx.evm.Get() - if !ok { - return tx - } - - // evm transactions can have txs waiting on this nonce - // if there are any, we should replace the heap with the next nonce - // for the address - - // remove the first item from the evmQueue - pq.removeQueuedEvmTxUnsafe(tx) - - // if there is a next item, now it can be added to the heap - if len(pq.evmQueue[evm.address]) > 0 { - heap.Push(pq, pq.evmQueue[evm.address][0]) - } - - return 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() - - return pq.popTxUnsafe() -} - -// dequeue up to `max` transactions and reenqueue while locked -func (pq *TxPriorityQueue) ForEachTx(handler func(wtx *WrappedTx) bool) { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - numTxs := len(pq.txs) + pq.numQueuedUnsafe() - - txs := make([]*WrappedTx, 0, numTxs) - - defer func() { - for _, tx := range txs { - pq.pushTxUnsafe(tx) - } - }() - - for range numTxs { - popped := pq.popTxUnsafe() - if popped == nil { - break - } - txs = append(txs, popped) - if !handler(popped) { - return - } - } -} - -// dequeue up to `max` transactions and reenqueue while locked -// TODO: use ForEachTx instead -func (pq *TxPriorityQueue) PeekTxs(max int) []*WrappedTx { - pq.mtx.Lock() - defer pq.mtx.Unlock() - - numTxs := len(pq.txs) + pq.numQueuedUnsafe() - if max < 0 { - max = numTxs - } - - cap := tmmath.MinInt(numTxs, max) - res := make([]*WrappedTx, 0, cap) - for i := 0; i < cap; i++ { - popped := pq.popTxUnsafe() - if popped == nil { - break - } - - res = append(res, popped) - } - - for _, tx := range res { - pq.pushTxUnsafe(tx) - } - return res -} - -// 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 - setHeapIndex(item, -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] - setHeapIndex(pq.txs[i], i) - setHeapIndex(pq.txs[j], j) -} - -func setHeapIndex(tx *WrappedTx, i int) { - // a removed tx can be nil - if tx == nil { - return - } - tx.heapIndex = i -} diff --git a/sei-tendermint/internal/mempool/priority_queue_test.go b/sei-tendermint/internal/mempool/priority_queue_test.go deleted file mode 100644 index 4b885c4e21..0000000000 --- a/sei-tendermint/internal/mempool/priority_queue_test.go +++ /dev/null @@ -1,489 +0,0 @@ -package mempool - -import ( - "fmt" - "math/rand" - "sort" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" - "github.com/sei-protocol/sei-chain/sei-tendermint/types" - "github.com/stretchr/testify/require" -) - -func wrappedEVMTx(tx types.Tx, address string, nonce uint64, priority int64) *WrappedTx { - return &WrappedTx{ - hashedTx: newHashedTx(tx), - priority: priority, - evm: utils.Some(evmTx{ - address: common.HexToAddress(address), - nonce: nonce, - }), - } -} - -// TxTestCase represents a single test case for the TxPriorityQueue -type TxTestCase struct { - name string - inputTxs []*WrappedTx // Input transactions - expectedOutput []int64 // Expected order of transaction IDs -} - -func TestTxPriorityQueue_ReapHalf(t *testing.T) { - pq := NewTxPriorityQueue() - - // Generate transactions with different priorities and nonces - txs := make([]*WrappedTx, 100) - for i := range txs { - txs[i] = &WrappedTx{ - hashedTx: newHashedTx(types.Tx(fmt.Sprintf("tx-%d", i))), - priority: int64(i), - } - - // Push the transaction - pq.PushTx(txs[i]) - } - - //reverse sort txs by priority - sort.Slice(txs, func(i, j int) bool { - return txs[i].priority > txs[j].priority - }) - - // Reap half of the transactions - reapedTxs := pq.PeekTxs(len(txs) / 2) - - // Check if the reaped transactions are in the correct order of their priorities and nonces - for i, reapedTx := range reapedTxs { - require.Equal(t, txs[i].priority, reapedTx.priority) - } -} - -func TestAvoidPanicIfTransactionIsNil(t *testing.T) { - pq := NewTxPriorityQueue() - pq.Push(wrappedEVMTx(nil, "0xabc", 1, 10)) - pq.txs = append(pq.txs, nil) - - var count int - pq.ForEachTx(func(tx *WrappedTx) bool { - count++ - return true - }) - - require.Equal(t, 1, count) -} - -func TestTxPriorityQueue_PriorityAndNonceOrdering(t *testing.T) { - testCases := []TxTestCase{ - { - name: "PriorityWithEVMAndNonEVMDuplicateNonce", - inputTxs: []*WrappedTx{ - wrappedEVMTx(types.Tx("1"), "0xabc", 1, 10), - wrappedEVMTx(types.Tx("3"), "0xabc", 3, 9), - wrappedEVMTx(types.Tx("2"), "0xabc", 1, 7), - }, - expectedOutput: []int64{1, 3}, - }, - { - name: "PriorityWithEVMAndNonEVMDuplicateNonce", - inputTxs: []*WrappedTx{ - wrappedEVMTx(types.Tx("1"), "0xabc", 1, 10), - {hashedTx: newHashedTx(types.Tx("2")), priority: 9}, - wrappedEVMTx(types.Tx("4"), "0xabc", 0, 9), // Same EVM address as first, lower nonce - wrappedEVMTx(types.Tx("5"), "0xdef", 1, 7), - wrappedEVMTx(types.Tx("3"), "0xdef", 0, 8), - {hashedTx: newHashedTx(types.Tx("6")), priority: 6}, - wrappedEVMTx(types.Tx("7"), "0xghi", 2, 5), - }, - expectedOutput: []int64{2, 4, 1, 3, 5, 6, 7}, - }, - { - name: "PriorityWithEVMAndNonEVM", - inputTxs: []*WrappedTx{ - wrappedEVMTx(types.Tx("1"), "0xabc", 1, 10), - {hashedTx: newHashedTx(types.Tx("2")), priority: 9}, - wrappedEVMTx(types.Tx("4"), "0xabc", 0, 9), // Same EVM address as first, lower nonce - wrappedEVMTx(types.Tx("5"), "0xdef", 1, 7), - wrappedEVMTx(types.Tx("3"), "0xdef", 0, 8), - {hashedTx: newHashedTx(types.Tx("6")), priority: 6}, - wrappedEVMTx(types.Tx("7"), "0xghi", 2, 5), - }, - expectedOutput: []int64{2, 4, 1, 3, 5, 6, 7}, - }, - { - name: "IdenticalPrioritiesAndNoncesDifferentAddresses", - inputTxs: []*WrappedTx{ - wrappedEVMTx(types.Tx("1"), "0xabc", 2, 5), - wrappedEVMTx(types.Tx("2"), "0xdef", 2, 5), - wrappedEVMTx(types.Tx("3"), "0xghi", 2, 5), - }, - expectedOutput: []int64{1, 2, 3}, - }, - { - name: "InterleavedEVAndNonEVMTransactions", - inputTxs: []*WrappedTx{ - {hashedTx: newHashedTx(types.Tx("7")), priority: 15}, - wrappedEVMTx(types.Tx("8"), "0xabc", 1, 20), - {hashedTx: newHashedTx(types.Tx("9")), priority: 10}, - wrappedEVMTx(types.Tx("10"), "0xdef", 2, 20), - }, - expectedOutput: []int64{8, 10, 7, 9}, - }, - { - name: "SameAddressPriorityDifferentNonces", - inputTxs: []*WrappedTx{ - wrappedEVMTx(types.Tx("11"), "0xabc", 3, 10), - wrappedEVMTx(types.Tx("12"), "0xabc", 1, 10), - wrappedEVMTx(types.Tx("13"), "0xabc", 2, 10), - }, - expectedOutput: []int64{12, 13, 11}, - }, - { - name: "OneItem", - inputTxs: []*WrappedTx{ - wrappedEVMTx(types.Tx("14"), "0xabc", 1, 10), - }, - expectedOutput: []int64{14}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - pq := NewTxPriorityQueue() - now := time.Now() - - // Add input transactions to the queue and set timestamp to order inserted - for i, tx := range tc.inputTxs { - tx.timestamp = now.Add(time.Duration(i) * time.Second) - pq.PushTx(tx) - } - - results := pq.PeekTxs(len(tc.inputTxs)) - // Validate the order of transactions - require.Len(t, results, len(tc.expectedOutput)) - for i, expectedTxID := range tc.expectedOutput { - tx := results[i] - require.Equal(t, fmt.Sprintf("%d", expectedTxID), string(tx.Tx())) - } - }) - } -} - -func TestTxPriorityQueue_SameAddressDifferentNonces(t *testing.T) { - pq := NewTxPriorityQueue() - address := "0x123" - - // Insert transactions with the same address but different nonces and priorities - pq.PushTx(wrappedEVMTx(types.Tx("tx1"), address, 2, 10)) - pq.PushTx(wrappedEVMTx(types.Tx("tx2"), address, 1, 5)) - pq.PushTx(wrappedEVMTx(types.Tx("tx3"), address, 3, 15)) - - // Pop transactions and verify they are in the correct order of nonce - tx1 := pq.PopTx() - evm, ok := tx1.evm.Get() - require.True(t, ok) - require.Equal(t, uint64(1), evm.nonce) - tx2 := pq.PopTx() - evm, ok = tx2.evm.Get() - require.True(t, ok) - require.Equal(t, uint64(2), evm.nonce) - tx3 := pq.PopTx() - evm, ok = tx3.evm.Get() - require.True(t, ok) - require.Equal(t, uint64(3), evm.nonce) -} - -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{ - hashedTx: newHashedTx(types.Tx(fmt.Sprintf("%d", i))), - 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{ - hashedTx: newHashedTx(types.Tx(fmt.Sprintf("%d", time.Now().UnixNano()))), - 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{ - hashedTx: newHashedTx(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 { - - 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_RemoveTxEvm(t *testing.T) { - pq := NewTxPriorityQueue() - - tx1 := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx1")), - priority: 1, - evm: utils.Some(evmTx{ - address: common.HexToAddress("0xabc"), - nonce: 1, - }), - } - tx2 := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx2")), - priority: 1, - evm: utils.Some(evmTx{ - address: common.HexToAddress("0xabc"), - nonce: 2, - }), - } - - pq.PushTx(tx1) - pq.PushTx(tx2) - - pq.RemoveTx(tx1, false) - - result := pq.PopTx() - require.Equal(t, tx2, result) -} - -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{ - hashedTx: newHashedTx(types.Tx(fmt.Sprintf("%d", i))), - 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, false) - 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}, false) - pq.RemoveTx(&WrappedTx{heapIndex: numTxs + 1}, false) - }) - require.Equal(t, numTxs-2, pq.NumTxs()) -} - -func TestTxPriorityQueue_TryReplacement(t *testing.T) { - for _, test := range []struct { - tx *WrappedTx - existing []*WrappedTx - expectedReplaced bool - expectedDropped bool - expectedQueue []*WrappedTx - expectedHeap []*WrappedTx - }{ - // non-evm transaction is inserted into empty queue - {&WrappedTx{}, []*WrappedTx{}, false, false, []*WrappedTx{{}}, []*WrappedTx{{}}}, - // evm transaction is inserted into empty queue - {wrappedEVMTx(nil, "addr1", 0, 0), []*WrappedTx{}, false, false, []*WrappedTx{wrappedEVMTx(nil, "addr1", 0, 0)}, []*WrappedTx{wrappedEVMTx(nil, "addr1", 0, 0)}}, - // evm transaction (new nonce) is inserted into queue with existing tx (lower nonce) - { - wrappedEVMTx(types.Tx("abc"), "addr1", 1, 100), []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - }, false, false, []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - wrappedEVMTx(types.Tx("abc"), "addr1", 1, 100), - }, []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - wrappedEVMTx(types.Tx("abc"), "addr1", 1, 100), - }, - }, - // evm transaction (new nonce) is not inserted because it's a duplicate nonce and same priority - { - wrappedEVMTx(types.Tx("abc"), "addr1", 0, 100), []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - }, false, true, []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - }, []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - }, - }, - // evm transaction (new nonce) replaces the existing nonce transaction because its priority is higher - { - wrappedEVMTx(types.Tx("abc"), "addr1", 0, 101), []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - }, true, false, []*WrappedTx{ - wrappedEVMTx(types.Tx("abc"), "addr1", 0, 101), - }, []*WrappedTx{ - wrappedEVMTx(types.Tx("abc"), "addr1", 0, 101), - }, - }, - { - wrappedEVMTx(types.Tx("abc"), "addr1", 1, 100), []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - wrappedEVMTx(types.Tx("ghi"), "addr1", 1, 99), - }, true, false, []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - wrappedEVMTx(types.Tx("abc"), "addr1", 1, 100), - }, []*WrappedTx{ - wrappedEVMTx(types.Tx("def"), "addr1", 0, 100), - }, - }, - } { - pq := NewTxPriorityQueue() - for _, e := range test.existing { - pq.PushTx(e) - } - replaced, inserted := pq.PushTx(test.tx) - if test.expectedReplaced { - require.NotNil(t, replaced) - } else { - require.Nil(t, replaced) - } - require.Equal(t, test.expectedDropped, !inserted) - txEVM, ok := test.tx.evm.Get() - if !ok { - require.Empty(t, pq.evmQueue) - continue - } - for i, q := range pq.evmQueue[txEVM.address] { - require.Equal(t, test.expectedQueue[i].Hash(), q.Hash()) - require.Equal(t, test.expectedQueue[i].priority, q.priority) - expectedEVM, ok := test.expectedQueue[i].evm.Get() - require.True(t, ok) - queueEVM, ok := q.evm.Get() - require.True(t, ok) - require.Equal(t, expectedEVM.nonce, queueEVM.nonce) - } - for i, q := range pq.txs { - require.Equal(t, test.expectedHeap[i].Hash(), q.Hash()) - require.Equal(t, test.expectedHeap[i].priority, q.priority) - expectedEVM, ok := test.expectedHeap[i].evm.Get() - if ok { - queueEVM, ok := q.evm.Get() - require.True(t, ok) - require.Equal(t, expectedEVM.nonce, queueEVM.nonce) - } else { - require.False(t, q.evm.IsPresent()) - } - } - } -} diff --git a/sei-tendermint/internal/mempool/reactor/ids.go b/sei-tendermint/internal/mempool/reactor/ids.go deleted file mode 100644 index b089526fe9..0000000000 --- a/sei-tendermint/internal/mempool/reactor/ids.go +++ /dev/null @@ -1,87 +0,0 @@ -package reactor - -import ( - "fmt" - "math" - "sync" - - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" - "github.com/sei-protocol/sei-chain/sei-tendermint/types" -) - -const MaxActiveIDs = math.MaxUint16 - -type IDs struct { - mtx sync.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() *IDs { - return &IDs{ - peerMap: make(map[types.NodeID]uint16), - - // reserve UnknownPeerID for mempoolReactor.BroadcastTx - activeIDs: map[uint16]struct{}{mempool.UnknownPeerID: {}}, - nextID: 1, - } -} - -// ReserveForPeer searches for the next unused ID and assigns it to the provided -// peer. -func (ids *IDs) ReserveForPeer(peerID types.NodeID) { - ids.mtx.Lock() - defer ids.mtx.Unlock() - - if _, ok := ids.peerMap[peerID]; ok { - // the peer has been reserved - return - } - - 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 *IDs) 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) - if removedID < ids.nextID { - ids.nextID = removedID - } - } -} - -// GetForPeer returns an ID reserved for the peer. -func (ids *IDs) 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 *IDs) nextPeerID() uint16 { - if len(ids.activeIDs) == MaxActiveIDs { - panic(fmt.Sprintf("node has maximum %d active IDs and wanted to get one more", MaxActiveIDs)) - } - - _, idExists := ids.activeIDs[ids.nextID] - for idExists { - ids.nextID++ - _, idExists = ids.activeIDs[ids.nextID] - } - - curID := ids.nextID - ids.nextID++ - - return curID -} diff --git a/sei-tendermint/internal/mempool/reactor/ids_test.go b/sei-tendermint/internal/mempool/reactor/ids_test.go deleted file mode 100644 index 53ad039b07..0000000000 --- a/sei-tendermint/internal/mempool/reactor/ids_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package reactor - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/sei-protocol/sei-chain/sei-tendermint/types" -) - -func TestMempoolIDsBasic(t *testing.T) { - ids := NewMempoolIDs() - - peerID, err := types.NewNodeID("0011223344556677889900112233445566778899") - require.NoError(t, err) - require.EqualValues(t, 0, ids.GetForPeer(peerID)) - - ids.ReserveForPeer(peerID) - require.EqualValues(t, 1, ids.GetForPeer(peerID)) - - ids.Reclaim(peerID) - require.EqualValues(t, 0, ids.GetForPeer(peerID)) - - ids.ReserveForPeer(peerID) - require.EqualValues(t, 1, ids.GetForPeer(peerID)) -} - -func TestMempoolIDsPeerDupReserve(t *testing.T) { - ids := NewMempoolIDs() - - peerID, err := types.NewNodeID("0011223344556677889900112233445566778899") - require.NoError(t, err) - require.EqualValues(t, 0, ids.GetForPeer(peerID)) - - ids.ReserveForPeer(peerID) - require.EqualValues(t, 1, ids.GetForPeer(peerID)) - - ids.ReserveForPeer(peerID) - require.EqualValues(t, 1, ids.GetForPeer(peerID)) -} - -func TestMempoolIDs2Peers(t *testing.T) { - ids := NewMempoolIDs() - - peer1ID, _ := types.NewNodeID("0011223344556677889900112233445566778899") - require.EqualValues(t, 0, ids.GetForPeer(peer1ID)) - - ids.ReserveForPeer(peer1ID) - require.EqualValues(t, 1, ids.GetForPeer(peer1ID)) - - ids.Reclaim(peer1ID) - require.EqualValues(t, 0, ids.GetForPeer(peer1ID)) - - peer2ID, _ := types.NewNodeID("1011223344556677889900112233445566778899") - - ids.ReserveForPeer(peer2ID) - require.EqualValues(t, 1, ids.GetForPeer(peer2ID)) - - ids.ReserveForPeer(peer1ID) - require.EqualValues(t, 2, ids.GetForPeer(peer1ID)) -} - -func TestMempoolIDsNextExistID(t *testing.T) { - ids := NewMempoolIDs() - - peer1ID, _ := types.NewNodeID("0011223344556677889900112233445566778899") - ids.ReserveForPeer(peer1ID) - require.EqualValues(t, 1, ids.GetForPeer(peer1ID)) - - peer2ID, _ := types.NewNodeID("1011223344556677889900112233445566778899") - ids.ReserveForPeer(peer2ID) - require.EqualValues(t, 2, ids.GetForPeer(peer2ID)) - - peer3ID, _ := types.NewNodeID("2011223344556677889900112233445566778899") - ids.ReserveForPeer(peer3ID) - require.EqualValues(t, 3, ids.GetForPeer(peer3ID)) - - ids.Reclaim(peer1ID) - require.EqualValues(t, 0, ids.GetForPeer(peer1ID)) - - ids.Reclaim(peer3ID) - require.EqualValues(t, 0, ids.GetForPeer(peer3ID)) - - ids.ReserveForPeer(peer1ID) - require.EqualValues(t, 1, ids.GetForPeer(peer1ID)) - - ids.ReserveForPeer(peer3ID) - require.EqualValues(t, 3, ids.GetForPeer(peer3ID)) -} diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index ecb23cad98..d5aae51173 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -34,7 +34,6 @@ type Reactor struct { cfg *config.MempoolConfig mempool *mempool.TxMempool - ids *IDs router *p2p.Router @@ -53,7 +52,6 @@ func NewReactor(cfg *config.MempoolConfig, txmp *mempool.TxMempool, router *p2p. r := &Reactor{ cfg: cfg, mempool: txmp, - ids: NewMempoolIDs(), router: router, channel: channel, failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), @@ -113,14 +111,8 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me return err } protoTxs := msg.Txs.GetTxs() - - txInfo := mempool.TxInfo{SenderID: r.ids.GetForPeer(m.From)} - if len(m.From) != 0 { - txInfo.SenderNodeID = m.From - } - for _, tx := range protoTxs { - if _, err := r.mempool.CheckTx(ctx, tx, txInfo); err != nil { + if _, err := r.mempool.CheckTx(ctx, tx); err != nil { r.accountFailedCheckTx(m.From, err) if errors.Is(err, mempool.ErrTxInCache) { // If the tx is in the cache, then we've been gossiped a tx @@ -219,7 +211,6 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) error { } pctx, pcancel := context.WithCancel(ctx) peerRoutines[update.NodeID] = pcancel - r.ids.ReserveForPeer(update.NodeID) // We keep peer management even when broadcasting is disabled, // so that failedCheckTxCounts WAI. if r.cfg.Broadcast { @@ -230,7 +221,6 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) error { } case p2p.PeerStatusDown: - r.ids.Reclaim(update.NodeID) for counts := range r.failedCheckTxCounts.Lock() { delete(counts, update.NodeID) } @@ -242,21 +232,18 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) error { } func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { - peerMempoolID := r.ids.GetForPeer(peerID) for { - next, err := r.mempool.WaitForNextTx(ctx) + next, err := r.mempool.WaitForReadyTx(ctx) if err != nil { return } for { - memTx := next.Value() - if ok := r.mempool.TxStore().TxHasPeer(memTx.Hash(), peerMempoolID); !ok { - r.channel.Send(&pb.Message{ - Sum: &pb.Message_Txs{ - Txs: &pb.Txs{Txs: [][]byte{memTx.Tx()}}, - }, - }, peerID) - } + tx := next.Value() + r.channel.Send(&pb.Message{ + Sum: &pb.Message_Txs{ + Txs: &pb.Txs{Txs: [][]byte{tx}}, + }, + }, peerID) next, err = next.NextWait(ctx) if err != nil { diff --git a/sei-tendermint/internal/mempool/reactor/reactor_test.go b/sei-tendermint/internal/mempool/reactor/reactor_test.go index 1b65130ea1..a866b91516 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor/reactor_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/fortytw2/leaktest" - "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -54,11 +53,10 @@ func setupMempool(t testing.TB, app *proxy.Proxy, cacheSize int, txConstraintsFe return mempool.NewTxMempool(cfg.Mempool.ToMempoolConfig(), app, mempool.NopMetrics(), txConstraintsFetcher) } -func checkTxs(ctx context.Context, t *testing.T, txmp *mempool.TxMempool, numTxs int, peerID uint16) []testTx { +func checkTxs(ctx context.Context, t *testing.T, txmp *mempool.TxMempool, numTxs int) []testTx { t.Helper() txs := make([]testTx, numTxs) - txInfo := mempool.TxInfo{SenderID: peerID} rng := rand.New(rand.NewSource(time.Now().UnixNano())) for i := range numTxs { @@ -67,9 +65,9 @@ func checkTxs(ctx context.Context, t *testing.T, txmp *mempool.TxMempool, numTxs require.NoError(t, err) txs[i] = testTx{ - tx: []byte(fmt.Sprintf("sender-%d-%d=%X=%d", i, peerID, prefix, i+1000)), + tx: []byte(fmt.Sprintf("sender-%d=%X=%d", i, prefix, i+1000)), } - _, err = txmp.CheckTx(ctx, txs[i].tx, txInfo) + _, err = txmp.CheckTx(ctx, txs[i].tx) require.NoError(t, err) } @@ -208,7 +206,7 @@ func TestReactorBroadcastTxs(t *testing.T) { primary := rts.nodes[0] secondaries := rts.nodes[1:] - txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, mempool.UnknownPeerID) + txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs) require.Equal(t, numTxs, rts.reactors[primary].mempool.Size()) @@ -305,7 +303,6 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { require.NoError(t, reactor.handleMempoolMessage(t.Context(), msg)) require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "sender")) - reactor.ids.Reclaim("sender") for counts := range reactor.failedCheckTxCounts.Lock() { delete(counts, "sender") } @@ -338,7 +335,6 @@ func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { counts["sender"] = 0 delete(counts, "sender") } - reactor.ids.Reclaim("sender") require.NoError(t, reactor.handleMempoolMessage(t.Context(), msg)) require.Equal(t, utils.None[int](), peerFailedCheckTxCount(reactor, "sender")) @@ -358,66 +354,44 @@ func TestReactorConcurrency(t *testing.T) { rts.start(t) var wg sync.WaitGroup + var primaryHeight int64 + var secondaryHeight int64 for range runtime.NumCPU() * 2 { - wg.Add(2) - - txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, mempool.UnknownPeerID) - go func() { - defer wg.Done() - + wg.Go(func() { + txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs) txmp := rts.mempools[primary] txmp.Lock() defer txmp.Unlock() + primaryHeight++ + height := primaryHeight deliverTxResponses := make([]*abci.ExecTxResult, len(txs)) for i := range txs { deliverTxResponses[i] = &abci.ExecTxResult{Code: 0} } - require.NoError(t, txmp.Update(ctx, 1, convertTex(txs), deliverTxResponses, mempool.NopTxConstraintsFetcher, true)) - }() - - _ = checkTxs(ctx, t, rts.reactors[secondary].mempool, numTxs, mempool.UnknownPeerID) - go func() { - defer wg.Done() + require.NoError(t, txmp.Update(ctx, height, convertTex(txs), deliverTxResponses, mempool.NopTxConstraints(), true)) + }) + wg.Go(func() { + _ = checkTxs(ctx, t, rts.reactors[secondary].mempool, numTxs) txmp := rts.mempools[secondary] txmp.Lock() defer txmp.Unlock() + secondaryHeight++ + height := secondaryHeight - err := txmp.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), mempool.NopTxConstraintsFetcher, true) + err := txmp.Update(ctx, height, []types.Tx{}, make([]*abci.ExecTxResult, 0), mempool.NopTxConstraints(), true) require.NoError(t, err) - }() + }) } wg.Wait() } -func TestReactorNoBroadcastToSender(t *testing.T) { - numTxs := 1000 - numNodes := 2 - ctx := t.Context() - - rts := setupReactors(ctx, t, numNodes) - t.Cleanup(leaktest.Check(t)) - - primary := rts.nodes[0] - secondary := rts.nodes[1] - - peerID := uint16(1) - _ = checkTxs(ctx, 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) -} - func TestReactor_MaxTxBytes(t *testing.T) { numNodes := 2 cfg := config.TestConfig() @@ -433,7 +407,6 @@ func TestReactor_MaxTxBytes(t *testing.T) { _, err := rts.reactors[primary].mempool.CheckTx( ctx, tx1, - mempool.TxInfo{SenderID: mempool.UnknownPeerID}, ) require.NoError(t, err) @@ -443,46 +416,10 @@ func TestReactor_MaxTxBytes(t *testing.T) { rts.reactors[secondary].mempool.Flush() tx2 := tmrand.Bytes(cfg.Mempool.MaxTxBytes + 1) - _, err = rts.mempools[primary].CheckTx(ctx, tx2, mempool.TxInfo{SenderID: mempool.UnknownPeerID}) + _, err = rts.mempools[primary].CheckTx(ctx, tx2) require.Error(t, err) } -func TestDontExhaustMaxActiveIDs(t *testing.T) { - t.Skip("this test fails, but the property it tests is not very useful") - - ctx := t.Context() - rts := setupReactors(ctx, t, 1) - t.Cleanup(leaktest.Check(t)) - - nodeID := rts.nodes[0] - - for range MaxActiveIDs + 1 { - privKey := ed25519.GenerateSecretKey() - peerID := types.NodeIDFromPubKey(privKey.Public()) - rts.reactors[nodeID].ids.ReserveForPeer(peerID) - } -} - -func TestMempoolIDsPanicsIfNodeRequestsOvermaxActiveIDs(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode") - } - - ids := NewMempoolIDs() - - for i := range MaxActiveIDs - 1 { - peerID, err := types.NewNodeID(fmt.Sprintf("%040d", i)) - require.NoError(t, err) - ids.ReserveForPeer(peerID) - } - - peerID, err := types.NewNodeID(fmt.Sprintf("%040d", MaxActiveIDs-1)) - require.NoError(t, err) - require.Panics(t, func() { - ids.ReserveForPeer(peerID) - }) -} - func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode") @@ -499,7 +436,7 @@ func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { rts.start(t) rts.network.Remove(t, secondary) - txs := checkTxs(ctx, t, rts.reactors[primary].mempool, 4, mempool.UnknownPeerID) + txs := checkTxs(ctx, t, rts.reactors[primary].mempool, 4) require.Equal(t, 4, len(txs)) require.Equal(t, 4, rts.mempools[primary].Size()) require.Equal(t, 0, rts.mempools[secondary].Size()) diff --git a/sei-tendermint/internal/mempool/recheck_drain_test.go b/sei-tendermint/internal/mempool/recheck_drain_test.go index 8583954990..dd914be05f 100644 --- a/sei-tendermint/internal/mempool/recheck_drain_test.go +++ b/sei-tendermint/internal/mempool/recheck_drain_test.go @@ -28,21 +28,46 @@ type evmNonceApp struct { abci.Application mu sync.Mutex - nextNonce map[string]uint64 + nextNonce map[common.Address]uint64 + balance map[common.Address]int } func newEVMNonceApp() *evmNonceApp { - return &evmNonceApp{nextNonce: map[string]uint64{}} + return &evmNonceApp{ + nextNonce: map[common.Address]uint64{}, + balance: map[common.Address]int{}, + } } // markMined bumps the sender's next-expected nonce by 1, simulating that the // previous next-expected nonce just landed in a block. -func (a *evmNonceApp) markMined(sender string) { +func (a *evmNonceApp) markMined(sender common.Address) { a.mu.Lock() a.nextNonce[sender]++ a.mu.Unlock() } +func (a *evmNonceApp) setNonce(sender common.Address, nonce uint64) { + a.mu.Lock() + a.nextNonce[sender] = nonce + a.mu.Unlock() +} + +func (a *evmNonceApp) setBalance(sender common.Address, balance int) { + a.mu.Lock() + a.balance[sender] = balance + a.mu.Unlock() +} + +func (a *evmNonceApp) balanceOf(sender common.Address) int { + a.mu.Lock() + defer a.mu.Unlock() + if balance, ok := a.balance[sender]; ok { + return balance + } + return 0 +} + func (a *evmNonceApp) parseTx(tx []byte) (sender string, nonce uint64, priority int64, ok bool) { parts := bytes.Split(tx, []byte("=")) if len(parts) != 4 || string(parts[0]) != "evm" { @@ -64,9 +89,10 @@ func (a *evmNonceApp) CheckTx(_ context.Context, req *abci.RequestCheckTxV2) *ab if !ok { return &abci.ResponseCheckTxV2{ResponseCheckTx: &abci.ResponseCheckTx{Code: 1}} } + senderAddr := common.HexToAddress(sender) a.mu.Lock() - expected := a.nextNonce[sender] + expected := a.nextNonce[senderAddr] a.mu.Unlock() if nonce < expected { @@ -82,8 +108,8 @@ func (a *evmNonceApp) CheckTx(_ context.Context, req *abci.RequestCheckTxV2) *ab GasEstimated: DefaultGasEstimated, }, EVMNonce: nonce, - EVMSenderAddress: common.HexToAddress(sender), - SeiSenderAddress: sdk.AccAddress(common.HexToAddress(sender).Bytes()), + EVMSenderAddress: senderAddr, + SeiSenderAddress: sdk.AccAddress(senderAddr.Bytes()), IsEVM: true, EVMRequiredBalance: big.NewInt(0), } @@ -97,10 +123,15 @@ func (a *evmNonceApp) GetTxPriorityHint(context.Context, *abci.RequestGetTxPrior func (a *evmNonceApp) EvmNonce(addr common.Address) uint64 { a.mu.Lock() defer a.mu.Unlock() - return a.nextNonce[addr.Hex()] + return a.nextNonce[addr] } -func (a *evmNonceApp) EvmBalance(common.Address, []byte) *big.Int { +func (a *evmNonceApp) EvmBalance(addr common.Address, _ []byte) *big.Int { + a.mu.Lock() + defer a.mu.Unlock() + if balance, ok := a.balance[addr]; ok { + return big.NewInt(int64(balance)) + } return big.NewInt(0) } @@ -121,7 +152,9 @@ func TestTxMempool_DescendingNonceDrain(t *testing.T) { ctx := t.Context() app := newEVMNonceApp() - txmp := setup(t, proxy.New(app, proxy.NopMetrics()), 5000, NopTxConstraintsFetcher) + cfg := TestConfig() + cfg.CacheSize = 5000 + txmp := setup(cfg, proxy.New(app, proxy.NopMetrics()), NopTxConstraintsFetcher) // Submit nonces N-1, N-2, ..., 1, 0. Every tx except the last enters // pendingTxs because its nonce is ahead of the sender's expected nonce @@ -129,7 +162,7 @@ func TestTxMempool_DescendingNonceDrain(t *testing.T) { // in the priority index as the evmQueue head. for n := N - 1; n >= 0; n-- { tx := []byte(fmt.Sprintf("evm=%s=%d=1", sender.Hex(), n)) - _, err := txmp.CheckTx(ctx, tx, TxInfo{}) + _, err := txmp.CheckTx(ctx, tx) require.NoError(t, err) } @@ -145,28 +178,25 @@ func TestTxMempool_DescendingNonceDrain(t *testing.T) { const maxBlocks = 5 totalMined := 0 for height := int64(1); txmp.Size() > 0 && height <= maxBlocks; height++ { - txs, _ := txmp.PopTxs(ReapLimits{ - MaxTxs: utils.Some(uint64(N)), - MaxBytes: utils.Some(int64(1 << 30)), - MaxGasWanted: utils.Some(int64(1 << 30)), - MaxGasEstimated: utils.Some(int64(1 << 30)), - }) + txs, _ := txmp.ReapTxs(ReapLimits{ + MaxTxs: utils.Some(uint64(N)), + }, true) require.NotEmpty(t, txs, "PopTxs returned no txs at height %d (mempool stalled)", height) txResults := make([]*abci.ExecTxResult, len(txs)) for i := range txs { - app.markMined(sender.Hex()) + app.markMined(sender) txResults[i] = &abci.ExecTxResult{Code: code.CodeTypeOK} } totalMined += len(txs) // recheck=false matches the post-fix Autobahn path and CometBFT's default. - require.NoError(t, txmp.Update(ctx, height, txs, txResults, NopTxConstraintsFetcher, false)) + require.NoError(t, txmp.Update(ctx, height, txs, txResults, utils.OrPanic1(NopTxConstraintsFetcher()), false)) } require.Equal(t, N, totalMined, "all N txs should have mined within %d blocks", maxBlocks) require.Zero(t, txmp.Size(), "mempool should fully drain within %d blocks", maxBlocks) - require.Equal(t, uint64(N), app.nextNonce[sender.Hex()], "all N nonces should have been mined") + require.Equal(t, uint64(N), app.nextNonce[sender], "all N nonces should have been mined") } func TestTxMempool_EvmNextPendingNonceIncludesPendingTransactions(t *testing.T) { @@ -174,17 +204,19 @@ func TestTxMempool_EvmNextPendingNonceIncludesPendingTransactions(t *testing.T) sender := common.HexToAddress("0x00000000000000000000000000000000000000aa") app := newEVMNonceApp() - app.nextNonce[sender.Hex()] = 5 - txmp := setup(t, proxy.New(app, proxy.NopMetrics()), 5000, NopTxConstraintsFetcher) + app.setNonce(sender, 5) + cfg := TestConfig() + cfg.CacheSize = 5000 + txmp := setup(cfg, proxy.New(app, proxy.NopMetrics()), NopTxConstraintsFetcher) for _, nonce := range []uint64{7, 5, 6} { tx := []byte(fmt.Sprintf("evm=%s=%d=1", sender.Hex(), nonce)) - _, err := txmp.CheckTx(ctx, tx, TxInfo{}) + _, err := txmp.CheckTx(ctx, tx) require.NoError(t, err) } - require.Equal(t, 2, txmp.NumTxsNotPending()) - require.Equal(t, 1, txmp.PendingSize()) + require.Equal(t, 3, txmp.NumTxsNotPending()) + require.Equal(t, 0, txmp.PendingSize()) require.Equal(t, uint64(8), txmp.EvmNextPendingNonce(sender)) } @@ -193,22 +225,24 @@ func TestTxMempool_EvmNextPendingNonceReplacesSameNonceByPriority(t *testing.T) sender := common.HexToAddress("0x00000000000000000000000000000000000000bb") app := newEVMNonceApp() - app.nextNonce[sender.Hex()] = 5 - txmp := setup(t, proxy.New(app, proxy.NopMetrics()), 5000, NopTxConstraintsFetcher) + app.setNonce(sender, 5) + cfg := TestConfig() + cfg.CacheSize = 5000 + txmp := setup(cfg, proxy.New(app, proxy.NopMetrics()), NopTxConstraintsFetcher) lowPriorityTx := []byte(fmt.Sprintf("evm=%s=%d=%d", sender.Hex(), 6, 1)) highPriorityTx := []byte(fmt.Sprintf("evm=%s=%d=%d", sender.Hex(), 6, 2)) - _, err := txmp.CheckTx(ctx, lowPriorityTx, TxInfo{}) + _, err := txmp.CheckTx(ctx, lowPriorityTx) require.NoError(t, err) - _, err = txmp.CheckTx(ctx, highPriorityTx, TxInfo{}) + _, err = txmp.CheckTx(ctx, highPriorityTx) require.NoError(t, err) - require.Equal(t, 2, txmp.PendingSize(), "pending store keeps both txs") - for byAddrNonce := range txmp.byAddrNonce.Lock() { - wtx, ok := byAddrNonce[evmAddrNonce{Address: sender, Nonce: 6}] - require.True(t, ok, "nonce bookkeeping should track one occupied nonce") - require.Equal(t, types.Tx(highPriorityTx).Hash(), wtx.Hash()) - } + require.Equal(t, 1, txmp.PendingSize()) + require.Equal(t, 1, txmp.Size()) + _, ok := txmp.txStore.ByHash(types.Tx(lowPriorityTx).Hash()) + require.False(t, ok) + _, ok = txmp.txStore.ByHash(types.Tx(highPriorityTx).Hash()) + require.True(t, ok) require.Equal(t, uint64(5), txmp.EvmNextPendingNonce(sender)) } diff --git a/sei-tendermint/internal/mempool/testonly.go b/sei-tendermint/internal/mempool/testonly.go index b351390813..30132013c8 100644 --- a/sei-tendermint/internal/mempool/testonly.go +++ b/sei-tendermint/internal/mempool/testonly.go @@ -1,11 +1,17 @@ package mempool +import ( + "time" + + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" +) + func TestConfig() *Config { cfg := DefaultConfig() cfg.CacheSize = 1000 cfg.DropUtilisationThreshold = 0.0 // Disable TTL purging in tests. - cfg.TTLNumBlocks = 0 - cfg.TTLDuration = 0 + cfg.TTLNumBlocks = utils.None[int64]() + cfg.TTLDuration = utils.None[time.Duration]() return cfg } diff --git a/sei-tendermint/internal/mempool/tx.go b/sei-tendermint/internal/mempool/tx.go index e057e93e76..264bb6c98e 100644 --- a/sei-tendermint/internal/mempool/tx.go +++ b/sei-tendermint/internal/mempool/tx.go @@ -1,92 +1,77 @@ package mempool import ( + "cmp" "context" "errors" + "fmt" "math/big" - "sync/atomic" + "slices" "time" "github.com/ethereum/go-ethereum/common" - abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/reservoir" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) -// 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 +var errDuplicateTx = errors.New("duplicate tx") +var errOldNonce = errors.New("nonce too old") +var errSameNonce = errors.New("tx with this nonce already in mempool") +var errMempoolFull = errors.New("mempool full") - // SenderNodeID is the actual types.NodeID of the sender. - SenderNodeID types.NodeID +type evmAddrNonce struct { + Address common.Address + Nonce uint64 } type hashedTx struct { - tx types.Tx - hash types.TxHash + tx types.Tx + hash types.TxHash + protoSize int64 } func newHashedTx(tx types.Tx) hashedTx { - return hashedTx{tx: tx, hash: tx.Hash()} + return hashedTx{tx: tx, hash: tx.Hash(), + protoSize: types.ComputeProtoSizeForTxs([]types.Tx{tx}), + } } func (ktx *hashedTx) Tx() types.Tx { return ktx.tx } func (ktx *hashedTx) Hash() types.TxHash { return ktx.hash } -func (ktx *WrappedTx) Size() int { return len(ktx.tx) } +func (ktx *hashedTx) Size() uint64 { return uint64(len(ktx.tx)) } // WrappedTx defines a wrapper around a raw transaction with additional metadata // that is used for indexing. type WrappedTx struct { - // hashedTx represents the raw binary transaction data and its memoized hash. hashedTx + height int64 // height defines the height at which the transaction was validated at + gasWanted int64 // gasWanted defines the amount of gas the transaction sender requires + estimatedGas int64 // estimatedGas defines the amount of gas that the transaction is estimated to use + priority int64 // ResponseCheckTx.priority + timestamp time.Time // time at which the transaction was received + evm utils.Option[evmTx] // evm transaction info + + readyEl utils.Option[*clist.CElement[types.Tx]] +} - // 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 - - // estimatedGas defines the amount of gas that the transaction is estimated to use - estimatedGas int64 - - // priority defines the transaction's priority as specified by the application - // in the ResponseCheckTx response. - priority int64 - - // 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[*WrappedTx] - - // 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 - - // evm properties that aid in prioritization - evm utils.Option[evmTx] +func (wtx *WrappedTx) check(c TxConstraints) error { + if wtx.gasWanted < 0 { + return fmt.Errorf("negative gas wanted: %d", wtx.gasWanted) + } + if c.MaxGas >= 0 && wtx.gasWanted > c.MaxGas { + return fmt.Errorf("gas wanted exceeds max gas: gas wanted %d is greater than max gas %d", wtx.gasWanted, c.MaxGas) + } + return nil } type evmTx struct { address common.Address seiAddress []byte nonce uint64 - // evmRequiredBalance is the sender balance threshold for this EVM tx to become ready. + // requiredBalance is the sender balance threshold for this EVM tx to become ready. requiredBalance *big.Int } @@ -99,320 +84,519 @@ func (wtx *WrappedTx) EVMNonce() uint64 { return 0 } +type evmAccount struct { + balance *big.Int + firstNonce uint64 + nextNonce uint64 +} + +type txCounter struct { + count int + bytes uint64 +} + +func (c *txCounter) Inc(bytes uint64) { + c.count += 1 + c.bytes += bytes +} + +func (c *txCounter) Dec(bytes uint64) { + c.count -= 1 + c.bytes -= bytes +} + +type txStoreState struct { + ready txCounter + total txCounter +} + +// Partial order. +func (c *txCounter) LessEqual(b *txCounter) bool { + return c.count <= b.count && c.bytes <= b.bytes +} + +func (s txStoreState) PendingBytes() uint64 { return s.total.bytes - s.ready.bytes } +func (s txStoreState) PendingCount() int { return s.total.count - s.ready.count } + type txStoreInner struct { - byHash map[types.TxHash]*WrappedTx // primary index - sizeBytes utils.AtomicSend[int64] + byHash map[types.TxHash]*WrappedTx + byNonce map[evmAddrNonce]*WrappedTx + accounts map[common.Address]*evmAccount + + softLimit txCounter + hardLimit txCounter + state utils.AtomicSend[txStoreState] + + // Cache of already seen txs, reducess pressure on app. + // It is a superset of transactions in txStore. + // * successfully inserted transactions are automatically added to cache. + // * txs which fail Insert() are NOT added to cache and can be reattempted later. + // * invalid transactions can be recorded via CachePush. + // * txs dropped due to pruning are removed from cache. + // * txs successfully executed are kept in cache to avoid reinsert. + // * txs failed execution are eligible to be reexecuted once (iff config.KeepInvalidTxsInCache). + cache *lruTxCache + // Tracks transactions which already failed execution once + // but are eligible for reexecution (not added yet to cache) + failedTxs *lruTxCache } -// 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 { - inner utils.RWMutex[*txStoreInner] - sizeBytes utils.AtomicRecv[int64] +// Properties: +// - tx is ready if all txs with lower nonces are ready or executed AND +// balance >= tx.requiredBalance +// - we keep at most 1 tx per nonce +// - we prefer ready tx to pending tx (then tx with the higher priority) for the same nonce +// - we don't store txs below account nonce. +// - account nonces are evaluated once per height +// - we keep at least capacity and up to 2*capacity txs +// - we reap by highest prio, while respecting nonces. +// - non-evm txs are always ready +type txStore struct { + config *Config + app *proxy.Proxy + metrics *Metrics + + inner utils.RWMutex[*txStoreInner] + state utils.AtomicRecv[txStoreState] + // List of transactions that were ready now OR at some point in the past. + // It is used for gossip and has to be stable - we cannot afford removing and reinserting transactions to this list, + // because it would cause them to be regossiped. + readyTxs *clist.CList[types.Tx] + // Sampler of priorites of all inserted READY txs. + // Used by TxMempool to damp re-gossiping of transactions. + priorityReservoir *reservoir.Sampler[int64] } -func NewTxStore() *TxStore { +func NewTxStore(cfg *Config, app *proxy.Proxy, metrics *Metrics) *txStore { + softLimit := txCounter{count: cfg.Size + cfg.PendingSize, bytes: utils.Clamp[uint64](cfg.MaxTxsBytes + cfg.MaxPendingTxsBytes)} + hardLimit := txCounter{count: 2 * softLimit.count, bytes: 2 * softLimit.bytes} inner := &txStoreInner{ - byHash: make(map[types.TxHash]*WrappedTx), - sizeBytes: utils.NewAtomicSend[int64](0), + byHash: map[types.TxHash]*WrappedTx{}, + byNonce: map[evmAddrNonce]*WrappedTx{}, + accounts: map[common.Address]*evmAccount{}, + softLimit: softLimit, + hardLimit: hardLimit, + state: utils.NewAtomicSend(txStoreState{}), + cache: newLRUTxCache(cfg.CacheSize, maxCacheKeySize), + failedTxs: newLRUTxCache(cfg.CacheSize, maxCacheKeySize), } - return &TxStore{ - inner: utils.NewRWMutex(inner), - sizeBytes: inner.sizeBytes.Subscribe(), + return &txStore{ + config: cfg, + app: app, + metrics: metrics, + inner: utils.NewRWMutex(inner), + state: inner.state.Subscribe(), + readyTxs: clist.New[types.Tx](), + priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG } } -// Size returns the total number of transactions in the store. -func (txs *TxStore) Size() int { - for inner := range txs.inner.RLock() { - return len(inner.byHash) +func (s *txStore) Clear() { + for inner := range s.inner.Lock() { + inner.cache.Reset() + s.metrics.CacheSize.Set(float64(inner.cache.Size())) + inner.failedTxs.Reset() + inner.byHash = map[types.TxHash]*WrappedTx{} + inner.byNonce = map[evmAddrNonce]*WrappedTx{} + inner.accounts = map[common.Address]*evmAccount{} + inner.state.Store(txStoreState{}) + s.readyTxs.Clear() } - panic("unreachable") } -// AllTxsBytes returns the total size in bytes of all transactions in the store. -func (txs *TxStore) AllTxsBytes() int64 { - return txs.sizeBytes.Load() -} - -// WaitForTxs waits until the store becomes non-empty. -func (txs *TxStore) WaitForTxs(ctx context.Context) error { - _, err := txs.sizeBytes.Wait(ctx, func(sizeBytes int64) bool { return sizeBytes > 0 }) - return err -} - -// GetAllTxs returns all the transactions currently in the store. -func (txs *TxStore) GetAllTxs() []*WrappedTx { - for inner := range txs.inner.RLock() { - wTxs := make([]*WrappedTx, len(inner.byHash)) - i := 0 - for _, wtx := range inner.byHash { - wTxs[i] = wtx - i++ - } - return wTxs +// Checks if cache contains a given hash. +func (s *txStore) CacheHas(txHash types.TxHash) bool { + for inner := range s.inner.RLock() { + return inner.cache.Has(txHash) } panic("unreachable") } -// GetOlderThan have older timestamp than minTime OR lower height than minHeight. -func (txs *TxStore) GetOlderThan(minTime utils.Option[time.Time], minHeight utils.Option[int64]) []*WrappedTx { - var older []*WrappedTx - for inner := range txs.inner.Lock() { - for _, wtx := range inner.byHash { - isOlder := func() bool { - if t, ok := minTime.Get(); ok && wtx.timestamp.Before(t) { - return true - } - if h, ok := minHeight.Get(); ok && wtx.height < h { - return true - } - return false - }() - if isOlder { - older = append(older, wtx) - } +// Pushes a tx to cache, effectively blocking it from being inserted. +func (s *txStore) CachePush(txHash types.TxHash) { + if s.config.KeepInvalidTxsInCache { + for inner := range s.inner.Lock() { + inner.cache.Push(txHash) + s.metrics.CacheSize.Set(float64(inner.cache.Size())) } } - return older } -// GetTxByHash returns a *WrappedTx by the transaction's hash. -func (txs *TxStore) GetTxByHash(key types.TxHash) *WrappedTx { - for inner := range txs.inner.RLock() { - return inner.byHash[key] +// Removes a tx from cache. +func (s *txStore) CacheRemove(txHash types.TxHash) { + for inner := range s.inner.Lock() { + inner.cache.Remove(txHash) + s.metrics.CacheSize.Set(float64(inner.cache.Size())) } - panic("unreachable") } -func (txs *TxStore) IsTxRemovedByHash(txHash types.TxHash) bool { - for inner := range txs.inner.RLock() { - wtx, ok := inner.byHash[txHash] - return !ok || wtx.removed - } - panic("unreachable") +// Size returns the total number of transactions in the store. +func (s *txStore) State() txStoreState { return s.state.Load() } + +// WaitForTxs waits until there is >0 ready txs. +func (s *txStore) WaitForTxs(ctx context.Context) error { + _, err := s.state.Wait(ctx, func(state txStoreState) bool { return state.ready.count > 0 }) + return err } -// IsTxRemoved returns true if a transaction by hash is marked as removed and -// false otherwise. -func (txs *TxStore) IsTxRemoved(wtx *WrappedTx) bool { - for inner := range txs.inner.RLock() { - // if this instance has already been marked, return true - if wtx.removed { - return true - } - // otherwise if the same hash exists, return its state - wtx, ok := inner.byHash[wtx.Hash()] - if ok { - return wtx.removed +// Nonce for the next tx of the given account to insert to mempool. +// It takes into consideration the account nonce at the last executed block +// and all the txs currently queued in the mempool. +func (s *txStore) NextNonce(addr common.Address) uint64 { + for inner := range s.inner.RLock() { + if acc, ok := inner.accounts[addr]; ok { + return acc.nextNonce } } - // otherwise we haven't seen this tx - return false + return s.app.EvmNonce(addr) } -// SetTx stores a *WrappedTx by its hash. -func (txs *TxStore) SetTx(wtx *WrappedTx) { - for inner := range txs.inner.Lock() { - existing := inner.byHash[wtx.Hash()] - inner.byHash[wtx.Hash()] = wtx - if existing == nil { - inner.sizeBytes.Store(inner.sizeBytes.Load() + int64(wtx.Size())) +// Returns all ready txs. +func (s *txStore) ReadyTxs() []*WrappedTx { + var res []*WrappedTx + for inner := range s.inner.RLock() { + for _, wtx := range inner.byHash { + if inner.isReady(wtx) { + res = append(res, wtx) + } } } + return res } -// RemoveTx removes a *WrappedTx from the transaction store. It deletes all -// indexes of the transaction. -func (txs *TxStore) RemoveTx(wtx *WrappedTx) { - for inner := range txs.inner.Lock() { - if _, ok := inner.byHash[wtx.Hash()]; ok { - delete(inner.byHash, wtx.Hash()) - inner.sizeBytes.Store(inner.sizeBytes.Load() - int64(wtx.Size())) +func (s *txStore) ByHash(key types.TxHash) (types.Tx, bool) { + for inner := range s.inner.RLock() { + if wtx, ok := inner.byHash[key]; ok { + return wtx.Tx(), true } - wtx.removed = true } + return nil, false } -// 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(key types.TxHash, peerID uint16) bool { - for inner := range txs.inner.RLock() { - wtx := inner.byHash[key] - if wtx == nil { - return false +func (s *txStore) SafeGetTxsForHashes(txHashes []types.TxHash) (types.Txs, []types.TxHash) { + got := make([]types.Tx, 0, len(txHashes)) + missing := make([]types.TxHash, 0) + for inner := range s.inner.RLock() { + for _, txHash := range txHashes { + if wtx, ok := inner.byHash[txHash]; ok { + got = append(got, wtx.Tx()) + } else { + missing = append(missing, txHash) + } } - _, ok := wtx.peers[peerID] - return ok } - panic("unreachable") + return got, missing } -// 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.TxHash, peerID uint16) (*WrappedTx, bool) { - for inner := range txs.inner.Lock() { - wtx := inner.byHash[hash] - if wtx == nil { - return nil, false +func (s *txStore) insert(inner *txStoreInner, wtx *WrappedTx) error { + if _, ok := inner.byHash[wtx.Hash()]; ok { + return errDuplicateTx + } + state := inner.state.Load() + if evm, ok := wtx.evm.Get(); ok { + // Fetch the evm account state. + account, ok := inner.accounts[evm.address] + if !ok { + // TODO(gprusak): consider whether we should move these queries out of the mutex. + b := s.app.EvmBalance(evm.address, evm.seiAddress) + n := s.app.EvmNonce(evm.address) + account = &evmAccount{b, n, n} + inner.accounts[evm.address] = account } - - if wtx.peers == nil { - wtx.peers = make(map[uint16]struct{}) + // Reject transactions with old nonces. + if evm.nonce < account.firstNonce { + return errOldNonce } - - if _, ok := wtx.peers[peerID]; ok { - return wtx, true + an := evmAddrNonce{evm.address, evm.nonce} + if old, ok := inner.byNonce[an]; ok { + oldReady := old.evm.OrPanic("non-evm tx").nonce < account.nextNonce + // If the old tx is ready but the new tx is not, then reject the new tx. + if oldReady && account.balance.Cmp(evm.requiredBalance) < 0 { + return errSameNonce + } + // If the old tx has >= priority, then reject new tx. + if old.priority >= wtx.priority { + return errSameNonce + } + // Remove the old transaction. + inner.cache.Remove(old.Hash()) // evicted txs are not cached + s.metrics.CacheSize.Set(float64(inner.cache.Size())) + delete(inner.byHash, old.Hash()) + s.metrics.RemovedTxs.Add(1) + state.total.Dec(old.Size()) + if el, ok := old.readyEl.Get(); ok { + s.readyTxs.Remove(el) + } + if oldReady { + state.ready.Dec(old.Size()) + state.ready.Inc(wtx.Size()) + wtx.readyEl = utils.Some(s.readyTxs.PushBack(wtx.Tx())) + } + } + state.total.Inc(wtx.Size()) + inner.byNonce[an] = wtx + // Update account ready txs. + for { + an.Nonce = account.nextNonce + wtx, ok := inner.byNonce[an] + if !ok || account.balance.Cmp(wtx.evm.OrPanic("non-evm tx").requiredBalance) < 0 { + break + } + account.nextNonce += 1 + state.ready.Inc(wtx.Size()) + if !wtx.readyEl.IsPresent() { + wtx.readyEl = utils.Some(s.readyTxs.PushBack(wtx.Tx())) + } + } + } else { + // Non-evm txs are automatically ready + state.total.Inc(wtx.Size()) + state.ready.Inc(wtx.Size()) + if !wtx.readyEl.IsPresent() { + wtx.readyEl = utils.Some(s.readyTxs.PushBack(wtx.Tx())) } - - wtx.peers[peerID] = struct{}{} - return wtx, false } - panic("unreachable") + inner.byHash[wtx.Hash()] = wtx + inner.state.Store(state) + return nil } -type PendingTxs struct { - inner utils.RWMutex[*pendingTxsInner] - config *Config - sizeBytes atomic.Int64 +// WARNING: works only if wtx has been already inserted. +func (inner *txStoreInner) isReady(wtx *WrappedTx) bool { + evm, ok := wtx.evm.Get() + return !ok || evm.nonce < inner.accounts[evm.address].nextNonce } -type pendingTxsInner struct { - txs []*WrappedTx -} - -func NewPendingTxs(conf *Config) *PendingTxs { - return &PendingTxs{ - inner: utils.NewRWMutex(&pendingTxsInner{}), - config: conf, +// Sorts transactions in inclusion order. Here we effectively simulate the following: +// * find account with the highest priority lowest nonce ready transaction and pop this transaction +// * repeat until no ready transactions are available +// * then repeat the same but for pending transactions (i.e. again in per-account nonce order, high priority first, just ignoring readiness) +// Cosmos transactions are all considered ready and from different accounts, so only priority is relevant. +func (inner *txStoreInner) inInclusionOrder() []*WrappedTx { + // Split txs into ready and pending. + var ready, pending []*WrappedTx + for _, wtx := range inner.byHash { + if inner.isReady(wtx) { + ready = append(ready, wtx) + } else { + pending = append(pending, wtx) + } } -} - -func (p *PendingTxs) EvaluatePendingTransactions( - evaluate func(*WrappedTx) abci.PendingTxCheckerResponse, -) ( - acceptedTxs []*WrappedTx, - rejectedTxs []*WrappedTx, -) { - poppedIndices := []int{} - for inner := range p.inner.Lock() { - for i := 0; i < len(inner.txs); i++ { - result := evaluate(inner.txs[i]) - switch result { - case abci.Accepted: - acceptedTxs = append(acceptedTxs, inner.txs[i]) - poppedIndices = append(poppedIndices, i) - case abci.Rejected: - rejectedTxs = append(rejectedTxs, inner.txs[i]) - poppedIndices = append(poppedIndices, i) + // Cap priority to obtain a linear order of txs per account by nonce. + // NOTE: this precisely emulates the heap behavior described in this functions docstring. + accPrio := make(map[common.Address]int64, len(inner.accounts)) + for _, txs := range utils.Slice(ready, pending) { + // Sort by nonce. + slices.SortFunc(txs, func(a, b *WrappedTx) int { return cmp.Compare(a.EVMNonce(), b.EVMNonce()) }) + txPrio := make(map[*WrappedTx]int64, len(txs)) + for _, tx := range txs { + if evm, ok := tx.evm.Get(); ok { + if prio, ok := accPrio[evm.address]; !ok || prio > tx.priority { + accPrio[evm.address] = tx.priority + } + txPrio[tx] = accPrio[evm.address] + } else { + txPrio[tx] = tx.priority } } - p.popTxsAtIndices(inner, poppedIndices) - return + // Stable sort by capped priority - it preserves the nonce ordering. + slices.SortStableFunc(txs, func(a, b *WrappedTx) int { return -cmp.Compare(txPrio[a], txPrio[b]) }) } - panic("unreachable") + return append(ready, pending...) } -// Assumes the pending tx store is already write-locked. -func (p *PendingTxs) popTxsAtIndices(inner *pendingTxsInner, indices []int) { - if len(indices) == 0 { - return - } - newTxs := make([]*WrappedTx, 0, max(0, len(inner.txs)-len(indices))) - start := 0 - for _, idx := range indices { - if idx <= start-1 { - panic("indices popped from pending tx store should be sorted without duplicate") +// Inserts a new transaction to txStore. +// txStore takes ownership of wtx. +func (s *txStore) Insert(wtx *WrappedTx) error { + for inner := range s.inner.Lock() { + if err := s.insert(inner, wtx); err != nil { + return err + } + if inner.isReady(wtx) { + s.priorityReservoir.Add(wtx.priority) } - if idx >= len(inner.txs) { - panic("indices popped from pending tx store out of range") + if total := inner.state.Load().total; !total.LessEqual(&inner.hardLimit) { + s.compact(inner, false) + if _, ok := inner.byHash[wtx.Hash()]; !ok { + return errMempoolFull + } } - p.sizeBytes.Add(int64(-inner.txs[idx].Size())) - newTxs = append(newTxs, inner.txs[start:idx]...) - start = idx + 1 + inner.cache.Push(wtx.Hash()) + s.metrics.CacheSize.Set(float64(inner.cache.Size())) } - newTxs = append(newTxs, inner.txs[start:]...) - inner.txs = newTxs + return nil } -func (p *PendingTxs) Insert(tx *WrappedTx) error { - for inner := range p.inner.Lock() { - if len(inner.txs) >= p.config.PendingSize || int64(tx.Size())+p.sizeBytes.Load() > p.config.MaxPendingTxsBytes { - return errors.New("pending store is full") +// O(m log m), prunes transactions above softLimit and recomputes all the indices. +func (s *txStore) compact(inner *txStoreInner, clearAccounts bool) { + // Order all txs by priority. + wtxs := inner.inInclusionOrder() + // Reset internal state. + inner.state.Store(txStoreState{}) + inner.byHash = map[types.TxHash]*WrappedTx{} + inner.byNonce = map[evmAddrNonce]*WrappedTx{} + if clearAccounts { + inner.accounts = map[common.Address]*evmAccount{} + } + for _, account := range inner.accounts { + account.nextNonce = account.firstNonce + } + for _, wtx := range wtxs { + total := inner.state.Load().total + total.Inc(wtx.Size()) + limitOk := total.LessEqual(&inner.softLimit) + // NOTE: insertion is lazily evaluated here. + if !limitOk || s.insert(inner, wtx) != nil { + // NOTE: evicted txs are not cached unconditionally + if !limitOk || !s.config.KeepInvalidTxsInCache { + inner.cache.Remove(wtx.Hash()) + } + s.metrics.RemovedTxs.Add(1) + s.metrics.EvictedTxs.Add(1) + if el, ok := wtx.readyEl.Get(); ok { + s.readyTxs.Remove(el) + } } - inner.txs = append(inner.txs, tx) - p.sizeBytes.Add(int64(tx.Size())) - return nil } - panic("unreachable") + s.metrics.CacheSize.Set(float64(inner.cache.Size())) } -func (p *PendingTxs) SizeBytes() int64 { return p.sizeBytes.Load() } +type updateSpec struct { + Now time.Time + Height int64 + TxResults map[types.TxHash]bool // true - success, false - failed, missing - not executed + Constraints TxConstraints + NewPriorities map[types.TxHash]int64 + InvalidTxs map[types.TxHash]bool +} -func (p *PendingTxs) Peek(max int) []*WrappedTx { - for inner := range p.inner.RLock() { - // priority is fifo - if max > len(inner.txs) { - return inner.txs +func (s *txStore) Update(spec updateSpec) { + minHeight := utils.None[int64]() + if ttl, ok := s.config.TTLNumBlocks.Get(); ok && spec.Height > ttl { + minHeight = utils.Some(spec.Height - ttl) + } + minTime := utils.None[time.Time]() + if d, ok := s.config.TTLDuration.Get(); ok { + minTime = utils.Some(spec.Now.Add(-d)) + } + isExpired := func(wtx *WrappedTx) bool { + if t, ok := minTime.Get(); ok && wtx.timestamp.Before(t) { + return true + } + if h, ok := minHeight.Get(); ok && wtx.height < h { + return true } - return inner.txs[:max] + return false } - panic("unreachable") -} - -func (p *PendingTxs) Size() int { - for inner := range p.inner.RLock() { - return len(inner.txs) + for inner := range s.inner.Lock() { + for txHash, wtx := range inner.byHash { + expired := isExpired(wtx) + if expired { + s.metrics.ExpiredTxs.Add(1) + } + invalid := spec.InvalidTxs[wtx.Hash()] || wtx.check(spec.Constraints) != nil + success, executed := spec.TxResults[wtx.Hash()] + remove := invalid || executed || (expired && (s.config.RemoveExpiredTxsFromQueue || !inner.isReady(wtx))) + if remove { + // KeepInvalidTxsInCache decides whether we give just 1 chance to each inserted transaction. + // In particular expired transactions caching depends on it. + // If not set, we just cache executed transactions (and txs invalidated pre-insertion) + if !s.config.KeepInvalidTxsInCache { + // Cleanup the cache. + // We keep executed txs in cache, unless they failed + // in which case we give them a second attempt. + // NOTE: failedTxs.Push is executed lazily. + if !executed || (!success && inner.failedTxs.Push(txHash)) { + inner.cache.Remove(txHash) + } else { + inner.failedTxs.Remove(txHash) + } + } + delete(inner.byHash, txHash) + s.metrics.RemovedTxs.Add(1) + if el, ok := wtx.readyEl.Get(); ok { + s.readyTxs.Remove(el) + } + } else if newPriority, ok := spec.NewPriorities[wtx.Hash()]; ok { + wtx.priority = newPriority + } + } + s.compact(inner, true) } - panic("unreachable") } -func (p *PendingTxs) PurgeExpired(blockHeight int64, now time.Time, cb func(wtx *WrappedTx)) { - for inner := range p.inner.Lock() { - if len(inner.txs) == 0 { - return - } +type ReapLimits struct { + MaxTxs utils.Option[uint64] + MaxBytes utils.Option[int64] // Max total bytes in proto representation. + MaxGasWanted utils.Option[int64] + MaxGasEstimated utils.Option[int64] +} - // txs retains the ordering of insertion - if p.config.TTLNumBlocks > 0 { - idxFirstNotExpiredTx := len(inner.txs) - for i, ptx := range inner.txs { - // once found, we can break because these are ordered - if (blockHeight - ptx.height) <= p.config.TTLNumBlocks { - idxFirstNotExpiredTx = i +// Reap returns a list of transactions within the provided tx, +// byte, and gas constraints together with the total estimated gas for the +// returned transactions. Reaped txs are removed iff remove == true. +// O(m log m) where m is the size of the txStore. +func (s *txStore) Reap(l ReapLimits, remove bool) (types.Txs, int64) { + maxTxs := l.MaxTxs.Or(utils.Max[uint64]()) + maxBytes := l.MaxBytes.Or(utils.Max[int64]()) + maxGasWanted := l.MaxGasWanted.Or(utils.Max[int64]()) + maxGasEstimated := l.MaxGasEstimated.Or(utils.Max[int64]()) + if maxBytes < 0 { + maxBytes = utils.Max[int64]() + } + if maxGasWanted < 0 { + maxGasWanted = utils.Max[int64]() + } + if maxGasEstimated < 0 { + maxGasEstimated = utils.Max[int64]() + } + totalGasWanted := int64(0) + totalGasEstimated := int64(0) + totalSize := int64(0) + + var wtxs []*WrappedTx + for inner := range s.inner.Lock() { + if uint64(inner.state.Load().ready.count) >= s.config.TxNotifyThreshold { //nolint:gosec // count is non-negative + for _, wtx := range inner.inInclusionOrder() { + if uint64(len(wtxs)) >= maxTxs || !inner.isReady(wtx) { break } - cb(ptx) - p.sizeBytes.Add(int64(-ptx.Size())) + if maxBytes-totalSize < wtx.protoSize { + break + } + if maxGasWanted-totalGasWanted < wtx.gasWanted { + break + } + if maxGasEstimated-totalGasEstimated < wtx.estimatedGas { + break + } + // include tx and update totals + totalSize += wtx.protoSize + totalGasWanted += wtx.gasWanted + totalGasEstimated += wtx.estimatedGas + wtxs = append(wtxs, wtx) } - inner.txs = inner.txs[idxFirstNotExpiredTx:] - } - - if len(inner.txs) == 0 { - return } - - if p.config.TTLDuration > 0 { - idxFirstNotExpiredTx := len(inner.txs) - for i, ptx := range inner.txs { - // once found, we can break because these are ordered - if now.Sub(ptx.timestamp) <= p.config.TTLDuration { - idxFirstNotExpiredTx = i - break + if remove { + for _, wtx := range wtxs { + delete(inner.byHash, wtx.Hash()) + s.metrics.RemovedTxs.Add(1) + if el, ok := wtx.readyEl.Get(); ok { + s.readyTxs.Remove(el) } - cb(ptx) - p.sizeBytes.Add(int64(-ptx.Size())) } - inner.txs = inner.txs[idxFirstNotExpiredTx:] + s.compact(inner, false) } - return } - panic("unreachable") + + // EVM txs go first. + var evmTxs, nonEvmTxs types.Txs + for _, wtx := range wtxs { + if wtx.evm.IsPresent() { + evmTxs = append(evmTxs, wtx.Tx()) + } else { + nonEvmTxs = append(nonEvmTxs, wtx.Tx()) + } + } + return append(evmTxs, nonEvmTxs...), totalGasEstimated } diff --git a/sei-tendermint/internal/mempool/tx_test.go b/sei-tendermint/internal/mempool/tx_test.go index f2c27b7701..064d9a2aac 100644 --- a/sei-tendermint/internal/mempool/tx_test.go +++ b/sei-tendermint/internal/mempool/tx_test.go @@ -1,121 +1,216 @@ package mempool import ( + "errors" "fmt" + "math/big" "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) -func TestTxStore_GetTxByHash(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("test_tx")), - priority: 1, - timestamp: time.Now(), +func newTxStoreForTest() *txStore { + return NewTxStore(TestConfig(), proxy.New(kvstore.NewApplication(), proxy.NopMetrics()), NopMetrics()) +} + +func txStoreStateForTest(ready, pending []*WrappedTx) txStoreState { + state := txStoreState{} + for _, wtx := range ready { + state.ready.Inc(wtx.Size()) + state.total.Inc(wtx.Size()) + } + for _, wtx := range pending { + state.total.Inc(wtx.Size()) } + return state +} - key := wtx.Hash() - res := txs.GetTxByHash(key) - require.Nil(t, res) +type testAccount struct { + address common.Address + baseNonce uint64 + lastNonce uint64 +} - txs.SetTx(wtx) +type testEnv struct { + rng utils.Rng + txStore *txStore + app *evmNonceApp + accounts []testAccount + byHash map[types.TxHash]*WrappedTx + everReady map[types.TxHash]struct{} +} - res = txs.GetTxByHash(key) - require.NotNil(t, res) - require.Equal(t, wtx, res) +func newTestEnv( + rng utils.Rng, + txStore *txStore, + app *evmNonceApp, + numAccounts int, +) *testEnv { + env := &testEnv{ + rng: rng, + txStore: txStore, + app: app, + accounts: make([]testAccount, numAccounts), + byHash: map[types.TxHash]*WrappedTx{}, + everReady: map[types.TxHash]struct{}{}, + } + for i := range env.accounts { + env.accounts[i].address = common.BytesToAddress(utils.GenBytes(rng, common.AddressLength)) + env.accounts[i].baseNonce = uint64(rng.Intn(20) + 1) + rangeLen := rng.Intn(16) + 12 + env.accounts[i].lastNonce = env.accounts[i].baseNonce + uint64(rangeLen-1) + env.app.setNonce(env.accounts[i].address, env.accounts[i].baseNonce) + } + return env } -func TestTxStore_SetTx(t *testing.T) { - txs := NewTxStore() - wtx := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("test_tx")), - priority: 1, - timestamp: time.Now(), +func (e *testEnv) insertTxs( + t *testing.T, + insertProbPercent int, + makeTx func(account *testAccount, nonce uint64) *WrappedTx, +) { + t.Helper() + + clear(e.byHash) + for i := range e.accounts { + account := &e.accounts[i] + rangeLen := int(account.lastNonce-account.baseNonce) + 1 + for offset := range rangeLen { + if e.rng.Intn(100) >= insertProbPercent { + continue + } + wtx := makeTx(account, account.baseNonce+uint64(offset)) + e.byHash[wtx.Hash()] = wtx + require.NoError(t, e.txStore.Insert(wtx)) + } } +} - key := wtx.Hash() - txs.SetTx(wtx) +func (e *testEnv) txs() []*WrappedTx { + txs := make([]*WrappedTx, 0, len(e.byHash)) + for _, wtx := range e.byHash { + txs = append(txs, wtx) + } + return txs +} - res := txs.GetTxByHash(key) - require.NotNil(t, res) - require.Equal(t, wtx, res) +func (e *testEnv) byNonce(account testAccount) map[uint64]*WrappedTx { + byNonce := map[uint64]*WrappedTx{} + for _, wtx := range e.byHash { + evm := wtx.evm.OrPanic("evm tx") + if evm.address == account.address { + byNonce[evm.nonce] = wtx + } + } + return byNonce } -func TestTxStore_IsTxRemoved(t *testing.T) { - // Initialize the store - txs := NewTxStore() +func (e *testEnv) readyTxs() []*WrappedTx { + var ready []*WrappedTx + for _, account := range e.accounts { + byNonce := e.byNonce(account) + currentNonce := e.app.EvmNonce(account.address) + balance := e.app.balanceOf(account.address) + for nonce := currentNonce; ; nonce++ { + wtx, ok := byNonce[nonce] + if !ok { + break + } + if int(wtx.evm.OrPanic("").requiredBalance.Int64()) > balance { + break + } + ready = append(ready, wtx) + } + } + return ready +} - // Current time for timestamping transactions - now := time.Now() +func (e *testEnv) markReadyTxs() { + for _, wtx := range e.readyTxs() { + e.everReady[wtx.Hash()] = struct{}{} + } +} - // Tests setup as a slice of anonymous structs - tests := []struct { - name string - wtx *WrappedTx - setup func(*TxStore, *WrappedTx) // Optional setup function to manipulate store state - wantRemoved bool - }{ - { - name: "Existing transaction not removed", - wtx: &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx_hash_1")), - removed: false, - timestamp: now, - }, - setup: func(ts *TxStore, w *WrappedTx) { - ts.SetTx(w) - }, - wantRemoved: false, - }, - { - name: "Existing transaction marked as removed", - wtx: &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx_hash_2")), - removed: true, - timestamp: now, - }, - setup: func(ts *TxStore, w *WrappedTx) { - ts.SetTx(w) - }, - wantRemoved: true, - }, - { - name: "Non-existing transaction", - wtx: &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx_hash_3")), - removed: false, - timestamp: now, - }, - wantRemoved: false, - }, - { - name: "Non-existing transaction but marked as removed", - wtx: &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx_hash_4")), - removed: true, - timestamp: now, - }, - wantRemoved: true, - }, +func (e *testEnv) stableReady() []*WrappedTx { + var stable []*WrappedTx + for _, wtx := range e.byHash { + if _, ok := e.everReady[wtx.Hash()]; ok { + stable = append(stable, wtx) + } } + return stable +} - // Execute test scenarios - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.setup != nil { - tt.setup(txs, tt.wtx) - } - removed := txs.IsTxRemoved(tt.wtx) - require.Equal(t, tt.wantRemoved, removed) - }) +func toTxs(wtxs []*WrappedTx) types.Txs { + var txs types.Txs + for _, wtx := range wtxs { + txs = append(txs, wtx.Tx()) } + return txs } -func TestTxStore_GetOrSetPeerByTxHash(t *testing.T) { - txs := NewTxStore() +func makeEvmTxForTest( + rng utils.Rng, + address common.Address, + nonce uint64, + priority int64, + requiredBalance int, +) *WrappedTx { + return &WrappedTx{ + hashedTx: newHashedTx(utils.GenBytes(rng, rng.Intn(48)+16)), + timestamp: time.Now(), + priority: priority, + gasWanted: 1, + estimatedGas: 1, + evm: utils.Some(evmTx{ + address: address, + seiAddress: address.Bytes(), + nonce: nonce, + requiredBalance: big.NewInt(int64(requiredBalance)), + }), + } +} + +func (e *testEnv) assertState(t *testing.T) { + t.Helper() + + expectedReady := e.readyTxs() + readySet := make(map[types.TxHash]struct{}, len(expectedReady)) + for _, wtx := range expectedReady { + readySet[wtx.Hash()] = struct{}{} + } + var expectedPending []*WrappedTx + for _, wtx := range e.txs() { + if _, ok := readySet[wtx.Hash()]; ok { + continue + } + expectedPending = append(expectedPending, wtx) + } + expectedStableReady := e.stableReady() + + require.Equal(t, txStoreStateForTest(expectedReady, expectedPending), e.txStore.State()) + + readyTxs := e.txStore.ReadyTxs() + require.ElementsMatch(t, toTxs(expectedReady), toTxs(readyTxs)) + + reaped, _ := e.txStore.Reap(ReapLimits{MaxTxs: utils.Some(uint64(len(e.byHash)))}, false) + require.ElementsMatch(t, toTxs(expectedReady), reaped) + + var listedTxs types.Txs + for el := e.txStore.readyTxs.Front(); el != nil; el = el.Next() { + listedTxs = append(listedTxs, el.Value()) + } + require.ElementsMatch(t, toTxs(expectedStableReady), listedTxs) +} + +func TestTxStore_GetTxByHash(t *testing.T) { + txs := newTxStoreForTest() wtx := &WrappedTx{ hashedTx: newHashedTx(types.Tx("test_tx")), priority: 1, @@ -123,205 +218,529 @@ func TestTxStore_GetOrSetPeerByTxHash(t *testing.T) { } key := wtx.Hash() - txs.SetTx(wtx) - - res, ok := txs.GetOrSetPeerByTxHash(types.Tx([]byte("test_tx_2")).Hash(), 15) - require.Nil(t, res) + res, ok := txs.ByHash(key) require.False(t, ok) + require.Nil(t, res) - res, ok = txs.GetOrSetPeerByTxHash(key, 15) - require.NotNil(t, res) - require.False(t, ok) + require.NoError(t, txs.Insert(wtx)) - res, ok = txs.GetOrSetPeerByTxHash(key, 15) - require.NotNil(t, res) + res, ok = txs.ByHash(key) require.True(t, ok) - - require.True(t, txs.TxHasPeer(key, 15)) - require.False(t, txs.TxHasPeer(key, 16)) + require.Equal(t, wtx.Tx(), res) } -func TestTxStore_RemoveTx(t *testing.T) { - txs := NewTxStore() +func TestTxStore_SetTx(t *testing.T) { + txs := newTxStoreForTest() wtx := &WrappedTx{ hashedTx: newHashedTx(types.Tx("test_tx")), priority: 1, timestamp: time.Now(), } - txs.SetTx(wtx) - key := wtx.Hash() - res := txs.GetTxByHash(key) - require.NotNil(t, res) + require.NoError(t, txs.Insert(wtx)) - txs.RemoveTx(res) - - res = txs.GetTxByHash(key) - require.Nil(t, res) + res, ok := txs.ByHash(key) + require.True(t, ok) + require.Equal(t, wtx.Tx(), res) } func TestTxStore_Size(t *testing.T) { - txStore := NewTxStore() + txStore := newTxStoreForTest() numTxs := 1000 for i := range numTxs { - txStore.SetTx(&WrappedTx{ + require.NoError(t, txStore.Insert(&WrappedTx{ hashedTx: newHashedTx(fmt.Appendf(nil, "test_tx_%d", i)), priority: int64(i), timestamp: time.Now(), - }) + })) } - require.Equal(t, numTxs, txStore.Size()) + require.Equal(t, numTxs, txStore.State().total.count) } -func TestPendingTxsPopTxsGood(t *testing.T) { - pendingTxs := NewPendingTxs(DefaultConfig()) - for _, test := range []struct { - origLen int - popIndices []int - expected []int - }{ - { - origLen: 1, - popIndices: []int{}, - expected: []int{0}, - }, { - origLen: 1, - popIndices: []int{0}, - expected: []int{}, - }, { - origLen: 2, - popIndices: []int{0}, - expected: []int{1}, - }, { - origLen: 2, - popIndices: []int{1}, - expected: []int{0}, - }, { - origLen: 2, - popIndices: []int{0, 1}, - expected: []int{}, - }, { - origLen: 3, - popIndices: []int{1}, - expected: []int{0, 2}, - }, { - origLen: 3, - popIndices: []int{0, 2}, - expected: []int{1}, - }, { - origLen: 3, - popIndices: []int{0, 1, 2}, - expected: []int{}, - }, { - origLen: 5, - popIndices: []int{0, 1, 4}, - expected: []int{2, 3}, - }, { - origLen: 5, - popIndices: []int{1, 3}, - expected: []int{0, 2, 4}, - }, - } { - for inner := range pendingTxs.inner.Lock() { - inner.txs = []*WrappedTx{} - pendingTxs.sizeBytes.Store(0) - for i := 0; i < test.origLen; i++ { - inner.txs = append(inner.txs, &WrappedTx{ - hashedTx: newHashedTx(types.Tx{byte(i)}), - peers: map[uint16]struct{}{uint16(i): {}}, - }) +func TestTxStore_RejectsAndEvictsTransactionsBelowAccountNonce(t *testing.T) { + rng := utils.TestRng() + app := newEVMNonceApp() + txStore := NewTxStore(TestConfig(), proxy.New(app, proxy.NopMetrics()), NopMetrics()) + + makeTx := func(address common.Address, nonce uint64) *WrappedTx { + requiredBalance := big.NewInt(rng.Int63n(256)) + return &WrappedTx{ + hashedTx: newHashedTx(utils.GenBytes(rng, 32)), + timestamp: time.Now(), + priority: rng.Int63n(1_000_000) + 1, + gasWanted: 1, + estimatedGas: 1, + evm: utils.Some(evmTx{ + address: address, + seiAddress: address.Bytes(), + nonce: nonce, + requiredBalance: requiredBalance, + }), + } + } + + // Seed the store with sparse per-account nonce ranges so each account has a + // mix of contiguous ready transactions and gaps that keep later transactions + // pending. + env := newTestEnv(rng, txStore, app, 8) + for _, account := range env.accounts { + app.setBalance(account.address, rng.Intn(256)) + } + env.insertTxs(t, 80, func(account *testAccount, nonce uint64) *WrappedTx { + return makeTx(account.address, nonce) + }) + for _, account := range env.accounts { + rejected := makeTx(account.address, account.baseNonce-1) + require.ErrorIs(t, txStore.Insert(rejected), errOldNonce) + } + + require.Equal(t, len(env.byHash), txStore.State().total.count) + + // Seed the stable-ready history with transactions that are already ready + // after the initial inserts. + env.markReadyTxs() + + // Advance the per-account nonce frontier in several randomized rounds and + // verify that Update removes every transaction that fell below the account + // nonce while preserving the rest. + for height := range int64(5) { + for _, account := range env.accounts { + currentNonce := app.EvmNonce(account.address) + if currentNonce > 0 { + rejected := makeTx(account.address, currentNonce-1) + require.ErrorIs(t, txStore.Insert(rejected), errOldNonce) } - pendingTxs.popTxsAtIndices(inner, test.popIndices) - require.Equal(t, len(test.expected), len(inner.txs)) - for i, e := range test.expected { - _, ok := inner.txs[i].peers[uint16(e)] - require.True(t, ok) + maxAdvance := max(0, int(account.lastNonce-currentNonce)+4) + for range rng.Intn(maxAdvance + 1) { + app.markMined(account.address) } + app.setBalance(account.address, rng.Intn(256)) } + + txStore.Update(updateSpec{ + Now: time.Now(), + Height: height + 1, + TxResults: map[types.TxHash]bool{}, + Constraints: NopTxConstraints(), + NewPriorities: map[types.TxHash]int64{}, + }) + + for txHash, wtx := range env.byHash { + if wtx.EVMNonce() < app.EvmNonce(wtx.evm.OrPanic("").address) { + delete(env.byHash, txHash) + } + } + env.markReadyTxs() + env.assertState(t) } } -func TestPendingTxsPopTxsBad(t *testing.T) { - pendingTxs := NewPendingTxs(DefaultConfig()) - // out of range - require.Panics(t, func() { - for inner := range pendingTxs.inner.Lock() { - pendingTxs.popTxsAtIndices(inner, []int{0}) +func testTxStoreUpdateExpiresTransactions(t *testing.T, removeExpiredTxsFromQueue bool) { + rng := utils.TestRng() + cfg := TestConfig() + cfg.CacheSize = 1_000 + cfg.TTLNumBlocks = utils.Some(int64(10)) + cfg.TTLDuration = utils.Some(10 * time.Second) + cfg.RemoveExpiredTxsFromQueue = removeExpiredTxsFromQueue + + app := newEVMNonceApp() + txStore := NewTxStore(cfg, proxy.New(app, proxy.NopMetrics()), NopMetrics()) + baseTime := time.Unix(1_700_000_000, 0) + + makeTx := func(address common.Address, nonce uint64, height int64, timestamp time.Time) *WrappedTx { + return &WrappedTx{ + hashedTx: newHashedTx(utils.GenBytes(rng, 32)), + height: height, + timestamp: timestamp, + priority: rng.Int63n(1_000_000) + 1, + gasWanted: 1, + estimatedGas: 1, + evm: utils.Some(evmTx{ + address: address, + seiAddress: address.Bytes(), + nonce: nonce, + requiredBalance: big.NewInt(0), + }), } + } + + // Seed the store with randomized timestamps, heights, and sparse nonce + // ranges across a bounded set of accounts. + env := newTestEnv(rng, txStore, app, 5) + for _, account := range env.accounts { + app.setBalance(account.address, 1_000_000) + } + env.insertTxs(t, 100, func(account *testAccount, nonce uint64) *WrappedTx { + return makeTx( + account.address, + nonce, + int64(rng.Intn(28)+1), + baseTime.Add(time.Duration(rng.Intn(31))*time.Second), + ) }) - // out of order - for inner := range pendingTxs.inner.Lock() { - inner.txs = []*WrappedTx{{}, {}, {}} + + // Record the transactions that are initially ready; the stable ready list + // keeps these entries until the transactions are removed. + env.markReadyTxs() + + updates := []updateSpec{ + {Now: baseTime.Add(16 * time.Second), Height: 14, TxResults: map[types.TxHash]bool{}, Constraints: NopTxConstraints(), NewPriorities: map[types.TxHash]int64{}}, + {Now: baseTime.Add(24 * time.Second), Height: 22, TxResults: map[types.TxHash]bool{}, Constraints: NopTxConstraints(), NewPriorities: map[types.TxHash]int64{}}, + {Now: baseTime.Add(36 * time.Second), Height: 34, TxResults: map[types.TxHash]bool{}, Constraints: NopTxConstraints(), NewPriorities: map[types.TxHash]int64{}}, } - require.Panics(t, func() { - for inner := range pendingTxs.inner.Lock() { - pendingTxs.popTxsAtIndices(inner, []int{1, 0}) + + for _, update := range updates { + readyBeforeUpdate := env.readyTxs() + readyBeforeUpdateSet := make(map[types.TxHash]struct{}, len(readyBeforeUpdate)) + for _, wtx := range readyBeforeUpdate { + readyBeforeUpdateSet[wtx.Hash()] = struct{}{} } - }) - // duplicate - require.Panics(t, func() { - for inner := range pendingTxs.inner.Lock() { - pendingTxs.popTxsAtIndices(inner, []int{2, 2}) + + txStore.Update(update) + minHeight := int64(-1) + if ttl, ok := cfg.TTLNumBlocks.Get(); ok && update.Height > ttl { + minHeight = update.Height - ttl } - }) + minTime := time.Time{} + if ttl, ok := cfg.TTLDuration.Get(); ok { + minTime = update.Now.Add(-ttl) + } + + for txHash, wtx := range env.byHash { + expiredByHeight := minHeight >= 0 && wtx.height < minHeight + expiredByTime := !minTime.IsZero() && wtx.timestamp.Before(minTime) + if !(expiredByHeight || expiredByTime) { + continue + } + if !cfg.RemoveExpiredTxsFromQueue { + if _, ok := readyBeforeUpdateSet[txHash]; ok { + continue + } + } + delete(env.byHash, txHash) + } + env.markReadyTxs() + env.assertState(t) + } } -func TestPendingTxs_InsertCondition(t *testing.T) { - mempoolCfg := DefaultConfig() +func TestTxStore_UpdateExpiresTransactions(t *testing.T) { + testTxStoreUpdateExpiresTransactions(t, true) +} - // First test exceeding number of txs - mempoolCfg.PendingSize = 2 +func TestTxStore_UpdateExpiresTransactionsKeepsReadyWhenConfigured(t *testing.T) { + testTxStoreUpdateExpiresTransactions(t, false) +} - pendingTxs := NewPendingTxs(mempoolCfg) +func TestTxStore_ExpiredTxCacheBehavior(t *testing.T) { + rng := utils.TestRng() + + for _, tc := range []struct { + name string + keepInvalidTxsInCache bool + removeExpiredFromQueue bool + wantReadyPresent bool + wantPendingPresent bool + wantReadyCached bool + wantPendingCached bool + }{ + { + name: "remove expired and drop from cache", + keepInvalidTxsInCache: false, + removeExpiredFromQueue: true, + wantReadyPresent: false, + wantPendingPresent: false, + wantReadyCached: false, + wantPendingCached: false, + }, + { + name: "remove expired and keep in cache", + keepInvalidTxsInCache: true, + removeExpiredFromQueue: true, + wantReadyPresent: false, + wantPendingPresent: false, + wantReadyCached: true, + wantPendingCached: true, + }, + { + name: "keep expired ready and drop expired pending from cache", + keepInvalidTxsInCache: false, + removeExpiredFromQueue: false, + wantReadyPresent: true, + wantPendingPresent: false, + wantReadyCached: true, + wantPendingCached: false, + }, + { + name: "keep expired ready and keep expired pending in cache", + keepInvalidTxsInCache: true, + removeExpiredFromQueue: false, + wantReadyPresent: true, + wantPendingPresent: false, + wantReadyCached: true, + wantPendingCached: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := TestConfig() + cfg.CacheSize = 10 + cfg.TTLDuration = utils.Some(time.Second) + cfg.TTLNumBlocks = utils.None[int64]() + cfg.KeepInvalidTxsInCache = tc.keepInvalidTxsInCache + cfg.RemoveExpiredTxsFromQueue = tc.removeExpiredFromQueue + + app := newEVMNonceApp() + txStore := NewTxStore(cfg, proxy.New(app, proxy.NopMetrics()), NopMetrics()) + env := newTestEnv(rng, txStore, app, 1) + address := env.accounts[0].address + env.app.setNonce(address, 7) + env.app.setBalance(address, 100) + + ready := &WrappedTx{ + hashedTx: newHashedTx(utils.GenBytes(rng, 32)), + timestamp: time.Unix(100, 0), + priority: 10, + gasWanted: 1, + estimatedGas: 1, + evm: utils.Some(evmTx{ + address: address, + seiAddress: address.Bytes(), + nonce: 7, + requiredBalance: big.NewInt(0), + }), + } + pending := &WrappedTx{ + hashedTx: newHashedTx(utils.GenBytes(rng, 32)), + timestamp: time.Unix(100, 0), + priority: 20, + gasWanted: 1, + estimatedGas: 1, + evm: utils.Some(evmTx{ + address: address, + seiAddress: address.Bytes(), + nonce: 8, + requiredBalance: big.NewInt(200), + }), + } - // Transaction setup - tx1 := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx1_data")), - priority: 1, + require.NoError(t, txStore.Insert(ready)) + require.NoError(t, txStore.Insert(pending)) + + txStore.Update(updateSpec{ + Now: time.Unix(102, 0), + Height: 1, + TxResults: map[types.TxHash]bool{}, + Constraints: NopTxConstraints(), + NewPriorities: map[types.TxHash]int64{}, + }) + + _, readyPresent := txStore.ByHash(ready.Hash()) + _, pendingPresent := txStore.ByHash(pending.Hash()) + require.Equal(t, tc.wantReadyPresent, readyPresent) + require.Equal(t, tc.wantPendingPresent, pendingPresent) + require.Equal(t, tc.wantReadyCached, txStore.CacheHas(ready.Hash())) + require.Equal(t, tc.wantPendingCached, txStore.CacheHas(pending.Hash())) + }) } - tx1Size := tx1.Size() +} + +func TestTxStore_NoncePrunedTxCacheBehavior(t *testing.T) { + rng := utils.TestRng() - tx2 := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx2_data")), - priority: 2, + for _, tc := range []struct { + name string + keepInvalidTxsInCache bool + wantCached bool + }{ + { + name: "drop pruned txs from cache", + keepInvalidTxsInCache: false, + wantCached: false, + }, + { + name: "keep pruned txs in cache", + keepInvalidTxsInCache: true, + wantCached: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := TestConfig() + cfg.CacheSize = 10 + cfg.KeepInvalidTxsInCache = tc.keepInvalidTxsInCache + + app := newEVMNonceApp() + txStore := NewTxStore(cfg, proxy.New(app, proxy.NopMetrics()), NopMetrics()) + env := newTestEnv(rng, txStore, app, 1) + address := env.accounts[0].address + env.app.setNonce(address, 7) + env.app.setBalance(address, 100) + + prunedReady := makeEvmTxForTest(rng, address, 7, 10, 0) + prunedPending := makeEvmTxForTest(rng, address, 8, 20, 200) + require.NoError(t, txStore.Insert(prunedReady)) + require.NoError(t, txStore.Insert(prunedPending)) + + env.app.setNonce(address, 9) + txStore.Update(updateSpec{ + Now: time.Now(), + Height: 1, + TxResults: map[types.TxHash]bool{}, + Constraints: NopTxConstraints(), + NewPriorities: map[types.TxHash]int64{}, + }) + + _, readyPresent := txStore.ByHash(prunedReady.Hash()) + _, pendingPresent := txStore.ByHash(prunedPending.Hash()) + require.False(t, readyPresent) + require.False(t, pendingPresent) + require.Equal(t, tc.wantCached, txStore.CacheHas(prunedReady.Hash())) + require.Equal(t, tc.wantCached, txStore.CacheHas(prunedPending.Hash())) + }) } - tx2Size := tx2.Size() +} - err := pendingTxs.Insert(tx1) - require.Nil(t, err) +func TestTxStore_ReplacesReadyTxByHigherPriority(t *testing.T) { + rng := utils.TestRng() + app := newEVMNonceApp() + txStore := NewTxStore(TestConfig(), proxy.New(app, proxy.NopMetrics()), NopMetrics()) + env := newTestEnv(rng, txStore, app, 1) + address := env.accounts[0].address + env.app.setNonce(address, 7) + env.app.setBalance(address, 100) + + // Insert one ready transaction, then replace it with a higher-priority ready + // transaction for the same nonce. + old := makeEvmTxForTest(rng, address, 7, 10, 20) + require.NoError(t, env.txStore.Insert(old)) + env.byHash = map[types.TxHash]*WrappedTx{old.Hash(): old} + env.markReadyTxs() + env.assertState(t) + + replacement := makeEvmTxForTest(rng, address, 7, 20, 30) + require.NoError(t, env.txStore.Insert(replacement)) + delete(env.byHash, old.Hash()) + env.byHash[replacement.Hash()] = replacement + env.markReadyTxs() + env.assertState(t) + _, ok := env.txStore.ByHash(old.Hash()) + require.False(t, ok) + got, ok := env.txStore.ByHash(replacement.Hash()) + require.True(t, ok) + require.Equal(t, replacement.Tx(), got) - err = pendingTxs.Insert(tx2) - require.Nil(t, err) + // A higher-priority transaction that would no longer be ready must not + // replace the current ready transaction for the same nonce. + blocked := makeEvmTxForTest(rng, address, 7, 30, 101) + require.ErrorIs(t, env.txStore.Insert(blocked), errSameNonce) - // Should fail due to pending store size limit - tx3 := &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx3_data_exceeding_pending_size")), - priority: 3, - } + env.assertState(t) + got, ok = env.txStore.ByHash(replacement.Hash()) + require.True(t, ok) + require.Equal(t, replacement.Tx(), got) + _, ok = env.txStore.ByHash(blocked.Hash()) + require.False(t, ok) +} - err = pendingTxs.Insert(tx3) - require.NotNil(t, err) +func TestTxStore_ReplacesReadyThenPendingTxByHigherPriority(t *testing.T) { + rng := utils.TestRng() + app := newEVMNonceApp() + txStore := NewTxStore(TestConfig(), proxy.New(app, proxy.NopMetrics()), NopMetrics()) + env := newTestEnv(rng, txStore, app, 1) + address := env.accounts[0].address + env.app.setNonce(address, 7) + env.app.setBalance(address, 100) + + becamePending := makeEvmTxForTest(rng, address, 7, 40, 60) + require.NoError(t, env.txStore.Insert(becamePending)) + env.byHash = map[types.TxHash]*WrappedTx{becamePending.Hash(): becamePending} + env.markReadyTxs() + env.assertState(t) + + env.app.setBalance(address, 50) + env.txStore.Update(updateSpec{ + Now: time.Now(), + Height: 1, + TxResults: map[types.TxHash]bool{}, + Constraints: NopTxConstraints(), + NewPriorities: map[types.TxHash]int64{}, + }) + env.assertState(t) + + becamePendingReplacement := makeEvmTxForTest(rng, address, 7, 50, 70) + require.NoError(t, env.txStore.Insert(becamePendingReplacement)) + delete(env.byHash, becamePending.Hash()) + env.byHash[becamePendingReplacement.Hash()] = becamePendingReplacement + env.assertState(t) + _, ok := env.txStore.ByHash(becamePending.Hash()) + require.False(t, ok) + got, ok := env.txStore.ByHash(becamePendingReplacement.Hash()) + require.True(t, ok) + require.Equal(t, becamePendingReplacement.Tx(), got) +} - // Second test exceeding byte size condition - mempoolCfg.PendingSize = 5 - pendingTxs = NewPendingTxs(mempoolCfg) - mempoolCfg.MaxPendingTxsBytes = int64(tx1Size + tx2Size) +func TestTxStore_ReplacesPendingTxByHigherPriority(t *testing.T) { + rng := utils.TestRng() + app := newEVMNonceApp() + txStore := NewTxStore(TestConfig(), proxy.New(app, proxy.NopMetrics()), NopMetrics()) + env := newTestEnv(rng, txStore, app, 1) + address := env.accounts[0].address + env.app.setNonce(address, 7) + env.app.setBalance(address, 0) + + pending := makeEvmTxForTest(rng, address, 7, 70, 40) + require.NoError(t, env.txStore.Insert(pending)) + env.byHash = map[types.TxHash]*WrappedTx{pending.Hash(): pending} + env.assertState(t) + + pendingReplacement := makeEvmTxForTest(rng, address, 7, 90, 50) + require.NoError(t, env.txStore.Insert(pendingReplacement)) + delete(env.byHash, pending.Hash()) + env.byHash[pendingReplacement.Hash()] = pendingReplacement + env.assertState(t) + _, ok := env.txStore.ByHash(pending.Hash()) + require.False(t, ok) + got, ok := env.txStore.ByHash(pendingReplacement.Hash()) + require.True(t, ok) + require.Equal(t, pendingReplacement.Tx(), got) +} - err = pendingTxs.Insert(tx1) - require.Nil(t, err) +func TestTxStore_InsertCompactionKeepsReadyListInSync(t *testing.T) { + rng := utils.TestRng() + cfg := TestConfig() + cfg.Size = 50 + cfg.PendingSize = 0 + + app := newEVMNonceApp() + txStore := NewTxStore(cfg, proxy.New(app, proxy.NopMetrics()), NopMetrics()) + inserted := map[types.TxHash]*WrappedTx{} + + for range 20 * cfg.Size { + address := common.BytesToAddress(utils.GenBytes(rng, common.AddressLength)) + wtx := makeEvmTxForTest(rng, address, 0, rng.Int63(), 0) + inserted[wtx.Hash()] = wtx + + err := txStore.Insert(wtx) + require.True(t, err == nil || errors.Is(err, errMempoolFull), "unexpected insert error: %v", err) + + expected := make([]*WrappedTx, 0, txStore.State().total.count) + for txHash, candidate := range inserted { + if tx, ok := txStore.ByHash(txHash); ok { + require.Equal(t, candidate.Tx(), tx) + expected = append(expected, candidate) + } + } - err = pendingTxs.Insert(tx2) - require.Nil(t, err) + ready := txStore.ReadyTxs() + require.Equal(t, txStore.State().total.count, txStore.State().ready.count) + require.ElementsMatch(t, toTxs(expected), toTxs(ready)) - // Should fail due to exceeding max pending transaction bytes - tx3 = &WrappedTx{ - hashedTx: newHashedTx(types.Tx("tx3_small_but_exceeds_byte_limit")), - priority: 3, + var listed types.Txs + for el := txStore.readyTxs.Front(); el != nil; el = el.Next() { + listed = append(listed, el.Value()) + } + require.ElementsMatch(t, toTxs(expected), listed) } - - err = pendingTxs.Insert(tx3) - require.NotNil(t, err) } diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index d3343beb37..8cc85d6dcb 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -2,12 +2,6 @@ package mempool import "math" -const ( - // UnknownPeerID is the peer ID to use when running CheckTx when there is - // no peer (e.g. RPC) - UnknownPeerID uint16 = 0 -) - // TxConstraints contains the precomputed consensus-derived mempool limits for // the current state snapshot. type TxConstraints struct { @@ -19,9 +13,13 @@ type TxConstraints struct { // state snapshot. type TxConstraintsFetcher func() (TxConstraints, error) -func NopTxConstraintsFetcher() (TxConstraints, error) { +func NopTxConstraints() TxConstraints { return TxConstraints{ MaxDataBytes: math.MaxInt64, MaxGas: -1, - }, nil + } +} + +func NopTxConstraintsFetcher() (TxConstraints, error) { + return NopTxConstraints(), nil } diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index a81fbf14eb..1eed9d2bb4 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -302,7 +302,7 @@ func (r *GigaRouter) executeBlock(ctx context.Context, b *atypes.GlobalBlock) (* // TODO: We need the constraints to be fixed per epoch, because we don't know where the lane blocks will be sequenced. // Therefore we disable constraints for now, until epochs are supported AND // chain state understands that consensus parameters can change only at the epoch boundary. - mempool.NopTxConstraintsFetcher, + mempool.NopTxConstraints(), // recheck=false; see TxMempool.Update doc for why. false, ) diff --git a/sei-tendermint/internal/p2p/giga_router_test.go b/sei-tendermint/internal/p2p/giga_router_test.go index 1cd283ddeb..2c68c7f5b3 100644 --- a/sei-tendermint/internal/p2p/giga_router_test.go +++ b/sei-tendermint/internal/p2p/giga_router_test.go @@ -336,7 +336,7 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) { } s.SpawnNamed(fmt.Sprintf("producer[%v]", i), func() error { for _, payload := range txs { - if _, err := txMempool.CheckTx(ctx, payload, mempool.TxInfo{}); err != nil { + if _, err := txMempool.CheckTx(ctx, payload); err != nil { return fmt.Errorf("txMempool.CheckTx(): %w", err) } } diff --git a/sei-tendermint/internal/rpc/core/mempool.go b/sei-tendermint/internal/rpc/core/mempool.go index bbf6be8476..b61cdae14c 100644 --- a/sei-tendermint/internal/rpc/core/mempool.go +++ b/sei-tendermint/internal/rpc/core/mempool.go @@ -13,6 +13,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/state/indexer" tmmath "github.com/sei-protocol/sei-chain/sei-tendermint/libs/math" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes" ) @@ -35,7 +36,7 @@ func (env *Environment) EvmProxy(sender common.Address) (*url.URL, bool) { // https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async // Deprecated and should be removed in 0.37 func (env *Environment) BroadcastTxAsync(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { - go func() { _, _ = env.Mempool.CheckTx(ctx, req.Tx, mempool.TxInfo{}) }() + go func() { _, _ = env.Mempool.CheckTx(ctx, req.Tx) }() return &coretypes.ResultBroadcastTx{Hash: req.Tx.Hash().Bytes()}, nil } @@ -49,7 +50,7 @@ func (env *Environment) BroadcastTxSync(ctx context.Context, req *coretypes.Requ // DeliverTx result. // More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync func (env *Environment) BroadcastTx(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTx, error) { - r, err := env.Mempool.CheckTx(ctx, req.Tx, mempool.TxInfo{}) + r, err := env.Mempool.CheckTx(ctx, req.Tx) if err != nil { return nil, err } @@ -65,7 +66,7 @@ func (env *Environment) BroadcastTx(ctx context.Context, req *coretypes.RequestB // BroadcastTxCommit returns with the responses from CheckTx and DeliverTx. // More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit func (env *Environment) BroadcastTxCommit(ctx context.Context, req *coretypes.RequestBroadcastTx) (*coretypes.ResultBroadcastTxCommit, error) { - r, err := env.Mempool.CheckTx(ctx, req.Tx, mempool.TxInfo{}) + r, err := env.Mempool.CheckTx(ctx, req.Tx) if err != nil { return nil, err } @@ -135,7 +136,9 @@ func (env *Environment) UnconfirmedTxs(ctx context.Context, req *coretypes.Reque skipCount := validateSkipCount(page, perPage) - txs := env.Mempool.ReapMaxTxs(skipCount + tmmath.MinInt(perPage, totalCount-skipCount)) + txs, _ := env.Mempool.ReapTxs(mempool.ReapLimits{ + MaxTxs: utils.Some(uint64(skipCount + tmmath.MinInt(perPage, totalCount-skipCount))), //nolint:gosec // guaranteed to be non-negative + }, false) if skipCount > len(txs) { skipCount = len(txs) } @@ -144,7 +147,7 @@ func (env *Environment) UnconfirmedTxs(ctx context.Context, req *coretypes.Reque return &coretypes.ResultUnconfirmedTxs{ Count: len(result), Total: totalCount, - TotalBytes: env.Mempool.SizeBytes(), + TotalBytes: utils.Clamp[int64](env.Mempool.SizeBytes()), Txs: result, }, nil } @@ -155,7 +158,8 @@ func (env *Environment) NumUnconfirmedTxs(ctx context.Context) (*coretypes.Resul return &coretypes.ResultUnconfirmedTxs{ Count: env.Mempool.Size(), Total: env.Mempool.Size(), - TotalBytes: env.Mempool.SizeBytes()}, nil + TotalBytes: utils.Clamp[int64](env.Mempool.SizeBytes()), + }, nil } // CheckTx checks the transaction without executing it. The transaction won't diff --git a/sei-tendermint/internal/state/execution.go b/sei-tendermint/internal/state/execution.go index 03147cc3e2..e422e2b7a6 100644 --- a/sei-tendermint/internal/state/execution.go +++ b/sei-tendermint/internal/state/execution.go @@ -14,6 +14,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/sei-protocol/seilog" @@ -122,15 +123,15 @@ func (blockExec *BlockExecutor) CreateProposalBlock( // Fetch a limited amount of valid txs maxDataBytes := types.MaxDataBytes(maxBytes, evSize, state.Validators.Size()) - txs := blockExec.mempool.ReapMaxBytesMaxGas(maxDataBytes, maxGasWanted, maxGas) + txs, _ := blockExec.mempool.ReapTxs(mempool.ReapLimits{ + MaxBytes: utils.Some(maxDataBytes), + MaxGasWanted: utils.Some(maxGasWanted), + MaxGasEstimated: utils.Some(maxGas), + }, false) block = state.MakeBlock(height, txs, lastCommit, evidence, proposerAddr) return block, nil } -func (blockExec *BlockExecutor) GetTxsForHashes(txHashes []types.TxHash) types.Txs { - return blockExec.mempool.GetTxsForHashes(txHashes) -} - func (blockExec *BlockExecutor) ProcessProposal( ctx context.Context, block *types.Block, @@ -484,7 +485,7 @@ func (blockExec *BlockExecutor) Commit( block.Height, block.Txs, txResults, - TxConstraintsFetcherForState(state), + TxConstraintsForState(state), state.ConsensusParams.ABCI.RecheckTx, ) blockExec.metrics.UpdateMempoolTime.Observe(float64(time.Since(start))) @@ -492,16 +493,6 @@ func (blockExec *BlockExecutor) Commit( return res.RetainHeight, err } -func (blockExec *BlockExecutor) GetMissingTxs(txHashes []types.TxHash) []types.TxHash { - var missingTxHashes []types.TxHash - for _, txHash := range txHashes { - if !blockExec.mempool.HasTx(txHash) { - missingTxHashes = append(missingTxHashes, txHash) - } - } - return missingTxHashes -} - func (blockExec *BlockExecutor) SafeGetTxsByHashes(txHashes []types.TxHash) (types.Txs, []types.TxHash) { return blockExec.mempool.SafeGetTxsForHashes(txHashes) } diff --git a/sei-tendermint/internal/state/tx_filter.go b/sei-tendermint/internal/state/tx_filter.go index 3ee20dc02e..01ee35d411 100644 --- a/sei-tendermint/internal/state/tx_filter.go +++ b/sei-tendermint/internal/state/tx_filter.go @@ -48,18 +48,16 @@ func TxConstraintsFetcherFromStore(store Store) mempool.TxConstraintsFetcher { return mempool.TxConstraints{}, err } - return TxConstraintsFetcherForState(state)() + return TxConstraintsForState(state), nil } } -func TxConstraintsFetcherForState(state State) mempool.TxConstraintsFetcher { - return func() (mempool.TxConstraints, error) { - return mempool.TxConstraints{ - MaxDataBytes: types.MaxDataBytesNoEvidence( - state.ConsensusParams.Block.MaxBytes, - state.Validators.Size(), - ), - MaxGas: state.ConsensusParams.Block.MaxGas, - }, nil +func TxConstraintsForState(state State) mempool.TxConstraints { + return mempool.TxConstraints{ + MaxDataBytes: types.MaxDataBytesNoEvidence( + state.ConsensusParams.Block.MaxBytes, + state.Validators.Size(), + ), + MaxGas: state.ConsensusParams.Block.MaxGas, } } diff --git a/sei-tendermint/internal/state/tx_filter_test.go b/sei-tendermint/internal/state/tx_filter_test.go index 653de9a687..9a37d0fc3a 100644 --- a/sei-tendermint/internal/state/tx_filter_test.go +++ b/sei-tendermint/internal/state/tx_filter_test.go @@ -31,8 +31,7 @@ func TestTxFilter(t *testing.T) { state, err := sm.MakeGenesisState(genDoc) require.NoError(t, err) - f := sm.TxConstraintsFetcherForState(state) - constraints, err := f() + constraints := sm.TxConstraintsForState(state) require.NoError(t, err) txSize := types.ComputeProtoSizeForTxs([]types.Tx{tc.tx}) if tc.isErr { diff --git a/sei-tendermint/node/node_test.go b/sei-tendermint/node/node_test.go index 8db98beedc..cd07b9a1a4 100644 --- a/sei-tendermint/node/node_test.go +++ b/sei-tendermint/node/node_test.go @@ -341,7 +341,7 @@ func TestCreateProposalBlock(t *testing.T) { txLength := 100 for i := 0; i <= maxBytes/txLength; i++ { tx := tmrand.Bytes(txLength) - _, err := mp.CheckTx(ctx, tx, mempool.TxInfo{}) + _, err := mp.CheckTx(ctx, tx) assert.NoError(t, err) } @@ -416,7 +416,7 @@ func TestMaxTxsProposalBlockSize(t *testing.T) { // fill the mempool with one txs just below the maximum size txLength := int(types.MaxDataBytesNoEvidence(maxBytes, 1)) tx := tmrand.Bytes(txLength - 4) // to account for the varint - _, err = mp.CheckTx(ctx, tx, mempool.TxInfo{}) + _, err = mp.CheckTx(ctx, tx) assert.NoError(t, err) eventBus := eventbus.NewDefault() @@ -481,13 +481,13 @@ func TestMaxProposalBlockSize(t *testing.T) { // fill the mempool with one txs just below the maximum size txLength := int(types.MaxDataBytesNoEvidence(maxBytes, types.MaxVotesCount)) tx := tmrand.Bytes(txLength - 6) // to account for the varint - _, err = mp.CheckTx(ctx, tx, mempool.TxInfo{}) + _, err = mp.CheckTx(ctx, tx) assert.NoError(t, err) // now produce more txs than what a normal block can hold with 10 smaller txs // At the end of the test, only the single big tx should be added for range 10 { tx := tmrand.Bytes(10) - _, err := mp.CheckTx(ctx, tx, mempool.TxInfo{}) + _, err := mp.CheckTx(ctx, tx) assert.NoError(t, err) } diff --git a/sei-tendermint/rpc/client/rpc_test.go b/sei-tendermint/rpc/client/rpc_test.go index 0974efda82..ec39af03e2 100644 --- a/sei-tendermint/rpc/client/rpc_test.go +++ b/sei-tendermint/rpc/client/rpc_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/gogo/protobuf/proto" - "github.com/stretchr/testify/assert" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" @@ -290,10 +289,10 @@ func TestClientMethodCalls(t *testing.T) { err = client.WaitForHeight(ctx, c, apph, nil) require.NoError(t, err) res, err := c.ABCIQuery(ctx, "/key", k) + require.NoError(t, err) qres := res.Response - if assert.NoError(t, err) && assert.True(t, qres.IsOK()) { - require.Equal(t, v, qres.Value) - } + require.True(t, qres.IsOK()) + require.Equal(t, v, qres.Value) }) t.Run("AppCalls", func(t *testing.T) { ctx := t.Context() @@ -323,10 +322,9 @@ func TestClientMethodCalls(t *testing.T) { _qres, err := c.ABCIQueryWithOptions(ctx, "/key", k, client.ABCIQueryOptions{Prove: false}) require.NoError(t, err) qres := _qres.Response - if assert.True(t, qres.IsOK()) { - require.Equal(t, k, qres.Key) - require.Equal(t, v, qres.Value) - } + require.True(t, qres.IsOK()) + require.Equal(t, k, qres.Key) + require.Equal(t, v, qres.Value) // make sure we can lookup the tx with proof ptx, err := c.Tx(ctx, bres.Hash, true) @@ -358,22 +356,20 @@ func TestClientMethodCalls(t *testing.T) { blockResults, err := c.BlockResults(ctx, &txh) require.NoError(t, err, "%d: %+v", i, err) require.Equal(t, txh, blockResults.Height) - if assert.Equal(t, 1, len(blockResults.TxsResults)) { - // check success code - require.Equal(t, 0, blockResults.TxsResults[0].Code) - } + require.Len(t, blockResults.TxsResults, 1) + // check success code + require.Equal(t, uint32(0), blockResults.TxsResults[0].Code) // check blockchain info, now that we know there is info info, err := c.BlockchainInfo(ctx, apph, apph) require.NoError(t, err) require.True(t, info.LastHeight >= apph) - if assert.Equal(t, 1, len(info.BlockMetas)) { - lastMeta := info.BlockMetas[0] - require.Equal(t, apph, lastMeta.Header.Height) - blockData := block.Block - require.Equal(t, blockData.Header.AppHash, lastMeta.Header.AppHash) - require.Equal(t, block.BlockID, lastMeta.BlockID) - } + require.Len(t, info.BlockMetas, 1) + lastMeta := info.BlockMetas[0] + require.Equal(t, apph, lastMeta.Header.Height) + blockData := block.Block + require.Equal(t, blockData.Header.AppHash, lastMeta.Header.AppHash) + require.Equal(t, block.BlockID, lastMeta.BlockID) // and get the corresponding commit with the same apphash commit, err := c.Commit(ctx, &apph) @@ -445,7 +441,7 @@ func TestClientMethodCalls(t *testing.T) { require.Equal(t, initMempoolSize+1, pool.Size()) - txs := pool.ReapMaxTxs(len(tx)) + txs, _ := pool.ReapTxs(mempool.ReapLimits{MaxTxs: utils.Some(uint64(len(tx)))}, false) require.Equal(t, tx, txs[0]) pool.Flush() }) @@ -594,7 +590,7 @@ func TestClientMethodCallsAdvanced(t *testing.T) { _, _, tx := MakeTxKV() txs[i] = tx - _, err := pool.CheckTx(ctx, tx, mempool.TxInfo{}) + _, err := pool.CheckTx(ctx, tx) require.NoError(t, err) ch <- nil } @@ -618,11 +614,11 @@ func TestClientMethodCallsAdvanced(t *testing.T) { if i == 2 { perPage = 2 } - assert.Equal(t, perPage, res.Count) - assert.Equal(t, 5, res.Total) - assert.Equal(t, pool.SizeBytes(), res.TotalBytes) + require.Equal(t, perPage, int(res.Count)) + require.Equal(t, 5, int(res.Total)) + require.Equal(t, pool.SizeBytes(), uint64(res.TotalBytes)) for _, tx := range res.Txs { - assert.Contains(t, txs, tx) + require.Contains(t, txs, tx) } } } @@ -636,7 +632,7 @@ func TestClientMethodCallsAdvanced(t *testing.T) { _, _, tx := MakeTxKV() - _, err := pool.CheckTx(ctx, tx, mempool.TxInfo{}) + _, err := pool.CheckTx(ctx, tx) require.NoError(t, err) close(ch) @@ -654,9 +650,9 @@ func TestClientMethodCallsAdvanced(t *testing.T) { res, err := mc.NumUnconfirmedTxs(ctx) require.NoError(t, err, "%d: %+v", i, err) - assert.Equal(t, mempoolSize, res.Count) - assert.Equal(t, mempoolSize, res.Total) - assert.Equal(t, pool.SizeBytes(), res.TotalBytes) + require.Equal(t, mempoolSize, int(res.Count)) + require.Equal(t, mempoolSize, int(res.Total)) + require.Equal(t, pool.SizeBytes(), uint64(res.TotalBytes)) } pool.Flush() @@ -770,9 +766,8 @@ func TestClientMethodCallsAdvanced(t *testing.T) { require.Equal(t, find.Hash, ptx.Hash) // time to verify the proof - if assert.Equal(t, find.Tx, ptx.Proof.Data) { - require.NoError(t, ptx.Proof.Proof.Verify(ptx.Proof.RootHash, find.Hash)) - } + require.Equal(t, find.Tx, ptx.Proof.Data) + require.NoError(t, ptx.Proof.Proof.Verify(ptx.Proof.RootHash, find.Hash)) // query by height result, err = c.TxSearch(ctx, fmt.Sprintf("tx.height=%d", find.Height), true, nil, nil, "asc") diff --git a/sei-tendermint/test/fuzz/tests/mempool_test.go b/sei-tendermint/test/fuzz/tests/mempool_test.go index 2ea0f2c5f3..32bfc3f2ed 100644 --- a/sei-tendermint/test/fuzz/tests/mempool_test.go +++ b/sei-tendermint/test/fuzz/tests/mempool_test.go @@ -17,6 +17,6 @@ func FuzzMempool(f *testing.F) { mp := mempool.NewTxMempool(cfg.ToMempoolConfig(), kvstore.NewProxy(), mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) f.Fuzz(func(t *testing.T, data []byte) { - _, _ = mp.CheckTx(t.Context(), data, mempool.TxInfo{}) + _, _ = mp.CheckTx(t.Context(), data) }) }