github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/consensus/mempool_test.go (about) 1 package consensus 2 3 import ( 4 "context" 5 "encoding/binary" 6 "fmt" 7 "os" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 dbm "github.com/tendermint/tm-db" 15 16 "github.com/ari-anchor/sei-tendermint/abci/example/code" 17 abci "github.com/ari-anchor/sei-tendermint/abci/types" 18 "github.com/ari-anchor/sei-tendermint/internal/mempool" 19 sm "github.com/ari-anchor/sei-tendermint/internal/state" 20 "github.com/ari-anchor/sei-tendermint/internal/store" 21 "github.com/ari-anchor/sei-tendermint/internal/test/factory" 22 "github.com/ari-anchor/sei-tendermint/libs/log" 23 "github.com/ari-anchor/sei-tendermint/types" 24 ) 25 26 // for testing 27 func assertMempool(t *testing.T, txn txNotifier) mempool.Mempool { 28 t.Helper() 29 mp, ok := txn.(mempool.Mempool) 30 require.True(t, ok) 31 return mp 32 } 33 34 func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { 35 ctx, cancel := context.WithCancel(context.Background()) 36 defer cancel() 37 38 baseConfig := configSetup(t) 39 40 config, err := ResetConfig(t.TempDir(), "consensus_mempool_txs_available_test") 41 require.NoError(t, err) 42 t.Cleanup(func() { _ = os.RemoveAll(config.RootDir) }) 43 44 config.Consensus.CreateEmptyBlocks = false 45 state, privVals := makeGenesisState(ctx, t, baseConfig, genesisStateArgs{ 46 Validators: 1, 47 Power: 10, 48 Params: factory.ConsensusParams()}) 49 cs := newStateWithConfig(ctx, t, log.NewNopLogger(), config, state, privVals[0], NewCounterApplication()) 50 assertMempool(t, cs.txNotifier).EnableTxsAvailable() 51 height, round := cs.roundState.Height(), cs.roundState.Round() 52 newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) 53 startTestRound(ctx, cs, height, round) 54 55 ensureNewEventOnChannel(t, newBlockCh) // first block gets committed 56 ensureNoNewEventOnChannel(t, newBlockCh) 57 checkTxsRange(ctx, t, cs, 0, 1) 58 ensureNewEventOnChannel(t, newBlockCh) // commit txs 59 ensureNewEventOnChannel(t, newBlockCh) // commit updated app hash 60 ensureNoNewEventOnChannel(t, newBlockCh) 61 } 62 63 func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) { 64 baseConfig := configSetup(t) 65 ctx, cancel := context.WithCancel(context.Background()) 66 defer cancel() 67 68 config, err := ResetConfig(t.TempDir(), "consensus_mempool_txs_available_test") 69 require.NoError(t, err) 70 t.Cleanup(func() { _ = os.RemoveAll(config.RootDir) }) 71 72 config.Consensus.CreateEmptyBlocksInterval = ensureTimeout 73 state, privVals := makeGenesisState(ctx, t, baseConfig, genesisStateArgs{ 74 Validators: 1, 75 Power: 10, 76 Params: factory.ConsensusParams()}) 77 cs := newStateWithConfig(ctx, t, log.NewNopLogger(), config, state, privVals[0], NewCounterApplication()) 78 79 assertMempool(t, cs.txNotifier).EnableTxsAvailable() 80 81 newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) 82 startTestRound(ctx, cs, cs.roundState.Height(), cs.roundState.Round()) 83 84 ensureNewEventOnChannel(t, newBlockCh) // first block gets committed 85 ensureNoNewEventOnChannel(t, newBlockCh) // then we dont make a block ... 86 ensureNewEventOnChannel(t, newBlockCh) // until the CreateEmptyBlocksInterval has passed 87 } 88 89 func TestMempoolProgressInHigherRound(t *testing.T) { 90 baseConfig := configSetup(t) 91 ctx, cancel := context.WithCancel(context.Background()) 92 defer cancel() 93 94 config, err := ResetConfig(t.TempDir(), "consensus_mempool_txs_available_test") 95 require.NoError(t, err) 96 t.Cleanup(func() { _ = os.RemoveAll(config.RootDir) }) 97 98 config.Consensus.CreateEmptyBlocks = false 99 state, privVals := makeGenesisState(ctx, t, baseConfig, genesisStateArgs{ 100 Validators: 1, 101 Power: 10, 102 Params: factory.ConsensusParams()}) 103 cs := newStateWithConfig(ctx, t, log.NewNopLogger(), config, state, privVals[0], NewCounterApplication()) 104 assertMempool(t, cs.txNotifier).EnableTxsAvailable() 105 height, round := cs.roundState.Height(), cs.roundState.Round() 106 newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) 107 newRoundCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound) 108 timeoutCh := subscribe(ctx, t, cs.eventBus, types.EventQueryTimeoutPropose) 109 cs.setProposal = func(proposal *types.Proposal, recvTime time.Time) error { 110 if cs.roundState.Height() == 2 && cs.roundState.Round() == 0 { 111 // dont set the proposal in round 0 so we timeout and 112 // go to next round 113 return nil 114 } 115 return cs.defaultSetProposal(proposal, recvTime) 116 } 117 startTestRound(ctx, cs, height, round) 118 119 ensureNewRound(t, newRoundCh, height, round) // first round at first height 120 ensureNewEventOnChannel(t, newBlockCh) // first block gets committed 121 122 height++ // moving to the next height 123 round = 0 124 125 ensureNewRound(t, newRoundCh, height, round) // first round at next height 126 checkTxsRange(ctx, t, cs, 0, 1) // we deliver txs, but don't set a proposal so we get the next round 127 ensureNewTimeout(t, timeoutCh, height, round, cs.state.ConsensusParams.Timeout.ProposeTimeout(round).Nanoseconds()) 128 round++ // moving to the next round 129 ensureNewRound(t, newRoundCh, height, round) // wait for the next round 130 ensureNewEventOnChannel(t, newBlockCh) // now we can commit the block 131 } 132 133 func checkTxsRange(ctx context.Context, t *testing.T, cs *State, start, end int) { 134 t.Helper() 135 // Deliver some txs. 136 for i := start; i < end; i++ { 137 txBytes := make([]byte, 8) 138 binary.BigEndian.PutUint64(txBytes, uint64(i)) 139 var rCode uint32 140 err := assertMempool(t, cs.txNotifier).CheckTx(ctx, txBytes, func(r *abci.ResponseCheckTx) { rCode = r.Code }, mempool.TxInfo{}) 141 require.NoError(t, err, "error after checkTx") 142 require.Equal(t, code.CodeTypeOK, rCode, "checkTx code is error, txBytes %X", txBytes) 143 } 144 } 145 146 func TestMempoolTxConcurrentWithCommit(t *testing.T) { 147 ctx, cancel := context.WithCancel(context.Background()) 148 defer cancel() 149 150 config := configSetup(t) 151 logger := log.NewNopLogger() 152 state, privVals := makeGenesisState(ctx, t, config, genesisStateArgs{ 153 Validators: 1, 154 Power: 10, 155 Params: factory.ConsensusParams(), 156 }) 157 stateStore := sm.NewStore(dbm.NewMemDB()) 158 blockStore := store.NewBlockStore(dbm.NewMemDB()) 159 160 cs := newStateWithConfigAndBlockStore( 161 ctx, 162 t, 163 logger, config, state, privVals[0], NewCounterApplication(), blockStore) 164 165 err := stateStore.Save(state) 166 require.NoError(t, err) 167 newBlockHeaderCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlockHeader) 168 169 const numTxs int64 = 100 170 go checkTxsRange(ctx, t, cs, 0, int(numTxs)) 171 172 startTestRound(ctx, cs, cs.roundState.Height(), cs.roundState.Round()) 173 for n := int64(0); n < numTxs; { 174 select { 175 case msg := <-newBlockHeaderCh: 176 headerEvent := msg.Data().(types.EventDataNewBlockHeader) 177 n += headerEvent.NumTxs 178 logger.Info("new transactions", "nTxs", headerEvent.NumTxs, "total", n) 179 case <-time.After(30 * time.Second): 180 t.Fatal("Timed out waiting 30s to commit blocks with transactions") 181 } 182 } 183 } 184 185 func TestMempoolRmBadTx(t *testing.T) { 186 config := configSetup(t) 187 ctx, cancel := context.WithCancel(context.Background()) 188 defer cancel() 189 190 state, privVals := makeGenesisState(ctx, t, config, genesisStateArgs{ 191 Validators: 1, 192 Power: 10, 193 Params: factory.ConsensusParams()}) 194 app := NewCounterApplication() 195 stateStore := sm.NewStore(dbm.NewMemDB()) 196 blockStore := store.NewBlockStore(dbm.NewMemDB()) 197 cs := newStateWithConfigAndBlockStore(ctx, t, log.NewNopLogger(), config, state, privVals[0], app, blockStore) 198 err := stateStore.Save(state) 199 require.NoError(t, err) 200 201 // increment the counter by 1 202 txBytes := make([]byte, 8) 203 binary.BigEndian.PutUint64(txBytes, uint64(0)) 204 205 resFinalize, err := app.FinalizeBlock(ctx, &abci.RequestFinalizeBlock{Txs: [][]byte{txBytes}}) 206 require.NoError(t, err) 207 assert.False(t, resFinalize.TxResults[0].IsErr(), fmt.Sprintf("expected no error. got %v", resFinalize)) 208 assert.True(t, len(resFinalize.AppHash) > 0) 209 210 _, err = app.Commit(ctx) 211 require.NoError(t, err) 212 213 emptyMempoolCh := make(chan struct{}) 214 checkTxRespCh := make(chan struct{}) 215 go func() { 216 // Try to send the tx through the mempool. 217 // CheckTx should not err, but the app should return a bad abci code 218 // and the tx should get removed from the pool 219 err := assertMempool(t, cs.txNotifier).CheckTx(ctx, txBytes, func(r *abci.ResponseCheckTx) { 220 if r.Code != code.CodeTypeBadNonce { 221 t.Errorf("expected checktx to return bad nonce, got %v", r) 222 return 223 } 224 checkTxRespCh <- struct{}{} 225 }, mempool.TxInfo{}) 226 if err != nil { 227 t.Errorf("error after CheckTx: %v", err) 228 return 229 } 230 231 // check for the tx 232 for { 233 txs := assertMempool(t, cs.txNotifier).ReapMaxBytesMaxGas(int64(len(txBytes)), -1) 234 if len(txs) == 0 { 235 emptyMempoolCh <- struct{}{} 236 return 237 } 238 time.Sleep(10 * time.Millisecond) 239 } 240 }() 241 242 // Wait until the tx returns 243 ticker := time.After(time.Second * 5) 244 select { 245 case <-checkTxRespCh: 246 // success 247 case <-ticker: 248 t.Errorf("timed out waiting for tx to return") 249 return 250 } 251 252 // Wait until the tx is removed 253 ticker = time.After(time.Second * 5) 254 select { 255 case <-emptyMempoolCh: 256 // success 257 case <-ticker: 258 t.Errorf("timed out waiting for tx to be removed") 259 return 260 } 261 } 262 263 // CounterApplication that maintains a mempool state and resets it upon commit 264 type CounterApplication struct { 265 abci.BaseApplication 266 267 txCount int 268 mempoolTxCount int 269 mu sync.Mutex 270 } 271 272 func NewCounterApplication() *CounterApplication { 273 return &CounterApplication{} 274 } 275 276 func (app *CounterApplication) Info(_ context.Context, req *abci.RequestInfo) (*abci.ResponseInfo, error) { 277 app.mu.Lock() 278 defer app.mu.Unlock() 279 280 return &abci.ResponseInfo{Data: fmt.Sprintf("txs:%v", app.txCount)}, nil 281 } 282 283 func (app *CounterApplication) FinalizeBlock(_ context.Context, req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { 284 app.mu.Lock() 285 defer app.mu.Unlock() 286 287 respTxs := make([]*abci.ExecTxResult, len(req.Txs)) 288 for i, tx := range req.Txs { 289 txValue := txAsUint64(tx) 290 if txValue != uint64(app.txCount) { 291 respTxs[i] = &abci.ExecTxResult{ 292 Code: code.CodeTypeBadNonce, 293 Log: fmt.Sprintf("Invalid nonce. Expected %d, got %d", app.txCount, txValue), 294 } 295 continue 296 } 297 app.txCount++ 298 respTxs[i] = &abci.ExecTxResult{Code: code.CodeTypeOK} 299 } 300 301 res := &abci.ResponseFinalizeBlock{TxResults: respTxs} 302 303 if app.txCount > 0 { 304 res.AppHash = make([]byte, 8) 305 binary.BigEndian.PutUint64(res.AppHash, uint64(app.txCount)) 306 } 307 308 return res, nil 309 } 310 311 func (app *CounterApplication) CheckTx(_ context.Context, req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { 312 app.mu.Lock() 313 defer app.mu.Unlock() 314 315 txValue := txAsUint64(req.Tx) 316 if txValue != uint64(app.mempoolTxCount) { 317 return &abci.ResponseCheckTx{ 318 Code: code.CodeTypeBadNonce, 319 }, nil 320 } 321 app.mempoolTxCount++ 322 return &abci.ResponseCheckTx{Code: code.CodeTypeOK}, nil 323 } 324 325 func txAsUint64(tx []byte) uint64 { 326 tx8 := make([]byte, 8) 327 copy(tx8[len(tx8)-len(tx):], tx) 328 return binary.BigEndian.Uint64(tx8) 329 } 330 331 func (app *CounterApplication) Commit(context.Context) (*abci.ResponseCommit, error) { 332 app.mu.Lock() 333 defer app.mu.Unlock() 334 335 app.mempoolTxCount = app.txCount 336 return &abci.ResponseCommit{}, nil 337 } 338 339 func (app *CounterApplication) PrepareProposal(_ context.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { 340 trs := make([]*abci.TxRecord, 0, len(req.Txs)) 341 var totalBytes int64 342 for _, tx := range req.Txs { 343 totalBytes += int64(len(tx)) 344 if totalBytes > req.MaxTxBytes { 345 break 346 } 347 trs = append(trs, &abci.TxRecord{ 348 Action: abci.TxRecord_UNMODIFIED, 349 Tx: tx, 350 }) 351 } 352 return &abci.ResponsePrepareProposal{TxRecords: trs}, nil 353 } 354 355 func (app *CounterApplication) ProcessProposal(_ context.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { 356 return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil 357 }