github.com/cosmos/cosmos-sdk@v0.50.10/x/simulation/simulate.go (about) 1 package simulation 2 3 import ( 4 "fmt" 5 "io" 6 "math/rand" 7 "os" 8 "os/signal" 9 "syscall" 10 "testing" 11 "time" 12 13 abci "github.com/cometbft/cometbft/abci/types" 14 cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" 15 16 "github.com/cosmos/cosmos-sdk/baseapp" 17 "github.com/cosmos/cosmos-sdk/codec" 18 sdk "github.com/cosmos/cosmos-sdk/types" 19 "github.com/cosmos/cosmos-sdk/types/simulation" 20 ) 21 22 const AverageBlockTime = 6 * time.Second 23 24 // initialize the chain for the simulation 25 func initChain( 26 r *rand.Rand, 27 params Params, 28 accounts []simulation.Account, 29 app *baseapp.BaseApp, 30 appStateFn simulation.AppStateFn, 31 config simulation.Config, 32 cdc codec.JSONCodec, 33 ) (mockValidators, time.Time, []simulation.Account, string) { 34 blockMaxGas := int64(-1) 35 if config.BlockMaxGas > 0 { 36 blockMaxGas = config.BlockMaxGas 37 } 38 appState, accounts, chainID, genesisTimestamp := appStateFn(r, accounts, config) 39 consensusParams := randomConsensusParams(r, appState, cdc, blockMaxGas) 40 req := abci.RequestInitChain{ 41 AppStateBytes: appState, 42 ChainId: chainID, 43 ConsensusParams: consensusParams, 44 Time: genesisTimestamp, 45 } 46 res, err := app.InitChain(&req) 47 if err != nil { 48 panic(err) 49 } 50 validators := newMockValidators(r, res.Validators, params) 51 52 return validators, genesisTimestamp, accounts, chainID 53 } 54 55 // SimulateFromSeed tests an application by running the provided 56 // operations, testing the provided invariants, but using the provided config.Seed. 57 func SimulateFromSeed( 58 tb testing.TB, 59 w io.Writer, 60 app *baseapp.BaseApp, 61 appStateFn simulation.AppStateFn, 62 randAccFn simulation.RandomAccountFn, 63 ops WeightedOperations, 64 blockedAddrs map[string]bool, 65 config simulation.Config, 66 cdc codec.JSONCodec, 67 ) (stopEarly bool, exportedParams Params, err error) { 68 // in case we have to end early, don't os.Exit so that we can run cleanup code. 69 testingMode, _, b := getTestingMode(tb) 70 71 r := rand.New(rand.NewSource(config.Seed)) 72 params := RandomParams(r) 73 74 fmt.Fprintf(w, "Starting SimulateFromSeed with randomness created with seed %d\n", int(config.Seed)) 75 fmt.Fprintf(w, "Randomized simulation params: \n%s\n", mustMarshalJSONIndent(params)) 76 77 timeDiff := maxTimePerBlock - minTimePerBlock 78 accs := randAccFn(r, params.NumKeys()) 79 eventStats := NewEventStats() 80 81 // Second variable to keep pending validator set (delayed one block since 82 // TM 0.24) Initially this is the same as the initial validator set 83 validators, blockTime, accs, chainID := initChain(r, params, accs, app, appStateFn, config, cdc) 84 // At least 2 accounts must be added here, otherwise when executing SimulateMsgSend 85 // two accounts will be selected to meet the conditions from != to and it will fall into an infinite loop. 86 if len(accs) <= 1 { 87 return true, params, fmt.Errorf("at least two genesis accounts are required") 88 } 89 90 config.ChainID = chainID 91 92 fmt.Printf( 93 "Starting the simulation from time %v (unixtime %v)\n", 94 blockTime.UTC().Format(time.UnixDate), blockTime.Unix(), 95 ) 96 97 // remove module account address if they exist in accs 98 var tmpAccs []simulation.Account 99 100 for _, acc := range accs { 101 if !blockedAddrs[acc.Address.String()] { 102 tmpAccs = append(tmpAccs, acc) 103 } 104 } 105 106 accs = tmpAccs 107 nextValidators := validators 108 109 var ( 110 pastTimes []time.Time 111 pastVoteInfos [][]abci.VoteInfo 112 timeOperationQueue []simulation.FutureOperation 113 114 blockHeight = int64(config.InitialBlockHeight) 115 proposerAddress = validators.randomProposer(r) 116 opCount = 0 117 ) 118 119 // Setup code to catch SIGTERM's 120 c := make(chan os.Signal, 1) 121 signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 122 123 go func() { 124 receivedSignal := <-c 125 fmt.Fprintf(w, "\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, blockHeight, opCount) 126 err = fmt.Errorf("exited due to %s", receivedSignal) 127 stopEarly = true 128 }() 129 130 finalizeBlockReq := RandomRequestFinalizeBlock( 131 r, 132 params, 133 validators, 134 pastTimes, 135 pastVoteInfos, 136 eventStats.Tally, 137 blockHeight, 138 blockTime, 139 validators.randomProposer(r), 140 ) 141 142 // These are operations which have been queued by previous operations 143 operationQueue := NewOperationQueue() 144 logWriter := NewLogWriter(testingMode) 145 146 blockSimulator := createBlockSimulator( 147 testingMode, 148 tb, 149 w, 150 params, 151 eventStats.Tally, 152 ops, 153 operationQueue, 154 timeOperationQueue, 155 logWriter, 156 config, 157 ) 158 159 if !testingMode { 160 b.ResetTimer() 161 } else { 162 // recover logs in case of panic 163 defer func() { 164 if r := recover(); r != nil { 165 _, _ = fmt.Fprintf(w, "simulation halted due to panic on block %d\n", blockHeight) 166 logWriter.PrintLogs() 167 panic(r) 168 } 169 }() 170 } 171 172 // set exported params to the initial state 173 if config.ExportParamsPath != "" && config.ExportParamsHeight == 0 { 174 exportedParams = params 175 } 176 177 for blockHeight < int64(config.NumBlocks+config.InitialBlockHeight) && !stopEarly { 178 pastTimes = append(pastTimes, blockTime) 179 pastVoteInfos = append(pastVoteInfos, finalizeBlockReq.DecidedLastCommit.Votes) 180 181 // Run the BeginBlock handler 182 logWriter.AddEntry(BeginBlockEntry(blockHeight)) 183 184 res, err := app.FinalizeBlock(finalizeBlockReq) 185 if err != nil { 186 return true, params, err 187 } 188 189 ctx := app.NewContextLegacy(false, cmtproto.Header{ 190 Height: blockHeight, 191 Time: blockTime, 192 ProposerAddress: proposerAddress, 193 ChainID: config.ChainID, 194 }) 195 196 // run queued operations; ignores block size if block size is too small 197 numQueuedOpsRan, futureOps := runQueuedOperations( 198 operationQueue, int(blockHeight), tb, r, app, ctx, accs, logWriter, 199 eventStats.Tally, config.Lean, config.ChainID, 200 ) 201 202 numQueuedTimeOpsRan, timeFutureOps := runQueuedTimeOperations( 203 timeOperationQueue, int(blockHeight), blockTime, 204 tb, r, app, ctx, accs, logWriter, eventStats.Tally, 205 config.Lean, config.ChainID, 206 ) 207 208 futureOps = append(futureOps, timeFutureOps...) 209 queueOperations(operationQueue, timeOperationQueue, futureOps) 210 211 // run standard operations 212 operations := blockSimulator(r, app, ctx, accs, cmtproto.Header{ 213 Height: blockHeight, 214 Time: blockTime, 215 ProposerAddress: proposerAddress, 216 ChainID: config.ChainID, 217 }) 218 opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan 219 220 blockHeight++ 221 222 blockTime = blockTime.Add(time.Duration(minTimePerBlock) * time.Second) 223 blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second) 224 proposerAddress = validators.randomProposer(r) 225 226 logWriter.AddEntry(EndBlockEntry(blockHeight)) 227 228 if config.Commit { 229 app.Commit() 230 } 231 232 if proposerAddress == nil { 233 fmt.Fprintf(w, "\nSimulation stopped early as all validators have been unbonded; nobody left to propose a block!\n") 234 stopEarly = true 235 break 236 } 237 238 // Generate a random RequestBeginBlock with the current validator set 239 // for the next block 240 finalizeBlockReq = RandomRequestFinalizeBlock(r, params, validators, pastTimes, pastVoteInfos, eventStats.Tally, blockHeight, blockTime, proposerAddress) 241 242 // Update the validator set, which will be reflected in the application 243 // on the next block 244 validators = nextValidators 245 nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, eventStats.Tally) 246 247 // update the exported params 248 if config.ExportParamsPath != "" && int64(config.ExportParamsHeight) == blockHeight { 249 exportedParams = params 250 } 251 } 252 253 if stopEarly { 254 if config.ExportStatsPath != "" { 255 fmt.Println("Exporting simulation statistics...") 256 eventStats.ExportJSON(config.ExportStatsPath) 257 } else { 258 eventStats.Print(w) 259 } 260 261 return true, exportedParams, err 262 } 263 264 fmt.Fprintf( 265 w, 266 "\nSimulation complete; Final height (blocks): %d, final time (seconds): %v, operations ran: %d\n", 267 blockHeight, blockTime, opCount, 268 ) 269 270 if config.ExportStatsPath != "" { 271 fmt.Println("Exporting simulation statistics...") 272 eventStats.ExportJSON(config.ExportStatsPath) 273 } else { 274 eventStats.Print(w) 275 } 276 277 return false, exportedParams, nil 278 } 279 280 type blockSimFn func( 281 r *rand.Rand, 282 app *baseapp.BaseApp, 283 ctx sdk.Context, 284 accounts []simulation.Account, 285 header cmtproto.Header, 286 ) (opCount int) 287 288 // Returns a function to simulate blocks. Written like this to avoid constant 289 // parameters being passed everytime, to minimize memory overhead. 290 func createBlockSimulator(testingMode bool, tb testing.TB, w io.Writer, params Params, 291 event func(route, op, evResult string), ops WeightedOperations, 292 operationQueue OperationQueue, timeOperationQueue []simulation.FutureOperation, 293 logWriter LogWriter, config simulation.Config, 294 ) blockSimFn { 295 lastBlockSizeState := 0 // state for [4 * uniform distribution] 296 blocksize := 0 297 selectOp := ops.getSelectOpFn() 298 299 return func( 300 r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account, header cmtproto.Header, 301 ) (opCount int) { 302 _, _ = fmt.Fprintf( 303 w, "\rSimulating... block %d/%d, operation %d/%d.", 304 header.Height, config.NumBlocks, opCount, blocksize, 305 ) 306 lastBlockSizeState, blocksize = getBlockSize(r, params, lastBlockSizeState, config.BlockSize) 307 308 type opAndR struct { 309 op simulation.Operation 310 rand *rand.Rand 311 } 312 313 opAndRz := make([]opAndR, 0, blocksize) 314 315 // Predetermine the blocksize slice so that we can do things like block 316 // out certain operations without changing the ops that follow. 317 for i := 0; i < blocksize; i++ { 318 opAndRz = append(opAndRz, opAndR{ 319 op: selectOp(r), 320 rand: simulation.DeriveRand(r), 321 }) 322 } 323 324 for i := 0; i < blocksize; i++ { 325 // NOTE: the Rand 'r' should not be used here. 326 opAndR := opAndRz[i] 327 op, r2 := opAndR.op, opAndR.rand 328 opMsg, futureOps, err := op(r2, app, ctx, accounts, config.ChainID) 329 opMsg.LogEvent(event) 330 331 if !config.Lean || opMsg.OK { 332 logWriter.AddEntry(MsgEntry(header.Height, int64(i), opMsg)) 333 } 334 335 if err != nil { 336 logWriter.PrintLogs() 337 tb.Fatalf(`error on block %d/%d, operation (%d/%d) from x/%s: 338 %v 339 Comment: %s`, 340 header.Height, config.NumBlocks, opCount, blocksize, opMsg.Route, err, opMsg.Comment) 341 } 342 343 queueOperations(operationQueue, timeOperationQueue, futureOps) 344 345 if testingMode && opCount%50 == 0 { 346 fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ", 347 header.Height, config.NumBlocks, opCount, blocksize) 348 } 349 350 opCount++ 351 } 352 353 return opCount 354 } 355 } 356 357 func runQueuedOperations(queueOps map[int][]simulation.Operation, 358 height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, 359 ctx sdk.Context, accounts []simulation.Account, logWriter LogWriter, 360 event func(route, op, evResult string), lean bool, chainID string, 361 ) (numOpsRan int, allFutureOps []simulation.FutureOperation) { 362 queuedOp, ok := queueOps[height] 363 if !ok { 364 return 0, nil 365 } 366 367 // Keep all future operations 368 allFutureOps = make([]simulation.FutureOperation, 0) 369 370 numOpsRan = len(queuedOp) 371 for i := 0; i < numOpsRan; i++ { 372 opMsg, futureOps, err := queuedOp[i](r, app, ctx, accounts, chainID) 373 if len(futureOps) > 0 { 374 allFutureOps = append(allFutureOps, futureOps...) 375 } 376 377 opMsg.LogEvent(event) 378 379 if !lean || opMsg.OK { 380 logWriter.AddEntry((QueuedMsgEntry(int64(height), opMsg))) 381 } 382 383 if err != nil { 384 logWriter.PrintLogs() 385 tb.FailNow() 386 } 387 } 388 delete(queueOps, height) 389 390 return numOpsRan, allFutureOps 391 } 392 393 func runQueuedTimeOperations(queueOps []simulation.FutureOperation, 394 height int, currentTime time.Time, tb testing.TB, r *rand.Rand, 395 app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account, 396 logWriter LogWriter, event func(route, op, evResult string), 397 lean bool, chainID string, 398 ) (numOpsRan int, allFutureOps []simulation.FutureOperation) { 399 // Keep all future operations 400 allFutureOps = make([]simulation.FutureOperation, 0) 401 402 numOpsRan = 0 403 for len(queueOps) > 0 && currentTime.After(queueOps[0].BlockTime) { 404 opMsg, futureOps, err := queueOps[0].Op(r, app, ctx, accounts, chainID) 405 406 opMsg.LogEvent(event) 407 408 if !lean || opMsg.OK { 409 logWriter.AddEntry(QueuedMsgEntry(int64(height), opMsg)) 410 } 411 412 if err != nil { 413 logWriter.PrintLogs() 414 tb.FailNow() 415 } 416 417 if len(futureOps) > 0 { 418 allFutureOps = append(allFutureOps, futureOps...) 419 } 420 421 queueOps = queueOps[1:] 422 numOpsRan++ 423 } 424 425 return numOpsRan, allFutureOps 426 }