Files
wasmd/x/wasm/module_test.go
Piotr Husiatyński b2fc913398 Keeper tests (#590)
* More keeper tests

x/wasm/keeper tests are extended to test various input validation.
Keeper input is validated before passing to the keeper method when used
within wasmd application. We cannot ensure such validation when this
keeper is used outside of wasmd application. To keep it safe, fully
validate keeper methods input.

hackatom.wasm is loaded into memory during initialization to avoid
reading file in each test separately. Once migrated to go 1.16, embed
package should be used instead.

Run goimport on certain files.

Some comments fixed or removed.

* ensure that creator address is not nil
2021-08-27 15:37:07 +02:00

581 lines
17 KiB
Go

package wasm
import (
"encoding/json"
"fmt"
"io/ioutil"
"testing"
"github.com/CosmWasm/wasmd/x/wasm/keeper"
"github.com/CosmWasm/wasmd/x/wasm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
"github.com/dvsekhvalnov/jose2go/base64url"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
)
type testData struct {
module module.AppModule
ctx sdk.Context
acctKeeper authkeeper.AccountKeeper
keeper Keeper
bankKeeper bankkeeper.Keeper
stakingKeeper stakingkeeper.Keeper
}
func setupTest(t *testing.T) testData {
ctx, keepers := CreateTestInput(t, false, "staking,stargate")
cdc := keeper.MakeTestCodec(t)
data := testData{
module: NewAppModule(cdc, keepers.WasmKeeper, keepers.StakingKeeper),
ctx: ctx,
acctKeeper: keepers.AccountKeeper,
keeper: *keepers.WasmKeeper,
bankKeeper: keepers.BankKeeper,
stakingKeeper: keepers.StakingKeeper,
}
return data
}
func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) {
key := ed25519.GenPrivKey()
pub := key.PubKey()
addr := sdk.AccAddress(pub.Address())
return key, pub, addr
}
func mustLoad(path string) []byte {
bz, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
return bz
}
var (
_, _, addrAcc1 = keyPubAddr()
addr1 = addrAcc1.String()
testContract = mustLoad("./keeper/testdata/hackatom.wasm")
maskContract = mustLoad("./keeper/testdata/reflect.wasm")
oldContract = mustLoad("./testdata/escrow_0.7.wasm")
)
func TestHandleCreate(t *testing.T) {
cases := map[string]struct {
msg sdk.Msg
isValid bool
}{
"empty": {
msg: &MsgStoreCode{},
isValid: false,
},
"invalid wasm": {
msg: &MsgStoreCode{
Sender: addr1,
WASMByteCode: []byte("foobar"),
},
isValid: false,
},
"valid wasm": {
msg: &MsgStoreCode{
Sender: addr1,
WASMByteCode: testContract,
},
isValid: true,
},
"other valid wasm": {
msg: &MsgStoreCode{
Sender: addr1,
WASMByteCode: maskContract,
},
isValid: true,
},
"old wasm (0.7)": {
msg: &MsgStoreCode{
Sender: addr1,
WASMByteCode: oldContract,
},
isValid: false,
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
data := setupTest(t)
h := data.module.Route().Handler()
q := data.module.LegacyQuerierHandler(nil)
res, err := h(data.ctx, tc.msg)
if !tc.isValid {
require.Error(t, err, "%#v", res)
assertCodeList(t, q, data.ctx, 0)
assertCodeBytes(t, q, data.ctx, 1, nil)
return
}
require.NoError(t, err)
assertCodeList(t, q, data.ctx, 1)
})
}
}
type initMsg struct {
Verifier sdk.AccAddress `json:"verifier"`
Beneficiary sdk.AccAddress `json:"beneficiary"`
}
type state struct {
Verifier string `json:"verifier"`
Beneficiary string `json:"beneficiary"`
Funder string `json:"funder"`
}
func TestHandleInstantiate(t *testing.T) {
data := setupTest(t)
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
creator := createFakeFundedAccount(t, data.ctx, data.acctKeeper, data.bankKeeper, deposit)
h := data.module.Route().Handler()
q := data.module.LegacyQuerierHandler(nil)
msg := &MsgStoreCode{
Sender: creator.String(),
WASMByteCode: testContract,
}
res, err := h(data.ctx, msg)
require.NoError(t, err)
assertStoreCodeResponse(t, res.Data, 1)
_, _, bob := keyPubAddr()
_, _, fred := keyPubAddr()
initMsg := initMsg{
Verifier: fred,
Beneficiary: bob,
}
initMsgBz, err := json.Marshal(initMsg)
require.NoError(t, err)
// create with no balance is also legal
initCmd := MsgInstantiateContract{
Sender: creator.String(),
CodeID: firstCodeID,
Msg: initMsgBz,
Funds: nil,
}
res, err = h(data.ctx, &initCmd)
require.NoError(t, err)
contractBech32Addr := parseInitResponse(t, res.Data)
require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhuc53mp6", contractBech32Addr)
// this should be standard x/wasm init event, nothing from contract
require.Equal(t, 3, len(res.Events), prettyEvents(res.Events))
require.Equal(t, "message", res.Events[0].Type)
assertAttribute(t, "module", "wasm", res.Events[0].Attributes[0])
require.Equal(t, "instantiate", res.Events[1].Type)
require.Equal(t, "wasm", res.Events[2].Type)
assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[2].Attributes[0])
assertCodeList(t, q, data.ctx, 1)
assertCodeBytes(t, q, data.ctx, 1, testContract)
assertContractList(t, q, data.ctx, 1, []string{contractBech32Addr})
assertContractInfo(t, q, data.ctx, contractBech32Addr, 1, creator)
assertContractState(t, q, data.ctx, contractBech32Addr, state{
Verifier: fred.String(),
Beneficiary: bob.String(),
Funder: creator.String(),
})
}
func TestHandleExecute(t *testing.T) {
data := setupTest(t)
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))
creator := createFakeFundedAccount(t, data.ctx, data.acctKeeper, data.bankKeeper, deposit.Add(deposit...))
fred := createFakeFundedAccount(t, data.ctx, data.acctKeeper, data.bankKeeper, topUp)
h := data.module.Route().Handler()
q := data.module.LegacyQuerierHandler(nil)
msg := &MsgStoreCode{
Sender: creator.String(),
WASMByteCode: testContract,
}
res, err := h(data.ctx, msg)
require.NoError(t, err)
assertStoreCodeResponse(t, res.Data, 1)
_, _, bob := keyPubAddr()
initMsg := initMsg{
Verifier: fred,
Beneficiary: bob,
}
initMsgBz, err := json.Marshal(initMsg)
require.NoError(t, err)
initCmd := MsgInstantiateContract{
Sender: creator.String(),
CodeID: firstCodeID,
Msg: initMsgBz,
Funds: deposit,
}
res, err = h(data.ctx, &initCmd)
require.NoError(t, err)
contractBech32Addr := parseInitResponse(t, res.Data)
require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhuc53mp6", contractBech32Addr)
// this should be standard x/wasm message event, init event, plus a bank send event (2), with no custom contract events
require.Equal(t, 4, len(res.Events), prettyEvents(res.Events))
require.Equal(t, "message", res.Events[0].Type)
assertAttribute(t, "module", "wasm", res.Events[0].Attributes[0])
require.Equal(t, "transfer", res.Events[1].Type)
require.Equal(t, "instantiate", res.Events[2].Type)
require.Equal(t, "wasm", res.Events[3].Type)
assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[3].Attributes[0])
// ensure bob doesn't exist
bobAcct := data.acctKeeper.GetAccount(data.ctx, bob)
require.Nil(t, bobAcct)
// ensure funder has reduced balance
creatorAcct := data.acctKeeper.GetAccount(data.ctx, creator)
require.NotNil(t, creatorAcct)
// we started at 2*deposit, should have spent one above
assert.Equal(t, deposit, data.bankKeeper.GetAllBalances(data.ctx, creatorAcct.GetAddress()))
// ensure contract has updated balance
contractAddr, _ := sdk.AccAddressFromBech32(contractBech32Addr)
contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr)
require.NotNil(t, contractAcct)
assert.Equal(t, deposit, data.bankKeeper.GetAllBalances(data.ctx, contractAcct.GetAddress()))
execCmd := MsgExecuteContract{
Sender: fred.String(),
Contract: contractBech32Addr,
Msg: []byte(`{"release":{}}`),
Funds: topUp,
}
res, err = h(data.ctx, &execCmd)
require.NoError(t, err)
// from https://github.com/CosmWasm/cosmwasm/blob/master/contracts/hackatom/src/contract.rs#L167
assertExecuteResponse(t, res.Data, []byte{0xf0, 0x0b, 0xaa})
// this should be standard message event, plus x/wasm init event, plus 2 bank send event, plus a special event from the contract
require.Equal(t, 6, len(res.Events), prettyEvents(res.Events))
assert.Equal(t, "message", res.Events[0].Type)
assertAttribute(t, "module", "wasm", res.Events[0].Attributes[0])
require.Equal(t, "transfer", res.Events[1].Type)
require.Len(t, res.Events[1].Attributes, 3)
assertAttribute(t, "recipient", contractBech32Addr, res.Events[1].Attributes[0])
assertAttribute(t, "sender", fred.String(), res.Events[1].Attributes[1])
assertAttribute(t, "amount", "5000denom", res.Events[1].Attributes[2])
assert.Equal(t, "execute", res.Events[2].Type)
// custom contract event attribute
assert.Equal(t, "wasm", res.Events[3].Type)
assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[3].Attributes[0])
assertAttribute(t, "action", "release", res.Events[3].Attributes[1])
// custom contract event
assert.Equal(t, "wasm-hackatom", res.Events[4].Type)
assertAttribute(t, "_contract_address", contractBech32Addr, res.Events[4].Attributes[0])
assertAttribute(t, "action", "release", res.Events[4].Attributes[1])
// second transfer (this without conflicting message)
assert.Equal(t, "transfer", res.Events[5].Type)
assertAttribute(t, "recipient", bob.String(), res.Events[5].Attributes[0])
assertAttribute(t, "sender", contractBech32Addr, res.Events[5].Attributes[1])
assertAttribute(t, "amount", "105000denom", res.Events[5].Attributes[2])
// finally, standard x/wasm tag
// ensure bob now exists and got both payments released
bobAcct = data.acctKeeper.GetAccount(data.ctx, bob)
require.NotNil(t, bobAcct)
balance := data.bankKeeper.GetAllBalances(data.ctx, bobAcct.GetAddress())
assert.Equal(t, deposit.Add(topUp...), balance)
// ensure contract has updated balance
contractAcct = data.acctKeeper.GetAccount(data.ctx, contractAddr)
require.NotNil(t, contractAcct)
assert.Equal(t, sdk.Coins(nil), data.bankKeeper.GetAllBalances(data.ctx, contractAcct.GetAddress()))
// ensure all contract state is as after init
assertCodeList(t, q, data.ctx, 1)
assertCodeBytes(t, q, data.ctx, 1, testContract)
assertContractList(t, q, data.ctx, 1, []string{contractBech32Addr})
assertContractInfo(t, q, data.ctx, contractBech32Addr, 1, creator)
assertContractState(t, q, data.ctx, contractBech32Addr, state{
Verifier: fred.String(),
Beneficiary: bob.String(),
Funder: creator.String(),
})
}
func TestHandleExecuteEscrow(t *testing.T) {
data := setupTest(t)
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))
creator := createFakeFundedAccount(t, data.ctx, data.acctKeeper, data.bankKeeper, deposit.Add(deposit...))
fred := createFakeFundedAccount(t, data.ctx, data.acctKeeper, data.bankKeeper, topUp)
h := data.module.Route().Handler()
msg := &MsgStoreCode{
Sender: creator.String(),
WASMByteCode: testContract,
}
res, err := h(data.ctx, msg)
require.NoError(t, err)
_, _, bob := keyPubAddr()
initMsg := map[string]interface{}{
"verifier": fred.String(),
"beneficiary": bob.String(),
}
initMsgBz, err := json.Marshal(initMsg)
require.NoError(t, err)
initCmd := MsgInstantiateContract{
Sender: creator.String(),
CodeID: firstCodeID,
Msg: initMsgBz,
Funds: deposit,
}
res, err = h(data.ctx, &initCmd)
require.NoError(t, err)
contractBech32Addr := parseInitResponse(t, res.Data)
require.Equal(t, "cosmos14hj2tavq8fpesdwxxcu44rty3hh90vhuc53mp6", contractBech32Addr)
handleMsg := map[string]interface{}{
"release": map[string]interface{}{},
}
handleMsgBz, err := json.Marshal(handleMsg)
require.NoError(t, err)
execCmd := MsgExecuteContract{
Sender: fred.String(),
Contract: contractBech32Addr,
Msg: handleMsgBz,
Funds: topUp,
}
res, err = h(data.ctx, &execCmd)
require.NoError(t, err)
// from https://github.com/CosmWasm/cosmwasm/blob/master/contracts/hackatom/src/contract.rs#L167
assertExecuteResponse(t, res.Data, []byte{0xf0, 0x0b, 0xaa})
// ensure bob now exists and got both payments released
bobAcct := data.acctKeeper.GetAccount(data.ctx, bob)
require.NotNil(t, bobAcct)
balance := data.bankKeeper.GetAllBalances(data.ctx, bobAcct.GetAddress())
assert.Equal(t, deposit.Add(topUp...), balance)
// ensure contract has updated balance
contractAddr, _ := sdk.AccAddressFromBech32(contractBech32Addr)
contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr)
require.NotNil(t, contractAcct)
assert.Equal(t, sdk.Coins(nil), data.bankKeeper.GetAllBalances(data.ctx, contractAcct.GetAddress()))
}
func TestReadWasmConfig(t *testing.T) {
defaults := DefaultWasmConfig()
specs := map[string]struct {
src AppOptionsMock
exp types.WasmConfig
}{
"set query gas limit via opts": {
src: AppOptionsMock{
"wasm.query_gas_limit": 1,
},
exp: types.WasmConfig{
SmartQueryGasLimit: 1,
MemoryCacheSize: defaults.MemoryCacheSize,
},
},
"set cache via opts": {
src: AppOptionsMock{
"wasm.memory_cache_size": 2,
},
exp: types.WasmConfig{
MemoryCacheSize: 2,
SmartQueryGasLimit: defaults.SmartQueryGasLimit,
},
},
"set debug via opts": {
src: AppOptionsMock{
"trace": true,
},
exp: types.WasmConfig{
SmartQueryGasLimit: defaults.SmartQueryGasLimit,
MemoryCacheSize: defaults.MemoryCacheSize,
ContractDebugMode: true,
},
},
"all defaults when no options set": {
exp: defaults,
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
got, err := ReadWasmConfig(spec.src)
require.NoError(t, err)
assert.Equal(t, spec.exp, got)
})
}
}
type AppOptionsMock map[string]interface{}
func (a AppOptionsMock) Get(s string) interface{} {
return a[s]
}
type prettyEvent struct {
Type string
Attr []sdk.Attribute
}
func prettyEvents(evts []abci.Event) string {
res := make([]prettyEvent, len(evts))
for i, e := range evts {
res[i] = prettyEvent{
Type: e.Type,
Attr: prettyAttrs(e.Attributes),
}
}
bz, err := json.MarshalIndent(res, "", " ")
if err != nil {
panic(err)
}
return string(bz)
}
func prettyAttrs(attrs []abci.EventAttribute) []sdk.Attribute {
pretty := make([]sdk.Attribute, len(attrs))
for i, a := range attrs {
pretty[i] = prettyAttr(a)
}
return pretty
}
func prettyAttr(attr abci.EventAttribute) sdk.Attribute {
return sdk.NewAttribute(string(attr.Key), string(attr.Value))
}
func assertAttribute(t *testing.T, key string, value string, attr abci.EventAttribute) {
t.Helper()
assert.Equal(t, key, string(attr.Key), prettyAttr(attr))
assert.Equal(t, value, string(attr.Value), prettyAttr(attr))
}
func assertCodeList(t *testing.T, q sdk.Querier, ctx sdk.Context, expectedNum int) {
bz, sdkerr := q(ctx, []string{QueryListCode}, abci.RequestQuery{})
require.NoError(t, sdkerr)
if len(bz) == 0 {
require.Equal(t, expectedNum, 0)
return
}
var res []CodeInfo
err := json.Unmarshal(bz, &res)
require.NoError(t, err)
assert.Equal(t, expectedNum, len(res))
}
func assertCodeBytes(t *testing.T, q sdk.Querier, ctx sdk.Context, codeID uint64, expectedBytes []byte) {
path := []string{QueryGetCode, fmt.Sprintf("%d", codeID)}
bz, sdkerr := q(ctx, path, abci.RequestQuery{})
require.NoError(t, sdkerr)
if len(expectedBytes) == 0 {
require.Equal(t, len(bz), 0, "%q", string(bz))
return
}
var res map[string]interface{}
err := json.Unmarshal(bz, &res)
require.NoError(t, err)
require.Contains(t, res, "data")
b, err := base64url.Decode(res["data"].(string))
require.NoError(t, err)
assert.Equal(t, expectedBytes, b)
assert.EqualValues(t, codeID, res["id"])
}
func assertContractList(t *testing.T, q sdk.Querier, ctx sdk.Context, codeID uint64, expContractAddrs []string) {
bz, sdkerr := q(ctx, []string{QueryListContractByCode, fmt.Sprintf("%d", codeID)}, abci.RequestQuery{})
require.NoError(t, sdkerr)
if len(bz) == 0 {
require.Equal(t, len(expContractAddrs), 0)
return
}
var res []string
err := json.Unmarshal(bz, &res)
require.NoError(t, err)
var hasAddrs = make([]string, len(res))
for i, r := range res {
hasAddrs[i] = r
}
assert.Equal(t, expContractAddrs, hasAddrs)
}
func assertContractState(t *testing.T, q sdk.Querier, ctx sdk.Context, contractBech32Addr string, expected state) {
t.Helper()
path := []string{QueryGetContractState, contractBech32Addr, keeper.QueryMethodContractStateAll}
bz, sdkerr := q(ctx, path, abci.RequestQuery{})
require.NoError(t, sdkerr)
var res []Model
err := json.Unmarshal(bz, &res)
require.NoError(t, err)
require.Equal(t, 1, len(res), "#v", res)
require.Equal(t, []byte("config"), []byte(res[0].Key))
expectedBz, err := json.Marshal(expected)
require.NoError(t, err)
assert.Equal(t, expectedBz, res[0].Value)
}
func assertContractInfo(t *testing.T, q sdk.Querier, ctx sdk.Context, contractBech32Addr string, codeID uint64, creator sdk.AccAddress) {
t.Helper()
path := []string{QueryGetContract, contractBech32Addr}
bz, sdkerr := q(ctx, path, abci.RequestQuery{})
require.NoError(t, sdkerr)
var res ContractInfo
err := json.Unmarshal(bz, &res)
require.NoError(t, err)
assert.Equal(t, codeID, res.CodeID)
assert.Equal(t, creator.String(), res.Creator)
}
func createFakeFundedAccount(t *testing.T, ctx sdk.Context, am authkeeper.AccountKeeper, bankKeeper bankkeeper.Keeper, coins sdk.Coins) sdk.AccAddress {
t.Helper()
_, _, addr := keyPubAddr()
acc := am.NewAccountWithAddress(ctx, addr)
am.SetAccount(ctx, acc)
require.NoError(t, bankKeeper.SetBalances(ctx, addr, coins))
return addr
}