cosmos-sdk/x/simulation/simulate.go

361 lines
11 KiB
Go
Raw Normal View History

2018-07-16 18:25:15 -07:00
package simulation
import (
2018-07-17 11:33:53 -07:00
"encoding/json"
"fmt"
"io"
"math/rand"
2018-08-29 23:02:15 -07:00
"os"
"os/signal"
"runtime/debug"
"syscall"
"testing"
"time"
abci "github.com/tendermint/tendermint/abci/types"
2018-07-16 18:15:50 -07:00
"github.com/cosmos/cosmos-sdk/baseapp"
2018-07-17 15:04:10 -07:00
sdk "github.com/cosmos/cosmos-sdk/types"
)
// AppStateFn returns the app state json bytes, the genesis accounts, and the chain identifier
type AppStateFn func(r *rand.Rand, accs []Account, genesisTimestamp time.Time) (appState json.RawMessage, accounts []Account, chainId string)
2018-11-07 20:26:02 -08:00
// initialize the chain for the simulation
func initChain(
r *rand.Rand, params Params, accounts []Account,
app *baseapp.BaseApp, appStateFn AppStateFn, genesisTimestamp time.Time,
) (mockValidators, []Account) {
appState, accounts, chainID := appStateFn(r, accounts, genesisTimestamp)
2018-11-07 20:26:02 -08:00
req := abci.RequestInitChain{
AppStateBytes: appState,
ChainId: chainID,
2018-08-29 23:02:15 -07:00
}
2018-11-07 20:26:02 -08:00
res := app.InitChain(req)
2018-11-07 22:57:37 -08:00
validators := newMockValidators(r, res.Validators, params)
2018-08-29 23:02:15 -07:00
return validators, accounts
2018-08-29 23:02:15 -07:00
}
2018-07-17 11:33:53 -07:00
// SimulateFromSeed tests an application by running the provided
// operations, testing the provided invariants, but using the provided seed.
// TODO: split this monster function up
func SimulateFromSeed(
tb testing.TB, w io.Writer, app *baseapp.BaseApp,
appStateFn AppStateFn, seed int64, ops WeightedOperations,
invariants sdk.Invariants,
numBlocks, exportParamsHeight, blockSize int,
exportParams, commit, lean, onOperation, allInvariants bool,
blackListedAccs map[string]bool,
) (stopEarly bool, exportedParams Params, err error) {
// in case we have to end early, don't os.Exit so that we can run cleanup code.
2018-09-01 12:32:24 -07:00
testingMode, t, b := getTestingMode(tb)
fmt.Fprintf(w, "Starting SimulateFromSeed with randomness created with seed %d\n", int(seed))
2018-11-07 21:12:51 -08:00
r := rand.New(rand.NewSource(seed))
params := RandomParams(r)
fmt.Fprintf(w, "Randomized simulation params: \n%s\n", mustMarshalJSONIndent(params))
2018-11-07 21:12:51 -08:00
genesisTimestamp := RandTimestamp(r)
fmt.Printf(
"Starting the simulation from time %v, unixtime %v\n",
genesisTimestamp.UTC().Format(time.UnixDate), genesisTimestamp.Unix(),
)
2018-11-07 21:12:51 -08:00
timeDiff := maxTimePerBlock - minTimePerBlock
accs := RandomAccounts(r, params.NumKeys)
eventStats := newEventStats()
2018-11-07 09:19:58 -08:00
// Second variable to keep pending validator set (delayed one block since
// TM 0.24) Initially this is the same as the initial validator set
validators, accs := initChain(r, params, accs, app, appStateFn, genesisTimestamp)
if len(accs) == 0 {
return true, params, fmt.Errorf("must have greater than zero genesis accounts")
}
// remove module account address if they exist in accs
var tmpAccs []Account
for _, acc := range accs {
if !blackListedAccs[acc.Address.String()] {
tmpAccs = append(tmpAccs, acc)
}
}
accs = tmpAccs
2018-11-07 22:57:37 -08:00
nextValidators := validators
2018-11-07 09:19:58 -08:00
header := abci.Header{
Height: 1,
Time: genesisTimestamp,
2018-11-07 22:57:37 -08:00
ProposerAddress: validators.randomProposer(r),
2018-11-07 09:19:58 -08:00
}
opCount := 0
// Setup code to catch SIGTERM's
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
go func() {
receivedSignal := <-c
fmt.Fprintf(w, "\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, header.Height, opCount)
err = fmt.Errorf("Exited due to %s", receivedSignal)
stopEarly = true
}()
2018-09-01 12:32:24 -07:00
var pastTimes []time.Time
var pastVoteInfos [][]abci.VoteInfo
2018-11-07 09:19:58 -08:00
request := RandomRequestBeginBlock(r, params,
validators, pastTimes, pastVoteInfos, eventStats.tally, header)
2018-11-07 07:28:18 -08:00
// These are operations which have been queued by previous operations
operationQueue := newOperationQueue()
timeOperationQueue := []FutureOperation{}
2019-03-14 11:13:15 -07:00
logWriter := NewLogWriter(testingMode)
2018-11-07 07:28:18 -08:00
blockSimulator := createBlockSimulator(
testingMode, tb, t, w, params, eventStats.tally, invariants,
2018-11-07 07:28:18 -08:00
ops, operationQueue, timeOperationQueue,
numBlocks, blockSize, logWriter, lean, onOperation, allInvariants)
2018-11-07 07:28:18 -08:00
if !testingMode {
b.ResetTimer()
} else {
// Recover logs in case of panic
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(w, "panic with err: %v\n", r)
stackTrace := string(debug.Stack())
fmt.Println(stackTrace)
2019-03-14 11:13:15 -07:00
logWriter.PrintLogs()
err = fmt.Errorf("Simulation halted due to panic on block %d", header.Height)
}
}()
}
2018-08-30 00:13:31 -07:00
// set exported params to the initial state
exportedParams = params
// TODO: split up the contents of this for loop into new functions
2019-03-14 11:13:15 -07:00
for height := 1; height <= numBlocks && !stopEarly; height++ {
2018-09-01 12:32:24 -07:00
// Log the header time for future lookup
pastTimes = append(pastTimes, header.Time)
pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes)
2018-09-01 12:32:24 -07:00
// Run the BeginBlock handler
2019-03-14 11:13:15 -07:00
logWriter.AddEntry(BeginBlockEntry(int64(height)))
app.BeginBlock(request)
2018-09-01 12:32:24 -07:00
if testingMode {
assertAllInvariants(t, app, invariants, "BeginBlock", logWriter, allInvariants)
2018-08-30 00:13:31 -07:00
}
ctx := app.NewContext(false, header)
2018-08-29 23:02:15 -07:00
// Run queued operations. Ignores blocksize if blocksize is too small
2018-11-07 07:28:18 -08:00
numQueuedOpsRan := runQueuedOperations(
operationQueue, int(header.Height),
2019-03-14 11:13:15 -07:00
tb, r, app, ctx, accs, logWriter, eventStats.tally, lean)
2018-11-07 07:28:18 -08:00
numQueuedTimeOpsRan := runQueuedTimeOperations(
2019-03-14 11:13:15 -07:00
timeOperationQueue, int(header.Height), header.Time,
tb, r, app, ctx, accs, logWriter, eventStats.tally, lean)
2018-10-18 18:55:14 -07:00
if testingMode && onOperation {
assertAllInvariants(t, app, invariants, "QueuedOperations", logWriter, allInvariants)
2018-10-18 18:55:14 -07:00
}
2019-03-14 11:13:15 -07:00
// run standard operations
operations := blockSimulator(r, app, ctx, accs, header)
opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan
2018-10-18 18:55:14 -07:00
if testingMode {
assertAllInvariants(t, app, invariants, "StandardOperations", logWriter, allInvariants)
2018-10-18 18:55:14 -07:00
}
res := app.EndBlock(abci.RequestEndBlock{})
header.Height++
2018-11-07 07:28:18 -08:00
header.Time = header.Time.Add(
time.Duration(minTimePerBlock) * time.Second)
header.Time = header.Time.Add(
time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second)
2018-11-07 22:57:37 -08:00
header.ProposerAddress = validators.randomProposer(r)
2019-03-14 11:13:15 -07:00
logWriter.AddEntry(EndBlockEntry(int64(height)))
2018-09-01 12:32:24 -07:00
if testingMode {
assertAllInvariants(t, app, invariants, "EndBlock", logWriter, allInvariants)
2018-08-30 00:13:31 -07:00
}
if commit {
app.Commit()
}
if header.ProposerAddress == nil {
fmt.Fprintf(w, "\nSimulation stopped early as all validators have been unbonded; nobody left to propose a block!\n")
stopEarly = true
break
}
// Generate a random RequestBeginBlock with the current validator set
// for the next block
2018-11-07 09:19:58 -08:00
request = RandomRequestBeginBlock(r, params, validators,
pastTimes, pastVoteInfos, eventStats.tally, header)
// Update the validator set, which will be reflected in the application
// on the next block
validators = nextValidators
2018-11-07 09:19:58 -08:00
nextValidators = updateValidators(tb, r, params,
validators, res.ValidatorUpdates, eventStats.tally)
// update the exported params
if exportParams && exportParamsHeight == height {
exportedParams = params
}
}
if stopEarly {
eventStats.Print(w)
return true, exportedParams, err
}
fmt.Fprintf(
w,
"\nSimulation complete; Final height (blocks): %d, final time (seconds): %v, operations ran: %d\n",
header.Height, header.Time, opCount,
)
eventStats.Print(w)
return false, exportedParams, nil
}
//______________________________________________________________________________
2018-11-07 07:28:18 -08:00
type blockSimFn func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
2019-03-14 11:13:15 -07:00
accounts []Account, header abci.Header) (opCount int)
2018-10-31 10:11:41 -07:00
2018-11-07 20:26:02 -08:00
// Returns a function to simulate blocks. Written like this to avoid constant
2018-11-07 21:12:51 -08:00
// parameters being passed everytime, to minimize memory overhead.
func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, w io.Writer, params Params,
event func(string), invariants sdk.Invariants, ops WeightedOperations,
2018-11-07 21:12:51 -08:00
operationQueue OperationQueue, timeOperationQueue []FutureOperation,
totalNumBlocks, avgBlockSize int, logWriter LogWriter, lean, onOperation, allInvariants bool) blockSimFn {
2018-10-31 09:37:50 -07:00
2019-03-14 11:13:15 -07:00
lastBlocksizeState := 0 // state for [4 * uniform distribution]
blocksize := 0
2018-11-07 20:26:02 -08:00
selectOp := ops.getSelectOpFn()
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
2019-03-14 11:13:15 -07:00
accounts []Account, header abci.Header) (opCount int) {
2018-11-07 07:28:18 -08:00
fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ",
2018-11-07 07:28:18 -08:00
header.Height, totalNumBlocks, opCount, blocksize)
lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize)
2018-11-07 07:28:18 -08:00
type opAndR struct {
op Operation
rand *rand.Rand
}
opAndRz := make([]opAndR, 0, blocksize)
// Predetermine the blocksize slice so that we can do things like block
// out certain operations without changing the ops that follow.
for i := 0; i < blocksize; i++ {
opAndRz = append(opAndRz, opAndR{
op: selectOp(r),
rand: DeriveRand(r),
})
}
for i := 0; i < blocksize; i++ {
// NOTE: the Rand 'r' should not be used here.
opAndR := opAndRz[i]
op, r2 := opAndR.op, opAndR.rand
2019-03-14 11:13:15 -07:00
opMsg, futureOps, err := op(r2, app, ctx, accounts)
opMsg.LogEvent(event)
if !lean || opMsg.OK {
logWriter.AddEntry(MsgEntry(header.Height, opMsg, int64(i)))
}
if err != nil {
2019-03-14 11:13:15 -07:00
logWriter.PrintLogs()
2018-11-07 07:28:18 -08:00
tb.Fatalf("error on operation %d within block %d, %v",
header.Height, opCount, err)
}
queueOperations(operationQueue, timeOperationQueue, futureOps)
2018-09-01 12:32:24 -07:00
if testingMode {
if onOperation {
fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ",
header.Height, totalNumBlocks, opCount, blocksize)
2019-03-14 11:13:15 -07:00
eventStr := fmt.Sprintf("operation: %v", opMsg.String())
assertAllInvariants(t, app, invariants, eventStr, logWriter, allInvariants)
} else if opCount%50 == 0 {
fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ",
2018-11-07 07:28:18 -08:00
header.Height, totalNumBlocks, opCount, blocksize)
}
}
opCount++
}
return opCount
}
}
2018-09-03 09:47:24 -07:00
// nolint: errcheck
2018-11-07 09:00:13 -08:00
func runQueuedOperations(queueOps map[int][]Operation,
height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp,
2019-03-14 11:13:15 -07:00
ctx sdk.Context, accounts []Account, logWriter LogWriter, tallyEvent func(string), lean bool) (numOpsRan int) {
2018-11-07 07:28:18 -08:00
2018-11-07 09:19:58 -08:00
queuedOp, ok := queueOps[height]
if !ok {
return 0
}
numOpsRan = len(queuedOp)
for i := 0; i < numOpsRan; i++ {
2018-11-07 09:19:58 -08:00
// For now, queued operations cannot queue more operations.
// If a need arises for us to support queued messages to queue more messages, this can
// be changed.
2019-03-14 11:13:15 -07:00
opMsg, _, err := queuedOp[i](r, app, ctx, accounts)
opMsg.LogEvent(tallyEvent)
if !lean || opMsg.OK {
logWriter.AddEntry((QueuedMsgEntry(int64(height), opMsg)))
}
2018-11-07 09:19:58 -08:00
if err != nil {
2019-03-14 11:13:15 -07:00
logWriter.PrintLogs()
2018-11-07 09:19:58 -08:00
tb.FailNow()
}
}
2018-11-07 09:19:58 -08:00
delete(queueOps, height)
return numOpsRan
}
2018-11-07 09:00:13 -08:00
func runQueuedTimeOperations(queueOps []FutureOperation,
2019-03-14 11:13:15 -07:00
height int, currentTime time.Time, tb testing.TB, r *rand.Rand,
2018-11-07 07:28:18 -08:00
app *baseapp.BaseApp, ctx sdk.Context, accounts []Account,
2019-03-14 11:13:15 -07:00
logWriter LogWriter, tallyEvent func(string), lean bool) (numOpsRan int) {
numOpsRan = 0
2018-11-07 09:00:13 -08:00
for len(queueOps) > 0 && currentTime.After(queueOps[0].BlockTime) {
// For now, queued operations cannot queue more operations.
// If a need arises for us to support queued messages to queue more messages, this can
// be changed.
2019-03-14 11:13:15 -07:00
opMsg, _, err := queueOps[0].Op(r, app, ctx, accounts)
opMsg.LogEvent(tallyEvent)
if !lean || opMsg.OK {
logWriter.AddEntry(QueuedMsgEntry(int64(height), opMsg))
}
if err != nil {
2019-03-14 11:13:15 -07:00
logWriter.PrintLogs()
tb.FailNow()
}
2018-11-07 09:00:13 -08:00
queueOps = queueOps[1:]
numOpsRan++
}
return numOpsRan
}