Skip to content

Commit a562bab

Browse files
Backport release/v6.4: feat(mempool): only allow one retry for failed txs (#3201)
Backport of #3200 to `release/v6.4`. Co-authored-by: Aayush Rajasekaran <arajasek94@gmail.com>
1 parent 551d8d5 commit a562bab

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

sei-tendermint/internal/mempool/mempool.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ type TxMempool struct {
6868
// reduces pressure on the proxyApp.
6969
cache TxCache
7070

71+
// blockFailedTxs tracks tx hashes that have previously failed during
72+
// block execution. Used to prevent infinite re-entry of txs that
73+
// consistently fail before fee charging in DeliverTx.
74+
blockFailedTxs TxCache
75+
7176
// A TTL cache which keeps all txs that we have seen before over the TTL window.
7277
// Currently, this can be used for tracking whether checkTx is always serving the same tx or not.
7378
duplicateTxsCache utils.Option[*DuplicateTxCache]
@@ -134,6 +139,7 @@ func NewTxMempool(
134139
proxyAppConn: proxyAppConn,
135140
height: -1,
136141
cache: NopTxCache{},
142+
blockFailedTxs: NopTxCache{},
137143
metrics: NopMetrics(),
138144
txStore: NewTxStore(),
139145
gossipIndex: clist.New(),
@@ -147,6 +153,7 @@ func NewTxMempool(
147153

148154
if cfg.CacheSize > 0 {
149155
txmp.cache = NewLRUTxCache(cfg.CacheSize, maxCacheKeySize)
156+
txmp.blockFailedTxs = NewLRUTxCache(cfg.CacheSize, maxCacheKeySize)
150157
}
151158

152159
for _, opt := range options {
@@ -666,9 +673,13 @@ func (txmp *TxMempool) Update(
666673
if execTxResult[i].Code == abci.CodeTypeOK {
667674
// add the valid committed transaction to the cache (if missing)
668675
_ = txmp.cache.Push(txKey)
676+
txmp.blockFailedTxs.Remove(txKey)
669677
} else if !txmp.config.KeepInvalidTxsInCache {
670-
// allow invalid transactions to be re-submitted
671-
txmp.cache.Remove(txKey)
678+
if txmp.blockFailedTxs.Push(txKey) {
679+
// First block failure: allow one retry
680+
txmp.cache.Remove(txKey)
681+
}
682+
// Subsequent failures: leave in cache to prevent infinite re-entry
672683
}
673684

674685
// remove the committed transaction from the transaction store and indexes

sei-tendermint/internal/mempool/mempool_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,3 +1183,97 @@ func TestReapMaxBytesMaxGas_EVMFirst(t *testing.T) {
11831183
require.True(t, strings.HasPrefix(string(reapedTxs[3]), "sender"), "Fourth tx should be non-EVM: %s", string(reapedTxs[3]))
11841184
require.True(t, strings.HasPrefix(string(reapedTxs[4]), "sender"), "Fifth tx should be non-EVM: %s", string(reapedTxs[4]))
11851185
}
1186+
1187+
func TestBlockFailedTxNotReAdmittedAfterSecondFailure(t *testing.T) {
1188+
ctx, cancel := context.WithCancel(context.Background())
1189+
defer cancel()
1190+
1191+
app := &application{Application: kvstore.NewApplication()}
1192+
txmp := setup(t, app, 500)
1193+
1194+
tx := types.Tx("sender-0-0=key=1000")
1195+
1196+
// Submit the tx — should enter the mempool
1197+
require.NoError(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}))
1198+
require.Equal(t, 1, txmp.Size())
1199+
1200+
// Simulate block inclusion where the tx fails (non-OK code)
1201+
txmp.Lock()
1202+
require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{
1203+
{Code: 11}, // out of gas
1204+
}, nil, nil, true))
1205+
txmp.Unlock()
1206+
1207+
// Tx should be removed from the mempool
1208+
require.Equal(t, 0, txmp.Size())
1209+
1210+
// First failure: tx should have been removed from cache, allowing re-entry
1211+
require.NoError(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}))
1212+
require.Equal(t, 1, txmp.Size())
1213+
1214+
// Simulate a second block failure for the same tx
1215+
txmp.Lock()
1216+
require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{
1217+
{Code: 11}, // out of gas again
1218+
}, nil, nil, true))
1219+
txmp.Unlock()
1220+
1221+
require.Equal(t, 0, txmp.Size())
1222+
1223+
// Second failure: tx should remain in cache — CheckTx should reject it
1224+
err := txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0})
1225+
require.Equal(t, types.ErrTxInCache, err)
1226+
require.Equal(t, 0, txmp.Size())
1227+
1228+
// A different tx (different hash) should still be admitted
1229+
differentTx := types.Tx("sender-0-0=key=2000")
1230+
require.NoError(t, txmp.CheckTx(ctx, differentTx, nil, TxInfo{SenderID: 0}))
1231+
require.Equal(t, 1, txmp.Size())
1232+
}
1233+
1234+
func TestBlockFailedTxTrackerClearedOnSuccess(t *testing.T) {
1235+
ctx, cancel := context.WithCancel(context.Background())
1236+
defer cancel()
1237+
1238+
app := &application{Application: kvstore.NewApplication()}
1239+
txmp := setup(t, app, 500)
1240+
1241+
tx := types.Tx("sender-0-0=key=1000")
1242+
txKey := tx.Key()
1243+
1244+
// Submit and fail once in a block
1245+
require.NoError(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}))
1246+
txmp.Lock()
1247+
require.NoError(t, txmp.Update(ctx, 1, types.Txs{tx}, []*abci.ExecTxResult{
1248+
{Code: 11},
1249+
}, nil, nil, true))
1250+
txmp.Unlock()
1251+
1252+
// Re-enter the mempool (first failure allows retry)
1253+
require.NoError(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}))
1254+
1255+
// This time the tx succeeds in the block
1256+
txmp.Lock()
1257+
require.NoError(t, txmp.Update(ctx, 2, types.Txs{tx}, []*abci.ExecTxResult{
1258+
{Code: abci.CodeTypeOK},
1259+
}, nil, nil, true))
1260+
txmp.Unlock()
1261+
1262+
// Success clears the failure tracker. Simulate LRU eviction of the
1263+
// main cache entry so we can verify the tracker was actually reset.
1264+
txmp.cache.Remove(txKey)
1265+
1266+
// Tx should now be re-admittable
1267+
require.NoError(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}))
1268+
1269+
// Fail again in a block — this should be treated as a fresh first failure
1270+
txmp.Lock()
1271+
require.NoError(t, txmp.Update(ctx, 3, types.Txs{tx}, []*abci.ExecTxResult{
1272+
{Code: 11},
1273+
}, nil, nil, true))
1274+
txmp.Unlock()
1275+
1276+
// First-failure grace should be restored: tx allowed to re-enter
1277+
require.NoError(t, txmp.CheckTx(ctx, tx, nil, TxInfo{SenderID: 0}))
1278+
require.Equal(t, 1, txmp.Size())
1279+
}

0 commit comments

Comments
 (0)