diff --git a/x/wasm/alias.go b/x/wasm/alias.go new file mode 100644 index 00000000..b89d1283 --- /dev/null +++ b/x/wasm/alias.go @@ -0,0 +1,73 @@ +// nolint +package wasm + +import ( + "github.com/cosmos/modules/incubator/wasm/internal/keeper" + "github.com/cosmos/modules/incubator/wasm/internal/types" +) + +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/cosmos/modules/incubator/wasm/internal/types/ +// ALIASGEN: github.com/cosmos/modules/incubator/wasm/internal/keeper/ + +const ( + DefaultCodespace = types.DefaultCodespace + CodeCreatedFailed = types.CodeCreatedFailed + CodeAccountExists = types.CodeAccountExists + CodeInstantiateFailed = types.CodeInstantiateFailed + CodeExecuteFailed = types.CodeExecuteFailed + CodeGasLimit = types.CodeGasLimit + ModuleName = types.ModuleName + StoreKey = types.StoreKey + TStoreKey = types.TStoreKey + QuerierRoute = types.QuerierRoute + RouterKey = types.RouterKey + MaxWasmSize = types.MaxWasmSize + GasMultiplier = keeper.GasMultiplier + MaxGas = keeper.MaxGas + QueryListContracts = keeper.QueryListContracts + QueryGetContract = keeper.QueryGetContract + QueryGetContractState = keeper.QueryGetContractState + QueryGetCode = keeper.QueryGetCode + QueryListCode = keeper.QueryListCode +) + +var ( + // functions aliases + RegisterCodec = types.RegisterCodec + ErrCreateFailed = types.ErrCreateFailed + ErrAccountExists = types.ErrAccountExists + ErrInstantiateFailed = types.ErrInstantiateFailed + ErrExecuteFailed = types.ErrExecuteFailed + ErrGasLimit = types.ErrGasLimit + GetCodeKey = types.GetCodeKey + GetContractAddressKey = types.GetContractAddressKey + GetContractStorePrefixKey = types.GetContractStorePrefixKey + NewCodeInfo = types.NewCodeInfo + NewParams = types.NewParams + NewWasmCoins = types.NewWasmCoins + NewContract = types.NewContract + CosmosResult = types.CosmosResult + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + MakeTestCodec = keeper.MakeTestCodec + CreateTestInput = keeper.CreateTestInput + + // variable aliases + ModuleCdc = types.ModuleCdc + KeyLastCodeID = types.KeyLastCodeID + KeyLastInstanceID = types.KeyLastInstanceID + CodeKeyPrefix = types.CodeKeyPrefix + ContractKeyPrefix = types.ContractKeyPrefix + ContractStorePrefix = types.ContractStorePrefix +) + +type ( + MsgStoreCode = types.MsgStoreCode + MsgInstantiateContract = types.MsgInstantiateContract + MsgExecuteContract = types.MsgExecuteContract + CodeInfo = types.CodeInfo + Contract = types.Contract + Keeper = keeper.Keeper +) diff --git a/x/wasm/client/cli/tx.go b/x/wasm/client/cli/tx.go new file mode 100644 index 00000000..29878fd7 --- /dev/null +++ b/x/wasm/client/cli/tx.go @@ -0,0 +1,155 @@ +package client + +import ( + "bufio" + "strconv" + "io/ioutil" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth" + + "github.com/cosmos/modules/incubator/wasm/internal/types" +) + +const ( + flagTo = "to" + flagAmount = "amount" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Wasm transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: utils.ValidateCmd, + } + txCmd.AddCommand( + StoreCodeCmd(cdc), + // InstantiateContractCmd(cdc), + // ExecuteContractCmd(cdc), + ) + return txCmd +} + +// StoreCodeCmd will upload code to be reused. +func StoreCodeCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "store [from_key_or_address] [wasm file]", + Short: "Upload a wasm binary", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + // parse coins trying to be sent + wasm, err := ioutil.ReadFile(args[1]) + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgStoreCode{ + Sender: cliCtx.GetFromAddress(), + WASMByteCode: wasm, + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd = client.PostCommands(cmd)[0] + + return cmd +} + +// // InstantiateContractCmd will instantiate a contract from previously uploaded code. +// func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command { +// cmd := &cobra.Command{ +// Use: "create [from_key_or_address] [code_id_int64] [coins] [json_encoded_init_args]", +// Short: "Instantiate a wasm contract", +// Args: cobra.ExactArgs(4), +// RunE: func(cmd *cobra.Command, args []string) error { +// txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) +// cliCtx := context.NewCLIContextWithFrom(args[0]). +// WithCodec(cdc). +// WithAccountDecoder(cdc) + +// // get the id of the code to instantiate +// codeID, err := strconv.Atoi(args[1]) +// if err != nil { +// return err +// } + +// // parse coins trying to be sent +// coins, err := sdk.ParseCoins(args[2]) +// if err != nil { +// return err +// } + +// initMsg := args[3] + +// // build and sign the transaction, then broadcast to Tendermint +// msg := MsgCreateContract{ +// Sender: cliCtx.GetFromAddress(), +// Code: CodeID(codeID), +// InitFunds: coins, +// InitMsg: []byte(initMsg), +// } +// return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) +// }, +// } + +// cmd = client.PostCommands(cmd)[0] + +// return cmd +// } + +// // ExecuteContractCmd will instantiate a contract from previously uploaded code. +// func ExecuteContractCmd(cdc *codec.Codec) *cobra.Command { +// cmd := &cobra.Command{ +// Use: "send [from_key_or_address] [contract_addr_bech32] [coins] [json_encoded_send_args]", +// Short: "Instantiate a wasm contract", +// Args: cobra.ExactArgs(4), +// RunE: func(cmd *cobra.Command, args []string) error { +// txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) +// cliCtx := context.NewCLIContextWithFrom(args[0]). +// WithCodec(cdc). +// WithAccountDecoder(cdc) + +// // get the id of the code to instantiate +// contractAddr, err := sdk.AccAddressFromBech32(args[1]) +// if err != nil { +// return err +// } + +// // parse coins trying to be sent +// coins, err := sdk.ParseCoins(args[2]) +// if err != nil { +// return err +// } + +// sendMsg := args[3] + +// // build and sign the transaction, then broadcast to Tendermint +// msg := MsgSendContract{ +// Sender: cliCtx.GetFromAddress(), +// Contract: contractAddr, +// Payment: coins, +// Msg: []byte(sendMsg), +// } +// return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) +// }, +// } + +// cmd = client.PostCommands(cmd)[0] + +// return cmd +// } diff --git a/x/wasm/genesis.go b/x/wasm/genesis.go new file mode 100644 index 00000000..1d6e31d9 --- /dev/null +++ b/x/wasm/genesis.go @@ -0,0 +1,29 @@ +package wasm + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + // authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + // "github.com/cosmos/modules/incubator/wasm/internal/types" +) + +type GenesisState struct { + // TODO +} + +// InitGenesis sets supply information for genesis. +// +// CONTRACT: all types of accounts must have been already initialized/created +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + // TODO +} + +// ExportGenesis returns a GenesisState for a given context and keeper. +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + return GenesisState{} +} + +// ValidateGenesis performs basic validation of supply genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return nil +} diff --git a/x/wasm/handler.go b/x/wasm/handler.go new file mode 100644 index 00000000..a01ca8e7 --- /dev/null +++ b/x/wasm/handler.go @@ -0,0 +1,99 @@ +package wasm + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + AttributeKeyContract = "contract_address" + AttributeKeyCodeID = "code_id" +) + +// NewHandler returns a handler for "bank" type messages. +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + switch msg := msg.(type) { + case MsgStoreCode: + return handleStoreCode(ctx, k, msg) + + case MsgInstantiateContract: + return handleInstantiate(ctx, k, msg) + + case MsgExecuteContract: + return handleExecute(ctx, k, msg) + + default: + errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg) + return sdk.ErrUnknownRequest(errMsg).Result() + } + } +} + +func handleStoreCode(ctx sdk.Context, k Keeper, msg MsgStoreCode) sdk.Result { + codeID, err := k.Create(ctx, msg.Sender, msg.WASMByteCode) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(sdk.AttributeKeyAction, "store-code"), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), + sdk.NewAttribute(AttributeKeyCodeID, fmt.Sprintf("%d", codeID)), + ), + ) + + return sdk.Result{ + Data: []byte(fmt.Sprintf("%d", codeID)), + Events: ctx.EventManager().Events(), + } +} + +func handleInstantiate(ctx sdk.Context, k Keeper, msg MsgInstantiateContract) sdk.Result { + contractAddr, err := k.Instantiate(ctx, msg.Sender, msg.Code, msg.InitMsg, msg.InitFunds) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(sdk.AttributeKeyAction, "instantiate"), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), + sdk.NewAttribute(AttributeKeyCodeID, fmt.Sprintf("%d", msg.Code)), + sdk.NewAttribute(AttributeKeyContract, contractAddr.String()), + ), + ) + + return sdk.Result{ + Data: contractAddr, + Events: ctx.EventManager().Events(), + } +} + +func handleExecute(ctx sdk.Context, k Keeper, msg MsgExecuteContract) sdk.Result { + res, err := k.Execute(ctx, msg.Contract, msg.Sender, msg.SentFunds, msg.Msg) + if err != nil { + return err.Result() + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(sdk.AttributeKeyAction, "execute"), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()), + sdk.NewAttribute(AttributeKeyContract, msg.Contract.String()), + ), + ) + + res.Events = append(res.Events, ctx.EventManager().Events()...) + return res +} diff --git a/x/wasm/internal/keeper/keeper.go b/x/wasm/internal/keeper/keeper.go new file mode 100644 index 00000000..882b13e1 --- /dev/null +++ b/x/wasm/internal/keeper/keeper.go @@ -0,0 +1,348 @@ +package keeper + +import ( + "encoding/binary" + "fmt" + "path/filepath" + + wasm "github.com/confio/go-cosmwasm" + wasmTypes "github.com/confio/go-cosmwasm/types" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/tendermint/tendermint/crypto" + + "github.com/cosmos/modules/incubator/wasm/internal/types" +) + +// GasMultiplier is how many cosmwasm gas points = 1 sdk gas point +// SDK reference costs can be found here: https://github.com/cosmos/cosmos-sdk/blob/02c6c9fafd58da88550ab4d7d494724a477c8a68/store/types/gas.go#L153-L164 +// A write at ~3000 gas and ~200us = 10 gas per us (microsecond) cpu/io +// Rough timing have 88k gas at 90us, which is equal to 1k sdk gas... (one read) +const GasMultiplier = 100 + +// MaxGas for a contract is 900 million (enforced in rust) +const MaxGas = 900_000_000 + +// Keeper will have a reference to Wasmer with it's own data directory. +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + accountKeeper auth.AccountKeeper + bankKeeper bank.Keeper + + router sdk.Router + + wasmer wasm.Wasmer +} + +// NewKeeper creates a new contract Keeper instance +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, accountKeeper auth.AccountKeeper, bankKeeper bank.Keeper, router sdk.Router, homeDir string) Keeper { + wasmer, err := wasm.NewWasmer(filepath.Join(homeDir, "wasm"), 3) + if err != nil { + panic(err) + } + + return Keeper{ + storeKey: storeKey, + cdc: cdc, + wasmer: *wasmer, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + router: router, + } +} + +// Create uploads and compiles a WASM contract, returning a short identifier for the contract +func (k Keeper) Create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte) (codeID uint64, sdkErr sdk.Error) { + codeHash, err := k.wasmer.Create(wasmCode) + if err != nil { + return 0, types.ErrCreateFailed(err) + } + + store := ctx.KVStore(k.storeKey) + codeID = k.autoIncrementID(ctx, types.KeyLastCodeID) + contractInfo := types.NewCodeInfo(codeHash, creator) + // 0x01 | codeID (uint64) -> ContractInfo + store.Set(types.GetCodeKey(codeID), k.cdc.MustMarshalBinaryBare(contractInfo)) + + return codeID, nil +} + +// Instantiate creates an instance of a WASM contract +func (k Keeper) Instantiate(ctx sdk.Context, creator sdk.AccAddress, codeID uint64, initMsg []byte, deposit sdk.Coins) (sdk.AccAddress, sdk.Error) { + // create contract address + contractAddress := k.generateContractAddress(ctx, codeID) + existingAccnt := k.accountKeeper.GetAccount(ctx, contractAddress) + if existingAccnt != nil { + return nil, types.ErrAccountExists(existingAccnt.GetAddress()) + } + + // deposit initial contract funds + sdkerr := k.bankKeeper.SendCoins(ctx, creator, contractAddress, deposit) + if sdkerr != nil { + return nil, sdkerr + } + contractAccount := k.accountKeeper.GetAccount(ctx, contractAddress) + + // get contact info + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.GetCodeKey(codeID)) + var codeInfo types.CodeInfo + if bz != nil { + k.cdc.MustUnmarshalBinaryBare(bz, &codeInfo) + } + + // prepare params for contract instantiate call + params := types.NewParams(ctx, creator, deposit, contractAccount) + + // create prefixed data store + // 0x03 | contractAddress (sdk.AccAddress) + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + + // instantiate wasm contract + gas := gasForContract(ctx) + res, err := k.wasmer.Instantiate(codeInfo.CodeHash, params, initMsg, prefixStore, gas) + if err != nil { + return contractAddress, types.ErrInstantiateFailed(err) + } + consumeGas(ctx, res.GasUsed) + + sdkerr = k.dispatchMessages(ctx, contractAccount, res.Messages) + if sdkerr != nil { + return nil, sdkerr + } + + // persist instance + instance := types.NewContract(codeID, creator, initMsg, prefixStore) + // 0x02 | contractAddress (sdk.AccAddress) -> Instance + store.Set(types.GetContractAddressKey(contractAddress), k.cdc.MustMarshalBinaryBare(instance)) + + return contractAddress, nil +} + +// Execute executes the contract instance +func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, coins sdk.Coins, msgs []byte) (sdk.Result, sdk.Error) { + store := ctx.KVStore(k.storeKey) + + var contract types.Contract + contractBz := store.Get(types.GetContractAddressKey(contractAddress)) + if contractBz != nil { + k.cdc.MustUnmarshalBinaryBare(contractBz, &contract) + } + + var codeInfo types.CodeInfo + contractInfoBz := store.Get(types.GetCodeKey(contract.CodeID)) + if contractInfoBz != nil { + k.cdc.MustUnmarshalBinaryBare(contractInfoBz, &codeInfo) + } + + // add more funds + sdkerr := k.bankKeeper.SendCoins(ctx, caller, contractAddress, coins) + if sdkerr != nil { + return sdk.Result{}, sdkerr + } + contractAccount := k.accountKeeper.GetAccount(ctx, contractAddress) + params := types.NewParams(ctx, caller, coins, contractAccount) + + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + + gas := gasForContract(ctx) + res, err := k.wasmer.Execute(codeInfo.CodeHash, params, msgs, prefixStore, gas) + if err != nil { + return sdk.Result{}, types.ErrExecuteFailed(err) + } + consumeGas(ctx, res.GasUsed) + + sdkerr = k.dispatchMessages(ctx, contractAccount, res.Messages) + if sdkerr != nil { + return sdk.Result{}, sdkerr + } + + return types.CosmosResult(*res), nil +} + +func (k Keeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.Contract { + store := ctx.KVStore(k.storeKey) + var contract types.Contract + contractBz := store.Get(types.GetContractAddressKey(contractAddress)) + if contractBz == nil { + return nil + } + k.cdc.MustUnmarshalBinaryBare(contractBz, &contract) + return &contract +} + +func (k Keeper) ListContractInfo(ctx sdk.Context, cb func(sdk.AccAddress, types.Contract) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ContractKeyPrefix) + iter := prefixStore.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + var contract types.Contract + k.cdc.MustUnmarshalBinaryBare(iter.Value(), &contract) + // cb returns true to stop early + if cb(iter.Key(), contract) { + break + } + } +} + +func (k Keeper) GetContractState(ctx sdk.Context, contractAddress sdk.AccAddress) sdk.Iterator { + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + return prefixStore.Iterator(nil, nil) +} + +func (k Keeper) GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo { + store := ctx.KVStore(k.storeKey) + var codeInfo types.CodeInfo + codeInfoBz := store.Get(types.GetCodeKey(codeID)) + if codeInfoBz == nil { + return nil + } + k.cdc.MustUnmarshalBinaryBare(codeInfoBz, &codeInfo) + return &codeInfo +} + +func (k Keeper) GetByteCode(ctx sdk.Context, codeID uint64) ([]byte, error) { + store := ctx.KVStore(k.storeKey) + var codeInfo types.CodeInfo + codeInfoBz := store.Get(types.GetCodeKey(codeID)) + if codeInfoBz == nil { + return nil, nil + } + k.cdc.MustUnmarshalBinaryBare(codeInfoBz, &codeInfo) + return k.wasmer.GetCode(codeInfo.CodeHash) +} + +func (k Keeper) dispatchMessages(ctx sdk.Context, contract exported.Account, msgs []wasmTypes.CosmosMsg) sdk.Error { + for _, msg := range msgs { + if err := k.dispatchMessage(ctx, contract, msg); err != nil { + return err + } + } + return nil +} + +func (k Keeper) dispatchMessage(ctx sdk.Context, contract exported.Account, msg wasmTypes.CosmosMsg) sdk.Error { + // we check each type (pointers would make it easier to test if set) + if msg.Send.FromAddress != "" { + sendMsg, err := convertCosmosSendMsg(msg.Send) + if err != nil { + return err + } + return k.handleSdkMessage(ctx, contract, sendMsg) + } else if msg.Contract.ContractAddr != "" { + targetAddr, stderr := sdk.AccAddressFromBech32(msg.Contract.ContractAddr) + if stderr != nil { + return sdk.ErrInvalidAddress(msg.Contract.ContractAddr) + } + // TODO: use non nil payment once we update go-cosmwasm (ContractMsg contains optional payment) + _, err := k.Execute(ctx, targetAddr, contract.GetAddress(), nil, []byte(msg.Contract.Msg)) + if err != nil { + return err + } + } else if msg.Opaque.Data != "" { + // TODO: handle opaque + panic("dispatch opaque message not yet implemented") + } + // what is it? + panic(fmt.Sprintf("Unknown CosmosMsg: %#v", msg)) +} + +func convertCosmosSendMsg(msg wasmTypes.SendMsg) (bank.MsgSend, sdk.Error) { + fromAddr, stderr := sdk.AccAddressFromBech32(msg.FromAddress) + if stderr != nil { + return bank.MsgSend{}, sdk.ErrInvalidAddress(msg.FromAddress) + } + toAddr, stderr := sdk.AccAddressFromBech32(msg.ToAddress) + if stderr != nil { + return bank.MsgSend{}, sdk.ErrInvalidAddress(msg.ToAddress) + } + + var coins sdk.Coins + for _, coin := range msg.Amount { + amount, ok := sdk.NewIntFromString(coin.Amount) + if !ok { + return bank.MsgSend{}, sdk.ErrInvalidCoins(coin.Amount + coin.Denom) + } + c := sdk.Coin{ + Denom: coin.Denom, + Amount: amount, + } + coins = append(coins, c) + } + sendMsg := bank.MsgSend{ + FromAddress: fromAddr, + ToAddress: toAddr, + Amount: coins, + } + return sendMsg, nil +} + +func (k Keeper) handleSdkMessage(ctx sdk.Context, contract exported.Account, msg sdk.Msg) sdk.Error { + // make sure this account can send it + contractAddr := contract.GetAddress() + for _, acct := range msg.GetSigners() { + if !acct.Equals(contractAddr) { + return sdk.ErrUnauthorized("contract doesn't have permission") + } + } + + // find the handler and execute it + h := k.router.Route(msg.Route()) + if h == nil { + return sdk.ErrUnknownRequest(msg.Route()) + } + res := h(ctx, msg) + if !res.IsOK() { + return sdk.NewError(res.Codespace, res.Code, res.Log) + } + return nil +} + +func gasForContract(ctx sdk.Context) uint64 { + meter := ctx.GasMeter() + remaining := (meter.Limit() - meter.GasConsumed()) * GasMultiplier + if remaining > MaxGas { + return MaxGas + } + return remaining +} + +func consumeGas(ctx sdk.Context, gas uint64) { + consumed := gas / GasMultiplier + ctx.GasMeter().ConsumeGas(consumed, "wasm contract") +} + +// generates a contract address from codeID + instanceID +func (k Keeper) generateContractAddress(ctx sdk.Context, codeID uint64) sdk.AccAddress { + instanceID := k.autoIncrementID(ctx, types.KeyLastInstanceID) + // NOTE: It is possible to get a duplicate address if either codeID or instanceID + // overflow 32 bits. This is highly improbable, but something that could be refactored. + contractID := codeID<<32 + instanceID + return addrFromUint64(contractID) +} + +func (k Keeper) autoIncrementID(ctx sdk.Context, lastIDKey []byte) uint64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(lastIDKey) + id := uint64(1) + if bz != nil { + id = binary.BigEndian.Uint64(bz) + } + bz = sdk.Uint64ToBigEndian(id + 1) + store.Set(lastIDKey, bz) + return id +} + +func addrFromUint64(id uint64) sdk.AccAddress { + addr := make([]byte, 20) + addr[0] = 'C' + binary.PutUvarint(addr[1:], id) + return sdk.AccAddress(crypto.AddressHash(addr)) +} diff --git a/x/wasm/internal/keeper/keeper_test.go b/x/wasm/internal/keeper/keeper_test.go new file mode 100644 index 00000000..8dbbc458 --- /dev/null +++ b/x/wasm/internal/keeper/keeper_test.go @@ -0,0 +1,176 @@ +package keeper + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +func TestNewKeeper(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + _, _, keeper := CreateTestInput(t, false, tempDir) + require.NotNil(t, keeper) +} + +func TestCreate(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) +} + +func TestInstantiate(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode) + require.NoError(t, err) + + initMsg := InitMsg{ + Verifier: "fred", + Beneficiary: "bob", + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + gasBefore := ctx.GasMeter().GasConsumed() + + // create with no balance is also legal + addr, err := keeper.Instantiate(ctx, creator, contractID, initMsgBz, nil) + require.NoError(t, err) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String()) + + gasAfter := ctx.GasMeter().GasConsumed() + kvStoreGas := uint64(28757) // calculated by disabling contract gas reduction and running test + require.Equal(t, kvStoreGas+285, gasAfter-gasBefore) +} + +func TestExecute(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, accKeeper, keeper := CreateTestInput(t, false, tempDir) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred.String(), + Beneficiary: bob.String(), + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, err := keeper.Instantiate(ctx, creator, contractID, initMsgBz, deposit) + require.NoError(t, err) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String()) + + // ensure bob doesn't exist + bobAcct := accKeeper.GetAccount(ctx, bob) + require.Nil(t, bobAcct) + + // ensure funder has reduced balance + creatorAcct := accKeeper.GetAccount(ctx, creator) + require.NotNil(t, creatorAcct) + // we started at 2*deposit, should have spent one above + assert.Equal(t, deposit, creatorAcct.GetCoins()) + + // ensure contract has updated balance + contractAcct := accKeeper.GetAccount(ctx, addr) + require.NotNil(t, contractAcct) + assert.Equal(t, deposit, contractAcct.GetCoins()) + + // unauthorized - trialCtx so we don't change state + trialCtx := ctx.WithMultiStore(ctx.MultiStore().CacheWrap().(sdk.MultiStore)) + res, err := keeper.Execute(trialCtx, addr, creator, nil, []byte(`{}`)) + require.Error(t, err) + require.Contains(t, err.Error(), "Unauthorized") + + // verifier can execute, and get proper gas amount + start := time.Now() + gasBefore := ctx.GasMeter().GasConsumed() + + res, err = keeper.Execute(ctx, addr, fred, topUp, []byte(`{}`)) + diff := time.Now().Sub(start) + require.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, uint64(81891), res.GasUsed) + + // make sure gas is properly deducted from ctx + gasAfter := ctx.GasMeter().GasConsumed() + kvStoreGas := uint64(30321) // calculated by disabling contract gas reduction and running test + require.Equal(t, kvStoreGas+815, gasAfter-gasBefore) + + // ensure bob now exists and got both payments released + bobAcct = accKeeper.GetAccount(ctx, bob) + require.NotNil(t, bobAcct) + balance := bobAcct.GetCoins() + assert.Equal(t, deposit.Add(topUp), balance) + + // ensure contract has updated balance + contractAcct = accKeeper.GetAccount(ctx, addr) + require.NotNil(t, contractAcct) + assert.Equal(t, sdk.Coins(nil), contractAcct.GetCoins()) + + t.Logf("Duration: %v (81488 gas)\n", diff) +} + +type InitMsg struct { + Verifier string `json:"verifier"` + Beneficiary string `json:"beneficiary"` +} + +func createFakeFundedAccount(ctx sdk.Context, am auth.AccountKeeper, coins sdk.Coins) sdk.AccAddress { + _, _, addr := keyPubAddr() + baseAcct := auth.NewBaseAccountWithAddress(addr) + _ = baseAcct.SetCoins(coins) + am.SetAccount(ctx, &baseAcct) + + return addr +} + +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { + key := ed25519.GenPrivKey() + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + return key, pub, addr +} diff --git a/x/wasm/internal/keeper/querier.go b/x/wasm/internal/keeper/querier.go new file mode 100644 index 00000000..e96969ea --- /dev/null +++ b/x/wasm/internal/keeper/querier.go @@ -0,0 +1,136 @@ +package keeper + +import ( + "encoding/json" + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/modules/incubator/wasm/internal/types" +) + +const ( + QueryListContracts = "list-contracts" + QueryGetContract = "contract-info" + QueryGetContractState = "contract-state" + QueryGetCode = "code" + QueryListCode = "list-code" +) + +// NewQuerier creates a new querier +func NewQuerier(keeper Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, sdk.Error) { + switch path[0] { + case QueryGetContract: + return queryContractInfo(ctx, path[1], req, keeper) + case QueryListContracts: + return queryContractList(ctx, req, keeper) + case QueryGetContractState: + return queryContractState(ctx, path[1], req, keeper) + case QueryGetCode: + return queryCode(ctx, path[1], req, keeper) + case QueryListCode: + return queryCodeList(ctx, req, keeper) + default: + return nil, sdk.ErrUnknownRequest("unknown data query endpoint") + } + } +} + +func queryContractInfo(ctx sdk.Context, bech string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + addr, err := sdk.AccAddressFromBech32(bech) + if err != nil { + return nil, sdk.ErrUnknownRequest(err.Error()) + } + info := keeper.GetContractInfo(ctx, addr) + + bz, err := json.MarshalIndent(info, "", " ") + if err != nil { + return nil, sdk.ErrInvalidAddress(err.Error()) + } + return bz, nil +} + +func queryContractList(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var addrs []string + keeper.ListContractInfo(ctx, func(addr sdk.AccAddress, _ types.Contract) bool { + addrs = append(addrs, addr.String()) + return false + }) + bz, err := json.MarshalIndent(addrs, "", " ") + if err != nil { + return nil, sdk.ErrInvalidAddress(err.Error()) + } + return bz, nil +} + +type model struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func queryContractState(ctx sdk.Context, bech string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + addr, err := sdk.AccAddressFromBech32(bech) + if err != nil { + return nil, sdk.ErrUnknownRequest(err.Error()) + } + iter := keeper.GetContractState(ctx, addr) + + var state []model + for ; iter.Valid(); iter.Next() { + m := model{ + Key: string(iter.Key()), + Value: string(iter.Value()), + } + state = append(state, m) + } + + bz, err := json.MarshalIndent(state, "", " ") + if err != nil { + return nil, sdk.ErrUnknownRequest(err.Error()) + } + return bz, nil +} + +type wasmCode struct { + Code []byte `json:"code", yaml:"code"` +} + +func queryCode(ctx sdk.Context, codeIDstr string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + codeID, err := strconv.ParseUint(codeIDstr, 10, 64) + if err != nil { + return nil, sdk.ErrUnknownRequest("invalid codeID: " + err.Error()) + } + + code, err := keeper.GetByteCode(ctx, codeID) + if err != nil { + return nil, sdk.ErrUnknownRequest("loading wasm code: " + err.Error()) + } + + bz, err := json.MarshalIndent(wasmCode{code}, "", " ") + if err != nil { + return nil, sdk.ErrUnknownRequest(err.Error()) + } + return bz, nil +} + +func queryCodeList(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) { + var info []*types.CodeInfo + + i := uint64(1) + for true { + res := keeper.GetCodeInfo(ctx, i) + i++ + if res == nil { + break + } + info = append(info, res) + } + + bz, err := json.MarshalIndent(info, "", " ") + if err != nil { + return nil, sdk.ErrUnknownRequest(err.Error()) + } + return bz, nil +} diff --git a/x/wasm/internal/keeper/test_common.go b/x/wasm/internal/keeper/test_common.go new file mode 100644 index 00000000..40242c91 --- /dev/null +++ b/x/wasm/internal/keeper/test_common.go @@ -0,0 +1,75 @@ +package keeper + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" +) + +func MakeTestCodec() *codec.Codec { + var cdc = codec.New() + + // Register AppAccount + cdc.RegisterInterface((*authexported.Account)(nil), nil) + cdc.RegisterConcrete(&auth.BaseAccount{}, "test/wasm/BaseAccount", nil) + codec.RegisterCrypto(cdc) + + return cdc +} + +func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string) (sdk.Context, auth.AccountKeeper, Keeper) { + keyContract := sdk.NewKVStoreKey(types.StoreKey) + keyAcc := sdk.NewKVStoreKey(auth.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyContract, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + err := ms.LoadLatestVersion() + require.Nil(t, err) + + ctx := sdk.NewContext(ms, abci.Header{}, isCheckTx, log.NewNopLogger()) + cdc := MakeTestCodec() + + pk := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace) + + accountKeeper := auth.NewAccountKeeper( + cdc, // amino codec + keyAcc, // target store + pk.Subspace(auth.DefaultParamspace), + auth.ProtoBaseAccount, // prototype + ) + + bk := bank.NewBaseKeeper( + accountKeeper, + pk.Subspace(bank.DefaultParamspace), + bank.DefaultCodespace, + nil, + ) + bk.SetSendEnabled(ctx, true) + + // TODO: register more than bank.send + router := baseapp.NewRouter() + h := bank.NewHandler(bk) + router.AddRoute(bank.RouterKey, h) + + keeper := NewKeeper(cdc, keyContract, accountKeeper, bk, router, tempDir) + + return ctx, accountKeeper, keeper +} diff --git a/x/wasm/internal/keeper/testdata/contract.wasm b/x/wasm/internal/keeper/testdata/contract.wasm new file mode 100644 index 00000000..5af085eb Binary files /dev/null and b/x/wasm/internal/keeper/testdata/contract.wasm differ diff --git a/x/wasm/internal/types/codec.go b/x/wasm/internal/types/codec.go new file mode 100644 index 00000000..7a66883b --- /dev/null +++ b/x/wasm/internal/types/codec.go @@ -0,0 +1,23 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + // "github.com/cosmos/cosmos-sdk/x/supply/exported" +) + +// RegisterCodec registers the account types and interface +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(&MsgStoreCode{}, "wasm/store-code", nil) + cdc.RegisterConcrete(&MsgInstantiateContract{}, "wasm/instantiate", nil) + cdc.RegisterConcrete(&MsgExecuteContract{}, "wasm/execute", nil) +} + +// ModuleCdc generic sealed codec to be used throughout module +var ModuleCdc *codec.Codec + +func init() { + cdc := codec.New() + RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + ModuleCdc = cdc.Seal() +} diff --git a/x/wasm/internal/types/errors.go b/x/wasm/internal/types/errors.go new file mode 100644 index 00000000..0b79f2b5 --- /dev/null +++ b/x/wasm/internal/types/errors.go @@ -0,0 +1,43 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Codes for wasm contract errors +const ( + DefaultCodespace sdk.CodespaceType = ModuleName + + CodeCreatedFailed sdk.CodeType = 1 + CodeAccountExists sdk.CodeType = 2 + CodeInstantiateFailed sdk.CodeType = 3 + CodeExecuteFailed sdk.CodeType = 4 + CodeGasLimit sdk.CodeType = 5 +) + +// ErrCreateFailed error for wasm code that has already been uploaded or failed +func ErrCreateFailed(err error) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeCreatedFailed, fmt.Sprintf("create wasm contract failed: %s", err.Error())) +} + +// ErrAccountExists error for a contract account that already exists +func ErrAccountExists(addr sdk.AccAddress) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeAccountExists, fmt.Sprintf("contract account %s already exists", addr.String())) +} + +// ErrInstantiateFailed error for rust instantiate contract failure +func ErrInstantiateFailed(err error) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeInstantiateFailed, fmt.Sprintf("instantiate wasm contract failed: %s", err.Error())) +} + +// ErrExecuteFailed error for rust execution contract failure +func ErrExecuteFailed(err error) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeExecuteFailed, fmt.Sprintf("execute wasm contract failed: %s", err.Error())) +} + +// ErrGasLimit error for out of gas +func ErrGasLimit(msg string) sdk.Error { + return sdk.NewError(DefaultCodespace, CodeGasLimit, fmt.Sprintf("insufficient gas: %s", msg)) +} diff --git a/x/wasm/internal/types/keys.go b/x/wasm/internal/types/keys.go new file mode 100644 index 00000000..c7c628dd --- /dev/null +++ b/x/wasm/internal/types/keys.go @@ -0,0 +1,48 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // ModuleName is the name of the contract module + ModuleName = "wasm" + + // StoreKey is the string store representation + StoreKey = ModuleName + + // TStoreKey is the string transient store representation + TStoreKey = "transient_" + ModuleName + + // QuerierRoute is the querier route for the staking module + QuerierRoute = ModuleName + + // RouterKey is the msg router key for the staking module + RouterKey = ModuleName +) + +// nolint +var ( + KeyLastCodeID = []byte("lastCodeId") + KeyLastInstanceID = []byte("lastContractId") + + CodeKeyPrefix = []byte{0x01} + ContractKeyPrefix = []byte{0x02} + ContractStorePrefix = []byte{0x03} +) + +// GetCodeKey constructs the key for retreiving the ID for the WASM code +func GetCodeKey(contractID uint64) []byte { + contractIDBz := sdk.Uint64ToBigEndian(contractID) + return append(CodeKeyPrefix, contractIDBz...) +} + +// GetContractAddressKey returns the key for the WASM contract instance +func GetContractAddressKey(addr sdk.AccAddress) []byte { + return append(ContractKeyPrefix, addr...) +} + +// GetContractStorePrefixKey returns the store prefix for the WASM contract instance +func GetContractStorePrefixKey(addr sdk.AccAddress) []byte { + return append(ContractStorePrefix, addr...) +} diff --git a/x/wasm/internal/types/msg.go b/x/wasm/internal/types/msg.go new file mode 100644 index 00000000..2859765f --- /dev/null +++ b/x/wasm/internal/types/msg.go @@ -0,0 +1,100 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + MaxWasmSize = 500 * 1024 +) + +type MsgStoreCode struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + WASMByteCode []byte `json:"wasm_byte_code" yaml:"wasm_byte_code"` +} + +func (msg MsgStoreCode) Route() string { + return RouterKey +} + +func (msg MsgStoreCode) Type() string { + return "store-code" +} + +func (msg MsgStoreCode) ValidateBasic() sdk.Error { + if len(msg.WASMByteCode) == 0 { + return sdk.ErrInternal("empty wasm code") + } + if len(msg.WASMByteCode) > MaxWasmSize { + return sdk.ErrInternal("wasm code too large") + } + return nil +} + +func (msg MsgStoreCode) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgStoreCode) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgInstantiateContract struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Code uint64 `json:"code_id" yaml:"code_id"` + InitMsg []byte `json:"init_msg" yaml:"init_msg"` + InitFunds sdk.Coins `json:"init_funds" yaml:"init_funds"` +} + +func (msg MsgInstantiateContract) Route() string { + return RouterKey +} + +func (msg MsgInstantiateContract) Type() string { + return "instantiate" +} + +func (msg MsgInstantiateContract) ValidateBasic() sdk.Error { + if msg.InitFunds.IsAnyNegative() { + return sdk.ErrInvalidCoins("negative InitFunds") + } + return nil +} + +func (msg MsgInstantiateContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgInstantiateContract) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgExecuteContract struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + Msg []byte `json:"msg" yaml:"msg"` + SentFunds sdk.Coins `json:"sent_funds" yaml:"sent_funds"` +} + +func (msg MsgExecuteContract) Route() string { + return RouterKey +} + +func (msg MsgExecuteContract) Type() string { + return "execute" +} + +func (msg MsgExecuteContract) ValidateBasic() sdk.Error { + if msg.SentFunds.IsAnyNegative() { + return sdk.ErrInvalidCoins("negative SentFunds") + } + return nil +} + +func (msg MsgExecuteContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgExecuteContract) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} diff --git a/x/wasm/internal/types/types.go b/x/wasm/internal/types/types.go new file mode 100644 index 00000000..43283d64 --- /dev/null +++ b/x/wasm/internal/types/types.go @@ -0,0 +1,80 @@ +package types + +import ( + wasmTypes "github.com/confio/go-cosmwasm/types" + "github.com/cosmos/cosmos-sdk/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +// CodeInfo is data for the uploaded contract WASM code +type CodeInfo struct { + CodeHash []byte `json:"code_hash"` + Creator sdk.AccAddress `json:"creator"` +} + +// NewCodeInfo fills a new Contract struct +func NewCodeInfo(codeHash []byte, creator sdk.AccAddress) CodeInfo { + return CodeInfo{ + CodeHash: codeHash, + Creator: creator, + } +} + +// Contract stores a WASM contract instance +type Contract struct { + CodeID uint64 `json:"code_id"` + Creator sdk.AccAddress `json:"creator"` + InitMsg []byte `json:"init_msg"` + PrefixStore prefix.Store `json:"prefix_store"` +} + +// NewParams initializes params for a contract instance +func NewParams(ctx sdk.Context, creator sdk.AccAddress, deposit sdk.Coins, contractAcct auth.Account) wasmTypes.Params { + return wasmTypes.Params{ + Block: wasmTypes.BlockInfo{ + Height: ctx.BlockHeight(), + Time: ctx.BlockTime().Unix(), + ChainID: ctx.ChainID(), + }, + Message: wasmTypes.MessageInfo{ + Signer: creator.String(), + SentFunds: NewWasmCoins(deposit), + }, + Contract: wasmTypes.ContractInfo{ + Address: contractAcct.GetAddress().String(), + Balance: NewWasmCoins(contractAcct.GetCoins()), + }, + } +} + +// NewWasmCoins translates between Cosmos SDK coins and Wasm coins +func NewWasmCoins(cosmosCoins sdk.Coins) (wasmCoins []wasmTypes.Coin) { + for _, coin := range cosmosCoins { + wasmCoin := wasmTypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + } + wasmCoins = append(wasmCoins, wasmCoin) + } + return wasmCoins +} + +// NewContract creates a new instance of a given WASM contract +func NewContract(codeID uint64, creator sdk.AccAddress, initMsg []byte, prefixStore prefix.Store) Contract { + return Contract{ + CodeID: codeID, + Creator: creator, + InitMsg: initMsg, + PrefixStore: prefixStore, + } +} + +// CosmosResult converts from a Wasm Result type +func CosmosResult(wasmResult wasmTypes.Result) sdk.Result { + return sdk.Result{ + Data: []byte(wasmResult.Data), + Log: wasmResult.Log, + GasUsed: wasmResult.GasUsed, + } +} diff --git a/x/wasm/module.go b/x/wasm/module.go new file mode 100644 index 00000000..905e9183 --- /dev/null +++ b/x/wasm/module.go @@ -0,0 +1,136 @@ +package wasm + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + // "github.com/cosmos/modules/incubator/wasm/client/cli" + // "github.com/cosmos/modules/incubator/wasm/client/rest" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the wasm module. +type AppModuleBasic struct{} + +// Name returns the wasm module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the wasm module's types for the given codec. +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis returns default genesis state as raw bytes for the wasm +// module. +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(&GenesisState{}) +} + +// ValidateGenesis performs genesis state validation for the wasm module. +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// RegisterRESTRoutes registers the REST routes for the wasm module. +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + // TODO + // rest.RegisterRoutes(ctx, rtr) +} + +// GetTxCmd returns the root tx command for the wasm module. +func (AppModuleBasic) GetTxCmd(_ *codec.Codec) *cobra.Command { return nil } + +// GetQueryCmd returns no root query command for the wasm module. +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + // TODO + // return cli.GetQueryCmd(cdc) + return nil +} + +//____________________________________________________________________________ + +// AppModule implements an application module for the wasm module. +type AppModule struct { + AppModuleBasic + keeper Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// Name returns the wasm module's name. +func (AppModule) Name() string { + return ModuleName +} + +// RegisterInvariants registers the wasm module invariants. +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} + +// Route returns the message routing key for the wasm module. +func (AppModule) Route() string { + return RouterKey +} + +// NewHandler returns an sdk.Handler for the wasm module. +func (am AppModule) NewHandler() sdk.Handler { + return NewHandler(am.keeper) +} + +// QuerierRoute returns the wasm module's querier route name. +func (AppModule) QuerierRoute() string { + return QuerierRoute +} + +// NewQuerierHandler returns the wasm module sdk.Querier. +func (am AppModule) NewQuerierHandler() sdk.Querier { + return NewQuerier(am.keeper) +} + +// InitGenesis performs genesis initialization for the wasm module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the wasm +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// BeginBlock returns the begin blocker for the wasm module. +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock returns the end blocker for the wasm module. It returns no validator +// updates. +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/wasm/module_test.go b/x/wasm/module_test.go new file mode 100644 index 00000000..310e2784 --- /dev/null +++ b/x/wasm/module_test.go @@ -0,0 +1,352 @@ +package wasm + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +type testData struct { + module module.AppModule + ctx sdk.Context + acctKeeper auth.AccountKeeper +} + +// returns a cleanup function, which must be defered on +func setupTest(t *testing.T) (testData, func()) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + + ctx, acctKeeper, keeper := CreateTestInput(t, false, tempDir) + data := testData{ + module: NewAppModule(keeper), + ctx: ctx, + acctKeeper: acctKeeper, + } + cleanup := func() { os.RemoveAll(tempDir) } + return data, cleanup +} + +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { + key := ed25519.GenPrivKey() + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + return key, pub, addr +} + +func mustLoad(path string) []byte { + bz, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + return bz +} + +var ( + key1, pub1, addr1 = keyPubAddr() + testContract = mustLoad("./internal/keeper/testdata/contract.wasm") +) + +func TestHandleCreate(t *testing.T) { + cases := map[string]struct { + msg sdk.Msg + isValid bool + }{ + "empty": { + msg: MsgStoreCode{}, + isValid: false, + }, + "invalid wasm": { + msg: MsgStoreCode{ + Sender: addr1, + WASMByteCode: []byte("foobar"), + }, + isValid: false, + }, + "valid wasm": { + msg: MsgStoreCode{ + Sender: addr1, + WASMByteCode: testContract, + }, + isValid: true, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + res := h(data.ctx, tc.msg) + if !tc.isValid { + require.False(t, res.IsOK(), "%#v", res) + assertCodeList(t, q, data.ctx, 0) + assertCodeBytes(t, q, data.ctx, 1, nil) + return + } + require.True(t, res.IsOK(), "%#v", res) + assertCodeList(t, q, data.ctx, 1) + assertCodeBytes(t, q, data.ctx, 1, testContract) + }) + } +} + +type initMsg struct { + Verifier string `json:"verifier"` + Beneficiary string `json:"beneficiary"` +} + +type state struct { + Verifier string `json:"verifier"` + Beneficiary string `json:"beneficiary"` + Funder string `json:"funder"` +} + +func TestHandleInstantiate(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(data.ctx, data.acctKeeper, deposit) + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + msg := MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + } + res := h(data.ctx, msg) + require.True(t, res.IsOK()) + require.Equal(t, res.Data, []byte("1")) + + initMsg := initMsg{ + Verifier: "fred", + Beneficiary: "bob", + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + // create with no balance is also legal + initCmd := MsgInstantiateContract{ + Sender: creator, + Code: 1, + InitMsg: initMsgBz, + InitFunds: nil, + } + res = h(data.ctx, initCmd) + require.True(t, res.IsOK()) + contractAddr := sdk.AccAddress(res.Data) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + + assertCodeList(t, q, data.ctx, 1) + assertCodeBytes(t, q, data.ctx, 1, testContract) + + assertContractList(t, q, data.ctx, []string{contractAddr.String()}) + assertContractInfo(t, q, data.ctx, contractAddr, 1, creator) + assertContractState(t, q, data.ctx, contractAddr, state{ + Verifier: "fred", + Beneficiary: "bob", + Funder: creator.String(), + }) +} + +func TestHandleExecute(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(data.ctx, data.acctKeeper, deposit.Add(deposit)) + fred := createFakeFundedAccount(data.ctx, data.acctKeeper, topUp) + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + msg := MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + } + res := h(data.ctx, msg) + require.True(t, res.IsOK()) + require.Equal(t, res.Data, []byte("1")) + + _, _, bob := keyPubAddr() + initMsg := initMsg{ + Verifier: fred.String(), + Beneficiary: bob.String(), + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator, + Code: 1, + InitMsg: initMsgBz, + InitFunds: deposit, + } + res = h(data.ctx, initCmd) + require.True(t, res.IsOK()) + contractAddr := sdk.AccAddress(res.Data) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + + // ensure bob doesn't exist + bobAcct := data.acctKeeper.GetAccount(data.ctx, bob) + require.Nil(t, bobAcct) + + // ensure funder has reduced balance + creatorAcct := data.acctKeeper.GetAccount(data.ctx, creator) + require.NotNil(t, creatorAcct) + // we started at 2*deposit, should have spent one above + assert.Equal(t, deposit, creatorAcct.GetCoins()) + + // ensure contract has updated balance + contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, deposit, contractAcct.GetCoins()) + + execCmd := MsgExecuteContract{ + Sender: fred, + Contract: contractAddr, + Msg: []byte("{}"), + SentFunds: topUp, + } + res = h(data.ctx, execCmd) + require.True(t, res.IsOK()) + + // ensure bob now exists and got both payments released + bobAcct = data.acctKeeper.GetAccount(data.ctx, bob) + require.NotNil(t, bobAcct) + balance := bobAcct.GetCoins() + assert.Equal(t, deposit.Add(topUp), balance) + + // ensure contract has updated balance + contractAcct = data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, sdk.Coins(nil), contractAcct.GetCoins()) + + // ensure all contract state is as after init + assertCodeList(t, q, data.ctx, 1) + assertCodeBytes(t, q, data.ctx, 1, testContract) + + assertContractList(t, q, data.ctx, []string{contractAddr.String()}) + assertContractInfo(t, q, data.ctx, contractAddr, 1, creator) + assertContractState(t, q, data.ctx, contractAddr, state{ + Verifier: fred.String(), + Beneficiary: bob.String(), + Funder: creator.String(), + }) +} + +func assertCodeList(t *testing.T, q sdk.Querier, ctx sdk.Context, expectedNum int) { + bz, sdkerr := q(ctx, []string{QueryListCode}, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, expectedNum, 0) + return + } + + var res []CodeInfo + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + assert.Equal(t, expectedNum, len(res)) +} + +type wasmCode struct { + Code []byte `json:"code", yaml:"code"` +} + +func assertCodeBytes(t *testing.T, q sdk.Querier, ctx sdk.Context, codeID uint64, expectedBytes []byte) { + path := []string{QueryGetCode, fmt.Sprintf("%d", codeID)} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, len(expectedBytes), 0) + return + } + + var res wasmCode + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + assert.Equal(t, expectedBytes, res.Code) +} + +func assertContractList(t *testing.T, q sdk.Querier, ctx sdk.Context, addrs []string) { + bz, sdkerr := q(ctx, []string{QueryListContracts}, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, len(addrs), 0) + return + } + + var res []string + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + assert.Equal(t, addrs, res) +} + +type model struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func assertContractState(t *testing.T, q sdk.Querier, ctx sdk.Context, addr sdk.AccAddress, expected state) { + path := []string{QueryGetContractState, addr.String()} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + var res []model + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + require.Equal(t, 1, len(res), "#v", res) + require.Equal(t, "config", res[0].Key) + + expectedBz, err := json.Marshal(expected) + require.NoError(t, err) + assert.Equal(t, string(expectedBz), res[0].Value) +} + +func assertContractInfo(t *testing.T, q sdk.Querier, ctx sdk.Context, addr sdk.AccAddress, codeID uint64, creator sdk.AccAddress) { + path := []string{QueryGetContract, addr.String()} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + var res Contract + err := json.Unmarshal(bz, &res) + require.NoError(t, err) + + assert.Equal(t, codeID, res.CodeID) + assert.Equal(t, creator, res.Creator) +} + +func createFakeFundedAccount(ctx sdk.Context, am auth.AccountKeeper, coins sdk.Coins) sdk.AccAddress { + _, _, addr := keyPubAddr() + baseAcct := auth.NewBaseAccountWithAddress(addr) + _ = baseAcct.SetCoins(coins) + am.SetAccount(ctx, &baseAcct) + + return addr +}