Merge PR #2720: x/mock/simulation cleanup and re-org
This commit is contained in:
commit
1049a2647f
|
@ -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
|
||||
|
|
|
@ -36,6 +36,7 @@ IMPROVEMENTS
|
|||
* Gaia
|
||||
|
||||
* SDK
|
||||
- [x/mock/simulation] [\#2720] major cleanup, introduction of helper objects, reorganization
|
||||
|
||||
* Tendermint
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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},
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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++ {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue