diff --git a/CHANGELOG.md b/CHANGELOG.md index 49bbaee9ff..ebad92fea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * [\#35](https://github.com/Finschia/wasmd/pull/35) stop wrap twice the response of handling non-plus wasm message in plus handler ### Breaking Changes +* [\#41](https://github.com/Finschia/wasmd/pull/41) add `cosmwasmAPIGenerator` to keeper ### Build, CI diff --git a/app/app.go b/app/app.go index eeeac1f154..68e368ba9a 100644 --- a/app/app.go +++ b/app/app.go @@ -108,7 +108,6 @@ import ( "github.com/line/wasmd/x/wasm" wasmclient "github.com/line/wasmd/x/wasm/client" wasmkeeper "github.com/line/wasmd/x/wasm/keeper" - wasmpluskeeper "github.com/line/wasmd/x/wasmplus/keeper" // unnamed import of statik for swagger UI support _ "github.com/line/lbm-sdk/client/docs/statik" @@ -521,7 +520,6 @@ func NewWasmApp( wasmDir, wasmConfig, availableCapabilities, - &wasmpluskeeper.Keeper{}, wasmOpts..., ) diff --git a/x/wasm/dynamic_link_test.go b/x/wasm/dynamic_link_test.go new file mode 100644 index 0000000000..cde3521580 --- /dev/null +++ b/x/wasm/dynamic_link_test.go @@ -0,0 +1,481 @@ +package wasm + +import ( + "fmt" + "testing" + + "github.com/line/wasmd/x/wasm/keeper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + // These come from https://github.com/line/cosmwasm/tree/main/contracts. + // Hashes of them are in testdata directory. + calleeContract = mustLoad("./testdata/dynamic_callee_contract.wasm") + callerContract = mustLoad("./testdata/dynamic_caller_contract.wasm") + numberContract = mustLoad("./testdata/number.wasm") + callNumberContract = mustLoad("./testdata/call_number.wasm") +) + +// This tests dynamic calls using callee_contract's pong +func TestDynamicPingPongWorks(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + + // store dynamic callee code + storeCalleeMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: calleeContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store dynamic caller code + storeCallerMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: callerContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "callee", + Msg: []byte(`{}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "caller", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // execute ping + cosmwasmExecuteMsg := `{"ping":{"ping_num":"100"}}` + executeMsg := MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + res, err = h(data.ctx, &executeMsg) + require.NoError(t, err) + + assert.Equal(t, len(res.Events), 3) + assert.Equal(t, "wasm", res.Events[2].Type) + assert.Equal(t, len(res.Events[2].Attributes), 6) + assertAttribute(t, "returned_pong", "101", res.Events[2].Attributes[1]) + assertAttribute(t, "returned_pong_with_struct", "hello world 101", res.Events[2].Attributes[2]) + assertAttribute(t, "returned_pong_with_tuple", "(hello world, 42)", res.Events[2].Attributes[3]) + assertAttribute(t, "returned_pong_with_tuple_takes_2_args", "(hello world, 42)", res.Events[2].Attributes[4]) + assertAttribute(t, "returned_contract_address", calleeContractAddress, res.Events[2].Attributes[5]) +} + +// This tests re-entrancy in dynamic call fails +func TestDynamicReEntrancyFails(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + + // store dynamic callee code + storeCalleeMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: calleeContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store dynamic caller code + storeCallerMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: callerContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "callee", + Msg: []byte(`{}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "caller", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // execute ping + cosmwasmExecuteMsg := `{"try_re_entrancy":{}}` + executeMsg := MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + res, err = h(data.ctx, &executeMsg) + assert.ErrorContains(t, err, "A contract can only be called once per one call stack.") +} + +func TestDynamicLinkInterfaceValidation(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + + // store dynamic callee code + storeCalleeMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: calleeContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store dynamic caller code + storeCallerMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: callerContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "callee", + Msg: []byte(`{}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "caller", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // execute validate interface + cosmwasmExecuteMsg := `{"validate_interface":{}}` + executeMsg := MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + res, err = h(data.ctx, &executeMsg) + require.NoError(t, err) + + // execute validate interface error + cosmwasmExecuteMsgErr := `{"validate_interface_err":{}}` + executeMsgErr := MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsgErr), + Funds: nil, + } + res, err = h(data.ctx, &executeMsgErr) + assert.ErrorContains(t, err, "The following functions are not implemented:") +} + +// This tests both of dynamic calls and traditional queries can be used +// in a contract call +func TestDynamicCallAndTraditionalQueryWork(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + q := data.module.LegacyQuerierHandler(nil) + + // store callee code (number) + storeCalleeMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: numberContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store caller code (call-number) + storeCallerMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: callNumberContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "number", + Msg: []byte(`{"value":21}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "call-number", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // traditional queries from caller + queryPath := []string{ + QueryGetContractState, + callerContractAddress, + keeper.QueryMethodContractStateSmart, + } + queryReq := abci.RequestQuery{Data: []byte(`{"number":{}}`)} + qRes, qErr := q(data.ctx, queryPath, queryReq) + require.NoError(t, qErr) + assert.Equal(t, []byte(`{"value":21}`), qRes) + + // query via dynamic call from caller + dynQueryReq := abci.RequestQuery{Data: []byte(`{"number_dyn":{}}`)} + qRes, qErr = q(data.ctx, queryPath, dynQueryReq) + require.NoError(t, qErr) + assert.Equal(t, []byte(`{"value":21}`), qRes) + + // execute mul + cosmwasmExecuteMsg := `{"mul":{"value":2}}` + executeMsg := MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + res, err = h(data.ctx, &executeMsg) + require.NoError(t, err) + assert.Equal(t, len(res.Events), 3) + assert.Equal(t, "wasm", res.Events[2].Type) + assert.Equal(t, len(res.Events[2].Attributes), 3) + assertAttribute(t, "value_by_dynamic", "42", res.Events[2].Attributes[1]) + assertAttribute(t, "value_by_query", "42", res.Events[2].Attributes[2]) + + // queries + qRes, qErr = q(data.ctx, queryPath, queryReq) + require.NoError(t, qErr) + assert.Equal(t, []byte(`{"value":42}`), qRes) + qRes, qErr = q(data.ctx, queryPath, dynQueryReq) + require.NoError(t, qErr) + assert.Equal(t, []byte(`{"value":42}`), qRes) +} + +// This tests dynamic call with writing something to storage fails +// if it is called by a query +func TestDynamicCallWithWriteFailsByQuery(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + q := data.module.LegacyQuerierHandler(nil) + + // store callee code (number) + storeCalleeMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: numberContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store caller code (call-number) + storeCallerMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: callNumberContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "number", + Msg: []byte(`{"value":21}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "call-number", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // query which tries to write value to storage + queryPath := []string{ + QueryGetContractState, + callerContractAddress, + keeper.QueryMethodContractStateSmart, + } + queryReq := abci.RequestQuery{Data: []byte(`{"mul":{"value":2}}`)} + _, qErr := q(data.ctx, queryPath, queryReq) + assert.ErrorContains(t, qErr, "a read-write callable point is called in read-only context") +} + +// This tests callee_panic in dynamic call fails +func TestDynamicCallCalleeFails(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + + // store dynamic callee code + storeCalleeMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: calleeContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store dynamic caller code + storeCallerMsg := &MsgStoreCode{ + Sender: addr1, + WASMByteCode: callerContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "callee", + Msg: []byte(`{}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "caller", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // execute do_panic + cosmwasmExecuteMsg := `{"do_panic":{}}` + executeMsg := MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + res, err = h(data.ctx, &executeMsg) + assert.ErrorContains(t, err, "Error in dynamic link") + assert.ErrorContains(t, err, "RuntimeError: unreachable") +} diff --git a/x/wasm/keeper/api.go b/x/wasm/keeper/api.go index 9da002b6a7..8f7934d492 100644 --- a/x/wasm/keeper/api.go +++ b/x/wasm/keeper/api.go @@ -1,7 +1,10 @@ package keeper import ( + "fmt" + sdk "github.com/line/lbm-sdk/types" + "github.com/line/wasmd/x/wasm/types" wasmvm "github.com/line/wasmvm" wasmvmtypes "github.com/line/wasmvm/types" ) @@ -25,6 +28,19 @@ var ( } ) +type CosmwasmAPIImpl struct { + keeper *Keeper + ctx *sdk.Context +} + +type cosmwasmAPIGeneratorImpl struct { + keeper *Keeper +} + +type CosmwasmAPIGenerator interface { + Generate(ctx *sdk.Context) wasmvm.GoAPI +} + func humanAddress(canon []byte) (string, uint64, error) { if err := sdk.VerifyAddressFormat(canon); err != nil { return "", costHumanize, err @@ -37,7 +53,80 @@ func canonicalAddress(human string) ([]byte, uint64, error) { return bz, costCanonical, err } -var cosmwasmAPI = wasmvm.GoAPI{ // nolint:varcheck,deadcode - HumanAddress: humanAddress, - CanonicalAddress: canonicalAddress, +// callCallablePoint is a wrapper function of `wasmvm` +// returns result, gas used, error +func (a CosmwasmAPIImpl) callCallablePoint(contractAddrStr string, name []byte, args []byte, isReadonly bool, callstack []byte, gasLimit uint64) ([]byte, uint64, error) { + contractAddr := sdk.MustAccAddressFromBech32(contractAddrStr) + contractInfo, codeInfo, prefixStore, err := a.keeper.contractInstance(*a.ctx, contractAddr) + if err != nil { + return nil, 0, err + } + + env := types.NewEnv(*a.ctx, contractAddr) + wasmStore := types.NewWasmStore(prefixStore) + gasRegister := a.keeper.GetGasRegister() + querier := NewQueryHandler(*a.ctx, a.keeper.wasmVMQueryHandler, contractAddr, gasRegister) + gasMeter := a.keeper.gasMeter(*a.ctx) + api := a.keeper.cosmwasmAPIGenerator.Generate(a.ctx) + + instantiateCost := gasRegister.ToWasmVMGas(gasRegister.InstantiateContractCosts(a.keeper.IsPinnedCode(*a.ctx, contractInfo.CodeID), len(args))) + if gasLimit < instantiateCost { + return nil, 0, fmt.Errorf("lack of gas for calling callable point") + } + wasmGasLimit := gasLimit - instantiateCost + + result, events, attrs, gas, err := a.keeper.wasmVM.CallCallablePoint(name, codeInfo.CodeHash, isReadonly, callstack, env, args, wasmStore, api, querier, gasMeter, wasmGasLimit, costJSONDeserialization) + gas += instantiateCost + if err != nil { + return nil, gas, err + } + + if !isReadonly { + // issue events and attrs + if len(attrs) != 0 { + eventsByAttr, err := newCallablePointEvent(attrs, contractAddr, callstack) + if err != nil { + return nil, gas, err + } + a.ctx.EventManager().EmitEvents(eventsByAttr) + } + + if len(events) != 0 { + customEvents, err := newCustomCallablePointEvents(events, contractAddr, callstack) + if err != nil { + return nil, gas, err + } + a.ctx.EventManager().EmitEvents(customEvents) + } + } + + return result, gas, err +} + +// validateInterface is a wrapper function of `wasmvm` +// returns result, gas used, error +func (a CosmwasmAPIImpl) validateInterface(contractAddrStr string, expectedInterface []byte) ([]byte, uint64, error) { + contractAddr := sdk.MustAccAddressFromBech32(contractAddrStr) + + _, codeInfo, _, err := a.keeper.contractInstance(*a.ctx, contractAddr) + if err != nil { + return nil, 0, err + } + + result, err := a.keeper.wasmVM.ValidateDynamicLinkInterface(codeInfo.CodeHash, expectedInterface) + + return result, 0, err +} + +func (c cosmwasmAPIGeneratorImpl) Generate(ctx *sdk.Context) wasmvm.GoAPI { + x := CosmwasmAPIImpl{ + keeper: c.keeper, + ctx: ctx, + } + return wasmvm.GoAPI{ + HumanAddress: humanAddress, + CanonicalAddress: canonicalAddress, + CallCallablePoint: x.callCallablePoint, + ValidateInterface: x.validateInterface, + } } diff --git a/x/wasm/keeper/api_test.go b/x/wasm/keeper/api_test.go new file mode 100644 index 0000000000..d5e58e74e4 --- /dev/null +++ b/x/wasm/keeper/api_test.go @@ -0,0 +1,259 @@ +package keeper + +import ( + "encoding/json" + "io/ioutil" + "testing" + + sdk "github.com/line/lbm-sdk/types" + wasmvm "github.com/line/wasmvm" + + wasmvmtypes "github.com/line/wasmvm/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newAPI(t *testing.T) wasmvm.GoAPI { + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + return keepers.WasmKeeper.cosmwasmAPIGenerator.Generate(&ctx) +} + +func TestAPIHumanAddress(t *testing.T) { + // prepare API + api := newAPI(t) + + t.Run("valid address", func(t *testing.T) { + // address for alice in testnet + addr := "link1twsfmuj28ndph54k4nw8crwu8h9c8mh3rtx705" + bz, err := sdk.AccAddressFromBech32(addr) + require.NoError(t, err) + result, gas, err := api.HumanAddress(bz) + require.NoError(t, err) + assert.Equal(t, addr, result) + assert.Equal(t, costHumanize, gas) + }) + + t.Run("invalid address", func(t *testing.T) { + _, gas, err := api.HumanAddress([]byte("invalid_address")) + require.Error(t, err) + assert.Equal(t, costHumanize, gas) + }) +} + +func TestAPICanonicalAddress(t *testing.T) { + // prepare API + api := newAPI(t) + + t.Run("valid address", func(t *testing.T) { + addr := "link1twsfmuj28ndph54k4nw8crwu8h9c8mh3rtx705" + expected, err := sdk.AccAddressFromBech32(addr) + require.NoError(t, err) + result, gas, err := api.CanonicalAddress(addr) + require.NoError(t, err) + assert.Equal(t, expected.Bytes(), result) + assert.Equal(t, costCanonical, gas) + }) + + t.Run("invalid address", func(t *testing.T) { + _, gas, err := api.CanonicalAddress("invalid_address") + assert.Error(t, err) + assert.Equal(t, costCanonical, gas) + }) +} + +func TestCallCallablePoint(t *testing.T) { + // prepare ctx and keeper + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + + // instantiate an events contract + numberWasm, err := ioutil.ReadFile("../testdata/events.wasm") + require.NoError(t, err) + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, numberWasm, nil) + require.NoError(t, err) + initMsg := []byte(`{}`) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsg, "events", nil) + require.NoError(t, err) + callstack := []sdk.AccAddress{RandomAccountAddress(t), RandomAccountAddress(t)} + callstackBin, err := json.Marshal(callstack) + require.NoError(t, err) + var gasLimit uint64 = keepers.WasmKeeper.GetGasRegister().ToWasmVMGas(400_000) + + // prepare API + api := keepers.WasmKeeper.cosmwasmAPIGenerator.Generate(&ctx) + + // prepare arg for succeed + eventsIn := wasmvmtypes.Events{ + wasmvmtypes.Event{ + Type: "ty1", + Attributes: wasmvmtypes.EventAttributes{ + wasmvmtypes.EventAttribute{ + Key: "alice", + Value: "101010", + }, + wasmvmtypes.EventAttribute{ + Key: "bob", + Value: "42", + }, + }, + }, + wasmvmtypes.Event{ + Type: "ty2", + Attributes: wasmvmtypes.EventAttributes{ + wasmvmtypes.EventAttribute{ + Key: "ALICE", + Value: "42", + }, + wasmvmtypes.EventAttribute{ + Key: "BOB", + Value: "101010", + }, + }, + }, + } + eventsInBin, err := eventsIn.MarshalJSON() + require.NoError(t, err) + + t.Run("succeed", func(t *testing.T) { + argsEv := [][]byte{eventsInBin} + argsEvBin, err := json.Marshal(argsEv) + require.NoError(t, err) + name := "add_events_dyn" + nameBin, err := json.Marshal(name) + require.NoError(t, err) + res, _, err := api.CallCallablePoint(contractAddr.String(), nameBin, argsEvBin, false, callstackBin, gasLimit) + require.NoError(t, err) + assert.Equal(t, []byte(`null`), res) + + eventsExpected, err := newCustomCallablePointEvents(eventsIn, contractAddr, callstackBin) + require.NoError(t, err) + for _, e := range eventsExpected { + assert.Contains(t, em.Events(), e) + } + }) + + t.Run("fail with no arg", func(t *testing.T) { + argsEv := [][]byte{} + argsEvBin, err := json.Marshal(argsEv) + require.NoError(t, err) + name := "add_events_dyn" + nameBin, err := json.Marshal(name) + require.NoError(t, err) + _, _, err = api.CallCallablePoint(contractAddr.String(), nameBin, argsEvBin, false, callstackBin, gasLimit) + require.Error(t, err) + assert.ErrorContains(t, err, "RuntimeError") + assert.ErrorContains(t, err, "Parameters of type [I32] did not match signature [I32, I32] -> []") + }) + + t.Run("fail with invalid name", func(t *testing.T) { + argsEv := [][]byte{eventsInBin} + argsEvBin, err := json.Marshal(argsEv) + require.NoError(t, err) + name := "invalid" + nameBin, err := json.Marshal(name) + require.NoError(t, err) + _, _, err = api.CallCallablePoint(contractAddr.String(), nameBin, argsEvBin, false, callstackBin, gasLimit) + + // fail to get permission + require.Error(t, err) + assert.ErrorContains(t, err, "Error during calling dynamic linked callable point") + require.ErrorContains(t, err, "callee function properties has not key:invalid") + }) + + t.Run("fail with invalid address", func(t *testing.T) { + argsEv := [][]byte{eventsInBin} + argsEvBin, err := json.Marshal(argsEv) + require.NoError(t, err) + name := "add_events_dyn" + nameBin, err := json.Marshal(name) + require.NoError(t, err) + _, _, err = api.CallCallablePoint(RandomAccountAddress(t).String(), nameBin, argsEvBin, false, callstackBin, gasLimit) + + require.Error(t, err) + assert.ErrorContains(t, err, "contract: not found") + }) + + t.Run("fail with lack of write permission", func(t *testing.T) { + argsEv := [][]byte{eventsInBin} + argsEvBin, err := json.Marshal(argsEv) + require.NoError(t, err) + name := "add_events_dyn" + nameBin, err := json.Marshal(name) + require.NoError(t, err) + _, _, err = api.CallCallablePoint(contractAddr.String(), nameBin, argsEvBin, true, callstackBin, gasLimit) + + require.Error(t, err) + assert.ErrorContains(t, err, "Error during calling dynamic linked callable point") + assert.ErrorContains(t, err, "a read-write callable point is called in read-only context.") + }) + + t.Run("fail with re-entrancing", func(t *testing.T) { + argsEv := [][]byte{eventsInBin} + argsEvBin, err := json.Marshal(argsEv) + require.NoError(t, err) + name := "add_events_dyn" + nameBin, err := json.Marshal(name) + require.NoError(t, err) + + // callstack with re-entrancy + callstack = append(callstack, contractAddr) + callstackBin, err := json.Marshal(callstack) + require.NoError(t, err) + _, _, err = api.CallCallablePoint(contractAddr.String(), nameBin, argsEvBin, true, callstackBin, gasLimit) + + require.Error(t, err) + assert.ErrorContains(t, err, "Error calling the VM") + assert.ErrorContains(t, err, "A contract can only be called once per one call stack.") + }) +} + +func TestValidateDynamicLinkInterface(t *testing.T) { + // prepare ctx and keeper + ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) + + // instantiate an events contract + numberWasm, err := ioutil.ReadFile("../testdata/events.wasm") + require.NoError(t, err) + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, numberWasm, nil) + require.NoError(t, err) + initMsg := []byte(`{}`) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsg, "events", nil) + require.NoError(t, err) + + // prepare API + api := keepers.WasmKeeper.cosmwasmAPIGenerator.Generate(&ctx) + + t.Run("succeed valid", func(t *testing.T) { + validInterface := []byte(`[{"name":"add_event_dyn","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_events_dyn","ty":{"params":["I32","I32"],"results":[]}},{"name":"add_attribute_dyn","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_attributes_dyn","ty":{"params":["I32","I32"],"results":[]}}]`) + res, _, err := api.ValidateInterface(contractAddr.String(), validInterface) + + require.NoError(t, err) + assert.Equal(t, []byte(`null`), res) + }) + + t.Run("succeed invalid", func(t *testing.T) { + invalidInterface := []byte(`[{"name":"add_event","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_events","ty":{"params":["I32","I32"],"results":[]}},{"name":"add_attribute","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_attributes","ty":{"params":["I32","I32"],"results":[]}}]`) + res, _, err := api.ValidateInterface(contractAddr.String(), invalidInterface) + + require.NoError(t, err) + assert.Contains(t, string(res), `following functions are not implemented`) + assert.Contains(t, string(res), `add_event`) + assert.Contains(t, string(res), `add_events`) + assert.Contains(t, string(res), `add_attribute`) + assert.Contains(t, string(res), `add_attributes`) + }) + + t.Run("fail with invalid address", func(t *testing.T) { + validInterface := []byte(`[{"name":"add_event_dyn","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_events_dyn","ty":{"params":["I32","I32"],"results":[]}},{"name":"add_attribute_dyn","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_attributes_dyn","ty":{"params":["I32","I32"],"results":[]}}]`) + _, _, err := api.ValidateInterface(RandomAccountAddress(t).String(), validInterface) + require.Error(t, err) + assert.ErrorContains(t, err, "contract: not found") + }) +} diff --git a/x/wasm/keeper/events.go b/x/wasm/keeper/events.go index ffb72553a5..0a49bb7684 100644 --- a/x/wasm/keeper/events.go +++ b/x/wasm/keeper/events.go @@ -65,3 +65,39 @@ func contractSDKEventAttributes(customAttributes []wasmvmtypes.EventAttribute, c } return attrs, nil } + +func newCallablePointEvent(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress, callstack []byte) (sdk.Events, error) { + attrs, err := callablePointSDKEventAttributes(customAttributes, contractAddr, callstack) + + if err != nil { + return nil, err + } + + return sdk.Events{sdk.NewEvent(types.CallablePointEventType, attrs...)}, nil +} + +func newCustomCallablePointEvents(evts wasmvmtypes.Events, contractAddr sdk.AccAddress, callstack []byte) (sdk.Events, error) { + events := make(sdk.Events, 0, len(evts)) + for _, e := range evts { + typ := strings.TrimSpace(e.Type) + if len(typ) <= eventTypeMinLength { + return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Event type too short: '%s'", typ)) + } + attributes, err := callablePointSDKEventAttributes(e.Attributes, contractAddr, callstack) + if err != nil { + return nil, err + } + events = append(events, sdk.NewEvent(fmt.Sprintf("%s%s", types.CustomCallablePointEventPrefix, typ), attributes...)) + } + return events, nil +} + +func callablePointSDKEventAttributes(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress, callstack []byte) ([]sdk.Attribute, error) { + attrs, err := contractSDKEventAttributes(customAttributes, contractAddr) + if err != nil { + return nil, err + } + // attrs[0] is addr + attrs = append([]sdk.Attribute{attrs[0], sdk.NewAttribute(types.AttributeKeyCallstack, string(callstack))}, attrs[1:]...) + return attrs, nil +} diff --git a/x/wasm/keeper/events_test.go b/x/wasm/keeper/events_test.go index 59ad98c0b9..2de9150418 100644 --- a/x/wasm/keeper/events_test.go +++ b/x/wasm/keeper/events_test.go @@ -2,9 +2,11 @@ package keeper import ( "context" + "encoding/json" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" sdk "github.com/line/lbm-sdk/types" wasmvmtypes "github.com/line/wasmvm/types" @@ -289,3 +291,240 @@ func hasWasmModuleEvent(ctx sdk.Context, contractAddr sdk.AccAddress) bool { } return false } + +func TestNewCustomCallablePointEvents(t *testing.T) { + myContract := RandomAccountAddress(t) + myCallstack := []sdk.AccAddress{RandomAccountAddress(t), RandomAccountAddress(t)} + myCallstackBinary, err := json.Marshal(myCallstack) + require.NoError(t, err) + specs := map[string]struct { + src wasmvmtypes.Events + exp sdk.Events + isError bool + }{ + "all good": { + src: wasmvmtypes.Events{{ + Type: "foo", + Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}}, + }}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("myKey", "myVal"))}, + }, + "multiple attributes": { + src: wasmvmtypes.Events{{ + Type: "foo", + Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}, + {Key: "myOtherKey", Value: "myOtherVal"}}, + }}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("myKey", "myVal"), + sdk.NewAttribute("myOtherKey", "myOtherVal"))}, + }, + "multiple events": { + src: wasmvmtypes.Events{{ + Type: "foo", + Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}}, + }, { + Type: "bar", + Attributes: []wasmvmtypes.EventAttribute{{Key: "otherKey", Value: "otherVal"}}, + }}, + exp: sdk.Events{ + sdk.NewEvent("wasm-callablepoint-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("myKey", "myVal")), + sdk.NewEvent("wasm-callablepoint-bar", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("otherKey", "otherVal")), + }, + }, + "without attributes": { + src: wasmvmtypes.Events{{ + Type: "foo", + }}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-foo", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary))), + }, + }, + "error on short event type": { + src: wasmvmtypes.Events{{ + Type: "f", + }}, + isError: true, + }, + "error on _contract_address": { + src: wasmvmtypes.Events{{ + Type: "foo", + Attributes: []wasmvmtypes.EventAttribute{{Key: "_contract_address", Value: RandomBech32AccountAddress(t)}}, + }}, + isError: true, + }, + "error on reserved prefix": { + src: wasmvmtypes.Events{{ + Type: "wasm", + Attributes: []wasmvmtypes.EventAttribute{ + {Key: "_reserved", Value: "is skipped"}, + {Key: "normal", Value: "is used"}}, + }}, + isError: true, + }, + "error on empty value": { + src: wasmvmtypes.Events{{ + Type: "boom", + Attributes: []wasmvmtypes.EventAttribute{ + {Key: "some", Value: "data"}, + {Key: "key", Value: ""}, + }, + }}, + isError: true, + }, + "error on empty key": { + src: wasmvmtypes.Events{{ + Type: "boom", + Attributes: []wasmvmtypes.EventAttribute{ + {Key: "some", Value: "data"}, + {Key: "", Value: "value"}, + }, + }}, + isError: true, + }, + "error on whitespace type": { + src: wasmvmtypes.Events{{ + Type: " f ", + Attributes: []wasmvmtypes.EventAttribute{ + {Key: "some", Value: "data"}, + }, + }}, + isError: true, + }, + "error on only whitespace key": { + src: wasmvmtypes.Events{{ + Type: "boom", + Attributes: []wasmvmtypes.EventAttribute{ + {Key: "some", Value: "data"}, + {Key: "\n\n\n\n", Value: "value"}, + }, + }}, + isError: true, + }, + "error on only whitespace value": { + src: wasmvmtypes.Events{{ + Type: "boom", + Attributes: []wasmvmtypes.EventAttribute{ + {Key: "some", Value: "data"}, + {Key: "myKey", Value: " \t\r\n"}, + }, + }}, + isError: true, + }, + "strip out whitespace": { + src: wasmvmtypes.Events{{ + Type: " food\n", + Attributes: []wasmvmtypes.EventAttribute{{Key: "my Key", Value: "\tmyVal"}}, + }}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-food", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + + sdk.NewAttribute("my Key", "myVal"))}, + }, + "empty event elements": { + src: make(wasmvmtypes.Events, 10), + isError: true, + }, + "nil": { + exp: sdk.Events{}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotEvent, err := newCustomCallablePointEvents(spec.src, myContract, myCallstackBinary) + if spec.isError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, spec.exp, gotEvent) + } + }) + } +} + +func TestNewCallablePointEvent(t *testing.T) { + myContract := RandomAccountAddress(t) + myCallstack := []sdk.AccAddress{RandomAccountAddress(t), RandomAccountAddress(t)} + myCallstackBinary, err := json.Marshal(myCallstack) + require.NoError(t, err) + specs := map[string]struct { + src []wasmvmtypes.EventAttribute + exp sdk.Events + isError bool + }{ + "all good": { + src: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("myKey", "myVal"))}, + }, + "multiple attributes": { + src: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}, + {Key: "myOtherKey", Value: "myOtherVal"}}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("myKey", "myVal"), + sdk.NewAttribute("myOtherKey", "myOtherVal"))}, + }, + "without attributes": { + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", + sdk.NewAttribute("_contract_address", myContract.String()), sdk.NewAttribute("_callstack", string(myCallstackBinary))), + }, + }, + "error on _contract_address": { + src: []wasmvmtypes.EventAttribute{{Key: "_contract_address", Value: RandomBech32AccountAddress(t)}}, + isError: true, + }, + "error on whitespace key": { + src: []wasmvmtypes.EventAttribute{{Key: " ", Value: "value"}}, + isError: true, + }, + "error on whitespace value": { + src: []wasmvmtypes.EventAttribute{{Key: "key", Value: "\n\n\n"}}, + isError: true, + }, + "strip whitespace": { + src: []wasmvmtypes.EventAttribute{{Key: " my-real-key ", Value: "\n\n\nsome-val\t\t\t"}}, + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + sdk.NewAttribute("my-real-key", "some-val"))}, + }, + "empty elements": { + src: make([]wasmvmtypes.EventAttribute, 10), + isError: true, + }, + "nil": { + exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", + sdk.NewAttribute("_contract_address", myContract.String()), + sdk.NewAttribute("_callstack", string(myCallstackBinary)), + )}, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + gotEvent, err := newCallablePointEvent(spec.src, myContract, myCallstackBinary) + if spec.isError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, spec.exp, gotEvent) + } + }) + } +} diff --git a/x/wasm/keeper/exported_api.go b/x/wasm/keeper/exported_api.go index 9ecad5d8c9..d3aeddf3db 100644 --- a/x/wasm/keeper/exported_api.go +++ b/x/wasm/keeper/exported_api.go @@ -1,9 +1,22 @@ package keeper +import sdk "github.com/line/lbm-sdk/types" + var ( - HumanAddress = humanAddress - CanonicalAddress = canonicalAddress - CostHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier - CostCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier - CostJSONDeserialization = costJSONDeserialization + HumanAddress = humanAddress + CanonicalAddress = canonicalAddress + CostHumanize = DefaultGasCostHumanAddress * DefaultGasMultiplier + CostCanonical = DefaultGasCostCanonicalAddress * DefaultGasMultiplier ) + +func NewCosmwasmAPIImpl(k *Keeper, ctx *sdk.Context) CosmwasmAPIImpl { + return CosmwasmAPIImpl{keeper: k, ctx: ctx} +} + +func (a CosmwasmAPIImpl) CallCallablePoint(contractAddrStr string, name []byte, args []byte, isReadonly bool, callstack []byte, gasLimit uint64) ([]byte, uint64, error) { + return a.callCallablePoint(contractAddrStr, name, args, isReadonly, callstack, gasLimit) +} + +func (a CosmwasmAPIImpl) ValidateInterface(contractAddrStr string, expectedInterface []byte) ([]byte, uint64, error) { + return a.validateInterface(contractAddrStr, expectedInterface) +} diff --git a/x/wasm/keeper/exported_events.go b/x/wasm/keeper/exported_events.go index d5515e1540..80427f1560 100644 --- a/x/wasm/keeper/exported_events.go +++ b/x/wasm/keeper/exported_events.go @@ -1,6 +1,10 @@ package keeper -var ( - ContractSDKEventAttributes = contractSDKEventAttributes - EventTypeMinLength = eventTypeMinLength +import ( + sdk "github.com/line/lbm-sdk/types" + wasmvmtypes "github.com/line/wasmvm/types" ) + +func NewCustomCallablePointEvents(evts wasmvmtypes.Events, contractAddr sdk.AccAddress, callstack []byte) (sdk.Events, error) { + return newCustomCallablePointEvents(evts, contractAddr, callstack) +} diff --git a/x/wasm/keeper/exported_keeper.go b/x/wasm/keeper/exported_keeper.go index 1a3ce0539d..1d042d16de 100644 --- a/x/wasm/keeper/exported_keeper.go +++ b/x/wasm/keeper/exported_keeper.go @@ -1,36 +1,13 @@ package keeper -import ( - "github.com/line/lbm-sdk/store/prefix" - sdk "github.com/line/lbm-sdk/types" - "github.com/line/wasmd/x/wasm/types" - wasmvm "github.com/line/wasmvm" -) - -type PlusKeeper interface { - CosmwasmAPI(ctx sdk.Context) wasmvm.GoAPI -} - -func (k Keeper) ContractInstance(ctx sdk.Context, contractAddress sdk.AccAddress) (types.ContractInfo, types.CodeInfo, prefix.Store, error) { - return k.contractInstance(ctx, contractAddress) -} - -func (k Keeper) GasMeter(ctx sdk.Context) MultipliedGasMeter { - return k.gasMeter(ctx) -} - func (k Keeper) GetGasRegister() GasRegister { return k.gasRegister } -func (k Keeper) GetWasmVM() types.WasmerEngine { - return k.wasmVM -} - -func (k Keeper) GetWasmVMQueryHandler() WasmVMQueryHandler { - return k.wasmVMQueryHandler +func (k Keeper) GetCosmwasmAPIGenerator() CosmwasmAPIGenerator { + return k.cosmwasmAPIGenerator } -func (k Keeper) GetPluskeeper() PlusKeeper { - return k.pluskeeper +func (k *Keeper) SetCosmwasmAPIGenerator(generator CosmwasmAPIGenerator) { + k.cosmwasmAPIGenerator = generator } diff --git a/x/wasm/keeper/genesis_test.go b/x/wasm/keeper/genesis_test.go index 4332e4969e..43673da13f 100644 --- a/x/wasm/keeper/genesis_test.go +++ b/x/wasm/keeper/genesis_test.go @@ -686,7 +686,6 @@ func setupKeeper(t *testing.T) (*Keeper, sdk.Context, []sdk.StoreKey) { tempDir, wasmConfig, AvailableCapabilities, - &mockpluskeeper{}, ) return &srcKeeper, ctx, []sdk.StoreKey{keyWasm, keyParams} } diff --git a/x/wasm/keeper/keeper.go b/x/wasm/keeper/keeper.go index 4dfafd2717..09833c1726 100644 --- a/x/wasm/keeper/keeper.go +++ b/x/wasm/keeper/keeper.go @@ -102,7 +102,7 @@ type Keeper struct { maxQueryStackSize uint32 acceptedAccountTypes map[reflect.Type]struct{} accountPruner AccountPruner - pluskeeper PlusKeeper + cosmwasmAPIGenerator CosmwasmAPIGenerator } // NewKeeper creates a new contract Keeper instance @@ -124,7 +124,6 @@ func NewKeeper( homeDir string, wasmConfig types.WasmConfig, availableCapabilities string, - pluskeeper PlusKeeper, opts ...Option, ) Keeper { wasmer, err := wasmvm.NewVM(filepath.Join(homeDir, "wasm"), availableCapabilities, contractMemoryLimit, wasmConfig.ContractDebugMode, wasmConfig.MemoryCacheSize) @@ -152,8 +151,8 @@ func NewKeeper( gasRegister: NewDefaultWasmGasRegister(), maxQueryStackSize: types.DefaultMaxQueryStackSize, acceptedAccountTypes: defaultAcceptedAccountTypes, - pluskeeper: pluskeeper, } + keeper.cosmwasmAPIGenerator = cosmwasmAPIGeneratorImpl{keeper: keeper} keeper.wasmVMQueryHandler = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, channelKeeper, queryRouter, keeper) for _, o := range opts { o.apply(keeper) @@ -355,7 +354,7 @@ func (k Keeper) instantiate( // instantiate wasm contract gas := k.runtimeGasForContract(ctx) - res, gasUsed, err := k.wasmVM.Instantiate(codeInfo.CodeHash, env, info, initMsg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) + res, gasUsed, err := k.wasmVM.Instantiate(codeInfo.CodeHash, env, info, initMsg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if err != nil { return nil, nil, sdkerrors.Wrap(types.ErrInstantiateFailed, err.Error()) @@ -423,7 +422,7 @@ func (k Keeper) execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller // prepare querier querier := k.newQueryHandler(ctx, contractAddress) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.Execute(codeInfo.CodeHash, env, info, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.Execute(codeInfo.CodeHash, env, info, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) @@ -488,7 +487,7 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller prefixStoreKey := types.GetContractStorePrefix(contractAddress) prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) gas := k.runtimeGasForContract(ctx) - res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, &prefixStore, k.pluskeeper.CosmwasmAPI(ctx), &querier, k.gasMeter(ctx), gas, costJSONDeserialization) + res, gasUsed, err := k.wasmVM.Migrate(newCodeInfo.CodeHash, env, msg, &prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), &querier, k.gasMeter(ctx), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if err != nil { return nil, sdkerrors.Wrap(types.ErrMigrationFailed, err.Error()) @@ -534,7 +533,7 @@ func (k Keeper) Sudo(ctx sdk.Context, contractAddress sdk.AccAddress, msg []byte // prepare querier querier := k.newQueryHandler(ctx, contractAddress) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.Sudo(codeInfo.CodeHash, env, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.Sudo(codeInfo.CodeHash, env, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) @@ -570,7 +569,7 @@ func (k Keeper) reply(ctx sdk.Context, contractAddress sdk.AccAddress, reply was querier := k.newQueryHandler(ctx, contractAddress) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.Reply(codeInfo.CodeHash, env, reply, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.Reply(codeInfo.CodeHash, env, reply, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, k.gasMeter(ctx), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) @@ -697,7 +696,7 @@ func (k Keeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []b querier := k.newQueryHandler(ctx, contractAddr) env := types.NewEnv(ctx, contractAddr) - queryResult, gasUsed, qErr := k.wasmVM.Query(codeInfo.CodeHash, env, req, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, k.gasMeter(ctx), k.runtimeGasForContract(ctx), costJSONDeserialization) + queryResult, gasUsed, qErr := k.wasmVM.Query(codeInfo.CodeHash, env, req, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, k.gasMeter(ctx), k.runtimeGasForContract(ctx), costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if qErr != nil { return nil, sdkerrors.Wrap(types.ErrQueryFailed, qErr.Error()) diff --git a/x/wasm/keeper/options_test.go b/x/wasm/keeper/options_test.go index 0b2278af80..802282d8e9 100644 --- a/x/wasm/keeper/options_test.go +++ b/x/wasm/keeper/options_test.go @@ -105,7 +105,7 @@ func TestConstructorOptions(t *testing.T) { } for name, spec := range specs { t.Run(name, func(t *testing.T) { - k := NewKeeper(nil, nil, paramtypes.NewSubspace(nil, nil, nil, nil, ""), authkeeper.AccountKeeper{}, bankpluskeeper.BaseKeeper{}, stakingkeeper.Keeper{}, distributionkeeper.Keeper{}, nil, nil, nil, nil, nil, nil, "tempDir", types.DefaultWasmConfig(), AvailableCapabilities, &mockpluskeeper{}, spec.srcOpt) + k := NewKeeper(nil, nil, paramtypes.NewSubspace(nil, nil, nil, nil, ""), authkeeper.AccountKeeper{}, bankpluskeeper.BaseKeeper{}, stakingkeeper.Keeper{}, distributionkeeper.Keeper{}, nil, nil, nil, nil, nil, nil, "tempDir", types.DefaultWasmConfig(), AvailableCapabilities, spec.srcOpt) spec.verify(t, k) }) } diff --git a/x/wasm/keeper/relay.go b/x/wasm/keeper/relay.go index c7c9fdf3f5..3f3359d3cc 100644 --- a/x/wasm/keeper/relay.go +++ b/x/wasm/keeper/relay.go @@ -35,7 +35,7 @@ func (k Keeper) OnOpenChannel( querier := k.newQueryHandler(ctx, contractAddr) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.IBCChannelOpen(codeInfo.CodeHash, env, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.IBCChannelOpen(codeInfo.CodeHash, env, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { return "", sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) @@ -70,7 +70,7 @@ func (k Keeper) OnConnectChannel( querier := k.newQueryHandler(ctx, contractAddr) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.IBCChannelConnect(codeInfo.CodeHash, env, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.IBCChannelConnect(codeInfo.CodeHash, env, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { @@ -102,7 +102,7 @@ func (k Keeper) OnCloseChannel( querier := k.newQueryHandler(ctx, contractAddr) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.IBCChannelClose(codeInfo.CodeHash, params, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.IBCChannelClose(codeInfo.CodeHash, params, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { @@ -133,7 +133,7 @@ func (k Keeper) OnRecvPacket( querier := k.newQueryHandler(ctx, contractAddr) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.IBCPacketReceive(codeInfo.CodeHash, env, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.IBCPacketReceive(codeInfo.CodeHash, env, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { @@ -168,7 +168,7 @@ func (k Keeper) OnAckPacket( querier := k.newQueryHandler(ctx, contractAddr) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.IBCPacketAck(codeInfo.CodeHash, env, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.IBCPacketAck(codeInfo.CodeHash, env, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { @@ -196,7 +196,7 @@ func (k Keeper) OnTimeoutPacket( querier := k.newQueryHandler(ctx, contractAddr) gas := k.runtimeGasForContract(ctx) - res, gasUsed, execErr := k.wasmVM.IBCPacketTimeout(codeInfo.CodeHash, env, msg, prefixStore, k.pluskeeper.CosmwasmAPI(ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) + res, gasUsed, execErr := k.wasmVM.IBCPacketTimeout(codeInfo.CodeHash, env, msg, prefixStore, k.cosmwasmAPIGenerator.Generate(&ctx), querier, ctx.GasMeter(), gas, costJSONDeserialization) k.consumeRuntimeGas(ctx, gasUsed) if execErr != nil { diff --git a/x/wasm/keeper/test_common.go b/x/wasm/keeper/test_common.go index 01d0f86db2..399d90f165 100644 --- a/x/wasm/keeper/test_common.go +++ b/x/wasm/keeper/test_common.go @@ -71,7 +71,6 @@ import ( "github.com/line/ostracon/crypto/ed25519" "github.com/line/ostracon/libs/log" "github.com/line/ostracon/libs/rand" - wasmvm "github.com/line/wasmvm" wasmappparams "github.com/line/wasmd/app/params" "github.com/line/wasmd/x/wasm/keeper/wasmtesting" @@ -199,21 +198,6 @@ func CreateTestInput(t testing.TB, isCheckTx bool, availableCapabilities string, return createTestInput(t, isCheckTx, availableCapabilities, types.DefaultWasmConfig(), dbm.NewMemDB(), opts...) } -type mockpluskeeper struct{} - -func (m *mockpluskeeper) CosmwasmAPI(ctx sdk.Context) wasmvm.GoAPI { - return wasmvm.GoAPI{ - HumanAddress: humanAddress, - CanonicalAddress: canonicalAddress, - CallCallablePoint: func(s string, b1, b2 []byte, b3 bool, b4 []byte, u uint64) ([]byte, uint64, error) { - return nil, 0, fmt.Errorf("mock does not implement CallCallablePoint") - }, - ValidateInterface: func(s string, b []byte) ([]byte, uint64, error) { - return nil, 0, fmt.Errorf("mock does not implement ValidateInterface") - }, - } -} - // encoders can be nil to accept the defaults, or set it to override some of the message handlers (like default) func createTestInput( t testing.TB, @@ -408,7 +392,6 @@ func createTestInput( tempDir, wasmConfig, availableCapabilities, - &mockpluskeeper{}, opts..., ) keeper.SetParams(ctx, types.DefaultParams()) diff --git a/x/wasm/testdata/call_number.wasm b/x/wasm/testdata/call_number.wasm new file mode 100644 index 0000000000..463fe2658e Binary files /dev/null and b/x/wasm/testdata/call_number.wasm differ diff --git a/x/wasm/testdata/dynamic_callee_contract.wasm b/x/wasm/testdata/dynamic_callee_contract.wasm new file mode 100644 index 0000000000..5b8abe4bdb Binary files /dev/null and b/x/wasm/testdata/dynamic_callee_contract.wasm differ diff --git a/x/wasm/testdata/dynamic_caller_contract.wasm b/x/wasm/testdata/dynamic_caller_contract.wasm new file mode 100644 index 0000000000..42a1e0fdd5 Binary files /dev/null and b/x/wasm/testdata/dynamic_caller_contract.wasm differ diff --git a/x/wasm/testdata/events.wasm b/x/wasm/testdata/events.wasm new file mode 100644 index 0000000000..decc643ff7 Binary files /dev/null and b/x/wasm/testdata/events.wasm differ diff --git a/x/wasm/testdata/number.wasm b/x/wasm/testdata/number.wasm new file mode 100644 index 0000000000..2c58452b11 Binary files /dev/null and b/x/wasm/testdata/number.wasm differ diff --git a/x/wasm/types/events.go b/x/wasm/types/events.go index 579024ed0c..7e00237ef4 100644 --- a/x/wasm/types/events.go +++ b/x/wasm/types/events.go @@ -5,6 +5,10 @@ const ( WasmModuleEventType = "wasm" // CustomContractEventPrefix contracts can create custom events. To not mix them with other system events they got the `wasm-` prefix. CustomContractEventPrefix = "wasm-" + // events from callable point + CallablePointEventType = "wasm-callablepoint" + // prefix for custom events from callable point + CustomCallablePointEventPrefix = "wasm-callablepoint-" EventTypeStoreCode = "store_code" EventTypeInstantiate = "instantiate" @@ -21,9 +25,12 @@ const ( const ( AttributeReservedPrefix = "_" + AttributeKeyCallstack = "_callstack" AttributeKeyContractAddr = "_contract_address" AttributeKeyCodeID = "code_id" + AttributeKeyCodeIDs = "code_ids" AttributeKeyChecksum = "code_checksum" + AttributeKeyFeature = "feature" AttributeKeyResultDataHex = "result" AttributeKeyRequiredCapability = "required_capability" ) diff --git a/x/wasmplus/types/store.go b/x/wasm/types/store.go similarity index 100% rename from x/wasmplus/types/store.go rename to x/wasm/types/store.go diff --git a/x/wasmplus/common_test.go b/x/wasmplus/common_test.go index f291bf1897..cb5d98b272 100644 --- a/x/wasmplus/common_test.go +++ b/x/wasmplus/common_test.go @@ -16,13 +16,6 @@ func assertStoreCodeResponse(t *testing.T, data []byte, expected uint64) { require.Equal(t, pStoreResp.CodeID, expected) } -// ensure execution returns the expected data -func assertExecuteResponse(t *testing.T, data []byte, expected []byte) { - var pExecResp wasm.MsgExecuteContractResponse - require.NoError(t, pExecResp.Unmarshal(data)) - require.Equal(t, pExecResp.Data, expected) -} - // ensures this returns a valid bech32 address and returns it func parseInitResponse(t *testing.T, data []byte) string { var pInstResp wasm.MsgInstantiateContractResponse diff --git a/x/wasmplus/dynamic_link_test.go b/x/wasmplus/dynamic_link_test.go index 2d2f6e7714..3eeb05e508 100644 --- a/x/wasmplus/dynamic_link_test.go +++ b/x/wasmplus/dynamic_link_test.go @@ -4,8 +4,10 @@ import ( "fmt" "testing" + sdk "github.com/line/lbm-sdk/types" "github.com/line/wasmd/x/wasm" "github.com/line/wasmd/x/wasm/keeper" + "github.com/line/wasmd/x/wasmplus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" @@ -480,3 +482,171 @@ func TestDynamicCallCalleeFails(t *testing.T) { assert.ErrorContains(t, err, "Error in dynamic link") assert.ErrorContains(t, err, "RuntimeError: unreachable") } + +// This tests callCallablePoint in inactive contract +func TestCallInActiveContractFails(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + + // store dynamic callee code + storeCalleeMsg := &wasm.MsgStoreCode{ + Sender: addr1, + WASMByteCode: calleeContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store dynamic caller code + storeCallerMsg := &wasm.MsgStoreCode{ + Sender: addr1, + WASMByteCode: callerContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &wasm.MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "callee", + Msg: []byte(`{}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &wasm.MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "caller", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // add contract to inactive + src := types.DeactivateContractProposal{ + Title: "Foo", + Description: "Bar", + Contract: calleeContractAddress, + } + storedProposal, err := data.govKeeper.SubmitProposal(data.ctx, &src) + require.NoError(t, err) + handler := data.govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(data.ctx.WithEventManager(sdk.NewEventManager()), storedProposal.GetContent()) + require.NoError(t, err) + calleeContractSdkAddress, err := sdk.AccAddressFromBech32(calleeContractAddress) + require.NoError(t, err) + isInactive := data.keeper.IsInactiveContract(data.ctx, calleeContractSdkAddress) + require.True(t, isInactive) + + // execute ping + cosmwasmExecuteMsg := `{"ping":{"ping_num":"100"}}` + executeMsg := wasm.MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + _, err = h(data.ctx, &executeMsg) + assert.ErrorContains(t, err, "called contract cannot be executed") +} + +// This tests validateInterface in inactive contract +func TestValidateInActiveContractFails(t *testing.T) { + // setup + data := setupTest(t) + + h := data.module.Route().Handler() + + // store dynamic callee code + storeCalleeMsg := &wasm.MsgStoreCode{ + Sender: addr1, + WASMByteCode: calleeContract, + } + res, err := h(data.ctx, storeCalleeMsg) + require.NoError(t, err) + + calleeCodeId := uint64(1) + assertStoreCodeResponse(t, res.Data, calleeCodeId) + + // store dynamic caller code + storeCallerMsg := &wasm.MsgStoreCode{ + Sender: addr1, + WASMByteCode: callerContract, + } + res, err = h(data.ctx, storeCallerMsg) + require.NoError(t, err) + + callerCodeId := uint64(2) + assertStoreCodeResponse(t, res.Data, callerCodeId) + + // instantiate callee contract + instantiateCalleeMsg := &wasm.MsgInstantiateContract{ + Sender: addr1, + CodeID: calleeCodeId, + Label: "callee", + Msg: []byte(`{}`), + Funds: nil, + } + res, err = h(data.ctx, instantiateCalleeMsg) + require.NoError(t, err) + + calleeContractAddress := parseInitResponse(t, res.Data) + + // instantiate caller contract + cosmwasmInstantiateCallerMsg := fmt.Sprintf(`{"callee_addr":"%s"}`, calleeContractAddress) + instantiateCallerMsg := &wasm.MsgInstantiateContract{ + Sender: addr1, + CodeID: callerCodeId, + Label: "caller", + Msg: []byte(cosmwasmInstantiateCallerMsg), + Funds: nil, + } + res, err = h(data.ctx, instantiateCallerMsg) + require.NoError(t, err) + + callerContractAddress := parseInitResponse(t, res.Data) + + // add contract to inactive + src := types.DeactivateContractProposal{ + Title: "Foo", + Description: "Bar", + Contract: calleeContractAddress, + } + storedProposal, err := data.govKeeper.SubmitProposal(data.ctx, &src) + require.NoError(t, err) + handler := data.govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(data.ctx.WithEventManager(sdk.NewEventManager()), storedProposal.GetContent()) + require.NoError(t, err) + calleeContractSdkAddress, err := sdk.AccAddressFromBech32(calleeContractAddress) + require.NoError(t, err) + isInactive := data.keeper.IsInactiveContract(data.ctx, calleeContractSdkAddress) + require.True(t, isInactive) + + // execute validate interface + cosmwasmExecuteMsg := `{"validate_interface":{}}` + executeMsg := wasm.MsgExecuteContract{ + Sender: addr1, + Contract: callerContractAddress, + Msg: []byte(cosmwasmExecuteMsg), + Funds: nil, + } + _, err = h(data.ctx, &executeMsg) + assert.ErrorContains(t, err, "try to validate a contract cannot be executed") +} diff --git a/x/wasmplus/keeper/api.go b/x/wasmplus/keeper/api.go index ba78ef3cea..43a79493eb 100644 --- a/x/wasmplus/keeper/api.go +++ b/x/wasmplus/keeper/api.go @@ -5,69 +5,29 @@ import ( sdk "github.com/line/lbm-sdk/types" wasmkeeper "github.com/line/wasmd/x/wasm/keeper" - "github.com/line/wasmd/x/wasm/types" - wasmplustypes "github.com/line/wasmd/x/wasmplus/types" wasmvm "github.com/line/wasmvm" ) type cosmwasmAPIImpl struct { + keeper *Keeper + ctx *sdk.Context + wasmAPI wasmkeeper.CosmwasmAPIImpl +} + +type cosmwasmAPIGeneratorImpl struct { keeper *Keeper - ctx *sdk.Context } func (a cosmwasmAPIImpl) callCallablePoint(contractAddrStr string, name []byte, args []byte, isReadonly bool, callstack []byte, gasLimit uint64) ([]byte, uint64, error) { contractAddr := sdk.MustAccAddressFromBech32(contractAddrStr) - contractInfo, codeInfo, prefixStore, err := a.keeper.ContractInstance(*a.ctx, contractAddr) - if err != nil { - return nil, 0, err - } if a.keeper.IsInactiveContract(*a.ctx, contractAddr) { return nil, 0, fmt.Errorf("called contract cannot be executed") } - env := types.NewEnv(*a.ctx, contractAddr) - wasmStore := wasmplustypes.NewWasmStore(prefixStore) - gasRegister := a.keeper.GetGasRegister() - querier := wasmkeeper.NewQueryHandler(*a.ctx, a.keeper.GetWasmVMQueryHandler(), contractAddr, gasRegister) - gasMeter := a.keeper.GasMeter(*a.ctx) - api := a.keeper.CosmwasmAPI(*a.ctx) - - instantiateCost := gasRegister.ToWasmVMGas(gasRegister.InstantiateContractCosts(a.keeper.IsPinnedCode(*a.ctx, contractInfo.CodeID), len(args))) - if gasLimit < instantiateCost { - return nil, 0, fmt.Errorf("lack of gas for calling callable point") - } - wasmGasLimit := gasLimit - instantiateCost - - result, events, attrs, gas, err := a.keeper.GetWasmVM().CallCallablePoint(name, codeInfo.CodeHash, isReadonly, callstack, env, args, wasmStore, api, querier, gasMeter, wasmGasLimit, wasmkeeper.CostJSONDeserialization) - gas += instantiateCost - if err != nil { - return nil, gas, err - } - - if !isReadonly { - // issue events and attrs - if len(attrs) != 0 { - eventsByAttr, err := newCallablePointEvent(attrs, contractAddr, callstack) - if err != nil { - return nil, gas, err - } - a.ctx.EventManager().EmitEvents(eventsByAttr) - } - - if len(events) != 0 { - customEvents, err := newCustomCallablePointEvents(events, contractAddr, callstack) - if err != nil { - return nil, gas, err - } - a.ctx.EventManager().EmitEvents(customEvents) - } - } - - return result, gas, err + return a.wasmAPI.CallCallablePoint(contractAddrStr, name, args, isReadonly, callstack, gasLimit) } -// returns result, gas used, error func (a cosmwasmAPIImpl) validateInterface(contractAddrStr string, expectedInterface []byte) ([]byte, uint64, error) { contractAddr := sdk.MustAccAddressFromBech32(contractAddrStr) @@ -75,20 +35,14 @@ func (a cosmwasmAPIImpl) validateInterface(contractAddrStr string, expectedInter return nil, 0, fmt.Errorf("try to validate a contract cannot be executed") } - _, codeInfo, _, err := a.keeper.ContractInstance(*a.ctx, contractAddr) - if err != nil { - return nil, 0, err - } - - result, err := a.keeper.GetWasmVM().ValidateDynamicLinkInterface(codeInfo.CodeHash, expectedInterface) - - return result, 0, err + return a.wasmAPI.ValidateInterface(contractAddrStr, expectedInterface) } -func (k *Keeper) CosmwasmAPI(ctx sdk.Context) wasmvm.GoAPI { +func (g cosmwasmAPIGeneratorImpl) Generate(ctx *sdk.Context) wasmvm.GoAPI { x := cosmwasmAPIImpl{ - keeper: k, - ctx: &ctx, + keeper: g.keeper, + ctx: ctx, + wasmAPI: wasmkeeper.NewCosmwasmAPIImpl(&g.keeper.Keeper, ctx), } return wasmvm.GoAPI{ HumanAddress: wasmkeeper.HumanAddress, diff --git a/x/wasmplus/keeper/api_test.go b/x/wasmplus/keeper/api_test.go index 5a05788cd0..0388a4b7e0 100644 --- a/x/wasmplus/keeper/api_test.go +++ b/x/wasmplus/keeper/api_test.go @@ -8,6 +8,7 @@ import ( sdk "github.com/line/lbm-sdk/types" wasmvm "github.com/line/wasmvm" + "github.com/line/wasmd/x/wasm/keeper" wasmkeeper "github.com/line/wasmd/x/wasm/keeper" wasmvmtypes "github.com/line/wasmvm/types" "github.com/stretchr/testify/assert" @@ -16,7 +17,7 @@ import ( func newAPI(t *testing.T) wasmvm.GoAPI { ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) - return keepers.WasmKeeper.CosmwasmAPI(ctx) + return keepers.WasmKeeper.GetCosmwasmAPIGenerator().Generate(&ctx) } func TestAPIHumanAddress(t *testing.T) { @@ -65,17 +66,18 @@ func TestAPICanonicalAddress(t *testing.T) { func TestCallCallablePoint(t *testing.T) { // prepare ctx and keeper ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) // instantiate an events contract numberWasm, err := ioutil.ReadFile("../testdata/events.wasm") require.NoError(t, err) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) - em := sdk.NewEventManager() - codeID, _, err := keepers.ContractKeeper.Create(ctx.WithEventManager(em), creator, numberWasm, nil) + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, numberWasm, nil) require.NoError(t, err) initMsg := []byte(`{}`) - contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), codeID, creator, nil, initMsg, "events", nil) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsg, "events", nil) require.NoError(t, err) callstack := []sdk.AccAddress{RandomAccountAddress(t), RandomAccountAddress(t)} callstackBin, err := json.Marshal(callstack) @@ -83,7 +85,7 @@ func TestCallCallablePoint(t *testing.T) { var gasLimit uint64 = keepers.WasmKeeper.GetGasRegister().ToWasmVMGas(400_000) // prepare API - api := keepers.WasmKeeper.CosmwasmAPI(ctx.WithEventManager(em)) + api := keepers.WasmKeeper.GetCosmwasmAPIGenerator().Generate(&ctx) // prepare arg for succeed eventsIn := wasmvmtypes.Events{ @@ -128,7 +130,7 @@ func TestCallCallablePoint(t *testing.T) { require.NoError(t, err) assert.Equal(t, []byte(`null`), res) - eventsExpected, err := newCustomCallablePointEvents(eventsIn, contractAddr, callstackBin) + eventsExpected, err := keeper.NewCustomCallablePointEvents(eventsIn, contractAddr, callstackBin) require.NoError(t, err) for _, e := range eventsExpected { assert.Contains(t, em.Events(), e) @@ -230,21 +232,22 @@ func TestCallCallablePoint(t *testing.T) { func TestValidateDynamicLinkInterface(t *testing.T) { // prepare ctx and keeper ctx, keepers := CreateTestInput(t, false, AvailableCapabilities) + em := sdk.NewEventManager() + ctx = ctx.WithEventManager(em) // instantiate an events contract numberWasm, err := ioutil.ReadFile("../testdata/events.wasm") require.NoError(t, err) deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) creator := keepers.Faucet.NewFundedRandomAccount(ctx, deposit...) - em := sdk.NewEventManager() - codeID, _, err := keepers.ContractKeeper.Create(ctx.WithEventManager(em), creator, numberWasm, nil) + codeID, _, err := keepers.ContractKeeper.Create(ctx, creator, numberWasm, nil) require.NoError(t, err) initMsg := []byte(`{}`) - contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx.WithEventManager(em), codeID, creator, nil, initMsg, "events", nil) + contractAddr, _, err := keepers.ContractKeeper.Instantiate(ctx, codeID, creator, nil, initMsg, "events", nil) require.NoError(t, err) // prepare API - api := keepers.WasmKeeper.CosmwasmAPI(ctx.WithEventManager(em)) + api := keepers.WasmKeeper.GetCosmwasmAPIGenerator().Generate(&ctx) t.Run("succeed valid", func(t *testing.T) { validInterface := []byte(`[{"name":"add_event_dyn","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_events_dyn","ty":{"params":["I32","I32"],"results":[]}},{"name":"add_attribute_dyn","ty":{"params":["I32","I32","I32"],"results":[]}},{"name":"add_attributes_dyn","ty":{"params":["I32","I32"],"results":[]}}]`) diff --git a/x/wasmplus/keeper/bench_test.go b/x/wasmplus/keeper/bench_test.go index 153024ecdb..2482c205d6 100644 --- a/x/wasmplus/keeper/bench_test.go +++ b/x/wasmplus/keeper/bench_test.go @@ -15,7 +15,7 @@ func BenchmarkAPI(b *testing.B) { wasmConfig := types.WasmConfig{MemoryCacheSize: 0} ctx, keepers := createTestInput(b, false, AvailableCapabilities, wasmConfig, dbm.NewMemDB()) example := InstantiateHackatomExampleContract(b, ctx, keepers) - api := keepers.WasmKeeper.CosmwasmAPI(ctx) + api := keepers.WasmKeeper.GetCosmwasmAPIGenerator().Generate(&ctx) addrStr := example.Contract.String() addrBytes, err := sdk.AccAddressFromBech32(example.Contract.String()) require.NoError(b, err) diff --git a/x/wasmplus/keeper/events.go b/x/wasmplus/keeper/events.go deleted file mode 100644 index 8c387fe2d1..0000000000 --- a/x/wasmplus/keeper/events.go +++ /dev/null @@ -1,50 +0,0 @@ -package keeper - -import ( - "fmt" - "strings" - - sdk "github.com/line/lbm-sdk/types" - sdkerrors "github.com/line/lbm-sdk/types/errors" - wasmvmtypes "github.com/line/wasmvm/types" - - wasmkeeper "github.com/line/wasmd/x/wasm/keeper" - "github.com/line/wasmd/x/wasm/types" - wasmplustypes "github.com/line/wasmd/x/wasmplus/types" -) - -func newCallablePointEvent(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress, callstack []byte) (sdk.Events, error) { - attrs, err := callablePointSDKEventAttributes(customAttributes, contractAddr, callstack) - - if err != nil { - return nil, err - } - - return sdk.Events{sdk.NewEvent(wasmplustypes.CallablePointEventType, attrs...)}, nil -} - -func newCustomCallablePointEvents(evts wasmvmtypes.Events, contractAddr sdk.AccAddress, callstack []byte) (sdk.Events, error) { - events := make(sdk.Events, 0, len(evts)) - for _, e := range evts { - typ := strings.TrimSpace(e.Type) - if len(typ) <= wasmkeeper.EventTypeMinLength { - return nil, sdkerrors.Wrap(types.ErrInvalidEvent, fmt.Sprintf("Event type too short: '%s'", typ)) - } - attributes, err := callablePointSDKEventAttributes(e.Attributes, contractAddr, callstack) - if err != nil { - return nil, err - } - events = append(events, sdk.NewEvent(fmt.Sprintf("%s%s", wasmplustypes.CustomCallablePointEventPrefix, typ), attributes...)) - } - return events, nil -} - -func callablePointSDKEventAttributes(customAttributes []wasmvmtypes.EventAttribute, contractAddr sdk.AccAddress, callstack []byte) ([]sdk.Attribute, error) { - attrs, err := wasmkeeper.ContractSDKEventAttributes(customAttributes, contractAddr) - if err != nil { - return nil, err - } - // attrs[0] is addr - attrs = append([]sdk.Attribute{attrs[0], sdk.NewAttribute(wasmplustypes.AttributeKeyCallstack, string(callstack))}, attrs[1:]...) - return attrs, nil -} diff --git a/x/wasmplus/keeper/events_test.go b/x/wasmplus/keeper/events_test.go deleted file mode 100644 index c1d8d72c6a..0000000000 --- a/x/wasmplus/keeper/events_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package keeper - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - sdk "github.com/line/lbm-sdk/types" - wasmvmtypes "github.com/line/wasmvm/types" -) - -func TestNewCustomCallablePointEvents(t *testing.T) { - myContract := RandomAccountAddress(t) - myCallstack := []sdk.AccAddress{RandomAccountAddress(t), RandomAccountAddress(t)} - myCallstackBinary, err := json.Marshal(myCallstack) - require.NoError(t, err) - specs := map[string]struct { - src wasmvmtypes.Events - exp sdk.Events - isError bool - }{ - "all good": { - src: wasmvmtypes.Events{{ - Type: "foo", - Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}}, - }}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-foo", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("myKey", "myVal"))}, - }, - "multiple attributes": { - src: wasmvmtypes.Events{{ - Type: "foo", - Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}, - {Key: "myOtherKey", Value: "myOtherVal"}}, - }}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-foo", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("myKey", "myVal"), - sdk.NewAttribute("myOtherKey", "myOtherVal"))}, - }, - "multiple events": { - src: wasmvmtypes.Events{{ - Type: "foo", - Attributes: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}}, - }, { - Type: "bar", - Attributes: []wasmvmtypes.EventAttribute{{Key: "otherKey", Value: "otherVal"}}, - }}, - exp: sdk.Events{ - sdk.NewEvent("wasm-callablepoint-foo", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("myKey", "myVal")), - sdk.NewEvent("wasm-callablepoint-bar", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("otherKey", "otherVal")), - }, - }, - "without attributes": { - src: wasmvmtypes.Events{{ - Type: "foo", - }}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-foo", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary))), - }, - }, - "error on short event type": { - src: wasmvmtypes.Events{{ - Type: "f", - }}, - isError: true, - }, - "error on _contract_address": { - src: wasmvmtypes.Events{{ - Type: "foo", - Attributes: []wasmvmtypes.EventAttribute{{Key: "_contract_address", Value: RandomBech32AccountAddress(t)}}, - }}, - isError: true, - }, - "error on reserved prefix": { - src: wasmvmtypes.Events{{ - Type: "wasm", - Attributes: []wasmvmtypes.EventAttribute{ - {Key: "_reserved", Value: "is skipped"}, - {Key: "normal", Value: "is used"}}, - }}, - isError: true, - }, - "error on empty value": { - src: wasmvmtypes.Events{{ - Type: "boom", - Attributes: []wasmvmtypes.EventAttribute{ - {Key: "some", Value: "data"}, - {Key: "key", Value: ""}, - }, - }}, - isError: true, - }, - "error on empty key": { - src: wasmvmtypes.Events{{ - Type: "boom", - Attributes: []wasmvmtypes.EventAttribute{ - {Key: "some", Value: "data"}, - {Key: "", Value: "value"}, - }, - }}, - isError: true, - }, - "error on whitespace type": { - src: wasmvmtypes.Events{{ - Type: " f ", - Attributes: []wasmvmtypes.EventAttribute{ - {Key: "some", Value: "data"}, - }, - }}, - isError: true, - }, - "error on only whitespace key": { - src: wasmvmtypes.Events{{ - Type: "boom", - Attributes: []wasmvmtypes.EventAttribute{ - {Key: "some", Value: "data"}, - {Key: "\n\n\n\n", Value: "value"}, - }, - }}, - isError: true, - }, - "error on only whitespace value": { - src: wasmvmtypes.Events{{ - Type: "boom", - Attributes: []wasmvmtypes.EventAttribute{ - {Key: "some", Value: "data"}, - {Key: "myKey", Value: " \t\r\n"}, - }, - }}, - isError: true, - }, - "strip out whitespace": { - src: wasmvmtypes.Events{{ - Type: " food\n", - Attributes: []wasmvmtypes.EventAttribute{{Key: "my Key", Value: "\tmyVal"}}, - }}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint-food", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - - sdk.NewAttribute("my Key", "myVal"))}, - }, - "empty event elements": { - src: make(wasmvmtypes.Events, 10), - isError: true, - }, - "nil": { - exp: sdk.Events{}, - }, - } - for name, spec := range specs { - t.Run(name, func(t *testing.T) { - gotEvent, err := newCustomCallablePointEvents(spec.src, myContract, myCallstackBinary) - if spec.isError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, spec.exp, gotEvent) - } - }) - } -} - -func TestNewCallablePointEvent(t *testing.T) { - myContract := RandomAccountAddress(t) - myCallstack := []sdk.AccAddress{RandomAccountAddress(t), RandomAccountAddress(t)} - myCallstackBinary, err := json.Marshal(myCallstack) - require.NoError(t, err) - specs := map[string]struct { - src []wasmvmtypes.EventAttribute - exp sdk.Events - isError bool - }{ - "all good": { - src: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("myKey", "myVal"))}, - }, - "multiple attributes": { - src: []wasmvmtypes.EventAttribute{{Key: "myKey", Value: "myVal"}, - {Key: "myOtherKey", Value: "myOtherVal"}}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("myKey", "myVal"), - sdk.NewAttribute("myOtherKey", "myOtherVal"))}, - }, - "without attributes": { - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", - sdk.NewAttribute("_contract_address", myContract.String()), sdk.NewAttribute("_callstack", string(myCallstackBinary))), - }, - }, - "error on _contract_address": { - src: []wasmvmtypes.EventAttribute{{Key: "_contract_address", Value: RandomBech32AccountAddress(t)}}, - isError: true, - }, - "error on whitespace key": { - src: []wasmvmtypes.EventAttribute{{Key: " ", Value: "value"}}, - isError: true, - }, - "error on whitespace value": { - src: []wasmvmtypes.EventAttribute{{Key: "key", Value: "\n\n\n"}}, - isError: true, - }, - "strip whitespace": { - src: []wasmvmtypes.EventAttribute{{Key: " my-real-key ", Value: "\n\n\nsome-val\t\t\t"}}, - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - sdk.NewAttribute("my-real-key", "some-val"))}, - }, - "empty elements": { - src: make([]wasmvmtypes.EventAttribute, 10), - isError: true, - }, - "nil": { - exp: sdk.Events{sdk.NewEvent("wasm-callablepoint", - sdk.NewAttribute("_contract_address", myContract.String()), - sdk.NewAttribute("_callstack", string(myCallstackBinary)), - )}, - }, - } - for name, spec := range specs { - t.Run(name, func(t *testing.T) { - gotEvent, err := newCallablePointEvent(spec.src, myContract, myCallstackBinary) - if spec.isError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, spec.exp, gotEvent) - } - }) - } -} diff --git a/x/wasmplus/keeper/keeper.go b/x/wasmplus/keeper/keeper.go index f8974765f5..41695f13b1 100644 --- a/x/wasmplus/keeper/keeper.go +++ b/x/wasmplus/keeper/keeper.go @@ -69,9 +69,9 @@ func NewKeeper( homeDir, wasmConfig, availableCapabilities, - &result, opts..., ) + result.SetCosmwasmAPIGenerator(cosmwasmAPIGeneratorImpl{keeper: &result}) return result } diff --git a/x/wasmplus/module_test.go b/x/wasmplus/module_test.go index b5df999ed8..8178f6aabb 100644 --- a/x/wasmplus/module_test.go +++ b/x/wasmplus/module_test.go @@ -15,6 +15,7 @@ import ( "github.com/line/lbm-sdk/types/module" authkeeper "github.com/line/lbm-sdk/x/auth/keeper" bankkeeper "github.com/line/lbm-sdk/x/bank/keeper" + govkeeper "github.com/line/lbm-sdk/x/gov/keeper" stakingkeeper "github.com/line/lbm-sdk/x/staking/keeper" "github.com/line/ostracon/crypto" "github.com/line/ostracon/crypto/ed25519" @@ -33,6 +34,7 @@ type testData struct { bankKeeper bankkeeper.Keeper stakingKeeper stakingkeeper.Keeper faucet *wasmkeeper.TestFaucet + govKeeper govkeeper.Keeper } func setupTest(t *testing.T) testData { @@ -46,6 +48,7 @@ func setupTest(t *testing.T) testData { bankKeeper: keepers.BankKeeper, stakingKeeper: keepers.StakingKeeper, faucet: keepers.Faucet, + govKeeper: keepers.GovKeeper, } return data }