github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/consensus/mempool_test.go (about)

     1  package consensus
     2  
     3  import (
     4  	"encoding/binary"
     5  	"fmt"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  
    12  	"github.com/gnolang/gno/tm2/pkg/bft/abci/example/errors"
    13  	abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
    14  	cstypes "github.com/gnolang/gno/tm2/pkg/bft/consensus/types"
    15  	mempl "github.com/gnolang/gno/tm2/pkg/bft/mempool"
    16  	sm "github.com/gnolang/gno/tm2/pkg/bft/state"
    17  	"github.com/gnolang/gno/tm2/pkg/bft/types"
    18  	"github.com/gnolang/gno/tm2/pkg/db/memdb"
    19  )
    20  
    21  // for testing
    22  func assertMempool(txn txNotifier) mempl.Mempool {
    23  	return txn.(mempl.Mempool)
    24  }
    25  
    26  func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) {
    27  	t.Parallel()
    28  
    29  	config, _ := ResetConfig("consensus_mempool_no_progress_until_txs_available")
    30  	defer os.RemoveAll(config.RootDir)
    31  	config.Consensus.CreateEmptyBlocks = false
    32  	state, privVals := randGenesisState(1, false, 10)
    33  	app := NewCounterApplication()
    34  	cs := newConsensusStateWithConfig(config, state, privVals[0], app)
    35  	assertMempool(cs.txNotifier).EnableTxsAvailable()
    36  	height, round := cs.Height, cs.Round
    37  	newBlockCh := subscribe(cs.evsw, types.EventNewBlock{})
    38  	startFrom(cs, height, round)
    39  	defer func() {
    40  		cs.Stop()
    41  		cs.Wait()
    42  		app.Close()
    43  	}()
    44  
    45  	ensureNewEventOnChannel(newBlockCh) // first block gets committed
    46  	ensureNoNewEventOnChannel(newBlockCh)
    47  	deliverTxsRange(cs, 0, 1)
    48  	ensureNewEventOnChannel(newBlockCh) // commit txs
    49  	ensureNewEventOnChannel(newBlockCh) // commit updated app hash
    50  	ensureNoNewEventOnChannel(newBlockCh)
    51  }
    52  
    53  func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) {
    54  	config, _ := ResetConfig("consensus_mempool_progress_after_create_empty_blocks_interval")
    55  	defer os.RemoveAll(config.RootDir)
    56  	config.Consensus.CreateEmptyBlocksInterval = ensureTimeout
    57  	state, privVals := randGenesisState(1, false, 10)
    58  	app := NewCounterApplication()
    59  	cs := newConsensusStateWithConfig(config, state, privVals[0], app)
    60  	assertMempool(cs.txNotifier).EnableTxsAvailable()
    61  	height, round := cs.Height, cs.Round
    62  	newBlockCh := subscribe(cs.evsw, types.EventNewBlock{})
    63  	startFrom(cs, height, round)
    64  	defer func() {
    65  		cs.Stop()
    66  		cs.Wait()
    67  		app.Close()
    68  	}()
    69  
    70  	ensureNewEventOnChannel(newBlockCh)   // first block gets committed
    71  	ensureNoNewEventOnChannel(newBlockCh) // then we dont make a block ...
    72  	ensureNewEventOnChannel(newBlockCh)   // until the CreateEmptyBlocksInterval has passed
    73  }
    74  
    75  func TestMempoolProgressInHigherRound(t *testing.T) {
    76  	t.Parallel()
    77  
    78  	config, _ := ResetConfig("consensus_mempool_progress_in_higher_round")
    79  	defer os.RemoveAll(config.RootDir)
    80  	config.Consensus.CreateEmptyBlocks = false
    81  	state, privVals := randGenesisState(1, false, 10)
    82  	app := NewCounterApplication()
    83  	cs := newConsensusStateWithConfig(config, state, privVals[0], app)
    84  	assertMempool(cs.txNotifier).EnableTxsAvailable()
    85  	height, round := cs.Height, cs.Round
    86  	newBlockCh := subscribe(cs.evsw, types.EventNewBlock{})
    87  	newStepCh := subscribe(cs.evsw, cstypes.EventNewRoundStep{})
    88  	timeoutCh := subscribe(cs.evsw, cstypes.EventTimeoutPropose{})
    89  	cs.setProposal = func(proposal *types.Proposal) error {
    90  		if cs.Height == 2 && cs.Round == 0 {
    91  			// dont set the proposal in round 0 so we timeout and
    92  			// go to next round
    93  			cs.Logger.Info("Ignoring set proposal at height 2, round 0")
    94  			return nil
    95  		}
    96  		return cs.defaultSetProposal(proposal)
    97  	}
    98  	startFrom(cs, height, round)
    99  	defer func() {
   100  		cs.Stop()
   101  		cs.Wait()
   102  		app.Close()
   103  	}()
   104  
   105  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPropose)   // first round at first height
   106  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrevote)   // ...
   107  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrecommit) // ...
   108  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepCommit)    // ...
   109  	ensureNewEventOnChannel(newBlockCh)                                      // first block gets committed
   110  
   111  	height++ // moving to the next height
   112  	round = 0
   113  
   114  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepNewHeight) // new height
   115  	deliverTxsRange(cs, 0, 1)                                                // we deliver txs, but dont set a proposal so we get the next round
   116  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPropose)   // first round at next height
   117  
   118  	ensureNewTimeout(timeoutCh, height, round, cs.config.TimeoutPropose.Nanoseconds())
   119  
   120  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrevote)       // ...
   121  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrecommit)     // ...
   122  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrecommitWait) // ...
   123  
   124  	round++                                                                  // moving to the next round
   125  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPropose)   // wait for the next round
   126  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrevote)   // ...
   127  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepPrecommit) // ...
   128  	ensureNewRoundStep(newStepCh, height, round, cstypes.RoundStepCommit)    // ...
   129  	ensureNewEventOnChannel(newBlockCh)                                      // now we can commit the block
   130  }
   131  
   132  func deliverTxsRange(cs *ConsensusState, start, end int) {
   133  	// Deliver some txs.
   134  	for i := start; i < end; i++ {
   135  		txBytes := make([]byte, 8)
   136  		binary.BigEndian.PutUint64(txBytes, uint64(i))
   137  		err := assertMempool(cs.txNotifier).CheckTx(txBytes, nil)
   138  		if err != nil {
   139  			panic(fmt.Sprintf("Error after CheckTx: %v", err))
   140  		}
   141  	}
   142  }
   143  
   144  func TestMempoolTxConcurrentWithCommit(t *testing.T) {
   145  	t.Parallel()
   146  
   147  	state, privVals := randGenesisState(1, false, 10)
   148  	blockDB := memdb.NewMemDB()
   149  	app := NewCounterApplication()
   150  	cs := newConsensusStateWithConfigAndBlockStore(config, state, privVals[0], app, blockDB)
   151  	sm.SaveState(blockDB, state)
   152  	height, round := cs.Height, cs.Round
   153  	newBlockCh := subscribe(cs.evsw, types.EventNewBlock{})
   154  
   155  	NTxs := 3000
   156  	go deliverTxsRange(cs, 0, NTxs)
   157  
   158  	startFrom(cs, height, round)
   159  	defer func() {
   160  		cs.Stop()
   161  		cs.Wait()
   162  		app.Close()
   163  	}()
   164  
   165  	for nTxs := 0; nTxs < NTxs; {
   166  		ticker := time.NewTicker(time.Second * 30)
   167  		select {
   168  		case msg := <-newBlockCh:
   169  			blockEvent := msg.(types.EventNewBlock)
   170  			nTxs += int(blockEvent.Block.Header.NumTxs)
   171  		case <-ticker.C:
   172  			panic("Timed out waiting to commit blocks with transactions")
   173  		}
   174  	}
   175  }
   176  
   177  func TestMempoolRmBadTx(t *testing.T) {
   178  	t.Parallel()
   179  
   180  	state, privVals := randGenesisState(1, false, 10)
   181  	app := NewCounterApplication()
   182  	blockDB := memdb.NewMemDB()
   183  	cs := newConsensusStateWithConfigAndBlockStore(config, state, privVals[0], app, blockDB)
   184  	sm.SaveState(blockDB, state)
   185  
   186  	// increment the counter by 1
   187  	txBytes := make([]byte, 8)
   188  	binary.BigEndian.PutUint64(txBytes, uint64(0))
   189  
   190  	resDeliver := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes})
   191  	assert.False(t, resDeliver.IsErr(), fmt.Sprintf("expected no error. got %v", resDeliver))
   192  
   193  	resCommit := app.Commit()
   194  	assert.True(t, len(resCommit.Data) > 0)
   195  
   196  	emptyMempoolCh := make(chan struct{})
   197  	checkTxRespCh := make(chan struct{})
   198  	go func() {
   199  		// Try to send the tx through the mempool.
   200  		// CheckTx should not err, but the app should return an abci Error.
   201  		// and the tx should get removed from the pool
   202  		err := assertMempool(cs.txNotifier).CheckTx(txBytes, func(r abci.Response) {
   203  			if _, ok := r.(abci.ResponseCheckTx).Error.(errors.BadNonceError); !ok {
   204  				t.Errorf("expected checktx to return bad nonce, got %v", r)
   205  				return
   206  			}
   207  			checkTxRespCh <- struct{}{}
   208  		})
   209  		if err != nil {
   210  			t.Errorf("Error after CheckTx: %v", err)
   211  			return
   212  		}
   213  
   214  		// check for the tx
   215  		for {
   216  			txs := assertMempool(cs.txNotifier).ReapMaxBytesMaxGas(int64(len(txBytes)), -1)
   217  			if len(txs) == 0 {
   218  				emptyMempoolCh <- struct{}{}
   219  				return
   220  			}
   221  			time.Sleep(10 * time.Millisecond)
   222  		}
   223  	}()
   224  
   225  	// Wait until the tx returns
   226  	ticker := time.After(time.Second * 5)
   227  	select {
   228  	case <-checkTxRespCh:
   229  		// success
   230  	case <-ticker:
   231  		t.Errorf("Timed out waiting for tx to return")
   232  		return
   233  	}
   234  
   235  	// Wait until the tx is removed
   236  	ticker = time.After(time.Second * 5)
   237  	select {
   238  	case <-emptyMempoolCh:
   239  		// success
   240  	case <-ticker:
   241  		t.Errorf("Timed out waiting for tx to be removed")
   242  		return
   243  	}
   244  }
   245  
   246  // CounterApplication that maintains a mempool state and resets it upon commit
   247  type CounterApplication struct {
   248  	abci.BaseApplication
   249  
   250  	txCount        int
   251  	mempoolTxCount int
   252  }
   253  
   254  func NewCounterApplication() *CounterApplication {
   255  	return &CounterApplication{}
   256  }
   257  
   258  func (app *CounterApplication) Info(req abci.RequestInfo) (res abci.ResponseInfo) {
   259  	res.Data = []byte(fmt.Sprintf("txs:%v", app.txCount))
   260  	return
   261  }
   262  
   263  func (app *CounterApplication) DeliverTx(req abci.RequestDeliverTx) (res abci.ResponseDeliverTx) {
   264  	txValue := txAsUint64(req.Tx)
   265  	if txValue != uint64(app.txCount) {
   266  		res.Error = errors.BadNonceError{}
   267  		res.Log = fmt.Sprintf("Invalid nonce. Expected %v, got %v", app.txCount, txValue)
   268  		return
   269  	}
   270  	app.txCount++
   271  	return
   272  }
   273  
   274  func (app *CounterApplication) CheckTx(req abci.RequestCheckTx) (res abci.ResponseCheckTx) {
   275  	txValue := txAsUint64(req.Tx)
   276  	if txValue != uint64(app.mempoolTxCount) {
   277  		res.Error = errors.BadNonceError{}
   278  		res.Log = fmt.Sprintf("Invalid nonce. Expected %v, got %v", app.mempoolTxCount, txValue)
   279  		return
   280  	}
   281  	app.mempoolTxCount++
   282  	return
   283  }
   284  
   285  func txAsUint64(tx []byte) uint64 {
   286  	tx8 := make([]byte, 8)
   287  	copy(tx8[len(tx8)-len(tx):], tx)
   288  	return binary.BigEndian.Uint64(tx8)
   289  }
   290  
   291  func (app *CounterApplication) Commit() (res abci.ResponseCommit) {
   292  	app.mempoolTxCount = app.txCount
   293  	if app.txCount == 0 {
   294  		return abci.ResponseCommit{}
   295  	}
   296  	hash := make([]byte, 8)
   297  	binary.BigEndian.PutUint64(hash, uint64(app.txCount))
   298  	res.Data = hash
   299  	return
   300  }