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  }