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 }