Merge pull request #1175: Randomized Module Testing

* WIP, ammend this later

* Add randomized testing suite

* Fix linting

* Auth invariant check, method to take in seed, way to run invariant check less frequently

* Fix merge conflicts

* Update bank

* Fix error on zero input by skipping it

* Add PeriodicInvariant Function

* Abstract verification / send functionality

* Fix liniting errors (PeriodicInvariant godoc)

* Update formatting and docs of randomization

* Minor refactor, update godocs

* Update godoc for mock

* Export TestAndRunTx

* fix cyclic dependencies

* Address PR most pr comments

* Fix merge conflict: Bring back codec.seal

* remove debug code, fix linting

* Fix merge conflicts
This commit is contained in:
Dev Ojha 2018-07-06 13:19:11 -07:00 committed by Rigel
parent 86d7b8f981
commit 6f8f222ef6
10 changed files with 757 additions and 338 deletions

View File

@ -74,6 +74,9 @@ FEATURES
* [gaiacli] Ledger support added
- You can now use a Ledger with `gaiacli --ledger` for all key-related commands
- Ledger keys can be named and tracked locally in the key DB
* [testing] created a randomized testing framework.
- Currently bank has limited functionality in the framework
- Auth has its invariants checked within the framework
* [gaiacli] added the following flags for commands that post transactions to the chain:
* async -- send the tx without waiting for a tendermint response
* json -- return the output in json format for increased readability

View File

@ -1,7 +1,6 @@
package baseapp
import (
"encoding/json"
"fmt"
"os"
"testing"
@ -19,7 +18,6 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
)
func defaultLogger() log.Logger {
@ -651,324 +649,6 @@ func TestValidatorChange(t *testing.T) {
//----------------------------------------
// Use burn and send msg types to test multiple msgs in one tx
type testBurnMsg struct {
Addr sdk.Address
Amount sdk.Coins
}
const msgType3 = "burn"
func (msg testBurnMsg) Type() string { return msgType3 }
func (msg testBurnMsg) GetSignBytes() []byte {
bz, _ := json.Marshal(msg)
return sdk.MustSortJSON(bz)
}
func (msg testBurnMsg) ValidateBasic() sdk.Error {
if msg.Addr == nil {
return sdk.ErrInvalidAddress("Cannot use nil as Address")
}
return nil
}
func (msg testBurnMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Addr}
}
type testSendMsg struct {
Sender sdk.Address
Receiver sdk.Address
Amount sdk.Coins
}
const msgType4 = "send"
func (msg testSendMsg) Type() string { return msgType4 }
func (msg testSendMsg) GetSignBytes() []byte {
bz, _ := json.Marshal(msg)
return sdk.MustSortJSON(bz)
}
func (msg testSendMsg) ValidateBasic() sdk.Error {
if msg.Sender == nil || msg.Receiver == nil {
return sdk.ErrInvalidAddress("Cannot use nil as Address")
}
return nil
}
func (msg testSendMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Sender}
}
// Simple Handlers for burn and send
func newHandleBurn(keeper bank.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
burnMsg := msg.(testBurnMsg)
_, _, err := keeper.SubtractCoins(ctx, burnMsg.Addr, burnMsg.Amount)
if err != nil {
return err.Result()
}
return sdk.Result{}
}
}
func newHandleSpend(keeper bank.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
spendMsg := msg.(testSendMsg)
_, _, err := keeper.SubtractCoins(ctx, spendMsg.Sender, spendMsg.Amount)
if err != nil {
return err.Result()
}
_, _, err = keeper.AddCoins(ctx, spendMsg.Receiver, spendMsg.Amount)
if err != nil {
return err.Result()
}
return sdk.Result{}
}
}
// generate a signed transaction
func GenTx(chainID string, msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
// make the transaction free
fee := auth.StdFee{
sdk.Coins{{"foocoin", sdk.NewInt(0)}},
100000,
}
sigs := make([]auth.StdSignature, len(priv))
for i, p := range priv {
sig, err := p.Sign(auth.StdSignBytes(chainID, accnums[i], seq[i], fee, msgs, ""))
// TODO: replace with proper error handling:
if err != nil {
panic(err)
}
sigs[i] = auth.StdSignature{
PubKey: p.PubKey(),
Signature: sig,
AccountNumber: accnums[i],
Sequence: seq[i],
}
}
return auth.NewStdTx(msgs, fee, sigs, "")
}
// spin up simple app for testing
type testApp struct {
*BaseApp
accountMapper auth.AccountMapper
accountKeeper bank.Keeper
}
func newTestApp(name string) testApp {
return testApp{
BaseApp: newBaseApp(name),
}
}
func MakeCodec() *wire.Codec {
cdc := wire.NewCodec()
cdc.RegisterInterface((*sdk.Msg)(nil), nil)
crypto.RegisterAmino(cdc)
cdc.RegisterInterface((*auth.Account)(nil), nil)
cdc.RegisterConcrete(&auth.BaseAccount{}, "cosmos-sdk/BaseAccount", nil)
cdc.Seal()
return cdc
}
// tests multiple msgs of same type from same address in single tx
func TestMultipleBurn(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.accountKeeper = bank.NewKeeper(app.accountMapper)
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountKeeper)).
AddRoute("send", newHandleSpend(app.accountKeeper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv := makePrivKey("my secret")
addr := priv.PubKey().Address()
app.accountKeeper.AddCoins(app.deliverState.ctx, addr, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr), "Balance did not update")
msg := testBurnMsg{addr, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
tx := GenTx(t.Name(), []sdk.Msg{msg, msg}, []int64{0}, []int64{0}, priv)
res := app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), app.accountKeeper.GetCoins(app.deliverState.ctx, addr), "Double burn did not work")
}
// tests multiples msgs of same type from different addresses in single tx
func TestBurnMultipleOwners(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.accountKeeper = bank.NewKeeper(app.accountMapper)
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountKeeper)).
AddRoute("send", newHandleSpend(app.accountKeeper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv1 := makePrivKey("my secret 1")
addr1 := priv1.PubKey().Address()
priv2 := makePrivKey("my secret 2")
addr2 := priv2.PubKey().Address()
// fund accounts
app.accountKeeper.AddCoins(app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
app.accountKeeper.AddCoins(app.deliverState.ctx, addr2, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr1), "Balance1 did not update")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr2), "Balance2 did not update")
msg1 := testBurnMsg{addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}}}
msg2 := testBurnMsg{addr2, sdk.Coins{{"foocoin", sdk.NewInt(100)}}}
// test wrong signers: Address 1 signs both messages
tx := GenTx(t.Name(), []sdk.Msg{msg1, msg2}, []int64{0, 0}, []int64{0, 0}, priv1, priv1)
res := app.Deliver(tx)
require.Equal(t, sdk.ABCICodeType(0x10003), res.Code, "Wrong signatures passed")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr1), "Balance1 changed after invalid sig")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr2), "Balance2 changed after invalid sig")
// test valid tx
tx = GenTx(t.Name(), []sdk.Msg{msg1, msg2}, []int64{0, 1}, []int64{1, 0}, priv1, priv2)
res = app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), app.accountKeeper.GetCoins(app.deliverState.ctx, addr1), "Balance1 did not change after valid tx")
require.Equal(t, sdk.Coins(nil), app.accountKeeper.GetCoins(app.deliverState.ctx, addr2), "Balance2 did not change after valid tx")
}
// tests different msg types in single tx with different addresses
func TestSendBurn(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.accountKeeper = bank.NewKeeper(app.accountMapper)
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountKeeper)).
AddRoute("send", newHandleSpend(app.accountKeeper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv1 := makePrivKey("my secret 1")
addr1 := priv1.PubKey().Address()
priv2 := makePrivKey("my secret 2")
addr2 := priv2.PubKey().Address()
// fund accounts
app.accountKeeper.AddCoins(app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
acc := app.accountMapper.NewAccountWithAddress(app.deliverState.ctx, addr2)
app.accountMapper.SetAccount(app.deliverState.ctx, acc)
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr1), "Balance1 did not update")
sendMsg := testSendMsg{addr1, addr2, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
msg1 := testBurnMsg{addr1, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
msg2 := testBurnMsg{addr2, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
// send then burn
tx := GenTx(t.Name(), []sdk.Msg{sendMsg, msg2, msg1}, []int64{0, 1}, []int64{0, 0}, priv1, priv2)
res := app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), app.accountKeeper.GetCoins(app.deliverState.ctx, addr1), "Balance1 did not change after valid tx")
require.Equal(t, sdk.Coins(nil), app.accountKeeper.GetCoins(app.deliverState.ctx, addr2), "Balance2 did not change after valid tx")
// Check that state is only updated if all msgs in tx pass.
app.accountKeeper.AddCoins(app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(50)}})
// burn then send
tx = GenTx(t.Name(), []sdk.Msg{msg1, sendMsg}, []int64{0}, []int64{1}, priv1)
res = app.Deliver(tx)
// Double check that state is correct after Commit.
app.EndBlock(abci.RequestEndBlock{})
app.Commit()
app.BeginBlock(abci.RequestBeginBlock{})
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
require.Equal(t, sdk.ABCICodeType(0x1000a), res.Code, "Allowed tx to pass with insufficient funds")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(50)}}, app.accountKeeper.GetCoins(app.deliverState.ctx, addr1), "Allowed valid msg to pass in invalid tx")
require.Equal(t, sdk.Coins(nil), app.accountKeeper.GetCoins(app.deliverState.ctx, addr2), "Balance2 changed after invalid tx")
}
//----------------------------------------
func randPower() int64 {
return cmn.RandInt64()
}

352
baseapp/multimsg_test.go Normal file
View File

@ -0,0 +1,352 @@
package baseapp
import (
"encoding/json"
"fmt"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
// tests multiple msgs of same type from same address in single tx
func TestMultipleBurn(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountMapper)).
AddRoute("send", newHandleSpend(app.accountMapper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv := makePrivKey("my secret")
addr := priv.PubKey().Address()
addCoins(app.accountMapper, app.deliverState.ctx, addr, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountMapper.GetAccount(app.deliverState.ctx, addr).GetCoins(), "Balance did not update")
msg := testBurnMsg{addr, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
tx := GenTx(t.Name(), []sdk.Msg{msg, msg}, []int64{0}, []int64{0}, priv)
res := app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr), "Double burn did not work")
}
// tests multiples msgs of same type from different addresses in single tx
func TestBurnMultipleOwners(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountMapper)).
AddRoute("send", newHandleSpend(app.accountMapper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv1 := makePrivKey("my secret 1")
addr1 := priv1.PubKey().Address()
priv2 := makePrivKey("my secret 2")
addr2 := priv2.PubKey().Address()
// fund accounts
addCoins(app.accountMapper, app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
addCoins(app.accountMapper, app.deliverState.ctx, addr2, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not update")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 did not update")
msg1 := testBurnMsg{addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}}}
msg2 := testBurnMsg{addr2, sdk.Coins{{"foocoin", sdk.NewInt(100)}}}
// test wrong signers: Address 1 signs both messages
tx := GenTx(t.Name(), []sdk.Msg{msg1, msg2}, []int64{0, 0}, []int64{0, 0}, priv1, priv1)
res := app.Deliver(tx)
require.Equal(t, sdk.ABCICodeType(0x10003), res.Code, "Wrong signatures passed")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 changed after invalid sig")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 changed after invalid sig")
// test valid tx
tx = GenTx(t.Name(), []sdk.Msg{msg1, msg2}, []int64{0, 1}, []int64{1, 0}, priv1, priv2)
res = app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not change after valid tx")
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 did not change after valid tx")
}
func getCoins(am auth.AccountMapper, ctx sdk.Context, addr sdk.Address) sdk.Coins {
return am.GetAccount(ctx, addr).GetCoins()
}
func addCoins(am auth.AccountMapper, ctx sdk.Context, addr sdk.Address, coins sdk.Coins) sdk.Error {
acc := am.GetAccount(ctx, addr)
if acc == nil {
acc = am.NewAccountWithAddress(ctx, addr)
}
err := acc.SetCoins(acc.GetCoins().Plus(coins))
if err != nil {
fmt.Println(err)
return sdk.ErrInternal(err.Error())
}
am.SetAccount(ctx, acc)
return nil
}
// tests different msg types in single tx with different addresses
func TestSendBurn(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountMapper)).
AddRoute("send", newHandleSpend(app.accountMapper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv1 := makePrivKey("my secret 1")
addr1 := priv1.PubKey().Address()
priv2 := makePrivKey("my secret 2")
addr2 := priv2.PubKey().Address()
// fund accounts
addCoins(app.accountMapper, app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
acc := app.accountMapper.NewAccountWithAddress(app.deliverState.ctx, addr2)
app.accountMapper.SetAccount(app.deliverState.ctx, acc)
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not update")
sendMsg := testSendMsg{addr1, addr2, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
msg1 := testBurnMsg{addr1, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
msg2 := testBurnMsg{addr2, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
// send then burn
tx := GenTx(t.Name(), []sdk.Msg{sendMsg, msg2, msg1}, []int64{0, 1}, []int64{0, 0}, priv1, priv2)
res := app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not change after valid tx")
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 did not change after valid tx")
// Check that state is only updated if all msgs in tx pass.
addCoins(app.accountMapper, app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(50)}})
// burn then send, with fee thats greater than individual tx, but less than combination
tx = GenTxWithFeeAmt(50000, t.Name(), []sdk.Msg{msg1, sendMsg}, []int64{0}, []int64{1}, priv1)
res = app.Deliver(tx)
require.Equal(t, sdk.ABCICodeType(0x1000c), res.Code, "Allowed tx to pass with insufficient funds")
// Double check that state is correct after Commit.
app.EndBlock(abci.RequestEndBlock{})
app.Commit()
app.BeginBlock(abci.RequestBeginBlock{})
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(50)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Allowed valid msg to pass in invalid tx")
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 changed after invalid tx")
}
// Use burn and send msg types to test multiple msgs in one tx
type testBurnMsg struct {
Addr sdk.Address
Amount sdk.Coins
}
const msgType3 = "burn"
func (msg testBurnMsg) Type() string { return msgType3 }
func (msg testBurnMsg) GetSignBytes() []byte {
bz, _ := json.Marshal(msg)
return sdk.MustSortJSON(bz)
}
func (msg testBurnMsg) ValidateBasic() sdk.Error {
if msg.Addr == nil {
return sdk.ErrInvalidAddress("Cannot use nil as Address")
}
return nil
}
func (msg testBurnMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Addr}
}
type testSendMsg struct {
Sender sdk.Address
Receiver sdk.Address
Amount sdk.Coins
}
const msgType4 = "send"
func (msg testSendMsg) Type() string { return msgType4 }
func (msg testSendMsg) GetSignBytes() []byte {
bz, _ := json.Marshal(msg)
return sdk.MustSortJSON(bz)
}
func (msg testSendMsg) ValidateBasic() sdk.Error {
if msg.Sender == nil || msg.Receiver == nil {
return sdk.ErrInvalidAddress("Cannot use nil as Address")
}
return nil
}
func (msg testSendMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Sender}
}
// Simple Handlers for burn and send
func newHandleBurn(am auth.AccountMapper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
ctx.GasMeter().ConsumeGas(20000, "burning coins")
burnMsg := msg.(testBurnMsg)
err := addCoins(am, ctx, burnMsg.Addr, burnMsg.Amount.Negative())
if err != nil {
return err.Result()
}
return sdk.Result{}
}
}
func newHandleSpend(am auth.AccountMapper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
ctx.GasMeter().ConsumeGas(40000, "spending coins")
spendMsg := msg.(testSendMsg)
err := addCoins(am, ctx, spendMsg.Sender, spendMsg.Amount.Negative())
if err != nil {
return err.Result()
}
err = addCoins(am, ctx, spendMsg.Receiver, spendMsg.Amount)
if err != nil {
return err.Result()
}
return sdk.Result{}
}
}
// generate a signed transaction
func GenTx(chainID string, msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
return GenTxWithFeeAmt(100000, chainID, msgs, accnums, seq, priv...)
}
// generate a signed transaction with the given fee amount
func GenTxWithFeeAmt(feeAmt int64, chainID string, msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
// make the transaction free
fee := auth.StdFee{
sdk.Coins{{"foocoin", sdk.NewInt(0)}},
feeAmt,
}
sigs := make([]auth.StdSignature, len(priv))
for i, p := range priv {
sig, err := p.Sign(auth.StdSignBytes(chainID, accnums[i], seq[i], fee, msgs, ""))
// TODO: replace with proper error handling:
if err != nil {
panic(err)
}
sigs[i] = auth.StdSignature{
PubKey: p.PubKey(),
Signature: sig,
AccountNumber: accnums[i],
Sequence: seq[i],
}
}
return auth.NewStdTx(msgs, fee, sigs, "")
}
// spin up simple app for testing
type testApp struct {
*BaseApp
accountMapper auth.AccountMapper
}
func newTestApp(name string) testApp {
return testApp{
BaseApp: newBaseApp(name),
}
}
func MakeCodec() *wire.Codec {
cdc := wire.NewCodec()
cdc.RegisterInterface((*sdk.Msg)(nil), nil)
crypto.RegisterAmino(cdc)
cdc.RegisterInterface((*auth.Account)(nil), nil)
cdc.RegisterConcrete(&auth.BaseAccount{}, "cosmos-sdk/BaseAccount", nil)
cdc.Seal()
return cdc
}

View File

@ -5,6 +5,8 @@ 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"
@ -81,17 +83,19 @@ func getMockApp(t *testing.T) *mock.App {
return mapp
}
// getBenchmarkMockApp initializes a mock application for this module, for purposes of benchmarking
// Any long term API support commitments do not apply to this function.
func getBenchmarkMockApp() (*mock.App, error) {
mapp := mock.NewApp()
func TestBankWithRandomMessages(t *testing.T) {
mapp := getMockApp(t)
setup := func(r *rand.Rand, keys []crypto.PrivKey) {
return
}
RegisterWire(mapp.Cdc)
coinKeeper := NewKeeper(mapp.AccountMapper)
mapp.Router().AddRoute("bank", NewHandler(coinKeeper))
err := mapp.CompleteSetup([]*sdk.KVStoreKey{})
return mapp, err
mapp.RandomizedTesting(
t,
[]mock.TestAndRunTx{TestAndRunSingleInputMsgSend},
[]mock.RandSetup{setup},
[]mock.Invariant{ModuleInvariants},
100, 30, 30,
)
}
func TestMsgSendWithAccounts(t *testing.T) {

View File

@ -10,6 +10,19 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
)
// getBenchmarkMockApp initializes a mock application for this module, for purposes of benchmarking
// Any long term API support commitments do not apply to this function.
func getBenchmarkMockApp() (*mock.App, error) {
mapp := mock.NewApp()
RegisterWire(mapp.Cdc)
coinKeeper := NewKeeper(mapp.AccountMapper)
mapp.Router().AddRoute("bank", NewHandler(coinKeeper))
err := mapp.CompleteSetup([]*sdk.KVStoreKey{})
return mapp, err
}
func BenchmarkOneBankSendTxPerBlock(b *testing.B) {
benchmarkApp, _ := getBenchmarkMockApp()

150
x/bank/test_helpers.go Normal file
View File

@ -0,0 +1,150 @@
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 := 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 := 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

@ -1,6 +1,7 @@
package mock
import (
"math/rand"
"os"
bam "github.com/cosmos/cosmos-sdk/baseapp"
@ -15,7 +16,9 @@ import (
const chainID = ""
// App extends an ABCI application.
// App extends an ABCI application, but with most of its parameters exported.
// They are exported for convenience in creating helper functions, as object
// capabilities aren't needed for testing.
type App struct {
*bam.BaseApp
Cdc *wire.Codec // Cdc is public since the codec is passed into the module anyways
@ -26,7 +29,8 @@ type App struct {
AccountMapper auth.AccountMapper
FeeCollectionKeeper auth.FeeCollectionKeeper
GenesisAccounts []auth.Account
GenesisAccounts []auth.Account
TotalCoinsSupply sdk.Coins
}
// NewApp partially constructs a new app on the memstore for module and genesis
@ -43,10 +47,11 @@ func NewApp() *App {
// Create your application object
app := &App{
BaseApp: bam.NewBaseApp("mock", cdc, logger, db),
Cdc: cdc,
KeyMain: sdk.NewKVStoreKey("main"),
KeyAccount: sdk.NewKVStoreKey("acc"),
BaseApp: bam.NewBaseApp("mock", cdc, logger, db),
Cdc: cdc,
KeyMain: sdk.NewKVStoreKey("main"),
KeyAccount: sdk.NewKVStoreKey("acc"),
TotalCoinsSupply: sdk.Coins{},
}
// Define the accountMapper
@ -124,8 +129,8 @@ func SetGenesis(app *App, accs []auth.Account) {
func GenTx(msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
// Make the transaction free
fee := auth.StdFee{
sdk.Coins{sdk.NewCoin("foocoin", 0)},
100000,
Amount: sdk.Coins{sdk.NewCoin("foocoin", 0)},
Gas: 100000,
}
sigs := make([]auth.StdSignature, len(priv))
@ -148,6 +153,70 @@ func GenTx(msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey)
return auth.NewStdTx(msgs, fee, sigs, memo)
}
// GeneratePrivKeys generates a total n Ed25519 private keys.
func GeneratePrivKeys(n int) (keys []crypto.PrivKey) {
// TODO: Randomize this between ed25519 and secp256k1
keys = make([]crypto.PrivKey, n, n)
for i := 0; i < n; i++ {
keys[i] = crypto.GenPrivKeyEd25519()
}
return
}
// GeneratePrivKeyAddressPairs generates a total of n private key, address
// pairs.
func GeneratePrivKeyAddressPairs(n int) (keys []crypto.PrivKey, addrs []sdk.Address) {
keys = make([]crypto.PrivKey, n, n)
addrs = make([]sdk.Address, n, n)
for i := 0; i < n; i++ {
keys[i] = crypto.GenPrivKeyEd25519()
addrs[i] = keys[i].PubKey().Address()
}
return
}
// RandomSetGenesis set genesis accounts with random coin values using the
// provided addresses and coin denominations.
func RandomSetGenesis(r *rand.Rand, app *App, addrs []sdk.Address, denoms []string) {
accts := make([]auth.Account, len(addrs), len(addrs))
randCoinIntervals := []BigInterval{
{sdk.NewIntWithDecimal(1, 0), sdk.NewIntWithDecimal(1, 1)},
{sdk.NewIntWithDecimal(1, 2), sdk.NewIntWithDecimal(1, 3)},
{sdk.NewIntWithDecimal(1, 40), sdk.NewIntWithDecimal(1, 50)},
}
for i := 0; i < len(accts); i++ {
coins := make([]sdk.Coin, len(denoms), len(denoms))
// generate a random coin for each denomination
for j := 0; j < len(denoms); j++ {
coins[j] = sdk.Coin{Denom: denoms[j],
Amount: RandFromBigInterval(r, randCoinIntervals),
}
}
app.TotalCoinsSupply = app.TotalCoinsSupply.Plus(coins)
baseAcc := auth.NewBaseAccountWithAddress(addrs[i])
(&baseAcc).SetCoins(coins)
accts[i] = &baseAcc
}
SetGenesis(app, accts)
}
// GetAllAccounts returns all accounts in the accountMapper.
func GetAllAccounts(mapper auth.AccountMapper, ctx sdk.Context) []auth.Account {
accounts := []auth.Account{}
appendAccount := func(acc auth.Account) (stop bool) {
accounts = append(accounts, acc)
return false
}
mapper.IterateAccounts(ctx, appendAccount)
return accounts
}
// GenSequenceOfTxs generates a set of signed transactions of messages, such
// that they differ only by having the sequence numbers incremented between
// every transaction.

15
x/mock/doc.go Normal file
View File

@ -0,0 +1,15 @@
/*
Package mock provides functions for creating applications for testing.
This module also features randomized testing, so that various modules can test
that their operations are interoperable.
The intended method of using this randomized testing framework is that every
module provides TestAndRunTx methods for each of its desired methods of fuzzing
its own txs, and it also provides the invariants that it assumes to be true.
You then pick and choose from these tx types and invariants. To pick and choose
these, you first build a mock app with the correct keepers. Then you call the
app.RandomizedTesting method with the set of desired txs, invariants, along
with the setups each module requires.
*/
package mock

View File

@ -0,0 +1,95 @@
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
}

38
x/mock/types.go Normal file
View File

@ -0,0 +1,38 @@
package mock
import (
"math/rand"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/crypto"
)
type (
// TestAndRunTx produces a fuzzed transaction, and ensures the state
// 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,
) (action string, err sdk.Error)
// RandSetup performs the random setup the mock module needs.
RandSetup func(r *rand.Rand, privKeys []crypto.PrivKey)
// 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)
)
// 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) {
if int(app.LastBlockHeight())%period == offset {
invariant(t, app, log)
}
}
}