2018-07-16 18:25:15 -07:00
package simulation
2018-07-06 13:19:11 -07:00
import (
2018-07-17 11:33:53 -07:00
"encoding/json"
2018-07-06 13:19:11 -07:00
"fmt"
"math/rand"
"testing"
"time"
2018-08-16 08:36:15 -07:00
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
tmtypes "github.com/tendermint/tendermint/types"
2018-07-16 18:15:50 -07:00
"github.com/cosmos/cosmos-sdk/baseapp"
2018-07-17 15:04:10 -07:00
sdk "github.com/cosmos/cosmos-sdk/types"
2018-07-16 18:25:15 -07:00
"github.com/cosmos/cosmos-sdk/x/mock"
2018-07-06 13:19:11 -07:00
"github.com/stretchr/testify/require"
)
2018-07-17 11:33:53 -07:00
// Simulate tests application by sending random messages.
func Simulate (
2018-08-16 08:36:15 -07:00
t * testing . T , app * baseapp . BaseApp , appStateFn func ( r * rand . Rand , keys [ ] crypto . PrivKey , accs [ ] sdk . AccAddress ) json . RawMessage , ops [ ] TestAndRunTx , setups [ ] RandSetup ,
invariants [ ] Invariant , numBlocks int , blockSize int ,
2018-07-06 13:19:11 -07:00
) {
time := time . Now ( ) . UnixNano ( )
2018-08-16 08:36:15 -07:00
SimulateFromSeed ( t , app , appStateFn , time , ops , setups , invariants , numBlocks , blockSize )
2018-07-06 13:19:11 -07:00
}
2018-07-17 11:33:53 -07:00
// SimulateFromSeed tests an application by running the provided
2018-07-06 13:19:11 -07:00
// operations, testing the provided invariants, but using the provided seed.
2018-07-17 11:33:53 -07:00
func SimulateFromSeed (
2018-08-16 08:36:15 -07:00
t * testing . T , app * baseapp . BaseApp , appStateFn func ( r * rand . Rand , keys [ ] crypto . PrivKey , accs [ ] sdk . AccAddress ) json . RawMessage , seed int64 , ops [ ] TestAndRunTx , setups [ ] RandSetup ,
invariants [ ] Invariant , numBlocks int , blockSize int ,
2018-07-06 13:19:11 -07:00
) {
2018-07-17 15:04:10 -07:00
log := fmt . Sprintf ( "Starting SimulateFromSeed with randomness created with seed %d" , int ( seed ) )
2018-08-16 08:36:15 -07:00
fmt . Printf ( "%s\n" , log )
2018-07-06 13:19:11 -07:00
r := rand . New ( rand . NewSource ( seed ) )
2018-08-16 08:36:15 -07:00
keys , accs := mock . GeneratePrivKeyAddressPairsFromRand ( r , numKeys )
2018-07-06 13:19:11 -07:00
2018-07-17 16:27:51 -07:00
// Setup event stats
events := make ( map [ string ] uint )
event := func ( what string ) {
2018-08-16 08:36:15 -07:00
log += "\nevent - " + what
2018-07-17 22:50:04 -07:00
events [ what ] ++
2018-07-17 16:27:51 -07:00
}
2018-08-16 08:36:15 -07:00
timestamp := time . Unix ( 0 , 0 )
timeDiff := maxTimePerBlock - minTimePerBlock
res := app . InitChain ( abci . RequestInitChain { AppStateBytes : appStateFn ( r , keys , accs ) } )
validators := make ( map [ string ] mockValidator )
for _ , validator := range res . Validators {
validators [ string ( validator . Address ) ] = mockValidator { validator , GetMemberOfInitialState ( r , initialLivenessWeightings ) }
}
2018-07-06 13:19:11 -07:00
for i := 0 ; i < len ( setups ) ; i ++ {
setups [ i ] ( r , keys )
}
2018-08-16 08:36:15 -07:00
header := abci . Header { Height : 0 , Time : timestamp }
opCount := 0
request := abci . RequestBeginBlock { Header : header }
var pastTimes [ ] time . Time
2018-07-06 13:19:11 -07:00
for i := 0 ; i < numBlocks ; i ++ {
2018-08-16 08:36:15 -07:00
// Log the header time for future lookup
pastTimes = append ( pastTimes , header . Time )
// Run the BeginBlock handler
app . BeginBlock ( request )
log += "\nBeginBlock"
// Make sure invariants hold at beginning of block
2018-07-16 18:15:50 -07:00
AssertAllInvariants ( t , app , invariants , log )
2018-07-06 13:19:11 -07:00
ctx := app . NewContext ( false , header )
2018-08-16 08:36:15 -07:00
var thisBlockSize int
load := r . Float64 ( )
switch {
case load < 0.33 :
thisBlockSize = 0
case load < 0.66 :
thisBlockSize = r . Intn ( blockSize * 2 )
default :
thisBlockSize = r . Intn ( blockSize * 4 )
}
for j := 0 ; j < thisBlockSize ; j ++ {
2018-07-17 16:27:51 -07:00
logUpdate , err := ops [ r . Intn ( len ( ops ) ) ] ( t , r , app , ctx , keys , log , event )
2018-07-06 13:19:11 -07:00
log += "\n" + logUpdate
require . Nil ( t , err , log )
2018-08-16 08:36:15 -07:00
if onOperation {
AssertAllInvariants ( t , app , invariants , log )
}
if opCount % 200 == 0 {
fmt . Printf ( "\rSimulating... block %d/%d, operation %d." , header . Height , numBlocks , opCount )
}
opCount ++
2018-07-06 13:19:11 -07:00
}
2018-08-16 08:36:15 -07:00
res := app . EndBlock ( abci . RequestEndBlock { } )
2018-07-06 13:19:11 -07:00
header . Height ++
2018-08-16 08:36:15 -07:00
header . Time = header . Time . Add ( time . Duration ( minTimePerBlock ) * time . Second ) . Add ( time . Duration ( int64 ( r . Intn ( int ( timeDiff ) ) ) ) * time . Second )
log += "\nEndBlock"
// Make sure invariants hold at end of block
AssertAllInvariants ( t , app , invariants , log )
// Generate a random RequestBeginBlock with the current validator set for the next block
request = RandomRequestBeginBlock ( t , r , validators , livenessTransitionMatrix , evidenceFraction , pastTimes , event , header , log )
// Update the validator set
validators = updateValidators ( t , r , validators , res . ValidatorUpdates , event )
2018-07-06 13:19:11 -07:00
}
2018-07-17 16:27:51 -07:00
2018-08-16 08:36:15 -07:00
fmt . Printf ( "\nSimulation complete. Final height (blocks): %d, final time (seconds): %v\n" , header . Height , header . Time )
2018-07-17 16:27:51 -07:00
DisplayEvents ( events )
2018-07-06 13:19:11 -07:00
}
2018-08-16 08:36:15 -07:00
// RandomRequestBeginBlock generates a list of signing validators according to the provided list of validators, signing fraction, and evidence fraction
func RandomRequestBeginBlock ( t * testing . T , r * rand . Rand , validators map [ string ] mockValidator , livenessTransitions TransitionMatrix , evidenceFraction float64 ,
pastTimes [ ] time . Time , event func ( string ) , header abci . Header , log string ) abci . RequestBeginBlock {
if len ( validators ) == 0 {
return abci . RequestBeginBlock { Header : header }
}
signingValidators := make ( [ ] abci . SigningValidator , len ( validators ) )
i := 0
for _ , mVal := range validators {
mVal . livenessState = livenessTransitions . 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" )
}
signingValidators [ i ] = abci . SigningValidator {
Validator : mVal . val ,
SignedLastBlock : signed ,
}
i ++
}
evidence := make ( [ ] abci . Evidence , 0 )
for r . Float64 ( ) < evidenceFraction {
height := header . Height
time := header . Time
if r . Float64 ( ) < pastEvidenceFraction {
height = int64 ( r . Intn ( int ( header . Height ) ) )
time = pastTimes [ height ]
}
validator := signingValidators [ r . Intn ( len ( signingValidators ) ) ] . Validator
var currentTotalVotingPower int64
for _ , mVal := range validators {
currentTotalVotingPower += mVal . val . Power
}
evidence = append ( evidence , abci . Evidence {
Type : tmtypes . ABCIEvidenceTypeDuplicateVote ,
Validator : validator ,
Height : height ,
Time : time ,
TotalVotingPower : currentTotalVotingPower ,
} )
event ( "beginblock/evidence" )
}
return abci . RequestBeginBlock {
Header : header ,
LastCommitInfo : abci . LastCommitInfo {
Validators : signingValidators ,
} ,
ByzantineValidators : evidence ,
}
}
2018-07-17 15:04:10 -07:00
// AssertAllInvariants asserts a list of provided invariants against application state
2018-07-16 18:15:50 -07:00
func AssertAllInvariants ( t * testing . T , app * baseapp . BaseApp , tests [ ] Invariant , log string ) {
2018-07-06 13:19:11 -07:00
for i := 0 ; i < len ( tests ) ; i ++ {
tests [ i ] ( t , app , log )
}
}
2018-08-16 08:36:15 -07:00
// updateValidators mimicks Tendermint's update logic
func updateValidators ( t * testing . T , r * rand . Rand , current map [ string ] mockValidator , updates [ ] abci . Validator , event func ( string ) ) map [ string ] mockValidator {
for _ , update := range updates {
switch {
case update . Power == 0 :
require . NotNil ( t , current [ string ( update . PubKey . Data ) ] , "tried to delete a nonexistent validator" )
event ( "endblock/validatorupdates/kicked" )
delete ( current , string ( update . PubKey . Data ) )
default :
// Does validator already exist?
if mVal , ok := current [ string ( update . PubKey . Data ) ] ; ok {
mVal . val = update
event ( "endblock/validatorupdates/updated" )
} else {
// Set this new validator
current [ string ( update . PubKey . Data ) ] = mockValidator { update , GetMemberOfInitialState ( r , initialLivenessWeightings ) }
event ( "endblock/validatorupdates/added" )
}
}
}
return current
}