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  }