diff --git a/x/wasm/keeper/keeper_test.go b/x/wasm/keeper/keeper_test.go index f48c9ba660..5cc9c35193 100644 --- a/x/wasm/keeper/keeper_test.go +++ b/x/wasm/keeper/keeper_test.go @@ -48,6 +48,9 @@ import ( //go:embed testdata/hackatom.wasm var hackatomWasm []byte +//go:embed testdata/replier.wasm +var replierWasm []byte + var AvailableCapabilities = []string{ "iterator", "staking", "stargate", "cosmwasm_1_1", "cosmwasm_1_2", "cosmwasm_1_3", "cosmwasm_1_4", "cosmwasm_2_0", "cosmwasm_2_1", "cosmwasm_2_2", @@ -2174,6 +2177,176 @@ func TestReply(t *testing.T) { } } +type replierExecMsg struct { + MsgId byte `json:"msg_id"` + SetDataInExecAndReply bool `json:"set_data_in_exec_and_reply"` + ReturnOrderInReply bool `json:"return_order_in_reply"` + ExecError bool `json:"exec_error"` + ReplyError bool `json:"reply_error"` + ReplyOnNever bool `json:"reply_on_never"` + Messages []replierExecMsg `json:"messages"` +} + +func defaultRepliesMsgTemplate() replierExecMsg { + return replierExecMsg{ + MsgId: 1, + SetDataInExecAndReply: true, + ReturnOrderInReply: false, + ExecError: false, + ReplyError: false, + ReplyOnNever: false, + Messages: []replierExecMsg{ + { + MsgId: 2, + SetDataInExecAndReply: true, + ReturnOrderInReply: false, + ExecError: false, + ReplyError: false, + ReplyOnNever: false, + Messages: []replierExecMsg{ + { + MsgId: 3, + SetDataInExecAndReply: true, + ReturnOrderInReply: false, + ExecError: false, + ReplyError: false, + ReplyOnNever: false, + Messages: []replierExecMsg{}, + }, + }, + }, + { + MsgId: 4, + SetDataInExecAndReply: true, + ReturnOrderInReply: false, + ExecError: false, + ReplyError: false, + ReplyOnNever: false, + Messages: []replierExecMsg{ + { + MsgId: 5, + SetDataInExecAndReply: true, + ReturnOrderInReply: false, + ExecError: false, + ReplyError: false, + ReplyOnNever: false, + Messages: []replierExecMsg{}, + }, + }, + }, + }, + } +} + +func repliesMsgTemplateReturnOrder() replierExecMsg { + repliesMsgTemplate := defaultRepliesMsgTemplate() + repliesMsgTemplate.ReturnOrderInReply = true + return repliesMsgTemplate +} + +func repliesMsgTemplateReplyNever() replierExecMsg { + repliesMsgTemplate := defaultRepliesMsgTemplate() + repliesMsgTemplate.Messages[1].ReplyOnNever = true + return repliesMsgTemplate +} + +func repliesMsgTemplateNoDataInResp() replierExecMsg { + repliesMsgTemplate := defaultRepliesMsgTemplate() + repliesMsgTemplate.Messages[1].SetDataInExecAndReply = false + return repliesMsgTemplate +} + +func repliesMsgTemplateExecError() replierExecMsg { + repliesMsgTemplate := defaultRepliesMsgTemplate() + repliesMsgTemplate.Messages[0].Messages[0].ExecError = true + repliesMsgTemplate.ReturnOrderInReply = true + return repliesMsgTemplate +} + +func repliesMsgTemplateReplyError() replierExecMsg { + repliesMsgTemplate := defaultRepliesMsgTemplate() + repliesMsgTemplate.Messages[0].ReplyError = true + repliesMsgTemplate.ReturnOrderInReply = true + return repliesMsgTemplate +} + +var repliesTestScenarios = []struct { + name string + in replierExecMsg + out []byte +}{ + { + "Assert the depth-first order of message handling", + repliesMsgTemplateReturnOrder(), + []byte{0xee, 0x1, 0xee, 0x2, 0xee, 0x3, 0xbb, 0x2, 0xbb, 0x1, 0xee, 0x4, 0xee, 0x5, 0xbb, 0x4, 0xbb, 0x1}, + }, + { + "Assert that with a list of submessages the `data` field will be set by the last submessage", + defaultRepliesMsgTemplate(), + []byte{0xa, 0x6, 0xa, 0x2, 0xee, 0x5, 0xbb, 0x4, 0xbb, 0x1}, + }, + { + "Assert that with a list of submessages the `data` field will be set by the last submessage that overrides it.", + repliesMsgTemplateReplyNever(), + []byte{0xa, 0x6, 0xa, 0x2, 0xee, 0x3, 0xbb, 0x2, 0xbb, 0x1}, + }, + + // Assert that in scenario C1 -> C4 -> C5 if C4 doesn't set `data`, + // the `data` set by C5 **is not forwarded** to the result of C1. + { + "Check data field forwarding", + repliesMsgTemplateNoDataInResp(), + []byte{0xbb, 0x1}, + }, + + // In this example we have the following scenario: + // `C1 -> C2 -> C3 -> reply(C2) -> reply(C1) -> C4 -> C5 -> reply(C4) -> reply(C1)`. + // The `C3` contract returns an error that is handled by reply entrypoint of `C2`. + // It means that the changes done by `C3` are reverted, but the rest of the changes are kept. + { + "Check error handling when execute fails", + repliesMsgTemplateExecError(), + []byte{0xee, 0x1, 0xee, 0x2, 0xbb, 0x2, 0xbb, 0x1, 0xee, 0x4, 0xee, 0x5, 0xbb, 0x4, 0xbb, 0x1}, + }, + + // The `C2` contract returns an error in reply entry-point that is handled by reply entrypoint of `C1`. + // It means that the changes done by either `C2` and `C3` are reverted, but the rest of the changes are kept. + { + "Check error handling when reply fails", + repliesMsgTemplateReplyError(), + []byte{0xee, 0x1, 0xbb, 0x1, 0xee, 0x4, 0xee, 0x5, 0xbb, 0x4, 0xbb, 0x1}, + }, +} + +func TestMultipleReplies(t *testing.T) { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + _, keeper, _ := keepers.AccountKeeper, keepers.ContractKeeper, keepers.BankKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := DeterministicAccountAddress(t, 1) + keepers.Faucet.Fund(ctx, creator, deposit.Add(deposit...)...) + creatorAddr := RandomAccountAddress(t) + + contractID, _, err := keeper.Create(ctx, creator, replierWasm, nil) + require.NoError(t, err) + + require.NoError(t, err) + addr, _, err := keepers.ContractKeeper.Instantiate(ctx, contractID, creator, nil, []byte("{}"), "demo contract replier", deposit) + require.NoError(t, err) + + for _, tt := range repliesTestScenarios { + t.Run(tt.name, func(t *testing.T) { + execMsg, err := json.Marshal(tt.in) + require.NoError(t, err) + em := sdk.NewEventManager() + res, err := keepers.ContractKeeper.Execute(ctx.WithEventManager(em), addr, creatorAddr, execMsg, nil) + require.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, tt.out, res) + }) + } +} + func TestQueryIsolation(t *testing.T) { ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) k := keepers.WasmKeeper diff --git a/x/wasm/keeper/testdata/replier.wasm b/x/wasm/keeper/testdata/replier.wasm new file mode 100644 index 0000000000..1f515f76ec Binary files /dev/null and b/x/wasm/keeper/testdata/replier.wasm differ