Merge branch 'develop' into cwgoes/fix-gov-sim-more-import-export

This commit is contained in:
Christopher Goes 2018-11-09 16:55:20 +01:00
commit 241f6364e9
16 changed files with 948 additions and 746 deletions

View File

@ -47,8 +47,7 @@ BUG FIXES
* Gaia
* [\#2670](https://github.com/cosmos/cosmos-sdk/issues/2670) [x/stake] fixed incorrect `IterateBondedValidators` and split into two functions: `IterateBondedValidators` and `IterateLastBlockConsValidators`
* [\#2691](https://github.com/cosmos/cosmos-sdk/issues/2691) Fix local testnet creation by using a single canonical genesis time
- [\#2670](https://github.com/cosmos/cosmos-sdk/issues/2670) [x/stake] fixed incorrent `IterateBondedValidators` and split into two functions: `IterateBondedValidators` and `IterateLastBlockConsValidators`
- [\#2648](https://github.com/cosmos/cosmos-sdk/issues/2648) [gaiad] Fix `gaiad export` / `gaiad import` consistency, test in CI
* [\#2648](https://github.com/cosmos/cosmos-sdk/issues/2648) [gaiad] Fix `gaiad export` / `gaiad import` consistency, test in CI
* SDK
* [\#2625](https://github.com/cosmos/cosmos-sdk/issues/2625) [x/gov] fix AppendTag function usage error

View File

@ -31,10 +31,12 @@ IMPROVEMENTS
* Gaia REST API (`gaiacli advanced rest-server`)
* Gaia CLI (`gaiacli`)
* [\#2749](https://github.com/cosmos/cosmos-sdk/pull/2749) Add --chain-id flag to gaiad testnet
* Gaia
* SDK
- [x/mock/simulation] [\#2720] major cleanup, introduction of helper objects, reorganization
* Tendermint

View File

@ -75,14 +75,20 @@ Example:
cmd.Flags().String(flagStartingIPAddress, "192.168.0.1",
"Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:46656, ID1@192.168.0.2:46656, ...)")
cmd.Flags().String(client.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created")
return cmd
}
func initTestnet(config *cfg.Config, cdc *codec.Codec) error {
var chainID string
outDir := viper.GetString(flagOutputDir)
numValidators := viper.GetInt(flagNumValidators)
chainID := "chain-" + cmn.RandStr(6)
chainID = viper.GetString(client.FlagChainID)
if chainID == "" {
chainID = "chain-" + cmn.RandStr(6)
}
monikers := make([]string, numValidators)
nodeIDs := make([]string, numValidators)

View File

@ -0,0 +1,51 @@
package simulation
import (
"math/rand"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Account contains a privkey, pubkey, address tuple
// eventually more useful data can be placed in here.
// (e.g. number of coins)
type Account struct {
PrivKey crypto.PrivKey
PubKey crypto.PubKey
Address sdk.AccAddress
}
// are two accounts equal
func (acc Account) Equals(acc2 Account) bool {
return acc.Address.Equals(acc2.Address)
}
// RandomAcc pick a random account from an array
func RandomAcc(r *rand.Rand, accs []Account) Account {
return accs[r.Intn(
len(accs),
)]
}
// RandomAccounts generates n random accounts
func RandomAccounts(r *rand.Rand, n int) []Account {
accs := make([]Account, n)
for i := 0; i < n; i++ {
// don't need that much entropy for simulation
privkeySeed := make([]byte, 15)
r.Read(privkeySeed)
useSecp := r.Int63()%2 == 0
if useSecp {
accs[i].PrivKey = secp256k1.GenPrivKeySecp256k1(privkeySeed)
} else {
accs[i].PrivKey = ed25519.GenPrivKeyFromSecret(privkeySeed)
}
accs[i].PubKey = accs[i].PrivKey.PubKey()
accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address())
}
return accs
}

View File

@ -2,26 +2,24 @@
Package simulation implements a simulation framework for any state machine
built on the SDK which utilizes auth.
It is primarily intended for fuzz testing the integration of modules.
It will test that the provided operations are interoperable,
and that the desired invariants hold.
It can additionally be used to detect what the performance benchmarks in the
system are, by using benchmarking mode and cpu / mem profiling.
If it detects a failure, it provides the entire log of what was ran,
It is primarily intended for fuzz testing the integration of modules. It will
test that the provided operations are interoperable, and that the desired
invariants hold. It can additionally be used to detect what the performance
benchmarks in the system are, by using benchmarking mode and cpu / mem
profiling. If it detects a failure, it provides the entire log of what was ran.
The simulator takes as input: a random seed, the set of operations to run,
the invariants to test, and additional parameters to configure how long to run,
and misc. parameters that affect simulation speed.
The simulator takes as input: a random seed, the set of operations to run, the
invariants to test, and additional parameters to configure how long to run, and
misc. parameters that affect simulation speed.
It is intended that every module provides a list of Operations which will randomly
create and run a message / tx in a manner that is interesting to fuzz, and verify that
the state transition was executed as expected.
Each module should additionally provide methods to assert that the desired invariants hold.
It is intended that every module provides a list of Operations which will
randomly create and run a message / tx in a manner that is interesting to fuzz,
and verify that the state transition was executed as expected. Each module
should additionally provide methods to assert that the desired invariants hold.
Then to perform a randomized simulation, select the set of desired operations,
the weightings for each, the invariants you want to test, and how long to run it for.
Then run simulation.Simulate!
The simulator will handle things like ensuring that validators periodically double signing,
or go offline.
the weightings for each, the invariants you want to test, and how long to run
it for. Then run simulation.Simulate! The simulator will handle things like
ensuring that validators periodically double signing, or go offline.
*/
package simulation

View File

@ -0,0 +1,30 @@
package simulation
import (
"fmt"
"sort"
)
type eventStats map[string]uint
func newEventStats() eventStats {
events := make(map[string]uint)
return events
}
func (es eventStats) tally(eventDesc string) {
es[eventDesc]++
}
// Pretty-print events as a table
func (es eventStats) Print() {
var keys []string
for key := range es {
keys = append(keys, key)
}
sort.Strings(keys)
fmt.Printf("Event statistics: \n")
for _, key := range keys {
fmt.Printf(" % 60s => %d\n", key, es[key])
}
}

View File

@ -0,0 +1,30 @@
package simulation
import (
"fmt"
"testing"
"github.com/cosmos/cosmos-sdk/baseapp"
)
// An Invariant is a function which tests a particular invariant.
// If the invariant has been broken, it should return an error
// containing a descriptive message about what happened.
// The simulator will then halt and print the logs.
type Invariant func(app *baseapp.BaseApp) error
// group of Invarient
type Invariants []Invariant
// assertAll asserts the all invariants against application state
func (invs Invariants) assertAll(t *testing.T, app *baseapp.BaseApp,
event string, displayLogs func()) {
for i := 0; i < len(invs); i++ {
if err := invs[i](app); err != nil {
fmt.Printf("Invariants broken after %s\n%s\n", event, err.Error())
displayLogs()
t.Fatal()
}
}
}

View File

@ -0,0 +1,201 @@
package simulation
import (
"fmt"
"math/rand"
"sort"
"testing"
"time"
abci "github.com/tendermint/tendermint/abci/types"
cmn "github.com/tendermint/tendermint/libs/common"
tmtypes "github.com/tendermint/tendermint/types"
)
type mockValidator struct {
val abci.ValidatorUpdate
livenessState int
}
type mockValidators map[string]mockValidator
// get mockValidators from abci validators
func newMockValidators(r *rand.Rand, abciVals []abci.ValidatorUpdate,
params Params) mockValidators {
validators := make(mockValidators)
for _, validator := range abciVals {
str := fmt.Sprintf("%v", validator.PubKey)
liveliness := GetMemberOfInitialState(r,
params.InitialLivenessWeightings)
validators[str] = mockValidator{
val: validator,
livenessState: liveliness,
}
}
return validators
}
// TODO describe usage
func (vals mockValidators) getKeys() []string {
keys := make([]string, len(vals))
i := 0
for key := range vals {
keys[i] = key
i++
}
sort.Strings(keys)
return keys
}
//_________________________________________________________________________________
// randomProposer picks a random proposer from the current validator set
func (vals mockValidators) randomProposer(r *rand.Rand) cmn.HexBytes {
keys := vals.getKeys()
if len(keys) == 0 {
return nil
}
key := keys[r.Intn(len(keys))]
proposer := vals[key].val
pk, err := tmtypes.PB2TM.PubKey(proposer.PubKey)
if err != nil {
panic(err)
}
return pk.Address()
}
// updateValidators mimicks Tendermint's update logic
// nolint: unparam
func updateValidators(tb testing.TB, r *rand.Rand, params Params,
current map[string]mockValidator, updates []abci.ValidatorUpdate,
event func(string)) map[string]mockValidator {
for _, update := range updates {
str := fmt.Sprintf("%v", update.PubKey)
if update.Power == 0 {
if _, ok := current[str]; !ok {
tb.Fatalf("tried to delete a nonexistent validator")
}
event("endblock/validatorupdates/kicked")
delete(current, str)
} else if mVal, ok := current[str]; ok {
// validator already exists
mVal.val = update
event("endblock/validatorupdates/updated")
} else {
// Set this new validator
current[str] = mockValidator{
update,
GetMemberOfInitialState(r, params.InitialLivenessWeightings),
}
event("endblock/validatorupdates/added")
}
}
return current
}
// RandomRequestBeginBlock generates a list of signing validators according to
// the provided list of validators, signing fraction, and evidence fraction
func RandomRequestBeginBlock(r *rand.Rand, params Params,
validators mockValidators, pastTimes []time.Time,
pastVoteInfos [][]abci.VoteInfo,
event func(string), header abci.Header) abci.RequestBeginBlock {
if len(validators) == 0 {
return abci.RequestBeginBlock{
Header: header,
}
}
voteInfos := make([]abci.VoteInfo, len(validators))
for i, key := range validators.getKeys() {
mVal := validators[key]
mVal.livenessState = params.LivenessTransitionMatrix.NextState(r, mVal.livenessState)
signed := true
if mVal.livenessState == 1 {
// spotty connection, 50% probability of success
// See https://github.com/golang/go/issues/23804#issuecomment-365370418
// for reasoning behind computing like this
signed = r.Int63()%2 == 0
} else if mVal.livenessState == 2 {
// offline
signed = false
}
if signed {
event("beginblock/signing/signed")
} else {
event("beginblock/signing/missed")
}
pubkey, err := tmtypes.PB2TM.PubKey(mVal.val.PubKey)
if err != nil {
panic(err)
}
voteInfos[i] = abci.VoteInfo{
Validator: abci.Validator{
Address: pubkey.Address(),
Power: mVal.val.Power,
},
SignedLastBlock: signed,
}
}
// return if no past times
if len(pastTimes) <= 0 {
return abci.RequestBeginBlock{
Header: header,
LastCommitInfo: abci.LastCommitInfo{
Votes: voteInfos,
},
}
}
// TODO: Determine capacity before allocation
evidence := make([]abci.Evidence, 0)
for r.Float64() < params.EvidenceFraction {
height := header.Height
time := header.Time
vals := voteInfos
if r.Float64() < params.PastEvidenceFraction {
height = int64(r.Intn(int(header.Height) - 1))
time = pastTimes[height]
vals = pastVoteInfos[height]
}
validator := vals[r.Intn(len(vals))].Validator
var totalVotingPower int64
for _, val := range vals {
totalVotingPower += val.Validator.Power
}
evidence = append(evidence,
abci.Evidence{
Type: tmtypes.ABCIEvidenceTypeDuplicateVote,
Validator: validator,
Height: height,
Time: time,
TotalVotingPower: totalVotingPower,
},
)
event("beginblock/evidence")
}
return abci.RequestBeginBlock{
Header: header,
LastCommitInfo: abci.LastCommitInfo{
Votes: voteInfos,
},
ByzantineValidators: evidence,
}
}

View File

@ -0,0 +1,112 @@
package simulation
import (
"math/rand"
"sort"
"time"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Operation runs a state machine transition, and ensures the transition
// happened as expected. The operation could be running and testing a fuzzed
// transaction, or doing the same for a message.
//
// For ease of debugging, an operation returns a descriptive message "action",
// which details what this fuzzed state machine transition actually did.
//
// Operations can optionally provide a list of "FutureOperations" to run later
// These will be ran at the beginning of the corresponding block.
type Operation func(r *rand.Rand, app *baseapp.BaseApp,
ctx sdk.Context, accounts []Account, event func(string)) (
action string, futureOps []FutureOperation, err error)
// queue of operations
type OperationQueue map[int][]Operation
func newOperationQueue() OperationQueue {
operationQueue := make(OperationQueue)
return operationQueue
}
// adds all future operations into the operation queue.
func queueOperations(queuedOps OperationQueue,
queuedTimeOps []FutureOperation, futureOps []FutureOperation) {
if futureOps == nil {
return
}
for _, futureOp := range futureOps {
if futureOp.BlockHeight != 0 {
if val, ok := queuedOps[futureOp.BlockHeight]; ok {
queuedOps[futureOp.BlockHeight] = append(val, futureOp.Op)
} else {
queuedOps[futureOp.BlockHeight] = []Operation{futureOp.Op}
}
continue
}
// TODO: Replace with proper sorted data structure, so don't have the
// copy entire slice
index := sort.Search(
len(queuedTimeOps),
func(i int) bool {
return queuedTimeOps[i].BlockTime.After(futureOp.BlockTime)
},
)
queuedTimeOps = append(queuedTimeOps, FutureOperation{})
copy(queuedTimeOps[index+1:], queuedTimeOps[index:])
queuedTimeOps[index] = futureOp
}
}
//________________________________________________________________________
// FutureOperation is an operation which will be ran at the beginning of the
// provided BlockHeight. If both a BlockHeight and BlockTime are specified, it
// will use the BlockHeight. In the (likely) event that multiple operations
// are queued at the same block height, they will execute in a FIFO pattern.
type FutureOperation struct {
BlockHeight int
BlockTime time.Time
Op Operation
}
//________________________________________________________________________
// WeightedOperation is an operation with associated weight.
// This is used to bias the selection operation within the simulator.
type WeightedOperation struct {
Weight int
Op Operation
}
// WeightedOperations is the group of all weighted operations to simulate.
type WeightedOperations []WeightedOperation
func (ops WeightedOperations) totalWeight() int {
totalOpWeight := 0
for _, op := range ops {
totalOpWeight += op.Weight
}
return totalOpWeight
}
type selectOpFn func(r *rand.Rand) Operation
func (ops WeightedOperations) getSelectOpFn() selectOpFn {
totalOpWeight := ops.totalWeight()
return func(r *rand.Rand) Operation {
x := r.Intn(totalOpWeight)
for i := 0; i < len(ops); i++ {
if x <= ops[i].Weight {
return ops[i].Op
}
x -= ops[i].Weight
}
// shouldn't happen
return ops[0].Op
}
}

View File

@ -15,15 +15,18 @@ const (
onOperation bool = false
)
// TODO explain transitional matrix usage
var (
// Currently there are 3 different liveness types, fully online, spotty connection, offline.
// Currently there are 3 different liveness types,
// fully online, spotty connection, offline.
defaultLivenessTransitionMatrix, _ = CreateTransitionMatrix([][]int{
{90, 20, 1},
{10, 50, 5},
{0, 10, 1000},
})
// 3 states: rand in range [0, 4*provided blocksize], rand in range [0, 2 * provided blocksize], 0
// 3 states: rand in range [0, 4*provided blocksize],
// rand in range [0, 2 * provided blocksize], 0
defaultBlockSizeTransitionMatrix, _ = CreateTransitionMatrix([][]int{
{85, 5, 0},
{15, 92, 1},

View File

@ -0,0 +1,64 @@
package simulation
import (
"math/big"
"math/rand"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
// shamelessly copied from
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang#31832326
// Generate a random string of a particular length
func RandStringOfLength(r *rand.Rand, n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, r.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = r.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}
// Generate a random amount
func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {
return sdk.NewInt(int64(r.Intn(int(max.Int64()))))
}
// RandomDecAmount generates a random decimal amount
func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec {
randInt := big.NewInt(0).Rand(r, max.Int)
return sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision)
}
// RandomSetGenesis wraps mock.RandomSetGenesis, but using simulation accounts
func RandomSetGenesis(r *rand.Rand, app *mock.App, accs []Account, denoms []string) {
addrs := make([]sdk.AccAddress, len(accs))
for i := 0; i < len(accs); i++ {
addrs[i] = accs[i].Address
}
mock.RandomSetGenesis(r, app, addrs, denoms)
}
// RandTimestamp generates a random timestamp
func RandTimestamp(r *rand.Rand) time.Time {
// json.Marshal breaks for timestamps greater with year greater than 9999
unixTime := r.Int63n(253373529600)
return time.Unix(unixTime, 0)
}

View File

@ -1,488 +0,0 @@
package simulation
import (
"encoding/json"
"fmt"
"math/rand"
"os"
"os/signal"
"runtime/debug"
"sort"
"strings"
"syscall"
"testing"
"time"
abci "github.com/tendermint/tendermint/abci/types"
cmn "github.com/tendermint/tendermint/libs/common"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Simulate tests application by sending random messages.
func Simulate(t *testing.T, app *baseapp.BaseApp,
appStateFn func(r *rand.Rand, accs []Account) json.RawMessage,
ops []WeightedOperation, setups []RandSetup,
invariants []Invariant, numBlocks int, blockSize int, commit bool) error {
time := time.Now().UnixNano()
return SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numBlocks, blockSize, commit)
}
func initChain(r *rand.Rand, params Params, accounts []Account, setups []RandSetup, app *baseapp.BaseApp,
appStateFn func(r *rand.Rand, accounts []Account) json.RawMessage) (validators map[string]mockValidator) {
res := app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, accounts)})
validators = make(map[string]mockValidator)
for _, validator := range res.Validators {
str := fmt.Sprintf("%v", validator.PubKey)
validators[str] = mockValidator{validator, GetMemberOfInitialState(r, params.InitialLivenessWeightings)}
}
for i := 0; i < len(setups); i++ {
setups[i](r, accounts)
}
return
}
func randTimestamp(r *rand.Rand) time.Time {
// json.Marshal breaks for timestamps greater with year greater than 9999
unixTime := r.Int63n(253373529600)
return time.Unix(unixTime, 0)
}
// SimulateFromSeed tests an application by running the provided
// operations, testing the provided invariants, but using the provided seed.
func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp,
appStateFn func(r *rand.Rand, accs []Account) json.RawMessage,
seed int64, ops []WeightedOperation, setups []RandSetup, invariants []Invariant,
numBlocks int, blockSize int, commit bool) (simError error) {
// in case we have to end early, don't os.Exit so that we can run cleanup code.
stopEarly := false
testingMode, t, b := getTestingMode(tb)
fmt.Printf("Starting SimulateFromSeed with randomness created with seed %d\n", int(seed))
r := rand.New(rand.NewSource(seed))
params := RandomParams(r) // := DefaultParams()
fmt.Printf("Randomized simulation params: %+v\n", params)
timestamp := randTimestamp(r)
fmt.Printf("Starting the simulation from time %v, unixtime %v\n", timestamp.UTC().Format(time.UnixDate), timestamp.Unix())
timeDiff := maxTimePerBlock - minTimePerBlock
accs := RandomAccounts(r, params.NumKeys)
// Setup event stats
events := make(map[string]uint)
event := func(what string) {
events[what]++
}
validators := initChain(r, params, accs, setups, app, appStateFn)
// Second variable to keep pending validator set (delayed one block since TM 0.24)
// Initially this is the same as the initial validator set
nextValidators := validators
header := abci.Header{Height: 1, Time: timestamp, ProposerAddress: randomProposer(r, validators)}
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.Printf("\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, header.Height, opCount)
simError = fmt.Errorf("Exited due to %s", receivedSignal)
stopEarly = true
}()
var pastTimes []time.Time
var pastVoteInfos [][]abci.VoteInfo
request := RandomRequestBeginBlock(r, params, validators, pastTimes, pastVoteInfos, event, header)
// These are operations which have been queued by previous operations
operationQueue := make(map[int][]Operation)
timeOperationQueue := []FutureOperation{}
var blockLogBuilders []*strings.Builder
if testingMode {
blockLogBuilders = make([]*strings.Builder, numBlocks)
}
displayLogs := logPrinter(testingMode, blockLogBuilders)
blockSimulator := createBlockSimulator(testingMode, tb, t, params, event, invariants, ops, operationQueue, timeOperationQueue, numBlocks, blockSize, displayLogs)
if !testingMode {
b.ResetTimer()
} else {
// Recover logs in case of panic
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic with err\n", r)
stackTrace := string(debug.Stack())
fmt.Println(stackTrace)
displayLogs()
simError = fmt.Errorf("Simulation halted due to panic on block %d", header.Height)
}
}()
}
for i := 0; i < numBlocks && !stopEarly; i++ {
// Log the header time for future lookup
pastTimes = append(pastTimes, header.Time)
pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes)
// Construct log writer
logWriter := addLogMessage(testingMode, blockLogBuilders, i)
// Run the BeginBlock handler
logWriter("BeginBlock")
app.BeginBlock(request)
if testingMode {
// Make sure invariants hold at beginning of block
assertAllInvariants(t, app, invariants, "BeginBlock", displayLogs)
}
ctx := app.NewContext(false, header)
// Run queued operations. Ignores blocksize if blocksize is too small
logWriter("Queued operations")
numQueuedOpsRan := runQueuedOperations(operationQueue, int(header.Height), tb, r, app, ctx, accs, logWriter, displayLogs, event)
numQueuedTimeOpsRan := runQueuedTimeOperations(timeOperationQueue, header.Time, tb, r, app, ctx, accs, logWriter, displayLogs, event)
if testingMode && onOperation {
// Make sure invariants hold at end of queued operations
assertAllInvariants(t, app, invariants, "QueuedOperations", displayLogs)
}
logWriter("Standard operations")
operations := blockSimulator(r, app, ctx, accs, header, logWriter)
opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan
if testingMode {
// Make sure invariants hold at end of block
assertAllInvariants(t, app, invariants, "StandardOperations", displayLogs)
}
res := app.EndBlock(abci.RequestEndBlock{})
header.Height++
header.Time = header.Time.Add(time.Duration(minTimePerBlock) * time.Second).Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second)
header.ProposerAddress = randomProposer(r, validators)
logWriter("EndBlock")
if testingMode {
// Make sure invariants hold at end of block
assertAllInvariants(t, app, invariants, "EndBlock", displayLogs)
}
if commit {
app.Commit()
}
if header.ProposerAddress == nil {
fmt.Printf("\nSimulation stopped early as all validators have been unbonded, there is nobody left propose a block!\n")
stopEarly = true
break
}
// Generate a random RequestBeginBlock with the current validator set for the next block
request = RandomRequestBeginBlock(r, params, validators, pastTimes, pastVoteInfos, event, header)
// Update the validator set, which will be reflected in the application on the next block
validators = nextValidators
nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, event)
}
if stopEarly {
DisplayEvents(events)
return
}
fmt.Printf("\nSimulation complete. Final height (blocks): %d, final time (seconds), : %v, operations ran %d\n", header.Height, header.Time, opCount)
DisplayEvents(events)
return nil
}
type blockSimFn func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, header abci.Header, logWriter func(string),
) (opCount int)
// Returns a function to simulate blocks. Written like this to avoid constant parameters being passed everytime, to minimize
// memory overhead
func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, params Params,
event func(string), invariants []Invariant,
ops []WeightedOperation, operationQueue map[int][]Operation, timeOperationQueue []FutureOperation,
totalNumBlocks int, avgBlockSize int, displayLogs func()) blockSimFn {
var (
lastBlocksizeState = 0 // state for [4 * uniform distribution]
totalOpWeight = 0
blocksize int
)
for i := 0; i < len(ops); i++ {
totalOpWeight += ops[i].Weight
}
selectOp := func(r *rand.Rand) Operation {
x := r.Intn(totalOpWeight)
for i := 0; i < len(ops); i++ {
if x <= ops[i].Weight {
return ops[i].Op
}
x -= ops[i].Weight
}
// shouldn't happen
return ops[0].Op
}
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, header abci.Header, logWriter func(string)) (opCount int) {
fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", header.Height, totalNumBlocks, opCount, blocksize)
lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize)
for j := 0; j < blocksize; j++ {
logUpdate, futureOps, err := selectOp(r)(r, app, ctx, accounts, event)
logWriter(logUpdate)
if err != nil {
displayLogs()
tb.Fatalf("error on operation %d within block %d, %v", header.Height, opCount, err)
}
queueOperations(operationQueue, timeOperationQueue, futureOps)
if testingMode {
if onOperation {
assertAllInvariants(t, app, invariants, fmt.Sprintf("operation: %v", logUpdate), displayLogs)
}
if opCount%50 == 0 {
fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", header.Height, totalNumBlocks, opCount, blocksize)
}
}
opCount++
}
return opCount
}
}
func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B) {
testingMode = false
if _t, ok := tb.(*testing.T); ok {
t = _t
testingMode = true
} else {
b = tb.(*testing.B)
}
return
}
// getBlockSize returns a block size as determined from the transition matrix.
// It targets making average block size the provided parameter. The three
// states it moves between are:
// "over stuffed" blocks with average size of 2 * avgblocksize,
// normal sized blocks, hitting avgBlocksize on average,
// and empty blocks, with no txs / only txs scheduled from the past.
func getBlockSize(r *rand.Rand, params Params, lastBlockSizeState, avgBlockSize int) (state, blocksize int) {
// TODO: Make default blocksize transition matrix actually make the average
// blocksize equal to avgBlockSize.
state = params.BlockSizeTransitionMatrix.NextState(r, lastBlockSizeState)
if state == 0 {
blocksize = r.Intn(avgBlockSize * 4)
} else if state == 1 {
blocksize = r.Intn(avgBlockSize * 2)
} else {
blocksize = 0
}
return
}
// adds all future operations into the operation queue.
func queueOperations(queuedOperations map[int][]Operation, queuedTimeOperations []FutureOperation, futureOperations []FutureOperation) {
if futureOperations == nil {
return
}
for _, futureOp := range futureOperations {
if futureOp.BlockHeight != 0 {
if val, ok := queuedOperations[futureOp.BlockHeight]; ok {
queuedOperations[futureOp.BlockHeight] = append(val, futureOp.Op)
} else {
queuedOperations[futureOp.BlockHeight] = []Operation{futureOp.Op}
}
} else {
// TODO: Replace with proper sorted data structure, so don't have the copy entire slice
index := sort.Search(len(queuedTimeOperations), func(i int) bool { return queuedTimeOperations[i].BlockTime.After(futureOp.BlockTime) })
queuedTimeOperations = append(queuedTimeOperations, FutureOperation{})
copy(queuedTimeOperations[index+1:], queuedTimeOperations[index:])
queuedTimeOperations[index] = futureOp
}
}
}
// nolint: errcheck
func runQueuedOperations(queueOperations map[int][]Operation, height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, logWriter func(string), displayLogs func(), event func(string)) (numOpsRan int) {
if queuedOps, ok := queueOperations[height]; ok {
numOps := len(queuedOps)
for i := 0; i < numOps; i++ {
// 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.
logUpdate, _, err := queuedOps[i](r, app, ctx, accounts, event)
logWriter(logUpdate)
if err != nil {
displayLogs()
tb.FailNow()
}
}
delete(queueOperations, height)
return numOps
}
return 0
}
func runQueuedTimeOperations(queueOperations []FutureOperation, currentTime time.Time, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, logWriter func(string), displayLogs func(), event func(string)) (numOpsRan int) {
numOpsRan = 0
for len(queueOperations) > 0 && currentTime.After(queueOperations[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.
logUpdate, _, err := queueOperations[0].Op(r, app, ctx, accounts, event)
logWriter(logUpdate)
if err != nil {
displayLogs()
tb.FailNow()
}
queueOperations = queueOperations[1:]
numOpsRan++
}
return numOpsRan
}
func getKeys(validators map[string]mockValidator) []string {
keys := make([]string, len(validators))
i := 0
for key := range validators {
keys[i] = key
i++
}
sort.Strings(keys)
return keys
}
// randomProposer picks a random proposer from the current validator set
func randomProposer(r *rand.Rand, validators map[string]mockValidator) cmn.HexBytes {
keys := getKeys(validators)
if len(keys) == 0 {
return nil
}
key := keys[r.Intn(len(keys))]
proposer := validators[key].val
pk, err := tmtypes.PB2TM.PubKey(proposer.PubKey)
if err != nil {
panic(err)
}
return pk.Address()
}
// RandomRequestBeginBlock generates a list of signing validators according to the provided list of validators, signing fraction, and evidence fraction
// nolint: unparam
func RandomRequestBeginBlock(r *rand.Rand, params Params, validators map[string]mockValidator,
pastTimes []time.Time, pastVoteInfos [][]abci.VoteInfo, event func(string), header abci.Header) abci.RequestBeginBlock {
if len(validators) == 0 {
return abci.RequestBeginBlock{Header: header}
}
voteInfos := make([]abci.VoteInfo, len(validators))
i := 0
for _, key := range getKeys(validators) {
mVal := validators[key]
mVal.livenessState = params.LivenessTransitionMatrix.NextState(r, mVal.livenessState)
signed := true
if mVal.livenessState == 1 {
// spotty connection, 50% probability of success
// See https://github.com/golang/go/issues/23804#issuecomment-365370418
// for reasoning behind computing like this
signed = r.Int63()%2 == 0
} else if mVal.livenessState == 2 {
// offline
signed = false
}
if signed {
event("beginblock/signing/signed")
} else {
event("beginblock/signing/missed")
}
pubkey, err := tmtypes.PB2TM.PubKey(mVal.val.PubKey)
if err != nil {
panic(err)
}
voteInfos[i] = abci.VoteInfo{
Validator: abci.Validator{
Address: pubkey.Address(),
Power: mVal.val.Power,
},
SignedLastBlock: signed,
}
i++
}
// TODO: Determine capacity before allocation
evidence := make([]abci.Evidence, 0)
// Anything but the first block
if len(pastTimes) > 0 {
for r.Float64() < params.EvidenceFraction {
height := header.Height
time := header.Time
vals := voteInfos
if r.Float64() < params.PastEvidenceFraction {
height = int64(r.Intn(int(header.Height) - 1))
time = pastTimes[height]
vals = pastVoteInfos[height]
}
validator := vals[r.Intn(len(vals))].Validator
var totalVotingPower int64
for _, val := range vals {
totalVotingPower += val.Validator.Power
}
evidence = append(evidence, abci.Evidence{
Type: tmtypes.ABCIEvidenceTypeDuplicateVote,
Validator: validator,
Height: height,
Time: time,
TotalVotingPower: totalVotingPower,
})
event("beginblock/evidence")
}
}
return abci.RequestBeginBlock{
Header: header,
LastCommitInfo: abci.LastCommitInfo{
Votes: voteInfos,
},
ByzantineValidators: evidence,
}
}
// updateValidators mimicks Tendermint's update logic
// nolint: unparam
func updateValidators(tb testing.TB, r *rand.Rand, params Params, current map[string]mockValidator, updates []abci.ValidatorUpdate, event func(string)) map[string]mockValidator {
for _, update := range updates {
str := fmt.Sprintf("%v", update.PubKey)
switch {
case update.Power == 0:
if _, ok := current[str]; !ok {
tb.Fatalf("tried to delete a nonexistent validator")
}
event("endblock/validatorupdates/kicked")
delete(current, str)
default:
// Does validator already exist?
if mVal, ok := current[str]; ok {
mVal.val = update
event("endblock/validatorupdates/updated")
} else {
// Set this new validator
current[str] = mockValidator{update, GetMemberOfInitialState(r, params.InitialLivenessWeightings)}
event("endblock/validatorupdates/added")
}
}
}
return current
}

View File

@ -0,0 +1,330 @@
package simulation
import (
"encoding/json"
"fmt"
"math/rand"
"os"
"os/signal"
"runtime/debug"
"strings"
"syscall"
"testing"
"time"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// RandSetup performs the random setup the mock module needs.
type RandSetup func(r *rand.Rand, accounts []Account)
// AppStateFn returns the app state json bytes
type AppStateFn func(r *rand.Rand, accs []Account) json.RawMessage
// Simulate tests application by sending random messages.
func Simulate(t *testing.T, app *baseapp.BaseApp,
appStateFn AppStateFn, ops WeightedOperations, setups []RandSetup,
invariants Invariants, numBlocks int, blockSize int, commit bool) error {
time := time.Now().UnixNano()
return SimulateFromSeed(t, app, appStateFn, time, ops,
setups, invariants, numBlocks, blockSize, commit)
}
// initialize the chain for the simulation
func initChain(r *rand.Rand, params Params, accounts []Account,
setups []RandSetup, app *baseapp.BaseApp,
appStateFn AppStateFn) mockValidators {
req := abci.RequestInitChain{
AppStateBytes: appStateFn(r, accounts),
}
res := app.InitChain(req)
validators := newMockValidators(r, res.Validators, params)
for i := 0; i < len(setups); i++ {
setups[i](r, accounts)
}
return validators
}
// 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, app *baseapp.BaseApp,
appStateFn AppStateFn, seed int64, ops WeightedOperations,
setups []RandSetup, invariants Invariants,
numBlocks int, blockSize int, commit bool) (simError error) {
// in case we have to end early, don't os.Exit so that we can run cleanup code.
stopEarly := false
testingMode, t, b := getTestingMode(tb)
fmt.Printf("Starting SimulateFromSeed with randomness "+
"created with seed %d\n", int(seed))
r := rand.New(rand.NewSource(seed))
params := RandomParams(r) // := DefaultParams()
fmt.Printf("Randomized simulation params: %+v\n", params)
timestamp := RandTimestamp(r)
fmt.Printf("Starting the simulation from time %v, unixtime %v\n",
timestamp.UTC().Format(time.UnixDate), timestamp.Unix())
timeDiff := maxTimePerBlock - minTimePerBlock
accs := RandomAccounts(r, params.NumKeys)
eventStats := newEventStats()
// 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 := initChain(r, params, accs, setups, app, appStateFn)
nextValidators := validators
header := abci.Header{
Height: 1,
Time: timestamp,
ProposerAddress: validators.randomProposer(r),
}
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.Printf("\nExiting early due to %s, on block %d, operation %d\n",
receivedSignal, header.Height, opCount)
simError = fmt.Errorf("Exited due to %s", receivedSignal)
stopEarly = true
}()
var pastTimes []time.Time
var pastVoteInfos [][]abci.VoteInfo
request := RandomRequestBeginBlock(r, params,
validators, pastTimes, pastVoteInfos, eventStats.tally, header)
// These are operations which have been queued by previous operations
operationQueue := newOperationQueue()
timeOperationQueue := []FutureOperation{}
var blockLogBuilders []*strings.Builder
if testingMode {
blockLogBuilders = make([]*strings.Builder, numBlocks)
}
displayLogs := logPrinter(testingMode, blockLogBuilders)
blockSimulator := createBlockSimulator(
testingMode, tb, t, params, eventStats.tally, invariants,
ops, operationQueue, timeOperationQueue,
numBlocks, blockSize, displayLogs)
if !testingMode {
b.ResetTimer()
} else {
// Recover logs in case of panic
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic with err\n", r)
stackTrace := string(debug.Stack())
fmt.Println(stackTrace)
displayLogs()
simError = fmt.Errorf(
"Simulation halted due to panic on block %d",
header.Height)
}
}()
}
// TODO split up the contents of this for loop into new functions
for i := 0; i < numBlocks && !stopEarly; i++ {
// Log the header time for future lookup
pastTimes = append(pastTimes, header.Time)
pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes)
// Construct log writer
logWriter := addLogMessage(testingMode, blockLogBuilders, i)
// Run the BeginBlock handler
logWriter("BeginBlock")
app.BeginBlock(request)
if testingMode {
invariants.assertAll(t, app, "BeginBlock", displayLogs)
}
ctx := app.NewContext(false, header)
// Run queued operations. Ignores blocksize if blocksize is too small
logWriter("Queued operations")
numQueuedOpsRan := runQueuedOperations(
operationQueue, int(header.Height),
tb, r, app, ctx, accs, logWriter,
displayLogs, eventStats.tally)
numQueuedTimeOpsRan := runQueuedTimeOperations(
timeOperationQueue, header.Time,
tb, r, app, ctx, accs,
logWriter, displayLogs, eventStats.tally)
if testingMode && onOperation {
invariants.assertAll(t, app, "QueuedOperations", displayLogs)
}
logWriter("Standard operations")
operations := blockSimulator(r, app, ctx, accs, header, logWriter)
opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan
if testingMode {
invariants.assertAll(t, app, "StandardOperations", displayLogs)
}
res := app.EndBlock(abci.RequestEndBlock{})
header.Height++
header.Time = header.Time.Add(
time.Duration(minTimePerBlock) * time.Second)
header.Time = header.Time.Add(
time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second)
header.ProposerAddress = validators.randomProposer(r)
logWriter("EndBlock")
if testingMode {
invariants.assertAll(t, app, "EndBlock", displayLogs)
}
if commit {
app.Commit()
}
if header.ProposerAddress == nil {
fmt.Printf("\nSimulation stopped early as all validators " +
"have been unbonded, there is nobody left propose a block!\n")
stopEarly = true
break
}
// Generate a random RequestBeginBlock with the current validator set
// for the next block
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
nextValidators = updateValidators(tb, r, params,
validators, res.ValidatorUpdates, eventStats.tally)
}
if stopEarly {
eventStats.Print()
return simError
}
fmt.Printf("\nSimulation complete. Final height (blocks): %d, "+
"final time (seconds), : %v, operations ran %d\n",
header.Height, header.Time, opCount)
eventStats.Print()
return nil
}
//______________________________________________________________________________
type blockSimFn func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, header abci.Header, logWriter func(string)) (opCount int)
// Returns a function to simulate blocks. Written like this to avoid constant
// parameters being passed everytime, to minimize memory overhead.
func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, params Params,
event func(string), invariants Invariants, ops WeightedOperations,
operationQueue OperationQueue, timeOperationQueue []FutureOperation,
totalNumBlocks int, avgBlockSize int, displayLogs func()) blockSimFn {
var lastBlocksizeState = 0 // state for [4 * uniform distribution]
var blocksize int
selectOp := ops.getSelectOpFn()
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, header abci.Header, logWriter func(string)) (opCount int) {
fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ",
header.Height, totalNumBlocks, opCount, blocksize)
lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize)
for i := 0; i < blocksize; i++ {
logUpdate, futureOps, err := selectOp(r)(r, app, ctx, accounts, event)
logWriter(logUpdate)
if err != nil {
displayLogs()
tb.Fatalf("error on operation %d within block %d, %v",
header.Height, opCount, err)
}
queueOperations(operationQueue, timeOperationQueue, futureOps)
if testingMode {
if onOperation {
eventStr := fmt.Sprintf("operation: %v", logUpdate)
invariants.assertAll(t, app, eventStr, displayLogs)
}
if opCount%50 == 0 {
fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ",
header.Height, totalNumBlocks, opCount, blocksize)
}
}
opCount++
}
return opCount
}
}
// nolint: errcheck
func runQueuedOperations(queueOps map[int][]Operation,
height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp,
ctx sdk.Context, accounts []Account, logWriter func(string),
displayLogs func(), tallyEvent func(string)) (numOpsRan int) {
queuedOp, ok := queueOps[height]
if !ok {
return 0
}
numOpsRan = len(queuedOp)
for i := 0; i < numOpsRan; i++ {
// 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.
logUpdate, _, err := queuedOp[i](r, app, ctx, accounts, tallyEvent)
logWriter(logUpdate)
if err != nil {
displayLogs()
tb.FailNow()
}
}
delete(queueOps, height)
return numOpsRan
}
func runQueuedTimeOperations(queueOps []FutureOperation,
currentTime time.Time, tb testing.TB, r *rand.Rand,
app *baseapp.BaseApp, ctx sdk.Context, accounts []Account,
logWriter func(string), displayLogs func(), tallyEvent func(string)) (numOpsRan int) {
numOpsRan = 0
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.
logUpdate, _, err := queueOps[0].Op(r, app, ctx, accounts, tallyEvent)
logWriter(logUpdate)
if err != nil {
displayLogs()
tb.FailNow()
}
queueOps = queueOps[1:]
numOpsRan++
}
return numOpsRan
}

View File

@ -5,12 +5,11 @@ import (
"math/rand"
)
// TransitionMatrix is _almost_ a left stochastic matrix.
// It is technically not one due to not normalizing the column values.
// In the future, if we want to find the steady state distribution,
// it will be quite easy to normalize these values to get a stochastic matrix.
// Floats aren't currently used as the default due to non-determinism across
// architectures
// TransitionMatrix is _almost_ a left stochastic matrix. It is technically
// not one due to not normalizing the column values. In the future, if we want
// to find the steady state distribution, it will be quite easy to normalize
// these values to get a stochastic matrix. Floats aren't currently used as
// the default due to non-determinism across architectures
type TransitionMatrix struct {
weights [][]int
// total in each column
@ -24,7 +23,8 @@ func CreateTransitionMatrix(weights [][]int) (TransitionMatrix, error) {
n := len(weights)
for i := 0; i < n; i++ {
if len(weights[i]) != n {
return TransitionMatrix{}, fmt.Errorf("Transition Matrix: Non-square matrix provided, error on row %d", i)
return TransitionMatrix{},
fmt.Errorf("Transition Matrix: Non-square matrix provided, error on row %d", i)
}
}
totals := make([]int, n)
@ -36,8 +36,8 @@ func CreateTransitionMatrix(weights [][]int) (TransitionMatrix, error) {
return TransitionMatrix{weights, totals, n}, nil
}
// NextState returns the next state randomly chosen using r, and the weightings provided
// in the transition matrix.
// NextState returns the next state randomly chosen using r, and the weightings
// provided in the transition matrix.
func (t TransitionMatrix) NextState(r *rand.Rand, i int) int {
randNum := r.Intn(t.totals[i])
for row := 0; row < t.n; row++ {

View File

@ -1,87 +0,0 @@
package simulation
import (
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
type (
// Operation runs a state machine transition,
// and ensures the transition happened as expected.
// The operation could be running and testing a fuzzed transaction,
// or doing the same for a message.
//
// For ease of debugging,
// an operation returns a descriptive message "action",
// which details what this fuzzed state machine transition actually did.
//
// Operations can optionally provide a list of "FutureOperations" to run later
// These will be ran at the beginning of the corresponding block.
Operation func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
accounts []Account, event func(string),
) (action string, futureOperations []FutureOperation, err error)
// RandSetup performs the random setup the mock module needs.
RandSetup func(r *rand.Rand, accounts []Account)
// An Invariant is a function which tests a particular invariant.
// If the invariant has been broken, it should return an error
// containing a descriptive message about what happened.
// The simulator will then halt and print the logs.
Invariant func(app *baseapp.BaseApp) error
// Account contains a privkey, pubkey, address tuple
// eventually more useful data can be placed in here.
// (e.g. number of coins)
Account struct {
PrivKey crypto.PrivKey
PubKey crypto.PubKey
Address sdk.AccAddress
}
mockValidator struct {
val abci.ValidatorUpdate
livenessState int
}
// FutureOperation is an operation which will be ran at the
// beginning of the provided BlockHeight.
// If both a BlockHeight and BlockTime are specified, it will use the BlockHeight.
// In the (likely) event that multiple operations are queued at the same
// block height, they will execute in a FIFO pattern.
FutureOperation struct {
BlockHeight int
BlockTime time.Time
Op Operation
}
// WeightedOperation is an operation with associated weight.
// This is used to bias the selection operation within the simulator.
WeightedOperation struct {
Weight int
Op Operation
}
)
// TODO remove? not being called anywhere
// PeriodicInvariant returns an Invariant function closure that asserts
// a given invariant if the mock application's last block modulo the given
// period is congruent to the given offset.
func PeriodicInvariant(invariant Invariant, period int, offset int) Invariant {
return func(app *baseapp.BaseApp) error {
if int(app.LastBlockHeight())%period == offset {
return invariant(app)
}
return nil
}
}
// nolint
func (acc Account) Equals(acc2 Account) bool {
return acc.Address.Equals(acc2.Address)
}

View File

@ -2,170 +2,121 @@ package simulation
import (
"fmt"
"math/big"
"math/rand"
"os"
"sort"
"strings"
"testing"
"time"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock"
)
// shamelessly copied from https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang#31832326
// TODO we should probably move this to tendermint/libs/common/random.go
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
// Generate a random string of a particular length
func RandStringOfLength(r *rand.Rand, n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, r.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = r.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B) {
testingMode = false
if _t, ok := tb.(*testing.T); ok {
t = _t
testingMode = true
} else {
b = tb.(*testing.B)
}
return string(b)
}
// Pretty-print events as a table
func DisplayEvents(events map[string]uint) {
var keys []string
for key := range events {
keys = append(keys, key)
}
sort.Strings(keys)
fmt.Printf("Event statistics: \n")
for _, key := range keys {
fmt.Printf(" % 60s => %d\n", key, events[key])
}
}
// RandomAcc pick a random account from an array
func RandomAcc(r *rand.Rand, accs []Account) Account {
return accs[r.Intn(
len(accs),
)]
}
// Generate a random amount
func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {
return sdk.NewInt(int64(r.Intn(int(max.Int64()))))
}
// RandomDecAmount generates a random decimal amount
func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec {
randInt := big.NewInt(0).Rand(r, max.Int)
return sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision)
}
// RandomAccounts generates n random accounts
func RandomAccounts(r *rand.Rand, n int) []Account {
accs := make([]Account, n)
for i := 0; i < n; i++ {
// don't need that much entropy for simulation
privkeySeed := make([]byte, 15)
r.Read(privkeySeed)
useSecp := r.Int63()%2 == 0
if useSecp {
accs[i].PrivKey = secp256k1.GenPrivKeySecp256k1(privkeySeed)
} else {
accs[i].PrivKey = ed25519.GenPrivKeyFromSecret(privkeySeed)
}
accs[i].PubKey = accs[i].PrivKey.PubKey()
accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address())
}
return accs
return
}
// Builds a function to add logs for this particular block
func addLogMessage(testingmode bool, blockLogBuilders []*strings.Builder, height int) func(string) {
if testingmode {
blockLogBuilders[height] = &strings.Builder{}
return func(x string) {
(*blockLogBuilders[height]).WriteString(x)
(*blockLogBuilders[height]).WriteString("\n")
}
}
return func(x string) {}
}
func addLogMessage(testingmode bool,
blockLogBuilders []*strings.Builder, height int) func(string) {
// assertAllInvariants asserts a list of provided invariants against application state
func assertAllInvariants(t *testing.T, app *baseapp.BaseApp,
invariants []Invariant, where string, displayLogs func()) {
for i := 0; i < len(invariants); i++ {
err := invariants[i](app)
if err != nil {
fmt.Printf("Invariants broken after %s\n", where)
fmt.Println(err.Error())
displayLogs()
t.Fatal()
}
if !testingmode {
return func(_ string) {}
}
}
// RandomSetGenesis wraps mock.RandomSetGenesis, but using simulation accounts
func RandomSetGenesis(r *rand.Rand, app *mock.App, accs []Account, denoms []string) {
addrs := make([]sdk.AccAddress, len(accs))
for i := 0; i < len(accs); i++ {
addrs[i] = accs[i].Address
blockLogBuilders[height] = &strings.Builder{}
return func(x string) {
(*blockLogBuilders[height]).WriteString(x)
(*blockLogBuilders[height]).WriteString("\n")
}
mock.RandomSetGenesis(r, app, addrs, denoms)
}
// Creates a function to print out the logs
func logPrinter(testingmode bool, logs []*strings.Builder) func() {
if testingmode {
return func() {
numLoggers := 0
for i := 0; i < len(logs); i++ {
// We're passed the last created block
if logs[i] == nil {
numLoggers = i
break
}
if !testingmode {
return func() {}
}
return func() {
numLoggers := 0
for i := 0; i < len(logs); i++ {
// We're passed the last created block
if logs[i] == nil {
numLoggers = i
break
}
var f *os.File
if numLoggers > 10 {
fileName := fmt.Sprintf("simulation_log_%s.txt", time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("Too many logs to display, instead writing to %s\n", fileName)
f, _ = os.Create(fileName)
}
var f *os.File
if numLoggers > 10 {
fileName := fmt.Sprintf("simulation_log_%s.txt",
time.Now().Format("2006-01-02 15:04:05"))
fmt.Printf("Too many logs to display, instead writing to %s\n",
fileName)
f, _ = os.Create(fileName)
}
for i := 0; i < numLoggers; i++ {
if f == nil {
fmt.Printf("Begin block %d\n", i+1)
fmt.Println((*logs[i]).String())
continue
}
for i := 0; i < numLoggers; i++ {
if f != nil {
_, err := f.WriteString(fmt.Sprintf("Begin block %d\n", i+1))
if err != nil {
panic("Failed to write logs to file")
}
_, err = f.WriteString((*logs[i]).String())
if err != nil {
panic("Failed to write logs to file")
}
} else {
fmt.Printf("Begin block %d\n", i+1)
fmt.Println((*logs[i]).String())
}
_, err := f.WriteString(fmt.Sprintf("Begin block %d\n", i+1))
if err != nil {
panic("Failed to write logs to file")
}
_, err = f.WriteString((*logs[i]).String())
if err != nil {
panic("Failed to write logs to file")
}
}
}
return func() {}
}
// getBlockSize returns a block size as determined from the transition matrix.
// It targets making average block size the provided parameter. The three
// states it moves between are:
// - "over stuffed" blocks with average size of 2 * avgblocksize,
// - normal sized blocks, hitting avgBlocksize on average,
// - and empty blocks, with no txs / only txs scheduled from the past.
func getBlockSize(r *rand.Rand, params Params,
lastBlockSizeState, avgBlockSize int) (state, blocksize int) {
// TODO: Make default blocksize transition matrix actually make the average
// blocksize equal to avgBlockSize.
state = params.BlockSizeTransitionMatrix.NextState(r, lastBlockSizeState)
switch state {
case 0:
blocksize = r.Intn(avgBlockSize * 4)
case 1:
blocksize = r.Intn(avgBlockSize * 2)
default:
blocksize = 0
}
return state, blocksize
}
// PeriodicInvariant returns an Invariant function closure that asserts a given
// invariant if the mock application's last block modulo the given period is
// congruent to the given offset.
//
// NOTE this function is intended to be used manually used while running
// computationally heavy simulations.
// TODO reference this function in the codebase probably through use of a switch
func PeriodicInvariant(invariant Invariant, period int, offset int) Invariant {
return func(app *baseapp.BaseApp) error {
if int(app.LastBlockHeight())%period == offset {
return invariant(app)
}
return nil
}
}