Merge pull request #22 from cosmwasm/improve_contract_queries_12

Improve wasm contract queries
This commit is contained in:
Ethan Frey
2020-01-15 11:53:29 +01:00
committed by GitHub
9 changed files with 382 additions and 42 deletions

View File

@@ -37,7 +37,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
## [Unreleased]
### Features
* (wasmd)[\#2](https://github.com/cosmwasm/wasmd/pull/22) Improve wasm contract queries (all, raw, smart)
* (wasmd) [\#119](https://github.com/cosmwasm/wasmd/pull/119) Add support for the `--inter-block-cache` CLI
flag and configuration.
* (wasmcli) [\#132](https://github.com/cosmwasm/wasmd/pull/132) Add `tx decode` command to decode

View File

@@ -95,7 +95,7 @@ sleep 3
wasmcli query wasm list-contracts
CONTRACT=cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5
wasmcli query wasm contract $CONTRACT
wasmcli query wasm contract-state $CONTRACT
wasmcli query wasm contract-state all $CONTRACT
wasmcli query account $CONTRACT
# execute fails if wrong person

1
go.mod
View File

@@ -19,6 +19,7 @@ require (
github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.5.0
github.com/stretchr/testify v1.4.0
github.com/tendermint/go-amino v0.15.1

View File

@@ -1,11 +1,16 @@
package cli
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"strconv"
flag "github.com/spf13/pflag"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
@@ -145,10 +150,27 @@ func GetCmdGetContractInfo(cdc *codec.Codec) *cobra.Command {
// GetCmdGetContractState dumps full internal state of a given contract
func GetCmdGetContractState(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "contract-state",
Short: "Querying commands for the wasm module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
cmd.AddCommand(client.GetCommands(
GetCmdGetContractStateAll(cdc),
GetCmdGetContractStateRaw(cdc),
GetCmdGetContractStateSmart(cdc),
)...)
return cmd
}
func GetCmdGetContractStateAll(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "contract-state [bech32_address]",
Short: "Prints out internal state of a contract given its address",
Long: "Prints out internal state of a contract given its address",
Use: "all [bech32_address]",
Short: "Prints out all internal state of a contract given its address",
Long: "Prints out all internal state of a contract given its address",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
@@ -158,7 +180,7 @@ func GetCmdGetContractState(cdc *codec.Codec) *cobra.Command {
return err
}
route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String())
route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateAll)
res, _, err := cliCtx.Query(route)
if err != nil {
return err
@@ -168,3 +190,114 @@ func GetCmdGetContractState(cdc *codec.Codec) *cobra.Command {
},
}
}
func GetCmdGetContractStateRaw(cdc *codec.Codec) *cobra.Command {
decoder := newArgDecoder(hex.DecodeString)
cmd := &cobra.Command{
Use: "raw [bech32_address] [key]",
Short: "Prints out internal state for key of a contract given its address",
Long: "Prints out internal state for of a contract given its address",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
addr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
queryData, err := decoder.DecodeString(args[1])
if err != nil {
return err
}
route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateRaw)
res, _, err := cliCtx.QueryWithData(route, queryData)
if err != nil {
return err
}
fmt.Println(string(res))
return nil
},
}
decoder.RegisterFlags(cmd.PersistentFlags(), "key argument")
return cmd
}
func GetCmdGetContractStateSmart(cdc *codec.Codec) *cobra.Command {
decoder := newArgDecoder(asciiDecodeString)
cmd := &cobra.Command{
Use: "smart [bech32_address] [query]",
Short: "Calls contract with given address with query data and prints the returned result",
Long: "Calls contract with given address with query data and prints the returned result",
Args: cobra.ExactArgs(2),
RunE: func(_ *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
addr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
key := args[1]
if key == "" {
return errors.New("key must not be empty")
}
route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateSmart)
queryData, err := decoder.DecodeString(args[1])
if err != nil {
return fmt.Errorf("decode query: %s", err)
}
res, _, err := cliCtx.QueryWithData(route, queryData)
if err != nil {
return err
}
fmt.Println(string(res))
return nil
},
}
decoder.RegisterFlags(cmd.PersistentFlags(), "query argument")
return cmd
}
type argumentDecoder struct {
// dec is the default decoder
dec func(string) ([]byte, error)
asciiF, hexF, b64F bool
}
func newArgDecoder(def func(string) ([]byte, error)) *argumentDecoder {
return &argumentDecoder{dec: def}
}
func (a *argumentDecoder) RegisterFlags(f *flag.FlagSet, argName string) {
f.BoolVar(&a.asciiF, "ascii", false, "ascii encoded "+argName)
f.BoolVar(&a.hexF, "hex", false, "hex encoded "+argName)
f.BoolVar(&a.b64F, "b64", false, "base64 encoded "+argName)
}
func (a *argumentDecoder) DecodeString(s string) ([]byte, error) {
found := -1
for i, v := range []*bool{&a.asciiF, &a.hexF, &a.b64F} {
if !*v {
continue
}
if found != -1 {
return nil, errors.New("multiple decoding flags used")
}
found = i
}
switch found {
case 0:
return asciiDecodeString(s)
case 1:
return hex.DecodeString(s)
case 2:
return base64.StdEncoding.DecodeString(s)
default:
return a.dec(s)
}
}
func asciiDecodeString(s string) ([]byte, error) {
return []byte(s), nil
}

View File

@@ -27,6 +27,8 @@ const GasMultiplier = 100
// MaxGas for a contract is 900 million (enforced in rust)
const MaxGas = 900_000_000
const smartQueryGasLimit = 3000000 // Todo: should be set by app.toml
// Keeper will have a reference to Wasmer with it's own data directory.
type Keeper struct {
storeKey sdk.StoreKey
@@ -37,6 +39,8 @@ type Keeper struct {
router sdk.Router
wasmer wasm.Wasmer
// queryGasLimit is the max wasm gas that can be spent on executing a query with a contract
queryGasLimit uint64
}
// NewKeeper creates a new contract Keeper instance
@@ -53,6 +57,7 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, accountKeeper auth.Accou
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
router: router,
queryGasLimit: smartQueryGasLimit,
}
}
@@ -132,22 +137,10 @@ func (k Keeper) Instantiate(ctx sdk.Context, creator sdk.AccAddress, codeID uint
// Execute executes the contract instance
func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, coins sdk.Coins, msgs []byte) (sdk.Result, sdk.Error) {
store := ctx.KVStore(k.storeKey)
contractBz := store.Get(types.GetContractAddressKey(contractAddress))
if contractBz == nil {
return sdk.Result{}, types.ErrNotFound("contract")
codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress)
if err != nil {
return sdk.Result{}, err
}
var contract types.ContractInfo
k.cdc.MustUnmarshalBinaryBare(contractBz, &contract)
contractInfoBz := store.Get(types.GetCodeKey(contract.CodeID))
if contractInfoBz == nil {
return sdk.Result{}, types.ErrNotFound("contract info")
}
var codeInfo types.CodeInfo
k.cdc.MustUnmarshalBinaryBare(contractInfoBz, &codeInfo)
// add more funds
sdkerr := k.bankKeeper.SendCoins(ctx, caller, contractAddress, coins)
if sdkerr != nil {
@@ -156,13 +149,10 @@ func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
contractAccount := k.accountKeeper.GetAccount(ctx, contractAddress)
params := types.NewParams(ctx, caller, coins, contractAccount)
prefixStoreKey := types.GetContractStorePrefixKey(contractAddress)
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey)
gas := gasForContract(ctx)
res, err := k.wasmer.Execute(codeInfo.CodeHash, params, msgs, prefixStore, gas)
if err != nil {
return sdk.Result{}, types.ErrExecuteFailed(err)
res, execErr := k.wasmer.Execute(codeInfo.CodeHash, params, msgs, prefixStore, gas)
if execErr != nil {
return sdk.Result{}, types.ErrExecuteFailed(execErr)
}
consumeGas(ctx, res.GasUsed)
@@ -174,6 +164,68 @@ func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
return types.CosmosResult(*res), nil
}
// QuerySmart queries the smart contract itself.
func (k Keeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]types.Model, sdk.Error) {
ctx = ctx.WithGasMeter(sdk.NewGasMeter(k.queryGasLimit))
codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr)
if err != nil {
return nil, err
}
queryResult, gasUsed, qErr := k.wasmer.Query(codeInfo.CodeHash, req, prefixStore, gasForContract(ctx))
if qErr != nil {
return nil, types.ErrExecuteFailed(qErr)
}
consumeGas(ctx, gasUsed)
models := make([]types.Model, len(queryResult.Results))
for i := range queryResult.Results {
models[i] = types.Model{
Key: queryResult.Results[i].Key,
Value: string(queryResult.Results[i].Value),
}
}
return models, nil
}
// QueryRaw returns the contract's state for give key. For a `nil` key a empty slice` result is returned.
func (k Keeper) QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []types.Model {
result := make([]types.Model, 0)
if key == nil {
return result
}
prefixStoreKey := types.GetContractStorePrefixKey(contractAddress)
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey)
if val := prefixStore.Get(key); val != nil {
return append(result, types.Model{
Key: string(key),
Value: string(val),
})
}
return result
}
func (k Keeper) contractInstance(ctx sdk.Context, contractAddress sdk.AccAddress) (types.CodeInfo, prefix.Store, sdk.Error) {
store := ctx.KVStore(k.storeKey)
contractBz := store.Get(types.GetContractAddressKey(contractAddress))
if contractBz == nil {
return types.CodeInfo{}, prefix.Store{}, types.ErrNotFound("contract")
}
var contract types.ContractInfo
k.cdc.MustUnmarshalBinaryBare(contractBz, &contract)
contractInfoBz := store.Get(types.GetCodeKey(contract.CodeID))
if contractInfoBz == nil {
return types.CodeInfo{}, prefix.Store{}, types.ErrNotFound("contract info")
}
var codeInfo types.CodeInfo
k.cdc.MustUnmarshalBinaryBare(contractInfoBz, &codeInfo)
prefixStoreKey := types.GetContractStorePrefixKey(contractAddress)
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey)
return codeInfo, prefixStore, nil
}
func (k Keeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo {
store := ctx.KVStore(k.storeKey)
var contract types.ContractInfo

View File

@@ -20,6 +20,12 @@ const (
QueryListCode = "list-code"
)
const (
QueryMethodContractStateSmart = "smart"
QueryMethodContractStateAll = "all"
QueryMethodContractStateRaw = "raw"
)
// NewQuerier creates a new querier
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, sdk.Error) {
@@ -29,7 +35,10 @@ func NewQuerier(keeper Keeper) sdk.Querier {
case QueryListContracts:
return queryContractList(ctx, req, keeper)
case QueryGetContractState:
return queryContractState(ctx, path[1], req, keeper)
if len(path) < 3 {
return nil, sdk.ErrUnknownRequest("unknown data query endpoint")
}
return queryContractState(ctx, path[1], path[2], req, keeper)
case QueryGetCode:
return queryCode(ctx, path[1], req, keeper)
case QueryListCode:
@@ -67,23 +76,36 @@ func queryContractList(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([
return bz, nil
}
func queryContractState(ctx sdk.Context, bech string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
addr, err := sdk.AccAddressFromBech32(bech)
func queryContractState(ctx sdk.Context, bech, queryMethod string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
contractAddr, err := sdk.AccAddressFromBech32(bech)
if err != nil {
return nil, sdk.ErrUnknownRequest(err.Error())
}
iter := keeper.GetContractState(ctx, addr)
var state []types.Model
for ; iter.Valid(); iter.Next() {
m := types.Model{
Key: string(iter.Key()),
Value: string(iter.Value()),
var resultData []types.Model
switch queryMethod {
case QueryMethodContractStateAll:
for iter := keeper.GetContractState(ctx, contractAddr); iter.Valid(); iter.Next() {
resultData = append(resultData, types.Model{
Key: string(iter.Key()),
Value: string(iter.Value()),
})
}
state = append(state, m)
if resultData == nil {
resultData = make([]types.Model, 0)
}
case QueryMethodContractStateRaw:
resultData = keeper.QueryRaw(ctx, contractAddr, req.Data)
case QueryMethodContractStateSmart:
res, err := keeper.QuerySmart(ctx, contractAddr, req.Data)
if err != nil {
return nil, err
}
resultData = res
default:
return nil, sdk.ErrUnknownRequest("unsupported data query method for contract-state")
}
bz, err := json.MarshalIndent(state, "", " ")
bz, err := json.MarshalIndent(resultData, "", " ")
if err != nil {
return nil, sdk.ErrUnknownRequest(err.Error())
}

View File

@@ -0,0 +1,131 @@
package keeper
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmwasm/wasmd/x/wasm/internal/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
)
func TestQueryContractState(t *testing.T) {
type model struct {
Key string `json:"key"`
Value string `json:"val"`
}
tempDir, err := ioutil.TempDir("", "wasm")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir)
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000))
topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))
creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit))
anyAddr := createFakeFundedAccount(ctx, accKeeper, topUp)
wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm")
require.NoError(t, err)
contractID, err := keeper.Create(ctx, creator, wasmCode)
require.NoError(t, err)
_, _, bob := keyPubAddr()
initMsg := InitMsg{
Verifier: anyAddr.String(),
Beneficiary: bob.String(),
}
initMsgBz, err := json.Marshal(initMsg)
require.NoError(t, err)
addr, err := keeper.Instantiate(ctx, creator, contractID, initMsgBz, deposit)
require.NoError(t, err)
require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String())
contractModel := []types.Model{
{Key: "foo", Value: "bar"},
{Key: string([]byte{0x0, 0x1}), Value: string([]byte{0x2, 0x3})},
}
keeper.setContractState(ctx, addr, contractModel)
q := NewQuerier(keeper)
specs := map[string]struct {
srcPath []string
srcReq abci.RequestQuery
expModelLen int
expModelContains []model
expErr sdk.Error
}{
"query all": {
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateAll},
expModelLen: 3,
expModelContains: []model{
{Key: "foo", Value: "bar"},
{Key: string([]byte{0x0, 0x1}), Value: string([]byte{0x2, 0x3})},
},
},
"query raw key": {
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
srcReq: abci.RequestQuery{Data: []byte("foo")},
expModelLen: 1,
expModelContains: []model{{Key: "foo", Value: "bar"}},
},
"query raw binary key": {
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
srcReq: abci.RequestQuery{Data: []byte{0x0, 0x1}},
expModelLen: 1,
expModelContains: []model{{Key: string([]byte{0x0, 0x1}), Value: string([]byte{0x2, 0x3})}},
},
"query smart": {
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateSmart},
srcReq: abci.RequestQuery{Data: []byte(`{"raw":{"key":"config"}}`)},
expModelLen: 1,
//expModelContains: []model{}, // stopping here as contract internals are not stable
},
"query unknown raw key": {
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
srcReq: abci.RequestQuery{Data: []byte("unknown")},
expModelLen: 0,
},
"query empty raw key": {
srcPath: []string{QueryGetContractState, addr.String(), QueryMethodContractStateRaw},
expModelLen: 0,
},
"query raw with unknown address": {
srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateRaw},
expModelLen: 0,
},
"query all with unknown address": {
srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateAll},
expModelLen: 0,
},
"query smart with unknown address": {
srcPath: []string{QueryGetContractState, anyAddr.String(), QueryMethodContractStateSmart},
expModelLen: 0,
expErr: types.ErrNotFound("contract"),
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
binResult, err := q(ctx, spec.srcPath, spec.srcReq)
require.Equal(t, spec.expErr, err)
// then
var r []model
if spec.expErr == nil {
require.NoError(t, json.Unmarshal(binResult, &r))
require.NotNil(t, r)
}
require.Len(t, r, spec.expModelLen)
// and in result set
for _, v := range spec.expModelContains {
assert.Contains(t, r, v)
}
})
}
}

View File

@@ -9,7 +9,7 @@ import (
// Model is a struct that holds a KV pair
type Model struct {
Key string `json:"key"`
Value string `json:"value"`
Value string `json:"val"`
}
// CodeInfo is data for the uploaded contract WASM code

View File

@@ -7,6 +7,7 @@ import (
"os"
"testing"
"github.com/cosmwasm/wasmd/x/wasm/internal/keeper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -395,11 +396,11 @@ func assertContractList(t *testing.T, q sdk.Querier, ctx sdk.Context, addrs []st
type model struct {
Key string `json:"key"`
Value string `json:"value"`
Value string `json:"val"`
}
func assertContractState(t *testing.T, q sdk.Querier, ctx sdk.Context, addr sdk.AccAddress, expected state) {
path := []string{QueryGetContractState, addr.String()}
path := []string{QueryGetContractState, addr.String(), keeper.QueryMethodContractStateAll}
bz, sdkerr := q(ctx, path, abci.RequestQuery{})
require.NoError(t, sdkerr)