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:
parent
86d7b8f981
commit
6f8f222ef6
|
@ -74,6 +74,9 @@ FEATURES
|
||||||
* [gaiacli] Ledger support added
|
* [gaiacli] Ledger support added
|
||||||
- You can now use a Ledger with `gaiacli --ledger` for all key-related commands
|
- 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
|
- 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:
|
* [gaiacli] added the following flags for commands that post transactions to the chain:
|
||||||
* async -- send the tx without waiting for a tendermint response
|
* async -- send the tx without waiting for a tendermint response
|
||||||
* json -- return the output in json format for increased readability
|
* json -- return the output in json format for increased readability
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package baseapp
|
package baseapp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -19,7 +18,6 @@ import (
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
"github.com/cosmos/cosmos-sdk/wire"
|
"github.com/cosmos/cosmos-sdk/wire"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||||
"github.com/cosmos/cosmos-sdk/x/bank"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultLogger() log.Logger {
|
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 {
|
func randPower() int64 {
|
||||||
return cmn.RandInt64()
|
return cmn.RandInt64()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||||
"github.com/cosmos/cosmos-sdk/x/mock"
|
"github.com/cosmos/cosmos-sdk/x/mock"
|
||||||
|
@ -81,17 +83,19 @@ func getMockApp(t *testing.T) *mock.App {
|
||||||
return mapp
|
return mapp
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBenchmarkMockApp initializes a mock application for this module, for purposes of benchmarking
|
func TestBankWithRandomMessages(t *testing.T) {
|
||||||
// Any long term API support commitments do not apply to this function.
|
mapp := getMockApp(t)
|
||||||
func getBenchmarkMockApp() (*mock.App, error) {
|
setup := func(r *rand.Rand, keys []crypto.PrivKey) {
|
||||||
mapp := mock.NewApp()
|
return
|
||||||
|
}
|
||||||
|
|
||||||
RegisterWire(mapp.Cdc)
|
mapp.RandomizedTesting(
|
||||||
coinKeeper := NewKeeper(mapp.AccountMapper)
|
t,
|
||||||
mapp.Router().AddRoute("bank", NewHandler(coinKeeper))
|
[]mock.TestAndRunTx{TestAndRunSingleInputMsgSend},
|
||||||
|
[]mock.RandSetup{setup},
|
||||||
err := mapp.CompleteSetup([]*sdk.KVStoreKey{})
|
[]mock.Invariant{ModuleInvariants},
|
||||||
return mapp, err
|
100, 30, 30,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMsgSendWithAccounts(t *testing.T) {
|
func TestMsgSendWithAccounts(t *testing.T) {
|
||||||
|
|
|
@ -10,6 +10,19 @@ import (
|
||||||
abci "github.com/tendermint/tendermint/abci/types"
|
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) {
|
func BenchmarkOneBankSendTxPerBlock(b *testing.B) {
|
||||||
benchmarkApp, _ := getBenchmarkMockApp()
|
benchmarkApp, _ := getBenchmarkMockApp()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package mock
|
package mock
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
bam "github.com/cosmos/cosmos-sdk/baseapp"
|
bam "github.com/cosmos/cosmos-sdk/baseapp"
|
||||||
|
@ -15,7 +16,9 @@ import (
|
||||||
|
|
||||||
const chainID = ""
|
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 {
|
type App struct {
|
||||||
*bam.BaseApp
|
*bam.BaseApp
|
||||||
Cdc *wire.Codec // Cdc is public since the codec is passed into the module anyways
|
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
|
AccountMapper auth.AccountMapper
|
||||||
FeeCollectionKeeper auth.FeeCollectionKeeper
|
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
|
// NewApp partially constructs a new app on the memstore for module and genesis
|
||||||
|
@ -43,10 +47,11 @@ func NewApp() *App {
|
||||||
|
|
||||||
// Create your application object
|
// Create your application object
|
||||||
app := &App{
|
app := &App{
|
||||||
BaseApp: bam.NewBaseApp("mock", cdc, logger, db),
|
BaseApp: bam.NewBaseApp("mock", cdc, logger, db),
|
||||||
Cdc: cdc,
|
Cdc: cdc,
|
||||||
KeyMain: sdk.NewKVStoreKey("main"),
|
KeyMain: sdk.NewKVStoreKey("main"),
|
||||||
KeyAccount: sdk.NewKVStoreKey("acc"),
|
KeyAccount: sdk.NewKVStoreKey("acc"),
|
||||||
|
TotalCoinsSupply: sdk.Coins{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the accountMapper
|
// 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 {
|
func GenTx(msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
|
||||||
// Make the transaction free
|
// Make the transaction free
|
||||||
fee := auth.StdFee{
|
fee := auth.StdFee{
|
||||||
sdk.Coins{sdk.NewCoin("foocoin", 0)},
|
Amount: sdk.Coins{sdk.NewCoin("foocoin", 0)},
|
||||||
100000,
|
Gas: 100000,
|
||||||
}
|
}
|
||||||
|
|
||||||
sigs := make([]auth.StdSignature, len(priv))
|
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)
|
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
|
// GenSequenceOfTxs generates a set of signed transactions of messages, such
|
||||||
// that they differ only by having the sequence numbers incremented between
|
// that they differ only by having the sequence numbers incremented between
|
||||||
// every transaction.
|
// every transaction.
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue