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