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