Merge remote-tracking branch 'origin/develop' into sunny/gov-cli-proposal-id

This commit is contained in:
rigelrozanski 2018-07-20 11:47:37 -04:00
commit 15138b80c8
33 changed files with 1109 additions and 521 deletions

View File

@ -85,6 +85,23 @@ jobs:
export PATH="$GOBIN:$PATH"
make test_cli
test_sim:
<<: *defaults
parallelism: 1
steps:
- attach_workspace:
at: /tmp/workspace
- restore_cache:
key: v1-pkg-cache
- restore_cache:
key: v1-tree-{{ .Environment.CIRCLE_SHA1 }}
- run:
name: Test simulation
command: |
export PATH="$GOBIN:$PATH"
export GAIA_SIMULATION_SEED=1531897442166404087
make test_sim
test_cover:
<<: *defaults
parallelism: 4
@ -144,6 +161,9 @@ workflows:
- test_cli:
requires:
- setup_dependencies
- test_sim:
requires:
- setup_dependencies
- test_cover:
requires:
- setup_dependencies

View File

@ -1,16 +1,5 @@
# Changelog
## PENDING
BREAKING CHANGES
* [x/gov] Increase VotingPeriod, DepositPeriod, and MinDeposit
IMPROVEMENTS
BUG FIXES
## 0.21.1
## 0.22.0
*July 16th, 2018*

View File

@ -1,5 +1,6 @@
PACKAGES=$(shell go list ./... | grep -v '/vendor/')
PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test)
PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v '/simulation' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test)
PACKAGES_SIMTEST=$(shell go list ./... | grep -v '/vendor/' | grep '/simulation')
COMMIT_HASH := $(shell git rev-parse --short HEAD)
BUILD_TAGS = netgo ledger
BUILD_FLAGS = -tags "${BUILD_TAGS}" -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=${COMMIT_HASH}"
@ -127,6 +128,16 @@ test_unit:
test_race:
@go test -race $(PACKAGES_NOCLITEST)
test_sim:
@echo "Running individual module simulations."
@go test $(PACKAGES_SIMTEST) -v
@echo "Running full Gaia simulation. This may take several minutes."
@echo "Pass the flag 'SimulationSeed' to run with a constant seed."
@echo "Pass the flag 'SimulationNumKeys' to run with the specified number of keys."
@echo "Pass the flag 'SimulationNumBlocks' to run with the specified number of blocks."
@echo "Pass the flag 'SimulationBlockSize' to run with the specified block size (operations per block)."
@go test ./cmd/gaia/app -run TestFullGaiaSimulation -SimulationEnabled=true -SimulationBlockSize=200 -v
test_cover:
@bash tests/test_cover.sh
@ -212,4 +223,4 @@ remotenet-status:
check_tools get_tools get_vendor_deps draw_deps test test_cli test_unit \
test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update \
build-linux build-docker-gaiadnode localnet-start localnet-stop remotenet-start \
remotenet-stop remotenet-status format check-ledger
remotenet-stop remotenet-status format check-ledger test_sim

View File

@ -18,6 +18,10 @@ BREAKING CHANGES
FEATURES
* [lcd] Can now query governance proposals by ProposalStatus
* [x/mock/simulation] Randomized simulation framework
* Modules specify invariants and operations, preferably in an x/[module]/simulation package
* Modules can test random combinations of their own operations
* Applications can integrate operations and invariants from modules together for an integrated simulation
* [baseapp] Initialize validator set on ResponseInitChain
* Added support for cosmos-sdk-cli tool under cosmos-sdk/cmd
* This allows SDK users to init a new project repository with a single command.
@ -27,6 +31,8 @@ IMPROVEMENTS
* [cli] Improve error messages for all txs when the account doesn't exist
* [tools] Remove `rm -rf vendor/` from `make get_vendor_deps`
* [x/stake] Add revoked to human-readable validator
* [x/gov] Votes on a proposal can now be queried
* [x/gov] gov cli proposalID to proposal-id
BUG FIXES
* \#1666 Add intra-tx counter to the genesis validators

View File

@ -611,6 +611,17 @@ func TestProposalsQuery(t *testing.T) {
// Test query voted and deposited by addr1
proposals = getProposalsFilterVoterDepositer(t, port, addr, addr)
require.Equal(t, proposalID2, (proposals[0]).GetProposalID())
// Test query votes on Proposal 2
votes := getVotes(t, port, proposalID2)
require.Len(t, votes, 1)
require.Equal(t, addr, votes[0].Voter)
// Test query votes on Proposal 3
votes = getVotes(t, port, proposalID3)
require.Len(t, votes, 2)
require.True(t, addr.String() == votes[0].Voter.String() || addr.String() == votes[1].Voter.String())
require.True(t, addr2.String() == votes[0].Voter.String() || addr2.String() == votes[1].Voter.String())
}
//_____________________________________________________________________________
@ -875,6 +886,15 @@ func getVote(t *testing.T, port string, proposalID int64, voterAddr sdk.AccAddre
return vote
}
func getVotes(t *testing.T, port string, proposalID int64) []gov.Vote {
res, body := Request(t, port, "GET", fmt.Sprintf("/gov/proposals/%d/votes", proposalID), nil)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var votes []gov.Vote
err := cdc.UnmarshalJSON([]byte(body), &votes)
require.Nil(t, err)
return votes
}
func getProposalsAll(t *testing.T, port string) []gov.Proposal {
res, body := Request(t, port, "GET", "/gov/proposals", nil)
require.Equal(t, http.StatusOK, res.StatusCode, body)

100
cmd/gaia/app/sim_test.go Normal file
View File

@ -0,0 +1,100 @@
package app
import (
"encoding/json"
"flag"
"math/rand"
"testing"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tendermint/libs/db"
"github.com/tendermint/tendermint/libs/log"
sdk "github.com/cosmos/cosmos-sdk/types"
banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
stake "github.com/cosmos/cosmos-sdk/x/stake"
stakesim "github.com/cosmos/cosmos-sdk/x/stake/simulation"
)
var (
seed int64
numKeys int
numBlocks int
blockSize int
enabled bool
)
func init() {
flag.Int64Var(&seed, "SimulationSeed", 42, "Simulation random seed")
flag.IntVar(&numKeys, "SimulationNumKeys", 10, "Number of keys (accounts)")
flag.IntVar(&numBlocks, "SimulationNumBlocks", 100, "Number of blocks")
flag.IntVar(&blockSize, "SimulationBlockSize", 100, "Operations per block")
flag.BoolVar(&enabled, "SimulationEnabled", false, "Enable the simulation")
}
func appStateFn(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage {
var genesisAccounts []GenesisAccount
// Randomly generate some genesis accounts
for _, addr := range accs {
coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(100)}}
genesisAccounts = append(genesisAccounts, GenesisAccount{
Address: addr,
Coins: coins,
})
}
// Default genesis state
stakeGenesis := stake.DefaultGenesisState()
stakeGenesis.Pool.LooseTokens = sdk.NewRat(1000)
genesis := GenesisState{
Accounts: genesisAccounts,
StakeData: stakeGenesis,
}
// Marshal genesis
appState, err := MakeCodec().MarshalJSON(genesis)
if err != nil {
panic(err)
}
return appState
}
func TestFullGaiaSimulation(t *testing.T) {
if !enabled {
t.Skip("Skipping Gaia simulation")
}
// Setup Gaia application
logger := log.NewNopLogger()
db := dbm.NewMemDB()
app := NewGaiaApp(logger, db, nil)
require.Equal(t, "GaiaApp", app.Name())
// Run randomized simulation
simulation.SimulateFromSeed(
t, app.BaseApp, appStateFn, seed,
[]simulation.TestAndRunTx{
banksim.TestAndRunSingleInputMsgSend(app.accountMapper),
stakesim.SimulateMsgCreateValidator(app.accountMapper, app.stakeKeeper),
stakesim.SimulateMsgEditValidator(app.stakeKeeper),
stakesim.SimulateMsgDelegate(app.accountMapper, app.stakeKeeper),
stakesim.SimulateMsgBeginUnbonding(app.accountMapper, app.stakeKeeper),
stakesim.SimulateMsgCompleteUnbonding(app.stakeKeeper),
stakesim.SimulateMsgBeginRedelegate(app.accountMapper, app.stakeKeeper),
stakesim.SimulateMsgCompleteRedelegate(app.stakeKeeper),
},
[]simulation.RandSetup{},
[]simulation.Invariant{
banksim.NonnegativeBalanceInvariant(app.accountMapper),
stakesim.AllInvariants(app.coinKeeper, app.stakeKeeper, app.accountMapper),
},
numKeys,
numBlocks,
blockSize,
)
}

View File

@ -217,6 +217,11 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
vote := executeGetVote(t, fmt.Sprintf("gaiacli gov query-vote --proposal-id=1 --voter=%s --output=json %v", fooAddr, flags))
require.Equal(t, int64(1), vote.ProposalID)
require.Equal(t, gov.OptionYes, vote.Option)
votes := executeGetVotes(t, fmt.Sprintf("gaiacli gov query-votes --proposalID=1 --output=json %v", flags))
require.Len(t, votes, 1)
require.Equal(t, int64(1), votes[0].ProposalID)
require.Equal(t, gov.OptionYes, votes[0].Option)
}
//___________________________________________________________________________________
@ -321,3 +326,12 @@ func executeGetVote(t *testing.T, cmdStr string) gov.Vote {
require.NoError(t, err, "out %v\n, err %v", out, err)
return vote
}
func executeGetVotes(t *testing.T, cmdStr string) []gov.Vote {
out := tests.ExecuteT(t, cmdStr)
var votes []gov.Vote
cdc := app.MakeCodec()
err := cdc.UnmarshalJSON([]byte(out), &votes)
require.NoError(t, err, "out %v\n, err %v", out, err)
return votes
}

View File

@ -112,6 +112,7 @@ func main() {
client.GetCommands(
govcmd.GetCmdQueryProposal("gov", cdc),
govcmd.GetCmdQueryVote("gov", cdc),
govcmd.GetCmdQueryVotes("gov", cdc),
)...)
govCmd.AddCommand(
client.PostCommands(

View File

@ -28,6 +28,11 @@ func (v Validator) GetPubKey() crypto.PubKey {
return nil
}
// Implements sdk.Validator
func (v Validator) GetTokens() sdk.Rat {
return sdk.ZeroRat()
}
// Implements sdk.Validator
func (v Validator) GetPower() sdk.Rat {
return v.Power

View File

@ -126,10 +126,10 @@ else
@echo "Installing unparam"
go get -v $(UNPARAM)
endif
ifdef GOYCLO_CHECK
@echo "goyclo is already installed. Run 'make update_tools' to update."
ifdef GOCYCLO_CHECK
@echo "gocyclo is already installed. Run 'make update_tools' to update."
else
@echo "Installing goyclo"
@echo "Installing gocyclo"
go get -v $(GOCYCLO)
endif

View File

@ -15,9 +15,13 @@ type Coin struct {
}
func NewCoin(denom string, amount int64) Coin {
return NewIntCoin(denom, NewInt(amount))
}
func NewIntCoin(denom string, amount Int) Coin {
return Coin{
Denom: denom,
Amount: NewInt(amount),
Amount: amount,
}
}

View File

@ -43,6 +43,7 @@ type Validator interface {
GetOwner() AccAddress // owner AccAddress to receive/return validators coins
GetPubKey() crypto.PubKey // validation pubkey
GetPower() Rat // validation power
GetTokens() Rat // validation tokens
GetDelegatorShares() Rat // Total out standing delegator shares
GetBondHeight() int64 // height in which the validator became active
}

View File

@ -5,8 +5,6 @@ import (
"github.com/stretchr/testify/require"
"math/rand"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/mock"
@ -83,21 +81,6 @@ func getMockApp(t *testing.T) *mock.App {
return mapp
}
func TestBankWithRandomMessages(t *testing.T) {
mapp := getMockApp(t)
setup := func(r *rand.Rand, keys []crypto.PrivKey) {
return
}
mapp.RandomizedTesting(
t,
[]mock.TestAndRunTx{TestAndRunSingleInputMsgSend},
[]mock.RandSetup{setup},
[]mock.Invariant{ModuleInvariants},
100, 30, 30,
)
}
func TestMsgSendWithAccounts(t *testing.T) {
mapp := getMockApp(t)

View File

@ -0,0 +1,50 @@
package simulation
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
abci "github.com/tendermint/tendermint/abci/types"
)
// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances
func NonnegativeBalanceInvariant(mapper auth.AccountMapper) simulation.Invariant {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
ctx := app.NewContext(false, abci.Header{})
accts := mock.GetAllAccounts(mapper, ctx)
for _, acc := range accts {
coins := acc.GetCoins()
require.True(t, coins.IsNotNegative(),
fmt.Sprintf("%s has a negative denomination of %s\n%s",
acc.GetAddress().String(),
coins.String(),
log),
)
}
}
}
// TotalCoinsInvariant checks that the sum of the coins across all accounts
// is what is expected
func TotalCoinsInvariant(mapper auth.AccountMapper, totalSupplyFn func() sdk.Coins) simulation.Invariant {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
ctx := app.NewContext(false, abci.Header{})
totalCoins := sdk.Coins{}
chkAccount := func(acc auth.Account) bool {
coins := acc.GetCoins()
totalCoins = totalCoins.Plus(coins)
return false
}
mapper.IterateAccounts(ctx, chkAccount)
require.Equal(t, totalSupplyFn(), totalCoins, log)
}
}

117
x/bank/simulation/msgs.go Normal file
View File

@ -0,0 +1,117 @@
package simulation
import (
"errors"
"fmt"
"math/big"
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
"github.com/tendermint/tendermint/crypto"
)
// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both
// accounts already exist.
func TestAndRunSingleInputMsgSend(mapper auth.AccountMapper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
fromKey := simulation.RandomKey(r, keys)
fromAddr := sdk.AccAddress(fromKey.PubKey().Address())
toKey := simulation.RandomKey(r, keys)
// Disallow sending money to yourself
for {
if !fromKey.Equals(toKey) {
break
}
toKey = simulation.RandomKey(r, keys)
}
toAddr := sdk.AccAddress(toKey.PubKey().Address())
initFromCoins := mapper.GetAccount(ctx, fromAddr).GetCoins()
denomIndex := r.Intn(len(initFromCoins))
amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount)
if goErr != nil {
return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil
}
action = fmt.Sprintf("%s is sending %s %s to %s",
fromAddr.String(),
amt.String(),
initFromCoins[denomIndex].Denom,
toAddr.String(),
)
log = fmt.Sprintf("%s\n%s", log, action)
coins := sdk.Coins{{initFromCoins[denomIndex].Denom, amt}}
var msg = bank.MsgSend{
Inputs: []bank.Input{bank.NewInput(fromAddr, coins)},
Outputs: []bank.Output{bank.NewOutput(toAddr, coins)},
}
sendAndVerifyMsgSend(t, app, mapper, msg, ctx, log, []crypto.PrivKey{fromKey})
event("bank/sendAndVerifyMsgSend/ok")
return action, nil
}
}
// Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs
func sendAndVerifyMsgSend(t *testing.T, app *baseapp.BaseApp, mapper auth.AccountMapper, msg bank.MsgSend, ctx sdk.Context, log string, privkeys []crypto.PrivKey) {
initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs))
initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs))
AccountNumbers := make([]int64, len(msg.Inputs))
SequenceNumbers := make([]int64, len(msg.Inputs))
for i := 0; i < len(msg.Inputs); i++ {
acc := mapper.GetAccount(ctx, msg.Inputs[i].Address)
AccountNumbers[i] = acc.GetAccountNumber()
SequenceNumbers[i] = acc.GetSequence()
initialInputAddrCoins[i] = acc.GetCoins()
}
for i := 0; i < len(msg.Outputs); i++ {
acc := mapper.GetAccount(ctx, msg.Outputs[i].Address)
initialOutputAddrCoins[i] = acc.GetCoins()
}
tx := mock.GenTx([]sdk.Msg{msg},
AccountNumbers,
SequenceNumbers,
privkeys...)
res := app.Deliver(tx)
if !res.IsOK() {
// TODO: Do this in a more 'canonical' way
fmt.Println(res)
fmt.Println(log)
t.FailNow()
}
for i := 0; i < len(msg.Inputs); i++ {
terminalInputCoins := mapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins()
require.Equal(t,
initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins),
terminalInputCoins,
fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log),
)
}
for i := 0; i < len(msg.Outputs); i++ {
terminalOutputCoins := mapper.GetAccount(ctx, msg.Outputs[i].Address).GetCoins()
require.Equal(t,
initialOutputAddrCoins[i].Plus(msg.Outputs[i].Coins),
terminalOutputCoins,
fmt.Sprintf("Output #%d had an incorrect amount of coins\n%s", i, log),
)
}
}
func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) {
if !max.GT(sdk.OneInt()) {
return sdk.Int{}, errors.New("max too small")
}
max = max.Sub(sdk.OneInt())
return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil
}

View File

@ -0,0 +1,44 @@
package simulation
import (
"encoding/json"
"math/rand"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
)
func TestBankWithRandomMessages(t *testing.T) {
mapp := mock.NewApp()
bank.RegisterWire(mapp.Cdc)
mapper := mapp.AccountMapper
coinKeeper := bank.NewKeeper(mapper)
mapp.Router().AddRoute("bank", bank.NewHandler(coinKeeper))
err := mapp.CompleteSetup([]*sdk.KVStoreKey{})
if err != nil {
panic(err)
}
appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage {
mock.RandomSetGenesis(r, mapp, accs, []string{"stake"})
return json.RawMessage("{}")
}
simulation.Simulate(
t, mapp.BaseApp, appStateFn,
[]simulation.TestAndRunTx{
TestAndRunSingleInputMsgSend(mapper),
},
[]simulation.RandSetup{},
[]simulation.Invariant{
NonnegativeBalanceInvariant(mapper),
TotalCoinsInvariant(mapper, func() sdk.Coins { return mapp.TotalCoinsSupply }),
},
100, 30, 30,
)
}

View File

@ -1,150 +0,0 @@
package bank
import (
"errors"
"fmt"
"math/big"
"math/rand"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
// ModuleInvariants runs all invariants of the bank module.
// Currently runs non-negative balance invariant and TotalCoinsInvariant
func ModuleInvariants(t *testing.T, app *mock.App, log string) {
NonnegativeBalanceInvariant(t, app, log)
TotalCoinsInvariant(t, app, log)
}
// NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances
func NonnegativeBalanceInvariant(t *testing.T, app *mock.App, log string) {
ctx := app.NewContext(false, abci.Header{})
accts := mock.GetAllAccounts(app.AccountMapper, ctx)
for _, acc := range accts {
coins := acc.GetCoins()
assert.True(t, coins.IsNotNegative(),
fmt.Sprintf("%s has a negative denomination of %s\n%s",
acc.GetAddress().String(),
coins.String(),
log),
)
}
}
// TotalCoinsInvariant checks that the sum of the coins across all accounts
// is what is expected
func TotalCoinsInvariant(t *testing.T, app *mock.App, log string) {
ctx := app.BaseApp.NewContext(false, abci.Header{})
totalCoins := sdk.Coins{}
chkAccount := func(acc auth.Account) bool {
coins := acc.GetCoins()
totalCoins = totalCoins.Plus(coins)
return false
}
app.AccountMapper.IterateAccounts(ctx, chkAccount)
require.Equal(t, app.TotalCoinsSupply, totalCoins, log)
}
// TestAndRunSingleInputMsgSend tests and runs a single msg send, with one input and one output, where both
// accounts already exist.
func TestAndRunSingleInputMsgSend(t *testing.T, r *rand.Rand, app *mock.App, ctx sdk.Context, keys []crypto.PrivKey, log string) (action string, err sdk.Error) {
fromKey := keys[r.Intn(len(keys))]
fromAddr := sdk.AccAddress(fromKey.PubKey().Address())
toKey := keys[r.Intn(len(keys))]
// Disallow sending money to yourself
for {
if !fromKey.Equals(toKey) {
break
}
toKey = keys[r.Intn(len(keys))]
}
toAddr := sdk.AccAddress(toKey.PubKey().Address())
initFromCoins := app.AccountMapper.GetAccount(ctx, fromAddr).GetCoins()
denomIndex := r.Intn(len(initFromCoins))
amt, goErr := randPositiveInt(r, initFromCoins[denomIndex].Amount)
if goErr != nil {
return "skipping bank send due to account having no coins of denomination " + initFromCoins[denomIndex].Denom, nil
}
action = fmt.Sprintf("%s is sending %s %s to %s",
fromAddr.String(),
amt.String(),
initFromCoins[denomIndex].Denom,
toAddr.String(),
)
log = fmt.Sprintf("%s\n%s", log, action)
coins := sdk.Coins{{initFromCoins[denomIndex].Denom, amt}}
var msg = MsgSend{
Inputs: []Input{NewInput(fromAddr, coins)},
Outputs: []Output{NewOutput(toAddr, coins)},
}
sendAndVerifyMsgSend(t, app, msg, ctx, log, []crypto.PrivKey{fromKey})
return action, nil
}
// Sends and verifies the transition of a msg send. This fails if there are repeated inputs or outputs
func sendAndVerifyMsgSend(t *testing.T, app *mock.App, msg MsgSend, ctx sdk.Context, log string, privkeys []crypto.PrivKey) {
initialInputAddrCoins := make([]sdk.Coins, len(msg.Inputs))
initialOutputAddrCoins := make([]sdk.Coins, len(msg.Outputs))
AccountNumbers := make([]int64, len(msg.Inputs))
SequenceNumbers := make([]int64, len(msg.Inputs))
for i := 0; i < len(msg.Inputs); i++ {
acc := app.AccountMapper.GetAccount(ctx, msg.Inputs[i].Address)
AccountNumbers[i] = acc.GetAccountNumber()
SequenceNumbers[i] = acc.GetSequence()
initialInputAddrCoins[i] = acc.GetCoins()
}
for i := 0; i < len(msg.Outputs); i++ {
acc := app.AccountMapper.GetAccount(ctx, msg.Outputs[i].Address)
initialOutputAddrCoins[i] = acc.GetCoins()
}
tx := mock.GenTx([]sdk.Msg{msg},
AccountNumbers,
SequenceNumbers,
privkeys...)
res := app.Deliver(tx)
if !res.IsOK() {
// TODO: Do this in a more 'canonical' way
fmt.Println(res)
fmt.Println(log)
t.FailNow()
}
for i := 0; i < len(msg.Inputs); i++ {
terminalInputCoins := app.AccountMapper.GetAccount(ctx, msg.Inputs[i].Address).GetCoins()
require.Equal(t,
initialInputAddrCoins[i].Minus(msg.Inputs[i].Coins),
terminalInputCoins,
fmt.Sprintf("Input #%d had an incorrect amount of coins\n%s", i, log),
)
}
for i := 0; i < len(msg.Outputs); i++ {
terminalOutputCoins := app.AccountMapper.GetAccount(ctx, msg.Outputs[i].Address).GetCoins()
require.Equal(t,
initialOutputAddrCoins[i].Plus(msg.Outputs[i].Coins),
terminalOutputCoins,
fmt.Sprintf("Output #%d had an incorrect amount of coins\n%s", i, log),
)
}
}
func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) {
if !max.GT(sdk.OneInt()) {
return sdk.Int{}, errors.New("max too small")
}
max = max.Sub(sdk.OneInt())
return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil
}

View File

@ -239,3 +239,54 @@ func GetCmdQueryVote(storeName string, cdc *wire.Codec) *cobra.Command {
return cmd
}
// Command to Get a Proposal Information
func GetCmdQueryVotes(storeName string, cdc *wire.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "query-votes",
Short: "query votes on a proposal",
RunE: func(cmd *cobra.Command, args []string) error {
proposalID := viper.GetInt64(flagProposalID)
ctx := context.NewCoreContextFromViper()
res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName)
if len(res) == 0 || err != nil {
return errors.Errorf("proposalID [%d] does not exist", proposalID)
}
var proposal gov.Proposal
cdc.MustUnmarshalBinary(res, &proposal)
if proposal.GetStatus() != gov.StatusVotingPeriod {
fmt.Println("Proposal not in voting period.")
return nil
}
res2, err := ctx.QuerySubspace(cdc, gov.KeyVotesSubspace(proposalID), storeName)
if err != nil {
return err
}
var votes []gov.Vote
for i := 0; i < len(res2); i++ {
var vote gov.Vote
cdc.MustUnmarshalBinary(res2[i].Value, &vote)
votes = append(votes, vote)
}
output, err := wire.MarshalJSONIndent(cdc, votes)
if err != nil {
return err
}
fmt.Println(string(output))
return nil
},
}
cmd.Flags().String(flagProposalID, "", "proposalID of which proposal's votes are being queried")
return cmd
}

View File

@ -33,6 +33,8 @@ func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec) {
r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits/{%s}", RestProposalID, RestDepositer), queryDepositHandlerFn(cdc)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes/{%s}", RestProposalID, RestVoter), queryVoteHandlerFn(cdc)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes", RestProposalID), queryVotesOnProposalHandlerFn(cdc)).Methods("GET")
r.HandleFunc("/gov/proposals", queryProposalsWithParameterFn(cdc)).Methods("GET")
}
@ -335,6 +337,71 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc {
}
}
// nolint: gocyclo
// todo: Split this functionality into helper functions to remove the above
func queryVotesOnProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
strProposalID := vars[RestProposalID]
if len(strProposalID) == 0 {
w.WriteHeader(http.StatusBadRequest)
err := errors.New("proposalId required but not specified")
w.Write([]byte(err.Error()))
return
}
proposalID, err := strconv.ParseInt(strProposalID, 10, 64)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
err := errors.Errorf("proposalID [%s] is not positive", proposalID)
w.Write([]byte(err.Error()))
return
}
ctx := context.NewCoreContextFromViper()
res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName)
if err != nil || len(res) == 0 {
err := errors.Errorf("proposalID [%d] does not exist", proposalID)
w.Write([]byte(err.Error()))
return
}
var proposal gov.Proposal
cdc.MustUnmarshalBinary(res, &proposal)
if proposal.GetStatus() != gov.StatusVotingPeriod {
err := errors.Errorf("proposal is not in Voting Period", proposalID)
w.Write([]byte(err.Error()))
return
}
res2, err := ctx.QuerySubspace(cdc, gov.KeyVotesSubspace(proposalID), storeName)
if err != nil {
err = errors.New("ProposalID doesn't exist")
w.Write([]byte(err.Error()))
return
}
var votes []gov.Vote
for i := 0; i < len(res2); i++ {
var vote gov.Vote
cdc.MustUnmarshalBinary(res2[i].Value, &vote)
votes = append(votes, vote)
}
output, err := wire.MarshalJSONIndent(cdc, votes)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
w.Write(output)
}
}
// nolint: gocyclo
// todo: Split this functionality into helper functions to remove the above
func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc {

View File

@ -202,8 +202,7 @@ func RandomSetGenesis(r *rand.Rand, app *App, addrs []sdk.AccAddress, denoms []s
(&baseAcc).SetCoins(coins)
accts[i] = &baseAcc
}
SetGenesis(app, accts)
app.GenesisAccounts = accts
}
// GetAllAccounts returns all accounts in the accountMapper.

View File

@ -1,95 +0,0 @@
package mock
import (
"fmt"
"math/big"
"math/rand"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
// RandomizedTesting tests application by sending random messages.
func (app *App) RandomizedTesting(
t *testing.T, ops []TestAndRunTx, setups []RandSetup,
invariants []Invariant, numKeys int, numBlocks int, blockSize int,
) {
time := time.Now().UnixNano()
app.RandomizedTestingFromSeed(t, time, ops, setups, invariants, numKeys, numBlocks, blockSize)
}
// RandomizedTestingFromSeed tests an application by running the provided
// operations, testing the provided invariants, but using the provided seed.
func (app *App) RandomizedTestingFromSeed(
t *testing.T, seed int64, ops []TestAndRunTx, setups []RandSetup,
invariants []Invariant, numKeys int, numBlocks int, blockSize int,
) {
log := fmt.Sprintf("Starting SingleModuleTest with randomness created with seed %d", int(seed))
keys, addrs := GeneratePrivKeyAddressPairs(numKeys)
r := rand.New(rand.NewSource(seed))
for i := 0; i < len(setups); i++ {
setups[i](r, keys)
}
RandomSetGenesis(r, app, addrs, []string{"foocoin"})
header := abci.Header{Height: 0}
for i := 0; i < numBlocks; i++ {
app.BeginBlock(abci.RequestBeginBlock{})
// Make sure invariants hold at beginning of block and when nothing was
// done.
app.assertAllInvariants(t, invariants, log)
ctx := app.NewContext(false, header)
// TODO: Add modes to simulate "no load", "medium load", and
// "high load" blocks.
for j := 0; j < blockSize; j++ {
logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log)
log += "\n" + logUpdate
require.Nil(t, err, log)
app.assertAllInvariants(t, invariants, log)
}
app.EndBlock(abci.RequestEndBlock{})
header.Height++
}
}
func (app *App) assertAllInvariants(t *testing.T, tests []Invariant, log string) {
for i := 0; i < len(tests); i++ {
tests[i](t, app, log)
}
}
// BigInterval is a representation of the interval [lo, hi), where
// lo and hi are both of type sdk.Int
type BigInterval struct {
lo sdk.Int
hi sdk.Int
}
// RandFromBigInterval chooses an interval uniformly from the provided list of
// BigIntervals, and then chooses an element from an interval uniformly at random.
func RandFromBigInterval(r *rand.Rand, intervals []BigInterval) sdk.Int {
if len(intervals) == 0 {
return sdk.ZeroInt()
}
interval := intervals[r.Intn(len(intervals))]
lo := interval.lo
hi := interval.hi
diff := hi.Sub(lo)
result := sdk.NewIntFromBigInt(new(big.Int).Rand(r, diff.BigInt()))
result = result.Add(lo)
return result
}

View File

@ -0,0 +1,81 @@
package simulation
import (
"encoding/json"
"fmt"
"math/rand"
"testing"
"time"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
// Simulate tests application by sending random messages.
func Simulate(
t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage, ops []TestAndRunTx, setups []RandSetup,
invariants []Invariant, numKeys int, numBlocks int, blockSize int,
) {
time := time.Now().UnixNano()
SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numKeys, numBlocks, blockSize)
}
// SimulateFromSeed tests an application by running the provided
// operations, testing the provided invariants, but using the provided seed.
func SimulateFromSeed(
t *testing.T, app *baseapp.BaseApp, appStateFn func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage, seed int64, ops []TestAndRunTx, setups []RandSetup,
invariants []Invariant, numKeys int, numBlocks int, blockSize int,
) {
log := fmt.Sprintf("Starting SimulateFromSeed with randomness created with seed %d", int(seed))
keys, addrs := mock.GeneratePrivKeyAddressPairs(numKeys)
r := rand.New(rand.NewSource(seed))
// Setup event stats
events := make(map[string]uint)
event := func(what string) {
events[what]++
}
app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, addrs)})
for i := 0; i < len(setups); i++ {
setups[i](r, keys)
}
app.Commit()
header := abci.Header{Height: 0}
for i := 0; i < numBlocks; i++ {
app.BeginBlock(abci.RequestBeginBlock{})
// Make sure invariants hold at beginning of block and when nothing was
// done.
AssertAllInvariants(t, app, invariants, log)
ctx := app.NewContext(false, header)
// TODO: Add modes to simulate "no load", "medium load", and
// "high load" blocks.
for j := 0; j < blockSize; j++ {
logUpdate, err := ops[r.Intn(len(ops))](t, r, app, ctx, keys, log, event)
log += "\n" + logUpdate
require.Nil(t, err, log)
AssertAllInvariants(t, app, invariants, log)
}
app.EndBlock(abci.RequestEndBlock{})
header.Height++
}
DisplayEvents(events)
}
// AssertAllInvariants asserts a list of provided invariants against application state
func AssertAllInvariants(t *testing.T, app *baseapp.BaseApp, tests []Invariant, log string) {
for i := 0; i < len(tests); i++ {
tests[i](t, app, log)
}
}

View File

@ -1,9 +1,10 @@
package mock
package simulation
import (
"math/rand"
"testing"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/crypto"
)
@ -13,8 +14,8 @@ type (
// transition was as expected. It returns a descriptive message "action"
// about what this fuzzed tx actually did, for ease of debugging.
TestAndRunTx func(
t *testing.T, r *rand.Rand, app *App, ctx sdk.Context,
privKeys []crypto.PrivKey, log string,
t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
privKeys []crypto.PrivKey, log string, event func(string),
) (action string, err sdk.Error)
// RandSetup performs the random setup the mock module needs.
@ -23,14 +24,14 @@ type (
// An Invariant is a function which tests a particular invariant.
// If the invariant has been broken, the function should halt the
// test and output the log.
Invariant func(t *testing.T, app *App, log string)
Invariant func(t *testing.T, app *baseapp.BaseApp, log string)
)
// 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(t *testing.T, app *App, log string) {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
if int(app.LastBlockHeight())%period == offset {
invariant(t, app, log)
}

56
x/mock/simulation/util.go Normal file
View File

@ -0,0 +1,56 @@
package simulation
import (
"fmt"
"math/rand"
crypto "github.com/tendermint/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// 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--
}
return string(b)
}
// Pretty-print events as a table
func DisplayEvents(events map[string]uint) {
// TODO
fmt.Printf("Events: %v\n", events)
}
// Pick a random key from an array
func RandomKey(r *rand.Rand, keys []crypto.PrivKey) crypto.PrivKey {
return keys[r.Intn(
len(keys),
)]
}
// Generate a random amount
func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {
return sdk.NewInt(int64(r.Intn(int(max.Int64()))))
}

View File

@ -1,6 +1,8 @@
package mock
import (
"math/big"
"math/rand"
"testing"
"github.com/cosmos/cosmos-sdk/baseapp"
@ -10,6 +12,32 @@ import (
"github.com/tendermint/tendermint/crypto"
)
// BigInterval is a representation of the interval [lo, hi), where
// lo and hi are both of type sdk.Int
type BigInterval struct {
lo sdk.Int
hi sdk.Int
}
// RandFromBigInterval chooses an interval uniformly from the provided list of
// BigIntervals, and then chooses an element from an interval uniformly at random.
func RandFromBigInterval(r *rand.Rand, intervals []BigInterval) sdk.Int {
if len(intervals) == 0 {
return sdk.ZeroInt()
}
interval := intervals[r.Intn(len(intervals))]
lo := interval.lo
hi := interval.hi
diff := hi.Sub(lo)
result := sdk.NewIntFromBigInt(new(big.Int).Rand(r, diff.BigInt()))
result = result.Add(lo)
return result
}
// CheckBalance checks the balance of an account.
func CheckBalance(t *testing.T, app *App, addr sdk.AccAddress, exp sdk.Coins) {
ctxCheck := app.BaseApp.NewContext(true, abci.Header{})

View File

@ -110,6 +110,22 @@ func (k Keeper) GetUnbondingDelegationsFromValidator(ctx sdk.Context, valAddr sd
return ubds
}
// iterate through all of the unbonding delegations
func (k Keeper) IterateUnbondingDelegations(ctx sdk.Context, fn func(index int64, ubd types.UnbondingDelegation) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, UnbondingDelegationKey)
i := int64(0)
for ; iterator.Valid(); iterator.Next() {
ubd := types.MustUnmarshalUBD(k.cdc, iterator.Key(), iterator.Value())
stop := fn(i, ubd)
if stop {
break
}
i++
}
iterator.Close()
}
// set the unbonding delegation and associated index
func (k Keeper) SetUnbondingDelegation(ctx sdk.Context, ubd types.UnbondingDelegation) {
store := ctx.KVStore(k.storeKey)
@ -298,6 +314,12 @@ func (k Keeper) unbond(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddr
// complete unbonding an unbonding record
func (k Keeper) BeginUnbonding(ctx sdk.Context, delegatorAddr, validatorAddr sdk.AccAddress, sharesAmount sdk.Rat) sdk.Error {
// TODO quick fix, instead we should use an index, see https://github.com/cosmos/cosmos-sdk/issues/1402
_, found := k.GetUnbondingDelegation(ctx, delegatorAddr, validatorAddr)
if found {
return types.ErrExistingUnbondingDelegation(k.Codespace())
}
returnAmount, err := k.unbond(ctx, delegatorAddr, validatorAddr, sharesAmount)
if err != nil {
return err

View File

@ -0,0 +1,81 @@
package simulation
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
"github.com/cosmos/cosmos-sdk/x/stake"
abci "github.com/tendermint/tendermint/abci/types"
)
// AllInvariants runs all invariants of the stake module.
// Currently: total supply, positive power
func AllInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
SupplyInvariants(ck, k, am)(t, app, log)
PositivePowerInvariant(k)(t, app, log)
ValidatorSetInvariant(k)(t, app, log)
}
}
// SupplyInvariants checks that the total supply reflects all held loose tokens, bonded tokens, and unbonding delegations
func SupplyInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
ctx := app.NewContext(false, abci.Header{})
pool := k.GetPool(ctx)
loose := sdk.ZeroInt()
bonded := sdk.ZeroRat()
am.IterateAccounts(ctx, func(acc auth.Account) bool {
loose = loose.Add(acc.GetCoins().AmountOf("steak"))
return false
})
k.IterateUnbondingDelegations(ctx, func(_ int64, ubd stake.UnbondingDelegation) bool {
loose = loose.Add(ubd.Balance.Amount)
return false
})
k.IterateValidators(ctx, func(_ int64, validator sdk.Validator) bool {
switch validator.GetStatus() {
case sdk.Bonded:
bonded = bonded.Add(validator.GetPower())
case sdk.Unbonding:
case sdk.Unbonded:
loose = loose.Add(validator.GetTokens().RoundInt())
}
return false
})
// Loose tokens should equal coin supply plus unbonding delegations plus tokens on unbonded validators
require.True(t, pool.LooseTokens.RoundInt64() == loose.Int64(), "expected loose tokens to equal total steak held by accounts - pool.LooseTokens: %v, sum of account tokens: %v\nlog: %s",
pool.LooseTokens.RoundInt64(), loose.Int64(), log)
// Bonded tokens should equal sum of tokens with bonded validators
require.True(t, pool.BondedTokens.Equal(bonded), "expected bonded tokens to equal total steak held by bonded validators\nlog: %s", log)
// TODO Inflation check on total supply
}
}
// PositivePowerInvariant checks that all stored validators have > 0 power
func PositivePowerInvariant(k stake.Keeper) simulation.Invariant {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
ctx := app.NewContext(false, abci.Header{})
k.IterateValidatorsBonded(ctx, func(_ int64, validator sdk.Validator) bool {
require.True(t, validator.GetPower().GT(sdk.ZeroRat()), "validator with non-positive power stored")
return false
})
}
}
// ValidatorSetInvariant checks equivalence of Tendermint validator set and SDK validator set
func ValidatorSetInvariant(k stake.Keeper) simulation.Invariant {
return func(t *testing.T, app *baseapp.BaseApp, log string) {
// TODO
}
}

253
x/stake/simulation/msgs.go Normal file
View File

@ -0,0 +1,253 @@
package simulation
import (
"fmt"
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
"github.com/cosmos/cosmos-sdk/x/stake"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
// SimulateMsgCreateValidator
func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
denom := k.GetParams(ctx).BondDenom
description := stake.Description{
Moniker: simulation.RandStringOfLength(r, 10),
}
key := simulation.RandomKey(r, keys)
pubkey := key.PubKey()
address := sdk.AccAddress(pubkey.Address())
amount := m.GetAccount(ctx, address).GetCoins().AmountOf(denom)
if amount.GT(sdk.ZeroInt()) {
amount = simulation.RandomAmount(r, amount)
}
if amount.Equal(sdk.ZeroInt()) {
return "no-operation", nil
}
msg := stake.MsgCreateValidator{
Description: description,
ValidatorAddr: address,
DelegatorAddr: address,
PubKey: pubkey,
Delegation: sdk.NewIntCoin(denom, amount),
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgCreateValidator/%v", result.IsOK()))
// require.True(t, result.IsOK(), "expected OK result but instead got %v", result)
action = fmt.Sprintf("TestMsgCreateValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil
}
}
// SimulateMsgEditValidator
func SimulateMsgEditValidator(k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
description := stake.Description{
Moniker: simulation.RandStringOfLength(r, 10),
Identity: simulation.RandStringOfLength(r, 10),
Website: simulation.RandStringOfLength(r, 10),
Details: simulation.RandStringOfLength(r, 10),
}
key := simulation.RandomKey(r, keys)
pubkey := key.PubKey()
address := sdk.AccAddress(pubkey.Address())
msg := stake.MsgEditValidator{
Description: description,
ValidatorAddr: address,
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgEditValidator/%v", result.IsOK()))
action = fmt.Sprintf("TestMsgEditValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil
}
}
// SimulateMsgDelegate
func SimulateMsgDelegate(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
denom := k.GetParams(ctx).BondDenom
validatorKey := simulation.RandomKey(r, keys)
validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address())
delegatorKey := simulation.RandomKey(r, keys)
delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address())
amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom)
if amount.GT(sdk.ZeroInt()) {
amount = simulation.RandomAmount(r, amount)
}
if amount.Equal(sdk.ZeroInt()) {
return "no-operation", nil
}
msg := stake.MsgDelegate{
DelegatorAddr: delegatorAddress,
ValidatorAddr: validatorAddress,
Delegation: sdk.NewIntCoin(denom, amount),
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgDelegate/%v", result.IsOK()))
action = fmt.Sprintf("TestMsgDelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil
}
}
// SimulateMsgBeginUnbonding
func SimulateMsgBeginUnbonding(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
denom := k.GetParams(ctx).BondDenom
validatorKey := simulation.RandomKey(r, keys)
validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address())
delegatorKey := simulation.RandomKey(r, keys)
delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address())
amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom)
if amount.GT(sdk.ZeroInt()) {
amount = simulation.RandomAmount(r, amount)
}
if amount.Equal(sdk.ZeroInt()) {
return "no-operation", nil
}
msg := stake.MsgBeginUnbonding{
DelegatorAddr: delegatorAddress,
ValidatorAddr: validatorAddress,
SharesAmount: sdk.NewRatFromInt(amount),
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgBeginUnbonding/%v", result.IsOK()))
action = fmt.Sprintf("TestMsgBeginUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil
}
}
// SimulateMsgCompleteUnbonding
func SimulateMsgCompleteUnbonding(k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
validatorKey := simulation.RandomKey(r, keys)
validatorAddress := sdk.AccAddress(validatorKey.PubKey().Address())
delegatorKey := simulation.RandomKey(r, keys)
delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address())
msg := stake.MsgCompleteUnbonding{
DelegatorAddr: delegatorAddress,
ValidatorAddr: validatorAddress,
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgCompleteUnbonding/%v", result.IsOK()))
action = fmt.Sprintf("TestMsgCompleteUnbonding: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil
}
}
// SimulateMsgBeginRedelegate
func SimulateMsgBeginRedelegate(m auth.AccountMapper, k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
denom := k.GetParams(ctx).BondDenom
sourceValidatorKey := simulation.RandomKey(r, keys)
sourceValidatorAddress := sdk.AccAddress(sourceValidatorKey.PubKey().Address())
destValidatorKey := simulation.RandomKey(r, keys)
destValidatorAddress := sdk.AccAddress(destValidatorKey.PubKey().Address())
delegatorKey := simulation.RandomKey(r, keys)
delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address())
// TODO
amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom)
if amount.GT(sdk.ZeroInt()) {
amount = simulation.RandomAmount(r, amount)
}
if amount.Equal(sdk.ZeroInt()) {
return "no-operation", nil
}
msg := stake.MsgBeginRedelegate{
DelegatorAddr: delegatorAddress,
ValidatorSrcAddr: sourceValidatorAddress,
ValidatorDstAddr: destValidatorAddress,
SharesAmount: sdk.NewRatFromInt(amount),
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgBeginRedelegate/%v", result.IsOK()))
action = fmt.Sprintf("TestMsgBeginRedelegate: %s", msg.GetSignBytes())
return action, nil
}
}
// SimulateMsgCompleteRedelegate
func SimulateMsgCompleteRedelegate(k stake.Keeper) simulation.TestAndRunTx {
return func(t *testing.T, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, keys []crypto.PrivKey, log string, event func(string)) (action string, err sdk.Error) {
validatorSrcKey := simulation.RandomKey(r, keys)
validatorSrcAddress := sdk.AccAddress(validatorSrcKey.PubKey().Address())
validatorDstKey := simulation.RandomKey(r, keys)
validatorDstAddress := sdk.AccAddress(validatorDstKey.PubKey().Address())
delegatorKey := simulation.RandomKey(r, keys)
delegatorAddress := sdk.AccAddress(delegatorKey.PubKey().Address())
msg := stake.MsgCompleteRedelegate{
DelegatorAddr: delegatorAddress,
ValidatorSrcAddr: validatorSrcAddress,
ValidatorDstAddr: validatorDstAddress,
}
require.Nil(t, msg.ValidateBasic(), "expected msg to pass ValidateBasic: %s", msg.GetSignBytes())
ctx, write := ctx.CacheContext()
result := stake.NewHandler(k)(ctx, msg)
if result.IsOK() {
write()
}
event(fmt.Sprintf("stake/MsgCompleteRedelegate/%v", result.IsOK()))
action = fmt.Sprintf("TestMsgCompleteRedelegate: ok %v, msg %s", result.IsOK(), msg.GetSignBytes())
return action, nil
}
}
// Setup
func Setup(mapp *mock.App, k stake.Keeper) simulation.RandSetup {
return func(r *rand.Rand, privKeys []crypto.PrivKey) {
ctx := mapp.NewContext(false, abci.Header{})
stake.InitGenesis(ctx, k, stake.DefaultGenesisState())
params := k.GetParams(ctx)
denom := params.BondDenom
loose := sdk.ZeroInt()
mapp.AccountMapper.IterateAccounts(ctx, func(acc auth.Account) bool {
balance := simulation.RandomAmount(r, sdk.NewInt(1000000))
acc.SetCoins(acc.GetCoins().Plus(sdk.Coins{sdk.NewIntCoin(denom, balance)}))
mapp.AccountMapper.SetAccount(ctx, acc)
loose = loose.Add(balance)
return false
})
pool := k.GetPool(ctx)
pool.LooseTokens = pool.LooseTokens.Add(sdk.NewRat(loose.Int64(), 1))
k.SetPool(ctx, pool)
}
}

View File

@ -0,0 +1,59 @@
package simulation
import (
"encoding/json"
"math/rand"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/mock"
"github.com/cosmos/cosmos-sdk/x/mock/simulation"
"github.com/cosmos/cosmos-sdk/x/stake"
abci "github.com/tendermint/tendermint/abci/types"
)
// TestStakeWithRandomMessages
func TestStakeWithRandomMessages(t *testing.T) {
mapp := mock.NewApp()
bank.RegisterWire(mapp.Cdc)
mapper := mapp.AccountMapper
coinKeeper := bank.NewKeeper(mapper)
stakeKey := sdk.NewKVStoreKey("stake")
stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, coinKeeper, stake.DefaultCodespace)
mapp.Router().AddRoute("stake", stake.NewHandler(stakeKeeper))
mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
validatorUpdates := stake.EndBlocker(ctx, stakeKeeper)
return abci.ResponseEndBlock{
ValidatorUpdates: validatorUpdates,
}
})
err := mapp.CompleteSetup([]*sdk.KVStoreKey{stakeKey})
if err != nil {
panic(err)
}
appStateFn := func(r *rand.Rand, accs []sdk.AccAddress) json.RawMessage {
mock.RandomSetGenesis(r, mapp, accs, []string{"stake"})
return json.RawMessage("{}")
}
simulation.Simulate(
t, mapp.BaseApp, appStateFn,
[]simulation.TestAndRunTx{
SimulateMsgCreateValidator(mapper, stakeKeeper),
SimulateMsgEditValidator(stakeKeeper),
SimulateMsgDelegate(mapper, stakeKeeper),
SimulateMsgBeginUnbonding(mapper, stakeKeeper),
SimulateMsgCompleteUnbonding(stakeKeeper),
SimulateMsgBeginRedelegate(mapper, stakeKeeper),
SimulateMsgCompleteRedelegate(stakeKeeper),
}, []simulation.RandSetup{
Setup(mapp, stakeKeeper),
}, []simulation.Invariant{
AllInvariants(coinKeeper, stakeKeeper, mapp.AccountMapper),
}, 10, 100, 100,
)
}

View File

@ -120,6 +120,10 @@ func ErrNoUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "no unbonding delegation found")
}
func ErrExistingUnbondingDelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "existing unbonding delegation found")
}
func ErrNoRedelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "no redelegation found")
}

View File

@ -1,12 +1,7 @@
package types
import (
"fmt"
"math/rand"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
)
@ -21,171 +16,3 @@ var (
emptyAddr sdk.AccAddress
emptyPubkey crypto.PubKey
)
// Operation reflects any operation that transforms staking state. It takes in
// a RNG instance, pool, validator and returns an updated pool, updated
// validator, delta tokens, and descriptive message.
type Operation func(r *rand.Rand, pool Pool, c Validator) (Pool, Validator, sdk.Rat, string)
// OpBondOrUnbond implements an operation that bonds or unbonds a validator
// depending on current status.
// nolint: unparam
// TODO split up into multiple operations
func OpBondOrUnbond(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) {
var (
msg string
newStatus sdk.BondStatus
)
if validator.Status == sdk.Bonded {
msg = fmt.Sprintf("sdk.Unbonded previously bonded validator %#v", validator)
newStatus = sdk.Unbonded
} else if validator.Status == sdk.Unbonded {
msg = fmt.Sprintf("sdk.Bonded previously bonded validator %#v", validator)
newStatus = sdk.Bonded
}
validator, pool = validator.UpdateStatus(pool, newStatus)
return pool, validator, sdk.ZeroRat(), msg
}
// OpAddTokens implements an operation that adds a random number of tokens to a
// validator.
func OpAddTokens(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) {
msg := fmt.Sprintf("validator %#v", validator)
tokens := int64(r.Int31n(1000))
validator, pool, _ = validator.AddTokensFromDel(pool, tokens)
msg = fmt.Sprintf("Added %d tokens to %s", tokens, msg)
// Tokens are removed so for accounting must be negative
return pool, validator, sdk.NewRat(-1 * tokens), msg
}
// OpRemoveShares implements an operation that removes a random number of
// delegatorshares from a validator.
func OpRemoveShares(r *rand.Rand, pool Pool, validator Validator) (Pool, Validator, sdk.Rat, string) {
var shares sdk.Rat
for {
shares = sdk.NewRat(int64(r.Int31n(1000)))
if shares.LT(validator.DelegatorShares) {
break
}
}
msg := fmt.Sprintf("Removed %v shares from validator %#v", shares, validator)
validator, pool, tokens := validator.RemoveDelShares(pool, shares)
return pool, validator, tokens, msg
}
// RandomOperation returns a random staking operation.
func RandomOperation(r *rand.Rand) Operation {
operations := []Operation{
OpBondOrUnbond,
OpAddTokens,
OpRemoveShares,
}
r.Shuffle(len(operations), func(i, j int) {
operations[i], operations[j] = operations[j], operations[i]
})
return operations[0]
}
// AssertInvariants ensures invariants that should always be true are true.
// nolint: unparam
func AssertInvariants(t *testing.T, msg string,
pOrig Pool, cOrig []Validator, pMod Pool, vMods []Validator) {
// total tokens conserved
require.True(t,
pOrig.LooseTokens.Add(pOrig.BondedTokens).Equal(
pMod.LooseTokens.Add(pMod.BondedTokens)),
"Tokens not conserved - msg: %v\n, pOrig.BondedTokens: %v, pOrig.LooseTokens: %v, pMod.BondedTokens: %v, pMod.LooseTokens: %v",
msg,
pOrig.BondedTokens, pOrig.LooseTokens,
pMod.BondedTokens, pMod.LooseTokens)
// Nonnegative bonded tokens
require.False(t, pMod.BondedTokens.LT(sdk.ZeroRat()),
"Negative bonded shares - msg: %v\npOrig: %v\npMod: %v\n",
msg, pOrig, pMod)
// Nonnegative loose tokens
require.False(t, pMod.LooseTokens.LT(sdk.ZeroRat()),
"Negative unbonded shares - msg: %v\npOrig: %v\npMod: %v\n",
msg, pOrig, pMod)
for _, vMod := range vMods {
// Nonnegative ex rate
require.False(t, vMod.DelegatorShareExRate().LT(sdk.ZeroRat()),
"Applying operation \"%s\" resulted in negative validator.DelegatorShareExRate(): %v (validator.Owner: %s)",
msg,
vMod.DelegatorShareExRate(),
vMod.Owner,
)
// Nonnegative poolShares
require.False(t, vMod.BondedTokens().LT(sdk.ZeroRat()),
"Applying operation \"%s\" resulted in negative validator.BondedTokens(): %#v",
msg,
vMod,
)
// Nonnegative delShares
require.False(t, vMod.DelegatorShares.LT(sdk.ZeroRat()),
"Applying operation \"%s\" resulted in negative validator.DelegatorShares: %#v",
msg,
vMod,
)
}
}
// TODO: refactor this random setup
// randomValidator generates a random validator.
// nolint: unparam
func randomValidator(r *rand.Rand, i int) Validator {
tokens := sdk.NewRat(int64(r.Int31n(10000)))
delShares := sdk.NewRat(int64(r.Int31n(10000)))
// TODO add more options here
status := sdk.Bonded
if r.Float64() > float64(0.5) {
status = sdk.Unbonded
}
validator := NewValidator(addr1, pk1, Description{})
validator.Status = status
validator.Tokens = tokens
validator.DelegatorShares = delShares
return validator
}
// RandomSetup generates a random staking state.
func RandomSetup(r *rand.Rand, numValidators int) (Pool, []Validator) {
pool := InitialPool()
pool.LooseTokens = sdk.NewRat(100000)
validators := make([]Validator, numValidators)
for i := 0; i < numValidators; i++ {
validator := randomValidator(r, i)
switch validator.Status {
case sdk.Bonded:
pool.BondedTokens = pool.BondedTokens.Add(validator.Tokens)
case sdk.Unbonded, sdk.Unbonding:
pool.LooseTokens = pool.LooseTokens.Add(validator.Tokens)
default:
panic("improper use of RandomSetup")
}
validators[i] = validator
}
return pool, validators
}

View File

@ -434,5 +434,6 @@ func (v Validator) GetStatus() sdk.BondStatus { return v.Status }
func (v Validator) GetOwner() sdk.AccAddress { return v.Owner }
func (v Validator) GetPubKey() crypto.PubKey { return v.PubKey }
func (v Validator) GetPower() sdk.Rat { return v.BondedTokens() }
func (v Validator) GetTokens() sdk.Rat { return v.Tokens }
func (v Validator) GetDelegatorShares() sdk.Rat { return v.DelegatorShares }
func (v Validator) GetBondHeight() int64 { return v.BondHeight }

View File

@ -2,7 +2,6 @@ package types
import (
"fmt"
"math/rand"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -234,67 +233,6 @@ func TestPossibleOverflow(t *testing.T) {
msg, newValidator.DelegatorShareExRate())
}
// run random operations in a random order on a random single-validator state, assert invariants hold
func TestSingleValidatorIntegrationInvariants(t *testing.T) {
r := rand.New(rand.NewSource(41))
for i := 0; i < 10; i++ {
poolOrig, validatorsOrig := RandomSetup(r, 1)
require.Equal(t, 1, len(validatorsOrig))
// sanity check
AssertInvariants(t, "no operation",
poolOrig, validatorsOrig,
poolOrig, validatorsOrig)
for j := 0; j < 5; j++ {
poolMod, validatorMod, _, msg := RandomOperation(r)(r, poolOrig, validatorsOrig[0])
validatorsMod := make([]Validator, len(validatorsOrig))
copy(validatorsMod[:], validatorsOrig[:])
require.Equal(t, 1, len(validatorsOrig), "j %v", j)
require.Equal(t, 1, len(validatorsMod), "j %v", j)
validatorsMod[0] = validatorMod
AssertInvariants(t, msg,
poolOrig, validatorsOrig,
poolMod, validatorsMod)
poolOrig = poolMod
validatorsOrig = validatorsMod
}
}
}
// run random operations in a random order on a random multi-validator state, assert invariants hold
func TestMultiValidatorIntegrationInvariants(t *testing.T) {
r := rand.New(rand.NewSource(42))
for i := 0; i < 10; i++ {
poolOrig, validatorsOrig := RandomSetup(r, 100)
AssertInvariants(t, "no operation",
poolOrig, validatorsOrig,
poolOrig, validatorsOrig)
for j := 0; j < 5; j++ {
index := int(r.Int31n(int32(len(validatorsOrig))))
poolMod, validatorMod, _, msg := RandomOperation(r)(r, poolOrig, validatorsOrig[index])
validatorsMod := make([]Validator, len(validatorsOrig))
copy(validatorsMod[:], validatorsOrig[:])
validatorsMod[index] = validatorMod
AssertInvariants(t, msg,
poolOrig, validatorsOrig,
poolMod, validatorsMod)
poolOrig = poolMod
validatorsOrig = validatorsMod
}
}
}
func TestHumanReadableString(t *testing.T) {
validator := NewValidator(addr1, pk1, Description{})