From 3b3071d8637dd6c55a9ce8f861adc0cc0eec8e43 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 13:34:32 +0200 Subject: [PATCH 01/43] removed unused function --- sei-tendermint/internal/state/execution.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/sei-tendermint/internal/state/execution.go b/sei-tendermint/internal/state/execution.go index d588d77618..ec1f20834d 100644 --- a/sei-tendermint/internal/state/execution.go +++ b/sei-tendermint/internal/state/execution.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "math" "time" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" @@ -451,18 +450,6 @@ func (blockExec *BlockExecutor) SafeGetTxsByKeys(txKeys []types.TxKey) (types.Tx return blockExec.mempool.SafeGetTxsForKeys(txKeys) } -func (blockExec *BlockExecutor) CheckTxFromPeerProposal(ctx context.Context, tx types.Tx) { - // Ignore errors from CheckTx because there could be benign errors due to the same tx being - // inserted into the mempool from gossiping. Since such simultaneous insertion could result in - // multiple different kinds of errors, we will ignore them all here, and verify in the consensus - // state machine whether all txs in the proposal are present in the mempool at a later time. - if err := blockExec.mempool.CheckTx(ctx, tx, func(rct *abci.ResponseCheckTx) {}, mempool.TxInfo{ - SenderID: math.MaxUint16, - }); err != nil { - logger.Info("CheckTx for proposal tx from peer raised error; this could be ignored if the error is because the tx is added to the mempool while this check was happening", "err", err) - } -} - func buildLastCommitInfo(block *types.Block, store Store, initialHeight int64) abci.CommitInfo { if block.Height == initialHeight { // there is no last commit for the initial height. From 3c866a62c2a8a5c36e28e07b7aecd8094d38042b Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 13:47:58 +0200 Subject: [PATCH 02/43] snapshot --- sei-tendermint/internal/mempool/mempool.go | 55 +++++++------------ .../internal/mempool/mempool_test.go | 42 +++----------- sei-tendermint/internal/mempool/reactor.go | 47 ++++++++++++---- .../internal/mempool/reactor_test.go | 35 ++++++++++++ sei-tendermint/internal/mempool/types.go | 4 ++ 5 files changed, 104 insertions(+), 79 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 1adcd04eae..bdc88eacfa 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -117,8 +117,7 @@ type TxMempool struct { preCheck PreCheckFunc postCheck PostCheckFunc - // NodeID to count of transactions failing CheckTx - failedCheckTxCounts utils.Mutex[map[types.NodeID]uint64] + failedCheckTxCounter utils.Option[peerFailedCheckTxCounter] router router priorityReservoir *reservoir.Sampler[int64] @@ -132,20 +131,19 @@ func NewTxMempool( ) *TxMempool { txmp := &TxMempool{ - config: cfg, - proxyAppConn: proxyAppConn, - height: -1, - cache: NopTxCache{}, - blockFailedTxs: NopTxCache{}, - metrics: NopMetrics(), - txStore: NewTxStore(), - gossipIndex: clist.New(), - priorityIndex: NewTxPriorityQueue(), - expirationIndex: NewWrappedTxList(), - pendingTxs: NewPendingTxs(cfg), - failedCheckTxCounts: utils.NewMutex(map[types.NodeID]uint64{}), - router: router, - priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG + config: cfg, + proxyAppConn: proxyAppConn, + height: -1, + cache: NopTxCache{}, + blockFailedTxs: NopTxCache{}, + metrics: NopMetrics(), + txStore: NewTxStore(), + gossipIndex: clist.New(), + priorityIndex: NewTxPriorityQueue(), + expirationIndex: NewWrappedTxList(), + pendingTxs: NewPendingTxs(cfg), + router: router, + priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG } if cfg.CacheSize > 0 { @@ -283,7 +281,6 @@ func (txmp *TxMempool) CheckTx( defer txmp.mtx.RUnlock() if txSize := len(tx); txSize > txmp.config.MaxTxBytes { - txmp.incrementBlacklistCounter(txInfo.SenderNodeID) return types.ErrTxTooLarge{ Max: txmp.config.MaxTxBytes, Actual: txSize, @@ -312,7 +309,6 @@ func (txmp *TxMempool) CheckTx( if txmp.preCheck != nil { if err := txmp.preCheck(tx); err != nil { - txmp.incrementBlacklistCounter(txInfo.SenderNodeID) return types.ErrPreCheck{Reason: err} } } @@ -424,19 +420,6 @@ func (txmp *TxMempool) CheckTx( return nil } -func (txmp *TxMempool) incrementBlacklistCounter(nodeID types.NodeID) { - if !txmp.config.CheckTxErrorBlacklistEnabled || nodeID == "" || txmp.router == nil { - return - } - - for counts := range txmp.failedCheckTxCounts.Lock() { - counts[nodeID]++ - if counts[nodeID] > uint64(txmp.config.CheckTxErrorThreshold) { //nolint:gosec // CheckTxErrorThreshold is a validated non-negative config value - txmp.router.Evict(nodeID, errors.New("mempool: checkTx error exceeded threshold")) - } - } -} - func (txmp *TxMempool) isInMempool(tx types.Tx) bool { existingTx := txmp.txStore.GetTxByHash(tx.Key()) return existingTx != nil && !existingTx.removed @@ -1141,10 +1124,14 @@ func (txmp *TxMempool) notifyTxsAvailable() { } func (txmp *TxMempool) GetPeerFailedCheckTxCount(nodeID types.NodeID) uint64 { - for counts := range txmp.failedCheckTxCounts.Lock() { - return counts[nodeID] + if counter, ok := txmp.failedCheckTxCounter.Get(); ok { + return counter.GetPeerFailedCheckTxCount(nodeID) } - panic("unreachable") + return 0 +} + +func (txmp *TxMempool) setPeerFailedCheckTxCounter(counter peerFailedCheckTxCounter) { + txmp.failedCheckTxCounter = utils.Some(counter) } // AppendCheckTxErr wraps error message into an ABCIMessageLogs json string diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 39ca8146af..86a8372c13 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -1039,47 +1039,19 @@ func TestTxMempool_CheckTxPostCheckError(t *testing.T) { } } -func TestTxMempool_FailedCheckTxCount(t *testing.T) { - ctx := t.Context() - - client := &application{Application: kvstore.NewApplication()} +type peerFailedCheckTxCounterStub map[types.NodeID]uint64 - postCheckFn := func(_ types.Tx, _ *abci.ResponseCheckTx) error { - return nil - } - txmp := setup(t, client, 0, WithPostCheck(postCheckFn)) - badTx := make([]byte, txmp.config.MaxTxBytes+1) - - callback := func(res *abci.ResponseCheckTx) { - require.Equal(t, nil, txmp.postCheck(badTx, res)) - } - require.Equal(t, uint64(0), txmp.GetPeerFailedCheckTxCount("sender")) +func (s peerFailedCheckTxCounterStub) GetPeerFailedCheckTxCount(nodeID types.NodeID) uint64 { + return s[nodeID] +} - txmp.config.CheckTxErrorBlacklistEnabled = false +func TestTxMempool_GetPeerFailedCheckTxCount(t *testing.T) { + txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0) - // bad tx before enabling blacklisting does not increment the failed count - require.Error(t, txmp.CheckTx(ctx, badTx, callback, TxInfo{SenderID: 0, SenderNodeID: "sender"})) require.Equal(t, uint64(0), txmp.GetPeerFailedCheckTxCount("sender")) - txmp.config.CheckTxErrorBlacklistEnabled = true - txmp.config.CheckTxErrorThreshold = 2 - - // first bad tx that should be tracked - require.Error(t, txmp.CheckTx(ctx, badTx, callback, TxInfo{SenderID: 0, SenderNodeID: "sender"})) - require.Equal(t, uint64(1), txmp.GetPeerFailedCheckTxCount("sender")) - - // second bad tx that should be tracked - require.Error(t, txmp.CheckTx(ctx, badTx, callback, TxInfo{SenderID: 0, SenderNodeID: "sender"})) + txmp.setPeerFailedCheckTxCounter(peerFailedCheckTxCounterStub{"sender": 2}) require.Equal(t, uint64(2), txmp.GetPeerFailedCheckTxCount("sender")) - - goodTx := []byte("sender=key=1") - // good tx doesn't increase failedCount - require.NoError(t, txmp.CheckTx(ctx, goodTx, callback, TxInfo{SenderID: 0, SenderNodeID: "sender"})) - require.Equal(t, uint64(2), txmp.GetPeerFailedCheckTxCount("sender")) - - // three strikes, you're out! - require.Error(t, txmp.CheckTx(ctx, badTx, callback, TxInfo{SenderID: 0, SenderNodeID: "sender"})) - require.True(t, txmp.router.(*TestPeerEvictor).IsEvicted("sender")) } func TestAppendCheckTxErr(t *testing.T) { diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 91151ceefb..5cdf772362 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -11,6 +11,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/service" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" pb "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/sei-protocol/seilog" @@ -38,8 +39,9 @@ type Reactor struct { // Reactor. observePanic is called with the recovered value. observePanic func(any) - mtx sync.Mutex - peerRoutines map[types.NodeID]context.CancelFunc + mtx sync.Mutex + peerRoutines map[types.NodeID]context.CancelFunc + failedCheckTxCounts utils.Mutex[map[types.NodeID]int] channel *p2p.Channel[*pb.Message] readyToStart chan struct{} @@ -56,15 +58,17 @@ func NewReactor( return nil, fmt.Errorf("router.OpenChannel(): %w", err) } r := &Reactor{ - cfg: cfg, - mempool: txmp, - ids: NewMempoolIDs(), - router: router, - channel: channel, - peerRoutines: make(map[types.NodeID]context.CancelFunc), - observePanic: defaultObservePanic, - readyToStart: make(chan struct{}, 1), + cfg: cfg, + mempool: txmp, + ids: NewMempoolIDs(), + router: router, + channel: channel, + peerRoutines: make(map[types.NodeID]context.CancelFunc), + failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), + observePanic: defaultObservePanic, + readyToStart: make(chan struct{}, 1), } + txmp.setPeerFailedCheckTxCounter(r) r.BaseService = *service.NewBaseService("Mempool", r) return r, nil @@ -135,6 +139,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me for _, tx := range protoTxs { if err := r.mempool.CheckTx(ctx, tx, nil, txInfo); err != nil { + r.accountFailedCheckTx(m.From, err) if errors.Is(err, types.ErrTxInCache) { // if the tx is in the cache, // then we've been gossiped a @@ -167,6 +172,28 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me return nil } +func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { + if !r.cfg.CheckTxErrorBlacklistEnabled { + return + } + if !utils.ErrorAs[types.ErrTxTooLarge](err).IsPresent() && !utils.ErrorAs[types.ErrPreCheck](err).IsPresent() { + return + } + for counts := range r.failedCheckTxCounts.Lock() { + counts[nodeID]++ + if counts[nodeID] > r.cfg.CheckTxErrorThreshold { + r.router.Evict(nodeID, errors.New("mempool: checkTx error exceeded threshold")) + } + } +} + +func (r *Reactor) GetPeerFailedCheckTxCount(nodeID types.NodeID) int { + for counts := range r.failedCheckTxCounts.Lock() { + return counts[nodeID] + } + panic("unreachable") +} + // handleMessage handles an Envelope sent from a peer on a specific p2p Channel. // It will handle errors and any possible panics gracefully. A caller can handle // any error returned by sending a PeerError on the respective channel. diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 9a2ceb2c4c..6670d20073 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -2,6 +2,7 @@ package mempool import ( "context" + "errors" "fmt" "os" "runtime" @@ -18,6 +19,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" tmrand "github.com/sei-protocol/sei-chain/sei-tendermint/libs/rand" + "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" ) @@ -177,6 +179,39 @@ func TestReactorBroadcastTxs(t *testing.T) { rts.waitForTxns(t, convertTex(txs), secondaries...) } +func TestReactorFailedCheckTxCount(t *testing.T) { + cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + + txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0) + reactor := &Reactor{ + cfg: cfg.Mempool, + mempool: txmp, + router: &p2p.Router{}, + failedCheckTxCounts: utils.NewMutex(map[types.NodeID]uint64{}), + } + txmp.setPeerFailedCheckTxCounter(reactor) + + reactor.cfg.CheckTxErrorBlacklistEnabled = false + reactor.accountFailedCheckTx("sender", types.ErrTxTooLarge{Max: 1, Actual: 2}) + require.Equal(t, uint64(0), reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, uint64(0), txmp.GetPeerFailedCheckTxCount("sender")) + + reactor.cfg.CheckTxErrorBlacklistEnabled = true + reactor.cfg.CheckTxErrorThreshold = 10 + + reactor.accountFailedCheckTx("sender", types.ErrTxTooLarge{Max: 1, Actual: 2}) + require.Equal(t, uint64(1), reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, uint64(1), txmp.GetPeerFailedCheckTxCount("sender")) + + reactor.accountFailedCheckTx("sender", types.ErrPreCheck{Reason: errors.New("bad tx")}) + require.Equal(t, uint64(2), reactor.GetPeerFailedCheckTxCount("sender")) + + reactor.accountFailedCheckTx("sender", errors.New("application rejected tx")) + require.Equal(t, uint64(2), reactor.GetPeerFailedCheckTxCount("sender")) +} + // regression test for https://github.com/tendermint/tendermint/issues/5408 func TestReactorConcurrency(t *testing.T) { numTxs := 10 diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index 3f3d94a060..be9cce2fe5 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -69,3 +69,7 @@ func PostCheckMaxGas(maxGas int64) PostCheckFunc { type router interface { Evict(types.NodeID, error) } + +type peerFailedCheckTxCounter interface { + GetPeerFailedCheckTxCount(types.NodeID) uint64 +} From 4764164fb7e4e41d32659b16bf00412efcb28eea Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 14:00:38 +0200 Subject: [PATCH 03/43] updated test --- sei-tendermint/internal/mempool/mempool.go | 13 ------ .../internal/mempool/mempool_test.go | 15 ------ sei-tendermint/internal/mempool/reactor.go | 2 - .../internal/mempool/reactor_test.go | 46 +++++++++++++------ sei-tendermint/internal/mempool/types.go | 4 -- 5 files changed, 33 insertions(+), 47 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index bdc88eacfa..7cc788e125 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -117,8 +117,6 @@ type TxMempool struct { preCheck PreCheckFunc postCheck PostCheckFunc - failedCheckTxCounter utils.Option[peerFailedCheckTxCounter] - router router priorityReservoir *reservoir.Sampler[int64] } @@ -1123,17 +1121,6 @@ func (txmp *TxMempool) notifyTxsAvailable() { } } -func (txmp *TxMempool) GetPeerFailedCheckTxCount(nodeID types.NodeID) uint64 { - if counter, ok := txmp.failedCheckTxCounter.Get(); ok { - return counter.GetPeerFailedCheckTxCount(nodeID) - } - return 0 -} - -func (txmp *TxMempool) setPeerFailedCheckTxCounter(counter peerFailedCheckTxCounter) { - txmp.failedCheckTxCounter = utils.Some(counter) -} - // AppendCheckTxErr wraps error message into an ABCIMessageLogs json string func (txmp *TxMempool) AppendCheckTxErr(existingLogs string, log string) string { var builder strings.Builder diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 86a8372c13..b306a56bfe 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -1039,21 +1039,6 @@ func TestTxMempool_CheckTxPostCheckError(t *testing.T) { } } -type peerFailedCheckTxCounterStub map[types.NodeID]uint64 - -func (s peerFailedCheckTxCounterStub) GetPeerFailedCheckTxCount(nodeID types.NodeID) uint64 { - return s[nodeID] -} - -func TestTxMempool_GetPeerFailedCheckTxCount(t *testing.T) { - txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0) - - require.Equal(t, uint64(0), txmp.GetPeerFailedCheckTxCount("sender")) - - txmp.setPeerFailedCheckTxCounter(peerFailedCheckTxCounterStub{"sender": 2}) - require.Equal(t, uint64(2), txmp.GetPeerFailedCheckTxCount("sender")) -} - func TestAppendCheckTxErr(t *testing.T) { client := &application{Application: kvstore.NewApplication()} txmp := setup(t, client, 500) diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 5cdf772362..d9520c640c 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -68,8 +68,6 @@ func NewReactor( observePanic: defaultObservePanic, readyToStart: make(chan struct{}, 1), } - txmp.setPeerFailedCheckTxCounter(r) - r.BaseService = *service.NewBaseService("Mempool", r) return r, nil } diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 6670d20073..dbb20b7298 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -21,6 +21,7 @@ import ( tmrand "github.com/sei-protocol/sei-chain/sei-tendermint/libs/rand" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" + pb "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -184,32 +185,51 @@ func TestReactorFailedCheckTxCount(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0) + preCheckErr := errors.New("bad tx") + txmp := setup( + t, + &application{Application: kvstore.NewApplication()}, + 0, + WithPreCheck(func(tx types.Tx) error { + if string(tx) == "precheck-bad" { + return preCheckErr + } + return nil + }), + ) reactor := &Reactor{ cfg: cfg.Mempool, mempool: txmp, + ids: NewMempoolIDs(), router: &p2p.Router{}, - failedCheckTxCounts: utils.NewMutex(map[types.NodeID]uint64{}), + failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), + } + msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { + return p2p.RecvMsg[*pb.Message]{ + From: "sender", + Message: &pb.Message{ + Sum: &pb.Message_Txs{ + Txs: &pb.Txs{Txs: [][]byte{tx}}, + }, + }, + } } - txmp.setPeerFailedCheckTxCounter(reactor) reactor.cfg.CheckTxErrorBlacklistEnabled = false - reactor.accountFailedCheckTx("sender", types.ErrTxTooLarge{Max: 1, Actual: 2}) - require.Equal(t, uint64(0), reactor.GetPeerFailedCheckTxCount("sender")) - require.Equal(t, uint64(0), txmp.GetPeerFailedCheckTxCount("sender")) + require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) + require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) reactor.cfg.CheckTxErrorBlacklistEnabled = true reactor.cfg.CheckTxErrorThreshold = 10 - reactor.accountFailedCheckTx("sender", types.ErrTxTooLarge{Max: 1, Actual: 2}) - require.Equal(t, uint64(1), reactor.GetPeerFailedCheckTxCount("sender")) - require.Equal(t, uint64(1), txmp.GetPeerFailedCheckTxCount("sender")) + require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) + require.Equal(t, 1, reactor.GetPeerFailedCheckTxCount("sender")) - reactor.accountFailedCheckTx("sender", types.ErrPreCheck{Reason: errors.New("bad tx")}) - require.Equal(t, uint64(2), reactor.GetPeerFailedCheckTxCount("sender")) + require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx([]byte("precheck-bad")))) + require.Equal(t, 2, reactor.GetPeerFailedCheckTxCount("sender")) - reactor.accountFailedCheckTx("sender", errors.New("application rejected tx")) - require.Equal(t, uint64(2), reactor.GetPeerFailedCheckTxCount("sender")) + require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx([]byte("sender=key=1")))) + require.Equal(t, 2, reactor.GetPeerFailedCheckTxCount("sender")) } // regression test for https://github.com/tendermint/tendermint/issues/5408 diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index be9cce2fe5..3f3d94a060 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -69,7 +69,3 @@ func PostCheckMaxGas(maxGas int64) PostCheckFunc { type router interface { Evict(types.NodeID, error) } - -type peerFailedCheckTxCounter interface { - GetPeerFailedCheckTxCount(types.NodeID) uint64 -} From 3b992a1d40e8b53f3e67f9e10776981b04a9b424 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 14:05:17 +0200 Subject: [PATCH 04/43] more cleanup --- .../internal/blocksync/reactor_test.go | 6 +----- .../internal/consensus/common_test.go | 1 - .../internal/consensus/reactor_test.go | 1 - sei-tendermint/internal/consensus/replay.go | 2 +- .../internal/consensus/replay_stubs.go | 5 ----- sei-tendermint/internal/mempool/mempool.go | 3 --- .../internal/mempool/mempool_test.go | 19 +------------------ sei-tendermint/internal/mempool/types.go | 4 ---- sei-tendermint/internal/state/helpers_test.go | 6 +----- sei-tendermint/node/node_test.go | 3 --- sei-tendermint/node/setup.go | 1 - .../test/fuzz/tests/mempool_test.go | 15 +-------------- 12 files changed, 5 insertions(+), 61 deletions(-) diff --git a/sei-tendermint/internal/blocksync/reactor_test.go b/sei-tendermint/internal/blocksync/reactor_test.go index c9f44cf5e4..83480ab6fa 100644 --- a/sei-tendermint/internal/blocksync/reactor_test.go +++ b/sei-tendermint/internal/blocksync/reactor_test.go @@ -26,10 +26,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) -type testMempoolRouter struct{} - -func (testMempoolRouter) Evict(types.NodeID, error) {} - type reactorTestSuite struct { network *p2p.TestNetwork nodes []types.NodeID @@ -97,7 +93,7 @@ func makeReactor( state, err := sm.MakeGenesisState(genDoc) require.NoError(t, err) require.NoError(t, stateStore.Save(state)) - mp := mempool.NewTxMempool(config.TestMempoolConfig(), app, testMempoolRouter{}) + mp := mempool.NewTxMempool(config.TestMempoolConfig(), app) eventbus := eventbus.NewDefault() require.NoError(t, eventbus.Start(ctx)) diff --git a/sei-tendermint/internal/consensus/common_test.go b/sei-tendermint/internal/consensus/common_test.go index 49619f7b4d..561454e7bd 100644 --- a/sei-tendermint/internal/consensus/common_test.go +++ b/sei-tendermint/internal/consensus/common_test.go @@ -467,7 +467,6 @@ func newStateWithConfigAndBlockStore( mempool := mempool.NewTxMempool( thisConfig.Mempool, proxyAppConnMem, - nil, ) if thisConfig.Consensus.WaitForTxs() { diff --git a/sei-tendermint/internal/consensus/reactor_test.go b/sei-tendermint/internal/consensus/reactor_test.go index d3c8d8c096..e29de08b61 100644 --- a/sei-tendermint/internal/consensus/reactor_test.go +++ b/sei-tendermint/internal/consensus/reactor_test.go @@ -275,7 +275,6 @@ func TestReactorWithEvidence(t *testing.T) { mempool := mempool.NewTxMempool( thisConfig.Mempool, proxyAppConnMem, - nil, ) if thisConfig.Consensus.WaitForTxs() { diff --git a/sei-tendermint/internal/consensus/replay.go b/sei-tendermint/internal/consensus/replay.go index be7ad4edeb..416882d791 100644 --- a/sei-tendermint/internal/consensus/replay.go +++ b/sei-tendermint/internal/consensus/replay.go @@ -135,7 +135,7 @@ func NewHandshaker( } func newReplayTxMempool(appClient abci.Application) *mempool.TxMempool { - return mempool.NewTxMempool(config.DefaultMempoolConfig(), appClient, replayPeerEvictor{}) + return mempool.NewTxMempool(config.DefaultMempoolConfig(), appClient) } // NBlocks returns the number of blocks applied to the state. diff --git a/sei-tendermint/internal/consensus/replay_stubs.go b/sei-tendermint/internal/consensus/replay_stubs.go index e96bcb5409..2a1fddf4ec 100644 --- a/sei-tendermint/internal/consensus/replay_stubs.go +++ b/sei-tendermint/internal/consensus/replay_stubs.go @@ -4,7 +4,6 @@ import ( "context" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) //----------------------------------------------------------------------------- @@ -43,7 +42,3 @@ func (mock *mockProxyApp) FinalizeBlock(_ context.Context, req *abci.RequestFina func (mock *mockProxyApp) Commit(context.Context) (*abci.ResponseCommit, error) { return &abci.ResponseCommit{}, nil } - -type replayPeerEvictor struct{} - -func (replayPeerEvictor) Evict(types.NodeID, error) {} diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 7cc788e125..aa3bad9d7e 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -117,14 +117,12 @@ type TxMempool struct { preCheck PreCheckFunc postCheck PostCheckFunc - router router priorityReservoir *reservoir.Sampler[int64] } func NewTxMempool( cfg *config.MempoolConfig, proxyAppConn abci.Application, - router router, options ...TxMempoolOption, ) *TxMempool { @@ -140,7 +138,6 @@ func NewTxMempool( priorityIndex: NewTxPriorityQueue(), expirationIndex: NewWrappedTxList(), pendingTxs: NewPendingTxs(cfg), - router: router, priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG } diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index b306a56bfe..98a25dcb6e 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -181,7 +181,7 @@ func setup(t testing.TB, app abci.Application, cacheSize int, options ...TxMempo t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - return NewTxMempool(cfg.Mempool, app, NewTestPeerEvictor(), options...) + return NewTxMempool(cfg.Mempool, app, options...) } func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { @@ -219,23 +219,6 @@ func convertTex(in []testTx) types.Txs { return out } -type TestPeerEvictor struct { - evicting map[types.NodeID]struct{} -} - -func NewTestPeerEvictor() *TestPeerEvictor { - return &TestPeerEvictor{evicting: map[types.NodeID]struct{}{}} -} - -func (e *TestPeerEvictor) IsEvicted(peerID types.NodeID) bool { - _, ok := e.evicting[peerID] - return ok -} - -func (e *TestPeerEvictor) Evict(id types.NodeID, _ error) { - e.evicting[id] = struct{}{} -} - func TestTxMempool_TxsAvailable(t *testing.T) { ctx := t.Context() diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index 3f3d94a060..0fb4971419 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -65,7 +65,3 @@ func PostCheckMaxGas(maxGas int64) PostCheckFunc { return nil } } - -type router interface { - Evict(types.NodeID, error) -} diff --git a/sei-tendermint/internal/state/helpers_test.go b/sei-tendermint/internal/state/helpers_test.go index 6fdb93a9f9..a958268e04 100644 --- a/sei-tendermint/internal/state/helpers_test.go +++ b/sei-tendermint/internal/state/helpers_test.go @@ -231,16 +231,12 @@ func randomGenesisDoc() *types.GenesisDoc { } } -type testMempoolRouter struct{} - -func (testMempoolRouter) Evict(types.NodeID, error) {} - func makeTxMempool(t testing.TB, app abci.Application) *mempool.TxMempool { t.Helper() cfg := config.TestMempoolConfig() - return mempool.NewTxMempool(cfg, app, testMempoolRouter{}) + return mempool.NewTxMempool(cfg, app) } // used for testing by state store diff --git a/sei-tendermint/node/node_test.go b/sei-tendermint/node/node_test.go index 22401fe5c1..928fb32cd4 100644 --- a/sei-tendermint/node/node_test.go +++ b/sei-tendermint/node/node_test.go @@ -287,7 +287,6 @@ func TestCreateProposalBlock(t *testing.T) { mp := mempool.NewTxMempool( cfg.Mempool, app, - nil, ) // Make EvidencePool @@ -383,7 +382,6 @@ func TestMaxTxsProposalBlockSize(t *testing.T) { mp := mempool.NewTxMempool( cfg.Mempool, app, - nil, ) // fill the mempool with one txs just below the maximum size @@ -447,7 +445,6 @@ func TestMaxProposalBlockSize(t *testing.T) { mp := mempool.NewTxMempool( cfg.Mempool, app, - nil, ) // fill the mempool with one txs just below the maximum size diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 05ae960b77..270fcb9d1a 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -153,7 +153,6 @@ func createMempoolReactor( mp := mempool.NewTxMempool( cfg.Mempool, appClient, - router, mempool.WithMetrics(memplMetrics), mempool.WithPreCheck(sm.TxPreCheckFromStore(store)), mempool.WithPostCheck(sm.TxPostCheckFromStore(store)), diff --git a/sei-tendermint/test/fuzz/tests/mempool_test.go b/sei-tendermint/test/fuzz/tests/mempool_test.go index 8696db3d10..e203a890e1 100644 --- a/sei-tendermint/test/fuzz/tests/mempool_test.go +++ b/sei-tendermint/test/fuzz/tests/mempool_test.go @@ -8,28 +8,15 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" - "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) -type TestPeerEvictor struct { - evicting map[types.NodeID]struct{} -} - -func NewTestPeerEvictor() *TestPeerEvictor { - return &TestPeerEvictor{evicting: map[types.NodeID]struct{}{}} -} - -func (e *TestPeerEvictor) Evict(id types.NodeID, _ error) { - e.evicting[id] = struct{}{} -} - func FuzzMempool(f *testing.F) { app := kvstore.NewApplication() cfg := config.DefaultMempoolConfig() cfg.Broadcast = false - mp := mempool.NewTxMempool(cfg, app, NewTestPeerEvictor()) + mp := mempool.NewTxMempool(cfg, app) f.Fuzz(func(t *testing.T, data []byte) { _ = mp.CheckTx(t.Context(), data, nil, mempool.TxInfo{}) From 9fd05fc15ec36e5a0296f086665f581e578b2b52 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 15:02:28 +0200 Subject: [PATCH 05/43] snapshot --- sei-tendermint/internal/mempool/reactor.go | 10 +++ .../internal/mempool/reactor_test.go | 81 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index d9520c640c..26ae9a1dae 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -178,6 +178,9 @@ func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { return } for counts := range r.failedCheckTxCounts.Lock() { + if _, ok := counts[nodeID]; !ok { + return + } counts[nodeID]++ if counts[nodeID] > r.cfg.CheckTxErrorThreshold { r.router.Evict(nodeID, errors.New("mempool: checkTx error exceeded threshold")) @@ -240,6 +243,10 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda switch peerUpdate.Status { case p2p.PeerStatusUp: + for counts := range r.failedCheckTxCounts.Lock() { + counts[peerUpdate.NodeID] = 0 + } + // Do not allow starting new tx broadcast loops after reactor shutdown // has been initiated. This can happen after we've manually closed all // peer broadcast, but the router still sends in-flight peer updates. @@ -266,6 +273,9 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda case p2p.PeerStatusDown: r.ids.Reclaim(peerUpdate.NodeID) + for counts := range r.failedCheckTxCounts.Lock() { + delete(counts, peerUpdate.NodeID) + } // Check if we've started a tx broadcasting goroutine for this peer. // If we have, we signal to terminate the goroutine via the channel's closure. diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index dbb20b7298..5e85e4383d 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -214,6 +214,10 @@ func TestReactorFailedCheckTxCount(t *testing.T) { }, } } + reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ + NodeID: "sender", + Status: p2p.PeerStatusUp, + }) reactor.cfg.CheckTxErrorBlacklistEnabled = false require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) @@ -232,6 +236,83 @@ func TestReactorFailedCheckTxCount(t *testing.T) { require.Equal(t, 2, reactor.GetPeerFailedCheckTxCount("sender")) } +func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { + cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + + reactor := &Reactor{ + cfg: cfg.Mempool, + ids: NewMempoolIDs(), + failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{"other": 1}), + peerRoutines: map[types.NodeID]context.CancelFunc{}, + } + + reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ + NodeID: "sender", + Status: p2p.PeerStatusUp, + }) + require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) + + reactor.setFailedCheckTxCount("sender", 2) + reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ + NodeID: "sender", + Status: p2p.PeerStatusDown, + }) + + require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, 1, reactor.GetPeerFailedCheckTxCount("other")) + require.Equal(t, uint16(0), reactor.ids.GetForPeer("sender")) +} + +func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { + cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + + preCheckErr := errors.New("bad tx") + txmp := setup( + t, + &application{Application: kvstore.NewApplication()}, + 0, + WithPreCheck(func(tx types.Tx) error { + if string(tx) == "precheck-bad" { + return preCheckErr + } + return nil + }), + ) + reactor := &Reactor{ + cfg: cfg.Mempool, + mempool: txmp, + ids: NewMempoolIDs(), + router: &p2p.Router{}, + failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), + peerRoutines: map[types.NodeID]context.CancelFunc{}, + } + msg := p2p.RecvMsg[*pb.Message]{ + From: "sender", + Message: &pb.Message{ + Sum: &pb.Message_Txs{ + Txs: &pb.Txs{Txs: [][]byte{[]byte("precheck-bad")}}, + }, + }, + } + + reactor.cfg.CheckTxErrorBlacklistEnabled = true + reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ + NodeID: "sender", + Status: p2p.PeerStatusUp, + }) + reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ + NodeID: "sender", + Status: p2p.PeerStatusDown, + }) + + require.NoError(t, reactor.handleMempoolMessage(t.Context(), msg)) + require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) +} + // regression test for https://github.com/tendermint/tendermint/issues/5408 func TestReactorConcurrency(t *testing.T) { numTxs := 10 From 1fd34534b7ebb0305de602198031f24205901382 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 15:11:10 +0200 Subject: [PATCH 06/43] wip --- sei-tendermint/internal/mempool/reactor.go | 7 --- .../internal/mempool/reactor_test.go | 53 +++++++++++++++---- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 26ae9a1dae..efb81fa492 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -188,13 +188,6 @@ func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { } } -func (r *Reactor) GetPeerFailedCheckTxCount(nodeID types.NodeID) int { - for counts := range r.failedCheckTxCounts.Lock() { - return counts[nodeID] - } - panic("unreachable") -} - // handleMessage handles an Envelope sent from a peer on a specific p2p Channel. // It will handle errors and any possible panics gracefully. A caller can handle // any error returned by sending a PeerError on the respective channel. diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 5e85e4383d..0f71f4da4d 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -180,6 +180,16 @@ func TestReactorBroadcastTxs(t *testing.T) { rts.waitForTxns(t, convertTex(txs), secondaries...) } +func peerFailedCheckTxCount(reactor *Reactor, nodeID types.NodeID) utils.Option[int] { + for counts := range reactor.failedCheckTxCounts.Lock() { + if count, ok := counts[nodeID]; ok { + return utils.Some(count) + } + return utils.None[int]() + } + panic("unreachable") +} + func TestReactorFailedCheckTxCount(t *testing.T) { cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) require.NoError(t, err) @@ -221,19 +231,19 @@ func TestReactorFailedCheckTxCount(t *testing.T) { reactor.cfg.CheckTxErrorBlacklistEnabled = false require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) - require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, utils.Some(0), peerFailedCheckTxCount(reactor, "sender")) reactor.cfg.CheckTxErrorBlacklistEnabled = true reactor.cfg.CheckTxErrorThreshold = 10 require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) - require.Equal(t, 1, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "sender")) require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx([]byte("precheck-bad")))) - require.Equal(t, 2, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, utils.Some(2), peerFailedCheckTxCount(reactor, "sender")) require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx([]byte("sender=key=1")))) - require.Equal(t, 2, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, utils.Some(2), peerFailedCheckTxCount(reactor, "sender")) } func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { @@ -241,27 +251,52 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + preCheckErr := errors.New("bad tx") + txmp := setup( + t, + &application{Application: kvstore.NewApplication()}, + 0, + WithPreCheck(func(tx types.Tx) error { + if string(tx) == "precheck-bad" { + return preCheckErr + } + return nil + }), + ) reactor := &Reactor{ cfg: cfg.Mempool, + mempool: txmp, ids: NewMempoolIDs(), failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{"other": 1}), + router: &p2p.Router{}, peerRoutines: map[types.NodeID]context.CancelFunc{}, } + msg := p2p.RecvMsg[*pb.Message]{ + From: "sender", + Message: &pb.Message{ + Sum: &pb.Message_Txs{ + Txs: &pb.Txs{Txs: [][]byte{[]byte("precheck-bad")}}, + }, + }, + } + reactor.cfg.CheckTxErrorBlacklistEnabled = true reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ NodeID: "sender", Status: p2p.PeerStatusUp, }) - require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, utils.Some(0), peerFailedCheckTxCount(reactor, "sender")) + + require.NoError(t, reactor.handleMempoolMessage(t.Context(), msg)) + require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "sender")) - reactor.setFailedCheckTxCount("sender", 2) reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ NodeID: "sender", Status: p2p.PeerStatusDown, }) - require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) - require.Equal(t, 1, reactor.GetPeerFailedCheckTxCount("other")) + require.Equal(t, utils.None[int](), peerFailedCheckTxCount(reactor, "sender")) + require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "other")) require.Equal(t, uint16(0), reactor.ids.GetForPeer("sender")) } @@ -310,7 +345,7 @@ func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { }) require.NoError(t, reactor.handleMempoolMessage(t.Context(), msg)) - require.Equal(t, 0, reactor.GetPeerFailedCheckTxCount("sender")) + require.Equal(t, utils.None[int](), peerFailedCheckTxCount(reactor, "sender")) } // regression test for https://github.com/tendermint/tendermint/issues/5408 From a0ef9c78045ba462c590328eabdfbbc2515f12b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 15:18:30 +0200 Subject: [PATCH 07/43] WIP --- .../internal/mempool/reactor_test.go | 72 ++++++++----------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 0f71f4da4d..97c371aa46 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -83,6 +83,29 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest return rts } +func setupReactorForTest(t *testing.T, options ...TxMempoolOption) (*Reactor, *TxMempool) { + t.Helper() + + cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + cfg.Mempool.DropUtilisationThreshold = 0.0 + cfg.Mempool.Broadcast = false + t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + + network := p2p.MakeTestNetwork(t, p2p.TestNetworkOptions{NumNodes: 1}) + node := network.Nodes()[0] + + txmp := NewTxMempool(cfg.Mempool, kvstore.NewApplication(), options...) + reactor, err := NewReactor(cfg.Mempool, txmp, node.Router) + require.NoError(t, err) + reactor.MarkReadyToStart() + require.NoError(t, reactor.Start(t.Context())) + require.True(t, reactor.IsRunning()) + t.Cleanup(reactor.Stop) + + return reactor, txmp +} + func (rts *reactorTestSuite) start(t *testing.T) { t.Helper() rts.network.Start(t) @@ -191,15 +214,9 @@ func peerFailedCheckTxCount(reactor *Reactor, nodeID types.NodeID) utils.Option[ } func TestReactorFailedCheckTxCount(t *testing.T) { - cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - preCheckErr := errors.New("bad tx") - txmp := setup( + reactor, txmp := setupReactorForTest( t, - &application{Application: kvstore.NewApplication()}, - 0, WithPreCheck(func(tx types.Tx) error { if string(tx) == "precheck-bad" { return preCheckErr @@ -207,13 +224,6 @@ func TestReactorFailedCheckTxCount(t *testing.T) { return nil }), ) - reactor := &Reactor{ - cfg: cfg.Mempool, - mempool: txmp, - ids: NewMempoolIDs(), - router: &p2p.Router{}, - failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), - } msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { return p2p.RecvMsg[*pb.Message]{ From: "sender", @@ -247,15 +257,9 @@ func TestReactorFailedCheckTxCount(t *testing.T) { } func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { - cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - preCheckErr := errors.New("bad tx") - txmp := setup( + reactor, _ := setupReactorForTest( t, - &application{Application: kvstore.NewApplication()}, - 0, WithPreCheck(func(tx types.Tx) error { if string(tx) == "precheck-bad" { return preCheckErr @@ -263,13 +267,8 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { return nil }), ) - reactor := &Reactor{ - cfg: cfg.Mempool, - mempool: txmp, - ids: NewMempoolIDs(), - failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{"other": 1}), - router: &p2p.Router{}, - peerRoutines: map[types.NodeID]context.CancelFunc{}, + for counts := range reactor.failedCheckTxCounts.Lock() { + counts["other"] = 1 } msg := p2p.RecvMsg[*pb.Message]{ From: "sender", @@ -297,19 +296,12 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { require.Equal(t, utils.None[int](), peerFailedCheckTxCount(reactor, "sender")) require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "other")) - require.Equal(t, uint16(0), reactor.ids.GetForPeer("sender")) } func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { - cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - preCheckErr := errors.New("bad tx") - txmp := setup( + reactor, _ := setupReactorForTest( t, - &application{Application: kvstore.NewApplication()}, - 0, WithPreCheck(func(tx types.Tx) error { if string(tx) == "precheck-bad" { return preCheckErr @@ -317,14 +309,6 @@ func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { return nil }), ) - reactor := &Reactor{ - cfg: cfg.Mempool, - mempool: txmp, - ids: NewMempoolIDs(), - router: &p2p.Router{}, - failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), - peerRoutines: map[types.NodeID]context.CancelFunc{}, - } msg := p2p.RecvMsg[*pb.Message]{ From: "sender", Message: &pb.Message{ From 63e6bd32f9b980524cff8ababf1751e19a0cc10e Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 15:53:00 +0200 Subject: [PATCH 08/43] wip --- .../internal/mempool/reactor_test.go | 62 +++++++++++-------- sei-tendermint/internal/p2p/testonly.go | 11 ++++ 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 97c371aa46..d34d47b04f 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -213,20 +213,31 @@ func peerFailedCheckTxCount(reactor *Reactor, nodeID types.NodeID) utils.Option[ panic("unreachable") } -func TestReactorFailedCheckTxCount(t *testing.T) { - preCheckErr := errors.New("bad tx") - reactor, txmp := setupReactorForTest( - t, - WithPreCheck(func(tx types.Tx) error { - if string(tx) == "precheck-bad" { - return preCheckErr - } - return nil - }), - ) +func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { + ctx := t.Context() + + rts := setupReactors(ctx, t, 2) + t.Cleanup(leaktest.Check(t)) + + sender := rts.nodes[0] + receiver := rts.nodes[1] + + rts.start(t) + + receiverReactor := rts.reactors[receiver] + receiverReactor.cfg.CheckTxErrorBlacklistEnabled = true + receiverReactor.cfg.CheckTxErrorThreshold = 2 + receiverReactor.mempool.preCheck = func(tx types.Tx) error { + if string(tx) == "bad" { + return errors.New("bad tx") + } + return nil + } + conn := rts.network.Node(receiver).WaitForConnAndGet(ctx, sender) + msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { return p2p.RecvMsg[*pb.Message]{ - From: "sender", + From: sender, Message: &pb.Message{ Sum: &pb.Message_Txs{ Txs: &pb.Txs{Txs: [][]byte{tx}}, @@ -234,26 +245,23 @@ func TestReactorFailedCheckTxCount(t *testing.T) { }, } } - reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ - NodeID: "sender", - Status: p2p.PeerStatusUp, - }) + require.Equal(t, utils.Some(0), peerFailedCheckTxCount(receiverReactor, sender)) - reactor.cfg.CheckTxErrorBlacklistEnabled = false - require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) - require.Equal(t, utils.Some(0), peerFailedCheckTxCount(reactor, "sender")) + require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx([]byte("good-1")))) + require.Equal(t, utils.Some(0), peerFailedCheckTxCount(receiverReactor, sender)) - reactor.cfg.CheckTxErrorBlacklistEnabled = true - reactor.cfg.CheckTxErrorThreshold = 10 + badTx := []byte("bad") + require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx(badTx))) + require.Equal(t, utils.Some(1), peerFailedCheckTxCount(receiverReactor, sender)) - require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx(make([]byte, txmp.config.MaxTxBytes+1)))) - require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "sender")) + require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx([]byte("good-2")))) + require.Equal(t, utils.Some(1), peerFailedCheckTxCount(receiverReactor, sender)) - require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx([]byte("precheck-bad")))) - require.Equal(t, utils.Some(2), peerFailedCheckTxCount(reactor, "sender")) + require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx(badTx))) + require.Equal(t, utils.Some(2), peerFailedCheckTxCount(receiverReactor, sender)) - require.NoError(t, reactor.handleMempoolMessage(t.Context(), msgForTx([]byte("sender=key=1")))) - require.Equal(t, utils.Some(2), peerFailedCheckTxCount(reactor, "sender")) + require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx(badTx))) + rts.network.Node(receiver).WaitForDisconnect(ctx, conn) } func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { diff --git a/sei-tendermint/internal/p2p/testonly.go b/sei-tendermint/internal/p2p/testonly.go index 6d03cdf0ef..ec487ad2ca 100644 --- a/sei-tendermint/internal/p2p/testonly.go +++ b/sei-tendermint/internal/p2p/testonly.go @@ -244,6 +244,17 @@ func (n *TestNode) WaitForConns(ctx context.Context, wantPeers int) { } } +func (n *TestNode) WaitForConnAndGet(ctx context.Context, target types.NodeID) (conn *ConnV2) { + if _, err := n.Router.peerManager.conns.Wait(ctx, func(conns ConnSet) bool { + ok := false + conn, ok = GetAny(conns, target) + return ok + }); err != nil { + panic(err) + } + return +} + func (n *TestNode) WaitForConn(ctx context.Context, target types.NodeID, status bool) { if _, err := n.Router.peerManager.conns.Wait(ctx, func(conns ConnSet) bool { _, ok := GetAny(conns, target) From 254426b536e3e8f11b47ff38a26c4ae532bced7a Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 16:50:23 +0200 Subject: [PATCH 09/43] wip --- sei-tendermint/internal/mempool/mempool.go | 95 +++++++++++-------- .../internal/mempool/mempool_test.go | 59 +++++------- .../internal/mempool/reactor_test.go | 45 ++++----- sei-tendermint/internal/mempool/types.go | 52 ++-------- sei-tendermint/internal/state/execution.go | 4 +- sei-tendermint/internal/state/tx_filter.go | 50 +++------- .../internal/state/tx_filter_test.go | 9 +- sei-tendermint/node/node.go | 9 +- sei-tendermint/node/setup.go | 3 +- 9 files changed, 141 insertions(+), 185 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index aa3bad9d7e..675ba05586 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -113,9 +113,8 @@ type TxMempool struct { // from the mempool. A read-lock is implicitly acquired when executing CheckTx, // however, a caller must explicitly grab a write-lock via Lock when updating // the mempool via Update(). - mtx sync.RWMutex - preCheck PreCheckFunc - postCheck PostCheckFunc + mtx sync.RWMutex + txStateFetcher utils.Option[TxStateFetcher] priorityReservoir *reservoir.Sampler[int64] } @@ -157,18 +156,9 @@ func NewTxMempool( return txmp } -// WithPreCheck sets a filter for the mempool to reject a transaction if f(tx) -// returns an error. This is executed before CheckTx. It only applies to the -// first created block. After that, Update() overwrites the existing value. -func WithPreCheck(f PreCheckFunc) TxMempoolOption { - return func(txmp *TxMempool) { txmp.preCheck = f } -} - -// WithPostCheck sets a filter for the mempool to reject a transaction if -// f(tx, resp) returns an error. This is executed after CheckTx. It only applies -// to the first created block. After that, Update overwrites the existing value. -func WithPostCheck(f PostCheckFunc) TxMempoolOption { - return func(txmp *TxMempool) { txmp.postCheck = f } +// WithTxStateFetcher sets the source of consensus-derived mempool limits. +func WithTxStateFetcher(fetcher TxStateFetcher) TxMempoolOption { + return func(txmp *TxMempool) { txmp.txStateFetcher = utils.Some(fetcher) } } // WithMetrics sets the mempool's metrics collector. @@ -245,6 +235,49 @@ func (txmp *TxMempool) TxsAvailable() <-chan struct{} { return txmp.txsAvailable } +func (txmp *TxMempool) checkTxState(tx types.Tx) error { + fetcher, ok := txmp.txStateFetcher.Get() + if !ok { + return nil + } + + constraints, err := fetcher() + if err != nil { + return err + } + + txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}) + if txSize > constraints.MaxDataBytes { + return fmt.Errorf("tx size is too big: %d, max: %d", txSize, constraints.MaxDataBytes) + } + + return nil +} + +func (txmp *TxMempool) checkResponseState(tx types.Tx, res *abci.ResponseCheckTx) error { + fetcher, ok := txmp.txStateFetcher.Get() + if !ok { + return nil + } + + constraints, err := fetcher() + if err != nil { + return err + } + + if constraints.MaxGas == -1 { + return nil + } + if res.GasWanted < 0 { + return fmt.Errorf("gas wanted %d is negative", res.GasWanted) + } + if res.GasWanted > constraints.MaxGas { + return fmt.Errorf("gas wanted %d is greater than max gas %d", res.GasWanted, constraints.MaxGas) + } + + return nil +} + // CheckTx executes the ABCI CheckTx method for a given transaction. // It acquires a read-lock and attempts to execute the application's // CheckTx ABCI method synchronously. We return an error if any of @@ -256,7 +289,7 @@ func (txmp *TxMempool) TxsAvailable() <-chan struct{} { // return nil. // - The transaction size exceeds the maximum transaction size as defined by the // configuration provided to the mempool. -// - The transaction fails Pre-Check (if it is defined). +// - The transaction fails the consensus-derived mempool checks. // - The proxyAppConn fails, e.g. the buffer is full. // // If the mempool is full, we still execute CheckTx and attempt to find a lower @@ -302,10 +335,8 @@ func (txmp *TxMempool) CheckTx( } } - if txmp.preCheck != nil { - if err := txmp.preCheck(tx); err != nil { - return types.ErrPreCheck{Reason: err} - } + if err := txmp.checkTxState(tx); err != nil { + return types.ErrPreCheck{Reason: err} } txHash := tx.Key() @@ -612,18 +643,14 @@ func (txmp *TxMempool) Update( blockHeight int64, blockTxs types.Txs, execTxResult []*abci.ExecTxResult, - newPreFn PreCheckFunc, - newPostFn PostCheckFunc, + newTxStateFetcher utils.Option[TxStateFetcher], recheck bool, ) error { txmp.height = blockHeight txmp.notifiedTxsAvailable = false - if newPreFn != nil { - txmp.preCheck = newPreFn - } - if newPostFn != nil { - txmp.postCheck = newPostFn + if fetcher, ok := newTxStateFetcher.Get(); ok { + txmp.txStateFetcher = utils.Some(fetcher) } for i, tx := range blockTxs { @@ -688,8 +715,8 @@ func (txmp *TxMempool) Update( // goes to handleRecheckResult. // // addNewTransaction runs after the ABCI application executes CheckTx. -// It runs the postCheck hook if one is defined on the mempool. -// If the CheckTx response code is not OK, or if the postCheck hook +// It runs the consensus-derived post-check for the current state snapshot. +// If the CheckTx response code is not OK, or if the post-check // reports an error, the transaction is rejected. Otherwise, we attempt to insert // the transaction into the mempool. // @@ -702,10 +729,7 @@ func (txmp *TxMempool) Update( // NOTE: // - An explicit lock is NOT required. func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, res *abci.ResponseCheckTx, txInfo TxInfo) error { - var err error - if txmp.postCheck != nil { - err = txmp.postCheck(wtx.tx, res) - } + err := txmp.checkResponseState(wtx.tx, res) if err != nil || res.Code != abci.CodeTypeOK { // ignore bad transactions @@ -851,10 +875,7 @@ func (txmp *TxMempool) handleRecheckResult(tx types.Tx, res *abci.ResponseCheckT // 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) { - var err error - if txmp.postCheck != nil { - err = txmp.postCheck(tx, res.ResponseCheckTx) - } + err := txmp.checkResponseState(tx, res.ResponseCheckTx) // we will treat a transaction that turns pending in a recheck as invalid and evict it if res.Code == abci.CodeTypeOK && err == nil && !res.IsPendingTransaction { diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 98a25dcb6e..d41f21a950 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -20,6 +20,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -266,7 +267,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) { // commit half the transactions and ensure we fire an event txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, nil, nil, true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxStateFetcher](), true)) txmp.Unlock() ensureTxFire() ensureNoTxFire() @@ -299,7 +300,7 @@ func TestTxMempool_Size(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, nil, nil, true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxStateFetcher](), true)) txmp.Unlock() require.Equal(t, len(rawTxs)/2, txmp.Size()) @@ -327,7 +328,7 @@ func TestTxMempool_Flush(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, nil, nil, true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxStateFetcher](), true)) txmp.Unlock() txmp.Flush() @@ -900,7 +901,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, nil, nil, true)) + require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, utils.None[TxStateFetcher](), true)) txmp.Unlock() height++ @@ -941,7 +942,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, nil, nil, true)) + require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, utils.None[TxStateFetcher](), true)) txmp.Unlock() require.Equal(t, 95, txmp.Size()) @@ -967,14 +968,14 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, nil, nil, true)) + require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, utils.None[TxStateFetcher](), true)) txmp.Unlock() require.GreaterOrEqual(t, txmp.Size(), 45) require.GreaterOrEqual(t, txmp.expirationIndex.Size(), 45) } -func TestTxMempool_CheckTxPostCheckError(t *testing.T) { +func TestTxMempool_CheckTxStateFetcherError(t *testing.T) { cases := []struct { name string err error @@ -991,33 +992,25 @@ func TestTxMempool_CheckTxPostCheckError(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ctx := t.Context() - - client := &application{Application: kvstore.NewApplication()} - - postCheckFn := func(_ types.Tx, _ *abci.ResponseCheckTx) error { - return tc.err - } - txmp := setup(t, client, 0, WithPostCheck(postCheckFn)) rng := rand.New(rand.NewSource(time.Now().UnixNano())) - tx := make([]byte, txmp.config.MaxTxBytes-1) + tx := make([]byte, config.TestMempoolConfig().MaxTxBytes-1) _, err := rng.Read(tx) require.NoError(t, err) - callback := func(res *abci.ResponseCheckTx) { - expectedErrString := "" - if tc.err != nil { - expectedErrString = tc.err.Error() - require.Equal(t, expectedErrString, txmp.postCheck(tx, res).Error()) - } else { - require.Equal(t, nil, txmp.postCheck(tx, res)) - } - } + txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, WithTxStateFetcher(func() (TxStateConstraints, error) { + return TxStateConstraints{ + MaxDataBytes: int64(len(tx) + 100), + MaxGas: 1, + }, tc.err + })) + if tc.err == nil { - require.NoError(t, txmp.CheckTx(ctx, tx, callback, TxInfo{SenderID: 0})) - } else { - err = txmp.CheckTx(ctx, tx, callback, TxInfo{SenderID: 0}) - fmt.Print(err.Error()) + require.Error(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0})) + return } + + err = txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) + require.ErrorIs(t, err, tc.err) }) } } @@ -1141,7 +1134,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, // out of gas - }, nil, nil, true)) + }, utils.None[TxStateFetcher](), true)) txmp.Unlock() // Tx should be removed from the mempool @@ -1155,7 +1148,7 @@ 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 - }, nil, nil, true)) + }, utils.None[TxStateFetcher](), true)) txmp.Unlock() require.Equal(t, 0, txmp.Size()) @@ -1186,7 +1179,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, nil, nil, true)) + }, utils.None[TxStateFetcher](), true)) txmp.Unlock() // Re-enter the mempool (first failure allows retry) @@ -1196,7 +1189,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{ {Code: abci.CodeTypeOK}, - }, nil, nil, true)) + }, utils.None[TxStateFetcher](), true)) txmp.Unlock() // Success clears the failure tracker. Simulate LRU eviction of the @@ -1210,7 +1203,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 3, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, nil, nil, true)) + }, utils.None[TxStateFetcher](), true)) txmp.Unlock() // First-failure grace should be restored: tx allowed to re-enter diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index d34d47b04f..b3471c3cf5 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -2,7 +2,6 @@ package mempool import ( "context" - "errors" "fmt" "os" "runtime" @@ -227,12 +226,12 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { receiverReactor := rts.reactors[receiver] receiverReactor.cfg.CheckTxErrorBlacklistEnabled = true receiverReactor.cfg.CheckTxErrorThreshold = 2 - receiverReactor.mempool.preCheck = func(tx types.Tx) error { - if string(tx) == "bad" { - return errors.New("bad tx") - } - return nil - } + receiverReactor.mempool.txStateFetcher = utils.Some(TxStateFetcher(func() (TxStateConstraints, error) { + return TxStateConstraints{ + MaxDataBytes: 10, + MaxGas: -1, + }, nil + })) conn := rts.network.Node(receiver).WaitForConnAndGet(ctx, sender) msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { @@ -250,7 +249,7 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx([]byte("good-1")))) require.Equal(t, utils.Some(0), peerFailedCheckTxCount(receiverReactor, sender)) - badTx := []byte("bad") + badTx := []byte("bad-transaction") require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx(badTx))) require.Equal(t, utils.Some(1), peerFailedCheckTxCount(receiverReactor, sender)) @@ -265,14 +264,13 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { } func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { - preCheckErr := errors.New("bad tx") reactor, _ := setupReactorForTest( t, - WithPreCheck(func(tx types.Tx) error { - if string(tx) == "precheck-bad" { - return preCheckErr - } - return nil + WithTxStateFetcher(func() (TxStateConstraints, error) { + return TxStateConstraints{ + MaxDataBytes: 10, + MaxGas: -1, + }, nil }), ) for counts := range reactor.failedCheckTxCounts.Lock() { @@ -282,7 +280,7 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { From: "sender", Message: &pb.Message{ Sum: &pb.Message_Txs{ - Txs: &pb.Txs{Txs: [][]byte{[]byte("precheck-bad")}}, + Txs: &pb.Txs{Txs: [][]byte{[]byte("precheck-bad-transaction")}}, }, }, } @@ -307,21 +305,20 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { } func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { - preCheckErr := errors.New("bad tx") reactor, _ := setupReactorForTest( t, - WithPreCheck(func(tx types.Tx) error { - if string(tx) == "precheck-bad" { - return preCheckErr - } - return nil + WithTxStateFetcher(func() (TxStateConstraints, error) { + return TxStateConstraints{ + MaxDataBytes: 10, + MaxGas: -1, + }, nil }), ) msg := p2p.RecvMsg[*pb.Message]{ From: "sender", Message: &pb.Message{ Sum: &pb.Message_Txs{ - Txs: &pb.Txs{Txs: [][]byte{[]byte("precheck-bad")}}, + Txs: &pb.Txs{Txs: [][]byte{[]byte("precheck-bad-transaction")}}, }, }, } @@ -377,7 +374,7 @@ func TestReactorConcurrency(t *testing.T) { deliverTxResponses[i] = &abci.ExecTxResult{Code: 0} } - require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, nil, nil, true)) + require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, utils.None[TxStateFetcher](), true)) }() // 1. submit a bunch of txs @@ -391,7 +388,7 @@ func TestReactorConcurrency(t *testing.T) { mempool.Lock() defer mempool.Unlock() - err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), nil, nil, true) + err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), utils.None[TxStateFetcher](), true) require.NoError(t, err) }() } diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index 0fb4971419..1acf4642fb 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -1,12 +1,9 @@ package mempool import ( - "fmt" "math" - abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" - "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) const ( @@ -22,46 +19,13 @@ const ( MaxActiveIDs = math.MaxUint16 ) -// PreCheckFunc is an optional filter executed before CheckTx and rejects -// transaction if false is returned. An example would be to ensure that a -// transaction doesn't exceeded the block size. -type PreCheckFunc func(types.Tx) error - -// PostCheckFunc is an optional filter executed after CheckTx and rejects -// transaction if false is returned. An example would be to ensure a -// transaction doesn't require more gas than available for the block. -type PostCheckFunc func(types.Tx, *abci.ResponseCheckTx) error - -// PreCheckMaxBytes checks that the size of the transaction is smaller or equal -// to the expected maxBytes. -func PreCheckMaxBytes(maxBytes int64) PreCheckFunc { - return func(tx types.Tx) error { - txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}) - - if txSize > maxBytes { - return fmt.Errorf("tx size is too big: %d, max: %d", txSize, maxBytes) - } - - return nil - } +// TxStateConstraints contains the consensus-derived mempool limits for the +// current state snapshot. +type TxStateConstraints struct { + MaxDataBytes int64 + MaxGas int64 } -// PostCheckMaxGas checks that the wanted gas is smaller or equal to the passed -// maxGas. Returns nil if maxGas is -1. -func PostCheckMaxGas(maxGas int64) PostCheckFunc { - return func(tx types.Tx, res *abci.ResponseCheckTx) error { - if maxGas == -1 { - return nil - } - if res.GasWanted < 0 { - return fmt.Errorf("gas wanted %d is negative", - res.GasWanted) - } - if res.GasWanted > maxGas { - return fmt.Errorf("gas wanted %d is greater than max gas %d", - res.GasWanted, maxGas) - } - - return nil - } -} +// TxStateFetcher returns the consensus-derived mempool limits for the current +// state snapshot. +type TxStateFetcher func() (TxStateConstraints, error) diff --git a/sei-tendermint/internal/state/execution.go b/sei-tendermint/internal/state/execution.go index ec1f20834d..73611b1dbd 100644 --- a/sei-tendermint/internal/state/execution.go +++ b/sei-tendermint/internal/state/execution.go @@ -12,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle" "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/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" @@ -427,8 +428,7 @@ func (blockExec *BlockExecutor) Commit( block.Height, block.Txs, txResults, - TxPreCheckForState(state), - TxPostCheckForState(state), + utils.Some(TxStateFetcherForState(state)), state.ConsensusParams.ABCI.RecheckTx, ) blockExec.metrics.UpdateMempoolTime.Observe(float64(time.Since(start))) diff --git a/sei-tendermint/internal/state/tx_filter.go b/sei-tendermint/internal/state/tx_filter.go index 7be737666a..e24920e7da 100644 --- a/sei-tendermint/internal/state/tx_filter.go +++ b/sei-tendermint/internal/state/tx_filter.go @@ -4,7 +4,6 @@ import ( "sync" "time" - abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -38,48 +37,29 @@ func cachingStateFetcher(store Store) func() (State, error) { } -// TxPreCheckFromStore returns a function to filter transactions before processing. -// The function limits the size of a transaction to the block's maximum data size. -func TxPreCheckFromStore(store Store) mempool.PreCheckFunc { +// TxStateFetcherFromStore returns the consensus-derived mempool limits for the +// current persisted state. +func TxStateFetcherFromStore(store Store) mempool.TxStateFetcher { fetch := cachingStateFetcher(store) - return func(tx types.Tx) error { + return func() (mempool.TxStateConstraints, error) { state, err := fetch() if err != nil { - return err + return mempool.TxStateConstraints{}, err } - return TxPreCheckForState(state)(tx) + return TxStateFetcherForState(state)() } } -func TxPreCheckForState(state State) mempool.PreCheckFunc { - return func(tx types.Tx) error { - maxDataBytes := types.MaxDataBytesNoEvidence( - state.ConsensusParams.Block.MaxBytes, - state.Validators.Size(), - ) - return mempool.PreCheckMaxBytes(maxDataBytes)(tx) - } - -} - -// TxPostCheckFromStore returns a function to filter transactions after processing. -// The function limits the gas wanted by a transaction to the block's maximum total gas. -func TxPostCheckFromStore(store Store) mempool.PostCheckFunc { - fetch := cachingStateFetcher(store) - - return func(tx types.Tx, resp *abci.ResponseCheckTx) error { - state, err := fetch() - if err != nil { - return err - } - return mempool.PostCheckMaxGas(state.ConsensusParams.Block.MaxGas)(tx, resp) - } -} - -func TxPostCheckForState(state State) mempool.PostCheckFunc { - return func(tx types.Tx, resp *abci.ResponseCheckTx) error { - return mempool.PostCheckMaxGas(state.ConsensusParams.Block.MaxGas)(tx, resp) +func TxStateFetcherForState(state State) mempool.TxStateFetcher { + return func() (mempool.TxStateConstraints, error) { + return mempool.TxStateConstraints{ + MaxDataBytes: types.MaxDataBytesNoEvidence( + state.ConsensusParams.Block.MaxBytes, + state.Validators.Size(), + ), + MaxGas: state.ConsensusParams.Block.MaxGas, + }, nil } } diff --git a/sei-tendermint/internal/state/tx_filter_test.go b/sei-tendermint/internal/state/tx_filter_test.go index 9a5aa372fc..105f3b23df 100644 --- a/sei-tendermint/internal/state/tx_filter_test.go +++ b/sei-tendermint/internal/state/tx_filter_test.go @@ -31,11 +31,14 @@ func TestTxFilter(t *testing.T) { state, err := sm.MakeGenesisState(genDoc) require.NoError(t, err) - f := sm.TxPreCheckForState(state) + f := sm.TxStateFetcherForState(state) + constraints, err := f() + require.NoError(t, err) + txSize := types.ComputeProtoSizeForTxs([]types.Tx{tc.tx}) if tc.isErr { - assert.NotNil(t, f(tc.tx), "#%v", i) + assert.Greater(t, txSize, constraints.MaxDataBytes, "#%v", i) } else { - assert.Nil(t, f(tc.tx), "#%v", i) + assert.LessOrEqual(t, txSize, constraints.MaxDataBytes, "#%v", i) } } } diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index cf96b17ca3..c6c30df85b 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -189,14 +189,13 @@ func makeNode( fmt.Errorf("failed to create router: %w", err), makeCloser(closers)) } - node.router = router - node.rpcEnv.Router = router - node.shutdownOps = makeCloser(closers) - - mpReactor, mp, err := createMempoolReactor(cfg, app, stateStore, nodeMetrics.mempool, node.router) + mpReactor, mp, err := createMempoolReactor(cfg, app, stateStore, nodeMetrics.mempool, router) if err != nil { return nil, fmt.Errorf("createMempoolReactor(): %w", err) } + node.router = router + node.rpcEnv.Router = router + node.shutdownOps = makeCloser(closers) evReactor, evPool, edbCloser, err := createEvidenceReactor(cfg, dbProvider, stateStore, blockStore, node.router, nodeMetrics.evidence, eventBus) diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 270fcb9d1a..6a20a59e26 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -154,8 +154,7 @@ func createMempoolReactor( cfg.Mempool, appClient, mempool.WithMetrics(memplMetrics), - mempool.WithPreCheck(sm.TxPreCheckFromStore(store)), - mempool.WithPostCheck(sm.TxPostCheckFromStore(store)), + mempool.WithTxStateFetcher(sm.TxStateFetcherFromStore(store)), ) reactor, err := mempool.NewReactor(cfg.Mempool, mp, router) From 44b928831e872ea7661928aba98519e294bc16bc Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 16:58:59 +0200 Subject: [PATCH 10/43] wip --- sei-tendermint/internal/mempool/mempool_test.go | 4 ++-- sei-tendermint/internal/mempool/reactor_test.go | 12 ++++++------ sei-tendermint/internal/mempool/types.go | 10 +++++----- sei-tendermint/internal/state/tx_filter.go | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index d41f21a950..62e2200217 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -997,8 +997,8 @@ func TestTxMempool_CheckTxStateFetcherError(t *testing.T) { _, err := rng.Read(tx) require.NoError(t, err) - txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, WithTxStateFetcher(func() (TxStateConstraints, error) { - return TxStateConstraints{ + txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, WithTxStateFetcher(func() (TxConstraints, error) { + return TxConstraints{ MaxDataBytes: int64(len(tx) + 100), MaxGas: 1, }, tc.err diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index b3471c3cf5..600d9fd978 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -226,8 +226,8 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { receiverReactor := rts.reactors[receiver] receiverReactor.cfg.CheckTxErrorBlacklistEnabled = true receiverReactor.cfg.CheckTxErrorThreshold = 2 - receiverReactor.mempool.txStateFetcher = utils.Some(TxStateFetcher(func() (TxStateConstraints, error) { - return TxStateConstraints{ + receiverReactor.mempool.txStateFetcher = utils.Some(TxStateFetcher(func() (TxConstraints, error) { + return TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil @@ -266,8 +266,8 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { reactor, _ := setupReactorForTest( t, - WithTxStateFetcher(func() (TxStateConstraints, error) { - return TxStateConstraints{ + WithTxStateFetcher(func() (TxConstraints, error) { + return TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil @@ -307,8 +307,8 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { reactor, _ := setupReactorForTest( t, - WithTxStateFetcher(func() (TxStateConstraints, error) { - return TxStateConstraints{ + WithTxStateFetcher(func() (TxConstraints, error) { + return TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index 1acf4642fb..a49dbee44a 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -19,13 +19,13 @@ const ( MaxActiveIDs = math.MaxUint16 ) -// TxStateConstraints contains the consensus-derived mempool limits for the -// current state snapshot. -type TxStateConstraints struct { +// TxConstraints contains the precomputed consensus-derived mempool limits for +// the current state snapshot. +type TxConstraints struct { MaxDataBytes int64 MaxGas int64 } -// TxStateFetcher returns the consensus-derived mempool limits for the current +// TxStateFetcher returns the precomputed consensus-derived mempool limits for the current // state snapshot. -type TxStateFetcher func() (TxStateConstraints, error) +type TxStateFetcher func() (TxConstraints, error) diff --git a/sei-tendermint/internal/state/tx_filter.go b/sei-tendermint/internal/state/tx_filter.go index e24920e7da..c82a590422 100644 --- a/sei-tendermint/internal/state/tx_filter.go +++ b/sei-tendermint/internal/state/tx_filter.go @@ -37,15 +37,15 @@ func cachingStateFetcher(store Store) func() (State, error) { } -// TxStateFetcherFromStore returns the consensus-derived mempool limits for the +// TxStateFetcherFromStore returns the precomputed consensus-derived mempool limits for the // current persisted state. func TxStateFetcherFromStore(store Store) mempool.TxStateFetcher { fetch := cachingStateFetcher(store) - return func() (mempool.TxStateConstraints, error) { + return func() (mempool.TxConstraints, error) { state, err := fetch() if err != nil { - return mempool.TxStateConstraints{}, err + return mempool.TxConstraints{}, err } return TxStateFetcherForState(state)() @@ -53,8 +53,8 @@ func TxStateFetcherFromStore(store Store) mempool.TxStateFetcher { } func TxStateFetcherForState(state State) mempool.TxStateFetcher { - return func() (mempool.TxStateConstraints, error) { - return mempool.TxStateConstraints{ + return func() (mempool.TxConstraints, error) { + return mempool.TxConstraints{ MaxDataBytes: types.MaxDataBytesNoEvidence( state.ConsensusParams.Block.MaxBytes, state.Validators.Size(), From 3061e4991252a9eb49efcf80613ea9df3fb53a4d Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 17:03:59 +0200 Subject: [PATCH 11/43] wip --- sei-tendermint/internal/mempool/reactor.go | 10 +++----- sei-tendermint/node/setup.go | 28 ++++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index efb81fa492..926e1b6630 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -48,17 +48,13 @@ type Reactor struct { } // NewReactor returns a reference to a new reactor. -func NewReactor( - cfg *config.MempoolConfig, - txmp *TxMempool, - router *p2p.Router, -) (*Reactor, error) { - channel, err := p2p.OpenChannel(router, GetChannelDescriptor(cfg)) +func NewReactor(txmp *TxMempool, router *p2p.Router) (*Reactor, error) { + channel, err := p2p.OpenChannel(router, GetChannelDescriptor(txmp.config)) if err != nil { return nil, fmt.Errorf("router.OpenChannel(): %w", err) } r := &Reactor{ - cfg: cfg, + cfg: txmp.config, mempool: txmp, ids: NewMempoolIDs(), router: router, diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 6a20a59e26..26eedb4c7d 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -142,31 +142,35 @@ func onlyValidatorIsUs(state sm.State, pubKey utils.Option[crypto.PubKey]) bool return ok && bytes.Equal(k.Address(), addr) } -func createMempoolReactor( +func createMempool( cfg *config.Config, - appClient abci.Application, + app abci.Application, store sm.Store, memplMetrics *mempool.Metrics, - router *p2p.Router, -) (*mempool.Reactor, *mempool.TxMempool, error) { - - mp := mempool.NewTxMempool( +) *mempool.TxMempool { + return mempool.NewTxMempool( cfg.Mempool, - appClient, + app, mempool.WithMetrics(memplMetrics), mempool.WithTxStateFetcher(sm.TxStateFetcherFromStore(store)), ) +} - reactor, err := mempool.NewReactor(cfg.Mempool, mp, router) +func createMempoolReactor( + cfg *config.Config, + router *p2p.Router, + mp *mempool.TxMempool, +) (*mempool.Reactor, error) { + reactor, err := mempool.NewReactor(mp, router) if err != nil { - return nil, nil, fmt.Errorf("mempool.NewReactor(): %w", err) + return nil, fmt.Errorf("mempool.NewReactor(): %w", err) } if cfg.Consensus.WaitForTxs() { mp.EnableTxsAvailable() } - return reactor, mp, nil + return reactor, nil } func createEvidenceReactor( @@ -196,7 +200,7 @@ func createRouter( nodeInfoProducer func() *types.NodeInfo, nodeKey types.NodeKey, cfg *config.Config, - appClient abci.Application, + app abci.Application, dbProvider config.DBProvider, ) (*p2p.Router, closer, error) { closer := func() error { return nil } @@ -204,7 +208,7 @@ func createRouter( if err != nil { return nil, closer, err } - options := getRouterConfig(cfg, appClient) + options := getRouterConfig(cfg, app) options.Endpoint = ep options.MaxIncomingConnectionAttempts = utils.Some(cfg.P2P.MaxIncomingConnectionAttempts) options.MaxDialRate = utils.Some(rate.Every(cfg.P2P.DialInterval)) From 47aa6884ebcbe458c9c8decd8bf27685eac6e6d7 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 17:42:46 +0200 Subject: [PATCH 12/43] wip --- .../internal/consensus/common_test.go | 4 -- .../internal/consensus/mempool_test.go | 4 -- .../internal/consensus/reactor_test.go | 4 -- sei-tendermint/internal/mempool/mempool.go | 42 +++++++------------ .../internal/mempool/mempool_test.go | 1 - sei-tendermint/internal/mempool/reactor.go | 2 +- .../internal/mempool/reactor_test.go | 8 +--- sei-tendermint/node/node.go | 10 ++++- sei-tendermint/node/setup.go | 31 -------------- 9 files changed, 26 insertions(+), 80 deletions(-) diff --git a/sei-tendermint/internal/consensus/common_test.go b/sei-tendermint/internal/consensus/common_test.go index 561454e7bd..c7c84ad2ec 100644 --- a/sei-tendermint/internal/consensus/common_test.go +++ b/sei-tendermint/internal/consensus/common_test.go @@ -469,10 +469,6 @@ func newStateWithConfigAndBlockStore( proxyAppConnMem, ) - if thisConfig.Consensus.WaitForTxs() { - mempool.EnableTxsAvailable() - } - evpool := sm.EmptyEvidencePool{} // Make State diff --git a/sei-tendermint/internal/consensus/mempool_test.go b/sei-tendermint/internal/consensus/mempool_test.go index 4cb3e0e264..17f66d2420 100644 --- a/sei-tendermint/internal/consensus/mempool_test.go +++ b/sei-tendermint/internal/consensus/mempool_test.go @@ -62,7 +62,6 @@ func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { Power: 10, Params: factory.ConsensusParams()}) cs := newStateWithConfig(ctx, config, state, privVals[0], NewCounterApplication()) - cs.txMempool.EnableTxsAvailable() height, round := cs.roundState.Height(), cs.roundState.Round() newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) startTestRound(ctx, cs, height, round) @@ -91,8 +90,6 @@ func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) { cs := newStateWithConfig(ctx, config, state, privVals[0], NewCounterApplication()) height, round := cs.roundState.Height(), cs.roundState.Round() - cs.txMempool.EnableTxsAvailable() - newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) startTestRound(ctx, cs, height, round) @@ -115,7 +112,6 @@ func TestMempoolProgressInHigherRound(t *testing.T) { Power: 10, Params: factory.ConsensusParams()}) cs := newStateWithConfig(ctx, config, state, privVals[0], NewCounterApplication()) - cs.txMempool.EnableTxsAvailable() height, round := cs.roundState.Height(), cs.roundState.Round() newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) newRoundCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound) diff --git a/sei-tendermint/internal/consensus/reactor_test.go b/sei-tendermint/internal/consensus/reactor_test.go index e29de08b61..40a61b5d9a 100644 --- a/sei-tendermint/internal/consensus/reactor_test.go +++ b/sei-tendermint/internal/consensus/reactor_test.go @@ -277,10 +277,6 @@ func TestReactorWithEvidence(t *testing.T) { proxyAppConnMem, ) - if thisConfig.Consensus.WaitForTxs() { - mempool.EnableTxsAvailable() - } - // mock the evidence pool // everyone includes evidence of another double signing vIdx := (i + 1) % n diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 675ba05586..5e744bbe88 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -47,7 +47,7 @@ type TxMempoolOption func(*TxMempool) type TxMempool struct { metrics *Metrics config *config.MempoolConfig - proxyAppConn abci.Application + app abci.Application // txsAvailable fires once for each height when the mempool is not empty txsAvailable chan struct{} @@ -121,13 +121,14 @@ type TxMempool struct { func NewTxMempool( cfg *config.MempoolConfig, - proxyAppConn abci.Application, + app abci.Application, options ...TxMempoolOption, ) *TxMempool { txmp := &TxMempool{ config: cfg, - proxyAppConn: proxyAppConn, + app: app, + txsAvailable: make(chan struct{}, 1), height: -1, cache: NopTxCache{}, blockFailedTxs: NopTxCache{}, @@ -221,14 +222,6 @@ func (txmp *TxMempool) WaitForNextTx() <-chan struct{} { return txmp.gossipIndex // thread-safe. func (txmp *TxMempool) NextGossipTx() *clist.CElement { return txmp.gossipIndex.Front() } -// EnableTxsAvailable enables the mempool to trigger events when transactions -// are available on a block by block basis. -func (txmp *TxMempool) EnableTxsAvailable() { - txmp.mtx.Lock() - defer txmp.mtx.Unlock() - txmp.txsAvailable = make(chan struct{}, 1) -} - // TxsAvailable returns a channel which fires once for every height, and only // when transactions are available in the mempool. It is thread-safe. func (txmp *TxMempool) TxsAvailable() <-chan struct{} { @@ -290,7 +283,7 @@ func (txmp *TxMempool) checkResponseState(tx types.Tx, res *abci.ResponseCheckTx // - The transaction size exceeds the maximum transaction size as defined by the // configuration provided to the mempool. // - The transaction fails the consensus-derived mempool checks. -// - The proxyAppConn fails, e.g. the buffer is full. +// - The app fails, e.g. the buffer is full. // // If the mempool is full, we still execute CheckTx and attempt to find a lower // priority transaction to evict. If such a transaction exists, we remove the @@ -320,7 +313,7 @@ func (txmp *TxMempool) CheckTx( if txmp.config.DropUtilisationThreshold > 0 && txmp.utilisation() >= txmp.config.DropUtilisationThreshold { txmp.metrics.CheckTxMetDropUtilisationThreshold.Add(1) - hint, err := txmp.proxyAppConn.GetTxPriorityHint(ctx, &abci.RequestGetTxPriorityHintV2{Tx: tx}) + hint, err := txmp.app.GetTxPriorityHint(ctx, &abci.RequestGetTxPriorityHintV2{Tx: tx}) if err != nil { txmp.metrics.observeCheckTxPriorityDistribution(0, true, txInfo.SenderNodeID, err) logger.Error("failed to get tx priority hint", "err", err) @@ -356,7 +349,7 @@ func (txmp *TxMempool) CheckTx( c.Increment(txHash) } - res, err := txmp.proxyAppConn.CheckTx(ctx, &abci.RequestCheckTxV2{Tx: tx}) + res, err := txmp.app.CheckTx(ctx, &abci.RequestCheckTxV2{Tx: tx}) if err != nil { txmp.metrics.NumberOfFailedCheckTxs.Add(1) txmp.metrics.observeCheckTxPriorityDistribution(0, false, txInfo.SenderNodeID, err) @@ -919,7 +912,7 @@ func (txmp *TxMempool) handleRecheckResult(tx types.Tx, res *abci.ResponseCheckT // updateReCheckTxs updates the recheck cursors using the gossipIndex. For // each transaction, it executes CheckTx. The global callback defined on -// the proxyAppConn will be executed for each transaction after CheckTx is +// the app will be executed for each transaction after CheckTx is // executed. // // NOTE: @@ -943,7 +936,7 @@ func (txmp *TxMempool) updateReCheckTxs(ctx context.Context) { // Only execute CheckTx if the transaction is not marked as removed which // could happen if the transaction was evicted. if !txmp.txStore.IsTxRemoved(wtx) { - res, err := txmp.proxyAppConn.CheckTx(ctx, &abci.RequestCheckTxV2{ + res, err := txmp.app.CheckTx(ctx, &abci.RequestCheckTxV2{ Tx: wtx.tx, Type: abci.CheckTxTypeV2Recheck, }) @@ -955,7 +948,6 @@ func (txmp *TxMempool) updateReCheckTxs(ctx context.Context) { txmp.handleRecheckResult(wtx.tx, res) } } - } // canAddTx returns an error if we cannot insert the provided *WrappedTx into @@ -1124,18 +1116,14 @@ func (txmp *TxMempool) purgeExpiredTxs(blockHeight int64) { } func (txmp *TxMempool) notifyTxsAvailable() { - if txmp.NumTxsNotPending() == 0 { + if txmp.NumTxsNotPending() == 0 || txmp.notifiedTxsAvailable { return } - - if txmp.txsAvailable != nil && !txmp.notifiedTxsAvailable { - // channel cap is 1, so this will send once - txmp.notifiedTxsAvailable = true - - select { - case txmp.txsAvailable <- struct{}{}: - default: - } + // channel cap is 1, so this will send once + txmp.notifiedTxsAvailable = true + select { + case txmp.txsAvailable <- struct{}{}: + default: } } diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 62e2200217..8d529e2241 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -226,7 +226,6 @@ func TestTxMempool_TxsAvailable(t *testing.T) { client := &application{Application: kvstore.NewApplication()} txmp := setup(t, client, 0) - txmp.EnableTxsAvailable() ensureNoTxFire := func() { timer := time.NewTimer(500 * time.Millisecond) diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 926e1b6630..0cac794628 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -59,7 +59,7 @@ func NewReactor(txmp *TxMempool, router *p2p.Router) (*Reactor, error) { ids: NewMempoolIDs(), router: router, channel: channel, - peerRoutines: make(map[types.NodeID]context.CancelFunc), + peerRoutines: map[types.NodeID]context.CancelFunc{}, failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), observePanic: defaultObservePanic, readyToStart: make(chan struct{}, 1), diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 600d9fd978..f54a0adfbe 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -56,11 +56,7 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest mempool := setup(t, app, 0) rts.mempools[nodeID] = mempool - reactor, err := NewReactor( - cfg.Mempool, - mempool, - node.Router, - ) + reactor, err := NewReactor(mempool, node.Router) if err != nil { t.Fatalf("NewReactor(): %v", err) } @@ -95,7 +91,7 @@ func setupReactorForTest(t *testing.T, options ...TxMempoolOption) (*Reactor, *T node := network.Nodes()[0] txmp := NewTxMempool(cfg.Mempool, kvstore.NewApplication(), options...) - reactor, err := NewReactor(cfg.Mempool, txmp, node.Router) + reactor, err := NewReactor(txmp, node.Router) require.NoError(t, err) reactor.MarkReadyToStart() require.NoError(t, reactor.Start(t.Context())) diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index c6c30df85b..1aaf38daee 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -189,9 +189,15 @@ func makeNode( fmt.Errorf("failed to create router: %w", err), makeCloser(closers)) } - mpReactor, mp, err := createMempoolReactor(cfg, app, stateStore, nodeMetrics.mempool, router) + mp := mempool.NewTxMempool( + cfg.Mempool, + app, + mempool.WithMetrics(nodeMetrics.mempool), + mempool.WithTxStateFetcher(sm.TxStateFetcherFromStore(stateStore)), + ) + mpReactor, err := mempool.NewReactor(mp, router) if err != nil { - return nil, fmt.Errorf("createMempoolReactor(): %w", err) + return nil, fmt.Errorf("mempool.NewReactor(): %w", err) } node.router = router node.rpcEnv.Router = router diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 26eedb4c7d..c40d2e5591 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -142,37 +142,6 @@ func onlyValidatorIsUs(state sm.State, pubKey utils.Option[crypto.PubKey]) bool return ok && bytes.Equal(k.Address(), addr) } -func createMempool( - cfg *config.Config, - app abci.Application, - store sm.Store, - memplMetrics *mempool.Metrics, -) *mempool.TxMempool { - return mempool.NewTxMempool( - cfg.Mempool, - app, - mempool.WithMetrics(memplMetrics), - mempool.WithTxStateFetcher(sm.TxStateFetcherFromStore(store)), - ) -} - -func createMempoolReactor( - cfg *config.Config, - router *p2p.Router, - mp *mempool.TxMempool, -) (*mempool.Reactor, error) { - reactor, err := mempool.NewReactor(mp, router) - if err != nil { - return nil, fmt.Errorf("mempool.NewReactor(): %w", err) - } - - if cfg.Consensus.WaitForTxs() { - mp.EnableTxsAvailable() - } - - return reactor, nil -} - func createEvidenceReactor( cfg *config.Config, dbProvider config.DBProvider, From aaf3ee92dbcc0a5505eb26ca63098d2512cf0f98 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 18:27:55 +0200 Subject: [PATCH 13/43] wip --- .../internal/blocksync/reactor_test.go | 2 +- .../internal/consensus/common_test.go | 2 + .../internal/consensus/reactor_test.go | 2 + sei-tendermint/internal/consensus/replay.go | 2 +- sei-tendermint/internal/mempool/mempool.go | 83 +++++++------------ .../internal/mempool/mempool_bench_test.go | 2 +- .../internal/mempool/mempool_test.go | 78 ++++++++--------- sei-tendermint/internal/mempool/reactor.go | 9 -- .../internal/mempool/reactor_test.go | 65 +++------------ sei-tendermint/internal/mempool/types.go | 11 ++- sei-tendermint/internal/state/execution.go | 2 +- sei-tendermint/internal/state/helpers_test.go | 2 +- sei-tendermint/internal/state/tx_filter.go | 8 +- .../internal/state/tx_filter_test.go | 2 +- sei-tendermint/node/node.go | 7 +- sei-tendermint/node/node_test.go | 6 ++ .../test/fuzz/tests/mempool_test.go | 2 +- 17 files changed, 110 insertions(+), 175 deletions(-) diff --git a/sei-tendermint/internal/blocksync/reactor_test.go b/sei-tendermint/internal/blocksync/reactor_test.go index 83480ab6fa..ef63c01e23 100644 --- a/sei-tendermint/internal/blocksync/reactor_test.go +++ b/sei-tendermint/internal/blocksync/reactor_test.go @@ -93,7 +93,7 @@ func makeReactor( state, err := sm.MakeGenesisState(genDoc) require.NoError(t, err) require.NoError(t, stateStore.Save(state)) - mp := mempool.NewTxMempool(config.TestMempoolConfig(), app) + mp := mempool.NewTxMempool(config.TestMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) eventbus := eventbus.NewDefault() require.NoError(t, eventbus.Start(ctx)) diff --git a/sei-tendermint/internal/consensus/common_test.go b/sei-tendermint/internal/consensus/common_test.go index c7c84ad2ec..da97918b42 100644 --- a/sei-tendermint/internal/consensus/common_test.go +++ b/sei-tendermint/internal/consensus/common_test.go @@ -467,6 +467,8 @@ func newStateWithConfigAndBlockStore( mempool := mempool.NewTxMempool( thisConfig.Mempool, proxyAppConnMem, + mempool.NopMetrics(), + mempool.NopTxConstraintsFetcher, ) evpool := sm.EmptyEvidencePool{} diff --git a/sei-tendermint/internal/consensus/reactor_test.go b/sei-tendermint/internal/consensus/reactor_test.go index 40a61b5d9a..11f6c4a666 100644 --- a/sei-tendermint/internal/consensus/reactor_test.go +++ b/sei-tendermint/internal/consensus/reactor_test.go @@ -275,6 +275,8 @@ func TestReactorWithEvidence(t *testing.T) { mempool := mempool.NewTxMempool( thisConfig.Mempool, proxyAppConnMem, + mempool.NopMetrics(), + mempool.NopTxConstraintsFetcher, ) // mock the evidence pool diff --git a/sei-tendermint/internal/consensus/replay.go b/sei-tendermint/internal/consensus/replay.go index 416882d791..f4d8040b78 100644 --- a/sei-tendermint/internal/consensus/replay.go +++ b/sei-tendermint/internal/consensus/replay.go @@ -135,7 +135,7 @@ func NewHandshaker( } func newReplayTxMempool(appClient abci.Application) *mempool.TxMempool { - return mempool.NewTxMempool(config.DefaultMempoolConfig(), appClient) + return mempool.NewTxMempool(config.DefaultMempoolConfig(), appClient, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) } // NBlocks returns the number of blocks applied to the state. diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 5e744bbe88..b9ef36fe19 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -37,17 +37,14 @@ const ( MinGasEVMTx = 21000 ) -// TxMempoolOption sets an optional parameter on the TxMempool. -type TxMempoolOption func(*TxMempool) - // TxMempool defines a prioritized mempool data structure used by the v1 mempool // reactor. It keeps a thread-safe priority queue of transactions that is used // when a block proposer constructs a block and a thread-safe linked-list that // is used to gossip transactions to peers in a FIFO manner. type TxMempool struct { - metrics *Metrics - config *config.MempoolConfig - app abci.Application + metrics *Metrics + config *config.MempoolConfig + app abci.Application // txsAvailable fires once for each height when the mempool is not empty txsAvailable chan struct{} @@ -113,8 +110,8 @@ type TxMempool struct { // from the mempool. A read-lock is implicitly acquired when executing CheckTx, // however, a caller must explicitly grab a write-lock via Lock when updating // the mempool via Update(). - mtx sync.RWMutex - txStateFetcher utils.Option[TxStateFetcher] + mtx sync.RWMutex + txConstraintsFetcher TxConstraintsFetcher priorityReservoir *reservoir.Sampler[int64] } @@ -122,23 +119,25 @@ type TxMempool struct { func NewTxMempool( cfg *config.MempoolConfig, app abci.Application, - options ...TxMempoolOption, + metrics *Metrics, + txConstraintsFetcher TxConstraintsFetcher, ) *TxMempool { txmp := &TxMempool{ - config: cfg, - app: app, - txsAvailable: make(chan struct{}, 1), - height: -1, - cache: NopTxCache{}, - blockFailedTxs: NopTxCache{}, - metrics: NopMetrics(), - txStore: NewTxStore(), - gossipIndex: clist.New(), - priorityIndex: NewTxPriorityQueue(), - expirationIndex: NewWrappedTxList(), - pendingTxs: NewPendingTxs(cfg), - priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG + config: cfg, + app: app, + txsAvailable: make(chan struct{}, 1), + height: -1, + cache: NopTxCache{}, + blockFailedTxs: NopTxCache{}, + metrics: metrics, + txStore: NewTxStore(), + gossipIndex: clist.New(), + priorityIndex: NewTxPriorityQueue(), + expirationIndex: NewWrappedTxList(), + pendingTxs: NewPendingTxs(cfg), + txConstraintsFetcher: txConstraintsFetcher, + priorityReservoir: reservoir.New[int64](cfg.DropPriorityReservoirSize, cfg.DropPriorityThreshold, nil), // Use non-deterministic RNG } if cfg.CacheSize > 0 { @@ -146,10 +145,6 @@ func NewTxMempool( txmp.blockFailedTxs = NewLRUTxCache(cfg.CacheSize, maxCacheKeySize) } - for _, opt := range options { - opt(txmp) - } - if cfg.DuplicateTxsCacheSize > 0 { txmp.duplicateTxsCache = utils.Some(NewDuplicateTxCache(cfg.DuplicateTxsCacheSize, 1*time.Minute, maxCacheKeySize)) } @@ -157,16 +152,6 @@ func NewTxMempool( return txmp } -// WithTxStateFetcher sets the source of consensus-derived mempool limits. -func WithTxStateFetcher(fetcher TxStateFetcher) TxMempoolOption { - return func(txmp *TxMempool) { txmp.txStateFetcher = utils.Some(fetcher) } -} - -// WithMetrics sets the mempool's metrics collector. -func WithMetrics(metrics *Metrics) TxMempoolOption { - return func(txmp *TxMempool) { txmp.metrics = metrics } -} - func (txmp *TxMempool) TxStore() *TxStore { return txmp.txStore } // Lock obtains a write-lock on the mempool. A caller must be sure to explicitly @@ -229,12 +214,7 @@ func (txmp *TxMempool) TxsAvailable() <-chan struct{} { } func (txmp *TxMempool) checkTxState(tx types.Tx) error { - fetcher, ok := txmp.txStateFetcher.Get() - if !ok { - return nil - } - - constraints, err := fetcher() + constraints, err := txmp.txConstraintsFetcher() if err != nil { return err } @@ -247,13 +227,8 @@ func (txmp *TxMempool) checkTxState(tx types.Tx) error { return nil } -func (txmp *TxMempool) checkResponseState(tx types.Tx, res *abci.ResponseCheckTx) error { - fetcher, ok := txmp.txStateFetcher.Get() - if !ok { - return nil - } - - constraints, err := fetcher() +func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { + constraints, err := txmp.txConstraintsFetcher() if err != nil { return err } @@ -636,14 +611,14 @@ func (txmp *TxMempool) Update( blockHeight int64, blockTxs types.Txs, execTxResult []*abci.ExecTxResult, - newTxStateFetcher utils.Option[TxStateFetcher], + newTxConstraintsFetcher utils.Option[TxConstraintsFetcher], recheck bool, ) error { txmp.height = blockHeight txmp.notifiedTxsAvailable = false - if fetcher, ok := newTxStateFetcher.Get(); ok { - txmp.txStateFetcher = utils.Some(fetcher) + if fetcher, ok := newTxConstraintsFetcher.Get(); ok { + txmp.txConstraintsFetcher = fetcher } for i, tx := range blockTxs { @@ -722,7 +697,7 @@ func (txmp *TxMempool) Update( // NOTE: // - An explicit lock is NOT required. func (txmp *TxMempool) addNewTransaction(wtx *WrappedTx, res *abci.ResponseCheckTx, txInfo TxInfo) error { - err := txmp.checkResponseState(wtx.tx, res) + err := txmp.checkResponseState(res) if err != nil || res.Code != abci.CodeTypeOK { // ignore bad transactions @@ -868,7 +843,7 @@ func (txmp *TxMempool) handleRecheckResult(tx types.Tx, res *abci.ResponseCheckT // 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(tx, res.ResponseCheckTx) + err := txmp.checkResponseState(res.ResponseCheckTx) // we will treat a transaction that turns pending in a recheck as invalid and evict it if res.Code == abci.CodeTypeOK && err == nil && !res.IsPendingTransaction { diff --git a/sei-tendermint/internal/mempool/mempool_bench_test.go b/sei-tendermint/internal/mempool/mempool_bench_test.go index 1fe7da8f53..3fa1fb122e 100644 --- a/sei-tendermint/internal/mempool/mempool_bench_test.go +++ b/sei-tendermint/internal/mempool/mempool_bench_test.go @@ -18,7 +18,7 @@ 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, client, 10000) + txmp := setup(b, client, 10000, NopTxConstraintsFetcher) txmp.config.Size = 5000 rng := rand.New(rand.NewSource(time.Now().UnixNano())) diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 8d529e2241..6d68da1743 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -172,7 +172,7 @@ func (app *application) GetTxPriorityHint(context.Context, *abci.RequestGetTxPri }, nil } -func setup(t testing.TB, app abci.Application, cacheSize int, options ...TxMempoolOption) *TxMempool { +func setup(t testing.TB, app abci.Application, cacheSize int, txConstraintsFetcher TxConstraintsFetcher) *TxMempool { t.Helper() cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) @@ -182,7 +182,7 @@ func setup(t testing.TB, app abci.Application, cacheSize int, options ...TxMempo t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - return NewTxMempool(cfg.Mempool, app, options...) + return NewTxMempool(cfg.Mempool, app, NopMetrics(), txConstraintsFetcher) } func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { @@ -225,7 +225,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) ensureNoTxFire := func() { timer := time.NewTimer(500 * time.Millisecond) @@ -266,7 +266,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) { // commit half the transactions and ensure we fire an event txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxStateFetcher](), true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() ensureTxFire() ensureNoTxFire() @@ -282,7 +282,7 @@ func TestTxMempool_Size(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) txs := checkTxs(ctx, t, txmp, 100, 0) require.Equal(t, len(txs), txmp.Size()) require.Equal(t, 0, txmp.PendingSize()) @@ -299,7 +299,7 @@ func TestTxMempool_Size(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxStateFetcher](), true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() require.Equal(t, len(rawTxs)/2, txmp.Size()) @@ -311,7 +311,7 @@ func TestTxMempool_Flush(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) txs := checkTxs(ctx, t, txmp, 100, 0) require.Equal(t, len(txs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -327,7 +327,7 @@ func TestTxMempool_Flush(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxStateFetcher](), true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() txmp.Flush() @@ -341,7 +341,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { gasEstimated := int64(1) // gas estimated set to 1 client := &application{Application: kvstore.NewApplication(), gasEstimated: &gasEstimated} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) tTxs := checkTxs(ctx, t, txmp, 100, 0) // all txs request 1 gas unit require.Equal(t, len(tTxs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -432,7 +432,7 @@ 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, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) tTxs := checkTxs(ctx, t, txmp, 100, 0) txMap := make(map[types.TxKey]testTx) @@ -475,7 +475,7 @@ func TestTxMempool_ReapMaxTxs(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) tTxs := checkTxs(ctx, t, txmp, 100, 0) require.Equal(t, len(tTxs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -547,7 +547,7 @@ func TestTxMempool_ReapMaxBytesMaxGas_MinGasEVMTxThreshold(t *testing.T) { gasWanted := int64(50000) client := &application{Application: kvstore.NewApplication(), gasEstimated: &gasEstimated, gasWanted: &gasWanted} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) peerID := uint16(1) address := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" @@ -568,7 +568,7 @@ func TestTxMempool_CheckTxExceedsMaxSize(t *testing.T) { ctx := t.Context() client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) rng := rand.New(rand.NewSource(time.Now().UnixNano())) tx := make([]byte, txmp.config.MaxTxBytes+1) @@ -590,7 +590,7 @@ func TestTxMempool_Reap_SkipGasUnfitAndCollectMinTxs(t *testing.T) { app := &application{Application: kvstore.NewApplication()} client := app - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) peerID := uint16(1) // Insert one high-priority tx that is unfit by gas (exceeds maxGasEstimated) @@ -627,7 +627,7 @@ func TestTxMempool_Reap_SkipGasUnfitStopsAtMinEvenWithCapacity(t *testing.T) { app := &application{Application: kvstore.NewApplication()} client := app - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) peerID := uint16(1) // First tx: unfit by gas (bigger than limit), highest priority @@ -658,7 +658,7 @@ func TestTxMempool_Prioritization(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 100) + txmp := setup(t, client, 100, NopTxConstraintsFetcher) peerID := uint16(1) address1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" @@ -722,7 +722,7 @@ func TestTxMempool_PendingStoreSize(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 100) + txmp := setup(t, client, 100, NopTxConstraintsFetcher) txmp.config.PendingSize = 1 peerID := uint16(1) @@ -739,7 +739,7 @@ func TestTxMempool_RemoveCacheWhenPendingTxIsFull(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 10) + txmp := setup(t, client, 10, NopTxConstraintsFetcher) txmp.config.PendingSize = 1 peerID := uint16(1) address1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" @@ -756,7 +756,7 @@ func TestTxMempool_EVMEviction(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 100) + txmp := setup(t, client, 100, NopTxConstraintsFetcher) txmp.config.Size = 1 peerID := uint16(1) @@ -812,7 +812,7 @@ func TestTxMempool_CheckTxSamePeer(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 100) + txmp := setup(t, client, 100, NopTxConstraintsFetcher) peerID := uint16(1) rng := rand.New(rand.NewSource(time.Now().UnixNano())) @@ -831,7 +831,7 @@ func TestTxMempool_CheckTxSameSender(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 100) + txmp := setup(t, client, 100, NopTxConstraintsFetcher) peerID := uint16(1) rng := rand.New(rand.NewSource(time.Now().UnixNano())) @@ -857,7 +857,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 100) + txmp := setup(t, client, 100, NopTxConstraintsFetcher) rng := rand.New(rand.NewSource(time.Now().UnixNano())) checkTxDone := make(chan struct{}) @@ -900,7 +900,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, utils.None[TxStateFetcher](), true)) + require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() height++ @@ -925,7 +925,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 500) + txmp := setup(t, client, 500, NopTxConstraintsFetcher) txmp.height = 100 txmp.config.TTLNumBlocks = 10 @@ -941,7 +941,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, utils.None[TxStateFetcher](), true)) + require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() require.Equal(t, 95, txmp.Size()) @@ -967,14 +967,14 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, utils.None[TxStateFetcher](), true)) + require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() require.GreaterOrEqual(t, txmp.Size(), 45) require.GreaterOrEqual(t, txmp.expirationIndex.Size(), 45) } -func TestTxMempool_CheckTxStateFetcherError(t *testing.T) { +func TestTxMempool_CheckTxConstraintsFetcherError(t *testing.T) { cases := []struct { name string err error @@ -996,12 +996,12 @@ func TestTxMempool_CheckTxStateFetcherError(t *testing.T) { _, err := rng.Read(tx) require.NoError(t, err) - txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, WithTxStateFetcher(func() (TxConstraints, error) { + txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, func() (TxConstraints, error) { return TxConstraints{ MaxDataBytes: int64(len(tx) + 100), MaxGas: 1, }, tc.err - })) + }) if tc.err == nil { require.Error(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0})) @@ -1016,7 +1016,7 @@ func TestTxMempool_CheckTxStateFetcherError(t *testing.T) { func TestAppendCheckTxErr(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 500) + txmp := setup(t, client, 500, NopTxConstraintsFetcher) existingLogData := "existing error log" newLogData := "sample error log" @@ -1037,7 +1037,7 @@ func TestMempoolExpiration(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) txmp.config.TTLDuration = time.Nanosecond // we want tx to expire immediately txmp.config.RemoveExpiredTxsFromQueue = true txs := checkTxs(ctx, t, txmp, 100, 0) @@ -1058,7 +1058,7 @@ func TestReapMaxBytesMaxGas_EVMFirst(t *testing.T) { client := &application{Application: kvstore.NewApplication()} - txmp := setup(t, client, 0) + txmp := setup(t, client, 0, NopTxConstraintsFetcher) peerID := uint16(1) evmAddress1 := "0xeD23B3A9DE15e92B9ef9540E587B3661E15A12fA" @@ -1121,7 +1121,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { defer cancel() app := &application{Application: kvstore.NewApplication()} - txmp := setup(t, app, 500) + txmp := setup(t, app, 500, NopTxConstraintsFetcher) tx := types.Tx("sender-0-0=key=1000") @@ -1133,7 +1133,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, // out of gas - }, utils.None[TxStateFetcher](), true)) + }, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() // Tx should be removed from the mempool @@ -1147,7 +1147,7 @@ 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 - }, utils.None[TxStateFetcher](), true)) + }, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() require.Equal(t, 0, txmp.Size()) @@ -1168,7 +1168,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { defer cancel() app := &application{Application: kvstore.NewApplication()} - txmp := setup(t, app, 500) + txmp := setup(t, app, 500, NopTxConstraintsFetcher) tx := types.Tx("sender-0-0=key=1000") txKey := tx.Key() @@ -1178,7 +1178,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, utils.None[TxStateFetcher](), true)) + }, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() // Re-enter the mempool (first failure allows retry) @@ -1188,7 +1188,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{ {Code: abci.CodeTypeOK}, - }, utils.None[TxStateFetcher](), true)) + }, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() // Success clears the failure tracker. Simulate LRU eviction of the @@ -1202,7 +1202,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 3, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, utils.None[TxStateFetcher](), true)) + }, utils.None[TxConstraintsFetcher](), true)) txmp.Unlock() // First-failure grace should be restored: tx allowed to re-enter diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 0cac794628..4e6e58335b 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -35,10 +35,6 @@ type Reactor struct { router *p2p.Router - // observePanic is a function for observing panics that were recovered in methods on - // Reactor. observePanic is called with the recovered value. - observePanic func(any) - mtx sync.Mutex peerRoutines map[types.NodeID]context.CancelFunc failedCheckTxCounts utils.Mutex[map[types.NodeID]int] @@ -61,7 +57,6 @@ func NewReactor(txmp *TxMempool, router *p2p.Router) (*Reactor, error) { channel: channel, peerRoutines: map[types.NodeID]context.CancelFunc{}, failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), - observePanic: defaultObservePanic, readyToStart: make(chan struct{}, 1), } r.BaseService = *service.NewBaseService("Mempool", r) @@ -70,8 +65,6 @@ func NewReactor(txmp *TxMempool, router *p2p.Router) (*Reactor, error) { func (r *Reactor) MarkReadyToStart() { r.readyToStart <- struct{}{} } -func defaultObservePanic(r any) {} - // getChannelDescriptor produces an instance of a descriptor for this // package's required channels. func GetChannelDescriptor(cfg *config.MempoolConfig) p2p.ChannelDescriptor[*pb.Message] { @@ -190,7 +183,6 @@ func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { func (r *Reactor) handleMessage(ctx context.Context, m p2p.RecvMsg[*pb.Message]) (err error) { defer func() { if e := recover(); e != nil { - r.observePanic(e) err = fmt.Errorf("panic in processing message: %v", e) logger.Error( "recovering from processing message panic", @@ -302,7 +294,6 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { r.mtx.Unlock() if e := recover(); e != nil { - r.observePanic(e) logger.Error( "recovering from broadcasting mempool loop", "err", e, diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index f54a0adfbe..6327bad04c 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -53,7 +53,7 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest rts.kvstores[nodeID] = kvstore.NewApplication() app := rts.kvstores[nodeID] - mempool := setup(t, app, 0) + mempool := setup(t, app, 0, NopTxConstraintsFetcher) rts.mempools[nodeID] = mempool reactor, err := NewReactor(mempool, node.Router) @@ -78,7 +78,7 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest return rts } -func setupReactorForTest(t *testing.T, options ...TxMempoolOption) (*Reactor, *TxMempool) { +func setupReactorForTest(t *testing.T, txConstraintsFetcher TxConstraintsFetcher) (*Reactor, *TxMempool) { t.Helper() cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) @@ -90,7 +90,7 @@ func setupReactorForTest(t *testing.T, options ...TxMempoolOption) (*Reactor, *T network := p2p.MakeTestNetwork(t, p2p.TestNetworkOptions{NumNodes: 1}) node := network.Nodes()[0] - txmp := NewTxMempool(cfg.Mempool, kvstore.NewApplication(), options...) + txmp := NewTxMempool(cfg.Mempool, kvstore.NewApplication(), NopMetrics(), txConstraintsFetcher) reactor, err := NewReactor(txmp, node.Router) require.NoError(t, err) reactor.MarkReadyToStart() @@ -133,49 +133,6 @@ func (rts *reactorTestSuite) waitForTxns(t *testing.T, txs []types.Tx, ids ...ty wg.Wait() } -func TestReactorBroadcastDoesNotPanic(t *testing.T) { - ctx := t.Context() - - const numNodes = 2 - - rts := setupReactors(ctx, t, numNodes) - t.Cleanup(leaktest.Check(t)) - - observePanic := func(r any) { - t.Fatal("panic detected in reactor") - } - - primary := rts.nodes[0] - secondary := rts.nodes[1] - primaryReactor := rts.reactors[primary] - primaryMempool := primaryReactor.mempool - secondaryReactor := rts.reactors[secondary] - - primaryReactor.observePanic = observePanic - secondaryReactor.observePanic = observePanic - - firstTx := &WrappedTx{} - primaryMempool.insertTx(firstTx) - - // run the router - rts.start(t) - - go primaryReactor.broadcastTxRoutine(ctx, secondary) - - wg := &sync.WaitGroup{} - for range 50 { - next := &WrappedTx{} - wg.Add(1) - go func() { - defer wg.Done() - primaryMempool.insertTx(next) - }() - } - - primaryReactor.Stop() - wg.Wait() -} - func TestReactorBroadcastTxs(t *testing.T) { numTxs := 512 numNodes := 4 @@ -222,12 +179,12 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { receiverReactor := rts.reactors[receiver] receiverReactor.cfg.CheckTxErrorBlacklistEnabled = true receiverReactor.cfg.CheckTxErrorThreshold = 2 - receiverReactor.mempool.txStateFetcher = utils.Some(TxStateFetcher(func() (TxConstraints, error) { + receiverReactor.mempool.txConstraintsFetcher = TxConstraintsFetcher(func() (TxConstraints, error) { return TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil - })) + }) conn := rts.network.Node(receiver).WaitForConnAndGet(ctx, sender) msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { @@ -262,12 +219,12 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { reactor, _ := setupReactorForTest( t, - WithTxStateFetcher(func() (TxConstraints, error) { + func() (TxConstraints, error) { return TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil - }), + }, ) for counts := range reactor.failedCheckTxCounts.Lock() { counts["other"] = 1 @@ -303,12 +260,12 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { reactor, _ := setupReactorForTest( t, - WithTxStateFetcher(func() (TxConstraints, error) { + func() (TxConstraints, error) { return TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil - }), + }, ) msg := p2p.RecvMsg[*pb.Message]{ From: "sender", @@ -370,7 +327,7 @@ func TestReactorConcurrency(t *testing.T) { deliverTxResponses[i] = &abci.ExecTxResult{Code: 0} } - require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, utils.None[TxStateFetcher](), true)) + require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, utils.None[TxConstraintsFetcher](), true)) }() // 1. submit a bunch of txs @@ -384,7 +341,7 @@ func TestReactorConcurrency(t *testing.T) { mempool.Lock() defer mempool.Unlock() - err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), utils.None[TxStateFetcher](), true) + err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), utils.None[TxConstraintsFetcher](), true) require.NoError(t, err) }() } diff --git a/sei-tendermint/internal/mempool/types.go b/sei-tendermint/internal/mempool/types.go index a49dbee44a..c0b67ec78d 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -26,6 +26,13 @@ type TxConstraints struct { MaxGas int64 } -// TxStateFetcher returns the precomputed consensus-derived mempool limits for the current +// TxConstraintsFetcher returns the precomputed consensus-derived mempool limits for the current // state snapshot. -type TxStateFetcher func() (TxConstraints, error) +type TxConstraintsFetcher func() (TxConstraints, error) + +func NopTxConstraintsFetcher() (TxConstraints, error) { + return TxConstraints{ + MaxDataBytes: math.MaxInt64, + MaxGas: -1, + }, nil +} diff --git a/sei-tendermint/internal/state/execution.go b/sei-tendermint/internal/state/execution.go index 73611b1dbd..c83923b48b 100644 --- a/sei-tendermint/internal/state/execution.go +++ b/sei-tendermint/internal/state/execution.go @@ -428,7 +428,7 @@ func (blockExec *BlockExecutor) Commit( block.Height, block.Txs, txResults, - utils.Some(TxStateFetcherForState(state)), + utils.Some(TxConstraintsFetcherForState(state)), state.ConsensusParams.ABCI.RecheckTx, ) blockExec.metrics.UpdateMempoolTime.Observe(float64(time.Since(start))) diff --git a/sei-tendermint/internal/state/helpers_test.go b/sei-tendermint/internal/state/helpers_test.go index a958268e04..6ddc412218 100644 --- a/sei-tendermint/internal/state/helpers_test.go +++ b/sei-tendermint/internal/state/helpers_test.go @@ -236,7 +236,7 @@ func makeTxMempool(t testing.TB, app abci.Application) *mempool.TxMempool { cfg := config.TestMempoolConfig() - return mempool.NewTxMempool(cfg, app) + return mempool.NewTxMempool(cfg, app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) } // used for testing by state store diff --git a/sei-tendermint/internal/state/tx_filter.go b/sei-tendermint/internal/state/tx_filter.go index c82a590422..3ee20dc02e 100644 --- a/sei-tendermint/internal/state/tx_filter.go +++ b/sei-tendermint/internal/state/tx_filter.go @@ -37,9 +37,9 @@ func cachingStateFetcher(store Store) func() (State, error) { } -// TxStateFetcherFromStore returns the precomputed consensus-derived mempool limits for the +// TxConstraintsFetcherFromStore returns the precomputed consensus-derived mempool limits for the // current persisted state. -func TxStateFetcherFromStore(store Store) mempool.TxStateFetcher { +func TxConstraintsFetcherFromStore(store Store) mempool.TxConstraintsFetcher { fetch := cachingStateFetcher(store) return func() (mempool.TxConstraints, error) { @@ -48,11 +48,11 @@ func TxStateFetcherFromStore(store Store) mempool.TxStateFetcher { return mempool.TxConstraints{}, err } - return TxStateFetcherForState(state)() + return TxConstraintsFetcherForState(state)() } } -func TxStateFetcherForState(state State) mempool.TxStateFetcher { +func TxConstraintsFetcherForState(state State) mempool.TxConstraintsFetcher { return func() (mempool.TxConstraints, error) { return mempool.TxConstraints{ MaxDataBytes: types.MaxDataBytesNoEvidence( diff --git a/sei-tendermint/internal/state/tx_filter_test.go b/sei-tendermint/internal/state/tx_filter_test.go index 105f3b23df..653de9a687 100644 --- a/sei-tendermint/internal/state/tx_filter_test.go +++ b/sei-tendermint/internal/state/tx_filter_test.go @@ -31,7 +31,7 @@ func TestTxFilter(t *testing.T) { state, err := sm.MakeGenesisState(genDoc) require.NoError(t, err) - f := sm.TxStateFetcherForState(state) + f := sm.TxConstraintsFetcherForState(state) constraints, err := f() require.NoError(t, err) txSize := types.ComputeProtoSizeForTxs([]types.Tx{tc.tx}) diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 1aaf38daee..4453496271 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -189,12 +189,7 @@ func makeNode( fmt.Errorf("failed to create router: %w", err), makeCloser(closers)) } - mp := mempool.NewTxMempool( - cfg.Mempool, - app, - mempool.WithMetrics(nodeMetrics.mempool), - mempool.WithTxStateFetcher(sm.TxStateFetcherFromStore(stateStore)), - ) + mp := mempool.NewTxMempool(cfg.Mempool, app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) mpReactor, err := mempool.NewReactor(mp, router) if err != nil { return nil, fmt.Errorf("mempool.NewReactor(): %w", err) diff --git a/sei-tendermint/node/node_test.go b/sei-tendermint/node/node_test.go index 928fb32cd4..4d2835b864 100644 --- a/sei-tendermint/node/node_test.go +++ b/sei-tendermint/node/node_test.go @@ -287,6 +287,8 @@ func TestCreateProposalBlock(t *testing.T) { mp := mempool.NewTxMempool( cfg.Mempool, app, + mempool.NopMetrics(), + mempool.NopTxConstraintsFetcher, ) // Make EvidencePool @@ -382,6 +384,8 @@ func TestMaxTxsProposalBlockSize(t *testing.T) { mp := mempool.NewTxMempool( cfg.Mempool, app, + mempool.NopMetrics(), + mempool.NopTxConstraintsFetcher, ) // fill the mempool with one txs just below the maximum size @@ -445,6 +449,8 @@ func TestMaxProposalBlockSize(t *testing.T) { mp := mempool.NewTxMempool( cfg.Mempool, app, + mempool.NopMetrics(), + mempool.NopTxConstraintsFetcher, ) // fill the mempool with one txs just below the maximum size diff --git a/sei-tendermint/test/fuzz/tests/mempool_test.go b/sei-tendermint/test/fuzz/tests/mempool_test.go index e203a890e1..5a7b351e88 100644 --- a/sei-tendermint/test/fuzz/tests/mempool_test.go +++ b/sei-tendermint/test/fuzz/tests/mempool_test.go @@ -16,7 +16,7 @@ func FuzzMempool(f *testing.F) { cfg := config.DefaultMempoolConfig() cfg.Broadcast = false - mp := mempool.NewTxMempool(cfg, app) + mp := mempool.NewTxMempool(cfg, app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) f.Fuzz(func(t *testing.T, data []byte) { _ = mp.CheckTx(t.Context(), data, nil, mempool.TxInfo{}) From 4e0faa96a84eda620b6e42ef063d72ef32ca3a72 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 18:33:06 +0200 Subject: [PATCH 14/43] addressed a flake --- sei-tendermint/internal/mempool/reactor_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index d34d47b04f..006a30e711 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -245,7 +245,11 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { }, } } - require.Equal(t, utils.Some(0), peerFailedCheckTxCount(receiverReactor, sender)) + // The network connection can be established before the reactor processes the + // async PeerStatusUp update that initializes the sender's failure count, so we need to wait. + require.Eventually(t, func() bool { + return peerFailedCheckTxCount(receiverReactor, sender) == utils.Some(0) + }, time.Second, 50*time.Millisecond) require.NoError(t, receiverReactor.handleMempoolMessage(ctx, msgForTx([]byte("good-1")))) require.Equal(t, utils.Some(0), peerFailedCheckTxCount(receiverReactor, sender)) From d2b6a2185da8b8ec303275f5f08be57bef24edd6 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Wed, 8 Apr 2026 18:42:58 +0200 Subject: [PATCH 15/43] wip --- sei-tendermint/internal/mempool/mempool.go | 7 ++---- .../internal/mempool/mempool_test.go | 23 +++++++++---------- .../internal/mempool/reactor_test.go | 4 ++-- sei-tendermint/internal/state/execution.go | 3 +-- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index b9ef36fe19..2086afc0a8 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -611,15 +611,12 @@ func (txmp *TxMempool) Update( blockHeight int64, blockTxs types.Txs, execTxResult []*abci.ExecTxResult, - newTxConstraintsFetcher utils.Option[TxConstraintsFetcher], + txConstraintsFetcher TxConstraintsFetcher, recheck bool, ) error { txmp.height = blockHeight txmp.notifiedTxsAvailable = false - - if fetcher, ok := newTxConstraintsFetcher.Get(); ok { - txmp.txConstraintsFetcher = fetcher - } + txmp.txConstraintsFetcher = txConstraintsFetcher for i, tx := range blockTxs { txKey := tx.Key() diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 6d68da1743..c24a472147 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -20,7 +20,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" - "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -266,7 +265,7 @@ func TestTxMempool_TxsAvailable(t *testing.T) { // commit half the transactions and ensure we fire an event txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, txmp.txConstraintsFetcher, true)) txmp.Unlock() ensureTxFire() ensureNoTxFire() @@ -299,7 +298,7 @@ func TestTxMempool_Size(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, txmp.txConstraintsFetcher, true)) txmp.Unlock() require.Equal(t, len(rawTxs)/2, txmp.Size()) @@ -327,7 +326,7 @@ func TestTxMempool_Flush(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, txmp.Update(ctx, 1, rawTxs[:50], responses, txmp.txConstraintsFetcher, true)) txmp.Unlock() txmp.Flush() @@ -900,7 +899,7 @@ func TestTxMempool_ConcurrentTxs(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, txmp.Update(ctx, height, reapedTxs, responses, txmp.txConstraintsFetcher, true)) txmp.Unlock() height++ @@ -941,7 +940,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, txmp.Update(ctx, txmp.height+1, reapedTxs, responses, txmp.txConstraintsFetcher, true)) txmp.Unlock() require.Equal(t, 95, txmp.Size()) @@ -967,7 +966,7 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { } txmp.Lock() - require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, txmp.Update(ctx, txmp.height+10, reapedTxs, responses, txmp.txConstraintsFetcher, true)) txmp.Unlock() require.GreaterOrEqual(t, txmp.Size(), 45) @@ -1133,7 +1132,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, // out of gas - }, utils.None[TxConstraintsFetcher](), true)) + }, txmp.txConstraintsFetcher, true)) txmp.Unlock() // Tx should be removed from the mempool @@ -1147,7 +1146,7 @@ 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 - }, utils.None[TxConstraintsFetcher](), true)) + }, txmp.txConstraintsFetcher, true)) txmp.Unlock() require.Equal(t, 0, txmp.Size()) @@ -1178,7 +1177,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, utils.None[TxConstraintsFetcher](), true)) + }, txmp.txConstraintsFetcher, true)) txmp.Unlock() // Re-enter the mempool (first failure allows retry) @@ -1188,7 +1187,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{ {Code: abci.CodeTypeOK}, - }, utils.None[TxConstraintsFetcher](), true)) + }, txmp.txConstraintsFetcher, true)) txmp.Unlock() // Success clears the failure tracker. Simulate LRU eviction of the @@ -1202,7 +1201,7 @@ func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) { txmp.Lock() require.NoError(t, txmp.Update(ctx, 3, types.Txs{tx}, []*abci.ExecTxResult{ {Code: 11}, - }, utils.None[TxConstraintsFetcher](), true)) + }, txmp.txConstraintsFetcher, true)) txmp.Unlock() // First-failure grace should be restored: tx allowed to re-enter diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor_test.go index 48490d0a5d..4b8b12b141 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor_test.go @@ -331,7 +331,7 @@ func TestReactorConcurrency(t *testing.T) { deliverTxResponses[i] = &abci.ExecTxResult{Code: 0} } - require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, utils.None[TxConstraintsFetcher](), true)) + require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, mempool.txConstraintsFetcher, true)) }() // 1. submit a bunch of txs @@ -345,7 +345,7 @@ func TestReactorConcurrency(t *testing.T) { mempool.Lock() defer mempool.Unlock() - err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), utils.None[TxConstraintsFetcher](), true) + err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), mempool.txConstraintsFetcher, true) require.NoError(t, err) }() } diff --git a/sei-tendermint/internal/state/execution.go b/sei-tendermint/internal/state/execution.go index c83923b48b..f73c0c341d 100644 --- a/sei-tendermint/internal/state/execution.go +++ b/sei-tendermint/internal/state/execution.go @@ -12,7 +12,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle" "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/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" @@ -428,7 +427,7 @@ func (blockExec *BlockExecutor) Commit( block.Height, block.Txs, txResults, - utils.Some(TxConstraintsFetcherForState(state)), + TxConstraintsFetcherForState(state), state.ConsensusParams.ABCI.RecheckTx, ) blockExec.metrics.UpdateMempoolTime.Observe(float64(time.Since(start))) From 4a3a57998eaef122fba67bc99c32774ccbb78306 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 14:21:49 +0200 Subject: [PATCH 16/43] snapshot --- sei-tendermint/internal/mempool/mempool.go | 39 ++++++----- .../internal/mempool/mempool_test.go | 41 +++++++----- sei-tendermint/internal/mempool/reactor.go | 4 +- sei-tendermint/types/mempool.go | 67 ------------------- 4 files changed, 47 insertions(+), 104 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 2086afc0a8..9906a17cd8 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -237,10 +237,10 @@ func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { return nil } if res.GasWanted < 0 { - return fmt.Errorf("gas wanted %d is negative", res.GasWanted) + return ErrNegativeGasWanted{fmt.Errorf("gas wanted %d is negative", res.GasWanted)} } if res.GasWanted > constraints.MaxGas { - return fmt.Errorf("gas wanted %d is greater than max gas %d", res.GasWanted, constraints.MaxGas) + return ErrGasWantedTooHigh{fmt.Errorf("gas wanted %d is greater than max gas %d", res.GasWanted, constraints.MaxGas)} } return nil @@ -277,10 +277,7 @@ func (txmp *TxMempool) CheckTx( defer txmp.mtx.RUnlock() if txSize := len(tx); txSize > txmp.config.MaxTxBytes { - return types.ErrTxTooLarge{ - Max: txmp.config.MaxTxBytes, - Actual: txSize, - } + return fmt.Errorf("%w: max size is %d, but got %d", ErrTxTooLarge, txmp.config.MaxTxBytes, txSize) } // Reject low priority transactions when the mempool is more than @@ -304,7 +301,7 @@ func (txmp *TxMempool) CheckTx( } if err := txmp.checkTxState(tx); err != nil { - return types.ErrPreCheck{Reason: err} + return ErrPreCheck{err} } txHash := tx.Key() @@ -314,7 +311,7 @@ func (txmp *TxMempool) CheckTx( // check if we've seen this transaction and error if we have. if !txmp.cache.Push(txHash) { txmp.txStore.GetOrSetPeerByTxHash(txHash, txInfo.SenderID) - return types.ErrTxInCache + return ErrTxInCache } txmp.metrics.CacheSize.Set(float64(txmp.cache.Size())) @@ -932,12 +929,13 @@ func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { ) if numTxs >= txmp.config.Size || int64(wtx.Size())+sizeBytes > txmp.config.MaxTxsBytes { - return types.ErrMempoolIsFull{ - NumTxs: numTxs, - MaxTxs: txmp.config.Size, - TxsBytes: sizeBytes, - MaxTxsBytes: txmp.config.MaxTxsBytes, - } + return ErrMempoolIsFull{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 @@ -950,12 +948,13 @@ func (txmp *TxMempool) canAddPendingTx(wtx *WrappedTx) error { ) if numTxs >= txmp.config.PendingSize || int64(wtx.Size())+sizeBytes > txmp.config.MaxPendingTxsBytes { - return types.ErrMempoolPendingIsFull{ - NumTxs: numTxs, - MaxTxs: txmp.config.PendingSize, - TxsBytes: sizeBytes, - MaxTxsBytes: txmp.config.MaxPendingTxsBytes, - } + return ErrMempoolPendingIsFull{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 diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index c24a472147..fd7ccf458a 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -975,16 +975,27 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { func TestTxMempool_CheckTxConstraintsFetcherError(t *testing.T) { cases := []struct { - name string - err error + name string + fetcherErr error + maxDataBytes int64 + maxGas int64 + assertErr func(*testing.T, error) }{ { - name: "error", - err: errors.New("test error"), + name: "fetcher error", + fetcherErr: errors.New("test error"), + maxGas: 1, }, { - name: "no error", - err: nil, + name: "post-check constraint violation", + maxDataBytes: int64(config.TestMempoolConfig().MaxTxBytes + 100), + maxGas: 0, + assertErr: func(t *testing.T, err error) { + t.Helper() + var gasErr ErrGasWantedTooHigh + require.ErrorAs(t, err, &gasErr) + require.EqualError(t, gasErr, "gas wanted 1 is greater than max gas 0") + }, }, } for _, tc := range cases { @@ -997,18 +1008,18 @@ func TestTxMempool_CheckTxConstraintsFetcherError(t *testing.T) { txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, func() (TxConstraints, error) { return TxConstraints{ - MaxDataBytes: int64(len(tx) + 100), - MaxGas: 1, - }, tc.err + MaxDataBytes: tc.maxDataBytes, + MaxGas: tc.maxGas, + }, tc.fetcherErr }) - if tc.err == nil { - require.Error(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0})) + err = txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) + if tc.fetcherErr != nil { + require.True(t, IsPreCheckError(err)) + require.ErrorIs(t, err, tc.fetcherErr) return } - - err = txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) - require.ErrorIs(t, err, tc.err) + tc.assertErr(t, err) }) } } @@ -1153,7 +1164,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { // Second failure: tx should remain in cache — CheckTx should reject it err := txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) - require.Equal(t, types.ErrTxInCache, err) + require.Equal(t, ErrTxInCache, err) require.Equal(t, 0, txmp.Size()) // A different tx (different hash) should still be admitted diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 4e6e58335b..7f01e9ba74 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -127,7 +127,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me for _, tx := range protoTxs { if err := r.mempool.CheckTx(ctx, tx, nil, txInfo); err != nil { r.accountFailedCheckTx(m.From, err) - if errors.Is(err, types.ErrTxInCache) { + if errors.Is(err, ErrTxInCache) { // if the tx is in the cache, // then we've been gossiped a // Tx that we've already @@ -163,7 +163,7 @@ func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { if !r.cfg.CheckTxErrorBlacklistEnabled { return } - if !utils.ErrorAs[types.ErrTxTooLarge](err).IsPresent() && !utils.ErrorAs[types.ErrPreCheck](err).IsPresent() { + if !errors.Is(err,ErrTxTooLarge) && !utils.ErrorAs[ErrPreCheck](err).IsPresent() { return } for counts := range r.failedCheckTxCounts.Lock() { diff --git a/sei-tendermint/types/mempool.go b/sei-tendermint/types/mempool.go index 30e65e5927..4bbbdd478c 100644 --- a/sei-tendermint/types/mempool.go +++ b/sei-tendermint/types/mempool.go @@ -3,15 +3,11 @@ package types import ( "crypto/sha256" "errors" - "fmt" tmbytes "github.com/sei-protocol/sei-chain/sei-tendermint/libs/bytes" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" ) -// ErrTxInCache is returned to the client if we saw tx earlier -var ErrTxInCache = errors.New("tx already exists in cache") - // TxKey is the fixed length array key used as an index. type TxKey [sha256.Size]byte @@ -55,66 +51,3 @@ func TxKeysListFromProto(dps []*tmproto.TxKey) ([]TxKey, error) { } return txKeys, nil } - -// ErrTxTooLarge defines an error when a transaction is too big to be sent in a -// message to other peers. -type ErrTxTooLarge struct { - Max int - Actual int -} - -func (e ErrTxTooLarge) Error() string { - return fmt.Sprintf("Tx too large. Max size is %d, but got %d", e.Max, e.Actual) -} - -// ErrMempoolIsFull defines an error where Tendermint and the application cannot -// handle that much load. -type ErrMempoolIsFull struct { - NumTxs int - MaxTxs int - TxsBytes int64 - MaxTxsBytes int64 -} - -func (e ErrMempoolIsFull) Error() string { - return fmt.Sprintf( - "mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", - e.NumTxs, - e.MaxTxs, - e.TxsBytes, - e.MaxTxsBytes, - ) -} - -// ErrMempoolPendingIsFull defines an error where there are too many pending transactions -// not processed yet -type ErrMempoolPendingIsFull struct { - NumTxs int - MaxTxs int - TxsBytes int64 - MaxTxsBytes int64 -} - -func (e ErrMempoolPendingIsFull) Error() string { - return fmt.Sprintf( - "mempool pending set is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", - e.NumTxs, - e.MaxTxs, - e.TxsBytes, - e.MaxTxsBytes, - ) -} - -// ErrPreCheck defines an error where a transaction fails a pre-check. -type ErrPreCheck struct { - Reason error -} - -func (e ErrPreCheck) Error() string { - return e.Reason.Error() -} - -// IsPreCheckError returns true if err is due to pre check failure. -func IsPreCheckError(err error) bool { - return errors.As(err, &ErrPreCheck{}) -} From 59b9f59d58c63d6a5f0f74460446f4d0a731209b Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 14:48:24 +0200 Subject: [PATCH 17/43] removed notion of precheck --- sei-tendermint/internal/mempool/mempool.go | 47 +++++++--------- .../internal/mempool/mempool_test.go | 53 +------------------ sei-tendermint/internal/mempool/reactor.go | 7 +-- 3 files changed, 21 insertions(+), 86 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 9906a17cd8..85a39bb730 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -213,20 +213,6 @@ func (txmp *TxMempool) TxsAvailable() <-chan struct{} { return txmp.txsAvailable } -func (txmp *TxMempool) checkTxState(tx types.Tx) error { - constraints, err := txmp.txConstraintsFetcher() - if err != nil { - return err - } - - txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}) - if txSize > constraints.MaxDataBytes { - return fmt.Errorf("tx size is too big: %d, max: %d", txSize, constraints.MaxDataBytes) - } - - return nil -} - func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { constraints, err := txmp.txConstraintsFetcher() if err != nil { @@ -237,10 +223,10 @@ func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { return nil } if res.GasWanted < 0 { - return ErrNegativeGasWanted{fmt.Errorf("gas wanted %d is negative", res.GasWanted)} + return fmt.Errorf("%w: %d", errNegativeGasWanted, res.GasWanted) } if res.GasWanted > constraints.MaxGas { - return ErrGasWantedTooHigh{fmt.Errorf("gas wanted %d is greater than max gas %d", res.GasWanted, constraints.MaxGas)} + return fmt.Errorf("%w: gas wanted %d is greater than max gas %d", errGasWantedTooHigh, res.GasWanted, constraints.MaxGas) } return nil @@ -276,8 +262,16 @@ func (txmp *TxMempool) CheckTx( txmp.mtx.RLock() defer txmp.mtx.RUnlock() + if txSize := len(tx); txSize > txmp.config.MaxTxBytes { - return fmt.Errorf("%w: max size is %d, but got %d", ErrTxTooLarge, txmp.config.MaxTxBytes, txSize) + return fmt.Errorf("%w: max size is %d, but got %d", errTxTooLarge, txmp.config.MaxTxBytes, txSize) + } + constraints, err := txmp.txConstraintsFetcher() + if err != nil { + return fmt.Errorf("txmp.txConstraintsFetcher(): %w",err) + } + if txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}); txSize > constraints.MaxDataBytes { + return fmt.Errorf("%w: tx size is too big: %d, max: %d", errTxTooLarge, txSize, constraints.MaxDataBytes) } // Reject low priority transactions when the mempool is more than @@ -299,11 +293,6 @@ func (txmp *TxMempool) CheckTx( return errors.New("priority not high enough for mempool") } } - - if err := txmp.checkTxState(tx); err != nil { - return ErrPreCheck{err} - } - txHash := tx.Key() // We add the transaction to the mempool's cache and if the @@ -311,7 +300,7 @@ func (txmp *TxMempool) CheckTx( // check if we've seen this transaction and error if we have. if !txmp.cache.Push(txHash) { txmp.txStore.GetOrSetPeerByTxHash(txHash, txInfo.SenderID) - return ErrTxInCache + return errTxInCache } txmp.metrics.CacheSize.Set(float64(txmp.cache.Size())) @@ -929,13 +918,13 @@ func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { ) if numTxs >= txmp.config.Size || int64(wtx.Size())+sizeBytes > txmp.config.MaxTxsBytes { - return ErrMempoolIsFull{fmt.Errorf( - "mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", + return fmt.Errorf("%w: number of txs %d (max: %d), total txs bytes %d (max: %d)", + errMempoolIsFull, numTxs, txmp.config.Size, sizeBytes, txmp.config.MaxTxsBytes, - )} + ) } return nil @@ -948,13 +937,13 @@ func (txmp *TxMempool) canAddPendingTx(wtx *WrappedTx) error { ) if numTxs >= txmp.config.PendingSize || int64(wtx.Size())+sizeBytes > txmp.config.MaxPendingTxsBytes { - return ErrMempoolPendingIsFull{fmt.Errorf( - "mempool pending set is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", + return fmt.Errorf("%w: number of txs %d (max: %d), total txs bytes %d (max: %d)", + errMempoolPendingIsFull, numTxs, txmp.config.PendingSize, sizeBytes, txmp.config.MaxPendingTxsBytes, - )} + ) } return nil diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index fd7ccf458a..61f59dd23f 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -3,7 +3,6 @@ package mempool import ( "bytes" "context" - "errors" "fmt" "math/rand" "os" @@ -973,56 +972,6 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { require.GreaterOrEqual(t, txmp.expirationIndex.Size(), 45) } -func TestTxMempool_CheckTxConstraintsFetcherError(t *testing.T) { - cases := []struct { - name string - fetcherErr error - maxDataBytes int64 - maxGas int64 - assertErr func(*testing.T, error) - }{ - { - name: "fetcher error", - fetcherErr: errors.New("test error"), - maxGas: 1, - }, - { - name: "post-check constraint violation", - maxDataBytes: int64(config.TestMempoolConfig().MaxTxBytes + 100), - maxGas: 0, - assertErr: func(t *testing.T, err error) { - t.Helper() - var gasErr ErrGasWantedTooHigh - require.ErrorAs(t, err, &gasErr) - require.EqualError(t, gasErr, "gas wanted 1 is greater than max gas 0") - }, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - ctx := t.Context() - rng := rand.New(rand.NewSource(time.Now().UnixNano())) - tx := make([]byte, config.TestMempoolConfig().MaxTxBytes-1) - _, err := rng.Read(tx) - require.NoError(t, err) - - txmp := setup(t, &application{Application: kvstore.NewApplication()}, 0, func() (TxConstraints, error) { - return TxConstraints{ - MaxDataBytes: tc.maxDataBytes, - MaxGas: tc.maxGas, - }, tc.fetcherErr - }) - - err = txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) - if tc.fetcherErr != nil { - require.True(t, IsPreCheckError(err)) - require.ErrorIs(t, err, tc.fetcherErr) - return - } - tc.assertErr(t, err) - }) - } -} func TestAppendCheckTxErr(t *testing.T) { client := &application{Application: kvstore.NewApplication()} @@ -1164,7 +1113,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { // Second failure: tx should remain in cache — CheckTx should reject it err := txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) - require.Equal(t, ErrTxInCache, err) + require.Equal(t, errTxInCache, err) require.Equal(t, 0, txmp.Size()) // A different tx (different hash) should still be admitted diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor.go index 7f01e9ba74..764d9c5140 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor.go @@ -127,7 +127,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me for _, tx := range protoTxs { if err := r.mempool.CheckTx(ctx, tx, nil, txInfo); err != nil { r.accountFailedCheckTx(m.From, err) - if errors.Is(err, ErrTxInCache) { + if errors.Is(err, errTxInCache) { // if the tx is in the cache, // then we've been gossiped a // Tx that we've already @@ -160,10 +160,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me } func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { - if !r.cfg.CheckTxErrorBlacklistEnabled { - return - } - if !errors.Is(err,ErrTxTooLarge) && !utils.ErrorAs[ErrPreCheck](err).IsPresent() { + if !r.cfg.CheckTxErrorBlacklistEnabled || !errors.Is(err, errTxTooLarge) { return } for counts := range r.failedCheckTxCounts.Lock() { From 28a2ff1143a1acf9c5afe7bd9917b49c710ea1cf Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 14:53:10 +0200 Subject: [PATCH 18/43] simplified errors --- sei-tendermint/internal/mempool/mempool.go | 19 +++++++++++-------- .../internal/mempool/mempool_test.go | 1 - 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 85a39bb730..f127524abe 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -20,6 +20,12 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) +// errTxInCache is returned to the client if we saw tx earlier. +var errTxInCache = errors.New("tx already exists in cache") + +// errTxTooLarge defines an error when a transaction is too big to be sent to peers. +var errTxTooLarge = errors.New("tx too large") + // Using SHA-256 truncated to 128 bits as the cache key: At 2K tx/sec, the // collision probability is effectively zero (≈10^-29 for 120K keys in a minute, // still negligible over years). If reduced 3× smaller (~43 bits), collisions @@ -223,10 +229,10 @@ func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { return nil } if res.GasWanted < 0 { - return fmt.Errorf("%w: %d", errNegativeGasWanted, res.GasWanted) + return fmt.Errorf("negative gas wanted: %d", res.GasWanted) } if res.GasWanted > constraints.MaxGas { - return fmt.Errorf("%w: gas wanted %d is greater than max gas %d", errGasWantedTooHigh, res.GasWanted, constraints.MaxGas) + return fmt.Errorf("gas wanted exceeds max gas: gas wanted %d is greater than max gas %d", res.GasWanted, constraints.MaxGas) } return nil @@ -262,13 +268,12 @@ func (txmp *TxMempool) CheckTx( txmp.mtx.RLock() defer txmp.mtx.RUnlock() - if txSize := len(tx); txSize > txmp.config.MaxTxBytes { return fmt.Errorf("%w: max size is %d, but got %d", errTxTooLarge, txmp.config.MaxTxBytes, txSize) } constraints, err := txmp.txConstraintsFetcher() if err != nil { - return fmt.Errorf("txmp.txConstraintsFetcher(): %w",err) + return fmt.Errorf("txmp.txConstraintsFetcher(): %w", err) } if txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}); txSize > constraints.MaxDataBytes { return fmt.Errorf("%w: tx size is too big: %d, max: %d", errTxTooLarge, txSize, constraints.MaxDataBytes) @@ -918,8 +923,7 @@ func (txmp *TxMempool) canAddTx(wtx *WrappedTx) error { ) if numTxs >= txmp.config.Size || int64(wtx.Size())+sizeBytes > txmp.config.MaxTxsBytes { - return fmt.Errorf("%w: number of txs %d (max: %d), total txs bytes %d (max: %d)", - errMempoolIsFull, + return fmt.Errorf("mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", numTxs, txmp.config.Size, sizeBytes, @@ -937,8 +941,7 @@ func (txmp *TxMempool) canAddPendingTx(wtx *WrappedTx) error { ) if numTxs >= txmp.config.PendingSize || int64(wtx.Size())+sizeBytes > txmp.config.MaxPendingTxsBytes { - return fmt.Errorf("%w: number of txs %d (max: %d), total txs bytes %d (max: %d)", - errMempoolPendingIsFull, + 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, diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 61f59dd23f..29b4392383 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -972,7 +972,6 @@ func TestTxMempool_ExpiredTxs_NumBlocks(t *testing.T) { require.GreaterOrEqual(t, txmp.expirationIndex.Size(), 45) } - func TestAppendCheckTxErr(t *testing.T) { client := &application{Application: kvstore.NewApplication()} txmp := setup(t, client, 500, NopTxConstraintsFetcher) From 15ed85974aa2d1bbd3b862684e10f3c124d59cf3 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 14:58:38 +0200 Subject: [PATCH 19/43] compilation fix --- sei-tendermint/node/setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 9154d495fe..c1dfd05733 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -392,7 +392,7 @@ func createRouter( if !ok { return nil, closer, fmt.Errorf("autobahn non-validator nodes are not supported yet; a local validator key is required") } - gigaCfg, err := buildGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, appClient, genDoc) + gigaCfg, err := buildGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, app, genDoc) if err != nil { return nil, closer, fmt.Errorf("buildGigaConfig: %w", err) } From ba81b8ba3d779a17e210be6c73c1bb48625fa72b Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 15:17:39 +0200 Subject: [PATCH 20/43] separated reactor --- sei-tendermint/internal/mempool/mempool.go | 8 +- .../internal/mempool/{ => reactor}/ids.go | 23 +-- .../mempool/{ => reactor}/ids_test.go | 2 +- .../internal/mempool/{ => reactor}/reactor.go | 88 +++------ .../mempool/{ => reactor}/reactor_test.go | 176 ++++++++++-------- sei-tendermint/internal/mempool/types.go | 13 +- sei-tendermint/node/node.go | 5 +- sei-tendermint/node/setup.go | 4 +- 8 files changed, 153 insertions(+), 166 deletions(-) rename sei-tendermint/internal/mempool/{ => reactor}/ids.go (64%) rename sei-tendermint/internal/mempool/{ => reactor}/ids_test.go (99%) rename sei-tendermint/internal/mempool/{ => reactor}/reactor.go (71%) rename sei-tendermint/internal/mempool/{ => reactor}/reactor_test.go (78%) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index f127524abe..02edc30b04 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -158,6 +158,8 @@ func NewTxMempool( return txmp } +func (txmp *TxMempool) Config() *config.MempoolConfig { return txmp.config } + func (txmp *TxMempool) TxStore() *TxStore { return txmp.txStore } // Lock obtains a write-lock on the mempool. A caller must be sure to explicitly @@ -211,7 +213,7 @@ func (txmp *TxMempool) WaitForNextTx() <-chan struct{} { return txmp.gossipIndex // NextGossipTx returns the next valid transaction to gossip. A caller must wait // for WaitForNextTx to signal a transaction is available to gossip first. It is // thread-safe. -func (txmp *TxMempool) NextGossipTx() *clist.CElement { return txmp.gossipIndex.Front() } +func (txmp *TxMempool) NextGossipTx() *GossipTx { return newGossipTx(txmp.gossipIndex.Front()) } // TxsAvailable returns a channel which fires once for every height, and only // when transactions are available in the mempool. It is thread-safe. @@ -219,6 +221,10 @@ func (txmp *TxMempool) TxsAvailable() <-chan struct{} { return txmp.txsAvailable } +func IsTxInCacheError(err error) bool { return errors.Is(err, errTxInCache) } + +func IsTxTooLargeError(err error) bool { return errors.Is(err, errTxTooLarge) } + func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { constraints, err := txmp.txConstraintsFetcher() if err != nil { diff --git a/sei-tendermint/internal/mempool/ids.go b/sei-tendermint/internal/mempool/reactor/ids.go similarity index 64% rename from sei-tendermint/internal/mempool/ids.go rename to sei-tendermint/internal/mempool/reactor/ids.go index 24e96c77fe..829e1c5119 100644 --- a/sei-tendermint/internal/mempool/ids.go +++ b/sei-tendermint/internal/mempool/reactor/ids.go @@ -1,37 +1,36 @@ -package mempool +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 + nextID uint16 + activeIDs map[uint16]struct{} } func NewMempoolIDs() *IDs { return &IDs{ - peerMap: make(map[types.NodeID]uint16), - - // reserve UnknownPeerID for mempoolReactor.BroadcastTx - activeIDs: map[uint16]struct{}{UnknownPeerID: {}}, + peerMap: make(map[types.NodeID]uint16), + 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 } @@ -40,7 +39,6 @@ func (ids *IDs) ReserveForPeer(peerID types.NodeID) { 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() @@ -55,7 +53,6 @@ func (ids *IDs) Reclaim(peerID types.NodeID) { } } -// GetForPeer returns an ID reserved for the peer. func (ids *IDs) GetForPeer(peerID types.NodeID) uint16 { ids.mtx.RLock() defer ids.mtx.RUnlock() @@ -63,8 +60,6 @@ func (ids *IDs) GetForPeer(peerID types.NodeID) uint16 { 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)) diff --git a/sei-tendermint/internal/mempool/ids_test.go b/sei-tendermint/internal/mempool/reactor/ids_test.go similarity index 99% rename from sei-tendermint/internal/mempool/ids_test.go rename to sei-tendermint/internal/mempool/reactor/ids_test.go index a5e75eeb68..53ad039b07 100644 --- a/sei-tendermint/internal/mempool/ids_test.go +++ b/sei-tendermint/internal/mempool/reactor/ids_test.go @@ -1,4 +1,4 @@ -package mempool +package reactor import ( "testing" diff --git a/sei-tendermint/internal/mempool/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go similarity index 71% rename from sei-tendermint/internal/mempool/reactor.go rename to sei-tendermint/internal/mempool/reactor/reactor.go index 764d9c5140..1855cef5e6 100644 --- a/sei-tendermint/internal/mempool/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -1,4 +1,4 @@ -package mempool +package reactor import ( "context" @@ -8,7 +8,7 @@ import ( "sync" "github.com/sei-protocol/sei-chain/sei-tendermint/config" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/service" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" @@ -23,6 +23,8 @@ var ( _ service.Service = (*Reactor)(nil) ) +const MempoolChannel p2p.ChannelID = 0x30 + // Reactor implements a service that contains mempool of txs that are broadcasted // amongst peers. It maintains a map from peer ID to counter, to prevent gossiping // txs to the peers you received it from. @@ -30,7 +32,7 @@ type Reactor struct { service.BaseService cfg *config.MempoolConfig - mempool *TxMempool + mempool *mempool.TxMempool ids *IDs router *p2p.Router @@ -44,13 +46,13 @@ type Reactor struct { } // NewReactor returns a reference to a new reactor. -func NewReactor(txmp *TxMempool, router *p2p.Router) (*Reactor, error) { - channel, err := p2p.OpenChannel(router, GetChannelDescriptor(txmp.config)) +func NewReactor(txmp *mempool.TxMempool, router *p2p.Router) (*Reactor, error) { + channel, err := p2p.OpenChannel(router, GetChannelDescriptor(txmp.Config())) if err != nil { return nil, fmt.Errorf("router.OpenChannel(): %w", err) } r := &Reactor{ - cfg: txmp.config, + cfg: txmp.Config(), mempool: txmp, ids: NewMempoolIDs(), router: router, @@ -65,8 +67,8 @@ func NewReactor(txmp *TxMempool, router *p2p.Router) (*Reactor, error) { func (r *Reactor) MarkReadyToStart() { r.readyToStart <- struct{}{} } -// getChannelDescriptor produces an instance of a descriptor for this -// package's required channels. +// GetChannelDescriptor produces an instance of a descriptor for this package's +// required channels. func GetChannelDescriptor(cfg *config.MempoolConfig) p2p.ChannelDescriptor[*pb.Message] { largestTx := make([]byte, cfg.MaxTxBytes) batchMsg := &pb.Message{ @@ -85,10 +87,10 @@ func GetChannelDescriptor(cfg *config.MempoolConfig) p2p.ChannelDescriptor[*pb.M } } -// OnStart starts separate go routines for each p2p Channel and listens for +// OnStart starts separate goroutines for each p2p channel and listens for // envelopes on each. In addition, it also listens for peer updates and handles // messages on that p2p channel accordingly. The caller must be sure to execute -// OnStop to ensure the outbound p2p Channels are closed. +// OnStop to ensure the outbound p2p channels are closed. func (r *Reactor) OnStart(ctx context.Context) error { if !r.cfg.Broadcast { logger.Info("tx broadcasting is disabled") @@ -119,7 +121,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me } protoTxs := msg.Txs.GetTxs() - txInfo := TxInfo{SenderID: r.ids.GetForPeer(m.From)} + txInfo := mempool.TxInfo{SenderID: r.ids.GetForPeer(m.From)} if len(m.From) != 0 { txInfo.SenderNodeID = m.From } @@ -127,21 +129,10 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me for _, tx := range protoTxs { if err := r.mempool.CheckTx(ctx, tx, nil, txInfo); err != nil { r.accountFailedCheckTx(m.From, err) - if errors.Is(err, errTxInCache) { - // if the tx is in the cache, - // then we've been gossiped a - // Tx that we've already - // got. Gossip should be - // smarter, but it's not a - // problem. + if mempool.IsTxInCacheError(err) { continue } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - // Do not propagate context - // cancellation errors, but do - // not continue to check - // transactions from this - // message if we are shutting down. return nil } @@ -160,7 +151,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me } func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { - if !r.cfg.CheckTxErrorBlacklistEnabled || !errors.Is(err, errTxTooLarge) { + if !r.cfg.CheckTxErrorBlacklistEnabled || !mempool.IsTxTooLargeError(err) { return } for counts := range r.failedCheckTxCounts.Lock() { @@ -174,7 +165,7 @@ func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { } } -// handleMessage handles an Envelope sent from a peer on a specific p2p Channel. +// handleMessage handles an envelope sent from a peer on a specific p2p channel. // It will handle errors and any possible panics gracefully. A caller can handle // any error returned by sending a PeerError on the respective channel. func (r *Reactor) handleMessage(ctx context.Context, m p2p.RecvMsg[*pb.Message]) (err error) { @@ -194,7 +185,7 @@ func (r *Reactor) handleMessage(ctx context.Context, m p2p.RecvMsg[*pb.Message]) } // processMempoolCh implements a blocking event loop where we listen for p2p -// Envelope messages from the mempoolCh. +// envelope messages from the mempool channel. func (r *Reactor) processMempoolCh(ctx context.Context) { <-r.readyToStart for { @@ -225,26 +216,17 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda counts[peerUpdate.NodeID] = 0 } - // Do not allow starting new tx broadcast loops after reactor shutdown - // has been initiated. This can happen after we've manually closed all - // peer broadcast, but the router still sends in-flight peer updates. if !r.IsRunning() { return } if r.cfg.Broadcast { - // Check if we've already started a goroutine for this peer, if not we create - // a new done channel so we can explicitly close the goroutine if the peer - // is later removed, we increment the waitgroup so the reactor can stop - // safely, and finally start the goroutine to broadcast txs to that peer. - _, ok := r.peerRoutines[peerUpdate.NodeID] - if !ok { + if _, ok := r.peerRoutines[peerUpdate.NodeID]; !ok { pctx, pcancel := context.WithCancel(ctx) r.peerRoutines[peerUpdate.NodeID] = pcancel r.ids.ReserveForPeer(peerUpdate.NodeID) - // start a broadcast routine ensuring all txs are forwarded to the peer go r.broadcastTxRoutine(pctx, peerUpdate.NodeID) } } @@ -255,10 +237,6 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda delete(counts, peerUpdate.NodeID) } - // Check if we've started a tx broadcasting goroutine for this peer. - // If we have, we signal to terminate the goroutine via the channel's closure. - // This will internally decrement the peer waitgroup and remove the peer - // from the map of peer tx broadcasting goroutines. closer, ok := r.peerRoutines[peerUpdate.NodeID] if ok { closer() @@ -266,9 +244,9 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda } } -// processPeerUpdates initiates a blocking process where we listen for and handle -// PeerUpdate messages. When the reactor is stopped, we will catch the signal and -// close the p2p PeerUpdatesCh gracefully. +// processPeerUpdates initiates a blocking process where we listen for and +// handle PeerUpdate messages. When the reactor is stopped, we will catch the +// signal and close the p2p PeerUpdatesCh gracefully. func (r *Reactor) processPeerUpdates(ctx context.Context) { recv := r.router.Subscribe() for { @@ -282,9 +260,8 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) { func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { peerMempoolID := r.ids.GetForPeer(peerID) - var nextGossipTx *clist.CElement + var nextGossipTx *mempool.GossipTx - // remove the peer ID from the map of routines and mark the waitgroup as done defer func() { r.mtx.Lock() delete(r.peerRoutines, peerID) @@ -304,31 +281,26 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { return } - // This happens because the CElement we were looking at got garbage - // collected (removed). That is, .NextWait() returned nil. Go ahead and - // start from the beginning. if nextGossipTx == nil { select { case <-ctx.Done(): return - case <-r.mempool.WaitForNextTx(): // wait until a tx is available + case <-r.mempool.WaitForNextTx(): if nextGossipTx = r.mempool.NextGossipTx(); nextGossipTx == nil { continue } } } - memTx := nextGossipTx.Value.(*WrappedTx) - - // NOTE: Transaction batching was disabled due to: - // https://github.com/tendermint/tendermint/issues/5796 - if ok := r.mempool.txStore.TxHasPeer(memTx.hash, peerMempoolID); !ok { - // Send the mempool tx to the corresponding peer. Note, the peer may be - // behind and thus would not be able to process the mempool tx correctly. - r.channel.Send(&pb.Message{Sum: &pb.Message_Txs{Txs: &pb.Txs{Txs: [][]byte{memTx.tx}}}}, peerID) + if ok := r.mempool.TxStore().TxHasPeer(nextGossipTx.Key(), peerMempoolID); !ok { + r.channel.Send(&pb.Message{ + Sum: &pb.Message_Txs{ + Txs: &pb.Txs{Txs: [][]byte{nextGossipTx.Tx()}}, + }, + }, peerID) logger.Debug( "gossiped tx to peer", - "tx", memTx.tx.Hash(), + "tx", nextGossipTx.Tx().Hash(), "peer", peerID, ) } diff --git a/sei-tendermint/internal/mempool/reactor_test.go b/sei-tendermint/internal/mempool/reactor/reactor_test.go similarity index 78% rename from sei-tendermint/internal/mempool/reactor_test.go rename to sei-tendermint/internal/mempool/reactor/reactor_test.go index 4b8b12b141..7438338ab0 100644 --- a/sei-tendermint/internal/mempool/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor/reactor_test.go @@ -1,8 +1,9 @@ -package mempool +package reactor import ( "context" "fmt" + "math/rand" "os" "runtime" "strings" @@ -16,6 +17,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" tmrand "github.com/sei-protocol/sei-chain/sei-tendermint/libs/rand" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" @@ -24,16 +26,62 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) +type testTx struct { + tx types.Tx +} + type reactorTestSuite struct { network *p2p.TestNetwork reactors map[types.NodeID]*Reactor - mempools map[types.NodeID]*TxMempool + mempools map[types.NodeID]*mempool.TxMempool kvstores map[types.NodeID]*kvstore.Application nodes []types.NodeID } +func setupMempool(t testing.TB, app abci.Application, cacheSize int, txConstraintsFetcher mempool.TxConstraintsFetcher) *mempool.TxMempool { + t.Helper() + + cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) + require.NoError(t, err) + cfg.Mempool.CacheSize = cacheSize + cfg.Mempool.DropUtilisationThreshold = 0.0 + + t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) + + return mempool.NewTxMempool(cfg.Mempool, app, mempool.NopMetrics(), txConstraintsFetcher) +} + +func checkTxs(ctx context.Context, t *testing.T, txmp *mempool.TxMempool, numTxs int, peerID uint16) []testTx { + t.Helper() + + txs := make([]testTx, numTxs) + txInfo := mempool.TxInfo{SenderID: peerID} + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + for i := range numTxs { + prefix := make([]byte, 20) + _, err := rng.Read(prefix) + require.NoError(t, err) + + txs[i] = testTx{ + tx: []byte(fmt.Sprintf("sender-%d-%d=%X=%d", i, peerID, prefix, i+1000)), + } + require.NoError(t, txmp.CheckTx(ctx, txs[i].tx, nil, txInfo)) + } + + return txs +} + +func convertTex(in []testTx) types.Txs { + out := make([]types.Tx, len(in)) + for i := range in { + out[i] = in[i].tx + } + return out +} + func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTestSuite { t.Helper() @@ -44,7 +92,7 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest rts := &reactorTestSuite{ network: p2p.MakeTestNetwork(t, p2p.TestNetworkOptions{NumNodes: numNodes}), reactors: make(map[types.NodeID]*Reactor, numNodes), - mempools: make(map[types.NodeID]*TxMempool, numNodes), + mempools: make(map[types.NodeID]*mempool.TxMempool, numNodes), kvstores: make(map[types.NodeID]*kvstore.Application, numNodes), } @@ -53,10 +101,10 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest rts.kvstores[nodeID] = kvstore.NewApplication() app := rts.kvstores[nodeID] - mempool := setup(t, app, 0, NopTxConstraintsFetcher) - rts.mempools[nodeID] = mempool + txmp := setupMempool(t, app, 0, mempool.NopTxConstraintsFetcher) + rts.mempools[nodeID] = txmp - reactor, err := NewReactor(mempool, node.Router) + reactor, err := NewReactor(txmp, node.Router) if err != nil { t.Fatalf("NewReactor(): %v", err) } @@ -78,19 +126,18 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest return rts } -func setupReactorForTest(t *testing.T, txConstraintsFetcher TxConstraintsFetcher) (*Reactor, *TxMempool) { +func setupReactorForTest(t *testing.T, txConstraintsFetcher mempool.TxConstraintsFetcher) (*Reactor, *mempool.TxMempool) { t.Helper() - cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) - require.NoError(t, err) + cfg := config.TestConfig() + cfg.SetRoot(t.TempDir()) cfg.Mempool.DropUtilisationThreshold = 0.0 cfg.Mempool.Broadcast = false - t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) network := p2p.MakeTestNetwork(t, p2p.TestNetworkOptions{NumNodes: 1}) node := network.Nodes()[0] - txmp := NewTxMempool(cfg.Mempool, kvstore.NewApplication(), NopMetrics(), txConstraintsFetcher) + txmp := mempool.NewTxMempool(cfg.Mempool, kvstore.NewApplication(), mempool.NopMetrics(), txConstraintsFetcher) reactor, err := NewReactor(txmp, node.Router) require.NoError(t, err) reactor.MarkReadyToStart() @@ -109,8 +156,6 @@ func (rts *reactorTestSuite) start(t *testing.T) { func (rts *reactorTestSuite) waitForTxns(t *testing.T, txs []types.Tx, ids ...types.NodeID) { t.Helper() - // ensure that the transactions get fully broadcast to the - // rest of the network wg := &sync.WaitGroup{} for name, pool := range rts.mempools { if !p2p.NodeInSlice(name, ids) { @@ -121,7 +166,7 @@ func (rts *reactorTestSuite) waitForTxns(t *testing.T, txs []types.Tx, ids ...ty } wg.Add(1) - go func(name types.NodeID, pool *TxMempool) { + go func(name types.NodeID, pool *mempool.TxMempool) { defer wg.Done() require.Eventually(t, func() bool { return len(txs) == pool.Size() }, time.Minute, @@ -133,6 +178,16 @@ func (rts *reactorTestSuite) waitForTxns(t *testing.T, txs []types.Tx, ids ...ty wg.Wait() } +func peerFailedCheckTxCount(reactor *Reactor, nodeID types.NodeID) utils.Option[int] { + for counts := range reactor.failedCheckTxCounts.Lock() { + if count, ok := counts[nodeID]; ok { + return utils.Some(count) + } + return utils.None[int]() + } + panic("unreachable") +} + func TestReactorBroadcastTxs(t *testing.T) { numTxs := 512 numNodes := 4 @@ -144,27 +199,14 @@ func TestReactorBroadcastTxs(t *testing.T) { primary := rts.nodes[0] secondaries := rts.nodes[1:] - txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, UnknownPeerID) + txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, mempool.UnknownPeerID) require.Equal(t, numTxs, rts.reactors[primary].mempool.Size()) rts.start(t) - - // Wait till all secondary suites (reactor) received all mempool txs from the - // primary suite (node). rts.waitForTxns(t, convertTex(txs), secondaries...) } -func peerFailedCheckTxCount(reactor *Reactor, nodeID types.NodeID) utils.Option[int] { - for counts := range reactor.failedCheckTxCounts.Lock() { - if count, ok := counts[nodeID]; ok { - return utils.Some(count) - } - return utils.None[int]() - } - panic("unreachable") -} - func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { ctx := t.Context() @@ -179,12 +221,17 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { receiverReactor := rts.reactors[receiver] receiverReactor.cfg.CheckTxErrorBlacklistEnabled = true receiverReactor.cfg.CheckTxErrorThreshold = 2 - receiverReactor.mempool.txConstraintsFetcher = TxConstraintsFetcher(func() (TxConstraints, error) { - return TxConstraints{ - MaxDataBytes: 10, - MaxGas: -1, - }, nil - }) + receiverReactor.mempool = mempool.NewTxMempool( + receiverReactor.cfg, + kvstore.NewApplication(), + mempool.NopMetrics(), + mempool.TxConstraintsFetcher(func() (mempool.TxConstraints, error) { + return mempool.TxConstraints{ + MaxDataBytes: 10, + MaxGas: -1, + }, nil + }), + ) conn := rts.network.Node(receiver).WaitForConnAndGet(ctx, sender) msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { @@ -197,8 +244,7 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { }, } } - // The network connection can be established before the reactor processes the - // async PeerStatusUp update that initializes the sender's failure count, so we need to wait. + require.Eventually(t, func() bool { return peerFailedCheckTxCount(receiverReactor, sender) == utils.Some(0) }, time.Second, 50*time.Millisecond) @@ -223,8 +269,8 @@ func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { reactor, _ := setupReactorForTest( t, - func() (TxConstraints, error) { - return TxConstraints{ + func() (mempool.TxConstraints, error) { + return mempool.TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil @@ -264,8 +310,8 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { reactor, _ := setupReactorForTest( t, - func() (TxConstraints, error) { - return TxConstraints{ + func() (mempool.TxConstraints, error) { + return mempool.TxConstraints{ MaxDataBytes: 10, MaxGas: -1, }, nil @@ -294,11 +340,9 @@ func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { require.Equal(t, utils.None[int](), peerFailedCheckTxCount(reactor, "sender")) } -// regression test for https://github.com/tendermint/tendermint/issues/5408 func TestReactorConcurrency(t *testing.T) { numTxs := 10 numNodes := 2 - ctx := t.Context() rts := setupReactors(ctx, t, numNodes) @@ -311,41 +355,36 @@ func TestReactorConcurrency(t *testing.T) { var wg sync.WaitGroup - for i := 0; i < runtime.NumCPU()*2; i++ { + for range runtime.NumCPU() * 2 { wg.Add(2) - // 1. submit a bunch of txs - // 2. update the whole mempool - - txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, UnknownPeerID) + txs := checkTxs(ctx, t, rts.reactors[primary].mempool, numTxs, mempool.UnknownPeerID) go func() { defer wg.Done() - mempool := rts.mempools[primary] + txmp := rts.mempools[primary] - mempool.Lock() - defer mempool.Unlock() + txmp.Lock() + defer txmp.Unlock() deliverTxResponses := make([]*abci.ExecTxResult, len(txs)) for i := range txs { deliverTxResponses[i] = &abci.ExecTxResult{Code: 0} } - require.NoError(t, mempool.Update(ctx, 1, convertTex(txs), deliverTxResponses, mempool.txConstraintsFetcher, true)) + require.NoError(t, txmp.Update(ctx, 1, convertTex(txs), deliverTxResponses, mempool.NopTxConstraintsFetcher, true)) }() - // 1. submit a bunch of txs - // 2. update none - _ = checkTxs(ctx, t, rts.reactors[secondary].mempool, numTxs, UnknownPeerID) + _ = checkTxs(ctx, t, rts.reactors[secondary].mempool, numTxs, mempool.UnknownPeerID) go func() { defer wg.Done() - mempool := rts.mempools[secondary] + txmp := rts.mempools[secondary] - mempool.Lock() - defer mempool.Unlock() + txmp.Lock() + defer txmp.Unlock() - err := mempool.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), mempool.txConstraintsFetcher, true) + err := txmp.Update(ctx, 1, []types.Tx{}, make([]*abci.ExecTxResult, 0), mempool.NopTxConstraintsFetcher, true) require.NoError(t, err) }() } @@ -356,7 +395,6 @@ func TestReactorConcurrency(t *testing.T) { func TestReactorNoBroadcastToSender(t *testing.T) { numTxs := 1000 numNodes := 2 - ctx := t.Context() rts := setupReactors(ctx, t, numNodes) @@ -369,7 +407,6 @@ func TestReactorNoBroadcastToSender(t *testing.T) { _ = checkTxs(ctx, t, rts.mempools[primary], numTxs, peerID) rts.start(t) - time.Sleep(100 * time.Millisecond) require.Eventually(t, func() bool { @@ -380,7 +417,6 @@ func TestReactorNoBroadcastToSender(t *testing.T) { func TestReactor_MaxTxBytes(t *testing.T) { numNodes := 2 cfg := config.TestConfig() - ctx := t.Context() rts := setupReactors(ctx, t, numNodes) @@ -389,16 +425,12 @@ func TestReactor_MaxTxBytes(t *testing.T) { primary := rts.nodes[0] secondary := rts.nodes[1] - // Broadcast a tx, which has the max size and ensure it's received by the - // second reactor. tx1 := tmrand.Bytes(cfg.Mempool.MaxTxBytes) err := rts.reactors[primary].mempool.CheckTx( ctx, tx1, nil, - TxInfo{ - SenderID: UnknownPeerID, - }, + mempool.TxInfo{SenderID: mempool.UnknownPeerID}, ) require.NoError(t, err) @@ -407,25 +439,20 @@ func TestReactor_MaxTxBytes(t *testing.T) { rts.reactors[primary].mempool.Flush() rts.reactors[secondary].mempool.Flush() - // broadcast a tx, which is beyond the max size and ensure it's not sent tx2 := tmrand.Bytes(cfg.Mempool.MaxTxBytes + 1) - err = rts.mempools[primary].CheckTx(ctx, tx2, nil, TxInfo{SenderID: UnknownPeerID}) + err = rts.mempools[primary].CheckTx(ctx, tx2, nil, mempool.TxInfo{SenderID: mempool.UnknownPeerID}) require.Error(t, err) } func TestDontExhaustMaxActiveIDs(t *testing.T) { t.Skip("this test fails, but the property it tests is not very useful") - // we're creating a single node network, but not starting the - // network. ctx := t.Context() - rts := setupReactors(ctx, t, 1) t.Cleanup(leaktest.Check(t)) nodeID := rts.nodes[0] - // ensure the reactor does not panic (i.e. exhaust active IDs) for range MaxActiveIDs + 1 { privKey := ed25519.GenerateSecretKey() peerID := types.NodeIDFromPubKey(privKey.Public()) @@ -441,7 +468,6 @@ func TestMempoolIDsPanicsIfNodeRequestsOvermaxActiveIDs(t *testing.T) { t.Skip("skipping test in short mode") } - // 0 is already reserved for UnknownPeerID ids := NewMempoolIDs() for i := range MaxActiveIDs - 1 { @@ -471,11 +497,9 @@ func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { secondary := rts.nodes[1] rts.start(t) - - // disconnect peer rts.network.Remove(t, secondary) - txs := checkTxs(ctx, t, rts.reactors[primary].mempool, 4, UnknownPeerID) + txs := checkTxs(ctx, t, rts.reactors[primary].mempool, 4, mempool.UnknownPeerID) 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/types.go b/sei-tendermint/internal/mempool/types.go index c0b67ec78d..d3343beb37 100644 --- a/sei-tendermint/internal/mempool/types.go +++ b/sei-tendermint/internal/mempool/types.go @@ -1,22 +1,11 @@ package mempool -import ( - "math" - - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" -) +import "math" const ( - MempoolChannel = p2p.ChannelID(0x30) - - // PeerCatchupSleepIntervalMS defines how much time to sleep if a peer is behind - PeerCatchupSleepIntervalMS = 100 - // UnknownPeerID is the peer ID to use when running CheckTx when there is // no peer (e.g. RPC) UnknownPeerID uint16 = 0 - - MaxActiveIDs = math.MaxUint16 ) // TxConstraints contains the precomputed consensus-derived mempool limits for diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 79e4df82ab..6e7e33bf2a 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -24,6 +24,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventlog" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/evidence" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" + mempoolreactor "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool/reactor" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/pex" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy" @@ -196,9 +197,9 @@ func makeNode( makeCloser(closers)) } mp := mempool.NewTxMempool(cfg.Mempool, app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) - mpReactor, err := mempool.NewReactor(mp, router) + mpReactor, err := mempoolreactor.NewReactor(mp, router) if err != nil { - return nil, fmt.Errorf("mempool.NewReactor(): %w", err) + return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) } node.router = router node.rpcEnv.Router = router diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index c1dfd05733..834d486080 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -21,7 +21,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/consensus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/evidence" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" + mempoolreactor "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool/reactor" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/pex" @@ -446,7 +446,7 @@ func makeNodeInfo( byte(consensus.DataChannel), byte(consensus.VoteChannel), byte(consensus.VoteSetBitsChannel), - byte(mempool.MempoolChannel), + byte(mempoolreactor.MempoolChannel), byte(evidence.EvidenceChannel), byte(statesync.SnapshotChannel), byte(statesync.ChunkChannel), From 46013910c5a54cdcde21e1b65ebe41fd1fb1906b Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 15:22:10 +0200 Subject: [PATCH 21/43] wip --- sei-tendermint/internal/mempool/mempool.go | 5 ++++- sei-tendermint/internal/mempool/reactor/reactor.go | 11 +++++++---- sei-tendermint/internal/mempool/tx.go | 4 ++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 02edc30b04..59814c9fb0 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -18,8 +18,11 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" "github.com/sei-protocol/sei-chain/sei-tendermint/types" + "github.com/sei-protocol/seilog" ) +var logger = seilog.NewLogger("tendermint", "internal", "mempool") + // errTxInCache is returned to the client if we saw tx earlier. var errTxInCache = errors.New("tx already exists in cache") @@ -213,7 +216,7 @@ func (txmp *TxMempool) WaitForNextTx() <-chan struct{} { return txmp.gossipIndex // NextGossipTx returns the next valid transaction to gossip. A caller must wait // for WaitForNextTx to signal a transaction is available to gossip first. It is // thread-safe. -func (txmp *TxMempool) NextGossipTx() *GossipTx { return newGossipTx(txmp.gossipIndex.Front()) } +func (txmp *TxMempool) NextGossipTx() *clist.CElement { return txmp.gossipIndex.Front() } // TxsAvailable returns a channel which fires once for every height, and only // when transactions are available in the mempool. It is thread-safe. diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 1855cef5e6..3425f263c8 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/sei-protocol/sei-chain/sei-tendermint/config" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/service" @@ -260,7 +261,7 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) { func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { peerMempoolID := r.ids.GetForPeer(peerID) - var nextGossipTx *mempool.GossipTx + var nextGossipTx *clist.CElement defer func() { r.mtx.Lock() @@ -292,15 +293,17 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { } } - if ok := r.mempool.TxStore().TxHasPeer(nextGossipTx.Key(), peerMempoolID); !ok { + memTx := nextGossipTx.Value.(*mempool.WrappedTx) + + if ok := r.mempool.TxStore().TxHasPeer(memTx.Key(), peerMempoolID); !ok { r.channel.Send(&pb.Message{ Sum: &pb.Message_Txs{ - Txs: &pb.Txs{Txs: [][]byte{nextGossipTx.Tx()}}, + Txs: &pb.Txs{Txs: [][]byte{memTx.Tx()}}, }, }, peerID) logger.Debug( "gossiped tx to peer", - "tx", nextGossipTx.Tx().Hash(), + "tx", memTx.Tx().Hash(), "peer", peerID, ) } diff --git a/sei-tendermint/internal/mempool/tx.go b/sei-tendermint/internal/mempool/tx.go index 1f76bc26d7..e19985f84b 100644 --- a/sei-tendermint/internal/mempool/tx.go +++ b/sei-tendermint/internal/mempool/tx.go @@ -85,6 +85,10 @@ func (wtx *WrappedTx) IsBefore(tx *WrappedTx) bool { return wtx.evmNonce < tx.evmNonce } +func (wtx *WrappedTx) Tx() types.Tx { return wtx.tx } + +func (wtx *WrappedTx) Key() types.TxKey { return wtx.hash } + func (wtx *WrappedTx) Size() int { return len(wtx.tx) } From b30fc914359f3c83053eb31565df76069d731e94 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 15:43:01 +0200 Subject: [PATCH 22/43] concurrency fix --- sei-tendermint/internal/mempool/mempool.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index f127524abe..d7aaf14fe1 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -54,7 +54,7 @@ type TxMempool struct { // txsAvailable fires once for each height when the mempool is not empty txsAvailable chan struct{} - notifiedTxsAvailable bool + notifiedTxsAvailable atomic.Bool // height defines the last block height process during Update() height int64 @@ -606,7 +606,7 @@ func (txmp *TxMempool) Update( recheck bool, ) error { txmp.height = blockHeight - txmp.notifiedTxsAvailable = false + txmp.notifiedTxsAvailable.Store(false) txmp.txConstraintsFetcher = txConstraintsFetcher for i, tx := range blockTxs { @@ -1079,11 +1079,10 @@ func (txmp *TxMempool) purgeExpiredTxs(blockHeight int64) { } func (txmp *TxMempool) notifyTxsAvailable() { - if txmp.NumTxsNotPending() == 0 || txmp.notifiedTxsAvailable { + if txmp.NumTxsNotPending() == 0 || txmp.notifiedTxsAvailable.Swap(true) { return } // channel cap is 1, so this will send once - txmp.notifiedTxsAvailable = true select { case txmp.txsAvailable <- struct{}{}: default: From 05e69c6f4d50fb06df22add638753a8ad6224af0 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 16:10:25 +0200 Subject: [PATCH 23/43] non-nil args to buildGigaConfig --- sei-tendermint/internal/p2p/giga_router.go | 2 + sei-tendermint/node/node.go | 14 ++++++- sei-tendermint/node/seed.go | 12 +++++- sei-tendermint/node/setup.go | 10 +++-- sei-tendermint/node/setup_test.go | 49 ++++++++++++++++------ 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index 189fb1946a..7cdd64c573 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -14,6 +14,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/giga" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" @@ -37,6 +38,7 @@ type GigaRouterConfig struct { ValidatorAddrs map[atypes.PublicKey]GigaNodeAddr Consensus *consensus.Config Producer *producer.Config + TxMempool *mempool.TxMempool App abci.Application GenDoc *types.GenesisDoc } diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 6e7e33bf2a..644a62947f 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -189,14 +189,24 @@ func makeNode( fmt.Errorf("autobahn does not support remote validator signers (priv-validator.laddr is set)"), makeCloser(closers)) } - router, peerCloser, err := createRouter(nodeMetrics.p2p, node.NodeInfo, nodeKey, utils.Some(atypes.SecretKeyFromED25519(filePrivval.Key.PrivKey)), cfg, app, genDoc, dbProvider) + mp := mempool.NewTxMempool(cfg.Mempool, app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) + router, peerCloser, err := createRouter( + nodeMetrics.p2p, + node.NodeInfo, + nodeKey, + utils.Some(atypes.SecretKeyFromED25519(filePrivval.Key.PrivKey)), + cfg, + mp, + app, + genDoc, + dbProvider, + ) closers = append(closers, peerCloser) if err != nil { return nil, combineCloseError( fmt.Errorf("failed to create router: %w", err), makeCloser(closers)) } - mp := mempool.NewTxMempool(cfg.Mempool, app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) mpReactor, err := mempoolreactor.NewReactor(mp, router) if err != nil { return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) diff --git a/sei-tendermint/node/seed.go b/sei-tendermint/node/seed.go index 22c83c2402..1eb381edaf 100644 --- a/sei-tendermint/node/seed.go +++ b/sei-tendermint/node/seed.go @@ -70,7 +70,17 @@ func makeSeedNode( return nil, err } - router, peerCloser, err := createRouter(nodeMetrics.p2p, func() *types.NodeInfo { return &nodeInfo }, nodeKey, utils.None[atypes.SecretKey](), cfg, nil, genDoc, dbProvider) + router, peerCloser, err := createRouter( + nodeMetrics.p2p, + func() *types.NodeInfo { return &nodeInfo }, + nodeKey, + utils.None[atypes.SecretKey](), + cfg, + nil, + nil, + genDoc, + dbProvider, + ) if err != nil { return nil, combineCloseError( fmt.Errorf("failed to create router: %w", err), diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index 834d486080..db9c66d922 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -21,6 +21,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/consensus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/evidence" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" mempoolreactor "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool/reactor" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn" @@ -236,6 +237,7 @@ func buildGigaConfig( autobahnConfigFile string, nodeKey types.NodeKey, validatorKey atypes.SecretKey, + txMempool *mempool.TxMempool, appClient abci.Application, genDoc *types.GenesisDoc, ) (*p2p.GigaRouterConfig, error) { @@ -292,8 +294,9 @@ func buildGigaConfig( BlockInterval: time.Duration(fc.BlockInterval), AllowEmptyBlocks: fc.AllowEmptyBlocks, }, - App: appClient, - GenDoc: genDoc, + TxMempool: txMempool, + App: appClient, + GenDoc: genDoc, }, nil } @@ -303,6 +306,7 @@ func createRouter( nodeKey types.NodeKey, validatorKey utils.Option[atypes.SecretKey], cfg *config.Config, + txMempool *mempool.TxMempool, app abci.Application, genDoc *types.GenesisDoc, dbProvider config.DBProvider, @@ -392,7 +396,7 @@ func createRouter( if !ok { return nil, closer, fmt.Errorf("autobahn non-validator nodes are not supported yet; a local validator key is required") } - gigaCfg, err := buildGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, app, genDoc) + gigaCfg, err := buildGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, txMempool, app, genDoc) if err != nil { return nil, closer, fmt.Errorf("buildGigaConfig: %w", err) } diff --git a/sei-tendermint/node/setup_test.go b/sei-tendermint/node/setup_test.go index 40c7ab86c2..aa5b64bcff 100644 --- a/sei-tendermint/node/setup_test.go +++ b/sei-tendermint/node/setup_test.go @@ -10,8 +10,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" + "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/tcp" @@ -64,10 +67,23 @@ func defaultFileConfig(validators []autobahnValidator) *autobahnFileConfig { } } +func makeTestGigaDeps() (*kvstore.Application, *mempool.TxMempool, *types.GenesisDoc) { + app := kvstore.NewApplication() + txMempool := mempool.NewTxMempool( + config.TestMempoolConfig(), + app, + mempool.NopMetrics(), + mempool.NopTxConstraintsFetcher, + ) + genDoc := &types.GenesisDoc{ChainID: "test-chain", InitialHeight: 1} + return app, txMempool, genDoc +} + func TestBuildGigaConfig_EmptyPathErrors(t *testing.T) { nodeKey := makeTestNodeKey([]byte("test-node-key")) valKey := makeTestValidatorKey([]byte("val-seed")) - _, err := buildGigaConfig("", nodeKey, valKey, nil, nil) + app, txMempool, genDoc := makeTestGigaDeps() + _, err := buildGigaConfig("", nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err, "empty path should error") } @@ -93,9 +109,9 @@ func TestBuildGigaConfig_EnabledWithValidators(t *testing.T) { nodeKey := makeTestNodeKey([]byte("node1-seed")) valKey := makeTestValidatorKey([]byte("val1-seed")) - genDoc := &types.GenesisDoc{ChainID: "test-chain", InitialHeight: 1} + app, txMempool, genDoc := makeTestGigaDeps() - result, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, genDoc) + result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) require.NoError(t, err) require.NotNil(t, result) @@ -133,8 +149,9 @@ func TestBuildGigaConfig_NoneMaxTxsPerSecond(t *testing.T) { cfgFile := writeAutobahnConfig(t, fc) nodeKey := makeTestNodeKey([]byte("node-seed")) valKey := makeTestValidatorKey([]byte("val-seed")) + app, txMempool, genDoc := makeTestGigaDeps() - result, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) require.NoError(t, err) assert.False(t, result.Producer.MaxTxsPerSecond.IsPresent()) } @@ -145,8 +162,9 @@ func TestBuildGigaConfig_NonePersistentStateDir(t *testing.T) { cfgFile := writeAutobahnConfig(t, fc) nodeKey := makeTestNodeKey([]byte("node-seed")) valKey := makeTestValidatorKey([]byte("val-seed")) + app, txMempool, genDoc := makeTestGigaDeps() - result, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) require.NoError(t, err) assert.False(t, result.Consensus.PersistentStateDir.IsPresent()) } @@ -154,23 +172,24 @@ func TestBuildGigaConfig_NonePersistentStateDir(t *testing.T) { func TestBuildGigaConfig_InvalidConfigFile(t *testing.T) { nodeKey := makeTestNodeKey([]byte("node-seed")) valKey := makeTestValidatorKey([]byte("val-seed")) + app, txMempool, genDoc := makeTestGigaDeps() t.Run("missing file", func(t *testing.T) { - _, err := buildGigaConfig("/nonexistent/autobahn.json", nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig("/nonexistent/autobahn.json", nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) }) t.Run("invalid json", func(t *testing.T) { path := filepath.Join(t.TempDir(), "bad.json") require.NoError(t, os.WriteFile(path, []byte("not json"), 0644)) - _, err := buildGigaConfig(path, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) }) t.Run("empty validators", func(t *testing.T) { fc := defaultFileConfig([]autobahnValidator{}) cfgFile := writeAutobahnConfig(t, fc) - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "validators must not be empty") }) @@ -180,7 +199,7 @@ func TestBuildGigaConfig_InvalidConfigFile(t *testing.T) { fc := defaultFileConfig([]autobahnValidator{v}) fc.MaxGasPerBlock = 0 cfgFile := writeAutobahnConfig(t, fc) - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "max_gas_per_block") }) @@ -195,8 +214,9 @@ func TestBuildGigaConfig_DuplicateValidatorKey(t *testing.T) { os.WriteFile(path, data, 0644) nodeKey := makeTestNodeKey([]byte("node1")) valKey := makeTestValidatorKey([]byte("val-seed")) + app, txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(path, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "duplicate validator key") } @@ -210,8 +230,9 @@ func TestBuildGigaConfig_DuplicateNodeKey(t *testing.T) { os.WriteFile(path, data, 0644) nodeKey := makeTestNodeKey([]byte("same-node")) valKey := makeTestValidatorKey([]byte("val1")) + app, txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(path, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "duplicate node key") } @@ -221,8 +242,9 @@ func TestBuildGigaConfig_SelfNotInValidators(t *testing.T) { cfgFile := writeAutobahnConfig(t, defaultFileConfig([]autobahnValidator{v1})) nodeKey := makeTestNodeKey([]byte("my-node")) valKey := makeTestValidatorKey([]byte("my-val")) + app, txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "validator key not found") } @@ -233,8 +255,9 @@ func TestBuildGigaConfig_NodeKeyMismatch(t *testing.T) { cfgFile := writeAutobahnConfig(t, defaultFileConfig([]autobahnValidator{v1})) nodeKey := makeTestNodeKey([]byte("my-node")) valKey := makeTestValidatorKey([]byte("my-val")) + app, txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, nil, &types.GenesisDoc{InitialHeight: 1}) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "node key mismatch") } From 0d3b1d09b2ad3a2cb8e884e1b9f21b9a4b7dc154 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 17:02:22 +0200 Subject: [PATCH 24/43] mempool in GigaRouter --- .../internal/autobahn/producer/state.go | 44 ++++++++++--------- sei-tendermint/internal/mempool/mempool.go | 29 +++++++++++- sei-tendermint/internal/p2p/giga_router.go | 37 +++++++++++----- .../internal/p2p/giga_router_test.go | 29 ++++++------ sei-tendermint/internal/p2p/router_test.go | 6 ++- sei-tendermint/node/setup.go | 1 - 6 files changed, 96 insertions(+), 50 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index e39786b832..d573871cf6 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -6,8 +6,8 @@ import ( "time" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" "golang.org/x/time/rate" @@ -32,19 +32,18 @@ func (c *Config) maxTxsPerBlock() uint64 { // State is the block producer state. type State struct { - cfg *Config - // channel of transactions to build the next block from. - mempool chan *pb.Transaction + cfg *Config + txMempool *mempool.TxMempool // consensus state to which published blocks will be reported. consensus *consensus.State } // NewState constructs a new block producer state. // Returns an error if the current node is NOT a producer. -func NewState(cfg *Config, consensus *consensus.State) *State { +func NewState(cfg *Config, txMempool *mempool.TxMempool, consensus *consensus.State) *State { return &State{ cfg: cfg, - mempool: make(chan *pb.Transaction, cfg.MempoolSize), + txMempool: txMempool, consensus: consensus, } } @@ -54,21 +53,29 @@ func NewState(cfg *Config, consensus *consensus.State) *State { func (s *State) makePayload(ctx context.Context) *types.Payload { ctx, cancel := context.WithTimeout(ctx, s.cfg.BlockInterval) defer cancel() - maxTxs := s.cfg.maxTxsPerBlock() - var totalGas uint64 - var txs [][]byte - for totalGas < s.cfg.MaxGasPerBlock && uint64(len(txs)) < maxTxs { - tx, err := utils.Recv(ctx, s.mempool) - if err != nil { - break + + if s.txMempool.NumTxsNotPending() == 0 { + select { + case <-ctx.Done(): + case <-s.txMempool.TxsAvailable(): } - txs = append(txs, tx.Payload) - totalGas += tx.GasUsed + } + + txs, totalGas := s.txMempool.ReapMaxTxsBytesMaxGas( + int(s.cfg.maxTxsPerBlock()), // nolint:gosec // config values fit into int on supported platforms. + -1, + int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. + int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. + ) + s.txMempool.RemoveTxs(txs) + payloadTxs := make([][]byte, 0, len(txs)) + for _, tx := range txs { + payloadTxs = append(payloadTxs, tx) } return types.PayloadBuilder{ CreatedAt: time.Now(), TotalGas: totalGas, - Txs: txs, + Txs: payloadTxs, }.Build() } @@ -86,11 +93,6 @@ func (s *State) nextPayload(ctx context.Context) (*types.Payload, error) { } } -// PushToMempool pushes the transaction to the mempool. -func (s *State) PushToMempool(ctx context.Context, tx *pb.Transaction) error { - return utils.Send(ctx, s.mempool, tx) -} - // Run runs the background tasks of the producer state. func (s *State) Run(ctx context.Context) error { return scope.Run(ctx, func(ctx context.Context, scope scope.Scope) error { diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 59814c9fb0..9722f487b9 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -163,6 +163,8 @@ func NewTxMempool( func (txmp *TxMempool) Config() *config.MempoolConfig { return txmp.config } +func (txmp *TxMempool) App() abci.Application { return txmp.app } + func (txmp *TxMempool) TxStore() *TxStore { return txmp.txStore } // Lock obtains a write-lock on the mempool. A caller must be sure to explicitly @@ -501,6 +503,14 @@ func (txmp *TxMempool) Flush() { // - Transactions returned are not removed from the mempool transaction // store or indexes. func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimated int64) types.Txs { + txs, _ := txmp.ReapMaxTxsBytesMaxGas(-1, maxBytes, maxGasWanted, maxGasEstimated) + return txs +} + +// ReapMaxTxsBytesMaxGas returns a list of transactions within the provided tx, +// byte, and gas constraints together with the total estimated gas for the +// returned transactions. A maxTxs value below zero disables the tx count limit. +func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, uint64) { txmp.mtx.Lock() defer txmp.mtx.Unlock() @@ -514,7 +524,7 @@ func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimate 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{} + return []types.Tx{}, 0 } totalTxs := txmp.priorityIndex.NumTxs() evmTxs := make([]types.Tx, 0, totalTxs) @@ -566,10 +576,25 @@ func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimate if encounteredGasUnfit && numTxs >= MinTxsToPeek { return false } + if maxTxs >= 0 && numTxs >= maxTxs { + return false + } return true }) - return append(evmTxs, nonEvmTxs...) + return append(evmTxs, nonEvmTxs...), uint64(totalGasEstimated) +} + +// RemoveTxs removes the provided transactions from the mempool if present. +func (txmp *TxMempool) RemoveTxs(txs types.Txs) { + txmp.Lock() + defer txmp.Unlock() + + for _, tx := range txs { + if wtx := txmp.txStore.GetTxByHash(tx.Key()); wtx != nil { + txmp.removeTx(wtx, false, true, true) + } + } } // ReapMaxTxs returns a list of transactions within the provided number of diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index 7cdd64c573..9a4dd0280e 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -11,7 +11,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/data" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" @@ -39,7 +38,6 @@ type GigaRouterConfig struct { Consensus *consensus.Config Producer *producer.Config TxMempool *mempool.TxMempool - App abci.Application GenDoc *types.GenesisDoc } @@ -54,10 +52,6 @@ type GigaRouter struct { poolOut *giga.Pool[NodePublicKey, rpc.Client[giga.API]] } -func (r *GigaRouter) PushToMempool(ctx context.Context, tx *pb.Transaction) error { - return r.producer.PushToMempool(ctx, tx) -} - func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (*GigaRouter, error) { if cfg.GenDoc.InitialHeight < 1 { return nil, fmt.Errorf("GenDoc.InitialHeight = %v, want >=1", cfg.GenDoc.InitialHeight) @@ -83,7 +77,7 @@ func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (*GigaRouter, error if err != nil { return nil, fmt.Errorf("consensus.NewState(): %w", err) } - producerState := producer.NewState(cfg.Producer, consensusState) + producerState := producer.NewState(cfg.Producer, cfg.TxMempool, consensusState) return &GigaRouter{ cfg: cfg, key: key, @@ -97,7 +91,9 @@ func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (*GigaRouter, error } func (r *GigaRouter) runExecute(ctx context.Context) error { - info, err := r.cfg.App.Info(ctx, &version.RequestInfo) + app := r.cfg.TxMempool.App() + + info, err := app.Info(ctx, &version.RequestInfo) if err != nil { return fmt.Errorf("App.Info(): %w", err) } @@ -107,7 +103,7 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { } next := last + 1 if last == 0 { - if _, err := r.cfg.App.InitChain(ctx, r.cfg.GenDoc.ToRequestInitChain()); err != nil { + if _, err := app.InitChain(ctx, r.cfg.GenDoc.ToRequestInitChain()); err != nil { return fmt.Errorf("App.InitChain(): %w", err) } var ok bool @@ -131,7 +127,7 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { hash := b.Header.Hash() var proposerAddress types.Address - if vals := r.cfg.App.GetValidators(); len(vals) > 0 { + if vals := app.GetValidators(); len(vals) > 0 { // Deterministically select a proposer from the app's validator committee. // We need it so that app does not emit error logs. proposer := slices.MinFunc(vals, func(a, b abci.ValidatorUpdate) int { return a.PubKey.Compare(b.PubKey) }) @@ -141,7 +137,7 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { } proposerAddress = key.Address() } - resp, err := r.cfg.App.FinalizeBlock(ctx, &abci.RequestFinalizeBlock{ + resp, err := app.FinalizeBlock(ctx, &abci.RequestFinalizeBlock{ Txs: b.Payload.Txs(), // Empty DecidedLastCommit does not indicate missing votes. DecidedLastCommit: abci.CommitInfo{}, @@ -165,10 +161,27 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { if err := r.data.PushAppHash(ctx, n, resp.AppHash); err != nil { return fmt.Errorf("r.data.PushAppHash(%v): %w", n, err) } - commitResp, err := r.cfg.App.Commit(ctx) + commitResp, err := app.Commit(ctx) if err != nil { return fmt.Errorf("r.cfg.App.Commit(): %w", err) } + blockTxs := make(types.Txs, len(b.Payload.Txs())) + for i, tx := range b.Payload.Txs() { + blockTxs[i] = tx + } + r.cfg.TxMempool.Lock() + err = r.cfg.TxMempool.Update( + ctx, + int64(n), // nolint:gosec // autobahn block numbers fit in int64. + blockTxs, + resp.TxResults, + mempool.NopTxConstraintsFetcher, + true, + ) + r.cfg.TxMempool.Unlock() + if err != nil { + return fmt.Errorf("r.cfg.TxMempool.Update(%v): %w", n, err) + } pruneBefore, ok := utils.SafeCast[atypes.GlobalBlockNumber](commitResp.RetainHeight) if !ok { return fmt.Errorf("invalid commitResp.RetainHeight = %v", commitResp.RetainHeight) diff --git a/sei-tendermint/internal/p2p/giga_router_test.go b/sei-tendermint/internal/p2p/giga_router_test.go index 07165d6045..2403aaff15 100644 --- a/sei-tendermint/internal/p2p/giga_router_test.go +++ b/sei-tendermint/internal/p2p/giga_router_test.go @@ -14,12 +14,13 @@ import ( "golang.org/x/time/rate" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" - apb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" @@ -77,6 +78,15 @@ func (a *testApp) Info(_ context.Context, _ *abci.RequestInfo) (*abci.ResponseIn panic("unreachable") } +func (a *testApp) CheckTx(context.Context, *abci.RequestCheckTxV2) (*abci.ResponseCheckTxV2, error) { + return &abci.ResponseCheckTxV2{ + ResponseCheckTx: &abci.ResponseCheckTx{ + Code: abci.CodeTypeOK, + GasWanted: 1, + }, + }, nil +} + func (a *testApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { for state, ctrl := range a.state.Lock() { if state.Init.IsPresent() { @@ -199,6 +209,7 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) { nodeInfo.Network = genDoc.ChainID e := Endpoint{AddrPort: cfg.addr} app := newTestApp() + txMempool := mempool.NewTxMempool(config.TestMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) router, err := NewRouter( NopMetrics(), cfg.nodeKey, @@ -228,8 +239,8 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) { BlockInterval: 100 * time.Millisecond, AllowEmptyBlocks: false, }, - App: app, - GenDoc: genDoc, + TxMempool: txMempool, + GenDoc: genDoc, }), }, ) @@ -245,17 +256,9 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) { allTxs = append(allTxs, tx) } s.SpawnNamed(fmt.Sprintf("producer[%v]", i), func() error { - giga, ok := router.giga.Get() - if !ok { - panic("giga router not set up") - } for _, payload := range txs { - tx := &apb.Transaction{ - Payload: payload, - GasUsed: txGasUsed, - } - if err := giga.PushToMempool(ctx, tx); err != nil { - return fmt.Errorf("PushToMempool(): %w", err) + if err := txMempool.CheckTx(ctx, payload, nil, mempool.TxInfo{}); err != nil { + return fmt.Errorf("txMempool.CheckTx(): %w", err) } } return nil diff --git a/sei-tendermint/internal/p2p/router_test.go b/sei-tendermint/internal/p2p/router_test.go index 9d98f57303..5194f510b7 100644 --- a/sei-tendermint/internal/p2p/router_test.go +++ b/sei-tendermint/internal/p2p/router_test.go @@ -16,10 +16,13 @@ import ( dbm "github.com/tendermint/tm-db" "golang.org/x/time/rate" + abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require" @@ -333,6 +336,7 @@ func TestRouter_GigaSetWhenConfigured(t *testing.T) { // Use intentionally non-default values to ensure config actually propagates. opts := makeRouterOptions() + txMempool := mempool.NewTxMempool(config.TestMempoolConfig(), abci.BaseApplication{}, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) opts.Giga = utils.Some(&GigaRouterConfig{ DialInterval: 7 * time.Second, ValidatorAddrs: validatorAddrs, @@ -349,7 +353,7 @@ func TestRouter_GigaSetWhenConfigured(t *testing.T) { BlockInterval: 777 * time.Millisecond, AllowEmptyBlocks: true, }, - App: nil, + TxMempool: txMempool, GenDoc: &types.GenesisDoc{ ChainID: "giga-e2e-test", InitialHeight: 42, diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index db9c66d922..fb5a387c79 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -295,7 +295,6 @@ func buildGigaConfig( AllowEmptyBlocks: fc.AllowEmptyBlocks, }, TxMempool: txMempool, - App: appClient, GenDoc: genDoc, }, nil } From f13a871ff4e7bf40335e3351dffb9fe872a69993 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Thu, 9 Apr 2026 17:36:06 +0200 Subject: [PATCH 25/43] -1 in Reap --- .../internal/autobahn/producer/state.go | 2 +- .../internal/consensus/mempool_test.go | 3 +- sei-tendermint/internal/mempool/mempool.go | 31 ++++++++++++++----- .../internal/mempool/mempool_test.go | 23 +++++++------- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index d573871cf6..324c39e3af 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -63,7 +63,7 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { txs, totalGas := s.txMempool.ReapMaxTxsBytesMaxGas( int(s.cfg.maxTxsPerBlock()), // nolint:gosec // config values fit into int on supported platforms. - -1, + utils.Max[int64](), int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. ) diff --git a/sei-tendermint/internal/consensus/mempool_test.go b/sei-tendermint/internal/consensus/mempool_test.go index 17f66d2420..513ae8e77a 100644 --- a/sei-tendermint/internal/consensus/mempool_test.go +++ b/sei-tendermint/internal/consensus/mempool_test.go @@ -20,6 +20,7 @@ import ( sm "github.com/sei-protocol/sei-chain/sei-tendermint/internal/state" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/store" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/test/factory" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -247,7 +248,7 @@ func TestMempoolRmBadTx(t *testing.T) { // check for the tx for { - txs := cs.txMempool.ReapMaxBytesMaxGas(int64(len(txBytes)), -1, -1) + txs := cs.txMempool.ReapMaxBytesMaxGas(int64(len(txBytes)), utils.Max[int64](), utils.Max[int64]()) if len(txs) == 0 { emptyMempoolCh <- struct{}{} return diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 9722f487b9..fc6372c8f7 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -493,7 +493,7 @@ func (txmp *TxMempool) Flush() { // and gas constraints. 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. Can be set to -1 to be ignored. +// 1. maxBytes - stops pulling txs from mempool once maxBytes is hit. // 2. maxGasWanted - stops pulling txs from mempool once total gas wanted exceeds maxGasWanted. // Can be set to -1 to be ignored. // 3. maxGasEstimated - similar to maxGasWanted but will use the estimated gas used for EVM txs @@ -503,14 +503,31 @@ func (txmp *TxMempool) Flush() { // - Transactions returned are not removed from the mempool transaction // store or indexes. func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimated int64) types.Txs { - txs, _ := txmp.ReapMaxTxsBytesMaxGas(-1, maxBytes, maxGasWanted, maxGasEstimated) + txs, _ := txmp.ReapMaxTxsBytesMaxGas(utils.Max[int](), maxBytes, maxGasWanted, maxGasEstimated) return txs } // ReapMaxTxsBytesMaxGas returns a list of transactions within the provided tx, // byte, and gas constraints together with the total estimated gas for the -// returned transactions. A maxTxs value below zero disables the tx count limit. +// 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) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, uint64) { + if maxTxs < 0 { + maxTxs = utils.Max[int]() + } + if maxBytes < 0 { + maxBytes = utils.Max[int64]() + } + if maxGasWanted < 0 { + maxGasWanted = utils.Max[int64]() + } + if maxGasEstimated < 0 { + maxGasEstimated = utils.Max[int64]() + } + txmp.mtx.Lock() defer txmp.mtx.Unlock() @@ -533,7 +550,7 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, size := types.ComputeProtoSizeForTxs([]types.Tx{wtx.tx}) // bytes limit is a hard stop - if maxBytes > -1 && totalSize+size > maxBytes { + if totalSize+size > maxBytes { return false } @@ -550,8 +567,8 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, prospectiveGasWanted := totalGasWanted + wtx.gasWanted prospectiveGasEstimated := totalGasEstimated + txGasEstimate - maxGasWantedExceeded := maxGasWanted > -1 && prospectiveGasWanted > maxGasWanted - maxGasEstimatedExceeded := maxGasEstimated > -1 && prospectiveGasEstimated > maxGasEstimated + 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 @@ -576,7 +593,7 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, if encounteredGasUnfit && numTxs >= MinTxsToPeek { return false } - if maxTxs >= 0 && numTxs >= maxTxs { + if numTxs >= maxTxs { return false } return true diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index 3f3225b200..fe841b7fd0 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -19,6 +19,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/config" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -370,7 +371,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(-1, 50, -1) + reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), 50, utils.Max[int64]()) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -381,7 +382,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(1000, -1, -1) + reapedTxs := txmp.ReapMaxBytesMaxGas(1000, utils.Max[int64](), utils.Max[int64]()) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -393,7 +394,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(1500, 30, -1) + reapedTxs := txmp.ReapMaxBytesMaxGas(1500, 30, utils.Max[int64]()) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -404,7 +405,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(-1, 2, -1) + reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), 2, utils.Max[int64]()) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Len(t, reapedTxs, 2) @@ -414,7 +415,7 @@ func TestTxMempool_ReapMaxBytesMaxGas(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(-1, -1, 50) + reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 50) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Len(t, reapedTxs, 50) @@ -458,7 +459,7 @@ func TestTxMempool_ReapMaxBytesMaxGas_FallbackToGasWanted(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxBytesMaxGas(-1, -1, 50) + reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 50) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Len(t, reapedTxs, 50) @@ -504,7 +505,7 @@ func TestTxMempool_ReapMaxTxs(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - reapedTxs := txmp.ReapMaxTxs(-1) + reapedTxs := txmp.ReapMaxTxs(utils.Max[int]()) ensurePrioritized(reapedTxs) require.Equal(t, len(tTxs), txmp.Size()) require.Equal(t, int64(5690), txmp.SizeBytes()) @@ -554,7 +555,7 @@ func TestTxMempool_ReapMaxBytesMaxGas_MinGasEVMTxThreshold(t *testing.T) { // 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(-1, -1, 40000) + reaped := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 40000) require.Len(t, reaped, 0) // Note: If MinGasEVMTx is changed to 0, the same scenario would use estimatedGas (10000) @@ -609,7 +610,7 @@ func TestTxMempool_Reap_SkipGasUnfitAndCollectMinTxs(t *testing.T) { } // Reap with a maxGasEstimated that makes the first tx unfit but allows many small txs - reaped := txmp.ReapMaxBytesMaxGas(-1, -1, 50) + reaped := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 50) require.Len(t, reaped, MinTxsToPeek) // Ensure all reaped small txs are under gas constraint @@ -646,7 +647,7 @@ func TestTxMempool_Reap_SkipGasUnfitStopsAtMinEvenWithCapacity(t *testing.T) { } // Make the gas limit very small so the first (big) tx is unfit and we only collect MinTxsPerBlock - reaped := txmp.ReapMaxBytesMaxGas(-1, -1, 10) + reaped := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), 10) require.Len(t, reaped, MinTxsToPeek) } @@ -1039,7 +1040,7 @@ func TestReapMaxBytesMaxGas_EVMFirst(t *testing.T) { require.Equal(t, 5, txmp.Size()) // Reap all transactions - reapedTxs := txmp.ReapMaxBytesMaxGas(-1, -1, -1) + reapedTxs := txmp.ReapMaxBytesMaxGas(utils.Max[int64](), utils.Max[int64](), utils.Max[int64]()) require.Len(t, reapedTxs, 5) // Verify EVM transactions come first, then non-EVM From cdc70472302a07b371da538635948e1f98c8b8f1 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 11:56:58 +0200 Subject: [PATCH 26/43] disabled some components for autobahn mode --- sei-tendermint/node/node.go | 106 +++++++++++++++++------------- sei-tendermint/node/seed.go | 4 +- sei-tendermint/node/setup.go | 18 +++-- sei-tendermint/node/setup_test.go | 46 ++++++------- 4 files changed, 97 insertions(+), 77 deletions(-) diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 644a62947f..63eaa12282 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -189,6 +189,7 @@ func makeNode( fmt.Errorf("autobahn does not support remote validator signers (priv-validator.laddr is set)"), makeCloser(closers)) } + gigaEnabled := cfg.AutobahnConfigFile != "" mp := mempool.NewTxMempool(cfg.Mempool, app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) router, peerCloser, err := createRouter( nodeMetrics.p2p, @@ -196,8 +197,7 @@ func makeNode( nodeKey, utils.Some(atypes.SecretKeyFromED25519(filePrivval.Key.PrivKey)), cfg, - mp, - app, + utils.Some(mp), genDoc, dbProvider, ) @@ -207,14 +207,19 @@ func makeNode( fmt.Errorf("failed to create router: %w", err), makeCloser(closers)) } - mpReactor, err := mempoolreactor.NewReactor(mp, router) - if err != nil { - return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) - } node.router = router node.rpcEnv.Router = router node.shutdownOps = makeCloser(closers) + if !gigaEnabled { + mpReactor, err := mempoolreactor.NewReactor(mp, router) + if err != nil { + return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) + } + mpReactor.MarkReadyToStart() + node.services = append(node.services, mpReactor) + } + evReactor, evPool, edbCloser, err := createEvidenceReactor(cfg, dbProvider, stateStore, blockStore, node.router, nodeMetrics.evidence, eventBus) closers = append(closers, edbCloser) @@ -225,10 +230,7 @@ func makeNode( node.rpcEnv.EvidencePool = evPool node.evPool = evPool - mpReactor.MarkReadyToStart() - node.rpcEnv.Mempool = mp - node.services = append(node.services, mpReactor) // make block executor for consensus and blockchain reactors to execute blocks blockExec := sm.NewBlockExecutor( @@ -251,6 +253,10 @@ func makeNode( // Determine whether we should do block sync. This must happen after the handshake, since the // app may modify the validator set, specifying ourself as the only validator. blockSync := !onlyValidatorIsUs(state, pubKey) + if gigaEnabled { + stateSync = false + blockSync = false + } waitSync := stateSync || blockSync csState, err := consensus.NewState( @@ -270,20 +276,23 @@ func makeNode( } node.rpcEnv.ConsensusState = csState - csReactor, err := consensus.NewReactor( - csState, - node.router, - eventBus, - waitSync, - nodeMetrics.consensus, - cfg, - ) - if err != nil { - return nil, fmt.Errorf("consensus.NewReactor(): %w", err) - } + var csReactor *consensus.Reactor + if !gigaEnabled { + csReactor, err = consensus.NewReactor( + csState, + node.router, + eventBus, + waitSync, + nodeMetrics.consensus, + cfg, + ) + if err != nil { + return nil, fmt.Errorf("consensus.NewReactor(): %w", err) + } - node.services = append(node.services, csReactor) - node.rpcEnv.ConsensusReactor = csReactor + node.services = append(node.services, csReactor) + node.rpcEnv.ConsensusReactor = csReactor + } // Create the blockchain reactor. Note, we do not start block sync if we're // doing a state sync first. @@ -343,29 +352,30 @@ func makeNode( // FIXME The way we do phased startups (e.g. replay -> block sync -> consensus) is very messy, // we should clean this whole thing up. See: // https://github.com/tendermint/tendermint/issues/4644 - ssReactor, err := statesync.NewReactor( - genDoc.ChainID, - genDoc.InitialHeight, - *cfg.StateSync, - app, - node.router, - stateStore, - blockStore, - cfg.StateSync.TempDir, - nodeMetrics.statesync, - eventBus, - // the post-sync operation - postSyncHook, - stateSync, - restartEvent, - cfg.SelfRemediation, - ) - if err != nil { - return nil, fmt.Errorf("statesync.NewReactor(): %w", err) - } - node.shouldHandshake = !stateSync - node.services = append(node.services, ssReactor) + if !gigaEnabled { + ssReactor, err := statesync.NewReactor( + genDoc.ChainID, + genDoc.InitialHeight, + *cfg.StateSync, + app, + node.router, + stateStore, + blockStore, + cfg.StateSync.TempDir, + nodeMetrics.statesync, + eventBus, + // the post-sync operation + postSyncHook, + stateSync, + restartEvent, + cfg.SelfRemediation, + ) + if err != nil { + return nil, fmt.Errorf("statesync.NewReactor(): %w", err) + } + node.services = append(node.services, ssReactor) + } if cfg.Mode == config.ModeValidator { if privValidator != nil { @@ -705,10 +715,14 @@ func LoadStateFromDBOrGenesisDocProvider(stateStore sm.Store, genDoc *types.Gene return state, nil } -func getRouterConfig(conf *config.Config, appClient abci.Application) *p2p.RouterOptions { +func getRouterConfig(conf *config.Config, appClient utils.Option[abci.Application]) *p2p.RouterOptions { opts := p2p.RouterOptions{} - if conf.FilterPeers && appClient != nil { + if conf.FilterPeers { + appClient, ok := appClient.Get() + if !ok { + return &opts + } opts.FilterPeerByID = utils.Some(func(ctx context.Context, id types.NodeID) error { res, err := appClient.Query(ctx, &abci.RequestQuery{ Path: fmt.Sprintf("/p2p/filter/id/%s", id), diff --git a/sei-tendermint/node/seed.go b/sei-tendermint/node/seed.go index 1eb381edaf..1abedb24ab 100644 --- a/sei-tendermint/node/seed.go +++ b/sei-tendermint/node/seed.go @@ -12,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/config" atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/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/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/pex" rpccore "github.com/sei-protocol/sei-chain/sei-tendermint/internal/rpc/core" @@ -76,8 +77,7 @@ func makeSeedNode( nodeKey, utils.None[atypes.SecretKey](), cfg, - nil, - nil, + utils.None[*mempool.TxMempool](), genDoc, dbProvider, ) diff --git a/sei-tendermint/node/setup.go b/sei-tendermint/node/setup.go index fb5a387c79..52d27cf4b6 100644 --- a/sei-tendermint/node/setup.go +++ b/sei-tendermint/node/setup.go @@ -232,13 +232,12 @@ func loadAutobahnFileConfig(path string) (*autobahnFileConfig, error) { return &fc, nil } -// buildGigaConfig constructs a GigaRouterConfig from the autobahn config file, node key, app, and genesis doc. +// buildGigaConfig constructs a GigaRouterConfig from the autobahn config file, node key, and genesis doc. func buildGigaConfig( autobahnConfigFile string, nodeKey types.NodeKey, validatorKey atypes.SecretKey, txMempool *mempool.TxMempool, - appClient abci.Application, genDoc *types.GenesisDoc, ) (*p2p.GigaRouterConfig, error) { if autobahnConfigFile == "" { @@ -305,8 +304,7 @@ func createRouter( nodeKey types.NodeKey, validatorKey utils.Option[atypes.SecretKey], cfg *config.Config, - txMempool *mempool.TxMempool, - app abci.Application, + txMempool utils.Option[*mempool.TxMempool], genDoc *types.GenesisDoc, dbProvider config.DBProvider, ) (*p2p.Router, closer, error) { @@ -315,7 +313,11 @@ func createRouter( if err != nil { return nil, closer, err } - options := getRouterConfig(cfg, app) + appClient := utils.None[abci.Application]() + if mp, ok := txMempool.Get(); ok { + appClient = utils.Some(mp.App()) + } + options := getRouterConfig(cfg, appClient) options.Endpoint = ep options.MaxIncomingConnectionAttempts = utils.Some(cfg.P2P.MaxIncomingConnectionAttempts) options.MaxDialRate = utils.Some(rate.Every(cfg.P2P.DialInterval)) @@ -395,7 +397,11 @@ func createRouter( if !ok { return nil, closer, fmt.Errorf("autobahn non-validator nodes are not supported yet; a local validator key is required") } - gigaCfg, err := buildGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, txMempool, app, genDoc) + mp, ok := txMempool.Get() + if !ok { + return nil, closer, errors.New("autobahn requires a tx mempool") + } + gigaCfg, err := buildGigaConfig(cfg.AutobahnConfigFile, nodeKey, valKey, mp, genDoc) if err != nil { return nil, closer, fmt.Errorf("buildGigaConfig: %w", err) } diff --git a/sei-tendermint/node/setup_test.go b/sei-tendermint/node/setup_test.go index aa5b64bcff..3df93bed06 100644 --- a/sei-tendermint/node/setup_test.go +++ b/sei-tendermint/node/setup_test.go @@ -67,7 +67,7 @@ func defaultFileConfig(validators []autobahnValidator) *autobahnFileConfig { } } -func makeTestGigaDeps() (*kvstore.Application, *mempool.TxMempool, *types.GenesisDoc) { +func makeTestGigaDeps() (*mempool.TxMempool, *types.GenesisDoc) { app := kvstore.NewApplication() txMempool := mempool.NewTxMempool( config.TestMempoolConfig(), @@ -76,14 +76,14 @@ func makeTestGigaDeps() (*kvstore.Application, *mempool.TxMempool, *types.Genesi mempool.NopTxConstraintsFetcher, ) genDoc := &types.GenesisDoc{ChainID: "test-chain", InitialHeight: 1} - return app, txMempool, genDoc + return txMempool, genDoc } func TestBuildGigaConfig_EmptyPathErrors(t *testing.T) { nodeKey := makeTestNodeKey([]byte("test-node-key")) valKey := makeTestValidatorKey([]byte("val-seed")) - app, txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig("", nodeKey, valKey, txMempool, app, genDoc) + txMempool, genDoc := makeTestGigaDeps() + _, err := buildGigaConfig("", nodeKey, valKey, txMempool, genDoc) assert.Error(t, err, "empty path should error") } @@ -109,9 +109,9 @@ func TestBuildGigaConfig_EnabledWithValidators(t *testing.T) { nodeKey := makeTestNodeKey([]byte("node1-seed")) valKey := makeTestValidatorKey([]byte("val1-seed")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) require.NoError(t, err) require.NotNil(t, result) @@ -149,9 +149,9 @@ func TestBuildGigaConfig_NoneMaxTxsPerSecond(t *testing.T) { cfgFile := writeAutobahnConfig(t, fc) nodeKey := makeTestNodeKey([]byte("node-seed")) valKey := makeTestValidatorKey([]byte("val-seed")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) require.NoError(t, err) assert.False(t, result.Producer.MaxTxsPerSecond.IsPresent()) } @@ -162,9 +162,9 @@ func TestBuildGigaConfig_NonePersistentStateDir(t *testing.T) { cfgFile := writeAutobahnConfig(t, fc) nodeKey := makeTestNodeKey([]byte("node-seed")) valKey := makeTestValidatorKey([]byte("val-seed")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + result, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) require.NoError(t, err) assert.False(t, result.Consensus.PersistentStateDir.IsPresent()) } @@ -172,24 +172,24 @@ func TestBuildGigaConfig_NonePersistentStateDir(t *testing.T) { func TestBuildGigaConfig_InvalidConfigFile(t *testing.T) { nodeKey := makeTestNodeKey([]byte("node-seed")) valKey := makeTestValidatorKey([]byte("val-seed")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() t.Run("missing file", func(t *testing.T) { - _, err := buildGigaConfig("/nonexistent/autobahn.json", nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig("/nonexistent/autobahn.json", nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) }) t.Run("invalid json", func(t *testing.T) { path := filepath.Join(t.TempDir(), "bad.json") require.NoError(t, os.WriteFile(path, []byte("not json"), 0644)) - _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) }) t.Run("empty validators", func(t *testing.T) { fc := defaultFileConfig([]autobahnValidator{}) cfgFile := writeAutobahnConfig(t, fc) - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "validators must not be empty") }) @@ -199,7 +199,7 @@ func TestBuildGigaConfig_InvalidConfigFile(t *testing.T) { fc := defaultFileConfig([]autobahnValidator{v}) fc.MaxGasPerBlock = 0 cfgFile := writeAutobahnConfig(t, fc) - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "max_gas_per_block") }) @@ -214,9 +214,9 @@ func TestBuildGigaConfig_DuplicateValidatorKey(t *testing.T) { os.WriteFile(path, data, 0644) nodeKey := makeTestNodeKey([]byte("node1")) valKey := makeTestValidatorKey([]byte("val-seed")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "duplicate validator key") } @@ -230,9 +230,9 @@ func TestBuildGigaConfig_DuplicateNodeKey(t *testing.T) { os.WriteFile(path, data, 0644) nodeKey := makeTestNodeKey([]byte("same-node")) valKey := makeTestValidatorKey([]byte("val1")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(path, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "duplicate node key") } @@ -242,9 +242,9 @@ func TestBuildGigaConfig_SelfNotInValidators(t *testing.T) { cfgFile := writeAutobahnConfig(t, defaultFileConfig([]autobahnValidator{v1})) nodeKey := makeTestNodeKey([]byte("my-node")) valKey := makeTestValidatorKey([]byte("my-val")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "validator key not found") } @@ -255,9 +255,9 @@ func TestBuildGigaConfig_NodeKeyMismatch(t *testing.T) { cfgFile := writeAutobahnConfig(t, defaultFileConfig([]autobahnValidator{v1})) nodeKey := makeTestNodeKey([]byte("my-node")) valKey := makeTestValidatorKey([]byte("my-val")) - app, txMempool, genDoc := makeTestGigaDeps() + txMempool, genDoc := makeTestGigaDeps() - _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, app, genDoc) + _, err := buildGigaConfig(cfgFile, nodeKey, valKey, txMempool, genDoc) assert.Error(t, err) assert.Contains(t, err.Error(), "node key mismatch") } From a21d3156c64715b0f98ff150a596496f2bd324b3 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 12:41:45 +0200 Subject: [PATCH 27/43] disabled mempool notifications when WaitForTxs is disabled --- sei-tendermint/config/config.go | 18 +++++++++--------- sei-tendermint/internal/consensus/state.go | 14 ++++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/sei-tendermint/config/config.go b/sei-tendermint/config/config.go index f5d413df6a..011295cde3 100644 --- a/sei-tendermint/config/config.go +++ b/sei-tendermint/config/config.go @@ -234,7 +234,7 @@ type BaseConfig struct { // so the app can decide if we should keep the connection or not FilterPeers bool `mapstructure:"filter-peers"` // false - Other map[string]interface{} `mapstructure:",remain"` + Other map[string]any `mapstructure:",remain"` } // DefaultBaseConfig returns a default base configuration for a Tendermint node @@ -1146,14 +1146,14 @@ type ConsensusConfig struct { // been included and provide a helpful error message. // These fields should be completely removed in v0.37. // See: https://github.com/tendermint/tendermint/issues/8188 - DeprecatedTimeoutPropose *interface{} `mapstructure:"timeout-propose"` - DeprecatedTimeoutProposeDelta *interface{} `mapstructure:"timeout-propose-delta"` - DeprecatedTimeoutPrevote *interface{} `mapstructure:"timeout-prevote"` - DeprecatedTimeoutPrevoteDelta *interface{} `mapstructure:"timeout-prevote-delta"` - DeprecatedTimeoutPrecommit *interface{} `mapstructure:"timeout-precommit"` - DeprecatedTimeoutPrecommitDelta *interface{} `mapstructure:"timeout-precommit-delta"` - DeprecatedTimeoutCommit *interface{} `mapstructure:"timeout-commit"` - DeprecatedSkipTimeoutCommit *interface{} `mapstructure:"skip-timeout-commit"` + DeprecatedTimeoutPropose *any `mapstructure:"timeout-propose"` + DeprecatedTimeoutProposeDelta *any `mapstructure:"timeout-propose-delta"` + DeprecatedTimeoutPrevote *any `mapstructure:"timeout-prevote"` + DeprecatedTimeoutPrevoteDelta *any `mapstructure:"timeout-prevote-delta"` + DeprecatedTimeoutPrecommit *any `mapstructure:"timeout-precommit"` + DeprecatedTimeoutPrecommitDelta *any `mapstructure:"timeout-precommit-delta"` + DeprecatedTimeoutCommit *any `mapstructure:"timeout-commit"` + DeprecatedSkipTimeoutCommit *any `mapstructure:"skip-timeout-commit"` } // DefaultConsensusConfig returns a default configuration for the consensus service diff --git a/sei-tendermint/internal/consensus/state.go b/sei-tendermint/internal/consensus/state.go index 55f9027485..3ca278707a 100644 --- a/sei-tendermint/internal/consensus/state.go +++ b/sei-tendermint/internal/consensus/state.go @@ -78,11 +78,6 @@ func (ti *timeoutInfo) String() string { return fmt.Sprintf("%v ; %d/%d %v", ti.Duration, ti.Height, ti.Round, ti.Step) } -// interface to the mempool -type txMempool interface { - TxsAvailable() <-chan struct{} -} - // interface to the evidence pool type evidencePool interface { // reports conflicting votes to the evidence pool to be processed into evidence @@ -749,6 +744,13 @@ func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) error { } }() + // Channel signaling that transactions are available. + // nil (blocks forever) if waiting for transactions is disabled. + var txsAvailable <-chan struct{} + if cs.config.WaitForTxs() { + txsAvailable = cs.txMempool.TxsAvailable() + } + for { if maxSteps > 0 { if cs.nSteps >= maxSteps { @@ -759,7 +761,7 @@ func (cs *State) receiveRoutine(ctx context.Context, maxSteps int) error { } select { - case <-cs.txMempool.TxsAvailable(): + case <-txsAvailable: cs.handleTxsAvailable(ctx) case mi := <-cs.peerMsgQueue: From 062f712f66df59a54ceab9e681d53cf0be62b7e2 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 13:19:35 +0200 Subject: [PATCH 28/43] reverted err visibility --- sei-tendermint/internal/mempool/mempool.go | 18 +++++++----------- .../internal/mempool/mempool_test.go | 2 +- .../internal/mempool/reactor/reactor.go | 4 ++-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 5491c01923..ddb6ca2f8d 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -23,11 +23,11 @@ import ( var logger = seilog.NewLogger("tendermint", "internal", "mempool") -// errTxInCache is returned to the client if we saw tx earlier. -var errTxInCache = errors.New("tx already exists in cache") +// ErrTxInCache is returned to the client if we saw tx earlier. +var ErrTxInCache = errors.New("tx already exists in cache") -// errTxTooLarge defines an error when a transaction is too big to be sent to peers. -var errTxTooLarge = errors.New("tx too large") +// ErrTxTooLarge defines an error when a transaction is too big to be sent to peers. +var ErrTxTooLarge = errors.New("tx too large") // Using SHA-256 truncated to 128 bits as the cache key: At 2K tx/sec, the // collision probability is effectively zero (≈10^-29 for 120K keys in a minute, @@ -226,10 +226,6 @@ func (txmp *TxMempool) TxsAvailable() <-chan struct{} { return txmp.txsAvailable } -func IsTxInCacheError(err error) bool { return errors.Is(err, errTxInCache) } - -func IsTxTooLargeError(err error) bool { return errors.Is(err, errTxTooLarge) } - func (txmp *TxMempool) checkResponseState(res *abci.ResponseCheckTx) error { constraints, err := txmp.txConstraintsFetcher() if err != nil { @@ -280,14 +276,14 @@ func (txmp *TxMempool) CheckTx( defer txmp.mtx.RUnlock() if txSize := len(tx); txSize > txmp.config.MaxTxBytes { - return fmt.Errorf("%w: max size is %d, but got %d", errTxTooLarge, txmp.config.MaxTxBytes, txSize) + return fmt.Errorf("%w: max size is %d, but got %d", ErrTxTooLarge, txmp.config.MaxTxBytes, txSize) } constraints, err := txmp.txConstraintsFetcher() if err != nil { return fmt.Errorf("txmp.txConstraintsFetcher(): %w", err) } if txSize := types.ComputeProtoSizeForTxs([]types.Tx{tx}); txSize > constraints.MaxDataBytes { - return fmt.Errorf("%w: tx size is too big: %d, max: %d", errTxTooLarge, txSize, constraints.MaxDataBytes) + return fmt.Errorf("%w: tx size is too big: %d, max: %d", ErrTxTooLarge, txSize, constraints.MaxDataBytes) } // Reject low priority transactions when the mempool is more than @@ -316,7 +312,7 @@ func (txmp *TxMempool) CheckTx( // check if we've seen this transaction and error if we have. if !txmp.cache.Push(txHash) { txmp.txStore.GetOrSetPeerByTxHash(txHash, txInfo.SenderID) - return errTxInCache + return ErrTxInCache } txmp.metrics.CacheSize.Set(float64(txmp.cache.Size())) diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index fe841b7fd0..a4d0d0799a 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -1112,7 +1112,7 @@ func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) { // Second failure: tx should remain in cache — CheckTx should reject it err := txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}) - require.Equal(t, errTxInCache, err) + require.Equal(t, ErrTxInCache, err) require.Equal(t, 0, txmp.Size()) // A different tx (different hash) should still be admitted diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 3425f263c8..11a90056d4 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -130,7 +130,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me for _, tx := range protoTxs { if err := r.mempool.CheckTx(ctx, tx, nil, txInfo); err != nil { r.accountFailedCheckTx(m.From, err) - if mempool.IsTxInCacheError(err) { + if errors.Is(err, mempool.ErrTxInCache) { continue } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { @@ -152,7 +152,7 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me } func (r *Reactor) accountFailedCheckTx(nodeID types.NodeID, err error) { - if !r.cfg.CheckTxErrorBlacklistEnabled || !mempool.IsTxTooLargeError(err) { + if !r.cfg.CheckTxErrorBlacklistEnabled || !errors.Is(err, mempool.ErrTxTooLarge) { return } for counts := range r.failedCheckTxCounts.Lock() { From 9b5b8e6bc5a09a594770b97aeed8465d85396fb9 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 13:22:08 +0200 Subject: [PATCH 29/43] restored comments --- .../internal/mempool/reactor/reactor.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 11a90056d4..7317c93af6 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -131,9 +131,15 @@ func (r *Reactor) handleMempoolMessage(ctx context.Context, m p2p.RecvMsg[*pb.Me if err := r.mempool.CheckTx(ctx, tx, nil, txInfo); 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 + // that we've already got. Gossip should be smarter, but it's + // not a problem. continue } if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + // Do not propagate context cancellation errors, but do not + // continue to check transactions from this message if we are + // shutting down. return nil } @@ -217,17 +223,24 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda counts[peerUpdate.NodeID] = 0 } + // Do not allow starting new tx broadcast loops after reactor shutdown + // has been initiated. This can happen after we've manually closed all + // peer broadcast, but the router still sends in-flight peer updates. if !r.IsRunning() { return } if r.cfg.Broadcast { + // Check if we've already started a goroutine for this peer. If not, + // create one and reserve the peer's mempool ID. if _, ok := r.peerRoutines[peerUpdate.NodeID]; !ok { pctx, pcancel := context.WithCancel(ctx) r.peerRoutines[peerUpdate.NodeID] = pcancel r.ids.ReserveForPeer(peerUpdate.NodeID) + // Start a broadcast routine ensuring all txs are forwarded to + // the peer. go r.broadcastTxRoutine(pctx, peerUpdate.NodeID) } } @@ -238,6 +251,8 @@ func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpda delete(counts, peerUpdate.NodeID) } + // Check if we've started a tx broadcasting goroutine for this peer. + // If so, signal it to terminate. closer, ok := r.peerRoutines[peerUpdate.NodeID] if ok { closer() From c8e6d174ca26e58c41dd461b2c6bda8fef65783a Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 13:28:48 +0200 Subject: [PATCH 30/43] more comments --- sei-tendermint/internal/mempool/reactor/ids.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor/ids.go b/sei-tendermint/internal/mempool/reactor/ids.go index 829e1c5119..b089526fe9 100644 --- a/sei-tendermint/internal/mempool/reactor/ids.go +++ b/sei-tendermint/internal/mempool/reactor/ids.go @@ -14,23 +14,28 @@ const MaxActiveIDs = math.MaxUint16 type IDs struct { mtx sync.RWMutex peerMap map[types.NodeID]uint16 - nextID uint16 - activeIDs map[uint16]struct{} + 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), + 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 } @@ -39,6 +44,7 @@ func (ids *IDs) ReserveForPeer(peerID types.NodeID) { 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() @@ -53,6 +59,7 @@ func (ids *IDs) Reclaim(peerID types.NodeID) { } } +// GetForPeer returns an ID reserved for the peer. func (ids *IDs) GetForPeer(peerID types.NodeID) uint16 { ids.mtx.RLock() defer ids.mtx.RUnlock() @@ -60,6 +67,8 @@ func (ids *IDs) GetForPeer(peerID types.NodeID) uint16 { 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)) From 708a690347dc595ef416622e406218e97c7df90a Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 13:47:30 +0200 Subject: [PATCH 31/43] broadcast fix --- .../internal/mempool/reactor/reactor.go | 105 ++++++------------ .../internal/mempool/reactor/reactor_test.go | 33 +++--- 2 files changed, 49 insertions(+), 89 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 7317c93af6..6f5f6ead18 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "runtime/debug" - "sync" "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" @@ -13,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/service" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" + "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope" pb "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/types" "github.com/sei-protocol/seilog" @@ -38,8 +38,6 @@ type Reactor struct { router *p2p.Router - mtx sync.Mutex - peerRoutines map[types.NodeID]context.CancelFunc failedCheckTxCounts utils.Mutex[map[types.NodeID]int] channel *p2p.Channel[*pb.Message] @@ -58,7 +56,6 @@ func NewReactor(txmp *mempool.TxMempool, router *p2p.Router) (*Reactor, error) { ids: NewMempoolIDs(), router: router, channel: channel, - peerRoutines: map[types.NodeID]context.CancelFunc{}, failedCheckTxCounts: utils.NewMutex(map[types.NodeID]int{}), readyToStart: make(chan struct{}, 1), } @@ -206,72 +203,46 @@ func (r *Reactor) processMempoolCh(ctx context.Context) { } } -// processPeerUpdate processes a PeerUpdate. For added peers, PeerStatusUp, we -// check if the reactor is running and if we've already started a tx broadcasting -// goroutine or not. If not, we start one for the newly added peer. For down or -// removed peers, we remove the peer from the mempool peer ID set and signal to -// stop the tx broadcasting goroutine. -func (r *Reactor) processPeerUpdate(ctx context.Context, peerUpdate p2p.PeerUpdate) { - logger.Debug("received peer update", "peer", peerUpdate.NodeID, "status", peerUpdate.Status) - - r.mtx.Lock() - defer r.mtx.Unlock() - - switch peerUpdate.Status { - case p2p.PeerStatusUp: - for counts := range r.failedCheckTxCounts.Lock() { - counts[peerUpdate.NodeID] = 0 - } - - // Do not allow starting new tx broadcast loops after reactor shutdown - // has been initiated. This can happen after we've manually closed all - // peer broadcast, but the router still sends in-flight peer updates. - if !r.IsRunning() { - return - } +// processPeerUpdates initiates a blocking process where we listen for and +// handle PeerUpdate messages. When the reactor is stopped, we will catch the +// signal and close the p2p PeerUpdatesCh gracefully. +func (r *Reactor) processPeerUpdates(ctx context.Context) error { + if !r.cfg.Broadcast { + return nil + } + return scope.Run(ctx, func(ctx context.Context, s scope.Scope) error { + recv := r.router.Subscribe() + peerRoutines := map[types.NodeID]context.CancelFunc{} + for { + update, err := recv.Recv(ctx) + if err != nil { + return err + } + logger.Debug("received peer update", "peer", update.NodeID, "status", update.Status) - if r.cfg.Broadcast { - // Check if we've already started a goroutine for this peer. If not, - // create one and reserve the peer's mempool ID. - if _, ok := r.peerRoutines[peerUpdate.NodeID]; !ok { + switch update.Status { + case p2p.PeerStatusUp: + for counts := range r.failedCheckTxCounts.Lock() { + counts[update.NodeID] = 0 + } pctx, pcancel := context.WithCancel(ctx) - r.peerRoutines[peerUpdate.NodeID] = pcancel - - r.ids.ReserveForPeer(peerUpdate.NodeID) + peerRoutines[update.NodeID] = pcancel + r.ids.ReserveForPeer(update.NodeID) + s.Spawn(func() error { + r.broadcastTxRoutine(pctx, update.NodeID) + return nil + }) - // Start a broadcast routine ensuring all txs are forwarded to - // the peer. - go r.broadcastTxRoutine(pctx, peerUpdate.NodeID) + case p2p.PeerStatusDown: + r.ids.Reclaim(update.NodeID) + for counts := range r.failedCheckTxCounts.Lock() { + delete(counts, update.NodeID) + } + peerRoutines[update.NodeID]() + delete(peerRoutines, update.NodeID) } } - - case p2p.PeerStatusDown: - r.ids.Reclaim(peerUpdate.NodeID) - for counts := range r.failedCheckTxCounts.Lock() { - delete(counts, peerUpdate.NodeID) - } - - // Check if we've started a tx broadcasting goroutine for this peer. - // If so, signal it to terminate. - closer, ok := r.peerRoutines[peerUpdate.NodeID] - if ok { - closer() - } - } -} - -// processPeerUpdates initiates a blocking process where we listen for and -// handle PeerUpdate messages. When the reactor is stopped, we will catch the -// signal and close the p2p PeerUpdatesCh gracefully. -func (r *Reactor) processPeerUpdates(ctx context.Context) { - recv := r.router.Subscribe() - for { - update, err := recv.Recv(ctx) - if err != nil { - return - } - r.processPeerUpdate(ctx, update) - } + }) } func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { @@ -279,10 +250,6 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { var nextGossipTx *clist.CElement defer func() { - r.mtx.Lock() - delete(r.peerRoutines, peerID) - r.mtx.Unlock() - if e := recover(); e != nil { logger.Error( "recovering from broadcasting mempool loop", diff --git a/sei-tendermint/internal/mempool/reactor/reactor_test.go b/sei-tendermint/internal/mempool/reactor/reactor_test.go index 7438338ab0..7a9d89fea4 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor/reactor_test.go @@ -289,19 +289,18 @@ func TestReactorPeerDownClearsFailedCheckTxCount(t *testing.T) { } reactor.cfg.CheckTxErrorBlacklistEnabled = true - reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ - NodeID: "sender", - Status: p2p.PeerStatusUp, - }) + for counts := range reactor.failedCheckTxCounts.Lock() { + counts["sender"] = 0 + } require.Equal(t, utils.Some(0), peerFailedCheckTxCount(reactor, "sender")) require.NoError(t, reactor.handleMempoolMessage(t.Context(), msg)) require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "sender")) - reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ - NodeID: "sender", - Status: p2p.PeerStatusDown, - }) + reactor.ids.Reclaim("sender") + for counts := range reactor.failedCheckTxCounts.Lock() { + delete(counts, "sender") + } require.Equal(t, utils.None[int](), peerFailedCheckTxCount(reactor, "sender")) require.Equal(t, utils.Some(1), peerFailedCheckTxCount(reactor, "other")) @@ -327,14 +326,11 @@ func TestReactorMissingFailedCheckTxCountIsNotRecreated(t *testing.T) { } reactor.cfg.CheckTxErrorBlacklistEnabled = true - reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ - NodeID: "sender", - Status: p2p.PeerStatusUp, - }) - reactor.processPeerUpdate(t.Context(), p2p.PeerUpdate{ - NodeID: "sender", - Status: p2p.PeerStatusDown, - }) + for counts := range reactor.failedCheckTxCounts.Lock() { + 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")) @@ -456,10 +452,7 @@ func TestDontExhaustMaxActiveIDs(t *testing.T) { for range MaxActiveIDs + 1 { privKey := ed25519.GenerateSecretKey() peerID := types.NodeIDFromPubKey(privKey.Public()) - rts.reactors[nodeID].processPeerUpdate(ctx, p2p.PeerUpdate{ - Status: p2p.PeerStatusUp, - NodeID: peerID, - }) + rts.reactors[nodeID].ids.ReserveForPeer(peerID) } } From b94c7190d8340f0d300a1c728866712d82e8563b Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 14:25:21 +0200 Subject: [PATCH 32/43] SpawnCritical --- sei-tendermint/internal/mempool/mempool.go | 16 ++++---- .../internal/mempool/reactor/reactor.go | 40 ++++++------------- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index ddb6ca2f8d..857615c8b5 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -211,14 +211,14 @@ func (txmp *TxMempool) SizeBytes() int64 { return atomic.LoadInt64(&txmp.sizeByt func (txmp *TxMempool) PendingSizeBytes() int64 { return atomic.LoadInt64(&txmp.pendingSizeBytes) } -// WaitForNextTx returns a blocking channel that will be closed when the next -// valid transaction is available to gossip. It is thread-safe. -func (txmp *TxMempool) WaitForNextTx() <-chan struct{} { return txmp.gossipIndex.WaitChan() } - -// NextGossipTx returns the next valid transaction to gossip. A caller must wait -// for WaitForNextTx to signal a transaction is available to gossip first. It is -// thread-safe. -func (txmp *TxMempool) NextGossipTx() *clist.CElement { return txmp.gossipIndex.Front() } +// 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,error) { + if _,_,err := utils.RecvOrClosed(ctx,txmp.gossipIndex.WaitChan()); err!=nil { + return nil,err + } + return txmp.gossipIndex.Front(),nil +} // TxsAvailable returns a channel which fires once for every height, and only // when transactions are available in the mempool. It is thread-safe. diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 6f5f6ead18..9bec57a88a 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -93,12 +93,8 @@ func (r *Reactor) OnStart(ctx context.Context) error { if !r.cfg.Broadcast { logger.Info("tx broadcasting is disabled") } - - if r.channel == nil { - return errors.New("mempool channel is not set") - } - go r.processMempoolCh(ctx) - go r.processPeerUpdates(ctx) + r.SpawnCritical("processMempoolCh", r.processMempoolCh) + r.SpawnCritical("processPeerUpdates", r.processPeerUpdates) r.SpawnCritical("mempool", r.mempool.Run) return nil } @@ -190,12 +186,12 @@ func (r *Reactor) handleMessage(ctx context.Context, m p2p.RecvMsg[*pb.Message]) // processMempoolCh implements a blocking event loop where we listen for p2p // envelope messages from the mempool channel. -func (r *Reactor) processMempoolCh(ctx context.Context) { +func (r *Reactor) processMempoolCh(ctx context.Context) error { <-r.readyToStart for { m, err := r.channel.Recv(ctx) if err != nil { - return + return err } if err := r.handleMessage(ctx, m); err != nil { r.router.Evict(m.From, fmt.Errorf("mempool: %w", err)) @@ -259,22 +255,12 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { } }() - for { - if !r.IsRunning() || ctx.Err() != nil { - return - } - - if nextGossipTx == nil { - select { - case <-ctx.Done(): - return - case <-r.mempool.WaitForNextTx(): - if nextGossipTx = r.mempool.NextGossipTx(); nextGossipTx == nil { - continue - } - } - } - + var err error + nextGossipTx,err = r.mempool.WaitForNextTx(ctx) + if err!=nil { + return + } + for ctx.Err() == nil { memTx := nextGossipTx.Value.(*mempool.WrappedTx) if ok := r.mempool.TxStore().TxHasPeer(memTx.Key(), peerMempoolID); !ok { @@ -290,11 +276,9 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { ) } - select { - case <-nextGossipTx.NextWaitChan(): - nextGossipTx = nextGossipTx.Next() - case <-ctx.Done(): + if _,_,err := utils.RecvOrClosed(ctx,nextGossipTx.NextWaitChan()); err!=nil { return } + nextGossipTx = nextGossipTx.Next() } } From 029113baaf976a61e1e011431e2721d97b227367 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 14:31:24 +0200 Subject: [PATCH 33/43] moved ownership of mempool --- sei-tendermint/internal/mempool/mempool.go | 12 ++++++------ sei-tendermint/internal/mempool/reactor/reactor.go | 7 +++---- sei-tendermint/node/node.go | 3 +++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 857615c8b5..c492934290 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -211,13 +211,13 @@ func (txmp *TxMempool) SizeBytes() int64 { return atomic.LoadInt64(&txmp.sizeByt func (txmp *TxMempool) PendingSizeBytes() int64 { return atomic.LoadInt64(&txmp.pendingSizeBytes) } -// 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,error) { - if _,_,err := utils.RecvOrClosed(ctx,txmp.gossipIndex.WaitChan()); err!=nil { - return nil,err +// 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, error) { + if _, _, err := utils.RecvOrClosed(ctx, txmp.gossipIndex.WaitChan()); err != nil { + return nil, err } - return txmp.gossipIndex.Front(),nil + return txmp.gossipIndex.Front(), nil } // TxsAvailable returns a channel which fires once for every height, and only diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 9bec57a88a..38628a4fa4 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -95,7 +95,6 @@ func (r *Reactor) OnStart(ctx context.Context) error { } r.SpawnCritical("processMempoolCh", r.processMempoolCh) r.SpawnCritical("processPeerUpdates", r.processPeerUpdates) - r.SpawnCritical("mempool", r.mempool.Run) return nil } @@ -256,8 +255,8 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { }() var err error - nextGossipTx,err = r.mempool.WaitForNextTx(ctx) - if err!=nil { + nextGossipTx, err = r.mempool.WaitForNextTx(ctx) + if err != nil { return } for ctx.Err() == nil { @@ -276,7 +275,7 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { ) } - if _,_,err := utils.RecvOrClosed(ctx,nextGossipTx.NextWaitChan()); err!=nil { + if _, _, err := utils.RecvOrClosed(ctx, nextGossipTx.NextWaitChan()); err != nil { return } nextGossipTx = nextGossipTx.Next() diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 63eaa12282..822872887e 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -67,6 +67,7 @@ type nodeImpl struct { initialState sm.State stateStore sm.Store blockStore *store.BlockStore // store the blockchain to disk + mempool *mempool.TxMempool evPool *evidence.Pool indexerService *indexer.Service services []service.Service @@ -208,6 +209,7 @@ func makeNode( makeCloser(closers)) } node.router = router + node.mempool = mp node.rpcEnv.Router = router node.shutdownOps = makeCloser(closers) @@ -492,6 +494,7 @@ func (n *nodeImpl) OnStart(ctx context.Context) error { return err } n.rpcEnv.IsListening = true + n.SpawnCritical("mempool", n.mempool.Run) for _, reactor := range n.services { if err := reactor.Start(ctx); err != nil { From 67ea52a979c43dec26a3e231669565e56cb3cd9f Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 14:41:01 +0200 Subject: [PATCH 34/43] lint --- sei-tendermint/internal/autobahn/producer/state.go | 7 +++++-- sei-tendermint/internal/mempool/mempool.go | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index 324c39e3af..16c58fa3d8 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -74,8 +74,11 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { } return types.PayloadBuilder{ CreatedAt: time.Now(), - TotalGas: totalGas, - Txs: payloadTxs, + // 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(totalGas), // nolint:gosec // guaranteed to be positive + Txs: payloadTxs, }.Build() } diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index c492934290..5e5abadad0 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -510,7 +510,7 @@ func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimate // 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) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, uint64) { +func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, int64) { if maxTxs < 0 { maxTxs = utils.Max[int]() } @@ -595,7 +595,7 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, return true }) - return append(evmTxs, nonEvmTxs...), uint64(totalGasEstimated) + return append(evmTxs, nonEvmTxs...), totalGasEstimated } // RemoveTxs removes the provided transactions from the mempool if present. From f6be3638494c457a18d54b3271b110aecbe4a79a Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 20:23:13 +0200 Subject: [PATCH 35/43] wip --- .../internal/mempool/reactor/reactor.go | 3 ++ .../internal/mempool/reactor/reactor_test.go | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 38628a4fa4..bd13c1cdbe 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -279,5 +279,8 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { return } nextGossipTx = nextGossipTx.Next() + if nextGossipTx == nil { + return + } } } diff --git a/sei-tendermint/internal/mempool/reactor/reactor_test.go b/sei-tendermint/internal/mempool/reactor/reactor_test.go index 7a9d89fea4..9a6bb5ac3e 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor/reactor_test.go @@ -83,6 +83,15 @@ func convertTex(in []testTx) types.Txs { } func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTestSuite { + return setupReactorsWithTxConstraintsFetchers(ctx, t, numNodes, nil) +} + +func setupReactorsWithTxConstraintsFetchers( + ctx context.Context, + t *testing.T, + numNodes int, + txConstraintsFetchers map[int]mempool.TxConstraintsFetcher, +) *reactorTestSuite { t.Helper() cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) @@ -96,12 +105,16 @@ func setupReactors(ctx context.Context, t *testing.T, numNodes int) *reactorTest kvstores: make(map[types.NodeID]*kvstore.Application, numNodes), } - for _, node := range rts.network.Nodes() { + for i, node := range rts.network.Nodes() { nodeID := node.NodeID rts.kvstores[nodeID] = kvstore.NewApplication() app := rts.kvstores[nodeID] - txmp := setupMempool(t, app, 0, mempool.NopTxConstraintsFetcher) + txConstraintsFetcher := mempool.NopTxConstraintsFetcher + if customFetcher, ok := txConstraintsFetchers[i]; ok { + txConstraintsFetcher = customFetcher + } + txmp := setupMempool(t, app, 0, txConstraintsFetcher) rts.mempools[nodeID] = txmp reactor, err := NewReactor(txmp, node.Router) @@ -210,28 +223,24 @@ func TestReactorBroadcastTxs(t *testing.T) { func TestReactorFailedCheckTxCountEvictsPeer(t *testing.T) { ctx := t.Context() - rts := setupReactors(ctx, t, 2) + rts := setupReactorsWithTxConstraintsFetchers(ctx, t, 2, map[int]mempool.TxConstraintsFetcher{ + 1: func() (mempool.TxConstraints, error) { + return mempool.TxConstraints{ + MaxDataBytes: 10, + MaxGas: -1, + }, nil + }, + }) t.Cleanup(leaktest.Check(t)) sender := rts.nodes[0] receiver := rts.nodes[1] - rts.start(t) - receiverReactor := rts.reactors[receiver] receiverReactor.cfg.CheckTxErrorBlacklistEnabled = true receiverReactor.cfg.CheckTxErrorThreshold = 2 - receiverReactor.mempool = mempool.NewTxMempool( - receiverReactor.cfg, - kvstore.NewApplication(), - mempool.NopMetrics(), - mempool.TxConstraintsFetcher(func() (mempool.TxConstraints, error) { - return mempool.TxConstraints{ - MaxDataBytes: 10, - MaxGas: -1, - }, nil - }), - ) + + rts.start(t) conn := rts.network.Node(receiver).WaitForConnAndGet(ctx, sender) msgForTx := func(tx []byte) p2p.RecvMsg[*pb.Message] { From de584fa108ed70beaf00c5bcbf3beaf75f3b569e Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Fri, 10 Apr 2026 20:29:18 +0200 Subject: [PATCH 36/43] mempool fix --- .../internal/mempool/reactor/reactor.go | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index bd13c1cdbe..41b296eba8 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -7,7 +7,6 @@ import ( "runtime/debug" "github.com/sei-protocol/sei-chain/sei-tendermint/config" - "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/service" @@ -242,8 +241,7 @@ func (r *Reactor) processPeerUpdates(ctx context.Context) error { func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { peerMempoolID := r.ids.GetForPeer(peerID) - var nextGossipTx *clist.CElement - + // TODO: this function does not call any external code, so panics should not be expected. defer func() { if e := recover(); e != nil { logger.Error( @@ -254,33 +252,32 @@ func (r *Reactor) broadcastTxRoutine(ctx context.Context, peerID types.NodeID) { } }() - var err error - nextGossipTx, err = r.mempool.WaitForNextTx(ctx) - if err != nil { - return - } for ctx.Err() == nil { - memTx := nextGossipTx.Value.(*mempool.WrappedTx) - - if ok := r.mempool.TxStore().TxHasPeer(memTx.Key(), peerMempoolID); !ok { - r.channel.Send(&pb.Message{ - Sum: &pb.Message_Txs{ - Txs: &pb.Txs{Txs: [][]byte{memTx.Tx()}}, - }, - }, peerID) - logger.Debug( - "gossiped tx to peer", - "tx", memTx.Tx().Hash(), - "peer", peerID, - ) - } - - if _, _, err := utils.RecvOrClosed(ctx, nextGossipTx.NextWaitChan()); err != nil { + nextGossipTx, err := r.mempool.WaitForNextTx(ctx) + if err != nil { return } - nextGossipTx = nextGossipTx.Next() - if nextGossipTx == nil { - return + for ctx.Err() == nil && nextGossipTx != nil { + memTx := nextGossipTx.Value.(*mempool.WrappedTx) + + if ok := r.mempool.TxStore().TxHasPeer(memTx.Key(), peerMempoolID); !ok { + r.channel.Send(&pb.Message{ + Sum: &pb.Message_Txs{ + Txs: &pb.Txs{Txs: [][]byte{memTx.Tx()}}, + }, + }, peerID) + logger.Debug( + "gossiped tx to peer", + "tx", memTx.Tx().Hash(), + "peer", peerID, + ) + } + + if _, _, err := utils.RecvOrClosed(ctx, nextGossipTx.NextWaitChan()); err != nil { + return + } + // WARNING: Next() may return nil in case element has been removed. + nextGossipTx = nextGossipTx.Next() } } } From 26e74ea701133481ca79c6555c7f1dac1fe6b8cd Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Mon, 13 Apr 2026 14:56:51 +0200 Subject: [PATCH 37/43] applied comments --- .../internal/autobahn/producer/state.go | 4 ++-- sei-tendermint/internal/mempool/mempool.go | 16 +++++----------- sei-tendermint/node/node.go | 2 ++ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index 16c58fa3d8..676837fca8 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -62,7 +62,7 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { } txs, totalGas := s.txMempool.ReapMaxTxsBytesMaxGas( - int(s.cfg.maxTxsPerBlock()), // nolint:gosec // config values fit into int on supported platforms. + s.cfg.maxTxsPerBlock(), // nolint:gosec // config values fit into int on supported platforms. utils.Max[int64](), int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. @@ -77,7 +77,7 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { // 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(totalGas), // nolint:gosec // guaranteed to be positive + TotalGas: uint64(totalGas), // nolint:gosec Txs: payloadTxs, }.Build() } diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 5e5abadad0..b1493fdbec 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -499,7 +499,7 @@ func (txmp *TxMempool) Flush() { // - Transactions returned are not removed from the mempool transaction // store or indexes. func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimated int64) types.Txs { - txs, _ := txmp.ReapMaxTxsBytesMaxGas(utils.Max[int](), maxBytes, maxGasWanted, maxGasEstimated) + txs, _ := txmp.ReapMaxTxsBytesMaxGas(utils.Max[uint64](), maxBytes, maxGasWanted, maxGasEstimated) return txs } @@ -510,10 +510,7 @@ func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimate // 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) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, int64) { - if maxTxs < 0 { - maxTxs = utils.Max[int]() - } +func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs uint64, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, int64) { if maxBytes < 0 { maxBytes = utils.Max[int64]() } @@ -533,7 +530,7 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, totalSize int64 ) - numTxs := 0 + 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 @@ -546,7 +543,7 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, size := types.ComputeProtoSizeForTxs([]types.Tx{wtx.tx}) // bytes limit is a hard stop - if totalSize+size > maxBytes { + if totalSize+size > maxBytes || numTxs+1 > maxTxs { return false } @@ -576,6 +573,7 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, } // include tx and update totals + numTxs += 1 totalSize += size totalGasWanted = prospectiveGasWanted totalGasEstimated = prospectiveGasEstimated @@ -585,13 +583,9 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs int, maxBytes, maxGasWanted, } else { nonEvmTxs = append(nonEvmTxs, wtx.tx) } - numTxs++ if encounteredGasUnfit && numTxs >= MinTxsToPeek { return false } - if numTxs >= maxTxs { - return false - } return true }) diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index 8ab11ee67e..ef0dd89288 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -212,6 +212,8 @@ func makeNode( node.rpcEnv.Router = router node.shutdownOps = makeCloser(closers) + // Mempool gossiping is not compatible with Giga, + // so we disable the mempool reactor. if !gigaEnabled { mpReactor, err := mempoolreactor.NewReactor(mp, router) if err != nil { From be79fac5e2f9524b302c7731bc32efb6e78c5b87 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Mon, 13 Apr 2026 16:03:07 +0200 Subject: [PATCH 38/43] keeping a tx mempool lock for Finalize + Commit + Update --- sei-tendermint/internal/p2p/giga_router.go | 123 +++++++++++---------- 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index 9a4dd0280e..9ca2279cd3 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -90,6 +90,70 @@ func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (*GigaRouter, error }, nil } +func (r *GigaRouter) executeBlock(ctx context.Context, b *atypes.GlobalBlock) (*abci.ResponseCommit, error) { + app := r.cfg.TxMempool.App() + hash := b.Header.Hash() + var proposerAddress types.Address + if vals := app.GetValidators(); len(vals) > 0 { + // Deterministically select a proposer from the app's validator committee. + // We need it so that app does not emit error logs. + proposer := slices.MinFunc(vals, func(a, b abci.ValidatorUpdate) int { return a.PubKey.Compare(b.PubKey) }) + key, err := crypto.PubKeyFromProto(proposer.PubKey) + if err != nil { + return nil, fmt.Errorf("crypto.PubKeyFromProto(): %w", err) + } + proposerAddress = key.Address() + } + + r.cfg.TxMempool.Lock() + defer r.cfg.TxMempool.Unlock() + + resp, err := app.FinalizeBlock(ctx, &abci.RequestFinalizeBlock{ + Txs: b.Payload.Txs(), + // Empty DecidedLastCommit does not indicate missing votes. + DecidedLastCommit: abci.CommitInfo{}, + // WARNING: this is a hash of the autobahn block header. + // It is used to identify block processed optimistically + // and is fed as block hash to EVM contracts. + Hash: hash[:], + Header: (&types.Header{ + ChainID: r.cfg.GenDoc.ChainID, + Height: int64(b.GlobalNumber), // nolint:gosec // different representations of the same value + Time: b.Timestamp, + // WARNING: the reward distribution has corner cases where it forgets the proposer, + // because reward is distributed with a delay. This is not our problem here though. + ProposerAddress: proposerAddress, + }).ToProto(), + }) + if err != nil { + return nil, fmt.Errorf("r.cfg.App.FinalizeBlock(): %w", err) + } + // TODO: we need the block to be persisted before we vote for apphash. + if err := r.data.PushAppHash(ctx, b.GlobalNumber, resp.AppHash); err != nil { + return nil, fmt.Errorf("r.data.PushAppHash(%v): %w", b.GlobalNumber, err) + } + commitResp, err := app.Commit(ctx) + if err != nil { + return nil, fmt.Errorf("r.cfg.App.Commit(): %w", err) + } + blockTxs := make(types.Txs, len(b.Payload.Txs())) + for i, tx := range b.Payload.Txs() { + blockTxs[i] = tx + } + err = r.cfg.TxMempool.Update( + ctx, + int64(b.GlobalNumber), // nolint:gosec // autobahn block numbers fit in int64. + blockTxs, + resp.TxResults, + mempool.NopTxConstraintsFetcher, + true, + ) + if err != nil { + return nil, fmt.Errorf("r.cfg.TxMempool.Update(%v): %w", b.GlobalNumber, err) + } + return commitResp, nil +} + func (r *GigaRouter) runExecute(ctx context.Context) error { app := r.cfg.TxMempool.App() @@ -124,64 +188,7 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { if err != nil { return err } - - hash := b.Header.Hash() - var proposerAddress types.Address - if vals := app.GetValidators(); len(vals) > 0 { - // Deterministically select a proposer from the app's validator committee. - // We need it so that app does not emit error logs. - proposer := slices.MinFunc(vals, func(a, b abci.ValidatorUpdate) int { return a.PubKey.Compare(b.PubKey) }) - key, err := crypto.PubKeyFromProto(proposer.PubKey) - if err != nil { - return fmt.Errorf("crypto.PubKeyFromProto(): %w", err) - } - proposerAddress = key.Address() - } - resp, err := app.FinalizeBlock(ctx, &abci.RequestFinalizeBlock{ - Txs: b.Payload.Txs(), - // Empty DecidedLastCommit does not indicate missing votes. - DecidedLastCommit: abci.CommitInfo{}, - // WARNING: this is a hash of the autobahn block header. - // It is used to identify block processed optimistically - // and is fed as block hash to EVM contracts. - Hash: hash[:], - Header: (&types.Header{ - ChainID: r.cfg.GenDoc.ChainID, - Height: int64(n), // nolint:gosec // different representations of the same value - Time: b.Timestamp, - // WARNING: the reward distribution has corner cases where it forgets the proposer, - // because reward is distributed with a delay. This is not our problem here though. - ProposerAddress: proposerAddress, - }).ToProto(), - }) - if err != nil { - return fmt.Errorf("r.cfg.App.FinalizeBlock(): %w", err) - } - // TODO: we need the block to be persisted before we vote for apphash. - if err := r.data.PushAppHash(ctx, n, resp.AppHash); err != nil { - return fmt.Errorf("r.data.PushAppHash(%v): %w", n, err) - } - commitResp, err := app.Commit(ctx) - if err != nil { - return fmt.Errorf("r.cfg.App.Commit(): %w", err) - } - blockTxs := make(types.Txs, len(b.Payload.Txs())) - for i, tx := range b.Payload.Txs() { - blockTxs[i] = tx - } - r.cfg.TxMempool.Lock() - err = r.cfg.TxMempool.Update( - ctx, - int64(n), // nolint:gosec // autobahn block numbers fit in int64. - blockTxs, - resp.TxResults, - mempool.NopTxConstraintsFetcher, - true, - ) - r.cfg.TxMempool.Unlock() - if err != nil { - return fmt.Errorf("r.cfg.TxMempool.Update(%v): %w", n, err) - } + commitResp, err := r.executeBlock(ctx, b) pruneBefore, ok := utils.SafeCast[atypes.GlobalBlockNumber](commitResp.RetainHeight) if !ok { return fmt.Errorf("invalid commitResp.RetainHeight = %v", commitResp.RetainHeight) From baac6b0cbc39d3786cfe4ab0053c5906745bf7dc Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Tue, 14 Apr 2026 16:09:03 +0200 Subject: [PATCH 39/43] applied comments --- .../internal/autobahn/producer/state.go | 15 +++++--- .../internal/autobahn/types/block.go | 37 ++++++++++++++++++- .../internal/autobahn/types/testonly.go | 4 +- sei-tendermint/internal/p2p/giga/api.go | 11 +++++- sei-tendermint/internal/p2p/giga_router.go | 10 ++++- sei-tendermint/internal/protoutils/msg.go | 5 +++ 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index 676837fca8..b1c153a5ea 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -62,17 +62,17 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { } txs, totalGas := s.txMempool.ReapMaxTxsBytesMaxGas( - s.cfg.maxTxsPerBlock(), // nolint:gosec // config values fit into int on supported platforms. - utils.Max[int64](), - int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. - int64(s.cfg.MaxGasPerBlock), // nolint:gosec // config values stay within int64 range. + min(types.MaxTxsPerBlock,s.cfg.maxTxsPerBlock()), + utils.Clamp[int64](types.MaxTxsBytesPerBlock), + utils.Clamp[int64](s.cfg.MaxGasPerBlock), + utils.Clamp[int64](s.cfg.MaxGasPerBlock), ) s.txMempool.RemoveTxs(txs) payloadTxs := make([][]byte, 0, len(txs)) for _, tx := range txs { payloadTxs = append(payloadTxs, tx) } - return types.PayloadBuilder{ + 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, @@ -80,6 +80,11 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { TotalGas: uint64(totalGas), // nolint:gosec Txs: payloadTxs, }.Build() + // This should never happen: we construct the payload from correctly sized data. + if err!=nil { + panic(fmt.Errorf("PayloadBuilder{}.Build(): %w",err)) + } + return payload } // nextPayload constructs the payload for the next block. diff --git a/sei-tendermint/internal/autobahn/types/block.go b/sei-tendermint/internal/autobahn/types/block.go index 6a871fd7bd..73c54dc556 100644 --- a/sei-tendermint/internal/autobahn/types/block.go +++ b/sei-tendermint/internal/autobahn/types/block.go @@ -72,6 +72,27 @@ func (h *BlockHeader) Verify(c *Committee) error { return nil } +const standardTxBytes uint64 = 1024 + +// Maximum number of transactions in a block. +const MaxTxsPerBlock uint64 = 2000 +// Maximum total size of all the transactions. +// It can be split arbitrarily across transactions (1 large, 2000 small ones, etc.) +// up to MaxTxsPerBlock limit. +const MaxTxsBytesPerBlock = MaxTxsPerBlock * standardTxBytes + +// Upper bound on the block proto encoding. +var MaxBlockProtoSize = func() uint64 { + // Payload.Txs represents the variable part of the Block size. + // Proto size is maximized if we distribute data evenly across transactions. + tx := make([]byte,standardTxBytes) + txs := make([][]byte,MaxTxsPerBlock) + for i := range txs { txs[i] = tx } + // Crude estimate of all other fields. + const otherFields = 100 * 1024 + return otherFields + uint64(protoutils.Size(&pb.Block{Payload:&pb.Payload{Txs:txs}})) +}() + // Block . type Block struct { utils.ReadOnly @@ -149,7 +170,19 @@ type Payload struct { } // Build builds the Payload. -func (b PayloadBuilder) Build() *Payload { return &Payload{p: b} } +func (b PayloadBuilder) Build() (*Payload,error) { + if uint64(len(b.Txs)) > MaxTxsPerBlock { + return nil,fmt.Errorf("too many transactions") + } + total := uint64(0) + for _,tx := range b.Txs { + total += uint64(len(tx)) + } + if total > MaxTxsBytesPerBlock { + return nil, fmt.Errorf("total txs bytes too large") + } + return &Payload{p: b},nil +} // ToBuilder converts the Payload to a PayloadBuilder. func (p *Payload) ToBuilder() PayloadBuilder { return p.p } @@ -245,7 +278,7 @@ var PayloadConv = protoutils.Conv[*Payload, *pb.Payload]{ Coinbase: p.Coinbase, Basefee: *p.Basefee, Txs: p.Txs, - }.Build(), nil + }.Build() }, } diff --git a/sei-tendermint/internal/autobahn/types/testonly.go b/sei-tendermint/internal/autobahn/types/testonly.go index 9a920dbe94..11a126d659 100644 --- a/sei-tendermint/internal/autobahn/types/testonly.go +++ b/sei-tendermint/internal/autobahn/types/testonly.go @@ -90,14 +90,14 @@ func GenBlockHeader(rng utils.Rng) *BlockHeader { // GenPayload generates a random Payload. func GenPayload(rng utils.Rng) *Payload { - return PayloadBuilder{ + return utils.OrPanic1(PayloadBuilder{ CreatedAt: utils.GenTimestamp(rng), TotalGas: rng.Uint64(), EdgeCount: rng.Int63(), Coinbase: utils.GenBytes(rng, 10), Basefee: rng.Int63(), Txs: utils.GenSlice(rng, func(rng utils.Rng) []byte { return utils.GenBytes(rng, 10) }), - }.Build() + }.Build()) } // GenBlock generates a random Block. diff --git a/sei-tendermint/internal/p2p/giga/api.go b/sei-tendermint/internal/p2p/giga/api.go index 109682a486..6a8d0b956f 100644 --- a/sei-tendermint/internal/p2p/giga/api.go +++ b/sei-tendermint/internal/p2p/giga/api.go @@ -2,6 +2,7 @@ package giga import ( apb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/pb" + "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" pb "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/giga/pb" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc" ) @@ -21,7 +22,10 @@ var StreamLaneProposals = rpc.Register[API]( 1, rpc.Limit{Rate: 1, Concurrent: 1}, rpc.Msg[*pb.StreamLaneProposalsReq]{MsgSize: kB, Window: 1}, - rpc.Msg[*pb.LaneProposal]{MsgSize: 2 * MB, Window: 5}, + rpc.Msg[*pb.LaneProposal]{ + MsgSize: rpc.InBytes(types.MaxBlockProtoSize) + 10 * kB, + Window: 5, + }, ) var StreamLaneVotes = rpc.Register[API]( 2, @@ -65,5 +69,8 @@ var StreamFullCommitQCs = rpc.Register[API](7, var GetBlock = rpc.Register[API](8, rpc.Limit{Rate: 10, Concurrent: 10}, rpc.Msg[*pb.GetBlockReq]{MsgSize: 10 * kB, Window: 1}, - rpc.Msg[*pb.GetBlockResp]{MsgSize: 2 * MB, Window: 1}, + rpc.Msg[*pb.GetBlockResp]{ + MsgSize: rpc.InBytes(types.MaxBlockProtoSize) + 10 * kB, + Window: 1, + }, ) diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index 9ca2279cd3..65a381ac59 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -168,7 +168,10 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { next := last + 1 if last == 0 { if _, err := app.InitChain(ctx, r.cfg.GenDoc.ToRequestInitChain()); err != nil { - return fmt.Errorf("App.InitChain(): %w", err) + return fmt.Errorf("app.InitChain(): %w", err) + } + if _, err := app.Commit(ctx); err!=nil { + return fmt.Errorf("app.Commit(): %w",err) } var ok bool next, ok = utils.SafeCast[atypes.GlobalBlockNumber](r.cfg.GenDoc.InitialHeight) @@ -186,9 +189,12 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { for n := next; ; n += 1 { b, err := r.data.GlobalBlock(ctx, n) if err != nil { - return err + return fmt.Errorf("r.data.GlobalBlock(%v): %w",n,err) } commitResp, err := r.executeBlock(ctx, b) + if err!=nil { + return fmt.Errorf("r.executeBlock(%v): %w",n,err) + } pruneBefore, ok := utils.SafeCast[atypes.GlobalBlockNumber](commitResp.RetainHeight) if !ok { return fmt.Errorf("invalid commitResp.RetainHeight = %v", commitResp.RetainHeight) diff --git a/sei-tendermint/internal/protoutils/msg.go b/sei-tendermint/internal/protoutils/msg.go index d71d849238..892a2973c7 100644 --- a/sei-tendermint/internal/protoutils/msg.go +++ b/sei-tendermint/internal/protoutils/msg.go @@ -16,6 +16,11 @@ func New[T Message]() T { return utils.Zero[T]().ProtoReflect().New().Interface().(T) } +// Computes the size of the message encoding. +func Size[T Message](t T) int { + return proto.Size(t) +} + func Marshal[T Message](t T) []byte { // Marshalling messages is always expected to succeed. return utils.OrPanic1(proto.Marshal(t)) From 121b2e4e4b4fe53114c9961439042479f9f27c4f Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Tue, 14 Apr 2026 17:11:21 +0200 Subject: [PATCH 40/43] applied comments --- .../internal/autobahn/producer/state.go | 21 ++++++------ .../internal/autobahn/types/block.go | 23 +++++++------ sei-tendermint/internal/mempool/mempool.go | 32 +++++++++++++------ sei-tendermint/internal/p2p/giga/api.go | 8 ++--- sei-tendermint/internal/p2p/giga_router.go | 15 +++++---- .../internal/p2p/giga_router_test.go | 13 ++++++++ 6 files changed, 72 insertions(+), 40 deletions(-) diff --git a/sei-tendermint/internal/autobahn/producer/state.go b/sei-tendermint/internal/autobahn/producer/state.go index b1c153a5ea..649c079bd8 100644 --- a/sei-tendermint/internal/autobahn/producer/state.go +++ b/sei-tendermint/internal/autobahn/producer/state.go @@ -61,28 +61,27 @@ func (s *State) makePayload(ctx context.Context) *types.Payload { } } - txs, totalGas := s.txMempool.ReapMaxTxsBytesMaxGas( - min(types.MaxTxsPerBlock,s.cfg.maxTxsPerBlock()), - utils.Clamp[int64](types.MaxTxsBytesPerBlock), - utils.Clamp[int64](s.cfg.MaxGasPerBlock), - utils.Clamp[int64](s.cfg.MaxGasPerBlock), - ) - s.txMempool.RemoveTxs(txs) + txs, gasEstimated := s.txMempool.PopTxs(mempool.ReapLimits{ + MaxTxs: utils.Some(min(types.MaxTxsPerBlock, s.cfg.maxTxsPerBlock())), + MaxBytes: utils.Some(utils.Clamp[int64](types.MaxTxsBytesPerBlock)), + MaxGasWanted: utils.Some(utils.Clamp[int64](s.cfg.MaxGasPerBlock)), + MaxGasEstimated: utils.Some(utils.Clamp[int64](s.cfg.MaxGasPerBlock)), + }) payloadTxs := make([][]byte, 0, len(txs)) for _, tx := range txs { payloadTxs = append(payloadTxs, tx) } - payload,err := types.PayloadBuilder{ + 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(totalGas), // nolint:gosec + TotalGas: uint64(gasEstimated), // nolint:gosec Txs: payloadTxs, }.Build() // This should never happen: we construct the payload from correctly sized data. - if err!=nil { - panic(fmt.Errorf("PayloadBuilder{}.Build(): %w",err)) + if err != nil { + panic(fmt.Errorf("PayloadBuilder{}.Build(): %w", err)) } return payload } diff --git a/sei-tendermint/internal/autobahn/types/block.go b/sei-tendermint/internal/autobahn/types/block.go index 73c54dc556..8f39301bb9 100644 --- a/sei-tendermint/internal/autobahn/types/block.go +++ b/sei-tendermint/internal/autobahn/types/block.go @@ -74,8 +74,9 @@ func (h *BlockHeader) Verify(c *Committee) error { const standardTxBytes uint64 = 1024 -// Maximum number of transactions in a block. +// Maximum number of transactions in a block. const MaxTxsPerBlock uint64 = 2000 + // Maximum total size of all the transactions. // It can be split arbitrarily across transactions (1 large, 2000 small ones, etc.) // up to MaxTxsPerBlock limit. @@ -84,13 +85,15 @@ const MaxTxsBytesPerBlock = MaxTxsPerBlock * standardTxBytes // Upper bound on the block proto encoding. var MaxBlockProtoSize = func() uint64 { // Payload.Txs represents the variable part of the Block size. - // Proto size is maximized if we distribute data evenly across transactions. - tx := make([]byte,standardTxBytes) - txs := make([][]byte,MaxTxsPerBlock) - for i := range txs { txs[i] = tx } + // Proto size is maximized if we distribute data evenly across transactions. + tx := make([]byte, standardTxBytes) + txs := make([][]byte, MaxTxsPerBlock) + for i := range txs { + txs[i] = tx + } // Crude estimate of all other fields. const otherFields = 100 * 1024 - return otherFields + uint64(protoutils.Size(&pb.Block{Payload:&pb.Payload{Txs:txs}})) + return otherFields + uint64(protoutils.Size(&pb.Block{Payload: &pb.Payload{Txs: txs}})) }() // Block . @@ -170,18 +173,18 @@ type Payload struct { } // Build builds the Payload. -func (b PayloadBuilder) Build() (*Payload,error) { +func (b PayloadBuilder) Build() (*Payload, error) { if uint64(len(b.Txs)) > MaxTxsPerBlock { - return nil,fmt.Errorf("too many transactions") + return nil, fmt.Errorf("too many transactions") } total := uint64(0) - for _,tx := range b.Txs { + for _, tx := range b.Txs { total += uint64(len(tx)) } if total > MaxTxsBytesPerBlock { return nil, fmt.Errorf("total txs bytes too large") } - return &Payload{p: b},nil + return &Payload{p: b}, nil } // ToBuilder converts the Payload to a PayloadBuilder. diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index b1493fdbec..1f14a6f374 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -499,10 +499,23 @@ func (txmp *TxMempool) Flush() { // - Transactions returned are not removed from the mempool transaction // store or indexes. func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimated int64) types.Txs { - txs, _ := txmp.ReapMaxTxsBytesMaxGas(utils.Max[uint64](), maxBytes, maxGasWanted, maxGasEstimated) + 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. @@ -510,7 +523,11 @@ func (txmp *TxMempool) ReapMaxBytesMaxGas(maxBytes, maxGasWanted, maxGasEstimate // 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) ReapMaxTxsBytesMaxGas(maxTxs uint64, maxBytes, maxGasWanted, maxGasEstimated int64) (types.Txs, int64) { +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]() } @@ -520,10 +537,6 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs uint64, maxBytes, maxGasWant if maxGasEstimated < 0 { maxGasEstimated = utils.Max[int64]() } - - txmp.mtx.Lock() - defer txmp.mtx.Unlock() - var ( totalGasWanted int64 totalGasEstimated int64 @@ -593,15 +606,16 @@ func (txmp *TxMempool) ReapMaxTxsBytesMaxGas(maxTxs uint64, maxBytes, maxGasWant } // RemoveTxs removes the provided transactions from the mempool if present. -func (txmp *TxMempool) RemoveTxs(txs types.Txs) { +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.Key()); wtx != nil { - txmp.removeTx(wtx, false, true, true) + txmp.removeTx(wtx, false, false, true) } } + return txs, gasEstimated } // ReapMaxTxs returns a list of transactions within the provided number of diff --git a/sei-tendermint/internal/p2p/giga/api.go b/sei-tendermint/internal/p2p/giga/api.go index 6a8d0b956f..84ef2b451f 100644 --- a/sei-tendermint/internal/p2p/giga/api.go +++ b/sei-tendermint/internal/p2p/giga/api.go @@ -23,8 +23,8 @@ var StreamLaneProposals = rpc.Register[API]( rpc.Limit{Rate: 1, Concurrent: 1}, rpc.Msg[*pb.StreamLaneProposalsReq]{MsgSize: kB, Window: 1}, rpc.Msg[*pb.LaneProposal]{ - MsgSize: rpc.InBytes(types.MaxBlockProtoSize) + 10 * kB, - Window: 5, + MsgSize: rpc.InBytes(types.MaxBlockProtoSize) + 10*kB, + Window: 5, }, ) var StreamLaneVotes = rpc.Register[API]( @@ -70,7 +70,7 @@ var GetBlock = rpc.Register[API](8, rpc.Limit{Rate: 10, Concurrent: 10}, rpc.Msg[*pb.GetBlockReq]{MsgSize: 10 * kB, Window: 1}, rpc.Msg[*pb.GetBlockResp]{ - MsgSize: rpc.InBytes(types.MaxBlockProtoSize) + 10 * kB, - Window: 1, + MsgSize: rpc.InBytes(types.MaxBlockProtoSize) + 10*kB, + Window: 1, }, ) diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index 65a381ac59..0438105c61 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -105,6 +105,7 @@ func (r *GigaRouter) executeBlock(ctx context.Context, b *atypes.GlobalBlock) (* proposerAddress = key.Address() } + // TODO: add metrics to understand execution latency. r.cfg.TxMempool.Lock() defer r.cfg.TxMempool.Unlock() @@ -128,7 +129,6 @@ func (r *GigaRouter) executeBlock(ctx context.Context, b *atypes.GlobalBlock) (* if err != nil { return nil, fmt.Errorf("r.cfg.App.FinalizeBlock(): %w", err) } - // TODO: we need the block to be persisted before we vote for apphash. if err := r.data.PushAppHash(ctx, b.GlobalNumber, resp.AppHash); err != nil { return nil, fmt.Errorf("r.data.PushAppHash(%v): %w", b.GlobalNumber, err) } @@ -145,6 +145,9 @@ func (r *GigaRouter) executeBlock(ctx context.Context, b *atypes.GlobalBlock) (* int64(b.GlobalNumber), // nolint:gosec // autobahn block numbers fit in int64. blockTxs, resp.TxResults, + // 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, true, ) @@ -170,8 +173,8 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { if _, err := app.InitChain(ctx, r.cfg.GenDoc.ToRequestInitChain()); err != nil { return fmt.Errorf("app.InitChain(): %w", err) } - if _, err := app.Commit(ctx); err!=nil { - return fmt.Errorf("app.Commit(): %w",err) + if _, err := app.Commit(ctx); err != nil { + return fmt.Errorf("app.Commit(): %w", err) } var ok bool next, ok = utils.SafeCast[atypes.GlobalBlockNumber](r.cfg.GenDoc.InitialHeight) @@ -189,11 +192,11 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { for n := next; ; n += 1 { b, err := r.data.GlobalBlock(ctx, n) if err != nil { - return fmt.Errorf("r.data.GlobalBlock(%v): %w",n,err) + return fmt.Errorf("r.data.GlobalBlock(%v): %w", n, err) } commitResp, err := r.executeBlock(ctx, b) - if err!=nil { - return fmt.Errorf("r.executeBlock(%v): %w",n,err) + if err != nil { + return fmt.Errorf("r.executeBlock(%v): %w", n, err) } pruneBefore, ok := utils.SafeCast[atypes.GlobalBlockNumber](commitResp.RetainHeight) if !ok { diff --git a/sei-tendermint/internal/p2p/giga_router_test.go b/sei-tendermint/internal/p2p/giga_router_test.go index 2403aaff15..6a168dc49c 100644 --- a/sei-tendermint/internal/p2p/giga_router_test.go +++ b/sei-tendermint/internal/p2p/giga_router_test.go @@ -37,6 +37,7 @@ type testAppState struct { Blocks []*abci.RequestFinalizeBlock Txs map[shaHash]bool AppHash shaHash + Committed bool } func testAppStateJSON(rng utils.Rng) json.RawMessage { @@ -102,6 +103,7 @@ func (a *testApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*abc state.Init = utils.Some(req) state.AppHash = sha256.Sum256(req.AppStateBytes) state.Validators = utils.Slice(val) + state.Committed = false ctrl.Updated() return &abci.ResponseInitChain{ AppHash: slices.Clone(state.AppHash[:]), @@ -113,6 +115,9 @@ func (a *testApp) InitChain(_ context.Context, req *abci.RequestInitChain) (*abc func (a *testApp) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { for state, ctrl := range a.state.Lock() { + if !state.Committed { + return nil, fmt.Errorf("not committed") + } init, ok := state.Init.Get() if !ok { return nil, fmt.Errorf("app not initialized") @@ -123,6 +128,7 @@ func (a *testApp) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBloc state.Txs[sha256.Sum256(tx)] = true } logger.Info("FinalizeBlock", "n", req.Header.Height-init.InitialHeight) + state.Committed = false ctrl.Updated() return &abci.ResponseFinalizeBlock{ AppHash: slices.Clone(state.AppHash[:]), @@ -133,6 +139,13 @@ func (a *testApp) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBloc } func (a *testApp) Commit(context.Context) (*abci.ResponseCommit, error) { + for state, ctrl := range a.state.Lock() { + if state.Committed { + return nil, fmt.Errorf("double commit") + } + state.Committed = true + ctrl.Updated() + } return &abci.ResponseCommit{ // Don't prune anything. RetainHeight: 0, From d217ee0c07148be21d4ea36b146cb465d98b84d3 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Tue, 14 Apr 2026 17:31:06 +0200 Subject: [PATCH 41/43] moved config --- sei-tendermint/internal/mempool/mempool.go | 100 +++++++++++++++++- .../internal/mempool/mempool_test.go | 36 +++++-- .../internal/mempool/reactor/reactor.go | 6 +- sei-tendermint/internal/mempool/tx.go | 5 +- sei-tendermint/internal/mempool/tx_test.go | 11 +- 5 files changed, 132 insertions(+), 26 deletions(-) diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index 1f14a6f374..d97eaccc5b 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -12,7 +12,6 @@ import ( "time" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "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/libs/utils" @@ -46,13 +45,106 @@ const ( MinGasEVMTx = 21000 ) +type Config struct { + // Maximum number of transactions in the mempool + Size int + + // Limit the total size of all txs in the mempool. + // This only accounts for raw transactions (e.g. given 1MB transactions and + // max-txs-bytes=5MB, mempool will only accept 5 transactions). + MaxTxsBytes int64 + + // Size of the cache (used to filter transactions we saw earlier) in transactions + CacheSize int + + // Size of the duplicate cache used to track duplicate txs + DuplicateTxsCacheSize int + + // Do not remove invalid transactions from the cache (default: false) + // Set to true if it's not possible for any invalid transaction to become + // valid again in the future. + KeepInvalidTxsInCache bool + + // Maximum size of a single transaction + // 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 + + // 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 + + // TxNotifyThreshold, if non-zero, defines the minimum number of transactions + // needed to trigger a notification in mempool's Tx notifier + TxNotifyThreshold uint64 + + // Maximum number of transactions in the pending set + PendingSize int + + // Limit the total size of all txs in the pending set. + MaxPendingTxsBytes int64 + + RemoveExpiredTxsFromQueue bool + + // DropPriorityThreshold defines the percentage of transactions with the lowest + // priority hint (expressed as a float in the range [0.0, 1.0]) that will be + // dropped from the mempool once the configured utilisation threshold is reached. + // + // The default value of 0.1 means that the lowest 10% of transactions by + // priority will be dropped when the mempool utilisation exceeds the + // DropUtilisationThreshold. + // + // See DropUtilisationThreshold. + DropPriorityThreshold float64 + + // DropUtilisationThreshold defines the mempool utilisation level (expressed as + // a percentage in the range [0.0, 1.0]) above which transactions will be + // selectively dropped based on their priority hint. + // + // For example, if this parameter is set to 0.8, then once the mempool reaches + // 80% capacity, transactions with priority hints below DropPriorityThreshold + // percentile will be dropped to make room for new transactions. + DropUtilisationThreshold float64 + + // DropPriorityReservoirSize defines the size of the reservoir for keeping track + // of the distribution of transaction priorities in the mempool. + // + // This is used to determine the priority threshold below which transactions will + // be dropped when the mempool utilisation exceeds DropUtilisationThreshold. + // + // The reservoir is a statistically representative sample of transaction + // priorities in the mempool, and is used to estimate the priority distribution + // without needing to store all transaction priorities. + // + // A larger reservoir size will yield a more accurate estimate of the priority + // distribution, but will consume more memory. + // + // The default value of 10,240 is a reasonable compromise between accuracy and + // memory usage for most use cases. It takes approximately 80KB of memory storing + // int64 transaction priorities. + // + // See DropUtilisationThreshold and DropPriorityThreshold. + DropPriorityReservoirSize int `mapstructure:"drop-priority-reservoir-size"` +} + // TxMempool defines a prioritized mempool data structure used by the v1 mempool // reactor. It keeps a thread-safe priority queue of transactions that is used // when a block proposer constructs a block and a thread-safe linked-list that // is used to gossip transactions to peers in a FIFO manner. type TxMempool struct { metrics *Metrics - config *config.MempoolConfig + config *Config app abci.Application // txsAvailable fires once for each height when the mempool is not empty @@ -126,7 +218,7 @@ type TxMempool struct { } func NewTxMempool( - cfg *config.MempoolConfig, + cfg *Config, app abci.Application, metrics *Metrics, txConstraintsFetcher TxConstraintsFetcher, @@ -161,7 +253,7 @@ func NewTxMempool( return txmp } -func (txmp *TxMempool) Config() *config.MempoolConfig { return txmp.config } +func (txmp *TxMempool) Config() *Config { return txmp.config } func (txmp *TxMempool) App() abci.Application { return txmp.app } diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index a4d0d0799a..ef4035ba24 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "math/rand" - "os" "sort" "strconv" "strings" @@ -18,7 +17,6 @@ import ( "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/code" "github.com/sei-protocol/sei-chain/sei-tendermint/abci/example/kvstore" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" ) @@ -170,17 +168,35 @@ func (app *application) GetTxPriorityHint(context.Context, *abci.RequestGetTxPri }, nil } -func setup(t testing.TB, app abci.Application, cacheSize int, txConstraintsFetcher TxConstraintsFetcher) *TxMempool { - t.Helper() +func defaultCfg() *Config { + return &Config{ + // Each signature verification takes .5ms, Size reduced until we implement + // ABCI Recheck + Size: 5000, + 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 + TxNotifyThreshold: 0, + PendingSize: 5000, + MaxPendingTxsBytes: 1024 * 1024 * 1024, // 1GB + RemoveExpiredTxsFromQueue: true, + DropPriorityThreshold: 0.1, + DropUtilisationThreshold: 1.0, + DropPriorityReservoirSize: 10_240, + } +} - cfg, err := config.ResetTestRoot(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "|")) - require.NoError(t, err) - cfg.Mempool.CacheSize = cacheSize - cfg.Mempool.DropUtilisationThreshold = 0.0 // disable dropping by priority hint to allow testing eviction logic - t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) +func setup(t testing.TB, app abci.Application, cacheSize int, txConstraintsFetcher TxConstraintsFetcher) *TxMempool { + t.Helper() - return NewTxMempool(cfg.Mempool, app, NopMetrics(), txConstraintsFetcher) + cfg := defaultCfg() + cfg.CacheSize = cacheSize + cfg.DropUtilisationThreshold = 0.0 // disable dropping by priority hint to allow testing eviction logic + return NewTxMempool(cfg, app, NopMetrics(), txConstraintsFetcher) } func checkTxs(ctx context.Context, t *testing.T, txmp *TxMempool, numTxs int, peerID uint16) []testTx { diff --git a/sei-tendermint/internal/mempool/reactor/reactor.go b/sei-tendermint/internal/mempool/reactor/reactor.go index 41b296eba8..bdc8dc6a88 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor.go +++ b/sei-tendermint/internal/mempool/reactor/reactor.go @@ -44,13 +44,13 @@ type Reactor struct { } // NewReactor returns a reference to a new reactor. -func NewReactor(txmp *mempool.TxMempool, router *p2p.Router) (*Reactor, error) { - channel, err := p2p.OpenChannel(router, GetChannelDescriptor(txmp.Config())) +func NewReactor(cfg *config.MempoolConfig, txmp *mempool.TxMempool, router *p2p.Router) (*Reactor, error) { + channel, err := p2p.OpenChannel(router, GetChannelDescriptor(cfg)) if err != nil { return nil, fmt.Errorf("router.OpenChannel(): %w", err) } r := &Reactor{ - cfg: txmp.Config(), + cfg: cfg, mempool: txmp, ids: NewMempoolIDs(), router: router, diff --git a/sei-tendermint/internal/mempool/tx.go b/sei-tendermint/internal/mempool/tx.go index e19985f84b..3b632e41b4 100644 --- a/sei-tendermint/internal/mempool/tx.go +++ b/sei-tendermint/internal/mempool/tx.go @@ -6,7 +6,6 @@ import ( "time" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/libs/clist" "github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils" "github.com/sei-protocol/sei-chain/sei-tendermint/types" @@ -312,7 +311,7 @@ func (wtl *WrappedTxList) Purge(minTime utils.Option[time.Time], minHeight utils type PendingTxs struct { mtx *sync.RWMutex txs []TxWithResponse - config *config.MempoolConfig + config *Config sizeBytes uint64 } @@ -322,7 +321,7 @@ type TxWithResponse struct { txInfo TxInfo } -func NewPendingTxs(conf *config.MempoolConfig) *PendingTxs { +func NewPendingTxs(conf *Config) *PendingTxs { return &PendingTxs{ mtx: &sync.RWMutex{}, txs: []TxWithResponse{}, diff --git a/sei-tendermint/internal/mempool/tx_test.go b/sei-tendermint/internal/mempool/tx_test.go index e4e52a53b0..51757412ed 100644 --- a/sei-tendermint/internal/mempool/tx_test.go +++ b/sei-tendermint/internal/mempool/tx_test.go @@ -7,7 +7,6 @@ import ( "time" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "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" @@ -200,9 +199,9 @@ func TestTxStore_Size(t *testing.T) { txStore := NewTxStore() numTxs := 1000 - for i := 0; i < numTxs; i++ { + for i := range numTxs { txStore.SetTx(&WrappedTx{ - tx: []byte(fmt.Sprintf("test_tx_%d", i)), + tx: fmt.Appendf(nil,"test_tx_%d", i), priority: int64(i), timestamp: time.Now(), }) @@ -271,7 +270,7 @@ func TestWrappedTxList(t *testing.T) { } func TestPendingTxsPopTxsGood(t *testing.T) { - pendingTxs := NewPendingTxs(config.TestMempoolConfig()) + pendingTxs := NewPendingTxs(defaultCfg()) for _, test := range []struct { origLen int popIndices []int @@ -334,7 +333,7 @@ func TestPendingTxsPopTxsGood(t *testing.T) { } func TestPendingTxsPopTxsBad(t *testing.T) { - pendingTxs := NewPendingTxs(config.TestMempoolConfig()) + pendingTxs := NewPendingTxs(defaultCfg()) // out of range require.Panics(t, func() { pendingTxs.popTxsAtIndices([]int{0}) }) // out of order @@ -345,7 +344,7 @@ func TestPendingTxsPopTxsBad(t *testing.T) { } func TestPendingTxs_InsertCondition(t *testing.T) { - mempoolCfg := config.TestMempoolConfig() + mempoolCfg := defaultCfg() // First test exceeding number of txs mempoolCfg.PendingSize = 2 From 5ed9b06862cf61cc2c006dc1779156367a4a04c7 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Tue, 14 Apr 2026 17:42:02 +0200 Subject: [PATCH 42/43] extracted config --- sei-tendermint/config/config.go | 63 +++++++++++++------ .../internal/blocksync/reactor_test.go | 2 +- .../internal/consensus/common_test.go | 2 +- .../internal/consensus/reactor_test.go | 2 +- sei-tendermint/internal/consensus/replay.go | 2 +- sei-tendermint/internal/mempool/mempool.go | 21 +++++++ .../internal/mempool/mempool_test.go | 25 +------- .../internal/mempool/reactor/reactor_test.go | 8 +-- sei-tendermint/internal/mempool/testonly.go | 8 +++ sei-tendermint/internal/mempool/tx_test.go | 8 +-- .../internal/p2p/giga_router_test.go | 3 +- sei-tendermint/internal/p2p/router_test.go | 3 +- sei-tendermint/internal/state/helpers_test.go | 5 +- sei-tendermint/node/node.go | 4 +- sei-tendermint/node/node_test.go | 6 +- sei-tendermint/node/setup_test.go | 2 +- .../test/fuzz/tests/mempool_test.go | 2 +- 17 files changed, 95 insertions(+), 71 deletions(-) create mode 100644 sei-tendermint/internal/mempool/testonly.go diff --git a/sei-tendermint/config/config.go b/sei-tendermint/config/config.go index 692ca922c5..16b45df709 100644 --- a/sei-tendermint/config/config.go +++ b/sei-tendermint/config/config.go @@ -11,6 +11,7 @@ import ( "strings" "time" + 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/types" ) @@ -857,38 +858,60 @@ type MempoolConfig struct { DropPriorityReservoirSize int `mapstructure:"drop-priority-reservoir-size"` } +func (cfg *MempoolConfig) ToMempoolConfig() *mempoolcfg.Config { + return &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, + RemoveExpiredTxsFromQueue: cfg.RemoveExpiredTxsFromQueue, + DropPriorityThreshold: cfg.DropPriorityThreshold, + DropUtilisationThreshold: cfg.DropUtilisationThreshold, + DropPriorityReservoirSize: cfg.DropPriorityReservoirSize, + } +} + // DefaultMempoolConfig returns a default configuration for the Tendermint mempool. func DefaultMempoolConfig() *MempoolConfig { + cfg := mempoolcfg.DefaultConfig() return &MempoolConfig{ - Broadcast: true, - // Each signature verification takes .5ms, Size reduced until we implement - // ABCI Recheck - Size: 5000, - 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 - TxNotifyThreshold: 0, + Broadcast: true, + Size: cfg.Size, + MaxTxsBytes: cfg.MaxTxsBytes, + CacheSize: cfg.CacheSize, + DuplicateTxsCacheSize: cfg.DuplicateTxsCacheSize, + KeepInvalidTxsInCache: cfg.KeepInvalidTxsInCache, + MaxTxBytes: cfg.MaxTxBytes, + MaxBatchBytes: 0, + TTLDuration: cfg.TTLDuration, + TTLNumBlocks: cfg.TTLNumBlocks, + TxNotifyThreshold: cfg.TxNotifyThreshold, CheckTxErrorBlacklistEnabled: true, CheckTxErrorThreshold: 50, - PendingSize: 5000, - MaxPendingTxsBytes: 1024 * 1024 * 1024, // 1GB - PendingTTLDuration: 0 * time.Second, + PendingSize: cfg.PendingSize, + MaxPendingTxsBytes: cfg.MaxPendingTxsBytes, + PendingTTLDuration: 0, PendingTTLNumBlocks: 0, - RemoveExpiredTxsFromQueue: true, - DropPriorityThreshold: 0.1, - DropUtilisationThreshold: 1.0, - DropPriorityReservoirSize: 10_240, + RemoveExpiredTxsFromQueue: cfg.RemoveExpiredTxsFromQueue, + DropPriorityThreshold: cfg.DropPriorityThreshold, + DropUtilisationThreshold: cfg.DropUtilisationThreshold, + DropPriorityReservoirSize: cfg.DropPriorityReservoirSize, } } // TestMempoolConfig returns a configuration for testing the Tendermint mempool func TestMempoolConfig() *MempoolConfig { cfg := DefaultMempoolConfig() - cfg.CacheSize = 1000 - cfg.DropUtilisationThreshold = 0.0 + testCfg := mempoolcfg.TestConfig() + cfg.CacheSize = testCfg.CacheSize + cfg.DropUtilisationThreshold = testCfg.DropUtilisationThreshold return cfg } diff --git a/sei-tendermint/internal/blocksync/reactor_test.go b/sei-tendermint/internal/blocksync/reactor_test.go index ef63c01e23..41eaa385b0 100644 --- a/sei-tendermint/internal/blocksync/reactor_test.go +++ b/sei-tendermint/internal/blocksync/reactor_test.go @@ -93,7 +93,7 @@ func makeReactor( state, err := sm.MakeGenesisState(genDoc) require.NoError(t, err) require.NoError(t, stateStore.Save(state)) - mp := mempool.NewTxMempool(config.TestMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) + mp := mempool.NewTxMempool(mempool.TestConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) eventbus := eventbus.NewDefault() require.NoError(t, eventbus.Start(ctx)) diff --git a/sei-tendermint/internal/consensus/common_test.go b/sei-tendermint/internal/consensus/common_test.go index da97918b42..76365cb628 100644 --- a/sei-tendermint/internal/consensus/common_test.go +++ b/sei-tendermint/internal/consensus/common_test.go @@ -465,7 +465,7 @@ func newStateWithConfigAndBlockStore( // Make Mempool mempool := mempool.NewTxMempool( - thisConfig.Mempool, + thisConfig.Mempool.ToMempoolConfig(), proxyAppConnMem, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher, diff --git a/sei-tendermint/internal/consensus/reactor_test.go b/sei-tendermint/internal/consensus/reactor_test.go index 4c1cffd5ed..05f18b2774 100644 --- a/sei-tendermint/internal/consensus/reactor_test.go +++ b/sei-tendermint/internal/consensus/reactor_test.go @@ -273,7 +273,7 @@ func TestReactorWithEvidence(t *testing.T) { proxyAppConnCon := app mempool := mempool.NewTxMempool( - thisConfig.Mempool, + thisConfig.Mempool.ToMempoolConfig(), proxyAppConnMem, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher, diff --git a/sei-tendermint/internal/consensus/replay.go b/sei-tendermint/internal/consensus/replay.go index f4d8040b78..bb4895ca4d 100644 --- a/sei-tendermint/internal/consensus/replay.go +++ b/sei-tendermint/internal/consensus/replay.go @@ -135,7 +135,7 @@ func NewHandshaker( } func newReplayTxMempool(appClient abci.Application) *mempool.TxMempool { - return mempool.NewTxMempool(config.DefaultMempoolConfig(), appClient, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) + return mempool.NewTxMempool(config.DefaultMempoolConfig().ToMempoolConfig(), appClient, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) } // NBlocks returns the number of blocks applied to the state. diff --git a/sei-tendermint/internal/mempool/mempool.go b/sei-tendermint/internal/mempool/mempool.go index d97eaccc5b..20416b2c15 100644 --- a/sei-tendermint/internal/mempool/mempool.go +++ b/sei-tendermint/internal/mempool/mempool.go @@ -138,6 +138,27 @@ type Config struct { DropPriorityReservoirSize int `mapstructure:"drop-priority-reservoir-size"` } +func DefaultConfig() *Config { + return &Config{ + // Each signature verification takes .5ms, Size reduced until we implement + // ABCI Recheck + Size: 5000, + 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 + TxNotifyThreshold: 0, + PendingSize: 5000, + MaxPendingTxsBytes: 1024 * 1024 * 1024, // 1GB + RemoveExpiredTxsFromQueue: true, + DropPriorityThreshold: 0.1, + DropUtilisationThreshold: 1.0, + DropPriorityReservoirSize: 10_240, + } +} + // TxMempool defines a prioritized mempool data structure used by the v1 mempool // reactor. It keeps a thread-safe priority queue of transactions that is used // when a block proposer constructs a block and a thread-safe linked-list that diff --git a/sei-tendermint/internal/mempool/mempool_test.go b/sei-tendermint/internal/mempool/mempool_test.go index ef4035ba24..bcb62c9314 100644 --- a/sei-tendermint/internal/mempool/mempool_test.go +++ b/sei-tendermint/internal/mempool/mempool_test.go @@ -168,34 +168,11 @@ func (app *application) GetTxPriorityHint(context.Context, *abci.RequestGetTxPri }, nil } -func defaultCfg() *Config { - return &Config{ - // Each signature verification takes .5ms, Size reduced until we implement - // ABCI Recheck - Size: 5000, - 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 - TxNotifyThreshold: 0, - PendingSize: 5000, - MaxPendingTxsBytes: 1024 * 1024 * 1024, // 1GB - RemoveExpiredTxsFromQueue: true, - DropPriorityThreshold: 0.1, - DropUtilisationThreshold: 1.0, - DropPriorityReservoirSize: 10_240, - } -} - - func setup(t testing.TB, app abci.Application, cacheSize int, txConstraintsFetcher TxConstraintsFetcher) *TxMempool { t.Helper() - cfg := defaultCfg() + cfg := TestConfig() cfg.CacheSize = cacheSize - cfg.DropUtilisationThreshold = 0.0 // disable dropping by priority hint to allow testing eviction logic return NewTxMempool(cfg, app, NopMetrics(), txConstraintsFetcher) } diff --git a/sei-tendermint/internal/mempool/reactor/reactor_test.go b/sei-tendermint/internal/mempool/reactor/reactor_test.go index 9a6bb5ac3e..2506cef75a 100644 --- a/sei-tendermint/internal/mempool/reactor/reactor_test.go +++ b/sei-tendermint/internal/mempool/reactor/reactor_test.go @@ -50,7 +50,7 @@ func setupMempool(t testing.TB, app abci.Application, cacheSize int, txConstrain t.Cleanup(func() { os.RemoveAll(cfg.RootDir) }) - return mempool.NewTxMempool(cfg.Mempool, app, mempool.NopMetrics(), txConstraintsFetcher) + 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 { @@ -117,7 +117,7 @@ func setupReactorsWithTxConstraintsFetchers( txmp := setupMempool(t, app, 0, txConstraintsFetcher) rts.mempools[nodeID] = txmp - reactor, err := NewReactor(txmp, node.Router) + reactor, err := NewReactor(config.TestMempoolConfig(), txmp, node.Router) if err != nil { t.Fatalf("NewReactor(): %v", err) } @@ -150,8 +150,8 @@ func setupReactorForTest(t *testing.T, txConstraintsFetcher mempool.TxConstraint network := p2p.MakeTestNetwork(t, p2p.TestNetworkOptions{NumNodes: 1}) node := network.Nodes()[0] - txmp := mempool.NewTxMempool(cfg.Mempool, kvstore.NewApplication(), mempool.NopMetrics(), txConstraintsFetcher) - reactor, err := NewReactor(txmp, node.Router) + txmp := mempool.NewTxMempool(cfg.Mempool.ToMempoolConfig(), kvstore.NewApplication(), mempool.NopMetrics(), txConstraintsFetcher) + reactor, err := NewReactor(cfg.Mempool, txmp, node.Router) require.NoError(t, err) reactor.MarkReadyToStart() require.NoError(t, reactor.Start(t.Context())) diff --git a/sei-tendermint/internal/mempool/testonly.go b/sei-tendermint/internal/mempool/testonly.go new file mode 100644 index 0000000000..2decfd0e9f --- /dev/null +++ b/sei-tendermint/internal/mempool/testonly.go @@ -0,0 +1,8 @@ +package mempool + +func TestConfig() *Config { + cfg := DefaultConfig() + cfg.CacheSize = 1000 + cfg.DropUtilisationThreshold = 0.0 + return cfg +} diff --git a/sei-tendermint/internal/mempool/tx_test.go b/sei-tendermint/internal/mempool/tx_test.go index 51757412ed..5ccc36b613 100644 --- a/sei-tendermint/internal/mempool/tx_test.go +++ b/sei-tendermint/internal/mempool/tx_test.go @@ -201,7 +201,7 @@ func TestTxStore_Size(t *testing.T) { for i := range numTxs { txStore.SetTx(&WrappedTx{ - tx: fmt.Appendf(nil,"test_tx_%d", i), + tx: fmt.Appendf(nil, "test_tx_%d", i), priority: int64(i), timestamp: time.Now(), }) @@ -270,7 +270,7 @@ func TestWrappedTxList(t *testing.T) { } func TestPendingTxsPopTxsGood(t *testing.T) { - pendingTxs := NewPendingTxs(defaultCfg()) + pendingTxs := NewPendingTxs(DefaultConfig()) for _, test := range []struct { origLen int popIndices []int @@ -333,7 +333,7 @@ func TestPendingTxsPopTxsGood(t *testing.T) { } func TestPendingTxsPopTxsBad(t *testing.T) { - pendingTxs := NewPendingTxs(defaultCfg()) + pendingTxs := NewPendingTxs(DefaultConfig()) // out of range require.Panics(t, func() { pendingTxs.popTxsAtIndices([]int{0}) }) // out of order @@ -344,7 +344,7 @@ func TestPendingTxsPopTxsBad(t *testing.T) { } func TestPendingTxs_InsertCondition(t *testing.T) { - mempoolCfg := defaultCfg() + mempoolCfg := DefaultConfig() // First test exceeding number of txs mempoolCfg.PendingSize = 2 diff --git a/sei-tendermint/internal/p2p/giga_router_test.go b/sei-tendermint/internal/p2p/giga_router_test.go index 6a168dc49c..c172583baf 100644 --- a/sei-tendermint/internal/p2p/giga_router_test.go +++ b/sei-tendermint/internal/p2p/giga_router_test.go @@ -14,7 +14,6 @@ import ( "golang.org/x/time/rate" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" @@ -222,7 +221,7 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) { nodeInfo.Network = genDoc.ChainID e := Endpoint{AddrPort: cfg.addr} app := newTestApp() - txMempool := mempool.NewTxMempool(config.TestMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) + txMempool := mempool.NewTxMempool(mempool.TestConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) router, err := NewRouter( NopMetrics(), cfg.nodeKey, diff --git a/sei-tendermint/internal/p2p/router_test.go b/sei-tendermint/internal/p2p/router_test.go index cec47f33b1..d6adaace71 100644 --- a/sei-tendermint/internal/p2p/router_test.go +++ b/sei-tendermint/internal/p2p/router_test.go @@ -15,7 +15,6 @@ import ( "golang.org/x/time/rate" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer" @@ -325,7 +324,7 @@ func TestRouter_GigaSetWhenConfigured(t *testing.T) { // Use intentionally non-default values to ensure config actually propagates. opts := makeRouterOptions() - txMempool := mempool.NewTxMempool(config.TestMempoolConfig(), abci.BaseApplication{}, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) + txMempool := mempool.NewTxMempool(mempool.TestConfig(), abci.BaseApplication{}, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) opts.Giga = utils.Some(&GigaRouterConfig{ DialInterval: 7 * time.Second, ValidatorAddrs: validatorAddrs, diff --git a/sei-tendermint/internal/state/helpers_test.go b/sei-tendermint/internal/state/helpers_test.go index 6ddc412218..eba72df804 100644 --- a/sei-tendermint/internal/state/helpers_test.go +++ b/sei-tendermint/internal/state/helpers_test.go @@ -11,7 +11,6 @@ import ( dbm "github.com/tendermint/tm-db" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" - "github.com/sei-protocol/sei-chain/sei-tendermint/config" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/ed25519" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/mempool" @@ -234,9 +233,7 @@ func randomGenesisDoc() *types.GenesisDoc { func makeTxMempool(t testing.TB, app abci.Application) *mempool.TxMempool { t.Helper() - cfg := config.TestMempoolConfig() - - return mempool.NewTxMempool(cfg, app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) + return mempool.NewTxMempool(mempool.TestConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) } // used for testing by state store diff --git a/sei-tendermint/node/node.go b/sei-tendermint/node/node.go index ef0dd89288..71953fa69e 100644 --- a/sei-tendermint/node/node.go +++ b/sei-tendermint/node/node.go @@ -190,7 +190,7 @@ func makeNode( makeCloser(closers)) } gigaEnabled := cfg.AutobahnConfigFile != "" - mp := mempool.NewTxMempool(cfg.Mempool, app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) + mp := mempool.NewTxMempool(cfg.Mempool.ToMempoolConfig(), app, nodeMetrics.mempool, sm.TxConstraintsFetcherFromStore(stateStore)) router, peerCloser, err := createRouter( nodeMetrics.p2p, node.NodeInfo, @@ -215,7 +215,7 @@ func makeNode( // Mempool gossiping is not compatible with Giga, // so we disable the mempool reactor. if !gigaEnabled { - mpReactor, err := mempoolreactor.NewReactor(mp, router) + mpReactor, err := mempoolreactor.NewReactor(cfg.Mempool, mp, router) if err != nil { return nil, fmt.Errorf("mempoolreactor.NewReactor(): %w", err) } diff --git a/sei-tendermint/node/node_test.go b/sei-tendermint/node/node_test.go index 4d2835b864..29ce64e507 100644 --- a/sei-tendermint/node/node_test.go +++ b/sei-tendermint/node/node_test.go @@ -285,7 +285,7 @@ func TestCreateProposalBlock(t *testing.T) { proposerAddr, _, ok := state.Validators.GetByIndex(0) require.True(t, ok) mp := mempool.NewTxMempool( - cfg.Mempool, + cfg.Mempool.ToMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher, @@ -382,7 +382,7 @@ func TestMaxTxsProposalBlockSize(t *testing.T) { // Make Mempool mp := mempool.NewTxMempool( - cfg.Mempool, + cfg.Mempool.ToMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher, @@ -447,7 +447,7 @@ func TestMaxProposalBlockSize(t *testing.T) { // Make Mempool mp := mempool.NewTxMempool( - cfg.Mempool, + cfg.Mempool.ToMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher, diff --git a/sei-tendermint/node/setup_test.go b/sei-tendermint/node/setup_test.go index 35faddfaa2..6c56940b0a 100644 --- a/sei-tendermint/node/setup_test.go +++ b/sei-tendermint/node/setup_test.go @@ -70,7 +70,7 @@ func defaultFileConfig(validators []config.AutobahnValidator) *config.AutobahnFi func makeTestGigaDeps() (*mempool.TxMempool, *types.GenesisDoc) { app := kvstore.NewApplication() txMempool := mempool.NewTxMempool( - config.TestMempoolConfig(), + mempool.TestConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher, diff --git a/sei-tendermint/test/fuzz/tests/mempool_test.go b/sei-tendermint/test/fuzz/tests/mempool_test.go index 5a7b351e88..2fb7efa9ca 100644 --- a/sei-tendermint/test/fuzz/tests/mempool_test.go +++ b/sei-tendermint/test/fuzz/tests/mempool_test.go @@ -16,7 +16,7 @@ func FuzzMempool(f *testing.F) { cfg := config.DefaultMempoolConfig() cfg.Broadcast = false - mp := mempool.NewTxMempool(cfg, app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) + mp := mempool.NewTxMempool(cfg.ToMempoolConfig(), app, mempool.NopMetrics(), mempool.NopTxConstraintsFetcher) f.Fuzz(func(t *testing.T, data []byte) { _ = mp.CheckTx(t.Context(), data, nil, mempool.TxInfo{}) From 6a2cd95188b31f0b9c26ea2dbbb34ee099df8d66 Mon Sep 17 00:00:00 2001 From: Grzegorz Prusak Date: Tue, 14 Apr 2026 18:37:11 +0200 Subject: [PATCH 43/43] deflake --- sei-tendermint/internal/p2p/giga_router_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sei-tendermint/internal/p2p/giga_router_test.go b/sei-tendermint/internal/p2p/giga_router_test.go index c172583baf..e7d590099e 100644 --- a/sei-tendermint/internal/p2p/giga_router_test.go +++ b/sei-tendermint/internal/p2p/giga_router_test.go @@ -167,6 +167,8 @@ func (a *testApp) Snapshot() testAppState { s := *state // Txs is derived and the only mutable field. s.Txs = nil + // "Committed" field is not guaranteed to be consistent. + s.Committed = false return s } panic("unreachable")