package consensus import ( "context" "encoding/binary" "fmt" "os" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" "github.com/tendermint/tendermint/abci/example/code" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/internal/mempool" sm "github.com/tendermint/tendermint/internal/state" "github.com/tendermint/tendermint/internal/store" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/types" ) // for testing func assertMempool(t *testing.T, txn txNotifier) mempool.Mempool { t.Helper() mp, ok := txn.(mempool.Mempool) require.True(t, ok) return mp } func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() baseConfig := configSetup(t) config, err := ResetConfig("consensus_mempool_txs_available_test") require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll(config.RootDir) }) config.Consensus.CreateEmptyBlocks = false state, privVals := randGenesisState(ctx, t, baseConfig, 1, false, 10) cs := newStateWithConfig(ctx, t, log.TestingLogger(), config, state, privVals[0], NewCounterApplication()) assertMempool(t, cs.txNotifier).EnableTxsAvailable() height, round := cs.Height, cs.Round newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) startTestRound(ctx, cs, height, round) ensureNewEventOnChannel(t, newBlockCh) // first block gets committed ensureNoNewEventOnChannel(t, newBlockCh) deliverTxsRange(ctx, t, cs, 0, 1) ensureNewEventOnChannel(t, newBlockCh) // commit txs ensureNewEventOnChannel(t, newBlockCh) // commit updated app hash ensureNoNewEventOnChannel(t, newBlockCh) } func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) { baseConfig := configSetup(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() config, err := ResetConfig("consensus_mempool_txs_available_test") require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll(config.RootDir) }) config.Consensus.CreateEmptyBlocksInterval = ensureTimeout state, privVals := randGenesisState(ctx, t, baseConfig, 1, false, 10) cs := newStateWithConfig(ctx, t, log.TestingLogger(), config, state, privVals[0], NewCounterApplication()) assertMempool(t, cs.txNotifier).EnableTxsAvailable() newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) startTestRound(ctx, cs, cs.Height, cs.Round) ensureNewEventOnChannel(t, newBlockCh) // first block gets committed ensureNoNewEventOnChannel(t, newBlockCh) // then we dont make a block ... ensureNewEventOnChannel(t, newBlockCh) // until the CreateEmptyBlocksInterval has passed } func TestMempoolProgressInHigherRound(t *testing.T) { baseConfig := configSetup(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() config, err := ResetConfig("consensus_mempool_txs_available_test") require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll(config.RootDir) }) config.Consensus.CreateEmptyBlocks = false state, privVals := randGenesisState(ctx, t, baseConfig, 1, false, 10) cs := newStateWithConfig(ctx, t, log.TestingLogger(), config, state, privVals[0], NewCounterApplication()) assertMempool(t, cs.txNotifier).EnableTxsAvailable() height, round := cs.Height, cs.Round newBlockCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlock) newRoundCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewRound) timeoutCh := subscribe(ctx, t, cs.eventBus, types.EventQueryTimeoutPropose) cs.setProposal = func(proposal *types.Proposal) error { if cs.Height == 2 && cs.Round == 0 { // dont set the proposal in round 0 so we timeout and // go to next round return nil } return cs.defaultSetProposal(proposal) } startTestRound(ctx, cs, height, round) ensureNewRound(t, newRoundCh, height, round) // first round at first height ensureNewEventOnChannel(t, newBlockCh) // first block gets committed height++ // moving to the next height round = 0 ensureNewRound(t, newRoundCh, height, round) // first round at next height deliverTxsRange(ctx, t, cs, 0, 1) // we deliver txs, but dont set a proposal so we get the next round ensureNewTimeout(t, timeoutCh, height, round, cs.config.TimeoutPropose.Nanoseconds()) round++ // moving to the next round ensureNewRound(t, newRoundCh, height, round) // wait for the next round ensureNewEventOnChannel(t, newBlockCh) // now we can commit the block } func deliverTxsRange(ctx context.Context, t *testing.T, cs *State, start, end int) { t.Helper() // Deliver some txs. for i := start; i < end; i++ { txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(i)) err := assertMempool(t, cs.txNotifier).CheckTx(ctx, txBytes, nil, mempool.TxInfo{}) require.NoError(t, err, "error after checkTx") } } func TestMempoolTxConcurrentWithCommit(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() config := configSetup(t) logger := log.TestingLogger() state, privVals := randGenesisState(ctx, t, config, 1, false, 10) stateStore := sm.NewStore(dbm.NewMemDB()) blockStore := store.NewBlockStore(dbm.NewMemDB()) cs := newStateWithConfigAndBlockStore( ctx, t, logger, config, state, privVals[0], NewCounterApplication(), blockStore) err := stateStore.Save(state) require.NoError(t, err) newBlockHeaderCh := subscribe(ctx, t, cs.eventBus, types.EventQueryNewBlockHeader) const numTxs int64 = 3000 go deliverTxsRange(ctx, t, cs, 0, int(numTxs)) startTestRound(ctx, cs, cs.Height, cs.Round) for n := int64(0); n < numTxs; { select { case msg := <-newBlockHeaderCh: headerEvent := msg.Data().(types.EventDataNewBlockHeader) n += headerEvent.NumTxs case <-time.After(30 * time.Second): t.Fatal("Timed out waiting 30s to commit blocks with transactions") } } } func TestMempoolRmBadTx(t *testing.T) { config := configSetup(t) ctx, cancel := context.WithCancel(context.Background()) defer cancel() state, privVals := randGenesisState(ctx, t, config, 1, false, 10) app := NewCounterApplication() stateStore := sm.NewStore(dbm.NewMemDB()) blockStore := store.NewBlockStore(dbm.NewMemDB()) cs := newStateWithConfigAndBlockStore(ctx, t, log.TestingLogger(), config, state, privVals[0], app, blockStore) err := stateStore.Save(state) require.NoError(t, err) // increment the counter by 1 txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(0)) resDeliver := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) assert.False(t, resDeliver.IsErr(), fmt.Sprintf("expected no error. got %v", resDeliver)) resCommit := app.Commit() assert.True(t, len(resCommit.Data) > 0) emptyMempoolCh := make(chan struct{}) checkTxRespCh := make(chan struct{}) go func() { // Try to send the tx through the mempool. // CheckTx should not err, but the app should return a bad abci code // and the tx should get removed from the pool err := assertMempool(t, cs.txNotifier).CheckTx(ctx, txBytes, func(r *abci.Response) { if r.GetCheckTx().Code != code.CodeTypeBadNonce { t.Errorf("expected checktx to return bad nonce, got %v", r) return } checkTxRespCh <- struct{}{} }, mempool.TxInfo{}) if err != nil { t.Errorf("error after CheckTx: %w", err) return } // check for the tx for { txs := assertMempool(t, cs.txNotifier).ReapMaxBytesMaxGas(int64(len(txBytes)), -1) if len(txs) == 0 { emptyMempoolCh <- struct{}{} return } time.Sleep(10 * time.Millisecond) } }() // Wait until the tx returns ticker := time.After(time.Second * 5) select { case <-checkTxRespCh: // success case <-ticker: t.Errorf("timed out waiting for tx to return") return } // Wait until the tx is removed ticker = time.After(time.Second * 5) select { case <-emptyMempoolCh: // success case <-ticker: t.Errorf("timed out waiting for tx to be removed") return } } // CounterApplication that maintains a mempool state and resets it upon commit type CounterApplication struct { abci.BaseApplication txCount int mempoolTxCount int } func NewCounterApplication() *CounterApplication { return &CounterApplication{} } func (app *CounterApplication) Info(req abci.RequestInfo) abci.ResponseInfo { return abci.ResponseInfo{Data: fmt.Sprintf("txs:%v", app.txCount)} } func (app *CounterApplication) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { txValue := txAsUint64(req.Tx) if txValue != uint64(app.txCount) { return abci.ResponseDeliverTx{ Code: code.CodeTypeBadNonce, Log: fmt.Sprintf("Invalid nonce. Expected %v, got %v", app.txCount, txValue)} } app.txCount++ return abci.ResponseDeliverTx{Code: code.CodeTypeOK} } func (app *CounterApplication) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { txValue := txAsUint64(req.Tx) if txValue != uint64(app.mempoolTxCount) { return abci.ResponseCheckTx{ Code: code.CodeTypeBadNonce, Log: fmt.Sprintf("Invalid nonce. Expected %v, got %v", app.mempoolTxCount, txValue)} } app.mempoolTxCount++ return abci.ResponseCheckTx{Code: code.CodeTypeOK} } func txAsUint64(tx []byte) uint64 { tx8 := make([]byte, 8) copy(tx8[len(tx8)-len(tx):], tx) return binary.BigEndian.Uint64(tx8) } func (app *CounterApplication) Commit() abci.ResponseCommit { app.mempoolTxCount = app.txCount if app.txCount == 0 { return abci.ResponseCommit{} } hash := make([]byte, 8) binary.BigEndian.PutUint64(hash, uint64(app.txCount)) return abci.ResponseCommit{Data: hash} }