Submessage reply can overwrite caller response (#502)

* Reply may overwrite result data

* Fix interface name

* Refacting for tests

* Test response handler

* Fix naked error
This commit is contained in:
Alexander Peters
2021-04-27 14:00:42 +02:00
committed by GitHub
parent c67cf14db1
commit 305f13cc0a
10 changed files with 664 additions and 207 deletions

View File

@@ -14,7 +14,6 @@ import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/libs/log"
"path/filepath"
@@ -51,33 +50,41 @@ type Option interface {
}
// WasmVMQueryHandler is an extension point for custom query handler implementations
type WASMVMQueryHandler interface {
type WasmVMQueryHandler interface {
// HandleQuery executes the requested query
HandleQuery(ctx sdk.Context, caller sdk.AccAddress, request wasmvmtypes.QueryRequest) ([]byte, error)
}
// Messenger is an extension point for custom wasmVM message handling
type Messenger interface {
// DispatchMsg encodes the wasmVM message and dispatches it.
DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error)
}
type CoinTransferrer interface {
// TransferCoins sends the coin amounts from the source to the destination with rules applied.
TransferCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
}
// WasmVMResponseHandler is an extension point to handles the response data returned by a contract call.
type WasmVMResponseHandler interface {
// Handle processes the data returned by a contract invocation.
Handle(
ctx sdk.Context,
contractAddr sdk.AccAddress,
ibcPort string,
submessages []wasmvmtypes.SubMsg,
messages []wasmvmtypes.CosmosMsg,
origRspData []byte,
) ([]byte, error)
}
// Keeper will have a reference to Wasmer with it's own data directory.
type Keeper struct {
storeKey sdk.StoreKey
cdc codec.Marshaler
accountKeeper types.AccountKeeper
bank CoinTransferrer
portKeeper types.PortKeeper
capabilityKeeper types.CapabilityKeeper
wasmVM types.WasmerEngine
wasmVMQueryHandler WASMVMQueryHandler
messenger Messenger
storeKey sdk.StoreKey
cdc codec.Marshaler
accountKeeper types.AccountKeeper
bank CoinTransferrer
portKeeper types.PortKeeper
capabilityKeeper types.CapabilityKeeper
wasmVM types.WasmerEngine
wasmVMQueryHandler WasmVMQueryHandler
wasmVMResponseHandler WasmVMResponseHandler
messenger Messenger
// queryGasLimit is the max wasmvm gas that can be spent on executing a query with a contract
queryGasLimit uint64
paramSpace paramtypes.Subspace
@@ -113,7 +120,7 @@ func NewKeeper(
paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable())
}
keeper := Keeper{
keeper := &Keeper{
storeKey: storeKey,
cdc: cdc,
wasmVM: wasmer,
@@ -125,11 +132,14 @@ func NewKeeper(
queryGasLimit: wasmConfig.SmartQueryGasLimit,
paramSpace: paramSpace,
}
keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, queryRouter, &keeper)
keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, queryRouter, keeper)
for _, o := range opts {
o.apply(&keeper)
o.apply(keeper)
}
return keeper
// not updateable, yet
keeper.wasmVMResponseHandler = NewDefaultWasmVMContractResponseHandler(NewMessageDispatcher(keeper.messenger, keeper))
return *keeper
}
func (k Keeper) getUploadAccessConfig(ctx sdk.Context) types.AccessConfig {
@@ -302,12 +312,12 @@ func (k Keeper) instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.A
k.storeContractInfo(ctx, contractAddress, &contractInfo)
// dispatch submessages then messages
err = k.dispatchAll(ctx, contractAddress, contractInfo.IBCPortID, res.Submessages, res.Messages)
data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res)
if err != nil {
return nil, nil, sdkerrors.Wrap(err, "dispatch")
}
return contractAddress, res.Data, nil
return contractAddress, data, nil
}
// Execute executes the contract instance
@@ -346,13 +356,13 @@ func (k Keeper) execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller
ctx.EventManager().EmitEvents(events)
// dispatch submessages then messages
err = k.dispatchAll(ctx, contractAddress, contractInfo.IBCPortID, res.Submessages, res.Messages)
data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res)
if err != nil {
return nil, sdkerrors.Wrap(err, "dispatch")
}
return &sdk.Result{
Data: res.Data,
Data: data,
}, nil
}
@@ -418,13 +428,13 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller
k.storeContractInfo(ctx, contractAddress, contractInfo)
// dispatch submessages then messages
err = k.dispatchAll(ctx, contractAddress, contractInfo.IBCPortID, res.Submessages, res.Messages)
data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res)
if err != nil {
return nil, sdkerrors.Wrap(err, "dispatch")
}
return &sdk.Result{
Data: res.Data,
Data: data,
}, nil
}
@@ -458,13 +468,13 @@ func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte
ctx.EventManager().EmitEvents(events)
// dispatch submessages then messages
err = k.dispatchAll(ctx, contractAddress, contractInfo.IBCPortID, res.Submessages, res.Messages)
data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res)
if err != nil {
return nil, sdkerrors.Wrap(err, "dispatch")
}
return &sdk.Result{
Data: res.Data,
Data: data,
}, nil
}
@@ -500,13 +510,12 @@ func (k Keeper) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply was
ctx.EventManager().EmitEvents(events)
// dispatch submessages then messages
err = k.dispatchAll(ctx, contractAddress, contractInfo.IBCPortID, res.Submessages, res.Messages)
data, err := k.handleContractResponse(ctx, contractAddress, contractInfo.IBCPortID, res)
if err != nil {
return nil, sdkerrors.Wrap(err, "dispatch")
}
return &sdk.Result{
Data: res.Data,
Data: data,
}, nil
}
@@ -803,146 +812,9 @@ func (k Keeper) setContractInfoExtension(ctx sdk.Context, contractAddr sdk.AccAd
return nil
}
func (k Keeper) dispatchAll(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, subMsgs []wasmvmtypes.SubMsg, msgs []wasmvmtypes.CosmosMsg) error {
// first dispatch all submessages (and the replies).
err := k.dispatchSubmessages(ctx, contractAddr, ibcPort, subMsgs)
if err != nil {
return err
}
// then dispatch all the normal messages
return k.dispatchMessages(ctx, contractAddr, ibcPort, msgs)
}
func (k Keeper) dispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
for _, msg := range msgs {
events, _, err := k.messenger.DispatchMsg(ctx, contractAddr, ibcPort, msg)
if err != nil {
return err
}
// redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler)
ctx.EventManager().EmitEvents(events)
}
return nil
}
func (k Keeper) dispatchMsgWithGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msg wasmvmtypes.CosmosMsg, gasLimit uint64) (events []sdk.Event, data [][]byte, err error) {
limitedMeter := sdk.NewGasMeter(gasLimit)
subCtx := ctx.WithGasMeter(limitedMeter)
// catch out of gas panic and just charge the entire gas limit
defer func() {
if r := recover(); r != nil {
// if it's not an OutOfGas error, raise it again
if _, ok := r.(sdk.ErrorOutOfGas); !ok {
// log it to get the original stack trace somewhere (as panic(r) keeps message but stacktrace to here
k.Logger(ctx).Info("SubMsg rethrowing panic: %#v", r)
panic(r)
}
ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic")
err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit")
}
}()
events, data, err = k.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg)
// make sure we charge the parent what was spent
spent := subCtx.GasMeter().GasConsumed()
ctx.GasMeter().ConsumeGas(spent, "From limited Sub-Message")
return events, data, err
}
// dispatchSubmessages builds a sandbox to execute these messages and returns the execution result to the contract
// that dispatched them, both on success as well as failure
func (k Keeper) dispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) error {
for _, msg := range msgs {
// first, we build a sub-context which we can use inside the submessages
subCtx, commit := ctx.CacheContext()
// check how much gas left locally, optionally wrap the gas meter
gasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumed()
limitGas := msg.GasLimit != nil && (*msg.GasLimit < gasRemaining)
var err error
var events []sdk.Event
var data [][]byte
if limitGas {
events, data, err = k.dispatchMsgWithGasLimit(subCtx, contractAddr, ibcPort, msg.Msg, *msg.GasLimit)
} else {
events, data, err = k.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg.Msg)
}
// if it succeeds, commit state changes from submessage, and pass on events to Event Manager
if err == nil {
commit()
ctx.EventManager().EmitEvents(events)
}
// on failure, revert state from sandbox, and ignore events (just skip doing the above)
// we only callback if requested. Short-circuit here the two cases we don't want to
if msg.ReplyOn == wasmvmtypes.ReplySuccess && err != nil {
return err
}
if msg.ReplyOn == wasmvmtypes.ReplyError && err == nil {
return nil
}
// otherwise, we create a SubcallResult and pass it into the calling contract
var result wasmvmtypes.SubcallResult
if err == nil {
// just take the first one for now if there are multiple sub-sdk messages
// and safely return nothing if no data
var responseData []byte
if len(data) > 0 {
responseData = data[0]
}
result = wasmvmtypes.SubcallResult{
Ok: &wasmvmtypes.SubcallResponse{
Events: sdkEventsToWasmVmEvents(events),
Data: responseData,
},
}
} else {
result = wasmvmtypes.SubcallResult{
Err: err.Error(),
}
}
// now handle the reply, we use the parent context, and abort on error
reply := wasmvmtypes.Reply{
ID: msg.ID,
Result: result,
}
// we can ignore any result returned as there is nothing to do with the data
// and the events are already in the ctx.EventManager()
_, err = k.reply(ctx, contractAddr, reply)
if err != nil {
return err
}
}
return nil
}
func sdkEventsToWasmVmEvents(events []sdk.Event) []wasmvmtypes.Event {
res := make([]wasmvmtypes.Event, len(events))
for i, ev := range events {
res[i] = wasmvmtypes.Event{
Type: ev.Type,
Attributes: sdkAttributesToWasmVmAttributes(ev.Attributes),
}
}
return res
}
func sdkAttributesToWasmVmAttributes(attrs []abci.EventAttribute) []wasmvmtypes.EventAttribute {
res := make([]wasmvmtypes.EventAttribute, len(attrs))
for i, attr := range attrs {
res[i] = wasmvmtypes.EventAttribute{
Key: string(attr.Key),
Value: string(attr.Value),
}
}
return res
// handleContractResponse processes the contract response
func (k *Keeper) handleContractResponse(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, res *wasmvmtypes.Response) ([]byte, error) {
return k.wasmVMResponseHandler.Handle(ctx, contractAddr, ibcPort, res.Submessages, res.Messages, res.Data)
}
func gasForContract(ctx sdk.Context) uint64 {
@@ -1111,3 +983,39 @@ func (c BankCoinTransferrer) TransferCoins(ctx sdk.Context, fromAddr sdk.AccAddr
}
return nil
}
type msgDispatcher interface {
DispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error
DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error)
}
// DefaultWasmVMContractResponseHandler default implementation that first dispatches submessage then normal messages.
// The Submessage execution may include an success/failure response handling by the contract that can overwrite the
// original
type DefaultWasmVMContractResponseHandler struct {
md msgDispatcher
}
func NewDefaultWasmVMContractResponseHandler(md msgDispatcher) *DefaultWasmVMContractResponseHandler {
return &DefaultWasmVMContractResponseHandler{md: md}
}
// Handle processes the data returned by a contract invocation.
func (h DefaultWasmVMContractResponseHandler) Handle(
ctx sdk.Context,
contractAddr sdk.AccAddress,
ibcPort string,
submessages []wasmvmtypes.SubMsg,
messages []wasmvmtypes.CosmosMsg,
origRspData []byte,
) ([]byte, error) {
result := origRspData
switch rsp, err := h.md.DispatchSubmessages(ctx, contractAddr, ibcPort, submessages); {
case err != nil:
return nil, sdkerrors.Wrap(err, "submessages")
case rsp != nil:
result = rsp
}
// then dispatch all the normal messages
return result, sdkerrors.Wrap(h.md.DispatchMessages(ctx, contractAddr, ibcPort, messages), "messages")
}

View File

@@ -1386,3 +1386,90 @@ func TestInitializePinnedCodes(t *testing.T) {
assert.Equal(t, exp, capturedChecksums[i])
}
}
func TestNewDefaultWasmVMContractResponseHandler(t *testing.T) {
noopDMsgs := func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
return nil
}
specs := map[string]struct {
srcData []byte
setup func(m *wasmtesting.MockMsgDispatcher)
expErr bool
expData []byte
}{
"submessage overwrites result when set": {
srcData: []byte("otherData"),
setup: func(m *wasmtesting.MockMsgDispatcher) {
m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
return []byte("mySubMsgData"), nil
}
m.DispatchMessagesFn = noopDMsgs
},
expErr: false,
expData: []byte("mySubMsgData"),
},
"submessage overwrites result when empty": {
srcData: []byte("otherData"),
setup: func(m *wasmtesting.MockMsgDispatcher) {
m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
return []byte(""), nil
}
m.DispatchMessagesFn = noopDMsgs
},
expErr: false,
expData: []byte(""),
},
"submessage do not overwrite result when nil": {
srcData: []byte("otherData"),
setup: func(m *wasmtesting.MockMsgDispatcher) {
m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
return nil, nil
}
m.DispatchMessagesFn = noopDMsgs
},
expErr: false,
expData: []byte("otherData"),
},
"submessage error aborts process": {
setup: func(m *wasmtesting.MockMsgDispatcher) {
m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
return nil, errors.New("test - ignore")
}
},
expErr: true,
},
"message error aborts process": {
setup: func(m *wasmtesting.MockMsgDispatcher) {
m.DispatchSubmessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
return []byte("mySubMsgData"), nil
}
m.DispatchMessagesFn = func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
return errors.New("test - ignore")
}
},
expErr: true,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
var (
subMsgs []wasmvmtypes.SubMsg
msgs []wasmvmtypes.CosmosMsg
)
var mock wasmtesting.MockMsgDispatcher
spec.setup(&mock)
d := NewDefaultWasmVMContractResponseHandler(&mock)
// when
gotData, gotErr := d.Handle(sdk.Context{}, RandomAccountAddress(t), "ibc-port", subMsgs, msgs, spec.srcData)
if spec.expErr {
require.Error(t, gotErr)
return
}
require.NoError(t, gotErr)
assert.Equal(t, spec.expData, gotData)
})
}
}

View File

@@ -0,0 +1,174 @@
package keeper
import (
"github.com/CosmWasm/wasmd/x/wasm/types"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
abci "github.com/tendermint/tendermint/abci/types"
)
// Messenger is an extension point for custom wasmd message handling
type Messenger interface {
// DispatchMsg encodes the wasmVM message and dispatches it.
DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error)
}
// replyer is a subset of keeper that can handle replies to submessages
type replyer interface {
reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error)
}
// MessageDispatcher coordinates message sending and submessage reply/ state commits
type MessageDispatcher struct {
messenger Messenger
keeper replyer
}
// NewMessageDispatcher constructor
func NewMessageDispatcher(messenger Messenger, keeper replyer) *MessageDispatcher {
return &MessageDispatcher{messenger: messenger, keeper: keeper}
}
// DispatchMessages sends all messages.
func (d MessageDispatcher) DispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
for _, msg := range msgs {
events, _, err := d.messenger.DispatchMsg(ctx, contractAddr, ibcPort, msg)
if err != nil {
return err
}
// redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler)
ctx.EventManager().EmitEvents(events)
}
return nil
}
// dispatchMsgWithGasLimit sends a message with gas limit applied
func (d MessageDispatcher) dispatchMsgWithGasLimit(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msg wasmvmtypes.CosmosMsg, gasLimit uint64) (events []sdk.Event, data [][]byte, err error) {
limitedMeter := sdk.NewGasMeter(gasLimit)
subCtx := ctx.WithGasMeter(limitedMeter)
// catch out of gas panic and just charge the entire gas limit
defer func() {
if r := recover(); r != nil {
// if it's not an OutOfGas error, raise it again
if _, ok := r.(sdk.ErrorOutOfGas); !ok {
// log it to get the original stack trace somewhere (as panic(r) keeps message but stacktrace to here
moduleLogger(ctx).Info("SubMsg rethrowing panic: %#v", r)
panic(r)
}
ctx.GasMeter().ConsumeGas(gasLimit, "Sub-Message OutOfGas panic")
err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "SubMsg hit gas limit")
}
}()
events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg)
// make sure we charge the parent what was spent
spent := subCtx.GasMeter().GasConsumed()
ctx.GasMeter().ConsumeGas(spent, "From limited Sub-Message")
return events, data, err
}
// DispatchSubmessages builds a sandbox to execute these messages and returns the execution result to the contract
// that dispatched them, both on success as well as failure
func (d MessageDispatcher) DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
var rsp []byte
for _, msg := range msgs {
switch msg.ReplyOn {
case wasmvmtypes.ReplySuccess, wasmvmtypes.ReplyError, wasmvmtypes.ReplyAlways:
default:
return nil, sdkerrors.Wrap(types.ErrInvalid, "replyOn")
}
// first, we build a sub-context which we can use inside the submessages
subCtx, commit := ctx.CacheContext()
// check how much gas left locally, optionally wrap the gas meter
gasRemaining := ctx.GasMeter().Limit() - ctx.GasMeter().GasConsumed()
limitGas := msg.GasLimit != nil && (*msg.GasLimit < gasRemaining)
var err error
var events []sdk.Event
var data [][]byte
if limitGas {
events, data, err = d.dispatchMsgWithGasLimit(subCtx, contractAddr, ibcPort, msg.Msg, *msg.GasLimit)
} else {
events, data, err = d.messenger.DispatchMsg(subCtx, contractAddr, ibcPort, msg.Msg)
}
// if it succeeds, commit state changes from submessage, and pass on events to Event Manager
if err == nil {
commit()
ctx.EventManager().EmitEvents(events)
}
// on failure, revert state from sandbox, and ignore events (just skip doing the above)
// we only callback if requested. Short-circuit here the two cases we don't want to
if msg.ReplyOn == wasmvmtypes.ReplySuccess && err != nil {
return nil, err
}
if msg.ReplyOn == wasmvmtypes.ReplyError && err == nil {
continue
}
// otherwise, we create a SubcallResult and pass it into the calling contract
var result wasmvmtypes.SubcallResult
if err == nil {
// just take the first one for now if there are multiple sub-sdk messages
// and safely return nothing if no data
var responseData []byte
if len(data) > 0 {
responseData = data[0]
}
result = wasmvmtypes.SubcallResult{
Ok: &wasmvmtypes.SubcallResponse{
Events: sdkEventsToWasmVmEvents(events),
Data: responseData,
},
}
} else {
result = wasmvmtypes.SubcallResult{
Err: err.Error(),
}
}
// now handle the reply, we use the parent context, and abort on error
reply := wasmvmtypes.Reply{
ID: msg.ID,
Result: result,
}
// we can ignore any result returned as there is nothing to do with the data
// and the events are already in the ctx.EventManager()
rData, err := d.keeper.reply(ctx, contractAddr, reply)
switch {
case err != nil:
return nil, sdkerrors.Wrap(err, "reply")
case rData.Data != nil:
rsp = rData.Data
}
}
return rsp, nil
}
func sdkEventsToWasmVmEvents(events []sdk.Event) []wasmvmtypes.Event {
res := make([]wasmvmtypes.Event, len(events))
for i, ev := range events {
res[i] = wasmvmtypes.Event{
Type: ev.Type,
Attributes: sdkAttributesToWasmVmAttributes(ev.Attributes),
}
}
return res
}
func sdkAttributesToWasmVmAttributes(attrs []abci.EventAttribute) []wasmvmtypes.EventAttribute {
res := make([]wasmvmtypes.EventAttribute, len(attrs))
for i, attr := range attrs {
res[i] = wasmvmtypes.EventAttribute{
Key: string(attr.Key),
Value: string(attr.Value),
}
}
return res
}

View File

@@ -0,0 +1,236 @@
package keeper
import (
"errors"
"fmt"
"github.com/CosmWasm/wasmd/x/wasm/keeper/wasmtesting"
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"testing"
)
func TestDispatchSubmessages(t *testing.T) {
noReplyCalled := &mockReplyer{}
var anyGasLimit uint64 = 1
specs := map[string]struct {
msgs []wasmvmtypes.SubMsg
replyer *mockReplyer
msgHandler *wasmtesting.MockMessageHandler
expErr bool
expData []byte
expCommits []bool
expEvents sdk.Events
}{
"no reply on error without error": {
msgs: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplyError}},
replyer: noReplyCalled,
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, [][]byte{[]byte("myData")}, nil
},
},
expCommits: []bool{true},
},
"no reply on success without success": {
msgs: []wasmvmtypes.SubMsg{{ReplyOn: wasmvmtypes.ReplySuccess}},
replyer: noReplyCalled,
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, nil, errors.New("test, ignore")
},
},
expCommits: []bool{false},
expErr: true,
},
"reply on success - received": {
msgs: []wasmvmtypes.SubMsg{{
ReplyOn: wasmvmtypes.ReplySuccess,
}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
return &sdk.Result{Data: []byte("myReplyData")}, nil
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, [][]byte{[]byte("myData")}, nil
},
},
expData: []byte("myReplyData"),
expCommits: []bool{true},
},
"reply on error - handled": {
msgs: []wasmvmtypes.SubMsg{{
ReplyOn: wasmvmtypes.ReplyError,
}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
return &sdk.Result{Data: []byte("myReplyData")}, nil
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, nil, errors.New("my error")
},
},
expData: []byte("myReplyData"),
expCommits: []bool{false},
},
"with reply events": {
msgs: []wasmvmtypes.SubMsg{{
ReplyOn: wasmvmtypes.ReplySuccess,
}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
return &sdk.Result{Data: []byte("myReplyData")}, nil
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
myEvents := []sdk.Event{{Type: "myEvent", Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}}}}
return myEvents, [][]byte{[]byte("myData")}, nil
},
},
expData: []byte("myReplyData"),
expCommits: []bool{true},
expEvents: []sdk.Event{{
Type: "myEvent",
Attributes: []abci.EventAttribute{{Key: []byte("foo"), Value: []byte("bar")}},
}},
},
"reply returns error": {
msgs: []wasmvmtypes.SubMsg{{
ReplyOn: wasmvmtypes.ReplySuccess,
}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
return nil, errors.New("reply failed")
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, nil, nil
},
},
expCommits: []bool{false},
expErr: true,
},
"with gas limit - out of gas": {
msgs: []wasmvmtypes.SubMsg{{
GasLimit: &anyGasLimit,
ReplyOn: wasmvmtypes.ReplyError,
}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
return &sdk.Result{Data: []byte("myReplyData")}, nil
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
ctx.GasMeter().ConsumeGas(sdk.Gas(101), "testing")
return nil, [][]byte{[]byte("someData")}, nil
},
},
expData: []byte("myReplyData"),
expCommits: []bool{false},
},
"with gas limit - within limit no error": {
msgs: []wasmvmtypes.SubMsg{{
GasLimit: &anyGasLimit,
ReplyOn: wasmvmtypes.ReplyError,
}},
replyer: &mockReplyer{},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
ctx.GasMeter().ConsumeGas(sdk.Gas(1), "testing")
return nil, [][]byte{[]byte("someData")}, nil
},
},
expCommits: []bool{true},
},
"multiple msg - last reply": {
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
return &sdk.Result{Data: []byte(fmt.Sprintf("myReplyData:%d", reply.ID))}, nil
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, nil, errors.New("my error")
},
},
expData: []byte("myReplyData:2"),
expCommits: []bool{false, false},
},
"multiple msg - last reply with non nil": {
msgs: []wasmvmtypes.SubMsg{{ID: 1, ReplyOn: wasmvmtypes.ReplyError}, {ID: 2, ReplyOn: wasmvmtypes.ReplyError}},
replyer: &mockReplyer{
replyFn: func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
if reply.ID == 2 {
return &sdk.Result{}, nil
}
return &sdk.Result{Data: []byte("myReplyData:1")}, nil
},
},
msgHandler: &wasmtesting.MockMessageHandler{
DispatchMsgFn: func(ctx sdk.Context, contractAddr sdk.AccAddress, contractIBCPortID string, msg wasmvmtypes.CosmosMsg) (events []sdk.Event, data [][]byte, err error) {
return nil, nil, errors.New("my error")
},
},
expData: []byte("myReplyData:1"),
expCommits: []bool{false, false},
},
"empty replyOn rejected": {
msgs: []wasmvmtypes.SubMsg{{}},
replyer: noReplyCalled,
msgHandler: &wasmtesting.MockMessageHandler{},
expErr: true,
},
"invalid replyOn rejected": {
msgs: []wasmvmtypes.SubMsg{{ReplyOn: "invalid"}},
replyer: noReplyCalled,
msgHandler: &wasmtesting.MockMessageHandler{},
expCommits: []bool{false},
expErr: true,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
var mockStore wasmtesting.MockCommitMultiStore
em := sdk.NewEventManager()
ctx := sdk.Context{}.WithMultiStore(&mockStore).
WithGasMeter(sdk.NewGasMeter(100)).
WithEventManager(em)
d := NewMessageDispatcher(spec.msgHandler, spec.replyer)
gotData, gotErr := d.DispatchSubmessages(ctx, RandomAccountAddress(t), "any_port", spec.msgs)
if spec.expErr {
require.Error(t, gotErr)
return
} else {
require.NoError(t, gotErr)
assert.Equal(t, spec.expData, gotData)
}
assert.Equal(t, spec.expCommits, mockStore.Committed)
if len(spec.expEvents) == 0 {
assert.Empty(t, em.Events())
} else {
assert.Equal(t, spec.expEvents, em.Events())
}
})
}
}
type mockReplyer struct {
replyFn func(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error)
}
func (m mockReplyer) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply wasmvmtypes.Reply) (*sdk.Result, error) {
if m.replyFn == nil {
panic("not expected to be called")
}
return m.replyFn(ctx, contractAddress, reply)
}

View File

@@ -11,7 +11,7 @@ func (f optsFn) apply(keeper *Keeper) {
f(keeper)
}
// WithMessageHandler is an optional constructor parameter to replace the default wasmVM engine with the
// WithWasmEngine is an optional constructor parameter to replace the default wasmVM engine with the
// given one.
func WithWasmEngine(x types.WasmerEngine) Option {
return optsFn(func(k *Keeper) {
@@ -29,7 +29,7 @@ func WithMessageHandler(x Messenger) Option {
// WithQueryHandler is an optional constructor parameter to set custom query handler for wasmVM requests.
// This option should not be combined with Option `WithQueryPlugins`.
func WithQueryHandler(x WASMVMQueryHandler) Option {
func WithQueryHandler(x WasmVMQueryHandler) Option {
return optsFn(func(k *Keeper) {
k.wasmVMQueryHandler = x
})

View File

@@ -16,11 +16,11 @@ import (
type QueryHandler struct {
Ctx sdk.Context
Plugins WASMVMQueryHandler
Plugins WasmVMQueryHandler
Caller sdk.AccAddress
}
func NewQueryHandler(ctx sdk.Context, vmQueryHandler WASMVMQueryHandler, caller sdk.AccAddress) QueryHandler {
func NewQueryHandler(ctx sdk.Context, vmQueryHandler WasmVMQueryHandler, caller sdk.AccAddress) QueryHandler {
return QueryHandler{
Ctx: ctx,
Plugins: vmQueryHandler,

View File

@@ -71,7 +71,7 @@ func (k Keeper) OnConnectChannel(
events := types.ParseEvents(res.Attributes, contractAddr)
ctx.EventManager().EmitEvents(events)
if err := k.dispatchAll(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages); err != nil {
if _, err := k.wasmVMResponseHandler.Handle(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages, nil); err != nil {
return err
}
return nil
@@ -109,7 +109,7 @@ func (k Keeper) OnCloseChannel(
events := types.ParseEvents(res.Attributes, contractAddr)
ctx.EventManager().EmitEvents(events)
if err := k.dispatchAll(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages); err != nil {
if _, err := k.wasmVMResponseHandler.Handle(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages, nil); err != nil {
return err
}
return nil
@@ -145,11 +145,7 @@ func (k Keeper) OnRecvPacket(
// emit all events from this contract itself
events := types.ParseEvents(res.Attributes, contractAddr)
ctx.EventManager().EmitEvents(events)
if err := k.dispatchAll(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages); err != nil {
return nil, err
}
return res.Acknowledgement, nil
return k.wasmVMResponseHandler.Handle(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages, res.Acknowledgement)
}
// OnAckPacket calls the contract to handle the "acknowledgement" data which can contain success or failure of a packet
@@ -184,7 +180,7 @@ func (k Keeper) OnAckPacket(
events := types.ParseEvents(res.Attributes, contractAddr)
ctx.EventManager().EmitEvents(events)
if err := k.dispatchAll(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages); err != nil {
if _, err := k.wasmVMResponseHandler.Handle(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages, nil); err != nil {
return err
}
return nil
@@ -219,7 +215,7 @@ func (k Keeper) OnTimeoutPacket(
events := types.ParseEvents(res.Attributes, contractAddr)
ctx.EventManager().EmitEvents(events)
if err := k.dispatchAll(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages); err != nil {
if _, err := k.wasmVMResponseHandler.Handle(ctx, contractAddr, contractInfo.IBCPortID, res.Submessages, res.Messages, nil); err != nil {
return err
}
return nil

View File

@@ -15,7 +15,8 @@ import (
func TestOnOpenChannel(t *testing.T) {
var m wasmtesting.MockWasmer
wasmtesting.MakeIBCInstantiable(&m)
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures)
var messenger = &wasmtesting.MockMessageHandler{}
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures, WithMessageHandler(messenger))
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
specs := map[string]struct {
@@ -74,7 +75,8 @@ func TestOnOpenChannel(t *testing.T) {
func TestOnConnectChannel(t *testing.T) {
var m wasmtesting.MockWasmer
wasmtesting.MakeIBCInstantiable(&m)
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures)
var messenger = &wasmtesting.MockMessageHandler{}
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures, WithMessageHandler(messenger))
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
specs := map[string]struct {
@@ -82,7 +84,7 @@ func TestOnConnectChannel(t *testing.T) {
contractGas sdk.Gas
contractResp *wasmvmtypes.IBCBasicResponse
contractErr error
overwriteMessenger Messenger
overwriteMessenger *wasmtesting.MockMessageHandler
expErr bool
expContractEventAttrs int
expNoEvents bool
@@ -148,10 +150,9 @@ func TestOnConnectChannel(t *testing.T) {
defer cancel()
before := ctx.GasMeter().GasConsumed()
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
keepers.WasmKeeper.messenger = msger
*messenger = *msger
if spec.overwriteMessenger != nil {
keepers.WasmKeeper.messenger = spec.overwriteMessenger
*messenger = *spec.overwriteMessenger
}
// when
@@ -184,7 +185,8 @@ func TestOnConnectChannel(t *testing.T) {
func TestOnCloseChannel(t *testing.T) {
var m wasmtesting.MockWasmer
wasmtesting.MakeIBCInstantiable(&m)
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures)
var messenger = &wasmtesting.MockMessageHandler{}
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures, WithMessageHandler(messenger))
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
specs := map[string]struct {
@@ -192,7 +194,7 @@ func TestOnCloseChannel(t *testing.T) {
contractGas sdk.Gas
contractResp *wasmvmtypes.IBCBasicResponse
contractErr error
overwriteMessenger Messenger
overwriteMessenger *wasmtesting.MockMessageHandler
expErr bool
expContractEventAttrs int
expNoEvents bool
@@ -257,10 +259,10 @@ func TestOnCloseChannel(t *testing.T) {
defer cancel()
before := ctx.GasMeter().GasConsumed()
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
keepers.WasmKeeper.messenger = msger
*messenger = *msger
if spec.overwriteMessenger != nil {
keepers.WasmKeeper.messenger = spec.overwriteMessenger
*messenger = *spec.overwriteMessenger
}
// when
@@ -294,7 +296,8 @@ func TestOnCloseChannel(t *testing.T) {
func TestOnRecvPacket(t *testing.T) {
var m wasmtesting.MockWasmer
wasmtesting.MakeIBCInstantiable(&m)
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures)
var messenger = &wasmtesting.MockMessageHandler{}
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures, WithMessageHandler(messenger))
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
specs := map[string]struct {
@@ -302,7 +305,7 @@ func TestOnRecvPacket(t *testing.T) {
contractGas sdk.Gas
contractResp *wasmvmtypes.IBCReceiveResponse
contractErr error
overwriteMessenger Messenger
overwriteMessenger *wasmtesting.MockMessageHandler
expErr bool
expContractEventAttrs int
expNoEvents bool
@@ -380,10 +383,10 @@ func TestOnRecvPacket(t *testing.T) {
before := ctx.GasMeter().GasConsumed()
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
keepers.WasmKeeper.messenger = msger
*messenger = *msger
if spec.overwriteMessenger != nil {
keepers.WasmKeeper.messenger = spec.overwriteMessenger
*messenger = *spec.overwriteMessenger
}
// when
@@ -419,7 +422,8 @@ func TestOnRecvPacket(t *testing.T) {
func TestOnAckPacket(t *testing.T) {
var m wasmtesting.MockWasmer
wasmtesting.MakeIBCInstantiable(&m)
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures)
var messenger = &wasmtesting.MockMessageHandler{}
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures, WithMessageHandler(messenger))
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
specs := map[string]struct {
@@ -427,7 +431,7 @@ func TestOnAckPacket(t *testing.T) {
contractGas sdk.Gas
contractResp *wasmvmtypes.IBCBasicResponse
contractErr error
overwriteMessenger Messenger
overwriteMessenger *wasmtesting.MockMessageHandler
expErr bool
expContractEventAttrs int
expNoEvents bool
@@ -493,10 +497,10 @@ func TestOnAckPacket(t *testing.T) {
defer cancel()
before := ctx.GasMeter().GasConsumed()
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
keepers.WasmKeeper.messenger = msger
*messenger = *msger
if spec.overwriteMessenger != nil {
keepers.WasmKeeper.messenger = spec.overwriteMessenger
*messenger = *spec.overwriteMessenger
}
// when
@@ -530,7 +534,8 @@ func TestOnAckPacket(t *testing.T) {
func TestOnTimeoutPacket(t *testing.T) {
var m wasmtesting.MockWasmer
wasmtesting.MakeIBCInstantiable(&m)
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures)
var messenger = &wasmtesting.MockMessageHandler{}
parentCtx, keepers := CreateTestInput(t, false, SupportedFeatures, WithMessageHandler(messenger))
example := SeedNewContractInstance(t, parentCtx, keepers, &m)
specs := map[string]struct {
@@ -538,7 +543,7 @@ func TestOnTimeoutPacket(t *testing.T) {
contractGas sdk.Gas
contractResp *wasmvmtypes.IBCBasicResponse
contractErr error
overwriteMessenger Messenger
overwriteMessenger *wasmtesting.MockMessageHandler
expErr bool
expContractEventAttrs int
expNoEvents bool
@@ -603,10 +608,10 @@ func TestOnTimeoutPacket(t *testing.T) {
defer cancel()
before := ctx.GasMeter().GasConsumed()
msger, capturedMsgs := wasmtesting.NewCapturingMessageHandler()
keepers.WasmKeeper.messenger = msger
*messenger = *msger
if spec.overwriteMessenger != nil {
keepers.WasmKeeper.messenger = spec.overwriteMessenger
*messenger = *spec.overwriteMessenger
}
// when

View File

@@ -0,0 +1,25 @@
package wasmtesting
import (
wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type MockMsgDispatcher struct {
DispatchMessagesFn func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error
DispatchSubmessagesFn func(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error)
}
func (m MockMsgDispatcher) DispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.CosmosMsg) error {
if m.DispatchMessagesFn == nil {
panic("not expected to be called")
}
return m.DispatchMessagesFn(ctx, contractAddr, ibcPort, msgs)
}
func (m MockMsgDispatcher) DispatchSubmessages(ctx sdk.Context, contractAddr sdk.AccAddress, ibcPort string, msgs []wasmvmtypes.SubMsg) ([]byte, error) {
if m.DispatchSubmessagesFn == nil {
panic("not expected to be called")
}
return m.DispatchSubmessagesFn(ctx, contractAddr, ibcPort, msgs)
}

View File

@@ -0,0 +1,26 @@
package wasmtesting
import (
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// MockCommitMultiStore mock with a CacheMultiStore to capture commits
type MockCommitMultiStore struct {
sdk.CommitMultiStore
Committed []bool
}
func (m *MockCommitMultiStore) CacheMultiStore() storetypes.CacheMultiStore {
m.Committed = append(m.Committed, false)
return &mockCMS{m, &m.Committed[len(m.Committed)-1]}
}
type mockCMS struct {
sdk.CommitMultiStore
committed *bool
}
func (m *mockCMS) Write() {
*m.committed = true
}